ocx 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9824,7 +9824,8 @@ var EXIT_CODES = {
9824
9824
  NOT_FOUND: 66,
9825
9825
  NETWORK: 69,
9826
9826
  CONFIG: 78,
9827
- INTEGRITY: 1
9827
+ INTEGRITY: 1,
9828
+ CONFLICT: 6
9828
9829
  };
9829
9830
 
9830
9831
  class OCXError extends Error {
@@ -9868,7 +9869,7 @@ class ValidationError extends OCXError {
9868
9869
 
9869
9870
  class ConflictError extends OCXError {
9870
9871
  constructor(message) {
9871
- super(message, "CONFLICT", EXIT_CODES.GENERAL);
9872
+ super(message, "CONFLICT", EXIT_CODES.CONFLICT);
9872
9873
  this.name = "ConflictError";
9873
9874
  }
9874
9875
  }
@@ -9909,11 +9910,32 @@ class ProfileNotFoundError extends OCXError {
9909
9910
 
9910
9911
  class ProfileExistsError extends OCXError {
9911
9912
  constructor(name) {
9912
- super(`Profile "${name}" already exists`, "CONFLICT", EXIT_CODES.GENERAL);
9913
+ super(`Profile "${name}" already exists. Use --force to overwrite.`, "CONFLICT", EXIT_CODES.CONFLICT);
9913
9914
  this.name = "ProfileExistsError";
9914
9915
  }
9915
9916
  }
9916
9917
 
9918
+ class RegistryExistsError extends OCXError {
9919
+ registryName;
9920
+ existingUrl;
9921
+ newUrl;
9922
+ targetLabel;
9923
+ constructor(registryName, existingUrl, newUrl, targetLabel) {
9924
+ const target = targetLabel ? ` in ${targetLabel}` : "";
9925
+ const message = `Registry "${registryName}" already exists${target}.
9926
+ ` + ` Current: ${existingUrl}
9927
+ ` + ` New: ${newUrl}
9928
+
9929
+ ` + `Use --force to overwrite.`;
9930
+ super(message, "CONFLICT", EXIT_CODES.CONFLICT);
9931
+ this.registryName = registryName;
9932
+ this.existingUrl = existingUrl;
9933
+ this.newUrl = newUrl;
9934
+ this.targetLabel = targetLabel;
9935
+ this.name = "RegistryExistsError";
9936
+ }
9937
+ }
9938
+
9917
9939
  class InvalidProfileNameError extends OCXError {
9918
9940
  constructor(name, reason) {
9919
9941
  super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
@@ -9923,7 +9945,7 @@ class InvalidProfileNameError extends OCXError {
9923
9945
 
9924
9946
  class ProfilesNotInitializedError extends OCXError {
9925
9947
  constructor() {
9926
- super("Profiles not initialized. Run 'ocx profile add default' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
9948
+ super("Profiles not initialized. Run 'ocx init --global' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
9927
9949
  this.name = "ProfilesNotInitializedError";
9928
9950
  }
9929
9951
  }
@@ -10399,7 +10421,7 @@ class GlobalConfigProvider {
10399
10421
  static async requireInitialized() {
10400
10422
  const basePath = getGlobalConfigPath();
10401
10423
  if (!await globalDirectoryExists()) {
10402
- throw new ConfigError("Global config not found. Run 'opencode' once to initialize, then retry.");
10424
+ throw new ConfigError("Global config not found. Run 'ocx init --global' first.");
10403
10425
  }
10404
10426
  const config = await readOcxConfig(basePath);
10405
10427
  return new GlobalConfigProvider(basePath, config);
@@ -10626,7 +10648,25 @@ class ProfileManager {
10626
10648
  const dir = getProfileDir(name);
10627
10649
  await mkdir2(dir, { recursive: true, mode: 448 });
10628
10650
  const ocxPath = getProfileOcxConfig(name);
10629
- await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
10651
+ const ocxFile = Bun.file(ocxPath);
10652
+ if (!await ocxFile.exists()) {
10653
+ await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
10654
+ }
10655
+ const opencodePath = getProfileOpencodeConfig(name);
10656
+ const opencodeFile = Bun.file(opencodePath);
10657
+ if (!await opencodeFile.exists()) {
10658
+ await atomicWrite(opencodePath, {});
10659
+ }
10660
+ const agentsPath = getProfileAgents(name);
10661
+ const agentsFile = Bun.file(agentsPath);
10662
+ if (!await agentsFile.exists()) {
10663
+ const agentsContent = `# Profile Instructions
10664
+
10665
+ <!-- Add your custom instructions for this profile here -->
10666
+ <!-- These will be included when running \`ocx opencode -p ${name}\` -->
10667
+ `;
10668
+ await Bun.write(agentsPath, agentsContent, { mode: 384 });
10669
+ }
10630
10670
  }
10631
10671
  async remove(name) {
10632
10672
  if (!await this.exists(name)) {
@@ -10765,11 +10805,13 @@ class ConfigResolver {
10765
10805
  }
10766
10806
  }
10767
10807
  const shouldLoadLocal = this.shouldLoadLocalConfig();
10768
- if (shouldLoadLocal && this.localConfigDir) {
10808
+ if (!this.profile && shouldLoadLocal && this.localConfigDir) {
10769
10809
  const localOcxConfig = this.loadLocalOcxConfig();
10770
10810
  if (localOcxConfig) {
10771
- registries = { ...registries, ...localOcxConfig.registries };
10811
+ registries = localOcxConfig.registries;
10772
10812
  }
10813
+ }
10814
+ if (shouldLoadLocal && this.localConfigDir) {
10773
10815
  const localOpencodeConfig = this.loadLocalOpencodeConfig();
10774
10816
  if (localOpencodeConfig) {
10775
10817
  opencode = this.deepMerge(opencode, localOpencodeConfig);
@@ -10810,7 +10852,7 @@ class ConfigResolver {
10810
10852
  }
10811
10853
  }
10812
10854
  const shouldLoadLocal = this.shouldLoadLocalConfig();
10813
- if (shouldLoadLocal && this.localConfigDir) {
10855
+ if (!this.profile && shouldLoadLocal && this.localConfigDir) {
10814
10856
  const localOcxConfig = this.loadLocalOcxConfig();
10815
10857
  if (localOcxConfig) {
10816
10858
  const localOcxPath = join2(this.localConfigDir, OCX_CONFIG_FILE);
@@ -10819,6 +10861,8 @@ class ConfigResolver {
10819
10861
  origins.set(`registries.${key}`, { path: localOcxPath, source: "local-config" });
10820
10862
  }
10821
10863
  }
10864
+ }
10865
+ if (shouldLoadLocal && this.localConfigDir) {
10822
10866
  const localOpencodeConfig = this.loadLocalOpencodeConfig();
10823
10867
  if (localOpencodeConfig) {
10824
10868
  opencode = this.deepMerge(opencode, localOpencodeConfig);
@@ -10938,7 +10982,7 @@ class ConfigResolver {
10938
10982
  // package.json
10939
10983
  var package_default = {
10940
10984
  name: "ocx",
10941
- version: "1.4.1",
10985
+ version: "1.4.2",
10942
10986
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10943
10987
  author: "kdcokenny",
10944
10988
  license: "MIT",
@@ -11632,6 +11676,25 @@ function wrapAction(action) {
11632
11676
  };
11633
11677
  }
11634
11678
  function formatErrorAsJson(error) {
11679
+ if (error instanceof RegistryExistsError) {
11680
+ return {
11681
+ success: false,
11682
+ error: {
11683
+ code: error.code,
11684
+ message: error.message,
11685
+ details: {
11686
+ registryName: error.registryName,
11687
+ existingUrl: error.existingUrl,
11688
+ newUrl: error.newUrl,
11689
+ ...error.targetLabel && { targetLabel: error.targetLabel }
11690
+ }
11691
+ },
11692
+ exitCode: error.exitCode,
11693
+ meta: {
11694
+ timestamp: new Date().toISOString()
11695
+ }
11696
+ };
11697
+ }
11635
11698
  if (error instanceof OCXError) {
11636
11699
  return {
11637
11700
  success: false,
@@ -11695,6 +11758,7 @@ var sharedOptions = {
11695
11758
  cwd: () => new Option("--cwd <path>", "Working directory").default(process.cwd()),
11696
11759
  quiet: () => new Option("-q, --quiet", "Suppress output"),
11697
11760
  json: () => new Option("--json", "Output as JSON"),
11761
+ profile: () => new Option("-p, --profile <name>", "Target a specific profile's config"),
11698
11762
  force: () => new Option("-f, --force", "Skip confirmation prompts"),
11699
11763
  verbose: () => new Option("-v, --verbose", "Verbose output"),
11700
11764
  global: new Option("-g, --global", "Install to global OpenCode config (~/.config/opencode)")
@@ -11711,6 +11775,20 @@ function addVerboseOption(cmd) {
11711
11775
  function addGlobalOption(cmd) {
11712
11776
  return cmd.addOption(sharedOptions.global);
11713
11777
  }
11778
+ function addProfileOption(cmd) {
11779
+ return cmd.addOption(sharedOptions.profile());
11780
+ }
11781
+ function validateProfileName(name) {
11782
+ if (!name || name.length === 0) {
11783
+ throw new InvalidProfileNameError(name, "cannot be empty");
11784
+ }
11785
+ if (name.length > 32) {
11786
+ throw new InvalidProfileNameError(name, "must be 32 characters or less");
11787
+ }
11788
+ if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
11789
+ throw new InvalidProfileNameError(name, "must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens");
11790
+ }
11791
+ }
11714
11792
  // ../../node_modules/.bun/ora@8.2.0/node_modules/ora/index.js
11715
11793
  import process9 from "process";
11716
11794
 
@@ -13861,7 +13939,7 @@ import { existsSync as existsSync6 } from "fs";
13861
13939
  import { mkdir as mkdir6 } from "fs/promises";
13862
13940
  import { join as join6 } from "path";
13863
13941
  function registerConfigEditCommand(parent) {
13864
- parent.command("edit").description("Open configuration file in editor").option("-g, --global", "Edit global ocx.jsonc").action(async (options2) => {
13942
+ parent.command("edit").description("Open configuration file in editor").option("-g, --global", "Edit global ocx.jsonc").option("-p, --profile <name>", "Edit specific profile's config").action(async (options2) => {
13865
13943
  try {
13866
13944
  await runConfigEdit(options2);
13867
13945
  } catch (error) {
@@ -13870,12 +13948,26 @@ function registerConfigEditCommand(parent) {
13870
13948
  });
13871
13949
  }
13872
13950
  async function runConfigEdit(options2) {
13951
+ if (options2.global && options2.profile) {
13952
+ throw new ValidationError("Cannot use both --global and --profile flags");
13953
+ }
13873
13954
  let configPath;
13874
- if (options2.global) {
13955
+ if (options2.profile) {
13956
+ const parseResult = profileNameSchema.safeParse(options2.profile);
13957
+ if (!parseResult.success) {
13958
+ throw new ValidationError(`Invalid profile name "${options2.profile}": ${parseResult.error.errors[0]?.message ?? "Invalid name"}`);
13959
+ }
13960
+ await ProfileManager.requireInitialized();
13961
+ const manager = ProfileManager.create();
13962
+ if (!await manager.exists(options2.profile)) {
13963
+ throw new ProfileNotFoundError(options2.profile);
13964
+ }
13965
+ configPath = getProfileOcxConfig(options2.profile);
13966
+ } else if (options2.global) {
13875
13967
  configPath = getGlobalConfig();
13876
13968
  if (!existsSync6(configPath)) {
13877
13969
  throw new ConfigError(`Global config not found at ${configPath}.
13878
- ` + "Run 'opencode' once to initialize, then retry.");
13970
+ Run 'ocx init --global' first.`);
13879
13971
  }
13880
13972
  } else {
13881
13973
  const localConfigDir = findLocalConfigDir(process.cwd());
@@ -15291,34 +15383,90 @@ async function runInit(options2) {
15291
15383
  }
15292
15384
  }
15293
15385
  async function runInitGlobal(options2) {
15294
- const manager = ProfileManager.create();
15295
- if (await manager.isInitialized()) {
15296
- const profilesDir = getProfilesDir();
15297
- throw new ProfileExistsError(`Global profiles already initialized at ${profilesDir}`);
15298
- }
15299
15386
  const spin = options2.quiet ? null : createSpinner({ text: "Initializing global profiles..." });
15300
15387
  spin?.start();
15301
15388
  try {
15302
- await manager.initialize();
15389
+ const created = [];
15390
+ const existed = [];
15391
+ const globalConfigPath = getGlobalConfig();
15392
+ if (existsSync8(globalConfigPath)) {
15393
+ existed.push("globalConfig");
15394
+ } else {
15395
+ await mkdir7(dirname3(globalConfigPath), { recursive: true, mode: 448 });
15396
+ await atomicWrite(globalConfigPath, {
15397
+ $schema: OCX_SCHEMA_URL,
15398
+ registries: {}
15399
+ });
15400
+ created.push("globalConfig");
15401
+ }
15303
15402
  const profilesDir = getProfilesDir();
15304
- const ocxConfigPath = getProfileOcxConfig("default");
15403
+ if (!existsSync8(profilesDir)) {
15404
+ await mkdir7(profilesDir, { recursive: true, mode: 448 });
15405
+ }
15406
+ const profileDir = getProfileDir("default");
15407
+ if (!existsSync8(profileDir)) {
15408
+ await mkdir7(profileDir, { recursive: true, mode: 448 });
15409
+ }
15410
+ const ocxPath = getProfileOcxConfig("default");
15411
+ if (existsSync8(ocxPath)) {
15412
+ existed.push("profileOcx");
15413
+ } else {
15414
+ await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
15415
+ created.push("profileOcx");
15416
+ }
15417
+ const opencodePath = getProfileOpencodeConfig("default");
15418
+ if (existsSync8(opencodePath)) {
15419
+ existed.push("profileOpencode");
15420
+ } else {
15421
+ await atomicWrite(opencodePath, {});
15422
+ created.push("profileOpencode");
15423
+ }
15424
+ const agentsPath = getProfileAgents("default");
15425
+ if (existsSync8(agentsPath)) {
15426
+ existed.push("profileAgents");
15427
+ } else {
15428
+ const agentsContent = `# Profile Instructions
15429
+
15430
+ <!-- Add your custom instructions for this profile here -->
15431
+ <!-- These will be included when running \`ocx opencode -p default\` -->
15432
+ `;
15433
+ await Bun.write(agentsPath, agentsContent, { mode: 384 });
15434
+ created.push("profileAgents");
15435
+ }
15305
15436
  spin?.succeed("Initialized global profiles");
15306
15437
  if (options2.json) {
15307
15438
  console.log(JSON.stringify({
15308
15439
  success: true,
15309
- profilesDir,
15310
- defaultProfile: "default",
15311
- ocxConfigPath
15440
+ files: {
15441
+ globalConfig: globalConfigPath,
15442
+ profileOcx: ocxPath,
15443
+ profileOpencode: opencodePath,
15444
+ profileAgents: agentsPath
15445
+ },
15446
+ created,
15447
+ existed
15312
15448
  }));
15313
15449
  } else if (!options2.quiet) {
15314
- logger.info(`Created ${profilesDir}`);
15315
- logger.info(`Created profile "default"`);
15316
- logger.info("");
15317
- logger.info("Next steps:");
15318
- logger.info(" 1. Edit your profile config: ocx profile config");
15319
- logger.info(" 2. Add registries: ocx registry add <url> --profile default");
15320
- logger.info(" 3. Launch OpenCode: ocx opencode");
15321
- logger.info(" 4. Create more profiles: ocx profile add <name>");
15450
+ if (created.length > 0) {
15451
+ for (const key of created) {
15452
+ if (key === "globalConfig")
15453
+ logger.info(`Created global config: ${globalConfigPath}`);
15454
+ if (key === "profileOcx")
15455
+ logger.info(`Created profile config: ${ocxPath}`);
15456
+ if (key === "profileOpencode")
15457
+ logger.info(`Created profile opencode config: ${opencodePath}`);
15458
+ if (key === "profileAgents")
15459
+ logger.info(`Created profile instructions: ${agentsPath}`);
15460
+ }
15461
+ logger.info("");
15462
+ logger.info("Next steps:");
15463
+ logger.info(" 1. Edit your profile config: ocx config edit -p default");
15464
+ logger.info(" 2. Add registries: ocx registry add <url> --name <name> --global");
15465
+ logger.info(" 3. Launch OpenCode: ocx opencode");
15466
+ logger.info(" 4. Create more profiles: ocx profile add <name>");
15467
+ } else {
15468
+ logger.info("Global profiles already initialized (all files exist)");
15469
+ }
15322
15470
  }
15323
15471
  } catch (error) {
15324
15472
  spin?.fail("Failed to initialize");
@@ -15346,7 +15494,7 @@ async function runInitRegistry(directory, options2) {
15346
15494
  await mkdir7(cwd, { recursive: true });
15347
15495
  await copyDir(options2.local, cwd);
15348
15496
  } else {
15349
- const version = options2.canary ? "main" : await getLatestVersion();
15497
+ const version = options2.canary ? "main" : getReleaseTag();
15350
15498
  await fetchAndExtractTemplate(cwd, version, options2.verbose);
15351
15499
  }
15352
15500
  if (spin)
@@ -15375,15 +15523,16 @@ async function runInitRegistry(directory, options2) {
15375
15523
  async function copyDir(src, dest) {
15376
15524
  await cp(src, dest, { recursive: true });
15377
15525
  }
15378
- async function getLatestVersion() {
15379
- const pkgPath = new URL("../../package.json", import.meta.url);
15380
- const pkgContent = await readFile(pkgPath);
15381
- const pkg = JSON.parse(pkgContent.toString());
15382
- return `v${pkg.version}`;
15526
+ function getReleaseTag() {
15527
+ if (false) {}
15528
+ return `v${"1.4.2"}`;
15383
15529
  }
15384
- async function fetchAndExtractTemplate(destDir, version, verbose) {
15530
+ function getTemplateUrl(version) {
15385
15531
  const ref = version === "main" ? "heads/main" : `tags/${version}`;
15386
- const tarballUrl = `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
15532
+ return `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
15533
+ }
15534
+ async function fetchAndExtractTemplate(destDir, version, verbose) {
15535
+ const tarballUrl = getTemplateUrl(version);
15387
15536
  if (verbose) {
15388
15537
  logger.info(`Fetching ${tarballUrl}`);
15389
15538
  }
@@ -15492,6 +15641,18 @@ function formatTerminalName(cwd, profileName, gitInfo) {
15492
15641
  }
15493
15642
 
15494
15643
  // src/commands/opencode.ts
15644
+ function resolveOpenCodeBinary(opts) {
15645
+ return opts.configBin ?? opts.envBin ?? "opencode";
15646
+ }
15647
+ function buildOpenCodeEnv(opts) {
15648
+ return {
15649
+ ...opts.baseEnv,
15650
+ ...opts.disableProjectConfig && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
15651
+ ...opts.profileDir && { OPENCODE_CONFIG_DIR: opts.profileDir },
15652
+ ...opts.mergedConfig && { OPENCODE_CONFIG_CONTENT: JSON.stringify(opts.mergedConfig) },
15653
+ ...opts.profileName && { OCX_PROFILE: opts.profileName }
15654
+ };
15655
+ }
15495
15656
  function registerOpencodeCommand(program2) {
15496
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) => {
15497
15658
  try {
@@ -15546,17 +15707,20 @@ async function runOpencode(pathArg, args, options2) {
15546
15707
  const gitInfo = await getGitInfo(projectDir);
15547
15708
  setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
15548
15709
  }
15549
- const bin = ocxConfig?.bin ?? process.env.OPENCODE_BIN ?? "opencode";
15710
+ const bin = resolveOpenCodeBinary({
15711
+ configBin: ocxConfig?.bin,
15712
+ envBin: process.env.OPENCODE_BIN
15713
+ });
15550
15714
  proc = Bun.spawn({
15551
15715
  cmd: [bin, ...args],
15552
15716
  cwd: projectDir,
15553
- env: {
15554
- ...process.env,
15555
- OPENCODE_DISABLE_PROJECT_CONFIG: "true",
15556
- ...profileDir && { OPENCODE_CONFIG_DIR: profileDir },
15557
- ...configToPass && { OPENCODE_CONFIG_CONTENT: JSON.stringify(configToPass) },
15558
- ...config.profileName && { OCX_PROFILE: config.profileName }
15559
- },
15717
+ env: buildOpenCodeEnv({
15718
+ baseEnv: process.env,
15719
+ profileDir,
15720
+ profileName: config.profileName ?? undefined,
15721
+ mergedConfig: configToPass,
15722
+ disableProjectConfig: true
15723
+ }),
15560
15724
  stdin: "inherit",
15561
15725
  stdout: "inherit",
15562
15726
  stderr: "inherit"
@@ -15846,14 +16010,14 @@ async function requireGlobalRegistry(namespace) {
15846
16010
  throw new ConfigError(`Registry "${namespace}" is not configured globally.
15847
16011
 
15848
16012
  ` + `Profile installation requires global registry configuration.
15849
- ` + `Run: ocx registry add ${namespace} <url> --global`);
16013
+ ` + `Run: ocx registry add <url> --name ${namespace} --global`);
15850
16014
  }
15851
16015
  const registry = globalConfig.registries[namespace];
15852
16016
  if (!registry) {
15853
16017
  throw new ConfigError(`Registry "${namespace}" is not configured globally.
15854
16018
 
15855
16019
  ` + `Profile installation requires global registry configuration.
15856
- ` + `Run: ocx registry add ${namespace} <url> --global`);
16020
+ ` + `Run: ocx registry add <url> --name ${namespace} --global`);
15857
16021
  }
15858
16022
  return { config: globalConfig, registryUrl: registry.url };
15859
16023
  }
@@ -15931,33 +16095,6 @@ async function cloneFromLocalProfile(manager, name, sourceName, exists) {
15931
16095
  logger.success(`Created profile "${name}" (cloned from "${sourceName}")`);
15932
16096
  }
15933
16097
 
15934
- // src/commands/profile/config.ts
15935
- function registerProfileConfigCommand(parent) {
15936
- parent.command("config [name]").description("Open profile ocx.jsonc in editor").action(async (name) => {
15937
- try {
15938
- await runProfileConfig(name);
15939
- } catch (error) {
15940
- handleError(error);
15941
- }
15942
- });
15943
- }
15944
- async function runProfileConfig(name) {
15945
- const manager = await ProfileManager.requireInitialized();
15946
- const profileName = name ?? await manager.resolveProfile();
15947
- await manager.get(profileName);
15948
- const configPath = getProfileOcxConfig(profileName);
15949
- const editor = process.env.EDITOR || process.env.VISUAL || "vi";
15950
- const proc = Bun.spawn([editor, configPath], {
15951
- stdin: "inherit",
15952
- stdout: "inherit",
15953
- stderr: "inherit"
15954
- });
15955
- const exitCode = await proc.exited;
15956
- if (exitCode !== 0) {
15957
- throw new Error(`Editor exited with code ${exitCode}`);
15958
- }
15959
- }
15960
-
15961
16098
  // src/commands/profile/list.ts
15962
16099
  function registerProfileListCommand(parent) {
15963
16100
  parent.command("list").alias("ls").description("List all global profiles").addOption(sharedOptions.json()).action(async (options2) => {
@@ -16044,22 +16181,43 @@ function registerProfileCommand(program2) {
16044
16181
  registerProfileAddCommand(profile);
16045
16182
  registerProfileRemoveCommand(profile);
16046
16183
  registerProfileShowCommand(profile);
16047
- registerProfileConfigCommand(profile);
16048
16184
  }
16049
16185
 
16050
16186
  // src/commands/registry.ts
16187
+ import { existsSync as existsSync10 } from "fs";
16188
+ import { dirname as dirname5, join as join9 } from "path";
16051
16189
  async function runRegistryAddCore2(url, options2, callbacks) {
16052
16190
  if (callbacks.isLocked?.()) {
16053
16191
  throw new Error("Registries are locked. Cannot add.");
16054
16192
  }
16055
- const name = options2.name || new URL(url).hostname.replace(/\./g, "-");
16193
+ const trimmedUrl = url.trim();
16194
+ if (!trimmedUrl) {
16195
+ throw new ValidationError("Registry URL is required");
16196
+ }
16197
+ let derivedName;
16198
+ try {
16199
+ const parsed = new URL(trimmedUrl);
16200
+ if (!["http:", "https:"].includes(parsed.protocol)) {
16201
+ throw new ValidationError(`Invalid registry URL: ${trimmedUrl} (must use http or https)`);
16202
+ }
16203
+ derivedName = options2.name || parsed.hostname.replace(/\./g, "-");
16204
+ } catch (error) {
16205
+ if (error instanceof ValidationError)
16206
+ throw error;
16207
+ throw new ValidationError(`Invalid registry URL: ${trimmedUrl}`);
16208
+ }
16209
+ const name = derivedName;
16056
16210
  const registries = callbacks.getRegistries();
16211
+ const existingRegistry = registries[name];
16212
+ if (existingRegistry && !options2.force) {
16213
+ throw new RegistryExistsError(name, existingRegistry.url, trimmedUrl);
16214
+ }
16057
16215
  const isUpdate = name in registries;
16058
16216
  await callbacks.setRegistry(name, {
16059
- url,
16217
+ url: trimmedUrl,
16060
16218
  version: options2.version
16061
16219
  });
16062
- return { name, url, updated: isUpdate };
16220
+ return { name, url: trimmedUrl, updated: isUpdate };
16063
16221
  }
16064
16222
  async function runRegistryRemoveCore(name, callbacks) {
16065
16223
  if (callbacks.isLocked?.()) {
@@ -16082,43 +16240,69 @@ function runRegistryListCore(callbacks) {
16082
16240
  }));
16083
16241
  return { registries: list, locked };
16084
16242
  }
16243
+ async function resolveRegistryTarget(options2, command, cwd) {
16244
+ const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16245
+ if (options2.global && options2.profile) {
16246
+ throw new ValidationError("Cannot use both --global and --profile flags");
16247
+ }
16248
+ if (cwdExplicitlyProvided && options2.profile) {
16249
+ throw new ValidationError("Cannot use both --cwd and --profile flags");
16250
+ }
16251
+ if (options2.global && cwdExplicitlyProvided) {
16252
+ throw new ValidationError("Cannot use both --global and --cwd flags");
16253
+ }
16254
+ if (options2.profile) {
16255
+ validateProfileName(options2.profile);
16256
+ const manager = await ProfileManager.requireInitialized();
16257
+ if (!await manager.exists(options2.profile)) {
16258
+ throw new ProfileNotFoundError(options2.profile);
16259
+ }
16260
+ const configPath = getProfileOcxConfig(options2.profile);
16261
+ if (!existsSync10(configPath)) {
16262
+ throw new OcxConfigError(`Profile '${options2.profile}' has no ocx.jsonc. Run 'ocx profile config ${options2.profile}' to create it.`);
16263
+ }
16264
+ return {
16265
+ scope: "profile",
16266
+ configPath,
16267
+ configDir: dirname5(configPath),
16268
+ targetLabel: `profile '${options2.profile}' config`
16269
+ };
16270
+ }
16271
+ if (options2.global) {
16272
+ const configDir = getGlobalConfigPath();
16273
+ return {
16274
+ scope: "global",
16275
+ configPath: join9(configDir, "ocx.jsonc"),
16276
+ configDir,
16277
+ targetLabel: "global config"
16278
+ };
16279
+ }
16280
+ const found = findOcxConfig(cwd);
16281
+ return {
16282
+ scope: "local",
16283
+ configPath: found.path,
16284
+ configDir: found.exists ? dirname5(found.path) : join9(cwd, ".opencode"),
16285
+ targetLabel: "local config"
16286
+ };
16287
+ }
16085
16288
  function registerRegistryCommand(program2) {
16086
16289
  const registry = program2.command("registry").description("Manage registries");
16087
- const addCmd = registry.command("add").description("Add a registry").argument("<url>", "Registry URL").option("--name <name>", "Registry alias (defaults to hostname)").option("--version <version>", "Pin to specific version");
16290
+ const addCmd = registry.command("add").description("Add a registry").argument("<url>", "Registry URL").option("--name <name>", "Registry alias (defaults to hostname)").option("--version <version>", "Pin to specific version").option("-f, --force", "Overwrite existing registry");
16088
16291
  addGlobalOption(addCmd);
16292
+ addProfileOption(addCmd);
16089
16293
  addCommonOptions(addCmd);
16090
16294
  addCmd.action(async (url, options2, command) => {
16295
+ let target;
16091
16296
  try {
16092
- const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16093
- if (options2.global && cwdExplicitlyProvided) {
16094
- logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
16297
+ const cwd = options2.cwd ?? process.cwd();
16298
+ target = await resolveRegistryTarget(options2, command, cwd);
16299
+ const { configDir, configPath } = target;
16300
+ const config = await readOcxConfig(configDir);
16301
+ if (!config) {
16302
+ const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
16303
+ logger.error(`${target.targetLabel} not found. ${initHint}`);
16095
16304
  process.exit(1);
16096
16305
  }
16097
- let configDir;
16098
- let configPath;
16099
- const config = await (async () => {
16100
- if (options2.global) {
16101
- configDir = getGlobalConfigPath();
16102
- const found = findOcxConfig(configDir);
16103
- configPath = found.path;
16104
- const cfg = await readOcxConfig(configDir);
16105
- if (!cfg) {
16106
- logger.error("Global config not found. Run 'opencode' once to initialize.");
16107
- process.exit(1);
16108
- }
16109
- return cfg;
16110
- } else {
16111
- configDir = options2.cwd ?? process.cwd();
16112
- const found = findOcxConfig(configDir);
16113
- configPath = found.path;
16114
- const cfg = await readOcxConfig(configDir);
16115
- if (!cfg) {
16116
- logger.error("No ocx.jsonc found. Run 'ocx init' first.");
16117
- process.exit(1);
16118
- }
16119
- return cfg;
16120
- }
16121
- })();
16122
16306
  const result = await runRegistryAddCore2(url, options2, {
16123
16307
  getRegistries: () => config.registries,
16124
16308
  isLocked: () => config.lockRegistries ?? false,
@@ -16130,65 +16314,46 @@ function registerRegistryCommand(program2) {
16130
16314
  if (options2.json) {
16131
16315
  outputJson({ success: true, data: result });
16132
16316
  } else if (!options2.quiet) {
16133
- const location = options2.global ? "global config" : "local config";
16134
16317
  if (result.updated) {
16135
- logger.success(`Updated registry in ${location}: ${result.name} -> ${result.url}`);
16318
+ logger.success(`Updated registry in ${target.targetLabel}: ${result.name} -> ${result.url}`);
16136
16319
  } else {
16137
- logger.success(`Added registry to ${location}: ${result.name} -> ${result.url}`);
16320
+ logger.success(`Added registry to ${target.targetLabel}: ${result.name} -> ${result.url}`);
16138
16321
  }
16139
16322
  }
16140
16323
  } catch (error) {
16324
+ if (error instanceof RegistryExistsError && !error.targetLabel) {
16325
+ const enrichedError = new RegistryExistsError(error.registryName, error.existingUrl, error.newUrl, target?.targetLabel ?? "config");
16326
+ handleError(enrichedError, { json: options2.json });
16327
+ }
16141
16328
  handleError(error, { json: options2.json });
16142
16329
  }
16143
16330
  });
16144
16331
  const removeCmd = registry.command("remove").description("Remove a registry").argument("<name>", "Registry name");
16145
16332
  addGlobalOption(removeCmd);
16333
+ addProfileOption(removeCmd);
16146
16334
  addCommonOptions(removeCmd);
16147
16335
  removeCmd.action(async (name, options2, command) => {
16148
16336
  try {
16149
- const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16150
- if (options2.global && cwdExplicitlyProvided) {
16151
- logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
16337
+ const cwd = options2.cwd ?? process.cwd();
16338
+ const target = await resolveRegistryTarget(options2, command, cwd);
16339
+ const config = await readOcxConfig(target.configDir);
16340
+ if (!config) {
16341
+ const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
16342
+ logger.error(`${target.targetLabel} not found. ${initHint}`);
16152
16343
  process.exit(1);
16153
16344
  }
16154
- let configDir;
16155
- let configPath;
16156
- const config = await (async () => {
16157
- if (options2.global) {
16158
- configDir = getGlobalConfigPath();
16159
- const found = findOcxConfig(configDir);
16160
- configPath = found.path;
16161
- const cfg = await readOcxConfig(configDir);
16162
- if (!cfg) {
16163
- logger.error("Global config not found. Run 'opencode' once to initialize.");
16164
- process.exit(1);
16165
- }
16166
- return cfg;
16167
- } else {
16168
- configDir = options2.cwd ?? process.cwd();
16169
- const found = findOcxConfig(configDir);
16170
- configPath = found.path;
16171
- const cfg = await readOcxConfig(configDir);
16172
- if (!cfg) {
16173
- logger.error("No ocx.jsonc found. Run 'ocx init' first.");
16174
- process.exit(1);
16175
- }
16176
- return cfg;
16177
- }
16178
- })();
16179
16345
  const result = await runRegistryRemoveCore(name, {
16180
16346
  getRegistries: () => config.registries,
16181
16347
  isLocked: () => config.lockRegistries ?? false,
16182
16348
  removeRegistry: async (regName) => {
16183
16349
  delete config.registries[regName];
16184
- await writeOcxConfig(configDir, config, configPath);
16350
+ await writeOcxConfig(target.configDir, config, target.configPath);
16185
16351
  }
16186
16352
  });
16187
16353
  if (options2.json) {
16188
16354
  outputJson({ success: true, data: result });
16189
16355
  } else if (!options2.quiet) {
16190
- const location = options2.global ? "global config" : "local config";
16191
- logger.success(`Removed registry from ${location}: ${result.removed}`);
16356
+ logger.success(`Removed registry from ${target.targetLabel}: ${result.removed}`);
16192
16357
  }
16193
16358
  } catch (error) {
16194
16359
  handleError(error, { json: options2.json });
@@ -16196,36 +16361,18 @@ function registerRegistryCommand(program2) {
16196
16361
  });
16197
16362
  const listCmd = registry.command("list").description("List configured registries");
16198
16363
  addGlobalOption(listCmd);
16364
+ addProfileOption(listCmd);
16199
16365
  addCommonOptions(listCmd);
16200
16366
  listCmd.action(async (options2, command) => {
16201
16367
  try {
16202
- const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
16203
- if (options2.global && cwdExplicitlyProvided) {
16204
- logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
16205
- process.exit(1);
16206
- }
16207
- let configDir;
16208
- const config = await (async () => {
16209
- if (options2.global) {
16210
- configDir = getGlobalConfigPath();
16211
- const cfg = await readOcxConfig(configDir);
16212
- if (!cfg) {
16213
- logger.warn("Global config not found. Run 'opencode' once to initialize.");
16214
- return null;
16215
- }
16216
- return cfg;
16217
- } else {
16218
- configDir = options2.cwd ?? process.cwd();
16219
- const cfg = await readOcxConfig(configDir);
16220
- if (!cfg) {
16221
- logger.warn("No ocx.jsonc found. Run 'ocx init' first.");
16222
- return null;
16223
- }
16224
- return cfg;
16225
- }
16226
- })();
16227
- if (!config)
16368
+ const cwd = options2.cwd ?? process.cwd();
16369
+ const target = await resolveRegistryTarget(options2, command, cwd);
16370
+ const config = await readOcxConfig(target.configDir);
16371
+ if (!config) {
16372
+ const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
16373
+ logger.warn(`${target.targetLabel} not found. ${initHint}`);
16228
16374
  return;
16375
+ }
16229
16376
  const result = runRegistryListCore({
16230
16377
  getRegistries: () => config.registries,
16231
16378
  isLocked: () => config.lockRegistries ?? false
@@ -16236,7 +16383,8 @@ function registerRegistryCommand(program2) {
16236
16383
  if (result.registries.length === 0) {
16237
16384
  logger.info("No registries configured.");
16238
16385
  } else {
16239
- logger.info(`Configured registries${options2.global ? " (global)" : ""}${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
16386
+ const scopeLabel = target.scope === "global" ? " (global)" : target.scope === "profile" ? ` (profile '${options2.profile}')` : "";
16387
+ logger.info(`Configured registries${scopeLabel}${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
16240
16388
  for (const reg of result.registries) {
16241
16389
  console.log(` ${kleur_default.cyan(reg.name)}: ${reg.url} ${kleur_default.dim(`(${reg.version})`)}`);
16242
16390
  }
@@ -16371,7 +16519,7 @@ async function runSearchCore(query, options2, provider) {
16371
16519
 
16372
16520
  // src/self-update/version-provider.ts
16373
16521
  class BuildTimeVersionProvider {
16374
- version = "1.4.1";
16522
+ version = "1.4.2";
16375
16523
  }
16376
16524
  var defaultVersionProvider = new BuildTimeVersionProvider;
16377
16525
 
@@ -16484,7 +16632,7 @@ function getExecutablePath() {
16484
16632
  }
16485
16633
 
16486
16634
  // src/self-update/download.ts
16487
- import { chmodSync, existsSync as existsSync10, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
16635
+ import { chmodSync, existsSync as existsSync11, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
16488
16636
  var GITHUB_REPO2 = "kdcokenny/ocx";
16489
16637
  var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
16490
16638
  var PLATFORM_MAP = {
@@ -16562,7 +16710,7 @@ async function downloadToTemp(version) {
16562
16710
  try {
16563
16711
  chmodSync(tempPath, 493);
16564
16712
  } catch (error) {
16565
- if (existsSync10(tempPath)) {
16713
+ if (existsSync11(tempPath)) {
16566
16714
  unlinkSync2(tempPath);
16567
16715
  }
16568
16716
  throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
@@ -16572,20 +16720,20 @@ async function downloadToTemp(version) {
16572
16720
  function atomicReplace(tempPath, execPath) {
16573
16721
  const backupPath = `${execPath}.backup`;
16574
16722
  try {
16575
- if (existsSync10(execPath)) {
16723
+ if (existsSync11(execPath)) {
16576
16724
  renameSync2(execPath, backupPath);
16577
16725
  }
16578
16726
  renameSync2(tempPath, execPath);
16579
- if (existsSync10(backupPath)) {
16727
+ if (existsSync11(backupPath)) {
16580
16728
  unlinkSync2(backupPath);
16581
16729
  }
16582
16730
  } catch (error) {
16583
- if (existsSync10(backupPath) && !existsSync10(execPath)) {
16731
+ if (existsSync11(backupPath) && !existsSync11(execPath)) {
16584
16732
  try {
16585
16733
  renameSync2(backupPath, execPath);
16586
16734
  } catch {}
16587
16735
  }
16588
- if (existsSync10(tempPath)) {
16736
+ if (existsSync11(tempPath)) {
16589
16737
  try {
16590
16738
  unlinkSync2(tempPath);
16591
16739
  } catch {}
@@ -16594,7 +16742,7 @@ function atomicReplace(tempPath, execPath) {
16594
16742
  }
16595
16743
  }
16596
16744
  function cleanupTempFile(tempPath) {
16597
- if (existsSync10(tempPath)) {
16745
+ if (existsSync11(tempPath)) {
16598
16746
  try {
16599
16747
  unlinkSync2(tempPath);
16600
16748
  } catch {}
@@ -16770,9 +16918,9 @@ function registerSelfCommand(program2) {
16770
16918
 
16771
16919
  // src/commands/update.ts
16772
16920
  import { createHash as createHash4 } from "crypto";
16773
- import { existsSync as existsSync11 } from "fs";
16921
+ import { existsSync as existsSync12 } from "fs";
16774
16922
  import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
16775
- import { dirname as dirname5, join as join9 } from "path";
16923
+ import { dirname as dirname6, join as join10 } from "path";
16776
16924
  function registerUpdateCommand(program2) {
16777
16925
  program2.command("update [components...]").description("Update installed components (use @version suffix to pin, e.g., kdco/agents@1.2.0)").option("--all", "Update all installed components").option("--registry <name>", "Update all components from a specific registry").option("--dry-run", "Preview changes without applying").option("--cwd <path>", "Working directory", process.cwd()).option("-q, --quiet", "Suppress output").option("-v, --verbose", "Verbose output").option("--json", "Output as JSON").action(async (components, options2) => {
16778
16926
  try {
@@ -16910,9 +17058,9 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
16910
17058
  const fileObj = update.component.files.find((f) => f.path === file.path);
16911
17059
  if (!fileObj)
16912
17060
  continue;
16913
- const targetPath = join9(provider.cwd, fileObj.target);
16914
- const targetDir = dirname5(targetPath);
16915
- if (!existsSync11(targetDir)) {
17061
+ const targetPath = join10(provider.cwd, fileObj.target);
17062
+ const targetDir = dirname6(targetPath);
17063
+ if (!existsSync12(targetDir)) {
16916
17064
  await mkdir9(targetDir, { recursive: true });
16917
17065
  }
16918
17066
  await writeFile4(targetPath, file.content);
@@ -17076,7 +17224,7 @@ function registerUpdateCheckHook(program2) {
17076
17224
  });
17077
17225
  }
17078
17226
  // src/index.ts
17079
- var version = "1.4.1";
17227
+ var version = "1.4.2";
17080
17228
  async function main2() {
17081
17229
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
17082
17230
  registerInitCommand(program2);
@@ -17108,4 +17256,4 @@ export {
17108
17256
  buildRegistry
17109
17257
  };
17110
17258
 
17111
- //# debugId=4E10E39074E11B3A64756E2164756E21
17259
+ //# debugId=52AD8D268EBA9E8864756E2164756E21