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 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.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(url, parse3) {
7064
- const cached = cache.get(url);
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(url);
7092
+ response = await fetch(requestUrl);
7072
7093
  } catch (error) {
7073
- throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
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: ${url}`);
7098
+ throw new NotFoundError(`Not found: ${requestUrl}`);
7078
7099
  }
7079
- throw new NetworkError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`, {
7080
- url,
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 ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
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(url, promise);
7094
- promise.catch(() => cache.delete(url));
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)}`, { url });
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}`, { url, status: response.status, statusText: 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 (!options2.json) {
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 join9 } from "path";
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 = join9(localConfigDir, OCX_CONFIG_FILE);
16428
+ configPath = join10(localConfigDir, OCX_CONFIG_FILE);
16039
16429
  } else {
16040
- const newConfigDir = join9(process.cwd(), LOCAL_CONFIG_DIR2);
16430
+ const newConfigDir = join10(process.cwd(), LOCAL_CONFIG_DIR2);
16041
16431
  await mkdir6(newConfigDir, { recursive: true });
16042
- configPath = join9(newConfigDir, OCX_CONFIG_FILE);
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 join10 } from "path";
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.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 = join10(destDir, ".ocx-temp");
16789
+ const tempDir = join11(destDir, ".ocx-temp");
16400
16790
  await mkdir7(tempDir, { recursive: true });
16401
16791
  try {
16402
- const tarPath = join10(tempDir, "template.tar.gz");
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 = join10(tempDir, extractedDir, TEMPLATE_PATH);
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 = join10(dir, file);
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 join11 } from "path";
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 = join11(globalRoot, "profiles");
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: join11(profilesDir, name) });
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
- function dedupeLastWins(items) {
17161
- const seen = new Set;
17162
- const result = [];
17163
- for (let i = items.length - 1;i >= 0; i--) {
17164
- const item = items[i];
17165
- if (!seen.has(item)) {
17166
- seen.add(item);
17167
- result.unshift(item);
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 result;
17581
+ return String(error);
17171
17582
  }
17172
- function resolveOpenCodeBinary(opts) {
17173
- return opts.configBin ?? opts.envBin ?? "opencode";
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 buildOpenCodeEnv(opts) {
17176
- const hasProfile = Boolean(opts.profileName);
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
- ...opts.baseEnv,
17179
- ...hasProfile && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
17180
- OPENCODE_CONFIG_DIR: hasProfile ? getProfileDir(opts.profileName) : getGlobalConfigPath(),
17181
- ...opts.configContent && { OPENCODE_CONFIG_CONTENT: opts.configContent },
17182
- ...opts.profileName && { OCX_PROFILE: opts.profileName }
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 registerOpencodeCommand(program2) {
17186
- 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) => {
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 runOpencode(command.args, options2);
17663
+ currentStats = await lstat2(currentPath);
17189
17664
  } catch (error) {
17190
- handleError(error);
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
- async function runOpencode(args, options2) {
17195
- const projectDir = process.cwd();
17196
- const resolver = await ConfigResolver.create(projectDir, { profile: options2.profile });
17197
- const config = resolver.resolve();
17198
- const profile = resolver.getProfile();
17199
- if (config.profileName) {
17200
- logger.info(`Using profile: ${config.profileName}`);
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
- const ocxConfig = profile?.ocx;
17203
- const shouldRename = options2.rename !== false && ocxConfig?.renameWindow !== false;
17204
- if (config.profileName) {
17205
- const profileOpencodePath = getProfileOpencodeConfig(config.profileName);
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
- if (shouldRename) {
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
- if (shouldRename) {
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
- const exitCode = await proc.exited;
17272
- process.off("SIGINT", sigintHandler);
17273
- process.off("SIGTERM", sigtermHandler);
17274
- process.off("exit", exitHandler);
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
- restoreTerminalTitle();
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
- process.exit(exitCode);
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
- throw error;
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 mkdir8, mkdtemp, rename as rename5, rm as rm5, writeFile as writeFile4 } from "fs/promises";
17296
- import { dirname as dirname5, join as join12, relative as relative6 } from "path";
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 = relative6(stagingDir, safeAbsolutePath).replace(/\\/g, "/");
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 mkdir8(profilesDir, { recursive: true, mode: 448 });
17390
- const stagingDir = await mkdtemp(join12(profilesDir, ".staging-"));
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 = join12(stagingDir, resolvedTarget);
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 = join12(stagingDir, resolvedTarget);
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 = dirname5(targetPath);
18571
+ const targetDir = dirname6(targetPath);
17421
18572
  if (!existsSync14(targetDir)) {
17422
- await mkdir8(targetDir, { recursive: true });
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 = dirname5(profileDir);
18585
+ const profilesDir2 = dirname6(profileDir);
17435
18586
  if (!existsSync14(profilesDir2)) {
17436
- await mkdir8(profilesDir2, { recursive: true, mode: 448 });
18587
+ await mkdir9(profilesDir2, { recursive: true, mode: 448 });
17437
18588
  }
17438
- await rename5(stagingDir, profileDir);
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 depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
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 rm5(stagingDir, { recursive: true });
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 rm5(profileDir, { recursive: true, force: true });
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 dirname6, join as join13 } from "path";
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: dirname6(configPath),
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: join13(configDir, "ocx.jsonc"),
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 ? dirname6(found.path) : join13(cwd, ".opencode"),
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 rm6 } from "fs/promises";
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 rm6(deleteTarget, { force: true });
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 relative7 = path8.relative(normalizedParent, normalizedChild);
18680
- if (relative7.startsWith("..") || path8.isAbsolute(relative7)) {
19847
+ const relative8 = path8.relative(normalizedParent, normalizedChild);
19848
+ if (relative8.startsWith("..") || path8.isAbsolute(relative8)) {
18681
19849
  return null;
18682
19850
  }
18683
- return relative7;
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.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 randomUUID2 } from "crypto";
20736
+ import { randomUUID as randomUUID3 } from "crypto";
19569
20737
  import { existsSync as existsSync18 } from "fs";
19570
- import { mkdir as mkdir9, rename as rename6, rm as rm7, stat as stat4, writeFile as writeFile5 } from "fs/promises";
19571
- import { dirname as dirname7, join as join14 } from "path";
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
- throw new NotFoundError(`Component '${componentNames[0]}' is not installed. Run 'ocx add ${componentNames[0]}' first.`);
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("No components installed. Run 'ocx add <component>' first.");
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 = join14(provider.cwd, resolvedTarget);
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 ?? rename6;
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 rm7(tempPath, { force: true });
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 rm7(appliedWrite.targetPath, { force: true, recursive: true });
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 rm7(appliedWrite.targetPath, { force: true, recursive: true });
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 rm7(appliedWrite.backupPath, { force: true });
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 = dirname7(preparedFile.targetPath);
21043
+ const targetDir = dirname8(preparedFile.targetPath);
19868
21044
  if (!existsSync18(targetDir)) {
19869
- await mkdir9(targetDir, { recursive: true });
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-${randomUUID2()}`;
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-${randomUUID2()}`;
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 'ocx add ${name}' to install it first.`);
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.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=851007C87A251D3864756E2164756E21
21406
+ //# debugId=4F8B9C341AC7574B64756E2164756E21