sneakoscope 5.1.2 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/commands/doctor.js +32 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agents/agent-lifecycle.js +5 -3
- package/dist/core/agents/agent-orchestrator.js +14 -2
- package/dist/core/agents/agent-proof-evidence.js +16 -0
- package/dist/core/agents/native-cli-worker.js +0 -1
- package/dist/core/bench.js +24 -2
- package/dist/core/codex-app/sks-menubar.js +526 -133
- package/dist/core/commands/agent-command.js +35 -1
- package/dist/core/commands/basic-cli.js +61 -4
- package/dist/core/commands/command-utils.js +14 -4
- package/dist/core/commands/goal-command.js +3 -0
- package/dist/core/commands/image-ux-review-command.js +4 -1
- package/dist/core/commands/mad-sks-command.js +10 -1
- package/dist/core/commands/menubar-command.js +2 -1
- package/dist/core/commands/naruto-command.js +12 -7
- package/dist/core/commands/ppt-command.js +40 -1
- package/dist/core/commands/qa-loop-command.js +4 -1
- package/dist/core/commands/release-command.js +3 -3
- package/dist/core/commands/research-command.js +4 -0
- package/dist/core/commands/route-success-helpers.js +2 -0
- package/dist/core/commands/run-command.js +22 -5
- package/dist/core/commands/seo-command.js +31 -4
- package/dist/core/db-safety.js +8 -1
- package/dist/core/doctor/doctor-dirty-planner.js +51 -7
- package/dist/core/feature-fixture-executor.js +262 -0
- package/dist/core/feature-fixtures.js +95 -52
- package/dist/core/fsx.js +1 -1
- package/dist/core/loops/loop-decomposer.js +4 -3
- package/dist/core/loops/loop-planner.js +7 -3
- package/dist/core/loops/loop-worker-runtime.js +6 -0
- package/dist/core/naruto/naruto-real-worker-child.js +1 -1
- package/dist/core/pipeline-internals/runtime-gates.js +39 -3
- package/dist/core/proof/selftest-proof-fixtures.js +8 -0
- package/dist/core/providers/glm/naruto/glm-naruto-requirement-coverage.js +18 -1
- package/dist/core/providers/glm/naruto/glm-naruto-requirement-ledger.js +28 -12
- package/dist/core/questions.js +12 -10
- package/dist/core/release/release-gate-node.js +2 -0
- package/dist/core/routes.js +11 -0
- package/dist/core/stop-gate/stop-gate-check.js +4 -0
- package/dist/core/stop-gate/stop-gate-writer.js +4 -0
- package/dist/core/trust-kernel/trust-report.js +10 -1
- package/dist/core/update/update-migration-state.js +23 -3
- package/dist/core/version.js +1 -1
- package/dist/core/work-order-ledger.js +60 -0
- package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
- package/dist/scripts/doctor-context7-mcp-repair-blackbox.js +16 -0
- package/dist/scripts/doctor-fix-production-blackbox.js +26 -0
- package/dist/scripts/doctor-startup-config-repair-blackbox.js +13 -0
- package/dist/scripts/doctor-supabase-mcp-repair-blackbox.js +14 -0
- package/dist/scripts/gate-policy-audit-check.js +2 -2
- package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
- package/dist/scripts/packlist-performance-check.js +7 -1
- package/dist/scripts/project-skill-dedupe-blackbox.js +36 -0
- package/dist/scripts/release-dag-full-coverage-check.js +1 -1
- package/dist/scripts/release-gate-existence-audit.js +1 -1
- package/dist/scripts/release-gate-planner.js +1 -1
- package/dist/scripts/release-gate-script-parity-check.js +44 -16
- package/dist/scripts/release-metadata-1-19-check.js +2 -2
- package/dist/scripts/sks-menubar-install-check.js +57 -20
- package/dist/scripts/supabase-secret-preservation-blackbox.js +29 -0
- package/dist/scripts/update-preserves-supabase-keys-blackbox.js +27 -0
- package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
- package/package.json +12 -65
- package/schemas/release/release-gate-node.schema.json +1 -0
- package/bench/tasks/t01-off-by-one/repo/package.json +0 -9
- package/bench/tasks/t01-off-by-one/repo/src/pagination.js +0 -5
- package/bench/tasks/t01-off-by-one/repo/test.js +0 -5
- package/bench/tasks/t01-off-by-one/task.json +0 -13
- package/bench/tasks/t02-signature-cochange/repo/package.json +0 -9
- package/bench/tasks/t02-signature-cochange/repo/src/admin.js +0 -5
- package/bench/tasks/t02-signature-cochange/repo/src/audit.js +0 -5
- package/bench/tasks/t02-signature-cochange/repo/src/labels.js +0 -3
- package/bench/tasks/t02-signature-cochange/repo/src/profile.js +0 -5
- package/bench/tasks/t02-signature-cochange/repo/test.js +0 -9
- package/bench/tasks/t02-signature-cochange/task.json +0 -12
- package/bench/tasks/t03-type-puzzle/repo/package.json +0 -9
- package/bench/tasks/t03-type-puzzle/repo/src/config.js +0 -4
- package/bench/tasks/t03-type-puzzle/repo/test.js +0 -6
- package/bench/tasks/t03-type-puzzle/task.json +0 -13
- package/bench/tasks/t04-refactor-preserve/repo/package.json +0 -9
- package/bench/tasks/t04-refactor-preserve/repo/src/cart.js +0 -8
- package/bench/tasks/t04-refactor-preserve/repo/test.js +0 -4
- package/bench/tasks/t04-refactor-preserve/task.json +0 -12
- package/bench/tasks/t05-performance/repo/package.json +0 -9
- package/bench/tasks/t05-performance/repo/src/pairs.js +0 -9
- package/bench/tasks/t05-performance/repo/test.js +0 -11
- package/bench/tasks/t05-performance/task.json +0 -12
- package/bench/tasks/t06-mistake-rule/repo/package.json +0 -9
- package/bench/tasks/t06-mistake-rule/repo/src/loader.js +0 -6
- package/bench/tasks/t06-mistake-rule/repo/test.js +0 -7
- package/bench/tasks/t06-mistake-rule/task.json +0 -12
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { ensureDir, exists, PACKAGE_VERSION, readJson, readText, runProcess, sha256, which, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
6
|
+
import { findCodexApp } from '../codex-app.js';
|
|
6
7
|
import { withHeartbeat } from '../../cli/cli-theme.js';
|
|
7
8
|
export const SKS_MENUBAR_LABEL = 'com.sneakoscope.sks-menubar';
|
|
8
9
|
const LABEL = SKS_MENUBAR_LABEL;
|
|
@@ -20,6 +21,7 @@ const MENU_ITEMS = [
|
|
|
20
21
|
'Open Dashboard',
|
|
21
22
|
'Open Codex Settings',
|
|
22
23
|
'Restart Codex',
|
|
24
|
+
'View Last Log',
|
|
23
25
|
'Quit SKS Menu'
|
|
24
26
|
];
|
|
25
27
|
export function sksMenuBarPaths(homeInput, rootInput) {
|
|
@@ -43,10 +45,13 @@ export function sksMenuBarPaths(homeInput, rootInput) {
|
|
|
43
45
|
info_plist_path: path.join(contentsPath, 'Info.plist'),
|
|
44
46
|
action_script_path: path.join(installDir, 'sks-menubar-action.sh'),
|
|
45
47
|
build_stamp_path: path.join(installDir, 'build-stamp.json'),
|
|
48
|
+
config_path: path.join(installDir, 'config.json'),
|
|
46
49
|
launch_agent_path: path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`),
|
|
47
50
|
report_path: path.join(root, '.sneakoscope', 'reports', 'sks-menubar.json'),
|
|
48
51
|
stdout_log_path: path.join(installDir, 'menubar.out.log'),
|
|
49
|
-
stderr_log_path: path.join(installDir, 'menubar.err.log')
|
|
52
|
+
stderr_log_path: path.join(installDir, 'menubar.err.log'),
|
|
53
|
+
logs_dir: path.join(installDir, 'logs'),
|
|
54
|
+
last_action_log_path: path.join(installDir, 'logs', 'last-action.log')
|
|
50
55
|
};
|
|
51
56
|
}
|
|
52
57
|
export async function installSksMenuBar(opts = {}) {
|
|
@@ -57,6 +62,7 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
57
62
|
const warnings = [];
|
|
58
63
|
const nextActions = defaultNextActions();
|
|
59
64
|
let secretEnvCleanup;
|
|
65
|
+
let codexBundleId = null;
|
|
60
66
|
if (process.platform !== 'darwin') {
|
|
61
67
|
const result = {
|
|
62
68
|
schema: 'sks.codex-app-sks-menubar.v1',
|
|
@@ -69,7 +75,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
69
75
|
launch_agent_path: null,
|
|
70
76
|
action_script_path: null,
|
|
71
77
|
build_stamp_path: null,
|
|
78
|
+
config_path: null,
|
|
72
79
|
report_path: apply ? paths.report_path : null,
|
|
80
|
+
codex_bundle_id: null,
|
|
73
81
|
menu_items: MENU_ITEMS,
|
|
74
82
|
actions: [],
|
|
75
83
|
launch: { requested: false, method: 'none', ok: true },
|
|
@@ -86,9 +94,10 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
86
94
|
const installed = await exists(paths.executable_path);
|
|
87
95
|
const launchAgent = await exists(paths.launch_agent_path);
|
|
88
96
|
const status = await inspectSksMenuBarStatus({ home: paths.home, root: paths.root }).catch(() => null);
|
|
97
|
+
const statusWarnings = [...(status?.warnings || []), ...(launchAgent ? [] : ['launch_agent_not_installed_yet'])];
|
|
89
98
|
return {
|
|
90
99
|
schema: 'sks.codex-app-sks-menubar.v1',
|
|
91
|
-
ok: true,
|
|
100
|
+
ok: status ? status.ok === true : true,
|
|
92
101
|
apply,
|
|
93
102
|
status: 'planned',
|
|
94
103
|
platform: process.platform,
|
|
@@ -97,7 +106,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
97
106
|
launch_agent_path: paths.launch_agent_path,
|
|
98
107
|
action_script_path: paths.action_script_path,
|
|
99
108
|
build_stamp_path: paths.build_stamp_path,
|
|
109
|
+
config_path: paths.config_path,
|
|
100
110
|
report_path: paths.report_path,
|
|
111
|
+
codex_bundle_id: status?.codex_sync.bundle_id || null,
|
|
101
112
|
menu_items: MENU_ITEMS,
|
|
102
113
|
actions: installed ? ['menubar_app_present'] : ['menubar_app_install_available'],
|
|
103
114
|
launch: {
|
|
@@ -116,11 +127,12 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
116
127
|
build_stamp: status?.build_stamp || null,
|
|
117
128
|
tcc_automation_status: 'unknown',
|
|
118
129
|
next_actions: launchAgent ? defaultNextActions() : ['Run: sks menubar install'],
|
|
119
|
-
blockers: [],
|
|
120
|
-
warnings:
|
|
130
|
+
blockers: status?.blockers || [],
|
|
131
|
+
warnings: statusWarnings
|
|
121
132
|
};
|
|
122
133
|
}
|
|
123
134
|
await ensureDir(paths.install_dir);
|
|
135
|
+
await ensureDir(paths.logs_dir);
|
|
124
136
|
await ensureDir(path.dirname(paths.launch_agent_path));
|
|
125
137
|
secretEnvCleanup = await cleanupMacLaunchSecretEnvironment({ env }).catch((err) => ({
|
|
126
138
|
ok: false,
|
|
@@ -139,9 +151,17 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
139
151
|
const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || await fallbackTool('/usr/bin/open');
|
|
140
152
|
const codesign = env.SKS_MENUBAR_CODESIGN || await which('codesign').catch(() => null) || await fallbackTool('/usr/bin/codesign');
|
|
141
153
|
const xcodeSelect = env.SKS_MENUBAR_XCODE_SELECT || await which('xcode-select').catch(() => null) || await fallbackTool('/usr/bin/xcode-select');
|
|
154
|
+
const clt = await xcodeCltStatus(xcodeSelect);
|
|
155
|
+
if (!clt.ok)
|
|
156
|
+
return await blockedResult('xcode_clt_missing', clt.error || 'Xcode Command Line Tools missing');
|
|
142
157
|
if (!swiftc)
|
|
143
158
|
return await blockedResult('swiftc_missing', 'swiftc not found');
|
|
144
159
|
const swiftcVersion = await toolVersion(swiftc, ['--version']);
|
|
160
|
+
codexBundleId = await resolveCodexBundleId({ home: paths.home, env, warnings });
|
|
161
|
+
if (!codexBundleId)
|
|
162
|
+
warnings.push('codex_app_bundle_id_unresolved');
|
|
163
|
+
const config = await writeDefaultMenuBarConfig(paths.config_path, codexBundleId);
|
|
164
|
+
actions.push(`wrote ${paths.config_path}`);
|
|
145
165
|
const target = await resolveSksEntryForInstall({
|
|
146
166
|
...(opts.sksEntry ? { explicit: opts.sksEntry } : {}),
|
|
147
167
|
root: paths.root,
|
|
@@ -162,6 +182,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
162
182
|
const swiftSource = swiftMenuSource({
|
|
163
183
|
actionScriptPath: paths.action_script_path,
|
|
164
184
|
buildStampPath: paths.build_stamp_path,
|
|
185
|
+
configPath: paths.config_path,
|
|
186
|
+
lastActionLogPath: paths.last_action_log_path,
|
|
187
|
+
codexBundleId: config.codex_bundle_id,
|
|
165
188
|
packageVersion: PACKAGE_VERSION
|
|
166
189
|
});
|
|
167
190
|
const infoPlist = infoPlistSource(PACKAGE_VERSION);
|
|
@@ -192,17 +215,25 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
192
215
|
else {
|
|
193
216
|
if (!target.used_previous_script && await readText(paths.action_script_path, '') !== actionScript) {
|
|
194
217
|
await writeTextAtomic(paths.action_script_path, actionScript);
|
|
195
|
-
await fs.chmod(paths.action_script_path, 0o755);
|
|
196
218
|
actions.push(`wrote ${paths.action_script_path}`);
|
|
197
219
|
}
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
}
|
|
221
|
+
// The Swift app executes the action script DIRECTLY (Process.executableURL points at the
|
|
222
|
+
// script itself), so a lost executable bit breaks every menu action even when the script
|
|
223
|
+
// content is current. Re-assert the bit on every install run — including the up-to-date
|
|
224
|
+
// fast path, which previously never touched permissions and therefore could never repair
|
|
225
|
+
// a 0644 script — and surface chmod failures instead of swallowing them.
|
|
226
|
+
if (await exists(paths.action_script_path)) {
|
|
227
|
+
const previouslyExecutable = await fs.access(paths.action_script_path, fs.constants.X_OK).then(() => true).catch(() => false);
|
|
228
|
+
const chmodError = await fs.chmod(paths.action_script_path, 0o755).then(() => null).catch((err) => (err?.message ? String(err.message) : String(err)));
|
|
229
|
+
if (chmodError) {
|
|
230
|
+
warnings.push(`action_script_chmod_failed:${chmodError}`);
|
|
231
|
+
}
|
|
232
|
+
else if (!previouslyExecutable) {
|
|
233
|
+
actions.push('restored action script executable bit');
|
|
200
234
|
}
|
|
201
235
|
}
|
|
202
236
|
if (!stampMatches && !binaryStable) {
|
|
203
|
-
const clt = await xcodeCltStatus(xcodeSelect);
|
|
204
|
-
if (!clt.ok)
|
|
205
|
-
return await blockedResult('xcode_clt_missing', clt.error || 'Xcode Command Line Tools missing');
|
|
206
237
|
try {
|
|
207
238
|
await buildMenuBarAppAtomically({
|
|
208
239
|
paths,
|
|
@@ -277,7 +308,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
277
308
|
launch_agent_path: paths.launch_agent_path,
|
|
278
309
|
action_script_path: paths.action_script_path,
|
|
279
310
|
build_stamp_path: paths.build_stamp_path,
|
|
311
|
+
config_path: paths.config_path,
|
|
280
312
|
report_path: paths.report_path,
|
|
313
|
+
codex_bundle_id: codexBundleId,
|
|
281
314
|
menu_items: MENU_ITEMS,
|
|
282
315
|
actions,
|
|
283
316
|
launch,
|
|
@@ -285,10 +318,7 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
285
318
|
build_stamp: stamp,
|
|
286
319
|
tcc_automation_status: 'unknown',
|
|
287
320
|
secret_env_cleanup: secretEnvCleanup,
|
|
288
|
-
next_actions:
|
|
289
|
-
...nextActions,
|
|
290
|
-
'If Terminal automation was denied: System Settings > Privacy & Security > Automation > SKS Menu Bar > Terminal.'
|
|
291
|
-
],
|
|
321
|
+
next_actions: nextActions,
|
|
292
322
|
blockers: ok ? [] : [launch.error || 'sks_menubar_launch_failed'],
|
|
293
323
|
warnings
|
|
294
324
|
};
|
|
@@ -306,7 +336,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
306
336
|
launch_agent_path: paths.launch_agent_path,
|
|
307
337
|
action_script_path: paths.action_script_path,
|
|
308
338
|
build_stamp_path: paths.build_stamp_path,
|
|
339
|
+
config_path: paths.config_path,
|
|
309
340
|
report_path: paths.report_path,
|
|
341
|
+
codex_bundle_id: typeof codexBundleId === 'string' ? codexBundleId : null,
|
|
310
342
|
menu_items: MENU_ITEMS,
|
|
311
343
|
actions,
|
|
312
344
|
launch: { requested: false, method: 'none', ok: false, error: detail || reason },
|
|
@@ -358,6 +390,99 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
|
|
|
358
390
|
next_actions: ['Rotate CODEX_LB_API_KEY and OPENROUTER_API_KEY if they were previously exposed in launchd.']
|
|
359
391
|
};
|
|
360
392
|
}
|
|
393
|
+
async function writeDefaultMenuBarConfig(configPath, codexBundleId) {
|
|
394
|
+
const previous = await readMenuBarConfig(configPath);
|
|
395
|
+
const config = {
|
|
396
|
+
schema: 'sks.sks-menubar-config.v1',
|
|
397
|
+
codex_bundle_id: codexBundleId,
|
|
398
|
+
quit_with_codex: previous.quit_with_codex === true
|
|
399
|
+
};
|
|
400
|
+
await writeJsonAtomic(configPath, config);
|
|
401
|
+
return config;
|
|
402
|
+
}
|
|
403
|
+
async function readMenuBarConfig(configPath) {
|
|
404
|
+
const config = await readJson(configPath, null);
|
|
405
|
+
return {
|
|
406
|
+
schema: 'sks.sks-menubar-config.v1',
|
|
407
|
+
codex_bundle_id: typeof config?.codex_bundle_id === 'string' && config.codex_bundle_id.trim()
|
|
408
|
+
? config.codex_bundle_id.trim()
|
|
409
|
+
: null,
|
|
410
|
+
quit_with_codex: config?.quit_with_codex === true
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
async function resolveCodexBundleId(input) {
|
|
414
|
+
if (process.platform !== 'darwin')
|
|
415
|
+
return null;
|
|
416
|
+
const appPath = await findCodexApp({ home: input.home, env: input.env }).catch(() => null);
|
|
417
|
+
if (!appPath) {
|
|
418
|
+
input.warnings.push('codex_app_not_found_for_bundle_sync');
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const mdls = input.env.SKS_MENUBAR_MDLS || await which('mdls').catch(() => null) || await fallbackTool('/usr/bin/mdls');
|
|
422
|
+
if (mdls) {
|
|
423
|
+
const result = await runProcess(mdls, ['-name', 'kMDItemCFBundleIdentifier', '-raw', appPath], {
|
|
424
|
+
timeoutMs: 3_000,
|
|
425
|
+
maxOutputBytes: 8 * 1024
|
|
426
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
427
|
+
const value = String(result.stdout || '').trim();
|
|
428
|
+
if (result.code === 0 && value && value !== '(null)' && value !== 'null')
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
const defaults = input.env.SKS_MENUBAR_DEFAULTS || await which('defaults').catch(() => null) || await fallbackTool('/usr/bin/defaults');
|
|
432
|
+
if (defaults) {
|
|
433
|
+
const result = await runProcess(defaults, ['read', path.join(appPath, 'Contents', 'Info'), 'CFBundleIdentifier'], {
|
|
434
|
+
timeoutMs: 3_000,
|
|
435
|
+
maxOutputBytes: 8 * 1024
|
|
436
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
437
|
+
const value = String(result.stdout || '').trim();
|
|
438
|
+
if (result.code === 0 && value)
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
export async function smokeSksMenuBarAction(actionScriptPath) {
|
|
444
|
+
if (!(await exists(actionScriptPath)))
|
|
445
|
+
return { ok: false, code: null, output: null, versionDetected: false, executable: false };
|
|
446
|
+
// The Swift app runs the script directly (which requires the executable bit), so the smoke
|
|
447
|
+
// check must invoke it the same way. Running it via `/bin/zsh <script>` — as this check used
|
|
448
|
+
// to — succeeds even when +x is missing, which let doctor/status report a healthy action
|
|
449
|
+
// target while the menu bar itself was showing "action script broken".
|
|
450
|
+
const executable = await fs.access(actionScriptPath, fs.constants.X_OK).then(() => true).catch(() => false);
|
|
451
|
+
if (!executable) {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
code: null,
|
|
455
|
+
output: 'action script is not executable (missing +x); the menu bar app cannot run it',
|
|
456
|
+
versionDetected: false,
|
|
457
|
+
executable: false
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const result = await runProcess(actionScriptPath, ['version'], {
|
|
461
|
+
timeoutMs: 5_000,
|
|
462
|
+
maxOutputBytes: 16 * 1024
|
|
463
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
464
|
+
const output = String(`${result.stdout || ''}\n${result.stderr || ''}`).trim();
|
|
465
|
+
const versionDetected = /\b(?:sks|sneakoscope)?\s*v?\d+\.\d+\.\d+\b/i.test(output);
|
|
466
|
+
return {
|
|
467
|
+
ok: result.code === 0 && versionDetected,
|
|
468
|
+
code: result.code,
|
|
469
|
+
output: output ? output.slice(0, 700) : null,
|
|
470
|
+
versionDetected,
|
|
471
|
+
executable: true
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function isCodexAppRunningByBundleId(bundleId, env = process.env) {
|
|
475
|
+
if (process.platform !== 'darwin' || !bundleId)
|
|
476
|
+
return false;
|
|
477
|
+
const osascript = env.SKS_MENUBAR_OSASCRIPT || await which('osascript').catch(() => null) || await fallbackTool('/usr/bin/osascript');
|
|
478
|
+
if (!osascript)
|
|
479
|
+
return false;
|
|
480
|
+
const result = await runProcess(osascript, ['-e', `application id "${bundleId.replace(/"/g, '\\"')}" is running`], {
|
|
481
|
+
timeoutMs: 2_000,
|
|
482
|
+
maxOutputBytes: 8 * 1024
|
|
483
|
+
}).catch(() => ({ code: 1, stdout: '', stderr: '' }));
|
|
484
|
+
return result.code === 0 && String(result.stdout || '').trim().toLowerCase() === 'true';
|
|
485
|
+
}
|
|
361
486
|
export async function inspectSksMenuBarStatus(opts = {}) {
|
|
362
487
|
const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
|
|
363
488
|
const installed = await exists(paths.executable_path);
|
|
@@ -367,6 +492,16 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
367
492
|
const sksEntry = shellAssignment(actionText, 'SKS_ENTRY');
|
|
368
493
|
const nodeExists = nodeBin ? await isExecutable(nodeBin) : false;
|
|
369
494
|
const sksEntryExists = sksEntry ? await exists(sksEntry) : false;
|
|
495
|
+
const actionSmoke = await smokeSksMenuBarAction(paths.action_script_path);
|
|
496
|
+
const config = await readMenuBarConfig(paths.config_path);
|
|
497
|
+
const codexRunning = config.codex_bundle_id ? await isCodexAppRunningByBundleId(config.codex_bundle_id, opts.env) : null;
|
|
498
|
+
const codexSync = {
|
|
499
|
+
ok: Boolean(config.codex_bundle_id),
|
|
500
|
+
bundle_id: config.codex_bundle_id,
|
|
501
|
+
codex_running: codexRunning,
|
|
502
|
+
icon_visible_expected: config.codex_bundle_id ? codexRunning === true : true,
|
|
503
|
+
warning: config.codex_bundle_id ? null : 'codex_sync_disabled'
|
|
504
|
+
};
|
|
370
505
|
const buildStamp = await readJson(paths.build_stamp_path, null);
|
|
371
506
|
const launchd = await inspectLaunchdService(opts.env);
|
|
372
507
|
const signature = await inspectSignature(paths.app_path, opts.env);
|
|
@@ -374,10 +509,16 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
374
509
|
const warnings = [];
|
|
375
510
|
if (!installed)
|
|
376
511
|
blockers.push('menubar_app_missing');
|
|
377
|
-
if (installed && !
|
|
378
|
-
blockers.push('
|
|
512
|
+
if (installed && launchd.checked && !launchd.ok)
|
|
513
|
+
blockers.push('launchd_not_running');
|
|
514
|
+
if (installed && !actionSmoke.executable)
|
|
515
|
+
blockers.push('action_script_not_executable');
|
|
516
|
+
if (installed && !actionSmoke.ok)
|
|
517
|
+
blockers.push('action_script_smoke_failed');
|
|
379
518
|
if (installed && signature.checked && !signature.ok)
|
|
380
519
|
warnings.push('codesign_identifier_unexpected');
|
|
520
|
+
if (!codexSync.ok)
|
|
521
|
+
warnings.push('codex_sync_disabled');
|
|
381
522
|
if (buildStamp?.package_version && buildStamp.package_version !== PACKAGE_VERSION)
|
|
382
523
|
warnings.push('build_stamp_package_version_mismatch');
|
|
383
524
|
return {
|
|
@@ -393,8 +534,13 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
393
534
|
node_exists: nodeExists,
|
|
394
535
|
sks_entry: sksEntry,
|
|
395
536
|
sks_entry_exists: sksEntryExists,
|
|
396
|
-
|
|
537
|
+
smoke_code: actionSmoke.code,
|
|
538
|
+
smoke_output: actionSmoke.output,
|
|
539
|
+
version_detected: actionSmoke.versionDetected,
|
|
540
|
+
executable: actionSmoke.executable,
|
|
541
|
+
ok: actionSmoke.ok
|
|
397
542
|
},
|
|
543
|
+
codex_sync: codexSync,
|
|
398
544
|
build_stamp: buildStamp,
|
|
399
545
|
package_version: PACKAGE_VERSION,
|
|
400
546
|
signature,
|
|
@@ -543,16 +689,7 @@ async function resolveSksEntryForInstall(input) {
|
|
|
543
689
|
let existsResolved = await exists(resolved);
|
|
544
690
|
let projectLocal = isSubpath(resolved, input.root);
|
|
545
691
|
if (projectLocal) {
|
|
546
|
-
|
|
547
|
-
if (globalCandidate && globalCandidate !== resolved) {
|
|
548
|
-
input.warnings.push('sks_entry_project_local_ignored_global_package_used');
|
|
549
|
-
resolved = globalCandidate;
|
|
550
|
-
existsResolved = true;
|
|
551
|
-
projectLocal = isSubpath(resolved, input.root);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
input.warnings.push('sks_entry_resolved_under_project_root');
|
|
555
|
-
}
|
|
692
|
+
input.warnings.push('sks_entry_project_local');
|
|
556
693
|
}
|
|
557
694
|
const usedPreviousScript = !existsResolved && await exists(input.actionScriptPath);
|
|
558
695
|
if (!existsResolved && usedPreviousScript)
|
|
@@ -566,81 +703,227 @@ async function resolveSksEntryForInstall(input) {
|
|
|
566
703
|
used_previous_script: usedPreviousScript
|
|
567
704
|
};
|
|
568
705
|
}
|
|
569
|
-
|
|
570
|
-
const candidates = new Set();
|
|
571
|
-
const nodeRoot = path.resolve(path.dirname(process.execPath), '..');
|
|
572
|
-
candidates.add(path.join(nodeRoot, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
|
|
573
|
-
candidates.add(path.join(home, '.nvm', 'versions', 'node'));
|
|
574
|
-
const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
|
|
575
|
-
try {
|
|
576
|
-
const versions = await fs.readdir(nvmRoot);
|
|
577
|
-
for (const version of versions.sort().reverse()) {
|
|
578
|
-
candidates.add(path.join(nvmRoot, version, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
catch { }
|
|
582
|
-
if (env.SKS_GLOBAL_ROOT)
|
|
583
|
-
candidates.add(path.join(path.resolve(env.SKS_GLOBAL_ROOT), 'dist', 'bin', 'sks.js'));
|
|
584
|
-
for (const candidate of candidates) {
|
|
585
|
-
if (candidate === nvmRoot)
|
|
586
|
-
continue;
|
|
587
|
-
if (path.resolve(candidate) === path.resolve(packaged))
|
|
588
|
-
continue;
|
|
589
|
-
if (await exists(candidate))
|
|
590
|
-
return path.resolve(candidate);
|
|
591
|
-
}
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
function actionScriptSource(input) {
|
|
706
|
+
export function actionScriptSource(input) {
|
|
595
707
|
return `#!/bin/zsh
|
|
596
708
|
set -e
|
|
597
709
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
598
710
|
NODE_BIN=${shellQuote(input.nodeBin)}
|
|
599
711
|
SKS_ENTRY=${shellQuote(input.sksEntry)}
|
|
600
|
-
|
|
601
|
-
|
|
712
|
+
|
|
713
|
+
notify_sks_missing() {
|
|
714
|
+
/usr/bin/osascript -e 'display notification "sks CLI를 찾을 수 없습니다. sks doctor --fix 또는 npm install -g sneakoscope 실행 후 다시 시도하세요." with title "SKS Menu Bar"' >/dev/null 2>&1 || true
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
resolve_node_bin() {
|
|
718
|
+
if [ -x "$NODE_BIN" ]; then
|
|
719
|
+
printf '%s\\n' "$NODE_BIN"
|
|
720
|
+
return 0
|
|
721
|
+
fi
|
|
722
|
+
local login_node
|
|
723
|
+
login_node="$(/bin/zsh -lc 'command -v node' 2>/dev/null | /usr/bin/head -n 1 || true)"
|
|
724
|
+
if [ -n "$login_node" ] && [ -x "$login_node" ]; then
|
|
725
|
+
printf '%s\\n' "$login_node"
|
|
726
|
+
return 0
|
|
727
|
+
fi
|
|
728
|
+
for cand in "$HOME"/.nvm/versions/node/*/bin/node(Nn[-1]) /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
|
|
729
|
+
if [ -x "$cand" ]; then
|
|
730
|
+
printf '%s\\n' "$cand"
|
|
731
|
+
return 0
|
|
732
|
+
fi
|
|
733
|
+
done
|
|
734
|
+
return 1
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
run_node_entry() {
|
|
738
|
+
local entry="$1"
|
|
739
|
+
shift
|
|
740
|
+
if [ ! -f "$entry" ]; then
|
|
741
|
+
return 1
|
|
742
|
+
fi
|
|
743
|
+
local node_bin
|
|
744
|
+
node_bin="$(resolve_node_bin || true)"
|
|
745
|
+
if [ -z "$node_bin" ]; then
|
|
746
|
+
return 1
|
|
747
|
+
fi
|
|
748
|
+
exec "$node_bin" "$entry" "$@"
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
SKS_BIN="$(/bin/zsh -lc 'command -v sks' 2>/dev/null | /usr/bin/head -n 1 || true)"
|
|
752
|
+
if [ -n "$SKS_BIN" ] && [ -x "$SKS_BIN" ]; then
|
|
753
|
+
exec "$SKS_BIN" "$@"
|
|
754
|
+
fi
|
|
755
|
+
|
|
756
|
+
NPM_ROOT="$(/bin/zsh -lc 'npm root -g' 2>/dev/null | /usr/bin/head -n 1 || true)"
|
|
757
|
+
if [ -n "$NPM_ROOT" ]; then
|
|
758
|
+
run_node_entry "$NPM_ROOT/sneakoscope/dist/bin/sks.js" "$@" || true
|
|
602
759
|
fi
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
760
|
+
|
|
761
|
+
for entry in "$HOME"/.nvm/versions/node/*/lib/node_modules/sneakoscope/dist/bin/sks.js(Nn[-1]) /opt/homebrew/lib/node_modules/sneakoscope/dist/bin/sks.js /usr/local/lib/node_modules/sneakoscope/dist/bin/sks.js; do
|
|
762
|
+
if [ -f "$entry" ]; then
|
|
763
|
+
run_node_entry "$entry" "$@" || true
|
|
606
764
|
fi
|
|
607
765
|
done
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
fi
|
|
766
|
+
run_node_entry "$SKS_ENTRY" "$@" || true
|
|
767
|
+
notify_sks_missing
|
|
611
768
|
echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
|
|
612
769
|
exit 127
|
|
613
770
|
`;
|
|
614
771
|
}
|
|
615
|
-
function swiftMenuSource(input) {
|
|
772
|
+
export function swiftMenuSource(input) {
|
|
773
|
+
const codexLifecycleSource = input.codexBundleId ? `
|
|
774
|
+
func configureCodexLifecycleSync() {
|
|
775
|
+
setIconVisible(isCodexRunning())
|
|
776
|
+
let center = NSWorkspace.shared.notificationCenter
|
|
777
|
+
center.addObserver(self, selector: #selector(workspaceAppLaunched(_:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
|
778
|
+
center.addObserver(self, selector: #selector(workspaceAppTerminated(_:)), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
@objc func workspaceAppLaunched(_ notification: Notification) {
|
|
782
|
+
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
|
|
783
|
+
if app.bundleIdentifier == codexBundleId {
|
|
784
|
+
setIconVisible(true)
|
|
785
|
+
updateState()
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
@objc func workspaceAppTerminated(_ notification: Notification) {
|
|
790
|
+
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
|
|
791
|
+
if app.bundleIdentifier == codexBundleId {
|
|
792
|
+
if quitWithCodex {
|
|
793
|
+
NSApplication.shared.terminate(nil)
|
|
794
|
+
} else {
|
|
795
|
+
setIconVisible(false)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
func setIconVisible(_ visible: Bool) {
|
|
801
|
+
statusItem.isVisible = visible
|
|
802
|
+
if visible {
|
|
803
|
+
reassertControlCenterVisibility()
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
func isCodexRunning() -> Bool {
|
|
808
|
+
guard let bundle = codexBundleId else { return true }
|
|
809
|
+
return NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundle }
|
|
810
|
+
}
|
|
811
|
+
` : `
|
|
812
|
+
func configureCodexLifecycleSync() {
|
|
813
|
+
setIconVisible(true)
|
|
814
|
+
statusLineItem.title = "Codex app not detected — sync disabled"
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
func setIconVisible(_ visible: Bool) {
|
|
818
|
+
statusItem.isVisible = visible
|
|
819
|
+
if visible {
|
|
820
|
+
reassertControlCenterVisibility()
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
func isCodexRunning() -> Bool {
|
|
825
|
+
return true
|
|
826
|
+
}
|
|
827
|
+
`;
|
|
616
828
|
return `import Cocoa
|
|
617
829
|
import Foundation
|
|
618
830
|
|
|
619
831
|
let actionScript = ${swiftString(input.actionScriptPath)}
|
|
620
832
|
let buildStampPath = ${swiftString(input.buildStampPath)}
|
|
833
|
+
let menubarConfigPath = ${swiftString(input.configPath)}
|
|
834
|
+
let lastActionLogPath = ${swiftString(input.lastActionLogPath)}
|
|
835
|
+
let codexBundleId: String? = ${input.codexBundleId ? swiftString(input.codexBundleId) : 'nil'}
|
|
621
836
|
let packageVersion = ${swiftString(input.packageVersion)}
|
|
837
|
+
let menuBarLabel = ${swiftString(SKS_MENUBAR_LABEL)}
|
|
838
|
+
let controlCenterDomain = ${swiftString(CONTROL_CENTER_DOMAIN)}
|
|
839
|
+
|
|
840
|
+
/// macOS persists status-item visibility hints per-label in Control Center's
|
|
841
|
+
/// defaults domain (see installSksMenuBar's seedMenuBarPreferredPosition).
|
|
842
|
+
/// Toggling NSStatusItem.isVisible back to true inside a resident process is
|
|
843
|
+
/// not always sufficient to make Control Center re-render a previously
|
|
844
|
+
/// hidden item, so re-show must reassert the same Control Center defaults
|
|
845
|
+
/// the installer seeds, or the icon can stay invisible after a Codex
|
|
846
|
+
/// quit/relaunch cycle even though isVisible is technically true again.
|
|
847
|
+
func reassertControlCenterVisibility() {
|
|
848
|
+
let defaultsBin = "/usr/bin/defaults"
|
|
849
|
+
guard FileManager.default.isExecutableFile(atPath: defaultsBin) else { return }
|
|
850
|
+
let writes: [[String]] = [
|
|
851
|
+
["write", controlCenterDomain, "NSStatusItem Visible \\(menuBarLabel)", "-bool", "true"],
|
|
852
|
+
["write", controlCenterDomain, "NSStatusItem VisibleCC \\(menuBarLabel)", "-bool", "true"]
|
|
853
|
+
]
|
|
854
|
+
for args in writes {
|
|
855
|
+
let process = Process()
|
|
856
|
+
process.executableURL = URL(fileURLWithPath: defaultsBin)
|
|
857
|
+
process.arguments = args
|
|
858
|
+
process.standardOutput = FileHandle.nullDevice
|
|
859
|
+
process.standardError = FileHandle.nullDevice
|
|
860
|
+
try? process.run()
|
|
861
|
+
}
|
|
862
|
+
}
|
|
622
863
|
|
|
623
864
|
func shellQuote(_ value: String) -> String {
|
|
624
865
|
return "'" + value.replacingOccurrences(of: "'", with: "'\\\\''") + "'"
|
|
625
866
|
}
|
|
626
867
|
|
|
868
|
+
func clipped(_ value: String, limit: Int = 700) -> String {
|
|
869
|
+
return String(value.prefix(limit))
|
|
870
|
+
}
|
|
871
|
+
|
|
627
872
|
func showAlert(_ message: String, informative: String = "") {
|
|
628
873
|
DispatchQueue.main.async {
|
|
874
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
629
875
|
let alert = NSAlert()
|
|
630
876
|
alert.messageText = message
|
|
631
|
-
alert.informativeText = informative
|
|
877
|
+
alert.informativeText = clipped(informative)
|
|
632
878
|
alert.alertStyle = .warning
|
|
633
879
|
alert.runModal()
|
|
634
880
|
}
|
|
635
881
|
}
|
|
636
882
|
|
|
637
|
-
func
|
|
883
|
+
func promptText(title: String, message: String, placeholder: String = "", secure: Bool = false) -> String? {
|
|
884
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
885
|
+
let alert = NSAlert()
|
|
886
|
+
alert.messageText = title
|
|
887
|
+
alert.informativeText = message
|
|
888
|
+
alert.addButton(withTitle: "OK")
|
|
889
|
+
alert.addButton(withTitle: "Cancel")
|
|
890
|
+
let field: NSTextField = secure ? NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 320, height: 24)) : NSTextField(frame: NSRect(x: 0, y: 0, width: 320, height: 24))
|
|
891
|
+
field.placeholderString = placeholder
|
|
892
|
+
alert.accessoryView = field
|
|
893
|
+
let response = alert.runModal()
|
|
894
|
+
if response != .alertFirstButtonReturn { return nil }
|
|
895
|
+
let value = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
896
|
+
return value.isEmpty ? nil : value
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
func promptChoice(title: String, message: String, options: [String]) -> String? {
|
|
900
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
901
|
+
let alert = NSAlert()
|
|
902
|
+
alert.messageText = title
|
|
903
|
+
alert.informativeText = message
|
|
904
|
+
alert.addButton(withTitle: "OK")
|
|
905
|
+
alert.addButton(withTitle: "Cancel")
|
|
906
|
+
let popup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 320, height: 26), pullsDown: false)
|
|
907
|
+
popup.addItems(withTitles: options)
|
|
908
|
+
alert.accessoryView = popup
|
|
909
|
+
let response = alert.runModal()
|
|
910
|
+
if response != .alertFirstButtonReturn { return nil }
|
|
911
|
+
return popup.titleOfSelectedItem
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
func runProcess(_ executable: String, _ args: [String] = [], stdinText: String? = nil, completion: ((Int32, String) -> Void)? = nil) {
|
|
638
915
|
let process = Process()
|
|
639
916
|
let output = Pipe()
|
|
640
917
|
process.executableURL = URL(fileURLWithPath: executable)
|
|
641
918
|
process.arguments = args
|
|
642
919
|
process.standardOutput = output
|
|
643
920
|
process.standardError = output
|
|
921
|
+
var inputPipe: Pipe?
|
|
922
|
+
if stdinText != nil {
|
|
923
|
+
let pipe = Pipe()
|
|
924
|
+
process.standardInput = pipe
|
|
925
|
+
inputPipe = pipe
|
|
926
|
+
}
|
|
644
927
|
process.terminationHandler = { proc in
|
|
645
928
|
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
646
929
|
let text = String(data: data, encoding: .utf8) ?? ""
|
|
@@ -648,14 +931,17 @@ func runProcess(_ executable: String, _ args: [String] = [], completion: ((Int32
|
|
|
648
931
|
}
|
|
649
932
|
do {
|
|
650
933
|
try process.run()
|
|
934
|
+
if let stdinText = stdinText, let inputPipe = inputPipe {
|
|
935
|
+
inputPipe.fileHandleForWriting.write(Data(stdinText.utf8))
|
|
936
|
+
inputPipe.fileHandleForWriting.closeFile()
|
|
937
|
+
}
|
|
651
938
|
} catch {
|
|
652
939
|
completion?(-1, String(describing: error))
|
|
653
940
|
}
|
|
654
941
|
}
|
|
655
942
|
|
|
656
943
|
func showNotification(_ title: String, _ body: String) {
|
|
657
|
-
let
|
|
658
|
-
let script = "display notification " + shellQuote(clipped) + " with title " + shellQuote(title)
|
|
944
|
+
let script = "display notification " + shellQuote(clipped(body)) + " with title " + shellQuote(title)
|
|
659
945
|
runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
|
|
660
946
|
if code != 0 {
|
|
661
947
|
showAlert(title, informative: output)
|
|
@@ -663,35 +949,40 @@ func showNotification(_ title: String, _ body: String) {
|
|
|
663
949
|
}
|
|
664
950
|
}
|
|
665
951
|
|
|
666
|
-
func
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
.replacingOccurrences(of:
|
|
670
|
-
|
|
671
|
-
let
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
end tell
|
|
684
|
-
"""
|
|
685
|
-
runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
|
|
686
|
-
if code != 0 {
|
|
687
|
-
let denied = output.contains("-1743") || output.localizedCaseInsensitiveContains("not authorized")
|
|
688
|
-
if denied {
|
|
689
|
-
showAlert("SKS menu cannot control Terminal", informative: "Open System Settings > Privacy & Security > Automation, then allow SKS Menu Bar to control Terminal.")
|
|
690
|
-
} else {
|
|
691
|
-
showAlert("SKS menu action failed", informative: output)
|
|
692
|
-
}
|
|
952
|
+
func redactSecrets(_ value: String, secrets: [String] = []) -> String {
|
|
953
|
+
var text = value
|
|
954
|
+
for secret in secrets where secret.count >= 4 {
|
|
955
|
+
text = text.replacingOccurrences(of: secret, with: "[redacted]")
|
|
956
|
+
}
|
|
957
|
+
let patterns = [
|
|
958
|
+
#"sk-proj-[A-Za-z0-9_-]{12,}"#,
|
|
959
|
+
#"sk-or-v1-[A-Za-z0-9_-]{12,}"#,
|
|
960
|
+
#"sk-or-[A-Za-z0-9_-]{12,}"#,
|
|
961
|
+
#"sk-clb-[A-Za-z0-9_-]{8,}"#,
|
|
962
|
+
#"sk-[A-Za-z0-9_-]{20,}"#,
|
|
963
|
+
#"(?i)(api[_-]?key|secret|token)\\s*[:=]\\s*[^\\s"',}]+"#
|
|
964
|
+
]
|
|
965
|
+
for pattern in patterns {
|
|
966
|
+
if let regex = try? NSRegularExpression(pattern: pattern) {
|
|
967
|
+
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
|
968
|
+
text = regex.stringByReplacingMatches(in: text, range: range, withTemplate: "[redacted]")
|
|
693
969
|
}
|
|
694
970
|
}
|
|
971
|
+
return text
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
func writeActionLog(_ text: String) {
|
|
975
|
+
let url = URL(fileURLWithPath: lastActionLogPath)
|
|
976
|
+
try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
977
|
+
if !FileManager.default.fileExists(atPath: lastActionLogPath) {
|
|
978
|
+
FileManager.default.createFile(atPath: lastActionLogPath, contents: Data(), attributes: [.posixPermissions: 0o600])
|
|
979
|
+
}
|
|
980
|
+
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: lastActionLogPath)
|
|
981
|
+
if let handle = try? FileHandle(forWritingTo: url) {
|
|
982
|
+
try? handle.truncate(atOffset: 0)
|
|
983
|
+
handle.write(Data(text.utf8))
|
|
984
|
+
try? handle.close()
|
|
985
|
+
}
|
|
695
986
|
}
|
|
696
987
|
|
|
697
988
|
struct MenuState {
|
|
@@ -699,27 +990,37 @@ struct MenuState {
|
|
|
699
990
|
let line: String
|
|
700
991
|
}
|
|
701
992
|
|
|
702
|
-
|
|
993
|
+
struct MenuBarConfig {
|
|
994
|
+
let quitWithCodex: Bool
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
703
998
|
var statusItem: NSStatusItem!
|
|
704
999
|
var statusLineItem: NSMenuItem!
|
|
1000
|
+
var codexLbItem: NSMenuItem!
|
|
1001
|
+
var oauthItem: NSMenuItem!
|
|
705
1002
|
var timer: Timer?
|
|
1003
|
+
var busy = false
|
|
1004
|
+
var lastFailure = false
|
|
1005
|
+
var quitWithCodex = false
|
|
706
1006
|
|
|
707
1007
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
708
1008
|
NSApp.setActivationPolicy(.accessory)
|
|
1009
|
+
quitWithCodex = readConfig().quitWithCodex
|
|
709
1010
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
710
1011
|
statusItem.autosaveName = "com.sneakoscope.sks-menubar"
|
|
711
|
-
statusItem.isVisible = true
|
|
712
1012
|
if let button = statusItem.button {
|
|
713
1013
|
configureStatusButton(button, title: "SKS")
|
|
714
1014
|
}
|
|
715
1015
|
|
|
716
1016
|
let menu = NSMenu()
|
|
1017
|
+
menu.delegate = self
|
|
717
1018
|
statusLineItem = NSMenuItem(title: "SKS v\\(packageVersion) - starting", action: nil, keyEquivalent: "")
|
|
718
1019
|
statusLineItem.isEnabled = false
|
|
719
1020
|
menu.addItem(statusLineItem)
|
|
720
1021
|
menu.addItem(NSMenuItem.separator())
|
|
721
|
-
add(menu, "Use codex-lb", #selector(useCodexLb))
|
|
722
|
-
add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
|
|
1022
|
+
codexLbItem = add(menu, "Use codex-lb", #selector(useCodexLb))
|
|
1023
|
+
oauthItem = add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
|
|
723
1024
|
add(menu, "Set codex-lb Domain and Key", #selector(setCodexLbDomainAndKey))
|
|
724
1025
|
menu.addItem(NSMenuItem.separator())
|
|
725
1026
|
add(menu, "Set OpenRouter Key and GLM Profiles", #selector(setOpenRouterKey))
|
|
@@ -731,12 +1032,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
731
1032
|
add(menu, "Open Codex Settings", #selector(openCodexSettings))
|
|
732
1033
|
add(menu, "Restart Codex", #selector(restartCodex))
|
|
733
1034
|
menu.addItem(NSMenuItem.separator())
|
|
1035
|
+
add(menu, "View Last Log", #selector(viewLastLog))
|
|
734
1036
|
add(menu, "Quit SKS Menu", #selector(quit))
|
|
735
1037
|
statusItem.menu = menu
|
|
1038
|
+
configureCodexLifecycleSync()
|
|
736
1039
|
updateState()
|
|
737
1040
|
timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in self?.updateState() }
|
|
738
1041
|
}
|
|
739
1042
|
|
|
1043
|
+
func menuWillOpen(_ menu: NSMenu) {
|
|
1044
|
+
updateState()
|
|
1045
|
+
updateAuthModeChecks()
|
|
1046
|
+
}
|
|
1047
|
+
|
|
740
1048
|
func configureStatusButton(_ button: NSStatusBarButton, title: String) {
|
|
741
1049
|
button.image = nil
|
|
742
1050
|
button.title = title
|
|
@@ -752,10 +1060,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
752
1060
|
button.setAccessibilityHelp("Open SKS menu")
|
|
753
1061
|
}
|
|
754
1062
|
|
|
755
|
-
func add(_ menu: NSMenu, _ title: String, _ selector: Selector) {
|
|
1063
|
+
func add(_ menu: NSMenu, _ title: String, _ selector: Selector) -> NSMenuItem {
|
|
756
1064
|
let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
|
|
757
1065
|
item.target = self
|
|
758
1066
|
menu.addItem(item)
|
|
1067
|
+
return item
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
${codexLifecycleSource}
|
|
1071
|
+
|
|
1072
|
+
func readConfig() -> MenuBarConfig {
|
|
1073
|
+
guard let data = FileManager.default.contents(atPath: menubarConfigPath),
|
|
1074
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
1075
|
+
return MenuBarConfig(quitWithCodex: false)
|
|
1076
|
+
}
|
|
1077
|
+
return MenuBarConfig(quitWithCodex: json["quit_with_codex"] as? Bool == true)
|
|
759
1078
|
}
|
|
760
1079
|
|
|
761
1080
|
func updateState() {
|
|
@@ -767,20 +1086,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
767
1086
|
}
|
|
768
1087
|
|
|
769
1088
|
func readMenuState() -> MenuState {
|
|
770
|
-
|
|
771
|
-
|
|
1089
|
+
if codexBundleId == nil {
|
|
1090
|
+
return MenuState(title: "SKS", line: "Codex app not detected — sync disabled")
|
|
1091
|
+
}
|
|
1092
|
+
if busy {
|
|
1093
|
+
return MenuState(title: "SKS ⋯", line: "SKS v\\(packageVersion) - working")
|
|
1094
|
+
}
|
|
1095
|
+
if !actionScriptUsable() {
|
|
772
1096
|
return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - action script broken (run sks doctor --fix)")
|
|
773
1097
|
}
|
|
1098
|
+
if lastFailure {
|
|
1099
|
+
return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - last action failed")
|
|
1100
|
+
}
|
|
774
1101
|
if updateAvailable() {
|
|
775
1102
|
return MenuState(title: "SKS ↑", line: "SKS v\\(packageVersion) - update available")
|
|
776
1103
|
}
|
|
777
1104
|
return MenuState(title: "SKS", line: "SKS v\\(packageVersion) - OK")
|
|
778
1105
|
}
|
|
779
1106
|
|
|
780
|
-
func
|
|
781
|
-
|
|
782
|
-
guard let entry = shellAssignment(text, key: "SKS_ENTRY") else { return false }
|
|
783
|
-
return FileManager.default.fileExists(atPath: entry)
|
|
1107
|
+
func actionScriptUsable() -> Bool {
|
|
1108
|
+
return FileManager.default.isExecutableFile(atPath: actionScript)
|
|
784
1109
|
}
|
|
785
1110
|
|
|
786
1111
|
func updateAvailable() -> Bool {
|
|
@@ -792,48 +1117,83 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
792
1117
|
return latest != packageVersion && !latest.isEmpty
|
|
793
1118
|
}
|
|
794
1119
|
|
|
795
|
-
func
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1120
|
+
func runSksCapture(_ args: [String], title: String, stdinText: String? = nil, notify: Bool = true, completion: ((Int32, String) -> Void)? = nil) {
|
|
1121
|
+
busy = true
|
|
1122
|
+
lastFailure = false
|
|
1123
|
+
updateState()
|
|
1124
|
+
runProcess(actionScript, args, stdinText: stdinText) { [weak self] code, output in
|
|
1125
|
+
let redacted = redactSecrets(output, secrets: stdinText == nil ? [] : [stdinText ?? ""])
|
|
1126
|
+
writeActionLog("$ sks \\(args.joined(separator: " "))\\n\\(redacted)\\n")
|
|
1127
|
+
DispatchQueue.main.async {
|
|
1128
|
+
self?.busy = false
|
|
1129
|
+
self?.lastFailure = code != 0
|
|
1130
|
+
self?.updateState()
|
|
1131
|
+
if notify {
|
|
1132
|
+
if code == 0 {
|
|
1133
|
+
showNotification(title, "OK\\n" + redacted)
|
|
1134
|
+
} else {
|
|
1135
|
+
showAlert(title + " failed", informative: redacted)
|
|
1136
|
+
}
|
|
803
1137
|
}
|
|
804
|
-
|
|
1138
|
+
completion?(code, redacted)
|
|
805
1139
|
}
|
|
806
1140
|
}
|
|
807
|
-
return nil
|
|
808
1141
|
}
|
|
809
1142
|
|
|
810
|
-
func
|
|
811
|
-
|
|
812
|
-
|
|
1143
|
+
func runSksBackground(_ args: [String], title: String, stdinText: String? = nil, completion: ((Int32, String) -> Void)? = nil) {
|
|
1144
|
+
runSksCapture(args, title: title, stdinText: stdinText, notify: true, completion: completion)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
func startSksDetached(_ args: [String], title: String) {
|
|
1148
|
+
let process = Process()
|
|
1149
|
+
process.executableURL = URL(fileURLWithPath: actionScript)
|
|
1150
|
+
process.arguments = args
|
|
1151
|
+
writeActionLog("$ sks \\(args.joined(separator: " "))\\nstarted\\n")
|
|
1152
|
+
if let handle = try? FileHandle(forWritingTo: URL(fileURLWithPath: lastActionLogPath)) {
|
|
1153
|
+
process.standardOutput = handle
|
|
1154
|
+
process.standardError = handle
|
|
1155
|
+
}
|
|
1156
|
+
do {
|
|
1157
|
+
try process.run()
|
|
1158
|
+
showNotification(title, "started")
|
|
1159
|
+
} catch {
|
|
1160
|
+
showAlert(title + " failed", informative: String(describing: error))
|
|
1161
|
+
}
|
|
813
1162
|
}
|
|
814
1163
|
|
|
815
|
-
func
|
|
816
|
-
|
|
817
|
-
let
|
|
818
|
-
|
|
1164
|
+
func updateAuthModeChecks() {
|
|
1165
|
+
runSksCapture(["codex-lb", "status", "--json"], title: "SKS Auth Status", notify: false) { [weak self] code, output in
|
|
1166
|
+
guard let self = self else { return }
|
|
1167
|
+
let lower = output.lowercased()
|
|
1168
|
+
let codexLbActive = code == 0 && (lower.contains(#""configured": true"#) || lower.contains(#""model_provider": "codex-lb""#) || lower.contains(#""mode": "codex-lb""#))
|
|
1169
|
+
self.codexLbItem.state = codexLbActive ? .on : .off
|
|
1170
|
+
self.oauthItem.state = codexLbActive ? .off : .on
|
|
819
1171
|
}
|
|
820
1172
|
}
|
|
821
1173
|
|
|
822
1174
|
@objc func useCodexLb() {
|
|
823
|
-
|
|
1175
|
+
runSksBackground(["codex-lb", "use-codex-lb", "--json"], title: "Use codex-lb") { [weak self] code, _ in
|
|
1176
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1177
|
+
}
|
|
824
1178
|
}
|
|
825
1179
|
|
|
826
1180
|
@objc func useChatGptOAuth() {
|
|
827
|
-
|
|
1181
|
+
runSksBackground(["codex-lb", "use-oauth", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
|
|
1182
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1183
|
+
}
|
|
828
1184
|
}
|
|
829
1185
|
|
|
830
1186
|
@objc func setCodexLbDomainAndKey() {
|
|
831
|
-
|
|
1187
|
+
guard let domain = promptText(title: "Set codex-lb Domain", message: "Enter your codex-lb domain or base URL.", placeholder: "https://lb.example.com/backend-api/codex") else { return }
|
|
1188
|
+
guard let key = promptText(title: "Set codex-lb Key", message: "Enter your codex-lb API key.", placeholder: "sk-clb-...", secure: true) else { return }
|
|
1189
|
+
runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
|
|
1190
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1191
|
+
}
|
|
832
1192
|
}
|
|
833
1193
|
|
|
834
1194
|
@objc func setOpenRouterKey() {
|
|
835
|
-
let
|
|
836
|
-
|
|
1195
|
+
guard let key = promptText(title: "Set OpenRouter Key", message: "Enter your OpenRouter API key.", placeholder: "sk-or-v1-...", secure: true) else { return }
|
|
1196
|
+
runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
|
|
837
1197
|
}
|
|
838
1198
|
|
|
839
1199
|
@objc func fastCheck() {
|
|
@@ -845,13 +1205,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
845
1205
|
}
|
|
846
1206
|
|
|
847
1207
|
@objc func updateSksNow() {
|
|
848
|
-
|
|
1208
|
+
runSksBackground(["update"], title: "Update SKS Now")
|
|
849
1209
|
}
|
|
850
1210
|
|
|
851
1211
|
@objc func openDashboard() {
|
|
852
|
-
let
|
|
853
|
-
|
|
854
|
-
|
|
1212
|
+
let urlString = "http://127.0.0.1:4477"
|
|
1213
|
+
runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
|
|
1214
|
+
DispatchQueue.main.async {
|
|
1215
|
+
if code != 0 {
|
|
1216
|
+
self?.startSksDetached(["ui"], title: "SKS Dashboard")
|
|
1217
|
+
}
|
|
1218
|
+
if let url = URL(string: urlString) {
|
|
1219
|
+
NSWorkspace.shared.open(url)
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
855
1223
|
}
|
|
856
1224
|
|
|
857
1225
|
@objc func openCodexSettings() {
|
|
@@ -859,7 +1227,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
859
1227
|
}
|
|
860
1228
|
|
|
861
1229
|
@objc func restartCodex() {
|
|
862
|
-
|
|
1230
|
+
let running = NSWorkspace.shared.runningApplications.filter { app in
|
|
1231
|
+
if let bundle = codexBundleId, app.bundleIdentifier == bundle { return true }
|
|
1232
|
+
return app.localizedName == "Codex"
|
|
1233
|
+
}
|
|
1234
|
+
for app in running {
|
|
1235
|
+
app.terminate()
|
|
1236
|
+
}
|
|
1237
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
1238
|
+
if let bundle = codexBundleId, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundle) {
|
|
1239
|
+
NSWorkspace.shared.open(url)
|
|
1240
|
+
showNotification("Restart Codex", "requested")
|
|
1241
|
+
} else {
|
|
1242
|
+
showAlert("Restart Codex failed", informative: "Codex app bundle could not be resolved.")
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
@objc func viewLastLog() {
|
|
1248
|
+
if FileManager.default.fileExists(atPath: lastActionLogPath) {
|
|
1249
|
+
NSWorkspace.shared.open(URL(fileURLWithPath: lastActionLogPath))
|
|
1250
|
+
} else {
|
|
1251
|
+
showAlert("No SKS menu log yet", informative: lastActionLogPath)
|
|
1252
|
+
}
|
|
863
1253
|
}
|
|
864
1254
|
|
|
865
1255
|
@objc func quit() {
|
|
@@ -1077,13 +1467,14 @@ async function inspectLaunchdService(env = process.env) {
|
|
|
1077
1467
|
const text = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
1078
1468
|
const state = text.match(/\bstate = ([^\n]+)/)?.[1]?.trim() || null;
|
|
1079
1469
|
const pidText = text.match(/\bpid = (\d+)/)?.[1] || null;
|
|
1470
|
+
const running = result.code === 0 && (state === 'running' || Boolean(pidText));
|
|
1080
1471
|
return {
|
|
1081
1472
|
checked: true,
|
|
1082
|
-
ok:
|
|
1473
|
+
ok: running,
|
|
1083
1474
|
service,
|
|
1084
1475
|
state,
|
|
1085
1476
|
pid: pidText ? Number(pidText) : null,
|
|
1086
|
-
error:
|
|
1477
|
+
error: running ? null : String(result.stderr || result.stdout || state || 'launchd_not_running').trim()
|
|
1087
1478
|
};
|
|
1088
1479
|
}
|
|
1089
1480
|
async function inspectSignature(appPath, env = process.env) {
|
|
@@ -1175,6 +1566,8 @@ async function writeReport(reportPath, result) {
|
|
|
1175
1566
|
}
|
|
1176
1567
|
catch (err) {
|
|
1177
1568
|
result.report_write_failed = true;
|
|
1569
|
+
if (!result.warnings.includes('menubar_report_write_failed'))
|
|
1570
|
+
result.warnings.push('menubar_report_write_failed');
|
|
1178
1571
|
console.error(`warning: failed to write SKS menubar report ${reportPath}: ${err?.message || err}`);
|
|
1179
1572
|
}
|
|
1180
1573
|
}
|