ocx 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1494 -172
- package/dist/index.js.map +18 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
1
|
+
#!/usr/bin/env -S bun --no-env-file
|
|
2
2
|
// @bun
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
@@ -6098,7 +6098,7 @@ var package_default;
|
|
|
6098
6098
|
var init_package = __esm(() => {
|
|
6099
6099
|
package_default = {
|
|
6100
6100
|
name: "ocx",
|
|
6101
|
-
version: "2.0.
|
|
6101
|
+
version: "2.0.2",
|
|
6102
6102
|
description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
|
|
6103
6103
|
author: "kdcokenny",
|
|
6104
6104
|
license: "MIT",
|
|
@@ -6178,7 +6178,7 @@ var init_constants = __esm(() => {
|
|
|
6178
6178
|
});
|
|
6179
6179
|
|
|
6180
6180
|
// src/utils/errors.ts
|
|
6181
|
-
var EXIT_CODES, OCXError, NotFoundError, NetworkError, ConfigError, ValidationError, ConflictError, IntegrityError, SelfUpdateError, RegistryCompatibilityError, ProfileNotFoundError, ProfileExistsError, RegistryExistsError, InvalidProfileNameError, ProfilesNotInitializedError, LocalProfileUnsupportedError;
|
|
6181
|
+
var EXIT_CODES, OCXError, NotFoundError, NetworkError, ConfigError, ValidationError, ValidationFailedError, ConflictError, IntegrityError, SelfUpdateError, RegistryCompatibilityError, ProfileNotFoundError, ProfileExistsError, RegistryExistsError, InvalidProfileNameError, ProfilesNotInitializedError, LocalProfileUnsupportedError;
|
|
6182
6182
|
var init_errors2 = __esm(() => {
|
|
6183
6183
|
EXIT_CODES = {
|
|
6184
6184
|
SUCCESS: 0,
|
|
@@ -6209,12 +6209,20 @@ var init_errors2 = __esm(() => {
|
|
|
6209
6209
|
url;
|
|
6210
6210
|
status;
|
|
6211
6211
|
statusText;
|
|
6212
|
+
phase;
|
|
6213
|
+
qualifiedName;
|
|
6214
|
+
registryContext;
|
|
6215
|
+
registryName;
|
|
6212
6216
|
constructor(message, options) {
|
|
6213
6217
|
super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
|
|
6214
6218
|
this.name = "NetworkError";
|
|
6215
6219
|
this.url = options?.url;
|
|
6216
6220
|
this.status = options?.status;
|
|
6217
6221
|
this.statusText = options?.statusText;
|
|
6222
|
+
this.phase = options?.phase;
|
|
6223
|
+
this.qualifiedName = options?.qualifiedName;
|
|
6224
|
+
this.registryContext = options?.registryContext;
|
|
6225
|
+
this.registryName = options?.registryName;
|
|
6218
6226
|
}
|
|
6219
6227
|
};
|
|
6220
6228
|
ConfigError = class ConfigError extends OCXError {
|
|
@@ -6229,6 +6237,14 @@ var init_errors2 = __esm(() => {
|
|
|
6229
6237
|
this.name = "ValidationError";
|
|
6230
6238
|
}
|
|
6231
6239
|
};
|
|
6240
|
+
ValidationFailedError = class ValidationFailedError extends OCXError {
|
|
6241
|
+
details;
|
|
6242
|
+
constructor(details) {
|
|
6243
|
+
super("Registry validation failed", "VALIDATION_FAILED", EXIT_CODES.CONFIG);
|
|
6244
|
+
this.details = details;
|
|
6245
|
+
this.name = "ValidationFailedError";
|
|
6246
|
+
}
|
|
6247
|
+
};
|
|
6232
6248
|
ConflictError = class ConflictError extends OCXError {
|
|
6233
6249
|
constructor(message) {
|
|
6234
6250
|
super(message, "CONFLICT", EXIT_CODES.CONFLICT);
|
|
@@ -7060,24 +7076,29 @@ function adaptLegacyRegistryIndex(data, url) {
|
|
|
7060
7076
|
components: adaptedComponents
|
|
7061
7077
|
};
|
|
7062
7078
|
}
|
|
7063
|
-
async function fetchWithCache(
|
|
7064
|
-
const cached = cache.get(
|
|
7079
|
+
async function fetchWithCache(cacheKey, parse3, options = {}) {
|
|
7080
|
+
const cached = cache.get(cacheKey);
|
|
7065
7081
|
if (cached) {
|
|
7066
7082
|
return cached;
|
|
7067
7083
|
}
|
|
7084
|
+
const requestUrl = options.requestUrl ?? cacheKey;
|
|
7068
7085
|
const promise = (async () => {
|
|
7086
|
+
const networkErrorContext = {
|
|
7087
|
+
url: requestUrl,
|
|
7088
|
+
phase: options.phase
|
|
7089
|
+
};
|
|
7069
7090
|
let response;
|
|
7070
7091
|
try {
|
|
7071
|
-
response = await fetch(
|
|
7092
|
+
response = await fetch(requestUrl);
|
|
7072
7093
|
} catch (error) {
|
|
7073
|
-
throw new NetworkError(`Network request failed for ${
|
|
7094
|
+
throw new NetworkError(`Network request failed for ${requestUrl}: ${error instanceof Error ? error.message : String(error)}`, networkErrorContext);
|
|
7074
7095
|
}
|
|
7075
7096
|
if (!response.ok) {
|
|
7076
7097
|
if (response.status === 404) {
|
|
7077
|
-
throw new NotFoundError(`Not found: ${
|
|
7098
|
+
throw new NotFoundError(`Not found: ${requestUrl}`);
|
|
7078
7099
|
}
|
|
7079
|
-
throw new NetworkError(`Failed to fetch ${
|
|
7080
|
-
|
|
7100
|
+
throw new NetworkError(`Failed to fetch ${requestUrl}: ${response.status} ${response.statusText}`, {
|
|
7101
|
+
...networkErrorContext,
|
|
7081
7102
|
status: response.status,
|
|
7082
7103
|
statusText: response.statusText
|
|
7083
7104
|
});
|
|
@@ -7086,12 +7107,12 @@ async function fetchWithCache(url, parse3) {
|
|
|
7086
7107
|
try {
|
|
7087
7108
|
data = await response.json();
|
|
7088
7109
|
} catch (error) {
|
|
7089
|
-
throw new NetworkError(`Invalid JSON response from ${
|
|
7110
|
+
throw new NetworkError(`Invalid JSON response from ${requestUrl}: ${error instanceof Error ? error.message : String(error)}`, networkErrorContext);
|
|
7090
7111
|
}
|
|
7091
7112
|
return await parse3(data);
|
|
7092
7113
|
})();
|
|
7093
|
-
cache.set(
|
|
7094
|
-
promise.catch(() => cache.delete(
|
|
7114
|
+
cache.set(cacheKey, promise);
|
|
7115
|
+
promise.catch(() => cache.delete(cacheKey));
|
|
7095
7116
|
return promise;
|
|
7096
7117
|
}
|
|
7097
7118
|
function classifyRegistryIndexIssue(data) {
|
|
@@ -7156,7 +7177,7 @@ async function fetchRegistryIndex(baseUrl) {
|
|
|
7156
7177
|
}
|
|
7157
7178
|
registrySchemaModeCache.set(normalizedBaseUrl, schemaMode);
|
|
7158
7179
|
return result.data;
|
|
7159
|
-
});
|
|
7180
|
+
}, { phase: "registry-index-fetch" });
|
|
7160
7181
|
}
|
|
7161
7182
|
async function fetchComponent(baseUrl, name) {
|
|
7162
7183
|
const result = await fetchComponentVersion(baseUrl, name);
|
|
@@ -7202,18 +7223,27 @@ async function fetchComponentVersion(baseUrl, name, version) {
|
|
|
7202
7223
|
throw new ValidationError(`Invalid component manifest for "${name}@${resolvedVersion}": ${manifestResult.error.message}`);
|
|
7203
7224
|
}
|
|
7204
7225
|
return { manifest: manifestResult.data, version: resolvedVersion };
|
|
7205
|
-
});
|
|
7226
|
+
}, { phase: "packument-fetch", requestUrl: url });
|
|
7206
7227
|
}
|
|
7207
7228
|
async function fetchFileContent(baseUrl, componentName, filePath) {
|
|
7208
7229
|
const url = `${normalizeRegistryUrl(baseUrl)}/components/${componentName}/${filePath}`;
|
|
7230
|
+
const networkErrorContext = {
|
|
7231
|
+
url,
|
|
7232
|
+
phase: "file-content-fetch",
|
|
7233
|
+
qualifiedName: componentName
|
|
7234
|
+
};
|
|
7209
7235
|
let response;
|
|
7210
7236
|
try {
|
|
7211
7237
|
response = await fetch(url);
|
|
7212
7238
|
} catch (error) {
|
|
7213
|
-
throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`,
|
|
7239
|
+
throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, networkErrorContext);
|
|
7214
7240
|
}
|
|
7215
7241
|
if (!response.ok) {
|
|
7216
|
-
throw new NetworkError(`Failed to fetch file ${filePath} for ${componentName} from ${url}: ${response.status} ${response.statusText}`, {
|
|
7242
|
+
throw new NetworkError(`Failed to fetch file ${filePath} for ${componentName} from ${url}: ${response.status} ${response.statusText}`, {
|
|
7243
|
+
...networkErrorContext,
|
|
7244
|
+
status: response.status,
|
|
7245
|
+
statusText: response.statusText
|
|
7246
|
+
});
|
|
7217
7247
|
}
|
|
7218
7248
|
return response.text();
|
|
7219
7249
|
}
|
|
@@ -11376,6 +11406,9 @@ class GlobalConfigProvider {
|
|
|
11376
11406
|
throw new ConfigError("Global config not found. Run 'ocx init --global' first.");
|
|
11377
11407
|
}
|
|
11378
11408
|
const config = await readOcxConfig(basePath);
|
|
11409
|
+
if (!config) {
|
|
11410
|
+
throw new ConfigError("Global config not found. Run 'ocx init --global' first.");
|
|
11411
|
+
}
|
|
11379
11412
|
return new GlobalConfigProvider(basePath, config);
|
|
11380
11413
|
}
|
|
11381
11414
|
getRegistries() {
|
|
@@ -13300,6 +13333,14 @@ function formatErrorAsJson(error) {
|
|
|
13300
13333
|
details.status = error.status;
|
|
13301
13334
|
if (error.statusText)
|
|
13302
13335
|
details.statusText = error.statusText;
|
|
13336
|
+
if (error.phase)
|
|
13337
|
+
details.phase = error.phase;
|
|
13338
|
+
if (error.qualifiedName)
|
|
13339
|
+
details.qualifiedName = error.qualifiedName;
|
|
13340
|
+
if (error.registryContext)
|
|
13341
|
+
details.registryContext = error.registryContext;
|
|
13342
|
+
if (error.registryName)
|
|
13343
|
+
details.registryName = error.registryName;
|
|
13303
13344
|
return {
|
|
13304
13345
|
success: false,
|
|
13305
13346
|
error: {
|
|
@@ -13403,6 +13444,20 @@ function formatErrorAsJson(error) {
|
|
|
13403
13444
|
}
|
|
13404
13445
|
};
|
|
13405
13446
|
}
|
|
13447
|
+
if (error instanceof ValidationFailedError) {
|
|
13448
|
+
return {
|
|
13449
|
+
success: false,
|
|
13450
|
+
error: {
|
|
13451
|
+
code: error.code,
|
|
13452
|
+
message: error.message,
|
|
13453
|
+
details: error.details
|
|
13454
|
+
},
|
|
13455
|
+
exitCode: error.exitCode,
|
|
13456
|
+
meta: {
|
|
13457
|
+
timestamp: new Date().toISOString()
|
|
13458
|
+
}
|
|
13459
|
+
};
|
|
13460
|
+
}
|
|
13406
13461
|
if (error instanceof OCXError) {
|
|
13407
13462
|
return {
|
|
13408
13463
|
success: false,
|
|
@@ -15942,11 +15997,343 @@ async function updateOpencodePackageDeps(cwd, npmDeps, npmDevDeps, options2 = {}
|
|
|
15942
15997
|
|
|
15943
15998
|
// src/commands/build.ts
|
|
15944
15999
|
import { relative as relative5, resolve as resolve6 } from "path";
|
|
16000
|
+
|
|
16001
|
+
// src/lib/validators.ts
|
|
16002
|
+
import { join as join9, posix } from "path";
|
|
16003
|
+
init_registry();
|
|
16004
|
+
function formatJsoncParseError3(parseErrors) {
|
|
16005
|
+
if (parseErrors.length === 0) {
|
|
16006
|
+
return "Unknown parse error";
|
|
16007
|
+
}
|
|
16008
|
+
const firstError = parseErrors[0];
|
|
16009
|
+
if (!firstError) {
|
|
16010
|
+
return "Unknown parse error";
|
|
16011
|
+
}
|
|
16012
|
+
return `${printParseErrorCode(firstError.error)} at offset ${firstError.offset}`;
|
|
16013
|
+
}
|
|
16014
|
+
async function loadRegistrySource(sourcePath) {
|
|
16015
|
+
const jsoncFile = Bun.file(`${sourcePath}/registry.jsonc`);
|
|
16016
|
+
const jsonFile = Bun.file(`${sourcePath}/registry.json`);
|
|
16017
|
+
const jsoncExists = await jsoncFile.exists();
|
|
16018
|
+
const jsonExists = await jsonFile.exists();
|
|
16019
|
+
if (!jsoncExists && !jsonExists) {
|
|
16020
|
+
return {
|
|
16021
|
+
success: false,
|
|
16022
|
+
error: "No registry.jsonc or registry.json found in source directory",
|
|
16023
|
+
errorKind: "not_found"
|
|
16024
|
+
};
|
|
16025
|
+
}
|
|
16026
|
+
const registryFile = jsoncExists ? jsoncFile : jsonFile;
|
|
16027
|
+
const fileName = jsoncExists ? "registry.jsonc" : "registry.json";
|
|
16028
|
+
const content2 = await registryFile.text();
|
|
16029
|
+
const parseErrors = [];
|
|
16030
|
+
const data = parse2(content2, parseErrors, { allowTrailingComma: true });
|
|
16031
|
+
if (parseErrors.length > 0) {
|
|
16032
|
+
const errorDetail = formatJsoncParseError3(parseErrors);
|
|
16033
|
+
return {
|
|
16034
|
+
success: false,
|
|
16035
|
+
error: `Invalid JSONC in ${fileName}: ${errorDetail}`,
|
|
16036
|
+
errorKind: "parse_error"
|
|
16037
|
+
};
|
|
16038
|
+
}
|
|
16039
|
+
return {
|
|
16040
|
+
success: true,
|
|
16041
|
+
data
|
|
16042
|
+
};
|
|
16043
|
+
}
|
|
16044
|
+
function validateRegistrySchema(registryData, sourcePath) {
|
|
16045
|
+
const schemaIssue = classifyRegistrySchemaIssue(registryData);
|
|
16046
|
+
if (schemaIssue) {
|
|
16047
|
+
return {
|
|
16048
|
+
valid: false,
|
|
16049
|
+
errors: [`Schema compatibility issue: ${schemaIssue.issue} - ${schemaIssue.remediation}`]
|
|
16050
|
+
};
|
|
16051
|
+
}
|
|
16052
|
+
return validateRegistrySource(registryData, sourcePath);
|
|
16053
|
+
}
|
|
16054
|
+
function validateRegistrySource(registryData, _sourcePath) {
|
|
16055
|
+
const parseResult = registrySchema.safeParse(registryData);
|
|
16056
|
+
if (!parseResult.success) {
|
|
16057
|
+
const errors3 = parseResult.error.errors.map((e3) => `${e3.path.join(".")}: ${e3.message}`);
|
|
16058
|
+
return {
|
|
16059
|
+
valid: false,
|
|
16060
|
+
errors: errors3
|
|
16061
|
+
};
|
|
16062
|
+
}
|
|
16063
|
+
return {
|
|
16064
|
+
valid: true,
|
|
16065
|
+
errors: [],
|
|
16066
|
+
data: parseResult.data
|
|
16067
|
+
};
|
|
16068
|
+
}
|
|
16069
|
+
async function validateSourceFiles(registry, sourcePath) {
|
|
16070
|
+
const errors3 = [];
|
|
16071
|
+
for (const component of registry.components) {
|
|
16072
|
+
for (const rawFile of component.files) {
|
|
16073
|
+
const file = normalizeFile(rawFile, component.type);
|
|
16074
|
+
const sourceFilePath = join9(sourcePath, "files", file.path);
|
|
16075
|
+
if (!await Bun.file(sourceFilePath).exists()) {
|
|
16076
|
+
errors3.push(`${component.name}: Source file not found at ${file.path}`);
|
|
16077
|
+
}
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
return {
|
|
16081
|
+
valid: errors3.length === 0,
|
|
16082
|
+
errors: errors3
|
|
16083
|
+
};
|
|
16084
|
+
}
|
|
16085
|
+
function validateCircularDependencies(registry) {
|
|
16086
|
+
const errors3 = [];
|
|
16087
|
+
const componentMap = new Map(registry.components.map((c) => [c.name, c]));
|
|
16088
|
+
function detectCycle(componentName, visiting, visited, path7) {
|
|
16089
|
+
if (visiting.has(componentName)) {
|
|
16090
|
+
return [...path7, componentName].join(" -> ");
|
|
16091
|
+
}
|
|
16092
|
+
if (visited.has(componentName)) {
|
|
16093
|
+
return null;
|
|
16094
|
+
}
|
|
16095
|
+
const component = componentMap.get(componentName);
|
|
16096
|
+
if (!component) {
|
|
16097
|
+
return null;
|
|
16098
|
+
}
|
|
16099
|
+
visiting.add(componentName);
|
|
16100
|
+
path7.push(componentName);
|
|
16101
|
+
for (const dep of component.dependencies) {
|
|
16102
|
+
if (dep.includes("/")) {
|
|
16103
|
+
continue;
|
|
16104
|
+
}
|
|
16105
|
+
const cycle = detectCycle(dep, visiting, visited, path7);
|
|
16106
|
+
if (cycle) {
|
|
16107
|
+
return cycle;
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
16110
|
+
visiting.delete(componentName);
|
|
16111
|
+
visited.add(componentName);
|
|
16112
|
+
path7.pop();
|
|
16113
|
+
return null;
|
|
16114
|
+
}
|
|
16115
|
+
const globalVisited = new Set;
|
|
16116
|
+
for (const component of registry.components) {
|
|
16117
|
+
if (globalVisited.has(component.name)) {
|
|
16118
|
+
continue;
|
|
16119
|
+
}
|
|
16120
|
+
const cycle = detectCycle(component.name, new Set, globalVisited, []);
|
|
16121
|
+
if (cycle) {
|
|
16122
|
+
errors3.push(`Circular dependency detected: ${cycle}`);
|
|
16123
|
+
break;
|
|
16124
|
+
}
|
|
16125
|
+
}
|
|
16126
|
+
return {
|
|
16127
|
+
valid: errors3.length === 0,
|
|
16128
|
+
errors: errors3
|
|
16129
|
+
};
|
|
16130
|
+
}
|
|
16131
|
+
function validateDuplicateTargets(registry) {
|
|
16132
|
+
const errors3 = [];
|
|
16133
|
+
const targetMap = new Map;
|
|
16134
|
+
const canonicalizeTargetForComparison = (target) => {
|
|
16135
|
+
const normalizedUnicode = target.normalize("NFC");
|
|
16136
|
+
const unifiedSeparators = normalizedUnicode.replace(/\\/g, "/");
|
|
16137
|
+
const normalizedTarget = posix.normalize(unifiedSeparators);
|
|
16138
|
+
return normalizedTarget.replace(/^\.\//, "");
|
|
16139
|
+
};
|
|
16140
|
+
for (const component of registry.components) {
|
|
16141
|
+
for (const rawFile of component.files) {
|
|
16142
|
+
const file = normalizeFile(rawFile, component.type);
|
|
16143
|
+
const canonicalTarget = canonicalizeTargetForComparison(file.target);
|
|
16144
|
+
const existing = targetMap.get(canonicalTarget);
|
|
16145
|
+
if (existing) {
|
|
16146
|
+
errors3.push(`Duplicate target "${canonicalTarget}" in components "${existing.componentName}" and "${component.name}"`);
|
|
16147
|
+
} else {
|
|
16148
|
+
targetMap.set(canonicalTarget, {
|
|
16149
|
+
componentName: component.name
|
|
16150
|
+
});
|
|
16151
|
+
}
|
|
16152
|
+
}
|
|
16153
|
+
}
|
|
16154
|
+
return {
|
|
16155
|
+
valid: errors3.length === 0,
|
|
16156
|
+
errors: errors3
|
|
16157
|
+
};
|
|
16158
|
+
}
|
|
16159
|
+
async function* validateRegistryWithOptions(registry, sourcePath, options2 = {}) {
|
|
16160
|
+
const filesResult = await validateSourceFiles(registry, sourcePath);
|
|
16161
|
+
for (const error of filesResult.errors) {
|
|
16162
|
+
yield error;
|
|
16163
|
+
}
|
|
16164
|
+
const circularResult = validateCircularDependencies(registry);
|
|
16165
|
+
for (const error of circularResult.errors) {
|
|
16166
|
+
yield error;
|
|
16167
|
+
}
|
|
16168
|
+
if (!options2.skipDuplicateTargets) {
|
|
16169
|
+
const duplicateTargetsResult = validateDuplicateTargets(registry);
|
|
16170
|
+
for (const error of duplicateTargetsResult.errors) {
|
|
16171
|
+
yield error;
|
|
16172
|
+
}
|
|
16173
|
+
}
|
|
16174
|
+
}
|
|
16175
|
+
|
|
16176
|
+
// src/lib/validation-runner.ts
|
|
16177
|
+
async function runCompleteValidation(sourcePath, options2 = {}) {
|
|
16178
|
+
const loadResult = await loadRegistrySource(sourcePath);
|
|
16179
|
+
if (!loadResult.success) {
|
|
16180
|
+
return {
|
|
16181
|
+
success: false,
|
|
16182
|
+
errors: [loadResult.error || "Failed to load registry"],
|
|
16183
|
+
failureType: "load",
|
|
16184
|
+
loadErrorKind: loadResult.errorKind
|
|
16185
|
+
};
|
|
16186
|
+
}
|
|
16187
|
+
const schemaResult = validateRegistrySchema(loadResult.data, sourcePath);
|
|
16188
|
+
if (!schemaResult.valid) {
|
|
16189
|
+
return {
|
|
16190
|
+
success: false,
|
|
16191
|
+
errors: schemaResult.errors,
|
|
16192
|
+
failureType: "schema"
|
|
16193
|
+
};
|
|
16194
|
+
}
|
|
16195
|
+
const registry = schemaResult.data;
|
|
16196
|
+
if (!registry) {
|
|
16197
|
+
throw new Error("Registry validation succeeded but returned no parsed data");
|
|
16198
|
+
}
|
|
16199
|
+
const validationErrors = [];
|
|
16200
|
+
for await (const error of validateRegistryWithOptions(registry, sourcePath, options2)) {
|
|
16201
|
+
validationErrors.push(error);
|
|
16202
|
+
}
|
|
16203
|
+
if (validationErrors.length > 0) {
|
|
16204
|
+
return {
|
|
16205
|
+
success: false,
|
|
16206
|
+
errors: validationErrors,
|
|
16207
|
+
failureType: "rules"
|
|
16208
|
+
};
|
|
16209
|
+
}
|
|
16210
|
+
return {
|
|
16211
|
+
success: true,
|
|
16212
|
+
errors: [],
|
|
16213
|
+
registry
|
|
16214
|
+
};
|
|
16215
|
+
}
|
|
16216
|
+
|
|
16217
|
+
// src/commands/build.ts
|
|
16218
|
+
init_errors2();
|
|
16219
|
+
|
|
16220
|
+
// src/utils/validation-errors.ts
|
|
16221
|
+
function categorizeValidationErrors(errors3) {
|
|
16222
|
+
return {
|
|
16223
|
+
file: errors3.filter((e3) => e3.includes("Source file not found")),
|
|
16224
|
+
circular: errors3.filter((e3) => e3.includes("Circular dependency")),
|
|
16225
|
+
duplicate: errors3.filter((e3) => e3.includes("Duplicate target"))
|
|
16226
|
+
};
|
|
16227
|
+
}
|
|
16228
|
+
function summarizeValidationErrors(errors3, options2 = {}) {
|
|
16229
|
+
const categorized = categorizeValidationErrors(errors3);
|
|
16230
|
+
const schemaErrors = options2.schemaErrors ?? 0;
|
|
16231
|
+
const totalErrors = errors3.length;
|
|
16232
|
+
const otherErrors = Math.max(0, totalErrors - schemaErrors - categorized.file.length - categorized.circular.length - categorized.duplicate.length);
|
|
16233
|
+
return {
|
|
16234
|
+
valid: totalErrors === 0,
|
|
16235
|
+
totalErrors,
|
|
16236
|
+
schemaErrors,
|
|
16237
|
+
sourceFileErrors: categorized.file.length,
|
|
16238
|
+
circularDependencyErrors: categorized.circular.length,
|
|
16239
|
+
duplicateTargetErrors: categorized.duplicate.length,
|
|
16240
|
+
otherErrors
|
|
16241
|
+
};
|
|
16242
|
+
}
|
|
16243
|
+
|
|
16244
|
+
// src/commands/build.ts
|
|
16245
|
+
function createLoadValidationError(message, errorKind) {
|
|
16246
|
+
if (errorKind === "not_found") {
|
|
16247
|
+
return new NotFoundError(message);
|
|
16248
|
+
}
|
|
16249
|
+
if (errorKind === "parse_error") {
|
|
16250
|
+
return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
16251
|
+
}
|
|
16252
|
+
return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
16253
|
+
}
|
|
16254
|
+
function createValidationFailureError(errors3, failureType) {
|
|
16255
|
+
const summary = summarizeValidationErrors(errors3, {
|
|
16256
|
+
schemaErrors: failureType === "schema" ? errors3.length : 0
|
|
16257
|
+
});
|
|
16258
|
+
const details = {
|
|
16259
|
+
valid: false,
|
|
16260
|
+
errors: errors3,
|
|
16261
|
+
summary: {
|
|
16262
|
+
valid: false,
|
|
16263
|
+
totalErrors: summary.totalErrors,
|
|
16264
|
+
schemaErrors: summary.schemaErrors,
|
|
16265
|
+
sourceFileErrors: summary.sourceFileErrors,
|
|
16266
|
+
circularDependencyErrors: summary.circularDependencyErrors,
|
|
16267
|
+
duplicateTargetErrors: summary.duplicateTargetErrors,
|
|
16268
|
+
otherErrors: summary.otherErrors
|
|
16269
|
+
}
|
|
16270
|
+
};
|
|
16271
|
+
return new ValidationFailedError(details);
|
|
16272
|
+
}
|
|
16273
|
+
function outputValidationFailures(errors3, failureType) {
|
|
16274
|
+
if (failureType === "schema") {
|
|
16275
|
+
logger.error("\u2717 Registry schema");
|
|
16276
|
+
for (const error of errors3) {
|
|
16277
|
+
console.log(kleur_default.red(` ${error}`));
|
|
16278
|
+
}
|
|
16279
|
+
return;
|
|
16280
|
+
}
|
|
16281
|
+
const categorized = categorizeValidationErrors(errors3);
|
|
16282
|
+
if (categorized.file.length > 0) {
|
|
16283
|
+
logger.error("\u2717 Source files");
|
|
16284
|
+
for (const error of categorized.file) {
|
|
16285
|
+
console.log(kleur_default.red(` ${error}`));
|
|
16286
|
+
}
|
|
16287
|
+
}
|
|
16288
|
+
if (categorized.circular.length > 0) {
|
|
16289
|
+
logger.error("\u2717 Circular dependencies");
|
|
16290
|
+
for (const error of categorized.circular) {
|
|
16291
|
+
console.log(kleur_default.red(` ${error}`));
|
|
16292
|
+
}
|
|
16293
|
+
}
|
|
16294
|
+
if (categorized.duplicate.length > 0) {
|
|
16295
|
+
logger.error("\u2717 Duplicate targets");
|
|
16296
|
+
for (const error of categorized.duplicate) {
|
|
16297
|
+
console.log(kleur_default.red(` ${error}`));
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
15945
16301
|
function registerBuildCommand(program2) {
|
|
15946
|
-
program2.command("build").description("Build a registry from source (for registry authors)").argument("[path]", "Registry source directory", ".").option("--out <dir>", "Output directory", "./dist").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output as JSON", false).option("-q, --quiet", "Suppress output", false).option("--dry-run", "Validate and show what would be built").action(async (path7, options2) => {
|
|
16302
|
+
program2.command("build").description("Build a registry from source (for registry authors)").argument("[path]", "Registry source directory", ".").option("--out <dir>", "Output directory", "./dist").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output as JSON", false).option("-q, --quiet", "Suppress output", false).option("--dry-run", "Validate and show what would be built").option("--show-validation", "Display validation results before building", false).action(async (path7, options2) => {
|
|
15947
16303
|
try {
|
|
15948
16304
|
const sourcePath = resolve6(options2.cwd, path7);
|
|
15949
16305
|
const outPath = resolve6(options2.cwd, options2.out);
|
|
16306
|
+
if (options2.showValidation) {
|
|
16307
|
+
const shouldDisplayValidation = !options2.json && !options2.quiet;
|
|
16308
|
+
if (shouldDisplayValidation) {
|
|
16309
|
+
logger.info("Running validation checks...");
|
|
16310
|
+
}
|
|
16311
|
+
const validationResult = await runCompleteValidation(sourcePath, {
|
|
16312
|
+
skipDuplicateTargets: false
|
|
16313
|
+
});
|
|
16314
|
+
if (!validationResult.success) {
|
|
16315
|
+
const [firstError = "Registry validation failed"] = validationResult.errors;
|
|
16316
|
+
if (validationResult.failureType === "load") {
|
|
16317
|
+
throw createLoadValidationError(firstError, validationResult.loadErrorKind);
|
|
16318
|
+
}
|
|
16319
|
+
const failureType = validationResult.failureType === "schema" ? "schema" : "rules";
|
|
16320
|
+
const validationError = createValidationFailureError(validationResult.errors, failureType);
|
|
16321
|
+
if (options2.json) {
|
|
16322
|
+
throw validationError;
|
|
16323
|
+
}
|
|
16324
|
+
if (!options2.quiet) {
|
|
16325
|
+
outputValidationFailures(validationResult.errors, failureType);
|
|
16326
|
+
}
|
|
16327
|
+
process.exit(validationError.exitCode);
|
|
16328
|
+
}
|
|
16329
|
+
if (shouldDisplayValidation) {
|
|
16330
|
+
logger.success("\u2713 Schema compatibility and structure");
|
|
16331
|
+
logger.success("\u2713 Source files");
|
|
16332
|
+
logger.success("\u2713 No circular dependencies");
|
|
16333
|
+
logger.success("\u2713 No duplicate targets");
|
|
16334
|
+
console.log("");
|
|
16335
|
+
}
|
|
16336
|
+
}
|
|
15950
16337
|
const spinner2 = createSpinner({
|
|
15951
16338
|
text: "Building registry...",
|
|
15952
16339
|
quiet: options2.quiet || options2.json
|
|
@@ -15983,7 +16370,10 @@ function registerBuildCommand(program2) {
|
|
|
15983
16370
|
}
|
|
15984
16371
|
} catch (error) {
|
|
15985
16372
|
if (error instanceof BuildRegistryError) {
|
|
15986
|
-
if (
|
|
16373
|
+
if (options2.json) {
|
|
16374
|
+
handleError(error, { json: true });
|
|
16375
|
+
}
|
|
16376
|
+
if (!options2.quiet) {
|
|
15987
16377
|
logger.error(error.message);
|
|
15988
16378
|
for (const err of error.errors) {
|
|
15989
16379
|
console.log(kleur_default.red(` ${err}`));
|
|
@@ -15999,7 +16389,7 @@ function registerBuildCommand(program2) {
|
|
|
15999
16389
|
// src/commands/config/edit.ts
|
|
16000
16390
|
import { existsSync as existsSync11 } from "fs";
|
|
16001
16391
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
16002
|
-
import { join as
|
|
16392
|
+
import { join as join10 } from "path";
|
|
16003
16393
|
init_errors2();
|
|
16004
16394
|
function registerConfigEditCommand(parent) {
|
|
16005
16395
|
parent.command("edit").description("Open configuration file in editor").option("-g, --global", "Edit global ocx.jsonc").option("-p, --profile <name>", "Edit specific profile's config").action(async (options2) => {
|
|
@@ -16035,11 +16425,11 @@ Run 'ocx init --global' first.`);
|
|
|
16035
16425
|
} else {
|
|
16036
16426
|
const localConfigDir = findLocalConfigDir(process.cwd());
|
|
16037
16427
|
if (localConfigDir) {
|
|
16038
|
-
configPath =
|
|
16428
|
+
configPath = join10(localConfigDir, OCX_CONFIG_FILE);
|
|
16039
16429
|
} else {
|
|
16040
|
-
const newConfigDir =
|
|
16430
|
+
const newConfigDir = join10(process.cwd(), LOCAL_CONFIG_DIR2);
|
|
16041
16431
|
await mkdir6(newConfigDir, { recursive: true });
|
|
16042
|
-
configPath =
|
|
16432
|
+
configPath = join10(newConfigDir, OCX_CONFIG_FILE);
|
|
16043
16433
|
if (!existsSync11(configPath)) {
|
|
16044
16434
|
const defaultConfig = {
|
|
16045
16435
|
$schema: "https://ocx.kdco.dev/schemas/ocx.json",
|
|
@@ -16171,7 +16561,7 @@ function registerConfigCommand(program2) {
|
|
|
16171
16561
|
init_constants();
|
|
16172
16562
|
import { existsSync as existsSync12 } from "fs";
|
|
16173
16563
|
import { cp, mkdir as mkdir7, readdir as readdir2, readFile, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
16174
|
-
import { dirname as dirname4, join as
|
|
16564
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
16175
16565
|
init_errors2();
|
|
16176
16566
|
var TEMPLATE_REPO = "kdcokenny/ocx";
|
|
16177
16567
|
var TEMPLATE_PATH = "examples/registry-starter";
|
|
@@ -16381,7 +16771,7 @@ async function copyDir(src, dest) {
|
|
|
16381
16771
|
}
|
|
16382
16772
|
function getReleaseTag() {
|
|
16383
16773
|
if (false) {}
|
|
16384
|
-
return `v${"2.0.
|
|
16774
|
+
return `v${"2.0.2"}`;
|
|
16385
16775
|
}
|
|
16386
16776
|
function getTemplateUrl(version) {
|
|
16387
16777
|
const ref = version === "main" ? "heads/main" : `tags/${version}`;
|
|
@@ -16396,10 +16786,10 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
|
|
|
16396
16786
|
if (!response.ok || !response.body) {
|
|
16397
16787
|
throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
|
|
16398
16788
|
}
|
|
16399
|
-
const tempDir =
|
|
16789
|
+
const tempDir = join11(destDir, ".ocx-temp");
|
|
16400
16790
|
await mkdir7(tempDir, { recursive: true });
|
|
16401
16791
|
try {
|
|
16402
|
-
const tarPath =
|
|
16792
|
+
const tarPath = join11(tempDir, "template.tar.gz");
|
|
16403
16793
|
const arrayBuffer = await response.arrayBuffer();
|
|
16404
16794
|
await writeFile2(tarPath, Buffer.from(arrayBuffer));
|
|
16405
16795
|
const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
|
|
@@ -16416,7 +16806,7 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
|
|
|
16416
16806
|
if (!extractedDir) {
|
|
16417
16807
|
throw new Error("Failed to find extracted template directory");
|
|
16418
16808
|
}
|
|
16419
|
-
const templateDir =
|
|
16809
|
+
const templateDir = join11(tempDir, extractedDir, TEMPLATE_PATH);
|
|
16420
16810
|
await copyDir(templateDir, destDir);
|
|
16421
16811
|
} finally {
|
|
16422
16812
|
await rm4(tempDir, { recursive: true, force: true });
|
|
@@ -16434,7 +16824,7 @@ async function replacePlaceholders(dir, values) {
|
|
|
16434
16824
|
"AGENTS.md"
|
|
16435
16825
|
];
|
|
16436
16826
|
for (const file of filesToProcess) {
|
|
16437
|
-
const filePath =
|
|
16827
|
+
const filePath = join11(dir, file);
|
|
16438
16828
|
if (!existsSync12(filePath))
|
|
16439
16829
|
continue;
|
|
16440
16830
|
let content2 = await readFile(filePath).then((b) => b.toString());
|
|
@@ -16456,7 +16846,7 @@ async function replacePlaceholders(dir, values) {
|
|
|
16456
16846
|
// src/commands/migrate/index.ts
|
|
16457
16847
|
import { existsSync as existsSync13, readdirSync, readFileSync as readFileSync3, statSync as statSync4 } from "fs";
|
|
16458
16848
|
import { rename as rename4, writeFile as writeFile3 } from "fs/promises";
|
|
16459
|
-
import { join as
|
|
16849
|
+
import { join as join12 } from "path";
|
|
16460
16850
|
|
|
16461
16851
|
// src/commands/migrate/transform.ts
|
|
16462
16852
|
init_errors2();
|
|
@@ -16630,12 +17020,12 @@ async function runGlobalMigrate(options2) {
|
|
|
16630
17020
|
function discoverGlobalTargets(globalRoot) {
|
|
16631
17021
|
const targets = [];
|
|
16632
17022
|
targets.push({ label: "root", path: globalRoot });
|
|
16633
|
-
const profilesDir =
|
|
17023
|
+
const profilesDir = join12(globalRoot, "profiles");
|
|
16634
17024
|
if (existsSync13(profilesDir) && statSync4(profilesDir).isDirectory()) {
|
|
16635
17025
|
const entries = readdirSync(profilesDir, { withFileTypes: true });
|
|
16636
17026
|
const profileNames = entries.filter((e3) => e3.isDirectory() && !e3.name.startsWith(".")).map((e3) => e3.name).sort();
|
|
16637
17027
|
for (const name of profileNames) {
|
|
16638
|
-
targets.push({ label: `profile:${name}`, path:
|
|
17028
|
+
targets.push({ label: `profile:${name}`, path: join12(profilesDir, name) });
|
|
16639
17029
|
}
|
|
16640
17030
|
}
|
|
16641
17031
|
return targets;
|
|
@@ -17104,6 +17494,9 @@ async function applyConfigNormalizationToFile(root, actions) {
|
|
|
17104
17494
|
await writeFile3(configInfo.path, content2);
|
|
17105
17495
|
}
|
|
17106
17496
|
|
|
17497
|
+
// src/commands/opencode.ts
|
|
17498
|
+
import * as path8 from "path";
|
|
17499
|
+
|
|
17107
17500
|
// src/utils/terminal-title.ts
|
|
17108
17501
|
import path7 from "path";
|
|
17109
17502
|
var MAX_BRANCH_LENGTH = 20;
|
|
@@ -17156,51 +17549,704 @@ function formatTerminalName(cwd, profileName, gitInfo) {
|
|
|
17156
17549
|
return `ocx[${profileName}]:${repoName}/${branch}`;
|
|
17157
17550
|
}
|
|
17158
17551
|
|
|
17159
|
-
// src/commands/opencode.ts
|
|
17160
|
-
|
|
17161
|
-
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17552
|
+
// src/commands/opencode-overlay.ts
|
|
17553
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
17554
|
+
import {
|
|
17555
|
+
copyFile,
|
|
17556
|
+
cp as cp2,
|
|
17557
|
+
lstat as lstat2,
|
|
17558
|
+
mkdir as mkdir8,
|
|
17559
|
+
mkdtemp,
|
|
17560
|
+
readdir as readdir3,
|
|
17561
|
+
readFile as readFile2,
|
|
17562
|
+
realpath,
|
|
17563
|
+
rename as rename5,
|
|
17564
|
+
rm as rm5,
|
|
17565
|
+
unlink as unlink3
|
|
17566
|
+
} from "fs/promises";
|
|
17567
|
+
import { tmpdir } from "os";
|
|
17568
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute7, join as join13, relative as relative6 } from "path";
|
|
17569
|
+
var {Glob: Glob4 } = globalThis.Bun;
|
|
17570
|
+
init_zod();
|
|
17571
|
+
init_errors2();
|
|
17572
|
+
init_path_security();
|
|
17573
|
+
var OPENCODE_OVERLAY_SOURCE_SCOPES = ["agent", "agents", "skill", "skills"];
|
|
17574
|
+
var OPENCODE_MERGED_DIR_PREFIX = "ocx-oc-merged-";
|
|
17575
|
+
var OVERLAY_TRANSACTION_MANIFEST_VERSION = 1;
|
|
17576
|
+
var OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE = "Full TOCTOU/symlink-swap hardening requires an fd-based native helper transaction.";
|
|
17577
|
+
function createOpencodeOcError(errorClass, detail) {
|
|
17578
|
+
return new ConfigError(`ocx oc ${errorClass} error: ${detail}`);
|
|
17579
|
+
}
|
|
17580
|
+
function formatUnknownError(error) {
|
|
17581
|
+
if (error instanceof Error) {
|
|
17582
|
+
return error.message;
|
|
17169
17583
|
}
|
|
17170
|
-
return
|
|
17584
|
+
return String(error);
|
|
17171
17585
|
}
|
|
17172
|
-
function
|
|
17173
|
-
|
|
17586
|
+
function formatJsoncParseError4(parseErrors) {
|
|
17587
|
+
if (parseErrors.length === 0) {
|
|
17588
|
+
return "Unknown parse error";
|
|
17589
|
+
}
|
|
17590
|
+
const firstError = parseErrors[0];
|
|
17591
|
+
if (!firstError) {
|
|
17592
|
+
return "Unknown parse error";
|
|
17593
|
+
}
|
|
17594
|
+
return `${printParseErrorCode(firstError.error)} at offset ${firstError.offset}`;
|
|
17174
17595
|
}
|
|
17175
|
-
function
|
|
17176
|
-
|
|
17596
|
+
function toPosixPath(pathValue) {
|
|
17597
|
+
return pathValue.replaceAll("\\", "/");
|
|
17598
|
+
}
|
|
17599
|
+
function normalizeGlobPattern(pattern) {
|
|
17600
|
+
return pattern.startsWith("./") ? pattern.slice(2) : pattern;
|
|
17601
|
+
}
|
|
17602
|
+
function isPathWithin(parentPath, childPath) {
|
|
17603
|
+
const relativePath = relative6(parentPath, childPath);
|
|
17604
|
+
if (relativePath.length === 0) {
|
|
17605
|
+
return true;
|
|
17606
|
+
}
|
|
17607
|
+
return !relativePath.startsWith("..") && !isAbsolute7(relativePath);
|
|
17608
|
+
}
|
|
17609
|
+
function getOverlayEntryType(stats) {
|
|
17610
|
+
if (stats.isFile()) {
|
|
17611
|
+
return "file";
|
|
17612
|
+
}
|
|
17613
|
+
if (stats.isDirectory()) {
|
|
17614
|
+
return "directory";
|
|
17615
|
+
}
|
|
17616
|
+
if (stats.isSymbolicLink()) {
|
|
17617
|
+
return "symlink";
|
|
17618
|
+
}
|
|
17619
|
+
return "other";
|
|
17620
|
+
}
|
|
17621
|
+
function captureOverlaySnapshot(stats) {
|
|
17177
17622
|
return {
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
|
|
17181
|
-
|
|
17182
|
-
|
|
17623
|
+
entryType: getOverlayEntryType(stats),
|
|
17624
|
+
device: String(stats.dev),
|
|
17625
|
+
inode: String(stats.ino),
|
|
17626
|
+
mode: String(stats.mode),
|
|
17627
|
+
size: String(stats.size),
|
|
17628
|
+
mtimeMs: String(stats.mtimeMs)
|
|
17183
17629
|
};
|
|
17184
17630
|
}
|
|
17185
|
-
function
|
|
17186
|
-
|
|
17631
|
+
function overlaySnapshotIdentityMatches(expected, actual) {
|
|
17632
|
+
return expected.entryType === actual.entryType && expected.device === actual.device && expected.inode === actual.inode && expected.mode === actual.mode;
|
|
17633
|
+
}
|
|
17634
|
+
function overlaySnapshotContentMatches(expected, actual) {
|
|
17635
|
+
return expected.size === actual.size && expected.mtimeMs === actual.mtimeMs;
|
|
17636
|
+
}
|
|
17637
|
+
async function assertPathSnapshotUnchanged(options2) {
|
|
17638
|
+
let currentStats;
|
|
17639
|
+
try {
|
|
17640
|
+
currentStats = await lstat2(options2.absolutePath);
|
|
17641
|
+
} catch {
|
|
17642
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17643
|
+
}
|
|
17644
|
+
if (options2.mustRemainDirectory && !currentStats.isDirectory()) {
|
|
17645
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17646
|
+
}
|
|
17647
|
+
if (options2.mustRemainFile && !currentStats.isFile()) {
|
|
17648
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17649
|
+
}
|
|
17650
|
+
const currentSnapshot = captureOverlaySnapshot(currentStats);
|
|
17651
|
+
const identityMatches = overlaySnapshotIdentityMatches(options2.expectedSnapshot, currentSnapshot);
|
|
17652
|
+
const shouldCompareContent = options2.compareContent ?? true;
|
|
17653
|
+
const contentMatches = !shouldCompareContent ? true : overlaySnapshotContentMatches(options2.expectedSnapshot, currentSnapshot);
|
|
17654
|
+
if (!identityMatches || !contentMatches) {
|
|
17655
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17656
|
+
}
|
|
17657
|
+
}
|
|
17658
|
+
async function assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, destinationRelativePath) {
|
|
17659
|
+
const relativeDestinationPath = toPosixPath(relative6(mergedConfigDir, destinationPath));
|
|
17660
|
+
const pathComponents = relativeDestinationPath.split("/").filter((component) => component.length > 0);
|
|
17661
|
+
let currentPath = mergedConfigDir;
|
|
17662
|
+
for (const component of pathComponents) {
|
|
17663
|
+
currentPath = join13(currentPath, component);
|
|
17664
|
+
let currentStats;
|
|
17187
17665
|
try {
|
|
17188
|
-
await
|
|
17666
|
+
currentStats = await lstat2(currentPath);
|
|
17189
17667
|
} catch (error) {
|
|
17190
|
-
|
|
17668
|
+
const errorCode = error.code;
|
|
17669
|
+
if (errorCode === "ENOENT") {
|
|
17670
|
+
return;
|
|
17671
|
+
}
|
|
17672
|
+
throw createOpencodeOcError("validate", `Failed to inspect overlay destination path (${destinationRelativePath}): ${formatUnknownError(error)}`);
|
|
17191
17673
|
}
|
|
17192
|
-
|
|
17674
|
+
if (currentStats.isSymbolicLink()) {
|
|
17675
|
+
throw createOpencodeOcError("validate", `Overlay destination path contains existing symlink (${destinationRelativePath})`);
|
|
17676
|
+
}
|
|
17677
|
+
}
|
|
17193
17678
|
}
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17679
|
+
var projectOverlayPolicySchema = exports_external.object({
|
|
17680
|
+
include: exports_external.array(exports_external.string()).optional(),
|
|
17681
|
+
exclude: exports_external.array(exports_external.string()).optional()
|
|
17682
|
+
}).passthrough();
|
|
17683
|
+
function validatePolicyPatterns(policyPath, field, patterns) {
|
|
17684
|
+
for (const pattern of patterns) {
|
|
17685
|
+
try {
|
|
17686
|
+
new Glob4(normalizeGlobPattern(pattern));
|
|
17687
|
+
} catch {
|
|
17688
|
+
throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: ${field} contains invalid glob pattern "${pattern}"`);
|
|
17689
|
+
}
|
|
17201
17690
|
}
|
|
17202
|
-
|
|
17203
|
-
|
|
17691
|
+
}
|
|
17692
|
+
async function loadProjectOverlayPolicy(localConfigDir) {
|
|
17693
|
+
if (!localConfigDir) {
|
|
17694
|
+
return { include: [], exclude: [] };
|
|
17695
|
+
}
|
|
17696
|
+
const policyPath = join13(localConfigDir, OCX_CONFIG_FILE);
|
|
17697
|
+
const policyReadPath = await resolveProjectOverlayPolicyReadPath(localConfigDir, policyPath);
|
|
17698
|
+
if (!policyReadPath) {
|
|
17699
|
+
return { include: [], exclude: [] };
|
|
17700
|
+
}
|
|
17701
|
+
let policyText;
|
|
17702
|
+
try {
|
|
17703
|
+
policyText = await readFile2(policyReadPath, "utf8");
|
|
17704
|
+
} catch (error) {
|
|
17705
|
+
throw createOpencodeOcError("read", `Failed to read project overlay policy at ${policyPath}: ${formatUnknownError(error)}`);
|
|
17706
|
+
}
|
|
17707
|
+
const parseErrors = [];
|
|
17708
|
+
const parsedPolicy = parse2(policyText, parseErrors, { allowTrailingComma: true });
|
|
17709
|
+
if (parseErrors.length > 0) {
|
|
17710
|
+
throw createOpencodeOcError("parse", `Failed to parse project overlay policy at ${policyPath}: ${formatJsoncParseError4(parseErrors)}`);
|
|
17711
|
+
}
|
|
17712
|
+
if (!parsedPolicy || typeof parsedPolicy !== "object" || Array.isArray(parsedPolicy)) {
|
|
17713
|
+
throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: root must be an object`);
|
|
17714
|
+
}
|
|
17715
|
+
const parsedResult = projectOverlayPolicySchema.safeParse(parsedPolicy);
|
|
17716
|
+
if (!parsedResult.success) {
|
|
17717
|
+
const firstIssue = parsedResult.error.issues[0];
|
|
17718
|
+
const issuePath = firstIssue?.path.length ? firstIssue.path.join(".") : "root";
|
|
17719
|
+
const issueMessage = firstIssue?.message ?? "Invalid project overlay policy";
|
|
17720
|
+
throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: ${issuePath} ${issueMessage}`);
|
|
17721
|
+
}
|
|
17722
|
+
const include = parsedResult.data.include ?? [];
|
|
17723
|
+
const exclude = parsedResult.data.exclude ?? [];
|
|
17724
|
+
validatePolicyPatterns(policyPath, "include", include);
|
|
17725
|
+
validatePolicyPatterns(policyPath, "exclude", exclude);
|
|
17726
|
+
return { include, exclude };
|
|
17727
|
+
}
|
|
17728
|
+
async function resolveProjectOverlayPolicyReadPath(localConfigDir, policyPath) {
|
|
17729
|
+
let policyStats;
|
|
17730
|
+
try {
|
|
17731
|
+
policyStats = await lstat2(policyPath);
|
|
17732
|
+
} catch (error) {
|
|
17733
|
+
const errorCode = error.code;
|
|
17734
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17735
|
+
return null;
|
|
17736
|
+
}
|
|
17737
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay policy at ${policyPath}: ${formatUnknownError(error)}`);
|
|
17738
|
+
}
|
|
17739
|
+
if (!policyStats.isSymbolicLink()) {
|
|
17740
|
+
return policyPath;
|
|
17741
|
+
}
|
|
17742
|
+
let projectConfigRealPath;
|
|
17743
|
+
try {
|
|
17744
|
+
projectConfigRealPath = await realpath(localConfigDir);
|
|
17745
|
+
} catch (error) {
|
|
17746
|
+
throw createOpencodeOcError("validate", `Unable to resolve project config directory ${localConfigDir}: ${formatUnknownError(error)}`);
|
|
17747
|
+
}
|
|
17748
|
+
let policyTargetRealPath;
|
|
17749
|
+
try {
|
|
17750
|
+
policyTargetRealPath = await realpath(policyPath);
|
|
17751
|
+
} catch (error) {
|
|
17752
|
+
const errorCode = error.code;
|
|
17753
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17754
|
+
throw createOpencodeOcError("validate", `Broken symlink in project overlay policy: ${policyPath}`);
|
|
17755
|
+
}
|
|
17756
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay policy symlink at ${policyPath}: ${formatUnknownError(error)}`);
|
|
17757
|
+
}
|
|
17758
|
+
if (!isPathWithin(projectConfigRealPath, policyTargetRealPath)) {
|
|
17759
|
+
throw createOpencodeOcError("validate", `Symlink escapes project overlay policy scope: ${policyPath}`);
|
|
17760
|
+
}
|
|
17761
|
+
return policyTargetRealPath;
|
|
17762
|
+
}
|
|
17763
|
+
function shouldIncludeOverlayPath(pathValue, policy) {
|
|
17764
|
+
for (const pattern of policy.include) {
|
|
17765
|
+
const glob = new Glob4(normalizeGlobPattern(pattern));
|
|
17766
|
+
if (glob.match(pathValue)) {
|
|
17767
|
+
return true;
|
|
17768
|
+
}
|
|
17769
|
+
}
|
|
17770
|
+
for (const pattern of policy.exclude) {
|
|
17771
|
+
const glob = new Glob4(normalizeGlobPattern(pattern));
|
|
17772
|
+
if (glob.match(pathValue)) {
|
|
17773
|
+
return false;
|
|
17774
|
+
}
|
|
17775
|
+
}
|
|
17776
|
+
return true;
|
|
17777
|
+
}
|
|
17778
|
+
function planOverlayCopyOperations(candidates, policy) {
|
|
17779
|
+
return candidates.filter((candidate) => shouldIncludeOverlayPath(candidate.overlayRelativePath, policy)).map((candidate) => ({
|
|
17780
|
+
sourcePath: candidate.sourcePath,
|
|
17781
|
+
destinationRelativePath: candidate.overlayRelativePath,
|
|
17782
|
+
sourceSnapshot: candidate.sourceSnapshot
|
|
17783
|
+
}));
|
|
17784
|
+
}
|
|
17785
|
+
async function rejectSymlinkEntry(projectConfigRealPath, absolutePath, overlayRelativePath) {
|
|
17786
|
+
let targetRealPath;
|
|
17787
|
+
try {
|
|
17788
|
+
targetRealPath = await realpath(absolutePath);
|
|
17789
|
+
} catch (error) {
|
|
17790
|
+
const errorCode = error.code;
|
|
17791
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17792
|
+
throw createOpencodeOcError("validate", `Broken symlink in project overlay scope: ${overlayRelativePath}`);
|
|
17793
|
+
}
|
|
17794
|
+
throw createOpencodeOcError("read", `Failed to inspect symlink at ${overlayRelativePath}: ${formatUnknownError(error)}`);
|
|
17795
|
+
}
|
|
17796
|
+
if (!isPathWithin(projectConfigRealPath, targetRealPath)) {
|
|
17797
|
+
throw createOpencodeOcError("validate", `Symlink escapes project overlay scope: ${overlayRelativePath}`);
|
|
17798
|
+
}
|
|
17799
|
+
throw createOpencodeOcError("validate", `Symlink entries are not supported in project overlay scope: ${overlayRelativePath}`);
|
|
17800
|
+
}
|
|
17801
|
+
async function collectOverlayCandidatesFromPath(projectConfigRealPath, absPath, overlayRelativePath, collector, seams) {
|
|
17802
|
+
let stats;
|
|
17803
|
+
try {
|
|
17804
|
+
stats = await lstat2(absPath);
|
|
17805
|
+
} catch (error) {
|
|
17806
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay path ${overlayRelativePath}: ${formatUnknownError(error)}`);
|
|
17807
|
+
}
|
|
17808
|
+
if (stats.isSymbolicLink()) {
|
|
17809
|
+
await rejectSymlinkEntry(projectConfigRealPath, absPath, overlayRelativePath);
|
|
17810
|
+
}
|
|
17811
|
+
const discoveredSnapshot = captureOverlaySnapshot(stats);
|
|
17812
|
+
if (stats.isDirectory()) {
|
|
17813
|
+
await seams.beforeDirectoryRead?.({
|
|
17814
|
+
absolutePath: absPath,
|
|
17815
|
+
overlayRelativePath
|
|
17816
|
+
});
|
|
17817
|
+
let children;
|
|
17818
|
+
try {
|
|
17819
|
+
children = await readdir3(absPath);
|
|
17820
|
+
} catch (error) {
|
|
17821
|
+
throw createOpencodeOcError("read", `Failed to read project overlay directory ${overlayRelativePath}: ${formatUnknownError(error)}`);
|
|
17822
|
+
}
|
|
17823
|
+
children.sort((left, right) => left.localeCompare(right));
|
|
17824
|
+
for (const childName of children) {
|
|
17825
|
+
const childAbsolutePath = join13(absPath, childName);
|
|
17826
|
+
const childRelativePath = toPosixPath(join13(overlayRelativePath, childName));
|
|
17827
|
+
await collectOverlayCandidatesFromPath(projectConfigRealPath, childAbsolutePath, childRelativePath, collector, seams);
|
|
17828
|
+
}
|
|
17829
|
+
await assertPathSnapshotUnchanged({
|
|
17830
|
+
absolutePath: absPath,
|
|
17831
|
+
overlayRelativePath,
|
|
17832
|
+
expectedSnapshot: discoveredSnapshot,
|
|
17833
|
+
phase: "overlay discovery",
|
|
17834
|
+
mustRemainDirectory: true
|
|
17835
|
+
});
|
|
17836
|
+
return;
|
|
17837
|
+
}
|
|
17838
|
+
if (!stats.isFile()) {
|
|
17839
|
+
return;
|
|
17840
|
+
}
|
|
17841
|
+
collector.push({
|
|
17842
|
+
sourcePath: absPath,
|
|
17843
|
+
overlayRelativePath: toPosixPath(overlayRelativePath),
|
|
17844
|
+
sourceSnapshot: discoveredSnapshot
|
|
17845
|
+
});
|
|
17846
|
+
}
|
|
17847
|
+
async function collectOverlayCandidates(projectConfigDir, seams = {}) {
|
|
17848
|
+
let projectConfigRealPath;
|
|
17849
|
+
try {
|
|
17850
|
+
projectConfigRealPath = await realpath(projectConfigDir);
|
|
17851
|
+
} catch (error) {
|
|
17852
|
+
throw createOpencodeOcError("validate", `Unable to resolve project config directory ${projectConfigDir}: ${formatUnknownError(error)}`);
|
|
17853
|
+
}
|
|
17854
|
+
let projectConfigRootStats;
|
|
17855
|
+
try {
|
|
17856
|
+
projectConfigRootStats = await lstat2(projectConfigDir);
|
|
17857
|
+
} catch (error) {
|
|
17858
|
+
throw createOpencodeOcError("validate", `Unable to inspect project overlay root ${projectConfigDir}: ${formatUnknownError(error)}`);
|
|
17859
|
+
}
|
|
17860
|
+
if (!projectConfigRootStats.isDirectory()) {
|
|
17861
|
+
throw createOpencodeOcError("validate", `Project overlay root changed before discovery: ${projectConfigDir}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17862
|
+
}
|
|
17863
|
+
const projectConfigRootSnapshot = captureOverlaySnapshot(projectConfigRootStats);
|
|
17864
|
+
const candidates = [];
|
|
17865
|
+
for (const scope of OPENCODE_OVERLAY_SOURCE_SCOPES) {
|
|
17866
|
+
const scopeAbsolutePath = join13(projectConfigDir, scope);
|
|
17867
|
+
await seams.beforeScopeInspect?.({
|
|
17868
|
+
scope,
|
|
17869
|
+
scopeAbsolutePath,
|
|
17870
|
+
projectConfigDir
|
|
17871
|
+
});
|
|
17872
|
+
await assertPathSnapshotUnchanged({
|
|
17873
|
+
absolutePath: projectConfigDir,
|
|
17874
|
+
overlayRelativePath: ".opencode",
|
|
17875
|
+
expectedSnapshot: projectConfigRootSnapshot,
|
|
17876
|
+
phase: "overlay discovery",
|
|
17877
|
+
mustRemainDirectory: true
|
|
17878
|
+
});
|
|
17879
|
+
let scopeStats;
|
|
17880
|
+
try {
|
|
17881
|
+
scopeStats = await lstat2(scopeAbsolutePath);
|
|
17882
|
+
} catch (error) {
|
|
17883
|
+
const errorCode = error.code;
|
|
17884
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17885
|
+
continue;
|
|
17886
|
+
}
|
|
17887
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay scope ${scope}: ${formatUnknownError(error)}`);
|
|
17888
|
+
}
|
|
17889
|
+
if (scopeStats.isSymbolicLink()) {
|
|
17890
|
+
await rejectSymlinkEntry(projectConfigRealPath, scopeAbsolutePath, scope);
|
|
17891
|
+
}
|
|
17892
|
+
await collectOverlayCandidatesFromPath(projectConfigRealPath, scopeAbsolutePath, scope, candidates, seams);
|
|
17893
|
+
}
|
|
17894
|
+
candidates.sort((left, right) => left.overlayRelativePath.localeCompare(right.overlayRelativePath));
|
|
17895
|
+
return candidates;
|
|
17896
|
+
}
|
|
17897
|
+
function buildOverlayTempPublicationPath(destinationPath) {
|
|
17898
|
+
const destinationDirPath = dirname5(destinationPath);
|
|
17899
|
+
const destinationBaseName = basename3(destinationPath);
|
|
17900
|
+
const atomicSuffix = `${process.pid}-${randomUUID2()}`;
|
|
17901
|
+
return join13(destinationDirPath, `.${destinationBaseName}.ocx-tmp-${atomicSuffix}`);
|
|
17902
|
+
}
|
|
17903
|
+
async function publishOverlayFileAtomically(sourcePath, destinationPath) {
|
|
17904
|
+
const tempPublicationPath = buildOverlayTempPublicationPath(destinationPath);
|
|
17905
|
+
try {
|
|
17906
|
+
await copyFile(sourcePath, tempPublicationPath);
|
|
17907
|
+
await rename5(tempPublicationPath, destinationPath);
|
|
17908
|
+
} catch (error) {
|
|
17909
|
+
try {
|
|
17910
|
+
await unlink3(tempPublicationPath);
|
|
17911
|
+
} catch {}
|
|
17912
|
+
throw error;
|
|
17913
|
+
}
|
|
17914
|
+
}
|
|
17915
|
+
async function applyOverlayCopyOperations(operations, mergedConfigDir, seams = {}) {
|
|
17916
|
+
const publishAtomically = seams.publishAtomically ?? publishOverlayFileAtomically;
|
|
17917
|
+
let mergedRootStats;
|
|
17918
|
+
try {
|
|
17919
|
+
mergedRootStats = await lstat2(mergedConfigDir);
|
|
17920
|
+
} catch (error) {
|
|
17921
|
+
throw createOpencodeOcError("validate", `Unable to inspect merged overlay root ${mergedConfigDir}: ${formatUnknownError(error)}`);
|
|
17922
|
+
}
|
|
17923
|
+
if (!mergedRootStats.isDirectory()) {
|
|
17924
|
+
throw createOpencodeOcError("validate", `Merged overlay root is not a directory: ${mergedConfigDir}`);
|
|
17925
|
+
}
|
|
17926
|
+
const mergedRootSnapshot = captureOverlaySnapshot(mergedRootStats);
|
|
17927
|
+
for (const operation of operations) {
|
|
17928
|
+
await seams.beforeSourceVerification?.(operation);
|
|
17929
|
+
if (operation.sourceSnapshot) {
|
|
17930
|
+
await assertPathSnapshotUnchanged({
|
|
17931
|
+
absolutePath: operation.sourcePath,
|
|
17932
|
+
overlayRelativePath: operation.destinationRelativePath,
|
|
17933
|
+
expectedSnapshot: operation.sourceSnapshot,
|
|
17934
|
+
phase: "overlay source verification",
|
|
17935
|
+
mustRemainFile: true
|
|
17936
|
+
});
|
|
17937
|
+
}
|
|
17938
|
+
await assertPathSnapshotUnchanged({
|
|
17939
|
+
absolutePath: mergedConfigDir,
|
|
17940
|
+
overlayRelativePath: ".merged",
|
|
17941
|
+
expectedSnapshot: mergedRootSnapshot,
|
|
17942
|
+
phase: "overlay destination verification",
|
|
17943
|
+
mustRemainDirectory: true,
|
|
17944
|
+
compareContent: false
|
|
17945
|
+
});
|
|
17946
|
+
let destinationPath;
|
|
17947
|
+
try {
|
|
17948
|
+
destinationPath = validatePath(mergedConfigDir, operation.destinationRelativePath);
|
|
17949
|
+
} catch (error) {
|
|
17950
|
+
throw createOpencodeOcError("validate", `Overlay destination path is invalid (${operation.destinationRelativePath}): ${formatUnknownError(error)}`);
|
|
17951
|
+
}
|
|
17952
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17953
|
+
const destinationParentPath = dirname5(destinationPath);
|
|
17954
|
+
await seams.beforeDestinationParentCreate?.({
|
|
17955
|
+
operation,
|
|
17956
|
+
destinationPath,
|
|
17957
|
+
destinationParentPath
|
|
17958
|
+
});
|
|
17959
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17960
|
+
try {
|
|
17961
|
+
await mkdir8(destinationParentPath, { recursive: true });
|
|
17962
|
+
} catch (error) {
|
|
17963
|
+
throw createOpencodeOcError("copy", `Failed to copy overlay file ${operation.destinationRelativePath}: ${formatUnknownError(error)}`);
|
|
17964
|
+
}
|
|
17965
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17966
|
+
let destinationParentStats;
|
|
17967
|
+
try {
|
|
17968
|
+
destinationParentStats = await lstat2(destinationParentPath);
|
|
17969
|
+
} catch (error) {
|
|
17970
|
+
throw createOpencodeOcError("validate", `Failed to inspect overlay destination parent (${operation.destinationRelativePath}): ${formatUnknownError(error)}`);
|
|
17971
|
+
}
|
|
17972
|
+
if (!destinationParentStats.isDirectory()) {
|
|
17973
|
+
throw createOpencodeOcError("validate", `Overlay destination parent changed before publish (${operation.destinationRelativePath}). ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17974
|
+
}
|
|
17975
|
+
const destinationParentSnapshot = captureOverlaySnapshot(destinationParentStats);
|
|
17976
|
+
await seams.beforeDestinationPublish?.({
|
|
17977
|
+
operation,
|
|
17978
|
+
destinationPath,
|
|
17979
|
+
destinationParentPath
|
|
17980
|
+
});
|
|
17981
|
+
await assertPathSnapshotUnchanged({
|
|
17982
|
+
absolutePath: mergedConfigDir,
|
|
17983
|
+
overlayRelativePath: ".merged",
|
|
17984
|
+
expectedSnapshot: mergedRootSnapshot,
|
|
17985
|
+
phase: "overlay destination publish",
|
|
17986
|
+
mustRemainDirectory: true,
|
|
17987
|
+
compareContent: false
|
|
17988
|
+
});
|
|
17989
|
+
await assertPathSnapshotUnchanged({
|
|
17990
|
+
absolutePath: destinationParentPath,
|
|
17991
|
+
overlayRelativePath: operation.destinationRelativePath,
|
|
17992
|
+
expectedSnapshot: destinationParentSnapshot,
|
|
17993
|
+
phase: "overlay destination publish",
|
|
17994
|
+
mustRemainDirectory: true,
|
|
17995
|
+
compareContent: false
|
|
17996
|
+
});
|
|
17997
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17998
|
+
try {
|
|
17999
|
+
await publishAtomically(operation.sourcePath, destinationPath);
|
|
18000
|
+
} catch (error) {
|
|
18001
|
+
if (error instanceof ConfigError) {
|
|
18002
|
+
throw error;
|
|
18003
|
+
}
|
|
18004
|
+
throw createOpencodeOcError("copy", `Failed to copy overlay file ${operation.destinationRelativePath}: ${formatUnknownError(error)}`);
|
|
18005
|
+
}
|
|
18006
|
+
}
|
|
18007
|
+
}
|
|
18008
|
+
async function copyProfileBaseToMergedDir(profileDir, mergedConfigDir) {
|
|
18009
|
+
let profileEntries;
|
|
18010
|
+
try {
|
|
18011
|
+
profileEntries = await readdir3(profileDir);
|
|
18012
|
+
} catch (error) {
|
|
18013
|
+
throw createOpencodeOcError("copy", `Failed to read profile directory ${profileDir}: ${formatUnknownError(error)}`);
|
|
18014
|
+
}
|
|
18015
|
+
for (const entryName of profileEntries) {
|
|
18016
|
+
const sourcePath = join13(profileDir, entryName);
|
|
18017
|
+
const destinationPath = join13(mergedConfigDir, entryName);
|
|
18018
|
+
try {
|
|
18019
|
+
await cp2(sourcePath, destinationPath, { recursive: true, force: true, errorOnExist: false });
|
|
18020
|
+
} catch (error) {
|
|
18021
|
+
throw createOpencodeOcError("copy", `Failed to copy profile base file ${entryName}: ${formatUnknownError(error)}`);
|
|
18022
|
+
}
|
|
18023
|
+
}
|
|
18024
|
+
}
|
|
18025
|
+
async function cleanupMergedConfigDir(mergedConfigDir) {
|
|
18026
|
+
try {
|
|
18027
|
+
await rm5(mergedConfigDir, { recursive: true, force: true });
|
|
18028
|
+
} catch (error) {
|
|
18029
|
+
throw createOpencodeOcError("cleanup", `Failed to remove temporary merged config directory ${mergedConfigDir}: ${formatUnknownError(error)}`);
|
|
18030
|
+
}
|
|
18031
|
+
}
|
|
18032
|
+
function toOverlaySourceRelativePath(projectConfigDir, sourcePath) {
|
|
18033
|
+
const relativeSourcePath = toPosixPath(relative6(projectConfigDir, sourcePath));
|
|
18034
|
+
if (!relativeSourcePath || relativeSourcePath === ".") {
|
|
18035
|
+
throw createOpencodeOcError("validate", `Overlay source path failed relative parsing: ${sourcePath}`);
|
|
18036
|
+
}
|
|
18037
|
+
if (relativeSourcePath === ".." || relativeSourcePath.startsWith("../") || isAbsolute7(relativeSourcePath)) {
|
|
18038
|
+
throw createOpencodeOcError("validate", `Overlay source path escapes project overlay scope: ${sourcePath}`);
|
|
18039
|
+
}
|
|
18040
|
+
return relativeSourcePath;
|
|
18041
|
+
}
|
|
18042
|
+
function buildOverlayTransactionManifest(projectConfigDir, operations) {
|
|
18043
|
+
const parsedOperations = operations.map((operation) => {
|
|
18044
|
+
if (!operation.sourceSnapshot) {
|
|
18045
|
+
throw createOpencodeOcError("validate", `Overlay source snapshot missing for ${operation.destinationRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
18046
|
+
}
|
|
18047
|
+
return {
|
|
18048
|
+
sourceRelativePath: toOverlaySourceRelativePath(projectConfigDir, operation.sourcePath),
|
|
18049
|
+
destinationRelativePath: operation.destinationRelativePath,
|
|
18050
|
+
sourceSnapshot: operation.sourceSnapshot
|
|
18051
|
+
};
|
|
18052
|
+
});
|
|
18053
|
+
return {
|
|
18054
|
+
version: OVERLAY_TRANSACTION_MANIFEST_VERSION,
|
|
18055
|
+
projectConfigDir,
|
|
18056
|
+
operations: parsedOperations
|
|
18057
|
+
};
|
|
18058
|
+
}
|
|
18059
|
+
function resolveOverlayNativeTransactionHelper() {
|
|
18060
|
+
return null;
|
|
18061
|
+
}
|
|
18062
|
+
async function applyOverlayTransactionManifestWithJs(options2) {
|
|
18063
|
+
const operations = options2.manifest.operations.map((operation) => ({
|
|
18064
|
+
sourcePath: validatePath(options2.manifest.projectConfigDir, operation.sourceRelativePath),
|
|
18065
|
+
destinationRelativePath: operation.destinationRelativePath,
|
|
18066
|
+
sourceSnapshot: operation.sourceSnapshot
|
|
18067
|
+
}));
|
|
18068
|
+
await applyOverlayCopyOperations(operations, options2.mergedConfigDir, options2.copySeams);
|
|
18069
|
+
}
|
|
18070
|
+
async function executeOverlayMergeTransaction(options2) {
|
|
18071
|
+
if (options2.manifest.operations.length === 0) {
|
|
18072
|
+
return "best-effort-js";
|
|
18073
|
+
}
|
|
18074
|
+
const nativeHelper = options2.nativeHelper ?? resolveOverlayNativeTransactionHelper();
|
|
18075
|
+
if (nativeHelper) {
|
|
18076
|
+
await nativeHelper.applyManifest(options2.manifest, options2.mergedConfigDir);
|
|
18077
|
+
return "native-fd";
|
|
18078
|
+
}
|
|
18079
|
+
if (options2.hardeningMode === "native-fd-required") {
|
|
18080
|
+
throw createOpencodeOcError("validate", `Native fd helper required for overlay merge, but none is available in this Bun/Node runtime. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
18081
|
+
}
|
|
18082
|
+
await applyOverlayTransactionManifestWithJs({
|
|
18083
|
+
manifest: options2.manifest,
|
|
18084
|
+
mergedConfigDir: options2.mergedConfigDir,
|
|
18085
|
+
copySeams: options2.copySeams
|
|
18086
|
+
});
|
|
18087
|
+
return "best-effort-js";
|
|
18088
|
+
}
|
|
18089
|
+
async function resolveProjectOverlayConfigDir(localConfigDir) {
|
|
18090
|
+
const projectRootDir = dirname5(localConfigDir);
|
|
18091
|
+
let projectRealPath;
|
|
18092
|
+
try {
|
|
18093
|
+
projectRealPath = await realpath(projectRootDir);
|
|
18094
|
+
} catch (error) {
|
|
18095
|
+
throw createOpencodeOcError("validate", `Unable to resolve project directory ${projectRootDir}: ${formatUnknownError(error)}`);
|
|
18096
|
+
}
|
|
18097
|
+
let localConfigRealPath;
|
|
18098
|
+
try {
|
|
18099
|
+
localConfigRealPath = await realpath(localConfigDir);
|
|
18100
|
+
} catch (error) {
|
|
18101
|
+
throw createOpencodeOcError("validate", `Unable to resolve project config directory ${localConfigDir}: ${formatUnknownError(error)}`);
|
|
18102
|
+
}
|
|
18103
|
+
if (!isPathWithin(projectRealPath, localConfigRealPath)) {
|
|
18104
|
+
throw createOpencodeOcError("validate", `Project .opencode root resolves outside project directory: ${localConfigDir}`);
|
|
18105
|
+
}
|
|
18106
|
+
return localConfigRealPath;
|
|
18107
|
+
}
|
|
18108
|
+
function toPrimaryPrepareError(error) {
|
|
18109
|
+
if (error instanceof ConfigError) {
|
|
18110
|
+
return error;
|
|
18111
|
+
}
|
|
18112
|
+
return createOpencodeOcError("copy", `Failed to prepare temporary merged config directory: ${formatUnknownError(error)}`);
|
|
18113
|
+
}
|
|
18114
|
+
async function prepareMergedConfigDirForProfile(options2) {
|
|
18115
|
+
const localConfigDir = findLocalConfigDir(options2.projectDir);
|
|
18116
|
+
const hardeningMode = options2.hardeningMode ?? "best-effort-js";
|
|
18117
|
+
let mergedConfigDir = null;
|
|
18118
|
+
try {
|
|
18119
|
+
mergedConfigDir = await mkdtemp(join13(tmpdir(), OPENCODE_MERGED_DIR_PREFIX));
|
|
18120
|
+
await copyProfileBaseToMergedDir(options2.profileDir, mergedConfigDir);
|
|
18121
|
+
let hardeningLevel = "best-effort-js";
|
|
18122
|
+
if (localConfigDir) {
|
|
18123
|
+
const projectOverlayConfigDir = await resolveProjectOverlayConfigDir(localConfigDir);
|
|
18124
|
+
const policy = await loadProjectOverlayPolicy(projectOverlayConfigDir);
|
|
18125
|
+
const candidates = await collectOverlayCandidates(projectOverlayConfigDir, options2.seams?.collection);
|
|
18126
|
+
const copyPlan = planOverlayCopyOperations(candidates, policy);
|
|
18127
|
+
const manifest = buildOverlayTransactionManifest(projectOverlayConfigDir, copyPlan);
|
|
18128
|
+
hardeningLevel = await executeOverlayMergeTransaction({
|
|
18129
|
+
manifest,
|
|
18130
|
+
mergedConfigDir,
|
|
18131
|
+
hardeningMode,
|
|
18132
|
+
copySeams: options2.seams?.copy,
|
|
18133
|
+
nativeHelper: options2.seams?.nativeHelper
|
|
18134
|
+
});
|
|
18135
|
+
}
|
|
18136
|
+
const preparedPath = mergedConfigDir;
|
|
18137
|
+
return {
|
|
18138
|
+
path: preparedPath,
|
|
18139
|
+
cleanup: () => cleanupMergedConfigDir(preparedPath),
|
|
18140
|
+
hardeningLevel
|
|
18141
|
+
};
|
|
18142
|
+
} catch (error) {
|
|
18143
|
+
const primaryError = toPrimaryPrepareError(error);
|
|
18144
|
+
if (mergedConfigDir) {
|
|
18145
|
+
try {
|
|
18146
|
+
await cleanupMergedConfigDir(mergedConfigDir);
|
|
18147
|
+
} catch (cleanupError) {
|
|
18148
|
+
primaryError.message = `${primaryError.message}
|
|
18149
|
+
Cleanup warning: ${formatUnknownError(cleanupError)}`;
|
|
18150
|
+
}
|
|
18151
|
+
}
|
|
18152
|
+
throw primaryError;
|
|
18153
|
+
}
|
|
18154
|
+
}
|
|
18155
|
+
|
|
18156
|
+
// src/commands/opencode.ts
|
|
18157
|
+
function dedupeLastWins(items) {
|
|
18158
|
+
const seen = new Set;
|
|
18159
|
+
const result = [];
|
|
18160
|
+
for (let i = items.length - 1;i >= 0; i--) {
|
|
18161
|
+
const item = items[i];
|
|
18162
|
+
if (!seen.has(item)) {
|
|
18163
|
+
seen.add(item);
|
|
18164
|
+
result.unshift(item);
|
|
18165
|
+
}
|
|
18166
|
+
}
|
|
18167
|
+
return result;
|
|
18168
|
+
}
|
|
18169
|
+
function resolveOpenCodeBinary(opts) {
|
|
18170
|
+
return opts.configBin ?? opts.envBin ?? "opencode";
|
|
18171
|
+
}
|
|
18172
|
+
function isPathLikeLauncherToken(token) {
|
|
18173
|
+
return token.includes("/") || token.includes("\\");
|
|
18174
|
+
}
|
|
18175
|
+
function resolveStableOpenCodeLauncherPath(opts) {
|
|
18176
|
+
const { configuredBin, cwd } = opts;
|
|
18177
|
+
const resolveExecutable = opts.resolveExecutable ?? ((command) => Bun.which(command));
|
|
18178
|
+
if (!configuredBin.trim()) {
|
|
18179
|
+
throw new Error("OpenCode launcher is empty and cannot be resolved to a stable path");
|
|
18180
|
+
}
|
|
18181
|
+
if (isPathLikeLauncherToken(configuredBin)) {
|
|
18182
|
+
return path8.isAbsolute(configuredBin) ? configuredBin : path8.resolve(cwd, configuredBin);
|
|
18183
|
+
}
|
|
18184
|
+
const resolvedFromPath = resolveExecutable(configuredBin);
|
|
18185
|
+
if (!resolvedFromPath) {
|
|
18186
|
+
throw new Error(`OpenCode launcher "${configuredBin}" is not available in PATH and cannot be used as OPENCODE_BIN`);
|
|
18187
|
+
}
|
|
18188
|
+
return path8.isAbsolute(resolvedFromPath) ? resolvedFromPath : path8.resolve(cwd, resolvedFromPath);
|
|
18189
|
+
}
|
|
18190
|
+
function resolveStableOcxExecutablePath(opts) {
|
|
18191
|
+
const resolveExecutable = opts.resolveExecutable ?? ((command) => Bun.which(command));
|
|
18192
|
+
const argv = opts.argv ?? process.argv;
|
|
18193
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
18194
|
+
const isCompiledBinary = opts.isCompiledBinary ?? (typeof Bun !== "undefined" && typeof Bun.main === "string" && Bun.main.startsWith("/$bunfs/"));
|
|
18195
|
+
const inheritedOcxBin = opts.inheritedOcxBin?.trim();
|
|
18196
|
+
const runtimeExecutable = isCompiledBinary ? execPath : argv[1];
|
|
18197
|
+
const candidate = inheritedOcxBin && inheritedOcxBin.length > 0 ? inheritedOcxBin : runtimeExecutable;
|
|
18198
|
+
if (!candidate?.trim()) {
|
|
18199
|
+
throw new Error("OCX executable path is empty and cannot be resolved from the current process");
|
|
18200
|
+
}
|
|
18201
|
+
if (isPathLikeLauncherToken(candidate)) {
|
|
18202
|
+
return path8.isAbsolute(candidate) ? candidate : path8.resolve(opts.cwd, candidate);
|
|
18203
|
+
}
|
|
18204
|
+
const resolvedFromPath = resolveExecutable(candidate);
|
|
18205
|
+
if (!resolvedFromPath) {
|
|
18206
|
+
throw new Error(`OCX executable "${candidate}" is not available in PATH and cannot be persisted as OCX_BIN`);
|
|
18207
|
+
}
|
|
18208
|
+
return path8.isAbsolute(resolvedFromPath) ? resolvedFromPath : path8.resolve(opts.cwd, resolvedFromPath);
|
|
18209
|
+
}
|
|
18210
|
+
function buildOpenCodeEnv(opts) {
|
|
18211
|
+
const hasProfile = Boolean(opts.profileName);
|
|
18212
|
+
const {
|
|
18213
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: _inheritedDisableProjectConfig,
|
|
18214
|
+
OPENCODE_BIN: _inheritedOpencodeBin,
|
|
18215
|
+
OCX_CONTEXT: _inheritedOcxContext,
|
|
18216
|
+
OCX_BIN: _inheritedOcxBin,
|
|
18217
|
+
OCX_PROFILE: _inheritedOcxProfile,
|
|
18218
|
+
...baseEnvWithoutDisableProjectConfig
|
|
18219
|
+
} = opts.baseEnv;
|
|
18220
|
+
return {
|
|
18221
|
+
...baseEnvWithoutDisableProjectConfig,
|
|
18222
|
+
...opts.opencodeBin !== undefined && { OPENCODE_BIN: opts.opencodeBin },
|
|
18223
|
+
...hasProfile && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
|
|
18224
|
+
OPENCODE_CONFIG_DIR: opts.configDir ?? (hasProfile ? getProfileDir(opts.profileName) : getGlobalConfigPath()),
|
|
18225
|
+
...opts.configContent && { OPENCODE_CONFIG_CONTENT: opts.configContent },
|
|
18226
|
+
...hasProfile && { OCX_CONTEXT: "1" },
|
|
18227
|
+
...hasProfile && opts.ocxBin && { OCX_BIN: opts.ocxBin },
|
|
18228
|
+
...opts.profileName && { OCX_PROFILE: opts.profileName }
|
|
18229
|
+
};
|
|
18230
|
+
}
|
|
18231
|
+
function registerOpencodeCommand(program2) {
|
|
18232
|
+
program2.command("oc").alias("opencode").description("Launch OpenCode with resolved configuration").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
|
|
18233
|
+
try {
|
|
18234
|
+
await runOpencode(command.args, options2);
|
|
18235
|
+
} catch (error) {
|
|
18236
|
+
handleError(error);
|
|
18237
|
+
}
|
|
18238
|
+
});
|
|
18239
|
+
}
|
|
18240
|
+
async function runOpencode(args, options2) {
|
|
18241
|
+
const projectDir = process.cwd();
|
|
18242
|
+
const resolver = await ConfigResolver.create(projectDir, { profile: options2.profile });
|
|
18243
|
+
const config = resolver.resolve();
|
|
18244
|
+
const profile = resolver.getProfile();
|
|
18245
|
+
if (config.profileName) {
|
|
18246
|
+
logger.info(`Using profile: ${config.profileName}`);
|
|
18247
|
+
}
|
|
18248
|
+
const ocxConfig = profile?.ocx;
|
|
18249
|
+
const shouldRename = options2.rename !== false && ocxConfig?.renameWindow !== false;
|
|
17204
18250
|
if (config.profileName) {
|
|
17205
18251
|
const profileOpencodePath = getProfileOpencodeConfig(config.profileName);
|
|
17206
18252
|
const profileOpencodeFile = Bun.file(profileOpencodePath);
|
|
@@ -17217,73 +18263,122 @@ async function runOpencode(args, options2) {
|
|
|
17217
18263
|
instructions: dedupedInstructions.length > 0 ? dedupedInstructions : undefined
|
|
17218
18264
|
} : undefined;
|
|
17219
18265
|
let proc = null;
|
|
18266
|
+
let mergedConfig = null;
|
|
18267
|
+
let primaryFailure = null;
|
|
18268
|
+
let childExitCode = null;
|
|
18269
|
+
let preSpawnSignalExitCode = null;
|
|
17220
18270
|
const sigintHandler = () => {
|
|
17221
18271
|
if (proc) {
|
|
17222
18272
|
proc.kill("SIGINT");
|
|
17223
18273
|
} else {
|
|
17224
|
-
|
|
17225
|
-
restoreTerminalTitle();
|
|
17226
|
-
}
|
|
17227
|
-
process.exit(130);
|
|
18274
|
+
preSpawnSignalExitCode = 130;
|
|
17228
18275
|
}
|
|
17229
18276
|
};
|
|
17230
18277
|
const sigtermHandler = () => {
|
|
17231
18278
|
if (proc) {
|
|
17232
18279
|
proc.kill("SIGTERM");
|
|
17233
18280
|
} else {
|
|
17234
|
-
|
|
17235
|
-
restoreTerminalTitle();
|
|
17236
|
-
}
|
|
17237
|
-
process.exit(143);
|
|
18281
|
+
preSpawnSignalExitCode = 143;
|
|
17238
18282
|
}
|
|
17239
18283
|
};
|
|
17240
|
-
process.on("SIGINT", sigintHandler);
|
|
17241
|
-
process.on("SIGTERM", sigtermHandler);
|
|
17242
18284
|
const exitHandler = () => {
|
|
17243
18285
|
if (shouldRename) {
|
|
17244
18286
|
restoreTerminalTitle();
|
|
17245
18287
|
}
|
|
17246
18288
|
};
|
|
17247
|
-
process.on("exit", exitHandler);
|
|
17248
|
-
if (shouldRename) {
|
|
17249
|
-
saveTerminalTitle();
|
|
17250
|
-
const gitInfo = await getGitInfo(projectDir);
|
|
17251
|
-
setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
|
|
17252
|
-
}
|
|
17253
|
-
const bin = resolveOpenCodeBinary({
|
|
17254
|
-
configBin: ocxConfig?.bin,
|
|
17255
|
-
envBin: process.env.OPENCODE_BIN
|
|
17256
|
-
});
|
|
17257
|
-
const configContent = configToPass ? JSON.stringify(configToPass) : undefined;
|
|
17258
|
-
proc = Bun.spawn({
|
|
17259
|
-
cmd: [bin, ...args],
|
|
17260
|
-
cwd: projectDir,
|
|
17261
|
-
env: buildOpenCodeEnv({
|
|
17262
|
-
baseEnv: process.env,
|
|
17263
|
-
profileName: config.profileName ?? undefined,
|
|
17264
|
-
configContent
|
|
17265
|
-
}),
|
|
17266
|
-
stdin: "inherit",
|
|
17267
|
-
stdout: "inherit",
|
|
17268
|
-
stderr: "inherit"
|
|
17269
|
-
});
|
|
17270
18289
|
try {
|
|
17271
|
-
|
|
17272
|
-
process.
|
|
17273
|
-
process.
|
|
17274
|
-
|
|
18290
|
+
process.on("SIGINT", sigintHandler);
|
|
18291
|
+
process.on("SIGTERM", sigtermHandler);
|
|
18292
|
+
process.on("exit", exitHandler);
|
|
18293
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18294
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18295
|
+
return;
|
|
18296
|
+
}
|
|
18297
|
+
if (config.profileName) {
|
|
18298
|
+
mergedConfig = await prepareMergedConfigDirForProfile({
|
|
18299
|
+
projectDir,
|
|
18300
|
+
profileDir: getProfileDir(config.profileName)
|
|
18301
|
+
});
|
|
18302
|
+
}
|
|
18303
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18304
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18305
|
+
return;
|
|
18306
|
+
}
|
|
17275
18307
|
if (shouldRename) {
|
|
17276
|
-
|
|
18308
|
+
saveTerminalTitle();
|
|
18309
|
+
const gitInfo = await getGitInfo(projectDir);
|
|
18310
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18311
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18312
|
+
return;
|
|
18313
|
+
}
|
|
18314
|
+
setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
|
|
17277
18315
|
}
|
|
17278
|
-
|
|
18316
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18317
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18318
|
+
return;
|
|
18319
|
+
}
|
|
18320
|
+
const configuredBin = resolveOpenCodeBinary({
|
|
18321
|
+
configBin: ocxConfig?.bin,
|
|
18322
|
+
envBin: process.env.OPENCODE_BIN
|
|
18323
|
+
});
|
|
18324
|
+
const hasProfileLaunchContext = Boolean(config.profileName);
|
|
18325
|
+
const resolvedOpenCodeLaunchBin = hasProfileLaunchContext ? resolveStableOpenCodeLauncherPath({
|
|
18326
|
+
configuredBin,
|
|
18327
|
+
cwd: projectDir
|
|
18328
|
+
}) : configuredBin;
|
|
18329
|
+
const resolvedOcxBin = hasProfileLaunchContext ? resolveStableOcxExecutablePath({
|
|
18330
|
+
cwd: projectDir,
|
|
18331
|
+
inheritedOcxBin: process.env.OCX_BIN
|
|
18332
|
+
}) : undefined;
|
|
18333
|
+
const configContent = configToPass ? JSON.stringify(configToPass) : undefined;
|
|
18334
|
+
try {
|
|
18335
|
+
proc = Bun.spawn({
|
|
18336
|
+
cmd: [resolvedOpenCodeLaunchBin, ...args],
|
|
18337
|
+
cwd: projectDir,
|
|
18338
|
+
env: buildOpenCodeEnv({
|
|
18339
|
+
baseEnv: process.env,
|
|
18340
|
+
profileName: config.profileName ?? undefined,
|
|
18341
|
+
ocxBin: resolvedOcxBin,
|
|
18342
|
+
opencodeBin: resolvedOpenCodeLaunchBin,
|
|
18343
|
+
configDir: mergedConfig?.path,
|
|
18344
|
+
configContent
|
|
18345
|
+
}),
|
|
18346
|
+
stdin: "inherit",
|
|
18347
|
+
stdout: "inherit",
|
|
18348
|
+
stderr: "inherit"
|
|
18349
|
+
});
|
|
18350
|
+
} catch (error) {
|
|
18351
|
+
throw createOpencodeOcError("spawn", `Failed to launch OpenCode binary "${configuredBin}": ${error instanceof Error ? error.message : String(error)}`);
|
|
18352
|
+
}
|
|
18353
|
+
childExitCode = await proc.exited;
|
|
17279
18354
|
} catch (error) {
|
|
18355
|
+
primaryFailure = error instanceof Error ? error : createOpencodeOcError("spawn", `OpenCode process failed: ${String(error)}`);
|
|
18356
|
+
} finally {
|
|
17280
18357
|
process.off("SIGINT", sigintHandler);
|
|
17281
18358
|
process.off("SIGTERM", sigtermHandler);
|
|
17282
18359
|
process.off("exit", exitHandler);
|
|
17283
18360
|
if (shouldRename) {
|
|
17284
18361
|
restoreTerminalTitle();
|
|
17285
18362
|
}
|
|
17286
|
-
|
|
18363
|
+
if (mergedConfig) {
|
|
18364
|
+
try {
|
|
18365
|
+
await mergedConfig.cleanup();
|
|
18366
|
+
} catch (cleanupError) {
|
|
18367
|
+
const hasPrimaryFailure = primaryFailure !== null || preSpawnSignalExitCode !== null || childExitCode !== null && childExitCode !== 0;
|
|
18368
|
+
if (hasPrimaryFailure) {
|
|
18369
|
+
logger.warn(`Cleanup warning: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
18370
|
+
} else {
|
|
18371
|
+
const cleanupFailure = cleanupError instanceof Error ? cleanupError : createOpencodeOcError("cleanup", `Failed to remove temporary merged config directory: ${String(cleanupError)}`);
|
|
18372
|
+
primaryFailure = cleanupFailure;
|
|
18373
|
+
}
|
|
18374
|
+
}
|
|
18375
|
+
}
|
|
18376
|
+
}
|
|
18377
|
+
if (primaryFailure) {
|
|
18378
|
+
throw primaryFailure;
|
|
18379
|
+
}
|
|
18380
|
+
if (childExitCode !== null) {
|
|
18381
|
+
process.exit(childExitCode);
|
|
17287
18382
|
}
|
|
17288
18383
|
}
|
|
17289
18384
|
|
|
@@ -17292,8 +18387,8 @@ init_errors2();
|
|
|
17292
18387
|
|
|
17293
18388
|
// src/commands/profile/install-from-registry.ts
|
|
17294
18389
|
import { existsSync as existsSync14 } from "fs";
|
|
17295
|
-
import { mkdir as
|
|
17296
|
-
import { dirname as
|
|
18390
|
+
import { mkdir as mkdir9, mkdtemp as mkdtemp2, rename as rename6, rm as rm6, writeFile as writeFile4 } from "fs/promises";
|
|
18391
|
+
import { dirname as dirname6, join as join14, relative as relative7 } from "path";
|
|
17297
18392
|
init_fetcher();
|
|
17298
18393
|
init_registry();
|
|
17299
18394
|
init_errors2();
|
|
@@ -17302,6 +18397,112 @@ function formatProfileRollbackCleanupWarning(action, targetPath, error) {
|
|
|
17302
18397
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
17303
18398
|
return `${action} "${targetPath}" (${errorMessage})`;
|
|
17304
18399
|
}
|
|
18400
|
+
function buildPackumentUrl(registryUrl, qualifiedName) {
|
|
18401
|
+
const [registryName, componentName] = qualifiedName.split("/");
|
|
18402
|
+
if (!registryName || !componentName) {
|
|
18403
|
+
return;
|
|
18404
|
+
}
|
|
18405
|
+
return `${normalizeRegistryUrl(registryUrl)}/components/${componentName}.json`;
|
|
18406
|
+
}
|
|
18407
|
+
function parseQualifiedNameParts(qualifiedName) {
|
|
18408
|
+
if (!qualifiedName)
|
|
18409
|
+
return null;
|
|
18410
|
+
const [registryName, componentName, ...extraParts] = qualifiedName.split("/");
|
|
18411
|
+
if (!registryName || !componentName || extraParts.length > 0) {
|
|
18412
|
+
return null;
|
|
18413
|
+
}
|
|
18414
|
+
return { registryName, componentName };
|
|
18415
|
+
}
|
|
18416
|
+
function inferDependencyFromErrorUrl(errorUrl, registries) {
|
|
18417
|
+
if (!errorUrl) {
|
|
18418
|
+
return {};
|
|
18419
|
+
}
|
|
18420
|
+
let componentName;
|
|
18421
|
+
try {
|
|
18422
|
+
const parsedErrorUrl = new URL(errorUrl);
|
|
18423
|
+
const packumentMatch = parsedErrorUrl.pathname.match(/(?:^|\/)components\/([^/]+)\.json$/);
|
|
18424
|
+
if (packumentMatch?.[1]) {
|
|
18425
|
+
componentName = decodeURIComponent(packumentMatch[1]);
|
|
18426
|
+
} else {
|
|
18427
|
+
const fileContentMatch = parsedErrorUrl.pathname.match(/(?:^|\/)components\/([^/]+)\/.+$/);
|
|
18428
|
+
if (fileContentMatch?.[1]) {
|
|
18429
|
+
componentName = decodeURIComponent(fileContentMatch[1]);
|
|
18430
|
+
}
|
|
18431
|
+
}
|
|
18432
|
+
} catch {}
|
|
18433
|
+
const normalizedErrorUrl = normalizeRegistryUrl(errorUrl);
|
|
18434
|
+
let registryName;
|
|
18435
|
+
let matchedPrefixLength = -1;
|
|
18436
|
+
for (const [candidateRegistryName, registryConfig] of Object.entries(registries)) {
|
|
18437
|
+
const normalizedRegistryUrl = normalizeRegistryUrl(registryConfig.url);
|
|
18438
|
+
const isMatch = normalizedErrorUrl === normalizedRegistryUrl || normalizedErrorUrl.startsWith(`${normalizedRegistryUrl}/`);
|
|
18439
|
+
if (!isMatch || normalizedRegistryUrl.length <= matchedPrefixLength) {
|
|
18440
|
+
continue;
|
|
18441
|
+
}
|
|
18442
|
+
registryName = candidateRegistryName;
|
|
18443
|
+
matchedPrefixLength = normalizedRegistryUrl.length;
|
|
18444
|
+
}
|
|
18445
|
+
return { registryName, componentName };
|
|
18446
|
+
}
|
|
18447
|
+
function resolveDependencyDiagnosticsContext(options2) {
|
|
18448
|
+
const fallbackQualifiedName = options2.depRefs[0] ?? `${options2.namespace}/${options2.component}`;
|
|
18449
|
+
const knownRegistries = {
|
|
18450
|
+
...options2.profileRegistries,
|
|
18451
|
+
...options2.profileRegistries[options2.namespace] ? {} : {
|
|
18452
|
+
[options2.namespace]: {
|
|
18453
|
+
url: options2.registryUrl
|
|
18454
|
+
}
|
|
18455
|
+
}
|
|
18456
|
+
};
|
|
18457
|
+
let registryName = options2.error.registryName;
|
|
18458
|
+
let qualifiedName = options2.error.qualifiedName;
|
|
18459
|
+
const qualifiedNameParts = parseQualifiedNameParts(qualifiedName);
|
|
18460
|
+
if (!registryName && qualifiedNameParts) {
|
|
18461
|
+
registryName = qualifiedNameParts.registryName;
|
|
18462
|
+
}
|
|
18463
|
+
const qualifiedComponentName = qualifiedNameParts?.componentName;
|
|
18464
|
+
const bareComponentNameFromError = qualifiedName && !qualifiedNameParts && !qualifiedName.includes("/") ? qualifiedName : undefined;
|
|
18465
|
+
const inferred = inferDependencyFromErrorUrl(options2.error.url, knownRegistries);
|
|
18466
|
+
if (!registryName && inferred.registryName) {
|
|
18467
|
+
registryName = inferred.registryName;
|
|
18468
|
+
}
|
|
18469
|
+
const resolvedComponentName = qualifiedComponentName ?? bareComponentNameFromError ?? inferred.componentName;
|
|
18470
|
+
if ((!qualifiedName || !qualifiedNameParts) && registryName && resolvedComponentName) {
|
|
18471
|
+
qualifiedName = `${registryName}/${resolvedComponentName}`;
|
|
18472
|
+
}
|
|
18473
|
+
if (!qualifiedName) {
|
|
18474
|
+
qualifiedName = fallbackQualifiedName;
|
|
18475
|
+
}
|
|
18476
|
+
if (!registryName) {
|
|
18477
|
+
registryName = parseQualifiedNameParts(qualifiedName)?.registryName ?? options2.namespace;
|
|
18478
|
+
}
|
|
18479
|
+
const fallbackRegistryUrl = knownRegistries[registryName]?.url || (registryName === options2.namespace ? options2.registryUrl : undefined);
|
|
18480
|
+
return {
|
|
18481
|
+
registryName,
|
|
18482
|
+
qualifiedName,
|
|
18483
|
+
fallbackUrl: options2.error.url || (fallbackRegistryUrl ? buildPackumentUrl(fallbackRegistryUrl, qualifiedName) : undefined)
|
|
18484
|
+
};
|
|
18485
|
+
}
|
|
18486
|
+
function withRegistryDiagnostics(error, context) {
|
|
18487
|
+
const phase = error.phase ?? context.fallbackPhase;
|
|
18488
|
+
const url2 = error.url ?? context.fallbackUrl;
|
|
18489
|
+
const diagnostics = [
|
|
18490
|
+
`phase: ${phase}`,
|
|
18491
|
+
`qualifiedName: ${context.qualifiedName}`,
|
|
18492
|
+
`registryContext: ${context.registryContext}`,
|
|
18493
|
+
`registryName: ${context.registryName}`,
|
|
18494
|
+
...url2 ? [`url: ${url2}`] : []
|
|
18495
|
+
];
|
|
18496
|
+
return new NetworkError(`${error.message} (${diagnostics.join(", ")})`, {
|
|
18497
|
+
url: url2,
|
|
18498
|
+
status: error.status,
|
|
18499
|
+
statusText: error.statusText,
|
|
18500
|
+
phase,
|
|
18501
|
+
qualifiedName: context.qualifiedName,
|
|
18502
|
+
registryContext: context.registryContext,
|
|
18503
|
+
registryName: context.registryName
|
|
18504
|
+
});
|
|
18505
|
+
}
|
|
17305
18506
|
function resolveEmbeddedProfileTarget(rawTarget, stagingDir) {
|
|
17306
18507
|
if (!rawTarget.startsWith(".opencode/")) {
|
|
17307
18508
|
throw new ValidationError(`Invalid embedded target "${rawTarget}": expected .opencode/ prefix for embedded profile files.`);
|
|
@@ -17319,7 +18520,7 @@ function resolveEmbeddedProfileTarget(rawTarget, stagingDir) {
|
|
|
17319
18520
|
}
|
|
17320
18521
|
throw error;
|
|
17321
18522
|
}
|
|
17322
|
-
const safeRelativeTarget =
|
|
18523
|
+
const safeRelativeTarget = relative7(stagingDir, safeAbsolutePath).replace(/\\/g, "/");
|
|
17323
18524
|
if (safeRelativeTarget === "." || safeRelativeTarget === "") {
|
|
17324
18525
|
throw new ValidationError(`Invalid embedded target "${rawTarget}": target must resolve to a file path.`);
|
|
17325
18526
|
}
|
|
@@ -17352,6 +18553,15 @@ async function installProfileFromRegistry(options2) {
|
|
|
17352
18553
|
manifest = await fetchComponent(registryUrl, component);
|
|
17353
18554
|
} catch (error) {
|
|
17354
18555
|
fetchSpin?.fail(`Failed to fetch ${qualifiedName}`);
|
|
18556
|
+
if (error instanceof NetworkError) {
|
|
18557
|
+
throw withRegistryDiagnostics(error, {
|
|
18558
|
+
registryContext: "source",
|
|
18559
|
+
registryName: namespace,
|
|
18560
|
+
qualifiedName,
|
|
18561
|
+
fallbackPhase: "packument-fetch",
|
|
18562
|
+
fallbackUrl: buildPackumentUrl(registryUrl, qualifiedName)
|
|
18563
|
+
});
|
|
18564
|
+
}
|
|
17355
18565
|
if (error instanceof NotFoundError) {
|
|
17356
18566
|
throw new NotFoundError(`Profile component "${qualifiedName}" not found in registry.
|
|
17357
18567
|
|
|
@@ -17386,8 +18596,8 @@ async function installProfileFromRegistry(options2) {
|
|
|
17386
18596
|
}
|
|
17387
18597
|
filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
|
|
17388
18598
|
const profilesDir = getProfilesDir();
|
|
17389
|
-
await
|
|
17390
|
-
const stagingDir = await
|
|
18599
|
+
await mkdir9(profilesDir, { recursive: true, mode: 448 });
|
|
18600
|
+
const stagingDir = await mkdtemp2(join14(profilesDir, ".staging-"));
|
|
17391
18601
|
let profilePromoted = false;
|
|
17392
18602
|
let installCommitted = false;
|
|
17393
18603
|
try {
|
|
@@ -17396,7 +18606,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17396
18606
|
const plannedWrites = new Map;
|
|
17397
18607
|
for (const file of profileFiles) {
|
|
17398
18608
|
const resolvedTarget = resolveComponentTargetRoot(file.target, stagingDir);
|
|
17399
|
-
const targetPath =
|
|
18609
|
+
const targetPath = join14(stagingDir, resolvedTarget);
|
|
17400
18610
|
registerPlannedWriteOrThrow(plannedWrites, {
|
|
17401
18611
|
absolutePath: targetPath,
|
|
17402
18612
|
relativePath: resolvedTarget,
|
|
@@ -17407,7 +18617,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17407
18617
|
for (const file of embeddedFiles) {
|
|
17408
18618
|
const target = resolveEmbeddedProfileTarget(file.target, stagingDir);
|
|
17409
18619
|
const resolvedTarget = resolveComponentTargetRoot(target, stagingDir);
|
|
17410
|
-
const targetPath =
|
|
18620
|
+
const targetPath = join14(stagingDir, resolvedTarget);
|
|
17411
18621
|
registerPlannedWriteOrThrow(plannedWrites, {
|
|
17412
18622
|
absolutePath: targetPath,
|
|
17413
18623
|
relativePath: resolvedTarget,
|
|
@@ -17417,9 +18627,9 @@ async function installProfileFromRegistry(options2) {
|
|
|
17417
18627
|
}
|
|
17418
18628
|
for (const plannedWrite of plannedWrites.values()) {
|
|
17419
18629
|
const targetPath = plannedWrite.absolutePath;
|
|
17420
|
-
const targetDir =
|
|
18630
|
+
const targetDir = dirname6(targetPath);
|
|
17421
18631
|
if (!existsSync14(targetDir)) {
|
|
17422
|
-
await
|
|
18632
|
+
await mkdir9(targetDir, { recursive: true });
|
|
17423
18633
|
}
|
|
17424
18634
|
await writeFile4(targetPath, plannedWrite.content);
|
|
17425
18635
|
}
|
|
@@ -17431,20 +18641,20 @@ async function installProfileFromRegistry(options2) {
|
|
|
17431
18641
|
});
|
|
17432
18642
|
const renameSpin = quiet ? null : createSpinner({ text: "Moving to profile directory..." });
|
|
17433
18643
|
renameSpin?.start();
|
|
17434
|
-
const profilesDir2 =
|
|
18644
|
+
const profilesDir2 = dirname6(profileDir);
|
|
17435
18645
|
if (!existsSync14(profilesDir2)) {
|
|
17436
|
-
await
|
|
18646
|
+
await mkdir9(profilesDir2, { recursive: true, mode: 448 });
|
|
17437
18647
|
}
|
|
17438
|
-
await
|
|
18648
|
+
await rename6(stagingDir, profileDir);
|
|
17439
18649
|
profilePromoted = true;
|
|
17440
18650
|
renameSpin?.succeed("Profile installed");
|
|
17441
18651
|
if (manifest.dependencies.length > 0) {
|
|
17442
18652
|
const depsSpin = quiet ? null : createSpinner({ text: "Installing dependencies..." });
|
|
17443
18653
|
depsSpin?.start();
|
|
18654
|
+
const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
|
|
18655
|
+
let profileRegistries = {};
|
|
17444
18656
|
try {
|
|
17445
|
-
const
|
|
17446
|
-
const profileOcxConfigPath = join12(profileDir, "ocx.jsonc");
|
|
17447
|
-
let profileRegistries = {};
|
|
18657
|
+
const profileOcxConfigPath = join14(profileDir, "ocx.jsonc");
|
|
17448
18658
|
if (existsSync14(profileOcxConfigPath)) {
|
|
17449
18659
|
const profileOcxFile = Bun.file(profileOcxConfigPath);
|
|
17450
18660
|
const profileOcxContent = await profileOcxFile.text();
|
|
@@ -17465,6 +18675,23 @@ async function installProfileFromRegistry(options2) {
|
|
|
17465
18675
|
depsSpin?.succeed(`Installed ${manifest.dependencies.length} dependencies`);
|
|
17466
18676
|
} catch (error) {
|
|
17467
18677
|
depsSpin?.fail("Failed to install dependencies");
|
|
18678
|
+
if (error instanceof NetworkError) {
|
|
18679
|
+
const dependencyDiagnosticsContext = resolveDependencyDiagnosticsContext({
|
|
18680
|
+
error,
|
|
18681
|
+
depRefs,
|
|
18682
|
+
namespace,
|
|
18683
|
+
component,
|
|
18684
|
+
registryUrl,
|
|
18685
|
+
profileRegistries
|
|
18686
|
+
});
|
|
18687
|
+
throw withRegistryDiagnostics(error, {
|
|
18688
|
+
registryContext: "dependency",
|
|
18689
|
+
registryName: dependencyDiagnosticsContext.registryName,
|
|
18690
|
+
qualifiedName: dependencyDiagnosticsContext.qualifiedName,
|
|
18691
|
+
fallbackPhase: "packument-fetch",
|
|
18692
|
+
fallbackUrl: dependencyDiagnosticsContext.fallbackUrl
|
|
18693
|
+
});
|
|
18694
|
+
}
|
|
17468
18695
|
throw error;
|
|
17469
18696
|
}
|
|
17470
18697
|
}
|
|
@@ -17486,7 +18713,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17486
18713
|
const cleanupWarnings = [];
|
|
17487
18714
|
try {
|
|
17488
18715
|
if (existsSync14(stagingDir)) {
|
|
17489
|
-
await
|
|
18716
|
+
await rm6(stagingDir, { recursive: true });
|
|
17490
18717
|
}
|
|
17491
18718
|
} catch (cleanupError) {
|
|
17492
18719
|
cleanupWarnings.push(formatProfileRollbackCleanupWarning("Profile add rollback cleanup warning: failed to remove staging directory", stagingDir, cleanupError));
|
|
@@ -17494,7 +18721,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17494
18721
|
if (profilePromoted && !installCommitted) {
|
|
17495
18722
|
try {
|
|
17496
18723
|
if (existsSync14(profileDir)) {
|
|
17497
|
-
await
|
|
18724
|
+
await rm6(profileDir, { recursive: true, force: true });
|
|
17498
18725
|
}
|
|
17499
18726
|
} catch (cleanupError) {
|
|
17500
18727
|
cleanupWarnings.push(formatProfileRollbackCleanupWarning("Profile add rollback cleanup warning: failed to remove promoted profile", profileDir, cleanupError));
|
|
@@ -17906,7 +19133,7 @@ function registerProfileCommand(program2) {
|
|
|
17906
19133
|
|
|
17907
19134
|
// src/commands/registry.ts
|
|
17908
19135
|
import { existsSync as existsSync15 } from "fs";
|
|
17909
|
-
import { dirname as
|
|
19136
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
17910
19137
|
init_errors2();
|
|
17911
19138
|
async function runRegistryAddCore2(url2, options2, callbacks) {
|
|
17912
19139
|
if (callbacks.isLocked?.()) {
|
|
@@ -18033,7 +19260,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
|
|
|
18033
19260
|
return {
|
|
18034
19261
|
scope: "profile",
|
|
18035
19262
|
configPath,
|
|
18036
|
-
configDir:
|
|
19263
|
+
configDir: dirname7(configPath),
|
|
18037
19264
|
targetLabel: `profile '${options2.profile}' config`
|
|
18038
19265
|
};
|
|
18039
19266
|
}
|
|
@@ -18041,7 +19268,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
|
|
|
18041
19268
|
const configDir = getGlobalConfigPath();
|
|
18042
19269
|
return {
|
|
18043
19270
|
scope: "global",
|
|
18044
|
-
configPath:
|
|
19271
|
+
configPath: join15(configDir, "ocx.jsonc"),
|
|
18045
19272
|
configDir,
|
|
18046
19273
|
targetLabel: "global config"
|
|
18047
19274
|
};
|
|
@@ -18050,7 +19277,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
|
|
|
18050
19277
|
return {
|
|
18051
19278
|
scope: "local",
|
|
18052
19279
|
configPath: found.path,
|
|
18053
|
-
configDir: found.exists ?
|
|
19280
|
+
configDir: found.exists ? dirname7(found.path) : join15(cwd, ".opencode"),
|
|
18054
19281
|
targetLabel: "local config"
|
|
18055
19282
|
};
|
|
18056
19283
|
}
|
|
@@ -18171,7 +19398,7 @@ function registerRegistryCommand(program2) {
|
|
|
18171
19398
|
|
|
18172
19399
|
// src/commands/remove.ts
|
|
18173
19400
|
import { realpathSync } from "fs";
|
|
18174
|
-
import { rm as
|
|
19401
|
+
import { rm as rm7 } from "fs/promises";
|
|
18175
19402
|
import { sep } from "path";
|
|
18176
19403
|
|
|
18177
19404
|
// src/utils/component-ref-resolver.ts
|
|
@@ -18424,7 +19651,7 @@ ${details}`);
|
|
|
18424
19651
|
}
|
|
18425
19652
|
continue;
|
|
18426
19653
|
}
|
|
18427
|
-
await
|
|
19654
|
+
await rm7(deleteTarget, { force: true });
|
|
18428
19655
|
if (options2.verbose) {
|
|
18429
19656
|
logger.info(` \u2713 Removed ${fileEntry.path}`);
|
|
18430
19657
|
}
|
|
@@ -18581,7 +19808,7 @@ async function runSearchCore(query, options2, provider) {
|
|
|
18581
19808
|
// src/commands/self/uninstall.ts
|
|
18582
19809
|
import { existsSync as existsSync16, lstatSync as lstatSync2, readdirSync as readdirSync2, realpathSync as realpathSync2, rmSync, unlinkSync } from "fs";
|
|
18583
19810
|
import { homedir as homedir5 } from "os";
|
|
18584
|
-
import
|
|
19811
|
+
import path9 from "path";
|
|
18585
19812
|
|
|
18586
19813
|
// src/self-update/detect-method.ts
|
|
18587
19814
|
init_errors2();
|
|
@@ -18595,11 +19822,11 @@ Valid methods: ${VALID_METHODS.join(", ")}`);
|
|
|
18595
19822
|
return method;
|
|
18596
19823
|
}
|
|
18597
19824
|
var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
|
|
18598
|
-
var isTempExecution = (
|
|
18599
|
-
var isYarnGlobalInstall = (
|
|
18600
|
-
var isPnpmGlobalInstall = (
|
|
18601
|
-
var isBunGlobalInstall = (
|
|
18602
|
-
var isNpmGlobalInstall = (
|
|
19825
|
+
var isTempExecution = (path9) => path9.includes("/_npx/") || path9.includes("/.cache/bunx/") || path9.includes("/.pnpm/_temp/");
|
|
19826
|
+
var isYarnGlobalInstall = (path9) => path9.includes("/.yarn/global") || path9.includes("/.config/yarn/global");
|
|
19827
|
+
var isPnpmGlobalInstall = (path9) => path9.includes("/.pnpm/") || path9.includes("/pnpm/global");
|
|
19828
|
+
var isBunGlobalInstall = (path9) => path9.includes("/.bun/bin") || path9.includes("/.bun/install/global");
|
|
19829
|
+
var isNpmGlobalInstall = (path9) => path9.includes("/.npm/") || path9.includes("/node_modules/");
|
|
18603
19830
|
function detectInstallMethod() {
|
|
18604
19831
|
if (isCompiledBinary()) {
|
|
18605
19832
|
return "curl";
|
|
@@ -18668,19 +19895,19 @@ function tildify(absolutePath) {
|
|
|
18668
19895
|
return absolutePath;
|
|
18669
19896
|
if (absolutePath === home)
|
|
18670
19897
|
return "~";
|
|
18671
|
-
if (absolutePath.startsWith(home +
|
|
19898
|
+
if (absolutePath.startsWith(home + path9.sep)) {
|
|
18672
19899
|
return `~${absolutePath.slice(home.length)}`;
|
|
18673
19900
|
}
|
|
18674
19901
|
return absolutePath;
|
|
18675
19902
|
}
|
|
18676
19903
|
function getRelativePathIfContained(parent, child) {
|
|
18677
|
-
const normalizedParent =
|
|
18678
|
-
const normalizedChild =
|
|
18679
|
-
const
|
|
18680
|
-
if (
|
|
19904
|
+
const normalizedParent = path9.normalize(parent);
|
|
19905
|
+
const normalizedChild = path9.normalize(child);
|
|
19906
|
+
const relative8 = path9.relative(normalizedParent, normalizedChild);
|
|
19907
|
+
if (relative8.startsWith("..") || path9.isAbsolute(relative8)) {
|
|
18681
19908
|
return null;
|
|
18682
19909
|
}
|
|
18683
|
-
return
|
|
19910
|
+
return relative8;
|
|
18684
19911
|
}
|
|
18685
19912
|
function isLexicallyInside(root, target) {
|
|
18686
19913
|
return getRelativePathIfContained(root, target) !== null;
|
|
@@ -18781,8 +20008,8 @@ function getPackageManagerCommand(method) {
|
|
|
18781
20008
|
}
|
|
18782
20009
|
}
|
|
18783
20010
|
function getGlobalConfigRoot() {
|
|
18784
|
-
const base = process.env.XDG_CONFIG_HOME ||
|
|
18785
|
-
return
|
|
20011
|
+
const base = process.env.XDG_CONFIG_HOME || path9.join(homedir5(), ".config");
|
|
20012
|
+
return path9.join(base, "opencode");
|
|
18786
20013
|
}
|
|
18787
20014
|
function buildConfigTargets() {
|
|
18788
20015
|
const rootPath = getGlobalConfigRoot();
|
|
@@ -18835,10 +20062,10 @@ function buildBinaryTarget() {
|
|
|
18835
20062
|
if (method === "curl") {
|
|
18836
20063
|
const binaryPath = getExecutablePath();
|
|
18837
20064
|
const kind = getPathKind(binaryPath);
|
|
18838
|
-
const parentDir =
|
|
20065
|
+
const parentDir = path9.dirname(binaryPath);
|
|
18839
20066
|
return {
|
|
18840
20067
|
rootPath: parentDir,
|
|
18841
|
-
relativePath:
|
|
20068
|
+
relativePath: path9.basename(binaryPath),
|
|
18842
20069
|
absolutePath: binaryPath,
|
|
18843
20070
|
displayPath: tildify(binaryPath),
|
|
18844
20071
|
kind,
|
|
@@ -18884,8 +20111,8 @@ function executeRemovals(targets) {
|
|
|
18884
20111
|
}
|
|
18885
20112
|
function removeBinary(binaryPath, options2 = {}) {
|
|
18886
20113
|
const target = {
|
|
18887
|
-
rootPath:
|
|
18888
|
-
relativePath:
|
|
20114
|
+
rootPath: path9.dirname(binaryPath),
|
|
20115
|
+
relativePath: path9.basename(binaryPath),
|
|
18889
20116
|
absolutePath: binaryPath,
|
|
18890
20117
|
displayPath: tildify(binaryPath),
|
|
18891
20118
|
kind: getPathKind(binaryPath),
|
|
@@ -19180,7 +20407,7 @@ init_errors2();
|
|
|
19180
20407
|
|
|
19181
20408
|
// src/self-update/version-provider.ts
|
|
19182
20409
|
class BuildTimeVersionProvider {
|
|
19183
|
-
version = "2.0.
|
|
20410
|
+
version = "2.0.2";
|
|
19184
20411
|
}
|
|
19185
20412
|
var defaultVersionProvider = new BuildTimeVersionProvider;
|
|
19186
20413
|
|
|
@@ -19565,13 +20792,16 @@ function registerSelfCommand(program2) {
|
|
|
19565
20792
|
}
|
|
19566
20793
|
|
|
19567
20794
|
// src/commands/update.ts
|
|
19568
|
-
import { randomUUID as
|
|
20795
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
19569
20796
|
import { existsSync as existsSync18 } from "fs";
|
|
19570
|
-
import { mkdir as
|
|
19571
|
-
import { dirname as
|
|
20797
|
+
import { mkdir as mkdir10, rename as rename7, rm as rm8, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
20798
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
19572
20799
|
init_fetcher();
|
|
19573
20800
|
init_registry();
|
|
19574
20801
|
init_errors2();
|
|
20802
|
+
function formatAddCommandHint(component, options2) {
|
|
20803
|
+
return `ocx add${options2.global ? " --global" : ""} ${component}`;
|
|
20804
|
+
}
|
|
19575
20805
|
function resolveUpdateFailureMessage(phase) {
|
|
19576
20806
|
return phase === "apply" ? "Failed to update components" : "Failed to check for updates";
|
|
19577
20807
|
}
|
|
@@ -19579,6 +20809,7 @@ function registerUpdateCommand(program2) {
|
|
|
19579
20809
|
const cmd = program2.command("update [components...]").description("Update installed components");
|
|
19580
20810
|
addCommonOptions(cmd);
|
|
19581
20811
|
addVerboseOption(cmd);
|
|
20812
|
+
addGlobalOption(cmd);
|
|
19582
20813
|
cmd.option("--all", "Update all installed components").option("--registry <name>", "Update all components from a specific registry").option("--dry-run", "Preview changes without applying").action(async (components, options2) => {
|
|
19583
20814
|
try {
|
|
19584
20815
|
await runUpdate(components, options2);
|
|
@@ -19589,7 +20820,7 @@ function registerUpdateCommand(program2) {
|
|
|
19589
20820
|
}
|
|
19590
20821
|
async function runUpdate(componentNames, options2) {
|
|
19591
20822
|
const cwd = options2.cwd ?? process.cwd();
|
|
19592
|
-
const provider = await LocalConfigProvider.requireInitialized(cwd);
|
|
20823
|
+
const provider = options2.global ? await GlobalConfigProvider.requireInitialized() : await LocalConfigProvider.requireInitialized(cwd);
|
|
19593
20824
|
await runUpdateCore(componentNames, options2, provider);
|
|
19594
20825
|
}
|
|
19595
20826
|
async function runUpdateCore(componentNames, options2, provider, fileOps) {
|
|
@@ -19600,9 +20831,13 @@ async function runUpdateCore(componentNames, options2, provider, fileOps) {
|
|
|
19600
20831
|
const receipt = await readReceipt(provider.cwd);
|
|
19601
20832
|
if (!receipt || Object.keys(receipt.installed).length === 0) {
|
|
19602
20833
|
if (componentNames.length > 0) {
|
|
19603
|
-
|
|
20834
|
+
const requestedComponent = componentNames[0];
|
|
20835
|
+
if (!requestedComponent) {
|
|
20836
|
+
throw new Error("Unexpected: component name missing despite non-empty componentNames");
|
|
20837
|
+
}
|
|
20838
|
+
throw new NotFoundError(`Component '${requestedComponent}' is not installed. Run '${formatAddCommandHint(requestedComponent, options2)}' first.`);
|
|
19604
20839
|
}
|
|
19605
|
-
throw new NotFoundError(
|
|
20840
|
+
throw new NotFoundError(`No components installed. Run '${formatAddCommandHint("<component>", options2)}' first.`);
|
|
19606
20841
|
}
|
|
19607
20842
|
const hasComponents = componentNames.length > 0;
|
|
19608
20843
|
const hasAll = options2.all === true;
|
|
@@ -19725,7 +20960,7 @@ async function runUpdateCore(componentNames, options2, provider, fileOps) {
|
|
|
19725
20960
|
throw new ValidationError(`File "${file.path}" not found in component manifest for "${update.registryName}/${update.name}".`);
|
|
19726
20961
|
}
|
|
19727
20962
|
const resolvedTarget = resolveTargetPath(fileObj.target, isFlattened, provider.cwd);
|
|
19728
|
-
const targetPath =
|
|
20963
|
+
const targetPath = join16(provider.cwd, resolvedTarget);
|
|
19729
20964
|
registerPlannedWriteOrThrow(plannedWrites, {
|
|
19730
20965
|
absolutePath: targetPath,
|
|
19731
20966
|
relativePath: resolvedTarget,
|
|
@@ -19814,7 +21049,7 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19814
21049
|
const appliedWrites = [];
|
|
19815
21050
|
const tempPaths = new Set;
|
|
19816
21051
|
let finalized = false;
|
|
19817
|
-
const renameFile = options2.fileOps?.rename ??
|
|
21052
|
+
const renameFile = options2.fileOps?.rename ?? rename7;
|
|
19818
21053
|
const rollback = async () => {
|
|
19819
21054
|
if (finalized) {
|
|
19820
21055
|
return;
|
|
@@ -19822,20 +21057,20 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19822
21057
|
finalized = true;
|
|
19823
21058
|
for (const tempPath of tempPaths) {
|
|
19824
21059
|
try {
|
|
19825
|
-
await
|
|
21060
|
+
await rm8(tempPath, { force: true });
|
|
19826
21061
|
} catch {}
|
|
19827
21062
|
}
|
|
19828
21063
|
for (const appliedWrite of [...appliedWrites].reverse()) {
|
|
19829
21064
|
try {
|
|
19830
21065
|
if (appliedWrite.backupPath) {
|
|
19831
21066
|
if (existsSync18(appliedWrite.targetPath)) {
|
|
19832
|
-
await
|
|
21067
|
+
await rm8(appliedWrite.targetPath, { force: true, recursive: true });
|
|
19833
21068
|
}
|
|
19834
21069
|
if (existsSync18(appliedWrite.backupPath)) {
|
|
19835
21070
|
await renameFile(appliedWrite.backupPath, appliedWrite.targetPath);
|
|
19836
21071
|
}
|
|
19837
21072
|
} else if (existsSync18(appliedWrite.targetPath)) {
|
|
19838
|
-
await
|
|
21073
|
+
await rm8(appliedWrite.targetPath, { force: true, recursive: true });
|
|
19839
21074
|
}
|
|
19840
21075
|
} catch {}
|
|
19841
21076
|
}
|
|
@@ -19851,7 +21086,7 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19851
21086
|
}
|
|
19852
21087
|
if (existsSync18(appliedWrite.backupPath)) {
|
|
19853
21088
|
try {
|
|
19854
|
-
await
|
|
21089
|
+
await rm8(appliedWrite.backupPath, { force: true });
|
|
19855
21090
|
} catch (error) {
|
|
19856
21091
|
if (!options2.quiet) {
|
|
19857
21092
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -19864,9 +21099,9 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19864
21099
|
try {
|
|
19865
21100
|
for (const prepared of preparedUpdates) {
|
|
19866
21101
|
for (const preparedFile of prepared.preparedFiles) {
|
|
19867
|
-
const targetDir =
|
|
21102
|
+
const targetDir = dirname8(preparedFile.targetPath);
|
|
19868
21103
|
if (!existsSync18(targetDir)) {
|
|
19869
|
-
await
|
|
21104
|
+
await mkdir10(targetDir, { recursive: true });
|
|
19870
21105
|
}
|
|
19871
21106
|
if (existsSync18(preparedFile.targetPath)) {
|
|
19872
21107
|
const currentTargetStats = await stat4(preparedFile.targetPath);
|
|
@@ -19874,12 +21109,12 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19874
21109
|
throw new ValidationError(`Cannot update "${preparedFile.resolvedTarget}": target path is a directory.`);
|
|
19875
21110
|
}
|
|
19876
21111
|
}
|
|
19877
|
-
const tempPath = `${preparedFile.targetPath}.ocx-update-tmp-${
|
|
21112
|
+
const tempPath = `${preparedFile.targetPath}.ocx-update-tmp-${randomUUID3()}`;
|
|
19878
21113
|
await writeFile5(tempPath, preparedFile.content);
|
|
19879
21114
|
tempPaths.add(tempPath);
|
|
19880
21115
|
let backupPath = null;
|
|
19881
21116
|
if (existsSync18(preparedFile.targetPath)) {
|
|
19882
|
-
backupPath = `${preparedFile.targetPath}.ocx-update-backup-${
|
|
21117
|
+
backupPath = `${preparedFile.targetPath}.ocx-update-backup-${randomUUID3()}`;
|
|
19883
21118
|
await renameFile(preparedFile.targetPath, backupPath);
|
|
19884
21119
|
appliedWrites.push({
|
|
19885
21120
|
targetPath: preparedFile.targetPath,
|
|
@@ -19950,7 +21185,7 @@ Please use a fully qualified name (alias/component).`);
|
|
|
19950
21185
|
});
|
|
19951
21186
|
if (matchingIds.length === 0) {
|
|
19952
21187
|
throw new NotFoundError(`Component '${name}' is not installed.
|
|
19953
|
-
Run '
|
|
21188
|
+
Run '${formatAddCommandHint(name, options2)}' to install it first.`);
|
|
19954
21189
|
}
|
|
19955
21190
|
const canonicalId = matchingIds[0];
|
|
19956
21191
|
if (!canonicalId) {
|
|
@@ -19977,6 +21212,92 @@ function outputUpdateDryRun(results, options2) {
|
|
|
19977
21212
|
outputDryRun(dryRunResult, { json: options2.json, quiet: options2.quiet });
|
|
19978
21213
|
}
|
|
19979
21214
|
|
|
21215
|
+
// src/commands/validate.ts
|
|
21216
|
+
import { resolve as resolve8 } from "path";
|
|
21217
|
+
init_errors2();
|
|
21218
|
+
function createLoadValidationError2(message, errorKind) {
|
|
21219
|
+
if (errorKind === "not_found") {
|
|
21220
|
+
return new NotFoundError(message);
|
|
21221
|
+
}
|
|
21222
|
+
if (errorKind === "parse_error") {
|
|
21223
|
+
return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
21224
|
+
}
|
|
21225
|
+
return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
21226
|
+
}
|
|
21227
|
+
function createValidationFailureError2(errors3, failureType) {
|
|
21228
|
+
const summary = summarizeValidationErrors(errors3, {
|
|
21229
|
+
schemaErrors: failureType === "schema" ? errors3.length : 0
|
|
21230
|
+
});
|
|
21231
|
+
const details = {
|
|
21232
|
+
valid: false,
|
|
21233
|
+
errors: errors3,
|
|
21234
|
+
summary: {
|
|
21235
|
+
valid: false,
|
|
21236
|
+
totalErrors: summary.totalErrors,
|
|
21237
|
+
schemaErrors: summary.schemaErrors,
|
|
21238
|
+
sourceFileErrors: summary.sourceFileErrors,
|
|
21239
|
+
circularDependencyErrors: summary.circularDependencyErrors,
|
|
21240
|
+
duplicateTargetErrors: summary.duplicateTargetErrors,
|
|
21241
|
+
otherErrors: summary.otherErrors
|
|
21242
|
+
}
|
|
21243
|
+
};
|
|
21244
|
+
return new ValidationFailedError(details);
|
|
21245
|
+
}
|
|
21246
|
+
function outputValidationErrors(errors3) {
|
|
21247
|
+
for (const error of errors3) {
|
|
21248
|
+
console.log(kleur_default.red(` ${error}`));
|
|
21249
|
+
}
|
|
21250
|
+
}
|
|
21251
|
+
function registerValidateCommand(program2) {
|
|
21252
|
+
program2.command("validate").description("Validate a registry source (for registry authors)").argument("[path]", "Registry source directory", ".").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output as JSON", false).option("-q, --quiet", "Suppress output", false).option("--no-duplicate-targets", "Skip duplicate target validation").action(async (path10, options2) => {
|
|
21253
|
+
try {
|
|
21254
|
+
const sourcePath = resolve8(options2.cwd, path10);
|
|
21255
|
+
const validationResult = await runCompleteValidation(sourcePath, {
|
|
21256
|
+
skipDuplicateTargets: options2.duplicateTargets === false
|
|
21257
|
+
});
|
|
21258
|
+
if (!validationResult.success) {
|
|
21259
|
+
const [firstError = "Registry validation failed"] = validationResult.errors;
|
|
21260
|
+
if (validationResult.failureType === "load") {
|
|
21261
|
+
const loadError = createLoadValidationError2(firstError, validationResult.loadErrorKind);
|
|
21262
|
+
if (!options2.json && options2.quiet) {
|
|
21263
|
+
const exitCode = loadError instanceof OCXError ? loadError.exitCode : EXIT_CODES.GENERAL;
|
|
21264
|
+
process.exit(exitCode);
|
|
21265
|
+
}
|
|
21266
|
+
throw loadError;
|
|
21267
|
+
}
|
|
21268
|
+
const validationError = createValidationFailureError2(validationResult.errors, validationResult.failureType === "schema" ? "schema" : "rules");
|
|
21269
|
+
if (options2.json) {
|
|
21270
|
+
throw validationError;
|
|
21271
|
+
}
|
|
21272
|
+
if (!options2.quiet) {
|
|
21273
|
+
logger.error(validationError.message);
|
|
21274
|
+
outputValidationErrors(validationResult.errors);
|
|
21275
|
+
}
|
|
21276
|
+
process.exit(validationError.exitCode);
|
|
21277
|
+
}
|
|
21278
|
+
if (!options2.quiet && !options2.json) {
|
|
21279
|
+
logger.success("\u2713 Registry source is valid");
|
|
21280
|
+
}
|
|
21281
|
+
if (options2.json) {
|
|
21282
|
+
outputJson({
|
|
21283
|
+
success: true,
|
|
21284
|
+
data: {
|
|
21285
|
+
valid: true,
|
|
21286
|
+
errors: [],
|
|
21287
|
+
summary: summarizeValidationErrors([])
|
|
21288
|
+
}
|
|
21289
|
+
});
|
|
21290
|
+
}
|
|
21291
|
+
} catch (error) {
|
|
21292
|
+
if (!options2.json && options2.quiet) {
|
|
21293
|
+
const exitCode = error instanceof OCXError ? error.exitCode : EXIT_CODES.GENERAL;
|
|
21294
|
+
process.exit(exitCode);
|
|
21295
|
+
}
|
|
21296
|
+
handleError(error, { json: options2.json });
|
|
21297
|
+
}
|
|
21298
|
+
});
|
|
21299
|
+
}
|
|
21300
|
+
|
|
19980
21301
|
// src/commands/verify.ts
|
|
19981
21302
|
init_errors2();
|
|
19982
21303
|
function registerVerifyCommand(program2) {
|
|
@@ -20053,11 +21374,11 @@ async function runVerify(componentNames, options2) {
|
|
|
20053
21374
|
}
|
|
20054
21375
|
} else {
|
|
20055
21376
|
logger.error(`\u2717 ${result.canonicalId} - integrity check failed`);
|
|
20056
|
-
for (const
|
|
20057
|
-
logger.error(` Modified: ${
|
|
21377
|
+
for (const path10 of result.modified) {
|
|
21378
|
+
logger.error(` Modified: ${path10}`);
|
|
20058
21379
|
}
|
|
20059
|
-
for (const
|
|
20060
|
-
logger.error(` Missing: ${
|
|
21380
|
+
for (const path10 of result.missing) {
|
|
21381
|
+
logger.error(` Missing: ${path10}`);
|
|
20061
21382
|
}
|
|
20062
21383
|
}
|
|
20063
21384
|
}
|
|
@@ -20093,7 +21414,7 @@ function registerUpdateCheckHook(program2) {
|
|
|
20093
21414
|
return;
|
|
20094
21415
|
}
|
|
20095
21416
|
const actionOptions = actionCommand.opts();
|
|
20096
|
-
if (actionOptions.json) {
|
|
21417
|
+
if (actionOptions.json || actionOptions.quiet) {
|
|
20097
21418
|
return;
|
|
20098
21419
|
}
|
|
20099
21420
|
if (!shouldCheckForUpdate())
|
|
@@ -20107,7 +21428,7 @@ function registerUpdateCheckHook(program2) {
|
|
|
20107
21428
|
});
|
|
20108
21429
|
}
|
|
20109
21430
|
// src/index.ts
|
|
20110
|
-
var version = "2.0.
|
|
21431
|
+
var version = "2.0.2";
|
|
20111
21432
|
async function main2() {
|
|
20112
21433
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
20113
21434
|
registerInitCommand(program2);
|
|
@@ -20116,6 +21437,7 @@ async function main2() {
|
|
|
20116
21437
|
registerSearchCommand(program2);
|
|
20117
21438
|
registerRegistryCommand(program2);
|
|
20118
21439
|
registerBuildCommand(program2);
|
|
21440
|
+
registerValidateCommand(program2);
|
|
20119
21441
|
registerSelfCommand(program2);
|
|
20120
21442
|
registerVerifyCommand(program2);
|
|
20121
21443
|
registerRemoveCommand(program2);
|
|
@@ -20140,4 +21462,4 @@ export {
|
|
|
20140
21462
|
buildRegistry
|
|
20141
21463
|
};
|
|
20142
21464
|
|
|
20143
|
-
//# debugId=
|
|
21465
|
+
//# debugId=F324B1CA6BEB983B64756E2164756E21
|