ocx 1.0.21 → 1.1.0

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
@@ -9864,6 +9864,18 @@ function isAbsolutePath(p) {
9864
9864
  var safeRelativePathSchema = exports_external.string().refine((val) => !val.includes("\x00"), "Path cannot contain null bytes").refine((val) => !val.split(/[/\\]/).some((seg) => seg === ".."), "Path cannot contain '..'").refine((val) => !isAbsolutePath(val), "Path must be relative, not absolute");
9865
9865
 
9866
9866
  // src/schemas/registry.ts
9867
+ var npmSpecifierSchema = exports_external.string().refine((val) => val.startsWith("npm:"), {
9868
+ message: 'npm specifier must start with "npm:" prefix'
9869
+ }).refine((val) => {
9870
+ const remainder = val.slice(4);
9871
+ if (!remainder)
9872
+ return false;
9873
+ if (remainder.includes("..") || remainder.includes("/./"))
9874
+ return false;
9875
+ return true;
9876
+ }, {
9877
+ message: "Invalid npm specifier format"
9878
+ });
9867
9879
  var openCodeNameSchema = exports_external.string().min(1, "Name cannot be empty").max(64, "Name cannot exceed 64 characters").regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, {
9868
9880
  message: "Must be lowercase alphanumeric with single hyphen separators (e.g., 'my-component', 'my-plugin'). Cannot start/end with hyphen or have consecutive hyphens."
9869
9881
  });
@@ -10420,7 +10432,7 @@ class GhostConfigProvider {
10420
10432
  // package.json
10421
10433
  var package_default = {
10422
10434
  name: "ocx",
10423
- version: "1.0.21",
10435
+ version: "1.1.0",
10424
10436
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10425
10437
  author: "kdcokenny",
10426
10438
  license: "MIT",
@@ -11082,14 +11094,14 @@ var sharedOptions = {
11082
11094
  cwd: () => new Option("--cwd <path>", "Working directory").default(process.cwd()),
11083
11095
  quiet: () => new Option("-q, --quiet", "Suppress output"),
11084
11096
  json: () => new Option("--json", "Output as JSON"),
11085
- yes: () => new Option("-y, --yes", "Skip confirmation prompts"),
11097
+ force: () => new Option("-f, --force", "Skip confirmation prompts"),
11086
11098
  verbose: () => new Option("-v, --verbose", "Verbose output")
11087
11099
  };
11088
11100
  function addCommonOptions(cmd) {
11089
11101
  return cmd.addOption(sharedOptions.cwd()).addOption(sharedOptions.quiet()).addOption(sharedOptions.json());
11090
11102
  }
11091
- function addConfirmationOptions(cmd) {
11092
- return cmd.addOption(sharedOptions.yes());
11103
+ function addForceOption(cmd) {
11104
+ return cmd.addOption(sharedOptions.force());
11093
11105
  }
11094
11106
  function addVerboseOption(cmd) {
11095
11107
  return cmd.addOption(sharedOptions.verbose());
@@ -12486,11 +12498,154 @@ function warnCompatIssues(registryName, issues) {
12486
12498
  logger.log(formatCompatWarning(registryName, issue));
12487
12499
  }
12488
12500
  }
12501
+ // src/utils/npm-registry.ts
12502
+ var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
12503
+ var NPM_FETCH_TIMEOUT_MS = 30000;
12504
+ var NPM_NAME_REGEX = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/;
12505
+ var MAX_NAME_LENGTH = 214;
12506
+ function validateNpmPackageName(name) {
12507
+ if (!name) {
12508
+ throw new ValidationError("npm package name cannot be empty");
12509
+ }
12510
+ if (name.length > MAX_NAME_LENGTH) {
12511
+ throw new ValidationError(`npm package name exceeds maximum length of ${MAX_NAME_LENGTH} characters: \`${name}\``);
12512
+ }
12513
+ if (name.includes("..") || name.includes("/./") || name.startsWith("./")) {
12514
+ throw new ValidationError(`Invalid npm package name - path traversal detected: \`${name}\``);
12515
+ }
12516
+ if (!NPM_NAME_REGEX.test(name)) {
12517
+ throw new ValidationError(`Invalid npm package name: \`${name}\`. ` + "Must be lowercase, start with alphanumeric, and contain only letters, numbers, hyphens, dots, or underscores.");
12518
+ }
12519
+ }
12520
+ function parseNpmSpecifier(specifier) {
12521
+ if (!specifier?.trim()) {
12522
+ throw new ValidationError("npm specifier cannot be empty");
12523
+ }
12524
+ const trimmed = specifier.trim();
12525
+ if (!trimmed.startsWith("npm:")) {
12526
+ throw new ValidationError(`Invalid npm specifier: \`${specifier}\`. Must start with \`npm:\` prefix.`);
12527
+ }
12528
+ const remainder = trimmed.slice(4);
12529
+ if (!remainder) {
12530
+ throw new ValidationError(`Invalid npm specifier: \`${specifier}\`. Package name is required.`);
12531
+ }
12532
+ const lastAt = remainder.lastIndexOf("@");
12533
+ let name;
12534
+ let version;
12535
+ if (lastAt > 0) {
12536
+ const beforeAt = remainder.slice(0, lastAt);
12537
+ const afterAt = remainder.slice(lastAt + 1);
12538
+ if (beforeAt.includes("/") || !beforeAt.startsWith("@")) {
12539
+ name = beforeAt;
12540
+ version = afterAt || undefined;
12541
+ } else {
12542
+ throw new ValidationError(`Invalid npm specifier: \`${specifier}\`. Scoped packages must have format @scope/pkg.`);
12543
+ }
12544
+ } else {
12545
+ name = remainder;
12546
+ }
12547
+ validateNpmPackageName(name);
12548
+ return { type: "npm", name, version };
12549
+ }
12550
+ function isNpmSpecifier(input) {
12551
+ return input.trim().startsWith("npm:");
12552
+ }
12553
+ async function validateNpmPackage(packageName) {
12554
+ validateNpmPackageName(packageName);
12555
+ const encodedName = packageName.startsWith("@") ? `@${encodeURIComponent(packageName.slice(1))}` : encodeURIComponent(packageName);
12556
+ const url = `${NPM_REGISTRY_BASE}/${encodedName}`;
12557
+ try {
12558
+ const controller = new AbortController;
12559
+ const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT_MS);
12560
+ const response = await fetch(url, {
12561
+ signal: controller.signal,
12562
+ headers: {
12563
+ Accept: "application/json"
12564
+ }
12565
+ });
12566
+ clearTimeout(timeoutId);
12567
+ if (response.status === 404) {
12568
+ throw new NotFoundError(`npm package \`${packageName}\` not found on registry`);
12569
+ }
12570
+ if (!response.ok) {
12571
+ throw new NetworkError(`Failed to fetch npm package \`${packageName}\`: HTTP ${response.status} ${response.statusText}`);
12572
+ }
12573
+ const data = await response.json();
12574
+ return data;
12575
+ } catch (error) {
12576
+ if (error instanceof NotFoundError || error instanceof NetworkError) {
12577
+ throw error;
12578
+ }
12579
+ if (error instanceof Error && error.name === "AbortError") {
12580
+ throw new NetworkError(`Request to npm registry timed out after ${NPM_FETCH_TIMEOUT_MS / 1000}s for package \`${packageName}\``);
12581
+ }
12582
+ const message = error instanceof Error ? error.message : String(error);
12583
+ throw new NetworkError(`Failed to fetch npm package \`${packageName}\`: ${message}`);
12584
+ }
12585
+ }
12586
+ function formatPluginEntry(name, version) {
12587
+ return version ? `${name}@${version}` : name;
12588
+ }
12589
+ function validateOpenCodePlugin(packageJson) {
12590
+ const warnings = [];
12591
+ if (packageJson.type !== "module") {
12592
+ throw new ValidationError(`Package \`${packageJson.name}\` is not an ESM module (missing "type": "module" in package.json)`);
12593
+ }
12594
+ const hasMain = Boolean(packageJson.main);
12595
+ const hasExports = packageJson.exports !== undefined;
12596
+ if (!hasMain && !hasExports) {
12597
+ throw new ValidationError(`Package \`${packageJson.name}\` has no entry point (missing "main" or "exports")`);
12598
+ }
12599
+ if (!packageJson.name.includes("opencode")) {
12600
+ warnings.push(`Package name \`${packageJson.name}\` doesn't contain "opencode" - this may not be an OpenCode plugin`);
12601
+ }
12602
+ return { valid: true, warnings };
12603
+ }
12604
+ async function fetchPackageVersion(packageName, version) {
12605
+ const metadata = await validateNpmPackage(packageName);
12606
+ const resolvedVersion = version ?? metadata["dist-tags"].latest;
12607
+ const versionData = metadata.versions[resolvedVersion];
12608
+ if (!versionData) {
12609
+ throw new NotFoundError(`Version \`${resolvedVersion}\` not found for npm package \`${packageName}\``);
12610
+ }
12611
+ return versionData;
12612
+ }
12613
+ function extractPackageName(pluginEntry) {
12614
+ const trimmed = pluginEntry.trim();
12615
+ const lastAt = trimmed.lastIndexOf("@");
12616
+ if (lastAt <= 0) {
12617
+ return trimmed;
12618
+ }
12619
+ const beforeAt = trimmed.slice(0, lastAt);
12620
+ if (beforeAt.includes("/") || !beforeAt.startsWith("@")) {
12621
+ return beforeAt;
12622
+ }
12623
+ return trimmed;
12624
+ }
12625
+
12489
12626
  // src/commands/add.ts
12627
+ function parseAddInput(input) {
12628
+ if (!input?.trim()) {
12629
+ throw new ValidationError("Component name cannot be empty");
12630
+ }
12631
+ const trimmed = input.trim();
12632
+ if (isNpmSpecifier(trimmed)) {
12633
+ const parsed = parseNpmSpecifier(trimmed);
12634
+ return { type: "npm", name: parsed.name, version: parsed.version };
12635
+ }
12636
+ if (trimmed.includes("/")) {
12637
+ const { namespace, component } = parseQualifiedComponent(trimmed);
12638
+ return { type: "registry", namespace, component };
12639
+ }
12640
+ return { type: "registry", namespace: "", component: trimmed };
12641
+ }
12490
12642
  function registerAddCommand(program2) {
12491
- const cmd = program2.command("add").description("Add components to your project").argument("<components...>", "Components to install").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks");
12643
+ const cmd = program2.command("add").description(`Add components or npm plugins to your project.
12644
+
12645
+ ` + ` Registry components: ocx add namespace/component
12646
+ ` + " npm plugins: ocx add npm:package-name[@version]").argument("<components...>", "Components to install (namespace/component or npm:package[@version])").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks").option("--trust", "Skip npm plugin validation (for packages that don't follow conventions)");
12492
12647
  addCommonOptions(cmd);
12493
- addConfirmationOptions(cmd);
12648
+ addForceOption(cmd);
12494
12649
  addVerboseOption(cmd);
12495
12650
  cmd.action(async (components, options2) => {
12496
12651
  try {
@@ -12502,6 +12657,104 @@ function registerAddCommand(program2) {
12502
12657
  });
12503
12658
  }
12504
12659
  async function runAddCore(componentNames, options2, provider) {
12660
+ const cwd = provider.cwd;
12661
+ const parsedInputs = componentNames.map(parseAddInput);
12662
+ const npmInputs = parsedInputs.filter((i) => i.type === "npm");
12663
+ const registryInputs = parsedInputs.filter((i) => i.type === "registry");
12664
+ if (npmInputs.length > 0) {
12665
+ await handleNpmPlugins(npmInputs, options2, cwd);
12666
+ }
12667
+ if (registryInputs.length > 0) {
12668
+ const registryComponentNames = registryInputs.map((i) => i.namespace ? `${i.namespace}/${i.component}` : i.component);
12669
+ await runRegistryAddCore(registryComponentNames, options2, provider);
12670
+ }
12671
+ }
12672
+ async function handleNpmPlugins(inputs, options2, cwd) {
12673
+ const spin = options2.quiet ? null : createSpinner({ text: "Validating npm packages..." });
12674
+ spin?.start();
12675
+ try {
12676
+ const allWarnings = [];
12677
+ for (const input of inputs) {
12678
+ await validateNpmPackage(input.name);
12679
+ if (!options2.trust) {
12680
+ try {
12681
+ const versionData = await fetchPackageVersion(input.name, input.version);
12682
+ const result = validateOpenCodePlugin(versionData);
12683
+ allWarnings.push(...result.warnings);
12684
+ } catch (error) {
12685
+ if (error instanceof ValidationError) {
12686
+ spin?.fail("Plugin validation failed");
12687
+ throw new ValidationError(`${error.message}
12688
+ ` + `hint OpenCode plugins must be ESM modules with an entry point
12689
+ ` + `hint Use \`--trust\` to add anyway`);
12690
+ }
12691
+ throw error;
12692
+ }
12693
+ }
12694
+ }
12695
+ spin?.succeed(`Validated ${inputs.length} npm package(s)`);
12696
+ if (allWarnings.length > 0 && !options2.quiet) {
12697
+ logger.info("");
12698
+ for (const warning of allWarnings) {
12699
+ logger.warn(warning);
12700
+ }
12701
+ }
12702
+ const existingConfig = await readOpencodeJsonConfig(cwd);
12703
+ const existingPlugins = existingConfig?.config.plugin ?? [];
12704
+ const existingPluginMap = new Map;
12705
+ for (const plugin of existingPlugins) {
12706
+ const name = extractPackageName(plugin);
12707
+ existingPluginMap.set(name, plugin);
12708
+ }
12709
+ const pluginsToAdd = [];
12710
+ const conflicts = [];
12711
+ for (const input of inputs) {
12712
+ const existingEntry = existingPluginMap.get(input.name);
12713
+ if (existingEntry) {
12714
+ if (!options2.force) {
12715
+ conflicts.push(input.name);
12716
+ } else {
12717
+ existingPluginMap.set(input.name, formatPluginEntry(input.name, input.version));
12718
+ }
12719
+ } else {
12720
+ pluginsToAdd.push(formatPluginEntry(input.name, input.version));
12721
+ }
12722
+ }
12723
+ if (conflicts.length > 0) {
12724
+ throw new ConflictError(`Plugin(s) already exist in opencode.json: ${conflicts.join(", ")}.
12725
+ ` + "Use --force to replace existing entries.");
12726
+ }
12727
+ const finalPlugins = [...existingPluginMap.values(), ...pluginsToAdd];
12728
+ if (options2.dryRun) {
12729
+ logger.info("");
12730
+ logger.info("Dry run - no changes made");
12731
+ logger.info("");
12732
+ logger.info("Would add npm plugins:");
12733
+ for (const input of inputs) {
12734
+ logger.info(` ${formatPluginEntry(input.name, input.version)}`);
12735
+ }
12736
+ return;
12737
+ }
12738
+ await updateOpencodeJsonConfig(cwd, { plugin: finalPlugins });
12739
+ if (!options2.quiet) {
12740
+ logger.info("");
12741
+ logger.success(`Added ${inputs.length} npm plugin(s) to opencode.json`);
12742
+ for (const input of inputs) {
12743
+ logger.info(` \u2713 ${formatPluginEntry(input.name, input.version)}`);
12744
+ }
12745
+ }
12746
+ if (options2.json) {
12747
+ console.log(JSON.stringify({
12748
+ success: true,
12749
+ plugins: inputs.map((i) => formatPluginEntry(i.name, i.version))
12750
+ }, null, 2));
12751
+ }
12752
+ } catch (error) {
12753
+ spin?.fail("Failed to add npm plugins");
12754
+ throw error;
12755
+ }
12756
+ }
12757
+ async function runRegistryAddCore(componentNames, options2, provider) {
12505
12758
  const cwd = provider.cwd;
12506
12759
  const lockPath = join2(cwd, "ocx.lock");
12507
12760
  const registries = provider.getRegistries();
@@ -12595,7 +12848,7 @@ async function runAddCore(componentNames, options2, provider) {
12595
12848
  if (existsSync(targetPath)) {
12596
12849
  const existingContent = await Bun.file(targetPath).text();
12597
12850
  const incomingContent = file.content.toString("utf-8");
12598
- if (!isContentIdentical(existingContent, incomingContent) && !options2.yes) {
12851
+ if (!isContentIdentical(existingContent, incomingContent) && !options2.force) {
12599
12852
  allConflicts.push(componentFile.target);
12600
12853
  }
12601
12854
  }
@@ -12609,14 +12862,14 @@ async function runAddCore(componentNames, options2, provider) {
12609
12862
  }
12610
12863
  logger.error("");
12611
12864
  logger.error("These files have been modified since installation.");
12612
- logger.error("Use --yes to overwrite, or review the changes first.");
12613
- throw new ConflictError(`${allConflicts.length} file(s) have conflicts. Use --yes to overwrite.`);
12865
+ logger.error("Use --force to overwrite, or review the changes first.");
12866
+ throw new ConflictError(`${allConflicts.length} file(s) have conflicts. Use --force to overwrite.`);
12614
12867
  }
12615
12868
  const installSpin = options2.quiet ? null : createSpinner({ text: "Installing components..." });
12616
12869
  installSpin?.start();
12617
12870
  for (const { component, files, computedHash } of componentBundles) {
12618
12871
  const installResult = await installComponent(component, files, cwd, {
12619
- yes: options2.yes,
12872
+ force: options2.force,
12620
12873
  verbose: options2.verbose
12621
12874
  });
12622
12875
  if (options2.verbose) {
@@ -13871,9 +14124,9 @@ Diff for ${res.name}:`));
13871
14124
 
13872
14125
  // src/commands/ghost/add.ts
13873
14126
  function registerGhostAddCommand(parent) {
13874
- const cmd = parent.command("add").description("Add components using ghost mode registries").argument("<components...>", "Components to install").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks");
14127
+ const cmd = parent.command("add").description("Add components using ghost mode registries").argument("<components...>", "Components to install").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks").option("--trust", "Skip npm plugin validation");
13875
14128
  addCommonOptions(cmd);
13876
- addConfirmationOptions(cmd);
14129
+ addForceOption(cmd);
13877
14130
  addVerboseOption(cmd);
13878
14131
  cmd.action(async (components, options2) => {
13879
14132
  try {
@@ -14239,7 +14492,7 @@ async function runGhostOpenCode(args, options2) {
14239
14492
  }
14240
14493
 
14241
14494
  // src/commands/registry.ts
14242
- async function runRegistryAddCore(url, options2, callbacks) {
14495
+ async function runRegistryAddCore2(url, options2, callbacks) {
14243
14496
  if (callbacks.isLocked?.()) {
14244
14497
  throw new Error("Registries are locked. Cannot add.");
14245
14498
  }
@@ -14285,7 +14538,7 @@ function registerRegistryCommand(program2) {
14285
14538
  logger.error("No ocx.jsonc found. Run 'ocx init' first.");
14286
14539
  process.exit(1);
14287
14540
  }
14288
- const result = await runRegistryAddCore(url, options2, {
14541
+ const result = await runRegistryAddCore2(url, options2, {
14289
14542
  getRegistries: () => config.registries,
14290
14543
  isLocked: () => config.lockRegistries ?? false,
14291
14544
  setRegistry: async (name, regConfig) => {
@@ -14380,7 +14633,7 @@ function registerGhostRegistryCommand(parent) {
14380
14633
  try {
14381
14634
  await ensureGhostInitialized();
14382
14635
  const config = await loadGhostConfig();
14383
- const result = await runRegistryAddCore(url, options2, {
14636
+ const result = await runRegistryAddCore2(url, options2, {
14384
14637
  getRegistries: () => config.registries,
14385
14638
  setRegistry: async (name, regConfig) => {
14386
14639
  config.registries[name] = regConfig;
@@ -15056,7 +15309,7 @@ async function hashBundle2(files) {
15056
15309
  `));
15057
15310
  }
15058
15311
  // src/index.ts
15059
- var version = "1.0.21";
15312
+ var version = "1.1.0";
15060
15313
  async function main2() {
15061
15314
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
15062
15315
  registerInitCommand(program2);
@@ -15083,4 +15336,4 @@ export {
15083
15336
  buildRegistry
15084
15337
  };
15085
15338
 
15086
- //# debugId=4A28ED9CEDFE469F64756E2164756E21
15339
+ //# debugId=46A2A0DCEF36C6A464756E2164756E21