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
@@ -0,0 +1,288 @@
1
+ import path from 'node:path';
2
+ import { exists, nowIso, readJson, readText, runProcess, writeJsonAtomic } from '../../fsx.js';
3
+ import { ensureConfinedDirectory, inspectConfinedPath, removeManagedPathVerified } from '../../managed-path-safety.js';
4
+ const MENU_BAR_EXECUTABLE_SUFFIX = path.join('SKSMenuBar.app', 'Contents', 'MacOS', 'SKSMenuBar');
5
+ export function projectMenuBarDuplicateInstallDirs(input) {
6
+ const root = path.resolve(input.root);
7
+ const home = path.resolve(input.paths.home);
8
+ const canonical = path.resolve(input.paths.install_dir);
9
+ return [...new Set([
10
+ path.join(root, '.codex', 'sks-menubar'),
11
+ path.join(root, '.sneakoscope', 'sks-menubar'),
12
+ path.join(root, '.sneakoscope', 'codex-app', 'sks-menubar'),
13
+ path.join(home, '.sneakoscope', 'sks-menubar'),
14
+ path.join(home, '.sneakoscope', 'codex-app', 'sks-menubar')
15
+ ].map((candidate) => path.resolve(candidate)).filter((candidate) => candidate !== canonical))];
16
+ }
17
+ export async function verifiedProjectMenuBarDuplicateExecutablePaths(input) {
18
+ const root = path.resolve(input.root);
19
+ const home = path.resolve(input.paths.home);
20
+ const executables = [];
21
+ const candidates = await discoverProjectMenuBarDuplicateCandidates(input);
22
+ for (const candidate of candidates.inspected) {
23
+ const confinementRoot = confinementRootForCandidate(root, home, candidate);
24
+ const state = await inspectConfinedPath(confinementRoot, candidate).catch(() => null);
25
+ if (!state?.exists || state.leafSymlink || !state.stat?.isDirectory())
26
+ continue;
27
+ const stamp = await readJson(path.join(candidate, 'build-stamp.json'), null);
28
+ const executable = path.join(candidate, 'SKSMenuBar.app', 'Contents', 'MacOS', 'SKSMenuBar');
29
+ if (stamp?.schema === 'sks.sks-menubar-build-stamp.v2'
30
+ && stamp.codesign_identifier === 'com.sneakoscope.sks-menubar'
31
+ && await exists(executable)) {
32
+ executables.push(executable);
33
+ }
34
+ }
35
+ return executables;
36
+ }
37
+ export async function inspectProjectMenuBarCanonicalState(input) {
38
+ const root = path.resolve(input.root);
39
+ const home = path.resolve(input.paths.home);
40
+ const discoveredCandidates = await discoverProjectMenuBarDuplicateCandidates(input);
41
+ const inspected = discoveredCandidates.inspected;
42
+ const verifiedDuplicates = [];
43
+ const unverifiedCollisions = [];
44
+ const blockers = [];
45
+ for (const candidate of inspected) {
46
+ const confinementRoot = confinementRootForCandidate(root, home, candidate);
47
+ const state = await inspectConfinedPath(confinementRoot, candidate).catch(() => null);
48
+ if (!state) {
49
+ blockers.push(`menubar_project_duplicate_inspection_failed:${candidate}`);
50
+ continue;
51
+ }
52
+ if (!state.exists)
53
+ continue;
54
+ if (state.leafSymlink || !state.stat?.isDirectory()) {
55
+ unverifiedCollisions.push(candidate);
56
+ continue;
57
+ }
58
+ const stamp = await readJson(path.join(candidate, 'build-stamp.json'), null);
59
+ const executable = path.join(candidate, 'SKSMenuBar.app', 'Contents', 'MacOS', 'SKSMenuBar');
60
+ if (stamp?.schema === 'sks.sks-menubar-build-stamp.v2'
61
+ && stamp.codesign_identifier === 'com.sneakoscope.sks-menubar'
62
+ && await exists(executable)) {
63
+ verifiedDuplicates.push(candidate);
64
+ }
65
+ else {
66
+ unverifiedCollisions.push(candidate);
67
+ }
68
+ }
69
+ const candidatePaths = [...new Set([
70
+ ...discoveredCandidates.discovered,
71
+ ...verifiedDuplicates,
72
+ ...unverifiedCollisions
73
+ ])].sort();
74
+ const warnings = candidatePaths.map((candidate) => `menubar_duplicate_candidate_detected:${candidate}`);
75
+ return {
76
+ canonical_only: candidatePaths.length === 0 && blockers.length === 0,
77
+ inspected,
78
+ candidate_paths: candidatePaths,
79
+ verified_duplicates: verifiedDuplicates,
80
+ unverified_collisions: unverifiedCollisions,
81
+ blockers,
82
+ warnings
83
+ };
84
+ }
85
+ export async function cleanupProjectMenuBarDuplicates(input) {
86
+ const root = path.resolve(input.root);
87
+ const canonical = path.resolve(input.paths.install_dir);
88
+ const discovered = await discoverProjectMenuBarDuplicateCandidates(input);
89
+ const retainedCandidates = (input.candidateExecutablePaths || [])
90
+ .map((executable) => duplicateInstallDirForExecutable(executable, canonical))
91
+ .filter((candidate) => Boolean(candidate));
92
+ const inspected = [...new Set([...discovered.inspected, ...retainedCandidates])];
93
+ const removed = [];
94
+ const preserved = [];
95
+ const blockers = [];
96
+ const warnings = [];
97
+ for (const candidate of inspected) {
98
+ const confinementRoot = confinementRootForCandidate(root, input.paths.home, candidate);
99
+ const state = await inspectConfinedPath(confinementRoot, candidate).catch(() => null);
100
+ if (!state) {
101
+ blockers.push(`menubar_project_duplicate_inspection_failed:${candidate}`);
102
+ continue;
103
+ }
104
+ if (!state.exists)
105
+ continue;
106
+ if (state.leafSymlink || !state.stat?.isDirectory()) {
107
+ preserved.push(candidate);
108
+ warnings.push(`menubar_project_duplicate_unmanaged_collision_preserved:${candidate}`);
109
+ continue;
110
+ }
111
+ const stamp = await readJson(path.join(candidate, 'build-stamp.json'), null);
112
+ if (stamp?.schema !== 'sks.sks-menubar-build-stamp.v2'
113
+ || stamp.codesign_identifier !== 'com.sneakoscope.sks-menubar'
114
+ || !await exists(path.join(candidate, 'SKSMenuBar.app', 'Contents', 'MacOS', 'SKSMenuBar'))) {
115
+ preserved.push(candidate);
116
+ warnings.push(`menubar_project_duplicate_unverified_preserved:${candidate}`);
117
+ continue;
118
+ }
119
+ if (isProtectedDuplicateRemovalTarget(candidate, root, input.paths.home)) {
120
+ preserved.push(candidate);
121
+ warnings.push(`menubar_project_duplicate_protected_path_preserved:${candidate}`);
122
+ continue;
123
+ }
124
+ try {
125
+ await removeManagedPathVerified(confinementRoot, candidate);
126
+ removed.push(candidate);
127
+ }
128
+ catch {
129
+ blockers.push(`menubar_project_duplicate_remove_failed:${candidate}`);
130
+ }
131
+ }
132
+ if (preserved.length > 0)
133
+ warnings.push('menubar_strict_canonical_only_unmet');
134
+ let receiptPath = null;
135
+ if (removed.length > 0 || preserved.length > 0 || blockers.length > 0) {
136
+ try {
137
+ await ensureConfinedDirectory(input.paths.install_dir, input.paths.duplicate_receipts_dir);
138
+ receiptPath = path.join(input.paths.duplicate_receipts_dir, `${Date.now()}-${process.pid}.json`);
139
+ await writeJsonAtomic(receiptPath, {
140
+ schema: 'sks.menubar-project-duplicate-cleanup-receipt.v1',
141
+ generated_at: nowIso(),
142
+ canonical_install_dir: canonical,
143
+ project_root: root,
144
+ inspected,
145
+ removed,
146
+ preserved,
147
+ blockers,
148
+ warnings
149
+ }, { mode: 0o600 });
150
+ }
151
+ catch {
152
+ blockers.push('menubar_project_duplicate_receipt_write_failed');
153
+ }
154
+ }
155
+ const canonicalOnly = preserved.length === 0 && blockers.length === 0;
156
+ return {
157
+ schema: 'sks.menubar-project-duplicate-cleanup.v1',
158
+ ok: blockers.length === 0,
159
+ canonical_only: canonicalOnly,
160
+ canonical_install_dir: canonical,
161
+ inspected,
162
+ removed,
163
+ preserved,
164
+ receipt_path: receiptPath,
165
+ blockers,
166
+ warnings
167
+ };
168
+ }
169
+ async function discoverProjectMenuBarDuplicateCandidates(input) {
170
+ const canonical = path.resolve(input.paths.install_dir);
171
+ const fixed = projectMenuBarDuplicateInstallDirs(input);
172
+ const discovered = [...new Set([
173
+ ...await runningMenuBarDuplicateInstallDirs(canonical, input.env || process.env),
174
+ ...await launchAgentDuplicateInstallDirs(input.paths.launch_agent_path, canonical)
175
+ ])].sort();
176
+ return {
177
+ inspected: [...new Set([...fixed, ...discovered])],
178
+ discovered
179
+ };
180
+ }
181
+ async function runningMenuBarDuplicateInstallDirs(canonicalInstallDir, env) {
182
+ const pgrep = await processTool(env, 'SKS_MENUBAR_PGREP', '/usr/bin/pgrep');
183
+ if (!pgrep)
184
+ return [];
185
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
186
+ const result = await runProcess(pgrep, [
187
+ ...(uid === null ? [] : ['-U', String(uid)]),
188
+ '-f',
189
+ 'SKSMenuBar\\.app/Contents/MacOS/SKSMenuBar'
190
+ ], { timeoutMs: 2_000, maxOutputBytes: 32 * 1024 }).catch(() => ({ code: 1, stdout: '' }));
191
+ if (result.code !== 0)
192
+ return [];
193
+ const rows = String(result.stdout || '').split(/\r?\n/).filter(Boolean).map((line) => ({
194
+ line,
195
+ executable: runningExecutablePath(line)
196
+ }));
197
+ const executablePaths = rows.map((row) => row.executable).filter((value) => Boolean(value));
198
+ const unresolvedPids = rows
199
+ .filter((row) => !row.executable)
200
+ .map((row) => Number(row.line.trim()))
201
+ .filter((pid) => Number.isInteger(pid) && pid > 0)
202
+ .slice(0, 128);
203
+ if (unresolvedPids.length > 0) {
204
+ const ps = await processTool(env, 'SKS_MENUBAR_PS', '/bin/ps');
205
+ if (ps) {
206
+ const processList = await runProcess(ps, [
207
+ '-p',
208
+ unresolvedPids.join(','),
209
+ '-o',
210
+ 'pid=,command='
211
+ ], { timeoutMs: 2_000, maxOutputBytes: 64 * 1024 }).catch(() => ({ code: 1, stdout: '' }));
212
+ if (processList.code === 0) {
213
+ executablePaths.push(...String(processList.stdout || '')
214
+ .split(/\r?\n/)
215
+ .map((line) => runningExecutablePath(line))
216
+ .filter((value) => Boolean(value)));
217
+ }
218
+ }
219
+ }
220
+ return [...new Set(executablePaths
221
+ .map((executable) => duplicateInstallDirForExecutable(executable, canonicalInstallDir))
222
+ .filter((candidate) => Boolean(candidate)))].sort();
223
+ }
224
+ async function launchAgentDuplicateInstallDirs(launchAgentPath, canonicalInstallDir) {
225
+ const source = await readText(launchAgentPath, '');
226
+ const block = source.match(/<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/i)?.[1] || '';
227
+ const encoded = block.match(/<string>([\s\S]*?)<\/string>/i)?.[1];
228
+ if (!encoded)
229
+ return [];
230
+ const executable = decodeXml(encoded.trim());
231
+ const candidate = duplicateInstallDirForExecutable(executable, canonicalInstallDir);
232
+ return candidate ? [candidate] : [];
233
+ }
234
+ function runningExecutablePath(line) {
235
+ const command = line.replace(/^\s*\d+\s+/, '').trim();
236
+ const suffixIndex = command.indexOf(MENU_BAR_EXECUTABLE_SUFFIX);
237
+ if (suffixIndex < 0)
238
+ return null;
239
+ const suffixEnd = suffixIndex + MENU_BAR_EXECUTABLE_SUFFIX.length;
240
+ const firstSlash = command.indexOf('/');
241
+ if (firstSlash < 0 || firstSlash > suffixIndex)
242
+ return null;
243
+ return command.slice(firstSlash, suffixEnd);
244
+ }
245
+ function duplicateInstallDirForExecutable(executablePath, canonicalInstallDir) {
246
+ if (!path.isAbsolute(executablePath))
247
+ return null;
248
+ const executable = path.resolve(executablePath);
249
+ const suffix = `${path.sep}${MENU_BAR_EXECUTABLE_SUFFIX}`;
250
+ if (!executable.endsWith(suffix))
251
+ return null;
252
+ if (isWithin(path.resolve(canonicalInstallDir), executable))
253
+ return null;
254
+ return executable.slice(0, -suffix.length);
255
+ }
256
+ function confinementRootForCandidate(rootInput, homeInput, candidate) {
257
+ const root = path.resolve(rootInput);
258
+ const home = path.resolve(homeInput);
259
+ if (isWithin(root, candidate))
260
+ return root;
261
+ if (isWithin(home, candidate))
262
+ return home;
263
+ return path.parse(path.resolve(candidate)).root;
264
+ }
265
+ function isProtectedDuplicateRemovalTarget(candidateInput, rootInput, homeInput) {
266
+ const candidate = path.resolve(candidateInput);
267
+ return candidate === path.resolve(rootInput)
268
+ || candidate === path.resolve(homeInput)
269
+ || candidate === path.parse(candidate).root;
270
+ }
271
+ async function processTool(env, key, fallback) {
272
+ const injected = env.SKS_MENUBAR_TEST_PROCESS_TOOLS === '1' ? env[key] : null;
273
+ if (injected)
274
+ return injected;
275
+ return exists(fallback).then((available) => available ? fallback : null);
276
+ }
277
+ function decodeXml(value) {
278
+ return value
279
+ .replace(/&apos;/g, "'")
280
+ .replace(/&quot;/g, '"')
281
+ .replace(/&gt;/g, '>')
282
+ .replace(/&lt;/g, '<')
283
+ .replace(/&amp;/g, '&');
284
+ }
285
+ function isWithin(root, candidate) {
286
+ const relative = path.relative(root, candidate);
287
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
288
+ }
@@ -8,7 +8,8 @@ import { aggregateFileHashes, createSksMenuBarBuildStamp, menuBarBuildStampsEqua
8
8
  import { MENU_ITEMS, SKS_MENUBAR_LABEL } from './constants.js';
9
9
  import { resolveCodexBundleId, writeDefaultMenuBarConfig } from './config.js';
10
10
  import { applyMenuBarGenerationTransaction, commitMenuBarGenerationTransaction, installGenerationPairs, MenuBarGenerationTransactionError, recoverMenuBarGenerationTransaction, rollbackGenerationPairs } from './generation-transaction.js';
11
- import { launchAgentSource, launchMenuBar, seedMenuBarPreferredPosition } from './launch-agent.js';
11
+ import { launchAgentSource, launchMenuBar, inspectRunningMenuBarProcess, seedMenuBarPreferredPosition, stopMenuBarForReplacement, terminateMenuBarProcesses } from './launch-agent.js';
12
+ import { cleanupProjectMenuBarDuplicates, verifiedProjectMenuBarDuplicateExecutablePaths } from './global-install.js';
12
13
  import { cleanupMacLaunchSecretEnvironment, cleanupRetiredRemoteBridgeLaunchAgent, quarantineRetiredRemoteBridgeBindings } from './migration.js';
13
14
  import { sksMenuBarPaths } from './paths.js';
14
15
  import { infoPlistSource, inspectInstalledResources, loadNativeMenuBarSources, nativeResourceHashes } from './resources.js';
@@ -56,6 +57,12 @@ export async function installSksMenuBar(opts = {}) {
56
57
  const result = baseResult(paths, false, 'planned', status?.ok !== false, status?.installed ? ['menubar_app_present'] : ['menubar_app_install_available'], status?.warnings || []);
57
58
  result.codex_bundle_id = status?.codex_sync.bundle_id || null;
58
59
  result.build_stamp = status?.build_stamp || null;
60
+ if (status) {
61
+ result.installed_version = status.installed_version;
62
+ result.running_process = status.running_process;
63
+ result.menubar_version_probe = status.menubar_version_probe;
64
+ }
65
+ result.duplicate_install_candidates = status?.duplicate_install_candidates || [];
59
66
  result.blockers = status?.blockers || [];
60
67
  result.launch = { requested: false, method: 'skipped', ok: true };
61
68
  return result;
@@ -198,6 +205,19 @@ export async function installSksMenuBar(opts = {}) {
198
205
  ? await inspectSignature(paths.app_path, { ...env, SKS_MENUBAR_CODESIGN: codesign })
199
206
  : { checked: false, identifier: null, ok: false, error: 'app_missing' };
200
207
  const installedInfoPlist = await readText(paths.info_plist_path, '');
208
+ const restartDeferred = sksMenuBarRestartDeferred(env);
209
+ const launchWanted = opts.launch !== false
210
+ && env.SKS_SKIP_SKS_MENUBAR_LAUNCH !== '1'
211
+ && !restartDeferred;
212
+ if (restartDeferred && opts.launch !== false && env.SKS_SKIP_SKS_MENUBAR_LAUNCH !== '1') {
213
+ warnings.push('launch_deferred_until_parent_operation_completes');
214
+ }
215
+ const launchAllowed = path.resolve(paths.home) === realUserHome()
216
+ && !isMenuBarInstallPathUnderTempDir(paths.executable_path, env);
217
+ if (launchWanted && path.resolve(paths.home) !== realUserHome())
218
+ warnings.push('launch_skipped_non_user_home');
219
+ if (launchWanted && isMenuBarInstallPathUnderTempDir(paths.executable_path, env))
220
+ warnings.push('launch_skipped_temp_install');
201
221
  const upToDate = bundleExists
202
222
  && currentAction === actionScript
203
223
  && menuBarBuildStampsEqual(previous, stamp)
@@ -273,6 +293,50 @@ export async function installSksMenuBar(opts = {}) {
273
293
  return blocked(error.blocker, error.message);
274
294
  return blocked('menubar_build_failed', error instanceof Error ? error.message : String(error));
275
295
  }
296
+ }
297
+ const duplicateExecutablePaths = await verifiedProjectMenuBarDuplicateExecutablePaths({
298
+ paths,
299
+ root: paths.root,
300
+ env
301
+ });
302
+ const mustStopCanonical = !upToDate || (launchWanted && launchAllowed);
303
+ let canonicalStopped = false;
304
+ if (mustStopCanonical) {
305
+ const stopped = await stopMenuBarForReplacement({
306
+ launchctl,
307
+ paths,
308
+ executablePaths: [paths.executable_path, ...duplicateExecutablePaths],
309
+ env
310
+ });
311
+ if (!stopped.ok) {
312
+ return blocked(stopped.error || 'menubar_stop_before_replace_failed', 'Menu Bar process shutdown could not be verified before replacement.');
313
+ }
314
+ canonicalStopped = true;
315
+ actions.push('booted out Menu Bar and verified all canonical/project duplicate processes stopped');
316
+ }
317
+ else {
318
+ const duplicateStop = await terminateMenuBarProcesses(duplicateExecutablePaths, env);
319
+ if (!duplicateStop.ok) {
320
+ return blocked('menubar_project_duplicate_stop_failed', `Remaining PIDs: ${duplicateStop.remaining_pids.join(',')}`);
321
+ }
322
+ }
323
+ const duplicateCleanup = await cleanupProjectMenuBarDuplicates({
324
+ paths,
325
+ root: paths.root,
326
+ env,
327
+ candidateExecutablePaths: duplicateExecutablePaths
328
+ });
329
+ warnings.push(...duplicateCleanup.warnings);
330
+ if (!duplicateCleanup.ok) {
331
+ return blocked(duplicateCleanup.blockers[0] || 'menubar_project_duplicate_cleanup_failed', 'Project-local Menu Bar duplicate cleanup could not be completed safely.');
332
+ }
333
+ if (duplicateCleanup.removed.length > 0) {
334
+ actions.push(`removed ${duplicateCleanup.removed.length} verified project-local Menu Bar duplicate(s)`);
335
+ }
336
+ if (duplicateCleanup.receipt_path)
337
+ actions.push(`wrote duplicate cleanup receipt ${duplicateCleanup.receipt_path}`);
338
+ if (!upToDate) {
339
+ const preservingPrevious = await exists(paths.app_path);
276
340
  try {
277
341
  transaction = await applyMenuBarGenerationTransaction({
278
342
  purpose: 'install', journalPath: paths.install_transaction_path, pairs: installPairs, env
@@ -325,23 +389,19 @@ export async function installSksMenuBar(opts = {}) {
325
389
  transaction = committed;
326
390
  }
327
391
  }
328
- const restartDeferred = sksMenuBarRestartDeferred(env);
329
- const launchWanted = opts.launch !== false
330
- && env.SKS_SKIP_SKS_MENUBAR_LAUNCH !== '1'
331
- && !restartDeferred;
332
- if (restartDeferred && opts.launch !== false && env.SKS_SKIP_SKS_MENUBAR_LAUNCH !== '1') {
333
- warnings.push('launch_deferred_until_parent_operation_completes');
334
- }
335
- const launchAllowed = path.resolve(paths.home) === realUserHome() && !isMenuBarInstallPathUnderTempDir(paths.executable_path, env);
336
- if (launchWanted && path.resolve(paths.home) !== realUserHome())
337
- warnings.push('launch_skipped_non_user_home');
338
- if (launchWanted && isMenuBarInstallPathUnderTempDir(paths.executable_path, env))
339
- warnings.push('launch_skipped_temp_install');
340
392
  let launch = { requested: false, method: 'skipped', ok: true };
341
393
  if (launchWanted && launchAllowed) {
342
394
  if (await seedMenuBarPreferredPosition(env))
343
395
  actions.push('seeded SKS menu bar preferred position');
344
- launch = await launchMenuBar({ launchctl, open, paths, env });
396
+ launch = await launchMenuBar({
397
+ launchctl,
398
+ open,
399
+ paths,
400
+ env,
401
+ alreadyStopped: canonicalStopped,
402
+ expectedVersion: stamp.package_version,
403
+ executablePaths: [paths.executable_path, ...duplicateExecutablePaths]
404
+ });
345
405
  }
346
406
  const rollbackCandidateExists = !upToDate && await exists(paths.backup_app_path);
347
407
  const rollback = shouldAutoRollbackMenuBarLaunch({ launch, upToDate, rollbackCandidateExists })
@@ -362,6 +422,11 @@ export async function installSksMenuBar(opts = {}) {
362
422
  : stamp;
363
423
  result.secret_env_cleanup = cleanup;
364
424
  result.launch = launch;
425
+ result.installed_version = result.build_stamp?.package_version || null;
426
+ if (launch.version_probe) {
427
+ result.menubar_version_probe = launch.version_probe;
428
+ result.running_process = await inspectRunningMenuBarProcess(paths, launch.version_probe.pid, env);
429
+ }
365
430
  result.rollback = rollback;
366
431
  result.transaction = transaction;
367
432
  result.blockers = ok ? [] : [