claude-setup 1.0.0 → 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
@@ -2,31 +2,41 @@
2
2
 
3
3
  Setup layer for Claude Code. Reads your project, writes command files, Claude Code does the rest.
4
4
 
5
- ## Install
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
6
8
 
7
9
  ```bash
8
10
  npx claude-setup init
9
11
  ```
10
12
 
13
+ Then open Claude Code and run `/stack-init`.
14
+
11
15
  ## Commands
12
16
 
13
17
  | Command | What it does |
14
18
  |---------|-------------|
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 |
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
+ ```
21
33
 
22
34
  ## How it works
23
35
 
24
- 1. **CLI collects** — reads project files (configs, source samples) with strict cost controls
36
+ 1. **CLI collects** — reads project files (configs, source samples) with strict token cost controls
25
37
  2. **CLI writes command files** — assembles markdown instructions into `.claude/commands/`
26
38
  3. **Claude Code executes** — you run `/stack-init` (or `/stack-sync`, etc.) in Claude Code
27
39
 
28
- The CLI has zero intelligence. All reasoning is delegated to Claude Code via the command files.
29
-
30
40
  ## Three project states
31
41
 
32
42
  - **Empty project** — Claude Code asks 3 discovery questions, then sets up a tailored environment
@@ -36,16 +46,74 @@ The CLI has zero intelligence. All reasoning is delegated to Claude Code via the
36
46
  ## What it creates
37
47
 
38
48
  - `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)
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)
42
52
  - `.claude/commands/` — project-specific slash commands
43
53
  - `.github/workflows/` — CI workflows (only if `.github/` exists)
44
54
 
45
- ## Cost controls
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>"] }`
46
118
 
47
- Every byte in a command file costs tokens. The CLI enforces:
48
- - Source file sampling (max 10 files, smallest first)
49
- - Hard truncation per file (150/400 line thresholds)
50
- - Token budget cap (20,000 tokens max per command file)
51
- - Universal blocklist (node_modules, dist, binaries, etc.)
119
+ `doctor` checks for mismatches and reports them as critical issues.
package/dist/builder.js CHANGED
@@ -1,17 +1,16 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { join, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
+ import { loadConfig } from "./config.js";
5
+ import { detectOS } from "./os.js";
4
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
7
  const TEMPLATES_DIR = join(__dirname, "..", "templates");
6
- const TOKEN_SOFT_WARN = 8_000;
7
- const TOKEN_HARD_CAP = 20_000;
8
8
  function estimateTokens(content) {
9
9
  return Math.ceil(content.length / 4);
10
10
  }
11
11
  function loadTemplate(name) {
12
12
  return readFileSync(join(TEMPLATES_DIR, name), "utf8");
13
13
  }
14
- // Simple {{VARIABLE}} replacement
15
14
  function replaceVars(template, vars) {
16
15
  let result = template;
17
16
  for (const [key, value] of Object.entries(vars)) {
@@ -19,56 +18,60 @@ function replaceVars(template, vars) {
19
18
  }
20
19
  return result;
21
20
  }
22
- // Conditional blocks: {{#if VAR}}...{{else}}...{{/if}} and {{#if VAR}}...{{/if}}
23
21
  function processConditionals(template, flags) {
24
- // Handle {{#if VAR}}...{{else}}...{{/if}}
25
22
  let result = template;
26
- const ifElseRegex = /\{\{#if\s+(\w+)\}\}\n?([\s\S]*?)\{\{else\}\}\n?([\s\S]*?)\{\{\/if\}\}/g;
27
- result = result.replace(ifElseRegex, (_match, key, ifBlock, elseBlock) => {
28
- return flags[key] ? ifBlock : elseBlock;
29
- });
30
- // Handle {{#if VAR}}...{{/if}} (no else)
31
- const ifRegex = /\{\{#if\s+(\w+)\}\}\n?([\s\S]*?)\{\{\/if\}\}/g;
32
- result = result.replace(ifRegex, (_match, key, block) => {
33
- return flags[key] ? block : "";
34
- });
23
+ // {{#if VAR}}...{{else}}...{{/if}}
24
+ result = result.replace(/\{\{#if\s+(\w+)\}\}\n?([\s\S]*?)\{\{else\}\}\n?([\s\S]*?)\{\{\/if\}\}/g, (_m, key, ifBlock, elseBlock) => flags[key] ? ifBlock : elseBlock);
25
+ // {{#if VAR}}...{{/if}}
26
+ result = result.replace(/\{\{#if\s+(\w+)\}\}\n?([\s\S]*?)\{\{\/if\}\}/g, (_m, key, block) => flags[key] ? block : "");
35
27
  return result;
36
28
  }
37
- function formatConfigFiles(configs) {
38
- if (Object.keys(configs).length === 0)
39
- return "(no config files found)";
40
- return Object.entries(configs)
41
- .map(([path, content]) => {
42
- return `#### ${path}\n\`\`\`\n${content}\n\`\`\``;
43
- })
44
- .join("\n\n");
29
+ function formatList(items) {
30
+ return items.length === 0 ? "none" : items.join(", ");
31
+ }
32
+ function getVersion() {
33
+ try {
34
+ return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8")).version ?? "0.0.0";
35
+ }
36
+ catch {
37
+ return "0.0.0";
38
+ }
45
39
  }
46
- function formatSourceFiles(source) {
40
+ // --- Project context formatter ---
41
+ // This is the key optimization: instead of dumping raw files, we format
42
+ // the digest compactly. Full file content only when truly needed.
43
+ function formatProjectContext(collected) {
44
+ const lines = [];
45
+ // Digest (compact signal extraction)
46
+ const digest = collected.configs["__digest__"];
47
+ if (digest) {
48
+ lines.push(digest);
49
+ }
50
+ // Additional config files that kept full content (docker-compose, etc.)
51
+ for (const [name, content] of Object.entries(collected.configs)) {
52
+ if (name === "__digest__" || name === ".env")
53
+ continue;
54
+ lines.push(`\n### ${name}\n\`\`\`\n${content}\n\`\`\``);
55
+ }
56
+ return lines.join("\n") || "(no config files found)";
57
+ }
58
+ function formatSourceContext(source) {
47
59
  if (source.length === 0)
48
- return "(no source files sampled)";
60
+ return "";
49
61
  return source
50
- .map(({ path, content }) => {
51
- return `#### ${path}\n\`\`\`\n${content}\n\`\`\``;
52
- })
62
+ .map(({ path, content }) => `### ${path}\n\`\`\`\n${content}\n\`\`\``)
53
63
  .join("\n\n");
54
64
  }
55
- function formatSkippedFiles(skipped) {
56
- return skipped.map(({ path, reason }) => `- ${path} — ${reason}`).join("\n");
57
- }
58
- function formatList(items) {
59
- if (items.length === 0)
60
- return "none";
61
- return items.join(", ");
62
- }
65
+ // --- Template variable building ---
63
66
  function buildVars(collected, state) {
64
- const version = getVersion();
65
- const date = new Date().toISOString().split("T")[0];
67
+ const skippedList = collected.skipped.length > 0
68
+ ? collected.skipped.map(s => `- ${s.path} — ${s.reason}`).join("\n")
69
+ : "";
66
70
  return {
67
- VERSION: version,
68
- DATE: date,
69
- CONFIG_FILES: formatConfigFiles(collected.configs),
70
- SOURCE_FILES: formatSourceFiles(collected.source),
71
- SKIPPED_LIST: formatSkippedFiles(collected.skipped),
71
+ VERSION: getVersion(),
72
+ DATE: new Date().toISOString().split("T")[0],
73
+ PROJECT_CONTEXT: formatProjectContext(collected),
74
+ SOURCE_CONTEXT: formatSourceContext(collected.source),
72
75
  CLAUDE_MD_CONTENT: state.claudeMd.content
73
76
  ? `\`\`\`\n${state.claudeMd.content}\n\`\`\``
74
77
  : "",
@@ -82,160 +85,245 @@ function buildVars(collected, state) {
82
85
  COMMANDS_LIST: formatList(state.commands),
83
86
  WORKFLOWS_LIST: formatList(state.workflows),
84
87
  HAS_GITHUB_DIR: state.hasGithubDir ? "yes" : "no",
88
+ SKIPPED_LIST: skippedList,
89
+ DETECTED_OS: detectOS(),
85
90
  };
86
91
  }
87
- function buildFlags(collected, state) {
92
+ function buildFlags(_collected, state) {
88
93
  return {
89
- HAS_SKIPPED: collected.skipped.length > 0,
94
+ HAS_SOURCE: _collected.source.length > 0,
95
+ HAS_SKIPPED: _collected.skipped.length > 0,
90
96
  HAS_CLAUDE_MD: state.claudeMd.exists,
91
97
  HAS_MCP_JSON: state.mcpJson.exists,
92
98
  HAS_SETTINGS: state.settings.exists,
93
99
  HAS_GITHUB_DIR: state.hasGithubDir,
94
100
  };
95
101
  }
96
- function getVersion() {
97
- try {
98
- const pkgPath = join(__dirname, "..", "package.json");
99
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
100
- return pkg.version ?? "0.0.0";
101
- }
102
- catch {
103
- return "0.0.0";
104
- }
105
- }
106
- function fitToTokenBudget(content, sources) {
107
- if (estimateTokens(content) <= TOKEN_HARD_CAP)
102
+ // --- Token budget enforcement ---
103
+ function fitToTokenBudget(content, sources, hardCap) {
104
+ if (estimateTokens(content) <= hardCap)
108
105
  return content;
109
- // Progressively remove source files, largest first
106
+ // Remove source files largest-first until under budget
110
107
  const sorted = [...sources].sort((a, b) => b.content.length - a.content.length);
111
108
  for (const remove of sorted) {
112
- const sourceBlock = `#### ${remove.path}\n\`\`\`\n${remove.content}\n\`\`\``;
113
- content = content.replace(sourceBlock, `[${remove.path} — removed to fit token budget]`);
114
- if (estimateTokens(content) <= TOKEN_HARD_CAP)
109
+ const block = `### ${remove.path}\n\`\`\`\n${remove.content}\n\`\`\``;
110
+ content = content.replace(block, `[${remove.path} — trimmed]`);
111
+ if (estimateTokens(content) <= hardCap)
115
112
  break;
116
113
  }
117
114
  return content;
118
115
  }
119
- function applyTemplate(templateName, collected, state, extraVars = {}) {
116
+ function applyTemplate(templateName, collected, state, extraVars = {}, budgetKey = "init") {
117
+ const config = loadConfig();
118
+ const budget = config.tokenBudget[budgetKey];
120
119
  const template = loadTemplate(templateName);
121
120
  const vars = { ...buildVars(collected, state), ...extraVars };
122
121
  const flags = buildFlags(collected, state);
123
122
  let content = replaceVars(template, vars);
124
123
  content = processConditionals(content, flags);
125
124
  const tokens = estimateTokens(content);
126
- if (tokens > TOKEN_SOFT_WARN) {
127
- console.warn(`⚠️ Command file is ${tokens} tokens (soft limit: ${TOKEN_SOFT_WARN})`);
125
+ if (tokens > budget) {
126
+ content = fitToTokenBudget(content, collected.source, budget);
127
+ const finalTokens = estimateTokens(content);
128
+ if (finalTokens > budget) {
129
+ console.warn(`⚠️ ${templateName}: ${finalTokens} tokens (budget: ${budget})`);
130
+ }
128
131
  }
129
- content = fitToTokenBudget(content, collected.source);
130
132
  return content;
131
133
  }
134
+ // --- Public API ---
132
135
  export function buildInitCommand(collected, state) {
133
- return applyTemplate("init.md", collected, state);
136
+ return applyTemplate("init.md", collected, state, {}, "init");
134
137
  }
135
138
  export function buildEmptyProjectCommand() {
136
139
  const template = loadTemplate("init-empty.md");
137
- const version = getVersion();
138
- const date = new Date().toISOString().split("T")[0];
139
- return replaceVars(template, { VERSION: version, DATE: date });
140
+ return replaceVars(template, { VERSION: getVersion(), DATE: new Date().toISOString().split("T")[0] });
140
141
  }
141
142
  export function buildAddCommand(input, collected, state) {
142
- return applyTemplate("add.md", collected, state, { USER_INPUT: input });
143
+ return applyTemplate("add.md", collected, state, { USER_INPUT: input }, "add");
143
144
  }
144
145
  export function buildSyncCommand(diff, collected, state) {
145
- const lastRun = state.manifest?.runs.at(-1);
146
+ // Compact diff format — paths + one-line summary, not full content
146
147
  const addedStr = diff.added.length > 0
147
- ? diff.added.map(f => `#### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n")
148
+ ? diff.added.map(f => `- **${f.path}** (new) — ${f.content.split("\n").length} lines`).join("\n")
148
149
  : "(none)";
149
150
  const modifiedStr = diff.changed.length > 0
150
- ? diff.changed.map(f => `#### ${f.path}\n\`\`\`\n${f.current}\n\`\`\``).join("\n\n")
151
+ ? diff.changed.map(f => `- **${f.path}** (modified)`).join("\n")
151
152
  : "(none)";
152
153
  const deletedStr = diff.deleted.length > 0
153
154
  ? diff.deleted.map(f => `- ${f}`).join("\n")
154
155
  : "(none)";
156
+ const lastRun = state.manifest?.runs.at(-1);
155
157
  return applyTemplate("sync.md", collected, state, {
156
158
  LAST_RUN_DATE: lastRun?.at ?? "unknown",
157
159
  ADDED_FILES: addedStr,
158
160
  MODIFIED_FILES: modifiedStr,
159
161
  DELETED_FILES: deletedStr,
160
- });
162
+ }, "sync");
161
163
  }
162
164
  export function buildRemoveCommand(input, state) {
163
- // Remove uses a minimal collected set — no project files needed
164
165
  const emptyCollected = { configs: {}, source: [], skipped: [] };
165
- return applyTemplate("remove.md", emptyCollected, state, { USER_INPUT: input });
166
+ return applyTemplate("remove.md", emptyCollected, state, { USER_INPUT: input }, "remove");
166
167
  }
167
168
  export function buildAtomicSteps(collected, state) {
168
- const fullContent = buildInitCommand(collected, state);
169
- const vars = buildVars(collected, state);
170
- const flags = buildFlags(collected, state);
171
169
  const version = getVersion();
172
170
  const date = new Date().toISOString().split("T")[0];
173
- const preamble = `<!-- Generated by claude-setup ${version} on ${date} — DO NOT hand-edit -->\n`;
174
- const idempotentCheck = `\nBefore writing: check if what you are about to write already exists in the target file\n(current content provided below). If yes: print "SKIPPED — already up to date" and stop.\nWrite only what is genuinely missing.\n\n`;
175
- // Each step gets project context + specific instructions for its target
171
+ const vars = buildVars(collected, state);
172
+ const os = detectOS();
173
+ const header = `<!-- claude-setup ${version} ${date} -->\n`;
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
176
+ const sharedContext = header +
177
+ `## Project\n\n${vars.PROJECT_CONTEXT}\n\n` +
178
+ `{{#if HAS_SOURCE}}## Source samples\n\n${vars.SOURCE_CONTEXT}\n{{/if}}`;
179
+ const sharedContextProcessed = processConditionals(sharedContext, buildFlags(collected, state));
176
180
  const steps = [
181
+ {
182
+ filename: "stack-0-context.md",
183
+ content: sharedContextProcessed,
184
+ },
185
+ // --- Step 1: CLAUDE.md ---
177
186
  {
178
187
  filename: "stack-1-claude-md.md",
179
- content: preamble + idempotentCheck +
180
- `## Project context\n\n${vars.CONFIG_FILES}\n\n${vars.SOURCE_FILES}\n\n` +
188
+ content: header + preamble +
181
189
  `## Target: CLAUDE.md\n\n` +
182
190
  (state.claudeMd.exists
183
- ? `### Current CLAUDE.mdEXISTS — append only, never rewrite, never remove\n${vars.CLAUDE_MD_CONTENT}\n\n`
184
- : `CLAUDE.md does not exist. Create it.\n\n`) +
185
- `Write or update CLAUDE.md for THIS specific project.\nMake it specific: reference actual file paths, actual patterns, actual conventions from the source above.\nNo generic boilerplate. Every line must trace back to something in the project files.\n` +
186
- (state.claudeMd.exists ? `\nAppend only never rewrite or remove existing content.` : ""),
191
+ ? `### Current contentAPPEND 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`,
187
207
  },
208
+ // --- Step 2: .mcp.json ---
188
209
  {
189
210
  filename: "stack-2-mcp.md",
190
- content: preamble + idempotentCheck +
191
- `## Project context\n\n${vars.CONFIG_FILES}\n\n` +
211
+ content: header + preamble +
192
212
  `## Target: .mcp.json\n\n` +
193
213
  (state.mcpJson.exists
194
- ? `### Current .mcp.jsonEXISTS — merge only, never remove existing entries\n${vars.MCP_JSON_CONTENT}\n\n`
195
- : `.mcp.json does not exist. Create only if you find evidence of external services in the config files above.\n\n`) +
196
- `Only add MCP servers for services evidenced in the project files. No evidence = no server.\n` +
197
- (state.mcpJson.exists ? `Merge only never remove existing entries. Produce valid JSON.` : ""),
214
+ ? `### Current contentMERGE 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`,
198
234
  },
235
+ // --- Step 3: .claude/settings.json ---
199
236
  {
200
237
  filename: "stack-3-settings.md",
201
- content: preamble + idempotentCheck +
202
- `## Project context\n\n${vars.CONFIG_FILES}\n\n` +
238
+ content: header + preamble +
203
239
  `## Target: .claude/settings.json\n\n` +
204
240
  (state.settings.exists
205
- ? `### Current settings.jsonEXISTS — merge only, never remove existing hooks\n${vars.SETTINGS_CONTENT}\n\n`
206
- : `.claude/settings.json does not exist. Create only if hooks are genuinely warranted for this project.\n\n`) +
207
- `Every hook adds overhead on every Claude Code action. Only add if clearly earned for THIS project.\n` +
208
- (state.settings.exists ? `Merge only never remove existing hooks. Never modify existing values.` : ""),
241
+ ? `### Current contentMERGE 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`,
209
259
  },
260
+ // --- Step 4: .claude/skills/ ---
210
261
  {
211
262
  filename: "stack-4-skills.md",
212
- content: preamble + idempotentCheck +
213
- `## Project context\n\n${vars.CONFIG_FILES}\n\n${vars.SOURCE_FILES}\n\n` +
214
- `## Target: .claude/skills/\n\n` +
215
- `Skills installed: ${vars.SKILLS_LIST}\n\n` +
216
- `Only create skills for patterns that recur across this codebase and benefit from automatic loading.\n` +
217
- `Use applies-when frontmatter so skills load only when relevant.\n` +
218
- `If a similar skill already exists: extend it. Do not create a parallel one.\n` +
219
- `Empty is fine not every project needs skills.`,
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`,
220
278
  },
279
+ // --- Step 5: .claude/commands/ ---
221
280
  {
222
281
  filename: "stack-5-commands.md",
223
- content: preamble + idempotentCheck +
224
- `## Project context\n\n${vars.CONFIG_FILES}\n\n` +
225
- `## Target: .claude/commands/ (not stack-*.md files)\n\n` +
226
- `Commands installed: ${vars.COMMANDS_LIST}\n\n` +
227
- `Only create commands that will actually be useful for this kind of project.\n` +
228
- `Do not duplicate existing commands. Do not create stack-*.md files.`,
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`,
229
299
  },
300
+ // --- Step 6: .github/workflows/ ---
230
301
  {
231
302
  filename: "stack-6-workflows.md",
232
- content: preamble + idempotentCheck +
233
- `## Target: .github/workflows/\n\n` +
303
+ content: header +
304
+ `## Target: .github/workflows/\n` +
234
305
  `.github/ exists: ${vars.HAS_GITHUB_DIR}\n` +
235
- `Workflows installed: ${vars.WORKFLOWS_LIST}\n\n` +
306
+ `Installed: ${vars.WORKFLOWS_LIST}\n\n` +
236
307
  (state.hasGithubDir
237
- ? `Only create workflows warranted by the project. If workflows already exist: do not touch them.`
238
- : `.github/ does not exist. Only create workflows if the project clearly warrants them.`),
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`)),
239
327
  },
240
328
  ];
241
329
  return steps;
@@ -243,17 +331,17 @@ export function buildAtomicSteps(collected, state) {
243
331
  export function buildOrchestratorCommand(steps) {
244
332
  const version = getVersion();
245
333
  const date = new Date().toISOString().split("T")[0];
246
- const stepList = steps
334
+ // Skip step 0 (context) in the run list — it's referenced by other steps
335
+ const runSteps = steps.filter(s => s.filename !== "stack-0-context.md");
336
+ const stepList = runSteps
247
337
  .map((s, i) => `${i + 1}. /${s.filename.replace(".md", "")}`)
248
338
  .join("\n");
249
- return `<!-- Generated by claude-setup ${version} on ${date} — DO NOT hand-edit -->
250
- <!-- Run /stack-init in Claude Code -->
339
+ return `<!-- claude-setup ${version} ${date} -->
251
340
 
252
- Run these in order. If one fails, fix it and continue from that step only.
253
- Do not re-run steps that already completed.
341
+ Run these in order. If one fails, fix and continue from that step.
254
342
 
255
343
  ${stepList}
256
344
 
257
- After all steps complete: one-line summary of what was created.
345
+ After all complete: one-line summary of what was created.
258
346
  `;
259
347
  }
package/dist/collect.d.ts CHANGED
@@ -9,5 +9,14 @@ export interface CollectedFiles {
9
9
  reason: string;
10
10
  }>;
11
11
  }
12
- export declare function collectProjectFiles(cwd?: string): Promise<CollectedFiles>;
12
+ export interface ProjectDigest {
13
+ configFilesFound: string[];
14
+ deps: string[];
15
+ scripts: string[];
16
+ tree: string;
17
+ envVars: string[];
18
+ configs: Record<string, string>;
19
+ }
20
+ export type CollectMode = "deep" | "normal" | "configOnly";
21
+ export declare function collectProjectFiles(cwd?: string, mode?: CollectMode): Promise<CollectedFiles>;
13
22
  export declare function isEmptyProject(collected: CollectedFiles): boolean;