@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,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mechanical helpers for driving a provider CLI as a child process.
|
|
5
|
+
*
|
|
6
|
+
* Lifted verbatim out of claude-auth-service.mjs, which held the only copy:
|
|
7
|
+
* the CLI install service and the Grok auth service need the same escape
|
|
8
|
+
* stripping, secret scrubbing, process-group kill and run-to-completion
|
|
9
|
+
* wrapper, and a second hand-rolled copy of any of them is exactly how two
|
|
10
|
+
* spawn paths drift apart.
|
|
11
|
+
*
|
|
12
|
+
* Only the pieces with no state of their own moved. The Claude auth state
|
|
13
|
+
* machine is live-verified and stayed where it is; claude-auth-service.mjs
|
|
14
|
+
* re-exports the helpers it used to export, so every importer and test of it
|
|
15
|
+
* is untouched.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Refusal used by every spawn path when the relay runs with CLI spawns off. */
|
|
19
|
+
export const CLI_SPAWN_DISABLED_ERROR = 'cli spawns disabled';
|
|
20
|
+
|
|
21
|
+
/** Retained-output ceiling shared by every capture path (auth, install). */
|
|
22
|
+
export const MAX_CAPTURED_OUTPUT_CHARS = 16_000;
|
|
23
|
+
const MAX_ERROR_TAIL_CHARS = 600;
|
|
24
|
+
|
|
25
|
+
// Any OSC sequence (hyperlinks, window-title sets, clipboard writes): ESC ]
|
|
26
|
+
// <payload> (BEL | ST). Stripped whole so no `0;title` residue reaches the
|
|
27
|
+
// prompt/JSON/error parsers.
|
|
28
|
+
const OSC_PATTERN = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
29
|
+
// Any CSI sequence (SGR colours, cursor moves, erases).
|
|
30
|
+
const CSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
31
|
+
// Remaining single-character / other escape sequences.
|
|
32
|
+
const OTHER_ESCAPE_PATTERN = /\x1b[@-Z\\-_]/g;
|
|
33
|
+
// `script` gives a CLI an 80-column PTY, and CLIs hard-wrap their own output at
|
|
34
|
+
// that width. Only a line that is *exactly* that wide is treated as continued:
|
|
35
|
+
// a line that merely ends in a long token ended because the CLI printed a
|
|
36
|
+
// newline, and gluing the next line onto it would corrupt the match. On a
|
|
37
|
+
// differently-sized terminal this degrades to the pre-wrap behaviour.
|
|
38
|
+
const PTY_WRAP_COLUMNS = 80;
|
|
39
|
+
|
|
40
|
+
function normalizeText(value) {
|
|
41
|
+
return String(value == null ? '' : value).trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Removes OSC sequences (including OSC-8 hyperlink wrappers), ANSI CSI
|
|
46
|
+
* sequences and the CR the PTY adds to every line, leaving the human-visible
|
|
47
|
+
* text.
|
|
48
|
+
*/
|
|
49
|
+
export function stripTerminalEscapes(value) {
|
|
50
|
+
return String(value == null ? '' : value)
|
|
51
|
+
.replace(OSC_PATTERN, '')
|
|
52
|
+
.replace(CSI_PATTERN, '')
|
|
53
|
+
.replace(OTHER_ESCAPE_PATTERN, '')
|
|
54
|
+
.replace(/\r/g, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rejoins lines the PTY hard-wrapped mid-token: a wrap fills the terminal width
|
|
59
|
+
* and leaves no trailing space, so a full-width line is stitched onto the line
|
|
60
|
+
* that follows it (repeatedly, for a URL spanning three or more rows).
|
|
61
|
+
*/
|
|
62
|
+
export function joinWrappedLines(value) {
|
|
63
|
+
const lines = String(value == null ? '' : value).split('\n');
|
|
64
|
+
const joined = [];
|
|
65
|
+
let previousWrapped = false;
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (previousWrapped && joined.length && /^\S/.test(line)) {
|
|
68
|
+
joined[joined.length - 1] = `${joined[joined.length - 1]}${line}`;
|
|
69
|
+
} else {
|
|
70
|
+
joined.push(line);
|
|
71
|
+
}
|
|
72
|
+
previousWrapped = line.length === PTY_WRAP_COLUMNS && /\S$/.test(line);
|
|
73
|
+
}
|
|
74
|
+
return joined.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Redacts anything token-shaped plus an exact secret the caller knows it
|
|
79
|
+
* handed over (the pasted Claude login code), so an error tail can be surfaced
|
|
80
|
+
* to the UI without leaking a credential. Long PKCE segments in an authorize
|
|
81
|
+
* URL get caught by the generic rule too, which is fine: the URL is already
|
|
82
|
+
* carried in the login payload.
|
|
83
|
+
*/
|
|
84
|
+
export function scrubSecrets(text, submittedCode = '') {
|
|
85
|
+
let output = String(text == null ? '' : text);
|
|
86
|
+
const code = normalizeText(submittedCode);
|
|
87
|
+
if (code.length >= 4) {
|
|
88
|
+
output = output.split(code).join('[redacted]');
|
|
89
|
+
}
|
|
90
|
+
return output
|
|
91
|
+
.replace(/sk-ant-[A-Za-z0-9_-]+/g, '[redacted]')
|
|
92
|
+
.replace(/[A-Za-z0-9_-]{32,}/g, '[redacted]');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Collapses captured output into one short single-line tail for an error. */
|
|
96
|
+
export function tailOf(text, limit = MAX_ERROR_TAIL_CHARS) {
|
|
97
|
+
const lines = String(text || '')
|
|
98
|
+
.split('\n')
|
|
99
|
+
.map((line) => line.trimEnd())
|
|
100
|
+
.filter((line) => line.trim());
|
|
101
|
+
const joined = lines.join(' · ');
|
|
102
|
+
return joined.length > limit ? `…${joined.slice(-limit)}` : joined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Signals the child's whole process group where the platform has one, so a
|
|
107
|
+
* `script`/`bash -lc` wrapper cannot leave the real CLI running behind it.
|
|
108
|
+
* A child that has already exited is left alone.
|
|
109
|
+
*/
|
|
110
|
+
export function killTree(child, signal, {
|
|
111
|
+
platform = process.platform,
|
|
112
|
+
processKillImpl = process.kill.bind(process),
|
|
113
|
+
} = {}) {
|
|
114
|
+
if (!child || child.exitCode !== null) return;
|
|
115
|
+
const pid = Number(child.pid);
|
|
116
|
+
if (platform !== 'win32' && Number.isInteger(pid) && pid > 0) {
|
|
117
|
+
try {
|
|
118
|
+
processKillImpl(-pid, signal);
|
|
119
|
+
return;
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
try { child.kill(signal); } catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Runs one already-configured child to completion and resolves (never rejects)
|
|
127
|
+
* with `{ok, code, output, error}`.
|
|
128
|
+
*
|
|
129
|
+
* `spawnChild` is a thunk so the caller keeps ownership of how the process is
|
|
130
|
+
* built — PTY or not, which stdio, which environment — and so a refused spawn
|
|
131
|
+
* (the CLI kill switch) surfaces as a result rather than a throw. `onChild`
|
|
132
|
+
* hands the live handle back for cancellation, and `onOutput` sees each raw
|
|
133
|
+
* chunk for callers that stream a log while it runs.
|
|
134
|
+
*/
|
|
135
|
+
export function runToCompletion(spawnChild, {
|
|
136
|
+
timeoutMs,
|
|
137
|
+
setTimeoutImpl = setTimeout,
|
|
138
|
+
clearTimeoutImpl = clearTimeout,
|
|
139
|
+
killChild = (child, signal) => killTree(child, signal),
|
|
140
|
+
maxOutputChars = MAX_CAPTURED_OUTPUT_CHARS,
|
|
141
|
+
onChild = null,
|
|
142
|
+
onOutput = null,
|
|
143
|
+
} = {}) {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
let child = null;
|
|
146
|
+
try {
|
|
147
|
+
child = spawnChild();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
resolve({ ok: false, code: null, output: '', error: error?.message || String(error) });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try { onChild?.(child); } catch {}
|
|
153
|
+
let output = '';
|
|
154
|
+
let settled = false;
|
|
155
|
+
const append = (chunk) => {
|
|
156
|
+
const text = chunk.toString();
|
|
157
|
+
output = `${output}${text}`.slice(-maxOutputChars);
|
|
158
|
+
try { onOutput?.(text); } catch {}
|
|
159
|
+
};
|
|
160
|
+
const finish = (result) => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
if (timer) clearTimeoutImpl(timer);
|
|
164
|
+
resolve(result);
|
|
165
|
+
};
|
|
166
|
+
const timer = setTimeoutImpl(() => {
|
|
167
|
+
killChild(child, 'SIGTERM');
|
|
168
|
+
finish({ ok: false, code: null, output, error: `timed out after ${timeoutMs}ms` });
|
|
169
|
+
}, timeoutMs);
|
|
170
|
+
timer?.unref?.();
|
|
171
|
+
child.stdout?.on?.('data', append);
|
|
172
|
+
child.stderr?.on?.('data', append);
|
|
173
|
+
child.stdin?.end?.();
|
|
174
|
+
child.on('error', (error) => finish({
|
|
175
|
+
ok: false, code: null, output, error: error?.message || String(error),
|
|
176
|
+
}));
|
|
177
|
+
child.on('close', (code) => finish({
|
|
178
|
+
ok: code === 0, code, output, error: null,
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
|
|
8
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
const BACKOFF_STEPS = [5_000, 10_000, 20_000, 40_000, 60_000];
|
|
11
|
+
const READINESS_WINDOW_MS = 5_000;
|
|
12
|
+
const STABLE_CONNECTION_MS = 30_000;
|
|
13
|
+
const FAST_EXIT_MS = 10_000;
|
|
14
|
+
const MAX_CONSECUTIVE_FAST_EXITS = 3;
|
|
15
|
+
|
|
16
|
+
function toText(value) {
|
|
17
|
+
return String(value || '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeTunnelMode(raw = {}) {
|
|
21
|
+
const mode = toText(raw.mode).toLowerCase();
|
|
22
|
+
if (mode === 'disabled' || mode === 'managed') return mode;
|
|
23
|
+
if (raw.enabled === true) return 'managed';
|
|
24
|
+
return 'disabled';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeExtraArgs(rawValue) {
|
|
28
|
+
if (!Array.isArray(rawValue)) return [];
|
|
29
|
+
return rawValue.map((entry) => toText(entry)).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The npm `cloudflared` package downloads the official binary on demand; it is an
|
|
33
|
+
// optional dependency, so absence must degrade to a config error, never a crash.
|
|
34
|
+
// `requireImpl` is a seam: the package is optional, so a suite running under
|
|
35
|
+
// `npm ci --omit=optional` (or after a failed postinstall download) must still be
|
|
36
|
+
// able to exercise this logic instead of silently testing the host's install state.
|
|
37
|
+
export function resolveCloudflaredBinaryFromPackage({
|
|
38
|
+
existsSync = fs.existsSync,
|
|
39
|
+
requireImpl = requireFromHere,
|
|
40
|
+
} = {}) {
|
|
41
|
+
try {
|
|
42
|
+
const mod = requireImpl('cloudflared');
|
|
43
|
+
const bin = toText(mod?.bin);
|
|
44
|
+
// `bin` is where the package *will* place the binary; it only exists after
|
|
45
|
+
// the on-demand download has run, so an un-downloaded path is not usable.
|
|
46
|
+
if (bin && existsSync(bin)) return bin;
|
|
47
|
+
} catch {}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeCloudflaredTunnelConfig(rawConfig = {}, {
|
|
52
|
+
env = process.env,
|
|
53
|
+
resolveBinary = resolveCloudflaredBinaryFromPackage,
|
|
54
|
+
configBaseDir = process.cwd(),
|
|
55
|
+
pathImpl = path,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const raw = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
58
|
+
const envMode = toText(env?.COPILOT_CLOUDFLARED_MODE).toLowerCase();
|
|
59
|
+
const mode = envMode === 'disabled' || envMode === 'managed'
|
|
60
|
+
? envMode
|
|
61
|
+
: normalizeTunnelMode(raw);
|
|
62
|
+
const required = raw.required === true;
|
|
63
|
+
const token = toText(env?.COPILOT_CLOUDFLARED_TOKEN) || toText(raw.token);
|
|
64
|
+
const binaryInput = toText(env?.COPILOT_CLOUDFLARED_BINARY) || toText(raw.binary);
|
|
65
|
+
const extraArgs = normalizeExtraArgs(raw.extraArgs);
|
|
66
|
+
|
|
67
|
+
let binary = '';
|
|
68
|
+
let binarySource = null;
|
|
69
|
+
if (binaryInput) {
|
|
70
|
+
binary = binaryInput.includes('/') || binaryInput.includes('\\')
|
|
71
|
+
? pathImpl.resolve(configBaseDir, binaryInput)
|
|
72
|
+
: binaryInput;
|
|
73
|
+
binarySource = 'config';
|
|
74
|
+
} else {
|
|
75
|
+
let packageBinary = null;
|
|
76
|
+
try {
|
|
77
|
+
packageBinary = toText(typeof resolveBinary === 'function' ? resolveBinary() : '');
|
|
78
|
+
} catch {
|
|
79
|
+
packageBinary = null;
|
|
80
|
+
}
|
|
81
|
+
if (packageBinary) {
|
|
82
|
+
binary = packageBinary;
|
|
83
|
+
binarySource = 'package';
|
|
84
|
+
} else {
|
|
85
|
+
binary = 'cloudflared';
|
|
86
|
+
binarySource = 'path';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const errors = [];
|
|
91
|
+
if (mode === 'managed') {
|
|
92
|
+
if (!token) errors.push('cloudflaredTunnel.token is required when cloudflaredTunnel.mode is "managed"');
|
|
93
|
+
if (!binary) errors.push('cloudflaredTunnel.binary could not be resolved (install the "cloudflared" package or set cloudflaredTunnel.binary)');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
mode,
|
|
98
|
+
enabled: mode === 'managed',
|
|
99
|
+
valid: errors.length === 0,
|
|
100
|
+
errors,
|
|
101
|
+
required,
|
|
102
|
+
token,
|
|
103
|
+
binary,
|
|
104
|
+
binarySource,
|
|
105
|
+
extraArgs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildCloudflaredArgs(tunnelConfig) {
|
|
110
|
+
return ['tunnel', 'run', '--token', tunnelConfig.token, ...tunnelConfig.extraArgs];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function redactCloudflaredArgs(args = []) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
116
|
+
if (args[i] === '--token') {
|
|
117
|
+
out.push('--token', '<redacted>');
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
out.push(args[i]);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildSpawnOptions(platform, stdio) {
|
|
127
|
+
if (platform === 'win32') return { stdio, windowsHide: true };
|
|
128
|
+
return { stdio };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRegistrationLine(text) {
|
|
132
|
+
return /registered tunnel connection|connection .* registered|registered connection/i.test(text);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createCloudflaredTunnelManager({
|
|
136
|
+
tunnelConfig: rawTunnelConfig = {},
|
|
137
|
+
runtimeLogPrefix = () => '',
|
|
138
|
+
io = null,
|
|
139
|
+
logger = console,
|
|
140
|
+
runtimeShutdownRef = () => false,
|
|
141
|
+
platform = process.platform,
|
|
142
|
+
spawnImpl = spawn,
|
|
143
|
+
nowIso = () => new Date().toISOString(),
|
|
144
|
+
setTimeoutImpl = setTimeout,
|
|
145
|
+
clearTimeoutImpl = clearTimeout,
|
|
146
|
+
configBaseDir = process.cwd(),
|
|
147
|
+
env = process.env,
|
|
148
|
+
resolveBinary = resolveCloudflaredBinaryFromPackage,
|
|
149
|
+
pathImpl = path,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const tunnelConfig = normalizeCloudflaredTunnelConfig(rawTunnelConfig, {
|
|
152
|
+
env,
|
|
153
|
+
resolveBinary,
|
|
154
|
+
configBaseDir,
|
|
155
|
+
pathImpl,
|
|
156
|
+
});
|
|
157
|
+
const log = (msg) => logger.log(`${runtimeLogPrefix()}[cloudflared-tunnel] ${msg}`);
|
|
158
|
+
const warn = (msg) => logger.warn(`${runtimeLogPrefix()}[cloudflared-tunnel] ${msg}`);
|
|
159
|
+
|
|
160
|
+
for (const error of tunnelConfig.errors) {
|
|
161
|
+
warn(error);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const state = {
|
|
165
|
+
mode: tunnelConfig.mode,
|
|
166
|
+
enabled: tunnelConfig.enabled && tunnelConfig.valid,
|
|
167
|
+
valid: tunnelConfig.valid,
|
|
168
|
+
required: tunnelConfig.required,
|
|
169
|
+
connected: false,
|
|
170
|
+
reconnectAttempts: 0,
|
|
171
|
+
fastExits: 0,
|
|
172
|
+
connectedSince: null,
|
|
173
|
+
blocking: tunnelConfig.required && tunnelConfig.mode === 'managed',
|
|
174
|
+
lastError: tunnelConfig.errors[0] || null,
|
|
175
|
+
lastEventAt: nowIso(),
|
|
176
|
+
binary: tunnelConfig.binary || null,
|
|
177
|
+
proc: null,
|
|
178
|
+
backoffTimer: null,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const emitStatus = () => {
|
|
182
|
+
state.lastEventAt = nowIso();
|
|
183
|
+
io?.emit?.('cloudflared_tunnel_status', {
|
|
184
|
+
connected: state.connected,
|
|
185
|
+
mode: state.mode,
|
|
186
|
+
enabled: state.enabled,
|
|
187
|
+
required: state.required,
|
|
188
|
+
blocking: state.blocking,
|
|
189
|
+
reconnectAttempts: state.reconnectAttempts,
|
|
190
|
+
connectedSince: state.connectedSince,
|
|
191
|
+
lastError: state.lastError,
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const updateBlockingState = () => {
|
|
196
|
+
state.blocking = state.required && state.mode === 'managed' && !state.connected;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const scheduleReconnect = (spawnTunnel, { slowest = false } = {}) => {
|
|
200
|
+
if (runtimeShutdownRef()) return;
|
|
201
|
+
if (state.backoffTimer) {
|
|
202
|
+
clearTimeoutImpl(state.backoffTimer);
|
|
203
|
+
state.backoffTimer = null;
|
|
204
|
+
}
|
|
205
|
+
const stepIndex = slowest
|
|
206
|
+
? BACKOFF_STEPS.length - 1
|
|
207
|
+
: Math.min(state.reconnectAttempts, BACKOFF_STEPS.length - 1);
|
|
208
|
+
const base = BACKOFF_STEPS[stepIndex];
|
|
209
|
+
const delay = Math.round(base + (Math.random() * base * 0.2));
|
|
210
|
+
state.reconnectAttempts += 1;
|
|
211
|
+
log(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${state.reconnectAttempts})...`);
|
|
212
|
+
state.backoffTimer = setTimeoutImpl(spawnTunnel, delay);
|
|
213
|
+
if (typeof state.backoffTimer?.unref === 'function') state.backoffTimer.unref();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const spawnTunnel = () => {
|
|
217
|
+
if (!state.enabled || runtimeShutdownRef()) return;
|
|
218
|
+
if (state.proc && state.proc.exitCode === null) {
|
|
219
|
+
log('Spawn skipped: existing cloudflared process is still running.');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const args = buildCloudflaredArgs(tunnelConfig);
|
|
224
|
+
log(`Spawning: ${tunnelConfig.binary} ${redactCloudflaredArgs(args).join(' ')}`);
|
|
225
|
+
const proc = spawnImpl(tunnelConfig.binary, args, buildSpawnOptions(platform, ['ignore', 'pipe', 'pipe']));
|
|
226
|
+
state.proc = proc;
|
|
227
|
+
state.lastError = null;
|
|
228
|
+
updateBlockingState();
|
|
229
|
+
emitStatus();
|
|
230
|
+
|
|
231
|
+
const startedAt = Date.now();
|
|
232
|
+
let readinessTimer = null;
|
|
233
|
+
|
|
234
|
+
const clearReadinessTimer = () => {
|
|
235
|
+
if (!readinessTimer) return;
|
|
236
|
+
clearTimeoutImpl(readinessTimer);
|
|
237
|
+
readinessTimer = null;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const markConnected = (reason) => {
|
|
241
|
+
if (state.connected) return;
|
|
242
|
+
state.connected = true;
|
|
243
|
+
state.connectedSince = nowIso();
|
|
244
|
+
state.lastError = null;
|
|
245
|
+
state.fastExits = 0;
|
|
246
|
+
updateBlockingState();
|
|
247
|
+
log(`Tunnel connected (${reason}).`);
|
|
248
|
+
emitStatus();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const armReadinessFallback = () => {
|
|
252
|
+
clearReadinessTimer();
|
|
253
|
+
readinessTimer = setTimeoutImpl(() => {
|
|
254
|
+
readinessTimer = null;
|
|
255
|
+
if (runtimeShutdownRef()) return;
|
|
256
|
+
if (state.proc !== proc) return;
|
|
257
|
+
if (proc.exitCode !== null) return;
|
|
258
|
+
markConnected('readiness-window');
|
|
259
|
+
}, READINESS_WINDOW_MS);
|
|
260
|
+
if (typeof readinessTimer?.unref === 'function') readinessTimer.unref();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const handleOutput = (text) => {
|
|
264
|
+
if (!text) return;
|
|
265
|
+
log(`stderr: ${text}`);
|
|
266
|
+
if (isRegistrationLine(text)) {
|
|
267
|
+
clearReadinessTimer();
|
|
268
|
+
markConnected('registration');
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
proc.stdout?.on?.('data', (d) => handleOutput(d.toString().trim()));
|
|
273
|
+
proc.stderr?.on?.('data', (d) => handleOutput(d.toString().trim()));
|
|
274
|
+
proc.on('spawn', armReadinessFallback);
|
|
275
|
+
proc.on('error', (error) => {
|
|
276
|
+
clearReadinessTimer();
|
|
277
|
+
state.lastError = error?.message || String(error);
|
|
278
|
+
updateBlockingState();
|
|
279
|
+
log(`Error: ${state.lastError}`);
|
|
280
|
+
emitStatus();
|
|
281
|
+
});
|
|
282
|
+
proc.on('close', (code) => {
|
|
283
|
+
clearReadinessTimer();
|
|
284
|
+
const wasConnected = state.connected;
|
|
285
|
+
const uptime = Date.now() - startedAt;
|
|
286
|
+
state.connected = false;
|
|
287
|
+
state.connectedSince = null;
|
|
288
|
+
state.proc = null;
|
|
289
|
+
if (wasConnected && uptime > STABLE_CONNECTION_MS) {
|
|
290
|
+
state.reconnectAttempts = 0;
|
|
291
|
+
}
|
|
292
|
+
updateBlockingState();
|
|
293
|
+
|
|
294
|
+
if (runtimeShutdownRef()) {
|
|
295
|
+
log(`Process exited (code=${code}) during shutdown.`);
|
|
296
|
+
emitStatus();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (uptime < FAST_EXIT_MS) {
|
|
301
|
+
state.fastExits += 1;
|
|
302
|
+
} else {
|
|
303
|
+
state.fastExits = 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (state.fastExits >= MAX_CONSECUTIVE_FAST_EXITS) {
|
|
307
|
+
state.lastError = 'auth-or-config';
|
|
308
|
+
log(`Process exited (code=${code}) after ${state.fastExits} fast exits; treating as auth/config failure.`);
|
|
309
|
+
emitStatus();
|
|
310
|
+
scheduleReconnect(spawnTunnel, { slowest: true });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
state.lastError = `exit:${code ?? 'null'}`;
|
|
315
|
+
log(`Process exited (code=${code}). Scheduling reconnect...`);
|
|
316
|
+
emitStatus();
|
|
317
|
+
scheduleReconnect(spawnTunnel);
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const start = () => {
|
|
322
|
+
if (state.mode === 'disabled') {
|
|
323
|
+
state.lastError = null;
|
|
324
|
+
state.blocking = false;
|
|
325
|
+
emitStatus();
|
|
326
|
+
log('Cloudflare tunnel mode disabled.');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!state.enabled) {
|
|
330
|
+
state.lastError = state.lastError || 'invalid-config';
|
|
331
|
+
updateBlockingState();
|
|
332
|
+
emitStatus();
|
|
333
|
+
log('Managed cloudflared mode requested but configuration is invalid; tunnel not started.');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
log(`Cloudflare tunnel enabled (binary source: ${tunnelConfig.binarySource}).`);
|
|
337
|
+
spawnTunnel();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const stop = () => {
|
|
341
|
+
if (state.backoffTimer) {
|
|
342
|
+
clearTimeoutImpl(state.backoffTimer);
|
|
343
|
+
state.backoffTimer = null;
|
|
344
|
+
}
|
|
345
|
+
if (state.proc) {
|
|
346
|
+
try { state.proc.kill('SIGTERM'); } catch {}
|
|
347
|
+
state.proc = null;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
state,
|
|
353
|
+
config: tunnelConfig,
|
|
354
|
+
start,
|
|
355
|
+
stop,
|
|
356
|
+
emitStatus,
|
|
357
|
+
};
|
|
358
|
+
}
|