@wirecat/cli-core 0.346.0 → 0.347.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,27 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.347.0 — 18.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`loginQr` shows a login QR code the same way in every messenger CLI**: drawn on the diagnostic stream, which
15
+ `--quiet` does not hide, or written to the `--qr-file` PNG with `writeSecurely` (a temporary file created with
16
+ mode `0600`, renamed over the path, never chmodded afterwards). On a terminal too narrow for the code it draws
17
+ nothing and says to widen the terminal or pass `--qr-file`; it opens nothing. `close()` removes the image it
18
+ wrote, never a file it did not. `qrNeedsTerminal(command)` is the shared refusal when nobody could scan, and
19
+ `atTerminal(input)` the shared test. `parity.json` plans `--qr-file` on `session start` and `setup` for max, beside tg and wa.
20
+
21
+ ### Fixed
22
+
23
+ - **A list that takes a query prints its help for `--help`.** Since 0.343.0, `chats list --help`, `tasks list --help`,
24
+ `contacts list --help` and every other list with `[query...]` refused `--help` as a mistyped option (exit 2), because
25
+ commander looks for it only after the query check had run. A mistyped `--limt` is still refused.
26
+ - **`help` and `guide` need no permission key**: `keyForCommand` returned nothing for them, so a tool that maps every
27
+ command to a key (max) crashed on `help options`. They are housekeeping now, like `commands` and `skill`.
28
+ - **`parity.json` has a `guide` row**, planned for max, tg and wa, so `cli-core-parity` stops failing a tool that
29
+ mounts it with "guide: not in the manifest".
30
+
10
31
  ## 0.346.0 — 18.10.2026
11
32
 
12
33
  ### Added
package/dist/index.d.ts CHANGED
@@ -22,5 +22,6 @@ export { defaultRules, ModerationRules, moderationPathFor } from "./moderation/r
22
22
  export { type OutputOptions, resolveOutput } from "./output.js";
23
23
  export { pollLines, type RenderOptions, renderMessage, renderMessages } from "./render/messages.js";
24
24
  export { isId, type PeopleLookup, pickChat, pickPerson } from "./resolve.js";
25
+ export { atTerminal, type LoginQr, type LoginQrOptions, loginQr, qrNeedsTerminal, } from "./terminal/login-qr.js";
25
26
  export { readSecret, type SecretInput } from "./terminal/prompt.js";
26
27
  export { qrPng, terminalQr } from "./terminal/qr.js";
package/dist/index.js CHANGED
@@ -15,5 +15,6 @@ export { defaultRules, ModerationRules, moderationPathFor } from "./moderation/r
15
15
  export { resolveOutput } from "./output.js";
16
16
  export { pollLines, renderMessage, renderMessages } from "./render/messages.js";
17
17
  export { isId, pickChat, pickPerson } from "./resolve.js";
18
+ export { atTerminal, loginQr, qrNeedsTerminal, } from "./terminal/login-qr.js";
18
19
  export { readSecret } from "./terminal/prompt.js";
19
20
  export { qrPng, terminalQr } from "./terminal/qr.js";
@@ -8,7 +8,11 @@ export const queryWordsAfterOptions = (command) => {
8
8
  command.parseOptions = (args) => {
9
9
  const parsed = parse(args);
10
10
  const end = parsed.unknown.indexOf("--");
11
- const typo = parsed.unknown.slice(0, end < 0 ? undefined : end).find((word) => word.startsWith("--"));
11
+ // Commander looks for --help in what parseOptions leaves unknown, after it returns.
12
+ const help = command._getHelpOption();
13
+ const typo = parsed.unknown
14
+ .slice(0, end < 0 ? undefined : end)
15
+ .find((word) => word.startsWith("--") && !help?.is(word));
12
16
  if (typo !== undefined) {
13
17
  command.allowUnknownOption(false);
14
18
  command.unknownOption(typo);
@@ -341,6 +341,8 @@ export const WRITE_KEYS = [
341
341
  ];
342
342
  /** Commands that look after the tool and its files and never show a message: no level stops them. */
343
343
  const HOUSEKEEPING = new Set([
344
+ "help",
345
+ "guide",
344
346
  "config",
345
347
  "session",
346
348
  "doctor",
@@ -0,0 +1,27 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ /** Whether a person can scan a code drawn here and answer what the login asks next. */
3
+ export declare const atTerminal: (input: NodeJS.ReadableStream & {
4
+ isTTY?: boolean;
5
+ }) => boolean;
6
+ /** The one refusal of a QR login that nobody could scan. */
7
+ export declare const qrNeedsTerminal: (command: string) => CliError;
8
+ export interface LoginQrOptions {
9
+ /** `--qr-file`: the PNG to write instead of drawing the code. */
10
+ qrFile?: string;
11
+ /** Where the code and the narrow-terminal hint go; `--quiet` must not hide either. */
12
+ diagnostic: (text: string) => void;
13
+ /** Where the file's location is said. */
14
+ note: (text: string) => void;
15
+ /** How far the code sits right, to line up under a setup step. */
16
+ indent?: number;
17
+ /** Read on every code, so a widened terminal draws the next one. Unknown draws the code. */
18
+ columns?: () => number | undefined;
19
+ }
20
+ export interface LoginQr {
21
+ readonly path: string | undefined;
22
+ /** Never throws for a code that does not fit: the messengers call it from inside their own event loops. */
23
+ show: (link: string) => void;
24
+ /** The image logs the account in while it is valid; call this in `finally`. */
25
+ close: () => void;
26
+ }
27
+ export declare const loginQr: ({ qrFile, diagnostic, note, indent: pad, columns, }: LoginQrOptions) => LoginQr;
@@ -0,0 +1,44 @@
1
+ import { rmSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { CliError, indent, writeSecurely } from "@wirecat/cli-base";
4
+ import { inputPolicy } from "../cli/input-policy.js";
5
+ import { qrPng, terminalQr } from "./qr.js";
6
+ const MARGIN = 2;
7
+ /** Whether a person can scan a code drawn here and answer what the login asks next. */
8
+ export const atTerminal = (input) => input.isTTY === true && !inputPolicy(input).noInput;
9
+ /** The one refusal of a QR login that nobody could scan. */
10
+ export const qrNeedsTerminal = (command) => new CliError("validation_error", `${command} shows a QR code to scan — run it in a terminal, or pass --qr-file <png>`, {
11
+ param: "--qr-file",
12
+ reason: "input.required",
13
+ });
14
+ export const loginQr = ({ qrFile, diagnostic, note, indent: pad = 0, columns = () => process.stderr.columns, }) => {
15
+ const path = qrFile === undefined ? undefined : resolve(qrFile);
16
+ // Only what this login wrote is removed: a refused or never-reached write leaves the owner's file alone.
17
+ let written = false;
18
+ return {
19
+ path,
20
+ show: (link) => {
21
+ if (path) {
22
+ writeSecurely(path, qrPng(link), 0o600);
23
+ written = true;
24
+ note(`the QR code is in ${path} — it is replaced when the code renews`);
25
+ return;
26
+ }
27
+ const drawn = terminalQr(link);
28
+ const need = drawn.width + pad + MARGIN;
29
+ const width = columns();
30
+ if (width !== undefined && need > width) {
31
+ diagnostic(`the terminal is ${width} columns wide and the QR code needs ${need} — widen it, ` +
32
+ "or run again with --qr-file <png>");
33
+ return;
34
+ }
35
+ diagnostic(`\n${indent(drawn.text, pad + MARGIN)}`);
36
+ },
37
+ close: () => {
38
+ if (!path || !written)
39
+ return;
40
+ written = false;
41
+ rmSync(path, { force: true });
42
+ },
43
+ };
44
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.346.0",
3
+ "version": "0.347.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",
package/parity.json CHANGED
@@ -2847,6 +2847,14 @@
2847
2847
  "wa": "wa backlog: mount the shared flood command"
2848
2848
  }
2849
2849
  },
2850
+ "guide": {
2851
+ "in": [],
2852
+ "planned": {
2853
+ "max": "help-navigation pass: max-cli#737 mounts guide with scenarios",
2854
+ "tg": "help-navigation pass: mount guide with scenarios",
2855
+ "wa": "help-navigation pass: mount guide with scenarios"
2856
+ }
2857
+ },
2850
2858
  "images": {
2851
2859
  "in": "all"
2852
2860
  },
@@ -4047,7 +4055,9 @@
4047
4055
  },
4048
4056
  "--qr-file": {
4049
4057
  "in": ["tg", "wa"],
4050
- "reason": "ruled tg-only with the app registration it belongs to"
4058
+ "planned": {
4059
+ "max": "max-cli#736, on the cli-core release that exports loginQr"
4060
+ }
4051
4061
  },
4052
4062
  "--sms": {
4053
4063
  "in": ["tg"],
@@ -4066,7 +4076,9 @@
4066
4076
  "--method": "all",
4067
4077
  "--qr-file": {
4068
4078
  "in": ["tg", "wa"],
4069
- "reason": "Telegram can expose its temporary QR login token as a PNG"
4079
+ "planned": {
4080
+ "max": "max-cli#736, on the cli-core release that exports loginQr"
4081
+ }
4070
4082
  }
4071
4083
  }
4072
4084
  },