@wirecat/cli-core 0.369.0 → 0.370.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,21 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.370.0 — 20.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **`checkGuide` finds a scenario step a shell would change**, so a tool's guide test that passed may now fail. It
15
+ splits each step as a POSIX shell does and reports `>`, `<`, `|`, `&`, `;`, `` ` ``, `$(` and the globs `*`, `?`,
16
+ `[` left outside quotes (`` ` `` and `$(` also inside double quotes), and a quote never closed. `max review
17
+ date>1d` passed before, though a shell writes a file named `1d` and drops the filter. Quote the word:
18
+ `'date>1d'`. `shellProblems(run)` gives the same check for one step.
19
+
20
+ ### Added
21
+
22
+ - **`MEMO_SCENARIOS` holds the titles of memo's cross-tool scenarios** (`personBrief`, `decisionTrail`), so every
23
+ tool's "Works with" list takes them from one place instead of typing its own copy.
24
+
10
25
  ## 0.369.0 — 20.10.2026
11
26
 
12
27
  ### Fixed
@@ -18,6 +18,19 @@ export interface GuideReference {
18
18
  name: string;
19
19
  title: string;
20
20
  }
21
+ /** memo's cross-tool scenarios: the one source of the titles every tool lists, and memo's files must carry. */
22
+ export declare const MEMO_SCENARIOS: {
23
+ readonly personBrief: {
24
+ readonly tool: "memo";
25
+ readonly name: "person-brief";
26
+ readonly title: "One person across every tool";
27
+ };
28
+ readonly decisionTrail: {
29
+ readonly tool: "memo";
30
+ readonly name: "decision-trail";
31
+ readonly title: "What did we decide about X, across every source";
32
+ };
33
+ };
21
34
  export interface GuideOptions {
22
35
  /** Absolute path of the tool's scenario files: `fileURLToPath(new URL("../scenarios", import.meta.url))`. */
23
36
  directory: string;
@@ -40,6 +53,8 @@ export declare const loadScenarios: (directory: string) => Promise<{
40
53
  * program is built. `checkGuide` validates them in the tool's CI.
41
54
  */
42
55
  export declare const guideCommand: (app: Pick<AppIdentity, "command">, options: GuideOptions) => Command;
56
+ /** Characters in a step that a shell would act on: the person who copies it would run something else. */
57
+ export declare const shellProblems: (run: string) => string[];
43
58
  /** What is wrong with one step on this program: the path, each option and its value, the arguments. Never runs it. */
44
59
  export declare const checkStep: (program: Command, run: string) => string[];
45
60
  /**
package/dist/cli/guide.js CHANGED
@@ -7,6 +7,11 @@ import { environmentOf, outputFor } from "./context.js";
7
7
  import { NO_EFFECTS } from "./effects.js";
8
8
  import { itemOf, listOf, renderAnswer } from "./envelope.js";
9
9
  import { PROFILE_FREE } from "./profile.js";
10
+ /** memo's cross-tool scenarios: the one source of the titles every tool lists, and memo's files must carry. */
11
+ export const MEMO_SCENARIOS = {
12
+ personBrief: { tool: "memo", name: "person-brief", title: "One person across every tool" },
13
+ decisionTrail: { tool: "memo", name: "decision-trail", title: "What did we decide about X, across every source" },
14
+ };
10
15
  const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
11
16
  const FIELDS = new Set(["title", "goal", "steps", "look"]);
12
17
  const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -157,17 +162,49 @@ export const guideCommand = (app, options) => {
157
162
  guides.set(command, options);
158
163
  return command;
159
164
  };
160
- /** A step's words, split as a shell would on spaces, with "…" and '…' kept together. */
161
- const words = (line) => {
165
+ const OPERATORS = {
166
+ ">": "redirects output to a file",
167
+ "<": "reads input from a file",
168
+ "|": "pipes into another command",
169
+ "&": "runs the command in the background",
170
+ ";": "ends the command there",
171
+ "`": "runs it as a command",
172
+ "$(": "runs it as a command",
173
+ "*": "expands it to file names",
174
+ "?": "expands it to file names",
175
+ "[": "expands it to file names",
176
+ };
177
+ /**
178
+ * A step split into words as a POSIX shell would — '…' literal, "…" with \ escapes, \ outside quotes — and the
179
+ * characters a shell would act on instead of passing them to the tool. `` ` `` and `$(` still act inside "…".
180
+ */
181
+ const scan = (line) => {
162
182
  const found = [];
183
+ const live = new Set();
163
184
  let word;
164
185
  let quote;
165
- for (const char of line) {
166
- if (quote) {
167
- if (char === quote)
186
+ for (let index = 0; index < line.length; index++) {
187
+ const char = line[index];
188
+ const next = line[index + 1];
189
+ if (quote === "'") {
190
+ if (char === "'")
168
191
  quote = undefined;
169
192
  else
170
- word = (word ?? "") + char;
193
+ word += char;
194
+ continue;
195
+ }
196
+ if (char === "\\" && next !== undefined && (quote === undefined || '$`"\\'.includes(next))) {
197
+ word = (word ?? "") + next;
198
+ index++;
199
+ continue;
200
+ }
201
+ if (char === "`" || (char === "$" && next === "("))
202
+ live.add(char === "`" ? "`" : "$(");
203
+ if (quote === '"') {
204
+ if (char === '"')
205
+ quote = undefined;
206
+ else
207
+ word += char;
171
208
  }
172
209
  else if (char === '"' || char === "'") {
173
210
  quote = char;
@@ -178,12 +215,24 @@ const words = (line) => {
178
215
  found.push(word);
179
216
  word = undefined;
180
217
  }
181
- else
218
+ else {
219
+ if (OPERATORS[char])
220
+ live.add(char);
182
221
  word = (word ?? "") + char;
222
+ }
183
223
  }
184
224
  if (word !== undefined)
185
225
  found.push(word);
186
- return found;
226
+ return { words: found, live: [...live], open: quote !== undefined };
227
+ };
228
+ const words = (line) => scan(line).words;
229
+ /** Characters in a step that a shell would act on: the person who copies it would run something else. */
230
+ export const shellProblems = (run) => {
231
+ const { live, open } = scan(run);
232
+ return [
233
+ ...live.map((char) => `a shell acts on ${char}: it ${OPERATORS[char]}; put the word in single quotes`),
234
+ ...(open ? ["a quote is never closed"] : []),
235
+ ];
187
236
  };
188
237
  const silent = (command) => command.exitOverride().configureOutput({ writeOut: () => { }, writeErr: () => { }, outputError: () => { } });
189
238
  const ownerOf = (chain, flag) => {
@@ -295,7 +344,7 @@ export const checkGuide = async (build, { packageRoot } = {}) => {
295
344
  const { scenarios, problems } = await loadScenarios(options.directory);
296
345
  for (const scenario of scenarios)
297
346
  scenario.steps.forEach((step, index) => {
298
- for (const problem of checkStep(build(), step.run))
347
+ for (const problem of [...shellProblems(step.run), ...checkStep(build(), step.run)])
299
348
  problems.push(`${scenario.name}.md step ${index + 1} (${step.run}): ${problem}`);
300
349
  });
301
350
  for (const reference of options.worksWith ?? [])
@@ -56,7 +56,7 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
56
56
  export { changesAuthority, 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";
57
57
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
58
58
  export { withAbort } from "./execution.js";
59
- export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, type Scenario, } from "./guide.js";
59
+ export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, MEMO_SCENARIOS, type Scenario, shellProblems, } from "./guide.js";
60
60
  export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
61
61
  export { commandsText } from "./help/commands-text.js";
62
62
  export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
package/dist/cli/index.js CHANGED
@@ -51,7 +51,7 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
51
51
  export { changesAuthority, 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";
52
52
  export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
53
53
  export { withAbort } from "./execution.js";
54
- export { checkGuide, checkStep, guideCommand, loadScenarios, } from "./guide.js";
54
+ export { checkGuide, checkStep, guideCommand, loadScenarios, MEMO_SCENARIOS, shellProblems, } from "./guide.js";
55
55
  export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
56
56
  export { commandsText } from "./help/commands-text.js";
57
57
  export { firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.369.0",
3
+ "version": "0.370.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",