ocx 1.4.2 → 1.4.4

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 = {
@@ -9847,9 +9847,15 @@ class NotFoundError extends OCXError {
9847
9847
  }
9848
9848
 
9849
9849
  class NetworkError extends OCXError {
9850
- constructor(message) {
9850
+ url;
9851
+ status;
9852
+ statusText;
9853
+ constructor(message, options) {
9851
9854
  super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
9852
9855
  this.name = "NetworkError";
9856
+ this.url = options?.url;
9857
+ this.status = options?.status;
9858
+ this.statusText = options?.statusText;
9853
9859
  }
9854
9860
  }
9855
9861
 
@@ -9875,6 +9881,9 @@ class ConflictError extends OCXError {
9875
9881
  }
9876
9882
 
9877
9883
  class IntegrityError extends OCXError {
9884
+ component;
9885
+ expected;
9886
+ found;
9878
9887
  constructor(component, expected, found) {
9879
9888
  const message = `Integrity verification failed for "${component}"
9880
9889
  ` + ` Expected: ${expected}
@@ -9883,6 +9892,9 @@ class IntegrityError extends OCXError {
9883
9892
  ` + `The registry content has changed since this component was locked.
9884
9893
  ` + `Use 'ocx update ${component}' to intentionally update this component.`;
9885
9894
  super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY);
9895
+ this.component = component;
9896
+ this.expected = expected;
9897
+ this.found = found;
9886
9898
  this.name = "IntegrityError";
9887
9899
  }
9888
9900
  }
@@ -9902,15 +9914,20 @@ class OcxConfigError extends OCXError {
9902
9914
  }
9903
9915
 
9904
9916
  class ProfileNotFoundError extends OCXError {
9905
- constructor(name) {
9906
- 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;
9907
9921
  this.name = "ProfileNotFoundError";
9908
9922
  }
9909
9923
  }
9910
9924
 
9911
9925
  class ProfileExistsError extends OCXError {
9912
- constructor(name) {
9913
- super(`Profile "${name}" already exists. Use --force to overwrite.`, "CONFLICT", EXIT_CODES.CONFLICT);
9926
+ profile;
9927
+ constructor(profile, hint) {
9928
+ const message = hint ? `Profile "${profile}" already exists. ${hint}` : `Profile "${profile}" already exists.`;
9929
+ super(message, "CONFLICT", EXIT_CODES.CONFLICT);
9930
+ this.profile = profile;
9914
9931
  this.name = "ProfileExistsError";
9915
9932
  }
9916
9933
  }
@@ -9937,8 +9954,12 @@ class RegistryExistsError extends OCXError {
9937
9954
  }
9938
9955
 
9939
9956
  class InvalidProfileNameError extends OCXError {
9940
- constructor(name, reason) {
9941
- super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
9957
+ profile;
9958
+ reason;
9959
+ constructor(profile, reason) {
9960
+ super(`Invalid profile name "${profile}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
9961
+ this.profile = profile;
9962
+ this.reason = reason;
9942
9963
  this.name = "InvalidProfileNameError";
9943
9964
  }
9944
9965
  }
@@ -9950,6 +9971,68 @@ class ProfilesNotInitializedError extends OCXError {
9950
9971
  }
9951
9972
  }
9952
9973
 
9974
+ // src/utils/path-security.ts
9975
+ import * as path from "path";
9976
+
9977
+ class PathValidationError extends Error {
9978
+ attemptedPath;
9979
+ reason;
9980
+ constructor(message, attemptedPath, reason) {
9981
+ super(message);
9982
+ this.attemptedPath = attemptedPath;
9983
+ this.reason = reason;
9984
+ this.name = "PathValidationError";
9985
+ }
9986
+ }
9987
+ var WINDOWS_RESERVED = new Set([
9988
+ "CON",
9989
+ "PRN",
9990
+ "AUX",
9991
+ "NUL",
9992
+ "COM1",
9993
+ "COM2",
9994
+ "COM3",
9995
+ "COM4",
9996
+ "COM5",
9997
+ "COM6",
9998
+ "COM7",
9999
+ "COM8",
10000
+ "COM9",
10001
+ "LPT1",
10002
+ "LPT2",
10003
+ "LPT3",
10004
+ "LPT4",
10005
+ "LPT5",
10006
+ "LPT6",
10007
+ "LPT7",
10008
+ "LPT8",
10009
+ "LPT9"
10010
+ ]);
10011
+ function validatePath(basePath, userPath) {
10012
+ if (userPath.includes("\x00")) {
10013
+ throw new PathValidationError("Path contains null bytes", userPath, "null_byte");
10014
+ }
10015
+ if (path.isAbsolute(userPath) || path.win32.isAbsolute(userPath)) {
10016
+ throw new PathValidationError("Path must be relative", userPath, "absolute_path");
10017
+ }
10018
+ if (/^[a-zA-Z]:/.test(userPath) || userPath.startsWith("\\\\")) {
10019
+ throw new PathValidationError("Path contains Windows absolute", userPath, "windows_absolute");
10020
+ }
10021
+ const baseName = path.basename(userPath).toUpperCase().split(".")[0] ?? "";
10022
+ if (WINDOWS_RESERVED.has(baseName)) {
10023
+ throw new PathValidationError("Path uses Windows reserved name", userPath, "windows_reserved");
10024
+ }
10025
+ const normalized = userPath.normalize("NFC");
10026
+ const unified = normalized.replace(/\\/g, "/");
10027
+ const resolvedBase = path.resolve(basePath);
10028
+ const resolvedCombined = path.resolve(resolvedBase, unified);
10029
+ const relativePath = path.relative(resolvedBase, resolvedCombined);
10030
+ if (relativePath.startsWith("../") || relativePath.startsWith("..\\") || relativePath === ".." || path.isAbsolute(relativePath)) {
10031
+ throw new PathValidationError("Path escapes base directory", userPath, "path_traversal");
10032
+ }
10033
+ return resolvedCombined;
10034
+ }
10035
+
9953
10036
  // src/schemas/registry.ts
9954
10037
  var npmSpecifierSchema = exports_external.string().refine((val) => val.startsWith("npm:"), {
9955
10038
  message: 'npm specifier must start with "npm:" prefix'
@@ -9999,11 +10082,11 @@ var componentTypeSchema = exports_external.enum([
9999
10082
  "ocx:bundle",
10000
10083
  "ocx:profile"
10001
10084
  ]);
10002
- var profileTargetPathSchema = exports_external.enum(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
10003
- var targetPathSchema = exports_external.string().refine((path) => path.startsWith(".opencode/"), {
10085
+ var PROFILE_RESERVED_TARGETS = new Set(["ocx.lock", ".opencode"]);
10086
+ var targetPathSchema = exports_external.string().refine((path2) => path2.startsWith(".opencode/"), {
10004
10087
  message: 'Target path must start with ".opencode/"'
10005
- }).refine((path) => {
10006
- const parts = path.split("/");
10088
+ }).refine((path2) => {
10089
+ const parts = path2.split("/");
10007
10090
  const dir = parts[1];
10008
10091
  if (!dir)
10009
10092
  return false;
@@ -10132,7 +10215,7 @@ var componentManifestSchema = exports_external.object({
10132
10215
  opencode: opencodeConfigSchema.optional()
10133
10216
  });
10134
10217
  function validateSafePath(filePath) {
10135
- if (isAbsolute(filePath)) {
10218
+ if (isAbsolute2(filePath)) {
10136
10219
  throw new ValidationError(`Invalid path: "${filePath}" - absolute paths not allowed`);
10137
10220
  }
10138
10221
  if (filePath.startsWith("~")) {
@@ -10149,17 +10232,18 @@ function inferTargetPath(sourcePath) {
10149
10232
  function validateFileTarget(target, componentType) {
10150
10233
  const isProfile = componentType === "ocx:profile";
10151
10234
  if (isProfile) {
10152
- const isProfileFile = profileTargetPathSchema.safeParse(target).success;
10153
- const isOpencodeTarget = target.startsWith(".opencode/");
10154
- if (!isProfileFile && !isOpencodeTarget) {
10155
- throw new ValidationError(`Invalid profile target: "${target}". ` + `Must be a profile file (ocx.jsonc, opencode.jsonc, AGENTS.md) or start with ".opencode/"`);
10235
+ if (PROFILE_RESERVED_TARGETS.has(target)) {
10236
+ throw new ValidationError(`Target "${target}" is reserved for installer use`);
10156
10237
  }
10157
- if (isOpencodeTarget) {
10158
- const parseResult = targetPathSchema.safeParse(target);
10159
- if (!parseResult.success) {
10160
- throw new ValidationError(`Invalid embedded target: "${target}". ${parseResult.error.errors[0]?.message}`);
10238
+ try {
10239
+ validatePath("/dummy/base", target);
10240
+ } catch (error) {
10241
+ if (error instanceof PathValidationError) {
10242
+ throw new ValidationError(`Invalid profile target "${target}": ${error.message}`);
10161
10243
  }
10244
+ throw error;
10162
10245
  }
10246
+ return;
10163
10247
  } else {
10164
10248
  const parseResult = targetPathSchema.safeParse(target);
10165
10249
  if (!parseResult.success) {
@@ -10283,16 +10367,24 @@ var installedComponentSchema = exports_external.object({
10283
10367
  installedAt: exports_external.string(),
10284
10368
  updatedAt: exports_external.string().optional()
10285
10369
  });
10370
+ var installedFromSchema = exports_external.object({
10371
+ registry: exports_external.string(),
10372
+ component: exports_external.string(),
10373
+ version: exports_external.string().optional(),
10374
+ hash: exports_external.string(),
10375
+ installedAt: exports_external.string()
10376
+ });
10286
10377
  var ocxLockSchema = exports_external.object({
10287
10378
  lockVersion: exports_external.literal(1),
10379
+ installedFrom: installedFromSchema.optional(),
10288
10380
  installed: exports_external.record(qualifiedComponentSchema, installedComponentSchema).default({})
10289
10381
  });
10290
10382
  var CONFIG_FILE = "ocx.jsonc";
10291
10383
  var LOCK_FILE = "ocx.lock";
10292
10384
  var LOCAL_CONFIG_DIR = ".opencode";
10293
10385
  function findOcxConfig(cwd) {
10294
- const dotOpencodePath = path.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10295
- const rootPath = path.join(cwd, CONFIG_FILE);
10386
+ const dotOpencodePath = path2.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10387
+ const rootPath = path2.join(cwd, CONFIG_FILE);
10296
10388
  const dotOpencodeExists = existsSync(dotOpencodePath);
10297
10389
  const rootExists = existsSync(rootPath);
10298
10390
  if (dotOpencodeExists && rootExists) {
@@ -10307,8 +10399,8 @@ function findOcxConfig(cwd) {
10307
10399
  return { path: dotOpencodePath, exists: false };
10308
10400
  }
10309
10401
  function findOcxLock(cwd, options) {
10310
- const dotOpencodePath = path.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10311
- const rootPath = path.join(cwd, LOCK_FILE);
10402
+ const dotOpencodePath = path2.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10403
+ const rootPath = path2.join(cwd, LOCK_FILE);
10312
10404
  if (options?.isFlattened) {
10313
10405
  if (existsSync(rootPath)) {
10314
10406
  return { path: rootPath, exists: true };
@@ -10339,8 +10431,8 @@ async function readOcxConfig(cwd) {
10339
10431
  }
10340
10432
  }
10341
10433
  async function writeOcxConfig(cwd, config, existingPath) {
10342
- const configPath = existingPath ?? path.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10343
- await mkdir(path.dirname(configPath), { recursive: true });
10434
+ const configPath = existingPath ?? path2.join(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE);
10435
+ await mkdir(path2.dirname(configPath), { recursive: true });
10344
10436
  const content = JSON.stringify(config, null, 2);
10345
10437
  await Bun.write(configPath, content);
10346
10438
  }
@@ -10355,8 +10447,8 @@ async function readOcxLock(cwd, options) {
10355
10447
  return ocxLockSchema.parse(json);
10356
10448
  }
10357
10449
  async function writeOcxLock(cwd, lock, existingPath) {
10358
- const lockPath = existingPath ?? path.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10359
- await mkdir(path.dirname(lockPath), { recursive: true });
10450
+ const lockPath = existingPath ?? path2.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
10451
+ await mkdir(path2.dirname(lockPath), { recursive: true });
10360
10452
  const content = JSON.stringify(lock, null, 2);
10361
10453
  await Bun.write(lockPath, content);
10362
10454
  }
@@ -10364,10 +10456,10 @@ async function writeOcxLock(cwd, lock, existingPath) {
10364
10456
  // src/utils/paths.ts
10365
10457
  import { stat } from "fs/promises";
10366
10458
  import { homedir } from "os";
10367
- import { isAbsolute as isAbsolute2, join } from "path";
10459
+ import { isAbsolute as isAbsolute3, join } from "path";
10368
10460
  function getGlobalConfigPath() {
10369
10461
  const xdg = process.env.XDG_CONFIG_HOME;
10370
- const base = xdg && isAbsolute2(xdg) ? xdg : join(homedir(), ".config");
10462
+ const base = xdg && isAbsolute3(xdg) ? xdg : join(homedir(), ".config");
10371
10463
  return join(base, "opencode");
10372
10464
  }
10373
10465
  async function globalDirectoryExists() {
@@ -10436,19 +10528,19 @@ class GlobalConfigProvider {
10436
10528
 
10437
10529
  // src/config/resolver.ts
10438
10530
  import { existsSync as existsSync3, statSync as statSync2 } from "fs";
10439
- import { join as join2, relative } from "path";
10531
+ import { join as join2, relative as relative2 } from "path";
10440
10532
  var {Glob: Glob2 } = globalThis.Bun;
10441
10533
 
10442
10534
  // src/profile/manager.ts
10443
- import { mkdir as mkdir2, readdir, rm, stat as stat2 } from "fs/promises";
10535
+ import { mkdir as mkdir2, readdir, rename as rename2, rm, stat as stat2 } from "fs/promises";
10444
10536
 
10445
10537
  // src/schemas/ocx.ts
10446
10538
  var {Glob } = globalThis.Bun;
10447
10539
 
10448
10540
  // src/utils/path-helpers.ts
10449
- import path2 from "path";
10541
+ import path3 from "path";
10450
10542
  function isAbsolutePath(p) {
10451
- return path2.posix.isAbsolute(p) || path2.win32.isAbsolute(p);
10543
+ return path3.posix.isAbsolute(p) || path3.win32.isAbsolute(p);
10452
10544
  }
10453
10545
 
10454
10546
  // src/schemas/common.ts
@@ -10470,7 +10562,6 @@ var profileOcxConfigSchema = exports_external.object({
10470
10562
  componentPath: safeRelativePathSchema.optional(),
10471
10563
  renameWindow: exports_external.boolean().default(true).describe("Set terminal/tmux window name when launching OpenCode"),
10472
10564
  exclude: exports_external.array(globPatternSchema).default([
10473
- "**/AGENTS.md",
10474
10565
  "**/CLAUDE.md",
10475
10566
  "**/CONTEXT.md",
10476
10567
  "**/.opencode/**",
@@ -10498,38 +10589,38 @@ async function atomicWrite(filePath, data) {
10498
10589
  // src/profile/paths.ts
10499
10590
  import { existsSync as existsSync2, statSync } from "fs";
10500
10591
  import { homedir as homedir2 } from "os";
10501
- import path3 from "path";
10592
+ import path4 from "path";
10502
10593
  var OCX_CONFIG_FILE = "ocx.jsonc";
10503
10594
  var OPENCODE_CONFIG_FILE = "opencode.jsonc";
10504
10595
  var LOCAL_CONFIG_DIR2 = ".opencode";
10505
10596
  function getProfilesDir() {
10506
- const base = process.env.XDG_CONFIG_HOME || path3.join(homedir2(), ".config");
10507
- return path3.join(base, "opencode", "profiles");
10597
+ const base = process.env.XDG_CONFIG_HOME || path4.join(homedir2(), ".config");
10598
+ return path4.join(base, "opencode", "profiles");
10508
10599
  }
10509
10600
  function getProfileDir(name) {
10510
- return path3.join(getProfilesDir(), name);
10601
+ return path4.join(getProfilesDir(), name);
10511
10602
  }
10512
10603
  function getProfileOcxConfig(name) {
10513
- return path3.join(getProfileDir(name), "ocx.jsonc");
10604
+ return path4.join(getProfileDir(name), "ocx.jsonc");
10514
10605
  }
10515
10606
  function getProfileOpencodeConfig(name) {
10516
- return path3.join(getProfileDir(name), "opencode.jsonc");
10607
+ return path4.join(getProfileDir(name), "opencode.jsonc");
10517
10608
  }
10518
10609
  function getProfileAgents(name) {
10519
- return path3.join(getProfileDir(name), "AGENTS.md");
10610
+ return path4.join(getProfileDir(name), "AGENTS.md");
10520
10611
  }
10521
10612
  function findLocalConfigDir(cwd) {
10522
10613
  let currentDir = cwd;
10523
10614
  while (true) {
10524
- const configDir = path3.join(currentDir, LOCAL_CONFIG_DIR2);
10615
+ const configDir = path4.join(currentDir, LOCAL_CONFIG_DIR2);
10525
10616
  if (existsSync2(configDir) && statSync(configDir).isDirectory()) {
10526
10617
  return configDir;
10527
10618
  }
10528
- const gitDir = path3.join(currentDir, ".git");
10619
+ const gitDir = path4.join(currentDir, ".git");
10529
10620
  if (existsSync2(gitDir)) {
10530
10621
  return null;
10531
10622
  }
10532
- const parentDir = path3.dirname(currentDir);
10623
+ const parentDir = path4.dirname(currentDir);
10533
10624
  if (parentDir === currentDir) {
10534
10625
  return null;
10535
10626
  }
@@ -10537,8 +10628,8 @@ function findLocalConfigDir(cwd) {
10537
10628
  }
10538
10629
  }
10539
10630
  function getGlobalConfig() {
10540
- const base = process.env.XDG_CONFIG_HOME || path3.join(homedir2(), ".config");
10541
- return path3.join(base, "opencode", "ocx.jsonc");
10631
+ const base = process.env.XDG_CONFIG_HOME || path4.join(homedir2(), ".config");
10632
+ return path4.join(base, "opencode", "ocx.jsonc");
10542
10633
  }
10543
10634
 
10544
10635
  // src/profile/schema.ts
@@ -10556,7 +10647,6 @@ var DEFAULT_OCX_CONFIG = {
10556
10647
  registries: {},
10557
10648
  renameWindow: true,
10558
10649
  exclude: [
10559
- "**/AGENTS.md",
10560
10650
  "**/CLAUDE.md",
10561
10651
  "**/CONTEXT.md",
10562
10652
  "**/.opencode/**",
@@ -10565,6 +10655,21 @@ var DEFAULT_OCX_CONFIG = {
10565
10655
  ],
10566
10656
  include: []
10567
10657
  };
10658
+ var DEFAULT_OCX_CONFIG_TEMPLATE = `{
10659
+ "$schema": "https://ocx.kdco.dev/schemas/ocx.json",
10660
+ "registries": {},
10661
+ "renameWindow": true,
10662
+ "exclude": [
10663
+ // "**/AGENTS.md",
10664
+ "**/CLAUDE.md",
10665
+ "**/CONTEXT.md",
10666
+ "**/.opencode/**",
10667
+ "**/opencode.jsonc",
10668
+ "**/opencode.json"
10669
+ ],
10670
+ "include": []
10671
+ }
10672
+ `;
10568
10673
 
10569
10674
  class ProfileManager {
10570
10675
  profilesDir;
@@ -10650,7 +10755,7 @@ class ProfileManager {
10650
10755
  const ocxPath = getProfileOcxConfig(name);
10651
10756
  const ocxFile = Bun.file(ocxPath);
10652
10757
  if (!await ocxFile.exists()) {
10653
- await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
10758
+ await Bun.write(ocxPath, DEFAULT_OCX_CONFIG_TEMPLATE, { mode: 384 });
10654
10759
  }
10655
10760
  const opencodePath = getProfileOpencodeConfig(name);
10656
10761
  const opencodeFile = Bun.file(opencodePath);
@@ -10679,6 +10784,44 @@ class ProfileManager {
10679
10784
  const dir = getProfileDir(name);
10680
10785
  await rm(dir, { recursive: true });
10681
10786
  }
10787
+ async move(oldName, newName) {
10788
+ const oldResult = profileNameSchema.safeParse(oldName);
10789
+ if (!oldResult.success) {
10790
+ throw new InvalidProfileNameError(oldName, oldResult.error.errors[0]?.message ?? "Invalid name");
10791
+ }
10792
+ const newResult = profileNameSchema.safeParse(newName);
10793
+ if (!newResult.success) {
10794
+ throw new InvalidProfileNameError(newName, newResult.error.errors[0]?.message ?? "Invalid name");
10795
+ }
10796
+ await this.ensureInitialized();
10797
+ if (!await this.exists(oldName)) {
10798
+ throw new ProfileNotFoundError(oldName);
10799
+ }
10800
+ if (oldName === newName) {
10801
+ return { warnActiveProfile: false };
10802
+ }
10803
+ if (await this.exists(newName)) {
10804
+ throw new ConflictError(`Cannot move: profile "${newName}" already exists. Remove it first with 'ocx p rm ${newName}'.`);
10805
+ }
10806
+ const warnActiveProfile = process.env.OCX_PROFILE === oldName;
10807
+ const oldDir = getProfileDir(oldName);
10808
+ const newDir = getProfileDir(newName);
10809
+ try {
10810
+ await rename2(oldDir, newDir);
10811
+ } catch (error) {
10812
+ if (error instanceof Error && "code" in error) {
10813
+ const code = error.code;
10814
+ if (code === "EEXIST" || code === "ENOTEMPTY") {
10815
+ throw new ConflictError(`Cannot move: profile "${newName}" already exists. Remove it first with 'ocx p rm ${newName}'.`);
10816
+ }
10817
+ if (code === "ENOENT") {
10818
+ throw new ProfileNotFoundError(oldName);
10819
+ }
10820
+ }
10821
+ throw error;
10822
+ }
10823
+ return { warnActiveProfile };
10824
+ }
10682
10825
  async resolveProfile(override) {
10683
10826
  if (override) {
10684
10827
  if (!await this.exists(override)) {
@@ -10728,7 +10871,7 @@ function discoverInstructionFiles(projectDir, gitRoot) {
10728
10871
  for (const filename of INSTRUCTION_FILES) {
10729
10872
  const filePath = join2(currentDir, filename);
10730
10873
  if (existsSync3(filePath) && statSync2(filePath).isFile()) {
10731
- const relativePath = relative(root, filePath);
10874
+ const relativePath = relative2(root, filePath);
10732
10875
  discovered.push(relativePath);
10733
10876
  }
10734
10877
  }
@@ -10889,7 +11032,7 @@ class ConfigResolver {
10889
11032
  return true;
10890
11033
  const gitRoot = detectGitRoot(this.cwd);
10891
11034
  const root = gitRoot ?? this.cwd;
10892
- const relativePath = relative(root, this.localConfigDir);
11035
+ const relativePath = relative2(root, this.localConfigDir);
10893
11036
  const exclude = this.profile.ocx.exclude ?? [];
10894
11037
  const include = this.profile.ocx.include ?? [];
10895
11038
  for (const pattern of include) {
@@ -10982,7 +11125,7 @@ class ConfigResolver {
10982
11125
  // package.json
10983
11126
  var package_default = {
10984
11127
  name: "ocx",
10985
- version: "1.4.2",
11128
+ version: "1.4.4",
10986
11129
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10987
11130
  author: "kdcokenny",
10988
11131
  license: "MIT",
@@ -11061,14 +11204,28 @@ async function fetchWithCache(url, parse3) {
11061
11204
  return cached;
11062
11205
  }
11063
11206
  const promise = (async () => {
11064
- const response = await fetch(url);
11207
+ let response;
11208
+ try {
11209
+ response = await fetch(url);
11210
+ } catch (error) {
11211
+ throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
11212
+ }
11065
11213
  if (!response.ok) {
11066
11214
  if (response.status === 404) {
11067
11215
  throw new NotFoundError(`Not found: ${url}`);
11068
11216
  }
11069
- throw new NetworkError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
11217
+ throw new NetworkError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`, {
11218
+ url,
11219
+ status: response.status,
11220
+ statusText: response.statusText
11221
+ });
11222
+ }
11223
+ let data;
11224
+ try {
11225
+ data = await response.json();
11226
+ } catch (error) {
11227
+ throw new NetworkError(`Invalid JSON response from ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
11070
11228
  }
11071
- const data = await response.json();
11072
11229
  return parse3(data);
11073
11230
  })();
11074
11231
  cache.set(url, promise);
@@ -11115,9 +11272,14 @@ async function fetchComponentVersion(baseUrl, name, version) {
11115
11272
  }
11116
11273
  async function fetchFileContent(baseUrl, componentName, filePath) {
11117
11274
  const url = `${baseUrl.replace(/\/$/, "")}/components/${componentName}/${filePath}`;
11118
- const response = await fetch(url);
11275
+ let response;
11276
+ try {
11277
+ response = await fetch(url);
11278
+ } catch (error) {
11279
+ throw new NetworkError(`Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, { url });
11280
+ }
11119
11281
  if (!response.ok) {
11120
- throw new NetworkError(`Failed to fetch file ${filePath} for ${componentName}: ${response.status} ${response.statusText}`);
11282
+ throw new NetworkError(`Failed to fetch file ${filePath} for ${componentName} from ${url}: ${response.status} ${response.statusText}`, { url, status: response.status, statusText: response.statusText });
11121
11283
  }
11122
11284
  return response.text();
11123
11285
  }
@@ -11192,13 +11354,13 @@ async function resolveDependencies(registries, componentNames) {
11192
11354
  const npmDeps = new Set;
11193
11355
  const npmDevDeps = new Set;
11194
11356
  let opencode = {};
11195
- async function resolve(componentNamespace, componentName, path4 = []) {
11357
+ async function resolve2(componentNamespace, componentName, path5 = []) {
11196
11358
  const qualifiedName = createQualifiedComponent(componentNamespace, componentName);
11197
11359
  if (resolved.has(qualifiedName)) {
11198
11360
  return;
11199
11361
  }
11200
11362
  if (visiting.has(qualifiedName)) {
11201
- const cycle = [...path4, qualifiedName].join(" \u2192 ");
11363
+ const cycle = [...path5, qualifiedName].join(" \u2192 ");
11202
11364
  throw new ValidationError(`Circular dependency detected: ${cycle}`);
11203
11365
  }
11204
11366
  visiting.add(qualifiedName);
@@ -11209,12 +11371,21 @@ async function resolveDependencies(registries, componentNames) {
11209
11371
  let component;
11210
11372
  try {
11211
11373
  component = await fetchComponent(regConfig.url, componentName);
11212
- } catch (_err) {
11213
- throw new OCXError(`Component '${componentName}' not found in registry '${componentNamespace}'.`, "NOT_FOUND");
11374
+ } catch (err) {
11375
+ if (err instanceof NetworkError) {
11376
+ throw err;
11377
+ }
11378
+ if (err instanceof NotFoundError) {
11379
+ throw new NotFoundError(`Component '${componentName}' not found in registry '${componentNamespace}'.`);
11380
+ }
11381
+ if (err instanceof OCXError) {
11382
+ throw err;
11383
+ }
11384
+ throw new NetworkError(`Failed to fetch component '${componentName}' from registry '${componentNamespace}': ${err instanceof Error ? err.message : String(err)}`, { url: regConfig.url });
11214
11385
  }
11215
11386
  for (const dep of component.dependencies) {
11216
11387
  const depRef = parseComponentRef(dep, componentNamespace);
11217
- await resolve(depRef.namespace, depRef.component, [...path4, qualifiedName]);
11388
+ await resolve2(depRef.namespace, depRef.component, [...path5, qualifiedName]);
11218
11389
  }
11219
11390
  const normalizedComponent = normalizeComponentManifest(component);
11220
11391
  resolved.set(qualifiedName, {
@@ -11241,7 +11412,7 @@ async function resolveDependencies(registries, componentNames) {
11241
11412
  }
11242
11413
  for (const name of componentNames) {
11243
11414
  const ref = parseComponentRef(name);
11244
- await resolve(ref.namespace, ref.component);
11415
+ await resolve2(ref.namespace, ref.component);
11245
11416
  }
11246
11417
  const components = Array.from(resolved.values());
11247
11418
  const installOrder = Array.from(resolved.keys());
@@ -11258,17 +11429,17 @@ async function resolveDependencies(registries, componentNames) {
11258
11429
  import { existsSync as existsSync4 } from "fs";
11259
11430
  import { mkdir as mkdir3 } from "fs/promises";
11260
11431
  import { homedir as homedir3 } from "os";
11261
- import path4 from "path";
11432
+ import path5 from "path";
11262
11433
  var LOCAL_CONFIG_DIR3 = ".opencode";
11263
11434
  function isGlobalConfigPath(cwd) {
11264
- const base = process.env.XDG_CONFIG_HOME || path4.join(homedir3(), ".config");
11265
- const globalConfigDir = path4.resolve(base, "opencode");
11266
- const resolvedCwd = path4.resolve(cwd);
11435
+ const base = process.env.XDG_CONFIG_HOME || path5.join(homedir3(), ".config");
11436
+ const globalConfigDir = path5.resolve(base, "opencode");
11437
+ const resolvedCwd = path5.resolve(cwd);
11267
11438
  if (resolvedCwd === globalConfigDir) {
11268
11439
  return true;
11269
11440
  }
11270
- const relative2 = path4.relative(globalConfigDir, resolvedCwd);
11271
- return relative2 !== "" && !relative2.startsWith("..") && !path4.isAbsolute(relative2);
11441
+ const relative3 = path5.relative(globalConfigDir, resolvedCwd);
11442
+ return relative3 !== "" && !relative3.startsWith("..") && !path5.isAbsolute(relative3);
11272
11443
  }
11273
11444
  var JSONC_OPTIONS = {
11274
11445
  formattingOptions: {
@@ -11285,8 +11456,8 @@ var OPENCODE_CONFIG_TEMPLATE = `{
11285
11456
  `;
11286
11457
  function findOpencodeConfig(cwd) {
11287
11458
  if (isGlobalConfigPath(cwd)) {
11288
- const rootJsonc2 = path4.join(cwd, "opencode.jsonc");
11289
- const rootJson2 = path4.join(cwd, "opencode.json");
11459
+ const rootJsonc2 = path5.join(cwd, "opencode.jsonc");
11460
+ const rootJson2 = path5.join(cwd, "opencode.json");
11290
11461
  if (existsSync4(rootJsonc2)) {
11291
11462
  return { path: rootJsonc2, exists: true };
11292
11463
  }
@@ -11295,16 +11466,16 @@ function findOpencodeConfig(cwd) {
11295
11466
  }
11296
11467
  return { path: rootJsonc2, exists: false };
11297
11468
  }
11298
- const dotOpencodeJsonc = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11299
- const dotOpencodeJson = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.json");
11469
+ const dotOpencodeJsonc = path5.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11470
+ const dotOpencodeJson = path5.join(cwd, LOCAL_CONFIG_DIR3, "opencode.json");
11300
11471
  if (existsSync4(dotOpencodeJsonc)) {
11301
11472
  return { path: dotOpencodeJsonc, exists: true };
11302
11473
  }
11303
11474
  if (existsSync4(dotOpencodeJson)) {
11304
11475
  return { path: dotOpencodeJson, exists: true };
11305
11476
  }
11306
- const rootJsonc = path4.join(cwd, "opencode.jsonc");
11307
- const rootJson = path4.join(cwd, "opencode.json");
11477
+ const rootJsonc = path5.join(cwd, "opencode.jsonc");
11478
+ const rootJson = path5.join(cwd, "opencode.json");
11308
11479
  if (existsSync4(rootJsonc)) {
11309
11480
  return { path: rootJsonc, exists: true };
11310
11481
  }
@@ -11318,7 +11489,7 @@ async function ensureOpencodeConfig(cwd) {
11318
11489
  if (exists) {
11319
11490
  return { path: configPath, created: false };
11320
11491
  }
11321
- await mkdir3(path4.dirname(configPath), { recursive: true });
11492
+ await mkdir3(path5.dirname(configPath), { recursive: true });
11322
11493
  await Bun.write(configPath, OPENCODE_CONFIG_TEMPLATE);
11323
11494
  return { path: configPath, created: true };
11324
11495
  }
@@ -11335,13 +11506,13 @@ async function readOpencodeJsonConfig(cwd) {
11335
11506
  path: configPath
11336
11507
  };
11337
11508
  }
11338
- async function writeOpencodeJsonConfig(path5, content) {
11339
- await Bun.write(path5, content);
11509
+ async function writeOpencodeJsonConfig(path6, content) {
11510
+ await Bun.write(path6, content);
11340
11511
  }
11341
- function getValueAtPath(content, path5) {
11512
+ function getValueAtPath(content, path6) {
11342
11513
  const parsed = parse2(content, [], { allowTrailingComma: true });
11343
11514
  let current = parsed;
11344
- for (const segment of path5) {
11515
+ for (const segment of path6) {
11345
11516
  if (current === null || current === undefined)
11346
11517
  return;
11347
11518
  if (typeof current !== "object")
@@ -11350,27 +11521,27 @@ function getValueAtPath(content, path5) {
11350
11521
  }
11351
11522
  return current;
11352
11523
  }
11353
- function applyValueAtPath(content, path5, value) {
11524
+ function applyValueAtPath(content, path6, value) {
11354
11525
  if (value === null || value === undefined) {
11355
11526
  return content;
11356
11527
  }
11357
11528
  if (typeof value === "object" && !Array.isArray(value)) {
11358
- const existingValue = getValueAtPath(content, path5);
11529
+ const existingValue = getValueAtPath(content, path6);
11359
11530
  if (existingValue !== undefined && (existingValue === null || typeof existingValue !== "object")) {
11360
- const edits2 = modify(content, path5, value, JSONC_OPTIONS);
11531
+ const edits2 = modify(content, path6, value, JSONC_OPTIONS);
11361
11532
  return applyEdits(content, edits2);
11362
11533
  }
11363
11534
  let updatedContent = content;
11364
11535
  for (const [key, val] of Object.entries(value)) {
11365
- updatedContent = applyValueAtPath(updatedContent, [...path5, key], val);
11536
+ updatedContent = applyValueAtPath(updatedContent, [...path6, key], val);
11366
11537
  }
11367
11538
  return updatedContent;
11368
11539
  }
11369
11540
  if (Array.isArray(value)) {
11370
- const edits2 = modify(content, path5, value, JSONC_OPTIONS);
11541
+ const edits2 = modify(content, path6, value, JSONC_OPTIONS);
11371
11542
  return applyEdits(content, edits2);
11372
11543
  }
11373
- const edits = modify(content, path5, value, JSONC_OPTIONS);
11544
+ const edits = modify(content, path6, value, JSONC_OPTIONS);
11374
11545
  return applyEdits(content, edits);
11375
11546
  }
11376
11547
  async function updateOpencodeJsonConfig(cwd, opencode) {
@@ -11384,8 +11555,8 @@ async function updateOpencodeJsonConfig(cwd, opencode) {
11384
11555
  } else {
11385
11556
  const config = { $schema: "https://opencode.ai/config.json" };
11386
11557
  content = JSON.stringify(config, null, "\t");
11387
- configPath = isGlobalConfigPath(cwd) ? path4.join(cwd, "opencode.jsonc") : path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11388
- await mkdir3(path4.dirname(configPath), { recursive: true });
11558
+ configPath = isGlobalConfigPath(cwd) ? path5.join(cwd, "opencode.jsonc") : path5.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
11559
+ await mkdir3(path5.dirname(configPath), { recursive: true });
11389
11560
  created = true;
11390
11561
  }
11391
11562
  const originalContent = content;
@@ -11427,7 +11598,7 @@ function parseEnvBool(value, defaultValue) {
11427
11598
  return defaultValue;
11428
11599
  }
11429
11600
  // src/utils/git-context.ts
11430
- import { basename, resolve } from "path";
11601
+ import { basename as basename2, resolve as resolve2 } from "path";
11431
11602
  function getGitEnv() {
11432
11603
  const { GIT_DIR: _, GIT_WORK_TREE: __, ...cleanEnv } = process.env;
11433
11604
  return cleanEnv;
@@ -11493,12 +11664,100 @@ async function getRepoName(cwd) {
11493
11664
  if (!rootPath) {
11494
11665
  return null;
11495
11666
  }
11496
- return basename(rootPath);
11667
+ return basename2(rootPath);
11497
11668
  }
11498
11669
  async function getGitInfo(cwd) {
11499
11670
  const [repoName, branch] = await Promise.all([getRepoName(cwd), getBranch(cwd)]);
11500
11671
  return { repoName, branch };
11501
11672
  }
11673
+ // src/lib/build-registry.ts
11674
+ import { mkdir as mkdir4 } from "fs/promises";
11675
+ import { dirname, join as join3 } from "path";
11676
+ class BuildRegistryError extends Error {
11677
+ errors;
11678
+ constructor(message, errors2 = []) {
11679
+ super(message);
11680
+ this.errors = errors2;
11681
+ this.name = "BuildRegistryError";
11682
+ }
11683
+ }
11684
+ async function buildRegistry(options) {
11685
+ const { source: sourcePath, out: outPath } = options;
11686
+ const jsoncFile = Bun.file(join3(sourcePath, "registry.jsonc"));
11687
+ const jsonFile = Bun.file(join3(sourcePath, "registry.json"));
11688
+ const jsoncExists = await jsoncFile.exists();
11689
+ const jsonExists = await jsonFile.exists();
11690
+ if (!jsoncExists && !jsonExists) {
11691
+ throw new BuildRegistryError("No registry.jsonc or registry.json found in source directory");
11692
+ }
11693
+ const registryFile = jsoncExists ? jsoncFile : jsonFile;
11694
+ const content = await registryFile.text();
11695
+ const registryData = parse2(content, [], { allowTrailingComma: true });
11696
+ const parseResult = registrySchema.safeParse(registryData);
11697
+ if (!parseResult.success) {
11698
+ const errors2 = parseResult.error.errors.map((e3) => `${e3.path.join(".")}: ${e3.message}`);
11699
+ throw new BuildRegistryError("Registry validation failed", errors2);
11700
+ }
11701
+ const registry = parseResult.data;
11702
+ const validationErrors = [];
11703
+ const componentsDir = join3(outPath, "components");
11704
+ await mkdir4(componentsDir, { recursive: true });
11705
+ for (const component of registry.components) {
11706
+ const packument = {
11707
+ name: component.name,
11708
+ versions: {
11709
+ [registry.version]: component
11710
+ },
11711
+ "dist-tags": {
11712
+ latest: registry.version
11713
+ }
11714
+ };
11715
+ const packumentPath = join3(componentsDir, `${component.name}.json`);
11716
+ await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
11717
+ for (const rawFile of component.files) {
11718
+ const file = normalizeFile(rawFile, component.type);
11719
+ const sourceFilePath = join3(sourcePath, "files", file.path);
11720
+ const destFilePath = join3(componentsDir, component.name, file.path);
11721
+ const destFileDir = dirname(destFilePath);
11722
+ if (!await Bun.file(sourceFilePath).exists()) {
11723
+ validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`);
11724
+ continue;
11725
+ }
11726
+ await mkdir4(destFileDir, { recursive: true });
11727
+ const sourceFile = Bun.file(sourceFilePath);
11728
+ await Bun.write(destFilePath, sourceFile);
11729
+ }
11730
+ }
11731
+ if (validationErrors.length > 0) {
11732
+ throw new BuildRegistryError(`Build failed with ${validationErrors.length} errors`, validationErrors);
11733
+ }
11734
+ const index = {
11735
+ name: registry.name,
11736
+ namespace: registry.namespace,
11737
+ version: registry.version,
11738
+ author: registry.author,
11739
+ ...registry.opencode && { opencode: registry.opencode },
11740
+ ...registry.ocx && { ocx: registry.ocx },
11741
+ components: registry.components.map((c) => ({
11742
+ name: c.name,
11743
+ type: c.type,
11744
+ description: c.description
11745
+ }))
11746
+ };
11747
+ await Bun.write(join3(outPath, "index.json"), JSON.stringify(index, null, 2));
11748
+ const wellKnownDir = join3(outPath, ".well-known");
11749
+ await mkdir4(wellKnownDir, { recursive: true });
11750
+ const discovery = { registry: "/index.json" };
11751
+ await Bun.write(join3(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
11752
+ return {
11753
+ name: registry.name,
11754
+ namespace: registry.namespace,
11755
+ version: registry.version,
11756
+ componentsCount: registry.components.length,
11757
+ outputPath: outPath
11758
+ };
11759
+ }
11760
+
11502
11761
  // ../../node_modules/.bun/kleur@4.1.5/node_modules/kleur/index.mjs
11503
11762
  var FORCE_COLOR;
11504
11763
  var NODE_DISABLE_COLORS;
@@ -11636,6 +11895,15 @@ var logger = {
11636
11895
  console.log("");
11637
11896
  }
11638
11897
  };
11898
+ var highlight = {
11899
+ component: (text) => kleur_default.cyan(text),
11900
+ path: (text) => kleur_default.green(text),
11901
+ command: (text) => kleur_default.yellow(text),
11902
+ url: (text) => kleur_default.blue().underline(text),
11903
+ error: (text) => kleur_default.red(text),
11904
+ dim: (text) => kleur_default.gray(text),
11905
+ bold: (text) => kleur_default.bold(text)
11906
+ };
11639
11907
 
11640
11908
  // src/utils/handle-error.ts
11641
11909
  function handleError(error, options2 = {}) {
@@ -11651,8 +11919,8 @@ function handleError(error, options2 = {}) {
11651
11919
  if (error instanceof ZodError) {
11652
11920
  logger.error("Validation failed:");
11653
11921
  for (const issue of error.issues) {
11654
- const path5 = issue.path.join(".");
11655
- logger.error(` ${path5}: ${issue.message}`);
11922
+ const path6 = issue.path.join(".");
11923
+ logger.error(` ${path6}: ${issue.message}`);
11656
11924
  }
11657
11925
  process.exit(EXIT_CODES.CONFIG);
11658
11926
  }
@@ -11695,6 +11963,110 @@ function formatErrorAsJson(error) {
11695
11963
  }
11696
11964
  };
11697
11965
  }
11966
+ if (error instanceof IntegrityError) {
11967
+ return {
11968
+ success: false,
11969
+ error: {
11970
+ code: error.code,
11971
+ message: error.message,
11972
+ details: {
11973
+ component: error.component,
11974
+ expected: error.expected,
11975
+ found: error.found
11976
+ }
11977
+ },
11978
+ exitCode: error.exitCode,
11979
+ meta: {
11980
+ timestamp: new Date().toISOString()
11981
+ }
11982
+ };
11983
+ }
11984
+ if (error instanceof NetworkError) {
11985
+ const details = {};
11986
+ if (error.url)
11987
+ details.url = error.url;
11988
+ if (error.status !== undefined)
11989
+ details.status = error.status;
11990
+ if (error.statusText)
11991
+ details.statusText = error.statusText;
11992
+ return {
11993
+ success: false,
11994
+ error: {
11995
+ code: error.code,
11996
+ message: error.message,
11997
+ ...Object.keys(details).length > 0 && { details }
11998
+ },
11999
+ exitCode: error.exitCode,
12000
+ meta: {
12001
+ timestamp: new Date().toISOString()
12002
+ }
12003
+ };
12004
+ }
12005
+ if (error instanceof ProfileNotFoundError) {
12006
+ return {
12007
+ success: false,
12008
+ error: {
12009
+ code: error.code,
12010
+ message: error.message,
12011
+ details: {
12012
+ profile: error.profile
12013
+ }
12014
+ },
12015
+ exitCode: error.exitCode,
12016
+ meta: {
12017
+ timestamp: new Date().toISOString()
12018
+ }
12019
+ };
12020
+ }
12021
+ if (error instanceof ProfileExistsError) {
12022
+ return {
12023
+ success: false,
12024
+ error: {
12025
+ code: error.code,
12026
+ message: error.message,
12027
+ details: {
12028
+ profile: error.profile
12029
+ }
12030
+ },
12031
+ exitCode: error.exitCode,
12032
+ meta: {
12033
+ timestamp: new Date().toISOString()
12034
+ }
12035
+ };
12036
+ }
12037
+ if (error instanceof InvalidProfileNameError) {
12038
+ return {
12039
+ success: false,
12040
+ error: {
12041
+ code: error.code,
12042
+ message: error.message,
12043
+ details: {
12044
+ profile: error.profile,
12045
+ reason: error.reason
12046
+ }
12047
+ },
12048
+ exitCode: error.exitCode,
12049
+ meta: {
12050
+ timestamp: new Date().toISOString()
12051
+ }
12052
+ };
12053
+ }
12054
+ if (error instanceof BuildRegistryError) {
12055
+ return {
12056
+ success: false,
12057
+ error: {
12058
+ code: "BUILD_ERROR",
12059
+ message: error.message,
12060
+ details: {
12061
+ errors: error.errors
12062
+ }
12063
+ },
12064
+ exitCode: EXIT_CODES.GENERAL,
12065
+ meta: {
12066
+ timestamp: new Date().toISOString()
12067
+ }
12068
+ };
12069
+ }
11698
12070
  if (error instanceof OCXError) {
11699
12071
  return {
11700
12072
  success: false,
@@ -11713,7 +12085,14 @@ function formatErrorAsJson(error) {
11713
12085
  success: false,
11714
12086
  error: {
11715
12087
  code: "VALIDATION_ERROR",
11716
- message: error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
12088
+ message: error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "),
12089
+ details: {
12090
+ issues: error.issues.map((i) => ({
12091
+ path: i.path.join("."),
12092
+ message: i.message,
12093
+ code: i.code
12094
+ }))
12095
+ }
11717
12096
  },
11718
12097
  exitCode: EXIT_CODES.CONFIG,
11719
12098
  meta: {
@@ -11738,15 +12117,15 @@ function outputJson(data) {
11738
12117
  console.log(JSON.stringify(data, null, 2));
11739
12118
  }
11740
12119
  // src/utils/path-safety.ts
11741
- import path5 from "path";
12120
+ import path6 from "path";
11742
12121
  function isPathInside(childPath, parentPath) {
11743
- const resolvedChild = path5.resolve(childPath);
11744
- const resolvedParent = path5.resolve(parentPath);
12122
+ const resolvedChild = path6.resolve(childPath);
12123
+ const resolvedParent = path6.resolve(parentPath);
11745
12124
  if (resolvedChild === resolvedParent) {
11746
12125
  return true;
11747
12126
  }
11748
- const relative2 = path5.relative(resolvedParent, resolvedChild);
11749
- return !!relative2 && !relative2.startsWith("..") && !isAbsolutePath(relative2);
12127
+ const relative3 = path6.relative(resolvedParent, resolvedChild);
12128
+ return !!relative3 && !relative3.startsWith("..") && !isAbsolutePath(relative3);
11750
12129
  }
11751
12130
  function assertPathInside(childPath, parentPath) {
11752
12131
  if (!isPathInside(childPath, parentPath)) {
@@ -13507,7 +13886,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13507
13886
  }
13508
13887
  const computedHash = await hashBundle(files);
13509
13888
  for (const file of component.files) {
13510
- const targetPath = join3(cwd, resolveTargetPath(file.target, isFlattened));
13889
+ const targetPath = join4(cwd, resolveTargetPath(file.target, isFlattened));
13511
13890
  assertPathInside(targetPath, cwd);
13512
13891
  }
13513
13892
  const existingEntry = lock.installed[component.qualifiedName];
@@ -13517,7 +13896,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13517
13896
  }
13518
13897
  for (const file of component.files) {
13519
13898
  const resolvedTarget = resolveTargetPath(file.target, isFlattened);
13520
- const targetPath = join3(cwd, resolvedTarget);
13899
+ const targetPath = join4(cwd, resolvedTarget);
13521
13900
  if (existsSync5(targetPath)) {
13522
13901
  const conflictingComponent = findComponentByFile(lock, resolvedTarget);
13523
13902
  if (conflictingComponent && conflictingComponent !== component.qualifiedName) {
@@ -13541,7 +13920,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13541
13920
  if (!componentFile)
13542
13921
  continue;
13543
13922
  const resolvedTarget = resolveTargetPath(componentFile.target, isFlattened);
13544
- const targetPath = join3(cwd, resolvedTarget);
13923
+ const targetPath = join4(cwd, resolvedTarget);
13545
13924
  if (existsSync5(targetPath)) {
13546
13925
  const existingContent = await Bun.file(targetPath).text();
13547
13926
  const incomingContent = file.content.toString("utf-8");
@@ -13606,7 +13985,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
13606
13985
  }
13607
13986
  const hasNpmDeps = resolved.npmDependencies.length > 0;
13608
13987
  const hasNpmDevDeps = resolved.npmDevDependencies.length > 0;
13609
- const packageJsonPath = options2.global || options2.profile ? join3(cwd, "package.json") : join3(cwd, ".opencode/package.json");
13988
+ const packageJsonPath = options2.global || options2.profile ? join4(cwd, "package.json") : join4(cwd, ".opencode/package.json");
13610
13989
  if (hasNpmDeps || hasNpmDevDeps) {
13611
13990
  const npmSpin = options2.quiet ? null : createSpinner({ text: `Updating ${packageJsonPath}...` });
13612
13991
  npmSpin?.start();
@@ -13648,8 +14027,8 @@ async function installComponent(component, files, cwd, options2) {
13648
14027
  if (!componentFile)
13649
14028
  continue;
13650
14029
  const resolvedTarget = resolveTargetPath(componentFile.target, !!options2.isFlattened);
13651
- const targetPath = join3(cwd, resolvedTarget);
13652
- const targetDir = dirname(targetPath);
14030
+ const targetPath = join4(cwd, resolvedTarget);
14031
+ const targetDir = dirname2(targetPath);
13653
14032
  if (existsSync5(targetPath)) {
13654
14033
  const existingContent = await Bun.file(targetPath).text();
13655
14034
  const incomingContent = file.content.toString("utf-8");
@@ -13662,7 +14041,7 @@ async function installComponent(component, files, cwd, options2) {
13662
14041
  result.written.push(resolvedTarget);
13663
14042
  }
13664
14043
  if (!existsSync5(targetDir)) {
13665
- await mkdir4(targetDir, { recursive: true });
14044
+ await mkdir5(targetDir, { recursive: true });
13666
14045
  }
13667
14046
  await writeFile(targetPath, file.content);
13668
14047
  }
@@ -13738,7 +14117,7 @@ function mergeDevDependencies(existing, newDeps) {
13738
14117
  return { ...existing, devDependencies: merged };
13739
14118
  }
13740
14119
  async function readOpencodePackageJson(opencodeDir) {
13741
- const pkgPath = join3(opencodeDir, "package.json");
14120
+ const pkgPath = join4(opencodeDir, "package.json");
13742
14121
  if (!existsSync5(pkgPath)) {
13743
14122
  return { ...DEFAULT_PACKAGE_JSON };
13744
14123
  }
@@ -13751,7 +14130,7 @@ async function readOpencodePackageJson(opencodeDir) {
13751
14130
  }
13752
14131
  }
13753
14132
  async function ensureManifestFilesAreTracked(opencodeDir) {
13754
- const gitignorePath = join3(opencodeDir, ".gitignore");
14133
+ const gitignorePath = join4(opencodeDir, ".gitignore");
13755
14134
  const filesToTrack = new Set(["package.json", "bun.lock"]);
13756
14135
  const requiredIgnores = ["node_modules"];
13757
14136
  let lines = [];
@@ -13774,12 +14153,12 @@ async function updateOpencodeDevDependencies(cwd, npmDeps, npmDevDeps, options2
13774
14153
  const allDepSpecs = [...npmDeps, ...npmDevDeps];
13775
14154
  if (allDepSpecs.length === 0)
13776
14155
  return;
13777
- const packageDir = options2.isFlattened ? cwd : join3(cwd, ".opencode");
13778
- await mkdir4(packageDir, { recursive: true });
14156
+ const packageDir = options2.isFlattened ? cwd : join4(cwd, ".opencode");
14157
+ await mkdir5(packageDir, { recursive: true });
13779
14158
  const parsedDeps = allDepSpecs.map(parseNpmDependency);
13780
14159
  const existing = await readOpencodePackageJson(packageDir);
13781
14160
  const updated = mergeDevDependencies(existing, parsedDeps);
13782
- await Bun.write(join3(packageDir, "package.json"), `${JSON.stringify(updated, null, 2)}
14161
+ await Bun.write(join4(packageDir, "package.json"), `${JSON.stringify(updated, null, 2)}
13783
14162
  `);
13784
14163
  if (!options2.isFlattened) {
13785
14164
  await ensureManifestFilesAreTracked(packageDir);
@@ -13795,101 +14174,11 @@ function findComponentByFile(lock, filePath) {
13795
14174
  }
13796
14175
 
13797
14176
  // src/commands/build.ts
13798
- import { join as join5, relative as relative2 } from "path";
13799
-
13800
- // src/lib/build-registry.ts
13801
- import { mkdir as mkdir5 } from "fs/promises";
13802
- import { dirname as dirname2, join as join4 } from "path";
13803
- class BuildRegistryError extends Error {
13804
- errors;
13805
- constructor(message, errors3 = []) {
13806
- super(message);
13807
- this.errors = errors3;
13808
- this.name = "BuildRegistryError";
13809
- }
13810
- }
13811
- async function buildRegistry(options2) {
13812
- const { source: sourcePath, out: outPath } = options2;
13813
- const jsoncFile = Bun.file(join4(sourcePath, "registry.jsonc"));
13814
- const jsonFile = Bun.file(join4(sourcePath, "registry.json"));
13815
- const jsoncExists = await jsoncFile.exists();
13816
- const jsonExists = await jsonFile.exists();
13817
- if (!jsoncExists && !jsonExists) {
13818
- throw new BuildRegistryError("No registry.jsonc or registry.json found in source directory");
13819
- }
13820
- const registryFile = jsoncExists ? jsoncFile : jsonFile;
13821
- const content2 = await registryFile.text();
13822
- const registryData = parse2(content2, [], { allowTrailingComma: true });
13823
- const parseResult = registrySchema.safeParse(registryData);
13824
- if (!parseResult.success) {
13825
- const errors3 = parseResult.error.errors.map((e3) => `${e3.path.join(".")}: ${e3.message}`);
13826
- throw new BuildRegistryError("Registry validation failed", errors3);
13827
- }
13828
- const registry = parseResult.data;
13829
- const validationErrors = [];
13830
- const componentsDir = join4(outPath, "components");
13831
- await mkdir5(componentsDir, { recursive: true });
13832
- for (const component of registry.components) {
13833
- const packument = {
13834
- name: component.name,
13835
- versions: {
13836
- [registry.version]: component
13837
- },
13838
- "dist-tags": {
13839
- latest: registry.version
13840
- }
13841
- };
13842
- const packumentPath = join4(componentsDir, `${component.name}.json`);
13843
- await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
13844
- for (const rawFile of component.files) {
13845
- const file = normalizeFile(rawFile, component.type);
13846
- const sourceFilePath = join4(sourcePath, "files", file.path);
13847
- const destFilePath = join4(componentsDir, component.name, file.path);
13848
- const destFileDir = dirname2(destFilePath);
13849
- if (!await Bun.file(sourceFilePath).exists()) {
13850
- validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`);
13851
- continue;
13852
- }
13853
- await mkdir5(destFileDir, { recursive: true });
13854
- const sourceFile = Bun.file(sourceFilePath);
13855
- await Bun.write(destFilePath, sourceFile);
13856
- }
13857
- }
13858
- if (validationErrors.length > 0) {
13859
- throw new BuildRegistryError(`Build failed with ${validationErrors.length} errors`, validationErrors);
13860
- }
13861
- const index = {
13862
- name: registry.name,
13863
- namespace: registry.namespace,
13864
- version: registry.version,
13865
- author: registry.author,
13866
- ...registry.opencode && { opencode: registry.opencode },
13867
- ...registry.ocx && { ocx: registry.ocx },
13868
- components: registry.components.map((c) => ({
13869
- name: c.name,
13870
- type: c.type,
13871
- description: c.description
13872
- }))
13873
- };
13874
- await Bun.write(join4(outPath, "index.json"), JSON.stringify(index, null, 2));
13875
- const wellKnownDir = join4(outPath, ".well-known");
13876
- await mkdir5(wellKnownDir, { recursive: true });
13877
- const discovery = { registry: "/index.json" };
13878
- await Bun.write(join4(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
13879
- return {
13880
- name: registry.name,
13881
- namespace: registry.namespace,
13882
- version: registry.version,
13883
- componentsCount: registry.components.length,
13884
- outputPath: outPath
13885
- };
13886
- }
13887
-
13888
- // src/commands/build.ts
14177
+ import { join as join5, relative as relative3 } from "path";
13889
14178
  function registerBuildCommand(program2) {
13890
- 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) => {
14179
+ 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) => {
13891
14180
  try {
13892
- const sourcePath = join5(options2.cwd, path6);
14181
+ const sourcePath = join5(options2.cwd, path7);
13893
14182
  const outPath = join5(options2.cwd, options2.out);
13894
14183
  const spinner2 = createSpinner({
13895
14184
  text: "Building registry...",
@@ -13902,7 +14191,7 @@ function registerBuildCommand(program2) {
13902
14191
  out: outPath
13903
14192
  });
13904
14193
  if (!options2.json) {
13905
- const msg = `Built ${result.componentsCount} components to ${relative2(options2.cwd, outPath)}`;
14194
+ const msg = `Built ${result.componentsCount} components to ${relative3(options2.cwd, outPath)}`;
13906
14195
  spinner2.succeed(msg);
13907
14196
  if (!process.stdout.isTTY) {
13908
14197
  logger.success(`Built ${result.componentsCount} components`);
@@ -14204,16 +14493,16 @@ class Diff {
14204
14493
  }
14205
14494
  }
14206
14495
  }
14207
- addToPath(path6, added, removed, oldPosInc, options2) {
14208
- const last = path6.lastComponent;
14496
+ addToPath(path7, added, removed, oldPosInc, options2) {
14497
+ const last = path7.lastComponent;
14209
14498
  if (last && !options2.oneChangePerToken && last.added === added && last.removed === removed) {
14210
14499
  return {
14211
- oldPos: path6.oldPos + oldPosInc,
14500
+ oldPos: path7.oldPos + oldPosInc,
14212
14501
  lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
14213
14502
  };
14214
14503
  } else {
14215
14504
  return {
14216
- oldPos: path6.oldPos + oldPosInc,
14505
+ oldPos: path7.oldPos + oldPosInc,
14217
14506
  lastComponent: { count: 1, added, removed, previousComponent: last }
14218
14507
  };
14219
14508
  }
@@ -15030,7 +15319,7 @@ import {
15030
15319
  rmSync,
15031
15320
  unlinkSync
15032
15321
  } from "fs";
15033
- import path6 from "path";
15322
+ import path7 from "path";
15034
15323
  var GHOST_CONFIG_FILE = "ghost.jsonc";
15035
15324
  var BACKUP_EXT = ".bak";
15036
15325
  var CURRENT_SYMLINK = "current";
@@ -15068,8 +15357,8 @@ function planMigration() {
15068
15357
  if (!entry.isDirectory() || entry.name === CURRENT_SYMLINK)
15069
15358
  continue;
15070
15359
  const profileName = entry.name;
15071
- const ghostConfig = path6.join(profilesDir, profileName, GHOST_CONFIG_FILE);
15072
- const ocxConfig = path6.join(profilesDir, profileName, OCX_CONFIG_FILE);
15360
+ const ghostConfig = path7.join(profilesDir, profileName, GHOST_CONFIG_FILE);
15361
+ const ocxConfig = path7.join(profilesDir, profileName, OCX_CONFIG_FILE);
15073
15362
  if (!existsSync7(ghostConfig))
15074
15363
  continue;
15075
15364
  if (existsSync7(ocxConfig)) {
@@ -15099,13 +15388,13 @@ function planMigration() {
15099
15388
  if (!entry.isDirectory() || entry.name === CURRENT_SYMLINK)
15100
15389
  continue;
15101
15390
  const profileName = entry.name;
15102
- const profileDir = path6.join(profilesDir, profileName);
15103
- const dotOpencode = path6.join(profileDir, ".opencode");
15391
+ const profileDir = path7.join(profilesDir, profileName);
15392
+ const dotOpencode = path7.join(profileDir, ".opencode");
15104
15393
  if (!existsSync7(dotOpencode))
15105
15394
  continue;
15106
15395
  for (const dir of FLATTEN_DIRS) {
15107
- const source = path6.join(dotOpencode, dir);
15108
- const destination = path6.join(profileDir, dir);
15396
+ const source = path7.join(dotOpencode, dir);
15397
+ const destination = path7.join(profileDir, dir);
15109
15398
  if (!existsSync7(source))
15110
15399
  continue;
15111
15400
  try {
@@ -15144,7 +15433,7 @@ function planMigration() {
15144
15433
  });
15145
15434
  }
15146
15435
  }
15147
- const currentPath = path6.join(profilesDir, CURRENT_SYMLINK);
15436
+ const currentPath = path7.join(profilesDir, CURRENT_SYMLINK);
15148
15437
  if (existsSync7(currentPath)) {
15149
15438
  try {
15150
15439
  const stat3 = lstatSync(currentPath);
@@ -15203,21 +15492,21 @@ function executeMigration(plan) {
15203
15492
  } catch {}
15204
15493
  }
15205
15494
  } catch (error) {
15206
- for (const rename2 of completedRenames) {
15495
+ for (const rename3 of completedRenames) {
15207
15496
  try {
15208
- if (existsSync7(rename2.destination)) {
15209
- if (rename2.isDir) {
15210
- rmSync(rename2.destination, { recursive: true, force: true });
15497
+ if (existsSync7(rename3.destination)) {
15498
+ if (rename3.isDir) {
15499
+ rmSync(rename3.destination, { recursive: true, force: true });
15211
15500
  } else {
15212
- unlinkSync(rename2.destination);
15501
+ unlinkSync(rename3.destination);
15213
15502
  }
15214
15503
  }
15215
- mkdirSync(path6.dirname(rename2.source), { recursive: true });
15216
- if (existsSync7(rename2.backup)) {
15217
- if (rename2.isDir) {
15218
- cpSync(rename2.backup, rename2.source, { recursive: true });
15504
+ mkdirSync(path7.dirname(rename3.source), { recursive: true });
15505
+ if (existsSync7(rename3.backup)) {
15506
+ if (rename3.isDir) {
15507
+ cpSync(rename3.backup, rename3.source, { recursive: true });
15219
15508
  } else {
15220
- copyFileSync(rename2.backup, rename2.source);
15509
+ copyFileSync(rename3.backup, rename3.source);
15221
15510
  }
15222
15511
  }
15223
15512
  } catch {}
@@ -15238,8 +15527,8 @@ function executeMigration(plan) {
15238
15527
  try {
15239
15528
  const processedProfiles = new Set(plan.profiles.filter((a) => a.type === "move-dir").map((a) => a.profileName));
15240
15529
  for (const profileName of processedProfiles) {
15241
- const profileDir = path6.join(getProfilesDir(), profileName);
15242
- const dotOpencode = path6.join(profileDir, ".opencode");
15530
+ const profileDir = path7.join(getProfilesDir(), profileName);
15531
+ const dotOpencode = path7.join(profileDir, ".opencode");
15243
15532
  if (existsSync7(dotOpencode)) {
15244
15533
  const remaining = readdirSync(dotOpencode);
15245
15534
  if (remaining.length === 0) {
@@ -15265,7 +15554,7 @@ function printPlan(plan, dryRun) {
15265
15554
  if (action.type === "rename") {
15266
15555
  console.log(` \u2713 ${action.profileName}: ${GHOST_CONFIG_FILE} \u2192 ${OCX_CONFIG_FILE}`);
15267
15556
  } else {
15268
- const dirName = path6.basename(action.source);
15557
+ const dirName = path7.basename(action.source);
15269
15558
  console.log(` \u2713 ${action.profileName}: .opencode/${dirName}/ \u2192 ${dirName}/`);
15270
15559
  }
15271
15560
  }
@@ -15525,7 +15814,7 @@ async function copyDir(src, dest) {
15525
15814
  }
15526
15815
  function getReleaseTag() {
15527
15816
  if (false) {}
15528
- return `v${"1.4.2"}`;
15817
+ return `v${"1.4.4"}`;
15529
15818
  }
15530
15819
  function getTemplateUrl(version) {
15531
15820
  const ref = version === "main" ? "heads/main" : `tags/${version}`;
@@ -15566,6 +15855,9 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
15566
15855
  await rm2(tempDir, { recursive: true, force: true });
15567
15856
  }
15568
15857
  }
15858
+ function toTitleCase(str) {
15859
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
15860
+ }
15569
15861
  async function replacePlaceholders(dir, values) {
15570
15862
  const filesToProcess = [
15571
15863
  "registry.jsonc",
@@ -15580,16 +15872,17 @@ async function replacePlaceholders(dir, values) {
15580
15872
  continue;
15581
15873
  let content2 = await readFile(filePath).then((b) => b.toString());
15582
15874
  content2 = content2.replace(/my-registry/g, values.namespace);
15875
+ content2 = content2.replace(/My Registry/g, toTitleCase(values.namespace));
15583
15876
  content2 = content2.replace(/Your Name/g, values.author);
15584
15877
  await writeFile2(filePath, content2);
15585
15878
  }
15586
15879
  }
15587
15880
 
15588
15881
  // src/commands/opencode.ts
15589
- import { resolve as resolve2 } from "path";
15882
+ import { resolve as resolve3 } from "path";
15590
15883
 
15591
15884
  // src/utils/terminal-title.ts
15592
- import path7 from "path";
15885
+ import path8 from "path";
15593
15886
  var MAX_BRANCH_LENGTH = 20;
15594
15887
  var titleSaved = false;
15595
15888
  function isInsideTmux() {
@@ -15632,7 +15925,7 @@ function restoreTerminalTitle() {
15632
15925
  titleSaved = false;
15633
15926
  }
15634
15927
  function formatTerminalName(cwd, profileName, gitInfo) {
15635
- const repoName = gitInfo.repoName ?? path7.basename(cwd);
15928
+ const repoName = gitInfo.repoName ?? path8.basename(cwd);
15636
15929
  if (!gitInfo.branch) {
15637
15930
  return `ocx[${profileName}]:${repoName}`;
15638
15931
  }
@@ -15654,16 +15947,16 @@ function buildOpenCodeEnv(opts) {
15654
15947
  };
15655
15948
  }
15656
15949
  function registerOpencodeCommand(program2) {
15657
- 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) => {
15950
+ 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) => {
15658
15951
  try {
15659
- await runOpencode(path8, command.args, options2);
15952
+ await runOpencode(path9, command.args, options2);
15660
15953
  } catch (error) {
15661
15954
  handleError(error, { json: options2.json });
15662
15955
  }
15663
15956
  });
15664
15957
  }
15665
15958
  async function runOpencode(pathArg, args, options2) {
15666
- const projectDir = pathArg ? resolve2(pathArg) : process.cwd();
15959
+ const projectDir = pathArg ? resolve3(pathArg) : process.cwd();
15667
15960
  const resolver = await ConfigResolver.create(projectDir, { profile: options2.profile });
15668
15961
  const config = resolver.resolve();
15669
15962
  const profile = resolver.getProfile();
@@ -15748,12 +16041,8 @@ async function runOpencode(pathArg, args, options2) {
15748
16041
  // src/commands/profile/install-from-registry.ts
15749
16042
  import { createHash as createHash2 } from "crypto";
15750
16043
  import { existsSync as existsSync9 } from "fs";
15751
- import { mkdir as mkdir8, mkdtemp, rename as rename2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
16044
+ import { mkdir as mkdir8, mkdtemp, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
15752
16045
  import { dirname as dirname4, join as join8 } from "path";
15753
- var PROFILE_FILE_TARGETS = new Set(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
15754
- function isProfileFile(target) {
15755
- return PROFILE_FILE_TARGETS.has(target);
15756
- }
15757
16046
  function hashContent2(content2) {
15758
16047
  return createHash2("sha256").update(content2).digest("hex");
15759
16048
  }
@@ -15768,7 +16057,7 @@ function hashBundle2(files) {
15768
16057
  `));
15769
16058
  }
15770
16059
  async function installProfileFromRegistry(options2) {
15771
- const { namespace, component, profileName, force, registryUrl, registries, quiet } = options2;
16060
+ const { namespace, component, profileName, registryUrl, registries, quiet } = options2;
15772
16061
  const parseResult = profileNameSchema.safeParse(profileName);
15773
16062
  if (!parseResult.success) {
15774
16063
  throw new ValidationError(`Invalid profile name: "${profileName}". ` + `Profile names must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens.`);
@@ -15776,9 +16065,8 @@ async function installProfileFromRegistry(options2) {
15776
16065
  const profileDir = getProfileDir(profileName);
15777
16066
  const qualifiedName = `${namespace}/${component}`;
15778
16067
  const profileExists = existsSync9(profileDir);
15779
- if (profileExists && !force) {
15780
- throw new ConflictError(`Profile "${profileName}" already exists.
15781
- Use --force to overwrite.`);
16068
+ if (profileExists) {
16069
+ throw new ConflictError(`Profile "${profileName}" already exists. Remove it first with 'ocx profile rm ${profileName}'.`);
15782
16070
  }
15783
16071
  const fetchSpin = quiet ? null : createSpinner({ text: `Fetching ${qualifiedName}...` });
15784
16072
  fetchSpin?.start();
@@ -15805,7 +16093,7 @@ Use --force to overwrite.`);
15805
16093
  const filesSpin = quiet ? null : createSpinner({ text: "Downloading profile files..." });
15806
16094
  filesSpin?.start();
15807
16095
  const profileFiles = [];
15808
- const dependencyFiles = [];
16096
+ const embeddedFiles = [];
15809
16097
  for (const file of normalized.files) {
15810
16098
  const content2 = await fetchFileContent(registryUrl, component, file.path);
15811
16099
  const fileEntry = {
@@ -15813,53 +16101,17 @@ Use --force to overwrite.`);
15813
16101
  target: file.target,
15814
16102
  content: Buffer.from(content2)
15815
16103
  };
15816
- if (isProfileFile(file.target)) {
15817
- profileFiles.push(fileEntry);
16104
+ if (file.target.startsWith(".opencode/")) {
16105
+ embeddedFiles.push(fileEntry);
15818
16106
  } else {
15819
- dependencyFiles.push(fileEntry);
16107
+ profileFiles.push(fileEntry);
15820
16108
  }
15821
16109
  }
15822
16110
  filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
15823
- let resolvedDeps = null;
15824
- const dependencyBundles = [];
15825
- if (manifest.dependencies.length > 0) {
15826
- const depsSpin = quiet ? null : createSpinner({ text: "Resolving dependencies..." });
15827
- depsSpin?.start();
15828
- try {
15829
- const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
15830
- resolvedDeps = await resolveDependencies(registries, depRefs);
15831
- for (const depComponent of resolvedDeps.components) {
15832
- const files = [];
15833
- for (const file of depComponent.files) {
15834
- const content2 = await fetchFileContent(depComponent.baseUrl, depComponent.name, file.path);
15835
- const resolvedTarget = resolveTargetPath(file.target, true);
15836
- files.push({
15837
- path: file.path,
15838
- target: resolvedTarget,
15839
- content: Buffer.from(content2)
15840
- });
15841
- }
15842
- const registryIndex = await fetchRegistryIndex(depComponent.baseUrl);
15843
- dependencyBundles.push({
15844
- qualifiedName: depComponent.qualifiedName,
15845
- registryName: depComponent.registryName,
15846
- files,
15847
- hash: hashBundle2(files),
15848
- version: registryIndex.version
15849
- });
15850
- }
15851
- depsSpin?.succeed(`Resolved ${resolvedDeps.components.length} dependencies`);
15852
- } catch (error) {
15853
- depsSpin?.fail("Failed to resolve dependencies");
15854
- throw error;
15855
- }
15856
- }
15857
16111
  const profilesDir = getProfilesDir();
15858
16112
  await mkdir8(profilesDir, { recursive: true, mode: 448 });
15859
16113
  const stagingDir = await mkdtemp(join8(profilesDir, ".staging-"));
15860
- const stagingOpencodeDir = join8(stagingDir, ".opencode");
15861
16114
  try {
15862
- await mkdir8(stagingOpencodeDir, { recursive: true, mode: 448 });
15863
16115
  const writeSpin = quiet ? null : createSpinner({ text: "Writing profile files..." });
15864
16116
  writeSpin?.start();
15865
16117
  for (const file of profileFiles) {
@@ -15870,37 +16122,20 @@ Use --force to overwrite.`);
15870
16122
  }
15871
16123
  await writeFile3(targetPath, file.content);
15872
16124
  }
15873
- for (const file of dependencyFiles) {
16125
+ for (const file of embeddedFiles) {
15874
16126
  const target = file.target.startsWith(".opencode/") ? file.target.slice(".opencode/".length) : file.target;
15875
- const targetPath = join8(stagingOpencodeDir, target);
16127
+ const targetPath = join8(stagingDir, target);
15876
16128
  const targetDir = dirname4(targetPath);
15877
16129
  if (!existsSync9(targetDir)) {
15878
16130
  await mkdir8(targetDir, { recursive: true });
15879
16131
  }
15880
16132
  await writeFile3(targetPath, file.content);
15881
16133
  }
15882
- writeSpin?.succeed(`Wrote ${profileFiles.length + dependencyFiles.length} profile files`);
15883
- if (dependencyBundles.length > 0) {
15884
- const depWriteSpin = quiet ? null : createSpinner({ text: "Writing dependency files..." });
15885
- depWriteSpin?.start();
15886
- let depFileCount = 0;
15887
- for (const bundle of dependencyBundles) {
15888
- for (const file of bundle.files) {
15889
- const targetPath = join8(stagingOpencodeDir, file.target);
15890
- const targetDir = dirname4(targetPath);
15891
- if (!existsSync9(targetDir)) {
15892
- await mkdir8(targetDir, { recursive: true });
15893
- }
15894
- await writeFile3(targetPath, file.content);
15895
- depFileCount++;
15896
- }
15897
- }
15898
- depWriteSpin?.succeed(`Wrote ${depFileCount} dependency files`);
15899
- }
16134
+ writeSpin?.succeed(`Wrote ${profileFiles.length + embeddedFiles.length} profile files`);
15900
16135
  const profileHash = hashBundle2(profileFiles.map((f) => ({ path: f.path, content: f.content })));
15901
16136
  const registryIndex = await fetchRegistryIndex(registryUrl);
15902
16137
  const lock = {
15903
- version: 1,
16138
+ lockVersion: 1,
15904
16139
  installedFrom: {
15905
16140
  registry: namespace,
15906
16141
  component,
@@ -15910,36 +16145,32 @@ Use --force to overwrite.`);
15910
16145
  },
15911
16146
  installed: {}
15912
16147
  };
15913
- for (const bundle of dependencyBundles) {
15914
- lock.installed[bundle.qualifiedName] = {
15915
- registry: bundle.registryName,
15916
- version: bundle.version,
15917
- hash: bundle.hash,
15918
- files: bundle.files.map((f) => f.target),
15919
- installedAt: new Date().toISOString()
15920
- };
15921
- }
15922
- await writeFile3(join8(stagingDir, "ocx.lock"), JSON.stringify(lock, null, "\t"));
15923
- const moveSpin = quiet ? null : createSpinner({ text: "Finalizing installation..." });
15924
- moveSpin?.start();
16148
+ await writeOcxLock(stagingDir, lock, join8(stagingDir, "ocx.lock"));
16149
+ const renameSpin = quiet ? null : createSpinner({ text: "Moving to profile directory..." });
16150
+ renameSpin?.start();
15925
16151
  const profilesDir2 = dirname4(profileDir);
15926
16152
  if (!existsSync9(profilesDir2)) {
15927
16153
  await mkdir8(profilesDir2, { recursive: true, mode: 448 });
15928
16154
  }
15929
- if (profileExists && force) {
15930
- const backupDir = `${profileDir}.backup-${Date.now()}`;
15931
- await rename2(profileDir, backupDir);
16155
+ await rename3(stagingDir, profileDir);
16156
+ renameSpin?.succeed("Profile installed");
16157
+ if (manifest.dependencies.length > 0) {
16158
+ const depsSpin = quiet ? null : createSpinner({ text: "Installing dependencies..." });
16159
+ depsSpin?.start();
15932
16160
  try {
15933
- await rename2(stagingDir, profileDir);
15934
- } catch (err) {
15935
- await rename2(backupDir, profileDir);
15936
- throw err;
16161
+ const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
16162
+ const provider = {
16163
+ cwd: profileDir,
16164
+ getRegistries: () => registries,
16165
+ getComponentPath: () => ""
16166
+ };
16167
+ await runAddCore(depRefs, { profile: profileName }, provider);
16168
+ depsSpin?.succeed(`Installed ${manifest.dependencies.length} dependencies`);
16169
+ } catch (error) {
16170
+ depsSpin?.fail("Failed to install dependencies");
16171
+ throw error;
15937
16172
  }
15938
- await rm3(backupDir, { recursive: true, force: true });
15939
- } else {
15940
- await rename2(stagingDir, profileDir);
15941
16173
  }
15942
- moveSpin?.succeed("Installation complete");
15943
16174
  if (!quiet) {
15944
16175
  logger.info("");
15945
16176
  logger.success(`Installed profile "${profileName}" from ${qualifiedName}`);
@@ -15948,12 +16179,9 @@ Use --force to overwrite.`);
15948
16179
  for (const file of profileFiles) {
15949
16180
  logger.info(` ${file.target}`);
15950
16181
  }
15951
- if (dependencyBundles.length > 0) {
15952
- logger.info("");
15953
- logger.info("Dependencies:");
15954
- for (const bundle of dependencyBundles) {
15955
- logger.info(` ${bundle.qualifiedName}`);
15956
- }
16182
+ for (const file of embeddedFiles) {
16183
+ const target = file.target.startsWith(".opencode/") ? file.target.slice(".opencode/".length) : file.target;
16184
+ logger.info(` ${target}`);
15957
16185
  }
15958
16186
  }
15959
16187
  } catch (error) {
@@ -15972,9 +16200,6 @@ function parseFromOption(from) {
15972
16200
  throw new ValidationError("--from value cannot be empty");
15973
16201
  }
15974
16202
  const trimmed = from.trim();
15975
- if (trimmed.startsWith("./") || trimmed.startsWith("~/") || trimmed.startsWith("/")) {
15976
- return { type: "local-path", path: trimmed };
15977
- }
15978
16203
  const slashCount = (trimmed.match(/\//g) || []).length;
15979
16204
  if (slashCount === 1) {
15980
16205
  const [namespace, component] = trimmed.split("/").map((s) => s.trim());
@@ -16022,12 +16247,11 @@ async function requireGlobalRegistry(namespace) {
16022
16247
  return { config: globalConfig, registryUrl: registry.url };
16023
16248
  }
16024
16249
  function registerProfileAddCommand(parent) {
16025
- parent.command("add <name>").description("Create a new profile, clone from existing, or install from registry").option("--from <source>", "Clone from existing profile or install from registry (e.g., kdco/minimal)").option("-f, --force", "Overwrite existing profile").addHelpText("after", `
16250
+ parent.command("add <name>").description("Create a new profile, clone from existing, or install from registry").option("--from <source>", "Clone from existing profile or install from registry (e.g., kdco/minimal)").addHelpText("after", `
16026
16251
  Examples:
16027
16252
  $ ocx profile add work # Create empty profile
16028
16253
  $ ocx profile add work --from dev # Clone from existing profile
16029
16254
  $ ocx profile add work --from kdco/minimal # Install from registry
16030
- $ ocx profile add work --from kdco/minimal --force # Overwrite existing
16031
16255
  `).action(async (name, options2) => {
16032
16256
  try {
16033
16257
  await runProfileAdd(name, options2);
@@ -16038,28 +16262,15 @@ Examples:
16038
16262
  }
16039
16263
  async function runProfileAdd(name, options2) {
16040
16264
  const manager = await ProfileManager.requireInitialized();
16041
- const profileExists = await manager.exists(name);
16042
- if (profileExists && !options2.force) {
16043
- logger.error(`\u2717 Profile "${name}" already exists`);
16044
- logger.error("");
16045
- logger.error("Use --force to overwrite the existing profile.");
16046
- throw new ProfileExistsError(name);
16047
- }
16048
16265
  if (!options2.from) {
16049
- await createEmptyProfile(manager, name, profileExists);
16266
+ await createEmptyProfile(manager, name);
16050
16267
  return;
16051
16268
  }
16052
16269
  const fromInput = parseFromOption(options2.from);
16053
16270
  switch (fromInput.type) {
16054
16271
  case "local-profile":
16055
- await cloneFromLocalProfile(manager, name, fromInput.name, profileExists);
16272
+ await cloneFromLocalProfile(manager, name, fromInput.name);
16056
16273
  break;
16057
- case "local-path":
16058
- throw new ValidationError(`Local path installation is not yet implemented: "${fromInput.path}"
16059
-
16060
- ` + `Currently supported sources:
16061
- ` + ` - Existing profile: --from <profile-name>
16062
- ` + ` - Registry: --from <namespace>/<component>`);
16063
16274
  case "registry": {
16064
16275
  const { config: globalConfig, registryUrl } = await requireGlobalRegistry(fromInput.namespace);
16065
16276
  const registries = {};
@@ -16070,7 +16281,6 @@ async function runProfileAdd(name, options2) {
16070
16281
  namespace: fromInput.namespace,
16071
16282
  component: fromInput.component,
16072
16283
  profileName: name,
16073
- force: options2.force,
16074
16284
  registryUrl,
16075
16285
  registries
16076
16286
  });
@@ -16078,18 +16288,20 @@ async function runProfileAdd(name, options2) {
16078
16288
  }
16079
16289
  }
16080
16290
  }
16081
- async function createEmptyProfile(manager, name, exists) {
16291
+ async function createEmptyProfile(manager, name) {
16292
+ const exists = await manager.exists(name);
16082
16293
  if (exists) {
16083
- await manager.remove(name);
16294
+ throw new ProfileExistsError(name, `Remove it first with 'ocx profile rm ${name}'.`);
16084
16295
  }
16085
16296
  await manager.add(name);
16086
16297
  logger.success(`Created profile "${name}"`);
16087
16298
  }
16088
- async function cloneFromLocalProfile(manager, name, sourceName, exists) {
16089
- const source = await manager.get(sourceName);
16299
+ async function cloneFromLocalProfile(manager, name, sourceName) {
16300
+ const exists = await manager.exists(name);
16090
16301
  if (exists) {
16091
- await manager.remove(name);
16302
+ throw new ProfileExistsError(name, `Remove it first with 'ocx profile rm ${name}'.`);
16092
16303
  }
16304
+ const source = await manager.get(sourceName);
16093
16305
  await manager.add(name);
16094
16306
  await atomicWrite(getProfileOcxConfig(name), source.ocx);
16095
16307
  logger.success(`Created profile "${name}" (cloned from "${sourceName}")`);
@@ -16122,6 +16334,25 @@ async function runProfileList(options2) {
16122
16334
  }
16123
16335
  }
16124
16336
 
16337
+ // src/commands/profile/move.ts
16338
+ function registerProfileMoveCommand(parent) {
16339
+ parent.command("move <old-name> <new-name>").alias("mv").description("Move (rename) a profile").action(async (oldName, newName) => {
16340
+ try {
16341
+ await runProfileMove(oldName, newName);
16342
+ } catch (error) {
16343
+ handleError(error);
16344
+ }
16345
+ });
16346
+ }
16347
+ async function runProfileMove(oldName, newName) {
16348
+ const manager = await ProfileManager.requireInitialized();
16349
+ const { warnActiveProfile } = await manager.move(oldName, newName);
16350
+ if (warnActiveProfile) {
16351
+ logger.warn(`Moving active profile. Update OCX_PROFILE env var to "${newName}".`);
16352
+ }
16353
+ logger.success(`Moved profile "${oldName}" \u2192 "${newName}"`);
16354
+ }
16355
+
16125
16356
  // src/commands/profile/remove.ts
16126
16357
  function registerProfileRemoveCommand(parent) {
16127
16358
  parent.command("remove <name>").alias("rm").description("Delete a global profile").action(async (name) => {
@@ -16180,6 +16411,7 @@ function registerProfileCommand(program2) {
16180
16411
  registerProfileListCommand(profile);
16181
16412
  registerProfileAddCommand(profile);
16182
16413
  registerProfileRemoveCommand(profile);
16414
+ registerProfileMoveCommand(profile);
16183
16415
  registerProfileShowCommand(profile);
16184
16416
  }
16185
16417
 
@@ -16517,9 +16749,460 @@ async function runSearchCore(query, options2, provider) {
16517
16749
  }
16518
16750
  }
16519
16751
 
16752
+ // src/commands/self/uninstall.ts
16753
+ import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync2, realpathSync, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
16754
+ import { homedir as homedir4 } from "os";
16755
+ import path9 from "path";
16756
+
16757
+ // src/self-update/detect-method.ts
16758
+ function parseInstallMethod(input) {
16759
+ const VALID_METHODS = ["curl", "npm", "yarn", "pnpm", "bun"];
16760
+ const method = VALID_METHODS.find((m) => m === input);
16761
+ if (!method) {
16762
+ throw new SelfUpdateError(`Invalid install method: "${input}"
16763
+ Valid methods: ${VALID_METHODS.join(", ")}`);
16764
+ }
16765
+ return method;
16766
+ }
16767
+ var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
16768
+ var isTempExecution = (path9) => path9.includes("/_npx/") || path9.includes("/.cache/bunx/") || path9.includes("/.pnpm/_temp/");
16769
+ var isYarnGlobalInstall = (path9) => path9.includes("/.yarn/global") || path9.includes("/.config/yarn/global");
16770
+ var isPnpmGlobalInstall = (path9) => path9.includes("/.pnpm/") || path9.includes("/pnpm/global");
16771
+ var isBunGlobalInstall = (path9) => path9.includes("/.bun/bin") || path9.includes("/.bun/install/global");
16772
+ var isNpmGlobalInstall = (path9) => path9.includes("/.npm/") || path9.includes("/node_modules/");
16773
+ function detectInstallMethod() {
16774
+ if (isCompiledBinary()) {
16775
+ return "curl";
16776
+ }
16777
+ const scriptPath = process.argv[1] ?? "";
16778
+ if (isTempExecution(scriptPath))
16779
+ return "unknown";
16780
+ if (isYarnGlobalInstall(scriptPath))
16781
+ return "yarn";
16782
+ if (isPnpmGlobalInstall(scriptPath))
16783
+ return "pnpm";
16784
+ if (isBunGlobalInstall(scriptPath))
16785
+ return "bun";
16786
+ if (isNpmGlobalInstall(scriptPath))
16787
+ return "npm";
16788
+ const userAgent = process.env.npm_config_user_agent ?? "";
16789
+ if (userAgent.includes("yarn"))
16790
+ return "yarn";
16791
+ if (userAgent.includes("pnpm"))
16792
+ return "pnpm";
16793
+ if (userAgent.includes("bun"))
16794
+ return "bun";
16795
+ if (userAgent.includes("npm"))
16796
+ return "npm";
16797
+ return "unknown";
16798
+ }
16799
+ function getExecutablePath() {
16800
+ if (typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/")) {
16801
+ return process.execPath;
16802
+ }
16803
+ return process.argv[1] ?? process.execPath;
16804
+ }
16805
+
16806
+ // src/commands/self/uninstall.ts
16807
+ var UNINSTALL_EXIT_CODES = {
16808
+ SUCCESS: 0,
16809
+ ERROR: 1,
16810
+ SAFETY_ERROR: 2
16811
+ };
16812
+ function isNodeError(err) {
16813
+ return err instanceof Error && "code" in err;
16814
+ }
16815
+ function tildify(absolutePath) {
16816
+ const home = homedir4();
16817
+ if (!home)
16818
+ return absolutePath;
16819
+ if (absolutePath === home)
16820
+ return "~";
16821
+ if (absolutePath.startsWith(home + path9.sep)) {
16822
+ return `~${absolutePath.slice(home.length)}`;
16823
+ }
16824
+ return absolutePath;
16825
+ }
16826
+ function getRelativePathIfContained(parent, child) {
16827
+ const normalizedParent = path9.normalize(parent);
16828
+ const normalizedChild = path9.normalize(child);
16829
+ const relative4 = path9.relative(normalizedParent, normalizedChild);
16830
+ if (relative4.startsWith("..") || path9.isAbsolute(relative4)) {
16831
+ return null;
16832
+ }
16833
+ return relative4;
16834
+ }
16835
+ function isLexicallyInside(root, target) {
16836
+ return getRelativePathIfContained(root, target) !== null;
16837
+ }
16838
+ function isRealpathInside(root, target) {
16839
+ if (!existsSync11(target)) {
16840
+ return { contained: true };
16841
+ }
16842
+ try {
16843
+ const realRoot = realpathSync(root);
16844
+ const realTarget = realpathSync(target);
16845
+ return { contained: getRelativePathIfContained(realRoot, realTarget) !== null };
16846
+ } catch (err) {
16847
+ if (isNodeError(err) && (err.code === "EACCES" || err.code === "EPERM")) {
16848
+ return { contained: false, error: "permission" };
16849
+ }
16850
+ return { contained: false, error: "io" };
16851
+ }
16852
+ }
16853
+ function validateRootDirectory(rootPath) {
16854
+ try {
16855
+ const stats = lstatSync2(rootPath);
16856
+ if (stats.isSymbolicLink()) {
16857
+ return { valid: false, reason: "symlink" };
16858
+ }
16859
+ if (!stats.isDirectory()) {
16860
+ return { valid: false, reason: "not-directory" };
16861
+ }
16862
+ return { valid: true };
16863
+ } catch (err) {
16864
+ if (isNodeError(err)) {
16865
+ if (err.code === "ENOENT") {
16866
+ return { valid: false, reason: "not-found" };
16867
+ }
16868
+ if (err.code === "EACCES" || err.code === "EPERM") {
16869
+ return { valid: false, reason: "permission" };
16870
+ }
16871
+ }
16872
+ return { valid: false, reason: "permission" };
16873
+ }
16874
+ }
16875
+ function getPathKind(targetPath) {
16876
+ if (!existsSync11(targetPath)) {
16877
+ return "missing";
16878
+ }
16879
+ try {
16880
+ const stats = lstatSync2(targetPath);
16881
+ if (stats.isSymbolicLink()) {
16882
+ return "symlink";
16883
+ }
16884
+ if (stats.isDirectory()) {
16885
+ return "directory";
16886
+ }
16887
+ return "file";
16888
+ } catch {
16889
+ return "missing";
16890
+ }
16891
+ }
16892
+ function isDirectoryEmpty(dirPath) {
16893
+ if (!existsSync11(dirPath)) {
16894
+ return true;
16895
+ }
16896
+ try {
16897
+ const entries = readdirSync2(dirPath);
16898
+ return entries.length === 0;
16899
+ } catch {
16900
+ return false;
16901
+ }
16902
+ }
16903
+ function classifyTargetSafety(target) {
16904
+ if (target.kind === "missing") {
16905
+ return "safe";
16906
+ }
16907
+ if (target.kind === "symlink") {
16908
+ return isLexicallyInside(target.rootPath, target.absolutePath) ? "safe" : "forbidden";
16909
+ }
16910
+ const result = isRealpathInside(target.rootPath, target.absolutePath);
16911
+ if (result.error) {
16912
+ return "error";
16913
+ }
16914
+ return result.contained ? "safe" : "forbidden";
16915
+ }
16916
+ function isPackageManaged(method) {
16917
+ return method === "npm" || method === "pnpm" || method === "bun" || method === "yarn";
16918
+ }
16919
+ function getPackageManagerCommand(method) {
16920
+ switch (method) {
16921
+ case "npm":
16922
+ return "npm uninstall -g ocx";
16923
+ case "pnpm":
16924
+ return "pnpm remove -g ocx";
16925
+ case "bun":
16926
+ return "bun remove -g ocx";
16927
+ case "yarn":
16928
+ return "yarn global remove ocx";
16929
+ default:
16930
+ return "npm uninstall -g ocx";
16931
+ }
16932
+ }
16933
+ function getGlobalConfigRoot() {
16934
+ const base = process.env.XDG_CONFIG_HOME || path9.join(homedir4(), ".config");
16935
+ return path9.join(base, "opencode");
16936
+ }
16937
+ function buildConfigTargets() {
16938
+ const rootPath = getGlobalConfigRoot();
16939
+ const targets = [];
16940
+ const profilesDir = getProfilesDir();
16941
+ const profilesRelative = getRelativePathIfContained(rootPath, profilesDir);
16942
+ if (profilesRelative) {
16943
+ const kind = getPathKind(profilesDir);
16944
+ targets.push({
16945
+ rootPath,
16946
+ relativePath: profilesRelative,
16947
+ absolutePath: profilesDir,
16948
+ displayPath: tildify(profilesDir),
16949
+ kind,
16950
+ deleteIfEmpty: false,
16951
+ safetyStatus: classifyTargetSafety({ rootPath, absolutePath: profilesDir, kind })
16952
+ });
16953
+ }
16954
+ const globalConfig = getGlobalConfig();
16955
+ const configRelative = getRelativePathIfContained(rootPath, globalConfig);
16956
+ if (configRelative) {
16957
+ const kind = getPathKind(globalConfig);
16958
+ targets.push({
16959
+ rootPath,
16960
+ relativePath: configRelative,
16961
+ absolutePath: globalConfig,
16962
+ displayPath: tildify(globalConfig),
16963
+ kind,
16964
+ deleteIfEmpty: false,
16965
+ safetyStatus: classifyTargetSafety({ rootPath, absolutePath: globalConfig, kind })
16966
+ });
16967
+ }
16968
+ const rootKind = getPathKind(rootPath);
16969
+ targets.push({
16970
+ rootPath,
16971
+ relativePath: ".",
16972
+ absolutePath: rootPath,
16973
+ displayPath: tildify(rootPath),
16974
+ kind: rootKind,
16975
+ deleteIfEmpty: true,
16976
+ safetyStatus: rootKind === "missing" ? "safe" : "safe"
16977
+ });
16978
+ return targets;
16979
+ }
16980
+ function buildBinaryTarget() {
16981
+ const method = detectInstallMethod();
16982
+ if (isPackageManaged(method)) {
16983
+ return null;
16984
+ }
16985
+ if (method === "curl") {
16986
+ const binaryPath = getExecutablePath();
16987
+ const kind = getPathKind(binaryPath);
16988
+ const parentDir = path9.dirname(binaryPath);
16989
+ return {
16990
+ rootPath: parentDir,
16991
+ relativePath: path9.basename(binaryPath),
16992
+ absolutePath: binaryPath,
16993
+ displayPath: tildify(binaryPath),
16994
+ kind,
16995
+ deleteIfEmpty: false,
16996
+ safetyStatus: kind === "missing" ? "safe" : "safe"
16997
+ };
16998
+ }
16999
+ return null;
17000
+ }
17001
+ function executeRemoval(target) {
17002
+ if (target.kind === "missing") {
17003
+ return { target, success: true, skipped: true, reason: "not found" };
17004
+ }
17005
+ if (target.safetyStatus === "forbidden") {
17006
+ return {
17007
+ target,
17008
+ success: false,
17009
+ skipped: true,
17010
+ reason: "containment violation",
17011
+ error: new Error("Target escapes containment boundary")
17012
+ };
17013
+ }
17014
+ if (target.deleteIfEmpty && target.kind === "directory") {
17015
+ if (!isDirectoryEmpty(target.absolutePath)) {
17016
+ return { target, success: true, skipped: true, reason: "not empty" };
17017
+ }
17018
+ }
17019
+ try {
17020
+ if (target.kind === "directory") {
17021
+ rmSync2(target.absolutePath, { recursive: true, force: true });
17022
+ } else {
17023
+ unlinkSync2(target.absolutePath);
17024
+ }
17025
+ return { target, success: true, skipped: false };
17026
+ } catch (err) {
17027
+ const error = err instanceof Error ? err : new Error(String(err));
17028
+ const reason = isNodeError(err) && (err.code === "EACCES" || err.code === "EPERM") ? "permission denied" : undefined;
17029
+ return { target, success: false, skipped: false, reason, error };
17030
+ }
17031
+ }
17032
+ function executeRemovals(targets) {
17033
+ return targets.map(executeRemoval);
17034
+ }
17035
+ function removeBinary(binaryPath) {
17036
+ const target = {
17037
+ rootPath: path9.dirname(binaryPath),
17038
+ relativePath: path9.basename(binaryPath),
17039
+ absolutePath: binaryPath,
17040
+ displayPath: tildify(binaryPath),
17041
+ kind: getPathKind(binaryPath),
17042
+ deleteIfEmpty: false,
17043
+ safetyStatus: "safe"
17044
+ };
17045
+ if (target.kind === "missing") {
17046
+ return { target, success: true, skipped: true, reason: "not found" };
17047
+ }
17048
+ if (process.platform === "win32") {
17049
+ logger.info(`To complete uninstall, manually delete: ${target.displayPath}`);
17050
+ return { target, success: true, skipped: true };
17051
+ }
17052
+ try {
17053
+ unlinkSync2(binaryPath);
17054
+ return { target, success: true, skipped: false };
17055
+ } catch (error) {
17056
+ const err = error instanceof Error ? error : new Error(String(error));
17057
+ return { target, success: false, skipped: false, reason: "permission denied", error: err };
17058
+ }
17059
+ }
17060
+ function printDryRun(configTargets, binaryTarget, installMethod) {
17061
+ logger.info(`Dry run - the following would be removed:
17062
+ `);
17063
+ const existingConfigTargets = configTargets.filter((t2) => t2.kind !== "missing");
17064
+ for (const target of existingConfigTargets) {
17065
+ const kindLabel = target.kind === "directory" ? "[dir] " : "[file]";
17066
+ const emptyNote = target.deleteIfEmpty ? " (if empty)" : "";
17067
+ logger.log(` ${kindLabel} ${highlight.path(target.displayPath)}${emptyNote}`);
17068
+ }
17069
+ if (binaryTarget && binaryTarget.kind !== "missing") {
17070
+ logger.log(` [bin] ${highlight.path(binaryTarget.displayPath)}`);
17071
+ }
17072
+ if (isPackageManaged(installMethod)) {
17073
+ logger.log("");
17074
+ logger.info(`Binary managed by ${installMethod}. Run:`);
17075
+ logger.log(` ${highlight.command(getPackageManagerCommand(installMethod))}`);
17076
+ }
17077
+ if (existingConfigTargets.length === 0 && (!binaryTarget || binaryTarget.kind === "missing")) {
17078
+ logger.info("Nothing to remove.");
17079
+ }
17080
+ }
17081
+ function printRemovalPlan(configTargets, binaryTarget) {
17082
+ const existingConfigTargets = configTargets.filter((t2) => t2.kind !== "missing");
17083
+ if (existingConfigTargets.length > 0 || binaryTarget && binaryTarget.kind !== "missing") {
17084
+ logger.info("Removing OCX files...");
17085
+ }
17086
+ }
17087
+ function printResults(results, binaryResult, installMethod) {
17088
+ logger.break();
17089
+ for (const result of results) {
17090
+ if (result.skipped) {
17091
+ if (result.reason === "not found") {
17092
+ continue;
17093
+ }
17094
+ if (result.reason === "not empty") {
17095
+ logger.info(`Kept ${highlight.path(result.target.displayPath)} (not empty)`);
17096
+ continue;
17097
+ }
17098
+ if (result.reason === "permission denied") {
17099
+ logger.warn(`Skipped ${highlight.path(result.target.displayPath)} (permission denied)`);
17100
+ continue;
17101
+ }
17102
+ if (result.reason === "containment violation") {
17103
+ logger.warn(`Skipped ${highlight.path(result.target.displayPath)} (containment violation)`);
17104
+ continue;
17105
+ }
17106
+ }
17107
+ if (result.success) {
17108
+ logger.success(`Removed ${highlight.path(result.target.displayPath)}`);
17109
+ } else {
17110
+ logger.error(`Failed to remove ${result.target.displayPath}: ${result.error?.message}`);
17111
+ }
17112
+ }
17113
+ if (binaryResult) {
17114
+ if (binaryResult.skipped && binaryResult.reason === "not found") {} else if (binaryResult.success && !binaryResult.skipped) {
17115
+ logger.success(`Removed binary ${highlight.path(binaryResult.target.displayPath)}`);
17116
+ } else if (!binaryResult.success) {
17117
+ logger.error(`Failed to remove binary ${binaryResult.target.displayPath}: ${binaryResult.error?.message}`);
17118
+ }
17119
+ }
17120
+ if (isPackageManaged(installMethod)) {
17121
+ logger.break();
17122
+ logger.info(`Binary is managed by ${installMethod}. To complete uninstall, run:`);
17123
+ logger.log(` ${highlight.command(getPackageManagerCommand(installMethod))}`);
17124
+ }
17125
+ }
17126
+ function printNothingToRemove() {
17127
+ logger.info("Nothing to remove. OCX is not installed globally.");
17128
+ }
17129
+ async function runUninstall(options2) {
17130
+ const rootPath = getGlobalConfigRoot();
17131
+ const rootValidation = validateRootDirectory(rootPath);
17132
+ if (!rootValidation.valid) {
17133
+ switch (rootValidation.reason) {
17134
+ case "not-found":
17135
+ break;
17136
+ case "symlink":
17137
+ logger.error("Safety error: Global config root is a symlink. Aborting.");
17138
+ process.exit(UNINSTALL_EXIT_CODES.SAFETY_ERROR);
17139
+ break;
17140
+ case "not-directory":
17141
+ logger.error("Safety error: Global config root is not a directory. Aborting.");
17142
+ process.exit(UNINSTALL_EXIT_CODES.SAFETY_ERROR);
17143
+ break;
17144
+ case "permission":
17145
+ logger.error("Error: Cannot access global config root (permission denied).");
17146
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17147
+ break;
17148
+ }
17149
+ }
17150
+ const configTargets = buildConfigTargets();
17151
+ const forbiddenTargets = configTargets.filter((t2) => t2.safetyStatus === "forbidden");
17152
+ if (forbiddenTargets.length > 0) {
17153
+ logger.error("Safety error: Target escapes containment boundary:");
17154
+ for (const target of forbiddenTargets) {
17155
+ logger.error(` ${target.displayPath}`);
17156
+ }
17157
+ process.exit(UNINSTALL_EXIT_CODES.SAFETY_ERROR);
17158
+ }
17159
+ const errorTargets = configTargets.filter((t2) => t2.safetyStatus === "error");
17160
+ if (errorTargets.length > 0) {
17161
+ logger.error("Error: Cannot verify containment for targets (permission/IO error):");
17162
+ for (const target of errorTargets) {
17163
+ logger.error(` ${target.displayPath}`);
17164
+ }
17165
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17166
+ }
17167
+ const installMethod = detectInstallMethod();
17168
+ const binaryTarget = buildBinaryTarget();
17169
+ const existingConfigTargets = configTargets.filter((t2) => t2.kind !== "missing");
17170
+ const hasBinary = binaryTarget && binaryTarget.kind !== "missing";
17171
+ const hasPackageManager = isPackageManaged(installMethod);
17172
+ if (existingConfigTargets.length === 0 && !hasBinary && !hasPackageManager) {
17173
+ printNothingToRemove();
17174
+ process.exit(UNINSTALL_EXIT_CODES.SUCCESS);
17175
+ }
17176
+ if (options2.dryRun) {
17177
+ printDryRun(configTargets, binaryTarget, installMethod);
17178
+ process.exit(UNINSTALL_EXIT_CODES.SUCCESS);
17179
+ }
17180
+ printRemovalPlan(configTargets, binaryTarget);
17181
+ const configResults = executeRemovals(configTargets);
17182
+ let binaryResult = null;
17183
+ if (binaryTarget) {
17184
+ binaryResult = removeBinary(binaryTarget.absolutePath);
17185
+ }
17186
+ printResults(configResults, binaryResult, installMethod);
17187
+ const hasFailures = configResults.some((r2) => !r2.success && !r2.skipped);
17188
+ const binaryFailed = binaryResult && !binaryResult.success && !binaryResult.skipped;
17189
+ if (hasFailures || binaryFailed) {
17190
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17191
+ }
17192
+ if (isPackageManaged(installMethod)) {
17193
+ process.exit(UNINSTALL_EXIT_CODES.ERROR);
17194
+ }
17195
+ process.exit(UNINSTALL_EXIT_CODES.SUCCESS);
17196
+ }
17197
+ function registerSelfUninstallCommand(parent) {
17198
+ parent.command("uninstall").description("Remove OCX global configuration and binary").option("--dry-run", "Preview what would be removed").action(wrapAction(async (options2) => {
17199
+ await runUninstall(options2);
17200
+ }));
17201
+ }
17202
+
16520
17203
  // src/self-update/version-provider.ts
16521
17204
  class BuildTimeVersionProvider {
16522
- version = "1.4.2";
17205
+ version = "1.4.4";
16523
17206
  }
16524
17207
  var defaultVersionProvider = new BuildTimeVersionProvider;
16525
17208
 
@@ -16582,57 +17265,8 @@ async function checkForUpdate(versionProvider, timeoutMs = VERSION_CHECK_TIMEOUT
16582
17265
  }
16583
17266
  }
16584
17267
 
16585
- // src/self-update/detect-method.ts
16586
- function parseInstallMethod(input) {
16587
- const VALID_METHODS = ["curl", "npm", "yarn", "pnpm", "bun"];
16588
- const method = VALID_METHODS.find((m) => m === input);
16589
- if (!method) {
16590
- throw new SelfUpdateError(`Invalid install method: "${input}"
16591
- Valid methods: ${VALID_METHODS.join(", ")}`);
16592
- }
16593
- return method;
16594
- }
16595
- var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
16596
- var isTempExecution = (path8) => path8.includes("/_npx/") || path8.includes("/.cache/bunx/") || path8.includes("/.pnpm/_temp/");
16597
- var isYarnGlobalInstall = (path8) => path8.includes("/.yarn/global") || path8.includes("/.config/yarn/global");
16598
- var isPnpmGlobalInstall = (path8) => path8.includes("/.pnpm/") || path8.includes("/pnpm/global");
16599
- var isBunGlobalInstall = (path8) => path8.includes("/.bun/bin") || path8.includes("/.bun/install/global");
16600
- var isNpmGlobalInstall = (path8) => path8.includes("/.npm/") || path8.includes("/node_modules/");
16601
- function detectInstallMethod() {
16602
- if (isCompiledBinary()) {
16603
- return "curl";
16604
- }
16605
- const scriptPath = process.argv[1] ?? "";
16606
- if (isTempExecution(scriptPath))
16607
- return "unknown";
16608
- if (isYarnGlobalInstall(scriptPath))
16609
- return "yarn";
16610
- if (isPnpmGlobalInstall(scriptPath))
16611
- return "pnpm";
16612
- if (isBunGlobalInstall(scriptPath))
16613
- return "bun";
16614
- if (isNpmGlobalInstall(scriptPath))
16615
- return "npm";
16616
- const userAgent = process.env.npm_config_user_agent ?? "";
16617
- if (userAgent.includes("yarn"))
16618
- return "yarn";
16619
- if (userAgent.includes("pnpm"))
16620
- return "pnpm";
16621
- if (userAgent.includes("bun"))
16622
- return "bun";
16623
- if (userAgent.includes("npm"))
16624
- return "npm";
16625
- return "unknown";
16626
- }
16627
- function getExecutablePath() {
16628
- if (typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/")) {
16629
- return process.execPath;
16630
- }
16631
- return process.argv[1] ?? process.execPath;
16632
- }
16633
-
16634
17268
  // src/self-update/download.ts
16635
- import { chmodSync, existsSync as existsSync11, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
17269
+ import { chmodSync, existsSync as existsSync12, renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
16636
17270
  var GITHUB_REPO2 = "kdcokenny/ocx";
16637
17271
  var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
16638
17272
  var PLATFORM_MAP = {
@@ -16710,8 +17344,8 @@ async function downloadToTemp(version) {
16710
17344
  try {
16711
17345
  chmodSync(tempPath, 493);
16712
17346
  } catch (error) {
16713
- if (existsSync11(tempPath)) {
16714
- unlinkSync2(tempPath);
17347
+ if (existsSync12(tempPath)) {
17348
+ unlinkSync3(tempPath);
16715
17349
  }
16716
17350
  throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
16717
17351
  }
@@ -16720,31 +17354,31 @@ async function downloadToTemp(version) {
16720
17354
  function atomicReplace(tempPath, execPath) {
16721
17355
  const backupPath = `${execPath}.backup`;
16722
17356
  try {
16723
- if (existsSync11(execPath)) {
17357
+ if (existsSync12(execPath)) {
16724
17358
  renameSync2(execPath, backupPath);
16725
17359
  }
16726
17360
  renameSync2(tempPath, execPath);
16727
- if (existsSync11(backupPath)) {
16728
- unlinkSync2(backupPath);
17361
+ if (existsSync12(backupPath)) {
17362
+ unlinkSync3(backupPath);
16729
17363
  }
16730
17364
  } catch (error) {
16731
- if (existsSync11(backupPath) && !existsSync11(execPath)) {
17365
+ if (existsSync12(backupPath) && !existsSync12(execPath)) {
16732
17366
  try {
16733
17367
  renameSync2(backupPath, execPath);
16734
17368
  } catch {}
16735
17369
  }
16736
- if (existsSync11(tempPath)) {
17370
+ if (existsSync12(tempPath)) {
16737
17371
  try {
16738
- unlinkSync2(tempPath);
17372
+ unlinkSync3(tempPath);
16739
17373
  } catch {}
16740
17374
  }
16741
17375
  throw new SelfUpdateError(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
16742
17376
  }
16743
17377
  }
16744
17378
  function cleanupTempFile(tempPath) {
16745
- if (existsSync11(tempPath)) {
17379
+ if (existsSync12(tempPath)) {
16746
17380
  try {
16747
- unlinkSync2(tempPath);
17381
+ unlinkSync3(tempPath);
16748
17382
  } catch {}
16749
17383
  }
16750
17384
  }
@@ -16913,12 +17547,13 @@ function registerSelfUpdateCommand(parent) {
16913
17547
  // src/commands/self/index.ts
16914
17548
  function registerSelfCommand(program2) {
16915
17549
  const self = program2.command("self").description("Manage the OCX CLI");
17550
+ registerSelfUninstallCommand(self);
16916
17551
  registerSelfUpdateCommand(self);
16917
17552
  }
16918
17553
 
16919
17554
  // src/commands/update.ts
16920
17555
  import { createHash as createHash4 } from "crypto";
16921
- import { existsSync as existsSync12 } from "fs";
17556
+ import { existsSync as existsSync13 } from "fs";
16922
17557
  import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
16923
17558
  import { dirname as dirname6, join as join10 } from "path";
16924
17559
  function registerUpdateCommand(program2) {
@@ -17060,7 +17695,7 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
17060
17695
  continue;
17061
17696
  const targetPath = join10(provider.cwd, fileObj.target);
17062
17697
  const targetDir = dirname6(targetPath);
17063
- if (!existsSync12(targetDir)) {
17698
+ if (!existsSync13(targetDir)) {
17064
17699
  await mkdir9(targetDir, { recursive: true });
17065
17700
  }
17066
17701
  await writeFile4(targetPath, file.content);
@@ -17209,8 +17844,8 @@ function shouldCheckForUpdate() {
17209
17844
  return true;
17210
17845
  }
17211
17846
  function registerUpdateCheckHook(program2) {
17212
- program2.hook("postAction", async (thisCommand) => {
17213
- if (thisCommand.name() === "update" && thisCommand.parent?.name() === "self") {
17847
+ program2.hook("postAction", async (_thisCommand, actionCommand) => {
17848
+ if (actionCommand.name() === "update" && actionCommand.parent?.name() === "self") {
17214
17849
  return;
17215
17850
  }
17216
17851
  if (!shouldCheckForUpdate())
@@ -17224,7 +17859,7 @@ function registerUpdateCheckHook(program2) {
17224
17859
  });
17225
17860
  }
17226
17861
  // src/index.ts
17227
- var version = "1.4.2";
17862
+ var version = "1.4.4";
17228
17863
  async function main2() {
17229
17864
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
17230
17865
  registerInitCommand(program2);
@@ -17256,4 +17891,4 @@ export {
17256
17891
  buildRegistry
17257
17892
  };
17258
17893
 
17259
- //# debugId=52AD8D268EBA9E8864756E2164756E21
17894
+ //# debugId=C29AE4337E16008A64756E2164756E21