@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.4
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/bin/doflow.js +2 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/registry/storage.json +87 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +60 -60
- package/src/adapters/claude/{index.ts → index.js} +48 -48
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +23 -23
- package/src/adapters/codex/{index.ts → index.js} +98 -98
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +70 -70
- package/src/adapters/gemini/{hooks.ts → hooks.js} +24 -24
- package/src/adapters/gemini/{index.ts → index.js} +41 -41
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +34 -34
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/{index.ts → index.js} +30 -30
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +24 -24
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +19 -19
- package/src/cli/{index.ts → index.js} +26 -26
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +48 -48
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/{index.ts → index.js} +140 -140
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +60 -60
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +30 -30
- package/src/runtime/{cli-result.ts → cli-result.js} +4 -4
- package/src/runtime/{cli.ts → cli.js} +37 -37
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/{context-pack.ts → context-pack.js} +20 -20
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +25 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +15 -15
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +35 -35
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +40 -40
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +19 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +61 -61
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +13 -13
- package/src/runtime/{task-state.ts → task-state.js} +15 -15
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
//
|
|
14
14
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
15
15
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
16
|
-
// caller's `require('../install/backup.
|
|
17
|
-
// `./paths` require already carries `.ts` (paths migrated first); `../helper/git.
|
|
16
|
+
// caller's `require('../install/backup.js')` keeps working while the tree is still `.js`. The
|
|
17
|
+
// `./paths` require already carries `.ts` (paths migrated first); `../helper/git.js` is `.ts` too.
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const crypto = require('node:crypto');
|
|
22
22
|
const { execFileSync } = require('node:child_process');
|
|
23
|
-
const { sourceCommit: gitSourceCommit } = require('../helper/git.
|
|
23
|
+
const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
24
24
|
const {
|
|
25
25
|
legacyBackupReadRoot, isLegacyBackupReadRoot, scopeRootFromCanonicalBackupRoot,
|
|
26
|
-
} = require('./paths.
|
|
27
|
-
const { validateLedger } = require('../state/index.
|
|
26
|
+
} = require('./paths.js');
|
|
27
|
+
const { validateLedger } = require('../state/index.js');
|
|
28
28
|
|
|
29
29
|
/** Where a listed/restorable backup was found. 'current' = the canonical `.doflow/backups` root the
|
|
30
30
|
* installer writes to; 'legacy' = the pre-7de6d5f `.claude/backups` root, readable only. Callers
|
|
@@ -34,7 +34,7 @@ const BACKUP_ORIGIN_LEGACY = 'legacy';
|
|
|
34
34
|
|
|
35
35
|
/** The roots a READ (list/restore) consults for a scope, canonical first, legacy second.
|
|
36
36
|
* Order is the precedence rule too: an id present in both resolves to the canonical copy. */
|
|
37
|
-
function backupReadRoots(backupRoot
|
|
37
|
+
function backupReadRoots(backupRoot ) {
|
|
38
38
|
const roots = [{ root: path.resolve(backupRoot), origin: BACKUP_ORIGIN_CURRENT }];
|
|
39
39
|
const scopeRoot = scopeRootFromCanonicalBackupRoot(backupRoot);
|
|
40
40
|
if (scopeRoot) {
|
|
@@ -47,16 +47,16 @@ function backupReadRoots(backupRoot: string): Array<{ root: string; origin: stri
|
|
|
47
47
|
/** Writes and deletes are canonical-only. The legacy root holds restore points DoFlow no longer
|
|
48
48
|
* manages and may be a user's sole recovery material, so a mutating call aimed at it is a bug to
|
|
49
49
|
* surface, not a path to follow. */
|
|
50
|
-
function assertMutableBackupRoot(backupRoot
|
|
50
|
+
function assertMutableBackupRoot(backupRoot , operation ) {
|
|
51
51
|
if (isLegacyBackupReadRoot(backupRoot)) {
|
|
52
52
|
throw new Error(`Refusing to ${operation} in the legacy backup root (read-only): ${backupRoot}`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function pad2(n
|
|
56
|
+
function pad2(n ) { return String(n).padStart(2, '0'); }
|
|
57
57
|
|
|
58
58
|
/** `<op>_YYYY-MM-DD_HH-MM-SS`, using a caller-supplied Date (never `new Date()` internally — keeps this testable/deterministic). */
|
|
59
|
-
function backupId(op
|
|
59
|
+
function backupId(op , date ) {
|
|
60
60
|
const y = date.getFullYear();
|
|
61
61
|
const mo = pad2(date.getMonth() + 1);
|
|
62
62
|
const d = pad2(date.getDate());
|
|
@@ -78,8 +78,8 @@ const DEFAULT_BACKUP_RETENTION = 3;
|
|
|
78
78
|
|
|
79
79
|
/** A backup could not be written completely. The caller stops before changing anything. */
|
|
80
80
|
class BackupError extends Error {
|
|
81
|
-
|
|
82
|
-
constructor(message
|
|
81
|
+
|
|
82
|
+
constructor(message ) {
|
|
83
83
|
super(message);
|
|
84
84
|
this.name = 'BackupError';
|
|
85
85
|
this.code = 'backup-failed';
|
|
@@ -87,14 +87,14 @@ class BackupError extends Error {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** POSIX path of `p` relative to `scopeRoot`, or null when `p` is not strictly inside it. */
|
|
90
|
-
function scopeRelative(scopeRoot
|
|
90
|
+
function scopeRelative(scopeRoot , p ) {
|
|
91
91
|
const rel = path.relative(scopeRoot, p);
|
|
92
92
|
if (!rel || path.isAbsolute(rel) || rel.split(path.sep)[0] === '..') return null;
|
|
93
93
|
return rel.split(path.sep).join('/');
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Manifest JSON: two-space indentation, except one compact line per `files` and `excluded` element. */
|
|
97
|
-
function serializeManifest(manifest
|
|
97
|
+
function serializeManifest(manifest ) {
|
|
98
98
|
const body = Object.entries(manifest).map(([key, value]) => {
|
|
99
99
|
if ((key === 'files' || key === 'excluded') && Array.isArray(value) && value.length > 0) {
|
|
100
100
|
return ` ${JSON.stringify(key)}: [\n${value.map((v) => ` ${JSON.stringify(v)}`).join(',\n')}\n ]`;
|
|
@@ -104,7 +104,7 @@ function serializeManifest(manifest: Record<string, any>): string {
|
|
|
104
104
|
return `{\n${body.join(',\n')}\n}\n`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function errorCode(err
|
|
107
|
+
function errorCode(err ) { return err.code || err.message; }
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Copy the files of a backup set into `<backupRoot>/<id>/` (format 2). Returns null for an empty
|
|
@@ -114,14 +114,14 @@ function errorCode(err: any): string { return err.code || err.message; }
|
|
|
114
114
|
function createFileBackup({
|
|
115
115
|
operation, set, backupRoot, repoRoot, sourceCommit, version, date, restores = null,
|
|
116
116
|
fsImpl = fs, randomHex = () => crypto.randomBytes(4).toString('hex'),
|
|
117
|
-
}
|
|
117
|
+
} ) {
|
|
118
118
|
assertMutableBackupRoot(backupRoot, 'create a backup');
|
|
119
119
|
if (set.entries.length === 0) return null;
|
|
120
120
|
|
|
121
121
|
const base = backupId(operation, date);
|
|
122
122
|
const tmpDir = path.join(backupRoot, `${TEMP_PREFIX}${base}-${process.pid}-${randomHex()}`);
|
|
123
123
|
let tmpCreated = false;
|
|
124
|
-
const writeFailure = (err
|
|
124
|
+
const writeFailure = (err ) => new BackupError(`could not write ${backupRoot}: ${errorCode(err)}`);
|
|
125
125
|
|
|
126
126
|
try {
|
|
127
127
|
try {
|
|
@@ -137,11 +137,11 @@ function createFileBackup({
|
|
|
137
137
|
const summary = { files: 0, existed: 0, absent: 0, bytes: 0 };
|
|
138
138
|
let external = 0;
|
|
139
139
|
for (const entry of set.entries) {
|
|
140
|
-
const record
|
|
140
|
+
const record = entry.rel !== null ? { path: entry.rel } : { path: entry.path, outside: true };
|
|
141
141
|
let stat = null;
|
|
142
142
|
try {
|
|
143
143
|
stat = fsImpl.statSync(entry.path);
|
|
144
|
-
} catch (err
|
|
144
|
+
} catch (err ) {
|
|
145
145
|
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
|
|
146
146
|
throw new BackupError(`could not read ${entry.path}: ${errorCode(err)}`);
|
|
147
147
|
}
|
|
@@ -172,21 +172,21 @@ function createFileBackup({
|
|
|
172
172
|
files.push(record);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
const manifest
|
|
175
|
+
const manifest = {
|
|
176
176
|
id: base,
|
|
177
177
|
operation,
|
|
178
178
|
timestamp: date.toISOString(),
|
|
179
179
|
source_path: repoRoot,
|
|
180
180
|
source_commit: sourceCommit ?? gitSourceCommit(repoRoot),
|
|
181
181
|
type: 'files',
|
|
182
|
-
tools_affected: [...new Set(set.entries.flatMap((e
|
|
182
|
+
tools_affected: [...new Set(set.entries.flatMap((e ) => e.harnesses))].sort(),
|
|
183
183
|
format: 2,
|
|
184
184
|
doflow_version: version,
|
|
185
185
|
scope: set.scope,
|
|
186
186
|
scope_root: set.scopeRoot,
|
|
187
187
|
restores,
|
|
188
188
|
summary,
|
|
189
|
-
excluded: set.excluded.map((x
|
|
189
|
+
excluded: set.excluded.map((x ) => {
|
|
190
190
|
const rel = scopeRelative(set.scopeRoot, x.path);
|
|
191
191
|
return rel !== null ? { path: rel, reason: x.reason } : { path: x.path, outside: true, reason: x.reason };
|
|
192
192
|
}),
|
|
@@ -209,7 +209,7 @@ function createFileBackup({
|
|
|
209
209
|
fsImpl.writeFileSync(manifestPath, serializeManifest(manifest));
|
|
210
210
|
}
|
|
211
211
|
fsImpl.renameSync(tmpDir, dir);
|
|
212
|
-
} catch (err
|
|
212
|
+
} catch (err ) {
|
|
213
213
|
if (RENAME_COLLISION_CODES.has(err.code) && fsImpl.existsSync(dir)) continue;
|
|
214
214
|
throw writeFailure(err);
|
|
215
215
|
}
|
|
@@ -224,17 +224,17 @@ function createFileBackup({
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
function defaultIsAlive(pid
|
|
227
|
+
function defaultIsAlive(pid ) {
|
|
228
228
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
229
229
|
try {
|
|
230
230
|
process.kill(pid, 0);
|
|
231
231
|
return true;
|
|
232
|
-
} catch (err
|
|
232
|
+
} catch (err ) {
|
|
233
233
|
return err.code === 'EPERM';
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
function isStaleTemp(dir
|
|
237
|
+
function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } ) {
|
|
238
238
|
let owner = null;
|
|
239
239
|
try { owner = JSON.parse(fsImpl.readFileSync(path.join(dir, OWNER_FILE), 'utf8')); } catch { /* no owner */ }
|
|
240
240
|
if (!owner || typeof owner !== 'object') {
|
|
@@ -248,7 +248,7 @@ function isStaleTemp(dir: string, { now, fsImpl, isAlive, hostname }: any): bool
|
|
|
248
248
|
* Remove `.tmp-*` directories left in `backupRoot` by runs that died: the owner process is gone on
|
|
249
249
|
* this host, or the temp is older than 24 hours. Returns how many were removed.
|
|
250
250
|
*/
|
|
251
|
-
function sweepStaleTemps(backupRoot
|
|
251
|
+
function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname() } = {}) {
|
|
252
252
|
assertMutableBackupRoot(backupRoot, 'clean up backups');
|
|
253
253
|
if (!fsImpl.existsSync(backupRoot)) return 0;
|
|
254
254
|
let removed = 0;
|
|
@@ -269,8 +269,8 @@ function sweepStaleTemps(backupRoot: string, { now = Date.now(), fsImpl = fs, is
|
|
|
269
269
|
* not know), 'full' or 'partial' (1.19, typed the way restoreBackup reads them), or 'incomplete'
|
|
270
270
|
* (no manifest, or one that does not parse). `manifest` is the parsed object, or null.
|
|
271
271
|
*/
|
|
272
|
-
function classifyBackupDir(dir
|
|
273
|
-
let manifest
|
|
272
|
+
function classifyBackupDir(dir , { fsImpl = fs } = {}) {
|
|
273
|
+
let manifest = null;
|
|
274
274
|
try { manifest = JSON.parse(fsImpl.readFileSync(path.join(dir, MANIFEST_FILE), 'utf8')); } catch { /* incomplete */ }
|
|
275
275
|
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return { kind: 'incomplete', manifest: null };
|
|
276
276
|
const { format } = manifest;
|
|
@@ -286,7 +286,7 @@ const ID_TIME = /^.+?_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})(?:-(\d+))?
|
|
|
286
286
|
* Ordering key of a backup directory: the manifest timestamp, else the time in its id (local
|
|
287
287
|
* time, as backupId writes it), else its mtime. Retention and listing share it.
|
|
288
288
|
*/
|
|
289
|
-
function sortKey(dir
|
|
289
|
+
function sortKey(dir , { fsImpl = fs, manifest } = {}) {
|
|
290
290
|
const id = path.basename(dir);
|
|
291
291
|
const m = manifest === undefined ? classifyBackupDir(dir, { fsImpl }).manifest : manifest;
|
|
292
292
|
const match = ID_TIME.exec(id);
|
|
@@ -303,7 +303,7 @@ function sortKey(dir: string, { fsImpl = fs, manifest }: any = {}): { id: string
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/** Newest first; ties go to the larger `-n` suffix, then the id in descending order. */
|
|
306
|
-
function compareSortKeys(a
|
|
306
|
+
function compareSortKeys(a , b ) {
|
|
307
307
|
if (a.time !== b.time) return b.time - a.time;
|
|
308
308
|
if (a.suffix !== b.suffix) return b.suffix - a.suffix;
|
|
309
309
|
if (a.id === b.id) return 0;
|
|
@@ -311,7 +311,7 @@ function compareSortKeys(a: { id: string; time: number; suffix: number }, b: { i
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/** Bytes of the regular files under `dir`, never following a link; null when any read fails. */
|
|
314
|
-
function backupSize(dir
|
|
314
|
+
function backupSize(dir , { fsImpl = fs } = {}) {
|
|
315
315
|
try {
|
|
316
316
|
let total = 0;
|
|
317
317
|
const pending = [dir];
|
|
@@ -333,7 +333,7 @@ function backupSize(dir: string, { fsImpl = fs }: any = {}): number | null {
|
|
|
333
333
|
const BYTE_UNITS = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
334
334
|
|
|
335
335
|
/** `?` for an unknown size, `<n> B` below 1 KiB, otherwise one decimal in 1024-based units. */
|
|
336
|
-
function formatBytes(n
|
|
336
|
+
function formatBytes(n ) {
|
|
337
337
|
if (n === null || n === undefined) return '?';
|
|
338
338
|
if (n < 1024) return `${n} B`;
|
|
339
339
|
let value = n / 1024;
|
|
@@ -346,14 +346,14 @@ function formatBytes(n: number | null | undefined): string {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
/** Security guard: a backup id is a directory *name*, never a path — reject traversal/absolute ids. */
|
|
349
|
-
function assertSafeBackupId(bid
|
|
349
|
+
function assertSafeBackupId(bid ) {
|
|
350
350
|
if (!bid || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
|
|
351
351
|
throw new Error(`Invalid backup id: '${bid}'`);
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
/** Restore a 1.19 backup id into the given tool dirs. dirs = {tool: absDstDir}. */
|
|
356
|
-
function restoreBackup({ bid, backupRoot, dirs, dryRun = false }
|
|
356
|
+
function restoreBackup({ bid, backupRoot, dirs, dryRun = false } ) {
|
|
357
357
|
assertSafeBackupId(bid);
|
|
358
358
|
// An id the user picked from `list-backups` may live in either root, so resolve it the same way
|
|
359
359
|
// the listing found it. Restoring reads the backup and writes only into the tool dirs, so a
|
|
@@ -397,24 +397,24 @@ function restoreBackup({ bid, backupRoot, dirs, dryRun = false }: any): void {
|
|
|
397
397
|
|
|
398
398
|
/** Raw member lines of a tar.gz archive. A whole-home archive can list far more than the default
|
|
399
399
|
* 1 MiB of output, so the buffer is unbounded. */
|
|
400
|
-
function defaultListTar(archive
|
|
400
|
+
function defaultListTar(archive ) {
|
|
401
401
|
return execFileSync('tar', ['-tzf', archive], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: Infinity })
|
|
402
402
|
.split(/\r?\n/);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
/** File members of an archive listing, relative to the archive root, without any `..` member. */
|
|
406
|
-
function archiveMembers(lines
|
|
406
|
+
function archiveMembers(lines ) {
|
|
407
407
|
return lines
|
|
408
408
|
.map((line) => line.replace(/^\.\//, ''))
|
|
409
409
|
.filter((m) => m && m !== '.' && !m.endsWith('/') && !m.split('/').includes('..'));
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
/** Paths, relative to `root`, of the regular files under it; links are not followed. */
|
|
413
|
-
function regularFilesUnder(root
|
|
414
|
-
const files
|
|
413
|
+
function regularFilesUnder(root , fsImpl ) {
|
|
414
|
+
const files = [];
|
|
415
415
|
const pending = [''];
|
|
416
416
|
while (pending.length > 0) {
|
|
417
|
-
const rel = pending.pop()
|
|
417
|
+
const rel = pending.pop() ;
|
|
418
418
|
for (const e of fsImpl.readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
419
419
|
const child = path.join(rel, e.name);
|
|
420
420
|
if (e.isDirectory()) pending.push(child);
|
|
@@ -425,7 +425,7 @@ function regularFilesUnder(root: string, fsImpl: any): string[] {
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
/** Why a format-2 entry cannot be restored, or null when it can. */
|
|
428
|
-
function refusalOf(entry
|
|
428
|
+
function refusalOf(entry , target , { scope, scopeRoot, bkDir, fsImpl } ) {
|
|
429
429
|
const raw = entry.path;
|
|
430
430
|
if (entry.outside === true) {
|
|
431
431
|
if (!path.isAbsolute(raw)) return 'unsafe path';
|
|
@@ -451,7 +451,7 @@ function refusalOf(entry: any, target: string, { scope, scopeRoot, bkDir, fsImpl
|
|
|
451
451
|
* tool through restoreBackup; its snapshot lists the files that restore will overwrite.
|
|
452
452
|
* Reads only.
|
|
453
453
|
*/
|
|
454
|
-
function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTargets, dirs, ledgerFile = null, fsImpl = fs, listTar = defaultListTar }
|
|
454
|
+
function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTargets, dirs, ledgerFile = null, fsImpl = fs, listTar = defaultListTar } ) {
|
|
455
455
|
assertSafeBackupId(bid);
|
|
456
456
|
const found = backupReadRoots(backupRoot).find((r) => fsImpl.existsSync(path.join(r.root, bid)));
|
|
457
457
|
if (!found) throw new Error(`Backup not found: ${bid}`);
|
|
@@ -461,7 +461,7 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
461
461
|
throw new Error(`Backup ${bid} was made by a newer DoFlow (format ${manifest.format}); this version cannot restore it`);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
const plan
|
|
464
|
+
const plan = {
|
|
465
465
|
bid,
|
|
466
466
|
origin: found.origin,
|
|
467
467
|
bkDir,
|
|
@@ -484,7 +484,7 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
484
484
|
// writes, so --target narrows it with the files and never skips it.
|
|
485
485
|
const isLedger = ledger !== null && entry.outside !== true && typeof entry.path === 'string' && entry.path !== ''
|
|
486
486
|
&& path.resolve(scopeRoot, entry.path) === ledger;
|
|
487
|
-
if (!isLedger && explicitTargets && !harnesses.some((h
|
|
487
|
+
if (!isLedger && explicitTargets && !harnesses.some((h ) => wanted.has(h))) {
|
|
488
488
|
plan.untargeted += 1;
|
|
489
489
|
continue;
|
|
490
490
|
}
|
|
@@ -505,12 +505,12 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
505
505
|
}
|
|
506
506
|
plan.restore.push({ path: target, stored: path.resolve(bkDir, entry.stored), mode: entry.mode, size: entry.size, harnesses, ...(isLedger ? { ledger: true } : {}) });
|
|
507
507
|
}
|
|
508
|
-
plan.snapshot = plan.restore.map((item
|
|
508
|
+
plan.snapshot = plan.restore.map((item ) => ({ path: item.path, harnesses: item.harnesses }));
|
|
509
509
|
return plan;
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
const tools = [];
|
|
513
|
-
for (const tool of wanted
|
|
513
|
+
for (const tool of wanted ) {
|
|
514
514
|
const dstDir = dirs[tool];
|
|
515
515
|
if (!dstDir) continue;
|
|
516
516
|
if (plan.type === 'full') {
|
|
@@ -536,21 +536,21 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
536
536
|
* up) stay as they are; restoring the whole old ledger would make DoFlow forget files and entries it
|
|
537
537
|
* still has on disk. With no current ledger the backup's is taken whole.
|
|
538
538
|
*/
|
|
539
|
-
function mergeRestoredLedger(backup
|
|
539
|
+
function mergeRestoredLedger(backup , current , restored ) {
|
|
540
540
|
if (!current) return backup;
|
|
541
|
-
const at = (target
|
|
541
|
+
const at = (target ) => typeof target === 'string' && restored.has(path.resolve(target));
|
|
542
542
|
return {
|
|
543
543
|
...current,
|
|
544
|
-
resources: [...current.resources.filter((r
|
|
544
|
+
resources: [...current.resources.filter((r ) => !at(r.target)), ...backup.resources.filter((r ) => at(r.target))],
|
|
545
545
|
tombstones: [
|
|
546
|
-
...(current.tombstones ?? []).filter((t
|
|
547
|
-
...(backup.tombstones ?? []).filter((t
|
|
546
|
+
...(current.tombstones ?? []).filter((t ) => !at(t.fromTarget)),
|
|
547
|
+
...(backup.tombstones ?? []).filter((t ) => at(t.fromTarget)),
|
|
548
548
|
],
|
|
549
549
|
};
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
/** The bytes a ledger restore item writes: the backup's ledger merged onto the current one. */
|
|
553
|
-
function restoredLedgerBytes(item
|
|
553
|
+
function restoredLedgerBytes(item , restored , fsImpl ) {
|
|
554
554
|
const backup = validateLedger(JSON.parse(fsImpl.readFileSync(item.stored, 'utf8')));
|
|
555
555
|
const current = fsImpl.existsSync(item.path) ? validateLedger(JSON.parse(fsImpl.readFileSync(item.path, 'utf8'))) : null;
|
|
556
556
|
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored)), null, 2)}\n`);
|
|
@@ -563,17 +563,17 @@ function restoredLedgerBytes(item: any, restored: Set<string>, fsImpl: any): Buf
|
|
|
563
563
|
* files written back before it (mergeRestoredLedger). It never deletes a file and never writes
|
|
564
564
|
* outside `plan.restore`. A 1.19 plan goes through restoreBackup unchanged.
|
|
565
565
|
*/
|
|
566
|
-
function executeRestore(plan
|
|
566
|
+
function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs } ) {
|
|
567
567
|
if (plan.format !== 2) {
|
|
568
|
-
const toolDirs = Object.fromEntries(plan.v1.tools.map((t
|
|
568
|
+
const toolDirs = Object.fromEntries(plan.v1.tools.map((t ) => [t.tool, t.dstDir]));
|
|
569
569
|
restoreBackup({ bid: plan.bid, backupRoot, dirs: toolDirs, dryRun });
|
|
570
|
-
return { restored: plan.v1.tools.map((t
|
|
570
|
+
return { restored: plan.v1.tools.map((t ) => t.tool), failed: [], absent: [], refused: [], untargeted: 0, legacy: true };
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
const restored = [];
|
|
574
574
|
const failed = [];
|
|
575
|
-
const files = new Set
|
|
576
|
-
const items = [...plan.restore.filter((item
|
|
575
|
+
const files = new Set ();
|
|
576
|
+
const items = [...plan.restore.filter((item ) => !item.ledger), ...plan.restore.filter((item ) => item.ledger)];
|
|
577
577
|
for (const item of items) {
|
|
578
578
|
if (dryRun) {
|
|
579
579
|
restored.push(item.path);
|
|
@@ -587,7 +587,7 @@ function executeRestore(plan: any, { backupRoot, dryRun = false, fsImpl = fs }:
|
|
|
587
587
|
let isLink = false;
|
|
588
588
|
try {
|
|
589
589
|
isLink = fsImpl.lstatSync(item.path).isSymbolicLink();
|
|
590
|
-
} catch (err
|
|
590
|
+
} catch (err ) {
|
|
591
591
|
if (err.code !== 'ENOENT') throw err;
|
|
592
592
|
}
|
|
593
593
|
if (isLink) {
|
|
@@ -613,11 +613,11 @@ function executeRestore(plan: any, { backupRoot, dryRun = false, fsImpl = fs }:
|
|
|
613
613
|
|
|
614
614
|
/** One `{ row, key }` per backup dir under `root`, tagged with where it came from. Dot-named
|
|
615
615
|
* directories (temps of a run in progress) and symlinks are not backups. */
|
|
616
|
-
function readBackupRows(root
|
|
616
|
+
function readBackupRows(root , origin ) {
|
|
617
617
|
if (!fs.existsSync(root)) return [];
|
|
618
618
|
return fs.readdirSync(root, { withFileTypes: true })
|
|
619
|
-
.filter((e
|
|
620
|
-
.map((e
|
|
619
|
+
.filter((e ) => e.isDirectory() && !e.name.startsWith('.'))
|
|
620
|
+
.map((e ) => {
|
|
621
621
|
const bkDir = path.join(root, e.name);
|
|
622
622
|
const { kind, manifest: m } = classifyBackupDir(bkDir);
|
|
623
623
|
const key = sortKey(bkDir, { manifest: m });
|
|
@@ -641,7 +641,7 @@ function readBackupRows(root: string, origin: string): Array<{ key: any; row: an
|
|
|
641
641
|
* manifest is missing), `complete` and `bytes` (null when unreadable), and are ordered by the
|
|
642
642
|
* same key retention uses.
|
|
643
643
|
*/
|
|
644
|
-
function listBackups(backupRoot
|
|
644
|
+
function listBackups(backupRoot ) {
|
|
645
645
|
const found = [];
|
|
646
646
|
const seen = new Set();
|
|
647
647
|
for (const { root, origin } of backupReadRoots(backupRoot)) {
|
|
@@ -663,10 +663,10 @@ function listBackups(backupRoot: string): any[] {
|
|
|
663
663
|
* nothing; `reserve` makes room for a backup the real run would add, and `kept` then counts it.
|
|
664
664
|
* Never reaches the legacy `.claude/backups` root: those may be a user's only restore points.
|
|
665
665
|
*/
|
|
666
|
-
function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun = false, now = Date.now(), fsImpl = fs }
|
|
666
|
+
function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun = false, now = Date.now(), fsImpl = fs } ) {
|
|
667
667
|
assertMutableBackupRoot(backupRoot, 'prune backups');
|
|
668
668
|
if (!Number.isInteger(keep) || keep < 0) throw new Error(`Backup retention must be a non-negative whole number, got ${keep}`);
|
|
669
|
-
const result
|
|
669
|
+
const result = { keep, kept: 0, removed: [], wouldRemove: [], failed: [], sweptTemps: 0 };
|
|
670
670
|
|
|
671
671
|
// Only DoFlow backups count: a directory with a readable manifest, or one named like a backup id
|
|
672
672
|
// (a 1.19 or interrupted backup can lack a manifest). Anything else in the root is not ours to
|
|
@@ -701,7 +701,7 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
|
|
|
701
701
|
try {
|
|
702
702
|
fsImpl.rmSync(dir, { recursive: true, force: true });
|
|
703
703
|
result.removed.push(id);
|
|
704
|
-
} catch (err
|
|
704
|
+
} catch (err ) {
|
|
705
705
|
result.failed.push({ id, error: err.message });
|
|
706
706
|
}
|
|
707
707
|
}
|
|
@@ -710,7 +710,7 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
/** The parenthesised end of a retention line: the `--prune` value given, or the default. */
|
|
713
|
-
function retentionNote(prune
|
|
713
|
+
function retentionNote(prune , keep ) {
|
|
714
714
|
if (prune === null || prune === undefined) return `keeping the newest ${keep}; --prune N changes this`;
|
|
715
715
|
if (prune === 0) return '--prune 0 keeps every backup';
|
|
716
716
|
return `--prune ${keep}`;
|
|
@@ -5,27 +5,27 @@
|
|
|
5
5
|
//
|
|
6
6
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
7
7
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
8
|
-
// caller's `require('../install/context.
|
|
8
|
+
// caller's `require('../install/context.js')` keeps working while the tree is still `.js`.
|
|
9
9
|
const path = require('node:path');
|
|
10
|
-
const { sourceCommit: gitSourceCommit } = require('../helper/git.
|
|
10
|
+
const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* `sourceCommit` lets a caller (bin/doflow.js) pass an already-resolved commit instead of this
|
|
24
24
|
* module spawning its own `git rev-parse`; omit it to resolve here (e.g. tests calling this module
|
|
25
25
|
* directly). Returns a plain properties object (used by both the text and --json renderers).
|
|
26
26
|
*/
|
|
27
|
-
function resolveContext({ repoRoot, global, projectRoot, targets, dirs, sourceCommit, managedResources }
|
|
28
|
-
const context
|
|
27
|
+
function resolveContext({ repoRoot, global, projectRoot, targets, dirs, sourceCommit, managedResources } ) {
|
|
28
|
+
const context = {
|
|
29
29
|
scope: global ? 'global' : 'project',
|
|
30
30
|
scopeRoot: global ? path.dirname(dirs.claude) : path.resolve(projectRoot),
|
|
31
31
|
targets,
|
|
@@ -41,12 +41,12 @@ function resolveContext({ repoRoot, global, projectRoot, targets, dirs, sourceCo
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Human-readable "Resolved context" block, printed to stderr (keeps stdout clean like sync.sh's log()). */
|
|
44
|
-
function printContext(ctx
|
|
44
|
+
function printContext(ctx ) {
|
|
45
45
|
const lines = [
|
|
46
46
|
'[CONTEXT] Resolved install context',
|
|
47
47
|
` scope : ${ctx.scope} (root: ${ctx.scopeRoot})`,
|
|
48
48
|
` targets : ${ctx.targets.join(', ')}`,
|
|
49
|
-
...ctx.targets.map((t
|
|
49
|
+
...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t]}`),
|
|
50
50
|
` repo root : ${ctx.repoRoot}`,
|
|
51
51
|
` source commit : ${ctx.sourceCommit}`,
|
|
52
52
|
` node : ${ctx.nodeVersion}`,
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
//
|
|
7
7
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
8
8
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
9
|
-
// caller's `require('../install/manifest.
|
|
9
|
+
// caller's `require('../install/manifest.js')` keeps working while the tree is still `.js`.
|
|
10
10
|
const fs = require('node:fs');
|
|
11
11
|
const path = require('node:path');
|
|
12
|
-
const { sourceCommit: gitSourceCommit } = require('../helper/git.
|
|
12
|
+
const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
13
13
|
|
|
14
14
|
// Explicit migration bridge: pre-registry installations stored this record beneath .claude.
|
|
15
15
|
// Lifecycle commands use the scope-neutral .doflow location; only the neutral-state importer
|
|
@@ -17,27 +17,27 @@ const { sourceCommit: gitSourceCommit } = require('../helper/git.ts');
|
|
|
17
17
|
const LEGACY_MANIFEST_FILE_NAME = '.install-manifest.json';
|
|
18
18
|
const MANIFEST_FILE_NAME = LEGACY_MANIFEST_FILE_NAME;
|
|
19
19
|
|
|
20
|
-
function manifestPath(claudeDir
|
|
20
|
+
function manifestPath(claudeDir ) {
|
|
21
21
|
return path.join(claudeDir, MANIFEST_FILE_NAME);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function canonicalManifestPath(scopeRoot
|
|
24
|
+
function canonicalManifestPath(scopeRoot ) {
|
|
25
25
|
return path.join(path.resolve(scopeRoot), '.doflow', MANIFEST_FILE_NAME);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* `sourceCommit` lets a caller (bin/doflow.js) pass an already-resolved commit instead of this
|
|
@@ -48,16 +48,16 @@ interface WriteManifestInput {
|
|
|
48
48
|
* for fine-grained Codex resources. Each record uses {target,scope,kind,identity,sourceVersion,
|
|
49
49
|
* fingerprint,selection,recoveryPoint}. Omit it to preserve a ledger written by a newer command.
|
|
50
50
|
*/
|
|
51
|
-
function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoot, backupId = '', tools, date, dryRun = false, sourceCommit, managedResources }
|
|
51
|
+
function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoot, backupId = '', tools, date, dryRun = false, sourceCommit, managedResources } ) {
|
|
52
52
|
if (!scopeRoot && !claudeDir) throw new Error('scopeRoot is required for lifecycle metadata');
|
|
53
|
-
const file = scopeRoot ? canonicalManifestPath(scopeRoot) : manifestPath(claudeDir
|
|
53
|
+
const file = scopeRoot ? canonicalManifestPath(scopeRoot) : manifestPath(claudeDir );
|
|
54
54
|
if (dryRun) return file;
|
|
55
55
|
|
|
56
56
|
// Preserve per-tool last_updated for tools NOT part of this operation (matches sync.sh's
|
|
57
57
|
// incremental jq merge, which only touches the tools passed to write_manifest).
|
|
58
|
-
let existingTools
|
|
59
|
-
let existingMcpServers
|
|
60
|
-
let existingManagedResources
|
|
58
|
+
let existingTools = {};
|
|
59
|
+
let existingMcpServers ;
|
|
60
|
+
let existingManagedResources ;
|
|
61
61
|
const existingFile = file;
|
|
62
62
|
if (existingFile && fs.existsSync(existingFile)) {
|
|
63
63
|
try {
|
|
@@ -72,10 +72,10 @@ function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoo
|
|
|
72
72
|
} catch { /* start fresh */ }
|
|
73
73
|
}
|
|
74
74
|
const ts = date.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
75
|
-
const toolsOut
|
|
75
|
+
const toolsOut = { ...existingTools };
|
|
76
76
|
for (const t of tools) toolsOut[t] = { installed: true, last_updated: ts };
|
|
77
77
|
|
|
78
|
-
const manifest
|
|
78
|
+
const manifest = {
|
|
79
79
|
script_version: scriptVersion,
|
|
80
80
|
last_operation: operation,
|
|
81
81
|
last_run: ts,
|
|
@@ -102,7 +102,7 @@ function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoo
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/** null if no manifest yet. */
|
|
105
|
-
function readManifestFile(file
|
|
105
|
+
function readManifestFile(file ) {
|
|
106
106
|
if (!fs.existsSync(file)) return null;
|
|
107
107
|
try {
|
|
108
108
|
const m = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -129,13 +129,13 @@ function readManifestFile(file: string): any {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/** Read only the canonical scope-neutral manifest used by lifecycle commands. */
|
|
132
|
-
function readInstallManifest({ scopeRoot }
|
|
132
|
+
function readInstallManifest({ scopeRoot } ) {
|
|
133
133
|
if (!scopeRoot) throw new Error('scopeRoot is required');
|
|
134
134
|
return readManifestFile(canonicalManifestPath(scopeRoot));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** Read only the legacy path; neutral-state migration uses this deliberately. */
|
|
138
|
-
function readManifest(claudeDir
|
|
138
|
+
function readManifest(claudeDir ) {
|
|
139
139
|
return readManifestFile(manifestPath(claudeDir));
|
|
140
140
|
}
|
|
141
141
|
|