@wirecat/cli-core 0.396.0 → 0.398.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 +74 -1
- package/README.md +13 -9
- package/dist/background/daily-archive-timer.d.ts +40 -0
- package/dist/background/daily-archive-timer.js +181 -0
- package/dist/background/index.d.ts +2 -0
- package/dist/background/index.js +1 -0
- package/dist/cli/bot/admins.js +2 -2
- package/dist/cli/bot/command.js +2 -2
- package/dist/cli/bot/setup.js +3 -3
- package/dist/cli/commands-command.js +1 -1
- package/dist/cli/config-command.js +1 -1
- package/dist/cli/guide.js +0 -1
- package/dist/cli/help/command-help.d.ts +9 -2
- package/dist/cli/help/command-help.js +22 -4
- package/dist/cli/help/first-screen.js +3 -3
- package/dist/cli/messenger/account-command.js +3 -1
- package/dist/cli/messenger/account-sessions-command.js +3 -1
- package/dist/cli/messenger/admin-commands.js +1 -0
- package/dist/cli/messenger/admin-contacts-command.js +9 -3
- package/dist/cli/messenger/archive-commands.js +5 -3
- package/dist/cli/messenger/attachments-command.js +17 -7
- package/dist/cli/messenger/backfill-command.js +7 -3
- package/dist/cli/messenger/catch-up-answers.d.ts +2 -0
- package/dist/cli/messenger/catch-up-answers.js +5 -1
- package/dist/cli/messenger/chats-command.js +3 -1
- package/dist/cli/messenger/chats-members-command.js +4 -0
- package/dist/cli/messenger/chats-stats-command.js +1 -0
- package/dist/cli/messenger/chats-tracking-command.js +4 -1
- package/dist/cli/messenger/complete-command.js +6 -2
- package/dist/cli/messenger/contacts-command.js +11 -4
- package/dist/cli/messenger/context.d.ts +5 -0
- package/dist/cli/messenger/conversations-command.js +18 -1
- package/dist/cli/messenger/counters-command.js +1 -0
- package/dist/cli/messenger/doctor-command.js +2 -2
- package/dist/cli/messenger/download-command.js +1 -0
- package/dist/cli/messenger/flood-command.js +2 -1
- package/dist/cli/messenger/guard-commands.js +5 -4
- package/dist/cli/messenger/inbox.js +2 -0
- package/dist/cli/messenger/lacks.d.ts +7 -0
- package/dist/cli/messenger/lacks.js +18 -1
- package/dist/cli/messenger/mcp-command.js +6 -2
- package/dist/cli/messenger/media-archive-command.d.ts +3 -0
- package/dist/cli/messenger/media-archive-command.js +72 -0
- package/dist/cli/messenger/media-archive-move-command.d.ts +3 -0
- package/dist/cli/messenger/media-archive-move-command.js +39 -0
- package/dist/cli/messenger/media-archive-timer-command.d.ts +3 -0
- package/dist/cli/messenger/media-archive-timer-command.js +70 -0
- package/dist/cli/messenger/messages-command.js +2 -1
- package/dist/cli/messenger/messages-comments-command.js +1 -0
- package/dist/cli/messenger/messages-delete-command.js +1 -1
- package/dist/cli/messenger/messages-search-command.js +1 -0
- package/dist/cli/messenger/messages-stats-command.js +1 -0
- package/dist/cli/messenger/models-command.js +8 -2
- package/dist/cli/messenger/private-people-command.js +12 -10
- package/dist/cli/messenger/rankings-command.js +2 -0
- package/dist/cli/messenger/reactions-command.js +8 -2
- package/dist/cli/messenger/replies-command.js +1 -1
- package/dist/cli/messenger/retention-command.js +1 -0
- package/dist/cli/messenger/review.js +8 -2
- package/dist/cli/messenger/schedule-command.js +3 -3
- package/dist/cli/messenger/search-command.js +7 -3
- package/dist/cli/messenger/searches-command.js +24 -7
- package/dist/cli/messenger/serve-command.js +5 -1
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/server-command.js +17 -3
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/store-maintenance-command.js +10 -3
- package/dist/cli/messenger/tags-command.js +6 -2
- package/dist/cli/messenger/tasks-command.js +10 -7
- package/dist/cli/messenger/transcribe-command.js +1 -0
- package/dist/cli/messenger/watch-command.js +6 -1
- package/dist/cli/runs/command.js +2 -3
- package/dist/cli/skill-command.js +1 -1
- package/dist/domain/models.d.ts +2 -0
- package/dist/entities/contact-read.d.ts +119 -0
- package/dist/entities/contact-read.js +50 -0
- package/dist/entities/index.d.ts +120 -1
- package/dist/entities/index.js +5 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/parity/catalogue.js +1 -1
- package/dist/parity/manifest.d.ts +3 -1
- package/dist/parity/manifest.js +2 -0
- package/dist/parity/wording.js +2 -0
- package/dist/sends/permissions.js +7 -0
- package/dist/services/deps.d.ts +1 -1
- package/dist/services/deps.js +1 -1
- package/dist/services/inbox.js +5 -5
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +2 -0
- package/dist/services/media-archive-files.d.ts +10 -0
- package/dist/services/media-archive-files.js +110 -0
- package/dist/services/media-archive-locks.d.ts +6 -0
- package/dist/services/media-archive-locks.js +71 -0
- package/dist/services/media-archive-manifest.d.ts +29 -0
- package/dist/services/media-archive-manifest.js +116 -0
- package/dist/services/media-archive-move.d.ts +21 -0
- package/dist/services/media-archive-move.js +163 -0
- package/dist/services/media-archive.d.ts +34 -0
- package/dist/services/media-archive.js +165 -0
- package/dist/services/search-kind.js +1 -1
- package/package.json +2 -2
- package/parity.json +116 -193
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,80 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.398.0 — 23.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Top-level summaries, listeners and shell completion include examples in help and command discovery.
|
|
13
|
+
|
|
14
|
+
- Clearing a remembered flood hold includes an example in help and command discovery.
|
|
15
|
+
|
|
16
|
+
- **`Messenger.never`**: the adapter methods a messenger can never have, as opposed to `lacks`, what it does not have
|
|
17
|
+
yet. Their commands refuse as before and are also left out of help, `commands --json` and MCP. A command a
|
|
18
|
+
messenger only `lacks` stays in sight. For WhatsApp: `endOtherSessions` (only the phone ends other devices) and
|
|
19
|
+
`removeContact`.
|
|
20
|
+
|
|
21
|
+
- **`review --offline` reads the local store** instead of refusing, for every messenger. It shows only what was
|
|
22
|
+
already synced and says so (`missing` in JSON, a warning on the terminal); its next step starts from the same
|
|
23
|
+
point, and `--new` keeps its saved point, so an online review still sees what was never synced.
|
|
24
|
+
|
|
25
|
+
- **MCP offers more commands**, each limited by the profile's permissions: `runs list|show|path`, `store status|info|check|export|reindex`,
|
|
26
|
+
`store jobs cancel|retry|clear`, `attachments move`, `attachments jobs status`, `config show`, `commands schema`, `guide`,
|
|
27
|
+
`skill show`, `doctor report`, the `models` lists, `replies status|test|consents show`, `schedule list`,
|
|
28
|
+
`server status|logs`, and on the bot server `list`, `chats leave`, `chats admins add|remove`, `commands set|clear` and
|
|
29
|
+
`webhooks list|set|delete`. What stays off and why is in [the messenger rules](docs/dev/STANDARD.md#mcp).
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
|
|
33
|
+
- `@modelcontextprotocol/server` 2.3.0 clears the three high-severity advisories `npm audit` reported for every
|
|
34
|
+
WireCat CLI.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **A pasted help example runs.** The `Example:` line of an account command's `--help`, generated or written, names
|
|
39
|
+
the profile `personal` (`tg personal messages list Team`), so it no longer fails with "name a profile".
|
|
40
|
+
|
|
41
|
+
- `search mail` with no mail in the store points at `memo sync --only mail`; it named a `--mail` option memo does not have.
|
|
42
|
+
|
|
43
|
+
- `parity.json` matches tg and max: `guide`, `mentions`, `runs search`, `tasks reject` and `--brief`/`--full` are in
|
|
44
|
+
both, and each gap planned for tg names its tg-cli backlog id.
|
|
45
|
+
|
|
46
|
+
- `search all --help` says the messenger's server is asked by default and names `--backend archive` and `--offline`
|
|
47
|
+
to keep it to the local store.
|
|
48
|
+
|
|
49
|
+
## 0.397.0 — 23.10.2026
|
|
50
|
+
|
|
51
|
+
### Changed — may break callers
|
|
52
|
+
|
|
53
|
+
- MCP argument names follow singular CLI placeholders: `messages delete` takes `message`, chat export takes `chat`, and duplicate-task operations take `task` (with `other_task` for a pair). Update saved MCP requests using `messages`, `chats` or `dup`. CLI positional values and their order are unchanged.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- The option catalogue records WhatsApp's JPEG-only profile photos. Wording differences can be justified for one command without exempting that option everywhere or hiding a missing option.
|
|
58
|
+
|
|
59
|
+
- **Short command summaries and argument help.** Messenger commands keep their detailed descriptions while help and discovery show concise summaries. Contact notes, aliases, contact surnames and shell completion explain their positional arguments.
|
|
60
|
+
|
|
61
|
+
- Message deletion, chat export and duplicate-task commands use singular argument names in help and discovery. Variadic arguments still accept several values, and task argument help identifies each side of a pair.
|
|
62
|
+
|
|
63
|
+
- Contact profiles, phone lookups and address-book sync summaries declare their existing answer fields. Profile schemas retain masked phones, incomplete activity counts, partial provider flags and the source and precision of account-age estimates.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- Reaction commands include concrete examples in help and command discovery.
|
|
68
|
+
|
|
69
|
+
- Recipient-list commands include concrete examples in help and command discovery.
|
|
70
|
+
|
|
71
|
+
- Saved-search commands include concrete examples in help and command discovery.
|
|
72
|
+
|
|
73
|
+
- **Independent media archives for messenger CLIs.** `attachments download <dir>` saves files by account,
|
|
74
|
+
chat, message and position, verifies size and SHA-256 on reruns, and preserves completed downloads after
|
|
75
|
+
interruption. `attachments check <dir>` verifies copies offline. `attachments jobs` installs and manages
|
|
76
|
+
opt-in Linux daily timers. Archive files stay independent of message-store deletion; interrupted streams
|
|
77
|
+
restart from byte zero because messenger downloads expose no range validators.
|
|
78
|
+
Download help clarifies that receiving messages saves no media copies and names the command to stop a daily timer.
|
|
79
|
+
`attachments move <dir> <archive-dir> --before-time <time>` moves older images to verified cold storage;
|
|
80
|
+
`--dry-run` previews counts and bytes. Relocation records preserve offline verification and prevent duplicate
|
|
81
|
+
downloads. New manifests retain message dates and image MIME types; undated draft images are left in place.
|
|
9
82
|
|
|
10
83
|
## 0.396.0 — 23.10.2026
|
|
11
84
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# @wirecat/cli-core
|
|
6
6
|
|
|
7
|
-
The engine behind the WireCat command-line tools for Telegram, MAX, mail, notes
|
|
7
|
+
The engine behind the WireCat command-line tools for Telegram, MAX, WhatsApp, mail, notes, meetings and Google
|
|
8
|
+
Drive. Everything that
|
|
8
9
|
does not depend on one messenger lives here: the data model, a local SQLite store with full-text and semantic
|
|
9
10
|
search, a Lucene query language, the shared commands, an MCP server for AI agents, and the guard around every
|
|
10
11
|
write. A messenger plugs in as one adapter and gets all of it.
|
|
@@ -25,6 +26,7 @@ write. A messenger plugs in as one adapter and gets all of it.
|
|
|
25
26
|
|---|---|
|
|
26
27
|
| [Architecture](docs/dev/ARCHITECTURE.md) | The layers, the module map, the store internals and migrations, who consumes what |
|
|
27
28
|
| [Document workflows](docs/dev/DOCUMENTS.md) | Shared file ingestion, sync, scoped knowledge and extraction; the ports a file provider supplies |
|
|
29
|
+
| [Media archives](docs/dev/MEDIA-ARCHIVE.md) | Independent verified file copies, interruption recovery and opt-in daily timers |
|
|
28
30
|
| [Write an adapter](docs/dev/ADAPTERS.md) | How to add a messenger: the port, the `Messenger` description, the contract cases |
|
|
29
31
|
| [Messenger rules](docs/dev/STANDARD.md) | What binds tg, max and wa beyond the [CLI standards handbook](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/README.md): parity, the option catalogue, `bot` |
|
|
30
32
|
| [Glossary](docs/dev/GLOSSARY.md) | One word, one meaning across the tools |
|
|
@@ -35,6 +37,7 @@ write. A messenger plugs in as one adapter and gets all of it.
|
|
|
35
37
|
| [Testing](docs/dev/TESTING.md) · [Parity audit](docs/dev/PARITY-AUDIT.md) | The sandbox, the coverage floor, the checks across tg and max |
|
|
36
38
|
| [Tool boundaries](docs/dev/TOOL-BOUNDARIES.md) | Which tool owns which command |
|
|
37
39
|
| [API reference](docs/REFERENCE.md) | Every entry point and the detailed contract of each feature |
|
|
40
|
+
| [Roadmap](docs/roadmap.md) | What is being built next for developers on this package |
|
|
38
41
|
| [Changelog](CHANGELOG.md) | What each version changed, and what may break callers |
|
|
39
42
|
|
|
40
43
|
## What it is
|
|
@@ -71,7 +74,7 @@ It runs on Node 22.16+ and 24+, and on Bun.
|
|
|
71
74
|
|
|
72
75
|
```mermaid
|
|
73
76
|
flowchart TB
|
|
74
|
-
tools["tg · max · memo · zm · drive"] --> interface
|
|
77
|
+
tools["tg · max · wa · memo · zm · drive"] --> interface
|
|
75
78
|
subgraph core ["@wirecat/cli-core"]
|
|
76
79
|
interface["Interface: CLI commands and MCP tools"] --> services["Services: one use case each"]
|
|
77
80
|
services --> ports["Ports: MessengerAdapter · MeetingSource · TaskStore"]
|
|
@@ -87,7 +90,7 @@ Five layers, and each calls only the layers below it: domain, adapters behind a
|
|
|
87
90
|
|
|
88
91
|
- **The CLI and the MCP server are two adapters over the same services.** A command and its MCP tool answer with
|
|
89
92
|
the same data, because one service produces it.
|
|
90
|
-
- **A port with one required core and optional groups.** `MessengerCore` is what every messenger must do.
|
|
93
|
+
- **A port with one required core and optional groups.** `MessengerCore` is what every messenger must do. The other
|
|
91
94
|
groups — server reads, polls, forums, reactions and the rest — are optional and reached through `capability()`.
|
|
92
95
|
A messenger without a group refuses the command before connecting; it never crashes and never silently drops an
|
|
93
96
|
option.
|
|
@@ -115,8 +118,9 @@ code and its document must agree. A search plan that reads an index once per mes
|
|
|
115
118
|
- **One query language for every list and every source.** A TypeScript port of the Apache Lucene 9.12.3 parsers,
|
|
116
119
|
with no runtime parser dependency. Phrases, `AND` / `OR` / `NOT`, `+required`, grouping, field groups like
|
|
117
120
|
`from:(alice OR bob)`, ranges `date:[2026-01-01 TO 2026-02-01}`, comparisons `size>10MB`, wildcards and regular
|
|
118
|
-
expressions. 27 fields: sender, chat, date, file name, MIME type, transcript, subject, label and more.
|
|
119
|
-
|
|
121
|
+
expressions. 27 fields: sender, chat, date, file name, MIME type, transcript, subject, label and more.
|
|
122
|
+
Proximity `"invoice paid"~3` finds the words close together in one message; the fuzzy operator `word~1` is
|
|
123
|
+
refused with an error that names the alternative.
|
|
120
124
|
[Syntax](docs/search/query-language.md) · [grammar](docs/search/query-language-spec.md)
|
|
121
125
|
- **Any three letters inside a word.** An FTS5 trigram index finds `port` inside `report`. A
|
|
122
126
|
second, word index ranks by BM25.
|
|
@@ -210,8 +214,9 @@ they deliberately differ. A rule changes in its own pull request, before the cod
|
|
|
210
214
|
- **Built for agents.** `--no-input` never prompts. Input and output are bounded (16 MiB in, 4 MiB out) and fail
|
|
211
215
|
visibly instead of truncating. One-shot commands time out after 30 seconds and close what they hold. `--dry-run`
|
|
212
216
|
previews any write.
|
|
213
|
-
- **The tool never picks an account.**
|
|
214
|
-
|
|
217
|
+
- **The tool never picks an account.** Every account command names its profile — the first word in tg, max and
|
|
218
|
+
wa, an option such as `--account` or `--profile` in the others; without one, the command stops and lists the
|
|
219
|
+
choices — even when there is only one.
|
|
215
220
|
- **Run records keep ids and timings, never content.** A failure is kept by default, so `runs show` can explain it
|
|
216
221
|
without anyone sending a log full of messages.
|
|
217
222
|
- **Tests that cannot touch real data.** A sandbox keeps every test away from the owner's store, and no test may
|
|
@@ -252,8 +257,7 @@ describe("the adapter keeps the port's promises", () => {
|
|
|
252
257
|
**Before 1.0, any release may break a caller.** Releases are frequent, sometimes several a day, and any of them
|
|
253
258
|
may change any entry point. Every such change is listed in the [changelog](CHANGELOG.md) under "Changed — may break
|
|
254
259
|
callers", with what to change. tg-cli and max-cli pin an exact version and move to a new one in their own pull request.
|
|
255
|
-
Pin an exact version too
|
|
256
|
-
release has.
|
|
260
|
+
Pin an exact version too.
|
|
257
261
|
The full list of entry points and what each holds is in the [API reference](docs/REFERENCE.md).
|
|
258
262
|
|
|
259
263
|
⚠ **Errors are recognised by shape, not by class** (`isCliFailure`). A package linked during development brings its
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AppIdentity } from "../ports/app.js";
|
|
2
|
+
import type { ServerSystem } from "./system.js";
|
|
3
|
+
export declare const runArchiveTimerCommand: (args: string[], env: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<{
|
|
4
|
+
code: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}>;
|
|
8
|
+
export interface DailyArchiveTimer {
|
|
9
|
+
name: string;
|
|
10
|
+
unitDirectory: string;
|
|
11
|
+
service: string;
|
|
12
|
+
timer: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DailyArchiveTimerInput {
|
|
15
|
+
app: AppIdentity;
|
|
16
|
+
profile: string;
|
|
17
|
+
directory: string;
|
|
18
|
+
entry: readonly string[];
|
|
19
|
+
env: NodeJS.ProcessEnv;
|
|
20
|
+
chats?: readonly string[];
|
|
21
|
+
sinceTime?: string;
|
|
22
|
+
at?: string;
|
|
23
|
+
timeZone?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const dailyArchiveTimer: (input: DailyArchiveTimerInput) => DailyArchiveTimer;
|
|
26
|
+
export declare const manageDailyArchiveTimer: (plan: DailyArchiveTimer, action: "install" | "start" | "stop" | "status" | "uninstall", system: Pick<ServerSystem, "platform" | "run">, env: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<{
|
|
27
|
+
name: string;
|
|
28
|
+
installed: boolean;
|
|
29
|
+
active: boolean;
|
|
30
|
+
servicePath: string;
|
|
31
|
+
timerPath: string;
|
|
32
|
+
action?: undefined;
|
|
33
|
+
} | {
|
|
34
|
+
active?: undefined;
|
|
35
|
+
name: string;
|
|
36
|
+
installed: boolean;
|
|
37
|
+
action: "install" | "start" | "stop" | "uninstall";
|
|
38
|
+
servicePath: string;
|
|
39
|
+
timerPath: string;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, realpath, unlink } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { CliError } from "@wirecat/cli-base";
|
|
5
|
+
import { acquireLock, readBoundedUtf8File, syncDirectory, writeFileAtomic } from "../files/index.js";
|
|
6
|
+
import { locationVariables } from "./units.js";
|
|
7
|
+
export const runArchiveTimerCommand = (args, env, signal) => new Promise((resolve, reject) => {
|
|
8
|
+
execFile(args[0] ?? "systemctl", args.slice(1), {
|
|
9
|
+
env,
|
|
10
|
+
signal,
|
|
11
|
+
timeout: 30000,
|
|
12
|
+
maxBuffer: 65536,
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
shell: false,
|
|
15
|
+
}, (error, stdout, stderr) => {
|
|
16
|
+
if (!error)
|
|
17
|
+
return resolve({ code: 0, stdout, stderr });
|
|
18
|
+
if (typeof error.code === "number")
|
|
19
|
+
return resolve({ code: error.code, stdout, stderr });
|
|
20
|
+
reject(new CliError("configuration_error", "archive timer command did not complete; inspect its systemctl status"));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
const missingMediaFile = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
24
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
25
|
+
const quote = (value, expandDollar) => `"${value
|
|
26
|
+
.replaceAll("\\", "\\\\")
|
|
27
|
+
.replaceAll('"', '\\"')
|
|
28
|
+
.replaceAll("%", "%%")
|
|
29
|
+
.replaceAll("$", () => (expandDollar ? "$$" : "$"))}"`;
|
|
30
|
+
const unitFolder = (env) => {
|
|
31
|
+
const folder = env.XDG_CONFIG_HOME ?? (env.HOME ? join(env.HOME, ".config") : "");
|
|
32
|
+
if (!isAbsolute(folder) || /[\p{Cc}]/u.test(folder))
|
|
33
|
+
throw new CliError("configuration_error", "a timer needs an absolute HOME or XDG_CONFIG_HOME");
|
|
34
|
+
return join(folder, "systemd", "user");
|
|
35
|
+
};
|
|
36
|
+
export const dailyArchiveTimer = (input) => {
|
|
37
|
+
const at = input.at ?? "03:00";
|
|
38
|
+
const timeZone = input.timeZone ?? "UTC";
|
|
39
|
+
const environment = locationVariables(input.app, input.env);
|
|
40
|
+
let zone;
|
|
41
|
+
try {
|
|
42
|
+
zone = new Intl.DateTimeFormat("en", { timeZone }).resolvedOptions().timeZone;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new CliError("validation_error", "--timezone needs an IANA time zone");
|
|
46
|
+
}
|
|
47
|
+
if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(at) ||
|
|
48
|
+
!/^[A-Za-z0-9_+/-]+$/.test(zone) ||
|
|
49
|
+
!/^[a-z][a-z0-9-]*$/.test(input.app.command) ||
|
|
50
|
+
!input.profile ||
|
|
51
|
+
!isAbsolute(input.directory) ||
|
|
52
|
+
input.entry.length < 1 ||
|
|
53
|
+
input.entry.some((one) => !isAbsolute(one)) ||
|
|
54
|
+
[
|
|
55
|
+
input.profile,
|
|
56
|
+
input.directory,
|
|
57
|
+
...input.entry,
|
|
58
|
+
...Object.values(environment),
|
|
59
|
+
...(input.chats ?? []),
|
|
60
|
+
input.sinceTime ?? "",
|
|
61
|
+
].some((one) => /[\p{Cc}]/u.test(one)))
|
|
62
|
+
throw new CliError("validation_error", "invalid daily archive timer arguments");
|
|
63
|
+
const scope = digest(JSON.stringify([
|
|
64
|
+
input.app.command,
|
|
65
|
+
input.profile,
|
|
66
|
+
input.directory,
|
|
67
|
+
Object.entries(environment).sort(([a], [b]) => a.localeCompare(b)),
|
|
68
|
+
]));
|
|
69
|
+
const name = `${input.app.command}-media-archive-${scope.slice(0, 32)}`;
|
|
70
|
+
const owner = `# WireCat daily media archive ${scope}`;
|
|
71
|
+
const command = [
|
|
72
|
+
...input.entry,
|
|
73
|
+
input.profile,
|
|
74
|
+
"attachments",
|
|
75
|
+
"download",
|
|
76
|
+
input.directory,
|
|
77
|
+
"--json",
|
|
78
|
+
"--no-input",
|
|
79
|
+
"--timeout",
|
|
80
|
+
"6h",
|
|
81
|
+
...(input.sinceTime ? ["--since-time", input.sinceTime] : []),
|
|
82
|
+
...(input.chats ?? []).flatMap((chat) => ["--chat", chat]),
|
|
83
|
+
];
|
|
84
|
+
const seal = (text) => `${text}# SHA-256 ${digest(text)}\n`;
|
|
85
|
+
return {
|
|
86
|
+
name,
|
|
87
|
+
unitDirectory: unitFolder(input.env),
|
|
88
|
+
service: seal(`${owner}\n[Unit]\nDescription=WireCat media archive\n\n[Service]\nType=oneshot\nUMask=0077\nTimeoutStartSec=6h\n${Object.entries(environment)
|
|
89
|
+
.map(([key, value]) => `Environment=${quote(`${key}=${value}`, false)}\n`)
|
|
90
|
+
.join("")}ExecStart=${command.map((one) => quote(one, true)).join(" ")}\n`),
|
|
91
|
+
timer: seal(`${owner}\n[Unit]\nDescription=WireCat daily media archive\n\n[Timer]\nOnCalendar=*-*-* ${at}:00 ${zone}\nPersistent=true\nUnit=${name}.service\n\n[Install]\nWantedBy=timers.target\n`),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const ownedUnit = async (path, expectedOwner) => {
|
|
95
|
+
let text;
|
|
96
|
+
try {
|
|
97
|
+
text = await readBoundedUtf8File(path, { maxBytes: 65536 });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (missingMediaFile(error))
|
|
101
|
+
return null;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const sealAt = text.lastIndexOf("# SHA-256 ");
|
|
105
|
+
if (!text.startsWith(`${expectedOwner}\n`) ||
|
|
106
|
+
sealAt < 0 ||
|
|
107
|
+
text.slice(sealAt) !== `# SHA-256 ${digest(text.slice(0, sealAt))}\n`)
|
|
108
|
+
throw new CliError("validation_error", "archive timer unit is foreign or edited; it was preserved");
|
|
109
|
+
return text;
|
|
110
|
+
};
|
|
111
|
+
export const manageDailyArchiveTimer = async (plan, action, system, env, signal) => {
|
|
112
|
+
if (system.platform !== "linux")
|
|
113
|
+
throw new CliError("configuration_error", "daily media archive timers require Linux systemd user services");
|
|
114
|
+
if (!isAbsolute(plan.unitDirectory) || !/^[a-z][a-z0-9-]*-media-archive-[a-f0-9]{32}$/.test(plan.name))
|
|
115
|
+
throw new CliError("validation_error", "invalid archive timer paths");
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
await mkdir(plan.unitDirectory, { recursive: true, mode: 0o700 });
|
|
118
|
+
const folder = await realpath(plan.unitDirectory);
|
|
119
|
+
const lease = await acquireLock(join(folder, `.${plan.name}.lock`), { signal });
|
|
120
|
+
const beforeWrite = async () => {
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
if (!(await lease.held()))
|
|
123
|
+
throw new CliError("validation_error", "archive timer lost its writer lock");
|
|
124
|
+
};
|
|
125
|
+
const servicePath = resolve(folder, `${plan.name}.service`);
|
|
126
|
+
const timerPath = resolve(folder, `${plan.name}.timer`);
|
|
127
|
+
const owner = plan.service.split("\n")[0] ?? "";
|
|
128
|
+
const run = async (...args) => {
|
|
129
|
+
await beforeWrite();
|
|
130
|
+
const result = await system.run(["systemctl", "--user", ...args], env);
|
|
131
|
+
if (result.code !== 0)
|
|
132
|
+
throw new CliError("configuration_error", "systemctl could not change the archive timer; inspect its status");
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const service = await ownedUnit(servicePath, owner);
|
|
136
|
+
const timer = await ownedUnit(timerPath, owner);
|
|
137
|
+
const installed = service !== null && timer !== null;
|
|
138
|
+
if (action === "status") {
|
|
139
|
+
const active = installed
|
|
140
|
+
? await system.run(["systemctl", "--user", "is-active", `${plan.name}.timer`], env)
|
|
141
|
+
: undefined;
|
|
142
|
+
return { name: plan.name, installed, active: active?.code === 0, servicePath, timerPath };
|
|
143
|
+
}
|
|
144
|
+
if (action === "install") {
|
|
145
|
+
if ((service !== null && service !== plan.service) || (timer !== null && timer !== plan.timer))
|
|
146
|
+
throw new CliError("validation_error", "this destination already has a different schedule; stop and uninstall it first");
|
|
147
|
+
await beforeWrite();
|
|
148
|
+
if (service === null)
|
|
149
|
+
await writeFileAtomic(servicePath, plan.service, { overwrite: false, beforePublish: beforeWrite });
|
|
150
|
+
if (timer === null)
|
|
151
|
+
await writeFileAtomic(timerPath, plan.timer, { overwrite: false, beforePublish: beforeWrite });
|
|
152
|
+
await run("daemon-reload");
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!installed)
|
|
156
|
+
throw new CliError("not_found", "archive timer is not installed");
|
|
157
|
+
if (action === "start")
|
|
158
|
+
await run("enable", "--now", `${plan.name}.timer`);
|
|
159
|
+
if (action === "stop")
|
|
160
|
+
await run("disable", "--now", `${plan.name}.timer`);
|
|
161
|
+
if (action === "uninstall") {
|
|
162
|
+
await beforeWrite();
|
|
163
|
+
const active = await system.run(["systemctl", "--user", "is-active", `${plan.name}.timer`], env);
|
|
164
|
+
const enabled = await system.run(["systemctl", "--user", "is-enabled", `${plan.name}.timer`], env);
|
|
165
|
+
const running = await system.run(["systemctl", "--user", "is-active", `${plan.name}.service`], env);
|
|
166
|
+
if (active.code !== 3 || enabled.code !== 1 || running.code !== 3)
|
|
167
|
+
throw new CliError("validation_error", "stop the archive timer and wait for its service to finish before uninstalling");
|
|
168
|
+
await beforeWrite();
|
|
169
|
+
await unlink(servicePath);
|
|
170
|
+
await unlink(timerPath);
|
|
171
|
+
await syncDirectory(folder);
|
|
172
|
+
await run("daemon-reload");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { name: plan.name, installed: action !== "uninstall", action, servicePath, timerPath };
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await lease.release();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
import { execFile } from "node:child_process";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { type Checkpoint, checkpointPath, clearCheckpoint, type Progress, progressLine, progressOf, readCheckpoint, saveCheckpoint, } from "./checkpoints.js";
|
|
2
|
+
export type { DailyArchiveTimer, DailyArchiveTimerInput } from "./daily-archive-timer.js";
|
|
3
|
+
export { dailyArchiveTimer, manageDailyArchiveTimer } from "./daily-archive-timer.js";
|
|
2
4
|
export { holdLock, type Lock, lockPath, readLock, releaseLock, servingProfiles, takeLock } from "./lock.js";
|
|
3
5
|
export { alive, carries, holdersOf } from "./processes.js";
|
|
4
6
|
export { listenRelay, RELAY_PROTOCOL, type RelayAddress, type RelayIo, type RelayRequest, relayCommand, relayPath, } from "./relay.js";
|
package/dist/background/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { checkpointPath, clearCheckpoint, progressLine, progressOf, readCheckpoint, saveCheckpoint, } from "./checkpoints.js";
|
|
2
|
+
export { dailyArchiveTimer, manageDailyArchiveTimer } from "./daily-archive-timer.js";
|
|
2
3
|
export { holdLock, lockPath, readLock, releaseLock, servingProfiles, takeLock } from "./lock.js";
|
|
3
4
|
export { alive, carries, holdersOf } from "./processes.js";
|
|
4
5
|
export { listenRelay, RELAY_PROTOCOL, relayCommand, relayPath, } from "./relay.js";
|
package/dist/cli/bot/admins.js
CHANGED
|
@@ -35,7 +35,7 @@ export const botAdminsCommand = (bot) => {
|
|
|
35
35
|
renderAnswer(this, context, wholeList("bot chats admins list", admins.map(snakeKeys)));
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
-
annotate(command.command("add"), { effects: REMOTE_WRITE
|
|
38
|
+
annotate(command.command("add"), { effects: REMOTE_WRITE })
|
|
39
39
|
.description("make a member an admin with these rights")
|
|
40
40
|
.argument("<chat>", CHAT)
|
|
41
41
|
.argument("<person>", PERSON)
|
|
@@ -61,7 +61,7 @@ export const botAdminsCommand = (bot) => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
-
annotate(command.command("remove"), { effects: REMOTE_WRITE
|
|
64
|
+
annotate(command.command("remove"), { effects: REMOTE_WRITE })
|
|
65
65
|
.description("take an admin's rights back; they stay a member")
|
|
66
66
|
.argument("<chat>", CHAT)
|
|
67
67
|
.argument("<person>", PERSON)
|
package/dist/cli/bot/command.js
CHANGED
|
@@ -111,7 +111,7 @@ const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ })
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
-
const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ
|
|
114
|
+
const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ })
|
|
115
115
|
.description(`every name on this machine that has a bot token; --check asks ${bot.name ?? "the messenger"} which bot each is`)
|
|
116
116
|
.option("--check", "ask the messenger who each bot is, with its token")
|
|
117
117
|
.action(async function () {
|
|
@@ -216,7 +216,7 @@ const chatsCommand = (bot) => {
|
|
|
216
216
|
renderItem(this, context, found);
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
-
annotate(command.command("leave"), { effects: REMOTE_WRITE
|
|
219
|
+
annotate(command.command("leave"), { effects: REMOTE_WRITE })
|
|
220
220
|
.description("take the bot out of a chat; only an admin of the chat can bring it back")
|
|
221
221
|
.argument("<chat>", "a chat id, or the title of a chat this bot has seen")
|
|
222
222
|
.action(async function (chat) {
|
package/dist/cli/bot/setup.js
CHANGED
|
@@ -48,13 +48,13 @@ export const botMenuCommand = (bot) => {
|
|
|
48
48
|
renderAnswer(command, context, wholeList("bot commands list", entries));
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
|
-
annotate(command.command("set"), { effects: REMOTE_WRITE
|
|
51
|
+
annotate(command.command("set"), { effects: REMOTE_WRITE })
|
|
52
52
|
.description("replace the whole menu: each command as name=description, e.g. start=Begin")
|
|
53
53
|
.argument("<commands...>", "name=description, one per command")
|
|
54
54
|
.action(async function (typed) {
|
|
55
55
|
await replace(this, typed.map(entryOf), "bot.commands.set");
|
|
56
56
|
});
|
|
57
|
-
annotate(command.command("clear"), { effects: REMOTE_WRITE
|
|
57
|
+
annotate(command.command("clear"), { effects: REMOTE_WRITE })
|
|
58
58
|
.description("empty the menu")
|
|
59
59
|
.action(async function () {
|
|
60
60
|
await replace(this, [], "bot.commands.clear");
|
|
@@ -97,7 +97,7 @@ export const botCallbacksCommand = (bot) => new Command("callbacks").description
|
|
|
97
97
|
}));
|
|
98
98
|
/** `bot webhooks`: where the messenger pushes this bot's updates instead of being polled. */
|
|
99
99
|
export const botWebhooksCommand = (bot) => {
|
|
100
|
-
const command =
|
|
100
|
+
const command = new Command("webhooks").description("where the messenger pushes this bot's updates — while one is set, `bot watch` gets nothing");
|
|
101
101
|
annotate(command.command("list"), { effects: REMOTE_READ })
|
|
102
102
|
.description("the webhooks this bot has")
|
|
103
103
|
.action(async function () {
|
|
@@ -50,7 +50,7 @@ export const commandsCommand = (app) => {
|
|
|
50
50
|
exit_codes: { ok: 0, [CRASH_CODE]: GENERIC_FAILURE, ...EXIT_CODES },
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
-
command.addCommand(annotate(new Command("schema"), { effects: NO_EFFECTS
|
|
53
|
+
command.addCommand(annotate(new Command("schema"), { effects: NO_EFFECTS })
|
|
54
54
|
.description("one command's argv and result schemas, effects, permissions and retry guidance")
|
|
55
55
|
.argument("<path...>", "one command path, for example: stats messages show")
|
|
56
56
|
.action(function (path) {
|
|
@@ -15,7 +15,7 @@ import { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } fro
|
|
|
15
15
|
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
16
16
|
*/
|
|
17
17
|
export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
18
|
-
const command =
|
|
18
|
+
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
19
19
|
annotate(command.command("show"), { effects: LOCAL_READ, examples: [`${app.command} personal config show`] })
|
|
20
20
|
.description("the profile, the profiles that exist, and each setting with where it came from")
|
|
21
21
|
.option("--bot", "the settings a bot command on this profile gets, rather than the personal account's")
|
package/dist/cli/guide.js
CHANGED
|
@@ -116,7 +116,6 @@ const scenarioText = (cli, scenario, profile) => [
|
|
|
116
116
|
export const guideCommand = (app, options) => {
|
|
117
117
|
const command = annotate(new Command("guide"), {
|
|
118
118
|
effects: NO_EFFECTS,
|
|
119
|
-
mcp: false,
|
|
120
119
|
examples: [`${app.command} guide`, `${app.command} guide <name> --json`],
|
|
121
120
|
})
|
|
122
121
|
.description("scenarios: the commands for a common task, in order, and why each")
|
|
@@ -7,6 +7,8 @@ export interface NextChoice {
|
|
|
7
7
|
}
|
|
8
8
|
/** Overrides the "Next usually" a command's help derives from types. Kept off `annotate`, like its section. */
|
|
9
9
|
export declare const nextUsually: <T extends Command>(command: T, choice: NextChoice) => T;
|
|
10
|
+
/** The profile an example names, so a pasted one runs; the name the first screen's "Start here" uses. */
|
|
11
|
+
export declare const EXAMPLE_PROFILE = "personal";
|
|
10
12
|
export interface CommandHelp {
|
|
11
13
|
returns: string;
|
|
12
14
|
next: readonly string[];
|
|
@@ -20,8 +22,13 @@ export declare const commandHelp: (command: Command) => CommandHelp;
|
|
|
20
22
|
export declare const commandHelpText: (command: Command) => string;
|
|
21
23
|
/** The same, as Markdown lines for `docs/commands.md`: hand it to cli-base's `commandsPage` per command. */
|
|
22
24
|
export declare const commandHelpMarkdown: (root: Command, path: readonly string[]) => string[];
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown.
|
|
27
|
+
* With `profile`, an example of an account command gets the profile word it would fail without.
|
|
28
|
+
*/
|
|
29
|
+
export declare const installCommandHelp: (program: Command, { profile }?: {
|
|
30
|
+
profile?: boolean;
|
|
31
|
+
}) => void;
|
|
25
32
|
/** Which leaf commands still show a generated example, so the important ones get a written one. Report only. */
|
|
26
33
|
export declare const exampleReport: (program: Command) => {
|
|
27
34
|
written: string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { PROFILE_FREE } from "../profile.js";
|
|
4
5
|
import { commandAt } from "../steps.js";
|
|
5
6
|
import { short } from "./commands-text.js";
|
|
6
7
|
/** Argument names onto the kinds `domain/references.ts` names; a plural takes the same kind. */
|
|
@@ -164,16 +165,28 @@ const generatedExample = (command) => {
|
|
|
164
165
|
return [flag];
|
|
165
166
|
return [flag, option.argChoices?.[0] ?? sample(placeholder ?? option.attributeName())];
|
|
166
167
|
});
|
|
167
|
-
// No profile word, as on the first screen and in written examples: the Usage line already shows `<profile>`.
|
|
168
168
|
return [rootOf(command).name(), ...commandPathOf(command), ...words, ...options].join(" ");
|
|
169
169
|
};
|
|
170
|
+
const profiled = new WeakSet();
|
|
171
|
+
/** The profile an example names, so a pasted one runs; the name the first screen's "Start here" uses. */
|
|
172
|
+
export const EXAMPLE_PROFILE = "personal";
|
|
173
|
+
const withProfile = (command, example) => {
|
|
174
|
+
const root = rootOf(command);
|
|
175
|
+
const top = commandPathOf(command)[0];
|
|
176
|
+
if (!profiled.has(root) || top === undefined || PROFILE_FREE.has(top))
|
|
177
|
+
return example;
|
|
178
|
+
const prefix = `${root.name()} ${top}`;
|
|
179
|
+
return example === prefix || example.startsWith(`${prefix} `)
|
|
180
|
+
? `${root.name()} ${EXAMPLE_PROFILE} ${example.slice(root.name().length + 1)}`
|
|
181
|
+
: example;
|
|
182
|
+
};
|
|
170
183
|
/** Returns, Next usually and Example for one command, from its definition alone. */
|
|
171
184
|
export const commandHelp = (command) => {
|
|
172
185
|
const written = metaOf(command).examples ?? [];
|
|
173
186
|
return {
|
|
174
187
|
returns: returnsOf(command),
|
|
175
188
|
next: nextOf(command).map(call),
|
|
176
|
-
examples: written.length > 0 ? written : [generatedExample(command)],
|
|
189
|
+
examples: (written.length > 0 ? written : [generatedExample(command)]).map((one) => withProfile(command, one)),
|
|
177
190
|
origin: written.length > 0 ? "written" : "generated",
|
|
178
191
|
};
|
|
179
192
|
};
|
|
@@ -197,8 +210,13 @@ export const commandHelpMarkdown = (root, path) => {
|
|
|
197
210
|
`**Example:** ${code(examples)}`,
|
|
198
211
|
];
|
|
199
212
|
};
|
|
200
|
-
/**
|
|
201
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown.
|
|
215
|
+
* With `profile`, an example of an account command gets the profile word it would fail without.
|
|
216
|
+
*/
|
|
217
|
+
export const installCommandHelp = (program, { profile = true } = {}) => {
|
|
218
|
+
if (profile)
|
|
219
|
+
profiled.add(program);
|
|
202
220
|
for (const command of leaves(program))
|
|
203
221
|
command.addHelpText("after", () => commandHelpText(command));
|
|
204
222
|
};
|
|
@@ -4,7 +4,7 @@ import { Command, Help, Option } from "commander";
|
|
|
4
4
|
import wrapAnsi from "wrap-ansi";
|
|
5
5
|
import { NO_EFFECTS } from "../effects.js";
|
|
6
6
|
import { commandAt } from "../steps.js";
|
|
7
|
-
import { installCommandHelp } from "./command-help.js";
|
|
7
|
+
import { EXAMPLE_PROFILE, installCommandHelp } from "./command-help.js";
|
|
8
8
|
/** Every WireCat tool, as "Works with" names it; a tool's own entry is left out of its screen. */
|
|
9
9
|
export const WIRECAT_TOOLS = [
|
|
10
10
|
{ command: "tg", does: "Telegram in the same local store: search, review and tasks offline" },
|
|
@@ -56,7 +56,7 @@ export const firstScreen = (program, screen = {}) => {
|
|
|
56
56
|
const can = (screen.can ?? []).map(({ command, does }) => [command, does]);
|
|
57
57
|
const works = WIRECAT_TOOLS.filter((tool) => tool.command !== cli).map(({ command, shown, does }) => [shown ?? command, does]);
|
|
58
58
|
const column = Math.max(22, widest([...can, ...works].map(([left]) => left))) + 2;
|
|
59
|
-
const start = screen.start ?? (commandAt(program, "setup") ? `${cli}
|
|
59
|
+
const start = screen.start ?? (commandAt(program, "setup") ? `${cli} ${EXAMPLE_PROFILE} setup` : undefined);
|
|
60
60
|
return [
|
|
61
61
|
wrapAnsi(`${cli} — ${pitch}`, WIDTH),
|
|
62
62
|
"",
|
|
@@ -95,7 +95,7 @@ export const installHelp = (program, screen = {}) => {
|
|
|
95
95
|
? firstScreen(program, screen)
|
|
96
96
|
: (own.formatHelp ?? Help.prototype.formatHelp).call(helper, command, helper),
|
|
97
97
|
});
|
|
98
|
-
installCommandHelp(program);
|
|
98
|
+
installCommandHelp(program, { profile: screen.profile !== false });
|
|
99
99
|
program.addOption(new Option("--help-all", "the full help: every command and option").hideHelp());
|
|
100
100
|
program.on("option:help-all", () => {
|
|
101
101
|
full = true;
|
|
@@ -12,12 +12,14 @@ import { recalledAccount } from "./accounts.js";
|
|
|
12
12
|
import { photoHelp, readProfilePhoto } from "./admin-commands.js";
|
|
13
13
|
import { accountAnswer, renderItem, wholeList } from "./chat-answers.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
|
+
import { hideNever } from "./lacks.js";
|
|
15
16
|
import { privacyCommand } from "./records-command.js";
|
|
16
17
|
export const accountCommand = (messenger) => {
|
|
17
18
|
const tool = `${messenger.app.command} personal`;
|
|
18
19
|
const account = new Command("account")
|
|
19
20
|
.description("the logged-in account")
|
|
20
21
|
.addCommand(annotate(new Command("list"), { effects: REMOTE_READ, examples: [`${tool} account list`] })
|
|
22
|
+
.summary("profiles on this computer and their signed-in accounts")
|
|
21
23
|
.description("every profile on this computer, and the account each is logged in as; asks the messenger nothing")
|
|
22
24
|
.action(async function () {
|
|
23
25
|
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
@@ -79,5 +81,5 @@ export const accountCommand = (messenger) => {
|
|
|
79
81
|
.addCommand(accountSessionsCommand(messenger));
|
|
80
82
|
if (messenger.privacy === true)
|
|
81
83
|
account.addCommand(privacyCommand(messenger));
|
|
82
|
-
return account;
|
|
84
|
+
return hideNever(messenger, account);
|
|
83
85
|
};
|
|
@@ -12,7 +12,9 @@ const SESSIONS_QUERY = unfilteredQuery("account_session", accountSession);
|
|
|
12
12
|
/** `account sessions …` — where else the account is logged in, not this tool's own `session`. */
|
|
13
13
|
export const accountSessionsCommand = (messenger) => {
|
|
14
14
|
const tool = `${messenger.app.command} personal`;
|
|
15
|
-
const sessions = new Command("sessions")
|
|
15
|
+
const sessions = new Command("sessions")
|
|
16
|
+
.summary("devices and apps signed in to this account")
|
|
17
|
+
.description(`where else this account is logged in — not \`${messenger.app.command} session\`, which is this tool's own login`);
|
|
16
18
|
withQuery(annotate(sessions.command("list"), { effects: REMOTE_READ, examples: [`${tool} account sessions list`] }), SESSIONS_QUERY)
|
|
17
19
|
.description("every device and app logged in to this account; nothing is ended")
|
|
18
20
|
.action(async function (words) {
|