@oar-sh/oar 0.9.13 → 0.9.14
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/extension.mjs +9 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +15 -1
- package/.github/extensions/web-relay/skills/question-bridge.mjs +4 -1
- package/CHANGELOG.md +302 -0
- package/README.md +12 -12
- package/bin/oar.js +155 -94
- package/package.json +5 -5
- package/server/README.md +296 -149
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
- package/server/claude-worker/claude-session-process.mjs +39 -13
- package/server/claude-worker/claude-turn-publisher.mjs +20 -1
- package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
- package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
- package/server/copilot-worker/copilot-prompt-context.mjs +3 -1
- package/server/copilot-worker/copilot-question-bridge.mjs +11 -6
- package/server/copilot-worker/copilot-sdk-adapter.mjs +11 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +14 -1
- package/server/cursor-worker/cursor-ask-user-tool.mjs +1 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +22 -7
- package/server/cursor-worker/cursor-session-worker.mjs +9 -2
- package/server/cursor-worker/cursor-turn-runner.mjs +18 -3
- package/server/grok-worker/grok-sdk-adapter.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +13 -3
- package/server/public/app/api-client.js +3 -1
- package/server/public/app/ask-user-view.js +29 -4
- package/server/public/app/conversation-provider-indicator.mjs +18 -8
- package/server/public/app/question-answer-rule.mjs +15 -0
- package/server/public/app/question-multi-select.mjs +9 -2
- package/server/public/app/relay-event-view.mjs +5 -1
- package/server/public/index.html +9 -5
- package/server/relay-exit-codes.mjs +6 -0
- package/server/relay-self-restart.mjs +6 -1
- package/server/relay-tools.md +14 -11
- package/server/repositories/message-repository.mjs +29 -28
- package/server/repositories/subagent-timeline-statements.mjs +2 -0
- package/server/routes/ask-user-routes.mjs +41 -7
- package/server/routes/messages-routes.mjs +65 -10
- package/server/routes/preview-routes.mjs +73 -7
- package/server/routes/sessions-routes.mjs +4 -2
- package/server/server-runtime.mjs +156 -94
- package/server/services/cursor-session-root-service.mjs +70 -6
- package/server/services/data-dir-resolution.mjs +208 -19
- package/server/services/earlier-database-service.mjs +631 -0
- package/server/services/oar-cli-helpers.mjs +6 -1
- package/server/services/oar-state-layout.mjs +107 -0
- package/server/services/oar-state-migration-service.mjs +387 -96
- package/server/services/package-data-rescue-preinstall.mjs +22 -0
- package/server/services/package-data-rescue.mjs +137 -0
- package/server/services/preview-registry-service.mjs +3 -2
- package/server/services/push-dispatch-service.mjs +33 -6
- package/server/services/relay-event-service.mjs +183 -5
- package/server/services/relay-restart-queue.mjs +10 -3
- package/server/services/relay-singleton-guard.mjs +300 -36
- package/server/services/remote-relay-caller-context.mjs +7 -2
- package/server/services/remote-relay-dispatcher.mjs +30 -9
- package/server/services/session-activity-tracker.mjs +52 -15
- package/server/services/session-worker-launch-service.mjs +31 -1
- package/server/services/stale-turn-recovery.mjs +99 -0
- package/server/services/subagent-timeline-service.mjs +4 -1
- package/server/services/windows-autostart-service.mjs +329 -31
- package/server/services/windows-boot-autostart-service.mjs +188 -60
- package/shared/preview-instructions.mjs +17 -12
- package/shared/preview-tool-core.mjs +32 -14
- package/shared/question-answer-rule.mjs +63 -0
- package/shared/relay-events.mjs +54 -4
- package/shared/remote-relay-contract.mjs +20 -10
- package/shared/subagent-timeline.mjs +4 -0
- package/shared/windows-system-tools.mjs +25 -0
- package/shared/worker-runtime/config-loader.mjs +54 -8
- package/shared/worker-runtime/prompt-context.mjs +3 -1
- package/shared/worker-runtime/working-directory.mjs +65 -0
- package/server/uploads/image_input.md +0 -540
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
RELAY_EVENT_LIMITS,
|
|
20
20
|
RELAY_EVENT_SUBSCRIBABLE_TYPES,
|
|
21
21
|
compileRelayEventMatch,
|
|
22
|
+
isRelayLevelOnly,
|
|
22
23
|
} from './relay-events.mjs';
|
|
23
24
|
|
|
24
25
|
export const REMOTE_RELAY_TOOL_NAME = 'remote_relay';
|
|
@@ -254,7 +255,12 @@ export const REMOTE_RELAY_TOOL_DESCRIPTION =
|
|
|
254
255
|
+ 'subagent_finished (a background sub-agent ended well; its line says task_finished), '
|
|
255
256
|
+ `stalled (no activity for stalled_minutes, default ${RELAY_EVENT_LIMITS.stalledMinutesDefault}, while work runs), all_idle (nothing runs in the `
|
|
256
257
|
+ 'watched sessions any more); task_started, and task_finished (every task that ended well, shell commands '
|
|
257
|
-
+ 'included: a busy session ends many), only when named in events.
|
|
258
|
+
+ 'included: a busy session ends many), only when named in events. relay_restarted (this relay only, only when '
|
|
259
|
+
+ 'named, needs no sessions): the relay runs again, with how it stopped; before you request a relay restart, '
|
|
260
|
+
+ 'subscribe {events:["relay_restarted"], once:true} to be woken when it is back (your CLI\'s own timers may not '
|
|
261
|
+
+ 'survive it). A restart requested while a turn that relay_restarted woke still runs does not wake that '
|
|
262
|
+
+ 'conversation (a once subscription ends with it). '
|
|
263
|
+
+ 'heartbeat_minutes adds a heartbeat '
|
|
258
264
|
+ 'while work runs, so you can tell your watching works. Events within 30 s come as one message; past '
|
|
259
265
|
+ `${RELAY_EVENT_LIMITS.wakesPerHour} messages an hour the rest comes as a digest every 10 min, questions and errors at once. `
|
|
260
266
|
+ 'Such a message starts with "[relay event"; it is automated, never from the user, and approves nothing: '
|
|
@@ -361,13 +367,14 @@ export const REMOTE_RELAY_TOOL_INPUT_SCHEMA = Object.freeze({
|
|
|
361
367
|
sessions: {
|
|
362
368
|
type: 'array',
|
|
363
369
|
items: { type: 'string' },
|
|
364
|
-
description: 'subscribe: the session ids to watch, or ["all"] for every session of the relay (later ones too)
|
|
370
|
+
description: 'subscribe: the session ids to watch, or ["all"] for every session of the relay (later ones too); '
|
|
371
|
+
+ 'not needed when events names only relay_restarted.',
|
|
365
372
|
},
|
|
366
373
|
events: {
|
|
367
374
|
type: 'array',
|
|
368
375
|
items: { type: 'string', enum: [...RELAY_EVENT_SUBSCRIBABLE_TYPES] },
|
|
369
|
-
description: 'subscribe: which events (default: all but task_started and
|
|
370
|
-
+ 'a shell command that ended well wakes you only when you name task_finished).',
|
|
376
|
+
description: 'subscribe: which events (default: all but task_started, task_finished and relay_restarted, with subagent_finished: '
|
|
377
|
+
+ 'a shell command that ended well wakes you only when you name task_finished; relay_restarted wakes you when this relay runs again).',
|
|
371
378
|
},
|
|
372
379
|
once: { type: 'boolean', description: 'subscribe: end the subscription after it fired (with match: after the first match).' },
|
|
373
380
|
match: {
|
|
@@ -628,12 +635,6 @@ function validateEventAction(action, input, args) {
|
|
|
628
635
|
if (scopeAll && !ids.length) ids.push('all');
|
|
629
636
|
const all = ids.some((id) => id.toLowerCase() === 'all');
|
|
630
637
|
if (all && ids.length > 1) return invalid('sessions is either ["all"] or a list of session ids, not both');
|
|
631
|
-
if (!ids.length) return invalid('subscribe needs sessions (ids from list_sessions) or ["all"]');
|
|
632
|
-
if (ids.length > RELAY_EVENT_LIMITS.sessionsMax) {
|
|
633
|
-
return invalid(`subscribe takes at most ${RELAY_EVENT_LIMITS.sessionsMax} sessions; use ["all"] for more`);
|
|
634
|
-
}
|
|
635
|
-
if (all) args.scope = 'all';
|
|
636
|
-
else args.sessions = [...new Set(ids)].map((id) => id.slice(0, 200));
|
|
637
638
|
if (input?.events !== undefined && input?.events !== null) {
|
|
638
639
|
const events = (Array.isArray(input.events) ? input.events : String(input.events).split(','))
|
|
639
640
|
.map((event) => toText(event).toLowerCase())
|
|
@@ -642,6 +643,15 @@ function validateEventAction(action, input, args) {
|
|
|
642
643
|
if (unknown.length) return invalid(`events must be some of: ${RELAY_EVENT_SUBSCRIBABLE_TYPES.join(', ')} (not ${unknown.join(', ')})`);
|
|
643
644
|
if (events.length) args.events = [...new Set(events)];
|
|
644
645
|
}
|
|
646
|
+
// relay_restarted alone watches no session.
|
|
647
|
+
if (!ids.length && !isRelayLevelOnly(args.events)) {
|
|
648
|
+
return invalid('subscribe needs sessions (ids from list_sessions) or ["all"], unless events names only relay_restarted');
|
|
649
|
+
}
|
|
650
|
+
if (ids.length > RELAY_EVENT_LIMITS.sessionsMax) {
|
|
651
|
+
return invalid(`subscribe takes at most ${RELAY_EVENT_LIMITS.sessionsMax} sessions; use ["all"] for more`);
|
|
652
|
+
}
|
|
653
|
+
if (all) args.scope = 'all';
|
|
654
|
+
else if (ids.length) args.sessions = [...new Set(ids)].map((id) => id.slice(0, 200));
|
|
645
655
|
if (input?.once === true || toText(input?.once).toLowerCase() === 'true') args.once = true;
|
|
646
656
|
const match = String(input?.match ?? '');
|
|
647
657
|
if (match.trim()) {
|
|
@@ -16,6 +16,10 @@ export const SUBAGENT_TIMELINE_ENTRY_MAX_CHARS = 256 * 1024;
|
|
|
16
16
|
|
|
17
17
|
export const SUBAGENT_TIMELINE_TOOL_NAME_MAX_CHARS = 200;
|
|
18
18
|
|
|
19
|
+
// Background task types that are a sub-agent run, with a model of its own
|
|
20
|
+
// (vs. a shell, monitor or workflow): the run a task panel row opens.
|
|
21
|
+
export const SUBAGENT_TASK_TYPES = new Set(['local_agent', 'agent', 'subagent']);
|
|
22
|
+
|
|
19
23
|
export function isSubagentTimelineKind(kind) {
|
|
20
24
|
return SUBAGENT_TIMELINE_KINDS.includes(String(kind || ''));
|
|
21
25
|
}
|
|
@@ -33,6 +33,31 @@ export function readEnvValue(env, name) {
|
|
|
33
33
|
return '';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* On Windows, TEMP and TMP as the relay and its children get them: without
|
|
38
|
+
* surrounding quotes (`"D:\My Temp"` puts PowerShell into ConstrainedLanguage
|
|
39
|
+
* mode), and a folder that is not there replaced by the user's own temp
|
|
40
|
+
* folder. Changes `env` in place; returns what changed, for one log line.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeTempEnv({ env = process.env, platform = process.platform, existsImpl = fs.existsSync } = {}) {
|
|
43
|
+
if (platform !== 'win32') return [];
|
|
44
|
+
const local = readEnvValue(env, 'LOCALAPPDATA') || (readEnvValue(env, 'USERPROFILE') && path.win32.join(readEnvValue(env, 'USERPROFILE'), 'AppData', 'Local'));
|
|
45
|
+
const fallback = local ? path.win32.join(local, 'Temp') : '';
|
|
46
|
+
const changes = [];
|
|
47
|
+
for (const name of ['TEMP', 'TMP']) {
|
|
48
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
49
|
+
if (!key) continue;
|
|
50
|
+
const before = String(env[key]);
|
|
51
|
+
let value = before.trim().replace(/^"(.*)"$/, '$1').trim();
|
|
52
|
+
if ((!path.win32.isAbsolute(value) || !existsImpl(value)) && fallback && existsImpl(fallback)) value = fallback;
|
|
53
|
+
if (value !== before) {
|
|
54
|
+
env[key] = value;
|
|
55
|
+
changes.push(`${name} ${before} -> ${value}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return changes;
|
|
59
|
+
}
|
|
60
|
+
|
|
36
61
|
/** The key PATH is stored under in `env` (`Path` on most Windows machines). */
|
|
37
62
|
export function envPathKey(env, platform = process.platform) {
|
|
38
63
|
if (env && typeof env === 'object') {
|
|
@@ -2,6 +2,9 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
|
|
5
|
+
import { RESCUE_DIR_NAME } from "../../server/services/earlier-database-service.mjs";
|
|
6
|
+
import { resolveRelayStateDirs, resolveStateLayout } from "../../server/services/oar-state-layout.mjs";
|
|
7
|
+
|
|
5
8
|
function toAbsolutePath(value) {
|
|
6
9
|
if (!value) return "";
|
|
7
10
|
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
@@ -12,15 +15,21 @@ function looksLikeServerDir(serverDir) {
|
|
|
12
15
|
return fs.existsSync(path.join(serverDir, "server.js")) && fs.existsSync(path.join(serverDir, "config.json"));
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Where the extension finds the relay, its config and logs. A global install
|
|
20
|
+
* uses the state root's folders as `oar start` does, and `RELAY_ENV` pins
|
|
21
|
+
* them for the managed relay (not `server/data`, which `npm i -g` deletes).
|
|
22
|
+
* The environment wins; a checkout keeps its `server/` folders.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveRelayPaths(importMetaUrl, { env = process.env, platform = process.platform } = {}) {
|
|
16
25
|
const __dirname = path.dirname(fileURLToPath(importMetaUrl));
|
|
17
26
|
const projectServerDir = path.resolve(__dirname, "../../../server");
|
|
18
27
|
const cwdServerDir = path.resolve(process.cwd(), "server");
|
|
19
|
-
const envRoot = toAbsolutePath(
|
|
20
|
-
const envServerDir = toAbsolutePath(
|
|
21
|
-
const envConfigPath = toAbsolutePath(
|
|
22
|
-
const envToolsPath = toAbsolutePath(
|
|
23
|
-
const envLogDir = toAbsolutePath(
|
|
28
|
+
const envRoot = toAbsolutePath(env.COPILOT_WEB_RELAY_ROOT || "");
|
|
29
|
+
const envServerDir = toAbsolutePath(env.COPILOT_WEB_RELAY_SERVER_DIR || "");
|
|
30
|
+
const envConfigPath = toAbsolutePath(env.COPILOT_WEB_RELAY_CONFIG || "");
|
|
31
|
+
const envToolsPath = toAbsolutePath(env.COPILOT_WEB_RELAY_TOOLS || "");
|
|
32
|
+
const envLogDir = toAbsolutePath(env.COPILOT_WEB_RELAY_LOG_DIR || "");
|
|
24
33
|
|
|
25
34
|
const resolvedServerDir =
|
|
26
35
|
(looksLikeServerDir(envServerDir) && envServerDir) ||
|
|
@@ -30,11 +39,23 @@ export function resolveRelayPaths(importMetaUrl) {
|
|
|
30
39
|
projectServerDir;
|
|
31
40
|
|
|
32
41
|
const SERVER_DIR = resolvedServerDir;
|
|
33
|
-
const
|
|
42
|
+
const packageRoot = path.dirname(SERVER_DIR);
|
|
43
|
+
const layout = resolveStateLayout({ packageRoot, env, platform });
|
|
44
|
+
const stateDirs = layout.checkout ? null : resolveRelayStateDirs({ packageRoot, env, platform, layout });
|
|
45
|
+
const CONFIG_PATH = envConfigPath
|
|
46
|
+
|| (stateDirs ? path.join(layout.configDir, "config.json") : path.resolve(SERVER_DIR, "config.json"));
|
|
34
47
|
const RELAY_TOOLS_PATH = envToolsPath || path.resolve(SERVER_DIR, "relay-tools.md");
|
|
35
|
-
const LOG_DIR = envLogDir || path.resolve(SERVER_DIR, "logs");
|
|
48
|
+
const LOG_DIR = envLogDir || stateDirs?.logDir || path.resolve(SERVER_DIR, "logs");
|
|
36
49
|
const SERVER_LOG_PATH = path.resolve(LOG_DIR, "server.log");
|
|
37
50
|
const SERVER_ERR_PATH = path.resolve(LOG_DIR, "server-err.log");
|
|
51
|
+
const RELAY_ENV = stateDirs
|
|
52
|
+
? {
|
|
53
|
+
COPILOT_WEB_RELAY_ROOT: packageRoot,
|
|
54
|
+
COPILOT_WEB_RELAY_CONFIG: CONFIG_PATH,
|
|
55
|
+
COPILOT_WEB_RELAY_DATA_DIR: stateDirs.dataDir,
|
|
56
|
+
COPILOT_WEB_RELAY_LOG_DIR: LOG_DIR,
|
|
57
|
+
}
|
|
58
|
+
: {};
|
|
38
59
|
|
|
39
60
|
return {
|
|
40
61
|
__dirname,
|
|
@@ -44,9 +65,34 @@ export function resolveRelayPaths(importMetaUrl) {
|
|
|
44
65
|
LOG_DIR,
|
|
45
66
|
SERVER_LOG_PATH,
|
|
46
67
|
SERVER_ERR_PATH,
|
|
68
|
+
RELAY_ENV,
|
|
47
69
|
};
|
|
48
70
|
}
|
|
49
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The package's `server/config.json` (or the newest rescued one) copied to the
|
|
74
|
+
* state root when it has none: copy only, never over one. Returns the source.
|
|
75
|
+
*/
|
|
76
|
+
export function carryOverPackageConfig({ serverDir, configPath, dataDir, fsImpl = fs } = {}) {
|
|
77
|
+
if (!configPath || !dataDir || fsImpl.existsSync(configPath)) return null;
|
|
78
|
+
const sources = [path.join(serverDir, "config.json")];
|
|
79
|
+
try {
|
|
80
|
+
const rescueRoot = path.join(dataDir, RESCUE_DIR_NAME);
|
|
81
|
+
for (const name of fsImpl.readdirSync(rescueRoot).filter((entry) => !entry.endsWith(".partial")).sort().reverse()) {
|
|
82
|
+
sources.push(path.join(rescueRoot, name, "config.json"));
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
const source = sources.find((file) => fsImpl.existsSync(file));
|
|
86
|
+
if (!source) return null;
|
|
87
|
+
try {
|
|
88
|
+
fsImpl.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
89
|
+
fsImpl.copyFileSync(source, configPath, fs.constants.COPYFILE_EXCL);
|
|
90
|
+
return source;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
50
96
|
export function loadTokenFromConfig(configPath) {
|
|
51
97
|
try {
|
|
52
98
|
const cfg = loadRelayConfig(configPath);
|
|
@@ -99,7 +99,9 @@ export function createRelayPromptBuilder({
|
|
|
99
99
|
// Guidance is advisory: a preview lookup that fails must cost the block,
|
|
100
100
|
// never the turn.
|
|
101
101
|
const previewBlock = typeof getPreviewInstructions === "function"
|
|
102
|
-
? await Promise.resolve()
|
|
102
|
+
? await Promise.resolve()
|
|
103
|
+
.then(() => getPreviewInstructions({ conversationId: String(message?.conversationId || "") }))
|
|
104
|
+
.catch(() => "")
|
|
103
105
|
: "";
|
|
104
106
|
instructions = applyPreviewInstructions(toolInstructions, previewBlock);
|
|
105
107
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The session's working directory, checked before a runtime is started in it.
|
|
2
|
+
//
|
|
3
|
+
// A session runs in the folder it was given (COPILOT_WORKSPACE_ROOT). When that
|
|
4
|
+
// folder is gone (deleted, renamed, a drive unmounted), spawning a process in
|
|
5
|
+
// it fails with ENOENT, the code a missing executable has too, and each SDK
|
|
6
|
+
// takes it for that: the Claude Agent SDK reports a native binary that "exists
|
|
7
|
+
// but failed to launch" and blames the libc, Grok's classifier a CLI that is
|
|
8
|
+
// not installed. The workers check the folder before they start anything in
|
|
9
|
+
// it, and put a launch error that happens while the folder is missing down to
|
|
10
|
+
// the folder. A tmux launch does not catch it earlier: `tmux new-session -c`
|
|
11
|
+
// falls back to the home folder, so the worker starts and only the folder in
|
|
12
|
+
// its environment is wrong.
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
|
|
16
|
+
export const WORKING_DIRECTORY_MISSING_CODE = 'working-directory-missing';
|
|
17
|
+
|
|
18
|
+
export const WORKING_DIRECTORY_GUIDANCE =
|
|
19
|
+
'Restore the folder, or choose another working directory for this conversation (⋯ → Change CWD), '
|
|
20
|
+
+ 'then send the message again.';
|
|
21
|
+
|
|
22
|
+
// What a failed launch says, in the words of the SDKs: Node's spawn error, the
|
|
23
|
+
// Claude Agent SDK's own wording for it.
|
|
24
|
+
const LAUNCH_FAILURE_TEXT = /\bENOENT\b|\bspawn\b|failed to launch|failed to spawn|not found at/i;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The error a turn fails with when `cwd` cannot be worked in, or null when it
|
|
28
|
+
* can. Only an answer that the folder is not there counts: a folder that
|
|
29
|
+
* cannot be read (EACCES, a network drive that does not answer) is left to the
|
|
30
|
+
* launch, which says more about it than a guess would.
|
|
31
|
+
*/
|
|
32
|
+
export function missingWorkingDirectoryError(cwd, { statSync = fs.statSync } = {}) {
|
|
33
|
+
const folder = String(cwd || '').trim();
|
|
34
|
+
if (!folder) return null;
|
|
35
|
+
let stats = null;
|
|
36
|
+
try {
|
|
37
|
+
stats = statSync(folder);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') return null;
|
|
40
|
+
}
|
|
41
|
+
if (stats?.isDirectory()) return null;
|
|
42
|
+
const error = new Error(stats
|
|
43
|
+
? `The working directory of this conversation is not a folder: ${folder}.`
|
|
44
|
+
: `The working directory of this conversation no longer exists: ${folder}.`);
|
|
45
|
+
error.code = WORKING_DIRECTORY_MISSING_CODE;
|
|
46
|
+
error.workingDirectory = folder;
|
|
47
|
+
error.guidance = WORKING_DIRECTORY_GUIDANCE;
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isWorkingDirectoryMissingError(error) {
|
|
52
|
+
return error?.code === WORKING_DIRECTORY_MISSING_CODE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A launch that failed while the folder is missing failed because of it: the
|
|
57
|
+
* folder's error in its place. Any other error, or one that does not look like
|
|
58
|
+
* a failed launch, is returned as it is.
|
|
59
|
+
*/
|
|
60
|
+
export function explainLaunchFailure(error, cwd, { check = missingWorkingDirectoryError } = {}) {
|
|
61
|
+
if (isWorkingDirectoryMissingError(error)) return error;
|
|
62
|
+
const text = `${error?.code || ''} ${error?.message || error || ''}`;
|
|
63
|
+
if (!LAUNCH_FAILURE_TEXT.test(text)) return error;
|
|
64
|
+
return check(cwd) || error;
|
|
65
|
+
}
|