ocx 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1418 -155
- 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.1",
|
|
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.1"}`;
|
|
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;
|
|
@@ -17156,53 +17546,661 @@ function formatTerminalName(cwd, profileName, gitInfo) {
|
|
|
17156
17546
|
return `ocx[${profileName}]:${repoName}/${branch}`;
|
|
17157
17547
|
}
|
|
17158
17548
|
|
|
17159
|
-
// src/commands/opencode.ts
|
|
17160
|
-
|
|
17161
|
-
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17549
|
+
// src/commands/opencode-overlay.ts
|
|
17550
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
17551
|
+
import {
|
|
17552
|
+
copyFile,
|
|
17553
|
+
cp as cp2,
|
|
17554
|
+
lstat as lstat2,
|
|
17555
|
+
mkdir as mkdir8,
|
|
17556
|
+
mkdtemp,
|
|
17557
|
+
readdir as readdir3,
|
|
17558
|
+
readFile as readFile2,
|
|
17559
|
+
realpath,
|
|
17560
|
+
rename as rename5,
|
|
17561
|
+
rm as rm5,
|
|
17562
|
+
unlink as unlink3
|
|
17563
|
+
} from "fs/promises";
|
|
17564
|
+
import { tmpdir } from "os";
|
|
17565
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute7, join as join13, relative as relative6 } from "path";
|
|
17566
|
+
var {Glob: Glob4 } = globalThis.Bun;
|
|
17567
|
+
init_zod();
|
|
17568
|
+
init_errors2();
|
|
17569
|
+
init_path_security();
|
|
17570
|
+
var OPENCODE_OVERLAY_SOURCE_SCOPES = ["agent", "agents", "skill", "skills"];
|
|
17571
|
+
var OPENCODE_MERGED_DIR_PREFIX = "ocx-oc-merged-";
|
|
17572
|
+
var OVERLAY_TRANSACTION_MANIFEST_VERSION = 1;
|
|
17573
|
+
var OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE = "Full TOCTOU/symlink-swap hardening requires an fd-based native helper transaction.";
|
|
17574
|
+
function createOpencodeOcError(errorClass, detail) {
|
|
17575
|
+
return new ConfigError(`ocx oc ${errorClass} error: ${detail}`);
|
|
17576
|
+
}
|
|
17577
|
+
function formatUnknownError(error) {
|
|
17578
|
+
if (error instanceof Error) {
|
|
17579
|
+
return error.message;
|
|
17169
17580
|
}
|
|
17170
|
-
return
|
|
17581
|
+
return String(error);
|
|
17171
17582
|
}
|
|
17172
|
-
function
|
|
17173
|
-
|
|
17583
|
+
function formatJsoncParseError4(parseErrors) {
|
|
17584
|
+
if (parseErrors.length === 0) {
|
|
17585
|
+
return "Unknown parse error";
|
|
17586
|
+
}
|
|
17587
|
+
const firstError = parseErrors[0];
|
|
17588
|
+
if (!firstError) {
|
|
17589
|
+
return "Unknown parse error";
|
|
17590
|
+
}
|
|
17591
|
+
return `${printParseErrorCode(firstError.error)} at offset ${firstError.offset}`;
|
|
17174
17592
|
}
|
|
17175
|
-
function
|
|
17176
|
-
|
|
17593
|
+
function toPosixPath(pathValue) {
|
|
17594
|
+
return pathValue.replaceAll("\\", "/");
|
|
17595
|
+
}
|
|
17596
|
+
function normalizeGlobPattern(pattern) {
|
|
17597
|
+
return pattern.startsWith("./") ? pattern.slice(2) : pattern;
|
|
17598
|
+
}
|
|
17599
|
+
function isPathWithin(parentPath, childPath) {
|
|
17600
|
+
const relativePath = relative6(parentPath, childPath);
|
|
17601
|
+
if (relativePath.length === 0) {
|
|
17602
|
+
return true;
|
|
17603
|
+
}
|
|
17604
|
+
return !relativePath.startsWith("..") && !isAbsolute7(relativePath);
|
|
17605
|
+
}
|
|
17606
|
+
function getOverlayEntryType(stats) {
|
|
17607
|
+
if (stats.isFile()) {
|
|
17608
|
+
return "file";
|
|
17609
|
+
}
|
|
17610
|
+
if (stats.isDirectory()) {
|
|
17611
|
+
return "directory";
|
|
17612
|
+
}
|
|
17613
|
+
if (stats.isSymbolicLink()) {
|
|
17614
|
+
return "symlink";
|
|
17615
|
+
}
|
|
17616
|
+
return "other";
|
|
17617
|
+
}
|
|
17618
|
+
function captureOverlaySnapshot(stats) {
|
|
17177
17619
|
return {
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
|
|
17181
|
-
|
|
17182
|
-
|
|
17620
|
+
entryType: getOverlayEntryType(stats),
|
|
17621
|
+
device: String(stats.dev),
|
|
17622
|
+
inode: String(stats.ino),
|
|
17623
|
+
mode: String(stats.mode),
|
|
17624
|
+
size: String(stats.size),
|
|
17625
|
+
mtimeMs: String(stats.mtimeMs)
|
|
17183
17626
|
};
|
|
17184
17627
|
}
|
|
17185
|
-
function
|
|
17186
|
-
|
|
17628
|
+
function overlaySnapshotIdentityMatches(expected, actual) {
|
|
17629
|
+
return expected.entryType === actual.entryType && expected.device === actual.device && expected.inode === actual.inode && expected.mode === actual.mode;
|
|
17630
|
+
}
|
|
17631
|
+
function overlaySnapshotContentMatches(expected, actual) {
|
|
17632
|
+
return expected.size === actual.size && expected.mtimeMs === actual.mtimeMs;
|
|
17633
|
+
}
|
|
17634
|
+
async function assertPathSnapshotUnchanged(options2) {
|
|
17635
|
+
let currentStats;
|
|
17636
|
+
try {
|
|
17637
|
+
currentStats = await lstat2(options2.absolutePath);
|
|
17638
|
+
} catch {
|
|
17639
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17640
|
+
}
|
|
17641
|
+
if (options2.mustRemainDirectory && !currentStats.isDirectory()) {
|
|
17642
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17643
|
+
}
|
|
17644
|
+
if (options2.mustRemainFile && !currentStats.isFile()) {
|
|
17645
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17646
|
+
}
|
|
17647
|
+
const currentSnapshot = captureOverlaySnapshot(currentStats);
|
|
17648
|
+
const identityMatches = overlaySnapshotIdentityMatches(options2.expectedSnapshot, currentSnapshot);
|
|
17649
|
+
const shouldCompareContent = options2.compareContent ?? true;
|
|
17650
|
+
const contentMatches = !shouldCompareContent ? true : overlaySnapshotContentMatches(options2.expectedSnapshot, currentSnapshot);
|
|
17651
|
+
if (!identityMatches || !contentMatches) {
|
|
17652
|
+
throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
async function assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, destinationRelativePath) {
|
|
17656
|
+
const relativeDestinationPath = toPosixPath(relative6(mergedConfigDir, destinationPath));
|
|
17657
|
+
const pathComponents = relativeDestinationPath.split("/").filter((component) => component.length > 0);
|
|
17658
|
+
let currentPath = mergedConfigDir;
|
|
17659
|
+
for (const component of pathComponents) {
|
|
17660
|
+
currentPath = join13(currentPath, component);
|
|
17661
|
+
let currentStats;
|
|
17187
17662
|
try {
|
|
17188
|
-
await
|
|
17663
|
+
currentStats = await lstat2(currentPath);
|
|
17189
17664
|
} catch (error) {
|
|
17190
|
-
|
|
17665
|
+
const errorCode = error.code;
|
|
17666
|
+
if (errorCode === "ENOENT") {
|
|
17667
|
+
return;
|
|
17668
|
+
}
|
|
17669
|
+
throw createOpencodeOcError("validate", `Failed to inspect overlay destination path (${destinationRelativePath}): ${formatUnknownError(error)}`);
|
|
17191
17670
|
}
|
|
17192
|
-
|
|
17671
|
+
if (currentStats.isSymbolicLink()) {
|
|
17672
|
+
throw createOpencodeOcError("validate", `Overlay destination path contains existing symlink (${destinationRelativePath})`);
|
|
17673
|
+
}
|
|
17674
|
+
}
|
|
17193
17675
|
}
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17676
|
+
var projectOverlayPolicySchema = exports_external.object({
|
|
17677
|
+
include: exports_external.array(exports_external.string()).optional(),
|
|
17678
|
+
exclude: exports_external.array(exports_external.string()).optional()
|
|
17679
|
+
}).passthrough();
|
|
17680
|
+
function validatePolicyPatterns(policyPath, field, patterns) {
|
|
17681
|
+
for (const pattern of patterns) {
|
|
17682
|
+
try {
|
|
17683
|
+
new Glob4(normalizeGlobPattern(pattern));
|
|
17684
|
+
} catch {
|
|
17685
|
+
throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: ${field} contains invalid glob pattern "${pattern}"`);
|
|
17686
|
+
}
|
|
17201
17687
|
}
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
if (
|
|
17205
|
-
|
|
17688
|
+
}
|
|
17689
|
+
async function loadProjectOverlayPolicy(localConfigDir) {
|
|
17690
|
+
if (!localConfigDir) {
|
|
17691
|
+
return { include: [], exclude: [] };
|
|
17692
|
+
}
|
|
17693
|
+
const policyPath = join13(localConfigDir, OCX_CONFIG_FILE);
|
|
17694
|
+
const policyReadPath = await resolveProjectOverlayPolicyReadPath(localConfigDir, policyPath);
|
|
17695
|
+
if (!policyReadPath) {
|
|
17696
|
+
return { include: [], exclude: [] };
|
|
17697
|
+
}
|
|
17698
|
+
let policyText;
|
|
17699
|
+
try {
|
|
17700
|
+
policyText = await readFile2(policyReadPath, "utf8");
|
|
17701
|
+
} catch (error) {
|
|
17702
|
+
throw createOpencodeOcError("read", `Failed to read project overlay policy at ${policyPath}: ${formatUnknownError(error)}`);
|
|
17703
|
+
}
|
|
17704
|
+
const parseErrors = [];
|
|
17705
|
+
const parsedPolicy = parse2(policyText, parseErrors, { allowTrailingComma: true });
|
|
17706
|
+
if (parseErrors.length > 0) {
|
|
17707
|
+
throw createOpencodeOcError("parse", `Failed to parse project overlay policy at ${policyPath}: ${formatJsoncParseError4(parseErrors)}`);
|
|
17708
|
+
}
|
|
17709
|
+
if (!parsedPolicy || typeof parsedPolicy !== "object" || Array.isArray(parsedPolicy)) {
|
|
17710
|
+
throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: root must be an object`);
|
|
17711
|
+
}
|
|
17712
|
+
const parsedResult = projectOverlayPolicySchema.safeParse(parsedPolicy);
|
|
17713
|
+
if (!parsedResult.success) {
|
|
17714
|
+
const firstIssue = parsedResult.error.issues[0];
|
|
17715
|
+
const issuePath = firstIssue?.path.length ? firstIssue.path.join(".") : "root";
|
|
17716
|
+
const issueMessage = firstIssue?.message ?? "Invalid project overlay policy";
|
|
17717
|
+
throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: ${issuePath} ${issueMessage}`);
|
|
17718
|
+
}
|
|
17719
|
+
const include = parsedResult.data.include ?? [];
|
|
17720
|
+
const exclude = parsedResult.data.exclude ?? [];
|
|
17721
|
+
validatePolicyPatterns(policyPath, "include", include);
|
|
17722
|
+
validatePolicyPatterns(policyPath, "exclude", exclude);
|
|
17723
|
+
return { include, exclude };
|
|
17724
|
+
}
|
|
17725
|
+
async function resolveProjectOverlayPolicyReadPath(localConfigDir, policyPath) {
|
|
17726
|
+
let policyStats;
|
|
17727
|
+
try {
|
|
17728
|
+
policyStats = await lstat2(policyPath);
|
|
17729
|
+
} catch (error) {
|
|
17730
|
+
const errorCode = error.code;
|
|
17731
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17732
|
+
return null;
|
|
17733
|
+
}
|
|
17734
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay policy at ${policyPath}: ${formatUnknownError(error)}`);
|
|
17735
|
+
}
|
|
17736
|
+
if (!policyStats.isSymbolicLink()) {
|
|
17737
|
+
return policyPath;
|
|
17738
|
+
}
|
|
17739
|
+
let projectConfigRealPath;
|
|
17740
|
+
try {
|
|
17741
|
+
projectConfigRealPath = await realpath(localConfigDir);
|
|
17742
|
+
} catch (error) {
|
|
17743
|
+
throw createOpencodeOcError("validate", `Unable to resolve project config directory ${localConfigDir}: ${formatUnknownError(error)}`);
|
|
17744
|
+
}
|
|
17745
|
+
let policyTargetRealPath;
|
|
17746
|
+
try {
|
|
17747
|
+
policyTargetRealPath = await realpath(policyPath);
|
|
17748
|
+
} catch (error) {
|
|
17749
|
+
const errorCode = error.code;
|
|
17750
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17751
|
+
throw createOpencodeOcError("validate", `Broken symlink in project overlay policy: ${policyPath}`);
|
|
17752
|
+
}
|
|
17753
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay policy symlink at ${policyPath}: ${formatUnknownError(error)}`);
|
|
17754
|
+
}
|
|
17755
|
+
if (!isPathWithin(projectConfigRealPath, policyTargetRealPath)) {
|
|
17756
|
+
throw createOpencodeOcError("validate", `Symlink escapes project overlay policy scope: ${policyPath}`);
|
|
17757
|
+
}
|
|
17758
|
+
return policyTargetRealPath;
|
|
17759
|
+
}
|
|
17760
|
+
function shouldIncludeOverlayPath(pathValue, policy) {
|
|
17761
|
+
for (const pattern of policy.include) {
|
|
17762
|
+
const glob = new Glob4(normalizeGlobPattern(pattern));
|
|
17763
|
+
if (glob.match(pathValue)) {
|
|
17764
|
+
return true;
|
|
17765
|
+
}
|
|
17766
|
+
}
|
|
17767
|
+
for (const pattern of policy.exclude) {
|
|
17768
|
+
const glob = new Glob4(normalizeGlobPattern(pattern));
|
|
17769
|
+
if (glob.match(pathValue)) {
|
|
17770
|
+
return false;
|
|
17771
|
+
}
|
|
17772
|
+
}
|
|
17773
|
+
return true;
|
|
17774
|
+
}
|
|
17775
|
+
function planOverlayCopyOperations(candidates, policy) {
|
|
17776
|
+
return candidates.filter((candidate) => shouldIncludeOverlayPath(candidate.overlayRelativePath, policy)).map((candidate) => ({
|
|
17777
|
+
sourcePath: candidate.sourcePath,
|
|
17778
|
+
destinationRelativePath: candidate.overlayRelativePath,
|
|
17779
|
+
sourceSnapshot: candidate.sourceSnapshot
|
|
17780
|
+
}));
|
|
17781
|
+
}
|
|
17782
|
+
async function rejectSymlinkEntry(projectConfigRealPath, absolutePath, overlayRelativePath) {
|
|
17783
|
+
let targetRealPath;
|
|
17784
|
+
try {
|
|
17785
|
+
targetRealPath = await realpath(absolutePath);
|
|
17786
|
+
} catch (error) {
|
|
17787
|
+
const errorCode = error.code;
|
|
17788
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17789
|
+
throw createOpencodeOcError("validate", `Broken symlink in project overlay scope: ${overlayRelativePath}`);
|
|
17790
|
+
}
|
|
17791
|
+
throw createOpencodeOcError("read", `Failed to inspect symlink at ${overlayRelativePath}: ${formatUnknownError(error)}`);
|
|
17792
|
+
}
|
|
17793
|
+
if (!isPathWithin(projectConfigRealPath, targetRealPath)) {
|
|
17794
|
+
throw createOpencodeOcError("validate", `Symlink escapes project overlay scope: ${overlayRelativePath}`);
|
|
17795
|
+
}
|
|
17796
|
+
throw createOpencodeOcError("validate", `Symlink entries are not supported in project overlay scope: ${overlayRelativePath}`);
|
|
17797
|
+
}
|
|
17798
|
+
async function collectOverlayCandidatesFromPath(projectConfigRealPath, absPath, overlayRelativePath, collector, seams) {
|
|
17799
|
+
let stats;
|
|
17800
|
+
try {
|
|
17801
|
+
stats = await lstat2(absPath);
|
|
17802
|
+
} catch (error) {
|
|
17803
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay path ${overlayRelativePath}: ${formatUnknownError(error)}`);
|
|
17804
|
+
}
|
|
17805
|
+
if (stats.isSymbolicLink()) {
|
|
17806
|
+
await rejectSymlinkEntry(projectConfigRealPath, absPath, overlayRelativePath);
|
|
17807
|
+
}
|
|
17808
|
+
const discoveredSnapshot = captureOverlaySnapshot(stats);
|
|
17809
|
+
if (stats.isDirectory()) {
|
|
17810
|
+
await seams.beforeDirectoryRead?.({
|
|
17811
|
+
absolutePath: absPath,
|
|
17812
|
+
overlayRelativePath
|
|
17813
|
+
});
|
|
17814
|
+
let children;
|
|
17815
|
+
try {
|
|
17816
|
+
children = await readdir3(absPath);
|
|
17817
|
+
} catch (error) {
|
|
17818
|
+
throw createOpencodeOcError("read", `Failed to read project overlay directory ${overlayRelativePath}: ${formatUnknownError(error)}`);
|
|
17819
|
+
}
|
|
17820
|
+
children.sort((left, right) => left.localeCompare(right));
|
|
17821
|
+
for (const childName of children) {
|
|
17822
|
+
const childAbsolutePath = join13(absPath, childName);
|
|
17823
|
+
const childRelativePath = toPosixPath(join13(overlayRelativePath, childName));
|
|
17824
|
+
await collectOverlayCandidatesFromPath(projectConfigRealPath, childAbsolutePath, childRelativePath, collector, seams);
|
|
17825
|
+
}
|
|
17826
|
+
await assertPathSnapshotUnchanged({
|
|
17827
|
+
absolutePath: absPath,
|
|
17828
|
+
overlayRelativePath,
|
|
17829
|
+
expectedSnapshot: discoveredSnapshot,
|
|
17830
|
+
phase: "overlay discovery",
|
|
17831
|
+
mustRemainDirectory: true
|
|
17832
|
+
});
|
|
17833
|
+
return;
|
|
17834
|
+
}
|
|
17835
|
+
if (!stats.isFile()) {
|
|
17836
|
+
return;
|
|
17837
|
+
}
|
|
17838
|
+
collector.push({
|
|
17839
|
+
sourcePath: absPath,
|
|
17840
|
+
overlayRelativePath: toPosixPath(overlayRelativePath),
|
|
17841
|
+
sourceSnapshot: discoveredSnapshot
|
|
17842
|
+
});
|
|
17843
|
+
}
|
|
17844
|
+
async function collectOverlayCandidates(projectConfigDir, seams = {}) {
|
|
17845
|
+
let projectConfigRealPath;
|
|
17846
|
+
try {
|
|
17847
|
+
projectConfigRealPath = await realpath(projectConfigDir);
|
|
17848
|
+
} catch (error) {
|
|
17849
|
+
throw createOpencodeOcError("validate", `Unable to resolve project config directory ${projectConfigDir}: ${formatUnknownError(error)}`);
|
|
17850
|
+
}
|
|
17851
|
+
let projectConfigRootStats;
|
|
17852
|
+
try {
|
|
17853
|
+
projectConfigRootStats = await lstat2(projectConfigDir);
|
|
17854
|
+
} catch (error) {
|
|
17855
|
+
throw createOpencodeOcError("validate", `Unable to inspect project overlay root ${projectConfigDir}: ${formatUnknownError(error)}`);
|
|
17856
|
+
}
|
|
17857
|
+
if (!projectConfigRootStats.isDirectory()) {
|
|
17858
|
+
throw createOpencodeOcError("validate", `Project overlay root changed before discovery: ${projectConfigDir}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17859
|
+
}
|
|
17860
|
+
const projectConfigRootSnapshot = captureOverlaySnapshot(projectConfigRootStats);
|
|
17861
|
+
const candidates = [];
|
|
17862
|
+
for (const scope of OPENCODE_OVERLAY_SOURCE_SCOPES) {
|
|
17863
|
+
const scopeAbsolutePath = join13(projectConfigDir, scope);
|
|
17864
|
+
await seams.beforeScopeInspect?.({
|
|
17865
|
+
scope,
|
|
17866
|
+
scopeAbsolutePath,
|
|
17867
|
+
projectConfigDir
|
|
17868
|
+
});
|
|
17869
|
+
await assertPathSnapshotUnchanged({
|
|
17870
|
+
absolutePath: projectConfigDir,
|
|
17871
|
+
overlayRelativePath: ".opencode",
|
|
17872
|
+
expectedSnapshot: projectConfigRootSnapshot,
|
|
17873
|
+
phase: "overlay discovery",
|
|
17874
|
+
mustRemainDirectory: true
|
|
17875
|
+
});
|
|
17876
|
+
let scopeStats;
|
|
17877
|
+
try {
|
|
17878
|
+
scopeStats = await lstat2(scopeAbsolutePath);
|
|
17879
|
+
} catch (error) {
|
|
17880
|
+
const errorCode = error.code;
|
|
17881
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
17882
|
+
continue;
|
|
17883
|
+
}
|
|
17884
|
+
throw createOpencodeOcError("read", `Failed to inspect project overlay scope ${scope}: ${formatUnknownError(error)}`);
|
|
17885
|
+
}
|
|
17886
|
+
if (scopeStats.isSymbolicLink()) {
|
|
17887
|
+
await rejectSymlinkEntry(projectConfigRealPath, scopeAbsolutePath, scope);
|
|
17888
|
+
}
|
|
17889
|
+
await collectOverlayCandidatesFromPath(projectConfigRealPath, scopeAbsolutePath, scope, candidates, seams);
|
|
17890
|
+
}
|
|
17891
|
+
candidates.sort((left, right) => left.overlayRelativePath.localeCompare(right.overlayRelativePath));
|
|
17892
|
+
return candidates;
|
|
17893
|
+
}
|
|
17894
|
+
function buildOverlayTempPublicationPath(destinationPath) {
|
|
17895
|
+
const destinationDirPath = dirname5(destinationPath);
|
|
17896
|
+
const destinationBaseName = basename3(destinationPath);
|
|
17897
|
+
const atomicSuffix = `${process.pid}-${randomUUID2()}`;
|
|
17898
|
+
return join13(destinationDirPath, `.${destinationBaseName}.ocx-tmp-${atomicSuffix}`);
|
|
17899
|
+
}
|
|
17900
|
+
async function publishOverlayFileAtomically(sourcePath, destinationPath) {
|
|
17901
|
+
const tempPublicationPath = buildOverlayTempPublicationPath(destinationPath);
|
|
17902
|
+
try {
|
|
17903
|
+
await copyFile(sourcePath, tempPublicationPath);
|
|
17904
|
+
await rename5(tempPublicationPath, destinationPath);
|
|
17905
|
+
} catch (error) {
|
|
17906
|
+
try {
|
|
17907
|
+
await unlink3(tempPublicationPath);
|
|
17908
|
+
} catch {}
|
|
17909
|
+
throw error;
|
|
17910
|
+
}
|
|
17911
|
+
}
|
|
17912
|
+
async function applyOverlayCopyOperations(operations, mergedConfigDir, seams = {}) {
|
|
17913
|
+
const publishAtomically = seams.publishAtomically ?? publishOverlayFileAtomically;
|
|
17914
|
+
let mergedRootStats;
|
|
17915
|
+
try {
|
|
17916
|
+
mergedRootStats = await lstat2(mergedConfigDir);
|
|
17917
|
+
} catch (error) {
|
|
17918
|
+
throw createOpencodeOcError("validate", `Unable to inspect merged overlay root ${mergedConfigDir}: ${formatUnknownError(error)}`);
|
|
17919
|
+
}
|
|
17920
|
+
if (!mergedRootStats.isDirectory()) {
|
|
17921
|
+
throw createOpencodeOcError("validate", `Merged overlay root is not a directory: ${mergedConfigDir}`);
|
|
17922
|
+
}
|
|
17923
|
+
const mergedRootSnapshot = captureOverlaySnapshot(mergedRootStats);
|
|
17924
|
+
for (const operation of operations) {
|
|
17925
|
+
await seams.beforeSourceVerification?.(operation);
|
|
17926
|
+
if (operation.sourceSnapshot) {
|
|
17927
|
+
await assertPathSnapshotUnchanged({
|
|
17928
|
+
absolutePath: operation.sourcePath,
|
|
17929
|
+
overlayRelativePath: operation.destinationRelativePath,
|
|
17930
|
+
expectedSnapshot: operation.sourceSnapshot,
|
|
17931
|
+
phase: "overlay source verification",
|
|
17932
|
+
mustRemainFile: true
|
|
17933
|
+
});
|
|
17934
|
+
}
|
|
17935
|
+
await assertPathSnapshotUnchanged({
|
|
17936
|
+
absolutePath: mergedConfigDir,
|
|
17937
|
+
overlayRelativePath: ".merged",
|
|
17938
|
+
expectedSnapshot: mergedRootSnapshot,
|
|
17939
|
+
phase: "overlay destination verification",
|
|
17940
|
+
mustRemainDirectory: true,
|
|
17941
|
+
compareContent: false
|
|
17942
|
+
});
|
|
17943
|
+
let destinationPath;
|
|
17944
|
+
try {
|
|
17945
|
+
destinationPath = validatePath(mergedConfigDir, operation.destinationRelativePath);
|
|
17946
|
+
} catch (error) {
|
|
17947
|
+
throw createOpencodeOcError("validate", `Overlay destination path is invalid (${operation.destinationRelativePath}): ${formatUnknownError(error)}`);
|
|
17948
|
+
}
|
|
17949
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17950
|
+
const destinationParentPath = dirname5(destinationPath);
|
|
17951
|
+
await seams.beforeDestinationParentCreate?.({
|
|
17952
|
+
operation,
|
|
17953
|
+
destinationPath,
|
|
17954
|
+
destinationParentPath
|
|
17955
|
+
});
|
|
17956
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17957
|
+
try {
|
|
17958
|
+
await mkdir8(destinationParentPath, { recursive: true });
|
|
17959
|
+
} catch (error) {
|
|
17960
|
+
throw createOpencodeOcError("copy", `Failed to copy overlay file ${operation.destinationRelativePath}: ${formatUnknownError(error)}`);
|
|
17961
|
+
}
|
|
17962
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17963
|
+
let destinationParentStats;
|
|
17964
|
+
try {
|
|
17965
|
+
destinationParentStats = await lstat2(destinationParentPath);
|
|
17966
|
+
} catch (error) {
|
|
17967
|
+
throw createOpencodeOcError("validate", `Failed to inspect overlay destination parent (${operation.destinationRelativePath}): ${formatUnknownError(error)}`);
|
|
17968
|
+
}
|
|
17969
|
+
if (!destinationParentStats.isDirectory()) {
|
|
17970
|
+
throw createOpencodeOcError("validate", `Overlay destination parent changed before publish (${operation.destinationRelativePath}). ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
17971
|
+
}
|
|
17972
|
+
const destinationParentSnapshot = captureOverlaySnapshot(destinationParentStats);
|
|
17973
|
+
await seams.beforeDestinationPublish?.({
|
|
17974
|
+
operation,
|
|
17975
|
+
destinationPath,
|
|
17976
|
+
destinationParentPath
|
|
17977
|
+
});
|
|
17978
|
+
await assertPathSnapshotUnchanged({
|
|
17979
|
+
absolutePath: mergedConfigDir,
|
|
17980
|
+
overlayRelativePath: ".merged",
|
|
17981
|
+
expectedSnapshot: mergedRootSnapshot,
|
|
17982
|
+
phase: "overlay destination publish",
|
|
17983
|
+
mustRemainDirectory: true,
|
|
17984
|
+
compareContent: false
|
|
17985
|
+
});
|
|
17986
|
+
await assertPathSnapshotUnchanged({
|
|
17987
|
+
absolutePath: destinationParentPath,
|
|
17988
|
+
overlayRelativePath: operation.destinationRelativePath,
|
|
17989
|
+
expectedSnapshot: destinationParentSnapshot,
|
|
17990
|
+
phase: "overlay destination publish",
|
|
17991
|
+
mustRemainDirectory: true,
|
|
17992
|
+
compareContent: false
|
|
17993
|
+
});
|
|
17994
|
+
await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
|
|
17995
|
+
try {
|
|
17996
|
+
await publishAtomically(operation.sourcePath, destinationPath);
|
|
17997
|
+
} catch (error) {
|
|
17998
|
+
if (error instanceof ConfigError) {
|
|
17999
|
+
throw error;
|
|
18000
|
+
}
|
|
18001
|
+
throw createOpencodeOcError("copy", `Failed to copy overlay file ${operation.destinationRelativePath}: ${formatUnknownError(error)}`);
|
|
18002
|
+
}
|
|
18003
|
+
}
|
|
18004
|
+
}
|
|
18005
|
+
async function copyProfileBaseToMergedDir(profileDir, mergedConfigDir) {
|
|
18006
|
+
let profileEntries;
|
|
18007
|
+
try {
|
|
18008
|
+
profileEntries = await readdir3(profileDir);
|
|
18009
|
+
} catch (error) {
|
|
18010
|
+
throw createOpencodeOcError("copy", `Failed to read profile directory ${profileDir}: ${formatUnknownError(error)}`);
|
|
18011
|
+
}
|
|
18012
|
+
for (const entryName of profileEntries) {
|
|
18013
|
+
const sourcePath = join13(profileDir, entryName);
|
|
18014
|
+
const destinationPath = join13(mergedConfigDir, entryName);
|
|
18015
|
+
try {
|
|
18016
|
+
await cp2(sourcePath, destinationPath, { recursive: true, force: true, errorOnExist: false });
|
|
18017
|
+
} catch (error) {
|
|
18018
|
+
throw createOpencodeOcError("copy", `Failed to copy profile base file ${entryName}: ${formatUnknownError(error)}`);
|
|
18019
|
+
}
|
|
18020
|
+
}
|
|
18021
|
+
}
|
|
18022
|
+
async function cleanupMergedConfigDir(mergedConfigDir) {
|
|
18023
|
+
try {
|
|
18024
|
+
await rm5(mergedConfigDir, { recursive: true, force: true });
|
|
18025
|
+
} catch (error) {
|
|
18026
|
+
throw createOpencodeOcError("cleanup", `Failed to remove temporary merged config directory ${mergedConfigDir}: ${formatUnknownError(error)}`);
|
|
18027
|
+
}
|
|
18028
|
+
}
|
|
18029
|
+
function toOverlaySourceRelativePath(projectConfigDir, sourcePath) {
|
|
18030
|
+
const relativeSourcePath = toPosixPath(relative6(projectConfigDir, sourcePath));
|
|
18031
|
+
if (!relativeSourcePath || relativeSourcePath === ".") {
|
|
18032
|
+
throw createOpencodeOcError("validate", `Overlay source path failed relative parsing: ${sourcePath}`);
|
|
18033
|
+
}
|
|
18034
|
+
if (relativeSourcePath === ".." || relativeSourcePath.startsWith("../") || isAbsolute7(relativeSourcePath)) {
|
|
18035
|
+
throw createOpencodeOcError("validate", `Overlay source path escapes project overlay scope: ${sourcePath}`);
|
|
18036
|
+
}
|
|
18037
|
+
return relativeSourcePath;
|
|
18038
|
+
}
|
|
18039
|
+
function buildOverlayTransactionManifest(projectConfigDir, operations) {
|
|
18040
|
+
const parsedOperations = operations.map((operation) => {
|
|
18041
|
+
if (!operation.sourceSnapshot) {
|
|
18042
|
+
throw createOpencodeOcError("validate", `Overlay source snapshot missing for ${operation.destinationRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
18043
|
+
}
|
|
18044
|
+
return {
|
|
18045
|
+
sourceRelativePath: toOverlaySourceRelativePath(projectConfigDir, operation.sourcePath),
|
|
18046
|
+
destinationRelativePath: operation.destinationRelativePath,
|
|
18047
|
+
sourceSnapshot: operation.sourceSnapshot
|
|
18048
|
+
};
|
|
18049
|
+
});
|
|
18050
|
+
return {
|
|
18051
|
+
version: OVERLAY_TRANSACTION_MANIFEST_VERSION,
|
|
18052
|
+
projectConfigDir,
|
|
18053
|
+
operations: parsedOperations
|
|
18054
|
+
};
|
|
18055
|
+
}
|
|
18056
|
+
function resolveOverlayNativeTransactionHelper() {
|
|
18057
|
+
return null;
|
|
18058
|
+
}
|
|
18059
|
+
async function applyOverlayTransactionManifestWithJs(options2) {
|
|
18060
|
+
const operations = options2.manifest.operations.map((operation) => ({
|
|
18061
|
+
sourcePath: validatePath(options2.manifest.projectConfigDir, operation.sourceRelativePath),
|
|
18062
|
+
destinationRelativePath: operation.destinationRelativePath,
|
|
18063
|
+
sourceSnapshot: operation.sourceSnapshot
|
|
18064
|
+
}));
|
|
18065
|
+
await applyOverlayCopyOperations(operations, options2.mergedConfigDir, options2.copySeams);
|
|
18066
|
+
}
|
|
18067
|
+
async function executeOverlayMergeTransaction(options2) {
|
|
18068
|
+
if (options2.manifest.operations.length === 0) {
|
|
18069
|
+
return "best-effort-js";
|
|
18070
|
+
}
|
|
18071
|
+
const nativeHelper = options2.nativeHelper ?? resolveOverlayNativeTransactionHelper();
|
|
18072
|
+
if (nativeHelper) {
|
|
18073
|
+
await nativeHelper.applyManifest(options2.manifest, options2.mergedConfigDir);
|
|
18074
|
+
return "native-fd";
|
|
18075
|
+
}
|
|
18076
|
+
if (options2.hardeningMode === "native-fd-required") {
|
|
18077
|
+
throw createOpencodeOcError("validate", `Native fd helper required for overlay merge, but none is available in this Bun/Node runtime. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
|
|
18078
|
+
}
|
|
18079
|
+
await applyOverlayTransactionManifestWithJs({
|
|
18080
|
+
manifest: options2.manifest,
|
|
18081
|
+
mergedConfigDir: options2.mergedConfigDir,
|
|
18082
|
+
copySeams: options2.copySeams
|
|
18083
|
+
});
|
|
18084
|
+
return "best-effort-js";
|
|
18085
|
+
}
|
|
18086
|
+
async function resolveProjectOverlayConfigDir(localConfigDir) {
|
|
18087
|
+
const projectRootDir = dirname5(localConfigDir);
|
|
18088
|
+
let projectRealPath;
|
|
18089
|
+
try {
|
|
18090
|
+
projectRealPath = await realpath(projectRootDir);
|
|
18091
|
+
} catch (error) {
|
|
18092
|
+
throw createOpencodeOcError("validate", `Unable to resolve project directory ${projectRootDir}: ${formatUnknownError(error)}`);
|
|
18093
|
+
}
|
|
18094
|
+
let localConfigRealPath;
|
|
18095
|
+
try {
|
|
18096
|
+
localConfigRealPath = await realpath(localConfigDir);
|
|
18097
|
+
} catch (error) {
|
|
18098
|
+
throw createOpencodeOcError("validate", `Unable to resolve project config directory ${localConfigDir}: ${formatUnknownError(error)}`);
|
|
18099
|
+
}
|
|
18100
|
+
if (!isPathWithin(projectRealPath, localConfigRealPath)) {
|
|
18101
|
+
throw createOpencodeOcError("validate", `Project .opencode root resolves outside project directory: ${localConfigDir}`);
|
|
18102
|
+
}
|
|
18103
|
+
return localConfigRealPath;
|
|
18104
|
+
}
|
|
18105
|
+
function toPrimaryPrepareError(error) {
|
|
18106
|
+
if (error instanceof ConfigError) {
|
|
18107
|
+
return error;
|
|
18108
|
+
}
|
|
18109
|
+
return createOpencodeOcError("copy", `Failed to prepare temporary merged config directory: ${formatUnknownError(error)}`);
|
|
18110
|
+
}
|
|
18111
|
+
async function prepareMergedConfigDirForProfile(options2) {
|
|
18112
|
+
const localConfigDir = findLocalConfigDir(options2.projectDir);
|
|
18113
|
+
const hardeningMode = options2.hardeningMode ?? "best-effort-js";
|
|
18114
|
+
let mergedConfigDir = null;
|
|
18115
|
+
try {
|
|
18116
|
+
mergedConfigDir = await mkdtemp(join13(tmpdir(), OPENCODE_MERGED_DIR_PREFIX));
|
|
18117
|
+
await copyProfileBaseToMergedDir(options2.profileDir, mergedConfigDir);
|
|
18118
|
+
let hardeningLevel = "best-effort-js";
|
|
18119
|
+
if (localConfigDir) {
|
|
18120
|
+
const projectOverlayConfigDir = await resolveProjectOverlayConfigDir(localConfigDir);
|
|
18121
|
+
const policy = await loadProjectOverlayPolicy(projectOverlayConfigDir);
|
|
18122
|
+
const candidates = await collectOverlayCandidates(projectOverlayConfigDir, options2.seams?.collection);
|
|
18123
|
+
const copyPlan = planOverlayCopyOperations(candidates, policy);
|
|
18124
|
+
const manifest = buildOverlayTransactionManifest(projectOverlayConfigDir, copyPlan);
|
|
18125
|
+
hardeningLevel = await executeOverlayMergeTransaction({
|
|
18126
|
+
manifest,
|
|
18127
|
+
mergedConfigDir,
|
|
18128
|
+
hardeningMode,
|
|
18129
|
+
copySeams: options2.seams?.copy,
|
|
18130
|
+
nativeHelper: options2.seams?.nativeHelper
|
|
18131
|
+
});
|
|
18132
|
+
}
|
|
18133
|
+
const preparedPath = mergedConfigDir;
|
|
18134
|
+
return {
|
|
18135
|
+
path: preparedPath,
|
|
18136
|
+
cleanup: () => cleanupMergedConfigDir(preparedPath),
|
|
18137
|
+
hardeningLevel
|
|
18138
|
+
};
|
|
18139
|
+
} catch (error) {
|
|
18140
|
+
const primaryError = toPrimaryPrepareError(error);
|
|
18141
|
+
if (mergedConfigDir) {
|
|
18142
|
+
try {
|
|
18143
|
+
await cleanupMergedConfigDir(mergedConfigDir);
|
|
18144
|
+
} catch (cleanupError) {
|
|
18145
|
+
primaryError.message = `${primaryError.message}
|
|
18146
|
+
Cleanup warning: ${formatUnknownError(cleanupError)}`;
|
|
18147
|
+
}
|
|
18148
|
+
}
|
|
18149
|
+
throw primaryError;
|
|
18150
|
+
}
|
|
18151
|
+
}
|
|
18152
|
+
|
|
18153
|
+
// src/commands/opencode.ts
|
|
18154
|
+
function dedupeLastWins(items) {
|
|
18155
|
+
const seen = new Set;
|
|
18156
|
+
const result = [];
|
|
18157
|
+
for (let i = items.length - 1;i >= 0; i--) {
|
|
18158
|
+
const item = items[i];
|
|
18159
|
+
if (!seen.has(item)) {
|
|
18160
|
+
seen.add(item);
|
|
18161
|
+
result.unshift(item);
|
|
18162
|
+
}
|
|
18163
|
+
}
|
|
18164
|
+
return result;
|
|
18165
|
+
}
|
|
18166
|
+
function resolveOpenCodeBinary(opts) {
|
|
18167
|
+
return opts.configBin ?? opts.envBin ?? "opencode";
|
|
18168
|
+
}
|
|
18169
|
+
function buildOpenCodeEnv(opts) {
|
|
18170
|
+
const hasProfile = Boolean(opts.profileName);
|
|
18171
|
+
const {
|
|
18172
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: _inheritedDisableProjectConfig,
|
|
18173
|
+
...baseEnvWithoutDisableProjectConfig
|
|
18174
|
+
} = opts.baseEnv;
|
|
18175
|
+
return {
|
|
18176
|
+
...baseEnvWithoutDisableProjectConfig,
|
|
18177
|
+
...hasProfile && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
|
|
18178
|
+
OPENCODE_CONFIG_DIR: opts.configDir ?? (hasProfile ? getProfileDir(opts.profileName) : getGlobalConfigPath()),
|
|
18179
|
+
...opts.configContent && { OPENCODE_CONFIG_CONTENT: opts.configContent },
|
|
18180
|
+
...opts.profileName && { OCX_PROFILE: opts.profileName }
|
|
18181
|
+
};
|
|
18182
|
+
}
|
|
18183
|
+
function registerOpencodeCommand(program2) {
|
|
18184
|
+
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) => {
|
|
18185
|
+
try {
|
|
18186
|
+
await runOpencode(command.args, options2);
|
|
18187
|
+
} catch (error) {
|
|
18188
|
+
handleError(error);
|
|
18189
|
+
}
|
|
18190
|
+
});
|
|
18191
|
+
}
|
|
18192
|
+
async function runOpencode(args, options2) {
|
|
18193
|
+
const projectDir = process.cwd();
|
|
18194
|
+
const resolver = await ConfigResolver.create(projectDir, { profile: options2.profile });
|
|
18195
|
+
const config = resolver.resolve();
|
|
18196
|
+
const profile = resolver.getProfile();
|
|
18197
|
+
if (config.profileName) {
|
|
18198
|
+
logger.info(`Using profile: ${config.profileName}`);
|
|
18199
|
+
}
|
|
18200
|
+
const ocxConfig = profile?.ocx;
|
|
18201
|
+
const shouldRename = options2.rename !== false && ocxConfig?.renameWindow !== false;
|
|
18202
|
+
if (config.profileName) {
|
|
18203
|
+
const profileOpencodePath = getProfileOpencodeConfig(config.profileName);
|
|
17206
18204
|
const profileOpencodeFile = Bun.file(profileOpencodePath);
|
|
17207
18205
|
const hasOpencodeConfig = await profileOpencodeFile.exists();
|
|
17208
18206
|
if (!hasOpencodeConfig) {
|
|
@@ -17217,73 +18215,111 @@ async function runOpencode(args, options2) {
|
|
|
17217
18215
|
instructions: dedupedInstructions.length > 0 ? dedupedInstructions : undefined
|
|
17218
18216
|
} : undefined;
|
|
17219
18217
|
let proc = null;
|
|
18218
|
+
let mergedConfig = null;
|
|
18219
|
+
let primaryFailure = null;
|
|
18220
|
+
let childExitCode = null;
|
|
18221
|
+
let preSpawnSignalExitCode = null;
|
|
17220
18222
|
const sigintHandler = () => {
|
|
17221
18223
|
if (proc) {
|
|
17222
18224
|
proc.kill("SIGINT");
|
|
17223
18225
|
} else {
|
|
17224
|
-
|
|
17225
|
-
restoreTerminalTitle();
|
|
17226
|
-
}
|
|
17227
|
-
process.exit(130);
|
|
18226
|
+
preSpawnSignalExitCode = 130;
|
|
17228
18227
|
}
|
|
17229
18228
|
};
|
|
17230
18229
|
const sigtermHandler = () => {
|
|
17231
18230
|
if (proc) {
|
|
17232
18231
|
proc.kill("SIGTERM");
|
|
17233
18232
|
} else {
|
|
17234
|
-
|
|
17235
|
-
restoreTerminalTitle();
|
|
17236
|
-
}
|
|
17237
|
-
process.exit(143);
|
|
18233
|
+
preSpawnSignalExitCode = 143;
|
|
17238
18234
|
}
|
|
17239
18235
|
};
|
|
17240
|
-
process.on("SIGINT", sigintHandler);
|
|
17241
|
-
process.on("SIGTERM", sigtermHandler);
|
|
17242
18236
|
const exitHandler = () => {
|
|
17243
18237
|
if (shouldRename) {
|
|
17244
18238
|
restoreTerminalTitle();
|
|
17245
18239
|
}
|
|
17246
18240
|
};
|
|
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
18241
|
try {
|
|
17271
|
-
|
|
17272
|
-
process.
|
|
17273
|
-
process.
|
|
17274
|
-
|
|
18242
|
+
process.on("SIGINT", sigintHandler);
|
|
18243
|
+
process.on("SIGTERM", sigtermHandler);
|
|
18244
|
+
process.on("exit", exitHandler);
|
|
18245
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18246
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18247
|
+
return;
|
|
18248
|
+
}
|
|
18249
|
+
if (config.profileName) {
|
|
18250
|
+
mergedConfig = await prepareMergedConfigDirForProfile({
|
|
18251
|
+
projectDir,
|
|
18252
|
+
profileDir: getProfileDir(config.profileName)
|
|
18253
|
+
});
|
|
18254
|
+
}
|
|
18255
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18256
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18257
|
+
return;
|
|
18258
|
+
}
|
|
17275
18259
|
if (shouldRename) {
|
|
17276
|
-
|
|
18260
|
+
saveTerminalTitle();
|
|
18261
|
+
const gitInfo = await getGitInfo(projectDir);
|
|
18262
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18263
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18264
|
+
return;
|
|
18265
|
+
}
|
|
18266
|
+
setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
|
|
18267
|
+
}
|
|
18268
|
+
if (preSpawnSignalExitCode !== null) {
|
|
18269
|
+
childExitCode = preSpawnSignalExitCode;
|
|
18270
|
+
return;
|
|
17277
18271
|
}
|
|
17278
|
-
|
|
18272
|
+
const bin = resolveOpenCodeBinary({
|
|
18273
|
+
configBin: ocxConfig?.bin,
|
|
18274
|
+
envBin: process.env.OPENCODE_BIN
|
|
18275
|
+
});
|
|
18276
|
+
const configContent = configToPass ? JSON.stringify(configToPass) : undefined;
|
|
18277
|
+
try {
|
|
18278
|
+
proc = Bun.spawn({
|
|
18279
|
+
cmd: [bin, ...args],
|
|
18280
|
+
cwd: projectDir,
|
|
18281
|
+
env: buildOpenCodeEnv({
|
|
18282
|
+
baseEnv: process.env,
|
|
18283
|
+
profileName: config.profileName ?? undefined,
|
|
18284
|
+
configDir: mergedConfig?.path,
|
|
18285
|
+
configContent
|
|
18286
|
+
}),
|
|
18287
|
+
stdin: "inherit",
|
|
18288
|
+
stdout: "inherit",
|
|
18289
|
+
stderr: "inherit"
|
|
18290
|
+
});
|
|
18291
|
+
} catch (error) {
|
|
18292
|
+
throw createOpencodeOcError("spawn", `Failed to launch OpenCode binary "${bin}": ${error instanceof Error ? error.message : String(error)}`);
|
|
18293
|
+
}
|
|
18294
|
+
childExitCode = await proc.exited;
|
|
17279
18295
|
} catch (error) {
|
|
18296
|
+
primaryFailure = error instanceof Error ? error : createOpencodeOcError("spawn", `OpenCode process failed: ${String(error)}`);
|
|
18297
|
+
} finally {
|
|
17280
18298
|
process.off("SIGINT", sigintHandler);
|
|
17281
18299
|
process.off("SIGTERM", sigtermHandler);
|
|
17282
18300
|
process.off("exit", exitHandler);
|
|
17283
18301
|
if (shouldRename) {
|
|
17284
18302
|
restoreTerminalTitle();
|
|
17285
18303
|
}
|
|
17286
|
-
|
|
18304
|
+
if (mergedConfig) {
|
|
18305
|
+
try {
|
|
18306
|
+
await mergedConfig.cleanup();
|
|
18307
|
+
} catch (cleanupError) {
|
|
18308
|
+
const hasPrimaryFailure = primaryFailure !== null || preSpawnSignalExitCode !== null || childExitCode !== null && childExitCode !== 0;
|
|
18309
|
+
if (hasPrimaryFailure) {
|
|
18310
|
+
logger.warn(`Cleanup warning: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
18311
|
+
} else {
|
|
18312
|
+
const cleanupFailure = cleanupError instanceof Error ? cleanupError : createOpencodeOcError("cleanup", `Failed to remove temporary merged config directory: ${String(cleanupError)}`);
|
|
18313
|
+
primaryFailure = cleanupFailure;
|
|
18314
|
+
}
|
|
18315
|
+
}
|
|
18316
|
+
}
|
|
18317
|
+
}
|
|
18318
|
+
if (primaryFailure) {
|
|
18319
|
+
throw primaryFailure;
|
|
18320
|
+
}
|
|
18321
|
+
if (childExitCode !== null) {
|
|
18322
|
+
process.exit(childExitCode);
|
|
17287
18323
|
}
|
|
17288
18324
|
}
|
|
17289
18325
|
|
|
@@ -17292,8 +18328,8 @@ init_errors2();
|
|
|
17292
18328
|
|
|
17293
18329
|
// src/commands/profile/install-from-registry.ts
|
|
17294
18330
|
import { existsSync as existsSync14 } from "fs";
|
|
17295
|
-
import { mkdir as
|
|
17296
|
-
import { dirname as
|
|
18331
|
+
import { mkdir as mkdir9, mkdtemp as mkdtemp2, rename as rename6, rm as rm6, writeFile as writeFile4 } from "fs/promises";
|
|
18332
|
+
import { dirname as dirname6, join as join14, relative as relative7 } from "path";
|
|
17297
18333
|
init_fetcher();
|
|
17298
18334
|
init_registry();
|
|
17299
18335
|
init_errors2();
|
|
@@ -17302,6 +18338,112 @@ function formatProfileRollbackCleanupWarning(action, targetPath, error) {
|
|
|
17302
18338
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
17303
18339
|
return `${action} "${targetPath}" (${errorMessage})`;
|
|
17304
18340
|
}
|
|
18341
|
+
function buildPackumentUrl(registryUrl, qualifiedName) {
|
|
18342
|
+
const [registryName, componentName] = qualifiedName.split("/");
|
|
18343
|
+
if (!registryName || !componentName) {
|
|
18344
|
+
return;
|
|
18345
|
+
}
|
|
18346
|
+
return `${normalizeRegistryUrl(registryUrl)}/components/${componentName}.json`;
|
|
18347
|
+
}
|
|
18348
|
+
function parseQualifiedNameParts(qualifiedName) {
|
|
18349
|
+
if (!qualifiedName)
|
|
18350
|
+
return null;
|
|
18351
|
+
const [registryName, componentName, ...extraParts] = qualifiedName.split("/");
|
|
18352
|
+
if (!registryName || !componentName || extraParts.length > 0) {
|
|
18353
|
+
return null;
|
|
18354
|
+
}
|
|
18355
|
+
return { registryName, componentName };
|
|
18356
|
+
}
|
|
18357
|
+
function inferDependencyFromErrorUrl(errorUrl, registries) {
|
|
18358
|
+
if (!errorUrl) {
|
|
18359
|
+
return {};
|
|
18360
|
+
}
|
|
18361
|
+
let componentName;
|
|
18362
|
+
try {
|
|
18363
|
+
const parsedErrorUrl = new URL(errorUrl);
|
|
18364
|
+
const packumentMatch = parsedErrorUrl.pathname.match(/(?:^|\/)components\/([^/]+)\.json$/);
|
|
18365
|
+
if (packumentMatch?.[1]) {
|
|
18366
|
+
componentName = decodeURIComponent(packumentMatch[1]);
|
|
18367
|
+
} else {
|
|
18368
|
+
const fileContentMatch = parsedErrorUrl.pathname.match(/(?:^|\/)components\/([^/]+)\/.+$/);
|
|
18369
|
+
if (fileContentMatch?.[1]) {
|
|
18370
|
+
componentName = decodeURIComponent(fileContentMatch[1]);
|
|
18371
|
+
}
|
|
18372
|
+
}
|
|
18373
|
+
} catch {}
|
|
18374
|
+
const normalizedErrorUrl = normalizeRegistryUrl(errorUrl);
|
|
18375
|
+
let registryName;
|
|
18376
|
+
let matchedPrefixLength = -1;
|
|
18377
|
+
for (const [candidateRegistryName, registryConfig] of Object.entries(registries)) {
|
|
18378
|
+
const normalizedRegistryUrl = normalizeRegistryUrl(registryConfig.url);
|
|
18379
|
+
const isMatch = normalizedErrorUrl === normalizedRegistryUrl || normalizedErrorUrl.startsWith(`${normalizedRegistryUrl}/`);
|
|
18380
|
+
if (!isMatch || normalizedRegistryUrl.length <= matchedPrefixLength) {
|
|
18381
|
+
continue;
|
|
18382
|
+
}
|
|
18383
|
+
registryName = candidateRegistryName;
|
|
18384
|
+
matchedPrefixLength = normalizedRegistryUrl.length;
|
|
18385
|
+
}
|
|
18386
|
+
return { registryName, componentName };
|
|
18387
|
+
}
|
|
18388
|
+
function resolveDependencyDiagnosticsContext(options2) {
|
|
18389
|
+
const fallbackQualifiedName = options2.depRefs[0] ?? `${options2.namespace}/${options2.component}`;
|
|
18390
|
+
const knownRegistries = {
|
|
18391
|
+
...options2.profileRegistries,
|
|
18392
|
+
...options2.profileRegistries[options2.namespace] ? {} : {
|
|
18393
|
+
[options2.namespace]: {
|
|
18394
|
+
url: options2.registryUrl
|
|
18395
|
+
}
|
|
18396
|
+
}
|
|
18397
|
+
};
|
|
18398
|
+
let registryName = options2.error.registryName;
|
|
18399
|
+
let qualifiedName = options2.error.qualifiedName;
|
|
18400
|
+
const qualifiedNameParts = parseQualifiedNameParts(qualifiedName);
|
|
18401
|
+
if (!registryName && qualifiedNameParts) {
|
|
18402
|
+
registryName = qualifiedNameParts.registryName;
|
|
18403
|
+
}
|
|
18404
|
+
const qualifiedComponentName = qualifiedNameParts?.componentName;
|
|
18405
|
+
const bareComponentNameFromError = qualifiedName && !qualifiedNameParts && !qualifiedName.includes("/") ? qualifiedName : undefined;
|
|
18406
|
+
const inferred = inferDependencyFromErrorUrl(options2.error.url, knownRegistries);
|
|
18407
|
+
if (!registryName && inferred.registryName) {
|
|
18408
|
+
registryName = inferred.registryName;
|
|
18409
|
+
}
|
|
18410
|
+
const resolvedComponentName = qualifiedComponentName ?? bareComponentNameFromError ?? inferred.componentName;
|
|
18411
|
+
if ((!qualifiedName || !qualifiedNameParts) && registryName && resolvedComponentName) {
|
|
18412
|
+
qualifiedName = `${registryName}/${resolvedComponentName}`;
|
|
18413
|
+
}
|
|
18414
|
+
if (!qualifiedName) {
|
|
18415
|
+
qualifiedName = fallbackQualifiedName;
|
|
18416
|
+
}
|
|
18417
|
+
if (!registryName) {
|
|
18418
|
+
registryName = parseQualifiedNameParts(qualifiedName)?.registryName ?? options2.namespace;
|
|
18419
|
+
}
|
|
18420
|
+
const fallbackRegistryUrl = knownRegistries[registryName]?.url || (registryName === options2.namespace ? options2.registryUrl : undefined);
|
|
18421
|
+
return {
|
|
18422
|
+
registryName,
|
|
18423
|
+
qualifiedName,
|
|
18424
|
+
fallbackUrl: options2.error.url || (fallbackRegistryUrl ? buildPackumentUrl(fallbackRegistryUrl, qualifiedName) : undefined)
|
|
18425
|
+
};
|
|
18426
|
+
}
|
|
18427
|
+
function withRegistryDiagnostics(error, context) {
|
|
18428
|
+
const phase = error.phase ?? context.fallbackPhase;
|
|
18429
|
+
const url2 = error.url ?? context.fallbackUrl;
|
|
18430
|
+
const diagnostics = [
|
|
18431
|
+
`phase: ${phase}`,
|
|
18432
|
+
`qualifiedName: ${context.qualifiedName}`,
|
|
18433
|
+
`registryContext: ${context.registryContext}`,
|
|
18434
|
+
`registryName: ${context.registryName}`,
|
|
18435
|
+
...url2 ? [`url: ${url2}`] : []
|
|
18436
|
+
];
|
|
18437
|
+
return new NetworkError(`${error.message} (${diagnostics.join(", ")})`, {
|
|
18438
|
+
url: url2,
|
|
18439
|
+
status: error.status,
|
|
18440
|
+
statusText: error.statusText,
|
|
18441
|
+
phase,
|
|
18442
|
+
qualifiedName: context.qualifiedName,
|
|
18443
|
+
registryContext: context.registryContext,
|
|
18444
|
+
registryName: context.registryName
|
|
18445
|
+
});
|
|
18446
|
+
}
|
|
17305
18447
|
function resolveEmbeddedProfileTarget(rawTarget, stagingDir) {
|
|
17306
18448
|
if (!rawTarget.startsWith(".opencode/")) {
|
|
17307
18449
|
throw new ValidationError(`Invalid embedded target "${rawTarget}": expected .opencode/ prefix for embedded profile files.`);
|
|
@@ -17319,7 +18461,7 @@ function resolveEmbeddedProfileTarget(rawTarget, stagingDir) {
|
|
|
17319
18461
|
}
|
|
17320
18462
|
throw error;
|
|
17321
18463
|
}
|
|
17322
|
-
const safeRelativeTarget =
|
|
18464
|
+
const safeRelativeTarget = relative7(stagingDir, safeAbsolutePath).replace(/\\/g, "/");
|
|
17323
18465
|
if (safeRelativeTarget === "." || safeRelativeTarget === "") {
|
|
17324
18466
|
throw new ValidationError(`Invalid embedded target "${rawTarget}": target must resolve to a file path.`);
|
|
17325
18467
|
}
|
|
@@ -17352,6 +18494,15 @@ async function installProfileFromRegistry(options2) {
|
|
|
17352
18494
|
manifest = await fetchComponent(registryUrl, component);
|
|
17353
18495
|
} catch (error) {
|
|
17354
18496
|
fetchSpin?.fail(`Failed to fetch ${qualifiedName}`);
|
|
18497
|
+
if (error instanceof NetworkError) {
|
|
18498
|
+
throw withRegistryDiagnostics(error, {
|
|
18499
|
+
registryContext: "source",
|
|
18500
|
+
registryName: namespace,
|
|
18501
|
+
qualifiedName,
|
|
18502
|
+
fallbackPhase: "packument-fetch",
|
|
18503
|
+
fallbackUrl: buildPackumentUrl(registryUrl, qualifiedName)
|
|
18504
|
+
});
|
|
18505
|
+
}
|
|
17355
18506
|
if (error instanceof NotFoundError) {
|
|
17356
18507
|
throw new NotFoundError(`Profile component "${qualifiedName}" not found in registry.
|
|
17357
18508
|
|
|
@@ -17386,8 +18537,8 @@ async function installProfileFromRegistry(options2) {
|
|
|
17386
18537
|
}
|
|
17387
18538
|
filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
|
|
17388
18539
|
const profilesDir = getProfilesDir();
|
|
17389
|
-
await
|
|
17390
|
-
const stagingDir = await
|
|
18540
|
+
await mkdir9(profilesDir, { recursive: true, mode: 448 });
|
|
18541
|
+
const stagingDir = await mkdtemp2(join14(profilesDir, ".staging-"));
|
|
17391
18542
|
let profilePromoted = false;
|
|
17392
18543
|
let installCommitted = false;
|
|
17393
18544
|
try {
|
|
@@ -17396,7 +18547,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17396
18547
|
const plannedWrites = new Map;
|
|
17397
18548
|
for (const file of profileFiles) {
|
|
17398
18549
|
const resolvedTarget = resolveComponentTargetRoot(file.target, stagingDir);
|
|
17399
|
-
const targetPath =
|
|
18550
|
+
const targetPath = join14(stagingDir, resolvedTarget);
|
|
17400
18551
|
registerPlannedWriteOrThrow(plannedWrites, {
|
|
17401
18552
|
absolutePath: targetPath,
|
|
17402
18553
|
relativePath: resolvedTarget,
|
|
@@ -17407,7 +18558,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17407
18558
|
for (const file of embeddedFiles) {
|
|
17408
18559
|
const target = resolveEmbeddedProfileTarget(file.target, stagingDir);
|
|
17409
18560
|
const resolvedTarget = resolveComponentTargetRoot(target, stagingDir);
|
|
17410
|
-
const targetPath =
|
|
18561
|
+
const targetPath = join14(stagingDir, resolvedTarget);
|
|
17411
18562
|
registerPlannedWriteOrThrow(plannedWrites, {
|
|
17412
18563
|
absolutePath: targetPath,
|
|
17413
18564
|
relativePath: resolvedTarget,
|
|
@@ -17417,9 +18568,9 @@ async function installProfileFromRegistry(options2) {
|
|
|
17417
18568
|
}
|
|
17418
18569
|
for (const plannedWrite of plannedWrites.values()) {
|
|
17419
18570
|
const targetPath = plannedWrite.absolutePath;
|
|
17420
|
-
const targetDir =
|
|
18571
|
+
const targetDir = dirname6(targetPath);
|
|
17421
18572
|
if (!existsSync14(targetDir)) {
|
|
17422
|
-
await
|
|
18573
|
+
await mkdir9(targetDir, { recursive: true });
|
|
17423
18574
|
}
|
|
17424
18575
|
await writeFile4(targetPath, plannedWrite.content);
|
|
17425
18576
|
}
|
|
@@ -17431,20 +18582,20 @@ async function installProfileFromRegistry(options2) {
|
|
|
17431
18582
|
});
|
|
17432
18583
|
const renameSpin = quiet ? null : createSpinner({ text: "Moving to profile directory..." });
|
|
17433
18584
|
renameSpin?.start();
|
|
17434
|
-
const profilesDir2 =
|
|
18585
|
+
const profilesDir2 = dirname6(profileDir);
|
|
17435
18586
|
if (!existsSync14(profilesDir2)) {
|
|
17436
|
-
await
|
|
18587
|
+
await mkdir9(profilesDir2, { recursive: true, mode: 448 });
|
|
17437
18588
|
}
|
|
17438
|
-
await
|
|
18589
|
+
await rename6(stagingDir, profileDir);
|
|
17439
18590
|
profilePromoted = true;
|
|
17440
18591
|
renameSpin?.succeed("Profile installed");
|
|
17441
18592
|
if (manifest.dependencies.length > 0) {
|
|
17442
18593
|
const depsSpin = quiet ? null : createSpinner({ text: "Installing dependencies..." });
|
|
17443
18594
|
depsSpin?.start();
|
|
18595
|
+
const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
|
|
18596
|
+
let profileRegistries = {};
|
|
17444
18597
|
try {
|
|
17445
|
-
const
|
|
17446
|
-
const profileOcxConfigPath = join12(profileDir, "ocx.jsonc");
|
|
17447
|
-
let profileRegistries = {};
|
|
18598
|
+
const profileOcxConfigPath = join14(profileDir, "ocx.jsonc");
|
|
17448
18599
|
if (existsSync14(profileOcxConfigPath)) {
|
|
17449
18600
|
const profileOcxFile = Bun.file(profileOcxConfigPath);
|
|
17450
18601
|
const profileOcxContent = await profileOcxFile.text();
|
|
@@ -17465,6 +18616,23 @@ async function installProfileFromRegistry(options2) {
|
|
|
17465
18616
|
depsSpin?.succeed(`Installed ${manifest.dependencies.length} dependencies`);
|
|
17466
18617
|
} catch (error) {
|
|
17467
18618
|
depsSpin?.fail("Failed to install dependencies");
|
|
18619
|
+
if (error instanceof NetworkError) {
|
|
18620
|
+
const dependencyDiagnosticsContext = resolveDependencyDiagnosticsContext({
|
|
18621
|
+
error,
|
|
18622
|
+
depRefs,
|
|
18623
|
+
namespace,
|
|
18624
|
+
component,
|
|
18625
|
+
registryUrl,
|
|
18626
|
+
profileRegistries
|
|
18627
|
+
});
|
|
18628
|
+
throw withRegistryDiagnostics(error, {
|
|
18629
|
+
registryContext: "dependency",
|
|
18630
|
+
registryName: dependencyDiagnosticsContext.registryName,
|
|
18631
|
+
qualifiedName: dependencyDiagnosticsContext.qualifiedName,
|
|
18632
|
+
fallbackPhase: "packument-fetch",
|
|
18633
|
+
fallbackUrl: dependencyDiagnosticsContext.fallbackUrl
|
|
18634
|
+
});
|
|
18635
|
+
}
|
|
17468
18636
|
throw error;
|
|
17469
18637
|
}
|
|
17470
18638
|
}
|
|
@@ -17486,7 +18654,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17486
18654
|
const cleanupWarnings = [];
|
|
17487
18655
|
try {
|
|
17488
18656
|
if (existsSync14(stagingDir)) {
|
|
17489
|
-
await
|
|
18657
|
+
await rm6(stagingDir, { recursive: true });
|
|
17490
18658
|
}
|
|
17491
18659
|
} catch (cleanupError) {
|
|
17492
18660
|
cleanupWarnings.push(formatProfileRollbackCleanupWarning("Profile add rollback cleanup warning: failed to remove staging directory", stagingDir, cleanupError));
|
|
@@ -17494,7 +18662,7 @@ async function installProfileFromRegistry(options2) {
|
|
|
17494
18662
|
if (profilePromoted && !installCommitted) {
|
|
17495
18663
|
try {
|
|
17496
18664
|
if (existsSync14(profileDir)) {
|
|
17497
|
-
await
|
|
18665
|
+
await rm6(profileDir, { recursive: true, force: true });
|
|
17498
18666
|
}
|
|
17499
18667
|
} catch (cleanupError) {
|
|
17500
18668
|
cleanupWarnings.push(formatProfileRollbackCleanupWarning("Profile add rollback cleanup warning: failed to remove promoted profile", profileDir, cleanupError));
|
|
@@ -17906,7 +19074,7 @@ function registerProfileCommand(program2) {
|
|
|
17906
19074
|
|
|
17907
19075
|
// src/commands/registry.ts
|
|
17908
19076
|
import { existsSync as existsSync15 } from "fs";
|
|
17909
|
-
import { dirname as
|
|
19077
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
17910
19078
|
init_errors2();
|
|
17911
19079
|
async function runRegistryAddCore2(url2, options2, callbacks) {
|
|
17912
19080
|
if (callbacks.isLocked?.()) {
|
|
@@ -18033,7 +19201,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
|
|
|
18033
19201
|
return {
|
|
18034
19202
|
scope: "profile",
|
|
18035
19203
|
configPath,
|
|
18036
|
-
configDir:
|
|
19204
|
+
configDir: dirname7(configPath),
|
|
18037
19205
|
targetLabel: `profile '${options2.profile}' config`
|
|
18038
19206
|
};
|
|
18039
19207
|
}
|
|
@@ -18041,7 +19209,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
|
|
|
18041
19209
|
const configDir = getGlobalConfigPath();
|
|
18042
19210
|
return {
|
|
18043
19211
|
scope: "global",
|
|
18044
|
-
configPath:
|
|
19212
|
+
configPath: join15(configDir, "ocx.jsonc"),
|
|
18045
19213
|
configDir,
|
|
18046
19214
|
targetLabel: "global config"
|
|
18047
19215
|
};
|
|
@@ -18050,7 +19218,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
|
|
|
18050
19218
|
return {
|
|
18051
19219
|
scope: "local",
|
|
18052
19220
|
configPath: found.path,
|
|
18053
|
-
configDir: found.exists ?
|
|
19221
|
+
configDir: found.exists ? dirname7(found.path) : join15(cwd, ".opencode"),
|
|
18054
19222
|
targetLabel: "local config"
|
|
18055
19223
|
};
|
|
18056
19224
|
}
|
|
@@ -18171,7 +19339,7 @@ function registerRegistryCommand(program2) {
|
|
|
18171
19339
|
|
|
18172
19340
|
// src/commands/remove.ts
|
|
18173
19341
|
import { realpathSync } from "fs";
|
|
18174
|
-
import { rm as
|
|
19342
|
+
import { rm as rm7 } from "fs/promises";
|
|
18175
19343
|
import { sep } from "path";
|
|
18176
19344
|
|
|
18177
19345
|
// src/utils/component-ref-resolver.ts
|
|
@@ -18424,7 +19592,7 @@ ${details}`);
|
|
|
18424
19592
|
}
|
|
18425
19593
|
continue;
|
|
18426
19594
|
}
|
|
18427
|
-
await
|
|
19595
|
+
await rm7(deleteTarget, { force: true });
|
|
18428
19596
|
if (options2.verbose) {
|
|
18429
19597
|
logger.info(` \u2713 Removed ${fileEntry.path}`);
|
|
18430
19598
|
}
|
|
@@ -18676,11 +19844,11 @@ function tildify(absolutePath) {
|
|
|
18676
19844
|
function getRelativePathIfContained(parent, child) {
|
|
18677
19845
|
const normalizedParent = path8.normalize(parent);
|
|
18678
19846
|
const normalizedChild = path8.normalize(child);
|
|
18679
|
-
const
|
|
18680
|
-
if (
|
|
19847
|
+
const relative8 = path8.relative(normalizedParent, normalizedChild);
|
|
19848
|
+
if (relative8.startsWith("..") || path8.isAbsolute(relative8)) {
|
|
18681
19849
|
return null;
|
|
18682
19850
|
}
|
|
18683
|
-
return
|
|
19851
|
+
return relative8;
|
|
18684
19852
|
}
|
|
18685
19853
|
function isLexicallyInside(root, target) {
|
|
18686
19854
|
return getRelativePathIfContained(root, target) !== null;
|
|
@@ -19180,7 +20348,7 @@ init_errors2();
|
|
|
19180
20348
|
|
|
19181
20349
|
// src/self-update/version-provider.ts
|
|
19182
20350
|
class BuildTimeVersionProvider {
|
|
19183
|
-
version = "2.0.
|
|
20351
|
+
version = "2.0.1";
|
|
19184
20352
|
}
|
|
19185
20353
|
var defaultVersionProvider = new BuildTimeVersionProvider;
|
|
19186
20354
|
|
|
@@ -19565,13 +20733,16 @@ function registerSelfCommand(program2) {
|
|
|
19565
20733
|
}
|
|
19566
20734
|
|
|
19567
20735
|
// src/commands/update.ts
|
|
19568
|
-
import { randomUUID as
|
|
20736
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
19569
20737
|
import { existsSync as existsSync18 } from "fs";
|
|
19570
|
-
import { mkdir as
|
|
19571
|
-
import { dirname as
|
|
20738
|
+
import { mkdir as mkdir10, rename as rename7, rm as rm8, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
20739
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
19572
20740
|
init_fetcher();
|
|
19573
20741
|
init_registry();
|
|
19574
20742
|
init_errors2();
|
|
20743
|
+
function formatAddCommandHint(component, options2) {
|
|
20744
|
+
return `ocx add${options2.global ? " --global" : ""} ${component}`;
|
|
20745
|
+
}
|
|
19575
20746
|
function resolveUpdateFailureMessage(phase) {
|
|
19576
20747
|
return phase === "apply" ? "Failed to update components" : "Failed to check for updates";
|
|
19577
20748
|
}
|
|
@@ -19579,6 +20750,7 @@ function registerUpdateCommand(program2) {
|
|
|
19579
20750
|
const cmd = program2.command("update [components...]").description("Update installed components");
|
|
19580
20751
|
addCommonOptions(cmd);
|
|
19581
20752
|
addVerboseOption(cmd);
|
|
20753
|
+
addGlobalOption(cmd);
|
|
19582
20754
|
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
20755
|
try {
|
|
19584
20756
|
await runUpdate(components, options2);
|
|
@@ -19589,7 +20761,7 @@ function registerUpdateCommand(program2) {
|
|
|
19589
20761
|
}
|
|
19590
20762
|
async function runUpdate(componentNames, options2) {
|
|
19591
20763
|
const cwd = options2.cwd ?? process.cwd();
|
|
19592
|
-
const provider = await LocalConfigProvider.requireInitialized(cwd);
|
|
20764
|
+
const provider = options2.global ? await GlobalConfigProvider.requireInitialized() : await LocalConfigProvider.requireInitialized(cwd);
|
|
19593
20765
|
await runUpdateCore(componentNames, options2, provider);
|
|
19594
20766
|
}
|
|
19595
20767
|
async function runUpdateCore(componentNames, options2, provider, fileOps) {
|
|
@@ -19600,9 +20772,13 @@ async function runUpdateCore(componentNames, options2, provider, fileOps) {
|
|
|
19600
20772
|
const receipt = await readReceipt(provider.cwd);
|
|
19601
20773
|
if (!receipt || Object.keys(receipt.installed).length === 0) {
|
|
19602
20774
|
if (componentNames.length > 0) {
|
|
19603
|
-
|
|
20775
|
+
const requestedComponent = componentNames[0];
|
|
20776
|
+
if (!requestedComponent) {
|
|
20777
|
+
throw new Error("Unexpected: component name missing despite non-empty componentNames");
|
|
20778
|
+
}
|
|
20779
|
+
throw new NotFoundError(`Component '${requestedComponent}' is not installed. Run '${formatAddCommandHint(requestedComponent, options2)}' first.`);
|
|
19604
20780
|
}
|
|
19605
|
-
throw new NotFoundError(
|
|
20781
|
+
throw new NotFoundError(`No components installed. Run '${formatAddCommandHint("<component>", options2)}' first.`);
|
|
19606
20782
|
}
|
|
19607
20783
|
const hasComponents = componentNames.length > 0;
|
|
19608
20784
|
const hasAll = options2.all === true;
|
|
@@ -19725,7 +20901,7 @@ async function runUpdateCore(componentNames, options2, provider, fileOps) {
|
|
|
19725
20901
|
throw new ValidationError(`File "${file.path}" not found in component manifest for "${update.registryName}/${update.name}".`);
|
|
19726
20902
|
}
|
|
19727
20903
|
const resolvedTarget = resolveTargetPath(fileObj.target, isFlattened, provider.cwd);
|
|
19728
|
-
const targetPath =
|
|
20904
|
+
const targetPath = join16(provider.cwd, resolvedTarget);
|
|
19729
20905
|
registerPlannedWriteOrThrow(plannedWrites, {
|
|
19730
20906
|
absolutePath: targetPath,
|
|
19731
20907
|
relativePath: resolvedTarget,
|
|
@@ -19814,7 +20990,7 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19814
20990
|
const appliedWrites = [];
|
|
19815
20991
|
const tempPaths = new Set;
|
|
19816
20992
|
let finalized = false;
|
|
19817
|
-
const renameFile = options2.fileOps?.rename ??
|
|
20993
|
+
const renameFile = options2.fileOps?.rename ?? rename7;
|
|
19818
20994
|
const rollback = async () => {
|
|
19819
20995
|
if (finalized) {
|
|
19820
20996
|
return;
|
|
@@ -19822,20 +20998,20 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19822
20998
|
finalized = true;
|
|
19823
20999
|
for (const tempPath of tempPaths) {
|
|
19824
21000
|
try {
|
|
19825
|
-
await
|
|
21001
|
+
await rm8(tempPath, { force: true });
|
|
19826
21002
|
} catch {}
|
|
19827
21003
|
}
|
|
19828
21004
|
for (const appliedWrite of [...appliedWrites].reverse()) {
|
|
19829
21005
|
try {
|
|
19830
21006
|
if (appliedWrite.backupPath) {
|
|
19831
21007
|
if (existsSync18(appliedWrite.targetPath)) {
|
|
19832
|
-
await
|
|
21008
|
+
await rm8(appliedWrite.targetPath, { force: true, recursive: true });
|
|
19833
21009
|
}
|
|
19834
21010
|
if (existsSync18(appliedWrite.backupPath)) {
|
|
19835
21011
|
await renameFile(appliedWrite.backupPath, appliedWrite.targetPath);
|
|
19836
21012
|
}
|
|
19837
21013
|
} else if (existsSync18(appliedWrite.targetPath)) {
|
|
19838
|
-
await
|
|
21014
|
+
await rm8(appliedWrite.targetPath, { force: true, recursive: true });
|
|
19839
21015
|
}
|
|
19840
21016
|
} catch {}
|
|
19841
21017
|
}
|
|
@@ -19851,7 +21027,7 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19851
21027
|
}
|
|
19852
21028
|
if (existsSync18(appliedWrite.backupPath)) {
|
|
19853
21029
|
try {
|
|
19854
|
-
await
|
|
21030
|
+
await rm8(appliedWrite.backupPath, { force: true });
|
|
19855
21031
|
} catch (error) {
|
|
19856
21032
|
if (!options2.quiet) {
|
|
19857
21033
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -19864,9 +21040,9 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19864
21040
|
try {
|
|
19865
21041
|
for (const prepared of preparedUpdates) {
|
|
19866
21042
|
for (const preparedFile of prepared.preparedFiles) {
|
|
19867
|
-
const targetDir =
|
|
21043
|
+
const targetDir = dirname8(preparedFile.targetPath);
|
|
19868
21044
|
if (!existsSync18(targetDir)) {
|
|
19869
|
-
await
|
|
21045
|
+
await mkdir10(targetDir, { recursive: true });
|
|
19870
21046
|
}
|
|
19871
21047
|
if (existsSync18(preparedFile.targetPath)) {
|
|
19872
21048
|
const currentTargetStats = await stat4(preparedFile.targetPath);
|
|
@@ -19874,12 +21050,12 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
|
|
|
19874
21050
|
throw new ValidationError(`Cannot update "${preparedFile.resolvedTarget}": target path is a directory.`);
|
|
19875
21051
|
}
|
|
19876
21052
|
}
|
|
19877
|
-
const tempPath = `${preparedFile.targetPath}.ocx-update-tmp-${
|
|
21053
|
+
const tempPath = `${preparedFile.targetPath}.ocx-update-tmp-${randomUUID3()}`;
|
|
19878
21054
|
await writeFile5(tempPath, preparedFile.content);
|
|
19879
21055
|
tempPaths.add(tempPath);
|
|
19880
21056
|
let backupPath = null;
|
|
19881
21057
|
if (existsSync18(preparedFile.targetPath)) {
|
|
19882
|
-
backupPath = `${preparedFile.targetPath}.ocx-update-backup-${
|
|
21058
|
+
backupPath = `${preparedFile.targetPath}.ocx-update-backup-${randomUUID3()}`;
|
|
19883
21059
|
await renameFile(preparedFile.targetPath, backupPath);
|
|
19884
21060
|
appliedWrites.push({
|
|
19885
21061
|
targetPath: preparedFile.targetPath,
|
|
@@ -19950,7 +21126,7 @@ Please use a fully qualified name (alias/component).`);
|
|
|
19950
21126
|
});
|
|
19951
21127
|
if (matchingIds.length === 0) {
|
|
19952
21128
|
throw new NotFoundError(`Component '${name}' is not installed.
|
|
19953
|
-
Run '
|
|
21129
|
+
Run '${formatAddCommandHint(name, options2)}' to install it first.`);
|
|
19954
21130
|
}
|
|
19955
21131
|
const canonicalId = matchingIds[0];
|
|
19956
21132
|
if (!canonicalId) {
|
|
@@ -19977,6 +21153,92 @@ function outputUpdateDryRun(results, options2) {
|
|
|
19977
21153
|
outputDryRun(dryRunResult, { json: options2.json, quiet: options2.quiet });
|
|
19978
21154
|
}
|
|
19979
21155
|
|
|
21156
|
+
// src/commands/validate.ts
|
|
21157
|
+
import { resolve as resolve7 } from "path";
|
|
21158
|
+
init_errors2();
|
|
21159
|
+
function createLoadValidationError2(message, errorKind) {
|
|
21160
|
+
if (errorKind === "not_found") {
|
|
21161
|
+
return new NotFoundError(message);
|
|
21162
|
+
}
|
|
21163
|
+
if (errorKind === "parse_error") {
|
|
21164
|
+
return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
21165
|
+
}
|
|
21166
|
+
return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
21167
|
+
}
|
|
21168
|
+
function createValidationFailureError2(errors3, failureType) {
|
|
21169
|
+
const summary = summarizeValidationErrors(errors3, {
|
|
21170
|
+
schemaErrors: failureType === "schema" ? errors3.length : 0
|
|
21171
|
+
});
|
|
21172
|
+
const details = {
|
|
21173
|
+
valid: false,
|
|
21174
|
+
errors: errors3,
|
|
21175
|
+
summary: {
|
|
21176
|
+
valid: false,
|
|
21177
|
+
totalErrors: summary.totalErrors,
|
|
21178
|
+
schemaErrors: summary.schemaErrors,
|
|
21179
|
+
sourceFileErrors: summary.sourceFileErrors,
|
|
21180
|
+
circularDependencyErrors: summary.circularDependencyErrors,
|
|
21181
|
+
duplicateTargetErrors: summary.duplicateTargetErrors,
|
|
21182
|
+
otherErrors: summary.otherErrors
|
|
21183
|
+
}
|
|
21184
|
+
};
|
|
21185
|
+
return new ValidationFailedError(details);
|
|
21186
|
+
}
|
|
21187
|
+
function outputValidationErrors(errors3) {
|
|
21188
|
+
for (const error of errors3) {
|
|
21189
|
+
console.log(kleur_default.red(` ${error}`));
|
|
21190
|
+
}
|
|
21191
|
+
}
|
|
21192
|
+
function registerValidateCommand(program2) {
|
|
21193
|
+
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 (path9, options2) => {
|
|
21194
|
+
try {
|
|
21195
|
+
const sourcePath = resolve7(options2.cwd, path9);
|
|
21196
|
+
const validationResult = await runCompleteValidation(sourcePath, {
|
|
21197
|
+
skipDuplicateTargets: options2.duplicateTargets === false
|
|
21198
|
+
});
|
|
21199
|
+
if (!validationResult.success) {
|
|
21200
|
+
const [firstError = "Registry validation failed"] = validationResult.errors;
|
|
21201
|
+
if (validationResult.failureType === "load") {
|
|
21202
|
+
const loadError = createLoadValidationError2(firstError, validationResult.loadErrorKind);
|
|
21203
|
+
if (!options2.json && options2.quiet) {
|
|
21204
|
+
const exitCode = loadError instanceof OCXError ? loadError.exitCode : EXIT_CODES.GENERAL;
|
|
21205
|
+
process.exit(exitCode);
|
|
21206
|
+
}
|
|
21207
|
+
throw loadError;
|
|
21208
|
+
}
|
|
21209
|
+
const validationError = createValidationFailureError2(validationResult.errors, validationResult.failureType === "schema" ? "schema" : "rules");
|
|
21210
|
+
if (options2.json) {
|
|
21211
|
+
throw validationError;
|
|
21212
|
+
}
|
|
21213
|
+
if (!options2.quiet) {
|
|
21214
|
+
logger.error(validationError.message);
|
|
21215
|
+
outputValidationErrors(validationResult.errors);
|
|
21216
|
+
}
|
|
21217
|
+
process.exit(validationError.exitCode);
|
|
21218
|
+
}
|
|
21219
|
+
if (!options2.quiet && !options2.json) {
|
|
21220
|
+
logger.success("\u2713 Registry source is valid");
|
|
21221
|
+
}
|
|
21222
|
+
if (options2.json) {
|
|
21223
|
+
outputJson({
|
|
21224
|
+
success: true,
|
|
21225
|
+
data: {
|
|
21226
|
+
valid: true,
|
|
21227
|
+
errors: [],
|
|
21228
|
+
summary: summarizeValidationErrors([])
|
|
21229
|
+
}
|
|
21230
|
+
});
|
|
21231
|
+
}
|
|
21232
|
+
} catch (error) {
|
|
21233
|
+
if (!options2.json && options2.quiet) {
|
|
21234
|
+
const exitCode = error instanceof OCXError ? error.exitCode : EXIT_CODES.GENERAL;
|
|
21235
|
+
process.exit(exitCode);
|
|
21236
|
+
}
|
|
21237
|
+
handleError(error, { json: options2.json });
|
|
21238
|
+
}
|
|
21239
|
+
});
|
|
21240
|
+
}
|
|
21241
|
+
|
|
19980
21242
|
// src/commands/verify.ts
|
|
19981
21243
|
init_errors2();
|
|
19982
21244
|
function registerVerifyCommand(program2) {
|
|
@@ -20093,7 +21355,7 @@ function registerUpdateCheckHook(program2) {
|
|
|
20093
21355
|
return;
|
|
20094
21356
|
}
|
|
20095
21357
|
const actionOptions = actionCommand.opts();
|
|
20096
|
-
if (actionOptions.json) {
|
|
21358
|
+
if (actionOptions.json || actionOptions.quiet) {
|
|
20097
21359
|
return;
|
|
20098
21360
|
}
|
|
20099
21361
|
if (!shouldCheckForUpdate())
|
|
@@ -20107,7 +21369,7 @@ function registerUpdateCheckHook(program2) {
|
|
|
20107
21369
|
});
|
|
20108
21370
|
}
|
|
20109
21371
|
// src/index.ts
|
|
20110
|
-
var version = "2.0.
|
|
21372
|
+
var version = "2.0.1";
|
|
20111
21373
|
async function main2() {
|
|
20112
21374
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
20113
21375
|
registerInitCommand(program2);
|
|
@@ -20116,6 +21378,7 @@ async function main2() {
|
|
|
20116
21378
|
registerSearchCommand(program2);
|
|
20117
21379
|
registerRegistryCommand(program2);
|
|
20118
21380
|
registerBuildCommand(program2);
|
|
21381
|
+
registerValidateCommand(program2);
|
|
20119
21382
|
registerSelfCommand(program2);
|
|
20120
21383
|
registerVerifyCommand(program2);
|
|
20121
21384
|
registerRemoveCommand(program2);
|
|
@@ -20140,4 +21403,4 @@ export {
|
|
|
20140
21403
|
buildRegistry
|
|
20141
21404
|
};
|
|
20142
21405
|
|
|
20143
|
-
//# debugId=
|
|
21406
|
+
//# debugId=4F8B9C341AC7574B64756E2164756E21
|