@huynle/hub 0.6.1 → 0.7.2
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/CHANGELOG.md +870 -0
- package/Dockerfile +33 -0
- package/README.md +1007 -66
- package/package.json +47 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +156 -0
- package/packages/cli/dist/hub-cli.js +21647 -0
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +123 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +252 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +104 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +95 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +553 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +107 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +70 -0
- package/packages/sdk/src/types.ts +517 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +101 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +152 -0
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +328 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +74 -0
- package/src/central/agent/escalation.ts +219 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +385 -0
- package/src/central/agent/routes.ts +281 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +112 -0
- package/src/central/agent/webhook-routes.ts +86 -0
- package/src/central/agent/webhooks.ts +493 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +350 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +116 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +246 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1149 -170
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/mcp-http.ts +116 -0
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +625 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +118 -7
- package/src/index.ts +195 -43
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +174 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +17 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// `hub launch` and `hub launches list | show | stop` (design §6.1; decision znbg8xa5): the flags as a launch spec
|
|
2
|
+
// for POST /api/agent/launches, and the launch routes' answers as short text. The CLI checks only the flags' own
|
|
3
|
+
// shape (enums, key=value, what needs what); the hub validates the spec and fills in its defaults.
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import type { LaunchSpec, LaunchStep, LaunchView } from "../../sdk/src/index.ts"
|
|
6
|
+
import { CLEANUP_POLICIES, LAUNCH_END_WHEN, RUNTIME_KINDS, WORKSPACE_MODES } from "../../protocol/src/launch.ts"
|
|
7
|
+
import { str, UsageError, type FlagKind, type Parsed } from "./args"
|
|
8
|
+
|
|
9
|
+
export const LAUNCH_FLAGS: Readonly<Record<string, FlagKind>> = {
|
|
10
|
+
key: "string",
|
|
11
|
+
name: "string",
|
|
12
|
+
role: "multi",
|
|
13
|
+
harness: "string",
|
|
14
|
+
agent: "string",
|
|
15
|
+
model: "string",
|
|
16
|
+
repo: "string",
|
|
17
|
+
mode: "string",
|
|
18
|
+
branch: "string",
|
|
19
|
+
base: "string",
|
|
20
|
+
remote: "string",
|
|
21
|
+
runtime: "string",
|
|
22
|
+
profile: "string",
|
|
23
|
+
machine: "string",
|
|
24
|
+
prefer: "bool",
|
|
25
|
+
"node-label": "multi",
|
|
26
|
+
label: "multi",
|
|
27
|
+
prompt: "string",
|
|
28
|
+
end: "string",
|
|
29
|
+
timeout: "string",
|
|
30
|
+
cleanup: "string",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `a, b or c` */
|
|
34
|
+
const choices = (options: readonly string[]) => (options.length === 2 ? options.join(" or ") : `${options.slice(0, -1).join(", ")} or ${options.at(-1)}`)
|
|
35
|
+
|
|
36
|
+
function oneOf<T extends string>(flags: Parsed["flags"], flag: string, options: readonly T[]): T | undefined {
|
|
37
|
+
const value = str(flags, flag)
|
|
38
|
+
if (value === undefined) return undefined
|
|
39
|
+
if (!options.includes(value as T)) throw new UsageError(`--${flag} is ${choices(options)}`)
|
|
40
|
+
return value as T
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Repeated `--flag key=value` as an object; undefined when none is given. */
|
|
44
|
+
export function pairsArg(flags: Parsed["flags"], flag: string): Record<string, string> | undefined {
|
|
45
|
+
const given = flags[flag] as string[] | undefined
|
|
46
|
+
if (!given?.length) return undefined
|
|
47
|
+
const out: Record<string, string> = {}
|
|
48
|
+
for (const pair of given) {
|
|
49
|
+
const at = pair.indexOf("=")
|
|
50
|
+
if (at < 1) throw new UsageError(`--${flag} takes key=value (got ${JSON.stringify(pair)})`)
|
|
51
|
+
out[pair.slice(0, at)] = pair.slice(at + 1)
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Only the defined fields of `fields`; undefined when none is. */
|
|
57
|
+
function defined<T extends Record<string, unknown>>(fields: T): Partial<T> | undefined {
|
|
58
|
+
const out: Record<string, unknown> = {}
|
|
59
|
+
for (const [key, value] of Object.entries(fields)) if (value !== undefined) out[key] = value
|
|
60
|
+
return Object.keys(out).length ? (out as Partial<T>) : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface LaunchArgContext {
|
|
64
|
+
/** Relative --repo paths resolve against it; the default repo. */
|
|
65
|
+
readonly cwd: string
|
|
66
|
+
readonly stdin?: () => Promise<string>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The launch spec the flags describe (only the given fields). */
|
|
70
|
+
export async function launchSpecArg(flags: Parsed["flags"], context: LaunchArgContext): Promise<LaunchSpec> {
|
|
71
|
+
const mode = oneOf(flags, "mode", WORKSPACE_MODES)
|
|
72
|
+
const kind = oneOf(flags, "runtime", RUNTIME_KINDS)
|
|
73
|
+
const when = oneOf(flags, "end", LAUNCH_END_WHEN)
|
|
74
|
+
const cleanup = oneOf(flags, "cleanup", CLEANUP_POLICIES)
|
|
75
|
+
const nodeLabels = pairsArg(flags, "node-label")
|
|
76
|
+
const labels = pairsArg(flags, "label")
|
|
77
|
+
const profile = str(flags, "profile")
|
|
78
|
+
const machine = str(flags, "machine")
|
|
79
|
+
if (profile !== undefined && kind === undefined) throw new UsageError("--profile needs --runtime (e.g. --runtime openshell)")
|
|
80
|
+
if (flags.prefer === true && machine === undefined && nodeLabels === undefined) throw new UsageError("--prefer needs --machine or --node-label")
|
|
81
|
+
|
|
82
|
+
let prompt = str(flags, "prompt")
|
|
83
|
+
if (prompt === "-") {
|
|
84
|
+
if (!context.stdin) throw new UsageError("cannot read the prompt from stdin here")
|
|
85
|
+
prompt = await context.stdin()
|
|
86
|
+
}
|
|
87
|
+
if (prompt !== undefined && !prompt.trim()) throw new UsageError("--prompt is empty")
|
|
88
|
+
|
|
89
|
+
const repo = path.resolve(context.cwd, str(flags, "repo") ?? ".")
|
|
90
|
+
const roles = (flags.role as string[] | undefined)?.map((r) => r.replace(/^@@/, ""))
|
|
91
|
+
const name = str(flags, "name")?.replace(/^@+/, "")
|
|
92
|
+
const workspace = { repo, ...defined({ mode, branch: str(flags, "branch"), base: str(flags, "base"), remote: str(flags, "remote") }) }
|
|
93
|
+
const runtime = kind === undefined ? undefined : { kind, ...(profile !== undefined ? { profile } : {}) }
|
|
94
|
+
const placement = defined({ labels: nodeLabels, machine, mode: flags.prefer === true ? ("prefer" as const) : undefined })
|
|
95
|
+
const end = defined({ when, timeout: str(flags, "timeout"), cleanup })
|
|
96
|
+
return {
|
|
97
|
+
...defined({ key: str(flags, "key"), name, roles, harness: str(flags, "harness"), agent: str(flags, "agent"), model: str(flags, "model") }),
|
|
98
|
+
workspace,
|
|
99
|
+
...defined({ runtime, placement, prompt, labels, end }),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------- text ----------
|
|
104
|
+
|
|
105
|
+
const at = (name: string) => `@${name.replace(/^@+/, "")}`
|
|
106
|
+
const stateText = (l: Pick<LaunchView, "state" | "outcome">) => (l.outcome ? `${l.state} ${l.outcome}` : l.state)
|
|
107
|
+
const sessionText = (l: LaunchView) => [l.name ? at(l.name) : undefined, l.sessionID].filter(Boolean).join(" ")
|
|
108
|
+
const labelsText = (labels: Readonly<Record<string, string>> | undefined) =>
|
|
109
|
+
Object.entries(labels ?? {})
|
|
110
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
111
|
+
.join(" ")
|
|
112
|
+
|
|
113
|
+
/** `lch_1: running (session started), @api ses_1` */
|
|
114
|
+
const summary = (l: LaunchView) => `${l.id}: ${stateText(l)} (${l.reason})${sessionText(l) ? `, ${sessionText(l)}` : ""}`
|
|
115
|
+
|
|
116
|
+
/** What `hub launch` prints: a new launch, or the existing launch of the same key. */
|
|
117
|
+
export function launchedText(launch: LaunchView, created: boolean, key: string | undefined = launch.spec.key): string {
|
|
118
|
+
if (created || key === undefined) return `launched ${summary(launch)}`
|
|
119
|
+
return `${launch.id} was already launched with key ${key}: ${summary(launch).slice(launch.id.length + 2)}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const stoppedLaunchText = (launch: LaunchView): string => `stopped ${summary(launch)}`
|
|
123
|
+
|
|
124
|
+
/** `lch_1 running @api ses_1 /repo (branch) app=brain` */
|
|
125
|
+
export function launchLine(l: LaunchView): string {
|
|
126
|
+
const where = l.directory ?? `${l.spec.workspace.repo}${l.spec.workspace.branch ? ` (${l.spec.workspace.branch})` : ""}`
|
|
127
|
+
return [l.id, stateText(l), sessionText(l) || undefined, where, labelsText(l.spec.labels) || undefined].filter(Boolean).join(" ")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const launchesText = (launches: readonly LaunchView[]): string => (launches.length ? launches.map(launchLine).join("\n") : "no launches")
|
|
131
|
+
|
|
132
|
+
function artifactsText(a: NonNullable<LaunchView["artifacts"]>): string {
|
|
133
|
+
const parts: string[] = []
|
|
134
|
+
if (a.branch) parts.push(`branch ${a.branch}${a.base ? ` from ${a.base}` : ""}`)
|
|
135
|
+
if (a.head) parts.push(`head ${a.head}`)
|
|
136
|
+
if (a.commits !== undefined) parts.push(`${a.commits} commit${a.commits === 1 ? "" : "s"}`)
|
|
137
|
+
if (a.dirty !== undefined) parts.push(a.dirty ? "uncommitted changes" : "clean")
|
|
138
|
+
return parts.join(", ")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const stepText = (s: LaunchStep) => ` ${new Date(s.at).toISOString()} ${stateText(s)}: ${s.reason}`
|
|
142
|
+
|
|
143
|
+
/** `hub launches show`: the launch, its spec, its steps. */
|
|
144
|
+
export function launchText(l: LaunchView): string {
|
|
145
|
+
const ws = l.spec.workspace
|
|
146
|
+
const lines = [`${l.id} ${stateText(l)} (${l.reason})`]
|
|
147
|
+
if (sessionText(l) || l.machineID) lines.push(`session: ${[sessionText(l) || "none yet", l.machineID ? `on ${l.machineID}` : undefined].filter(Boolean).join(" ")}`)
|
|
148
|
+
lines.push(`workspace: ${[ws.repo, ws.mode, ws.branch].filter(Boolean).join(" ")}${ws.base ? ` from ${ws.base}` : ""}${l.directory ? ` → ${l.directory}` : ""}`)
|
|
149
|
+
const run = [l.spec.harness, l.spec.agent, l.spec.model, l.spec.runtime ? `${l.spec.runtime.kind}${l.spec.runtime.profile ? `/${l.spec.runtime.profile}` : ""}` : undefined].filter(Boolean)
|
|
150
|
+
if (run.length) lines.push(`runs: ${run.join(" ")}`)
|
|
151
|
+
if (l.spec.end) lines.push(`ends: ${[l.spec.end.when, l.spec.end.timeout ? `timeout ${l.spec.end.timeout}` : undefined, l.spec.end.cleanup ? `cleanup ${l.spec.end.cleanup}` : undefined].filter(Boolean).join(", ")}`)
|
|
152
|
+
if (l.by) lines.push(`by: ${l.by}`)
|
|
153
|
+
if (labelsText(l.spec.labels)) lines.push(`labels: ${labelsText(l.spec.labels)}`)
|
|
154
|
+
if (l.artifacts && artifactsText(l.artifacts)) lines.push(`artifacts: ${artifactsText(l.artifacts)}`)
|
|
155
|
+
if (l.history?.length) lines.push("steps:", ...l.history.map(stepText))
|
|
156
|
+
return lines.join("\n")
|
|
157
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// `hub login` without --token: the CLI login device code (src/central/http.ts, POST /cli/login and /cli/login/poll).
|
|
2
|
+
// The terminal asks with its name and the scopes it wants; a signed-in user approves it in the console as
|
|
3
|
+
// themselves, choosing the scopes (launch only when they tick it or the terminal asked for it); the next poll hands
|
|
4
|
+
// out an app token of that user, once.
|
|
5
|
+
//
|
|
6
|
+
// Policy (the machine enrollment's, packages/cli/src/bootstrap/enrollment/policy.ts):
|
|
7
|
+
// - The hub URL is https (plain http only for a loopback hub); redirects are refused, never followed.
|
|
8
|
+
// - The approval link shown is on the hub's own origin (approvalURL).
|
|
9
|
+
// - The wait is bounded by the hub's expiry and by maxWaitMs; `signal` cancels it.
|
|
10
|
+
// - The token is returned to the caller only, never printed. A poll that fails in transit is retried until the
|
|
11
|
+
// deadline (a reply lost after the hub handed the token out cannot be fetched again: the user revokes that token).
|
|
12
|
+
import { approvalURL, checkServerURL, isUserCode } from "./bootstrap/enrollment/policy.ts"
|
|
13
|
+
import { UsageError } from "./args"
|
|
14
|
+
import { CliError, EXIT } from "./http"
|
|
15
|
+
|
|
16
|
+
/** The longest a login waits for approval, whatever the hub says (the hub's own expiry is 10 minutes). */
|
|
17
|
+
export const LOGIN_MAX_WAIT_MS = 15 * 60_000
|
|
18
|
+
const MIN_INTERVAL_MS = 500
|
|
19
|
+
const MAX_INTERVAL_MS = 30_000
|
|
20
|
+
|
|
21
|
+
/** What to show the user while waiting. Never a credential. */
|
|
22
|
+
export interface LoginPrompt {
|
|
23
|
+
readonly userCode: string
|
|
24
|
+
readonly approveURL: string
|
|
25
|
+
readonly expiresAt: number
|
|
26
|
+
/** The scopes the approval page offers ticked. */
|
|
27
|
+
readonly scopes: readonly string[]
|
|
28
|
+
readonly warning?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DeviceLoginOptions {
|
|
32
|
+
readonly server: string
|
|
33
|
+
/** Names this terminal on the approval page (and in the token's label). */
|
|
34
|
+
readonly clientName: string
|
|
35
|
+
/** Asked for beyond the hub's defaults (sessions:read, messages:send, messages:read). */
|
|
36
|
+
readonly scopes?: readonly string[]
|
|
37
|
+
readonly fetch?: typeof fetch
|
|
38
|
+
readonly signal?: AbortSignal
|
|
39
|
+
readonly now?: () => number
|
|
40
|
+
/** The poll interval (default: the hub's, at least 500 ms). */
|
|
41
|
+
readonly intervalMs?: number
|
|
42
|
+
readonly maxWaitMs?: number
|
|
43
|
+
readonly onPrompt?: (prompt: LoginPrompt) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DeviceLogin {
|
|
47
|
+
readonly token: string
|
|
48
|
+
readonly tokenID: string
|
|
49
|
+
readonly scopes: readonly string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cancelled = () => new CliError(EXIT.needsUser, "login cancelled; nothing was stored", "Run `hub login` again")
|
|
53
|
+
|
|
54
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
if (signal?.aborted) return resolve()
|
|
57
|
+
const timer = setTimeout(done, ms)
|
|
58
|
+
function done() {
|
|
59
|
+
clearTimeout(timer)
|
|
60
|
+
signal?.removeEventListener("abort", done)
|
|
61
|
+
resolve()
|
|
62
|
+
}
|
|
63
|
+
signal?.addEventListener("abort", done, { once: true })
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
68
|
+
const stringList = (value: unknown): string[] | undefined => (Array.isArray(value) && value.every((s) => typeof s === "string") ? (value as string[]) : undefined)
|
|
69
|
+
|
|
70
|
+
/** POSTs JSON without following redirects; the parsed reply, or a CliError. */
|
|
71
|
+
async function post(fetcher: typeof fetch, server: string, pathname: string, body: unknown, signal?: AbortSignal): Promise<{ status: number; body: unknown }> {
|
|
72
|
+
let response: Response
|
|
73
|
+
try {
|
|
74
|
+
response = await fetcher(`${server}${pathname}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), redirect: "manual", ...(signal ? { signal } : {}) })
|
|
75
|
+
} catch (cause) {
|
|
76
|
+
if (signal?.aborted) throw cancelled()
|
|
77
|
+
throw new CliError(EXIT.unavailable, `could not reach ${server}`, "Check the URL given to `hub login`")
|
|
78
|
+
}
|
|
79
|
+
if (response.status >= 300 && response.status < 400) throw new CliError(EXIT.other, `${server} redirected the login request; login does not follow redirects`, "Use the hub's final https URL")
|
|
80
|
+
let parsed: unknown
|
|
81
|
+
try {
|
|
82
|
+
parsed = await response.json()
|
|
83
|
+
} catch {
|
|
84
|
+
parsed = undefined
|
|
85
|
+
}
|
|
86
|
+
return { status: response.status, body: parsed }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const hubMessage = (body: unknown): string | undefined => (isRecord(body) && isRecord(body.error) && typeof body.error.message === "string" ? body.error.message.slice(0, 200) : undefined)
|
|
90
|
+
|
|
91
|
+
/** Runs the device-code login: the approved app token, or a CliError (nothing to store). */
|
|
92
|
+
export async function deviceLogin(options: DeviceLoginOptions): Promise<DeviceLogin> {
|
|
93
|
+
const server = checkServerURL(options.server)
|
|
94
|
+
if (!server) throw new UsageError(`the hub URL must be https (plain http only for a loopback hub), without user info, query or fragment: ${String(options.server).slice(0, 200)}`)
|
|
95
|
+
const fetcher = options.fetch ?? fetch
|
|
96
|
+
const now = options.now ?? Date.now
|
|
97
|
+
const { signal } = options
|
|
98
|
+
if (signal?.aborted) throw cancelled()
|
|
99
|
+
|
|
100
|
+
const started = await post(fetcher, server, "/cli/login", { clientName: options.clientName, ...(options.scopes?.length ? { scopes: options.scopes } : {}) }, signal)
|
|
101
|
+
if (started.status === 404) throw new CliError(EXIT.unavailable, `${server} has no CLI login (an older hub)`, "Create an app token in the console (Settings → Agents) and run `hub login --token <token>`")
|
|
102
|
+
if (started.status !== 200) throw new CliError(started.status === 429 ? EXIT.rateLimited : started.status === 400 ? EXIT.usage : EXIT.other, `${server} refused the login: ${hubMessage(started.body) ?? `HTTP ${started.status}`}`)
|
|
103
|
+
const start = started.body
|
|
104
|
+
if (!isRecord(start) || !isUserCode(start.userCode) || typeof start.deviceCode !== "string" || !start.deviceCode || !Number.isFinite(start.expiresAt) || !Number.isFinite(start.intervalMs)) {
|
|
105
|
+
throw new CliError(EXIT.other, `${server} sent an invalid login reply`)
|
|
106
|
+
}
|
|
107
|
+
const link = approvalURL(server, start.approveURL, start.userCode)
|
|
108
|
+
const expiresAt = Math.min(start.expiresAt as number, now() + (options.maxWaitMs ?? LOGIN_MAX_WAIT_MS))
|
|
109
|
+
options.onPrompt?.({ userCode: start.userCode, approveURL: link.url, expiresAt, scopes: stringList(start.scopes) ?? [], ...(link.warning ? { warning: link.warning } : {}) })
|
|
110
|
+
|
|
111
|
+
const interval = options.intervalMs ?? Math.min(Math.max(start.intervalMs as number, MIN_INTERVAL_MS), MAX_INTERVAL_MS)
|
|
112
|
+
for (;;) {
|
|
113
|
+
if (signal?.aborted) throw cancelled()
|
|
114
|
+
if (now() > expiresAt) break
|
|
115
|
+
let poll: { status: number; body: unknown } | undefined
|
|
116
|
+
try {
|
|
117
|
+
poll = await post(fetcher, server, "/cli/login/poll", { deviceCode: start.deviceCode }, signal)
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
if (signal?.aborted) throw cancelled()
|
|
120
|
+
if (!(cause instanceof CliError) || cause.exit !== EXIT.unavailable) throw cause
|
|
121
|
+
poll = undefined // in transit: try again
|
|
122
|
+
}
|
|
123
|
+
if (poll?.status === 429) throw new CliError(EXIT.rateLimited, `the login was approved, but ${hubMessage(poll.body) ?? "the hub could not issue a token"}`, "Revoke unused app tokens in the console (Settings → Agents) and run `hub login` again")
|
|
124
|
+
const status = poll && isRecord(poll.body) ? poll.body.status : undefined
|
|
125
|
+
if (status === "approved") {
|
|
126
|
+
const body = poll!.body as Record<string, unknown>
|
|
127
|
+
const scopes = stringList(body.scopes)
|
|
128
|
+
if (typeof body.token !== "string" || !body.token || typeof body.tokenID !== "string" || !scopes) throw new CliError(EXIT.other, `${server} sent an invalid login reply`)
|
|
129
|
+
return { token: body.token, tokenID: body.tokenID, scopes }
|
|
130
|
+
}
|
|
131
|
+
if (status === "denied") throw new CliError(EXIT.forbidden, "the login was denied on the hub; nothing was stored")
|
|
132
|
+
if (status === "expired") break
|
|
133
|
+
await sleep(interval, signal)
|
|
134
|
+
}
|
|
135
|
+
if (signal?.aborted) throw cancelled()
|
|
136
|
+
throw new CliError(EXIT.needsUser, "the login code expired before it was approved; nothing was stored", "Run `hub login` again and approve the code in the console")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The lines printed while waiting (stderr). */
|
|
140
|
+
export function promptText(prompt: LoginPrompt): string {
|
|
141
|
+
const minutes = Math.max(1, Math.round((prompt.expiresAt - Date.now()) / 60_000))
|
|
142
|
+
return [
|
|
143
|
+
...(prompt.warning ? [`hub: warning: ${prompt.warning}`] : []),
|
|
144
|
+
`To log in, open ${prompt.approveURL}`,
|
|
145
|
+
`and approve code ${prompt.userCode} as yourself (expires in ${minutes} min; Ctrl-C cancels).`,
|
|
146
|
+
...(prompt.scopes.length ? [`Requested scopes: ${prompt.scopes.join(", ")} (you choose on the page).`] : []),
|
|
147
|
+
"Waiting for approval…",
|
|
148
|
+
].join("\n")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What the new login may not do yet, and how to get it (stdout, after the login). */
|
|
152
|
+
export function scopeNotes(scopes: readonly string[], tokenID: string): string[] {
|
|
153
|
+
if (!scopes.includes("sessions:launch")) return ["note: this login cannot launch sessions; to launch from this terminal run `hub login --scope sessions:launch` (or tick \"Launch sessions\" on the approval page)"]
|
|
154
|
+
return [
|
|
155
|
+
`note: launching also needs a grant with verb launch for this token (holder app:${tokenID}) covering the repository:`,
|
|
156
|
+
scopes.includes("messages:send")
|
|
157
|
+
? ` create it in the console, or ask for it: hub grant request --project <repo path> --allow launch --why "launch from my terminal"`
|
|
158
|
+
: " create it in the console (Settings → Agents)",
|
|
159
|
+
]
|
|
160
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Entry of the hub CLI (`hub` / `hubctl`, one command surface): the npm bundle the spoke's launchers run as
|
|
3
|
+
// `BUN_BE_BUN=1 <opencode binary> hub-cli.js …` (or node), and the standalone hubctl (`bun build --compile` of this
|
|
4
|
+
// file; scripts/build-hubctl.sh). See cli.ts for the program.
|
|
5
|
+
import { homedir } from "node:os"
|
|
6
|
+
import { createInterface } from "node:readline"
|
|
7
|
+
import { run } from "./cli"
|
|
8
|
+
|
|
9
|
+
// The launcher sets BUN_BE_BUN so the OpenCode binary acts as bun; nothing this process starts may inherit it.
|
|
10
|
+
delete process.env.BUN_BE_BUN
|
|
11
|
+
|
|
12
|
+
const argv = process.argv.slice(2)
|
|
13
|
+
|
|
14
|
+
/** All of stdin when piped; a prompted line on a terminal (`hub login` without --token). */
|
|
15
|
+
async function stdin(): Promise<string> {
|
|
16
|
+
if (!process.stdin.isTTY) {
|
|
17
|
+
const chunks: Buffer[] = []
|
|
18
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk))
|
|
19
|
+
return Buffer.concat(chunks).toString("utf8")
|
|
20
|
+
}
|
|
21
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true })
|
|
22
|
+
try {
|
|
23
|
+
return await new Promise<string>((resolve) => rl.question("App token: ", resolve))
|
|
24
|
+
} finally {
|
|
25
|
+
rl.close()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only `watch`, `mcp` and `node run` run until interrupted: SIGINT/SIGTERM end them cleanly (exit 0; the service
|
|
30
|
+
// units stop the node with SIGTERM); other commands keep the default.
|
|
31
|
+
const nodeRun = argv[0] === "node" && argv[1] === "run"
|
|
32
|
+
// `hub claude-code daemon` and `hub claude-code mcp` (the Claude Code spoke) run until interrupted too.
|
|
33
|
+
const claudeDaemon = argv[0] === "claude-code" && argv[1] === "daemon"
|
|
34
|
+
const claudeMcp = argv[0] === "claude-code" && argv[1] === "mcp"
|
|
35
|
+
// `hub codex-spoke serve` (the Codex spoke service) and `hub preset run` (until its pane ends) too.
|
|
36
|
+
const codexServe = argv[0] === "codex-spoke" && argv[1] === "serve"
|
|
37
|
+
const presetRun = argv[0] === "preset" && argv[1] === "run"
|
|
38
|
+
// `hubctl machine enroll` and `hub login` (device code) poll until the code is approved: Ctrl-C cancels them (exit 4,
|
|
39
|
+
// nothing stored). `hubctl setup apply` / `recover` pass the signal to their probes and the verification launch, so
|
|
40
|
+
// Ctrl-C fails the running step and the transaction rolls back, instead of the process dying between a write and its
|
|
41
|
+
// journal entry.
|
|
42
|
+
const enroll = (argv[0] === "machine" && argv[1] === "enroll") || argv[0] === "login"
|
|
43
|
+
const applying = argv[0] === "setup" && (argv[1] === "apply" || argv[1] === "recover")
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
if (argv[0] === "watch" || argv[0] === "mcp" || nodeRun || claudeDaemon || claudeMcp || codexServe || presetRun || enroll || applying)
|
|
46
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) process.once(signal, () => controller.abort())
|
|
47
|
+
|
|
48
|
+
process.exitCode = await run(argv, {
|
|
49
|
+
env: process.env,
|
|
50
|
+
home: homedir(),
|
|
51
|
+
stdout: (s) => process.stdout.write(s),
|
|
52
|
+
stderr: (s) => process.stderr.write(s),
|
|
53
|
+
stdin,
|
|
54
|
+
stdinIsTerminal: process.stdin.isTTY === true,
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
})
|
|
59
|
+
// A signal ends `hub mcp` with its stdin still open; let go of it so the process can exit.
|
|
60
|
+
if (argv[0] === "mcp" || claudeMcp) process.stdin.destroy()
|
|
61
|
+
// Nothing else (sockets, timers) may keep a stopped node, Claude Code daemon or Codex spoke alive, nor `hub claude` /
|
|
62
|
+
// `hub codex` once the harness exited, nor a stopped `hub preset run`.
|
|
63
|
+
if (nodeRun || claudeDaemon || codexServe || argv[0] === "claude" || argv[0] === "codex" || presetRun) process.exit()
|