@wirecat/cli-core 0.597.0 → 0.599.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 +55 -0
- package/dist/calendar/cli/index.js +4 -11
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/context.js +17 -3
- package/dist/cli/contract-templates.js +1 -0
- package/dist/cli/early-refusal.js +7 -5
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/messenger/doctor-command.d.ts +3 -0
- package/dist/cli/messenger/doctor-command.js +17 -3
- package/dist/cli/messenger/store-maintenance-command.d.ts +28 -3
- package/dist/cli/messenger/store-maintenance-command.js +25 -3
- package/dist/cli/program.js +3 -0
- package/dist/cli/store-info.d.ts +4 -1
- package/dist/cli/store-info.js +7 -3
- package/dist/cli/store-recover.d.ts +56 -0
- package/dist/cli/store-recover.js +153 -0
- package/dist/crm/cli/index.js +8 -6
- package/dist/errors/reasons.js +8 -0
- package/dist/kit/index.d.ts +1 -0
- package/dist/kit/index.js +1 -0
- package/dist/kit/run-scope.d.ts +34 -0
- package/dist/kit/run-scope.js +112 -0
- package/dist/mcp/app-server.js +7 -2
- package/dist/meetings/cli/index.d.ts +5 -0
- package/dist/meetings/cli/index.js +4 -3
- package/dist/store/damage.d.ts +19 -0
- package/dist/store/damage.js +58 -0
- package/dist/store/health.d.ts +50 -0
- package/dist/store/health.js +170 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +2 -0
- package/dist/store/recover.d.ts +103 -0
- package/dist/store/recover.js +404 -0
- package/dist/store/sqlite/open-store.js +9 -2
- package/package.json +1 -1
- package/parity.json +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.599.0 — 07.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`store recover`** tells whether the store file is damaged: it reads every page and every search index, names the
|
|
15
|
+
damaged tables, and answers in plain words what happened, likely causes, how to look further, what is affected, that
|
|
16
|
+
the messengers', mail servers' and Drive's own data is not, and the commands to run. It changes nothing.
|
|
17
|
+
`storeRecoveryCheck` gives the same to a CLI with its own `store` command.
|
|
18
|
+
- **`store recover run`** rebuilds a damaged store: it copies every readable row into a new file with the `sqlite3`
|
|
19
|
+
program (3.43 or newer; node:sqlite has no recovery API), rebuilds every search index, the mentions and the vector
|
|
20
|
+
index from the data, checks the result, and puts it in place of the store, keeping the damaged file beside it. It
|
|
21
|
+
refuses while a WireCat timer is scheduled or a process has the store open, and names them. The answer lists the
|
|
22
|
+
tables that lost rows, the rows that point at lost ones, and the mail sync that fetches lost mail again.
|
|
23
|
+
`storeRecoveryRun` and `storeRecoverCommand` give it to other CLIs.
|
|
24
|
+
- **`doctor` checks the store's health**: the file header, its size and the schema's own pages, in milliseconds. A
|
|
25
|
+
damaged store is explained in the answer's `store.damage` (`explainDamage`: what, causes, investigate, impact,
|
|
26
|
+
external_unaffected, steps) and points at `store recover`, the whole-file check. `storeSummary` takes
|
|
27
|
+
`{ deep, command }`, and `DOCTOR_DEEP_OPTION` words a CLI's own `--deep`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Opening the store a second time in one process no longer drops the locks of the connection already open.** The
|
|
32
|
+
open touched the existing file through its own descriptor, and on Linux closing any descriptor of a file releases
|
|
33
|
+
every lock the process holds on it, so another process could checkpoint and delete the write-ahead log under a
|
|
34
|
+
writer that went on writing into it. That can damage the store. The file is now created only when it is missing.
|
|
35
|
+
|
|
36
|
+
## 0.598.0 — 07.11.2026
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`runScopeCases`** (`@wirecat/cli-core/testing`): cases that each remote command of a program runs inside
|
|
41
|
+
cli-core's run, so `--record` keeps a run named after it, and that `--timeout` ends it when the service never
|
|
42
|
+
answers. A command beside the run answers under the process run and fails. `neverAnswers` is the `fetch` for the
|
|
43
|
+
silent run.
|
|
44
|
+
- **`inCommandRun(command, body)`** (`@wirecat/cli-core/cli`): runs `body` inside cli-core's run with the settings of
|
|
45
|
+
the app `run()` was given, which `run()` now hands every command (`environment.resolveSettings`).
|
|
46
|
+
|
|
47
|
+
### Changed — may break callers
|
|
48
|
+
|
|
49
|
+
- **`addMeetingCommands` takes `inRun`**, which `meetings sync` runs inside: pass `inCommandRun` from
|
|
50
|
+
`@wirecat/cli-core/cli`. The meetings module cannot reach cli-core's run itself.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **The shared CRM, calendar and meetings commands run inside cli-core's run.** `sync` and the deal and lead writes
|
|
55
|
+
from `addCrmCommands`, every command from `addCalendarCommands`, and `meetings sync` answered under the process
|
|
56
|
+
run: `--record` kept no run, and `runs show` could not find the id they answered. A CLI that wrapped them in a run
|
|
57
|
+
of its own can drop that.
|
|
58
|
+
- **An MCP server's `--permission` levels refuse a messenger write before it connects.** The early refusal of a change
|
|
59
|
+
on the messenger read the command's flags only, so `mcp serve --permission messages.send=deny` let the command
|
|
60
|
+
connect and left the refusal to the send guard. It now merges the server's levels as `baseContext` does.
|
|
61
|
+
`withAgentLevels(command, flags)` (`@wirecat/cli-core/cli`) is that merge, for a CLI's own resolver.
|
|
62
|
+
- **An app's `mcp serve` over stdio ends when it was stopped before it listened.** `serveAppOverStdio` waited only
|
|
63
|
+
for an abort event, which a signal that had already fired never sends, so the server hung with stdin open.
|
|
64
|
+
|
|
10
65
|
## 0.597.0 — 07.11.2026
|
|
11
66
|
|
|
12
67
|
### Added
|
|
@@ -3,7 +3,7 @@ import { annotate, metaOf, sharedCommand } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
4
4
|
import { clockMeta } from "@wirecat/cli-base/standards";
|
|
5
5
|
import { Argument, InvalidArgumentError, Option } from "commander";
|
|
6
|
-
import { environmentOf, outputFor, runBudgetOf } from "../../cli/context.js";
|
|
6
|
+
import { environmentOf, inCommandRun, outputFor, runBudgetOf } from "../../cli/context.js";
|
|
7
7
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
8
8
|
import { changesStore, ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
|
|
9
9
|
import { renderAnswer } from "../../cli/envelope.js";
|
|
@@ -77,17 +77,10 @@ const addCalendarCommandsDefinition = (program, options) => {
|
|
|
77
77
|
renderAnswer(command, output, body, view ?? ((shown) => localTimesView(shown, zone(), output.color)));
|
|
78
78
|
};
|
|
79
79
|
/** Opens the profile for `work` and closes it before the answer prints, also when `work` fails. */
|
|
80
|
-
const within =
|
|
80
|
+
const within = (command, work, view) => inCommandRun(command, async () => {
|
|
81
81
|
const run = await options.open(command, { readOnly: readsOnly(command) });
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
body = await work(run);
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
await run.close?.();
|
|
88
|
-
}
|
|
89
|
-
answer(command, body, view);
|
|
90
|
-
};
|
|
82
|
+
answer(command, await work(run).finally(() => run.close?.()), view);
|
|
83
|
+
});
|
|
91
84
|
const example = (command, ...lines) => annotate(command, { examples: lines.map((line) => `${options.app.command} personal ${line}`) });
|
|
92
85
|
const calendars = annotate(program.command("calendars").description("the account's calendars"), { effects: GROUP });
|
|
93
86
|
withQuery(example(annotate(calendars.command("list"), { effects: FETCH, offline: false }).description("list the account's calendars from the provider, and which of them sync"), "calendars list --json", "calendars list synced:true"), CALENDARS_QUERY).action(async (words, _opts, command) => {
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface BaseEnvironment {
|
|
|
34
34
|
answer?: (question: string) => Promise<string | null> | string | null;
|
|
35
35
|
/** Set by `run()`; a command's run is recorded only when it is known whose run it is. */
|
|
36
36
|
app?: AppIdentity;
|
|
37
|
+
/** Set by `run()`: the app's own, so a shared command group opens its run with the app's settings. */
|
|
38
|
+
resolveSettings?: (flags: GlobalFlags, options?: ResolveOptions) => Settings;
|
|
37
39
|
/** Set by `run()` from `process.platform` unless a test hands one in: the OS a command's paths and units are for. */
|
|
38
40
|
platform?: NodeJS.Platform;
|
|
39
41
|
/** The local speech recognizer in place of the downloaded model; tests hand one in. */
|
|
@@ -120,6 +122,10 @@ export declare const runBudgetOf: (command: Command, requests?: number) => RunBu
|
|
|
120
122
|
export declare const setExitCode: (command: Command, code: number) => void;
|
|
121
123
|
export declare const provide: (program: Command, environment: BaseEnvironment) => void;
|
|
122
124
|
export declare const environmentOf: <E extends BaseEnvironment = BaseEnvironment>(command: Command) => E;
|
|
125
|
+
/** The command's flags with an MCP server's `--permission` levels first: they reach a run on its agent, never in argv. */
|
|
126
|
+
export declare const withAgentLevels: <F extends {
|
|
127
|
+
permission?: string[];
|
|
128
|
+
}>(command: Command, flags: F) => F;
|
|
123
129
|
/**
|
|
124
130
|
* The input and output `readSecret` uses for this command: the run's own, so a prompt goes where the run's other
|
|
125
131
|
* output goes and a test that captures it sees it. `readSecret(prompt, { ...secretInputOf(this), echo: true })`.
|
|
@@ -166,4 +172,10 @@ export interface BaseContext {
|
|
|
166
172
|
type Resolve = (flags: GlobalFlags, options?: ResolveOptions) => Settings;
|
|
167
173
|
/** Everything a command needs that no messenger decides, resolved once. */
|
|
168
174
|
export declare const baseContext: (command: Command, resolveSettings: Resolve) => BaseContext;
|
|
175
|
+
/**
|
|
176
|
+
* A shared group's command inside cli-core's run, with the settings of the app `run()` was given: recorded, and ended
|
|
177
|
+
* by `--timeout` (WC-LIFE-5, WC-LIFE-15). The group's own opener still decides whether it needs a profile. A program
|
|
178
|
+
* parsed without `run()` has no run to open, so `body` just runs.
|
|
179
|
+
*/
|
|
180
|
+
export declare const inCommandRun: <T>(command: Command, body: (events: EventSink) => Promise<T>) => Promise<T>;
|
|
169
181
|
export {};
|
package/dist/cli/context.js
CHANGED
|
@@ -37,6 +37,11 @@ export const provide = (program, environment) => {
|
|
|
37
37
|
environments.set(program, environment);
|
|
38
38
|
};
|
|
39
39
|
export const environmentOf = (command) => (environments.get(rootOf(command)) ?? {});
|
|
40
|
+
/** The command's flags with an MCP server's `--permission` levels first: they reach a run on its agent, never in argv. */
|
|
41
|
+
export const withAgentLevels = (command, flags) => {
|
|
42
|
+
const overrides = environmentOf(command).agent?.overrides ?? [];
|
|
43
|
+
return overrides.length > 0 ? { ...flags, permission: [...overrides, ...(flags.permission ?? [])] } : flags;
|
|
44
|
+
};
|
|
40
45
|
/**
|
|
41
46
|
* The input and output `readSecret` uses for this command: the run's own, so a prompt goes where the run's other
|
|
42
47
|
* output goes and a test that captures it sees it. `readSecret(prompt, { ...secretInputOf(this), echo: true })`.
|
|
@@ -70,9 +75,7 @@ export const outputFor = (command) => {
|
|
|
70
75
|
export const baseContext = (command, resolveSettings) => {
|
|
71
76
|
const environment = environmentOf(command);
|
|
72
77
|
const env = environment.env ?? process.env;
|
|
73
|
-
const
|
|
74
|
-
const overrides = environment.agent?.overrides ?? [];
|
|
75
|
-
const settings = resolveSettings(overrides.length > 0 ? { ...flags, permission: [...overrides, ...(flags.permission ?? [])] } : flags, { env });
|
|
78
|
+
const settings = resolveSettings(withAgentLevels(command, command.optsWithGlobals()), { env });
|
|
76
79
|
const { renderer, format, color, streams, compact, full } = resolveOutput({
|
|
77
80
|
...settings,
|
|
78
81
|
env,
|
|
@@ -141,6 +144,17 @@ export const baseContext = (command, resolveSettings) => {
|
|
|
141
144
|
},
|
|
142
145
|
};
|
|
143
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* A shared group's command inside cli-core's run, with the settings of the app `run()` was given: recorded, and ended
|
|
149
|
+
* by `--timeout` (WC-LIFE-5, WC-LIFE-15). The group's own opener still decides whether it needs a profile. A program
|
|
150
|
+
* parsed without `run()` has no run to open, so `body` just runs.
|
|
151
|
+
*/
|
|
152
|
+
export const inCommandRun = (command, body) => {
|
|
153
|
+
const { resolveSettings } = environmentOf(command);
|
|
154
|
+
if (!resolveSettings)
|
|
155
|
+
return body(() => { });
|
|
156
|
+
return baseContext(command, (flags, options) => resolveSettings(flags, { ...options, needsProfile: false })).run(body);
|
|
157
|
+
};
|
|
144
158
|
/** `messages send` — the words that name commands, never the arguments: those can be a message. */
|
|
145
159
|
const commandPath = (command) => {
|
|
146
160
|
const words = [];
|
|
@@ -7,7 +7,7 @@ import { SendJournal, sendsPathFor } from "../sends/journal.js";
|
|
|
7
7
|
import { CEILING_KEY, keyForCommand, levelFor, writeOfKey } from "../sends/permissions.js";
|
|
8
8
|
import { newOperationId } from "../sends/send-id.js";
|
|
9
9
|
import { commandPathOf } from "./command-contract.js";
|
|
10
|
-
import {
|
|
10
|
+
import { withAgentLevels } from "./context.js";
|
|
11
11
|
import { changesMessenger } from "./effects.js";
|
|
12
12
|
import { refuseUnknownPermissionKeys } from "./permission-keys.js";
|
|
13
13
|
import { NONE_NAMED, settingsFor } from "./settings.js";
|
|
@@ -33,7 +33,7 @@ export const refuseWriteBeforeConnecting = (action, { app, env, dryRun, resolve
|
|
|
33
33
|
const key = keyForCommand(path);
|
|
34
34
|
if (!key)
|
|
35
35
|
return;
|
|
36
|
-
const settings = (resolve ?? settingsFor(app).resolveSettings)(action.optsWithGlobals(), {
|
|
36
|
+
const settings = (resolve ?? settingsFor(app).resolveSettings)(withAgentLevels(action, action.optsWithGlobals()), {
|
|
37
37
|
env,
|
|
38
38
|
needsProfile: false,
|
|
39
39
|
readOnly: true,
|
|
@@ -91,9 +91,11 @@ export const refuseByCommandPermissions = (action, permissions, { app, env, dryR
|
|
|
91
91
|
const preview = dryRun && app.messenger === false;
|
|
92
92
|
if (lock && effects?.remote === "write" && !preview)
|
|
93
93
|
throw readOnlyRefusal(lock, `run ${commandPathOf(action).join(" ")}`);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const settings = resolve(withAgentLevels(action, action.optsWithGlobals()), {
|
|
95
|
+
env,
|
|
96
|
+
needsProfile: false,
|
|
97
|
+
readOnly: true,
|
|
98
|
+
});
|
|
97
99
|
refuseUnknownPermissionKeys(action, settings, app.command, permissions);
|
|
98
100
|
if (dryRun || settings.sources?.profile === NONE_NAMED)
|
|
99
101
|
return;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export { type CompactOptions, compactText, entityRows, GENERIC_ROW, type RowColu
|
|
|
66
66
|
export { refuseUnknownKey } from "./config-command.js";
|
|
67
67
|
export { type ConfigIssue, type ConfigIssueKind, checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
|
|
68
68
|
export { loadSnakeCaseConfig } from "./config-keys.js";
|
|
69
|
-
export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, runBudgetOf, secretInputOf, } from "./context.js";
|
|
69
|
+
export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, inCommandRun, outputFor, provide, runBudgetOf, secretInputOf, withAgentLevels, } from "./context.js";
|
|
70
70
|
export { addContractInfo, type ContractEvidenceOptions, contractEvidence } from "./contract-evidence.js";
|
|
71
71
|
export { sharedCommandTemplate } from "./contract-templates.js";
|
|
72
72
|
export { type Closeable, withDeadline } from "./deadline.js";
|
|
@@ -99,6 +99,7 @@ export { accountFileFor, profilesHere, rememberAccount } from "./messenger/accou
|
|
|
99
99
|
export { terminalAsker } from "./messenger/ask.js";
|
|
100
100
|
export type { CompletionOptions } from "./messenger/complete-command.js";
|
|
101
101
|
export { type ConnectOptions, connectFor, type Messenger, type MessengerContext, messengerContext, type ProfileState, type ReadConnection, type StoredMessages, } from "./messenger/context.js";
|
|
102
|
+
export { DOCTOR_DEEP_OPTION, damageNotes } from "./messenger/doctor-command.js";
|
|
102
103
|
export { COMMAND_NEEDS } from "./messenger/lacks.js";
|
|
103
104
|
export { httpTokenFile, type McpEnvironment, type McpHost, serverEntry } from "./messenger/mcp-command.js";
|
|
104
105
|
export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
|
|
@@ -131,5 +132,6 @@ export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, a
|
|
|
131
132
|
export { assertSharedContracts, customizeSharedCommand, hasSharedContract, overrideSharedContract, type SharedContractOverride, sharedContractOverrides, sharedContracts, } from "./shared-contracts.js";
|
|
132
133
|
export { runnable, withSafety } from "./steps.js";
|
|
133
134
|
export { type StoreInfo, sharedStoreCommand, storeInfo, storeInfoCommand } from "./store-info.js";
|
|
135
|
+
export { type RecoveryCheck, type RecoveryRun, recoveryNotes, storeRecoverCommand, storeRecoveryCheck, storeRecoveryRun, } from "./store-recover.js";
|
|
134
136
|
export { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } from "./store-settings.js";
|
|
135
137
|
export { restartServers, type UpgradeContext } from "./upgrade-command.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -57,7 +57,7 @@ export { compactText, entityRows, GENERIC_ROW, rowShapeOf, rowsOfEntity, SHARED_
|
|
|
57
57
|
export { refuseUnknownKey } from "./config-command.js";
|
|
58
58
|
export { checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
|
|
59
59
|
export { loadSnakeCaseConfig } from "./config-keys.js";
|
|
60
|
-
export { baseContext, environmentOf, outputFor, provide, runBudgetOf, secretInputOf, } from "./context.js";
|
|
60
|
+
export { baseContext, environmentOf, inCommandRun, outputFor, provide, runBudgetOf, secretInputOf, withAgentLevels, } from "./context.js";
|
|
61
61
|
export { addContractInfo, contractEvidence } from "./contract-evidence.js";
|
|
62
62
|
export { sharedCommandTemplate } from "./contract-templates.js";
|
|
63
63
|
export { withDeadline } from "./deadline.js";
|
|
@@ -89,6 +89,7 @@ export { readMeetingSeriesInventory } from "./meeting-series-inventory-read.js";
|
|
|
89
89
|
export { accountFileFor, profilesHere, rememberAccount } from "./messenger/accounts.js";
|
|
90
90
|
export { terminalAsker } from "./messenger/ask.js";
|
|
91
91
|
export { connectFor, messengerContext, } from "./messenger/context.js";
|
|
92
|
+
export { DOCTOR_DEEP_OPTION, damageNotes } from "./messenger/doctor-command.js";
|
|
92
93
|
export { COMMAND_NEEDS } from "./messenger/lacks.js";
|
|
93
94
|
export { httpTokenFile, serverEntry } from "./messenger/mcp-command.js";
|
|
94
95
|
export { messageItem } from "./messenger/message-items.js";
|
|
@@ -120,5 +121,6 @@ export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, a
|
|
|
120
121
|
export { assertSharedContracts, customizeSharedCommand, hasSharedContract, overrideSharedContract, sharedContractOverrides, sharedContracts, } from "./shared-contracts.js";
|
|
121
122
|
export { runnable, withSafety } from "./steps.js";
|
|
122
123
|
export { sharedStoreCommand, storeInfo, storeInfoCommand } from "./store-info.js";
|
|
124
|
+
export { recoveryNotes, storeRecoverCommand, storeRecoveryCheck, storeRecoveryRun, } from "./store-recover.js";
|
|
123
125
|
export { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } from "./store-settings.js";
|
|
124
126
|
export { restartServers } from "./upgrade-command.js";
|
|
@@ -8,6 +8,9 @@ import { type Messenger } from "./context.js";
|
|
|
8
8
|
* open — each is a field in the answer, never an exception.
|
|
9
9
|
*/
|
|
10
10
|
export declare const doctorCommand: (messenger: Messenger) => Command;
|
|
11
|
+
export declare const DOCTOR_DEEP_OPTION = "also read every page of the store and check every search index (about 30 s for 6 GB); without it, only the file header and schema";
|
|
12
|
+
/** The plain-words account of a damaged store, under the answer. */
|
|
13
|
+
export declare const damageNotes: (store: unknown) => string[];
|
|
11
14
|
/** What the messenger asked this profile to wait for, still in force — what the next command would be refused. */
|
|
12
15
|
export declare const floodState: (memory: FloodMemory) => {
|
|
13
16
|
path: string;
|
|
@@ -49,10 +49,24 @@ export const doctorCommand = (messenger) => {
|
|
|
49
49
|
renderAnswer(this, { ...output, ...(renderer ? { renderer } : {}) }, itemOf(report, { next: unchecked ? [nextStep("doctor.online", { args: { online: true } })] : [] }));
|
|
50
50
|
for (const fix of fixes)
|
|
51
51
|
(renderer ?? output.renderer).note(`to make it private: ${fix}`);
|
|
52
|
+
for (const line of damageNotes(report.store))
|
|
53
|
+
(renderer ?? output.renderer).note(line);
|
|
52
54
|
});
|
|
53
55
|
command.addCommand(reportCommand(messenger));
|
|
54
56
|
return command;
|
|
55
57
|
};
|
|
58
|
+
export const DOCTOR_DEEP_OPTION = "also read every page of the store and check every search index (about 30 s for 6 GB); without it, only the file header and schema";
|
|
59
|
+
/** The shared doctor stays quick; `store recover` is its deep check, and a CLI may add `--deep` to its own doctor. */
|
|
60
|
+
const summaryOptions = (command, app) => ({
|
|
61
|
+
deep: command.opts().deep === true,
|
|
62
|
+
command: app,
|
|
63
|
+
});
|
|
64
|
+
/** The plain-words account of a damaged store, under the answer. */
|
|
65
|
+
export const damageNotes = (store) => {
|
|
66
|
+
const damage = store
|
|
67
|
+
?.damage;
|
|
68
|
+
return damage ? [damage.what, damage.external_unaffected, ...damage.steps.map((step) => `next: ${step}`)] : [];
|
|
69
|
+
};
|
|
56
70
|
const INCLUDES = [
|
|
57
71
|
"the version, the runtime and the operating system",
|
|
58
72
|
"what `doctor` answers: paths with the home folder shown as ~, the store's schema and counts, the messenger's own checks",
|
|
@@ -149,7 +163,7 @@ const diagnose = async (command, messenger) => {
|
|
|
149
163
|
cli,
|
|
150
164
|
config: { path, found: existsSync(path), issues: [] },
|
|
151
165
|
settings: { error: messageOf(error) },
|
|
152
|
-
store: snakeKeys(await storeSummary(env)),
|
|
166
|
+
store: snakeKeys(await storeSummary(env, summaryOptions(command, app.command))),
|
|
153
167
|
runs: { directory: runsDirFor(app, env), kept: listRuns(runsDirFor(app, env)).length },
|
|
154
168
|
files: filesReport(files),
|
|
155
169
|
},
|
|
@@ -174,7 +188,7 @@ const diagnose = async (command, messenger) => {
|
|
|
174
188
|
cli,
|
|
175
189
|
profile: null,
|
|
176
190
|
config,
|
|
177
|
-
store: snakeKeys(await storeSummary(env)),
|
|
191
|
+
store: snakeKeys(await storeSummary(env, summaryOptions(command, app.command))),
|
|
178
192
|
readers: engineStates(),
|
|
179
193
|
runs: { directory: runsDirFor(app, env), kept: listRuns(runsDirFor(app, env)).length },
|
|
180
194
|
files: filesReport(files),
|
|
@@ -210,7 +224,7 @@ const diagnose = async (command, messenger) => {
|
|
|
210
224
|
account: { remembered: account?.account ?? null },
|
|
211
225
|
// A session file on disk says nothing about whether the messenger still accepts it.
|
|
212
226
|
login: { state: "not checked" },
|
|
213
|
-
store: snakeKeys(await storeSummary(env)),
|
|
227
|
+
store: snakeKeys(await storeSummary(env, summaryOptions(command, app.command))),
|
|
214
228
|
readers: engineStates(),
|
|
215
229
|
sends: sendsState(new SendJournal(sendsPathFor(app, profile, env))),
|
|
216
230
|
flood: floodState(new FloodMemory(flood)),
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import type { CacheDatabase } from "../../store/driver.js";
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. `health` is the quick
|
|
6
|
+
* check unless `deep`; `damage` says in plain words what a failed one means and what to run.
|
|
7
|
+
*/
|
|
8
|
+
export declare const storeSummary: (env: NodeJS.ProcessEnv, { deep, command }?: {
|
|
9
|
+
deep?: boolean;
|
|
10
|
+
command?: string;
|
|
11
|
+
}) => Promise<{
|
|
6
12
|
path: string;
|
|
7
13
|
exists: boolean;
|
|
8
|
-
error?: undefined;
|
|
9
14
|
} | {
|
|
15
|
+
health: {
|
|
16
|
+
checked: "deep" | "quick";
|
|
17
|
+
covers: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
ms: number;
|
|
20
|
+
problems?: number | undefined;
|
|
21
|
+
damagedTables?: import("../../store/health.js").DamagedTable[] | undefined;
|
|
22
|
+
damagedSearchIndexes?: string[] | undefined;
|
|
23
|
+
};
|
|
24
|
+
damage?: import("../../store/damage.js").DamageExplanation | undefined;
|
|
10
25
|
path: string;
|
|
11
26
|
exists: boolean;
|
|
12
27
|
schema: number;
|
|
@@ -15,6 +30,16 @@ export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
|
15
30
|
chats?: number | undefined;
|
|
16
31
|
messages?: number | undefined;
|
|
17
32
|
} | {
|
|
33
|
+
health: {
|
|
34
|
+
checked: "deep" | "quick";
|
|
35
|
+
covers: string;
|
|
36
|
+
ok: boolean;
|
|
37
|
+
ms: number;
|
|
38
|
+
problems?: number | undefined;
|
|
39
|
+
damagedTables?: import("../../store/health.js").DamagedTable[] | undefined;
|
|
40
|
+
damagedSearchIndexes?: string[] | undefined;
|
|
41
|
+
};
|
|
42
|
+
damage?: import("../../store/damage.js").DamageExplanation | undefined;
|
|
18
43
|
path: string;
|
|
19
44
|
exists: boolean;
|
|
20
45
|
error: string;
|
|
@@ -8,6 +8,8 @@ import { RULES_VERSION } from "../../conversations/link.js";
|
|
|
8
8
|
import { syncsOneChat } from "../../ports/traits.js";
|
|
9
9
|
import { isSealed, sealFile, unsealFile } from "../../sealed.js";
|
|
10
10
|
import { fileSchemaOf, SPEAKS, writableBy } from "../../store/compatibility.js";
|
|
11
|
+
import { explainDamage } from "../../store/damage.js";
|
|
12
|
+
import { QUICK_HEALTH_CHECKS, storeHealth } from "../../store/health.js";
|
|
11
13
|
import { migrate } from "../../store/migrations.js";
|
|
12
14
|
import { openCache } from "../../store/open.js";
|
|
13
15
|
import { storePath } from "../../store/path.js";
|
|
@@ -18,6 +20,7 @@ import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
|
18
20
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
19
21
|
import { lineOf, nextStep } from "../steps.js";
|
|
20
22
|
import { storeInfoCommand } from "../store-info.js";
|
|
23
|
+
import { storeRecoverCommand } from "../store-recover.js";
|
|
21
24
|
import { answerOf } from "./ask.js";
|
|
22
25
|
import { ENCRYPT_OPTION, passwordOf } from "./password.js";
|
|
23
26
|
import { servingProfiles } from "./serve-command.js";
|
|
@@ -55,11 +58,28 @@ const readingOnly = async (path, read) => {
|
|
|
55
58
|
database.close();
|
|
56
59
|
}
|
|
57
60
|
};
|
|
58
|
-
/**
|
|
59
|
-
|
|
61
|
+
/**
|
|
62
|
+
* `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. `health` is the quick
|
|
63
|
+
* check unless `deep`; `damage` says in plain words what a failed one means and what to run.
|
|
64
|
+
*/
|
|
65
|
+
export const storeSummary = async (env, { deep = false, command } = {}) => {
|
|
60
66
|
const path = storePath(env);
|
|
61
67
|
if (!existsSync(path))
|
|
62
68
|
return { path, exists: false };
|
|
69
|
+
const checked = await storeHealth(path, { deep });
|
|
70
|
+
const health = {
|
|
71
|
+
checked: checked.checked,
|
|
72
|
+
covers: deep ? "every page and every search index" : QUICK_HEALTH_CHECKS,
|
|
73
|
+
ok: checked.ok,
|
|
74
|
+
ms: checked.ms,
|
|
75
|
+
...(checked.problems ? { problems: checked.problems } : {}),
|
|
76
|
+
...(checked.damagedTables?.length ? { damagedTables: checked.damagedTables } : {}),
|
|
77
|
+
...(checked.searchIndexes?.some((index) => !index.ok)
|
|
78
|
+
? { damagedSearchIndexes: checked.searchIndexes.filter((index) => !index.ok).map((index) => index.name) }
|
|
79
|
+
: {}),
|
|
80
|
+
};
|
|
81
|
+
const damage = command ? explainDamage(checked, command) : null;
|
|
82
|
+
const healthOf = { health, ...(damage ? { damage } : {}) };
|
|
63
83
|
try {
|
|
64
84
|
return await reading(path, (database) => {
|
|
65
85
|
const { version, writable } = schemaOf(database);
|
|
@@ -70,11 +90,12 @@ export const storeSummary = async (env) => {
|
|
|
70
90
|
speaks: SPEAKS,
|
|
71
91
|
writable,
|
|
72
92
|
...(version > 0 ? { chats: count(database, "chats"), messages: count(database, "messages") } : {}),
|
|
93
|
+
...healthOf,
|
|
73
94
|
};
|
|
74
95
|
});
|
|
75
96
|
}
|
|
76
97
|
catch (error) {
|
|
77
|
-
return { path, exists: true, error: messageOf(error) };
|
|
98
|
+
return { path, exists: true, error: messageOf(error), ...healthOf };
|
|
78
99
|
}
|
|
79
100
|
};
|
|
80
101
|
/** A maintenance answer: one item, snake_case, its hints as steps where a registered one fits. */
|
|
@@ -91,6 +112,7 @@ export const storeMaintenanceCommands = (messenger) => [
|
|
|
91
112
|
resetCommand(messenger),
|
|
92
113
|
copiesCommand(),
|
|
93
114
|
storeTablesCommand(messenger),
|
|
115
|
+
storeRecoverCommand({ command: messenger.app.command, version: messenger.app.version }),
|
|
94
116
|
];
|
|
95
117
|
/** Renaming tables under a running `serve` would pull them from under its statements. */
|
|
96
118
|
const refuseWhileServing = (command, messenger) => {
|
package/dist/cli/program.js
CHANGED
|
@@ -228,6 +228,9 @@ const runOnce = async (argv, definition, options) => {
|
|
|
228
228
|
commandSignal: control.signal,
|
|
229
229
|
trackCloseable: control.trackCloseable,
|
|
230
230
|
app: definition.app,
|
|
231
|
+
// `settingsFor`'s, which every CLI passes; the definition's type is looser only so a test can stub it.
|
|
232
|
+
resolveSettings: (definition.configuration ?? settingsFor(definition.app))
|
|
233
|
+
.resolveSettings,
|
|
231
234
|
...(definition.rows ? { rows: definition.rows } : {}),
|
|
232
235
|
platform: options.platform ?? process.platform,
|
|
233
236
|
exit,
|
package/dist/cli/store-info.d.ts
CHANGED
|
@@ -9,4 +9,7 @@ export type StoreInfo = v.InferOutput<typeof storeInfoEntity>;
|
|
|
9
9
|
export declare const storeInfo: (env: NodeJS.ProcessEnv) => Promise<StoreInfo>;
|
|
10
10
|
export declare const storeInfoCommand: () => Command;
|
|
11
11
|
/** `store info` for a tool with no `store` command of its own. */
|
|
12
|
-
export declare const sharedStoreCommand: (
|
|
12
|
+
export declare const sharedStoreCommand: (app?: {
|
|
13
|
+
command: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
}) => Command;
|
package/dist/cli/store-info.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileFacts, openSqlite, pendingNormalization, rowCounts, searchIndexStat
|
|
|
7
7
|
import { environmentOf, outputFor } from "./context.js";
|
|
8
8
|
import { LOCAL_READ } from "./effects.js";
|
|
9
9
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
10
|
+
import { storeRecoverCommand } from "./store-recover.js";
|
|
10
11
|
/** Before version 6 the column does not exist, and a file behind this build is not migrated here. */
|
|
11
12
|
const pendingIfKnown = (database) => database.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = 'normalized_text'").get()
|
|
12
13
|
? pendingNormalization(database)
|
|
@@ -67,6 +68,9 @@ export const storeInfoCommand = () => annotate(new Command("info"), { output: {
|
|
|
67
68
|
renderAnswer(this, outputFor(this), itemOf(answer));
|
|
68
69
|
});
|
|
69
70
|
/** `store info` for a tool with no `store` command of its own. */
|
|
70
|
-
export const sharedStoreCommand = () =>
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
export const sharedStoreCommand = (app) => {
|
|
72
|
+
const store = annotate(new Command("store"), { effects: LOCAL_READ })
|
|
73
|
+
.description("the shared local store every WireCat tool reads")
|
|
74
|
+
.addCommand(storeInfoCommand());
|
|
75
|
+
return app ? store.addCommand(storeRecoverCommand(app)) : store;
|
|
76
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type DamageExplanation } from "../store/damage.js";
|
|
3
|
+
import { type RecoverOptions, type Recovery } from "../store/recover.js";
|
|
4
|
+
export interface RecoveryCheck {
|
|
5
|
+
path: string;
|
|
6
|
+
exists: boolean;
|
|
7
|
+
ok: boolean;
|
|
8
|
+
seconds?: number;
|
|
9
|
+
problems?: number;
|
|
10
|
+
damagedTables?: {
|
|
11
|
+
table: string | null;
|
|
12
|
+
problems: number;
|
|
13
|
+
sample: string;
|
|
14
|
+
}[];
|
|
15
|
+
damagedSearchIndexes?: {
|
|
16
|
+
name: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}[];
|
|
19
|
+
rows?: Record<string, number | null>;
|
|
20
|
+
explanation?: DamageExplanation | null;
|
|
21
|
+
inUse?: {
|
|
22
|
+
pid: number;
|
|
23
|
+
name: string;
|
|
24
|
+
}[] | null;
|
|
25
|
+
timers?: string[] | null;
|
|
26
|
+
sqlite3?: {
|
|
27
|
+
version: string | null;
|
|
28
|
+
usable: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** The commands to run, in order. */
|
|
31
|
+
next: string[];
|
|
32
|
+
}
|
|
33
|
+
/** `store recover`: what is wrong with the store and what to run, reading every page; changes nothing. */
|
|
34
|
+
export declare const storeRecoveryCheck: (env: NodeJS.ProcessEnv, command: string) => Promise<RecoveryCheck>;
|
|
35
|
+
export type RecoveryRun = Omit<Recovery, "health" | "copyErrors"> & {
|
|
36
|
+
/** `sqlite3` reported these while copying; most are rows that broke a constraint and were skipped. */
|
|
37
|
+
copyErrors: number;
|
|
38
|
+
checkedOk: boolean;
|
|
39
|
+
/** The commands that fetch again what was lost, then the timers to start. */
|
|
40
|
+
next: string[];
|
|
41
|
+
};
|
|
42
|
+
/** `store recover run`: refuses while a WireCat timer is scheduled, so none writes to the file being replaced. */
|
|
43
|
+
export declare const storeRecoveryRun: (env: NodeJS.ProcessEnv, { command, version, note, timers: scheduled, copy, }: {
|
|
44
|
+
command: string;
|
|
45
|
+
version?: string;
|
|
46
|
+
note?: (text: string) => void;
|
|
47
|
+
timers?: () => string[] | undefined;
|
|
48
|
+
copy?: RecoverOptions["copy"];
|
|
49
|
+
}) => Promise<RecoveryRun>;
|
|
50
|
+
/** The lines a person reads under the answer; the JSON carries the same in its fields. */
|
|
51
|
+
export declare const recoveryNotes: (answer: RecoveryCheck | RecoveryRun) => string[];
|
|
52
|
+
/** `store recover` and `store recover run`, for a CLI whose commands run on cli-core's context. */
|
|
53
|
+
export declare const storeRecoverCommand: ({ command, version }: {
|
|
54
|
+
command: string;
|
|
55
|
+
version?: string;
|
|
56
|
+
}) => Command;
|