@wirecat/cli-core 0.345.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 +41 -0
- package/dist/cli/messenger/chat-answers.js +1 -1
- package/dist/domain/models.d.ts +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kit/contract.d.ts +2 -2
- package/dist/kit/contract.js +36 -0
- package/dist/kit/fake.d.ts +3 -2
- package/dist/kit/fake.js +12 -0
- package/dist/meetings/cli/query-options.js +5 -1
- package/dist/sends/permissions.js +2 -0
- package/dist/store/sqlite/chats.js +25 -4
- package/dist/terminal/login-qr.d.ts +27 -0
- package/dist/terminal/login-qr.js +44 -0
- package/package.json +1 -1
- package/parity.json +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ 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
|
+
|
|
31
|
+
## 0.346.0 — 18.10.2026
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **The contract kit checks `listen`** when the adapter has it: on a signal already aborted it resolves at once
|
|
36
|
+
and does not reject, and otherwise it keeps listening until the signal aborts, then resolves. `OPTIONAL_METHODS`
|
|
37
|
+
lists `listen`, and `fakeAdapter(seed, { listen: true })` has it. The cases cannot see whether `listen` connects.
|
|
38
|
+
|
|
39
|
+
### Changed — may break callers
|
|
40
|
+
|
|
41
|
+
- **Saving a chat the store already holds merges its `providerMetadata` key by key, and `parentChatId` keeps or
|
|
42
|
+
clears the same way.** A metadata key the new chat leaves out keeps its stored value; a key set to `null` is
|
|
43
|
+
removed, and metadata with no key left is stored as none. `parentChatId` left out keeps the stored parent, `null`
|
|
44
|
+
clears it (it could never be cleared before), and `Chat.parentChatId` is now `Id | null`. Before, the new
|
|
45
|
+
metadata replaced the stored one whole, so a push that left a key out erased it: a WhatsApp community lost
|
|
46
|
+
`community: true` on a push that did not say, and max-cli's login erased each dialog's `partnerId`. **Adapters
|
|
47
|
+
must send `null` for a key that is gone, or a flag that turned false; an absent key now means unknown.** An
|
|
48
|
+
adapter that leaves such a key out keeps the old value in the store, and a stale `username` still answers to
|
|
49
|
+
`@name` until the chat is saved with `username: null`. Chats the store already holds are not changed.
|
|
50
|
+
|
|
10
51
|
## 0.345.0 — 18.10.2026
|
|
11
52
|
|
|
12
53
|
### Added
|
|
@@ -30,7 +30,7 @@ export const chatAnswer = (account, chat) => defined({
|
|
|
30
30
|
muted: chat.muted,
|
|
31
31
|
archived: chat.archived,
|
|
32
32
|
membership_state: chat.membershipState,
|
|
33
|
-
parent_chat_id: chat.parentChatId,
|
|
33
|
+
parent_chat_id: chat.parentChatId ?? undefined,
|
|
34
34
|
scope: chat.scope,
|
|
35
35
|
provider_metadata: chat.providerMetadata,
|
|
36
36
|
description: chat.description,
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export type ChatKind = "dialog" | "group" | "channel" | "saved" | "unknown";
|
|
|
19
19
|
/** Where the owner stands in a chat. Kept apart from whether its messages are searchable: a chat left keeps them. */
|
|
20
20
|
export type MembershipState = "joined" | "left" | "public" | "imported" | "archived" | "external";
|
|
21
21
|
export interface Chat {
|
|
22
|
-
|
|
22
|
+
/** Saving a chat: absent keeps the stored parent, `null` clears it. */
|
|
23
|
+
parentChatId?: Id | null;
|
|
23
24
|
scope?: "personal" | "work";
|
|
24
25
|
id: Id;
|
|
25
26
|
title: string | null;
|
|
@@ -36,6 +37,10 @@ export interface Chat {
|
|
|
36
37
|
unreadMentions?: number;
|
|
37
38
|
/** Absent where the messenger does not say. */
|
|
38
39
|
membershipState?: MembershipState;
|
|
40
|
+
/**
|
|
41
|
+
* Saving a chat merges this into the stored one key by key: a key left out keeps its stored value, a key set to
|
|
42
|
+
* `null` is removed. So a key that is gone, or a flag that turned false, is sent as `null`.
|
|
43
|
+
*/
|
|
39
44
|
providerMetadata?: ProviderMetadata;
|
|
40
45
|
}
|
|
41
46
|
/** A group's switches; `null` where the messenger does not say, or has no such switch. */
|
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";
|
package/dist/kit/contract.d.ts
CHANGED
|
@@ -16,13 +16,13 @@ export interface ContractOptions {
|
|
|
16
16
|
ids?: IdMaker;
|
|
17
17
|
/** As the messenger's `fetching.orderBy`: whether `history`'s `before` is a message id or an ISO time. */
|
|
18
18
|
orderBy?: "id" | "time";
|
|
19
|
-
/** How long `watch` may take to say it listens, and to end once aborted. */
|
|
19
|
+
/** How long `watch` may take to say it listens, and `watch`, `listen` and `feed` to end once aborted. */
|
|
20
20
|
waitMs?: number;
|
|
21
21
|
/** As the messenger's `history`: a `"server"` messenger's adapter must have every `ServerReads` method. */
|
|
22
22
|
history?: "server" | "store";
|
|
23
23
|
}
|
|
24
24
|
/** Every method of the optional groups, which `capability` hands over or refuses. */
|
|
25
|
-
export declare const OPTIONAL_METHODS: readonly ["fetchCounters", "chats", "history", "around", "contact", "historyAfter", "historyBefore", "topics", "inspect", "edit", "forward", "delete", "pin", "unpin", "star", "react", "markRead", "markTopicRead", "poll", "vote", "closePoll", "createPoll", "watch", "feed", "download", "transcribe", "scheduled", "members", "admins", "chatEvents", "sessions", "lookup", "addressBook", "people", "createGroup", "join", "leave", "group", "updateGroup", "resetInviteLink", "addMembers", "removeMembers", "addAdmin", "removeAdmin", "folders", "createFolder", "updateFolder", "deleteFolder", "addContact", "removeContact", "block", "unblock", "renameContact", "importContacts", "updateProfile", "endOtherSessions"];
|
|
25
|
+
export declare const OPTIONAL_METHODS: readonly ["fetchCounters", "chats", "history", "around", "contact", "historyAfter", "historyBefore", "topics", "inspect", "edit", "forward", "delete", "pin", "unpin", "star", "react", "markRead", "markTopicRead", "poll", "vote", "closePoll", "createPoll", "watch", "listen", "feed", "download", "transcribe", "scheduled", "members", "admins", "chatEvents", "sessions", "lookup", "addressBook", "people", "createGroup", "join", "leave", "group", "updateGroup", "resetInviteLink", "addMembers", "removeMembers", "addAdmin", "removeAdmin", "folders", "createFolder", "updateFolder", "deleteFolder", "addContact", "removeContact", "block", "unblock", "renameContact", "importContacts", "updateProfile", "endOtherSessions"];
|
|
26
26
|
/**
|
|
27
27
|
* The port's promises as cases any test runner can run: each connects through `connect`, checks
|
|
28
28
|
* with `node:assert/strict` and closes the adapter. None needs a live service.
|
package/dist/kit/contract.js
CHANGED
|
@@ -28,6 +28,7 @@ export const OPTIONAL_METHODS = [
|
|
|
28
28
|
"closePoll",
|
|
29
29
|
"createPoll",
|
|
30
30
|
"watch",
|
|
31
|
+
"listen",
|
|
31
32
|
"feed",
|
|
32
33
|
"download",
|
|
33
34
|
"transcribe",
|
|
@@ -82,6 +83,17 @@ const within = async (work, failure, ms) => {
|
|
|
82
83
|
clearTimeout(timer);
|
|
83
84
|
}
|
|
84
85
|
};
|
|
86
|
+
const settledSoon = async (work) => {
|
|
87
|
+
let settled = false;
|
|
88
|
+
work.then(() => {
|
|
89
|
+
settled = true;
|
|
90
|
+
}, () => {
|
|
91
|
+
settled = true;
|
|
92
|
+
});
|
|
93
|
+
for (let turn = 0; turn < 5; turn += 1)
|
|
94
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
95
|
+
return settled;
|
|
96
|
+
};
|
|
85
97
|
const idsOf = (messages) => messages.map((message) => message.id);
|
|
86
98
|
/** Every `id`, `chatId` and `senderId` anywhere in `value`, so a number among them is caught. */
|
|
87
99
|
const everyId = (value, found = []) => {
|
|
@@ -394,6 +406,30 @@ export const contractCases = ({ connect, ids = wordIds, orderBy = "id", waitMs =
|
|
|
394
406
|
},
|
|
395
407
|
"`connect` opens a connection that listens, as `{ listen: true }` does",
|
|
396
408
|
],
|
|
409
|
+
[
|
|
410
|
+
"listen on a signal already aborted resolves at once and does not reject",
|
|
411
|
+
async (adapter) => {
|
|
412
|
+
const skipped = lacking(adapter, "listen");
|
|
413
|
+
if (skipped)
|
|
414
|
+
return skipped;
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
controller.abort();
|
|
417
|
+
await within(Promise.resolve(adapter.listen?.(() => { }, controller.signal)), "listen did not resolve on a signal already aborted", waitMs);
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
[
|
|
421
|
+
"listen keeps listening until the signal aborts, then resolves",
|
|
422
|
+
async (adapter) => {
|
|
423
|
+
const skipped = lacking(adapter, "listen");
|
|
424
|
+
if (skipped)
|
|
425
|
+
return skipped;
|
|
426
|
+
const controller = new AbortController();
|
|
427
|
+
const listening = Promise.resolve(adapter.listen?.(() => { }, controller.signal));
|
|
428
|
+
assert.equal(await settledSoon(listening), false, "listen ended before the signal aborted");
|
|
429
|
+
controller.abort();
|
|
430
|
+
await within(listening, "listen did not end when the signal aborted", waitMs);
|
|
431
|
+
},
|
|
432
|
+
],
|
|
397
433
|
[
|
|
398
434
|
"feed pushes only the seed's chats and messages, with ids as strings, and ends when the signal aborts",
|
|
399
435
|
async (adapter, seed) => {
|
package/dist/kit/fake.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AccountEditing, AccountTools, ChatFolders, ChatReading, ContactBook, GroupAdmin, GroupModeration, LiveUpdates, MessageEditing, MessageMedia, MessagePins, MessagePolls, MessageReactions, MessageStars, MessengerCore, PushedHistory, ReadState, ScheduledMessages, ServerReads, TopicReadState } from "../ports/messenger.js";
|
|
1
|
+
import type { AccountEditing, AccountTools, ChatFolders, ChatReading, ContactBook, GroupAdmin, GroupModeration, LiveUpdates, MessageEditing, MessageMedia, MessagePins, MessagePolls, MessageReactions, MessageStars, MessengerCore, PassiveUpdates, PushedHistory, ReadState, ScheduledMessages, ServerReads, TopicReadState } from "../ports/messenger.js";
|
|
2
2
|
import { type Seed } from "./seed.js";
|
|
3
3
|
/** Every group of the port, over memory. */
|
|
4
|
-
export type FakeAdapter = MessengerCore & ServerReads & ChatReading & MessageEditing & MessagePins & MessageStars & MessageReactions & ReadState & TopicReadState & MessagePolls & LiveUpdates & MessageMedia & ScheduledMessages & GroupModeration & AccountTools & GroupAdmin & ChatFolders & ContactBook & AccountEditing & Partial<PushedHistory>;
|
|
4
|
+
export type FakeAdapter = MessengerCore & ServerReads & ChatReading & MessageEditing & MessagePins & MessageStars & MessageReactions & ReadState & TopicReadState & MessagePolls & LiveUpdates & MessageMedia & ScheduledMessages & GroupModeration & AccountTools & GroupAdmin & ChatFolders & ContactBook & AccountEditing & Partial<PushedHistory> & Partial<PassiveUpdates>;
|
|
5
5
|
export declare const fakeAdapter: (seed?: Seed, options?: {
|
|
6
6
|
feed?: boolean;
|
|
7
|
+
listen?: boolean;
|
|
7
8
|
}) => FakeAdapter;
|
package/dist/kit/fake.js
CHANGED
|
@@ -28,6 +28,7 @@ const missing = (what) => new CliError("not_found", `the fake adapter has no ${w
|
|
|
28
28
|
*
|
|
29
29
|
* With `{ feed: true }` it also pushes its history, as a messenger with `history: "store"` does:
|
|
30
30
|
* the chats and people in one batch, then each chat's messages in a batch of their own.
|
|
31
|
+
* With `{ listen: true }` it also has `listen`, which hands on what `watch` does.
|
|
31
32
|
*/
|
|
32
33
|
const withRules = (folder, { emoji, include, skip, exclude, pin }) => {
|
|
33
34
|
if (emoji !== undefined)
|
|
@@ -311,6 +312,17 @@ export const fakeAdapter = (seed = contractSeed(), options = {}) => {
|
|
|
311
312
|
await new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
312
313
|
listeners.delete(onEvent);
|
|
313
314
|
},
|
|
315
|
+
...(options.listen
|
|
316
|
+
? {
|
|
317
|
+
listen: async (onEvent, signal) => {
|
|
318
|
+
if (signal.aborted)
|
|
319
|
+
return;
|
|
320
|
+
listeners.add(onEvent);
|
|
321
|
+
await new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
322
|
+
listeners.delete(onEvent);
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
: {}),
|
|
314
326
|
...(options.feed
|
|
315
327
|
? {
|
|
316
328
|
feed: async (onBatch, signal) => {
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -4,7 +4,7 @@ import { identityPk } from "./identities.js";
|
|
|
4
4
|
import { bound } from "./query-filter.js";
|
|
5
5
|
import { chatEventPeople, chatEvents, chatMembers, chats, chunks, fetchLeases, identities, involvements, memberCounts, memberObservationMembers, memberObservations, memberStays, messageRevisions, messages, messageTranscripts, pollVotes, syncRanges, } from "./schema.js";
|
|
6
6
|
import { ensureTag } from "./tags.js";
|
|
7
|
-
import {
|
|
7
|
+
import { parsed, present, toIso, toMs } from "./values.js";
|
|
8
8
|
/** The chat's own id, when the store key is a chat of this account. */
|
|
9
9
|
export const chatOf = ({ orm }, accountKey, chatKey) => orm
|
|
10
10
|
.select({ id: chats.externalId })
|
|
@@ -31,7 +31,12 @@ export const chatPkFor = (context, accountKey, chatId) => findChatPk(context, ac
|
|
|
31
31
|
.get()?.pk);
|
|
32
32
|
export const upsertChat = (context, accountKey, chat) => {
|
|
33
33
|
const { orm, now } = context;
|
|
34
|
-
const parent = chat.parentChatId
|
|
34
|
+
const parent = chat.parentChatId == null ? null : chatPkFor(context, accountKey, chat.parentChatId);
|
|
35
|
+
const stored = orm
|
|
36
|
+
.select({ metadata: chats.metadata })
|
|
37
|
+
.from(chats)
|
|
38
|
+
.where(and(eq(chats.accountId, accountKey), eq(chats.externalId, chat.id)))
|
|
39
|
+
.get();
|
|
35
40
|
orm
|
|
36
41
|
.insert(chats)
|
|
37
42
|
.values({
|
|
@@ -44,7 +49,7 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
44
49
|
unreadCount: chat.unreadCount,
|
|
45
50
|
lastMessageAt: toMs(chat.lastMessageAt),
|
|
46
51
|
participantsCount: chat.participantsCount,
|
|
47
|
-
metadata:
|
|
52
|
+
metadata: mergedMetadata(stored?.metadata ?? null, chat.providerMetadata),
|
|
48
53
|
membershipState: chat.membershipState ?? null,
|
|
49
54
|
createdAt: now(),
|
|
50
55
|
updatedAt: now(),
|
|
@@ -53,7 +58,7 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
53
58
|
target: [chats.accountId, chats.externalId],
|
|
54
59
|
set: {
|
|
55
60
|
kind: sql `excluded.kind`,
|
|
56
|
-
parentChatId: sql `
|
|
61
|
+
...(chat.parentChatId === undefined ? {} : { parentChatId: sql `excluded.parent_chat_id` }),
|
|
57
62
|
scope: sql `coalesce(excluded.scope, ${chats.scope})`,
|
|
58
63
|
title: sql `excluded.title`,
|
|
59
64
|
unreadCount: sql `excluded.unread_count`,
|
|
@@ -68,6 +73,22 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
68
73
|
})
|
|
69
74
|
.run();
|
|
70
75
|
};
|
|
76
|
+
const mergedMetadata = (stored, incoming) => {
|
|
77
|
+
if (incoming === undefined)
|
|
78
|
+
return stored;
|
|
79
|
+
const merged = Object.entries({ ...metadataObject(stored), ...incoming }).filter(([, value]) => value !== null);
|
|
80
|
+
return merged.length === 0 ? null : JSON.stringify(Object.fromEntries(merged));
|
|
81
|
+
};
|
|
82
|
+
// Stored metadata that is not an object has no keys to keep.
|
|
83
|
+
const metadataObject = (stored) => {
|
|
84
|
+
try {
|
|
85
|
+
const value = stored === null ? null : JSON.parse(stored);
|
|
86
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
71
92
|
/** Replaces who is in the chat, whole. */
|
|
72
93
|
export const writeMembers = (context, key, accountKey, chatId, memberIds) => {
|
|
73
94
|
const chatKey = chatPkFor(context, accountKey, chatId);
|
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
4079
|
+
"planned": {
|
|
4080
|
+
"max": "max-cli#736, on the cli-core release that exports loginQr"
|
|
4081
|
+
}
|
|
4070
4082
|
}
|
|
4071
4083
|
}
|
|
4072
4084
|
},
|