total-recall-brain 3.47.3 → 3.48.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/metadata.plugin.schema.json +4 -0
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +7 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access.mjs +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/references/mesh.md +2 -0
- package/scaffold/.agent/skills/total-recall/references/plugins.md +2 -0
- package/src/core/plugin-contracts/manifest.mjs +2 -0
- package/src/core/plugin-runner.mjs +56 -15
|
@@ -151,6 +151,10 @@
|
|
|
151
151
|
"minimum": 1,
|
|
152
152
|
"maximum": 600000
|
|
153
153
|
},
|
|
154
|
+
"interactive": {
|
|
155
|
+
"type": "boolean",
|
|
156
|
+
"description": "The command is a terminal session (a remote shell, a sign-in). Run from the terminal CLI with stdin and stdout on a TTY, it is attached to the terminal with no runner timeout. Every other caller keeps piped output and the timeout."
|
|
157
|
+
},
|
|
154
158
|
"args": {
|
|
155
159
|
"type": "array",
|
|
156
160
|
"items": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.48.0",
|
|
4
4
|
"description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-plugin-mesh",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "tr-plugin-mesh",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@gregiteen/ssss-cli": "0.14.5",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"jsdom": "^29.1.1",
|
|
17
17
|
"supertest": "^7.3.1",
|
|
18
|
-
"total-recall-brain": "3.47.
|
|
18
|
+
"total-recall-brain": "3.47.4",
|
|
19
19
|
"vitest": "^4.1.12"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
@@ -7177,9 +7177,9 @@
|
|
|
7177
7177
|
}
|
|
7178
7178
|
},
|
|
7179
7179
|
"node_modules/total-recall-brain": {
|
|
7180
|
-
"version": "3.47.
|
|
7181
|
-
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.
|
|
7182
|
-
"integrity": "sha512-
|
|
7180
|
+
"version": "3.47.4",
|
|
7181
|
+
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.4.tgz",
|
|
7182
|
+
"integrity": "sha512-XqUOHFfxiiMVCE7J6nGJdZNrh2M9HXEhIA9ZiKdrGab+AebxMzN5C8mmGVM1Wo1we9szHVUyYr6w+FHOEvni+A==",
|
|
7183
7183
|
"dev": true,
|
|
7184
7184
|
"license": "MIT",
|
|
7185
7185
|
"workspaces": [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
|
|
3
3
|
"id": "mesh",
|
|
4
4
|
"name": "Headscale",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.2",
|
|
6
6
|
"description": "Control-server mesh administration, Tailscale SSH, and peer node discovery.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"reuse": {
|
|
@@ -166,8 +166,9 @@
|
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
168
|
"name": "ssh",
|
|
169
|
-
"description": "
|
|
170
|
-
"effect": "write"
|
|
169
|
+
"description": "Open a terminal session on a mesh node, or run a command there",
|
|
170
|
+
"effect": "write",
|
|
171
|
+
"interactive": true
|
|
171
172
|
},
|
|
172
173
|
{
|
|
173
174
|
"name": "exec",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mesh
|
|
3
3
|
description: "Control-server mesh administration, Tailscale SSH, and peer node discovery."
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
repo_scoped: false
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -22,8 +22,8 @@ total-recall mesh status # Inspect live mesh state
|
|
|
22
22
|
total-recall mesh view # Read independent owner observations for mesh screens
|
|
23
23
|
total-recall mesh nodes # Mesh nodes operation
|
|
24
24
|
total-recall mesh doctor # Mesh doctor operation
|
|
25
|
-
total-recall mesh ssh #
|
|
26
|
-
total-recall mesh exec #
|
|
25
|
+
total-recall mesh ssh <node> [cmd] # Terminal session on a node, no time limit when run from a terminal
|
|
26
|
+
total-recall mesh exec <node> <cmd> # Scripted command (bounded by the runner timeout)
|
|
27
27
|
total-recall mesh access # Mesh access operation
|
|
28
28
|
total-recall mesh ping # Mesh ping operation
|
|
29
29
|
total-recall mesh leader # Read the elected leader and its server port
|
|
@@ -31,6 +31,10 @@ total-recall mesh leader-pin # Change the pinned leader address and optional se
|
|
|
31
31
|
total-recall mesh enroll # Mesh enroll operation
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Terminal sessions
|
|
35
|
+
|
|
36
|
+
`ssh` is declared `interactive`. Run from a terminal (stdin and stdout on a TTY), Total Recall attaches it to that terminal with no runner timeout: window resizes and Ctrl-C reach ssh. A command given from a terminal gets a remote terminal (`ssh -t`), so `total-recall mesh ssh <node> 'claude setup-token'` can prompt. Piped or scripted, `ssh` runs under the runner timeout like any other command; use `exec` for scripted work.
|
|
37
|
+
|
|
34
38
|
## Owned Raw Commands
|
|
35
39
|
|
|
36
40
|
This plugin owns: `ssh\s`, `scp\s`, `tailscale\s+(status|ping)\b`.
|
|
@@ -402,8 +402,11 @@ export default async function meshCli(argv = []) {
|
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
// Run from a terminal, total-recall attaches this command to it (manifest
|
|
406
|
+
// `interactive`), so ssh reads and writes the person's terminal directly.
|
|
405
407
|
const sshArgs = buildSshArgs(resolved, {
|
|
406
408
|
command: args.length ? args.join(' ') : null,
|
|
409
|
+
terminal: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
407
410
|
});
|
|
408
411
|
const res = spawnSync('ssh', sshArgs, { stdio: 'inherit' });
|
|
409
412
|
process.exitCode = res.status ?? 1;
|
|
@@ -234,10 +234,13 @@ export function formatAccessTarget(resolved) {
|
|
|
234
234
|
* cannot silently offer a different key first and get the connection refused
|
|
235
235
|
* before the right key is ever tried.
|
|
236
236
|
*/
|
|
237
|
-
export function buildSshArgs(resolved, { command = null, extraOptions = [] } = {}) {
|
|
237
|
+
export function buildSshArgs(resolved, { command = null, extraOptions = [], terminal = false } = {}) {
|
|
238
238
|
if (!resolved?.complete) return null;
|
|
239
239
|
|
|
240
240
|
const args = [];
|
|
241
|
+
// ssh gives a remote command no terminal unless asked, so a sign-in run from a
|
|
242
|
+
// person's terminal (`mesh ssh node 'claude setup-token'`) could not prompt.
|
|
243
|
+
if (terminal && command) args.push('-t');
|
|
241
244
|
if (resolved.port && resolved.port !== DEFAULT_SSH_PORT) args.push('-p', String(resolved.port));
|
|
242
245
|
if (resolved.identity_file) {
|
|
243
246
|
args.push('-i', resolved.identity_file, '-o', 'IdentitiesOnly=yes');
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-plugin-meta-harness",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "tr-plugin-meta-harness",
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.4",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@gregiteen/ssss-cli": "0.14.5"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"total-recall-brain": "3.47.
|
|
14
|
+
"total-recall-brain": "3.47.3"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"node_modules/@alcalzone/ansi-tokenize": {
|
|
@@ -5546,9 +5546,9 @@
|
|
|
5546
5546
|
}
|
|
5547
5547
|
},
|
|
5548
5548
|
"node_modules/total-recall-brain": {
|
|
5549
|
-
"version": "3.47.
|
|
5550
|
-
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.
|
|
5551
|
-
"integrity": "sha512-
|
|
5549
|
+
"version": "3.47.3",
|
|
5550
|
+
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.3.tgz",
|
|
5551
|
+
"integrity": "sha512-F7QAR+Sj8fWb2h0a0zrgBSmsujE3P0i7lrt4Dj3aI8FjZf1tcEMmwM57BDDo7IIXZAEu3OSCDw9ggnbrOy4pYA==",
|
|
5552
5552
|
"dev": true,
|
|
5553
5553
|
"license": "MIT",
|
|
5554
5554
|
"workspaces": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "meta-harness",
|
|
3
3
|
"name": "Metaharness",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"description": "Discover, configure, document, track, and run coding agent harnesses through Total Recall across mesh nodes.",
|
|
6
6
|
"gate": {
|
|
7
7
|
"tasks": ["Run work in another agent harness", "Choose which harness takes a task", "Check harness versions, sign-in and quota", "Read a harness's official documentation"],
|
|
@@ -286,7 +286,12 @@ export async function routeTask(taskDescription, options = {}) {
|
|
|
286
286
|
...(remote || lean.fleet ? {} : { fleet: complexity.tier === 'local' ? cachedFleet : () => ({ ok: false, error: 'Not read: this route places no local work', nodes: [] }) }),
|
|
287
287
|
});
|
|
288
288
|
const quotas = quotaReport.quotas;
|
|
289
|
-
|
|
289
|
+
// No ready harness means none can take the task. The ranking reads an empty list as the whole
|
|
290
|
+
// catalog, so a node whose only ready harness was walled for quota chose an OpenRouter harness
|
|
291
|
+
// outside the included policy, with no credit behind it.
|
|
292
|
+
const rank = ready => (ready.length
|
|
293
|
+
? selectOptimalHarness(taskDescription, ready.map(h => h.id), quotas, { ...options, tierOverride: complexity.tier, empiricalScorecard: computeEmpiricalScorecard(receipts) })
|
|
294
|
+
: { selected: null, candidates: [] });
|
|
290
295
|
let excluded = pool.excluded;
|
|
291
296
|
let routing = rank(pool.ready);
|
|
292
297
|
// Quota walls come from launches this machine saw refused. With none recorded here, a route kept
|
|
@@ -7,6 +7,8 @@ Credential lookup, control-server API keys and mesh sync tokens use auth. Enroll
|
|
|
7
7
|
Presence accepts fresh authenticated observations. Unknown activity cannot imply an active user. No active target is an error for automatic dispatch. Commands succeed only after their actual execution result; notification acknowledgement is not proof the user viewed it. Test timeouts, stale records, denied peers, local execution and remote failures. Never copy/merge brains during leader changes. Significant mesh extraction and truthfulness work remains in progress until owner and integration suites pass.
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
`mesh ssh <node>` is declared `interactive`: from a terminal it is a real session with no time limit (window resizes and Ctrl-C reach ssh). `mesh ssh <node> '<command>'` from a terminal gets a remote terminal (`ssh -t`), so sign-ins such as `claude setup-token` work. Piped or scripted, it runs as before under the runner timeout; use `mesh exec` for scripted commands.
|
|
11
|
+
|
|
10
12
|
Host service adapters require the explicitly selected absolute TR_PACKAGE_ROOT; independently installed host packages are never execution substitutes. The mesh owner runtime suite passes 249 tests plus three Node manifest/host-boundary tests; these do not prove live mesh provider health.
|
|
11
13
|
|
|
12
14
|
## Developer-owned control server
|
|
@@ -8,6 +8,8 @@ Plugin UI is purpose-specific and belongs in its repository. Bind cards to actua
|
|
|
8
8
|
|
|
9
9
|
The bridge rejects undeclared commands even when a manifest declares no commands. Cards execute only explicitly declared read effects. Owner panels may submit one declared secret string with `host.run(command, args, { stdin: value })`; the authenticated server validates the declaration and 64 KiB UTF-8 bound, pipes it with `--stdin`, and suppresses child output. Workspace components resolve to owner UI element ids/tags; missing interfaces and unsupported device access stay unavailable. Live card regions mount owner panels. Full provider behavior and audio sessions still require separate end-to-end verification.
|
|
10
10
|
|
|
11
|
+
Plugin commands run in a child process with piped, bounded output and a timeout (`timeout_ms`, 1..600000; default 30 s). A subcommand that is a terminal session, such as a remote shell or a sign-in, declares `interactive: true`. When the terminal CLI runs it with stdin and stdout on a TTY, the child is attached to the terminal (all three streams, the terminal's foreground process group) with no timeout, and the CLI exits with its code. The server, scripts, piped calls and composable calls keep piped output and the timeout.
|
|
12
|
+
|
|
11
13
|
Configuration uses validated records/SSSS contracts. A manifest task schedule is not an active runner. Resolve node placement and daemon evidence; do not run recurring work on an arbitrary laptop. Verify tests/package provenance separately from install/runtime readiness. Keep the owner skill and this router's relevant reference synchronized whenever capabilities change.
|
|
12
14
|
|
|
13
15
|
|
|
@@ -53,6 +53,8 @@ export function validateManifestContract(manifest) {
|
|
|
53
53
|
// The runner refuses anything outside this range, so a manifest that
|
|
54
54
|
// declares it must fail validation rather than every call at run time.
|
|
55
55
|
if (sub.timeout_ms !== undefined && (!Number.isInteger(sub.timeout_ms) || sub.timeout_ms < 1 || sub.timeout_ms > 600000)) errors.push(`${where}.timeout_ms must be an integer from 1 to 600000`);
|
|
56
|
+
// Run from a terminal, an interactive command is attached to it with no timeout (plugin-runner.mjs).
|
|
57
|
+
if (sub.interactive !== undefined && typeof sub.interactive !== 'boolean') errors.push(`${where}.interactive must be a boolean`);
|
|
56
58
|
if (sub.args !== undefined) {
|
|
57
59
|
if (!Array.isArray(sub.args)) errors.push(`${where}.args must be an array`);
|
|
58
60
|
else {
|
|
@@ -24,6 +24,11 @@ export const CLI_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
|
24
24
|
export const MAX_INPUT_BYTES = 64 * 1024;
|
|
25
25
|
export const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
26
26
|
|
|
27
|
+
/** Both ends a person types into and reads from are a terminal. */
|
|
28
|
+
export function stdioIsTerminal() {
|
|
29
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
/** A credential is one declared string on stdin, never a command-line value. */
|
|
28
33
|
export function validatePluginSecretInput(plugin, subcommand, input) {
|
|
29
34
|
if (input === undefined) return;
|
|
@@ -39,8 +44,9 @@ export function validatePluginSecretInput(plugin, subcommand, input) {
|
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
46
|
* @param {object} plugin A discovered plugin ({ id, dir, manifest }).
|
|
42
|
-
* @param {{ subcommand?: string, args?: string[], cwd?: string, projectRoot?: string|null, timeoutMs?: number }} [options]
|
|
43
|
-
*
|
|
47
|
+
* @param {{ subcommand?: string, args?: string[], cwd?: string, projectRoot?: string|null, timeoutMs?: number, tty?: boolean }} [options]
|
|
48
|
+
* `tty` overrides terminal detection (stdin and stdout are TTYs) for an interactive subcommand.
|
|
49
|
+
* @returns {Promise<{ ok: boolean, exitCode: number|null, signal: string|null, timedOut: boolean, truncated: boolean, output: string, durationMs: number, interactive?: boolean }>}
|
|
44
50
|
*/
|
|
45
51
|
export function runPluginCommand(plugin, options = {}) {
|
|
46
52
|
const { subcommand = '', args = [], cwd = process.cwd(), timeoutMs: requestedTimeoutMs, stdin = 'ignore', stdinText, stdinRequest, composable = false, secretsBrainDir } = options;
|
|
@@ -95,6 +101,24 @@ export function runPluginCommand(plugin, options = {}) {
|
|
|
95
101
|
const childArgv = [CHILD, handler, command, ...(subcommand ? [String(subcommand)] : []), ...safeArgs];
|
|
96
102
|
// Only an explicitly selected project owns repository state. The cwd fallback above preserves legacy activation.
|
|
97
103
|
const statePluginsDir = options.projectRoot ? projectPluginsDir(projectRoot) : recordPluginsDir(plugin);
|
|
104
|
+
const env = {
|
|
105
|
+
...process.env,
|
|
106
|
+
TR_PACKAGE_ROOT: PACKAGE_ROOT,
|
|
107
|
+
TOTAL_RECALL_BIN: path.join(PACKAGE_ROOT, 'bin', 'total-recall.mjs'),
|
|
108
|
+
TR_PLUGIN_ID: plugin.id,
|
|
109
|
+
TR_PLUGIN_DIR: plugin.dir,
|
|
110
|
+
TR_PLUGIN_CONFIG: JSON.stringify(readPluginRecord(plugin)?.configuration || {}),
|
|
111
|
+
...(statePluginsDir ? { TR_PLUGIN_STATE_DIR: path.join(path.dirname(statePluginsDir), 'plugin-state', plugin.id) } : {}),
|
|
112
|
+
...(secretsBrainDir ? { TR_SECRETS_BRAIN: secretsBrainDir } : {}),
|
|
113
|
+
TR_PLUGIN_COMMAND_MODE: composable ? 'composable' : 'cli',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// An interactive subcommand (a remote shell, a sign-in) run by a person at a terminal is
|
|
117
|
+
// attached to that terminal: no pipes, no output buffer, no timer. Every other caller (the
|
|
118
|
+
// server, a script, a nested composable call, a stdin request) keeps the bounded path below.
|
|
119
|
+
const attach = declaration?.interactive === true && stdin === 'inherit' && stdinPayload === undefined &&
|
|
120
|
+
!composable && (options.tty ?? stdioIsTerminal());
|
|
121
|
+
if (attach) return runAttached(childArgv, { cwd, env });
|
|
98
122
|
|
|
99
123
|
return new Promise((resolve) => {
|
|
100
124
|
const started = Date.now();
|
|
@@ -108,19 +132,7 @@ export function runPluginCommand(plugin, options = {}) {
|
|
|
108
132
|
const child = spawn(process.execPath, childArgv, {
|
|
109
133
|
cwd,
|
|
110
134
|
detached: process.platform !== 'win32',
|
|
111
|
-
env: {
|
|
112
|
-
...process.env,
|
|
113
|
-
TR_PACKAGE_ROOT: PACKAGE_ROOT,
|
|
114
|
-
TOTAL_RECALL_BIN: path.join(PACKAGE_ROOT, 'bin', 'total-recall.mjs'),
|
|
115
|
-
TR_PLUGIN_ID: plugin.id,
|
|
116
|
-
TR_PLUGIN_DIR: plugin.dir,
|
|
117
|
-
TR_PLUGIN_CONFIG: JSON.stringify(readPluginRecord(plugin)?.configuration || {}),
|
|
118
|
-
...(statePluginsDir ? { TR_PLUGIN_STATE_DIR: path.join(path.dirname(statePluginsDir), 'plugin-state', plugin.id) } : {}),
|
|
119
|
-
...(secretsBrainDir ? { TR_SECRETS_BRAIN: secretsBrainDir } : {}),
|
|
120
|
-
TR_PLUGIN_COMMAND_MODE: composable ? 'composable' : 'cli',
|
|
121
|
-
FORCE_COLOR: '0',
|
|
122
|
-
NO_COLOR: '1'
|
|
123
|
-
},
|
|
135
|
+
env: { ...env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
124
136
|
stdio: [stdinPayload !== undefined ? 'pipe' : stdin === 'inherit' ? 'inherit' : 'ignore', 'pipe', 'pipe']
|
|
125
137
|
});
|
|
126
138
|
|
|
@@ -181,3 +193,32 @@ export function runPluginCommand(plugin, options = {}) {
|
|
|
181
193
|
child.on('close', (code, signal) => finish(code, signal));
|
|
182
194
|
});
|
|
183
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Run a handler on this process's own terminal. The child is not detached: it stays in the
|
|
199
|
+
* terminal's session and foreground process group, so it gets window resizes (SIGWINCH), Ctrl-C
|
|
200
|
+
* and the other terminal signals itself. Like a shell waiting on a job, this process ignores
|
|
201
|
+
* SIGINT and SIGQUIT until the child exits, then reports the child's exit code.
|
|
202
|
+
*/
|
|
203
|
+
function runAttached(childArgv, { cwd, env }) {
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
const started = Date.now();
|
|
206
|
+
const signals = process.platform === 'win32' ? ['SIGINT'] : ['SIGINT', 'SIGQUIT'];
|
|
207
|
+
const ignore = () => {};
|
|
208
|
+
for (const name of signals) process.on(name, ignore);
|
|
209
|
+
let settled = false;
|
|
210
|
+
const finish = (exitCode, signal, spawnError) => {
|
|
211
|
+
if (settled) return;
|
|
212
|
+
settled = true;
|
|
213
|
+
for (const name of signals) process.removeListener(name, ignore);
|
|
214
|
+
const message = spawnError ? `[runner] ${spawnError.message}` : '';
|
|
215
|
+
resolve({
|
|
216
|
+
ok: !spawnError && exitCode === 0, exitCode, signal, timedOut: false, truncated: false, interactive: true,
|
|
217
|
+
output: message, stdout: '', stderr: message, durationMs: Date.now() - started,
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
const child = spawn(process.execPath, childArgv, { cwd, env, stdio: 'inherit' });
|
|
221
|
+
child.on('error', (err) => finish(null, null, err));
|
|
222
|
+
child.on('close', (code, signal) => finish(code, signal));
|
|
223
|
+
});
|
|
224
|
+
}
|