@wirecat/cli-core 0.425.0 → 0.426.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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.426.0 — 25.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`profileFree(command)` and `takesProfile(command)` from `/cli`: a CLI marks the commands that read no profile, and help asks its own command tree.** A marked command, and every command under it, gets no `personal` in its help example and no `<profile>` in its usage line: `drive doctor`, `drive api list`, not `drive personal doctor`. `PROFILE_FREE` (`commands`, `complete`, `guide`, `help`, `skill`, `upgrade`) still counts for every CLI. drive and zm mark their own housekeeping commands to use it.
15
+ - **`storeChangeRefusal(app)` and `previewFlag(program)` from `/cli`.**
16
+
17
+ ### Changed — may break callers
18
+
19
+ - **Help and examples that cli-core writes no longer speak of messages to a CLI that is not a messenger, or show a command that does something unasked.** The first screen's "Works with" says zm and drive are "searchable with your other WireCat data", in every CLI (tg, max and wa included). `guide`'s example names the CLI's first scenario (`tg guide catch-up --json`) in place of `<name>`, and is left out when the CLI has none. `models download` (the messenger `models` command) says its size and examples the default text model only, not `parakeet-v3`. `meetings scheduled` and `meetings prepare` example `--dry-run` where the program has it. `commands`' argument example is `commands info` for a CLI that is not a messenger. The `<PREFIX>_READ_ONLY` refusal of a store change says "change the local store" for such a CLI; tg, max and wa keep "change the message store".
20
+ - **"Availability: available for this profile" is shown only where a profile is in play.** A CLI whose first screen has `profile: false` (memo), and a command that reads no profile, show the line only when the command is unavailable, with its reason.
21
+ - **`createProgram` puts no `<profile>` in the usage line of a CLI whose first screen has `profile: false`**, nor of a `profileFree` command.
22
+ - **A guide scenario step may start with a profile word** (`tg personal chats list`), so scenarios can name the profile as the command line requires. `checkStep` skips a first word that is not a command and is a usable profile name; its "steps leave out the profile" refusal is gone.
23
+
10
24
  ## 0.425.0 — 25.10.2026
11
25
 
12
26
  ### Added
@@ -28,7 +28,7 @@ export const commandsCommand = (app) => {
28
28
  reason: "output.one-object",
29
29
  });
30
30
  })
31
- .argument("[path...]", "one command path, for example: search messages; inspect other groups in separate calls")
31
+ .argument("[path...]", `one command path, for example: ${app.messenger === false ? "commands info" : "search messages"}; inspect other groups in separate calls`)
32
32
  .action(function (path) {
33
33
  const root = rootOf(this);
34
34
  const output = outputFor(this);
@@ -48,6 +48,10 @@ export declare const changesStore: <T extends Command>(command: T) => T;
48
48
  */
49
49
  export declare const isStoreChange: (command: Command) => boolean;
50
50
  export declare const STORE_CHANGE_REFUSAL = "change the message store";
51
+ /** ` --dry-run` where the program has it, so an example of a command that calls a provider calls nothing. */
52
+ export declare const previewFlag: (program: Command) => string;
53
+ /** What a read-only refusal says the command would do: the store holds more than messages outside a messenger. */
54
+ export declare const storeChangeRefusal: (app: Pick<AppIdentity, "messenger">) => string;
51
55
  /** Under `<PREFIX>_READ_ONLY`, refuses a change to the store — for a caller outside `run()`, an MCP tool. */
52
56
  export declare const refuseStoreChange: (app: AppIdentity, env: NodeJS.ProcessEnv) => void;
53
57
  /**
@@ -79,11 +79,15 @@ export const isStoreChange = (command) => {
79
79
  !changesAuthority(command));
80
80
  };
81
81
  export const STORE_CHANGE_REFUSAL = "change the message store";
82
+ /** ` --dry-run` where the program has it, so an example of a command that calls a provider calls nothing. */
83
+ export const previewFlag = (program) => program.options.some((option) => option.long === "--dry-run") ? " --dry-run" : "";
84
+ /** What a read-only refusal says the command would do: the store holds more than messages outside a messenger. */
85
+ export const storeChangeRefusal = (app) => app.messenger === false ? "change the local store" : STORE_CHANGE_REFUSAL;
82
86
  /** Under `<PREFIX>_READ_ONLY`, refuses a change to the store — for a caller outside `run()`, an MCP tool. */
83
87
  export const refuseStoreChange = (app, env) => {
84
88
  const lock = readOnlyLockOf(app, env);
85
89
  if (lock)
86
- throw readOnlyRefusal(lock, STORE_CHANGE_REFUSAL);
90
+ throw readOnlyRefusal(lock, storeChangeRefusal(app));
87
91
  };
88
92
  const pathOf = (command) => {
89
93
  const words = [];
@@ -49,7 +49,7 @@ export declare const loadScenarios: (directory: string) => Promise<{
49
49
  }>;
50
50
  /**
51
51
  * `<tool> guide` lists the tool's scenarios and the ones it works with; `<tool> guide <name>` prints one: the goal,
52
- * each step as a command and why, and what to look at. The files are read when the command runs, never when the
52
+ * each step as a command and why, and what to look at. The files are parsed when the command runs, never when the
53
53
  * program is built. `checkGuide` validates them in the tool's CI.
54
54
  */
55
55
  export declare const guideCommand: (app: Pick<AppIdentity, "command">, options: GuideOptions) => Command;
package/dist/cli/guide.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  import { environmentOf, outputFor } from "./context.js";
7
7
  import { NO_EFFECTS } from "./effects.js";
8
8
  import { itemOf, listOf, renderAnswer } from "./envelope.js";
9
- import { PROFILE_FREE } from "./profile.js";
9
+ import { commandWords, PROFILE_FREE, usableProfileName } from "./profile.js";
10
10
  /** memo's cross-tool scenarios: the one source of the titles every tool lists, and memo's files must carry. */
11
11
  export const MEMO_SCENARIOS = {
12
12
  personBrief: { tool: "memo", name: "person-brief", title: "One person across every tool" },
@@ -79,6 +79,19 @@ export const loadScenarios = async (directory) => {
79
79
  problems: parsed.flatMap(({ problems }) => problems),
80
80
  };
81
81
  };
82
+ /** Only the file names, for the example: building the program parses no scenario. */
83
+ const firstScenarioName = (directory) => {
84
+ try {
85
+ return readdirSync(directory)
86
+ .filter((file) => file.endsWith(".md") && file.toLowerCase() !== "readme.md")
87
+ .map((file) => basename(file, ".md"))
88
+ .filter((name) => NAME.test(name))
89
+ .sort()[0];
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ };
82
95
  const guides = new WeakMap();
83
96
  const rows = (pairs) => {
84
97
  const width = pairs.reduce((most, [left]) => Math.max(most, left.length), 0) + 2;
@@ -110,13 +123,14 @@ const scenarioText = (cli, scenario, profile) => [
110
123
  ].join("\n");
111
124
  /**
112
125
  * `<tool> guide` lists the tool's scenarios and the ones it works with; `<tool> guide <name>` prints one: the goal,
113
- * each step as a command and why, and what to look at. The files are read when the command runs, never when the
126
+ * each step as a command and why, and what to look at. The files are parsed when the command runs, never when the
114
127
  * program is built. `checkGuide` validates them in the tool's CI.
115
128
  */
116
129
  export const guideCommand = (app, options) => {
130
+ const first = firstScenarioName(options.directory);
117
131
  const command = annotate(new Command("guide"), {
118
132
  effects: NO_EFFECTS,
119
- examples: [`${app.command} guide`, `${app.command} guide <name> --json`],
133
+ examples: [`${app.command} guide`, ...(first ? [`${app.command} guide ${first} --json`] : [])],
120
134
  })
121
135
  .description("scenarios: the commands for a common task, in order, and why each")
122
136
  .argument("[name]", "one scenario; without it, the list")
@@ -242,6 +256,18 @@ const ownerOf = (chain, flag) => {
242
256
  }
243
257
  return undefined;
244
258
  };
259
+ const isCommandWord = (program, word) => word !== undefined && commandWords(program).has(word);
260
+ const isProfileWord = (word) => {
261
+ if (word === undefined || word.startsWith("-"))
262
+ return false;
263
+ try {
264
+ usableProfileName(word);
265
+ return true;
266
+ }
267
+ catch {
268
+ return false;
269
+ }
270
+ };
245
271
  /** What is wrong with one step on this program: the path, each option and its value, the arguments. Never runs it. */
246
272
  export const checkStep = (program, run) => {
247
273
  const [tool, ...rest] = words(run);
@@ -249,7 +275,8 @@ export const checkStep = (program, run) => {
249
275
  return [`starts with "${tool ?? ""}", not ${program.name()}`];
250
276
  const chain = [program];
251
277
  let at = program;
252
- let index = 0;
278
+ // The first word may be the profile, as the command line takes it: `tg personal chats list`.
279
+ let index = isCommandWord(program, rest[0]) || !isProfileWord(rest[0]) ? 0 : 1;
253
280
  for (; index < rest.length; index++) {
254
281
  const next = at.commands.find((one) => one.name() === rest[index] || one.aliases().includes(rest[index] ?? ""));
255
282
  if (!next)
@@ -257,8 +284,9 @@ export const checkStep = (program, run) => {
257
284
  chain.unshift(next);
258
285
  at = next;
259
286
  }
287
+ // A mistyped command reads like a profile word; when nothing follows that is a command, name the first word.
260
288
  if (at === program)
261
- return [`no command "${rest[0] ?? ""}" — steps leave out the profile`];
289
+ return [`no command "${rest[0] ?? ""}"`];
262
290
  const help = at._getHelpOption();
263
291
  const beforeDashes = rest.slice(index, rest.includes("--") ? rest.indexOf("--") : undefined);
264
292
  if (help && beforeDashes.some((word) => word === help.long || word === help.short))
@@ -2,7 +2,7 @@ import { metaOf } from "@wirecat/cli-base/commands";
2
2
  import { writes } from "@wirecat/cli-base/standards";
3
3
  import { commandPathOf, outputOf } from "../command-contract.js";
4
4
  import { environmentOf } from "../context.js";
5
- import { PROFILE_FREE } from "../profile.js";
5
+ import { takesProfile } from "../profile.js";
6
6
  import { commandAt } from "../steps.js";
7
7
  import { short } from "./commands-text.js";
8
8
  /** Argument names onto the kinds `domain/references.ts` names; a plural takes the same kind. */
@@ -177,7 +177,7 @@ export const EXAMPLE_PROFILE = "personal";
177
177
  const withProfile = (command, example) => {
178
178
  const root = rootOf(command);
179
179
  const top = commandPathOf(command)[0];
180
- if (!profiled.has(root) || top === undefined || PROFILE_FREE.has(top))
180
+ if (!profiled.has(root) || top === undefined || !takesProfile(command))
181
181
  return example;
182
182
  const prefix = `${root.name()} ${top}`;
183
183
  return example === prefix || example.startsWith(`${prefix} `)
@@ -203,11 +203,16 @@ export const commandHelpText = (command) => {
203
203
  ...metaOf(command),
204
204
  path: commandPathOf(command),
205
205
  });
206
- const access = availability
207
- ? rows("Availability:", [
208
- availability.available ? "available for this profile" : `unavailable — ${availability.reason}`,
209
- ])
210
- : [];
206
+ // "Available for this profile" says nothing where no profile is in play: a tool without profiles, or a command
207
+ // that reads none.
208
+ const profileMatters = profiled.has(rootOf(command)) && takesProfile(command);
209
+ const access = !availability
210
+ ? []
211
+ : availability.available
212
+ ? profileMatters
213
+ ? rows("Availability:", ["available for this profile"])
214
+ : []
215
+ : rows("Availability:", [`unavailable — ${availability.reason}`]);
211
216
  return [
212
217
  "",
213
218
  ...access,
@@ -25,12 +25,12 @@ export const WIRECAT_TOOLS = [
25
25
  {
26
26
  command: "zm",
27
27
  package: "@wirecat/zoom-cli",
28
- does: "Zoom meetings: transcripts and summaries, searchable with your messages",
28
+ does: "Zoom meetings: transcripts and summaries, searchable with your other WireCat data",
29
29
  },
30
30
  {
31
31
  command: "drive",
32
32
  package: "@wirecat/drive-cli",
33
- does: "Google Drive and Docs as local text, searchable with your messages",
33
+ does: "Google Drive and Docs as local text, searchable with your other WireCat data",
34
34
  },
35
35
  {
36
36
  command: "memo",
@@ -60,7 +60,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
60
60
  export { type Closeable, withDeadline } from "./deadline.js";
61
61
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
62
62
  export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
63
- export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
63
+ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
64
64
  export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
65
65
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
66
66
  export { withAbort } from "./execution.js";
@@ -100,7 +100,7 @@ export { type Saving, stored } from "./messenger/stored.js";
100
100
  export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
101
101
  export { window, withPaging } from "./paging.js";
102
102
  export { knownBeside, knownPermissionKeys, type PermissionKeyOf, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
103
- export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
103
+ export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
104
104
  export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
105
105
  export { fieldsOf, projectFields } from "./result-fields.js";
106
106
  export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, namedProfile, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
package/dist/cli/index.js CHANGED
@@ -53,7 +53,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
53
53
  export { withDeadline } from "./deadline.js";
54
54
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
55
55
  export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
56
- export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
56
+ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
57
57
  export { startCli } from "./entry.js";
58
58
  export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
59
59
  export { withAbort } from "./execution.js";
@@ -91,7 +91,7 @@ export { stored } from "./messenger/stored.js";
91
91
  export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
92
92
  export { window, withPaging } from "./paging.js";
93
93
  export { knownBeside, knownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
94
- export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
94
+ export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
95
95
  export { createProgram, run } from "./program.js";
96
96
  export { fieldsOf, projectFields } from "./result-fields.js";
97
97
  export { count, first, flag, fromFile, NONE_NAMED, namedProfile, plain, settingsFor, } from "./settings.js";
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
- import { REMOTE_READ } from "./effects.js";
3
+ import { previewFlag, REMOTE_READ } from "./effects.js";
4
4
  import { communicationOptions } from "./meeting-communications.js";
5
5
  import { readMeetingPreparation } from "./meeting-preparation-read.js";
6
6
  const integer = (value) => {
@@ -48,6 +48,6 @@ export const addMeetingPreparationCommand = (program, deps) => {
48
48
  origin: "handwritten",
49
49
  effects: REMOTE_READ,
50
50
  output: { entity: "meeting_preparation" },
51
- examples: [`${program.name()} meetings prepare 123 --json`],
51
+ examples: [`${program.name()} meetings prepare 123${previewFlag(program)} --json`],
52
52
  });
53
53
  };
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
- import { REMOTE_READ } from "./effects.js";
3
+ import { previewFlag, REMOTE_READ } from "./effects.js";
4
4
  import { readMeetingSeriesInventory } from "./meeting-series-inventory-read.js";
5
5
  const integer = (value) => {
6
6
  if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
@@ -32,6 +32,6 @@ export const addMeetingSeriesInventoryCommand = (program, deps) => {
32
32
  effects: REMOTE_READ,
33
33
  offline: false,
34
34
  output: { entity: "meeting_series_inventory" },
35
- examples: [`${program.name()} meetings scheduled --json`],
35
+ examples: [`${program.name()} meetings scheduled${previewFlag(program)} --json`],
36
36
  });
37
37
  };
@@ -165,9 +165,10 @@ export const modelsCommand = (messenger) => {
165
165
  reachesOtherService(annotate(models.command("download"), {
166
166
  effects: FETCH,
167
167
  output: { entity: "model" },
168
- examples: [`${tool} models download parakeet-v3`, `${tool} models download ${DEFAULT_TEXT_MODEL}`],
168
+ examples: [`${tool} models download ${DEFAULT_TEXT_MODEL}`],
169
169
  }))
170
- .description("download and verify one speech or embedding model")
170
+ .summary("download and verify one speech or embedding model")
171
+ .description("download and verify one speech or embedding model: tens to hundreds of MB, each size in models list")
171
172
  .argument("<model>", "a model id from models list")
172
173
  .option("--accept-terms", "accept the model's licence terms, for a model that has its own")
173
174
  .action(async function (id, options) {
@@ -4,6 +4,13 @@ export declare const asFirstWord: (profile: string) => string;
4
4
  export declare const usableProfileName: (profile: string) => string;
5
5
  /** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
6
6
  export declare const PROFILE_FREE: ReadonlySet<string>;
7
+ /**
8
+ * Marks a command, and every command under it, as one that reads no profile, so help shows it without one:
9
+ * `drive doctor`, not `drive personal doctor`. Which commands those are is the CLI's to say.
10
+ */
11
+ export declare const profileFree: <T extends Command>(command: T) => T;
12
+ /** Whether the command line puts a profile before this command: not for `PROFILE_FREE` or a `profileFree` one. */
13
+ export declare const takesProfile: (command: Command) => boolean;
7
14
  /**
8
15
  * Every word that means a command at the top level. Taken off the built program rather than
9
16
  * written out, because a list written out goes stale the day a command is added — and `help` is in
@@ -16,6 +16,25 @@ export const usableProfileName = (profile) => {
16
16
  };
17
17
  /** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
18
18
  export const PROFILE_FREE = new Set(["commands", "complete", "guide", "help", "skill", "upgrade"]);
19
+ const freeOfProfile = new WeakSet();
20
+ /**
21
+ * Marks a command, and every command under it, as one that reads no profile, so help shows it without one:
22
+ * `drive doctor`, not `drive personal doctor`. Which commands those are is the CLI's to say.
23
+ */
24
+ export const profileFree = (command) => {
25
+ freeOfProfile.add(command);
26
+ return command;
27
+ };
28
+ /** Whether the command line puts a profile before this command: not for `PROFILE_FREE` or a `profileFree` one. */
29
+ export const takesProfile = (command) => {
30
+ let top;
31
+ for (let at = command; at?.parent; at = at.parent) {
32
+ if (freeOfProfile.has(at))
33
+ return false;
34
+ top = at;
35
+ }
36
+ return top !== undefined && !PROFILE_FREE.has(top.name());
37
+ };
19
38
  /**
20
39
  * Every word that means a command at the top level. Taken off the built program rather than
21
40
  * written out, because a list written out goes stale the day a command is added — and `help` is in
@@ -18,7 +18,7 @@ import { commandAvailability } from "./command-availability.js";
18
18
  import { commandPathOf } from "./command-contract.js";
19
19
  import { outputFor, provide } from "./context.js";
20
20
  import { refuseWriteBeforeConnecting } from "./early-refusal.js";
21
- import { changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, STORE_CHANGE_REFUSAL, } from "./effects.js";
21
+ import { changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, storeChangeRefusal, } from "./effects.js";
22
22
  import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
23
23
  import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
24
24
  import { setupAtStart } from "./first-run.js";
@@ -27,7 +27,7 @@ import { showInputFields } from "./input-fields.js";
27
27
  import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
28
28
  import { checkJson, liftJson } from "./json-fields.js";
29
29
  import { PreviewComplete, preview } from "./preview.js";
30
- import { commandWords, liftProfile, PROFILE_FREE, sampleCommand, valuedOptions } from "./profile.js";
30
+ import { commandWords, liftProfile, sampleCommand, takesProfile, valuedOptions } from "./profile.js";
31
31
  import { absolutePaths, relayTarget } from "./relay-route.js";
32
32
  import { NoProfileHere, namedProfile, settingsFor, } from "./settings.js";
33
33
  import { assertSharedContracts } from "./shared-contracts.js";
@@ -74,14 +74,15 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
74
74
  assertSharedContracts(program);
75
75
  installHelp(program, { pitch: app.description, ...firstScreen });
76
76
  // Commander builds a usage line from the command tree, where the profile is not a word.
77
- for (const top of program.commands) {
78
- if (PROFILE_FREE.has(top.name()))
79
- continue;
80
- forEachCommand(top, (command) => command.configureHelp({
81
- ...command.configureHelp(),
82
- commandUsage: (shown) => `${app.command} <profile>${new Help().commandUsage(shown).slice(app.command.length)}`,
83
- }));
84
- }
77
+ if (firstScreen?.profile !== false)
78
+ for (const top of program.commands)
79
+ forEachCommand(top, (command) => command.configureHelp({
80
+ ...command.configureHelp(),
81
+ commandUsage: (shown) => {
82
+ const usage = new Help().commandUsage(shown);
83
+ return takesProfile(shown) ? `${app.command} <profile>${usage.slice(app.command.length)}` : usage;
84
+ },
85
+ }));
85
86
  // Depth-first: Commander does not pass `configureOutput` down to a command added with
86
87
  // `addCommand`, so `tg messages --help` would write to the real terminal.
87
88
  if (out || err) {
@@ -284,7 +285,7 @@ const runOnce = async (argv, definition, options) => {
284
285
  if (lock && isLogin(action) && !flags.dryRun)
285
286
  throw readOnlyRefusal(lock, "log in or out");
286
287
  if (lock && isStoreChange(action) && !flags.dryRun)
287
- throw readOnlyRefusal(lock, STORE_CHANGE_REFUSAL);
288
+ throw readOnlyRefusal(lock, storeChangeRefusal(definition.app));
288
289
  refuseWriteBeforeConnecting(action, {
289
290
  app: definition.app,
290
291
  env: commandEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.425.0",
3
+ "version": "0.426.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",