sneakoscope 7.3.0 → 7.6.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 +129 -10
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/codex-app-command.js +1 -1
- package/dist/cli/command-manifest-lite.js +4 -1
- package/dist/cli/command-registry.js +2 -0
- package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
- package/dist/cli/install-helpers-codex-lb-config.js +114 -9
- package/dist/cli/install-helpers-install-support.js +6 -16
- package/dist/cli/install-helpers.js +788 -600
- package/dist/cli/router.js +30 -2
- package/dist/commands/codex-app.js +37 -1
- package/dist/commands/codex-lb.js +491 -300
- package/dist/config/skills-manifest.json +57 -71
- package/dist/core/align/align-route.js +127 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
- package/dist/core/codex-app/codex-app-restart.js +83 -0
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
- package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
- package/dist/core/codex-app/menubar/constants.js +5 -0
- package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
- package/dist/core/codex-app/menubar/installer.js +18 -0
- package/dist/core/codex-app/multi-provider-router.js +22 -10
- package/dist/core/codex-app/openrouter-activate.js +26 -1
- package/dist/core/codex-app.js +118 -26
- package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
- package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
- package/dist/core/codex-control/codex-task-runner.js +2 -2
- package/dist/core/codex-lb/capability-runner.js +204 -0
- package/dist/core/codex-lb/capability-types.js +1 -0
- package/dist/core/codex-lb/codex-lb-env.js +9 -13
- package/dist/core/codex-lb/codex-lb-setup.js +45 -8
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
- package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
- package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
- package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
- package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
- package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
- package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
- package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
- package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
- package/dist/core/codex-lb/desktop-controller.js +1410 -0
- package/dist/core/codex-lb/desktop-mode.js +32 -0
- package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
- package/dist/core/codex-lb/desktop-service.js +613 -0
- package/dist/core/codex-lb/legacy-migration.js +575 -0
- package/dist/core/codex-lb/migration-receipt.js +246 -0
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
- package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
- package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
- package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
- package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
- package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
- package/dist/core/codex-native/core-skill-manifest.js +22 -6
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
- package/dist/core/commands/align-command.js +220 -0
- package/dist/core/commands/image-ux-review-command.js +280 -68
- package/dist/core/commands/naruto-command.js +257 -8
- package/dist/core/commands/ppt-command.js +2 -2
- package/dist/core/commands/telegram-command.js +402 -34
- package/dist/core/commands/triwiki-command.js +11 -1
- package/dist/core/commands/triwiki-graph-command.js +160 -0
- package/dist/core/commands/wiki-command.js +100 -20
- package/dist/core/doctor/skill-legacy-surface.js +1 -3
- package/dist/core/errors/message.js +3 -0
- package/dist/core/evidence/evidence-freshness.js +14 -0
- package/dist/core/feature-fixtures.js +9 -1
- package/dist/core/feature-registry.js +5 -3
- package/dist/core/fsx.js +7 -3
- package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
- package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
- package/dist/core/hooks-runtime/hook-context.js +15 -2
- package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
- package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +33 -89
- package/dist/core/image-ux-review/callout-extraction.js +6 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
- package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
- package/dist/core/image-ux-review.js +230 -12
- package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
- package/dist/core/imagegen/imagegen-capability.js +10 -4
- package/dist/core/imagegen/require-imagegen.js +29 -5
- package/dist/core/init/skills.js +25 -13
- package/dist/core/init.js +1 -1
- package/dist/core/install/installed-package-smoke.js +157 -4
- package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
- package/dist/core/managed-path-safety.js +8 -10
- package/dist/core/mcp-config/backup.js +2 -4
- package/dist/core/mcp-config/config-reader.js +2 -4
- package/dist/core/mcp-config/scope.js +3 -5
- package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
- package/dist/core/naruto/context-graph-advisor.js +0 -0
- package/dist/core/perf/release-latency-slo.js +12 -6
- package/dist/core/pipeline-internals/runtime-core.js +78 -0
- package/dist/core/pipeline-internals/runtime-gates.js +5 -2
- package/dist/core/policy/latest-version-guidance.js +113 -0
- package/dist/core/ppt.js +8 -8
- package/dist/core/preflight/parallel-preflight-engine.js +1 -1
- package/dist/core/proof/auto-finalize.js +98 -6
- package/dist/core/proof/route-proof-gate.js +12 -1
- package/dist/core/proof/route-proof-policy.js +8 -8
- package/dist/core/release/gate-manifest.js +32 -1
- package/dist/core/release/npm-stage-contract.js +21 -0
- package/dist/core/release/package-size-budget.js +2 -2
- package/dist/core/release/release-gate-contract.js +5 -0
- package/dist/core/release/release-proof-io.js +1 -3
- package/dist/core/release/release-real-contract.js +5 -4
- package/dist/core/release/release-stamp-proof.js +1 -3
- package/dist/core/release/stage-publish.js +27 -0
- package/dist/core/remote/audit-idempotency.js +50 -18
- package/dist/core/remote/codex-public-activity.js +283 -0
- package/dist/core/remote/index.js +1 -0
- package/dist/core/remote/ssh-worker-client.js +4 -1
- package/dist/core/remote/worker.js +206 -27
- package/dist/core/research/research-adversarial-review.js +1 -3
- package/dist/core/research/research-claim-synthesizer.js +1 -3
- package/dist/core/research/research-falsification-runner.js +1 -3
- package/dist/core/research/research-source-evidence.js +1 -3
- package/dist/core/responses-stream.js +65 -19
- package/dist/core/routes/constants.js +1 -1
- package/dist/core/routes/design-policy.js +7 -2
- package/dist/core/routes/dollar-manifest-lite.js +2 -4
- package/dist/core/routes/evidence.js +1 -1
- package/dist/core/routes.js +30 -10
- package/dist/core/safety/command-contract/contracts.js +2 -0
- package/dist/core/safety/command-contract/types.js +1 -1
- package/dist/core/search/context-graph-seeds.js +205 -0
- package/dist/core/search/context.js +0 -0
- package/dist/core/search/provider.js +2 -2
- package/dist/core/secret-redaction.js +6 -0
- package/dist/core/subagents/naruto-help-contract.js +2 -1
- package/dist/core/subagents/naruto-host-credentials.js +161 -0
- package/dist/core/subagents/official-subagent-preparation.js +4 -1
- package/dist/core/subagents/official-subagent-prompt.js +44 -2
- package/dist/core/subagents/official-subagent-runner.js +26 -8
- package/dist/core/subagents/triwiki-attention.js +135 -123
- package/dist/core/telegram/bot-api.js +85 -10
- package/dist/core/telegram/center-snapshot.js +718 -0
- package/dist/core/telegram/config.js +158 -31
- package/dist/core/telegram/hub.js +288 -33
- package/dist/core/telegram/index.js +2 -0
- package/dist/core/telegram/ingress-spool.js +419 -0
- package/dist/core/telegram/ledgers.js +188 -29
- package/dist/core/telegram/messages.js +26 -6
- package/dist/core/telegram/owner-lock.js +10 -7
- package/dist/core/telegram/runtime-projection.js +38 -3
- package/dist/core/telegram/runtime.js +298 -63
- package/dist/core/telegram/service.js +137 -8
- package/dist/core/telegram/setup.js +291 -73
- package/dist/core/text/strings.js +3 -0
- package/dist/core/triwiki/code-pack.js +12 -120
- package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
- package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
- package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
- package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
- package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
- package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
- package/dist/core/triwiki/context-graph/contracts.js +236 -0
- package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
- package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
- package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
- package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
- package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
- package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
- package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
- package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
- package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
- package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
- package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
- package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
- package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
- package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
- package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
- package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
- package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
- package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
- package/dist/core/triwiki/context-graph/graph-index.js +143 -0
- package/dist/core/triwiki/context-graph/ids.js +0 -0
- package/dist/core/triwiki/context-graph/lint/index.js +0 -0
- package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
- package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
- package/dist/core/triwiki/context-graph/paths.js +116 -0
- package/dist/core/triwiki/context-graph/profiles.js +78 -0
- package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
- package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
- package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
- package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
- package/dist/core/triwiki/context-graph/projections/index.js +7 -0
- package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
- package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
- package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
- package/dist/core/triwiki/context-graph/query/explain.js +0 -0
- package/dist/core/triwiki/context-graph/query/index.js +178 -0
- package/dist/core/triwiki/context-graph/query/load.js +116 -0
- package/dist/core/triwiki/context-graph/query/pack.js +117 -0
- package/dist/core/triwiki/context-graph/query/rank.js +142 -0
- package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
- package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
- package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
- package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
- package/dist/core/triwiki/context-graph/query-types.js +29 -0
- package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
- package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
- package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
- package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
- package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
- package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
- package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
- package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
- package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
- package/dist/core/verification/context-graph-affected.js +271 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
- package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
- package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
- package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
- package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
- package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
- package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
- package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
- package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
- package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
- package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
- package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
- package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
- package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
- package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
- package/dist/scripts/context-graph-check.js +191 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
- package/dist/scripts/gate-policy-audit-check.js +1 -1
- package/dist/scripts/helper-dedup-check.js +0 -2
- package/dist/scripts/installed-package-smoke-check.js +4 -1
- package/dist/scripts/latest-version-guidance-check.js +74 -0
- package/dist/scripts/release-metadata-check.js +21 -4
- package/dist/scripts/release-real-check.js +7 -0
- package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
- package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
- package/package.json +8 -3
- package/release-gates.v2.json +176 -1
- package/runtime-required-scripts.json +30 -0
- package/schemas/triwiki/context-graph.schema.json +119 -0
- package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
- package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
- package/dist/core/triwiki/code-index-scanner.js +0 -309
|
@@ -16,6 +16,9 @@ enum NativeView {
|
|
|
16
16
|
static func title(_ value: String) -> NSTextField {
|
|
17
17
|
let field = NSTextField(labelWithString: value)
|
|
18
18
|
field.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
|
|
19
|
+
field.alignment = .left
|
|
20
|
+
field.setAccessibilityLabel(value)
|
|
21
|
+
field.setAccessibilityIdentifier("sks-center-heading-\(identifier(value))")
|
|
19
22
|
return field
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -23,12 +26,18 @@ enum NativeView {
|
|
|
23
26
|
let field = NSTextField(wrappingLabelWithString: value)
|
|
24
27
|
field.font = NSFont.systemFont(ofSize: 12)
|
|
25
28
|
field.textColor = .secondaryLabelColor
|
|
29
|
+
field.alignment = .left
|
|
30
|
+
// Long status and help copy must wrap inside the current window instead
|
|
31
|
+
// of contributing an intrinsic minimum width while users change pages.
|
|
32
|
+
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
26
33
|
return field
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
static func sectionTitle(_ value: String) -> NSTextField {
|
|
30
37
|
let field = NSTextField(labelWithString: value)
|
|
31
38
|
field.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
|
|
39
|
+
field.alignment = .left
|
|
40
|
+
field.setAccessibilityLabel(value)
|
|
32
41
|
return field
|
|
33
42
|
}
|
|
34
43
|
|
|
@@ -36,6 +45,7 @@ enum NativeView {
|
|
|
36
45
|
let button = NSButton(title: title, target: target, action: action)
|
|
37
46
|
button.bezelStyle = .rounded
|
|
38
47
|
button.setAccessibilityLabel(title)
|
|
48
|
+
button.setAccessibilityIdentifier("sks-center-button-\(identifier(title))")
|
|
39
49
|
return button
|
|
40
50
|
}
|
|
41
51
|
|
|
@@ -51,27 +61,34 @@ enum NativeView {
|
|
|
51
61
|
static func page(_ views: [NSView]) -> NSStackView {
|
|
52
62
|
let stack = stack(views)
|
|
53
63
|
stack.alignment = .width
|
|
64
|
+
for view in stack.arrangedSubviews {
|
|
65
|
+
view.widthAnchor.constraint(equalTo: stack.widthAnchor, constant: -48).isActive = true
|
|
66
|
+
}
|
|
54
67
|
return stack
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
static func row(_ views: [NSView], spacing: CGFloat = 8) -> NSStackView {
|
|
58
71
|
let row = NSStackView(views: views)
|
|
59
72
|
row.orientation = .horizontal; row.alignment = .centerY; row.spacing = spacing
|
|
73
|
+
row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
60
74
|
return row
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
static func card(title: String, subtitle: String, views: [NSView]) -> NSBox {
|
|
77
|
+
static func card(title: String, subtitle: String, views: [NSView], fullWidthLeadingContent: Bool = false) -> NSBox {
|
|
64
78
|
let box = NSBox()
|
|
65
79
|
box.boxType = .custom; box.titlePosition = .noTitle
|
|
66
80
|
box.cornerRadius = 10; box.borderWidth = 1
|
|
67
81
|
box.borderColor = .separatorColor; box.fillColor = .controlBackgroundColor
|
|
68
|
-
|
|
69
|
-
let help = detail(subtitle)
|
|
82
|
+
box.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
83
|
+
let heading = sectionTitle(title); let help = detail(subtitle)
|
|
70
84
|
let content = NSStackView(views: [heading, help] + views)
|
|
71
|
-
content.orientation = .vertical
|
|
72
|
-
content.
|
|
73
|
-
content.
|
|
74
|
-
|
|
85
|
+
content.orientation = .vertical; content.alignment = .width
|
|
86
|
+
content.spacing = 10; content.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
|
87
|
+
content.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
88
|
+
for view in content.arrangedSubviews {
|
|
89
|
+
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
90
|
+
view.widthAnchor.constraint(equalTo: content.widthAnchor, constant: -32).isActive = true
|
|
91
|
+
}
|
|
75
92
|
content.translatesAutoresizingMaskIntoConstraints = false
|
|
76
93
|
box.contentView?.addSubview(content)
|
|
77
94
|
if let host = box.contentView {
|
|
@@ -83,9 +100,39 @@ enum NativeView {
|
|
|
83
100
|
])
|
|
84
101
|
}
|
|
85
102
|
box.setAccessibilityLabel(title)
|
|
103
|
+
box.setAccessibilityHelp(subtitle)
|
|
104
|
+
box.setAccessibilityRole(.group)
|
|
105
|
+
box.setAccessibilityIdentifier("sks-center-card-\(identifier(title))")
|
|
86
106
|
return box
|
|
87
107
|
}
|
|
88
108
|
|
|
109
|
+
static func badge(_ text: String, color: NSColor) -> NSView {
|
|
110
|
+
let dot = NSTextField(labelWithString: "●")
|
|
111
|
+
dot.font = NSFont.systemFont(ofSize: 10)
|
|
112
|
+
dot.textColor = color
|
|
113
|
+
dot.setAccessibilityHidden(true)
|
|
114
|
+
let label = NSTextField(labelWithString: text)
|
|
115
|
+
label.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
|
116
|
+
label.alignment = .left
|
|
117
|
+
label.lineBreakMode = .byTruncatingTail
|
|
118
|
+
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
119
|
+
label.setAccessibilityLabel(text)
|
|
120
|
+
let row = NSStackView(views: [dot, label])
|
|
121
|
+
row.orientation = .horizontal
|
|
122
|
+
row.alignment = .centerY
|
|
123
|
+
row.spacing = 5
|
|
124
|
+
return row
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static func setBadge(_ view: NSView, text: String, color: NSColor) {
|
|
128
|
+
guard let row = view as? NSStackView, row.arrangedSubviews.count >= 2,
|
|
129
|
+
let dot = row.arrangedSubviews[0] as? NSTextField,
|
|
130
|
+
let label = row.arrangedSubviews[1] as? NSTextField else { return }
|
|
131
|
+
dot.textColor = color
|
|
132
|
+
label.stringValue = text
|
|
133
|
+
label.setAccessibilityLabel(text)
|
|
134
|
+
}
|
|
135
|
+
|
|
89
136
|
static func spinner(label: String) -> NSProgressIndicator {
|
|
90
137
|
let indicator = NSProgressIndicator()
|
|
91
138
|
indicator.style = .spinning
|
|
@@ -100,10 +147,13 @@ enum NativeView {
|
|
|
100
147
|
scroll.drawsBackground = false
|
|
101
148
|
scroll.hasVerticalScroller = true
|
|
102
149
|
scroll.hasHorizontalScroller = false
|
|
150
|
+
scroll.scrollerStyle = .overlay
|
|
103
151
|
scroll.borderType = .noBorder
|
|
104
152
|
scroll.autohidesScrollers = true
|
|
105
153
|
scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
154
|
+
scroll.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
106
155
|
document.translatesAutoresizingMaskIntoConstraints = false
|
|
156
|
+
document.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
107
157
|
scroll.documentView = document
|
|
108
158
|
if let content = scroll.contentView.documentView {
|
|
109
159
|
NSLayoutConstraint.activate([
|
|
@@ -121,166 +171,12 @@ enum NativeView {
|
|
|
121
171
|
if compact.count <= limit { return compact }
|
|
122
172
|
return String(compact.prefix(limit)) + "…"
|
|
123
173
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
enum OverviewSummary {
|
|
127
|
-
static func render(
|
|
128
|
-
update: [String: Any]?,
|
|
129
|
-
mcp: [String: Any]?,
|
|
130
|
-
telegram: [String: Any]?,
|
|
131
|
-
menuBarBuild: String,
|
|
132
|
-
codexRunning: Bool?,
|
|
133
|
-
operationSummary: String
|
|
134
|
-
) -> String {
|
|
135
|
-
let codexAppState = codexRunning.map { $0 ? "Running" : "Not running" } ?? "Not configured"
|
|
136
|
-
var lines: [String] = []
|
|
137
|
-
let update = validatedUpdate(update)
|
|
138
|
-
let mcp = validatedMCP(mcp)
|
|
139
|
-
let telegram = validatedTelegram(telegram)
|
|
140
|
-
|
|
141
|
-
if let update = update {
|
|
142
|
-
let sks = update["sks"] as? [String: Any]
|
|
143
|
-
let codex = update["codex_cli"] as? [String: Any]
|
|
144
|
-
let menu = update["menubar"] as? [String: Any]
|
|
145
|
-
lines.append("SKS install: \(versionSummary(sks))")
|
|
146
|
-
lines.append("Codex CLI: \(versionSummary(codex)) · Codex app: \(codexAppState)")
|
|
147
|
-
if let inducement = codexUpdateInducement(codex) {
|
|
148
|
-
lines.append(inducement)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
var menuParts = ["running build \(menuBarBuild)"]
|
|
152
|
-
if let expected = menu?["expected_version"] as? String { menuParts.append("expected \(expected)") }
|
|
153
|
-
if let installed = menu?["installed_version"] as? String, installed != menuBarBuild {
|
|
154
|
-
menuParts.append("snapshot installed \(installed)")
|
|
155
|
-
}
|
|
156
|
-
if let rebuildRequired = menu?["rebuild_required"] as? Bool {
|
|
157
|
-
menuParts.append(rebuildRequired ? "rebuild required" : "current")
|
|
158
|
-
} else {
|
|
159
|
-
menuParts.append("rebuild state unknown")
|
|
160
|
-
}
|
|
161
|
-
menuParts.append("signature \(verificationState(menu?["signature_ok"]))")
|
|
162
|
-
menuParts.append("resources \(verificationState(menu?["resources_ok"]))")
|
|
163
|
-
lines.append("Menu Bar: \(menuParts.joined(separator: " · "))")
|
|
164
|
-
|
|
165
|
-
let source = snapshotSource(update["source"] as? String)
|
|
166
|
-
var updateParts = [integer(update["update_count"]).map { "\($0) pending" } ?? "pending count unknown", "\(source) snapshot"]
|
|
167
|
-
if let error = diagnosticNotice(update["public_error"] as? String, update: update) {
|
|
168
|
-
updateParts.append("notice: \(error)")
|
|
169
|
-
} else if let warnings = update["warnings"] as? [String], !warnings.isEmpty {
|
|
170
|
-
updateParts.append("\(warnings.count) warning\(warnings.count == 1 ? "" : "s")")
|
|
171
|
-
}
|
|
172
|
-
lines.append("Updates: \(updateParts.joined(separator: " · "))")
|
|
173
|
-
} else {
|
|
174
|
-
lines.append("SKS install: unavailable")
|
|
175
|
-
lines.append("Codex CLI: unavailable · Codex app: \(codexAppState)")
|
|
176
|
-
lines.append("Menu Bar: running build \(menuBarBuild) · update status unavailable")
|
|
177
|
-
lines.append("Updates: unavailable")
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if let mcp = mcp,
|
|
181
|
-
let enabled = integer(mcp["enabled_count"]),
|
|
182
|
-
let failed = integer(mcp["failed_count"]) {
|
|
183
|
-
lines.append("MCP: \(enabled) enabled · \(failed) failed")
|
|
184
|
-
} else {
|
|
185
|
-
lines.append("MCP: unavailable")
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if let telegram = telegram {
|
|
189
|
-
let owner = telegram["owner"] as? [String: Any]
|
|
190
|
-
let configured = telegram["configured"] as? Bool
|
|
191
|
-
let hubState = owner != nil ? "Running" : configured == true ? "Stopped" : configured == false ? "Not configured" : "Unknown"
|
|
192
|
-
let machines = integer(telegram["machine_count"]).map { "\($0) registered Macs" } ?? "registered Macs unknown"
|
|
193
|
-
let targets = integer(telegram["target_count"]).map { "\($0) configured targets" } ?? "configured targets unknown"
|
|
194
|
-
let issues = ((telegram["config_issues"] as? [String]) ?? []).count
|
|
195
|
-
+ ((telegram["remote_config_issues"] as? [String]) ?? []).count
|
|
196
|
-
var fleet = "Remote fleet: \(machines) · \(targets)"
|
|
197
|
-
if issues > 0 { fleet += " · \(issues) issue\(issues == 1 ? "" : "s")" }
|
|
198
|
-
lines.append("Telegram Hub: \(hubState) · \(fleet)")
|
|
199
|
-
} else {
|
|
200
|
-
lines.append("Telegram Hub: unavailable · Remote fleet: unavailable")
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
lines.append("Last operation: \(operationSummary) · Logs and snapshots use mode 0600")
|
|
204
|
-
return lines.joined(separator: "\n")
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
private static func validatedUpdate(_ value: [String: Any]?) -> [String: Any]? {
|
|
208
|
-
guard value?["schema"] as? String == "sks.update-status.v3",
|
|
209
|
-
value?["source"] is String,
|
|
210
|
-
integer(value?["update_count"]) != nil,
|
|
211
|
-
let sks = value?["sks"] as? [String: Any],
|
|
212
|
-
let codex = value?["codex_cli"] as? [String: Any],
|
|
213
|
-
let menu = value?["menubar"] as? [String: Any],
|
|
214
|
-
sks["update_available"] is Bool,
|
|
215
|
-
codex["update_available"] is Bool,
|
|
216
|
-
menu["expected_version"] is String,
|
|
217
|
-
menu["rebuild_required"] is Bool else { return nil }
|
|
218
|
-
return value
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private static func validatedMCP(_ value: [String: Any]?) -> [String: Any]? {
|
|
222
|
-
guard value?["schema"] as? String == "sks.mcp-inventory.v2",
|
|
223
|
-
integer(value?["enabled_count"]) != nil,
|
|
224
|
-
integer(value?["failed_count"]) != nil else { return nil }
|
|
225
|
-
return value
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private static func validatedTelegram(_ value: [String: Any]?) -> [String: Any]? {
|
|
229
|
-
guard value?["schema"] as? String == "sks.telegram-status.v1",
|
|
230
|
-
value?["configured"] is Bool,
|
|
231
|
-
integer(value?["machine_count"]) != nil,
|
|
232
|
-
integer(value?["target_count"]) != nil,
|
|
233
|
-
value?["config_issues"] is [String],
|
|
234
|
-
value?["remote_config_issues"] is [String] else { return nil }
|
|
235
|
-
return value
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private static func versionSummary(_ value: [String: Any]?) -> String {
|
|
239
|
-
guard let current = nonEmpty(value?["current"] as? String) else { return "not detected" }
|
|
240
|
-
guard let latest = nonEmpty(value?["latest"] as? String) else { return current }
|
|
241
|
-
if value?["update_available"] as? Bool == true, latest != current { return "\(current) → \(latest) available" }
|
|
242
|
-
if latest == current { return "\(current) (current)" }
|
|
243
|
-
return "\(current) · registry last seen \(latest)"
|
|
244
|
-
}
|
|
245
174
|
|
|
246
|
-
private static func
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private static func verificationState(_ value: Any?) -> String {
|
|
254
|
-
guard let value = value as? Bool else { return "unknown" }
|
|
255
|
-
return value ? "verified" : "needs attention"
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private static func snapshotSource(_ value: String?) -> String {
|
|
259
|
-
guard let value = nonEmpty(value) else { return "unknown" }
|
|
260
|
-
return value.replacingOccurrences(of: "_", with: " ")
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
private static func diagnosticNotice(_ value: String?, update: [String: Any]) -> String? {
|
|
264
|
-
guard let value = nonEmpty(value) else { return nil }
|
|
265
|
-
let versions = [
|
|
266
|
-
(update["sks"] as? [String: Any])?["current"] as? String,
|
|
267
|
-
(update["sks"] as? [String: Any])?["latest"] as? String,
|
|
268
|
-
(update["codex_cli"] as? [String: Any])?["current"] as? String,
|
|
269
|
-
(update["codex_cli"] as? [String: Any])?["latest"] as? String
|
|
270
|
-
].compactMap { $0 }
|
|
271
|
-
if versions.contains(value) { return nil }
|
|
272
|
-
if value.range(of: #"^v?\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?$"#, options: .regularExpression) != nil { return nil }
|
|
273
|
-
return value
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private static func integer(_ value: Any?) -> Int? {
|
|
277
|
-
if let value = value as? Int { return value }
|
|
278
|
-
return (value as? NSNumber)?.intValue
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private static func nonEmpty(_ value: String?) -> String? {
|
|
282
|
-
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil }
|
|
283
|
-
return value
|
|
175
|
+
private static func identifier(_ value: String) -> String {
|
|
176
|
+
value.lowercased()
|
|
177
|
+
.components(separatedBy: CharacterSet.alphanumerics.inverted)
|
|
178
|
+
.filter { !$0.isEmpty }
|
|
179
|
+
.joined(separator: "-")
|
|
284
180
|
}
|
|
285
181
|
}
|
|
286
182
|
|
|
@@ -289,7 +185,10 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
289
185
|
private let operations: OperationCoordinator
|
|
290
186
|
private let status = NativeView.detail("Loading local SKS status…")
|
|
291
187
|
private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
|
|
188
|
+
private let healthBadge = NativeView.badge("Checking local status", color: .systemBlue)
|
|
189
|
+
private let statusSpinner = NativeView.spinner(label: "Checking SKS Center status")
|
|
292
190
|
private var generation = 0
|
|
191
|
+
private var completedGeneration = 0
|
|
293
192
|
private var doctorButton: NSButton!
|
|
294
193
|
private var refreshButton: NSButton!
|
|
295
194
|
private var updateCodexButton: NSButton!
|
|
@@ -308,28 +207,32 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
308
207
|
refreshButton = NativeView.button("Refresh", target: self, action: #selector(refreshStatus))
|
|
309
208
|
updateCodexButton = NativeView.button("Update Codex CLI", target: self, action: #selector(updateCodexCLI))
|
|
310
209
|
updateCodexButton.setAccessibilityHelp("Update the operator Codex CLI to the preferred latest channel.")
|
|
311
|
-
let buttons =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
let shortcuts = NSStackView(views: [
|
|
210
|
+
let buttons = NativeView.row([refreshButton, doctorButton, updateCodexButton])
|
|
211
|
+
let shortcuts = NativeView.row([
|
|
212
|
+
NativeView.button("Set Up Telegram…", target: self, action: #selector(openTelegram)),
|
|
315
213
|
NativeView.button("Providers…", target: self, action: #selector(openProviders)),
|
|
316
214
|
NativeView.button("Updates…", target: self, action: #selector(openUpdates)),
|
|
317
215
|
NativeView.button("Diagnostics…", target: self, action: #selector(openDiagnostics))
|
|
318
216
|
])
|
|
319
|
-
shortcuts.orientation = .horizontal
|
|
320
|
-
shortcuts.spacing = 8
|
|
321
217
|
shortcuts.setAccessibilityLabel("Open Control Center sections")
|
|
322
|
-
|
|
218
|
+
let healthCard = NativeView.card(
|
|
219
|
+
title: "System health",
|
|
220
|
+
subtitle: "A bounded local snapshot of versions, services, and the latest operation.",
|
|
221
|
+
views: [NativeView.row([healthBadge, statusSpinner]), status, buttons]
|
|
222
|
+
)
|
|
223
|
+
let nextStepsCard = NativeView.card(
|
|
224
|
+
title: "Notifications & next steps",
|
|
225
|
+
subtitle: "Results remain available here even when macOS notifications are disabled.",
|
|
226
|
+
views: [notificationInbox, shortcuts]
|
|
227
|
+
)
|
|
228
|
+
view = NativeView.page([
|
|
323
229
|
NativeView.title("Overview"),
|
|
324
230
|
NativeView.detail("Menu Bar build \(AppRuntime.packageVersion) · Local health for SKS, Codex CLI, MCP, Remote, and operations. Prefer the latest Codex CLI; SKS stays version-agnostic and capability-gates features."),
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
buttons,
|
|
328
|
-
shortcuts
|
|
231
|
+
healthCard,
|
|
232
|
+
nextStepsCard
|
|
329
233
|
])
|
|
330
|
-
loadStatus(forceUpdateRefresh: false)
|
|
331
234
|
}
|
|
332
|
-
|
|
235
|
+
@objc private func openTelegram() { openSection?("Remote & Telegram") }
|
|
333
236
|
@objc private func openProviders() { openSection?("Providers") }
|
|
334
237
|
@objc private func openUpdates() { openSection?("Updates") }
|
|
335
238
|
@objc private func openDiagnostics() { openSection?("Diagnostics") }
|
|
@@ -353,9 +256,10 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
353
256
|
status.stringValue = "Another update or MCP mutation is already running. Open Updates to review it."
|
|
354
257
|
return
|
|
355
258
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
259
|
+
generation += 1
|
|
260
|
+
setActionBusy(true)
|
|
261
|
+
NativeView.setBadge(healthBadge, text: "Updating Codex CLI", color: .systemBlue)
|
|
262
|
+
statusSpinner.startAnimation(nil)
|
|
359
263
|
status.stringValue = "Updating Codex CLI to the preferred latest…"
|
|
360
264
|
_ = operations.update(operation, state: .running, stage: "running", progress: nil, summary: status.stringValue)
|
|
361
265
|
processClient.run(["codex", "update", "--json"], timeout: nil) { [weak self] result in
|
|
@@ -371,9 +275,8 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
371
275
|
progress: 1,
|
|
372
276
|
summary: ok ? "Codex CLI update completed" : "Codex CLI update failed"
|
|
373
277
|
)
|
|
374
|
-
self.
|
|
375
|
-
self.
|
|
376
|
-
self.refreshButton?.isEnabled = true
|
|
278
|
+
self.setActionBusy(false)
|
|
279
|
+
self.statusSpinner.stopAnimation(nil)
|
|
377
280
|
self.status.stringValue = ok
|
|
378
281
|
? "Codex CLI update completed. Refreshing shared update status…"
|
|
379
282
|
: "Codex CLI update needs attention. Open Updates or Diagnostics for structured guidance."
|
|
@@ -385,6 +288,8 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
385
288
|
generation += 1
|
|
386
289
|
let requestGeneration = generation
|
|
387
290
|
refreshButton?.isEnabled = false
|
|
291
|
+
NativeView.setBadge(healthBadge, text: "Checking local status", color: .systemBlue)
|
|
292
|
+
statusSpinner.startAnimation(nil)
|
|
388
293
|
status.stringValue = "Checking versions, MCP servers, Telegram Hub, remote fleet, and operations…"
|
|
389
294
|
var update: [String: Any]?
|
|
390
295
|
var mcp: [String: Any]?
|
|
@@ -422,30 +327,71 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
422
327
|
}
|
|
423
328
|
group.notify(queue: .main) { [weak self] in
|
|
424
329
|
guard let self = self, self.generation == requestGeneration else { return }
|
|
330
|
+
self.completedGeneration = requestGeneration
|
|
425
331
|
self.refreshButton?.isEnabled = true
|
|
426
|
-
self.
|
|
332
|
+
self.statusSpinner.stopAnimation(nil)
|
|
333
|
+
self.renderStatus(update: update, mcp: mcp, telegram: telegram, partial: false)
|
|
427
334
|
}
|
|
428
335
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
|
|
429
|
-
guard let self = self,
|
|
336
|
+
guard let self = self,
|
|
337
|
+
self.generation == requestGeneration,
|
|
338
|
+
self.completedGeneration != requestGeneration else { return }
|
|
430
339
|
self.refreshButton?.isEnabled = true
|
|
431
|
-
self.
|
|
340
|
+
self.renderStatus(update: update, mcp: mcp, telegram: telegram, partial: true)
|
|
432
341
|
}
|
|
433
342
|
}
|
|
434
343
|
|
|
435
344
|
@objc private func doctor() {
|
|
436
|
-
|
|
345
|
+
guard let operation = operations.begin(kind: "doctor", mutationGroup: nil, summary: "Run Doctor") else {
|
|
346
|
+
status.stringValue = "Another guarded operation is already running. Open Diagnostics to review it."
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
generation += 1
|
|
350
|
+
setActionBusy(true)
|
|
351
|
+
NativeView.setBadge(healthBadge, text: "Doctor is running", color: .systemBlue)
|
|
352
|
+
statusSpinner.startAnimation(nil)
|
|
437
353
|
status.stringValue = "Doctor is running…"
|
|
354
|
+
_ = operations.update(operation, state: .running, stage: "running", progress: nil, summary: status.stringValue)
|
|
438
355
|
processClient.run(["doctor", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
439
356
|
guard let self = self else { return }
|
|
440
|
-
self.
|
|
357
|
+
self.setActionBusy(false)
|
|
358
|
+
self.statusSpinner.stopAnimation(nil)
|
|
359
|
+
_ = self.operations.update(
|
|
360
|
+
operation,
|
|
361
|
+
state: result.code == 0 ? .succeeded : .failed,
|
|
362
|
+
stage: "complete",
|
|
363
|
+
progress: 1,
|
|
364
|
+
summary: result.code == 0 ? "Doctor completed" : "Doctor found an issue"
|
|
365
|
+
)
|
|
441
366
|
if result.code == 0 {
|
|
367
|
+
NativeView.setBadge(self.healthBadge, text: "Doctor completed", color: .systemGreen)
|
|
442
368
|
self.status.stringValue = "Doctor completed. No blocking issue was reported."
|
|
443
369
|
} else {
|
|
370
|
+
NativeView.setBadge(self.healthBadge, text: "Doctor needs attention", color: .systemOrange)
|
|
444
371
|
self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
|
|
445
372
|
}
|
|
446
373
|
}
|
|
447
374
|
}
|
|
448
375
|
|
|
376
|
+
private func setActionBusy(_ busy: Bool) {
|
|
377
|
+
updateCodexButton?.isEnabled = !busy
|
|
378
|
+
doctorButton?.isEnabled = !busy
|
|
379
|
+
refreshButton?.isEnabled = !busy
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private func renderStatus(update: [String: Any]?, mcp: [String: Any]?, telegram: [String: Any]?, partial: Bool) {
|
|
383
|
+
let rendered = summary(update: update, mcp: mcp, telegram: telegram)
|
|
384
|
+
status.stringValue = rendered
|
|
385
|
+
if partial {
|
|
386
|
+
NativeView.setBadge(healthBadge, text: "Partial status · still checking", color: .systemBlue)
|
|
387
|
+
} else if rendered.localizedCaseInsensitiveContains("unavailable")
|
|
388
|
+
|| rendered.localizedCaseInsensitiveContains("needs attention") {
|
|
389
|
+
NativeView.setBadge(healthBadge, text: "Status refreshed · attention needed", color: .systemOrange)
|
|
390
|
+
} else {
|
|
391
|
+
NativeView.setBadge(healthBadge, text: "Status refreshed", color: .systemGreen)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
449
395
|
private func summary(update: [String: Any]?, mcp: [String: Any]?, telegram: [String: Any]?) -> String {
|
|
450
396
|
let codexRunning = AppRuntime.codexBundleId.map { bundle in
|
|
451
397
|
NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundle }
|
|
@@ -5,9 +5,13 @@ struct ProcessResult {
|
|
|
5
5
|
let code: Int32
|
|
6
6
|
let output: String
|
|
7
7
|
let truncated: Bool
|
|
8
|
+
let timedOut: Bool
|
|
9
|
+
let emptyOutput: Bool
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
final class ProcessClient {
|
|
13
|
+
static let telegramStatusTimeout: TimeInterval = 12
|
|
14
|
+
|
|
11
15
|
private let actionScript: String
|
|
12
16
|
private let logPath: String
|
|
13
17
|
private let projectRoot: String
|
|
@@ -24,12 +28,17 @@ final class ProcessClient {
|
|
|
24
28
|
stdin: String? = nil,
|
|
25
29
|
environment: [String: String] = [:],
|
|
26
30
|
timeout: TimeInterval? = nil,
|
|
31
|
+
maxOutputBytes: Int? = nil,
|
|
32
|
+
logOutput: Bool = true,
|
|
27
33
|
completion: @escaping (ProcessResult) -> Void
|
|
28
34
|
) {
|
|
29
35
|
let process = Process()
|
|
30
36
|
let output = Pipe()
|
|
37
|
+
let effectiveOutputLimit = max(1024, min(1024 * 1024, maxOutputBytes ?? outputLimit))
|
|
31
38
|
let sensitiveValues = sensitiveStdinValues(arguments: arguments, stdin: stdin)
|
|
32
39
|
let childEnvironment = ProcessInfo.processInfo.environment.merging(environment) { _, override in override }
|
|
40
|
+
let effectiveTimeout = Self.alignedTimeout(arguments: arguments, requested: timeout)
|
|
41
|
+
let execution = ProcessExecutionState()
|
|
33
42
|
process.executableURL = URL(fileURLWithPath: actionScript)
|
|
34
43
|
process.arguments = arguments
|
|
35
44
|
if !environment.isEmpty {
|
|
@@ -60,9 +69,10 @@ final class ProcessClient {
|
|
|
60
69
|
input.fileHandleForWriting.closeFile()
|
|
61
70
|
}
|
|
62
71
|
var timeoutWorkItem: DispatchWorkItem?
|
|
63
|
-
if let timeout =
|
|
72
|
+
if let timeout = effectiveTimeout, timeout > 0 {
|
|
64
73
|
let item = DispatchWorkItem {
|
|
65
74
|
guard process.isRunning else { return }
|
|
75
|
+
execution.markTimedOut()
|
|
66
76
|
let pid = process.processIdentifier
|
|
67
77
|
process.terminate()
|
|
68
78
|
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1) {
|
|
@@ -76,17 +86,43 @@ final class ProcessClient {
|
|
|
76
86
|
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
77
87
|
process.waitUntilExit()
|
|
78
88
|
timeoutWorkItem?.cancel()
|
|
79
|
-
let truncated = data.count >
|
|
80
|
-
let bounded = Data(data.suffix(
|
|
89
|
+
let truncated = data.count > effectiveOutputLimit
|
|
90
|
+
let bounded = Data(data.suffix(effectiveOutputLimit))
|
|
81
91
|
let rawText = String(data: bounded, encoding: .utf8) ?? ""
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
let timedOut = execution.timedOut
|
|
93
|
+
let emptyOutput = !timedOut
|
|
94
|
+
&& rawText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
95
|
+
let resultCode: Int32 = timedOut ? -2 : emptyOutput ? -3 : process.terminationStatus
|
|
96
|
+
let text = timedOut
|
|
97
|
+
? self.timeoutOutput(rawText, sensitiveValues: sensitiveValues)
|
|
98
|
+
: emptyOutput
|
|
99
|
+
? Self.nativeFailureOutput("native_process_empty_output")
|
|
100
|
+
: self.publicOutput(rawText, code: resultCode, sensitiveValues: sensitiveValues)
|
|
101
|
+
if logOutput {
|
|
102
|
+
self.writeLog(command: arguments, output: text, sensitiveValues: sensitiveValues)
|
|
103
|
+
}
|
|
104
|
+
DispatchQueue.main.async {
|
|
105
|
+
completion(ProcessResult(
|
|
106
|
+
code: resultCode,
|
|
107
|
+
output: text,
|
|
108
|
+
truncated: truncated,
|
|
109
|
+
timedOut: timedOut,
|
|
110
|
+
emptyOutput: emptyOutput
|
|
111
|
+
))
|
|
112
|
+
}
|
|
85
113
|
}
|
|
86
114
|
} catch {
|
|
87
115
|
let text = publicOutput(String(describing: error), code: -1, sensitiveValues: sensitiveValues)
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
if logOutput {
|
|
117
|
+
writeLog(command: arguments, output: text, sensitiveValues: sensitiveValues)
|
|
118
|
+
}
|
|
119
|
+
completion(ProcessResult(
|
|
120
|
+
code: -1,
|
|
121
|
+
output: text,
|
|
122
|
+
truncated: false,
|
|
123
|
+
timedOut: false,
|
|
124
|
+
emptyOutput: false
|
|
125
|
+
))
|
|
90
126
|
}
|
|
91
127
|
}
|
|
92
128
|
|
|
@@ -108,6 +144,24 @@ final class ProcessClient {
|
|
|
108
144
|
return URL(fileURLWithPath: home, isDirectory: true)
|
|
109
145
|
}
|
|
110
146
|
|
|
147
|
+
static func alignedTimeout(arguments: [String], requested: TimeInterval?) -> TimeInterval? {
|
|
148
|
+
guard arguments.starts(with: ["telegram", "status"]) else { return requested }
|
|
149
|
+
return max(requested ?? 0, telegramStatusTimeout)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static func nativeFailureOutput(_ error: String) -> String {
|
|
153
|
+
#"{"schema":"sks.native-process-error.v1","ok":false,"error":"\#(error)"}"#
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func timeoutOutput(_ partialOutput: String, sensitiveValues: [String]) -> String {
|
|
157
|
+
let failure = Self.nativeFailureOutput("native_process_timeout")
|
|
158
|
+
guard !partialOutput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
159
|
+
return failure
|
|
160
|
+
}
|
|
161
|
+
let partial = publicOutput(partialOutput, code: -2, sensitiveValues: sensitiveValues)
|
|
162
|
+
return "\(partial)\n\(failure)"
|
|
163
|
+
}
|
|
164
|
+
|
|
111
165
|
func redact(_ value: String, sensitiveValues: [String] = []) -> String {
|
|
112
166
|
var text = value
|
|
113
167
|
for sensitiveValue in sensitiveValues where !sensitiveValue.isEmpty {
|
|
@@ -119,7 +173,7 @@ final class ProcessClient {
|
|
|
119
173
|
#"github_pat_[A-Za-z0-9_]{20,}"#,
|
|
120
174
|
#"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}"#,
|
|
121
175
|
#"(?i)(api[_-]?key|secret|token)\s*[:=]\s*[^\s\"',}]+"#,
|
|
122
|
-
#"
|
|
176
|
+
#"(?:\\/|/)Users(?:\\/|/)[A-Za-z0-9._-]+"#,
|
|
123
177
|
NSRegularExpression.escapedPattern(for: projectRoot)
|
|
124
178
|
]
|
|
125
179
|
for pattern in patterns where !pattern.isEmpty {
|
|
@@ -140,9 +194,34 @@ final class ProcessClient {
|
|
|
140
194
|
private func publicOutput(_ value: String, code: Int32, sensitiveValues: [String]) -> String {
|
|
141
195
|
let redacted = redact(value, sensitiveValues: sensitiveValues)
|
|
142
196
|
guard sensitiveValues.isEmpty else {
|
|
197
|
+
let payload = extractJsonPayload(redacted)
|
|
198
|
+
var envelope: [String: Any] = [
|
|
199
|
+
"schema": "sks.secure-input-operation.v1",
|
|
200
|
+
"ok": code == 0,
|
|
201
|
+
"output_suppressed": true
|
|
202
|
+
]
|
|
203
|
+
if let data = payload.data(using: .utf8),
|
|
204
|
+
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
205
|
+
if let sourceSchema = object["schema"] as? String, !sourceSchema.isEmpty {
|
|
206
|
+
envelope["source_schema"] = String(sourceSchema.prefix(120))
|
|
207
|
+
}
|
|
208
|
+
if let status = object["status"] as? String, !status.isEmpty {
|
|
209
|
+
envelope["status"] = String(status.prefix(160))
|
|
210
|
+
}
|
|
211
|
+
if code != 0, let error = object["error"] as? String, !error.isEmpty {
|
|
212
|
+
envelope["error"] = String(error.prefix(240))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if code != 0, envelope["error"] == nil {
|
|
216
|
+
envelope["error"] = "secure_input_operation_failed_exit_\(code)"
|
|
217
|
+
}
|
|
218
|
+
if let data = try? JSONSerialization.data(withJSONObject: envelope, options: [.sortedKeys]),
|
|
219
|
+
let text = String(data: data, encoding: .utf8) {
|
|
220
|
+
return text
|
|
221
|
+
}
|
|
143
222
|
return code == 0
|
|
144
|
-
? "
|
|
145
|
-
: "
|
|
223
|
+
? #"{"schema":"sks.secure-input-operation.v1","ok":true,"output_suppressed":true}"#
|
|
224
|
+
: #"{"schema":"sks.secure-input-operation.v1","ok":false,"error":"secure_input_operation_failed","output_suppressed":true}"#
|
|
146
225
|
}
|
|
147
226
|
return extractJsonPayload(redacted)
|
|
148
227
|
}
|
|
@@ -163,3 +242,20 @@ final class ProcessClient {
|
|
|
163
242
|
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: logPath)
|
|
164
243
|
}
|
|
165
244
|
}
|
|
245
|
+
|
|
246
|
+
private final class ProcessExecutionState {
|
|
247
|
+
private let lock = NSLock()
|
|
248
|
+
private var didTimeOut = false
|
|
249
|
+
|
|
250
|
+
var timedOut: Bool {
|
|
251
|
+
lock.lock()
|
|
252
|
+
defer { lock.unlock() }
|
|
253
|
+
return didTimeOut
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func markTimedOut() {
|
|
257
|
+
lock.lock()
|
|
258
|
+
didTimeOut = true
|
|
259
|
+
lock.unlock()
|
|
260
|
+
}
|
|
261
|
+
}
|