@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.
Files changed (315) hide show
  1. package/.pi/themes/turri-dark.json +80 -0
  2. package/.pi/themes/turri-light.json +82 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -2
  5. package/assets/jellyfish/dark-00.png +0 -0
  6. package/assets/jellyfish/dark-01.png +0 -0
  7. package/assets/jellyfish/dark-02.png +0 -0
  8. package/assets/jellyfish/dark-03.png +0 -0
  9. package/assets/jellyfish/dark-04.png +0 -0
  10. package/assets/jellyfish/dark-05.png +0 -0
  11. package/assets/jellyfish/dark-06.png +0 -0
  12. package/assets/jellyfish/dark-07.png +0 -0
  13. package/assets/jellyfish/dark-08.png +0 -0
  14. package/assets/jellyfish/dark-09.png +0 -0
  15. package/assets/jellyfish/dark-10.png +0 -0
  16. package/assets/jellyfish/dark-11.png +0 -0
  17. package/assets/jellyfish/dark-12.png +0 -0
  18. package/assets/jellyfish/dark-13.png +0 -0
  19. package/assets/jellyfish/dark-14.png +0 -0
  20. package/assets/jellyfish/fallback.txt +252 -0
  21. package/assets/jellyfish/light-00.png +0 -0
  22. package/assets/jellyfish/light-01.png +0 -0
  23. package/assets/jellyfish/light-02.png +0 -0
  24. package/assets/jellyfish/light-03.png +0 -0
  25. package/assets/jellyfish/light-04.png +0 -0
  26. package/assets/jellyfish/light-05.png +0 -0
  27. package/assets/jellyfish/light-06.png +0 -0
  28. package/assets/jellyfish/light-07.png +0 -0
  29. package/assets/jellyfish/light-08.png +0 -0
  30. package/assets/jellyfish/light-09.png +0 -0
  31. package/assets/jellyfish/light-10.png +0 -0
  32. package/assets/jellyfish/light-11.png +0 -0
  33. package/assets/jellyfish/light-12.png +0 -0
  34. package/assets/jellyfish/light-13.png +0 -0
  35. package/assets/jellyfish/light-14.png +0 -0
  36. package/bin/turri.mjs +29 -0
  37. package/dist/ask.js +149 -0
  38. package/dist/brand.js +50 -0
  39. package/dist/btw.js +174 -0
  40. package/dist/chat-models.js +37 -0
  41. package/dist/chat-skills.js +18 -0
  42. package/dist/cli.js +178 -0
  43. package/dist/config-command.js +197 -0
  44. package/dist/config-edit.js +367 -0
  45. package/dist/config.js +489 -0
  46. package/dist/conform-cli.js +29 -0
  47. package/dist/cultivate-cli.js +110 -0
  48. package/dist/drift-cli.js +25 -0
  49. package/dist/environment.js +70 -0
  50. package/dist/extension.js +2372 -0
  51. package/dist/genome.js +301 -0
  52. package/dist/graph.js +246 -0
  53. package/dist/identity.js +35 -0
  54. package/dist/init-config.js +511 -0
  55. package/dist/judge.js +62 -0
  56. package/dist/lineage-cli.js +13 -0
  57. package/dist/loop-cli.js +132 -0
  58. package/dist/loop-tools.js +225 -0
  59. package/dist/mature-tools.js +193 -0
  60. package/dist/model-check.js +117 -0
  61. package/dist/module-report.js +9 -0
  62. package/dist/pi-launch.js +31 -0
  63. package/dist/pi.js +813 -0
  64. package/dist/plain/check.js +76 -0
  65. package/dist/plain/concepts.js +213 -0
  66. package/dist/plain/errors.js +43 -0
  67. package/dist/plain/files.js +57 -0
  68. package/dist/plain/highlight.js +195 -0
  69. package/dist/plain/index.js +15 -0
  70. package/dist/plain/liquid.js +129 -0
  71. package/dist/plain/markdown.js +208 -0
  72. package/dist/plain/parser.js +752 -0
  73. package/dist/plain/plain.tmLanguage.json +124 -0
  74. package/dist/plain/spec.js +291 -0
  75. package/dist/plain/stdlib.js +44 -0
  76. package/dist/question-dialog.js +125 -0
  77. package/dist/questioning.js +109 -0
  78. package/dist/quiet.js +125 -0
  79. package/dist/regenerate/activity.js +187 -0
  80. package/dist/regenerate/agent-env.js +190 -0
  81. package/dist/regenerate/agent-shell.js +114 -0
  82. package/dist/regenerate/arbiter.js +133 -0
  83. package/dist/regenerate/audit.js +203 -0
  84. package/dist/regenerate/cases.js +83 -0
  85. package/dist/regenerate/change-view.js +102 -0
  86. package/dist/regenerate/choose-conformance.js +117 -0
  87. package/dist/regenerate/chunk-plan.js +625 -0
  88. package/dist/regenerate/codebase.js +75 -0
  89. package/dist/regenerate/command-check.js +312 -0
  90. package/dist/regenerate/compaction.js +20 -0
  91. package/dist/regenerate/confine.js +310 -0
  92. package/dist/regenerate/conform.js +133 -0
  93. package/dist/regenerate/conformance-agents.js +955 -0
  94. package/dist/regenerate/conformance.js +936 -0
  95. package/dist/regenerate/conversation-writes.js +100 -0
  96. package/dist/regenerate/credentials.js +88 -0
  97. package/dist/regenerate/debug-log.js +228 -0
  98. package/dist/regenerate/debugger.js +90 -0
  99. package/dist/regenerate/delivery.js +19 -0
  100. package/dist/regenerate/diagnose-job.js +40 -0
  101. package/dist/regenerate/diagnose.js +81 -0
  102. package/dist/regenerate/diff-view.js +240 -0
  103. package/dist/regenerate/drift.js +194 -0
  104. package/dist/regenerate/engine.js +118 -0
  105. package/dist/regenerate/environment-error.js +220 -0
  106. package/dist/regenerate/job-log.js +102 -0
  107. package/dist/regenerate/job-panel.js +336 -0
  108. package/dist/regenerate/jobs-tree.js +300 -0
  109. package/dist/regenerate/jobs.js +427 -0
  110. package/dist/regenerate/lanes.js +32 -0
  111. package/dist/regenerate/lineage.js +384 -0
  112. package/dist/regenerate/loop.js +1435 -0
  113. package/dist/regenerate/masking.js +95 -0
  114. package/dist/regenerate/notes.js +397 -0
  115. package/dist/regenerate/os-sandbox.js +243 -0
  116. package/dist/regenerate/paths.js +165 -0
  117. package/dist/regenerate/plain-palette.js +83 -0
  118. package/dist/regenerate/prompt.js +704 -0
  119. package/dist/regenerate/records.js +294 -0
  120. package/dist/regenerate/regenerate.js +598 -0
  121. package/dist/regenerate/regression.js +181 -0
  122. package/dist/regenerate/release.js +262 -0
  123. package/dist/regenerate/requires.js +241 -0
  124. package/dist/regenerate/resources.js +164 -0
  125. package/dist/regenerate/self-check.js +111 -0
  126. package/dist/regenerate/services.js +155 -0
  127. package/dist/regenerate/shell.js +320 -0
  128. package/dist/regenerate/side-panel.js +278 -0
  129. package/dist/regenerate/spec-map-view.js +574 -0
  130. package/dist/regenerate/spec-map.js +561 -0
  131. package/dist/regenerate/spec-steps.js +527 -0
  132. package/dist/regenerate/state.js +10 -0
  133. package/dist/regenerate/steer.js +91 -0
  134. package/dist/regenerate/steering.js +119 -0
  135. package/dist/regenerate/stop-report.js +161 -0
  136. package/dist/regenerate/submit-work.js +71 -0
  137. package/dist/regenerate/test-kinds.js +124 -0
  138. package/dist/regenerate/test-scripts.js +695 -0
  139. package/dist/regenerate/testing.js +406 -0
  140. package/dist/regenerate/transcript-view.js +1 -0
  141. package/dist/regenerate/unit-gate.js +20 -0
  142. package/dist/regenerate/unit-review.js +347 -0
  143. package/dist/regenerate/web.js +256 -0
  144. package/dist/regenerate/workspace.js +99 -0
  145. package/dist/regenerate-cli.js +68 -0
  146. package/dist/release-agent.js +114 -0
  147. package/dist/release-cli.js +131 -0
  148. package/dist/release-tools.js +148 -0
  149. package/dist/review/live.js +165 -0
  150. package/dist/rules.js +47 -0
  151. package/dist/self-invocation.js +110 -0
  152. package/dist/setup.js +245 -0
  153. package/dist/skill-progress.js +78 -0
  154. package/dist/skill.js +149 -0
  155. package/dist/skills.js +74 -0
  156. package/dist/spec-diff-cli.js +37 -0
  157. package/dist/spec-tools.js +155 -0
  158. package/dist/stack.js +158 -0
  159. package/dist/stacks.js +97 -0
  160. package/dist/stdlib.js +100 -0
  161. package/dist/terminal-images.js +46 -0
  162. package/dist/tools.js +242 -0
  163. package/dist/validate-cli.js +52 -0
  164. package/docs/README.md +58 -0
  165. package/docs/authoring-plain.md +245 -0
  166. package/docs/config.md +353 -0
  167. package/docs/deploy.md +109 -0
  168. package/docs/development.md +88 -0
  169. package/docs/headless.md +148 -0
  170. package/docs/images/how-turri-works.svg +50 -0
  171. package/docs/images/turri-banner.png +0 -0
  172. package/docs/installation.md +119 -0
  173. package/docs/parser.md +108 -0
  174. package/docs/regenerating.md +1116 -0
  175. package/docs/sandbox.md +233 -0
  176. package/docs/skills-and-gates.md +254 -0
  177. package/docs/theming.md +76 -0
  178. package/docs/using.md +454 -0
  179. package/genome/rules/bullet-continuation.md +69 -0
  180. package/genome/rules/concise-specs-examples.md +431 -0
  181. package/genome/rules/concise-specs-integration-examples.md +270 -0
  182. package/genome/rules/concise-specs.md +80 -0
  183. package/genome/rules/definitions.md +107 -0
  184. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
  185. package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
  186. package/genome/rules/exported-concepts.md +39 -0
  187. package/genome/rules/func-specs.md +129 -0
  188. package/genome/rules/impl-reqs.md +68 -0
  189. package/genome/rules/import-modules.md +48 -0
  190. package/genome/rules/integration-embedded-testing.md +276 -0
  191. package/genome/rules/integration-embedded.md +171 -0
  192. package/genome/rules/integration-standalone.md +135 -0
  193. package/genome/rules/integrations.md +137 -0
  194. package/genome/rules/linked-resources.md +69 -0
  195. package/genome/rules/module-structure.md +105 -0
  196. package/genome/rules/project-model.md +88 -0
  197. package/genome/rules/required-concepts.md +44 -0
  198. package/genome/rules/requires-modules.md +80 -0
  199. package/genome/rules/stdlib.md +73 -0
  200. package/genome/rules/test-reqs.md +57 -0
  201. package/genome/skills/add-acceptance-test/SKILL.md +100 -0
  202. package/genome/skills/add-concept/SKILL.md +71 -0
  203. package/genome/skills/add-functional-spec/SKILL.md +84 -0
  204. package/genome/skills/add-functional-specs/SKILL.md +118 -0
  205. package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
  206. package/genome/skills/add-resource/SKILL.md +105 -0
  207. package/genome/skills/add-template/SKILL.md +64 -0
  208. package/genome/skills/add-test-requirement/SKILL.md +73 -0
  209. package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
  210. package/genome/skills/analyze-func-specs/SKILL.md +123 -0
  211. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
  212. package/genome/skills/break-down-func-spec/SKILL.md +205 -0
  213. package/genome/skills/brood/SKILL.md +229 -0
  214. package/genome/skills/brood/references/intent.md +35 -0
  215. package/genome/skills/brood/references/product.md +101 -0
  216. package/genome/skills/brood/references/stack.md +61 -0
  217. package/genome/skills/brood/references/testing.md +56 -0
  218. package/genome/skills/bud/SKILL.md +148 -0
  219. package/genome/skills/bud/references/checklist.md +44 -0
  220. package/genome/skills/bud/references/intent.md +30 -0
  221. package/genome/skills/check-plain-env/SKILL.md +285 -0
  222. package/genome/skills/consolidate-concepts/SKILL.md +192 -0
  223. package/genome/skills/create-import-module/SKILL.md +104 -0
  224. package/genome/skills/create-requires-module/SKILL.md +109 -0
  225. package/genome/skills/debug-specs/SKILL.md +188 -0
  226. package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
  227. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
  228. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
  229. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
  230. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
  231. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
  232. package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
  233. package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
  234. package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
  235. package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
  236. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
  237. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
  238. package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
  239. package/genome/skills/init-plain-project/SKILL.md +117 -0
  240. package/genome/skills/init-plain-project/references/checklist.md +30 -0
  241. package/genome/skills/mature/SKILL.md +126 -0
  242. package/genome/skills/refactor-module/SKILL.md +196 -0
  243. package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
  244. package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
  245. package/genome/skills/spawn/SKILL.md +163 -0
  246. package/package.json +68 -4
  247. package/stdlib/backend/common.plain +25 -0
  248. package/stdlib/backend/conformance/common.plain +12 -0
  249. package/stdlib/backend/conformance/go-gin.plain +9 -0
  250. package/stdlib/backend/conformance/go-nethttp.plain +9 -0
  251. package/stdlib/backend/conformance/java-springboot.plain +9 -0
  252. package/stdlib/backend/conformance/python-django.plain +9 -0
  253. package/stdlib/backend/conformance/python-fastapi.plain +9 -0
  254. package/stdlib/backend/conformance/python-flask.plain +9 -0
  255. package/stdlib/backend/conformance/rust-axum.plain +9 -0
  256. package/stdlib/backend/conformance/typescript-express.plain +9 -0
  257. package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
  258. package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
  259. package/stdlib/backend/go-gin.plain +18 -0
  260. package/stdlib/backend/go-nethttp.plain +18 -0
  261. package/stdlib/backend/java-springboot.plain +19 -0
  262. package/stdlib/backend/python-django.plain +19 -0
  263. package/stdlib/backend/python-fastapi.plain +23 -0
  264. package/stdlib/backend/python-flask.plain +23 -0
  265. package/stdlib/backend/rust-axum.plain +17 -0
  266. package/stdlib/backend/typescript-express.plain +19 -0
  267. package/stdlib/backend/typescript-fastify.plain +19 -0
  268. package/stdlib/backend/typescript-nestjs.plain +19 -0
  269. package/stdlib/cli/common.plain +17 -0
  270. package/stdlib/cli/conformance/common.plain +11 -0
  271. package/stdlib/cli/conformance/go-cobra.plain +9 -0
  272. package/stdlib/cli/conformance/go-flag.plain +9 -0
  273. package/stdlib/cli/conformance/java-picocli.plain +9 -0
  274. package/stdlib/cli/conformance/python-argparse.plain +9 -0
  275. package/stdlib/cli/conformance/python-click.plain +9 -0
  276. package/stdlib/cli/conformance/python-typer.plain +9 -0
  277. package/stdlib/cli/conformance/rust-clap.plain +9 -0
  278. package/stdlib/cli/conformance/typescript-commander.plain +9 -0
  279. package/stdlib/cli/go-cobra.plain +14 -0
  280. package/stdlib/cli/go-flag.plain +14 -0
  281. package/stdlib/cli/java-picocli.plain +15 -0
  282. package/stdlib/cli/python-argparse.plain +18 -0
  283. package/stdlib/cli/python-click.plain +16 -0
  284. package/stdlib/cli/python-typer.plain +16 -0
  285. package/stdlib/cli/rust-clap.plain +14 -0
  286. package/stdlib/cli/typescript-commander.plain +17 -0
  287. package/stdlib/frontend/angular.plain +17 -0
  288. package/stdlib/frontend/common.plain +17 -0
  289. package/stdlib/frontend/conformance/angular.plain +5 -0
  290. package/stdlib/frontend/conformance/common.plain +11 -0
  291. package/stdlib/frontend/conformance/react-vite.plain +5 -0
  292. package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
  293. package/stdlib/frontend/conformance/vue-vite.plain +5 -0
  294. package/stdlib/frontend/react-vite.plain +16 -0
  295. package/stdlib/frontend/sveltekit-spa.plain +16 -0
  296. package/stdlib/frontend/vue-vite.plain +16 -0
  297. package/stdlib/lang/go.plain +13 -0
  298. package/stdlib/lang/java.plain +12 -0
  299. package/stdlib/lang/python.plain +18 -0
  300. package/stdlib/lang/rust.plain +12 -0
  301. package/stdlib/lang/typescript.plain +13 -0
  302. package/stdlib/library/common.plain +15 -0
  303. package/stdlib/library/conformance/common.plain +9 -0
  304. package/stdlib/library/conformance/go.plain +9 -0
  305. package/stdlib/library/conformance/java-gradle.plain +9 -0
  306. package/stdlib/library/conformance/java-maven.plain +9 -0
  307. package/stdlib/library/conformance/python.plain +9 -0
  308. package/stdlib/library/conformance/rust.plain +9 -0
  309. package/stdlib/library/conformance/typescript.plain +9 -0
  310. package/stdlib/library/go.plain +12 -0
  311. package/stdlib/library/java-gradle.plain +13 -0
  312. package/stdlib/library/java-maven.plain +13 -0
  313. package/stdlib/library/python.plain +13 -0
  314. package/stdlib/library/rust.plain +12 -0
  315. package/stdlib/library/typescript.plain +15 -0
package/dist/skill.js ADDED
@@ -0,0 +1,149 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import matter from "gray-matter";
4
+ const REVIEW_DEFAULTS = {
5
+ judges: 2,
6
+ quorum: "all",
7
+ onFail: "retry",
8
+ maxRetries: 2,
9
+ };
10
+ /**
11
+ * Parse the skill's steps.
12
+ *
13
+ * A `## Steps` section declares them explicitly: each `###` heading under it is a step.
14
+ * Without that section the whole skill is one step - which is what most skills are, and
15
+ * what the Agent Skills standard describes. Steps are never inferred from arbitrary `###`
16
+ * headings elsewhere in the body: prose skills are full of those, and reading them as
17
+ * executable steps would run a skill's explanatory subsections as work.
18
+ */
19
+ function parseSteps(name, body) {
20
+ const stepsHeading = /^##\s+Steps\s*$/im.exec(body);
21
+ if (stepsHeading === null) {
22
+ const instructions = body.trim();
23
+ return instructions === "" ? [] : [{ name, instructions }];
24
+ }
25
+ const afterSteps = body.slice(stepsHeading.index + stepsHeading[0].length);
26
+ const nextSection = /^##\s+(?!#)/m.exec(afterSteps);
27
+ const lines = (nextSection === null ? afterSteps : afterSteps.slice(0, nextSection.index)).split("\n");
28
+ const steps = [];
29
+ let current;
30
+ for (const line of lines) {
31
+ const heading = /^###\s+(.+?)\s*$/.exec(line);
32
+ if (heading) {
33
+ if (current)
34
+ steps.push({ name: current.name, instructions: current.instructions.join("\n").trim() });
35
+ current = { name: heading[1].trim(), instructions: [] };
36
+ continue;
37
+ }
38
+ if (current)
39
+ current.instructions.push(line);
40
+ }
41
+ if (current)
42
+ steps.push({ name: current.name, instructions: current.instructions.join("\n").trim() });
43
+ return steps;
44
+ }
45
+ export function loadSkill(skillDir) {
46
+ const dir = resolve(skillDir);
47
+ const raw = readFileSync(join(dir, "SKILL.md"), "utf8");
48
+ const { data, content } = matter(raw);
49
+ const name = String(data.name ?? "");
50
+ const description = String(data.description ?? "");
51
+ if (!name || !description) {
52
+ throw new Error(`${dir}/SKILL.md: 'name' and 'description' are required by the Agent Skills spec`);
53
+ }
54
+ const rawGates = data.metadata?.gates ?? [];
55
+ const gatesByStep = new Map();
56
+ for (const g of rawGates) {
57
+ if (!g.after)
58
+ throw new Error(`${name}: every gate needs an 'after: <step>'`);
59
+ if (!g.review?.rubric)
60
+ throw new Error(`${name}: gate after '${g.after}' has no 'review.rubric'`);
61
+ const live = g.continuous;
62
+ if (live !== undefined && !live.rubric) {
63
+ throw new Error(`${name}: continuous review after '${g.after}' has no 'rubric'`);
64
+ }
65
+ gatesByStep.set(g.after, {
66
+ after: g.after,
67
+ continuous: live === undefined
68
+ ? undefined
69
+ : {
70
+ rubric: live.rubric,
71
+ // One judge per pass. The pass runs on every write, so a second opinion
72
+ // costs a multiple of the phase rather than a single review.
73
+ judges: live.judges ?? 1,
74
+ model: live.model,
75
+ judgeTools: live.judge_tools ?? live.judgeTools ?? [],
76
+ },
77
+ review: {
78
+ rubric: g.review.rubric,
79
+ judges: g.review.judges ?? REVIEW_DEFAULTS.judges,
80
+ model: g.review.model,
81
+ quorum: g.review.quorum ?? REVIEW_DEFAULTS.quorum,
82
+ judgeTools: g.review.judge_tools ?? g.review.judgeTools ?? [],
83
+ onFail: g.review.on_fail ?? g.review.onFail ?? REVIEW_DEFAULTS.onFail,
84
+ maxRetries: g.review.max_retries ?? g.review.maxRetries ?? REVIEW_DEFAULTS.maxRetries,
85
+ },
86
+ });
87
+ }
88
+ // What each step does, for the person: `metadata.phases`, a short line a step by its name.
89
+ const rawPhases = data.metadata?.phases ?? {};
90
+ if (typeof rawPhases !== "object" || rawPhases === null || Array.isArray(rawPhases)) {
91
+ throw new Error(`${name}: 'metadata.phases' is a line a step, by the step's name`);
92
+ }
93
+ const abouts = new Map(Object.entries(rawPhases).map(([step, about]) => [step, String(about ?? "").trim()]));
94
+ const steps = parseSteps(name, content).map((s) => {
95
+ const about = abouts.get(s.name);
96
+ return { ...s, ...(about !== undefined && about !== "" ? { about } : {}), gate: gatesByStep.get(s.name) };
97
+ });
98
+ if (steps.length === 0)
99
+ throw new Error(`${name}: the skill body is empty`);
100
+ for (const stepName of abouts.keys()) {
101
+ if (!steps.some((s) => s.name === stepName)) {
102
+ throw new Error(`${name}: 'metadata.phases' describes unknown step '${stepName}'`);
103
+ }
104
+ }
105
+ const questioning = data.metadata?.questioning;
106
+ const capped = Array.isArray(questioning) ? questioning.map(String) : [];
107
+ for (const stepName of capped) {
108
+ if (!steps.some((s) => s.name === stepName)) {
109
+ throw new Error(`${name}: 'metadata.questioning' names unknown step '${stepName}'`);
110
+ }
111
+ }
112
+ const perAnswer = data.metadata?.["spec-diff-per-answer"];
113
+ const diffPerAnswer = Array.isArray(perAnswer) ? perAnswer.map(String) : [];
114
+ for (const stepName of diffPerAnswer) {
115
+ if (!steps.some((s) => s.name === stepName)) {
116
+ throw new Error(`${name}: 'metadata.spec-diff-per-answer' names unknown step '${stepName}'`);
117
+ }
118
+ }
119
+ for (const stepName of gatesByStep.keys()) {
120
+ if (!steps.some((s) => s.name === stepName)) {
121
+ throw new Error(`${name}: gate references unknown step '${stepName}'`);
122
+ }
123
+ }
124
+ return {
125
+ name,
126
+ description,
127
+ dir,
128
+ // What a skill may use is what its frontmatter names, and nothing else: no list, no tools.
129
+ allowedTools: Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : [],
130
+ steps,
131
+ paced: data.metadata?.paced !== false,
132
+ questioning: capped,
133
+ diffPerAnswer,
134
+ };
135
+ }
136
+ export function readRubric(skill, review) {
137
+ return readFileSync(join(skill.dir, review.rubric), "utf8").trim();
138
+ }
139
+ /**
140
+ * The status line while a skill runs: which phase, and how far through the skill it is.
141
+ * The phase is counted from 1 and includes the one in progress, so "3 of 6" means two are
142
+ * done. A retry after a failed gate says so - the same phase twice is not progress.
143
+ */
144
+ export function skillStatus(skill, step, attempt) {
145
+ const index = skill.steps.findIndex((s) => s.name === step);
146
+ const position = index === -1 ? "" : ` (phase ${index + 1} of ${skill.steps.length})`;
147
+ const retry = attempt > 1 ? `, attempt ${attempt}` : "";
148
+ return `${skill.name}: ${step}${position}${retry}`;
149
+ }
package/dist/skills.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Where skills are found: turri's own, in `genome/skills/` of this package, and the project's - `.turri/skills/` and
3
+ * `.agents/skills/` in the project's folder, a folder a skill with its SKILL.md, in the format of turri's own. A
4
+ * project's skill is run the same way: its steps, its gates and judges, the tools its frontmatter names.
5
+ *
6
+ * A name is one skill: turri's own come first, then `.turri/skills/`, then `.agents/skills/`, and a skill whose name is
7
+ * taken already is left out and said to be - a project's `brood` never quietly stands in for turri's.
8
+ */
9
+ import { readdirSync } from "node:fs";
10
+ import { join, relative } from "node:path";
11
+ import { loadSkill } from "./skill.js";
12
+ /** A project's skill folders, in the order they are read, relative to the project. */
13
+ export const PROJECT_SKILL_DIRS = [join(".turri", "skills"), join(".agents", "skills")];
14
+ function folders(root, includeFixtures) {
15
+ try {
16
+ return readdirSync(root, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory() && (!entry.name.startsWith("_") || includeFixtures))
18
+ .map((entry) => entry.name)
19
+ .sort();
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ /** Every skill turri can run in `projectDir`: its own from `packageRoot`, then the project's. */
26
+ export function discoverSkills(packageRoot, projectDir) {
27
+ const catalogue = { skills: [], pending: [], shadowed: [] };
28
+ const builtIn = join(packageRoot, "genome", "skills");
29
+ for (const name of folders(builtIn, process.env.TURRI_INCLUDE_FIXTURES === "1")) {
30
+ try {
31
+ catalogue.skills.push({ ...loadSkill(join(builtIn, name)), origin: "turri" });
32
+ }
33
+ catch {
34
+ catalogue.pending.push(name);
35
+ }
36
+ }
37
+ if (projectDir === undefined)
38
+ return catalogue;
39
+ for (const dir of PROJECT_SKILL_DIRS) {
40
+ const root = join(projectDir, dir);
41
+ for (const name of folders(root, true)) {
42
+ const where = relative(projectDir, join(root, name));
43
+ let skill;
44
+ try {
45
+ skill = loadSkill(join(root, name));
46
+ }
47
+ catch {
48
+ catalogue.pending.push(where);
49
+ continue;
50
+ }
51
+ const taken = catalogue.skills.find((other) => other.name === skill.name);
52
+ if (taken !== undefined) {
53
+ catalogue.shadowed.push(`${where} (${skill.name} is ${taken.origin === "turri" ? "turri's own" : `${taken.origin.project}`})`);
54
+ continue;
55
+ }
56
+ catalogue.skills.push({ ...skill, origin: { project: where } });
57
+ }
58
+ }
59
+ return catalogue;
60
+ }
61
+ /** A skill's line in cultivate's list: a project's says so, and where it is. */
62
+ export function catalogueLine(skill) {
63
+ const whose = skill.origin === "turri" ? "" : ` (this project's, ${skill.origin.project})`;
64
+ return `- ${skill.name}${whose}: ${skill.description}`;
65
+ }
66
+ /** What is said under the list about the skills it leaves out. */
67
+ export function leftOutNote(catalogue) {
68
+ return [
69
+ ...(catalogue.pending.length > 0 ? [`Failed to load and therefore not runnable: ${catalogue.pending.join(", ")}.`] : []),
70
+ ...(catalogue.shadowed.length > 0 ? [`Left out, their names taken: ${catalogue.shadowed.join(", ")}.`] : []),
71
+ ]
72
+ .map((line) => `\n\n${line}`)
73
+ .join("");
74
+ }
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { checkPlainFile } from "./plain/check.js";
4
+ import { applySpecDiff, revertSpecDiff } from "./regenerate/regenerate.js";
5
+ import { parseArgs, runLoop } from "./loop-cli.js";
6
+ const USAGE = `usage: turri spec-diff <file.plain> <diff-file> [options]
7
+
8
+ Applies a change to the specs, given as a git diff of the .plain files, and builds it: the headless
9
+ counterpart of regenerate, given a diff. The diff is applied first, unless it is already in; then, as
10
+ turri regenerate does, what is left to build is planned into jobs and built.
11
+
12
+ --model <provider/id> model for the agents (default: config.turri.yaml's coding-model)
13
+ --start-over start over, without asking, a codebase regenerated without a module it now requires`;
14
+ const { positional, options } = parseArgs(process.argv.slice(2), ["--start-over"], USAGE);
15
+ const [plainFile, diffFile] = positional;
16
+ if (plainFile === undefined || diffFile === undefined) {
17
+ console.error(USAGE);
18
+ process.exit(1);
19
+ }
20
+ const diff = readFileSync(diffFile, "utf8");
21
+ const cwd = process.cwd();
22
+ applySpecDiff(diff, cwd);
23
+ const check = checkPlainFile(resolve(plainFile));
24
+ if (!check.ok) {
25
+ revertSpecDiff(diff, cwd);
26
+ console.error(`Nothing was changed: with this diff ${plainFile} no longer parses. ${check.error}`);
27
+ process.exit(1);
28
+ }
29
+ runLoop({
30
+ plainFile,
31
+ ...(options.model !== undefined ? { model: options.model } : {}),
32
+ startOver: options["start-over"] !== undefined,
33
+ ask: (loop) => loop.bringUp(plainFile, options.model !== undefined ? { model: options.model } : {}),
34
+ }).catch((error) => {
35
+ console.error(error instanceof Error ? error.message : error);
36
+ process.exit(1);
37
+ });
@@ -0,0 +1,155 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import { IMPORT_DIRECTIVE, PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, readPlainSourceMetadata } from "./plain/parser.js";
6
+ import { resolveTemplateDirs } from "./regenerate/paths.js";
7
+ import { applySpecDiff, plainFilesOf, revertSpecDiff } from "./regenerate/regenerate.js";
8
+ import { MissingFunctionalitiesError } from "./plain/errors.js";
9
+ import { describeModules } from "./regenerate/loop.js";
10
+ import { reviewNote } from "./tools.js";
11
+ /**
12
+ * Writing a spec change as a diff, and building it, as one step.
13
+ *
14
+ * In an interview whose unit of work is a spec change, every change is meant to be built the
15
+ * moment it is written. Left to two tool calls - change the file, then start the render - the
16
+ * second is the one that gets forgotten, and nothing notices. So the diff is applied and its
17
+ * build queued in the same call: a change cannot reach the specs without its render being
18
+ * started. A new change needs an answer since the last one; a correction does not.
19
+ */
20
+ export function createSpecDiffTools(options) {
21
+ const { loop, cwd, liveReview } = options;
22
+ const skillPaced = options.paced !== false;
23
+ const isPaced = () => skillPaced && options.free?.() !== true;
24
+ /** An answer has come since the last change: it is owed one. */
25
+ let owed = false;
26
+ let answeredSinceLastDiff = true;
27
+ const writeDiff = defineTool({
28
+ name: "write_spec_diff",
29
+ label: "Write spec diff",
30
+ description: "Apply a change to the ***plain specs, given as a git diff of the .plain files, and start building it in " +
31
+ "the background. The diff is applied with git; if the specs no longer parse, it is undone and nothing is " +
32
+ "built. Any section may change - definitions, implementation reqs, functional specs, added, changed or removed - and " +
33
+ "every change is built, whatever it touches.",
34
+ promptSnippet: "apply a git diff of the .plain specs and start building it",
35
+ promptGuidelines: [
36
+ "Every change to a .plain file in the product phase goes through write_spec_diff, as a git diff with paths relative to the project root. It writes the change and starts its build in one step.",
37
+ skillPaced
38
+ ? "One change per answer, and a new one only after the user has answered a question about it. Set fixes_earlier to correct something already written."
39
+ : "Write the changes one after another, on your own: no answer is needed between them, and none is asked for. Set fixes_earlier to correct something already written.",
40
+ "To take something out of what is already written and queued - the user drops a page or a feature - write the diff that removes it from every spec that names it, with fixes_earlier. What is left to build is worked out from the spec each time, and a build the change undoes is stopped by itself; never stop builds to drop a change - it stays in the files.",
41
+ ],
42
+ parameters: Type.Object({
43
+ plain_file: Type.String({ description: "The .plain file of the module the change is built for" }),
44
+ diff: Type.String({ description: "The change, as a git diff (diff --git a/<file> b/<file> ...)" }),
45
+ fixes_earlier: Type.Optional(Type.Boolean({ description: "This corrects something already written - a contradiction, a review finding" })),
46
+ guidance: Type.Optional(Type.String({
47
+ description: "How to plan the queue for this change, in a sentence or two - which functional specs go together, what first - as the " +
48
+ "user said it or as the interview settled it. A strong preference for the planner. Omit when there is none.",
49
+ })),
50
+ }),
51
+ async execute(_id, params) {
52
+ const paced = isPaced();
53
+ if (paced && params.fixes_earlier !== true && !answeredSinceLastDiff) {
54
+ throw new Error("No question has been answered since the last spec change. Each change comes from its own answer: ask " +
55
+ "the user with ask_user first. To correct something already written, set fixes_earlier.");
56
+ }
57
+ const touched = plainFilesOf(params.diff);
58
+ if (touched.length === 0)
59
+ throw new Error("The diff changes no .plain file. Nothing was written.");
60
+ const result = applySpecDiff(params.diff, cwd);
61
+ // A change that breaks a spec is not written at all, so nothing is ever built from a
62
+ // file that cannot be parsed. A module with no functional specs yet is not broken. An import
63
+ // module - definitions only, say - is not a module of its own: it is parsed as the modules
64
+ // that import it read it, the one the change is built for among them.
65
+ const importers = (file) => {
66
+ const name = basename(file, ".plain");
67
+ const dir = dirname(resolve(cwd, file));
68
+ return readdirSync(dir)
69
+ .filter((entry) => entry.endsWith(".plain"))
70
+ .map((entry) => join(dir, entry))
71
+ .filter((path) => {
72
+ try {
73
+ return (readPlainSourceMetadata(readFileSync(path, "utf8")).metadata[IMPORT_DIRECTIVE] ?? []).includes(name);
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ });
79
+ };
80
+ const imported = importedNames(resolveTemplateDirs(resolve(cwd, params.plain_file)));
81
+ const toParse = [
82
+ ...new Set([params.plain_file, ...touched].flatMap((file) => imported.has(basename(file, ".plain")) ? importers(file).filter((path) => !imported.has(basename(path, ".plain"))) : [resolve(cwd, file)])),
83
+ ];
84
+ for (const path of toParse) {
85
+ const file = relative(cwd, path);
86
+ try {
87
+ plainFileParser(basename(path), resolveTemplateDirs(path), { allowNoFunctionalities: true });
88
+ }
89
+ catch (error) {
90
+ if (error instanceof MissingFunctionalitiesError)
91
+ continue;
92
+ if (result === "applied")
93
+ revertSpecDiff(params.diff, cwd);
94
+ const reason = error instanceof Error ? error.message : String(error);
95
+ throw new Error(`Nothing was written: with this change ${file} no longer parses. ${reason}`);
96
+ }
97
+ }
98
+ if (params.fixes_earlier !== true)
99
+ answeredSinceLastDiff = false;
100
+ owed = false;
101
+ liveReview?.notice(`${params.plain_file}, after the change to ${touched.join(", ")}`);
102
+ // In the files, the change is work: the loop works out what is left to build and plans it.
103
+ const statuses = await loop.bringUp(params.plain_file, params.guidance !== undefined ? { guidance: params.guidance } : {});
104
+ const lines = [
105
+ result === "applied" ? `Applied the change to ${touched.join(", ")}.` : `The change to ${touched.join(", ")} was already applied.`,
106
+ `Building in the background:\n${describeModules(statuses)}\n`,
107
+ // An interview goes back to its questions; a skill that writes on its own, straight to its next change - a stop to ask ends it.
108
+ paced
109
+ ? "Carry on with the interview."
110
+ : skillPaced
111
+ ? "Carry on: write the next change you decided yourself, or ask the next question - every answer is written as a change before the question after it."
112
+ : "Write the next change now, without stopping to ask: this skill goes on until everything it is to write is written.",
113
+ ];
114
+ return {
115
+ content: [{ type: "text", text: lines.join(" ") + reviewNote(liveReview) }],
116
+ details: { jobIds: statuses.flatMap((status) => (status.running !== undefined ? [status.running.jobId] : [])), files: touched },
117
+ };
118
+ },
119
+ });
120
+ return {
121
+ tools: [writeDiff],
122
+ pace: {
123
+ answered(owesChange = true) {
124
+ answeredSinceLastDiff = true;
125
+ if (owesChange)
126
+ owed = true;
127
+ },
128
+ owed: () => owed,
129
+ },
130
+ };
131
+ }
132
+ /** The module names some .plain file in these folders imports: import modules, not codebases. */
133
+ export function importedNames(dirs) {
134
+ const names = new Set();
135
+ for (const dir of dirs) {
136
+ let entries;
137
+ try {
138
+ entries = readdirSync(dir).filter((name) => name.endsWith(PLAIN_SOURCE_FILE_EXTENSION));
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ for (const entry of entries) {
144
+ try {
145
+ const imports = readPlainSourceMetadata(readFileSync(join(dir, entry), "utf8")).metadata[IMPORT_DIRECTIVE];
146
+ for (const name of imports ?? [])
147
+ names.add(name);
148
+ }
149
+ catch {
150
+ // A file that does not read imports nothing.
151
+ }
152
+ }
153
+ }
154
+ return names;
155
+ }
package/dist/stack.js ADDED
@@ -0,0 +1,158 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { plainFileParser, withParseCache } from "./plain/parser.js";
5
+ import { resolveTemplateDirs } from "./regenerate/paths.js";
6
+ import { sectionsOf } from "./regenerate/state.js";
7
+ import { onThisMachine } from "./regenerate/test-scripts.js";
8
+ /** Words in implementation reqs, and the toolchain each means. */
9
+ const STACKS = [
10
+ { toolchain: "Python", commands: ["python3", "python"], words: /\b(python|fastapi|django|flask|pytest|pydantic|sqlalchemy|uvicorn)\b/gi },
11
+ { toolchain: "Node", commands: ["node"], words: /\b(node(\.js)?|typescript|javascript|react|vite|next\.js|express|vue|svelte|npm|vitest|jest|playwright)\b/gi },
12
+ { toolchain: "Java", commands: ["java"], words: /\b(java|spring( boot)?|maven|gradle|junit|kotlin)\b/gi },
13
+ { toolchain: "Go", commands: ["go"], words: /\b(golang|go \d+\.\d+|go modules?|written in go)\b/gi },
14
+ { toolchain: "Rust", commands: ["cargo"], words: /\b(rust|cargo|tokio|axum)\b/gi },
15
+ { toolchain: "C/C++", commands: ["cc", "gcc", "clang"], words: /(\bc\+\+|\bcmake\b|\bgcc\b|\bclang\b|\bwritten in c\b)/gi },
16
+ { toolchain: "Zig", commands: ["zig"], words: /\bzig\b/gi },
17
+ { toolchain: "Odin", commands: ["odin"], words: /\bodin\b/gi },
18
+ { toolchain: "Ruby", commands: ["ruby"], words: /\b(ruby|rails)\b/gi },
19
+ { toolchain: "PHP", commands: ["php"], words: /\b(php|laravel|symfony)\b/gi },
20
+ { toolchain: "Elixir", commands: ["elixir"], words: /\b(elixir|phoenix)\b/gi },
21
+ ];
22
+ /** The toolchains a text of implementation reqs names. */
23
+ export function stackOf(text) {
24
+ const needs = [];
25
+ for (const stack of STACKS) {
26
+ const words = [...new Set([...text.matchAll(stack.words)].map((match) => match[0].toLowerCase()))];
27
+ if (words.length > 0)
28
+ needs.push({ toolchain: stack.toolchain, commands: stack.commands, words });
29
+ }
30
+ return needs;
31
+ }
32
+ /** The implementation reqs of every module in a project's folder, as the parser has them - imports in. */
33
+ export function projectImplementationReqs(project) {
34
+ let files;
35
+ try {
36
+ files = readdirSync(project).filter((name) => name.endsWith(".plain"));
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ // One parse a module for all of them: each parse goes down its requires chain, and a project of long chains would
42
+ // parse the modules at the bottom once for every module above them.
43
+ return withParseCache(() => {
44
+ const modules = [];
45
+ for (const name of files) {
46
+ const path = join(project, name);
47
+ try {
48
+ const [module, tree] = plainFileParser(basename(path), resolveTemplateDirs(path), { allowNoFunctionalities: true });
49
+ modules.push({ module, reqs: sectionsOf(tree)["implementation reqs"] });
50
+ }
51
+ catch {
52
+ // A file that does not parse, or an import module read on its own, names nothing here.
53
+ }
54
+ }
55
+ return modules;
56
+ });
57
+ }
58
+ /** Whether a command is on this machine's PATH. */
59
+ const onPath = (command) => onThisMachine(command) !== undefined;
60
+ /** What a project's modules need of this machine, and which of it the machine lacks. */
61
+ export function stackOnMachine(project, has = onPath) {
62
+ return projectImplementationReqs(project).map(({ module, reqs }) => {
63
+ const needs = stackOf(reqs.join("\n"));
64
+ return { module, needs, missing: needs.filter((need) => !need.commands.some(has)) };
65
+ });
66
+ }
67
+ /**
68
+ * The standard library's languages, and what builds and tests their stacks here - every command needed. Python's stacks
69
+ * run through uv, which fetches Python itself; TypeScript's through npm; Java's through the Maven or Gradle wrapper,
70
+ * which needs only Java.
71
+ */
72
+ export const STDLIB_LANGUAGES = [
73
+ { language: "Python", commands: ["uv"] },
74
+ { language: "TypeScript", commands: ["node", "npm"] },
75
+ { language: "Go", commands: ["go"] },
76
+ { language: "Rust", commands: ["cargo"] },
77
+ { language: "Java", commands: ["java"] },
78
+ ];
79
+ /**
80
+ * Whether a toolchain's command is really there: on the PATH - and Java on macOS, which keeps a `/usr/bin/java` that only
81
+ * asks to install a JDK, by `java_home` finding one.
82
+ */
83
+ function toolchainHere(command) {
84
+ if (!onPath(command))
85
+ return false;
86
+ if (command === "java" && process.platform === "darwin")
87
+ return spawnSync("/usr/libexec/java_home", [], { stdio: "ignore" }).status === 0;
88
+ return true;
89
+ }
90
+ /** Which of the standard library's languages this machine can build and test now, and which it lacks a command of. */
91
+ export function languagesOnMachine(has = toolchainHere) {
92
+ const ready = STDLIB_LANGUAGES.filter(({ commands }) => commands.every(has));
93
+ return { ready, missing: STDLIB_LANGUAGES.filter((entry) => !ready.includes(entry)) };
94
+ }
95
+ /**
96
+ * What a spec-writing agent and the conversation are told of this machine before a stack is chosen: the languages it
97
+ * can build now. The agents install nothing, so a stack in a language the machine lacks stops at its first build -
98
+ * offering one is offering a project that cannot be regenerated here until the person installs it.
99
+ */
100
+ export function toolchainsBlock(has = toolchainHere) {
101
+ const { ready, missing } = languagesOnMachine(has);
102
+ const named = (entries) => entries.map(({ language, commands }) => `${language} (${commands.join(", ")})`).join(", ");
103
+ return [
104
+ "<toolchains on this machine>",
105
+ ready.length > 0 ? `It can build and test now: ${named(ready)}.` : "It can build none of the standard library's languages now.",
106
+ ...(missing.length > 0 ? [`It lacks, to build: ${named(missing)} - not installed.`] : []),
107
+ "Every frontend stack - React, Vue, Svelte, Angular - is TypeScript, and needs Node and npm.",
108
+ "Choosing a stack, offer only those in a language it can build now, and choose only among them. A stack in a",
109
+ "language it lacks cannot be regenerated here until the person installs its toolchain - nothing is installed for them;",
110
+ "asked for one anyway, say what to install first, and that `turri doctor` checks it.",
111
+ "</toolchains on this machine>",
112
+ ].join("\n");
113
+ }
114
+ /** The toolchain commands a testing script is checked for. */
115
+ const SCRIPT_COMMANDS = new Set([
116
+ "python3", "python", "pip", "pip3", "pytest", "uv", "poetry", "node", "npm", "npx", "yarn", "pnpm", "bun", "deno", "java", "javac",
117
+ "mvn", "gradle", "go", "cargo", "rustc", "cc", "gcc", "clang", "make", "cmake", "zig", "ruby", "bundle", "php", "composer", "elixir",
118
+ "mix", "dotnet", "flutter", "dart", "swift",
119
+ ]);
120
+ /** Folders of the system a script names a program in by its whole path. */
121
+ const SYSTEM_PATH = /(?:^|[\s"'`$(=])(\/(?:usr|opt|Library|Applications|bin|sbin|lib|etc)\/[\w.+/-]+)/g;
122
+ /**
123
+ * What in a testing script this machine does not have: a program named by a whole path that is not there - a
124
+ * macOS-only `/usr/libexec/java_home` on Linux - and a toolchain it runs that is not on the PATH. The script is the
125
+ * person's: this is for them to fix, before an agent ever sees it fail.
126
+ */
127
+ export function scriptAgainstMachine(text, machine = {}) {
128
+ const has = machine.has ?? onPath;
129
+ const exists = machine.exists ?? existsSync;
130
+ const found = [];
131
+ const lines = text.split("\n").map((line) => line.replace(/(^|\s)#.*$/, ""));
132
+ for (const line of lines) {
133
+ for (const match of line.matchAll(SYSTEM_PATH)) {
134
+ const path = match[1];
135
+ if (!exists(path) && !found.some((item) => item.includes(`\`${path}\``)))
136
+ found.push(`names \`${path}\`, which this machine does not have`);
137
+ }
138
+ for (const part of line.split(/&&|\|\||[;|]|\$\(|`/)) {
139
+ const words = part.trim().split(/\s+/).filter((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word) && word !== "exec" && word !== "export" && word !== "timeout" && !/^\d+[smh]?$/.test(word));
140
+ const command = words[0];
141
+ if (command === undefined || !SCRIPT_COMMANDS.has(command) || has(command))
142
+ continue;
143
+ const said = `runs \`${command}\`, which is not on this machine's PATH`;
144
+ if (!found.includes(said))
145
+ found.push(said);
146
+ }
147
+ }
148
+ return found;
149
+ }
150
+ /** A testing script's text, or nothing when it cannot be read. */
151
+ export function readScript(path) {
152
+ try {
153
+ return readFileSync(path, "utf8");
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }