claude-setup 1.1.5 → 1.1.7

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/builder.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface FileDiff {
15
15
  deleted: string[];
16
16
  }
17
17
  export declare function buildSyncCommand(diff: FileDiff, collected: CollectedFiles, state: ExistingState): string;
18
+ /**
19
+ * Bootstrap stack-sync.md — installed by init so /stack-sync works immediately.
20
+ * When run, it tells Claude to invoke npx claude-setup sync first (which
21
+ * regenerates this file with the real diff), then re-read and process it.
22
+ */
23
+ export declare function buildBootstrapSync(): string;
18
24
  export declare function buildRemoveCommand(input: string, state: ExistingState): string;
19
25
  export interface AtomicStep {
20
26
  filename: string;
package/dist/builder.js CHANGED
@@ -171,6 +171,34 @@ export function buildSyncCommand(diff, collected, state) {
171
171
  DELETED_FILES: deletedStr,
172
172
  }, "sync");
173
173
  }
174
+ /**
175
+ * Bootstrap stack-sync.md — installed by init so /stack-sync works immediately.
176
+ * When run, it tells Claude to invoke npx claude-setup sync first (which
177
+ * regenerates this file with the real diff), then re-read and process it.
178
+ */
179
+ export function buildBootstrapSync() {
180
+ return `<!-- stack-sync: self-refreshing sync command. Last generated: ${new Date().toISOString().split("T")[0]} -->
181
+
182
+ > **Step 1 — always do this first:** Run \`npx claude-setup sync\` to refresh this file with the latest diff.
183
+ > - If it outputs "No changes since…" → respond "Stack already in sync." and STOP.
184
+ > - Otherwise → re-read \`.claude/commands/stack-sync.md\` (Step 2), then process the diff below (Step 3).
185
+
186
+ ## Changes since last setup
187
+
188
+ ### Added files
189
+ (none — run \`npx claude-setup sync\` to populate)
190
+
191
+ ### Modified files
192
+ (none)
193
+
194
+ ### Deleted files
195
+ (none)
196
+
197
+ ## Your job
198
+
199
+ For EACH changed file, update the Claude Code setup. New source files (routes, services, etc.) MUST be reflected in CLAUDE.md. Config changes may require .mcp.json or settings.json updates. Surgical edits only.
200
+ `;
201
+ }
174
202
  export function buildRemoveCommand(input, state) {
175
203
  const emptyCollected = { configs: {}, source: [], skipped: [] };
176
204
  return applyTemplate("remove.md", emptyCollected, state, { USER_INPUT: input }, "remove");
@@ -1 +1,3 @@
1
- export declare function runAdd(): Promise<void>;
1
+ export declare function runAdd(opts?: {
2
+ input?: string;
3
+ }): Promise<void>;
@@ -4,8 +4,8 @@ import { collectProjectFiles } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
6
  import { buildAddCommand } from "../builder.js";
7
- import { estimateTokens, estimateCost, formatCost } from "../tokens.js";
8
- import { c, section } from "../output.js";
7
+ import { estimateTokens, estimateCost } from "../tokens.js";
8
+ import { c } from "../output.js";
9
9
  function ensureDir(dir) {
10
10
  if (!existsSync(dir))
11
11
  mkdirSync(dir, { recursive: true });
@@ -19,16 +19,13 @@ async function promptFreeText(question) {
19
19
  });
20
20
  });
21
21
  }
22
- // Conservative — only redirect when unambiguously single-file
23
- // False negatives (multi-step for single-file request) are fine
24
- // False positives (redirecting a genuinely multi-file request) are bad
25
22
  function isSingleFileOperation(input) {
26
23
  return (/to \.mcp\.json\s*$/i.test(input) ||
27
24
  /to settings\.json\s*$/i.test(input) ||
28
25
  /to claude\.md\s*$/i.test(input));
29
26
  }
30
- export async function runAdd() {
31
- const userInput = await promptFreeText("What do you want to add to your Claude Code setup?");
27
+ export async function runAdd(opts = {}) {
28
+ const userInput = opts.input ?? await promptFreeText("What do you want to add to your Claude Code setup?");
32
29
  if (!userInput) {
33
30
  console.log("No input provided.");
34
31
  return;
@@ -44,10 +41,8 @@ capabilities that need documentation, MCP servers, skills, and hooks together.
44
41
  return;
45
42
  }
46
43
  const state = await readState();
47
- // add only needs config files — source files are irrelevant and waste tokens
48
44
  const collected = await collectProjectFiles(process.cwd(), "configOnly");
49
45
  const content = buildAddCommand(userInput, collected, state);
50
- // Token tracking
51
46
  const tokens = estimateTokens(content);
52
47
  const cost = estimateCost(tokens);
53
48
  ensureDir(".claude/commands");
@@ -57,8 +52,5 @@ capabilities that need documentation, MCP servers, skills, and hooks together.
57
52
  estimatedTokens: tokens,
58
53
  estimatedCost: cost,
59
54
  });
60
- console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-add")}`);
61
- section("Token cost");
62
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
63
- console.log("");
55
+ console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-add")}\n`);
64
56
  }
@@ -3,9 +3,9 @@ import { join } from "path";
3
3
  import { collectProjectFiles, isEmptyProject } from "../collect.js";
4
4
  import { readState } from "../state.js";
5
5
  import { updateManifest } from "../manifest.js";
6
- import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, } from "../builder.js";
6
+ import { buildEmptyProjectCommand, buildAtomicSteps, buildOrchestratorCommand, buildBootstrapSync, } from "../builder.js";
7
7
  import { createSnapshot, collectFilesForSnapshot } from "../snapshot.js";
8
- import { estimateTokens, estimateCost, formatCost, getTokenHookScript, formatRealCostSummary } from "../tokens.js";
8
+ import { estimateTokens, estimateCost, formatCost, getTokenHookScript } from "../tokens.js";
9
9
  import { c, section } from "../output.js";
10
10
  import { ensureConfig } from "../config.js";
11
11
  import { applyTemplate } from "./export.js";
@@ -13,6 +13,57 @@ function ensureDir(dir) {
13
13
  if (!existsSync(dir))
14
14
  mkdirSync(dir, { recursive: true });
15
15
  }
16
+ /** Install bootstrap slash commands so all operations work from within Claude Code */
17
+ function installBootstrapCommands(dir) {
18
+ const cmds = {
19
+ "stack-add.md": `<!-- stack-add: bootstrap — regenerated by npx claude-setup add -->
20
+
21
+ > **Step 1:** Ask the user: "What do you want to add to your Claude Code setup?"
22
+ > **Step 2:** After the user responds, run: \`npx claude-setup add "<their exact answer>"\`
23
+ > **Step 3:** Re-read this file (\`.claude/commands/stack-add.md\`) — it was just regenerated with full marketplace instructions.
24
+ > **Step 4:** Follow the instructions in the updated file.
25
+ `,
26
+ "stack-status.md": `<!-- stack-status: show project state -->
27
+
28
+ Run this command and display the output to the user:
29
+ \`\`\`bash
30
+ npx claude-setup status
31
+ \`\`\`
32
+ No further action needed — the output IS the status.
33
+ `,
34
+ "stack-doctor.md": `<!-- stack-doctor: validate environment -->
35
+
36
+ > **Step 1:** Run: \`npx claude-setup doctor\`
37
+ > **Step 2:** Show the output to the user.
38
+ > **Step 3:** If any issues are found (lines with red or warning markers), ask: "Want me to auto-fix these?"
39
+ > **Step 4:** If yes, run: \`npx claude-setup doctor --fix\`
40
+ `,
41
+ "stack-restore.md": `<!-- stack-restore: time-travel to any snapshot -->
42
+
43
+ > **Step 1:** Run: \`npx claude-setup restore --list\` to show the snapshot timeline.
44
+ > **Step 2:** Show the timeline to the user and ask: "Which snapshot do you want to restore to? (enter the ID)"
45
+ > **Step 3:** After the user picks an ID, run: \`npx claude-setup restore --id "<snapshot-id>"\`
46
+ > **Step 4:** Show the result.
47
+ `,
48
+ "stack-remove.md": `<!-- stack-remove: bootstrap — regenerated by npx claude-setup remove -->
49
+
50
+ > **Step 1:** Ask the user: "What do you want to remove from your Claude Code setup?"
51
+ > **Step 2:** After the user responds, run: \`npx claude-setup remove "<their exact answer>"\`
52
+ > **Step 3:** Re-read this file (\`.claude/commands/stack-remove.md\`) — it was just regenerated.
53
+ > **Step 4:** Follow the instructions in the updated file.
54
+ `,
55
+ };
56
+ for (const [filename, content] of Object.entries(cmds)) {
57
+ const filepath = join(dir, filename);
58
+ // Don't overwrite if already populated with real content (e.g., after an add/remove run)
59
+ if (existsSync(filepath)) {
60
+ const existing = readFileSync(filepath, "utf8");
61
+ if (!existing.includes("bootstrap"))
62
+ continue;
63
+ }
64
+ writeFileSync(filepath, content, "utf8");
65
+ }
66
+ }
16
67
  function installTokenHook(cwd = process.cwd()) {
17
68
  // Write the hook script
18
69
  const hooksDir = join(cwd, ".claude", "hooks");
@@ -79,12 +130,13 @@ export async function runInit(opts = {}) {
79
130
  }
80
131
  ensureDir(".claude/commands");
81
132
  writeFileSync(".claude/commands/stack-init.md", content, "utf8");
133
+ writeFileSync(".claude/commands/stack-sync.md", buildBootstrapSync(), "utf8");
134
+ installBootstrapCommands(".claude/commands");
82
135
  await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
83
136
  installTokenHook();
84
- // Feature A: Create initial snapshot node
137
+ // Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
85
138
  const cwd = process.cwd();
86
- const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
87
- const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
139
+ const snapshotFiles = collectFilesForSnapshot(cwd, Object.keys(collected.configs));
88
140
  createSnapshot(cwd, "init", snapshotFiles, { summary: "initial setup (empty project)" });
89
141
  console.log(`
90
142
  ${c.green("✅")} New project detected.
@@ -94,17 +146,6 @@ Open Claude Code and run:
94
146
 
95
147
  Claude Code will ask 3 questions, then set up your environment.
96
148
  `);
97
- section("Token cost");
98
- const realSummary1 = formatRealCostSummary(cwd);
99
- if (realSummary1) {
100
- console.log(realSummary1);
101
- console.log(` ${c.dim(`This command estimate: ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`)}`);
102
- }
103
- else {
104
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
105
- console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
106
- }
107
- console.log("");
108
149
  return;
109
150
  }
110
151
  // Standard init — atomic steps + orchestrator
@@ -131,12 +172,13 @@ Claude Code will ask 3 questions, then set up your environment.
131
172
  writeFileSync(join(".claude/commands", step.filename), step.content, "utf8");
132
173
  }
133
174
  writeFileSync(".claude/commands/stack-init.md", orchestrator, "utf8");
175
+ writeFileSync(".claude/commands/stack-sync.md", buildBootstrapSync(), "utf8");
176
+ installBootstrapCommands(".claude/commands");
134
177
  await updateManifest("init", collected, { estimatedTokens: tokens, estimatedCost: cost });
135
178
  installTokenHook();
136
- // Feature A: Create initial snapshot node
179
+ // Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
137
180
  const cwd = process.cwd();
138
- const allPaths = [...Object.keys(collected.configs), ...collected.source.map(s => s.path)];
139
- const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
181
+ const snapshotFiles = collectFilesForSnapshot(cwd, Object.keys(collected.configs));
140
182
  createSnapshot(cwd, "init", snapshotFiles, {
141
183
  summary: `${steps.length - 1} atomic steps generated`,
142
184
  });
@@ -146,15 +188,4 @@ ${c.green("✅")} Ready. Open Claude Code and run:
146
188
 
147
189
  Runs ${steps.length - 1} atomic steps. If one fails, re-run only that step.
148
190
  `);
149
- section("Token cost");
150
- const realSummary2 = formatRealCostSummary(cwd);
151
- if (realSummary2) {
152
- console.log(realSummary2);
153
- console.log(` ${c.dim(`This command estimate: ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`)}`);
154
- }
155
- else {
156
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
157
- console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
158
- }
159
- console.log("");
160
191
  }
@@ -1 +1,3 @@
1
- export declare function runRemove(): Promise<void>;
1
+ export declare function runRemove(opts?: {
2
+ input?: string;
3
+ }): Promise<void>;
@@ -4,8 +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 { estimateTokens, estimateCost, formatCost } from "../tokens.js";
8
- import { c, section } from "../output.js";
7
+ import { estimateTokens, estimateCost } from "../tokens.js";
8
+ import { c } from "../output.js";
9
9
  function ensureDir(dir) {
10
10
  if (!existsSync(dir))
11
11
  mkdirSync(dir, { recursive: true });
@@ -19,8 +19,8 @@ async function promptFreeText(question) {
19
19
  });
20
20
  });
21
21
  }
22
- export async function runRemove() {
23
- const userInput = await promptFreeText("What do you want to remove from your Claude Code setup?");
22
+ export async function runRemove(opts = {}) {
23
+ const userInput = opts.input ?? await promptFreeText("What do you want to remove from your Claude Code setup?");
24
24
  if (!userInput) {
25
25
  console.log("No input provided.");
26
26
  return;
@@ -28,7 +28,6 @@ export async function runRemove() {
28
28
  const state = await readState();
29
29
  const collected = await collectProjectFiles(process.cwd(), "configOnly");
30
30
  const content = buildRemoveCommand(userInput, state);
31
- // Token tracking
32
31
  const tokens = estimateTokens(content);
33
32
  const cost = estimateCost(tokens);
34
33
  ensureDir(".claude/commands");
@@ -38,8 +37,5 @@ export async function runRemove() {
38
37
  estimatedTokens: tokens,
39
38
  estimatedCost: cost,
40
39
  });
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(`${formatCost(cost)}`)})`);
44
- console.log("");
40
+ console.log(`\n${c.green("✅")} Ready. Open Claude Code and run:\n ${c.cyan("/stack-remove")}\n`);
45
41
  }
@@ -1 +1,4 @@
1
- export declare function runRestore(): Promise<void>;
1
+ export declare function runRestore(opts?: {
2
+ list?: boolean;
3
+ id?: string;
4
+ }): Promise<void>;
@@ -10,32 +10,187 @@ async function promptFreeText(question) {
10
10
  });
11
11
  });
12
12
  }
13
- export async function runRestore() {
13
+ async function promptSelectSnapshot(items) {
14
+ const allItems = [
15
+ ...items,
16
+ { id: "__custom__", label: c.dim("Type snapshot ID manually…") },
17
+ { id: "__cancel__", label: c.dim("Cancel") },
18
+ ];
19
+ // Non-TTY fallback: numbered list
20
+ if (!process.stdin.isTTY) {
21
+ for (let i = 0; i < allItems.length; i++) {
22
+ console.log(` [${i + 1}] ${allItems[i].label}`);
23
+ }
24
+ const raw = await promptFreeText("\nEnter number (or 'cancel'):");
25
+ if (!raw || raw === "cancel")
26
+ return null;
27
+ const num = parseInt(raw, 10);
28
+ if (!isNaN(num) && num >= 1 && num <= allItems.length) {
29
+ const val = allItems[num - 1].id;
30
+ if (val === "__cancel__")
31
+ return null;
32
+ if (val === "__custom__")
33
+ return (await promptFreeText("Enter snapshot ID:")) || null;
34
+ return val;
35
+ }
36
+ return raw;
37
+ }
38
+ return new Promise((resolve) => {
39
+ let selectedIndex = 0;
40
+ let lineCount = 0;
41
+ const clearLines = () => {
42
+ if (lineCount > 0)
43
+ process.stdout.write(`\x1b[${lineCount}A\x1b[0J`);
44
+ };
45
+ const render = () => {
46
+ clearLines();
47
+ const lines = [];
48
+ for (let i = 0; i < allItems.length; i++) {
49
+ const isSelected = i === selectedIndex;
50
+ const marker = isSelected ? c.cyan("❯") : " ";
51
+ const text = isSelected ? c.bold(allItems[i].label) : allItems[i].label;
52
+ lines.push(` ${marker} ${text}`);
53
+ }
54
+ lines.push(``);
55
+ lines.push(` ${c.dim("↑/↓ navigate · Enter select · Ctrl+C cancel")}`);
56
+ process.stdout.write(lines.join("\n") + "\n");
57
+ lineCount = lines.length;
58
+ };
59
+ let onKey;
60
+ const cleanup = () => {
61
+ process.stdin.removeListener("data", onKey);
62
+ try {
63
+ process.stdin.setRawMode(false);
64
+ }
65
+ catch { }
66
+ process.stdin.pause();
67
+ };
68
+ onKey = (key) => {
69
+ if (key === "\u0003") {
70
+ cleanup();
71
+ process.stdout.write("\n");
72
+ process.exit(0);
73
+ }
74
+ else if (key === "\u001b[A" || key === "\u001bOA") {
75
+ selectedIndex = Math.max(0, selectedIndex - 1);
76
+ render();
77
+ }
78
+ else if (key === "\u001b[B" || key === "\u001bOB") {
79
+ selectedIndex = Math.min(allItems.length - 1, selectedIndex + 1);
80
+ render();
81
+ }
82
+ else if (key === "\r" || key === "\n") {
83
+ const chosen = allItems[selectedIndex];
84
+ cleanup();
85
+ process.stdout.write("\n");
86
+ if (chosen.id === "__cancel__") {
87
+ resolve(null);
88
+ }
89
+ else if (chosen.id === "__custom__") {
90
+ promptFreeText("Enter snapshot ID:").then(id => resolve(id || null));
91
+ }
92
+ else {
93
+ resolve(chosen.id);
94
+ }
95
+ }
96
+ };
97
+ process.stdin.setRawMode(true);
98
+ process.stdin.resume();
99
+ process.stdin.setEncoding("utf8");
100
+ process.stdin.on("data", onKey);
101
+ render();
102
+ });
103
+ }
104
+ export async function runRestore(opts = {}) {
14
105
  const cwd = process.cwd();
15
106
  const timeline = readTimeline(cwd);
16
107
  if (!timeline.nodes.length) {
17
108
  console.log(`${c.yellow("⚠️")} No snapshots found. Run ${c.cyan("npx claude-setup sync")} to create one.`);
18
109
  return;
19
110
  }
20
- // Display timeline
111
+ const restoredIdx = timeline.restoredTo
112
+ ? timeline.nodes.findIndex(n => n.id === timeline.restoredTo)
113
+ : timeline.nodes.length - 1;
114
+ const latestIdx = timeline.nodes.length - 1;
115
+ // --list: just print timeline and exit (for use by Claude Code slash commands)
116
+ if (opts.list) {
117
+ for (let i = 0; i < timeline.nodes.length; i++) {
118
+ const node = timeline.nodes[i];
119
+ const date = new Date(node.timestamp).toLocaleString();
120
+ const isLatest = i === latestIdx;
121
+ const isHere = i === restoredIdx && timeline.restoredTo;
122
+ const marker = (isHere && !isLatest) ? " ← current"
123
+ : (isLatest && !timeline.restoredTo) ? " ← current"
124
+ : "";
125
+ const inputStr = node.input ? ` "${node.input}"` : "";
126
+ console.log(`${node.id} ${node.command}${inputStr} ${date} ${node.summary}${marker}`);
127
+ }
128
+ return;
129
+ }
130
+ // --id: restore directly to a specific snapshot (for use by Claude Code slash commands)
131
+ if (opts.id) {
132
+ const node = timeline.nodes.find(n => n.id === opts.id);
133
+ if (!node) {
134
+ console.log(`${c.red("🔴")} Snapshot "${opts.id}" not found.`);
135
+ return;
136
+ }
137
+ console.log(`Restoring to snapshot ${c.cyan(node.id)}...`);
138
+ const result = restoreSnapshot(cwd, opts.id, timeline);
139
+ updateRestoredNode(cwd, opts.id);
140
+ const parts = [];
141
+ if (result.restored.length)
142
+ parts.push(`${result.restored.length} restored`);
143
+ if (result.deleted.length)
144
+ parts.push(`${result.deleted.length} removed`);
145
+ if (parts.length) {
146
+ console.log(`${c.green("✅")} ${parts.join(", ")} → project at snapshot ${c.cyan(node.id)}.`);
147
+ }
148
+ else {
149
+ console.log(`${c.yellow("⚠️")} Snapshot captured 0 files — nothing to restore.`);
150
+ }
151
+ return;
152
+ }
153
+ // Interactive mode — display timeline
21
154
  section("Snapshot timeline");
22
- console.log("");
23
- const restoredTo = timeline.restoredTo;
155
+ console.log(` ${c.dim("All snapshots are always preserved — you can go back or forward freely.")}\n`);
24
156
  for (let i = 0; i < timeline.nodes.length; i++) {
25
157
  const node = timeline.nodes[i];
26
158
  const date = new Date(node.timestamp).toLocaleString();
27
- const isLatest = i === timeline.nodes.length - 1;
28
- const isRestored = restoredTo === node.id && !isLatest;
29
- const current = isLatest ? ` ${c.green("← current")}` : isRestored ? ` ${c.cyan("← restored here")}` : "";
30
- const connector = i < timeline.nodes.length - 1 ? "──→" : " ";
159
+ const isLatest = i === latestIdx;
160
+ const isHere = i === restoredIdx && timeline.restoredTo;
161
+ let marker = "";
162
+ let prefix = " ";
163
+ if (isHere && !isLatest) {
164
+ marker = ` ${c.cyan("◀ you are here")}`;
165
+ prefix = c.cyan("▶ ");
166
+ }
167
+ else if (isLatest && !timeline.restoredTo) {
168
+ marker = ` ${c.green("◀ you are here")}`;
169
+ prefix = c.green("▶ ");
170
+ }
171
+ else if (i > restoredIdx) {
172
+ marker = ` ${c.dim("(future — reachable)")}`;
173
+ prefix = c.dim(" ");
174
+ }
31
175
  const inputStr = node.input ? ` "${node.input}"` : "";
32
- console.log(` ${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(date)} ${node.summary}${current}`);
176
+ console.log(` ${prefix}${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(date)} ${node.summary}${marker}`);
33
177
  if (i < timeline.nodes.length - 1)
34
- console.log(` ${c.dim(connector)}`);
178
+ console.log(` ${c.dim(" ──→")}`);
35
179
  }
36
180
  console.log("");
37
- const input = await promptFreeText("Enter snapshot ID to restore (or 'cancel'):");
38
- if (!input || input === "cancel") {
181
+ const items = timeline.nodes.map((node, i) => {
182
+ const date = new Date(node.timestamp).toLocaleString();
183
+ const isLatest = i === latestIdx;
184
+ const isHere = i === restoredIdx && timeline.restoredTo;
185
+ const tag = isHere && !isLatest ? ` ${c.cyan("← you are here")}`
186
+ : isLatest && !timeline.restoredTo ? ` ${c.green("← you are here")}`
187
+ : i > restoredIdx ? ` ${c.dim("(future)")}`
188
+ : "";
189
+ return { id: node.id, label: `${node.id} ${node.command} ${c.dim(date)} ${node.summary}${tag}` };
190
+ });
191
+ console.log("Select a snapshot to restore to:\n");
192
+ const input = await promptSelectSnapshot(items);
193
+ if (!input) {
39
194
  console.log("Cancelled.");
40
195
  return;
41
196
  }
@@ -45,7 +200,7 @@ export async function runRestore() {
45
200
  return;
46
201
  }
47
202
  console.log(`\nRestoring to snapshot ${c.cyan(node.id)} (${new Date(node.timestamp).toLocaleString()})...`);
48
- console.log(`${c.dim("Other snapshots are preserved you can jump forward or back at any time.")}\n`);
203
+ console.log(`${c.dim("Config files will be rewritten to their state at this snapshot. Other files are untouched.")}\n`);
49
204
  const result = restoreSnapshot(cwd, input, timeline);
50
205
  updateRestoredNode(cwd, input);
51
206
  if (result.restored.length) {
@@ -54,6 +209,12 @@ export async function runRestore() {
54
209
  console.log(` ${c.green("✅")} ${f}`);
55
210
  }
56
211
  }
212
+ if (result.deleted.length) {
213
+ section(`Removed (added after this snapshot — ${result.deleted.length} files)`);
214
+ for (const f of result.deleted) {
215
+ console.log(` ${c.red("🗑")} ${f}`);
216
+ }
217
+ }
57
218
  if (result.failed.length) {
58
219
  section("Failed to restore");
59
220
  for (const f of result.failed) {
@@ -61,36 +222,45 @@ export async function runRestore() {
61
222
  }
62
223
  }
63
224
  if (result.stale.length) {
64
- section("Files not in this snapshot (may be stale)");
65
- console.log(` ${c.dim("These files exist now but were not part of the restored snapshot:")}`);
225
+ section("Could not delete (permission error)");
226
+ console.log(` ${c.dim("These files exist but couldn't be removed delete manually:")}`);
66
227
  for (const f of result.stale) {
67
228
  console.log(` ${c.yellow("⚠️")} ${f}`);
68
229
  }
69
- console.log(` ${c.dim("To fully reset, delete these manually or run sync to update the snapshot.")}`);
70
230
  }
71
- if (result.restored.length === 0 && result.stale.length === 0) {
72
- console.log(`\n${c.yellow("⚠️")} This snapshot captured 0 files — the project was empty at that point.`);
73
- console.log(` Files added since this snapshot have been left in place.`);
231
+ if (result.restored.length === 0 && result.deleted.length === 0) {
232
+ console.log(`\n${c.yellow("⚠️")} This snapshot captured 0 files — nothing to restore.`);
74
233
  }
75
234
  else {
76
- console.log(`\n${c.green("✅")} Restored ${result.restored.length} file(s) to snapshot ${c.cyan(node.id)}.`);
235
+ const parts = [];
236
+ if (result.restored.length)
237
+ parts.push(`${result.restored.length} restored`);
238
+ if (result.deleted.length)
239
+ parts.push(`${result.deleted.length} removed`);
240
+ console.log(`\n${c.green("✅")} ${parts.join(", ")} → project is now at snapshot ${c.cyan(node.id)}.`);
77
241
  }
78
- // Re-read and display updated timeline showing the restored position
242
+ // Re-read and display updated timeline
79
243
  const updatedTimeline = readTimeline(cwd);
244
+ const updatedRestoredIdx = updatedTimeline.restoredTo
245
+ ? updatedTimeline.nodes.findIndex(n => n.id === updatedTimeline.restoredTo)
246
+ : updatedTimeline.nodes.length - 1;
80
247
  console.log("");
81
- section("Updated timeline");
248
+ section("Timeline — you can restore to any node at any time");
82
249
  console.log("");
83
250
  for (let i = 0; i < updatedTimeline.nodes.length; i++) {
84
251
  const n = updatedTimeline.nodes[i];
85
252
  const date = new Date(n.timestamp).toLocaleString();
86
- const isRestored = updatedTimeline.restoredTo === n.id;
87
- const marker = isRestored ? ` ${c.cyan("← restored here")}` : "";
88
- const connector = i < updatedTimeline.nodes.length - 1 ? "──→" : " ";
253
+ const isHere = i === updatedRestoredIdx;
254
+ const isFuture = i > updatedRestoredIdx;
255
+ const marker = isHere ? ` ${c.cyan("◀ you are here")}` : isFuture ? ` ${c.dim("(future — reachable)")}` : "";
256
+ const prefix = isHere ? c.cyan("▶ ") : isFuture ? c.dim(" ") : " ";
89
257
  const inputStr = n.input ? ` "${n.input}"` : "";
90
- console.log(` ${c.cyan(n.id)} ${n.command}${inputStr} ${c.dim(date)} ${n.summary}${marker}`);
258
+ console.log(` ${prefix}${c.cyan(n.id)} ${n.command}${inputStr} ${c.dim(date)} ${n.summary}${marker}`);
91
259
  if (i < updatedTimeline.nodes.length - 1)
92
- console.log(` ${c.dim(connector)}`);
260
+ console.log(` ${c.dim(" ──→")}`);
93
261
  }
94
- console.log(`\nTimeline position updated → snapshot ${c.cyan(node.id)}`);
95
- console.log(`Run ${c.cyan("npx claude-setup sync")} to capture the current state as a new node.\n`);
262
+ console.log(``);
263
+ console.log(` ${c.green("▶")} Run ${c.cyan("claude")} in this directory to start working from this point.`);
264
+ console.log(` ${c.dim("Run npx claude-setup sync to save the current state as a new snapshot.")}`);
265
+ console.log(``);
96
266
  }