@karmaniverous/stan-cli 0.12.1 → 0.12.3
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 +6 -6
- package/dist/cjs/index.js +1 -1
- package/dist/cli/stan.js +228 -129
- package/dist/mjs/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/stan.js
CHANGED
|
@@ -19728,6 +19728,9 @@ const printHeader = (kind, last) => {
|
|
|
19728
19728
|
*/
|
|
19729
19729
|
/** Return true to proceed; false to abort. */
|
|
19730
19730
|
const confirmLoopReversal = async () => {
|
|
19731
|
+
// Tests must never block on interactive prompts.
|
|
19732
|
+
if (process.env.NODE_ENV === 'test')
|
|
19733
|
+
return true;
|
|
19731
19734
|
// Non-interactive: proceed by default (CI-compatible; matches other prompts).
|
|
19732
19735
|
const isTTY = Boolean(process.stdout.isTTY);
|
|
19733
19736
|
if (!isTTY)
|
|
@@ -30049,23 +30052,15 @@ const resolveCorePromptPath = () => {
|
|
|
30049
30052
|
catch {
|
|
30050
30053
|
/* ignore */
|
|
30051
30054
|
}
|
|
30052
|
-
// Fallback A: resolve core's
|
|
30055
|
+
// Fallback A: resolve core's entry, then use package-directory to locate its package root.
|
|
30053
30056
|
try {
|
|
30054
30057
|
const req = createRequire(import.meta.url);
|
|
30055
30058
|
const mainEntry = req.resolve('@karmaniverous/stan-core');
|
|
30056
|
-
|
|
30057
|
-
|
|
30058
|
-
const
|
|
30059
|
-
if (
|
|
30060
|
-
|
|
30061
|
-
if (existsSync(candidate))
|
|
30062
|
-
return candidate;
|
|
30063
|
-
break;
|
|
30064
|
-
}
|
|
30065
|
-
const parent = path.dirname(dir);
|
|
30066
|
-
if (parent === dir)
|
|
30067
|
-
break;
|
|
30068
|
-
dir = parent;
|
|
30059
|
+
const pkgRoot = packageDirectorySync({ cwd: path.dirname(mainEntry) }) ?? null;
|
|
30060
|
+
if (pkgRoot) {
|
|
30061
|
+
const candidate = path.join(pkgRoot, 'dist', 'stan.system.md');
|
|
30062
|
+
if (existsSync(candidate))
|
|
30063
|
+
return candidate;
|
|
30069
30064
|
}
|
|
30070
30065
|
}
|
|
30071
30066
|
catch {
|
|
@@ -30090,17 +30085,11 @@ const resolveCorePromptPath = () => {
|
|
|
30090
30085
|
const getCliPackagedSystemPromptPath = () => {
|
|
30091
30086
|
try {
|
|
30092
30087
|
const here = fileURLToPath(import.meta.url);
|
|
30093
|
-
|
|
30094
|
-
|
|
30095
|
-
|
|
30096
|
-
const distDir = path.join(dir, 'dist');
|
|
30097
|
-
const candidate = path.join(distDir, 'stan.system.md');
|
|
30088
|
+
const pkgRoot = packageDirectorySync({ cwd: path.dirname(here) }) ?? null;
|
|
30089
|
+
if (pkgRoot) {
|
|
30090
|
+
const candidate = path.join(pkgRoot, 'dist', 'stan.system.md');
|
|
30098
30091
|
if (existsSync(candidate))
|
|
30099
30092
|
return candidate;
|
|
30100
|
-
const parent = path.dirname(dir);
|
|
30101
|
-
if (parent === dir)
|
|
30102
|
-
break;
|
|
30103
|
-
dir = parent;
|
|
30104
30093
|
}
|
|
30105
30094
|
}
|
|
30106
30095
|
catch {
|
|
@@ -32022,27 +32011,28 @@ const isSubtreePattern = (s) => {
|
|
|
32022
32011
|
const p = posix(s.trim());
|
|
32023
32012
|
return p.endsWith('/**') || p.endsWith('/*');
|
|
32024
32013
|
};
|
|
32025
|
-
/** Extract the "tail" (after last '/') from a glob (e.g., '**\/*.test.ts' -\> '*.test.ts'). */
|
|
32026
|
-
const globTail = (s) => {
|
|
32027
|
-
const p = posix(s.trim());
|
|
32028
|
-
const idx = p.lastIndexOf('/');
|
|
32029
|
-
return idx >= 0 ? p.slice(idx + 1) : p;
|
|
32030
|
-
};
|
|
32031
32014
|
/** Normalize subtree roots from a list of exclude patterns. */
|
|
32032
32015
|
const collectSubtreeRoots = (patterns) => (patterns ?? [])
|
|
32033
32016
|
.filter((p) => isSubtreePattern(p))
|
|
32034
32017
|
.map(stripGlobTail)
|
|
32035
32018
|
.filter((r) => r.length > 0);
|
|
32036
|
-
/** Collect leaf‑glob tails (e.g., '**\/*.test.ts' -\> '*.test.ts') from a list of exclude patterns. */
|
|
32037
|
-
const collectLeafGlobTails = (patterns) => (patterns ?? [])
|
|
32038
|
-
.filter((p) => !isSubtreePattern(p))
|
|
32039
|
-
.map(globTail)
|
|
32040
|
-
.filter((t) => t.length > 0);
|
|
32041
32019
|
const isUnder = (childRel, root) => {
|
|
32042
32020
|
const c = posix(childRel);
|
|
32043
32021
|
const r = posix(root);
|
|
32044
32022
|
return c === r || c.startsWith(r.length ? r + '/' : '');
|
|
32045
32023
|
};
|
|
32024
|
+
const toSubtreeGlob = (root) => `${posix(root)}/**`;
|
|
32025
|
+
const segmentUnderRoot = (root, p) => {
|
|
32026
|
+
const r = posix(root);
|
|
32027
|
+
const full = posix(p);
|
|
32028
|
+
if (!isUnder(full, r))
|
|
32029
|
+
return null;
|
|
32030
|
+
const rest = full.slice(r.length).replace(/^\/+/, '');
|
|
32031
|
+
if (!rest.length)
|
|
32032
|
+
return null;
|
|
32033
|
+
const first = rest.split('/')[0];
|
|
32034
|
+
return first && first.length ? first : null;
|
|
32035
|
+
};
|
|
32046
32036
|
const readFacetMeta = async (cwd, stanPath) => {
|
|
32047
32037
|
const abs = toAbs(cwd, path.join(stanPath, 'system', 'facet.meta.json'));
|
|
32048
32038
|
const meta = await safeReadJson(abs);
|
|
@@ -32096,14 +32086,10 @@ const computeFacetOverlay = async (input) => {
|
|
|
32096
32086
|
const anchorsOverlaySet = new Set();
|
|
32097
32087
|
// Final excludes overlay entries (subtree roots only; leaf-globs are not propagated here).
|
|
32098
32088
|
const excludesOverlayArr = [];
|
|
32099
|
-
// Track subtree-root entries for enabled-wins filtering.
|
|
32100
|
-
const excludesOverlayRoots = [];
|
|
32101
32089
|
const autosuspended = [];
|
|
32102
32090
|
const anchorsKeptCounts = {};
|
|
32103
32091
|
// Track per-facet inactive subtree roots for overlap-kept diagnostics.
|
|
32104
32092
|
const inactiveEntries = [];
|
|
32105
|
-
// Collect leaf‑glob tails from active facets (protected patterns).
|
|
32106
|
-
const activeLeafTails = new Set();
|
|
32107
32093
|
// Precompute active subtree roots across all facets (for tie-breakers and scoped anchors).
|
|
32108
32094
|
const activeRoots = new Set();
|
|
32109
32095
|
for (const name of facetNames) {
|
|
@@ -32112,15 +32098,7 @@ const computeFacetOverlay = async (input) => {
|
|
|
32112
32098
|
if (isActive)
|
|
32113
32099
|
for (const r of exRoots)
|
|
32114
32100
|
activeRoots.add(posix(r));
|
|
32115
|
-
// Also collect leaf‑glob tails for active facets so they can be protected
|
|
32116
|
-
// under inactive subtree roots (enabled‑wins across leaf‑glob vs subtree).
|
|
32117
|
-
if (isActive) {
|
|
32118
|
-
for (const tail of collectLeafGlobTails(meta[name].exclude))
|
|
32119
|
-
activeLeafTails.add(tail);
|
|
32120
|
-
}
|
|
32121
32101
|
}
|
|
32122
|
-
// Collect leaf-glob tails from inactive facets (for scoped anchors under active roots).
|
|
32123
|
-
const inactiveLeafTails = new Set();
|
|
32124
32102
|
// Always include all anchors (keep docs breadcrumbs visible even when overlay off)
|
|
32125
32103
|
for (const name of facetNames) {
|
|
32126
32104
|
const def = defOf(name);
|
|
@@ -32177,7 +32155,6 @@ const computeFacetOverlay = async (input) => {
|
|
|
32177
32155
|
.filter(isSubtreePattern)
|
|
32178
32156
|
.map(stripGlobTail)
|
|
32179
32157
|
.filter(Boolean);
|
|
32180
|
-
const leafGlobs = excludes.filter((p) => !isSubtreePattern(p));
|
|
32181
32158
|
const inc = Array.isArray(def.include) ? def.include.map(posix) : [];
|
|
32182
32159
|
// Count anchors present on disk (for metadata)
|
|
32183
32160
|
anchorsKeptCounts[name] = inc.filter((a) => existsSync(toAbs(cwd, a))).length;
|
|
@@ -32205,75 +32182,80 @@ const computeFacetOverlay = async (input) => {
|
|
|
32205
32182
|
if (!root)
|
|
32206
32183
|
continue;
|
|
32207
32184
|
inactiveEntries.push({ facet: name, root });
|
|
32208
|
-
excludesOverlayRoots.push(root.endsWith('/') ? root : root);
|
|
32209
|
-
}
|
|
32210
|
-
// Collect leaf-glob tails for scoped re-inclusions under active roots.
|
|
32211
|
-
for (const g of leafGlobs) {
|
|
32212
|
-
const tail = globTail(g);
|
|
32213
|
-
if (tail)
|
|
32214
|
-
inactiveLeafTails.add(tail);
|
|
32215
32185
|
}
|
|
32216
32186
|
}
|
|
32217
|
-
//
|
|
32187
|
+
// Nested structural facets: carve-out inactive roots that contain active descendant roots.
|
|
32188
|
+
// - Keep exact-match "enabled wins" behavior (drop inactive root when the same root is active).
|
|
32189
|
+
// - If an inactive root contains one or more active descendant subtree roots, exclude all
|
|
32190
|
+
// immediate children under the inactive root that are NOT ancestors of any active root.
|
|
32191
|
+
// This expresses "B on, rest of A off" without using anchors as filter machinery.
|
|
32218
32192
|
const overlapKeptCounts = {};
|
|
32219
|
-
|
|
32220
|
-
|
|
32221
|
-
const
|
|
32222
|
-
|
|
32223
|
-
|
|
32224
|
-
|
|
32225
|
-
|
|
32226
|
-
|
|
32227
|
-
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
}
|
|
32231
|
-
//
|
|
32232
|
-
|
|
32233
|
-
|
|
32234
|
-
|
|
32235
|
-
for (const { facet } of keptEntries) {
|
|
32236
|
-
overlapKeptCounts[facet] = (overlapKeptCounts[facet] ?? 0) + 1;
|
|
32237
|
-
}
|
|
32238
|
-
}
|
|
32239
|
-
else {
|
|
32240
|
-
// No filtering occurred (no active roots or no excludes); consider all inactive entries as kept.
|
|
32241
|
-
for (const { facet } of inactiveEntries) {
|
|
32242
|
-
overlapKeptCounts[facet] = (overlapKeptCounts[facet] ?? 0) + 1;
|
|
32243
|
-
}
|
|
32244
|
-
// Append the raw subtree roots only.
|
|
32245
|
-
const uniq = Array.from(new Set(excludesOverlayRoots));
|
|
32246
|
-
excludesOverlayArr.length = 0;
|
|
32247
|
-
excludesOverlayArr.push(...uniq);
|
|
32248
|
-
}
|
|
32249
|
-
// Enabled‑wins for leaf‑glob patterns: protect ACTIVE facets' leaf‑glob tails
|
|
32250
|
-
// under any remaining inactive subtree roots by adding scoped anchors
|
|
32251
|
-
// "<inactiveRoot>/**/<tail>" so those files remain visible.
|
|
32252
|
-
if (activeLeafTails.size > 0 && excludesOverlayArr.length > 0) {
|
|
32193
|
+
const activeRootsArr = Array.from(activeRoots);
|
|
32194
|
+
const carveOutOrExcludeRoot = async (root) => {
|
|
32195
|
+
const protectedRoots = activeRootsArr.filter((ar) => ar !== root && isUnder(ar, root));
|
|
32196
|
+
if (protectedRoots.length === 0)
|
|
32197
|
+
return [toSubtreeGlob(root)];
|
|
32198
|
+
// Compute which immediate child entries are "keepers".
|
|
32199
|
+
const keep = new Set();
|
|
32200
|
+
for (const pr of protectedRoots) {
|
|
32201
|
+
const seg = segmentUnderRoot(root, pr);
|
|
32202
|
+
if (seg)
|
|
32203
|
+
keep.add(seg);
|
|
32204
|
+
}
|
|
32205
|
+
// Enumerate immediate children under the inactive root and exclude everything
|
|
32206
|
+
// except the keeper segments. Keep deterministic output ordering.
|
|
32207
|
+
const abs = toAbs(cwd, root);
|
|
32208
|
+
let dirents = null;
|
|
32253
32209
|
try {
|
|
32254
|
-
|
|
32255
|
-
for (const tail of activeLeafTails) {
|
|
32256
|
-
const scoped = posix(`${root}/**/${tail}`);
|
|
32257
|
-
anchorsOverlaySet.add(scoped);
|
|
32258
|
-
}
|
|
32259
|
-
}
|
|
32210
|
+
dirents = (await readdir(abs, { withFileTypes: true }));
|
|
32260
32211
|
}
|
|
32261
32212
|
catch {
|
|
32262
|
-
|
|
32213
|
+
dirents = null;
|
|
32263
32214
|
}
|
|
32264
|
-
|
|
32265
|
-
|
|
32266
|
-
|
|
32267
|
-
|
|
32268
|
-
|
|
32269
|
-
const
|
|
32270
|
-
|
|
32215
|
+
// If we can't enumerate, fall back to excluding the full root and
|
|
32216
|
+
// anchor-rescuing the protected roots. This preserves correctness without
|
|
32217
|
+
// reintroducing leaf-glob anchor tricks.
|
|
32218
|
+
if (!dirents) {
|
|
32219
|
+
try {
|
|
32220
|
+
for (const pr of protectedRoots)
|
|
32221
|
+
anchorsOverlaySet.add(toSubtreeGlob(pr));
|
|
32271
32222
|
}
|
|
32223
|
+
catch {
|
|
32224
|
+
/* best-effort */
|
|
32225
|
+
}
|
|
32226
|
+
return [toSubtreeGlob(root)];
|
|
32227
|
+
}
|
|
32228
|
+
const entries = dirents
|
|
32229
|
+
.map((d) => ({ name: d.name, isDir: d.isDirectory() }))
|
|
32230
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
32231
|
+
const out = [];
|
|
32232
|
+
for (const e of entries) {
|
|
32233
|
+
if (keep.has(e.name))
|
|
32234
|
+
continue;
|
|
32235
|
+
const rel = posix(path.join(root, e.name));
|
|
32236
|
+
out.push(e.isDir ? toSubtreeGlob(rel) : rel);
|
|
32272
32237
|
}
|
|
32238
|
+
return out;
|
|
32239
|
+
};
|
|
32240
|
+
if (inactiveEntries.length > 0) {
|
|
32241
|
+
const patterns = new Set();
|
|
32242
|
+
for (const entry of inactiveEntries) {
|
|
32243
|
+
const r = entry.root;
|
|
32244
|
+
// Enabled-wins (exact match only): if the same subtree root is active, do not apply the inactive drop.
|
|
32245
|
+
if (activeRoots.has(r))
|
|
32246
|
+
continue;
|
|
32247
|
+
overlapKeptCounts[entry.facet] =
|
|
32248
|
+
(overlapKeptCounts[entry.facet] ?? 0) + 1;
|
|
32249
|
+
const ps = await carveOutOrExcludeRoot(r);
|
|
32250
|
+
for (const p of ps)
|
|
32251
|
+
patterns.add(posix(p));
|
|
32252
|
+
}
|
|
32253
|
+
excludesOverlayArr.length = 0;
|
|
32254
|
+
excludesOverlayArr.push(...Array.from(patterns).sort((a, b) => a.localeCompare(b)));
|
|
32273
32255
|
}
|
|
32274
32256
|
// Deduplicate anchors overlay
|
|
32275
32257
|
const anchorsOverlay = Array.from(anchorsOverlaySet);
|
|
32276
|
-
// Excludes overlay contains subtree
|
|
32258
|
+
// Excludes overlay contains engine-ready patterns (subtree globs and/or exact paths).
|
|
32277
32259
|
return {
|
|
32278
32260
|
enabled: true,
|
|
32279
32261
|
excludesOverlay: excludesOverlayArr,
|
|
@@ -32285,11 +32267,6 @@ const computeFacetOverlay = async (input) => {
|
|
|
32285
32267
|
};
|
|
32286
32268
|
};
|
|
32287
32269
|
|
|
32288
|
-
const hasGlob = (s) => s.includes('*') || s.includes('?') || s.includes('[');
|
|
32289
|
-
const ensureSubtreeGlob = (p) => {
|
|
32290
|
-
const s = p.replace(/\/+$/, '');
|
|
32291
|
-
return hasGlob(s) ? s : `${s}/**`;
|
|
32292
|
-
};
|
|
32293
32270
|
const toPosix = (p) => p.replace(/\\+/g, '/');
|
|
32294
32271
|
const buildOverlayInputs = async (args) => {
|
|
32295
32272
|
const { cwd, stanPath, enabled, activateNames, deactivateNames } = args;
|
|
@@ -32317,12 +32294,9 @@ const buildOverlayInputs = async (args) => {
|
|
|
32317
32294
|
};
|
|
32318
32295
|
}
|
|
32319
32296
|
const shouldMap = enabled;
|
|
32320
|
-
//
|
|
32321
|
-
|
|
32322
|
-
//
|
|
32323
|
-
const overlayExcludesRaw = shouldMap ? overlay.excludesOverlay : [];
|
|
32324
|
-
const overlayExcludes = overlayExcludesRaw.map(ensureSubtreeGlob);
|
|
32325
|
-
// Also include leaf-glob excludes from inactive facets (e.g., "**/*.test.ts").
|
|
32297
|
+
// Overlay structural excludes are already engine-ready patterns (subtree globs and/or exact paths).
|
|
32298
|
+
const overlayExcludes = shouldMap ? overlay.excludesOverlay : [];
|
|
32299
|
+
// Also include leaf‑glob excludes from inactive facets (e.g., "**/*.test.ts").
|
|
32326
32300
|
// Read facet.meta.json directly and derive leaf-globs for facets that are
|
|
32327
32301
|
// currently inactive per overlay.effective.
|
|
32328
32302
|
const leafGlobs = [];
|
|
@@ -32350,7 +32324,10 @@ const buildOverlayInputs = async (args) => {
|
|
|
32350
32324
|
catch {
|
|
32351
32325
|
/* best-effort only */
|
|
32352
32326
|
}
|
|
32353
|
-
const engineExcludes = Array.from(new Set([
|
|
32327
|
+
const engineExcludes = Array.from(new Set([
|
|
32328
|
+
...overlayExcludes.map(toPosix),
|
|
32329
|
+
...leafGlobs.map(toPosix),
|
|
32330
|
+
]));
|
|
32354
32331
|
// Facet view lines for plan (same as before; keep compact)
|
|
32355
32332
|
const overlayPlan = (() => {
|
|
32356
32333
|
const lines = [];
|
|
@@ -32825,6 +32802,51 @@ const writeJson = async (p, v) => {
|
|
|
32825
32802
|
*/
|
|
32826
32803
|
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
32827
32804
|
const statePath = (cwd, stanPath) => path.join(cwd, stanPath, 'diff', STATE_FILE);
|
|
32805
|
+
const snapshotBaselinePath = (diffDirAbs) => path.join(diffDirAbs, '.archive.snapshot.json');
|
|
32806
|
+
const findConfigPathUpwards = (start) => {
|
|
32807
|
+
const names = ['stan.config.yml', 'stan.config.yaml', 'stan.config.json'];
|
|
32808
|
+
let cur = start;
|
|
32809
|
+
for (;;) {
|
|
32810
|
+
for (const n of names) {
|
|
32811
|
+
try {
|
|
32812
|
+
const p = path.join(cur, n);
|
|
32813
|
+
if (existsSync(p))
|
|
32814
|
+
return p;
|
|
32815
|
+
}
|
|
32816
|
+
catch {
|
|
32817
|
+
/* ignore */
|
|
32818
|
+
}
|
|
32819
|
+
}
|
|
32820
|
+
const parent = path.dirname(cur);
|
|
32821
|
+
if (parent === cur)
|
|
32822
|
+
break;
|
|
32823
|
+
cur = parent;
|
|
32824
|
+
}
|
|
32825
|
+
return null;
|
|
32826
|
+
};
|
|
32827
|
+
const readStanPathFromConfig = (cfgPath) => {
|
|
32828
|
+
try {
|
|
32829
|
+
const raw = readFileSync(cfgPath, 'utf8');
|
|
32830
|
+
const rootUnknown = parseText(cfgPath, raw);
|
|
32831
|
+
if (!rootUnknown || typeof rootUnknown !== 'object')
|
|
32832
|
+
return null;
|
|
32833
|
+
const root = rootUnknown;
|
|
32834
|
+
const core = root['stan-core'];
|
|
32835
|
+
if (core && typeof core === 'object') {
|
|
32836
|
+
const sp = core.stanPath;
|
|
32837
|
+
if (typeof sp === 'string' && sp.trim().length > 0)
|
|
32838
|
+
return sp.trim();
|
|
32839
|
+
}
|
|
32840
|
+
// Legacy fallback (ensure tests/older configs still work)
|
|
32841
|
+
const legacy = root.stanPath;
|
|
32842
|
+
if (typeof legacy === 'string' && legacy.trim().length > 0)
|
|
32843
|
+
return legacy.trim();
|
|
32844
|
+
}
|
|
32845
|
+
catch {
|
|
32846
|
+
/* ignore */
|
|
32847
|
+
}
|
|
32848
|
+
return null;
|
|
32849
|
+
};
|
|
32828
32850
|
const readSnapState = async (p) => {
|
|
32829
32851
|
return (await readJson(p)) ?? null;
|
|
32830
32852
|
};
|
|
@@ -32836,48 +32858,107 @@ const writeSnapState = async (p, s) => {
|
|
|
32836
32858
|
const setIndexSnap = async (p, rawIndex) => {
|
|
32837
32859
|
const cur = await readSnapState(p);
|
|
32838
32860
|
if (!cur)
|
|
32839
|
-
return;
|
|
32861
|
+
return null;
|
|
32840
32862
|
const n = Number.parseInt(rawIndex, 10);
|
|
32841
32863
|
const max = cur.entries.length > 0 ? cur.entries.length - 1 : 0;
|
|
32842
32864
|
const next = Number.isFinite(n) ? clamp(n, 0, max) : cur.index;
|
|
32843
32865
|
await writeSnapState(p, { ...cur, index: next });
|
|
32866
|
+
return (await readSnapState(p)) ?? null;
|
|
32844
32867
|
};
|
|
32845
32868
|
const undoSnap = async (p) => {
|
|
32846
32869
|
const cur = await readSnapState(p);
|
|
32847
32870
|
if (!cur)
|
|
32848
|
-
return;
|
|
32871
|
+
return null;
|
|
32849
32872
|
const max = cur.entries.length > 0 ? cur.entries.length - 1 : 0;
|
|
32850
32873
|
const next = clamp(cur.index - 1, 0, max);
|
|
32851
32874
|
await writeSnapState(p, { ...cur, index: next });
|
|
32875
|
+
return (await readSnapState(p)) ?? null;
|
|
32852
32876
|
};
|
|
32853
32877
|
const redoSnap = async (p) => {
|
|
32854
32878
|
const cur = await readSnapState(p);
|
|
32855
32879
|
if (!cur)
|
|
32856
|
-
return;
|
|
32880
|
+
return null;
|
|
32857
32881
|
const max = cur.entries.length > 0 ? cur.entries.length - 1 : 0;
|
|
32858
32882
|
const next = clamp(cur.index + 1, 0, max);
|
|
32859
32883
|
await writeSnapState(p, { ...cur, index: next });
|
|
32884
|
+
return (await readSnapState(p)) ?? null;
|
|
32885
|
+
};
|
|
32886
|
+
/**
|
|
32887
|
+
* Restore the active diff snapshot baseline (<stanPath>/diff/.archive.snapshot.json)
|
|
32888
|
+
* from the currently selected snap entry in <stanPath>/diff/.snap.state.json.
|
|
32889
|
+
*
|
|
32890
|
+
* This is the missing link that makes undo/redo/set affect `stan run` diffs.
|
|
32891
|
+
*/
|
|
32892
|
+
const restoreSnapshotBaseline = async (snapStateAbs, st) => {
|
|
32893
|
+
// Guard: nothing to restore
|
|
32894
|
+
const entry = st.entries[st.index];
|
|
32895
|
+
if (!entry.snapshot)
|
|
32896
|
+
return { restored: false };
|
|
32897
|
+
const diffDirAbs = path.dirname(snapStateAbs);
|
|
32898
|
+
const src = path.join(diffDirAbs, entry.snapshot);
|
|
32899
|
+
const dest = snapshotBaselinePath(diffDirAbs);
|
|
32900
|
+
try {
|
|
32901
|
+
if (!existsSync(src))
|
|
32902
|
+
return { restored: false, ts: entry.ts };
|
|
32903
|
+
}
|
|
32904
|
+
catch {
|
|
32905
|
+
return { restored: false, ts: entry.ts };
|
|
32906
|
+
}
|
|
32907
|
+
try {
|
|
32908
|
+
await copyFile(src, dest);
|
|
32909
|
+
return { restored: true, ts: entry.ts };
|
|
32910
|
+
}
|
|
32911
|
+
catch {
|
|
32912
|
+
return { restored: false, ts: entry.ts };
|
|
32913
|
+
}
|
|
32860
32914
|
};
|
|
32861
32915
|
/**
|
|
32862
32916
|
* CLI handlers (snap subcommands)
|
|
32863
32917
|
* - resolve stanPath robustly
|
|
32864
32918
|
* - operate on the history file under <stanPath>/diff/.snap.state.json
|
|
32865
32919
|
*/
|
|
32866
|
-
const
|
|
32920
|
+
const resolveRepoRoot = () => {
|
|
32867
32921
|
const cwd = process.cwd();
|
|
32922
|
+
// Prefer core's resolver, but it may return null without throwing.
|
|
32923
|
+
// Fall back to a direct upward scan for repos/tests without package.json.
|
|
32924
|
+
const cfgPath = (() => {
|
|
32925
|
+
try {
|
|
32926
|
+
return ke(cwd);
|
|
32927
|
+
}
|
|
32928
|
+
catch {
|
|
32929
|
+
return null;
|
|
32930
|
+
}
|
|
32931
|
+
})();
|
|
32932
|
+
const chosen = cfgPath ?? findConfigPathUpwards(cwd);
|
|
32933
|
+
return chosen ? path.dirname(chosen) : cwd;
|
|
32934
|
+
};
|
|
32935
|
+
const resolveHistoryPath = () => {
|
|
32936
|
+
const repoRoot = resolveRepoRoot();
|
|
32868
32937
|
// Prefer the configured stanPath; probe legacy/common names for existing files.
|
|
32869
32938
|
let configured = '.stan';
|
|
32870
32939
|
try {
|
|
32871
|
-
configured = Wc(
|
|
32940
|
+
configured = Wc(repoRoot);
|
|
32872
32941
|
}
|
|
32873
32942
|
catch {
|
|
32874
|
-
|
|
32943
|
+
// Fallback: derive stanPath by parsing stan.config.* directly when core
|
|
32944
|
+
// resolution cannot locate config from the current working directory.
|
|
32945
|
+
const cfgPath = (() => {
|
|
32946
|
+
try {
|
|
32947
|
+
return ke(repoRoot);
|
|
32948
|
+
}
|
|
32949
|
+
catch {
|
|
32950
|
+
return null;
|
|
32951
|
+
}
|
|
32952
|
+
})() ?? findConfigPathUpwards(repoRoot);
|
|
32953
|
+
const fromCfg = cfgPath ? readStanPathFromConfig(cfgPath) : null;
|
|
32954
|
+
if (fromCfg)
|
|
32955
|
+
configured = fromCfg;
|
|
32875
32956
|
}
|
|
32876
|
-
// Probe order:
|
|
32877
|
-
const ordered = ['out',
|
|
32957
|
+
// Probe order: configured first (real repos), then common test/legacy names.
|
|
32958
|
+
const ordered = [configured, 'out', 'stan', '.stan'];
|
|
32878
32959
|
const candidates = Array.from(new Set(ordered.filter(Boolean).map((s) => s)));
|
|
32879
32960
|
for (const sp of candidates) {
|
|
32880
|
-
const p = statePath(
|
|
32961
|
+
const p = statePath(repoRoot, sp);
|
|
32881
32962
|
try {
|
|
32882
32963
|
if (existsSync(p))
|
|
32883
32964
|
return p;
|
|
@@ -32887,12 +32968,22 @@ const resolveHistoryPath = () => {
|
|
|
32887
32968
|
}
|
|
32888
32969
|
}
|
|
32889
32970
|
// None exist yet: select the configured workspace path.
|
|
32890
|
-
return statePath(
|
|
32971
|
+
return statePath(repoRoot, configured);
|
|
32972
|
+
};
|
|
32973
|
+
const printMove = (action, st, restore) => {
|
|
32974
|
+
const base = `stan: snap history: ${action} -> index ${st.index.toString()} of ${st.entries.length.toString()}`;
|
|
32975
|
+
const ts = restore.ts ? ` (ts ${restore.ts})` : '';
|
|
32976
|
+
const note = restore.restored ? '' : ' (baseline not restored)';
|
|
32977
|
+
console.log(`${base}${ts}${note}`);
|
|
32891
32978
|
};
|
|
32892
32979
|
const handleUndo = async () => {
|
|
32893
32980
|
try {
|
|
32894
32981
|
const p = resolveHistoryPath();
|
|
32895
|
-
await undoSnap(p);
|
|
32982
|
+
const st = await undoSnap(p);
|
|
32983
|
+
if (!st)
|
|
32984
|
+
return;
|
|
32985
|
+
const restore = await restoreSnapshotBaseline(p, st);
|
|
32986
|
+
printMove('undo', st, restore);
|
|
32896
32987
|
}
|
|
32897
32988
|
catch {
|
|
32898
32989
|
/* best‑effort */
|
|
@@ -32901,7 +32992,11 @@ const handleUndo = async () => {
|
|
|
32901
32992
|
const handleRedo = async () => {
|
|
32902
32993
|
try {
|
|
32903
32994
|
const p = resolveHistoryPath();
|
|
32904
|
-
await redoSnap(p);
|
|
32995
|
+
const st = await redoSnap(p);
|
|
32996
|
+
if (!st)
|
|
32997
|
+
return;
|
|
32998
|
+
const restore = await restoreSnapshotBaseline(p, st);
|
|
32999
|
+
printMove('redo', st, restore);
|
|
32905
33000
|
}
|
|
32906
33001
|
catch {
|
|
32907
33002
|
/* best‑effort */
|
|
@@ -32910,7 +33005,11 @@ const handleRedo = async () => {
|
|
|
32910
33005
|
const handleSet = async (indexArg) => {
|
|
32911
33006
|
try {
|
|
32912
33007
|
const p = resolveHistoryPath();
|
|
32913
|
-
await setIndexSnap(p, indexArg);
|
|
33008
|
+
const st = await setIndexSnap(p, indexArg);
|
|
33009
|
+
if (!st)
|
|
33010
|
+
return;
|
|
33011
|
+
const restore = await restoreSnapshotBaseline(p, st);
|
|
33012
|
+
printMove('set', st, restore);
|
|
32914
33013
|
}
|
|
32915
33014
|
catch {
|
|
32916
33015
|
/* best‑effort */
|