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 +119 -45
- package/dist/builder.js +131 -32
- package/dist/collect.js +40 -5
- package/dist/commands/add.js +10 -7
- 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 +40 -9
- package/dist/commands/remove.js +2 -1
- package/dist/commands/status.js +123 -11
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +35 -7
- package/dist/config.d.ts +14 -0
- package/dist/config.js +89 -3
- 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 +8 -4
- package/templates/init.md +81 -17
- package/templates/remove.md +10 -5
- package/templates/sync.md +28 -13
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
|
+
import { join } from "path";
|
|
3
4
|
import { readManifest } from "./manifest.js";
|
|
4
5
|
import { readState } from "./state.js";
|
|
6
|
+
import { detectOS } from "./os.js";
|
|
7
|
+
import { c, statusLine, section } from "./output.js";
|
|
5
8
|
function tryExec(cmd) {
|
|
6
9
|
try {
|
|
7
10
|
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -20,53 +23,286 @@ function readIfExists(filePath) {
|
|
|
20
23
|
return null;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
26
|
+
function safeJsonParse(content) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
25
33
|
}
|
|
26
|
-
export async function runDoctor() {
|
|
34
|
+
export async function runDoctor(verbose = false) {
|
|
35
|
+
const os = detectOS();
|
|
27
36
|
const manifest = await readManifest();
|
|
28
37
|
const state = await readState();
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
const counts = { critical: 0, warnings: 0, healthy: 0 };
|
|
39
|
+
console.log(`${c.bold("claude-setup doctor")} — ${new Date().toISOString().split("T")[0]} | OS: ${os}\n`);
|
|
40
|
+
// --- Check 1: Claude Code version ---
|
|
41
|
+
section("Environment");
|
|
31
42
|
const cv = tryExec("claude --version");
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
if (cv) {
|
|
44
|
+
statusLine("✅", "Claude Code", cv.trim());
|
|
45
|
+
counts.healthy++;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
statusLine("🔴", "Claude Code", c.red("not found in PATH"));
|
|
49
|
+
counts.critical++;
|
|
50
|
+
}
|
|
51
|
+
// --- Check 2: Manifest integrity ---
|
|
34
52
|
const lastRun = manifest?.runs.at(-1);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
if (manifest) {
|
|
54
|
+
const staleCheck = lastRun ? checkStaleness(lastRun.at) : null;
|
|
55
|
+
const detail = lastRun
|
|
56
|
+
? `last: ${lastRun.command} at ${lastRun.at}${staleCheck ? ` ${staleCheck}` : ""}`
|
|
57
|
+
: "empty runs array";
|
|
58
|
+
statusLine("✅", "Manifest", detail);
|
|
59
|
+
counts.healthy++;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
statusLine("⚠️ ", "Manifest", c.yellow("not found — run: npx claude-setup init"));
|
|
63
|
+
counts.warnings++;
|
|
64
|
+
}
|
|
65
|
+
// --- Check 3: OS/MCP format mismatch ---
|
|
66
|
+
if (state.mcpJson.content) {
|
|
67
|
+
section("MCP servers");
|
|
68
|
+
const mcp = safeJsonParse(state.mcpJson.content);
|
|
69
|
+
if (mcp && typeof mcp.mcpServers === "object" && mcp.mcpServers !== null) {
|
|
70
|
+
const servers = mcp.mcpServers;
|
|
71
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
72
|
+
const cmd = config.command;
|
|
73
|
+
if (!cmd) {
|
|
74
|
+
statusLine("⚠️ ", name, c.yellow("no command field"));
|
|
75
|
+
counts.warnings++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (os === "Windows" && cmd === "npx") {
|
|
79
|
+
statusLine("🔴", name, c.red(`BROKEN: Windows can't execute npx directly — use cmd /c npx`));
|
|
80
|
+
counts.critical++;
|
|
81
|
+
}
|
|
82
|
+
else if (os !== "Windows" && cmd === "cmd") {
|
|
83
|
+
statusLine("⚠️ ", name, c.yellow(`UNNECESSARY: cmd wrapper not needed on ${os}`));
|
|
84
|
+
counts.warnings++;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
statusLine("✅", name, `valid, OS-format correct (${cmd})`);
|
|
88
|
+
counts.healthy++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (mcp) {
|
|
93
|
+
// Check for flat structure (some .mcp.json files use flat keys)
|
|
94
|
+
if (verbose)
|
|
95
|
+
statusLine("⚠️ ", ".mcp.json", "no mcpServers key found");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (verbose) {
|
|
99
|
+
section("MCP servers");
|
|
100
|
+
statusLine("⏭ ", ".mcp.json", "does not exist");
|
|
101
|
+
}
|
|
102
|
+
// --- Check 4: Hook quoting bugs ---
|
|
103
|
+
if (state.settings.content) {
|
|
104
|
+
section("Hooks");
|
|
105
|
+
const settings = safeJsonParse(state.settings.content);
|
|
106
|
+
if (settings) {
|
|
107
|
+
const hookCategories = [
|
|
108
|
+
"PreToolUse", "PostToolUse", "PreCompact", "PostCompact",
|
|
109
|
+
"Notification", "Stop", "SubagentStop"
|
|
110
|
+
];
|
|
111
|
+
let foundHooks = false;
|
|
112
|
+
for (const category of hookCategories) {
|
|
113
|
+
const hooks = settings[category];
|
|
114
|
+
if (!hooks || !Array.isArray(hooks))
|
|
115
|
+
continue;
|
|
116
|
+
for (const hook of hooks) {
|
|
117
|
+
if (typeof hook !== "object" || !hook)
|
|
118
|
+
continue;
|
|
119
|
+
const h = hook;
|
|
120
|
+
const hookCmd = h.command;
|
|
121
|
+
const args = h.args;
|
|
122
|
+
if (!hookCmd || !args)
|
|
123
|
+
continue;
|
|
124
|
+
foundHooks = true;
|
|
125
|
+
// Check for bash -c quoting bugs
|
|
126
|
+
if (hookCmd === "bash" && args[0] === "-c" && args[1]) {
|
|
127
|
+
const shellStr = args[1];
|
|
128
|
+
const quotingIssue = checkBashQuoting(shellStr);
|
|
129
|
+
if (quotingIssue) {
|
|
130
|
+
statusLine("🔴", `${category} hook`, c.red(`quoting bug: ${quotingIssue}`));
|
|
131
|
+
counts.critical++;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
statusLine("✅", `${category} hook`, "quoting clean");
|
|
135
|
+
counts.healthy++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
statusLine("✅", `${category} hook`, "valid");
|
|
140
|
+
counts.healthy++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!foundHooks && verbose) {
|
|
145
|
+
statusLine("⏭ ", "Hooks", "none configured");
|
|
146
|
+
}
|
|
41
147
|
}
|
|
42
148
|
}
|
|
43
|
-
|
|
149
|
+
else if (verbose) {
|
|
150
|
+
section("Hooks");
|
|
151
|
+
statusLine("⏭ ", "settings.json", "does not exist");
|
|
152
|
+
}
|
|
153
|
+
// --- Check 5: Env var coverage ---
|
|
44
154
|
if (state.mcpJson.content) {
|
|
45
155
|
const refs = [...state.mcpJson.content.matchAll(/\$\{?([A-Z_][A-Z0-9_]+)\}?/g)]
|
|
46
156
|
.map(m => m[1]);
|
|
47
157
|
const unique = [...new Set(refs)];
|
|
48
158
|
if (unique.length) {
|
|
49
|
-
const template = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? "";
|
|
50
|
-
|
|
159
|
+
const template = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? readIfExists(".env.template") ?? "";
|
|
160
|
+
section("Env vars");
|
|
51
161
|
for (const v of unique) {
|
|
52
|
-
|
|
162
|
+
if (template.includes(v)) {
|
|
163
|
+
statusLine("✅", `\${${v}}`, "found in env template");
|
|
164
|
+
counts.healthy++;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
statusLine("⚠️ ", `\${${v}}`, c.yellow("used in .mcp.json but missing from .env.example"));
|
|
168
|
+
counts.warnings++;
|
|
169
|
+
}
|
|
53
170
|
}
|
|
54
171
|
}
|
|
55
172
|
}
|
|
56
|
-
// Workflow
|
|
173
|
+
// --- Check 6: Workflow secret coverage ---
|
|
57
174
|
if (state.workflows.length) {
|
|
58
175
|
const secrets = new Set();
|
|
59
176
|
for (const wf of state.workflows) {
|
|
60
177
|
const content = readIfExists(wf) ?? "";
|
|
61
|
-
for (const m of content.matchAll(/\$\{\{\s*secrets\.([A-Z_]
|
|
178
|
+
for (const m of content.matchAll(/\$\{\{\s*secrets\.([A-Z_][A-Z0-9_]*)\s*\}\}/g)) {
|
|
62
179
|
secrets.add(m[1]);
|
|
63
180
|
}
|
|
64
181
|
}
|
|
65
182
|
if (secrets.size) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
183
|
+
section("Workflow secrets");
|
|
184
|
+
const readme = readIfExists("README.md") ?? "";
|
|
185
|
+
const envTemplate = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? "";
|
|
186
|
+
for (const s of secrets) {
|
|
187
|
+
if (readme.includes(s) || envTemplate.includes(s)) {
|
|
188
|
+
statusLine("✅", s, "documented");
|
|
189
|
+
counts.healthy++;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
statusLine("⚠️ ", s, c.yellow("needs GitHub Settings → Secrets"));
|
|
193
|
+
counts.warnings++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
69
196
|
}
|
|
70
197
|
}
|
|
71
|
-
|
|
198
|
+
// --- Check 7: Stale skill paths ---
|
|
199
|
+
if (state.skills.length) {
|
|
200
|
+
section("Skills");
|
|
201
|
+
for (const skillPath of state.skills) {
|
|
202
|
+
const content = readIfExists(skillPath);
|
|
203
|
+
if (!content) {
|
|
204
|
+
statusLine("⚠️ ", skillPath, c.yellow("skill file not readable"));
|
|
205
|
+
counts.warnings++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Extract file/dir references from skill content
|
|
209
|
+
const pathRefs = extractPathReferences(content);
|
|
210
|
+
const staleRefs = pathRefs.filter(ref => !existsSync(ref) && !existsSync(join(process.cwd(), ref)));
|
|
211
|
+
if (staleRefs.length) {
|
|
212
|
+
statusLine("⚠️ ", skillPath, c.yellow(`stale paths: ${staleRefs.join(", ")}`));
|
|
213
|
+
counts.warnings++;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
statusLine("✅", skillPath, "valid");
|
|
217
|
+
counts.healthy++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (verbose) {
|
|
222
|
+
section("Skills");
|
|
223
|
+
statusLine("⏭ ", "Skills", "none installed");
|
|
224
|
+
}
|
|
225
|
+
// --- Check 8: Files from last run still on disk ---
|
|
226
|
+
// Filter __digest__ — it's a virtual key, not a real file
|
|
227
|
+
if (lastRun?.filesRead.length && verbose) {
|
|
228
|
+
section("Files from last run");
|
|
229
|
+
const realFiles = lastRun.filesRead.filter(f => f !== "__digest__" && f !== ".env");
|
|
230
|
+
for (const f of realFiles.slice(0, 8)) {
|
|
231
|
+
if (existsSync(f)) {
|
|
232
|
+
statusLine("✅", f, "");
|
|
233
|
+
counts.healthy++;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
statusLine("⚠️ ", f, c.yellow("not found on disk"));
|
|
237
|
+
counts.warnings++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (realFiles.length > 8) {
|
|
241
|
+
console.log(c.dim(` ... +${realFiles.length - 8} more`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// --- Summary ---
|
|
245
|
+
console.log("");
|
|
246
|
+
section("Summary");
|
|
247
|
+
if (counts.critical > 0)
|
|
248
|
+
console.log(` 🔴 ${c.red(`${counts.critical} critical`)} (will break Claude Code)`);
|
|
249
|
+
if (counts.warnings > 0)
|
|
250
|
+
console.log(` ⚠️ ${c.yellow(`${counts.warnings} warning(s)`)} (degraded behavior)`);
|
|
251
|
+
console.log(` ✅ ${c.green(`${counts.healthy} healthy`)}`);
|
|
252
|
+
if (counts.critical > 0) {
|
|
253
|
+
console.log(`\n${c.red("Fix critical issues first.")} Run ${c.cyan("npx claude-setup sync")} after fixing.`);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
console.log(`\n${c.green("✅ Done.")}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// --- Helpers ---
|
|
260
|
+
function checkStaleness(lastRunDate) {
|
|
261
|
+
try {
|
|
262
|
+
const last = new Date(lastRunDate).getTime();
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
const daysSince = Math.floor((now - last) / (1000 * 60 * 60 * 24));
|
|
265
|
+
if (daysSince > 7)
|
|
266
|
+
return c.yellow(`(${daysSince} days ago — may be stale)`);
|
|
267
|
+
}
|
|
268
|
+
catch { /* invalid date */ }
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function checkBashQuoting(shellStr) {
|
|
272
|
+
// Check for unescaped double quotes inside the command string
|
|
273
|
+
// The -c "..." outer string must never contain a bare " character
|
|
274
|
+
let inSingleQuote = false;
|
|
275
|
+
let prevChar = "";
|
|
276
|
+
for (let i = 0; i < shellStr.length; i++) {
|
|
277
|
+
const ch = shellStr[i];
|
|
278
|
+
if (ch === "'" && prevChar !== "\\") {
|
|
279
|
+
inSingleQuote = !inSingleQuote;
|
|
280
|
+
}
|
|
281
|
+
// Unescaped double quote inside single-quoted context is a problem
|
|
282
|
+
// when the outer wrapper is double quotes
|
|
283
|
+
if (ch === '"' && !inSingleQuote && prevChar !== "\\") {
|
|
284
|
+
// Check for patterns like ["'] which mix quote types
|
|
285
|
+
if (i > 0 && shellStr[i - 1] === "[") {
|
|
286
|
+
return `mixed quotes in character class at position ${i}: ...${shellStr.slice(Math.max(0, i - 10), i + 10)}...`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
prevChar = ch;
|
|
290
|
+
}
|
|
291
|
+
// Check for unmatched brackets in grep patterns
|
|
292
|
+
const bracketCount = (shellStr.match(/\[/g) || []).length;
|
|
293
|
+
const closeBracketCount = (shellStr.match(/\]/g) || []).length;
|
|
294
|
+
if (bracketCount !== closeBracketCount) {
|
|
295
|
+
return "unmatched brackets in pattern";
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
function extractPathReferences(skillContent) {
|
|
300
|
+
const paths = [];
|
|
301
|
+
// Match file/directory references in skill content
|
|
302
|
+
// Look for patterns like src/, lib/, *.ts references
|
|
303
|
+
const pathPatterns = skillContent.matchAll(/(?:^|\s)((?:src|lib|app|cmd|bin|pkg|internal|api|core|test|tests|spec)\/[\w/.\\-]*)/gm);
|
|
304
|
+
for (const m of pathPatterns) {
|
|
305
|
+
paths.push(m[1].trim());
|
|
306
|
+
}
|
|
307
|
+
return paths;
|
|
72
308
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,20 +5,42 @@ import { runInit } from "./commands/init.js";
|
|
|
5
5
|
import { runAdd } from "./commands/add.js";
|
|
6
6
|
import { runSync } from "./commands/sync.js";
|
|
7
7
|
import { runStatus } from "./commands/status.js";
|
|
8
|
-
import {
|
|
8
|
+
import { runDoctorCommand } from "./commands/doctor.js";
|
|
9
9
|
import { runRemove } from "./commands/remove.js";
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const pkg = require("../package.json");
|
|
12
12
|
const program = new Command();
|
|
13
13
|
program
|
|
14
14
|
.name("claude-setup")
|
|
15
|
-
.description("Setup layer for Claude Code")
|
|
15
|
+
.description("Setup layer for Claude Code — reads your project, writes command files, Claude Code does the rest")
|
|
16
16
|
.version(pkg.version);
|
|
17
|
-
program
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
program
|
|
23
|
-
|
|
17
|
+
program
|
|
18
|
+
.command("init")
|
|
19
|
+
.description("Full project setup — new or existing")
|
|
20
|
+
.option("--dry-run", "Preview what would be written without writing")
|
|
21
|
+
.action((opts) => runInit({ dryRun: opts.dryRun }));
|
|
22
|
+
program
|
|
23
|
+
.command("add")
|
|
24
|
+
.description("Add a multi-file capability")
|
|
25
|
+
.action(runAdd);
|
|
26
|
+
program
|
|
27
|
+
.command("sync")
|
|
28
|
+
.description("Update setup after project changes")
|
|
29
|
+
.option("--dry-run", "Preview changes without writing")
|
|
30
|
+
.action((opts) => runSync({ dryRun: opts.dryRun }));
|
|
31
|
+
program
|
|
32
|
+
.command("status")
|
|
33
|
+
.description("Show current setup state (instant, no file reads)")
|
|
34
|
+
.action(runStatus);
|
|
35
|
+
program
|
|
36
|
+
.command("doctor")
|
|
37
|
+
.description("Validate environment — OS, MCP, hooks, env vars, skills")
|
|
38
|
+
.option("-v, --verbose", "Show passing checks too")
|
|
39
|
+
.action((opts) => runDoctorCommand({ verbose: opts.verbose }));
|
|
40
|
+
program
|
|
41
|
+
.command("remove")
|
|
42
|
+
.description("Remove a capability cleanly")
|
|
43
|
+
.action(runRemove);
|
|
44
|
+
// Default action when no command given
|
|
45
|
+
program.action(() => runInit({}));
|
|
24
46
|
program.parse();
|
package/dist/os.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DetectedOS = "Windows" | "macOS" | "Linux";
|
|
2
|
+
/**
|
|
3
|
+
* Detect OS once per session. Order per spec:
|
|
4
|
+
* 1. COMSPEC set → Windows
|
|
5
|
+
* 2. OS === "Windows_NT" → Windows
|
|
6
|
+
* 3. process.platform === "win32" → Windows
|
|
7
|
+
* 4. uname() === "Darwin" → macOS
|
|
8
|
+
* 5. default → Linux
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectOS(): DetectedOS;
|
|
11
|
+
/** MCP command format per OS */
|
|
12
|
+
export declare function mcpCommandFormat(os: DetectedOS, pkg: string): {
|
|
13
|
+
command: string;
|
|
14
|
+
args: string[];
|
|
15
|
+
};
|
|
16
|
+
/** Hook shell format per OS */
|
|
17
|
+
export declare function hookShellFormat(os: DetectedOS, cmd: string): {
|
|
18
|
+
command: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
};
|
package/dist/os.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Detect OS once per session. Order per spec:
|
|
4
|
+
* 1. COMSPEC set → Windows
|
|
5
|
+
* 2. OS === "Windows_NT" → Windows
|
|
6
|
+
* 3. process.platform === "win32" → Windows
|
|
7
|
+
* 4. uname() === "Darwin" → macOS
|
|
8
|
+
* 5. default → Linux
|
|
9
|
+
*/
|
|
10
|
+
export function detectOS() {
|
|
11
|
+
if (process.env.COMSPEC)
|
|
12
|
+
return "Windows";
|
|
13
|
+
if (process.env.OS === "Windows_NT")
|
|
14
|
+
return "Windows";
|
|
15
|
+
if (process.platform === "win32")
|
|
16
|
+
return "Windows";
|
|
17
|
+
try {
|
|
18
|
+
const uname = execSync("uname", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
19
|
+
if (uname === "Darwin")
|
|
20
|
+
return "macOS";
|
|
21
|
+
}
|
|
22
|
+
catch { /* not unix — unlikely to reach here */ }
|
|
23
|
+
return "Linux";
|
|
24
|
+
}
|
|
25
|
+
/** MCP command format per OS */
|
|
26
|
+
export function mcpCommandFormat(os, pkg) {
|
|
27
|
+
if (os === "Windows") {
|
|
28
|
+
return { command: "cmd", args: ["/c", "npx", pkg] };
|
|
29
|
+
}
|
|
30
|
+
return { command: "npx", args: [pkg] };
|
|
31
|
+
}
|
|
32
|
+
/** Hook shell format per OS */
|
|
33
|
+
export function hookShellFormat(os, cmd) {
|
|
34
|
+
if (os === "Windows") {
|
|
35
|
+
return { command: "cmd", args: ["/c", cmd] };
|
|
36
|
+
}
|
|
37
|
+
return { command: "bash", args: ["-c", cmd] };
|
|
38
|
+
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output helpers — no dependencies.
|
|
3
|
+
* Raw ANSI codes. Falls back gracefully if NO_COLOR or piped.
|
|
4
|
+
*/
|
|
5
|
+
export declare const c: {
|
|
6
|
+
bold: (t: string) => string;
|
|
7
|
+
dim: (t: string) => string;
|
|
8
|
+
red: (t: string) => string;
|
|
9
|
+
green: (t: string) => string;
|
|
10
|
+
yellow: (t: string) => string;
|
|
11
|
+
blue: (t: string) => string;
|
|
12
|
+
cyan: (t: string) => string;
|
|
13
|
+
gray: (t: string) => string;
|
|
14
|
+
};
|
|
15
|
+
/** Print a status line: icon + label + optional detail */
|
|
16
|
+
export declare function statusLine(icon: string, label: string, detail?: string): void;
|
|
17
|
+
/** Print a section header */
|
|
18
|
+
export declare function section(title: string): void;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output helpers — no dependencies.
|
|
3
|
+
* Raw ANSI codes. Falls back gracefully if NO_COLOR or piped.
|
|
4
|
+
*/
|
|
5
|
+
const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
6
|
+
const codes = {
|
|
7
|
+
reset: "\x1b[0m",
|
|
8
|
+
bold: "\x1b[1m",
|
|
9
|
+
dim: "\x1b[2m",
|
|
10
|
+
red: "\x1b[31m",
|
|
11
|
+
green: "\x1b[32m",
|
|
12
|
+
yellow: "\x1b[33m",
|
|
13
|
+
blue: "\x1b[34m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
gray: "\x1b[90m",
|
|
16
|
+
};
|
|
17
|
+
function wrap(code, text) {
|
|
18
|
+
if (!supportsColor)
|
|
19
|
+
return text;
|
|
20
|
+
return `${code}${text}${codes.reset}`;
|
|
21
|
+
}
|
|
22
|
+
export const c = {
|
|
23
|
+
bold: (t) => wrap(codes.bold, t),
|
|
24
|
+
dim: (t) => wrap(codes.dim, t),
|
|
25
|
+
red: (t) => wrap(codes.red, t),
|
|
26
|
+
green: (t) => wrap(codes.green, t),
|
|
27
|
+
yellow: (t) => wrap(codes.yellow, t),
|
|
28
|
+
blue: (t) => wrap(codes.blue, t),
|
|
29
|
+
cyan: (t) => wrap(codes.cyan, t),
|
|
30
|
+
gray: (t) => wrap(codes.gray, t),
|
|
31
|
+
};
|
|
32
|
+
/** Print a status line: icon + label + optional detail */
|
|
33
|
+
export function statusLine(icon, label, detail) {
|
|
34
|
+
const detailStr = detail ? ` — ${detail}` : "";
|
|
35
|
+
console.log(` ${icon} ${label}${detailStr}`);
|
|
36
|
+
}
|
|
37
|
+
/** Print a section header */
|
|
38
|
+
export function section(title) {
|
|
39
|
+
console.log(`\n${c.bold(title)}`);
|
|
40
|
+
}
|
package/package.json
CHANGED
package/templates/add.md
CHANGED
|
@@ -5,7 +5,7 @@ Add to Claude Code setup: "{{USER_INPUT}}"
|
|
|
5
5
|
## Project context
|
|
6
6
|
{{PROJECT_CONTEXT}}
|
|
7
7
|
|
|
8
|
-
## Current setup
|
|
8
|
+
## Current setup — read before writing anything
|
|
9
9
|
|
|
10
10
|
{{#if HAS_CLAUDE_MD}}
|
|
11
11
|
### CLAUDE.md
|
|
@@ -25,9 +25,13 @@ Add to Claude Code setup: "{{USER_INPUT}}"
|
|
|
25
25
|
Skills: {{SKILLS_LIST}} | Commands: {{COMMANDS_LIST}}
|
|
26
26
|
|
|
27
27
|
## Rules
|
|
28
|
-
- Read current content before writing. Merge/append only.
|
|
29
|
-
- If request mentions something not in project files: ask first.
|
|
28
|
+
- Read current content above before writing. Merge/append only.
|
|
29
|
+
- If request mentions something not evidenced in project files: ask first.
|
|
30
|
+
- OS detected: {{DETECTED_OS}}. Use correct command format for MCP/hooks:
|
|
31
|
+
- Windows: `{ "command": "cmd", "args": ["/c", "npx", "<pkg>"] }`
|
|
32
|
+
- macOS/Linux: `{ "command": "npx", "args": ["<pkg>"] }`
|
|
33
|
+
- All env var refs use `${VARNAME}` syntax. Document new vars in .env.example.
|
|
30
34
|
|
|
31
35
|
## Output — one line per file
|
|
32
36
|
Updated: ✅ [path] — [what and why]
|
|
33
|
-
Skipped: ⏭ [path] — [why]
|
|
37
|
+
Skipped: ⏭ [path] — [why not needed for this request]
|
package/templates/init.md
CHANGED
|
@@ -12,38 +12,102 @@ Set up this project for Claude Code. Reason from what you see. Don't ask questio
|
|
|
12
12
|
{{SOURCE_CONTEXT}}
|
|
13
13
|
{{/if}}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
{{#if HAS_SKIPPED}}
|
|
16
|
+
## Files skipped (too large or filtered)
|
|
17
|
+
|
|
18
|
+
{{SKIPPED_LIST}}
|
|
19
|
+
{{/if}}
|
|
20
|
+
|
|
21
|
+
## Existing setup — READ EVERY LINE BEFORE TOUCHING ANYTHING
|
|
16
22
|
|
|
17
23
|
{{#if HAS_CLAUDE_MD}}
|
|
18
|
-
### CLAUDE.md
|
|
24
|
+
### CLAUDE.md — EXISTS — append only, never rewrite, never remove
|
|
19
25
|
{{CLAUDE_MD_CONTENT}}
|
|
20
26
|
{{else}}
|
|
21
|
-
CLAUDE.md
|
|
27
|
+
CLAUDE.md → does not exist (create it)
|
|
22
28
|
{{/if}}
|
|
23
29
|
|
|
24
30
|
{{#if HAS_MCP_JSON}}
|
|
25
|
-
### .mcp.json
|
|
31
|
+
### .mcp.json — EXISTS — merge only, never remove existing entries
|
|
26
32
|
{{MCP_JSON_CONTENT}}
|
|
33
|
+
|
|
34
|
+
OS detected: {{DETECTED_OS}}. Use correct MCP command format:
|
|
35
|
+
- Windows: `{ "command": "cmd", "args": ["/c", "npx", "<package>"] }`
|
|
36
|
+
- macOS/Linux: `{ "command": "npx", "args": ["<package>"] }`
|
|
27
37
|
{{else}}
|
|
28
|
-
.mcp.json
|
|
38
|
+
.mcp.json → does not exist (create only if you find evidence of external services)
|
|
29
39
|
{{/if}}
|
|
30
40
|
|
|
31
41
|
{{#if HAS_SETTINGS}}
|
|
32
|
-
### settings.json
|
|
42
|
+
### .claude/settings.json — EXISTS — merge only, never remove existing hooks
|
|
33
43
|
{{SETTINGS_CONTENT}}
|
|
44
|
+
|
|
45
|
+
Hook shell format for {{DETECTED_OS}}:
|
|
46
|
+
- Windows: `{ "command": "cmd", "args": ["/c", "<command>"] }`
|
|
47
|
+
- macOS/Linux: `{ "command": "bash", "args": ["-c", "<command>"] }`
|
|
48
|
+
- Bash quoting rule: never use bare `"` inside `-c "..."` — use `\x22` instead
|
|
34
49
|
{{else}}
|
|
35
|
-
settings.json
|
|
50
|
+
settings.json → does not exist (create only if hooks are warranted)
|
|
36
51
|
{{/if}}
|
|
37
52
|
|
|
38
|
-
Skills:
|
|
53
|
+
Skills installed: {{SKILLS_LIST}}
|
|
54
|
+
Commands installed: {{COMMANDS_LIST}}
|
|
55
|
+
Workflows installed: {{WORKFLOWS_LIST}}
|
|
56
|
+
.github/ exists: {{HAS_GITHUB_DIR}}
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Your job
|
|
61
|
+
|
|
62
|
+
Read the files above. Figure out the project from what you see — language, runtime,
|
|
63
|
+
dependencies, structure, conventions. Do not assume anything not visible in the files.
|
|
64
|
+
|
|
65
|
+
Then write the Claude Code setup for THIS specific project.
|
|
66
|
+
|
|
67
|
+
### CLAUDE.md
|
|
68
|
+
Always write or update. Make it specific: reference actual file paths, actual patterns,
|
|
69
|
+
actual conventions from the code. Generic advice belongs in docs, not here.
|
|
70
|
+
If it exists: read it above first. Add only what is genuinely missing. Never remove.
|
|
71
|
+
|
|
72
|
+
### .mcp.json
|
|
73
|
+
Only if you found evidence of external services in the config files, dependencies,
|
|
74
|
+
or environment template. No evidence = no server.
|
|
75
|
+
If it exists: add to it. Never remove existing entries. Produce valid JSON.
|
|
76
|
+
Use OS-correct command format (see above).
|
|
77
|
+
|
|
78
|
+
### .claude/settings.json
|
|
79
|
+
Only if hooks genuinely earn their cost for this specific project.
|
|
80
|
+
Every hook adds overhead on every Claude Code action.
|
|
81
|
+
If it exists: add to it. Never remove existing hooks.
|
|
82
|
+
Use OS-correct shell format (see above).
|
|
83
|
+
|
|
84
|
+
### .claude/skills/
|
|
85
|
+
Only for patterns that recur across this codebase and benefit from automatic loading.
|
|
86
|
+
Use `applies-when` frontmatter so skills load only when relevant.
|
|
87
|
+
If a similar skill already exists: extend it.
|
|
88
|
+
|
|
89
|
+
### .github/workflows/
|
|
90
|
+
Only if .github/ exists ({{HAS_GITHUB_DIR}}).
|
|
91
|
+
Only workflows warranted by what you found.
|
|
92
|
+
If workflows already exist: do not touch them.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Absolute rules
|
|
97
|
+
|
|
98
|
+
1. You have the full content of every existing Claude config file above.
|
|
99
|
+
Read it before writing. Never write blind.
|
|
100
|
+
2. Append and merge only. Never rewrite a file in full. Never remove existing content.
|
|
101
|
+
3. Write only what is evidenced by the project files. No evidence = skip it.
|
|
102
|
+
4. Every line in CLAUDE.md must reference something you actually saw.
|
|
103
|
+
No generic boilerplate. No advice identical for every project.
|
|
104
|
+
5. MCP servers, skills, and hooks add cost on every Claude Code session.
|
|
105
|
+
Only add them if they clearly earn their keep for THIS project.
|
|
106
|
+
|
|
107
|
+
---
|
|
39
108
|
|
|
40
|
-
##
|
|
41
|
-
1. Read existing content above before writing. Never write blind.
|
|
42
|
-
2. Append/merge only. Never rewrite or remove.
|
|
43
|
-
3. Every line must trace to something in the project. No generic boilerplate.
|
|
44
|
-
4. MCP/skills/hooks cost tokens every session. Only add if clearly earned.
|
|
109
|
+
## Output format — one line per file, nothing else
|
|
45
110
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Skipped: ⏭ [path] — [why not needed]
|
|
111
|
+
Created: ✅ [path] — [one clause: what you saw that justified it]
|
|
112
|
+
Updated: ✅ [path] — [one clause: what was added]
|
|
113
|
+
Skipped: ⏭ [path] — [one clause: why not needed]
|