ocx 1.4.1 → 1.4.3

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
@@ -3949,7 +3949,7 @@ var require_fuzzysort = __commonJS((exports, module) => {
3949
3949
  results.total = resultsLen + limitedCount;
3950
3950
  return results;
3951
3951
  };
3952
- var highlight = (result, open = "<b>", close = "</b>") => {
3952
+ var highlight2 = (result, open = "<b>", close = "</b>") => {
3953
3953
  var callback = typeof open === "function" ? open : undefined;
3954
3954
  var target = result.target;
3955
3955
  var targetLen = target.length;
@@ -4019,7 +4019,7 @@ var require_fuzzysort = __commonJS((exports, module) => {
4019
4019
  return this._indexes = indexes;
4020
4020
  }
4021
4021
  ["highlight"](open, close) {
4022
- return highlight(this, open, close);
4022
+ return highlight2(this, open, close);
4023
4023
  }
4024
4024
  get ["score"]() {
4025
4025
  return normalizeScore(this._score);
@@ -4510,13 +4510,13 @@ var {
4510
4510
  // src/commands/add.ts
4511
4511
  import { createHash } from "crypto";
4512
4512
  import { existsSync as existsSync5 } from "fs";
4513
- import { mkdir as mkdir4, writeFile } from "fs/promises";
4514
- import { dirname, join as join3 } from "path";
4513
+ import { mkdir as mkdir5, writeFile } from "fs/promises";
4514
+ import { dirname as dirname2, join as join4 } from "path";
4515
4515
 
4516
4516
  // src/schemas/config.ts
4517
4517
  import { existsSync } from "fs";
4518
4518
  import { mkdir } from "fs/promises";
4519
- import path from "path";
4519
+ import path2 from "path";
4520
4520
 
4521
4521
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
4522
4522
  function createScanner(text, ignoreTrivia = false) {
@@ -9815,7 +9815,7 @@ var coerce = {
9815
9815
  };
9816
9816
  var NEVER = INVALID;
9817
9817
  // src/schemas/registry.ts
9818
- import { isAbsolute, normalize } from "path";
9818
+ import { isAbsolute as isAbsolute2, normalize } from "path";
9819
9819
 
9820
9820
  // src/utils/errors.ts
9821
9821
  var EXIT_CODES = {
@@ -9824,7 +9824,8 @@ var EXIT_CODES = {
9824
9824
  NOT_FOUND: 66,
9825
9825
  NETWORK: 69,
9826
9826
  CONFIG: 78,
9827
- INTEGRITY: 1
9827
+ INTEGRITY: 1,
9828
+ CONFLICT: 6
9828
9829
  };
9829
9830
 
9830
9831
  class OCXError extends Error {
@@ -9846,9 +9847,15 @@ class NotFoundError extends OCXError {
9846
9847
  }
9847
9848
 
9848
9849
  class NetworkError extends OCXError {
9849
- constructor(message) {
9850
+ url;
9851
+ status;
9852
+ statusText;
9853
+ constructor(message, options) {
9850
9854
  super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
9851
9855
  this.name = "NetworkError";
9856
+ this.url = options?.url;
9857
+ this.status = options?.status;
9858
+ this.statusText = options?.statusText;
9852
9859
  }
9853
9860
  }
9854
9861
 
@@ -9868,12 +9875,15 @@ class ValidationError extends OCXError {
9868
9875
 
9869
9876
  class ConflictError extends OCXError {
9870
9877
  constructor(message) {
9871
- super(message, "CONFLICT", EXIT_CODES.GENERAL);
9878
+ super(message, "CONFLICT", EXIT_CODES.CONFLICT);
9872
9879
  this.name = "ConflictError";
9873
9880
  }
9874
9881
  }
9875
9882
 
9876
9883
  class IntegrityError extends OCXError {
9884
+ component;
9885
+ expected;
9886
+ found;
9877
9887
  constructor(component, expected, found) {
9878
9888
  const message = `Integrity verification failed for "${component}"
9879
9889
  ` + ` Expected: ${expected}
@@ -9882,6 +9892,9 @@ class IntegrityError extends OCXError {
9882
9892
  ` + `The registry content has changed since this component was locked.
9883
9893
  ` + `Use 'ocx update ${component}' to intentionally update this component.`;
9884
9894
  super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY);
9895
+ this.component = component;
9896
+ this.expected = expected;
9897
+ this.found = found;
9885
9898
  this.name = "IntegrityError";
9886
9899
  }
9887
9900
  }
@@ -9901,33 +9914,124 @@ class OcxConfigError extends OCXError {
9901
9914
  }
9902
9915
 
9903
9916
  class ProfileNotFoundError extends OCXError {
9904
- constructor(name) {
9905
- super(`Profile "${name}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
9917
+ profile;
9918
+ constructor(profile) {
9919
+ super(`Profile "${profile}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
9920
+ this.profile = profile;
9906
9921
  this.name = "ProfileNotFoundError";
9907
9922
  }
9908
9923
  }
9909
9924
 
9910
9925
  class ProfileExistsError extends OCXError {
9911
- constructor(name) {
9912
- super(`Profile "${name}" already exists`, "CONFLICT", EXIT_CODES.GENERAL);
9926
+ profile;
9927
+ constructor(profile) {
9928
+ super(`Profile "${profile}" already exists. Use --force to overwrite.`, "CONFLICT", EXIT_CODES.CONFLICT);
9929
+ this.profile = profile;
9913
9930
  this.name = "ProfileExistsError";
9914
9931
  }
9915
9932
  }
9916
9933
 
9934
+ class RegistryExistsError extends OCXError {
9935
+ registryName;
9936
+ existingUrl;
9937
+ newUrl;
9938
+ targetLabel;
9939
+ constructor(registryName, existingUrl, newUrl, targetLabel) {
9940
+ const target = targetLabel ? ` in ${targetLabel}` : "";
9941
+ const message = `Registry "${registryName}" already exists${target}.
9942
+ ` + ` Current: ${existingUrl}
9943
+ ` + ` New: ${newUrl}
9944
+
9945
+ ` + `Use --force to overwrite.`;
9946
+ super(message, "CONFLICT", EXIT_CODES.CONFLICT);
9947
+ this.registryName = registryName;
9948
+ this.existingUrl = existingUrl;
9949
+ this.newUrl = newUrl;
9950
+ this.targetLabel = targetLabel;
9951
+ this.name = "RegistryExistsError";
9952
+ }
9953
+ }
9954
+
9917
9955
  class InvalidProfileNameError extends OCXError {
9918
- constructor(name, reason) {
9919
- super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
9956
+ profile;
9957
+ reason;
9958
+ constructor(profile, reason) {
9959
+ super(`Invalid profile name "${profile}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
9960
+ this.profile = profile;
9961
+ this.reason = reason;
9920
9962
  this.name = "InvalidProfileNameError";
9921
9963
  }
9922
9964
  }
9923
9965
 
9924
9966
  class ProfilesNotInitializedError extends OCXError {
9925
9967
  constructor() {
9926
- super("Profiles not initialized. Run 'ocx profile add default' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
9968
+ super("Profiles not initialized. Run 'ocx init --global' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
9927
9969
  this.name = "ProfilesNotInitializedError";
9928
9970
  }
9929
9971
  }
9930
9972
 
9973
+ // src/utils/path-security.ts
9974
+ import * as path from "path";
9975
+
9976
+ class PathValidationError extends Error {
9977
+ attemptedPath;
9978
+ reason;
9979
+ constructor(message, attemptedPath, reason) {
9980
+ super(message);
9981
+ this.attemptedPath = attemptedPath;
9982
+ this.reason = reason;
9983
+ this.name = "PathValidationError";
9984
+ }
9985
+ }
9986
+ var WINDOWS_RESERVED = new Set([
9987
+ "CON",
9988
+ "PRN",
9989
+ "AUX",
9990
+ "NUL",
9991
+ "COM1",
9992
+ "COM2",
9993
+ "COM3",
9994
+ "COM4",
9995
+ "COM5",
9996
+ "COM6",
9997
+ "COM7",
9998
+ "COM8",
9999
+ "COM9",
10000
+ "LPT1",
10001
+ "LPT2",
10002
+ "LPT3",
10003
+ "LPT4",
10004
+ "LPT5",
10005
+ "LPT6",
10006
+ "LPT7",
10007
+ "LPT8",
10008
+ "LPT9"
10009
+ ]);
10010
+ function validatePath(basePath, userPath) {
10011
+ if (userPath.includes("\x00")) {
10012
+ throw new PathValidationError("Path contains null bytes", userPath, "null_byte");
10013
+ }
10014
+ if (path.isAbsolute(userPath) || path.win32.isAbsolute(userPath)) {
10015
+ throw new PathValidationError("Path must be relative", userPath, "absolute_path");
10016
+ }
10017
+ if (/^[a-zA-Z]:/.test(userPath) || userPath.startsWith("\\\\")) {
10018
+ throw new PathValidationError("Path contains Windows absolute", userPath, "windows_absolute");
10019
+ }
10020
+ const baseName = path.basename(userPath).toUpperCase().split(".")[0] ?? "";
10021
+ if (WINDOWS_RESERVED.has(baseName)) {
10022
+ throw new PathValidationError("Path uses Windows reserved name", userPath, "windows_reserved");
10023
+ }
10024
+ const normalized = userPath.normalize("NFC");
10025
+ const unified = normalized.replace(/\\/g, "/");
10026
+ const resolvedBase = path.resolve(basePath);
10027
+ const resolvedCombined = path.resolve(resolvedBase, unified);
10028
+ const relativePath = path.relative(resolvedBase, resolvedCombined);
10029
+ if (relativePath.startsWith("../") || relativePath.startsWith("..\\") || relativePath === ".." || path.isAbsolute(relativePath)) {
10030
+ throw new PathValidationError("Path escapes base directory", userPath, "path_traversal");
10031
+ }
10032
+ return resolvedCombined;
10033
+ }
10034
+
9931
10035
  // src/schemas/registry.ts
9932
10036
  var npmSpecifierSchema = exports_external.string().refine((val) => val.startsWith("npm:"), {
9933
10037
  message: 'npm specifier must start with "npm:" prefix'
@@ -9977,11 +10081,11 @@ var componentTypeSchema = exports_external.enum([
9977
10081
  "ocx:bundle",
9978
10082
  "ocx:profile"
9979
10083
  ]);
9980
- var profileTargetPathSchema = exports_external.enum(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
9981
- var targetPathSchema = exports_external.string().refine((path) => path.startsWith(".opencode/"), {
10084
+ var PROFILE_RESERVED_TARGETS = new Set(["ocx.lock", ".opencode"]);
10085
+ var targetPathSchema = exports_external.string().refine((path2) => path2.startsWith(".opencode/"), {
9982
10086
  message: 'Target path must start with ".opencode/"'
9983
- }).refine((path) => {
9984
- const parts = path.split("/");
10087
+ }).refine((path2) => {
10088
+ const parts = path2.split("/");
9985
10089
  const dir = parts[1];
9986
10090
  if (!dir)
9987
10091
  return false;
@@ -10110,7 +10214,7 @@ var componentManifestSchema = exports_external.object({
10110
10214
  opencode: opencodeConfigSchema.optional()
10111
10215
  });
10112
10216
  function validateSafePath(filePath) {
10113
- if (isAbsolute(filePath)) {
10217
+ if (isAbsolute2(filePath)) {
10114
10218
  throw new ValidationError(`Invalid path: "${filePath}" - absolute paths not allowed`);
10115
10219
  }
10116
10220
  if (filePath.startsWith("~")) {
@@ -10127,17 +10231,18 @@ function inferTargetPath(sourcePath) {
10127
10231
  function validateFileTarget(target, componentType) {
10128
10232
  const isProfile = componentType === "ocx:profile";
10129
10233
  if (isProfile) {
10130
- const isProfileFile = profileTargetPathSchema.safeParse(target).success;
10131
- const isOpencodeTarget = target.startsWith(".opencode/");
10132
- if (!isProfileFile && !isOpencodeTarget) {
10133
- throw new ValidationError(`Invalid profile target: "${target}". ` + `Must be a profile file (ocx.jsonc, opencode.jsonc, AGENTS.md) or start with ".opencode/"`);
10234
+ if (PROFILE_RESERVED_TARGETS.has(target)) {
10235
+ throw new ValidationError(`Target "${target}" is reserved for installer use`);
10134
10236
  }
10135
- if (isOpencodeTarget) {
10136
- const parseResult = targetPathSchema.safeParse(target);
10137
- if (!parseResult.success) {
10138
- throw new ValidationError(`Invalid embedded target: "${target}". ${parseResult.error.errors[0]?.message}`);
10237
+ try {
10238
+ validatePath("/dummy/base", target);
10239
+ } catch (error) {
10240
+ if (error instanceof PathValidationError) {
10241
+ throw new ValidationError(`Invalid profile target "${target}": ${error.message}`);
10139
10242
  }
10243
+ throw error;
10140
10244
  }
10245
+ return;
10141
10246
  } else {
10142
10247
  const parseResult = targetPathSchema.safeParse(target);
10143
10248
  if (!parseResult.success) {
@@ -10269,8 +10374,8 @@ var CONFIG_FILE = "ocx.jsonc";
10269
10374
  var LOCK_FILE = "ocx.lock";
10270
10375
  var LOCAL_CONFIG_DIR = ".opencode";
10271
10376
  function findOcxConfig(cwd) {
10272
- const dotOpencodePath = path.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10273
- const rootPath = path.join(cwd, CONFIG_FILE);
10377
+ const dotOpencodePath = path2.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10378
+ const rootPath = path2.join(cwd, CONFIG_FILE);
10274
10379
  const dotOpencodeExists = existsSync(dotOpencodePath);
10275
10380
  const rootExists = existsSync(rootPath);
10276
10381
  if (dotOpencodeExists && rootExists) {
@@ -10285,8 +10390,8 @@ function findOcxConfig(cwd) {
10285
10390
  return { path: dotOpencodePath, exists: false };
10286
10391
  }
10287
10392
  function findOcxLock(cwd, options) {
10288
- const dotOpencodePath = path.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10289
- const rootPath = path.join(cwd, LOCK_FILE);
10393
+ const dotOpencodePath = path2.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10394
+ const rootPath = path2.join(cwd, LOCK_FILE);
10290
10395
  if (options?.isFlattened) {
10291
10396
  if (existsSync(rootPath)) {
10292
10397
  return { path: rootPath, exists: true };
@@ -10317,8 +10422,8 @@ async function readOcxConfig(cwd) {
10317
10422
  }
10318
10423
  }
10319
10424
  async function writeOcxConfig(cwd, config, existingPath) {
10320
- const configPath = existingPath ?? path.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10321
- await mkdir(path.dirname(configPath), { recursive: true });
10425
+ const configPath = existingPath ?? path2.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10426
+ await mkdir(path2.dirname(configPath), { recursive: true });
10322
10427
  const content = JSON.stringify(config, null, 2);
10323
10428
  await Bun.write(configPath, content);
10324
10429
  }
@@ -10333,8 +10438,8 @@ async function readOcxLock(cwd, options) {
10333
10438
  return ocxLockSchema.parse(json);
10334
10439
  }
10335
10440
  async function writeOcxLock(cwd, lock, existingPath) {
10336
- const lockPath = existingPath ?? path.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10337
- await mkdir(path.dirname(lockPath), { recursive: true });
10441
+ const lockPath = existingPath ?? path2.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10442
+ await mkdir(path2.dirname(lockPath), { recursive: true });
10338
10443
  const content = JSON.stringify(lock, null, 2);
10339
10444
  await Bun.write(lockPath, content);
10340
10445
  }
@@ -10342,10 +10447,10 @@ async function writeOcxLock(cwd, lock, existingPath) {
10342
10447
  // src/utils/paths.ts
10343
10448
  import { stat } from "fs/promises";
10344
10449
  import { homedir } from "os";
10345
- import { isAbsolute as isAbsolute2, join } from "path";
10450
+ import { isAbsolute as isAbsolute3, join } from "path";
10346
10451
  function getGlobalConfigPath() {
10347
10452
  const xdg = process.env.XDG_CONFIG_HOME;
10348
- const base = xdg && isAbsolute2(xdg) ? xdg : join(homedir(), ".config");
10453
+ const base = xdg && isAbsolute3(xdg) ? xdg : join(homedir(), ".config");
10349
10454
  return join(base, "opencode");
10350
10455
  }
10351
10456
  async function globalDirectoryExists() {
@@ -10399,7 +10504,7 @@ class GlobalConfigProvider {
10399
10504
  static async requireInitialized() {
10400
10505
  const basePath = getGlobalConfigPath();
10401
10506
  if (!await globalDirectoryExists()) {
10402
- throw new ConfigError("Global config not found. Run 'opencode' once to initialize, then retry.");
10507
+ throw new ConfigError("Global config not found. Run 'ocx init --global' first.");
10403
10508
  }
10404
10509
  const config = await readOcxConfig(basePath);
10405
10510
  return new GlobalConfigProvider(basePath, config);
@@ -10414,19 +10519,19 @@ class GlobalConfigProvider {
10414
10519
 
10415
10520
  // src/config/resolver.ts
10416
10521
  import { existsSync as existsSync3, statSync as statSync2 } from "fs";
10417
- import { join as join2, relative } from "path";
10522
+ import { join as join2, relative as relative2 } from "path";
10418
10523
  var {Glob: Glob2 } = globalThis.Bun;
10419
10524
 
10420
10525
  // src/profile/manager.ts
10421
- import { mkdir as mkdir2, readdir, rm, stat as stat2 } from "fs/promises";
10526
+ import { mkdir as mkdir2, readdir, rename as rename2, rm, stat as stat2 } from "fs/promises";
10422
10527
 
10423
10528
  // src/schemas/ocx.ts
10424
10529
  var {Glob } = globalThis.Bun;
10425
10530
 
10426
10531
  // src/utils/path-helpers.ts
10427
- import path2 from "path";
10532
+ import path3 from "path";
10428
10533
  function isAbsolutePath(p) {
10429
- return path2.posix.isAbsolute(p) || path2.win32.isAbsolute(p);
10534
+ return path3.posix.isAbsolute(p) || path3.win32.isAbsolute(p);
10430
10535
  }
10431
10536
 
10432
10537
  // src/schemas/common.ts
@@ -10448,7 +10553,6 @@ var profileOcxConfigSchema = exports_external.object({
10448
10553
  componentPath: safeRelativePathSchema.optional(),
10449
10554
  renameWindow: exports_external.boolean().default(true).describe("Set terminal/tmux window name when launching OpenCode"),
10450
10555
  exclude: exports_external.array(globPatternSchema).default([
10451
- "**/AGENTS.md",
10452
10556
  "**/CLAUDE.md",
10453
10557
  "**/CONTEXT.md",
10454
10558
  "**/.opencode/**",
@@ -10476,38 +10580,38 @@ async function atomicWrite(filePath, data) {
10476
10580
  // src/profile/paths.ts
10477
10581
  import { existsSync as existsSync2, statSync } from "fs";
10478
10582
  import { homedir as homedir2 } from "os";
10479
- import path3 from "path";
10583
+ import path4 from "path";
10480
10584
  var OCX_CONFIG_FILE = "ocx.jsonc";
10481
10585
  var OPENCODE_CONFIG_FILE = "opencode.jsonc";
10482
10586
  var LOCAL_CONFIG_DIR2 = ".opencode";
10483
10587
  function getProfilesDir() {
10484
- const base = process.env.XDG_CONFIG_HOME || path3.join(homedir2(), ".config");
10485
- return path3.join(base, "opencode", "profiles");
10588
+ const base = process.env.XDG_CONFIG_HOME || path4.join(homedir2(), ".config");
10589
+ return path4.join(base, "opencode", "profiles");
10486
10590
  }
10487
10591
  function getProfileDir(name) {
10488
- return path3.join(getProfilesDir(), name);
10592
+ return path4.join(getProfilesDir(), name);
10489
10593
  }
10490
10594
  function getProfileOcxConfig(name) {
10491
- return path3.join(getProfileDir(name), "ocx.jsonc");
10595
+ return path4.join(getProfileDir(name), "ocx.jsonc");
10492
10596
  }
10493
10597
  function getProfileOpencodeConfig(name) {
10494
- return path3.join(getProfileDir(name), "opencode.jsonc");
10598
+ return path4.join(getProfileDir(name), "opencode.jsonc");
10495
10599
  }
10496
10600
  function getProfileAgents(name) {
10497
- return path3.join(getProfileDir(name), "AGENTS.md");
10601
+ return path4.join(getProfileDir(name), "AGENTS.md");
10498
10602
  }
10499
10603
  function findLocalConfigDir(cwd) {
10500
10604
  let currentDir = cwd;
10501
10605
  while (true) {
10502
- const configDir = path3.join(currentDir, LOCAL_CONFIG_DIR2);
10606
+ const configDir = path4.join(currentDir, LOCAL_CONFIG_DIR2);
10503
10607
  if (existsSync2(configDir) && statSync(configDir).isDirectory()) {
10504
10608
  return configDir;
10505
10609
  }
10506
- const gitDir = path3.join(currentDir, ".git");
10610
+ const gitDir = path4.join(currentDir, ".git");
10507
10611
  if (existsSync2(gitDir)) {
10508
10612
  return null;
10509
10613
  }
10510
- const parentDir = path3.dirname(currentDir);
10614
+ const parentDir = path4.dirname(currentDir);
10511
10615
  if (parentDir === currentDir) {
10512
10616
  return null;
10513
10617
  }
@@ -10515,8 +10619,8 @@ function findLocalConfigDir(cwd) {
10515
10619
  }
10516
10620
  }
10517
10621
  function getGlobalConfig() {
10518
- const base = process.env.XDG_CONFIG_HOME || path3.join(homedir2(), ".config");
10519
- return path3.join(base, "opencode", "ocx.jsonc");
10622
+ const base = process.env.XDG_CONFIG_HOME || path4.join(homedir2(), ".config");
10623
+ return path4.join(base, "opencode", "ocx.jsonc");
10520
10624
  }
10521
10625
 
10522
10626
  // src/profile/schema.ts
@@ -10534,7 +10638,6 @@ var DEFAULT_OCX_CONFIG = {
10534
10638
  registries: {},
10535
10639
  renameWindow: true,
10536
10640
  exclude: [
10537
- "**/AGENTS.md",
10538
10641
  "**/CLAUDE.md",
10539
10642
  "**/CONTEXT.md",
10540
10643
  "**/.opencode/**",
@@ -10543,6 +10646,21 @@ var DEFAULT_OCX_CONFIG = {
10543
10646
  ],
10544
10647
  include: []
10545
10648
  };
10649
+ var DEFAULT_OCX_CONFIG_TEMPLATE = `{
10650
+ "$schema": "https://ocx.kdco.dev/schemas/ocx.json",
10651
+ "registries": {},
10652
+ "renameWindow": true,
10653
+ "exclude": [
10654
+ // "**/AGENTS.md",
10655
+ "**/CLAUDE.md",
10656
+ "**/CONTEXT.md",
10657
+ "**/.opencode/**",
10658
+ "**/opencode.jsonc",
10659
+ "**/opencode.json"
10660
+ ],
10661
+ "include": []
10662
+ }
10663
+ `;
10546
10664
 
10547
10665
  class ProfileManager {
10548
10666
  profilesDir;
@@ -10626,7 +10744,25 @@ class ProfileManager {
10626
10744
  const dir = getProfileDir(name);
10627
10745
  await mkdir2(dir, { recursive: true, mode: 448 });
10628
10746
  const ocxPath = getProfileOcxConfig(name);
10629
- await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
10747
+ const ocxFile = Bun.file(ocxPath);
10748
+ if (!await ocxFile.exists()) {
10749
+ await Bun.write(ocxPath, DEFAULT_OCX_CONFIG_TEMPLATE, { mode: 384 });
10750
+ }
10751
+ const opencodePath = getProfileOpencodeConfig(name);
10752
+ const opencodeFile = Bun.file(opencodePath);
10753
+ if (!await opencodeFile.exists()) {
10754
+ await atomicWrite(opencodePath, {});
10755
+ }
10756
+ const agentsPath = getProfileAgents(name);
10757
+ const agentsFile = Bun.file(agentsPath);
10758
+ if (!await agentsFile.exists()) {
10759
+ const agentsContent = `# Profile Instructions
10760
+
10761
+ <!-- Add your custom instructions for this profile here -->
10762
+ <!-- These will be included when running \`ocx opencode -p ${name}\` -->
10763
+ `;
10764
+ await Bun.write(agentsPath, agentsContent, { mode: 384 });
10765
+ }
10630
10766
  }
10631
10767
  async remove(name) {
10632
10768
  if (!await this.exists(name)) {
@@ -10639,6 +10775,44 @@ class ProfileManager {
10639
10775
  const dir = getProfileDir(name);
10640
10776
  await rm(dir, { recursive: true });
10641
10777
  }
10778
+ async move(oldName, newName) {
10779
+ const oldResult = profileNameSchema.safeParse(oldName);
10780
+ if (!oldResult.success) {
10781
+ throw new InvalidProfileNameError(oldName, oldResult.error.errors[0]?.message ?? "Invalid name");
10782
+ }
10783
+ const newResult = profileNameSchema.safeParse(newName);
10784
+ if (!newResult.success) {
10785
+ throw new InvalidProfileNameError(newName, newResult.error.errors[0]?.message ?? "Invalid name");
10786
+ }
10787
+ await this.ensureInitialized();
10788
+ if (!await this.exists(oldName)) {
10789
+ throw new ProfileNotFoundError(oldName);
10790
+ }
10791
+ if (oldName === newName) {
10792
+ return { warnActiveProfile: false };
10793
+ }
10794
+ if (await this.exists(newName)) {
10795
+ throw new ConflictError(`Cannot move: profile "${newName}" already exists. Remove it first with 'ocx p rm ${newName}'.`);
10796
+ }
10797
+ const warnActiveProfile = process.env.OCX_PROFILE === oldName;
10798
+ const oldDir = getProfileDir(oldName);
10799
+ const newDir = getProfileDir(newName);
10800
+ try {
10801
+ await rename2(oldDir, newDir);
10802
+ } catch (error) {
10803
+ if (error instanceof Error && "code" in error) {
10804
+ const code = error.code;
10805
+ if (code === "EEXIST" || code === "ENOTEMPTY") {
10806
+ throw new ConflictError(`Cannot move: profile "${newName}" already exists. Remove it first with 'ocx p rm ${newName}'.`);
10807
+ }
10808
+ if (code === "ENOENT") {
10809
+ throw new ProfileNotFoundError(oldName);
10810
+ }
10811
+ }
10812
+ throw error;
10813
+ }
10814
+ return { warnActiveProfile };
10815
+ }
10642
10816
  async resolveProfile(override) {
10643
10817
  if (override) {
10644
10818
  if (!await this.exists(override)) {
@@ -10688,7 +10862,7 @@ function discoverInstructionFiles(projectDir, gitRoot) {
10688
10862
  for (const filename of INSTRUCTION_FILES) {
10689
10863
  const filePath = join2(currentDir, filename);
10690
10864
  if (existsSync3(filePath) && statSync2(filePath).isFile()) {
10691
- const relativePath = relative(root, filePath);
10865
+ const relativePath = relative2(root, filePath);
10692
10866
  discovered.push(relativePath);
10693
10867
  }
10694
10868
  }
@@ -10765,11 +10939,13 @@ class ConfigResolver {
10765
10939
  }
10766
10940
  }
10767
10941
  const shouldLoadLocal = this.shouldLoadLocalConfig();
10768
- if (shouldLoadLocal && this.localConfigDir) {
10942
+ if (!this.profile && shouldLoadLocal && this.localConfigDir) {
10769
10943
  const localOcxConfig = this.loadLocalOcxConfig();
10770
10944
  if (localOcxConfig) {
10771
- registries = { ...registries, ...localOcxConfig.registries };
10945
+ registries = localOcxConfig.registries;
10772
10946
  }
10947
+ }
10948
+ if (shouldLoadLocal && this.localConfigDir) {
10773
10949
  const localOpencodeConfig = this.loadLocalOpencodeConfig();
10774
10950
  if (localOpencodeConfig) {
10775
10951
  opencode = this.deepMerge(opencode, localOpencodeConfig);
@@ -10810,7 +10986,7 @@ class ConfigResolver {
10810
10986
  }
10811
10987
  }
10812
10988
  const shouldLoadLocal = this.shouldLoadLocalConfig();
10813
- if (shouldLoadLocal && this.localConfigDir) {
10989
+ if (!this.profile && shouldLoadLocal && this.localConfigDir) {
10814
10990
  const localOcxConfig = this.loadLocalOcxConfig();
10815
10991
  if (localOcxConfig) {
10816
10992
  const localOcxPath = join2(this.localConfigDir, OCX_CONFIG_FILE);
@@ -10819,6 +10995,8 @@ class ConfigResolver {
10819
10995
  origins.set(`registries.${key}`, { path: localOcxPath, source: "local-config" });
10820
10996
  }
10821
10997
  }
10998
+ }
10999
+ if (shouldLoadLocal && this.localConfigDir) {
10822
11000
  const localOpencodeConfig = this.loadLocalOpencodeConfig();
10823
11001
  if (localOpencodeConfig) {
10824
11002
  opencode = this.deepMerge(opencode, localOpencodeConfig);
@@ -10845,7 +11023,7 @@ class ConfigResolver {
10845
11023
  return true;
10846
11024
  const gitRoot = detectGitRoot(this.cwd);
10847
11025
  const root = gitRoot ?? this.cwd;
10848
- const relativePath = relative(root, this.localConfigDir);
11026
+ const relativePath = relative2(root, this.localConfigDir);
10849
11027
  const exclude = this.profile.ocx.exclude ?? [];
10850
11028
  const include = this.profile.ocx.include ?? [];
10851
11029
  for (const pattern of include) {
@@ -10938,7 +11116,7 @@ class ConfigResolver {
10938
11116
  // package.json
10939
11117
  var package_default = {
10940
11118
  name: "ocx",
10941
- version: "1.4.1",
11119
+ version: "1.4.3",
10942
11120
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10943
11121
  author: "kdcokenny",
10944
11122
  license: "MIT",
@@ -11017,14 +11195,28 @@ async function fetchWithCache(url, parse3) {
11017
11195
  return cached;
11018
11196
  }
11019
11197
  const promise = (async () => {
11020
- const response = await fetch(url);
11198
+ let response;
11199
+ try {
11200
+ response = await fetch(url);
11201
+ } catch (error) {
11202
+ throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
11203
+ }
11021
11204
  if (!response.ok) {
11022
11205
  if (response.status === 404) {
11023
11206
  throw new NotFoundError(`Not found: ${url}`);
11024
11207
  }
11025
- throw new NetworkError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
11208
+ throw new NetworkError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`, {
11209
+ url,
11210
+ status: response.status,
11211
+ statusText: response.statusText
11212
+ });
11213
+ }
11214
+ let data;
11215
+ try {
11216
+ data = await response.json();
11217
+ } catch (error) {
11218
+ throw new NetworkError(`Invalid JSON response from ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
11026
11219
  }
11027
- const data = await response.json();
11028
11220
  return parse3(data);
11029
11221
  })();
11030
11222
  cache.set(url, promise);
@@ -11071,9 +11263,14 @@ async function fetchComponentVersion(baseUrl, name, version) {
11071
11263
  }
11072
11264
  async function fetchFileContent(baseUrl, componentName, filePath) {
11073
11265
  const url = `${baseUrl.replace(/\/$/, "")}/components/${componentName}/${filePath}`;
11074
- const response = await fetch(url);
11266
+ let response;
11267
+ try {
11268
+ response = await fetch(url);
11269
+ } catch (error) {
11270
+ throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
11271
+ }
11075
11272
  if (!response.ok) {
11076
- throw new NetworkError(`Failed to fetch file ${filePath} for ${componentName}: ${response.status} ${response.statusText}`);
11273
+ throw new NetworkError(`Failed to fetch file ${filePath} for ${componentName} from ${url}: ${response.status} ${response.statusText}`, { url, status: response.status, statusText: response.statusText });
11077
11274
  }
11078
11275
  return response.text();
11079
11276
  }
@@ -11148,13 +11345,13 @@ async function resolveDependencies(registries, componentNames) {
11148
11345
  const npmDeps = new Set;
11149
11346
  const npmDevDeps = new Set;
11150
11347
  let opencode = {};
11151
- async function resolve(componentNamespace, componentName, path4 = []) {
11348
+ async function resolve2(componentNamespace, componentName, path5 = []) {
11152
11349
  const qualifiedName = createQualifiedComponent(componentNamespace, componentName);
11153
11350
  if (resolved.has(qualifiedName)) {
11154
11351
  return;
11155
11352
  }
11156
11353
  if (visiting.has(qualifiedName)) {
11157
- const cycle = [...path4, qualifiedName].join(" \u2192 ");
11354
+ const cycle = [...path5, qualifiedName].join(" \u2192 ");
11158
11355
  throw new ValidationError(`Circular dependency detected: ${cycle}`);
11159
11356
  }
11160
11357
  visiting.add(qualifiedName);
@@ -11165,12 +11362,21 @@ async function resolveDependencies(registries, componentNames) {
11165
11362
  let component;
11166
11363
  try {
11167
11364
  component = await fetchComponent(regConfig.url, componentName);
11168
- } catch (_err) {
11169
- throw new OCXError(`Component '${componentName}' not found in registry '${componentNamespace}'.`, "NOT_FOUND");
11365
+ } catch (err) {
11366
+ if (err instanceof NetworkError) {
11367
+ throw err;
11368
+ }
11369
+ if (err instanceof NotFoundError) {
11370
+ throw new NotFoundError(`Component '${componentName}' not found in registry '${componentNamespace}'.`);
11371
+ }
11372
+ if (err instanceof OCXError) {
11373
+ throw err;
11374
+ }
11375
+ throw new NetworkError(`Failed to fetch component '${componentName}' from registry '${componentNamespace}': ${err instanceof Error ? err.message : String(err)}`, { url: regConfig.url });
11170
11376
  }
11171
11377
  for (const dep of component.dependencies) {
11172
11378
  const depRef = parseComponentRef(dep, componentNamespace);
11173
- await resolve(depRef.namespace, depRef.component, [...path4, qualifiedName]);
11379
+ await resolve2(depRef.namespace, depRef.component, [...path5, qualifiedName]);
11174
11380
  }
11175
11381
  const normalizedComponent = normalizeComponentManifest(component);
11176
11382
  resolved.set(qualifiedName, {
@@ -11197,7 +11403,7 @@ async function resolveDependencies(registries, componentNames) {
11197
11403
  }
11198
11404
  for (const name of componentNames) {
11199
11405
  const ref = parseComponentRef(name);
11200
- await resolve(ref.namespace, ref.component);
11406
+ await resolve2(ref.namespace, ref.component);
11201
11407
  }
11202
11408
  const components = Array.from(resolved.values());
11203
11409
  const installOrder = Array.from(resolved.keys());
@@ -11214,17 +11420,17 @@ async function resolveDependencies(registries, componentNames) {
11214
11420
  import { existsSync as existsSync4 } from "fs";
11215
11421
  import { mkdir as mkdir3 } from "fs/promises";
11216
11422
  import { homedir as homedir3 } from "os";
11217
- import path4 from "path";
11423
+ import path5 from "path";
11218
11424
  var LOCAL_CONFIG_DIR3 = ".opencode";
11219
11425
  function isGlobalConfigPath(cwd) {
11220
- const base = process.env.XDG_CONFIG_HOME || path4.join(homedir3(), ".config");
11221
- const globalConfigDir = path4.resolve(base, "opencode");
11222
- const resolvedCwd = path4.resolve(cwd);
11426
+ const base = process.env.XDG_CONFIG_HOME || path5.join(homedir3(), ".config");
11427
+ const globalConfigDir = path5.resolve(base, "opencode");
11428
+ const resolvedCwd = path5.resolve(cwd);
11223
11429
  if (resolvedCwd === globalConfigDir) {
11224
11430
  return true;
11225
11431
  }
11226
- const relative2 = path4.relative(globalConfigDir, resolvedCwd);
11227
- return relative2 !== "" && !relative2.startsWith("..") && !path4.isAbsolute(relative2);
11432
+ const relative3 = path5.relative(globalConfigDir, resolvedCwd);
11433
+ return relative3 !== "" && !relative3.startsWith("..") && !path5.isAbsolute(relative3);
11228
11434
  }
11229
11435
  var JSONC_OPTIONS = {
11230
11436
  formattingOptions: {
@@ -11241,8 +11447,8 @@ var OPENCODE_CONFIG_TEMPLATE = `{
11241
11447
  `;
11242
11448
  function findOpencodeConfig(cwd) {
11243
11449
  if (isGlobalConfigPath(cwd)) {
11244
- const rootJsonc2 = path4.join(cwd, "opencode.jsonc");
11245
- const rootJson2 = path4.join(cwd, "opencode.json");
11450
+ const rootJsonc2 = path5.join(cwd, "opencode.jsonc");
11451
+ const rootJson2 = path5.join(cwd, "opencode.json");
11246
11452
  if (existsSync4(rootJsonc2)) {
11247
11453
  return { path: rootJsonc2, exists: true };
11248
11454
  }
@@ -11251,16 +11457,16 @@ function findOpencodeConfig(cwd) {
11251
11457
  }
11252
11458
  return { path: rootJsonc2, exists: false };
11253
11459
  }
11254
- const dotOpencodeJsonc = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11255
- const dotOpencodeJson = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.json");
11460
+ const dotOpencodeJsonc = path5.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11461
+ const dotOpencodeJson = path5.join(cwd, LOCAL_CONFIG_DIR3, "opencode.json");
11256
11462
  if (existsSync4(dotOpencodeJsonc)) {
11257
11463
  return { path: dotOpencodeJsonc, exists: true };
11258
11464
  }
11259
11465
  if (existsSync4(dotOpencodeJson)) {
11260
11466
  return { path: dotOpencodeJson, exists: true };
11261
11467
  }
11262
- const rootJsonc = path4.join(cwd, "opencode.jsonc");
11263
- const rootJson = path4.join(cwd, "opencode.json");
11468
+ const rootJsonc = path5.join(cwd, "opencode.jsonc");
11469
+ const rootJson = path5.join(cwd, "opencode.json");
11264
11470
  if (existsSync4(rootJsonc)) {
11265
11471
  return { path: rootJsonc, exists: true };
11266
11472
  }
@@ -11274,7 +11480,7 @@ async function ensureOpencodeConfig(cwd) {
11274
11480
  if (exists) {
11275
11481
  return { path: configPath, created: false };
11276
11482
  }
11277
- await mkdir3(path4.dirname(configPath), { recursive: true });
11483
+ await mkdir3(path5.dirname(configPath), { recursive: true });
11278
11484
  await Bun.write(configPath, OPENCODE_CONFIG_TEMPLATE);
11279
11485
  return { path: configPath, created: true };
11280
11486
  }
@@ -11291,13 +11497,13 @@ async function readOpencodeJsonConfig(cwd) {
11291
11497
  path: configPath
11292
11498
  };
11293
11499
  }
11294
- async function writeOpencodeJsonConfig(path5, content) {
11295
- await Bun.write(path5, content);
11500
+ async function writeOpencodeJsonConfig(path6, content) {
11501
+ await Bun.write(path6, content);
11296
11502
  }
11297
- function getValueAtPath(content, path5) {
11503
+ function getValueAtPath(content, path6) {
11298
11504
  const parsed = parse2(content, [], { allowTrailingComma: true });
11299
11505
  let current = parsed;
11300
- for (const segment of path5) {
11506
+ for (const segment of path6) {
11301
11507
  if (current === null || current === undefined)
11302
11508
  return;
11303
11509
  if (typeof current !== "object")
@@ -11306,27 +11512,27 @@ function getValueAtPath(content, path5) {
11306
11512
  }
11307
11513
  return current;
11308
11514
  }
11309
- function applyValueAtPath(content, path5, value) {
11515
+ function applyValueAtPath(content, path6, value) {
11310
11516
  if (value === null || value === undefined) {
11311
11517
  return content;
11312
11518
  }
11313
11519
  if (typeof value === "object" && !Array.isArray(value)) {
11314
- const existingValue = getValueAtPath(content, path5);
11520
+ const existingValue = getValueAtPath(content, path6);
11315
11521
  if (existingValue !== undefined && (existingValue === null || typeof existingValue !== "object")) {
11316
- const edits2 = modify(content, path5, value, JSONC_OPTIONS);
11522
+ const edits2 = modify(content, path6, value, JSONC_OPTIONS);
11317
11523
  return applyEdits(content, edits2);
11318
11524
  }
11319
11525
  let updatedContent = content;
11320
11526
  for (const [key, val] of Object.entries(value)) {
11321
- updatedContent = applyValueAtPath(updatedContent, [...path5, key], val);
11527
+ updatedContent = applyValueAtPath(updatedContent, [...path6, key], val);
11322
11528
  }
11323
11529
  return updatedContent;
11324
11530
  }
11325
11531
  if (Array.isArray(value)) {
11326
- const edits2 = modify(content, path5, value, JSONC_OPTIONS);
11532
+ const edits2 = modify(content, path6, value, JSONC_OPTIONS);
11327
11533
  return applyEdits(content, edits2);
11328
11534
  }
11329
- const edits = modify(content, path5, value, JSONC_OPTIONS);
11535
+ const edits = modify(content, path6, value, JSONC_OPTIONS);
11330
11536
  return applyEdits(content, edits);
11331
11537
  }
11332
11538
  async function updateOpencodeJsonConfig(cwd, opencode) {
@@ -11340,8 +11546,8 @@ async function updateOpencodeJsonConfig(cwd, opencode) {
11340
11546
  } else {
11341
11547
  const config = { $schema: "https://opencode.ai/config.json" };
11342
11548
  content = JSON.stringify(config, null, "\t");
11343
- configPath = isGlobalConfigPath(cwd) ? path4.join(cwd, "opencode.jsonc") : path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11344
- await mkdir3(path4.dirname(configPath), { recursive: true });
11549
+ configPath = isGlobalConfigPath(cwd) ? path5.join(cwd, "opencode.jsonc") : path5.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11550
+ await mkdir3(path5.dirname(configPath), { recursive: true });
11345
11551
  created = true;
11346
11552
  }
11347
11553
  const originalContent = content;
@@ -11383,7 +11589,7 @@ function parseEnvBool(value, defaultValue) {
11383
11589
  return defaultValue;
11384
11590
  }
11385
11591
  // src/utils/git-context.ts
11386
- import { basename, resolve } from "path";
11592
+ import { basename as basename2, resolve as resolve2 } from "path";
11387
11593
  function getGitEnv() {
11388
11594
  const { GIT_DIR: _, GIT_WORK_TREE: __, ...cleanEnv } = process.env;
11389
11595
  return cleanEnv;
@@ -11449,12 +11655,100 @@ async function getRepoName(cwd) {
11449
11655
  if (!rootPath) {
11450
11656
  return null;
11451
11657
  }
11452
- return basename(rootPath);
11658
+ return basename2(rootPath);
11453
11659
  }
11454
11660
  async function getGitInfo(cwd) {
11455
11661
  const [repoName, branch] = await Promise.all([getRepoName(cwd), getBranch(cwd)]);
11456
11662
  return { repoName, branch };
11457
11663
  }
11664
+ // src/lib/build-registry.ts
11665
+ import { mkdir as mkdir4 } from "fs/promises";
11666
+ import { dirname, join as join3 } from "path";
11667
+ class BuildRegistryError extends Error {
11668
+ errors;
11669
+ constructor(message, errors2 = []) {
11670
+ super(message);
11671
+ this.errors = errors2;
11672
+ this.name = "BuildRegistryError";
11673
+ }
11674
+ }
11675
+ async function buildRegistry(options) {
11676
+ const { source: sourcePath, out: outPath } = options;
11677
+ const jsoncFile = Bun.file(join3(sourcePath, "registry.jsonc"));
11678
+ const jsonFile = Bun.file(join3(sourcePath, "registry.json"));
11679
+ const jsoncExists = await jsoncFile.exists();
11680
+ const jsonExists = await jsonFile.exists();
11681
+ if (!jsoncExists && !jsonExists) {
11682
+ throw new BuildRegistryError("No registry.jsonc or registry.json found in source directory");
11683
+ }
11684
+ const registryFile = jsoncExists ? jsoncFile : jsonFile;
11685
+ const content = await registryFile.text();
11686
+ const registryData = parse2(content, [], { allowTrailingComma: true });
11687
+ const parseResult = registrySchema.safeParse(registryData);
11688
+ if (!parseResult.success) {
11689
+ const errors2 = parseResult.error.errors.map((e3) => `${e3.path.join(".")}: ${e3.message}`);
11690
+ throw new BuildRegistryError("Registry validation failed", errors2);
11691
+ }
11692
+ const registry = parseResult.data;
11693
+ const validationErrors = [];
11694
+ const componentsDir = join3(outPath, "components");
11695
+ await mkdir4(componentsDir, { recursive: true });
11696
+ for (const component of registry.components) {
11697
+ const packument = {
11698
+ name: component.name,
11699
+ versions: {
11700
+ [registry.version]: component
11701
+ },
11702
+ "dist-tags": {
11703
+ latest: registry.version
11704
+ }
11705
+ };
11706
+ const packumentPath = join3(componentsDir, `${component.name}.json`);
11707
+ await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
11708
+ for (const rawFile of component.files) {
11709
+ const file = normalizeFile(rawFile, component.type);
11710
+ const sourceFilePath = join3(sourcePath, "files", file.path);
11711
+ const destFilePath = join3(componentsDir, component.name, file.path);
11712
+ const destFileDir = dirname(destFilePath);
11713
+ if (!await Bun.file(sourceFilePath).exists()) {
11714
+ validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`);
11715
+ continue;
11716
+ }
11717
+ await mkdir4(destFileDir, { recursive: true });
11718
+ const sourceFile = Bun.file(sourceFilePath);
11719
+ await Bun.write(destFilePath, sourceFile);
11720
+ }
11721
+ }
11722
+ if (validationErrors.length > 0) {
11723
+ throw new BuildRegistryError(`Build failed with ${validationErrors.length} errors`, validationErrors);
11724
+ }
11725
+ const index = {
11726
+ name: registry.name,
11727
+ namespace: registry.namespace,
11728
+ version: registry.version,
11729
+ author: registry.author,
11730
+ ...registry.opencode && { opencode: registry.opencode },
11731
+ ...registry.ocx && { ocx: registry.ocx },
11732
+ components: registry.components.map((c) => ({
11733
+ name: c.name,
11734
+ type: c.type,
11735
+ description: c.description
11736
+ }))
11737
+ };
11738
+ await Bun.write(join3(outPath, "index.json"), JSON.stringify(index, null, 2));
11739
+ const wellKnownDir = join3(outPath, ".well-known");
11740
+ await mkdir4(wellKnownDir, { recursive: true });
11741
+ const discovery = { registry: "/index.json" };
11742
+ await Bun.write(join3(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
11743
+ return {
11744
+ name: registry.name,
11745
+ namespace: registry.namespace,
11746
+ version: registry.version,
11747
+ componentsCount: registry.components.length,
11748
+ outputPath: outPath
11749
+ };
11750
+ }
11751
+
11458
11752
  // ../../node_modules/.bun/kleur@4.1.5/node_modules/kleur/index.mjs
11459
11753
  var FORCE_COLOR;
11460
11754
  var NODE_DISABLE_COLORS;
@@ -11592,6 +11886,15 @@ var logger = {
11592
11886
  console.log("");
11593
11887
  }
11594
11888
  };
11889
+ var highlight = {
11890
+ component: (text) => kleur_default.cyan(text),
11891
+ path: (text) => kleur_default.green(text),
11892
+ command: (text) => kleur_default.yellow(text),
11893
+ url: (text) => kleur_default.blue().underline(text),
11894
+ error: (text) => kleur_default.red(text),
11895
+ dim: (text) => kleur_default.gray(text),
11896
+ bold: (text) => kleur_default.bold(text)
11897
+ };
11595
11898
 
11596
11899
  // src/utils/handle-error.ts
11597
11900
  function handleError(error, options2 = {}) {
@@ -11607,8 +11910,8 @@ function handleError(error, options2 = {}) {
11607
11910
  if (error instanceof ZodError) {
11608
11911
  logger.error("Validation failed:");
11609
11912
  for (const issue of error.issues) {
11610
- const path5 = issue.path.join(".");
11611
- logger.error(` ${path5}: ${issue.message}`);
11913
+ const path6 = issue.path.join(".");
11914
+ logger.error(` ${path6}: ${issue.message}`);
11612
11915
  }
11613
11916
  process.exit(EXIT_CODES.CONFIG);
11614
11917
  }
@@ -11632,6 +11935,129 @@ function wrapAction(action) {
11632
11935
  };
11633
11936
  }
11634
11937
  function formatErrorAsJson(error) {
11938
+ if (error instanceof RegistryExistsError) {
11939
+ return {
11940
+ success: false,
11941
+ error: {
11942
+ code: error.code,
11943
+ message: error.message,
11944
+ details: {
11945
+ registryName: error.registryName,
11946
+ existingUrl: error.existingUrl,
11947
+ newUrl: error.newUrl,
11948
+ ...error.targetLabel && { targetLabel: error.targetLabel }
11949
+ }
11950
+ },
11951
+ exitCode: error.exitCode,
11952
+ meta: {
11953
+ timestamp: new Date().toISOString()
11954
+ }
11955
+ };
11956
+ }
11957
+ if (error instanceof IntegrityError) {
11958
+ return {
11959
+ success: false,
11960
+ error: {
11961
+ code: error.code,
11962
+ message: error.message,
11963
+ details: {
11964
+ component: error.component,
11965
+ expected: error.expected,
11966
+ found: error.found
11967
+ }
11968
+ },
11969
+ exitCode: error.exitCode,
11970
+ meta: {
11971
+ timestamp: new Date().toISOString()
11972
+ }
11973
+ };
11974
+ }
11975
+ if (error instanceof NetworkError) {
11976
+ const details = {};
11977
+ if (error.url)
11978
+ details.url = error.url;
11979
+ if (error.status !== undefined)
11980
+ details.status = error.status;
11981
+ if (error.statusText)
11982
+ details.statusText = error.statusText;
11983
+ return {
11984
+ success: false,
11985
+ error: {
11986
+ code: error.code,
11987
+ message: error.message,
11988
+ ...Object.keys(details).length > 0 && { details }
11989
+ },
11990
+ exitCode: error.exitCode,
11991
+ meta: {
11992
+ timestamp: new Date().toISOString()
11993
+ }
11994
+ };
11995
+ }
11996
+ if (error instanceof ProfileNotFoundError) {
11997
+ return {
11998
+ success: false,
11999
+ error: {
12000
+ code: error.code,
12001
+ message: error.message,
12002
+ details: {
12003
+ profile: error.profile
12004
+ }
12005
+ },
12006
+ exitCode: error.exitCode,
12007
+ meta: {
12008
+ timestamp: new Date().toISOString()
12009
+ }
12010
+ };
12011
+ }
12012
+ if (error instanceof ProfileExistsError) {
12013
+ return {
12014
+ success: false,
12015
+ error: {
12016
+ code: error.code,
12017
+ message: error.message,
12018
+ details: {
12019
+ profile: error.profile
12020
+ }
12021
+ },
12022
+ exitCode: error.exitCode,
12023
+ meta: {
12024
+ timestamp: new Date().toISOString()
12025
+ }
12026
+ };
12027
+ }
12028
+ if (error instanceof InvalidProfileNameError) {
12029
+ return {
12030
+ success: false,
12031
+ error: {
12032
+ code: error.code,
12033
+ message: error.message,
12034
+ details: {
12035
+ profile: error.profile,
12036
+ reason: error.reason
12037
+ }
12038
+ },
12039
+ exitCode: error.exitCode,
12040
+ meta: {
12041
+ timestamp: new Date().toISOString()
12042
+ }
12043
+ };
12044
+ }
12045
+ if (error instanceof BuildRegistryError) {
12046
+ return {
12047
+ success: false,
12048
+ error: {
12049
+ code: "BUILD_ERROR",
12050
+ message: error.message,
12051
+ details: {
12052
+ errors: error.errors
12053
+ }
12054
+ },
12055
+ exitCode: EXIT_CODES.GENERAL,
12056
+ meta: {
12057
+ timestamp: new Date().toISOString()
12058
+ }
12059
+ };
12060
+ }
11635
12061
  if (error instanceof OCXError) {
11636
12062
  return {
11637
12063
  success: false,
@@ -11650,7 +12076,14 @@ function formatErrorAsJson(error) {
11650
12076
  success: false,
11651
12077
  error: {
11652
12078
  code: "VALIDATION_ERROR",
11653
- message: error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
12079
+ message: error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "),
12080
+ details: {
12081
+ issues: error.issues.map((i) => ({
12082
+ path: i.path.join("."),
12083
+ message: i.message,
12084
+ code: i.code
12085
+ }))
12086
+ }
11654
12087
  },
11655
12088
  exitCode: EXIT_CODES.CONFIG,
11656
12089
  meta: {
@@ -11675,15 +12108,15 @@ function outputJson(data) {
11675
12108
  console.log(JSON.stringify(data, null, 2));
11676
12109
  }
11677
12110
  // src/utils/path-safety.ts
11678
- import path5 from "path";
12111
+ import path6 from "path";
11679
12112
  function isPathInside(childPath, parentPath) {
11680
- const resolvedChild = path5.resolve(childPath);
11681
- const resolvedParent = path5.resolve(parentPath);
12113
+ const resolvedChild = path6.resolve(childPath);
12114
+ const resolvedParent = path6.resolve(parentPath);
11682
12115
  if (resolvedChild === resolvedParent) {
11683
12116
  return true;
11684
12117
  }
11685
- const relative2 = path5.relative(resolvedParent, resolvedChild);
11686
- return !!relative2 && !relative2.startsWith("..") && !isAbsolutePath(relative2);
12118
+ const relative3 = path6.relative(resolvedParent, resolvedChild);
12119
+ return !!relative3 && !relative3.startsWith("..") && !isAbsolutePath(relative3);
11687
12120
  }
11688
12121
  function assertPathInside(childPath, parentPath) {
11689
12122
  if (!isPathInside(childPath, parentPath)) {
@@ -11695,6 +12128,7 @@ var sharedOptions = {
11695
12128
  cwd: () => new Option("--cwd <path>", "Working directory").default(process.cwd()),
11696
12129
  quiet: () => new Option("-q, --quiet", "Suppress output"),
11697
12130
  json: () => new Option("--json", "Output as JSON"),
12131
+ profile: () => new Option("-p, --profile <name>", "Target a specific profile's config"),
11698
12132
  force: () => new Option("-f, --force", "Skip confirmation prompts"),
11699
12133
  verbose: () => new Option("-v, --verbose", "Verbose output"),
11700
12134
  global: new Option("-g, --global", "Install to global OpenCode config (~/.config/opencode)")
@@ -11711,6 +12145,20 @@ function addVerboseOption(cmd) {
11711
12145
  function addGlobalOption(cmd) {
11712
12146
  return cmd.addOption(sharedOptions.global);
11713
12147
  }
12148
+ function addProfileOption(cmd) {
12149
+ return cmd.addOption(sharedOptions.profile());
12150
+ }
12151
+ function validateProfileName(name) {
12152
+ if (!name || name.length === 0) {
12153
+ throw new InvalidProfileNameError(name, "cannot be empty");
12154
+ }
12155
+ if (name.length > 32) {
12156
+ throw new InvalidProfileNameError(name, "must be 32 characters or less");
12157
+ }
12158
+ if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
12159
+ throw new InvalidProfileNameError(name, "must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens");
12160
+ }
12161
+ }
11714
12162
  // ../../node_modules/.bun/ora@8.2.0/node_modules/ora/index.js
11715
12163
  import process9 from "process";
11716
12164
 
@@ -13429,7 +13877,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13429
13877
  }
13430
13878
  const computedHash = await hashBundle(files);
13431
13879
  for (const file of component.files) {
13432
- const targetPath = join3(cwd, resolveTargetPath(file.target, isFlattened));
13880
+ const targetPath = join4(cwd, resolveTargetPath(file.target, isFlattened));
13433
13881
  assertPathInside(targetPath, cwd);
13434
13882
  }
13435
13883
  const existingEntry = lock.installed[component.qualifiedName];
@@ -13439,7 +13887,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13439
13887
  }
13440
13888
  for (const file of component.files) {
13441
13889
  const resolvedTarget = resolveTargetPath(file.target, isFlattened);
13442
- const targetPath = join3(cwd, resolvedTarget);
13890
+ const targetPath = join4(cwd, resolvedTarget);
13443
13891
  if (existsSync5(targetPath)) {
13444
13892
  const conflictingComponent = findComponentByFile(lock, resolvedTarget);
13445
13893
  if (conflictingComponent && conflictingComponent !== component.qualifiedName) {
@@ -13463,7 +13911,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13463
13911
  if (!componentFile)
13464
13912
  continue;
13465
13913
  const resolvedTarget = resolveTargetPath(componentFile.target, isFlattened);
13466
- const targetPath = join3(cwd, resolvedTarget);
13914
+ const targetPath = join4(cwd, resolvedTarget);
13467
13915
  if (existsSync5(targetPath)) {
13468
13916
  const existingContent = await Bun.file(targetPath).text();
13469
13917
  const incomingContent = file.content.toString("utf-8");
@@ -13528,7 +13976,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13528
13976
  }
13529
13977
  const hasNpmDeps = resolved.npmDependencies.length > 0;
13530
13978
  const hasNpmDevDeps = resolved.npmDevDependencies.length > 0;
13531
- const packageJsonPath = options2.global || options2.profile ? join3(cwd, "package.json") : join3(cwd, ".opencode/package.json");
13979
+ const packageJsonPath = options2.global || options2.profile ? join4(cwd, "package.json") : join4(cwd, ".opencode/package.json");
13532
13980
  if (hasNpmDeps || hasNpmDevDeps) {
13533
13981
  const npmSpin = options2.quiet ? null : createSpinner({ text: `Updating ${packageJsonPath}...` });
13534
13982
  npmSpin?.start();
@@ -13570,8 +14018,8 @@ async function installComponent(component, files, cwd, options2) {
13570
14018
  if (!componentFile)
13571
14019
  continue;
13572
14020
  const resolvedTarget = resolveTargetPath(componentFile.target, !!options2.isFlattened);
13573
- const targetPath = join3(cwd, resolvedTarget);
13574
- const targetDir = dirname(targetPath);
14021
+ const targetPath = join4(cwd, resolvedTarget);
14022
+ const targetDir = dirname2(targetPath);
13575
14023
  if (existsSync5(targetPath)) {
13576
14024
  const existingContent = await Bun.file(targetPath).text();
13577
14025
  const incomingContent = file.content.toString("utf-8");
@@ -13584,7 +14032,7 @@ async function installComponent(component, files, cwd, options2) {
13584
14032
  result.written.push(resolvedTarget);
13585
14033
  }
13586
14034
  if (!existsSync5(targetDir)) {
13587
- await mkdir4(targetDir, { recursive: true });
14035
+ await mkdir5(targetDir, { recursive: true });
13588
14036
  }
13589
14037
  await writeFile(targetPath, file.content);
13590
14038
  }
@@ -13660,7 +14108,7 @@ function mergeDevDependencies(existing, newDeps) {
13660
14108
  return { ...existing, devDependencies: merged };
13661
14109
  }
13662
14110
  async function readOpencodePackageJson(opencodeDir) {
13663
- const pkgPath = join3(opencodeDir, "package.json");
14111
+ const pkgPath = join4(opencodeDir, "package.json");
13664
14112
  if (!existsSync5(pkgPath)) {
13665
14113
  return { ...DEFAULT_PACKAGE_JSON };
13666
14114
  }
@@ -13673,7 +14121,7 @@ async function readOpencodePackageJson(opencodeDir) {
13673
14121
  }
13674
14122
  }
13675
14123
  async function ensureManifestFilesAreTracked(opencodeDir) {
13676
- const gitignorePath = join3(opencodeDir, ".gitignore");
14124
+ const gitignorePath = join4(opencodeDir, ".gitignore");
13677
14125
  const filesToTrack = new Set(["package.json", "bun.lock"]);
13678
14126
  const requiredIgnores = ["node_modules"];
13679
14127
  let lines = [];
@@ -13696,12 +14144,12 @@ async function updateOpencodeDevDependencies(cwd, npmDeps, npmDevDeps, options2
13696
14144
  const allDepSpecs = [...npmDeps, ...npmDevDeps];
13697
14145
  if (allDepSpecs.length === 0)
13698
14146
  return;
13699
- const packageDir = options2.isFlattened ? cwd : join3(cwd, ".opencode");
13700
- await mkdir4(packageDir, { recursive: true });
14147
+ const packageDir = options2.isFlattened ? cwd : join4(cwd, ".opencode");
14148
+ await mkdir5(packageDir, { recursive: true });
13701
14149
  const parsedDeps = allDepSpecs.map(parseNpmDependency);
13702
14150
  const existing = await readOpencodePackageJson(packageDir);
13703
14151
  const updated = mergeDevDependencies(existing, parsedDeps);
13704
- await Bun.write(join3(packageDir, "package.json"), `${JSON.stringify(updated, null, 2)}
14152
+ await Bun.write(join4(packageDir, "package.json"), `${JSON.stringify(updated, null, 2)}
13705
14153
  `);
13706
14154
  if (!options2.isFlattened) {
13707
14155
  await ensureManifestFilesAreTracked(packageDir);
@@ -13717,101 +14165,11 @@ function findComponentByFile(lock, filePath) {
13717
14165
  }
13718
14166
 
13719
14167
  // src/commands/build.ts
13720
- import { join as join5, relative as relative2 } from "path";
13721
-
13722
- // src/lib/build-registry.ts
13723
- import { mkdir as mkdir5 } from "fs/promises";
13724
- import { dirname as dirname2, join as join4 } from "path";
13725
- class BuildRegistryError extends Error {
13726
- errors;
13727
- constructor(message, errors3 = []) {
13728
- super(message);
13729
- this.errors = errors3;
13730
- this.name = "BuildRegistryError";
13731
- }
13732
- }
13733
- async function buildRegistry(options2) {
13734
- const { source: sourcePath, out: outPath } = options2;
13735
- const jsoncFile = Bun.file(join4(sourcePath, "registry.jsonc"));
13736
- const jsonFile = Bun.file(join4(sourcePath, "registry.json"));
13737
- const jsoncExists = await jsoncFile.exists();
13738
- const jsonExists = await jsonFile.exists();
13739
- if (!jsoncExists && !jsonExists) {
13740
- throw new BuildRegistryError("No registry.jsonc or registry.json found in source directory");
13741
- }
13742
- const registryFile = jsoncExists ? jsoncFile : jsonFile;
13743
- const content2 = await registryFile.text();
13744
- const registryData = parse2(content2, [], { allowTrailingComma: true });
13745
- const parseResult = registrySchema.safeParse(registryData);
13746
- if (!parseResult.success) {
13747
- const errors3 = parseResult.error.errors.map((e3) => `${e3.path.join(".")}: ${e3.message}`);
13748
- throw new BuildRegistryError("Registry validation failed", errors3);
13749
- }
13750
- const registry = parseResult.data;
13751
- const validationErrors = [];
13752
- const componentsDir = join4(outPath, "components");
13753
- await mkdir5(componentsDir, { recursive: true });
13754
- for (const component of registry.components) {
13755
- const packument = {
13756
- name: component.name,
13757
- versions: {
13758
- [registry.version]: component
13759
- },
13760
- "dist-tags": {
13761
- latest: registry.version
13762
- }
13763
- };
13764
- const packumentPath = join4(componentsDir, `${component.name}.json`);
13765
- await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
13766
- for (const rawFile of component.files) {
13767
- const file = normalizeFile(rawFile, component.type);
13768
- const sourceFilePath = join4(sourcePath, "files", file.path);
13769
- const destFilePath = join4(componentsDir, component.name, file.path);
13770
- const destFileDir = dirname2(destFilePath);
13771
- if (!await Bun.file(sourceFilePath).exists()) {
13772
- validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`);
13773
- continue;
13774
- }
13775
- await mkdir5(destFileDir, { recursive: true });
13776
- const sourceFile = Bun.file(sourceFilePath);
13777
- await Bun.write(destFilePath, sourceFile);
13778
- }
13779
- }
13780
- if (validationErrors.length > 0) {
13781
- throw new BuildRegistryError(`Build failed with ${validationErrors.length} errors`, validationErrors);
13782
- }
13783
- const index = {
13784
- name: registry.name,
13785
- namespace: registry.namespace,
13786
- version: registry.version,
13787
- author: registry.author,
13788
- ...registry.opencode && { opencode: registry.opencode },
13789
- ...registry.ocx && { ocx: registry.ocx },
13790
- components: registry.components.map((c) => ({
13791
- name: c.name,
13792
- type: c.type,
13793
- description: c.description
13794
- }))
13795
- };
13796
- await Bun.write(join4(outPath, "index.json"), JSON.stringify(index, null, 2));
13797
- const wellKnownDir = join4(outPath, ".well-known");
13798
- await mkdir5(wellKnownDir, { recursive: true });
13799
- const discovery = { registry: "/index.json" };
13800
- await Bun.write(join4(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
13801
- return {
13802
- name: registry.name,
13803
- namespace: registry.namespace,
13804
- version: registry.version,
13805
- componentsCount: registry.components.length,
13806
- outputPath: outPath
13807
- };
13808
- }
13809
-
13810
- // src/commands/build.ts
14168
+ import { join as join5, relative as relative3 } from "path";
13811
14169
  function registerBuildCommand(program2) {
13812
- 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).action(async (path6, options2) => {
14170
+ 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).action(async (path7, options2) => {
13813
14171
  try {
13814
- const sourcePath = join5(options2.cwd, path6);
14172
+ const sourcePath = join5(options2.cwd, path7);
13815
14173
  const outPath = join5(options2.cwd, options2.out);
13816
14174
  const spinner2 = createSpinner({
13817
14175
  text: "Building registry...",
@@ -13824,7 +14182,7 @@ function registerBuildCommand(program2) {
13824
14182
  out: outPath
13825
14183
  });
13826
14184
  if (!options2.json) {
13827
- const msg = `Built ${result.componentsCount} components to ${relative2(options2.cwd, outPath)}`;
14185
+ const msg = `Built ${result.componentsCount} components to ${relative3(options2.cwd, outPath)}`;
13828
14186
  spinner2.succeed(msg);
13829
14187
  if (!process.stdout.isTTY) {
13830
14188
  logger.success(`Built ${result.componentsCount} components`);
@@ -13861,7 +14219,7 @@ import { existsSync as existsSync6 } from "fs";
13861
14219
  import { mkdir as mkdir6 } from "fs/promises";
13862
14220
  import { join as join6 } from "path";
13863
14221
  function registerConfigEditCommand(parent) {
13864
- parent.command("edit").description("Open configuration file in editor").option("-g, --global", "Edit global ocx.jsonc").action(async (options2) => {
14222
+ 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) => {
13865
14223
  try {
13866
14224
  await runConfigEdit(options2);
13867
14225
  } catch (error) {
@@ -13870,12 +14228,26 @@ function registerConfigEditCommand(parent) {
13870
14228
  });
13871
14229
  }
13872
14230
  async function runConfigEdit(options2) {
14231
+ if (options2.global && options2.profile) {
14232
+ throw new ValidationError("Cannot use both --global and --profile flags");
14233
+ }
13873
14234
  let configPath;
13874
- if (options2.global) {
14235
+ if (options2.profile) {
14236
+ const parseResult = profileNameSchema.safeParse(options2.profile);
14237
+ if (!parseResult.success) {
14238
+ throw new ValidationError(`Invalid profile name "${options2.profile}": ${parseResult.error.errors[0]?.message ?? "Invalid name"}`);
14239
+ }
14240
+ await ProfileManager.requireInitialized();
14241
+ const manager = ProfileManager.create();
14242
+ if (!await manager.exists(options2.profile)) {
14243
+ throw new ProfileNotFoundError(options2.profile);
14244
+ }
14245
+ configPath = getProfileOcxConfig(options2.profile);
14246
+ } else if (options2.global) {
13875
14247
  configPath = getGlobalConfig();
13876
14248
  if (!existsSync6(configPath)) {
13877
14249
  throw new ConfigError(`Global config not found at ${configPath}.
13878
- ` + "Run 'opencode' once to initialize, then retry.");
14250
+ Run 'ocx init --global' first.`);
13879
14251
  }
13880
14252
  } else {
13881
14253
  const localConfigDir = findLocalConfigDir(process.cwd());
@@ -14112,16 +14484,16 @@ class Diff {
14112
14484
  }
14113
14485
  }
14114
14486
  }
14115
- addToPath(path6, added, removed, oldPosInc, options2) {
14116
- const last = path6.lastComponent;
14487
+ addToPath(path7, added, removed, oldPosInc, options2) {
14488
+ const last = path7.lastComponent;
14117
14489
  if (last && !options2.oneChangePerToken && last.added === added && last.removed === removed) {
14118
14490
  return {
14119
- oldPos: path6.oldPos + oldPosInc,
14491
+ oldPos: path7.oldPos + oldPosInc,
14120
14492
  lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
14121
14493
  };
14122
14494
  } else {
14123
14495
  return {
14124
- oldPos: path6.oldPos + oldPosInc,
14496
+ oldPos: path7.oldPos + oldPosInc,
14125
14497
  lastComponent: { count: 1, added, removed, previousComponent: last }
14126
14498
  };
14127
14499
  }
@@ -14938,7 +15310,7 @@ import {
14938
15310
  rmSync,
14939
15311
  unlinkSync
14940
15312
  } from "fs";
14941
- import path6 from "path";
15313
+ import path7 from "path";
14942
15314
  var GHOST_CONFIG_FILE = "ghost.jsonc";
14943
15315
  var BACKUP_EXT = ".bak";
14944
15316
  var CURRENT_SYMLINK = "current";
@@ -14976,8 +15348,8 @@ function planMigration() {
14976
15348
  if (!entry.isDirectory() || entry.name === CURRENT_SYMLINK)
14977
15349
  continue;
14978
15350
  const profileName = entry.name;
14979
- const ghostConfig = path6.join(profilesDir, profileName, GHOST_CONFIG_FILE);
14980
- const ocxConfig = path6.join(profilesDir, profileName, OCX_CONFIG_FILE);
15351
+ const ghostConfig = path7.join(profilesDir, profileName, GHOST_CONFIG_FILE);
15352
+ const ocxConfig = path7.join(profilesDir, profileName, OCX_CONFIG_FILE);
14981
15353
  if (!existsSync7(ghostConfig))
14982
15354
  continue;
14983
15355
  if (existsSync7(ocxConfig)) {
@@ -15007,13 +15379,13 @@ function planMigration() {
15007
15379
  if (!entry.isDirectory() || entry.name === CURRENT_SYMLINK)
15008
15380
  continue;
15009
15381
  const profileName = entry.name;
15010
- const profileDir = path6.join(profilesDir, profileName);
15011
- const dotOpencode = path6.join(profileDir, ".opencode");
15382
+ const profileDir = path7.join(profilesDir, profileName);
15383
+ const dotOpencode = path7.join(profileDir, ".opencode");
15012
15384
  if (!existsSync7(dotOpencode))
15013
15385
  continue;
15014
15386
  for (const dir of FLATTEN_DIRS) {
15015
- const source = path6.join(dotOpencode, dir);
15016
- const destination = path6.join(profileDir, dir);
15387
+ const source = path7.join(dotOpencode, dir);
15388
+ const destination = path7.join(profileDir, dir);
15017
15389
  if (!existsSync7(source))
15018
15390
  continue;
15019
15391
  try {
@@ -15052,7 +15424,7 @@ function planMigration() {
15052
15424
  });
15053
15425
  }
15054
15426
  }
15055
- const currentPath = path6.join(profilesDir, CURRENT_SYMLINK);
15427
+ const currentPath = path7.join(profilesDir, CURRENT_SYMLINK);
15056
15428
  if (existsSync7(currentPath)) {
15057
15429
  try {
15058
15430
  const stat3 = lstatSync(currentPath);
@@ -15111,21 +15483,21 @@ function executeMigration(plan) {
15111
15483
  } catch {}
15112
15484
  }
15113
15485
  } catch (error) {
15114
- for (const rename2 of completedRenames) {
15486
+ for (const rename3 of completedRenames) {
15115
15487
  try {
15116
- if (existsSync7(rename2.destination)) {
15117
- if (rename2.isDir) {
15118
- rmSync(rename2.destination, { recursive: true, force: true });
15488
+ if (existsSync7(rename3.destination)) {
15489
+ if (rename3.isDir) {
15490
+ rmSync(rename3.destination, { recursive: true, force: true });
15119
15491
  } else {
15120
- unlinkSync(rename2.destination);
15492
+ unlinkSync(rename3.destination);
15121
15493
  }
15122
15494
  }
15123
- mkdirSync(path6.dirname(rename2.source), { recursive: true });
15124
- if (existsSync7(rename2.backup)) {
15125
- if (rename2.isDir) {
15126
- cpSync(rename2.backup, rename2.source, { recursive: true });
15495
+ mkdirSync(path7.dirname(rename3.source), { recursive: true });
15496
+ if (existsSync7(rename3.backup)) {
15497
+ if (rename3.isDir) {
15498
+ cpSync(rename3.backup, rename3.source, { recursive: true });
15127
15499
  } else {
15128
- copyFileSync(rename2.backup, rename2.source);
15500
+ copyFileSync(rename3.backup, rename3.source);
15129
15501
  }
15130
15502
  }
15131
15503
  } catch {}
@@ -15146,8 +15518,8 @@ function executeMigration(plan) {
15146
15518
  try {
15147
15519
  const processedProfiles = new Set(plan.profiles.filter((a) => a.type === "move-dir").map((a) => a.profileName));
15148
15520
  for (const profileName of processedProfiles) {
15149
- const profileDir = path6.join(getProfilesDir(), profileName);
15150
- const dotOpencode = path6.join(profileDir, ".opencode");
15521
+ const profileDir = path7.join(getProfilesDir(), profileName);
15522
+ const dotOpencode = path7.join(profileDir, ".opencode");
15151
15523
  if (existsSync7(dotOpencode)) {
15152
15524
  const remaining = readdirSync(dotOpencode);
15153
15525
  if (remaining.length === 0) {
@@ -15173,7 +15545,7 @@ function printPlan(plan, dryRun) {
15173
15545
  if (action.type === "rename") {
15174
15546
  console.log(` \u2713 ${action.profileName}: ${GHOST_CONFIG_FILE} \u2192 ${OCX_CONFIG_FILE}`);
15175
15547
  } else {
15176
- const dirName = path6.basename(action.source);
15548
+ const dirName = path7.basename(action.source);
15177
15549
  console.log(` \u2713 ${action.profileName}: .opencode/${dirName}/ \u2192 ${dirName}/`);
15178
15550
  }
15179
15551
  }
@@ -15291,34 +15663,90 @@ async function runInit(options2) {
15291
15663
  }
15292
15664
  }
15293
15665
  async function runInitGlobal(options2) {
15294
- const manager = ProfileManager.create();
15295
- if (await manager.isInitialized()) {
15296
- const profilesDir = getProfilesDir();
15297
- throw new ProfileExistsError(`Global profiles already initialized at ${profilesDir}`);
15298
- }
15299
15666
  const spin = options2.quiet ? null : createSpinner({ text: "Initializing global profiles..." });
15300
15667
  spin?.start();
15301
15668
  try {
15302
- await manager.initialize();
15669
+ const created = [];
15670
+ const existed = [];
15671
+ const globalConfigPath = getGlobalConfig();
15672
+ if (existsSync8(globalConfigPath)) {
15673
+ existed.push("globalConfig");
15674
+ } else {
15675
+ await mkdir7(dirname3(globalConfigPath), { recursive: true, mode: 448 });
15676
+ await atomicWrite(globalConfigPath, {
15677
+ $schema: OCX_SCHEMA_URL,
15678
+ registries: {}
15679
+ });
15680
+ created.push("globalConfig");
15681
+ }
15303
15682
  const profilesDir = getProfilesDir();
15304
- const ocxConfigPath = getProfileOcxConfig("default");
15683
+ if (!existsSync8(profilesDir)) {
15684
+ await mkdir7(profilesDir, { recursive: true, mode: 448 });
15685
+ }
15686
+ const profileDir = getProfileDir("default");
15687
+ if (!existsSync8(profileDir)) {
15688
+ await mkdir7(profileDir, { recursive: true, mode: 448 });
15689
+ }
15690
+ const ocxPath = getProfileOcxConfig("default");
15691
+ if (existsSync8(ocxPath)) {
15692
+ existed.push("profileOcx");
15693
+ } else {
15694
+ await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
15695
+ created.push("profileOcx");
15696
+ }
15697
+ const opencodePath = getProfileOpencodeConfig("default");
15698
+ if (existsSync8(opencodePath)) {
15699
+ existed.push("profileOpencode");
15700
+ } else {
15701
+ await atomicWrite(opencodePath, {});
15702
+ created.push("profileOpencode");
15703
+ }
15704
+ const agentsPath = getProfileAgents("default");
15705
+ if (existsSync8(agentsPath)) {
15706
+ existed.push("profileAgents");
15707
+ } else {
15708
+ const agentsContent = `# Profile Instructions
15709
+
15710
+ <!-- Add your custom instructions for this profile here -->
15711
+ <!-- These will be included when running \`ocx opencode -p default\` -->
15712
+ `;
15713
+ await Bun.write(agentsPath, agentsContent, { mode: 384 });
15714
+ created.push("profileAgents");
15715
+ }
15305
15716
  spin?.succeed("Initialized global profiles");
15306
15717
  if (options2.json) {
15307
15718
  console.log(JSON.stringify({
15308
15719
  success: true,
15309
- profilesDir,
15310
- defaultProfile: "default",
15311
- ocxConfigPath
15720
+ files: {
15721
+ globalConfig: globalConfigPath,
15722
+ profileOcx: ocxPath,
15723
+ profileOpencode: opencodePath,
15724
+ profileAgents: agentsPath
15725
+ },
15726
+ created,
15727
+ existed
15312
15728
  }));
15313
15729
  } else if (!options2.quiet) {
15314
- logger.info(`Created ${profilesDir}`);
15315
- logger.info(`Created profile "default"`);
15316
- logger.info("");
15317
- logger.info("Next steps:");
15318
- logger.info(" 1. Edit your profile config: ocx profile config");
15319
- logger.info(" 2. Add registries: ocx registry add <url> --profile default");
15320
- logger.info(" 3. Launch OpenCode: ocx opencode");
15321
- logger.info(" 4. Create more profiles: ocx profile add <name>");
15730
+ if (created.length > 0) {
15731
+ for (const key of created) {
15732
+ if (key === "globalConfig")
15733
+ logger.info(`Created global config: ${globalConfigPath}`);
15734
+ if (key === "profileOcx")
15735
+ logger.info(`Created profile config: ${ocxPath}`);
15736
+ if (key === "profileOpencode")
15737
+ logger.info(`Created profile opencode config: ${opencodePath}`);
15738
+ if (key === "profileAgents")
15739
+ logger.info(`Created profile instructions: ${agentsPath}`);
15740
+ }
15741
+ logger.info("");
15742
+ logger.info("Next steps:");
15743
+ logger.info(" 1. Edit your profile config: ocx config edit -p default");
15744
+ logger.info(" 2. Add registries: ocx registry add <url> --name <name> --global");
15745
+ logger.info(" 3. Launch OpenCode: ocx opencode");
15746
+ logger.info(" 4. Create more profiles: ocx profile add <name>");
15747
+ } else {
15748
+ logger.info("Global profiles already initialized (all files exist)");
15749
+ }
15322
15750
  }
15323
15751
  } catch (error) {
15324
15752
  spin?.fail("Failed to initialize");
@@ -15346,7 +15774,7 @@ async function runInitRegistry(directory, options2) {
15346
15774
  await mkdir7(cwd, { recursive: true });
15347
15775
  await copyDir(options2.local, cwd);
15348
15776
  } else {
15349
- const version = options2.canary ? "main" : await getLatestVersion();
15777
+ const version = options2.canary ? "main" : getReleaseTag();
15350
15778
  await fetchAndExtractTemplate(cwd, version, options2.verbose);
15351
15779
  }
15352
15780
  if (spin)
@@ -15375,15 +15803,16 @@ async function runInitRegistry(directory, options2) {
15375
15803
  async function copyDir(src, dest) {
15376
15804
  await cp(src, dest, { recursive: true });
15377
15805
  }
15378
- async function getLatestVersion() {
15379
- const pkgPath = new URL("../../package.json", import.meta.url);
15380
- const pkgContent = await readFile(pkgPath);
15381
- const pkg = JSON.parse(pkgContent.toString());
15382
- return `v${pkg.version}`;
15806
+ function getReleaseTag() {
15807
+ if (false) {}
15808
+ return `v${"1.4.3"}`;
15383
15809
  }
15384
- async function fetchAndExtractTemplate(destDir, version, verbose) {
15810
+ function getTemplateUrl(version) {
15385
15811
  const ref = version === "main" ? "heads/main" : `tags/${version}`;
15386
- const tarballUrl = `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
15812
+ return `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
15813
+ }
15814
+ async function fetchAndExtractTemplate(destDir, version, verbose) {
15815
+ const tarballUrl = getTemplateUrl(version);
15387
15816
  if (verbose) {
15388
15817
  logger.info(`Fetching ${tarballUrl}`);
15389
15818
  }
@@ -15417,6 +15846,9 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
15417
15846
  await rm2(tempDir, { recursive: true, force: true });
15418
15847
  }
15419
15848
  }
15849
+ function toTitleCase(str) {
15850
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
15851
+ }
15420
15852
  async function replacePlaceholders(dir, values) {
15421
15853
  const filesToProcess = [
15422
15854
  "registry.jsonc",
@@ -15431,16 +15863,17 @@ async function replacePlaceholders(dir, values) {
15431
15863
  continue;
15432
15864
  let content2 = await readFile(filePath).then((b) => b.toString());
15433
15865
  content2 = content2.replace(/my-registry/g, values.namespace);
15866
+ content2 = content2.replace(/My Registry/g, toTitleCase(values.namespace));
15434
15867
  content2 = content2.replace(/Your Name/g, values.author);
15435
15868
  await writeFile2(filePath, content2);
15436
15869
  }
15437
15870
  }
15438
15871
 
15439
15872
  // src/commands/opencode.ts
15440
- import { resolve as resolve2 } from "path";
15873
+ import { resolve as resolve3 } from "path";
15441
15874
 
15442
15875
  // src/utils/terminal-title.ts
15443
- import path7 from "path";
15876
+ import path8 from "path";
15444
15877
  var MAX_BRANCH_LENGTH = 20;
15445
15878
  var titleSaved = false;
15446
15879
  function isInsideTmux() {
@@ -15483,7 +15916,7 @@ function restoreTerminalTitle() {
15483
15916
  titleSaved = false;
15484
15917
  }
15485
15918
  function formatTerminalName(cwd, profileName, gitInfo) {
15486
- const repoName = gitInfo.repoName ?? path7.basename(cwd);
15919
+ const repoName = gitInfo.repoName ?? path8.basename(cwd);
15487
15920
  if (!gitInfo.branch) {
15488
15921
  return `ocx[${profileName}]:${repoName}`;
15489
15922
  }
@@ -15492,17 +15925,29 @@ function formatTerminalName(cwd, profileName, gitInfo) {
15492
15925
  }
15493
15926
 
15494
15927
  // src/commands/opencode.ts
15928
+ function resolveOpenCodeBinary(opts) {
15929
+ return opts.configBin ?? opts.envBin ?? "opencode";
15930
+ }
15931
+ function buildOpenCodeEnv(opts) {
15932
+ return {
15933
+ ...opts.baseEnv,
15934
+ ...opts.disableProjectConfig && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
15935
+ ...opts.profileDir && { OPENCODE_CONFIG_DIR: opts.profileDir },
15936
+ ...opts.mergedConfig && { OPENCODE_CONFIG_CONTENT: JSON.stringify(opts.mergedConfig) },
15937
+ ...opts.profileName && { OCX_PROFILE: opts.profileName }
15938
+ };
15939
+ }
15495
15940
  function registerOpencodeCommand(program2) {
15496
- program2.command("opencode [path]").alias("oc").description("Launch OpenCode with resolved configuration").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").addOption(sharedOptions.quiet()).addOption(sharedOptions.json()).allowUnknownOption().allowExcessArguments(true).action(async (path8, options2, command) => {
15941
+ program2.command("opencode [path]").alias("oc").description("Launch OpenCode with resolved configuration").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").addOption(sharedOptions.quiet()).addOption(sharedOptions.json()).allowUnknownOption().allowExcessArguments(true).action(async (path9, options2, command) => {
15497
15942
  try {
15498
- await runOpencode(path8, command.args, options2);
15943
+ await runOpencode(path9, command.args, options2);
15499
15944
  } catch (error) {
15500
15945
  handleError(error, { json: options2.json });
15501
15946
  }
15502
15947
  });
15503
15948
  }
15504
15949
  async function runOpencode(pathArg, args, options2) {
15505
- const projectDir = pathArg ? resolve2(pathArg) : process.cwd();
15950
+ const projectDir = pathArg ? resolve3(pathArg) : process.cwd();
15506
15951
  const resolver = await ConfigResolver.create(projectDir, { profile: options2.profile });
15507
15952
  const config = resolver.resolve();
15508
15953
  const profile = resolver.getProfile();
@@ -15546,17 +15991,20 @@ async function runOpencode(pathArg, args, options2) {
15546
15991
  const gitInfo = await getGitInfo(projectDir);
15547
15992
  setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
15548
15993
  }
15549
- const bin = ocxConfig?.bin ?? process.env.OPENCODE_BIN ?? "opencode";
15994
+ const bin = resolveOpenCodeBinary({
15995
+ configBin: ocxConfig?.bin,
15996
+ envBin: process.env.OPENCODE_BIN
15997
+ });
15550
15998
  proc = Bun.spawn({
15551
15999
  cmd: [bin, ...args],
15552
16000
  cwd: projectDir,
15553
- env: {
15554
- ...process.env,
15555
- OPENCODE_DISABLE_PROJECT_CONFIG: "true",
15556
- ...profileDir && { OPENCODE_CONFIG_DIR: profileDir },
15557
- ...configToPass && { OPENCODE_CONFIG_CONTENT: JSON.stringify(configToPass) },
15558
- ...config.profileName && { OCX_PROFILE: config.profileName }
15559
- },
16001
+ env: buildOpenCodeEnv({
16002
+ baseEnv: process.env,
16003
+ profileDir,
16004
+ profileName: config.profileName ?? undefined,
16005
+ mergedConfig: configToPass,
16006
+ disableProjectConfig: true
16007
+ }),
15560
16008
  stdin: "inherit",
15561
16009
  stdout: "inherit",
15562
16010
  stderr: "inherit"
@@ -15584,12 +16032,8 @@ async function runOpencode(pathArg, args, options2) {
15584
16032
  // src/commands/profile/install-from-registry.ts
15585
16033
  import { createHash as createHash2 } from "crypto";
15586
16034
  import { existsSync as existsSync9 } from "fs";
15587
- import { mkdir as mkdir8, mkdtemp, rename as rename2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
16035
+ import { mkdir as mkdir8, mkdtemp, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
15588
16036
  import { dirname as dirname4, join as join8 } from "path";
15589
- var PROFILE_FILE_TARGETS = new Set(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
15590
- function isProfileFile(target) {
15591
- return PROFILE_FILE_TARGETS.has(target);
15592
- }
15593
16037
  function hashContent2(content2) {
15594
16038
  return createHash2("sha256").update(content2).digest("hex");
15595
16039
  }
@@ -15641,7 +16085,7 @@ Use --force to overwrite.`);
15641
16085
  const filesSpin = quiet ? null : createSpinner({ text: "Downloading profile files..." });
15642
16086
  filesSpin?.start();
15643
16087
  const profileFiles = [];
15644
- const dependencyFiles = [];
16088
+ const embeddedFiles = [];
15645
16089
  for (const file of normalized.files) {
15646
16090
  const content2 = await fetchFileContent(registryUrl, component, file.path);
15647
16091
  const fileEntry = {
@@ -15649,10 +16093,10 @@ Use --force to overwrite.`);
15649
16093
  target: file.target,
15650
16094
  content: Buffer.from(content2)
15651
16095
  };
15652
- if (isProfileFile(file.target)) {
15653
- profileFiles.push(fileEntry);
16096
+ if (file.target.startsWith(".opencode/")) {
16097
+ embeddedFiles.push(fileEntry);
15654
16098
  } else {
15655
- dependencyFiles.push(fileEntry);
16099
+ profileFiles.push(fileEntry);
15656
16100
  }
15657
16101
  }
15658
16102
  filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
@@ -15706,7 +16150,7 @@ Use --force to overwrite.`);
15706
16150
  }
15707
16151
  await writeFile3(targetPath, file.content);
15708
16152
  }
15709
- for (const file of dependencyFiles) {
16153
+ for (const file of embeddedFiles) {
15710
16154
  const target = file.target.startsWith(".opencode/") ? file.target.slice(".opencode/".length) : file.target;
15711
16155
  const targetPath = join8(stagingOpencodeDir, target);
15712
16156
  const targetDir = dirname4(targetPath);
@@ -15715,7 +16159,7 @@ Use --force to overwrite.`);
15715
16159
  }
15716
16160
  await writeFile3(targetPath, file.content);
15717
16161
  }
15718
- writeSpin?.succeed(`Wrote ${profileFiles.length + dependencyFiles.length} profile files`);
16162
+ writeSpin?.succeed(`Wrote ${profileFiles.length + embeddedFiles.length} profile files`);
15719
16163
  if (dependencyBundles.length > 0) {
15720
16164
  const depWriteSpin = quiet ? null : createSpinner({ text: "Writing dependency files..." });
15721
16165
  depWriteSpin?.start();
@@ -15764,16 +16208,16 @@ Use --force to overwrite.`);
15764
16208
  }
15765
16209
  if (profileExists && force) {
15766
16210
  const backupDir = `${profileDir}.backup-${Date.now()}`;
15767
- await rename2(profileDir, backupDir);
16211
+ await rename3(profileDir, backupDir);
15768
16212
  try {
15769
- await rename2(stagingDir, profileDir);
16213
+ await rename3(stagingDir, profileDir);
15770
16214
  } catch (err) {
15771
- await rename2(backupDir, profileDir);
16215
+ await rename3(backupDir, profileDir);
15772
16216
  throw err;
15773
16217
  }
15774
16218
  await rm3(backupDir, { recursive: true, force: true });
15775
16219
  } else {
15776
- await rename2(stagingDir, profileDir);
16220
+ await rename3(stagingDir, profileDir);
15777
16221
  }
15778
16222
  moveSpin?.succeed("Installation complete");
15779
16223
  if (!quiet) {
@@ -15846,14 +16290,14 @@ async function requireGlobalRegistry(namespace) {
15846
16290
  throw new ConfigError(`Registry "${namespace}" is not configured globally.
15847
16291
 
15848
16292
  ` + `Profile installation requires global registry configuration.
15849
- ` + `Run: ocx registry add ${namespace} <url> --global`);
16293
+ ` + `Run: ocx registry add <url> --name ${namespace} --global`);
15850
16294
  }
15851
16295
  const registry = globalConfig.registries[namespace];
15852
16296
  if (!registry) {
15853
16297
  throw new ConfigError(`Registry "${namespace}" is not configured globally.
15854
16298
 
15855
16299
  ` + `Profile installation requires global registry configuration.
15856
- ` + `Run: ocx registry add ${namespace} <url> --global`);
16300
+ ` + `Run: ocx registry add <url> --name ${namespace} --global`);
15857
16301
  }
15858
16302
  return { config: globalConfig, registryUrl: registry.url };
15859
16303
  }
@@ -15931,33 +16375,6 @@ async function cloneFromLocalProfile(manager, name, sourceName, exists) {
15931
16375
  logger.success(`Created profile "${name}" (cloned from "${sourceName}")`);
15932
16376
  }
15933
16377
 
15934
- // src/commands/profile/config.ts
15935
- function registerProfileConfigCommand(parent) {
15936
- parent.command("config [name]").description("Open profile ocx.jsonc in editor").action(async (name) => {
15937
- try {
15938
- await runProfileConfig(name);
15939
- } catch (error) {
15940
- handleError(error);
15941
- }
15942
- });
15943
- }
15944
- async function runProfileConfig(name) {
15945
- const manager = await ProfileManager.requireInitialized();
15946
- const profileName = name ?? await manager.resolveProfile();
15947
- await manager.get(profileName);
15948
- const configPath = getProfileOcxConfig(profileName);
15949
- const editor = process.env.EDITOR || process.env.VISUAL || "vi";
15950
- const proc = Bun.spawn([editor, configPath], {
15951
- stdin: "inherit",
15952
- stdout: "inherit",
15953
- stderr: "inherit"
15954
- });
15955
- const exitCode = await proc.exited;
15956
- if (exitCode !== 0) {
15957
- throw new Error(`Editor exited with code ${exitCode}`);
15958
- }
15959
- }
15960
-
15961
16378
  // src/commands/profile/list.ts
15962
16379
  function registerProfileListCommand(parent) {
15963
16380
  parent.command("list").alias("ls").description("List all global profiles").addOption(sharedOptions.json()).action(async (options2) => {
@@ -15985,6 +16402,25 @@ async function runProfileList(options2) {
15985
16402
  }
15986
16403
  }
15987
16404
 
16405
+ // src/commands/profile/move.ts
16406
+ function registerProfileMoveCommand(parent) {
16407
+ parent.command("move <old-name> <new-name>").alias("mv").description("Move (rename) a profile").action(async (oldName, newName) => {
16408
+ try {
16409
+ await runProfileMove(oldName, newName);
16410
+ } catch (error) {
16411
+ handleError(error);
16412
+ }
16413
+ });
16414
+ }
16415
+ async function runProfileMove(oldName, newName) {
16416
+ const manager = await ProfileManager.requireInitialized();
16417
+ const { warnActiveProfile } = await manager.move(oldName, newName);
16418
+ if (warnActiveProfile) {
16419
+ logger.warn(`Moving active profile. Update OCX_PROFILE env var to "${newName}".`);
16420
+ }
16421
+ logger.success(`Moved profile "${oldName}" \u2192 "${newName}"`);
16422
+ }
16423
+
15988
16424
  // src/commands/profile/remove.ts
15989
16425
  function registerProfileRemoveCommand(parent) {
15990
16426
  parent.command("remove <name>").alias("rm").description("Delete a global profile").action(async (name) => {
@@ -16043,23 +16479,45 @@ function registerProfileCommand(program2) {
16043
16479
  registerProfileListCommand(profile);
16044
16480
  registerProfileAddCommand(profile);
16045
16481
  registerProfileRemoveCommand(profile);
16482
+ registerProfileMoveCommand(profile);
16046
16483
  registerProfileShowCommand(profile);
16047
- registerProfileConfigCommand(profile);
16048
16484
  }
16049
16485
 
16050
16486
  // src/commands/registry.ts
16487
+ import { existsSync as existsSync10 } from "fs";
16488
+ import { dirname as dirname5, join as join9 } from "path";
16051
16489
  async function runRegistryAddCore2(url, options2, callbacks) {
16052
16490
  if (callbacks.isLocked?.()) {
16053
16491
  throw new Error("Registries are locked. Cannot add.");
16054
16492
  }
16055
- const name = options2.name || new URL(url).hostname.replace(/\./g, "-");
16493
+ const trimmedUrl = url.trim();
16494
+ if (!trimmedUrl) {
16495
+ throw new ValidationError("Registry URL is required");
16496
+ }
16497
+ let derivedName;
16498
+ try {
16499
+ const parsed = new URL(trimmedUrl);
16500
+ if (!["http:", "https:"].includes(parsed.protocol)) {
16501
+ throw new ValidationError(`Invalid registry URL: ${trimmedUrl} (must use http or https)`);
16502
+ }
16503
+ derivedName = options2.name || parsed.hostname.replace(/\./g, "-");
16504
+ } catch (error) {
16505
+ if (error instanceof ValidationError)
16506
+ throw error;
16507
+ throw new ValidationError(`Invalid registry URL: ${trimmedUrl}`);
16508
+ }
16509
+ const name = derivedName;
16056
16510
  const registries = callbacks.getRegistries();
16511
+ const existingRegistry = registries[name];
16512
+ if (existingRegistry && !options2.force) {
16513
+ throw new RegistryExistsError(name, existingRegistry.url, trimmedUrl);
16514
+ }
16057
16515
  const isUpdate = name in registries;
16058
16516
  await callbacks.setRegistry(name, {
16059
- url,
16517
+ url: trimmedUrl,
16060
16518
  version: options2.version
16061
16519
  });
16062
- return { name, url, updated: isUpdate };
16520
+ return { name, url: trimmedUrl, updated: isUpdate };
16063
16521
  }
16064
16522
  async function runRegistryRemoveCore(name, callbacks) {
16065
16523
  if (callbacks.isLocked?.()) {
@@ -16082,43 +16540,69 @@ function runRegistryListCore(callbacks) {
16082
16540
  }));
16083
16541
  return { registries: list, locked };
16084
16542
  }
16543
+ async function resolveRegistryTarget(options2, command, cwd) {
16544
+ const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16545
+ if (options2.global && options2.profile) {
16546
+ throw new ValidationError("Cannot use both --global and --profile flags");
16547
+ }
16548
+ if (cwdExplicitlyProvided && options2.profile) {
16549
+ throw new ValidationError("Cannot use both --cwd and --profile flags");
16550
+ }
16551
+ if (options2.global && cwdExplicitlyProvided) {
16552
+ throw new ValidationError("Cannot use both --global and --cwd flags");
16553
+ }
16554
+ if (options2.profile) {
16555
+ validateProfileName(options2.profile);
16556
+ const manager = await ProfileManager.requireInitialized();
16557
+ if (!await manager.exists(options2.profile)) {
16558
+ throw new ProfileNotFoundError(options2.profile);
16559
+ }
16560
+ const configPath = getProfileOcxConfig(options2.profile);
16561
+ if (!existsSync10(configPath)) {
16562
+ throw new OcxConfigError(`Profile '${options2.profile}' has no ocx.jsonc. Run 'ocx profile config ${options2.profile}' to create it.`);
16563
+ }
16564
+ return {
16565
+ scope: "profile",
16566
+ configPath,
16567
+ configDir: dirname5(configPath),
16568
+ targetLabel: `profile '${options2.profile}' config`
16569
+ };
16570
+ }
16571
+ if (options2.global) {
16572
+ const configDir = getGlobalConfigPath();
16573
+ return {
16574
+ scope: "global",
16575
+ configPath: join9(configDir, "ocx.jsonc"),
16576
+ configDir,
16577
+ targetLabel: "global config"
16578
+ };
16579
+ }
16580
+ const found = findOcxConfig(cwd);
16581
+ return {
16582
+ scope: "local",
16583
+ configPath: found.path,
16584
+ configDir: found.exists ? dirname5(found.path) : join9(cwd, ".opencode"),
16585
+ targetLabel: "local config"
16586
+ };
16587
+ }
16085
16588
  function registerRegistryCommand(program2) {
16086
16589
  const registry = program2.command("registry").description("Manage registries");
16087
- const addCmd = registry.command("add").description("Add a registry").argument("<url>", "Registry URL").option("--name <name>", "Registry alias (defaults to hostname)").option("--version <version>", "Pin to specific version");
16590
+ const addCmd = registry.command("add").description("Add a registry").argument("<url>", "Registry URL").option("--name <name>", "Registry alias (defaults to hostname)").option("--version <version>", "Pin to specific version").option("-f, --force", "Overwrite existing registry");
16088
16591
  addGlobalOption(addCmd);
16592
+ addProfileOption(addCmd);
16089
16593
  addCommonOptions(addCmd);
16090
16594
  addCmd.action(async (url, options2, command) => {
16595
+ let target;
16091
16596
  try {
16092
- const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16093
- if (options2.global && cwdExplicitlyProvided) {
16094
- logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
16597
+ const cwd = options2.cwd ?? process.cwd();
16598
+ target = await resolveRegistryTarget(options2, command, cwd);
16599
+ const { configDir, configPath } = target;
16600
+ const config = await readOcxConfig(configDir);
16601
+ if (!config) {
16602
+ const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
16603
+ logger.error(`${target.targetLabel} not found. ${initHint}`);
16095
16604
  process.exit(1);
16096
16605
  }
16097
- let configDir;
16098
- let configPath;
16099
- const config = await (async () => {
16100
- if (options2.global) {
16101
- configDir = getGlobalConfigPath();
16102
- const found = findOcxConfig(configDir);
16103
- configPath = found.path;
16104
- const cfg = await readOcxConfig(configDir);
16105
- if (!cfg) {
16106
- logger.error("Global config not found. Run 'opencode' once to initialize.");
16107
- process.exit(1);
16108
- }
16109
- return cfg;
16110
- } else {
16111
- configDir = options2.cwd ?? process.cwd();
16112
- const found = findOcxConfig(configDir);
16113
- configPath = found.path;
16114
- const cfg = await readOcxConfig(configDir);
16115
- if (!cfg) {
16116
- logger.error("No ocx.jsonc found. Run 'ocx init' first.");
16117
- process.exit(1);
16118
- }
16119
- return cfg;
16120
- }
16121
- })();
16122
16606
  const result = await runRegistryAddCore2(url, options2, {
16123
16607
  getRegistries: () => config.registries,
16124
16608
  isLocked: () => config.lockRegistries ?? false,
@@ -16130,65 +16614,46 @@ function registerRegistryCommand(program2) {
16130
16614
  if (options2.json) {
16131
16615
  outputJson({ success: true, data: result });
16132
16616
  } else if (!options2.quiet) {
16133
- const location = options2.global ? "global config" : "local config";
16134
16617
  if (result.updated) {
16135
- logger.success(`Updated registry in ${location}: ${result.name} -> ${result.url}`);
16618
+ logger.success(`Updated registry in ${target.targetLabel}: ${result.name} -> ${result.url}`);
16136
16619
  } else {
16137
- logger.success(`Added registry to ${location}: ${result.name} -> ${result.url}`);
16620
+ logger.success(`Added registry to ${target.targetLabel}: ${result.name} -> ${result.url}`);
16138
16621
  }
16139
16622
  }
16140
16623
  } catch (error) {
16624
+ if (error instanceof RegistryExistsError && !error.targetLabel) {
16625
+ const enrichedError = new RegistryExistsError(error.registryName, error.existingUrl, error.newUrl, target?.targetLabel ?? "config");
16626
+ handleError(enrichedError, { json: options2.json });
16627
+ }
16141
16628
  handleError(error, { json: options2.json });
16142
16629
  }
16143
16630
  });
16144
16631
  const removeCmd = registry.command("remove").description("Remove a registry").argument("<name>", "Registry name");
16145
16632
  addGlobalOption(removeCmd);
16633
+ addProfileOption(removeCmd);
16146
16634
  addCommonOptions(removeCmd);
16147
16635
  removeCmd.action(async (name, options2, command) => {
16148
16636
  try {
16149
- const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16150
- if (options2.global && cwdExplicitlyProvided) {
16151
- logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
16637
+ const cwd = options2.cwd ?? process.cwd();
16638
+ const target = await resolveRegistryTarget(options2, command, cwd);
16639
+ const config = await readOcxConfig(target.configDir);
16640
+ if (!config) {
16641
+ const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
16642
+ logger.error(`${target.targetLabel} not found. ${initHint}`);
16152
16643
  process.exit(1);
16153
16644
  }
16154
- let configDir;
16155
- let configPath;
16156
- const config = await (async () => {
16157
- if (options2.global) {
16158
- configDir = getGlobalConfigPath();
16159
- const found = findOcxConfig(configDir);
16160
- configPath = found.path;
16161
- const cfg = await readOcxConfig(configDir);
16162
- if (!cfg) {
16163
- logger.error("Global config not found. Run 'opencode' once to initialize.");
16164
- process.exit(1);
16165
- }
16166
- return cfg;
16167
- } else {
16168
- configDir = options2.cwd ?? process.cwd();
16169
- const found = findOcxConfig(configDir);
16170
- configPath = found.path;
16171
- const cfg = await readOcxConfig(configDir);
16172
- if (!cfg) {
16173
- logger.error("No ocx.jsonc found. Run 'ocx init' first.");
16174
- process.exit(1);
16175
- }
16176
- return cfg;
16177
- }
16178
- })();
16179
16645
  const result = await runRegistryRemoveCore(name, {
16180
16646
  getRegistries: () => config.registries,
16181
16647
  isLocked: () => config.lockRegistries ?? false,
16182
16648
  removeRegistry: async (regName) => {
16183
16649
  delete config.registries[regName];
16184
- await writeOcxConfig(configDir, config, configPath);
16650
+ await writeOcxConfig(target.configDir, config, target.configPath);
16185
16651
  }
16186
16652
  });
16187
16653
  if (options2.json) {
16188
16654
  outputJson({ success: true, data: result });
16189
16655
  } else if (!options2.quiet) {
16190
- const location = options2.global ? "global config" : "local config";
16191
- logger.success(`Removed registry from ${location}: ${result.removed}`);
16656
+ logger.success(`Removed registry from ${target.targetLabel}: ${result.removed}`);
16192
16657
  }
16193
16658
  } catch (error) {
16194
16659
  handleError(error, { json: options2.json });
@@ -16196,36 +16661,18 @@ function registerRegistryCommand(program2) {
16196
16661
  });
16197
16662
  const listCmd = registry.command("list").description("List configured registries");
16198
16663
  addGlobalOption(listCmd);
16664
+ addProfileOption(listCmd);
16199
16665
  addCommonOptions(listCmd);
16200
16666
  listCmd.action(async (options2, command) => {
16201
16667
  try {
16202
- const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16203
- if (options2.global && cwdExplicitlyProvided) {
16204
- logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
16205
- process.exit(1);
16206
- }
16207
- let configDir;
16208
- const config = await (async () => {
16209
- if (options2.global) {
16210
- configDir = getGlobalConfigPath();
16211
- const cfg = await readOcxConfig(configDir);
16212
- if (!cfg) {
16213
- logger.warn("Global config not found. Run 'opencode' once to initialize.");
16214
- return null;
16215
- }
16216
- return cfg;
16217
- } else {
16218
- configDir = options2.cwd ?? process.cwd();
16219
- const cfg = await readOcxConfig(configDir);
16220
- if (!cfg) {
16221
- logger.warn("No ocx.jsonc found. Run 'ocx init' first.");
16222
- return null;
16223
- }
16224
- return cfg;
16225
- }
16226
- })();
16227
- if (!config)
16668
+ const cwd = options2.cwd ?? process.cwd();
16669
+ const target = await resolveRegistryTarget(options2, command, cwd);
16670
+ const config = await readOcxConfig(target.configDir);
16671
+ if (!config) {
16672
+ const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
16673
+ logger.warn(`${target.targetLabel} not found. ${initHint}`);
16228
16674
  return;
16675
+ }
16229
16676
  const result = runRegistryListCore({
16230
16677
  getRegistries: () => config.registries,
16231
16678
  isLocked: () => config.lockRegistries ?? false
@@ -16236,7 +16683,8 @@ function registerRegistryCommand(program2) {
16236
16683
  if (result.registries.length === 0) {
16237
16684
  logger.info("No registries configured.");
16238
16685
  } else {
16239
- logger.info(`Configured registries${options2.global ? " (global)" : ""}${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
16686
+ const scopeLabel = target.scope === "global" ? " (global)" : target.scope === "profile" ? ` (profile '${options2.profile}')` : "";
16687
+ logger.info(`Configured registries${scopeLabel}${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
16240
16688
  for (const reg of result.registries) {
16241
16689
  console.log(` ${kleur_default.cyan(reg.name)}: ${reg.url} ${kleur_default.dim(`(${reg.version})`)}`);
16242
16690
  }
@@ -16369,9 +16817,460 @@ async function runSearchCore(query, options2, provider) {
16369
16817
  }
16370
16818
  }
16371
16819
 
16820
+ // src/commands/self/uninstall.ts
16821
+ import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync2, realpathSync, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
16822
+ import { homedir as homedir4 } from "os";
16823
+ import path9 from "path";
16824
+
16825
+ // src/self-update/detect-method.ts
16826
+ function parseInstallMethod(input) {
16827
+ const VALID_METHODS = ["curl", "npm", "yarn", "pnpm", "bun"];
16828
+ const method = VALID_METHODS.find((m) => m === input);
16829
+ if (!method) {
16830
+ throw new SelfUpdateError(`Invalid install method: "${input}"
16831
+ Valid methods: ${VALID_METHODS.join(", ")}`);
16832
+ }
16833
+ return method;
16834
+ }
16835
+ var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
16836
+ var isTempExecution = (path9) => path9.includes("/_npx/") || path9.includes("/.cache/bunx/") || path9.includes("/.pnpm/_temp/");
16837
+ var isYarnGlobalInstall = (path9) => path9.includes("/.yarn/global") || path9.includes("/.config/yarn/global");
16838
+ var isPnpmGlobalInstall = (path9) => path9.includes("/.pnpm/") || path9.includes("/pnpm/global");
16839
+ var isBunGlobalInstall = (path9) => path9.includes("/.bun/bin") || path9.includes("/.bun/install/global");
16840
+ var isNpmGlobalInstall = (path9) => path9.includes("/.npm/") || path9.includes("/node_modules/");
16841
+ function detectInstallMethod() {
16842
+ if (isCompiledBinary()) {
16843
+ return "curl";
16844
+ }
16845
+ const scriptPath = process.argv[1] ?? "";
16846
+ if (isTempExecution(scriptPath))
16847
+ return "unknown";
16848
+ if (isYarnGlobalInstall(scriptPath))
16849
+ return "yarn";
16850
+ if (isPnpmGlobalInstall(scriptPath))
16851
+ return "pnpm";
16852
+ if (isBunGlobalInstall(scriptPath))
16853
+ return "bun";
16854
+ if (isNpmGlobalInstall(scriptPath))
16855
+ return "npm";
16856
+ const userAgent = process.env.npm_config_user_agent ?? "";
16857
+ if (userAgent.includes("yarn"))
16858
+ return "yarn";
16859
+ if (userAgent.includes("pnpm"))
16860
+ return "pnpm";
16861
+ if (userAgent.includes("bun"))
16862
+ return "bun";
16863
+ if (userAgent.includes("npm"))
16864
+ return "npm";
16865
+ return "unknown";
16866
+ }
16867
+ function getExecutablePath() {
16868
+ if (typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/")) {
16869
+ return process.execPath;
16870
+ }
16871
+ return process.argv[1] ?? process.execPath;
16872
+ }
16873
+
16874
+ // src/commands/self/uninstall.ts
16875
+ var UNINSTALL_EXIT_CODES = {
16876
+ SUCCESS: 0,
16877
+ ERROR: 1,
16878
+ SAFETY_ERROR: 2
16879
+ };
16880
+ function isNodeError(err) {
16881
+ return err instanceof Error && "code" in err;
16882
+ }
16883
+ function tildify(absolutePath) {
16884
+ const home = homedir4();
16885
+ if (!home)
16886
+ return absolutePath;
16887
+ if (absolutePath === home)
16888
+ return "~";
16889
+ if (absolutePath.startsWith(home + path9.sep)) {
16890
+ return `~${absolutePath.slice(home.length)}`;
16891
+ }
16892
+ return absolutePath;
16893
+ }
16894
+ function getRelativePathIfContained(parent, child) {
16895
+ const normalizedParent = path9.normalize(parent);
16896
+ const normalizedChild = path9.normalize(child);
16897
+ const relative4 = path9.relative(normalizedParent, normalizedChild);
16898
+ if (relative4.startsWith("..") || path9.isAbsolute(relative4)) {
16899
+ return null;
16900
+ }
16901
+ return relative4;
16902
+ }
16903
+ function isLexicallyInside(root, target) {
16904
+ return getRelativePathIfContained(root, target) !== null;
16905
+ }
16906
+ function isRealpathInside(root, target) {
16907
+ if (!existsSync11(target)) {
16908
+ return { contained: true };
16909
+ }
16910
+ try {
16911
+ const realRoot = realpathSync(root);
16912
+ const realTarget = realpathSync(target);
16913
+ return { contained: getRelativePathIfContained(realRoot, realTarget) !== null };
16914
+ } catch (err) {
16915
+ if (isNodeError(err) && (err.code === "EACCES" || err.code === "EPERM")) {
16916
+ return { contained: false, error: "permission" };
16917
+ }
16918
+ return { contained: false, error: "io" };
16919
+ }
16920
+ }
16921
+ function validateRootDirectory(rootPath) {
16922
+ try {
16923
+ const stats = lstatSync2(rootPath);
16924
+ if (stats.isSymbolicLink()) {
16925
+ return { valid: false, reason: "symlink" };
16926
+ }
16927
+ if (!stats.isDirectory()) {
16928
+ return { valid: false, reason: "not-directory" };
16929
+ }
16930
+ return { valid: true };
16931
+ } catch (err) {
16932
+ if (isNodeError(err)) {
16933
+ if (err.code === "ENOENT") {
16934
+ return { valid: false, reason: "not-found" };
16935
+ }
16936
+ if (err.code === "EACCES" || err.code === "EPERM") {
16937
+ return { valid: false, reason: "permission" };
16938
+ }
16939
+ }
16940
+ return { valid: false, reason: "permission" };
16941
+ }
16942
+ }
16943
+ function getPathKind(targetPath) {
16944
+ if (!existsSync11(targetPath)) {
16945
+ return "missing";
16946
+ }
16947
+ try {
16948
+ const stats = lstatSync2(targetPath);
16949
+ if (stats.isSymbolicLink()) {
16950
+ return "symlink";
16951
+ }
16952
+ if (stats.isDirectory()) {
16953
+ return "directory";
16954
+ }
16955
+ return "file";
16956
+ } catch {
16957
+ return "missing";
16958
+ }
16959
+ }
16960
+ function isDirectoryEmpty(dirPath) {
16961
+ if (!existsSync11(dirPath)) {
16962
+ return true;
16963
+ }
16964
+ try {
16965
+ const entries = readdirSync2(dirPath);
16966
+ return entries.length === 0;
16967
+ } catch {
16968
+ return false;
16969
+ }
16970
+ }
16971
+ function classifyTargetSafety(target) {
16972
+ if (target.kind === "missing") {
16973
+ return "safe";
16974
+ }
16975
+ if (target.kind === "symlink") {
16976
+ return isLexicallyInside(target.rootPath, target.absolutePath) ? "safe" : "forbidden";
16977
+ }
16978
+ const result = isRealpathInside(target.rootPath, target.absolutePath);
16979
+ if (result.error) {
16980
+ return "error";
16981
+ }
16982
+ return result.contained ? "safe" : "forbidden";
16983
+ }
16984
+ function isPackageManaged(method) {
16985
+ return method === "npm" || method === "pnpm" || method === "bun" || method === "yarn";
16986
+ }
16987
+ function getPackageManagerCommand(method) {
16988
+ switch (method) {
16989
+ case "npm":
16990
+ return "npm uninstall -g ocx";
16991
+ case "pnpm":
16992
+ return "pnpm remove -g ocx";
16993
+ case "bun":
16994
+ return "bun remove -g ocx";
16995
+ case "yarn":
16996
+ return "yarn global remove ocx";
16997
+ default:
16998
+ return "npm uninstall -g ocx";
16999
+ }
17000
+ }
17001
+ function getGlobalConfigRoot() {
17002
+ const base = process.env.XDG_CONFIG_HOME || path9.join(homedir4(), ".config");
17003
+ return path9.join(base, "opencode");
17004
+ }
17005
+ function buildConfigTargets() {
17006
+ const rootPath = getGlobalConfigRoot();
17007
+ const targets = [];
17008
+ const profilesDir = getProfilesDir();
17009
+ const profilesRelative = getRelativePathIfContained(rootPath, profilesDir);
17010
+ if (profilesRelative) {
17011
+ const kind = getPathKind(profilesDir);
17012
+ targets.push({
17013
+ rootPath,
17014
+ relativePath: profilesRelative,
17015
+ absolutePath: profilesDir,
17016
+ displayPath: tildify(profilesDir),
17017
+ kind,
17018
+ deleteIfEmpty: false,
17019
+ safetyStatus: classifyTargetSafety({ rootPath, absolutePath: profilesDir, kind })
17020
+ });
17021
+ }
17022
+ const globalConfig = getGlobalConfig();
17023
+ const configRelative = getRelativePathIfContained(rootPath, globalConfig);
17024
+ if (configRelative) {
17025
+ const kind = getPathKind(globalConfig);
17026
+ targets.push({
17027
+ rootPath,
17028
+ relativePath: configRelative,
17029
+ absolutePath: globalConfig,
17030
+ displayPath: tildify(globalConfig),
17031
+ kind,
17032
+ deleteIfEmpty: false,
17033
+ safetyStatus: classifyTargetSafety({ rootPath, absolutePath: globalConfig, kind })
17034
+ });
17035
+ }
17036
+ const rootKind = getPathKind(rootPath);
17037
+ targets.push({
17038
+ rootPath,
17039
+ relativePath: ".",
17040
+ absolutePath: rootPath,
17041
+ displayPath: tildify(rootPath),
17042
+ kind: rootKind,
17043
+ deleteIfEmpty: true,
17044
+ safetyStatus: rootKind === "missing" ? "safe" : "safe"
17045
+ });
17046
+ return targets;
17047
+ }
17048
+ function buildBinaryTarget() {
17049
+ const method = detectInstallMethod();
17050
+ if (isPackageManaged(method)) {
17051
+ return null;
17052
+ }
17053
+ if (method === "curl") {
17054
+ const binaryPath = getExecutablePath();
17055
+ const kind = getPathKind(binaryPath);
17056
+ const parentDir = path9.dirname(binaryPath);
17057
+ return {
17058
+ rootPath: parentDir,
17059
+ relativePath: path9.basename(binaryPath),
17060
+ absolutePath: binaryPath,
17061
+ displayPath: tildify(binaryPath),
17062
+ kind,
17063
+ deleteIfEmpty: false,
17064
+ safetyStatus: kind === "missing" ? "safe" : "safe"
17065
+ };
17066
+ }
17067
+ return null;
17068
+ }
17069
+ function executeRemoval(target) {
17070
+ if (target.kind === "missing") {
17071
+ return { target, success: true, skipped: true, reason: "not found" };
17072
+ }
17073
+ if (target.safetyStatus === "forbidden") {
17074
+ return {
17075
+ target,
17076
+ success: false,
17077
+ skipped: true,
17078
+ reason: "containment violation",
17079
+ error: new Error("Target escapes containment boundary")
17080
+ };
17081
+ }
17082
+ if (target.deleteIfEmpty && target.kind === "directory") {
17083
+ if (!isDirectoryEmpty(target.absolutePath)) {
17084
+ return { target, success: true, skipped: true, reason: "not empty" };
17085
+ }
17086
+ }
17087
+ try {
17088
+ if (target.kind === "directory") {
17089
+ rmSync2(target.absolutePath, { recursive: true, force: true });
17090
+ } else {
17091
+ unlinkSync2(target.absolutePath);
17092
+ }
17093
+ return { target, success: true, skipped: false };
17094
+ } catch (err) {
17095
+ const error = err instanceof Error ? err : new Error(String(err));
17096
+ const reason = isNodeError(err) && (err.code === "EACCES" || err.code === "EPERM") ? "permission denied" : undefined;
17097
+ return { target, success: false, skipped: false, reason, error };
17098
+ }
17099
+ }
17100
+ function executeRemovals(targets) {
17101
+ return targets.map(executeRemoval);
17102
+ }
17103
+ function removeBinary(binaryPath) {
17104
+ const target = {
17105
+ rootPath: path9.dirname(binaryPath),
17106
+ relativePath: path9.basename(binaryPath),
17107
+ absolutePath: binaryPath,
17108
+ displayPath: tildify(binaryPath),
17109
+ kind: getPathKind(binaryPath),
17110
+ deleteIfEmpty: false,
17111
+ safetyStatus: "safe"
17112
+ };
17113
+ if (target.kind === "missing") {
17114
+ return { target, success: true, skipped: true, reason: "not found" };
17115
+ }
17116
+ if (process.platform === "win32") {
17117
+ logger.info(`To complete uninstall, manually delete: ${target.displayPath}`);
17118
+ return { target, success: true, skipped: true };
17119
+ }
17120
+ try {
17121
+ unlinkSync2(binaryPath);
17122
+ return { target, success: true, skipped: false };
17123
+ } catch (error) {
17124
+ const err = error instanceof Error ? error : new Error(String(error));
17125
+ return { target, success: false, skipped: false, reason: "permission denied", error: err };
17126
+ }
17127
+ }
17128
+ function printDryRun(configTargets, binaryTarget, installMethod) {
17129
+ logger.info(`Dry run - the following would be removed:
17130
+ `);
17131
+ const existingConfigTargets = configTargets.filter((t2) => t2.kind !== "missing");
17132
+ for (const target of existingConfigTargets) {
17133
+ const kindLabel = target.kind === "directory" ? "[dir] " : "[file]";
17134
+ const emptyNote = target.deleteIfEmpty ? " (if empty)" : "";
17135
+ logger.log(` ${kindLabel} ${highlight.path(target.displayPath)}${emptyNote}`);
17136
+ }
17137
+ if (binaryTarget && binaryTarget.kind !== "missing") {
17138
+ logger.log(` [bin] ${highlight.path(binaryTarget.displayPath)}`);
17139
+ }
17140
+ if (isPackageManaged(installMethod)) {
17141
+ logger.log("");
17142
+ logger.info(`Binary managed by ${installMethod}. Run:`);
17143
+ logger.log(` ${highlight.command(getPackageManagerCommand(installMethod))}`);
17144
+ }
17145
+ if (existingConfigTargets.length === 0 && (!binaryTarget || binaryTarget.kind === "missing")) {
17146
+ logger.info("Nothing to remove.");
17147
+ }
17148
+ }
17149
+ function printRemovalPlan(configTargets, binaryTarget) {
17150
+ const existingConfigTargets = configTargets.filter((t2) => t2.kind !== "missing");
17151
+ if (existingConfigTargets.length > 0 || binaryTarget && binaryTarget.kind !== "missing") {
17152
+ logger.info("Removing OCX files...");
17153
+ }
17154
+ }
17155
+ function printResults(results, binaryResult, installMethod) {
17156
+ logger.break();
17157
+ for (const result of results) {
17158
+ if (result.skipped) {
17159
+ if (result.reason === "not found") {
17160
+ continue;
17161
+ }
17162
+ if (result.reason === "not empty") {
17163
+ logger.info(`Kept ${highlight.path(result.target.displayPath)} (not empty)`);
17164
+ continue;
17165
+ }
17166
+ if (result.reason === "permission denied") {
17167
+ logger.warn(`Skipped ${highlight.path(result.target.displayPath)} (permission denied)`);
17168
+ continue;
17169
+ }
17170
+ if (result.reason === "containment violation") {
17171
+ logger.warn(`Skipped ${highlight.path(result.target.displayPath)} (containment violation)`);
17172
+ continue;
17173
+ }
17174
+ }
17175
+ if (result.success) {
17176
+ logger.success(`Removed ${highlight.path(result.target.displayPath)}`);
17177
+ } else {
17178
+ logger.error(`Failed to remove ${result.target.displayPath}: ${result.error?.message}`);
17179
+ }
17180
+ }
17181
+ if (binaryResult) {
17182
+ if (binaryResult.skipped && binaryResult.reason === "not found") {} else if (binaryResult.success && !binaryResult.skipped) {
17183
+ logger.success(`Removed binary ${highlight.path(binaryResult.target.displayPath)}`);
17184
+ } else if (!binaryResult.success) {
17185
+ logger.error(`Failed to remove binary ${binaryResult.target.displayPath}: ${binaryResult.error?.message}`);
17186
+ }
17187
+ }
17188
+ if (isPackageManaged(installMethod)) {
17189
+ logger.break();
17190
+ logger.info(`Binary is managed by ${installMethod}. To complete uninstall, run:`);
17191
+ logger.log(` ${highlight.command(getPackageManagerCommand(installMethod))}`);
17192
+ }
17193
+ }
17194
+ function printNothingToRemove() {
17195
+ logger.info("Nothing to remove. OCX is not installed globally.");
17196
+ }
17197
+ async function runUninstall(options2) {
17198
+ const rootPath = getGlobalConfigRoot();
17199
+ const rootValidation = validateRootDirectory(rootPath);
17200
+ if (!rootValidation.valid) {
17201
+ switch (rootValidation.reason) {
17202
+ case "not-found":
17203
+ break;
17204
+ case "symlink":
17205
+ logger.error("Safety error: Global config root is a symlink. Aborting.");
17206
+ process.exit(UNINSTALL_EXIT_CODES.SAFETY_ERROR);
17207
+ break;
17208
+ case "not-directory":
17209
+ logger.error("Safety error: Global config root is not a directory. Aborting.");
17210
+ process.exit(UNINSTALL_EXIT_CODES.SAFETY_ERROR);
17211
+ break;
17212
+ case "permission":
17213
+ logger.error("Error: Cannot access global config root (permission denied).");
17214
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17215
+ break;
17216
+ }
17217
+ }
17218
+ const configTargets = buildConfigTargets();
17219
+ const forbiddenTargets = configTargets.filter((t2) => t2.safetyStatus === "forbidden");
17220
+ if (forbiddenTargets.length > 0) {
17221
+ logger.error("Safety error: Target escapes containment boundary:");
17222
+ for (const target of forbiddenTargets) {
17223
+ logger.error(` ${target.displayPath}`);
17224
+ }
17225
+ process.exit(UNINSTALL_EXIT_CODES.SAFETY_ERROR);
17226
+ }
17227
+ const errorTargets = configTargets.filter((t2) => t2.safetyStatus === "error");
17228
+ if (errorTargets.length > 0) {
17229
+ logger.error("Error: Cannot verify containment for targets (permission/IO error):");
17230
+ for (const target of errorTargets) {
17231
+ logger.error(` ${target.displayPath}`);
17232
+ }
17233
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17234
+ }
17235
+ const installMethod = detectInstallMethod();
17236
+ const binaryTarget = buildBinaryTarget();
17237
+ const existingConfigTargets = configTargets.filter((t2) => t2.kind !== "missing");
17238
+ const hasBinary = binaryTarget && binaryTarget.kind !== "missing";
17239
+ const hasPackageManager = isPackageManaged(installMethod);
17240
+ if (existingConfigTargets.length === 0 && !hasBinary && !hasPackageManager) {
17241
+ printNothingToRemove();
17242
+ process.exit(UNINSTALL_EXIT_CODES.SUCCESS);
17243
+ }
17244
+ if (options2.dryRun) {
17245
+ printDryRun(configTargets, binaryTarget, installMethod);
17246
+ process.exit(UNINSTALL_EXIT_CODES.SUCCESS);
17247
+ }
17248
+ printRemovalPlan(configTargets, binaryTarget);
17249
+ const configResults = executeRemovals(configTargets);
17250
+ let binaryResult = null;
17251
+ if (binaryTarget) {
17252
+ binaryResult = removeBinary(binaryTarget.absolutePath);
17253
+ }
17254
+ printResults(configResults, binaryResult, installMethod);
17255
+ const hasFailures = configResults.some((r2) => !r2.success && !r2.skipped);
17256
+ const binaryFailed = binaryResult && !binaryResult.success && !binaryResult.skipped;
17257
+ if (hasFailures || binaryFailed) {
17258
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17259
+ }
17260
+ if (isPackageManaged(installMethod)) {
17261
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17262
+ }
17263
+ process.exit(UNINSTALL_EXIT_CODES.SUCCESS);
17264
+ }
17265
+ function registerSelfUninstallCommand(parent) {
17266
+ parent.command("uninstall").description("Remove OCX global configuration and binary").option("--dry-run", "Preview what would be removed").action(wrapAction(async (options2) => {
17267
+ await runUninstall(options2);
17268
+ }));
17269
+ }
17270
+
16372
17271
  // src/self-update/version-provider.ts
16373
17272
  class BuildTimeVersionProvider {
16374
- version = "1.4.1";
17273
+ version = "1.4.3";
16375
17274
  }
16376
17275
  var defaultVersionProvider = new BuildTimeVersionProvider;
16377
17276
 
@@ -16434,57 +17333,8 @@ async function checkForUpdate(versionProvider, timeoutMs = VERSION_CHECK_TIMEOUT
16434
17333
  }
16435
17334
  }
16436
17335
 
16437
- // src/self-update/detect-method.ts
16438
- function parseInstallMethod(input) {
16439
- const VALID_METHODS = ["curl", "npm", "yarn", "pnpm", "bun"];
16440
- const method = VALID_METHODS.find((m) => m === input);
16441
- if (!method) {
16442
- throw new SelfUpdateError(`Invalid install method: "${input}"
16443
- Valid methods: ${VALID_METHODS.join(", ")}`);
16444
- }
16445
- return method;
16446
- }
16447
- var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
16448
- var isTempExecution = (path8) => path8.includes("/_npx/") || path8.includes("/.cache/bunx/") || path8.includes("/.pnpm/_temp/");
16449
- var isYarnGlobalInstall = (path8) => path8.includes("/.yarn/global") || path8.includes("/.config/yarn/global");
16450
- var isPnpmGlobalInstall = (path8) => path8.includes("/.pnpm/") || path8.includes("/pnpm/global");
16451
- var isBunGlobalInstall = (path8) => path8.includes("/.bun/bin") || path8.includes("/.bun/install/global");
16452
- var isNpmGlobalInstall = (path8) => path8.includes("/.npm/") || path8.includes("/node_modules/");
16453
- function detectInstallMethod() {
16454
- if (isCompiledBinary()) {
16455
- return "curl";
16456
- }
16457
- const scriptPath = process.argv[1] ?? "";
16458
- if (isTempExecution(scriptPath))
16459
- return "unknown";
16460
- if (isYarnGlobalInstall(scriptPath))
16461
- return "yarn";
16462
- if (isPnpmGlobalInstall(scriptPath))
16463
- return "pnpm";
16464
- if (isBunGlobalInstall(scriptPath))
16465
- return "bun";
16466
- if (isNpmGlobalInstall(scriptPath))
16467
- return "npm";
16468
- const userAgent = process.env.npm_config_user_agent ?? "";
16469
- if (userAgent.includes("yarn"))
16470
- return "yarn";
16471
- if (userAgent.includes("pnpm"))
16472
- return "pnpm";
16473
- if (userAgent.includes("bun"))
16474
- return "bun";
16475
- if (userAgent.includes("npm"))
16476
- return "npm";
16477
- return "unknown";
16478
- }
16479
- function getExecutablePath() {
16480
- if (typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/")) {
16481
- return process.execPath;
16482
- }
16483
- return process.argv[1] ?? process.execPath;
16484
- }
16485
-
16486
17336
  // src/self-update/download.ts
16487
- import { chmodSync, existsSync as existsSync10, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
17337
+ import { chmodSync, existsSync as existsSync12, renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
16488
17338
  var GITHUB_REPO2 = "kdcokenny/ocx";
16489
17339
  var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
16490
17340
  var PLATFORM_MAP = {
@@ -16562,8 +17412,8 @@ async function downloadToTemp(version) {
16562
17412
  try {
16563
17413
  chmodSync(tempPath, 493);
16564
17414
  } catch (error) {
16565
- if (existsSync10(tempPath)) {
16566
- unlinkSync2(tempPath);
17415
+ if (existsSync12(tempPath)) {
17416
+ unlinkSync3(tempPath);
16567
17417
  }
16568
17418
  throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
16569
17419
  }
@@ -16572,31 +17422,31 @@ async function downloadToTemp(version) {
16572
17422
  function atomicReplace(tempPath, execPath) {
16573
17423
  const backupPath = `${execPath}.backup`;
16574
17424
  try {
16575
- if (existsSync10(execPath)) {
17425
+ if (existsSync12(execPath)) {
16576
17426
  renameSync2(execPath, backupPath);
16577
17427
  }
16578
17428
  renameSync2(tempPath, execPath);
16579
- if (existsSync10(backupPath)) {
16580
- unlinkSync2(backupPath);
17429
+ if (existsSync12(backupPath)) {
17430
+ unlinkSync3(backupPath);
16581
17431
  }
16582
17432
  } catch (error) {
16583
- if (existsSync10(backupPath) && !existsSync10(execPath)) {
17433
+ if (existsSync12(backupPath) && !existsSync12(execPath)) {
16584
17434
  try {
16585
17435
  renameSync2(backupPath, execPath);
16586
17436
  } catch {}
16587
17437
  }
16588
- if (existsSync10(tempPath)) {
17438
+ if (existsSync12(tempPath)) {
16589
17439
  try {
16590
- unlinkSync2(tempPath);
17440
+ unlinkSync3(tempPath);
16591
17441
  } catch {}
16592
17442
  }
16593
17443
  throw new SelfUpdateError(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
16594
17444
  }
16595
17445
  }
16596
17446
  function cleanupTempFile(tempPath) {
16597
- if (existsSync10(tempPath)) {
17447
+ if (existsSync12(tempPath)) {
16598
17448
  try {
16599
- unlinkSync2(tempPath);
17449
+ unlinkSync3(tempPath);
16600
17450
  } catch {}
16601
17451
  }
16602
17452
  }
@@ -16765,14 +17615,15 @@ function registerSelfUpdateCommand(parent) {
16765
17615
  // src/commands/self/index.ts
16766
17616
  function registerSelfCommand(program2) {
16767
17617
  const self = program2.command("self").description("Manage the OCX CLI");
17618
+ registerSelfUninstallCommand(self);
16768
17619
  registerSelfUpdateCommand(self);
16769
17620
  }
16770
17621
 
16771
17622
  // src/commands/update.ts
16772
17623
  import { createHash as createHash4 } from "crypto";
16773
- import { existsSync as existsSync11 } from "fs";
17624
+ import { existsSync as existsSync13 } from "fs";
16774
17625
  import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
16775
- import { dirname as dirname5, join as join9 } from "path";
17626
+ import { dirname as dirname6, join as join10 } from "path";
16776
17627
  function registerUpdateCommand(program2) {
16777
17628
  program2.command("update [components...]").description("Update installed components (use @version suffix to pin, e.g., kdco/agents@1.2.0)").option("--all", "Update all installed components").option("--registry <name>", "Update all components from a specific registry").option("--dry-run", "Preview changes without applying").option("--cwd <path>", "Working directory", process.cwd()).option("-q, --quiet", "Suppress output").option("-v, --verbose", "Verbose output").option("--json", "Output as JSON").action(async (components, options2) => {
16778
17629
  try {
@@ -16910,9 +17761,9 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
16910
17761
  const fileObj = update.component.files.find((f) => f.path === file.path);
16911
17762
  if (!fileObj)
16912
17763
  continue;
16913
- const targetPath = join9(provider.cwd, fileObj.target);
16914
- const targetDir = dirname5(targetPath);
16915
- if (!existsSync11(targetDir)) {
17764
+ const targetPath = join10(provider.cwd, fileObj.target);
17765
+ const targetDir = dirname6(targetPath);
17766
+ if (!existsSync13(targetDir)) {
16916
17767
  await mkdir9(targetDir, { recursive: true });
16917
17768
  }
16918
17769
  await writeFile4(targetPath, file.content);
@@ -17061,8 +17912,8 @@ function shouldCheckForUpdate() {
17061
17912
  return true;
17062
17913
  }
17063
17914
  function registerUpdateCheckHook(program2) {
17064
- program2.hook("postAction", async (thisCommand) => {
17065
- if (thisCommand.name() === "update" && thisCommand.parent?.name() === "self") {
17915
+ program2.hook("postAction", async (_thisCommand, actionCommand) => {
17916
+ if (actionCommand.name() === "update" && actionCommand.parent?.name() === "self") {
17066
17917
  return;
17067
17918
  }
17068
17919
  if (!shouldCheckForUpdate())
@@ -17076,7 +17927,7 @@ function registerUpdateCheckHook(program2) {
17076
17927
  });
17077
17928
  }
17078
17929
  // src/index.ts
17079
- var version = "1.4.1";
17930
+ var version = "1.4.3";
17080
17931
  async function main2() {
17081
17932
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
17082
17933
  registerInitCommand(program2);
@@ -17108,4 +17959,4 @@ export {
17108
17959
  buildRegistry
17109
17960
  };
17110
17961
 
17111
- //# debugId=4E10E39074E11B3A64756E2164756E21
17962
+ //# debugId=947205F975538A0764756E2164756E21