claude-setup 1.1.1 → 1.1.3

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/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,347 @@ function readIfExists(filePath) {
20
23
  return null;
21
24
  }
22
25
  }
23
- function line(icon, label, detail) {
24
- console.log(` ${icon} ${label}${detail ? ` — ${detail}` : ""}`);
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
- console.log("claude-setup doctor\n");
30
- // Claude Code installed?
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
- line(cv ? "✅" : "❌", "Claude Code", cv?.trim() ?? "not found");
33
- // Manifest?
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
- line(manifest ? "✅" : "⚠️ ", ".claude/claude-setup.json", manifest ? `last: ${lastRun?.command} at ${lastRun?.at}` : "not found — run: npx claude-setup init");
36
- // Files from last run still on disk?
37
- if (lastRun?.filesRead.length) {
38
- console.log("\nFiles from last run (sample):");
39
- for (const f of lastRun.filesRead.slice(0, 8)) {
40
- line(existsSync(f) ? "✅" : "⚠️ ", f, existsSync(f) ? "" : "not found on disk");
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 2b: Out-of-band edit detection ---
66
+ if (lastRun) {
67
+ const { createHash } = await import("crypto");
68
+ const oobFiles = [
69
+ { label: "CLAUDE.md", path: join(process.cwd(), "CLAUDE.md"), snapshotKey: "CLAUDE.md" },
70
+ { label: ".mcp.json", path: join(process.cwd(), ".mcp.json"), snapshotKey: ".mcp.json" },
71
+ { label: "settings.json", path: join(process.cwd(), ".claude", "settings.json"), snapshotKey: ".claude/settings.json" },
72
+ ];
73
+ for (const mf of oobFiles) {
74
+ if (!existsSync(mf.path))
75
+ continue;
76
+ const content = readFileSync(mf.path, "utf8");
77
+ const currentHash = createHash("sha256").update(content).digest("hex");
78
+ const snapshotHash = lastRun.snapshot[mf.snapshotKey];
79
+ if (snapshotHash && currentHash !== snapshotHash) {
80
+ statusLine("⚠️ ", mf.label, c.yellow("modified outside the CLI since last run — run sync to re-snapshot"));
81
+ counts.warnings++;
82
+ }
41
83
  }
42
84
  }
43
- // Env vars in .mcp.json present in env template?
85
+ // --- Check 3: OS/MCP format mismatch ---
86
+ if (state.mcpJson.content) {
87
+ section("MCP servers");
88
+ const mcp = safeJsonParse(state.mcpJson.content);
89
+ if (mcp && typeof mcp.mcpServers === "object" && mcp.mcpServers !== null) {
90
+ const servers = mcp.mcpServers;
91
+ for (const [name, config] of Object.entries(servers)) {
92
+ const cmd = config.command;
93
+ if (!cmd) {
94
+ statusLine("⚠️ ", name, c.yellow("no command field"));
95
+ counts.warnings++;
96
+ continue;
97
+ }
98
+ if (os === "Windows" && cmd === "npx") {
99
+ statusLine("🔴", name, c.red(`BROKEN: Windows can't execute npx directly — use cmd /c npx`));
100
+ counts.critical++;
101
+ }
102
+ else if (os !== "Windows" && cmd === "cmd") {
103
+ statusLine("⚠️ ", name, c.yellow(`UNNECESSARY: cmd wrapper not needed on ${os}`));
104
+ counts.warnings++;
105
+ }
106
+ else {
107
+ statusLine("✅", name, `valid, OS-format correct (${cmd})`);
108
+ counts.healthy++;
109
+ }
110
+ // Check for missing -y flag in npx args
111
+ const args = config.args;
112
+ if (args) {
113
+ const npxIndex = args.indexOf("npx");
114
+ if (npxIndex >= 0 && args[npxIndex + 1] !== "-y") {
115
+ statusLine("⚠️ ", name, c.yellow(`npx without -y flag — installs may hang`));
116
+ counts.warnings++;
117
+ }
118
+ // Check for hardcoded connection strings in args
119
+ for (const arg of args) {
120
+ if (/^(postgresql|postgres|mysql|mongodb|redis|amqp):\/\//.test(arg)) {
121
+ statusLine("🔴", name, c.red(`HARDCODED connection string in args — move to env: { "DATABASE_URL": "\${DATABASE_URL}" }`));
122
+ counts.critical++;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ // Check for channel-type servers
129
+ const channelNames = ["telegram", "discord", "fakechat"];
130
+ for (const [name] of Object.entries(servers)) {
131
+ if (channelNames.includes(name.toLowerCase())) {
132
+ statusLine("⚠️ ", `${name} (CHANNEL)`, c.yellow(`channel server detected — .mcp.json alone does not activate delivery. ` +
133
+ `Launch with: claude --channels plugin:${name.toLowerCase()}@claude-plugins-official`));
134
+ counts.warnings++;
135
+ }
136
+ }
137
+ }
138
+ else if (mcp) {
139
+ // Check for flat structure (some .mcp.json files use flat keys)
140
+ if (verbose)
141
+ statusLine("⚠️ ", ".mcp.json", "no mcpServers key found");
142
+ }
143
+ }
144
+ else if (verbose) {
145
+ section("MCP servers");
146
+ statusLine("⏭ ", ".mcp.json", "does not exist");
147
+ }
148
+ // --- Check 4: Hook quoting bugs ---
149
+ if (state.settings.content) {
150
+ section("Hooks");
151
+ const settings = safeJsonParse(state.settings.content);
152
+ if (settings) {
153
+ const hookCategories = [
154
+ "PreToolUse", "PostToolUse", "PostToolUseFailure",
155
+ "Stop", "SessionStart"
156
+ ];
157
+ // Bug 6: Check for model override
158
+ if (settings["model"]) {
159
+ statusLine("⚠️ ", "MODEL OVERRIDE", c.yellow(`"model": "${settings["model"]}" in settings.json forces this model on every session. Remove it if not intentional.`));
160
+ counts.warnings++;
161
+ }
162
+ // Check for invalid hook event names
163
+ const validHookNames = new Set(hookCategories);
164
+ for (const key of Object.keys(settings)) {
165
+ if (key === "permissions" || key === "model" || key === "env" || key === "allowedTools")
166
+ continue;
167
+ if (Array.isArray(settings[key]) && !validHookNames.has(key)) {
168
+ statusLine("🔴", `"${key}"`, c.red(`INVALID hook event name. Valid names: ${hookCategories.join(", ")}`));
169
+ counts.critical++;
170
+ }
171
+ }
172
+ let foundHooks = false;
173
+ for (const category of hookCategories) {
174
+ const hooks = settings[category];
175
+ if (!hooks || !Array.isArray(hooks))
176
+ continue;
177
+ for (const hook of hooks) {
178
+ if (typeof hook !== "object" || !hook)
179
+ continue;
180
+ const h = hook;
181
+ const hookCmd = h.command;
182
+ const args = h.args;
183
+ if (!hookCmd || !args)
184
+ continue;
185
+ foundHooks = true;
186
+ // Check for bash -c quoting bugs
187
+ if (hookCmd === "bash" && args[0] === "-c" && args[1]) {
188
+ const shellStr = args[1];
189
+ const quotingIssue = checkBashQuoting(shellStr);
190
+ if (quotingIssue) {
191
+ statusLine("🔴", `${category} hook`, c.red(`quoting bug: ${quotingIssue}`));
192
+ counts.critical++;
193
+ }
194
+ else {
195
+ statusLine("✅", `${category} hook`, "quoting clean");
196
+ counts.healthy++;
197
+ }
198
+ }
199
+ else {
200
+ statusLine("✅", `${category} hook`, "valid");
201
+ counts.healthy++;
202
+ }
203
+ }
204
+ }
205
+ if (!foundHooks && verbose) {
206
+ statusLine("⏭ ", "Hooks", "none configured");
207
+ }
208
+ }
209
+ }
210
+ else if (verbose) {
211
+ section("Hooks");
212
+ statusLine("⏭ ", "settings.json", "does not exist");
213
+ }
214
+ // --- Check 5: Env var coverage ---
44
215
  if (state.mcpJson.content) {
45
216
  const refs = [...state.mcpJson.content.matchAll(/\$\{?([A-Z_][A-Z0-9_]+)\}?/g)]
46
217
  .map(m => m[1]);
47
218
  const unique = [...new Set(refs)];
48
219
  if (unique.length) {
49
- const template = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? "";
50
- console.log("\nMCP environment variables:");
220
+ const template = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? readIfExists(".env.template") ?? "";
221
+ section("Env vars");
51
222
  for (const v of unique) {
52
- line(template.includes(v) ? "✅" : "⚠️ ", v, template.includes(v) ? "found in env template" : "missing from .env.example");
223
+ if (template.includes(v)) {
224
+ statusLine("✅", `\${${v}}`, "found in env template");
225
+ counts.healthy++;
226
+ }
227
+ else {
228
+ statusLine("⚠️ ", `\${${v}}`, c.yellow("used in .mcp.json but missing from .env.example"));
229
+ counts.warnings++;
230
+ }
53
231
  }
54
232
  }
55
233
  }
56
- // Workflow secrets
234
+ // --- Check 6: Workflow secret coverage ---
57
235
  if (state.workflows.length) {
58
236
  const secrets = new Set();
59
237
  for (const wf of state.workflows) {
60
238
  const content = readIfExists(wf) ?? "";
61
- for (const m of content.matchAll(/\$\{\{\s*secrets\.([A-Z_]+)\s*\}\}/g)) {
239
+ for (const m of content.matchAll(/\$\{\{\s*secrets\.([A-Z_][A-Z0-9_]*)\s*\}\}/g)) {
62
240
  secrets.add(m[1]);
63
241
  }
64
242
  }
65
243
  if (secrets.size) {
66
- console.log("\nWorkflow secrets (add to GitHub Settings → Secrets):");
67
- for (const s of secrets)
68
- console.log(` ⚠️ ${s}`);
244
+ section("Workflow secrets");
245
+ const readme = readIfExists("README.md") ?? "";
246
+ const envTemplate = readIfExists(".env.example") ?? readIfExists(".env.sample") ?? "";
247
+ for (const s of secrets) {
248
+ if (readme.includes(s) || envTemplate.includes(s)) {
249
+ statusLine("✅", s, "documented");
250
+ counts.healthy++;
251
+ }
252
+ else {
253
+ statusLine("⚠️ ", s, c.yellow("needs GitHub Settings → Secrets"));
254
+ counts.warnings++;
255
+ }
256
+ }
69
257
  }
70
258
  }
71
- console.log("\n✅ Done.");
259
+ // --- Check 7: Stale skill paths ---
260
+ if (state.skills.length) {
261
+ section("Skills");
262
+ for (const skillPath of state.skills) {
263
+ const content = readIfExists(skillPath);
264
+ if (!content) {
265
+ statusLine("⚠️ ", skillPath, c.yellow("skill file not readable"));
266
+ counts.warnings++;
267
+ continue;
268
+ }
269
+ // Extract file/dir references from skill content
270
+ const pathRefs = extractPathReferences(content);
271
+ const staleRefs = pathRefs.filter(ref => !existsSync(ref) && !existsSync(join(process.cwd(), ref)));
272
+ if (staleRefs.length) {
273
+ statusLine("⚠️ ", skillPath, c.yellow(`stale paths: ${staleRefs.join(", ")}`));
274
+ counts.warnings++;
275
+ }
276
+ else {
277
+ statusLine("✅", skillPath, "valid");
278
+ counts.healthy++;
279
+ }
280
+ }
281
+ }
282
+ else if (verbose) {
283
+ section("Skills");
284
+ statusLine("⏭ ", "Skills", "none installed");
285
+ }
286
+ // --- Check 8: Files from last run still on disk ---
287
+ // Filter __digest__ — it's a virtual key, not a real file
288
+ if (lastRun?.filesRead.length && verbose) {
289
+ section("Files from last run");
290
+ const realFiles = lastRun.filesRead.filter(f => f !== "__digest__" && f !== ".env");
291
+ for (const f of realFiles.slice(0, 8)) {
292
+ if (existsSync(f)) {
293
+ statusLine("✅", f, "");
294
+ counts.healthy++;
295
+ }
296
+ else {
297
+ statusLine("⚠️ ", f, c.yellow("not found on disk"));
298
+ counts.warnings++;
299
+ }
300
+ }
301
+ if (realFiles.length > 8) {
302
+ console.log(c.dim(` ... +${realFiles.length - 8} more`));
303
+ }
304
+ }
305
+ // --- Summary ---
306
+ console.log("");
307
+ section("Summary");
308
+ if (counts.critical > 0)
309
+ console.log(` 🔴 ${c.red(`${counts.critical} critical`)} (will break Claude Code)`);
310
+ if (counts.warnings > 0)
311
+ console.log(` ⚠️ ${c.yellow(`${counts.warnings} warning(s)`)} (degraded behavior)`);
312
+ console.log(` ✅ ${c.green(`${counts.healthy} healthy`)}`);
313
+ if (counts.critical > 0) {
314
+ console.log(`\n${c.red("Fix critical issues first.")} Run ${c.cyan("npx claude-setup sync")} after fixing.`);
315
+ }
316
+ else {
317
+ console.log(`\n${c.green("✅ Done.")}`);
318
+ }
319
+ }
320
+ // --- Helpers ---
321
+ function checkStaleness(lastRunDate) {
322
+ try {
323
+ const last = new Date(lastRunDate).getTime();
324
+ const now = Date.now();
325
+ const daysSince = Math.floor((now - last) / (1000 * 60 * 60 * 24));
326
+ if (daysSince > 7)
327
+ return c.yellow(`(${daysSince} days ago — may be stale)`);
328
+ }
329
+ catch { /* invalid date */ }
330
+ return null;
331
+ }
332
+ function checkBashQuoting(shellStr) {
333
+ // Check for unescaped double quotes inside the command string
334
+ // The -c "..." outer string must never contain a bare " character
335
+ let inSingleQuote = false;
336
+ let prevChar = "";
337
+ for (let i = 0; i < shellStr.length; i++) {
338
+ const ch = shellStr[i];
339
+ if (ch === "'" && prevChar !== "\\") {
340
+ inSingleQuote = !inSingleQuote;
341
+ }
342
+ // Unescaped double quote inside single-quoted context is a problem
343
+ // when the outer wrapper is double quotes
344
+ if (ch === '"' && !inSingleQuote && prevChar !== "\\") {
345
+ // Check for patterns like ["'] which mix quote types
346
+ if (i > 0 && shellStr[i - 1] === "[") {
347
+ return `mixed quotes in character class at position ${i}: ...${shellStr.slice(Math.max(0, i - 10), i + 10)}...`;
348
+ }
349
+ }
350
+ prevChar = ch;
351
+ }
352
+ // Check for unmatched brackets in grep patterns
353
+ const bracketCount = (shellStr.match(/\[/g) || []).length;
354
+ const closeBracketCount = (shellStr.match(/\]/g) || []).length;
355
+ if (bracketCount !== closeBracketCount) {
356
+ return "unmatched brackets in pattern";
357
+ }
358
+ return null;
359
+ }
360
+ function extractPathReferences(skillContent) {
361
+ const paths = [];
362
+ // Match file/directory references in skill content
363
+ // Look for patterns like src/, lib/, *.ts references
364
+ const pathPatterns = skillContent.matchAll(/(?:^|\s)((?:src|lib|app|cmd|bin|pkg|internal|api|core|test|tests|spec)\/[\w/.\\-]*)/gm);
365
+ for (const m of pathPatterns) {
366
+ paths.push(m[1].trim());
367
+ }
368
+ return paths;
72
369
  }
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 { runDoctor } from "./commands/doctor.js";
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.command("init").description("Full project setup — new or existing").action(runInit);
18
- program.command("add").description("Add a multi-file capability").action(runAdd);
19
- program.command("sync").description("Update setup after project changes").action(runSync);
20
- program.command("status").description("Show current setup (instant)").action(runStatus);
21
- program.command("doctor").description("Validate environment").action(runDoctor);
22
- program.command("remove").description("Remove a capability cleanly").action(runRemove);
23
- program.action(runInit);
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/manifest.js CHANGED
@@ -47,6 +47,18 @@ export async function updateManifest(command, collected, opts = {}) {
47
47
  for (const { path, content } of collected.source) {
48
48
  snapshot[path] = sha256(content);
49
49
  }
50
+ // Also snapshot CLI-managed files for out-of-band edit detection
51
+ const managedFiles = [
52
+ { key: "CLAUDE.md", path: join(cwd, "CLAUDE.md") },
53
+ { key: ".mcp.json", path: join(cwd, ".mcp.json") },
54
+ { key: ".claude/settings.json", path: join(cwd, ".claude", "settings.json") },
55
+ ];
56
+ for (const mf of managedFiles) {
57
+ if (existsSync(mf.path)) {
58
+ const content = readFileSync(mf.path, "utf8");
59
+ snapshot[mf.key] = sha256(content);
60
+ }
61
+ }
50
62
  const filesRead = [
51
63
  ...Object.keys(collected.configs),
52
64
  ...collected.source.map(s => s.path),
package/dist/os.d.ts ADDED
@@ -0,0 +1,25 @@
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
+ /**
12
+ * Verified MCP package names — ONLY use these.
13
+ * If a service is not in this map, do not guess a package name.
14
+ */
15
+ export declare const VERIFIED_MCP_PACKAGES: Record<string, string>;
16
+ /** MCP command format per OS — always includes -y to prevent npx install hangs */
17
+ export declare function mcpCommandFormat(os: DetectedOS, pkg: string): {
18
+ command: string;
19
+ args: string[];
20
+ };
21
+ /** Hook shell format per OS */
22
+ export declare function hookShellFormat(os: DetectedOS, cmd: string): {
23
+ command: string;
24
+ args: string[];
25
+ };
package/dist/os.js ADDED
@@ -0,0 +1,52 @@
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
+ /**
26
+ * Verified MCP package names — ONLY use these.
27
+ * If a service is not in this map, do not guess a package name.
28
+ */
29
+ export const VERIFIED_MCP_PACKAGES = {
30
+ playwright: "@playwright/mcp@latest",
31
+ postgres: "@modelcontextprotocol/server-postgres",
32
+ filesystem: "@modelcontextprotocol/server-filesystem",
33
+ memory: "@modelcontextprotocol/server-memory",
34
+ github: "@modelcontextprotocol/server-github",
35
+ brave: "@modelcontextprotocol/server-brave-search",
36
+ puppeteer: "@modelcontextprotocol/server-puppeteer",
37
+ slack: "@modelcontextprotocol/server-slack",
38
+ };
39
+ /** MCP command format per OS — always includes -y to prevent npx install hangs */
40
+ export function mcpCommandFormat(os, pkg) {
41
+ if (os === "Windows") {
42
+ return { command: "cmd", args: ["/c", "npx", "-y", pkg] };
43
+ }
44
+ return { command: "npx", args: ["-y", pkg] };
45
+ }
46
+ /** Hook shell format per OS */
47
+ export function hookShellFormat(os, cmd) {
48
+ if (os === "Windows") {
49
+ return { command: "cmd", args: ["/c", cmd] };
50
+ }
51
+ return { command: "bash", args: ["-c", cmd] };
52
+ }
@@ -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/dist/state.js CHANGED
@@ -9,9 +9,13 @@ export async function readState(cwd = process.cwd()) {
9
9
  const claudeMd = readIfExists(claudeMdPath);
10
10
  const mcpJson = readIfExists(mcpJsonPath);
11
11
  const settings = readIfExists(settingsPath);
12
+ // Scan all three skill patterns and deduplicate (Bug 4 fix)
12
13
  let skills = [];
13
14
  try {
14
- skills = await glob(".claude/skills/*/SKILL.md", { cwd, posix: true });
15
+ const structured = await glob(".claude/skills/*/SKILL.md", { cwd, posix: true });
16
+ const flat = await glob(".claude/skills/*.md", { cwd, posix: true });
17
+ const nested = await glob(".claude/skills/**/*.md", { cwd, posix: true });
18
+ skills = [...new Set([...structured, ...flat, ...nested])];
15
19
  }
16
20
  catch { /* no skills */ }
17
21
  let commands = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-setup",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Setup layer for Claude Code — reads your project, writes command files, Claude Code does the rest",
5
5
  "type": "module",
6
6
  "bin": {
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]