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.
@@ -1,12 +1,11 @@
1
1
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
- import { glob } from "glob";
4
3
  import { collectProjectFiles } from "../collect.js";
5
4
  import { readState } from "../state.js";
6
5
  import { readManifest, sha256, updateManifest } from "../manifest.js";
7
6
  import { buildSyncCommand } from "../builder.js";
8
- import { createSnapshot, collectFilesForSnapshot } from "../snapshot.js";
9
- import { estimateTokens, estimateCost, formatCost, formatTokenReport, buildTokenEstimate, generateHints, getTokenHookScript, formatRealCostSummary } from "../tokens.js";
7
+ import { createSnapshot, collectFilesForSnapshot, readTimeline, readNodeData } from "../snapshot.js";
8
+ import { estimateTokens, estimateCost, formatTokenReport, buildTokenEstimate, getTokenHookScript } from "../tokens.js";
10
9
  import { loadConfig } from "../config.js";
11
10
  import { c, section } from "../output.js";
12
11
  function ensureDir(dir) {
@@ -14,12 +13,10 @@ function ensureDir(dir) {
14
13
  mkdirSync(dir, { recursive: true });
15
14
  }
16
15
  function installTokenHook(cwd = process.cwd()) {
17
- // Write the hook script
18
16
  const hooksDir = join(cwd, ".claude", "hooks");
19
17
  if (!existsSync(hooksDir))
20
18
  mkdirSync(hooksDir, { recursive: true });
21
19
  writeFileSync(join(hooksDir, "track-tokens.cjs"), getTokenHookScript(), "utf8");
22
- // Merge Stop hook into settings.json
23
20
  const settingsPath = join(cwd, ".claude", "settings.json");
24
21
  let settings = {};
25
22
  if (existsSync(settingsPath)) {
@@ -31,13 +28,11 @@ function installTokenHook(cwd = process.cwd()) {
31
28
  const hookEntry = {
32
29
  hooks: [{ type: "command", command: "node \".claude/hooks/track-tokens.cjs\"" }]
33
30
  };
34
- // Merge into settings.hooks.Stop
35
31
  if (!settings.hooks)
36
32
  settings.hooks = {};
37
33
  const hooks = settings.hooks;
38
34
  if (!Array.isArray(hooks.Stop))
39
35
  hooks.Stop = [];
40
- // Only add if not already present
41
36
  const alreadyPresent = hooks.Stop.some(e => Array.isArray(e.hooks) && e.hooks.some((h) => typeof h.command === "string" && h.command.includes("track-tokens")));
42
37
  if (!alreadyPresent) {
43
38
  hooks.Stop.push(hookEntry);
@@ -51,7 +46,11 @@ function truncate(content, maxChars) {
51
46
  return content;
52
47
  return content.slice(0, maxChars) + "\n[... truncated for sync diff]";
53
48
  }
54
- function computeDiff(snapshot, collected, cwd) {
49
+ /**
50
+ * Legacy diff — compares manifest hashes against collected files.
51
+ * Only used when no snapshot data is available (e.g., old projects).
52
+ */
53
+ function computeLegacyDiff(snapshot, collected, cwd) {
55
54
  const current = {
56
55
  ...collected.configs,
57
56
  ...Object.fromEntries(collected.source.map(f => [f.path, f.content])),
@@ -60,7 +59,6 @@ function computeDiff(snapshot, collected, cwd) {
60
59
  const changed = [];
61
60
  const deleted = [];
62
61
  for (const [path, content] of Object.entries(current)) {
63
- // Skip virtual keys — they're not real files
64
62
  if (path === "__digest__")
65
63
  continue;
66
64
  const hash = sha256(content);
@@ -71,57 +69,58 @@ function computeDiff(snapshot, collected, cwd) {
71
69
  changed.push({ path, current: truncate(content, 2000) });
72
70
  }
73
71
  }
74
- // BUG 1 FIX: Verify file existence on disk before reporting deletions.
75
- // Files may appear "deleted" because they weren't in the current collection set
76
- // (different collect mode, or CLI-managed files like CLAUDE.md/settings.json).
77
- // If the file still exists on disk, it was "modified outside the CLI", not deleted.
78
72
  for (const path of Object.keys(snapshot)) {
79
- // Skip virtual keys
80
73
  if (path === "__digest__")
81
74
  continue;
82
75
  if (!current[path]) {
83
- // Check if file actually exists on disk
84
76
  const fullPath = join(cwd, path);
85
77
  if (existsSync(fullPath)) {
86
- // File exists but wasn't in our collection — it was modified outside CLI
87
- // Read it and check if its hash changed
88
78
  try {
89
79
  const diskContent = readFileSync(fullPath, "utf8");
90
80
  const diskHash = sha256(diskContent);
91
81
  if (snapshot[path] !== diskHash) {
92
82
  changed.push({ path, current: truncate(diskContent, 2000) });
93
83
  }
94
- // If hash matches, file is unchanged — don't report anything
95
84
  }
96
85
  catch {
97
- // Can't read — treat as changed
98
86
  changed.push({ path, current: "[file exists but could not be read]" });
99
87
  }
100
88
  }
101
89
  else {
102
- // File genuinely does not exist on disk — truly deleted
103
90
  deleted.push(path);
104
91
  }
105
92
  }
106
93
  }
107
94
  return { added, changed, deleted };
108
95
  }
109
- async function collectClaudeInternalFiles(cwd) {
110
- const files = [];
111
- try {
112
- const skillFiles = await glob(".claude/skills/**/*.md", { cwd, posix: true });
113
- const allCmds = await glob(".claude/commands/*.md", { cwd, posix: true });
114
- const commandFiles = allCmds.filter(f => !f.split("/").pop().startsWith("stack-"));
115
- for (const f of [...skillFiles, ...commandFiles]) {
116
- try {
117
- const content = readFileSync(join(cwd, f), "utf8");
118
- files.push({ path: f, content });
96
+ /**
97
+ * Full-scan diff — compares every file on disk against a reference snapshot.
98
+ * This is the authoritative diff: catches ALL file changes, no sampling.
99
+ */
100
+ function computeFullDiff(currentFiles, referenceFiles) {
101
+ const added = [];
102
+ const changed = [];
103
+ const deleted = [];
104
+ const currentPathSet = new Set();
105
+ for (const f of currentFiles) {
106
+ currentPathSet.add(f.path);
107
+ if (!referenceFiles[f.path]) {
108
+ added.push({ path: f.path, content: truncate(f.content, 2000) });
109
+ }
110
+ else {
111
+ const currentHash = sha256(f.content);
112
+ const refHash = sha256(referenceFiles[f.path]);
113
+ if (currentHash !== refHash) {
114
+ changed.push({ path: f.path, current: truncate(f.content, 2000) });
119
115
  }
120
- catch { /* skip unreadable */ }
121
116
  }
122
117
  }
123
- catch { /* skip */ }
124
- return files;
118
+ for (const path of Object.keys(referenceFiles)) {
119
+ if (!currentPathSet.has(path)) {
120
+ deleted.push(path);
121
+ }
122
+ }
123
+ return { added, changed, deleted };
125
124
  }
126
125
  export async function runSync(opts = {}) {
127
126
  const dryRun = opts.dryRun ?? false;
@@ -133,11 +132,10 @@ export async function runSync(opts = {}) {
133
132
  const lastRun = manifest.runs.at(-1);
134
133
  const cwd = process.cwd();
135
134
  const config = loadConfig(cwd);
136
- // Apply --budget override if provided
137
135
  if (opts.budget) {
138
136
  config.tokenBudget.sync = opts.budget;
139
137
  }
140
- // --- Out-of-band edit detection ---
138
+ // --- Out-of-band edit detection (early warning) ---
141
139
  const managedFiles = [
142
140
  { label: "CLAUDE.md", path: join(cwd, "CLAUDE.md"), snapshotKey: "CLAUDE.md" },
143
141
  { label: ".mcp.json", path: join(cwd, ".mcp.json"), snapshotKey: ".mcp.json" },
@@ -161,39 +159,39 @@ export async function runSync(opts = {}) {
161
159
  }
162
160
  if (oobDetected)
163
161
  console.log("");
164
- const collected = await collectProjectFiles(cwd, "normal");
165
- const diff = computeDiff(lastRun.snapshot, collected, cwd);
166
- // Bug 3 fix: Also detect changes inside .claude/ (skills, commands)
167
- const claudeInternalFiles = await collectClaudeInternalFiles(cwd);
168
- for (const f of claudeInternalFiles) {
169
- const hash = sha256(f.content);
170
- if (!lastRun.snapshot[f.path]) {
171
- diff.added.push({ path: f.path, content: truncate(f.content, 2000) });
172
- }
173
- else if (lastRun.snapshot[f.path] !== hash) {
174
- diff.changed.push({ path: f.path, current: truncate(f.content, 2000) });
175
- }
162
+ // --- Full project scan (single scan, used for both diff and snapshot) ---
163
+ const currentFiles = collectFilesForSnapshot(cwd, []);
164
+ // --- Determine reference snapshot ---
165
+ // After restore: compare against the restored-to snapshot
166
+ // Normal: compare against the latest snapshot
167
+ const timeline = readTimeline(cwd);
168
+ const referenceNodeId = timeline.restoredTo ?? timeline.nodes.at(-1)?.id;
169
+ let referenceFiles = null;
170
+ if (referenceNodeId) {
171
+ const data = readNodeData(cwd, referenceNodeId);
172
+ if (data)
173
+ referenceFiles = data.files;
174
+ }
175
+ // --- Compute diff ---
176
+ let diff;
177
+ if (referenceFiles) {
178
+ // Full-scan comparison (authoritative — catches ALL changes)
179
+ diff = computeFullDiff(currentFiles, referenceFiles);
176
180
  }
177
- // Also detect deleted .claude/ files (were in snapshot but no longer exist)
178
- for (const path of Object.keys(lastRun.snapshot)) {
179
- if ((path.startsWith(".claude/skills/") || (path.startsWith(".claude/commands/") && !path.split("/").pop().startsWith("stack-"))) && !path.includes("__digest__")) {
180
- const alreadyInDiff = diff.added.some(f => f.path === path) ||
181
- diff.changed.some(f => f.path === path) ||
182
- diff.deleted.includes(path);
183
- if (!alreadyInDiff && !claudeInternalFiles.some(f => f.path === path)) {
184
- if (!existsSync(join(cwd, path))) {
185
- diff.deleted.push(path);
186
- }
187
- }
188
- }
181
+ else {
182
+ // Legacy fallback no snapshot data available
183
+ const collected = await collectProjectFiles(cwd, "normal");
184
+ diff = computeLegacyDiff(lastRun.snapshot, collected, cwd);
189
185
  }
190
- if (!diff.added.length && !diff.changed.length && !diff.deleted.length && !oobDetected) {
186
+ const hasChanges = diff.added.length > 0 || diff.changed.length > 0 || diff.deleted.length > 0 || oobDetected;
187
+ if (!hasChanges) {
191
188
  console.log(`${c.green("✅")} No changes since ${c.dim(lastRun.at)}. Setup is current.`);
192
189
  return;
193
190
  }
191
+ // --- Build sync command (needs collected project context for template) ---
192
+ const collected = await collectProjectFiles(cwd, "normal");
194
193
  const state = await readState();
195
194
  const content = buildSyncCommand(diff, collected, state);
196
- // Token tracking
197
195
  const tokens = estimateTokens(content);
198
196
  const cost = estimateCost(tokens);
199
197
  if (dryRun) {
@@ -214,57 +212,23 @@ export async function runSync(opts = {}) {
214
212
  console.log(` ${f}`);
215
213
  }
216
214
  console.log(`\n Would write: .claude/commands/stack-sync.md (~${tokens.toLocaleString()} tokens)`);
217
- // Token cost display
218
215
  section("Token cost estimate");
219
216
  const estimate = buildTokenEstimate([{ label: "sync command", content }]);
220
217
  console.log(formatTokenReport(estimate));
221
218
  return;
222
219
  }
223
- // Add .claude/ internal files to snapshot
224
- for (const f of claudeInternalFiles) {
225
- collected.configs[f.path] = f.content;
226
- }
227
220
  ensureDir(".claude/commands");
228
221
  writeFileSync(".claude/commands/stack-sync.md", content, "utf8");
229
222
  await updateManifest("sync", collected, { estimatedTokens: tokens, estimatedCost: cost });
230
223
  installTokenHook();
231
- // Feature A: Create snapshot node
232
- const allPaths = [
233
- ...Object.keys(collected.configs),
234
- ...collected.source.map(s => s.path),
235
- ...claudeInternalFiles.map(f => f.path),
236
- ];
237
- const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
238
- const changeCount = diff.added.length + diff.changed.length + diff.deleted.length;
239
- createSnapshot(cwd, "sync", snapshotFiles, {
224
+ // Create snapshot — reuse the full scan data (no second scan needed)
225
+ createSnapshot(cwd, "sync", currentFiles, {
240
226
  summary: `+${diff.added.length} added, ~${diff.changed.length} modified, -${diff.deleted.length} deleted`,
241
227
  });
242
228
  console.log(`
243
229
  Changes since ${c.dim(lastRun.at)}:
244
230
  ${c.green(`+${diff.added.length}`)} added ${c.yellow(`~${diff.changed.length}`)} modified ${c.red(`-${diff.deleted.length}`)} deleted
245
231
 
246
- ${c.green("✅")} Ready. Open Claude Code and run:
247
- ${c.cyan("/stack-sync")}
232
+ ${c.green("✅")} Run ${c.cyan("/stack-sync")} in Claude Code to apply.
248
233
  `);
249
- // Token cost display
250
- section("Token cost");
251
- const realSummary = formatRealCostSummary(cwd);
252
- if (realSummary) {
253
- console.log(realSummary);
254
- console.log(` ${c.dim(`This command estimate: ~${tokens.toLocaleString()} input tokens (${formatCost(cost)})`)}`);
255
- }
256
- else {
257
- console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`${formatCost(cost)}`)})`);
258
- console.log(` ${c.dim("Estimates only — real costs tracked after first Claude Code session")}`);
259
- }
260
- // Optimization hints
261
- const runs = manifest.runs.map(r => ({ command: r.command, estimatedTokens: r.estimatedTokens }));
262
- const hints = generateHints(runs, tokens, config.tokenBudget.sync);
263
- if (hints.length) {
264
- section("Optimization hints");
265
- for (const hint of hints) {
266
- console.log(` ${c.yellow("💡")} ${hint}`);
267
- }
268
- }
269
- console.log("");
270
234
  }
package/dist/doctor.js CHANGED
@@ -375,9 +375,14 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
375
375
  counts.warnings++;
376
376
  }
377
377
  else if (result.status === "error") {
378
+ const stderr = result.stderr ?? "";
379
+ const isFileMissing = stderr.includes("Cannot find module") || stderr.includes("MODULE_NOT_FOUND");
380
+ const hint = isFileMissing
381
+ ? `\n Hint: hook file not found — run ${c.cyan("npx claude-setup init")} to reinstall it.`
382
+ : "";
378
383
  statusLine("⚠️ ", label, c.yellow(`FAIL (exit ${result.exitCode}, ${result.timeMs}ms)\n` +
379
- ` Command: ${hook.command.slice(0, 50)}\n` +
380
- ` ${result.stderr ? `stderr: ${result.stderr.slice(0, 100)}` : ""}`));
384
+ ` Command: ${hook.command.slice(0, 60)}\n` +
385
+ ` ${stderr ? `stderr: ${stderr.slice(0, 200)}` : ""}${hint}`));
381
386
  counts.warnings++;
382
387
  }
383
388
  else if (result.status === "permission") {
@@ -409,13 +414,13 @@ export async function runDoctor(verbose = false, fix = false, testHooks = false)
409
414
  counts.healthy++;
410
415
  }
411
416
  else if (isInTemplate) {
412
- statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime.\n` +
417
+ statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime and won't appear in /mcp.\n` +
413
418
  ` Documented in .env.example but not loaded into environment.\n` +
414
419
  ` Fix: set ${v} in your shell or .env file, then restart Claude Code.`));
415
420
  counts.critical++;
416
421
  }
417
422
  else {
418
- statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime.\n` +
423
+ statusLine("🔴", `\${${v}}`, c.red(`NOT SET — MCP server will fail at runtime and won't appear in /mcp.\n` +
419
424
  ` Missing from both environment and .env.example.\n` +
420
425
  ` Fix: add ${v} to .env.example and set its value in your shell or .env file.`));
421
426
  counts.critical++;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { createInterface } from "readline";
3
4
  import { createRequire } from "module";
4
5
  import { runInit } from "./commands/init.js";
5
6
  import { runAdd } from "./commands/add.js";
@@ -10,6 +11,7 @@ import { runRemove } from "./commands/remove.js";
10
11
  import { runRestore } from "./commands/restore.js";
11
12
  import { runCompare } from "./commands/compare.js";
12
13
  import { runExport } from "./commands/export.js";
14
+ import { c } from "./output.js";
13
15
  const require = createRequire(import.meta.url);
14
16
  const pkg = require("../package.json");
15
17
  const program = new Command();
@@ -24,9 +26,9 @@ program
24
26
  .option("--template <path>", "Apply a template instead of scanning (local path or URL)")
25
27
  .action((opts) => runInit({ dryRun: opts.dryRun, template: opts.template }));
26
28
  program
27
- .command("add")
29
+ .command("add [input...]")
28
30
  .description("Add a multi-file capability")
29
- .action(runAdd);
31
+ .action((input) => runAdd({ input: input?.length ? input.join(" ") : undefined }));
30
32
  program
31
33
  .command("sync")
32
34
  .description("Update setup after project changes")
@@ -49,23 +51,51 @@ program
49
51
  testHooks: opts.testHooks,
50
52
  }));
51
53
  program
52
- .command("remove")
54
+ .command("remove [input...]")
53
55
  .description("Remove a capability cleanly")
54
- .action(runRemove);
55
- // Feature A: Time-travel snapshot commands
56
+ .action((input) => runRemove({ input: input?.length ? input.join(" ") : undefined }));
56
57
  program
57
58
  .command("restore")
58
59
  .description("Jump to any snapshot node, restore files to that state")
59
- .action(runRestore);
60
+ .option("--list", "Show snapshot timeline without prompting")
61
+ .option("--id <snapshotId>", "Restore directly to a specific snapshot ID")
62
+ .action((opts) => runRestore({ list: opts.list, id: opts.id }));
60
63
  program
61
64
  .command("compare")
62
65
  .description("Diff between any two snapshot nodes to see what changed")
63
66
  .action(runCompare);
64
- // Feature H: Config template export
65
67
  program
66
68
  .command("export")
67
69
  .description("Save current project config as a reusable template")
68
70
  .action(runExport);
69
- // Default action when no command given
70
- program.action(() => runInit({}));
71
+ // Default action — interactive menu when no command given
72
+ program.action(async () => {
73
+ const choices = [
74
+ { key: "1", label: "init", desc: "Full project setup", run: () => runInit({}) },
75
+ { key: "2", label: "add", desc: "Add a capability", run: () => runAdd({}) },
76
+ { key: "3", label: "sync", desc: "Update after changes", run: () => runSync({}) },
77
+ { key: "4", label: "status", desc: "Show current state", run: () => runStatus() },
78
+ { key: "5", label: "doctor", desc: "Validate environment", run: () => runDoctorCommand({}) },
79
+ { key: "6", label: "restore", desc: "Time-travel to snapshot", run: () => runRestore({}) },
80
+ { key: "7", label: "compare", desc: "Diff between snapshots", run: () => runCompare() },
81
+ { key: "8", label: "remove", desc: "Remove a capability", run: () => runRemove({}) },
82
+ { key: "9", label: "export", desc: "Save as template", run: () => runExport() },
83
+ ];
84
+ console.log(`\n${c.bold("Claude Setup")} ${c.dim(`v${pkg.version}`)}\n`);
85
+ for (const ch of choices) {
86
+ console.log(` ${c.cyan(ch.key)} ${ch.label.padEnd(10)} ${c.dim(ch.desc)}`);
87
+ }
88
+ console.log("");
89
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
90
+ const answer = await new Promise(resolve => {
91
+ rl.question(` ${c.bold("Choose (1-9):")} `, a => { rl.close(); resolve(a.trim()); });
92
+ });
93
+ const choice = choices.find(ch => ch.key === answer || ch.label === answer.toLowerCase());
94
+ if (!choice) {
95
+ console.log(`\n Invalid choice. Run ${c.cyan("npx claude-setup <command>")} or pick 1-9.\n`);
96
+ return;
97
+ }
98
+ console.log("");
99
+ await choice.run();
100
+ });
71
101
  program.parse();
@@ -138,11 +138,12 @@ export function buildMarketplaceInstructions(input) {
138
138
  lines.push(``);
139
139
  lines.push(`NOTE: /plugin marketplace add FAILS for this repo (one entry has broken schema).`);
140
140
  lines.push(`Use the DIRECT FETCH approach below instead — no marketplace add needed.`);
141
+ lines.push(`If the curl command fails or returns an error, skip to STEP 3 immediately — do NOT stop.`);
141
142
  lines.push(``);
142
143
  lines.push(`**2a. Fetch catalog and find matching plugin:**`);
143
144
  lines.push(`\`\`\`bash`);
144
145
  lines.push(`curl -s "${MARKETPLACE_CATALOG_URL}" \\`);
145
- lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));const q='${categoryFilter}';const r=d.plugins.filter(p=>(!q||p.category.includes(q))&&p.name&&p.source).slice(0,5).map(p=>({name:p.name,source:p.source,desc:p.description}));console.log(JSON.stringify(r,null,2));"`);
146
+ lines.push(` | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));const q='${categoryFilter}';const r=d.plugins.filter(p=>(q===''||p.category.includes(q))&&p.name&&p.source).slice(0,10).map(p=>({name:p.name,source:p.source,desc:p.description}));console.log(JSON.stringify(r,null,2));"`);
146
147
  lines.push(`\`\`\``);
147
148
  lines.push(``);
148
149
  lines.push(`**2b. Pick the best match — get its source path (e.g. \`./plugins/productivity/my-skill\`)**`);
@@ -152,7 +153,7 @@ export function buildMarketplaceInstructions(input) {
152
153
  lines.push(`# Replace PLUGIN_SOURCE_PATH with value from step 2b (e.g. plugins/productivity/my-skill)`);
153
154
  lines.push(`PLUGIN_SOURCE_PATH="plugins/productivity/my-skill"`);
154
155
  lines.push(`curl -s "https://api.github.com/repos/${MARKETPLACE_REPO}/contents/\${PLUGIN_SOURCE_PATH}/skills" \\`);
155
- lines.push(` | node -e "const a=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(a.map(x=>x.name).join('\\n'));"`);
156
+ lines.push(` | node -e "const a=JSON.parse(require('fs').readFileSync(0,'utf8'));console.log(a.map(x=>x.name).join('\\n'));"`);
156
157
  lines.push(`\`\`\``);
157
158
  lines.push(``);
158
159
  lines.push(`**2d. For each skill listed, download and install it:**`);
@@ -19,6 +19,7 @@ export interface SnapshotNode {
19
19
  input?: string;
20
20
  changedFiles: string[];
21
21
  summary: string;
22
+ fullSnapshot?: boolean;
22
23
  }
23
24
  export interface SnapshotTimeline {
24
25
  nodes: SnapshotNode[];
@@ -41,9 +42,14 @@ export declare function createSnapshot(cwd: string, command: string, changedFile
41
42
  summary?: string;
42
43
  }): SnapshotNode;
43
44
  /**
44
- * Build the cumulative file state at a given node by accumulating
45
- * all files from node 0 through the target node. Later nodes override
46
- * earlier ones (last-write-wins), giving the full state at that point.
45
+ * Build the complete file state at a given node.
46
+ *
47
+ * Full snapshots (fullSnapshot: true) store the entire project state used directly.
48
+ * Legacy delta snapshots accumulate from node 0 to target (last-write-wins).
49
+ *
50
+ * Why the distinction matters: with delta snapshots, if a file was deleted between A→B,
51
+ * it would wrongly appear in cumulative state at B (still present from A). Full snapshots
52
+ * avoid this because the target node's data IS the complete truth at that point.
47
53
  */
48
54
  export declare function buildCumulativeState(cwd: string, nodeId: string, timeline: SnapshotTimeline): Record<string, string> | null;
49
55
  /**
@@ -56,6 +62,7 @@ export declare function buildCumulativeState(cwd: string, nodeId: string, timeli
56
62
  export declare function restoreSnapshot(cwd: string, nodeId: string, timeline?: SnapshotTimeline): {
57
63
  restored: string[];
58
64
  failed: string[];
65
+ deleted: string[];
59
66
  stale: string[];
60
67
  };
61
68
  /**
@@ -76,10 +83,11 @@ export declare function compareSnapshots(cwd: string, nodeIdA: string, nodeIdB:
76
83
  identical: string[];
77
84
  };
78
85
  /**
79
- * Collect current file contents for snapshot.
80
- * Reads tracked files + CLI-managed files from disk.
86
+ * Collect ALL project files for snapshot — full git-like coverage.
87
+ * Respects .gitignore + hard exclusions (node_modules, .git, binaries, .env).
88
+ * The trackedPaths param is kept for API compat but ignored.
81
89
  */
82
- export declare function collectFilesForSnapshot(cwd: string, trackedPaths: string[]): Array<{
90
+ export declare function collectFilesForSnapshot(cwd: string, _trackedPaths: string[]): Array<{
83
91
  path: string;
84
92
  content: string;
85
93
  }>;