@turri-dev/cli 0.1.3-next.17f0f7f → 0.1.3-next.79e2ad0

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.
@@ -0,0 +1,82 @@
1
+ import { conformanceOption, overrideConfig, reviewOption, splitModelSpec } from "./config.js";
2
+ /**
3
+ * The command line of turri's commands: their words, and the options that stand for the config's own keys - kept apart
4
+ * from the commands that use them, so a light one (doctor) reads them as a regenerate does without loading it.
5
+ */
6
+ /**
7
+ * A command's arguments: the positional ones, `--flag`s and `--name value` options (`--name=value` too) -
8
+ * `--coding-model` is `--model`. An option it does not take is refused, rather than taking the word after it, and so is an
9
+ * option with no value - the end of the words, or another option where its value should be.
10
+ */
11
+ export function parseArgs(argv, flags, usage, valued = []) {
12
+ const positional = [];
13
+ const options = {};
14
+ const takes = new Set(["--model", ...valued]);
15
+ for (let i = 0; i < argv.length; i += 1) {
16
+ const arg = argv[i];
17
+ const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
18
+ const option = name === "--coding-model" ? "--model" : name;
19
+ if (arg === "-h" || arg === "--help") {
20
+ console.log(usage);
21
+ process.exit(0);
22
+ }
23
+ else if (flags.includes(option) && inline === undefined)
24
+ options[option.slice(2)] = "yes";
25
+ else if (takes.has(option)) {
26
+ // Its value is the next word - never another option, which would be taken as the value and lost.
27
+ const value = inline ?? argv[i + 1];
28
+ if (value === undefined || (inline === undefined && value.startsWith("--"))) {
29
+ console.error(`${usage}\n\n${name} needs a value${value !== undefined ? `, not the option ${value}` : ""} - ${name}=<value> gives one that starts with --.`);
30
+ process.exit(1);
31
+ }
32
+ if (inline === undefined)
33
+ i += 1;
34
+ options[option.slice(2)] = value;
35
+ }
36
+ else if (arg.startsWith("-")) {
37
+ console.error(`${usage}\n\nNo such option: ${name}.`);
38
+ process.exit(1);
39
+ }
40
+ else
41
+ positional.push(arg);
42
+ }
43
+ return { positional, options };
44
+ }
45
+ /** An option's value, as parseArgs left it: refused when it was given with none. */
46
+ export function optionValue(options, name) {
47
+ const value = options[name];
48
+ if (name in options && (value === undefined || value.trim() === ""))
49
+ failOption(`--${name} needs a value`);
50
+ return value;
51
+ }
52
+ /**
53
+ * The options that stand for the config's own keys - `--harness-model`, `--coding-model` (`--model`), `--judge-model`
54
+ * and, as asked, `--review` and `--conformance-tests` - read as if config.turri.yaml had them, in their place in the
55
+ * order the config resolves them in (overrideConfig): a run, or doctor, with them is the same as one with them in the
56
+ * file. Exits on a value it cannot read.
57
+ */
58
+ export function configKeyOptions(options, take = {}) {
59
+ const tests = take.conformance === true ? optionValue(options, "conformance-tests") : undefined;
60
+ const setting = tests !== undefined ? conformanceOption(tests) : undefined;
61
+ if (tests !== undefined && setting === undefined)
62
+ failOption(`--conformance-tests takes all, chosen, module or false, not ${tests}`);
63
+ const reviewed = take.review === true ? optionValue(options, "review") : undefined;
64
+ const review = reviewed !== undefined ? reviewOption(reviewed) : undefined;
65
+ if (reviewed !== undefined && review === undefined)
66
+ failOption(`--review takes true or false, not ${reviewed}`);
67
+ const model = (name, key) => {
68
+ const spec = optionValue(options, name);
69
+ return spec !== undefined ? { [key]: splitModelSpec(spec) } : {};
70
+ };
71
+ overrideConfig({
72
+ ...model("harness-model", "harnessModel"),
73
+ ...model("model", "codingModel"),
74
+ ...model("judge-model", "judgeModel"),
75
+ ...(setting !== undefined ? { conformanceTests: setting } : {}),
76
+ ...(review !== undefined ? { review } : {}),
77
+ });
78
+ }
79
+ function failOption(message) {
80
+ console.error(message);
81
+ process.exit(1);
82
+ }
@@ -1,4 +1,5 @@
1
- import { headlessOptions, parseArgs, runLoop } from "./loop-cli.js";
1
+ import { parseArgs } from "./cli-options.js";
2
+ import { headlessOptions, runLoop } from "./loop-cli.js";
2
3
  const USAGE = `usage: turri conform <file.plain> [frid ...] [options]
3
4
 
4
5
  Writes and runs the black-box conformance tests of a module's functional specs - those named, or
package/dist/loop-cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sweepOrphanedGroups } from "./regenerate/shell.js";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { resolve, sep } from "node:path";
4
- import { configForSpec, conformanceOption, reviewOption, inheritModel, modelOnly, modelSpec, overrideConfig, projectModels, readProjectConfig, roleModel, ROLE_THINKING, splitModelSpec, } from "./config.js";
4
+ import { configForSpec, inheritModel, modelOnly, modelSpec, projectModels, readProjectConfig, roleModel, ROLE_THINKING, splitModelSpec, } from "./config.js";
5
5
  import { PACKAGE_ROOT } from "./environment.js";
6
6
  import { c } from "./module-report.js";
7
7
  import { tryItCommand } from "./regenerate/delivery.js";
@@ -13,6 +13,7 @@ import { MODULES_DIR, projectDirOf } from "./regenerate/paths.js";
13
13
  import { exportModule } from "./regenerate/release.js";
14
14
  import { listFiles } from "./regenerate/testing.js";
15
15
  import { startRunReport } from "./run-report.js";
16
+ import { configKeyOptions, optionValue } from "./cli-options.js";
16
17
  /**
17
18
  * What every command that builds - `turri regenerate`, `conform`, `spec-diff` - does around its own
18
19
  * request of the work loop, as the chat does it: the project's models, a job that fails run once more and, failing again,
@@ -235,83 +236,15 @@ function reportModels(project, model) {
235
236
  }
236
237
  /**
237
238
  * The options every command that regenerates takes for a headless run: `--report` and `--build-dir`, and the config's
238
- * own keys - `--harness-model`, `--coding-model` (`--model`), `--judge-model`, `--review` and, where `conformance` is
239
- * true, `--conformance-tests` - read as if config.turri.yaml had them, the config's order of them kept (overrideConfig).
240
- * Exits on a value it cannot read.
239
+ * own keys (configKeyOptions) - `--harness-model`, `--coding-model` (`--model`), `--judge-model`, `--review` and, where
240
+ * `conformance` is true, `--conformance-tests` - read as if config.turri.yaml had them. Exits on a value it cannot read.
241
241
  */
242
242
  export function headlessOptions(options, conformance) {
243
- const given = (name) => {
244
- const value = options[name];
245
- if (name in options && (value === undefined || value.trim() === ""))
246
- failOption(`--${name} needs a value`);
247
- return value;
248
- };
249
- const tests = conformance ? given("conformance-tests") : undefined;
250
- const setting = tests !== undefined ? conformanceOption(tests) : undefined;
251
- if (tests !== undefined && setting === undefined)
252
- failOption(`--conformance-tests takes all, chosen, module or false, not ${tests}`);
253
- const reviewed = given("review");
254
- const review = reviewed !== undefined ? reviewOption(reviewed) : undefined;
255
- if (reviewed !== undefined && review === undefined)
256
- failOption(`--review takes true or false, not ${reviewed}`);
257
- const model = (name) => {
258
- const spec = given(name);
259
- return spec !== undefined ? { [name === "model" ? "codingModel" : name === "harness-model" ? "harnessModel" : "judgeModel"]: splitModelSpec(spec) } : {};
260
- };
261
- overrideConfig({
262
- ...model("harness-model"),
263
- ...model("model"),
264
- ...model("judge-model"),
265
- ...(setting !== undefined ? { conformanceTests: setting } : {}),
266
- ...(review !== undefined ? { review } : {}),
267
- });
268
- const report = given("report");
269
- const buildDir = given("build-dir");
243
+ configKeyOptions(options, { conformance, review: true });
244
+ const report = optionValue(options, "report");
245
+ const buildDir = optionValue(options, "build-dir");
270
246
  return { ...(report !== undefined ? { report } : {}), ...(buildDir !== undefined ? { buildDir } : {}) };
271
247
  }
272
- function failOption(message) {
273
- console.error(message);
274
- process.exit(1);
275
- }
276
- /**
277
- * A command's arguments: the positional ones, `--flag`s and `--name value` options (`--name=value` too) -
278
- * `--coding-model` is `--model`. An option it does not take is refused, rather than taking the word after it, and so is an
279
- * option with no value - the end of the words, or another option where its value should be.
280
- */
281
- export function parseArgs(argv, flags, usage, valued = []) {
282
- const positional = [];
283
- const options = {};
284
- const takes = new Set(["--model", ...valued]);
285
- for (let i = 0; i < argv.length; i += 1) {
286
- const arg = argv[i];
287
- const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
288
- const option = name === "--coding-model" ? "--model" : name;
289
- if (arg === "-h" || arg === "--help") {
290
- console.log(usage);
291
- process.exit(0);
292
- }
293
- else if (flags.includes(option) && inline === undefined)
294
- options[option.slice(2)] = "yes";
295
- else if (takes.has(option)) {
296
- // Its value is the next word - never another option, which would be taken as the value and lost.
297
- const value = inline ?? argv[i + 1];
298
- if (value === undefined || (inline === undefined && value.startsWith("--"))) {
299
- console.error(`${usage}\n\n${name} needs a value${value !== undefined ? `, not the option ${value}` : ""} - ${name}=<value> gives one that starts with --.`);
300
- process.exit(1);
301
- }
302
- if (inline === undefined)
303
- i += 1;
304
- options[option.slice(2)] = value;
305
- }
306
- else if (arg.startsWith("-")) {
307
- console.error(`${usage}\n\nNo such option: ${name}.`);
308
- process.exit(1);
309
- }
310
- else
311
- positional.push(arg);
312
- }
313
- return { positional, options };
314
- }
315
248
  /** Yes or no at the terminal - or undefined when there is none, and so no one to ask. */
316
249
  async function askAtTerminal(title, body) {
317
250
  if (process.stdin.isTTY !== true)
@@ -1,5 +1,6 @@
1
1
  import { CONFIG_FILE, readProjectConfig } from "./config.js";
2
- import { headlessOptions, parseArgs, runLoop } from "./loop-cli.js";
2
+ import { parseArgs } from "./cli-options.js";
3
+ import { headlessOptions, runLoop } from "./loop-cli.js";
3
4
  const USAGE = `usage: turri regenerate [file.plain] [frid ...] [options]
4
5
 
5
6
  Regenerates the module's code from its spec as it is now, and everything it requires, bottom first, as
package/dist/setup.js CHANGED
@@ -6,6 +6,7 @@ import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
6
6
  import { globalExtensionFile, MIN_NODE, nodeVersionOk, PACKAGE_ROOT, turriOnPath } from "./environment.js";
7
7
  import { sandboxAvailable } from "./regenerate/os-sandbox.js";
8
8
  import { findTestScripts } from "./regenerate/test-scripts.js";
9
+ import { configKeyOptions, parseArgs } from "./cli-options.js";
9
10
  import { hasProjectConfig, readProjectConfig, stackOf } from "./config.js";
10
11
  import { checkModels, modelsInUse, piModelAccess } from "./model-check.js";
11
12
  import { brand } from "./brand.js";
@@ -151,7 +152,7 @@ function checkGlobal() {
151
152
  export async function checkProjectModels(project, access) {
152
153
  const used = modelsInUse(readProjectConfig(project, stackOf(project)));
153
154
  if (used.length === 0) {
154
- return [{ level: "missing", name: "Models", detail: "config.turri.yaml names none", fix: "turri init-config writes the two models" }];
155
+ return [{ level: "missing", name: "Models", detail: "config.turri.yaml names none", fix: "turri init-config writes the two models - or name them here: --harness-model, --coding-model" }];
155
156
  }
156
157
  let verdicts;
157
158
  try {
@@ -167,16 +168,17 @@ export async function checkProjectModels(project, access) {
167
168
  : { level: "missing", name, detail: verdict.spec, more: [verdict.problem], fix: verdict.fix };
168
169
  });
169
170
  }
171
+ /** `modelsGiven`: models named on the command line - checked wherever doctor runs, a project or not. */
170
172
  async function allChecks(options = {}) {
171
173
  const checks = [checkNode(), checkShell(), checkGit(), checkSandbox(), await checkProvider(), checkPath(), ...checkGlobal()];
172
174
  // In a project: what it needs of this machine, and whether its models answer.
173
175
  const project = process.cwd();
174
- if (hasProjectConfig(project) || existsSync(join(project, "plain_modules"))) {
176
+ const inProject = hasProjectConfig(project) || existsSync(join(project, "plain_modules"));
177
+ if (inProject)
175
178
  checks.push(...checkProject(project));
176
- if (options.models !== false && hasProjectConfig(project)) {
177
- console.log(brand.muted("Asking each model the config names for a word..."));
178
- checks.push(...(await checkProjectModels(project)));
179
- }
179
+ if (options.models !== false && (hasProjectConfig(project) || options.modelsGiven === true)) {
180
+ console.log(brand.muted("Asking each model the config names for a word..."));
181
+ checks.push(...(await checkProjectModels(project)));
180
182
  }
181
183
  return checks;
182
184
  }
@@ -235,21 +237,27 @@ function removeGlobalExtension() {
235
237
  rmSync(file, { force: true });
236
238
  console.log(` removed ${file}`);
237
239
  }
240
+ const USAGE = [
241
+ "usage: turri doctor [--harness-model <m>] [--coding-model <m>] [--judge-model <m>] [--no-models] [--yes]",
242
+ "",
243
+ "Check everything turri needs - in a project, what its modules need of this machine too, and that",
244
+ "each model its config names answers with this machine's credentials (one short request a model;",
245
+ "--no-models skips it). The model options are read as the config's keys, as turri regenerate reads them:",
246
+ "a headless run's models can be checked before it starts, with no config.turri.yaml.",
247
+ "It changes nothing but this, asked first: a plain pi that an older turri made load turri too is put back to",
248
+ "plain pi. --yes answers yes.",
249
+ ].join("\n");
238
250
  export async function main(args) {
239
- if (args.includes("--help") || args.includes("-h")) {
240
- console.log("usage: turri doctor [--no-models] [--yes]\n\nCheck everything turri needs - in a project, what its modules need of this machine too, and that\n" +
241
- "each model its config names answers with this machine's credentials (one short request a model;\n--no-models skips it).\n" +
242
- "It changes nothing but this, asked first: a plain pi that an older turri made load turri too is put back to\n" +
243
- "plain pi. --yes answers yes.");
244
- return 0;
245
- }
251
+ const { options } = parseArgs(args, ["--no-models", "--yes", "-y"], USAGE, ["--harness-model", "--judge-model"]);
252
+ configKeyOptions(options);
246
253
  console.log(brand.bold(brand.accent("Checking what turri needs.")));
247
254
  if (!nodeVersionOk())
248
255
  return report([checkNode()]);
249
- const code = report(await allChecks({ models: !args.includes("--no-models") }));
256
+ const modelsGiven = ["harness-model", "model", "judge-model"].some((name) => options[name] !== undefined);
257
+ const code = report(await allChecks({ models: options["no-models"] === undefined, modelsGiven }));
250
258
  // turri and pi are kept apart: a plain pi does not start turri. An older turri's file that made it do so goes, asked.
251
259
  const global = globalExtensionFile();
252
- if (global !== undefined && (await confirm(`\nA plain \`pi\` loads turri too, through ${global}. Remove it, so pi stays pi?`, true, args.includes("--yes") || args.includes("-y")))) {
260
+ if (global !== undefined && (await confirm(`\nA plain \`pi\` loads turri too, through ${global}. Remove it, so pi stays pi?`, true, options.yes !== undefined || args.includes("-y")))) {
253
261
  removeGlobalExtension();
254
262
  }
255
263
  return code;
@@ -2,7 +2,8 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { checkPlainFile } from "./plain/check.js";
4
4
  import { applySpecDiff, revertSpecDiff } from "./regenerate/regenerate.js";
5
- import { headlessOptions, parseArgs, runLoop } from "./loop-cli.js";
5
+ import { parseArgs } from "./cli-options.js";
6
+ import { headlessOptions, runLoop } from "./loop-cli.js";
6
7
  const USAGE = `usage: turri spec-diff <file.plain> <diff-file> [options]
7
8
 
8
9
  Applies a change to the specs, given as a git diff of the .plain files, and builds it: the headless
package/docs/headless.md CHANGED
@@ -14,7 +14,8 @@ Everything marked `✗` stops the run, so fix those first. Two more things matte
14
14
  a module's implementation reqs name that this machine lacks.
15
15
  - **The models.** In a project, `turri doctor` sends each model `config.turri.yaml` names one short request, as turri's
16
16
  agents will - so a key the provider refuses, a model pi does not know or an account without access to it shows now,
17
- not an hour into the run.
17
+ not an hour into the run. Models the run is given on the command line are given to doctor the same way -
18
+ `--harness-model`, `--coding-model`, `--judge-model`, read as the config's keys - and are checked with the config's.
18
19
  - **Credentials that do not expire mid-run.** An API key in the environment
19
20
  (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, ...) or pi's saved login - whatever
20
21
  `doctor` shows as the model provider.
@@ -260,9 +261,16 @@ turri regenerate app.plain --harness-model amazon-bedrock/eu.anthropic.claude-op
260
261
  - **What it needs:** Node 22.19 or newer, `bash`, `git` (each module's history, and the spec diffs
261
262
  the work is planned from), `/bin/sh` with `kill` and `sleep`, a writable `HOME` (pi keeps
262
263
  `~/.pi/agent/auth.json` there, even with no login - or set `PI_CODING_AGENT_DIR`) and `TMPDIR`, and
263
- the toolchains the project's testing scripts and implementation reqs name. `turri doctor --no-models`
264
- in the project checks them; `--no-models` because a `config.yaml` written for another tool names no models, and
265
- doctor would count that against it.
264
+ the toolchains the project's testing scripts and implementation reqs name. `turri doctor` in the project, given
265
+ the run's models, checks them all and that each model answers - at the start of the task's log, not when a pulse
266
+ fails:
267
+
268
+ ```bash
269
+ turri doctor --harness-model amazon-bedrock/eu.anthropic.claude-opus-5-5 \
270
+ --coding-model amazon-bedrock/eu.anthropic.claude-sonnet-5-5 --judge-model amazon-bedrock/eu.anthropic.claude-opus-5-5
271
+ ```
272
+
273
+ It exits 1 on anything marked `✗`.
266
274
  - **Without bubblewrap:** a container that cannot make user namespaces - no
267
275
  `--security-opt seccomp=unconfined` - has no OS sandbox. turri runs anyway, the command check and the
268
276
  audit holding alone under the agents' commands; `turri doctor` shows it as `!`, not `✗`. Installing
@@ -68,7 +68,7 @@ In a project - a folder with `config.turri.yaml` (or the old `config.yaml`) or `
68
68
  | --- | --- |
69
69
  | each module's toolchains | what its implementation reqs name - Python, Node, Java, Go, Rust, C/C++, Zig, ... - against this machine's PATH: the agents use this machine's toolchains and install none, so a missing one is yours to install |
70
70
  | the testing scripts | what in them this machine does not have: a program named by its whole path that is not here (a macOS-only `/usr/libexec/java_home` on Linux), a toolchain they run that is not on the PATH |
71
- | the models | each model `config.turri.yaml` names - `harness-model`, `coding-model`, `judge-model`, every role under `models:` - sent one short request through pi, with the credentials turri's agents will use, for any provider pi supports. One that answers is ready; for one that does not, it says what the provider said and what to do: the credentials pi reads for that provider (`AWS_PROFILE` and `AWS_REGION` for Amazon Bedrock, `OPENAI_API_KEY`, `/login`, ...), model access in the Bedrock console, the closest model ids pi knows for one it does not. `--no-models` skips it |
71
+ | the models | each model `config.turri.yaml` names - `harness-model`, `coding-model`, `judge-model`, every role under `models:` - sent one short request through pi, with the credentials turri's agents will use, for any provider pi supports. One that answers is ready; for one that does not, it says what the provider said and what to do: the credentials pi reads for that provider (`AWS_PROFILE` and `AWS_REGION` for Amazon Bedrock, `OPENAI_API_KEY`, `/login`, ...), model access in the Bedrock console, the closest model ids pi knows for one it does not. `--harness-model`, `--coding-model` (`--model`) and `--judge-model` name models as the config's keys, as `turri regenerate` reads them - checked in a project or not. `--no-models` skips it |
72
72
 
73
73
  Removing that file is the one thing `doctor` changes, and it asks first; `turri doctor --yes` says yes. Run without a
74
74
  terminal - piped, or in CI - it asks nothing and changes nothing. Run it again whenever something breaks;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.3-next.17f0f7f",
3
+ "version": "0.1.3-next.79e2ad0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {