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.
- package/README.md +47 -23
- package/config/skills-hash-ledger.v1.json +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +55 -13
- package/dist/bin/sks-dispatch.js +1 -1
- package/dist/cli/command-manifest-lite.js +4 -0
- package/dist/cli/command-registry.js +4 -0
- package/dist/cli/install-helpers-codex-lb-chain.js +187 -0
- package/dist/cli/install-helpers-codex-lb-config.js +74 -8
- package/dist/cli/install-helpers.js +89 -44
- package/dist/commands/codex-lb.js +117 -19
- package/dist/commands/doctor.js +697 -37
- package/dist/config/skills-manifest.json +3 -2
- package/dist/core/agents/agent-runner-codex-exec.js +5 -3
- package/dist/core/codex/codex-config-guard.js +238 -52
- package/dist/core/codex-adapter.js +5 -3
- package/dist/core/codex-app/menubar/constants.js +10 -1
- package/dist/core/codex-app/menubar/global-install.js +288 -0
- package/dist/core/codex-app/menubar/installer.js +79 -14
- package/dist/core/codex-app/menubar/launch-agent.js +267 -45
- package/dist/core/codex-app/menubar/paths.js +3 -0
- package/dist/core/codex-app/menubar/rollback.js +55 -7
- package/dist/core/codex-app/menubar/status.js +75 -4
- package/dist/core/codex-control/codex-0139-image-path-real-probe.js +3 -1
- package/dist/core/codex-control/codex-0139-web-search-probe.js +3 -1
- package/dist/core/codex-control/codex-app-server-runtime-env.js +13 -4
- package/dist/core/codex-control/codex-app-server-v2-client.js +3 -3
- package/dist/core/codex-exec-output-schema.js +4 -1
- package/dist/core/codex-lb/codex-lb-env.js +63 -72
- package/dist/core/codex-lb/codex-lb-setup.js +15 -2
- package/dist/core/codex-lb/desktop-center-credentials.js +517 -58
- package/dist/core/codex-lb/desktop-controller.js +153 -24
- package/dist/core/codex-lb/routing-truth.js +327 -0
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/commands/basic-cli.js +7 -1
- package/dist/core/commands/qa-loop-command.js +3 -1
- package/dist/core/commands/telegram-command.js +241 -0
- package/dist/core/config-adopt/config-adopt-command.js +35 -0
- package/dist/core/config-adopt/config-adopt.js +346 -0
- package/dist/core/config-adopt/index.js +3 -0
- package/dist/core/doctor/command-alias-cleanup.js +4 -1
- package/dist/core/doctor/current-project-guidance-nested.js +37 -6
- package/dist/core/doctor/current-project-guidance.js +11 -6
- package/dist/core/doctor/doctor-readiness-matrix.js +14 -0
- package/dist/core/doctor/retired-managed-residue-private.js +1 -1
- package/dist/core/doctor/sks-menubar-doctor.js +4 -0
- package/dist/core/feature-fixtures.js +8 -0
- package/dist/core/hooks-runtime/hook-context.js +9 -0
- package/dist/core/hooks-runtime/managed-skill-admission.js +54 -4
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +69 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +2 -2
- package/dist/core/hooks-runtime/subagent-skill-availability-evidence.js +3 -3
- package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +43 -5
- package/dist/core/hooks-runtime/subagent-skill-availability.js +64 -30
- package/dist/core/hooks-runtime/subagent-stop-hook.js +15 -2
- package/dist/core/hooks-runtime.js +148 -4
- package/dist/core/imagegen/imagegen-capability.js +46 -0
- package/dist/core/imagegen/require-imagegen.js +2 -2
- package/dist/core/init/skills.js +17 -3
- package/dist/core/install/installed-package-smoke.js +8 -2
- package/dist/core/naruto/hardware-capacity-probe.js +9 -3
- package/dist/core/naruto/naruto-concurrency-governor.js +68 -24
- package/dist/core/perf/import-graph-budget.js +0 -1
- package/dist/core/product-design-app-server.js +13 -1
- package/dist/core/release/package-size-budget.js +2 -2
- package/dist/core/release/release-pack-receipt.js +1 -2
- package/dist/core/routes/dollar-manifest-lite.js +1 -1
- package/dist/core/routes.js +1 -1
- package/dist/core/security/private-credential-file.js +209 -0
- package/dist/core/subagents/agent-catalog.js +17 -32
- package/dist/core/subagents/naruto-help-contract.js +5 -5
- package/dist/core/subagents/official-subagent-config.js +16 -6
- package/dist/core/subagents/official-subagent-preparation.js +34 -15
- package/dist/core/subagents/official-subagent-prompt.js +4 -4
- package/dist/core/subagents/official-subagent-runner.js +27 -4
- package/dist/core/subagents/thread-budget.js +19 -12
- package/dist/core/subagents/wave-lifecycle.js +18 -6
- package/dist/core/telegram/access.js +54 -0
- package/dist/core/telegram/client.js +144 -0
- package/dist/core/telegram/confirmation.js +37 -0
- package/dist/core/telegram/doctor.js +151 -0
- package/dist/core/telegram/index.js +9 -0
- package/dist/core/telegram/keychain.js +265 -0
- package/dist/core/telegram/liveness.js +109 -0
- package/dist/core/telegram/poller.js +90 -0
- package/dist/core/telegram/redaction.js +15 -0
- package/dist/core/telegram/types.js +1 -0
- package/dist/core/update/update-diagnostics.js +52 -0
- package/dist/core/update/update-migration-state.js +5 -1
- package/dist/core/update/update-operation.js +2 -1
- package/dist/core/update/update-stage-diagnostics.js +38 -0
- package/dist/core/update/update-status.js +2 -9
- package/dist/core/update-check.js +147 -31
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-launcher.js +23 -4
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +13 -0
- package/dist/native/sks-menubar/Sources/AppIdentity.swift +7 -0
- package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +3 -2
- package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +115 -0
- package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +17 -0
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +190 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +100 -59
- package/dist/native/sks-menubar/Sources/SingletonInstanceGuard.swift +328 -0
- package/dist/native/sks-menubar/Sources/TelegramPrivateFileStore.swift +320 -0
- package/dist/native/sks-menubar/Sources/TelegramPrivateFileSupport.swift +303 -0
- package/dist/native/sks-menubar/Sources/TelegramProcessGateway.swift +238 -0
- package/dist/native/sks-menubar/Sources/TelegramRuntimeSupport.swift +56 -0
- package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
- package/dist/native/sks-menubar/Sources/TelegramTransport.swift +474 -0
- package/dist/native/sks-menubar/Sources/main.swift +15 -1
- package/dist/native/sks-menubar/Tests/OperationCoordinatorTests.swift +4 -3
- package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +129 -0
- package/dist/native/sks-menubar/Tests/SingletonInstanceGuardTests.swift +183 -0
- package/dist/native/sks-menubar/Tests/TelegramRuntimeSmokeTests.swift +267 -0
- package/dist/native/sks-menubar/Tests/TelegramRuntimeTests.swift +222 -0
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +1 -1
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +1 -1
- package/dist/scripts/current-command-surface-check.js +2 -4
- package/dist/scripts/official-subagent-workflow-check.js +1 -1
- package/dist/scripts/packlist-performance-check.js +1 -2
- 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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',
|