claude-toolkit 0.9.0 → 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 +10 -0
- package/README.md +27 -32
- package/bin/cli.ts +198 -162
- package/bin/postinstall.mjs +69 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.9.0 (2026-06-08)
|
|
4
14
|
|
|
5
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.
|
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
|
|
|
@@ -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);
|
package/docs/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Complete reference for all skills, commands, and agents provided by claude-toolkit.
|
|
4
4
|
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Run `bunx claude-toolkit` in your project — on the first run it creates `claude-toolkit.config.ts` (pre-filled from stack detection) and generates `.claude/`. Run it again anytime to regenerate; add `--update` to pull newly-detected stacks into the config. `.claude/` also regenerates automatically on install when the toolkit version changes. See the [README](../README.md#cli-commands) for the full CLI.
|
|
8
|
+
|
|
5
9
|
## Core Skills
|
|
6
10
|
|
|
7
11
|
Skills that are always included regardless of stack configuration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
+
"postinstall": "bun bin/postinstall.mjs || node bin/postinstall.mjs || exit 0",
|
|
28
29
|
"prepare": "npx husky"
|
|
29
30
|
},
|
|
30
31
|
"lint-staged": {
|
package/src/generator.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { copyFile as fsCopyFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile as fsCopyFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import type { ClaudeToolkitConfig, ResolvedConfig, StackPack } from "./types.js";
|
|
4
|
-
import { copyDir, exists, readJson, writeFileEnsureDir } from "./utils.js";
|
|
4
|
+
import { copyDir, exists, readJson, removePath, writeFileEnsureDir } from "./utils.js";
|
|
5
5
|
|
|
6
6
|
const TOOLKIT_ROOT = resolve(import.meta.dirname, "..");
|
|
7
7
|
|
|
@@ -48,11 +48,52 @@ async function resolveConfig(config: ClaudeToolkitConfig): Promise<ResolvedConfi
|
|
|
48
48
|
return { config, skills, directoryMappings: allMappings, hooks, stacks };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Remove only `ct-` prefixed entries in a directory (preserves user-authored files). */
|
|
52
|
+
async function removeCtPrefixed(dir: string): Promise<void> {
|
|
53
|
+
if (!exists(dir)) return;
|
|
54
|
+
const entries = await readdir(dir);
|
|
55
|
+
await Promise.all(
|
|
56
|
+
entries.filter((e) => e.startsWith("ct-")).map((e) => removePath(join(dir, e))),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Remove toolkit-generated artifacts from .claude/ before a rebuild, so a stack
|
|
62
|
+
* removed from the config doesn't leave an orphaned skill behind.
|
|
63
|
+
*
|
|
64
|
+
* Scoped to what the toolkit owns: `ct-` prefixed skills/agents, the generated
|
|
65
|
+
* skills index, the `ct/` command namespace, and the toolkit's own hook files.
|
|
66
|
+
* `.claude/skills`, `/agents`, and `/hooks` are shared Claude Code namespaces, so
|
|
67
|
+
* any user-authored files there — and the user files at the .claude root
|
|
68
|
+
* (settings.local.json, user-team-info.json, tasks/) — are preserved.
|
|
69
|
+
* (settings.json and .gitignore are single generated files, overwritten by generate.)
|
|
70
|
+
*/
|
|
71
|
+
async function removeGenerated(claudeDir: string): Promise<void> {
|
|
72
|
+
const coreHooks = join(TOOLKIT_ROOT, "core", "hooks");
|
|
73
|
+
const hookFiles = exists(coreHooks) ? await readdir(coreHooks) : [];
|
|
74
|
+
await Promise.all([
|
|
75
|
+
removeCtPrefixed(join(claudeDir, "skills")),
|
|
76
|
+
removeCtPrefixed(join(claudeDir, "agents")),
|
|
77
|
+
removePath(join(claudeDir, "skills", "README.md")),
|
|
78
|
+
removePath(join(claudeDir, "commands", "ct")),
|
|
79
|
+
// Toolkit hook files (copied from core/hooks) + the generated skill-rules.json.
|
|
80
|
+
...hookFiles.map((f) => removePath(join(claudeDir, "hooks", f))),
|
|
81
|
+
removePath(join(claudeDir, "hooks", "skill-rules.json")),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
/** Generate the .claude/ directory from resolved config */
|
|
52
|
-
export async function generate(
|
|
86
|
+
export async function generate(
|
|
87
|
+
projectDir: string,
|
|
88
|
+
config: ClaudeToolkitConfig,
|
|
89
|
+
options: { quiet?: boolean; scaffold?: boolean } = {},
|
|
90
|
+
): Promise<void> {
|
|
53
91
|
const resolved = await resolveConfig(config);
|
|
54
92
|
const claudeDir = join(projectDir, ".claude");
|
|
55
93
|
|
|
94
|
+
// 0. Clean previously-generated output so removed stacks don't leave stale skills.
|
|
95
|
+
await removeGenerated(claudeDir);
|
|
96
|
+
|
|
56
97
|
// 1. Copy core hooks
|
|
57
98
|
await copyDir(join(TOOLKIT_ROOT, "core", "hooks"), join(claudeDir, "hooks"));
|
|
58
99
|
|
|
@@ -92,18 +133,30 @@ export async function generate(projectDir: string, config: ClaudeToolkitConfig):
|
|
|
92
133
|
"# Task-specific context",
|
|
93
134
|
"tasks/",
|
|
94
135
|
"",
|
|
136
|
+
"# Toolkit regeneration marker",
|
|
137
|
+
".toolkit-version",
|
|
138
|
+
"",
|
|
95
139
|
].join("\n"),
|
|
96
140
|
);
|
|
97
141
|
|
|
98
|
-
// 9. Scaffold base configs
|
|
99
|
-
|
|
142
|
+
// 9. Scaffold base configs into the project root (committed files). Skipped for
|
|
143
|
+
// automatic/postinstall regeneration so an install never writes committed files.
|
|
144
|
+
if (options.scaffold !== false) {
|
|
145
|
+
await scaffoldConfigs(projectDir, resolved, options.quiet);
|
|
146
|
+
}
|
|
100
147
|
|
|
101
148
|
// 10. Generate skills README
|
|
102
149
|
await generateSkillsReadme(claudeDir, resolved);
|
|
103
150
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
151
|
+
// Record the toolkit version that produced this output — drives auto-regen on update.
|
|
152
|
+
const { version } = await readJson<{ version: string }>(join(TOOLKIT_ROOT, "package.json"));
|
|
153
|
+
await writeFileEnsureDir(join(claudeDir, ".toolkit-version"), `${version}\n`);
|
|
154
|
+
|
|
155
|
+
if (!options.quiet) {
|
|
156
|
+
console.log(
|
|
157
|
+
`Generated .claude/ with ${resolved.stacks.length} stack(s) and ${resolved.skills.length} core skills`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
107
160
|
}
|
|
108
161
|
|
|
109
162
|
/** Generate skill-rules.json from resolved config */
|
|
@@ -271,7 +324,11 @@ async function generateSettings(claudeDir: string, resolved: ResolvedConfig): Pr
|
|
|
271
324
|
}
|
|
272
325
|
|
|
273
326
|
/** Scaffold base config files (biome.json, tsconfig.json) into the project */
|
|
274
|
-
async function scaffoldConfigs(
|
|
327
|
+
async function scaffoldConfigs(
|
|
328
|
+
projectDir: string,
|
|
329
|
+
resolved: ResolvedConfig,
|
|
330
|
+
quiet = false,
|
|
331
|
+
): Promise<void> {
|
|
275
332
|
if (resolved.config.scaffoldConfigs === false) return;
|
|
276
333
|
|
|
277
334
|
const configsDir = join(TOOLKIT_ROOT, "templates", "configs");
|
|
@@ -283,10 +340,10 @@ async function scaffoldConfigs(projectDir: string, resolved: ResolvedConfig): Pr
|
|
|
283
340
|
for (const { src, dest } of configs) {
|
|
284
341
|
const destPath = join(projectDir, dest);
|
|
285
342
|
if (exists(destPath)) {
|
|
286
|
-
console.log(` Skipped ${dest} (already exists)`);
|
|
343
|
+
if (!quiet) console.log(` Skipped ${dest} (already exists)`);
|
|
287
344
|
} else {
|
|
288
345
|
await fsCopyFile(join(configsDir, src), destPath);
|
|
289
|
-
console.log(` Scaffolded ${dest}`);
|
|
346
|
+
if (!quiet) console.log(` Scaffolded ${dest}`);
|
|
290
347
|
}
|
|
291
348
|
}
|
|
292
349
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
copyFile,
|
|
4
|
-
mkdir,
|
|
5
|
-
readdir,
|
|
6
|
-
readFile,
|
|
7
|
-
writeFile,
|
|
8
|
-
} from "node:fs/promises";
|
|
2
|
+
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
3
|
import { dirname, join } from "node:path";
|
|
10
4
|
|
|
11
5
|
/** Recursively copy a directory */
|
|
@@ -24,11 +18,13 @@ export async function copyDir(src: string, dest: string): Promise<void> {
|
|
|
24
18
|
}
|
|
25
19
|
}
|
|
26
20
|
|
|
21
|
+
/** Remove a file or directory recursively. No-op if the path doesn't exist. */
|
|
22
|
+
export async function removePath(path: string): Promise<void> {
|
|
23
|
+
await rm(path, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
27
26
|
/** Write a file, creating parent directories as needed */
|
|
28
|
-
export async function writeFileEnsureDir(
|
|
29
|
-
filePath: string,
|
|
30
|
-
content: string,
|
|
31
|
-
): Promise<void> {
|
|
27
|
+
export async function writeFileEnsureDir(filePath: string, content: string): Promise<void> {
|
|
32
28
|
await mkdir(dirname(filePath), { recursive: true });
|
|
33
29
|
await writeFile(filePath, content, "utf-8");
|
|
34
30
|
}
|
|
@@ -45,9 +41,6 @@ export function exists(filePath: string): boolean {
|
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
/** Simple template replacement: {{key}} → value */
|
|
48
|
-
export function renderTemplate(
|
|
49
|
-
template: string,
|
|
50
|
-
vars: Record<string, string>,
|
|
51
|
-
): string {
|
|
44
|
+
export function renderTemplate(template: string, vars: Record<string, string>): string {
|
|
52
45
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
53
46
|
}
|