@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/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 main entry, walk up to the module root "stan-core", then join dist/stan.system.md
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
- let dir = path.dirname(mainEntry);
30057
- for (let i = 0; i < 10; i += 1) {
30058
- const base = path.basename(dir);
30059
- if (base === 'stan-core') {
30060
- const candidate = path.join(dir, 'dist', 'stan.system.md');
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
- let dir = path.dirname(here);
30094
- // Walk upward to find a "dist/stan.system.md" near the package root.
30095
- for (let i = 0; i < 8; i += 1) {
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
- // Subtree tie-breaker: enabled facet wins (drop inactive roots that equal/overlap with active roots).
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
- if (inactiveEntries.length > 0 && activeRoots.size > 0) {
32220
- const act = Array.from(activeRoots);
32221
- const keptRoots = [];
32222
- const keptEntries = [];
32223
- for (const entry of inactiveEntries) {
32224
- const r = entry.root;
32225
- const drop = act.some((ar) => ar === r || isUnder(r, ar) || isUnder(ar, r));
32226
- if (!drop) {
32227
- keptRoots.push(r);
32228
- keptEntries.push(entry);
32229
- }
32230
- }
32231
- // Replace with filtered roots only.
32232
- excludesOverlayArr.length = 0;
32233
- excludesOverlayArr.push(...keptRoots);
32234
- // Count kept roots per facet
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
- for (const root of excludesOverlayArr) {
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
- /* best-effort */
32213
+ dirents = null;
32263
32214
  }
32264
- }
32265
- // Leaf-glob scoped re-inclusion: add anchors "<activeRoot>/**/<tail>" for every collected tail.
32266
- if (inactiveLeafTails.size > 0 && activeRoots.size > 0) {
32267
- for (const ar of activeRoots) {
32268
- for (const tail of inactiveLeafTails) {
32269
- const scoped = posix(`${ar}/**/${tail}`);
32270
- anchorsOverlaySet.add(scoped);
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 roots only (already deduped above).
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
- // Map overlay excludes to effective deny-list globs for the engine:
32321
- // - subtree roots like "docs" -> "docs/**"
32322
- // - existing glob patterns (contain *, ?, or [) pass through unchanged.
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([...overlayExcludes, ...leafGlobs]));
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 resolveHistoryPath = () => {
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(cwd);
32940
+ configured = Wc(repoRoot);
32872
32941
  }
32873
32942
  catch {
32874
- /* keep default */
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: prefer "out" first (common in tests), then configured, then legacy names.
32877
- const ordered = ['out', configured, 'stan', '.stan'];
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(cwd, sp);
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(cwd, configured);
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 */