sneakoscope 4.8.7 → 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 +51 -726
- package/config/bench-baseline.json +7 -0
- 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/install.js +35 -0
- package/dist/bin/sks.js +1 -1
- package/dist/cli/cli-theme.js +51 -0
- package/dist/cli/command-registry.js +15 -4
- package/dist/cli/help-fast.js +18 -8
- package/dist/cli/insane-search-command.js +36 -8
- package/dist/cli/install-helpers.js +32 -0
- package/dist/commands/codex-lb.js +2 -0
- package/dist/commands/doctor.js +94 -9
- package/dist/commands/proof.js +6 -3
- package/dist/config/skills-manifest.json +82 -54
- package/dist/core/agents/agent-conflict-graph.js +5 -0
- package/dist/core/agents/agent-lease.js +5 -0
- package/dist/core/agents/agent-ledger-schemas.js +1 -0
- package/dist/core/agents/agent-lifecycle.js +5 -3
- package/dist/core/agents/agent-orchestrator.js +268 -62
- package/dist/core/agents/agent-output-validator.js +14 -3
- package/dist/core/agents/agent-patch-queue-store.js +95 -2
- package/dist/core/agents/agent-patch-queue.js +6 -3
- package/dist/core/agents/agent-patch-schema.js +13 -0
- package/dist/core/agents/agent-proof-evidence.js +6 -1
- package/dist/core/agents/agent-runner-fake.js +40 -0
- package/dist/core/agents/agent-worker-pipeline.js +36 -2
- package/dist/core/agents/codex-exec-worker-adapter.js +2 -0
- package/dist/core/agents/native-cli-worker.js +0 -1
- package/dist/core/agents/native-worker-backend-router.js +58 -1
- package/dist/core/codex-app/sks-menubar.js +477 -132
- package/dist/core/codex-app.js +1 -0
- package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
- package/dist/core/codex-control/codex-sdk-env-policy.js +1 -1
- package/dist/core/codex-control/schemas/agent-worker-result.schema.js +12 -3
- package/dist/core/codex-hooks/codex-hook-managed-install.js +5 -0
- package/dist/core/codex-hooks/codex-hook-state-writer.js +70 -11
- package/dist/core/codex-hooks/codex-hook-trust-doctor.js +2 -2
- package/dist/core/codex-lb/codex-lb-env.js +20 -1
- package/dist/core/commands/basic-cli.js +18 -1
- package/dist/core/commands/check-command.js +37 -5
- package/dist/core/commands/computer-use-command.js +62 -7
- package/dist/core/commands/db-command.js +31 -5
- package/dist/core/commands/fast-mode-command.js +3 -0
- package/dist/core/commands/gate-result-contract.js +43 -0
- package/dist/core/commands/gates-command.js +6 -1
- package/dist/core/commands/gc-command.js +29 -2
- package/dist/core/commands/goal-command.js +9 -2
- package/dist/core/commands/gx-command.js +79 -7
- package/dist/core/commands/image-ux-review-command.js +188 -13
- package/dist/core/commands/mad-db-command.js +85 -176
- package/dist/core/commands/mad-sks-command.js +230 -29
- package/dist/core/commands/menubar-command.js +24 -2
- package/dist/core/commands/naruto-command.js +64 -10
- package/dist/core/commands/plan-command.js +76 -0
- package/dist/core/commands/ppt-command.js +159 -24
- package/dist/core/commands/qa-loop-command.js +6 -2
- package/dist/core/commands/release-command.js +55 -2
- package/dist/core/commands/research-command.js +1 -1
- package/dist/core/commands/review-command.js +217 -0
- package/dist/core/commands/route-success-helpers.js +59 -0
- package/dist/core/commands/run-command.js +19 -7
- package/dist/core/commands/seo-command.js +49 -1
- package/dist/core/commands/ui-command.js +161 -0
- package/dist/core/commands/uninstall-command.js +15 -1
- package/dist/core/db-safety.js +2 -2
- package/dist/core/doctor/doctor-dirty-planner.js +51 -7
- package/dist/core/doctor/doctor-transaction.js +8 -0
- package/dist/core/doctor/doctor-zellij-repair.js +1 -0
- package/dist/core/doctor/imagegen-repair.js +161 -0
- package/dist/core/feature-fixture-runner.js +2 -2
- package/dist/core/feature-fixtures.js +58 -37
- package/dist/core/feature-registry.js +102 -2
- package/dist/core/fsx.js +39 -6
- package/dist/core/image-ux-review/imagegen-adapter.js +48 -11
- package/dist/core/image-ux-review.js +16 -0
- package/dist/core/imagegen/imagegen-capability.js +11 -5
- package/dist/core/imagegen/require-imagegen.js +57 -0
- package/dist/core/init/skills.js +10 -5
- package/dist/core/mad-db/mad-db-coordinator.js +94 -18
- package/dist/core/mad-db/mad-db-policy.js +12 -10
- package/dist/core/mad-sks/executors/executor-base.js +8 -2
- package/dist/core/mad-sks/executors/index.js +4 -0
- package/dist/core/mad-sks/executors/sql-plane-executor.js +194 -0
- package/dist/core/naruto/naruto-active-pool.js +15 -4
- package/dist/core/naruto/naruto-real-worker-child.js +11 -1
- package/dist/core/naruto/naruto-role-policy.js +3 -0
- package/dist/core/naruto/naruto-task-hints.js +10 -0
- package/dist/core/naruto/naruto-work-graph.js +8 -2
- package/dist/core/naruto/naruto-work-item.js +6 -0
- package/dist/core/naruto/solution-tournament.js +101 -0
- package/dist/core/permission-gates.js +30 -0
- package/dist/core/pipeline-internals/runtime-core.js +11 -3
- package/dist/core/pipeline-internals/runtime-gates.js +27 -0
- package/dist/core/ppt-review/index.js +6 -1
- package/dist/core/ppt-review/slide-imagegen-review.js +75 -10
- package/dist/core/ppt.js +70 -2
- package/dist/core/proof/auto-finalize.js +70 -11
- package/dist/core/proof/proof-schema.js +2 -0
- package/dist/core/proof/route-adapter.js +5 -1
- package/dist/core/proof/route-finalizer.js +8 -5
- package/dist/core/proof/selftest-proof-fixtures.js +18 -5
- package/dist/core/proof/validation.js +2 -0
- package/dist/core/provider/model-router.js +53 -0
- package/dist/core/release/release-gate-node.js +3 -0
- package/dist/core/release/release-gate-resource-governor.js +1 -0
- package/dist/core/routes/constants.js +1 -1
- package/dist/core/routes.js +65 -18
- package/dist/core/safety/mutation-guard.js +5 -1
- package/dist/core/stop-gate/gate-evaluator.js +102 -0
- package/dist/core/stop-gate/stop-gate-check.js +16 -1
- package/dist/core/stop-gate/stop-gate-writer.js +4 -0
- package/dist/core/triwiki/agents-md-projector.js +184 -0
- package/dist/core/triwiki-wrongness/wrongness-schema.js +3 -1
- package/dist/core/trust-kernel/trust-kernel-schema.js +1 -0
- package/dist/core/trust-kernel/trust-report.js +1 -1
- package/dist/core/trust-kernel/trust-status.js +2 -0
- package/dist/core/ui/dashboard-html.js +111 -0
- package/dist/core/update/update-migration-state.js +386 -15
- package/dist/core/update-check.js +178 -81
- package/dist/core/verification/diff-quality.js +100 -0
- package/dist/core/verification/impact-scan.js +164 -0
- package/dist/core/verification/machine-feedback.js +146 -0
- package/dist/core/verification/mistake-rule-compiler.js +195 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-self-heal.js +9 -6
- package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
- package/dist/scripts/check-feature-quality.js +2 -2
- package/dist/scripts/cli-output-consistency-check.js +57 -0
- package/dist/scripts/coding-bench-check.js +136 -0
- package/dist/scripts/doctor-imagegen-repair-check.js +62 -0
- package/dist/scripts/ensure-bin-executable.js +11 -3
- package/dist/scripts/harness-benchmark-check.js +104 -0
- package/dist/scripts/legacy-update-e2e-check.js +228 -0
- package/dist/scripts/mad-db-command-check.js +11 -8
- package/dist/scripts/mad-db-route-identity-check.js +8 -6
- package/dist/scripts/mad-db-safety-conflict-matrix-check.js +31 -8
- package/dist/scripts/mad-db-skill-policy-snapshot-check.js +4 -5
- package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
- package/dist/scripts/ppt-real-imagegen-wiring-check.js +5 -1
- package/dist/scripts/proof-root-cause-policy-check.js +2 -0
- package/dist/scripts/release-dag-full-coverage-check.js +13 -4
- package/dist/scripts/release-gate-dag-runner.js +14 -0
- package/dist/scripts/release-gate-existence-audit.js +2 -1
- package/dist/scripts/release-gate-planner.js +2 -1
- package/dist/scripts/release-gate-script-parity-check.js +45 -17
- package/dist/scripts/release-metadata-1-19-check.js +2 -1
- package/dist/scripts/seo-geo-feature-fixture-quality-check.js +1 -1
- package/dist/scripts/sks-1-11-gate-lib.js +8 -6
- package/dist/scripts/sks-menubar-install-check.js +57 -20
- package/dist/scripts/ux-review-real-loop-fixture-check.js +9 -4
- package/dist/scripts/ux-review-run-wires-imagegen-check.js +4 -0
- package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
- package/docs/assets/sneakoscope-architecture-pipeline.jpg +0 -0
- package/docs/demo.tape +28 -0
- package/package.json +31 -24
- package/schemas/codex/completion-proof.schema.json +3 -2
- package/schemas/release/release-gate-node.schema.json +2 -1
- package/dist/scripts/agent-backfill-route-blackbox.js +0 -5
- package/dist/scripts/agent-dynamic-pool-route-blackbox.js +0 -5
- package/dist/scripts/agent-parallel-write-blackbox.js +0 -56
- package/dist/scripts/agent-patch-swarm-route-blackbox.js +0 -10
- package/dist/scripts/agent-route-blackbox-lib.js +0 -132
- package/dist/scripts/blackbox-command-import-smoke.js +0 -143
- package/dist/scripts/blackbox-global-shim.js +0 -77
- package/dist/scripts/blackbox-matrix.js +0 -70
- package/dist/scripts/blackbox-npx-one-shot.js +0 -69
- package/dist/scripts/blackbox-pack-install.js +0 -174
- package/dist/scripts/brand-neutrality-zero-leakage-blackbox.js +0 -4
- package/dist/scripts/build-once-runner-blackbox.js +0 -34
- package/dist/scripts/codex-0140-integration-blackbox.js +0 -13
- package/dist/scripts/codex-agent-type-blackbox.js +0 -4
- package/dist/scripts/codex-app-harness-blackbox.js +0 -4
- package/dist/scripts/codex-app-skill-agent-blackbox.js +0 -4
- package/dist/scripts/codex-hook-approval-blackbox.js +0 -4
- package/dist/scripts/codex-init-deep-directory-local-blackbox.js +0 -4
- package/dist/scripts/codex-native-feature-broker-blackbox.js +0 -4
- package/dist/scripts/codex-native-pattern-analysis-blackbox.js +0 -4
- package/dist/scripts/codex-native-read-repair-split-blackbox.js +0 -55
- package/dist/scripts/codex-native-reference-cache-blackbox.js +0 -41
- package/dist/scripts/core-skill-integrity-blackbox.js +0 -32
- package/dist/scripts/dfix-fast-blackbox-check.js +0 -37
- package/dist/scripts/dfix-parallel-write-blackbox.js +0 -48
- package/dist/scripts/dfix-patch-swarm-route-blackbox.js +0 -10
- package/dist/scripts/doctor-dirty-repair-blackbox.js +0 -22
- package/dist/scripts/doctor-dirty-semantic-blackbox.js +0 -8
- package/dist/scripts/doctor-native-capability-repair-blackbox.js +0 -39
- package/dist/scripts/doctor-transaction-engine-blackbox.js +0 -28
- package/dist/scripts/doctor-zellij-fix-blackbox.js +0 -4
- package/dist/scripts/doctor-zellij-no-homebrew-blackbox.js +0 -4
- package/dist/scripts/doctor-zellij-upgrade-blackbox.js +0 -4
- package/dist/scripts/gate-pack-runner-blackbox.js +0 -27
- package/dist/scripts/gate-pack-v2-blackbox.js +0 -18
- package/dist/scripts/geo-cli-blackbox-check.js +0 -18
- package/dist/scripts/loop-collision-blackbox.js +0 -3
- package/dist/scripts/loop-concurrency-oversubscription-blackbox.js +0 -3
- package/dist/scripts/loop-fixture-production-misuse-blackbox.js +0 -3
- package/dist/scripts/loop-kill-interrupt-real-blackbox.js +0 -3
- package/dist/scripts/loop-merge-strategy-blackbox.js +0 -3
- package/dist/scripts/loop-mesh-production-e2e-blackbox.js +0 -3
- package/dist/scripts/loop-side-effect-blackbox.js +0 -3
- package/dist/scripts/mad-db-operation-lifecycle-blackbox.js +0 -29
- package/dist/scripts/mad-sks-actual-executor-blackbox.js +0 -5
- package/dist/scripts/mad-zellij-headless-fallback-blackbox.js +0 -4
- package/dist/scripts/mad-zellij-self-heal-blackbox.js +0 -4
- package/dist/scripts/naruto-loop-mesh-blackbox.js +0 -3
- package/dist/scripts/naruto-real-parallelism-blackbox.js +0 -307
- package/dist/scripts/naruto-worktree-coding-blackbox.js +0 -29
- package/dist/scripts/parallel-runtime-real-blackbox.js +0 -44
- package/dist/scripts/pipeline-codex-native-doctor-mad-routing-real-blackbox.js +0 -77
- package/dist/scripts/pipeline-codex-native-e2e-blackbox.js +0 -13
- package/dist/scripts/pipeline-codex-native-image-routing-real-blackbox.js +0 -50
- package/dist/scripts/pipeline-codex-native-loop-routing-real-blackbox.js +0 -74
- package/dist/scripts/pipeline-codex-native-qa-routing-real-blackbox.js +0 -51
- package/dist/scripts/pipeline-codex-native-research-routing-real-blackbox.js +0 -45
- package/dist/scripts/pipeline-execution-profile-routing-blackbox.js +0 -4
- package/dist/scripts/postinstall-global-doctor-blackbox.js +0 -12
- package/dist/scripts/ppt-full-e2e-blackbox-check.js +0 -109
- package/dist/scripts/ppt-imagegen-blackbox-check.js +0 -46
- package/dist/scripts/qa-backfill-route-blackbox.js +0 -5
- package/dist/scripts/qa-patch-swarm-route-blackbox.js +0 -10
- package/dist/scripts/release-full-parallelism-blackbox.js +0 -41
- package/dist/scripts/release-triwiki-first-runner-blackbox.js +0 -48
- package/dist/scripts/release-wiring-3110-blackbox.js +0 -27
- package/dist/scripts/release-wiring-3112-blackbox.js +0 -17
- package/dist/scripts/release-wiring-3113-blackbox.js +0 -17
- package/dist/scripts/research-backfill-route-blackbox.js +0 -5
- package/dist/scripts/research-final-reviewer-blackbox.js +0 -70
- package/dist/scripts/research-stage-cycle-runtime-blackbox.js +0 -40
- package/dist/scripts/research-synthesis-writer-blackbox.js +0 -24
- package/dist/scripts/route-blackbox-realism-check.js +0 -21
- package/dist/scripts/scheduler-resource-claim-blackbox.js +0 -24
- package/dist/scripts/seo-cli-blackbox-check.js +0 -18
- package/dist/scripts/sks-3110-all-feature-regression-blackbox.js +0 -116
- package/dist/scripts/sks-3112-all-feature-regression-blackbox.js +0 -29
- package/dist/scripts/sks-3113-all-feature-regression-blackbox.js +0 -17
- package/dist/scripts/sks-400-all-feature-regression-blackbox.js +0 -21
- package/dist/scripts/sks-400-extreme-parallel-blackbox.js +0 -8
- package/dist/scripts/sks-400-five-minute-blackbox.js +0 -9
- package/dist/scripts/sks-400-legacy-purge-blackbox.js +0 -8
- package/dist/scripts/sks-401-all-feature-regression-blackbox.js +0 -46
- package/dist/scripts/sks-401-five-minute-actual-blackbox.js +0 -23
- package/dist/scripts/sks-402-all-feature-regression-blackbox.js +0 -9
- package/dist/scripts/sks-402-five-minute-real-blackbox.js +0 -22
- package/dist/scripts/sksd-warm-cache-blackbox.js +0 -12
- package/dist/scripts/team-backfill-route-blackbox.js +0 -5
- package/dist/scripts/team-parallel-write-blackbox.js +0 -55
- package/dist/scripts/team-patch-swarm-route-blackbox.js +0 -10
- package/dist/scripts/triwiki-affected-graph-blackbox.js +0 -28
- package/dist/scripts/triwiki-proof-bank-blackbox.js +0 -30
- package/dist/scripts/triwiki-proof-bank-lock-blackbox.js +0 -7
- package/dist/scripts/ux-patch-swarm-route-blackbox.js +0 -10
- package/dist/scripts/ux-review-imagegen-blackbox-check.js +0 -67
- package/dist/scripts/zellij-initial-main-only-blackbox.js +0 -28
- package/dist/scripts/zellij-pane-lock-concurrency-blackbox.js +0 -80
- package/dist/scripts/zellij-pane-lock-open-worker-integration-blackbox.js +0 -137
- package/dist/scripts/zellij-self-heal-typed-blackbox.js +0 -4
- package/dist/scripts/zellij-slot-telemetry-real-blackbox.js +0 -20
- package/dist/scripts/zellij-stacked-fallback-integration-blackbox.js +0 -81
- package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +0 -202
|
@@ -3,6 +3,8 @@ 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';
|
|
7
|
+
import { withHeartbeat } from '../../cli/cli-theme.js';
|
|
6
8
|
export const SKS_MENUBAR_LABEL = 'com.sneakoscope.sks-menubar';
|
|
7
9
|
const LABEL = SKS_MENUBAR_LABEL;
|
|
8
10
|
const CONTROL_CENTER_DOMAIN = 'com.apple.controlcenter';
|
|
@@ -16,8 +18,10 @@ const MENU_ITEMS = [
|
|
|
16
18
|
'Fast Check',
|
|
17
19
|
'SKS Version Check',
|
|
18
20
|
'Update SKS Now',
|
|
21
|
+
'Open Dashboard',
|
|
19
22
|
'Open Codex Settings',
|
|
20
23
|
'Restart Codex',
|
|
24
|
+
'View Last Log',
|
|
21
25
|
'Quit SKS Menu'
|
|
22
26
|
];
|
|
23
27
|
export function sksMenuBarPaths(homeInput, rootInput) {
|
|
@@ -41,10 +45,13 @@ export function sksMenuBarPaths(homeInput, rootInput) {
|
|
|
41
45
|
info_plist_path: path.join(contentsPath, 'Info.plist'),
|
|
42
46
|
action_script_path: path.join(installDir, 'sks-menubar-action.sh'),
|
|
43
47
|
build_stamp_path: path.join(installDir, 'build-stamp.json'),
|
|
48
|
+
config_path: path.join(installDir, 'config.json'),
|
|
44
49
|
launch_agent_path: path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`),
|
|
45
50
|
report_path: path.join(root, '.sneakoscope', 'reports', 'sks-menubar.json'),
|
|
46
51
|
stdout_log_path: path.join(installDir, 'menubar.out.log'),
|
|
47
|
-
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')
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
export async function installSksMenuBar(opts = {}) {
|
|
@@ -55,6 +62,7 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
55
62
|
const warnings = [];
|
|
56
63
|
const nextActions = defaultNextActions();
|
|
57
64
|
let secretEnvCleanup;
|
|
65
|
+
let codexBundleId = null;
|
|
58
66
|
if (process.platform !== 'darwin') {
|
|
59
67
|
const result = {
|
|
60
68
|
schema: 'sks.codex-app-sks-menubar.v1',
|
|
@@ -67,7 +75,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
67
75
|
launch_agent_path: null,
|
|
68
76
|
action_script_path: null,
|
|
69
77
|
build_stamp_path: null,
|
|
78
|
+
config_path: null,
|
|
70
79
|
report_path: apply ? paths.report_path : null,
|
|
80
|
+
codex_bundle_id: null,
|
|
71
81
|
menu_items: MENU_ITEMS,
|
|
72
82
|
actions: [],
|
|
73
83
|
launch: { requested: false, method: 'none', ok: true },
|
|
@@ -84,9 +94,10 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
84
94
|
const installed = await exists(paths.executable_path);
|
|
85
95
|
const launchAgent = await exists(paths.launch_agent_path);
|
|
86
96
|
const status = await inspectSksMenuBarStatus({ home: paths.home, root: paths.root }).catch(() => null);
|
|
97
|
+
const statusWarnings = [...(status?.warnings || []), ...(launchAgent ? [] : ['launch_agent_not_installed_yet'])];
|
|
87
98
|
return {
|
|
88
99
|
schema: 'sks.codex-app-sks-menubar.v1',
|
|
89
|
-
ok: true,
|
|
100
|
+
ok: status ? status.ok === true : true,
|
|
90
101
|
apply,
|
|
91
102
|
status: 'planned',
|
|
92
103
|
platform: process.platform,
|
|
@@ -95,7 +106,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
95
106
|
launch_agent_path: paths.launch_agent_path,
|
|
96
107
|
action_script_path: paths.action_script_path,
|
|
97
108
|
build_stamp_path: paths.build_stamp_path,
|
|
109
|
+
config_path: paths.config_path,
|
|
98
110
|
report_path: paths.report_path,
|
|
111
|
+
codex_bundle_id: status?.codex_sync.bundle_id || null,
|
|
99
112
|
menu_items: MENU_ITEMS,
|
|
100
113
|
actions: installed ? ['menubar_app_present'] : ['menubar_app_install_available'],
|
|
101
114
|
launch: {
|
|
@@ -114,11 +127,12 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
114
127
|
build_stamp: status?.build_stamp || null,
|
|
115
128
|
tcc_automation_status: 'unknown',
|
|
116
129
|
next_actions: launchAgent ? defaultNextActions() : ['Run: sks menubar install'],
|
|
117
|
-
blockers: [],
|
|
118
|
-
warnings:
|
|
130
|
+
blockers: status?.blockers || [],
|
|
131
|
+
warnings: statusWarnings
|
|
119
132
|
};
|
|
120
133
|
}
|
|
121
134
|
await ensureDir(paths.install_dir);
|
|
135
|
+
await ensureDir(paths.logs_dir);
|
|
122
136
|
await ensureDir(path.dirname(paths.launch_agent_path));
|
|
123
137
|
secretEnvCleanup = await cleanupMacLaunchSecretEnvironment({ env }).catch((err) => ({
|
|
124
138
|
ok: false,
|
|
@@ -137,9 +151,17 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
137
151
|
const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || await fallbackTool('/usr/bin/open');
|
|
138
152
|
const codesign = env.SKS_MENUBAR_CODESIGN || await which('codesign').catch(() => null) || await fallbackTool('/usr/bin/codesign');
|
|
139
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');
|
|
140
157
|
if (!swiftc)
|
|
141
158
|
return await blockedResult('swiftc_missing', 'swiftc not found');
|
|
142
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}`);
|
|
143
165
|
const target = await resolveSksEntryForInstall({
|
|
144
166
|
...(opts.sksEntry ? { explicit: opts.sksEntry } : {}),
|
|
145
167
|
root: paths.root,
|
|
@@ -160,6 +182,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
160
182
|
const swiftSource = swiftMenuSource({
|
|
161
183
|
actionScriptPath: paths.action_script_path,
|
|
162
184
|
buildStampPath: paths.build_stamp_path,
|
|
185
|
+
configPath: paths.config_path,
|
|
186
|
+
lastActionLogPath: paths.last_action_log_path,
|
|
187
|
+
codexBundleId: config.codex_bundle_id,
|
|
163
188
|
packageVersion: PACKAGE_VERSION
|
|
164
189
|
});
|
|
165
190
|
const infoPlist = infoPlistSource(PACKAGE_VERSION);
|
|
@@ -198,9 +223,6 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
198
223
|
}
|
|
199
224
|
}
|
|
200
225
|
if (!stampMatches && !binaryStable) {
|
|
201
|
-
const clt = await xcodeCltStatus(xcodeSelect);
|
|
202
|
-
if (!clt.ok)
|
|
203
|
-
return await blockedResult('xcode_clt_missing', clt.error || 'Xcode Command Line Tools missing');
|
|
204
226
|
try {
|
|
205
227
|
await buildMenuBarAppAtomically({
|
|
206
228
|
paths,
|
|
@@ -208,7 +230,8 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
208
230
|
codesign,
|
|
209
231
|
swiftSource,
|
|
210
232
|
infoPlist,
|
|
211
|
-
actions
|
|
233
|
+
actions,
|
|
234
|
+
quiet: opts.quiet === true
|
|
212
235
|
});
|
|
213
236
|
}
|
|
214
237
|
catch (err) {
|
|
@@ -245,7 +268,7 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
245
268
|
warnings.push(preferredPosition.warning);
|
|
246
269
|
}
|
|
247
270
|
const launch = launchRequested && launchctl && !(stampMatches && runningBeforeLaunch)
|
|
248
|
-
? await launchWithLaunchctl({ launchctl, open, paths, skipIfRunning: stampMatches })
|
|
271
|
+
? await launchWithLaunchctl({ launchctl, open, paths, skipIfRunning: stampMatches, quiet: opts.quiet === true })
|
|
249
272
|
: {
|
|
250
273
|
requested: launchRequested && !(stampMatches && runningBeforeLaunch),
|
|
251
274
|
method: 'skipped',
|
|
@@ -274,7 +297,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
274
297
|
launch_agent_path: paths.launch_agent_path,
|
|
275
298
|
action_script_path: paths.action_script_path,
|
|
276
299
|
build_stamp_path: paths.build_stamp_path,
|
|
300
|
+
config_path: paths.config_path,
|
|
277
301
|
report_path: paths.report_path,
|
|
302
|
+
codex_bundle_id: codexBundleId,
|
|
278
303
|
menu_items: MENU_ITEMS,
|
|
279
304
|
actions,
|
|
280
305
|
launch,
|
|
@@ -282,10 +307,7 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
282
307
|
build_stamp: stamp,
|
|
283
308
|
tcc_automation_status: 'unknown',
|
|
284
309
|
secret_env_cleanup: secretEnvCleanup,
|
|
285
|
-
next_actions:
|
|
286
|
-
...nextActions,
|
|
287
|
-
'If Terminal automation was denied: System Settings > Privacy & Security > Automation > SKS Menu Bar > Terminal.'
|
|
288
|
-
],
|
|
310
|
+
next_actions: nextActions,
|
|
289
311
|
blockers: ok ? [] : [launch.error || 'sks_menubar_launch_failed'],
|
|
290
312
|
warnings
|
|
291
313
|
};
|
|
@@ -303,7 +325,9 @@ export async function installSksMenuBar(opts = {}) {
|
|
|
303
325
|
launch_agent_path: paths.launch_agent_path,
|
|
304
326
|
action_script_path: paths.action_script_path,
|
|
305
327
|
build_stamp_path: paths.build_stamp_path,
|
|
328
|
+
config_path: paths.config_path,
|
|
306
329
|
report_path: paths.report_path,
|
|
330
|
+
codex_bundle_id: typeof codexBundleId === 'string' ? codexBundleId : null,
|
|
307
331
|
menu_items: MENU_ITEMS,
|
|
308
332
|
actions,
|
|
309
333
|
launch: { requested: false, method: 'none', ok: false, error: detail || reason },
|
|
@@ -355,6 +379,84 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
|
|
|
355
379
|
next_actions: ['Rotate CODEX_LB_API_KEY and OPENROUTER_API_KEY if they were previously exposed in launchd.']
|
|
356
380
|
};
|
|
357
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
|
+
}
|
|
358
460
|
export async function inspectSksMenuBarStatus(opts = {}) {
|
|
359
461
|
const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
|
|
360
462
|
const installed = await exists(paths.executable_path);
|
|
@@ -364,6 +466,16 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
364
466
|
const sksEntry = shellAssignment(actionText, 'SKS_ENTRY');
|
|
365
467
|
const nodeExists = nodeBin ? await isExecutable(nodeBin) : false;
|
|
366
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
|
+
};
|
|
367
479
|
const buildStamp = await readJson(paths.build_stamp_path, null);
|
|
368
480
|
const launchd = await inspectLaunchdService(opts.env);
|
|
369
481
|
const signature = await inspectSignature(paths.app_path, opts.env);
|
|
@@ -371,10 +483,14 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
371
483
|
const warnings = [];
|
|
372
484
|
if (!installed)
|
|
373
485
|
blockers.push('menubar_app_missing');
|
|
374
|
-
if (installed && !
|
|
375
|
-
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');
|
|
376
490
|
if (installed && signature.checked && !signature.ok)
|
|
377
491
|
warnings.push('codesign_identifier_unexpected');
|
|
492
|
+
if (!codexSync.ok)
|
|
493
|
+
warnings.push('codex_sync_disabled');
|
|
378
494
|
if (buildStamp?.package_version && buildStamp.package_version !== PACKAGE_VERSION)
|
|
379
495
|
warnings.push('build_stamp_package_version_mismatch');
|
|
380
496
|
return {
|
|
@@ -390,8 +506,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
|
|
|
390
506
|
node_exists: nodeExists,
|
|
391
507
|
sks_entry: sksEntry,
|
|
392
508
|
sks_entry_exists: sksEntryExists,
|
|
393
|
-
|
|
509
|
+
smoke_code: actionSmoke.code,
|
|
510
|
+
smoke_output: actionSmoke.output,
|
|
511
|
+
version_detected: actionSmoke.versionDetected,
|
|
512
|
+
ok: actionSmoke.ok
|
|
394
513
|
},
|
|
514
|
+
codex_sync: codexSync,
|
|
395
515
|
build_stamp: buildStamp,
|
|
396
516
|
package_version: PACKAGE_VERSION,
|
|
397
517
|
signature,
|
|
@@ -540,16 +660,7 @@ async function resolveSksEntryForInstall(input) {
|
|
|
540
660
|
let existsResolved = await exists(resolved);
|
|
541
661
|
let projectLocal = isSubpath(resolved, input.root);
|
|
542
662
|
if (projectLocal) {
|
|
543
|
-
|
|
544
|
-
if (globalCandidate && globalCandidate !== resolved) {
|
|
545
|
-
input.warnings.push('sks_entry_project_local_ignored_global_package_used');
|
|
546
|
-
resolved = globalCandidate;
|
|
547
|
-
existsResolved = true;
|
|
548
|
-
projectLocal = isSubpath(resolved, input.root);
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
input.warnings.push('sks_entry_resolved_under_project_root');
|
|
552
|
-
}
|
|
663
|
+
input.warnings.push('sks_entry_project_local');
|
|
553
664
|
}
|
|
554
665
|
const usedPreviousScript = !existsResolved && await exists(input.actionScriptPath);
|
|
555
666
|
if (!existsResolved && usedPreviousScript)
|
|
@@ -563,81 +674,195 @@ async function resolveSksEntryForInstall(input) {
|
|
|
563
674
|
used_previous_script: usedPreviousScript
|
|
564
675
|
};
|
|
565
676
|
}
|
|
566
|
-
|
|
567
|
-
const candidates = new Set();
|
|
568
|
-
const nodeRoot = path.resolve(path.dirname(process.execPath), '..');
|
|
569
|
-
candidates.add(path.join(nodeRoot, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
|
|
570
|
-
candidates.add(path.join(home, '.nvm', 'versions', 'node'));
|
|
571
|
-
const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
|
|
572
|
-
try {
|
|
573
|
-
const versions = await fs.readdir(nvmRoot);
|
|
574
|
-
for (const version of versions.sort().reverse()) {
|
|
575
|
-
candidates.add(path.join(nvmRoot, version, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
catch { }
|
|
579
|
-
if (env.SKS_GLOBAL_ROOT)
|
|
580
|
-
candidates.add(path.join(path.resolve(env.SKS_GLOBAL_ROOT), 'dist', 'bin', 'sks.js'));
|
|
581
|
-
for (const candidate of candidates) {
|
|
582
|
-
if (candidate === nvmRoot)
|
|
583
|
-
continue;
|
|
584
|
-
if (path.resolve(candidate) === path.resolve(packaged))
|
|
585
|
-
continue;
|
|
586
|
-
if (await exists(candidate))
|
|
587
|
-
return path.resolve(candidate);
|
|
588
|
-
}
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
function actionScriptSource(input) {
|
|
677
|
+
export function actionScriptSource(input) {
|
|
592
678
|
return `#!/bin/zsh
|
|
593
679
|
set -e
|
|
594
680
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
595
681
|
NODE_BIN=${shellQuote(input.nodeBin)}
|
|
596
682
|
SKS_ENTRY=${shellQuote(input.sksEntry)}
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
599
730
|
fi
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
603
735
|
fi
|
|
604
736
|
done
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
fi
|
|
737
|
+
run_node_entry "$SKS_ENTRY" "$@" || true
|
|
738
|
+
notify_sks_missing
|
|
608
739
|
echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
|
|
609
740
|
exit 127
|
|
610
741
|
`;
|
|
611
742
|
}
|
|
612
|
-
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
|
+
`;
|
|
613
793
|
return `import Cocoa
|
|
614
794
|
import Foundation
|
|
615
795
|
|
|
616
796
|
let actionScript = ${swiftString(input.actionScriptPath)}
|
|
617
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'}
|
|
618
801
|
let packageVersion = ${swiftString(input.packageVersion)}
|
|
619
802
|
|
|
620
803
|
func shellQuote(_ value: String) -> String {
|
|
621
804
|
return "'" + value.replacingOccurrences(of: "'", with: "'\\\\''") + "'"
|
|
622
805
|
}
|
|
623
806
|
|
|
807
|
+
func clipped(_ value: String, limit: Int = 700) -> String {
|
|
808
|
+
return String(value.prefix(limit))
|
|
809
|
+
}
|
|
810
|
+
|
|
624
811
|
func showAlert(_ message: String, informative: String = "") {
|
|
625
812
|
DispatchQueue.main.async {
|
|
813
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
626
814
|
let alert = NSAlert()
|
|
627
815
|
alert.messageText = message
|
|
628
|
-
alert.informativeText = informative
|
|
816
|
+
alert.informativeText = clipped(informative)
|
|
629
817
|
alert.alertStyle = .warning
|
|
630
818
|
alert.runModal()
|
|
631
819
|
}
|
|
632
820
|
}
|
|
633
821
|
|
|
634
|
-
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) {
|
|
635
854
|
let process = Process()
|
|
636
855
|
let output = Pipe()
|
|
637
856
|
process.executableURL = URL(fileURLWithPath: executable)
|
|
638
857
|
process.arguments = args
|
|
639
858
|
process.standardOutput = output
|
|
640
859
|
process.standardError = output
|
|
860
|
+
var inputPipe: Pipe?
|
|
861
|
+
if stdinText != nil {
|
|
862
|
+
let pipe = Pipe()
|
|
863
|
+
process.standardInput = pipe
|
|
864
|
+
inputPipe = pipe
|
|
865
|
+
}
|
|
641
866
|
process.terminationHandler = { proc in
|
|
642
867
|
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
643
868
|
let text = String(data: data, encoding: .utf8) ?? ""
|
|
@@ -645,14 +870,17 @@ func runProcess(_ executable: String, _ args: [String] = [], completion: ((Int32
|
|
|
645
870
|
}
|
|
646
871
|
do {
|
|
647
872
|
try process.run()
|
|
873
|
+
if let stdinText = stdinText, let inputPipe = inputPipe {
|
|
874
|
+
inputPipe.fileHandleForWriting.write(Data(stdinText.utf8))
|
|
875
|
+
inputPipe.fileHandleForWriting.closeFile()
|
|
876
|
+
}
|
|
648
877
|
} catch {
|
|
649
878
|
completion?(-1, String(describing: error))
|
|
650
879
|
}
|
|
651
880
|
}
|
|
652
881
|
|
|
653
882
|
func showNotification(_ title: String, _ body: String) {
|
|
654
|
-
let
|
|
655
|
-
let script = "display notification " + shellQuote(clipped) + " with title " + shellQuote(title)
|
|
883
|
+
let script = "display notification " + shellQuote(clipped(body)) + " with title " + shellQuote(title)
|
|
656
884
|
runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
|
|
657
885
|
if code != 0 {
|
|
658
886
|
showAlert(title, informative: output)
|
|
@@ -660,35 +888,40 @@ func showNotification(_ title: String, _ body: String) {
|
|
|
660
888
|
}
|
|
661
889
|
}
|
|
662
890
|
|
|
663
|
-
func
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
.replacingOccurrences(of:
|
|
667
|
-
|
|
668
|
-
let
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
end tell
|
|
681
|
-
"""
|
|
682
|
-
runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
|
|
683
|
-
if code != 0 {
|
|
684
|
-
let denied = output.contains("-1743") || output.localizedCaseInsensitiveContains("not authorized")
|
|
685
|
-
if denied {
|
|
686
|
-
showAlert("SKS menu cannot control Terminal", informative: "Open System Settings > Privacy & Security > Automation, then allow SKS Menu Bar to control Terminal.")
|
|
687
|
-
} else {
|
|
688
|
-
showAlert("SKS menu action failed", informative: output)
|
|
689
|
-
}
|
|
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]")
|
|
690
908
|
}
|
|
691
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
|
+
}
|
|
692
925
|
}
|
|
693
926
|
|
|
694
927
|
struct MenuState {
|
|
@@ -696,27 +929,37 @@ struct MenuState {
|
|
|
696
929
|
let line: String
|
|
697
930
|
}
|
|
698
931
|
|
|
699
|
-
|
|
932
|
+
struct MenuBarConfig {
|
|
933
|
+
let quitWithCodex: Bool
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
700
937
|
var statusItem: NSStatusItem!
|
|
701
938
|
var statusLineItem: NSMenuItem!
|
|
939
|
+
var codexLbItem: NSMenuItem!
|
|
940
|
+
var oauthItem: NSMenuItem!
|
|
702
941
|
var timer: Timer?
|
|
942
|
+
var busy = false
|
|
943
|
+
var lastFailure = false
|
|
944
|
+
var quitWithCodex = false
|
|
703
945
|
|
|
704
946
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
705
947
|
NSApp.setActivationPolicy(.accessory)
|
|
948
|
+
quitWithCodex = readConfig().quitWithCodex
|
|
706
949
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
707
950
|
statusItem.autosaveName = "com.sneakoscope.sks-menubar"
|
|
708
|
-
statusItem.isVisible = true
|
|
709
951
|
if let button = statusItem.button {
|
|
710
952
|
configureStatusButton(button, title: "SKS")
|
|
711
953
|
}
|
|
712
954
|
|
|
713
955
|
let menu = NSMenu()
|
|
956
|
+
menu.delegate = self
|
|
714
957
|
statusLineItem = NSMenuItem(title: "SKS v\\(packageVersion) - starting", action: nil, keyEquivalent: "")
|
|
715
958
|
statusLineItem.isEnabled = false
|
|
716
959
|
menu.addItem(statusLineItem)
|
|
717
960
|
menu.addItem(NSMenuItem.separator())
|
|
718
|
-
add(menu, "Use codex-lb", #selector(useCodexLb))
|
|
719
|
-
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))
|
|
720
963
|
add(menu, "Set codex-lb Domain and Key", #selector(setCodexLbDomainAndKey))
|
|
721
964
|
menu.addItem(NSMenuItem.separator())
|
|
722
965
|
add(menu, "Set OpenRouter Key and GLM Profiles", #selector(setOpenRouterKey))
|
|
@@ -724,15 +967,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
724
967
|
add(menu, "SKS Version Check", #selector(sksVersionCheck))
|
|
725
968
|
add(menu, "Update SKS Now", #selector(updateSksNow))
|
|
726
969
|
menu.addItem(NSMenuItem.separator())
|
|
970
|
+
add(menu, "Open Dashboard", #selector(openDashboard))
|
|
727
971
|
add(menu, "Open Codex Settings", #selector(openCodexSettings))
|
|
728
972
|
add(menu, "Restart Codex", #selector(restartCodex))
|
|
729
973
|
menu.addItem(NSMenuItem.separator())
|
|
974
|
+
add(menu, "View Last Log", #selector(viewLastLog))
|
|
730
975
|
add(menu, "Quit SKS Menu", #selector(quit))
|
|
731
976
|
statusItem.menu = menu
|
|
977
|
+
configureCodexLifecycleSync()
|
|
732
978
|
updateState()
|
|
733
979
|
timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in self?.updateState() }
|
|
734
980
|
}
|
|
735
981
|
|
|
982
|
+
func menuWillOpen(_ menu: NSMenu) {
|
|
983
|
+
updateState()
|
|
984
|
+
updateAuthModeChecks()
|
|
985
|
+
}
|
|
986
|
+
|
|
736
987
|
func configureStatusButton(_ button: NSStatusBarButton, title: String) {
|
|
737
988
|
button.image = nil
|
|
738
989
|
button.title = title
|
|
@@ -748,10 +999,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
748
999
|
button.setAccessibilityHelp("Open SKS menu")
|
|
749
1000
|
}
|
|
750
1001
|
|
|
751
|
-
func add(_ menu: NSMenu, _ title: String, _ selector: Selector) {
|
|
1002
|
+
func add(_ menu: NSMenu, _ title: String, _ selector: Selector) -> NSMenuItem {
|
|
752
1003
|
let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
|
|
753
1004
|
item.target = self
|
|
754
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)
|
|
755
1017
|
}
|
|
756
1018
|
|
|
757
1019
|
func updateState() {
|
|
@@ -763,20 +1025,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
763
1025
|
}
|
|
764
1026
|
|
|
765
1027
|
func readMenuState() -> MenuState {
|
|
766
|
-
|
|
767
|
-
|
|
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() {
|
|
768
1035
|
return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - action script broken (run sks doctor --fix)")
|
|
769
1036
|
}
|
|
1037
|
+
if lastFailure {
|
|
1038
|
+
return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - last action failed")
|
|
1039
|
+
}
|
|
770
1040
|
if updateAvailable() {
|
|
771
1041
|
return MenuState(title: "SKS ↑", line: "SKS v\\(packageVersion) - update available")
|
|
772
1042
|
}
|
|
773
1043
|
return MenuState(title: "SKS", line: "SKS v\\(packageVersion) - OK")
|
|
774
1044
|
}
|
|
775
1045
|
|
|
776
|
-
func
|
|
777
|
-
|
|
778
|
-
guard let entry = shellAssignment(text, key: "SKS_ENTRY") else { return false }
|
|
779
|
-
return FileManager.default.fileExists(atPath: entry)
|
|
1046
|
+
func actionScriptUsable() -> Bool {
|
|
1047
|
+
return FileManager.default.isExecutableFile(atPath: actionScript)
|
|
780
1048
|
}
|
|
781
1049
|
|
|
782
1050
|
func updateAvailable() -> Bool {
|
|
@@ -788,48 +1056,83 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
788
1056
|
return latest != packageVersion && !latest.isEmpty
|
|
789
1057
|
}
|
|
790
1058
|
|
|
791
|
-
func
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
}
|
|
799
1076
|
}
|
|
800
|
-
|
|
1077
|
+
completion?(code, redacted)
|
|
801
1078
|
}
|
|
802
1079
|
}
|
|
803
|
-
return nil
|
|
804
1080
|
}
|
|
805
1081
|
|
|
806
|
-
func
|
|
807
|
-
|
|
808
|
-
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)
|
|
809
1084
|
}
|
|
810
1085
|
|
|
811
|
-
func
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
|
815
1110
|
}
|
|
816
1111
|
}
|
|
817
1112
|
|
|
818
1113
|
@objc func useCodexLb() {
|
|
819
|
-
|
|
1114
|
+
runSksBackground(["codex-lb", "use-codex-lb", "--json"], title: "Use codex-lb") { [weak self] code, _ in
|
|
1115
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1116
|
+
}
|
|
820
1117
|
}
|
|
821
1118
|
|
|
822
1119
|
@objc func useChatGptOAuth() {
|
|
823
|
-
|
|
1120
|
+
runSksBackground(["codex-lb", "use-oauth", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
|
|
1121
|
+
if code == 0 { self?.updateAuthModeChecks() }
|
|
1122
|
+
}
|
|
824
1123
|
}
|
|
825
1124
|
|
|
826
1125
|
@objc func setCodexLbDomainAndKey() {
|
|
827
|
-
|
|
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
|
+
}
|
|
828
1131
|
}
|
|
829
1132
|
|
|
830
1133
|
@objc func setOpenRouterKey() {
|
|
831
|
-
let
|
|
832
|
-
|
|
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")
|
|
833
1136
|
}
|
|
834
1137
|
|
|
835
1138
|
@objc func fastCheck() {
|
|
@@ -841,7 +1144,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
841
1144
|
}
|
|
842
1145
|
|
|
843
1146
|
@objc func updateSksNow() {
|
|
844
|
-
|
|
1147
|
+
runSksBackground(["update"], title: "Update SKS Now")
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
@objc func openDashboard() {
|
|
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
|
+
}
|
|
845
1162
|
}
|
|
846
1163
|
|
|
847
1164
|
@objc func openCodexSettings() {
|
|
@@ -849,7 +1166,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
849
1166
|
}
|
|
850
1167
|
|
|
851
1168
|
@objc func restartCodex() {
|
|
852
|
-
|
|
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
|
+
}
|
|
853
1192
|
}
|
|
854
1193
|
|
|
855
1194
|
@objc func quit() {
|
|
@@ -916,10 +1255,11 @@ async function buildMenuBarAppAtomically(input) {
|
|
|
916
1255
|
input.actions.push(`wrote ${input.paths.source_path}`);
|
|
917
1256
|
await writeTextAtomic(path.join(input.paths.staging_app_path, 'Contents', 'Info.plist'), input.infoPlist);
|
|
918
1257
|
const stagingExecutable = path.join(input.paths.staging_app_path, 'Contents', 'MacOS', 'SKSMenuBar');
|
|
919
|
-
const
|
|
1258
|
+
const compileWork = runProcess(input.swiftc, ['-framework', 'Cocoa', input.paths.source_path, '-o', stagingExecutable], {
|
|
920
1259
|
timeoutMs: 60_000,
|
|
921
1260
|
maxOutputBytes: 64 * 1024
|
|
922
1261
|
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
1262
|
+
const compile = input.quiet === true ? await compileWork : await withHeartbeat('swiftc SKS menu bar', compileWork, { warnAfterMs: 30_000 });
|
|
923
1263
|
if (compile.code !== 0) {
|
|
924
1264
|
throw new MenuBarBuildError('swift_compile_failed', String(compile.stderr || compile.stdout || '').trim());
|
|
925
1265
|
}
|
|
@@ -956,7 +1296,8 @@ async function launchWithLaunchctl(input) {
|
|
|
956
1296
|
stdoutFile: path.join(input.paths.install_dir, 'launchctl.out.log'),
|
|
957
1297
|
stderrFile: path.join(input.paths.install_dir, 'launchctl.err.log')
|
|
958
1298
|
};
|
|
959
|
-
const
|
|
1299
|
+
const alreadyWork = waitForLaunchctlRunning(input.launchctl, service);
|
|
1300
|
+
const already = input.quiet === true ? await alreadyWork : await withHeartbeat('launchctl SKS menu bar wait', alreadyWork, { warnAfterMs: 10_000 });
|
|
960
1301
|
if (input.skipIfRunning && already.running) {
|
|
961
1302
|
return { requested: true, method: 'launchctl', ok: true, print_code: already.code, error: null };
|
|
962
1303
|
}
|
|
@@ -975,7 +1316,8 @@ async function launchWithLaunchctl(input) {
|
|
|
975
1316
|
...stdio
|
|
976
1317
|
}).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
|
|
977
1318
|
if (kickstart.code !== 0) {
|
|
978
|
-
const
|
|
1319
|
+
const printedWork = waitForLaunchctlRunning(input.launchctl, service);
|
|
1320
|
+
const printed = input.quiet === true ? await printedWork : await withHeartbeat('launchctl SKS menu bar wait', printedWork, { warnAfterMs: 10_000 });
|
|
979
1321
|
if (printed.running) {
|
|
980
1322
|
return {
|
|
981
1323
|
requested: true,
|
|
@@ -1064,13 +1406,14 @@ async function inspectLaunchdService(env = process.env) {
|
|
|
1064
1406
|
const text = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
1065
1407
|
const state = text.match(/\bstate = ([^\n]+)/)?.[1]?.trim() || null;
|
|
1066
1408
|
const pidText = text.match(/\bpid = (\d+)/)?.[1] || null;
|
|
1409
|
+
const running = result.code === 0 && (state === 'running' || Boolean(pidText));
|
|
1067
1410
|
return {
|
|
1068
1411
|
checked: true,
|
|
1069
|
-
ok:
|
|
1412
|
+
ok: running,
|
|
1070
1413
|
service,
|
|
1071
1414
|
state,
|
|
1072
1415
|
pid: pidText ? Number(pidText) : null,
|
|
1073
|
-
error:
|
|
1416
|
+
error: running ? null : String(result.stderr || result.stdout || state || 'launchd_not_running').trim()
|
|
1074
1417
|
};
|
|
1075
1418
|
}
|
|
1076
1419
|
async function inspectSignature(appPath, env = process.env) {
|
|
@@ -1162,6 +1505,8 @@ async function writeReport(reportPath, result) {
|
|
|
1162
1505
|
}
|
|
1163
1506
|
catch (err) {
|
|
1164
1507
|
result.report_write_failed = true;
|
|
1508
|
+
if (!result.warnings.includes('menubar_report_write_failed'))
|
|
1509
|
+
result.warnings.push('menubar_report_write_failed');
|
|
1165
1510
|
console.error(`warning: failed to write SKS menubar report ${reportPath}: ${err?.message || err}`);
|
|
1166
1511
|
}
|
|
1167
1512
|
}
|