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 +87 -19
- package/dist/builder.js +212 -124
- package/dist/collect.d.ts +10 -1
- package/dist/collect.js +482 -178
- package/dist/commands/add.js +7 -3
- package/dist/commands/doctor.d.ts +5 -1
- package/dist/commands/doctor.js +5 -1
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.js +41 -10
- package/dist/commands/remove.js +3 -2
- package/dist/commands/status.js +123 -11
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +36 -8
- package/dist/config.d.ts +29 -0
- package/dist/config.js +128 -0
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +259 -23
- package/dist/index.js +31 -9
- package/dist/os.d.ts +20 -0
- package/dist/os.js +38 -0
- package/dist/output.d.ts +18 -0
- package/dist/output.js +40 -0
- package/package.json +1 -1
- package/templates/add.md +15 -42
- package/templates/init.md +34 -53
- package/templates/remove.md +13 -30
- package/templates/sync.md +19 -35
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
result = result.replace(
|
|
28
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
67
|
+
const skippedList = collected.skipped.length > 0
|
|
68
|
+
? collected.skipped.map(s => `- ${s.path} — ${s.reason}`).join("\n")
|
|
69
|
+
: "";
|
|
66
70
|
return {
|
|
67
|
-
VERSION:
|
|
68
|
-
DATE:
|
|
69
|
-
|
|
70
|
-
|
|
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(
|
|
92
|
+
function buildFlags(_collected, state) {
|
|
88
93
|
return {
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
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
|
|
113
|
-
content = content.replace(
|
|
114
|
-
if (estimateTokens(content) <=
|
|
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 >
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
+
// Compact diff format — paths + one-line summary, not full content
|
|
146
147
|
const addedStr = diff.added.length > 0
|
|
147
|
-
? diff.added.map(f =>
|
|
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 =>
|
|
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
|
|
174
|
-
const
|
|
175
|
-
|
|
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:
|
|
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
|
|
184
|
-
: `
|
|
185
|
-
|
|
186
|
-
|
|
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`,
|
|
187
207
|
},
|
|
208
|
+
// --- Step 2: .mcp.json ---
|
|
188
209
|
{
|
|
189
210
|
filename: "stack-2-mcp.md",
|
|
190
|
-
content:
|
|
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
|
|
195
|
-
:
|
|
196
|
-
|
|
197
|
-
|
|
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`,
|
|
198
234
|
},
|
|
235
|
+
// --- Step 3: .claude/settings.json ---
|
|
199
236
|
{
|
|
200
237
|
filename: "stack-3-settings.md",
|
|
201
|
-
content:
|
|
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
|
|
206
|
-
:
|
|
207
|
-
|
|
208
|
-
|
|
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`,
|
|
209
259
|
},
|
|
260
|
+
// --- Step 4: .claude/skills/ ---
|
|
210
261
|
{
|
|
211
262
|
filename: "stack-4-skills.md",
|
|
212
|
-
content:
|
|
213
|
-
`##
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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:
|
|
224
|
-
`##
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
`
|
|
228
|
-
|
|
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:
|
|
233
|
-
`## Target: .github/workflows/\n
|
|
303
|
+
content: header +
|
|
304
|
+
`## Target: .github/workflows/\n` +
|
|
234
305
|
`.github/ exists: ${vars.HAS_GITHUB_DIR}\n` +
|
|
235
|
-
`
|
|
306
|
+
`Installed: ${vars.WORKFLOWS_LIST}\n\n` +
|
|
236
307
|
(state.hasGithubDir
|
|
237
|
-
?
|
|
238
|
-
|
|
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
|
-
|
|
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 `<!--
|
|
250
|
-
<!-- Run /stack-init in Claude Code -->
|
|
339
|
+
return `<!-- claude-setup ${version} ${date} -->
|
|
251
340
|
|
|
252
|
-
Run these in order. If one fails, fix
|
|
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
|
|
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
|
|
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;
|