@cassiomc1/forgeloop 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +298 -3
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/next.js +19 -7
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/config/guides.json +44 -0
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-presets.js +82 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +281 -3
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
EMULATED_SERVICES_ERROR_CODES,
|
|
7
|
+
EMULATED_SERVICES_ID,
|
|
8
|
+
EMULATED_SERVICES_PROCESS_LIMITS,
|
|
9
|
+
EMULATED_SERVICES_SUPPORTED_VERSION,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import {
|
|
12
|
+
emulatedServicesProcessError,
|
|
13
|
+
runBoundedEmulatedServicesCommand,
|
|
14
|
+
spawnManagedEmulatedServicesProcess,
|
|
15
|
+
} from "./process.js";
|
|
16
|
+
|
|
17
|
+
const FACTORY_KEYS = new Set([
|
|
18
|
+
"executablePath",
|
|
19
|
+
"expectedVersion",
|
|
20
|
+
"tempRoot",
|
|
21
|
+
"targetRoot",
|
|
22
|
+
"spawnImpl",
|
|
23
|
+
"fsImpl",
|
|
24
|
+
"mkdtempImpl",
|
|
25
|
+
"rmImpl",
|
|
26
|
+
"fetchImpl",
|
|
27
|
+
"sleepImpl",
|
|
28
|
+
"nowImpl",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function providerError(code, message, cause) {
|
|
32
|
+
return emulatedServicesProcessError(code, message, cause);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertPortableString(value, label) {
|
|
36
|
+
if (typeof value !== "string" || value.length === 0 || /\p{Cc}/u.test(value)) {
|
|
37
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} must be a non-empty portable string`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertAbsolutePath(value, label) {
|
|
43
|
+
assertPortableString(value, label);
|
|
44
|
+
if (!path.isAbsolute(value)) {
|
|
45
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} must be absolute`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertRegularFile(filePath, label, fsImpl) {
|
|
51
|
+
assertAbsolutePath(filePath, label);
|
|
52
|
+
let stat;
|
|
53
|
+
try {
|
|
54
|
+
stat = fsImpl.lstatSync(filePath);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} is unavailable`, error);
|
|
57
|
+
}
|
|
58
|
+
if (!stat.isFile() || stat.isSymbolicLink?.()) {
|
|
59
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} must be a regular non-symlink file`);
|
|
60
|
+
}
|
|
61
|
+
return filePath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isWithin(parent, candidate) {
|
|
65
|
+
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
|
|
66
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertTempRootSafe(tempRoot, targetRoot) {
|
|
70
|
+
if (targetRoot && (isWithin(targetRoot, tempRoot) || isWithin(tempRoot, targetRoot))) {
|
|
71
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services temporary state must be outside the target root");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function filteredEnvironment() {
|
|
76
|
+
const allowed = new Set(["PATH", "HOME", "TMPDIR", "TEMP", "TMP", "SystemRoot", "ComSpec", "LANG", "LC_ALL", "NODE_PATH"]);
|
|
77
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => allowed.has(key)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseVersion(stdout) {
|
|
81
|
+
if (typeof stdout !== "string") {
|
|
82
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_INVALID, "Emulated Services version output was not text");
|
|
83
|
+
}
|
|
84
|
+
const match = /^\s*(?:emulate(?:\s+|@))?v?(\d+\.\d+\.\d+)\s*$/iu.exec(stdout);
|
|
85
|
+
if (!match) {
|
|
86
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_INVALID, "Emulated Services version output was malformed");
|
|
87
|
+
}
|
|
88
|
+
return match[1];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeServices(services) {
|
|
92
|
+
if (!Array.isArray(services) || services.length === 0 || services.length > EMULATED_SERVICES_PROCESS_LIMITS.maxServices) {
|
|
93
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "services must be a non-empty bounded array");
|
|
94
|
+
}
|
|
95
|
+
const normalized = services.map((service) => {
|
|
96
|
+
assertPortableString(service, "service");
|
|
97
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(service)) {
|
|
98
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "service identifiers must be portable lowercase names");
|
|
99
|
+
}
|
|
100
|
+
return service;
|
|
101
|
+
});
|
|
102
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
103
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "services must not contain duplicates");
|
|
104
|
+
}
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeRequest(request, { fsImpl, targetRoot }) {
|
|
109
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
110
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services request must be an object");
|
|
111
|
+
}
|
|
112
|
+
const services = normalizeServices(request.services);
|
|
113
|
+
const basePort = request.basePort ?? 4000;
|
|
114
|
+
if (!Number.isSafeInteger(basePort) || basePort < 1 || basePort + services.length - 1 > 65535) {
|
|
115
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "basePort must provide a valid loopback port range");
|
|
116
|
+
}
|
|
117
|
+
const timeoutMs = request.timeoutMs ?? 30_000;
|
|
118
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > EMULATED_SERVICES_PROCESS_LIMITS.maxTimeoutMs) {
|
|
119
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "timeoutMs is outside the supported bounded range");
|
|
120
|
+
}
|
|
121
|
+
let seedPath = null;
|
|
122
|
+
if (request.seedPath !== undefined && request.seedPath !== null) {
|
|
123
|
+
seedPath = assertRegularFile(request.seedPath, "seedPath", fsImpl);
|
|
124
|
+
if (targetRoot && isWithin(targetRoot, seedPath)) {
|
|
125
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "seedPath must not be inside the target root");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { services, basePort, timeoutMs, seedPath, signal: request.signal };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateSignal(signal) {
|
|
132
|
+
if (signal !== undefined && (!signal || typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function")) {
|
|
133
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "signal must be an AbortSignal");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assertNotCancelled(signal) {
|
|
138
|
+
if (signal?.aborted) {
|
|
139
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CANCELLED, "Emulated Services operation was cancelled");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeEndpoint(port) {
|
|
144
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
145
|
+
const url = new URL(endpoint);
|
|
146
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1") {
|
|
147
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.ENDPOINT_INVALID, "Emulated Services endpoint was not loopback HTTP");
|
|
148
|
+
}
|
|
149
|
+
return endpoint;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function waitForReady(endpoint, { fetchImpl, signal, deadline, sleepImpl, nowImpl }) {
|
|
153
|
+
while (nowImpl() < deadline) {
|
|
154
|
+
assertNotCancelled(signal);
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetchImpl(endpoint, { method: "GET", redirect: "manual", signal });
|
|
157
|
+
const status = Number(response?.status);
|
|
158
|
+
if (Number.isInteger(status) && status >= 200 && status < 500) return;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (signal?.aborted) throw providerError(EMULATED_SERVICES_ERROR_CODES.CANCELLED, "Emulated Services operation was cancelled", error);
|
|
161
|
+
}
|
|
162
|
+
const remaining = deadline - nowImpl();
|
|
163
|
+
if (remaining <= 0) break;
|
|
164
|
+
await sleepImpl(Math.min(EMULATED_SERVICES_PROCESS_LIMITS.readinessPollMs, remaining), signal);
|
|
165
|
+
}
|
|
166
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.TIMEOUT, "Emulated Services did not become ready before the timeout");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateFactoryOptions(options) {
|
|
170
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
171
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services provider options must be an object");
|
|
172
|
+
}
|
|
173
|
+
for (const key of Object.keys(options)) {
|
|
174
|
+
if (!FACTORY_KEYS.has(key)) throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `Unsupported Emulated Services option: ${key}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createEmulatedServicesProvider(options = {}) {
|
|
179
|
+
validateFactoryOptions(options);
|
|
180
|
+
const fsImpl = options.fsImpl ?? fs;
|
|
181
|
+
const executablePath = assertRegularFile(options.executablePath, "executablePath", fsImpl);
|
|
182
|
+
const expectedVersion = options.expectedVersion ?? EMULATED_SERVICES_SUPPORTED_VERSION;
|
|
183
|
+
if (expectedVersion !== EMULATED_SERVICES_SUPPORTED_VERSION) {
|
|
184
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `Only Emulated Services ${EMULATED_SERVICES_SUPPORTED_VERSION} is supported`);
|
|
185
|
+
}
|
|
186
|
+
const tempRoot = assertAbsolutePath(options.tempRoot ?? os.tmpdir(), "tempRoot");
|
|
187
|
+
const targetRoot = options.targetRoot === undefined ? null : assertAbsolutePath(options.targetRoot, "targetRoot");
|
|
188
|
+
assertTempRootSafe(tempRoot, targetRoot);
|
|
189
|
+
const mkdtempImpl = options.mkdtempImpl ?? fs.promises.mkdtemp;
|
|
190
|
+
const rmImpl = options.rmImpl ?? fs.promises.rm;
|
|
191
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
192
|
+
const sleepImpl = options.sleepImpl ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
193
|
+
const nowImpl = options.nowImpl ?? (() => Date.now());
|
|
194
|
+
if (typeof fetchImpl !== "function" || typeof mkdtempImpl !== "function" || typeof rmImpl !== "function") {
|
|
195
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services host capabilities are incomplete");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
id: EMULATED_SERVICES_ID,
|
|
200
|
+
version: expectedVersion,
|
|
201
|
+
async start(request = {}) {
|
|
202
|
+
const normalized = normalizeRequest(request, { fsImpl, targetRoot });
|
|
203
|
+
validateSignal(normalized.signal);
|
|
204
|
+
assertNotCancelled(normalized.signal);
|
|
205
|
+
const tempPrefix = path.join(tempRoot, "forgeloop-emulated-");
|
|
206
|
+
let stateRoot = null;
|
|
207
|
+
let managed = null;
|
|
208
|
+
let result = null;
|
|
209
|
+
let operationError = null;
|
|
210
|
+
try {
|
|
211
|
+
stateRoot = await mkdtempImpl(tempPrefix);
|
|
212
|
+
assertAbsolutePath(stateRoot, "Emulated Services temporary state");
|
|
213
|
+
assertTempRootSafe(stateRoot, targetRoot);
|
|
214
|
+
const versionResult = await runBoundedEmulatedServicesCommand(executablePath, ["--version"], {
|
|
215
|
+
cwd: stateRoot,
|
|
216
|
+
env: filteredEnvironment(),
|
|
217
|
+
spawnImpl: options.spawnImpl,
|
|
218
|
+
timeoutMs: Math.min(normalized.timeoutMs, 10_000),
|
|
219
|
+
});
|
|
220
|
+
const actualVersion = parseVersion(versionResult.stdout);
|
|
221
|
+
if (actualVersion !== expectedVersion) {
|
|
222
|
+
throw providerError(EMULATED_SERVICES_ERROR_CODES.VERSION_UNSUPPORTED, `Unsupported Emulated Services version: ${actualVersion}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const args = ["--service", normalized.services.join(","), "--port", String(normalized.basePort)];
|
|
226
|
+
if (normalized.seedPath) args.push("--seed", normalized.seedPath);
|
|
227
|
+
managed = spawnManagedEmulatedServicesProcess(executablePath, args, {
|
|
228
|
+
cwd: stateRoot,
|
|
229
|
+
env: filteredEnvironment(),
|
|
230
|
+
spawnImpl: options.spawnImpl,
|
|
231
|
+
});
|
|
232
|
+
const deadline = nowImpl() + normalized.timeoutMs;
|
|
233
|
+
const endpoints = normalized.services.map((service, index) => ({
|
|
234
|
+
id: service,
|
|
235
|
+
endpoint: normalizeEndpoint(normalized.basePort + index),
|
|
236
|
+
}));
|
|
237
|
+
for (const service of endpoints) {
|
|
238
|
+
await waitForReady(service.endpoint, {
|
|
239
|
+
fetchImpl,
|
|
240
|
+
signal: normalized.signal,
|
|
241
|
+
deadline,
|
|
242
|
+
sleepImpl,
|
|
243
|
+
nowImpl,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
result = {
|
|
247
|
+
provider: { id: EMULATED_SERVICES_ID, version: expectedVersion },
|
|
248
|
+
services: endpoints,
|
|
249
|
+
status: "READY",
|
|
250
|
+
diagnostics: ["Host-provided emulated services were observed on loopback"],
|
|
251
|
+
authority: "OBSERVATION",
|
|
252
|
+
evidenceAuthority: "NONE",
|
|
253
|
+
lifecycleAuthority: false,
|
|
254
|
+
completionAuthority: false,
|
|
255
|
+
persisted: false,
|
|
256
|
+
cleanup: { status: "PENDING" },
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
operationError = error;
|
|
260
|
+
} finally {
|
|
261
|
+
if (managed) {
|
|
262
|
+
managed.stop();
|
|
263
|
+
await managed.waitForClose();
|
|
264
|
+
}
|
|
265
|
+
if (stateRoot) {
|
|
266
|
+
try {
|
|
267
|
+
await rmImpl(stateRoot, { recursive: true, force: true });
|
|
268
|
+
if (result) result.cleanup = { status: "COMPLETED" };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const cleanupError = providerError(EMULATED_SERVICES_ERROR_CODES.CLEANUP_FAILED, "Emulated Services temporary state cleanup failed", error);
|
|
271
|
+
if (!operationError) operationError = cleanupError;
|
|
272
|
+
if (result) result.cleanup = { status: "FAILED" };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (operationError) throw operationError;
|
|
277
|
+
return Object.freeze(result);
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export { normalizeEndpoint, parseVersion };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ADVISORY_CONTEXT_LIMITS,
|
|
3
|
+
normalizeAdvisoryRecallOptions,
|
|
4
|
+
} from "../../core/advisory-context/constants.js";
|
|
5
|
+
import {
|
|
6
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
7
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
8
|
+
} from "../../core/error-codes.js";
|
|
9
|
+
|
|
10
|
+
function adapterError(code, message) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.name = "OpenSrcAdapterError";
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fitText(value, maxLength) {
|
|
18
|
+
if (value.length <= maxLength) return value;
|
|
19
|
+
return `${value.slice(0, Math.max(1, maxLength - 1))}…`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function lineRangeLabel(startLine, endLine) {
|
|
23
|
+
return startLine === endLine ? `L${startLine}` : `L${startLine}-L${endLine}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeMatch(match) {
|
|
27
|
+
if (!match || typeof match !== "object" || Array.isArray(match)) return null;
|
|
28
|
+
const { sourceSpec, relativePath, snippet, snippetStartLine, snippetEndLine } = match;
|
|
29
|
+
if (typeof sourceSpec !== "string" || sourceSpec === "") return null;
|
|
30
|
+
if (typeof relativePath !== "string" || relativePath === "" || relativePath.startsWith("/")) return null;
|
|
31
|
+
if (typeof snippet !== "string") return null;
|
|
32
|
+
if (!Number.isSafeInteger(snippetStartLine) || snippetStartLine < 1) return null;
|
|
33
|
+
if (!Number.isSafeInteger(snippetEndLine) || snippetEndLine < snippetStartLine) return null;
|
|
34
|
+
const range = lineRangeLabel(snippetStartLine, snippetEndLine);
|
|
35
|
+
const flatSnippet = snippet.replace(/\s+/gu, " ").trim();
|
|
36
|
+
return {
|
|
37
|
+
title: `${sourceSpec} — ${relativePath}`,
|
|
38
|
+
summary: flatSnippet === "" ? range : `${range} | ${flatSnippet}`,
|
|
39
|
+
sourceRef: `opensrc:${sourceSpec}:${relativePath}#${range}`,
|
|
40
|
+
identity: [sourceSpec, relativePath, range, snippet].join(" "),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert bounded deterministic search matches into ForgeLoop advisory items.
|
|
46
|
+
*
|
|
47
|
+
* Titles, summaries, and source references carry only portable relative
|
|
48
|
+
* paths; absolute cache locations never leave this boundary.
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeOpenSrcResult(matches, {
|
|
51
|
+
limit,
|
|
52
|
+
maxItemChars,
|
|
53
|
+
maxTotalChars,
|
|
54
|
+
timeoutMs,
|
|
55
|
+
} = {}) {
|
|
56
|
+
if (!Array.isArray(matches)) {
|
|
57
|
+
throw adapterError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc search matches must be an array");
|
|
58
|
+
}
|
|
59
|
+
if (matches.length > ADVISORY_CONTEXT_LIMITS.maxProviderReturnedItems) {
|
|
60
|
+
throw adapterError(
|
|
61
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
62
|
+
`OpenSrc returned ${matches.length} matches, exceeding the raw item ceiling of ${ADVISORY_CONTEXT_LIMITS.maxProviderReturnedItems}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const options = normalizeAdvisoryRecallOptions({ limit, maxItemChars, maxTotalChars, timeoutMs });
|
|
66
|
+
const prepared = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const match of matches) {
|
|
69
|
+
const normalized = normalizeMatch(match);
|
|
70
|
+
if (!normalized) continue;
|
|
71
|
+
if (seen.has(normalized.identity)) continue;
|
|
72
|
+
seen.add(normalized.identity);
|
|
73
|
+
const summary = fitText(normalized.summary, options.maxItemChars);
|
|
74
|
+
prepared.push({ title: normalized.title, summary, sourceRef: normalized.sourceRef });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const items = [];
|
|
78
|
+
let totalChars = 0;
|
|
79
|
+
for (const candidate of prepared) {
|
|
80
|
+
if (items.length >= options.limit) break;
|
|
81
|
+
const chars = candidate.title.length + candidate.summary.length + candidate.sourceRef.length;
|
|
82
|
+
if (totalChars + chars > options.maxTotalChars) break;
|
|
83
|
+
totalChars += chars;
|
|
84
|
+
items.push(candidate);
|
|
85
|
+
}
|
|
86
|
+
if (items.length === 0 && prepared.length > 0) {
|
|
87
|
+
throw adapterError(E_ADVISORY_CONTEXT_OUTPUT_LIMIT, "OpenSrc matches cannot fit the requested output budget");
|
|
88
|
+
}
|
|
89
|
+
return { items };
|
|
90
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
6
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
7
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
8
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
9
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
10
|
+
} from "../../core/error-codes.js";
|
|
11
|
+
|
|
12
|
+
export const OPENSRC_PROCESS_LIMITS = Object.freeze({
|
|
13
|
+
maxStdoutBytes: 64 * 1024,
|
|
14
|
+
maxStderrBytes: 64 * 1024,
|
|
15
|
+
terminationGraceMs: 250,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function processError(code, message) {
|
|
19
|
+
const error = new Error(message);
|
|
20
|
+
error.name = "OpenSrcProcessError";
|
|
21
|
+
error.code = code;
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertSafeText(value, label) {
|
|
26
|
+
if (typeof value !== "string" || value.length === 0 || /\p{Cc}/u.test(value)) {
|
|
27
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be a non-empty portable string`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertAbsolutePath(value, label) {
|
|
33
|
+
assertSafeText(value, label);
|
|
34
|
+
if (!path.isAbsolute(value)) {
|
|
35
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be an absolute path`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function outputBytes(chunk) {
|
|
41
|
+
return Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(String(chunk));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function terminateChild(child) {
|
|
45
|
+
if (!child || typeof child.kill !== "function") return;
|
|
46
|
+
try {
|
|
47
|
+
child.kill("SIGTERM");
|
|
48
|
+
} catch {
|
|
49
|
+
// The process may have exited between the limit check and kill.
|
|
50
|
+
}
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
try {
|
|
53
|
+
child.kill("SIGKILL");
|
|
54
|
+
} catch {
|
|
55
|
+
// Preserve the original timeout or output-limit error.
|
|
56
|
+
}
|
|
57
|
+
}, OPENSRC_PROCESS_LIMITS.terminationGraceMs).unref?.();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function waitForChildClose(child) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let settled = false;
|
|
63
|
+
const finish = () => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimeout(fallbackTimer);
|
|
67
|
+
resolve();
|
|
68
|
+
};
|
|
69
|
+
const finishAfterClose = () => {
|
|
70
|
+
// Windows can retain a child working directory for a short interval
|
|
71
|
+
// after close; allow the OS to release it before the caller tears down
|
|
72
|
+
// a temporary project tree.
|
|
73
|
+
setTimeout(finish, 500);
|
|
74
|
+
};
|
|
75
|
+
child.once?.("close", finishAfterClose);
|
|
76
|
+
const fallbackTimer = setTimeout(finish, OPENSRC_PROCESS_LIMITS.terminationGraceMs + 1000);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeRunOptions({
|
|
81
|
+
cwd,
|
|
82
|
+
timeoutMs,
|
|
83
|
+
maxStdoutBytes,
|
|
84
|
+
maxStderrBytes,
|
|
85
|
+
spawnImpl,
|
|
86
|
+
env,
|
|
87
|
+
} = {}) {
|
|
88
|
+
assertAbsolutePath(cwd, "OpenSrc project path");
|
|
89
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
90
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc timeout must be a positive integer");
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isSafeInteger(maxStdoutBytes) || maxStdoutBytes < 1) {
|
|
93
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc stdout limit must be a positive integer");
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isSafeInteger(maxStderrBytes) || maxStderrBytes < 1) {
|
|
96
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc stderr limit must be a positive integer");
|
|
97
|
+
}
|
|
98
|
+
if (typeof spawnImpl !== "function") {
|
|
99
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc spawn implementation must be a function");
|
|
100
|
+
}
|
|
101
|
+
if (env !== undefined && (!env || typeof env !== "object" || Array.isArray(env))) {
|
|
102
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc process environment must be an object");
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
cwd,
|
|
106
|
+
timeoutMs,
|
|
107
|
+
maxStdoutBytes,
|
|
108
|
+
maxStderrBytes,
|
|
109
|
+
spawnImpl,
|
|
110
|
+
env,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run one explicitly constructed OpenSrc command.
|
|
116
|
+
*
|
|
117
|
+
* This helper is intentionally private to the adapter boundary: callers pass
|
|
118
|
+
* an absolute executable and an argv array, while this function always keeps
|
|
119
|
+
* shell execution disabled and never returns raw stderr in an error message.
|
|
120
|
+
*/
|
|
121
|
+
export function runOpenSrcCommand(executablePath, args, options = {}) {
|
|
122
|
+
assertAbsolutePath(executablePath, "OpenSrc executable path");
|
|
123
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || /\p{Cc}/u.test(arg))) {
|
|
124
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc arguments must be a portable string array");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const runOptions = normalizeRunOptions({
|
|
128
|
+
cwd: options.cwd,
|
|
129
|
+
timeoutMs: options.timeoutMs,
|
|
130
|
+
maxStdoutBytes: options.maxStdoutBytes ?? OPENSRC_PROCESS_LIMITS.maxStdoutBytes,
|
|
131
|
+
maxStderrBytes: options.maxStderrBytes ?? OPENSRC_PROCESS_LIMITS.maxStderrBytes,
|
|
132
|
+
spawnImpl: options.spawnImpl ?? nodeSpawn,
|
|
133
|
+
env: options.env,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const startedAt = Date.now();
|
|
137
|
+
let child;
|
|
138
|
+
try {
|
|
139
|
+
child = runOptions.spawnImpl(executablePath, [...args], {
|
|
140
|
+
cwd: runOptions.cwd,
|
|
141
|
+
shell: false,
|
|
142
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
143
|
+
...(runOptions.env ? { env: { ...process.env, ...runOptions.env } } : {}),
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw processError(
|
|
147
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
148
|
+
error?.code === "ENOENT" ? "OpenSrc executable is unavailable" : "Unable to start OpenSrc",
|
|
149
|
+
error,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!child || !child.stdout || !child.stderr || typeof child.on !== "function") {
|
|
154
|
+
terminateChild(child);
|
|
155
|
+
throw processError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc process did not expose piped stdout and stderr");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
let stdoutBytes = 0;
|
|
160
|
+
let stderrBytes = 0;
|
|
161
|
+
const stdoutChunks = [];
|
|
162
|
+
let settled = false;
|
|
163
|
+
let timer = null;
|
|
164
|
+
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
if (timer) clearTimeout(timer);
|
|
167
|
+
child.stdout?.removeAllListeners?.("data");
|
|
168
|
+
child.stderr?.removeAllListeners?.("data");
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const fail = (error) => {
|
|
172
|
+
if (settled) return;
|
|
173
|
+
settled = true;
|
|
174
|
+
cleanup();
|
|
175
|
+
terminateChild(child);
|
|
176
|
+
child.stdout?.resume?.();
|
|
177
|
+
child.stderr?.resume?.();
|
|
178
|
+
waitForChildClose(child).then(() => reject(error));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const succeed = (code, signal) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
cleanup();
|
|
185
|
+
if (code !== 0) {
|
|
186
|
+
reject(processError(
|
|
187
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
188
|
+
`OpenSrc exited unsuccessfully (${code ?? "null"}/${signal ?? "none"})`,
|
|
189
|
+
));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
resolve({
|
|
193
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
194
|
+
stderrBytes,
|
|
195
|
+
stdoutBytes,
|
|
196
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
child.stdout.on("data", (chunk) => {
|
|
201
|
+
stdoutBytes += outputBytes(chunk);
|
|
202
|
+
if (stdoutBytes > runOptions.maxStdoutBytes) {
|
|
203
|
+
fail(processError(
|
|
204
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
205
|
+
`OpenSrc stdout exceeded ${runOptions.maxStdoutBytes} bytes`,
|
|
206
|
+
));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
210
|
+
});
|
|
211
|
+
child.stderr.on("data", (chunk) => {
|
|
212
|
+
stderrBytes += outputBytes(chunk);
|
|
213
|
+
if (stderrBytes > runOptions.maxStderrBytes) {
|
|
214
|
+
fail(processError(
|
|
215
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
216
|
+
`OpenSrc stderr exceeded ${runOptions.maxStderrBytes} bytes`,
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
child.on("error", (error) => {
|
|
221
|
+
fail(processError(
|
|
222
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
223
|
+
error?.code === "ENOENT" ? "OpenSrc executable is unavailable" : "OpenSrc process failed",
|
|
224
|
+
error,
|
|
225
|
+
));
|
|
226
|
+
});
|
|
227
|
+
child.on("close", (code, signal) => succeed(code, signal));
|
|
228
|
+
timer = setTimeout(() => {
|
|
229
|
+
fail(processError(
|
|
230
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
231
|
+
`OpenSrc exceeded the ${runOptions.timeoutMs}ms timeout`,
|
|
232
|
+
));
|
|
233
|
+
}, runOptions.timeoutMs);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function parseOpenSrcVersion(stdout) {
|
|
238
|
+
if (typeof stdout !== "string") {
|
|
239
|
+
throw processError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc version output was not text");
|
|
240
|
+
}
|
|
241
|
+
const match = /^\s*opensrc\s+([^\s]+)(?:\s|$)/iu.exec(stdout);
|
|
242
|
+
if (!match) {
|
|
243
|
+
throw processError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc version output did not contain a qualified version");
|
|
244
|
+
}
|
|
245
|
+
return match[1];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { processError as openSrcProcessError };
|