claude-toolkit 0.1.25 → 0.1.27

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.27 (2026-06-01)
4
+
5
+ - feat: detect stacks across workspace packages and monorepo subdirectories
6
+
7
+ ## 0.1.26 (2026-06-01)
8
+
9
+ - docs: document update command in README CLI commands table
10
+
3
11
  ## 0.1.25 (2026-06-01)
4
12
 
5
13
  - feat: add update command to sync detected stacks into existing config
package/README.md CHANGED
@@ -45,11 +45,12 @@ export default defineConfig({
45
45
 
46
46
  ## CLI Commands
47
47
 
48
- | Command | Description |
49
- | -------------------------- | -------------------------------------------------------- |
50
- | `bunx claude-toolkit init` | Scaffold config file and generate `.claude/` |
51
- | `bunx claude-toolkit sync` | Regenerate `.claude/` from config (after toolkit update) |
52
- | `bunx claude-toolkit help` | Show available commands |
48
+ | Command | Description |
49
+ | ---------------------------- | ---------------------------------------------------- |
50
+ | `bunx claude-toolkit init` | Scaffold config and generate `.claude/` (first run) |
51
+ | `bunx claude-toolkit update` | Add newly detected stacks to config, then regenerate |
52
+ | `bunx claude-toolkit sync` | Regenerate `.claude/` from the current config |
53
+ | `bunx claude-toolkit help` | Show available commands |
53
54
 
54
55
  ## Stack Auto-Detection
55
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-toolkit",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Reusable Claude Code configuration toolkit with stack-specific connectors",
5
5
  "type": "module",
6
6
  "bin": {
package/src/detect.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { StackName } from "./types.js";
4
4
 
@@ -11,15 +11,51 @@ export interface DetectedStack {
11
11
  interface PackageJson {
12
12
  dependencies?: Record<string, string>;
13
13
  devDependencies?: Record<string, string>;
14
+ workspaces?: string[] | { packages?: string[] };
15
+ }
16
+
17
+ /** A directory to inspect for stack markers, with its project-relative label */
18
+ interface ScanRoot {
19
+ /** Absolute path */
20
+ dir: string;
21
+ /** Project-relative path ("." for the project root) */
22
+ rel: string;
23
+ }
24
+
25
+ /** Pre-resolved view of the project: every package and directory worth scanning */
26
+ interface DetectContext {
27
+ projectDir: string;
28
+ /** Package.json files found at the root and across workspaces/subdirs */
29
+ packages: { rel: string; pkg: PackageJson }[];
30
+ /** Directories to check for marker files (root + subdirs + workspace packages) */
31
+ scanRoots: ScanRoot[];
14
32
  }
15
33
 
16
34
  interface StackDetector {
17
35
  name: StackName;
18
- detect: (projectDir: string, pkg: PackageJson | null) => DetectedStack | null;
36
+ detect: (ctx: DetectContext) => DetectedStack | null;
19
37
  }
20
38
 
21
- function loadPackageJson(projectDir: string): PackageJson | null {
22
- const pkgPath = join(projectDir, "package.json");
39
+ /** Directories never worth scanning into */
40
+ const IGNORE_DIRS = new Set([
41
+ "node_modules",
42
+ "dist",
43
+ "build",
44
+ "out",
45
+ "target",
46
+ "coverage",
47
+ ".git",
48
+ ".wrangler",
49
+ ".next",
50
+ ".cache",
51
+ ".vercel",
52
+ ".turbo",
53
+ "playwright-report",
54
+ "test-results",
55
+ ]);
56
+
57
+ function readPackageJson(dir: string): PackageJson | null {
58
+ const pkgPath = join(dir, "package.json");
23
59
  if (!existsSync(pkgPath)) return null;
24
60
  try {
25
61
  return JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson;
@@ -28,19 +64,112 @@ function loadPackageJson(projectDir: string): PackageJson | null {
28
64
  }
29
65
  }
30
66
 
31
- function hasDep(pkg: PackageJson | null, name: string): boolean {
32
- if (!pkg) return false;
67
+ /** Immediate, non-ignored subdirectory names of a directory */
68
+ function listSubdirs(dir: string): string[] {
69
+ try {
70
+ return readdirSync(dir, { withFileTypes: true })
71
+ .filter((e) => e.isDirectory() && !IGNORE_DIRS.has(e.name) && !e.name.startsWith("."))
72
+ .map((e) => e.name);
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ /** Resolve workspace package directories (project-relative) from the root package.json */
79
+ function resolveWorkspaceDirs(projectDir: string, rootPkg: PackageJson | null): string[] {
80
+ const ws = rootPkg?.workspaces;
81
+ const patterns = Array.isArray(ws) ? ws : (ws?.packages ?? []);
82
+ const dirs: string[] = [];
83
+ for (const pattern of patterns) {
84
+ if (pattern.includes("*")) {
85
+ // Resolve a glob like "packages/*" or "apps/*" to concrete package dirs
86
+ const glob = new Bun.Glob(`${pattern}/package.json`);
87
+ for (const match of glob.scanSync({ cwd: projectDir, onlyFiles: true })) {
88
+ dirs.push(match.replace(/[/\\]package\.json$/, "").replace(/\\/g, "/"));
89
+ }
90
+ } else {
91
+ dirs.push(pattern.replace(/\\/g, "/"));
92
+ }
93
+ }
94
+ return dirs;
95
+ }
96
+
97
+ /** Build the set of directories and package.json files worth inspecting */
98
+ function buildContext(projectDir: string): DetectContext {
99
+ const rootPkg = readPackageJson(projectDir);
100
+
101
+ // Root + immediate subdirs + workspace packages (deduped)
102
+ const rels = new Set<string>(["."]);
103
+ for (const name of listSubdirs(projectDir)) rels.add(name);
104
+ for (const dir of resolveWorkspaceDirs(projectDir, rootPkg)) rels.add(dir);
105
+
106
+ const scanRoots: ScanRoot[] = [...rels].map((rel) => ({
107
+ rel,
108
+ dir: rel === "." ? projectDir : join(projectDir, rel),
109
+ }));
110
+
111
+ const packages: { rel: string; pkg: PackageJson }[] = [];
112
+ for (const { rel, dir } of scanRoots) {
113
+ const pkg = rel === "." ? rootPkg : readPackageJson(dir);
114
+ if (pkg) packages.push({ rel, pkg });
115
+ }
116
+
117
+ return { projectDir, packages, scanRoots };
118
+ }
119
+
120
+ function pkgHasDep(pkg: PackageJson, name: string): boolean {
33
121
  return name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {});
34
122
  }
35
123
 
36
- function fileExists(projectDir: string, ...segments: string[]): boolean {
37
- return existsSync(join(projectDir, ...segments));
124
+ function pkgLabel(rel: string): string {
125
+ return rel === "." ? "root package.json" : `${rel}/package.json`;
38
126
  }
39
127
 
40
- function rootConfigExists(projectDir: string, prefix: string): string | null {
41
- const glob = new Bun.Glob(`${prefix}.*`);
42
- for (const match of glob.scanSync({ cwd: projectDir, onlyFiles: true })) {
43
- return match;
128
+ function relPath(rel: string, name: string): string {
129
+ return rel === "." ? name : `${rel}/${name}`;
130
+ }
131
+
132
+ /** First package (project-relative dir) declaring `name`, or null */
133
+ function findDep(ctx: DetectContext, name: string): string | null {
134
+ for (const { rel, pkg } of ctx.packages) {
135
+ if (pkgHasDep(pkg, name)) return rel;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /** First scan root containing a file/dir named `name` (top-level only), as a project-relative path */
141
+ function findFile(ctx: DetectContext, name: string): string | null {
142
+ for (const { rel, dir } of ctx.scanRoots) {
143
+ if (existsSync(join(dir, name))) return relPath(rel, name);
144
+ }
145
+ return null;
146
+ }
147
+
148
+ /** First scan root containing a `${prefix}.*` config file (top-level only) */
149
+ function findConfig(ctx: DetectContext, prefix: string): string | null {
150
+ for (const { rel, dir } of ctx.scanRoots) {
151
+ const glob = new Bun.Glob(`${prefix}.*`);
152
+ for (const match of glob.scanSync({ cwd: dir, onlyFiles: true })) {
153
+ return relPath(rel, match);
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /** Look for `*.proto` files directly in scan roots or under a conventional `proto/` dir */
160
+ function findProtoFiles(ctx: DetectContext): string | null {
161
+ for (const { rel, dir } of ctx.scanRoots) {
162
+ const direct = new Bun.Glob("*.proto");
163
+ for (const match of direct.scanSync({ cwd: dir, onlyFiles: true })) {
164
+ return relPath(rel, match);
165
+ }
166
+ const protoDir = join(dir, "proto");
167
+ if (existsSync(protoDir)) {
168
+ const nested = new Bun.Glob("**/*.proto");
169
+ for (const match of nested.scanSync({ cwd: protoDir, onlyFiles: true })) {
170
+ return relPath(rel, `proto/${match}`);
171
+ }
172
+ }
44
173
  }
45
174
  return null;
46
175
  }
@@ -48,104 +177,108 @@ function rootConfigExists(projectDir: string, prefix: string): string | null {
48
177
  const DETECTORS: StackDetector[] = [
49
178
  {
50
179
  name: "solidjs",
51
- detect: (_dir, pkg) =>
52
- hasDep(pkg, "solid-js")
53
- ? { name: "solidjs", reason: "found solid-js in dependencies" }
54
- : null,
180
+ detect: (ctx) => {
181
+ const at = findDep(ctx, "solid-js");
182
+ return at ? { name: "solidjs", reason: `found solid-js in ${pkgLabel(at)}` } : null;
183
+ },
55
184
  },
56
185
  {
57
186
  name: "vite",
58
- detect: (dir, pkg) => {
59
- if (hasDep(pkg, "vite")) return { name: "vite", reason: "found vite in dependencies" };
60
- const viteConfig = rootConfigExists(dir, "vite.config");
61
- if (viteConfig) return { name: "vite", reason: `found ${viteConfig}` };
62
- const vitestConfig = rootConfigExists(dir, "vitest.config");
63
- if (vitestConfig) return { name: "vite", reason: `found ${vitestConfig}` };
64
- return null;
187
+ detect: (ctx) => {
188
+ const dep = findDep(ctx, "vite");
189
+ if (dep) return { name: "vite", reason: `found vite in ${pkgLabel(dep)}` };
190
+ const config = findConfig(ctx, "vite.config") ?? findConfig(ctx, "vitest.config");
191
+ return config ? { name: "vite", reason: `found ${config}` } : null;
65
192
  },
66
193
  },
67
194
  {
68
195
  name: "vanilla-extract",
69
- detect: (_dir, pkg) =>
70
- hasDep(pkg, "@vanilla-extract/css")
71
- ? { name: "vanilla-extract", reason: "found @vanilla-extract/css in dependencies" }
72
- : null,
196
+ detect: (ctx) => {
197
+ const at = findDep(ctx, "@vanilla-extract/css");
198
+ return at
199
+ ? { name: "vanilla-extract", reason: `found @vanilla-extract/css in ${pkgLabel(at)}` }
200
+ : null;
201
+ },
73
202
  },
74
203
  {
75
204
  name: "rust-wasm",
76
- detect: (dir) =>
77
- fileExists(dir, "Cargo.toml") ? { name: "rust-wasm", reason: "found Cargo.toml" } : null,
205
+ detect: (ctx) => {
206
+ const f = findFile(ctx, "Cargo.toml");
207
+ return f ? { name: "rust-wasm", reason: `found ${f}` } : null;
208
+ },
78
209
  },
79
210
  {
80
211
  name: "protobuf",
81
- detect: (dir) => {
82
- if (fileExists(dir, "buf.yaml")) return { name: "protobuf", reason: "found buf.yaml" };
83
- if (fileExists(dir, "buf.gen.yaml"))
84
- return { name: "protobuf", reason: "found buf.gen.yaml" };
85
- const glob = new Bun.Glob("**/*.proto");
86
- for (const _match of glob.scanSync({ cwd: dir, onlyFiles: true })) {
87
- return { name: "protobuf", reason: "found .proto files" };
88
- }
89
- return null;
212
+ detect: (ctx) => {
213
+ const buf =
214
+ findFile(ctx, "buf.yaml") ??
215
+ findFile(ctx, "buf.gen.yaml") ??
216
+ findFile(ctx, "buf.work.yaml");
217
+ if (buf) return { name: "protobuf", reason: `found ${buf}` };
218
+ const proto = findProtoFiles(ctx);
219
+ return proto ? { name: "protobuf", reason: `found ${proto}` } : null;
90
220
  },
91
221
  },
92
222
  {
93
223
  name: "cloudflare",
94
- detect: (dir) => {
95
- if (fileExists(dir, "wrangler.toml"))
96
- return { name: "cloudflare", reason: "found wrangler.toml" };
97
- if (fileExists(dir, "wrangler.jsonc"))
98
- return { name: "cloudflare", reason: "found wrangler.jsonc" };
99
- return null;
224
+ detect: (ctx) => {
225
+ const f =
226
+ findFile(ctx, "wrangler.toml") ??
227
+ findFile(ctx, "wrangler.jsonc") ??
228
+ findFile(ctx, "wrangler.json");
229
+ return f ? { name: "cloudflare", reason: `found ${f}` } : null;
100
230
  },
101
231
  },
102
232
  {
103
233
  name: "i18n-typesafe",
104
- detect: (_dir, pkg) =>
105
- hasDep(pkg, "typesafe-i18n")
106
- ? { name: "i18n-typesafe", reason: "found typesafe-i18n in dependencies" }
107
- : null,
234
+ detect: (ctx) => {
235
+ const at = findDep(ctx, "typesafe-i18n");
236
+ if (at) return { name: "i18n-typesafe", reason: `found typesafe-i18n in ${pkgLabel(at)}` };
237
+ const f = findFile(ctx, ".typesafe-i18n.json");
238
+ return f ? { name: "i18n-typesafe", reason: `found ${f}` } : null;
239
+ },
108
240
  },
109
241
  {
110
242
  name: "playwright",
111
- detect: (dir, pkg) => {
112
- if (hasDep(pkg, "@playwright/test"))
113
- return { name: "playwright", reason: "found @playwright/test in dependencies" };
114
- const config = rootConfigExists(dir, "playwright.config");
115
- if (config) return { name: "playwright", reason: `found ${config}` };
116
- return null;
243
+ detect: (ctx) => {
244
+ const dep = findDep(ctx, "@playwright/test");
245
+ if (dep) return { name: "playwright", reason: `found @playwright/test in ${pkgLabel(dep)}` };
246
+ const config = findConfig(ctx, "playwright.config");
247
+ return config ? { name: "playwright", reason: `found ${config}` } : null;
117
248
  },
118
249
  },
119
250
  {
120
251
  name: "storybook",
121
- detect: (dir, pkg) => {
122
- if (hasDep(pkg, "storybook"))
123
- return { name: "storybook", reason: "found storybook in dependencies" };
124
- if (fileExists(dir, ".storybook"))
125
- return { name: "storybook", reason: "found .storybook/ directory" };
126
- return null;
252
+ detect: (ctx) => {
253
+ const dep = findDep(ctx, "storybook");
254
+ if (dep) return { name: "storybook", reason: `found storybook in ${pkgLabel(dep)}` };
255
+ const dir = findFile(ctx, ".storybook");
256
+ return dir ? { name: "storybook", reason: `found ${dir}/ directory` } : null;
127
257
  },
128
258
  },
129
259
  {
130
260
  name: "capacitor",
131
- detect: (dir, pkg) => {
132
- if (hasDep(pkg, "@capgo/capacitor-updater"))
133
- return { name: "capacitor", reason: "found @capgo/capacitor-updater in dependencies" };
134
- if (hasDep(pkg, "@capacitor/core"))
135
- return { name: "capacitor", reason: "found @capacitor/core in dependencies" };
136
- const config = rootConfigExists(dir, "capacitor.config");
137
- if (config) return { name: "capacitor", reason: `found ${config}` };
138
- return null;
261
+ detect: (ctx) => {
262
+ const capgo = findDep(ctx, "@capgo/capacitor-updater");
263
+ if (capgo)
264
+ return {
265
+ name: "capacitor",
266
+ reason: `found @capgo/capacitor-updater in ${pkgLabel(capgo)}`,
267
+ };
268
+ const core = findDep(ctx, "@capacitor/core");
269
+ if (core) return { name: "capacitor", reason: `found @capacitor/core in ${pkgLabel(core)}` };
270
+ const config = findConfig(ctx, "capacitor.config");
271
+ return config ? { name: "capacitor", reason: `found ${config}` } : null;
139
272
  },
140
273
  },
141
274
  ];
142
275
 
143
- /** Scan a project directory and detect which stacks are present */
276
+ /** Scan a project directory (root + subdirs + workspace packages) and detect which stacks are present */
144
277
  export function detectStacks(projectDir: string): DetectedStack[] {
145
- const pkg = loadPackageJson(projectDir);
278
+ const ctx = buildContext(projectDir);
146
279
  const detected: DetectedStack[] = [];
147
280
  for (const detector of DETECTORS) {
148
- const result = detector.detect(projectDir, pkg);
281
+ const result = detector.detect(ctx);
149
282
  if (result) detected.push(result);
150
283
  }
151
284
  return detected;