claude-setup 1.1.2 → 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 +113 -71
- package/dist/builder.js +202 -42
- 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 +100 -10
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +355 -46
- package/dist/index.js +28 -3
- package/dist/manifest.d.ts +12 -0
- package/dist/manifest.js +14 -0
- package/dist/marketplace.d.ts +21 -0
- package/dist/marketplace.js +159 -0
- package/dist/os.d.ts +6 -1
- package/dist/os.js +22 -3
- package/dist/snapshot.d.ts +71 -0
- package/dist/snapshot.js +195 -0
- package/dist/state.js +5 -1
- 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 +48 -2
- package/templates/sync.md +20 -1
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { basename, join } from "path";
|
|
1
3
|
import { readManifest } from "../manifest.js";
|
|
2
4
|
import { readState } from "../state.js";
|
|
3
5
|
import { detectOS } from "../os.js";
|
|
6
|
+
import { readTimeline } from "../snapshot.js";
|
|
7
|
+
import { computeCumulativeStats, formatCost } from "../tokens.js";
|
|
4
8
|
import { c, statusLine, section } from "../output.js";
|
|
5
9
|
function safeJsonParse(content) {
|
|
6
10
|
try {
|
|
@@ -23,10 +27,13 @@ export async function runStatus() {
|
|
|
23
27
|
// --- Header ---
|
|
24
28
|
console.log(c.bold("status") + ` — ${new Date().toISOString().split("T")[0]}\n`);
|
|
25
29
|
// --- Project info ---
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
console.log(`
|
|
30
|
+
// BUG 2 FIX: Use directory name or package manifest name, not language
|
|
31
|
+
const projectName = getProjectName();
|
|
32
|
+
const language = inferLanguage(state);
|
|
33
|
+
console.log(`Project : ${projectName}`);
|
|
34
|
+
console.log(`Language : ${language}`);
|
|
35
|
+
console.log(`OS : ${os}`);
|
|
36
|
+
console.log(`Version : claude-setup v${version}`);
|
|
30
37
|
// --- Setup files ---
|
|
31
38
|
section("Setup files");
|
|
32
39
|
// CLAUDE.md
|
|
@@ -50,14 +57,7 @@ export async function runStatus() {
|
|
|
50
57
|
// settings.json
|
|
51
58
|
if (state.settings.exists && state.settings.content) {
|
|
52
59
|
const settings = safeJsonParse(state.settings.content);
|
|
53
|
-
|
|
54
|
-
if (settings) {
|
|
55
|
-
for (const key of ["PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Notification", "Stop", "SubagentStop"]) {
|
|
56
|
-
const hooks = settings[key];
|
|
57
|
-
if (Array.isArray(hooks))
|
|
58
|
-
hookCount += hooks.length;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
60
|
+
const hookCount = countHooks(settings);
|
|
61
61
|
statusLine("✅", "settings.json", `${hookCount} hook(s)`);
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
@@ -67,11 +67,64 @@ export async function runStatus() {
|
|
|
67
67
|
console.log(` Skills : ${state.skills.length ? state.skills.map(s => s.split("/").at(-2) ?? s).join(", ") : "none"}`);
|
|
68
68
|
console.log(` Commands : ${state.commands.length ? state.commands.map(s => s.split("/").pop()?.replace(".md", "") ?? s).join(", ") : "none"}`);
|
|
69
69
|
console.log(` Workflows : ${state.workflows.length ? state.workflows.map(s => s.split("/").pop() ?? s).join(", ") : "none"}`);
|
|
70
|
+
// --- Feature A/B: Snapshot timeline ---
|
|
71
|
+
const cwd = process.cwd();
|
|
72
|
+
const timeline = readTimeline(cwd);
|
|
73
|
+
if (timeline.nodes.length > 0) {
|
|
74
|
+
section("Snapshot timeline");
|
|
75
|
+
const displayNodes = timeline.nodes.slice(-8); // Show last 8
|
|
76
|
+
if (timeline.nodes.length > 8) {
|
|
77
|
+
console.log(` ${c.dim(`... +${timeline.nodes.length - 8} older snapshots`)}`);
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < displayNodes.length; i++) {
|
|
80
|
+
const node = displayNodes[i];
|
|
81
|
+
const date = new Date(node.timestamp).toLocaleDateString();
|
|
82
|
+
const time = new Date(node.timestamp).toLocaleTimeString();
|
|
83
|
+
const isLatest = i === displayNodes.length - 1;
|
|
84
|
+
const marker = isLatest ? ` ${c.green("← current")}` : "";
|
|
85
|
+
const inputStr = node.input ? ` "${node.input}"` : "";
|
|
86
|
+
console.log(` ${c.cyan(node.id)} ${node.command}${inputStr} ${c.dim(`${date} ${time}`)} ${node.summary}${marker}`);
|
|
87
|
+
}
|
|
88
|
+
console.log(`\n ${c.dim("Use")} ${c.cyan("npx claude-setup restore")} ${c.dim("to jump to any snapshot")}`);
|
|
89
|
+
console.log(` ${c.dim("Use")} ${c.cyan("npx claude-setup compare")} ${c.dim("to diff two snapshots")}`);
|
|
90
|
+
}
|
|
91
|
+
// --- Feature I: Token usage stats ---
|
|
92
|
+
const runsWithTokens = manifest.runs.filter(r => r.estimatedTokens !== undefined);
|
|
93
|
+
if (runsWithTokens.length > 0) {
|
|
94
|
+
section("Token usage");
|
|
95
|
+
const stats = computeCumulativeStats(manifest.runs);
|
|
96
|
+
console.log(` Total tokens : ~${stats.totalTokens.toLocaleString()} across ${stats.runCount} run(s)`);
|
|
97
|
+
console.log(` Total cost : ${formatCost(stats.totalCost)}`);
|
|
98
|
+
// Average by command type
|
|
99
|
+
const avgEntries = Object.entries(stats.avgByCommand);
|
|
100
|
+
if (avgEntries.length > 0) {
|
|
101
|
+
console.log(` Avg by type :`);
|
|
102
|
+
for (const [cmd, avg] of avgEntries) {
|
|
103
|
+
console.log(` ${cmd}: ~${avg.toLocaleString()} tokens/run`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Cost trend (last 3 vs previous 3)
|
|
107
|
+
if (runsWithTokens.length >= 6) {
|
|
108
|
+
const recent3 = runsWithTokens.slice(-3);
|
|
109
|
+
const prev3 = runsWithTokens.slice(-6, -3);
|
|
110
|
+
const recentAvg = recent3.reduce((s, r) => s + (r.estimatedTokens ?? 0), 0) / 3;
|
|
111
|
+
const prevAvg = prev3.reduce((s, r) => s + (r.estimatedTokens ?? 0), 0) / 3;
|
|
112
|
+
const change = ((recentAvg - prevAvg) / prevAvg) * 100;
|
|
113
|
+
if (Math.abs(change) > 10) {
|
|
114
|
+
const trend = change > 0 ? c.yellow(`↑ +${change.toFixed(0)}%`) : c.green(`↓ ${change.toFixed(0)}%`);
|
|
115
|
+
console.log(` Trend : ${trend} (recent vs previous)`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(` Trend : ${c.green("→ stable")}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
70
122
|
// --- Run history ---
|
|
71
123
|
section("Run history (last 5)");
|
|
72
124
|
for (const r of manifest.runs.slice(-5)) {
|
|
73
125
|
const inputStr = r.input ? ` — "${r.input}"` : "";
|
|
74
|
-
|
|
126
|
+
const tokenStr = r.estimatedTokens ? ` (${r.estimatedTokens.toLocaleString()} tokens)` : "";
|
|
127
|
+
console.log(` ${c.dim(r.at)} ${r.command}${inputStr}${tokenStr}`);
|
|
75
128
|
}
|
|
76
129
|
// --- Health hint ---
|
|
77
130
|
const hint = getHealthHint(manifest, state);
|
|
@@ -87,9 +140,130 @@ export async function runStatus() {
|
|
|
87
140
|
}
|
|
88
141
|
console.log("");
|
|
89
142
|
}
|
|
90
|
-
|
|
143
|
+
/**
|
|
144
|
+
* BUG 3 FIX: Count hooks across all event types, handling both formats:
|
|
145
|
+
* - Correct format: { "hooks": { "PostToolUse": [{ "matcher": "...", "hooks": [{ "type": "command", "command": "..." }] }] } }
|
|
146
|
+
* - Legacy/flat format: { "PostToolUse": [{ "command": "...", "args": [...] }] }
|
|
147
|
+
*/
|
|
148
|
+
function countHooks(settings) {
|
|
149
|
+
if (!settings)
|
|
150
|
+
return 0;
|
|
151
|
+
let count = 0;
|
|
152
|
+
const HOOK_EVENTS = [
|
|
153
|
+
"PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "SessionStart",
|
|
154
|
+
"Notification", "SubagentStart", "SubagentStop", "UserPromptSubmit",
|
|
155
|
+
"PermissionRequest", "ConfigChange", "InstructionsLoaded", "TaskCompleted",
|
|
156
|
+
"TeammateIdle", "StopFailure", "SessionEnd",
|
|
157
|
+
"PreCompact", "PostCompact", "WorktreeCreate", "WorktreeRemove",
|
|
158
|
+
"Elicitation", "ElicitationResult",
|
|
159
|
+
];
|
|
160
|
+
// Check inside "hooks" key first (correct Claude Code format)
|
|
161
|
+
const hooksObj = settings["hooks"];
|
|
162
|
+
if (hooksObj && typeof hooksObj === "object") {
|
|
163
|
+
for (const event of HOOK_EVENTS) {
|
|
164
|
+
const eventHooks = hooksObj[event];
|
|
165
|
+
if (Array.isArray(eventHooks)) {
|
|
166
|
+
for (const entry of eventHooks) {
|
|
167
|
+
if (typeof entry === "object" && entry !== null) {
|
|
168
|
+
const e = entry;
|
|
169
|
+
if (Array.isArray(e.hooks)) {
|
|
170
|
+
count += e.hooks.length;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
count++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return count;
|
|
180
|
+
}
|
|
181
|
+
// Fallback: check top-level keys (legacy flat format)
|
|
182
|
+
for (const event of HOOK_EVENTS) {
|
|
183
|
+
const hooks = settings[event];
|
|
184
|
+
if (Array.isArray(hooks)) {
|
|
185
|
+
for (const entry of hooks) {
|
|
186
|
+
if (typeof entry === "object" && entry !== null) {
|
|
187
|
+
const e = entry;
|
|
188
|
+
if (Array.isArray(e.hooks)) {
|
|
189
|
+
count += e.hooks.length;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
count++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return count;
|
|
199
|
+
}
|
|
200
|
+
/** BUG 2 FIX: Get project name from package manifest or directory name */
|
|
201
|
+
function getProjectName() {
|
|
202
|
+
const cwd = process.cwd();
|
|
203
|
+
// Try package.json name
|
|
204
|
+
try {
|
|
205
|
+
const pkgPath = join(cwd, "package.json");
|
|
206
|
+
if (existsSync(pkgPath)) {
|
|
207
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
208
|
+
if (pkg.name)
|
|
209
|
+
return pkg.name;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch { /* skip */ }
|
|
213
|
+
// Try pom.xml artifactId
|
|
214
|
+
try {
|
|
215
|
+
const pomPath = join(cwd, "pom.xml");
|
|
216
|
+
if (existsSync(pomPath)) {
|
|
217
|
+
const pom = readFileSync(pomPath, "utf8");
|
|
218
|
+
const match = pom.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
219
|
+
if (match)
|
|
220
|
+
return match[1];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { /* skip */ }
|
|
224
|
+
// Try Cargo.toml name
|
|
225
|
+
try {
|
|
226
|
+
const cargoPath = join(cwd, "Cargo.toml");
|
|
227
|
+
if (existsSync(cargoPath)) {
|
|
228
|
+
const cargo = readFileSync(cargoPath, "utf8");
|
|
229
|
+
const match = cargo.match(/^name\s*=\s*"([^"]+)"/m);
|
|
230
|
+
if (match)
|
|
231
|
+
return match[1];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch { /* skip */ }
|
|
235
|
+
// Try pyproject.toml name
|
|
236
|
+
try {
|
|
237
|
+
const pyPath = join(cwd, "pyproject.toml");
|
|
238
|
+
if (existsSync(pyPath)) {
|
|
239
|
+
const py = readFileSync(pyPath, "utf8");
|
|
240
|
+
const match = py.match(/^name\s*=\s*"([^"]+)"/m);
|
|
241
|
+
if (match)
|
|
242
|
+
return match[1];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { /* skip */ }
|
|
246
|
+
// Fallback: directory name
|
|
247
|
+
return basename(cwd);
|
|
248
|
+
}
|
|
249
|
+
/** Detect language/runtime as a separate field */
|
|
250
|
+
function inferLanguage(state) {
|
|
251
|
+
const cwd = process.cwd();
|
|
252
|
+
if (existsSync(join(cwd, "package.json")))
|
|
253
|
+
return "Node.js / TypeScript";
|
|
254
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "requirements.txt")))
|
|
255
|
+
return "Python";
|
|
256
|
+
if (existsSync(join(cwd, "go.mod")))
|
|
257
|
+
return "Go";
|
|
258
|
+
if (existsSync(join(cwd, "Cargo.toml")))
|
|
259
|
+
return "Rust";
|
|
260
|
+
if (existsSync(join(cwd, "pom.xml")) || existsSync(join(cwd, "build.gradle")))
|
|
261
|
+
return "Java";
|
|
262
|
+
if (existsSync(join(cwd, "Gemfile")))
|
|
263
|
+
return "Ruby";
|
|
264
|
+
if (existsSync(join(cwd, "composer.json")))
|
|
265
|
+
return "PHP";
|
|
91
266
|
if (state.claudeMd.content) {
|
|
92
|
-
// Try to infer from CLAUDE.md content
|
|
93
267
|
const content = state.claudeMd.content.toLowerCase();
|
|
94
268
|
if (content.includes("typescript") || content.includes("node"))
|
|
95
269
|
return "Node.js / TypeScript";
|
|
@@ -111,7 +285,6 @@ function getHealthHint(manifest, _state) {
|
|
|
111
285
|
if (!last)
|
|
112
286
|
return `${c.yellow("⚠️")} No runs recorded. Run: ${c.cyan("npx claude-setup init")}`;
|
|
113
287
|
const daysSince = Math.floor((Date.now() - new Date(last.at).getTime()) / (1000 * 60 * 60 * 24));
|
|
114
|
-
// Check for recent deletions
|
|
115
288
|
if (last.command === "sync") {
|
|
116
289
|
const snapshot = last.snapshot;
|
|
117
290
|
const deletionCount = Object.keys(snapshot).filter(k => k.startsWith("[deleted]")).length;
|
package/dist/commands/sync.d.ts
CHANGED
package/dist/commands/sync.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
2
3
|
import { collectProjectFiles } from "../collect.js";
|
|
3
4
|
import { readState } from "../state.js";
|
|
4
5
|
import { readManifest, sha256, updateManifest } from "../manifest.js";
|
|
5
6
|
import { buildSyncCommand } from "../builder.js";
|
|
6
|
-
import {
|
|
7
|
+
import { createSnapshot, collectFilesForSnapshot } from "../snapshot.js";
|
|
8
|
+
import { estimateTokens, estimateCost, formatTokenReport, buildTokenEstimate, generateHints } from "../tokens.js";
|
|
9
|
+
import { loadConfig } from "../config.js";
|
|
10
|
+
import { c, section } from "../output.js";
|
|
7
11
|
function ensureDir(dir) {
|
|
8
12
|
if (!existsSync(dir))
|
|
9
13
|
mkdirSync(dir, { recursive: true });
|
|
@@ -13,7 +17,7 @@ function truncate(content, maxChars) {
|
|
|
13
17
|
return content;
|
|
14
18
|
return content.slice(0, maxChars) + "\n[... truncated for sync diff]";
|
|
15
19
|
}
|
|
16
|
-
function computeDiff(snapshot, collected) {
|
|
20
|
+
function computeDiff(snapshot, collected, cwd) {
|
|
17
21
|
const current = {
|
|
18
22
|
...collected.configs,
|
|
19
23
|
...Object.fromEntries(collected.source.map(f => [f.path, f.content])),
|
|
@@ -33,12 +37,38 @@ function computeDiff(snapshot, collected) {
|
|
|
33
37
|
changed.push({ path, current: truncate(content, 2000) });
|
|
34
38
|
}
|
|
35
39
|
}
|
|
40
|
+
// BUG 1 FIX: Verify file existence on disk before reporting deletions.
|
|
41
|
+
// Files may appear "deleted" because they weren't in the current collection set
|
|
42
|
+
// (different collect mode, or CLI-managed files like CLAUDE.md/settings.json).
|
|
43
|
+
// If the file still exists on disk, it was "modified outside the CLI", not deleted.
|
|
36
44
|
for (const path of Object.keys(snapshot)) {
|
|
37
45
|
// Skip virtual keys
|
|
38
46
|
if (path === "__digest__")
|
|
39
47
|
continue;
|
|
40
|
-
if (!current[path])
|
|
41
|
-
|
|
48
|
+
if (!current[path]) {
|
|
49
|
+
// Check if file actually exists on disk
|
|
50
|
+
const fullPath = join(cwd, path);
|
|
51
|
+
if (existsSync(fullPath)) {
|
|
52
|
+
// File exists but wasn't in our collection — it was modified outside CLI
|
|
53
|
+
// Read it and check if its hash changed
|
|
54
|
+
try {
|
|
55
|
+
const diskContent = readFileSync(fullPath, "utf8");
|
|
56
|
+
const diskHash = sha256(diskContent);
|
|
57
|
+
if (snapshot[path] !== diskHash) {
|
|
58
|
+
changed.push({ path, current: truncate(diskContent, 2000) });
|
|
59
|
+
}
|
|
60
|
+
// If hash matches, file is unchanged — don't report anything
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Can't read — treat as changed
|
|
64
|
+
changed.push({ path, current: "[file exists but could not be read]" });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// File genuinely does not exist on disk — truly deleted
|
|
69
|
+
deleted.push(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
42
72
|
}
|
|
43
73
|
return { added, changed, deleted };
|
|
44
74
|
}
|
|
@@ -50,14 +80,47 @@ export async function runSync(opts = {}) {
|
|
|
50
80
|
return;
|
|
51
81
|
}
|
|
52
82
|
const lastRun = manifest.runs.at(-1);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
83
|
+
const cwd = process.cwd();
|
|
84
|
+
const config = loadConfig(cwd);
|
|
85
|
+
// Apply --budget override if provided
|
|
86
|
+
if (opts.budget) {
|
|
87
|
+
config.tokenBudget.sync = opts.budget;
|
|
88
|
+
}
|
|
89
|
+
// --- Out-of-band edit detection ---
|
|
90
|
+
const managedFiles = [
|
|
91
|
+
{ label: "CLAUDE.md", path: join(cwd, "CLAUDE.md"), snapshotKey: "CLAUDE.md" },
|
|
92
|
+
{ label: ".mcp.json", path: join(cwd, ".mcp.json"), snapshotKey: ".mcp.json" },
|
|
93
|
+
{ label: "settings.json", path: join(cwd, ".claude", "settings.json"), snapshotKey: ".claude/settings.json" },
|
|
94
|
+
];
|
|
95
|
+
let oobDetected = false;
|
|
96
|
+
for (const mf of managedFiles) {
|
|
97
|
+
if (!existsSync(mf.path))
|
|
98
|
+
continue;
|
|
99
|
+
const currentContent = readFileSync(mf.path, "utf8");
|
|
100
|
+
const currentHash = sha256(currentContent);
|
|
101
|
+
const snapshotHash = lastRun.snapshot[mf.snapshotKey];
|
|
102
|
+
if (snapshotHash && currentHash !== snapshotHash) {
|
|
103
|
+
if (!oobDetected) {
|
|
104
|
+
oobDetected = true;
|
|
105
|
+
console.log("");
|
|
106
|
+
}
|
|
107
|
+
console.log(`${c.yellow("⚠️")} OUT-OF-BAND EDIT — ${mf.label} was modified outside the CLI`);
|
|
108
|
+
console.log(` Re-snapshotting. Run ${c.cyan("npx claude-setup doctor")} to validate the new state.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (oobDetected)
|
|
112
|
+
console.log("");
|
|
113
|
+
const collected = await collectProjectFiles(cwd, "normal");
|
|
114
|
+
const diff = computeDiff(lastRun.snapshot, collected, cwd);
|
|
115
|
+
if (!diff.added.length && !diff.changed.length && !diff.deleted.length && !oobDetected) {
|
|
56
116
|
console.log(`${c.green("✅")} No changes since ${c.dim(lastRun.at)}. Setup is current.`);
|
|
57
117
|
return;
|
|
58
118
|
}
|
|
59
119
|
const state = await readState();
|
|
60
120
|
const content = buildSyncCommand(diff, collected, state);
|
|
121
|
+
// Token tracking
|
|
122
|
+
const tokens = estimateTokens(content);
|
|
123
|
+
const cost = estimateCost(tokens);
|
|
61
124
|
if (dryRun) {
|
|
62
125
|
console.log(c.bold("[DRY RUN] Changes detected:\n"));
|
|
63
126
|
if (diff.added.length) {
|
|
@@ -75,12 +138,26 @@ export async function runSync(opts = {}) {
|
|
|
75
138
|
for (const f of diff.deleted)
|
|
76
139
|
console.log(` ${f}`);
|
|
77
140
|
}
|
|
78
|
-
console.log(`\n Would write: .claude/commands/stack-sync.md (~${
|
|
141
|
+
console.log(`\n Would write: .claude/commands/stack-sync.md (~${tokens.toLocaleString()} tokens)`);
|
|
142
|
+
// Token cost display
|
|
143
|
+
section("Token cost estimate");
|
|
144
|
+
const estimate = buildTokenEstimate([{ label: "sync command", content }]);
|
|
145
|
+
console.log(formatTokenReport(estimate));
|
|
79
146
|
return;
|
|
80
147
|
}
|
|
81
148
|
ensureDir(".claude/commands");
|
|
82
149
|
writeFileSync(".claude/commands/stack-sync.md", content, "utf8");
|
|
83
|
-
await updateManifest("sync", collected);
|
|
150
|
+
await updateManifest("sync", collected, { estimatedTokens: tokens, estimatedCost: cost });
|
|
151
|
+
// Feature A: Create snapshot node
|
|
152
|
+
const allPaths = [
|
|
153
|
+
...Object.keys(collected.configs),
|
|
154
|
+
...collected.source.map(s => s.path),
|
|
155
|
+
];
|
|
156
|
+
const snapshotFiles = collectFilesForSnapshot(cwd, allPaths);
|
|
157
|
+
const changeCount = diff.added.length + diff.changed.length + diff.deleted.length;
|
|
158
|
+
createSnapshot(cwd, "sync", snapshotFiles, {
|
|
159
|
+
summary: `+${diff.added.length} added, ~${diff.changed.length} modified, -${diff.deleted.length} deleted`,
|
|
160
|
+
});
|
|
84
161
|
console.log(`
|
|
85
162
|
Changes since ${c.dim(lastRun.at)}:
|
|
86
163
|
${c.green(`+${diff.added.length}`)} added ${c.yellow(`~${diff.changed.length}`)} modified ${c.red(`-${diff.deleted.length}`)} deleted
|
|
@@ -88,4 +165,17 @@ Changes since ${c.dim(lastRun.at)}:
|
|
|
88
165
|
${c.green("✅")} Ready. Open Claude Code and run:
|
|
89
166
|
${c.cyan("/stack-sync")}
|
|
90
167
|
`);
|
|
168
|
+
// Token cost display
|
|
169
|
+
section("Token cost");
|
|
170
|
+
console.log(` ~${tokens.toLocaleString()} input tokens (${c.dim(`Opus $${cost.opus.toFixed(4)} | Sonnet $${cost.sonnet.toFixed(4)} | Haiku $${cost.haiku.toFixed(4)}`)})`);
|
|
171
|
+
// Optimization hints
|
|
172
|
+
const runs = manifest.runs.map(r => ({ command: r.command, estimatedTokens: r.estimatedTokens }));
|
|
173
|
+
const hints = generateHints(runs, tokens, config.tokenBudget.sync);
|
|
174
|
+
if (hints.length) {
|
|
175
|
+
section("Optimization hints");
|
|
176
|
+
for (const hint of hints) {
|
|
177
|
+
console.log(` ${c.yellow("💡")} ${hint}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log("");
|
|
91
181
|
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runDoctor(verbose?: boolean): Promise<void>;
|
|
1
|
+
export declare function runDoctor(verbose?: boolean, fix?: boolean, testHooks?: boolean): Promise<void>;
|