@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
package/bin/oar.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
migrateStateToOarRoot,
|
|
13
|
+
resolveOarRoot,
|
|
14
|
+
} from '../server/services/oar-state-migration-service.mjs';
|
|
15
|
+
|
|
16
|
+
function resolvePackageRoot(metaUrl = import.meta.url) {
|
|
17
|
+
return path.resolve(path.dirname(fileURLToPath(metaUrl)), '..');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parsePort(argv = []) {
|
|
21
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const arg = String(args[index] || '');
|
|
24
|
+
if (arg === '--port' && args[index + 1]) {
|
|
25
|
+
const parsed = Number.parseInt(String(args[index + 1]), 10);
|
|
26
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) return parsed;
|
|
27
|
+
}
|
|
28
|
+
if (arg.startsWith('--port=')) {
|
|
29
|
+
const parsed = Number.parseInt(arg.slice('--port='.length), 10);
|
|
30
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) return parsed;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return 3333;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readRelayLock(lockPath) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return {
|
|
41
|
+
pid: Number.parseInt(String(parsed?.pid ?? ''), 10),
|
|
42
|
+
startedAt: typeof parsed?.startedAt === 'string' ? parsed.startedAt : null,
|
|
43
|
+
token: typeof parsed?.token === 'string' ? parsed.token : '',
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isProcessAlive(pid) {
|
|
51
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJsonFile(filePath) {
|
|
61
|
+
if (!filePath) return null;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeJsonFile(filePath, value) {
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function openAppendFileDescriptor(filePath) {
|
|
75
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
76
|
+
return fs.openSync(filePath, 'a');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pre-OAR managed config location — kept only as a migration source and as the
|
|
80
|
+
// config home for git checkouts, where nothing may change underfoot.
|
|
81
|
+
function getLegacyManagedConfigDir(env = process.env) {
|
|
82
|
+
if (process.platform === 'win32') {
|
|
83
|
+
return path.join(
|
|
84
|
+
env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
|
85
|
+
'copilot-remote',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return path.join(
|
|
89
|
+
env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
|
90
|
+
'copilot-remote',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isGitCheckout(packageRoot) {
|
|
95
|
+
try { return fs.existsSync(path.join(packageRoot, '.git')); } catch { return false; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Where launcher-managed state lives. A git checkout keeps every pre-OAR
|
|
100
|
+
* default (repo-local server/data, legacy managed config) so development and
|
|
101
|
+
* the two existing machines change nothing. A global install gets the OAR
|
|
102
|
+
* state root: config, data, logs all under ~/.oar (APPDATA\oar on Windows),
|
|
103
|
+
* which survives `npm i -g` updates.
|
|
104
|
+
*/
|
|
105
|
+
function resolveStateLayout({ packageRoot, env = process.env } = {}) {
|
|
106
|
+
if (isGitCheckout(packageRoot)) {
|
|
107
|
+
return { checkout: true, root: null, configDir: getLegacyManagedConfigDir(env), dataDir: null };
|
|
108
|
+
}
|
|
109
|
+
const root = resolveOarRoot(env);
|
|
110
|
+
return { checkout: false, root, configDir: root, dataDir: path.join(root, 'data') };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getLauncherLogDir(env = process.env, layout = null) {
|
|
114
|
+
const envLogDir = String(env.COPILOT_WEB_RELAY_LOG_DIR || '').trim();
|
|
115
|
+
if (envLogDir) return envLogDir;
|
|
116
|
+
if (layout && !layout.checkout) return path.join(layout.root, 'logs');
|
|
117
|
+
return path.join(getLegacyManagedConfigDir(env), 'logs');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getCopilotHomeDir(env = process.env) {
|
|
121
|
+
const override = String(env.COPILOT_CONFIG_HOME || '').trim();
|
|
122
|
+
if (override) return override;
|
|
123
|
+
if (process.platform === 'win32') {
|
|
124
|
+
const profile = String(env.USERPROFILE || '').trim();
|
|
125
|
+
return path.join(profile || os.homedir(), '.copilot');
|
|
126
|
+
}
|
|
127
|
+
return path.join(os.homedir(), '.copilot');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getCopilotUserExtensionsDir(env = process.env) {
|
|
131
|
+
return path.join(getCopilotHomeDir(env), 'extensions');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildGlobalExtensionWrapperSource(entryPath) {
|
|
135
|
+
const entryUrl = pathToFileURL(path.resolve(String(entryPath || ''))).href;
|
|
136
|
+
const packageRoot = path.resolve(path.dirname(String(entryPath || '')), '..', '..', '..');
|
|
137
|
+
return [
|
|
138
|
+
'// Auto-generated by oar to provide a stable user-global extension entrypoint.',
|
|
139
|
+
'// Do not edit manually; regenerate via `oar --install-extension`.',
|
|
140
|
+
'',
|
|
141
|
+
'import path from "path";',
|
|
142
|
+
'',
|
|
143
|
+
`const PACKAGE_ROOT = ${JSON.stringify(packageRoot)};`,
|
|
144
|
+
'const cwd = path.resolve(process.cwd());',
|
|
145
|
+
'const normalizedPackageRoot = path.resolve(PACKAGE_ROOT);',
|
|
146
|
+
'const runningInsidePackage = cwd === normalizedPackageRoot || cwd.startsWith(`${normalizedPackageRoot}${path.sep}`);',
|
|
147
|
+
'const forceGlobalExtension = process.env.COPILOT_WEB_RELAY_FORCE_GLOBAL_EXTENSION === "true";',
|
|
148
|
+
'',
|
|
149
|
+
'// Avoid double-loading when the project-local extension is already discoverable from the repo itself.',
|
|
150
|
+
'if (!runningInsidePackage || forceGlobalExtension) {',
|
|
151
|
+
` await import(${JSON.stringify(entryUrl)});`,
|
|
152
|
+
'}',
|
|
153
|
+
'',
|
|
154
|
+
].join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ensureGlobalExtensionWrapper({ packageRoot, env = process.env, logger = console } = {}) {
|
|
158
|
+
const sourceEntryPath = path.join(packageRoot, '.github', 'extensions', 'web-relay', 'extension.mjs');
|
|
159
|
+
if (!fs.existsSync(sourceEntryPath)) {
|
|
160
|
+
throw new Error(`missing extension entrypoint at ${sourceEntryPath}`);
|
|
161
|
+
}
|
|
162
|
+
const targetDir = path.join(getCopilotUserExtensionsDir(env), 'web-relay');
|
|
163
|
+
const targetFilePath = path.join(targetDir, 'extension.mjs');
|
|
164
|
+
const nextSource = buildGlobalExtensionWrapperSource(sourceEntryPath);
|
|
165
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
166
|
+
const currentSource = fs.existsSync(targetFilePath) ? String(fs.readFileSync(targetFilePath, 'utf8')) : '';
|
|
167
|
+
if (currentSource !== nextSource) {
|
|
168
|
+
fs.writeFileSync(targetFilePath, nextSource, 'utf8');
|
|
169
|
+
logger.log?.(`[oar] Updated user extension wrapper: ${targetFilePath}`);
|
|
170
|
+
} else {
|
|
171
|
+
logger.log?.(`[oar] User extension wrapper already current: ${targetFilePath}`);
|
|
172
|
+
}
|
|
173
|
+
return { targetDir, targetFilePath, sourceEntryPath };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createDefaultConfig({ port = 3333, token = '' } = {}) {
|
|
177
|
+
const parsedPort = Number.parseInt(String(port || ''), 10);
|
|
178
|
+
return {
|
|
179
|
+
authToken: String(token || '').trim() || randomUUID(),
|
|
180
|
+
port: Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : 3333,
|
|
181
|
+
localhostOnly: true,
|
|
182
|
+
pollIntervalMs: 3000,
|
|
183
|
+
conversationSessionMode: 'isolated',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveLauncherConfig({ packageRoot, env = process.env, relayToken = '', relayPort = 3333, layout = null } = {}) {
|
|
188
|
+
const envConfigPath = String(env.COPILOT_WEB_RELAY_CONFIG || '').trim();
|
|
189
|
+
const repoConfigPath = path.join(packageRoot, 'server', 'config.json');
|
|
190
|
+
const oarConfigPath = layout && !layout.checkout ? path.join(layout.configDir, 'config.json') : null;
|
|
191
|
+
const seedPath = (
|
|
192
|
+
(envConfigPath && fs.existsSync(envConfigPath) && envConfigPath)
|
|
193
|
+
|| (oarConfigPath && fs.existsSync(oarConfigPath) && oarConfigPath)
|
|
194
|
+
|| (fs.existsSync(repoConfigPath) && repoConfigPath)
|
|
195
|
+
|| null
|
|
196
|
+
);
|
|
197
|
+
const managedPath = oarConfigPath || path.join(getLegacyManagedConfigDir(env), 'config.json');
|
|
198
|
+
const configPath = seedPath || managedPath;
|
|
199
|
+
const seedConfig = readJsonFile(seedPath) || {};
|
|
200
|
+
const runtimeConfig = {
|
|
201
|
+
...createDefaultConfig({ port: relayPort }),
|
|
202
|
+
...seedConfig,
|
|
203
|
+
};
|
|
204
|
+
if (relayToken) runtimeConfig.authToken = relayToken;
|
|
205
|
+
if (!String(runtimeConfig.authToken || '').trim()) runtimeConfig.authToken = randomUUID();
|
|
206
|
+
const parsedPort = Number.parseInt(String(runtimeConfig.port || relayPort || 3333), 10);
|
|
207
|
+
runtimeConfig.port = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
|
208
|
+
? parsedPort
|
|
209
|
+
: 3333;
|
|
210
|
+
return {
|
|
211
|
+
configPath,
|
|
212
|
+
managed: !seedPath,
|
|
213
|
+
config: runtimeConfig,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function detectRunningRelay({
|
|
218
|
+
lockPath,
|
|
219
|
+
statusUrl,
|
|
220
|
+
fetchImpl = globalThis.fetch,
|
|
221
|
+
isProcessAliveImpl = isProcessAlive,
|
|
222
|
+
} = {}) {
|
|
223
|
+
const lock = readRelayLock(lockPath);
|
|
224
|
+
if (Number.isInteger(lock?.pid) && isProcessAliveImpl(lock.pid)) {
|
|
225
|
+
return { running: true, source: 'lock', lock };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof fetchImpl === 'function' && statusUrl) {
|
|
229
|
+
try {
|
|
230
|
+
const response = await fetchImpl(statusUrl, { method: 'GET' });
|
|
231
|
+
if (response) {
|
|
232
|
+
return { running: true, source: 'status', lock };
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Not running or unreachable.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { running: false, source: null, lock };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function launchRelay({
|
|
243
|
+
argv = process.argv.slice(2),
|
|
244
|
+
cwd = process.cwd(),
|
|
245
|
+
packageRoot = resolvePackageRoot(),
|
|
246
|
+
nodeBin = process.execPath,
|
|
247
|
+
spawnImpl = spawn,
|
|
248
|
+
fetchImpl = globalThis.fetch,
|
|
249
|
+
env = process.env,
|
|
250
|
+
logger = console,
|
|
251
|
+
} = {}) {
|
|
252
|
+
const args = Array.isArray(argv) ? [...argv] : [];
|
|
253
|
+
const installExtensionOnly = args.includes('--install-extension');
|
|
254
|
+
const skipExtensionInstall = args.includes('--no-install-extension');
|
|
255
|
+
const separatorIdx = args.indexOf('--');
|
|
256
|
+
const rawLauncherArgs = separatorIdx === -1 ? args : args.slice(0, separatorIdx);
|
|
257
|
+
const migrateFromIdx = rawLauncherArgs.indexOf('--migrate-from');
|
|
258
|
+
const migrateFromRoot = migrateFromIdx !== -1 ? String(rawLauncherArgs[migrateFromIdx + 1] || '').trim() : '';
|
|
259
|
+
const launcherArgs = rawLauncherArgs
|
|
260
|
+
.filter((arg, idx) => idx !== migrateFromIdx && idx !== migrateFromIdx + 1)
|
|
261
|
+
.filter((arg) => arg !== '--install-extension' && arg !== '--no-install-extension');
|
|
262
|
+
const forwardedArgs = separatorIdx === -1 ? [] : args.slice(separatorIdx + 1);
|
|
263
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
264
|
+
logger.log([
|
|
265
|
+
'Usage: oar [--port <port>] [--token <token>] [--migrate-from <old-checkout>] [--install-extension] [--no-install-extension] [-- [gh copilot args...]]',
|
|
266
|
+
'',
|
|
267
|
+
'Starts the web relay server if needed, then launches gh copilot in the current shell.',
|
|
268
|
+
'--install-extension installs/updates a user-global web-relay wrapper extension and exits.',
|
|
269
|
+
'--no-install-extension skips automatic wrapper setup for this run.',
|
|
270
|
+
'Install globally with: npm i -g @oar-sh/oar (or from a checkout: npm link)',
|
|
271
|
+
].join('\n'));
|
|
272
|
+
return { code: 0, reason: 'help' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!skipExtensionInstall || installExtensionOnly) {
|
|
276
|
+
try {
|
|
277
|
+
ensureGlobalExtensionWrapper({ packageRoot, env, logger });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.error?.(`[oar] Failed to prepare user extension wrapper: ${error?.message || error}`);
|
|
280
|
+
if (installExtensionOnly) return { code: 1, reason: 'extension-install-failed', error };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (installExtensionOnly) {
|
|
284
|
+
return { code: 0, reason: 'extension-installed' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const port = parsePort(launcherArgs);
|
|
288
|
+
const serverDir = path.join(packageRoot, 'server');
|
|
289
|
+
const layout = resolveStateLayout({ packageRoot, env });
|
|
290
|
+
|
|
291
|
+
if (!layout.checkout) {
|
|
292
|
+
// Global install: state lives in the OAR root. Migrate pre-OAR state in
|
|
293
|
+
// exactly once (marker-guarded); a live source relay blocks the migration.
|
|
294
|
+
try {
|
|
295
|
+
const migration = await migrateStateToOarRoot({
|
|
296
|
+
targetRoot: layout.root,
|
|
297
|
+
repoServerDir: migrateFromRoot ? path.join(path.resolve(migrateFromRoot), 'server') : null,
|
|
298
|
+
managedConfigDir: getLegacyManagedConfigDir(env),
|
|
299
|
+
logger,
|
|
300
|
+
});
|
|
301
|
+
if (migration.status === 'blocked-live-relay') {
|
|
302
|
+
logger.error?.(`[oar] ${migration.error}`);
|
|
303
|
+
return { code: 1, reason: 'migration-blocked' };
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.error?.(`[oar] State migration failed, nothing was changed at the source: ${error?.message || error}`);
|
|
307
|
+
return { code: 1, reason: 'migration-failed', error };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const lockPath = layout.checkout
|
|
312
|
+
? path.join(serverDir, 'data', 'relay-server.lock')
|
|
313
|
+
: path.join(layout.dataDir, 'relay-server.lock');
|
|
314
|
+
const statusUrl = `http://localhost:${port}/api/status`;
|
|
315
|
+
const running = await detectRunningRelay({
|
|
316
|
+
lockPath,
|
|
317
|
+
statusUrl,
|
|
318
|
+
fetchImpl,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const runtimeConfig = resolveLauncherConfig({
|
|
322
|
+
packageRoot,
|
|
323
|
+
env,
|
|
324
|
+
relayToken: running?.lock?.token || '',
|
|
325
|
+
relayPort: port,
|
|
326
|
+
layout,
|
|
327
|
+
});
|
|
328
|
+
if (runtimeConfig.managed || !fs.existsSync(runtimeConfig.configPath)) {
|
|
329
|
+
writeJsonFile(runtimeConfig.configPath, runtimeConfig.config);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const baseEnv = {
|
|
333
|
+
...env,
|
|
334
|
+
COPILOT_WORKSPACE_ROOT: cwd,
|
|
335
|
+
COPILOT_WEB_RELAY_ROOT: packageRoot,
|
|
336
|
+
COPILOT_WEB_RELAY_SERVER_DIR: serverDir,
|
|
337
|
+
COPILOT_WEB_RELAY_CONFIG: runtimeConfig.configPath,
|
|
338
|
+
COPILOT_WEB_RELAY_LOG_DIR: getLauncherLogDir(env, layout),
|
|
339
|
+
// Checkouts keep the repo-local server/data default; global installs point
|
|
340
|
+
// the server at the OAR root so updates cannot wipe the database.
|
|
341
|
+
...(layout.checkout ? {} : {
|
|
342
|
+
COPILOT_WEB_RELAY_DATA_DIR: String(env.COPILOT_WEB_RELAY_DATA_DIR || '').trim() || layout.dataDir,
|
|
343
|
+
}),
|
|
344
|
+
GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS: String(env.GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS || 'true'),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
let serverProc = null;
|
|
348
|
+
let relayStartedByThisCommand = false;
|
|
349
|
+
if (running.running) {
|
|
350
|
+
const pid = Number.isInteger(running?.lock?.pid) ? running.lock.pid : null;
|
|
351
|
+
logger.log(
|
|
352
|
+
pid
|
|
353
|
+
? `Copilot relay is already running (pid=${pid}) at ${statusUrl}.`
|
|
354
|
+
: `Copilot relay is already running at ${statusUrl}.`,
|
|
355
|
+
);
|
|
356
|
+
} else {
|
|
357
|
+
logger.log(`Starting Copilot relay from ${cwd}...`);
|
|
358
|
+
const serverArgs = [path.join(serverDir, 'server.js'), '--token', runtimeConfig.config.authToken, '--port', String(runtimeConfig.config.port)];
|
|
359
|
+
const logDir = getLauncherLogDir(baseEnv);
|
|
360
|
+
const serverLogPath = path.join(logDir, 'server.log');
|
|
361
|
+
const serverErrPath = path.join(logDir, 'server-err.log');
|
|
362
|
+
const serverOutFd = openAppendFileDescriptor(serverLogPath);
|
|
363
|
+
const serverErrFd = openAppendFileDescriptor(serverErrPath);
|
|
364
|
+
try {
|
|
365
|
+
serverProc = spawnImpl(nodeBin, serverArgs, {
|
|
366
|
+
cwd: packageRoot,
|
|
367
|
+
env: baseEnv,
|
|
368
|
+
stdio: ['ignore', serverOutFd, serverErrFd],
|
|
369
|
+
windowsHide: false,
|
|
370
|
+
});
|
|
371
|
+
relayStartedByThisCommand = true;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
try { fs.closeSync(serverOutFd); } catch {}
|
|
374
|
+
try { fs.closeSync(serverErrFd); } catch {}
|
|
375
|
+
logger.error?.(`[oar] Failed to launch relay: ${error?.message || error}`);
|
|
376
|
+
return { code: 1, reason: 'spawn-failed', error };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!(serverProc instanceof EventEmitter) && typeof serverProc?.on !== 'function') {
|
|
380
|
+
return { code: 1, reason: 'spawn-invalid-child' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const readyDeadlineMs = 20_000;
|
|
384
|
+
const ready = await new Promise((resolve) => {
|
|
385
|
+
const deadline = Date.now() + readyDeadlineMs;
|
|
386
|
+
const probe = async () => {
|
|
387
|
+
if (serverProc && serverProc.exitCode !== null) {
|
|
388
|
+
resolve(false);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetchImpl(statusUrl, {
|
|
393
|
+
headers: { Authorization: `Bearer ${runtimeConfig.config.authToken}` },
|
|
394
|
+
});
|
|
395
|
+
if (res && res.ok) {
|
|
396
|
+
resolve(true);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
// retry
|
|
401
|
+
}
|
|
402
|
+
if (Date.now() >= deadline) {
|
|
403
|
+
resolve(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
setTimeout(probe, 300);
|
|
407
|
+
};
|
|
408
|
+
probe();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!ready) {
|
|
412
|
+
logger.error?.(`[oar] Relay did not become ready at ${statusUrl}`);
|
|
413
|
+
try {
|
|
414
|
+
if (serverProc && typeof serverProc.kill === 'function') serverProc.kill();
|
|
415
|
+
} catch {}
|
|
416
|
+
try { fs.closeSync(serverOutFd); } catch {}
|
|
417
|
+
try { fs.closeSync(serverErrFd); } catch {}
|
|
418
|
+
return { code: 1, reason: 'server-not-ready' };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
serverProc.once('exit', () => {
|
|
422
|
+
try { fs.closeSync(serverOutFd); } catch {}
|
|
423
|
+
try { fs.closeSync(serverErrFd); } catch {}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const ghArgs = forwardedArgs.length
|
|
428
|
+
? ['copilot', '--', ...forwardedArgs]
|
|
429
|
+
: ['copilot'];
|
|
430
|
+
logger.log(`Launching Copilot CLI from ${cwd}...`);
|
|
431
|
+
|
|
432
|
+
let child;
|
|
433
|
+
try {
|
|
434
|
+
child = spawnImpl('gh', ghArgs, {
|
|
435
|
+
cwd,
|
|
436
|
+
env: baseEnv,
|
|
437
|
+
stdio: 'inherit',
|
|
438
|
+
windowsHide: false,
|
|
439
|
+
});
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger.error?.(`[oar] Failed to launch Copilot CLI: ${error?.message || error}`);
|
|
442
|
+
try {
|
|
443
|
+
if (relayStartedByThisCommand && serverProc && typeof serverProc.kill === 'function') serverProc.kill();
|
|
444
|
+
} catch {}
|
|
445
|
+
return { code: 1, reason: 'spawn-failed', error };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!(child instanceof EventEmitter) && typeof child?.on !== 'function') {
|
|
449
|
+
try {
|
|
450
|
+
if (relayStartedByThisCommand && serverProc && typeof serverProc.kill === 'function') serverProc.kill();
|
|
451
|
+
} catch {}
|
|
452
|
+
return { code: 1, reason: 'spawn-invalid-child' };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const exitCode = await new Promise((resolve) => {
|
|
456
|
+
child.on('exit', (code, signal) => {
|
|
457
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
458
|
+
});
|
|
459
|
+
child.on('error', (error) => {
|
|
460
|
+
logger.error?.(`[oar] Copilot CLI process error: ${error?.message || error}`);
|
|
461
|
+
resolve(1);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (relayStartedByThisCommand && serverProc && serverProc.exitCode === null) {
|
|
466
|
+
try {
|
|
467
|
+
serverProc.kill();
|
|
468
|
+
} catch {}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { code: exitCode, reason: 'exited' };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function readPackageVersion(packageRoot = resolvePackageRoot()) {
|
|
475
|
+
try {
|
|
476
|
+
return String(JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version || '0.0.0');
|
|
477
|
+
} catch {
|
|
478
|
+
return '0.0.0';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function promptYesNo(question, { defaultYes = false, interactive = true } = {}) {
|
|
483
|
+
if (!interactive) return defaultYes;
|
|
484
|
+
const readline = await import('node:readline/promises');
|
|
485
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
486
|
+
try {
|
|
487
|
+
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
|
|
488
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
489
|
+
if (!answer) return defaultYes;
|
|
490
|
+
return answer === 'y' || answer === 'yes';
|
|
491
|
+
} finally {
|
|
492
|
+
rl.close();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function runSetup({ argv = [], env = process.env, logger = console } = {}) {
|
|
497
|
+
const {
|
|
498
|
+
buildDefaultConfig, buildSystemdUnit, generateAuthToken, primaryLanAddress, relayUrl,
|
|
499
|
+
} = await import('../server/services/oar-cli-helpers.mjs');
|
|
500
|
+
|
|
501
|
+
const packageRoot = resolvePackageRoot();
|
|
502
|
+
const layout = resolveStateLayout({ packageRoot, env });
|
|
503
|
+
const useDefaults = argv.includes('--defaults') || !process.stdin.isTTY;
|
|
504
|
+
const migrateFromIdx = argv.indexOf('--migrate-from');
|
|
505
|
+
const migrateFromRoot = migrateFromIdx !== -1 ? String(argv[migrateFromIdx + 1] || '').trim() : '';
|
|
506
|
+
|
|
507
|
+
if (!layout.checkout) {
|
|
508
|
+
const migration = await migrateStateToOarRoot({
|
|
509
|
+
targetRoot: layout.root,
|
|
510
|
+
repoServerDir: migrateFromRoot ? path.join(path.resolve(migrateFromRoot), 'server') : null,
|
|
511
|
+
managedConfigDir: getLegacyManagedConfigDir(env),
|
|
512
|
+
logger,
|
|
513
|
+
});
|
|
514
|
+
if (migration.status === 'blocked-live-relay') {
|
|
515
|
+
logger.error?.(`[oar] ${migration.error}`);
|
|
516
|
+
return { code: 1, reason: 'migration-blocked' };
|
|
517
|
+
}
|
|
518
|
+
if (migration.status === 'migrated') {
|
|
519
|
+
logger.log(`[oar] Migrated existing relay state into ${layout.root} (sources untouched).`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const configPath = layout.checkout
|
|
524
|
+
? path.join(packageRoot, 'server', 'config.json')
|
|
525
|
+
: path.join(layout.configDir, 'config.json');
|
|
526
|
+
const existing = readJsonFile(configPath);
|
|
527
|
+
|
|
528
|
+
let token = String(existing?.authToken || '').trim();
|
|
529
|
+
if (token) {
|
|
530
|
+
const regenerate = await promptYesNo('A config already exists. Generate a new auth token (logs every device out)?', {
|
|
531
|
+
defaultYes: false,
|
|
532
|
+
interactive: !useDefaults,
|
|
533
|
+
});
|
|
534
|
+
if (regenerate) token = generateAuthToken();
|
|
535
|
+
} else {
|
|
536
|
+
token = generateAuthToken();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const lanAccess = existing
|
|
540
|
+
? existing.localhostOnly === false
|
|
541
|
+
: await promptYesNo('Allow LAN access so your phone can connect directly (otherwise localhost + tunnel only)?', {
|
|
542
|
+
defaultYes: false,
|
|
543
|
+
interactive: !useDefaults,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const config = { ...buildDefaultConfig({ token }), ...(existing || {}) };
|
|
547
|
+
config.authToken = token;
|
|
548
|
+
config.localhostOnly = !lanAccess;
|
|
549
|
+
|
|
550
|
+
if (await promptYesNo('Enable the managed cloudflared tunnel (public URL for remote access)?', {
|
|
551
|
+
defaultYes: false,
|
|
552
|
+
interactive: !useDefaults,
|
|
553
|
+
})) {
|
|
554
|
+
config.cloudflaredTunnel = { ...(config.cloudflaredTunnel || {}), enabled: true };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
558
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
559
|
+
if (process.platform !== 'win32') {
|
|
560
|
+
try { fs.chmodSync(configPath, 0o600); } catch {}
|
|
561
|
+
}
|
|
562
|
+
logger.log(`[oar] Wrote ${configPath}`);
|
|
563
|
+
|
|
564
|
+
if (process.platform === 'linux' && !layout.checkout) {
|
|
565
|
+
if (await promptYesNo('Install a systemd user service so the relay starts on login?', {
|
|
566
|
+
defaultYes: false,
|
|
567
|
+
interactive: !useDefaults,
|
|
568
|
+
})) {
|
|
569
|
+
const unitDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
570
|
+
const unitPath = path.join(unitDir, 'oar.service');
|
|
571
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
572
|
+
fs.writeFileSync(unitPath, buildSystemdUnit({
|
|
573
|
+
nodeBin: process.execPath,
|
|
574
|
+
packageRoot,
|
|
575
|
+
configPath,
|
|
576
|
+
dataDir: layout.dataDir,
|
|
577
|
+
logDir: getLauncherLogDir(env, layout),
|
|
578
|
+
}));
|
|
579
|
+
logger.log(`[oar] Wrote ${unitPath} — enable it with: systemctl --user enable --now oar`);
|
|
580
|
+
}
|
|
581
|
+
} else if (process.platform === 'win32') {
|
|
582
|
+
logger.log('[oar] Autostart on Windows: enable it in the web UI under Settings → Autostart.');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const url = relayUrl({ config, lanAddress: primaryLanAddress() });
|
|
586
|
+
logger.log('');
|
|
587
|
+
logger.log(`[oar] Relay URL: ${url}`);
|
|
588
|
+
if (config.localhostOnly) {
|
|
589
|
+
logger.log('[oar] localhostOnly is on — reach it from other devices via the tunnel, or rerun `oar setup` for LAN access.');
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
const qrcode = (await import('qrcode-terminal')).default;
|
|
593
|
+
qrcode.generate(url, { small: true }, (qr) => logger.log(qr));
|
|
594
|
+
} catch {
|
|
595
|
+
// QR is a convenience — the URL above is the contract.
|
|
596
|
+
}
|
|
597
|
+
logger.log('[oar] Start the relay with: oar');
|
|
598
|
+
return { code: 0, reason: 'setup-complete' };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function runDoctor({ env = process.env, logger = console } = {}) {
|
|
602
|
+
const { DOCTOR_PROBES, renderDoctorReport } = await import('../server/services/oar-cli-helpers.mjs');
|
|
603
|
+
const { spawnSync } = await import('node:child_process');
|
|
604
|
+
|
|
605
|
+
const packageRoot = resolvePackageRoot();
|
|
606
|
+
const layout = resolveStateLayout({ packageRoot, env });
|
|
607
|
+
const configPath = layout.checkout
|
|
608
|
+
? path.join(packageRoot, 'server', 'config.json')
|
|
609
|
+
: path.join(layout.configDir, 'config.json');
|
|
610
|
+
const dbPath = layout.checkout
|
|
611
|
+
? path.join(packageRoot, 'server', 'data', 'copilot.db')
|
|
612
|
+
: path.join(layout.dataDir, 'copilot.db');
|
|
613
|
+
|
|
614
|
+
let dbSizeBytes = null;
|
|
615
|
+
try { dbSizeBytes = fs.statSync(dbPath).size; } catch {}
|
|
616
|
+
|
|
617
|
+
const probes = DOCTOR_PROBES.map((probe) => {
|
|
618
|
+
try {
|
|
619
|
+
const result = spawnSync(probe.binary, probe.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
620
|
+
const ok = result.status === 0;
|
|
621
|
+
const version = ok ? String(result.stdout || '').trim().split('\n')[0].slice(0, 60) : '';
|
|
622
|
+
return { id: probe.id, ok, version };
|
|
623
|
+
} catch {
|
|
624
|
+
return { id: probe.id, ok: false };
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
probes.push({ id: 'cursor', ok: true, version: 'bundled (@cursor/sdk)' });
|
|
628
|
+
|
|
629
|
+
logger.log(renderDoctorReport({
|
|
630
|
+
version: readPackageVersion(packageRoot),
|
|
631
|
+
nodeVersion: process.version,
|
|
632
|
+
platform: process.platform,
|
|
633
|
+
layout,
|
|
634
|
+
configPath,
|
|
635
|
+
config: readJsonFile(configPath),
|
|
636
|
+
dbPath,
|
|
637
|
+
dbSizeBytes,
|
|
638
|
+
probes,
|
|
639
|
+
}));
|
|
640
|
+
return { code: 0, reason: 'doctor' };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function main() {
|
|
644
|
+
const argv = process.argv.slice(2);
|
|
645
|
+
const command = String(argv[0] || '').trim();
|
|
646
|
+
if (command === '--version' || command === '-v') {
|
|
647
|
+
console.log(readPackageVersion());
|
|
648
|
+
return Promise.resolve({ code: 0, reason: 'version' });
|
|
649
|
+
}
|
|
650
|
+
if (command === 'setup') return runSetup({ argv: argv.slice(1) });
|
|
651
|
+
if (command === 'doctor') return runDoctor({});
|
|
652
|
+
return launchRelay();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const executedName = process.argv[1] ? path.basename(String(process.argv[1])) : '';
|
|
656
|
+
if (executedName === 'oar.js') {
|
|
657
|
+
main().then(({ code }) => {
|
|
658
|
+
process.exit(code ?? 0);
|
|
659
|
+
}).catch((error) => {
|
|
660
|
+
console.error(`[oar] Unhandled error: ${error?.message || error}`);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export { main, parsePort, readRelayLock, resolvePackageRoot };
|