diff --git a/.claude/rules/documentation.md b/.claude/rules/documentation.md index 1e56c22..24ea8e1 100644 --- a/.claude/rules/documentation.md +++ b/.claude/rules/documentation.md @@ -1,8 +1,8 @@ --- paths: - - '**/*.md' - - '!CLAUDE.md' - - '!.claude/**' + - "**/*.md" + - "!CLAUDE.md" + - "!.claude/**" --- # Documentation Rules @@ -20,11 +20,11 @@ See the full standards: [Writing](../../../docs/contributing/standards/documenta Use the correct template based on document type. See [Writing Standards](../../../docs/contributing/standards/documentation/writing.md) for full templates: -| Type | Structure | Title Convention | -| --------------- | ------------------------------------------------------ | ---------------------- | -| Standard | Overview > Rules > Resources > References | Noun phrase | -| Guide | Prerequisites > Steps > Verification > Troubleshooting | Starts with verb | -| Overview | Architecture > Key Concepts > Usage > Configuration | Topic name | +| Type | Structure | Title Convention | +| --------------- | ------------------------------------------------------ | ------------------------ | +| Standard | Overview > Rules > Resources > References | Noun phrase | +| Guide | Prerequisites > Steps > Verification > Troubleshooting | Starts with verb | +| Overview | Architecture > Key Concepts > Usage > Configuration | Topic name | | Troubleshooting | Issue sections with Fix blocks | "Domain Troubleshooting" | ## Sections @@ -36,7 +36,7 @@ Use the correct template based on document type. See [Writing Standards](../../. ## Formatting - Code examples: minimal snippets showing the pattern, not full files -- Specify language on all fenced code blocks (```ts, ```bash, etc.) +- Specify language on all fenced code blocks (`ts`, `bash`, etc.) - Use tables for structured comparisons (Correct vs Incorrect, options, conventions) - Prefer relative links for internal docs, full URLs for external diff --git a/.claude/rules/errors.md b/.claude/rules/errors.md index 942656a..7d65da4 100644 --- a/.claude/rules/errors.md +++ b/.claude/rules/errors.md @@ -1,6 +1,6 @@ --- paths: - - 'scripts/**/*.ts' + - "scripts/**/*.ts" --- # Error Handling Rules @@ -10,7 +10,7 @@ All expected failures use the `Result` tuple type. Never throw exceptions. ## Result Type ```ts -type Result = readonly [E, null] | readonly [null, T] +type Result = readonly [E, null] | readonly [null, T]; ``` - Success: `[null, value]` @@ -25,21 +25,21 @@ type Result = readonly [E, null] | readonly [null, T] ## Chaining with Early Returns ```ts -const [configError, config] = loadConfig(workspace) -if (configError) return [configError, null] +const [configError, config] = loadConfig(workspace); +if (configError) return [configError, null]; -const [resolveError, result] = resolve(config, name) -if (resolveError) return [resolveError, null] +const [resolveError, result] = resolve(config, name); +if (resolveError) return [resolveError, null]; -return [null, result] +return [null, result]; ``` ## Error Type Pattern ```ts interface ConfigError { - readonly type: 'invalid_json' | 'missing_field' | 'unknown_key' - readonly message: string + readonly type: "invalid_json" | "missing_field" | "unknown_key"; + readonly message: string; } ``` diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 078a58c..ec596b7 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -1,6 +1,6 @@ --- paths: - - '**/*.test.ts' + - "**/*.test.ts" --- # Testing Rules @@ -15,14 +15,14 @@ All tests use [Vitest](https://vitest.dev). Follow these conventions in every te - One assertion focus per test case ```ts -import { describe, it, expect } from 'vitest' - -describe('resolveScript', () => { - it('should resolve path relative to workspace root', () => { - const result = resolveScriptPath('build', '/project') - expect(result).toBe('/project/scripts/build.ts') - }) -}) +import { describe, it, expect } from "vitest"; + +describe("resolveScript", () => { + it("should resolve path relative to workspace root", () => { + const result = resolveScriptPath("build", "/project"); + expect(result).toBe("/project/scripts/build.ts"); + }); +}); ``` ## Mocking @@ -45,10 +45,10 @@ describe('resolveScript', () => { - Use `toMatchObject` for partial matching on error shapes ```ts -it('should return error result for missing config', async () => { - const [error] = await loadConfig('/missing') - expect(error).toMatchObject({ type: 'parse_error' }) -}) +it("should return error result for missing config", async () => { + const [error] = await loadConfig("/missing"); + expect(error).toMatchObject({ type: "parse_error" }); +}); ``` ## Coverage Targets diff --git a/.claude/rules/typescript.md b/.claude/rules/typescript.md index 19e8a7e..779102c 100644 --- a/.claude/rules/typescript.md +++ b/.claude/rules/typescript.md @@ -1,6 +1,6 @@ --- paths: - - 'scripts/**/*.ts' + - "scripts/**/*.ts" --- # TypeScript Rules @@ -47,6 +47,7 @@ These rules are enforced by OXLint and must be followed in all TypeScript files. > Full standard: [Coding Style](../../../docs/contributing/standards/typescript/coding-style.md) | [Naming](../../../docs/contributing/standards/typescript/naming.md) Every source file follows this order: + 1. **Imports** — node builtins, blank line, external packages, blank line, internal (farthest-to-closest, alphabetical). Top-level `import type`, no inline type specifiers. 2. **Module-level constants** 3. **Exported functions** — public API first, each with full JSDoc. See [Functions](../../../docs/contributing/standards/typescript/functions.md). @@ -68,5 +69,5 @@ Every source file follows this order: ## Formatting (OXFmt) -- 100-char line width, 2-space indent, semicolons, single quotes, trailing commas +- 100-char line width, 2-space indent, semicolons, double quotes, trailing commas - Import sorting: builtin > external > internal > parent/sibling/index diff --git a/.claude/settings.json b/.claude/settings.json index 172edeb..28d099a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,4 +2,4 @@ "enabledPlugins": { "plugin-dev@claude-plugins-official": true } -} \ No newline at end of file +} diff --git a/.claude/skills/feature/SKILL.md b/.claude/skills/feature/SKILL.md index 6359d23..c3960cf 100644 --- a/.claude/skills/feature/SKILL.md +++ b/.claude/skills/feature/SKILL.md @@ -17,15 +17,15 @@ Read `scripts/conf/project.json` at the repository root to get the current list Collect the following from the user before proceeding: -| Field | Required | Notes | -|-------|----------|-------| -| Title | Yes | Human-readable feature name | -| Status | Yes | ! One of the statuses from `scripts/conf/project.json` | -| Summary | Yes | One-sentence description of the feature | -| Problem | Yes | What limitation or gap this addresses | -| Solution | Yes | How the feature solves the problem | -| Impact | Yes | What users gain from this feature | -| GitHub Issue | No | Whether to search for / create a GitHub issue | +| Field | Required | Notes | +| ------------ | -------- | ------------------------------------------------------ | +| Title | Yes | Human-readable feature name | +| Status | Yes | ! One of the statuses from `scripts/conf/project.json` | +| Summary | Yes | One-sentence description of the feature | +| Problem | Yes | What limitation or gap this addresses | +| Solution | Yes | How the feature solves the problem | +| Impact | Yes | What users gain from this feature | +| GitHub Issue | No | Whether to search for / create a GitHub issue | Use `AskUserQuestion` to collect any missing fields. All six content fields (Title, Summary, Problem, Solution, Impact, and Status) are required before creating the file. @@ -87,12 +87,13 @@ issue: The status badge colors follow a darkwave/synthwave palette: -| Status | Color | Badge | -|--------|-------|-------| -| Idea | Electric Purple | `![Idea](https://img.shields.io/badge/Idea-%238a04ed)` | -| Planned | Imperial Blue | `![Planned](https://img.shields.io/badge/Planned-%230C1565)` | -| In progress | Burnt Orange | `![In progress](https://img.shields.io/badge/In%20progress-%23e85d04)` | -| Released | Dark Teal | `![Released](https://img.shields.io/badge/Released-%2300a67e)` | +| Status | Color | Badge | +| ----------- | --------------- | ---------------------------------------------------------------------- | +| Idea | Electric Purple | `![Idea](https://img.shields.io/badge/Idea-%238a04ed)` | +| Planned | Imperial Blue | `![Planned](https://img.shields.io/badge/Planned-%230C1565)` | +| Upcoming | Imperial Blue | `![Upcoming](https://img.shields.io/badge/Upcoming-%230C1565)` | +| In progress | Burnt Orange | `![In progress](https://img.shields.io/badge/In%20progress-%23e85d04)` | +| Released | Dark Teal | `![Released](https://img.shields.io/badge/Released-%2300a67e)` | ### Step 6: Update Overview Table diff --git a/.claude/skills/feature/references/feature-template.md b/.claude/skills/feature/references/feature-template.md index efb34b2..9ae3700 100644 --- a/.claude/skills/feature/references/feature-template.md +++ b/.claude/skills/feature/references/feature-template.md @@ -29,10 +29,10 @@ issue: Every feature file starts with YAML frontmatter containing: -| Field | Required | Description | -|-------|----------|-------------| -| `status` | Yes | Current status — must match a value from `scripts/conf/project.json` | -| `issue` | No | GitHub issue number (leave empty if none) | +| Field | Required | Description | +| -------- | -------- | -------------------------------------------------------------------- | +| `status` | Yes | Current status — must match a value from `scripts/conf/project.json` | +| `issue` | No | GitHub issue number (leave empty if none) | ## Status Badges @@ -96,13 +96,13 @@ Convert the feature title to kebab-case: Examples: -| Title | Filename | -|-------|----------| -| Agent Harness | `agent-harness.md` | -| Coding Agent Setup Doctor | `coding-agent-setup-doctor.md` | -| GG Workflow | `gg-workflow.md` | -| Coding Agent Toolkit MCP (Serena) | `coding-agent-toolkit-mcp.md` | -| Secure Data Access Layer (unmcp) | `secure-data-access-layer.md` | +| Title | Filename | +| --------------------------------- | ------------------------------ | +| Agent Harness | `agent-harness.md` | +| Coding Agent Setup Doctor | `coding-agent-setup-doctor.md` | +| GG Workflow | `gg-workflow.md` | +| Coding Agent Toolkit MCP (Serena) | `coding-agent-toolkit-mcp.md` | +| Secure Data Access Layer (unmcp) | `secure-data-access-layer.md` | ## Overview Table Row Format @@ -111,6 +111,7 @@ Examples: ``` Where `` is either: + - `[#N](https://github.com/joggrdocs/home/issues/N)` — linked GitHub issue - `-` — no issue yet diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml index 3ca50b0..7acdc7e 100644 --- a/.github/DISCUSSION_TEMPLATE/ideas.yml +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -1,4 +1,4 @@ -title: 'Ideas and Feature Requests' +title: "Ideas and Feature Requests" labels: [enhancement] body: - type: textarea diff --git a/.github/DISCUSSION_TEMPLATE/q-and-a.yml b/.github/DISCUSSION_TEMPLATE/q-and-a.yml index 7937b9b..8348fe6 100644 --- a/.github/DISCUSSION_TEMPLATE/q-and-a.yml +++ b/.github/DISCUSSION_TEMPLATE/q-and-a.yml @@ -1,4 +1,4 @@ -title: 'Questions and Support' +title: "Questions and Support" labels: [question] body: - type: textarea diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml index 7ebdc8b..55a6a88 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yml +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -16,7 +16,7 @@ body: attributes: label: Location description: Where is the documentation that needs improvement? - placeholder: 'e.g., README.md, docs/getting-started.md, or a URL' + placeholder: "e.g., README.md, docs/getting-started.md, or a URL" validations: required: false diff --git a/.github/labeler.yaml b/.github/labeler.yaml index 7ad609f..67ad54f 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -4,49 +4,49 @@ documentation: - changed-files: - any-glob-to-any-file: - - 'docs/**' - - '**/*.md' - - '.github/ISSUE_TEMPLATE/**' - - '.github/DISCUSSION_TEMPLATE/**' + - "docs/**" + - "**/*.md" + - ".github/ISSUE_TEMPLATE/**" + - ".github/DISCUSSION_TEMPLATE/**" bug: - changed-files: - any-glob-to-any-file: - - '**/*.patch' + - "**/*.patch" -'product:cli': +"product:cli": - changed-files: - any-glob-to-any-file: - - 'cli/**' - - 'packages/cli/**' + - "cli/**" + - "packages/cli/**" -'product:sdk': +"product:sdk": - changed-files: - any-glob-to-any-file: - - 'sdk/**' - - 'packages/sdk/**' + - "sdk/**" + - "packages/sdk/**" -'product:api': +"product:api": - changed-files: - any-glob-to-any-file: - - 'api/**' - - 'packages/api/**' + - "api/**" + - "packages/api/**" -'product:mcp': +"product:mcp": - changed-files: - any-glob-to-any-file: - - 'mcp/**' - - 'packages/mcp/**' + - "mcp/**" + - "packages/mcp/**" -'product:integrations': +"product:integrations": - changed-files: - any-glob-to-any-file: - - 'integrations/**' - - 'packages/integrations/**' + - "integrations/**" + - "packages/integrations/**" -'product:console': +"product:console": - changed-files: - any-glob-to-any-file: - - 'console/**' - - 'packages/console/**' - - 'apps/console/**' + - "console/**" + - "packages/console/**" + - "apps/console/**" diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index efbe852..fbcc28e 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ name: stale on: schedule: # Run daily at midnight UTC - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: permissions: @@ -26,8 +26,8 @@ jobs: with: days-before-stale: 60 days-before-close: 14 - stale-issue-label: 'stale' - stale-pr-label: 'stale' + stale-issue-label: "stale" + stale-pr-label: "stale" stale-issue-message: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further @@ -36,5 +36,5 @@ jobs: This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. - exempt-issue-labels: 'status:accepted,status:in-progress,priority:critical,priority:high' - exempt-pr-labels: 'status:in-progress,priority:critical,priority:high' + exempt-issue-labels: "status:accepted,status:in-progress,priority:critical,priority:high" + exempt-pr-labels: "status:in-progress,priority:critical,priority:high" diff --git a/README.md b/README.md index 2246e00..45f88b3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ [![Roadmap](https://img.shields.io/badge/Roadmap-8B5CF6?style=for-the-badge&logo=googlemaps&logoColor=white)](https://github.com/orgs/joggrdocs/projects/9) [![Issues](https://img.shields.io/badge/Issues-8B5CF6?style=for-the-badge&logo=github&logoColor=white)](https://github.com/joggrdocs/home/issues) + @@ -26,11 +27,13 @@ Build the perfect AI agent setup — automatically. Joggr creates custom coding What we're actively working on right now: -| Feature | Status | Assignee | -| ------- | ------ | -------- | -| [Agent Harness](https://github.com/orgs/joggrdocs/projects/9/views/3?pane=issue&itemId=PVTI_lADOAyJs4c4BQ0KszgmsstQ&issue=joggrdocs%7Chome%7C2) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | [![@zrosenbauer](https://img.shields.io/badge/%40zrosenbauer-black?style=flat-square&logo=github)](https://github.com/zrosenbauer) | -| [Coding Agent Setup CLI](https://github.com/orgs/joggrdocs/projects/9/views/3?pane=issue&itemId=PVTI_lADOAyJs4c4BQ0Kszgms50A&issue=joggrdocs%7Chome%7C6) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | [![@srosenbauer](https://img.shields.io/badge/%40srosenbauer-black?style=flat-square&logo=github)](https://github.com/srosenbauer) | + +| Feature | Status | Assignee | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [Agent Harness](https://github.com/orgs/joggrdocs/projects/9/views/3?pane=issue&itemId=PVTI_lADOAyJs4c4BQ0KszgmsstQ&issue=joggrdocs%7Chome%7C2) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | [![@zrosenbauer](https://img.shields.io/badge/%40zrosenbauer-black?style=flat-square&logo=github)](https://github.com/zrosenbauer) | +| [Coding Agent Setup CLI](https://github.com/orgs/joggrdocs/projects/9/views/3?pane=issue&itemId=PVTI_lADOAyJs4c4BQ0Kszgms50A&issue=joggrdocs%7Chome%7C6) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | [![@srosenbauer](https://img.shields.io/badge/%40srosenbauer-black?style=flat-square&logo=github)](https://github.com/srosenbauer) | | [Coding Agent Setup Remediation](https://github.com/orgs/joggrdocs/projects/9/views/3?pane=issue&itemId=PVTI_lADOAyJs4c4BQ0Kszgms53o&issue=joggrdocs%7Chome%7C9) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | [![@zrosenbauer](https://img.shields.io/badge/%40zrosenbauer-black?style=flat-square&logo=github)](https://github.com/zrosenbauer) | + **[View full roadmap →](https://github.com/orgs/joggrdocs/projects/9)** diff --git a/commitlint.config.ts b/commitlint.config.ts index 58388e4..4d94f7f 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -1,13 +1,13 @@ -import conventions from './.config/commit-conventions.json' with { type: 'json' } +import conventions from "./.config/commit-conventions.json" with { type: "json" }; export default { - extends: ['@commitlint/config-conventional'], + extends: ["@commitlint/config-conventional"], rules: { - 'type-enum': [2, 'always', conventions.types], - 'scope-enum': [2, 'always', conventions.scopes], - 'scope-case': [2, 'always', 'kebab-case'], - 'subject-case': [2, 'always', 'lower-case'], - 'subject-max-length': [2, 'always', 72], - 'header-max-length': [2, 'always', 100], + "type-enum": [2, "always", conventions.types], + "scope-enum": [2, "always", conventions.scopes], + "scope-case": [2, "always", "kebab-case"], + "subject-case": [2, "always", "lower-case"], + "subject-max-length": [2, "always", 72], + "header-max-length": [2, "always", 100], }, -} +}; diff --git a/docs/contributing/concepts/project-management.md b/docs/contributing/concepts/project-management.md index 1b1a56b..e347cee 100644 --- a/docs/contributing/concepts/project-management.md +++ b/docs/contributing/concepts/project-management.md @@ -109,40 +109,40 @@ We use [shields.io](https://shields.io) badges throughout the documentation and Status badges indicate the current state of features in the roadmap. Use `flat-square` style for tables. Colors follow a darkwave/synthwave palette for high contrast in dark mode. -| Status | Color | Badge | Code | -|--------|-------|-------|------| -| Released | Dark Teal (`00a67e`) | ![Released](https://img.shields.io/badge/Released-00a67e?style=flat-square) | `![Released](https://img.shields.io/badge/Released-00a67e?style=flat-square)` | -| In Progress | Burnt Orange (`e85d04`) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | `![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square)` | -| Planned | Imperial Blue (`0C1565`) | ![Planned](https://img.shields.io/badge/Planned-0C1565?style=flat-square) | `![Planned](https://img.shields.io/badge/Planned-0C1565?style=flat-square)` | -| Upcoming | Imperial Blue (`0C1565`) | ![Upcoming](https://img.shields.io/badge/Upcoming-0C1565?style=flat-square) | `![Upcoming](https://img.shields.io/badge/Upcoming-0C1565?style=flat-square)` | -| Idea | Electric Purple (`8a04ed`) | ![Idea](https://img.shields.io/badge/Idea-8a04ed?style=flat-square) | `![Idea](https://img.shields.io/badge/Idea-8a04ed?style=flat-square)` | +| Status | Color | Badge | Code | +| ----------- | -------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Released | Dark Teal (`00a67e`) | ![Released](https://img.shields.io/badge/Released-00a67e?style=flat-square) | `![Released](https://img.shields.io/badge/Released-00a67e?style=flat-square)` | +| In Progress | Burnt Orange (`e85d04`) | ![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square) | `![In Progress](https://img.shields.io/badge/In%20Progress-e85d04?style=flat-square)` | +| Planned | Imperial Blue (`0C1565`) | ![Planned](https://img.shields.io/badge/Planned-0C1565?style=flat-square) | `![Planned](https://img.shields.io/badge/Planned-0C1565?style=flat-square)` | +| Upcoming | Imperial Blue (`0C1565`) | ![Upcoming](https://img.shields.io/badge/Upcoming-0C1565?style=flat-square) | `![Upcoming](https://img.shields.io/badge/Upcoming-0C1565?style=flat-square)` | +| Idea | Electric Purple (`8a04ed`) | ![Idea](https://img.shields.io/badge/Idea-8a04ed?style=flat-square) | `![Idea](https://img.shields.io/badge/Idea-8a04ed?style=flat-square)` | ### Action Badges Action badges link to project views, discussions, or other interactive elements. Always use links with these badges. Uses brand purple (`8B5CF6`). -| Type | Color | Badge | Code | -|------|-------|-------|------| -| View | Purple (`8B5CF6`) | [![View](https://img.shields.io/badge/View-8B5CF6?style=flat-square)](https://github.com) | `[![View](https://img.shields.io/badge/View-8B5CF6?style=flat-square)](URL)` | +| Type | Color | Badge | Code | +| ------- | ----------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| View | Purple (`8B5CF6`) | [![View](https://img.shields.io/badge/View-8B5CF6?style=flat-square)](https://github.com) | `[![View](https://img.shields.io/badge/View-8B5CF6?style=flat-square)](URL)` | | Discuss | Purple (`8B5CF6`) | [![Discuss](https://img.shields.io/badge/Discuss-8B5CF6?style=flat-square)](https://github.com) | `[![Discuss](https://img.shields.io/badge/Discuss-8B5CF6?style=flat-square)](URL)` | ### Profile Badges GitHub profile badges display contributor avatars. Generated automatically by the `readme` script. -| Style | Badge | Code | -|-------|-------|------| +| Style | Badge | Code | +| ------ | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Simple | [![@zrosenbauer](https://img.shields.io/badge/%40zrosenbauer-black?style=flat-square&logo=github)](https://github.com/zrosenbauer) | `[![@username](https://img.shields.io/badge/%40username-black?style=flat-square&logo=github)](https://github.com/username)` | ### Header Badges Header badges appear at the top of the README with logos. Use `for-the-badge` style for headers. -| Type | Badge | Code | -|------|-------|------| -| Roadmap | [![Roadmap](https://img.shields.io/badge/Roadmap-8B5CF6?style=for-the-badge&logo=googlemaps&logoColor=white)](https://github.com) | `[![Roadmap](https://img.shields.io/badge/Roadmap-8B5CF6?style=for-the-badge&logo=googlemaps&logoColor=white)](URL)` | +| Type | Badge | Code | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Roadmap | [![Roadmap](https://img.shields.io/badge/Roadmap-8B5CF6?style=for-the-badge&logo=googlemaps&logoColor=white)](https://github.com) | `[![Roadmap](https://img.shields.io/badge/Roadmap-8B5CF6?style=for-the-badge&logo=googlemaps&logoColor=white)](URL)` | | Discussions | [![Discussions](https://img.shields.io/badge/Discussions-8B5CF6?style=for-the-badge&logo=imessage&logoColor=white)](https://github.com) | `[![Discussions](https://img.shields.io/badge/Discussions-8B5CF6?style=for-the-badge&logo=imessage&logoColor=white)](URL)` | -| Issues | [![Issues](https://img.shields.io/badge/Issues-8B5CF6?style=for-the-badge&logo=github&logoColor=white)](https://github.com) | `[![Issues](https://img.shields.io/badge/Issues-8B5CF6?style=for-the-badge&logo=github&logoColor=white)](URL)` | +| Issues | [![Issues](https://img.shields.io/badge/Issues-8B5CF6?style=for-the-badge&logo=github&logoColor=white)](https://github.com) | `[![Issues](https://img.shields.io/badge/Issues-8B5CF6?style=for-the-badge&logo=github&logoColor=white)](URL)` | ### Badge Guidelines diff --git a/docs/contributing/standards/documentation/formatting.md b/docs/contributing/standards/documentation/formatting.md index 2e8e983..df6b2b5 100644 --- a/docs/contributing/standards/documentation/formatting.md +++ b/docs/contributing/standards/documentation/formatting.md @@ -15,8 +15,8 @@ Show only the critical parts. Omit imports, boilerplate, and obvious code. This example is focused on the API. ```ts -const config = await loadConfig('kidd') -const scripts = discoverScripts(config) +const config = await loadConfig("kidd"); +const scripts = discoverScripts(config); ``` #### Incorrect @@ -24,25 +24,25 @@ const scripts = discoverScripts(config) This example is too noisy and the reader is distracted by the boilerplate and obvious code. ```ts -import { loadConfig } from './lib/config' -import { discoverScripts } from './lib/discovery' -import { createRunner } from './runtime/runner' -import { createContext } from './runtime/context' -import { log } from '@clack/prompts' +import { loadConfig } from "./lib/config"; +import { discoverScripts } from "./lib/discovery"; +import { createRunner } from "./runtime/runner"; +import { createContext } from "./runtime/context"; +import { log } from "@clack/prompts"; async function main() { - const config = await loadConfig('kidd') - const scripts = discoverScripts(config) - const runner = createRunner(config) - const context = createContext({ cwd: process.cwd() }) + const config = await loadConfig("kidd"); + const scripts = discoverScripts(config); + const runner = createRunner(config); + const context = createContext({ cwd: process.cwd() }); for (const script of scripts) { - const result = await runner.execute(script, context) - log.info(`Completed: ${result.name}`) + const result = await runner.execute(script, context); + log.info(`Completed: ${result.name}`); } } -main() +main(); ``` ### Use Full Examples for Copy-Paste Templates @@ -53,18 +53,18 @@ When the reader should copy the entire block, show everything including imports ```ts // Full file template - reader copies this -import { cli, z } from '@kidd-cli/core' +import { cli, z } from "@kidd-cli/core"; export default cli({ - description: 'Generate types from the API schema', + description: "Generate types from the API schema", args: { - output: z.string().default('src/generated'), + output: z.string().default("src/generated"), watch: z.boolean().default(false), }, async run(ctx) { - ctx.logger.info(`Generating types to ${ctx.args.output}`) + ctx.logger.info(`Generating types to ${ctx.args.output}`); }, -}) +}); ``` ### Follow Code Example Rules @@ -92,7 +92,7 @@ Always specify the language for syntax highlighting. #### Correct ```ts -const example = 'typescript' +const example = "typescript"; ``` ```bash diff --git a/docs/contributing/standards/typescript/coding-style.md b/docs/contributing/standards/typescript/coding-style.md index a783d10..f51e205 100644 --- a/docs/contributing/standards/typescript/coding-style.md +++ b/docs/contributing/standards/typescript/coding-style.md @@ -13,18 +13,18 @@ All bindings must be `const`. No reassignment, no mutation. Mutable state inside #### Correct ```ts -const timeout = 5000 -const scripts = config.scripts.filter(isEnabled) +const timeout = 5000; +const scripts = config.scripts.filter(isEnabled); ``` #### Incorrect ```ts -let timeout = 5000 -timeout = 10000 +let timeout = 5000; +timeout = 10000; -let scripts: Script[] = [] -scripts.push(newScript) +let scripts: Script[] = []; +scripts.push(newScript); ``` ### No Loops @@ -34,17 +34,17 @@ Use `map`, `filter`, `reduce`, `flatMap`, and `es-toolkit` utilities instead of #### Correct ```ts -const names = scripts.map((s) => s.name) -const enabled = scripts.filter((s) => s.enabled) -const total = items.reduce((sum, item) => sum + item.count, 0) +const names = scripts.map((s) => s.name); +const enabled = scripts.filter((s) => s.enabled); +const total = items.reduce((sum, item) => sum + item.count, 0); ``` #### Incorrect ```ts -const names: string[] = [] +const names: string[] = []; for (const script of scripts) { - names.push(script.name) + names.push(script.name); } ``` @@ -63,7 +63,7 @@ Use plain objects, closures, and factory functions. Classes are permitted only w ```ts export function capitalize(s: string) { - return s.charAt(0).toUpperCase() + s.slice(1) + return s.charAt(0).toUpperCase() + s.slice(1); } ``` @@ -72,7 +72,7 @@ export function capitalize(s: string) { ```ts class StringUtils { static capitalize(s: string) { - return s.charAt(0).toUpperCase() + s.slice(1) + return s.charAt(0).toUpperCase() + s.slice(1); } } ``` @@ -90,9 +90,9 @@ Return errors as values using the `Result` tuple type. No `throw` statements or ```ts function parseConfig(raw: string): Result { if (!raw) { - return [{ type: 'parse_error', message: 'Empty input' }, null] + return [{ type: "parse_error", message: "Empty input" }, null]; } - return [null, JSON.parse(raw)] + return [null, JSON.parse(raw)]; } ``` @@ -101,9 +101,9 @@ function parseConfig(raw: string): Result { ```ts function parseConfig(raw: string): Config { if (!raw) { - throw new Error('Empty input') + throw new Error("Empty input"); } - return JSON.parse(raw) + return JSON.parse(raw); } ``` @@ -115,7 +115,7 @@ Do not mutate objects or arrays after creation. Return new values from every tra ```ts function addScript(scripts: readonly Script[], script: Script): readonly Script[] { - return [...scripts, script] + return [...scripts, script]; } ``` @@ -123,7 +123,7 @@ function addScript(scripts: readonly Script[], script: Script): readonly Script[ ```ts function addScript(scripts: Script[], script: Script) { - scripts.push(script) + scripts.push(script); } ``` @@ -135,16 +135,16 @@ Use `if`/`else` or `match` expressions. Ternaries are banned by the linter. ```ts if (isVerbose) { - log.info(details) + log.info(details); } else { - log.info(summary) + log.info(summary); } ``` #### Incorrect ```ts -const message = isVerbose ? details : summary +const message = isVerbose ? details : summary; ``` ### No Optional Chaining @@ -155,14 +155,14 @@ Use explicit `if`/`else` or pattern matching instead of `?.`. ```ts if (config.scripts) { - runAll(config.scripts) + runAll(config.scripts); } ``` #### Incorrect ```ts -config.scripts?.forEach(run) +config.scripts?.forEach(run); ``` ### No `any` @@ -174,9 +174,9 @@ Use `unknown`, generics, or proper types. Narrow with type guards when needed. ```ts function parse(data: unknown): Result { if (!isPlainObject(data)) { - return [{ type: 'parse_error', message: 'Expected object' }, null] + return [{ type: "parse_error", message: "Expected object" }, null]; } - return [null, validateConfig(data)] + return [null, validateConfig(data)]; } ``` @@ -184,7 +184,7 @@ function parse(data: unknown): Result { ```ts function parse(data: any): Config { - return data + return data; } ``` @@ -195,15 +195,15 @@ When passing a named function to a higher-order function, pass it directly inste #### Correct ```ts -const valid = scripts.filter(isEnabled) -const names = items.map(getName) +const valid = scripts.filter(isEnabled); +const names = items.map(getName); ``` #### Incorrect ```ts -const valid = scripts.filter((s) => isEnabled(s)) -const names = items.map((item) => getName(item)) +const valid = scripts.filter((s) => isEnabled(s)); +const names = items.map((item) => getName(item)); ``` ### ESM Only @@ -213,17 +213,17 @@ Use ES module syntax with `verbatimModuleSyntax`. Use `import type` for type-onl #### Correct ```ts -import type { Config } from './types' -import { readFile } from 'node:fs/promises' -import { loadConfig } from './lib/config' +import type { Config } from "./types"; +import { readFile } from "node:fs/promises"; +import { loadConfig } from "./lib/config"; ``` #### Incorrect ```ts -const fs = require('fs') -import { Config } from './types' // should use import type -import { readFile } from 'fs' // should use node: protocol +const fs = require("fs"); +import { Config } from "./types"; // should use import type +import { readFile } from "fs"; // should use node: protocol ``` ### No IIFEs @@ -241,7 +241,7 @@ async function execute(options: Options): Promise { } function start(options: Options): void { - void execute(options) + void execute(options); } ``` @@ -251,7 +251,7 @@ function start(options: Options): void { function start(options: Options): void { void (async () => { // ... - })() + })(); } ``` @@ -266,27 +266,27 @@ Organize imports into three groups separated by blank lines, sorted alphabetical #### Correct ```ts -import { readdir } from 'node:fs/promises' -import { basename, resolve } from 'node:path' +import { readdir } from "node:fs/promises"; +import { basename, resolve } from "node:path"; -import { isPlainObject } from 'es-toolkit' -import { match } from 'ts-pattern' +import { isPlainObject } from "es-toolkit"; +import { match } from "ts-pattern"; -import type { Command } from '../types.js' -import { createLogger } from '../lib/logger.js' -import { registerCommandArgs } from './args.js' -import type { ResolvedRef } from './register.js' +import type { Command } from "../types.js"; +import { createLogger } from "../lib/logger.js"; +import { registerCommandArgs } from "./args.js"; +import type { ResolvedRef } from "./register.js"; ``` #### Incorrect ```ts -import { match } from 'ts-pattern' -import { readdir } from 'node:fs/promises' // node: should be first -import { registerCommandArgs } from './args.js' -import type { Command } from '../types.js' // ../ should come before ./ -import { isPlainObject } from 'es-toolkit' -import { createLogger, type Logger } from '../lib/logger.js' // no inline type specifiers +import { match } from "ts-pattern"; +import { readdir } from "node:fs/promises"; // node: should be first +import { registerCommandArgs } from "./args.js"; +import type { Command } from "../types.js"; // ../ should come before ./ +import { isPlainObject } from "es-toolkit"; +import { createLogger, type Logger } from "../lib/logger.js"; // no inline type specifiers ``` ### File Structure @@ -304,9 +304,9 @@ Exported functions appear first so readers see the public API without scrolling. #### Correct ```ts -import type { Config } from '../types.js' +import type { Config } from "../types.js"; -const DEFAULT_NAME = 'untitled' +const DEFAULT_NAME = "untitled"; /** * Load and validate a configuration file. @@ -315,8 +315,8 @@ const DEFAULT_NAME = 'untitled' * @returns The validated configuration record. */ export function loadConfig(path: string): Config { - const raw = readRawConfig(path) - return validateConfig(raw) + const raw = readRawConfig(path); + return validateConfig(raw); } // --------------------------------------------------------------------------- @@ -358,8 +358,8 @@ function validateConfig(raw: string): Config { } export function loadConfig(path: string): Config { - const raw = readRawConfig(path) - return validateConfig(raw) + const raw = readRawConfig(path); + return validateConfig(raw); } ``` diff --git a/docs/contributing/standards/typescript/conditionals.md b/docs/contributing/standards/typescript/conditionals.md index b8e2ed0..8490a86 100644 --- a/docs/contributing/standards/typescript/conditionals.md +++ b/docs/contributing/standards/typescript/conditionals.md @@ -20,28 +20,28 @@ Use `ts-pattern` for conditional logic with 2+ branches. It provides exhaustiven #### Correct ```ts -import { match, P } from 'ts-pattern' +import { match, P } from "ts-pattern"; // Match on value const message = match(status) - .with('pending', () => 'Waiting...') - .with('success', () => 'Done!') - .with('error', () => 'Failed') - .exhaustive() + .with("pending", () => "Waiting...") + .with("success", () => "Done!") + .with("error", () => "Failed") + .exhaustive(); // Match on object shape const result = match(event) - .with({ type: 'script', status: 'running' }, () => showProgress()) - .with({ type: 'script' }, () => showIdle()) - .with({ type: 'task' }, () => showTaskInfo()) - .exhaustive() + .with({ type: "script", status: "running" }, () => showProgress()) + .with({ type: "script" }, () => showIdle()) + .with({ type: "task" }, () => showTaskInfo()) + .exhaustive(); // Match with wildcards and predicates const label = match(count) - .with(0, () => 'None') - .with(1, () => 'One') - .with(P.number.gte(2), () => 'Many') - .exhaustive() + .with(0, () => "None") + .with(1, () => "One") + .with(P.number.gte(2), () => "Many") + .exhaustive(); ``` #### Incorrect @@ -49,20 +49,20 @@ const label = match(count) ```ts // Nested ternaries are hard to read const message = - status === 'pending' - ? 'Waiting' - : status === 'success' - ? 'Done' - : status === 'error' - ? 'Failed' - : 'Unknown' + status === "pending" + ? "Waiting" + : status === "success" + ? "Done" + : status === "error" + ? "Failed" + : "Unknown"; // Switch without exhaustiveness switch (status) { - case 'pending': - return 'Waiting' - case 'success': - return 'Done' + case "pending": + return "Waiting"; + case "success": + return "Done"; // Missing 'error' case - no compiler warning! } ``` @@ -77,9 +77,9 @@ Always use the inferred type from the `ts-pattern` callback parameter. Never cas match(event) .with({ config: P.nonNullable, action: P.string }, (e) => { // `e` is automatically narrowed - use it directly - console.log(e.config.path, e.action) + console.log(e.config.path, e.action); }) - .otherwise(() => {}) + .otherwise(() => {}); ``` #### Incorrect @@ -87,10 +87,10 @@ match(event) ```ts match(event) .with({ config: P.nonNullable, action: P.string }, () => { - const configEvent = event as ConfigEvent // Don't cast - console.log(configEvent.config.path) + const configEvent = event as ConfigEvent; // Don't cast + console.log(configEvent.config.path); }) - .otherwise(() => {}) + .otherwise(() => {}); ``` ### Match on Shape, Not Categories @@ -103,17 +103,17 @@ Use `ts-pattern` directly to match on object shape rather than creating intermed match(event) .with({ scripts: P.nonNullable, workspace: P.string }, (e) => handleScripts(e)) .with({ config: P.nonNullable }, (e) => handleConfig(e)) - .otherwise(() => handleUnknown()) + .otherwise(() => handleUnknown()); ``` #### Incorrect ```ts -const category = categorizeEvent(event) // Don't pre-categorize +const category = categorizeEvent(event); // Don't pre-categorize match(category) - .with('scripts', () => handleScripts(event as ScriptsEvent)) - .with('config', () => handleConfig(event as ConfigEvent)) - .otherwise(() => {}) + .with("scripts", () => handleScripts(event as ScriptsEvent)) + .with("config", () => handleConfig(event as ConfigEvent)) + .otherwise(() => {}); ``` ### Always Use .exhaustive() @@ -123,22 +123,22 @@ Use `.exhaustive()` to ensure all cases are handled at compile time. Reserve `.o #### Correct ```ts -type Status = 'pending' | 'success' | 'error' +type Status = "pending" | "success" | "error"; // Compiler error if a case is missing match(status) - .with('pending', () => 'Waiting') - .with('success', () => 'Done') - .with('error', () => 'Failed') - .exhaustive() + .with("pending", () => "Waiting") + .with("success", () => "Done") + .with("error", () => "Failed") + .exhaustive(); ``` #### Incorrect ```ts match(status) - .with('pending', () => 'Waiting') - .otherwise(() => 'Unknown') // Hides missing cases + .with("pending", () => "Waiting") + .otherwise(() => "Unknown"); // Hides missing cases ``` ### Use if/else for Simple Conditions @@ -150,19 +150,19 @@ Use `if`/`else` for simple boolean conditions. Ternaries are not permitted by th ```ts function getLabel(isActive: boolean): string { if (isActive) { - return 'Active' + return "Active"; } - return 'Inactive' + return "Inactive"; } -const label = getLabel(isActive) +const label = getLabel(isActive); ``` #### Incorrect ```ts // Ternaries are banned by oxlint -const label = isActive ? 'Active' : 'Inactive' +const label = isActive ? "Active" : "Inactive"; ``` ### Use Early Returns for Guards @@ -173,11 +173,11 @@ Use `if` statements with early returns for guard clauses that reject invalid sta ```ts function processScript(script: Script | null) { - if (!script) return null - if (!script.enabled) return null + if (!script) return null; + if (!script.enabled) return null; // Main logic here - return execute(script) + return execute(script); } ``` @@ -187,10 +187,10 @@ function processScript(script: Script | null) { function processScript(script: Script | null) { if (script) { if (script.enabled) { - return execute(script) + return execute(script); } } - return null + return null; } ``` diff --git a/docs/contributing/standards/typescript/design-patterns.md b/docs/contributing/standards/typescript/design-patterns.md index e5ae98c..57ea6de 100644 --- a/docs/contributing/standards/typescript/design-patterns.md +++ b/docs/contributing/standards/typescript/design-patterns.md @@ -14,45 +14,45 @@ Use factory functions to encapsulate state instead of classes. Factories avoid ` ```ts interface Runner { - run: (script: string) => Promise - stop: () => void - isRunning: () => boolean + run: (script: string) => Promise; + stop: () => void; + isRunning: () => boolean; } function createRunner(config: RunnerConfig): Runner { - let running = false + let running = false; return { run: async (script) => { - running = true - const result = await execute(script, config) - running = false - return result + running = true; + const result = await execute(script, config); + running = false; + return result; }, stop: () => { - running = false + running = false; }, isRunning: () => running, - } + }; } // Usage -const runner = createRunner({ timeout: 5000 }) -await runner.run('build') +const runner = createRunner({ timeout: 5000 }); +await runner.run("build"); ``` ```ts // Factory can return different implementations -function createLogger(env: 'dev' | 'prod') { - if (env === 'dev') { +function createLogger(env: "dev" | "prod") { + if (env === "dev") { return { log: (msg: string) => console.log(`[DEV] ${msg}`), - } + }; } return { log: (msg: string) => sendToLogService(msg), - } + }; } ``` @@ -60,21 +60,21 @@ function createLogger(env: 'dev' | 'prod') { ```ts class Runner { - private running = false + private running = false; constructor(private config: RunnerConfig) {} async run(script: string) { - this.running = true - const result = await execute(script, this.config) - this.running = false - return result + this.running = true; + const result = await execute(script, this.config); + this.running = false; + return result; } } -const runner = new Runner({ timeout: 5000 }) -const fn = runner.run -fn('build') // `this` is lost! +const runner = new Runner({ timeout: 5000 }); +const fn = runner.run; +fn("build"); // `this` is lost! ``` ### Transform Data Through Pipelines @@ -88,14 +88,14 @@ Transform data through pure pipelines. Avoid shared mutable state by returning n const result = scripts .filter((script) => script.enabled) .map((script) => script.name) - .join(', ') + .join(", "); // Explicit transformations function processConfig(raw: RawConfig): ProcessedConfig { - const parsed = parseToml(raw.content) - const validated = validateSchema(parsed) - const resolved = resolvePaths(validated) - return resolved + const parsed = parseToml(raw.content); + const validated = validateSchema(parsed); + const resolved = resolvePaths(validated); + return resolved; } ``` @@ -103,15 +103,15 @@ function processConfig(raw: RawConfig): ProcessedConfig { ```ts // Mutating shared state -const scripts: Script[] = [] +const scripts: Script[] = []; function addScript(script: Script) { - scripts.push(script) // Mutation! + scripts.push(script); // Mutation! } // Return new state instead function addScript(scripts: readonly Script[], script: Script): Script[] { - return [...scripts, script] + return [...scripts, script]; } ``` @@ -123,24 +123,24 @@ Combine small, focused interfaces and factory functions instead of building inhe ```ts interface Runnable { - run: () => Promise + run: () => Promise; } interface Configurable { - configure: (config: Record) => void + configure: (config: Record) => void; } function createTask(name: string): Runnable & Configurable { - let taskConfig: Record = {} + let taskConfig: Record = {}; return { run: async () => { - await execute(name, taskConfig) + await execute(name, taskConfig); }, configure: (config) => { - taskConfig = { ...config } + taskConfig = { ...config }; }, - } + }; } ``` diff --git a/docs/contributing/standards/typescript/errors.md b/docs/contributing/standards/typescript/errors.md index ec62ea4..4777b4e 100644 --- a/docs/contributing/standards/typescript/errors.md +++ b/docs/contributing/standards/typescript/errors.md @@ -11,34 +11,37 @@ All operations that can fail in expected ways use the `Result` type instea Define success and failure as a tuple where the first element is the error (or `null`) and the second is the value (or `null`). Destructure the tuple to check which case occurred. ```ts -type Result = readonly [E, null] | readonly [null, T] +type Result = readonly [E, null] | readonly [null, T]; ``` Construct success and failure tuples directly: ```ts // Success -const success: Result = [null, config] +const success: Result = [null, config]; // Failure -const failure: Result = [{ type: 'parse_error', message: 'Invalid JSON' }, null] +const failure: Result = [ + { type: "parse_error", message: "Invalid JSON" }, + null, +]; ``` For CLI handlers, use the `HandlerResult` specialization with `ok()` and `fail()` constructors from `src/lib/result.ts`: ```ts -type HandlerResult = Result +type HandlerResult = Result; interface HandlerError { - readonly message: string - readonly hint?: string - readonly exitCode?: number + readonly message: string; + readonly hint?: string; + readonly exitCode?: number; } -function ok(): HandlerResult -function ok(value: T): HandlerResult +function ok(): HandlerResult; +function ok(value: T): HandlerResult; -function fail(error: HandlerError): HandlerResult +function fail(error: HandlerError): HandlerResult; ``` ### Return Results for Expected Failures @@ -48,32 +51,32 @@ Use `Result` for operations that can fail in expected ways such as parsing #### Correct ```ts -import type { Result } from '../lib/result.ts' +import type { Result } from "../lib/result.ts"; interface ParseError { - type: 'parse_error' | 'validation_error' - message: string + type: "parse_error" | "validation_error"; + message: string; } function parseConfig(json: string): Result { try { - const data = JSON.parse(json) - return [null, data] + const data = JSON.parse(json); + return [null, data]; } catch { - return [{ type: 'parse_error', message: 'Invalid JSON' }, null] + return [{ type: "parse_error", message: "Invalid JSON" }, null]; } } // Usage — destructure the tuple -const [parseError, config] = parseConfig(input) +const [parseError, config] = parseConfig(input); if (parseError) { - logger.error({ error: parseError }, 'Failed to parse config') - return + logger.error({ error: parseError }, "Failed to parse config"); + return; } // config is typed as Config -processConfig(config) +processConfig(config); ``` #### Incorrect @@ -82,9 +85,9 @@ processConfig(config) // Throwing instead of returning a Result function parseConfig(json: string): Config { if (!json) { - throw new Error('Empty input') // Don't throw + throw new Error("Empty input"); // Don't throw } - return JSON.parse(json) + return JSON.parse(json); } ``` @@ -97,22 +100,22 @@ Use a wrapper to convert promise rejections into `Result` tuples. ```ts async function attemptAsync(fn: () => Promise): Promise> { try { - return [null, await fn()] + return [null, await fn()]; } catch (error) { - return [error as E, null] + return [error as E, null]; } } // Usage — destructure the tuple -const [readError, contents] = await attemptAsync(() => readFile(configPath)) +const [readError, contents] = await attemptAsync(() => readFile(configPath)); if (readError) { - console.error('Read failed:', readError) - return + console.error("Read failed:", readError); + return; } // contents is typed as string (or whatever readFile returns) -processContents(contents) +processContents(contents); ``` ### Define Domain-Specific Results @@ -124,12 +127,12 @@ Create type aliases for consistency within a domain. This keeps function signatu ```ts // types.ts interface ConfigError { - type: 'invalid_toml' | 'missing_field' | 'unknown_workspace' - message: string - details?: unknown + type: "invalid_toml" | "missing_field" | "unknown_workspace"; + message: string; + details?: unknown; } -export type ConfigResult = Result +export type ConfigResult = Result; // implementation function loadConfig(path: string): ConfigResult { @@ -146,18 +149,18 @@ Use early returns to chain multiple Result-producing steps. Each step bails out ```ts async function runScript(name: string, workspace: string): Promise> { // Step 1: Load config - const [configError, config] = loadConfig(workspace) - if (configError) return [configError, null] + const [configError, config] = loadConfig(workspace); + if (configError) return [configError, null]; // Step 2: Resolve script - const [resolveError, script] = resolveScript(config, name) - if (resolveError) return [resolveError, null] + const [resolveError, script] = resolveScript(config, name); + if (resolveError) return [resolveError, null]; // Step 3: Execute - const [execError, output] = await execute(script) - if (execError) return [execError, null] + const [execError, output] = await execute(script); + if (execError) return [execError, null]; - return [null, output] + return [null, output]; } ``` @@ -168,24 +171,24 @@ Use destructuring and early returns to handle different error types. For exhaust #### Correct ```ts -const [error, config] = loadConfig(path) +const [error, config] = loadConfig(path); if (error) { match(error.type) - .with('invalid_toml', () => { - logger.warn('Invalid TOML in config file') + .with("invalid_toml", () => { + logger.warn("Invalid TOML in config file"); }) - .with('missing_field', () => { - logger.warn('Missing required field') + .with("missing_field", () => { + logger.warn("Missing required field"); }) - .with('unknown_workspace', () => { - logger.warn('Unknown workspace') + .with("unknown_workspace", () => { + logger.warn("Unknown workspace"); }) - .exhaustive() - return + .exhaustive(); + return; } -applyConfig(config) +applyConfig(config); ``` ### Never Throw in Result-Returning Functions @@ -197,9 +200,9 @@ A function that declares `Result` as its return type must never throw. All failu ```ts function parse(json: string): Result { if (!json) { - return [{ type: 'parse_error', message: 'Empty input' }, null] + return [{ type: "parse_error", message: "Empty input" }, null]; } - return [null, JSON.parse(json)] + return [null, JSON.parse(json)]; } ``` @@ -208,9 +211,9 @@ function parse(json: string): Result { ```ts function parse(json: string): Result { if (!json) { - throw new Error('Empty input') // Don't throw! + throw new Error("Empty input"); // Don't throw! } - return [null, JSON.parse(json)] + return [null, JSON.parse(json)]; } ``` @@ -221,17 +224,17 @@ Never access the value element without first confirming the error element is `nu #### Correct ```ts -const [error, config] = parseConfig(input) +const [error, config] = parseConfig(input); if (!error) { - processConfig(config) + processConfig(config); } ``` #### Incorrect ```ts -const [, config] = parseConfig(input) -processConfig(config) // config might be null — error was not checked +const [, config] = parseConfig(input); +processConfig(config); // config might be null — error was not checked ``` ## References diff --git a/docs/contributing/standards/typescript/functions.md b/docs/contributing/standards/typescript/functions.md index 413eb0b..2c51ce8 100644 --- a/docs/contributing/standards/typescript/functions.md +++ b/docs/contributing/standards/typescript/functions.md @@ -29,9 +29,9 @@ Define an interface with a `Params`, `Options`, or `Args` suffix, then destructu ```ts interface RunScriptParams { - name: string - workspace: string - dryRun: boolean + name: string; + workspace: string; + dryRun: boolean; } export function runScript({ name, workspace, dryRun }: RunScriptParams): RunResult { @@ -39,18 +39,18 @@ export function runScript({ name, workspace, dryRun }: RunScriptParams): RunResu } // Usage is self-documenting -runScript({ name: 'build', workspace: 'packages/core', dryRun: false }) +runScript({ name: "build", workspace: "packages/core", dryRun: false }); ``` ```ts interface ResolvePathParams { - root: string - workspace: string + root: string; + workspace: string; } interface ResolvePathOptions { - absolute?: boolean - followSymlinks?: boolean + absolute?: boolean; + followSymlinks?: boolean; } function resolvePath({ root, workspace }: ResolvePathParams, options?: ResolvePathOptions): string { @@ -67,7 +67,7 @@ function runScript(name: string, workspace: string, dryRun: boolean): RunResult } // Easy to swap by mistake -runScript('packages/core', 'build', false) +runScript("packages/core", "build", false); ``` ### Document All Functions with JSDoc @@ -104,7 +104,7 @@ export function resolveScript({ * @returns The normalized name. */ function normalizeName(name: string): string { - return kebabCase(name) + return kebabCase(name); } ``` @@ -116,7 +116,7 @@ export function resolveScript(params: ResolveScriptParams) {} // Missing @private on non-exported function function normalizeName(name: string): string { - return kebabCase(name) + return kebabCase(name); } // Listing every property in JSDoc @@ -136,7 +136,7 @@ Prefer pure functions that have no side effects and return predictable outputs. ```ts // Pure function - no side effects function buildScriptCommand(script: Script, args: readonly string[]): string { - return [script.command, ...args].join(' ') + return [script.command, ...args].join(" "); } ``` @@ -148,14 +148,14 @@ function validateConfig(config: LaufConfig): ValidationResult { // Side effects isolated in handler async function handleInit(config: LaufConfig) { - const validation = validateConfig(config) // Pure + const validation = validateConfig(config); // Pure if (!validation.ok) { - logger.warn({ validation }, 'Invalid config') // Side effect at edge - return + logger.warn({ validation }, "Invalid config"); // Side effect at edge + return; } - await writeConfig(config) // Side effect at edge + await writeConfig(config); // Side effect at edge } ``` @@ -164,10 +164,10 @@ async function handleInit(config: LaufConfig) { ```ts // Side effects mixed into business logic function buildScriptCommand(script: Script, args: readonly string[]): string { - console.log('Building command...') // Side effect - const cmd = [script.command, ...args].join(' ') - analytics.track('command_built') // Side effect - return cmd + console.log("Building command..."); // Side effect + const cmd = [script.command, ...args].join(" "); + analytics.track("command_built"); // Side effect + return cmd; } ``` @@ -179,24 +179,24 @@ Prefer small, focused functions that can be composed together. Use early returns ```ts // Small, focused functions -const normalize = (s: string) => s.trim().toLowerCase() -const validate = (s: string) => s.length > 0 -const format = (s: string) => s.charAt(0).toUpperCase() + s.slice(1) +const normalize = (s: string) => s.trim().toLowerCase(); +const validate = (s: string) => s.length > 0; +const format = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); // Composed together function processName(input: string): string | null { - const normalized = normalize(input) - if (!validate(normalized)) return null - return format(normalized) + const normalized = normalize(input); + if (!validate(normalized)) return null; + return format(normalized); } ``` ```ts // Early returns to avoid deep nesting function process(data: Data) { - if (data.type !== 'script') return - if (data.status !== 'active') return - if (data.items.length === 0) return + if (data.type !== "script") return; + if (data.status !== "active") return; + if (data.items.length === 0) return; // Main logic here } @@ -207,8 +207,8 @@ function process(data: Data) { ```ts // Deeply nested conditionals function process(data: Data) { - if (data.type === 'script') { - if (data.status === 'active') { + if (data.type === "script") { + if (data.status === "active") { if (data.items.length > 0) { // ... } diff --git a/docs/contributing/standards/typescript/naming.md b/docs/contributing/standards/typescript/naming.md index b118ea6..a24a0da 100644 --- a/docs/contributing/standards/typescript/naming.md +++ b/docs/contributing/standards/typescript/naming.md @@ -44,16 +44,16 @@ Use **camelCase** for variables and function names. #### Correct ```ts -const userId = '123' -const isAuthenticated = true +const userId = "123"; +const isAuthenticated = true; function parseHeaders() {} ``` #### Incorrect ```ts -const user_id = '123' -const IsAuthenticated = true +const user_id = "123"; +const IsAuthenticated = true; function ParseHeaders() {} ``` @@ -64,19 +64,19 @@ Use **SCREAMING_SNAKE_CASE** for constants. Group related constants in objects w #### Correct ```ts -export const MAX_RETRIES = 3 +export const MAX_RETRIES = 3; export const SCRIPT_EVENTS = { - START: 'start', - COMPLETE: 'complete', -} as const + START: "start", + COMPLETE: "complete", +} as const; ``` #### Incorrect ```ts -export const maxRetries = 3 -export const scriptEvents = { start: 'start' } +export const maxRetries = 3; +export const scriptEvents = { start: "start" }; ``` ### Object Property Naming @@ -89,20 +89,20 @@ Prefer **nested objects** when properties form a logical group. Use flat naming // Nested — grouped by relationship interface Config { runner: { - timeout: number - parallel: boolean - } + timeout: number; + parallel: boolean; + }; output: { - format: string - verbose: boolean - } + format: string; + verbose: boolean; + }; } // Flat — simple DTO, destructuring is primary use interface RunScriptParams { - name: string - workspace: string - dryRun: boolean + name: string; + workspace: string; + dryRun: boolean; } ``` @@ -111,18 +111,18 @@ interface RunScriptParams { ```ts // Concatenated names instead of nesting interface Config { - runnerTimeout: number - runnerParallel: boolean - outputFormat: string - outputVerbose: boolean + runnerTimeout: number; + runnerParallel: boolean; + outputFormat: string; + outputVerbose: boolean; } // Unnecessary nesting for unrelated properties interface RunScriptParams { data: { - name: string - workspace: string - } + name: string; + workspace: string; + }; } ``` diff --git a/docs/contributing/standards/typescript/state.md b/docs/contributing/standards/typescript/state.md index 18a228e..70ee7be 100644 --- a/docs/contributing/standards/typescript/state.md +++ b/docs/contributing/standards/typescript/state.md @@ -14,15 +14,15 @@ State should never be mutated in place. Return new arrays and objects from every ```ts function addItem(items: readonly Item[], newItem: Item): readonly Item[] { - return [...items, newItem] + return [...items, newItem]; } function updateItem(items: readonly Item[], id: string, updates: Partial): readonly Item[] { - return items.map((item) => (item.id === id ? { ...item, ...updates } : item)) + return items.map((item) => (item.id === id ? { ...item, ...updates } : item)); } function removeItem(items: readonly Item[], id: string): readonly Item[] { - return items.filter((item) => item.id !== id) + return items.filter((item) => item.id !== id); } ``` @@ -30,12 +30,12 @@ function removeItem(items: readonly Item[], id: string): readonly Item[] { ```ts function addItem(items: Item[], newItem: Item) { - items.push(newItem) // Mutation! + items.push(newItem); // Mutation! } function updateItem(items: Item[], id: string, updates: Partial) { - const item = items.find((i) => i.id === id) - Object.assign(item, updates) // Mutation! + const item = items.find((i) => i.id === id); + Object.assign(item, updates); // Mutation! } ``` @@ -47,34 +47,34 @@ Use factories and closures to encapsulate state. Never use classes. Mutation ins ```ts function createCache() { - const cache = new Map() + const cache = new Map(); return { get: (key: string) => cache.get(key), set: (key: string, value: T) => { - cache.set(key, value) + cache.set(key, value); }, has: (key: string) => cache.has(key), clear: () => cache.clear(), - } + }; } -const configCache = createCache() -configCache.set('root', resolvedConfig) +const configCache = createCache(); +configCache.set("root", resolvedConfig); ``` #### Incorrect ```ts class Cache { - private cache = new Map() + private cache = new Map(); get(key: string) { - return this.cache.get(key) + return this.cache.get(key); } set(key: string, value: T) { - this.cache.set(key, value) + this.cache.set(key, value); } } ``` @@ -87,29 +87,29 @@ Compute derived values from source state on demand. Never store values that can ```ts interface WorkspaceState { - scripts: readonly Script[] + scripts: readonly Script[]; } function getScriptCount(state: WorkspaceState): number { - return state.scripts.length + return state.scripts.length; } function getScriptNames(state: WorkspaceState): readonly string[] { - return state.scripts.map((s) => s.name) + return state.scripts.map((s) => s.name); } // Usage - compute when needed -const count = getScriptCount(workspace) -const names = getScriptNames(workspace) +const count = getScriptCount(workspace); +const names = getScriptNames(workspace); ``` #### Incorrect ```ts interface WorkspaceState { - scripts: Script[] - scriptCount: number // Derived - will get out of sync! - scriptNames: string[] // Derived - will get out of sync! + scripts: Script[]; + scriptCount: number; // Derived - will get out of sync! + scriptNames: string[]; // Derived - will get out of sync! } ``` diff --git a/docs/contributing/standards/typescript/testing.md b/docs/contributing/standards/typescript/testing.md index d221ca6..614c021 100644 --- a/docs/contributing/standards/typescript/testing.md +++ b/docs/contributing/standards/typescript/testing.md @@ -37,27 +37,27 @@ Each test should have a single assertion focus. Use `async`/`await` for asynchro #### Correct ```ts -import { describe, it, expect } from 'vitest' -import { resolveScriptPath } from './resolver' +import { describe, it, expect } from "vitest"; +import { resolveScriptPath } from "./resolver"; -describe('resolveScriptPath', () => { - it('should resolve path relative to workspace root', () => { - const result = resolveScriptPath('build', '/project') - expect(result).toBe('/project/scripts/build.ts') - }) +describe("resolveScriptPath", () => { + it("should resolve path relative to workspace root", () => { + const result = resolveScriptPath("build", "/project"); + expect(result).toBe("/project/scripts/build.ts"); + }); - it('should return undefined for missing scripts', () => { - expect(resolveScriptPath('missing', '/project')).toBeUndefined() - }) -}) + it("should return undefined for missing scripts", () => { + expect(resolveScriptPath("missing", "/project")).toBeUndefined(); + }); +}); // Async tests -it('should load config from parent directories', async () => { - const config = await loadConfig('/project/packages/core') +it("should load config from parent directories", async () => { + const config = await loadConfig("/project/packages/core"); expect(config).toMatchObject({ name: expect.any(String), - }) -}) + }); +}); ``` ### Mock External Dependencies @@ -67,22 +67,22 @@ Use `vi.mock` for module-level mocks and `vi.fn` for individual functions. Repla #### Correct ```ts -import { vi, describe, it, expect } from 'vitest' +import { vi, describe, it, expect } from "vitest"; // Mock a module -vi.mock('node:fs/promises', () => ({ +vi.mock("node:fs/promises", () => ({ readFile: vi.fn().mockResolvedValue('{ "name": "test" }'), stat: vi.fn().mockResolvedValue({ isFile: () => true }), -})) +})); // Mock individual functions -const mockCallback = vi.fn() -mockCallback.mockReturnValue('result') -mockCallback.mockResolvedValue('async result') +const mockCallback = vi.fn(); +mockCallback.mockReturnValue("result"); +mockCallback.mockResolvedValue("async result"); // Assert calls -expect(mockCallback).toHaveBeenCalledWith('arg') -expect(mockCallback).toHaveBeenCalledTimes(1) +expect(mockCallback).toHaveBeenCalledWith("arg"); +expect(mockCallback).toHaveBeenCalledTimes(1); ``` ### Organize Tests by Feature @@ -92,23 +92,23 @@ Group related tests with nested `describe` blocks. Use `beforeEach` to reset moc #### Correct ```ts -import { beforeEach, describe, it, vi } from 'vitest' +import { beforeEach, describe, it, vi } from "vitest"; -describe('ScriptRunner', () => { +describe("ScriptRunner", () => { beforeEach(() => { - vi.clearAllMocks() - }) - - describe('execute', () => { - it('should run the script command', () => {}) - it('should pass environment variables', () => {}) - }) - - describe('resolve', () => { - it('should find scripts in workspace root', () => {}) - it('should return error for missing scripts', () => {}) - }) -}) + vi.clearAllMocks(); + }); + + describe("execute", () => { + it("should run the script command", () => {}); + it("should pass environment variables", () => {}); + }); + + describe("resolve", () => { + it("should find scripts in workspace root", () => {}); + it("should return error for missing scripts", () => {}); + }); +}); ``` ### Meet Coverage Requirements @@ -128,26 +128,26 @@ Test pure functions exhaustively, including boundary values and error paths. #### Correct ```ts -describe('parseTimeout', () => { - it('should parse valid number', () => { - expect(parseTimeout('5000')).toBe(5000) - }) - - it('should return default for NaN', () => { - expect(parseTimeout('abc')).toBe(30000) - }) - - it('should clamp negative values to zero', () => { - expect(parseTimeout('-1')).toBe(0) - }) -}) - -it('should return error result for missing config', async () => { - vi.mocked(readFile).mockRejectedValue(new Error('ENOENT')) - - const [error] = await loadConfig('/missing') - expect(error).toMatchObject({ message: expect.stringContaining('ENOENT') }) -}) +describe("parseTimeout", () => { + it("should parse valid number", () => { + expect(parseTimeout("5000")).toBe(5000); + }); + + it("should return default for NaN", () => { + expect(parseTimeout("abc")).toBe(30000); + }); + + it("should clamp negative values to zero", () => { + expect(parseTimeout("-1")).toBe(0); + }); +}); + +it("should return error result for missing config", async () => { + vi.mocked(readFile).mockRejectedValue(new Error("ENOENT")); + + const [error] = await loadConfig("/missing"); + expect(error).toMatchObject({ message: expect.stringContaining("ENOENT") }); +}); ``` ### Avoid Testing Anti-patterns diff --git a/docs/contributing/standards/typescript/types.md b/docs/contributing/standards/typescript/types.md index 6328c7d..29b50d9 100644 --- a/docs/contributing/standards/typescript/types.md +++ b/docs/contributing/standards/typescript/types.md @@ -14,29 +14,29 @@ Define a common discriminator field (usually `type`, `kind`, or `strategy`) that ```ts type RunResult = - | { type: 'success'; output: string } - | { type: 'failure'; error: string; exitCode: number } - | { type: 'skipped'; reason: string } + | { type: "success"; output: string } + | { type: "failure"; error: string; exitCode: number } + | { type: "skipped"; reason: string }; // Narrowing with if-checks function summarize(result: RunResult): string { - if (result.type === 'success') { - return result.output + if (result.type === "success") { + return result.output; } - if (result.type === 'failure') { - return `Exit ${result.exitCode}: ${result.error}` + if (result.type === "failure") { + return `Exit ${result.exitCode}: ${result.error}`; } - return `Skipped: ${result.reason}` + return `Skipped: ${result.reason}`; } // Exhaustive matching with ts-pattern -import { match } from 'ts-pattern' +import { match } from "ts-pattern"; const summary = match(result) - .with({ type: 'success' }, (r) => r.output) - .with({ type: 'failure' }, (r) => `Exit ${r.exitCode}: ${r.error}`) - .with({ type: 'skipped' }, (r) => `Skipped: ${r.reason}`) - .exhaustive() + .with({ type: "success" }, (r) => r.output) + .with({ type: "failure" }, (r) => `Exit ${r.exitCode}: ${r.error}`) + .with({ type: "skipped" }, (r) => `Skipped: ${r.reason}`) + .exhaustive(); ``` ### Use type-fest for Common Utilities @@ -55,19 +55,19 @@ Use [type-fest](https://github.com/sindresorhus/type-fest) for type utilities no #### Correct ```ts -import type { SetRequired, PartialDeep } from 'type-fest' +import type { SetRequired, PartialDeep } from "type-fest"; interface Config { - name: string - root?: string - scripts?: Record + name: string; + root?: string; + scripts?: Record; } // Make root required after resolution -type ResolvedConfig = SetRequired +type ResolvedConfig = SetRequired; // Deep partial for patch operations -type ConfigPatch = PartialDeep +type ConfigPatch = PartialDeep; ``` ### Write Type Guards for Runtime Checks @@ -78,17 +78,17 @@ Create custom type guard functions that return `value is T` for runtime type nar ```ts function isNonNullable(value: T): value is NonNullable { - return value != null + return value != null; } function isOk(result: Result): result is { ok: true; value: T } { - return result.ok === true + return result.ok === true; } // Usage -const result = loadConfig() +const result = loadConfig(); if (isOk(result)) { - console.log(result.value) + console.log(result.value); } ``` @@ -97,8 +97,8 @@ if (isOk(result)) { ```ts // Using `as` assertion instead of a guard function getConfig(data: unknown) { - const config = data as Config // Unsafe - no runtime check - return config + const config = data as Config; // Unsafe - no runtime check + return config; } ``` @@ -120,20 +120,20 @@ TypeScript ships utility types for common transformations. Use them instead of h ```ts interface Script { - name: string - command: string - workspace: string - description: string + name: string; + command: string; + workspace: string; + description: string; } // For update operations - all fields optional -type ScriptUpdate = Partial