@cleocode/core 2026.9.15 → 2026.9.17
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/changesets/parser.d.ts.map +1 -1
- package/dist/changesets/parser.js +67 -0
- package/dist/changesets/parser.js.map +1 -1
- package/dist/crypto/credentials.d.ts +128 -26
- package/dist/crypto/credentials.d.ts.map +1 -1
- package/dist/crypto/credentials.js +183 -142
- package/dist/crypto/credentials.js.map +1 -1
- package/dist/docs/export-document.js +14705 -12949
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/index.d.ts +1 -1
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +1 -1
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/nexus-schema-residency.d.ts +1 -1
- package/dist/doctor/nexus-schema-residency.d.ts.map +1 -1
- package/dist/doctor/nexus-schema-residency.js +1 -0
- package/dist/doctor/nexus-schema-residency.js.map +1 -1
- package/dist/doctor/project-identity.d.ts +124 -0
- package/dist/doctor/project-identity.d.ts.map +1 -0
- package/dist/doctor/project-identity.js +307 -0
- package/dist/doctor/project-identity.js.map +1 -0
- package/dist/doctor/superseded-store.d.ts +8 -0
- package/dist/doctor/superseded-store.d.ts.map +1 -1
- package/dist/doctor/superseded-store.js +48 -3
- package/dist/doctor/superseded-store.js.map +1 -1
- package/dist/init.d.ts +7 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +17 -2
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +5 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -2
- package/dist/internal.js.map +1 -1
- package/dist/llm/credentials-store.d.ts +10 -0
- package/dist/llm/credentials-store.d.ts.map +1 -1
- package/dist/llm/credentials-store.js +7 -1
- package/dist/llm/credentials-store.js.map +1 -1
- package/dist/llm/plugin-facade.js +13368 -1550
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/nexus/analyze-orchestrator.d.ts +20 -3
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +159 -10
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/assessment-store.d.ts +57 -0
- package/dist/nexus/assessment-store.d.ts.map +1 -0
- package/dist/nexus/assessment-store.js +73 -0
- package/dist/nexus/assessment-store.js.map +1 -0
- package/dist/nexus/freshness.d.ts +131 -0
- package/dist/nexus/freshness.d.ts.map +1 -0
- package/dist/nexus/freshness.js +391 -0
- package/dist/nexus/freshness.js.map +1 -0
- package/dist/nexus/graph-manifest.d.ts +88 -0
- package/dist/nexus/graph-manifest.d.ts.map +1 -0
- package/dist/nexus/graph-manifest.js +114 -0
- package/dist/nexus/graph-manifest.js.map +1 -0
- package/dist/nexus/knowledge.d.ts +21 -1
- package/dist/nexus/knowledge.d.ts.map +1 -1
- package/dist/nexus/knowledge.js +76 -37
- package/dist/nexus/knowledge.js.map +1 -1
- package/dist/nexus/path-map.d.ts +55 -0
- package/dist/nexus/path-map.d.ts.map +1 -0
- package/dist/nexus/path-map.js +87 -0
- package/dist/nexus/path-map.js.map +1 -0
- package/dist/nexus/projects-clean.d.ts +21 -28
- package/dist/nexus/projects-clean.d.ts.map +1 -1
- package/dist/nexus/projects-clean.js +168 -85
- package/dist/nexus/projects-clean.js.map +1 -1
- package/dist/nexus/query.d.ts.map +1 -1
- package/dist/nexus/query.js +3 -2
- package/dist/nexus/query.js.map +1 -1
- package/dist/nexus/registry-hygiene.d.ts +70 -0
- package/dist/nexus/registry-hygiene.d.ts.map +1 -0
- package/dist/nexus/registry-hygiene.js +113 -0
- package/dist/nexus/registry-hygiene.js.map +1 -0
- package/dist/nexus/registry.d.ts +2 -2
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +48 -12
- package/dist/nexus/registry.js.map +1 -1
- package/dist/paths.d.ts +26 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +99 -6
- package/dist/paths.js.map +1 -1
- package/dist/render/nexus/graph/index.d.ts.map +1 -1
- package/dist/render/nexus/graph/index.js +8 -0
- package/dist/render/nexus/graph/index.js.map +1 -1
- package/dist/resources/spawn-wrapper.d.ts +0 -11
- package/dist/resources/spawn-wrapper.d.ts.map +1 -1
- package/dist/resources/spawn-wrapper.js +62 -6
- package/dist/resources/spawn-wrapper.js.map +1 -1
- package/dist/scaffold/ensure-config.d.ts +12 -3
- package/dist/scaffold/ensure-config.d.ts.map +1 -1
- package/dist/scaffold/ensure-config.js +61 -38
- package/dist/scaffold/ensure-config.js.map +1 -1
- package/dist/scaffold/project-identity.d.ts +136 -0
- package/dist/scaffold/project-identity.d.ts.map +1 -0
- package/dist/scaffold/project-identity.js +225 -0
- package/dist/scaffold/project-identity.js.map +1 -0
- package/dist/selfimprove/fix-gen.js +13319 -1556
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/store/agent-api-key.d.ts +114 -0
- package/dist/store/agent-api-key.d.ts.map +1 -0
- package/dist/store/agent-api-key.js +136 -0
- package/dist/store/agent-api-key.js.map +1 -0
- package/dist/store/agent-registry-accessor.d.ts +4 -2
- package/dist/store/agent-registry-accessor.d.ts.map +1 -1
- package/dist/store/agent-registry-accessor.js +39 -89
- package/dist/store/agent-registry-accessor.js.map +1 -1
- package/dist/store/api-key-kdf.d.ts +7 -0
- package/dist/store/api-key-kdf.d.ts.map +1 -1
- package/dist/store/api-key-kdf.js +7 -0
- package/dist/store/api-key-kdf.js.map +1 -1
- package/dist/store/backup-crypto.d.ts +49 -0
- package/dist/store/backup-crypto.d.ts.map +1 -1
- package/dist/store/backup-crypto.js +154 -0
- package/dist/store/backup-crypto.js.map +1 -1
- package/dist/store/credential-transfer.d.ts +303 -0
- package/dist/store/credential-transfer.d.ts.map +1 -0
- package/dist/store/credential-transfer.js +774 -0
- package/dist/store/credential-transfer.js.map +1 -0
- package/dist/store/exodus/column-transforms.d.ts +79 -4
- package/dist/store/exodus/column-transforms.d.ts.map +1 -1
- package/dist/store/exodus/column-transforms.js +121 -4
- package/dist/store/exodus/column-transforms.js.map +1 -1
- package/dist/store/exodus/index.d.ts +1 -0
- package/dist/store/exodus/index.d.ts.map +1 -1
- package/dist/store/exodus/index.js +1 -0
- package/dist/store/exodus/index.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts +17 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +155 -33
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/on-open.d.ts +14 -0
- package/dist/store/exodus/on-open.d.ts.map +1 -1
- package/dist/store/exodus/on-open.js +115 -6
- package/dist/store/exodus/on-open.js.map +1 -1
- package/dist/store/exodus/reconcile.d.ts +103 -0
- package/dist/store/exodus/reconcile.d.ts.map +1 -0
- package/dist/store/exodus/reconcile.js +728 -0
- package/dist/store/exodus/reconcile.js.map +1 -0
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +14 -2
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/store/exodus/runtime-targets.d.ts +39 -0
- package/dist/store/exodus/runtime-targets.d.ts.map +1 -0
- package/dist/store/exodus/runtime-targets.js +63 -0
- package/dist/store/exodus/runtime-targets.js.map +1 -0
- package/dist/store/exodus/table-name-map.d.ts.map +1 -1
- package/dist/store/exodus/table-name-map.js +5 -0
- package/dist/store/exodus/table-name-map.js.map +1 -1
- package/dist/store/exodus/table-order.d.ts +35 -0
- package/dist/store/exodus/table-order.d.ts.map +1 -0
- package/dist/store/exodus/table-order.js +62 -0
- package/dist/store/exodus/table-order.js.map +1 -0
- package/dist/store/exodus/verify-migration.d.ts +4 -1
- package/dist/store/exodus/verify-migration.d.ts.map +1 -1
- package/dist/store/exodus/verify-migration.js +142 -10
- package/dist/store/exodus/verify-migration.js.map +1 -1
- package/dist/store/global-salt.d.ts +16 -0
- package/dist/store/global-salt.d.ts.map +1 -1
- package/dist/store/global-salt.js +22 -7
- package/dist/store/global-salt.js.map +1 -1
- package/dist/store/legacy-tasks-lineage.d.ts +86 -0
- package/dist/store/legacy-tasks-lineage.d.ts.map +1 -0
- package/dist/store/legacy-tasks-lineage.js +285 -0
- package/dist/store/legacy-tasks-lineage.js.map +1 -0
- package/dist/store/migration-manager.d.ts +27 -0
- package/dist/store/migration-manager.d.ts.map +1 -1
- package/dist/store/migration-manager.js +63 -0
- package/dist/store/migration-manager.js.map +1 -1
- package/dist/store/portable-bundle-import.d.ts +117 -0
- package/dist/store/portable-bundle-import.d.ts.map +1 -0
- package/dist/store/portable-bundle-import.js +589 -0
- package/dist/store/portable-bundle-import.js.map +1 -0
- package/dist/store/portable-bundle-relocate.d.ts +66 -0
- package/dist/store/portable-bundle-relocate.d.ts.map +1 -0
- package/dist/store/portable-bundle-relocate.js +304 -0
- package/dist/store/portable-bundle-relocate.js.map +1 -0
- package/dist/store/portable-bundle-scan.d.ts +167 -0
- package/dist/store/portable-bundle-scan.d.ts.map +1 -0
- package/dist/store/portable-bundle-scan.js +569 -0
- package/dist/store/portable-bundle-scan.js.map +1 -0
- package/dist/store/portable-bundle.d.ts +146 -0
- package/dist/store/portable-bundle.d.ts.map +1 -0
- package/dist/store/portable-bundle.js +674 -0
- package/dist/store/portable-bundle.js.map +1 -0
- package/dist/store/regenerators.d.ts +3 -1
- package/dist/store/regenerators.d.ts.map +1 -1
- package/dist/store/regenerators.js +6 -2
- package/dist/store/regenerators.js.map +1 -1
- package/dist/store/schema/cleo-global/nexus.d.ts +98 -0
- package/dist/store/schema/cleo-global/nexus.d.ts.map +1 -1
- package/dist/store/schema/cleo-global/nexus.js +19 -0
- package/dist/store/schema/cleo-global/nexus.js.map +1 -1
- package/dist/store/schema/nexus-schema.d.ts +102 -0
- package/dist/store/schema/nexus-schema.d.ts.map +1 -1
- package/dist/store/schema/nexus-schema.js +24 -0
- package/dist/store/schema/nexus-schema.js.map +1 -1
- package/dist/store/split-brain-import.d.ts +176 -0
- package/dist/store/split-brain-import.d.ts.map +1 -0
- package/dist/store/split-brain-import.js +680 -0
- package/dist/store/split-brain-import.js.map +1 -0
- package/dist/store/sqlite.d.ts +29 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +57 -5
- package/dist/store/sqlite.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +30 -5
- package/dist/system/health.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +60 -0
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +14 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +33 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/migrations/drizzle-cleo-global/20260924000000_t12354-project-paths/migration.sql +40 -0
- package/migrations/drizzle-nexus/20260924000000_t12315-nexus-parse-cache/migration.sql +21 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +3 -3
- package/templates/cleo-gitignore +7 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* portable-bundle.ts — export side of the lossless machine-migration bundle
|
|
3
|
+
* (manifest v2).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the v1 pack path for `cleo backup export`. v1 snapshotted a fixed
|
|
6
|
+
* list of pre-E6 filenames (`tasks.db`, `brain.db`, `nexus.db`, …); after the
|
|
7
|
+
* E6 cutover (ADR-068) the live stores are the consolidated `cleo.db` files,
|
|
8
|
+
* so v1 exported nothing of value and still reported success. v2:
|
|
9
|
+
*
|
|
10
|
+
* - snapshots the live primary store of every in-scope root with
|
|
11
|
+
* `VACUUM INTO` (read-only, consistent under WAL) and fails when a primary
|
|
12
|
+
* store is missing or unreadable;
|
|
13
|
+
* - captures the rest of the root byte-for-byte, except an explicit denylist
|
|
14
|
+
* whose sizes are reported, and SQLite files, which are also snapshotted;
|
|
15
|
+
* - includes secrets only in encrypted bundles, recording what was omitted
|
|
16
|
+
* and what the user must redo (each credential with its re-entry command);
|
|
17
|
+
* - never carries the machine-key: encrypted bundles instead carry the
|
|
18
|
+
* credential VALUES sealed under the passphrase (`secrets/<section>.sealed`,
|
|
19
|
+
* T12326), which the importing device re-encrypts under its own key;
|
|
20
|
+
* - records per-table row counts so import can prove the restore lossless.
|
|
21
|
+
*
|
|
22
|
+
* Archive layout:
|
|
23
|
+
* manifest.json FIRST entry
|
|
24
|
+
* checksums.sha256 GNU sha256sum format over every other file
|
|
25
|
+
* global/home/<rel> <cleoHome> content
|
|
26
|
+
* global/config/<rel> <configHome> content
|
|
27
|
+
* projects/<nnn>-<name>/cleo/<rel> a project's .cleo/ content
|
|
28
|
+
* secrets/<section>.sealed passphrase-sealed credentials (encrypted only)
|
|
29
|
+
*
|
|
30
|
+
* @task T12318
|
|
31
|
+
* @epic T12317
|
|
32
|
+
* @module store/portable-bundle
|
|
33
|
+
*/
|
|
34
|
+
import crypto from 'node:crypto';
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
import os from 'node:os';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { ExitCode, } from '@cleocode/contracts';
|
|
40
|
+
import { create as tarCreate } from 'tar';
|
|
41
|
+
import { getCleoConfigDir, getCleoHome } from '../paths.js';
|
|
42
|
+
import { getCleoVersion } from '../scaffold/ensure-config.js';
|
|
43
|
+
import { encryptFileStream } from './backup-crypto.js';
|
|
44
|
+
import { listCredentialsForReentry, sealCredentials, } from './credential-transfer.js';
|
|
45
|
+
import { resolveDualScopeDbPath } from './dual-scope-db.js';
|
|
46
|
+
import { CONFIG_HOME_RULES, countRows, GLOBAL_HOME_RULES, LEGACY_STORE_BASENAMES, LEGACY_TABLE_MAP, MEMORY_TABLES, PRIMARY_STORE_BASENAME, PROJECT_SECTION_RULES, pickKeyCounts, redactCredentials, scanSection, sha256File, vacuumSnapshot, } from './portable-bundle-scan.js';
|
|
47
|
+
const _require = createRequire(import.meta.url);
|
|
48
|
+
const { DatabaseSync } = _require('node:sqlite');
|
|
49
|
+
/**
|
|
50
|
+
* Numeric exit codes for {@link PortableBundleErrorCode}. Decrypt / format /
|
|
51
|
+
* integrity / data-exists keep the v1 bundle codes (ADR-038 §4.3) so scripts
|
|
52
|
+
* written against v1 keep working; the rest reuse the shared `ExitCode` enum.
|
|
53
|
+
*/
|
|
54
|
+
export const PORTABLE_BUNDLE_EXIT_CODES = {
|
|
55
|
+
E_PRIMARY_STORE_MISSING: ExitCode.NOT_FOUND,
|
|
56
|
+
E_PRIMARY_STORE_UNREADABLE: ExitCode.FILE_ERROR,
|
|
57
|
+
E_NO_PROJECT: ExitCode.NOT_FOUND,
|
|
58
|
+
E_REGISTRY_UNREADABLE: ExitCode.FILE_ERROR,
|
|
59
|
+
E_PASSPHRASE_REQUIRED: ExitCode.INVALID_INPUT,
|
|
60
|
+
E_BUNDLE_DECRYPT: 70,
|
|
61
|
+
E_BUNDLE_FORMAT: 71,
|
|
62
|
+
E_BUNDLE_INTEGRITY: 72,
|
|
63
|
+
E_DATA_EXISTS: 78,
|
|
64
|
+
E_TARGET_AMBIGUOUS: ExitCode.INVALID_INPUT,
|
|
65
|
+
E_RESTORE_MISMATCH: ExitCode.CHECKSUM_MISMATCH,
|
|
66
|
+
E_REDACTION_FAILED: ExitCode.GENERAL_ERROR,
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Error raised by the portable bundle export/import. Carries a symbolic code
|
|
70
|
+
* and an exit code so the CLI can emit a LAFS error envelope.
|
|
71
|
+
*/
|
|
72
|
+
export class PortableBundleError extends Error {
|
|
73
|
+
code;
|
|
74
|
+
details;
|
|
75
|
+
/** Numeric process exit code. */
|
|
76
|
+
exitCode;
|
|
77
|
+
/**
|
|
78
|
+
* @param code - Symbolic error code.
|
|
79
|
+
* @param message - Human-readable description.
|
|
80
|
+
* @param details - Structured context (e.g. the full import report on a mismatch).
|
|
81
|
+
*/
|
|
82
|
+
constructor(code, message, details) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.code = code;
|
|
85
|
+
this.details = details;
|
|
86
|
+
this.name = 'PortableBundleError';
|
|
87
|
+
this.exitCode = PORTABLE_BUNDLE_EXIT_CODES[code];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* True when a path is a temp or test-fixture directory that must never be
|
|
92
|
+
* exported as a project (the registry accumulates these from test runs).
|
|
93
|
+
*
|
|
94
|
+
* @param absPath - Resolved project path.
|
|
95
|
+
* @returns Whether the path is temp/fixture material.
|
|
96
|
+
*/
|
|
97
|
+
export function isTempProjectPath(absPath) {
|
|
98
|
+
const home = os.homedir();
|
|
99
|
+
const tempRoots = [os.tmpdir(), '/tmp', '/var/tmp', path.join(home, '.temp')].map((p) => path.resolve(p));
|
|
100
|
+
const resolved = path.resolve(absPath);
|
|
101
|
+
if (tempRoots.some((r) => resolved === r || resolved.startsWith(`${r}${path.sep}`)))
|
|
102
|
+
return true;
|
|
103
|
+
return /(^|[/\\._-])(vitest|regression|fixtures?)([/\\._-]|$)/i.test(resolved);
|
|
104
|
+
}
|
|
105
|
+
function realpathOrNull(p) {
|
|
106
|
+
try {
|
|
107
|
+
return fs.realpathSync(p);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Choose which registered projects `machine` scope exports.
|
|
115
|
+
*
|
|
116
|
+
* @param rows - Registry rows.
|
|
117
|
+
* @param cleoHome - Global home (a project whose `.cleo/` IS the home is skipped).
|
|
118
|
+
* @param isTempPath - Temp/fixture predicate.
|
|
119
|
+
* @returns Included project roots and skipped rows with reasons.
|
|
120
|
+
*/
|
|
121
|
+
export function selectMachineProjects(rows, cleoHome, isTempPath = isTempProjectPath) {
|
|
122
|
+
const included = [];
|
|
123
|
+
const skipped = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const homeReal = realpathOrNull(cleoHome) ?? path.resolve(cleoHome);
|
|
126
|
+
const skip = (row, reason) => {
|
|
127
|
+
skipped.push({ path: row.path, projectId: row.projectId, reason });
|
|
128
|
+
};
|
|
129
|
+
for (const row of [...rows].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
130
|
+
if (isTempPath(row.path)) {
|
|
131
|
+
skip(row, 'temp-path');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const real = realpathOrNull(row.path);
|
|
135
|
+
if (real === null || !fs.statSync(real).isDirectory()) {
|
|
136
|
+
skip(row, 'path-missing');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (isTempPath(real)) {
|
|
140
|
+
skip(row, 'temp-path');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const cleoDirReal = realpathOrNull(path.join(real, '.cleo'));
|
|
144
|
+
if (cleoDirReal !== null && cleoDirReal === homeReal) {
|
|
145
|
+
skip(row, 'is-global-home');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const storeNames = [PRIMARY_STORE_BASENAME, ...LEGACY_STORE_BASENAMES.project];
|
|
149
|
+
if (!storeNames.some((n) => fs.existsSync(path.join(real, '.cleo', n)))) {
|
|
150
|
+
skip(row, 'no-live-store');
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (seen.has(real)) {
|
|
154
|
+
skip(row, 'duplicate-path');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
seen.add(real);
|
|
158
|
+
included.push({ projectId: row.projectId, path: real });
|
|
159
|
+
}
|
|
160
|
+
return { included, skipped };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Read the project registry out of a global-store snapshot.
|
|
164
|
+
*
|
|
165
|
+
* @param snapshotPath - VACUUM snapshot of the global primary store.
|
|
166
|
+
* @returns Registry rows.
|
|
167
|
+
* @throws {PortableBundleError} `E_REGISTRY_UNREADABLE` when the table cannot be read.
|
|
168
|
+
*/
|
|
169
|
+
function readRegistrySnapshot(snapshotPath) {
|
|
170
|
+
const db = new DatabaseSync(snapshotPath, { readOnly: true });
|
|
171
|
+
try {
|
|
172
|
+
return db
|
|
173
|
+
.prepare('SELECT project_id AS projectId, project_path AS path FROM nexus_project_registry')
|
|
174
|
+
.all()
|
|
175
|
+
.filter((r) => typeof r.path === 'string')
|
|
176
|
+
.map((r) => ({ projectId: r.projectId ?? '', path: r.path }));
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
throw new PortableBundleError('E_REGISTRY_UNREADABLE', `Cannot read the project registry from the global store: ${err instanceof Error ? err.message : String(err)}`);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
db.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Credential tables whose rows {@link listCredentialsForReentry} enumerates, by store. */
|
|
186
|
+
const CREDENTIAL_STORE_BY_TABLE = {
|
|
187
|
+
tasks_agent_credentials: 'project-agent',
|
|
188
|
+
service_connections: 'service-connection',
|
|
189
|
+
agent_registry_agents: 'agent-registry',
|
|
190
|
+
};
|
|
191
|
+
/** The LLM pool file in the global home. */
|
|
192
|
+
const LLM_POOL_RELPATH = 'llm-credentials.json';
|
|
193
|
+
/**
|
|
194
|
+
* Seal a section's credentials under the bundle passphrase (encrypted bundles
|
|
195
|
+
* only) and stage the payload at `secrets/<key>.sealed`. The payload carries
|
|
196
|
+
* credential values — never the machine-key — so the importing device can
|
|
197
|
+
* re-encrypt them under its own key (T12326).
|
|
198
|
+
*
|
|
199
|
+
* @param state - Staging accumulator.
|
|
200
|
+
* @param section - Section to annotate.
|
|
201
|
+
* @param key - Unique payload name within the bundle.
|
|
202
|
+
* @param sources - Staged snapshot paths (+ project identity) to read.
|
|
203
|
+
*/
|
|
204
|
+
async function sealSectionCredentials(state, section, key, sources) {
|
|
205
|
+
if (state.passphrase === null)
|
|
206
|
+
return;
|
|
207
|
+
const sealed = await sealCredentials({ ...sources, cleoHome: state.cleoHome }, state.passphrase);
|
|
208
|
+
if (sealed.sealedCredentials.length === 0 && sealed.reentry.length === 0)
|
|
209
|
+
return;
|
|
210
|
+
const archivePath = `secrets/${key}.sealed`;
|
|
211
|
+
const staged = path.join(state.stagingDir, archivePath);
|
|
212
|
+
fs.mkdirSync(path.dirname(staged), { recursive: true });
|
|
213
|
+
fs.writeFileSync(staged, sealed.sealed, { mode: 0o600 });
|
|
214
|
+
section.sealedCredentials = {
|
|
215
|
+
bundlePath: archivePath,
|
|
216
|
+
size: fs.statSync(staged).size,
|
|
217
|
+
sha256: await sha256File(staged),
|
|
218
|
+
credentials: [...sealed.sealedCredentials],
|
|
219
|
+
reentry: [...sealed.reentry],
|
|
220
|
+
};
|
|
221
|
+
state.archivePaths.push(archivePath);
|
|
222
|
+
}
|
|
223
|
+
function toArchive(prefix, relPath) {
|
|
224
|
+
return `${prefix}/${relPath}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Stage one root into the archive staging directory.
|
|
228
|
+
*
|
|
229
|
+
* @param state - Staging accumulator.
|
|
230
|
+
* @param root - Absolute root.
|
|
231
|
+
* @param prefix - Archive prefix for this section.
|
|
232
|
+
* @param rules - Walk rules.
|
|
233
|
+
* @param tier - Which legacy-name set applies.
|
|
234
|
+
* @param primaryRelPath - Relative path of the primary store (null when the section has none).
|
|
235
|
+
* @returns The populated section.
|
|
236
|
+
*/
|
|
237
|
+
async function stageSection(state, root, prefix, rules, tier, primaryRelPath) {
|
|
238
|
+
const scan = scanSection(root, rules, state.skipAbsolute);
|
|
239
|
+
const section = {
|
|
240
|
+
originalRoot: root,
|
|
241
|
+
bundlePrefix: prefix,
|
|
242
|
+
databases: [],
|
|
243
|
+
files: [],
|
|
244
|
+
symlinks: scan.symlinks,
|
|
245
|
+
excluded: scan.excluded,
|
|
246
|
+
requiresReentry: [],
|
|
247
|
+
};
|
|
248
|
+
const legacy = tier === 'config' ? new Set() : LEGACY_STORE_BASENAMES[tier];
|
|
249
|
+
// A missing primary is fatal only when no legacy store could hold the data:
|
|
250
|
+
// some projects were never migrated and their ONLY copy is in tasks.db/brain.db.
|
|
251
|
+
const hasLegacyStore = scan.sqlite.some((rel) => legacy.has(rel));
|
|
252
|
+
const primaryAbs = primaryRelPath === null ? null : path.join(root, primaryRelPath);
|
|
253
|
+
const primaryUnreadable = primaryAbs !== null && fs.existsSync(primaryAbs);
|
|
254
|
+
if (primaryRelPath !== null &&
|
|
255
|
+
!scan.sqlite.includes(primaryRelPath) &&
|
|
256
|
+
(primaryUnreadable || !hasLegacyStore)) {
|
|
257
|
+
const abs = path.join(root, primaryRelPath);
|
|
258
|
+
throw new PortableBundleError(fs.existsSync(abs) ? 'E_PRIMARY_STORE_UNREADABLE' : 'E_PRIMARY_STORE_MISSING', fs.existsSync(abs)
|
|
259
|
+
? `Primary store is not a readable SQLite database: ${abs}`
|
|
260
|
+
: `Primary store is missing: ${abs}`);
|
|
261
|
+
}
|
|
262
|
+
for (const relPath of scan.sqlite) {
|
|
263
|
+
const archivePath = toArchive(prefix, relPath);
|
|
264
|
+
const staged = path.join(state.stagingDir, archivePath);
|
|
265
|
+
const isPrimary = relPath === primaryRelPath;
|
|
266
|
+
try {
|
|
267
|
+
vacuumSnapshot(path.join(root, relPath), staged);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (isPrimary) {
|
|
271
|
+
throw new PortableBundleError('E_PRIMARY_STORE_UNREADABLE', `Cannot snapshot primary store ${path.join(root, relPath)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
272
|
+
}
|
|
273
|
+
fs.rmSync(staged, { force: true });
|
|
274
|
+
if (state.includeSecrets) {
|
|
275
|
+
// Encrypted bundle: capture the unreadable file byte-for-byte so nothing is lost.
|
|
276
|
+
scan.files.push(relPath);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Unencrypted: a raw copy could carry credential columns that cannot be cleared.
|
|
280
|
+
section.excluded.push({
|
|
281
|
+
relPath,
|
|
282
|
+
reason: `SQLite file could not be opened for a snapshot (${err instanceof Error ? err.message : String(err)}); not copied raw into an unencrypted bundle because its credential columns cannot be cleared — re-export with --encrypt to carry it`,
|
|
283
|
+
bytes: fs.statSync(path.join(root, relPath)).size,
|
|
284
|
+
fileCount: 1,
|
|
285
|
+
sizeComplete: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!state.includeSecrets) {
|
|
291
|
+
try {
|
|
292
|
+
// Enumerate BEFORE clearing: afterwards the rows no longer say which
|
|
293
|
+
// credentials they held.
|
|
294
|
+
const enumerable = tier === 'project'
|
|
295
|
+
? listCredentialsForReentry({ projectDbPath: staged })
|
|
296
|
+
: tier === 'global'
|
|
297
|
+
? listCredentialsForReentry({ globalDbPath: staged })
|
|
298
|
+
: [];
|
|
299
|
+
for (const r of redactCredentials(staged)) {
|
|
300
|
+
const credentials = enumerable.filter((c) => CREDENTIAL_STORE_BY_TABLE[r.table] === c.store);
|
|
301
|
+
section.requiresReentry.push({
|
|
302
|
+
relPath,
|
|
303
|
+
table: r.table,
|
|
304
|
+
columns: r.columns,
|
|
305
|
+
rows: r.rows,
|
|
306
|
+
remedy: r.remedy,
|
|
307
|
+
...(credentials.length > 0 ? { credentials } : {}),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
throw new PortableBundleError('E_REDACTION_FAILED', `Cannot clear credential columns in the snapshot of ${path.join(root, relPath)}; refusing to write an unencrypted bundle that could carry them (use --encrypt): ${err instanceof Error ? err.message : String(err)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const counts = countRows(staged);
|
|
316
|
+
const entry = {
|
|
317
|
+
relPath,
|
|
318
|
+
bundlePath: archivePath,
|
|
319
|
+
role: isPrimary ? 'primary' : legacy.has(relPath) ? 'legacy' : 'auxiliary',
|
|
320
|
+
size: fs.statSync(staged).size,
|
|
321
|
+
sha256: await sha256File(staged),
|
|
322
|
+
rowCounts: counts.rowCounts,
|
|
323
|
+
uncountedTables: counts.uncountedTables,
|
|
324
|
+
};
|
|
325
|
+
section.databases.push(entry);
|
|
326
|
+
state.archivePaths.push(archivePath);
|
|
327
|
+
}
|
|
328
|
+
const copyFile = async (relPath, secret) => {
|
|
329
|
+
const src = path.join(root, relPath);
|
|
330
|
+
const archivePath = toArchive(prefix, relPath);
|
|
331
|
+
const staged = path.join(state.stagingDir, archivePath);
|
|
332
|
+
fs.mkdirSync(path.dirname(staged), { recursive: true });
|
|
333
|
+
fs.copyFileSync(src, staged);
|
|
334
|
+
fs.chmodSync(staged, fs.statSync(src).mode & 0o777);
|
|
335
|
+
const entry = {
|
|
336
|
+
relPath,
|
|
337
|
+
bundlePath: archivePath,
|
|
338
|
+
size: fs.statSync(staged).size,
|
|
339
|
+
sha256: await sha256File(staged),
|
|
340
|
+
secret,
|
|
341
|
+
};
|
|
342
|
+
section.files.push(entry);
|
|
343
|
+
state.archivePaths.push(archivePath);
|
|
344
|
+
};
|
|
345
|
+
for (const relPath of scan.files.sort())
|
|
346
|
+
await copyFile(relPath, false);
|
|
347
|
+
for (const s of scan.secrets) {
|
|
348
|
+
if (state.includeSecrets) {
|
|
349
|
+
await copyFile(s.relPath, true);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const credentials = tier === 'global' && s.relPath === LLM_POOL_RELPATH
|
|
353
|
+
? listCredentialsForReentry({ llmStorePath: path.join(root, s.relPath) })
|
|
354
|
+
: [];
|
|
355
|
+
section.requiresReentry.push({
|
|
356
|
+
relPath: s.relPath,
|
|
357
|
+
remedy: s.remedy,
|
|
358
|
+
...(credentials.length > 0 ? { credentials } : {}),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return section;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Compare legacy per-domain tables with their consolidated counterparts.
|
|
365
|
+
*
|
|
366
|
+
* @param databases - Snapshotted databases of one root.
|
|
367
|
+
* @returns Report; `detected` when any legacy table holds more rows than the primary.
|
|
368
|
+
*/
|
|
369
|
+
export function detectUnmigratedLegacy(databases) {
|
|
370
|
+
const primary = databases.find((d) => d.role === 'primary');
|
|
371
|
+
const evidence = [];
|
|
372
|
+
for (const db of databases.filter((d) => d.role === 'legacy')) {
|
|
373
|
+
for (const [table, primaryTable] of Object.entries(LEGACY_TABLE_MAP)) {
|
|
374
|
+
const legacyRows = db.rowCounts[table];
|
|
375
|
+
if (legacyRows === undefined || legacyRows === 0)
|
|
376
|
+
continue;
|
|
377
|
+
const primaryRows = primary?.rowCounts[primaryTable] ?? 0;
|
|
378
|
+
if (legacyRows > primaryRows) {
|
|
379
|
+
const unprefixed = table === primaryTable ? undefined : primary?.rowCounts[table];
|
|
380
|
+
evidence.push({
|
|
381
|
+
database: db.relPath,
|
|
382
|
+
table,
|
|
383
|
+
legacyRows,
|
|
384
|
+
primaryTable,
|
|
385
|
+
primaryRows,
|
|
386
|
+
...(unprefixed !== undefined ? { primaryUnprefixedRows: unprefixed } : {}),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { detected: evidence.length > 0, evidence };
|
|
392
|
+
}
|
|
393
|
+
function readProjectInfo(cleoDir) {
|
|
394
|
+
try {
|
|
395
|
+
const raw = JSON.parse(fs.readFileSync(path.join(cleoDir, 'project-info.json'), 'utf-8'));
|
|
396
|
+
return {
|
|
397
|
+
projectId: typeof raw.projectId === 'string' ? raw.projectId : null,
|
|
398
|
+
name: typeof raw.name === 'string' ? raw.name : null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return { projectId: null, name: null };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function stageProject(state, projectRoot, index, registryProjectId) {
|
|
406
|
+
const cleoDir = path.join(projectRoot, '.cleo');
|
|
407
|
+
if (!fs.existsSync(cleoDir)) {
|
|
408
|
+
throw new PortableBundleError('E_NO_PROJECT', `No .cleo directory at ${projectRoot}`);
|
|
409
|
+
}
|
|
410
|
+
const info = readProjectInfo(cleoDir);
|
|
411
|
+
const name = info.name ?? path.basename(projectRoot);
|
|
412
|
+
const safe = name.replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 60) || 'project';
|
|
413
|
+
const prefix = `projects/${String(index).padStart(3, '0')}-${safe}/cleo`;
|
|
414
|
+
const base = await stageSection(state, cleoDir, prefix, PROJECT_SECTION_RULES, 'project', PRIMARY_STORE_BASENAME);
|
|
415
|
+
const primary = base.databases.find((d) => d.role === 'primary');
|
|
416
|
+
const projectId = info.projectId ?? (registryProjectId || null);
|
|
417
|
+
if (primary) {
|
|
418
|
+
await sealSectionCredentials(state, base, `project-${String(index).padStart(3, '0')}`, {
|
|
419
|
+
projectDbPath: path.join(state.stagingDir, primary.bundlePath),
|
|
420
|
+
...(projectId !== null ? { projectId } : {}),
|
|
421
|
+
legacyProjectPaths: [projectRoot],
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
...base,
|
|
426
|
+
originalPath: projectRoot,
|
|
427
|
+
projectId,
|
|
428
|
+
name,
|
|
429
|
+
keyCounts: primary ? pickKeyCounts(primary.rowCounts) : {},
|
|
430
|
+
unmigratedLegacyData: detectUnmigratedLegacy(base.databases),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Public API
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
/**
|
|
437
|
+
* Compute the manifest self-hash (SHA-256 of the compact JSON with
|
|
438
|
+
* `integrity.manifestHash` set to "").
|
|
439
|
+
*
|
|
440
|
+
* @param manifest - Manifest to hash.
|
|
441
|
+
* @returns Lowercase hex digest.
|
|
442
|
+
*/
|
|
443
|
+
export function computeManifestHash(manifest) {
|
|
444
|
+
const placeholder = {
|
|
445
|
+
...manifest,
|
|
446
|
+
integrity: { ...manifest.integrity, manifestHash: '' },
|
|
447
|
+
};
|
|
448
|
+
return crypto.createHash('sha256').update(JSON.stringify(placeholder), 'utf-8').digest('hex');
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Export a portable (manifest v2) bundle.
|
|
452
|
+
*
|
|
453
|
+
* Reads only: every database is snapshotted over a read-only connection and
|
|
454
|
+
* nothing is written outside the output path's directory (staging lives
|
|
455
|
+
* beside the output and is removed on success or failure).
|
|
456
|
+
*
|
|
457
|
+
* @param input - Export options.
|
|
458
|
+
* @returns Export summary.
|
|
459
|
+
* @throws {PortableBundleError} When a primary store is missing/unreadable, the
|
|
460
|
+
* registry cannot be read (machine scope), or encryption lacks a passphrase.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```ts
|
|
464
|
+
* const r = await exportPortableBundle({ scope: 'project', projectRoot: '/p', outputPath: '/b/p.cleobundle.tar.gz', label: 'p' });
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
export async function exportPortableBundle(input) {
|
|
468
|
+
const encrypt = input.encrypt === true;
|
|
469
|
+
if (encrypt && !input.passphrase) {
|
|
470
|
+
throw new PortableBundleError('E_PASSPHRASE_REQUIRED', '--encrypt requires a passphrase');
|
|
471
|
+
}
|
|
472
|
+
const scope = input.scope;
|
|
473
|
+
const withProject = scope === 'project' || scope === 'all';
|
|
474
|
+
const withGlobal = scope !== 'project';
|
|
475
|
+
if (withProject && !input.projectRoot) {
|
|
476
|
+
throw new PortableBundleError('E_NO_PROJECT', `scope "${scope}" requires a project root`);
|
|
477
|
+
}
|
|
478
|
+
const cleoHome = path.resolve(input.cleoHome ?? getCleoHome());
|
|
479
|
+
const configHome = path.resolve(input.configHome ?? getCleoConfigDir());
|
|
480
|
+
const outputPath = path.resolve(input.outputPath);
|
|
481
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
482
|
+
const stagingDir = fs.mkdtempSync(path.join(path.dirname(outputPath), '.cleo-export-'));
|
|
483
|
+
const partialPath = `${outputPath}.partial`;
|
|
484
|
+
const tarPath = encrypt ? `${outputPath}.partial-tar` : partialPath;
|
|
485
|
+
const state = {
|
|
486
|
+
stagingDir,
|
|
487
|
+
archivePaths: [],
|
|
488
|
+
skipAbsolute: new Set([stagingDir, partialPath, tarPath, outputPath]),
|
|
489
|
+
includeSecrets: encrypt,
|
|
490
|
+
passphrase: encrypt && input.passphrase ? input.passphrase : null,
|
|
491
|
+
cleoHome,
|
|
492
|
+
};
|
|
493
|
+
try {
|
|
494
|
+
let global = null;
|
|
495
|
+
const projects = [];
|
|
496
|
+
let skippedProjects = [];
|
|
497
|
+
let registeredCount = 0;
|
|
498
|
+
if (withGlobal) {
|
|
499
|
+
const primaryAbs = resolveDualScopeDbPath('global', undefined, cleoHome);
|
|
500
|
+
const primaryRel = path.relative(cleoHome, primaryAbs).split(path.sep).join('/');
|
|
501
|
+
if (!fs.existsSync(cleoHome)) {
|
|
502
|
+
throw new PortableBundleError('E_PRIMARY_STORE_MISSING', `Global CLEO home does not exist: ${cleoHome}`);
|
|
503
|
+
}
|
|
504
|
+
const home = await stageSection(state, cleoHome, 'global/home', GLOBAL_HOME_RULES, 'global', primaryRel);
|
|
505
|
+
const config = fs.existsSync(configHome)
|
|
506
|
+
? await stageSection(state, configHome, 'global/config', CONFIG_HOME_RULES, 'config', null)
|
|
507
|
+
: null;
|
|
508
|
+
const primary = home.databases.find((d) => d.role === 'primary');
|
|
509
|
+
if (primary) {
|
|
510
|
+
await sealSectionCredentials(state, home, 'global-home', {
|
|
511
|
+
globalDbPath: path.join(stagingDir, primary.bundlePath),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
global = {
|
|
515
|
+
home,
|
|
516
|
+
config,
|
|
517
|
+
keyCounts: primary ? pickKeyCounts(primary.rowCounts) : {},
|
|
518
|
+
unmigratedLegacyData: detectUnmigratedLegacy(home.databases),
|
|
519
|
+
};
|
|
520
|
+
if (scope === 'machine' && primary) {
|
|
521
|
+
const rows = readRegistrySnapshot(path.join(stagingDir, primary.bundlePath));
|
|
522
|
+
registeredCount = rows.length;
|
|
523
|
+
const selection = selectMachineProjects(rows, cleoHome, input.isTempPath);
|
|
524
|
+
skippedProjects = selection.skipped;
|
|
525
|
+
for (const row of selection.included) {
|
|
526
|
+
projects.push(await stageProject(state, row.path, projects.length, row.projectId));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (withProject && input.projectRoot) {
|
|
531
|
+
projects.push(await stageProject(state, path.resolve(input.projectRoot), projects.length));
|
|
532
|
+
}
|
|
533
|
+
const manifest = {
|
|
534
|
+
format: 'cleo-portable-bundle',
|
|
535
|
+
manifestVersion: '2.0.0',
|
|
536
|
+
backup: {
|
|
537
|
+
createdAt: new Date().toISOString(),
|
|
538
|
+
cleoVersion: getCleoVersion(),
|
|
539
|
+
scope,
|
|
540
|
+
label: input.label,
|
|
541
|
+
sourceHost: os.hostname(),
|
|
542
|
+
encrypted: encrypt,
|
|
543
|
+
secretsIncluded: encrypt,
|
|
544
|
+
memoriesIncluded: true,
|
|
545
|
+
},
|
|
546
|
+
global,
|
|
547
|
+
projects,
|
|
548
|
+
skippedProjects,
|
|
549
|
+
integrity: { algorithm: 'sha256', manifestHash: '' },
|
|
550
|
+
};
|
|
551
|
+
manifest.integrity.manifestHash = computeManifestHash(manifest);
|
|
552
|
+
fs.writeFileSync(path.join(stagingDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
553
|
+
const checksumLines = [];
|
|
554
|
+
for (const section of allSections(manifest)) {
|
|
555
|
+
for (const d of section.databases)
|
|
556
|
+
checksumLines.push(`${d.sha256} ${d.bundlePath}`);
|
|
557
|
+
for (const f of section.files)
|
|
558
|
+
checksumLines.push(`${f.sha256} ${f.bundlePath}`);
|
|
559
|
+
const sealed = section.sealedCredentials;
|
|
560
|
+
if (sealed)
|
|
561
|
+
checksumLines.push(`${sealed.sha256} ${sealed.bundlePath}`);
|
|
562
|
+
}
|
|
563
|
+
fs.writeFileSync(path.join(stagingDir, 'checksums.sha256'), `${checksumLines.join('\n')}\n`);
|
|
564
|
+
await tarCreate({ gzip: true, file: tarPath, cwd: stagingDir }, [
|
|
565
|
+
'manifest.json',
|
|
566
|
+
'checksums.sha256',
|
|
567
|
+
...state.archivePaths,
|
|
568
|
+
]);
|
|
569
|
+
if (encrypt && input.passphrase) {
|
|
570
|
+
await encryptFileStream(tarPath, partialPath, input.passphrase);
|
|
571
|
+
fs.rmSync(tarPath, { force: true });
|
|
572
|
+
}
|
|
573
|
+
fs.renameSync(partialPath, outputPath);
|
|
574
|
+
const skippedByReason = {};
|
|
575
|
+
for (const s of skippedProjects) {
|
|
576
|
+
skippedByReason[s.reason] = (skippedByReason[s.reason] ?? 0) + 1;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
bundlePath: outputPath,
|
|
580
|
+
size: fs.statSync(outputPath).size,
|
|
581
|
+
scope,
|
|
582
|
+
encrypted: encrypt,
|
|
583
|
+
secretsIncluded: encrypt,
|
|
584
|
+
memory: memoryDisclosure(manifest, encrypt),
|
|
585
|
+
sections: [
|
|
586
|
+
...(global
|
|
587
|
+
? [
|
|
588
|
+
{
|
|
589
|
+
...summarise('global-home', global.home, global.keyCounts),
|
|
590
|
+
unmigratedLegacyData: global.unmigratedLegacyData,
|
|
591
|
+
},
|
|
592
|
+
...(global.config ? [summarise('global-config', global.config)] : []),
|
|
593
|
+
]
|
|
594
|
+
: []),
|
|
595
|
+
...projects.map((p) => ({
|
|
596
|
+
...summarise('project', p, p.keyCounts),
|
|
597
|
+
name: p.name,
|
|
598
|
+
unmigratedLegacyData: p.unmigratedLegacyData,
|
|
599
|
+
})),
|
|
600
|
+
],
|
|
601
|
+
...(scope === 'machine'
|
|
602
|
+
? {
|
|
603
|
+
machine: {
|
|
604
|
+
registered: registeredCount,
|
|
605
|
+
included: projects.length,
|
|
606
|
+
skippedByReason,
|
|
607
|
+
},
|
|
608
|
+
}
|
|
609
|
+
: {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
614
|
+
fs.rmSync(partialPath, { force: true });
|
|
615
|
+
fs.rmSync(tarPath, { force: true });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Build the ADR-093 memory disclosure: memories always travel, and the result
|
|
620
|
+
* says whether bundle encryption protects them.
|
|
621
|
+
*
|
|
622
|
+
* @param manifest - Written manifest.
|
|
623
|
+
* @param encrypted - Whether the bundle is encrypted.
|
|
624
|
+
* @returns Disclosure block for the export result.
|
|
625
|
+
*/
|
|
626
|
+
function memoryDisclosure(manifest, encrypted) {
|
|
627
|
+
const counts = {};
|
|
628
|
+
for (const section of allSections(manifest)) {
|
|
629
|
+
// Live and legacy stores only; auxiliary `.bak` snapshots would double-count.
|
|
630
|
+
for (const db of section.databases.filter((d) => d.role !== 'auxiliary')) {
|
|
631
|
+
for (const table of MEMORY_TABLES) {
|
|
632
|
+
const n = db.rowCounts[table];
|
|
633
|
+
if (n !== undefined)
|
|
634
|
+
counts[table] = (counts[table] ?? 0) + n;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
included: true,
|
|
640
|
+
encrypted,
|
|
641
|
+
counts,
|
|
642
|
+
notice: encrypted
|
|
643
|
+
? 'Memories (brain observations, decisions, learnings, patterns) are included and protected by bundle encryption.'
|
|
644
|
+
: 'Memories (brain observations, decisions, learnings, patterns) are included in PLAIN TEXT in this unencrypted bundle (ADR-093). Re-export with --encrypt to protect them.',
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function summarise(kind, section, keyCounts) {
|
|
648
|
+
return {
|
|
649
|
+
kind,
|
|
650
|
+
root: section.originalRoot,
|
|
651
|
+
databases: section.databases.length,
|
|
652
|
+
files: section.files.length,
|
|
653
|
+
...(keyCounts ? { keyCounts } : {}),
|
|
654
|
+
excluded: section.excluded,
|
|
655
|
+
requiresReentry: section.requiresReentry,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Every section of a manifest, in archive order.
|
|
660
|
+
*
|
|
661
|
+
* @param manifest - Portable manifest.
|
|
662
|
+
* @returns Sections (global home, global config, projects).
|
|
663
|
+
*/
|
|
664
|
+
export function allSections(manifest) {
|
|
665
|
+
const out = [];
|
|
666
|
+
if (manifest.global) {
|
|
667
|
+
out.push(manifest.global.home);
|
|
668
|
+
if (manifest.global.config)
|
|
669
|
+
out.push(manifest.global.config);
|
|
670
|
+
}
|
|
671
|
+
out.push(...manifest.projects);
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
//# sourceMappingURL=portable-bundle.js.map
|