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 +6 -0
- package/dist/builder.js +28 -0
- package/dist/commands/add.d.ts +3 -1
- package/dist/commands/add.js +5 -13
- package/dist/commands/init.js +61 -30
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +5 -9
- package/dist/commands/restore.d.ts +4 -1
- package/dist/commands/restore.js +199 -29
- package/dist/commands/sync.js +62 -98
- package/dist/doctor.js +9 -4
- package/dist/index.js +39 -9
- package/dist/marketplace.js +3 -2
- package/dist/snapshot.d.ts +14 -6
- package/dist/snapshot.js +194 -58
- package/dist/tokens.js +2 -2
- package/package.json +1 -1
- package/templates/sync.md +13 -5
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");
|
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
8
|
-
import { c
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
137
|
+
// Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
|
|
85
138
|
const cwd = process.cwd();
|
|
86
|
-
const
|
|
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
|
-
//
|
|
179
|
+
// Create initial snapshot — collectFilesForSnapshot scans all .claude/ automatically
|
|
137
180
|
const cwd = process.cwd();
|
|
138
|
-
const
|
|
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
|
}
|
package/dist/commands/remove.js
CHANGED
|
@@ -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
|
|
8
|
-
import { c
|
|
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
|
}
|
package/dist/commands/restore.js
CHANGED
|
@@ -10,32 +10,187 @@ async function promptFreeText(question) {
|
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
-
|
|
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
|
-
|
|
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 ===
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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}${
|
|
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(
|
|
178
|
+
console.log(` ${c.dim(" ──→")}`);
|
|
35
179
|
}
|
|
36
180
|
console.log("");
|
|
37
|
-
const
|
|
38
|
-
|
|
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("
|
|
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("
|
|
65
|
-
console.log(` ${c.dim("These files exist
|
|
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.
|
|
72
|
-
console.log(`\n${c.yellow("⚠️")} This snapshot captured 0 files —
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
|
87
|
-
const
|
|
88
|
-
const
|
|
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(
|
|
260
|
+
console.log(` ${c.dim(" ──→")}`);
|
|
93
261
|
}
|
|
94
|
-
console.log(
|
|
95
|
-
console.log(`Run ${c.cyan("
|
|
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
|
}
|