sneakoscope 8.0.2 → 8.0.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.
Files changed (122) hide show
  1. package/README.md +47 -23
  2. package/config/skills-hash-ledger.v1.json +1 -1
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/bin/fast-inline.js +55 -13
  6. package/dist/bin/sks-dispatch.js +1 -1
  7. package/dist/cli/command-manifest-lite.js +4 -0
  8. package/dist/cli/command-registry.js +4 -0
  9. package/dist/cli/install-helpers-codex-lb-chain.js +187 -0
  10. package/dist/cli/install-helpers-codex-lb-config.js +74 -8
  11. package/dist/cli/install-helpers.js +89 -44
  12. package/dist/commands/codex-lb.js +117 -19
  13. package/dist/commands/doctor.js +697 -37
  14. package/dist/config/skills-manifest.json +3 -2
  15. package/dist/core/agents/agent-runner-codex-exec.js +5 -3
  16. package/dist/core/codex/codex-config-guard.js +238 -52
  17. package/dist/core/codex-adapter.js +5 -3
  18. package/dist/core/codex-app/menubar/constants.js +10 -1
  19. package/dist/core/codex-app/menubar/global-install.js +288 -0
  20. package/dist/core/codex-app/menubar/installer.js +79 -14
  21. package/dist/core/codex-app/menubar/launch-agent.js +267 -45
  22. package/dist/core/codex-app/menubar/paths.js +3 -0
  23. package/dist/core/codex-app/menubar/rollback.js +55 -7
  24. package/dist/core/codex-app/menubar/status.js +75 -4
  25. package/dist/core/codex-control/codex-0139-image-path-real-probe.js +3 -1
  26. package/dist/core/codex-control/codex-0139-web-search-probe.js +3 -1
  27. package/dist/core/codex-control/codex-app-server-runtime-env.js +13 -4
  28. package/dist/core/codex-control/codex-app-server-v2-client.js +3 -3
  29. package/dist/core/codex-exec-output-schema.js +4 -1
  30. package/dist/core/codex-lb/codex-lb-env.js +63 -72
  31. package/dist/core/codex-lb/codex-lb-setup.js +15 -2
  32. package/dist/core/codex-lb/desktop-center-credentials.js +517 -58
  33. package/dist/core/codex-lb/desktop-controller.js +153 -24
  34. package/dist/core/codex-lb/routing-truth.js +327 -0
  35. package/dist/core/codex-native/core-skill-manifest.js +1 -1
  36. package/dist/core/commands/basic-cli.js +7 -1
  37. package/dist/core/commands/qa-loop-command.js +3 -1
  38. package/dist/core/commands/telegram-command.js +241 -0
  39. package/dist/core/config-adopt/config-adopt-command.js +35 -0
  40. package/dist/core/config-adopt/config-adopt.js +346 -0
  41. package/dist/core/config-adopt/index.js +3 -0
  42. package/dist/core/doctor/command-alias-cleanup.js +4 -1
  43. package/dist/core/doctor/current-project-guidance-nested.js +37 -6
  44. package/dist/core/doctor/current-project-guidance.js +11 -6
  45. package/dist/core/doctor/doctor-readiness-matrix.js +14 -0
  46. package/dist/core/doctor/retired-managed-residue-private.js +1 -1
  47. package/dist/core/doctor/sks-menubar-doctor.js +4 -0
  48. package/dist/core/feature-fixtures.js +8 -0
  49. package/dist/core/hooks-runtime/hook-context.js +9 -0
  50. package/dist/core/hooks-runtime/managed-skill-admission.js +54 -4
  51. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +69 -1
  52. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +2 -2
  53. package/dist/core/hooks-runtime/subagent-skill-availability-evidence.js +3 -3
  54. package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +43 -5
  55. package/dist/core/hooks-runtime/subagent-skill-availability.js +64 -30
  56. package/dist/core/hooks-runtime/subagent-stop-hook.js +15 -2
  57. package/dist/core/hooks-runtime.js +148 -4
  58. package/dist/core/imagegen/imagegen-capability.js +46 -0
  59. package/dist/core/imagegen/require-imagegen.js +2 -2
  60. package/dist/core/init/skills.js +17 -3
  61. package/dist/core/install/installed-package-smoke.js +8 -2
  62. package/dist/core/naruto/hardware-capacity-probe.js +9 -3
  63. package/dist/core/naruto/naruto-concurrency-governor.js +68 -24
  64. package/dist/core/perf/import-graph-budget.js +0 -1
  65. package/dist/core/product-design-app-server.js +13 -1
  66. package/dist/core/release/package-size-budget.js +2 -2
  67. package/dist/core/release/release-pack-receipt.js +1 -2
  68. package/dist/core/routes/dollar-manifest-lite.js +1 -1
  69. package/dist/core/routes.js +1 -1
  70. package/dist/core/security/private-credential-file.js +209 -0
  71. package/dist/core/subagents/agent-catalog.js +17 -32
  72. package/dist/core/subagents/naruto-help-contract.js +5 -5
  73. package/dist/core/subagents/official-subagent-config.js +16 -6
  74. package/dist/core/subagents/official-subagent-preparation.js +34 -15
  75. package/dist/core/subagents/official-subagent-prompt.js +4 -4
  76. package/dist/core/subagents/official-subagent-runner.js +27 -4
  77. package/dist/core/subagents/thread-budget.js +19 -12
  78. package/dist/core/subagents/wave-lifecycle.js +18 -6
  79. package/dist/core/telegram/access.js +54 -0
  80. package/dist/core/telegram/client.js +144 -0
  81. package/dist/core/telegram/confirmation.js +37 -0
  82. package/dist/core/telegram/doctor.js +151 -0
  83. package/dist/core/telegram/index.js +9 -0
  84. package/dist/core/telegram/keychain.js +265 -0
  85. package/dist/core/telegram/liveness.js +109 -0
  86. package/dist/core/telegram/poller.js +90 -0
  87. package/dist/core/telegram/redaction.js +15 -0
  88. package/dist/core/telegram/types.js +1 -0
  89. package/dist/core/update/update-diagnostics.js +52 -0
  90. package/dist/core/update/update-migration-state.js +5 -1
  91. package/dist/core/update/update-operation.js +2 -1
  92. package/dist/core/update/update-stage-diagnostics.js +38 -0
  93. package/dist/core/update/update-status.js +2 -9
  94. package/dist/core/update-check.js +147 -31
  95. package/dist/core/version.js +1 -1
  96. package/dist/core/zellij/zellij-launcher.js +23 -4
  97. package/dist/native/sks-menubar/Sources/AppDelegate.swift +13 -0
  98. package/dist/native/sks-menubar/Sources/AppIdentity.swift +7 -0
  99. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +3 -2
  100. package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +115 -0
  101. package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +17 -0
  102. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +190 -0
  103. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +100 -59
  104. package/dist/native/sks-menubar/Sources/SingletonInstanceGuard.swift +328 -0
  105. package/dist/native/sks-menubar/Sources/TelegramPrivateFileStore.swift +320 -0
  106. package/dist/native/sks-menubar/Sources/TelegramPrivateFileSupport.swift +303 -0
  107. package/dist/native/sks-menubar/Sources/TelegramProcessGateway.swift +238 -0
  108. package/dist/native/sks-menubar/Sources/TelegramRuntimeSupport.swift +56 -0
  109. package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
  110. package/dist/native/sks-menubar/Sources/TelegramTransport.swift +474 -0
  111. package/dist/native/sks-menubar/Sources/main.swift +15 -1
  112. package/dist/native/sks-menubar/Tests/OperationCoordinatorTests.swift +4 -3
  113. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +129 -0
  114. package/dist/native/sks-menubar/Tests/SingletonInstanceGuardTests.swift +183 -0
  115. package/dist/native/sks-menubar/Tests/TelegramRuntimeSmokeTests.swift +267 -0
  116. package/dist/native/sks-menubar/Tests/TelegramRuntimeTests.swift +222 -0
  117. package/dist/scripts/codex-lb-fast-mode-truth-check.js +1 -1
  118. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +1 -1
  119. package/dist/scripts/current-command-surface-check.js +2 -4
  120. package/dist/scripts/official-subagent-workflow-check.js +1 -1
  121. package/dist/scripts/packlist-performance-check.js +1 -2
  122. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { exists, runProcess, which } from '../../fsx.js';
3
+ import { exists, readJson, runProcess, which, writeJsonAtomic } from '../../fsx.js';
4
4
  import { CONTROL_CENTER_DOMAIN, CONTROL_CENTER_PREFERRED_POSITION, SKS_MENUBAR_LABEL } from './constants.js';
5
5
  export function launchAgentSource(executablePath, installDir) {
6
6
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -31,25 +31,45 @@ export async function seedMenuBarPreferredPosition(env) {
31
31
  export async function launchMenuBar(input) {
32
32
  const domain = launchDomain();
33
33
  const service = launchServiceName();
34
- await runProcess(input.launchctl, ['bootout', service], { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
35
- await runProcess(input.launchctl, ['bootout', domain, input.paths.launch_agent_path], { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
36
- await terminateExistingMenuBarProcess(input.paths.executable_path);
34
+ if (!input.alreadyStopped) {
35
+ const stopped = await stopMenuBarForReplacement({
36
+ launchctl: input.launchctl,
37
+ paths: input.paths,
38
+ executablePaths: input.executablePaths || [input.paths.executable_path],
39
+ ...(input.env ? { env: input.env } : {})
40
+ });
41
+ if (!stopped.ok) {
42
+ return {
43
+ requested: true,
44
+ method: 'launchctl',
45
+ ok: false,
46
+ terminal_uncertain: stopped.timed_out,
47
+ error: stopped.error || 'menubar_stop_failed'
48
+ };
49
+ }
50
+ }
37
51
  const bootstrap = await runProcess(input.launchctl, ['bootstrap', domain, input.paths.launch_agent_path], {
38
52
  timeoutMs: timeoutFromEnv(input.env, 'SKS_MENUBAR_BOOTSTRAP_TIMEOUT_MS', 8_000),
39
53
  maxOutputBytes: 16 * 1024
40
54
  }).catch((error) => failedProcess(error));
41
55
  if (bootstrap.timedOut) {
42
56
  const probe = await waitForLaunchdServiceRunning(input.launchctl, service, input.env);
57
+ const versionProbe = input.expectedVersion && probe.running
58
+ ? await waitForRunningMenuBarVersion(input.paths, input.expectedVersion, probe.pid, input.env)
59
+ : undefined;
43
60
  return {
44
61
  requested: true,
45
62
  method: 'launchctl',
46
- ok: probe.running,
63
+ ok: probe.running && (versionProbe?.ok ?? true),
47
64
  bootstrap_code: bootstrap.code,
48
65
  bootstrap_timed_out: true,
49
66
  print_code: probe.code,
50
67
  verified_running_after_timeout: probe.running,
51
68
  terminal_uncertain: !probe.running,
52
- error: probe.running ? null : probe.error || 'launchctl_bootstrap_timed_out'
69
+ ...(versionProbe ? { version_probe: versionProbe } : {}),
70
+ error: probe.running
71
+ ? versionProbe?.error || null
72
+ : probe.error || 'launchctl_bootstrap_timed_out'
53
73
  };
54
74
  }
55
75
  if (bootstrap.code === 0) {
@@ -59,11 +79,14 @@ export async function launchMenuBar(input) {
59
79
  }).catch((error) => failedProcess(error));
60
80
  if (kickstart.code === 0 || kickstart.timedOut) {
61
81
  const probe = await waitForLaunchdServiceRunning(input.launchctl, service, input.env);
82
+ const versionProbe = input.expectedVersion && probe.running
83
+ ? await waitForRunningMenuBarVersion(input.paths, input.expectedVersion, probe.pid, input.env)
84
+ : undefined;
62
85
  const terminalUncertain = !probe.running && (kickstart.timedOut || probe.code !== 0);
63
86
  return {
64
87
  requested: true,
65
88
  method: 'launchctl',
66
- ok: probe.running,
89
+ ok: probe.running && (versionProbe?.ok ?? true),
67
90
  bootstrap_code: bootstrap.code,
68
91
  bootstrap_timed_out: false,
69
92
  kickstart_code: kickstart.code,
@@ -71,7 +94,10 @@ export async function launchMenuBar(input) {
71
94
  print_code: probe.code,
72
95
  verified_running_after_timeout: kickstart.timedOut && probe.running,
73
96
  terminal_uncertain: terminalUncertain,
74
- error: probe.running ? null : probe.error || (kickstart.timedOut ? 'launchctl_kickstart_timed_out' : 'launchctl_kickstart_not_running')
97
+ ...(versionProbe ? { version_probe: versionProbe } : {}),
98
+ error: probe.running
99
+ ? versionProbe?.error || null
100
+ : probe.error || (kickstart.timedOut ? 'launchctl_kickstart_timed_out' : 'launchctl_kickstart_not_running')
75
101
  };
76
102
  }
77
103
  return { requested: true, method: 'launchctl', ok: false, bootstrap_code: bootstrap.code, bootstrap_timed_out: false, kickstart_code: kickstart.code, kickstart_timed_out: false, error: String(kickstart.stderr || kickstart.stdout).trim() };
@@ -81,16 +107,20 @@ export async function launchMenuBar(input) {
81
107
  .catch((error) => ({ code: 1, stdout: '', stderr: error instanceof Error ? error.message : String(error) }));
82
108
  const running = opened.code === 0
83
109
  && await waitForMenuBarProcessRunning(input.paths.executable_path, input.env);
110
+ const versionProbe = input.expectedVersion && running
111
+ ? await waitForRunningMenuBarVersion(input.paths, input.expectedVersion, null, input.env)
112
+ : undefined;
84
113
  return {
85
114
  requested: true,
86
115
  method: 'open-fallback',
87
- ok: running,
116
+ ok: running && (versionProbe?.ok ?? true),
88
117
  bootstrap_code: bootstrap.code,
89
118
  bootstrap_timed_out: false,
90
119
  open_code: opened.code,
91
120
  open_verified_running: running,
121
+ ...(versionProbe ? { version_probe: versionProbe } : {}),
92
122
  error: running
93
- ? null
123
+ ? versionProbe?.error || null
94
124
  : opened.code === 0
95
125
  ? 'open_succeeded_process_not_running'
96
126
  : String(opened.stderr || bootstrap.stderr).trim()
@@ -98,6 +128,86 @@ export async function launchMenuBar(input) {
98
128
  }
99
129
  return { requested: true, method: 'launchctl', ok: false, bootstrap_code: bootstrap.code, bootstrap_timed_out: false, error: String(bootstrap.stderr || bootstrap.stdout || 'launchctl_bootstrap_failed').trim() };
100
130
  }
131
+ export async function stopMenuBarForReplacement(input) {
132
+ const service = launchServiceName();
133
+ const domain = launchDomain();
134
+ const bootoutTimeout = timeoutFromEnv(input.env, 'SKS_MENUBAR_BOOTOUT_TIMEOUT_MS', 5_000);
135
+ const bootoutResults = [await runProcess(input.launchctl, ['bootout', service], {
136
+ timeoutMs: bootoutTimeout,
137
+ maxOutputBytes: 16 * 1024
138
+ }).catch((error) => failedProcess(error))];
139
+ if (!bootoutAccepted(bootoutResults[0])) {
140
+ bootoutResults.push(await runProcess(input.launchctl, ['bootout', domain, input.paths.launch_agent_path], {
141
+ timeoutMs: bootoutTimeout,
142
+ maxOutputBytes: 16 * 1024
143
+ }).catch((error) => failedProcess(error)));
144
+ }
145
+ const bootoutVerified = bootoutResults.some(bootoutAccepted);
146
+ const bootoutTimedOut = bootoutResults.some((result) => result.timedOut);
147
+ if (!bootoutVerified || bootoutTimedOut) {
148
+ const executablePaths = [...new Set(input.executablePaths.map((value) => path.resolve(value)))];
149
+ const remaining = await exactMenuBarProcessIds(executablePaths, input.env);
150
+ return {
151
+ ok: false,
152
+ service,
153
+ executable_paths: executablePaths,
154
+ bootout_codes: bootoutResults.map((result) => result.code),
155
+ terminated_pids: [],
156
+ remaining_pids: remaining,
157
+ timed_out: bootoutTimedOut,
158
+ error: bootoutTimedOut
159
+ ? 'launchctl_bootout_timed_out'
160
+ : `launchctl_bootout_failed:${bootoutResults.map((result) => result.code ?? 'unknown').join(',')}`
161
+ };
162
+ }
163
+ const terminated = await terminateMenuBarProcesses(input.executablePaths, input.env);
164
+ const executablePaths = terminated.executable_paths;
165
+ const before = terminated.before_pids;
166
+ const remaining = terminated.remaining_pids;
167
+ const processTimedOut = terminated.timed_out;
168
+ return {
169
+ ok: !processTimedOut && !bootoutTimedOut && bootoutVerified,
170
+ service,
171
+ executable_paths: executablePaths,
172
+ bootout_codes: bootoutResults.map((result) => result.code),
173
+ terminated_pids: before.filter((pid) => !remaining.includes(pid)),
174
+ remaining_pids: remaining,
175
+ timed_out: bootoutTimedOut || processTimedOut,
176
+ error: bootoutTimedOut
177
+ ? 'launchctl_bootout_timed_out'
178
+ : !bootoutVerified
179
+ ? `launchctl_bootout_failed:${bootoutResults.map((result) => result.code ?? 'unknown').join(',')}`
180
+ : remaining.length > 0
181
+ ? `menubar_process_stop_timed_out:${remaining.join(',')}`
182
+ : null
183
+ };
184
+ }
185
+ export async function terminateMenuBarProcesses(requestedExecutablePaths, env = process.env) {
186
+ const executablePaths = [...new Set(requestedExecutablePaths.map((value) => path.resolve(value)))];
187
+ const before = await exactMenuBarProcessIds(executablePaths, env);
188
+ const pkill = await processTool(env, 'SKS_MENUBAR_PKILL', '/usr/bin/pkill');
189
+ if (before.length > 0 && pkill) {
190
+ await runProcess(pkill, exactProcessArguments(executablePaths), {
191
+ timeoutMs: 3_000,
192
+ maxOutputBytes: 8 * 1024
193
+ }).catch(() => undefined);
194
+ }
195
+ const readbackTimeout = timeoutFromEnv(env, 'SKS_MENUBAR_STOP_READBACK_TIMEOUT_MS', 5_000);
196
+ const interval = timeoutFromEnv(env, 'SKS_MENUBAR_STOP_READBACK_INTERVAL_MS', 100);
197
+ const deadline = Date.now() + readbackTimeout;
198
+ let remaining = await exactMenuBarProcessIds(executablePaths, env);
199
+ while (remaining.length > 0 && Date.now() < deadline) {
200
+ await delay(Math.min(interval, Math.max(1, deadline - Date.now())));
201
+ remaining = await exactMenuBarProcessIds(executablePaths, env);
202
+ }
203
+ return {
204
+ ok: remaining.length === 0,
205
+ executable_paths: executablePaths,
206
+ before_pids: before,
207
+ remaining_pids: remaining,
208
+ timed_out: remaining.length > 0
209
+ };
210
+ }
101
211
  export async function inspectLaunchdService(env = process.env) {
102
212
  if (process.platform !== 'darwin')
103
213
  return { checked: false, ok: true, service: null, state: null, pid: null, error: null };
@@ -110,28 +220,42 @@ export async function restartLaunchAgent(paths, env) {
110
220
  const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
111
221
  const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || '/usr/bin/open';
112
222
  const service = launchServiceName();
223
+ const buildStamp = await readJson(paths.build_stamp_path, null);
224
+ const expectedVersion = typeof buildStamp?.package_version === 'string' ? buildStamp.package_version : null;
113
225
  const result = await runProcess(launchctl, ['kickstart', '-k', service], {
114
226
  timeoutMs: timeoutFromEnv(env, 'SKS_MENUBAR_KICKSTART_TIMEOUT_MS', 5_000),
115
227
  maxOutputBytes: 16 * 1024
116
228
  }).catch((error) => failedProcess(error));
117
229
  if (result.code === 0 || result.timedOut) {
118
230
  const probe = await waitForLaunchdServiceRunning(launchctl, service, env);
231
+ const versionProbe = probe.running && expectedVersion
232
+ ? await waitForRunningMenuBarVersion(paths, expectedVersion, probe.pid, env)
233
+ : undefined;
119
234
  const terminalUncertain = !probe.running && (result.timedOut || probe.code !== 0);
120
235
  return {
121
- ok: probe.running,
236
+ ok: probe.running && (versionProbe?.ok ?? true),
122
237
  code: result.code,
123
238
  timed_out: result.timedOut,
124
239
  print_code: probe.code,
125
240
  verified_running_after_timeout: result.timedOut && probe.running,
126
241
  terminal_uncertain: terminalUncertain,
127
242
  recovered_via_bootstrap: false,
128
- error: probe.running ? null : probe.error || (result.timedOut ? 'launchctl_kickstart_timed_out' : 'launchctl_kickstart_not_running'),
243
+ ...(versionProbe ? { version_probe: versionProbe } : {}),
244
+ error: probe.running
245
+ ? versionProbe?.error || null
246
+ : probe.error || (result.timedOut ? 'launchctl_kickstart_timed_out' : 'launchctl_kickstart_not_running'),
129
247
  paths
130
248
  };
131
249
  }
132
250
  const kickstartError = String(result.stderr || result.stdout).trim();
133
251
  if (await exists(paths.launch_agent_path) && await exists(paths.executable_path) && isUnloadableLaunchdKickstartError(kickstartError)) {
134
- const launch = await launchMenuBar({ launchctl, open, paths, env });
252
+ const launch = await launchMenuBar({
253
+ launchctl,
254
+ open,
255
+ paths,
256
+ env,
257
+ ...(expectedVersion ? { expectedVersion } : {})
258
+ });
135
259
  return {
136
260
  ok: launch.ok,
137
261
  code: launch.kickstart_code ?? launch.bootstrap_code ?? result.code,
@@ -140,6 +264,7 @@ export async function restartLaunchAgent(paths, env) {
140
264
  verified_running_after_timeout: Boolean(launch.verified_running_after_timeout),
141
265
  terminal_uncertain: Boolean(launch.terminal_uncertain),
142
266
  recovered_via_bootstrap: true,
267
+ ...(launch.version_probe ? { version_probe: launch.version_probe } : {}),
143
268
  error: launch.error ?? (launch.ok ? null : kickstartError),
144
269
  paths
145
270
  };
@@ -156,6 +281,16 @@ export async function restartLaunchAgent(paths, env) {
156
281
  paths
157
282
  };
158
283
  }
284
+ export function isLaunchdServiceAbsent(result) {
285
+ if (result.timedOut || result.code === 0)
286
+ return false;
287
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
288
+ return /could not find service|no such (?:process|file)|service[^\n]*not found|not found[^\n]*service|not loaded|does not exist/i.test(output)
289
+ || (result.code === 113 && /\bbad request\b/i.test(output));
290
+ }
291
+ function bootoutAccepted(result) {
292
+ return result.code === 0 || isLaunchdServiceAbsent(result);
293
+ }
159
294
  export function isUnloadableLaunchdKickstartError(text) {
160
295
  const normalized = String(text || '').toLowerCase();
161
296
  return normalized.includes('could not find service')
@@ -170,8 +305,16 @@ export async function removeLaunchAgent(paths, env) {
170
305
  const warnings = [];
171
306
  const blockers = [];
172
307
  const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
173
- await runProcess(launchctl, ['bootout', launchServiceName()], { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
174
- await terminateExistingMenuBarProcess(paths.executable_path);
308
+ const stopped = await stopMenuBarForReplacement({
309
+ launchctl,
310
+ paths,
311
+ executablePaths: [paths.executable_path],
312
+ env
313
+ });
314
+ if (!stopped.ok)
315
+ blockers.push(stopped.error || 'menubar_process_stop_failed');
316
+ if (blockers.length > 0)
317
+ return { actions, warnings, blockers };
175
318
  await fs.rm(paths.launch_agent_path, { force: true }).catch((error) => blockers.push(`remove_launch_agent_failed:${String(error)}`));
176
319
  await fs.rm(paths.install_dir, { recursive: true, force: true }).catch((error) => blockers.push(`remove_install_dir_failed:${String(error)}`));
177
320
  actions.push(`removed ${paths.launch_agent_path}`, `removed ${paths.install_dir}`);
@@ -182,36 +325,7 @@ export async function removeLaunchAgent(paths, env) {
182
325
  return { actions, warnings, blockers };
183
326
  }
184
327
  export async function isMenuBarProcessRunning(executablePath, env = process.env) {
185
- const injectedPgrep = env.SKS_MENUBAR_TEST_PROCESS_TOOLS === '1'
186
- ? env.SKS_MENUBAR_PGREP
187
- : null;
188
- const pgrep = injectedPgrep
189
- || (await exists('/usr/bin/pgrep') ? '/usr/bin/pgrep' : null);
190
- if (!pgrep)
191
- return false;
192
- const uid = typeof process.getuid === 'function' ? process.getuid() : null;
193
- const args = [
194
- ...(uid === null ? [] : ['-U', String(uid)]),
195
- '-f',
196
- '-x',
197
- `^${posixExtendedRegexLiteral(path.resolve(executablePath))}$`
198
- ];
199
- const result = await runProcess(pgrep, args, { timeoutMs: 2_000, maxOutputBytes: 8 * 1024 }).catch(() => ({ code: 1 }));
200
- return result.code === 0;
201
- }
202
- async function terminateExistingMenuBarProcess(executablePath) {
203
- if (!(await exists(executablePath)))
204
- return;
205
- if (!await exists('/usr/bin/pkill'))
206
- return;
207
- const uid = typeof process.getuid === 'function' ? process.getuid() : null;
208
- const args = [
209
- ...(uid === null ? [] : ['-U', String(uid)]),
210
- '-f',
211
- '-x',
212
- `^${posixExtendedRegexLiteral(path.resolve(executablePath))}$`
213
- ];
214
- await runProcess('/usr/bin/pkill', args, { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
328
+ return (await exactMenuBarProcessIds([executablePath], env)).length > 0;
215
329
  }
216
330
  export function launchDomain() {
217
331
  const uid = typeof process.getuid === 'function' ? process.getuid() : null;
@@ -257,6 +371,74 @@ async function waitForMenuBarProcessRunning(executablePath, env) {
257
371
  await delay(Math.min(intervalMs, remainingMs));
258
372
  }
259
373
  }
374
+ export async function inspectRunningMenuBarProcess(paths, launchdPid, env = process.env) {
375
+ const state = await readJson(paths.runtime_state_path, null);
376
+ if (!state || state.schema !== 'sks.menubar-runtime-state.v1') {
377
+ return {
378
+ checked: true, ok: false, pid: null, package_version: null, build_version: null,
379
+ executable_path: null, started_at: null, source: 'none', error: 'runtime_state_missing'
380
+ };
381
+ }
382
+ const pid = Number.isInteger(state.pid) && state.pid > 0 ? Number(state.pid) : null;
383
+ const executablePath = typeof state.executable_path === 'string' ? path.resolve(state.executable_path) : null;
384
+ const expectedExecutable = path.resolve(paths.executable_path);
385
+ const exactPath = executablePath === expectedExecutable;
386
+ const pidMatches = launchdPid === null || pid === launchdPid;
387
+ const runningPids = exactPath ? await exactMenuBarProcessIds([expectedExecutable], env) : [];
388
+ const running = pid !== null && runningPids.includes(pid);
389
+ const singleton = runningPids.length === 1;
390
+ const ok = Boolean(pid && pidMatches && running && singleton && typeof state.package_version === 'string');
391
+ return {
392
+ checked: true,
393
+ ok,
394
+ pid,
395
+ package_version: typeof state.package_version === 'string' ? state.package_version : null,
396
+ build_version: typeof state.build_version === 'string' ? state.build_version : null,
397
+ executable_path: executablePath,
398
+ started_at: typeof state.started_at === 'string' ? state.started_at : null,
399
+ source: 'runtime-state',
400
+ error: ok
401
+ ? null
402
+ : !exactPath
403
+ ? 'runtime_executable_path_mismatch'
404
+ : !pidMatches
405
+ ? 'runtime_pid_mismatch'
406
+ : !singleton && runningPids.length > 1
407
+ ? `multiple_menubar_processes:${runningPids.join(',')}`
408
+ : !running
409
+ ? 'runtime_process_not_running'
410
+ : 'runtime_state_invalid'
411
+ };
412
+ }
413
+ export async function waitForRunningMenuBarVersion(paths, expectedVersion, expectedPid, env = process.env) {
414
+ const timeout = timeoutFromEnv(env, 'SKS_MENUBAR_VERSION_PROBE_TIMEOUT_MS', 5_000);
415
+ const interval = timeoutFromEnv(env, 'SKS_MENUBAR_VERSION_PROBE_INTERVAL_MS', 100);
416
+ const deadline = Date.now() + timeout;
417
+ let runtime = await inspectRunningMenuBarProcess(paths, expectedPid, env);
418
+ while ((!runtime.ok || runtime.package_version !== expectedVersion) && Date.now() < deadline) {
419
+ await delay(Math.min(interval, Math.max(1, deadline - Date.now())));
420
+ runtime = await inspectRunningMenuBarProcess(paths, expectedPid, env);
421
+ }
422
+ const ok = runtime.ok && runtime.package_version === expectedVersion;
423
+ const probe = {
424
+ schema: 'sks.menubar-version-probe.v1',
425
+ checked: true,
426
+ ok,
427
+ expected_version: expectedVersion,
428
+ running_version: runtime.package_version,
429
+ pid: runtime.pid,
430
+ generated_at: new Date().toISOString(),
431
+ persisted_path: paths.version_probe_path,
432
+ error: ok ? null : runtime.ok ? 'menubar_running_version_mismatch' : runtime.error
433
+ };
434
+ try {
435
+ await writeJsonAtomic(paths.version_probe_path, probe, { mode: 0o600 });
436
+ return probe;
437
+ }
438
+ catch {
439
+ return { ...probe, ok: false, error: 'menubar_version_probe_persist_failed' };
440
+ }
441
+ }
260
442
  async function printLaunchdService(launchctl, service, timeoutMs) {
261
443
  const result = await runProcess(launchctl, ['print', service], { timeoutMs, maxOutputBytes: 32 * 1024 })
262
444
  .catch((error) => failedProcess(error));
@@ -290,6 +472,46 @@ function errorToken(value) {
290
472
  function posixExtendedRegexLiteral(value) {
291
473
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
292
474
  }
475
+ async function exactMenuBarProcessIds(executablePaths, env = process.env) {
476
+ const pgrep = await processTool(env, 'SKS_MENUBAR_PGREP', '/usr/bin/pgrep');
477
+ if (!pgrep || executablePaths.length === 0)
478
+ return [];
479
+ const found = new Set();
480
+ const result = await runProcess(pgrep, exactProcessArguments(executablePaths), {
481
+ timeoutMs: 2_000,
482
+ maxOutputBytes: 8 * 1024
483
+ }).catch(() => ({ code: 1, stdout: '' }));
484
+ if (result.code === 0) {
485
+ for (const line of String(result.stdout || '').split(/\r?\n/)) {
486
+ const pid = Number(line.trim());
487
+ if (Number.isInteger(pid) && pid > 0)
488
+ found.add(pid);
489
+ }
490
+ if (!String(result.stdout || '').trim())
491
+ found.add(-1);
492
+ }
493
+ return [...found].sort((a, b) => a - b);
494
+ }
495
+ function exactProcessArguments(executablePaths) {
496
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
497
+ const alternatives = executablePaths
498
+ .map((executablePath) => posixExtendedRegexLiteral(path.resolve(executablePath)));
499
+ const pattern = alternatives.length === 1
500
+ ? `^${alternatives[0]}$`
501
+ : `^(${alternatives.join('|')})$`;
502
+ return [
503
+ ...(uid === null ? [] : ['-U', String(uid)]),
504
+ '-f',
505
+ '-x',
506
+ pattern
507
+ ];
508
+ }
509
+ async function processTool(env, key, fallback) {
510
+ const injected = env?.SKS_MENUBAR_TEST_PROCESS_TOOLS === '1' ? env[key] : null;
511
+ if (injected)
512
+ return injected;
513
+ return fs.access(fallback).then(() => fallback).catch(() => null);
514
+ }
293
515
  function delay(ms) {
294
516
  return new Promise((resolve) => setTimeout(resolve, ms));
295
517
  }
@@ -25,6 +25,9 @@ export function sksMenuBarPaths(homeInput, rootInput) {
25
25
  staging_action_script_path: path.join(installDir, 'sks-menubar-action.sh.staging'),
26
26
  previous_action_script_path: path.join(installDir, 'sks-menubar-action.sh.previous'),
27
27
  build_stamp_path: path.join(installDir, 'build-stamp.json'),
28
+ runtime_state_path: path.join(installDir, 'runtime-state.json'),
29
+ version_probe_path: path.join(installDir, 'menubar-version-probe.json'),
30
+ duplicate_receipts_dir: path.join(installDir, 'receipts', 'project-duplicates'),
28
31
  staging_build_stamp_path: path.join(installDir, 'build-stamp.json.staging'),
29
32
  previous_build_stamp_path: path.join(installDir, 'build-stamp.json.previous'),
30
33
  config_path: path.join(installDir, 'config.json'),
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { readJson, sha256, which } from '../../fsx.js';
4
4
  import { applyMenuBarGenerationTransaction, commitMenuBarGenerationTransaction, installGenerationPairs, MenuBarGenerationTransactionError, recoverMenuBarGenerationTransaction, rollbackGenerationPairs } from './generation-transaction.js';
5
- import { launchMenuBar } from './launch-agent.js';
5
+ import { launchMenuBar, stopMenuBarForReplacement } from './launch-agent.js';
6
6
  import { sksMenuBarPaths } from './paths.js';
7
7
  import { aggregateFileHashes } from './build-stamp.js';
8
8
  import { inspectInstalledResources } from './resources.js';
@@ -162,6 +162,14 @@ export async function rollbackSksMenuBar(opts = {}) {
162
162
  if (process.platform !== 'darwin') {
163
163
  return result('unsupported_platform', true, null, null, null, null, { requested: false, method: 'none', ok: true }, [], ['not_macos']);
164
164
  }
165
+ const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
166
+ const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || '/usr/bin/open';
167
+ let replacementStopped = false;
168
+ if (await existsWithoutFollowingSymlink(paths.install_transaction_path)) {
169
+ const stopFailure = await stopBeforeReplacement();
170
+ if (stopFailure)
171
+ return stopFailure;
172
+ }
165
173
  const installRecovery = await recoverMenuBarGenerationTransaction({
166
174
  purpose: 'install',
167
175
  journalPath: paths.install_transaction_path,
@@ -189,14 +197,24 @@ export async function rollbackSksMenuBar(opts = {}) {
189
197
  if (opts.launch === false || env.SKS_SKIP_SKS_MENUBAR_LAUNCH === '1') {
190
198
  return result('rolled_back_launch_skipped', true, restored.package_version, null, null, restored, { requested: false, method: 'skipped', ok: true }, [], warnings);
191
199
  }
192
- const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
193
- const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || '/usr/bin/open';
194
- const launch = await launchMenuBar({ launchctl, open, paths, env });
200
+ const launch = await launchMenuBar({
201
+ launchctl,
202
+ open,
203
+ paths,
204
+ env,
205
+ alreadyStopped: replacementStopped,
206
+ ...(restored.package_version ? { expectedVersion: restored.package_version } : {})
207
+ });
195
208
  return result(launch.ok ? 'rolled_back' : launch.terminal_uncertain ? 'terminal_uncertain' : 'failed', launch.ok, restored.package_version, null, null, restored, launch, launch.ok ? [] : [launch.terminal_uncertain ? 'menubar_rollback_launch_terminal_uncertain' : 'menubar_rollback_launch_failed'], warnings);
196
209
  }
197
210
  if (installRecovery.status === 'completed_commit')
198
211
  actions.push('completed interrupted committed Menu Bar install cleanup');
199
212
  const rollbackPairs = rollbackGenerationPairs(paths);
213
+ if (await existsWithoutFollowingSymlink(paths.rollback_transaction_path)) {
214
+ const stopFailure = await stopBeforeReplacement();
215
+ if (stopFailure)
216
+ return stopFailure;
217
+ }
200
218
  const rollbackRecovery = await recoverMenuBarGenerationTransaction({
201
219
  purpose: 'rollback', journalPath: paths.rollback_transaction_path, pairs: rollbackPairs, env
202
220
  });
@@ -219,6 +237,9 @@ export async function rollbackSksMenuBar(opts = {}) {
219
237
  if (!verificationBefore.ok) {
220
238
  return result('failed', false, verificationBefore.package_version, replacedStamp?.package_version || null, verificationBefore, null, { requested: false, method: 'none', ok: false, error: 'rollback_candidate_invalid' }, verificationBefore.blockers, warnings);
221
239
  }
240
+ const stopFailure = await stopBeforeReplacement();
241
+ if (stopFailure)
242
+ return stopFailure;
222
243
  try {
223
244
  transaction = await applyMenuBarGenerationTransaction({
224
245
  purpose: 'rollback', journalPath: paths.rollback_transaction_path, pairs: rollbackPairs, env
@@ -268,9 +289,14 @@ export async function rollbackSksMenuBar(opts = {}) {
268
289
  if (opts.launch === false || env.SKS_SKIP_SKS_MENUBAR_LAUNCH === '1') {
269
290
  return result('rolled_back_launch_skipped', true, verificationBefore.package_version, replacedStamp?.package_version || null, verificationBefore, verificationAfter, { requested: false, method: 'skipped', ok: true }, [], warnings);
270
291
  }
271
- const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
272
- const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || '/usr/bin/open';
273
- const launch = await launchMenuBar({ launchctl, open, paths, env });
292
+ const launch = await launchMenuBar({
293
+ launchctl,
294
+ open,
295
+ paths,
296
+ env,
297
+ alreadyStopped: replacementStopped,
298
+ ...(verificationBefore.package_version ? { expectedVersion: verificationBefore.package_version } : {})
299
+ });
274
300
  if (!launch.ok) {
275
301
  return result(launch.terminal_uncertain ? 'terminal_uncertain' : 'failed', false, verificationBefore.package_version, replacedStamp?.package_version || null, verificationBefore, verificationAfter, launch, [launch.terminal_uncertain ? 'menubar_rollback_launch_terminal_uncertain' : 'menubar_rollback_launch_failed'], warnings);
276
302
  }
@@ -278,6 +304,28 @@ export async function rollbackSksMenuBar(opts = {}) {
278
304
  ? 'launchctl kickstart timed out; launchctl print verified the restored service is running'
279
305
  : 'launchctl restarted the restored Menu Bar service');
280
306
  return result('rolled_back', true, verificationBefore.package_version, replacedStamp?.package_version || null, verificationBefore, verificationAfter, launch, [], warnings);
307
+ async function stopBeforeReplacement() {
308
+ if (replacementStopped)
309
+ return null;
310
+ const stopped = await stopMenuBarForReplacement({
311
+ launchctl,
312
+ paths,
313
+ executablePaths: [paths.executable_path],
314
+ env
315
+ });
316
+ if (!stopped.ok) {
317
+ return result(stopped.timed_out ? 'terminal_uncertain' : 'failed', false, null, null, null, null, {
318
+ requested: false,
319
+ method: 'none',
320
+ ok: false,
321
+ terminal_uncertain: stopped.timed_out,
322
+ error: stopped.error || 'menubar_rollback_stop_failed'
323
+ }, [stopped.error || 'menubar_rollback_stop_failed'], warnings);
324
+ }
325
+ replacementStopped = true;
326
+ actions.push('booted out Menu Bar and verified process termination before journaled rollback replacement');
327
+ return null;
328
+ }
281
329
  function result(status, ok, previousVersion, replacedVersion, before, after, launch, blockers, resultWarnings) {
282
330
  return {
283
331
  schema: 'sks.menubar-rollback.v1',