opencode-plugin-flow 5.3.1 → 5.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +33 -25
- package/dist/cli.js +729 -62
- package/dist/cli.js.map +6 -5
- package/dist/index.js +49 -18
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readdir,
|
|
12
12
|
rename,
|
|
13
13
|
rm,
|
|
14
|
+
rmdir,
|
|
14
15
|
writeFile
|
|
15
16
|
} from "node:fs/promises";
|
|
16
17
|
import { homedir } from "node:os";
|
|
@@ -26,6 +27,93 @@ import {
|
|
|
26
27
|
} from "node:path";
|
|
27
28
|
import { fileURLToPath } from "node:url";
|
|
28
29
|
|
|
30
|
+
// src/platform/opencode/leadership.ts
|
|
31
|
+
var REGISTRY_KIND = "opencode-plugin-flow.runtime-leadership";
|
|
32
|
+
var MAX_VERSION_LENGTH = 256;
|
|
33
|
+
var FLOW_LEADERSHIP_REGISTRY_SYMBOL = Symbol.for(REGISTRY_KIND);
|
|
34
|
+
var SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
35
|
+
function parseSemanticVersion(version) {
|
|
36
|
+
if (version.length === 0 || version.length > MAX_VERSION_LENGTH)
|
|
37
|
+
return null;
|
|
38
|
+
const match = SEMVER_PATTERN.exec(version);
|
|
39
|
+
const major = match?.[1];
|
|
40
|
+
const minor = match?.[2];
|
|
41
|
+
const patch = match?.[3];
|
|
42
|
+
if (major === undefined || minor === undefined || patch === undefined) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const prereleaseText = match?.[4];
|
|
46
|
+
const prerelease = prereleaseText ? prereleaseText.split(".").map((identifier) => /^\d+$/.test(identifier) ? { kind: "numeric", value: BigInt(identifier) } : { kind: "text", value: identifier }) : null;
|
|
47
|
+
return {
|
|
48
|
+
major: BigInt(major),
|
|
49
|
+
minor: BigInt(minor),
|
|
50
|
+
patch: BigInt(patch),
|
|
51
|
+
prerelease
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function compareBigInts(left, right) {
|
|
55
|
+
if (left < right)
|
|
56
|
+
return -1;
|
|
57
|
+
if (left > right)
|
|
58
|
+
return 1;
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
function compareText(left, right) {
|
|
62
|
+
if (left < right)
|
|
63
|
+
return -1;
|
|
64
|
+
if (left > right)
|
|
65
|
+
return 1;
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
function comparePrerelease(left, right) {
|
|
69
|
+
if (left === null && right === null)
|
|
70
|
+
return 0;
|
|
71
|
+
if (left === null)
|
|
72
|
+
return 1;
|
|
73
|
+
if (right === null)
|
|
74
|
+
return -1;
|
|
75
|
+
const length = Math.max(left.length, right.length);
|
|
76
|
+
for (let index = 0;index < length; index += 1) {
|
|
77
|
+
const leftIdentifier = left[index];
|
|
78
|
+
const rightIdentifier = right[index];
|
|
79
|
+
if (leftIdentifier === undefined)
|
|
80
|
+
return -1;
|
|
81
|
+
if (rightIdentifier === undefined)
|
|
82
|
+
return 1;
|
|
83
|
+
if (leftIdentifier.kind === "numeric" && rightIdentifier.kind === "numeric") {
|
|
84
|
+
const comparison2 = compareBigInts(leftIdentifier.value, rightIdentifier.value);
|
|
85
|
+
if (comparison2 !== 0)
|
|
86
|
+
return comparison2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (leftIdentifier.kind === "numeric")
|
|
90
|
+
return -1;
|
|
91
|
+
if (rightIdentifier.kind === "numeric")
|
|
92
|
+
return 1;
|
|
93
|
+
const comparison = compareText(leftIdentifier.value, rightIdentifier.value);
|
|
94
|
+
if (comparison !== 0)
|
|
95
|
+
return comparison;
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
function compareSemanticVersions(left, right) {
|
|
100
|
+
const leftVersion = parseSemanticVersion(left);
|
|
101
|
+
const rightVersion = parseSemanticVersion(right);
|
|
102
|
+
if (!leftVersion || !rightVersion) {
|
|
103
|
+
throw new TypeError("Flow leadership versions must be valid exact semantic versions.");
|
|
104
|
+
}
|
|
105
|
+
for (const [leftPart, rightPart] of [
|
|
106
|
+
[leftVersion.major, rightVersion.major],
|
|
107
|
+
[leftVersion.minor, rightVersion.minor],
|
|
108
|
+
[leftVersion.patch, rightVersion.patch]
|
|
109
|
+
]) {
|
|
110
|
+
const comparison = compareBigInts(leftPart, rightPart);
|
|
111
|
+
if (comparison !== 0)
|
|
112
|
+
return comparison;
|
|
113
|
+
}
|
|
114
|
+
return comparePrerelease(leftVersion.prerelease, rightVersion.prerelease);
|
|
115
|
+
}
|
|
116
|
+
|
|
29
117
|
// src/version.ts
|
|
30
118
|
import { createRequire } from "node:module";
|
|
31
119
|
function resolveFlowPluginVersion() {
|
|
@@ -182,6 +270,15 @@ async function optionalLstat(path) {
|
|
|
182
270
|
throw error;
|
|
183
271
|
}
|
|
184
272
|
}
|
|
273
|
+
async function removeEmptyDirectory(path) {
|
|
274
|
+
try {
|
|
275
|
+
await rmdir(path);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const code = error.code;
|
|
278
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY")
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
185
282
|
async function symlinkInPathRange(safetyRoot, target) {
|
|
186
283
|
const root = normalize(safetyRoot);
|
|
187
284
|
const destination = normalize(target);
|
|
@@ -497,6 +594,31 @@ function parseOwnedWrapper(content) {
|
|
|
497
594
|
}
|
|
498
595
|
return { kind: "owned", version };
|
|
499
596
|
}
|
|
597
|
+
function legacyFlowWrapperContent(version) {
|
|
598
|
+
return [
|
|
599
|
+
"const flowPluginUrl = new URL(",
|
|
600
|
+
` "../.cache/opencode/packages/${FLOW_PACKAGE_NAME}@${version}/node_modules/${FLOW_PACKAGE_NAME}/dist/index.js",`,
|
|
601
|
+
` \`file://\${process.env.HOME}/\`,`,
|
|
602
|
+
")",
|
|
603
|
+
"",
|
|
604
|
+
"export default async function flowPlugin(input, options) {",
|
|
605
|
+
' process.env.BUN_BE_BUN = "1"',
|
|
606
|
+
" const { default: plugin } = await import(flowPluginUrl.href)",
|
|
607
|
+
" return plugin(input, options)",
|
|
608
|
+
"}",
|
|
609
|
+
""
|
|
610
|
+
].join(`
|
|
611
|
+
`);
|
|
612
|
+
}
|
|
613
|
+
function parseLegacyFlowWrapper(path, content) {
|
|
614
|
+
const normalized = content.replaceAll(`\r
|
|
615
|
+
`, `
|
|
616
|
+
`);
|
|
617
|
+
const version = hintedFlowVersion(normalized);
|
|
618
|
+
if (!version || basename(path) !== `flow-${version}-wrapper.js`)
|
|
619
|
+
return null;
|
|
620
|
+
return normalized === legacyFlowWrapperContent(version) ? { version } : null;
|
|
621
|
+
}
|
|
500
622
|
function hintedFlowVersion(content) {
|
|
501
623
|
const candidate = new RegExp(`${FLOW_PACKAGE_NAME.replaceAll("-", "\\-")}@([^/\\s"']+)`).exec(content)?.[1];
|
|
502
624
|
return candidate && isExactFlowVersion(candidate) ? candidate : null;
|
|
@@ -575,6 +697,19 @@ async function classifyLocalPlugin(path, source, scope, target, specifier) {
|
|
|
575
697
|
reason: owned.version === target ? "local wrapper duplicates the canonical npm activation source" : "local wrapper activates another Flow version"
|
|
576
698
|
};
|
|
577
699
|
}
|
|
700
|
+
const legacy = parseLegacyFlowWrapper(path, content);
|
|
701
|
+
if (legacy) {
|
|
702
|
+
return {
|
|
703
|
+
source,
|
|
704
|
+
scope,
|
|
705
|
+
path,
|
|
706
|
+
specifier,
|
|
707
|
+
resolvedVersion: legacy.version,
|
|
708
|
+
ownership: "legacy-flow-wrapper",
|
|
709
|
+
status: "conflict",
|
|
710
|
+
reason: "exact known legacy Flow wrapper must be removed"
|
|
711
|
+
};
|
|
712
|
+
}
|
|
578
713
|
const flowLike = looksLikeFlowPath(specifier) || looksLikeFlowPath(path) || content.includes(FLOW_PACKAGE_NAME) || content.includes(OWNED_WRAPPER_MARKER);
|
|
579
714
|
if (!flowLike)
|
|
580
715
|
return null;
|
|
@@ -887,7 +1022,7 @@ async function activationLimitations(paths) {
|
|
|
887
1022
|
source: "remote-config",
|
|
888
1023
|
coverage: "runtime-leadership",
|
|
889
1024
|
blocking: false,
|
|
890
|
-
detail: "Authenticated .well-known and organization API configs cannot be inspected offline; Flow runtime leadership detects duplicate loaded versions and fails closed."
|
|
1025
|
+
detail: "Authenticated .well-known and organization API configs cannot be inspected offline; Flow runtime leadership detects duplicate loaded versions within each OpenCode project context and fails closed there."
|
|
891
1026
|
}
|
|
892
1027
|
];
|
|
893
1028
|
if (paths.managedPreferencePaths.length > 0) {
|
|
@@ -904,7 +1039,7 @@ async function activationLimitations(paths) {
|
|
|
904
1039
|
source: "managed-preferences",
|
|
905
1040
|
coverage: "runtime-leadership",
|
|
906
1041
|
blocking: false,
|
|
907
|
-
detail: detected.length > 0 ? `Detected managed preference source(s) ${detected.join(", ")}; plist/MDM plugin values are not decoded by this dependency-free preflight, and runtime leadership fails closed on duplicates.` : "macOS MDM preferences may add runtime config outside readable JSON/JSONC files; Flow runtime leadership fails closed on duplicate loaded versions."
|
|
1042
|
+
detail: detected.length > 0 ? `Detected managed preference source(s) ${detected.join(", ")}; plist/MDM plugin values are not decoded by this dependency-free preflight, and runtime leadership fails closed on same-project duplicates.` : "macOS MDM preferences may add runtime config outside readable JSON/JSONC files; Flow runtime leadership fails closed on duplicate loaded versions within each OpenCode project context."
|
|
908
1043
|
});
|
|
909
1044
|
}
|
|
910
1045
|
return limitations;
|
|
@@ -1005,12 +1140,18 @@ async function checkFlowActivation(options) {
|
|
|
1005
1140
|
};
|
|
1006
1141
|
}
|
|
1007
1142
|
issues.push(...cache.issues);
|
|
1143
|
+
issues.push(...await activationJournalIssues(paths, options.ignoreRecoveryRunId));
|
|
1008
1144
|
const limitations = await activationLimitations(paths);
|
|
1009
1145
|
const reasons = activationReasons(records, cache.artifacts, issues, target);
|
|
1010
1146
|
return {
|
|
1011
1147
|
mode: "check",
|
|
1012
1148
|
project: paths.project,
|
|
1013
1149
|
target,
|
|
1150
|
+
coverage: {
|
|
1151
|
+
globalSources: true,
|
|
1152
|
+
selectedProject: paths.project,
|
|
1153
|
+
otherProjectTrees: false
|
|
1154
|
+
},
|
|
1014
1155
|
paths,
|
|
1015
1156
|
records,
|
|
1016
1157
|
cacheArtifacts: cache.artifacts,
|
|
@@ -1113,14 +1254,15 @@ function wrapperMutationSafetyRoot(paths, wrapper) {
|
|
|
1113
1254
|
function uniqueOwnedWrappers(records) {
|
|
1114
1255
|
const wrappers = new Map;
|
|
1115
1256
|
for (const record of records) {
|
|
1116
|
-
if (record.ownership !== "marker-owned-wrapper" || record.resolvedVersion === null) {
|
|
1257
|
+
if (record.ownership !== "marker-owned-wrapper" && record.ownership !== "legacy-flow-wrapper" || record.resolvedVersion === null) {
|
|
1117
1258
|
continue;
|
|
1118
1259
|
}
|
|
1119
1260
|
wrappers.set(record.path, {
|
|
1120
1261
|
path: record.path,
|
|
1121
1262
|
scope: record.scope,
|
|
1122
1263
|
source: record.source,
|
|
1123
|
-
version: record.resolvedVersion
|
|
1264
|
+
version: record.resolvedVersion,
|
|
1265
|
+
ownership: record.ownership
|
|
1124
1266
|
});
|
|
1125
1267
|
}
|
|
1126
1268
|
return [...wrappers.values()];
|
|
@@ -1138,7 +1280,7 @@ function removableConfigEntry(entry, descriptor, records) {
|
|
|
1138
1280
|
} catch {
|
|
1139
1281
|
return false;
|
|
1140
1282
|
}
|
|
1141
|
-
return records.some((record) => record.source === descriptor.source && record.specifier === specifier && record.path === localPath && record.ownership === "marker-owned-wrapper");
|
|
1283
|
+
return records.some((record) => record.source === descriptor.source && record.specifier === specifier && record.path === localPath && (record.ownership === "marker-owned-wrapper" || record.ownership === "legacy-flow-wrapper"));
|
|
1142
1284
|
}
|
|
1143
1285
|
function activationRefusals(before) {
|
|
1144
1286
|
return [
|
|
@@ -1147,6 +1289,18 @@ function activationRefusals(before) {
|
|
|
1147
1289
|
...before.cacheArtifacts.filter((artifact) => artifact.status === "ambiguous").map((artifact) => `${artifact.path}: ${artifact.reason ?? "ambiguous cache artifact refused"}`)
|
|
1148
1290
|
];
|
|
1149
1291
|
}
|
|
1292
|
+
function downgradeRefusals(before) {
|
|
1293
|
+
const newerVersions = new Set;
|
|
1294
|
+
for (const version of [
|
|
1295
|
+
...before.records.map((record) => record.resolvedVersion),
|
|
1296
|
+
...before.cacheArtifacts.map((artifact) => artifact.resolvedVersion)
|
|
1297
|
+
]) {
|
|
1298
|
+
if (version && compareSemanticVersions(version, before.target) > 0) {
|
|
1299
|
+
newerVersions.add(version);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return [...newerVersions].sort((left, right) => compareSemanticVersions(right, left)).map((version) => `refusing to replace newer installed Flow ${version} with older target ${before.target}; run ${FLOW_PACKAGE_NAME}@latest instead`);
|
|
1303
|
+
}
|
|
1150
1304
|
async function assertUnchangedConfig(snapshot) {
|
|
1151
1305
|
await assertSafeMutationPath(snapshot.descriptor.safetyRoot, snapshot.descriptor.path);
|
|
1152
1306
|
const metadata = await optionalLstat(snapshot.descriptor.path);
|
|
@@ -1233,11 +1387,153 @@ async function writeJournal(journalPath, journal) {
|
|
|
1233
1387
|
await rm(temporaryPath, { force: true });
|
|
1234
1388
|
}
|
|
1235
1389
|
}
|
|
1390
|
+
var TERMINAL_JOURNAL_STATES = new Set([
|
|
1391
|
+
"complete",
|
|
1392
|
+
"rolled-back"
|
|
1393
|
+
]);
|
|
1394
|
+
function isRecord(value) {
|
|
1395
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1396
|
+
}
|
|
1397
|
+
function parseActivationJournal(content, journalPath) {
|
|
1398
|
+
let value;
|
|
1399
|
+
try {
|
|
1400
|
+
value = JSON.parse(content);
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
throw new Error(`${journalPath}: recovery journal is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (!isRecord(value) || typeof value.runId !== "string" || value.runId !== basename(dirname(journalPath)) || typeof value.state !== "string") {
|
|
1405
|
+
throw new Error(`${journalPath}: recovery journal schema is invalid`);
|
|
1406
|
+
}
|
|
1407
|
+
if (value.format === "flow-activation-journal-v1") {
|
|
1408
|
+
if (![
|
|
1409
|
+
"prepared",
|
|
1410
|
+
"applying",
|
|
1411
|
+
"complete",
|
|
1412
|
+
"failed",
|
|
1413
|
+
"rolled-back",
|
|
1414
|
+
"rollback-failed"
|
|
1415
|
+
].includes(value.state)) {
|
|
1416
|
+
throw new Error(`${journalPath}: legacy recovery journal state is invalid`);
|
|
1417
|
+
}
|
|
1418
|
+
return value;
|
|
1419
|
+
}
|
|
1420
|
+
if (value.format !== "flow-activation-journal-v2" || typeof value.createdAt !== "string" || typeof value.project !== "string" || !isAbsolute(value.project) || typeof value.target !== "string" || !isExactFlowVersion(value.target) || value.scope !== "global" && value.scope !== "project" || ![
|
|
1421
|
+
"prepared",
|
|
1422
|
+
"applying",
|
|
1423
|
+
"committed",
|
|
1424
|
+
"complete",
|
|
1425
|
+
"failed",
|
|
1426
|
+
"cleanup-failed",
|
|
1427
|
+
"rolled-back",
|
|
1428
|
+
"rollback-failed"
|
|
1429
|
+
].includes(value.state) || !Array.isArray(value.actions)) {
|
|
1430
|
+
throw new Error(`${journalPath}: recovery journal schema is invalid`);
|
|
1431
|
+
}
|
|
1432
|
+
for (const action of value.actions) {
|
|
1433
|
+
if (!isRecord(action) || !["rewrite-config", "remove-wrapper", "remove-cache"].includes(String(action.action)) || typeof action.path !== "string" || !isAbsolute(action.path) || !["pending", "complete", "rolled-back", "rollback-failed"].includes(String(action.state))) {
|
|
1434
|
+
throw new Error(`${journalPath}: recovery journal action is invalid`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (value.ownerPid !== undefined && (!Number.isSafeInteger(value.ownerPid) || Number(value.ownerPid) <= 0)) {
|
|
1438
|
+
throw new Error(`${journalPath}: recovery journal owner pid is invalid`);
|
|
1439
|
+
}
|
|
1440
|
+
return value;
|
|
1441
|
+
}
|
|
1442
|
+
async function readActivationJournalEntries(paths) {
|
|
1443
|
+
try {
|
|
1444
|
+
await assertSafeMutationPath(dirname(paths.configRoot), paths.journalRoot);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
return [
|
|
1447
|
+
{
|
|
1448
|
+
journalPath: paths.journalRoot,
|
|
1449
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1450
|
+
}
|
|
1451
|
+
];
|
|
1452
|
+
}
|
|
1453
|
+
const rootMetadata = await optionalLstat(paths.journalRoot);
|
|
1454
|
+
if (!rootMetadata)
|
|
1455
|
+
return [];
|
|
1456
|
+
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
|
|
1457
|
+
return [
|
|
1458
|
+
{
|
|
1459
|
+
journalPath: paths.journalRoot,
|
|
1460
|
+
error: `${paths.journalRoot}: recovery root is not a real directory`
|
|
1461
|
+
}
|
|
1462
|
+
];
|
|
1463
|
+
}
|
|
1464
|
+
const entries = [];
|
|
1465
|
+
for (const directory of await readdir(paths.journalRoot, {
|
|
1466
|
+
withFileTypes: true
|
|
1467
|
+
})) {
|
|
1468
|
+
const runRoot = join(paths.journalRoot, directory.name);
|
|
1469
|
+
const journalPath = join(runRoot, "journal.json");
|
|
1470
|
+
if (directory.isSymbolicLink()) {
|
|
1471
|
+
entries.push({
|
|
1472
|
+
journalPath,
|
|
1473
|
+
error: `${runRoot}: symbolic recovery directory refused`
|
|
1474
|
+
});
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (!directory.isDirectory() || !await optionalLstat(journalPath)) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
const content = await readRegularFileWithoutFollowing(journalPath, MAX_LOCAL_PLUGIN_BYTES);
|
|
1482
|
+
entries.push({
|
|
1483
|
+
journalPath,
|
|
1484
|
+
journal: parseActivationJournal(content, journalPath)
|
|
1485
|
+
});
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
entries.push({
|
|
1488
|
+
journalPath,
|
|
1489
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return entries.sort((left, right) => left.journalPath.localeCompare(right.journalPath));
|
|
1494
|
+
}
|
|
1495
|
+
function processIsAlive(pid) {
|
|
1496
|
+
try {
|
|
1497
|
+
process.kill(pid, 0);
|
|
1498
|
+
return true;
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
return error.code === "EPERM";
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async function activationJournalIssues(paths, ignoreRunId) {
|
|
1504
|
+
const issues = [];
|
|
1505
|
+
for (const entry of await readActivationJournalEntries(paths)) {
|
|
1506
|
+
if (entry.error) {
|
|
1507
|
+
issues.push({
|
|
1508
|
+
source: "recovery",
|
|
1509
|
+
path: entry.journalPath,
|
|
1510
|
+
code: "incomplete-recovery",
|
|
1511
|
+
message: entry.error
|
|
1512
|
+
});
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
const journal = entry.journal;
|
|
1516
|
+
if (journal.runId === ignoreRunId || TERMINAL_JOURNAL_STATES.has(journal.state)) {
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const activeOwner = journal.format === "flow-activation-journal-v2" && journal.ownerPid !== undefined && processIsAlive(journal.ownerPid);
|
|
1520
|
+
issues.push({
|
|
1521
|
+
source: "recovery",
|
|
1522
|
+
path: entry.journalPath,
|
|
1523
|
+
code: "incomplete-recovery",
|
|
1524
|
+
message: activeOwner ? `activation recovery is still owned by running process ${journal.ownerPid}` : journal.format === "flow-activation-journal-v1" ? `legacy activation recovery is incomplete in state ${journal.state}; follow that journal's manual recovery guidance before installing` : journal.state === "rollback-failed" ? "activation rollback previously failed and requires the journal's manual recovery guidance" : `activation recovery is incomplete in state ${journal.state}; rerun install to reconcile it before evaluating success`
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
return issues;
|
|
1528
|
+
}
|
|
1236
1529
|
async function verifyOwnedWrapper(wrapper) {
|
|
1237
1530
|
const content = await readRegularFileWithoutFollowing(wrapper.path, MAX_LOCAL_PLUGIN_BYTES);
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1531
|
+
const version = wrapper.ownership === "marker-owned-wrapper" ? (() => {
|
|
1532
|
+
const parsed = parseOwnedWrapper(content);
|
|
1533
|
+
return parsed.kind === "owned" ? parsed.version : null;
|
|
1534
|
+
})() : parseLegacyFlowWrapper(wrapper.path, content)?.version;
|
|
1535
|
+
if (version !== wrapper.version) {
|
|
1536
|
+
throw new Error(`${wrapper.path}: removable wrapper changed while activation was running`);
|
|
1241
1537
|
}
|
|
1242
1538
|
}
|
|
1243
1539
|
async function verifyCacheArtifact(artifact, target) {
|
|
@@ -1247,19 +1543,19 @@ async function verifyCacheArtifact(artifact, target) {
|
|
|
1247
1543
|
}
|
|
1248
1544
|
}
|
|
1249
1545
|
async function replaceKnownConfigContent(options) {
|
|
1250
|
-
await assertSafeMutationPath(options.
|
|
1251
|
-
const current = await readRegularFileWithoutFollowing(options.
|
|
1546
|
+
await assertSafeMutationPath(options.descriptor.safetyRoot, options.descriptor.path);
|
|
1547
|
+
const current = await readRegularFileWithoutFollowing(options.descriptor.path);
|
|
1252
1548
|
if (sha256(current) !== options.expectedDigest) {
|
|
1253
|
-
throw new Error(`${options.
|
|
1549
|
+
throw new Error(`${options.descriptor.path}: automatic restore refused because the applied config changed`);
|
|
1254
1550
|
}
|
|
1255
|
-
const temporaryPath = join(dirname(options.
|
|
1551
|
+
const temporaryPath = join(dirname(options.descriptor.path), `.${basename(options.descriptor.path)}.restore-${options.runId}.tmp`);
|
|
1256
1552
|
try {
|
|
1257
1553
|
await writeFile(temporaryPath, options.content, {
|
|
1258
1554
|
encoding: "utf8",
|
|
1259
1555
|
flag: "wx",
|
|
1260
1556
|
mode: options.mode
|
|
1261
1557
|
});
|
|
1262
|
-
await rename(temporaryPath, options.
|
|
1558
|
+
await rename(temporaryPath, options.descriptor.path);
|
|
1263
1559
|
} finally {
|
|
1264
1560
|
await rm(temporaryPath, { force: true });
|
|
1265
1561
|
}
|
|
@@ -1295,18 +1591,18 @@ async function rollbackCompletedActions(options) {
|
|
|
1295
1591
|
throw new Error("config backup is missing");
|
|
1296
1592
|
const backup = await readRegularFileWithoutFollowing(action.backupPath);
|
|
1297
1593
|
await replaceKnownConfigContent({
|
|
1298
|
-
snapshot,
|
|
1594
|
+
descriptor: snapshot.descriptor,
|
|
1299
1595
|
expectedDigest: action.appliedDigest,
|
|
1300
1596
|
content: backup,
|
|
1301
1597
|
mode: action.originalMode ?? snapshot.mode,
|
|
1302
1598
|
runId: options.runId
|
|
1303
1599
|
});
|
|
1304
1600
|
}
|
|
1305
|
-
} else if (action.action === "
|
|
1306
|
-
if (!action.
|
|
1307
|
-
throw new Error("
|
|
1601
|
+
} else if (action.action === "remove-wrapper" || action.action === "remove-cache") {
|
|
1602
|
+
if (!action.stagingPath)
|
|
1603
|
+
throw new Error("staging path is missing");
|
|
1308
1604
|
let safetyRoot;
|
|
1309
|
-
if (action.action === "
|
|
1605
|
+
if (action.action === "remove-cache") {
|
|
1310
1606
|
safetyRoot = dirname(options.paths.cacheRoot);
|
|
1311
1607
|
} else {
|
|
1312
1608
|
const wrapper = options.wrappers.find((candidate) => candidate.path === action.path);
|
|
@@ -1315,16 +1611,16 @@ async function rollbackCompletedActions(options) {
|
|
|
1315
1611
|
safetyRoot = wrapperMutationSafetyRoot(options.paths, wrapper);
|
|
1316
1612
|
}
|
|
1317
1613
|
await assertSafeMutationPath(safetyRoot, action.path);
|
|
1318
|
-
await assertSafeMutationPath(safetyRoot, action.
|
|
1614
|
+
await assertSafeMutationPath(safetyRoot, action.stagingPath);
|
|
1319
1615
|
if (await optionalLstat(action.path)) {
|
|
1320
1616
|
throw new Error("original path is occupied; automatic restore refused");
|
|
1321
1617
|
}
|
|
1322
|
-
const
|
|
1323
|
-
if (!
|
|
1324
|
-
throw new Error("
|
|
1618
|
+
const staged = await optionalLstat(action.stagingPath);
|
|
1619
|
+
if (!staged || staged.isSymbolicLink()) {
|
|
1620
|
+
throw new Error("staged artifact is missing or symbolic");
|
|
1325
1621
|
}
|
|
1326
1622
|
await mkdir(dirname(action.path), { recursive: true });
|
|
1327
|
-
await rename(action.
|
|
1623
|
+
await rename(action.stagingPath, action.path);
|
|
1328
1624
|
}
|
|
1329
1625
|
action.state = "rolled-back";
|
|
1330
1626
|
} catch (error) {
|
|
@@ -1341,14 +1637,264 @@ async function rollbackCompletedActions(options) {
|
|
|
1341
1637
|
}
|
|
1342
1638
|
return failures;
|
|
1343
1639
|
}
|
|
1640
|
+
function journalConfigDescriptor(paths, action) {
|
|
1641
|
+
const descriptor = configDescriptors(paths).find((candidate) => candidate.path === action.path && (action.source === undefined || candidate.source === action.source));
|
|
1642
|
+
if (!descriptor) {
|
|
1643
|
+
throw new Error(`${action.path}: journal config path is outside inventory`);
|
|
1644
|
+
}
|
|
1645
|
+
return descriptor;
|
|
1646
|
+
}
|
|
1647
|
+
function journalOwnedWrapper(paths, journal, action) {
|
|
1648
|
+
if (action.ownership !== "marker-owned-wrapper" && action.ownership !== "legacy-flow-wrapper" || typeof action.resolvedVersion !== "string" || !isExactFlowVersion(action.resolvedVersion) || typeof action.source !== "string" || typeof action.scope !== "string") {
|
|
1649
|
+
throw new Error(`${action.path}: wrapper recovery metadata is incomplete`);
|
|
1650
|
+
}
|
|
1651
|
+
const wrapper = {
|
|
1652
|
+
path: action.path,
|
|
1653
|
+
source: action.source,
|
|
1654
|
+
scope: action.scope,
|
|
1655
|
+
version: action.resolvedVersion,
|
|
1656
|
+
ownership: action.ownership
|
|
1657
|
+
};
|
|
1658
|
+
const pluginDirectory = pluginDirectoryDescriptors(paths).find((descriptor) => descriptor.source === wrapper.source && descriptor.scope === wrapper.scope && dirname(wrapper.path) === descriptor.path);
|
|
1659
|
+
const config = configDescriptors(paths).find((descriptor) => {
|
|
1660
|
+
if (descriptor.source !== wrapper.source || descriptor.scope !== wrapper.scope) {
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
const fromRoot = relative(descriptor.safetyRoot, wrapper.path);
|
|
1664
|
+
return fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
1665
|
+
});
|
|
1666
|
+
const safetyRoot = pluginDirectory?.safetyRoot ?? config?.safetyRoot;
|
|
1667
|
+
if (!safetyRoot) {
|
|
1668
|
+
throw new Error(`${action.path}: wrapper path is outside inventory`);
|
|
1669
|
+
}
|
|
1670
|
+
const expectedStagingPath = join(wrapperRecoveryRoot(paths, wrapper), journal.runId, sha256(action.path).slice(0, 12), basename(action.path));
|
|
1671
|
+
if (action.stagingPath !== expectedStagingPath) {
|
|
1672
|
+
throw new Error(`${action.path}: wrapper staging path does not match journal`);
|
|
1673
|
+
}
|
|
1674
|
+
return { wrapper, safetyRoot };
|
|
1675
|
+
}
|
|
1676
|
+
function journalCacheArtifact(paths, journal, action) {
|
|
1677
|
+
if (typeof action.specifier !== "string" || action.specifier !== FLOW_PACKAGE_NAME && !action.specifier.startsWith(`${FLOW_PACKAGE_NAME}@`) || action.specifier.includes("/") || action.specifier.includes("\\") || typeof action.resolvedVersion !== "string" || !isExactFlowVersion(action.resolvedVersion) || action.path !== join(paths.packageCacheRoot, action.specifier)) {
|
|
1678
|
+
throw new Error(`${action.path}: cache recovery metadata is incomplete`);
|
|
1679
|
+
}
|
|
1680
|
+
const expectedStagingPath = join(paths.cacheRecoveryRoot, journal.runId, sha256(action.path).slice(0, 12), basename(action.path));
|
|
1681
|
+
if (action.stagingPath !== expectedStagingPath) {
|
|
1682
|
+
throw new Error(`${action.path}: cache staging path does not match journal`);
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
path: action.path,
|
|
1686
|
+
specifier: action.specifier,
|
|
1687
|
+
resolvedVersion: action.resolvedVersion,
|
|
1688
|
+
status: "inactive"
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
async function rollbackInterruptedConfigAction(options) {
|
|
1692
|
+
const { action, journal, journalPath, paths } = options;
|
|
1693
|
+
const descriptor = journalConfigDescriptor(paths, action);
|
|
1694
|
+
await assertSafeMutationPath(descriptor.safetyRoot, action.path);
|
|
1695
|
+
const currentMetadata = await optionalLstat(action.path);
|
|
1696
|
+
if (currentMetadata?.isSymbolicLink() || currentMetadata && !currentMetadata.isFile()) {
|
|
1697
|
+
throw new Error(`${action.path}: interrupted config is not a regular file`);
|
|
1698
|
+
}
|
|
1699
|
+
const current = currentMetadata ? await readRegularFileWithoutFollowing(action.path) : null;
|
|
1700
|
+
if (action.originalAbsent === true) {
|
|
1701
|
+
const expectedRecoveryPath = join(dirname(action.path), ".flow-activation-recovery", journal.runId, `${basename(action.path)}-${sha256(action.path).slice(0, 12)}`);
|
|
1702
|
+
if (action.recoveryPath !== expectedRecoveryPath) {
|
|
1703
|
+
throw new Error(`${action.path}: created-config recovery path is invalid`);
|
|
1704
|
+
}
|
|
1705
|
+
if (current === null)
|
|
1706
|
+
return;
|
|
1707
|
+
if (!action.appliedDigest || sha256(current) !== action.appliedDigest) {
|
|
1708
|
+
throw new Error(`${action.path}: interrupted created config changed; automatic recovery refused`);
|
|
1709
|
+
}
|
|
1710
|
+
await assertSafeMutationPath(descriptor.safetyRoot, action.recoveryPath);
|
|
1711
|
+
if (await optionalLstat(action.recoveryPath)) {
|
|
1712
|
+
throw new Error(`${action.path}: created-config recovery path is occupied`);
|
|
1713
|
+
}
|
|
1714
|
+
await mkdir(dirname(action.recoveryPath), {
|
|
1715
|
+
recursive: true,
|
|
1716
|
+
mode: 448
|
|
1717
|
+
});
|
|
1718
|
+
await rename(action.path, action.recoveryPath);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const expectedBackupPath = join(dirname(journalPath), "configs", `${descriptor.source}-${sha256(action.path).slice(0, 12)}.backup`);
|
|
1722
|
+
if (action.backupPath !== expectedBackupPath) {
|
|
1723
|
+
throw new Error(`${action.path}: config backup path is invalid`);
|
|
1724
|
+
}
|
|
1725
|
+
const backup = await readRegularFileWithoutFollowing(action.backupPath, MAX_LOCAL_PLUGIN_BYTES);
|
|
1726
|
+
if (current === null) {
|
|
1727
|
+
throw new Error(`${action.path}: interrupted config is missing`);
|
|
1728
|
+
}
|
|
1729
|
+
if (sha256(current) === sha256(backup))
|
|
1730
|
+
return;
|
|
1731
|
+
if (!action.appliedDigest || sha256(current) !== action.appliedDigest) {
|
|
1732
|
+
throw new Error(`${action.path}: interrupted config changed; automatic recovery refused`);
|
|
1733
|
+
}
|
|
1734
|
+
await replaceKnownConfigContent({
|
|
1735
|
+
descriptor,
|
|
1736
|
+
expectedDigest: action.appliedDigest,
|
|
1737
|
+
content: backup,
|
|
1738
|
+
mode: action.originalMode ?? 384,
|
|
1739
|
+
runId: journal.runId
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
async function rollbackInterruptedRemovalAction(options) {
|
|
1743
|
+
const { action, journal, paths } = options;
|
|
1744
|
+
let safetyRoot;
|
|
1745
|
+
let verifyStaged;
|
|
1746
|
+
if (action.action === "remove-wrapper") {
|
|
1747
|
+
const { wrapper, safetyRoot: wrapperSafetyRoot } = journalOwnedWrapper(paths, journal, action);
|
|
1748
|
+
safetyRoot = wrapperSafetyRoot;
|
|
1749
|
+
verifyStaged = () => verifyOwnedWrapper({ ...wrapper, path: action.stagingPath });
|
|
1750
|
+
} else {
|
|
1751
|
+
const artifact = journalCacheArtifact(paths, journal, action);
|
|
1752
|
+
safetyRoot = dirname(paths.cacheRoot);
|
|
1753
|
+
verifyStaged = () => verifyCacheArtifact({ ...artifact, path: action.stagingPath }, journal.target);
|
|
1754
|
+
}
|
|
1755
|
+
await assertSafeMutationPath(safetyRoot, action.path);
|
|
1756
|
+
await assertSafeMutationPath(safetyRoot, action.stagingPath);
|
|
1757
|
+
const original = await optionalLstat(action.path);
|
|
1758
|
+
const staged = await optionalLstat(action.stagingPath);
|
|
1759
|
+
if (original && staged) {
|
|
1760
|
+
throw new Error(`${action.path}: both original and staged artifacts exist`);
|
|
1761
|
+
}
|
|
1762
|
+
if (original) {
|
|
1763
|
+
if (original.isSymbolicLink()) {
|
|
1764
|
+
throw new Error(`${action.path}: restored artifact is symbolic`);
|
|
1765
|
+
}
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (!staged || staged.isSymbolicLink()) {
|
|
1769
|
+
throw new Error(`${action.path}: interrupted staged artifact is missing`);
|
|
1770
|
+
}
|
|
1771
|
+
await verifyStaged();
|
|
1772
|
+
await mkdir(dirname(action.path), { recursive: true });
|
|
1773
|
+
await rename(action.stagingPath, action.path);
|
|
1774
|
+
}
|
|
1775
|
+
async function rollbackInterruptedJournal(journal, journalPath, paths) {
|
|
1776
|
+
for (const action of journal.actions.toReversed()) {
|
|
1777
|
+
if (action.state === "rolled-back")
|
|
1778
|
+
continue;
|
|
1779
|
+
if (action.action === "rewrite-config") {
|
|
1780
|
+
await rollbackInterruptedConfigAction({
|
|
1781
|
+
journal,
|
|
1782
|
+
journalPath,
|
|
1783
|
+
action,
|
|
1784
|
+
paths
|
|
1785
|
+
});
|
|
1786
|
+
} else {
|
|
1787
|
+
await rollbackInterruptedRemovalAction({ journal, action, paths });
|
|
1788
|
+
}
|
|
1789
|
+
action.state = "rolled-back";
|
|
1790
|
+
delete action.error;
|
|
1791
|
+
await writeJournal(journalPath, journal);
|
|
1792
|
+
}
|
|
1793
|
+
journal.state = "rolled-back";
|
|
1794
|
+
delete journal.ownerPid;
|
|
1795
|
+
delete journal.error;
|
|
1796
|
+
await writeJournal(journalPath, journal);
|
|
1797
|
+
}
|
|
1798
|
+
async function finishCommittedJournalCleanup(journal, journalPath, paths) {
|
|
1799
|
+
const removalActions = journal.actions.filter((action) => action.action === "remove-wrapper" || action.action === "remove-cache");
|
|
1800
|
+
for (const action of removalActions) {
|
|
1801
|
+
let safetyRoot;
|
|
1802
|
+
let verifyStaged;
|
|
1803
|
+
if (action.action === "remove-wrapper") {
|
|
1804
|
+
const { wrapper, safetyRoot: wrapperSafetyRoot } = journalOwnedWrapper(paths, journal, action);
|
|
1805
|
+
safetyRoot = wrapperSafetyRoot;
|
|
1806
|
+
verifyStaged = () => verifyOwnedWrapper({ ...wrapper, path: action.stagingPath });
|
|
1807
|
+
} else {
|
|
1808
|
+
const artifact = journalCacheArtifact(paths, journal, action);
|
|
1809
|
+
safetyRoot = dirname(paths.cacheRoot);
|
|
1810
|
+
verifyStaged = () => verifyCacheArtifact({ ...artifact, path: action.stagingPath }, journal.target);
|
|
1811
|
+
}
|
|
1812
|
+
await assertSafeMutationPath(safetyRoot, action.path);
|
|
1813
|
+
await assertSafeMutationPath(safetyRoot, action.stagingPath);
|
|
1814
|
+
if (await optionalLstat(action.path)) {
|
|
1815
|
+
throw new Error(`${action.path}: obsolete original path reappeared after activation commit`);
|
|
1816
|
+
}
|
|
1817
|
+
if (await optionalLstat(action.stagingPath)) {
|
|
1818
|
+
await verifyStaged();
|
|
1819
|
+
await rm(action.stagingPath, {
|
|
1820
|
+
recursive: action.action === "remove-cache"
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
if (await optionalLstat(action.stagingPath)) {
|
|
1824
|
+
throw new Error(`${action.path}: obsolete staged artifact still exists`);
|
|
1825
|
+
}
|
|
1826
|
+
action.deleted = true;
|
|
1827
|
+
await removeEmptyDirectory(dirname(action.stagingPath));
|
|
1828
|
+
await writeJournal(journalPath, journal);
|
|
1829
|
+
}
|
|
1830
|
+
for (const runDirectory of new Set(removalActions.map((action) => dirname(dirname(action.stagingPath))))) {
|
|
1831
|
+
await removeEmptyDirectory(runDirectory);
|
|
1832
|
+
}
|
|
1833
|
+
await removeEmptyDirectory(paths.cacheRecoveryRoot);
|
|
1834
|
+
journal.state = "complete";
|
|
1835
|
+
delete journal.ownerPid;
|
|
1836
|
+
delete journal.error;
|
|
1837
|
+
await writeJournal(journalPath, journal);
|
|
1838
|
+
}
|
|
1839
|
+
async function reconcileIncompleteActivationJournals(paths, pathOptions) {
|
|
1840
|
+
const failures = [];
|
|
1841
|
+
for (const entry of await readActivationJournalEntries(paths)) {
|
|
1842
|
+
if (entry.error) {
|
|
1843
|
+
failures.push(`recovery journal could not be inspected safely at ${entry.journalPath}: ${entry.error}`);
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
const journal = entry.journal;
|
|
1847
|
+
if (TERMINAL_JOURNAL_STATES.has(journal.state))
|
|
1848
|
+
continue;
|
|
1849
|
+
if (journal.format === "flow-activation-journal-v1") {
|
|
1850
|
+
failures.push(`${entry.journalPath}: legacy activation recovery is incomplete in state ${journal.state}; follow its manual recovery guidance before retrying`);
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (journal.ownerPid !== undefined && processIsAlive(journal.ownerPid)) {
|
|
1854
|
+
failures.push(`${entry.journalPath}: activation is still owned by running process ${journal.ownerPid}`);
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
if (journal.state === "rollback-failed") {
|
|
1858
|
+
failures.push(`${entry.journalPath}: previous rollback failed; follow its manual recovery guidance before retrying`);
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
const journalPaths = resolveActivationPaths(journal.project, pathOptions);
|
|
1862
|
+
if (journalPaths.journalRoot !== paths.journalRoot) {
|
|
1863
|
+
failures.push(`${entry.journalPath}: journal resolves to a different recovery root`);
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
try {
|
|
1867
|
+
if (journal.state === "committed" || journal.state === "cleanup-failed") {
|
|
1868
|
+
await finishCommittedJournalCleanup(journal, entry.journalPath, journalPaths);
|
|
1869
|
+
} else {
|
|
1870
|
+
await rollbackInterruptedJournal(journal, entry.journalPath, journalPaths);
|
|
1871
|
+
}
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
journal.state = journal.state === "committed" || journal.state === "cleanup-failed" ? "cleanup-failed" : "rollback-failed";
|
|
1874
|
+
journal.error = error instanceof Error ? error.message : String(error);
|
|
1875
|
+
delete journal.ownerPid;
|
|
1876
|
+
try {
|
|
1877
|
+
await writeJournal(entry.journalPath, journal);
|
|
1878
|
+
} catch {}
|
|
1879
|
+
failures.push(`${entry.journalPath}: ${journal.error}`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return failures;
|
|
1883
|
+
}
|
|
1344
1884
|
async function applyFlowActivation(options) {
|
|
1345
1885
|
const target = resolveActivationTarget(options.target);
|
|
1886
|
+
const paths = resolveActivationPaths(options.project, options.paths);
|
|
1887
|
+
const recoveryRefusals = options.apply === true ? await reconcileIncompleteActivationJournals(paths, options.paths) : [];
|
|
1346
1888
|
const before = await checkFlowActivation({
|
|
1347
1889
|
project: options.project,
|
|
1348
1890
|
target,
|
|
1349
1891
|
...options.paths ? { paths: options.paths } : {}
|
|
1350
1892
|
});
|
|
1351
|
-
const refusals =
|
|
1893
|
+
const refusals = [
|
|
1894
|
+
...recoveryRefusals,
|
|
1895
|
+
...activationRefusals(before),
|
|
1896
|
+
...downgradeRefusals(before)
|
|
1897
|
+
];
|
|
1352
1898
|
const snapshots = [];
|
|
1353
1899
|
for (const descriptor of configDescriptors(before.paths)) {
|
|
1354
1900
|
try {
|
|
@@ -1375,7 +1921,7 @@ async function applyFlowActivation(options) {
|
|
|
1375
1921
|
await assertSafeMutationPath(safetyRoot, wrapper.path);
|
|
1376
1922
|
await assertSafeMutationPath(safetyRoot, recoveryRoot);
|
|
1377
1923
|
if (!await pathCanBeMoved(wrapper.path, recoveryRoot)) {
|
|
1378
|
-
throw new Error(`${wrapper.path}:
|
|
1924
|
+
throw new Error(`${wrapper.path}: removable wrapper or staging parent is not writable; remove it manually and rerun activation-apply`);
|
|
1379
1925
|
}
|
|
1380
1926
|
wrappers.push(wrapper);
|
|
1381
1927
|
} catch (error) {
|
|
@@ -1444,17 +1990,17 @@ async function applyFlowActivation(options) {
|
|
|
1444
1990
|
const canonicalSnapshot = snapshots.find((snapshot) => snapshot.descriptor.path === canonicalPath);
|
|
1445
1991
|
for (const wrapper of wrappers) {
|
|
1446
1992
|
plan.push({
|
|
1447
|
-
action: "
|
|
1993
|
+
action: "remove-wrapper",
|
|
1448
1994
|
scope: wrapper.scope,
|
|
1449
1995
|
path: wrapper.path,
|
|
1450
|
-
detail: "
|
|
1996
|
+
detail: "permanently remove the proven Flow wrapper after reversible staging and activation verification"
|
|
1451
1997
|
});
|
|
1452
1998
|
}
|
|
1453
1999
|
for (const artifact of inactiveCache) {
|
|
1454
2000
|
plan.push({
|
|
1455
|
-
action: "
|
|
2001
|
+
action: "remove-cache",
|
|
1456
2002
|
path: artifact.path,
|
|
1457
|
-
detail: `
|
|
2003
|
+
detail: `permanently remove proven inactive Flow ${artifact.resolvedVersion} after reversible staging; preserve the cache root and unrelated packages`
|
|
1458
2004
|
});
|
|
1459
2005
|
}
|
|
1460
2006
|
const base = {
|
|
@@ -1480,6 +2026,8 @@ async function applyFlowActivation(options) {
|
|
|
1480
2026
|
const action = {
|
|
1481
2027
|
action: "rewrite-config",
|
|
1482
2028
|
path: snapshot.descriptor.path,
|
|
2029
|
+
source: snapshot.descriptor.source,
|
|
2030
|
+
scope: snapshot.descriptor.scope,
|
|
1483
2031
|
originalAbsent: !snapshot.exists,
|
|
1484
2032
|
originalMode: snapshot.mode,
|
|
1485
2033
|
state: "pending"
|
|
@@ -1493,24 +2041,31 @@ async function applyFlowActivation(options) {
|
|
|
1493
2041
|
}
|
|
1494
2042
|
for (const wrapper of wrappers) {
|
|
1495
2043
|
actions.push({
|
|
1496
|
-
action: "
|
|
2044
|
+
action: "remove-wrapper",
|
|
1497
2045
|
path: wrapper.path,
|
|
1498
|
-
|
|
2046
|
+
source: wrapper.source,
|
|
2047
|
+
scope: wrapper.scope,
|
|
2048
|
+
resolvedVersion: wrapper.version,
|
|
2049
|
+
ownership: wrapper.ownership,
|
|
2050
|
+
stagingPath: join(wrapperRecoveryRoot(before.paths, wrapper), runId, sha256(wrapper.path).slice(0, 12), basename(wrapper.path)),
|
|
1499
2051
|
state: "pending"
|
|
1500
2052
|
});
|
|
1501
2053
|
}
|
|
1502
2054
|
for (const artifact of inactiveCache) {
|
|
1503
2055
|
actions.push({
|
|
1504
|
-
action: "
|
|
2056
|
+
action: "remove-cache",
|
|
1505
2057
|
path: artifact.path,
|
|
1506
|
-
|
|
2058
|
+
...artifact.resolvedVersion ? { resolvedVersion: artifact.resolvedVersion } : {},
|
|
2059
|
+
specifier: artifact.specifier,
|
|
2060
|
+
stagingPath: join(before.paths.cacheRecoveryRoot, runId, sha256(artifact.path).slice(0, 12), basename(artifact.path)),
|
|
1507
2061
|
state: "pending"
|
|
1508
2062
|
});
|
|
1509
2063
|
}
|
|
1510
2064
|
const journal = {
|
|
1511
|
-
format: "flow-activation-journal-
|
|
2065
|
+
format: "flow-activation-journal-v2",
|
|
1512
2066
|
runId,
|
|
1513
2067
|
createdAt: new Date().toISOString(),
|
|
2068
|
+
ownerPid: process.pid,
|
|
1514
2069
|
project: before.project,
|
|
1515
2070
|
target,
|
|
1516
2071
|
scope: options.scope,
|
|
@@ -1529,6 +2084,8 @@ async function applyFlowActivation(options) {
|
|
|
1529
2084
|
refusals: [...new Set([...base.refusals, message])]
|
|
1530
2085
|
};
|
|
1531
2086
|
}
|
|
2087
|
+
let removalCommitStarted = false;
|
|
2088
|
+
let committedAfter;
|
|
1532
2089
|
try {
|
|
1533
2090
|
for (const snapshot of changedSnapshots) {
|
|
1534
2091
|
await assertUnchangedConfig(snapshot);
|
|
@@ -1560,68 +2117,116 @@ async function applyFlowActivation(options) {
|
|
|
1560
2117
|
for (const snapshot of nonTargetConfigs) {
|
|
1561
2118
|
const entries = nextEntries.get(snapshot.descriptor.path) ?? [];
|
|
1562
2119
|
const content = updatedConfigContent(snapshot, entries);
|
|
1563
|
-
await atomicWriteConfig(snapshot, content, runId);
|
|
1564
2120
|
const action = actions.find((candidate) => candidate.action === "rewrite-config" && candidate.path === snapshot.descriptor.path);
|
|
1565
2121
|
if (action) {
|
|
1566
2122
|
action.appliedDigest = sha256(content);
|
|
1567
|
-
action.state = "complete";
|
|
1568
2123
|
}
|
|
1569
2124
|
await writeJournal(journalPath, journal);
|
|
2125
|
+
await atomicWriteConfig(snapshot, content, runId);
|
|
2126
|
+
if (action)
|
|
2127
|
+
action.state = "complete";
|
|
2128
|
+
await writeJournal(journalPath, journal);
|
|
1570
2129
|
await options.afterMutation?.(plan.find((operation) => operation.action === "rewrite-config" && operation.path === snapshot.descriptor.path));
|
|
1571
2130
|
}
|
|
1572
2131
|
for (const wrapper of wrappers) {
|
|
1573
|
-
const action = actions.find((candidate) => candidate.action === "
|
|
1574
|
-
if (!action?.
|
|
1575
|
-
throw new Error(`${wrapper.path}: missing wrapper
|
|
2132
|
+
const action = actions.find((candidate) => candidate.action === "remove-wrapper" && candidate.path === wrapper.path);
|
|
2133
|
+
if (!action?.stagingPath) {
|
|
2134
|
+
throw new Error(`${wrapper.path}: missing wrapper staging path`);
|
|
1576
2135
|
}
|
|
1577
2136
|
await verifyOwnedWrapper(wrapper);
|
|
1578
2137
|
await assertSafeMutationPath(wrapperMutationSafetyRoot(before.paths, wrapper), wrapper.path);
|
|
1579
|
-
await
|
|
2138
|
+
await assertSafeMutationPath(wrapperMutationSafetyRoot(before.paths, wrapper), action.stagingPath);
|
|
2139
|
+
await mkdir(dirname(action.stagingPath), {
|
|
1580
2140
|
recursive: true,
|
|
1581
2141
|
mode: 448
|
|
1582
2142
|
});
|
|
1583
|
-
await rename(wrapper.path, action.
|
|
2143
|
+
await rename(wrapper.path, action.stagingPath);
|
|
1584
2144
|
action.state = "complete";
|
|
1585
2145
|
await writeJournal(journalPath, journal);
|
|
1586
|
-
await options.afterMutation?.(plan.find((operation) => operation.action === "
|
|
2146
|
+
await options.afterMutation?.(plan.find((operation) => operation.action === "remove-wrapper" && operation.path === wrapper.path));
|
|
1587
2147
|
}
|
|
1588
2148
|
for (const artifact of inactiveCache) {
|
|
1589
|
-
const action = actions.find((candidate) => candidate.action === "
|
|
1590
|
-
if (!action?.
|
|
1591
|
-
throw new Error(`${artifact.path}: missing cache
|
|
2149
|
+
const action = actions.find((candidate) => candidate.action === "remove-cache" && candidate.path === artifact.path);
|
|
2150
|
+
if (!action?.stagingPath) {
|
|
2151
|
+
throw new Error(`${artifact.path}: missing cache staging path`);
|
|
1592
2152
|
}
|
|
1593
2153
|
await verifyCacheArtifact(artifact, target);
|
|
1594
2154
|
await assertSafeMutationPath(dirname(before.paths.cacheRoot), artifact.path);
|
|
1595
|
-
await
|
|
2155
|
+
await assertSafeMutationPath(dirname(before.paths.cacheRoot), action.stagingPath);
|
|
2156
|
+
await mkdir(dirname(action.stagingPath), {
|
|
1596
2157
|
recursive: true,
|
|
1597
2158
|
mode: 448
|
|
1598
2159
|
});
|
|
1599
|
-
await rename(artifact.path, action.
|
|
2160
|
+
await rename(artifact.path, action.stagingPath);
|
|
1600
2161
|
action.state = "complete";
|
|
1601
2162
|
await writeJournal(journalPath, journal);
|
|
1602
|
-
await options.afterMutation?.(plan.find((operation) => operation.action === "
|
|
2163
|
+
await options.afterMutation?.(plan.find((operation) => operation.action === "remove-cache" && operation.path === artifact.path));
|
|
1603
2164
|
}
|
|
1604
2165
|
if (targetConfig) {
|
|
1605
2166
|
const targetEntries = nextEntries.get(targetConfig.descriptor.path) ?? [];
|
|
1606
2167
|
const targetContent = updatedConfigContent(targetConfig, targetEntries);
|
|
1607
|
-
await atomicWriteConfig(targetConfig, targetContent, runId);
|
|
1608
2168
|
const targetAction = actions.find((candidate) => candidate.action === "rewrite-config" && candidate.path === targetConfig.descriptor.path);
|
|
1609
2169
|
if (targetAction) {
|
|
1610
2170
|
targetAction.appliedDigest = sha256(targetContent);
|
|
1611
|
-
targetAction.state = "complete";
|
|
1612
2171
|
}
|
|
1613
2172
|
await writeJournal(journalPath, journal);
|
|
2173
|
+
await atomicWriteConfig(targetConfig, targetContent, runId);
|
|
2174
|
+
if (targetAction)
|
|
2175
|
+
targetAction.state = "complete";
|
|
2176
|
+
await writeJournal(journalPath, journal);
|
|
1614
2177
|
await options.afterMutation?.(plan.find((operation) => operation.action === "rewrite-config" && operation.path === targetConfig.descriptor.path));
|
|
1615
2178
|
}
|
|
1616
2179
|
const after = await checkFlowActivation({
|
|
1617
2180
|
project: before.project,
|
|
1618
2181
|
target,
|
|
2182
|
+
ignoreRecoveryRunId: runId,
|
|
1619
2183
|
...options.paths ? { paths: options.paths } : {}
|
|
1620
2184
|
});
|
|
1621
2185
|
if (!after.singleVersionSatisfied) {
|
|
1622
2186
|
throw new Error(`post-apply inventory did not prove a single version: ${after.reasons.join("; ")}`);
|
|
1623
2187
|
}
|
|
2188
|
+
committedAfter = after;
|
|
2189
|
+
for (const wrapper of wrappers) {
|
|
2190
|
+
const action = actions.find((candidate) => candidate.action === "remove-wrapper" && candidate.path === wrapper.path);
|
|
2191
|
+
if (!action?.stagingPath) {
|
|
2192
|
+
throw new Error(`${wrapper.path}: missing staged wrapper at commit`);
|
|
2193
|
+
}
|
|
2194
|
+
await verifyOwnedWrapper({ ...wrapper, path: action.stagingPath });
|
|
2195
|
+
}
|
|
2196
|
+
for (const artifact of inactiveCache) {
|
|
2197
|
+
const action = actions.find((candidate) => candidate.action === "remove-cache" && candidate.path === artifact.path);
|
|
2198
|
+
if (!action?.stagingPath) {
|
|
2199
|
+
throw new Error(`${artifact.path}: missing staged cache at commit`);
|
|
2200
|
+
}
|
|
2201
|
+
await verifyCacheArtifact({ ...artifact, path: action.stagingPath }, target);
|
|
2202
|
+
}
|
|
2203
|
+
const removalActions = actions.filter((candidate) => candidate.action === "remove-wrapper" || candidate.action === "remove-cache");
|
|
2204
|
+
if (removalActions.length > 0) {
|
|
2205
|
+
journal.state = "committed";
|
|
2206
|
+
await writeJournal(journalPath, journal);
|
|
2207
|
+
removalCommitStarted = true;
|
|
2208
|
+
await options.afterRemovalCommit?.();
|
|
2209
|
+
}
|
|
2210
|
+
for (const action of removalActions) {
|
|
2211
|
+
if (!action.stagingPath) {
|
|
2212
|
+
throw new Error(`${action.path}: missing staging path at deletion`);
|
|
2213
|
+
}
|
|
2214
|
+
await rm(action.stagingPath, {
|
|
2215
|
+
recursive: action.action === "remove-cache"
|
|
2216
|
+
});
|
|
2217
|
+
if (await optionalLstat(action.stagingPath)) {
|
|
2218
|
+
throw new Error(`${action.path}: staged obsolete artifact still exists`);
|
|
2219
|
+
}
|
|
2220
|
+
action.deleted = true;
|
|
2221
|
+
await removeEmptyDirectory(dirname(action.stagingPath));
|
|
2222
|
+
await writeJournal(journalPath, journal);
|
|
2223
|
+
}
|
|
2224
|
+
for (const runDirectory of new Set(removalActions.flatMap((action) => action.stagingPath ? [dirname(dirname(action.stagingPath))] : []))) {
|
|
2225
|
+
await removeEmptyDirectory(runDirectory);
|
|
2226
|
+
}
|
|
2227
|
+
await removeEmptyDirectory(before.paths.cacheRecoveryRoot);
|
|
1624
2228
|
journal.state = "complete";
|
|
2229
|
+
delete journal.ownerPid;
|
|
1625
2230
|
await writeJournal(journalPath, journal);
|
|
1626
2231
|
return {
|
|
1627
2232
|
...base,
|
|
@@ -1631,6 +2236,29 @@ async function applyFlowActivation(options) {
|
|
|
1631
2236
|
refusals: []
|
|
1632
2237
|
};
|
|
1633
2238
|
} catch (error) {
|
|
2239
|
+
if (removalCommitStarted) {
|
|
2240
|
+
journal.state = "cleanup-failed";
|
|
2241
|
+
delete journal.ownerPid;
|
|
2242
|
+
journal.error = error instanceof Error ? error.message : String(error);
|
|
2243
|
+
try {
|
|
2244
|
+
await writeJournal(journalPath, journal);
|
|
2245
|
+
} catch {}
|
|
2246
|
+
return {
|
|
2247
|
+
...base,
|
|
2248
|
+
status: "refused",
|
|
2249
|
+
recovery: { runId, journalPath },
|
|
2250
|
+
...committedAfter ? { after: committedAfter } : {},
|
|
2251
|
+
failure: {
|
|
2252
|
+
message: journal.error,
|
|
2253
|
+
recoveryState: "cleanup-failed",
|
|
2254
|
+
guidance: [
|
|
2255
|
+
"The newest Flow activation is committed and remains authoritative; do not restore an older config or plugin source.",
|
|
2256
|
+
`Inspect ${journalPath} and permanently delete each remaining remove-wrapper or remove-cache stagingPath after verifying it still contains only the recorded obsolete Flow version.`
|
|
2257
|
+
]
|
|
2258
|
+
},
|
|
2259
|
+
refusals: [journal.error]
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
1634
2262
|
journal.state = "failed";
|
|
1635
2263
|
journal.error = error instanceof Error ? error.message : String(error);
|
|
1636
2264
|
try {
|
|
@@ -1646,6 +2274,7 @@ async function applyFlowActivation(options) {
|
|
|
1646
2274
|
});
|
|
1647
2275
|
const recoveryState = rollbackFailures.length === 0 ? "rolled-back" : "rollback-failed";
|
|
1648
2276
|
journal.state = recoveryState;
|
|
2277
|
+
delete journal.ownerPid;
|
|
1649
2278
|
if (rollbackFailures.length > 0) {
|
|
1650
2279
|
journal.error = `${journal.error ?? "apply failed"}; rollback: ${rollbackFailures.join("; ")}`;
|
|
1651
2280
|
}
|
|
@@ -1653,11 +2282,11 @@ async function applyFlowActivation(options) {
|
|
|
1653
2282
|
await writeJournal(journalPath, journal);
|
|
1654
2283
|
} catch {}
|
|
1655
2284
|
const guidance = recoveryState === "rolled-back" ? [
|
|
1656
|
-
"All completed mutations were restored from exact backups or
|
|
2285
|
+
"All completed mutations were restored from exact backups or reversible staging moves.",
|
|
1657
2286
|
`Inspect ${journalPath} and resolve the recorded failure before retrying.`
|
|
1658
2287
|
] : [
|
|
1659
2288
|
"Stop OpenCode before manual recovery.",
|
|
1660
|
-
`Inspect ${journalPath}; for rollback-failed actions, restore backupPath to path or rename
|
|
2289
|
+
`Inspect ${journalPath}; for rollback-failed actions, restore backupPath to path or rename stagingPath back to path only after verifying the destination is absent or unchanged.`,
|
|
1661
2290
|
"Do not delete the recovery directory until activation-check succeeds."
|
|
1662
2291
|
];
|
|
1663
2292
|
return {
|
|
@@ -1706,7 +2335,7 @@ var NO_FOLLOW2 = constants2.O_NOFOLLOW ?? 0;
|
|
|
1706
2335
|
var SUPPORTED_LEGACY_MAJOR = "4";
|
|
1707
2336
|
var POST_MOVE_VERIFICATION_ATTEMPTS = 4;
|
|
1708
2337
|
var POST_MOVE_VERIFICATION_RETRY_MS = 25;
|
|
1709
|
-
var
|
|
2338
|
+
var SEMVER_PATTERN2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
1710
2339
|
function configuredHome2() {
|
|
1711
2340
|
return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || homedir2();
|
|
1712
2341
|
}
|
|
@@ -1799,7 +2428,7 @@ function parseMarker(content) {
|
|
|
1799
2428
|
return { version, files };
|
|
1800
2429
|
}
|
|
1801
2430
|
function assertSupportedLegacyVersion(version) {
|
|
1802
|
-
const match =
|
|
2431
|
+
const match = SEMVER_PATTERN2.exec(version);
|
|
1803
2432
|
if (!match) {
|
|
1804
2433
|
throw new Error(`marker version '${version}' is not a valid semantic version`);
|
|
1805
2434
|
}
|
|
@@ -1983,17 +2612,19 @@ async function cleanupLegacySkills(options) {
|
|
|
1983
2612
|
function usage() {
|
|
1984
2613
|
return [
|
|
1985
2614
|
"usage:",
|
|
2615
|
+
" opencode-plugin-flow install --project <absolute-path> --scope <global|project> [--json]",
|
|
1986
2616
|
" opencode-plugin-flow activation-check --project <absolute-path> [--target <exact-version>] [--json]",
|
|
1987
2617
|
" opencode-plugin-flow activation-apply --project <absolute-path> --scope <global|project> [--target <exact-version>] [--apply] [--json]",
|
|
1988
2618
|
" opencode-plugin-flow legacy-cleanup <--dry-run|--apply> [--json]",
|
|
1989
2619
|
"",
|
|
1990
2620
|
"commands:",
|
|
1991
|
-
"
|
|
2621
|
+
" install Converge immediately to this package's exact version and remove proven older copies",
|
|
2622
|
+
" activation-check Inventory global sources, one selected project, and cache artifacts",
|
|
1992
2623
|
" activation-apply Plan a single-version activation; mutate only with --apply",
|
|
1993
2624
|
" legacy-cleanup Inspect or archive marker-proven legacy global Flow skills",
|
|
1994
2625
|
"",
|
|
1995
2626
|
"activation options:",
|
|
1996
|
-
" --project <path> Absolute project/worktree path
|
|
2627
|
+
" --project <path> Absolute project/worktree path; other project trees are not scanned",
|
|
1997
2628
|
" --scope <scope> Config that receives the one canonical exact npm pin",
|
|
1998
2629
|
" --target <version> Exact version only; defaults to this package's embedded version",
|
|
1999
2630
|
" --apply Create backups/journal and apply the activation plan",
|
|
@@ -2045,6 +2676,8 @@ function writeActivationCheck(report, json) {
|
|
|
2045
2676
|
process.stdout.write(`Flow activation check: ${report.singleVersionSatisfied ? "satisfied" : "not satisfied"}
|
|
2046
2677
|
`);
|
|
2047
2678
|
process.stdout.write(`- project: ${report.project}
|
|
2679
|
+
`);
|
|
2680
|
+
process.stdout.write(`- coverage: global sources plus the selected project; other project trees are not scanned
|
|
2048
2681
|
`);
|
|
2049
2682
|
process.stdout.write(`- target: opencode-plugin-flow@${report.target}
|
|
2050
2683
|
`);
|
|
@@ -2085,18 +2718,25 @@ function writeActivationApply(report, json) {
|
|
|
2085
2718
|
process.stdout.write(`Flow activation ${report.mode}: ${report.status}
|
|
2086
2719
|
`);
|
|
2087
2720
|
process.stdout.write(`- project: ${report.project}
|
|
2721
|
+
`);
|
|
2722
|
+
process.stdout.write(`- coverage: global sources plus the selected project; other project trees are not scanned
|
|
2088
2723
|
`);
|
|
2089
2724
|
process.stdout.write(`- canonical scope: ${report.scope}
|
|
2090
2725
|
`);
|
|
2091
2726
|
process.stdout.write(`- target: opencode-plugin-flow@${report.target}
|
|
2092
2727
|
`);
|
|
2093
|
-
for (const operation of report.plan) {
|
|
2094
|
-
process.stdout.write(`- ${operation.action}: ${operation.path}
|
|
2095
|
-
${operation.detail}
|
|
2096
|
-
`);
|
|
2097
|
-
}
|
|
2098
2728
|
for (const refusal of report.refusals) {
|
|
2099
2729
|
process.stdout.write(`- refused: ${refusal}
|
|
2730
|
+
`);
|
|
2731
|
+
}
|
|
2732
|
+
if (report.status === "refused" && report.plan.length > 0) {
|
|
2733
|
+
process.stdout.write(`- blocked plan (not executed):
|
|
2734
|
+
`);
|
|
2735
|
+
}
|
|
2736
|
+
for (const operation of report.plan) {
|
|
2737
|
+
const action = report.status === "refused" ? `would-${operation.action}` : operation.action;
|
|
2738
|
+
process.stdout.write(`- ${action}: ${operation.path}
|
|
2739
|
+
${operation.detail}
|
|
2100
2740
|
`);
|
|
2101
2741
|
}
|
|
2102
2742
|
if (report.recovery) {
|
|
@@ -2204,6 +2844,29 @@ async function runActivationApply(flags) {
|
|
|
2204
2844
|
if (report.status === "refused")
|
|
2205
2845
|
process.exitCode = 1;
|
|
2206
2846
|
}
|
|
2847
|
+
async function runInstall(flags) {
|
|
2848
|
+
const parsed = parseActivationFlags(flags);
|
|
2849
|
+
const scope = activationScope(parsed?.scope);
|
|
2850
|
+
if (!parsed || parsed.apply || parsed.target || !parsed.project && !parsed.help || !scope && !parsed.help) {
|
|
2851
|
+
process.stderr.write(`${usage()}
|
|
2852
|
+
`);
|
|
2853
|
+
process.exitCode = 2;
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
if (parsed.help) {
|
|
2857
|
+
process.stdout.write(`${usage()}
|
|
2858
|
+
`);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
const report = await applyFlowActivation({
|
|
2862
|
+
project: parsed.project,
|
|
2863
|
+
scope,
|
|
2864
|
+
apply: true
|
|
2865
|
+
});
|
|
2866
|
+
writeActivationApply(report, parsed.json);
|
|
2867
|
+
if (report.status === "refused")
|
|
2868
|
+
process.exitCode = 1;
|
|
2869
|
+
}
|
|
2207
2870
|
async function runLegacyCleanup(flags) {
|
|
2208
2871
|
const knownFlags = new Set(["--dry-run", "--apply", "--json"]);
|
|
2209
2872
|
const validFlags = flags.every((flag) => knownFlags.has(flag));
|
|
@@ -2237,6 +2900,10 @@ async function main(argv) {
|
|
|
2237
2900
|
await runActivationCheck(flags);
|
|
2238
2901
|
return;
|
|
2239
2902
|
}
|
|
2903
|
+
if (command === "install") {
|
|
2904
|
+
await runInstall(flags);
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2240
2907
|
if (command === "activation-apply") {
|
|
2241
2908
|
await runActivationApply(flags);
|
|
2242
2909
|
return;
|
|
@@ -2255,4 +2922,4 @@ main(process.argv).catch((error) => {
|
|
|
2255
2922
|
process.exitCode = 1;
|
|
2256
2923
|
});
|
|
2257
2924
|
|
|
2258
|
-
//# debugId=
|
|
2925
|
+
//# debugId=8E9D5A34AAD016F364756E2164756E21
|