@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc
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 +320 -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 +88 -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 +119 -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,384 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { reportActivity } from "./activity.js";
|
|
5
|
+
import { projectRootOf } from "./paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* Version history for each module's generated code and tests.
|
|
8
|
+
*
|
|
9
|
+
* A module folder - `plain_modules/<module>`, holding `code/` and `tests/` - is a git
|
|
10
|
+
* repository whose git directory is `.plaingit` rather than `.git`. Everything git does
|
|
11
|
+
* works as usual; the name is what keeps it from being an embedded repository inside the
|
|
12
|
+
* project's own, so the generated code stays plain files to the outer repo. Only the harness
|
|
13
|
+
* writes it: the agents' fences cover `code/` and `tests/`, and `.plaingit` sits beside them.
|
|
14
|
+
*
|
|
15
|
+
* A commit is made after every step that changes the module - a render, a regeneration, a
|
|
16
|
+
* conformance run, a regression repair, a spec change, a start from the module below - with
|
|
17
|
+
* a message saying what it was. History is a record of the work, never a gate on it: a git
|
|
18
|
+
* failure is swallowed, not raised.
|
|
19
|
+
*/
|
|
20
|
+
export const HISTORY_DIR = ".plaingit";
|
|
21
|
+
const AUTHOR = ["-c", "user.name=turri", "-c", "user.email=turri@localhost", "-c", "commit.gpgsign=false"];
|
|
22
|
+
/**
|
|
23
|
+
* Kept out of history: the history itself - git skips only a folder named `.git` on its own -
|
|
24
|
+
* installed dependencies, and caches.
|
|
25
|
+
*/
|
|
26
|
+
const EXCLUDED = [
|
|
27
|
+
`/${HISTORY_DIR}/`,
|
|
28
|
+
".turri/",
|
|
29
|
+
"node_modules/",
|
|
30
|
+
".venv/",
|
|
31
|
+
"venv/",
|
|
32
|
+
"__pycache__/",
|
|
33
|
+
"*.pyc",
|
|
34
|
+
".pytest_cache/",
|
|
35
|
+
".mypy_cache/",
|
|
36
|
+
".ruff_cache/",
|
|
37
|
+
"target/",
|
|
38
|
+
".cargo/",
|
|
39
|
+
".DS_Store",
|
|
40
|
+
];
|
|
41
|
+
function git(moduleDir, args, input) {
|
|
42
|
+
return spawnSync("git", [...AUTHOR, "--git-dir", join(moduleDir, HISTORY_DIR), "--work-tree", moduleDir, ...args], {
|
|
43
|
+
cwd: moduleDir,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
input,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The module folder a build or tests directory belongs to. */
|
|
49
|
+
export function moduleDirOf(dir) {
|
|
50
|
+
return dirname(dir);
|
|
51
|
+
}
|
|
52
|
+
/** Keep the project's own repository from committing the history's objects. */
|
|
53
|
+
function ignoreInProject(moduleDir) {
|
|
54
|
+
const root = projectRootOf(moduleDir);
|
|
55
|
+
if (!existsSync(join(root, ".git")))
|
|
56
|
+
return;
|
|
57
|
+
const ignore = join(root, ".gitignore");
|
|
58
|
+
const pattern = `plain_modules/**/${HISTORY_DIR}/`;
|
|
59
|
+
const current = existsSync(ignore) ? readFileSync(ignore, "utf8") : "";
|
|
60
|
+
if (current.split("\n").some((line) => line.trim() === pattern))
|
|
61
|
+
return;
|
|
62
|
+
appendFileSync(ignore, `${current === "" || current.endsWith("\n") ? "" : "\n"}${pattern}\n`);
|
|
63
|
+
}
|
|
64
|
+
function ensureHistory(moduleDir) {
|
|
65
|
+
const gitDir = join(moduleDir, HISTORY_DIR);
|
|
66
|
+
if (existsSync(gitDir))
|
|
67
|
+
return true;
|
|
68
|
+
mkdirSync(moduleDir, { recursive: true });
|
|
69
|
+
if (git(moduleDir, ["init", "-q"]).status !== 0)
|
|
70
|
+
return false;
|
|
71
|
+
mkdirSync(join(gitDir, "info"), { recursive: true });
|
|
72
|
+
writeFileSync(join(gitDir, "info", "exclude"), `${EXCLUDED.join("\n")}\n`);
|
|
73
|
+
ignoreInProject(moduleDir);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Commit whatever the step changed in the module folder that `dir` - its build or its tests
|
|
78
|
+
* directory - belongs to. Nothing changed, nothing is committed. Returns the commit, if any.
|
|
79
|
+
*/
|
|
80
|
+
export function recordHistory(dir, subject, body) {
|
|
81
|
+
try {
|
|
82
|
+
const moduleDir = moduleDirOf(dir);
|
|
83
|
+
if (!existsSync(moduleDir) || !ensureHistory(moduleDir))
|
|
84
|
+
return undefined;
|
|
85
|
+
git(moduleDir, ["add", "-A"]);
|
|
86
|
+
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const message = body ? `${subject}\n\n${body.trim()}\n` : subject;
|
|
89
|
+
if (git(moduleDir, ["commit", "-q", "-F", "-"], message).status !== 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
return head(dir);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The notes the harness writes once a pulse has ended, committed - or the next pulse, putting the module back to its last
|
|
99
|
+
* commit, would take them away. Into the pulse's own commit when it landed one - `landedSince` (ms), when it started - so
|
|
100
|
+
* the history keeps a commit a pulse; a commit of their own otherwise - a pulse rolled back leaves its line, and nothing
|
|
101
|
+
* else.
|
|
102
|
+
*/
|
|
103
|
+
export function commitNotes(dir, file, subject, landedSince) {
|
|
104
|
+
try {
|
|
105
|
+
const moduleDir = moduleDirOf(dir);
|
|
106
|
+
if (!existsSync(moduleDir) || !ensureHistory(moduleDir))
|
|
107
|
+
return;
|
|
108
|
+
const path = [basename(dir), file].join("/");
|
|
109
|
+
git(moduleDir, ["add", "--", path]);
|
|
110
|
+
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
111
|
+
return;
|
|
112
|
+
const last = Number(git(moduleDir, ["log", "-1", "--format=%ct"]).stdout.trim()) * 1000;
|
|
113
|
+
const ownCommit = landedSince !== undefined && Number.isFinite(last) && last >= Math.floor(landedSince / 1000) * 1000;
|
|
114
|
+
git(moduleDir, ownCommit ? ["commit", "-q", "--amend", "--no-edit"] : ["commit", "-q", "-F", "-"], ownCommit ? undefined : subject);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// History is a record, not a gate.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Put a module back to its last commit before work starts on it.
|
|
122
|
+
*
|
|
123
|
+
* Every step commits what it did, so a module with changes nobody committed is one whose last
|
|
124
|
+
* step never finished - turri was stopped or restarted mid-job - and holds half-written
|
|
125
|
+
* code the next render would build on. Its last commit is the last known good state, so the
|
|
126
|
+
* working tree is reset to it: changed files restored, files nothing committed removed. What
|
|
127
|
+
* history does not track stays - installed dependencies, caches - so nothing needs installing
|
|
128
|
+
* again. A module with no commit yet has no good state to go back to and is left alone -
|
|
129
|
+
* `startWork` gives it one.
|
|
130
|
+
*
|
|
131
|
+
* Returns the paths that were put back or removed, relative to the module folder.
|
|
132
|
+
*/
|
|
133
|
+
/**
|
|
134
|
+
* The tests folder's build output too - ignored, so a clean leaves it: reports and classes of tests
|
|
135
|
+
* that are no longer there tell whoever looks that they are. The code's is kept: it can be a cache
|
|
136
|
+
* worth having, and the code is what is built from.
|
|
137
|
+
*/
|
|
138
|
+
function cleanTestsBuild(moduleDir) {
|
|
139
|
+
if (existsSync(join(moduleDir, "tests")))
|
|
140
|
+
git(moduleDir, ["clean", "-q", "-fdX", "--", "tests"]);
|
|
141
|
+
}
|
|
142
|
+
export function resetToLastCommit(dir) {
|
|
143
|
+
try {
|
|
144
|
+
const moduleDir = moduleDirOf(dir);
|
|
145
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)) || head(dir) === undefined)
|
|
146
|
+
return [];
|
|
147
|
+
const status = git(moduleDir, ["status", "--porcelain", "--untracked-files=all"]);
|
|
148
|
+
if (status.status !== 0)
|
|
149
|
+
return [];
|
|
150
|
+
const changed = status.stdout
|
|
151
|
+
.split("\n")
|
|
152
|
+
.filter((line) => line.trim() !== "")
|
|
153
|
+
.map((line) => line.slice(3));
|
|
154
|
+
cleanTestsBuild(moduleDir);
|
|
155
|
+
if (changed.length === 0)
|
|
156
|
+
return [];
|
|
157
|
+
git(moduleDir, ["reset", "-q", "--hard", "HEAD"]);
|
|
158
|
+
git(moduleDir, ["clean", "-q", "-fd"]);
|
|
159
|
+
return changed;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** The module's commit, made first when it has none: what is there, as found, or an empty one. */
|
|
166
|
+
function ensureCommit(moduleDir) {
|
|
167
|
+
mkdirSync(moduleDir, { recursive: true });
|
|
168
|
+
if (!ensureHistory(moduleDir))
|
|
169
|
+
return undefined;
|
|
170
|
+
const current = git(moduleDir, ["rev-parse", "HEAD"]);
|
|
171
|
+
if (current.status === 0)
|
|
172
|
+
return current.stdout.trim();
|
|
173
|
+
git(moduleDir, ["add", "-A"]);
|
|
174
|
+
git(moduleDir, ["commit", "-q", "--allow-empty", "-m", "the module as turri found it"]);
|
|
175
|
+
const made = git(moduleDir, ["rev-parse", "HEAD"]);
|
|
176
|
+
return made.status === 0 ? made.stdout.trim() : undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The start of every unit of work that writes a module's code or tests: each module folder it
|
|
180
|
+
* writes back at its last commit - saying so when that undid anything - and that commit kept, to
|
|
181
|
+
* go back to if the work fails. A module with no commit yet is given one first, so there always
|
|
182
|
+
* is one.
|
|
183
|
+
*/
|
|
184
|
+
export function startWork(dirs, moduleName) {
|
|
185
|
+
const modules = [];
|
|
186
|
+
for (const moduleDir of [...new Set(dirs.map(moduleDirOf))]) {
|
|
187
|
+
try {
|
|
188
|
+
const commit = ensureCommit(moduleDir);
|
|
189
|
+
if (commit === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
const undone = resetToLastCommit(join(moduleDir, "code"));
|
|
192
|
+
if (undone.length > 0) {
|
|
193
|
+
const shown = undone.slice(0, 5).join(", ");
|
|
194
|
+
reportActivity(`${moduleName}: back to its last commit - ${undone.length} change${undone.length === 1 ? "" : "s"} an unfinished ` +
|
|
195
|
+
`run left behind undone (${shown}${undone.length > 5 ? ", …" : ""})`);
|
|
196
|
+
}
|
|
197
|
+
modules.push({ moduleDir, commit });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// History is a record, not a gate: without it, the work goes on.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { modules };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Where work already under way is now, to go back to if what comes next fails - without
|
|
207
|
+
* putting anything back first: what is uncommitted belongs to the work that is under way.
|
|
208
|
+
*/
|
|
209
|
+
export function markWork(dirs) {
|
|
210
|
+
const modules = [];
|
|
211
|
+
for (const moduleDir of [...new Set(dirs.map(moduleDirOf))]) {
|
|
212
|
+
try {
|
|
213
|
+
const commit = ensureCommit(moduleDir);
|
|
214
|
+
if (commit !== undefined)
|
|
215
|
+
modules.push({ moduleDir, commit });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// History is a record, not a gate.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { modules };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Undo a unit of work that failed: every module folder it wrote back at the commit it started
|
|
225
|
+
* from - its changes, the files it added and any commit it made since, gone. What history does
|
|
226
|
+
* not track stays: installed dependencies, caches.
|
|
227
|
+
*/
|
|
228
|
+
export function abandonWork(start, moduleName, why) {
|
|
229
|
+
for (const { moduleDir, commit } of start?.modules ?? []) {
|
|
230
|
+
try {
|
|
231
|
+
const moved = git(moduleDir, ["rev-parse", "HEAD"]).stdout.trim() !== commit;
|
|
232
|
+
const status = git(moduleDir, ["status", "--porcelain", "--untracked-files=all"]).stdout.trim();
|
|
233
|
+
if (!moved && status === "")
|
|
234
|
+
continue;
|
|
235
|
+
git(moduleDir, ["reset", "-q", "--hard", commit]);
|
|
236
|
+
git(moduleDir, ["clean", "-q", "-fd"]);
|
|
237
|
+
cleanTestsBuild(moduleDir);
|
|
238
|
+
reportActivity(`${moduleName}: ${why} - ${basename(moduleDir)} is back where the work started`);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Nothing to undo with.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A file of a module as its last commit has it - `file` relative to `dir`, its build or its tests
|
|
247
|
+
* folder - or undefined when there is no history, no commit, or the file is not in it.
|
|
248
|
+
*/
|
|
249
|
+
export function committedText(dir, file) {
|
|
250
|
+
try {
|
|
251
|
+
const moduleDir = moduleDirOf(dir);
|
|
252
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
253
|
+
return undefined;
|
|
254
|
+
const path = [basename(dir), file].join("/");
|
|
255
|
+
const shown = git(moduleDir, ["show", `HEAD:${path}`]);
|
|
256
|
+
return shown.status === 0 ? shown.stdout : undefined;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Commit these paths of a module, and nothing else of it - removed, added or changed. Whatever else is in the folder,
|
|
264
|
+
* uncommitted - what an interrupted job left - stays as it is, for the next unit of work to put back. Returns the commit.
|
|
265
|
+
*/
|
|
266
|
+
export function commitPaths(moduleDir, paths, subject) {
|
|
267
|
+
if (paths.length === 0 || !existsSync(join(moduleDir, HISTORY_DIR)))
|
|
268
|
+
return undefined;
|
|
269
|
+
git(moduleDir, ["add", "-A", "--", ...paths]);
|
|
270
|
+
if (git(moduleDir, ["diff", "--cached", "--quiet", "--", ...paths]).status === 0)
|
|
271
|
+
return undefined;
|
|
272
|
+
if (git(moduleDir, ["commit", "-q", "-m", subject, "--", ...paths]).status !== 0)
|
|
273
|
+
return undefined;
|
|
274
|
+
return git(moduleDir, ["rev-parse", "HEAD"]).stdout.trim() || undefined;
|
|
275
|
+
}
|
|
276
|
+
/** Bring a history's exclude list up to the one it is made with now. */
|
|
277
|
+
export function refreshExcludes(moduleDir) {
|
|
278
|
+
const exclude = join(moduleDir, HISTORY_DIR, "info", "exclude");
|
|
279
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
280
|
+
return;
|
|
281
|
+
mkdirSync(dirname(exclude), { recursive: true });
|
|
282
|
+
writeFileSync(exclude, `${EXCLUDED.join("\n")}\n`);
|
|
283
|
+
}
|
|
284
|
+
/** The current commit of a module's history, if it has one. */
|
|
285
|
+
export function head(dir) {
|
|
286
|
+
const moduleDir = moduleDirOf(dir);
|
|
287
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
288
|
+
return undefined;
|
|
289
|
+
const result = git(moduleDir, ["rev-parse", "--short", "HEAD"]);
|
|
290
|
+
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
291
|
+
}
|
|
292
|
+
/** The module's history, newest first, one line per commit. */
|
|
293
|
+
export function historyLog(dir, limit = 30) {
|
|
294
|
+
const moduleDir = moduleDirOf(dir);
|
|
295
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
296
|
+
return `No history for ${basename(moduleDir)} yet.`;
|
|
297
|
+
const result = git(moduleDir, ["log", `-${limit}`, "--format=%h %ad %s", "--date=format:%Y-%m-%d %H:%M"]);
|
|
298
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trimEnd() : `No history for ${basename(moduleDir)} yet.`;
|
|
299
|
+
}
|
|
300
|
+
/** One commit in full: its message and what it changed. */
|
|
301
|
+
export function historyShow(dir, commit) {
|
|
302
|
+
const moduleDir = moduleDirOf(dir);
|
|
303
|
+
const result = git(moduleDir, ["show", "--stat", "--patch", "--format=%h %ad%n%n%B", "--date=format:%Y-%m-%d %H:%M", commit]);
|
|
304
|
+
return result.status === 0 ? result.stdout : result.stderr.trim() || `No commit ${commit}.`;
|
|
305
|
+
}
|
|
306
|
+
/** Kept out of a work's diff: the harness's own files, and lockfiles - long, generated, and nothing to review. */
|
|
307
|
+
const NOT_REVIEWED = /(^|\/)(\.turri\.json|\.turri-notes\.md|package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|uv\.lock|poetry\.lock|Pipfile\.lock|Cargo\.lock|go\.sum|composer\.lock|Gemfile\.lock)$/;
|
|
308
|
+
/** How much of a work's diff is shown whole; past it, the rest is named, to be read from the files. */
|
|
309
|
+
const WORK_DIFF_MAX = 80_000;
|
|
310
|
+
/**
|
|
311
|
+
* What the work under way changed in `dir` - the code folder, or the tests folder - since the module's last
|
|
312
|
+
* commit: a unified diff of every file changed, and every new file whole. Every unit of work starts from that
|
|
313
|
+
* commit, so this is exactly what its agents wrote. Lockfiles and the harness's own files are left out. Empty when
|
|
314
|
+
* there is no history, or nothing changed.
|
|
315
|
+
*/
|
|
316
|
+
export function workDiff(dir) {
|
|
317
|
+
const moduleDir = moduleDirOf(dir);
|
|
318
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
319
|
+
return "";
|
|
320
|
+
const folder = basename(dir);
|
|
321
|
+
// Paths relative to `dir`, where the agent reading the diff works.
|
|
322
|
+
const within = (path) => (path.startsWith(`${folder}/`) ? path.slice(folder.length + 1) : path);
|
|
323
|
+
const changed = git(moduleDir, ["diff", "HEAD", "--no-color", "--name-only", "--", folder]).stdout.split("\n").filter(Boolean).map(within);
|
|
324
|
+
const added = git(moduleDir, ["ls-files", "--others", "--exclude-standard", "--", folder]).stdout.split("\n").filter(Boolean).map(within);
|
|
325
|
+
const parts = [];
|
|
326
|
+
for (const file of changed.filter((path) => !NOT_REVIEWED.test(path))) {
|
|
327
|
+
parts.push(git(moduleDir, ["diff", "HEAD", "--no-color", `--relative=${folder}`, "--", `${folder}/${file}`]).stdout.trimEnd());
|
|
328
|
+
}
|
|
329
|
+
// A new file, whole: git diff of it against nothing.
|
|
330
|
+
for (const file of added.filter((path) => !NOT_REVIEWED.test(path))) {
|
|
331
|
+
parts.push(spawnSync("git", ["diff", "--no-index", "--no-color", "--", "/dev/null", file], { cwd: dir, encoding: "utf8" }).stdout.trimEnd());
|
|
332
|
+
}
|
|
333
|
+
const diff = parts.filter(Boolean).join("\n");
|
|
334
|
+
if (diff.length <= WORK_DIFF_MAX)
|
|
335
|
+
return diff;
|
|
336
|
+
const shown = diff.slice(0, WORK_DIFF_MAX);
|
|
337
|
+
const cut = shown.lastIndexOf("\ndiff --git ");
|
|
338
|
+
const kept = cut > 0 ? shown.slice(0, cut) : shown;
|
|
339
|
+
const rest = [...changed, ...added].filter((path) => !NOT_REVIEWED.test(path) && !kept.includes(` b/${path}\n`));
|
|
340
|
+
return `${kept}\n\n(the diff goes on past what is shown: read these files whole - ${rest.join(", ")})`;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The work in `dir` as it is now - the code folder, or the tests folder - as a git tree, without a commit and without
|
|
344
|
+
* touching the module's index: what a review saw, so the next review is shown only what changed since. Undefined
|
|
345
|
+
* when there is no history.
|
|
346
|
+
*/
|
|
347
|
+
export function workSnapshot(dir) {
|
|
348
|
+
const moduleDir = moduleDirOf(dir);
|
|
349
|
+
if (!existsSync(join(moduleDir, HISTORY_DIR)))
|
|
350
|
+
return undefined;
|
|
351
|
+
const folder = basename(dir);
|
|
352
|
+
// An index of its own, so the module's - what the next commit is made from - is never touched.
|
|
353
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(moduleDir, HISTORY_DIR, "turri-snapshot-index") };
|
|
354
|
+
const run = (args) => spawnSync("git", ["--git-dir", join(moduleDir, HISTORY_DIR), "--work-tree", moduleDir, ...args], { cwd: moduleDir, encoding: "utf8", env });
|
|
355
|
+
if (run(["read-tree", "--empty"]).status !== 0)
|
|
356
|
+
return undefined;
|
|
357
|
+
if (run(["add", "-A", "--", folder]).status !== 0)
|
|
358
|
+
return undefined;
|
|
359
|
+
const tree = run(["write-tree"]);
|
|
360
|
+
return tree.status === 0 ? tree.stdout.trim() : undefined;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* What changed in `dir` since a snapshot of it (`workSnapshot`): a unified diff of every file changed, added or
|
|
364
|
+
* removed since, as `workDiff` shows the whole work. Empty when nothing changed, or there is no history.
|
|
365
|
+
*/
|
|
366
|
+
export function diffSinceSnapshot(dir, snapshot) {
|
|
367
|
+
const now = workSnapshot(dir);
|
|
368
|
+
if (now === undefined)
|
|
369
|
+
return "";
|
|
370
|
+
if (now === snapshot)
|
|
371
|
+
return "";
|
|
372
|
+
const moduleDir = moduleDirOf(dir);
|
|
373
|
+
const folder = basename(dir);
|
|
374
|
+
const files = git(moduleDir, ["diff", "--no-color", "--name-only", `--relative=${folder}`, snapshot, now, "--", folder]).stdout.split("\n").filter((path) => path !== "" && !NOT_REVIEWED.test(path));
|
|
375
|
+
const parts = files.map((file) => git(moduleDir, ["diff", "--no-color", `--relative=${folder}`, snapshot, now, "--", `${folder}/${file}`]).stdout.trimEnd());
|
|
376
|
+
const diff = parts.filter(Boolean).join("\n");
|
|
377
|
+
if (diff.length <= WORK_DIFF_MAX)
|
|
378
|
+
return diff;
|
|
379
|
+
const shown = diff.slice(0, WORK_DIFF_MAX);
|
|
380
|
+
const cut = shown.lastIndexOf("\ndiff --git ");
|
|
381
|
+
const kept = cut > 0 ? shown.slice(0, cut) : shown;
|
|
382
|
+
const rest = files.filter((path) => !kept.includes(` b/${path}\n`));
|
|
383
|
+
return `${kept}\n\n(the diff goes on past what is shown: read these files whole - ${rest.join(", ")})`;
|
|
384
|
+
}
|