@logicseek/hello-cc 0.1.9 → 1.0.1
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 +136 -0
- package/README.md +50 -3
- package/README.zh-CN.md +48 -3
- package/bin/hcc.mjs +3 -5656
- package/docs/README.md +8 -0
- package/docs/README.zh-CN.md +6 -0
- package/docs/architecture.md +29 -18
- package/docs/architecture.zh-CN.md +20 -10
- package/docs/commands.md +13 -4
- package/docs/commands.zh-CN.md +13 -4
- package/docs/guide.md +97 -3
- package/docs/guide.zh-CN.md +89 -2
- package/lib/cli/app.mjs +644 -0
- package/lib/cli/commands/coordination.mjs +249 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +1002 -0
- package/lib/cli/commands/hook.mjs +205 -0
- package/lib/cli/commands/install.mjs +294 -0
- package/lib/cli/commands/lifecycle.mjs +101 -0
- package/lib/cli/commands/lock.mjs +197 -0
- package/lib/cli/commands/misc.mjs +76 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +249 -0
- package/lib/cli/commands/query.mjs +96 -0
- package/lib/cli/commands/run.mjs +463 -0
- package/lib/cli/commands/task.mjs +405 -0
- package/lib/cli/commands/team.mjs +131 -0
- package/lib/cli/commands/tmux.mjs +838 -0
- package/lib/cli/commands/up.mjs +135 -0
- package/lib/cli-args.mjs +16 -2
- package/lib/coordination-state.mjs +112 -51
- package/lib/core/coordination/automation.mjs +5 -8
- package/lib/core/coordination/clock-safety.mjs +323 -0
- package/lib/core/coordination/event-retention.mjs +17 -0
- package/lib/core/coordination/gc-plan.mjs +614 -0
- package/lib/core/coordination/lease-renewal.mjs +102 -0
- package/lib/core/coordination/lock-evidence.mjs +77 -0
- package/lib/core/coordination/optimistic-evidence.mjs +26 -0
- package/lib/core/coordination/tasks.mjs +156 -36
- package/lib/core/peers/evidence-runtime.mjs +312 -0
- package/lib/core/peers/evidence.mjs +103 -0
- package/lib/core/peers/hook-owner.mjs +32 -0
- package/lib/core/peers/liveness.mjs +44 -4
- package/lib/core/peers/peer-helpers.mjs +121 -0
- package/lib/core/peers/session.mjs +8 -2
- package/lib/core/peers/tmux-safety.mjs +290 -0
- package/lib/core/sessions/launch.mjs +13 -1
- package/lib/db/connection.mjs +104 -0
- package/lib/db/events.mjs +34 -0
- package/lib/db/migration-backup.mjs +225 -0
- package/lib/db/schema.mjs +102 -5
- package/lib/db/stores/peers.mjs +34 -0
- package/lib/integrations/peers/identity.mjs +62 -16
- package/lib/integrations/shims/script.mjs +111 -9
- package/lib/process/identity.mjs +222 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +48 -0
- package/lib/runtime/buffer-evidence.mjs +336 -0
- package/lib/runtime/buffer-gc-protocol.mjs +167 -0
- package/lib/runtime/buffer-gc.mjs +270 -0
- package/lib/runtime/client.mjs +63 -7
- package/lib/runtime/fatal-shutdown.mjs +54 -0
- package/lib/runtime/project-path.mjs +139 -0
- package/lib/runtime/projects.mjs +320 -40
- package/lib/runtime/state.mjs +227 -27
- package/lib/shared/cleanup-error.mjs +54 -0
- package/lib/shared/clock-grace.mjs +106 -0
- package/lib/shared/errors.mjs +9 -0
- package/lib/shared/file-lock.mjs +377 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +250 -0
- package/lib/terminal/tmux.mjs +131 -27
- package/lib/tmux.mjs +4 -0
- package/lib/ui/help.mjs +53 -15
- package/lib/web/api-version.mjs +42 -0
- package/lib/web/auto-attach.mjs +185 -0
- package/lib/web/buffer-gc-runtime.mjs +175 -0
- package/lib/web/cookie-auth.mjs +119 -0
- package/lib/web/external-sessions.mjs +254 -0
- package/lib/web/http-routes.mjs +542 -0
- package/lib/web/http.mjs +135 -6
- package/lib/web/liveness-reaper.mjs +145 -0
- package/lib/web/peer-actions.mjs +132 -23
- package/lib/web/project-contexts.mjs +183 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +717 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +335 -0
- package/lib/web/tls.mjs +365 -0
- package/lib/web/tmux-sessions.mjs +1361 -0
- package/lib/web/tmux-stream.mjs +166 -0
- package/lib/web/ui-template.mjs +152 -20
- package/package.json +6 -3
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5404 -273
- package/docs/architecture-split-review.md +0 -217
- package/docs/layout-split-stack-audit.md +0 -298
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Coordination CLI commands extracted from bin/hcc.mjs: handoff, event,
|
|
2
|
+
// heartbeat, ask, broadcast, inject. Factory pattern with injected deps.
|
|
3
|
+
|
|
4
|
+
import { RUNTIME_SESSION_MUTATION_TIMEOUT_MS } from '../../runtime/client.mjs';
|
|
5
|
+
|
|
6
|
+
export function createCoordinationCommands(deps) {
|
|
7
|
+
const {
|
|
8
|
+
connect,
|
|
9
|
+
now,
|
|
10
|
+
iso,
|
|
11
|
+
tx,
|
|
12
|
+
addEvent,
|
|
13
|
+
auditPayload,
|
|
14
|
+
touchCurrentPeer,
|
|
15
|
+
resolveCurrentPeer,
|
|
16
|
+
registerProjectActivity,
|
|
17
|
+
parseOpts,
|
|
18
|
+
intOpt,
|
|
19
|
+
required,
|
|
20
|
+
positiveSafeIntOpt,
|
|
21
|
+
wantsHelp,
|
|
22
|
+
helpHandoff,
|
|
23
|
+
helpEvent,
|
|
24
|
+
helpAsk,
|
|
25
|
+
helpBroadcast,
|
|
26
|
+
helpInject,
|
|
27
|
+
printResult,
|
|
28
|
+
table,
|
|
29
|
+
CliError,
|
|
30
|
+
DEFAULT_LOCK_TTL,
|
|
31
|
+
leaseDeadline,
|
|
32
|
+
sendMessage,
|
|
33
|
+
readRuntime,
|
|
34
|
+
runtimeRequest,
|
|
35
|
+
observeLockClockSafety,
|
|
36
|
+
peerEvidenceFromDb,
|
|
37
|
+
renewOwnedLocks,
|
|
38
|
+
normalizeListText,
|
|
39
|
+
changedFiles
|
|
40
|
+
} = deps;
|
|
41
|
+
|
|
42
|
+
async function cmdHandoff(ctx, args) {
|
|
43
|
+
const sub = args[0];
|
|
44
|
+
if (!sub || wantsHelp(args)) return helpHandoff();
|
|
45
|
+
if (sub === 'create') return handoffCreate(ctx, args.slice(1));
|
|
46
|
+
if (sub === 'list') return handoffList(ctx, args.slice(1));
|
|
47
|
+
throw new CliError('BAD_ARGS', `Unknown handoff command: ${sub}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handoffCreate(ctx, args) {
|
|
51
|
+
const opts = parseOpts(args);
|
|
52
|
+
const identity = resolveCurrentPeer(ctx, opts, 'from', 'shell');
|
|
53
|
+
const from = identity.id;
|
|
54
|
+
const taskId = intOpt(opts, 'task', null);
|
|
55
|
+
const to = opts.to || null;
|
|
56
|
+
const summary = required(opts, 'summary');
|
|
57
|
+
const files = opts['changed-files']
|
|
58
|
+
? normalizeListText(opts['changed-files'])
|
|
59
|
+
: JSON.stringify(changedFiles(ctx.cwd));
|
|
60
|
+
const tests = normalizeListText(opts.tests, []);
|
|
61
|
+
const risks = normalizeListText(opts.risks, []);
|
|
62
|
+
const db = connect(ctx);
|
|
63
|
+
touchCurrentPeer(db, ctx, identity, 'idle', 'shell');
|
|
64
|
+
const id = tx(db, () => {
|
|
65
|
+
const info = db.prepare(`
|
|
66
|
+
INSERT INTO handoffs(task_id, from_peer, to_peer, summary, changed_files, tests, risks, created_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
`).run(taskId, from, to, summary, files, tests, risks, now());
|
|
69
|
+
const handoffId = Number(info.lastInsertRowid);
|
|
70
|
+
addEvent(db, 'handoff.created', from, taskId, { handoff_id: handoffId, to });
|
|
71
|
+
if (to) sendMessage(db, from, to, taskId, 'handoff', `Handoff #${handoffId}: ${summary}`);
|
|
72
|
+
return handoffId;
|
|
73
|
+
});
|
|
74
|
+
printResult(ctx, { id, task_id: taskId, from, to, summary, changed_files: files, tests, risks }, (data) => `created handoff #${data.id}${data.to ? ` -> ${data.to}` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function handoffList(ctx, args) {
|
|
78
|
+
const opts = parseOpts(args);
|
|
79
|
+
const taskId = intOpt(opts, 'task', null);
|
|
80
|
+
const limit = intOpt(opts, 'limit', 20);
|
|
81
|
+
const db = connect(ctx);
|
|
82
|
+
const rows = taskId
|
|
83
|
+
? db.prepare('SELECT * FROM handoffs WHERE task_id = ? ORDER BY id DESC LIMIT ?').all(taskId, limit)
|
|
84
|
+
: db.prepare('SELECT * FROM handoffs ORDER BY id DESC LIMIT ?').all(limit);
|
|
85
|
+
printResult(ctx, rows, (data) => table(data, [
|
|
86
|
+
{ label: 'id', value: (r) => `#${r.id}` },
|
|
87
|
+
{ label: 'task', value: (r) => r.task_id ? `#${r.task_id}` : '' },
|
|
88
|
+
{ label: 'from', value: (r) => r.from_peer },
|
|
89
|
+
{ label: 'to', value: (r) => r.to_peer || '' },
|
|
90
|
+
{ label: 'time', value: (r) => iso(r.created_at) },
|
|
91
|
+
{ label: 'summary', value: (r) => r.summary }
|
|
92
|
+
]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function cmdEvent(ctx, args) {
|
|
96
|
+
const sub = args[0];
|
|
97
|
+
if (!sub || wantsHelp(args)) return helpEvent();
|
|
98
|
+
if (sub === 'tail') return eventTail(ctx, args.slice(1));
|
|
99
|
+
throw new CliError('BAD_ARGS', `Unknown event command: ${sub}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function eventTail(ctx, args) {
|
|
103
|
+
const opts = parseOpts(args);
|
|
104
|
+
const limit = intOpt(opts, 'limit', 30);
|
|
105
|
+
const db = connect(ctx);
|
|
106
|
+
const rows = db.prepare('SELECT * FROM events ORDER BY id DESC LIMIT ?').all(limit).reverse();
|
|
107
|
+
printResult(ctx, rows, (data) => table(data, [
|
|
108
|
+
{ label: 'id', value: (r) => `#${r.id}` },
|
|
109
|
+
{ label: 'type', value: (r) => r.type },
|
|
110
|
+
{ label: 'actor', value: (r) => r.actor || '' },
|
|
111
|
+
{ label: 'task', value: (r) => r.task_id ? `#${r.task_id}` : '' },
|
|
112
|
+
{ label: 'time', value: (r) => iso(r.created_at) }
|
|
113
|
+
]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function cmdHeartbeat(ctx, args) {
|
|
117
|
+
const opts = parseOpts(args, { booleans: ['renew-locks'] });
|
|
118
|
+
const identity = resolveCurrentPeer(ctx, opts, 'peer', 'shell');
|
|
119
|
+
const peer = identity.id;
|
|
120
|
+
const status = opts.status || null;
|
|
121
|
+
const ttlOverride = opts.ttl === undefined ? null : positiveSafeIntOpt(opts, 'ttl', DEFAULT_LOCK_TTL);
|
|
122
|
+
const t = now();
|
|
123
|
+
if (ttlOverride !== null) leaseDeadline(t, ttlOverride);
|
|
124
|
+
const db = connect(ctx);
|
|
125
|
+
const clockObservation = opts['renew-locks']
|
|
126
|
+
? observeLockClockSafety(db, ctx, { owner: peer, observedAt: t })
|
|
127
|
+
: null;
|
|
128
|
+
touchCurrentPeer(db, ctx, identity, status, 'shell');
|
|
129
|
+
let renewed = 0;
|
|
130
|
+
if (opts['renew-locks']) {
|
|
131
|
+
const evidenceLive = peerEvidenceFromDb(db, ctx, peer).state === 'live';
|
|
132
|
+
if (clockObservation?.renewed > 0 && ttlOverride === null) {
|
|
133
|
+
renewed = clockObservation.renewed;
|
|
134
|
+
} else if (ttlOverride !== null) {
|
|
135
|
+
renewed = renewOwnedLocks(db, {
|
|
136
|
+
owner: peer,
|
|
137
|
+
nowSec: t,
|
|
138
|
+
ttlOverride,
|
|
139
|
+
includeExpired: evidenceLive
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
renewed = renewOwnedLocks(db, {
|
|
143
|
+
owner: peer,
|
|
144
|
+
nowSec: t,
|
|
145
|
+
includeExpired: evidenceLive
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
addEvent(db, 'peer.heartbeat', peer, null, { status, renewed });
|
|
150
|
+
printResult(ctx, { peer, status, renewed }, (data) => `heartbeat ${data.peer}${data.renewed ? `, renewed locks: ${data.renewed}` : ''}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function cmdAsk(ctx, args) {
|
|
154
|
+
if (args[0] === '--help' || args[0] === '-h') return helpAsk();
|
|
155
|
+
const opts = parseOpts(args, { booleans: ['inject', 'no-enter'] });
|
|
156
|
+
const recipient = opts.to || opts._[0];
|
|
157
|
+
if (!recipient) throw new CliError('BAD_ARGS', 'Missing peer');
|
|
158
|
+
const body = opts.body || opts._.slice(opts.to ? 0 : 1).join(' ');
|
|
159
|
+
if (!body) throw new CliError('BAD_ARGS', 'Missing message');
|
|
160
|
+
const identity = resolveCurrentPeer(ctx, opts, 'from', 'shell');
|
|
161
|
+
const sender = identity.id;
|
|
162
|
+
const taskId = intOpt(opts, 'task', null);
|
|
163
|
+
const kind = opts.kind || 'ask';
|
|
164
|
+
const runtime = opts.inject ? readRuntime(ctx) : null;
|
|
165
|
+
const db = connect(ctx);
|
|
166
|
+
touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
167
|
+
const id = sendMessage(db, sender, recipient, taskId, kind, body);
|
|
168
|
+
let injected = false;
|
|
169
|
+
if (opts.inject) {
|
|
170
|
+
await injectPeer(ctx, recipient, body, !opts['no-enter'], runtime, sender);
|
|
171
|
+
injected = true;
|
|
172
|
+
}
|
|
173
|
+
printResult(ctx, { id, sender, recipient, task_id: taskId, kind, body, injected }, (data) => `asked ${data.recipient} with message #${data.id}${data.injected ? ' and injected terminal input' : ''}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function cmdBroadcast(ctx, args) {
|
|
177
|
+
if (args[0] === '--help' || args[0] === '-h') return helpBroadcast();
|
|
178
|
+
const opts = parseOpts(args, { booleans: ['inject', 'no-enter'] });
|
|
179
|
+
const body = opts.body || opts._.join(' ');
|
|
180
|
+
if (!body) throw new CliError('BAD_ARGS', 'Missing message');
|
|
181
|
+
const identity = resolveCurrentPeer(ctx, opts, 'from', 'shell');
|
|
182
|
+
const sender = identity.id;
|
|
183
|
+
const taskId = intOpt(opts, 'task', null);
|
|
184
|
+
const kind = opts.kind || 'broadcast';
|
|
185
|
+
const runtime = opts.inject ? readRuntime(ctx) : null;
|
|
186
|
+
const db = connect(ctx);
|
|
187
|
+
touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
188
|
+
const id = sendMessage(db, sender, 'all', taskId, kind, body);
|
|
189
|
+
let injected = 0;
|
|
190
|
+
let skipped = 0;
|
|
191
|
+
if (opts.inject) {
|
|
192
|
+
const sessions = await runtimeRequest(ctx, 'GET', '/api/sessions', null, runtime);
|
|
193
|
+
const running = (sessions.sessions || []).filter((session) => session.status === 'running');
|
|
194
|
+
for (const session of running) {
|
|
195
|
+
try {
|
|
196
|
+
await injectPeer(ctx, session.id, body, !opts['no-enter'], runtime, sender);
|
|
197
|
+
injected += 1;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err instanceof CliError && ['NOT_FOUND', 'SESSION_NOT_RUNNING', 'TMUX_ERROR'].includes(err.code)) {
|
|
200
|
+
skipped += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
printResult(ctx, { id, sender, recipient: 'all', task_id: taskId, kind, body, injected, skipped }, (data) => {
|
|
208
|
+
const injectedText = data.injected ? ` and injected ${data.injected} terminal(s)` : '';
|
|
209
|
+
const skippedText = data.skipped ? `, skipped ${data.skipped} stale terminal(s)` : '';
|
|
210
|
+
return `broadcast message #${data.id}${injectedText}${skippedText}`;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function injectPeer(ctx, peer, text, enter = true, runtime = null, auditActor = null) {
|
|
215
|
+
const actor = auditActor || resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
216
|
+
const db = connect(ctx);
|
|
217
|
+
try {
|
|
218
|
+
addEvent(db, 'web.session.input.requested', actor, null, auditPayload({
|
|
219
|
+
actor,
|
|
220
|
+
target: peer,
|
|
221
|
+
peer,
|
|
222
|
+
source: 'cli',
|
|
223
|
+
admin: actor !== peer,
|
|
224
|
+
bytes: text.length,
|
|
225
|
+
enter
|
|
226
|
+
}));
|
|
227
|
+
} finally {
|
|
228
|
+
db.close();
|
|
229
|
+
}
|
|
230
|
+
return runtimeRequest(ctx, 'POST', `/api/sessions/${encodeURIComponent(peer)}/input`, {
|
|
231
|
+
text,
|
|
232
|
+
enter
|
|
233
|
+
}, runtime, { timeoutMs: RUNTIME_SESSION_MUTATION_TIMEOUT_MS });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function cmdInject(ctx, args) {
|
|
237
|
+
if (args[0] === '--help' || args[0] === '-h') return helpInject();
|
|
238
|
+
const opts = parseOpts(args, { booleans: ['no-enter'] });
|
|
239
|
+
const peer = opts.peer || opts._[0];
|
|
240
|
+
if (!peer) throw new CliError('BAD_ARGS', 'Missing peer');
|
|
241
|
+
const text = opts.body || opts._.slice(opts.peer ? 0 : 1).join(' ');
|
|
242
|
+
if (!text) throw new CliError('BAD_ARGS', 'Missing text');
|
|
243
|
+
const enter = !opts['no-enter'];
|
|
244
|
+
const result = await injectPeer(ctx, peer, text, enter);
|
|
245
|
+
printResult(ctx, { peer, text, enter, result }, (data) => `injected ${data.peer}${data.enter ? ' and pressed Enter' : ''}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { cmdHandoff, cmdEvent, cmdHeartbeat, cmdAsk, cmdBroadcast, cmdInject, injectPeer };
|
|
249
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Database health check command extracted from bin/hcc.mjs.
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { printResult } from '../../format.mjs';
|
|
5
|
+
|
|
6
|
+
export function createDoctorCommand({ connectReadOnly, readSchemaVersion, DB_SCHEMA_VERSION, CLI_NAME }) {
|
|
7
|
+
async function cmdDoctor(ctx, args) {
|
|
8
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
9
|
+
console.log(`${CLI_NAME} doctor [--json]
|
|
10
|
+
|
|
11
|
+
Runs a read-only health check on the project database: PRAGMA integrity_check,
|
|
12
|
+
schema compatibility, persistent journal mode, DB and WAL file sizes, and
|
|
13
|
+
per-table row counts. Exits non-zero for corruption or an unsupported schema.
|
|
14
|
+
`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const db = connectReadOnly(ctx);
|
|
18
|
+
let report;
|
|
19
|
+
try {
|
|
20
|
+
const integrity = db.prepare('PRAGMA integrity_check').get();
|
|
21
|
+
const quick = db.prepare('PRAGMA quick_check').get();
|
|
22
|
+
const schemaVersion = readSchemaVersion(db);
|
|
23
|
+
const journalMode = db.prepare('PRAGMA journal_mode').get();
|
|
24
|
+
const synchronous = db.prepare('PRAGMA synchronous').get();
|
|
25
|
+
const walAutocheckpoint = db.prepare('PRAGMA wal_autocheckpoint').get();
|
|
26
|
+
const userVersion = db.prepare('PRAGMA user_version').get();
|
|
27
|
+
const fk = db.prepare('PRAGMA foreign_keys').get();
|
|
28
|
+
const counts = {};
|
|
29
|
+
for (const table of ['peers', 'peer_bindings', 'tasks', 'messages', 'message_reads', 'locks', 'handoffs', 'events']) {
|
|
30
|
+
try { counts[table] = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n; } catch { counts[table] = null; }
|
|
31
|
+
}
|
|
32
|
+
report = {
|
|
33
|
+
root: ctx.root,
|
|
34
|
+
db: ctx.dbPath,
|
|
35
|
+
integrity_check: integrity ? Object.values(integrity)[0] : null,
|
|
36
|
+
quick_check: quick ? Object.values(quick)[0] : null,
|
|
37
|
+
schema_version: schemaVersion,
|
|
38
|
+
supported_schema_version: DB_SCHEMA_VERSION,
|
|
39
|
+
schema_compatible: schemaVersion > 0 && schemaVersion <= DB_SCHEMA_VERSION,
|
|
40
|
+
migration_required: schemaVersion > 0 && schemaVersion < DB_SCHEMA_VERSION,
|
|
41
|
+
user_version: userVersion ? Object.values(userVersion)[0] : 0,
|
|
42
|
+
journal_mode: journalMode ? Object.values(journalMode)[0] : null,
|
|
43
|
+
diagnostic_connection: {
|
|
44
|
+
synchronous: synchronous ? Object.values(synchronous)[0] : null,
|
|
45
|
+
wal_autocheckpoint: walAutocheckpoint ? Object.values(walAutocheckpoint)[0] : null,
|
|
46
|
+
foreign_keys: fk ? Object.values(fk)[0] : null
|
|
47
|
+
},
|
|
48
|
+
runtime_connection_defaults: {
|
|
49
|
+
synchronous: 'NORMAL',
|
|
50
|
+
wal_autocheckpoint: 1000,
|
|
51
|
+
foreign_keys: true
|
|
52
|
+
},
|
|
53
|
+
row_counts: counts
|
|
54
|
+
};
|
|
55
|
+
} finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
report.db_size_bytes = fs.statSync(ctx.dbPath).size;
|
|
60
|
+
const walPath = `${ctx.dbPath}-wal`;
|
|
61
|
+
report.wal_size_bytes = fs.existsSync(walPath) ? fs.statSync(walPath).size : 0;
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
const healthy = report.integrity_check === 'ok' && report.schema_compatible;
|
|
65
|
+
printResult(ctx, report, (r) => {
|
|
66
|
+
const lines = [
|
|
67
|
+
`doctor (${r.root}):`,
|
|
68
|
+
` integrity_check: ${r.integrity_check}`,
|
|
69
|
+
` quick_check: ${r.quick_check}`,
|
|
70
|
+
` schema: ${r.schema_version} (supported ${r.supported_schema_version})`,
|
|
71
|
+
` journal_mode: ${r.journal_mode} synchronous: ${r.diagnostic_connection.synchronous} wal_autocheckpoint: ${r.diagnostic_connection.wal_autocheckpoint} foreign_keys: ${r.diagnostic_connection.foreign_keys}`,
|
|
72
|
+
` db size: ${r.db_size_bytes ?? '?'} bytes${r.wal_size_bytes ? ` (wal ${r.wal_size_bytes})` : ''}`,
|
|
73
|
+
` rows: ` + Object.entries(r.row_counts).map(([k, v]) => `${k}=${v}`).join(' ')
|
|
74
|
+
];
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
});
|
|
77
|
+
if (!healthy) process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { cmdDoctor };
|
|
81
|
+
}
|