ocx 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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.2",
6102
6102
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
6103
6103
  author: "kdcokenny",
6104
6104
  license: "MIT",
@@ -6178,7 +6178,7 @@ var init_constants = __esm(() => {
6178
6178
  });
6179
6179
 
6180
6180
  // src/utils/errors.ts
6181
- var EXIT_CODES, OCXError, NotFoundError, NetworkError, ConfigError, ValidationError, ConflictError, IntegrityError, SelfUpdateError, RegistryCompatibilityError, ProfileNotFoundError, ProfileExistsError, RegistryExistsError, InvalidProfileNameError, ProfilesNotInitializedError, LocalProfileUnsupportedError;
6181
+ var EXIT_CODES, OCXError, NotFoundError, NetworkError, ConfigError, ValidationError, ValidationFailedError, ConflictError, IntegrityError, SelfUpdateError, RegistryCompatibilityError, ProfileNotFoundError, ProfileExistsError, RegistryExistsError, InvalidProfileNameError, ProfilesNotInitializedError, LocalProfileUnsupportedError;
6182
6182
  var init_errors2 = __esm(() => {
6183
6183
  EXIT_CODES = {
6184
6184
  SUCCESS: 0,
@@ -6209,12 +6209,20 @@ var init_errors2 = __esm(() => {
6209
6209
  url;
6210
6210
  status;
6211
6211
  statusText;
6212
+ phase;
6213
+ qualifiedName;
6214
+ registryContext;
6215
+ registryName;
6212
6216
  constructor(message, options) {
6213
6217
  super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
6214
6218
  this.name = "NetworkError";
6215
6219
  this.url = options?.url;
6216
6220
  this.status = options?.status;
6217
6221
  this.statusText = options?.statusText;
6222
+ this.phase = options?.phase;
6223
+ this.qualifiedName = options?.qualifiedName;
6224
+ this.registryContext = options?.registryContext;
6225
+ this.registryName = options?.registryName;
6218
6226
  }
6219
6227
  };
6220
6228
  ConfigError = class ConfigError extends OCXError {
@@ -6229,6 +6237,14 @@ var init_errors2 = __esm(() => {
6229
6237
  this.name = "ValidationError";
6230
6238
  }
6231
6239
  };
6240
+ ValidationFailedError = class ValidationFailedError extends OCXError {
6241
+ details;
6242
+ constructor(details) {
6243
+ super("Registry validation failed", "VALIDATION_FAILED", EXIT_CODES.CONFIG);
6244
+ this.details = details;
6245
+ this.name = "ValidationFailedError";
6246
+ }
6247
+ };
6232
6248
  ConflictError = class ConflictError extends OCXError {
6233
6249
  constructor(message) {
6234
6250
  super(message, "CONFLICT", EXIT_CODES.CONFLICT);
@@ -7060,24 +7076,29 @@ function adaptLegacyRegistryIndex(data, url) {
7060
7076
  components: adaptedComponents
7061
7077
  };
7062
7078
  }
7063
- async function fetchWithCache(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.2"}`;
16385
16775
  }
16386
16776
  function getTemplateUrl(version) {
16387
16777
  const ref = version === "main" ? "heads/main" : `tags/${version}`;
@@ -16396,10 +16786,10 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
16396
16786
  if (!response.ok || !response.body) {
16397
16787
  throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
16398
16788
  }
16399
- const tempDir = 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;
@@ -17104,6 +17494,9 @@ async function applyConfigNormalizationToFile(root, actions) {
17104
17494
  await writeFile3(configInfo.path, content2);
17105
17495
  }
17106
17496
 
17497
+ // src/commands/opencode.ts
17498
+ import * as path8 from "path";
17499
+
17107
17500
  // src/utils/terminal-title.ts
17108
17501
  import path7 from "path";
17109
17502
  var MAX_BRANCH_LENGTH = 20;
@@ -17156,51 +17549,704 @@ function formatTerminalName(cwd, profileName, gitInfo) {
17156
17549
  return `ocx[${profileName}]:${repoName}/${branch}`;
17157
17550
  }
17158
17551
 
17159
- // src/commands/opencode.ts
17160
- 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
- }
17552
+ // src/commands/opencode-overlay.ts
17553
+ import { randomUUID as randomUUID2 } from "crypto";
17554
+ import {
17555
+ copyFile,
17556
+ cp as cp2,
17557
+ lstat as lstat2,
17558
+ mkdir as mkdir8,
17559
+ mkdtemp,
17560
+ readdir as readdir3,
17561
+ readFile as readFile2,
17562
+ realpath,
17563
+ rename as rename5,
17564
+ rm as rm5,
17565
+ unlink as unlink3
17566
+ } from "fs/promises";
17567
+ import { tmpdir } from "os";
17568
+ import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute7, join as join13, relative as relative6 } from "path";
17569
+ var {Glob: Glob4 } = globalThis.Bun;
17570
+ init_zod();
17571
+ init_errors2();
17572
+ init_path_security();
17573
+ var OPENCODE_OVERLAY_SOURCE_SCOPES = ["agent", "agents", "skill", "skills"];
17574
+ var OPENCODE_MERGED_DIR_PREFIX = "ocx-oc-merged-";
17575
+ var OVERLAY_TRANSACTION_MANIFEST_VERSION = 1;
17576
+ var OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE = "Full TOCTOU/symlink-swap hardening requires an fd-based native helper transaction.";
17577
+ function createOpencodeOcError(errorClass, detail) {
17578
+ return new ConfigError(`ocx oc ${errorClass} error: ${detail}`);
17579
+ }
17580
+ function formatUnknownError(error) {
17581
+ if (error instanceof Error) {
17582
+ return error.message;
17169
17583
  }
17170
- return result;
17584
+ return String(error);
17171
17585
  }
17172
- function resolveOpenCodeBinary(opts) {
17173
- return opts.configBin ?? opts.envBin ?? "opencode";
17586
+ function formatJsoncParseError4(parseErrors) {
17587
+ if (parseErrors.length === 0) {
17588
+ return "Unknown parse error";
17589
+ }
17590
+ const firstError = parseErrors[0];
17591
+ if (!firstError) {
17592
+ return "Unknown parse error";
17593
+ }
17594
+ return `${printParseErrorCode(firstError.error)} at offset ${firstError.offset}`;
17174
17595
  }
17175
- function buildOpenCodeEnv(opts) {
17176
- const hasProfile = Boolean(opts.profileName);
17596
+ function toPosixPath(pathValue) {
17597
+ return pathValue.replaceAll("\\", "/");
17598
+ }
17599
+ function normalizeGlobPattern(pattern) {
17600
+ return pattern.startsWith("./") ? pattern.slice(2) : pattern;
17601
+ }
17602
+ function isPathWithin(parentPath, childPath) {
17603
+ const relativePath = relative6(parentPath, childPath);
17604
+ if (relativePath.length === 0) {
17605
+ return true;
17606
+ }
17607
+ return !relativePath.startsWith("..") && !isAbsolute7(relativePath);
17608
+ }
17609
+ function getOverlayEntryType(stats) {
17610
+ if (stats.isFile()) {
17611
+ return "file";
17612
+ }
17613
+ if (stats.isDirectory()) {
17614
+ return "directory";
17615
+ }
17616
+ if (stats.isSymbolicLink()) {
17617
+ return "symlink";
17618
+ }
17619
+ return "other";
17620
+ }
17621
+ function captureOverlaySnapshot(stats) {
17177
17622
  return {
17178
- ...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 }
17623
+ entryType: getOverlayEntryType(stats),
17624
+ device: String(stats.dev),
17625
+ inode: String(stats.ino),
17626
+ mode: String(stats.mode),
17627
+ size: String(stats.size),
17628
+ mtimeMs: String(stats.mtimeMs)
17183
17629
  };
17184
17630
  }
17185
- function 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) => {
17631
+ function overlaySnapshotIdentityMatches(expected, actual) {
17632
+ return expected.entryType === actual.entryType && expected.device === actual.device && expected.inode === actual.inode && expected.mode === actual.mode;
17633
+ }
17634
+ function overlaySnapshotContentMatches(expected, actual) {
17635
+ return expected.size === actual.size && expected.mtimeMs === actual.mtimeMs;
17636
+ }
17637
+ async function assertPathSnapshotUnchanged(options2) {
17638
+ let currentStats;
17639
+ try {
17640
+ currentStats = await lstat2(options2.absolutePath);
17641
+ } catch {
17642
+ throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
17643
+ }
17644
+ if (options2.mustRemainDirectory && !currentStats.isDirectory()) {
17645
+ throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
17646
+ }
17647
+ if (options2.mustRemainFile && !currentStats.isFile()) {
17648
+ throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
17649
+ }
17650
+ const currentSnapshot = captureOverlaySnapshot(currentStats);
17651
+ const identityMatches = overlaySnapshotIdentityMatches(options2.expectedSnapshot, currentSnapshot);
17652
+ const shouldCompareContent = options2.compareContent ?? true;
17653
+ const contentMatches = !shouldCompareContent ? true : overlaySnapshotContentMatches(options2.expectedSnapshot, currentSnapshot);
17654
+ if (!identityMatches || !contentMatches) {
17655
+ throw createOpencodeOcError("validate", `Overlay path changed during ${options2.phase}: ${options2.overlayRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
17656
+ }
17657
+ }
17658
+ async function assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, destinationRelativePath) {
17659
+ const relativeDestinationPath = toPosixPath(relative6(mergedConfigDir, destinationPath));
17660
+ const pathComponents = relativeDestinationPath.split("/").filter((component) => component.length > 0);
17661
+ let currentPath = mergedConfigDir;
17662
+ for (const component of pathComponents) {
17663
+ currentPath = join13(currentPath, component);
17664
+ let currentStats;
17187
17665
  try {
17188
- await runOpencode(command.args, options2);
17666
+ currentStats = await lstat2(currentPath);
17189
17667
  } catch (error) {
17190
- handleError(error);
17668
+ const errorCode = error.code;
17669
+ if (errorCode === "ENOENT") {
17670
+ return;
17671
+ }
17672
+ throw createOpencodeOcError("validate", `Failed to inspect overlay destination path (${destinationRelativePath}): ${formatUnknownError(error)}`);
17191
17673
  }
17192
- });
17674
+ if (currentStats.isSymbolicLink()) {
17675
+ throw createOpencodeOcError("validate", `Overlay destination path contains existing symlink (${destinationRelativePath})`);
17676
+ }
17677
+ }
17193
17678
  }
17194
- 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}`);
17679
+ var projectOverlayPolicySchema = exports_external.object({
17680
+ include: exports_external.array(exports_external.string()).optional(),
17681
+ exclude: exports_external.array(exports_external.string()).optional()
17682
+ }).passthrough();
17683
+ function validatePolicyPatterns(policyPath, field, patterns) {
17684
+ for (const pattern of patterns) {
17685
+ try {
17686
+ new Glob4(normalizeGlobPattern(pattern));
17687
+ } catch {
17688
+ throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: ${field} contains invalid glob pattern "${pattern}"`);
17689
+ }
17201
17690
  }
17202
- const ocxConfig = profile?.ocx;
17203
- const shouldRename = options2.rename !== false && ocxConfig?.renameWindow !== false;
17691
+ }
17692
+ async function loadProjectOverlayPolicy(localConfigDir) {
17693
+ if (!localConfigDir) {
17694
+ return { include: [], exclude: [] };
17695
+ }
17696
+ const policyPath = join13(localConfigDir, OCX_CONFIG_FILE);
17697
+ const policyReadPath = await resolveProjectOverlayPolicyReadPath(localConfigDir, policyPath);
17698
+ if (!policyReadPath) {
17699
+ return { include: [], exclude: [] };
17700
+ }
17701
+ let policyText;
17702
+ try {
17703
+ policyText = await readFile2(policyReadPath, "utf8");
17704
+ } catch (error) {
17705
+ throw createOpencodeOcError("read", `Failed to read project overlay policy at ${policyPath}: ${formatUnknownError(error)}`);
17706
+ }
17707
+ const parseErrors = [];
17708
+ const parsedPolicy = parse2(policyText, parseErrors, { allowTrailingComma: true });
17709
+ if (parseErrors.length > 0) {
17710
+ throw createOpencodeOcError("parse", `Failed to parse project overlay policy at ${policyPath}: ${formatJsoncParseError4(parseErrors)}`);
17711
+ }
17712
+ if (!parsedPolicy || typeof parsedPolicy !== "object" || Array.isArray(parsedPolicy)) {
17713
+ throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: root must be an object`);
17714
+ }
17715
+ const parsedResult = projectOverlayPolicySchema.safeParse(parsedPolicy);
17716
+ if (!parsedResult.success) {
17717
+ const firstIssue = parsedResult.error.issues[0];
17718
+ const issuePath = firstIssue?.path.length ? firstIssue.path.join(".") : "root";
17719
+ const issueMessage = firstIssue?.message ?? "Invalid project overlay policy";
17720
+ throw createOpencodeOcError("validate", `Invalid project overlay policy at ${policyPath}: ${issuePath} ${issueMessage}`);
17721
+ }
17722
+ const include = parsedResult.data.include ?? [];
17723
+ const exclude = parsedResult.data.exclude ?? [];
17724
+ validatePolicyPatterns(policyPath, "include", include);
17725
+ validatePolicyPatterns(policyPath, "exclude", exclude);
17726
+ return { include, exclude };
17727
+ }
17728
+ async function resolveProjectOverlayPolicyReadPath(localConfigDir, policyPath) {
17729
+ let policyStats;
17730
+ try {
17731
+ policyStats = await lstat2(policyPath);
17732
+ } catch (error) {
17733
+ const errorCode = error.code;
17734
+ if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
17735
+ return null;
17736
+ }
17737
+ throw createOpencodeOcError("read", `Failed to inspect project overlay policy at ${policyPath}: ${formatUnknownError(error)}`);
17738
+ }
17739
+ if (!policyStats.isSymbolicLink()) {
17740
+ return policyPath;
17741
+ }
17742
+ let projectConfigRealPath;
17743
+ try {
17744
+ projectConfigRealPath = await realpath(localConfigDir);
17745
+ } catch (error) {
17746
+ throw createOpencodeOcError("validate", `Unable to resolve project config directory ${localConfigDir}: ${formatUnknownError(error)}`);
17747
+ }
17748
+ let policyTargetRealPath;
17749
+ try {
17750
+ policyTargetRealPath = await realpath(policyPath);
17751
+ } catch (error) {
17752
+ const errorCode = error.code;
17753
+ if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
17754
+ throw createOpencodeOcError("validate", `Broken symlink in project overlay policy: ${policyPath}`);
17755
+ }
17756
+ throw createOpencodeOcError("read", `Failed to inspect project overlay policy symlink at ${policyPath}: ${formatUnknownError(error)}`);
17757
+ }
17758
+ if (!isPathWithin(projectConfigRealPath, policyTargetRealPath)) {
17759
+ throw createOpencodeOcError("validate", `Symlink escapes project overlay policy scope: ${policyPath}`);
17760
+ }
17761
+ return policyTargetRealPath;
17762
+ }
17763
+ function shouldIncludeOverlayPath(pathValue, policy) {
17764
+ for (const pattern of policy.include) {
17765
+ const glob = new Glob4(normalizeGlobPattern(pattern));
17766
+ if (glob.match(pathValue)) {
17767
+ return true;
17768
+ }
17769
+ }
17770
+ for (const pattern of policy.exclude) {
17771
+ const glob = new Glob4(normalizeGlobPattern(pattern));
17772
+ if (glob.match(pathValue)) {
17773
+ return false;
17774
+ }
17775
+ }
17776
+ return true;
17777
+ }
17778
+ function planOverlayCopyOperations(candidates, policy) {
17779
+ return candidates.filter((candidate) => shouldIncludeOverlayPath(candidate.overlayRelativePath, policy)).map((candidate) => ({
17780
+ sourcePath: candidate.sourcePath,
17781
+ destinationRelativePath: candidate.overlayRelativePath,
17782
+ sourceSnapshot: candidate.sourceSnapshot
17783
+ }));
17784
+ }
17785
+ async function rejectSymlinkEntry(projectConfigRealPath, absolutePath, overlayRelativePath) {
17786
+ let targetRealPath;
17787
+ try {
17788
+ targetRealPath = await realpath(absolutePath);
17789
+ } catch (error) {
17790
+ const errorCode = error.code;
17791
+ if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
17792
+ throw createOpencodeOcError("validate", `Broken symlink in project overlay scope: ${overlayRelativePath}`);
17793
+ }
17794
+ throw createOpencodeOcError("read", `Failed to inspect symlink at ${overlayRelativePath}: ${formatUnknownError(error)}`);
17795
+ }
17796
+ if (!isPathWithin(projectConfigRealPath, targetRealPath)) {
17797
+ throw createOpencodeOcError("validate", `Symlink escapes project overlay scope: ${overlayRelativePath}`);
17798
+ }
17799
+ throw createOpencodeOcError("validate", `Symlink entries are not supported in project overlay scope: ${overlayRelativePath}`);
17800
+ }
17801
+ async function collectOverlayCandidatesFromPath(projectConfigRealPath, absPath, overlayRelativePath, collector, seams) {
17802
+ let stats;
17803
+ try {
17804
+ stats = await lstat2(absPath);
17805
+ } catch (error) {
17806
+ throw createOpencodeOcError("read", `Failed to inspect project overlay path ${overlayRelativePath}: ${formatUnknownError(error)}`);
17807
+ }
17808
+ if (stats.isSymbolicLink()) {
17809
+ await rejectSymlinkEntry(projectConfigRealPath, absPath, overlayRelativePath);
17810
+ }
17811
+ const discoveredSnapshot = captureOverlaySnapshot(stats);
17812
+ if (stats.isDirectory()) {
17813
+ await seams.beforeDirectoryRead?.({
17814
+ absolutePath: absPath,
17815
+ overlayRelativePath
17816
+ });
17817
+ let children;
17818
+ try {
17819
+ children = await readdir3(absPath);
17820
+ } catch (error) {
17821
+ throw createOpencodeOcError("read", `Failed to read project overlay directory ${overlayRelativePath}: ${formatUnknownError(error)}`);
17822
+ }
17823
+ children.sort((left, right) => left.localeCompare(right));
17824
+ for (const childName of children) {
17825
+ const childAbsolutePath = join13(absPath, childName);
17826
+ const childRelativePath = toPosixPath(join13(overlayRelativePath, childName));
17827
+ await collectOverlayCandidatesFromPath(projectConfigRealPath, childAbsolutePath, childRelativePath, collector, seams);
17828
+ }
17829
+ await assertPathSnapshotUnchanged({
17830
+ absolutePath: absPath,
17831
+ overlayRelativePath,
17832
+ expectedSnapshot: discoveredSnapshot,
17833
+ phase: "overlay discovery",
17834
+ mustRemainDirectory: true
17835
+ });
17836
+ return;
17837
+ }
17838
+ if (!stats.isFile()) {
17839
+ return;
17840
+ }
17841
+ collector.push({
17842
+ sourcePath: absPath,
17843
+ overlayRelativePath: toPosixPath(overlayRelativePath),
17844
+ sourceSnapshot: discoveredSnapshot
17845
+ });
17846
+ }
17847
+ async function collectOverlayCandidates(projectConfigDir, seams = {}) {
17848
+ let projectConfigRealPath;
17849
+ try {
17850
+ projectConfigRealPath = await realpath(projectConfigDir);
17851
+ } catch (error) {
17852
+ throw createOpencodeOcError("validate", `Unable to resolve project config directory ${projectConfigDir}: ${formatUnknownError(error)}`);
17853
+ }
17854
+ let projectConfigRootStats;
17855
+ try {
17856
+ projectConfigRootStats = await lstat2(projectConfigDir);
17857
+ } catch (error) {
17858
+ throw createOpencodeOcError("validate", `Unable to inspect project overlay root ${projectConfigDir}: ${formatUnknownError(error)}`);
17859
+ }
17860
+ if (!projectConfigRootStats.isDirectory()) {
17861
+ throw createOpencodeOcError("validate", `Project overlay root changed before discovery: ${projectConfigDir}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
17862
+ }
17863
+ const projectConfigRootSnapshot = captureOverlaySnapshot(projectConfigRootStats);
17864
+ const candidates = [];
17865
+ for (const scope of OPENCODE_OVERLAY_SOURCE_SCOPES) {
17866
+ const scopeAbsolutePath = join13(projectConfigDir, scope);
17867
+ await seams.beforeScopeInspect?.({
17868
+ scope,
17869
+ scopeAbsolutePath,
17870
+ projectConfigDir
17871
+ });
17872
+ await assertPathSnapshotUnchanged({
17873
+ absolutePath: projectConfigDir,
17874
+ overlayRelativePath: ".opencode",
17875
+ expectedSnapshot: projectConfigRootSnapshot,
17876
+ phase: "overlay discovery",
17877
+ mustRemainDirectory: true
17878
+ });
17879
+ let scopeStats;
17880
+ try {
17881
+ scopeStats = await lstat2(scopeAbsolutePath);
17882
+ } catch (error) {
17883
+ const errorCode = error.code;
17884
+ if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
17885
+ continue;
17886
+ }
17887
+ throw createOpencodeOcError("read", `Failed to inspect project overlay scope ${scope}: ${formatUnknownError(error)}`);
17888
+ }
17889
+ if (scopeStats.isSymbolicLink()) {
17890
+ await rejectSymlinkEntry(projectConfigRealPath, scopeAbsolutePath, scope);
17891
+ }
17892
+ await collectOverlayCandidatesFromPath(projectConfigRealPath, scopeAbsolutePath, scope, candidates, seams);
17893
+ }
17894
+ candidates.sort((left, right) => left.overlayRelativePath.localeCompare(right.overlayRelativePath));
17895
+ return candidates;
17896
+ }
17897
+ function buildOverlayTempPublicationPath(destinationPath) {
17898
+ const destinationDirPath = dirname5(destinationPath);
17899
+ const destinationBaseName = basename3(destinationPath);
17900
+ const atomicSuffix = `${process.pid}-${randomUUID2()}`;
17901
+ return join13(destinationDirPath, `.${destinationBaseName}.ocx-tmp-${atomicSuffix}`);
17902
+ }
17903
+ async function publishOverlayFileAtomically(sourcePath, destinationPath) {
17904
+ const tempPublicationPath = buildOverlayTempPublicationPath(destinationPath);
17905
+ try {
17906
+ await copyFile(sourcePath, tempPublicationPath);
17907
+ await rename5(tempPublicationPath, destinationPath);
17908
+ } catch (error) {
17909
+ try {
17910
+ await unlink3(tempPublicationPath);
17911
+ } catch {}
17912
+ throw error;
17913
+ }
17914
+ }
17915
+ async function applyOverlayCopyOperations(operations, mergedConfigDir, seams = {}) {
17916
+ const publishAtomically = seams.publishAtomically ?? publishOverlayFileAtomically;
17917
+ let mergedRootStats;
17918
+ try {
17919
+ mergedRootStats = await lstat2(mergedConfigDir);
17920
+ } catch (error) {
17921
+ throw createOpencodeOcError("validate", `Unable to inspect merged overlay root ${mergedConfigDir}: ${formatUnknownError(error)}`);
17922
+ }
17923
+ if (!mergedRootStats.isDirectory()) {
17924
+ throw createOpencodeOcError("validate", `Merged overlay root is not a directory: ${mergedConfigDir}`);
17925
+ }
17926
+ const mergedRootSnapshot = captureOverlaySnapshot(mergedRootStats);
17927
+ for (const operation of operations) {
17928
+ await seams.beforeSourceVerification?.(operation);
17929
+ if (operation.sourceSnapshot) {
17930
+ await assertPathSnapshotUnchanged({
17931
+ absolutePath: operation.sourcePath,
17932
+ overlayRelativePath: operation.destinationRelativePath,
17933
+ expectedSnapshot: operation.sourceSnapshot,
17934
+ phase: "overlay source verification",
17935
+ mustRemainFile: true
17936
+ });
17937
+ }
17938
+ await assertPathSnapshotUnchanged({
17939
+ absolutePath: mergedConfigDir,
17940
+ overlayRelativePath: ".merged",
17941
+ expectedSnapshot: mergedRootSnapshot,
17942
+ phase: "overlay destination verification",
17943
+ mustRemainDirectory: true,
17944
+ compareContent: false
17945
+ });
17946
+ let destinationPath;
17947
+ try {
17948
+ destinationPath = validatePath(mergedConfigDir, operation.destinationRelativePath);
17949
+ } catch (error) {
17950
+ throw createOpencodeOcError("validate", `Overlay destination path is invalid (${operation.destinationRelativePath}): ${formatUnknownError(error)}`);
17951
+ }
17952
+ await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
17953
+ const destinationParentPath = dirname5(destinationPath);
17954
+ await seams.beforeDestinationParentCreate?.({
17955
+ operation,
17956
+ destinationPath,
17957
+ destinationParentPath
17958
+ });
17959
+ await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
17960
+ try {
17961
+ await mkdir8(destinationParentPath, { recursive: true });
17962
+ } catch (error) {
17963
+ throw createOpencodeOcError("copy", `Failed to copy overlay file ${operation.destinationRelativePath}: ${formatUnknownError(error)}`);
17964
+ }
17965
+ await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
17966
+ let destinationParentStats;
17967
+ try {
17968
+ destinationParentStats = await lstat2(destinationParentPath);
17969
+ } catch (error) {
17970
+ throw createOpencodeOcError("validate", `Failed to inspect overlay destination parent (${operation.destinationRelativePath}): ${formatUnknownError(error)}`);
17971
+ }
17972
+ if (!destinationParentStats.isDirectory()) {
17973
+ throw createOpencodeOcError("validate", `Overlay destination parent changed before publish (${operation.destinationRelativePath}). ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
17974
+ }
17975
+ const destinationParentSnapshot = captureOverlaySnapshot(destinationParentStats);
17976
+ await seams.beforeDestinationPublish?.({
17977
+ operation,
17978
+ destinationPath,
17979
+ destinationParentPath
17980
+ });
17981
+ await assertPathSnapshotUnchanged({
17982
+ absolutePath: mergedConfigDir,
17983
+ overlayRelativePath: ".merged",
17984
+ expectedSnapshot: mergedRootSnapshot,
17985
+ phase: "overlay destination publish",
17986
+ mustRemainDirectory: true,
17987
+ compareContent: false
17988
+ });
17989
+ await assertPathSnapshotUnchanged({
17990
+ absolutePath: destinationParentPath,
17991
+ overlayRelativePath: operation.destinationRelativePath,
17992
+ expectedSnapshot: destinationParentSnapshot,
17993
+ phase: "overlay destination publish",
17994
+ mustRemainDirectory: true,
17995
+ compareContent: false
17996
+ });
17997
+ await assertSafeOverlayDestinationPath(mergedConfigDir, destinationPath, operation.destinationRelativePath);
17998
+ try {
17999
+ await publishAtomically(operation.sourcePath, destinationPath);
18000
+ } catch (error) {
18001
+ if (error instanceof ConfigError) {
18002
+ throw error;
18003
+ }
18004
+ throw createOpencodeOcError("copy", `Failed to copy overlay file ${operation.destinationRelativePath}: ${formatUnknownError(error)}`);
18005
+ }
18006
+ }
18007
+ }
18008
+ async function copyProfileBaseToMergedDir(profileDir, mergedConfigDir) {
18009
+ let profileEntries;
18010
+ try {
18011
+ profileEntries = await readdir3(profileDir);
18012
+ } catch (error) {
18013
+ throw createOpencodeOcError("copy", `Failed to read profile directory ${profileDir}: ${formatUnknownError(error)}`);
18014
+ }
18015
+ for (const entryName of profileEntries) {
18016
+ const sourcePath = join13(profileDir, entryName);
18017
+ const destinationPath = join13(mergedConfigDir, entryName);
18018
+ try {
18019
+ await cp2(sourcePath, destinationPath, { recursive: true, force: true, errorOnExist: false });
18020
+ } catch (error) {
18021
+ throw createOpencodeOcError("copy", `Failed to copy profile base file ${entryName}: ${formatUnknownError(error)}`);
18022
+ }
18023
+ }
18024
+ }
18025
+ async function cleanupMergedConfigDir(mergedConfigDir) {
18026
+ try {
18027
+ await rm5(mergedConfigDir, { recursive: true, force: true });
18028
+ } catch (error) {
18029
+ throw createOpencodeOcError("cleanup", `Failed to remove temporary merged config directory ${mergedConfigDir}: ${formatUnknownError(error)}`);
18030
+ }
18031
+ }
18032
+ function toOverlaySourceRelativePath(projectConfigDir, sourcePath) {
18033
+ const relativeSourcePath = toPosixPath(relative6(projectConfigDir, sourcePath));
18034
+ if (!relativeSourcePath || relativeSourcePath === ".") {
18035
+ throw createOpencodeOcError("validate", `Overlay source path failed relative parsing: ${sourcePath}`);
18036
+ }
18037
+ if (relativeSourcePath === ".." || relativeSourcePath.startsWith("../") || isAbsolute7(relativeSourcePath)) {
18038
+ throw createOpencodeOcError("validate", `Overlay source path escapes project overlay scope: ${sourcePath}`);
18039
+ }
18040
+ return relativeSourcePath;
18041
+ }
18042
+ function buildOverlayTransactionManifest(projectConfigDir, operations) {
18043
+ const parsedOperations = operations.map((operation) => {
18044
+ if (!operation.sourceSnapshot) {
18045
+ throw createOpencodeOcError("validate", `Overlay source snapshot missing for ${operation.destinationRelativePath}. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
18046
+ }
18047
+ return {
18048
+ sourceRelativePath: toOverlaySourceRelativePath(projectConfigDir, operation.sourcePath),
18049
+ destinationRelativePath: operation.destinationRelativePath,
18050
+ sourceSnapshot: operation.sourceSnapshot
18051
+ };
18052
+ });
18053
+ return {
18054
+ version: OVERLAY_TRANSACTION_MANIFEST_VERSION,
18055
+ projectConfigDir,
18056
+ operations: parsedOperations
18057
+ };
18058
+ }
18059
+ function resolveOverlayNativeTransactionHelper() {
18060
+ return null;
18061
+ }
18062
+ async function applyOverlayTransactionManifestWithJs(options2) {
18063
+ const operations = options2.manifest.operations.map((operation) => ({
18064
+ sourcePath: validatePath(options2.manifest.projectConfigDir, operation.sourceRelativePath),
18065
+ destinationRelativePath: operation.destinationRelativePath,
18066
+ sourceSnapshot: operation.sourceSnapshot
18067
+ }));
18068
+ await applyOverlayCopyOperations(operations, options2.mergedConfigDir, options2.copySeams);
18069
+ }
18070
+ async function executeOverlayMergeTransaction(options2) {
18071
+ if (options2.manifest.operations.length === 0) {
18072
+ return "best-effort-js";
18073
+ }
18074
+ const nativeHelper = options2.nativeHelper ?? resolveOverlayNativeTransactionHelper();
18075
+ if (nativeHelper) {
18076
+ await nativeHelper.applyManifest(options2.manifest, options2.mergedConfigDir);
18077
+ return "native-fd";
18078
+ }
18079
+ if (options2.hardeningMode === "native-fd-required") {
18080
+ throw createOpencodeOcError("validate", `Native fd helper required for overlay merge, but none is available in this Bun/Node runtime. ${OVERLAY_NATIVE_HELPER_REQUIRED_MESSAGE}`);
18081
+ }
18082
+ await applyOverlayTransactionManifestWithJs({
18083
+ manifest: options2.manifest,
18084
+ mergedConfigDir: options2.mergedConfigDir,
18085
+ copySeams: options2.copySeams
18086
+ });
18087
+ return "best-effort-js";
18088
+ }
18089
+ async function resolveProjectOverlayConfigDir(localConfigDir) {
18090
+ const projectRootDir = dirname5(localConfigDir);
18091
+ let projectRealPath;
18092
+ try {
18093
+ projectRealPath = await realpath(projectRootDir);
18094
+ } catch (error) {
18095
+ throw createOpencodeOcError("validate", `Unable to resolve project directory ${projectRootDir}: ${formatUnknownError(error)}`);
18096
+ }
18097
+ let localConfigRealPath;
18098
+ try {
18099
+ localConfigRealPath = await realpath(localConfigDir);
18100
+ } catch (error) {
18101
+ throw createOpencodeOcError("validate", `Unable to resolve project config directory ${localConfigDir}: ${formatUnknownError(error)}`);
18102
+ }
18103
+ if (!isPathWithin(projectRealPath, localConfigRealPath)) {
18104
+ throw createOpencodeOcError("validate", `Project .opencode root resolves outside project directory: ${localConfigDir}`);
18105
+ }
18106
+ return localConfigRealPath;
18107
+ }
18108
+ function toPrimaryPrepareError(error) {
18109
+ if (error instanceof ConfigError) {
18110
+ return error;
18111
+ }
18112
+ return createOpencodeOcError("copy", `Failed to prepare temporary merged config directory: ${formatUnknownError(error)}`);
18113
+ }
18114
+ async function prepareMergedConfigDirForProfile(options2) {
18115
+ const localConfigDir = findLocalConfigDir(options2.projectDir);
18116
+ const hardeningMode = options2.hardeningMode ?? "best-effort-js";
18117
+ let mergedConfigDir = null;
18118
+ try {
18119
+ mergedConfigDir = await mkdtemp(join13(tmpdir(), OPENCODE_MERGED_DIR_PREFIX));
18120
+ await copyProfileBaseToMergedDir(options2.profileDir, mergedConfigDir);
18121
+ let hardeningLevel = "best-effort-js";
18122
+ if (localConfigDir) {
18123
+ const projectOverlayConfigDir = await resolveProjectOverlayConfigDir(localConfigDir);
18124
+ const policy = await loadProjectOverlayPolicy(projectOverlayConfigDir);
18125
+ const candidates = await collectOverlayCandidates(projectOverlayConfigDir, options2.seams?.collection);
18126
+ const copyPlan = planOverlayCopyOperations(candidates, policy);
18127
+ const manifest = buildOverlayTransactionManifest(projectOverlayConfigDir, copyPlan);
18128
+ hardeningLevel = await executeOverlayMergeTransaction({
18129
+ manifest,
18130
+ mergedConfigDir,
18131
+ hardeningMode,
18132
+ copySeams: options2.seams?.copy,
18133
+ nativeHelper: options2.seams?.nativeHelper
18134
+ });
18135
+ }
18136
+ const preparedPath = mergedConfigDir;
18137
+ return {
18138
+ path: preparedPath,
18139
+ cleanup: () => cleanupMergedConfigDir(preparedPath),
18140
+ hardeningLevel
18141
+ };
18142
+ } catch (error) {
18143
+ const primaryError = toPrimaryPrepareError(error);
18144
+ if (mergedConfigDir) {
18145
+ try {
18146
+ await cleanupMergedConfigDir(mergedConfigDir);
18147
+ } catch (cleanupError) {
18148
+ primaryError.message = `${primaryError.message}
18149
+ Cleanup warning: ${formatUnknownError(cleanupError)}`;
18150
+ }
18151
+ }
18152
+ throw primaryError;
18153
+ }
18154
+ }
18155
+
18156
+ // src/commands/opencode.ts
18157
+ function dedupeLastWins(items) {
18158
+ const seen = new Set;
18159
+ const result = [];
18160
+ for (let i = items.length - 1;i >= 0; i--) {
18161
+ const item = items[i];
18162
+ if (!seen.has(item)) {
18163
+ seen.add(item);
18164
+ result.unshift(item);
18165
+ }
18166
+ }
18167
+ return result;
18168
+ }
18169
+ function resolveOpenCodeBinary(opts) {
18170
+ return opts.configBin ?? opts.envBin ?? "opencode";
18171
+ }
18172
+ function isPathLikeLauncherToken(token) {
18173
+ return token.includes("/") || token.includes("\\");
18174
+ }
18175
+ function resolveStableOpenCodeLauncherPath(opts) {
18176
+ const { configuredBin, cwd } = opts;
18177
+ const resolveExecutable = opts.resolveExecutable ?? ((command) => Bun.which(command));
18178
+ if (!configuredBin.trim()) {
18179
+ throw new Error("OpenCode launcher is empty and cannot be resolved to a stable path");
18180
+ }
18181
+ if (isPathLikeLauncherToken(configuredBin)) {
18182
+ return path8.isAbsolute(configuredBin) ? configuredBin : path8.resolve(cwd, configuredBin);
18183
+ }
18184
+ const resolvedFromPath = resolveExecutable(configuredBin);
18185
+ if (!resolvedFromPath) {
18186
+ throw new Error(`OpenCode launcher "${configuredBin}" is not available in PATH and cannot be used as OPENCODE_BIN`);
18187
+ }
18188
+ return path8.isAbsolute(resolvedFromPath) ? resolvedFromPath : path8.resolve(cwd, resolvedFromPath);
18189
+ }
18190
+ function resolveStableOcxExecutablePath(opts) {
18191
+ const resolveExecutable = opts.resolveExecutable ?? ((command) => Bun.which(command));
18192
+ const argv = opts.argv ?? process.argv;
18193
+ const execPath = opts.execPath ?? process.execPath;
18194
+ const isCompiledBinary = opts.isCompiledBinary ?? (typeof Bun !== "undefined" && typeof Bun.main === "string" && Bun.main.startsWith("/$bunfs/"));
18195
+ const inheritedOcxBin = opts.inheritedOcxBin?.trim();
18196
+ const runtimeExecutable = isCompiledBinary ? execPath : argv[1];
18197
+ const candidate = inheritedOcxBin && inheritedOcxBin.length > 0 ? inheritedOcxBin : runtimeExecutable;
18198
+ if (!candidate?.trim()) {
18199
+ throw new Error("OCX executable path is empty and cannot be resolved from the current process");
18200
+ }
18201
+ if (isPathLikeLauncherToken(candidate)) {
18202
+ return path8.isAbsolute(candidate) ? candidate : path8.resolve(opts.cwd, candidate);
18203
+ }
18204
+ const resolvedFromPath = resolveExecutable(candidate);
18205
+ if (!resolvedFromPath) {
18206
+ throw new Error(`OCX executable "${candidate}" is not available in PATH and cannot be persisted as OCX_BIN`);
18207
+ }
18208
+ return path8.isAbsolute(resolvedFromPath) ? resolvedFromPath : path8.resolve(opts.cwd, resolvedFromPath);
18209
+ }
18210
+ function buildOpenCodeEnv(opts) {
18211
+ const hasProfile = Boolean(opts.profileName);
18212
+ const {
18213
+ OPENCODE_DISABLE_PROJECT_CONFIG: _inheritedDisableProjectConfig,
18214
+ OPENCODE_BIN: _inheritedOpencodeBin,
18215
+ OCX_CONTEXT: _inheritedOcxContext,
18216
+ OCX_BIN: _inheritedOcxBin,
18217
+ OCX_PROFILE: _inheritedOcxProfile,
18218
+ ...baseEnvWithoutDisableProjectConfig
18219
+ } = opts.baseEnv;
18220
+ return {
18221
+ ...baseEnvWithoutDisableProjectConfig,
18222
+ ...opts.opencodeBin !== undefined && { OPENCODE_BIN: opts.opencodeBin },
18223
+ ...hasProfile && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
18224
+ OPENCODE_CONFIG_DIR: opts.configDir ?? (hasProfile ? getProfileDir(opts.profileName) : getGlobalConfigPath()),
18225
+ ...opts.configContent && { OPENCODE_CONFIG_CONTENT: opts.configContent },
18226
+ ...hasProfile && { OCX_CONTEXT: "1" },
18227
+ ...hasProfile && opts.ocxBin && { OCX_BIN: opts.ocxBin },
18228
+ ...opts.profileName && { OCX_PROFILE: opts.profileName }
18229
+ };
18230
+ }
18231
+ function registerOpencodeCommand(program2) {
18232
+ program2.command("oc").alias("opencode").description("Launch OpenCode with resolved configuration").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
18233
+ try {
18234
+ await runOpencode(command.args, options2);
18235
+ } catch (error) {
18236
+ handleError(error);
18237
+ }
18238
+ });
18239
+ }
18240
+ async function runOpencode(args, options2) {
18241
+ const projectDir = process.cwd();
18242
+ const resolver = await ConfigResolver.create(projectDir, { profile: options2.profile });
18243
+ const config = resolver.resolve();
18244
+ const profile = resolver.getProfile();
18245
+ if (config.profileName) {
18246
+ logger.info(`Using profile: ${config.profileName}`);
18247
+ }
18248
+ const ocxConfig = profile?.ocx;
18249
+ const shouldRename = options2.rename !== false && ocxConfig?.renameWindow !== false;
17204
18250
  if (config.profileName) {
17205
18251
  const profileOpencodePath = getProfileOpencodeConfig(config.profileName);
17206
18252
  const profileOpencodeFile = Bun.file(profileOpencodePath);
@@ -17217,73 +18263,122 @@ async function runOpencode(args, options2) {
17217
18263
  instructions: dedupedInstructions.length > 0 ? dedupedInstructions : undefined
17218
18264
  } : undefined;
17219
18265
  let proc = null;
18266
+ let mergedConfig = null;
18267
+ let primaryFailure = null;
18268
+ let childExitCode = null;
18269
+ let preSpawnSignalExitCode = null;
17220
18270
  const sigintHandler = () => {
17221
18271
  if (proc) {
17222
18272
  proc.kill("SIGINT");
17223
18273
  } else {
17224
- if (shouldRename) {
17225
- restoreTerminalTitle();
17226
- }
17227
- process.exit(130);
18274
+ preSpawnSignalExitCode = 130;
17228
18275
  }
17229
18276
  };
17230
18277
  const sigtermHandler = () => {
17231
18278
  if (proc) {
17232
18279
  proc.kill("SIGTERM");
17233
18280
  } else {
17234
- if (shouldRename) {
17235
- restoreTerminalTitle();
17236
- }
17237
- process.exit(143);
18281
+ preSpawnSignalExitCode = 143;
17238
18282
  }
17239
18283
  };
17240
- process.on("SIGINT", sigintHandler);
17241
- process.on("SIGTERM", sigtermHandler);
17242
18284
  const exitHandler = () => {
17243
18285
  if (shouldRename) {
17244
18286
  restoreTerminalTitle();
17245
18287
  }
17246
18288
  };
17247
- process.on("exit", exitHandler);
17248
- if (shouldRename) {
17249
- saveTerminalTitle();
17250
- const gitInfo = await getGitInfo(projectDir);
17251
- setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
17252
- }
17253
- const bin = resolveOpenCodeBinary({
17254
- configBin: ocxConfig?.bin,
17255
- envBin: process.env.OPENCODE_BIN
17256
- });
17257
- const configContent = configToPass ? JSON.stringify(configToPass) : undefined;
17258
- proc = Bun.spawn({
17259
- cmd: [bin, ...args],
17260
- cwd: projectDir,
17261
- env: buildOpenCodeEnv({
17262
- baseEnv: process.env,
17263
- profileName: config.profileName ?? undefined,
17264
- configContent
17265
- }),
17266
- stdin: "inherit",
17267
- stdout: "inherit",
17268
- stderr: "inherit"
17269
- });
17270
18289
  try {
17271
- const exitCode = await proc.exited;
17272
- process.off("SIGINT", sigintHandler);
17273
- process.off("SIGTERM", sigtermHandler);
17274
- process.off("exit", exitHandler);
18290
+ process.on("SIGINT", sigintHandler);
18291
+ process.on("SIGTERM", sigtermHandler);
18292
+ process.on("exit", exitHandler);
18293
+ if (preSpawnSignalExitCode !== null) {
18294
+ childExitCode = preSpawnSignalExitCode;
18295
+ return;
18296
+ }
18297
+ if (config.profileName) {
18298
+ mergedConfig = await prepareMergedConfigDirForProfile({
18299
+ projectDir,
18300
+ profileDir: getProfileDir(config.profileName)
18301
+ });
18302
+ }
18303
+ if (preSpawnSignalExitCode !== null) {
18304
+ childExitCode = preSpawnSignalExitCode;
18305
+ return;
18306
+ }
17275
18307
  if (shouldRename) {
17276
- restoreTerminalTitle();
18308
+ saveTerminalTitle();
18309
+ const gitInfo = await getGitInfo(projectDir);
18310
+ if (preSpawnSignalExitCode !== null) {
18311
+ childExitCode = preSpawnSignalExitCode;
18312
+ return;
18313
+ }
18314
+ setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
17277
18315
  }
17278
- process.exit(exitCode);
18316
+ if (preSpawnSignalExitCode !== null) {
18317
+ childExitCode = preSpawnSignalExitCode;
18318
+ return;
18319
+ }
18320
+ const configuredBin = resolveOpenCodeBinary({
18321
+ configBin: ocxConfig?.bin,
18322
+ envBin: process.env.OPENCODE_BIN
18323
+ });
18324
+ const hasProfileLaunchContext = Boolean(config.profileName);
18325
+ const resolvedOpenCodeLaunchBin = hasProfileLaunchContext ? resolveStableOpenCodeLauncherPath({
18326
+ configuredBin,
18327
+ cwd: projectDir
18328
+ }) : configuredBin;
18329
+ const resolvedOcxBin = hasProfileLaunchContext ? resolveStableOcxExecutablePath({
18330
+ cwd: projectDir,
18331
+ inheritedOcxBin: process.env.OCX_BIN
18332
+ }) : undefined;
18333
+ const configContent = configToPass ? JSON.stringify(configToPass) : undefined;
18334
+ try {
18335
+ proc = Bun.spawn({
18336
+ cmd: [resolvedOpenCodeLaunchBin, ...args],
18337
+ cwd: projectDir,
18338
+ env: buildOpenCodeEnv({
18339
+ baseEnv: process.env,
18340
+ profileName: config.profileName ?? undefined,
18341
+ ocxBin: resolvedOcxBin,
18342
+ opencodeBin: resolvedOpenCodeLaunchBin,
18343
+ configDir: mergedConfig?.path,
18344
+ configContent
18345
+ }),
18346
+ stdin: "inherit",
18347
+ stdout: "inherit",
18348
+ stderr: "inherit"
18349
+ });
18350
+ } catch (error) {
18351
+ throw createOpencodeOcError("spawn", `Failed to launch OpenCode binary "${configuredBin}": ${error instanceof Error ? error.message : String(error)}`);
18352
+ }
18353
+ childExitCode = await proc.exited;
17279
18354
  } catch (error) {
18355
+ primaryFailure = error instanceof Error ? error : createOpencodeOcError("spawn", `OpenCode process failed: ${String(error)}`);
18356
+ } finally {
17280
18357
  process.off("SIGINT", sigintHandler);
17281
18358
  process.off("SIGTERM", sigtermHandler);
17282
18359
  process.off("exit", exitHandler);
17283
18360
  if (shouldRename) {
17284
18361
  restoreTerminalTitle();
17285
18362
  }
17286
- throw error;
18363
+ if (mergedConfig) {
18364
+ try {
18365
+ await mergedConfig.cleanup();
18366
+ } catch (cleanupError) {
18367
+ const hasPrimaryFailure = primaryFailure !== null || preSpawnSignalExitCode !== null || childExitCode !== null && childExitCode !== 0;
18368
+ if (hasPrimaryFailure) {
18369
+ logger.warn(`Cleanup warning: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
18370
+ } else {
18371
+ const cleanupFailure = cleanupError instanceof Error ? cleanupError : createOpencodeOcError("cleanup", `Failed to remove temporary merged config directory: ${String(cleanupError)}`);
18372
+ primaryFailure = cleanupFailure;
18373
+ }
18374
+ }
18375
+ }
18376
+ }
18377
+ if (primaryFailure) {
18378
+ throw primaryFailure;
18379
+ }
18380
+ if (childExitCode !== null) {
18381
+ process.exit(childExitCode);
17287
18382
  }
17288
18383
  }
17289
18384
 
@@ -17292,8 +18387,8 @@ init_errors2();
17292
18387
 
17293
18388
  // src/commands/profile/install-from-registry.ts
17294
18389
  import { existsSync as existsSync14 } from "fs";
17295
- import { mkdir as 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";
18390
+ import { mkdir as mkdir9, mkdtemp as mkdtemp2, rename as rename6, rm as rm6, writeFile as writeFile4 } from "fs/promises";
18391
+ import { dirname as dirname6, join as join14, relative as relative7 } from "path";
17297
18392
  init_fetcher();
17298
18393
  init_registry();
17299
18394
  init_errors2();
@@ -17302,6 +18397,112 @@ function formatProfileRollbackCleanupWarning(action, targetPath, error) {
17302
18397
  const errorMessage = error instanceof Error ? error.message : String(error);
17303
18398
  return `${action} "${targetPath}" (${errorMessage})`;
17304
18399
  }
18400
+ function buildPackumentUrl(registryUrl, qualifiedName) {
18401
+ const [registryName, componentName] = qualifiedName.split("/");
18402
+ if (!registryName || !componentName) {
18403
+ return;
18404
+ }
18405
+ return `${normalizeRegistryUrl(registryUrl)}/components/${componentName}.json`;
18406
+ }
18407
+ function parseQualifiedNameParts(qualifiedName) {
18408
+ if (!qualifiedName)
18409
+ return null;
18410
+ const [registryName, componentName, ...extraParts] = qualifiedName.split("/");
18411
+ if (!registryName || !componentName || extraParts.length > 0) {
18412
+ return null;
18413
+ }
18414
+ return { registryName, componentName };
18415
+ }
18416
+ function inferDependencyFromErrorUrl(errorUrl, registries) {
18417
+ if (!errorUrl) {
18418
+ return {};
18419
+ }
18420
+ let componentName;
18421
+ try {
18422
+ const parsedErrorUrl = new URL(errorUrl);
18423
+ const packumentMatch = parsedErrorUrl.pathname.match(/(?:^|\/)components\/([^/]+)\.json$/);
18424
+ if (packumentMatch?.[1]) {
18425
+ componentName = decodeURIComponent(packumentMatch[1]);
18426
+ } else {
18427
+ const fileContentMatch = parsedErrorUrl.pathname.match(/(?:^|\/)components\/([^/]+)\/.+$/);
18428
+ if (fileContentMatch?.[1]) {
18429
+ componentName = decodeURIComponent(fileContentMatch[1]);
18430
+ }
18431
+ }
18432
+ } catch {}
18433
+ const normalizedErrorUrl = normalizeRegistryUrl(errorUrl);
18434
+ let registryName;
18435
+ let matchedPrefixLength = -1;
18436
+ for (const [candidateRegistryName, registryConfig] of Object.entries(registries)) {
18437
+ const normalizedRegistryUrl = normalizeRegistryUrl(registryConfig.url);
18438
+ const isMatch = normalizedErrorUrl === normalizedRegistryUrl || normalizedErrorUrl.startsWith(`${normalizedRegistryUrl}/`);
18439
+ if (!isMatch || normalizedRegistryUrl.length <= matchedPrefixLength) {
18440
+ continue;
18441
+ }
18442
+ registryName = candidateRegistryName;
18443
+ matchedPrefixLength = normalizedRegistryUrl.length;
18444
+ }
18445
+ return { registryName, componentName };
18446
+ }
18447
+ function resolveDependencyDiagnosticsContext(options2) {
18448
+ const fallbackQualifiedName = options2.depRefs[0] ?? `${options2.namespace}/${options2.component}`;
18449
+ const knownRegistries = {
18450
+ ...options2.profileRegistries,
18451
+ ...options2.profileRegistries[options2.namespace] ? {} : {
18452
+ [options2.namespace]: {
18453
+ url: options2.registryUrl
18454
+ }
18455
+ }
18456
+ };
18457
+ let registryName = options2.error.registryName;
18458
+ let qualifiedName = options2.error.qualifiedName;
18459
+ const qualifiedNameParts = parseQualifiedNameParts(qualifiedName);
18460
+ if (!registryName && qualifiedNameParts) {
18461
+ registryName = qualifiedNameParts.registryName;
18462
+ }
18463
+ const qualifiedComponentName = qualifiedNameParts?.componentName;
18464
+ const bareComponentNameFromError = qualifiedName && !qualifiedNameParts && !qualifiedName.includes("/") ? qualifiedName : undefined;
18465
+ const inferred = inferDependencyFromErrorUrl(options2.error.url, knownRegistries);
18466
+ if (!registryName && inferred.registryName) {
18467
+ registryName = inferred.registryName;
18468
+ }
18469
+ const resolvedComponentName = qualifiedComponentName ?? bareComponentNameFromError ?? inferred.componentName;
18470
+ if ((!qualifiedName || !qualifiedNameParts) && registryName && resolvedComponentName) {
18471
+ qualifiedName = `${registryName}/${resolvedComponentName}`;
18472
+ }
18473
+ if (!qualifiedName) {
18474
+ qualifiedName = fallbackQualifiedName;
18475
+ }
18476
+ if (!registryName) {
18477
+ registryName = parseQualifiedNameParts(qualifiedName)?.registryName ?? options2.namespace;
18478
+ }
18479
+ const fallbackRegistryUrl = knownRegistries[registryName]?.url || (registryName === options2.namespace ? options2.registryUrl : undefined);
18480
+ return {
18481
+ registryName,
18482
+ qualifiedName,
18483
+ fallbackUrl: options2.error.url || (fallbackRegistryUrl ? buildPackumentUrl(fallbackRegistryUrl, qualifiedName) : undefined)
18484
+ };
18485
+ }
18486
+ function withRegistryDiagnostics(error, context) {
18487
+ const phase = error.phase ?? context.fallbackPhase;
18488
+ const url2 = error.url ?? context.fallbackUrl;
18489
+ const diagnostics = [
18490
+ `phase: ${phase}`,
18491
+ `qualifiedName: ${context.qualifiedName}`,
18492
+ `registryContext: ${context.registryContext}`,
18493
+ `registryName: ${context.registryName}`,
18494
+ ...url2 ? [`url: ${url2}`] : []
18495
+ ];
18496
+ return new NetworkError(`${error.message} (${diagnostics.join(", ")})`, {
18497
+ url: url2,
18498
+ status: error.status,
18499
+ statusText: error.statusText,
18500
+ phase,
18501
+ qualifiedName: context.qualifiedName,
18502
+ registryContext: context.registryContext,
18503
+ registryName: context.registryName
18504
+ });
18505
+ }
17305
18506
  function resolveEmbeddedProfileTarget(rawTarget, stagingDir) {
17306
18507
  if (!rawTarget.startsWith(".opencode/")) {
17307
18508
  throw new ValidationError(`Invalid embedded target "${rawTarget}": expected .opencode/ prefix for embedded profile files.`);
@@ -17319,7 +18520,7 @@ function resolveEmbeddedProfileTarget(rawTarget, stagingDir) {
17319
18520
  }
17320
18521
  throw error;
17321
18522
  }
17322
- const safeRelativeTarget = relative6(stagingDir, safeAbsolutePath).replace(/\\/g, "/");
18523
+ const safeRelativeTarget = relative7(stagingDir, safeAbsolutePath).replace(/\\/g, "/");
17323
18524
  if (safeRelativeTarget === "." || safeRelativeTarget === "") {
17324
18525
  throw new ValidationError(`Invalid embedded target "${rawTarget}": target must resolve to a file path.`);
17325
18526
  }
@@ -17352,6 +18553,15 @@ async function installProfileFromRegistry(options2) {
17352
18553
  manifest = await fetchComponent(registryUrl, component);
17353
18554
  } catch (error) {
17354
18555
  fetchSpin?.fail(`Failed to fetch ${qualifiedName}`);
18556
+ if (error instanceof NetworkError) {
18557
+ throw withRegistryDiagnostics(error, {
18558
+ registryContext: "source",
18559
+ registryName: namespace,
18560
+ qualifiedName,
18561
+ fallbackPhase: "packument-fetch",
18562
+ fallbackUrl: buildPackumentUrl(registryUrl, qualifiedName)
18563
+ });
18564
+ }
17355
18565
  if (error instanceof NotFoundError) {
17356
18566
  throw new NotFoundError(`Profile component "${qualifiedName}" not found in registry.
17357
18567
 
@@ -17386,8 +18596,8 @@ async function installProfileFromRegistry(options2) {
17386
18596
  }
17387
18597
  filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
17388
18598
  const profilesDir = getProfilesDir();
17389
- await mkdir8(profilesDir, { recursive: true, mode: 448 });
17390
- const stagingDir = await mkdtemp(join12(profilesDir, ".staging-"));
18599
+ await mkdir9(profilesDir, { recursive: true, mode: 448 });
18600
+ const stagingDir = await mkdtemp2(join14(profilesDir, ".staging-"));
17391
18601
  let profilePromoted = false;
17392
18602
  let installCommitted = false;
17393
18603
  try {
@@ -17396,7 +18606,7 @@ async function installProfileFromRegistry(options2) {
17396
18606
  const plannedWrites = new Map;
17397
18607
  for (const file of profileFiles) {
17398
18608
  const resolvedTarget = resolveComponentTargetRoot(file.target, stagingDir);
17399
- const targetPath = join12(stagingDir, resolvedTarget);
18609
+ const targetPath = join14(stagingDir, resolvedTarget);
17400
18610
  registerPlannedWriteOrThrow(plannedWrites, {
17401
18611
  absolutePath: targetPath,
17402
18612
  relativePath: resolvedTarget,
@@ -17407,7 +18617,7 @@ async function installProfileFromRegistry(options2) {
17407
18617
  for (const file of embeddedFiles) {
17408
18618
  const target = resolveEmbeddedProfileTarget(file.target, stagingDir);
17409
18619
  const resolvedTarget = resolveComponentTargetRoot(target, stagingDir);
17410
- const targetPath = join12(stagingDir, resolvedTarget);
18620
+ const targetPath = join14(stagingDir, resolvedTarget);
17411
18621
  registerPlannedWriteOrThrow(plannedWrites, {
17412
18622
  absolutePath: targetPath,
17413
18623
  relativePath: resolvedTarget,
@@ -17417,9 +18627,9 @@ async function installProfileFromRegistry(options2) {
17417
18627
  }
17418
18628
  for (const plannedWrite of plannedWrites.values()) {
17419
18629
  const targetPath = plannedWrite.absolutePath;
17420
- const targetDir = dirname5(targetPath);
18630
+ const targetDir = dirname6(targetPath);
17421
18631
  if (!existsSync14(targetDir)) {
17422
- await mkdir8(targetDir, { recursive: true });
18632
+ await mkdir9(targetDir, { recursive: true });
17423
18633
  }
17424
18634
  await writeFile4(targetPath, plannedWrite.content);
17425
18635
  }
@@ -17431,20 +18641,20 @@ async function installProfileFromRegistry(options2) {
17431
18641
  });
17432
18642
  const renameSpin = quiet ? null : createSpinner({ text: "Moving to profile directory..." });
17433
18643
  renameSpin?.start();
17434
- const profilesDir2 = dirname5(profileDir);
18644
+ const profilesDir2 = dirname6(profileDir);
17435
18645
  if (!existsSync14(profilesDir2)) {
17436
- await mkdir8(profilesDir2, { recursive: true, mode: 448 });
18646
+ await mkdir9(profilesDir2, { recursive: true, mode: 448 });
17437
18647
  }
17438
- await rename5(stagingDir, profileDir);
18648
+ await rename6(stagingDir, profileDir);
17439
18649
  profilePromoted = true;
17440
18650
  renameSpin?.succeed("Profile installed");
17441
18651
  if (manifest.dependencies.length > 0) {
17442
18652
  const depsSpin = quiet ? null : createSpinner({ text: "Installing dependencies..." });
17443
18653
  depsSpin?.start();
18654
+ const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
18655
+ let profileRegistries = {};
17444
18656
  try {
17445
- const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
17446
- const profileOcxConfigPath = join12(profileDir, "ocx.jsonc");
17447
- let profileRegistries = {};
18657
+ const profileOcxConfigPath = join14(profileDir, "ocx.jsonc");
17448
18658
  if (existsSync14(profileOcxConfigPath)) {
17449
18659
  const profileOcxFile = Bun.file(profileOcxConfigPath);
17450
18660
  const profileOcxContent = await profileOcxFile.text();
@@ -17465,6 +18675,23 @@ async function installProfileFromRegistry(options2) {
17465
18675
  depsSpin?.succeed(`Installed ${manifest.dependencies.length} dependencies`);
17466
18676
  } catch (error) {
17467
18677
  depsSpin?.fail("Failed to install dependencies");
18678
+ if (error instanceof NetworkError) {
18679
+ const dependencyDiagnosticsContext = resolveDependencyDiagnosticsContext({
18680
+ error,
18681
+ depRefs,
18682
+ namespace,
18683
+ component,
18684
+ registryUrl,
18685
+ profileRegistries
18686
+ });
18687
+ throw withRegistryDiagnostics(error, {
18688
+ registryContext: "dependency",
18689
+ registryName: dependencyDiagnosticsContext.registryName,
18690
+ qualifiedName: dependencyDiagnosticsContext.qualifiedName,
18691
+ fallbackPhase: "packument-fetch",
18692
+ fallbackUrl: dependencyDiagnosticsContext.fallbackUrl
18693
+ });
18694
+ }
17468
18695
  throw error;
17469
18696
  }
17470
18697
  }
@@ -17486,7 +18713,7 @@ async function installProfileFromRegistry(options2) {
17486
18713
  const cleanupWarnings = [];
17487
18714
  try {
17488
18715
  if (existsSync14(stagingDir)) {
17489
- await rm5(stagingDir, { recursive: true });
18716
+ await rm6(stagingDir, { recursive: true });
17490
18717
  }
17491
18718
  } catch (cleanupError) {
17492
18719
  cleanupWarnings.push(formatProfileRollbackCleanupWarning("Profile add rollback cleanup warning: failed to remove staging directory", stagingDir, cleanupError));
@@ -17494,7 +18721,7 @@ async function installProfileFromRegistry(options2) {
17494
18721
  if (profilePromoted && !installCommitted) {
17495
18722
  try {
17496
18723
  if (existsSync14(profileDir)) {
17497
- await rm5(profileDir, { recursive: true, force: true });
18724
+ await rm6(profileDir, { recursive: true, force: true });
17498
18725
  }
17499
18726
  } catch (cleanupError) {
17500
18727
  cleanupWarnings.push(formatProfileRollbackCleanupWarning("Profile add rollback cleanup warning: failed to remove promoted profile", profileDir, cleanupError));
@@ -17906,7 +19133,7 @@ function registerProfileCommand(program2) {
17906
19133
 
17907
19134
  // src/commands/registry.ts
17908
19135
  import { existsSync as existsSync15 } from "fs";
17909
- import { dirname as dirname6, join as join13 } from "path";
19136
+ import { dirname as dirname7, join as join15 } from "path";
17910
19137
  init_errors2();
17911
19138
  async function runRegistryAddCore2(url2, options2, callbacks) {
17912
19139
  if (callbacks.isLocked?.()) {
@@ -18033,7 +19260,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
18033
19260
  return {
18034
19261
  scope: "profile",
18035
19262
  configPath,
18036
- configDir: dirname6(configPath),
19263
+ configDir: dirname7(configPath),
18037
19264
  targetLabel: `profile '${options2.profile}' config`
18038
19265
  };
18039
19266
  }
@@ -18041,7 +19268,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
18041
19268
  const configDir = getGlobalConfigPath();
18042
19269
  return {
18043
19270
  scope: "global",
18044
- configPath: join13(configDir, "ocx.jsonc"),
19271
+ configPath: join15(configDir, "ocx.jsonc"),
18045
19272
  configDir,
18046
19273
  targetLabel: "global config"
18047
19274
  };
@@ -18050,7 +19277,7 @@ async function resolveRegistryTarget(options2, command, cwd) {
18050
19277
  return {
18051
19278
  scope: "local",
18052
19279
  configPath: found.path,
18053
- configDir: found.exists ? dirname6(found.path) : join13(cwd, ".opencode"),
19280
+ configDir: found.exists ? dirname7(found.path) : join15(cwd, ".opencode"),
18054
19281
  targetLabel: "local config"
18055
19282
  };
18056
19283
  }
@@ -18171,7 +19398,7 @@ function registerRegistryCommand(program2) {
18171
19398
 
18172
19399
  // src/commands/remove.ts
18173
19400
  import { realpathSync } from "fs";
18174
- import { rm as rm6 } from "fs/promises";
19401
+ import { rm as rm7 } from "fs/promises";
18175
19402
  import { sep } from "path";
18176
19403
 
18177
19404
  // src/utils/component-ref-resolver.ts
@@ -18424,7 +19651,7 @@ ${details}`);
18424
19651
  }
18425
19652
  continue;
18426
19653
  }
18427
- await rm6(deleteTarget, { force: true });
19654
+ await rm7(deleteTarget, { force: true });
18428
19655
  if (options2.verbose) {
18429
19656
  logger.info(` \u2713 Removed ${fileEntry.path}`);
18430
19657
  }
@@ -18581,7 +19808,7 @@ async function runSearchCore(query, options2, provider) {
18581
19808
  // src/commands/self/uninstall.ts
18582
19809
  import { existsSync as existsSync16, lstatSync as lstatSync2, readdirSync as readdirSync2, realpathSync as realpathSync2, rmSync, unlinkSync } from "fs";
18583
19810
  import { homedir as homedir5 } from "os";
18584
- import path8 from "path";
19811
+ import path9 from "path";
18585
19812
 
18586
19813
  // src/self-update/detect-method.ts
18587
19814
  init_errors2();
@@ -18595,11 +19822,11 @@ Valid methods: ${VALID_METHODS.join(", ")}`);
18595
19822
  return method;
18596
19823
  }
18597
19824
  var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
18598
- var isTempExecution = (path8) => path8.includes("/_npx/") || path8.includes("/.cache/bunx/") || path8.includes("/.pnpm/_temp/");
18599
- var isYarnGlobalInstall = (path8) => path8.includes("/.yarn/global") || path8.includes("/.config/yarn/global");
18600
- var isPnpmGlobalInstall = (path8) => path8.includes("/.pnpm/") || path8.includes("/pnpm/global");
18601
- var isBunGlobalInstall = (path8) => path8.includes("/.bun/bin") || path8.includes("/.bun/install/global");
18602
- var isNpmGlobalInstall = (path8) => path8.includes("/.npm/") || path8.includes("/node_modules/");
19825
+ var isTempExecution = (path9) => path9.includes("/_npx/") || path9.includes("/.cache/bunx/") || path9.includes("/.pnpm/_temp/");
19826
+ var isYarnGlobalInstall = (path9) => path9.includes("/.yarn/global") || path9.includes("/.config/yarn/global");
19827
+ var isPnpmGlobalInstall = (path9) => path9.includes("/.pnpm/") || path9.includes("/pnpm/global");
19828
+ var isBunGlobalInstall = (path9) => path9.includes("/.bun/bin") || path9.includes("/.bun/install/global");
19829
+ var isNpmGlobalInstall = (path9) => path9.includes("/.npm/") || path9.includes("/node_modules/");
18603
19830
  function detectInstallMethod() {
18604
19831
  if (isCompiledBinary()) {
18605
19832
  return "curl";
@@ -18668,19 +19895,19 @@ function tildify(absolutePath) {
18668
19895
  return absolutePath;
18669
19896
  if (absolutePath === home)
18670
19897
  return "~";
18671
- if (absolutePath.startsWith(home + path8.sep)) {
19898
+ if (absolutePath.startsWith(home + path9.sep)) {
18672
19899
  return `~${absolutePath.slice(home.length)}`;
18673
19900
  }
18674
19901
  return absolutePath;
18675
19902
  }
18676
19903
  function getRelativePathIfContained(parent, child) {
18677
- const normalizedParent = path8.normalize(parent);
18678
- const normalizedChild = path8.normalize(child);
18679
- const relative7 = path8.relative(normalizedParent, normalizedChild);
18680
- if (relative7.startsWith("..") || path8.isAbsolute(relative7)) {
19904
+ const normalizedParent = path9.normalize(parent);
19905
+ const normalizedChild = path9.normalize(child);
19906
+ const relative8 = path9.relative(normalizedParent, normalizedChild);
19907
+ if (relative8.startsWith("..") || path9.isAbsolute(relative8)) {
18681
19908
  return null;
18682
19909
  }
18683
- return relative7;
19910
+ return relative8;
18684
19911
  }
18685
19912
  function isLexicallyInside(root, target) {
18686
19913
  return getRelativePathIfContained(root, target) !== null;
@@ -18781,8 +20008,8 @@ function getPackageManagerCommand(method) {
18781
20008
  }
18782
20009
  }
18783
20010
  function getGlobalConfigRoot() {
18784
- const base = process.env.XDG_CONFIG_HOME || path8.join(homedir5(), ".config");
18785
- return path8.join(base, "opencode");
20011
+ const base = process.env.XDG_CONFIG_HOME || path9.join(homedir5(), ".config");
20012
+ return path9.join(base, "opencode");
18786
20013
  }
18787
20014
  function buildConfigTargets() {
18788
20015
  const rootPath = getGlobalConfigRoot();
@@ -18835,10 +20062,10 @@ function buildBinaryTarget() {
18835
20062
  if (method === "curl") {
18836
20063
  const binaryPath = getExecutablePath();
18837
20064
  const kind = getPathKind(binaryPath);
18838
- const parentDir = path8.dirname(binaryPath);
20065
+ const parentDir = path9.dirname(binaryPath);
18839
20066
  return {
18840
20067
  rootPath: parentDir,
18841
- relativePath: path8.basename(binaryPath),
20068
+ relativePath: path9.basename(binaryPath),
18842
20069
  absolutePath: binaryPath,
18843
20070
  displayPath: tildify(binaryPath),
18844
20071
  kind,
@@ -18884,8 +20111,8 @@ function executeRemovals(targets) {
18884
20111
  }
18885
20112
  function removeBinary(binaryPath, options2 = {}) {
18886
20113
  const target = {
18887
- rootPath: path8.dirname(binaryPath),
18888
- relativePath: path8.basename(binaryPath),
20114
+ rootPath: path9.dirname(binaryPath),
20115
+ relativePath: path9.basename(binaryPath),
18889
20116
  absolutePath: binaryPath,
18890
20117
  displayPath: tildify(binaryPath),
18891
20118
  kind: getPathKind(binaryPath),
@@ -19180,7 +20407,7 @@ init_errors2();
19180
20407
 
19181
20408
  // src/self-update/version-provider.ts
19182
20409
  class BuildTimeVersionProvider {
19183
- version = "2.0.0";
20410
+ version = "2.0.2";
19184
20411
  }
19185
20412
  var defaultVersionProvider = new BuildTimeVersionProvider;
19186
20413
 
@@ -19565,13 +20792,16 @@ function registerSelfCommand(program2) {
19565
20792
  }
19566
20793
 
19567
20794
  // src/commands/update.ts
19568
- import { randomUUID as randomUUID2 } from "crypto";
20795
+ import { randomUUID as randomUUID3 } from "crypto";
19569
20796
  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";
20797
+ import { mkdir as mkdir10, rename as rename7, rm as rm8, stat as stat4, writeFile as writeFile5 } from "fs/promises";
20798
+ import { dirname as dirname8, join as join16 } from "path";
19572
20799
  init_fetcher();
19573
20800
  init_registry();
19574
20801
  init_errors2();
20802
+ function formatAddCommandHint(component, options2) {
20803
+ return `ocx add${options2.global ? " --global" : ""} ${component}`;
20804
+ }
19575
20805
  function resolveUpdateFailureMessage(phase) {
19576
20806
  return phase === "apply" ? "Failed to update components" : "Failed to check for updates";
19577
20807
  }
@@ -19579,6 +20809,7 @@ function registerUpdateCommand(program2) {
19579
20809
  const cmd = program2.command("update [components...]").description("Update installed components");
19580
20810
  addCommonOptions(cmd);
19581
20811
  addVerboseOption(cmd);
20812
+ addGlobalOption(cmd);
19582
20813
  cmd.option("--all", "Update all installed components").option("--registry <name>", "Update all components from a specific registry").option("--dry-run", "Preview changes without applying").action(async (components, options2) => {
19583
20814
  try {
19584
20815
  await runUpdate(components, options2);
@@ -19589,7 +20820,7 @@ function registerUpdateCommand(program2) {
19589
20820
  }
19590
20821
  async function runUpdate(componentNames, options2) {
19591
20822
  const cwd = options2.cwd ?? process.cwd();
19592
- const provider = await LocalConfigProvider.requireInitialized(cwd);
20823
+ const provider = options2.global ? await GlobalConfigProvider.requireInitialized() : await LocalConfigProvider.requireInitialized(cwd);
19593
20824
  await runUpdateCore(componentNames, options2, provider);
19594
20825
  }
19595
20826
  async function runUpdateCore(componentNames, options2, provider, fileOps) {
@@ -19600,9 +20831,13 @@ async function runUpdateCore(componentNames, options2, provider, fileOps) {
19600
20831
  const receipt = await readReceipt(provider.cwd);
19601
20832
  if (!receipt || Object.keys(receipt.installed).length === 0) {
19602
20833
  if (componentNames.length > 0) {
19603
- throw new NotFoundError(`Component '${componentNames[0]}' is not installed. Run 'ocx add ${componentNames[0]}' first.`);
20834
+ const requestedComponent = componentNames[0];
20835
+ if (!requestedComponent) {
20836
+ throw new Error("Unexpected: component name missing despite non-empty componentNames");
20837
+ }
20838
+ throw new NotFoundError(`Component '${requestedComponent}' is not installed. Run '${formatAddCommandHint(requestedComponent, options2)}' first.`);
19604
20839
  }
19605
- throw new NotFoundError("No components installed. Run 'ocx add <component>' first.");
20840
+ throw new NotFoundError(`No components installed. Run '${formatAddCommandHint("<component>", options2)}' first.`);
19606
20841
  }
19607
20842
  const hasComponents = componentNames.length > 0;
19608
20843
  const hasAll = options2.all === true;
@@ -19725,7 +20960,7 @@ async function runUpdateCore(componentNames, options2, provider, fileOps) {
19725
20960
  throw new ValidationError(`File "${file.path}" not found in component manifest for "${update.registryName}/${update.name}".`);
19726
20961
  }
19727
20962
  const resolvedTarget = resolveTargetPath(fileObj.target, isFlattened, provider.cwd);
19728
- const targetPath = join14(provider.cwd, resolvedTarget);
20963
+ const targetPath = join16(provider.cwd, resolvedTarget);
19729
20964
  registerPlannedWriteOrThrow(plannedWrites, {
19730
20965
  absolutePath: targetPath,
19731
20966
  relativePath: resolvedTarget,
@@ -19814,7 +21049,7 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
19814
21049
  const appliedWrites = [];
19815
21050
  const tempPaths = new Set;
19816
21051
  let finalized = false;
19817
- const renameFile = options2.fileOps?.rename ?? rename6;
21052
+ const renameFile = options2.fileOps?.rename ?? rename7;
19818
21053
  const rollback = async () => {
19819
21054
  if (finalized) {
19820
21055
  return;
@@ -19822,20 +21057,20 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
19822
21057
  finalized = true;
19823
21058
  for (const tempPath of tempPaths) {
19824
21059
  try {
19825
- await rm7(tempPath, { force: true });
21060
+ await rm8(tempPath, { force: true });
19826
21061
  } catch {}
19827
21062
  }
19828
21063
  for (const appliedWrite of [...appliedWrites].reverse()) {
19829
21064
  try {
19830
21065
  if (appliedWrite.backupPath) {
19831
21066
  if (existsSync18(appliedWrite.targetPath)) {
19832
- await rm7(appliedWrite.targetPath, { force: true, recursive: true });
21067
+ await rm8(appliedWrite.targetPath, { force: true, recursive: true });
19833
21068
  }
19834
21069
  if (existsSync18(appliedWrite.backupPath)) {
19835
21070
  await renameFile(appliedWrite.backupPath, appliedWrite.targetPath);
19836
21071
  }
19837
21072
  } else if (existsSync18(appliedWrite.targetPath)) {
19838
- await rm7(appliedWrite.targetPath, { force: true, recursive: true });
21073
+ await rm8(appliedWrite.targetPath, { force: true, recursive: true });
19839
21074
  }
19840
21075
  } catch {}
19841
21076
  }
@@ -19851,7 +21086,7 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
19851
21086
  }
19852
21087
  if (existsSync18(appliedWrite.backupPath)) {
19853
21088
  try {
19854
- await rm7(appliedWrite.backupPath, { force: true });
21089
+ await rm8(appliedWrite.backupPath, { force: true });
19855
21090
  } catch (error) {
19856
21091
  if (!options2.quiet) {
19857
21092
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -19864,9 +21099,9 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
19864
21099
  try {
19865
21100
  for (const prepared of preparedUpdates) {
19866
21101
  for (const preparedFile of prepared.preparedFiles) {
19867
- const targetDir = dirname7(preparedFile.targetPath);
21102
+ const targetDir = dirname8(preparedFile.targetPath);
19868
21103
  if (!existsSync18(targetDir)) {
19869
- await mkdir9(targetDir, { recursive: true });
21104
+ await mkdir10(targetDir, { recursive: true });
19870
21105
  }
19871
21106
  if (existsSync18(preparedFile.targetPath)) {
19872
21107
  const currentTargetStats = await stat4(preparedFile.targetPath);
@@ -19874,12 +21109,12 @@ async function applyPreparedUpdatesAtomically(preparedUpdates, options2) {
19874
21109
  throw new ValidationError(`Cannot update "${preparedFile.resolvedTarget}": target path is a directory.`);
19875
21110
  }
19876
21111
  }
19877
- const tempPath = `${preparedFile.targetPath}.ocx-update-tmp-${randomUUID2()}`;
21112
+ const tempPath = `${preparedFile.targetPath}.ocx-update-tmp-${randomUUID3()}`;
19878
21113
  await writeFile5(tempPath, preparedFile.content);
19879
21114
  tempPaths.add(tempPath);
19880
21115
  let backupPath = null;
19881
21116
  if (existsSync18(preparedFile.targetPath)) {
19882
- backupPath = `${preparedFile.targetPath}.ocx-update-backup-${randomUUID2()}`;
21117
+ backupPath = `${preparedFile.targetPath}.ocx-update-backup-${randomUUID3()}`;
19883
21118
  await renameFile(preparedFile.targetPath, backupPath);
19884
21119
  appliedWrites.push({
19885
21120
  targetPath: preparedFile.targetPath,
@@ -19950,7 +21185,7 @@ Please use a fully qualified name (alias/component).`);
19950
21185
  });
19951
21186
  if (matchingIds.length === 0) {
19952
21187
  throw new NotFoundError(`Component '${name}' is not installed.
19953
- Run 'ocx add ${name}' to install it first.`);
21188
+ Run '${formatAddCommandHint(name, options2)}' to install it first.`);
19954
21189
  }
19955
21190
  const canonicalId = matchingIds[0];
19956
21191
  if (!canonicalId) {
@@ -19977,6 +21212,92 @@ function outputUpdateDryRun(results, options2) {
19977
21212
  outputDryRun(dryRunResult, { json: options2.json, quiet: options2.quiet });
19978
21213
  }
19979
21214
 
21215
+ // src/commands/validate.ts
21216
+ import { resolve as resolve8 } from "path";
21217
+ init_errors2();
21218
+ function createLoadValidationError2(message, errorKind) {
21219
+ if (errorKind === "not_found") {
21220
+ return new NotFoundError(message);
21221
+ }
21222
+ if (errorKind === "parse_error") {
21223
+ return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
21224
+ }
21225
+ return new OCXError(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
21226
+ }
21227
+ function createValidationFailureError2(errors3, failureType) {
21228
+ const summary = summarizeValidationErrors(errors3, {
21229
+ schemaErrors: failureType === "schema" ? errors3.length : 0
21230
+ });
21231
+ const details = {
21232
+ valid: false,
21233
+ errors: errors3,
21234
+ summary: {
21235
+ valid: false,
21236
+ totalErrors: summary.totalErrors,
21237
+ schemaErrors: summary.schemaErrors,
21238
+ sourceFileErrors: summary.sourceFileErrors,
21239
+ circularDependencyErrors: summary.circularDependencyErrors,
21240
+ duplicateTargetErrors: summary.duplicateTargetErrors,
21241
+ otherErrors: summary.otherErrors
21242
+ }
21243
+ };
21244
+ return new ValidationFailedError(details);
21245
+ }
21246
+ function outputValidationErrors(errors3) {
21247
+ for (const error of errors3) {
21248
+ console.log(kleur_default.red(` ${error}`));
21249
+ }
21250
+ }
21251
+ function registerValidateCommand(program2) {
21252
+ program2.command("validate").description("Validate a registry source (for registry authors)").argument("[path]", "Registry source directory", ".").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output as JSON", false).option("-q, --quiet", "Suppress output", false).option("--no-duplicate-targets", "Skip duplicate target validation").action(async (path10, options2) => {
21253
+ try {
21254
+ const sourcePath = resolve8(options2.cwd, path10);
21255
+ const validationResult = await runCompleteValidation(sourcePath, {
21256
+ skipDuplicateTargets: options2.duplicateTargets === false
21257
+ });
21258
+ if (!validationResult.success) {
21259
+ const [firstError = "Registry validation failed"] = validationResult.errors;
21260
+ if (validationResult.failureType === "load") {
21261
+ const loadError = createLoadValidationError2(firstError, validationResult.loadErrorKind);
21262
+ if (!options2.json && options2.quiet) {
21263
+ const exitCode = loadError instanceof OCXError ? loadError.exitCode : EXIT_CODES.GENERAL;
21264
+ process.exit(exitCode);
21265
+ }
21266
+ throw loadError;
21267
+ }
21268
+ const validationError = createValidationFailureError2(validationResult.errors, validationResult.failureType === "schema" ? "schema" : "rules");
21269
+ if (options2.json) {
21270
+ throw validationError;
21271
+ }
21272
+ if (!options2.quiet) {
21273
+ logger.error(validationError.message);
21274
+ outputValidationErrors(validationResult.errors);
21275
+ }
21276
+ process.exit(validationError.exitCode);
21277
+ }
21278
+ if (!options2.quiet && !options2.json) {
21279
+ logger.success("\u2713 Registry source is valid");
21280
+ }
21281
+ if (options2.json) {
21282
+ outputJson({
21283
+ success: true,
21284
+ data: {
21285
+ valid: true,
21286
+ errors: [],
21287
+ summary: summarizeValidationErrors([])
21288
+ }
21289
+ });
21290
+ }
21291
+ } catch (error) {
21292
+ if (!options2.json && options2.quiet) {
21293
+ const exitCode = error instanceof OCXError ? error.exitCode : EXIT_CODES.GENERAL;
21294
+ process.exit(exitCode);
21295
+ }
21296
+ handleError(error, { json: options2.json });
21297
+ }
21298
+ });
21299
+ }
21300
+
19980
21301
  // src/commands/verify.ts
19981
21302
  init_errors2();
19982
21303
  function registerVerifyCommand(program2) {
@@ -20053,11 +21374,11 @@ async function runVerify(componentNames, options2) {
20053
21374
  }
20054
21375
  } else {
20055
21376
  logger.error(`\u2717 ${result.canonicalId} - integrity check failed`);
20056
- for (const path9 of result.modified) {
20057
- logger.error(` Modified: ${path9}`);
21377
+ for (const path10 of result.modified) {
21378
+ logger.error(` Modified: ${path10}`);
20058
21379
  }
20059
- for (const path9 of result.missing) {
20060
- logger.error(` Missing: ${path9}`);
21380
+ for (const path10 of result.missing) {
21381
+ logger.error(` Missing: ${path10}`);
20061
21382
  }
20062
21383
  }
20063
21384
  }
@@ -20093,7 +21414,7 @@ function registerUpdateCheckHook(program2) {
20093
21414
  return;
20094
21415
  }
20095
21416
  const actionOptions = actionCommand.opts();
20096
- if (actionOptions.json) {
21417
+ if (actionOptions.json || actionOptions.quiet) {
20097
21418
  return;
20098
21419
  }
20099
21420
  if (!shouldCheckForUpdate())
@@ -20107,7 +21428,7 @@ function registerUpdateCheckHook(program2) {
20107
21428
  });
20108
21429
  }
20109
21430
  // src/index.ts
20110
- var version = "2.0.0";
21431
+ var version = "2.0.2";
20111
21432
  async function main2() {
20112
21433
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
20113
21434
  registerInitCommand(program2);
@@ -20116,6 +21437,7 @@ async function main2() {
20116
21437
  registerSearchCommand(program2);
20117
21438
  registerRegistryCommand(program2);
20118
21439
  registerBuildCommand(program2);
21440
+ registerValidateCommand(program2);
20119
21441
  registerSelfCommand(program2);
20120
21442
  registerVerifyCommand(program2);
20121
21443
  registerRemoveCommand(program2);
@@ -20140,4 +21462,4 @@ export {
20140
21462
  buildRegistry
20141
21463
  };
20142
21464
 
20143
- //# debugId=851007C87A251D3864756E2164756E21
21465
+ //# debugId=F324B1CA6BEB983B64756E2164756E21