claude-toolkit 0.1.27 → 0.10.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 +44 -0
- package/README.md +28 -33
- package/bin/cli.ts +198 -162
- package/bin/postinstall.mjs +69 -0
- package/core/hooks/skill-eval.js +3 -20
- package/core/skills/ct-testing-patterns/SKILL.md +12 -0
- package/docs/README.md +4 -0
- package/package.json +2 -1
- package/src/generator.ts +68 -11
- package/src/utils.ts +8 -15
- package/stacks/capacitor/skills/ct-capacitor-ota/SKILL.md +16 -0
- package/stacks/capacitor/skills/ct-capacitor-ui/SKILL.md +66 -0
- package/stacks/capacitor/stack.json +47 -1
- package/stacks/cloudflare/skills/ct-cloudflare-d1-kv/SKILL.md +86 -0
- package/stacks/i18n-typesafe/skills/ct-i18n-typesafe/SKILL.md +52 -0
- package/stacks/i18n-typesafe/stack.json +2 -1
- package/stacks/playwright/skills/ct-playwright-patterns/SKILL.md +70 -2
- package/stacks/protobuf/skills/ct-protobuf-contracts/SKILL.md +36 -0
- package/stacks/rust-wasm/skills/ct-rust-wasm-patterns/SKILL.md +62 -0
- package/stacks/solidjs/skills/ct-solidjs-patterns/SKILL.md +70 -0
- package/stacks/solidjs/stack.json +2 -1
- package/stacks/storybook/skills/ct-storybook-patterns/SKILL.md +62 -0
- package/stacks/vanilla-extract/skills/ct-vanilla-extract-patterns/SKILL.md +62 -0
- package/stacks/vanilla-extract/stack.json +1 -1
- package/stacks/vite/skills/ct-vite-vitest-patterns/SKILL.md +71 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-06-09)
|
|
4
|
+
|
|
5
|
+
One idempotent CLI command replaces `init`/`update`/`sync`, and `.claude/` now regenerates itself on toolkit upgrade.
|
|
6
|
+
|
|
7
|
+
- feat: `bunx claude-toolkit` does it all — creates the config from stack detection on the first run, then cleanly regenerates `.claude/` on every run. `--update` pulls newly-detected stacks into the config. `init`/`update`/`sync` remain as back-compat aliases (`sync` deprecated).
|
|
8
|
+
- feat: automatic regeneration on install — a `postinstall` hook rebuilds `.claude/` when the installed toolkit version changes, so an upgrade ships its updated skills without running anything. Never fails the consumer's install and never writes committed files.
|
|
9
|
+
- feat: clean rebuilds — generation removes toolkit-owned (`ct-` prefixed) skills, agents, commands, and hooks before regenerating, so a stack removed from the config leaves no stale skills behind; user-authored files in `.claude/` are preserved.
|
|
10
|
+
- feat: stricter CLI parsing — unknown flags and typo'd commands now error instead of silently doing nothing; added `--quiet`/`-q`.
|
|
11
|
+
- docs: README and reference docs updated for the single-command workflow and conventional-commit versioning.
|
|
12
|
+
|
|
13
|
+
## 0.9.0 (2026-06-08)
|
|
14
|
+
|
|
15
|
+
Re-baselined from 0.1.x to reflect accumulated scope: 10 stack connectors, the full `init`/`update`/`sync` CLI, stack auto-detection with drift and monorepo/workspace support, and a complete skill/command/agent/hook system. Versioning is now conventional-commit-driven from this release onward.
|
|
16
|
+
|
|
17
|
+
- feat: render/runtime-speed guidance across every stack skill, each paired with a security guardrail
|
|
18
|
+
- feat: new `ct-capacitor-ui` skill for webview performance and native feel
|
|
19
|
+
- feat: canonical test-speed rule in core testing skill + cross-stack `relatedSkills` wiring
|
|
20
|
+
- build: conventional-commit-aware versioning (feat→minor, fix/perf→patch, breaking→major)
|
|
21
|
+
- ci: automated npm publish on GitHub release
|
|
22
|
+
|
|
23
|
+
## 0.1.33 (2026-06-07)
|
|
24
|
+
|
|
25
|
+
- chore: apply biome formatting to skill-eval hook
|
|
26
|
+
|
|
27
|
+
## 0.1.32 (2026-06-07)
|
|
28
|
+
|
|
29
|
+
- feat: add ct-capacitor-ui skill for webview performance and native feel
|
|
30
|
+
|
|
31
|
+
## 0.1.31 (2026-06-07)
|
|
32
|
+
|
|
33
|
+
- feat: add i18n performance guidance and wire relatedSkills to solidjs/vanilla-extract
|
|
34
|
+
|
|
35
|
+
## 0.1.30 (2026-06-07)
|
|
36
|
+
|
|
37
|
+
- feat: add test-speed guidance to vite, playwright, storybook, and core testing skill
|
|
38
|
+
|
|
39
|
+
## 0.1.29 (2026-06-07)
|
|
40
|
+
|
|
41
|
+
- feat: add performance guidance to cloudflare, rust-wasm, and protobuf skills
|
|
42
|
+
|
|
43
|
+
## 0.1.28 (2026-06-07)
|
|
44
|
+
|
|
45
|
+
- feat: add render-speed guidance to solidjs and vanilla-extract skills
|
|
46
|
+
|
|
3
47
|
## 0.1.27 (2026-06-01)
|
|
4
48
|
|
|
5
49
|
- feat: detect stacks across workspace packages and monorepo subdirectories
|
package/README.md
CHANGED
|
@@ -14,15 +14,16 @@ Whether you're starting a new project from scratch or consolidating an existing
|
|
|
14
14
|
# Install as a dev dependency
|
|
15
15
|
bun add -d claude-toolkit
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
bunx claude-toolkit
|
|
17
|
+
# Generate .claude/ — creates the config from detection on the first run
|
|
18
|
+
bunx claude-toolkit
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## How It Works
|
|
22
22
|
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
23
|
+
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
|
+
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
25
|
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
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
import { defineConfig } from "claude-toolkit";
|
|
@@ -45,18 +46,19 @@ export default defineConfig({
|
|
|
45
46
|
|
|
46
47
|
## CLI Commands
|
|
47
48
|
|
|
48
|
-
| Command
|
|
49
|
-
|
|
|
50
|
-
| `bunx claude-toolkit
|
|
51
|
-
| `bunx claude-toolkit update` |
|
|
52
|
-
| `bunx claude-toolkit
|
|
53
|
-
|
|
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 |
|
|
54
|
+
|
|
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.
|
|
54
56
|
|
|
55
57
|
## Stack Auto-Detection
|
|
56
58
|
|
|
57
|
-
The toolkit
|
|
59
|
+
The toolkit detects which stacks your project uses by scanning `package.json` dependencies, config files, and project structure.
|
|
58
60
|
|
|
59
|
-
**
|
|
61
|
+
**First run** (no config yet) — detected stacks are pre-filled into a new config:
|
|
60
62
|
|
|
61
63
|
```text
|
|
62
64
|
Detected stacks:
|
|
@@ -65,36 +67,31 @@ Detected stacks:
|
|
|
65
67
|
cloudflare — found wrangler.toml
|
|
66
68
|
|
|
67
69
|
Created claude-toolkit.config.ts
|
|
70
|
+
Generated .claude/ with 3 stack(s) and 4 core skills
|
|
68
71
|
```
|
|
69
72
|
|
|
70
|
-
**
|
|
73
|
+
**Later runs** (config exists) — `bunx claude-toolkit` regenerates `.claude/` and reports drift between your config and what it detects, but never edits the config:
|
|
71
74
|
|
|
72
75
|
```text
|
|
73
76
|
Stack drift detected:
|
|
74
|
-
+ playwright — found @playwright/test in dependencies (not in config)
|
|
75
|
-
- rust-wasm — in config
|
|
77
|
+
+ playwright — found @playwright/test in dependencies (detected, not in config)
|
|
78
|
+
- rust-wasm — in config, not detected
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
stacks: ["solidjs", "vite", "cloudflare", "playwright"]
|
|
79
|
-
|
|
80
|
-
Run "claude-toolkit update" to add detected stacks automatically.
|
|
80
|
+
Run "bunx claude-toolkit --update" to add detected stacks to your config.
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
**On `update`** (existing config), the toolkit adds any newly detected stacks to your config and regenerates `.claude/` in one step — use this when you add a new stack (e.g. Capacitor) to an existing project:
|
|
83
|
+
**Pulling in new stacks** — `bunx claude-toolkit --update` adds any newly-detected stacks to your config and regenerates in one step (use it when you add a stack, e.g. Capacitor):
|
|
86
84
|
|
|
87
85
|
```text
|
|
88
86
|
Adding newly detected stacks to config:
|
|
89
87
|
+ capacitor — found @capacitor/core in dependencies
|
|
90
88
|
Updated claude-toolkit.config.ts
|
|
91
|
-
Generated .claude/ with
|
|
92
|
-
Update complete.
|
|
89
|
+
Generated .claude/ with 4 stack(s) and 4 core skills
|
|
93
90
|
```
|
|
94
91
|
|
|
95
|
-
Stacks
|
|
92
|
+
Stacks in your config that are no longer detected are reported but left unchanged (remove them manually if intended).
|
|
96
93
|
|
|
97
|
-
|
|
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.
|
|
98
95
|
|
|
99
96
|
## Available Stacks
|
|
100
97
|
|
|
@@ -109,7 +106,7 @@ This keeps your config aligned as your project evolves — `init` for first-time
|
|
|
109
106
|
| `i18n-typesafe` | typesafe-i18n internationalization |
|
|
110
107
|
| `playwright` | Playwright E2E testing, Page Objects, fixtures, CI/CD |
|
|
111
108
|
| `storybook` | Storybook interaction testing, CSF 3, visual regression |
|
|
112
|
-
| `capacitor` | Capacitor 8
|
|
109
|
+
| `capacitor` | Capacitor 8 runtime, Capgo OTA, channels; webview UI & native feel |
|
|
113
110
|
|
|
114
111
|
## Core Features (always included)
|
|
115
112
|
|
|
@@ -163,14 +160,12 @@ Full reference documentation for all skills, commands, and agents is available i
|
|
|
163
160
|
|
|
164
161
|
## Versioning
|
|
165
162
|
|
|
166
|
-
|
|
163
|
+
Version bumps are derived from your commit messages (Conventional Commits) by a post-commit hook, and `CHANGELOG.md` is updated automatically:
|
|
167
164
|
|
|
168
|
-
|
|
165
|
+
- `feat:` → minor · `fix:` / `perf:` → patch · `feat!:` / `BREAKING CHANGE` → major (capped to minor while pre-1.0)
|
|
166
|
+
- `docs:` / `chore:` / `refactor:` / `style:` / `test:` / `ci:` / `build:` → no version change
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
bun version major # 0.1.x → 1.0.0
|
|
172
|
-
bun version minor # 0.1.x → 0.2.0
|
|
173
|
-
```
|
|
168
|
+
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`).
|
|
174
169
|
|
|
175
170
|
## Development
|
|
176
171
|
|
package/bin/cli.ts
CHANGED
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* claude-toolkit CLI
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* update
|
|
9
|
-
*
|
|
6
|
+
* One idempotent command does it all:
|
|
7
|
+
* bunx claude-toolkit Create the config if missing, then (re)generate .claude/
|
|
8
|
+
* bunx claude-toolkit --update Also pull newly-detected stacks into the config
|
|
9
|
+
*
|
|
10
|
+
* Aliases / back-compat:
|
|
11
|
+
* init Friendly name for the first run (same as the bare command)
|
|
12
|
+
* update Same as `--update`
|
|
13
|
+
* sync Deprecated alias of the bare command
|
|
14
|
+
*
|
|
15
|
+
* .claude/ is also regenerated automatically on install when the installed
|
|
16
|
+
* toolkit version changes (see bin/postinstall.mjs).
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import { existsSync } from "node:fs";
|
|
@@ -18,12 +25,12 @@ import type { ClaudeToolkitConfig } from "../src/types.js";
|
|
|
18
25
|
|
|
19
26
|
const CONFIG_FILENAME = "claude-toolkit.config.ts";
|
|
20
27
|
|
|
21
|
-
/** Build a `stacks: [...]` literal for injection into the config file */
|
|
28
|
+
/** Build a `stacks: [...]` literal for injection into the config file. */
|
|
22
29
|
function buildStacksLiteral(stacks: string[]): string {
|
|
23
30
|
return stacks.length > 0 ? `stacks: [${stacks.map((s) => `"${s}"`).join(", ")}]` : "stacks: []";
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
/** Rewrite the `stacks: [...]` array in an existing config file in place */
|
|
33
|
+
/** Rewrite the `stacks: [...]` array in an existing config file in place. */
|
|
27
34
|
async function updateConfigStacks(configPath: string, stacks: string[]): Promise<void> {
|
|
28
35
|
const content = await readFile(configPath, "utf-8");
|
|
29
36
|
const stacksArray = /stacks:\s*\[[^\]]*\]/;
|
|
@@ -39,194 +46,223 @@ async function updateConfigStacks(configPath: string, stacks: string[]): Promise
|
|
|
39
46
|
async function loadConfig(projectDir: string): Promise<ClaudeToolkitConfig> {
|
|
40
47
|
const configPath = join(projectDir, CONFIG_FILENAME);
|
|
41
48
|
if (!existsSync(configPath)) {
|
|
42
|
-
throw new Error(`Config not found: ${configPath}\nRun "claude-toolkit
|
|
49
|
+
throw new Error(`Config not found: ${configPath}\nRun "bunx claude-toolkit" first.`);
|
|
43
50
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (existsSync(configPath)) {
|
|
52
|
-
console.log(`Config already exists: ${configPath}`);
|
|
53
|
-
console.log("\nThis project is already initialized. Did you mean to:");
|
|
54
|
-
console.log(
|
|
55
|
-
" claude-toolkit update # detect & add new stacks to your config, then regenerate",
|
|
51
|
+
let mod: { default?: ClaudeToolkitConfig };
|
|
52
|
+
try {
|
|
53
|
+
mod = await import(configPath);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Failed to load ${CONFIG_FILENAME}. Make sure claude-toolkit is installed in this project ` +
|
|
57
|
+
`(e.g. "bun add -d claude-toolkit"), then run "bunx claude-toolkit" again.\n ${(err as Error).message}`,
|
|
56
58
|
);
|
|
57
|
-
console.log(" claude-toolkit sync # regenerate .claude/ from the current config");
|
|
58
|
-
return;
|
|
59
59
|
}
|
|
60
|
+
return mod.default as ClaudeToolkitConfig;
|
|
61
|
+
}
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log("Detected stacks:");
|
|
65
|
-
const maxLen = Math.max(...detected.map((d) => d.name.length));
|
|
66
|
-
for (const d of detected) {
|
|
67
|
-
console.log(` ${d.name.padEnd(maxLen)} — ${d.reason}`);
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
console.log(`No stacks detected. You can add them manually in ${CONFIG_FILENAME}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Build stacks literal for config injection
|
|
74
|
-
const stacksLiteral = buildStacksLiteral(detected.map((d) => d.name));
|
|
75
|
-
|
|
76
|
-
// Copy starter config with detected stacks injected
|
|
63
|
+
/** Create claude-toolkit.config.ts from the template with detected stacks injected. */
|
|
64
|
+
async function scaffoldConfig(configPath: string, stacks: string[]): Promise<void> {
|
|
65
|
+
const stacksLiteral = buildStacksLiteral(stacks);
|
|
77
66
|
const templatePath = join(import.meta.dirname, "..", "templates", "claude-toolkit.config.ts");
|
|
67
|
+
let content: string;
|
|
78
68
|
if (existsSync(templatePath)) {
|
|
79
|
-
|
|
80
|
-
const configContent = template.replace("stacks: []", stacksLiteral);
|
|
81
|
-
await writeFile(configPath, configContent, "utf-8");
|
|
82
|
-
console.log(`Created ${CONFIG_FILENAME}`);
|
|
69
|
+
content = (await readFile(templatePath, "utf-8")).replace("stacks: []", stacksLiteral);
|
|
83
70
|
} else {
|
|
84
|
-
|
|
85
|
-
const defaultConfig = `import { defineConfig } from 'claude-toolkit'
|
|
71
|
+
content = `import { defineConfig } from "claude-toolkit";
|
|
86
72
|
|
|
87
73
|
export default defineConfig({
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})
|
|
74
|
+
${stacksLiteral},
|
|
75
|
+
packageManager: "bun",
|
|
76
|
+
hooks: {
|
|
77
|
+
formatter: "bun run prettier --write",
|
|
78
|
+
testRunner: "bun run vitest run",
|
|
79
|
+
typeCheck: "bun run tsc --noEmit",
|
|
80
|
+
},
|
|
81
|
+
git: {
|
|
82
|
+
branchPrefix: "dev",
|
|
83
|
+
protectedBranches: ["main"],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
100
86
|
`;
|
|
101
|
-
await writeFile(configPath, defaultConfig, "utf-8");
|
|
102
|
-
console.log(`Created ${CONFIG_FILENAME}`);
|
|
103
87
|
}
|
|
88
|
+
await writeFile(configPath, content, "utf-8");
|
|
89
|
+
}
|
|
104
90
|
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
interface RunOptions {
|
|
92
|
+
/** Also write newly-detected stacks into the config (the old `update`). */
|
|
93
|
+
update?: boolean;
|
|
94
|
+
/** Suppress informational output. */
|
|
95
|
+
quiet?: boolean;
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The one command. Idempotent:
|
|
100
|
+
* - no config yet -> create it from detection
|
|
101
|
+
* - config exists -> report drift (or, with `update`, merge detected stacks in)
|
|
102
|
+
* Always ends by cleanly regenerating .claude/.
|
|
103
|
+
*/
|
|
104
|
+
async function run(projectDir: string, options: RunOptions = {}): Promise<void> {
|
|
105
|
+
const { update = false, quiet = false } = options;
|
|
106
|
+
const log = quiet ? (_msg = "") => {} : (msg = "") => console.log(msg);
|
|
107
|
+
const configPath = join(projectDir, CONFIG_FILENAME);
|
|
111
108
|
|
|
112
|
-
|
|
113
|
-
const detected = detectStacks(projectDir);
|
|
114
|
-
const configuredNames = new Set(config.stacks);
|
|
115
|
-
const detectedNames = new Set(detected.map((d) => d.name));
|
|
109
|
+
let config: ClaudeToolkitConfig;
|
|
116
110
|
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
if (!existsSync(configPath)) {
|
|
112
|
+
// First run: create the config from what we detect.
|
|
113
|
+
const detected = detectStacks(projectDir);
|
|
114
|
+
if (detected.length > 0) {
|
|
115
|
+
log("Detected stacks:");
|
|
116
|
+
const pad = Math.max(...detected.map((d) => d.name.length));
|
|
117
|
+
for (const d of detected) log(` ${d.name.padEnd(pad)} — ${d.reason}`);
|
|
118
|
+
} else {
|
|
119
|
+
log(`No stacks detected. Add them manually in ${CONFIG_FILENAME}.`);
|
|
120
|
+
}
|
|
121
|
+
await scaffoldConfig(
|
|
122
|
+
configPath,
|
|
123
|
+
detected.map((d) => d.name),
|
|
124
|
+
);
|
|
125
|
+
log(`Created ${CONFIG_FILENAME}`);
|
|
126
|
+
// Build the config in memory (mirroring the scaffolded template defaults) so the
|
|
127
|
+
// first run never depends on importing the freshly-written config file — which
|
|
128
|
+
// imports "claude-toolkit" and would fail if the package isn't installed yet.
|
|
129
|
+
config = {
|
|
130
|
+
stacks: detected.map((d) => d.name),
|
|
131
|
+
packageManager: "bun",
|
|
132
|
+
hooks: {
|
|
133
|
+
formatter: "bun run prettier --write",
|
|
134
|
+
testRunner: "bun run vitest run",
|
|
135
|
+
typeCheck: "bun run tsc --noEmit",
|
|
136
|
+
},
|
|
137
|
+
git: { branchPrefix: "dev", protectedBranches: ["main"] },
|
|
138
|
+
};
|
|
139
|
+
} else {
|
|
140
|
+
config = await loadConfig(projectDir);
|
|
141
|
+
const detected = detectStacks(projectDir);
|
|
142
|
+
const configured = new Set(config.stacks);
|
|
143
|
+
const detectedNames = new Set(detected.map((d) => d.name));
|
|
144
|
+
const missing = detected.filter((d) => !configured.has(d.name));
|
|
145
|
+
const stale = config.stacks.filter((s) => !detectedNames.has(s));
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
if (update) {
|
|
148
|
+
if (missing.length > 0) {
|
|
149
|
+
log("Adding newly detected stacks to config:");
|
|
150
|
+
const pad = Math.max(...missing.map((d) => d.name.length));
|
|
151
|
+
for (const d of missing) log(` + ${d.name.padEnd(pad)} — ${d.reason}`);
|
|
152
|
+
config.stacks = [...config.stacks, ...missing.map((d) => d.name)];
|
|
153
|
+
await updateConfigStacks(configPath, config.stacks);
|
|
154
|
+
log(`Updated ${CONFIG_FILENAME}`);
|
|
155
|
+
} else {
|
|
156
|
+
log("Config already includes all detected stacks.");
|
|
157
|
+
}
|
|
158
|
+
if (stale.length > 0) {
|
|
159
|
+
log("\nIn config but not detected (left unchanged):");
|
|
160
|
+
for (const s of stale) log(` - ${s}`);
|
|
161
|
+
log("Remove them from the config manually if they no longer apply.");
|
|
162
|
+
}
|
|
163
|
+
} else if (missing.length > 0 || stale.length > 0) {
|
|
164
|
+
log("\nStack drift detected:");
|
|
165
|
+
const pad = Math.max(1, ...missing.map((d) => d.name.length));
|
|
124
166
|
for (const d of missing) {
|
|
125
|
-
|
|
167
|
+
log(` + ${d.name.padEnd(pad)} — ${d.reason} (detected, not in config)`);
|
|
126
168
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.log(` - ${s} — in config but not detected in project`);
|
|
169
|
+
for (const s of stale) log(` - ${s} — in config, not detected`);
|
|
170
|
+
if (missing.length > 0) {
|
|
171
|
+
log(`\nRun "bunx claude-toolkit --update" to add detected stacks to your config.`);
|
|
131
172
|
}
|
|
132
173
|
}
|
|
133
|
-
const suggested = [
|
|
134
|
-
...new Set([
|
|
135
|
-
...config.stacks.filter((s) => !stale.includes(s)),
|
|
136
|
-
...missing.map((d) => d.name),
|
|
137
|
-
]),
|
|
138
|
-
];
|
|
139
|
-
console.log(`\nSuggested update in ${CONFIG_FILENAME}:`);
|
|
140
|
-
console.log(` ${buildStacksLiteral(suggested)}`);
|
|
141
|
-
if (missing.length > 0) {
|
|
142
|
-
console.log('\nRun "claude-toolkit update" to add detected stacks automatically.\n');
|
|
143
|
-
}
|
|
144
174
|
}
|
|
145
175
|
|
|
146
|
-
await generate(projectDir, config);
|
|
147
|
-
|
|
176
|
+
await generate(projectDir, config, { quiet });
|
|
177
|
+
log("Done.");
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* detected are reported but left unchanged. Errors out if no config exists.
|
|
154
|
-
*/
|
|
155
|
-
async function update(projectDir: string): Promise<void> {
|
|
156
|
-
const configPath = join(projectDir, CONFIG_FILENAME);
|
|
157
|
-
if (!existsSync(configPath)) {
|
|
158
|
-
console.error(`No ${CONFIG_FILENAME} found in ${projectDir}.`);
|
|
159
|
-
console.error(`Run "claude-toolkit init" to create one first.`);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
|
|
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;
|
|
163
183
|
const config = await loadConfig(projectDir);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
// scaffold:false — never write committed project files (biome.json/tsconfig.json) on install.
|
|
185
|
+
await generate(projectDir, config, { quiet: true, scaffold: false });
|
|
186
|
+
console.log("[claude-toolkit] Regenerated .claude/ for the updated toolkit version.");
|
|
187
|
+
}
|
|
167
188
|
|
|
168
|
-
|
|
169
|
-
|
|
189
|
+
const HELP = `
|
|
190
|
+
claude-toolkit — Reusable Claude Code configuration
|
|
170
191
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.log("Adding newly detected stacks to config:");
|
|
175
|
-
const maxLen = Math.max(...missing.map((d) => d.name.length));
|
|
176
|
-
for (const d of missing) {
|
|
177
|
-
console.log(` + ${d.name.padEnd(maxLen)} — ${d.reason}`);
|
|
178
|
-
}
|
|
179
|
-
const nextStacks = [...config.stacks, ...missing.map((d) => d.name)];
|
|
180
|
-
await updateConfigStacks(configPath, nextStacks);
|
|
181
|
-
config.stacks = nextStacks;
|
|
182
|
-
console.log(`Updated ${CONFIG_FILENAME}`);
|
|
183
|
-
}
|
|
192
|
+
Usage:
|
|
193
|
+
bunx claude-toolkit [project-dir] Create config if missing, then regenerate .claude/
|
|
194
|
+
bunx claude-toolkit --update [project-dir] Also add newly-detected stacks to the config
|
|
184
195
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
console.log("Remove them from the config manually if they no longer apply.");
|
|
191
|
-
}
|
|
196
|
+
Commands (aliases):
|
|
197
|
+
init First-run friendly name (same as the bare command)
|
|
198
|
+
update Same as --update
|
|
199
|
+
sync Deprecated — use the bare command
|
|
200
|
+
help Show this message
|
|
192
201
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
Flags:
|
|
203
|
+
--update, -u Add newly-detected stacks to the config before regenerating
|
|
204
|
+
--quiet, -q Suppress informational output
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const command = args[0];
|
|
200
|
-
const projectDir = resolve(args[1] ?? ".");
|
|
201
|
-
|
|
202
|
-
switch (command) {
|
|
203
|
-
case "init":
|
|
204
|
-
await init(projectDir);
|
|
205
|
-
break;
|
|
206
|
-
case "sync":
|
|
207
|
-
await sync(projectDir);
|
|
208
|
-
break;
|
|
209
|
-
case "update":
|
|
210
|
-
await update(projectDir);
|
|
211
|
-
break;
|
|
212
|
-
case undefined:
|
|
213
|
-
case "help":
|
|
214
|
-
console.log(`
|
|
215
|
-
claude-toolkit — Reusable Claude Code configuration
|
|
206
|
+
.claude/ also regenerates automatically on install when the toolkit version changes.
|
|
207
|
+
`;
|
|
216
208
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
// ---- entry ----
|
|
210
|
+
const argv = process.argv.slice(2);
|
|
211
|
+
const flags = new Set(argv.filter((a) => a.startsWith("-")));
|
|
212
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
213
|
+
const COMMANDS = new Set(["init", "update", "sync", "help", "postinstall"]);
|
|
214
|
+
const KNOWN_FLAGS = new Set(["--update", "-u", "--quiet", "-q"]);
|
|
222
215
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
`);
|
|
228
|
-
break;
|
|
229
|
-
default:
|
|
230
|
-
console.error(`Unknown command: ${command}`);
|
|
216
|
+
// Reject unknown flags so a typo'd flag (e.g. --updat) isn't silently ignored.
|
|
217
|
+
for (const f of flags) {
|
|
218
|
+
if (!KNOWN_FLAGS.has(f)) {
|
|
219
|
+
console.error(`Unknown flag: ${f}\nRun "bunx claude-toolkit help".`);
|
|
231
220
|
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const first = positional[0];
|
|
225
|
+
// A positional is treated as a project dir only when it's path-like (".", "..",
|
|
226
|
+
// "./x", or contains a separator) AND exists — so a typo'd command that happens to
|
|
227
|
+
// match a sibling dir name errors instead of silently generating in the wrong place.
|
|
228
|
+
const looksLikePath = (s: string) =>
|
|
229
|
+
s === "." || s === ".." || s.startsWith(".") || /[\\/]/.test(s);
|
|
230
|
+
let command: string | undefined;
|
|
231
|
+
let dirArg: string | undefined;
|
|
232
|
+
if (first && COMMANDS.has(first)) {
|
|
233
|
+
command = first;
|
|
234
|
+
dirArg = positional[1];
|
|
235
|
+
} else if (first && looksLikePath(first) && existsSync(resolve(first))) {
|
|
236
|
+
dirArg = first; // explicit project dir
|
|
237
|
+
} else if (first) {
|
|
238
|
+
console.error(`Unknown command: ${first}\nRun "bunx claude-toolkit help".`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const projectDir = resolve(dirArg ?? ".");
|
|
243
|
+
const wantUpdate = command === "update" || flags.has("--update") || flags.has("-u");
|
|
244
|
+
const quiet = flags.has("--quiet") || flags.has("-q");
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
switch (command) {
|
|
248
|
+
case "help":
|
|
249
|
+
console.log(HELP);
|
|
250
|
+
break;
|
|
251
|
+
case "postinstall":
|
|
252
|
+
await postinstall(projectDir);
|
|
253
|
+
break;
|
|
254
|
+
case "sync":
|
|
255
|
+
if (!quiet) {
|
|
256
|
+
console.log('Note: "sync" is deprecated — just run "bunx claude-toolkit".');
|
|
257
|
+
}
|
|
258
|
+
await run(projectDir, { update: wantUpdate, quiet });
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
// undefined (bare command), "init", or "update"
|
|
262
|
+
await run(projectDir, { update: wantUpdate, quiet });
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
267
|
+
process.exit(1);
|
|
232
268
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* claude-toolkit postinstall
|
|
5
|
+
*
|
|
6
|
+
* Runs in the consumer project after install. If a claude-toolkit.config.ts
|
|
7
|
+
* exists, it regenerates .claude/ — but only when the installed toolkit version
|
|
8
|
+
* differs from the one that last generated it, so it fires on a toolkit update,
|
|
9
|
+
* not on every install.
|
|
10
|
+
*
|
|
11
|
+
* Guarantees:
|
|
12
|
+
* - Never fails the consumer's install (all errors swallowed; always exit 0).
|
|
13
|
+
* - Never runs inside the toolkit's own repo (dev install).
|
|
14
|
+
* - Never creates or edits the committed config — only regenerates .claude/.
|
|
15
|
+
* - Writes no committed files when there's no config (prints a one-line nudge).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
function readJsonSafe(path) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const toolkitDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
// INIT_CWD is set by npm/bun/pnpm/yarn to the dir where install was invoked.
|
|
34
|
+
const consumerRoot = resolve(process.env.INIT_CWD || process.cwd());
|
|
35
|
+
|
|
36
|
+
// 1. Skip when running inside the toolkit's own repo (dev install).
|
|
37
|
+
if (consumerRoot === toolkitDir) process.exit(0);
|
|
38
|
+
const consumerPkg = readJsonSafe(join(consumerRoot, "package.json"));
|
|
39
|
+
if (consumerPkg?.name === "claude-toolkit") process.exit(0);
|
|
40
|
+
|
|
41
|
+
// 2. No config -> stay hands-off; just nudge once. Never write committed files.
|
|
42
|
+
const hasConfig =
|
|
43
|
+
existsSync(join(consumerRoot, "claude-toolkit.config.ts")) ||
|
|
44
|
+
existsSync(join(consumerRoot, "claude-toolkit.config.js"));
|
|
45
|
+
if (!hasConfig) {
|
|
46
|
+
console.log(
|
|
47
|
+
'[claude-toolkit] Run "bunx claude-toolkit" to set up your Claude Code config (.claude/).',
|
|
48
|
+
);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Only regenerate when the toolkit version changed since the last generation.
|
|
53
|
+
const toolkitVersion = readJsonSafe(join(toolkitDir, "package.json"))?.version;
|
|
54
|
+
const markerPath = join(consumerRoot, ".claude", ".toolkit-version");
|
|
55
|
+
const lastVersion = existsSync(markerPath) ? readFileSync(markerPath, "utf8").trim() : null;
|
|
56
|
+
if (toolkitVersion && lastVersion === toolkitVersion) process.exit(0);
|
|
57
|
+
|
|
58
|
+
// 4. Regenerate via the CLI (reuses config loading + clean rebuild). Best-effort:
|
|
59
|
+
// spawnSync sets `.error` if bun is missing — we ignore it rather than fail.
|
|
60
|
+
spawnSync("bun", [join(toolkitDir, "bin", "cli.ts"), "postinstall"], {
|
|
61
|
+
cwd: consumerRoot,
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
env: { ...process.env, INIT_CWD: consumerRoot },
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
// Never break the consumer's install over config regeneration.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.exit(0);
|