@karmaniverous/stan-cli 0.11.0 → 0.11.1

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.
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ import { writeFile, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { ensureDir } from 'fs-extra';
5
+ import { b as resolveNamedOrDefaultFunction, D as DBG_SCOPE_SNAP_CONTEXT_LEGACY, l as loadCliConfig } from './stan.js';
6
+
7
+ /**
8
+ * UTC timestamp in YYYYMMDD-HHMMSS for filenames and logs.
9
+ */
10
+ function utcStamp() {
11
+ const d = new Date();
12
+ const pad = (n) => n.toString().padStart(2, '0');
13
+ return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
14
+ }
15
+ /**
16
+ * Convert a UTC stamp (YYYYMMDD-HHMMSS) to a local time string.
17
+ * Falls back to the original stamp if parsing fails.
18
+ *
19
+ * @param ts - UTC stamp in the form YYYYMMDD-HHMMSS.
20
+ * @returns Local time string "YYYY-MM-DD HH:MM:SS", or the original input if parsing fails.
21
+ */
22
+ function formatUtcStampLocal(ts) {
23
+ const m = ts.match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/);
24
+ if (!m)
25
+ return ts;
26
+ const [, y, mo, d, h, mi, s] = m.map((x) => Number.parseInt(x ?? '', 10));
27
+ const dt = new Date(Date.UTC(y, mo - 1, d, h, mi, s));
28
+ // Fixed-width local: YYYY-MM-DD HH:MM:SS
29
+ const pad = (n) => n.toString().padStart(2, '0');
30
+ const yyyy = dt.getFullYear();
31
+ const MM = pad(dt.getMonth() + 1);
32
+ const DD = pad(dt.getDate());
33
+ const HH = pad(dt.getHours());
34
+ const MMi = pad(dt.getMinutes());
35
+ const SS = pad(dt.getSeconds());
36
+ return `${yyyy}-${MM}-${DD} ${HH}:${MMi}:${SS}`;
37
+ }
38
+
39
+ const STATE_FILE = '.snap.state.json';
40
+ const SNAP_DIR = 'snapshots';
41
+ const ARCH_DIR = 'archives';
42
+ const within = (...parts) => path.join(...parts);
43
+ /**
44
+ * Ensure all provided directories exist (best‑effort).
45
+ *
46
+ * @param paths - Absolute directory paths to create (recursively).
47
+ * @returns A promise that resolves when creation attempts finish.
48
+ */
49
+ const ensureDirs = async (paths) => {
50
+ await Promise.all(paths.map((p) => ensureDir(p)));
51
+ };
52
+ /**
53
+ * Read and parse a JSON file, returning `null` on failure.
54
+ *
55
+ * @param p - Absolute file path.
56
+ * @returns Parsed value or `null` if the file is missing or invalid.
57
+ */
58
+ const readJson = async (p) => {
59
+ try {
60
+ const raw = await readFile(p, 'utf8');
61
+ return JSON.parse(raw);
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ };
67
+ /**
68
+ * Write a JSON value to a file with 2‑space indentation.
69
+ *
70
+ * @param p - Absolute destination path.
71
+ * @param v - Value to serialize.
72
+ */
73
+ const writeJson = async (p, v) => {
74
+ await writeFile(p, JSON.stringify(v, null, 2), 'utf8');
75
+ };
76
+
77
+ /* src/stan/snap/context.ts
78
+ * Resolve execution context for snap commands (cwd, stanPath, maxUndos).
79
+ */
80
+ /**
81
+ * Resolve the effective execution context for snapshot operations.
82
+ * Starting from `cwd0`, locates the nearest `stan.config.*` and returns:
83
+ * - `cwd`: the directory containing that config (or `cwd0` if none found),
84
+ * - `stanPath`: configured workspace folder (defaults to ".stan"),
85
+ * - `maxUndos`: normalized retention for snapshot history (default 10).
86
+ *
87
+ * @param cwd0 - Directory to start searching from.
88
+ * @returns Resolved `{ cwd, stanPath, maxUndos }`.
89
+ */
90
+ async function resolveContext(cwd0) {
91
+ let cwd = cwd0;
92
+ let resolveStanPathSyncResolved = null;
93
+ try {
94
+ const coreMod = (await import('./stan.js').then(function (n) { return n.j; }));
95
+ const findConfigPathSyncResolved = resolveNamedOrDefaultFunction(coreMod, (m) => m.findConfigPathSync, (m) => m.default?.findConfigPathSync, 'findConfigPathSync');
96
+ const cfgPath = findConfigPathSyncResolved(cwd0);
97
+ cwd = cfgPath ? path.dirname(cfgPath) : cwd0;
98
+ // Keep a resolved handle for fallback stanPath derivation
99
+ resolveStanPathSyncResolved =
100
+ resolveNamedOrDefaultFunction(coreMod, (m) => m.resolveStanPathSync, (m) => m.default?.resolveStanPathSync, 'resolveStanPathSync');
101
+ }
102
+ catch {
103
+ // best‑effort; keep cwd=cwd0 and derive stanPath in the general fallback below
104
+ resolveStanPathSyncResolved = null;
105
+ }
106
+ // Engine context (namespaced or legacy), resolved lazily to avoid SSR/ESM
107
+ // evaluation-order hazards during module import.
108
+ let engine;
109
+ try {
110
+ const effModUnknown = (await import('./effective-CXoq4vkL.js'));
111
+ // Decide if we should print a single success trace when STAN_DEBUG=1.
112
+ const debugOn = () => process.env.STAN_DEBUG === '1';
113
+ const debugTrace = (kind) => {
114
+ if (!debugOn())
115
+ return;
116
+ try {
117
+ // concise, single-line marker for CI logs
118
+ console.error(`stan: debug: snap.context: candidate=${kind}`);
119
+ }
120
+ catch {
121
+ /* ignore */
122
+ }
123
+ };
124
+ // Candidate caller: try multiple signatures and short-circuit on first valid config.
125
+ const tryCall = async (fnMaybe, kind) => {
126
+ if (typeof fnMaybe !== 'function')
127
+ return null;
128
+ const invoke = async (...args) => {
129
+ try {
130
+ const out = await fnMaybe(...args);
131
+ if (out &&
132
+ typeof out === 'object' &&
133
+ typeof out.stanPath === 'string') {
134
+ return out;
135
+ }
136
+ }
137
+ catch {
138
+ /* try next signature */
139
+ }
140
+ return null;
141
+ };
142
+ // Prefer 2+ arity with scope when declared suggests it.
143
+ const declared = fnMaybe.length;
144
+ if (typeof declared === 'number' && declared >= 2) {
145
+ const a2 = await invoke(cwd, DBG_SCOPE_SNAP_CONTEXT_LEGACY);
146
+ if (a2) {
147
+ debugTrace(kind);
148
+ return a2;
149
+ }
150
+ }
151
+ // Always try (cwd)
152
+ const a1 = await invoke(cwd);
153
+ if (a1) {
154
+ debugTrace(kind);
155
+ return a1;
156
+ }
157
+ // Finally, no-arg call
158
+ const a0 = await invoke();
159
+ if (a0) {
160
+ debugTrace(kind);
161
+ return a0;
162
+ }
163
+ return null;
164
+ };
165
+ // Build ordered candidates (short‑circuit on first success).
166
+ const mod = effModUnknown;
167
+ // Immediate fast path: function-as-default — call directly before building the list
168
+ try {
169
+ const dAny = mod.default;
170
+ if (typeof dAny === 'function') {
171
+ const fast = await tryCall(dAny, 'default-fn');
172
+ if (fast) {
173
+ engine = fast;
174
+ }
175
+ }
176
+ }
177
+ catch {
178
+ /* best-effort */
179
+ }
180
+ const candidates = [];
181
+ if (!engine) {
182
+ // Prefer default‑only shapes first (matches common test/mock shapes):
183
+ // 1) function‑as‑default (still include in list for completeness)
184
+ const dAny = mod.default;
185
+ if (typeof dAny === 'function')
186
+ candidates.push({ fn: dAny, kind: 'default-fn' });
187
+ // 2) default.resolveEffectiveEngineConfig
188
+ const dObj = dAny && typeof dAny === 'object'
189
+ ? dAny
190
+ : undefined;
191
+ if (dObj && typeof dObj.resolveEffectiveEngineConfig === 'function') {
192
+ candidates.push({
193
+ fn: dObj.resolveEffectiveEngineConfig,
194
+ kind: 'default.resolve',
195
+ });
196
+ }
197
+ // 3) named export
198
+ if (typeof mod.resolveEffectiveEngineConfig === 'function') {
199
+ candidates.push({
200
+ fn: mod.resolveEffectiveEngineConfig,
201
+ kind: 'named',
202
+ });
203
+ }
204
+ // 4) nested default.default function (rare)
205
+ if (dObj && typeof dObj.default === 'function') {
206
+ candidates.push({ fn: dObj.default, kind: 'default.default-fn' });
207
+ }
208
+ // 5) module‑as‑function (edge mocks)
209
+ if (typeof mod === 'function') {
210
+ candidates.push({
211
+ fn: mod,
212
+ kind: 'module-fn',
213
+ });
214
+ }
215
+ // Also scan the immediate default object for any function-valued properties.
216
+ // This catches odd default-only mock shapes without waiting for the deeper walk.
217
+ if (dObj && typeof dObj === 'object') {
218
+ try {
219
+ for (const [, v] of Object.entries(dObj)) {
220
+ if (typeof v === 'function') {
221
+ // Avoid duplicate candidates (simple identity/label guard)
222
+ if (!candidates.some((c) => c.fn === v)) {
223
+ candidates.push({ fn: v, kind: 'default.obj-fn' });
224
+ }
225
+ }
226
+ }
227
+ }
228
+ catch {
229
+ /* best-effort */
230
+ }
231
+ }
232
+ // Try in order
233
+ for (const c of candidates) {
234
+ const out = await tryCall(c.fn, c.kind);
235
+ if (out) {
236
+ engine = out;
237
+ break;
238
+ }
239
+ }
240
+ // As a last‑resort, walk nested defaults a couple of levels to catch exotic shapes.
241
+ if (!engine) {
242
+ const seen = new Set();
243
+ const walk = (obj, depth = 0) => {
244
+ if (!obj || seen.has(obj) || depth > 3)
245
+ return;
246
+ seen.add(obj);
247
+ if (typeof obj === 'function')
248
+ candidates.push({
249
+ fn: obj,
250
+ kind: 'nested.fn',
251
+ });
252
+ if (typeof obj !== 'object' && typeof obj !== 'function')
253
+ return;
254
+ const o = obj;
255
+ if (typeof o.resolveEffectiveEngineConfig === 'function')
256
+ candidates.push({
257
+ fn: o.resolveEffectiveEngineConfig,
258
+ kind: 'nested.resolve',
259
+ });
260
+ if ('default' in o)
261
+ walk(o.default, depth + 1);
262
+ };
263
+ walk(mod);
264
+ for (const c of candidates) {
265
+ const out = await tryCall(c.fn ?? c, c.kind ?? 'nested-fn');
266
+ if (out) {
267
+ engine = out;
268
+ break;
269
+ }
270
+ }
271
+ if (!engine)
272
+ throw new Error('resolveEffectiveEngineConfig not found');
273
+ }
274
+ }
275
+ }
276
+ catch {
277
+ // Minimal, safe fallback: derive stanPath only. This preserves snap
278
+ // behavior even when the effective-config module cannot be resolved
279
+ // (e.g., SSR/mock edge cases). Downstream consumers treat includes/
280
+ // excludes as optional and default to [].
281
+ let stanPath = '.stan';
282
+ try {
283
+ stanPath = resolveStanPathSyncResolved
284
+ ? resolveStanPathSyncResolved(cwd)
285
+ : stanPath;
286
+ }
287
+ catch {
288
+ /* keep default */
289
+ }
290
+ engine = { stanPath };
291
+ }
292
+ // CLI config for snap retention
293
+ let maxUndos;
294
+ try {
295
+ const cli = await loadCliConfig(cwd);
296
+ maxUndos = cli.maxUndos;
297
+ }
298
+ catch {
299
+ /* ignore */
300
+ }
301
+ return {
302
+ cwd,
303
+ stanPath: engine.stanPath,
304
+ maxUndos: maxUndos ?? 10,
305
+ };
306
+ }
307
+
308
+ export { ARCH_DIR as A, SNAP_DIR as S, writeJson as a, STATE_FILE as b, resolveContext as c, ensureDirs as e, formatUtcStampLocal as f, readJson as r, utcStamp as u, within as w };
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { c as DBG_SCOPE_EFFECTIVE_ENGINE_LEGACY, V as Vc, $ as $e, p as parseText, Y as Yc, w as we, d as debugFallback, e as DBG_SCOPE_EFFECTIVE_STANPATH_FALLBACK } from './stan.js';
4
+ import 'node:module';
5
+ import 'node:fs';
6
+ import 'node:path';
7
+ import 'fs-extra';
8
+ import 'node:process';
9
+ import 'node:url';
10
+ import 'process';
11
+ import 'buffer';
12
+ import 'node:crypto';
13
+ import 'node:child_process';
14
+ import 'os';
15
+ import 'path';
16
+ import 'util';
17
+ import 'stream';
18
+ import 'events';
19
+ import 'fs';
20
+ import 'node:os';
21
+ import 'node:tty';
22
+ import 'node:readline';
23
+ import 'clipboardy';
24
+ import 'child_process';
25
+
26
+ // src/runner/config/effective.ts
27
+ // Resolve effective engine ContextConfig (stanPath, includes, excludes, imports),
28
+ // with a transitional legacy extractor. Callers pass a debug scope label so
29
+ // debugFallback messages remain stable for existing tests.
30
+ const isObj = (v) => v !== null && typeof v === 'object';
31
+ const toStringArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
32
+ const normalizeImports = (raw) => {
33
+ if (!isObj(raw))
34
+ return undefined;
35
+ const out = {};
36
+ for (const [k, v] of Object.entries(raw)) {
37
+ if (typeof k !== 'string')
38
+ continue;
39
+ if (Array.isArray(v)) {
40
+ const arr = v.filter((x) => typeof x === 'string');
41
+ if (arr.length)
42
+ out[k] = arr;
43
+ }
44
+ else if (typeof v === 'string' && v.trim().length) {
45
+ out[k] = [v];
46
+ }
47
+ }
48
+ return Object.keys(out).length ? out : undefined;
49
+ };
50
+ /** Phase‑2: accept legacy engine shape only when explicitly enabled by env. */
51
+ const legacyAccepted = () => {
52
+ try {
53
+ const v = String(process.env.STAN_ACCEPT_LEGACY ?? '')
54
+ .trim()
55
+ .toLowerCase();
56
+ return v === '1' || v === 'true';
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ };
62
+ /**
63
+ * Resolve effective engine ContextConfig with a legacy extractor.
64
+ *
65
+ * @param cwd - Repo root (or descendant). The nearest config is used.
66
+ * @param debugScope - Label for debugFallback when synthesizing from legacy root keys.
67
+ */
68
+ async function resolveEffectiveEngineConfig(cwd, debugScope = DBG_SCOPE_EFFECTIVE_ENGINE_LEGACY) {
69
+ // Happy path — namespaced engine loader
70
+ try {
71
+ return await Vc(cwd);
72
+ }
73
+ catch {
74
+ // continue to fallback
75
+ }
76
+ const p = $e(cwd);
77
+ if (p) {
78
+ try {
79
+ const raw = await readFile(p, 'utf8');
80
+ const rootUnknown = parseText(p, raw);
81
+ const root = isObj(rootUnknown) ? rootUnknown : {};
82
+ const stanCore = isObj(root['stan-core']) ? root['stan-core'] : null;
83
+ if (stanCore) {
84
+ // Minimal fallback when loader failed but stan-core node exists:
85
+ // prefer stan-core.stanPath; otherwise resolve or default.
86
+ const sp = stanCore['stanPath'];
87
+ const stanPath = typeof sp === 'string' && sp.trim().length
88
+ ? sp
89
+ : (() => {
90
+ try {
91
+ return Yc(cwd);
92
+ }
93
+ catch {
94
+ return we;
95
+ }
96
+ })();
97
+ return { stanPath };
98
+ }
99
+ // Phase‑2 gate: config file exists but top‑level "stan-core" is missing.
100
+ // Accept legacy only when env allows; otherwise fail early with clear guidance.
101
+ if (!legacyAccepted()) {
102
+ const rel = p.replace(/\\/g, '/');
103
+ throw new Error([
104
+ `stan: legacy engine configuration detected in ${rel} (missing top-level "stan-core").`,
105
+ `Run "stan init" to migrate your config,`,
106
+ `or set STAN_ACCEPT_LEGACY=1 to temporarily accept legacy keys during the transition.`,
107
+ ].join(' '));
108
+ }
109
+ // Legacy root keys extractor (transitional)
110
+ const stanPathRaw = root['stanPath'];
111
+ const stanPath = typeof stanPathRaw === 'string' && stanPathRaw.trim().length
112
+ ? stanPathRaw
113
+ : (() => {
114
+ try {
115
+ return Yc(cwd);
116
+ }
117
+ catch {
118
+ return we;
119
+ }
120
+ })();
121
+ const includes = toStringArray(root['includes']);
122
+ const excludes = toStringArray(root['excludes']);
123
+ const imports = normalizeImports(root['imports']);
124
+ debugFallback(debugScope, `synthesized engine config from legacy root keys in ${p.replace(/\\/g, '/')}`);
125
+ return { stanPath, includes, excludes, imports };
126
+ }
127
+ catch {
128
+ // fall through to default
129
+ }
130
+ }
131
+ // No config file found or parse error — default stanPath
132
+ let stanPathFallback = we;
133
+ try {
134
+ stanPathFallback = Yc(cwd);
135
+ }
136
+ catch {
137
+ /* ignore */
138
+ }
139
+ try {
140
+ // Concise, opt-in notice for fallback path resolution
141
+ debugFallback(DBG_SCOPE_EFFECTIVE_STANPATH_FALLBACK, `using fallback stanPath "${stanPathFallback}" (no config found or parse failed)`);
142
+ }
143
+ catch {
144
+ /* ignore */
145
+ }
146
+ return { stanPath: stanPathFallback };
147
+ }
148
+
149
+ export { resolveEffectiveEngineConfig };
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { c as resolveContext, a as writeJson, f as formatUtcStampLocal, w as within, r as readJson, b as STATE_FILE } from './context-CToKAifL.js';
5
+ import 'fs-extra';
6
+ import './stan.js';
7
+ import 'node:module';
8
+ import 'node:fs';
9
+ import 'node:process';
10
+ import 'node:url';
11
+ import 'process';
12
+ import 'buffer';
13
+ import 'node:crypto';
14
+ import 'node:child_process';
15
+ import 'os';
16
+ import 'path';
17
+ import 'util';
18
+ import 'stream';
19
+ import 'events';
20
+ import 'fs';
21
+ import 'node:os';
22
+ import 'node:tty';
23
+ import 'node:readline';
24
+ import 'clipboardy';
25
+ import 'child_process';
26
+
27
+ /* src/stan/snap/history.ts
28
+ * Snapshot history operations: undo, redo, set, info.
29
+ * Keeps the CLI-visible API compatible with previous handlers.
30
+ */
31
+ const getStatePaths = (cwd, stanPath) => {
32
+ const diffDir = path.join(cwd, stanPath, 'diff');
33
+ const statePath = within(diffDir, STATE_FILE);
34
+ const snapPath = within(diffDir, '.archive.snapshot.json');
35
+ return { diffDir, statePath, snapPath };
36
+ };
37
+ /** Restore the snapshot for a target index and update the in-memory state. */
38
+ const restoreEntryAt = async (st, nextIndex, diffDir, snapPath) => {
39
+ if (nextIndex < 0 || nextIndex >= st.entries.length)
40
+ return null;
41
+ const entry = st.entries[nextIndex];
42
+ const snapAbs = within(diffDir, entry.snapshot);
43
+ const body = await readFile(snapAbs, 'utf8');
44
+ await writeFile(snapPath, body, 'utf8');
45
+ st.index = nextIndex;
46
+ return { st, ts: entry.ts };
47
+ };
48
+ const ensureState = async (statePath, maxUndos) => {
49
+ const st = (await readJson(statePath)) ?? {
50
+ entries: [],
51
+ index: -1,
52
+ maxUndos,
53
+ };
54
+ // normalize maxUndos if missing
55
+ if (!st.maxUndos)
56
+ st.maxUndos = maxUndos;
57
+ return st;
58
+ };
59
+ /**
60
+ * Revert to the previous snapshot in the history stack.
61
+ * Restores `.archive.snapshot.json`, updates the state file, and logs a summary.
62
+ *
63
+ * @returns A promise that resolves when the operation completes.
64
+ */
65
+ const handleUndo = async () => {
66
+ const { cwd, stanPath, maxUndos } = await resolveContext(process.cwd());
67
+ const { diffDir, statePath, snapPath } = getStatePaths(cwd, stanPath);
68
+ const st = await ensureState(statePath, maxUndos);
69
+ if (st.entries.length === 0 || st.index <= 0) {
70
+ console.log('stan: nothing to undo');
71
+ return;
72
+ }
73
+ const next = await restoreEntryAt(st, st.index - 1, diffDir, snapPath).catch((err) => {
74
+ console.error('stan: failed to restore snapshot', err);
75
+ return null;
76
+ });
77
+ if (!next)
78
+ return;
79
+ await writeJson(statePath, st);
80
+ const undos = st.index;
81
+ const redos = st.entries.length - 1 - st.index;
82
+ console.log(`stan: undo -> ${next.ts} (undos left ${undos.toString()}, redos left ${redos.toString()})`);
83
+ };
84
+ /**
85
+ * Advance to the next snapshot in the history stack.
86
+ * Restores `.archive.snapshot.json`, updates the state file, and logs a summary.
87
+ *
88
+ * @returns A promise that resolves when the operation completes.
89
+ */
90
+ const handleRedo = async () => {
91
+ const { cwd, stanPath, maxUndos } = await resolveContext(process.cwd());
92
+ const { diffDir, statePath, snapPath } = getStatePaths(cwd, stanPath);
93
+ const st = await ensureState(statePath, maxUndos);
94
+ if (st.entries.length === 0 || st.index >= st.entries.length - 1) {
95
+ console.log('stan: nothing to redo');
96
+ return;
97
+ }
98
+ const next = await restoreEntryAt(st, st.index + 1, diffDir, snapPath).catch((err) => {
99
+ console.error('stan: failed to restore snapshot', err);
100
+ return null;
101
+ });
102
+ if (!next)
103
+ return;
104
+ await writeJson(statePath, st);
105
+ const undos = st.index;
106
+ const redos = st.entries.length - 1 - st.index;
107
+ console.log(`stan: redo -> ${next.ts} (undos left ${undos.toString()}, redos left ${redos.toString()})`);
108
+ };
109
+ /**
110
+ * Activate a specific snapshot by index and make it current.
111
+ * Overwrites the active `.archive.snapshot.json` with the selected entry
112
+ * and updates the state file.
113
+ *
114
+ * @param indexArg - Index string (0‑based) to select; must be within range.
115
+ * @returns A promise that resolves when the operation completes.
116
+ */
117
+ const handleSet = async (indexArg) => {
118
+ const idx = Number.parseInt(indexArg, 10);
119
+ if (!Number.isFinite(idx) || idx < 0) {
120
+ console.error('stan: invalid index');
121
+ return;
122
+ }
123
+ const { cwd, stanPath, maxUndos } = await resolveContext(process.cwd());
124
+ const { diffDir, statePath, snapPath } = getStatePaths(cwd, stanPath);
125
+ const st = await ensureState(statePath, maxUndos);
126
+ if (idx < 0 || idx >= st.entries.length) {
127
+ console.error('stan: index out of range');
128
+ return;
129
+ }
130
+ const next = await restoreEntryAt(st, idx, diffDir, snapPath).catch(() => null);
131
+ if (!next)
132
+ return;
133
+ await writeJson(statePath, st);
134
+ const undos = st.index;
135
+ const redos = st.entries.length - 1 - st.index;
136
+ console.log(`stan: set -> ${next.ts} (undos left ${undos.toString()}, redos left ${redos.toString()})`);
137
+ };
138
+ /**
139
+ * Print a summary of the snapshot stack with the current index highlighted.
140
+ *
141
+ * @returns A promise that resolves when printing is complete.
142
+ */
143
+ const handleInfo = async () => {
144
+ const { cwd, stanPath, maxUndos } = await resolveContext(process.cwd());
145
+ const { statePath } = getStatePaths(cwd, stanPath);
146
+ const st = await ensureState(statePath, maxUndos);
147
+ const undos = Math.max(0, st.index);
148
+ const redos = st.entries.length > 0 ? Math.max(0, st.entries.length - 1 - st.index) : 0;
149
+ console.log('stan: snap stack (newest → oldest)');
150
+ if (st.entries.length === 0) {
151
+ console.log(' (empty)');
152
+ }
153
+ else {
154
+ st.entries
155
+ .map((e, i) => ({ e, i }))
156
+ .reverse()
157
+ .forEach(({ e, i }) => {
158
+ const mark = i === st.index ? '*' : ' ';
159
+ const hasArch = Boolean(e.archive);
160
+ const hasDiff = Boolean(e.archiveDiff);
161
+ const local = formatUtcStampLocal(e.ts);
162
+ const file = path.basename(e.snapshot);
163
+ console.log(` ${mark} [${i.toString()}] ${local} file: ${file} archive: ${hasArch ? 'yes' : 'no'} diff: ${hasDiff ? 'yes' : 'no'}`);
164
+ });
165
+ }
166
+ console.log(` current index: ${st.index.toString()} undos left: ${undos.toString()} redos left: ${redos.toString()}`);
167
+ };
168
+
169
+ export { handleInfo, handleRedo, handleSet, handleUndo };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as readline$1 from 'node:readline';
3
3
  import readline__default from 'node:readline';
4
- import { g as getDefaultExportFromCjs, r as requireIsFullwidthCodePoint, a as requireEmojiRegex, b as requireColorConvert, o as onExit } from './stan.js';
4
+ import { g as getDefaultExportFromCjs, f as requireIsFullwidthCodePoint, h as requireEmojiRegex, i as requireColorConvert, o as onExit } from './stan.js';
5
5
  import R from 'stream';
6
6
  import { createRequire } from 'node:module';
7
7
  import process$1 from 'node:process';