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.
- package/README.md +111 -46
- package/dist/builder.js +94 -29
- package/dist/commands/add.js +14 -3
- package/dist/commands/compare.d.ts +1 -0
- package/dist/commands/compare.js +84 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/export.d.ts +25 -0
- package/dist/commands/export.js +289 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +45 -9
- package/dist/commands/remove.js +14 -3
- package/dist/commands/restore.d.ts +1 -0
- package/dist/commands/restore.js +61 -0
- package/dist/commands/status.js +189 -16
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +71 -8
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +307 -59
- package/dist/index.js +28 -3
- package/dist/manifest.d.ts +12 -0
- package/dist/manifest.js +2 -0
- package/dist/marketplace.d.ts +21 -0
- package/dist/marketplace.js +159 -0
- package/dist/os.js +5 -0
- package/dist/snapshot.d.ts +71 -0
- package/dist/snapshot.js +195 -0
- package/dist/tokens.d.ts +58 -0
- package/dist/tokens.js +132 -0
- package/package.json +49 -49
- package/templates/add.md +122 -3
- package/templates/init-empty.md +58 -1
- package/templates/init.md +53 -16
- package/templates/remove.md +27 -2
- package/templates/sync.md +20 -1
|
@@ -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
|
+
}
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -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 {
|
|
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, ~${
|
|
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
|
|
54
|
-
console.log(` .claude/commands/${step.filename} (${step.content.length} chars, ~${
|
|
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
|
-
|
|
58
|
-
|
|
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
|
}
|
package/dist/commands/remove.js
CHANGED
|
@@ -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 {
|
|
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, {
|
|
33
|
-
|
|
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
|
+
}
|