@openprd/cli 0.1.19 → 0.1.21
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/.openprd/changes/openprd-control-plane-v020/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-control-plane-v020/design.md +78 -0
- package/.openprd/changes/openprd-control-plane-v020/proposal.md +54 -0
- package/.openprd/changes/openprd-control-plane-v020/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-control-plane-v020/task-events.jsonl +27 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks-002.md +35 -0
- package/.openprd/changes/openprd-control-plane-v020/tasks.md +427 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/.openprd.yaml +2 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/design.md +52 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/proposal.md +35 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/task-events.jsonl +1 -0
- package/.openprd/changes/remove-session-tracking-humane-approval/tasks.md +170 -0
- package/.openprd/design/active/asset-spec.md +19 -14
- package/.openprd/design/active/direction-plan.md +19 -3
- package/.openprd/design/active/facts-sheet.md +16 -7
- package/.openprd/design/active/image-preflight.md +6 -5
- package/.openprd/design/active/review-studio-v020-directions/compare-plan.json +34 -0
- package/.openprd/design/active/review-studio-v020-directions/contact-sheet.jpg +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/01.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/02.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/crops/03.png +0 -0
- package/.openprd/design/active/review-studio-v020-directions/focus-board.template.json +69 -0
- package/.openprd/design/active/review-studio-v020-directions/parallel-board.template.json +45 -0
- package/.openprd/design/active/review-studio-v020-directions/reference-set.json +143 -0
- package/.openprd/design/active/review-studio-v020-directions/source.png +0 -0
- package/.openprd/design/active/selected-direction.md +23 -9
- package/.openprd/engagements/active/control-plane-architecture.json +203 -0
- package/.openprd/engagements/active/control-plane-intake.json +418 -0
- package/.openprd/engagements/active/prd.md +183 -119
- package/.openprd/engagements/active/review-presentation-v0018.json +176 -0
- package/.openprd/i18n-config.json +12 -0
- package/.openprd/ledger/events.jsonl +6 -0
- package/.openprd/ledger/heads/ebea1a71a9daa566f1c91b53.json +10 -0
- package/.openprd/manifest.json +21 -0
- package/AGENTS.md +1 -0
- package/README.md +2 -0
- package/README_EN.md +2 -0
- package/package.json +1 -1
- package/skills/openprd-frontend-design/SKILL.md +16 -0
- package/skills/openprd-harness/SKILL.md +1 -1
- package/skills/openprd-quality/SKILL.md +1 -1
- package/skills/openprd-requirement-intake/SKILL.md +1 -1
- package/skills/openprd-shared/SKILL.md +1 -1
- package/src/adapters/adapter-spi.js +193 -0
- package/src/adapters/capability-envelope.js +98 -0
- package/src/adapters/event-normalizer.js +55 -0
- package/src/adapters/index.js +4 -0
- package/src/adapters/install-safety.js +249 -0
- package/src/agent-canonical-content.js +4 -2
- package/src/agent-integration.js +147 -42
- package/src/cli/args.js +63 -4
- package/src/cli/gate-print.js +17 -0
- package/src/cli/quality-commands.js +18 -0
- package/src/cli/quality-print.js +10 -0
- package/src/cli/runtime-print.js +24 -0
- package/src/codex-hook-runner-template.mjs +65 -77
- package/src/codex-runtime.js +48 -5
- package/src/context/cache.js +245 -0
- package/src/context/compiler.js +438 -0
- package/src/context/constants.js +30 -0
- package/src/context/index.js +39 -0
- package/src/context/redaction.js +84 -0
- package/src/context/stable.js +69 -0
- package/src/context/telemetry.js +42 -0
- package/src/dev-standards.js +57 -0
- package/src/fleet.js +112 -95
- package/src/gates/index.js +2 -0
- package/src/gates/scoped-gates.js +256 -0
- package/src/gates/store.js +126 -0
- package/src/gates/workspace.js +41 -0
- package/src/html-artifacts.js +725 -28
- package/src/kernel/atomic-store.js +299 -0
- package/src/kernel/event-envelope.js +166 -0
- package/src/kernel/index.js +4 -0
- package/src/kernel/project-ledger.js +467 -0
- package/src/kernel/project-manifest.js +205 -0
- package/src/knowledge-v3/index.js +1 -0
- package/src/knowledge-v3/lifecycle.js +290 -0
- package/src/knowledge.js +14 -7
- package/src/openprd.js +71 -2
- package/src/review-model.js +413 -0
- package/src/review-presentation.js +1 -1
- package/src/run-harness.js +431 -37
- package/src/runtime/cli_runtime_README.md +28 -0
- package/src/runtime/errors.js +66 -0
- package/src/runtime/index.js +44 -0
- package/src/runtime/lane-schema.js +141 -0
- package/src/runtime/lane-store.js +279 -0
- package/src/runtime/task-runtime.js +449 -0
- package/src/runtime/workspace.js +179 -0
- package/src/runtime/write-set.js +206 -0
- package/src/session-binding.js +16 -3
- package/src/session-registry.js +59 -1
- package/src/upgrade/fleet-mutation.js +166 -0
- package/src/upgrade/fleet-transaction.js +398 -0
- package/src/upgrade/transaction-store.js +416 -0
- package/src/visual-compare-core.js +66 -27
- package/src/visual-compare.js +18 -12
- package/src/workspace-core.js +109 -7
- package/src/workspace-registry.js +39 -1
- package/src/workspace-workflow.js +18 -15
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 为 OpenPrd fleet 项目刷新提供预检、preimage 备份、隔离 staging、校验和 CAS、原子落盘与自动恢复。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收单个项目路径、fleet 分类与动作、目标版本、备份根目录,以及在 staging 项目上执行的更新回调。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 返回稳定的事务计划和执行报告,包含版本、校验和、备份、迁移、拒绝原因与 rollback 结果。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 fleet 编排和具体 workspace 更新器之间,隔离批量更新风险,不直接实现 Agent/PRD 迁移逻辑。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 使用 kernel atomic-store 的原子 JSON 写与文件锁、project-manifest 兼容性断言和 transaction-store。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 新增 fleet 写入类型时必须补分类门禁、失败恢复与拒绝原因测试。
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
21
|
+
import fs from 'node:fs/promises';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
assertProjectManifestCompatible,
|
|
27
|
+
atomicWriteJson,
|
|
28
|
+
withFileLock,
|
|
29
|
+
} from '../kernel/index.js';
|
|
30
|
+
import {
|
|
31
|
+
CHECKSUM_ALGORITHM,
|
|
32
|
+
FLEET_MANAGED_SCOPES,
|
|
33
|
+
FLEET_TRANSACTION_SCHEMA,
|
|
34
|
+
FLEET_TRANSACTION_VERSION,
|
|
35
|
+
applyStagedState,
|
|
36
|
+
collectSymlinkHazards,
|
|
37
|
+
copyManagedScopes,
|
|
38
|
+
createPreimageBackup,
|
|
39
|
+
relocateStagingPaths,
|
|
40
|
+
restorePreimageBackup,
|
|
41
|
+
sha256,
|
|
42
|
+
snapshotManagedState,
|
|
43
|
+
} from './transaction-store.js';
|
|
44
|
+
|
|
45
|
+
const INSTALL_MANIFEST_SCHEMA_VERSION = 1;
|
|
46
|
+
const ARCHIVE_PATH_SEGMENTS = new Set([
|
|
47
|
+
'.trash',
|
|
48
|
+
'archive',
|
|
49
|
+
'archives',
|
|
50
|
+
'archived',
|
|
51
|
+
'backup',
|
|
52
|
+
'backups',
|
|
53
|
+
'deprecated',
|
|
54
|
+
]);
|
|
55
|
+
const ACTION_CATEGORY_RULES = Object.freeze({
|
|
56
|
+
update: new Set(['openprd-workspace']),
|
|
57
|
+
'backfill-work-units': new Set(['openprd-workspace']),
|
|
58
|
+
setup: new Set(['agent-configured']),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
class FleetUpgradeRefusalError extends Error {
|
|
62
|
+
constructor(refusals) {
|
|
63
|
+
super(`Fleet upgrade refused: ${refusals.map((item) => item.message).join('; ')}`);
|
|
64
|
+
this.name = 'FleetUpgradeRefusalError';
|
|
65
|
+
this.code = 'OPENPRD_FLEET_UPGRADE_REFUSED';
|
|
66
|
+
this.refusals = refusals;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class FleetUpgradeMutationError extends Error {
|
|
71
|
+
constructor(message, details = {}) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'FleetUpgradeMutationError';
|
|
74
|
+
this.code = 'OPENPRD_FLEET_UPGRADE_MUTATION_FAILED';
|
|
75
|
+
this.details = details;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sanitizePathToken(value) {
|
|
80
|
+
return String(value || 'project')
|
|
81
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
82
|
+
.replace(/^-+|-+$/g, '')
|
|
83
|
+
.slice(0, 80) || 'project';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function timestampToken(clock = () => new Date()) {
|
|
87
|
+
const value = clock();
|
|
88
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
89
|
+
if (Number.isNaN(date.getTime())) throw new TypeError('Fleet transaction clock must return a valid date');
|
|
90
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveOpenPrdHome(options = {}) {
|
|
94
|
+
return path.resolve(options.openprdHome ?? process.env.OPENPRD_HOME ?? path.join(os.homedir(), '.openprd'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function packageVersion() {
|
|
98
|
+
const raw = await fs.readFile(new URL('../../package.json', import.meta.url), 'utf8');
|
|
99
|
+
return JSON.parse(raw).version ?? '0.0.0';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseSemver(value) {
|
|
103
|
+
const match = String(value ?? '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
104
|
+
return match ? match.slice(1).map((part) => Number.parseInt(part, 10)) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function compareSemver(left, right) {
|
|
108
|
+
const a = parseSemver(left);
|
|
109
|
+
const b = parseSemver(right);
|
|
110
|
+
if (!a || !b) return null;
|
|
111
|
+
for (let index = 0; index < 3; index += 1) {
|
|
112
|
+
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readJsonIfPresent(filePath) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.code === 'ENOENT') return null;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pathSegments(filePath) {
|
|
127
|
+
const parsed = path.parse(path.resolve(filePath));
|
|
128
|
+
return path.resolve(filePath)
|
|
129
|
+
.slice(parsed.root.length)
|
|
130
|
+
.split(path.sep)
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.map((part) => part.toLowerCase());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isArchivePathSegment(segment) {
|
|
136
|
+
return ARCHIVE_PATH_SEGMENTS.has(segment)
|
|
137
|
+
|| /(^|[-_.])(archive|archived|backup|backups)([-_.]|$)/.test(segment);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isArchiveProject(projectRoot) {
|
|
141
|
+
return pathSegments(projectRoot).some(isArchivePathSegment);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function inspectManifestCompatibility(projectRoot, targetVersion) {
|
|
145
|
+
const refusals = [];
|
|
146
|
+
const projectManifest = await readJsonIfPresent(path.join(projectRoot, '.openprd', 'manifest.json')).catch((error) => {
|
|
147
|
+
refusals.push({
|
|
148
|
+
code: 'invalid-project-manifest',
|
|
149
|
+
path: '.openprd/manifest.json',
|
|
150
|
+
message: `Project manifest cannot be read: ${error.message}`,
|
|
151
|
+
});
|
|
152
|
+
return null;
|
|
153
|
+
});
|
|
154
|
+
if (projectManifest) {
|
|
155
|
+
try {
|
|
156
|
+
assertProjectManifestCompatible(projectManifest, { mode: 'write' });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
refusals.push({
|
|
159
|
+
code: error.code ?? 'incompatible-project-manifest',
|
|
160
|
+
path: '.openprd/manifest.json',
|
|
161
|
+
message: error.message,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const installManifestPath = path.join(projectRoot, '.openprd', 'harness', 'install-manifest.json');
|
|
167
|
+
const installManifest = await readJsonIfPresent(installManifestPath).catch((error) => {
|
|
168
|
+
refusals.push({
|
|
169
|
+
code: 'invalid-install-manifest',
|
|
170
|
+
path: '.openprd/harness/install-manifest.json',
|
|
171
|
+
message: `Install manifest cannot be read: ${error.message}`,
|
|
172
|
+
});
|
|
173
|
+
return null;
|
|
174
|
+
});
|
|
175
|
+
if (installManifest && !Number.isSafeInteger(installManifest.version)) {
|
|
176
|
+
refusals.push({
|
|
177
|
+
code: 'invalid-install-manifest-version',
|
|
178
|
+
path: '.openprd/harness/install-manifest.json',
|
|
179
|
+
message: 'Install manifest version must be an integer before fleet can write the project',
|
|
180
|
+
});
|
|
181
|
+
} else if (Number.isSafeInteger(installManifest?.version) && installManifest.version > INSTALL_MANIFEST_SCHEMA_VERSION) {
|
|
182
|
+
refusals.push({
|
|
183
|
+
code: 'install-manifest-too-new',
|
|
184
|
+
path: '.openprd/harness/install-manifest.json',
|
|
185
|
+
message: `Install manifest schema ${installManifest.version} is newer than supported ${INSTALL_MANIFEST_SCHEMA_VERSION}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (compareSemver(installManifest?.openprdVersion, targetVersion) === 1) {
|
|
189
|
+
refusals.push({
|
|
190
|
+
code: 'project-version-too-new',
|
|
191
|
+
path: '.openprd/harness/install-manifest.json',
|
|
192
|
+
message: `Project OpenPrd ${installManifest.openprdVersion} is newer than fleet target ${targetVersion}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return { refusals, installManifest, projectManifest };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function inspectFleetUpgradeSafety(project, options = {}) {
|
|
199
|
+
const targetVersion = options.targetVersion ?? await packageVersion();
|
|
200
|
+
const refusals = [];
|
|
201
|
+
const allowedCategories = ACTION_CATEGORY_RULES[project.plannedAction];
|
|
202
|
+
if (!allowedCategories || !allowedCategories.has(project.category)) {
|
|
203
|
+
refusals.push({
|
|
204
|
+
code: 'classification-unknown',
|
|
205
|
+
path: '.',
|
|
206
|
+
message: `Project classification ${project.category ?? 'unknown'} is not writable for ${project.plannedAction ?? 'unknown action'}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (isArchiveProject(project.path)) {
|
|
210
|
+
refusals.push({
|
|
211
|
+
code: 'archive-project',
|
|
212
|
+
path: '.',
|
|
213
|
+
message: 'Archive and backup project paths are immutable during fleet refresh',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const compatibility = await inspectManifestCompatibility(project.path, targetVersion);
|
|
217
|
+
refusals.push(...compatibility.refusals, ...await collectSymlinkHazards(project.path));
|
|
218
|
+
return {
|
|
219
|
+
ok: refusals.length === 0,
|
|
220
|
+
targetVersion,
|
|
221
|
+
currentVersion: compatibility.installManifest?.openprdVersion ?? null,
|
|
222
|
+
refusals,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function createFleetTransactionContext(options = {}) {
|
|
227
|
+
const targetVersion = options.targetVersion ?? await packageVersion();
|
|
228
|
+
const transactionId = options.transactionId ?? `fleet-${timestampToken(options.clock)}-${randomUUID().slice(0, 8)}`;
|
|
229
|
+
const backupRoot = path.resolve(options.backupRoot ?? path.join(
|
|
230
|
+
resolveOpenPrdHome(options),
|
|
231
|
+
'registry',
|
|
232
|
+
'backups',
|
|
233
|
+
`fleet-refresh-${targetVersion}-${transactionId}`,
|
|
234
|
+
));
|
|
235
|
+
return {
|
|
236
|
+
schema: FLEET_TRANSACTION_SCHEMA,
|
|
237
|
+
version: FLEET_TRANSACTION_VERSION,
|
|
238
|
+
transactionId,
|
|
239
|
+
targetVersion,
|
|
240
|
+
backupRoot,
|
|
241
|
+
checksumAlgorithm: CHECKSUM_ALGORITHM,
|
|
242
|
+
applyStrategy: 'staging+checksum-cas+atomic-file-replace',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function planFleetProjectTransaction(project, context, options = {}) {
|
|
247
|
+
const safety = await inspectFleetUpgradeSafety(project, { targetVersion: context.targetVersion });
|
|
248
|
+
const preimage = safety.ok ? await snapshotManagedState(project.path) : null;
|
|
249
|
+
const projectToken = sanitizePathToken(project.relativePath === '.' ? path.basename(project.path) : project.relativePath);
|
|
250
|
+
const projectDigest = sha256(path.resolve(project.path)).slice(0, 12);
|
|
251
|
+
const backupPath = path.join(context.backupRoot, `${projectToken}-${projectDigest}`);
|
|
252
|
+
return {
|
|
253
|
+
schema: FLEET_TRANSACTION_SCHEMA,
|
|
254
|
+
version: FLEET_TRANSACTION_VERSION,
|
|
255
|
+
transactionId: context.transactionId,
|
|
256
|
+
projectRoot: path.resolve(project.path),
|
|
257
|
+
relativePath: project.relativePath,
|
|
258
|
+
category: project.category,
|
|
259
|
+
action: project.plannedAction,
|
|
260
|
+
eligible: safety.ok,
|
|
261
|
+
refusal: safety.refusals,
|
|
262
|
+
versions: { before: safety.currentVersion, target: context.targetVersion, after: null },
|
|
263
|
+
checksum: {
|
|
264
|
+
algorithm: CHECKSUM_ALGORITHM,
|
|
265
|
+
before: preimage?.checksum ?? null,
|
|
266
|
+
staged: null,
|
|
267
|
+
after: null,
|
|
268
|
+
},
|
|
269
|
+
preimage,
|
|
270
|
+
backup: {
|
|
271
|
+
status: !safety.ok ? 'refused' : (options.dryRun ? 'planned' : 'pending'),
|
|
272
|
+
path: backupPath,
|
|
273
|
+
manifestPath: path.join(backupPath, 'manifest.json'),
|
|
274
|
+
checksum: preimage?.checksum ?? null,
|
|
275
|
+
},
|
|
276
|
+
migration: null,
|
|
277
|
+
rollback: { attempted: false, ok: null, reason: null, checksum: null },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function readAppliedVersion(projectRoot) {
|
|
282
|
+
const manifest = await readJsonIfPresent(path.join(projectRoot, '.openprd', 'harness', 'install-manifest.json'));
|
|
283
|
+
return manifest?.openprdVersion ?? null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function executeFleetProjectTransaction(plan, options = {}) {
|
|
287
|
+
if (!plan.eligible) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
plan,
|
|
291
|
+
error: new FleetUpgradeRefusalError(plan.refusal),
|
|
292
|
+
mutationResult: null,
|
|
293
|
+
afterApplyResult: null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const lockPath = path.join(path.dirname(plan.backup.path), 'locks', `${sha256(plan.projectRoot).slice(0, 16)}.lock`);
|
|
297
|
+
return withFileLock(lockPath, async () => {
|
|
298
|
+
let backupCreated = false;
|
|
299
|
+
let mutationResult = null;
|
|
300
|
+
try {
|
|
301
|
+
const freshSafety = await inspectFleetUpgradeSafety({
|
|
302
|
+
path: plan.projectRoot,
|
|
303
|
+
category: plan.category,
|
|
304
|
+
plannedAction: plan.action,
|
|
305
|
+
}, { targetVersion: plan.versions.target });
|
|
306
|
+
if (!freshSafety.ok) throw new FleetUpgradeRefusalError(freshSafety.refusals);
|
|
307
|
+
const freshPreimage = await snapshotManagedState(plan.projectRoot);
|
|
308
|
+
if (freshPreimage.checksum !== plan.preimage.checksum) {
|
|
309
|
+
const error = new Error(`Project changed after fleet planning: expected ${plan.preimage.checksum}, found ${freshPreimage.checksum}`);
|
|
310
|
+
error.code = 'OPENPRD_FLEET_PREIMAGE_CONFLICT';
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await createPreimageBackup(plan);
|
|
315
|
+
backupCreated = true;
|
|
316
|
+
const stagingRoot = path.join(plan.backup.path, 'staging');
|
|
317
|
+
const stagingHome = path.join(plan.backup.path, 'staging-openprd-home');
|
|
318
|
+
await copyManagedScopes(plan.projectRoot, stagingRoot);
|
|
319
|
+
mutationResult = await options.mutate(stagingRoot, {
|
|
320
|
+
openprdHome: stagingHome,
|
|
321
|
+
projectRoot: plan.projectRoot,
|
|
322
|
+
});
|
|
323
|
+
plan.migration = mutationResult?.migration ?? null;
|
|
324
|
+
if (!mutationResult?.ok) {
|
|
325
|
+
throw new FleetUpgradeMutationError(
|
|
326
|
+
mutationResult?.message ?? 'Staged fleet mutation reported failure',
|
|
327
|
+
mutationResult ?? {},
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
await relocateStagingPaths(stagingRoot, stagingHome, plan.projectRoot, resolveOpenPrdHome(options));
|
|
331
|
+
const stagedSnapshot = await snapshotManagedState(stagingRoot);
|
|
332
|
+
plan.checksum.staged = stagedSnapshot.checksum;
|
|
333
|
+
const applied = await applyStagedState(plan, stagingRoot, stagedSnapshot, { beforeApply: options.beforeApply });
|
|
334
|
+
plan.checksum.after = applied.checksum;
|
|
335
|
+
plan.versions.after = await readAppliedVersion(plan.projectRoot);
|
|
336
|
+
const afterApplyResult = options.afterApply
|
|
337
|
+
? await options.afterApply({ plan, mutationResult, applied })
|
|
338
|
+
: null;
|
|
339
|
+
plan.checksum.after = (await snapshotManagedState(plan.projectRoot)).checksum;
|
|
340
|
+
try {
|
|
341
|
+
await fs.rm(stagingRoot, { recursive: true, force: true });
|
|
342
|
+
await fs.rm(stagingHome, { recursive: true, force: true });
|
|
343
|
+
plan.backup.stagingStatus = 'removed-after-apply';
|
|
344
|
+
} catch (error) {
|
|
345
|
+
plan.backup.stagingStatus = 'retained-cleanup-failed';
|
|
346
|
+
plan.backup.cleanupWarning = error.message;
|
|
347
|
+
}
|
|
348
|
+
await atomicWriteJson(path.join(plan.backup.path, 'result.json'), {
|
|
349
|
+
schema: FLEET_TRANSACTION_SCHEMA,
|
|
350
|
+
version: FLEET_TRANSACTION_VERSION,
|
|
351
|
+
ok: true,
|
|
352
|
+
plan,
|
|
353
|
+
});
|
|
354
|
+
return { ok: true, plan, mutationResult, afterApplyResult, error: null };
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (!backupCreated && plan.backup.status !== 'refused') plan.backup.status = 'failed';
|
|
357
|
+
let rollbackError = null;
|
|
358
|
+
if (backupCreated) {
|
|
359
|
+
try {
|
|
360
|
+
await restorePreimageBackup(plan, error.code ?? error.message);
|
|
361
|
+
} catch (restoreError) {
|
|
362
|
+
rollbackError = restoreError;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const resultError = rollbackError
|
|
366
|
+
? new AggregateError([error, rollbackError], `Fleet upgrade failed and rollback failed for ${plan.projectRoot}`)
|
|
367
|
+
: error;
|
|
368
|
+
if (backupCreated) {
|
|
369
|
+
await atomicWriteJson(path.join(plan.backup.path, 'result.json'), {
|
|
370
|
+
schema: FLEET_TRANSACTION_SCHEMA,
|
|
371
|
+
version: FLEET_TRANSACTION_VERSION,
|
|
372
|
+
ok: false,
|
|
373
|
+
error: {
|
|
374
|
+
name: resultError.name,
|
|
375
|
+
code: resultError.code ?? null,
|
|
376
|
+
message: resultError.message,
|
|
377
|
+
},
|
|
378
|
+
plan,
|
|
379
|
+
}).catch(() => {});
|
|
380
|
+
}
|
|
381
|
+
return { ok: false, plan, mutationResult, afterApplyResult: null, error: resultError };
|
|
382
|
+
}
|
|
383
|
+
}, options.lockOptions);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export {
|
|
387
|
+
CHECKSUM_ALGORITHM,
|
|
388
|
+
FLEET_MANAGED_SCOPES,
|
|
389
|
+
FLEET_TRANSACTION_SCHEMA,
|
|
390
|
+
FLEET_TRANSACTION_VERSION,
|
|
391
|
+
FleetUpgradeMutationError,
|
|
392
|
+
FleetUpgradeRefusalError,
|
|
393
|
+
createFleetTransactionContext,
|
|
394
|
+
executeFleetProjectTransaction,
|
|
395
|
+
inspectFleetUpgradeSafety,
|
|
396
|
+
planFleetProjectTransaction,
|
|
397
|
+
snapshotManagedState,
|
|
398
|
+
};
|