@cleocode/core 2026.4.11 → 2026.4.12
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/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
- package/dist/codebase-map/analyzers/architecture.js +0 -1
- package/dist/codebase-map/analyzers/architecture.js.map +1 -1
- package/dist/conduit/local-transport.d.ts +18 -8
- package/dist/conduit/local-transport.d.ts.map +1 -1
- package/dist/conduit/local-transport.js +23 -13
- package/dist/conduit/local-transport.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -1
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.js +175 -68950
- package/dist/index.js.map +1 -7
- package/dist/init.d.ts +1 -2
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1 -2
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +8 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +13 -6
- package/dist/internal.js.map +1 -1
- package/dist/memory/learnings.d.ts +2 -2
- package/dist/memory/patterns.d.ts +6 -6
- package/dist/output.d.ts +32 -11
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +67 -67
- package/dist/output.js.map +1 -1
- package/dist/paths.js +80 -14
- package/dist/paths.js.map +1 -1
- package/dist/skills/dynamic-skill-generator.d.ts +0 -2
- package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
- package/dist/skills/dynamic-skill-generator.js.map +1 -1
- package/dist/store/agent-registry-accessor.d.ts +203 -12
- package/dist/store/agent-registry-accessor.d.ts.map +1 -1
- package/dist/store/agent-registry-accessor.js +618 -100
- package/dist/store/agent-registry-accessor.js.map +1 -1
- package/dist/store/api-key-kdf.d.ts +73 -0
- package/dist/store/api-key-kdf.d.ts.map +1 -0
- package/dist/store/api-key-kdf.js +84 -0
- package/dist/store/api-key-kdf.js.map +1 -0
- package/dist/store/cleanup-legacy.js +171 -0
- package/dist/store/cleanup-legacy.js.map +1 -0
- package/dist/store/conduit-sqlite.d.ts +184 -0
- package/dist/store/conduit-sqlite.d.ts.map +1 -0
- package/dist/store/conduit-sqlite.js +570 -0
- package/dist/store/conduit-sqlite.js.map +1 -0
- package/dist/store/global-salt.d.ts +78 -0
- package/dist/store/global-salt.d.ts.map +1 -0
- package/dist/store/global-salt.js +147 -0
- package/dist/store/global-salt.js.map +1 -0
- package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
- package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
- package/dist/store/migrate-signaldock-to-conduit.js +555 -0
- package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
- package/dist/store/nexus-sqlite.js +28 -3
- package/dist/store/nexus-sqlite.js.map +1 -1
- package/dist/store/signaldock-sqlite.d.ts +122 -19
- package/dist/store/signaldock-sqlite.d.ts.map +1 -1
- package/dist/store/signaldock-sqlite.js +401 -251
- package/dist/store/signaldock-sqlite.js.map +1 -1
- package/dist/store/sqlite-backup.js +122 -4
- package/dist/store/sqlite-backup.js.map +1 -1
- package/dist/system/backup.d.ts +0 -26
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/runtime.d.ts +0 -2
- package/dist/system/runtime.d.ts.map +1 -1
- package/dist/system/runtime.js +3 -3
- package/dist/system/runtime.js.map +1 -1
- package/dist/tasks/add.d.ts +1 -1
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +98 -23
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +4 -1
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +4 -1
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/labels.d.ts.map +1 -1
- package/dist/tasks/labels.js +4 -1
- package/dist/tasks/labels.js.map +1 -1
- package/dist/tasks/relates.d.ts.map +1 -1
- package/dist/tasks/relates.js +16 -4
- package/dist/tasks/relates.js.map +1 -1
- package/dist/tasks/show.d.ts.map +1 -1
- package/dist/tasks/show.js +4 -1
- package/dist/tasks/show.js.map +1 -1
- package/dist/tasks/update.d.ts.map +1 -1
- package/dist/tasks/update.js +32 -6
- package/dist/tasks/update.js.map +1 -1
- package/dist/validation/engine.d.ts.map +1 -1
- package/dist/validation/engine.js +16 -4
- package/dist/validation/engine.js.map +1 -1
- package/dist/validation/param-utils.d.ts +5 -3
- package/dist/validation/param-utils.d.ts.map +1 -1
- package/dist/validation/param-utils.js +8 -6
- package/dist/validation/param-utils.js.map +1 -1
- package/dist/validation/protocols/_shared.d.ts.map +1 -1
- package/dist/validation/protocols/_shared.js +13 -6
- package/dist/validation/protocols/_shared.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/__tests__/manager.test.ts +0 -1
- package/src/codebase-map/analyzers/architecture.ts +0 -1
- package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
- package/src/conduit/__tests__/local-transport.test.ts +14 -12
- package/src/conduit/local-transport.ts +23 -13
- package/src/config.ts +0 -1
- package/src/errors.ts +24 -0
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
- package/src/init.ts +1 -2
- package/src/internal.ts +49 -2
- package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
- package/src/memory/__tests__/engine-compat.test.ts +2 -2
- package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
- package/src/observability/__tests__/index.test.ts +4 -4
- package/src/observability/__tests__/log-filter.test.ts +4 -4
- package/src/output.ts +73 -75
- package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
- package/src/sessions/__tests__/session-grade.test.ts +2 -2
- package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
- package/src/skills/dynamic-skill-generator.ts +0 -2
- package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
- package/src/store/__tests__/api-key-kdf.test.ts +113 -0
- package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
- package/src/store/__tests__/global-salt.test.ts +195 -0
- package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
- package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
- package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
- package/src/store/__tests__/sqlite-backup.test.ts +5 -1
- package/src/store/__tests__/t310-integration.test.ts +1150 -0
- package/src/store/agent-registry-accessor.ts +847 -140
- package/src/store/api-key-kdf.ts +104 -0
- package/src/store/conduit-sqlite.ts +655 -0
- package/src/store/global-salt.ts +175 -0
- package/src/store/migrate-signaldock-to-conduit.ts +669 -0
- package/src/store/signaldock-sqlite.ts +431 -254
- package/src/store/sqlite-backup.ts +185 -10
- package/src/system/backup.ts +2 -62
- package/src/system/runtime.ts +4 -6
- package/src/tasks/__tests__/error-hints.test.ts +256 -0
- package/src/tasks/add.ts +99 -9
- package/src/tasks/complete.ts +4 -1
- package/src/tasks/find.ts +4 -1
- package/src/tasks/labels.ts +4 -1
- package/src/tasks/relates.ts +16 -4
- package/src/tasks/show.ts +4 -1
- package/src/tasks/update.ts +32 -3
- package/src/validation/__tests__/error-hints.test.ts +97 -0
- package/src/validation/engine.ts +16 -1
- package/src/validation/param-utils.ts +10 -7
- package/src/validation/protocols/_shared.ts +14 -6
- package/src/validation/protocols/cant/architecture-decision.cant +80 -0
- package/src/validation/protocols/cant/artifact-publish.cant +95 -0
- package/src/validation/protocols/cant/consensus.cant +74 -0
- package/src/validation/protocols/cant/contribution.cant +82 -0
- package/src/validation/protocols/cant/decomposition.cant +92 -0
- package/src/validation/protocols/cant/implementation.cant +67 -0
- package/src/validation/protocols/cant/provenance.cant +88 -0
- package/src/validation/protocols/cant/release.cant +96 -0
- package/src/validation/protocols/cant/research.cant +66 -0
- package/src/validation/protocols/cant/specification.cant +67 -0
- package/src/validation/protocols/cant/testing.cant +88 -0
- package/src/validation/protocols/cant/validation.cant +65 -0
- package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
- package/templates/config.template.json +0 -1
- package/templates/global-config.template.json +0 -1
|
@@ -2,22 +2,36 @@
|
|
|
2
2
|
* SQLite backup via VACUUM INTO with snapshot rotation.
|
|
3
3
|
*
|
|
4
4
|
* Produces self-contained, WAL-free copies of CLEO SQLite databases
|
|
5
|
-
* (tasks.db, brain.db at project tier; nexus.db at
|
|
6
|
-
* `.cleo/backups/sqlite/` (project) or
|
|
7
|
-
* (global) with a configurable rotation
|
|
8
|
-
* backup
|
|
5
|
+
* (tasks.db, brain.db, conduit.db at project tier; nexus.db, signaldock.db at
|
|
6
|
+
* global tier) into `.cleo/backups/sqlite/` (project) or
|
|
7
|
+
* `$XDG_DATA_HOME/cleo/backups/sqlite/` (global) with a configurable rotation
|
|
8
|
+
* limit. Also provides raw-file backup for the global-salt binary (not SQLite).
|
|
9
|
+
* All errors are swallowed — backup failure must never interrupt normal operation.
|
|
9
10
|
*
|
|
10
11
|
* @task T4873
|
|
11
12
|
* @task T5158 — extended to cover brain.db
|
|
12
13
|
* @task T306 — extended to cover global-tier nexus.db (epic T299)
|
|
14
|
+
* @task T369 — extended to cover conduit.db (project), signaldock.db (global),
|
|
15
|
+
* and global-salt raw-file backup (epic T310)
|
|
13
16
|
* @epic T4867
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
chmodSync,
|
|
21
|
+
copyFileSync,
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
statSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
} from 'node:fs';
|
|
17
28
|
import { join } from 'node:path';
|
|
18
29
|
import { getCleoDir, getCleoHome } from '../paths.js';
|
|
19
30
|
import { getBrainNativeDb } from './brain-sqlite.js';
|
|
31
|
+
import { getConduitNativeDb } from './conduit-sqlite.js';
|
|
32
|
+
import { getGlobalSaltPath } from './global-salt.js';
|
|
20
33
|
import { getNexusNativeDb } from './nexus-sqlite.js';
|
|
34
|
+
import { getGlobalSignaldockNativeDb } from './signaldock-sqlite.js';
|
|
21
35
|
import { getNativeDb } from './sqlite.js';
|
|
22
36
|
|
|
23
37
|
/** Maximum number of snapshots retained per database (oldest rotated out). */
|
|
@@ -46,11 +60,16 @@ interface SnapshotTarget {
|
|
|
46
60
|
|
|
47
61
|
/**
|
|
48
62
|
* Canonical list of snapshot targets. Ordering is insertion order — tasks.db
|
|
49
|
-
* snapshots first (highest-value operational state), then brain.db
|
|
63
|
+
* snapshots first (highest-value operational state), then brain.db, then
|
|
64
|
+
* conduit.db (project messaging state).
|
|
65
|
+
*
|
|
66
|
+
* @task T369
|
|
67
|
+
* @epic T310
|
|
50
68
|
*/
|
|
51
69
|
const SNAPSHOT_TARGETS: SnapshotTarget[] = [
|
|
52
70
|
{ prefix: 'tasks', getDb: getNativeDb },
|
|
53
71
|
{ prefix: 'brain', getDb: getBrainNativeDb },
|
|
72
|
+
{ prefix: 'conduit', getDb: getConduitNativeDb }, // Added T369 — project messaging DB
|
|
54
73
|
];
|
|
55
74
|
|
|
56
75
|
/**
|
|
@@ -308,10 +327,16 @@ export function listSqliteBackupsAll(
|
|
|
308
327
|
export type BackupScope = 'project' | 'global';
|
|
309
328
|
|
|
310
329
|
/**
|
|
311
|
-
* Registered global-tier snapshot targets. `
|
|
312
|
-
*
|
|
330
|
+
* Registered global-tier snapshot targets. Both `nexus` and `signaldock` are
|
|
331
|
+
* active as of T369 (epic T310).
|
|
332
|
+
*
|
|
333
|
+
* @task T369
|
|
334
|
+
* @epic T310
|
|
313
335
|
*/
|
|
314
|
-
const GLOBAL_SNAPSHOT_TARGETS: SnapshotTarget[] = [
|
|
336
|
+
const GLOBAL_SNAPSHOT_TARGETS: SnapshotTarget[] = [
|
|
337
|
+
{ prefix: 'nexus', getDb: getNexusNativeDb },
|
|
338
|
+
{ prefix: 'signaldock', getDb: getGlobalSignaldockNativeDb }, // Activated T369 — global agent registry
|
|
339
|
+
];
|
|
315
340
|
|
|
316
341
|
/**
|
|
317
342
|
* Resolve the global-tier backup directory, creating it on first use.
|
|
@@ -333,12 +358,13 @@ function resolveGlobalBackupDir(cleoHomeOverride?: string): string {
|
|
|
333
358
|
* Non-fatal: errors from any individual step are surfaced via the return value
|
|
334
359
|
* but never thrown — a failed snapshot MUST NOT interrupt normal operation.
|
|
335
360
|
*
|
|
336
|
-
* @param dbName - Which global-tier DB to snapshot (`'nexus'
|
|
361
|
+
* @param dbName - Which global-tier DB to snapshot (`'nexus'` or `'signaldock'`)
|
|
337
362
|
* @param opts.rotation - Maximum retained snapshots per prefix (default 10)
|
|
338
363
|
* @param opts.cleoHomeOverride - Override `getCleoHome()` path (use in tests to target a tmp dir)
|
|
339
364
|
* @returns Object containing the new snapshot path and any rotated (deleted) file paths
|
|
340
365
|
*
|
|
341
366
|
* @task T306
|
|
367
|
+
* @task T369 — activated signaldock target (epic T310)
|
|
342
368
|
* @epic T299
|
|
343
369
|
* @why ADR-036 §Backup Mechanism requires VACUUM INTO rotation at the global tier;
|
|
344
370
|
* nexus.db has zero backup coverage prior to v2026.4.11.
|
|
@@ -453,3 +479,152 @@ export function listGlobalSqliteBackups(
|
|
|
453
479
|
return [];
|
|
454
480
|
}
|
|
455
481
|
}
|
|
482
|
+
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// Global-salt raw-file backup (ADR-037 §5)
|
|
485
|
+
// @task T369
|
|
486
|
+
// @epic T310
|
|
487
|
+
// ============================================================================
|
|
488
|
+
|
|
489
|
+
/** Filename prefix for global-salt backup files. */
|
|
490
|
+
const GLOBAL_SALT_BACKUP_PREFIX = 'global-salt';
|
|
491
|
+
|
|
492
|
+
/** Regex matching global-salt backup filenames: `global-salt-YYYYMMDD-HHmmss`. */
|
|
493
|
+
const GLOBAL_SALT_BACKUP_PATTERN = /^global-salt-\d{8}-\d{6}$/;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Resolve the backup directory for global-salt files: `{cleoHome}/backups/`.
|
|
497
|
+
* Global-salt backups live directly under `backups/` (not `backups/sqlite/`)
|
|
498
|
+
* to make clear they are binary files, not SQLite databases.
|
|
499
|
+
*/
|
|
500
|
+
function resolveGlobalSaltBackupDir(cleoHomeOverride?: string): string {
|
|
501
|
+
const base = cleoHomeOverride ?? getCleoHome();
|
|
502
|
+
return join(base, 'backups');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Rotate global-salt backup files: delete the oldest until fewer than
|
|
507
|
+
* {@link MAX_SNAPSHOTS} remain. Returns the paths of deleted files.
|
|
508
|
+
* Non-fatal on any filesystem error.
|
|
509
|
+
*/
|
|
510
|
+
function rotateGlobalSaltBackups(backupDir: string): string[] {
|
|
511
|
+
const rotated: string[] = [];
|
|
512
|
+
try {
|
|
513
|
+
const files = readdirSync(backupDir)
|
|
514
|
+
.filter((f) => GLOBAL_SALT_BACKUP_PATTERN.test(f))
|
|
515
|
+
.map((f) => ({
|
|
516
|
+
name: f,
|
|
517
|
+
path: join(backupDir, f),
|
|
518
|
+
mtimeMs: statSync(join(backupDir, f)).mtimeMs,
|
|
519
|
+
}))
|
|
520
|
+
.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
|
|
521
|
+
|
|
522
|
+
while (files.length >= MAX_SNAPSHOTS) {
|
|
523
|
+
const oldest = files.shift();
|
|
524
|
+
if (!oldest) break;
|
|
525
|
+
try {
|
|
526
|
+
unlinkSync(oldest.path);
|
|
527
|
+
rotated.push(oldest.path);
|
|
528
|
+
} catch {
|
|
529
|
+
// non-fatal rotation failure
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
// non-fatal
|
|
534
|
+
}
|
|
535
|
+
return rotated;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Creates a raw-file backup of the global-salt binary at
|
|
540
|
+
* `${getCleoHome()}/backups/global-salt-YYYYMMDD-HHmmss` with `0o600`
|
|
541
|
+
* permissions. Rotates to {@link MAX_SNAPSHOTS} (10) copies, deleting the
|
|
542
|
+
* oldest when the limit is reached.
|
|
543
|
+
*
|
|
544
|
+
* Non-fatal: errors are swallowed — salt backup failure must never block cleo.
|
|
545
|
+
* Returns empty strings and no rotated paths on failure.
|
|
546
|
+
*
|
|
547
|
+
* @param opts.cleoHomeOverride - Override `getCleoHome()` path (use in tests to target a tmp dir)
|
|
548
|
+
* @returns Object with the new snapshot path and any rotated (deleted) file paths
|
|
549
|
+
*
|
|
550
|
+
* @task T369
|
|
551
|
+
* @epic T310
|
|
552
|
+
* @why ADR-037 §5 — global-salt is security-critical; losing it invalidates
|
|
553
|
+
* all API keys. Backup enables recovery from accidental deletion.
|
|
554
|
+
*/
|
|
555
|
+
export async function backupGlobalSalt(opts?: {
|
|
556
|
+
cleoHomeOverride?: string;
|
|
557
|
+
}): Promise<{ snapshotPath: string; rotated: string[] }> {
|
|
558
|
+
try {
|
|
559
|
+
const cleoHome = opts?.cleoHomeOverride ?? getCleoHome();
|
|
560
|
+
const saltSourcePath = opts?.cleoHomeOverride
|
|
561
|
+
? join(cleoHome, 'global-salt')
|
|
562
|
+
: getGlobalSaltPath();
|
|
563
|
+
|
|
564
|
+
if (!existsSync(saltSourcePath)) {
|
|
565
|
+
return { snapshotPath: '', rotated: [] };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const backupDir = resolveGlobalSaltBackupDir(opts?.cleoHomeOverride);
|
|
569
|
+
mkdirSync(backupDir, { recursive: true });
|
|
570
|
+
|
|
571
|
+
const rotated = rotateGlobalSaltBackups(backupDir);
|
|
572
|
+
|
|
573
|
+
const snapshotName = `${GLOBAL_SALT_BACKUP_PREFIX}-${formatTimestamp(new Date())}`;
|
|
574
|
+
const snapshotPath = join(backupDir, snapshotName);
|
|
575
|
+
|
|
576
|
+
copyFileSync(saltSourcePath, snapshotPath);
|
|
577
|
+
chmodSync(snapshotPath, 0o600);
|
|
578
|
+
|
|
579
|
+
return { snapshotPath, rotated };
|
|
580
|
+
} catch {
|
|
581
|
+
// non-fatal — backup failure must never interrupt normal operation
|
|
582
|
+
return { snapshotPath: '', rotated: [] };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* A single entry returned by {@link listGlobalSaltBackups}.
|
|
588
|
+
*
|
|
589
|
+
* @task T369
|
|
590
|
+
* @epic T310
|
|
591
|
+
*/
|
|
592
|
+
export interface GlobalSaltBackupEntry {
|
|
593
|
+
/** Backup filename, e.g. `global-salt-20260408-143022`. */
|
|
594
|
+
name: string;
|
|
595
|
+
/** Absolute path to the backup file. */
|
|
596
|
+
path: string;
|
|
597
|
+
/** File size in bytes (should be 32 for a valid global-salt). */
|
|
598
|
+
size: number;
|
|
599
|
+
/** Last-modified timestamp. */
|
|
600
|
+
mtime: Date;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* List global-salt backup files from `$XDG_DATA_HOME/cleo/backups/`, sorted
|
|
605
|
+
* newest-first by mtime.
|
|
606
|
+
*
|
|
607
|
+
* Returns an empty array when the backup directory does not exist.
|
|
608
|
+
*
|
|
609
|
+
* @param cleoHomeOverride - Override `getCleoHome()` path (use in tests to target a tmp dir)
|
|
610
|
+
*
|
|
611
|
+
* @task T369
|
|
612
|
+
* @epic T310
|
|
613
|
+
*/
|
|
614
|
+
export function listGlobalSaltBackups(cleoHomeOverride?: string): GlobalSaltBackupEntry[] {
|
|
615
|
+
try {
|
|
616
|
+
const backupDir = resolveGlobalSaltBackupDir(cleoHomeOverride);
|
|
617
|
+
if (!existsSync(backupDir)) return [];
|
|
618
|
+
|
|
619
|
+
return readdirSync(backupDir)
|
|
620
|
+
.filter((f) => GLOBAL_SALT_BACKUP_PATTERN.test(f))
|
|
621
|
+
.map((f) => {
|
|
622
|
+
const filePath = join(backupDir, f);
|
|
623
|
+
const s = statSync(filePath);
|
|
624
|
+
return { name: f, path: filePath, size: s.size, mtime: new Date(s.mtimeMs) };
|
|
625
|
+
})
|
|
626
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // newest first
|
|
627
|
+
} catch {
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
}
|
package/src/system/backup.ts
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @task T4783
|
|
24
24
|
* @task T5158 — extended to use VACUUM INTO for .db files and atomicWrite for JSON
|
|
25
|
-
* @task T306 — extended to support global-tier scope (nexus.db; epic T299)
|
|
26
25
|
*/
|
|
27
26
|
|
|
28
27
|
import {
|
|
@@ -87,15 +86,6 @@ export interface BackupResult {
|
|
|
87
86
|
type: string;
|
|
88
87
|
/** Files that were successfully captured into this backup. */
|
|
89
88
|
files: string[];
|
|
90
|
-
/**
|
|
91
|
-
* Global-tier snapshot results, populated when `opts.includeGlobal` is true.
|
|
92
|
-
* Key is the DB name (e.g. `'nexus'`), value is the snapshot path or an
|
|
93
|
-
* empty string when the DB was not initialized.
|
|
94
|
-
*
|
|
95
|
-
* @task T306
|
|
96
|
-
* @epic T299
|
|
97
|
-
*/
|
|
98
|
-
global?: Record<string, string>;
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
/** Result shape returned by {@link restoreBackup}. */
|
|
@@ -122,31 +112,12 @@ export interface RestoreResult {
|
|
|
122
112
|
* when `safeSqliteSnapshot` asks for them. This makes the function
|
|
123
113
|
* self-contained — callers do not need to pre-open the DBs.
|
|
124
114
|
*
|
|
125
|
-
* When `opts.includeGlobal` is true, also snapshots global-tier databases
|
|
126
|
-
* (currently `nexus.db`) via {@link vacuumIntoGlobalBackup}. Global snapshots
|
|
127
|
-
* are written to `$XDG_DATA_HOME/cleo/backups/sqlite/` and returned in
|
|
128
|
-
* `result.global`.
|
|
129
|
-
*
|
|
130
115
|
* Async because opening the database engines requires async migration
|
|
131
116
|
* reconciliation (ADR-012). The CLI dispatch layer awaits this result.
|
|
132
|
-
*
|
|
133
|
-
* @task T306
|
|
134
|
-
* @epic T299
|
|
135
117
|
*/
|
|
136
118
|
export async function createBackup(
|
|
137
119
|
projectRoot: string,
|
|
138
|
-
opts?: {
|
|
139
|
-
type?: string;
|
|
140
|
-
note?: string;
|
|
141
|
-
/**
|
|
142
|
-
* When true, also snapshot global-tier databases (nexus.db).
|
|
143
|
-
* Defaults to false for backwards compatibility.
|
|
144
|
-
*
|
|
145
|
-
* @task T306
|
|
146
|
-
* @epic T299
|
|
147
|
-
*/
|
|
148
|
-
includeGlobal?: boolean;
|
|
149
|
-
},
|
|
120
|
+
opts?: { type?: string; note?: string },
|
|
150
121
|
): Promise<BackupResult> {
|
|
151
122
|
const cleoDir = join(projectRoot, '.cleo');
|
|
152
123
|
const btype = opts?.type || 'snapshot';
|
|
@@ -241,38 +212,7 @@ export async function createBackup(
|
|
|
241
212
|
// non-fatal
|
|
242
213
|
}
|
|
243
214
|
|
|
244
|
-
|
|
245
|
-
// @task T306 @epic T299
|
|
246
|
-
const globalResults: Record<string, string> = {};
|
|
247
|
-
if (opts?.includeGlobal) {
|
|
248
|
-
// Ensure nexus.db is initialized so its native handle is available.
|
|
249
|
-
try {
|
|
250
|
-
const { getNexusDb } = await import('../store/nexus-sqlite.js');
|
|
251
|
-
await getNexusDb();
|
|
252
|
-
} catch {
|
|
253
|
-
// nexus.db open failed — vacuumIntoGlobalBackup will return empty path
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
const { vacuumIntoGlobalBackup } = await import('../store/sqlite-backup.js');
|
|
257
|
-
const nexusResult = await vacuumIntoGlobalBackup('nexus');
|
|
258
|
-
globalResults['nexus'] = nexusResult.snapshotPath;
|
|
259
|
-
} catch {
|
|
260
|
-
// non-fatal — global backup failure must not block project backup
|
|
261
|
-
globalResults['nexus'] = '';
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const result: BackupResult = {
|
|
266
|
-
backupId,
|
|
267
|
-
path: backupDir,
|
|
268
|
-
timestamp,
|
|
269
|
-
type: btype,
|
|
270
|
-
files: backedUp,
|
|
271
|
-
};
|
|
272
|
-
if (opts?.includeGlobal) {
|
|
273
|
-
result.global = globalResults;
|
|
274
|
-
}
|
|
275
|
-
return result;
|
|
215
|
+
return { backupId, path: backupDir, timestamp, type: btype, files: backedUp };
|
|
276
216
|
}
|
|
277
217
|
|
|
278
218
|
/** A single backup entry returned by listSystemBackups. */
|
package/src/system/runtime.ts
CHANGED
|
@@ -35,8 +35,6 @@ export interface RuntimeDiagnostics {
|
|
|
35
35
|
};
|
|
36
36
|
naming: {
|
|
37
37
|
cli: string;
|
|
38
|
-
/** Legacy field. CLI dispatch only. */
|
|
39
|
-
mcp: string;
|
|
40
38
|
server: string;
|
|
41
39
|
};
|
|
42
40
|
node: string;
|
|
@@ -73,14 +71,14 @@ function detectFromDataRoot(dataRoot: string): RuntimeChannel | null {
|
|
|
73
71
|
return null;
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
function getExpectedNaming(channel: RuntimeChannel): { cli: string;
|
|
74
|
+
function getExpectedNaming(channel: RuntimeChannel): { cli: string; server: string } {
|
|
77
75
|
switch (channel) {
|
|
78
76
|
case 'dev':
|
|
79
|
-
return { cli: 'cleo-dev',
|
|
77
|
+
return { cli: 'cleo-dev', server: 'cleo-dev' };
|
|
80
78
|
case 'beta':
|
|
81
|
-
return { cli: 'cleo-beta',
|
|
79
|
+
return { cli: 'cleo-beta', server: 'cleo-beta' };
|
|
82
80
|
default:
|
|
83
|
-
return { cli: 'cleo',
|
|
81
|
+
return { cli: 'cleo', server: 'cleo' };
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests: CleoError fix hints on task-layer throws.
|
|
3
|
+
*
|
|
4
|
+
* Each test invokes a core function with invalid input and asserts that
|
|
5
|
+
* the thrown CleoError carries:
|
|
6
|
+
* - options.fix — a non-empty string with a concrete recovery action
|
|
7
|
+
* - options.details.field — the specific field that failed
|
|
8
|
+
*
|
|
9
|
+
* @task T341
|
|
10
|
+
* @epic T335
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { CleoError } from '../../errors.js';
|
|
15
|
+
import { createTestDb, type TestDbEnv } from '../../store/__tests__/test-db-helper.js';
|
|
16
|
+
import { resetDbState } from '../../store/sqlite.js';
|
|
17
|
+
import {
|
|
18
|
+
addTask,
|
|
19
|
+
validateDepends,
|
|
20
|
+
validateLabels,
|
|
21
|
+
validatePhaseFormat,
|
|
22
|
+
validateSize,
|
|
23
|
+
validateStatus,
|
|
24
|
+
validateTaskType,
|
|
25
|
+
validateTitle,
|
|
26
|
+
} from '../add.js';
|
|
27
|
+
import { findTasks } from '../find.js';
|
|
28
|
+
import { showLabelTasks } from '../labels.js';
|
|
29
|
+
import { showTask } from '../show.js';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helper: assert a CleoError has fix + details
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function assertErrorHints(
|
|
36
|
+
fn: () => unknown,
|
|
37
|
+
opts: { fixIncludes?: string; detailsField?: string },
|
|
38
|
+
): void {
|
|
39
|
+
let caught: unknown;
|
|
40
|
+
try {
|
|
41
|
+
fn();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
caught = err;
|
|
44
|
+
}
|
|
45
|
+
expect(caught).toBeInstanceOf(CleoError);
|
|
46
|
+
const e = caught as CleoError;
|
|
47
|
+
expect(e.fix).toBeTruthy();
|
|
48
|
+
if (opts.fixIncludes) {
|
|
49
|
+
expect(e.fix).toContain(opts.fixIncludes);
|
|
50
|
+
}
|
|
51
|
+
if (opts.detailsField) {
|
|
52
|
+
expect(e.details).toBeDefined();
|
|
53
|
+
expect(e.details!.field).toBe(opts.detailsField);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function assertAsyncErrorHints(
|
|
58
|
+
fn: () => Promise<unknown>,
|
|
59
|
+
opts: { fixIncludes?: string; detailsField?: string },
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
let caught: unknown;
|
|
62
|
+
try {
|
|
63
|
+
await fn();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
caught = err;
|
|
66
|
+
}
|
|
67
|
+
expect(caught).toBeInstanceOf(CleoError);
|
|
68
|
+
const e = caught as CleoError;
|
|
69
|
+
expect(e.fix).toBeTruthy();
|
|
70
|
+
if (opts.fixIncludes) {
|
|
71
|
+
expect(e.fix).toContain(opts.fixIncludes);
|
|
72
|
+
}
|
|
73
|
+
if (opts.detailsField) {
|
|
74
|
+
expect(e.details).toBeDefined();
|
|
75
|
+
expect(e.details!.field).toBe(opts.detailsField);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Tests
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
describe('error-hints: validateTitle', () => {
|
|
84
|
+
it('empty title — fix mentions cleo add, field=title', () => {
|
|
85
|
+
assertErrorHints(() => validateTitle(''), {
|
|
86
|
+
fixIncludes: 'cleo add',
|
|
87
|
+
detailsField: 'title',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('whitespace-only title — fix mentions cleo add, field=title', () => {
|
|
92
|
+
assertErrorHints(() => validateTitle(' '), {
|
|
93
|
+
fixIncludes: 'cleo add',
|
|
94
|
+
detailsField: 'title',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('title too long — fix mentions 200, field=title, details has expected/actual', () => {
|
|
99
|
+
let caught: unknown;
|
|
100
|
+
try {
|
|
101
|
+
validateTitle('a'.repeat(201));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
caught = err;
|
|
104
|
+
}
|
|
105
|
+
expect(caught).toBeInstanceOf(CleoError);
|
|
106
|
+
const e = caught as CleoError;
|
|
107
|
+
expect(e.fix).toContain('200');
|
|
108
|
+
expect(e.details).toBeDefined();
|
|
109
|
+
expect(e.details!.field).toBe('title');
|
|
110
|
+
expect(e.details!.expected).toBe(200);
|
|
111
|
+
expect(e.details!.actual).toBe(201);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('error-hints: validateStatus', () => {
|
|
116
|
+
it('invalid status — fix mentions --status, field=status', () => {
|
|
117
|
+
assertErrorHints(() => validateStatus('broken'), {
|
|
118
|
+
fixIncludes: '--status',
|
|
119
|
+
detailsField: 'status',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('error-hints: validateSize', () => {
|
|
125
|
+
it('invalid size — fix mentions --size, field=size', () => {
|
|
126
|
+
assertErrorHints(() => validateSize('giant'), {
|
|
127
|
+
fixIncludes: '--size',
|
|
128
|
+
detailsField: 'size',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('error-hints: validateTaskType', () => {
|
|
134
|
+
it('invalid type — fix mentions --type, field=type', () => {
|
|
135
|
+
assertErrorHints(() => validateTaskType('mega-task'), {
|
|
136
|
+
fixIncludes: '--type',
|
|
137
|
+
detailsField: 'type',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('error-hints: validateLabels', () => {
|
|
143
|
+
it('invalid label — fix mentions pattern, field=labels', () => {
|
|
144
|
+
assertErrorHints(() => validateLabels(['UPPERCASE']), {
|
|
145
|
+
fixIncludes: '^[a-z]',
|
|
146
|
+
detailsField: 'labels',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('error-hints: validatePhaseFormat', () => {
|
|
152
|
+
it('invalid phase — fix mentions pattern, field=phase', () => {
|
|
153
|
+
assertErrorHints(() => validatePhaseFormat('UPPER_CASE'), {
|
|
154
|
+
fixIncludes: '^[a-z]',
|
|
155
|
+
detailsField: 'phase',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('error-hints: validateDepends (sync helper)', () => {
|
|
161
|
+
it('invalid dep ID format — fix mentions T### format, field=depends', () => {
|
|
162
|
+
assertErrorHints(() => validateDepends(['invalid'], []), {
|
|
163
|
+
fixIncludes: 'T###',
|
|
164
|
+
detailsField: 'depends',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('dep not found — fix mentions cleo find, field=depends', () => {
|
|
169
|
+
assertErrorHints(() => validateDepends(['T999'], []), {
|
|
170
|
+
fixIncludes: 'find',
|
|
171
|
+
detailsField: 'depends',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Anti-hallucination throw (add.ts line ~436)
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
describe('error-hints: anti-hallucination (title === description)', () => {
|
|
181
|
+
let env: TestDbEnv;
|
|
182
|
+
|
|
183
|
+
beforeEach(async () => {
|
|
184
|
+
resetDbState();
|
|
185
|
+
env = await createTestDb();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
afterEach(async () => {
|
|
189
|
+
await env.cleanup();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('throws with fix mentioning --desc, field=description', async () => {
|
|
193
|
+
await assertAsyncErrorHints(
|
|
194
|
+
() =>
|
|
195
|
+
addTask(
|
|
196
|
+
{
|
|
197
|
+
title: 'same text',
|
|
198
|
+
description: 'same text',
|
|
199
|
+
},
|
|
200
|
+
env.tempDir,
|
|
201
|
+
env.accessor,
|
|
202
|
+
),
|
|
203
|
+
{ fixIncludes: '--desc', detailsField: 'description' },
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// findTasks — query required
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
describe('error-hints: findTasks (query required)', () => {
|
|
213
|
+
it('missing query — fix mentions cleo find, field=query', async () => {
|
|
214
|
+
await assertAsyncErrorHints(() => findTasks({}), {
|
|
215
|
+
fixIncludes: 'cleo find',
|
|
216
|
+
detailsField: 'query',
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// showTask — task ID required
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
describe('error-hints: showTask (id required)', () => {
|
|
226
|
+
it('empty id — fix mentions cleo show, field=taskId', async () => {
|
|
227
|
+
await assertAsyncErrorHints(() => showTask(''), {
|
|
228
|
+
fixIncludes: 'cleo show',
|
|
229
|
+
detailsField: 'taskId',
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// showLabelTasks — no tasks with label
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
describe('error-hints: showLabelTasks (label not found)', () => {
|
|
239
|
+
let env: TestDbEnv;
|
|
240
|
+
|
|
241
|
+
beforeEach(async () => {
|
|
242
|
+
resetDbState();
|
|
243
|
+
env = await createTestDb();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
afterEach(async () => {
|
|
247
|
+
await env.cleanup();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('no tasks with label — fix mentions cleo labels, field=label', async () => {
|
|
251
|
+
await assertAsyncErrorHints(
|
|
252
|
+
() => showLabelTasks('nonexistent-label', env.tempDir, env.accessor),
|
|
253
|
+
{ fixIncludes: 'cleo labels', detailsField: 'label' },
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|