@aiwg/cli 2026.10.1 → 2026.10.3
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/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +210 -4
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/driver.js +3 -1
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +21 -3
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +49 -32
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { compileModelPolicy, } from './provider-policy.js';
|
|
2
2
|
import { MODEL_WRAPPERS } from './provider-models.js';
|
|
3
3
|
import { routeModelTier } from './router.js';
|
|
4
|
+
import { resolveWorkerPlan } from './resolver.js';
|
|
4
5
|
function roleForTier(tier) {
|
|
5
6
|
if (tier === 'economy')
|
|
6
7
|
return 'efficiency';
|
|
@@ -14,6 +15,23 @@ export function buildWrapperRouteEnvelope(request) {
|
|
|
14
15
|
if (!request.assignment.trim())
|
|
15
16
|
throw new Error('Wrapper routing requires a bounded assignment.');
|
|
16
17
|
const decision = routeModelTier(request);
|
|
18
|
+
if (request.worker?.snapshot.config.parallelism?.worker_dispatch && !request.deterministic) {
|
|
19
|
+
const worker = resolveWorkerPlan(request.worker.snapshot, {
|
|
20
|
+
...request.worker.request,
|
|
21
|
+
authority: request,
|
|
22
|
+
});
|
|
23
|
+
const role = worker.role;
|
|
24
|
+
const wrapper = role ? MODEL_WRAPPERS[role] : null;
|
|
25
|
+
return {
|
|
26
|
+
version: 1, provider: request.provider, decision, role,
|
|
27
|
+
tier: role ? worker.policy?.roleGuidance?.[role]?.tier ?? null : null,
|
|
28
|
+
wrapper, capability: request.capability, assignment: request.assignment.trim(), model: null, worker,
|
|
29
|
+
launch: { mechanism: request.launchMechanism, target: worker.selected ? wrapper : null,
|
|
30
|
+
prompt: worker.selected
|
|
31
|
+
? `Run this bounded assignment through ${wrapper}.\nSelected worker: ${worker.selected.group}/${worker.selected.member}; model ${worker.selected.model}; effort ${worker.selected.effort ?? 'not-applicable'}.\nAssignment: ${request.assignment.trim()}\nRevalidate current input digests before invocation; return claims for conductor verification.`
|
|
32
|
+
: 'Worker resolution is blocked; inspect the plan diagnostics and exclusions.' },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
17
35
|
const tier = decision.modelTier;
|
|
18
36
|
const role = roleForTier(tier);
|
|
19
37
|
const wrapper = role ? MODEL_WRAPPERS[role] : null;
|
package/dist/src/ops/registry.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readFile, writeFile, mkdir, readdir } from 'fs/promises';
|
|
|
10
10
|
import { resolve, dirname, basename, sep } from 'path';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
13
|
-
import {
|
|
13
|
+
import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
|
|
14
14
|
import { resolveConfigDir } from '../config/user-config.js';
|
|
15
15
|
import { resolveWorkspace } from '../config/workspace.js';
|
|
16
16
|
import { projectControlPath } from '../config/project-artifacts.js';
|
|
@@ -495,7 +495,10 @@ export class OpsRegistry {
|
|
|
495
495
|
if (remote) {
|
|
496
496
|
console.log(` Pushing ${repoName} to ${remote}/${branch}...`);
|
|
497
497
|
try {
|
|
498
|
-
|
|
498
|
+
if (remote.startsWith('-') || branch.startsWith('-')) {
|
|
499
|
+
throw new Error('Remote and branch must not start with "-"');
|
|
500
|
+
}
|
|
501
|
+
boundedExecFileSync('git', ['push', '--end-of-options', remote, branch], 'network', {
|
|
499
502
|
cwd: repo.path,
|
|
500
503
|
stdio: 'pipe',
|
|
501
504
|
});
|
|
@@ -541,23 +544,15 @@ async function provisionRepo(repoPath, fromUrl, silent) {
|
|
|
541
544
|
}
|
|
542
545
|
}
|
|
543
546
|
await mkdir(dirname(repoPath), { recursive: true });
|
|
544
|
-
|
|
547
|
+
// Cloning a full repository; long tier (#3062).
|
|
548
|
+
boundedExecFileSync('git', ['clone', '--', fromUrl, repoPath], 'long', { stdio: silent ? 'pipe' : 'inherit' });
|
|
545
549
|
return { action: 'Cloned', remote: readGitRemote(repoPath) };
|
|
546
550
|
}
|
|
547
551
|
// Case 3: fresh init.
|
|
548
552
|
await mkdir(repoPath, { recursive: true });
|
|
549
|
-
|
|
553
|
+
boundedExecFileSync('git', ['init'], 'quick', { cwd: repoPath, stdio: 'pipe' });
|
|
550
554
|
return { action: 'Created', remote: undefined };
|
|
551
555
|
}
|
|
552
|
-
/**
|
|
553
|
-
* Minimal shell escaping for git URLs and paths passed via execSync.
|
|
554
|
-
* URLs are URL-encoded already, but paths may contain spaces.
|
|
555
|
-
*/
|
|
556
|
-
function shellEscape(s) {
|
|
557
|
-
if (/^[A-Za-z0-9_@:./~+,=-]+$/.test(s))
|
|
558
|
-
return s;
|
|
559
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
560
|
-
}
|
|
561
556
|
/**
|
|
562
557
|
* Return the `origin` remote URL for a git repo rooted at `dirPath`, or
|
|
563
558
|
* undefined if not a git repo or no origin configured.
|
|
@@ -566,7 +561,7 @@ function readGitRemote(dirPath) {
|
|
|
566
561
|
if (!existsSync(resolve(dirPath, '.git')))
|
|
567
562
|
return undefined;
|
|
568
563
|
try {
|
|
569
|
-
const out =
|
|
564
|
+
const out = boundedExecFileSync('git', ['config', '--get', 'remote.origin.url'], 'quick', {
|
|
570
565
|
cwd: dirPath,
|
|
571
566
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
572
567
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import {
|
|
2
|
+
import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
@@ -65,7 +65,8 @@ function writeProviderMetadata(root, provider, manifest) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
function createDeterministicArchive(sourceDir, archivePath) {
|
|
68
|
-
|
|
68
|
+
// Packaging a local plugin directory; quick tier (#3062).
|
|
69
|
+
const result = boundedSpawnSync('tar', [
|
|
69
70
|
'--sort=name',
|
|
70
71
|
'--mtime=@0',
|
|
71
72
|
'--owner=0',
|
|
@@ -76,7 +77,7 @@ function createDeterministicArchive(sourceDir, archivePath) {
|
|
|
76
77
|
'-C',
|
|
77
78
|
path.dirname(sourceDir),
|
|
78
79
|
path.basename(sourceDir),
|
|
79
|
-
], { encoding: 'utf8' });
|
|
80
|
+
], 'quick', { encoding: 'utf8' });
|
|
80
81
|
if (result.status !== 0)
|
|
81
82
|
throw new Error(`Could not create deterministic archive: ${result.stderr || 'tar failed'}`);
|
|
82
83
|
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded synchronous child-process wrappers (#3062).
|
|
3
|
+
*
|
|
4
|
+
* `execSync`/`execFileSync`/`spawnSync` block the calling thread for as long
|
|
5
|
+
* as the child — or any grandchild still holding an inherited pipe — lives.
|
|
6
|
+
* With no timeout that is unbounded: it hung the test suite once and can
|
|
7
|
+
* hang the CLI when it runs without a terminal. These wrappers make every
|
|
8
|
+
* call bounded by construction: a required `tier` selects the wall-clock
|
|
9
|
+
* cap, the call never inherits stdin, and a timeout always surfaces as the
|
|
10
|
+
* normal Node `ETIMEDOUT` error/result with a message that names the
|
|
11
|
+
* command, tier and bound.
|
|
12
|
+
*
|
|
13
|
+
* Tiers (see `SYNC_PROCESS_TIER_BOUNDS_MS`):
|
|
14
|
+
* - `quick` 30s — local queries: git status/rev-parse/config, version
|
|
15
|
+
* probes, `which`/`where`, `df`, tmux queries.
|
|
16
|
+
* - `network` 2m — git fetch/pull/ls-remote/push, `gh`/API calls.
|
|
17
|
+
* - `long` 10m — installs, clones, docker, builds, test runs launched
|
|
18
|
+
* by scripts. Overridable via `AIWG_SYNC_LONG_TIMEOUT_MS`
|
|
19
|
+
* (see `resolveSyncProcessTimeoutMs`); `quick`/`network`
|
|
20
|
+
* are not overridable.
|
|
21
|
+
*
|
|
22
|
+
* Choose the tier by what the command does, not by where the call site
|
|
23
|
+
* lives.
|
|
24
|
+
*
|
|
25
|
+
* Interactive-by-design calls (an editor, a login flow, launching a harness
|
|
26
|
+
* binary the user is meant to talk to) do not belong here — bounding them
|
|
27
|
+
* would just make the interactive session time out. Leave those sites on
|
|
28
|
+
* the plain Node APIs.
|
|
29
|
+
*
|
|
30
|
+
* ## Why timeout kills with SIGKILL, not SIGTERM-then-SIGKILL
|
|
31
|
+
*
|
|
32
|
+
* Node's own `timeout`/`killSignal` option sends `killSignal` exactly once
|
|
33
|
+
* when the bound elapses, then keeps blocking until the child actually
|
|
34
|
+
* exits. If the child ignores that signal — trivial to do for SIGTERM, and
|
|
35
|
+
* exactly the "grandchild holding an inherited pipe" scenario this gate
|
|
36
|
+
* exists to catch — the call hangs forever regardless of the configured
|
|
37
|
+
* timeout; killSignal only picks *which* signal is sent once, not whether
|
|
38
|
+
* the wait ever ends. There is no portable way to schedule a second signal
|
|
39
|
+
* from inside a single blocking native call: the calling thread is blocked
|
|
40
|
+
* in a C++ synchronous spawn, so no JS timer can run to escalate.
|
|
41
|
+
*
|
|
42
|
+
* These wrappers therefore always kill with `killSignal: 'SIGKILL'` and
|
|
43
|
+
* ignore any `killSignal` a caller passes in `options` — honoring a
|
|
44
|
+
* caller-supplied `'SIGTERM'` would silently reopen exactly the hang this
|
|
45
|
+
* module exists to close for a child that traps and ignores it. SIGKILL
|
|
46
|
+
* cannot be caught, blocked or ignored, so the *direct child* reliably dies
|
|
47
|
+
* at the bound — confirmed with a real child that traps and ignores SIGTERM
|
|
48
|
+
* (see `test/unit/process/bounded-child-process.test.mjs`): with
|
|
49
|
+
* `killSignal: 'SIGTERM'` the call hangs indefinitely; with `'SIGKILL'` it
|
|
50
|
+
* returns at the bound, every time.
|
|
51
|
+
*
|
|
52
|
+
* ## What SIGKILL does *not* guarantee: descendant cleanup
|
|
53
|
+
*
|
|
54
|
+
* These wrappers bound one synchronous call to one direct child process.
|
|
55
|
+
* SIGKILL is sent to that child only — never to a process group or a
|
|
56
|
+
* descendant tree. A direct child that itself spawned a grandchild (for
|
|
57
|
+
* example a backgrounded, detached, or double-forked process) is not
|
|
58
|
+
* supervised here: that grandchild is not signaled, is not waited for, and
|
|
59
|
+
* may keep running after this call returns (see
|
|
60
|
+
* `test/fixtures/process/spawn-detached-pipe-holder.mjs` and its test for a
|
|
61
|
+
* concrete case). A synchronous Node call cannot supervise a process tree —
|
|
62
|
+
* doing so needs an asynchronous process-group/session owner, which is a
|
|
63
|
+
* different mechanism than what `execSync`/`execFileSync`/`spawnSync` give
|
|
64
|
+
* us. Callers that spawn a tool known to background work of its own are
|
|
65
|
+
* responsible for that tool's own cleanup contract; this module only
|
|
66
|
+
* guarantees the direct child is gone and the calling thread is unblocked
|
|
67
|
+
* by the stated bound.
|
|
68
|
+
*
|
|
69
|
+
* @module src/process/bounded-child-process
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
import {
|
|
73
|
+
execSync as nodeExecSync,
|
|
74
|
+
execFileSync as nodeExecFileSync,
|
|
75
|
+
spawnSync as nodeSpawnSync,
|
|
76
|
+
} from 'node:child_process';
|
|
77
|
+
|
|
78
|
+
/** Env var that raises or disables the `long` tier bound. `0` disables it (no timeout). */
|
|
79
|
+
export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV = 'AIWG_SYNC_LONG_TIMEOUT_MS';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Default wall-clock bounds per tier, in milliseconds. `quick` and `network`
|
|
83
|
+
* are fixed; `long` is the default absent `AIWG_SYNC_LONG_TIMEOUT_MS`.
|
|
84
|
+
*/
|
|
85
|
+
export const SYNC_PROCESS_TIER_BOUNDS_MS = Object.freeze({
|
|
86
|
+
quick: 30_000,
|
|
87
|
+
network: 120_000,
|
|
88
|
+
long: 600_000,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const VALID_TIERS = new Set(Object.keys(SYNC_PROCESS_TIER_BOUNDS_MS));
|
|
92
|
+
|
|
93
|
+
let warnedInvalidLongTimeoutEnv = false;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the `long` tier bound, honoring `AIWG_SYNC_LONG_TIMEOUT_MS`.
|
|
97
|
+
*
|
|
98
|
+
* - Unset/empty → the default (`SYNC_PROCESS_TIER_BOUNDS_MS.long`).
|
|
99
|
+
* - `"0"` → disabled: no bound is applied to the `long` tier at all.
|
|
100
|
+
* - A positive integer → that many milliseconds.
|
|
101
|
+
* - Anything else → the default, with one warning to stderr per process.
|
|
102
|
+
*
|
|
103
|
+
* @returns {number} Milliseconds, or `0` meaning "no bound".
|
|
104
|
+
*/
|
|
105
|
+
function resolveLongTierTimeoutMs() {
|
|
106
|
+
const raw = process.env[AIWG_SYNC_LONG_TIMEOUT_MS_ENV];
|
|
107
|
+
if (raw === undefined || raw.trim() === '') return SYNC_PROCESS_TIER_BOUNDS_MS.long;
|
|
108
|
+
const trimmed = raw.trim();
|
|
109
|
+
if (trimmed === '0') return 0;
|
|
110
|
+
const parsed = Number(trimmed);
|
|
111
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
112
|
+
if (!warnedInvalidLongTimeoutEnv) {
|
|
113
|
+
warnedInvalidLongTimeoutEnv = true;
|
|
114
|
+
console.warn(
|
|
115
|
+
`[aiwg] ${AIWG_SYNC_LONG_TIMEOUT_MS_ENV}=${JSON.stringify(raw)} is not a positive integer or "0"; ` +
|
|
116
|
+
`using the default long-tier bound (${SYNC_PROCESS_TIER_BOUNDS_MS.long}ms).`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return SYNC_PROCESS_TIER_BOUNDS_MS.long;
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Exposed for tests and for the gate/report tooling: resolve the effective
|
|
126
|
+
* bound for a tier right now (reads the env override for `long` each call,
|
|
127
|
+
* same as the wrappers do).
|
|
128
|
+
*
|
|
129
|
+
* @param {'quick'|'network'|'long'} tier
|
|
130
|
+
* @returns {number} Milliseconds; `0` means "no bound" (long tier disabled).
|
|
131
|
+
*/
|
|
132
|
+
export function resolveSyncProcessTimeoutMs(tier) {
|
|
133
|
+
assertValidTier(tier);
|
|
134
|
+
if (tier === 'long') return resolveLongTierTimeoutMs();
|
|
135
|
+
return SYNC_PROCESS_TIER_BOUNDS_MS[tier];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assertValidTier(tier) {
|
|
139
|
+
if (!VALID_TIERS.has(tier)) {
|
|
140
|
+
throw new TypeError(
|
|
141
|
+
`Sync process tier must be one of ${[...VALID_TIERS].map((t) => `'${t}'`).join(', ')}; got ${JSON.stringify(tier)}.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The bound actually applied to one call: the tier bound, unless the
|
|
148
|
+
* caller's own `options.timeout` is a smaller positive number (an explicit
|
|
149
|
+
* smaller timeout always wins; a zero/negative/non-numeric one is ignored —
|
|
150
|
+
* a call site cannot silently opt out of the tier bound by passing
|
|
151
|
+
* `timeout: 0` itself; only the `long`-tier env var can disable a bound).
|
|
152
|
+
*
|
|
153
|
+
* @param {'quick'|'network'|'long'} tier
|
|
154
|
+
* @param {{timeout?: number}} [options]
|
|
155
|
+
* @returns {number}
|
|
156
|
+
*/
|
|
157
|
+
function effectiveTimeoutMs(tier, options) {
|
|
158
|
+
const bound = resolveSyncProcessTimeoutMs(tier);
|
|
159
|
+
const explicit = options?.timeout;
|
|
160
|
+
if (typeof explicit === 'number' && Number.isFinite(explicit) && explicit > 0 && (bound === 0 || explicit < bound)) {
|
|
161
|
+
return explicit;
|
|
162
|
+
}
|
|
163
|
+
return bound;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clamp a caller-controlled value to a guaranteed-positive millisecond
|
|
168
|
+
* bound, for the one case `boundedExecSync`/`boundedExecFileSync`/
|
|
169
|
+
* `boundedSpawnSync` don't cover by themselves: a *raw* `execSync`/
|
|
170
|
+
* `execFileSync`/`spawnSync` call site whose `timeout` option is computed
|
|
171
|
+
* from caller input (a CLI flag, a config value, a converted unit) rather
|
|
172
|
+
* than a literal — `timeout: options.timeout` or `timeout: timeoutSec *
|
|
173
|
+
* 1000` with an untrusted `timeoutSec`, for example. `tools/testing/
|
|
174
|
+
* check-sync-child-process-timeouts.mjs` (#3062 B/C finding 8) recognizes
|
|
175
|
+
* a call to this function, by binding, as a provably positive timeout —
|
|
176
|
+
* so wrap the dynamic value here instead of passing it through directly.
|
|
177
|
+
*
|
|
178
|
+
* `value` passes through only when it is itself a finite number `> 0`;
|
|
179
|
+
* anything else (`0`, a negative number, `NaN`, `undefined`, a string,
|
|
180
|
+
* ...) falls back to `fallbackMs` — so a caller-controlled `timeoutSec: 0`
|
|
181
|
+
* cannot disable the bound. `fallbackMs` itself falls back to the `quick`
|
|
182
|
+
* tier bound if it is not a valid positive number, so this function can
|
|
183
|
+
* never itself return a non-positive or non-finite value.
|
|
184
|
+
*
|
|
185
|
+
* @param {unknown} value
|
|
186
|
+
* @param {number} fallbackMs
|
|
187
|
+
* @returns {number}
|
|
188
|
+
*/
|
|
189
|
+
export function positiveTimeoutMs(value, fallbackMs) {
|
|
190
|
+
const resolvedFallback = typeof fallbackMs === 'number' && Number.isFinite(fallbackMs) && fallbackMs > 0
|
|
191
|
+
? fallbackMs
|
|
192
|
+
: SYNC_PROCESS_TIER_BOUNDS_MS.quick;
|
|
193
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : resolvedFallback;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether a `stdio[0]` (stdin slot) value describes the parent's own stdin —
|
|
198
|
+
* the form Node would otherwise inherit from. Covers the string shorthand,
|
|
199
|
+
* the literal fd `0`, and the `process.stdin` stream object itself; leaves
|
|
200
|
+
* `'pipe'`, `'ignore'`, other fds, and unrelated stream objects alone.
|
|
201
|
+
*
|
|
202
|
+
* @param {unknown} value
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
function isParentStdinDescriptor(value) {
|
|
206
|
+
return value === 'inherit' || value === 0 || value === process.stdin;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A bounded call never inherits stdin; stdout/stderr may still be inherited
|
|
211
|
+
* for progress output. When the caller supplies `input`, an inherited
|
|
212
|
+
* stdin form is normalized to `'pipe'` instead of `'ignore'` — `input`
|
|
213
|
+
* needs a real pipe to deliver its payload through, and `'ignore'` would
|
|
214
|
+
* silently drop it (#3062 B/C finding 9). `options` is otherwise left
|
|
215
|
+
* untouched when stdin was not going to be inherited anyway.
|
|
216
|
+
*
|
|
217
|
+
* @param {Record<string, unknown>} options
|
|
218
|
+
* @returns {Record<string, unknown>}
|
|
219
|
+
*/
|
|
220
|
+
function withoutInheritedStdin(options) {
|
|
221
|
+
if (options.input !== undefined) {
|
|
222
|
+
// `input` needs stdin to actually be a pipe to deliver its payload —
|
|
223
|
+
// a caller-specified inherited-stdin form in `stdio[0]` must not win
|
|
224
|
+
// over that (#3062 B/C finding 9: an earlier version returned
|
|
225
|
+
// `options` completely untouched here, so `input` combined with
|
|
226
|
+
// `stdio: 'inherit'`/fd `0`/`process.stdin` could still leave stdin
|
|
227
|
+
// connected to the parent's real stdin instead of the pipe `input`
|
|
228
|
+
// is written to). stdout/stderr and the payload itself are untouched.
|
|
229
|
+
if (options.stdio === 'inherit') {
|
|
230
|
+
return { ...options, stdio: ['pipe', 'inherit', 'inherit'] };
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(options.stdio) && isParentStdinDescriptor(options.stdio[0])) {
|
|
233
|
+
const stdio = options.stdio.slice();
|
|
234
|
+
stdio[0] = 'pipe';
|
|
235
|
+
return { ...options, stdio };
|
|
236
|
+
}
|
|
237
|
+
return options;
|
|
238
|
+
}
|
|
239
|
+
if (options.stdio === 'inherit') {
|
|
240
|
+
return { ...options, stdio: ['ignore', 'inherit', 'inherit'] };
|
|
241
|
+
}
|
|
242
|
+
if (Array.isArray(options.stdio) && isParentStdinDescriptor(options.stdio[0])) {
|
|
243
|
+
const stdio = options.stdio.slice();
|
|
244
|
+
stdio[0] = 'ignore';
|
|
245
|
+
return { ...options, stdio };
|
|
246
|
+
}
|
|
247
|
+
return options;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Build the final Node options: bounded stdin, resolved timeout, and a
|
|
252
|
+
* forced `killSignal: 'SIGKILL'`. Unlike `timeout`, `killSignal` is not
|
|
253
|
+
* negotiable: a caller-supplied `killSignal` (e.g. `'SIGTERM'`) is
|
|
254
|
+
* overwritten, not honored, because a child that ignores it would defeat
|
|
255
|
+
* the bound this module exists to guarantee (see the module header).
|
|
256
|
+
*
|
|
257
|
+
* @param {Record<string, unknown> | undefined} options
|
|
258
|
+
* @param {number} boundMs
|
|
259
|
+
* @returns {Record<string, unknown>}
|
|
260
|
+
*/
|
|
261
|
+
function toNodeOptions(options, boundMs) {
|
|
262
|
+
const base = withoutInheritedStdin(options ?? {});
|
|
263
|
+
return {
|
|
264
|
+
...base,
|
|
265
|
+
timeout: boundMs,
|
|
266
|
+
killSignal: 'SIGKILL',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function describeCommand(command, args) {
|
|
271
|
+
if (args === undefined) return command;
|
|
272
|
+
return [command, ...args].join(' ');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* On timeout (`error.code === 'ETIMEDOUT'`), prepend a message naming the
|
|
277
|
+
* command, tier and bound. Leaves every other field (`code`, `signal`,
|
|
278
|
+
* `status`, `killed`) exactly as Node set them, and leaves non-timeout
|
|
279
|
+
* errors untouched, so existing catch blocks that inspect those fields (or
|
|
280
|
+
* just catch-and-ignore) keep working.
|
|
281
|
+
*
|
|
282
|
+
* `signal` disambiguates *why* `ETIMEDOUT` fired: when it is `'SIGKILL'` the
|
|
283
|
+
* direct child itself was still running and got killed at the bound: the
|
|
284
|
+
* common case. When it is anything else (including `null`), the direct
|
|
285
|
+
* child had already exited — `status`/`signal` on the result reflect its
|
|
286
|
+
* real exit — but this call was still blocked waiting for its stdout/stderr
|
|
287
|
+
* pipe to reach EOF, which only happens once every process holding that
|
|
288
|
+
* pipe's write end (the child *and* any surviving descendant it handed the
|
|
289
|
+
* fd to) has closed it. A surviving, pipe-holding descendant reproduces
|
|
290
|
+
* that even though the child it came from is long gone (see
|
|
291
|
+
* `test/fixtures/process/spawn-detached-pipe-holder.mjs`).
|
|
292
|
+
*
|
|
293
|
+
* @template {Error & {code?: string, signal?: string | null}} E
|
|
294
|
+
* @param {E | null | undefined} error
|
|
295
|
+
* @param {{description: string, tier: string, boundMs: number, signal?: string | null}} context
|
|
296
|
+
* @returns {E | null | undefined}
|
|
297
|
+
*/
|
|
298
|
+
function enrichTimeoutError(error, { description, tier, boundMs, signal }) {
|
|
299
|
+
if (!error || error.code !== 'ETIMEDOUT') return error;
|
|
300
|
+
const effectiveSignal = signal !== undefined ? signal : error.signal;
|
|
301
|
+
const killClause = effectiveSignal === 'SIGKILL'
|
|
302
|
+
? 'the direct child was killed (SIGKILL)'
|
|
303
|
+
: 'the direct child had already exited, but this call was still waiting for its stdout/stderr pipe to close';
|
|
304
|
+
error.message =
|
|
305
|
+
`\`${description}\` exceeded the '${tier}' tier bound of ${boundMs}ms; ${killClause}. ` +
|
|
306
|
+
`Any descendant the child spawned (detached, backgrounded, or otherwise ignoring signals) is not supervised ` +
|
|
307
|
+
`by this call and may still be running — holding that pipe open is enough to trigger this timeout even after ` +
|
|
308
|
+
`the child itself has exited. ` +
|
|
309
|
+
(tier === 'long' ? `Set ${AIWG_SYNC_LONG_TIMEOUT_MS_ENV} to a larger number of milliseconds, or 0 to remove the bound. ` : '') +
|
|
310
|
+
error.message;
|
|
311
|
+
return error;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Bounded `child_process.execSync`. Same arguments and return value as
|
|
316
|
+
* Node's `execSync(command, options)`, with a required `tier` inserted
|
|
317
|
+
* before `options`.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} command
|
|
320
|
+
* @param {'quick'|'network'|'long'} tier
|
|
321
|
+
* @param {Record<string, unknown>} [options]
|
|
322
|
+
* @returns {Buffer | string}
|
|
323
|
+
*/
|
|
324
|
+
export function boundedExecSync(command, tier, options) {
|
|
325
|
+
assertValidTier(tier);
|
|
326
|
+
const boundMs = effectiveTimeoutMs(tier, options);
|
|
327
|
+
const nodeOptions = toNodeOptions(options, boundMs);
|
|
328
|
+
try {
|
|
329
|
+
return nodeExecSync(command, nodeOptions);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw enrichTimeoutError(error, { description: command, tier, boundMs });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Bounded `child_process.execFileSync`. Same arguments and return value as
|
|
337
|
+
* Node's `execFileSync(file, args, options)`, with a required `tier`
|
|
338
|
+
* inserted before `options`. Unlike the native API, `args` is always
|
|
339
|
+
* required (pass `[]` for none) so tier can occupy a fixed position.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} file
|
|
342
|
+
* @param {readonly string[]} args
|
|
343
|
+
* @param {'quick'|'network'|'long'} tier
|
|
344
|
+
* @param {Record<string, unknown>} [options]
|
|
345
|
+
* @returns {Buffer | string}
|
|
346
|
+
*/
|
|
347
|
+
export function boundedExecFileSync(file, args, tier, options) {
|
|
348
|
+
assertValidTier(tier);
|
|
349
|
+
const boundMs = effectiveTimeoutMs(tier, options);
|
|
350
|
+
const nodeOptions = toNodeOptions(options, boundMs);
|
|
351
|
+
try {
|
|
352
|
+
return nodeExecFileSync(file, args, nodeOptions);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
throw enrichTimeoutError(error, { description: describeCommand(file, args), tier, boundMs });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Bounded `child_process.spawnSync`. Same arguments and return shape as
|
|
360
|
+
* Node's `spawnSync(command, args, options)`, with a required `tier`
|
|
361
|
+
* inserted before `options`. Unlike the native API, `args` is always
|
|
362
|
+
* required (pass `[]` for none) so tier can occupy a fixed position.
|
|
363
|
+
*
|
|
364
|
+
* `spawnSync` never throws; on timeout the returned object has an `error`
|
|
365
|
+
* with `code: 'ETIMEDOUT'` whose message has been enriched in place. In the
|
|
366
|
+
* common case the direct child was still running and gets killed at the
|
|
367
|
+
* bound, so `status: null` and `signal: 'SIGKILL'`. If the direct child had
|
|
368
|
+
* already exited but a surviving descendant is still holding its
|
|
369
|
+
* stdout/stderr pipe open, `status`/`signal` instead reflect that real exit
|
|
370
|
+
* (e.g. `status: 0`, `signal: null`) while `error.code` is still
|
|
371
|
+
* `'ETIMEDOUT'` — check `error`, not just `status`/`signal`, to detect the
|
|
372
|
+
* timeout in that case.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} command
|
|
375
|
+
* @param {readonly string[]} args
|
|
376
|
+
* @param {'quick'|'network'|'long'} tier
|
|
377
|
+
* @param {Record<string, unknown>} [options]
|
|
378
|
+
* @returns {import('node:child_process').SpawnSyncReturns<Buffer | string>}
|
|
379
|
+
*/
|
|
380
|
+
export function boundedSpawnSync(command, args, tier, options) {
|
|
381
|
+
assertValidTier(tier);
|
|
382
|
+
const boundMs = effectiveTimeoutMs(tier, options);
|
|
383
|
+
const nodeOptions = toNodeOptions(options, boundMs);
|
|
384
|
+
const result = nodeSpawnSync(command, args, nodeOptions);
|
|
385
|
+
enrichTimeoutError(result.error, { description: describeCommand(command, args), tier, boundMs, signal: result.signal });
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Share PATH/PATHEXT lookup with callers that can launch arbitrary batch files.
|
|
5
|
+
function* executableCandidates(command, searchPath, pathExt, preferNative = false, cwd = process.cwd()) {
|
|
6
|
+
const candidates = path.isAbsolute(command)
|
|
7
|
+
? [command]
|
|
8
|
+
: /[\\/]/.test(command)
|
|
9
|
+
? [path.resolve(cwd, ...command.split(/[\\/]/))]
|
|
10
|
+
: searchPath.split(';').filter(Boolean).map(dir => path.join(dir, command));
|
|
11
|
+
for (const candidate of candidates) {
|
|
12
|
+
const pathExtensions = pathExt.split(';').filter(Boolean).map(ext => ext.toLowerCase());
|
|
13
|
+
const extension = path.extname(candidate).toLowerCase();
|
|
14
|
+
const extensions = extension && (pathExtensions.includes(extension) || existsSync(candidate))
|
|
15
|
+
? ['']
|
|
16
|
+
: pathExtensions;
|
|
17
|
+
if (preferNative) {
|
|
18
|
+
extensions.sort((a, b) => Number(!/\.(?:exe|com)$/i.test(a)) - Number(!/\.(?:exe|com)$/i.test(b)));
|
|
19
|
+
}
|
|
20
|
+
for (const extension of extensions) {
|
|
21
|
+
const executable = `${candidate}${extension}`;
|
|
22
|
+
if (existsSync(executable)) yield executable;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveExecutable(
|
|
28
|
+
command,
|
|
29
|
+
platform = process.platform,
|
|
30
|
+
searchPath = process.env.PATH ?? '',
|
|
31
|
+
pathExt = process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD',
|
|
32
|
+
cwd = process.cwd(),
|
|
33
|
+
) {
|
|
34
|
+
if (platform !== 'win32') return command;
|
|
35
|
+
for (const executable of executableCandidates(command, searchPath, pathExt, true, cwd)) {
|
|
36
|
+
return executable;
|
|
37
|
+
}
|
|
38
|
+
return command;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// npm's Windows shims cannot be spawned directly without a shell. Resolve only
|
|
42
|
+
// their standard Node invocation; never evaluate batch contents or shell args.
|
|
43
|
+
export function cliCommand(
|
|
44
|
+
command,
|
|
45
|
+
args,
|
|
46
|
+
platform = process.platform,
|
|
47
|
+
searchPath = process.env.PATH ?? '',
|
|
48
|
+
pathExt = process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD',
|
|
49
|
+
cwd = process.cwd(),
|
|
50
|
+
) {
|
|
51
|
+
if (platform !== 'win32') return [command, args];
|
|
52
|
+
for (const shim of executableCandidates(command, searchPath, pathExt, false, cwd)) {
|
|
53
|
+
if (!/\.(?:cmd|bat)$/i.test(shim)) return [shim, args];
|
|
54
|
+
const contents = readFileSync(shim, 'utf8');
|
|
55
|
+
const entry = contents.match(
|
|
56
|
+
/^\s*(?:endLocal & goto #_undefined_# 2>NUL \|\| title %COMSPEC% & )?(?:set PATHEXT=%PATHEXT:;.JS;=;% & )?"%_prog%"\s+((?:(?:"[^"\r\n&|<>^%]*"|[^\s"&|<>^%]+)\s+)*)"%dp0%\\([^"\r\n]+)"\s+%\*\s*$/m,
|
|
57
|
+
);
|
|
58
|
+
// Node distributions bundle npm/npx launchers with named entry variables
|
|
59
|
+
// rather than cmd-shim's _prog invocation. Resolve their local JS entry.
|
|
60
|
+
const bundled = !entry && /^(?:npm|npx)\.cmd$/i.test(path.basename(shim))
|
|
61
|
+
? contents.match(/^SET "(NPM|NPX)_CLI_JS=%~dp0\\(node_modules\\npm\\bin\\(?:npm|npx)-cli\.js)"\s*$/im)
|
|
62
|
+
: null;
|
|
63
|
+
if (!entry && (!bundled || !contents.includes(`"%NODE_EXE%" "%${bundled[1]}_CLI_JS%" %*`))) {
|
|
64
|
+
throw new Error(`Unsupported Windows CLI shim: ${shim}`);
|
|
65
|
+
}
|
|
66
|
+
const nodeArgs = entry
|
|
67
|
+
? Array.from(entry[1].matchAll(/"([^"]*)"|([^\s"]+)/g), match => match[1] ?? match[2])
|
|
68
|
+
: [];
|
|
69
|
+
const script = path.resolve(path.dirname(shim), ...(entry ? entry[2] : bundled[2]).split('\\'));
|
|
70
|
+
if (!existsSync(script)) throw new Error(`Missing Windows CLI entry point: ${script}`);
|
|
71
|
+
const localNode = path.join(path.dirname(shim), 'node.exe');
|
|
72
|
+
return [existsSync(localNode) ? localNode : process.execPath, [...nodeArgs, script, ...args]];
|
|
73
|
+
}
|
|
74
|
+
return [command, args];
|
|
75
|
+
}
|
|
@@ -82,6 +82,14 @@ export function getProviderCapabilities(providerKey) {
|
|
|
82
82
|
}
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the S0 route-feasibility row for a provider by key or alias.
|
|
87
|
+
* Returns undefined when the matrix carries no row (e.g. registry-only
|
|
88
|
+
* `generic`, which is not a matrix provider).
|
|
89
|
+
*/
|
|
90
|
+
export function getRouteFeasibility(providerKey) {
|
|
91
|
+
return getProviderCapabilities(providerKey)?.route_feasibility;
|
|
92
|
+
}
|
|
85
93
|
export function getAgentCapabilities(providerKey, agentType = 'default') {
|
|
86
94
|
const caps = getProviderCapabilities(providerKey);
|
|
87
95
|
if (!caps?.agent_capabilities)
|