@antasphere/chassis-cli 1.0.2 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antasphere/chassis-cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,10 +28,10 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@antasphere/cli-core": "^0.5.0",
31
+ "@antasphere/cli-core": "^0.6.0",
32
32
  "commander": "^12.1.0",
33
- "@antasphere/chassis-contract": "1.0.2",
34
- "@antasphere/chassis-sdk": "1.0.2"
33
+ "@antasphere/chassis-contract": "1.1.0",
34
+ "@antasphere/chassis-sdk": "1.1.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.15.0",
@@ -1,12 +1,11 @@
1
1
  import type { Command } from 'commander';
2
2
  import {
3
- activeHubProfile,
4
3
  ask,
5
4
  CliAuthClient,
6
5
  CliAuthError,
7
6
  CliUsageError as CoreUsageError,
8
7
  CLOUD_PROFILE,
9
- connectOnDemand,
8
+ firstSet,
10
9
  HUB_TOOL,
11
10
  loadConfig as loadCoreConfig,
12
11
  normalizeUrl,
@@ -15,10 +14,12 @@ import {
15
14
  sameInstance,
16
15
  selectProfile,
17
16
  saveConfig as saveCoreConfig,
17
+ selectHubProfile,
18
18
  type CliProfile
19
19
  } from '@antasphere/cli-core';
20
20
  import type { ChassisClient } from '@antasphere/chassis-sdk';
21
21
  import { redactKey } from '../config.js';
22
+ import { DEFAULT_HUB_URL } from '../identity.js';
22
23
  import {
23
24
  CliUsageError,
24
25
  printJson,
@@ -48,7 +49,6 @@ import type { MeResponse } from '../workspace.js';
48
49
 
49
50
  /** The hub a tool CLI signs in against inline: the same variable the hub CLI reads. */
50
51
  const HUB_URL_ENV = 'ANTASPHERE_URL';
51
- const DEFAULT_HUB_URL = 'https://account.antasphere.com';
52
52
 
53
53
  interface AuthGlobals {
54
54
  apiUrl?: string;
@@ -109,7 +109,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
109
109
  createClient,
110
110
  clearConfig,
111
111
  configPath,
112
- connectOptions,
112
+ connect,
113
113
  loadConfig,
114
114
  profileUrl,
115
115
  removeConnectKey,
@@ -158,6 +158,10 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
158
158
  // active one, else the implicit `cloud` at account.antasphere.com; the
159
159
  // URL is the variable, else that profile's own, else the default. A
160
160
  // stored hub URL is never repointed at the production hub.
161
+ // `selectHubProfile`, the exchange's pick, goes through the same
162
+ // `selectProfile` with the same cloud URL, so the two agree (with no
163
+ // variable and no active profile it names none; the write below makes
164
+ // this pick the active one).
161
165
  const hubConfig = loadCoreConfig(io.env, HUB_TOOL);
162
166
  const selected = selectProfile({
163
167
  config: hubConfig,
@@ -283,20 +287,29 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
283
287
  // stores it, so every other tool CLI is signed in too. `--email`
284
288
  // naming ANOTHER account than the stored login signs that account in
285
289
  // afresh, replacing the stored login (PRDCT-3032): the person who
286
- // signed in with the wrong address switches from here.
287
- let hub = activeHubProfile(io.env);
288
- const stored = hub.profile?.apiKey ? hub.profile.email : undefined;
290
+ // signed in with the wrong address switches from here. A stored key
291
+ // with no usable address (absent or blank) is someone unknown, so
292
+ // `--email` on it signs in afresh too (verifier round 1, F2).
293
+ let hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
294
+ const hasKey = Boolean(hub.profile?.apiKey);
295
+ const stored = hasKey ? firstSet(hub.profile?.email) : undefined;
289
296
  const switching =
290
- Boolean(stored) && opts.email !== undefined && opts.email.toLowerCase() !== stored!.toLowerCase();
297
+ hasKey &&
298
+ opts.email !== undefined &&
299
+ (stored === undefined || opts.email.toLowerCase() !== stored.toLowerCase());
291
300
  if (switching) {
292
- io.err.write(`Replacing the Antasphere login ${stored} with ${opts.email}.\n`);
301
+ io.err.write(
302
+ stored
303
+ ? `Replacing the Antasphere login ${stored} with ${opts.email}.\n`
304
+ : `Signing in as ${opts.email}: the stored Antasphere login carries no address.\n`
305
+ );
293
306
  }
294
307
  if (!hub.profile?.apiKey || switching) await hubLogin(opts);
295
308
  // A login is an explicit re-sign-in (verifier round 1, F3): the key
296
309
  // cached on this profile is revoked on its instance (best effort: a
297
310
  // dead one answers 401) and forgotten, and the exchange runs afresh,
298
311
  // so a stale cached key never survives a login.
299
- hub = activeHubProfile(io.env);
312
+ hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
300
313
  const stale = hub.name
301
314
  ? loadConfig(io.env).profiles[profileName]?.connectKeys?.[hub.name]
302
315
  : undefined;
@@ -314,13 +327,13 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
314
327
  // no longer accepts is replaced by a fresh sign-in, once.
315
328
  let outcome;
316
329
  try {
317
- outcome = await connectOnDemand({ ...connectOptions(ctx), profileName, cached: false });
330
+ outcome = await connect(ctx, { profileName, cached: false });
318
331
  } catch (e) {
319
332
  if (!(e instanceof CoreUsageError) || !/rejected/.test(e.message)) throw e;
320
333
  io.err.write('The stored Antasphere login was rejected: signing in again.\n');
321
334
  await hubLogin(opts);
322
- hub = activeHubProfile(io.env);
323
- outcome = await connectOnDemand({ ...connectOptions(ctx), profileName, cached: false });
335
+ hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
336
+ outcome = await connect(ctx, { profileName, cached: false });
324
337
  }
325
338
  if (outcome.outcome === 'not_cloud') {
326
339
  throw new CliUsageError(`${baseUrl} stopped answering as an Antasphere-cloud instance; try again.`);
@@ -369,10 +382,13 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
369
382
 
370
383
  /** The hub login `--all` ends: `antasphere logout` from here (the key revoked on the hub, the profile cleared). */
371
384
  async function hubLogout(): Promise<{ email: string | undefined; revoked: boolean } | null> {
372
- const hub = activeHubProfile(io.env);
385
+ const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
373
386
  if (!hub.name || !hub.profile?.apiKey) return null;
387
+ // Revoked at the HUB's own URL: the profile's, else the default hub. Never
388
+ // the tool kit's `profileUrl`, whose fallback is the tool's cloud host: a
389
+ // hub profile with no baseUrl would send the hub key there.
374
390
  const auth = new CliAuthClient({
375
- baseUrl: profileUrl(hub.profile),
391
+ baseUrl: hub.profile.baseUrl?.trim() || DEFAULT_HUB_URL,
376
392
  ...(io.fetch ? { fetch: io.fetch } : {})
377
393
  });
378
394
  let revoked = false;
@@ -402,7 +418,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
402
418
  const ctx = resolveContext(cmd, io);
403
419
  const profileName = ctx.profileName;
404
420
  const profile = profileName ? ctx.config.profiles[profileName] : undefined;
405
- const hubLogin = activeHubProfile(io.env).profile;
421
+ const hubLogin = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL }).profile;
406
422
  if (!profileName || !profile) {
407
423
  // Nothing of this tool's on the machine. The Antasphere login, when
408
424
  // there is one, is the family's: it is ended here on --all, else
@@ -510,7 +526,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
510
526
  // `me` is the answer WITH the selection applied, so its workspace is
511
527
  // the one this command (and any other, same flags) really ran in.
512
528
  const source = workspaceSource(ctx);
513
- const hub = activeHubProfile(io.env);
529
+ const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
514
530
  const hubLine = hubLoginLine(ctx, hub.profile);
515
531
  if (ctx.json) {
516
532
  return printJson(io, {
@@ -600,7 +616,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
600
616
  .action(async (_opts, cmd: Command) => {
601
617
  const globals = cmd.optsWithGlobals() as AuthGlobals;
602
618
  const config = loadConfig(io.env);
603
- const hub = activeHubProfile(io.env);
619
+ const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
604
620
  // The implicit cloud profile is listed even before it is on disk: it is
605
621
  // where a clean machine runs, and where `login` with no URL signs in.
606
622
  const rows: Array<[string, CliProfile]> = Object.entries(config.profiles);
@@ -612,8 +628,12 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
612
628
  const entry = hub.name ? p.connectKeys?.[hub.name] : undefined;
613
629
  if (entry) return { kind: 'hub', text: `via Antasphere${entry.email ? ` (${entry.email})` : ''}` };
614
630
  const others = Object.keys(p.connectKeys ?? {});
631
+ // The hub profile is the one ANTASPHERE_URL names when it is set, else the active one.
632
+ const chosen = firstSet(io.env[HUB_URL_ENV])
633
+ ? 'not the one ANTASPHERE_URL names'
634
+ : 'not the active one';
615
635
  if (others.length > 0)
616
- return { kind: 'hub', text: `via Antasphere (login "${others.join('", "')}", not the active one)` };
636
+ return { kind: 'hub', text: `via Antasphere (login "${others.join('", "')}", ${chosen})` };
617
637
  return { kind: 'none', text: '(not signed in)' };
618
638
  };
619
639
  if (globals.json) {
package/src/context.ts CHANGED
@@ -1,24 +1,29 @@
1
1
  import { PlatformApiError, type ChassisClient, type ClientOptions } from '@antasphere/chassis-sdk';
2
2
  import type { Command } from 'commander';
3
3
  import {
4
- activeHubProfile,
4
+ cachedConnectKey,
5
5
  CLOUD_PROFILE,
6
6
  CliAuthClient,
7
7
  CliUsageError,
8
8
  connectOnDemand,
9
- lookupConnectKey,
9
+ firstSet,
10
+ HUB_TOOL,
11
+ loadConfig as loadCoreConfig,
10
12
  printJson as corePrintJson,
11
13
  refreshConnectKey,
12
14
  resolveApiKey,
13
15
  resolveBaseUrl,
14
16
  sameInstance,
17
+ saveConfig as saveCoreConfig,
18
+ selectHubProfile,
15
19
  selectProfile,
16
20
  type CliIo as CoreCliIo,
17
21
  type ConnectOnDemandOptions,
22
+ type ConnectOnDemandOutcome,
18
23
  type ProfileSource
19
24
  } from '@antasphere/cli-core';
20
25
  import type { CliConfig, CliConfigStore, CliProfile } from './config.js';
21
- import type { CliIdentity } from './identity.js';
26
+ import { DEFAULT_HUB_URL, type CliIdentity } from './identity.js';
22
27
  import {
23
28
  isWorkspaceId,
24
29
  matchWorkspace,
@@ -216,6 +221,15 @@ export interface CliContextKit<TClient extends ChassisClient<string>> {
216
221
  profileUrl(profile: CliProfile | undefined): string;
217
222
  /** The `connectOnDemand` options of one context (the tool, the instance, the profile, the wire). */
218
223
  connectOptions(ctx: CliContext<TClient>): ConnectOnDemandOptions;
224
+ /**
225
+ * The hub exchange of one context: cli-core `connectOnDemand` with
226
+ * `connectOptions(ctx)` and `overrides`, and, after a fresh exchange, the
227
+ * hub profile healed with the address the hub answered when it has none.
228
+ */
229
+ connect(
230
+ ctx: CliContext<TClient>,
231
+ overrides?: Partial<ConnectOnDemandOptions>
232
+ ): Promise<ConnectOnDemandOutcome>;
219
233
  }
220
234
 
221
235
  /**
@@ -305,15 +319,18 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
305
319
  ? 'profile'
306
320
  : 'none';
307
321
  if (!apiKey && sameProfileInstance) {
308
- // No direct key: the connect cache's slot is the ACTIVE hub profile
309
- // (what `antasphere login` stored) — org-independent by design. A key
310
- // cached for ANOTHER account than the login now stored on that slot
311
- // (`antasphere login --email other` since) is never served: it is the
312
- // other person's credential (PRDCT-3032); connectForKey retires it.
313
- const hub = activeHubProfile(io.env);
314
- if (hub.name && !cachedForAnotherAccount(profile, hub.name, hub.profile?.email)) {
315
- apiKey = lookupConnectKey(profile, hub.name);
316
- }
322
+ // No direct key: the connect cache's slot is the hub profile of this
323
+ // run (the one ANTASPHERE_URL names, else the active one: what
324
+ // `antasphere login` stored), org-independent by design. A key is
325
+ // served only to the address that minted it (cli-core
326
+ // `cachedConnectKey`): another account's key is the other person's
327
+ // credential (PRDCT-3032), and a missing address on either side, the
328
+ // entry's or the hub profile's, is unknown, so nothing is served
329
+ // (PRDCT-3046); connectForKey retires the entry. This read repeats
330
+ // cli-core's own (connectOnDemand step 0) on purpose: the credential
331
+ // source and the recovering fetch are decided here.
332
+ const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
333
+ if (hub.name) apiKey = cachedConnectKey(profile, hub.name, hub.profile?.email);
317
334
  if (apiKey) credentialSource = 'hub-cache';
318
335
  }
319
336
  const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
@@ -344,21 +361,6 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
344
361
  return ctx;
345
362
  }
346
363
 
347
- /**
348
- * The cached connect key on `slot` was minted for another Antasphere
349
- * account than the one whose login the slot now holds. Both addresses must
350
- * be known: a cache entry or a hub profile without one is trusted as before.
351
- */
352
- function cachedForAnotherAccount(
353
- profile: CliProfile | undefined,
354
- slot: string,
355
- hubEmail: string | undefined
356
- ): boolean {
357
- const cached = profile?.connectKeys?.[slot]?.email;
358
- if (!cached || !hubEmail) return false;
359
- return cached.toLowerCase() !== hubEmail.toLowerCase();
360
- }
361
-
362
364
  const NO_KEY_MESSAGE =
363
365
  `An API key is required. Sign in (\`${identity.bin} login\`), pass --api-key, or set ` +
364
366
  `${identity.envPrefix}_API_KEY.`;
@@ -376,6 +378,10 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
376
378
  toolBaseUrl: ctx.baseUrl,
377
379
  // A profile with no baseUrl of its own is at the cloud URL (cli-core 0.5.0).
378
380
  cloudUrl: identity.cloudUrl,
381
+ // The same for a hub profile (cli-core 0.6.0, verifier round 1, F4):
382
+ // one with no baseUrl is at the default hub, so ANTASPHERE_URL naming
383
+ // that hub finds it, and the slot it names is the one this kit reads.
384
+ hubCloudUrl: DEFAULT_HUB_URL,
379
385
  profileName: ctx.profileName ?? CLOUD_PROFILE,
380
386
  env: io.env,
381
387
  // Thread the injected fetch so the probe + exchange stay on the test
@@ -418,9 +424,14 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
418
424
  }
419
425
  const outcome = await refreshConnectKey(connectOptions(ctx));
420
426
  if (outcome.outcome === 'not_cloud') return res;
427
+ // The cause before its consequence (verifier round 3, F1): the sign-in
428
+ // is said first, then what the fresh mint healed.
429
+ ctx.io.err.write('The cached key was refused; signed in again through Antasphere.\n');
430
+ // A fresh mint like any other: the hub profile is healed here too, or
431
+ // the next run would retire the key just minted (verifier round 2, F1).
432
+ healHubAddress(ctx, outcome);
421
433
  ctx.apiKey = outcome.key;
422
434
  ctx.credentialSource = 'hub-exchange';
423
- ctx.io.err.write('The cached key was refused; signed in again through Antasphere.\n');
424
435
  const headers = new Headers(init?.headers);
425
436
  headers.set('authorization', `Bearer ${outcome.key}`);
426
437
  return base(input, { ...init, headers });
@@ -515,32 +526,80 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
515
526
  return ` (looked in the ${noun} "${selection.value}", selected by ${describeSelection(selection)})`;
516
527
  }
517
528
 
529
+ async function connect(
530
+ ctx: CliContext<TClient>,
531
+ overrides: Partial<ConnectOnDemandOptions> = {}
532
+ ): Promise<ConnectOnDemandOutcome> {
533
+ const outcome = await connectOnDemand({ ...connectOptions(ctx), ...overrides });
534
+ healHubAddress(ctx, outcome);
535
+ return outcome;
536
+ }
537
+
538
+ /**
539
+ * The hub profile healed with the address the hub answered, after a fresh
540
+ * exchange: when it carries none or a blank one (an older login, a
541
+ * hand-edited file), or one the hub no longer answers (the account changed
542
+ * its address at the hub). Without it the slot would be refused and a key
543
+ * re-minted and revoked on every run, since a key is served only to the
544
+ * address that minted it. The answer is the key owner's own current
545
+ * address, so no other person's address can be written. That profile
546
+ * only, nothing else of the hub's file moves. A replaced address is said
547
+ * on stderr; an absent one is filled in silently. Called after every
548
+ * fresh exchange the kit runs: `connect` and the stranded-key recovery.
549
+ */
550
+ function healHubAddress(ctx: CliContext<TClient>, outcome: ConnectOnDemandOutcome): void {
551
+ // `!outcome.cached`: a served key implies a hub address already equal to
552
+ // the entry's, so only a fresh exchange can have anything to heal; the
553
+ // guard states that intent.
554
+ if (outcome.outcome !== 'connected' || outcome.cached || !outcome.result) return;
555
+ // A chassis tool never answers a blank address; a foreign one must not
556
+ // blank the profile.
557
+ const answered = firstSet(outcome.result.user.email);
558
+ if (!answered) return;
559
+ const hub = selectHubProfile(ctx.io.env, { cloudUrl: DEFAULT_HUB_URL });
560
+ const hubConfig = loadCoreConfig(ctx.io.env, HUB_TOOL);
561
+ const stored = hub.name ? hubConfig.profiles[hub.name] : undefined;
562
+ const had = firstSet(stored?.email);
563
+ if (!hub.name || !stored || had?.toLowerCase() === answered.toLowerCase()) return;
564
+ hubConfig.profiles[hub.name] = { ...stored, email: answered };
565
+ saveCoreConfig(ctx.io.env, HUB_TOOL, hubConfig);
566
+ if (had) {
567
+ ctx.io.err.write(
568
+ `The Antasphere login's address is now ${answered}, replaced from ${had} (the hub's own answer).\n`
569
+ );
570
+ }
571
+ }
572
+
518
573
  async function connectForKey(ctx: CliContext<TClient>): Promise<string> {
519
574
  const { io } = ctx;
520
- // The cached key of another account (the hub login on the slot changed)
521
- // is revoked on its instance and forgotten before the exchange runs for
522
- // the account now signed in (PRDCT-3032). Best effort: a dead key
523
- // answers 401 and is forgotten all the same.
524
- const hub = activeHubProfile(io.env);
575
+ // An entry on the slot the cache refuses to serve (another account's
576
+ // key, the hub login on the slot having changed, PRDCT-3032; or a key
577
+ // whose address, or the hub profile's, is missing, PRDCT-3046) is
578
+ // revoked on its instance and forgotten before the exchange runs for the
579
+ // account now signed in, so the fresh mint never strands it live. Best
580
+ // effort: a dead key answers 401 and is forgotten all the same.
581
+ const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
525
582
  if (ctx.profileName && hub.name) {
526
583
  const profile = loadConfig(io.env).profiles[ctx.profileName];
527
- if (cachedForAnotherAccount(profile, hub.name, hub.profile?.email)) {
528
- const stale = profile?.connectKeys?.[hub.name];
529
- if (stale) {
530
- io.err.write(
531
- `The ${identity.displayName} key cached for ${stale.email ?? 'another Antasphere login'} is retired: ` +
532
- `the Antasphere login is now ${hub.profile?.email ?? 'another account'}.\n`
533
- );
534
- const auth = new CliAuthClient({
535
- baseUrl: profileUrl(profile),
536
- ...(io.fetch ? { fetch: io.fetch } : {})
537
- });
538
- await auth.revoke(stale.apiKey).catch(() => undefined);
539
- removeConnectKey(io.env, ctx.profileName, hub.name);
540
- }
584
+ const stale = profile?.connectKeys?.[hub.name];
585
+ if (stale && !cachedConnectKey(profile, hub.name, hub.profile?.email)) {
586
+ // A blank address is no address: it never reaches a sentence.
587
+ const hubEmail = firstSet(hub.profile?.email);
588
+ io.err.write(
589
+ `The ${identity.displayName} key cached for ${firstSet(stale.email) ?? 'an unknown login'} is retired: ` +
590
+ (hubEmail
591
+ ? `the Antasphere login is now ${hubEmail}.\n`
592
+ : 'the Antasphere login carries no address to match it with.\n')
593
+ );
594
+ const auth = new CliAuthClient({
595
+ baseUrl: profileUrl(profile),
596
+ ...(io.fetch ? { fetch: io.fetch } : {})
597
+ });
598
+ await auth.revoke(stale.apiKey).catch(() => undefined);
599
+ removeConnectKey(io.env, ctx.profileName, hub.name);
541
600
  }
542
601
  }
543
- const outcome = await connectOnDemand(connectOptions(ctx));
602
+ const outcome = await connect(ctx);
544
603
  if (outcome.outcome === 'not_cloud') {
545
604
  // Self-hosted / unreachable: the classic error, byte-identical.
546
605
  throw new CliUsageError(NO_KEY_MESSAGE);
@@ -566,7 +625,8 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
566
625
  explainWorkspaceRefusal,
567
626
  workspaceNotFoundHint,
568
627
  profileUrl,
569
- connectOptions
628
+ connectOptions,
629
+ connect
570
630
  };
571
631
  }
572
632
 
package/src/identity.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import type { ToolIdentity } from '@antasphere/chassis-contract';
2
2
 
3
+ /**
4
+ * The Antasphere hub every tool signs in through, when nothing names another
5
+ * (`ANTASPHERE_URL`, a hub profile's own `baseUrl`): the instance of the
6
+ * implicit `cloud` hub profile, and the cloud URL the hub profile is selected
7
+ * with (cli-core `selectHubProfile`, `hubCloudUrl`), so that a hub profile
8
+ * with no `baseUrl` of its own is at this hub, as the account CLI reads it.
9
+ */
10
+ export const DEFAULT_HUB_URL = 'https://account.antasphere.com';
11
+
3
12
  /**
4
13
  * What a tool is called, in every place the generic CLI spells it. ONE object,
5
14
  * passed by the tool to `defineCli` (index.ts): the chassis holds no product