sneakoscope 2.0.18 → 3.0.0

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.
Files changed (39) hide show
  1. package/README.md +125 -71
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/commands/mad-sks.js +2 -0
  8. package/dist/commands/zellij.js +58 -1
  9. package/dist/core/agents/agent-scheduler.js +32 -24
  10. package/dist/core/agents/native-cli-session-swarm.js +22 -2
  11. package/dist/core/codex-app/codex-app-handoff.js +30 -9
  12. package/dist/core/codex-app/codex-app-launcher.js +103 -0
  13. package/dist/core/codex-control/codex-0138-capability.js +42 -4
  14. package/dist/core/codex-control/codex-model-capabilities.js +25 -4
  15. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  16. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  17. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  18. package/dist/core/codex-plugins/codex-plugin-json.js +35 -11
  19. package/dist/core/commands/mad-sks-command.js +4 -0
  20. package/dist/core/commands/naruto-command.js +27 -0
  21. package/dist/core/commands/qa-loop-command.js +41 -6
  22. package/dist/core/fsx.js +1 -1
  23. package/dist/core/image/image-artifact-path-contract.js +2 -0
  24. package/dist/core/image/image-artifact-registry.js +33 -0
  25. package/dist/core/image-ux-review/imagegen-adapter.js +27 -16
  26. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  27. package/dist/core/qa-loop/qa-loop-budget-policy.js +1 -1
  28. package/dist/core/qa-loop.js +44 -3
  29. package/dist/core/release/release-gate-cache-v2.js +47 -5
  30. package/dist/core/usage/codex-account-usage.js +77 -16
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/zellij/zellij-slot-pane-renderer.js +5 -2
  33. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  34. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  35. package/dist/core/zellij/zellij-update.js +307 -0
  36. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  37. package/package.json +22 -2
  38. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  39. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -215,7 +215,10 @@ async function tryRenderTelemetrySlotPane(input) {
215
215
  return null;
216
216
  const staleRows = staleTelemetryRows(telemetryStatus(snapshot).telemetry_age_ms);
217
217
  const fallbackRows = artifactFallbackRows(input.artifactRender);
218
- const liveRows = staleRows.length || !slot.progress ? fallbackRows : [];
218
+ // Always surface the live artifact rows (current file, tool events, stdout
219
+ // tail). Telemetry freshness only tells us the worker is alive — the user
220
+ // still needs to see WHAT the worker is doing right now.
221
+ const liveRows = fallbackRows;
219
222
  if (slot.status === 'failed') {
220
223
  return [
221
224
  `${slot.slot_id} gen-${slot.generation_index} · FAILED`,
@@ -254,7 +257,7 @@ function artifactFallbackRows(text) {
254
257
  .map((line) => line.replace(/^\|\s?/, '').replace(/\s?\|$/, '').trim())
255
258
  .filter((line) => /^(heartbeat|doing|files|event|out|err):\s+/i.test(line))
256
259
  .filter((line) => !/unknown|waiting for worker intake|no changed file yet/i.test(line))
257
- .slice(-4)
260
+ .slice(-7)
258
261
  .map((line) => `live: ${trimInline(line, 72)}`);
259
262
  }
260
263
  function findTelemetrySlot(snapshot, slotId, generationIndex) {
@@ -7,6 +7,7 @@ const telemetrySnapshotCache = new Map();
7
7
  const telemetrySnapshotWriteCounts = new Map();
8
8
  const telemetrySnapshotFlushCounts = new Map();
9
9
  const telemetrySnapshotLastFlushMs = new Map();
10
+ const telemetrySnapshotDiskStat = new Map();
10
11
  export function slotTelemetryEventPath(root, missionId) {
11
12
  return path.join(inferMissionDir(root, missionId), 'zellij', 'slot-telemetry.events.jsonl');
12
13
  }
@@ -33,24 +34,68 @@ export async function appendZellijSlotTelemetry(root, event) {
33
34
  await rebuildZellijSlotTelemetrySnapshot(root, missionId);
34
35
  }
35
36
  export async function readZellijSlotTelemetrySnapshot(root, missionId) {
36
- const snapshotPath = slotTelemetrySnapshotPath(root, missionId);
37
- const cached = telemetrySnapshotCache.get(snapshotPath);
38
- if (cached?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA)
39
- return cached;
40
- const existing = await readJson(snapshotPath, null);
41
- if (existing?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA)
42
- return existing;
37
+ const fresh = await readZellijSlotTelemetrySnapshotNoRebuild(root, missionId);
38
+ if (fresh)
39
+ return fresh;
43
40
  return rebuildZellijSlotTelemetrySnapshot(root, missionId);
44
41
  }
45
42
  export async function readZellijSlotTelemetrySnapshotNoRebuild(root, missionId) {
46
43
  const snapshotPath = slotTelemetrySnapshotPath(root, missionId);
47
44
  const cached = telemetrySnapshotCache.get(snapshotPath);
48
- if (cached?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA)
45
+ const stat = await statTelemetryFile(snapshotPath);
46
+ const recorded = telemetrySnapshotDiskStat.get(snapshotPath);
47
+ const diskChanged = Boolean(stat) && (!recorded || recorded.mtimeMs !== stat.mtimeMs || recorded.size !== stat.size);
48
+ // CRITICAL: never serve a process-local cache forever. Long-lived reader
49
+ // processes (zellij slot pane renderers in --watch mode) must observe
50
+ // snapshot flushes performed by the orchestrator and worker processes,
51
+ // otherwise the pane renders the same frame for the entire mission.
52
+ if (!diskChanged && cached?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA)
49
53
  return cached;
50
54
  const existing = await readJson(snapshotPath, null);
51
- if (existing?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA)
52
- telemetrySnapshotCache.set(snapshotPath, existing);
53
- return existing?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? existing : null;
55
+ if (stat)
56
+ telemetrySnapshotDiskStat.set(snapshotPath, stat);
57
+ if (existing?.schema !== ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA) {
58
+ return cached?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? cached : null;
59
+ }
60
+ const disk = existing;
61
+ // Merge with any locally cached (possibly not-yet-flushed) slot state so a
62
+ // writer process does not lose its pending events when another process
63
+ // flushed the snapshot file in the meantime. The DISK updated_at stays
64
+ // authoritative on the read path: it reflects the last real flush, which is
65
+ // what stale-telemetry detection must measure.
66
+ const merged = cached?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? mergeTelemetrySnapshots(disk, cached, { updatedAt: 'base' }) : disk;
67
+ telemetrySnapshotCache.set(snapshotPath, merged);
68
+ return merged;
69
+ }
70
+ export function mergeTelemetrySnapshots(base, overlay, opts = {}) {
71
+ const slots = { ...(base.slots || {}) };
72
+ for (const [key, row] of Object.entries(overlay.slots || {})) {
73
+ const existing = slots[key];
74
+ slots[key] = !existing || telemetryTsMs(row.latest_ts) >= telemetryTsMs(existing.latest_ts) ? row : existing;
75
+ }
76
+ const baseTs = Date.parse(String(base.updated_at || '')) || 0;
77
+ const overlayTs = Date.parse(String(overlay.updated_at || '')) || 0;
78
+ return {
79
+ schema: ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA,
80
+ mission_id: base.mission_id || overlay.mission_id,
81
+ updated_at: opts.updatedAt === 'base' ? base.updated_at : overlayTs > baseTs ? overlay.updated_at : base.updated_at,
82
+ flush_count: Math.max(Number(base.flush_count || 0), Number(overlay.flush_count || 0)),
83
+ slots,
84
+ counts: countSlotTelemetry(slots)
85
+ };
86
+ }
87
+ function telemetryTsMs(value) {
88
+ const parsed = Date.parse(String(value || ''));
89
+ return Number.isFinite(parsed) ? parsed : 0;
90
+ }
91
+ async function statTelemetryFile(file) {
92
+ try {
93
+ const st = await fsp.stat(file);
94
+ return { mtimeMs: st.mtimeMs, size: st.size };
95
+ }
96
+ catch {
97
+ return null;
98
+ }
54
99
  }
55
100
  export function applyTelemetryEventToSnapshot(snapshot, event) {
56
101
  const key = slotTelemetryKey(event.slot_id || event.worker_id, event.generation_index);
@@ -216,9 +261,17 @@ async function writeTelemetrySnapshotFast(file, snapshot) {
216
261
  const flushCount = Number(telemetrySnapshotFlushCounts.get(file) || 0) + 1;
217
262
  telemetrySnapshotFlushCounts.set(file, flushCount);
218
263
  telemetrySnapshotLastFlushMs.set(file, Date.now());
219
- const next = { ...snapshot, flush_count: flushCount };
264
+ // Merge with the on-disk snapshot before overwriting: multiple processes
265
+ // (orchestrator + worker children) flush this file concurrently and a plain
266
+ // overwrite would drop slots that only the other process has observed.
267
+ const disk = await readJson(file, null);
268
+ const merged = disk?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? mergeTelemetrySnapshots(disk, snapshot) : snapshot;
269
+ const next = { ...merged, flush_count: Math.max(flushCount, Number(merged.flush_count || 0)) };
220
270
  telemetrySnapshotCache.set(file, next);
221
271
  await fsp.writeFile(file, `${JSON.stringify(next)}\n`, 'utf8');
272
+ const stat = await statTelemetryFile(file);
273
+ if (stat)
274
+ telemetrySnapshotDiskStat.set(file, stat);
222
275
  }
223
276
  function shouldFlushTelemetrySnapshot(file, event) {
224
277
  const next = (telemetrySnapshotWriteCounts.get(file) || 0) + 1;
@@ -2,7 +2,14 @@ export function resolveZellijUiMode(args = [], env = process.env) {
2
2
  return resolveExplicitZellijUiMode(args, env) || 'compact-slots';
3
3
  }
4
4
  export function resolveZellijWorkerPaneUiMode(args = [], env = process.env) {
5
- return resolveExplicitZellijUiMode(args, env) || 'full-debug';
5
+ // Default worker panes to the live slot renderer (compact-slots). In
6
+ // 'full-debug' the pane runs the worker process itself, but the worker is
7
+ // invoked with --json and the codex SDK streams events to JSONL files — so
8
+ // the pane stays blank until the worker exits. The slot renderer re-reads
9
+ // heartbeat/event/stdout artifacts every second and actually shows what each
10
+ // parallel worker is doing in real time. 'full-debug' remains available via
11
+ // --zellij-full-debug or SKS_ZELLIJ_UI_MODE=full-debug.
12
+ return resolveExplicitZellijUiMode(args, env) || 'compact-slots';
6
13
  }
7
14
  function resolveExplicitZellijUiMode(args = [], env = process.env) {
8
15
  const fromEnv = String(env.SKS_ZELLIJ_UI_MODE || '').trim();
@@ -0,0 +1,307 @@
1
+ import https from 'node:https';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { ensureDir, globalSksRoot, nowIso, readJson, runProcess, writeJsonAtomic } from '../fsx.js';
6
+ import { guardContextForRoute, guardedPackageInstall } from '../safety/mutation-guard.js';
7
+ import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
8
+ import { checkZellijCapability } from './zellij-capability.js';
9
+ import { compareVersionLike, parseZellijVersionText } from './zellij-command.js';
10
+ export const ZELLIJ_UPDATE_NOTICE_SCHEMA = 'sks.zellij-update-notice.v1';
11
+ const ZELLIJ_RELEASES_API_PATH = '/repos/zellij-org/zellij/releases/latest';
12
+ export function zellijUpgradeCommandHint(missing = false) {
13
+ if (process.platform === 'darwin')
14
+ return missing ? 'brew install zellij' : 'brew upgrade zellij';
15
+ if (process.platform === 'linux')
16
+ return 'cargo install --locked zellij # or your distro package manager';
17
+ return 'See https://zellij.dev/documentation/installation';
18
+ }
19
+ /**
20
+ * Resolve the latest STABLE zellij version. GitHub's /releases/latest endpoint
21
+ * already excludes prereleases and drafts. Results are cached on disk
22
+ * (default TTL 6h, override with SKS_ZELLIJ_UPDATE_TTL_MS) so command launches
23
+ * stay fast and offline-safe. SKS_ZELLIJ_LATEST_VERSION pins the value for
24
+ * tests and air-gapped environments.
25
+ */
26
+ export async function fetchLatestZellijVersion(input = {}) {
27
+ const env = input.env || process.env;
28
+ const pinned = stripVersionPrefix(String(env.SKS_ZELLIJ_LATEST_VERSION || '').trim());
29
+ if (pinned)
30
+ return { version: parseZellijVersionText(pinned) || pinned, source: 'env' };
31
+ const ttlMs = normalizePositiveInt(env.SKS_ZELLIJ_UPDATE_TTL_MS, 6 * 60 * 60 * 1000);
32
+ const cachePath = zellijUpdateCachePath();
33
+ const cached = await readJson(cachePath, null);
34
+ if (cached?.schema === ZELLIJ_UPDATE_NOTICE_SCHEMA && cached.latest_version && Date.now() - Date.parse(cached.checked_at || '') < ttlMs) {
35
+ return { version: cached.latest_version, source: 'cache' };
36
+ }
37
+ try {
38
+ const tag = await githubLatestTag(input.timeoutMs || normalizePositiveInt(env.SKS_ZELLIJ_UPDATE_TIMEOUT_MS, 2500));
39
+ // GitHub release tags carry a leading "v" (v0.44.3); \b-based version
40
+ // parsing cannot start inside "v0", so strip the prefix first.
41
+ const version = parseZellijVersionText(stripVersionPrefix(tag));
42
+ if (!version)
43
+ throw new Error(`zellij_latest_tag_unparsed:${tag}`);
44
+ return { version, source: 'github-releases' };
45
+ }
46
+ catch (err) {
47
+ return {
48
+ version: cached?.latest_version || null,
49
+ source: 'error',
50
+ error: err?.message || String(err)
51
+ };
52
+ }
53
+ }
54
+ export async function checkZellijUpdateNotice(input = {}) {
55
+ const env = input.env || process.env;
56
+ const ttlMs = normalizePositiveInt(env.SKS_ZELLIJ_UPDATE_TTL_MS, 6 * 60 * 60 * 1000);
57
+ if (env.SKS_SKIP_ZELLIJ_UPDATE === '1' || env.SKS_DISABLE_UPDATE_NOTICE === '1') {
58
+ return persistNotice(input.missionDir, {
59
+ schema: ZELLIJ_UPDATE_NOTICE_SCHEMA,
60
+ checked_at: nowIso(),
61
+ current_version: null,
62
+ latest_version: null,
63
+ update_available: false,
64
+ zellij_missing: false,
65
+ source: 'disabled',
66
+ cache_ttl_ms: ttlMs,
67
+ upgrade_command: zellijUpgradeCommandHint(),
68
+ message: 'Zellij update notice disabled by environment.'
69
+ });
70
+ }
71
+ const capability = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
72
+ const current = capability?.version || null;
73
+ const missing = capability?.status === 'missing' || !capability;
74
+ const fetchInput = { env };
75
+ if (input.timeoutMs !== undefined)
76
+ fetchInput.timeoutMs = input.timeoutMs;
77
+ const latest = await fetchLatestZellijVersion(fetchInput);
78
+ const updateAvailable = Boolean(!missing && current && latest.version && compareVersionLike(latest.version, current) > 0);
79
+ const notice = {
80
+ schema: ZELLIJ_UPDATE_NOTICE_SCHEMA,
81
+ checked_at: nowIso(),
82
+ current_version: current,
83
+ latest_version: latest.version,
84
+ update_available: updateAvailable,
85
+ zellij_missing: missing,
86
+ source: latest.source,
87
+ cache_ttl_ms: ttlMs,
88
+ upgrade_command: zellijUpgradeCommandHint(missing),
89
+ message: missing
90
+ ? `Zellij is not installed. Install with: ${zellijUpgradeCommandHint(true)}`
91
+ : updateAvailable
92
+ ? `Zellij ${latest.version} is available; current ${current}. Upgrade with: ${zellijUpgradeCommandHint()}`
93
+ : `Zellij ${current || 'unknown'} is current enough.`,
94
+ ...(latest.error ? { error: latest.error } : {})
95
+ };
96
+ if (latest.source === 'github-releases') {
97
+ await ensureDir(path.dirname(zellijUpdateCachePath())).catch(() => undefined);
98
+ await writeJsonAtomic(zellijUpdateCachePath(), notice).catch(() => undefined);
99
+ }
100
+ return persistNotice(input.missionDir, notice);
101
+ }
102
+ /**
103
+ * Upgrade zellij to the latest stable release. Only Homebrew automation is
104
+ * attempted (macOS / Linuxbrew); everything else returns manual_required with
105
+ * the exact operator command, mirroring how the Codex CLI update flow behaves.
106
+ */
107
+ export async function upgradeZellijToLatest(input = {}) {
108
+ const before = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
109
+ const beforeVersion = before?.version || null;
110
+ const missing = before?.status === 'missing' || !before;
111
+ const latest = await fetchLatestZellijVersion({ env: input.env || process.env });
112
+ const brew = await runProcess('brew', ['--version'], { timeoutMs: 8000, maxOutputBytes: 4096 });
113
+ if (brew.code !== 0) {
114
+ return {
115
+ status: 'manual_required',
116
+ before_version: beforeVersion,
117
+ after_version: beforeVersion,
118
+ latest_version: latest.version,
119
+ command: zellijUpgradeCommandHint(missing),
120
+ error: 'homebrew_not_available'
121
+ };
122
+ }
123
+ const upgradeArgs = missing ? ['install', 'zellij'] : ['upgrade', 'zellij'];
124
+ // Package installs go through the mutation guard with an explicit
125
+ // zellij_install scope contract (same path ensureZellijCliTool uses), so the
126
+ // mutation ledger records the install and safety gates can audit it. The
127
+ // upgrade only runs after the operator confirmed the [Y/n] prompt or passed
128
+ // --yes / `sks zellij update --yes`.
129
+ const guardRoot = globalSksRoot();
130
+ const guardCommand = `brew ${upgradeArgs.join(' ')}`;
131
+ const contract = createRequestedScopeContract({
132
+ route: 'zellij-update',
133
+ userRequest: guardCommand,
134
+ projectRoot: guardRoot,
135
+ overrides: { package_install: true, zellij_install: true }
136
+ });
137
+ const guardCtx = guardContextForRoute(guardRoot, contract, guardCommand);
138
+ let run = await guardedPackageInstall(guardCtx, 'zellij', {
139
+ confirmed: true,
140
+ command: 'brew',
141
+ args: upgradeArgs,
142
+ timeoutMs: input.timeoutMs || 180000,
143
+ maxOutputBytes: 256 * 1024
144
+ }).catch((err) => ({ code: 1, stdout: '', stderr: String(err?.message || err) }));
145
+ if (run.code !== 0 && !missing && /No such keg|No available formula|not installed/i.test(`${run.stderr}\n${run.stdout}`)) {
146
+ // zellij exists on PATH but was not installed through Homebrew (e.g. cargo
147
+ // or a manual binary). Installing the brew formula gives a managed copy.
148
+ run = await guardedPackageInstall(guardCtx, 'zellij', {
149
+ confirmed: true,
150
+ command: 'brew',
151
+ args: ['install', 'zellij'],
152
+ timeoutMs: input.timeoutMs || 180000,
153
+ maxOutputBytes: 256 * 1024
154
+ }).catch((err) => ({ code: 1, stdout: '', stderr: String(err?.message || err) }));
155
+ }
156
+ if (run.code !== 0 && /already installed|already up-to-date/i.test(`${run.stderr}\n${run.stdout}`)) {
157
+ return {
158
+ status: 'noop',
159
+ before_version: beforeVersion,
160
+ after_version: beforeVersion,
161
+ latest_version: latest.version,
162
+ command: `brew ${upgradeArgs.join(' ')}`,
163
+ error: null
164
+ };
165
+ }
166
+ if (run.code !== 0) {
167
+ return {
168
+ status: 'failed',
169
+ before_version: beforeVersion,
170
+ after_version: beforeVersion,
171
+ latest_version: latest.version,
172
+ command: `brew ${upgradeArgs.join(' ')}`,
173
+ error: `${run.stderr || run.stdout || 'brew upgrade failed'}`.trim().slice(-1000)
174
+ };
175
+ }
176
+ const after = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
177
+ return {
178
+ status: missing ? 'installed' : 'upgraded',
179
+ before_version: beforeVersion,
180
+ after_version: after?.version || null,
181
+ latest_version: latest.version,
182
+ command: `brew ${upgradeArgs.join(' ')}`,
183
+ error: null
184
+ };
185
+ }
186
+ /**
187
+ * Launch-time prompt, mirroring maybePromptCodexUpdateForLaunch: check the
188
+ * installed zellij version against the latest stable release and offer an
189
+ * upgrade before opening the live session. Never blocks the launch.
190
+ *
191
+ * Skips: --json, --skip-cli-tools, --skip-zellij-update, SKS_SKIP_ZELLIJ_UPDATE=1.
192
+ * Auto-approves: --yes / -y.
193
+ */
194
+ export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
195
+ const env = opts.env || process.env;
196
+ const list = (args || []).map((arg) => String(arg));
197
+ if (list.includes('--json') || list.includes('--skip-cli-tools') || list.includes('--skip-zellij-update') || env.SKS_SKIP_ZELLIJ_UPDATE === '1') {
198
+ return { status: 'skipped', current: null, latest: null, command: null };
199
+ }
200
+ const notice = await checkZellijUpdateNotice({ env }).catch(() => null);
201
+ if (!notice)
202
+ return { status: 'skipped', current: null, latest: null, command: null };
203
+ if (notice.zellij_missing) {
204
+ // Zellij is an optional integration; installation is owned by
205
+ // `sks deps check --yes` / `sks zellij update --yes`. Just surface the hint.
206
+ console.log(`Zellij not found (optional live panes disabled). Install with: ${notice.upgrade_command}`);
207
+ return { status: 'missing', current: null, latest: notice.latest_version, command: notice.upgrade_command };
208
+ }
209
+ if (!notice.update_available) {
210
+ return { status: 'current', current: notice.current_version, latest: notice.latest_version, command: null, error: notice.error || null };
211
+ }
212
+ const label = opts.label || 'Zellij launch';
213
+ const autoYes = list.includes('--yes') || list.includes('-y');
214
+ if (!autoYes && !canAskYesNo(env)) {
215
+ console.log(`Zellij update available: ${notice.current_version} -> ${notice.latest_version}. Run: ${notice.upgrade_command}`);
216
+ return { status: 'available', current: notice.current_version, latest: notice.latest_version, command: notice.upgrade_command };
217
+ }
218
+ if (!autoYes) {
219
+ const yes = await askYesNoDefaultYes(`Zellij ${notice.current_version} -> ${notice.latest_version} update before ${label}? [Y/n] `);
220
+ if (!yes)
221
+ return { status: 'skipped_by_user', current: notice.current_version, latest: notice.latest_version, command: notice.upgrade_command };
222
+ }
223
+ const upgraded = await upgradeZellijToLatest({ env });
224
+ if (upgraded.status === 'upgraded' || upgraded.status === 'installed') {
225
+ console.log(`Zellij ${upgraded.before_version || 'unknown'} -> ${upgraded.after_version || upgraded.latest_version || 'latest'} (${upgraded.command})`);
226
+ }
227
+ else if (upgraded.status === 'manual_required') {
228
+ console.log(`Zellij upgrade needs a manual step: ${upgraded.command}`);
229
+ }
230
+ else if (upgraded.status === 'failed') {
231
+ console.log(`Zellij upgrade failed (launch continues): ${upgraded.error || upgraded.command}`);
232
+ }
233
+ return {
234
+ status: upgraded.status,
235
+ current: upgraded.after_version || upgraded.before_version,
236
+ latest: upgraded.latest_version,
237
+ command: upgraded.command,
238
+ error: upgraded.error || null
239
+ };
240
+ }
241
+ export function zellijUpdateCachePath() {
242
+ return path.join(os.homedir(), '.sneakoscope', 'cache', 'zellij-update-notice.json');
243
+ }
244
+ async function persistNotice(missionDir, notice) {
245
+ if (missionDir)
246
+ await writeJsonAtomic(path.join(missionDir, 'zellij-update-notice.json'), notice).catch(() => undefined);
247
+ return notice;
248
+ }
249
+ function githubLatestTag(timeoutMs) {
250
+ return new Promise((resolve, reject) => {
251
+ const req = https.request({
252
+ hostname: 'api.github.com',
253
+ path: ZELLIJ_RELEASES_API_PATH,
254
+ method: 'GET',
255
+ timeout: timeoutMs,
256
+ headers: {
257
+ Accept: 'application/vnd.github+json',
258
+ 'User-Agent': 'sneakoscope-cli'
259
+ }
260
+ }, (res) => {
261
+ let body = '';
262
+ res.setEncoding('utf8');
263
+ res.on('data', (chunk) => { body += chunk; });
264
+ res.on('end', () => {
265
+ try {
266
+ if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300)
267
+ throw new Error(`github_status_${res.statusCode}`);
268
+ const parsed = JSON.parse(body);
269
+ const tag = String(parsed?.tag_name || '').trim();
270
+ if (!tag)
271
+ throw new Error('github_latest_tag_missing');
272
+ resolve(tag);
273
+ }
274
+ catch (err) {
275
+ reject(err);
276
+ }
277
+ });
278
+ });
279
+ req.on('timeout', () => {
280
+ req.destroy(new Error('zellij_update_check_timeout'));
281
+ });
282
+ req.on('error', reject);
283
+ req.end();
284
+ });
285
+ }
286
+ function canAskYesNo(env) {
287
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && env.CI !== 'true');
288
+ }
289
+ async function askYesNoDefaultYes(question) {
290
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
291
+ try {
292
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
293
+ const trimmed = String(answer || '').trim();
294
+ return trimmed === '' || /^(y|yes|예|네|응)$/i.test(trimmed);
295
+ }
296
+ finally {
297
+ rl.close();
298
+ }
299
+ }
300
+ function normalizePositiveInt(value, fallback) {
301
+ const parsed = Number(value);
302
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
303
+ }
304
+ function stripVersionPrefix(value) {
305
+ return value.replace(/^v(?=\d)/i, '');
306
+ }
307
+ //# sourceMappingURL=zellij-update.js.map