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/snapshot.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Zero API calls. All local filesystem operations.
|
|
14
14
|
*/
|
|
15
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
16
|
-
import { join, dirname } from "path";
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, statSync } from "fs";
|
|
16
|
+
import { join, dirname, extname } from "path";
|
|
17
17
|
import { createHash } from "crypto";
|
|
18
18
|
const SNAPSHOTS_DIR = ".claude/snapshots";
|
|
19
19
|
const TIMELINE_FILE = "timeline.json";
|
|
@@ -85,21 +85,35 @@ export function createSnapshot(cwd, command, changedFiles, opts = {}) {
|
|
|
85
85
|
...(opts.input ? { input: opts.input } : {}),
|
|
86
86
|
changedFiles: changedFiles.map(f => f.path),
|
|
87
87
|
summary: opts.summary ?? `${changedFiles.length} file(s) captured`,
|
|
88
|
+
fullSnapshot: true,
|
|
88
89
|
};
|
|
89
90
|
timeline.nodes.push(node);
|
|
91
|
+
delete timeline.restoredTo; // User is at latest — clear any restore marker
|
|
90
92
|
writeTimeline(cwd, timeline);
|
|
91
93
|
writeNodeData(cwd, nodeId, data);
|
|
92
94
|
return node;
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
|
-
* Build the
|
|
96
|
-
*
|
|
97
|
-
*
|
|
97
|
+
* Build the complete file state at a given node.
|
|
98
|
+
*
|
|
99
|
+
* Full snapshots (fullSnapshot: true) store the entire project state — used directly.
|
|
100
|
+
* Legacy delta snapshots accumulate from node 0 to target (last-write-wins).
|
|
101
|
+
*
|
|
102
|
+
* Why the distinction matters: with delta snapshots, if a file was deleted between A→B,
|
|
103
|
+
* it would wrongly appear in cumulative state at B (still present from A). Full snapshots
|
|
104
|
+
* avoid this because the target node's data IS the complete truth at that point.
|
|
98
105
|
*/
|
|
99
106
|
export function buildCumulativeState(cwd, nodeId, timeline) {
|
|
100
107
|
const targetIndex = timeline.nodes.findIndex(n => n.id === nodeId);
|
|
101
108
|
if (targetIndex < 0)
|
|
102
109
|
return null;
|
|
110
|
+
const targetNode = timeline.nodes[targetIndex];
|
|
111
|
+
// Full snapshot: the node's own data is already the complete project state
|
|
112
|
+
if (targetNode.fullSnapshot) {
|
|
113
|
+
const data = readNodeData(cwd, nodeId);
|
|
114
|
+
return data ? { ...data.files } : null;
|
|
115
|
+
}
|
|
116
|
+
// Legacy delta snapshot: accumulate from beginning to target
|
|
103
117
|
const cumulative = {};
|
|
104
118
|
for (let i = 0; i <= targetIndex; i++) {
|
|
105
119
|
const data = readNodeData(cwd, timeline.nodes[i].id);
|
|
@@ -119,12 +133,11 @@ export function buildCumulativeState(cwd, nodeId, timeline) {
|
|
|
119
133
|
* Does NOT delete other nodes — all nodes are preserved (like git).
|
|
120
134
|
*/
|
|
121
135
|
export function restoreSnapshot(cwd, nodeId, timeline) {
|
|
122
|
-
// If no timeline provided, read it
|
|
123
136
|
const tl = timeline ?? readTimeline(cwd);
|
|
124
|
-
// Build cumulative state up to this node
|
|
125
137
|
const cumulativeFiles = buildCumulativeState(cwd, nodeId, tl);
|
|
126
138
|
if (!cumulativeFiles)
|
|
127
|
-
return { restored: [], failed: [nodeId], stale: [] };
|
|
139
|
+
return { restored: [], failed: [nodeId], deleted: [], stale: [] };
|
|
140
|
+
// Step 1: Write all snapshot files to disk
|
|
128
141
|
const restored = [];
|
|
129
142
|
const failed = [];
|
|
130
143
|
for (const [filePath, content] of Object.entries(cumulativeFiles)) {
|
|
@@ -140,29 +153,28 @@ export function restoreSnapshot(cwd, nodeId, timeline) {
|
|
|
140
153
|
failed.push(filePath);
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
|
-
//
|
|
144
|
-
//
|
|
156
|
+
// Step 2: Scan the project NOW (using the just-restored .gitignore)
|
|
157
|
+
// and delete any file that isn't part of the snapshot.
|
|
158
|
+
// This makes restore a true time machine — the directory looks exactly
|
|
159
|
+
// like it did at this snapshot.
|
|
160
|
+
const rules = loadGitignoreRules(cwd); // uses restored .gitignore if it was snapshotted
|
|
161
|
+
const currentFiles = [];
|
|
162
|
+
scanProject(cwd, "", rules, currentFiles);
|
|
163
|
+
const deleted = [];
|
|
145
164
|
const stale = [];
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for (const fp of Object.keys(nodeData.files)) {
|
|
154
|
-
allLaterFiles.add(fp);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
165
|
+
for (const f of currentFiles) {
|
|
166
|
+
if (cumulativeFiles[f.path])
|
|
167
|
+
continue; // in snapshot — already restored
|
|
168
|
+
// Not in snapshot → delete
|
|
169
|
+
try {
|
|
170
|
+
unlinkSync(join(cwd, f.path));
|
|
171
|
+
deleted.push(f.path);
|
|
157
172
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!cumulativeFiles[filePath] && existsSync(join(cwd, filePath))) {
|
|
161
|
-
stale.push(filePath);
|
|
162
|
-
}
|
|
173
|
+
catch {
|
|
174
|
+
stale.push(f.path); // couldn't delete (permissions etc.)
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
|
-
return { restored, failed, stale };
|
|
177
|
+
return { restored, failed, deleted, stale };
|
|
166
178
|
}
|
|
167
179
|
/**
|
|
168
180
|
* Record the last restored node in the timeline (for display purposes).
|
|
@@ -211,40 +223,164 @@ export function compareSnapshots(cwd, nodeIdA, nodeIdB) {
|
|
|
211
223
|
}
|
|
212
224
|
return { onlyInA, onlyInB, changed, identical };
|
|
213
225
|
}
|
|
214
|
-
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
// ── Full-project file scanner (git-like coverage) ──────────────────────
|
|
227
|
+
const MAX_FILE_BYTES = 1024 * 1024; // 1 MB per file
|
|
228
|
+
/** Directory names always excluded (regardless of location in tree) */
|
|
229
|
+
const EXCLUDE_DIRS = new Set([
|
|
230
|
+
".git", "node_modules",
|
|
231
|
+
"dist", "build", "out", ".next", ".nuxt", ".svelte-kit", ".remix",
|
|
232
|
+
"__pycache__", "target", ".gradle", ".mvn", "vendor",
|
|
233
|
+
"coverage", ".nyc_output", ".c8",
|
|
234
|
+
".cache", ".parcel-cache", ".turbo", ".vercel", ".netlify",
|
|
235
|
+
"tmp", "temp", ".tmp",
|
|
236
|
+
]);
|
|
237
|
+
/** Relative paths always excluded */
|
|
238
|
+
const EXCLUDE_REL = new Set([
|
|
239
|
+
".claude/snapshots",
|
|
240
|
+
".claude/token-usage.json",
|
|
241
|
+
".claude/claude-setup.json",
|
|
242
|
+
]);
|
|
243
|
+
/** Filenames always excluded (sensitive or OS noise) */
|
|
244
|
+
const EXCLUDE_NAMES = new Set([
|
|
245
|
+
".env", ".DS_Store", "Thumbs.db", "desktop.ini",
|
|
246
|
+
]);
|
|
247
|
+
/** Binary file extensions — skip */
|
|
248
|
+
const BINARY_EXT = new Set([
|
|
249
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".ico", ".avif",
|
|
250
|
+
".pdf",
|
|
251
|
+
".zip", ".tar", ".gz", ".bz2", ".7z", ".rar", ".xz",
|
|
252
|
+
".exe", ".dll", ".so", ".dylib", ".bin",
|
|
253
|
+
".wasm",
|
|
254
|
+
".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
255
|
+
".mp3", ".mp4", ".wav", ".ogg", ".flac", ".avi", ".mov", ".mkv",
|
|
256
|
+
".class", ".jar", ".war",
|
|
257
|
+
".pyc", ".pyo", ".pyd",
|
|
258
|
+
".o", ".a", ".lib",
|
|
259
|
+
".db", ".sqlite", ".sqlite3",
|
|
260
|
+
]);
|
|
261
|
+
function parseGitignoreLine(line) {
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
264
|
+
return null;
|
|
265
|
+
let pattern = trimmed;
|
|
266
|
+
const negated = pattern.startsWith("!");
|
|
267
|
+
if (negated)
|
|
268
|
+
pattern = pattern.slice(1);
|
|
269
|
+
const anchored = pattern.startsWith("/");
|
|
270
|
+
if (anchored)
|
|
271
|
+
pattern = pattern.slice(1);
|
|
272
|
+
const dirOnly = pattern.endsWith("/");
|
|
273
|
+
if (dirOnly)
|
|
274
|
+
pattern = pattern.slice(0, -1);
|
|
275
|
+
// Convert glob to regex
|
|
276
|
+
let regexStr = "";
|
|
277
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
278
|
+
const ch = pattern[i];
|
|
279
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
280
|
+
if (pattern[i + 2] === "/") {
|
|
281
|
+
regexStr += "(?:.+/)?";
|
|
282
|
+
i += 2;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
regexStr += ".*";
|
|
286
|
+
i++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (ch === "*") {
|
|
290
|
+
regexStr += "[^/]*";
|
|
291
|
+
}
|
|
292
|
+
else if (ch === "?") {
|
|
293
|
+
regexStr += "[^/]";
|
|
294
|
+
}
|
|
295
|
+
else if (".+^${}()|[]\\".includes(ch)) {
|
|
296
|
+
regexStr += "\\" + ch;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
regexStr += ch;
|
|
232
300
|
}
|
|
233
|
-
catch { /* skip unreadable */ }
|
|
234
301
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
302
|
+
const full = (anchored || pattern.includes("/"))
|
|
303
|
+
? `^${regexStr}(?:/.*)?$`
|
|
304
|
+
: `(?:^|/)${regexStr}(?:/.*)?$`;
|
|
305
|
+
try {
|
|
306
|
+
return { negated, dirOnly, regex: new RegExp(full) };
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function loadGitignoreRules(cwd) {
|
|
313
|
+
try {
|
|
314
|
+
return readFileSync(join(cwd, ".gitignore"), "utf8")
|
|
315
|
+
.split("\n").map(parseGitignoreLine)
|
|
316
|
+
.filter((r) => r !== null);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function matchesAnyRule(relPath, isDir, rules) {
|
|
323
|
+
let excluded = false;
|
|
324
|
+
for (const rule of rules) {
|
|
325
|
+
if (rule.dirOnly && !isDir)
|
|
242
326
|
continue;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
327
|
+
if (rule.regex.test(relPath))
|
|
328
|
+
excluded = !rule.negated;
|
|
329
|
+
}
|
|
330
|
+
return excluded;
|
|
331
|
+
}
|
|
332
|
+
function tryReadText(absPath) {
|
|
333
|
+
try {
|
|
334
|
+
const st = statSync(absPath);
|
|
335
|
+
if (!st.isFile() || st.size > MAX_FILE_BYTES)
|
|
336
|
+
return null;
|
|
337
|
+
const content = readFileSync(absPath, "utf8");
|
|
338
|
+
if (content.includes("\0"))
|
|
339
|
+
return null; // binary
|
|
340
|
+
return content;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function scanProject(cwd, relBase, rules, out) {
|
|
347
|
+
const abs = relBase ? join(cwd, relBase) : cwd;
|
|
348
|
+
try {
|
|
349
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
350
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
351
|
+
const isDir = entry.isDirectory();
|
|
352
|
+
// Hard excludes
|
|
353
|
+
if (isDir && EXCLUDE_DIRS.has(entry.name))
|
|
354
|
+
continue;
|
|
355
|
+
if (EXCLUDE_REL.has(relPath) || relPath.startsWith(".claude/snapshots/"))
|
|
356
|
+
continue;
|
|
357
|
+
if (!isDir && EXCLUDE_NAMES.has(entry.name))
|
|
358
|
+
continue;
|
|
359
|
+
if (!isDir && BINARY_EXT.has(extname(entry.name).toLowerCase()))
|
|
360
|
+
continue;
|
|
361
|
+
// Gitignore
|
|
362
|
+
if (matchesAnyRule(relPath, isDir, rules))
|
|
363
|
+
continue;
|
|
364
|
+
if (isDir) {
|
|
365
|
+
scanProject(cwd, relPath, rules, out);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const content = tryReadText(join(cwd, relPath));
|
|
369
|
+
if (content !== null)
|
|
370
|
+
out.push({ path: relPath, content });
|
|
371
|
+
}
|
|
246
372
|
}
|
|
247
|
-
catch { /* skip */ }
|
|
248
373
|
}
|
|
249
|
-
|
|
374
|
+
catch { /* skip unreadable */ }
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Collect ALL project files for snapshot — full git-like coverage.
|
|
378
|
+
* Respects .gitignore + hard exclusions (node_modules, .git, binaries, .env).
|
|
379
|
+
* The trackedPaths param is kept for API compat but ignored.
|
|
380
|
+
*/
|
|
381
|
+
export function collectFilesForSnapshot(cwd, _trackedPaths) {
|
|
382
|
+
const rules = loadGitignoreRules(cwd);
|
|
383
|
+
const out = [];
|
|
384
|
+
scanProject(cwd, "", rules, out);
|
|
385
|
+
return out;
|
|
250
386
|
}
|
package/dist/tokens.js
CHANGED
|
@@ -292,8 +292,8 @@ export function readProjectSessions(projectCwd) {
|
|
|
292
292
|
// Encode the CWD the way Claude Code does, then exact-match against project dirs.
|
|
293
293
|
// This is the only reliable cross-platform approach — decoding is lossy when
|
|
294
294
|
// folder names contain hyphens (e.g. "Claude-code-documentation").
|
|
295
|
-
// Windows: C:\Users\
|
|
296
|
-
// Unix: /Users/
|
|
295
|
+
// Windows: C:\Users\alice\Desktop\my-app → C--Users-alice-Desktop-my-app
|
|
296
|
+
// Unix: /Users/alice/dev/my-app → -Users-alice-dev-my-app
|
|
297
297
|
const encodedCwd = projectCwd
|
|
298
298
|
.replace(/\\/g, "/") // normalize backslashes to forward slashes
|
|
299
299
|
.replace(/:\//g, "--") // drive letter: C:/ → C--
|
package/package.json
CHANGED
package/templates/sync.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<!-- claude-setup sync {{DATE}} | last: {{LAST_RUN_DATE}} -->
|
|
2
2
|
|
|
3
|
+
> **Step 1 — always do this first:** Run `npx claude-setup sync` to refresh this file with the latest diff.
|
|
4
|
+
> - If it outputs "No changes since…" → respond "Stack already in sync." and STOP.
|
|
5
|
+
> - Otherwise → re-read `.claude/commands/stack-sync.md` (Step 2), then process the diff below (Step 3).
|
|
6
|
+
|
|
3
7
|
Project changed since last setup. Update ONLY what the changes demand.
|
|
4
8
|
|
|
5
9
|
## Changes since last setup
|
|
@@ -34,16 +38,20 @@ Skills: {{SKILLS_LIST}} | Commands: {{COMMANDS_LIST}} | Workflows: {{WORKFLOWS_L
|
|
|
34
38
|
|
|
35
39
|
## Your job
|
|
36
40
|
|
|
37
|
-
For EACH changed file
|
|
41
|
+
For EACH changed file, update the Claude Code setup:
|
|
42
|
+
|
|
43
|
+
**Source files added/removed/modified — ALWAYS update CLAUDE.md:**
|
|
44
|
+
- New source directories or modules → add to key dirs section
|
|
45
|
+
- New routes, services, controllers → document the new endpoints/patterns
|
|
46
|
+
- New dependencies or frameworks → update runtime section
|
|
47
|
+
- Renamed or restructured files → update stale paths
|
|
48
|
+
- CLAUDE.md must reflect the CURRENT project structure, not just config files
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
**Config and infrastructure changes:**
|
|
40
51
|
- New dependency → new MCP server needed? New hook justified?
|
|
41
52
|
- New docker-compose service → new MCP entry? Env vars changed?
|
|
42
|
-
- Source file added/removed → CLAUDE.md paths stale? Skill still applies?
|
|
43
53
|
- Config deleted → remove its MCP/hook reference if it was the only evidence?
|
|
44
54
|
|
|
45
|
-
Update ONLY what the change demands.
|
|
46
|
-
Do NOT update things that did not change.
|
|
47
55
|
Do NOT rewrite files — surgical edits only.
|
|
48
56
|
If unsure about a change's implication: flag it, don't guess.
|
|
49
57
|
|