claude-setup 1.1.1 → 1.1.2

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/README.md CHANGED
@@ -1,45 +1,119 @@
1
- # claude-setup
2
-
3
- Setup layer for Claude Code. Reads your project, writes command files, Claude Code does the rest.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npx claude-setup init
9
- ```
10
-
11
- ## Commands
12
-
13
- | Command | What it does |
14
- |---------|-------------|
15
- | `npx claude-setup init` | Full project setup — new or existing |
16
- | `npx claude-setup add` | Add a multi-file capability |
17
- | `npx claude-setup sync` | Update setup after project changes |
18
- | `npx claude-setup status` | Show current setup |
19
- | `npx claude-setup doctor` | Validate environment |
20
- | `npx claude-setup remove` | Remove a capability cleanly |
21
-
22
- ## How it works
23
-
24
- 1. **CLI collects** reads project files (configs, source samples) with strict cost controls
25
- 2. **CLI writes command files** — assembles markdown instructions into `.claude/commands/`
26
- 3. **Claude Code executes** — you run `/stack-init` (or `/stack-sync`, etc.) in Claude Code
27
-
28
- The CLI has zero intelligence. All reasoning is delegated to Claude Code via the command files.
29
-
30
- ## Three project states
31
-
32
- - **Empty project** — Claude Code asks 3 discovery questions, then sets up a tailored environment
33
- - **In development** — reads existing files, writes setup that references actual code patterns
34
- - **Production** same as development; merge rules protect existing Claude config (append only, never rewrite)
35
-
36
- ## What it creates
37
-
38
- - `CLAUDE.md`project-specific context for Claude Code
39
- - `.mcp.json` — MCP server connections (only if evidenced by project files)
40
- - `.claude/settings.json` hooks (only if warranted)
41
- - `.claude/skills/` — reusable patterns (only if recurring)
42
- - `.claude/commands/`project-specific slash commands
43
- - `.github/workflows/`CI workflows (only if `.github/` exists)
44
-
45
-
1
+ # claude-setup
2
+
3
+ Setup layer for Claude Code. Reads your project, writes command files, Claude Code does the rest.
4
+
5
+ **The CLI has zero intelligence.** All reasoning is delegated to Claude Code via the command files. The CLI reads files. Claude Code decides.
6
+
7
+ ## Install & Quick Start
8
+
9
+ ```bash
10
+ npx claude-setup init
11
+ ```
12
+
13
+ Then open Claude Code and run `/stack-init`.
14
+
15
+ ## Commands
16
+
17
+ | Command | What it does |
18
+ |---------|-------------|
19
+ | `npx claude-setup init` | Full project setup — new or existing. Detects empty projects automatically. |
20
+ | `npx claude-setup add` | Add a multi-file capability (MCP + hooks + skills together) |
21
+ | `npx claude-setup sync` | Update setup after project changes (uses diff, not full re-scan) |
22
+ | `npx claude-setup status` | Show current setup state — OS, servers, hooks, staleness |
23
+ | `npx claude-setup doctor` | Validate environment — OS/MCP format, hook quoting, env vars, stale skills |
24
+ | `npx claude-setup remove` | Remove a capability cleanly with dangling reference detection |
25
+
26
+ ### Flags
27
+
28
+ ```bash
29
+ npx claude-setup init --dry-run # Preview without writing
30
+ npx claude-setup sync --dry-run # Show changes without writing
31
+ npx claude-setup doctor --verbose # Include passing checks in output
32
+ ```
33
+
34
+ ## How it works
35
+
36
+ 1. **CLI collects** — reads project files (configs, source samples) with strict token cost controls
37
+ 2. **CLI writes command files** — assembles markdown instructions into `.claude/commands/`
38
+ 3. **Claude Code executes** you run `/stack-init` (or `/stack-sync`, etc.) in Claude Code
39
+
40
+ ## Three project states
41
+
42
+ - **Empty project** Claude Code asks 3 discovery questions, then sets up a tailored environment
43
+ - **In development** reads existing files, writes setup that references actual code patterns
44
+ - **Production** — same as development; merge rules protect existing Claude config (append only, never rewrite)
45
+
46
+ ## What it creates
47
+
48
+ - `CLAUDE.md` — project-specific context for Claude Code
49
+ - `.mcp.json` — MCP server connections (only if evidenced by project files, OS-correct format)
50
+ - `.claude/settings.json` — hooks (only if warranted, OS-correct shell format)
51
+ - `.claude/skills/` — reusable patterns (only if recurring, with `applies-when` frontmatter)
52
+ - `.claude/commands/` — project-specific slash commands
53
+ - `.github/workflows/` — CI workflows (only if `.github/` exists)
54
+
55
+ ## Token cost controls
56
+
57
+ Every byte injected into command files costs tokens. The CLI enforces:
58
+
59
+ | Control | Default |
60
+ |---------|---------|
61
+ | Init token budget | 12,000 |
62
+ | Sync token budget | 6,000 |
63
+ | Add token budget | 3,000 |
64
+ | Remove token budget | 2,000 |
65
+ | Max source files sampled | 15 |
66
+ | Max file size | 80KB |
67
+ | Max depth | 6 levels |
68
+
69
+ ### File-specific truncation
70
+
71
+ | File | Strategy |
72
+ |------|----------|
73
+ | `package-lock.json` | Extract `{ name, version, lockfileVersion }` only |
74
+ | `Dockerfile` | First 50 lines |
75
+ | `docker-compose.yml` | First 100 lines if > 8KB |
76
+ | `pom.xml`, `build.gradle*` | First 80 lines |
77
+ | `setup.py` | First 60 lines |
78
+ | `*.config.{js,ts,mjs}` | First 100 lines |
79
+
80
+ ## Configuration
81
+
82
+ Create `.claude-setup.json` in your project root to customize:
83
+
84
+ ```json
85
+ {
86
+ "maxSourceFiles": 15,
87
+ "maxDepth": 6,
88
+ "maxFileSizeKB": 80,
89
+ "tokenBudget": {
90
+ "init": 12000,
91
+ "sync": 6000,
92
+ "add": 3000,
93
+ "remove": 2000
94
+ },
95
+ "digestMode": true,
96
+ "extraBlockedDirs": ["my-custom-dir"],
97
+ "sourceDirs": ["src", "lib"]
98
+ }
99
+ ```
100
+
101
+ ## Digest mode
102
+
103
+ When `digestMode` is enabled (default), the CLI extracts compact signal instead of dumping raw file content:
104
+
105
+ - **Config files found** — just names, not content
106
+ - **Dependencies** — extracted from any package manifest
107
+ - **Scripts** — available commands/tasks
108
+ - **Env vars** — names from `.env.example`
109
+ - **Directory tree** — compact structure (3 levels deep)
110
+ - **Source signatures** — imports, exports, declarations (not full content)
111
+
112
+ ## OS detection
113
+
114
+ The CLI detects your OS and ensures command files tell Claude Code to use the correct format:
115
+
116
+ - **Windows**: `{ "command": "cmd", "args": ["/c", "npx", "<package>"] }`
117
+ - **macOS/Linux**: `{ "command": "npx", "args": ["<package>"] }`
118
+
119
+ `doctor` checks for mismatches and reports them as critical issues.
package/dist/builder.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from "fs";
2
2
  import { join, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { loadConfig } from "./config.js";
5
+ import { detectOS } from "./os.js";
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  const TEMPLATES_DIR = join(__dirname, "..", "templates");
7
8
  function estimateTokens(content) {
@@ -63,6 +64,9 @@ function formatSourceContext(source) {
63
64
  }
64
65
  // --- Template variable building ---
65
66
  function buildVars(collected, state) {
67
+ const skippedList = collected.skipped.length > 0
68
+ ? collected.skipped.map(s => `- ${s.path} — ${s.reason}`).join("\n")
69
+ : "";
66
70
  return {
67
71
  VERSION: getVersion(),
68
72
  DATE: new Date().toISOString().split("T")[0],
@@ -81,11 +85,14 @@ function buildVars(collected, state) {
81
85
  COMMANDS_LIST: formatList(state.commands),
82
86
  WORKFLOWS_LIST: formatList(state.workflows),
83
87
  HAS_GITHUB_DIR: state.hasGithubDir ? "yes" : "no",
88
+ SKIPPED_LIST: skippedList,
89
+ DETECTED_OS: detectOS(),
84
90
  };
85
91
  }
86
92
  function buildFlags(_collected, state) {
87
93
  return {
88
94
  HAS_SOURCE: _collected.source.length > 0,
95
+ HAS_SKIPPED: _collected.skipped.length > 0,
89
96
  HAS_CLAUDE_MD: state.claudeMd.exists,
90
97
  HAS_MCP_JSON: state.mcpJson.exists,
91
98
  HAS_SETTINGS: state.settings.exists,
@@ -162,9 +169,10 @@ export function buildAtomicSteps(collected, state) {
162
169
  const version = getVersion();
163
170
  const date = new Date().toISOString().split("T")[0];
164
171
  const vars = buildVars(collected, state);
172
+ const os = detectOS();
165
173
  const header = `<!-- claude-setup ${version} ${date} -->\n`;
166
- const check = `Check if target already has this content. If yes: print "SKIPPED" and stop. Write only what's missing.\n\n`;
167
- // Shared context block — written once to a reference file, not duplicated
174
+ const preamble = `Before writing: check if what you are about to write already exists in the target file (current content provided below if it exists). If already up to date: print "SKIPPED — already up to date" and stop. Write only what is genuinely missing.\n\nRead /stack-0-context for full project info.\n\n`;
175
+ // Shared context block — written once, referenced by all steps
168
176
  const sharedContext = header +
169
177
  `## Project\n\n${vars.PROJECT_CONTEXT}\n\n` +
170
178
  `{{#if HAS_SOURCE}}## Source samples\n\n${vars.SOURCE_CONTEXT}\n{{/if}}`;
@@ -174,57 +182,148 @@ export function buildAtomicSteps(collected, state) {
174
182
  filename: "stack-0-context.md",
175
183
  content: sharedContextProcessed,
176
184
  },
185
+ // --- Step 1: CLAUDE.md ---
177
186
  {
178
187
  filename: "stack-1-claude-md.md",
179
- content: header + check +
180
- `Read /stack-0-context for project info.\n\n` +
181
- `## Target: CLAUDE.md\n` +
188
+ content: header + preamble +
189
+ `## Target: CLAUDE.md\n\n` +
182
190
  (state.claudeMd.exists
183
- ? `Current content (append only, never rewrite):\n${vars.CLAUDE_MD_CONTENT}\n\n`
184
- : `Does not exist. Create it.\n\n`) +
185
- `Write CLAUDE.md specific to this project. Reference actual paths and patterns. No generic boilerplate.`,
191
+ ? `### Current content APPEND ONLY, never rewrite, never remove:\n${vars.CLAUDE_MD_CONTENT}\n\n`
192
+ : `Does not exist create it.\n\n`) +
193
+ `### What to write\n` +
194
+ `CLAUDE.md is the most valuable artifact. Make it specific to THIS project:\n` +
195
+ `- **Purpose**: one sentence describing what this project does\n` +
196
+ `- **Runtime**: language, framework, key dependencies from /stack-0-context\n` +
197
+ `- **Key dirs**: reference actual directory paths from the project tree\n` +
198
+ `- **Run/test/build commands**: extract from scripts in /stack-0-context\n` +
199
+ `- **Non-obvious conventions**: patterns you see in the source samples\n\n` +
200
+ `### Rules\n` +
201
+ `- Every line must reference something you actually saw in /stack-0-context\n` +
202
+ `- No generic boilerplate. Two different projects must produce two different CLAUDE.md files\n` +
203
+ `- If it exists above: read it fully, add only what is genuinely missing\n\n` +
204
+ `### Output\n` +
205
+ `Created/Updated: ✅ CLAUDE.md — [one clause: what you wrote and why]\n` +
206
+ `Skipped: ⏭ CLAUDE.md — [why not needed]\n`,
186
207
  },
208
+ // --- Step 2: .mcp.json ---
187
209
  {
188
210
  filename: "stack-2-mcp.md",
189
- content: header + check +
190
- `Read /stack-0-context for project info.\n\n` +
191
- `## Target: .mcp.json\n` +
211
+ content: header + preamble +
212
+ `## Target: .mcp.json\n\n` +
192
213
  (state.mcpJson.exists
193
- ? `Current (merge only, never remove):\n${vars.MCP_JSON_CONTENT}\n\n`
194
- : `Does not exist. Create only if services are evidenced.\n\n`) +
195
- `No evidence = no server. Valid JSON only.`,
214
+ ? `### Current content — MERGE ONLY, never remove existing entries:\n${vars.MCP_JSON_CONTENT}\n\n`
215
+ : `Does not exist.\n\n`) +
216
+ `### When to create/update\n` +
217
+ `Add an MCP server ONLY if you find evidence in /stack-0-context:\n` +
218
+ `- Import statement referencing an external service\n` +
219
+ `- docker-compose service (database, cache, queue)\n` +
220
+ `- Env var name in .env.example matching a known service pattern\n` +
221
+ `- Explicit dependency on an MCP-compatible package\n\n` +
222
+ `No evidence = no server. Do not invent services.\n\n` +
223
+ `### OS-correct format (detected: ${os})\n` +
224
+ (os === "Windows"
225
+ ? `Use: \`{ "command": "cmd", "args": ["/c", "npx", "<package>"] }\`\n`
226
+ : `Use: \`{ "command": "npx", "args": ["<package>"] }\`\n`) +
227
+ `\n### Rules\n` +
228
+ `- All env var refs use \`\${VARNAME}\` syntax\n` +
229
+ `- Produce valid JSON only\n` +
230
+ `- If creating: document every new env var in .env.example\n\n` +
231
+ `### Output\n` +
232
+ `Created/Updated: ✅ .mcp.json — [what server and evidence source]\n` +
233
+ `Skipped: ⏭ .mcp.json — checked [files], found [nothing], no action\n`,
196
234
  },
235
+ // --- Step 3: .claude/settings.json ---
197
236
  {
198
237
  filename: "stack-3-settings.md",
199
- content: header + check +
200
- `Read /stack-0-context for project info.\n\n` +
201
- `## Target: .claude/settings.json\n` +
238
+ content: header + preamble +
239
+ `## Target: .claude/settings.json\n\n` +
202
240
  (state.settings.exists
203
- ? `Current (merge only, never remove hooks):\n${vars.SETTINGS_CONTENT}\n\n`
204
- : `Does not exist. Create only if hooks earn their cost.\n\n`) +
205
- `Every hook runs on every action. Only add if clearly justified.`,
241
+ ? `### Current content — MERGE ONLY, never remove existing hooks:\n${vars.SETTINGS_CONTENT}\n\n`
242
+ : `Does not exist.\n\n`) +
243
+ `### When to create/update\n` +
244
+ `Add a hook ONLY if it runs on a pattern that repeats every session AND the cost is justified.\n` +
245
+ `Every hook adds overhead on every Claude Code action. Only add if clearly earned.\n\n` +
246
+ `### OS-correct hook format (detected: ${os})\n` +
247
+ (os === "Windows"
248
+ ? `Use: \`{ "command": "cmd", "args": ["/c", "<command>"] }\`\n`
249
+ : `Use: \`{ "command": "bash", "args": ["-c", "<command>"] }\`\n` +
250
+ `**Bash quoting rule**: never use bare \`"\` inside \`-c "..."\` — use \`\\x22\` instead.\n` +
251
+ `Replace \`'\` with \`\\x27\`, \`$\` in character classes with \`\\x24\`.\n`) +
252
+ `\n### Rules\n` +
253
+ `- If it exists above: audit quoting of existing hooks first, fix broken ones\n` +
254
+ `- Only add hooks for patterns that genuinely recur for this project type\n` +
255
+ `- Produce valid JSON only\n\n` +
256
+ `### Output\n` +
257
+ `Created/Updated: ✅ settings.json — [hook name and justification]\n` +
258
+ `Skipped: ⏭ settings.json — [why no hooks warranted]\n`,
206
259
  },
260
+ // --- Step 4: .claude/skills/ ---
207
261
  {
208
262
  filename: "stack-4-skills.md",
209
- content: header + check +
210
- `Read /stack-0-context for project info.\n\n` +
211
- `## Target: .claude/skills/\nInstalled: ${vars.SKILLS_LIST}\n\n` +
212
- `Only for recurring patterns. Use applies-when frontmatter. Empty is fine.`,
263
+ content: header + preamble +
264
+ `## Target: .claude/skills/\n` +
265
+ `Installed: ${vars.SKILLS_LIST}\n\n` +
266
+ `### When to create\n` +
267
+ `Create a skill ONLY if:\n` +
268
+ `- A recurring multi-step project-specific pattern exists in /stack-0-context\n` +
269
+ `- It is NOT something Claude already knows (standard patterns don't need skills)\n` +
270
+ `- It will save time across multiple Claude Code sessions\n\n` +
271
+ `### Rules\n` +
272
+ `- Use \`applies-when:\` frontmatter so skills load only when relevant, not every message\n` +
273
+ `- If a similar skill already exists above: extend it, don't create a parallel one\n` +
274
+ `- Empty is valid — no skills is better than useless skills\n\n` +
275
+ `### Output\n` +
276
+ `Created: ✅ .claude/skills/[name] — [what pattern it captures]\n` +
277
+ `Skipped: ⏭ skills — checked [patterns], found [nothing project-specific]\n`,
213
278
  },
279
+ // --- Step 5: .claude/commands/ ---
214
280
  {
215
281
  filename: "stack-5-commands.md",
216
- content: header + check +
217
- `Read /stack-0-context for project info.\n\n` +
218
- `## Target: .claude/commands/ (not stack-*.md)\nInstalled: ${vars.COMMANDS_LIST}\n\n` +
219
- `Only useful commands for this project type. No duplicates.`,
282
+ content: header + preamble +
283
+ `## Target: .claude/commands/ (excluding stack-*.md those are setup artifacts)\n` +
284
+ `Installed: ${vars.COMMANDS_LIST}\n\n` +
285
+ `### When to create\n` +
286
+ `Create a command ONLY for project-specific multi-step workflows a developer repeats:\n` +
287
+ `- Deploy sequences\n` +
288
+ `- Database migration + seed\n` +
289
+ `- Release workflows\n` +
290
+ `- Environment setup for a new contributor\n\n` +
291
+ `Do NOT create commands for things expressible as a single shell alias.\n` +
292
+ `Look at the scripts in /stack-0-context for evidence of multi-step workflows.\n\n` +
293
+ `### Rules\n` +
294
+ `- If existing commands cover the same workflow: skip\n` +
295
+ `- Commands should be specific to this project, not generic\n\n` +
296
+ `### Output\n` +
297
+ `Created: ✅ .claude/commands/[name].md — [what workflow and why useful]\n` +
298
+ `Skipped: ⏭ commands — [why no project-specific workflows found]\n`,
220
299
  },
300
+ // --- Step 6: .github/workflows/ ---
221
301
  {
222
302
  filename: "stack-6-workflows.md",
223
- content: header + check +
224
- `## Target: .github/workflows/\n.github/ exists: ${vars.HAS_GITHUB_DIR}\nInstalled: ${vars.WORKFLOWS_LIST}\n\n` +
303
+ content: header +
304
+ `## Target: .github/workflows/\n` +
305
+ `.github/ exists: ${vars.HAS_GITHUB_DIR}\n` +
306
+ `Installed: ${vars.WORKFLOWS_LIST}\n\n` +
225
307
  (state.hasGithubDir
226
- ? `Only warranted workflows. Don't touch existing ones.`
227
- : `Only create if clearly warranted.`),
308
+ ? (`### What to do\n` +
309
+ `Check /stack-0-context for CI evidence: tests dir, Dockerfile, CI-related scripts.\n` +
310
+ `If evidence found, print EXACTLY:\n\n` +
311
+ `\`\`\`\n` +
312
+ `⚙️ CI/CD GATE — action required\n\n` +
313
+ `Evidence found:\n` +
314
+ ` [list each piece of evidence]\n\n` +
315
+ `Two questions before I proceed:\n` +
316
+ ` 1. Set up CI/CD? (yes / no / later)\n` +
317
+ ` 2. Connected to a remote GitHub repo? (yes / no)\n\n` +
318
+ `I will not write .github/workflows/ until you answer.\n` +
319
+ `\`\`\`\n\n` +
320
+ `### Rules\n` +
321
+ `- NEVER create or modify workflows without explicit developer confirmation\n` +
322
+ `- If existing workflows exist: do not touch them\n` +
323
+ `- Secrets must use \`\${{ secrets.VARNAME }}\` syntax only\n`)
324
+ : (`### .github/ does not exist\n` +
325
+ `Do not create workflows. Print:\n` +
326
+ `Skipped: ⏭ .github/workflows/ — .github/ directory does not exist\n`)),
228
327
  },
229
328
  ];
230
329
  return steps;
package/dist/collect.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, existsSync, statSync, readdirSync } from "fs";
2
2
  import { glob } from "glob";
3
3
  import { join, extname, basename, dirname } from "path";
4
- import { loadConfig } from "./config.js";
4
+ import { loadConfig, applyTruncation } from "./config.js";
5
5
  // --- Blocklists ---
6
6
  const BLOCKED_DIRS = new Set([
7
7
  "node_modules", "vendor", ".venv", "venv", "env", "__pypackages__",
@@ -22,7 +22,7 @@ const BLOCKED_EXTENSIONS = new Set([
22
22
  ]);
23
23
  const BLOCKED_FILES = new Set([
24
24
  "go.sum", "poetry.lock", "Pipfile.lock", "composer.lock",
25
- ".DS_Store", "Thumbs.db", "package-lock.json",
25
+ ".DS_Store", "Thumbs.db",
26
26
  ]);
27
27
  const SOURCE_EXTENSIONS = new Set([
28
28
  ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
@@ -35,7 +35,8 @@ const ENTRY_DIRS = [".", "src", "app", "cmd", "bin"];
35
35
  const PRIMARY_SOURCE_DIRS = ["src", "app", "lib", "core", "pkg", "internal", "api", "cmd"];
36
36
  // Config files the CLI knows how to find — but NOT what they mean
37
37
  const KNOWN_CONFIG_FILES = [
38
- "package.json", "pyproject.toml", "setup.py", "requirements.txt", "Pipfile",
38
+ "package.json", "package-lock.json", "pyproject.toml", "setup.py",
39
+ "requirements.txt", "Pipfile",
39
40
  "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts",
40
41
  "composer.json", "Gemfile", "turbo.json", "nx.json", "pnpm-workspace.yaml",
41
42
  "lerna.json", ".env.example", ".env.sample", ".env.template",
@@ -441,18 +442,52 @@ function truncateSource(content) {
441
442
  }
442
443
  // --- Legacy raw config collection ---
443
444
  async function collectRawConfigs(cwd, configs, skipped) {
445
+ const config = loadConfig(cwd);
444
446
  for (const name of KNOWN_CONFIG_FILES) {
445
447
  const p = join(cwd, name);
446
448
  if (existsSync(p)) {
447
449
  try {
448
- const content = readFileSync(p, "utf8");
449
- configs[name] = content.length > 4000 ? content.slice(0, 4000) + "\n[... truncated]" : content;
450
+ const raw = readFileSync(p, "utf8");
451
+ // Dynamic truncation driven by config, not hardcoded
452
+ configs[name] = applyTruncation(name, raw, config);
450
453
  }
451
454
  catch {
452
455
  skipped.push({ path: name, reason: "could not read" });
453
456
  }
454
457
  }
455
458
  }
459
+ // Scan for *.config.{js,ts,mjs} at root level — truncation from config
460
+ try {
461
+ for (const ext of ["js", "ts", "mjs"]) {
462
+ const matches = await glob(`*.config.${ext}`, { cwd, nodir: true });
463
+ for (const match of matches) {
464
+ if (configs[match])
465
+ continue;
466
+ const p = join(cwd, match);
467
+ try {
468
+ const content = readFileSync(p, "utf8");
469
+ configs[match] = applyTruncation(match, content, config);
470
+ }
471
+ catch { /* skip */ }
472
+ }
473
+ }
474
+ }
475
+ catch { /* skip */ }
476
+ // Scan for *.csproj at root level
477
+ try {
478
+ const csprojFiles = await glob("*.csproj", { cwd, nodir: true });
479
+ for (const match of csprojFiles) {
480
+ if (configs[match])
481
+ continue;
482
+ const p = join(cwd, match);
483
+ try {
484
+ const content = readFileSync(p, "utf8");
485
+ configs[match] = applyTruncation(match, content, config);
486
+ }
487
+ catch { /* skip */ }
488
+ }
489
+ }
490
+ catch { /* skip */ }
456
491
  }
457
492
  // --- Source file discovery ---
458
493
  async function findSourceFiles(cwd, maxDepth, blocked) {
@@ -4,6 +4,7 @@ import { collectProjectFiles } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildAddCommand } from "../builder.js";
7
+ import { c } from "../output.js";
7
8
  function ensureDir(dir) {
8
9
  if (!existsSync(dir))
9
10
  mkdirSync(dir, { recursive: true });
@@ -18,6 +19,8 @@ async function promptFreeText(question) {
18
19
  });
19
20
  }
20
21
  // Conservative — only redirect when unambiguously single-file
22
+ // False negatives (multi-step for single-file request) are fine
23
+ // False positives (redirecting a genuinely multi-file request) are bad
21
24
  function isSingleFileOperation(input) {
22
25
  return (/to \.mcp\.json\s*$/i.test(input) ||
23
26
  /to settings\.json\s*$/i.test(input) ||
@@ -30,12 +33,12 @@ export async function runAdd() {
30
33
  return;
31
34
  }
32
35
  if (isSingleFileOperation(userInput)) {
33
- console.log(`
34
- For single changes, Claude Code is faster:
35
- Just tell it: "${userInput}"
36
-
37
- Use claude-setup add when the change spans multiple files —
38
- capabilities that need documentation, MCP servers, skills, and hooks together.
36
+ console.log(`
37
+ For single changes, Claude Code is faster:
38
+ Just tell it: "${userInput}"
39
+
40
+ Use ${c.cyan("claude-setup add")} when the change spans multiple files —
41
+ capabilities that need documentation, MCP servers, skills, and hooks together.
39
42
  `);
40
43
  return;
41
44
  }
@@ -46,5 +49,5 @@ capabilities that need documentation, MCP servers, skills, and hooks together.
46
49
  ensureDir(".claude/commands");
47
50
  writeFileSync(".claude/commands/stack-add.md", content, "utf8");
48
51
  await updateManifest("add", collected, { input: userInput });
49
- console.log(`\n✅ Ready. Open Claude Code and run:\n /stack-add\n`);
52
+ console.log(`\n${c.green("")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-add")}\n`);
50
53
  }
@@ -1 +1,5 @@
1
- export { runDoctor } from "../doctor.js";
1
+ import { runDoctor } from "../doctor.js";
2
+ export { runDoctor };
3
+ export declare function runDoctorCommand(opts?: {
4
+ verbose?: boolean;
5
+ }): Promise<void>;
@@ -1 +1,5 @@
1
- export { runDoctor } from "../doctor.js";
1
+ import { runDoctor } from "../doctor.js";
2
+ export { runDoctor };
3
+ export async function runDoctorCommand(opts = {}) {
4
+ return runDoctor(opts.verbose ?? false);
5
+ }
@@ -1 +1,3 @@
1
- export declare function runInit(): Promise<void>;
1
+ export declare function runInit(opts?: {
2
+ dryRun?: boolean;
3
+ }): Promise<void>;
@@ -4,39 +4,70 @@ import { collectProjectFiles, isEmptyProject } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, } from "../builder.js";
7
+ import { c } from "../output.js";
8
+ import { ensureConfig } from "../config.js";
7
9
  function ensureDir(dir) {
8
10
  if (!existsSync(dir))
9
11
  mkdirSync(dir, { recursive: true });
10
12
  }
11
- export async function runInit() {
13
+ export async function runInit(opts = {}) {
14
+ const dryRun = opts.dryRun ?? false;
15
+ // Auto-generate .claude-setup.json if it doesn't exist
16
+ // Developer can edit it anytime to tune token budgets, truncation rules, etc.
17
+ const configCreated = ensureConfig();
18
+ if (configCreated) {
19
+ console.log(`${c.dim("Created .claude-setup.json — edit to tune token budgets and truncation rules")}`);
20
+ }
12
21
  const state = await readState();
13
22
  const collected = await collectProjectFiles(process.cwd(), "deep");
14
- ensureDir(".claude/commands");
15
23
  if (isEmptyProject(collected)) {
16
24
  const content = buildEmptyProjectCommand();
25
+ if (dryRun) {
26
+ console.log(c.bold("[DRY RUN] Would write:\n"));
27
+ console.log(` .claude/commands/stack-init.md (${content.length} chars, ~${Math.ceil(content.length / 4)} tokens)`);
28
+ console.log(`\n${c.dim("--- preview ---")}`);
29
+ console.log(content.slice(0, 500));
30
+ if (content.length > 500)
31
+ console.log(c.dim(`\n... +${content.length - 500} chars`));
32
+ return;
33
+ }
34
+ ensureDir(".claude/commands");
17
35
  writeFileSync(".claude/commands/stack-init.md", content, "utf8");
18
36
  await updateManifest("init", collected);
19
37
  console.log(`
20
- ✅ New project detected.
38
+ ${c.green("")} New project detected.
21
39
 
22
40
  Open Claude Code and run:
23
- /stack-init
41
+ ${c.cyan("/stack-init")}
24
42
 
25
43
  Claude Code will ask 3 questions, then set up your environment.
26
44
  `);
27
45
  return;
28
46
  }
29
- // Standard init — 6 atomic steps + orchestrator
47
+ // Standard init — atomic steps + orchestrator
30
48
  const steps = buildAtomicSteps(collected, state);
49
+ const orchestrator = buildOrchestratorCommand(steps);
50
+ if (dryRun) {
51
+ console.log(c.bold("[DRY RUN] Would write:\n"));
52
+ for (const step of steps) {
53
+ const tokens = Math.ceil(step.content.length / 4);
54
+ console.log(` .claude/commands/${step.filename} (${step.content.length} chars, ~${tokens} tokens)`);
55
+ }
56
+ console.log(` .claude/commands/stack-init.md (orchestrator)`);
57
+ const totalTokens = steps.reduce((sum, s) => sum + Math.ceil(s.content.length / 4), 0);
58
+ console.log(`\n${c.dim(`Total: ~${totalTokens} tokens across ${steps.length} files`)}`);
59
+ return;
60
+ }
61
+ ensureDir(".claude/commands");
31
62
  for (const step of steps) {
32
63
  writeFileSync(join(".claude/commands", step.filename), step.content, "utf8");
33
64
  }
34
- writeFileSync(".claude/commands/stack-init.md", buildOrchestratorCommand(steps), "utf8");
65
+ writeFileSync(".claude/commands/stack-init.md", orchestrator, "utf8");
35
66
  await updateManifest("init", collected);
36
67
  console.log(`
37
- ✅ Ready. Open Claude Code and run:
38
- /stack-init
68
+ ${c.green("")} Ready. Open Claude Code and run:
69
+ ${c.cyan("/stack-init")}
39
70
 
40
- Runs 6 atomic steps. If one fails, re-run only that step.
71
+ Runs ${steps.length - 1} atomic steps. If one fails, re-run only that step.
41
72
  `);
42
73
  }
@@ -4,6 +4,7 @@ import { collectProjectFiles } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildRemoveCommand } from "../builder.js";
7
+ import { c } from "../output.js";
7
8
  function ensureDir(dir) {
8
9
  if (!existsSync(dir))
9
10
  mkdirSync(dir, { recursive: true });
@@ -29,5 +30,5 @@ export async function runRemove() {
29
30
  ensureDir(".claude/commands");
30
31
  writeFileSync(".claude/commands/stack-remove.md", content, "utf8");
31
32
  await updateManifest("remove", collected, { input: userInput });
32
- console.log(`\n✅ Ready. Open Claude Code and run:\n /stack-remove\n`);
33
+ console.log(`\n${c.green("")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-remove")}\n`);
33
34
  }