@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,99 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { projectImplementationReqs, stackOf } from "../stack.js";
|
|
5
|
+
import { MODULES_DIR, projectRootOf } from "./paths.js";
|
|
6
|
+
import { RECORD_FILE } from "./records.js";
|
|
7
|
+
import { listFiles } from "./testing.js";
|
|
8
|
+
/**
|
|
9
|
+
* What an agent is told up front about its folder and this machine, beside its walls - what agents otherwise go looking
|
|
10
|
+
* for, a command at a time: whether there is anything above their folder, its git history, the toolchains' versions,
|
|
11
|
+
* where the build's dependencies come from. On the hycu runs a wandering model spent a third of its calls on these.
|
|
12
|
+
*/
|
|
13
|
+
/** The commands that say a toolchain's version, by the toolchain's name as `stackOf` gives it. */
|
|
14
|
+
const VERSION_COMMANDS = {
|
|
15
|
+
Python: ["python3"],
|
|
16
|
+
Node: ["node", "npm"],
|
|
17
|
+
Java: ["java", "mvn", "gradle"],
|
|
18
|
+
Go: ["go"],
|
|
19
|
+
Rust: ["cargo"],
|
|
20
|
+
"C/C++": ["cc"],
|
|
21
|
+
Zig: ["zig"],
|
|
22
|
+
Odin: ["odin"],
|
|
23
|
+
Ruby: ["ruby"],
|
|
24
|
+
PHP: ["php"],
|
|
25
|
+
Elixir: ["elixir"],
|
|
26
|
+
};
|
|
27
|
+
/** How each command is asked its version. */
|
|
28
|
+
const VERSION_ARGS = { java: ["-version"], go: ["version"], zig: ["version"], odin: ["version"], ruby: ["-v"], php: ["-v"], mvn: ["-v"] };
|
|
29
|
+
const probed = new Map();
|
|
30
|
+
/** The first line a command gives for its version on this machine - asked once - or nothing when it is not here. */
|
|
31
|
+
function probeVersion(command) {
|
|
32
|
+
if (probed.has(command))
|
|
33
|
+
return probed.get(command);
|
|
34
|
+
const run = spawnSync(command, VERSION_ARGS[command] ?? ["--version"], { encoding: "utf8", timeout: 10_000, env: { ...process.env, JAVA_TOOL_OPTIONS: "" } });
|
|
35
|
+
const first = run.status === 0 || (run.status !== null && (run.stdout || run.stderr))
|
|
36
|
+
? `${run.stdout ?? ""}\n${run.stderr ?? ""}`
|
|
37
|
+
.split("\n")
|
|
38
|
+
.map((line) => line.trim())
|
|
39
|
+
.find((line) => line !== "" && !line.startsWith("Picked up"))
|
|
40
|
+
: undefined;
|
|
41
|
+
const version = run.error === undefined ? first : undefined;
|
|
42
|
+
probed.set(command, version);
|
|
43
|
+
return version;
|
|
44
|
+
}
|
|
45
|
+
/** What the build tools of a toolchain fetch from, and where it is kept - said without a path. */
|
|
46
|
+
const DEPENDENCIES = {
|
|
47
|
+
Java: "Maven's local repository is set for you, a cache shared by the project's modules: you need not look for it - and if " +
|
|
48
|
+
"you set `MAVEN_OPTS`, keep `-Dmaven.repo.local` from it. Gradle's home is set the same way. `JAVA_TOOL_OPTIONS` gives " +
|
|
49
|
+
'every JVM its temporary folder and home: leave it set; each JVM printing "Picked up JAVA_TOOL_OPTIONS" is expected.',
|
|
50
|
+
Node: "npm's cache is set for you, shared by the project's modules; packages install into the project's `node_modules`.",
|
|
51
|
+
Python: "pip's and uv's caches are set for you; packages install into the project's own virtual environment.",
|
|
52
|
+
Go: "Go's module cache and build cache are set for you, shared by the project's modules.",
|
|
53
|
+
Rust: "Cargo's home is set for you, shared by the project's modules.",
|
|
54
|
+
};
|
|
55
|
+
/** The implementation reqs of the module a folder is part of - `plain_modules/<module>/...` - from its spec, imports in. */
|
|
56
|
+
function reqsOfModule(dir) {
|
|
57
|
+
const parts = resolve(dir).split(sep);
|
|
58
|
+
const index = parts.lastIndexOf(MODULES_DIR);
|
|
59
|
+
if (index < 0 || parts.length <= index + 1)
|
|
60
|
+
return [];
|
|
61
|
+
const module = parts[index + 1];
|
|
62
|
+
return projectImplementationReqs(projectRootOf(dir)).find((entry) => entry.module === module)?.reqs ?? [];
|
|
63
|
+
}
|
|
64
|
+
/** The block, for the agent's system prompt beside its walls. */
|
|
65
|
+
export function workspaceBlock(input) {
|
|
66
|
+
const probe = input.probe ?? probeVersion;
|
|
67
|
+
const files = listFiles(input.dir);
|
|
68
|
+
const folder = files.length === 0
|
|
69
|
+
? `- Your folder, \`${input.dir}\`, is empty: nothing is built yet, and there is nothing above it to find.`
|
|
70
|
+
: existsSync(join(input.dir, RECORD_FILE))
|
|
71
|
+
? `- Your folder, \`${input.dir}\`, holds the code built so far, and its unit tests passed when it was committed: you need not run them before you change anything.`
|
|
72
|
+
: `- Your folder, \`${input.dir}\`, holds what was written so far, ${files.length} file${files.length === 1 ? "" : "s"}.`;
|
|
73
|
+
const lines = [
|
|
74
|
+
"<your folder and this machine>",
|
|
75
|
+
folder,
|
|
76
|
+
input.hasMyDiff
|
|
77
|
+
? "- It is not a git repository, and none above it is yours: `my_diff` shows what you have changed in this pulse."
|
|
78
|
+
: "- It is not a git repository, and none above it is yours.",
|
|
79
|
+
];
|
|
80
|
+
const needs = stackOf((input.implementationReqs ?? reqsOfModule(input.dir)).join("\n"));
|
|
81
|
+
for (const need of needs) {
|
|
82
|
+
const commands = VERSION_COMMANDS[need.toolchain] ?? need.commands;
|
|
83
|
+
// A bare number says nothing on its own: `npm 11.19.1`, not `11.19.1`.
|
|
84
|
+
const versions = commands
|
|
85
|
+
.map((command) => {
|
|
86
|
+
const version = probe(command);
|
|
87
|
+
return version === undefined ? undefined : /^v?\d/.test(version) ? `${command} ${version}` : version;
|
|
88
|
+
})
|
|
89
|
+
.filter((version) => version !== undefined);
|
|
90
|
+
lines.push(versions.length > 0
|
|
91
|
+
? `- ${need.toolchain}: ${versions.join("; ")}.`
|
|
92
|
+
: `- ${need.toolchain}: not on this machine - report it with report_environment_error if the work needs it.`);
|
|
93
|
+
const dependencies = DEPENDENCIES[need.toolchain];
|
|
94
|
+
if (dependencies !== undefined && versions.length > 0)
|
|
95
|
+
lines.push(` ${dependencies}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push("- The network is open: the build tools fetch what the manifest declares.", "</your folder and this machine>");
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CONFIG_FILE, readProjectConfig } from "./config.js";
|
|
2
|
+
import { c } from "./module-report.js";
|
|
3
|
+
import { parseArgs, runLoop } from "./loop-cli.js";
|
|
4
|
+
const USAGE = `usage: turri regenerate [file.plain] [frid ...] [options]
|
|
5
|
+
|
|
6
|
+
Regenerates the module's code from its spec as it is now, and everything it requires, bottom first, as
|
|
7
|
+
the chat does: what is left to regenerate - from what each module's code was regenerated from to its
|
|
8
|
+
spec now - is planned into jobs, and they run one after the other until the code is regenerated from
|
|
9
|
+
the spec, each followed by the conformance tests of what it regenerated when they are on. Then it says
|
|
10
|
+
how each went. With functional specs named, those are regenerated though their spec did not change - as regenerate
|
|
11
|
+
does in the chat with functionalities named: their code rewritten or started over, doing what the spec says.
|
|
12
|
+
|
|
13
|
+
The project's config.turri.yaml, in the current directory, gives the defaults: module (the file to
|
|
14
|
+
build when none is given), coding-model (the agents that build), harness-model (the planner), and
|
|
15
|
+
conformance-tests.
|
|
16
|
+
|
|
17
|
+
--dry-run plan the work and print the prompt of the first job, and run nothing (turri glimpse)
|
|
18
|
+
--start-over start over, without asking, a codebase regenerated without a module it now requires
|
|
19
|
+
--from-scratch delete the module's code and conformance tests (its lineage keeps them) and regenerate all of it
|
|
20
|
+
--build-dir <dir> default: plain_modules/<module>/code
|
|
21
|
+
--model <provider/id> model for the agents
|
|
22
|
+
|
|
23
|
+
PLAIN_TEMPLATE_DIRS colon-separated template directories`;
|
|
24
|
+
/** Options of the build this replaced, and what to do instead. */
|
|
25
|
+
const GONE = {
|
|
26
|
+
force: "to regenerate a functionality again, name it: turri regenerate <file.plain> <frid>",
|
|
27
|
+
only: "every module the spec requires is built, bottom first",
|
|
28
|
+
to: "the whole spec is built: stop the jobs to stop partway",
|
|
29
|
+
from: "every functionality not built is queued; built ones are skipped",
|
|
30
|
+
"conformance-root": "conformance tests are in plain_modules/<module>/tests",
|
|
31
|
+
"max-regression-rounds": "each conformance job runs the whole suite",
|
|
32
|
+
"try-it": "run turri release to try what is regenerated",
|
|
33
|
+
conformance: `set conformance-tests: in ${CONFIG_FILE}, or turri conform`,
|
|
34
|
+
"no-conformance": `set conformance-tests: false in ${CONFIG_FILE}`,
|
|
35
|
+
"template-dir": "set PLAIN_TEMPLATE_DIRS",
|
|
36
|
+
};
|
|
37
|
+
const { positional, options } = parseArgs(process.argv.slice(2), ["--only", "--force", "--conformance", "--no-conformance", "--try-it", "--dry-run", "--start-over", "--from-scratch"], USAGE);
|
|
38
|
+
for (const [name, instead] of Object.entries(GONE)) {
|
|
39
|
+
if (options[name] !== undefined)
|
|
40
|
+
console.log(c.dim(`--${name} is no longer an option: ${instead}.`));
|
|
41
|
+
}
|
|
42
|
+
const named = positional[0] ?? readProjectConfig(process.cwd()).module;
|
|
43
|
+
// Functional specs named after the file: regenerated again though their spec did not change.
|
|
44
|
+
const again = positional.slice(1);
|
|
45
|
+
if (!named) {
|
|
46
|
+
console.error(`${USAGE}\n\nNo file given, and ${CONFIG_FILE} names no module.`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// A dry run starts nothing, and from scratch deletes before it plans: the two do not go together.
|
|
50
|
+
if (options["from-scratch"] !== undefined && options["dry-run"] !== undefined) {
|
|
51
|
+
console.error("--from-scratch deletes the module's code before anything is planned: it cannot be a dry run.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
runLoop({
|
|
55
|
+
plainFile: named,
|
|
56
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
57
|
+
...(options["build-dir"] !== undefined ? { buildDir: options["build-dir"] } : {}),
|
|
58
|
+
dryRun: options["dry-run"] !== undefined,
|
|
59
|
+
startOver: options["start-over"] !== undefined,
|
|
60
|
+
ask: (loop) => again.length > 0
|
|
61
|
+
? loop.regenerateAgain(named, again, options.model !== undefined ? { model: options.model } : {})
|
|
62
|
+
: options["from-scratch"] !== undefined
|
|
63
|
+
? loop.fromScratch(named, options.model !== undefined ? { model: options.model } : {})
|
|
64
|
+
: loop.bringUp(named, options.model !== undefined ? { model: options.model } : {}),
|
|
65
|
+
}).catch((error) => {
|
|
66
|
+
console.error(error instanceof Error ? error.message : error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createDeployTools, DEPLOY_TOOLS } from "./release-tools.js";
|
|
5
|
+
import { hostDirOf, shortPath } from "./regenerate/release.js";
|
|
6
|
+
import { projectDirOf } from "./regenerate/paths.js";
|
|
7
|
+
import { ask, BEGIN, createExecutor } from "./pi.js";
|
|
8
|
+
/** The person's own shell, in the deployed folder: it installs and runs what the release needs. */
|
|
9
|
+
function hostBash(dir) {
|
|
10
|
+
const bash = createBashToolDefinition(dir);
|
|
11
|
+
return {
|
|
12
|
+
...bash,
|
|
13
|
+
description: `${bash.description}\n\nIt runs on the person's own machine, in ${shortPath(dir)}. Never start a server here: activate does.`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** A file tool that writes, refused inside plain_modules: the built code is the spec's to change, never a release's. */
|
|
17
|
+
function outsideBuiltCode(tool, dir) {
|
|
18
|
+
return {
|
|
19
|
+
...tool,
|
|
20
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
21
|
+
const path = resolve(dir, String(params.path ?? ""));
|
|
22
|
+
if (relative(dir, path).split(sep).includes("plain_modules")) {
|
|
23
|
+
throw new Error(`Not written: ${shortPath(path)} is built code. A release never changes it - a fix belongs in the spec.`);
|
|
24
|
+
}
|
|
25
|
+
return tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function shown(file, max = 40_000) {
|
|
30
|
+
const text = readFileSync(file, "utf8");
|
|
31
|
+
const body = text.length > max ? `${text.slice(0, max)}\n… [the rest of ${shortPath(file)} is not shown]` : text;
|
|
32
|
+
return `<file path="${shortPath(file)}">\n${body.trimEnd()}\n</file>`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What the project says of itself, for the agent to find release guidance in: its AGENTS.md and its skills
|
|
36
|
+
* (`.agents/skills`, `.pi/skills`, `.claude/skills`), where turri runs and in the host codebase - once each, whole.
|
|
37
|
+
*/
|
|
38
|
+
export function projectNotes(dirs) {
|
|
39
|
+
const roots = [...new Set(dirs.map((dir) => resolve(dir)))];
|
|
40
|
+
const files = new Set();
|
|
41
|
+
for (const root of roots) {
|
|
42
|
+
const agents = join(root, "AGENTS.md");
|
|
43
|
+
if (existsSync(agents))
|
|
44
|
+
files.add(agents);
|
|
45
|
+
for (const skills of [".agents/skills", ".pi/skills", ".claude/skills"].map((folder) => join(root, folder))) {
|
|
46
|
+
if (!existsSync(skills))
|
|
47
|
+
continue;
|
|
48
|
+
for (const name of readdirSync(skills)) {
|
|
49
|
+
const skill = join(skills, name, "SKILL.md");
|
|
50
|
+
if (existsSync(skill))
|
|
51
|
+
files.add(skill);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...files].map((file) => shown(file));
|
|
56
|
+
}
|
|
57
|
+
/** Run the release agent to its end; its last reply. */
|
|
58
|
+
export async function runReleaseAgent(input) {
|
|
59
|
+
const { plainFile } = input;
|
|
60
|
+
const tools = createDeployTools({ cwd: input.cwd, state: input.state, defaultPlainFile: () => plainFile });
|
|
61
|
+
// It releases from the host codebase, which holds dist too.
|
|
62
|
+
const dir = hostDirOf(plainFile, input.cwd);
|
|
63
|
+
const notes = projectNotes([input.cwd, dir, projectDirOf(plainFile)]);
|
|
64
|
+
const said = input.guidance ?? input.state.guidance.get(input.afterPulse?.moduleName ?? "");
|
|
65
|
+
const fileTools = [
|
|
66
|
+
createReadToolDefinition(dir),
|
|
67
|
+
createLsToolDefinition(dir),
|
|
68
|
+
createGrepToolDefinition(dir),
|
|
69
|
+
createFindToolDefinition(dir),
|
|
70
|
+
// A guided release may say to change the host - a registration, an import.
|
|
71
|
+
outsideBuiltCode(createEditToolDefinition(dir), dir),
|
|
72
|
+
outsideBuiltCode(createWriteToolDefinition(dir), dir),
|
|
73
|
+
];
|
|
74
|
+
const executor = await createExecutor({
|
|
75
|
+
// Its sessions are kept in the project, not in the folder it deploys.
|
|
76
|
+
cwd: projectDirOf(plainFile),
|
|
77
|
+
customTools: [...fileTools, hostBash(dir), ...tools],
|
|
78
|
+
tools: [...fileTools.map((tool) => tool.name), "bash", ...DEPLOY_TOOLS],
|
|
79
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
80
|
+
role: "release",
|
|
81
|
+
system: [
|
|
82
|
+
"You get software that was just built running on the person's own machine, so they can try it. You ask no one: do it.",
|
|
83
|
+
`Your file tools and your shell start in ${dir}. Your shell is the person's own - not a sandbox.`,
|
|
84
|
+
].join("\n"),
|
|
85
|
+
task: [
|
|
86
|
+
input.afterPulse !== undefined
|
|
87
|
+
? `A pulse of module ${input.afterPulse.moduleName} just landed: ${input.afterPulse.subject}. You release it now, before the module's next pulse starts - if the project's release guidance says to release after a build or a pulse.`
|
|
88
|
+
: `Release ${shortPath(plainFile)} for the person to try, with the release tool, then do what it says.`,
|
|
89
|
+
"",
|
|
90
|
+
"First ask yourself: is there guidance for how this project releases it - what the person said below, the project's",
|
|
91
|
+
"notes and skills below, or anything else you find? If there is, call release with it as guidance, in full: the code is",
|
|
92
|
+
"put in dist/<module>, and the guidance takes it on from there. If there is none, call release without guidance: the",
|
|
93
|
+
"code is put in dist/<module> and started from there, as best it can be.",
|
|
94
|
+
...(input.afterPulse !== undefined
|
|
95
|
+
? [
|
|
96
|
+
"",
|
|
97
|
+
"Released after a pulse: when nothing in the guidance says to release after a build, call nothing and say so in a line.",
|
|
98
|
+
"When release says it is already released, leave it at that.",
|
|
99
|
+
]
|
|
100
|
+
: []),
|
|
101
|
+
...(input.inModule !== undefined ? ["", `Release the codebase of module ${input.inModule}: pass in_module.`] : []),
|
|
102
|
+
...(said !== undefined ? ["", "What the person said of how to release it:", said] : []),
|
|
103
|
+
...(notes.length > 0 ? ["", "The project's notes and skills:", ...notes] : []),
|
|
104
|
+
"",
|
|
105
|
+
"End with what you did, in a line or two: where it runs, or why nothing was released.",
|
|
106
|
+
].join("\n"),
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
return await ask(executor, BEGIN, input.onDelta);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
executor.dispose();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { configForSpec, modelSpec, projectModels, readProjectConfig } from "./config.js";
|
|
4
|
+
import { createDeployState } from "./release-tools.js";
|
|
5
|
+
import { runReleaseAgent } from "./release-agent.js";
|
|
6
|
+
import { listDeployed, runningDeployed, stopDeployed } from "./regenerate/release.js";
|
|
7
|
+
const USAGE = `usage: turri release [file.plain] [options]
|
|
8
|
+
|
|
9
|
+
Releases the module on top of the spec's requires chain, which holds the code of the ones below, and
|
|
10
|
+
has an agent start it here, on this machine. The module's code, as its last commit has it, always
|
|
11
|
+
goes in dist/<module> first. The agent looks for guidance on how this project releases it - what
|
|
12
|
+
--guidance says, the project's AGENTS.md - and follows it from there when there is some: an
|
|
13
|
+
integration its host application takes with the host's own scripts, say. When there is none, it
|
|
14
|
+
reads how the code in dist runs, installs what it needs, and starts what serves. Either way it asks
|
|
15
|
+
nothing, and stays in the foreground with the servers' output; Ctrl-C stops them.
|
|
16
|
+
|
|
17
|
+
The project's config.turri.yaml gives the defaults: module (the file when none is given) and
|
|
18
|
+
harness-model (the agent's model).
|
|
19
|
+
|
|
20
|
+
--in-module <module> the codebase to take, when modules built on this one branch
|
|
21
|
+
--guidance <text> how this project releases it: the scripts to run, in order
|
|
22
|
+
--model <provider/id> the agent's model (default: harness-model)`;
|
|
23
|
+
const c = {
|
|
24
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
25
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
26
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
27
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
28
|
+
};
|
|
29
|
+
const PROBLEMS = "If you are having problems, use chat mode to deploy: run turri and ask to try it.";
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const positional = [];
|
|
32
|
+
let model;
|
|
33
|
+
let inModule;
|
|
34
|
+
let guidance;
|
|
35
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
36
|
+
const arg = argv[i];
|
|
37
|
+
if (arg === "-h" || arg === "--help") {
|
|
38
|
+
console.log(USAGE);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
else if (arg === "--model" || arg === "--harness-model")
|
|
42
|
+
model = argv[++i];
|
|
43
|
+
else if (arg === "--in-module")
|
|
44
|
+
inModule = argv[++i];
|
|
45
|
+
else if (arg === "--guidance")
|
|
46
|
+
guidance = argv[++i];
|
|
47
|
+
else if (arg.startsWith("-")) {
|
|
48
|
+
console.error(`unknown option: ${arg}\n\n${USAGE}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
positional.push(arg);
|
|
53
|
+
}
|
|
54
|
+
return { plainFile: positional[0], model, inModule, guidance };
|
|
55
|
+
}
|
|
56
|
+
/** Follow every running server's output, prefixed with its name, until Ctrl-C or they have all ended. */
|
|
57
|
+
function followLogs() {
|
|
58
|
+
const offsets = new Map();
|
|
59
|
+
return new Promise((done) => {
|
|
60
|
+
const tick = () => {
|
|
61
|
+
const running = runningDeployed();
|
|
62
|
+
for (const item of listDeployed()) {
|
|
63
|
+
let size = 0;
|
|
64
|
+
try {
|
|
65
|
+
size = statSync(item.log).size;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const from = offsets.get(item.log) ?? 0;
|
|
71
|
+
if (size <= from)
|
|
72
|
+
continue;
|
|
73
|
+
const buffer = Buffer.alloc(size - from);
|
|
74
|
+
const fd = openSync(item.log, "r");
|
|
75
|
+
readSync(fd, buffer, 0, buffer.length, from);
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
offsets.set(item.log, size);
|
|
78
|
+
for (const line of buffer.toString("utf8").split("\n").filter((l) => l !== ""))
|
|
79
|
+
console.log(`${c.cyan(`[${item.name}]`)} ${line}`);
|
|
80
|
+
}
|
|
81
|
+
if (running.length === 0) {
|
|
82
|
+
clearInterval(timer);
|
|
83
|
+
console.log(c.dim("Everything it started has ended."));
|
|
84
|
+
done();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const timer = setInterval(tick, 500);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function main() {
|
|
91
|
+
const args = parseArgs(process.argv.slice(2));
|
|
92
|
+
const named = args.plainFile ?? readProjectConfig(process.cwd()).module;
|
|
93
|
+
if (named === undefined) {
|
|
94
|
+
console.error(`${USAGE}\n\nNo file given, and config.turri.yaml names no module.`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const plainFile = resolve(named);
|
|
98
|
+
// A release follows steps - a script, a start command: the coding model does that well, thinking low.
|
|
99
|
+
const model = args.model ?? modelSpec(projectModels(configForSpec(plainFile)).coding);
|
|
100
|
+
const state = createDeployState();
|
|
101
|
+
const stopAll = async () => {
|
|
102
|
+
const stopped = await stopDeployed();
|
|
103
|
+
if (stopped.length > 0)
|
|
104
|
+
console.log(c.dim(`\nStopped ${stopped.join(", ")}.`));
|
|
105
|
+
};
|
|
106
|
+
process.on("SIGINT", () => {
|
|
107
|
+
void stopAll().then(() => process.exit(0));
|
|
108
|
+
});
|
|
109
|
+
console.log(c.dim(`\nworking out how to start it${model !== undefined ? ` (${model})` : ""}…\n`));
|
|
110
|
+
await runReleaseAgent({
|
|
111
|
+
plainFile,
|
|
112
|
+
cwd: process.cwd(),
|
|
113
|
+
state,
|
|
114
|
+
...(model !== undefined ? { model } : {}),
|
|
115
|
+
...(args.inModule !== undefined ? { inModule: args.inModule } : {}),
|
|
116
|
+
...(args.guidance !== undefined ? { guidance: args.guidance } : {}),
|
|
117
|
+
onDelta: (delta) => process.stdout.write(delta),
|
|
118
|
+
});
|
|
119
|
+
console.log(`\n\n${c.dim(PROBLEMS)}`);
|
|
120
|
+
if (runningDeployed().length === 0)
|
|
121
|
+
process.exit(0);
|
|
122
|
+
console.log(c.green(c.bold("\nRunning - Ctrl-C stops it.")) + c.dim(" Its output follows.\n"));
|
|
123
|
+
await followLogs();
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
main().catch(async (error) => {
|
|
127
|
+
await stopDeployed();
|
|
128
|
+
console.error(error instanceof Error ? error.message : error);
|
|
129
|
+
console.error(`\n${PROBLEMS}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { deployLogDir, deployTarget, describeDeployment, exportModule, hostDirOf, shortPath, startDeployed, stopDeployed, } from "./regenerate/release.js";
|
|
5
|
+
import { projectDirOf } from "./regenerate/paths.js";
|
|
6
|
+
import { haltTool } from "./loop-tools.js";
|
|
7
|
+
export function createDeployState() {
|
|
8
|
+
return { released: new Map(), guidance: new Map() };
|
|
9
|
+
}
|
|
10
|
+
/** What the agent is to do once the code is in dist - in the deploy tool's answer, and the CLI agent's prompt. */
|
|
11
|
+
export function deployGuide(dir) {
|
|
12
|
+
const where = shortPath(dir);
|
|
13
|
+
return [
|
|
14
|
+
`What to do now, to have the person try it - it runs on this machine, outside the sandbox, from ${where}:`,
|
|
15
|
+
`1. Find out how it runs: its README, its manifests (package.json, pyproject.toml, requirements.txt, go.mod, Cargo.toml, pom.xml, build.gradle, docker-compose.yml) and entry points, in ${where}.`,
|
|
16
|
+
"2. Check the toolchains it needs are on this machine (`command -v node`, `python3 --version`, `go version`, ...). One missing: install it with this machine's package manager (brew on macOS, apt or dnf on Linux), then go on.",
|
|
17
|
+
"3. Install what it needs yourself, in your shell - dependencies into a virtual environment or node_modules, a build that fetches packages, an image pull. Do not ask the person: do it, and fix what fails.",
|
|
18
|
+
"4. Start anything that keeps running - a backend, a frontend's dev server - with activate, with the URL it serves at: never in the shell with & or nohup. An app with a backend and a frontend: start both, the backend first.",
|
|
19
|
+
"5. Then tell the person, briefly. Something that serves: only where to open it - the frontend's URL, and the API's in one line when there is one. A CLI or a library: a few commands to paste, run from the folder, with what they print. A port that is taken: say which and by what, and do not move the app to another.",
|
|
20
|
+
`Do not change the code in ${where} - it is a copy of what was built, and a fix belongs in the spec. A config file it needs made from its example (a .env from .env.example) is fine.`,
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What the agent is to do on a guided release: the code is in dist, as on any release, and the guidance takes it on from
|
|
25
|
+
* there. The rules of a release to dist hold here too: what keeps running started with activate, and the built code never
|
|
26
|
+
* edited - and nothing put to the person.
|
|
27
|
+
*/
|
|
28
|
+
export function guidedGuide(deployment, guidance) {
|
|
29
|
+
const host = shortPath(deployment.hostDir ?? deployment.dir);
|
|
30
|
+
const dist = shortPath(deployment.dir);
|
|
31
|
+
const built = shortPath(deployment.buildDir);
|
|
32
|
+
return [
|
|
33
|
+
"The guidance for releasing it - follow it:",
|
|
34
|
+
guidance.trim(),
|
|
35
|
+
"",
|
|
36
|
+
`What to do now - on this machine, outside the sandbox, from ${host}:`,
|
|
37
|
+
`1. The code to release is in ${dist}: the last commit that landed whole. Take it from there, never from ${built}, where a pulse may be writing. Where the guidance's own tooling reads ${built}, point it at ${dist} if it can take another source; if it cannot, do by hand, from ${dist}, what it does.`,
|
|
38
|
+
`2. Read what the guidance leaves open - the scripts, files and commands it names, in ${host} - then do what it says, in its order. Where it says to change the host - a registration, an import, a config entry - change it. Never change ${built} or ${dist}: a fix to the code belongs in the spec.`,
|
|
39
|
+
"3. Run the project's scripts and install what it needs yourself, in your shell. Do not ask the person: do it, and fix what fails.",
|
|
40
|
+
"4. Start what keeps running - a start script, a dev server - with activate, with the URL it serves at - never in the shell with & or nohup.",
|
|
41
|
+
`5. Then tell the person briefly: what was done, what you changed in ${host} if anything, and where to open it or how to try it. Where the guidance does not fit what you find, say so, and do not guess past it.`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
function text(value, details = {}) {
|
|
45
|
+
return { content: [{ type: "text", text: value }], details };
|
|
46
|
+
}
|
|
47
|
+
/** The folder a command runs in: dist's, or - guided - the host codebase's, or one inside either. */
|
|
48
|
+
function inside(current, dir) {
|
|
49
|
+
const base = current.hostDir ?? current.dir;
|
|
50
|
+
const target = resolve(base, dir ?? ".");
|
|
51
|
+
const within = (root) => {
|
|
52
|
+
const rel = relative(root, target);
|
|
53
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
54
|
+
};
|
|
55
|
+
if (!within(base) && !within(current.dir))
|
|
56
|
+
throw new Error(`${dir} is outside ${shortPath(base)}: what release starts runs from there.`);
|
|
57
|
+
return target;
|
|
58
|
+
}
|
|
59
|
+
export function createDeployTools(options) {
|
|
60
|
+
const { state } = options;
|
|
61
|
+
const needsDeploy = () => {
|
|
62
|
+
if (state.current === undefined)
|
|
63
|
+
throw new Error("Nothing is released yet: call release first.");
|
|
64
|
+
return state.current;
|
|
65
|
+
};
|
|
66
|
+
const deploy = defineTool({
|
|
67
|
+
name: "release",
|
|
68
|
+
label: "Release",
|
|
69
|
+
description: "Release a module's code for the person to try - the module on top of the spec's requires chain, which holds the " +
|
|
70
|
+
"code of the ones below - stopping whatever an earlier release of it started. Before you call it, ask yourself: is " +
|
|
71
|
+
"there guidance for releasing this project - in what the person said, the project's AGENTS.md, a skill, or anything " +
|
|
72
|
+
"else you were given? An integration a host application loads, say, released with the host's own scripts. If there " +
|
|
73
|
+
"is, pass it as guidance, in full. Either way its last commit - what last landed whole - is exported to dist/<module> " +
|
|
74
|
+
"first; with guidance it is taken on from there as the guidance says, in the host codebase; without, it is started " +
|
|
75
|
+
"from there, as best it can be. Call it when the " +
|
|
76
|
+
"person wants to try, run, see or use what was built, and whenever guidance says to release after a pulse: when " +
|
|
77
|
+
"the module's last commit is already released it does nothing and says so, so never decide that yourself. It says " +
|
|
78
|
+
"what to do next.",
|
|
79
|
+
promptSnippet: "release the regenerated code for the person to try - as the project's release guidance says, or to dist/<module> - then activate it",
|
|
80
|
+
parameters: Type.Object({
|
|
81
|
+
plain_file: Type.Optional(Type.String({ description: "A .plain file of the module. Default: the project's module from config.turri.yaml." })),
|
|
82
|
+
in_module: Type.Optional(Type.String({ description: "The codebase to take, when modules built on this one branch" })),
|
|
83
|
+
again: Type.Optional(Type.Boolean({ description: "Release even when the module's last commit is already released - only when what the last release said to do was not done." })),
|
|
84
|
+
guidance: Type.Optional(Type.String({ description: "The guidance for releasing this project, whole, from wherever it is - the person, AGENTS.md, a skill: the scripts to run and in which order, where the code goes. Omit when there is none." })),
|
|
85
|
+
}),
|
|
86
|
+
async execute(_id, params) {
|
|
87
|
+
const named = params.plain_file ?? options.defaultPlainFile();
|
|
88
|
+
if (named === undefined)
|
|
89
|
+
throw new Error("Which spec? config.turri.yaml names no module: pass plain_file.");
|
|
90
|
+
const plainFile = resolve(options.cwd, named);
|
|
91
|
+
const target = deployTarget(plainFile, params.in_module);
|
|
92
|
+
const guidance = params.guidance?.trim() || undefined;
|
|
93
|
+
// Released already: the module's last commit is what the last release shipped. Nothing is exported or stopped -
|
|
94
|
+
// releasing it again would only restart what runs - so it is safe to call after every pulse.
|
|
95
|
+
const last = state.released.get(target.entry.moduleName);
|
|
96
|
+
if (last !== undefined && !params.again && target.commit.startsWith(last.commit) && last.way === (guidance === undefined ? "dist" : "guided")) {
|
|
97
|
+
state.current = last;
|
|
98
|
+
return text(`Already released: ${target.entry.moduleName}'s last commit, ${last.commit} (${last.subject}), is what the last release shipped, and what that release said to do was done then. Nothing to do - what runs is left running. Say so in a line.`, { module: last.moduleName, commit: last.commit, already: true });
|
|
99
|
+
}
|
|
100
|
+
// What the last release started runs from what is about to be replaced.
|
|
101
|
+
const stopped = await stopDeployed({ moduleName: target.entry.moduleName });
|
|
102
|
+
// Always the last commit, clean, in dist first; guidance takes it on from there.
|
|
103
|
+
const exported = exportModule(plainFile, params.in_module);
|
|
104
|
+
const deployment = guidance === undefined ? exported : { ...exported, way: "guided", hostDir: hostDirOf(plainFile, options.cwd) };
|
|
105
|
+
state.current = deployment;
|
|
106
|
+
state.released.set(deployment.moduleName, deployment);
|
|
107
|
+
if (guidance !== undefined)
|
|
108
|
+
state.guidance.set(deployment.moduleName, guidance);
|
|
109
|
+
return text([
|
|
110
|
+
describeDeployment(deployment),
|
|
111
|
+
...(stopped.length > 0 ? [`Stopped what the last release started: ${stopped.join(", ")}.`] : []),
|
|
112
|
+
"",
|
|
113
|
+
guidance === undefined ? deployGuide(deployment.dir) : guidedGuide(deployment, guidance),
|
|
114
|
+
].join("\n"), { module: deployment.moduleName, way: deployment.way, dir: deployment.dir, ...(deployment.hostDir !== undefined ? { host: deployment.hostDir } : {}), commit: deployment.commit });
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const startTool = defineTool({
|
|
118
|
+
name: "activate",
|
|
119
|
+
label: "Activate",
|
|
120
|
+
description: "Start something that keeps running - a backend, a frontend's dev server - from the deployed folder, on this " +
|
|
121
|
+
"machine: in the background, its output kept, stopped on the next release, on /halt and when turri exits. " +
|
|
122
|
+
"With `url`, waits until something answers there and says so, or shows the end of its output. A port something " +
|
|
123
|
+
"else holds is said, and nothing is started.",
|
|
124
|
+
promptSnippet: "start a server of the deployed code, and wait for it to answer",
|
|
125
|
+
parameters: Type.Object({
|
|
126
|
+
name: Type.String({ description: "A short name for it: backend, frontend, app" }),
|
|
127
|
+
command: Type.String({ description: "The command that starts it, as run from its folder" }),
|
|
128
|
+
url: Type.Optional(Type.String({ description: "Where it serves, e.g. http://localhost:5173 - it is ready once this answers" })),
|
|
129
|
+
dir: Type.Optional(Type.String({ description: "A folder inside the deployed one to run it in, e.g. frontend. Default: the deployed folder." })),
|
|
130
|
+
}),
|
|
131
|
+
async execute(_id, params) {
|
|
132
|
+
const current = needsDeploy();
|
|
133
|
+
const started = await startDeployed({
|
|
134
|
+
name: params.name,
|
|
135
|
+
command: params.command,
|
|
136
|
+
dir: inside(current, params.dir),
|
|
137
|
+
moduleName: current.moduleName,
|
|
138
|
+
logDir: deployLogDir(projectDirOf(current.plainFile)),
|
|
139
|
+
...(params.url !== undefined ? { url: params.url } : {}),
|
|
140
|
+
});
|
|
141
|
+
return text(started.text, { ok: started.ok, ...(started.process !== undefined ? { pid: started.process.pid, log: started.process.log } : {}) });
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
// The one halt: with the pulses in the conversation, and without them for the release agent.
|
|
145
|
+
const stopTool = haltTool(options.jobs !== undefined ? { jobs: options.jobs } : {});
|
|
146
|
+
return [deploy, startTool, stopTool];
|
|
147
|
+
}
|
|
148
|
+
export const DEPLOY_TOOLS = ["release", "activate", "halt"];
|