opencode-plugin-flow 5.2.2 → 5.3.1
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 +50 -0
- package/README.md +92 -16
- package/dist/cli.js +1919 -58
- package/dist/cli.js.map +6 -5
- package/dist/index.js +11454 -7617
- package/dist/index.js.map +36 -23
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,1691 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/distribution/activation.ts
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import {
|
|
7
|
+
access,
|
|
8
|
+
lstat,
|
|
9
|
+
mkdir,
|
|
10
|
+
open,
|
|
11
|
+
readdir,
|
|
12
|
+
rename,
|
|
13
|
+
rm,
|
|
14
|
+
writeFile
|
|
15
|
+
} from "node:fs/promises";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import {
|
|
18
|
+
basename,
|
|
19
|
+
dirname,
|
|
20
|
+
isAbsolute,
|
|
21
|
+
join,
|
|
22
|
+
normalize,
|
|
23
|
+
relative,
|
|
24
|
+
resolve,
|
|
25
|
+
sep
|
|
26
|
+
} from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
// src/version.ts
|
|
30
|
+
import { createRequire } from "node:module";
|
|
31
|
+
function resolveFlowPluginVersion() {
|
|
32
|
+
try {
|
|
33
|
+
const require2 = createRequire(import.meta.url);
|
|
34
|
+
const manifest = require2("../package.json");
|
|
35
|
+
if (manifest.version)
|
|
36
|
+
return manifest.version;
|
|
37
|
+
} catch {}
|
|
38
|
+
return "0.0.0";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/distribution/activation.ts
|
|
42
|
+
var FLOW_PACKAGE_NAME = "opencode-plugin-flow";
|
|
43
|
+
var OWNED_WRAPPER_MARKER = "// @opencode-plugin-flow-owned-wrapper v1";
|
|
44
|
+
var NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
45
|
+
var MAX_LOCAL_PLUGIN_BYTES = 1024 * 1024;
|
|
46
|
+
var EXACT_VERSION_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-]+)*)?$/;
|
|
47
|
+
var FLOW_NPM_SPECIFIER_PATTERN = /^opencode-plugin-flow(?:@(.+))?$/;
|
|
48
|
+
var LOCAL_PLUGIN_EXTENSION_PATTERN = /\.(?:js|ts)$/i;
|
|
49
|
+
function sha256(value) {
|
|
50
|
+
return createHash("sha256").update(value).digest("hex");
|
|
51
|
+
}
|
|
52
|
+
function configuredHome(env) {
|
|
53
|
+
return env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
|
|
54
|
+
}
|
|
55
|
+
function normalizedInjectedRoot(path) {
|
|
56
|
+
return normalize(resolve(path));
|
|
57
|
+
}
|
|
58
|
+
function resolveEnvironmentPath(value, project) {
|
|
59
|
+
return normalize(isAbsolute(value) ? value : resolve(project, value));
|
|
60
|
+
}
|
|
61
|
+
function systemManagedConfigRoot(platform, env) {
|
|
62
|
+
if (platform === "darwin")
|
|
63
|
+
return "/Library/Application Support/opencode";
|
|
64
|
+
if (platform === "win32") {
|
|
65
|
+
return join(env.ProgramData?.trim() || "C:\\ProgramData", "opencode");
|
|
66
|
+
}
|
|
67
|
+
return "/etc/opencode";
|
|
68
|
+
}
|
|
69
|
+
function systemManagedPreferencePaths(platform, home) {
|
|
70
|
+
if (platform !== "darwin")
|
|
71
|
+
return [];
|
|
72
|
+
const username = basename(home) || "user";
|
|
73
|
+
return [
|
|
74
|
+
join("/Library/Managed Preferences", username, "ai.opencode.managed.plist"),
|
|
75
|
+
join("/Library/Managed Preferences", "ai.opencode.managed.plist")
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function isExactFlowVersion(value) {
|
|
79
|
+
return EXACT_VERSION_PATTERN.test(value);
|
|
80
|
+
}
|
|
81
|
+
function resolveActivationTarget(target) {
|
|
82
|
+
const resolved = target ?? resolveFlowPluginVersion();
|
|
83
|
+
if (!isExactFlowVersion(resolved)) {
|
|
84
|
+
throw new Error(`Flow activation target '${resolved}' must be an exact semantic version; tags and ranges are not resolved.`);
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
function resolveActivationPaths(project, options = {}) {
|
|
89
|
+
if (!isAbsolute(project)) {
|
|
90
|
+
throw new Error(`Flow activation project path must be absolute: ${project}`);
|
|
91
|
+
}
|
|
92
|
+
const env = options.env ?? process.env;
|
|
93
|
+
const home = normalizedInjectedRoot(options.home ?? configuredHome(env));
|
|
94
|
+
const configRoot = normalizedInjectedRoot(options.configRoot ?? (options.home ? join(home, ".config", "opencode") : env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, "opencode") : join(home, ".config", "opencode")));
|
|
95
|
+
const cacheRoot = normalizedInjectedRoot(options.cacheRoot ?? (options.home ? join(home, ".cache", "opencode") : env.XDG_CACHE_HOME ? join(env.XDG_CACHE_HOME, "opencode") : join(home, ".cache", "opencode")));
|
|
96
|
+
const absoluteProject = normalizedInjectedRoot(project);
|
|
97
|
+
const platform = options.platform ?? process.platform;
|
|
98
|
+
const customConfigFile = env.OPENCODE_CONFIG?.trim() ? resolveEnvironmentPath(env.OPENCODE_CONFIG.trim(), absoluteProject) : null;
|
|
99
|
+
const customConfigDirectory = env.OPENCODE_CONFIG_DIR?.trim() ? resolveEnvironmentPath(env.OPENCODE_CONFIG_DIR.trim(), absoluteProject) : null;
|
|
100
|
+
const managedConfigRoot = normalizedInjectedRoot(options.managedConfigRoot ?? env.OPENCODE_TEST_MANAGED_CONFIG_DIR ?? systemManagedConfigRoot(platform, env));
|
|
101
|
+
const globalConfigFiles = [
|
|
102
|
+
join(configRoot, "config.json"),
|
|
103
|
+
join(configRoot, "opencode.json"),
|
|
104
|
+
join(configRoot, "opencode.jsonc")
|
|
105
|
+
];
|
|
106
|
+
const projectConfigFiles = [
|
|
107
|
+
join(absoluteProject, "opencode.jsonc"),
|
|
108
|
+
join(absoluteProject, "opencode.json")
|
|
109
|
+
];
|
|
110
|
+
const projectDirectoryRoot = join(absoluteProject, ".opencode");
|
|
111
|
+
const projectDirectoryConfigFiles = [
|
|
112
|
+
join(projectDirectoryRoot, "opencode.json"),
|
|
113
|
+
join(projectDirectoryRoot, "opencode.jsonc")
|
|
114
|
+
];
|
|
115
|
+
const customDirectoryConfigFiles = customConfigDirectory ? [
|
|
116
|
+
join(customConfigDirectory, "opencode.json"),
|
|
117
|
+
join(customConfigDirectory, "opencode.jsonc")
|
|
118
|
+
] : [];
|
|
119
|
+
const managedConfigFiles = [
|
|
120
|
+
join(managedConfigRoot, "opencode.json"),
|
|
121
|
+
join(managedConfigRoot, "opencode.jsonc")
|
|
122
|
+
];
|
|
123
|
+
const pluginDirectories = [
|
|
124
|
+
...["plugin", "plugins"].map((name) => ({
|
|
125
|
+
source: "global-plugin-directory",
|
|
126
|
+
scope: "global",
|
|
127
|
+
path: join(configRoot, name),
|
|
128
|
+
safetyRoot: dirname(configRoot)
|
|
129
|
+
})),
|
|
130
|
+
...["plugin", "plugins"].map((name) => ({
|
|
131
|
+
source: "home-plugin-directory",
|
|
132
|
+
scope: "global",
|
|
133
|
+
path: join(home, ".opencode", name),
|
|
134
|
+
safetyRoot: home
|
|
135
|
+
})),
|
|
136
|
+
...["plugin", "plugins"].map((name) => ({
|
|
137
|
+
source: "project-plugin-directory",
|
|
138
|
+
scope: "project",
|
|
139
|
+
path: join(projectDirectoryRoot, name),
|
|
140
|
+
safetyRoot: absoluteProject
|
|
141
|
+
})),
|
|
142
|
+
...customConfigDirectory ? ["plugin", "plugins"].map((name) => ({
|
|
143
|
+
source: "custom-plugin-directory",
|
|
144
|
+
scope: "custom",
|
|
145
|
+
path: join(customConfigDirectory, name),
|
|
146
|
+
safetyRoot: dirname(customConfigDirectory)
|
|
147
|
+
})) : []
|
|
148
|
+
];
|
|
149
|
+
return {
|
|
150
|
+
project: absoluteProject,
|
|
151
|
+
home,
|
|
152
|
+
configRoot,
|
|
153
|
+
cacheRoot,
|
|
154
|
+
globalConfig: join(configRoot, "opencode.json"),
|
|
155
|
+
projectConfig: join(absoluteProject, "opencode.json"),
|
|
156
|
+
globalPluginDirectory: join(configRoot, "plugins"),
|
|
157
|
+
projectPluginDirectory: join(absoluteProject, ".opencode", "plugins"),
|
|
158
|
+
globalConfigFiles,
|
|
159
|
+
projectConfigFiles,
|
|
160
|
+
projectDirectoryConfigFiles,
|
|
161
|
+
customConfigFile,
|
|
162
|
+
customConfigDirectory,
|
|
163
|
+
customDirectoryConfigFiles,
|
|
164
|
+
managedConfigRoot,
|
|
165
|
+
managedConfigFiles,
|
|
166
|
+
pluginDirectories,
|
|
167
|
+
managedPreferencePaths: options.managedPreferencePaths?.map(normalizedInjectedRoot) ?? systemManagedPreferencePaths(platform, home),
|
|
168
|
+
hasInlineConfig: Boolean(env.OPENCODE_CONFIG_CONTENT?.trim()),
|
|
169
|
+
packageCacheRoot: join(cacheRoot, "packages"),
|
|
170
|
+
journalRoot: join(configRoot, "flow-activation-recovery"),
|
|
171
|
+
globalWrapperRecoveryRoot: join(configRoot, "flow-activation-recovery"),
|
|
172
|
+
projectWrapperRecoveryRoot: join(absoluteProject, ".opencode", "flow-activation-recovery"),
|
|
173
|
+
cacheRecoveryRoot: join(cacheRoot, "flow-activation-recovery")
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function optionalLstat(path) {
|
|
177
|
+
try {
|
|
178
|
+
return await lstat(path);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.code === "ENOENT")
|
|
181
|
+
return null;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function symlinkInPathRange(safetyRoot, target) {
|
|
186
|
+
const root = normalize(safetyRoot);
|
|
187
|
+
const destination = normalize(target);
|
|
188
|
+
const pathFromRoot = relative(root, destination);
|
|
189
|
+
if (pathFromRoot === ".." || pathFromRoot.startsWith(`..${sep}`) || isAbsolute(pathFromRoot)) {
|
|
190
|
+
throw new Error(`${destination} is outside its mutation safety root ${root}`);
|
|
191
|
+
}
|
|
192
|
+
let current = root;
|
|
193
|
+
const parts = pathFromRoot ? pathFromRoot.split(sep) : [];
|
|
194
|
+
for (let index = -1;index < parts.length; index += 1) {
|
|
195
|
+
if (index >= 0) {
|
|
196
|
+
const part = parts[index];
|
|
197
|
+
if (!part)
|
|
198
|
+
continue;
|
|
199
|
+
current = join(current, part);
|
|
200
|
+
}
|
|
201
|
+
const metadata = await optionalLstat(current);
|
|
202
|
+
if (!metadata)
|
|
203
|
+
return null;
|
|
204
|
+
if (metadata.isSymbolicLink())
|
|
205
|
+
return current;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
async function assertSafeMutationPath(safetyRoot, target) {
|
|
210
|
+
const symlink = await symlinkInPathRange(safetyRoot, target);
|
|
211
|
+
if (symlink) {
|
|
212
|
+
throw new Error(`${target}: mutation refused because ancestor ${symlink} is a symbolic link`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function readRegularFileWithoutFollowing(path, maximumBytes) {
|
|
216
|
+
const pathMetadata = await lstat(path);
|
|
217
|
+
if (pathMetadata.isSymbolicLink()) {
|
|
218
|
+
throw new Error("symbolic link refused");
|
|
219
|
+
}
|
|
220
|
+
if (!pathMetadata.isFile())
|
|
221
|
+
throw new Error("not a regular file");
|
|
222
|
+
if (maximumBytes !== undefined && pathMetadata.size > maximumBytes) {
|
|
223
|
+
throw new Error(`file exceeds ${maximumBytes} bytes`);
|
|
224
|
+
}
|
|
225
|
+
const handle = await open(path, constants.O_RDONLY | NO_FOLLOW);
|
|
226
|
+
try {
|
|
227
|
+
const metadata = await handle.stat();
|
|
228
|
+
if (!metadata.isFile())
|
|
229
|
+
throw new Error("not a regular file");
|
|
230
|
+
if (metadata.dev !== pathMetadata.dev || metadata.ino !== pathMetadata.ino) {
|
|
231
|
+
throw new Error("file changed while activation was running");
|
|
232
|
+
}
|
|
233
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.code === "ELOOP") {
|
|
236
|
+
throw new Error("symbolic link refused");
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
} finally {
|
|
240
|
+
await handle.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function configDescriptors(paths) {
|
|
244
|
+
const descriptors = [
|
|
245
|
+
...paths.globalConfigFiles.map((path) => ({
|
|
246
|
+
source: "global-config",
|
|
247
|
+
scope: "global",
|
|
248
|
+
path,
|
|
249
|
+
mutable: true,
|
|
250
|
+
safetyRoot: dirname(paths.configRoot),
|
|
251
|
+
manualRemediation: `edit ${path} manually`
|
|
252
|
+
})),
|
|
253
|
+
...paths.customConfigFile ? [
|
|
254
|
+
{
|
|
255
|
+
source: "custom-config",
|
|
256
|
+
scope: "custom",
|
|
257
|
+
path: paths.customConfigFile,
|
|
258
|
+
mutable: true,
|
|
259
|
+
safetyRoot: dirname(paths.customConfigFile),
|
|
260
|
+
manualRemediation: `edit OPENCODE_CONFIG file ${paths.customConfigFile} manually`
|
|
261
|
+
}
|
|
262
|
+
] : [],
|
|
263
|
+
...paths.projectConfigFiles.map((path) => ({
|
|
264
|
+
source: "project-config",
|
|
265
|
+
scope: "project",
|
|
266
|
+
path,
|
|
267
|
+
mutable: true,
|
|
268
|
+
safetyRoot: paths.project,
|
|
269
|
+
manualRemediation: `edit ${path} manually`
|
|
270
|
+
})),
|
|
271
|
+
...paths.projectDirectoryConfigFiles.map((path) => ({
|
|
272
|
+
source: "project-directory-config",
|
|
273
|
+
scope: "project",
|
|
274
|
+
path,
|
|
275
|
+
mutable: true,
|
|
276
|
+
safetyRoot: paths.project,
|
|
277
|
+
manualRemediation: `edit ${path} manually`
|
|
278
|
+
})),
|
|
279
|
+
...paths.customDirectoryConfigFiles.map((path) => ({
|
|
280
|
+
source: "custom-directory-config",
|
|
281
|
+
scope: "custom",
|
|
282
|
+
path,
|
|
283
|
+
mutable: true,
|
|
284
|
+
safetyRoot: dirname(paths.customConfigDirectory),
|
|
285
|
+
manualRemediation: `edit OPENCODE_CONFIG_DIR file ${path} manually`
|
|
286
|
+
})),
|
|
287
|
+
...paths.managedConfigFiles.map((path) => ({
|
|
288
|
+
source: "managed-config",
|
|
289
|
+
scope: "managed",
|
|
290
|
+
path,
|
|
291
|
+
mutable: false,
|
|
292
|
+
safetyRoot: dirname(paths.managedConfigRoot),
|
|
293
|
+
manualRemediation: `ask the OpenCode administrator to remove the Flow entry from ${path}`
|
|
294
|
+
}))
|
|
295
|
+
];
|
|
296
|
+
const unique = new Map;
|
|
297
|
+
for (const descriptor of descriptors) {
|
|
298
|
+
const previous = unique.get(descriptor.path);
|
|
299
|
+
if (!previous || !descriptor.mutable && previous.mutable) {
|
|
300
|
+
unique.set(descriptor.path, descriptor);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return [...unique.values()];
|
|
304
|
+
}
|
|
305
|
+
function pluginDirectoryDescriptors(paths) {
|
|
306
|
+
const unique = new Map;
|
|
307
|
+
for (const descriptor of paths.pluginDirectories) {
|
|
308
|
+
if (!unique.has(descriptor.path))
|
|
309
|
+
unique.set(descriptor.path, descriptor);
|
|
310
|
+
}
|
|
311
|
+
return [...unique.values()];
|
|
312
|
+
}
|
|
313
|
+
function parseFlowNpmSpecifier(specifier) {
|
|
314
|
+
const match = FLOW_NPM_SPECIFIER_PATTERN.exec(specifier);
|
|
315
|
+
if (!match)
|
|
316
|
+
return { isFlow: false, version: null };
|
|
317
|
+
const candidate = match[1];
|
|
318
|
+
return {
|
|
319
|
+
isFlow: true,
|
|
320
|
+
version: candidate && isExactFlowVersion(candidate) ? candidate : null
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function pluginSpecifier(entry) {
|
|
324
|
+
return Array.isArray(entry) ? entry[0] : entry;
|
|
325
|
+
}
|
|
326
|
+
function isPluginOptions(value) {
|
|
327
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
328
|
+
}
|
|
329
|
+
function parsePluginSpecs(value) {
|
|
330
|
+
if (value === undefined)
|
|
331
|
+
return [];
|
|
332
|
+
if (!Array.isArray(value)) {
|
|
333
|
+
throw new Error("config plugin must be an array");
|
|
334
|
+
}
|
|
335
|
+
return value.map((entry, index) => {
|
|
336
|
+
if (typeof entry === "string")
|
|
337
|
+
return entry;
|
|
338
|
+
if (Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && isPluginOptions(entry[1])) {
|
|
339
|
+
return [entry[0], entry[1]];
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`config plugin[${index}] must be a string or [specifier, options] tuple`);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function stripJsoncComments(content) {
|
|
345
|
+
let output = "";
|
|
346
|
+
let inString = false;
|
|
347
|
+
let escaped = false;
|
|
348
|
+
for (let index = 0;index < content.length; index += 1) {
|
|
349
|
+
const character = content[index];
|
|
350
|
+
if (!character)
|
|
351
|
+
continue;
|
|
352
|
+
if (inString) {
|
|
353
|
+
output += character;
|
|
354
|
+
if (escaped)
|
|
355
|
+
escaped = false;
|
|
356
|
+
else if (character === "\\")
|
|
357
|
+
escaped = true;
|
|
358
|
+
else if (character === '"')
|
|
359
|
+
inString = false;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (character === '"') {
|
|
363
|
+
inString = true;
|
|
364
|
+
output += character;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const next = content[index + 1];
|
|
368
|
+
if (character === "/" && next === "/") {
|
|
369
|
+
output += " ";
|
|
370
|
+
index += 2;
|
|
371
|
+
while (index < content.length) {
|
|
372
|
+
const commentCharacter = content[index];
|
|
373
|
+
if (commentCharacter === `
|
|
374
|
+
` || commentCharacter === "\r") {
|
|
375
|
+
index -= 1;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
output += " ";
|
|
379
|
+
index += 1;
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (character === "/" && next === "*") {
|
|
384
|
+
output += " ";
|
|
385
|
+
index += 2;
|
|
386
|
+
let closed = false;
|
|
387
|
+
while (index < content.length) {
|
|
388
|
+
const commentCharacter = content[index];
|
|
389
|
+
const following = content[index + 1];
|
|
390
|
+
if (commentCharacter === "*" && following === "/") {
|
|
391
|
+
output += " ";
|
|
392
|
+
index += 1;
|
|
393
|
+
closed = true;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
output += commentCharacter === `
|
|
397
|
+
` || commentCharacter === "\r" ? commentCharacter : " ";
|
|
398
|
+
index += 1;
|
|
399
|
+
}
|
|
400
|
+
if (!closed)
|
|
401
|
+
throw new Error("unterminated block comment");
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
output += character;
|
|
405
|
+
}
|
|
406
|
+
if (inString)
|
|
407
|
+
throw new Error("unterminated JSON string");
|
|
408
|
+
return output;
|
|
409
|
+
}
|
|
410
|
+
function removeJsoncTrailingCommas(content) {
|
|
411
|
+
let output = "";
|
|
412
|
+
let inString = false;
|
|
413
|
+
let escaped = false;
|
|
414
|
+
for (let index = 0;index < content.length; index += 1) {
|
|
415
|
+
const character = content[index];
|
|
416
|
+
if (!character)
|
|
417
|
+
continue;
|
|
418
|
+
if (inString) {
|
|
419
|
+
output += character;
|
|
420
|
+
if (escaped)
|
|
421
|
+
escaped = false;
|
|
422
|
+
else if (character === "\\")
|
|
423
|
+
escaped = true;
|
|
424
|
+
else if (character === '"')
|
|
425
|
+
inString = false;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (character === '"') {
|
|
429
|
+
inString = true;
|
|
430
|
+
output += character;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (character === ",") {
|
|
434
|
+
let lookahead = index + 1;
|
|
435
|
+
while (/\s/.test(content[lookahead] ?? ""))
|
|
436
|
+
lookahead += 1;
|
|
437
|
+
if (content[lookahead] === "}" || content[lookahead] === "]") {
|
|
438
|
+
output += " ";
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
output += character;
|
|
443
|
+
}
|
|
444
|
+
return output;
|
|
445
|
+
}
|
|
446
|
+
function parseConfigContent(content) {
|
|
447
|
+
let parsed;
|
|
448
|
+
let format = "strict-json";
|
|
449
|
+
try {
|
|
450
|
+
parsed = JSON.parse(content);
|
|
451
|
+
} catch {
|
|
452
|
+
format = "jsonc";
|
|
453
|
+
parsed = JSON.parse(removeJsoncTrailingCommas(stripJsoncComments(content)));
|
|
454
|
+
}
|
|
455
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
456
|
+
throw new Error("config root must be a JSON object");
|
|
457
|
+
}
|
|
458
|
+
const value = parsed;
|
|
459
|
+
return { value, plugin: parsePluginSpecs(value.plugin), format };
|
|
460
|
+
}
|
|
461
|
+
function looksLikeFlowPath(path) {
|
|
462
|
+
const name = basename(path);
|
|
463
|
+
return /opencode-plugin-flow/i.test(path) || /(?:^|[-_.])flow(?:[-_.].*)?(?:plugin|wrapper)|(?:plugin|wrapper).*flow/i.test(name);
|
|
464
|
+
}
|
|
465
|
+
function parseOwnedWrapper(content) {
|
|
466
|
+
if (!content.startsWith(OWNED_WRAPPER_MARKER)) {
|
|
467
|
+
return { kind: "not-owned" };
|
|
468
|
+
}
|
|
469
|
+
const firstNewline = content.indexOf(`
|
|
470
|
+
`);
|
|
471
|
+
const secondNewline = content.indexOf(`
|
|
472
|
+
`, firstNewline + 1);
|
|
473
|
+
const thirdNewline = content.indexOf(`
|
|
474
|
+
`, secondNewline + 1);
|
|
475
|
+
if (firstNewline < 0 || secondNewline < 0 || thirdNewline < 0) {
|
|
476
|
+
return { kind: "invalid", reason: "owned wrapper marker is incomplete" };
|
|
477
|
+
}
|
|
478
|
+
const versionLine = content.slice(firstNewline + 1, secondNewline);
|
|
479
|
+
const hashLine = content.slice(secondNewline + 1, thirdNewline);
|
|
480
|
+
const version = /^\/\/ version=(.+)$/.exec(versionLine)?.[1];
|
|
481
|
+
const expectedHash = /^\/\/ body-sha256=([a-f0-9]{64})$/.exec(hashLine)?.[1];
|
|
482
|
+
if (!version || !isExactFlowVersion(version)) {
|
|
483
|
+
return { kind: "invalid", reason: "owned wrapper version is invalid" };
|
|
484
|
+
}
|
|
485
|
+
if (!expectedHash) {
|
|
486
|
+
return { kind: "invalid", reason: "owned wrapper hash is invalid" };
|
|
487
|
+
}
|
|
488
|
+
const body = content.slice(thirdNewline + 1);
|
|
489
|
+
if (sha256(body) !== expectedHash) {
|
|
490
|
+
return { kind: "invalid", reason: "owned wrapper was edited" };
|
|
491
|
+
}
|
|
492
|
+
if (!body.includes(`${FLOW_PACKAGE_NAME}@${version}`)) {
|
|
493
|
+
return {
|
|
494
|
+
kind: "invalid",
|
|
495
|
+
reason: "owned wrapper body does not reference its declared Flow version"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return { kind: "owned", version };
|
|
499
|
+
}
|
|
500
|
+
function hintedFlowVersion(content) {
|
|
501
|
+
const candidate = new RegExp(`${FLOW_PACKAGE_NAME.replaceAll("-", "\\-")}@([^/\\s"']+)`).exec(content)?.[1];
|
|
502
|
+
return candidate && isExactFlowVersion(candidate) ? candidate : null;
|
|
503
|
+
}
|
|
504
|
+
async function classifyLocalPlugin(path, source, scope, target, specifier) {
|
|
505
|
+
const metadata = await optionalLstat(path);
|
|
506
|
+
if (!metadata) {
|
|
507
|
+
if (!looksLikeFlowPath(specifier))
|
|
508
|
+
return null;
|
|
509
|
+
return {
|
|
510
|
+
source,
|
|
511
|
+
scope,
|
|
512
|
+
path,
|
|
513
|
+
specifier,
|
|
514
|
+
resolvedVersion: null,
|
|
515
|
+
ownership: "unknown-flow-like",
|
|
516
|
+
status: "refused",
|
|
517
|
+
reason: "referenced local plugin is absent"
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (metadata.isSymbolicLink()) {
|
|
521
|
+
if (!looksLikeFlowPath(specifier) && !looksLikeFlowPath(path))
|
|
522
|
+
return null;
|
|
523
|
+
return {
|
|
524
|
+
source,
|
|
525
|
+
scope,
|
|
526
|
+
path,
|
|
527
|
+
specifier,
|
|
528
|
+
resolvedVersion: null,
|
|
529
|
+
ownership: "unknown-flow-like",
|
|
530
|
+
status: "refused",
|
|
531
|
+
reason: "symbolic link refused"
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (!metadata.isFile()) {
|
|
535
|
+
if (!looksLikeFlowPath(specifier) && !looksLikeFlowPath(path))
|
|
536
|
+
return null;
|
|
537
|
+
return {
|
|
538
|
+
source,
|
|
539
|
+
scope,
|
|
540
|
+
path,
|
|
541
|
+
specifier,
|
|
542
|
+
resolvedVersion: null,
|
|
543
|
+
ownership: "unknown-flow-like",
|
|
544
|
+
status: "refused",
|
|
545
|
+
reason: "local plugin is not a regular file"
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
let content;
|
|
549
|
+
try {
|
|
550
|
+
content = await readRegularFileWithoutFollowing(path, MAX_LOCAL_PLUGIN_BYTES);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (!looksLikeFlowPath(specifier) && !looksLikeFlowPath(path))
|
|
553
|
+
return null;
|
|
554
|
+
return {
|
|
555
|
+
source,
|
|
556
|
+
scope,
|
|
557
|
+
path,
|
|
558
|
+
specifier,
|
|
559
|
+
resolvedVersion: null,
|
|
560
|
+
ownership: "unknown-flow-like",
|
|
561
|
+
status: "refused",
|
|
562
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const owned = parseOwnedWrapper(content);
|
|
566
|
+
if (owned.kind === "owned") {
|
|
567
|
+
return {
|
|
568
|
+
source,
|
|
569
|
+
scope,
|
|
570
|
+
path,
|
|
571
|
+
specifier,
|
|
572
|
+
resolvedVersion: owned.version,
|
|
573
|
+
ownership: "marker-owned-wrapper",
|
|
574
|
+
status: "conflict",
|
|
575
|
+
reason: owned.version === target ? "local wrapper duplicates the canonical npm activation source" : "local wrapper activates another Flow version"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const flowLike = looksLikeFlowPath(specifier) || looksLikeFlowPath(path) || content.includes(FLOW_PACKAGE_NAME) || content.includes(OWNED_WRAPPER_MARKER);
|
|
579
|
+
if (!flowLike)
|
|
580
|
+
return null;
|
|
581
|
+
return {
|
|
582
|
+
source,
|
|
583
|
+
scope,
|
|
584
|
+
path,
|
|
585
|
+
specifier,
|
|
586
|
+
resolvedVersion: hintedFlowVersion(content),
|
|
587
|
+
ownership: "unknown-flow-like",
|
|
588
|
+
status: "refused",
|
|
589
|
+
reason: owned.kind === "invalid" ? owned.reason : "Flow-like local plugin has no verifiable ownership marker"
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function relativeLocalSpecifier(configPath, specifier) {
|
|
593
|
+
if (specifier.startsWith("file:"))
|
|
594
|
+
return fileURLToPath(specifier);
|
|
595
|
+
if (isAbsolute(specifier))
|
|
596
|
+
return normalize(specifier);
|
|
597
|
+
return resolve(dirname(configPath), specifier);
|
|
598
|
+
}
|
|
599
|
+
function isLocalPluginSpecifier(specifier) {
|
|
600
|
+
return specifier.startsWith(".") || specifier.startsWith("file:") || isAbsolute(specifier);
|
|
601
|
+
}
|
|
602
|
+
async function inspectConfig(descriptor, target) {
|
|
603
|
+
const records = [];
|
|
604
|
+
const issues = [];
|
|
605
|
+
const symlinkAncestor = await symlinkInPathRange(descriptor.safetyRoot, descriptor.path);
|
|
606
|
+
if (symlinkAncestor) {
|
|
607
|
+
issues.push({
|
|
608
|
+
source: descriptor.source,
|
|
609
|
+
path: descriptor.path,
|
|
610
|
+
code: "unsafe-symlink",
|
|
611
|
+
message: `config ancestor ${symlinkAncestor} is a symbolic link`
|
|
612
|
+
});
|
|
613
|
+
return { records, issues };
|
|
614
|
+
}
|
|
615
|
+
const metadata = await optionalLstat(descriptor.path);
|
|
616
|
+
if (!metadata)
|
|
617
|
+
return { records, issues };
|
|
618
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
619
|
+
issues.push({
|
|
620
|
+
source: descriptor.source,
|
|
621
|
+
path: descriptor.path,
|
|
622
|
+
code: "invalid-config",
|
|
623
|
+
message: metadata.isSymbolicLink() ? "config symbolic link refused" : "config is not a regular file"
|
|
624
|
+
});
|
|
625
|
+
return { records, issues };
|
|
626
|
+
}
|
|
627
|
+
let content;
|
|
628
|
+
try {
|
|
629
|
+
content = await readRegularFileWithoutFollowing(descriptor.path);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
issues.push({
|
|
632
|
+
source: descriptor.source,
|
|
633
|
+
path: descriptor.path,
|
|
634
|
+
code: "invalid-config",
|
|
635
|
+
message: error instanceof Error ? error.message : String(error)
|
|
636
|
+
});
|
|
637
|
+
return { records, issues };
|
|
638
|
+
}
|
|
639
|
+
let parsed;
|
|
640
|
+
try {
|
|
641
|
+
parsed = parseConfigContent(content);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
issues.push({
|
|
644
|
+
source: descriptor.source,
|
|
645
|
+
path: descriptor.path,
|
|
646
|
+
code: "invalid-config",
|
|
647
|
+
message: `config cannot be conservatively parsed as JSON/JSONC: ${error instanceof Error ? error.message : String(error)}`
|
|
648
|
+
});
|
|
649
|
+
return { records, issues };
|
|
650
|
+
}
|
|
651
|
+
for (const entry of parsed.plugin) {
|
|
652
|
+
const specifier = pluginSpecifier(entry);
|
|
653
|
+
const npm = parseFlowNpmSpecifier(specifier);
|
|
654
|
+
if (npm.isFlow) {
|
|
655
|
+
const record = {
|
|
656
|
+
source: descriptor.source,
|
|
657
|
+
scope: descriptor.scope,
|
|
658
|
+
path: descriptor.path,
|
|
659
|
+
specifier,
|
|
660
|
+
resolvedVersion: npm.version,
|
|
661
|
+
ownership: "flow-npm",
|
|
662
|
+
status: npm.version === target ? "target" : "conflict"
|
|
663
|
+
};
|
|
664
|
+
if (npm.version === null) {
|
|
665
|
+
record.reason = "Flow npm activation is not pinned to an exact version";
|
|
666
|
+
} else if (npm.version !== target) {
|
|
667
|
+
record.reason = "Flow npm activation targets another version";
|
|
668
|
+
}
|
|
669
|
+
records.push(record);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (!looksLikeFlowPath(specifier) && !isLocalPluginSpecifier(specifier)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
let localPath;
|
|
676
|
+
try {
|
|
677
|
+
localPath = relativeLocalSpecifier(descriptor.path, specifier);
|
|
678
|
+
} catch {
|
|
679
|
+
if (!looksLikeFlowPath(specifier))
|
|
680
|
+
continue;
|
|
681
|
+
records.push({
|
|
682
|
+
source: descriptor.source,
|
|
683
|
+
scope: descriptor.scope,
|
|
684
|
+
path: specifier,
|
|
685
|
+
specifier,
|
|
686
|
+
resolvedVersion: null,
|
|
687
|
+
ownership: "unknown-flow-like",
|
|
688
|
+
status: "refused",
|
|
689
|
+
reason: "local Flow-like plugin specifier is invalid"
|
|
690
|
+
});
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const local = await classifyLocalPlugin(localPath, descriptor.source, descriptor.scope, target, specifier);
|
|
694
|
+
if (local)
|
|
695
|
+
records.push(local);
|
|
696
|
+
}
|
|
697
|
+
return { records, issues };
|
|
698
|
+
}
|
|
699
|
+
async function inspectInlineConfig(content, project, target) {
|
|
700
|
+
const records = [];
|
|
701
|
+
const issues = [];
|
|
702
|
+
let parsed;
|
|
703
|
+
try {
|
|
704
|
+
parsed = parseConfigContent(content);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
issues.push({
|
|
707
|
+
source: "inline-config",
|
|
708
|
+
path: "env:OPENCODE_CONFIG_CONTENT",
|
|
709
|
+
code: "invalid-config",
|
|
710
|
+
message: `inline config cannot be conservatively parsed as JSON/JSONC: ${error instanceof Error ? error.message : String(error)}`
|
|
711
|
+
});
|
|
712
|
+
return { records, issues };
|
|
713
|
+
}
|
|
714
|
+
const virtualConfigPath = join(project, "opencode.inline.json");
|
|
715
|
+
for (const entry of parsed.plugin) {
|
|
716
|
+
const specifier = pluginSpecifier(entry);
|
|
717
|
+
const npm = parseFlowNpmSpecifier(specifier);
|
|
718
|
+
if (npm.isFlow) {
|
|
719
|
+
const record = {
|
|
720
|
+
source: "inline-config",
|
|
721
|
+
scope: "inline",
|
|
722
|
+
path: "env:OPENCODE_CONFIG_CONTENT",
|
|
723
|
+
specifier,
|
|
724
|
+
resolvedVersion: npm.version,
|
|
725
|
+
ownership: "flow-npm",
|
|
726
|
+
status: npm.version === target ? "target" : "conflict"
|
|
727
|
+
};
|
|
728
|
+
if (npm.version === null) {
|
|
729
|
+
record.reason = "Flow npm activation is not pinned to an exact version";
|
|
730
|
+
} else if (npm.version !== target) {
|
|
731
|
+
record.reason = "Flow npm activation targets another version";
|
|
732
|
+
}
|
|
733
|
+
records.push(record);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (!looksLikeFlowPath(specifier) && !isLocalPluginSpecifier(specifier)) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
let localPath;
|
|
740
|
+
try {
|
|
741
|
+
localPath = relativeLocalSpecifier(virtualConfigPath, specifier);
|
|
742
|
+
} catch {
|
|
743
|
+
if (!looksLikeFlowPath(specifier))
|
|
744
|
+
continue;
|
|
745
|
+
records.push({
|
|
746
|
+
source: "inline-config",
|
|
747
|
+
scope: "inline",
|
|
748
|
+
path: "env:OPENCODE_CONFIG_CONTENT",
|
|
749
|
+
specifier,
|
|
750
|
+
resolvedVersion: null,
|
|
751
|
+
ownership: "unknown-flow-like",
|
|
752
|
+
status: "refused",
|
|
753
|
+
reason: "inline local Flow-like plugin specifier is invalid"
|
|
754
|
+
});
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const local = await classifyLocalPlugin(localPath, "inline-config", "inline", target, specifier);
|
|
758
|
+
if (local)
|
|
759
|
+
records.push(local);
|
|
760
|
+
}
|
|
761
|
+
return { records, issues };
|
|
762
|
+
}
|
|
763
|
+
async function inspectPluginDirectory(descriptor, target) {
|
|
764
|
+
const records = [];
|
|
765
|
+
const issues = [];
|
|
766
|
+
const symlinkAncestor = await symlinkInPathRange(descriptor.safetyRoot, descriptor.path);
|
|
767
|
+
if (symlinkAncestor) {
|
|
768
|
+
issues.push({
|
|
769
|
+
source: descriptor.source,
|
|
770
|
+
path: descriptor.path,
|
|
771
|
+
code: "unsafe-symlink",
|
|
772
|
+
message: `plugin directory ancestor ${symlinkAncestor} is a symbolic link`
|
|
773
|
+
});
|
|
774
|
+
return { records, issues };
|
|
775
|
+
}
|
|
776
|
+
const metadata = await optionalLstat(descriptor.path);
|
|
777
|
+
if (!metadata)
|
|
778
|
+
return { records, issues };
|
|
779
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
780
|
+
issues.push({
|
|
781
|
+
source: descriptor.source,
|
|
782
|
+
path: descriptor.path,
|
|
783
|
+
code: "invalid-plugin-directory",
|
|
784
|
+
message: metadata.isSymbolicLink() ? "plugin directory symbolic link refused" : "plugin directory is not a real directory"
|
|
785
|
+
});
|
|
786
|
+
return { records, issues };
|
|
787
|
+
}
|
|
788
|
+
const entries = await readdir(descriptor.path, { withFileTypes: true });
|
|
789
|
+
for (const entry of entries) {
|
|
790
|
+
if (!LOCAL_PLUGIN_EXTENSION_PATTERN.test(entry.name))
|
|
791
|
+
continue;
|
|
792
|
+
const path = join(descriptor.path, entry.name);
|
|
793
|
+
const record = await classifyLocalPlugin(path, descriptor.source, descriptor.scope, target, path);
|
|
794
|
+
if (record)
|
|
795
|
+
records.push(record);
|
|
796
|
+
}
|
|
797
|
+
return { records, issues };
|
|
798
|
+
}
|
|
799
|
+
async function inspectCacheArtifact(path, specifier, target) {
|
|
800
|
+
const metadata = await optionalLstat(path);
|
|
801
|
+
if (!metadata?.isDirectory() || metadata.isSymbolicLink()) {
|
|
802
|
+
return {
|
|
803
|
+
path,
|
|
804
|
+
specifier,
|
|
805
|
+
resolvedVersion: null,
|
|
806
|
+
status: "ambiguous",
|
|
807
|
+
reason: "cache artifact is not a real directory"
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const nodeModulesPath = join(path, "node_modules");
|
|
811
|
+
const packagePath = join(nodeModulesPath, FLOW_PACKAGE_NAME);
|
|
812
|
+
const manifestPath = join(packagePath, "package.json");
|
|
813
|
+
try {
|
|
814
|
+
for (const directory of [nodeModulesPath, packagePath]) {
|
|
815
|
+
const directoryMetadata = await lstat(directory);
|
|
816
|
+
if (directoryMetadata.isSymbolicLink() || !directoryMetadata.isDirectory()) {
|
|
817
|
+
throw new Error("nested package path is not a real directory");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const manifest = JSON.parse(await readRegularFileWithoutFollowing(manifestPath));
|
|
821
|
+
if (manifest.name !== FLOW_PACKAGE_NAME || typeof manifest.version !== "string" || !isExactFlowVersion(manifest.version)) {
|
|
822
|
+
throw new Error("nested package manifest does not prove a Flow version");
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
path,
|
|
826
|
+
specifier,
|
|
827
|
+
resolvedVersion: manifest.version,
|
|
828
|
+
status: manifest.version === target ? "target" : "inactive"
|
|
829
|
+
};
|
|
830
|
+
} catch (error) {
|
|
831
|
+
return {
|
|
832
|
+
path,
|
|
833
|
+
specifier,
|
|
834
|
+
resolvedVersion: null,
|
|
835
|
+
status: "ambiguous",
|
|
836
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async function inspectCache(paths, target) {
|
|
841
|
+
const artifacts = [];
|
|
842
|
+
const issues = [];
|
|
843
|
+
const symlinkAncestor = await symlinkInPathRange(dirname(paths.cacheRoot), paths.packageCacheRoot);
|
|
844
|
+
if (symlinkAncestor) {
|
|
845
|
+
issues.push({
|
|
846
|
+
source: "cache",
|
|
847
|
+
path: paths.packageCacheRoot,
|
|
848
|
+
code: "unsafe-symlink",
|
|
849
|
+
message: `cache ancestor ${symlinkAncestor} is a symbolic link`
|
|
850
|
+
});
|
|
851
|
+
return { artifacts, issues };
|
|
852
|
+
}
|
|
853
|
+
const metadata = await optionalLstat(paths.packageCacheRoot);
|
|
854
|
+
if (!metadata)
|
|
855
|
+
return { artifacts, issues };
|
|
856
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
857
|
+
issues.push({
|
|
858
|
+
source: "cache",
|
|
859
|
+
path: paths.packageCacheRoot,
|
|
860
|
+
code: "ambiguous-cache-artifact",
|
|
861
|
+
message: "OpenCode package cache root is not a real directory"
|
|
862
|
+
});
|
|
863
|
+
return { artifacts, issues };
|
|
864
|
+
}
|
|
865
|
+
for (const entry of await readdir(paths.packageCacheRoot, {
|
|
866
|
+
withFileTypes: true
|
|
867
|
+
})) {
|
|
868
|
+
if (entry.name !== FLOW_PACKAGE_NAME && !entry.name.startsWith(`${FLOW_PACKAGE_NAME}@`)) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const artifact = await inspectCacheArtifact(join(paths.packageCacheRoot, entry.name), entry.name, target);
|
|
872
|
+
artifacts.push(artifact);
|
|
873
|
+
if (artifact.status === "ambiguous") {
|
|
874
|
+
issues.push({
|
|
875
|
+
source: "cache",
|
|
876
|
+
path: artifact.path,
|
|
877
|
+
code: "ambiguous-cache-artifact",
|
|
878
|
+
message: artifact.reason ?? "cache artifact does not prove a Flow version"
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return { artifacts, issues };
|
|
883
|
+
}
|
|
884
|
+
async function activationLimitations(paths) {
|
|
885
|
+
const limitations = [
|
|
886
|
+
{
|
|
887
|
+
source: "remote-config",
|
|
888
|
+
coverage: "runtime-leadership",
|
|
889
|
+
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."
|
|
891
|
+
}
|
|
892
|
+
];
|
|
893
|
+
if (paths.managedPreferencePaths.length > 0) {
|
|
894
|
+
const detected = [];
|
|
895
|
+
for (const path of paths.managedPreferencePaths) {
|
|
896
|
+
try {
|
|
897
|
+
if (await optionalLstat(path))
|
|
898
|
+
detected.push(path);
|
|
899
|
+
} catch {
|
|
900
|
+
detected.push(`${path} (unreadable)`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
limitations.push({
|
|
904
|
+
source: "managed-preferences",
|
|
905
|
+
coverage: "runtime-leadership",
|
|
906
|
+
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."
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return limitations;
|
|
911
|
+
}
|
|
912
|
+
function activationReasons(records, cacheArtifacts, issues, target) {
|
|
913
|
+
const reasons = issues.map((issue) => `${issue.path}: ${issue.message}`);
|
|
914
|
+
const targetPins = records.filter((record) => record.ownership === "flow-npm" && record.resolvedVersion === target && record.status === "target");
|
|
915
|
+
if (targetPins.length !== 1) {
|
|
916
|
+
reasons.push(`expected one exact ${FLOW_PACKAGE_NAME}@${target} activation, found ${targetPins.length}`);
|
|
917
|
+
}
|
|
918
|
+
if (records.length !== 1) {
|
|
919
|
+
reasons.push(`expected one Flow activation source, found ${records.length}`);
|
|
920
|
+
}
|
|
921
|
+
const inactive = cacheArtifacts.filter((artifact) => artifact.status === "inactive");
|
|
922
|
+
if (inactive.length > 0) {
|
|
923
|
+
reasons.push(`found ${inactive.length} inactive Flow cache artifact(s)`);
|
|
924
|
+
}
|
|
925
|
+
return [...new Set(reasons)];
|
|
926
|
+
}
|
|
927
|
+
async function checkFlowActivation(options) {
|
|
928
|
+
const target = resolveActivationTarget(options.target);
|
|
929
|
+
const paths = resolveActivationPaths(options.project, options.paths);
|
|
930
|
+
const env = options.paths?.env ?? process.env;
|
|
931
|
+
const records = [];
|
|
932
|
+
const issues = [];
|
|
933
|
+
try {
|
|
934
|
+
const projectMetadata = await lstat(paths.project);
|
|
935
|
+
if (projectMetadata.isSymbolicLink()) {
|
|
936
|
+
issues.push({
|
|
937
|
+
source: "project",
|
|
938
|
+
path: paths.project,
|
|
939
|
+
code: "unsafe-symlink",
|
|
940
|
+
message: "project root symbolic link refused for activation mutation safety"
|
|
941
|
+
});
|
|
942
|
+
} else if (!projectMetadata.isDirectory()) {
|
|
943
|
+
issues.push({
|
|
944
|
+
source: "project",
|
|
945
|
+
path: paths.project,
|
|
946
|
+
code: "invalid-project",
|
|
947
|
+
message: "project path is not a directory"
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
} catch (error) {
|
|
951
|
+
issues.push({
|
|
952
|
+
source: "project",
|
|
953
|
+
path: paths.project,
|
|
954
|
+
code: "invalid-project",
|
|
955
|
+
message: error.code === "ENOENT" ? "project path does not exist" : error instanceof Error ? error.message : String(error)
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
for (const descriptor of configDescriptors(paths)) {
|
|
959
|
+
try {
|
|
960
|
+
const inspected = await inspectConfig(descriptor, target);
|
|
961
|
+
records.push(...inspected.records);
|
|
962
|
+
issues.push(...inspected.issues);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
issues.push({
|
|
965
|
+
source: descriptor.source,
|
|
966
|
+
path: descriptor.path,
|
|
967
|
+
code: "invalid-config",
|
|
968
|
+
message: `config could not be inspected safely: ${error instanceof Error ? error.message : String(error)}`
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (env.OPENCODE_CONFIG_CONTENT?.trim()) {
|
|
973
|
+
const inspected = await inspectInlineConfig(env.OPENCODE_CONFIG_CONTENT, paths.project, target);
|
|
974
|
+
records.push(...inspected.records);
|
|
975
|
+
issues.push(...inspected.issues);
|
|
976
|
+
}
|
|
977
|
+
for (const descriptor of pluginDirectoryDescriptors(paths)) {
|
|
978
|
+
try {
|
|
979
|
+
const inspected = await inspectPluginDirectory(descriptor, target);
|
|
980
|
+
records.push(...inspected.records);
|
|
981
|
+
issues.push(...inspected.issues);
|
|
982
|
+
} catch (error) {
|
|
983
|
+
issues.push({
|
|
984
|
+
source: descriptor.source,
|
|
985
|
+
path: descriptor.path,
|
|
986
|
+
code: "invalid-plugin-directory",
|
|
987
|
+
message: `plugin directory could not be inspected safely: ${error instanceof Error ? error.message : String(error)}`
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
let cache;
|
|
992
|
+
try {
|
|
993
|
+
cache = await inspectCache(paths, target);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
cache = {
|
|
996
|
+
artifacts: [],
|
|
997
|
+
issues: [
|
|
998
|
+
{
|
|
999
|
+
source: "cache",
|
|
1000
|
+
path: paths.packageCacheRoot,
|
|
1001
|
+
code: "ambiguous-cache-artifact",
|
|
1002
|
+
message: `package cache could not be inspected safely: ${error instanceof Error ? error.message : String(error)}`
|
|
1003
|
+
}
|
|
1004
|
+
]
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
issues.push(...cache.issues);
|
|
1008
|
+
const limitations = await activationLimitations(paths);
|
|
1009
|
+
const reasons = activationReasons(records, cache.artifacts, issues, target);
|
|
1010
|
+
return {
|
|
1011
|
+
mode: "check",
|
|
1012
|
+
project: paths.project,
|
|
1013
|
+
target,
|
|
1014
|
+
paths,
|
|
1015
|
+
records,
|
|
1016
|
+
cacheArtifacts: cache.artifacts,
|
|
1017
|
+
issues,
|
|
1018
|
+
limitations,
|
|
1019
|
+
singleVersionSatisfied: reasons.length === 0,
|
|
1020
|
+
reasons
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function detectIndent(content) {
|
|
1024
|
+
return /\n([ \t]+)"/.exec(content)?.[1] ?? "\t";
|
|
1025
|
+
}
|
|
1026
|
+
async function readConfigSnapshot(descriptor) {
|
|
1027
|
+
const metadata = await optionalLstat(descriptor.path);
|
|
1028
|
+
if (!metadata) {
|
|
1029
|
+
return {
|
|
1030
|
+
descriptor,
|
|
1031
|
+
exists: false,
|
|
1032
|
+
content: null,
|
|
1033
|
+
digest: null,
|
|
1034
|
+
value: {},
|
|
1035
|
+
plugin: [],
|
|
1036
|
+
format: "strict-json",
|
|
1037
|
+
indent: "\t",
|
|
1038
|
+
newline: `
|
|
1039
|
+
`,
|
|
1040
|
+
finalNewline: true,
|
|
1041
|
+
mode: 384
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
1045
|
+
throw new Error(`${descriptor.path}: config must be a regular file`);
|
|
1046
|
+
}
|
|
1047
|
+
await assertSafeMutationPath(descriptor.safetyRoot, descriptor.path);
|
|
1048
|
+
const content = await readRegularFileWithoutFollowing(descriptor.path);
|
|
1049
|
+
let parsed;
|
|
1050
|
+
try {
|
|
1051
|
+
parsed = parseConfigContent(content);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
throw new Error(`${descriptor.path}: config cannot be conservatively parsed as JSON/JSONC: ${error instanceof Error ? error.message : String(error)}`);
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
descriptor,
|
|
1057
|
+
exists: true,
|
|
1058
|
+
content,
|
|
1059
|
+
digest: sha256(content),
|
|
1060
|
+
value: parsed.value,
|
|
1061
|
+
plugin: parsed.plugin,
|
|
1062
|
+
format: parsed.format,
|
|
1063
|
+
indent: detectIndent(content),
|
|
1064
|
+
newline: content.includes(`\r
|
|
1065
|
+
`) ? `\r
|
|
1066
|
+
` : `
|
|
1067
|
+
`,
|
|
1068
|
+
finalNewline: content.endsWith(`
|
|
1069
|
+
`),
|
|
1070
|
+
mode: metadata.mode & 511
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function updatedConfigContent(snapshot, entries) {
|
|
1074
|
+
const value = { ...snapshot.value, plugin: entries };
|
|
1075
|
+
let content = JSON.stringify(value, null, snapshot.indent).replaceAll(`
|
|
1076
|
+
`, snapshot.newline);
|
|
1077
|
+
if (snapshot.finalNewline)
|
|
1078
|
+
content += snapshot.newline;
|
|
1079
|
+
return content;
|
|
1080
|
+
}
|
|
1081
|
+
function wrapperRecoveryRoot(paths, wrapper) {
|
|
1082
|
+
if ([
|
|
1083
|
+
"global-config",
|
|
1084
|
+
"project-config",
|
|
1085
|
+
"project-directory-config",
|
|
1086
|
+
"custom-config",
|
|
1087
|
+
"custom-directory-config"
|
|
1088
|
+
].includes(wrapper.source)) {
|
|
1089
|
+
return join(dirname(wrapper.path), ".flow-activation-recovery");
|
|
1090
|
+
}
|
|
1091
|
+
if (wrapper.source === "home-plugin-directory") {
|
|
1092
|
+
return join(paths.home, ".opencode", "flow-activation-recovery");
|
|
1093
|
+
}
|
|
1094
|
+
if (wrapper.scope === "global")
|
|
1095
|
+
return paths.globalWrapperRecoveryRoot;
|
|
1096
|
+
if (wrapper.scope === "custom" && paths.customConfigDirectory) {
|
|
1097
|
+
return join(paths.customConfigDirectory, "flow-activation-recovery");
|
|
1098
|
+
}
|
|
1099
|
+
return paths.projectWrapperRecoveryRoot;
|
|
1100
|
+
}
|
|
1101
|
+
function wrapperMutationSafetyRoot(paths, wrapper) {
|
|
1102
|
+
const directory = pluginDirectoryDescriptors(paths).find((descriptor) => {
|
|
1103
|
+
if (descriptor.source !== wrapper.source)
|
|
1104
|
+
return false;
|
|
1105
|
+
const fromDirectory = relative(descriptor.path, wrapper.path);
|
|
1106
|
+
return fromDirectory !== ".." && !fromDirectory.startsWith(`..${sep}`) && !isAbsolute(fromDirectory);
|
|
1107
|
+
});
|
|
1108
|
+
if (directory)
|
|
1109
|
+
return directory.safetyRoot;
|
|
1110
|
+
const config = configDescriptors(paths).find((descriptor) => descriptor.source === wrapper.source);
|
|
1111
|
+
return config?.safetyRoot ?? dirname(wrapper.path);
|
|
1112
|
+
}
|
|
1113
|
+
function uniqueOwnedWrappers(records) {
|
|
1114
|
+
const wrappers = new Map;
|
|
1115
|
+
for (const record of records) {
|
|
1116
|
+
if (record.ownership !== "marker-owned-wrapper" || record.resolvedVersion === null) {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
wrappers.set(record.path, {
|
|
1120
|
+
path: record.path,
|
|
1121
|
+
scope: record.scope,
|
|
1122
|
+
source: record.source,
|
|
1123
|
+
version: record.resolvedVersion
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return [...wrappers.values()];
|
|
1127
|
+
}
|
|
1128
|
+
function removableConfigEntry(entry, descriptor, records) {
|
|
1129
|
+
const specifier = pluginSpecifier(entry);
|
|
1130
|
+
if (parseFlowNpmSpecifier(specifier).isFlow)
|
|
1131
|
+
return true;
|
|
1132
|
+
if (!isLocalPluginSpecifier(specifier) && !looksLikeFlowPath(specifier)) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
let localPath;
|
|
1136
|
+
try {
|
|
1137
|
+
localPath = relativeLocalSpecifier(descriptor.path, specifier);
|
|
1138
|
+
} catch {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
return records.some((record) => record.source === descriptor.source && record.specifier === specifier && record.path === localPath && record.ownership === "marker-owned-wrapper");
|
|
1142
|
+
}
|
|
1143
|
+
function activationRefusals(before) {
|
|
1144
|
+
return [
|
|
1145
|
+
...before.issues.map((issue) => `${issue.path}: ${issue.message}`),
|
|
1146
|
+
...before.records.filter((record) => record.ownership === "unknown-flow-like").map((record) => `${record.path}: ${record.reason ?? "unknown Flow-like activation refused"}`),
|
|
1147
|
+
...before.cacheArtifacts.filter((artifact) => artifact.status === "ambiguous").map((artifact) => `${artifact.path}: ${artifact.reason ?? "ambiguous cache artifact refused"}`)
|
|
1148
|
+
];
|
|
1149
|
+
}
|
|
1150
|
+
async function assertUnchangedConfig(snapshot) {
|
|
1151
|
+
await assertSafeMutationPath(snapshot.descriptor.safetyRoot, snapshot.descriptor.path);
|
|
1152
|
+
const metadata = await optionalLstat(snapshot.descriptor.path);
|
|
1153
|
+
if (!snapshot.exists) {
|
|
1154
|
+
if (metadata) {
|
|
1155
|
+
throw new Error(`${snapshot.descriptor.path}: config appeared while activation was running`);
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (!metadata?.isFile() || metadata.isSymbolicLink()) {
|
|
1160
|
+
throw new Error(`${snapshot.descriptor.path}: config changed while activation was running`);
|
|
1161
|
+
}
|
|
1162
|
+
const current = await readRegularFileWithoutFollowing(snapshot.descriptor.path);
|
|
1163
|
+
if (sha256(current) !== snapshot.digest) {
|
|
1164
|
+
throw new Error(`${snapshot.descriptor.path}: config changed while activation was running`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
async function atomicWriteConfig(snapshot, content, runId) {
|
|
1168
|
+
await assertUnchangedConfig(snapshot);
|
|
1169
|
+
await mkdir(dirname(snapshot.descriptor.path), { recursive: true });
|
|
1170
|
+
await assertSafeMutationPath(snapshot.descriptor.safetyRoot, snapshot.descriptor.path);
|
|
1171
|
+
const temporaryPath = join(dirname(snapshot.descriptor.path), `.${basename(snapshot.descriptor.path)}.flow-${runId}.tmp`);
|
|
1172
|
+
try {
|
|
1173
|
+
await writeFile(temporaryPath, content, {
|
|
1174
|
+
encoding: "utf8",
|
|
1175
|
+
flag: "wx",
|
|
1176
|
+
mode: snapshot.mode
|
|
1177
|
+
});
|
|
1178
|
+
await rename(temporaryPath, snapshot.descriptor.path);
|
|
1179
|
+
} finally {
|
|
1180
|
+
await rm(temporaryPath, { force: true });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async function configIsWritable(snapshot) {
|
|
1184
|
+
if (!snapshot.descriptor.mutable)
|
|
1185
|
+
return false;
|
|
1186
|
+
try {
|
|
1187
|
+
await assertSafeMutationPath(snapshot.descriptor.safetyRoot, snapshot.descriptor.path);
|
|
1188
|
+
if (snapshot.exists) {
|
|
1189
|
+
await access(snapshot.descriptor.path, constants.W_OK);
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
1192
|
+
let parent = dirname(snapshot.descriptor.path);
|
|
1193
|
+
while (!await optionalLstat(parent)) {
|
|
1194
|
+
const next = dirname(parent);
|
|
1195
|
+
if (next === parent)
|
|
1196
|
+
return false;
|
|
1197
|
+
parent = next;
|
|
1198
|
+
}
|
|
1199
|
+
await access(parent, constants.W_OK);
|
|
1200
|
+
return true;
|
|
1201
|
+
} catch {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async function pathCanBeMoved(source, destinationRoot) {
|
|
1206
|
+
try {
|
|
1207
|
+
await access(dirname(source), constants.W_OK);
|
|
1208
|
+
let parent = destinationRoot;
|
|
1209
|
+
while (!await optionalLstat(parent)) {
|
|
1210
|
+
const next = dirname(parent);
|
|
1211
|
+
if (next === parent)
|
|
1212
|
+
return false;
|
|
1213
|
+
parent = next;
|
|
1214
|
+
}
|
|
1215
|
+
await access(parent, constants.W_OK);
|
|
1216
|
+
return true;
|
|
1217
|
+
} catch {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function writeJournal(journalPath, journal) {
|
|
1222
|
+
await mkdir(dirname(journalPath), { recursive: true, mode: 448 });
|
|
1223
|
+
const temporaryPath = `${journalPath}.${randomUUID()}.tmp`;
|
|
1224
|
+
try {
|
|
1225
|
+
await writeFile(temporaryPath, `${JSON.stringify(journal, null, 2)}
|
|
1226
|
+
`, {
|
|
1227
|
+
encoding: "utf8",
|
|
1228
|
+
flag: "wx",
|
|
1229
|
+
mode: 384
|
|
1230
|
+
});
|
|
1231
|
+
await rename(temporaryPath, journalPath);
|
|
1232
|
+
} finally {
|
|
1233
|
+
await rm(temporaryPath, { force: true });
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async function verifyOwnedWrapper(wrapper) {
|
|
1237
|
+
const content = await readRegularFileWithoutFollowing(wrapper.path, MAX_LOCAL_PLUGIN_BYTES);
|
|
1238
|
+
const parsed = parseOwnedWrapper(content);
|
|
1239
|
+
if (parsed.kind !== "owned" || parsed.version !== wrapper.version) {
|
|
1240
|
+
throw new Error(`${wrapper.path}: owned wrapper changed while activation was running`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
async function verifyCacheArtifact(artifact, target) {
|
|
1244
|
+
const inspected = await inspectCacheArtifact(artifact.path, artifact.specifier, target);
|
|
1245
|
+
if (inspected.status !== "inactive" || inspected.resolvedVersion !== artifact.resolvedVersion) {
|
|
1246
|
+
throw new Error(`${artifact.path}: cache artifact changed while activation was running`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function replaceKnownConfigContent(options) {
|
|
1250
|
+
await assertSafeMutationPath(options.snapshot.descriptor.safetyRoot, options.snapshot.descriptor.path);
|
|
1251
|
+
const current = await readRegularFileWithoutFollowing(options.snapshot.descriptor.path);
|
|
1252
|
+
if (sha256(current) !== options.expectedDigest) {
|
|
1253
|
+
throw new Error(`${options.snapshot.descriptor.path}: automatic restore refused because the applied config changed`);
|
|
1254
|
+
}
|
|
1255
|
+
const temporaryPath = join(dirname(options.snapshot.descriptor.path), `.${basename(options.snapshot.descriptor.path)}.restore-${options.runId}.tmp`);
|
|
1256
|
+
try {
|
|
1257
|
+
await writeFile(temporaryPath, options.content, {
|
|
1258
|
+
encoding: "utf8",
|
|
1259
|
+
flag: "wx",
|
|
1260
|
+
mode: options.mode
|
|
1261
|
+
});
|
|
1262
|
+
await rename(temporaryPath, options.snapshot.descriptor.path);
|
|
1263
|
+
} finally {
|
|
1264
|
+
await rm(temporaryPath, { force: true });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async function rollbackCompletedActions(options) {
|
|
1268
|
+
const failures = [];
|
|
1269
|
+
for (const action of options.journal.actions.toReversed()) {
|
|
1270
|
+
if (action.state !== "complete")
|
|
1271
|
+
continue;
|
|
1272
|
+
try {
|
|
1273
|
+
if (action.action === "rewrite-config") {
|
|
1274
|
+
const snapshot = options.snapshots.find((candidate) => candidate.descriptor.path === action.path);
|
|
1275
|
+
if (!snapshot || !action.appliedDigest) {
|
|
1276
|
+
throw new Error("restore metadata is incomplete");
|
|
1277
|
+
}
|
|
1278
|
+
if (action.originalAbsent) {
|
|
1279
|
+
await assertSafeMutationPath(snapshot.descriptor.safetyRoot, action.path);
|
|
1280
|
+
const current = await readRegularFileWithoutFollowing(action.path);
|
|
1281
|
+
if (sha256(current) !== action.appliedDigest) {
|
|
1282
|
+
throw new Error("created config changed after apply; automatic removal refused");
|
|
1283
|
+
}
|
|
1284
|
+
if (!action.recoveryPath) {
|
|
1285
|
+
throw new Error("created config recovery path is missing");
|
|
1286
|
+
}
|
|
1287
|
+
await assertSafeMutationPath(snapshot.descriptor.safetyRoot, action.recoveryPath);
|
|
1288
|
+
await mkdir(dirname(action.recoveryPath), {
|
|
1289
|
+
recursive: true,
|
|
1290
|
+
mode: 448
|
|
1291
|
+
});
|
|
1292
|
+
await rename(action.path, action.recoveryPath);
|
|
1293
|
+
} else {
|
|
1294
|
+
if (!action.backupPath)
|
|
1295
|
+
throw new Error("config backup is missing");
|
|
1296
|
+
const backup = await readRegularFileWithoutFollowing(action.backupPath);
|
|
1297
|
+
await replaceKnownConfigContent({
|
|
1298
|
+
snapshot,
|
|
1299
|
+
expectedDigest: action.appliedDigest,
|
|
1300
|
+
content: backup,
|
|
1301
|
+
mode: action.originalMode ?? snapshot.mode,
|
|
1302
|
+
runId: options.runId
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
} else if (action.action === "quarantine-wrapper" || action.action === "quarantine-cache") {
|
|
1306
|
+
if (!action.recoveryPath)
|
|
1307
|
+
throw new Error("recovery path is missing");
|
|
1308
|
+
let safetyRoot;
|
|
1309
|
+
if (action.action === "quarantine-cache") {
|
|
1310
|
+
safetyRoot = dirname(options.paths.cacheRoot);
|
|
1311
|
+
} else {
|
|
1312
|
+
const wrapper = options.wrappers.find((candidate) => candidate.path === action.path);
|
|
1313
|
+
if (!wrapper)
|
|
1314
|
+
throw new Error("wrapper safety metadata is missing");
|
|
1315
|
+
safetyRoot = wrapperMutationSafetyRoot(options.paths, wrapper);
|
|
1316
|
+
}
|
|
1317
|
+
await assertSafeMutationPath(safetyRoot, action.path);
|
|
1318
|
+
await assertSafeMutationPath(safetyRoot, action.recoveryPath);
|
|
1319
|
+
if (await optionalLstat(action.path)) {
|
|
1320
|
+
throw new Error("original path is occupied; automatic restore refused");
|
|
1321
|
+
}
|
|
1322
|
+
const recovery = await optionalLstat(action.recoveryPath);
|
|
1323
|
+
if (!recovery || recovery.isSymbolicLink()) {
|
|
1324
|
+
throw new Error("quarantined artifact is missing or symbolic");
|
|
1325
|
+
}
|
|
1326
|
+
await mkdir(dirname(action.path), { recursive: true });
|
|
1327
|
+
await rename(action.recoveryPath, action.path);
|
|
1328
|
+
}
|
|
1329
|
+
action.state = "rolled-back";
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1332
|
+
action.state = "rollback-failed";
|
|
1333
|
+
action.error = message;
|
|
1334
|
+
failures.push(`${action.path}: ${message}`);
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
await writeJournal(options.journalPath, options.journal);
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
failures.push(`journal update: ${error instanceof Error ? error.message : String(error)}`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return failures;
|
|
1343
|
+
}
|
|
1344
|
+
async function applyFlowActivation(options) {
|
|
1345
|
+
const target = resolveActivationTarget(options.target);
|
|
1346
|
+
const before = await checkFlowActivation({
|
|
1347
|
+
project: options.project,
|
|
1348
|
+
target,
|
|
1349
|
+
...options.paths ? { paths: options.paths } : {}
|
|
1350
|
+
});
|
|
1351
|
+
const refusals = activationRefusals(before);
|
|
1352
|
+
const snapshots = [];
|
|
1353
|
+
for (const descriptor of configDescriptors(before.paths)) {
|
|
1354
|
+
try {
|
|
1355
|
+
snapshots.push(await readConfigSnapshot(descriptor));
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
refusals.push(error instanceof Error ? error.message : String(error));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const pin = `${FLOW_PACKAGE_NAME}@${target}`;
|
|
1361
|
+
const canonicalPath = options.scope === "global" ? before.paths.globalConfig : before.paths.projectConfig;
|
|
1362
|
+
const nextEntries = new Map;
|
|
1363
|
+
for (const snapshot of snapshots) {
|
|
1364
|
+
const retained = snapshot.plugin.filter((entry) => !removableConfigEntry(entry, snapshot.descriptor, before.records));
|
|
1365
|
+
if (snapshot.descriptor.path === canonicalPath)
|
|
1366
|
+
retained.push(pin);
|
|
1367
|
+
nextEntries.set(snapshot.descriptor.path, retained);
|
|
1368
|
+
}
|
|
1369
|
+
const ownedWrappers = uniqueOwnedWrappers(before.records).filter((wrapper) => wrapper.source !== "inline-config" && wrapper.source !== "managed-config");
|
|
1370
|
+
const wrappers = [];
|
|
1371
|
+
for (const wrapper of ownedWrappers) {
|
|
1372
|
+
try {
|
|
1373
|
+
const safetyRoot = wrapperMutationSafetyRoot(before.paths, wrapper);
|
|
1374
|
+
const recoveryRoot = wrapperRecoveryRoot(before.paths, wrapper);
|
|
1375
|
+
await assertSafeMutationPath(safetyRoot, wrapper.path);
|
|
1376
|
+
await assertSafeMutationPath(safetyRoot, recoveryRoot);
|
|
1377
|
+
if (!await pathCanBeMoved(wrapper.path, recoveryRoot)) {
|
|
1378
|
+
throw new Error(`${wrapper.path}: marker-owned wrapper or recovery parent is not writable; archive it manually and rerun activation-apply`);
|
|
1379
|
+
}
|
|
1380
|
+
wrappers.push(wrapper);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
refusals.push(error instanceof Error ? error.message : String(error));
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const provenInactiveCache = before.cacheArtifacts.filter((artifact) => artifact.status === "inactive");
|
|
1386
|
+
const inactiveCache = [];
|
|
1387
|
+
for (const artifact of provenInactiveCache) {
|
|
1388
|
+
try {
|
|
1389
|
+
const safetyRoot = dirname(before.paths.cacheRoot);
|
|
1390
|
+
await assertSafeMutationPath(safetyRoot, artifact.path);
|
|
1391
|
+
await assertSafeMutationPath(safetyRoot, before.paths.cacheRecoveryRoot);
|
|
1392
|
+
if (!await pathCanBeMoved(artifact.path, before.paths.cacheRecoveryRoot)) {
|
|
1393
|
+
throw new Error(`${artifact.path}: proven inactive cache artifact or recovery parent is not writable; move it outside ${before.paths.packageCacheRoot} manually and rerun activation-apply`);
|
|
1394
|
+
}
|
|
1395
|
+
inactiveCache.push(artifact);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
refusals.push(error instanceof Error ? error.message : String(error));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const plan = [];
|
|
1401
|
+
const changedSnapshots = [];
|
|
1402
|
+
const addManualRemediation = (snapshot, path, detail) => {
|
|
1403
|
+
plan.push({
|
|
1404
|
+
action: "manual-remediation",
|
|
1405
|
+
...snapshot ? { scope: snapshot.descriptor.scope } : {},
|
|
1406
|
+
path,
|
|
1407
|
+
detail
|
|
1408
|
+
});
|
|
1409
|
+
refusals.push(`${path}: ${detail}`);
|
|
1410
|
+
};
|
|
1411
|
+
const scheduleConfigRewrite = async (snapshot, detail) => {
|
|
1412
|
+
if (changedSnapshots.includes(snapshot))
|
|
1413
|
+
return;
|
|
1414
|
+
if (!snapshot.descriptor.mutable) {
|
|
1415
|
+
addManualRemediation(snapshot, snapshot.descriptor.path, `managed config is immutable; ${snapshot.descriptor.manualRemediation}`);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (snapshot.format === "jsonc") {
|
|
1419
|
+
addManualRemediation(snapshot, snapshot.descriptor.path, `JSONC Flow activation was inventoried but cannot be edited losslessly without a JSONC editor; ${snapshot.descriptor.manualRemediation}, remove every ${FLOW_PACKAGE_NAME} entry, then rerun activation-apply`);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (!await configIsWritable(snapshot)) {
|
|
1423
|
+
addManualRemediation(snapshot, snapshot.descriptor.path, `config or its mutation path is not safely writable; ${snapshot.descriptor.manualRemediation}`);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
changedSnapshots.push(snapshot);
|
|
1427
|
+
plan.push({
|
|
1428
|
+
action: "rewrite-config",
|
|
1429
|
+
scope: snapshot.descriptor.scope,
|
|
1430
|
+
path: snapshot.descriptor.path,
|
|
1431
|
+
detail
|
|
1432
|
+
});
|
|
1433
|
+
};
|
|
1434
|
+
for (const snapshot of snapshots) {
|
|
1435
|
+
const entries = nextEntries.get(snapshot.descriptor.path) ?? [];
|
|
1436
|
+
const changed = snapshot.descriptor.path === canonicalPath && !snapshot.exists || JSON.stringify(entries) !== JSON.stringify(snapshot.plugin);
|
|
1437
|
+
if (!changed)
|
|
1438
|
+
continue;
|
|
1439
|
+
await scheduleConfigRewrite(snapshot, snapshot.descriptor.path === canonicalPath ? `write canonical exact pin ${pin} last` : "remove recognized Flow activation entries while preserving string/tuple entries and options");
|
|
1440
|
+
}
|
|
1441
|
+
for (const record of before.records.filter((record2) => record2.source === "inline-config")) {
|
|
1442
|
+
addManualRemediation(null, "env:OPENCODE_CONFIG_CONTENT", `inline Flow activation ${record.specifier} is immutable; remove it from OPENCODE_CONFIG_CONTENT or unset that variable, then rerun activation-apply`);
|
|
1443
|
+
}
|
|
1444
|
+
const canonicalSnapshot = snapshots.find((snapshot) => snapshot.descriptor.path === canonicalPath);
|
|
1445
|
+
for (const wrapper of wrappers) {
|
|
1446
|
+
plan.push({
|
|
1447
|
+
action: "quarantine-wrapper",
|
|
1448
|
+
scope: wrapper.scope,
|
|
1449
|
+
path: wrapper.path,
|
|
1450
|
+
detail: "move marker-proven wrapper outside OpenCode plugin discovery"
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
for (const artifact of inactiveCache) {
|
|
1454
|
+
plan.push({
|
|
1455
|
+
action: "quarantine-cache",
|
|
1456
|
+
path: artifact.path,
|
|
1457
|
+
detail: `move proven inactive Flow ${artifact.resolvedVersion} cache artifact; never clear the cache root`
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
const base = {
|
|
1461
|
+
mode: options.apply === true ? "apply" : "dry-run",
|
|
1462
|
+
project: before.project,
|
|
1463
|
+
target,
|
|
1464
|
+
scope: options.scope,
|
|
1465
|
+
status: refusals.length > 0 ? "refused" : "ready",
|
|
1466
|
+
before,
|
|
1467
|
+
plan,
|
|
1468
|
+
refusals: [...new Set(refusals)]
|
|
1469
|
+
};
|
|
1470
|
+
if (options.apply !== true || refusals.length > 0)
|
|
1471
|
+
return base;
|
|
1472
|
+
if (plan.length === 0) {
|
|
1473
|
+
return { ...base, status: "applied", after: before };
|
|
1474
|
+
}
|
|
1475
|
+
const runId = `${new Date().toISOString().replaceAll(":", "-")}-${randomUUID()}`;
|
|
1476
|
+
const runRoot = join(before.paths.journalRoot, runId);
|
|
1477
|
+
const journalPath = join(runRoot, "journal.json");
|
|
1478
|
+
const actions = [];
|
|
1479
|
+
for (const snapshot of changedSnapshots) {
|
|
1480
|
+
const action = {
|
|
1481
|
+
action: "rewrite-config",
|
|
1482
|
+
path: snapshot.descriptor.path,
|
|
1483
|
+
originalAbsent: !snapshot.exists,
|
|
1484
|
+
originalMode: snapshot.mode,
|
|
1485
|
+
state: "pending"
|
|
1486
|
+
};
|
|
1487
|
+
if (snapshot.exists) {
|
|
1488
|
+
action.backupPath = join(runRoot, "configs", `${snapshot.descriptor.source}-${sha256(snapshot.descriptor.path).slice(0, 12)}.backup`);
|
|
1489
|
+
} else {
|
|
1490
|
+
action.recoveryPath = join(dirname(snapshot.descriptor.path), ".flow-activation-recovery", runId, `${basename(snapshot.descriptor.path)}-${sha256(snapshot.descriptor.path).slice(0, 12)}`);
|
|
1491
|
+
}
|
|
1492
|
+
actions.push(action);
|
|
1493
|
+
}
|
|
1494
|
+
for (const wrapper of wrappers) {
|
|
1495
|
+
actions.push({
|
|
1496
|
+
action: "quarantine-wrapper",
|
|
1497
|
+
path: wrapper.path,
|
|
1498
|
+
recoveryPath: join(wrapperRecoveryRoot(before.paths, wrapper), runId, `${basename(wrapper.path)}-${sha256(wrapper.path).slice(0, 12)}`),
|
|
1499
|
+
state: "pending"
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
for (const artifact of inactiveCache) {
|
|
1503
|
+
actions.push({
|
|
1504
|
+
action: "quarantine-cache",
|
|
1505
|
+
path: artifact.path,
|
|
1506
|
+
recoveryPath: join(before.paths.cacheRecoveryRoot, runId, `${basename(artifact.path)}-${sha256(artifact.path).slice(0, 12)}`),
|
|
1507
|
+
state: "pending"
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
const journal = {
|
|
1511
|
+
format: "flow-activation-journal-v1",
|
|
1512
|
+
runId,
|
|
1513
|
+
createdAt: new Date().toISOString(),
|
|
1514
|
+
project: before.project,
|
|
1515
|
+
target,
|
|
1516
|
+
scope: options.scope,
|
|
1517
|
+
state: "prepared",
|
|
1518
|
+
actions
|
|
1519
|
+
};
|
|
1520
|
+
try {
|
|
1521
|
+
await assertSafeMutationPath(dirname(before.paths.configRoot), runRoot);
|
|
1522
|
+
await mkdir(runRoot, { recursive: true, mode: 448 });
|
|
1523
|
+
await writeJournal(journalPath, journal);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
const message = `recovery journal could not be prepared safely at ${journalPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1526
|
+
return {
|
|
1527
|
+
...base,
|
|
1528
|
+
status: "refused",
|
|
1529
|
+
refusals: [...new Set([...base.refusals, message])]
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
try {
|
|
1533
|
+
for (const snapshot of changedSnapshots) {
|
|
1534
|
+
await assertUnchangedConfig(snapshot);
|
|
1535
|
+
const action = actions.find((candidate) => candidate.action === "rewrite-config" && candidate.path === snapshot.descriptor.path);
|
|
1536
|
+
if (snapshot.exists && action?.backupPath) {
|
|
1537
|
+
await mkdir(dirname(action.backupPath), {
|
|
1538
|
+
recursive: true,
|
|
1539
|
+
mode: 448
|
|
1540
|
+
});
|
|
1541
|
+
await writeFile(action.backupPath, snapshot.content, {
|
|
1542
|
+
encoding: "utf8",
|
|
1543
|
+
flag: "wx",
|
|
1544
|
+
mode: 384
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (canonicalSnapshot && !changedSnapshots.includes(canonicalSnapshot)) {
|
|
1549
|
+
await assertUnchangedConfig(canonicalSnapshot);
|
|
1550
|
+
}
|
|
1551
|
+
for (const wrapper of wrappers)
|
|
1552
|
+
await verifyOwnedWrapper(wrapper);
|
|
1553
|
+
for (const artifact of inactiveCache) {
|
|
1554
|
+
await verifyCacheArtifact(artifact, target);
|
|
1555
|
+
}
|
|
1556
|
+
journal.state = "applying";
|
|
1557
|
+
await writeJournal(journalPath, journal);
|
|
1558
|
+
const nonTargetConfigs = changedSnapshots.filter((snapshot) => snapshot.descriptor.path !== canonicalPath);
|
|
1559
|
+
const targetConfig = changedSnapshots.find((snapshot) => snapshot.descriptor.path === canonicalPath);
|
|
1560
|
+
for (const snapshot of nonTargetConfigs) {
|
|
1561
|
+
const entries = nextEntries.get(snapshot.descriptor.path) ?? [];
|
|
1562
|
+
const content = updatedConfigContent(snapshot, entries);
|
|
1563
|
+
await atomicWriteConfig(snapshot, content, runId);
|
|
1564
|
+
const action = actions.find((candidate) => candidate.action === "rewrite-config" && candidate.path === snapshot.descriptor.path);
|
|
1565
|
+
if (action) {
|
|
1566
|
+
action.appliedDigest = sha256(content);
|
|
1567
|
+
action.state = "complete";
|
|
1568
|
+
}
|
|
1569
|
+
await writeJournal(journalPath, journal);
|
|
1570
|
+
await options.afterMutation?.(plan.find((operation) => operation.action === "rewrite-config" && operation.path === snapshot.descriptor.path));
|
|
1571
|
+
}
|
|
1572
|
+
for (const wrapper of wrappers) {
|
|
1573
|
+
const action = actions.find((candidate) => candidate.action === "quarantine-wrapper" && candidate.path === wrapper.path);
|
|
1574
|
+
if (!action?.recoveryPath) {
|
|
1575
|
+
throw new Error(`${wrapper.path}: missing wrapper recovery path`);
|
|
1576
|
+
}
|
|
1577
|
+
await verifyOwnedWrapper(wrapper);
|
|
1578
|
+
await assertSafeMutationPath(wrapperMutationSafetyRoot(before.paths, wrapper), wrapper.path);
|
|
1579
|
+
await mkdir(dirname(action.recoveryPath), {
|
|
1580
|
+
recursive: true,
|
|
1581
|
+
mode: 448
|
|
1582
|
+
});
|
|
1583
|
+
await rename(wrapper.path, action.recoveryPath);
|
|
1584
|
+
action.state = "complete";
|
|
1585
|
+
await writeJournal(journalPath, journal);
|
|
1586
|
+
await options.afterMutation?.(plan.find((operation) => operation.action === "quarantine-wrapper" && operation.path === wrapper.path));
|
|
1587
|
+
}
|
|
1588
|
+
for (const artifact of inactiveCache) {
|
|
1589
|
+
const action = actions.find((candidate) => candidate.action === "quarantine-cache" && candidate.path === artifact.path);
|
|
1590
|
+
if (!action?.recoveryPath) {
|
|
1591
|
+
throw new Error(`${artifact.path}: missing cache recovery path`);
|
|
1592
|
+
}
|
|
1593
|
+
await verifyCacheArtifact(artifact, target);
|
|
1594
|
+
await assertSafeMutationPath(dirname(before.paths.cacheRoot), artifact.path);
|
|
1595
|
+
await mkdir(dirname(action.recoveryPath), {
|
|
1596
|
+
recursive: true,
|
|
1597
|
+
mode: 448
|
|
1598
|
+
});
|
|
1599
|
+
await rename(artifact.path, action.recoveryPath);
|
|
1600
|
+
action.state = "complete";
|
|
1601
|
+
await writeJournal(journalPath, journal);
|
|
1602
|
+
await options.afterMutation?.(plan.find((operation) => operation.action === "quarantine-cache" && operation.path === artifact.path));
|
|
1603
|
+
}
|
|
1604
|
+
if (targetConfig) {
|
|
1605
|
+
const targetEntries = nextEntries.get(targetConfig.descriptor.path) ?? [];
|
|
1606
|
+
const targetContent = updatedConfigContent(targetConfig, targetEntries);
|
|
1607
|
+
await atomicWriteConfig(targetConfig, targetContent, runId);
|
|
1608
|
+
const targetAction = actions.find((candidate) => candidate.action === "rewrite-config" && candidate.path === targetConfig.descriptor.path);
|
|
1609
|
+
if (targetAction) {
|
|
1610
|
+
targetAction.appliedDigest = sha256(targetContent);
|
|
1611
|
+
targetAction.state = "complete";
|
|
1612
|
+
}
|
|
1613
|
+
await writeJournal(journalPath, journal);
|
|
1614
|
+
await options.afterMutation?.(plan.find((operation) => operation.action === "rewrite-config" && operation.path === targetConfig.descriptor.path));
|
|
1615
|
+
}
|
|
1616
|
+
const after = await checkFlowActivation({
|
|
1617
|
+
project: before.project,
|
|
1618
|
+
target,
|
|
1619
|
+
...options.paths ? { paths: options.paths } : {}
|
|
1620
|
+
});
|
|
1621
|
+
if (!after.singleVersionSatisfied) {
|
|
1622
|
+
throw new Error(`post-apply inventory did not prove a single version: ${after.reasons.join("; ")}`);
|
|
1623
|
+
}
|
|
1624
|
+
journal.state = "complete";
|
|
1625
|
+
await writeJournal(journalPath, journal);
|
|
1626
|
+
return {
|
|
1627
|
+
...base,
|
|
1628
|
+
status: "applied",
|
|
1629
|
+
recovery: { runId, journalPath },
|
|
1630
|
+
after,
|
|
1631
|
+
refusals: []
|
|
1632
|
+
};
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
journal.state = "failed";
|
|
1635
|
+
journal.error = error instanceof Error ? error.message : String(error);
|
|
1636
|
+
try {
|
|
1637
|
+
await writeJournal(journalPath, journal);
|
|
1638
|
+
} catch {}
|
|
1639
|
+
const rollbackFailures = await rollbackCompletedActions({
|
|
1640
|
+
journal,
|
|
1641
|
+
journalPath,
|
|
1642
|
+
runId,
|
|
1643
|
+
snapshots: changedSnapshots,
|
|
1644
|
+
paths: before.paths,
|
|
1645
|
+
wrappers
|
|
1646
|
+
});
|
|
1647
|
+
const recoveryState = rollbackFailures.length === 0 ? "rolled-back" : "rollback-failed";
|
|
1648
|
+
journal.state = recoveryState;
|
|
1649
|
+
if (rollbackFailures.length > 0) {
|
|
1650
|
+
journal.error = `${journal.error ?? "apply failed"}; rollback: ${rollbackFailures.join("; ")}`;
|
|
1651
|
+
}
|
|
1652
|
+
try {
|
|
1653
|
+
await writeJournal(journalPath, journal);
|
|
1654
|
+
} catch {}
|
|
1655
|
+
const guidance = recoveryState === "rolled-back" ? [
|
|
1656
|
+
"All completed mutations were restored from exact backups or quarantine renames.",
|
|
1657
|
+
`Inspect ${journalPath} and resolve the recorded failure before retrying.`
|
|
1658
|
+
] : [
|
|
1659
|
+
"Stop OpenCode before manual recovery.",
|
|
1660
|
+
`Inspect ${journalPath}; for rollback-failed actions, restore backupPath to path or rename recoveryPath back to path only after verifying the destination is absent or unchanged.`,
|
|
1661
|
+
"Do not delete the recovery directory until activation-check succeeds."
|
|
1662
|
+
];
|
|
1663
|
+
return {
|
|
1664
|
+
...base,
|
|
1665
|
+
status: "refused",
|
|
1666
|
+
recovery: { runId, journalPath },
|
|
1667
|
+
failure: {
|
|
1668
|
+
message: journal.error ?? "activation apply failed",
|
|
1669
|
+
recoveryState,
|
|
1670
|
+
guidance
|
|
1671
|
+
},
|
|
1672
|
+
refusals: [journal.error ?? "activation apply failed"]
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
3
1677
|
// src/distribution/legacy-cleanup.ts
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { constants } from "node:fs";
|
|
1678
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1679
|
+
import { constants as constants2 } from "node:fs";
|
|
6
1680
|
import {
|
|
7
|
-
lstat,
|
|
8
|
-
mkdir,
|
|
9
|
-
open,
|
|
10
|
-
readdir,
|
|
11
|
-
rename
|
|
1681
|
+
lstat as lstat2,
|
|
1682
|
+
mkdir as mkdir2,
|
|
1683
|
+
open as open2,
|
|
1684
|
+
readdir as readdir2,
|
|
1685
|
+
rename as rename2
|
|
12
1686
|
} from "node:fs/promises";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
|
-
import { isAbsolute, join, normalize, sep } from "node:path";
|
|
1687
|
+
import { homedir as homedir2 } from "node:os";
|
|
1688
|
+
import { isAbsolute as isAbsolute2, join as join2, normalize as normalize2, sep as sep2 } from "node:path";
|
|
15
1689
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
16
1690
|
|
|
17
1691
|
// src/guidance/ids.ts
|
|
@@ -28,52 +1702,52 @@ var FLOW_GUIDANCE_TOPICS = [
|
|
|
28
1702
|
|
|
29
1703
|
// src/distribution/legacy-cleanup.ts
|
|
30
1704
|
var LEGACY_MARKER = ".flow-skill-version";
|
|
31
|
-
var
|
|
1705
|
+
var NO_FOLLOW2 = constants2.O_NOFOLLOW ?? 0;
|
|
32
1706
|
var SUPPORTED_LEGACY_MAJOR = "4";
|
|
33
1707
|
var POST_MOVE_VERIFICATION_ATTEMPTS = 4;
|
|
34
1708
|
var POST_MOVE_VERIFICATION_RETRY_MS = 25;
|
|
35
1709
|
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-]+)*)?$/;
|
|
36
|
-
function
|
|
37
|
-
return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() ||
|
|
1710
|
+
function configuredHome2() {
|
|
1711
|
+
return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || homedir2();
|
|
38
1712
|
}
|
|
39
|
-
function resolveLegacySkillsRoot(home =
|
|
40
|
-
return
|
|
1713
|
+
function resolveLegacySkillsRoot(home = configuredHome2()) {
|
|
1714
|
+
return join2(home, ".config", "opencode", "skills");
|
|
41
1715
|
}
|
|
42
|
-
function resolveLegacyArchiveRoot(home =
|
|
43
|
-
return
|
|
1716
|
+
function resolveLegacyArchiveRoot(home = configuredHome2()) {
|
|
1717
|
+
return join2(home, ".config", "opencode", "flow-legacy-skills");
|
|
44
1718
|
}
|
|
45
|
-
function
|
|
46
|
-
return
|
|
1719
|
+
function sha2562(content) {
|
|
1720
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
47
1721
|
}
|
|
48
1722
|
function safeLegacyPath(folder, relativePath) {
|
|
49
|
-
if (!relativePath ||
|
|
1723
|
+
if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\\") || relativePath.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
50
1724
|
throw new Error(`unsafe marker path '${relativePath}'`);
|
|
51
1725
|
}
|
|
52
|
-
const resolved =
|
|
53
|
-
if (!resolved.startsWith(`${folder}${
|
|
1726
|
+
const resolved = normalize2(join2(folder, ...relativePath.split("/")));
|
|
1727
|
+
if (!resolved.startsWith(`${folder}${sep2}`)) {
|
|
54
1728
|
throw new Error(`unsafe marker path '${relativePath}'`);
|
|
55
1729
|
}
|
|
56
1730
|
return resolved;
|
|
57
1731
|
}
|
|
58
1732
|
async function optionalStat(path) {
|
|
59
1733
|
try {
|
|
60
|
-
return await
|
|
1734
|
+
return await lstat2(path, { bigint: false });
|
|
61
1735
|
} catch (error) {
|
|
62
1736
|
if (error.code === "ENOENT")
|
|
63
1737
|
return null;
|
|
64
1738
|
throw error;
|
|
65
1739
|
}
|
|
66
1740
|
}
|
|
67
|
-
async function
|
|
1741
|
+
async function readRegularFileWithoutFollowing2(path) {
|
|
68
1742
|
let handle;
|
|
69
1743
|
try {
|
|
70
|
-
const pathMetadata = await
|
|
1744
|
+
const pathMetadata = await lstat2(path);
|
|
71
1745
|
if (pathMetadata.isSymbolicLink()) {
|
|
72
1746
|
throw new Error(`symbolic link refused: ${path}`);
|
|
73
1747
|
}
|
|
74
1748
|
if (!pathMetadata.isFile())
|
|
75
1749
|
throw new Error(`not a regular file: ${path}`);
|
|
76
|
-
handle = await
|
|
1750
|
+
handle = await open2(path, constants2.O_RDONLY | NO_FOLLOW2);
|
|
77
1751
|
const metadata = await handle.stat();
|
|
78
1752
|
if (!metadata.isFile())
|
|
79
1753
|
throw new Error(`not a regular file: ${path}`);
|
|
@@ -169,8 +1843,8 @@ async function inspectLegacyFolder(name, folder) {
|
|
|
169
1843
|
};
|
|
170
1844
|
}
|
|
171
1845
|
try {
|
|
172
|
-
const markerPath =
|
|
173
|
-
const marker = parseMarker(await
|
|
1846
|
+
const markerPath = join2(folder, LEGACY_MARKER);
|
|
1847
|
+
const marker = parseMarker(await readRegularFileWithoutFollowing2(markerPath));
|
|
174
1848
|
assertSupportedLegacyVersion(marker.version);
|
|
175
1849
|
const directories = expectedDirectoryEntries(marker);
|
|
176
1850
|
for (const [relativeDirectory, expectedEntries] of directories) {
|
|
@@ -179,7 +1853,7 @@ async function inspectLegacyFolder(name, folder) {
|
|
|
179
1853
|
if (!directoryMetadata?.isDirectory()) {
|
|
180
1854
|
throw new Error(`expected real directory: ${relativeDirectory || "."}`);
|
|
181
1855
|
}
|
|
182
|
-
const actualEntries = await
|
|
1856
|
+
const actualEntries = await readdir2(directory);
|
|
183
1857
|
const unexpected = actualEntries.filter((entry) => !expectedEntries.has(entry));
|
|
184
1858
|
const missing = [...expectedEntries].filter((entry) => !actualEntries.includes(entry));
|
|
185
1859
|
if (unexpected.length > 0 || missing.length > 0) {
|
|
@@ -191,8 +1865,8 @@ async function inspectLegacyFolder(name, folder) {
|
|
|
191
1865
|
}
|
|
192
1866
|
for (const [relativePath, expectedHash] of marker.files) {
|
|
193
1867
|
const path = safeLegacyPath(folder, relativePath);
|
|
194
|
-
const content = await
|
|
195
|
-
if (
|
|
1868
|
+
const content = await readRegularFileWithoutFollowing2(path);
|
|
1869
|
+
if (sha2562(content) !== expectedHash) {
|
|
196
1870
|
throw new Error(`edited file refused: ${relativePath}`);
|
|
197
1871
|
}
|
|
198
1872
|
}
|
|
@@ -219,7 +1893,7 @@ async function ensureRealArchiveRoot(path) {
|
|
|
219
1893
|
return;
|
|
220
1894
|
}
|
|
221
1895
|
try {
|
|
222
|
-
await
|
|
1896
|
+
await mkdir2(path, { mode: 448 });
|
|
223
1897
|
} catch (error) {
|
|
224
1898
|
if (error.code !== "EEXIST")
|
|
225
1899
|
throw error;
|
|
@@ -238,14 +1912,14 @@ async function verifyMovedLegacyFolder(name, archivePath) {
|
|
|
238
1912
|
return verified;
|
|
239
1913
|
}
|
|
240
1914
|
async function cleanupLegacySkills(options) {
|
|
241
|
-
const home = options?.home ??
|
|
1915
|
+
const home = options?.home ?? configuredHome2();
|
|
242
1916
|
const root = resolveLegacySkillsRoot(home);
|
|
243
1917
|
const archiveRoot = resolveLegacyArchiveRoot(home);
|
|
244
1918
|
const apply = options?.apply === true;
|
|
245
1919
|
const results = [];
|
|
246
1920
|
let archiveReady = false;
|
|
247
1921
|
for (const name of FLOW_GUIDANCE_TOPICS) {
|
|
248
|
-
const path =
|
|
1922
|
+
const path = join2(root, name);
|
|
249
1923
|
const inspected = await inspectLegacyFolder(name, path);
|
|
250
1924
|
if (!apply || inspected.status !== "eligible") {
|
|
251
1925
|
results.push(inspected);
|
|
@@ -255,9 +1929,9 @@ async function cleanupLegacySkills(options) {
|
|
|
255
1929
|
await ensureRealArchiveRoot(archiveRoot);
|
|
256
1930
|
archiveReady = true;
|
|
257
1931
|
}
|
|
258
|
-
const archivePath =
|
|
1932
|
+
const archivePath = join2(archiveRoot, `${name}-${new Date().toISOString().replaceAll(":", "-")}-${crypto.randomUUID()}`);
|
|
259
1933
|
try {
|
|
260
|
-
await
|
|
1934
|
+
await rename2(path, archivePath);
|
|
261
1935
|
} catch (error) {
|
|
262
1936
|
if (error.code !== "ENOENT")
|
|
263
1937
|
throw error;
|
|
@@ -305,37 +1979,39 @@ async function cleanupLegacySkills(options) {
|
|
|
305
1979
|
};
|
|
306
1980
|
}
|
|
307
1981
|
|
|
308
|
-
// src/version.ts
|
|
309
|
-
import { createRequire } from "node:module";
|
|
310
|
-
function resolveFlowPluginVersion() {
|
|
311
|
-
try {
|
|
312
|
-
const require2 = createRequire(import.meta.url);
|
|
313
|
-
const manifest = require2("../package.json");
|
|
314
|
-
if (manifest.version)
|
|
315
|
-
return manifest.version;
|
|
316
|
-
} catch {}
|
|
317
|
-
return "0.0.0";
|
|
318
|
-
}
|
|
319
|
-
|
|
320
1982
|
// src/cli.ts
|
|
321
1983
|
function usage() {
|
|
322
1984
|
return [
|
|
323
|
-
"usage:
|
|
1985
|
+
"usage:",
|
|
1986
|
+
" opencode-plugin-flow activation-check --project <absolute-path> [--target <exact-version>] [--json]",
|
|
1987
|
+
" opencode-plugin-flow activation-apply --project <absolute-path> --scope <global|project> [--target <exact-version>] [--apply] [--json]",
|
|
1988
|
+
" opencode-plugin-flow legacy-cleanup <--dry-run|--apply> [--json]",
|
|
324
1989
|
"",
|
|
325
1990
|
"commands:",
|
|
1991
|
+
" activation-check Inventory all OpenCode Flow activation sources and cache artifacts",
|
|
1992
|
+
" activation-apply Plan a single-version activation; mutate only with --apply",
|
|
326
1993
|
" legacy-cleanup Inspect or archive marker-proven legacy global Flow skills",
|
|
327
1994
|
"",
|
|
328
|
-
"options:",
|
|
1995
|
+
"activation options:",
|
|
1996
|
+
" --project <path> Absolute project/worktree path whose sources are inventoried",
|
|
1997
|
+
" --scope <scope> Config that receives the one canonical exact npm pin",
|
|
1998
|
+
" --target <version> Exact version only; defaults to this package's embedded version",
|
|
1999
|
+
" --apply Create backups/journal and apply the activation plan",
|
|
2000
|
+
" Without --apply, activation-apply is read-only",
|
|
2001
|
+
"",
|
|
2002
|
+
"legacy cleanup options:",
|
|
329
2003
|
" --dry-run Report eligible folders without changing the filesystem",
|
|
330
2004
|
" --apply Move eligible folders to a recoverable archive outside skill discovery",
|
|
331
2005
|
" Cleanup never deletes legacy folders",
|
|
2006
|
+
"",
|
|
2007
|
+
"common options:",
|
|
332
2008
|
" --json Write the report as JSON",
|
|
333
2009
|
" --help Show this help",
|
|
334
2010
|
" --version Print the plugin version"
|
|
335
2011
|
].join(`
|
|
336
2012
|
`);
|
|
337
2013
|
}
|
|
338
|
-
function
|
|
2014
|
+
function writeLegacyReport(report, json) {
|
|
339
2015
|
if (json) {
|
|
340
2016
|
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
341
2017
|
`);
|
|
@@ -360,38 +2036,223 @@ function writeReport(report, json) {
|
|
|
360
2036
|
}
|
|
361
2037
|
}
|
|
362
2038
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
2039
|
+
function writeActivationCheck(report, json) {
|
|
2040
|
+
if (json) {
|
|
2041
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
2042
|
+
`);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
process.stdout.write(`Flow activation check: ${report.singleVersionSatisfied ? "satisfied" : "not satisfied"}
|
|
2046
|
+
`);
|
|
2047
|
+
process.stdout.write(`- project: ${report.project}
|
|
2048
|
+
`);
|
|
2049
|
+
process.stdout.write(`- target: opencode-plugin-flow@${report.target}
|
|
2050
|
+
`);
|
|
2051
|
+
process.stdout.write(`- activation sources: ${report.records.length}
|
|
2052
|
+
`);
|
|
2053
|
+
for (const record of report.records) {
|
|
2054
|
+
process.stdout.write(` - ${record.source}: ${record.specifier} (${record.ownership}, ${record.status}, version=${record.resolvedVersion ?? "unresolved"})
|
|
2055
|
+
`);
|
|
2056
|
+
if (record.reason)
|
|
2057
|
+
process.stdout.write(` reason: ${record.reason}
|
|
2058
|
+
`);
|
|
2059
|
+
}
|
|
2060
|
+
process.stdout.write(`- Flow cache artifacts: ${report.cacheArtifacts.length}
|
|
2061
|
+
`);
|
|
2062
|
+
for (const artifact of report.cacheArtifacts) {
|
|
2063
|
+
process.stdout.write(` - ${artifact.specifier}: ${artifact.status} (version=${artifact.resolvedVersion ?? "unresolved"})
|
|
2064
|
+
`);
|
|
2065
|
+
if (artifact.reason)
|
|
2066
|
+
process.stdout.write(` reason: ${artifact.reason}
|
|
2067
|
+
`);
|
|
2068
|
+
}
|
|
2069
|
+
for (const limitation of report.limitations) {
|
|
2070
|
+
process.stdout.write(`- limitation (${limitation.coverage}): ${limitation.source}
|
|
2071
|
+
${limitation.detail}
|
|
2072
|
+
`);
|
|
2073
|
+
}
|
|
2074
|
+
for (const reason of report.reasons) {
|
|
2075
|
+
process.stdout.write(`- blocked: ${reason}
|
|
2076
|
+
`);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
function writeActivationApply(report, json) {
|
|
2080
|
+
if (json) {
|
|
2081
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
2082
|
+
`);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
process.stdout.write(`Flow activation ${report.mode}: ${report.status}
|
|
2086
|
+
`);
|
|
2087
|
+
process.stdout.write(`- project: ${report.project}
|
|
2088
|
+
`);
|
|
2089
|
+
process.stdout.write(`- canonical scope: ${report.scope}
|
|
2090
|
+
`);
|
|
2091
|
+
process.stdout.write(`- target: opencode-plugin-flow@${report.target}
|
|
2092
|
+
`);
|
|
2093
|
+
for (const operation of report.plan) {
|
|
2094
|
+
process.stdout.write(`- ${operation.action}: ${operation.path}
|
|
2095
|
+
${operation.detail}
|
|
2096
|
+
`);
|
|
2097
|
+
}
|
|
2098
|
+
for (const refusal of report.refusals) {
|
|
2099
|
+
process.stdout.write(`- refused: ${refusal}
|
|
2100
|
+
`);
|
|
2101
|
+
}
|
|
2102
|
+
if (report.recovery) {
|
|
2103
|
+
process.stdout.write(`- recovery journal: ${report.recovery.journalPath}
|
|
2104
|
+
`);
|
|
2105
|
+
}
|
|
2106
|
+
if (report.failure) {
|
|
2107
|
+
process.stdout.write(`- recovery state: ${report.failure.recoveryState}
|
|
2108
|
+
- failure: ${report.failure.message}
|
|
2109
|
+
`);
|
|
2110
|
+
for (const guidance of report.failure.guidance) {
|
|
2111
|
+
process.stdout.write(` recovery: ${guidance}
|
|
2112
|
+
`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (report.mode === "dry-run" && report.status === "ready") {
|
|
2116
|
+
process.stdout.write(`- no files changed; repeat with --apply to execute
|
|
2117
|
+
`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function parseActivationFlags(flags) {
|
|
2121
|
+
const parsed = {
|
|
2122
|
+
apply: false,
|
|
2123
|
+
json: false,
|
|
2124
|
+
help: false
|
|
2125
|
+
};
|
|
2126
|
+
const seen = new Set;
|
|
2127
|
+
for (let index = 0;index < flags.length; index += 1) {
|
|
2128
|
+
const flag = flags[index];
|
|
2129
|
+
if (!flag || seen.has(flag))
|
|
2130
|
+
return null;
|
|
2131
|
+
seen.add(flag);
|
|
2132
|
+
if (flag === "--apply") {
|
|
2133
|
+
parsed.apply = true;
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
if (flag === "--json") {
|
|
2137
|
+
parsed.json = true;
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
if (flag === "--help" || flag === "-h") {
|
|
2141
|
+
parsed.help = true;
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
if (!["--project", "--target", "--scope"].includes(flag))
|
|
2145
|
+
return null;
|
|
2146
|
+
const value = flags[index + 1];
|
|
2147
|
+
if (!value || value.startsWith("--"))
|
|
2148
|
+
return null;
|
|
2149
|
+
index += 1;
|
|
2150
|
+
if (flag === "--project")
|
|
2151
|
+
parsed.project = value;
|
|
2152
|
+
if (flag === "--target")
|
|
2153
|
+
parsed.target = value;
|
|
2154
|
+
if (flag === "--scope")
|
|
2155
|
+
parsed.scope = value;
|
|
2156
|
+
}
|
|
2157
|
+
return parsed;
|
|
2158
|
+
}
|
|
2159
|
+
function activationScope(value) {
|
|
2160
|
+
return value === "global" || value === "project" ? value : null;
|
|
2161
|
+
}
|
|
2162
|
+
async function runActivationCheck(flags) {
|
|
2163
|
+
const parsed = parseActivationFlags(flags);
|
|
2164
|
+
if (!parsed || parsed.apply || parsed.scope || !parsed.project && !parsed.help) {
|
|
2165
|
+
process.stderr.write(`${usage()}
|
|
2166
|
+
`);
|
|
2167
|
+
process.exitCode = 2;
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
if (parsed.help) {
|
|
366
2171
|
process.stdout.write(`${usage()}
|
|
367
2172
|
`);
|
|
368
2173
|
return;
|
|
369
2174
|
}
|
|
370
|
-
|
|
371
|
-
|
|
2175
|
+
const report = await checkFlowActivation({
|
|
2176
|
+
project: parsed.project,
|
|
2177
|
+
...parsed.target ? { target: parsed.target } : {}
|
|
2178
|
+
});
|
|
2179
|
+
writeActivationCheck(report, parsed.json);
|
|
2180
|
+
if (!report.singleVersionSatisfied)
|
|
2181
|
+
process.exitCode = 1;
|
|
2182
|
+
}
|
|
2183
|
+
async function runActivationApply(flags) {
|
|
2184
|
+
const parsed = parseActivationFlags(flags);
|
|
2185
|
+
const scope = activationScope(parsed?.scope);
|
|
2186
|
+
if (!parsed || !parsed.project && !parsed.help || !scope && !parsed.help) {
|
|
2187
|
+
process.stderr.write(`${usage()}
|
|
2188
|
+
`);
|
|
2189
|
+
process.exitCode = 2;
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (parsed.help) {
|
|
2193
|
+
process.stdout.write(`${usage()}
|
|
372
2194
|
`);
|
|
373
2195
|
return;
|
|
374
2196
|
}
|
|
2197
|
+
const report = await applyFlowActivation({
|
|
2198
|
+
project: parsed.project,
|
|
2199
|
+
scope,
|
|
2200
|
+
apply: parsed.apply,
|
|
2201
|
+
...parsed.target ? { target: parsed.target } : {}
|
|
2202
|
+
});
|
|
2203
|
+
writeActivationApply(report, parsed.json);
|
|
2204
|
+
if (report.status === "refused")
|
|
2205
|
+
process.exitCode = 1;
|
|
2206
|
+
}
|
|
2207
|
+
async function runLegacyCleanup(flags) {
|
|
375
2208
|
const knownFlags = new Set(["--dry-run", "--apply", "--json"]);
|
|
376
2209
|
const validFlags = flags.every((flag) => knownFlags.has(flag));
|
|
377
2210
|
const dryRun = flags.includes("--dry-run");
|
|
378
2211
|
const apply = flags.includes("--apply");
|
|
379
|
-
if (
|
|
2212
|
+
if (!validFlags || dryRun === apply) {
|
|
380
2213
|
process.stderr.write(`${usage()}
|
|
381
2214
|
`);
|
|
382
2215
|
process.exitCode = 2;
|
|
383
2216
|
return;
|
|
384
2217
|
}
|
|
385
2218
|
const report = await cleanupLegacySkills({ apply });
|
|
386
|
-
|
|
2219
|
+
writeLegacyReport(report, flags.includes("--json"));
|
|
387
2220
|
if (apply && report.results.some((result) => ["refused", "quarantined"].includes(result.status))) {
|
|
388
2221
|
process.exitCode = 1;
|
|
389
2222
|
}
|
|
390
2223
|
}
|
|
2224
|
+
async function main(argv) {
|
|
2225
|
+
const [command, ...flags] = argv.slice(2);
|
|
2226
|
+
if (command === "--help" || command === "-h") {
|
|
2227
|
+
process.stdout.write(`${usage()}
|
|
2228
|
+
`);
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (command === "--version" || command === "-v") {
|
|
2232
|
+
process.stdout.write(`${resolveFlowPluginVersion()}
|
|
2233
|
+
`);
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (command === "activation-check") {
|
|
2237
|
+
await runActivationCheck(flags);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (command === "activation-apply") {
|
|
2241
|
+
await runActivationApply(flags);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (command === "legacy-cleanup") {
|
|
2245
|
+
await runLegacyCleanup(flags);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
process.stderr.write(`${usage()}
|
|
2249
|
+
`);
|
|
2250
|
+
process.exitCode = 2;
|
|
2251
|
+
}
|
|
391
2252
|
main(process.argv).catch((error) => {
|
|
392
2253
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
393
2254
|
`);
|
|
394
2255
|
process.exitCode = 1;
|
|
395
2256
|
});
|
|
396
2257
|
|
|
397
|
-
//# debugId=
|
|
2258
|
+
//# debugId=963853A3888CECEA64756E2164756E21
|