@turri-dev/cli 0.0.0-stage → 0.1.1
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/.pi/themes/turri-dark.json +80 -0
- package/.pi/themes/turri-light.json +82 -0
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/assets/jellyfish/dark-00.png +0 -0
- package/assets/jellyfish/dark-01.png +0 -0
- package/assets/jellyfish/dark-02.png +0 -0
- package/assets/jellyfish/dark-03.png +0 -0
- package/assets/jellyfish/dark-04.png +0 -0
- package/assets/jellyfish/dark-05.png +0 -0
- package/assets/jellyfish/dark-06.png +0 -0
- package/assets/jellyfish/dark-07.png +0 -0
- package/assets/jellyfish/dark-08.png +0 -0
- package/assets/jellyfish/dark-09.png +0 -0
- package/assets/jellyfish/dark-10.png +0 -0
- package/assets/jellyfish/dark-11.png +0 -0
- package/assets/jellyfish/dark-12.png +0 -0
- package/assets/jellyfish/dark-13.png +0 -0
- package/assets/jellyfish/dark-14.png +0 -0
- package/assets/jellyfish/fallback.txt +252 -0
- package/assets/jellyfish/light-00.png +0 -0
- package/assets/jellyfish/light-01.png +0 -0
- package/assets/jellyfish/light-02.png +0 -0
- package/assets/jellyfish/light-03.png +0 -0
- package/assets/jellyfish/light-04.png +0 -0
- package/assets/jellyfish/light-05.png +0 -0
- package/assets/jellyfish/light-06.png +0 -0
- package/assets/jellyfish/light-07.png +0 -0
- package/assets/jellyfish/light-08.png +0 -0
- package/assets/jellyfish/light-09.png +0 -0
- package/assets/jellyfish/light-10.png +0 -0
- package/assets/jellyfish/light-11.png +0 -0
- package/assets/jellyfish/light-12.png +0 -0
- package/assets/jellyfish/light-13.png +0 -0
- package/assets/jellyfish/light-14.png +0 -0
- package/bin/turri.mjs +29 -0
- package/dist/ask.js +149 -0
- package/dist/brand.js +50 -0
- package/dist/btw.js +174 -0
- package/dist/chat-models.js +37 -0
- package/dist/chat-skills.js +18 -0
- package/dist/cli.js +178 -0
- package/dist/config-command.js +197 -0
- package/dist/config-edit.js +367 -0
- package/dist/config.js +489 -0
- package/dist/conform-cli.js +29 -0
- package/dist/cultivate-cli.js +110 -0
- package/dist/drift-cli.js +25 -0
- package/dist/environment.js +70 -0
- package/dist/extension.js +2372 -0
- package/dist/genome.js +301 -0
- package/dist/graph.js +246 -0
- package/dist/identity.js +35 -0
- package/dist/init-config.js +511 -0
- package/dist/judge.js +62 -0
- package/dist/lineage-cli.js +13 -0
- package/dist/loop-cli.js +132 -0
- package/dist/loop-tools.js +225 -0
- package/dist/mature-tools.js +193 -0
- package/dist/model-check.js +117 -0
- package/dist/module-report.js +9 -0
- package/dist/pi-launch.js +31 -0
- package/dist/pi.js +813 -0
- package/dist/plain/check.js +76 -0
- package/dist/plain/concepts.js +213 -0
- package/dist/plain/errors.js +43 -0
- package/dist/plain/files.js +57 -0
- package/dist/plain/highlight.js +195 -0
- package/dist/plain/index.js +15 -0
- package/dist/plain/liquid.js +129 -0
- package/dist/plain/markdown.js +208 -0
- package/dist/plain/parser.js +752 -0
- package/dist/plain/plain.tmLanguage.json +124 -0
- package/dist/plain/spec.js +291 -0
- package/dist/plain/stdlib.js +44 -0
- package/dist/question-dialog.js +125 -0
- package/dist/questioning.js +109 -0
- package/dist/quiet.js +125 -0
- package/dist/regenerate/activity.js +187 -0
- package/dist/regenerate/agent-env.js +190 -0
- package/dist/regenerate/agent-shell.js +114 -0
- package/dist/regenerate/arbiter.js +133 -0
- package/dist/regenerate/audit.js +203 -0
- package/dist/regenerate/cases.js +83 -0
- package/dist/regenerate/change-view.js +102 -0
- package/dist/regenerate/choose-conformance.js +117 -0
- package/dist/regenerate/chunk-plan.js +625 -0
- package/dist/regenerate/codebase.js +75 -0
- package/dist/regenerate/command-check.js +312 -0
- package/dist/regenerate/compaction.js +20 -0
- package/dist/regenerate/confine.js +310 -0
- package/dist/regenerate/conform.js +133 -0
- package/dist/regenerate/conformance-agents.js +955 -0
- package/dist/regenerate/conformance.js +936 -0
- package/dist/regenerate/conversation-writes.js +100 -0
- package/dist/regenerate/credentials.js +88 -0
- package/dist/regenerate/debug-log.js +228 -0
- package/dist/regenerate/debugger.js +90 -0
- package/dist/regenerate/delivery.js +19 -0
- package/dist/regenerate/diagnose-job.js +40 -0
- package/dist/regenerate/diagnose.js +81 -0
- package/dist/regenerate/diff-view.js +240 -0
- package/dist/regenerate/drift.js +194 -0
- package/dist/regenerate/engine.js +118 -0
- package/dist/regenerate/environment-error.js +220 -0
- package/dist/regenerate/job-log.js +102 -0
- package/dist/regenerate/job-panel.js +336 -0
- package/dist/regenerate/jobs-tree.js +300 -0
- package/dist/regenerate/jobs.js +427 -0
- package/dist/regenerate/lanes.js +32 -0
- package/dist/regenerate/lineage.js +384 -0
- package/dist/regenerate/loop.js +1435 -0
- package/dist/regenerate/masking.js +95 -0
- package/dist/regenerate/notes.js +397 -0
- package/dist/regenerate/os-sandbox.js +243 -0
- package/dist/regenerate/paths.js +165 -0
- package/dist/regenerate/plain-palette.js +83 -0
- package/dist/regenerate/prompt.js +704 -0
- package/dist/regenerate/records.js +294 -0
- package/dist/regenerate/regenerate.js +598 -0
- package/dist/regenerate/regression.js +181 -0
- package/dist/regenerate/release.js +262 -0
- package/dist/regenerate/requires.js +241 -0
- package/dist/regenerate/resources.js +164 -0
- package/dist/regenerate/self-check.js +111 -0
- package/dist/regenerate/services.js +155 -0
- package/dist/regenerate/shell.js +319 -0
- package/dist/regenerate/side-panel.js +278 -0
- package/dist/regenerate/spec-map-view.js +574 -0
- package/dist/regenerate/spec-map.js +561 -0
- package/dist/regenerate/spec-steps.js +527 -0
- package/dist/regenerate/state.js +10 -0
- package/dist/regenerate/steer.js +91 -0
- package/dist/regenerate/steering.js +119 -0
- package/dist/regenerate/stop-report.js +161 -0
- package/dist/regenerate/submit-work.js +71 -0
- package/dist/regenerate/test-kinds.js +124 -0
- package/dist/regenerate/test-scripts.js +695 -0
- package/dist/regenerate/testing.js +406 -0
- package/dist/regenerate/transcript-view.js +1 -0
- package/dist/regenerate/unit-gate.js +20 -0
- package/dist/regenerate/unit-review.js +347 -0
- package/dist/regenerate/web.js +256 -0
- package/dist/regenerate/workspace.js +99 -0
- package/dist/regenerate-cli.js +68 -0
- package/dist/release-agent.js +114 -0
- package/dist/release-cli.js +131 -0
- package/dist/release-tools.js +148 -0
- package/dist/review/live.js +165 -0
- package/dist/rules.js +47 -0
- package/dist/self-invocation.js +110 -0
- package/dist/setup.js +245 -0
- package/dist/skill-progress.js +78 -0
- package/dist/skill.js +149 -0
- package/dist/skills.js +74 -0
- package/dist/spec-diff-cli.js +37 -0
- package/dist/spec-tools.js +155 -0
- package/dist/stack.js +158 -0
- package/dist/stacks.js +97 -0
- package/dist/stdlib.js +100 -0
- package/dist/terminal-images.js +46 -0
- package/dist/tools.js +242 -0
- package/dist/validate-cli.js +52 -0
- package/docs/README.md +58 -0
- package/docs/authoring-plain.md +245 -0
- package/docs/config.md +353 -0
- package/docs/deploy.md +109 -0
- package/docs/development.md +75 -0
- package/docs/headless.md +148 -0
- package/docs/images/how-turri-works.svg +50 -0
- package/docs/images/turri-banner.png +0 -0
- package/docs/installation.md +118 -0
- package/docs/parser.md +108 -0
- package/docs/regenerating.md +1116 -0
- package/docs/sandbox.md +233 -0
- package/docs/skills-and-gates.md +254 -0
- package/docs/theming.md +76 -0
- package/docs/using.md +454 -0
- package/genome/rules/bullet-continuation.md +69 -0
- package/genome/rules/concise-specs-examples.md +431 -0
- package/genome/rules/concise-specs-integration-examples.md +270 -0
- package/genome/rules/concise-specs.md +80 -0
- package/genome/rules/definitions.md +107 -0
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
- package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
- package/genome/rules/exported-concepts.md +39 -0
- package/genome/rules/func-specs.md +129 -0
- package/genome/rules/impl-reqs.md +68 -0
- package/genome/rules/import-modules.md +48 -0
- package/genome/rules/integration-embedded-testing.md +276 -0
- package/genome/rules/integration-embedded.md +171 -0
- package/genome/rules/integration-standalone.md +135 -0
- package/genome/rules/integrations.md +137 -0
- package/genome/rules/linked-resources.md +69 -0
- package/genome/rules/module-structure.md +105 -0
- package/genome/rules/project-model.md +88 -0
- package/genome/rules/required-concepts.md +44 -0
- package/genome/rules/requires-modules.md +80 -0
- package/genome/rules/stdlib.md +73 -0
- package/genome/rules/test-reqs.md +57 -0
- package/genome/skills/add-acceptance-test/SKILL.md +100 -0
- package/genome/skills/add-concept/SKILL.md +71 -0
- package/genome/skills/add-functional-spec/SKILL.md +84 -0
- package/genome/skills/add-functional-specs/SKILL.md +118 -0
- package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
- package/genome/skills/add-resource/SKILL.md +105 -0
- package/genome/skills/add-template/SKILL.md +64 -0
- package/genome/skills/add-test-requirement/SKILL.md +73 -0
- package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
- package/genome/skills/analyze-func-specs/SKILL.md +123 -0
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
- package/genome/skills/break-down-func-spec/SKILL.md +205 -0
- package/genome/skills/brood/SKILL.md +229 -0
- package/genome/skills/brood/references/intent.md +35 -0
- package/genome/skills/brood/references/product.md +101 -0
- package/genome/skills/brood/references/stack.md +61 -0
- package/genome/skills/brood/references/testing.md +56 -0
- package/genome/skills/bud/SKILL.md +148 -0
- package/genome/skills/bud/references/checklist.md +44 -0
- package/genome/skills/bud/references/intent.md +30 -0
- package/genome/skills/check-plain-env/SKILL.md +285 -0
- package/genome/skills/consolidate-concepts/SKILL.md +192 -0
- package/genome/skills/create-import-module/SKILL.md +104 -0
- package/genome/skills/create-requires-module/SKILL.md +109 -0
- package/genome/skills/debug-specs/SKILL.md +188 -0
- package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
- package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
- package/genome/skills/init-plain-project/SKILL.md +117 -0
- package/genome/skills/init-plain-project/references/checklist.md +30 -0
- package/genome/skills/mature/SKILL.md +126 -0
- package/genome/skills/refactor-module/SKILL.md +196 -0
- package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
- package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
- package/genome/skills/spawn/SKILL.md +163 -0
- package/package.json +68 -4
- package/stdlib/backend/common.plain +25 -0
- package/stdlib/backend/conformance/common.plain +12 -0
- package/stdlib/backend/conformance/go-gin.plain +9 -0
- package/stdlib/backend/conformance/go-nethttp.plain +9 -0
- package/stdlib/backend/conformance/java-springboot.plain +9 -0
- package/stdlib/backend/conformance/python-django.plain +9 -0
- package/stdlib/backend/conformance/python-fastapi.plain +9 -0
- package/stdlib/backend/conformance/python-flask.plain +9 -0
- package/stdlib/backend/conformance/rust-axum.plain +9 -0
- package/stdlib/backend/conformance/typescript-express.plain +9 -0
- package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
- package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
- package/stdlib/backend/go-gin.plain +18 -0
- package/stdlib/backend/go-nethttp.plain +18 -0
- package/stdlib/backend/java-springboot.plain +19 -0
- package/stdlib/backend/python-django.plain +19 -0
- package/stdlib/backend/python-fastapi.plain +23 -0
- package/stdlib/backend/python-flask.plain +23 -0
- package/stdlib/backend/rust-axum.plain +17 -0
- package/stdlib/backend/typescript-express.plain +19 -0
- package/stdlib/backend/typescript-fastify.plain +19 -0
- package/stdlib/backend/typescript-nestjs.plain +19 -0
- package/stdlib/cli/common.plain +17 -0
- package/stdlib/cli/conformance/common.plain +11 -0
- package/stdlib/cli/conformance/go-cobra.plain +9 -0
- package/stdlib/cli/conformance/go-flag.plain +9 -0
- package/stdlib/cli/conformance/java-picocli.plain +9 -0
- package/stdlib/cli/conformance/python-argparse.plain +9 -0
- package/stdlib/cli/conformance/python-click.plain +9 -0
- package/stdlib/cli/conformance/python-typer.plain +9 -0
- package/stdlib/cli/conformance/rust-clap.plain +9 -0
- package/stdlib/cli/conformance/typescript-commander.plain +9 -0
- package/stdlib/cli/go-cobra.plain +14 -0
- package/stdlib/cli/go-flag.plain +14 -0
- package/stdlib/cli/java-picocli.plain +15 -0
- package/stdlib/cli/python-argparse.plain +18 -0
- package/stdlib/cli/python-click.plain +16 -0
- package/stdlib/cli/python-typer.plain +16 -0
- package/stdlib/cli/rust-clap.plain +14 -0
- package/stdlib/cli/typescript-commander.plain +17 -0
- package/stdlib/frontend/angular.plain +17 -0
- package/stdlib/frontend/common.plain +17 -0
- package/stdlib/frontend/conformance/angular.plain +5 -0
- package/stdlib/frontend/conformance/common.plain +11 -0
- package/stdlib/frontend/conformance/react-vite.plain +5 -0
- package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
- package/stdlib/frontend/conformance/vue-vite.plain +5 -0
- package/stdlib/frontend/react-vite.plain +16 -0
- package/stdlib/frontend/sveltekit-spa.plain +16 -0
- package/stdlib/frontend/vue-vite.plain +16 -0
- package/stdlib/lang/go.plain +13 -0
- package/stdlib/lang/java.plain +12 -0
- package/stdlib/lang/python.plain +18 -0
- package/stdlib/lang/rust.plain +12 -0
- package/stdlib/lang/typescript.plain +13 -0
- package/stdlib/library/common.plain +15 -0
- package/stdlib/library/conformance/common.plain +9 -0
- package/stdlib/library/conformance/go.plain +9 -0
- package/stdlib/library/conformance/java-gradle.plain +9 -0
- package/stdlib/library/conformance/java-maven.plain +9 -0
- package/stdlib/library/conformance/python.plain +9 -0
- package/stdlib/library/conformance/rust.plain +9 -0
- package/stdlib/library/conformance/typescript.plain +9 -0
- package/stdlib/library/go.plain +12 -0
- package/stdlib/library/java-gradle.plain +13 -0
- package/stdlib/library/java-maven.plain +13 -0
- package/stdlib/library/python.plain +13 -0
- package/stdlib/library/rust.plain +12 -0
- package/stdlib/library/typescript.plain +15 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
3
|
+
import { getSpecificationsForFrid } from "../plain/spec.js";
|
|
4
|
+
import { conformanceDirFor } from "./conformance.js";
|
|
5
|
+
import { reportActivity, reportStage, unitOf } from "./activity.js";
|
|
6
|
+
import { confinedTools } from "./confine.js";
|
|
7
|
+
import { projectRootOf } from "./paths.js";
|
|
8
|
+
import { buildRegressionFixPrompt, sectionModules } from "./prompt.js";
|
|
9
|
+
import { abandonWork, markWork, recordHistory } from "./lineage.js";
|
|
10
|
+
import { missingToolchainError } from "./environment-error.js";
|
|
11
|
+
import { workspaceBlock } from "./workspace.js";
|
|
12
|
+
import { CODER_COMPACTION } from "./compaction.js";
|
|
13
|
+
import { NOTES_TURN, notesForSession, withNotes } from "./notes.js";
|
|
14
|
+
import { createScriptTool, MissingTestScriptError, runConformanceScript, runUnitScript, staticChecksOnly, withTestingScripts } from "./test-scripts.js";
|
|
15
|
+
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
16
|
+
import { ATTEMPTS } from "./stop-report.js";
|
|
17
|
+
import { keepingTestsUnchanged, listFiles } from "./testing.js";
|
|
18
|
+
/**
|
|
19
|
+
* The regression gate: every conformance test of a codebase - its own and those it inherits - run together with
|
|
20
|
+
* the unit tests, and the code repaired by an agent that may not touch the tests until they all pass. A conformance
|
|
21
|
+
* job ends on it: tests that pass on their own are not done until the whole suite passes with them.
|
|
22
|
+
*/
|
|
23
|
+
/** On top of the file tools, which are fenced to the build directory by `confine.ts`. */
|
|
24
|
+
const FIX_TOOLS = ["run_unit_tests", "run_conformance_tests", "update_notes"];
|
|
25
|
+
const DEFAULT_MAX_REGRESSION_ROUNDS = ATTEMPTS.regression;
|
|
26
|
+
/** The module's conformance tests, when there are any: the chain's, copied up, and its own. */
|
|
27
|
+
function conformanceDirs(conformanceRoot, moduleName) {
|
|
28
|
+
const dir = conformanceDirFor(moduleName, conformanceRoot);
|
|
29
|
+
return existsSync(dir) && listFiles(dir).length > 0 ? [dir] : [];
|
|
30
|
+
}
|
|
31
|
+
/** Repair the implementation after the merged suite went red. */
|
|
32
|
+
async function repairRegression(input) {
|
|
33
|
+
const [specifications] = getSpecificationsForFrid(input.tree, input.frids[input.frids.length - 1]);
|
|
34
|
+
const runs = [];
|
|
35
|
+
const fence = confinedTools({ workdir: input.buildDir, reportsEnvironment: true });
|
|
36
|
+
const notes = notesForSession(input.buildDir, `${input.step} regression repair`);
|
|
37
|
+
reportStage(`${unitOf(input.moduleName)}: starting the repair agent${input.model ? ` (${input.model})` : ""}`);
|
|
38
|
+
const session = await createExecutor({
|
|
39
|
+
cwd: input.buildDir,
|
|
40
|
+
customTools: [
|
|
41
|
+
notes.tool,
|
|
42
|
+
...createWebTools(),
|
|
43
|
+
...fence.tools,
|
|
44
|
+
createScriptTool({ kind: "unit", script: input.scripts.unit, run: () => runUnitScript(input.buildDir, input.scripts.unit), runs }),
|
|
45
|
+
createScriptTool({
|
|
46
|
+
kind: "conformance",
|
|
47
|
+
script: input.scripts.conformance,
|
|
48
|
+
run: () => runConformanceScript(input.buildDir, input.testsDirs, input.scripts),
|
|
49
|
+
runs: [],
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
tools: [...fence.names, ...FIX_TOOLS, ...WEB_TOOLS],
|
|
53
|
+
model: input.model,
|
|
54
|
+
label: `${input.step} repair agent`, role: "coding",
|
|
55
|
+
checkEnvironment: { run: () => runUnitScript(input.buildDir, input.scripts.unit), script: input.scripts.unit },
|
|
56
|
+
system: `${fence.note}\n\n${workspaceBlock({ dir: input.buildDir, hasMyDiff: FIX_TOOLS.includes("my_diff") })}`,
|
|
57
|
+
task: withNotes(withTestingScripts(buildRegressionFixPrompt({
|
|
58
|
+
moduleName: input.moduleName,
|
|
59
|
+
frids: input.frids,
|
|
60
|
+
specifications,
|
|
61
|
+
modules: sectionModules(input.tree, input.moduleName),
|
|
62
|
+
required: input.required,
|
|
63
|
+
failure: input.failure,
|
|
64
|
+
}), input.scripts, ["conformance", "unit"], projectRootOf(input.buildDir)), input.buildDir),
|
|
65
|
+
compaction: { instructions: CODER_COMPACTION },
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
reportStage(`${unitOf(input.moduleName)}: turri is repairing the code so every functionality's conformance tests pass together`);
|
|
69
|
+
const { report, restored } = await keepingTestsUnchanged({
|
|
70
|
+
dirs: input.testsDirs,
|
|
71
|
+
label: input.moduleName,
|
|
72
|
+
start: () => ask(session, BEGIN, input.onDelta),
|
|
73
|
+
again: (prompt) => ask(session, prompt, input.onDelta),
|
|
74
|
+
});
|
|
75
|
+
if (restored.length > 0)
|
|
76
|
+
reportActivity(` put back ${restored.length} conformance test file(s) the repair agent changed`);
|
|
77
|
+
reportStage(`${unitOf(input.moduleName)}: checking the working notes were left`);
|
|
78
|
+
await notes.ensureWritten((next) => ask(session, next, input.onDelta, NOTES_TURN));
|
|
79
|
+
return restored.length > 0
|
|
80
|
+
? `${report}\n\n(The harness put back ${restored.length} conformance test file(s) this repair changed.)`
|
|
81
|
+
: report;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
session.dispose();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Run every conformance test built so far, and repair the code until they all pass.
|
|
89
|
+
*
|
|
90
|
+
* This is the gate between one functionality and the next. A functionality that passed on
|
|
91
|
+
* its own is not finished until the ones before it still pass alongside it, and each
|
|
92
|
+
* repair changes the code - so the suite is run again after every one.
|
|
93
|
+
*/
|
|
94
|
+
export async function ensureRegressionPasses(input) {
|
|
95
|
+
const dirs = conformanceDirs(input.conformanceRoot, input.moduleName);
|
|
96
|
+
const runs = [];
|
|
97
|
+
const repairs = [];
|
|
98
|
+
let command = null;
|
|
99
|
+
if (dirs.length === 0) {
|
|
100
|
+
reportActivity(` ${input.moduleName} has no conformance tests yet - nothing to keep passing`);
|
|
101
|
+
return { passed: true, runs, repairs, command };
|
|
102
|
+
}
|
|
103
|
+
if (input.scripts.conformanceOff === true) {
|
|
104
|
+
reportActivity(` conformance is off: ${input.moduleName}'s conformance tests were not run`);
|
|
105
|
+
return { passed: true, runs, repairs, command, conformanceOff: true };
|
|
106
|
+
}
|
|
107
|
+
// There are tests: the project's conformance testing script runs them, or nothing does.
|
|
108
|
+
if (input.scripts.conformance === undefined)
|
|
109
|
+
throw new MissingTestScriptError(input.moduleName, "conformance");
|
|
110
|
+
const start = input.start ?? markWork([input.buildDir, ...dirs]);
|
|
111
|
+
const maxRounds = input.maxRounds ?? DEFAULT_MAX_REGRESSION_ROUNDS;
|
|
112
|
+
for (let round = 0; round <= maxRounds; round += 1) {
|
|
113
|
+
reportStage(round === 0
|
|
114
|
+
? `${unitOf(input.moduleName)}: running the codebase's whole conformance suite - every functionality's tests together (${input.frids.join(", ")})`
|
|
115
|
+
: `${unitOf(input.moduleName)}: repair ${round} done - running the whole conformance suite again`);
|
|
116
|
+
const result = await runConformanceScript(input.buildDir, dirs, input.scripts);
|
|
117
|
+
// The script cannot find its toolchain: no repair would pass it - the pulse stops, and the person is told.
|
|
118
|
+
const lacking = result.exitCode !== 0 ? missingToolchainError(result, { moduleName: input.moduleName, script: input.scripts.conformance, kind: "conformance" }) : undefined;
|
|
119
|
+
if (lacking !== undefined) {
|
|
120
|
+
abandonWork(start, input.moduleName, "the conformance testing script could not run");
|
|
121
|
+
throw lacking;
|
|
122
|
+
}
|
|
123
|
+
command = result.command;
|
|
124
|
+
runs.push({ command, exitCode: result.exitCode, frids: input.frids });
|
|
125
|
+
// The suite passes and its static checks do not: the findings are in the tests, which no repair may touch - the
|
|
126
|
+
// conformance tests' own job fixes them.
|
|
127
|
+
if (staticChecksOnly(result)) {
|
|
128
|
+
reportStage(`${unitOf(input.moduleName)}: the whole conformance suite passes, and the tests' static checks do not - a repair of the code cannot fix them - stopping`);
|
|
129
|
+
abandonWork(start, input.moduleName, "the conformance tests' static checks do not pass");
|
|
130
|
+
return { passed: false, runs, repairs, command };
|
|
131
|
+
}
|
|
132
|
+
reportActivity(result.exitCode === 0 ? " ✓ whole suite passing" : ` ✗ whole suite failing (exit ${result.exitCode})`);
|
|
133
|
+
input.onEvent?.({ type: "run", command, exitCode: result.exitCode, frids: input.frids });
|
|
134
|
+
// The whole suite passing is half of it: the unit tests have to pass with it.
|
|
135
|
+
let failure = result.output;
|
|
136
|
+
let passing = result.exitCode === 0;
|
|
137
|
+
if (passing && input.scripts.unit !== undefined) {
|
|
138
|
+
reportStage(`${unitOf(input.moduleName)}: the whole conformance suite passes - running the unit testing script too`);
|
|
139
|
+
const unit = await runUnitScript(input.buildDir, input.scripts.unit);
|
|
140
|
+
const lackingUnit = unit.exitCode !== 0 ? missingToolchainError(unit, { moduleName: input.moduleName, script: input.scripts.unit, kind: "unit" }) : undefined;
|
|
141
|
+
if (lackingUnit !== undefined) {
|
|
142
|
+
abandonWork(start, input.moduleName, "the unit testing script could not run");
|
|
143
|
+
throw lackingUnit;
|
|
144
|
+
}
|
|
145
|
+
runs.push({ command: unit.command, exitCode: unit.exitCode, frids: input.frids });
|
|
146
|
+
input.onEvent?.({ type: "run", command: unit.command, exitCode: unit.exitCode, frids: input.frids });
|
|
147
|
+
if (unit.exitCode !== 0) {
|
|
148
|
+
reportActivity(` ✗ unit tests failing (exit ${unit.exitCode}) with the conformance suite passing`);
|
|
149
|
+
passing = false;
|
|
150
|
+
failure = `The conformance suite passes, but the code's unit tests do not - and both have to.\n\n${unit.output}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (passing) {
|
|
154
|
+
reportStage(`${unitOf(input.moduleName)}: the whole conformance suite passes, and the unit tests with it${repairs.length > 0 ? ` after ${repairs.length} repair${repairs.length === 1 ? "" : "s"}` : ""}`);
|
|
155
|
+
if (repairs.length > 0) {
|
|
156
|
+
recordHistory(input.buildDir, `regression ${input.moduleName} (pass after ${repairs.length} repair(s))`);
|
|
157
|
+
}
|
|
158
|
+
return { passed: true, runs, repairs, command };
|
|
159
|
+
}
|
|
160
|
+
if (round === maxRounds)
|
|
161
|
+
break;
|
|
162
|
+
input.onEvent?.({ type: "repair", frids: input.frids });
|
|
163
|
+
reportStage(`${unitOf(input.moduleName)}: an earlier functionality broke - repairing (round ${round + 1} of ${maxRounds})`);
|
|
164
|
+
repairs.push(await repairRegression({
|
|
165
|
+
testsDirs: dirs,
|
|
166
|
+
buildDir: input.buildDir,
|
|
167
|
+
moduleName: input.moduleName,
|
|
168
|
+
frids: input.frids,
|
|
169
|
+
tree: input.tree,
|
|
170
|
+
required: input.required,
|
|
171
|
+
failure,
|
|
172
|
+
step: input.step ?? `${input.moduleName} ${input.frids.at(-1)}`,
|
|
173
|
+
scripts: input.scripts,
|
|
174
|
+
model: input.model,
|
|
175
|
+
onDelta: input.onDelta,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
reportStage(`${unitOf(input.moduleName)}: the whole conformance suite still fails after ${repairs.length} repair${repairs.length === 1 ? "" : "s"} - stopping`);
|
|
179
|
+
abandonWork(start, input.moduleName, `the whole conformance suite still fails after ${repairs.length} repair(s)`);
|
|
180
|
+
return { passed: false, runs, repairs, command };
|
|
181
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { connect } from "node:net";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { HISTORY_DIR } from "./lineage.js";
|
|
6
|
+
import { NOTES_FILE } from "./notes.js";
|
|
7
|
+
import { distDirFor, ensureInScratch, projectDirOf } from "./paths.js";
|
|
8
|
+
import { findCodebaseTip } from "./requires.js";
|
|
9
|
+
import { RECORD_FILE } from "./records.js";
|
|
10
|
+
function git(moduleDir, args) {
|
|
11
|
+
return spawnSync("git", ["--git-dir", join(moduleDir, HISTORY_DIR), "--work-tree", moduleDir, ...args], { cwd: moduleDir, encoding: "utf8" });
|
|
12
|
+
}
|
|
13
|
+
/** The module's last commit, when it has one with code in it. */
|
|
14
|
+
function committedCode(moduleDir) {
|
|
15
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
16
|
+
return undefined;
|
|
17
|
+
const commit = git(moduleDir, ["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
18
|
+
if (commit.status !== 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
// A commit with no code folder in it has nothing to run.
|
|
21
|
+
if (git(moduleDir, ["rev-parse", "--verify", "--quiet", "HEAD:code"]).status !== 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
return commit.stdout.trim();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Which module to deploy for a spec: the tip of its codebase - code accumulates up a requires chain, so
|
|
27
|
+
* the top module holds everything below it. Before the tip has anything committed, the nearest module
|
|
28
|
+
* below it that has.
|
|
29
|
+
*/
|
|
30
|
+
export function deployTarget(plainFile, inModule) {
|
|
31
|
+
const { tip, chain } = findCodebaseTip(plainFile, { allowNoFunctionalities: true, ...(inModule !== undefined ? { inModule } : {}) });
|
|
32
|
+
const candidates = [tip, ...[...chain].reverse().filter((entry) => entry.moduleName !== tip.moduleName)];
|
|
33
|
+
for (const entry of candidates) {
|
|
34
|
+
const commit = committedCode(dirname(entry.buildDir));
|
|
35
|
+
if (commit !== undefined)
|
|
36
|
+
return { entry, commit, ...(entry.moduleName !== tip.moduleName ? { instead: tip.moduleName } : {}) };
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Nothing is built yet for ${tip.moduleName}${candidates.length > 1 ? " or the modules it requires" : ""}: there is no code to deploy. Render it first.`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Export the module's code at its last commit into `dist/<module>`, replacing whatever was there - the
|
|
42
|
+
* harness's own files (what the code is built from, the agents' notes) left out.
|
|
43
|
+
*/
|
|
44
|
+
export function exportModule(plainFile, inModule) {
|
|
45
|
+
const { entry, commit, instead } = deployTarget(plainFile, inModule);
|
|
46
|
+
const moduleDir = dirname(entry.buildDir);
|
|
47
|
+
const dir = distDirFor(projectDirOf(entry.plainFile), entry.moduleName);
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const exported = spawnSync("bash", ["-o", "pipefail", "-c", 'git --git-dir "$1" archive --format=tar "$2" | tar -x -f - -C "$3"', "export", join(moduleDir, HISTORY_DIR), `${commit}:code`, dir], { encoding: "utf8" });
|
|
51
|
+
if (exported.status !== 0)
|
|
52
|
+
throw new Error(`Could not export ${entry.moduleName}'s code at ${commit.slice(0, 8)}: ${(exported.stderr ?? "").trim()}`);
|
|
53
|
+
for (const file of [RECORD_FILE, NOTES_FILE])
|
|
54
|
+
rmSync(join(dir, file), { force: true });
|
|
55
|
+
const [short, subject, when] = git(moduleDir, ["log", "-1", "--format=%h%x1f%s%x1f%cr", commit]).stdout.trim().split("\x1f");
|
|
56
|
+
const unfinished = git(moduleDir, ["status", "--porcelain", "--untracked-files=all", "--", "code"]).stdout.trim() !== "";
|
|
57
|
+
return {
|
|
58
|
+
moduleName: entry.moduleName,
|
|
59
|
+
plainFile: entry.plainFile,
|
|
60
|
+
way: "dist",
|
|
61
|
+
dir,
|
|
62
|
+
buildDir: entry.buildDir,
|
|
63
|
+
commit: short ?? commit.slice(0, 8),
|
|
64
|
+
subject: subject ?? "",
|
|
65
|
+
when: when ?? "",
|
|
66
|
+
unfinished,
|
|
67
|
+
...(instead !== undefined ? { instead } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The host codebase a guided release works in: the git repository the project is in - an embedded project's host -
|
|
72
|
+
* or else `fallback`, where turri runs.
|
|
73
|
+
*/
|
|
74
|
+
export function hostDirOf(plainFile, fallback) {
|
|
75
|
+
const top = spawnSync("git", ["-C", projectDirOf(plainFile), "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
76
|
+
return top.status === 0 && top.stdout.trim() !== "" ? top.stdout.trim() : fallback;
|
|
77
|
+
}
|
|
78
|
+
/** Where a path is, said from the current directory when it is below it. */
|
|
79
|
+
export function shortPath(path) {
|
|
80
|
+
const fromHere = relative(process.cwd(), path);
|
|
81
|
+
return fromHere === "" || fromHere.startsWith("..") ? path : fromHere;
|
|
82
|
+
}
|
|
83
|
+
/** What was deployed, in a line or three. */
|
|
84
|
+
export function describeDeployment(deployment) {
|
|
85
|
+
return [
|
|
86
|
+
`Deployed ${deployment.moduleName} to ${shortPath(deployment.dir)} - commit ${deployment.commit}, ${deployment.when}: ${deployment.subject}.`,
|
|
87
|
+
...(deployment.hostDir !== undefined ? [`Its guidance takes it on from there, in ${shortPath(deployment.hostDir)}.`] : []),
|
|
88
|
+
...(deployment.instead !== undefined ? [`${deployment.instead}, the module on top, has nothing built yet - this is ${deployment.moduleName}, the module below it.`] : []),
|
|
89
|
+
...(deployment.unfinished
|
|
90
|
+
? [
|
|
91
|
+
deployment.hostDir === undefined
|
|
92
|
+
? "A pulse is at work on the module right now: what it has written so far is not finished, and is left out."
|
|
93
|
+
: `A pulse is at work on the module right now: what it has written so far is not finished, and is left out of ${shortPath(deployment.dir)} - but ${shortPath(deployment.buildDir)} holds it, so take the code from ${shortPath(deployment.dir)}, never from there.`,
|
|
94
|
+
]
|
|
95
|
+
: []),
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
const processes = new Map();
|
|
99
|
+
/** Everything deploy started, running or ended, oldest first. */
|
|
100
|
+
export function listDeployed() {
|
|
101
|
+
return [...processes.values()].map((entry) => entry.info);
|
|
102
|
+
}
|
|
103
|
+
/** The ones still running. */
|
|
104
|
+
export function runningDeployed(moduleName) {
|
|
105
|
+
return listDeployed().filter((info) => info.exited === undefined && (moduleName === undefined || info.moduleName === moduleName));
|
|
106
|
+
}
|
|
107
|
+
/** Where a project's deployed processes write their output: its scratch, not the code in dist. */
|
|
108
|
+
export function deployLogDir(projectDir) {
|
|
109
|
+
const scratch = join(projectDir, ".turri");
|
|
110
|
+
return ensureInScratch(scratch, join(scratch, "deploy"));
|
|
111
|
+
}
|
|
112
|
+
/** The end of a log, for when something did not come up. */
|
|
113
|
+
export function logTail(log, lines = 40, chars = 4000) {
|
|
114
|
+
try {
|
|
115
|
+
const text = readFileSync(log, "utf8").split("\n").slice(-lines).join("\n");
|
|
116
|
+
return text.length > chars ? text.slice(-chars) : text;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function signalGroup(pid, signal) {
|
|
123
|
+
try {
|
|
124
|
+
// Its own process group: the server and whatever it started.
|
|
125
|
+
process.kill(-pid, signal);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
try {
|
|
129
|
+
process.kill(pid, signal);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Already gone.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
137
|
+
/** Stop one deployed process, or every one of a module, or every one: politely, then not. Returns the names stopped. */
|
|
138
|
+
export async function stopDeployed(which = {}) {
|
|
139
|
+
const targets = [...processes.values()].filter(({ info }) => info.exited === undefined && (which.name === undefined || info.name === which.name) && (which.moduleName === undefined || info.moduleName === which.moduleName));
|
|
140
|
+
for (const { info } of targets)
|
|
141
|
+
signalGroup(info.pid, "SIGTERM");
|
|
142
|
+
const deadline = Date.now() + 5000;
|
|
143
|
+
while (Date.now() < deadline && targets.some(({ info }) => info.exited === undefined))
|
|
144
|
+
await sleep(100);
|
|
145
|
+
for (const { info } of targets)
|
|
146
|
+
if (info.exited === undefined)
|
|
147
|
+
signalGroup(info.pid, "SIGKILL");
|
|
148
|
+
return targets.map(({ info }) => info.name);
|
|
149
|
+
}
|
|
150
|
+
/** Stop everything at once, without waiting: for when turri itself is going. */
|
|
151
|
+
export function stopAllDeployedNow() {
|
|
152
|
+
for (const { info } of processes.values())
|
|
153
|
+
if (info.exited === undefined)
|
|
154
|
+
signalGroup(info.pid, "SIGTERM");
|
|
155
|
+
}
|
|
156
|
+
let exitHooked = false;
|
|
157
|
+
function stopOnExit() {
|
|
158
|
+
if (exitHooked)
|
|
159
|
+
return;
|
|
160
|
+
exitHooked = true;
|
|
161
|
+
process.on("exit", stopAllDeployedNow);
|
|
162
|
+
}
|
|
163
|
+
function portOf(url) {
|
|
164
|
+
if (url.port !== "")
|
|
165
|
+
return Number(url.port);
|
|
166
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
167
|
+
}
|
|
168
|
+
/** Whether something already answers on the port. */
|
|
169
|
+
function portTaken(host, port) {
|
|
170
|
+
return new Promise((done) => {
|
|
171
|
+
const socket = connect({ host: host === "0.0.0.0" ? "127.0.0.1" : host.replace(/^\[|\]$/g, ""), port });
|
|
172
|
+
const finish = (taken) => {
|
|
173
|
+
socket.destroy();
|
|
174
|
+
done(taken);
|
|
175
|
+
};
|
|
176
|
+
socket.setTimeout(700, () => finish(false));
|
|
177
|
+
socket.once("connect", () => finish(true));
|
|
178
|
+
socket.once("error", () => finish(false));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** What holds a port, as lsof says it - or nothing when it cannot say. */
|
|
182
|
+
function holderOf(port) {
|
|
183
|
+
const found = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" });
|
|
184
|
+
return found.status === 0 ? found.stdout.trim().split("\n").slice(0, 4).join("\n") : "";
|
|
185
|
+
}
|
|
186
|
+
async function answers(url) {
|
|
187
|
+
try {
|
|
188
|
+
await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(2000) });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** How long a server is given to answer at its URL, and to stay up when it has none. */
|
|
196
|
+
export const READY_TIMEOUT_MS = Number(process.env.TURRI_DEPLOY_READY_TIMEOUT ?? 60) * 1000;
|
|
197
|
+
const ALIVE_AFTER_MS = 4000;
|
|
198
|
+
/**
|
|
199
|
+
* Start something that keeps running - a server, a frontend's dev server - in `dir`, on this machine,
|
|
200
|
+
* in its own process group, its output to a log. One already running under the name is stopped first.
|
|
201
|
+
*
|
|
202
|
+
* With a URL, it is ready once something answers there - any answer - and not when it exits first or
|
|
203
|
+
* nothing answers in time. A port something else holds already is said, with what holds it, and
|
|
204
|
+
* nothing is started: an app moved to a port of the harness's choosing would not be the one the
|
|
205
|
+
* person was told about. With no URL, it only has to still be running a few seconds on.
|
|
206
|
+
*/
|
|
207
|
+
export async function startDeployed(input) {
|
|
208
|
+
await stopDeployed({ name: input.name });
|
|
209
|
+
let url;
|
|
210
|
+
if (input.url !== undefined) {
|
|
211
|
+
try {
|
|
212
|
+
url = new URL(input.url);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return { ok: false, text: `'${input.url}' is not a URL. Give the one it serves at, e.g. http://localhost:5173.` };
|
|
216
|
+
}
|
|
217
|
+
const port = portOf(url);
|
|
218
|
+
if (await portTaken(url.hostname, port)) {
|
|
219
|
+
const holder = holderOf(port);
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
text: `Port ${port} is in use already - by something turri did not start - so ${input.name} was not started.${holder !== "" ? `\n${holder}` : ""}\nTell the person which port and what holds it; do not move the app to another port.`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
mkdirSync(input.logDir, { recursive: true });
|
|
227
|
+
const log = join(input.logDir, `${input.moduleName}-${input.name.replace(/[^\w.-]+/g, "-")}.log`);
|
|
228
|
+
const fd = openSync(log, "w");
|
|
229
|
+
const child = spawn("bash", ["-c", input.command], { cwd: input.dir, detached: true, stdio: ["ignore", fd, fd], env: process.env });
|
|
230
|
+
closeSync(fd);
|
|
231
|
+
if (child.pid === undefined)
|
|
232
|
+
return { ok: false, text: `${input.name} could not be started.` };
|
|
233
|
+
const info = { name: input.name, moduleName: input.moduleName, command: input.command, dir: input.dir, log, pid: child.pid, startedAt: Date.now(), ...(input.url !== undefined ? { url: input.url } : {}) };
|
|
234
|
+
child.on("exit", (code, signal) => {
|
|
235
|
+
info.exited = { code, signal };
|
|
236
|
+
});
|
|
237
|
+
child.on("error", () => {
|
|
238
|
+
info.exited ??= { code: null, signal: null };
|
|
239
|
+
});
|
|
240
|
+
processes.set(input.name, { info, child });
|
|
241
|
+
stopOnExit();
|
|
242
|
+
const failed = (why) => {
|
|
243
|
+
const tail = logTail(log);
|
|
244
|
+
return { ok: false, text: `${input.name} ${why}.${tail !== "" ? `\nThe end of its output (${shortPath(log)}):\n${tail}` : ""}`, process: info };
|
|
245
|
+
};
|
|
246
|
+
const ended = () => `exited${info.exited?.code !== null && info.exited?.code !== undefined ? ` with ${info.exited.code}` : ""} before it was ready`;
|
|
247
|
+
if (url === undefined) {
|
|
248
|
+
await sleep(ALIVE_AFTER_MS);
|
|
249
|
+
if (info.exited !== undefined)
|
|
250
|
+
return failed(ended());
|
|
251
|
+
return { ok: true, text: `${input.name} is running (pid ${info.pid}); its output is in ${shortPath(log)}.`, process: info };
|
|
252
|
+
}
|
|
253
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
254
|
+
while (Date.now() < deadline) {
|
|
255
|
+
if (info.exited !== undefined)
|
|
256
|
+
return failed(ended());
|
|
257
|
+
if (await answers(url.href))
|
|
258
|
+
return { ok: true, text: `${input.name} is up at ${url.href} (pid ${info.pid}); its output is in ${shortPath(log)}.`, process: info };
|
|
259
|
+
await sleep(1000);
|
|
260
|
+
}
|
|
261
|
+
return failed(`is running, but nothing answered at ${url.href} in ${Math.round(READY_TIMEOUT_MS / 1000)}s`);
|
|
262
|
+
}
|