evolcore 0.0.25 → 0.0.26
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/CHANGELOG.md +607 -565
- package/LICENSE +21 -21
- package/README.md +455 -455
- package/bin/codex-managed-acl.ps1 +160 -160
- package/bin/codex-managed-hook.mjs +208 -208
- package/bin/ec-safe-output.js +327 -327
- package/bin/ec.js +40 -29
- package/bin/install-codex-managed-hooks.mjs +582 -555
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +50 -12
- package/dist/agents/codex/app-server-client.js +13 -5
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +121 -38
- package/dist/agents/ecagent-runner.js +4 -9
- package/dist/agents/gemini-runner.js +2 -7
- package/dist/agents/runner-types.js +25 -17
- package/dist/aun/aid/agentmd.js +81 -8
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +17 -13
- package/dist/aun/menu-client.js +74 -0
- package/dist/aun/msg/group.js +1 -1
- package/dist/aun/msg/managed-operation.js +1 -1
- package/dist/aun/msg/p2p.js +6 -1
- package/dist/aun/msg/upload.js +30 -22
- package/dist/channels/aun.js +329 -94
- package/dist/cli/agent-command.js +105 -84
- package/dist/cli/agent-create.js +112 -10
- package/dist/cli/agent.js +16 -5
- package/dist/cli/aun-commands.js +297 -296
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +47 -28
- package/dist/cli/contact.js +70 -13
- package/dist/cli/ctl-command.js +22 -22
- package/dist/cli/daemon-commands.js +105 -40
- package/dist/cli/diag.js +31 -0
- package/dist/cli/fs-command.js +67 -67
- package/dist/cli/handoff-command.js +41 -9
- package/dist/cli/index.js +83 -77
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +46 -31
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +29 -29
- package/dist/cli/response.js +30 -30
- package/dist/cli/restart-monitor.js +87 -40
- package/dist/cli/stats.js +51 -51
- package/dist/cli/trigger-command.js +56 -56
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-operation-service.js +145 -0
- package/dist/config/contact-operation-service.js +96 -7
- package/dist/config/resolved-config-op.js +27 -7
- package/dist/core/agent-application-service.js +2 -1
- package/dist/core/auth/authorization-audit.js +20 -12
- package/dist/core/auth/operation-authorizer.js +25 -4
- package/dist/core/auth/operation-catalog.js +35 -4
- package/dist/core/auth/remote-access-store.js +40 -1
- package/dist/core/bootstrap-service.js +1 -1
- package/dist/core/command/command-handler.js +83 -26
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/menu-catalog.js +12 -3
- package/dist/core/command/menu-handler.js +47 -26
- package/dist/core/command/menu-protocol.js +5 -0
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +86 -21
- package/dist/core/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/message/im-renderer.js +72 -21
- package/dist/core/message/message-bridge.js +69 -9
- package/dist/core/message/message-queue.js +26 -0
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +47 -28
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/ec-command-parser.js +51 -12
- package/dist/core/permission/sandbox-runtime.js +36 -14
- package/dist/core/permission/tool-policy.js +2 -1
- package/dist/index.js +280 -163
- package/dist/ipc.js +54 -2
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/price-resolver.js +28 -10
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- package/dist/utils/cross-platform.js +30 -1
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +21 -0
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +3 -1
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -23
- package/dist/utils/npm-ops.js +176 -35
- package/dist/utils/welcome.js +22 -22
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +66 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +56 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +364 -364
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +71 -71
- package/kits/docs/evolcore/agent-create.md +170 -165
- package/kits/docs/evolcore/agent.md +66 -66
- package/kits/docs/evolcore/aid.md +60 -60
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
- package/kits/docs/evolcore/config.md +202 -195
- package/kits/docs/evolcore/contact.md +75 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +243 -243
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +152 -152
- package/kits/docs/evolcore/menu-client.md +42 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +166 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +50 -50
- package/kits/docs/evolcore/trigger.md +575 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +269 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +37 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +190 -190
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +162 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +36 -36
- package/kits/rules/04-relation.md +75 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +63 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +427 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +201 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +92 -92
- package/kits/templates/roles/member.json +96 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +85 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +31 -29
- package/kits/templates/system-fragments/channel.md +64 -64
- package/kits/templates/system-fragments/commands.md +28 -28
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +100 -98
package/bin/ec-safe-output.js
CHANGED
|
@@ -1,327 +1,327 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
8
|
-
const EC_EXECUTABLES = new Set(['ec']);
|
|
9
|
-
const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
|
|
10
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
11
|
-
const HELPER_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
// A rooted POSIX path is not absolute on Windows: Node resolves `/usr/bin/head`
|
|
13
|
-
// onto the current drive as `C:\usr\bin\head`. Keep these fixed candidates as
|
|
14
|
-
// the trusted fast path where they are genuinely absolute, and discover the
|
|
15
|
-
// executable through PATH everywhere else.
|
|
16
|
-
const POSIX_HEAD_CANDIDATES = ['/usr/bin/head', '/bin/head'];
|
|
17
|
-
|
|
18
|
-
function isRegularFile(candidate) {
|
|
19
|
-
try {
|
|
20
|
-
return fs.statSync(candidate).isFile();
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function pathExtensionCandidates() {
|
|
27
|
-
if (!IS_WINDOWS) return [''];
|
|
28
|
-
const configured = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
29
|
-
.split(';')
|
|
30
|
-
.map(value => value.trim())
|
|
31
|
-
.filter(Boolean);
|
|
32
|
-
// The extensionless name is tried first so an explicitly named file wins over
|
|
33
|
-
// a same-named shim; npm publishes both `ec` (sh) and `ec.cmd` on Windows.
|
|
34
|
-
return ['', ...configured];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Resolve one executable to an absolute path without invoking a shell.
|
|
39
|
-
* `spawn` performs no PATH or PATHEXT lookup on Windows, which is why a bare
|
|
40
|
-
* command name has to be materialized here before the child is started.
|
|
41
|
-
*/
|
|
42
|
-
function resolveExecutablePath(executable) {
|
|
43
|
-
if (executable.includes('/') || executable.includes('\\')) {
|
|
44
|
-
return isRegularFile(executable) ? path.resolve(executable) : undefined;
|
|
45
|
-
}
|
|
46
|
-
const entries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
for (const extension of pathExtensionCandidates()) {
|
|
49
|
-
const candidate = path.join(entry, `${executable}${extension}`);
|
|
50
|
-
if (isRegularFile(candidate)) return path.resolve(candidate);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function resolveHeadExecutable() {
|
|
57
|
-
if (!IS_WINDOWS) {
|
|
58
|
-
const fixed = POSIX_HEAD_CANDIDATES.find(isRegularFile);
|
|
59
|
-
if (fixed) return fixed;
|
|
60
|
-
}
|
|
61
|
-
return resolveExecutablePath('head');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const HEAD_EXECUTABLE = resolveHeadExecutable();
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Resolve the concrete process for a validated payload.
|
|
68
|
-
*
|
|
69
|
-
* For `ec`, prefer this package's own Node entrypoint next to the helper. It is
|
|
70
|
-
* the same installation that published this file, needs no PATH lookup, and
|
|
71
|
-
* avoids the Windows batch shim entirely — `CreateProcess` cannot execute a
|
|
72
|
-
* `.cmd` wrapper, so `spawn('ec')` fails with ENOENT there.
|
|
73
|
-
*/
|
|
74
|
-
function resolveChildProcessCommand(payload) {
|
|
75
|
-
if (payload.kind === 'ec') {
|
|
76
|
-
const nodeEntrypoint = path.join(HELPER_DIR, 'ec.js');
|
|
77
|
-
// On Windows CreateProcess cannot execute the npm `.cmd` shim reliably,
|
|
78
|
-
// so prefer this package's Node entrypoint. POSIX hosts can resolve the
|
|
79
|
-
// trusted `ec` executable normally; this also preserves caller-provided
|
|
80
|
-
// PATH shims used by isolated runtimes and tests.
|
|
81
|
-
if (IS_WINDOWS && isRegularFile(nodeEntrypoint)) {
|
|
82
|
-
return { file: process.execPath, args: [nodeEntrypoint, ...payload.argv.slice(1)] };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const resolved = resolveExecutablePath(payload.argv[0]);
|
|
86
|
-
if (!resolved) fail(`unable to resolve executable: ${payload.argv[0]}`);
|
|
87
|
-
// Quoting rules for batch files differ from CreateProcess argument parsing,
|
|
88
|
-
// so a `.cmd`/`.bat` target could execute something other than this argv.
|
|
89
|
-
if (IS_WINDOWS && /\.(?:cmd|bat)$/i.test(resolved)) {
|
|
90
|
-
fail(`refusing to run a batch wrapper: ${resolved}`);
|
|
91
|
-
}
|
|
92
|
-
return { file: resolved, args: payload.argv.slice(1) };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function fail(message) {
|
|
96
|
-
process.stderr.write(`ec-safe-output: ${message}\n`);
|
|
97
|
-
process.exit(2);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function sameOrDescendant(candidate, parent) {
|
|
101
|
-
const relative = path.relative(parent, candidate);
|
|
102
|
-
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function decodePayload(encoded) {
|
|
106
|
-
try {
|
|
107
|
-
const value = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
108
|
-
if (!value || value.v !== 1 || !Array.isArray(value.argv) || value.argv.some(arg => typeof arg !== 'string')) return null;
|
|
109
|
-
return value;
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function validateCommand(payload) {
|
|
116
|
-
const executable = path.basename(payload.argv[0] ?? '').toLowerCase();
|
|
117
|
-
if (payload.kind === 'ec') return EC_EXECUTABLES.has(executable);
|
|
118
|
-
if (payload.kind !== 'host-process' || !HOST_PROCESS_EXECUTABLES.has(executable)) return false;
|
|
119
|
-
return !payload.argv.some(arg => /\bnode(?:js|\.exe)?\b/i.test(arg));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function validateStdinOnlyHeadArgs(args) {
|
|
123
|
-
if (args === undefined) return true;
|
|
124
|
-
if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return false;
|
|
125
|
-
let optionsEnded = false;
|
|
126
|
-
let expectsOptionValue = false;
|
|
127
|
-
for (const arg of args) {
|
|
128
|
-
if (expectsOptionValue) {
|
|
129
|
-
expectsOptionValue = false;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (optionsEnded) {
|
|
133
|
-
if (arg !== '-') return false;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (arg === '--') {
|
|
137
|
-
optionsEnded = true;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (arg === '-') continue;
|
|
141
|
-
if (!arg.startsWith('-')) return false;
|
|
142
|
-
if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
|
|
143
|
-
expectsOptionValue = true;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function resolveExecutionPaths(payload) {
|
|
150
|
-
if (typeof payload.workspace !== 'string' || typeof payload.cwd !== 'string') fail('invalid workspace');
|
|
151
|
-
let workspace;
|
|
152
|
-
let cwd;
|
|
153
|
-
try {
|
|
154
|
-
workspace = fs.realpathSync(payload.workspace);
|
|
155
|
-
cwd = fs.realpathSync(payload.cwd);
|
|
156
|
-
} catch {
|
|
157
|
-
fail('workspace or cwd does not exist');
|
|
158
|
-
}
|
|
159
|
-
if (!sameOrDescendant(cwd, workspace)) fail('cwd escapes workspace');
|
|
160
|
-
return { workspace, cwd };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function resolveManagedTempRoot() {
|
|
164
|
-
const raw = typeof process.env.TMPDIR === 'string' ? process.env.TMPDIR.trim() : '';
|
|
165
|
-
if (!raw || !path.isAbsolute(raw)) fail('managed TMPDIR is unavailable');
|
|
166
|
-
try {
|
|
167
|
-
const root = fs.realpathSync(raw);
|
|
168
|
-
if (!fs.statSync(root).isDirectory()) fail('managed TMPDIR is not a directory');
|
|
169
|
-
return root;
|
|
170
|
-
} catch {
|
|
171
|
-
fail('managed TMPDIR is unavailable');
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function openOutputFile(payload, paths) {
|
|
176
|
-
if (payload.outputPath === undefined) return undefined;
|
|
177
|
-
if (typeof payload.outputPath !== 'string' || !payload.outputPath) fail('output path is invalid');
|
|
178
|
-
|
|
179
|
-
const isManagedTempOutput = path.isAbsolute(payload.outputPath);
|
|
180
|
-
const outputRoot = isManagedTempOutput ? resolveManagedTempRoot() : paths.workspace;
|
|
181
|
-
const target = isManagedTempOutput
|
|
182
|
-
? path.resolve(payload.outputPath)
|
|
183
|
-
: path.resolve(paths.cwd, payload.outputPath);
|
|
184
|
-
const parent = path.dirname(target);
|
|
185
|
-
let realParent;
|
|
186
|
-
try {
|
|
187
|
-
realParent = fs.realpathSync(parent);
|
|
188
|
-
if (!fs.statSync(realParent).isDirectory()) fail('output parent is not a directory');
|
|
189
|
-
} catch {
|
|
190
|
-
fail('output parent does not exist');
|
|
191
|
-
}
|
|
192
|
-
const canonicalTarget = path.join(realParent, path.basename(target));
|
|
193
|
-
if (!sameOrDescendant(canonicalTarget, outputRoot)) {
|
|
194
|
-
fail(isManagedTempOutput ? 'output path escapes managed TMPDIR' : 'output path escapes workspace');
|
|
195
|
-
}
|
|
196
|
-
if (!isManagedTempOutput) {
|
|
197
|
-
const relative = path.relative(paths.workspace, canonicalTarget);
|
|
198
|
-
if (relative === '.git' || relative.startsWith(`.git${path.sep}`)) fail('output path may not target Git metadata');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
202
|
-
try {
|
|
203
|
-
const fd = fs.openSync(
|
|
204
|
-
canonicalTarget,
|
|
205
|
-
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
|
|
206
|
-
0o600,
|
|
207
|
-
);
|
|
208
|
-
if (!fs.fstatSync(fd).isFile()) {
|
|
209
|
-
fs.closeSync(fd);
|
|
210
|
-
fail('output target is not a regular file');
|
|
211
|
-
}
|
|
212
|
-
return { fd, target: canonicalTarget };
|
|
213
|
-
} catch (error) {
|
|
214
|
-
fail(error?.code === 'EEXIST' ? 'output file already exists' : 'unable to create output file');
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const payload = decodePayload(process.argv[2]);
|
|
219
|
-
if (!payload) fail('invalid payload');
|
|
220
|
-
if (!validateCommand(payload)) fail('command is not permitted');
|
|
221
|
-
if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
|
|
222
|
-
if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
|
|
223
|
-
|
|
224
|
-
// Resolve the child executable before anything with a side effect: an
|
|
225
|
-
// unresolvable command must not leave an empty create-only output file behind.
|
|
226
|
-
const childCommand = resolveChildProcessCommand(payload);
|
|
227
|
-
const paths = resolveExecutionPaths(payload);
|
|
228
|
-
const output = openOutputFile(payload, paths);
|
|
229
|
-
let bytesWritten = 0;
|
|
230
|
-
let limitReached = false;
|
|
231
|
-
let writeFailed = false;
|
|
232
|
-
|
|
233
|
-
function writeLimited(chunk) {
|
|
234
|
-
if (limitReached || writeFailed) return;
|
|
235
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
236
|
-
const allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
|
|
237
|
-
if (allowed <= 0) {
|
|
238
|
-
limitReached = true;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const slice = buffer.subarray(0, allowed);
|
|
242
|
-
try {
|
|
243
|
-
if (output) fs.writeSync(output.fd, slice);
|
|
244
|
-
else process.stdout.write(slice);
|
|
245
|
-
bytesWritten += slice.length;
|
|
246
|
-
if (bytesWritten >= MAX_OUTPUT_BYTES) limitReached = true;
|
|
247
|
-
} catch {
|
|
248
|
-
writeFailed = true;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const head = payload.headArgs === undefined
|
|
253
|
-
? undefined
|
|
254
|
-
: spawn(HEAD_EXECUTABLE, payload.headArgs, {
|
|
255
|
-
cwd: paths.cwd,
|
|
256
|
-
env: process.env,
|
|
257
|
-
stdio: ['pipe', 'pipe', 'inherit'],
|
|
258
|
-
});
|
|
259
|
-
if (head) {
|
|
260
|
-
head.stdout.on('data', writeLimited);
|
|
261
|
-
// Early-exit options such as --help or a small -n/-c close stdin before the
|
|
262
|
-
// EC process finishes. Ignore EPIPE and keep draining EC output below.
|
|
263
|
-
head.stdin.on('error', () => {});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const child = spawn(childCommand.file, childCommand.args, {
|
|
267
|
-
cwd: paths.cwd,
|
|
268
|
-
env: process.env,
|
|
269
|
-
stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
|
|
270
|
-
windowsHide: IS_WINDOWS,
|
|
271
|
-
});
|
|
272
|
-
const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
|
|
273
|
-
if (head) {
|
|
274
|
-
for (const stream of sourceStreams) {
|
|
275
|
-
// The no-op data listener keeps draining the source if head exits early.
|
|
276
|
-
stream.on('data', () => {});
|
|
277
|
-
stream.pipe(head.stdin, { end: false });
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
for (const stream of sourceStreams) stream.on('data', writeLimited);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
child.once('error', error => {
|
|
284
|
-
if (output) {
|
|
285
|
-
try { fs.closeSync(output.fd); } catch {}
|
|
286
|
-
try { fs.unlinkSync(output.target); } catch {}
|
|
287
|
-
}
|
|
288
|
-
fail(`unable to start command: ${error.message}`);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
let sourceClosed = false;
|
|
292
|
-
let sourceCode = 1;
|
|
293
|
-
let headClosed = !head;
|
|
294
|
-
let headCode = 1;
|
|
295
|
-
|
|
296
|
-
function finish() {
|
|
297
|
-
if (!sourceClosed || !headClosed) return;
|
|
298
|
-
if (output) {
|
|
299
|
-
try { fs.closeSync(output.fd); } catch { writeFailed = true; }
|
|
300
|
-
}
|
|
301
|
-
if (writeFailed) {
|
|
302
|
-
if (output) try { fs.unlinkSync(output.target); } catch {}
|
|
303
|
-
fail('failed to write command output');
|
|
304
|
-
}
|
|
305
|
-
if (output) process.stderr.write(`Saved output to ${output.target}\n`);
|
|
306
|
-
process.exitCode = head ? headCode : sourceCode;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (head) {
|
|
310
|
-
head.once('error', error => {
|
|
311
|
-
writeFailed = true;
|
|
312
|
-
process.stderr.write(`ec-safe-output: unable to start head: ${error.message}\n`);
|
|
313
|
-
});
|
|
314
|
-
head.once('close', code => {
|
|
315
|
-
headCode = code ?? 1;
|
|
316
|
-
headClosed = true;
|
|
317
|
-
for (const stream of sourceStreams) stream.unpipe(head.stdin);
|
|
318
|
-
finish();
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
child.once('close', code => {
|
|
323
|
-
sourceCode = code ?? 1;
|
|
324
|
-
sourceClosed = true;
|
|
325
|
-
if (head && !head.stdin.destroyed) head.stdin.end();
|
|
326
|
-
finish();
|
|
327
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
8
|
+
const EC_EXECUTABLES = new Set(['ec']);
|
|
9
|
+
const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
|
|
10
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
11
|
+
const HELPER_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// A rooted POSIX path is not absolute on Windows: Node resolves `/usr/bin/head`
|
|
13
|
+
// onto the current drive as `C:\usr\bin\head`. Keep these fixed candidates as
|
|
14
|
+
// the trusted fast path where they are genuinely absolute, and discover the
|
|
15
|
+
// executable through PATH everywhere else.
|
|
16
|
+
const POSIX_HEAD_CANDIDATES = ['/usr/bin/head', '/bin/head'];
|
|
17
|
+
|
|
18
|
+
function isRegularFile(candidate) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.statSync(candidate).isFile();
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pathExtensionCandidates() {
|
|
27
|
+
if (!IS_WINDOWS) return [''];
|
|
28
|
+
const configured = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
29
|
+
.split(';')
|
|
30
|
+
.map(value => value.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
// The extensionless name is tried first so an explicitly named file wins over
|
|
33
|
+
// a same-named shim; npm publishes both `ec` (sh) and `ec.cmd` on Windows.
|
|
34
|
+
return ['', ...configured];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve one executable to an absolute path without invoking a shell.
|
|
39
|
+
* `spawn` performs no PATH or PATHEXT lookup on Windows, which is why a bare
|
|
40
|
+
* command name has to be materialized here before the child is started.
|
|
41
|
+
*/
|
|
42
|
+
function resolveExecutablePath(executable) {
|
|
43
|
+
if (executable.includes('/') || executable.includes('\\')) {
|
|
44
|
+
return isRegularFile(executable) ? path.resolve(executable) : undefined;
|
|
45
|
+
}
|
|
46
|
+
const entries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
for (const extension of pathExtensionCandidates()) {
|
|
49
|
+
const candidate = path.join(entry, `${executable}${extension}`);
|
|
50
|
+
if (isRegularFile(candidate)) return path.resolve(candidate);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveHeadExecutable() {
|
|
57
|
+
if (!IS_WINDOWS) {
|
|
58
|
+
const fixed = POSIX_HEAD_CANDIDATES.find(isRegularFile);
|
|
59
|
+
if (fixed) return fixed;
|
|
60
|
+
}
|
|
61
|
+
return resolveExecutablePath('head');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const HEAD_EXECUTABLE = resolveHeadExecutable();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the concrete process for a validated payload.
|
|
68
|
+
*
|
|
69
|
+
* For `ec`, prefer this package's own Node entrypoint next to the helper. It is
|
|
70
|
+
* the same installation that published this file, needs no PATH lookup, and
|
|
71
|
+
* avoids the Windows batch shim entirely — `CreateProcess` cannot execute a
|
|
72
|
+
* `.cmd` wrapper, so `spawn('ec')` fails with ENOENT there.
|
|
73
|
+
*/
|
|
74
|
+
function resolveChildProcessCommand(payload) {
|
|
75
|
+
if (payload.kind === 'ec') {
|
|
76
|
+
const nodeEntrypoint = path.join(HELPER_DIR, 'ec.js');
|
|
77
|
+
// On Windows CreateProcess cannot execute the npm `.cmd` shim reliably,
|
|
78
|
+
// so prefer this package's Node entrypoint. POSIX hosts can resolve the
|
|
79
|
+
// trusted `ec` executable normally; this also preserves caller-provided
|
|
80
|
+
// PATH shims used by isolated runtimes and tests.
|
|
81
|
+
if (IS_WINDOWS && isRegularFile(nodeEntrypoint)) {
|
|
82
|
+
return { file: process.execPath, args: [nodeEntrypoint, ...payload.argv.slice(1)] };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const resolved = resolveExecutablePath(payload.argv[0]);
|
|
86
|
+
if (!resolved) fail(`unable to resolve executable: ${payload.argv[0]}`);
|
|
87
|
+
// Quoting rules for batch files differ from CreateProcess argument parsing,
|
|
88
|
+
// so a `.cmd`/`.bat` target could execute something other than this argv.
|
|
89
|
+
if (IS_WINDOWS && /\.(?:cmd|bat)$/i.test(resolved)) {
|
|
90
|
+
fail(`refusing to run a batch wrapper: ${resolved}`);
|
|
91
|
+
}
|
|
92
|
+
return { file: resolved, args: payload.argv.slice(1) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fail(message) {
|
|
96
|
+
process.stderr.write(`ec-safe-output: ${message}\n`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sameOrDescendant(candidate, parent) {
|
|
101
|
+
const relative = path.relative(parent, candidate);
|
|
102
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decodePayload(encoded) {
|
|
106
|
+
try {
|
|
107
|
+
const value = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
108
|
+
if (!value || value.v !== 1 || !Array.isArray(value.argv) || value.argv.some(arg => typeof arg !== 'string')) return null;
|
|
109
|
+
return value;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateCommand(payload) {
|
|
116
|
+
const executable = path.basename(payload.argv[0] ?? '').toLowerCase();
|
|
117
|
+
if (payload.kind === 'ec') return EC_EXECUTABLES.has(executable);
|
|
118
|
+
if (payload.kind !== 'host-process' || !HOST_PROCESS_EXECUTABLES.has(executable)) return false;
|
|
119
|
+
return !payload.argv.some(arg => /\bnode(?:js|\.exe)?\b/i.test(arg));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateStdinOnlyHeadArgs(args) {
|
|
123
|
+
if (args === undefined) return true;
|
|
124
|
+
if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return false;
|
|
125
|
+
let optionsEnded = false;
|
|
126
|
+
let expectsOptionValue = false;
|
|
127
|
+
for (const arg of args) {
|
|
128
|
+
if (expectsOptionValue) {
|
|
129
|
+
expectsOptionValue = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (optionsEnded) {
|
|
133
|
+
if (arg !== '-') return false;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (arg === '--') {
|
|
137
|
+
optionsEnded = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === '-') continue;
|
|
141
|
+
if (!arg.startsWith('-')) return false;
|
|
142
|
+
if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
|
|
143
|
+
expectsOptionValue = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveExecutionPaths(payload) {
|
|
150
|
+
if (typeof payload.workspace !== 'string' || typeof payload.cwd !== 'string') fail('invalid workspace');
|
|
151
|
+
let workspace;
|
|
152
|
+
let cwd;
|
|
153
|
+
try {
|
|
154
|
+
workspace = fs.realpathSync(payload.workspace);
|
|
155
|
+
cwd = fs.realpathSync(payload.cwd);
|
|
156
|
+
} catch {
|
|
157
|
+
fail('workspace or cwd does not exist');
|
|
158
|
+
}
|
|
159
|
+
if (!sameOrDescendant(cwd, workspace)) fail('cwd escapes workspace');
|
|
160
|
+
return { workspace, cwd };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveManagedTempRoot() {
|
|
164
|
+
const raw = typeof process.env.TMPDIR === 'string' ? process.env.TMPDIR.trim() : '';
|
|
165
|
+
if (!raw || !path.isAbsolute(raw)) fail('managed TMPDIR is unavailable');
|
|
166
|
+
try {
|
|
167
|
+
const root = fs.realpathSync(raw);
|
|
168
|
+
if (!fs.statSync(root).isDirectory()) fail('managed TMPDIR is not a directory');
|
|
169
|
+
return root;
|
|
170
|
+
} catch {
|
|
171
|
+
fail('managed TMPDIR is unavailable');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function openOutputFile(payload, paths) {
|
|
176
|
+
if (payload.outputPath === undefined) return undefined;
|
|
177
|
+
if (typeof payload.outputPath !== 'string' || !payload.outputPath) fail('output path is invalid');
|
|
178
|
+
|
|
179
|
+
const isManagedTempOutput = path.isAbsolute(payload.outputPath);
|
|
180
|
+
const outputRoot = isManagedTempOutput ? resolveManagedTempRoot() : paths.workspace;
|
|
181
|
+
const target = isManagedTempOutput
|
|
182
|
+
? path.resolve(payload.outputPath)
|
|
183
|
+
: path.resolve(paths.cwd, payload.outputPath);
|
|
184
|
+
const parent = path.dirname(target);
|
|
185
|
+
let realParent;
|
|
186
|
+
try {
|
|
187
|
+
realParent = fs.realpathSync(parent);
|
|
188
|
+
if (!fs.statSync(realParent).isDirectory()) fail('output parent is not a directory');
|
|
189
|
+
} catch {
|
|
190
|
+
fail('output parent does not exist');
|
|
191
|
+
}
|
|
192
|
+
const canonicalTarget = path.join(realParent, path.basename(target));
|
|
193
|
+
if (!sameOrDescendant(canonicalTarget, outputRoot)) {
|
|
194
|
+
fail(isManagedTempOutput ? 'output path escapes managed TMPDIR' : 'output path escapes workspace');
|
|
195
|
+
}
|
|
196
|
+
if (!isManagedTempOutput) {
|
|
197
|
+
const relative = path.relative(paths.workspace, canonicalTarget);
|
|
198
|
+
if (relative === '.git' || relative.startsWith(`.git${path.sep}`)) fail('output path may not target Git metadata');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
202
|
+
try {
|
|
203
|
+
const fd = fs.openSync(
|
|
204
|
+
canonicalTarget,
|
|
205
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
|
|
206
|
+
0o600,
|
|
207
|
+
);
|
|
208
|
+
if (!fs.fstatSync(fd).isFile()) {
|
|
209
|
+
fs.closeSync(fd);
|
|
210
|
+
fail('output target is not a regular file');
|
|
211
|
+
}
|
|
212
|
+
return { fd, target: canonicalTarget };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
fail(error?.code === 'EEXIST' ? 'output file already exists' : 'unable to create output file');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const payload = decodePayload(process.argv[2]);
|
|
219
|
+
if (!payload) fail('invalid payload');
|
|
220
|
+
if (!validateCommand(payload)) fail('command is not permitted');
|
|
221
|
+
if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
|
|
222
|
+
if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
|
|
223
|
+
|
|
224
|
+
// Resolve the child executable before anything with a side effect: an
|
|
225
|
+
// unresolvable command must not leave an empty create-only output file behind.
|
|
226
|
+
const childCommand = resolveChildProcessCommand(payload);
|
|
227
|
+
const paths = resolveExecutionPaths(payload);
|
|
228
|
+
const output = openOutputFile(payload, paths);
|
|
229
|
+
let bytesWritten = 0;
|
|
230
|
+
let limitReached = false;
|
|
231
|
+
let writeFailed = false;
|
|
232
|
+
|
|
233
|
+
function writeLimited(chunk) {
|
|
234
|
+
if (limitReached || writeFailed) return;
|
|
235
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
236
|
+
const allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
|
|
237
|
+
if (allowed <= 0) {
|
|
238
|
+
limitReached = true;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const slice = buffer.subarray(0, allowed);
|
|
242
|
+
try {
|
|
243
|
+
if (output) fs.writeSync(output.fd, slice);
|
|
244
|
+
else process.stdout.write(slice);
|
|
245
|
+
bytesWritten += slice.length;
|
|
246
|
+
if (bytesWritten >= MAX_OUTPUT_BYTES) limitReached = true;
|
|
247
|
+
} catch {
|
|
248
|
+
writeFailed = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const head = payload.headArgs === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: spawn(HEAD_EXECUTABLE, payload.headArgs, {
|
|
255
|
+
cwd: paths.cwd,
|
|
256
|
+
env: process.env,
|
|
257
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
258
|
+
});
|
|
259
|
+
if (head) {
|
|
260
|
+
head.stdout.on('data', writeLimited);
|
|
261
|
+
// Early-exit options such as --help or a small -n/-c close stdin before the
|
|
262
|
+
// EC process finishes. Ignore EPIPE and keep draining EC output below.
|
|
263
|
+
head.stdin.on('error', () => {});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const child = spawn(childCommand.file, childCommand.args, {
|
|
267
|
+
cwd: paths.cwd,
|
|
268
|
+
env: process.env,
|
|
269
|
+
stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
|
|
270
|
+
windowsHide: IS_WINDOWS,
|
|
271
|
+
});
|
|
272
|
+
const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
|
|
273
|
+
if (head) {
|
|
274
|
+
for (const stream of sourceStreams) {
|
|
275
|
+
// The no-op data listener keeps draining the source if head exits early.
|
|
276
|
+
stream.on('data', () => {});
|
|
277
|
+
stream.pipe(head.stdin, { end: false });
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
for (const stream of sourceStreams) stream.on('data', writeLimited);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
child.once('error', error => {
|
|
284
|
+
if (output) {
|
|
285
|
+
try { fs.closeSync(output.fd); } catch {}
|
|
286
|
+
try { fs.unlinkSync(output.target); } catch {}
|
|
287
|
+
}
|
|
288
|
+
fail(`unable to start command: ${error.message}`);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
let sourceClosed = false;
|
|
292
|
+
let sourceCode = 1;
|
|
293
|
+
let headClosed = !head;
|
|
294
|
+
let headCode = 1;
|
|
295
|
+
|
|
296
|
+
function finish() {
|
|
297
|
+
if (!sourceClosed || !headClosed) return;
|
|
298
|
+
if (output) {
|
|
299
|
+
try { fs.closeSync(output.fd); } catch { writeFailed = true; }
|
|
300
|
+
}
|
|
301
|
+
if (writeFailed) {
|
|
302
|
+
if (output) try { fs.unlinkSync(output.target); } catch {}
|
|
303
|
+
fail('failed to write command output');
|
|
304
|
+
}
|
|
305
|
+
if (output) process.stderr.write(`Saved output to ${output.target}\n`);
|
|
306
|
+
process.exitCode = head ? headCode : sourceCode;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (head) {
|
|
310
|
+
head.once('error', error => {
|
|
311
|
+
writeFailed = true;
|
|
312
|
+
process.stderr.write(`ec-safe-output: unable to start head: ${error.message}\n`);
|
|
313
|
+
});
|
|
314
|
+
head.once('close', code => {
|
|
315
|
+
headCode = code ?? 1;
|
|
316
|
+
headClosed = true;
|
|
317
|
+
for (const stream of sourceStreams) stream.unpipe(head.stdin);
|
|
318
|
+
finish();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
child.once('close', code => {
|
|
323
|
+
sourceCode = code ?? 1;
|
|
324
|
+
sourceClosed = true;
|
|
325
|
+
if (head && !head.stdin.destroyed) head.stdin.end();
|
|
326
|
+
finish();
|
|
327
|
+
});
|