claude-setup 1.1.3 → 1.1.4

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.
@@ -0,0 +1,289 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join, basename } from "path";
3
+ import { readState } from "../state.js";
4
+ import { detectOS } from "../os.js";
5
+ import { c, section } from "../output.js";
6
+ import { createInterface } from "readline";
7
+ async function promptFreeText(question) {
8
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9
+ return new Promise((resolve) => {
10
+ rl.question(question + " ", (answer) => {
11
+ rl.close();
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+ export async function runExport() {
17
+ const cwd = process.cwd();
18
+ const state = await readState(cwd);
19
+ const os = detectOS();
20
+ const name = await promptFreeText("Template name:");
21
+ if (!name) {
22
+ console.log("No name provided.");
23
+ return;
24
+ }
25
+ const template = {
26
+ name,
27
+ version: "1",
28
+ exportedAt: new Date().toISOString(),
29
+ exportedFrom: basename(cwd),
30
+ os,
31
+ skills: [],
32
+ commands: [],
33
+ };
34
+ // Capture CLAUDE.md
35
+ if (state.claudeMd.content) {
36
+ template.claudeMd = state.claudeMd.content;
37
+ }
38
+ // Capture .mcp.json (parsed as object for OS adaptation on import)
39
+ if (state.mcpJson.content) {
40
+ try {
41
+ template.mcpJson = JSON.parse(state.mcpJson.content);
42
+ }
43
+ catch { /* skip invalid */ }
44
+ }
45
+ // Capture settings.json — never export model key
46
+ if (state.settings.content) {
47
+ try {
48
+ const settings = JSON.parse(state.settings.content);
49
+ delete settings["model"];
50
+ template.settings = settings;
51
+ }
52
+ catch { /* skip invalid */ }
53
+ }
54
+ // Capture skills
55
+ for (const skillPath of state.skills) {
56
+ const fullPath = join(cwd, skillPath);
57
+ if (!existsSync(fullPath))
58
+ continue;
59
+ try {
60
+ const content = readFileSync(fullPath, "utf8");
61
+ const skillName = skillPath.split("/").at(-2) ?? skillPath.split("/").pop()?.replace(".md", "") ?? skillPath;
62
+ template.skills.push({ name: skillName, content });
63
+ }
64
+ catch { /* skip */ }
65
+ }
66
+ // Capture commands (excluding stack-* artifacts)
67
+ for (const cmdPath of state.commands) {
68
+ const fullPath = join(cwd, cmdPath);
69
+ if (!existsSync(fullPath))
70
+ continue;
71
+ try {
72
+ const content = readFileSync(fullPath, "utf8");
73
+ const cmdName = cmdPath.split("/").pop()?.replace(".md", "") ?? cmdPath;
74
+ template.commands.push({ name: cmdName, content });
75
+ }
76
+ catch { /* skip */ }
77
+ }
78
+ const filename = `${name.replace(/[^a-zA-Z0-9_-]/g, "-")}.claude-template.json`;
79
+ writeFileSync(join(cwd, filename), JSON.stringify(template, null, 2), "utf8");
80
+ const serverCount = template.mcpJson
81
+ ? Object.keys(template.mcpJson.mcpServers ?? {}).length
82
+ : 0;
83
+ console.log(`
84
+ ${c.green("✅")} Template exported: ${c.cyan(filename)}
85
+
86
+ Contents:
87
+ CLAUDE.md : ${template.claudeMd ? "included" : "not found"}
88
+ .mcp.json : ${serverCount ? `${serverCount} server(s)` : "not found"}
89
+ settings.json: ${template.settings ? "included" : "not found"}
90
+ Skills : ${template.skills.length}
91
+ Commands : ${template.commands.length}
92
+
93
+ Apply to another project:
94
+ ${c.cyan(`npx claude-setup init --template ${filename}`)}
95
+ `);
96
+ }
97
+ /**
98
+ * Apply a template to the current project.
99
+ * Merge logic: existing content kept, new content added.
100
+ * OS adaptation: MCP commands auto-converted for target OS.
101
+ */
102
+ export async function applyTemplate(templateSource) {
103
+ const cwd = process.cwd();
104
+ let templateContent;
105
+ // Resolve template source — local file or URL
106
+ if (templateSource.startsWith("http://") || templateSource.startsWith("https://")) {
107
+ try {
108
+ const response = await fetch(templateSource);
109
+ if (!response.ok) {
110
+ console.log(`${c.red("🔴")} Failed to fetch template: HTTP ${response.status}`);
111
+ return;
112
+ }
113
+ templateContent = await response.text();
114
+ }
115
+ catch (err) {
116
+ console.log(`${c.red("🔴")} Failed to fetch template: ${err}`);
117
+ return;
118
+ }
119
+ }
120
+ else {
121
+ // Try relative to cwd first, then absolute
122
+ const resolved = existsSync(join(cwd, templateSource))
123
+ ? join(cwd, templateSource)
124
+ : existsSync(templateSource)
125
+ ? templateSource
126
+ : null;
127
+ if (!resolved) {
128
+ console.log(`${c.red("🔴")} Template not found: ${templateSource}`);
129
+ return;
130
+ }
131
+ templateContent = readFileSync(resolved, "utf8");
132
+ }
133
+ let template;
134
+ try {
135
+ template = JSON.parse(templateContent);
136
+ }
137
+ catch {
138
+ console.log(`${c.red("🔴")} Invalid template JSON.`);
139
+ return;
140
+ }
141
+ const targetOS = detectOS();
142
+ let applied = 0;
143
+ section(`Applying template: ${template.name}`);
144
+ console.log(` Source OS: ${template.os} → Target OS: ${targetOS}\n`);
145
+ // 1. CLAUDE.md — append only, never remove existing lines
146
+ if (template.claudeMd) {
147
+ const claudeMdPath = join(cwd, "CLAUDE.md");
148
+ if (existsSync(claudeMdPath)) {
149
+ const existing = readFileSync(claudeMdPath, "utf8");
150
+ const newLines = template.claudeMd
151
+ .split("\n")
152
+ .filter(l => l.trim() && !existing.includes(l.trim()));
153
+ if (newLines.length) {
154
+ writeFileSync(claudeMdPath, existing + "\n\n# From template: " + template.name + "\n" + newLines.join("\n") + "\n", "utf8");
155
+ console.log(` ${c.green("✅")} CLAUDE.md — appended ${newLines.length} line(s)`);
156
+ applied++;
157
+ }
158
+ else {
159
+ console.log(` ${c.dim("⏭")} CLAUDE.md — all content already present`);
160
+ }
161
+ }
162
+ else {
163
+ writeFileSync(claudeMdPath, template.claudeMd, "utf8");
164
+ console.log(` ${c.green("✅")} CLAUDE.md — created`);
165
+ applied++;
166
+ }
167
+ }
168
+ // 2. .mcp.json — merge servers, adapt OS format
169
+ if (template.mcpJson) {
170
+ const mcpPath = join(cwd, ".mcp.json");
171
+ let existing = {};
172
+ if (existsSync(mcpPath)) {
173
+ try {
174
+ existing = JSON.parse(readFileSync(mcpPath, "utf8"));
175
+ }
176
+ catch { /* start fresh */ }
177
+ }
178
+ const existingServers = (existing.mcpServers ?? {});
179
+ const templateServers = ((template.mcpJson).mcpServers ?? {});
180
+ let addedCount = 0;
181
+ for (const [name, config] of Object.entries(templateServers)) {
182
+ if (existingServers[name]) {
183
+ console.log(` ${c.dim("⏭")} MCP server: ${name} — already exists`);
184
+ continue;
185
+ }
186
+ existingServers[name] = adaptMcpForOS(config, template.os, targetOS);
187
+ console.log(` ${c.green("✅")} MCP server: ${name} — added (OS-adapted)`);
188
+ addedCount++;
189
+ }
190
+ if (addedCount > 0) {
191
+ existing.mcpServers = existingServers;
192
+ writeFileSync(mcpPath, JSON.stringify(existing, null, 2), "utf8");
193
+ applied++;
194
+ }
195
+ }
196
+ // 3. settings.json — merge hooks, never write model key
197
+ if (template.settings) {
198
+ const settingsDir = join(cwd, ".claude");
199
+ const settingsPath = join(settingsDir, "settings.json");
200
+ if (!existsSync(settingsDir))
201
+ mkdirSync(settingsDir, { recursive: true });
202
+ let existing = {};
203
+ if (existsSync(settingsPath)) {
204
+ try {
205
+ existing = JSON.parse(readFileSync(settingsPath, "utf8"));
206
+ }
207
+ catch { /* start fresh */ }
208
+ }
209
+ const templateHooks = (template.settings.hooks ?? {});
210
+ const existingHooks = (existing.hooks ?? {});
211
+ let addedHooks = 0;
212
+ for (const [event, hooks] of Object.entries(templateHooks)) {
213
+ if (!existingHooks[event]) {
214
+ existingHooks[event] = hooks;
215
+ addedHooks += Array.isArray(hooks) ? hooks.length : 0;
216
+ console.log(` ${c.green("✅")} Hook: ${event} — added`);
217
+ }
218
+ else {
219
+ console.log(` ${c.dim("⏭")} Hook: ${event} — already exists`);
220
+ }
221
+ }
222
+ if (addedHooks > 0) {
223
+ existing.hooks = existingHooks;
224
+ delete existing["model"]; // Never write model key
225
+ writeFileSync(settingsPath, JSON.stringify(existing, null, 2), "utf8");
226
+ applied++;
227
+ }
228
+ }
229
+ // 4. Skills — skip if same name exists
230
+ for (const skill of template.skills) {
231
+ const skillDir = join(cwd, ".claude", "skills", skill.name);
232
+ const skillPath = join(skillDir, "SKILL.md");
233
+ if (existsSync(skillPath)) {
234
+ console.log(` ${c.dim("⏭")} Skill: ${skill.name} — already exists`);
235
+ continue;
236
+ }
237
+ mkdirSync(skillDir, { recursive: true });
238
+ writeFileSync(skillPath, skill.content, "utf8");
239
+ console.log(` ${c.green("✅")} Skill: ${skill.name} — created`);
240
+ applied++;
241
+ }
242
+ // 5. Commands — skip if same name exists
243
+ for (const cmd of template.commands) {
244
+ const cmdDir = join(cwd, ".claude", "commands");
245
+ const cmdPath = join(cmdDir, `${cmd.name}.md`);
246
+ if (existsSync(cmdPath)) {
247
+ console.log(` ${c.dim("⏭")} Command: ${cmd.name} — already exists`);
248
+ continue;
249
+ }
250
+ if (!existsSync(cmdDir))
251
+ mkdirSync(cmdDir, { recursive: true });
252
+ writeFileSync(cmdPath, cmd.content, "utf8");
253
+ console.log(` ${c.green("✅")} Command: ${cmd.name} — created`);
254
+ applied++;
255
+ }
256
+ console.log(`\n${c.green("✅")} Template "${template.name}" applied — ${applied} item(s) added.`);
257
+ }
258
+ /**
259
+ * Adapt MCP server config for target OS.
260
+ * Templates are stored with the source OS format.
261
+ * On import, commands are auto-converted:
262
+ * Windows → macOS/Linux: cmd /c npx → npx
263
+ * macOS/Linux → Windows: npx → cmd /c npx
264
+ * Path separators and env var syntax are also adapted.
265
+ */
266
+ function adaptMcpForOS(config, sourceOS, targetOS) {
267
+ if (sourceOS === targetOS)
268
+ return config;
269
+ const result = { ...config };
270
+ const cmd = config.command;
271
+ const args = [...(config.args ?? [])];
272
+ if (!cmd)
273
+ return result;
274
+ // Source is Windows → target is macOS/Linux
275
+ if (sourceOS === "Windows" && targetOS !== "Windows") {
276
+ if (cmd === "cmd" && args[0] === "/c") {
277
+ result.command = args[1]; // "npx", "bun", etc.
278
+ result.args = args.slice(2);
279
+ }
280
+ }
281
+ // Source is macOS/Linux → target is Windows
282
+ if (sourceOS !== "Windows" && targetOS === "Windows") {
283
+ if (cmd === "npx" || cmd === "bun" || cmd === "node") {
284
+ result.command = "cmd";
285
+ result.args = ["/c", cmd, ...args];
286
+ }
287
+ }
288
+ return result;
289
+ }
@@ -1,3 +1,4 @@
1
1
  export declare function runInit(opts?: {
2
2
  dryRun?: boolean;
3
+ template?: string;
3
4
  }): Promise<void>;
@@ -4,16 +4,24 @@ import { collectProjectFiles, isEmptyProject } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, } from "../builder.js";
7
- import { c } from "../output.js";
7
+ import { createSnapshot, collectFilesForSnapshot } from "../snapshot.js";
8
+ import { estimateTokens, estimateCost } from "../tokens.js";
9
+ import { c, section } from "../output.js";
8
10
  import { ensureConfig } from "../config.js";
11
+ import { applyTemplate } from "./export.js";
9
12
  function ensureDir(dir) {
10
13
  if (!existsSync(dir))
11
14
  mkdirSync(dir, { recursive: true });
12
15
  }
13
16
  export async function runInit(opts = {}) {
14
17
  const dryRun = opts.dryRun ?? false;
18
+ // Feature H: --template flag — apply a template instead of scanning
19
+ if (opts.template) {
20
+ console.log(`Applying template: ${c.cyan(opts.template)}\n`);
21
+ await applyTemplate(opts.template);
22
+ return;
23
+ }
15
24
  // Auto-generate .claude-setup.json if it doesn't exist
16
- // Developer can edit it anytime to tune token budgets, truncation rules, etc.
17
25
  const configCreated = ensureConfig();
18
26
  if (configCreated) {
19
27
  console.log(`${c.dim("Created .claude-setup.json — edit to tune token budgets and truncation rules")}`);
@@ -22,18 +30,28 @@ export async function runInit(opts = {}) {
22
30
  const collected = await collectProjectFiles(process.cwd(), "deep");
23
31
  if (isEmptyProject(collected)) {
24
32
  const content = buildEmptyProjectCommand();
33
+ // Token tracking
34
+ const tokens = estimateTokens(content);
35
+ const cost = estimateCost(tokens);
25
36
  if (dryRun) {
26
37
  console.log(c.bold("[DRY RUN] Would write:\n"));
27
- console.log(` .claude/commands/stack-init.md (${content.length} chars, ~${Math.ceil(content.length / 4)} tokens)`);
38
+ console.log(` .claude/commands/stack-init.md (${content.length} chars, ~${tokens.toLocaleString()} tokens)`);
28
39
  console.log(`\n${c.dim("--- preview ---")}`);
29
40
  console.log(content.slice(0, 500));
30
41
  if (content.length > 500)
31
42
  console.log(c.dim(`\n... +${content.length - 500} chars`));
43
+ section("Token cost estimate");
44
+ console.log(` ~${tokens.toLocaleString()} input tokens (Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)})`);
32
45
  return;
33
46
  }
34
47
  ensureDir(".claude/commands");
35
48
  writeFileSync(".claude/commands/stack-init.md", content, "utf8");
36
- await updateManifest("init", collected);
49
+ await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
50
+ // Feature A: Create initial snapshot node
51
+ const cwd = process.cwd();
52
+ const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
53
+ const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
54
+ createSnapshot(cwd, "init", snapshotFiles, { summary: "initial setup (empty project)" });
37
55
  console.log(`
38
56
  ${c.green("✅")} New project detected.
39
57
 
@@ -42,20 +60,28 @@ Open Claude Code and run:
42
60
 
43
61
  Claude Code will ask 3 questions, then set up your environment.
44
62
  `);
63
+ section("Token cost");
64
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
65
+ console.log("");
45
66
  return;
46
67
  }
47
68
  // Standard init — atomic steps + orchestrator
48
69
  const steps = buildAtomicSteps(collected, state);
49
70
  const orchestrator = buildOrchestratorCommand(steps);
71
+ // Token tracking — sum all steps
72
+ const totalContent = steps.map(s => s.content).join("\n") + "\n" + orchestrator;
73
+ const tokens = estimateTokens(totalContent);
74
+ const cost = estimateCost(tokens);
50
75
  if (dryRun) {
51
76
  console.log(c.bold("[DRY RUN] Would write:\n"));
52
77
  for (const step of steps) {
53
- const tokens = Math.ceil(step.content.length / 4);
54
- console.log(` .claude/commands/${step.filename} (${step.content.length} chars, ~${tokens} tokens)`);
78
+ const stepTokens = estimateTokens(step.content);
79
+ console.log(` .claude/commands/${step.filename} (${step.content.length} chars, ~${stepTokens.toLocaleString()} tokens)`);
55
80
  }
56
81
  console.log(` .claude/commands/stack-init.md (orchestrator)`);
57
- const totalTokens = steps.reduce((sum, s) => sum + Math.ceil(s.content.length / 4), 0);
58
- console.log(`\n${c.dim(`Total: ~${totalTokens} tokens across ${steps.length} files`)}`);
82
+ console.log(`\n${c.dim(`Total: ~${tokens.toLocaleString()} tokens across ${steps.length} files`)}`);
83
+ section("Token cost estimate");
84
+ console.log(` ~${tokens.toLocaleString()} input tokens (Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)})`);
59
85
  return;
60
86
  }
61
87
  ensureDir(".claude/commands");
@@ -63,11 +89,21 @@ Claude Code will ask 3 questions, then set up your environment.
63
89
  writeFileSync(join(".claude/commands", step.filename), step.content, "utf8");
64
90
  }
65
91
  writeFileSync(".claude/commands/stack-init.md", orchestrator, "utf8");
66
- await updateManifest("init", collected);
92
+ await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
93
+ // Feature A: Create initial snapshot node
94
+ const cwd = process.cwd();
95
+ const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
96
+ const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
97
+ createSnapshot(cwd, "init", snapshotFiles, {
98
+ summary: `${steps.length - 1} atomic steps generated`,
99
+ });
67
100
  console.log(`
68
101
  ${c.green("✅")} Ready. Open Claude Code and run:
69
102
  ${c.cyan("/stack-init")}
70
103
 
71
104
  Runs ${steps.length - 1} atomic steps. If one fails, re-run only that step.
72
105
  `);
106
+ section("Token cost");
107
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
108
+ console.log("");
73
109
  }
@@ -4,7 +4,8 @@ import { collectProjectFiles } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildRemoveCommand } from "../builder.js";
7
- import { c } from "../output.js";
7
+ import { estimateTokens, estimateCost } from "../tokens.js";
8
+ import { c, section } from "../output.js";
8
9
  function ensureDir(dir) {
9
10
  if (!existsSync(dir))
10
11
  mkdirSync(dir, { recursive: true });
@@ -27,8 +28,18 @@ export async function runRemove() {
27
28
  const state = await readState();
28
29
  const collected = await collectProjectFiles(process.cwd(), "configOnly");
29
30
  const content = buildRemoveCommand(userInput, state);
31
+ // Token tracking
32
+ const tokens = estimateTokens(content);
33
+ const cost = estimateCost(tokens);
30
34
  ensureDir(".claude/commands");
31
35
  writeFileSync(".claude/commands/stack-remove.md", content, "utf8");
32
- await updateManifest("remove", collected, { input: userInput });
33
- console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-remove")}\n`);
36
+ await updateManifest("remove", collected, {
37
+ input: userInput,
38
+ estimatedTokens: tokens,
39
+ estimatedCost: cost,
40
+ });
41
+ console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-remove")}`);
42
+ section("Token cost");
43
+ console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
44
+ console.log("");
34
45
  }
@@ -0,0 +1 @@
1
+ export declare function runRestore(): Promise<void>;
@@ -0,0 +1,61 @@
1
+ import { readTimeline, restoreSnapshot } from "../snapshot.js";
2
+ import { c, section } from "../output.js";
3
+ import { createInterface } from "readline";
4
+ async function promptFreeText(question) {
5
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
6
+ return new Promise((resolve) => {
7
+ rl.question(question + " ", (answer) => {
8
+ rl.close();
9
+ resolve(answer.trim());
10
+ });
11
+ });
12
+ }
13
+ export async function runRestore() {
14
+ const cwd = process.cwd();
15
+ const timeline = readTimeline(cwd);
16
+ if (!timeline.nodes.length) {
17
+ console.log(`${c.yellow("⚠️")} No snapshots found. Run ${c.cyan("npx claude-setup sync")} to create one.`);
18
+ return;
19
+ }
20
+ // Display timeline
21
+ section("Snapshot timeline");
22
+ console.log("");
23
+ for (let i = 0; i < timeline.nodes.length; i++) {
24
+ const node = timeline.nodes[i];
25
+ const date = new Date(node.timestamp).toLocaleString();
26
+ const current = i === timeline.nodes.length - 1 ? ` ${c.green("← current")}` : "";
27
+ const connector = i < timeline.nodes.length - 1 ? "──→" : " ";
28
+ const inputStr = node.input ? ` "${node.input}"` : "";
29
+ console.log(` ${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(date)} ${node.summary}${current}`);
30
+ if (i < timeline.nodes.length - 1)
31
+ console.log(` ${c.dim(connector)}`);
32
+ }
33
+ console.log("");
34
+ const input = await promptFreeText("Enter snapshot ID to restore (or 'cancel'):");
35
+ if (!input || input === "cancel") {
36
+ console.log("Cancelled.");
37
+ return;
38
+ }
39
+ const node = timeline.nodes.find(n => n.id === input);
40
+ if (!node) {
41
+ console.log(`${c.red("🔴")} Snapshot "${input}" not found.`);
42
+ return;
43
+ }
44
+ console.log(`\nRestoring to snapshot ${c.cyan(node.id)} (${new Date(node.timestamp).toLocaleString()})...`);
45
+ console.log(`${c.dim("Other snapshots are preserved — you can jump forward or back at any time.")}\n`);
46
+ const result = restoreSnapshot(cwd, input);
47
+ if (result.restored.length) {
48
+ section("Restored files");
49
+ for (const f of result.restored) {
50
+ console.log(` ${c.green("✅")} ${f}`);
51
+ }
52
+ }
53
+ if (result.failed.length) {
54
+ section("Failed to restore");
55
+ for (const f of result.failed) {
56
+ console.log(` ${c.red("🔴")} ${f}`);
57
+ }
58
+ }
59
+ console.log(`\n${c.green("✅")} Restored ${result.restored.length} file(s) to snapshot ${c.cyan(node.id)}.`);
60
+ console.log(`Run ${c.cyan("npx claude-setup sync")} to capture the current state as a new node.`);
61
+ }