@starci/cli 1.1.0 → 1.2.0

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/src/main.mjs CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import process from 'node:process';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { spawnSync } from 'node:child_process';
6
+ import { retargetNotice, targetOf, treeTargetRefusal } from './runtime-target.mjs';
6
7
  import { foreignRuntimeNotice, locateRuntime, ownRuntimeRoot, runtimeEntryOf } from './runtime-locate.mjs';
7
8
  import { runtimeEnv } from './shim.mjs';
8
9
 
@@ -202,11 +203,16 @@ const hfsDispatch = async (split, group, args, { io, cwd, stdout, stderr }) => {
202
203
  };
203
204
 
204
205
  /** Spawn the located runtime for the resolved verb; its exit status is the command's. */
205
- const runRuntime = (split, args, { io, cwd, env, home, stderr }) => {
206
+ const runRuntime = (split, args, { io, cwd, env, home, stderr, command }) => {
206
207
  const skipped = [];
207
- const located = (io.locateRuntime ?? locateRuntime)({ cwd, env, skipped, ...(home ? { home } : {}) });
208
- if (!located) return noRuntime(stderr, split.group, skipped);
209
- const notice = foreignRuntimeNotice(located, (io.ownRuntimeRoot ?? ownRuntimeRoot)());
208
+ const record = (io.locateRuntime ?? locateRuntime)({ cwd, env, skipped, ...(home ? { home } : {}) });
209
+ if (!record) return noRuntime(stderr, split.group, skipped);
210
+ const own = (io.ownRuntimeRoot ?? ownRuntimeRoot)();
211
+ const { located, retargeted } = targetOf({ command, args, located: record, own });
212
+ if (retargeted) writeTo(stderr, retargetNotice(retargeted, own));
213
+ const refused = treeTargetRefusal({ command, args, located, own });
214
+ if (refused) return fail(stderr, refused.message, 2);
215
+ const notice = foreignRuntimeNotice(located, own);
210
216
  if (notice) writeTo(stderr, notice);
211
217
  const spawn = io.spawn ?? spawnSync;
212
218
  let result;
@@ -268,5 +274,5 @@ export async function main(argv = process.argv.slice(2), io = {}) {
268
274
  if (runtimeOut !== null) return runtimeOut;
269
275
  const hfsOut = await hfsDispatch(split, group, args, { ...ctx, cwd });
270
276
  if (hfsOut !== null) return hfsOut;
271
- return runRuntime(split, args, { ...ctx, cwd });
277
+ return runRuntime(split, args, { ...ctx, cwd, command });
272
278
  }
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/cli/gen-catalog.mjs — do not edit
2
- export const RUNTIME_VERSION = "1.0.0-alpha.7";
2
+ export const RUNTIME_VERSION = "1.0.0-alpha.9";
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+
3
+ /** The code a verb that writes the runtime tree is refused with when it would write a tree other than the CLI's own. */
4
+ export const RUNTIME_TREE_FOREIGN = 'RUNTIME_TREE_TARGET_FOREIGN';
5
+
6
+ const sameRoot = (a, b) => (process.platform === 'win32' ? path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase() : path.resolve(a) === path.resolve(b));
7
+
8
+ /** Whether this invocation of `command` writes the runtime tree: its `writesRuntimeTree` is true (always), a flag name that makes it write, or `!flag` (writes unless that flag is given). */
9
+ function writesTree(command, args) {
10
+ const mark = command?.writesRuntimeTree;
11
+ if (mark === true) return true;
12
+ if (typeof mark !== 'string') return false;
13
+ return mark.startsWith('!') ? !args.includes(`--${mark.slice(1)}`) : args.includes(`--${mark}`);
14
+ }
15
+
16
+ /**
17
+ * The refusal of a verb that writes the runtime tree while the located runtime is not the tree that owns the running CLI and nothing chose it
18
+ * explicitly (STARCI_RUNTIME, or the verb's own --root). The per-user record outranks the checkout for reading, so a clone's bin would otherwise
19
+ * write the live tree. Null when the verb does not write the tree, the CLI is not in a runtime, or the target is explicit or the CLI's own. Pure.
20
+ */
21
+ export function treeTargetRefusal({ command, args, located, own }) {
22
+ if (!located || !own || !writesTree(command, args)) return null;
23
+ if (located.source === 'STARCI_RUNTIME' || args.includes('--root') || sameRoot(located.root, own)) return null;
24
+ return { code: RUNTIME_TREE_FOREIGN, message: `${RUNTIME_TREE_FOREIGN}: this verb writes the runtime tree, and the located runtime ${located.root} (${located.source}) is not the tree ${own} that owns this CLI; set STARCI_RUNTIME=${own} (or pass --root) to say which tree to write` };
25
+ }
26
+
27
+ /** The explicit runtime choices that outrank a verb's side: STARCI_RUNTIME, or the verb's own --root. */
28
+ const chosenExplicitly = (located, args) => located.source === 'STARCI_RUNTIME' || args.includes('--root');
29
+
30
+ /**
31
+ * The runtime a verb runs: a tree-side verb (one whose result is about the runtime tree that owns the running bin: a check, a test selection, a receipt, a catalog
32
+ * regeneration) runs THIS checkout unless a tree was chosen explicitly, so its verdict is about the tree it ran in; a host-side verb runs the located runtime (the
33
+ * per-user record included). {located, retargeted}: `retargeted` carries the runtime the record pointed at when this checkout replaced it. Pure.
34
+ */
35
+ export function targetOf({ command, args, located, own }) {
36
+ if (!located || !own || command?.runtimeSide !== 'tree' || chosenExplicitly(located, args) || sameRoot(located.root, own)) return { located, retargeted: null };
37
+ return { located: { root: own, source: 'own-checkout' }, retargeted: located };
38
+ }
39
+
40
+ /** The one stderr line saying a tree-side verb ran this checkout although the located runtime is another tree. */
41
+ export const retargetNotice = (retargeted, own) => `starci: this verb is about the runtime tree that owns this CLI, so it ran ${own}, not ${retargeted.root} (${retargeted.source}); set STARCI_RUNTIME to judge another tree\n`;
@@ -31,15 +31,24 @@ const typedValue = (flag, raw) => {
31
31
  return raw;
32
32
  };
33
33
 
34
+ /** The values a list flag takes after its first one when the verb has no positional slot: every following token that is no option (`--knowledge a b c`), as the contracts write it. */
35
+ const listTail = (argv, index, flag, verb) => {
36
+ if (flag.type !== 'list' || (verb.positional ?? []).length > 0) return [];
37
+ const tail = [];
38
+ while (index + 2 + tail.length < argv.length && !argv[index + 2 + tail.length].startsWith('-')) tail.push(argv[index + 2 + tail.length]);
39
+ return tail;
40
+ };
41
+
34
42
  /** One `--name[=value]` token: type-check it into values/global, or pass it through to localArgs. */
35
- const applyOption = (argv, index, token, { known, values, global, localArgs }) => {
43
+ const applyOption = (argv, index, token, { known, internal, values, global, localArgs, verb }) => {
36
44
  const name = optionName(token);
37
45
  const flag = known.get(name);
38
- if (!flag) throw new Error(`unknown option --${name}`);
46
+ if (!flag) throw new Error(internal.has(name) ? `unknown option --${name}: it is a call of the runtime itself, not a CLI option; ${internal.get(name)}` : `unknown option --${name}`);
39
47
  const { value: raw, consumed } = optionValue(argv, index, flag, known);
40
48
  const value = typedValue(flag, raw);
49
+ const tail = consumed ? listTail(argv, index, flag, verb) : [];
41
50
  if (flag.type === 'list') {
42
- values[name] = [...(values[name] ?? []), value];
51
+ values[name] = [...(values[name] ?? []), value, ...tail];
43
52
  } else if (Object.hasOwn(values, name)) {
44
53
  throw new Error(`--${name} may be given only once`);
45
54
  } else {
@@ -49,8 +58,10 @@ const applyOption = (argv, index, token, { known, values, global, localArgs }) =
49
58
  else {
50
59
  localArgs.push(token);
51
60
  if (consumed) localArgs.push(argv[index + consumed]);
61
+ // each further value of a list flag reaches the handler as a repeated option, the form every list-flag handler parses
62
+ for (const extra of tail) localArgs.push(token, extra);
52
63
  }
53
- return consumed;
64
+ return consumed + tail.length;
54
65
  };
55
66
 
56
67
  /** Every required local option must have been given. */
@@ -121,6 +132,8 @@ export function validateArgs(argv, verb, globalFlags = []) {
121
132
  const globals = new Map(globalFlags.map((flag) => [flag.name, { ...flag, global: true }]));
122
133
  const locals = new Map((verb.flags ?? []).map((flag) => [flag.name, flag]));
123
134
  const known = new Map([...globals, ...locals]);
135
+ // Flags the runtime's own scripts take and the CLI refuses, each with the pointer a person is given instead.
136
+ const internal = new Map((verb.internalFlags ?? []).map((entry) => [entry.name, entry.pointer]));
124
137
  const values = {};
125
138
  const global = {};
126
139
  const localArgs = [];
@@ -142,7 +155,7 @@ export function validateArgs(argv, verb, globalFlags = []) {
142
155
  continue;
143
156
  }
144
157
  if (token.startsWith('--')) {
145
- index += applyOption(argv, index, token, { known, values, global, localArgs });
158
+ index += applyOption(argv, index, token, { known, internal, values, global, localArgs, verb });
146
159
  continue;
147
160
  }
148
161
  if (token.startsWith('-')) throw new Error(`unknown option ${token}`);