claude-toolkit 0.1.24 → 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 +12 -0
- package/README.md +23 -6
- package/bin/cli.ts +86 -11
- package/package.json +1 -1
- package/src/detect.ts +204 -71
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.1.25 (2026-06-01)
|
|
12
|
+
|
|
13
|
+
- feat: add update command to sync detected stacks into existing config
|
|
14
|
+
|
|
3
15
|
## 0.1.24 (2026-06-01)
|
|
4
16
|
|
|
5
17
|
- feat: add capacitor stack with Capgo OTA live updates, channels, and encryption
|
package/README.md
CHANGED
|
@@ -45,11 +45,12 @@ export default defineConfig({
|
|
|
45
45
|
|
|
46
46
|
## CLI Commands
|
|
47
47
|
|
|
48
|
-
| Command
|
|
49
|
-
|
|
|
50
|
-
| `bunx claude-toolkit init`
|
|
51
|
-
| `bunx claude-toolkit
|
|
52
|
-
| `bunx claude-toolkit
|
|
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
|
|
|
@@ -75,9 +76,25 @@ Stack drift detected:
|
|
|
75
76
|
|
|
76
77
|
Suggested update in claude-toolkit.config.ts:
|
|
77
78
|
stacks: ["solidjs", "vite", "cloudflare", "playwright"]
|
|
79
|
+
|
|
80
|
+
Run "claude-toolkit update" to add detected stacks automatically.
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
`sync` is non-destructive — it reports drift but never edits your config.
|
|
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:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
Adding newly detected stacks to config:
|
|
89
|
+
+ capacitor — found @capacitor/core in dependencies
|
|
90
|
+
Updated claude-toolkit.config.ts
|
|
91
|
+
Generated .claude/ with 3 stack(s) and 4 core skills
|
|
92
|
+
Update complete.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Stacks already in your config that are no longer detected are reported but left unchanged (remove them manually if intended). If no config exists yet, `update` tells you to run `init` first.
|
|
96
|
+
|
|
97
|
+
This keeps your config aligned as your project evolves — `init` for first-time setup, `update` to pull in new stacks, `sync` to regenerate from the current config.
|
|
81
98
|
|
|
82
99
|
## Available Stacks
|
|
83
100
|
|
package/bin/cli.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* claude-toolkit CLI
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
|
-
* init
|
|
8
|
-
*
|
|
7
|
+
* init — Scaffold config file and generate .claude/ (first-time setup)
|
|
8
|
+
* update — Add newly detected stacks to an existing config, then regenerate
|
|
9
|
+
* sync — Regenerate .claude/ from existing config
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { existsSync } from "node:fs";
|
|
@@ -17,6 +18,24 @@ import type { ClaudeToolkitConfig } from "../src/types.js";
|
|
|
17
18
|
|
|
18
19
|
const CONFIG_FILENAME = "claude-toolkit.config.ts";
|
|
19
20
|
|
|
21
|
+
/** Build a `stacks: [...]` literal for injection into the config file */
|
|
22
|
+
function buildStacksLiteral(stacks: string[]): string {
|
|
23
|
+
return stacks.length > 0 ? `stacks: [${stacks.map((s) => `"${s}"`).join(", ")}]` : "stacks: []";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Rewrite the `stacks: [...]` array in an existing config file in place */
|
|
27
|
+
async function updateConfigStacks(configPath: string, stacks: string[]): Promise<void> {
|
|
28
|
+
const content = await readFile(configPath, "utf-8");
|
|
29
|
+
const stacksArray = /stacks:\s*\[[^\]]*\]/;
|
|
30
|
+
if (!stacksArray.test(content)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Could not find a "stacks: [...]" array in ${configPath} to update.\n` +
|
|
33
|
+
`Add it manually: ${buildStacksLiteral(stacks)}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
await writeFile(configPath, content.replace(stacksArray, buildStacksLiteral(stacks)), "utf-8");
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
async function loadConfig(projectDir: string): Promise<ClaudeToolkitConfig> {
|
|
21
40
|
const configPath = join(projectDir, CONFIG_FILENAME);
|
|
22
41
|
if (!existsSync(configPath)) {
|
|
@@ -31,8 +50,12 @@ async function init(projectDir: string): Promise<void> {
|
|
|
31
50
|
|
|
32
51
|
if (existsSync(configPath)) {
|
|
33
52
|
console.log(`Config already exists: ${configPath}`);
|
|
34
|
-
console.log("
|
|
35
|
-
|
|
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",
|
|
56
|
+
);
|
|
57
|
+
console.log(" claude-toolkit sync # regenerate .claude/ from the current config");
|
|
58
|
+
return;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
// Detect stacks
|
|
@@ -48,10 +71,7 @@ async function init(projectDir: string): Promise<void> {
|
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
// Build stacks literal for config injection
|
|
51
|
-
const stacksLiteral =
|
|
52
|
-
detected.length > 0
|
|
53
|
-
? `stacks: [${detected.map((d) => `"${d.name}"`).join(", ")}]`
|
|
54
|
-
: "stacks: []";
|
|
74
|
+
const stacksLiteral = buildStacksLiteral(detected.map((d) => d.name));
|
|
55
75
|
|
|
56
76
|
// Copy starter config with detected stacks injected
|
|
57
77
|
const templatePath = join(import.meta.dirname, "..", "templates", "claude-toolkit.config.ts");
|
|
@@ -117,13 +137,63 @@ async function sync(projectDir: string): Promise<void> {
|
|
|
117
137
|
]),
|
|
118
138
|
];
|
|
119
139
|
console.log(`\nSuggested update in ${CONFIG_FILENAME}:`);
|
|
120
|
-
console.log(`
|
|
140
|
+
console.log(` ${buildStacksLiteral(suggested)}`);
|
|
141
|
+
if (missing.length > 0) {
|
|
142
|
+
console.log('\nRun "claude-toolkit update" to add detected stacks automatically.\n');
|
|
143
|
+
}
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
await generate(projectDir, config);
|
|
124
147
|
console.log("Sync complete.");
|
|
125
148
|
}
|
|
126
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Update an existing config by adding newly detected stacks, then regenerate.
|
|
152
|
+
* Detected-but-missing stacks are added; stacks in config that are no longer
|
|
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
|
+
|
|
163
|
+
const config = await loadConfig(projectDir);
|
|
164
|
+
const detected = detectStacks(projectDir);
|
|
165
|
+
const configuredNames = new Set(config.stacks);
|
|
166
|
+
const detectedNames = new Set(detected.map((d) => d.name));
|
|
167
|
+
|
|
168
|
+
const missing = detected.filter((d) => !configuredNames.has(d.name));
|
|
169
|
+
const stale = config.stacks.filter((s) => !detectedNames.has(s));
|
|
170
|
+
|
|
171
|
+
if (missing.length === 0) {
|
|
172
|
+
console.log("Config is already up to date — all detected stacks are present.");
|
|
173
|
+
} else {
|
|
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
|
+
}
|
|
184
|
+
|
|
185
|
+
if (stale.length > 0) {
|
|
186
|
+
console.log("\nIn config but not detected (left unchanged):");
|
|
187
|
+
for (const s of stale) {
|
|
188
|
+
console.log(` - ${s}`);
|
|
189
|
+
}
|
|
190
|
+
console.log("Remove them from the config manually if they no longer apply.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await generate(projectDir, config);
|
|
194
|
+
console.log("Update complete.");
|
|
195
|
+
}
|
|
196
|
+
|
|
127
197
|
// CLI entry
|
|
128
198
|
const args = process.argv.slice(2);
|
|
129
199
|
const command = args[0];
|
|
@@ -136,18 +206,23 @@ switch (command) {
|
|
|
136
206
|
case "sync":
|
|
137
207
|
await sync(projectDir);
|
|
138
208
|
break;
|
|
209
|
+
case "update":
|
|
210
|
+
await update(projectDir);
|
|
211
|
+
break;
|
|
139
212
|
case undefined:
|
|
140
213
|
case "help":
|
|
141
214
|
console.log(`
|
|
142
215
|
claude-toolkit — Reusable Claude Code configuration
|
|
143
216
|
|
|
144
217
|
Commands:
|
|
145
|
-
init Scaffold config file and generate .claude/
|
|
146
|
-
|
|
218
|
+
init Scaffold config file and generate .claude/ (first-time setup)
|
|
219
|
+
update Add newly detected stacks to an existing config, then regenerate
|
|
220
|
+
sync Regenerate .claude/ from the current config
|
|
147
221
|
help Show this message
|
|
148
222
|
|
|
149
223
|
Usage:
|
|
150
224
|
bunx claude-toolkit init [project-dir]
|
|
225
|
+
bunx claude-toolkit update [project-dir]
|
|
151
226
|
bunx claude-toolkit sync [project-dir]
|
|
152
227
|
`);
|
|
153
228
|
break;
|
package/package.json
CHANGED
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: (
|
|
36
|
+
detect: (ctx: DetectContext) => DetectedStack | null;
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
37
|
-
return
|
|
124
|
+
function pkgLabel(rel: string): string {
|
|
125
|
+
return rel === "." ? "root package.json" : `${rel}/package.json`;
|
|
38
126
|
}
|
|
39
127
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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: (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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: (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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: (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
:
|
|
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: (
|
|
77
|
-
|
|
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: (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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: (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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: (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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: (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const config =
|
|
115
|
-
|
|
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: (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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: (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
278
|
+
const ctx = buildContext(projectDir);
|
|
146
279
|
const detected: DetectedStack[] = [];
|
|
147
280
|
for (const detector of DETECTORS) {
|
|
148
|
-
const result = detector.detect(
|
|
281
|
+
const result = detector.detect(ctx);
|
|
149
282
|
if (result) detected.push(result);
|
|
150
283
|
}
|
|
151
284
|
return detected;
|