substrate-ai 0.15.0 → 0.16.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/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createLogger } from "../logger-KeHncl-f.js";
|
|
|
4
4
|
import { createEventBus } from "../helpers-CElYrONe.js";
|
|
5
5
|
import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, EXPERIMENT_RESULT, GlobalSettingsSchema, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createDoltClient, createPipelineRun, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initializeDolt, listRequirements, listRunMetrics, loadParentRunDecisions, supersedeDecision, tagRunAsBaseline, updatePipelineRun } from "../dist-CLvAwmT7.js";
|
|
6
6
|
import "../adapter-registry-DXLMTmfD.js";
|
|
7
|
-
import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, listGraphRuns, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-
|
|
7
|
+
import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-z84Uu_U2.js";
|
|
8
8
|
import "../errors-D1LU8CZ9.js";
|
|
9
9
|
import "../routing-CcBOCuC9.js";
|
|
10
10
|
import "../decisions-C0pz9Clx.js";
|
|
@@ -4359,7 +4359,7 @@ async function runSupervisorAction(options, deps = {}) {
|
|
|
4359
4359
|
await initSchema(expAdapter);
|
|
4360
4360
|
const { runRunAction: runPipeline } = await import(
|
|
4361
4361
|
/* @vite-ignore */
|
|
4362
|
-
"../run-
|
|
4362
|
+
"../run-Q2TZstl9.js"
|
|
4363
4363
|
);
|
|
4364
4364
|
const runStoryFn = async (opts) => {
|
|
4365
4365
|
const exitCode = await runPipeline({
|
|
@@ -4998,6 +4998,19 @@ async function runMetricsAction(options) {
|
|
|
4998
4998
|
const execAt = String(r.executed_at).slice(0, 19);
|
|
4999
4999
|
process.stdout.write(` ${String(r.iteration).padStart(3)} ${scoreStr.padStart(7)} ${passesStr.padStart(7)} ${passedTotal.padStart(13)} ${execAt.padEnd(20)}\n`);
|
|
5000
5000
|
}
|
|
5001
|
+
try {
|
|
5002
|
+
const twinRuns = await getTwinRunsForRun(adapter, resolvedRunId);
|
|
5003
|
+
if (twinRuns.length > 0) {
|
|
5004
|
+
process.stdout.write("\nTwins:\n");
|
|
5005
|
+
for (const twin of twinRuns) {
|
|
5006
|
+
const ports = twin.ports.map((p) => `${p.host}:${p.container}`).join(", ");
|
|
5007
|
+
const stoppedAt = twin.stopped_at ?? "still running";
|
|
5008
|
+
process.stdout.write(` ${twin.twin_name} [${twin.status}] ports: ${ports || "none"} started: ${twin.started_at} stopped: ${stoppedAt} health failures: ${twin.health_failure_count}\n`);
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
} catch (err) {
|
|
5012
|
+
logger$10.debug({ err }, "getTwinRunsForRun failed — twin_runs table may not exist yet");
|
|
5013
|
+
}
|
|
5001
5014
|
}
|
|
5002
5015
|
return 0;
|
|
5003
5016
|
}
|
|
@@ -2,7 +2,7 @@ import "./health-DswaC1q5.js";
|
|
|
2
2
|
import "./logger-KeHncl-f.js";
|
|
3
3
|
import "./helpers-CElYrONe.js";
|
|
4
4
|
import "./dist-CLvAwmT7.js";
|
|
5
|
-
import { normalizeGraphSummaryToStatus, registerRunCommand, runRunAction } from "./run-
|
|
5
|
+
import { normalizeGraphSummaryToStatus, registerRunCommand, runRunAction } from "./run-z84Uu_U2.js";
|
|
6
6
|
import "./routing-CcBOCuC9.js";
|
|
7
7
|
import "./decisions-C0pz9Clx.js";
|
|
8
8
|
|
|
@@ -7,12 +7,13 @@ import { access, readFile, readdir, stat } from "fs/promises";
|
|
|
7
7
|
import { EventEmitter } from "node:events";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
import * as actualFS from "node:fs";
|
|
10
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { execFile, execSync, spawn } from "node:child_process";
|
|
12
|
-
import path, { dirname as dirname$1, extname as extname$1, join as join$1, posix, win32 } from "node:path";
|
|
12
|
+
import path, { dirname as dirname$1, extname as extname$1, join as join$1, posix, resolve as resolve$1, win32 } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
13
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
14
15
|
import { z } from "zod";
|
|
15
|
-
import { lstat, mkdir as mkdir$1, readFile as readFile$1, readdir as readdir$1, readlink, realpath, stat as stat$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
16
|
+
import { access as access$1, lstat, mkdir as mkdir$1, readFile as readFile$1, readdir as readdir$1, readlink, realpath, stat as stat$1, unlink, writeFile as writeFile$1 } from "node:fs/promises";
|
|
16
17
|
import { existsSync as existsSync$1, lstatSync, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdir as readdir$2, readdirSync as readdirSync$1, readlinkSync, realpathSync, unlinkSync, writeFileSync as writeFileSync$1 } from "fs";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
18
19
|
import { createHash as createHash$1 } from "crypto";
|
|
@@ -23174,6 +23175,7 @@ function createGraphExecutor() {
|
|
|
23174
23175
|
firstResumedFidelity = "";
|
|
23175
23176
|
const startedAt = Date.now();
|
|
23176
23177
|
let outcome = await dispatchWithRetry(nodeToDispatch, context, graph, config, nodeRetries);
|
|
23178
|
+
if (Date.now() - startedAt < 50) await new Promise((r) => setTimeout(r, 50));
|
|
23177
23179
|
if (outcome.status === "PARTIAL_SUCCESS" && !nodeToDispatch.allowPartial) outcome = {
|
|
23178
23180
|
...outcome,
|
|
23179
23181
|
status: "FAIL",
|
|
@@ -27315,7 +27317,7 @@ var Pattern = class Pattern {
|
|
|
27315
27317
|
#isUNC;
|
|
27316
27318
|
#isAbsolute;
|
|
27317
27319
|
#followGlobstar = true;
|
|
27318
|
-
constructor(patternList, globList, index, platform) {
|
|
27320
|
+
constructor(patternList, globList, index, platform$1) {
|
|
27319
27321
|
if (!isPatternList(patternList)) throw new TypeError("empty pattern list");
|
|
27320
27322
|
if (!isGlobList(globList)) throw new TypeError("empty glob list");
|
|
27321
27323
|
if (globList.length !== patternList.length) throw new TypeError("mismatched pattern list and glob list lengths");
|
|
@@ -27324,7 +27326,7 @@ var Pattern = class Pattern {
|
|
|
27324
27326
|
this.#patternList = patternList;
|
|
27325
27327
|
this.#globList = globList;
|
|
27326
27328
|
this.#index = index;
|
|
27327
|
-
this.#platform = platform;
|
|
27329
|
+
this.#platform = platform$1;
|
|
27328
27330
|
if (this.#index === 0) {
|
|
27329
27331
|
if (this.isUNC()) {
|
|
27330
27332
|
const [p0, p1, p2, p3, ...prest] = this.#patternList;
|
|
@@ -27471,12 +27473,12 @@ var Ignore = class {
|
|
|
27471
27473
|
absoluteChildren;
|
|
27472
27474
|
platform;
|
|
27473
27475
|
mmopts;
|
|
27474
|
-
constructor(ignored, { nobrace, nocase, noext, noglobstar, platform = defaultPlatform$1 }) {
|
|
27476
|
+
constructor(ignored, { nobrace, nocase, noext, noglobstar, platform: platform$1 = defaultPlatform$1 }) {
|
|
27475
27477
|
this.relative = [];
|
|
27476
27478
|
this.absolute = [];
|
|
27477
27479
|
this.relativeChildren = [];
|
|
27478
27480
|
this.absoluteChildren = [];
|
|
27479
|
-
this.platform = platform;
|
|
27481
|
+
this.platform = platform$1;
|
|
27480
27482
|
this.mmopts = {
|
|
27481
27483
|
dot: true,
|
|
27482
27484
|
nobrace,
|
|
@@ -27484,7 +27486,7 @@ var Ignore = class {
|
|
|
27484
27486
|
noext,
|
|
27485
27487
|
noglobstar,
|
|
27486
27488
|
optimizationLevel: 2,
|
|
27487
|
-
platform,
|
|
27489
|
+
platform: platform$1,
|
|
27488
27490
|
nocomment: true,
|
|
27489
27491
|
nonegate: true
|
|
27490
27492
|
};
|
|
@@ -28375,14 +28377,24 @@ function getExecutionCommand(filePath) {
|
|
|
28375
28377
|
}
|
|
28376
28378
|
/**
|
|
28377
28379
|
* Run a single scenario file and return its result.
|
|
28380
|
+
*
|
|
28381
|
+
* @param entry - Scenario entry with name and path.
|
|
28382
|
+
* @param projectRoot - Working directory for scenario execution.
|
|
28383
|
+
* @param env - Optional extra environment variables to inject into the subprocess.
|
|
28384
|
+
* When provided, merged on top of `process.env`.
|
|
28378
28385
|
*/
|
|
28379
|
-
function runScenario(entry, projectRoot) {
|
|
28386
|
+
function runScenario(entry, projectRoot, env) {
|
|
28380
28387
|
return new Promise((resolve$2) => {
|
|
28381
28388
|
const startTime = Date.now();
|
|
28382
28389
|
const command = getExecutionCommand(entry.path);
|
|
28390
|
+
const spawnEnv = env != null ? {
|
|
28391
|
+
...process.env,
|
|
28392
|
+
...env
|
|
28393
|
+
} : void 0;
|
|
28383
28394
|
const child = spawn$1(command, [], {
|
|
28384
28395
|
cwd: projectRoot,
|
|
28385
|
-
shell: true
|
|
28396
|
+
shell: true,
|
|
28397
|
+
env: spawnEnv
|
|
28386
28398
|
});
|
|
28387
28399
|
let stdoutBuf = "";
|
|
28388
28400
|
let stderrBuf = "";
|
|
@@ -28426,25 +28438,93 @@ function runScenario(entry, projectRoot) {
|
|
|
28426
28438
|
});
|
|
28427
28439
|
}
|
|
28428
28440
|
/**
|
|
28441
|
+
* Build a ScenarioRunResult where every scenario is marked as failed due to
|
|
28442
|
+
* a twin startup error. No scripts were executed.
|
|
28443
|
+
*/
|
|
28444
|
+
function buildStartupFailureResult(scenarioEntries, err) {
|
|
28445
|
+
const scenarios = scenarioEntries.map((entry) => ({
|
|
28446
|
+
name: entry.name,
|
|
28447
|
+
status: "fail",
|
|
28448
|
+
exitCode: -1,
|
|
28449
|
+
stdout: "",
|
|
28450
|
+
stderr: err.message,
|
|
28451
|
+
durationMs: 0
|
|
28452
|
+
}));
|
|
28453
|
+
return {
|
|
28454
|
+
scenarios,
|
|
28455
|
+
summary: {
|
|
28456
|
+
total: scenarios.length,
|
|
28457
|
+
passed: 0,
|
|
28458
|
+
failed: scenarios.length
|
|
28459
|
+
},
|
|
28460
|
+
durationMs: 0
|
|
28461
|
+
};
|
|
28462
|
+
}
|
|
28463
|
+
/**
|
|
28429
28464
|
* Create a ScenarioRunner that executes all scenarios in a manifest.
|
|
28430
28465
|
*
|
|
28431
|
-
* @param
|
|
28466
|
+
* @param options - Optional configuration including twin coordinator and timeout.
|
|
28432
28467
|
*/
|
|
28433
|
-
function createScenarioRunner(
|
|
28468
|
+
function createScenarioRunner(options) {
|
|
28434
28469
|
return { async run(manifest, projectRoot) {
|
|
28435
28470
|
const startTime = Date.now();
|
|
28436
|
-
const
|
|
28437
|
-
const
|
|
28438
|
-
|
|
28439
|
-
|
|
28440
|
-
|
|
28441
|
-
|
|
28442
|
-
|
|
28443
|
-
|
|
28444
|
-
|
|
28445
|
-
|
|
28446
|
-
|
|
28447
|
-
|
|
28471
|
+
const { twinCoordinator } = options ?? {};
|
|
28472
|
+
const requiresTwins = (manifest.twins?.length ?? 0) > 0 && twinCoordinator != null;
|
|
28473
|
+
if (!requiresTwins) {
|
|
28474
|
+
if (options?.twinHealthMonitor) {
|
|
28475
|
+
const healthStatus = options.twinHealthMonitor.getStatus();
|
|
28476
|
+
const unhealthyNames = Object.entries(healthStatus).filter(([, s$1]) => s$1 === "unhealthy").map(([name]) => name);
|
|
28477
|
+
if (unhealthyNames.length > 0) {
|
|
28478
|
+
const msg = unhealthyNames.map((n$1) => `Twin '${n$1}' is unhealthy`).join("; ");
|
|
28479
|
+
return buildStartupFailureResult(manifest.scenarios, new Error(msg));
|
|
28480
|
+
}
|
|
28481
|
+
}
|
|
28482
|
+
const scenarios = await Promise.all(manifest.scenarios.map((entry) => runScenario(entry, projectRoot)));
|
|
28483
|
+
const passed = scenarios.filter((s$1) => s$1.status === "pass").length;
|
|
28484
|
+
const failed = scenarios.filter((s$1) => s$1.status === "fail").length;
|
|
28485
|
+
return {
|
|
28486
|
+
scenarios,
|
|
28487
|
+
summary: {
|
|
28488
|
+
total: scenarios.length,
|
|
28489
|
+
passed,
|
|
28490
|
+
failed
|
|
28491
|
+
},
|
|
28492
|
+
durationMs: Date.now() - startTime
|
|
28493
|
+
};
|
|
28494
|
+
}
|
|
28495
|
+
let twinEnv;
|
|
28496
|
+
try {
|
|
28497
|
+
twinEnv = await twinCoordinator.startTwins(manifest.twins);
|
|
28498
|
+
} catch (err) {
|
|
28499
|
+
return buildStartupFailureResult(manifest.scenarios, err);
|
|
28500
|
+
}
|
|
28501
|
+
if (options?.twinHealthMonitor) {
|
|
28502
|
+
const healthStatus = options.twinHealthMonitor.getStatus();
|
|
28503
|
+
const unhealthyNames = Object.entries(healthStatus).filter(([, s$1]) => s$1 === "unhealthy").map(([name]) => name);
|
|
28504
|
+
if (unhealthyNames.length > 0) {
|
|
28505
|
+
const msg = unhealthyNames.map((n$1) => `Twin '${n$1}' is unhealthy`).join("; ");
|
|
28506
|
+
try {
|
|
28507
|
+
await twinCoordinator.stopTwins();
|
|
28508
|
+
} catch {}
|
|
28509
|
+
return buildStartupFailureResult(manifest.scenarios, new Error(msg));
|
|
28510
|
+
}
|
|
28511
|
+
}
|
|
28512
|
+
try {
|
|
28513
|
+
const scenarios = await Promise.all(manifest.scenarios.map((entry) => runScenario(entry, projectRoot, twinEnv)));
|
|
28514
|
+
const passed = scenarios.filter((s$1) => s$1.status === "pass").length;
|
|
28515
|
+
const failed = scenarios.filter((s$1) => s$1.status === "fail").length;
|
|
28516
|
+
return {
|
|
28517
|
+
scenarios,
|
|
28518
|
+
summary: {
|
|
28519
|
+
total: scenarios.length,
|
|
28520
|
+
passed,
|
|
28521
|
+
failed
|
|
28522
|
+
},
|
|
28523
|
+
durationMs: Date.now() - startTime
|
|
28524
|
+
};
|
|
28525
|
+
} finally {
|
|
28526
|
+
await twinCoordinator.stopTwins();
|
|
28527
|
+
}
|
|
28448
28528
|
} };
|
|
28449
28529
|
}
|
|
28450
28530
|
|
|
@@ -28487,6 +28567,455 @@ function registerScenariosCommand(program) {
|
|
|
28487
28567
|
});
|
|
28488
28568
|
}
|
|
28489
28569
|
|
|
28570
|
+
//#endregion
|
|
28571
|
+
//#region packages/factory/dist/twins/schema.js
|
|
28572
|
+
/**
|
|
28573
|
+
* Validates and transforms a "host:container" port string into a PortMapping object.
|
|
28574
|
+
*/
|
|
28575
|
+
const portMappingStringSchema = z.string().regex(/^\d+:\d+$/, "Port mapping must be in \"host:container\" format (e.g., \"5432:5432\")").transform((val) => {
|
|
28576
|
+
const parts = val.split(":");
|
|
28577
|
+
const host = Number(parts[0]);
|
|
28578
|
+
const container = Number(parts[1]);
|
|
28579
|
+
return {
|
|
28580
|
+
host,
|
|
28581
|
+
container
|
|
28582
|
+
};
|
|
28583
|
+
});
|
|
28584
|
+
/**
|
|
28585
|
+
* Validates a healthcheck configuration object.
|
|
28586
|
+
*/
|
|
28587
|
+
const twinHealthcheckSchema = z.object({
|
|
28588
|
+
url: z.string().url("Healthcheck URL must be a valid URL"),
|
|
28589
|
+
interval_ms: z.number().int().positive().default(500),
|
|
28590
|
+
timeout_ms: z.number().int().positive().default(1e4)
|
|
28591
|
+
});
|
|
28592
|
+
/**
|
|
28593
|
+
* Validates a full twin definition. Unknown fields are rejected via `.strict()`.
|
|
28594
|
+
*/
|
|
28595
|
+
const twinDefinitionSchema = z.object({
|
|
28596
|
+
name: z.string().min(1, "Twin name must not be empty"),
|
|
28597
|
+
image: z.string().min(1, "Twin image must not be empty"),
|
|
28598
|
+
ports: z.array(portMappingStringSchema).default([]),
|
|
28599
|
+
environment: z.record(z.string(), z.string()).default({}),
|
|
28600
|
+
healthcheck: twinHealthcheckSchema.optional()
|
|
28601
|
+
}).strict();
|
|
28602
|
+
const TwinDefinitionSchema = twinDefinitionSchema;
|
|
28603
|
+
|
|
28604
|
+
//#endregion
|
|
28605
|
+
//#region packages/factory/dist/twins/types.js
|
|
28606
|
+
/**
|
|
28607
|
+
* Twin Registry — Type definitions for TwinDefinition and related interfaces.
|
|
28608
|
+
*
|
|
28609
|
+
* Story 47-1.
|
|
28610
|
+
*/
|
|
28611
|
+
/**
|
|
28612
|
+
* Thrown when a single twin definition file fails validation or parsing.
|
|
28613
|
+
*/
|
|
28614
|
+
var TwinDefinitionError = class extends Error {
|
|
28615
|
+
sourceFile;
|
|
28616
|
+
constructor(message, sourceFile) {
|
|
28617
|
+
super(message);
|
|
28618
|
+
this.sourceFile = sourceFile;
|
|
28619
|
+
this.name = "TwinDefinitionError";
|
|
28620
|
+
}
|
|
28621
|
+
};
|
|
28622
|
+
/**
|
|
28623
|
+
* Thrown when a registry-level constraint is violated (e.g., duplicate twin names).
|
|
28624
|
+
*/
|
|
28625
|
+
var TwinRegistryError = class extends Error {
|
|
28626
|
+
constructor(message) {
|
|
28627
|
+
super(message);
|
|
28628
|
+
this.name = "TwinRegistryError";
|
|
28629
|
+
}
|
|
28630
|
+
};
|
|
28631
|
+
|
|
28632
|
+
//#endregion
|
|
28633
|
+
//#region packages/factory/dist/twins/registry.js
|
|
28634
|
+
var TwinRegistry = class {
|
|
28635
|
+
_twins = new Map();
|
|
28636
|
+
/**
|
|
28637
|
+
* Discovers and validates all *.yaml and *.yml twin definition files in the given directory.
|
|
28638
|
+
* Non-recursive — only top-level files are processed.
|
|
28639
|
+
*
|
|
28640
|
+
* @throws {TwinDefinitionError} if a file contains invalid YAML or fails schema validation
|
|
28641
|
+
* @throws {TwinRegistryError} if two files declare the same twin name
|
|
28642
|
+
*/
|
|
28643
|
+
async discover(dir) {
|
|
28644
|
+
let entries;
|
|
28645
|
+
try {
|
|
28646
|
+
entries = await readdir$1(dir);
|
|
28647
|
+
} catch (err) {
|
|
28648
|
+
throw new TwinDefinitionError(`Failed to read directory: ${dir} — ${err.message}`);
|
|
28649
|
+
}
|
|
28650
|
+
const yamlFiles = entries.filter((f$1) => f$1.endsWith(".yaml") || f$1.endsWith(".yml"));
|
|
28651
|
+
const perFileErrors = [];
|
|
28652
|
+
for (const filename of yamlFiles) {
|
|
28653
|
+
const filePath = resolve$1(join$1(dir, filename));
|
|
28654
|
+
let raw;
|
|
28655
|
+
try {
|
|
28656
|
+
raw = await readFile$1(filePath, "utf-8");
|
|
28657
|
+
} catch (err) {
|
|
28658
|
+
perFileErrors.push(new TwinDefinitionError(`Failed to read file: ${filePath} — ${err.message}`, filePath));
|
|
28659
|
+
continue;
|
|
28660
|
+
}
|
|
28661
|
+
let parsed;
|
|
28662
|
+
try {
|
|
28663
|
+
parsed = yaml.load(raw);
|
|
28664
|
+
} catch (err) {
|
|
28665
|
+
perFileErrors.push(new TwinDefinitionError(`Twin definition at ${filePath} contains invalid YAML: ${err.message}`, filePath));
|
|
28666
|
+
continue;
|
|
28667
|
+
}
|
|
28668
|
+
const result = TwinDefinitionSchema.safeParse(parsed);
|
|
28669
|
+
if (!result.success) {
|
|
28670
|
+
const firstIssue = result.error.issues[0];
|
|
28671
|
+
const fieldPath = firstIssue?.path?.join(".") ?? "unknown";
|
|
28672
|
+
const fieldMessage = firstIssue?.message ?? result.error.message;
|
|
28673
|
+
let message;
|
|
28674
|
+
const issueCode = firstIssue?.code;
|
|
28675
|
+
if (issueCode === "unrecognized_keys") {
|
|
28676
|
+
const keys = firstIssue.keys ?? [];
|
|
28677
|
+
message = `Twin definition at ${filePath} contains unrecognised field(s): ${keys.join(", ")}`;
|
|
28678
|
+
} else if (issueCode === "unrecognized_key") {
|
|
28679
|
+
const key = firstIssue.key ?? fieldPath;
|
|
28680
|
+
message = `Twin definition at ${filePath} contains unrecognised field(s): ${key}`;
|
|
28681
|
+
} else {
|
|
28682
|
+
const parsedObj = parsed;
|
|
28683
|
+
const isMissingField = parsedObj && typeof parsedObj === "object" && fieldPath !== "unknown" && !(fieldPath in parsedObj);
|
|
28684
|
+
if (isMissingField) message = `Twin definition at ${filePath} is missing required field: ${fieldPath}`;
|
|
28685
|
+
else message = `Twin definition at ${filePath} failed validation — ${fieldPath}: ${fieldMessage}`;
|
|
28686
|
+
}
|
|
28687
|
+
perFileErrors.push(new TwinDefinitionError(message, filePath));
|
|
28688
|
+
continue;
|
|
28689
|
+
}
|
|
28690
|
+
const data = result.data;
|
|
28691
|
+
const twin = {
|
|
28692
|
+
name: data.name,
|
|
28693
|
+
image: data.image,
|
|
28694
|
+
ports: data.ports,
|
|
28695
|
+
environment: data.environment,
|
|
28696
|
+
sourceFile: filePath,
|
|
28697
|
+
...data.healthcheck !== void 0 && { healthcheck: data.healthcheck }
|
|
28698
|
+
};
|
|
28699
|
+
if (this._twins.has(twin.name)) {
|
|
28700
|
+
const existing = this._twins.get(twin.name);
|
|
28701
|
+
throw new TwinRegistryError(`Duplicate twin name "${twin.name}" found in: ${existing.sourceFile} and ${filePath}`);
|
|
28702
|
+
}
|
|
28703
|
+
this._twins.set(twin.name, twin);
|
|
28704
|
+
}
|
|
28705
|
+
if (perFileErrors.length > 0) throw perFileErrors[0];
|
|
28706
|
+
}
|
|
28707
|
+
/**
|
|
28708
|
+
* Returns all discovered twin definitions.
|
|
28709
|
+
*/
|
|
28710
|
+
list() {
|
|
28711
|
+
return Array.from(this._twins.values());
|
|
28712
|
+
}
|
|
28713
|
+
/**
|
|
28714
|
+
* Returns a twin definition by name, or undefined if not found.
|
|
28715
|
+
*/
|
|
28716
|
+
get(name) {
|
|
28717
|
+
return this._twins.get(name);
|
|
28718
|
+
}
|
|
28719
|
+
/**
|
|
28720
|
+
* Polls the health endpoint of a twin definition until healthy or timed out.
|
|
28721
|
+
*
|
|
28722
|
+
* @param twin - The twin definition to poll
|
|
28723
|
+
* @param options - Optional overrides (e.g., mock fetch for testing)
|
|
28724
|
+
* @returns HealthPollResult indicating success or timeout
|
|
28725
|
+
*/
|
|
28726
|
+
async pollHealth(twin, options) {
|
|
28727
|
+
const { healthcheck } = twin;
|
|
28728
|
+
if (!healthcheck) return {
|
|
28729
|
+
healthy: true,
|
|
28730
|
+
attempts: 0
|
|
28731
|
+
};
|
|
28732
|
+
const fetchFn = options?.fetch ?? globalThis.fetch;
|
|
28733
|
+
const { url, interval_ms = 500, timeout_ms = 1e4 } = healthcheck;
|
|
28734
|
+
const startTime = Date.now();
|
|
28735
|
+
let attempts = 0;
|
|
28736
|
+
while (true) {
|
|
28737
|
+
attempts++;
|
|
28738
|
+
try {
|
|
28739
|
+
const response = await fetchFn(url);
|
|
28740
|
+
if (response.ok) return {
|
|
28741
|
+
healthy: true,
|
|
28742
|
+
attempts
|
|
28743
|
+
};
|
|
28744
|
+
} catch {}
|
|
28745
|
+
const elapsed = Date.now() - startTime;
|
|
28746
|
+
if (elapsed >= timeout_ms) return {
|
|
28747
|
+
healthy: false,
|
|
28748
|
+
error: `Health check timed out after ${timeout_ms}ms`
|
|
28749
|
+
};
|
|
28750
|
+
await new Promise((resolve$2) => setTimeout(resolve$2, interval_ms));
|
|
28751
|
+
if (Date.now() - startTime >= timeout_ms) return {
|
|
28752
|
+
healthy: false,
|
|
28753
|
+
error: `Health check timed out after ${timeout_ms}ms`
|
|
28754
|
+
};
|
|
28755
|
+
}
|
|
28756
|
+
}
|
|
28757
|
+
};
|
|
28758
|
+
/**
|
|
28759
|
+
* Factory function that creates a new TwinRegistry instance.
|
|
28760
|
+
*/
|
|
28761
|
+
function createTwinRegistry() {
|
|
28762
|
+
return new TwinRegistry();
|
|
28763
|
+
}
|
|
28764
|
+
|
|
28765
|
+
//#endregion
|
|
28766
|
+
//#region packages/factory/dist/twins/docker-compose.js
|
|
28767
|
+
/**
|
|
28768
|
+
* Thrown when Docker is unavailable or the compose lifecycle fails.
|
|
28769
|
+
*/
|
|
28770
|
+
var TwinError = class extends Error {
|
|
28771
|
+
constructor(message) {
|
|
28772
|
+
super(message);
|
|
28773
|
+
this.name = "TwinError";
|
|
28774
|
+
}
|
|
28775
|
+
};
|
|
28776
|
+
/**
|
|
28777
|
+
* Generates a Docker Compose v3.8 YAML string from an array of twin definitions.
|
|
28778
|
+
* Built manually with template strings — no external YAML library required.
|
|
28779
|
+
*/
|
|
28780
|
+
function generateComposeYaml(twins) {
|
|
28781
|
+
const lines = [];
|
|
28782
|
+
lines.push("version: '3.8'");
|
|
28783
|
+
lines.push("services:");
|
|
28784
|
+
for (const twin of twins) {
|
|
28785
|
+
lines.push(` ${twin.name}:`);
|
|
28786
|
+
lines.push(` image: ${twin.image}`);
|
|
28787
|
+
if (twin.ports.length > 0) {
|
|
28788
|
+
lines.push(" ports:");
|
|
28789
|
+
for (const port of twin.ports) lines.push(` - "${port.host}:${port.container}"`);
|
|
28790
|
+
}
|
|
28791
|
+
if (twin.environment && Object.keys(twin.environment).length > 0) {
|
|
28792
|
+
lines.push(" environment:");
|
|
28793
|
+
for (const [key, value] of Object.entries(twin.environment)) lines.push(` ${key}: ${value}`);
|
|
28794
|
+
}
|
|
28795
|
+
}
|
|
28796
|
+
return lines.join("\n") + "\n";
|
|
28797
|
+
}
|
|
28798
|
+
/**
|
|
28799
|
+
* Polls a twin's health endpoint until it returns a 2xx response or exhausts attempts.
|
|
28800
|
+
*
|
|
28801
|
+
* Algorithm (from story 47-2 dev notes):
|
|
28802
|
+
* attempts = 0
|
|
28803
|
+
* while attempts < maxAttempts:
|
|
28804
|
+
* try fetch → if ok, return
|
|
28805
|
+
* catch (connection refused) → continue
|
|
28806
|
+
* attempts++
|
|
28807
|
+
* sleep(intervalMs)
|
|
28808
|
+
* throw TwinError
|
|
28809
|
+
*/
|
|
28810
|
+
async function pollTwinHealth(twin, maxAttempts, healthIntervalMs) {
|
|
28811
|
+
if (!twin.healthcheck?.url) return;
|
|
28812
|
+
const url = twin.healthcheck.url;
|
|
28813
|
+
const intervalMs = twin.healthcheck.interval_ms ?? healthIntervalMs;
|
|
28814
|
+
let attempts = 0;
|
|
28815
|
+
while (attempts < maxAttempts) {
|
|
28816
|
+
try {
|
|
28817
|
+
const response = await fetch(url);
|
|
28818
|
+
if (response.ok) return;
|
|
28819
|
+
} catch {}
|
|
28820
|
+
attempts++;
|
|
28821
|
+
if (attempts < maxAttempts) await new Promise((resolve$2) => setTimeout(resolve$2, intervalMs));
|
|
28822
|
+
}
|
|
28823
|
+
throw new TwinError(`Twin '${twin.name}' failed health check after ${maxAttempts} attempts`);
|
|
28824
|
+
}
|
|
28825
|
+
/**
|
|
28826
|
+
* Creates a TwinManager that orchestrates Docker Compose for digital twin services.
|
|
28827
|
+
*
|
|
28828
|
+
* The event bus is injected — do NOT import a global singleton or create one internally.
|
|
28829
|
+
*
|
|
28830
|
+
* @param eventBus - Typed event bus for emitting twin lifecycle events
|
|
28831
|
+
* @param options - Optional health check configuration
|
|
28832
|
+
* @returns TwinManager with start() and stop() methods
|
|
28833
|
+
*/
|
|
28834
|
+
function createTwinManager(eventBus, options) {
|
|
28835
|
+
const maxHealthAttempts = options?.maxHealthAttempts ?? 30;
|
|
28836
|
+
const healthIntervalMs = options?.healthIntervalMs ?? 1e3;
|
|
28837
|
+
/** Temp directory containing the generated docker-compose.yml. Null when not started. */
|
|
28838
|
+
let composeDir = null;
|
|
28839
|
+
/** Twins that were passed to start() — used by stop() to emit twin:stopped events. */
|
|
28840
|
+
let startedTwins = [];
|
|
28841
|
+
return {
|
|
28842
|
+
async start(twins) {
|
|
28843
|
+
try {
|
|
28844
|
+
execSync("docker info", { stdio: "ignore" });
|
|
28845
|
+
} catch {
|
|
28846
|
+
throw new TwinError("Docker not found — twins require Docker");
|
|
28847
|
+
}
|
|
28848
|
+
const yaml$1 = generateComposeYaml(twins);
|
|
28849
|
+
const dir = join$1(tmpdir(), randomUUID());
|
|
28850
|
+
mkdirSync(dir, { recursive: true });
|
|
28851
|
+
writeFileSync(join$1(dir, "docker-compose.yml"), yaml$1, "utf-8");
|
|
28852
|
+
composeDir = dir;
|
|
28853
|
+
startedTwins = twins;
|
|
28854
|
+
try {
|
|
28855
|
+
execSync("docker compose up -d", {
|
|
28856
|
+
cwd: dir,
|
|
28857
|
+
stdio: "pipe"
|
|
28858
|
+
});
|
|
28859
|
+
} catch (err) {
|
|
28860
|
+
const error = err;
|
|
28861
|
+
const stderr = error.stderr?.toString() ?? "";
|
|
28862
|
+
throw new TwinError(`docker compose up failed: ${stderr}`);
|
|
28863
|
+
}
|
|
28864
|
+
for (const twin of twins) await pollTwinHealth(twin, maxHealthAttempts, healthIntervalMs);
|
|
28865
|
+
for (const twin of twins) eventBus.emit("twin:started", {
|
|
28866
|
+
twinName: twin.name,
|
|
28867
|
+
ports: twin.ports,
|
|
28868
|
+
healthStatus: "healthy"
|
|
28869
|
+
});
|
|
28870
|
+
},
|
|
28871
|
+
async stop() {
|
|
28872
|
+
if (!composeDir) return;
|
|
28873
|
+
const dir = composeDir;
|
|
28874
|
+
try {
|
|
28875
|
+
execSync("docker compose down --remove-orphans", {
|
|
28876
|
+
cwd: dir,
|
|
28877
|
+
stdio: "pipe"
|
|
28878
|
+
});
|
|
28879
|
+
} catch {}
|
|
28880
|
+
rmSync(dir, {
|
|
28881
|
+
recursive: true,
|
|
28882
|
+
force: true
|
|
28883
|
+
});
|
|
28884
|
+
for (const twin of startedTwins) eventBus.emit("twin:stopped", { twinName: twin.name });
|
|
28885
|
+
composeDir = null;
|
|
28886
|
+
startedTwins = [];
|
|
28887
|
+
},
|
|
28888
|
+
getComposeDir() {
|
|
28889
|
+
return composeDir;
|
|
28890
|
+
}
|
|
28891
|
+
};
|
|
28892
|
+
}
|
|
28893
|
+
|
|
28894
|
+
//#endregion
|
|
28895
|
+
//#region packages/factory/dist/twins/templates.js
|
|
28896
|
+
/**
|
|
28897
|
+
* Twin Template Catalog — pre-built twin definition templates for common external services.
|
|
28898
|
+
*
|
|
28899
|
+
* Story 47-4.
|
|
28900
|
+
*/
|
|
28901
|
+
const localstackTemplate = {
|
|
28902
|
+
name: "localstack",
|
|
28903
|
+
description: "LocalStack — AWS cloud service emulator (S3, SQS, DynamoDB)",
|
|
28904
|
+
definition: {
|
|
28905
|
+
name: "localstack",
|
|
28906
|
+
image: "localstack/localstack:latest",
|
|
28907
|
+
ports: ["4566:4566"],
|
|
28908
|
+
environment: { SERVICES: "s3,sqs,dynamodb" },
|
|
28909
|
+
healthcheck: {
|
|
28910
|
+
url: "http://localhost:4566/_localstack/health",
|
|
28911
|
+
interval_ms: 500,
|
|
28912
|
+
timeout_ms: 1e4
|
|
28913
|
+
}
|
|
28914
|
+
}
|
|
28915
|
+
};
|
|
28916
|
+
const wiremockTemplate = {
|
|
28917
|
+
name: "wiremock",
|
|
28918
|
+
description: "WireMock — HTTP API mock and stub server",
|
|
28919
|
+
definition: {
|
|
28920
|
+
name: "wiremock",
|
|
28921
|
+
image: "wiremock/wiremock:latest",
|
|
28922
|
+
ports: ["8080:8080"],
|
|
28923
|
+
environment: {},
|
|
28924
|
+
healthcheck: {
|
|
28925
|
+
url: "http://localhost:8080/__admin/health",
|
|
28926
|
+
interval_ms: 500,
|
|
28927
|
+
timeout_ms: 1e4
|
|
28928
|
+
}
|
|
28929
|
+
}
|
|
28930
|
+
};
|
|
28931
|
+
/**
|
|
28932
|
+
* Map of all built-in twin templates, keyed by template name.
|
|
28933
|
+
*/
|
|
28934
|
+
const TWIN_TEMPLATES = new Map([[localstackTemplate.name, localstackTemplate], [wiremockTemplate.name, wiremockTemplate]]);
|
|
28935
|
+
/**
|
|
28936
|
+
* Returns all available twin template entries.
|
|
28937
|
+
*/
|
|
28938
|
+
function listTwinTemplates() {
|
|
28939
|
+
return Array.from(TWIN_TEMPLATES.values());
|
|
28940
|
+
}
|
|
28941
|
+
/**
|
|
28942
|
+
* Returns the twin template entry for the given name, or `undefined` if not found.
|
|
28943
|
+
*/
|
|
28944
|
+
function getTwinTemplate(name) {
|
|
28945
|
+
return TWIN_TEMPLATES.get(name);
|
|
28946
|
+
}
|
|
28947
|
+
|
|
28948
|
+
//#endregion
|
|
28949
|
+
//#region packages/factory/dist/twins/run-state.js
|
|
28950
|
+
/**
|
|
28951
|
+
* Returns the absolute path to the run-state JSON file for the given project.
|
|
28952
|
+
*
|
|
28953
|
+
* @param projectDir - Absolute path to the project root (usually `process.cwd()`)
|
|
28954
|
+
*/
|
|
28955
|
+
function runStatePath(projectDir) {
|
|
28956
|
+
return path.join(projectDir, ".substrate", "twins", ".run-state.json");
|
|
28957
|
+
}
|
|
28958
|
+
/**
|
|
28959
|
+
* Reads and JSON-parses the run-state file.
|
|
28960
|
+
*
|
|
28961
|
+
* @returns The parsed `TwinRunState`, or `null` if the file does not exist.
|
|
28962
|
+
* @throws If any I/O error other than ENOENT occurs, or if the JSON is invalid.
|
|
28963
|
+
*/
|
|
28964
|
+
async function readRunState(projectDir) {
|
|
28965
|
+
const filePath = runStatePath(projectDir);
|
|
28966
|
+
try {
|
|
28967
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
28968
|
+
return JSON.parse(content);
|
|
28969
|
+
} catch (err) {
|
|
28970
|
+
if (err.code === "ENOENT") return null;
|
|
28971
|
+
throw err;
|
|
28972
|
+
}
|
|
28973
|
+
}
|
|
28974
|
+
/**
|
|
28975
|
+
* Writes the given run state to disk, creating parent directories as needed.
|
|
28976
|
+
*
|
|
28977
|
+
* @param projectDir - Absolute path to the project root
|
|
28978
|
+
* @param state - The `TwinRunState` to persist
|
|
28979
|
+
*/
|
|
28980
|
+
async function writeRunState(projectDir, state) {
|
|
28981
|
+
const filePath = runStatePath(projectDir);
|
|
28982
|
+
await mkdir$1(path.dirname(filePath), { recursive: true });
|
|
28983
|
+
await writeFile$1(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
28984
|
+
}
|
|
28985
|
+
/**
|
|
28986
|
+
* Deletes the run-state file. No-op if the file does not exist.
|
|
28987
|
+
*
|
|
28988
|
+
* @param projectDir - Absolute path to the project root
|
|
28989
|
+
*/
|
|
28990
|
+
async function clearRunState(projectDir) {
|
|
28991
|
+
const filePath = runStatePath(projectDir);
|
|
28992
|
+
try {
|
|
28993
|
+
await unlink(filePath);
|
|
28994
|
+
} catch (err) {
|
|
28995
|
+
if (err.code !== "ENOENT") throw err;
|
|
28996
|
+
}
|
|
28997
|
+
}
|
|
28998
|
+
|
|
28999
|
+
//#endregion
|
|
29000
|
+
//#region packages/factory/dist/twins/persistence.js
|
|
29001
|
+
/**
|
|
29002
|
+
* Retrieve all twin run summaries for a given run_id, with health failure counts.
|
|
29003
|
+
*
|
|
29004
|
+
* Uses two portable queries and merges results client-side.
|
|
29005
|
+
*
|
|
29006
|
+
* @returns Array of TwinRunSummary — empty array if no twins found for this run.
|
|
29007
|
+
*/
|
|
29008
|
+
async function getTwinRunsForRun(adapter, runId) {
|
|
29009
|
+
const rows = await adapter.query("SELECT * FROM twin_runs WHERE run_id = ?", [runId]);
|
|
29010
|
+
const failureCounts = await adapter.query("SELECT twin_name, COUNT(*) as cnt FROM twin_health_failures WHERE run_id = ? GROUP BY twin_name", [runId]);
|
|
29011
|
+
const failureMap = new Map(failureCounts.map((r) => [r.twin_name, r.cnt]));
|
|
29012
|
+
return rows.map((row) => ({
|
|
29013
|
+
...row,
|
|
29014
|
+
ports: row.ports_json ? JSON.parse(row.ports_json) : [],
|
|
29015
|
+
health_failure_count: failureMap.get(row.twin_name) ?? 0
|
|
29016
|
+
}));
|
|
29017
|
+
}
|
|
29018
|
+
|
|
28490
29019
|
//#endregion
|
|
28491
29020
|
//#region packages/factory/dist/config.js
|
|
28492
29021
|
/**
|
|
@@ -28500,7 +29029,7 @@ const FactoryConfigSchema = z.object({
|
|
|
28500
29029
|
scenario_dir: z.string().default(".substrate/scenarios/"),
|
|
28501
29030
|
satisfaction_threshold: z.number().min(0).max(1).default(.8),
|
|
28502
29031
|
budget_cap_usd: z.number().min(0).default(0),
|
|
28503
|
-
wall_clock_cap_seconds: z.number().min(0).default(
|
|
29032
|
+
wall_clock_cap_seconds: z.number().min(0).default(3600),
|
|
28504
29033
|
plateau_window: z.number().int().min(2).default(3),
|
|
28505
29034
|
plateau_threshold: z.number().min(0).max(1).default(.05),
|
|
28506
29035
|
backend: z.enum(["cli", "direct"]).default("cli"),
|
|
@@ -28610,6 +29139,27 @@ async function factorySchema(adapter) {
|
|
|
28610
29139
|
)
|
|
28611
29140
|
`);
|
|
28612
29141
|
await adapter.exec("CREATE INDEX IF NOT EXISTS idx_scenario_results_run ON scenario_results(run_id)");
|
|
29142
|
+
await adapter.exec(`
|
|
29143
|
+
CREATE TABLE IF NOT EXISTS twin_runs (
|
|
29144
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
29145
|
+
run_id VARCHAR(255),
|
|
29146
|
+
twin_name TEXT NOT NULL,
|
|
29147
|
+
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
29148
|
+
stopped_at DATETIME,
|
|
29149
|
+
status VARCHAR(32) NOT NULL DEFAULT 'running',
|
|
29150
|
+
ports_json TEXT
|
|
29151
|
+
)
|
|
29152
|
+
`);
|
|
29153
|
+
await adapter.exec(`
|
|
29154
|
+
CREATE TABLE IF NOT EXISTS twin_health_failures (
|
|
29155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29156
|
+
twin_name TEXT NOT NULL,
|
|
29157
|
+
run_id VARCHAR(255),
|
|
29158
|
+
checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
29159
|
+
error_message TEXT NOT NULL
|
|
29160
|
+
)
|
|
29161
|
+
`);
|
|
29162
|
+
await adapter.exec("CREATE INDEX IF NOT EXISTS idx_twin_health_failures_twin ON twin_health_failures(twin_name)");
|
|
28613
29163
|
}
|
|
28614
29164
|
|
|
28615
29165
|
//#endregion
|
|
@@ -28769,6 +29319,160 @@ function registerFactoryCommand(program) {
|
|
|
28769
29319
|
else process.stdout.write(`✗ ${passedCount}/${TOTAL_RULE_COUNT} rules passed, ${errors.length} ${errLabel}, ${warnings.length} ${warnLabel}\n`);
|
|
28770
29320
|
if (errors.length > 0) process.exit(1);
|
|
28771
29321
|
});
|
|
29322
|
+
const twinsCmd = factoryCmd.command("twins").description("Digital twin template management");
|
|
29323
|
+
twinsCmd.command("templates").description("List available built-in twin templates").action(() => {
|
|
29324
|
+
const templates = listTwinTemplates();
|
|
29325
|
+
for (const t of templates) process.stdout.write(` ${t.name.padEnd(16)} ${t.description}\n`);
|
|
29326
|
+
});
|
|
29327
|
+
twinsCmd.command("init").description("Initialize a twin definition file from a built-in template").requiredOption("--template <name>", "Template name (e.g. localstack, wiremock)").option("--force", "Overwrite existing file if it already exists").action(async (opts) => {
|
|
29328
|
+
try {
|
|
29329
|
+
const entry = getTwinTemplate(opts.template);
|
|
29330
|
+
if (!entry) {
|
|
29331
|
+
const available = listTwinTemplates().map((t) => t.name).join(", ");
|
|
29332
|
+
process.stderr.write(`Error: Unknown template '${opts.template}'. Available: ${available}\n`);
|
|
29333
|
+
process.exit(1);
|
|
29334
|
+
return;
|
|
29335
|
+
}
|
|
29336
|
+
const targetPath = path.join(process.cwd(), ".substrate", "twins", `${opts.template}.yaml`);
|
|
29337
|
+
if (!opts.force) try {
|
|
29338
|
+
await access$1(targetPath);
|
|
29339
|
+
process.stderr.write(`Error: File already exists: ${targetPath} — use --force to overwrite\n`);
|
|
29340
|
+
process.exit(1);
|
|
29341
|
+
return;
|
|
29342
|
+
} catch {}
|
|
29343
|
+
await mkdir$1(path.dirname(targetPath), { recursive: true });
|
|
29344
|
+
const yamlContent = yaml.dump(entry.definition);
|
|
29345
|
+
await writeFile$1(targetPath, yamlContent, "utf-8");
|
|
29346
|
+
process.stdout.write(`Created ${targetPath}\n`);
|
|
29347
|
+
} catch (err) {
|
|
29348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29349
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29350
|
+
process.exit(1);
|
|
29351
|
+
}
|
|
29352
|
+
});
|
|
29353
|
+
twinsCmd.command("start").description("Start all discovered twin definitions via Docker Compose").action(async () => {
|
|
29354
|
+
try {
|
|
29355
|
+
const projectDir = process.cwd();
|
|
29356
|
+
const twinsDir = path.join(projectDir, ".substrate", "twins");
|
|
29357
|
+
const registry = createTwinRegistry();
|
|
29358
|
+
try {
|
|
29359
|
+
await registry.discover(twinsDir);
|
|
29360
|
+
} catch (err) {
|
|
29361
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29362
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29363
|
+
process.exit(1);
|
|
29364
|
+
return;
|
|
29365
|
+
}
|
|
29366
|
+
const twins = registry.list();
|
|
29367
|
+
if (twins.length === 0) {
|
|
29368
|
+
process.stderr.write("No twin definitions found in .substrate/twins/\n");
|
|
29369
|
+
process.exit(1);
|
|
29370
|
+
return;
|
|
29371
|
+
}
|
|
29372
|
+
const eventBus = new TypedEventBusImpl();
|
|
29373
|
+
eventBus.on("twin:started", (e) => {
|
|
29374
|
+
process.stdout.write(` Started: ${e.twinName}\n`);
|
|
29375
|
+
});
|
|
29376
|
+
const manager = createTwinManager(eventBus);
|
|
29377
|
+
try {
|
|
29378
|
+
await manager.start(twins);
|
|
29379
|
+
} catch (err) {
|
|
29380
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29381
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29382
|
+
process.exit(1);
|
|
29383
|
+
return;
|
|
29384
|
+
}
|
|
29385
|
+
const composeDir = manager.getComposeDir();
|
|
29386
|
+
await writeRunState(projectDir, {
|
|
29387
|
+
composeDir,
|
|
29388
|
+
twinNames: twins.map((t) => t.name),
|
|
29389
|
+
startedAt: new Date().toISOString()
|
|
29390
|
+
});
|
|
29391
|
+
process.stdout.write("\nAll twins started successfully.\n");
|
|
29392
|
+
} catch (err) {
|
|
29393
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29394
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29395
|
+
process.exit(1);
|
|
29396
|
+
}
|
|
29397
|
+
});
|
|
29398
|
+
twinsCmd.command("stop").description("Stop all running twins").action(async () => {
|
|
29399
|
+
try {
|
|
29400
|
+
const projectDir = process.cwd();
|
|
29401
|
+
const state = await readRunState(projectDir);
|
|
29402
|
+
if (!state) {
|
|
29403
|
+
process.stderr.write("No twins are currently running\n");
|
|
29404
|
+
process.exit(1);
|
|
29405
|
+
return;
|
|
29406
|
+
}
|
|
29407
|
+
try {
|
|
29408
|
+
execSync("docker compose down --remove-orphans", {
|
|
29409
|
+
cwd: state.composeDir,
|
|
29410
|
+
stdio: "pipe"
|
|
29411
|
+
});
|
|
29412
|
+
} catch {}
|
|
29413
|
+
rmSync(state.composeDir, {
|
|
29414
|
+
recursive: true,
|
|
29415
|
+
force: true
|
|
29416
|
+
});
|
|
29417
|
+
await clearRunState(projectDir);
|
|
29418
|
+
process.stdout.write(`Stopped twins: ${state.twinNames.join(", ")}\n`);
|
|
29419
|
+
} catch (err) {
|
|
29420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29421
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29422
|
+
process.exit(1);
|
|
29423
|
+
}
|
|
29424
|
+
});
|
|
29425
|
+
twinsCmd.command("status").description("Show status of all discovered twins").action(async () => {
|
|
29426
|
+
try {
|
|
29427
|
+
const projectDir = process.cwd();
|
|
29428
|
+
const state = await readRunState(projectDir);
|
|
29429
|
+
const runningNames = new Set(state?.twinNames ?? []);
|
|
29430
|
+
const registry = createTwinRegistry();
|
|
29431
|
+
let twins = [];
|
|
29432
|
+
try {
|
|
29433
|
+
await registry.discover(path.join(projectDir, ".substrate", "twins"));
|
|
29434
|
+
twins = registry.list();
|
|
29435
|
+
} catch {}
|
|
29436
|
+
if (twins.length === 0) {
|
|
29437
|
+
process.stdout.write("No twin definitions found in .substrate/twins/\n");
|
|
29438
|
+
return;
|
|
29439
|
+
}
|
|
29440
|
+
for (const twin of twins) {
|
|
29441
|
+
const status = runningNames.has(twin.name) ? "running" : "stopped";
|
|
29442
|
+
const portsStr = twin.ports.length > 0 ? twin.ports.map((p) => `${p.host}:${p.container}`).join(", ") : "—";
|
|
29443
|
+
process.stdout.write(` ${twin.name.padEnd(20)} ${status.padEnd(10)} ${portsStr}\n`);
|
|
29444
|
+
}
|
|
29445
|
+
} catch (err) {
|
|
29446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29447
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29448
|
+
process.exit(1);
|
|
29449
|
+
}
|
|
29450
|
+
});
|
|
29451
|
+
twinsCmd.command("list").description("List all discovered twin definitions").action(async () => {
|
|
29452
|
+
try {
|
|
29453
|
+
const projectDir = process.cwd();
|
|
29454
|
+
const registry = createTwinRegistry();
|
|
29455
|
+
let twins = [];
|
|
29456
|
+
try {
|
|
29457
|
+
await registry.discover(path.join(projectDir, ".substrate", "twins"));
|
|
29458
|
+
twins = registry.list();
|
|
29459
|
+
} catch {}
|
|
29460
|
+
if (twins.length === 0) {
|
|
29461
|
+
process.stdout.write("No twin definitions found in .substrate/twins/\n");
|
|
29462
|
+
return;
|
|
29463
|
+
}
|
|
29464
|
+
process.stdout.write(" NAME IMAGE PORTS HEALTHCHECK\n");
|
|
29465
|
+
for (const twin of twins) {
|
|
29466
|
+
const ports = twin.ports.length > 0 ? twin.ports.map((p) => `${p.host}:${p.container}`).join(", ") : "—";
|
|
29467
|
+
const healthcheck = twin.healthcheck?.url ?? "—";
|
|
29468
|
+
process.stdout.write(` ${twin.name.padEnd(20)} ${twin.image.padEnd(38)} ${ports.padEnd(16)} ${healthcheck}\n`);
|
|
29469
|
+
}
|
|
29470
|
+
} catch (err) {
|
|
29471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29472
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
29473
|
+
process.exit(1);
|
|
29474
|
+
}
|
|
29475
|
+
});
|
|
28772
29476
|
}
|
|
28773
29477
|
|
|
28774
29478
|
//#endregion
|
|
@@ -30243,5 +30947,5 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
30243
30947
|
}
|
|
30244
30948
|
|
|
30245
30949
|
//#endregion
|
|
30246
|
-
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
30247
|
-
//# sourceMappingURL=run-
|
|
30950
|
+
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
30951
|
+
//# sourceMappingURL=run-z84Uu_U2.js.map
|