@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
// The mechanical half of this module now lives in cli-process-runner.mjs, so
|
|
5
|
+
// the CLI install service and the Grok auth service share one implementation
|
|
6
|
+
// of it rather than each growing its own. The four names this module has
|
|
7
|
+
// always exported are re-exported below: every importer and test of this path
|
|
8
|
+
// keeps working, and the state machine underneath is unchanged.
|
|
9
|
+
import {
|
|
10
|
+
CLI_SPAWN_DISABLED_ERROR,
|
|
11
|
+
MAX_CAPTURED_OUTPUT_CHARS,
|
|
12
|
+
joinWrappedLines,
|
|
13
|
+
killTree as killProcessTree,
|
|
14
|
+
runToCompletion as runProcessToCompletion,
|
|
15
|
+
scrubSecrets,
|
|
16
|
+
stripTerminalEscapes,
|
|
17
|
+
tailOf,
|
|
18
|
+
} from './cli-process-runner.mjs';
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
CLI_SPAWN_DISABLED_ERROR,
|
|
22
|
+
joinWrappedLines,
|
|
23
|
+
scrubSecrets,
|
|
24
|
+
stripTerminalEscapes,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Claude CLI auth (status / login / logout) driven from the relay.
|
|
29
|
+
*
|
|
30
|
+
* The CLI's `auth login` prints an OAuth authorize URL and then blocks reading
|
|
31
|
+
* the pasted code from a TTY, so it runs under the same `script` pseudo-TTY
|
|
32
|
+
* trick the Copilot worker uses (session-worker-launch-service.mjs:407);
|
|
33
|
+
* `script` forwards our pipe stdin into the PTY, so writing `code + "\n"` to
|
|
34
|
+
* the child's stdin reaches the prompt.
|
|
35
|
+
*
|
|
36
|
+
* One login session at a time, relay-wide:
|
|
37
|
+
*
|
|
38
|
+
* idle -> starting -> awaiting_code -> exchanging -> success
|
|
39
|
+
* | |
|
|
40
|
+
* +-- cancel/timeout, error (rejected code / dead
|
|
41
|
+
* process) --> session released
|
|
42
|
+
*
|
|
43
|
+
* `success` and `error` are terminal *display* states: the session is already
|
|
44
|
+
* released (a new `startLogin()` may begin, `logout()` is allowed again) but the
|
|
45
|
+
* outcome stays readable until the next start or `cancel()` resets it to idle.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export const CLAUDE_AUTH_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
49
|
+
export const CLAUDE_AUTH_STATUS_TTL_MS = 5_000;
|
|
50
|
+
const CLAUDE_AUTH_STATUS_TIMEOUT_MS = 20_000;
|
|
51
|
+
const CLAUDE_AUTH_LOGOUT_TIMEOUT_MS = 60_000;
|
|
52
|
+
const KILL_ESCALATION_MS = 2_000;
|
|
53
|
+
const MAX_CODE_LENGTH = 4_096;
|
|
54
|
+
|
|
55
|
+
// The authorize URL has moved hosts across CLI releases (claude.com today,
|
|
56
|
+
// claude.ai and console.anthropic.com in older/enterprise builds), so the host
|
|
57
|
+
// is matched as a family rather than pinned to one domain.
|
|
58
|
+
const AUTH_URL_PATTERN = /https:\/\/(?:[A-Za-z0-9-]+\.)*(?:claude\.(?:com|ai)|anthropic\.com)\/\S*/;
|
|
59
|
+
// OSC-8 hyperlink: ESC ] 8 ; ; <target> (BEL | ESC \)
|
|
60
|
+
const OSC8_PATTERN = /\x1b\]8;;(.*?)(?:\x07|\x1b\\)/g;
|
|
61
|
+
const PROMPT_SENTINEL = 'Paste code here';
|
|
62
|
+
|
|
63
|
+
const LOGIN_STATES = Object.freeze({
|
|
64
|
+
IDLE: 'idle',
|
|
65
|
+
STARTING: 'starting',
|
|
66
|
+
AWAITING_CODE: 'awaiting_code',
|
|
67
|
+
EXCHANGING: 'exchanging',
|
|
68
|
+
SUCCESS: 'success',
|
|
69
|
+
ERROR: 'error',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const CLAUDE_AUTH_LOGIN_STATES = LOGIN_STATES;
|
|
73
|
+
|
|
74
|
+
function normalizeText(value) {
|
|
75
|
+
return String(value == null ? '' : value).trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pulls the OAuth authorize URL out of raw CLI output.
|
|
80
|
+
*
|
|
81
|
+
* The OSC-8 hyperlink *target* is preferred over the visible label: the label is
|
|
82
|
+
* what a terminal would wrap or truncate, the target is the real href. Falls
|
|
83
|
+
* back to matching the escape-stripped text so a CLI release that drops the
|
|
84
|
+
* hyperlink wrapper still parses (risk row 1 of the plan: degrade, never break),
|
|
85
|
+
* rejoining a hard-wrapped URL when the visible match runs into a line break.
|
|
86
|
+
*/
|
|
87
|
+
export function extractAuthUrl(rawOutput) {
|
|
88
|
+
const raw = String(rawOutput == null ? '' : rawOutput);
|
|
89
|
+
for (const match of raw.matchAll(OSC8_PATTERN)) {
|
|
90
|
+
const target = normalizeText(match[1]);
|
|
91
|
+
const hit = target.match(AUTH_URL_PATTERN);
|
|
92
|
+
if (hit) return hit[0];
|
|
93
|
+
}
|
|
94
|
+
const visible = stripTerminalEscapes(raw);
|
|
95
|
+
const direct = visible.match(AUTH_URL_PATTERN);
|
|
96
|
+
if (!direct) return '';
|
|
97
|
+
const endedAtLineBreak = visible[direct.index + direct[0].length] === '\n';
|
|
98
|
+
if (!endedAtLineBreak) return direct[0];
|
|
99
|
+
const rejoined = joinWrappedLines(visible).match(AUTH_URL_PATTERN);
|
|
100
|
+
return rejoined && rejoined[0].length > direct[0].length ? rejoined[0] : direct[0];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function hasCodePrompt(rawOutput) {
|
|
104
|
+
return stripTerminalEscapes(rawOutput).includes(PROMPT_SENTINEL);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shellQuote(value) {
|
|
108
|
+
return `'${String(value == null ? '' : value).replace(/'/g, `'\\''`)}'`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseAuthStatusJson(stdout) {
|
|
112
|
+
const text = stripTerminalEscapes(stdout).trim();
|
|
113
|
+
if (!text) return null;
|
|
114
|
+
// Tolerate leading noise (npm shim banners etc.) by taking the last JSON
|
|
115
|
+
// object in the output.
|
|
116
|
+
const start = text.indexOf('{');
|
|
117
|
+
const end = text.lastIndexOf('}');
|
|
118
|
+
if (start < 0 || end <= start) return null;
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeStatusPayload(parsed) {
|
|
127
|
+
return {
|
|
128
|
+
loggedIn: parsed?.loggedIn === true,
|
|
129
|
+
authMethod: normalizeText(parsed?.authMethod) || null,
|
|
130
|
+
apiProvider: normalizeText(parsed?.apiProvider) || null,
|
|
131
|
+
email: normalizeText(parsed?.email) || null,
|
|
132
|
+
orgId: normalizeText(parsed?.orgId) || null,
|
|
133
|
+
orgName: normalizeText(parsed?.orgName) || null,
|
|
134
|
+
subscriptionType: normalizeText(parsed?.subscriptionType) || null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolveClaudeAuthBinary(env = process.env) {
|
|
139
|
+
return normalizeText(env?.COPILOT_WEB_RELAY_CLAUDE_AUTH_BIN) || 'claude';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createClaudeAuthService({
|
|
143
|
+
env = process.env,
|
|
144
|
+
platform = process.platform,
|
|
145
|
+
spawnImpl = spawn,
|
|
146
|
+
processKillImpl = process.kill.bind(process),
|
|
147
|
+
setTimeoutImpl = setTimeout,
|
|
148
|
+
clearTimeoutImpl = clearTimeout,
|
|
149
|
+
now = () => Date.now(),
|
|
150
|
+
logger = console,
|
|
151
|
+
loginTimeoutMs = CLAUDE_AUTH_LOGIN_TIMEOUT_MS,
|
|
152
|
+
statusTtlMs = CLAUDE_AUTH_STATUS_TTL_MS,
|
|
153
|
+
statusTimeoutMs = CLAUDE_AUTH_STATUS_TIMEOUT_MS,
|
|
154
|
+
logoutTimeoutMs = CLAUDE_AUTH_LOGOUT_TIMEOUT_MS,
|
|
155
|
+
} = {}) {
|
|
156
|
+
const listeners = new Set();
|
|
157
|
+
const successHooks = new Set();
|
|
158
|
+
const login = {
|
|
159
|
+
state: LOGIN_STATES.IDLE,
|
|
160
|
+
authUrl: '',
|
|
161
|
+
error: '',
|
|
162
|
+
startedAt: null,
|
|
163
|
+
};
|
|
164
|
+
let session = null;
|
|
165
|
+
// Bumped by every startLogin/cancel so a slow post-success continuation can
|
|
166
|
+
// tell whether it still owns the visible login state.
|
|
167
|
+
let loginGeneration = 0;
|
|
168
|
+
let statusCache = null;
|
|
169
|
+
let statusInFlight = null;
|
|
170
|
+
// Serialises status spawns: a forced read never joins an older in-flight
|
|
171
|
+
// probe (it could predate the login/logout that prompted the force), but it
|
|
172
|
+
// waits for it rather than running two `claude auth status` at once.
|
|
173
|
+
let statusChain = Promise.resolve();
|
|
174
|
+
|
|
175
|
+
const log = (message) => {
|
|
176
|
+
try { logger?.log?.(`[claude-auth] ${message}`); } catch {}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function loginSnapshot() {
|
|
180
|
+
return {
|
|
181
|
+
state: login.state,
|
|
182
|
+
authUrl: login.authUrl || null,
|
|
183
|
+
error: login.error || null,
|
|
184
|
+
startedAt: login.startedAt || null,
|
|
185
|
+
active: Boolean(session),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function emitState() {
|
|
190
|
+
const snapshot = loginSnapshot();
|
|
191
|
+
for (const listener of [...listeners]) {
|
|
192
|
+
try { listener(snapshot); } catch (error) { log(`listener failed: ${error?.message || error}`); }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function setLoginState(nextState, { authUrl, error } = {}) {
|
|
197
|
+
login.state = nextState;
|
|
198
|
+
if (authUrl !== undefined) login.authUrl = authUrl || '';
|
|
199
|
+
if (error !== undefined) login.error = error || '';
|
|
200
|
+
emitState();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildSpawnEnv() {
|
|
204
|
+
// A plain copy of the relay environment: CLAUDE_CONFIG_DIR (and HOME) come
|
|
205
|
+
// along, so status/login/logout all address the same config root the
|
|
206
|
+
// workers read.
|
|
207
|
+
return { ...env };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cliSpawnsDisabled() {
|
|
211
|
+
// Same kill switch (and same truthiness rule) as
|
|
212
|
+
// session-worker-launch-service.mjs: a relay started with it must never run
|
|
213
|
+
// a real CLI — including `claude auth logout`, which would sign the
|
|
214
|
+
// developer's own host out.
|
|
215
|
+
if (!String(env?.COPILOT_WEB_RELAY_DISABLE_CLI_SPAWN || '').trim()) return false;
|
|
216
|
+
// Narrow escape hatch for the e2e harness, which keeps the kill switch on
|
|
217
|
+
// (no real Copilot/Claude workers) but still drives the whole auth flow.
|
|
218
|
+
// Both halves are required: the opt-in flag AND an explicit binary override,
|
|
219
|
+
// so "spawns disabled" can never end up running the host's real `claude`.
|
|
220
|
+
return !(
|
|
221
|
+
String(env?.COPILOT_WEB_RELAY_CLAUDE_AUTH_ALLOW_STUB_SPAWN || '').trim()
|
|
222
|
+
&& String(env?.COPILOT_WEB_RELAY_CLAUDE_AUTH_BIN || '').trim()
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The single spawn site for every auth subcommand.
|
|
228
|
+
*
|
|
229
|
+
* POSIX login/logout run under a pseudo-TTY (`script -qec "<cmd>" /dev/null`)
|
|
230
|
+
* with piped stdio, detached so the whole process group can be signalled. On
|
|
231
|
+
* Windows there is no `script`, so the binary is spawned directly (best
|
|
232
|
+
* effort — the auth flow is a POSIX-host feature). `auth status --json` needs
|
|
233
|
+
* no TTY and runs without one.
|
|
234
|
+
*/
|
|
235
|
+
function spawnAuthProcess(args, { pty = true, stdin = 'pipe' } = {}) {
|
|
236
|
+
if (cliSpawnsDisabled()) throw new Error(CLI_SPAWN_DISABLED_ERROR);
|
|
237
|
+
const bin = resolveClaudeAuthBinary(env);
|
|
238
|
+
const child = (!pty || platform === 'win32')
|
|
239
|
+
? spawnImpl(bin, args, {
|
|
240
|
+
stdio: [stdin, 'pipe', 'pipe'],
|
|
241
|
+
env: buildSpawnEnv(),
|
|
242
|
+
windowsHide: true,
|
|
243
|
+
})
|
|
244
|
+
: spawnImpl('script', ['-qec', [bin, ...args].map(shellQuote).join(' '), '/dev/null'], {
|
|
245
|
+
stdio: [stdin, 'pipe', 'pipe'],
|
|
246
|
+
env: buildSpawnEnv(),
|
|
247
|
+
detached: true,
|
|
248
|
+
});
|
|
249
|
+
// stdin errors (EPIPE when the CLI exits mid-write) arrive asynchronously
|
|
250
|
+
// as 'error' events; without a listener Node turns them into an unhandled
|
|
251
|
+
// error that would take the relay down.
|
|
252
|
+
child?.stdin?.on?.('error', (error) => {
|
|
253
|
+
log(`child stdin error: ${error?.message || error}`);
|
|
254
|
+
});
|
|
255
|
+
return child;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function killTree(child, signal) {
|
|
259
|
+
killProcessTree(child, signal, { platform, processKillImpl });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function runToCompletion(args, { timeoutMs, usePty = true } = {}) {
|
|
263
|
+
return runProcessToCompletion(
|
|
264
|
+
() => spawnAuthProcess(args, { pty: usePty, stdin: usePty ? 'pipe' : 'ignore' }),
|
|
265
|
+
{
|
|
266
|
+
timeoutMs,
|
|
267
|
+
setTimeoutImpl,
|
|
268
|
+
clearTimeoutImpl,
|
|
269
|
+
killChild: killTree,
|
|
270
|
+
maxOutputChars: MAX_CAPTURED_OUTPUT_CHARS,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function fetchStatus() {
|
|
276
|
+
const result = await runToCompletion(['auth', 'status', '--json'], {
|
|
277
|
+
timeoutMs: statusTimeoutMs,
|
|
278
|
+
usePty: false,
|
|
279
|
+
});
|
|
280
|
+
const parsed = parseAuthStatusJson(result.output);
|
|
281
|
+
if (!parsed) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
...normalizeStatusPayload(null),
|
|
285
|
+
error: result.error
|
|
286
|
+
|| tailOf(stripTerminalEscapes(result.output))
|
|
287
|
+
|| `claude auth status exited with code ${result.code}`,
|
|
288
|
+
checkedAt: new Date(now()).toISOString(),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
...normalizeStatusPayload(parsed),
|
|
294
|
+
error: null,
|
|
295
|
+
checkedAt: new Date(now()).toISOString(),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function getStatus({ force = false } = {}) {
|
|
300
|
+
if (!force && statusCache && !statusCache.stale && (now() - statusCache.at) < statusTtlMs) {
|
|
301
|
+
return statusCache.value;
|
|
302
|
+
}
|
|
303
|
+
// A non-forced caller is happy with whatever probe is already running; a
|
|
304
|
+
// forced one is not (that probe may have started before the login or logout
|
|
305
|
+
// it is meant to observe), so it queues its own behind it.
|
|
306
|
+
if (statusInFlight && !force) return statusInFlight;
|
|
307
|
+
const pending = statusChain
|
|
308
|
+
.then(() => fetchStatus())
|
|
309
|
+
.then((value) => {
|
|
310
|
+
statusCache = { at: now(), value, stale: false };
|
|
311
|
+
return value;
|
|
312
|
+
});
|
|
313
|
+
statusChain = pending.then(() => {}, () => {});
|
|
314
|
+
statusInFlight = pending;
|
|
315
|
+
pending.then(
|
|
316
|
+
() => { if (statusInFlight === pending) statusInFlight = null; },
|
|
317
|
+
() => { if (statusInFlight === pending) statusInFlight = null; },
|
|
318
|
+
);
|
|
319
|
+
return pending;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Last status the CLI reported, without ever spawning. */
|
|
323
|
+
function getCachedStatus() {
|
|
324
|
+
return statusCache ? statusCache.value : null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Keeps the last-known value readable (broadcasts render it) while forcing
|
|
329
|
+
* the next `getStatus()` to re-read the CLI.
|
|
330
|
+
*/
|
|
331
|
+
function invalidateStatusCache() {
|
|
332
|
+
if (statusCache) statusCache.stale = true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Drops the session's timers and, when it is still the current one, the
|
|
337
|
+
* module-level handle. A late `close` from an already-settled session must
|
|
338
|
+
* never tear down a newer session started in the meantime.
|
|
339
|
+
*/
|
|
340
|
+
function releaseSession(owner = null) {
|
|
341
|
+
const current = owner || session;
|
|
342
|
+
if (!current) return;
|
|
343
|
+
if (current.timeoutTimer) clearTimeoutImpl(current.timeoutTimer);
|
|
344
|
+
current.timeoutTimer = null;
|
|
345
|
+
if (session === current) session = null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function terminateSession(current, { signal = 'SIGTERM' } = {}) {
|
|
349
|
+
const child = current?.child;
|
|
350
|
+
if (!child) return;
|
|
351
|
+
killTree(child, signal);
|
|
352
|
+
// Deliberately not stored on the session: releaseSession() runs immediately
|
|
353
|
+
// after most terminate calls, so a session-owned timer would be cleared
|
|
354
|
+
// before it could escalate and a SIGTERM-ignoring CLI would live forever.
|
|
355
|
+
// Unref'd and closed over the child, so it is a no-op once the child exits.
|
|
356
|
+
const escalation = setTimeoutImpl(() => killTree(child, 'SIGKILL'), KILL_ESCALATION_MS);
|
|
357
|
+
escalation?.unref?.();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function failSession(current, message) {
|
|
361
|
+
if (!current || current.settled) return;
|
|
362
|
+
current.settled = true;
|
|
363
|
+
terminateSession(current);
|
|
364
|
+
const scrubbed = scrubSecrets(message, current.submittedCode);
|
|
365
|
+
releaseSession(current);
|
|
366
|
+
invalidateStatusCache();
|
|
367
|
+
setLoginState(LOGIN_STATES.ERROR, { error: scrubbed || 'Claude login failed' });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function succeedSession(current) {
|
|
371
|
+
if (!current || current.settled) return;
|
|
372
|
+
current.settled = true;
|
|
373
|
+
releaseSession(current);
|
|
374
|
+
invalidateStatusCache();
|
|
375
|
+
// Success is published before the confirming status read: that read spawns
|
|
376
|
+
// the CLI again (up to statusTimeoutMs), and the UI must not sit on
|
|
377
|
+
// "exchanging" while it runs.
|
|
378
|
+
const generation = loginGeneration;
|
|
379
|
+
setLoginState(LOGIN_STATES.SUCCESS, { error: '' });
|
|
380
|
+
try {
|
|
381
|
+
await getStatus({ force: true });
|
|
382
|
+
} catch (error) {
|
|
383
|
+
log(`status refresh after login failed: ${error?.message || error}`);
|
|
384
|
+
}
|
|
385
|
+
if (generation !== loginGeneration) {
|
|
386
|
+
// A new login (or a cancel) took over while the status read ran; its own
|
|
387
|
+
// outcome owns the state and the hooks now.
|
|
388
|
+
log('skipping post-login refresh broadcast: a newer login session took over');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// Re-broadcast the unchanged login state so subscribers pick up the fresh
|
|
392
|
+
// status that just landed in the cache.
|
|
393
|
+
emitState();
|
|
394
|
+
for (const hook of [...successHooks]) {
|
|
395
|
+
try { await hook(); } catch (error) { log(`post-login hook failed: ${error?.message || error}`); }
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function handleLoginOutput(current, chunk) {
|
|
400
|
+
const text = chunk.toString();
|
|
401
|
+
current.rawOutput = `${current.rawOutput}${text}`.slice(-MAX_CAPTURED_OUTPUT_CHARS);
|
|
402
|
+
// Once the code has been written to stdin nothing more from this process is
|
|
403
|
+
// logged: the exchange output can echo credential material.
|
|
404
|
+
if (!current.submittedCode) {
|
|
405
|
+
const visible = stripTerminalEscapes(text).trim();
|
|
406
|
+
if (visible) log(`login: ${visible.replace(/\s+/g, ' ').slice(0, 300)}`);
|
|
407
|
+
}
|
|
408
|
+
if (current.settled) return;
|
|
409
|
+
if (login.authUrl) return;
|
|
410
|
+
const url = extractAuthUrl(current.rawOutput);
|
|
411
|
+
if (url) setLoginState(LOGIN_STATES.AWAITING_CODE, { authUrl: url, error: '' });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function startLogin() {
|
|
415
|
+
if (session) {
|
|
416
|
+
// Idempotent: a second Relogin press (or a second browser tab) joins the
|
|
417
|
+
// session already in flight rather than racing a new PKCE challenge.
|
|
418
|
+
return { ok: true, login: loginSnapshot(), reused: true };
|
|
419
|
+
}
|
|
420
|
+
const startedAt = new Date(now()).toISOString();
|
|
421
|
+
let child = null;
|
|
422
|
+
try {
|
|
423
|
+
child = spawnAuthProcess(['auth', 'login']);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
loginGeneration += 1;
|
|
426
|
+
setLoginState(LOGIN_STATES.ERROR, {
|
|
427
|
+
authUrl: '',
|
|
428
|
+
error: scrubSecrets(error?.message || String(error)),
|
|
429
|
+
});
|
|
430
|
+
return { ok: false, error: login.error, login: loginSnapshot() };
|
|
431
|
+
}
|
|
432
|
+
const current = {
|
|
433
|
+
child,
|
|
434
|
+
rawOutput: '',
|
|
435
|
+
submittedCode: '',
|
|
436
|
+
settled: false,
|
|
437
|
+
timeoutTimer: null,
|
|
438
|
+
};
|
|
439
|
+
session = current;
|
|
440
|
+
loginGeneration += 1;
|
|
441
|
+
login.startedAt = startedAt;
|
|
442
|
+
setLoginState(LOGIN_STATES.STARTING, { authUrl: '', error: '' });
|
|
443
|
+
|
|
444
|
+
current.timeoutTimer = setTimeoutImpl(() => {
|
|
445
|
+
failSession(current, `Claude login timed out after ${Math.round(loginTimeoutMs / 1000)}s`);
|
|
446
|
+
}, loginTimeoutMs);
|
|
447
|
+
current.timeoutTimer?.unref?.();
|
|
448
|
+
|
|
449
|
+
child.stdout?.on?.('data', (chunk) => handleLoginOutput(current, chunk));
|
|
450
|
+
child.stderr?.on?.('data', (chunk) => handleLoginOutput(current, chunk));
|
|
451
|
+
child.on('error', (error) => failSession(current, error?.message || String(error)));
|
|
452
|
+
child.on('close', (code) => {
|
|
453
|
+
if (current.settled) {
|
|
454
|
+
releaseSession(current);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (code === 0 && current.submittedCode) {
|
|
458
|
+
void succeedSession(current);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const tail = tailOf(stripTerminalEscapes(current.rawOutput));
|
|
462
|
+
failSession(current, current.submittedCode
|
|
463
|
+
? (tail || `Claude login exited with code ${code}`)
|
|
464
|
+
: `Claude login exited before a code was submitted (code ${code})${tail ? `: ${tail}` : ''}`);
|
|
465
|
+
});
|
|
466
|
+
return { ok: true, login: loginSnapshot(), reused: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function submitCode(code) {
|
|
470
|
+
const current = session;
|
|
471
|
+
if (!current) {
|
|
472
|
+
return { ok: false, statusCode: 409, error: 'No Claude login is in progress', login: loginSnapshot() };
|
|
473
|
+
}
|
|
474
|
+
if (login.state !== LOGIN_STATES.AWAITING_CODE) {
|
|
475
|
+
return {
|
|
476
|
+
ok: false,
|
|
477
|
+
statusCode: 409,
|
|
478
|
+
error: `Claude login is not waiting for a code (state: ${login.state})`,
|
|
479
|
+
login: loginSnapshot(),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const trimmed = normalizeText(code);
|
|
483
|
+
if (!trimmed) {
|
|
484
|
+
return { ok: false, statusCode: 400, error: 'Missing code', login: loginSnapshot() };
|
|
485
|
+
}
|
|
486
|
+
if (trimmed.length > MAX_CODE_LENGTH || /[\r\n]/.test(trimmed)) {
|
|
487
|
+
return { ok: false, statusCode: 400, error: 'Invalid code', login: loginSnapshot() };
|
|
488
|
+
}
|
|
489
|
+
current.submittedCode = trimmed;
|
|
490
|
+
try {
|
|
491
|
+
current.child.stdin.write(`${trimmed}\n`);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
failSession(current, `Failed to hand the code to the Claude CLI: ${error?.message || error}`);
|
|
494
|
+
return { ok: false, statusCode: 500, error: login.error, login: loginSnapshot() };
|
|
495
|
+
}
|
|
496
|
+
setLoginState(LOGIN_STATES.EXCHANGING, { error: '' });
|
|
497
|
+
return { ok: true, login: loginSnapshot() };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function cancel() {
|
|
501
|
+
const current = session;
|
|
502
|
+
loginGeneration += 1;
|
|
503
|
+
if (current) {
|
|
504
|
+
current.settled = true;
|
|
505
|
+
terminateSession(current);
|
|
506
|
+
releaseSession(current);
|
|
507
|
+
}
|
|
508
|
+
// Also the "dismiss the terminal state" path for the UI.
|
|
509
|
+
login.startedAt = null;
|
|
510
|
+
setLoginState(LOGIN_STATES.IDLE, { authUrl: '', error: '' });
|
|
511
|
+
return { ok: true, login: loginSnapshot() };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function logout() {
|
|
515
|
+
if (session) {
|
|
516
|
+
return { ok: false, statusCode: 409, error: 'A Claude login is in progress; cancel it first' };
|
|
517
|
+
}
|
|
518
|
+
// Same PTY harness as login: `auth logout` was never probed live, so treat
|
|
519
|
+
// it as possibly-interactive and capture whatever it prints.
|
|
520
|
+
const result = await runToCompletion(['auth', 'logout'], { timeoutMs: logoutTimeoutMs });
|
|
521
|
+
const visible = stripTerminalEscapes(result.output);
|
|
522
|
+
if (hasCodePrompt(visible) || /\(y\/n\)|\[y\/N\]/i.test(visible)) {
|
|
523
|
+
log(`logout produced an unexpected prompt: ${tailOf(visible)}`);
|
|
524
|
+
}
|
|
525
|
+
invalidateStatusCache();
|
|
526
|
+
const status = await getStatus({ force: true });
|
|
527
|
+
if (!result.ok) {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
statusCode: 500,
|
|
531
|
+
error: scrubSecrets(result.error || tailOf(visible) || `claude auth logout exited with code ${result.code}`),
|
|
532
|
+
status,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
login.startedAt = null;
|
|
536
|
+
// The status cache is fresh at this point, so the IDLE broadcast subscribers
|
|
537
|
+
// build carries the post-logout account state without another spawn.
|
|
538
|
+
setLoginState(LOGIN_STATES.IDLE, { authUrl: '', error: '' });
|
|
539
|
+
return { ok: true, status };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function subscribe(listener) {
|
|
543
|
+
if (typeof listener !== 'function') return () => {};
|
|
544
|
+
listeners.add(listener);
|
|
545
|
+
return () => listeners.delete(listener);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function onLoginSuccess(hook) {
|
|
549
|
+
if (typeof hook !== 'function') return () => {};
|
|
550
|
+
successHooks.add(hook);
|
|
551
|
+
return () => successHooks.delete(hook);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function dispose() {
|
|
555
|
+
const current = session;
|
|
556
|
+
if (current) {
|
|
557
|
+
current.settled = true;
|
|
558
|
+
loginGeneration += 1;
|
|
559
|
+
terminateSession(current);
|
|
560
|
+
releaseSession(current);
|
|
561
|
+
}
|
|
562
|
+
listeners.clear();
|
|
563
|
+
successHooks.clear();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
getStatus,
|
|
568
|
+
getCachedStatus,
|
|
569
|
+
getLoginState: loginSnapshot,
|
|
570
|
+
startLogin,
|
|
571
|
+
submitCode,
|
|
572
|
+
cancel,
|
|
573
|
+
logout,
|
|
574
|
+
subscribe,
|
|
575
|
+
onLoginSuccess,
|
|
576
|
+
dispose,
|
|
577
|
+
};
|
|
578
|
+
}
|