sneakoscope 5.1.2 → 5.3.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 +1 -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/native-cli-worker.js +0 -1
- package/dist/core/codex-app/sks-menubar.js +462 -130
- package/dist/core/commands/basic-cli.js +2 -4
- package/dist/core/commands/menubar-command.js +2 -1
- package/dist/core/doctor/doctor-dirty-planner.js +51 -7
- package/dist/core/fsx.js +1 -1
- package/dist/core/naruto/naruto-real-worker-child.js +1 -1
- package/dist/core/update/update-migration-state.js +23 -3
- package/dist/core/version.js +1 -1
- 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/naruto-real-active-pool-runtime-check.js +26 -1
- package/dist/scripts/project-skill-dedupe-blackbox.js +36 -0
- package/dist/scripts/release-gate-script-parity-check.js +44 -16
- package/dist/scripts/release-metadata-1-19-check.js +1 -1
- 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 +8 -65
- 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);
|
|
@@ -200,9 +223,6 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
200
223
|
}
|
|
201
224
|
}
|
|
202
225
|
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
226
|
try {
|
|
207
227
|
await buildMenuBarAppAtomically({
|
|
208
228
|
paths,
|
|
@@ -277,7 +297,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
277
297
|
launch_agent_path: paths.launch_agent_path,
|
|
278
298
|
action_script_path: paths.action_script_path,
|
|
279
299
|
build_stamp_path: paths.build_stamp_path,
|
|
300
|
+
config_path: paths.config_path,
|
|
280
301
|
report_path: paths.report_path,
|
|
302
|
+
codex_bundle_id: codexBundleId,
|
|
281
303
|
menu_items: MENU_ITEMS,
|
|
282
304
|
actions,
|
|
283
305
|
launch,
|
|
@@ -285,10 +307,7 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
285
307
|
build_stamp: stamp,
|
|
286
308
|
tcc_automation_status: 'unknown',
|
|
287
309
|
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
|
-
],
|
|
310
|
+
next_actions: nextActions,
|
|
292
311
|
blockers: ok ? [] : [launch.error || 'sks_menubar_launch_failed'],
|
|
293
312
|
warnings
|
|
294
313
|
};
|
|
@@ -306,7 +325,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
306
325
|
launch_agent_path: paths.launch_agent_path,
|
|
307
326
|
action_script_path: paths.action_script_path,
|
|
308
327
|
build_stamp_path: paths.build_stamp_path,
|
|
328
|
+
config_path: paths.config_path,
|
|
309
329
|
report_path: paths.report_path,
|
|
330
|
+
codex_bundle_id: typeof codexBundleId === 'string' ? codexBundleId : null,
|
|
310
331
|
menu_items: MENU_ITEMS,
|
|
311
332
|
actions,
|
|
312
333
|
launch: { requested: false, method: 'none', ok: false, error: detail || reason },
|
|
@@ -358,6 +379,84 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
|
|
|
358
379
|
next_actions: ['Rotate CODEX_LB_API_KEY and OPENROUTER_API_KEY if they were previously exposed in launchd.']
|
|
359
380
|
};
|
|
360
381
|
}
|
|
382
|
+
async function writeDefaultMenuBarConfig(configPath, codexBundleId) {
|
|
383
|
+
const previous = await readMenuBarConfig(configPath);
|
|
384
|
+
const config = {
|
|
385
|
+
schema: 'sks.sks-menubar-config.v1',
|
|
386
|
+
codex_bundle_id: codexBundleId,
|
|
387
|
+
quit_with_codex: previous.quit_with_codex === true
|
|
388
|
+
};
|
|
389
|
+
await writeJsonAtomic(configPath, config);
|
|
390
|
+
return config;
|
|
391
|
+
}
|
|
392
|
+
async function readMenuBarConfig(configPath) {
|
|
393
|
+
const config = await readJson(configPath, null);
|
|
394
|
+
return {
|
|
395
|
+
schema: 'sks.sks-menubar-config.v1',
|
|
396
|
+
codex_bundle_id: typeof config?.codex_bundle_id === 'string' && config.codex_bundle_id.trim()
|
|
397
|
+
? config.codex_bundle_id.trim()
|
|
398
|
+
: null,
|
|
399
|
+
quit_with_codex: config?.quit_with_codex === true
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function resolveCodexBundleId(input) {
|
|
403
|
+
if (process.platform !== 'darwin')
|
|
404
|
+
return null;
|
|
405
|
+
const appPath = await findCodexApp({ home: input.home, env: input.env }).catch(() => null);
|
|
406
|
+
if (!appPath) {
|
|
407
|
+
input.warnings.push('codex_app_not_found_for_bundle_sync');
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const mdls = input.env.SKS_MENUBAR_MDLS || await which('mdls').catch(() => null) || await fallbackTool('/usr/bin/mdls');
|
|
411
|
+
if (mdls) {
|
|
412
|
+
const result = await runProcess(mdls, ['-name', 'kMDItemCFBundleIdentifier', '-raw', appPath], {
|
|
413
|
+
timeoutMs: 3_000,
|
|
414
|
+
maxOutputBytes: 8 * 1024
|
|
415
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
416
|
+
const value = String(result.stdout || '').trim();
|
|
417
|
+
if (result.code === 0 && value && value !== '(null)' && value !== 'null')
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
const defaults = input.env.SKS_MENUBAR_DEFAULTS || await which('defaults').catch(() => null) || await fallbackTool('/usr/bin/defaults');
|
|
421
|
+
if (defaults) {
|
|
422
|
+
const result = await runProcess(defaults, ['read', path.join(appPath, 'Contents', 'Info'), 'CFBundleIdentifier'], {
|
|
423
|
+
timeoutMs: 3_000,
|
|
424
|
+
maxOutputBytes: 8 * 1024
|
|
425
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
426
|
+
const value = String(result.stdout || '').trim();
|
|
427
|
+
if (result.code === 0 && value)
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
async function smokeSksMenuBarAction(actionScriptPath) {
|
|
433
|
+
if (!(await exists(actionScriptPath)))
|
|
434
|
+
return { ok: false, code: null, output: null, versionDetected: false };
|
|
435
|
+
const result = await runProcess('/bin/zsh', [actionScriptPath, 'version'], {
|
|
436
|
+
timeoutMs: 5_000,
|
|
437
|
+
maxOutputBytes: 16 * 1024
|
|
438
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
439
|
+
const output = String(`${result.stdout || ''}\n${result.stderr || ''}`).trim();
|
|
440
|
+
const versionDetected = /\b(?:sks|sneakoscope)?\s*v?\d+\.\d+\.\d+\b/i.test(output);
|
|
441
|
+
return {
|
|
442
|
+
ok: result.code === 0 && versionDetected,
|
|
443
|
+
code: result.code,
|
|
444
|
+
output: output ? output.slice(0, 700) : null,
|
|
445
|
+
versionDetected
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function isCodexAppRunningByBundleId(bundleId, env = process.env) {
|
|
449
|
+
if (process.platform !== 'darwin' || !bundleId)
|
|
450
|
+
return false;
|
|
451
|
+
const osascript = env.SKS_MENUBAR_OSASCRIPT || await which('osascript').catch(() => null) || await fallbackTool('/usr/bin/osascript');
|
|
452
|
+
if (!osascript)
|
|
453
|
+
return false;
|
|
454
|
+
const result = await runProcess(osascript, ['-e', `application id "${bundleId.replace(/"/g, '\\"')}" is running`], {
|
|
455
|
+
timeoutMs: 2_000,
|
|
456
|
+
maxOutputBytes: 8 * 1024
|
|
457
|
+
}).catch(() => ({ code: 1, stdout: '', stderr: '' }));
|
|
458
|
+
return result.code === 0 && String(result.stdout || '').trim().toLowerCase() === 'true';
|
|
459
|
+
}
|
|
361
460
|
export async function inspectSksMenuBarStatus(opts = {}) {
|
|
362
461
|
const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
|
|
363
462
|
const installed = await exists(paths.executable_path);
|
|
@@ -367,6 +466,16 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
367
466
|
const sksEntry = shellAssignment(actionText, 'SKS_ENTRY');
|
|
368
467
|
const nodeExists = nodeBin ? await isExecutable(nodeBin) : false;
|
|
369
468
|
const sksEntryExists = sksEntry ? await exists(sksEntry) : false;
|
|
469
|
+
const actionSmoke = await smokeSksMenuBarAction(paths.action_script_path);
|
|
470
|
+
const config = await readMenuBarConfig(paths.config_path);
|
|
471
|
+
const codexRunning = config.codex_bundle_id ? await isCodexAppRunningByBundleId(config.codex_bundle_id, opts.env) : null;
|
|
472
|
+
const codexSync = {
|
|
473
|
+
ok: Boolean(config.codex_bundle_id),
|
|
474
|
+
bundle_id: config.codex_bundle_id,
|
|
475
|
+
codex_running: codexRunning,
|
|
476
|
+
icon_visible_expected: config.codex_bundle_id ? codexRunning === true : true,
|
|
477
|
+
warning: config.codex_bundle_id ? null : 'codex_sync_disabled'
|
|
478
|
+
};
|
|
370
479
|
const buildStamp = await readJson(paths.build_stamp_path, null);
|
|
371
480
|
const launchd = await inspectLaunchdService(opts.env);
|
|
372
481
|
const signature = await inspectSignature(paths.app_path, opts.env);
|
|
@@ -374,10 +483,14 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
374
483
|
const warnings = [];
|
|
375
484
|
if (!installed)
|
|
376
485
|
blockers.push('menubar_app_missing');
|
|
377
|
-
if (installed && !
|
|
378
|
-
blockers.push('
|
|
486
|
+
if (installed && launchd.checked && !launchd.ok)
|
|
487
|
+
blockers.push('launchd_not_running');
|
|
488
|
+
if (installed && !actionSmoke.ok)
|
|
489
|
+
blockers.push('action_script_smoke_failed');
|
|
379
490
|
if (installed && signature.checked && !signature.ok)
|
|
380
491
|
warnings.push('codesign_identifier_unexpected');
|
|
492
|
+
if (!codexSync.ok)
|
|
493
|
+
warnings.push('codex_sync_disabled');
|
|
381
494
|
if (buildStamp?.package_version && buildStamp.package_version !== PACKAGE_VERSION)
|
|
382
495
|
warnings.push('build_stamp_package_version_mismatch');
|
|
383
496
|
return {
|
|
@@ -393,8 +506,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
393
506
|
node_exists: nodeExists,
|
|
394
507
|
sks_entry: sksEntry,
|
|
395
508
|
sks_entry_exists: sksEntryExists,
|
|
396
|
-
|
|
509
|
+
smoke_code: actionSmoke.code,
|
|
510
|
+
smoke_output: actionSmoke.output,
|
|
511
|
+
version_detected: actionSmoke.versionDetected,
|
|
512
|
+
ok: actionSmoke.ok
|
|
397
513
|
},
|
|
514
|
+
codex_sync: codexSync,
|
|
398
515
|
build_stamp: buildStamp,
|
|
399
516
|
package_version: PACKAGE_VERSION,
|
|
400
517
|
signature,
|
|
@@ -543,16 +660,7 @@ async function resolveSksEntryForInstall(input) {
|
|
|
543
660
|
let existsResolved = await exists(resolved);
|
|
544
661
|
let projectLocal = isSubpath(resolved, input.root);
|
|
545
662
|
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
|
-
}
|
|
663
|
+
input.warnings.push('sks_entry_project_local');
|
|
556
664
|
}
|
|
557
665
|
const usedPreviousScript = !existsResolved && await exists(input.actionScriptPath);
|
|
558
666
|
if (!existsResolved && usedPreviousScript)
|
|
@@ -566,81 +674,195 @@ async function resolveSksEntryForInstall(input) {
|
|
|
566
674
|
used_previous_script: usedPreviousScript
|
|
567
675
|
};
|
|
568
676
|
}
|
|
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) {
|
|
677
|
+
export function actionScriptSource(input) {
|
|
595
678
|
return `#!/bin/zsh
|
|
596
679
|
set -e
|
|
597
680
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
598
681
|
NODE_BIN=${shellQuote(input.nodeBin)}
|
|
599
682
|
SKS_ENTRY=${shellQuote(input.sksEntry)}
|
|
600
|
-
|
|
601
|
-
|
|
683
|
+
|
|
684
|
+
notify_sks_missing() {
|
|
685
|
+
/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
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
resolve_node_bin() {
|
|
689
|
+
if [ -x "$NODE_BIN" ]; then
|
|
690
|
+
printf '%s\\n' "$NODE_BIN"
|
|
691
|
+
return 0
|
|
692
|
+
fi
|
|
693
|
+
local login_node
|
|
694
|
+
login_node="$(/bin/zsh -lc 'command -v node' 2>/dev/null | /usr/bin/head -n 1 || true)"
|
|
695
|
+
if [ -n "$login_node" ] && [ -x "$login_node" ]; then
|
|
696
|
+
printf '%s\\n' "$login_node"
|
|
697
|
+
return 0
|
|
698
|
+
fi
|
|
699
|
+
for cand in "$HOME"/.nvm/versions/node/*/bin/node(Nn[-1]) /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
|
|
700
|
+
if [ -x "$cand" ]; then
|
|
701
|
+
printf '%s\\n' "$cand"
|
|
702
|
+
return 0
|
|
703
|
+
fi
|
|
704
|
+
done
|
|
705
|
+
return 1
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
run_node_entry() {
|
|
709
|
+
local entry="$1"
|
|
710
|
+
shift
|
|
711
|
+
if [ ! -f "$entry" ]; then
|
|
712
|
+
return 1
|
|
713
|
+
fi
|
|
714
|
+
local node_bin
|
|
715
|
+
node_bin="$(resolve_node_bin || true)"
|
|
716
|
+
if [ -z "$node_bin" ]; then
|
|
717
|
+
return 1
|
|
718
|
+
fi
|
|
719
|
+
exec "$node_bin" "$entry" "$@"
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
SKS_BIN="$(/bin/zsh -lc 'command -v sks' 2>/dev/null | /usr/bin/head -n 1 || true)"
|
|
723
|
+
if [ -n "$SKS_BIN" ] && [ -x "$SKS_BIN" ]; then
|
|
724
|
+
exec "$SKS_BIN" "$@"
|
|
725
|
+
fi
|
|
726
|
+
|
|
727
|
+
NPM_ROOT="$(/bin/zsh -lc 'npm root -g' 2>/dev/null | /usr/bin/head -n 1 || true)"
|
|
728
|
+
if [ -n "$NPM_ROOT" ]; then
|
|
729
|
+
run_node_entry "$NPM_ROOT/sneakoscope/dist/bin/sks.js" "$@" || true
|
|
602
730
|
fi
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
731
|
+
|
|
732
|
+
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
|
|
733
|
+
if [ -f "$entry" ]; then
|
|
734
|
+
run_node_entry "$entry" "$@" || true
|
|
606
735
|
fi
|
|
607
736
|
done
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
fi
|
|
737
|
+
run_node_entry "$SKS_ENTRY" "$@" || true
|
|
738
|
+
notify_sks_missing
|
|
611
739
|
echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
|
|
612
740
|
exit 127
|
|
613
741
|
`;
|
|
614
742
|
}
|
|
615
|
-
function swiftMenuSource(input) {
|
|
743
|
+
export function swiftMenuSource(input) {
|
|
744
|
+
const codexLifecycleSource = input.codexBundleId ? `
|
|
745
|
+
func configureCodexLifecycleSync() {
|
|
746
|
+
setIconVisible(isCodexRunning())
|
|
747
|
+
let center = NSWorkspace.shared.notificationCenter
|
|
748
|
+
center.addObserver(self, selector: #selector(workspaceAppLaunched(_:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
|
749
|
+
center.addObserver(self, selector: #selector(workspaceAppTerminated(_:)), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
@objc func workspaceAppLaunched(_ notification: Notification) {
|
|
753
|
+
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
|
|
754
|
+
if app.bundleIdentifier == codexBundleId {
|
|
755
|
+
setIconVisible(true)
|
|
756
|
+
updateState()
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
@objc func workspaceAppTerminated(_ notification: Notification) {
|
|
761
|
+
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
|
|
762
|
+
if app.bundleIdentifier == codexBundleId {
|
|
763
|
+
if quitWithCodex {
|
|
764
|
+
NSApplication.shared.terminate(nil)
|
|
765
|
+
} else {
|
|
766
|
+
setIconVisible(false)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
func setIconVisible(_ visible: Bool) {
|
|
772
|
+
statusItem.isVisible = visible
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
func isCodexRunning() -> Bool {
|
|
776
|
+
guard let bundle = codexBundleId else { return true }
|
|
777
|
+
return NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundle }
|
|
778
|
+
}
|
|
779
|
+
` : `
|
|
780
|
+
func configureCodexLifecycleSync() {
|
|
781
|
+
setIconVisible(true)
|
|
782
|
+
statusLineItem.title = "Codex app not detected — sync disabled"
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
func setIconVisible(_ visible: Bool) {
|
|
786
|
+
statusItem.isVisible = visible
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
func isCodexRunning() -> Bool {
|
|
790
|
+
return true
|
|
791
|
+
}
|
|
792
|
+
`;
|
|
616
793
|
return `import Cocoa
|
|
617
794
|
import Foundation
|
|
618
795
|
|
|
619
796
|
let actionScript = ${swiftString(input.actionScriptPath)}
|
|
620
797
|
let buildStampPath = ${swiftString(input.buildStampPath)}
|
|
798
|
+
let menubarConfigPath = ${swiftString(input.configPath)}
|
|
799
|
+
let lastActionLogPath = ${swiftString(input.lastActionLogPath)}
|
|
800
|
+
let codexBundleId: String? = ${input.codexBundleId ? swiftString(input.codexBundleId) : 'nil'}
|
|
621
801
|
let packageVersion = ${swiftString(input.packageVersion)}
|
|
622
802
|
|
|
623
803
|
func shellQuote(_ value: String) -> String {
|
|
624
804
|
return "'" + value.replacingOccurrences(of: "'", with: "'\\\\''") + "'"
|
|
625
805
|
}
|
|
626
806
|
|
|
807
|
+
func clipped(_ value: String, limit: Int = 700) -> String {
|
|
808
|
+
return String(value.prefix(limit))
|
|
809
|
+
}
|
|
810
|
+
|
|
627
811
|
func showAlert(_ message: String, informative: String = "") {
|
|
628
812
|
DispatchQueue.main.async {
|
|
813
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
629
814
|
let alert = NSAlert()
|
|
630
815
|
alert.messageText = message
|
|
631
|
-
alert.informativeText = informative
|
|
816
|
+
alert.informativeText = clipped(informative)
|
|
632
817
|
alert.alertStyle = .warning
|
|
633
818
|
alert.runModal()
|
|
634
819
|
}
|
|
635
820
|
}
|
|
636
821
|
|
|
637
|
-
func
|
|
822
|
+
func promptText(title: String, message: String, placeholder: String = "", secure: Bool = false) -> String? {
|
|
823
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
824
|
+
let alert = NSAlert()
|
|
825
|
+
alert.messageText = title
|
|
826
|
+
alert.informativeText = message
|
|
827
|
+
alert.addButton(withTitle: "OK")
|
|
828
|
+
alert.addButton(withTitle: "Cancel")
|
|
829
|
+
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))
|
|
830
|
+
field.placeholderString = placeholder
|
|
831
|
+
alert.accessoryView = field
|
|
832
|
+
let response = alert.runModal()
|
|
833
|
+
if response != .alertFirstButtonReturn { return nil }
|
|
834
|
+
let value = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
835
|
+
return value.isEmpty ? nil : value
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
func promptChoice(title: String, message: String, options: [String]) -> String? {
|
|
839
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
840
|
+
let alert = NSAlert()
|
|
841
|
+
alert.messageText = title
|
|
842
|
+
alert.informativeText = message
|
|
843
|
+
alert.addButton(withTitle: "OK")
|
|
844
|
+
alert.addButton(withTitle: "Cancel")
|
|
845
|
+
let popup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 320, height: 26), pullsDown: false)
|
|
846
|
+
popup.addItems(withTitles: options)
|
|
847
|
+
alert.accessoryView = popup
|
|
848
|
+
let response = alert.runModal()
|
|
849
|
+
if response != .alertFirstButtonReturn { return nil }
|
|
850
|
+
return popup.titleOfSelectedItem
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
func runProcess(_ executable: String, _ args: [String] = [], stdinText: String? = nil, completion: ((Int32, String) -> Void)? = nil) {
|
|
638
854
|
let process = Process()
|
|
639
855
|
let output = Pipe()
|
|
640
856
|
process.executableURL = URL(fileURLWithPath: executable)
|
|
641
857
|
process.arguments = args
|
|
642
858
|
process.standardOutput = output
|
|
643
859
|
process.standardError = output
|
|
860
|
+
var inputPipe: Pipe?
|
|
861
|
+
if stdinText != nil {
|
|
862
|
+
let pipe = Pipe()
|
|
863
|
+
process.standardInput = pipe
|
|
864
|
+
inputPipe = pipe
|
|
865
|
+
}
|
|
644
866
|
process.terminationHandler = { proc in
|
|
645
867
|
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
646
868
|
let text = String(data: data, encoding: .utf8) ?? ""
|
|
@@ -648,14 +870,17 @@ func runProcess(_ executable: String, _ args: [String] = [], completion: ((Int32
|
|
|
648
870
|
}
|
|
649
871
|
do {
|
|
650
872
|
try process.run()
|
|
873
|
+
if let stdinText = stdinText, let inputPipe = inputPipe {
|
|
874
|
+
inputPipe.fileHandleForWriting.write(Data(stdinText.utf8))
|
|
875
|
+
inputPipe.fileHandleForWriting.closeFile()
|
|
876
|
+
}
|
|
651
877
|
} catch {
|
|
652
878
|
completion?(-1, String(describing: error))
|
|
653
879
|
}
|
|
654
880
|
}
|
|
655
881
|
|
|
656
882
|
func showNotification(_ title: String, _ body: String) {
|
|
657
|
-
let
|
|
658
|
-
let script = "display notification " + shellQuote(clipped) + " with title " + shellQuote(title)
|
|
883
|
+
let script = "display notification " + shellQuote(clipped(body)) + " with title " + shellQuote(title)
|
|
659
884
|
runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
|
|
660
885
|
if code != 0 {
|
|
661
886
|
showAlert(title, informative: output)
|
|
@@ -663,35 +888,40 @@ func showNotification(_ title: String, _ body: String) {
|
|
|
663
888
|
}
|
|
664
889
|
}
|
|
665
890
|
|
|
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
|
-
}
|
|
891
|
+
func redactSecrets(_ value: String, secrets: [String] = []) -> String {
|
|
892
|
+
var text = value
|
|
893
|
+
for secret in secrets where secret.count >= 4 {
|
|
894
|
+
text = text.replacingOccurrences(of: secret, with: "[redacted]")
|
|
895
|
+
}
|
|
896
|
+
let patterns = [
|
|
897
|
+
#"sk-proj-[A-Za-z0-9_-]{12,}"#,
|
|
898
|
+
#"sk-or-v1-[A-Za-z0-9_-]{12,}"#,
|
|
899
|
+
#"sk-or-[A-Za-z0-9_-]{12,}"#,
|
|
900
|
+
#"sk-clb-[A-Za-z0-9_-]{8,}"#,
|
|
901
|
+
#"sk-[A-Za-z0-9_-]{20,}"#,
|
|
902
|
+
#"(?i)(api[_-]?key|secret|token)\\s*[:=]\\s*[^\\s"',}]+"#
|
|
903
|
+
]
|
|
904
|
+
for pattern in patterns {
|
|
905
|
+
if let regex = try? NSRegularExpression(pattern: pattern) {
|
|
906
|
+
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
|
907
|
+
text = regex.stringByReplacingMatches(in: text, range: range, withTemplate: "[redacted]")
|
|
693
908
|
}
|
|
694
909
|
}
|
|
910
|
+
return text
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
func writeActionLog(_ text: String) {
|
|
914
|
+
let url = URL(fileURLWithPath: lastActionLogPath)
|
|
915
|
+
try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
916
|
+
if !FileManager.default.fileExists(atPath: lastActionLogPath) {
|
|
917
|
+
FileManager.default.createFile(atPath: lastActionLogPath, contents: Data(), attributes: [.posixPermissions: 0o600])
|
|
918
|
+
}
|
|
919
|
+
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: lastActionLogPath)
|
|
920
|
+
if let handle = try? FileHandle(forWritingTo: url) {
|
|
921
|
+
try? handle.truncate(atOffset: 0)
|
|
922
|
+
handle.write(Data(text.utf8))
|
|
923
|
+
try? handle.close()
|
|
924
|
+
}
|
|
695
925
|
}
|
|
696
926
|
|
|
697
927
|
struct MenuState {
|
|
@@ -699,27 +929,37 @@ struct MenuState {
|
|
|
699
929
|
let line: String
|
|
700
930
|
}
|
|
701
931
|
|
|
702
|
-
|
|
932
|
+
struct MenuBarConfig {
|
|
933
|
+
let quitWithCodex: Bool
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
703
937
|
var statusItem: NSStatusItem!
|
|
704
938
|
var statusLineItem: NSMenuItem!
|
|
939
|
+
var codexLbItem: NSMenuItem!
|
|
940
|
+
var oauthItem: NSMenuItem!
|
|
705
941
|
var timer: Timer?
|
|
942
|
+
var busy = false
|
|
943
|
+
var lastFailure = false
|
|
944
|
+
var quitWithCodex = false
|
|
706
945
|
|
|
707
946
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
708
947
|
NSApp.setActivationPolicy(.accessory)
|
|
948
|
+
quitWithCodex = readConfig().quitWithCodex
|
|
709
949
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
710
950
|
statusItem.autosaveName = "com.sneakoscope.sks-menubar"
|
|
711
|
-
statusItem.isVisible = true
|
|
712
951
|
if let button = statusItem.button {
|
|
713
952
|
configureStatusButton(button, title: "SKS")
|
|
714
953
|
}
|
|
715
954
|
|
|
716
955
|
let menu = NSMenu()
|
|
956
|
+
menu.delegate = self
|
|
717
957
|
statusLineItem = NSMenuItem(title: "SKS v\\(packageVersion) - starting", action: nil, keyEquivalent: "")
|
|
718
958
|
statusLineItem.isEnabled = false
|
|
719
959
|
menu.addItem(statusLineItem)
|
|
720
960
|
menu.addItem(NSMenuItem.separator())
|
|
721
|
-
add(menu, "Use codex-lb", #selector(useCodexLb))
|
|
722
|
-
add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
|
|
961
|
+
codexLbItem = add(menu, "Use codex-lb", #selector(useCodexLb))
|
|
962
|
+
oauthItem = add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
|
|
723
963
|
add(menu, "Set codex-lb Domain and Key", #selector(setCodexLbDomainAndKey))
|
|
724
964
|
menu.addItem(NSMenuItem.separator())
|
|
725
965
|
add(menu, "Set OpenRouter Key and GLM Profiles", #selector(setOpenRouterKey))
|
|
@@ -731,12 +971,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
731
971
|
add(menu, "Open Codex Settings", #selector(openCodexSettings))
|
|
732
972
|
add(menu, "Restart Codex", #selector(restartCodex))
|
|
733
973
|
menu.addItem(NSMenuItem.separator())
|
|
974
|
+
add(menu, "View Last Log", #selector(viewLastLog))
|
|
734
975
|
add(menu, "Quit SKS Menu", #selector(quit))
|
|
735
976
|
statusItem.menu = menu
|
|
977
|
+
configureCodexLifecycleSync()
|
|
736
978
|
updateState()
|
|
737
979
|
timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in self?.updateState() }
|
|
738
980
|
}
|
|
739
981
|
|
|
982
|
+
func menuWillOpen(_ menu: NSMenu) {
|
|
983
|
+
updateState()
|
|
984
|
+
updateAuthModeChecks()
|
|
985
|
+
}
|
|
986
|
+
|
|
740
987
|
func configureStatusButton(_ button: NSStatusBarButton, title: String) {
|
|
741
988
|
button.image = nil
|
|
742
989
|
button.title = title
|
|
@@ -752,10 +999,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
752
999
|
button.setAccessibilityHelp("Open SKS menu")
|
|
753
1000
|
}
|
|
754
1001
|
|
|
755
|
-
func add(_ menu: NSMenu, _ title: String, _ selector: Selector) {
|
|
1002
|
+
func add(_ menu: NSMenu, _ title: String, _ selector: Selector) -> NSMenuItem {
|
|
756
1003
|
let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
|
|
757
1004
|
item.target = self
|
|
758
1005
|
menu.addItem(item)
|
|
1006
|
+
return item
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
${codexLifecycleSource}
|
|
1010
|
+
|
|
1011
|
+
func readConfig() -> MenuBarConfig {
|
|
1012
|
+
guard let data = FileManager.default.contents(atPath: menubarConfigPath),
|
|
1013
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
1014
|
+
return MenuBarConfig(quitWithCodex: false)
|
|
1015
|
+
}
|
|
1016
|
+
return MenuBarConfig(quitWithCodex: json["quit_with_codex"] as? Bool == true)
|
|
759
1017
|
}
|
|
760
1018
|
|
|
761
1019
|
func updateState() {
|
|
@@ -767,20 +1025,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
767
1025
|
}
|
|
768
1026
|
|
|
769
1027
|
func readMenuState() -> MenuState {
|
|
770
|
-
|
|
771
|
-
|
|
1028
|
+
if codexBundleId == nil {
|
|
1029
|
+
return MenuState(title: "SKS", line: "Codex app not detected — sync disabled")
|
|
1030
|
+
}
|
|
1031
|
+
if busy {
|
|
1032
|
+
return MenuState(title: "SKS ⋯", line: "SKS v\\(packageVersion) - working")
|
|
1033
|
+
}
|
|
1034
|
+
if !actionScriptUsable() {
|
|
772
1035
|
return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - action script broken (run sks doctor --fix)")
|
|
773
1036
|
}
|
|
1037
|
+
if lastFailure {
|
|
1038
|
+
return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - last action failed")
|
|
1039
|
+
}
|
|
774
1040
|
if updateAvailable() {
|
|
775
1041
|
return MenuState(title: "SKS ↑", line: "SKS v\\(packageVersion) - update available")
|
|
776
1042
|
}
|
|
777
1043
|
return MenuState(title: "SKS", line: "SKS v\\(packageVersion) - OK")
|
|
778
1044
|
}
|
|
779
1045
|
|
|
780
|
-
func
|
|
781
|
-
|
|
782
|
-
guard let entry = shellAssignment(text, key: "SKS_ENTRY") else { return false }
|
|
783
|
-
return FileManager.default.fileExists(atPath: entry)
|
|
1046
|
+
func actionScriptUsable() -> Bool {
|
|
1047
|
+
return FileManager.default.isExecutableFile(atPath: actionScript)
|
|
784
1048
|
}
|
|
785
1049
|
|
|
786
1050
|
func updateAvailable() -> Bool {
|
|
@@ -792,48 +1056,83 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
792
1056
|
return latest != packageVersion && !latest.isEmpty
|
|
793
1057
|
}
|
|
794
1058
|
|
|
795
|
-
func
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1059
|
+
func runSksCapture(_ args: [String], title: String, stdinText: String? = nil, notify: Bool = true, completion: ((Int32, String) -> Void)? = nil) {
|
|
1060
|
+
busy = true
|
|
1061
|
+
lastFailure = false
|
|
1062
|
+
updateState()
|
|
1063
|
+
runProcess(actionScript, args, stdinText: stdinText) { [weak self] code, output in
|
|
1064
|
+
let redacted = redactSecrets(output, secrets: stdinText == nil ? [] : [stdinText ?? ""])
|
|
1065
|
+
writeActionLog("$ sks \\(args.joined(separator: " "))\\n\\(redacted)\\n")
|
|
1066
|
+
DispatchQueue.main.async {
|
|
1067
|
+
self?.busy = false
|
|
1068
|
+
self?.lastFailure = code != 0
|
|
1069
|
+
self?.updateState()
|
|
1070
|
+
if notify {
|
|
1071
|
+
if code == 0 {
|
|
1072
|
+
showNotification(title, "OK\\n" + redacted)
|
|
1073
|
+
} else {
|
|
1074
|
+
showAlert(title + " failed", informative: redacted)
|
|
1075
|
+
}
|
|
803
1076
|
}
|
|
804
|
-
|
|
1077
|
+
completion?(code, redacted)
|
|
805
1078
|
}
|
|
806
1079
|
}
|
|
807
|
-
return nil
|
|
808
1080
|
}
|
|
809
1081
|
|
|
810
|
-
func
|
|
811
|
-
|
|
812
|
-
runInTerminal("\\(shellQuote(actionScript)) \\(quoted); \\(tail)")
|
|
1082
|
+
func runSksBackground(_ args: [String], title: String, stdinText: String? = nil, completion: ((Int32, String) -> Void)? = nil) {
|
|
1083
|
+
runSksCapture(args, title: title, stdinText: stdinText, notify: true, completion: completion)
|
|
813
1084
|
}
|
|
814
1085
|
|
|
815
|
-
func
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1086
|
+
func startSksDetached(_ args: [String], title: String) {
|
|
1087
|
+
let process = Process()
|
|
1088
|
+
process.executableURL = URL(fileURLWithPath: actionScript)
|
|
1089
|
+
process.arguments = args
|
|
1090
|
+
writeActionLog("$ sks \\(args.joined(separator: " "))\\nstarted\\n")
|
|
1091
|
+
if let handle = try? FileHandle(forWritingTo: URL(fileURLWithPath: lastActionLogPath)) {
|
|
1092
|
+
process.standardOutput = handle
|
|
1093
|
+
process.standardError = handle
|
|
1094
|
+
}
|
|
1095
|
+
do {
|
|
1096
|
+
try process.run()
|
|
1097
|
+
showNotification(title, "started")
|
|
1098
|
+
} catch {
|
|
1099
|
+
showAlert(title + " failed", informative: String(describing: error))
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
func updateAuthModeChecks() {
|
|
1104
|
+
runSksCapture(["codex-lb", "status", "--json"], title: "SKS Auth Status", notify: false) { [weak self] code, output in
|
|
1105
|
+
guard let self = self else { return }
|
|
1106
|
+
let lower = output.lowercased()
|
|
1107
|
+
let codexLbActive = code == 0 && (lower.contains(#""configured": true"#) || lower.contains(#""model_provider": "codex-lb""#) || lower.contains(#""mode": "codex-lb""#))
|
|
1108
|
+
self.codexLbItem.state = codexLbActive ? .on : .off
|
|
1109
|
+
self.oauthItem.state = codexLbActive ? .off : .on
|
|
819
1110
|
}
|
|
820
1111
|
}
|
|
821
1112
|
|
|
822
1113
|
@objc func useCodexLb() {
|
|
823
|
-
|
|
1114
|
+
runSksBackground(["codex-lb", "use-codex-lb", "--json"], title: "Use codex-lb") { [weak self] code, _ in
|
|
1115
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1116
|
+
}
|
|
824
1117
|
}
|
|
825
1118
|
|
|
826
1119
|
@objc func useChatGptOAuth() {
|
|
827
|
-
|
|
1120
|
+
runSksBackground(["codex-lb", "use-oauth", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
|
|
1121
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1122
|
+
}
|
|
828
1123
|
}
|
|
829
1124
|
|
|
830
1125
|
@objc func setCodexLbDomainAndKey() {
|
|
831
|
-
|
|
1126
|
+
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 }
|
|
1127
|
+
guard let key = promptText(title: "Set codex-lb Key", message: "Enter your codex-lb API key.", placeholder: "sk-clb-...", secure: true) else { return }
|
|
1128
|
+
runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
|
|
1129
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1130
|
+
}
|
|
832
1131
|
}
|
|
833
1132
|
|
|
834
1133
|
@objc func setOpenRouterKey() {
|
|
835
|
-
let
|
|
836
|
-
|
|
1134
|
+
guard let key = promptText(title: "Set OpenRouter Key", message: "Enter your OpenRouter API key.", placeholder: "sk-or-v1-...", secure: true) else { return }
|
|
1135
|
+
runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
|
|
837
1136
|
}
|
|
838
1137
|
|
|
839
1138
|
@objc func fastCheck() {
|
|
@@ -845,13 +1144,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
845
1144
|
}
|
|
846
1145
|
|
|
847
1146
|
@objc func updateSksNow() {
|
|
848
|
-
|
|
1147
|
+
runSksBackground(["update"], title: "Update SKS Now")
|
|
849
1148
|
}
|
|
850
1149
|
|
|
851
1150
|
@objc func openDashboard() {
|
|
852
|
-
let
|
|
853
|
-
|
|
854
|
-
|
|
1151
|
+
let urlString = "http://127.0.0.1:4477"
|
|
1152
|
+
runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
|
|
1153
|
+
DispatchQueue.main.async {
|
|
1154
|
+
if code != 0 {
|
|
1155
|
+
self?.startSksDetached(["ui"], title: "SKS Dashboard")
|
|
1156
|
+
}
|
|
1157
|
+
if let url = URL(string: urlString) {
|
|
1158
|
+
NSWorkspace.shared.open(url)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
855
1162
|
}
|
|
856
1163
|
|
|
857
1164
|
@objc func openCodexSettings() {
|
|
@@ -859,7 +1166,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
859
1166
|
}
|
|
860
1167
|
|
|
861
1168
|
@objc func restartCodex() {
|
|
862
|
-
|
|
1169
|
+
let running = NSWorkspace.shared.runningApplications.filter { app in
|
|
1170
|
+
if let bundle = codexBundleId, app.bundleIdentifier == bundle { return true }
|
|
1171
|
+
return app.localizedName == "Codex"
|
|
1172
|
+
}
|
|
1173
|
+
for app in running {
|
|
1174
|
+
app.terminate()
|
|
1175
|
+
}
|
|
1176
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
1177
|
+
if let bundle = codexBundleId, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundle) {
|
|
1178
|
+
NSWorkspace.shared.open(url)
|
|
1179
|
+
showNotification("Restart Codex", "requested")
|
|
1180
|
+
} else {
|
|
1181
|
+
showAlert("Restart Codex failed", informative: "Codex app bundle could not be resolved.")
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
@objc func viewLastLog() {
|
|
1187
|
+
if FileManager.default.fileExists(atPath: lastActionLogPath) {
|
|
1188
|
+
NSWorkspace.shared.open(URL(fileURLWithPath: lastActionLogPath))
|
|
1189
|
+
} else {
|
|
1190
|
+
showAlert("No SKS menu log yet", informative: lastActionLogPath)
|
|
1191
|
+
}
|
|
863
1192
|
}
|
|
864
1193
|
|
|
865
1194
|
@objc func quit() {
|
|
@@ -1077,13 +1406,14 @@ async function inspectLaunchdService(env = process.env) {
|
|
|
1077
1406
|
const text = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
1078
1407
|
const state = text.match(/\bstate = ([^\n]+)/)?.[1]?.trim() || null;
|
|
1079
1408
|
const pidText = text.match(/\bpid = (\d+)/)?.[1] || null;
|
|
1409
|
+
const running = result.code === 0 && (state === 'running' || Boolean(pidText));
|
|
1080
1410
|
return {
|
|
1081
1411
|
checked: true,
|
|
1082
|
-
ok:
|
|
1412
|
+
ok: running,
|
|
1083
1413
|
service,
|
|
1084
1414
|
state,
|
|
1085
1415
|
pid: pidText ? Number(pidText) : null,
|
|
1086
|
-
error:
|
|
1416
|
+
error: running ? null : String(result.stderr || result.stdout || state || 'launchd_not_running').trim()
|
|
1087
1417
|
};
|
|
1088
1418
|
}
|
|
1089
1419
|
async function inspectSignature(appPath, env = process.env) {
|
|
@@ -1175,6 +1505,8 @@ async function writeReport(reportPath, result) {
|
|
|
1175
1505
|
}
|
|
1176
1506
|
catch (err) {
|
|
1177
1507
|
result.report_write_failed = true;
|
|
1508
|
+
if (!result.warnings.includes('menubar_report_write_failed'))
|
|
1509
|
+
result.warnings.push('menubar_report_write_failed');
|
|
1178
1510
|
console.error(`warning: failed to write SKS menubar report ${reportPath}: ${err?.message || err}`);
|
|
1179
1511
|
}
|
|
1180
1512
|
}
|