claude-toolkit 0.15.0 → 0.17.0-rc.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +55 -14
  3. package/bin/cli.ts +75 -19
  4. package/core/skills/ct-typescript-conventions/SKILL.md +59 -1
  5. package/package.json +6 -7
  6. package/src/generator.ts +66 -27
  7. package/src/refresh-hint.ts +95 -0
  8. package/templates/skill-rules.base.json +29 -6
  9. package/bin/postinstall.mjs +0 -69
  10. package/docs/README.md +0 -59
  11. package/docs/agents/code-reviewer.md +0 -76
  12. package/docs/agents/github-workflow.md +0 -98
  13. package/docs/best-practices/capacitor/README.md +0 -64
  14. package/docs/best-practices/capacitor/capacitor-8.md +0 -60
  15. package/docs/best-practices/capacitor/capgo-setup.md +0 -151
  16. package/docs/best-practices/capacitor/channels-and-rollouts.md +0 -101
  17. package/docs/best-practices/capacitor/live-updates-ota.md +0 -75
  18. package/docs/best-practices/capacitor/security-encryption.md +0 -68
  19. package/docs/best-practices/solidjs/README.md +0 -43
  20. package/docs/best-practices/solidjs/anti-patterns.md +0 -166
  21. package/docs/best-practices/solidjs/component-patterns.md +0 -131
  22. package/docs/best-practices/solidjs/context-and-global-state.md +0 -131
  23. package/docs/best-practices/solidjs/control-flow.md +0 -124
  24. package/docs/best-practices/solidjs/data-fetching.md +0 -205
  25. package/docs/best-practices/solidjs/effects-and-lifecycle.md +0 -113
  26. package/docs/best-practices/solidjs/performance.md +0 -100
  27. package/docs/best-practices/solidjs/props-patterns.md +0 -100
  28. package/docs/best-practices/solidjs/reactivity-model.md +0 -104
  29. package/docs/best-practices/solidjs/signals-and-state.md +0 -78
  30. package/docs/best-practices/solidjs/stores-and-nested-state.md +0 -111
  31. package/docs/best-practices/solidjs/typescript-integration.md +0 -186
  32. package/docs/best-practices/testing/README.md +0 -84
  33. package/docs/best-practices/testing/playwright-e2e.md +0 -649
  34. package/docs/best-practices/testing/storybook-interaction.md +0 -455
  35. package/docs/best-practices/testing/vitest-unit.md +0 -451
  36. package/docs/best-practices/typescript/README.md +0 -45
  37. package/docs/best-practices/typescript/any-and-unknown.md +0 -73
  38. package/docs/best-practices/typescript/deriving-vs-decoupling.md +0 -83
  39. package/docs/best-practices/typescript/discriminated-unions.md +0 -75
  40. package/docs/best-practices/typescript/enums-alternatives.md +0 -72
  41. package/docs/best-practices/typescript/essential-patterns.md +0 -119
  42. package/docs/best-practices/typescript/generics-patterns.md +0 -105
  43. package/docs/best-practices/typescript/micro-opinions.md +0 -87
  44. package/docs/best-practices/typescript/runtime-validation.md +0 -62
  45. package/docs/best-practices/typescript/satisfies-operator.md +0 -100
  46. package/docs/best-practices/typescript/tsconfig-cheat-sheet.md +0 -129
  47. package/docs/best-practices/typescript/type-organization.md +0 -64
  48. package/docs/best-practices/typescript/type-vs-interface.md +0 -80
  49. package/docs/commands/code-quality.md +0 -36
  50. package/docs/commands/onboard.md +0 -63
  51. package/docs/commands/pr-review.md +0 -91
  52. package/docs/commands/pr-summary.md +0 -50
  53. package/docs/commands/proto-check.md +0 -59
  54. package/docs/commands/ticket.md +0 -56
  55. package/docs/skills/code-style.md +0 -26
  56. package/docs/skills/systematic-debugging.md +0 -70
  57. package/docs/skills/testing-patterns.md +0 -140
  58. package/docs/skills/typescript-conventions.md +0 -191
  59. package/docs/skills/verification-before-completion.md +0 -103
  60. package/docs/stacks/capacitor-ota.md +0 -145
  61. package/docs/stacks/cloudflare-d1-kv.md +0 -144
  62. package/docs/stacks/esnext-idioms.md +0 -25
  63. package/docs/stacks/i18n-typesafe.md +0 -141
  64. package/docs/stacks/playwright-patterns.md +0 -179
  65. package/docs/stacks/protobuf-contracts.md +0 -85
  66. package/docs/stacks/rust-wasm-patterns.md +0 -106
  67. package/docs/stacks/solidjs-patterns.md +0 -110
  68. package/docs/stacks/storybook-patterns.md +0 -177
  69. package/docs/stacks/vanilla-extract-patterns.md +0 -115
  70. package/docs/stacks/vite-vitest-patterns.md +0 -485
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 (unreleased)
4
+
5
+ Nothing runs at install time any more. The `postinstall` script that regenerated `.claude/` is gone; projects opt in with one line in their own `package.json`, and the new `claude-toolkit refresh` does the work. The package also stops shipping `docs/`, and releases move to npm trusted publishing behind a tarball guard.
6
+
7
+ **BREAKING:** installing or upgrading claude-toolkit no longer regenerates `.claude/`. After upgrading, run `bunx claude-toolkit` once, then add to your `package.json` scripts:
8
+
9
+ "prepare": "claude-toolkit refresh || exit 0"
10
+
11
+ This line is **required** when `.claude/` is gitignored (the README's recommended setup): without it a fresh clone has no `.claude/` until someone runs `bunx claude-toolkit`. Yarn 2+ never runs `prepare` for your own project — use `"postinstall"` there. npm runs `prepare` only on a plain `npm install` and pnpm skips it on `pnpm add`; after those, run `bunx claude-toolkit refresh`. If you added `claude-toolkit` to Bun's `trustedDependencies` or pnpm's `onlyBuiltDependencies`, remove it.
12
+
13
+ - feat!: remove the install-time `postinstall` script (`bin/postinstall.mjs`) and the internal `postinstall` command. The package now ships no install scripts and no `child_process` or `process.env` use.
14
+ - feat: `claude-toolkit refresh [dir]` regenerates `.claude/` only when the installed toolkit version differs from `.claude/.toolkit-version`; the up-to-date path never imports the config. It never creates the config and never scaffolds `biome.json`/`tsconfig.json`; a config that fails to load exits 1. Accepts `claude-toolkit.config.ts` or `claude-toolkit.config.js`.
15
+ - feat: every non-quiet `bunx claude-toolkit` run ends with the `prepare` line until `package.json` has a script running `claude-toolkit refresh` — with a named-script variant when `prepare` is already taken, `postinstall` for Yarn 2+ (`.yarnrc.yml`), the npm and pnpm cases where `prepare` does not run, and a leading line when upgrading from 0.16 or earlier. The CLI never edits `package.json`.
16
+ - fix(settings): the protected-branch guard blocked every edit on every branch when two or more `git.protectedBranches` were configured (`[` does not accept `&&`). It is now a `case` statement; branch names are validated against `^[A-Za-z0-9._/][A-Za-z0-9._/-]*$`, and an invalid name fails generation with an error naming it. An empty list now generates no guard instead of blocking every edit.
17
+ - build: stop shipping `docs/` (about 270 KB); the README links the docs on GitHub. `prepare` runs the husky 9 binary instead of `npx husky`. Dropped the dead pre-0.11 `skill-eval.sh` cleanup.
18
+ - ci: publish with npm trusted publishing (OIDC, automatic provenance, no token secret); prerelease versions go to the `next` dist-tag, and the workflow can be re-run by hand on a tag. Before publishing it runs typecheck, lint and tests, strips `prepare`, and runs `check:pack --publish` (no install scripts, no shell or environment access in shipped code, no `docs/`) and `check:smoke` (installs the packed tarball and runs `refresh`). A new CI workflow runs the checks on every push and pull request.
19
+ - build: the post-commit version hook no longer bumps prerelease versions (it turned `0.17.0-rc.0` into `0.17.NaN`); `@types/bun` is pinned to `^1.3.11`.
20
+ - docs: README documents `refresh`, the `prepare` opt-in per package manager, upgrading from 0.16, and the release-candidate flow; the auto-format, auto-test and type-check hooks are marked not yet functional (fixed in 0.18.0). CLAUDE.md drops the postinstall description and corrects the post-commit and `core/hooks` notes.
21
+ - fix(package): `bin` is now `bin/cli.ts` without a leading `./`. npm 11.10 and later silently drop a `./`-prefixed `bin` entry at publish time (`npm pack` keeps it), which would have shipped 0.17.0 without its CLI; `check:pack` now rejects such paths. `repository.url` uses the `git+https` form npm normalizes to.
22
+
23
+ ## 0.16.0 (2026-08-08)
24
+
25
+ `ct-typescript-conventions` now selects a compiler as well as types: prefer the TypeScript 7 native compiler where a project already ships it, fall back cleanly where it does not, and never upgrade a project unprompted. Availability-aware in the same spirit as `ct-esnext-idioms`.
26
+
27
+ - feat: prioritise the TypeScript 7 native compiler in `ct-typescript-conventions` — TS7 reached GA on 2026-07-08 as `typescript@7.0.2`, with Microsoft reporting 8x–12x faster full builds. The skill detects the *installed* version (`require('typescript/package.json').version`) rather than the declared semver range, which `overrides`/`resolutions`/catalogs routinely defeat, and names the three detection traps: the binary is `tsc` not `tsgo` (that was the pre-GA `@typescript/native-preview` name), range-sniffing is unreliable, and an API probe gives a false negative because TS7 exposes only `version`/`versionMajorMinor`.
28
+ - feat: document when *not* to switch — TS7.0 ships no stable programmatic API, so `typescript-eslint`, `ts-jest`, `ts-node`, `vue-tsc`, `svelte-check`, `@astrojs/check` and `@angular/compiler-cli` break at install or lint time rather than at `tsc --noEmit`, which means a green type-check proves nothing. The skill prescribes the documented side-by-side alias install instead of a downgrade.
29
+ - feat: add the TS7 `tsconfig.json` deltas — the options TS7 rejects outright (`ignoreDeprecations` no longer silences them), plus the two changed defaults behind most real breakage. Verified empirically against 5.9.3 / 6.0.3 / 7.0.2: both defaults land in 6.0 and carry forward, `types` fails `TS2591` even with `@types/node` installed, `rootDir` only bites when an `outDir` is set, and `types: ["*"]` is `TS2688` on 5.x — so no literal value is safe across majors.
30
+ - feat: skill triggers now fire on typecheck, tsconfig, and compiler work — added the `tsc`, `tsgo`, `tsconfig`, `typecheck` and `compiler` keywords, the `**/tsconfig*.json` path pattern, and intents for running a typecheck or upgrading TypeScript.
31
+ - docs: TSConfig cheat sheet gains a TypeScript 7 delta section; the skill's new remit is reflected in the generated skills index, README, and docs index.
32
+
3
33
  ## 0.15.0 (2026-07-04)
4
34
 
5
35
  Two new authoring-convention assets — an opt-in `esnext` stack and an always-on `ct-code-style` core skill — plus Biome enforcement of the mechanically-checkable style rules.
package/README.md CHANGED
@@ -18,12 +18,31 @@ bun add -d claude-toolkit
18
18
  bunx claude-toolkit
19
19
  ```
20
20
 
21
+ Then add one line to your `package.json` so `.claude/` is rebuilt after toolkit upgrades and on fresh clones. It is **required** when `.claude/` is gitignored (the setup recommended below):
22
+
23
+ ```json
24
+ {
25
+ "scripts": {
26
+ "prepare": "claude-toolkit refresh || exit 0"
27
+ }
28
+ }
29
+ ```
30
+
31
+ claude-toolkit itself runs nothing at install time: this is your project's own script, and `|| exit 0` keeps installs working where bun is missing. If `prepare` is already taken, add `"claude-toolkit:refresh": "claude-toolkit refresh || exit 0"` and append `&& bun run claude-toolkit:refresh` (or `npm run`/`pnpm run`/`yarn run`) to your `prepare`. Until one of these is in place, `bunx claude-toolkit` prints the exact line for your project.
32
+
33
+ When your package manager runs it:
34
+
35
+ - **bun** runs `prepare` on `bun install`, `bun add` and `bun update`.
36
+ - **npm** runs it only on a plain `npm install`. After `npm install <pkg>` or `npm update`, run `bunx claude-toolkit refresh`.
37
+ - **pnpm** runs it on `pnpm install` and `pnpm update`, not on `pnpm add`. After `pnpm add`, run `bunx claude-toolkit refresh`.
38
+ - **Yarn 2+** never runs `prepare` for your own project. Use `"postinstall": "claude-toolkit refresh || exit 0"` instead; it runs whenever your dependencies change. If you publish the package, don't ship that `postinstall` — run `bunx claude-toolkit refresh` by hand instead.
39
+
21
40
  ## How It Works
22
41
 
23
42
  1. Run `bunx claude-toolkit` — on the first run it creates a `claude-toolkit.config.ts` at your project root, pre-filled with the stacks it detects
24
43
  2. It generates a `.claude/` directory with skills, hooks, commands, and agents — regenerated cleanly each run, so stacks you remove leave no stale skills behind
25
44
  3. Claude Code picks up the generated config automatically
26
- 4. `.claude/` regenerates automatically on install whenever the toolkit version changes, so upgrades land without running anything
45
+ 4. After a toolkit upgrade, the `prepare` line from Quick Start runs `claude-toolkit refresh`, which rebuilds `.claude/` only when the installed version differs from the one that generated it. Nothing else runs at install time
27
46
 
28
47
  ```ts
29
48
  import { defineConfig } from "claude-toolkit";
@@ -46,13 +65,14 @@ export default defineConfig({
46
65
 
47
66
  ## CLI Commands
48
67
 
49
- | Command | Description |
50
- | ------------------------------ | ----------------------------------------------------------------------- |
51
- | `bunx claude-toolkit` | Create the config if missing, then (re)generate `.claude/`. Idempotent. |
52
- | `bunx claude-toolkit --update` | Same, and also add newly-detected stacks to your config |
53
- | `bunx claude-toolkit help` | Show available commands |
68
+ | Command | Description |
69
+ | ----------------------------------- | ---------------------------------------------------------------------------------------------------- |
70
+ | `bunx claude-toolkit` | Create the config if missing, then (re)generate `.claude/`. Idempotent. |
71
+ | `bunx claude-toolkit --update` | Same, and also add newly-detected stacks to your config |
72
+ | `bunx claude-toolkit refresh [dir]` | Regenerate `.claude/` only if the toolkit version changed. Never creates the config or other files. |
73
+ | `bunx claude-toolkit help` | Show available commands |
54
74
 
55
- Aliases (back-compat): `init` is a friendly name for the bare command, `update` equals `--update`, and `sync` is deprecated — use the bare command. `.claude/` also regenerates automatically on install when the toolkit version changes.
75
+ Aliases (back-compat): `init` is a friendly name for the bare command, `update` equals `--update`, and `sync` is deprecated — use the bare command. `refresh` is what your `prepare` script runs: with no config it prints one setup line and exits 0, a config that fails to load exits 1, and `--quiet` silences it.
56
76
 
57
77
  ## Stack Auto-Detection
58
78
 
@@ -91,7 +111,7 @@ Generated .claude/ with 4 stack(s) and 4 core skills
91
111
 
92
112
  Stacks in your config that are no longer detected are reported but left unchanged (remove them manually if intended).
93
113
 
94
- **Automatic regeneration** — when the installed toolkit version changes, `.claude/` is regenerated on install automatically (via a `postinstall` hook), so a toolkit upgrade ships its updated skills without you running anything. This only refreshes the generated `.claude/` — it never creates or edits committed files.
114
+ **Keeping `.claude/` in sync** — `claude-toolkit refresh` compares the installed toolkit version with `.claude/.toolkit-version` and regenerates only when they differ; when they match it returns without loading your config. It never creates the config and never scaffolds `biome.json`/`tsconfig.json`. Run it from your `prepare` script (see Quick Start).
95
115
 
96
116
  ## Available Stacks
97
117
 
@@ -113,15 +133,15 @@ Stacks in your config that are no longer detected are reported but left unchange
113
133
 
114
134
  - **Skill Evaluation Engine** — Analyzes prompts and suggests relevant skills
115
135
  - **Main Branch Protection** — Prevents accidental edits on protected branches
116
- - **Auto-formatting** — Formats files after edits
117
- - **Auto-testing** — Runs related tests when test files change
118
- - **Type checking** — Checks TypeScript types on save
136
+ - **Auto-formatting** — Formats files after edits _(not yet functional — fixed in 0.18.0)_
137
+ - **Auto-testing** — Runs related tests when test files change _(not yet functional — fixed in 0.18.0)_
138
+ - **Type checking** — Checks TypeScript types on save _(not yet functional — fixed in 0.18.0)_
119
139
 
120
140
  ### Core Skills
121
141
 
122
142
  - `ct-systematic-debugging` — Four-phase debugging methodology
123
143
  - `ct-testing-patterns` — TDD workflow and patterns
124
- - `ct-typescript-conventions` — TypeScript strict mode best practices
144
+ - `ct-typescript-conventions` — TypeScript strict mode best practices, and preferring the TS7 native compiler where available
125
145
  - `ct-verification-before-completion` — Evidence-based completion claims
126
146
  - `ct-code-style` — Code structure & style: guard clauses, lookup tables, no magic values
127
147
 
@@ -149,6 +169,8 @@ Add `.claude/` to your `.gitignore` (it's generated, not tracked):
149
169
  CLAUDE.local.md
150
170
  ```
151
171
 
172
+ With `.claude/` gitignored, a fresh clone has no `.claude/` until something generates it, so the `prepare` line from Quick Start is **required** in this setup.
173
+
152
174
  Track the config file and CLAUDE.md:
153
175
 
154
176
  ```
@@ -156,9 +178,17 @@ claude-toolkit.config.ts # tracked — your project's Claude config
156
178
  CLAUDE.md # tracked — project-specific documentation
157
179
  ```
158
180
 
181
+ ## Upgrading from 0.16 or earlier
182
+
183
+ 0.17.0 removed the install-time `postinstall` script: installing or upgrading claude-toolkit no longer runs anything. After upgrading:
184
+
185
+ 1. Run `bunx claude-toolkit` once to regenerate `.claude/`.
186
+ 2. Add `"prepare": "claude-toolkit refresh || exit 0"` to your `package.json` scripts (the CLI prints the exact line for your project, including the Yarn 2+ and existing-`prepare` variants).
187
+ 3. If you added `claude-toolkit` to Bun's `trustedDependencies` or pnpm's `onlyBuiltDependencies` so its install script could run, remove it — there is no install script any more.
188
+
159
189
  ## Documentation
160
190
 
161
- Full reference documentation for all skills, commands, and agents is available in the [`docs/`](docs/README.md) directory.
191
+ Full reference documentation for all skills, commands, and agents lives in the [`docs/` directory on GitHub](https://github.com/william-queant/claude-toolkit/blob/main/docs/README.md). It is not included in the npm package.
162
192
 
163
193
  ## Versioning
164
194
 
@@ -167,7 +197,15 @@ Version bumps are derived from your commit messages (Conventional Commits) by a
167
197
  - `feat:` → minor · `fix:` / `perf:` → patch · `feat!:` / `BREAKING CHANGE` → major (capped to minor while pre-1.0)
168
198
  - `docs:` / `chore:` / `refactor:` / `style:` / `test:` / `ci:` / `build:` → no version change
169
199
 
170
- To set an exact version deliberately (a re-baseline, or `1.0.0`), edit `package.json` and prepend a `CHANGELOG.md` entry, then commit with `SKIP_POST_COMMIT=1` so the hook doesn't re-bump, and tag `vX.Y.Z`. Publishing to npm happens automatically when a GitHub Release is published (see `.github/workflows/publish.yml`).
200
+ To set an exact version deliberately (a re-baseline, a release candidate, or `1.0.0`), edit `package.json` and the `CHANGELOG.md` entry, then commit with `SKIP_POST_COMMIT=1` so the hook doesn't re-bump. While `package.json` holds a prerelease version (for example `0.17.0-rc.0`), the hook never bumps.
201
+
202
+ Releases go through a release candidate:
203
+
204
+ 1. Set `X.Y.0-rc.N`, commit with `SKIP_POST_COMMIT=1`, and publish a GitHub **pre-release** for tag `vX.Y.0-rc.N`. `.github/workflows/publish.yml` publishes it to the `next` dist-tag.
205
+ 2. Verify the published candidate; fix and cut `rc.N+1` if needed.
206
+ 3. Set `X.Y.0`, commit with `SKIP_POST_COMMIT=1`, and publish a normal GitHub Release for `vX.Y.0`. It goes to `latest`.
207
+
208
+ Publishing uses npm trusted publishing (OIDC): there is no npm token secret, and provenance is automatic. Before publishing, the workflow runs typecheck, lint and tests, strips the dev-only `prepare` script, then runs `bun run check:pack --publish` (no install scripts, no shell or environment access in shipped code, no `docs/`) and `bun run check:smoke` (installs the packed tarball and runs `claude-toolkit refresh`). `.github/workflows/ci.yml` runs typecheck, lint, tests, `check:pack` and `check:smoke` on every push and pull request.
171
209
 
172
210
  ## Development
173
211
 
@@ -175,4 +213,7 @@ To set an exact version deliberately (a re-baseline, or `1.0.0`), edit `package.
175
213
  git clone https://github.com/william-queant/claude-toolkit.git
176
214
  cd claude-toolkit
177
215
  bun install
216
+ bun test # test suite
217
+ bun run check:pack # tarball guard
218
+ bun run check:smoke # pack, install into a temp project, run refresh
178
219
  ```
package/bin/cli.ts CHANGED
@@ -6,24 +6,34 @@
6
6
  * One idempotent command does it all:
7
7
  * bunx claude-toolkit Create the config if missing, then (re)generate .claude/
8
8
  * bunx claude-toolkit --update Also pull newly-detected stacks into the config
9
+ * bunx claude-toolkit refresh Regenerate .claude/ only if the toolkit version changed
9
10
  *
10
11
  * Aliases / back-compat:
11
12
  * init Friendly name for the first run (same as the bare command)
12
13
  * update Same as `--update`
13
14
  * sync Deprecated alias of the bare command
14
15
  *
15
- * .claude/ is also regenerated automatically on install when the installed
16
- * toolkit version changes (see bin/postinstall.mjs).
16
+ * Nothing runs at install time. Consumers keep .claude/ in sync by adding
17
+ * "prepare": "claude-toolkit refresh || exit 0" to their own package.json scripts;
18
+ * the bare command prints that hint until they do.
17
19
  */
18
20
 
19
21
  import { existsSync } from "node:fs";
20
22
  import { readFile, writeFile } from "node:fs/promises";
21
- import { join, resolve } from "node:path";
23
+ import { basename, join, resolve } from "node:path";
22
24
  import { detectStacks } from "../src/detect.js";
23
- import { generate } from "../src/generator.js";
25
+ import { generate, readMarker, readToolkitVersion } from "../src/generator.js";
26
+ import { buildRefreshHint } from "../src/refresh-hint.js";
24
27
  import type { ClaudeToolkitConfig } from "../src/types.js";
25
28
 
26
29
  const CONFIG_FILENAME = "claude-toolkit.config.ts";
30
+ /** Config files `loadConfig` and `refresh` accept, in lookup order. */
31
+ const CONFIG_FILENAMES = [CONFIG_FILENAME, "claude-toolkit.config.js"];
32
+
33
+ /** Path of the project's config file, or null when there is none. */
34
+ function findConfigPath(projectDir: string): string | null {
35
+ return CONFIG_FILENAMES.map((name) => join(projectDir, name)).find((p) => existsSync(p)) ?? null;
36
+ }
27
37
 
28
38
  /** Build a `stacks: [...]` literal for injection into the config file. */
29
39
  function buildStacksLiteral(stacks: string[]): string {
@@ -44,16 +54,16 @@ async function updateConfigStacks(configPath: string, stacks: string[]): Promise
44
54
  }
45
55
 
46
56
  async function loadConfig(projectDir: string): Promise<ClaudeToolkitConfig> {
47
- const configPath = join(projectDir, CONFIG_FILENAME);
48
- if (!existsSync(configPath)) {
49
- throw new Error(`Config not found: ${configPath}\nRun "bunx claude-toolkit" first.`);
57
+ const configPath = findConfigPath(projectDir);
58
+ if (configPath === null) {
59
+ throw new Error(`Config not found in ${projectDir}\nRun "bunx claude-toolkit" first.`);
50
60
  }
51
61
  let mod: { default?: ClaudeToolkitConfig };
52
62
  try {
53
63
  mod = await import(configPath);
54
64
  } catch (err) {
55
65
  throw new Error(
56
- `Failed to load ${CONFIG_FILENAME}. Make sure claude-toolkit is installed in this project ` +
66
+ `Failed to load ${basename(configPath)}. Make sure claude-toolkit is installed in this project ` +
57
67
  `(e.g. "bun add -d claude-toolkit"), then run "bunx claude-toolkit" again.\n ${(err as Error).message}`,
58
68
  );
59
69
  }
@@ -104,11 +114,14 @@ interface RunOptions {
104
114
  async function run(projectDir: string, options: RunOptions = {}): Promise<void> {
105
115
  const { update = false, quiet = false } = options;
106
116
  const log = quiet ? (_msg = "") => {} : (msg = "") => console.log(msg);
107
- const configPath = join(projectDir, CONFIG_FILENAME);
117
+ const existingConfig = findConfigPath(projectDir);
118
+ const configPath = existingConfig ?? join(projectDir, CONFIG_FILENAME);
119
+ // Read before generate() rewrites it: a pre-0.17 marker means this run is an upgrade.
120
+ const previousVersion = await readMarker(projectDir);
108
121
 
109
122
  let config: ClaudeToolkitConfig;
110
123
 
111
- if (!existsSync(configPath)) {
124
+ if (existingConfig === null) {
112
125
  // First run: create the config from what we detect.
113
126
  const detected = detectStacks(projectDir);
114
127
  if (detected.length > 0) {
@@ -175,15 +188,52 @@ async function run(projectDir: string, options: RunOptions = {}): Promise<void>
175
188
 
176
189
  await generate(projectDir, config, { quiet });
177
190
  log("Done.");
191
+ if (!quiet) await printRefreshHint(projectDir, config.packageManager, previousVersion);
178
192
  }
179
193
 
180
- /** postinstall: quietly regenerate from an existing config. Never creates one. */
181
- async function postinstall(projectDir: string): Promise<void> {
182
- if (!existsSync(join(projectDir, CONFIG_FILENAME))) return;
194
+ /**
195
+ * Print the "keep .claude/ in sync" hint unless package.json already runs
196
+ * `claude-toolkit refresh`. Only prints: package.json is a committed file.
197
+ */
198
+ async function printRefreshHint(
199
+ projectDir: string,
200
+ packageManager: ClaudeToolkitConfig["packageManager"],
201
+ previousVersion: string | null,
202
+ ): Promise<void> {
203
+ const pkgPath = join(projectDir, "package.json");
204
+ if (!existsSync(pkgPath)) return;
205
+ let scripts: Record<string, unknown>;
206
+ try {
207
+ scripts = JSON.parse(await readFile(pkgPath, "utf-8")).scripts ?? {};
208
+ } catch {
209
+ return; // an unparsable package.json is not worth failing the run over
210
+ }
211
+ const lines = buildRefreshHint({
212
+ scripts,
213
+ packageManager,
214
+ yarnBerry: existsSync(join(projectDir, ".yarnrc.yml")),
215
+ previousVersion,
216
+ });
217
+ if (lines.length > 0) console.log(`\n${lines.join("\n")}`);
218
+ }
219
+
220
+ /**
221
+ * refresh: regenerate .claude/ when the installed toolkit version differs from the
222
+ * one recorded in .claude/.toolkit-version. Built for the consumer's own "prepare"
223
+ * script: it never creates the config, never scaffolds committed files, and the
224
+ * up-to-date path returns before the config is imported.
225
+ */
226
+ async function refresh(projectDir: string, quiet: boolean): Promise<void> {
227
+ const log = quiet ? (_msg = "") => {} : (msg = "") => console.log(msg);
228
+ if (findConfigPath(projectDir) === null) {
229
+ log('[claude-toolkit] No config found — run "bunx claude-toolkit" to set up .claude/.');
230
+ return;
231
+ }
232
+ const version = await readToolkitVersion();
233
+ if ((await readMarker(projectDir)) === version) return;
183
234
  const config = await loadConfig(projectDir);
184
- // scaffold:false — never write committed project files (biome.json/tsconfig.json) on install.
185
235
  await generate(projectDir, config, { quiet: true, scaffold: false });
186
- console.log("[claude-toolkit] Regenerated .claude/ for the updated toolkit version.");
236
+ log(`[claude-toolkit] Regenerated .claude/ for toolkit ${version}.`);
187
237
  }
188
238
 
189
239
  const HELP = `
@@ -193,6 +243,10 @@ Usage:
193
243
  bunx claude-toolkit [project-dir] Create config if missing, then regenerate .claude/
194
244
  bunx claude-toolkit --update [project-dir] Also add newly-detected stacks to the config
195
245
 
246
+ Commands:
247
+ refresh [project-dir] Regenerate .claude/ only if the toolkit version changed.
248
+ Never creates the config; meant for a "prepare" script.
249
+
196
250
  Commands (aliases):
197
251
  init First-run friendly name (same as the bare command)
198
252
  update Same as --update
@@ -203,14 +257,16 @@ Flags:
203
257
  --update, -u Add newly-detected stacks to the config before regenerating
204
258
  --quiet, -q Suppress informational output
205
259
 
206
- .claude/ also regenerates automatically on install when the toolkit version changes.
260
+ Nothing runs at install time. To keep .claude/ in sync after upgrades, add to
261
+ your package.json "scripts":
262
+ "prepare": "claude-toolkit refresh || exit 0"
207
263
  `;
208
264
 
209
265
  // ---- entry ----
210
266
  const argv = process.argv.slice(2);
211
267
  const flags = new Set(argv.filter((a) => a.startsWith("-")));
212
268
  const positional = argv.filter((a) => !a.startsWith("-"));
213
- const COMMANDS = new Set(["init", "update", "sync", "help", "postinstall"]);
269
+ const COMMANDS = new Set(["init", "update", "sync", "help", "refresh"]);
214
270
  const KNOWN_FLAGS = new Set(["--update", "-u", "--quiet", "-q"]);
215
271
 
216
272
  // Reject unknown flags so a typo'd flag (e.g. --updat) isn't silently ignored.
@@ -248,8 +304,8 @@ try {
248
304
  case "help":
249
305
  console.log(HELP);
250
306
  break;
251
- case "postinstall":
252
- await postinstall(projectDir);
307
+ case "refresh":
308
+ await refresh(projectDir, quiet);
253
309
  break;
254
310
  case "sync":
255
311
  if (!quiet) {
@@ -1,12 +1,70 @@
1
1
  ---
2
2
  name: ct-typescript-conventions
3
- description: 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
3
+ description: Strict TypeScript patterns and compiler selection -- prefer the TypeScript 7 native compiler when a project ships it. Use when writing or reviewing TypeScript, running a typecheck, adding or tightening types, or deciding between type and interface
4
4
  ---
5
5
 
6
6
  # TypeScript Conventions
7
7
 
8
8
  Strict mode enabled. Catch errors at compile time, not runtime.
9
9
 
10
+ ## Compiler -- prefer TypeScript 7 when the project has it
11
+
12
+ TypeScript 7 (GA 2026-07-08, `typescript@7.0.2`) is the Go-native compiler; Microsoft reports **8x--12x faster full builds**. In a project that already ships it, use it for every type-check and build. Never install or bump it unprompted.
13
+
14
+ **Detect the installed version, not the declared range:**
15
+
16
+ ```bash
17
+ node -p "require('typescript/package.json').version" # major >= 7 -> TS7
18
+ ```
19
+
20
+ Resolve from the directory you are editing -- monorepo workspaces can pin different majors. Three traps:
21
+
22
+ - The TS7 binary is **`tsc`**. `tsgo` was the pre-GA `@typescript/native-preview` name -- never put it in a script.
23
+ - `package.json` ranges lie: `overrides`, `resolutions`, catalogs, and stale lockfiles all win, and `*` / `latest` / `>=5` resolve to 7.x today.
24
+ - Don't probe the API -- on TS7 `require("typescript")` exposes only `version` and `versionMajorMinor`, so a `ts.createProgram` check gives a false negative.
25
+
26
+ **Using it** -- same flags as 6.x:
27
+
28
+ ```bash
29
+ tsc --noEmit # type-check
30
+ tsc -b # build mode / project references
31
+ tsc --checkers 1 --noEmit # CI: pin the worker count
32
+ ```
33
+
34
+ `--checkers` defaults to 4. Pin it in CI -- Microsoft warns that varying it can surface order-dependent results.
35
+
36
+ **Stay on 6.x when a tool embeds the compiler.** TS7.0 ships no stable programmatic API (7.1 targets a new one), so `typescript-eslint`, `ts-jest`, `ts-node`, `vue-tsc`, `svelte-check`, `@astrojs/check`, and `@angular/compiler-cli` all break -- at install or lint time, *not* at `tsc --noEmit`. A green type-check proves nothing here. Vite, Bun, `tsx`, Vitest, and Biome are unaffected. Run both rather than downgrading:
37
+
38
+ ```json
39
+ {
40
+ "devDependencies": {
41
+ "@typescript/native": "npm:typescript@^7.0.2",
42
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
43
+ }
44
+ }
45
+ ```
46
+
47
+ Keep `typescript` pointed at the 6.0 package -- blocked tools import it by peer dependency. Where TS7 is absent, the floor is `typescript@6.0.3`, the bridge release; its readiness gate is a clean build with `"stableTypeOrdering": true` and no `ignoreDeprecations`.
48
+
49
+ **`tsconfig.json` rules TS7 enforces** -- `ignoreDeprecations` no longer silences any of them:
50
+
51
+ | Now rejected | Fix |
52
+ |---|---|
53
+ | `baseUrl` | project-root-relative `paths` |
54
+ | `outFile`, `downlevelIteration` | remove |
55
+ | `target: "es5"` | `"es2015"` or later |
56
+ | `module: "amd" \| "umd" \| "system" \| "none" \| "systemjs"` | `"esnext"`, `"preserve"`, or `"nodenext"` |
57
+ | `moduleResolution: "node" \| "node10" \| "classic"` | `"bundler"`, `"node16"`, or `"nodenext"` |
58
+ | `esModuleInterop` / `allowSyntheticDefaultImports` / `alwaysStrict` set to `false` | drop the override |
59
+ | `out`, `charset`, `keyofStringsOnly`, `importsNotUsedAsValues`, `preserveValueImports`, `suppressImplicitAnyIndexErrors`, `suppressExcessPropertyErrors`, `noStrictGenericChecks`, `noImplicitUseStrict` | gone -- `TS5023: Unknown compiler option` |
60
+
61
+ Two changed defaults cause most real-world breakage. Both arrived in 6.0 and carry forward, so they bite on either major -- verified identical on 6.0.3 and 7.0.2:
62
+
63
+ - **`types` defaults to `[]`** -- `process`, `Bun` and friends disappear (`TS2591`) *even with `@types/node` installed*. Set `"types": ["node"]` / `["bun"]` explicitly, naming only packages that are actually installed (a missing one is `TS2688`). `"types": ["*"]` restores the old catch-all on 6.x/7.x but is itself `TS2688` on 5.x.
64
+ - **`rootDir` defaults to `./`** -- with sources in `src/` and an `outDir`, emit fails `TS5011`. Set `"rootDir": "./src"`. `--noEmit` does *not* surface this, so type-check CI stays green while the build breaks.
65
+
66
+ `strict` and `stableTypeOrdering` are on by default; `stableTypeOrdering` cannot be turned off.
67
+
10
68
  ## Rules
11
69
 
12
70
  - **No `any`** -- Use `unknown` + narrowing, generics, or specific types.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "claude-toolkit",
3
- "version": "0.15.0",
3
+ "version": "0.17.0-rc.1",
4
4
  "description": "Reusable Claude Code configuration toolkit with stack-specific connectors",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-toolkit": "./bin/cli.ts"
7
+ "claude-toolkit": "bin/cli.ts"
8
8
  },
9
9
  "exports": {
10
10
  ".": {
@@ -18,7 +18,6 @@
18
18
  "core/",
19
19
  "stacks/",
20
20
  "templates/",
21
- "docs/",
22
21
  "CHANGELOG.md"
23
22
  ],
24
23
  "scripts": {
@@ -26,8 +25,8 @@
26
25
  "lint": "biome check --write",
27
26
  "lint:check": "biome check",
28
27
  "test": "bun test",
29
- "postinstall": "bun bin/postinstall.mjs || node bin/postinstall.mjs || exit 0",
30
- "prepare": "npx husky"
28
+ "check:pack": "bun scripts/check-pack.ts",
29
+ "check:smoke": "bun scripts/check-smoke.ts"
31
30
  },
32
31
  "lint-staged": {
33
32
  "*.{ts,tsx,js,jsx,json,css}": "biome check --write --no-errors-on-unmatched"
@@ -44,11 +43,11 @@
44
43
  "license": "MIT",
45
44
  "repository": {
46
45
  "type": "git",
47
- "url": "https://github.com/william-queant/claude-toolkit.git"
46
+ "url": "git+https://github.com/william-queant/claude-toolkit.git"
48
47
  },
49
48
  "devDependencies": {
50
49
  "@biomejs/biome": "^2.4.10",
51
- "@types/bun": "latest",
50
+ "@types/bun": "^1.3.11",
52
51
  "husky": "^9.1.7",
53
52
  "lint-staged": "^16.4.0",
54
53
  "typescript": "^6.0.2"
package/src/generator.ts CHANGED
@@ -1,12 +1,58 @@
1
- import { copyFile as fsCopyFile, readdir } from "node:fs/promises";
1
+ import { copyFile as fsCopyFile, readdir, readFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import type { ClaudeToolkitConfig, ResolvedConfig, StackPack } from "./types.js";
4
4
  import { copyDir, exists, readJson, removePath, writeFileEnsureDir } from "./utils.js";
5
5
 
6
6
  const TOOLKIT_ROOT = resolve(import.meta.dirname, "..");
7
7
 
8
+ /** Branches protected when the config sets no `git.protectedBranches`. */
9
+ const DEFAULT_PROTECTED_BRANCHES = ["main"];
10
+ /** A protected branch name must match this before it is quoted into the guard's `case` pattern. */
11
+ const BRANCH_NAME_PATTERN = /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/;
12
+
13
+ /** Throw a clear error naming the first protected branch that fails BRANCH_NAME_PATTERN. */
14
+ function assertValidBranchNames(branches: string[]): void {
15
+ const invalid = branches.find((b) => !BRANCH_NAME_PATTERN.test(b));
16
+ if (invalid === undefined) return;
17
+ throw new Error(
18
+ `Invalid branch name in git.protectedBranches: ${JSON.stringify(invalid)}. ` +
19
+ 'Use only letters, digits, ".", "_", "/" and "-", and do not start with "-".',
20
+ );
21
+ }
22
+
23
+ /**
24
+ * The PreToolUse command that blocks edits on a protected branch, or null when no
25
+ * branch is protected. Names are validated first, then single-quoted into a `case`
26
+ * pattern, so each name is matched literally and never parsed by the shell.
27
+ */
28
+ function buildBranchGuard(branches: string[]): string | null {
29
+ assertValidBranchNames(branches);
30
+ if (branches.length === 0) return null;
31
+ const patterns = branches.map((b) => `'${b}'`).join("|");
32
+ return `# Prevent editing on protected branches\ncase "$(git branch --show-current)" in ${patterns}) echo '{"block": true, "message": "Cannot edit files on a protected branch. Create a feature branch first."}' >&2; exit 2;; esac`;
33
+ }
34
+
35
+ /** File in .claude/ recording the toolkit version that last generated it. */
36
+ const MARKER_FILE = ".toolkit-version";
37
+
38
+ /** The installed toolkit version, read from the toolkit's own package.json. */
39
+ export async function readToolkitVersion(): Promise<string> {
40
+ const { version } = await readJson<{ version: string }>(join(TOOLKIT_ROOT, "package.json"));
41
+ return version;
42
+ }
43
+
44
+ /** The version recorded in `<projectDir>/.claude/.toolkit-version`, or null when absent. */
45
+ export async function readMarker(projectDir: string): Promise<string | null> {
46
+ const markerPath = join(projectDir, ".claude", MARKER_FILE);
47
+ if (!exists(markerPath)) return null;
48
+ return (await readFile(markerPath, "utf-8")).trim();
49
+ }
50
+
8
51
  /** Resolve the full configuration by merging core + stacks + project config */
9
52
  async function resolveConfig(config: ClaudeToolkitConfig): Promise<ResolvedConfig> {
53
+ // Fail before anything is written when a branch name is unsafe to quote into a hook.
54
+ assertValidBranchNames(config.git?.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES);
55
+
10
56
  const stacks: StackPack[] = [];
11
57
  const allMappings: Record<string, string> = {};
12
58
 
@@ -79,10 +125,8 @@ async function removeGenerated(claudeDir: string): Promise<void> {
79
125
  // Toolkit hook files (copied from core/hooks) + the generated skill-rules.json.
80
126
  ...hookFiles.map((f) => removePath(join(claudeDir, "hooks", f))),
81
127
  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.
128
+ // Stale file from prior installs: skill-eval.js was renamed to .cjs.
84
129
  removePath(join(claudeDir, "hooks", "skill-eval.js")),
85
- removePath(join(claudeDir, "hooks", "skill-eval.sh")),
86
130
  ]);
87
131
  }
88
132
 
@@ -153,8 +197,8 @@ export async function generate(
153
197
  ].join("\n"),
154
198
  );
155
199
 
156
- // 9. Scaffold base configs into the project root (committed files). Skipped for
157
- // automatic/postinstall regeneration so an install never writes committed files.
200
+ // 9. Scaffold base configs into the project root (committed files). Skipped by
201
+ // `refresh` (scaffold: false), which must never write committed files.
158
202
  if (options.scaffold !== false) {
159
203
  await scaffoldConfigs(projectDir, resolved, options.quiet);
160
204
  }
@@ -162,9 +206,9 @@ export async function generate(
162
206
  // 10. Generate skills README
163
207
  await generateSkillsReadme(claudeDir, resolved);
164
208
 
165
- // Record the toolkit version that produced this output — drives auto-regen on update.
166
- const { version } = await readJson<{ version: string }>(join(TOOLKIT_ROOT, "package.json"));
167
- await writeFileEnsureDir(join(claudeDir, ".toolkit-version"), `${version}\n`);
209
+ // Record the toolkit version that produced this output — `refresh` compares against it.
210
+ const version = await readToolkitVersion();
211
+ await writeFileEnsureDir(join(claudeDir, MARKER_FILE), `${version}\n`);
168
212
 
169
213
  if (!options.quiet) {
170
214
  console.log(
@@ -230,11 +274,7 @@ async function generateSkillRules(claudeDir: string, resolved: ResolvedConfig):
230
274
  /** Generate settings.json with hooks */
231
275
  async function generateSettings(claudeDir: string, resolved: ResolvedConfig): Promise<void> {
232
276
  const { hooks, config } = resolved;
233
- const protectedBranches = config.git?.protectedBranches ?? ["main"];
234
-
235
- const branchCheck = protectedBranches
236
- .map((b) => `"$(git branch --show-current)" != "${b}"`)
237
- .join(" && ");
277
+ const branchGuard = buildBranchGuard(config.git?.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES);
238
278
 
239
279
  const postToolUseHooks: unknown[] = [];
240
280
 
@@ -313,18 +353,16 @@ async function generateSettings(claudeDir: string, resolved: ResolvedConfig): Pr
313
353
  ],
314
354
  },
315
355
  ],
316
- PreToolUse: [
317
- {
318
- matcher: "Edit|MultiEdit|Write",
319
- hooks: [
320
- {
321
- type: "command",
322
- command: `# Prevent editing on protected branches\n[ ${branchCheck} ] || { echo '{"block": true, "message": "Cannot edit files on a protected branch. Create a feature branch first."}' >&2; exit 2; }`,
323
- timeout: 5,
324
- },
325
- ],
326
- },
327
- ],
356
+ ...(branchGuard === null
357
+ ? {}
358
+ : {
359
+ PreToolUse: [
360
+ {
361
+ matcher: "Edit|MultiEdit|Write",
362
+ hooks: [{ type: "command", command: branchGuard, timeout: 5 }],
363
+ },
364
+ ],
365
+ }),
328
366
  PostToolUse: postToolUseHooks.map((hook) => ({
329
367
  matcher: "Edit|MultiEdit|Write",
330
368
  hooks: [hook],
@@ -373,7 +411,8 @@ async function generateSkillsReadme(claudeDir: string, resolved: ResolvedConfig)
373
411
  content +=
374
412
  "| ct-systematic-debugging | Four-phase debugging methodology, root cause analysis |\n";
375
413
  content += "| ct-testing-patterns | Test-driven development workflow and patterns |\n";
376
- content += "| ct-typescript-conventions | TypeScript strict mode and conventions |\n";
414
+ content +=
415
+ "| ct-typescript-conventions | TypeScript strict mode, conventions, and TS7 compiler selection |\n";
377
416
  content += "| ct-verification-before-completion | Evidence-based completion claims protocol |\n";
378
417
  content +=
379
418
  "| ct-code-style | Code structure & style: guard clauses, lookup tables, no magic values |\n";