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/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
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
<!-- Run /stack-add in Claude Code -->
|
|
1
|
+
<!-- claude-setup add {{DATE}} -->
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
"{{USER_INPUT}}"
|
|
6
|
-
|
|
7
|
-
---
|
|
3
|
+
Add to Claude Code setup: "{{USER_INPUT}}"
|
|
8
4
|
|
|
9
5
|
## Project context
|
|
10
|
-
|
|
11
|
-
{{CONFIG_FILES}}
|
|
12
|
-
|
|
13
|
-
{{SOURCE_FILES}}
|
|
14
|
-
|
|
15
|
-
---
|
|
6
|
+
{{PROJECT_CONTEXT}}
|
|
16
7
|
|
|
17
8
|
## Current setup — read before writing anything
|
|
18
9
|
|
|
@@ -27,38 +18,20 @@ The developer wants to add this to their Claude Code setup:
|
|
|
27
18
|
{{/if}}
|
|
28
19
|
|
|
29
20
|
{{#if HAS_SETTINGS}}
|
|
30
|
-
###
|
|
21
|
+
### settings.json
|
|
31
22
|
{{SETTINGS_CONTENT}}
|
|
32
23
|
{{/if}}
|
|
33
24
|
|
|
34
|
-
Skills
|
|
35
|
-
Commands installed: {{COMMANDS_LIST}}
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Your job
|
|
40
|
-
|
|
41
|
-
Figure out which files need to change to fulfill the request.
|
|
42
|
-
Adding a capability to Claude Code is rarely one file — it usually means touching
|
|
43
|
-
CLAUDE.md, possibly an MCP entry, possibly a skill, possibly a hook.
|
|
44
|
-
|
|
45
|
-
For every file you touch:
|
|
46
|
-
- Read its current content above first
|
|
47
|
-
- Merge and append only
|
|
48
|
-
- Do not duplicate what already exists
|
|
49
|
-
- Extend existing files rather than creating parallel ones
|
|
50
|
-
|
|
51
|
-
If the request mentions something not evidenced in the project files: say so and ask
|
|
52
|
-
whether they want to add it generically or need to add the underlying service first.
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Output format — strict
|
|
57
|
-
|
|
58
|
-
Updated:
|
|
59
|
-
✅ [path] — [what changed and why]
|
|
25
|
+
Skills: {{SKILLS_LIST}} | Commands: {{COMMANDS_LIST}}
|
|
60
26
|
|
|
61
|
-
|
|
62
|
-
|
|
27
|
+
## Rules
|
|
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.
|
|
63
34
|
|
|
64
|
-
|
|
35
|
+
## Output — one line per file
|
|
36
|
+
Updated: ✅ [path] — [what and why]
|
|
37
|
+
Skipped: ⏭ [path] — [why not needed for this request]
|