claude-toolkit 0.10.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +2 -0
- package/core/agents/ct-code-reviewer.md +5 -4
- package/core/agents/ct-github-workflow.md +28 -15
- package/core/commands/ct/code-quality.md +22 -2
- package/core/commands/ct/onboard.md +8 -3
- package/core/commands/ct/pr-review.md +22 -88
- package/core/commands/ct/pr-summary.md +6 -3
- package/core/commands/ct/ticket.md +20 -3
- package/core/hooks/{skill-eval.js → skill-eval.cjs} +103 -41
- package/core/hooks/skill-rules.schema.json +4 -0
- package/core/skills/ct-code-style/SKILL.md +134 -0
- package/core/skills/ct-systematic-debugging/SKILL.md +1 -1
- package/core/skills/ct-testing-patterns/SKILL.md +5 -7
- package/core/skills/ct-typescript-conventions/SKILL.md +15 -11
- package/core/skills/ct-verification-before-completion/SKILL.md +2 -1
- package/docs/README.md +2 -0
- package/docs/commands/code-quality.md +2 -8
- package/docs/commands/onboard.md +2 -11
- package/docs/commands/pr-review.md +2 -13
- package/docs/commands/pr-summary.md +2 -2
- package/docs/commands/proto-check.md +4 -4
- package/docs/commands/ticket.md +2 -2
- package/docs/skills/code-style.md +26 -0
- package/docs/skills/systematic-debugging.md +1 -1
- package/docs/skills/testing-patterns.md +78 -56
- package/docs/skills/typescript-conventions.md +23 -10
- package/docs/skills/verification-before-completion.md +1 -1
- package/docs/stacks/esnext-idioms.md +25 -0
- package/package.json +2 -1
- package/src/detect.ts +74 -0
- package/src/generator.ts +19 -2
- package/src/types.ts +1 -0
- package/stacks/esnext/skills/ct-esnext-idioms/SKILL.md +130 -0
- package/stacks/esnext/stack.json +37 -0
- package/{core/commands/ct → stacks/protobuf/commands}/proto-check.md +11 -2
- package/stacks/solidjs/skills/ct-solidjs-patterns/SKILL.md +1 -1
- package/templates/configs/biome.base.json +8 -1
- package/templates/skill-rules.base.json +18 -23
- package/core/hooks/skill-eval.sh +0 -35
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Testing Patterns
|
|
2
2
|
|
|
3
|
-
> Generic test-driven development practices and patterns applicable to any language or test framework.
|
|
3
|
+
> Generic test-driven development practices and patterns applicable to any language or test framework. Use when writing tests, setting up a test suite, or deciding what to test at which layer
|
|
4
4
|
|
|
5
5
|
**Type:** Core Skill (always included)
|
|
6
6
|
**Source:** [`core/skills/ct-testing-patterns/SKILL.md`](../core/skills/ct-testing-patterns/SKILL.md)
|
|
@@ -11,77 +11,90 @@ Principles and patterns for writing tests that catch bugs, enable refactoring, a
|
|
|
11
11
|
|
|
12
12
|
## TDD Cycle: Red, Green, Refactor
|
|
13
13
|
|
|
14
|
-
1. **Red** -- Write a failing test
|
|
15
|
-
2. **Green** -- Write the minimal code to make
|
|
16
|
-
3. **Refactor** --
|
|
14
|
+
1. **Red** -- Write a failing test for the desired behavior. Confirm it fails for the right reason.
|
|
15
|
+
2. **Green** -- Write the minimal code to make it pass. No optimization yet.
|
|
16
|
+
3. **Refactor** -- Clean up while keeping tests green.
|
|
17
17
|
|
|
18
18
|
Commit at each stage. The cycle keeps scope small and provides a safety net for every change.
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## 3-Layer Testing Strategy
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
```
|
|
23
|
+
/ E2E \ Real browsers, full user flows
|
|
24
|
+
/----------\
|
|
25
|
+
/ Interaction \ Component sandbox, behavior, a11y
|
|
26
|
+
/----------------\
|
|
27
|
+
/ Unit \ Pure logic, signals, utilities
|
|
28
|
+
/--------------------\
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Layer | Target Ratio | What to Cover |
|
|
32
|
+
| ----------- | ------------ | ---------------------------------------------------------------------------- |
|
|
33
|
+
| Unit | ~70% | Business logic, utilities, data transforms, reactive primitives, error paths |
|
|
34
|
+
| Interaction | ~20% | Component variants, user interactions, accessibility, visual regression |
|
|
35
|
+
| E2E | ~10% | Critical user journeys, authentication flows, cross-page workflows |
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
> **Scope:** This core skill owns the layering decision below, the unit and interaction layers, and the cross-cutting E2E principles. Runner-specific E2E setup and execution live in the stack skills (e.g. `ct-playwright-patterns`) when a matching stack is installed.
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
- **Bad:** "calls `String.prototype.concat` with first name and space and last name"
|
|
39
|
+
### When to Use Which Layer
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
| Question | Layer |
|
|
42
|
+
| ------------------------------------------------- | -------------------------------------- |
|
|
43
|
+
| Does this logic need a DOM? | **Unit** if no, **Interaction** if yes |
|
|
44
|
+
| Does it involve multiple pages or navigation? | **E2E** |
|
|
45
|
+
| Am I testing a single component's visual states? | **Interaction** |
|
|
46
|
+
| Am I testing a reactive primitive (signal, memo)? | **Unit** |
|
|
47
|
+
| Does it require authentication or real network? | **E2E** |
|
|
48
|
+
| Am I testing component accessibility? | **Interaction** (axe-core) |
|
|
49
|
+
| Does it need the full server running? | **E2E** |
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
## Test Behavior, Not Implementation
|
|
52
|
+
|
|
53
|
+
Test _what_ code does (public interface, observable outputs), not _how_ (internal state, call counts). Implementation-coupled tests break on every refactor.
|
|
54
|
+
|
|
55
|
+
## Factory Pattern
|
|
32
56
|
|
|
33
57
|
```
|
|
34
58
|
function getMockUser(overrides = {}) {
|
|
35
|
-
return {
|
|
36
|
-
id: "user-1",
|
|
37
|
-
email: "test@example.com",
|
|
38
|
-
name: "Test User",
|
|
39
|
-
role: "member",
|
|
40
|
-
createdAt: new Date("2024-01-01"),
|
|
41
|
-
...overrides,
|
|
42
|
-
};
|
|
59
|
+
return { id: "user-1", email: "test@example.com", name: "Test User", role: "member", ...overrides };
|
|
43
60
|
}
|
|
44
|
-
|
|
45
|
-
// Usage: only specify what matters for this test
|
|
46
61
|
const admin = getMockUser({ role: "admin" });
|
|
47
62
|
```
|
|
48
63
|
|
|
49
|
-
|
|
64
|
+
New required fields update the factory once, not every test.
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
- **One test file per module** -- name it `module.test.ext` or `module.spec.ext`
|
|
53
|
-
- **Group related tests** using `describe` blocks
|
|
54
|
-
- **Use descriptive test names** that read as sentences
|
|
66
|
+
## Organization
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
- Co-locate tests or mirror source structure. One test file per module (`module.test.ext`).
|
|
69
|
+
- Group with `describe`, name as sentences: `it("rejects expired tokens")`.
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
- **Mock at boundaries** -- external services only, not internal modules
|
|
60
|
-
- **Keep mocks minimal** -- only mock the methods your code actually calls
|
|
61
|
-
- **Verify mock contracts** -- ensure mocks match real API behavior
|
|
62
|
-
- **Reset mocks between tests** -- prevent shared state and order-dependent failures
|
|
71
|
+
## Shared Principles
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
- **Prefer role-based queries.** Use `getByRole`, `getByLabel`, `getByText` over test IDs or CSS selectors.
|
|
74
|
+
- **Never sleep.** Use framework-provided waiting mechanisms (`waitFor`, `findBy*`, web-first assertions).
|
|
75
|
+
- **One assertion focus per test.** Multiple assertions are fine if they verify facets of the same behavior.
|
|
76
|
+
- **Isolate tests.** No test should depend on another test's side effects. Reset state between tests.
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
|---|---|---|---|
|
|
68
|
-
| **Unit tests** | Single function or class | Fast | Many |
|
|
69
|
-
| **Integration tests** | Multiple components together | Slower | Some |
|
|
70
|
-
| **End-to-end tests** | Full user flow | Slowest | Few |
|
|
78
|
+
## Mocking Rules
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
- Prefer real dependencies (in-memory DB, local server) when feasible.
|
|
81
|
+
- Mock at boundaries only (external APIs, email, payments), not internal modules.
|
|
82
|
+
- Keep mocks minimal -- only methods your code calls. Reset between tests.
|
|
83
|
+
- Verify mocks match the real API. Update when APIs change.
|
|
73
84
|
|
|
74
85
|
## Property-Based Testing
|
|
75
86
|
|
|
76
|
-
When a function has a contract ("for all valid inputs X, property Y holds"), use property-based tests to verify across generated inputs instead of hand-picked examples:
|
|
87
|
+
When a function has a contract ("for all valid inputs X, property Y holds"), use property-based tests to verify it across generated inputs instead of hand-picked examples:
|
|
77
88
|
|
|
78
89
|
```typescript
|
|
79
90
|
// Instead of testing 3-4 specific inputs:
|
|
80
91
|
test("sort is idempotent", () => {
|
|
81
|
-
fc.assert(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
fc.assert(
|
|
93
|
+
fc.property(fc.array(fc.integer()), (arr) => {
|
|
94
|
+
const sorted = mySort(arr);
|
|
95
|
+
expect(mySort(sorted)).toEqual(sorted);
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
85
98
|
});
|
|
86
99
|
```
|
|
87
100
|
|
|
@@ -89,7 +102,7 @@ Good candidates: serialization round-trips, math properties, parsers, encoding/d
|
|
|
89
102
|
|
|
90
103
|
## Bun Test Runner
|
|
91
104
|
|
|
92
|
-
Bun includes a built-in test runner
|
|
105
|
+
Bun includes a built-in test runner. Use `bun test` for projects on the Bun runtime:
|
|
93
106
|
|
|
94
107
|
```bash
|
|
95
108
|
bun test # run all *.test.ts files
|
|
@@ -98,21 +111,30 @@ bun test --coverage # with code coverage
|
|
|
98
111
|
bun test path/to/file.test.ts # single file
|
|
99
112
|
```
|
|
100
113
|
|
|
101
|
-
|
|
114
|
+
Bun supports `describe`, `it`/`test`, `expect`, lifecycle hooks, and snapshot testing out of the box with no additional dependencies.
|
|
115
|
+
|
|
116
|
+
## Test Speed
|
|
117
|
+
|
|
118
|
+
Three levers recur across common runners (Vitest, Playwright, Storybook browser mode). Stated once here; each stack skill carries the framework-specific syntax.
|
|
119
|
+
|
|
120
|
+
- **Parallelism sized to the runner.** Enable file-level parallelism (Vitest `fileParallelism`, Playwright `fullyParallel`), but cap workers to the runner's _real_ vCPUs (`"50%"` or a measured count). GitHub standard runners are 2 vCPU (private) / 4 (public); oversubscribing thrashes.
|
|
121
|
+
- **Shard across CI jobs, then merge the blob reports.** Sharding splits at the _file_ level (`--shard=i/n`), so wall-clock is gated by the slowest shard -- balance file sizes. Always emit a blob report per shard (`--reporter=blob`) and merge it (`merge-reports` / `--merge-reports`) in a dependent job, or results and coverage are fragmented and incomplete.
|
|
122
|
+
- **Skip isolation where state is clean.** Per-file environment isolation is the safe default but the biggest run-speed cost. Disable it (Vitest `isolate: false`, `pool: 'threads'`) only for side-effect-free logic suites; keep it where tests mutate globals/env/timers.
|
|
123
|
+
|
|
124
|
+
Profile before tuning, and never `sleep` to mask timing -- use the framework's auto-waiting (see Shared Principles).
|
|
125
|
+
|
|
126
|
+
Per-runner config (if the stack is installed): `ct-vite-vitest-patterns`, `ct-playwright-patterns`, `ct-storybook-patterns`.
|
|
102
127
|
|
|
103
|
-
## Anti-
|
|
128
|
+
## Anti-Patterns
|
|
104
129
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
| **Ignoring flaky tests** | A sometimes-failing test is either wrong or exposing a real bug. Fix or delete it. |
|
|
112
|
-
| **Testing only the happy path** | Error cases, edge cases, and boundary conditions are where bugs live. |
|
|
130
|
+
1. **Testing implementation** -- Asserting internal state or call counts breaks on refactors.
|
|
131
|
+
2. **Overmocking** -- If most of the test is mock setup, you're testing mocks.
|
|
132
|
+
3. **Copy-paste tests** -- Use factories and parameterized tests.
|
|
133
|
+
4. **No assertions** -- Worse than no test. False confidence.
|
|
134
|
+
5. **Ignoring flaky tests** -- Fix or delete; never skip indefinitely.
|
|
135
|
+
6. **Happy path only** -- Error cases and boundary conditions are where bugs live.
|
|
113
136
|
|
|
114
137
|
## Trigger Conditions
|
|
115
138
|
|
|
116
139
|
- **Keywords:** `test`, `spec`, `tdd`, `mock`, `factory`
|
|
117
140
|
- **File patterns:** `**/*.test.ts`, `**/*.spec.ts`
|
|
118
|
-
- **Excludes:** "e2e", "end-to-end"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TypeScript Conventions
|
|
2
2
|
|
|
3
|
-
> Strict TypeScript patterns for type safety, readability, and maintainable codebases.
|
|
3
|
+
> Strict TypeScript patterns for type safety, readability, and maintainable codebases. Use when writing or reviewing TypeScript, adding or tightening types, or deciding between type and interface
|
|
4
4
|
|
|
5
5
|
**Type:** Core Skill (always included)
|
|
6
6
|
**Source:** [`core/skills/ct-typescript-conventions/SKILL.md`](../core/skills/ct-typescript-conventions/SKILL.md)
|
|
@@ -36,21 +36,29 @@ Never use `any`. It disables type checking entirely.
|
|
|
36
36
|
- Use generics when the type varies but has a consistent shape
|
|
37
37
|
- Use specific types when you know the data shape
|
|
38
38
|
|
|
39
|
-
###
|
|
39
|
+
### Type vs Interface
|
|
40
40
|
|
|
41
|
-
- **
|
|
42
|
-
- **Use `
|
|
41
|
+
- **Default to `type`.** It covers object shapes, unions, intersections, mapped types, and utility types.
|
|
42
|
+
- **Use `interface` only when you need `extends`** (an inheritance hierarchy or declaration merging).
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
|
-
//
|
|
46
|
-
|
|
45
|
+
// Default: use type for object shapes
|
|
46
|
+
type User = {
|
|
47
47
|
id: string;
|
|
48
48
|
email: string;
|
|
49
49
|
role: UserRole;
|
|
50
|
-
}
|
|
50
|
+
};
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Unions, intersections, utilities: type
|
|
53
53
|
type UserRole = "admin" | "member" | "guest";
|
|
54
|
+
|
|
55
|
+
// interface only when you need extends
|
|
56
|
+
interface Animal {
|
|
57
|
+
name: string;
|
|
58
|
+
}
|
|
59
|
+
interface Dog extends Animal {
|
|
60
|
+
breed: string;
|
|
61
|
+
}
|
|
54
62
|
```
|
|
55
63
|
|
|
56
64
|
### `as const` for Literal Types
|
|
@@ -96,7 +104,7 @@ type AsyncState<T> =
|
|
|
96
104
|
|
|
97
105
|
### `satisfies` Operator
|
|
98
106
|
|
|
99
|
-
Validate a value conforms to a type
|
|
107
|
+
Validate a value conforms to a type while keeping the value's own narrower inferred type instead of collapsing to the annotation. Here it preserves the exact key set (`home`, `user`) rather than the index signature `Record<string, Route>`:
|
|
100
108
|
|
|
101
109
|
```typescript
|
|
102
110
|
type Route = { path: string; children?: Route[] };
|
|
@@ -106,7 +114,9 @@ const routes = {
|
|
|
106
114
|
user: { path: "/user/:id", children: [{ path: "settings" }] },
|
|
107
115
|
} satisfies Record<string, Route>;
|
|
108
116
|
|
|
109
|
-
// routes.home.
|
|
117
|
+
// routes.home and routes.user are known keys -- not `Route | undefined` from the index signature.
|
|
118
|
+
// Note: routes.home.path is `string`, NOT the literal "/", because `Route` declares `path: string`,
|
|
119
|
+
// which widens the literal. To keep a literal, target a narrower type (e.g. a union) or add `as const`.
|
|
110
120
|
```
|
|
111
121
|
|
|
112
122
|
### `const` Type Parameters
|
|
@@ -115,6 +125,9 @@ Infer literal types from arguments without requiring callers to write `as const`
|
|
|
115
125
|
|
|
116
126
|
```typescript
|
|
117
127
|
function createConfig<const T extends readonly string[]>(names: T): Record<T[number], boolean> {
|
|
128
|
+
// `Object.fromEntries` is typed to return a wide `Record<string, boolean>`, so a single
|
|
129
|
+
// localized `as` at a generic helper's return boundary is acceptable here -- it keeps the
|
|
130
|
+
// public signature precise. This is the narrow exception to the "no `as` casts" anti-pattern.
|
|
118
131
|
return Object.fromEntries(names.map(n => [n, false])) as Record<T[number], boolean>;
|
|
119
132
|
}
|
|
120
133
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Verification Before Completion
|
|
2
2
|
|
|
3
|
-
> Evidence-based completion claims -- never say "done" without proof that the work is correct.
|
|
3
|
+
> Evidence-based completion claims -- never say "done" without proof that the work is correct. Use when about to claim a task is done, fixed, or passing, or before committing or opening a PR
|
|
4
4
|
|
|
5
5
|
**Type:** Core Skill (always included)
|
|
6
6
|
**Source:** [`core/skills/ct-verification-before-completion/SKILL.md`](../core/skills/ct-verification-before-completion/SKILL.md)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ESNext Idioms
|
|
2
|
+
|
|
3
|
+
> Modern ECMAScript runtime idioms — Temporal, iterator helpers, `using`, structuredClone.
|
|
4
|
+
|
|
5
|
+
**Type:** Stack Skill (requires `esnext` stack)
|
|
6
|
+
**Source:** [`stacks/esnext/skills/ct-esnext-idioms/SKILL.md`](../../stacks/esnext/skills/ct-esnext-idioms/SKILL.md)
|
|
7
|
+
**Detected by:** tsconfig `target`/`module`/`lib` = `ESNext`/`ES2022+`, package.json `"type":"module"`, `engines.node >= 20`, or a `.mjs` file
|
|
8
|
+
**File Extensions:** `.mjs`, `.cjs`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Guidance for preferring current ECMAScript **runtime features** over legacy patterns and hand-rolled polyfills. It is availability-aware: each newer feature notes the runtime/polyfill it needs, so the recommendation never generates code the target can't run. Types are covered by `ct-typescript-conventions`; general code structure by `ct-code-style`.
|
|
13
|
+
|
|
14
|
+
## Topics
|
|
15
|
+
|
|
16
|
+
- **Dates → `Temporal`** — immutable, timezone-correct calendar/clock math (polyfill until baseline).
|
|
17
|
+
- **Resource management → `using` / `await using`** — deterministic cleanup via `Symbol.dispose`/`asyncDispose`.
|
|
18
|
+
- **Iterator helpers** — lazy `map`/`filter`/`take`, `Iterator.concat`, `Array.fromAsync`.
|
|
19
|
+
- **Clone & group** — `structuredClone`, `Object.groupBy` / `Map.groupBy`.
|
|
20
|
+
- **Async ergonomics** — `Promise.withResolvers`, `AbortSignal.timeout`/`any`, top-level await.
|
|
21
|
+
- **Immutable array ops** — `at`, `findLast`, `toSorted`/`toReversed`/`with`.
|
|
22
|
+
- **Errors** — `Error({ cause })`, `AggregateError`.
|
|
23
|
+
- **ESM hygiene** — `import.meta.dirname` over `__dirname`, dynamic `import()`.
|
|
24
|
+
|
|
25
|
+
See the [skill source](../../stacks/esnext/skills/ct-esnext-idioms/SKILL.md) for the full patterns, availability notes, and anti-patterns table.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Reusable Claude Code configuration toolkit with stack-specific connectors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"lint": "biome check --write",
|
|
27
27
|
"lint:check": "biome check",
|
|
28
|
+
"test": "bun test",
|
|
28
29
|
"postinstall": "bun bin/postinstall.mjs || node bin/postinstall.mjs || exit 0",
|
|
29
30
|
"prepare": "npx husky"
|
|
30
31
|
},
|
package/src/detect.ts
CHANGED
|
@@ -12,6 +12,8 @@ interface PackageJson {
|
|
|
12
12
|
dependencies?: Record<string, string>;
|
|
13
13
|
devDependencies?: Record<string, string>;
|
|
14
14
|
workspaces?: string[] | { packages?: string[] };
|
|
15
|
+
type?: string;
|
|
16
|
+
engines?: { node?: string };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** A directory to inspect for stack markers, with its project-relative label */
|
|
@@ -54,6 +56,9 @@ const IGNORE_DIRS = new Set([
|
|
|
54
56
|
"test-results",
|
|
55
57
|
]);
|
|
56
58
|
|
|
59
|
+
/** Node major version at/above which modern ESNext runtime features are assumed available. */
|
|
60
|
+
const MIN_MODERN_NODE_MAJOR = 20;
|
|
61
|
+
|
|
57
62
|
function readPackageJson(dir: string): PackageJson | null {
|
|
58
63
|
const pkgPath = join(dir, "package.json");
|
|
59
64
|
if (!existsSync(pkgPath)) return null;
|
|
@@ -174,6 +179,57 @@ function findProtoFiles(ctx: DetectContext): string | null {
|
|
|
174
179
|
return null;
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
/** Read a file's text, or null if missing/unreadable (tolerates comments/trailing commas). */
|
|
183
|
+
function readTextFile(path: string): string | null {
|
|
184
|
+
try {
|
|
185
|
+
return readFileSync(path, "utf-8");
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* First scan root whose tsconfig.json sets target/module/lib to ESNext or ES2022+.
|
|
193
|
+
* Uses a lenient text scan because tsconfig files routinely carry comments and
|
|
194
|
+
* trailing commas that would break JSON.parse.
|
|
195
|
+
*/
|
|
196
|
+
function tsconfigTargetsEsnext(ctx: DetectContext): string | null {
|
|
197
|
+
const modern = /es(?:next|20(?:2[2-9]|[3-9]\d))/i; // ESNext or ES2022..ES2099
|
|
198
|
+
const keyValue = /"(?:target|module|lib)"\s*:\s*(?:"([^"]*)"|\[([^\]]*)\])/gi;
|
|
199
|
+
for (const { rel, dir } of ctx.scanRoots) {
|
|
200
|
+
const text = readTextFile(join(dir, "tsconfig.json"));
|
|
201
|
+
if (!text) continue;
|
|
202
|
+
for (const m of text.matchAll(keyValue)) {
|
|
203
|
+
if (modern.test(m[1] ?? m[2] ?? "")) return relPath(rel, "tsconfig.json");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Whether a package.json's engines.node floor is >= `major` (first integer in the range). */
|
|
210
|
+
function enginesNodeAtLeast(pkg: PackageJson, major: number): boolean {
|
|
211
|
+
const range = pkg.engines?.node;
|
|
212
|
+
if (!range) return false;
|
|
213
|
+
const m = range.match(/(\d+)/);
|
|
214
|
+
return m ? Number(m[1]) >= major : false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** First `.mjs` file in a bounded search: each scan root's top level + bin/, src/, scripts/. */
|
|
218
|
+
function findMjs(ctx: DetectContext): string | null {
|
|
219
|
+
const subs = ["", "bin", "src", "scripts"];
|
|
220
|
+
for (const { rel, dir } of ctx.scanRoots) {
|
|
221
|
+
for (const sub of subs) {
|
|
222
|
+
const searchDir = sub ? join(dir, sub) : dir;
|
|
223
|
+
if (!existsSync(searchDir)) continue;
|
|
224
|
+
const glob = new Bun.Glob("*.mjs"); // non-recursive: never descends into node_modules
|
|
225
|
+
for (const match of glob.scanSync({ cwd: searchDir, onlyFiles: true })) {
|
|
226
|
+
return relPath(rel, sub ? `${sub}/${match}` : match);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
177
233
|
const DETECTORS: StackDetector[] = [
|
|
178
234
|
{
|
|
179
235
|
name: "solidjs",
|
|
@@ -271,6 +327,24 @@ const DETECTORS: StackDetector[] = [
|
|
|
271
327
|
return config ? { name: "capacitor", reason: `found ${config}` } : null;
|
|
272
328
|
},
|
|
273
329
|
},
|
|
330
|
+
{
|
|
331
|
+
name: "esnext",
|
|
332
|
+
detect: (ctx) => {
|
|
333
|
+
const ts = tsconfigTargetsEsnext(ctx);
|
|
334
|
+
if (ts) return { name: "esnext", reason: `found ESNext target in ${ts}` };
|
|
335
|
+
const mod = ctx.packages.find(({ pkg }) => pkg.type === "module");
|
|
336
|
+
if (mod) return { name: "esnext", reason: `found "type":"module" in ${pkgLabel(mod.rel)}` };
|
|
337
|
+
const eng = ctx.packages.find(({ pkg }) => enginesNodeAtLeast(pkg, MIN_MODERN_NODE_MAJOR));
|
|
338
|
+
if (eng)
|
|
339
|
+
return {
|
|
340
|
+
name: "esnext",
|
|
341
|
+
reason: `found engines.node >=${MIN_MODERN_NODE_MAJOR} in ${pkgLabel(eng.rel)}`,
|
|
342
|
+
};
|
|
343
|
+
const mjs = findMjs(ctx);
|
|
344
|
+
if (mjs) return { name: "esnext", reason: `found ${mjs}` };
|
|
345
|
+
return null;
|
|
346
|
+
},
|
|
347
|
+
},
|
|
274
348
|
];
|
|
275
349
|
|
|
276
350
|
/** Scan a project directory (root + subdirs + workspace packages) and detect which stacks are present */
|
package/src/generator.ts
CHANGED
|
@@ -42,7 +42,7 @@ async function resolveConfig(config: ClaudeToolkitConfig): Promise<ResolvedConfi
|
|
|
42
42
|
"ct-testing-patterns",
|
|
43
43
|
"ct-typescript-conventions",
|
|
44
44
|
"ct-verification-before-completion",
|
|
45
|
-
|
|
45
|
+
"ct-code-style",
|
|
46
46
|
].filter((s) => !config.excludeSkills?.includes(s));
|
|
47
47
|
|
|
48
48
|
return { config, skills, directoryMappings: allMappings, hooks, stacks };
|
|
@@ -79,6 +79,10 @@ async function removeGenerated(claudeDir: string): Promise<void> {
|
|
|
79
79
|
// Toolkit hook files (copied from core/hooks) + the generated skill-rules.json.
|
|
80
80
|
...hookFiles.map((f) => removePath(join(claudeDir, "hooks", f))),
|
|
81
81
|
removePath(join(claudeDir, "hooks", "skill-rules.json")),
|
|
82
|
+
// Stale files from prior installs: skill-eval.js was renamed to .cjs, and the
|
|
83
|
+
// skill-eval.sh wrapper was dropped in favour of exec-form node registration.
|
|
84
|
+
removePath(join(claudeDir, "hooks", "skill-eval.js")),
|
|
85
|
+
removePath(join(claudeDir, "hooks", "skill-eval.sh")),
|
|
82
86
|
]);
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -114,6 +118,16 @@ export async function generate(
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// 5b. Copy stack-specific commands into the ct/ namespace. Only installs when
|
|
122
|
+
// the owning stack is active; removeGenerated() already deletes the whole
|
|
123
|
+
// .claude/commands/ct dir each rebuild, so a removed stack leaves no orphan.
|
|
124
|
+
for (const stackName of config.stacks) {
|
|
125
|
+
const stackCommandsDir = join(TOOLKIT_ROOT, "stacks", stackName, "commands");
|
|
126
|
+
if (exists(stackCommandsDir)) {
|
|
127
|
+
await copyDir(stackCommandsDir, join(claudeDir, "commands", "ct"));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
// 6. Generate skill-rules.json
|
|
118
132
|
await generateSkillRules(claudeDir, resolved);
|
|
119
133
|
|
|
@@ -292,7 +306,8 @@ async function generateSettings(claudeDir: string, resolved: ResolvedConfig): Pr
|
|
|
292
306
|
hooks: [
|
|
293
307
|
{
|
|
294
308
|
type: "command",
|
|
295
|
-
|
|
309
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Claude Code substitutes ${CLAUDE_PROJECT_DIR} at hook-run time; this must be a literal placeholder string.
|
|
310
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/skill-eval.cjs"',
|
|
296
311
|
timeout: 5,
|
|
297
312
|
},
|
|
298
313
|
],
|
|
@@ -360,6 +375,8 @@ async function generateSkillsReadme(claudeDir: string, resolved: ResolvedConfig)
|
|
|
360
375
|
content += "| ct-testing-patterns | Test-driven development workflow and patterns |\n";
|
|
361
376
|
content += "| ct-typescript-conventions | TypeScript strict mode and conventions |\n";
|
|
362
377
|
content += "| ct-verification-before-completion | Evidence-based completion claims protocol |\n";
|
|
378
|
+
content +=
|
|
379
|
+
"| ct-code-style | Code structure & style: guard clauses, lookup tables, no magic values |\n";
|
|
363
380
|
content += "\n## Stack Skills\n\n";
|
|
364
381
|
content += "| Skill | Stack | Description |\n";
|
|
365
382
|
content += "|-------|-------|-------------|\n";
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ct-esnext-idioms
|
|
3
|
+
description: Modern ECMAScript runtime idioms — Temporal, iterator helpers, using, structuredClone
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Modern ESNext Idioms
|
|
7
|
+
|
|
8
|
+
Prefer current ECMAScript runtime features over legacy patterns and hand-rolled polyfills. Types live in `ct-typescript-conventions`; code structure lives in `ct-code-style`; this skill is about *runtime features*.
|
|
9
|
+
|
|
10
|
+
> Some features below are newer than the Node/browser baseline. Each is tagged with what it needs. When a target can't guarantee it, use the noted polyfill/flag rather than dropping back to the legacy pattern.
|
|
11
|
+
|
|
12
|
+
## Dates & time → `Temporal`
|
|
13
|
+
|
|
14
|
+
`Date` is mutable, timezone-naive, and error-prone for math. Use `Temporal` for calendar/clock work.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const today = Temporal.Now.plainDateISO(); // no time, no tz confusion
|
|
18
|
+
const due = today.add({ days: 30 }); // immutable — returns a new value
|
|
19
|
+
const start = Temporal.ZonedDateTime.from("2026-07-04T09:00[Europe/Paris]");
|
|
20
|
+
const mins = start.until(Temporal.Now.zonedDateTimeISO()).total("minutes");
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
_Availability: Stage 3 — not yet baseline in Node. Add `temporal-polyfill` (or `@js-temporal/polyfill`) until it ships._ Avoid `Date` mutation, manual `getTime()` ms math, and moment.js.
|
|
24
|
+
|
|
25
|
+
## Deterministic cleanup → `using` / `await using`
|
|
26
|
+
|
|
27
|
+
`Symbol.dispose` / `Symbol.asyncDispose` free resources when scope exits — no `try/finally` ladder.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const openFile = (path: string) => {
|
|
31
|
+
const handle = fs.openSync(path, "r");
|
|
32
|
+
return { handle, [Symbol.dispose]: () => fs.closeSync(handle) };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const readHead = (path: string) => {
|
|
36
|
+
using file = openFile(path); // closed automatically at end of block
|
|
37
|
+
return fs.readSync(file.handle, Buffer.alloc(16), 0, 16, 0);
|
|
38
|
+
}; // ← dispose runs here, even on throw
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`await using` awaits `Symbol.asyncDispose` (db connections, locks, spans). _Availability: TS 5.2+ (downlevels) or Node 24+._
|
|
42
|
+
|
|
43
|
+
## Iterator helpers → lazy, no intermediate arrays
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const firstThree = users.values()
|
|
47
|
+
.filter((u) => u.active)
|
|
48
|
+
.map((u) => u.name)
|
|
49
|
+
.take(3)
|
|
50
|
+
.toArray(); // only 3 names ever materialised
|
|
51
|
+
|
|
52
|
+
const merged = Iterator.concat(pageA.values(), pageB.values()); // Iterator Sequencing — Stage 4 (2025); polyfill on older runtimes
|
|
53
|
+
const rows = await Array.fromAsync(streamRows()); // async iterable → array
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
_Availability: standard iterator helpers (`.filter`/`.map`/`.take`) and `Array.fromAsync` are Node 22+ / modern browsers; `Iterator.concat` is newer (Iterator Sequencing, 2025) — polyfill until it lands._ Replaces `[...a, ...b].filter().map().slice()`, which allocates at every step.
|
|
57
|
+
|
|
58
|
+
## Clone & group
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const copy = structuredClone(state); // deep clone — Map/Set/Date/typed arrays
|
|
62
|
+
const byRole = Object.groupBy(users, (u) => u.role); // { admin: [...], guest: [...] }
|
|
63
|
+
const byId = Map.groupBy(users, (u) => u.id); // Map keys (any type)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Avoid `JSON.parse(JSON.stringify(x))` (drops `undefined`, turns `Date` into a string, throws on cycles) and manual `reduce` grouping.
|
|
67
|
+
|
|
68
|
+
## Promise & async ergonomics
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const { promise, resolve, reject } = Promise.withResolvers<Data>(); // no deferred-hack closure
|
|
72
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5_000) }); // auto-abort
|
|
73
|
+
const signal = AbortSignal.any([userAbort, AbortSignal.timeout(30_000)]);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Top-level `await` is available in ESM modules — no IIFE wrapper.
|
|
77
|
+
|
|
78
|
+
## Immutable array ops
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const last = items.at(-1); // negative index
|
|
82
|
+
const newest = orders.findLast((o) => o.settled);
|
|
83
|
+
const sorted = scores.toSorted((a, b) => b - a); // non-mutating; also toReversed/toSpliced/with
|
|
84
|
+
const patched = row.with(2, "x"); // copy with index 2 replaced
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Errors with cause
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
try {
|
|
91
|
+
await save();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error("Checkout failed", { cause: err }); // preserve the chain
|
|
94
|
+
}
|
|
95
|
+
throw new AggregateError(errors, "Some uploads failed"); // aggregate independent failures
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## ESM hygiene
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const here = import.meta.dirname; // replaces __dirname (Node 20.11+/21.2+)
|
|
102
|
+
const url = import.meta.url;
|
|
103
|
+
const mod = await import("./plugin.js"); // dynamic import, not require()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Performance
|
|
107
|
+
|
|
108
|
+
> _Verified against ECMAScript 2024 / Node 22 (2026-07)._
|
|
109
|
+
|
|
110
|
+
- **Iterator helpers are lazy** — `take(3)` stops the pipeline after 3 items; the array form processes everything first. Use them for early-exit and large/infinite sequences.
|
|
111
|
+
- **`structuredClone` is a native deep copy** — faster and more correct than `JSON.parse(JSON.stringify())`, with no string round-trip.
|
|
112
|
+
- **`AbortSignal.timeout`** removes `setTimeout` bookkeeping and leaks; the runtime clears the timer when the signal settles.
|
|
113
|
+
|
|
114
|
+
## Anti-Patterns
|
|
115
|
+
|
|
116
|
+
| Legacy | Modern |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `new Date()` math / moment.js | `Temporal` (via polyfill) |
|
|
119
|
+
| `try { } finally { close() }` | `using` / `await using` |
|
|
120
|
+
| `[...a, ...b].filter().map().slice()` | iterator helpers + `Iterator.concat` |
|
|
121
|
+
| `JSON.parse(JSON.stringify(x))` | `structuredClone(x)` |
|
|
122
|
+
| manual `reduce` grouping | `Object.groupBy` / `Map.groupBy` |
|
|
123
|
+
| deferred `let resolve; new Promise(...)` | `Promise.withResolvers()` |
|
|
124
|
+
| `arr[arr.length - 1]` | `arr.at(-1)` |
|
|
125
|
+
| `__dirname` / `require()` | `import.meta.dirname` / `import()` |
|
|
126
|
+
|
|
127
|
+
## See Also
|
|
128
|
+
|
|
129
|
+
- `ct-typescript-conventions` — typing these values (`satisfies`, discriminated unions for async state).
|
|
130
|
+
- `ct-code-style` — structure around these features (guard clauses, no magic numbers, `??`).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ct-esnext-idioms",
|
|
3
|
+
"description": "Modern ECMAScript runtime idioms — Temporal, iterator helpers, using, structuredClone",
|
|
4
|
+
"defaultMappings": {},
|
|
5
|
+
"fileExtensions": ["mjs", "cjs"],
|
|
6
|
+
"skillRules": {
|
|
7
|
+
"ct-esnext-idioms": {
|
|
8
|
+
"description": "Modern ECMAScript runtime idioms and features",
|
|
9
|
+
"priority": 5,
|
|
10
|
+
"triggers": {
|
|
11
|
+
"keywords": [
|
|
12
|
+
"Temporal",
|
|
13
|
+
"structuredClone",
|
|
14
|
+
"iterator",
|
|
15
|
+
"using",
|
|
16
|
+
"dispose",
|
|
17
|
+
"groupBy",
|
|
18
|
+
"withResolvers",
|
|
19
|
+
"AbortSignal",
|
|
20
|
+
"fromAsync",
|
|
21
|
+
"esnext",
|
|
22
|
+
"moment",
|
|
23
|
+
"date"
|
|
24
|
+
],
|
|
25
|
+
"keywordPatterns": ["\\bTemporal\\b", "\\busing\\b", "\\bstructuredClone\\b"],
|
|
26
|
+
"contentPatterns": [
|
|
27
|
+
"new Date\\(",
|
|
28
|
+
"JSON\\.parse\\(JSON\\.stringify",
|
|
29
|
+
"__dirname",
|
|
30
|
+
"\\.groupBy\\("
|
|
31
|
+
],
|
|
32
|
+
"intentPatterns": ["(?:format|parse|manipulate).*date", "(?:clone|copy).*(?:object|deep)"]
|
|
33
|
+
},
|
|
34
|
+
"relatedSkills": ["ct-typescript-conventions", "ct-code-style"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|