ocx 1.0.14 → 1.0.15

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
@@ -8492,7 +8492,7 @@ var openCodeNameSchema = exports_external.string().min(1, "Name cannot be empty"
8492
8492
  });
8493
8493
  var namespaceSchema = openCodeNameSchema;
8494
8494
  var qualifiedComponentSchema = exports_external.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*\/[a-z0-9]+(-[a-z0-9]+)*$/, {
8495
- message: 'Must be in format "namespace/component" (e.g., "kdco/librarian"). Both parts must be lowercase alphanumeric with hyphens.'
8495
+ message: 'Must be in format "namespace/component" (e.g., "kdco/researcher"). Both parts must be lowercase alphanumeric with hyphens.'
8496
8496
  });
8497
8497
  function parseQualifiedComponent(ref) {
8498
8498
  if (!ref.includes("/")) {
@@ -8734,7 +8734,7 @@ class IntegrityError extends OCXError {
8734
8734
  ` + ` Found: ${found}
8735
8735
 
8736
8736
  ` + `The registry content has changed since this component was locked.
8737
- ` + `This could indicate tampering or an unauthorized update.`;
8737
+ ` + `Use 'ocx update ${component}' to intentionally update this component.`;
8738
8738
  super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY);
8739
8739
  this.name = "IntegrityError";
8740
8740
  }
@@ -8773,23 +8773,31 @@ async function fetchRegistryIndex(baseUrl) {
8773
8773
  });
8774
8774
  }
8775
8775
  async function fetchComponent(baseUrl, name) {
8776
+ const result = await fetchComponentVersion(baseUrl, name);
8777
+ return result.manifest;
8778
+ }
8779
+ async function fetchComponentVersion(baseUrl, name, version) {
8776
8780
  const url = `${baseUrl.replace(/\/$/, "")}/components/${name}.json`;
8777
- return fetchWithCache(url, (data) => {
8781
+ return fetchWithCache(`${url}#v=${version ?? "latest"}`, (data) => {
8778
8782
  const packumentResult = packumentSchema.safeParse(data);
8779
8783
  if (!packumentResult.success) {
8780
8784
  throw new ValidationError(`Invalid packument format for "${name}": ${packumentResult.error.message}`);
8781
8785
  }
8782
8786
  const packument = packumentResult.data;
8783
- const latestVersion = packument["dist-tags"].latest;
8784
- const manifest = packument.versions[latestVersion];
8787
+ const resolvedVersion = version ?? packument["dist-tags"].latest;
8788
+ const manifest = packument.versions[resolvedVersion];
8785
8789
  if (!manifest) {
8786
- throw new ValidationError(`Component "${name}" has no manifest for latest version ${latestVersion}`);
8790
+ if (version) {
8791
+ const availableVersions = Object.keys(packument.versions).join(", ");
8792
+ throw new ValidationError(`Component "${name}" has no version "${version}". Available: ${availableVersions}`);
8793
+ }
8794
+ throw new ValidationError(`Component "${name}" has no manifest for latest version ${resolvedVersion}`);
8787
8795
  }
8788
8796
  const manifestResult = componentManifestSchema.safeParse(manifest);
8789
8797
  if (!manifestResult.success) {
8790
- throw new ValidationError(`Invalid component manifest for "${name}@${latestVersion}": ${manifestResult.error.message}`);
8798
+ throw new ValidationError(`Invalid component manifest for "${name}@${resolvedVersion}": ${manifestResult.error.message}`);
8791
8799
  }
8792
- return manifestResult.data;
8800
+ return { manifest: manifestResult.data, version: resolvedVersion };
8793
8801
  });
8794
8802
  }
8795
8803
  async function fetchFileContent(baseUrl, componentName, filePath) {
@@ -10272,7 +10280,8 @@ var installedComponentSchema = exports_external.object({
10272
10280
  version: exports_external.string(),
10273
10281
  hash: exports_external.string(),
10274
10282
  files: exports_external.array(exports_external.string()),
10275
- installedAt: exports_external.string()
10283
+ installedAt: exports_external.string(),
10284
+ updatedAt: exports_external.string().optional()
10276
10285
  });
10277
10286
  var ocxLockSchema = exports_external.object({
10278
10287
  lockVersion: exports_external.literal(1),
@@ -13635,12 +13644,292 @@ function registerSearchCommand(program2) {
13635
13644
  }
13636
13645
  });
13637
13646
  }
13647
+
13648
+ // src/commands/update.ts
13649
+ import { createHash as createHash2 } from "crypto";
13650
+ import { existsSync as existsSync3 } from "fs";
13651
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
13652
+ import { dirname as dirname3, join as join5 } from "path";
13653
+ function registerUpdateCommand(program2) {
13654
+ 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) => {
13655
+ try {
13656
+ await runUpdate(components, options2);
13657
+ } catch (error) {
13658
+ handleError(error, { json: options2.json });
13659
+ }
13660
+ });
13661
+ }
13662
+ async function runUpdate(componentNames, options2) {
13663
+ const cwd = options2.cwd ?? process.cwd();
13664
+ const lockPath = join5(cwd, "ocx.lock");
13665
+ const config = await readOcxConfig(cwd);
13666
+ if (!config) {
13667
+ throw new ConfigError("No ocx.jsonc found. Run 'ocx init' first.");
13668
+ }
13669
+ const lock = await readOcxLock(cwd);
13670
+ if (!lock || Object.keys(lock.installed).length === 0) {
13671
+ throw new ConfigError("Nothing installed yet. Run 'ocx add <component>' first.");
13672
+ }
13673
+ const hasComponents = componentNames.length > 0;
13674
+ const hasAll = options2.all === true;
13675
+ const hasRegistry = options2.registry !== undefined;
13676
+ if (!hasComponents && !hasAll && !hasRegistry) {
13677
+ throw new ValidationError(`Specify components, use --all, or use --registry <name>.
13678
+
13679
+ ` + `Examples:
13680
+ ` + ` ocx update kdco/agents # Update specific component
13681
+ ` + ` ocx update --all # Update all installed components
13682
+ ` + " ocx update --registry kdco # Update all from a registry");
13683
+ }
13684
+ if (hasAll && hasComponents) {
13685
+ throw new ValidationError(`Cannot specify components with --all.
13686
+ ` + "Use either 'ocx update --all' or 'ocx update <components>'.");
13687
+ }
13688
+ if (hasRegistry && hasComponents) {
13689
+ throw new ValidationError(`Cannot specify components with --registry.
13690
+ ` + "Use either 'ocx update --registry <name>' or 'ocx update <components>'.");
13691
+ }
13692
+ if (hasAll && hasRegistry) {
13693
+ throw new ValidationError(`Cannot use --all with --registry.
13694
+ ` + "Use either 'ocx update --all' or 'ocx update --registry <name>'.");
13695
+ }
13696
+ const parsedComponents = componentNames.map(parseComponentSpec);
13697
+ for (const spec of parsedComponents) {
13698
+ if (spec.version !== undefined && spec.version === "") {
13699
+ throw new ValidationError(`Invalid version specifier in '${spec.component}@'.` + `
13700
+ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.`);
13701
+ }
13702
+ }
13703
+ const componentsToUpdate = resolveComponentsToUpdate(lock, parsedComponents, options2);
13704
+ if (componentsToUpdate.length === 0) {
13705
+ if (hasRegistry) {
13706
+ throw new NotFoundError(`No installed components from registry '${options2.registry}'.`);
13707
+ }
13708
+ throw new NotFoundError("No matching components found to update.");
13709
+ }
13710
+ const spin = options2.quiet ? null : createSpinner({ text: "Checking for updates..." });
13711
+ spin?.start();
13712
+ const results = [];
13713
+ const updates = [];
13714
+ try {
13715
+ for (const spec of componentsToUpdate) {
13716
+ const qualifiedName = spec.component;
13717
+ const lockEntry = lock.installed[qualifiedName];
13718
+ if (!lockEntry) {
13719
+ throw new NotFoundError(`Component '${qualifiedName}' not found in lock file.`);
13720
+ }
13721
+ const { namespace, component: componentName } = parseQualifiedComponent(qualifiedName);
13722
+ const regConfig = config.registries[namespace];
13723
+ if (!regConfig) {
13724
+ throw new ConfigError(`Registry '${namespace}' not configured. Component '${qualifiedName}' cannot be updated.`);
13725
+ }
13726
+ const fetchResult = await fetchComponentVersion(regConfig.url, componentName, spec.version);
13727
+ const manifest = fetchResult.manifest;
13728
+ const version = fetchResult.version;
13729
+ const normalizedManifest = normalizeComponentManifest(manifest);
13730
+ const files = [];
13731
+ for (const file of normalizedManifest.files) {
13732
+ const content2 = await fetchFileContent(regConfig.url, componentName, file.path);
13733
+ files.push({ path: file.path, content: Buffer.from(content2) });
13734
+ }
13735
+ const newHash = await hashBundle2(files);
13736
+ if (newHash === lockEntry.hash) {
13737
+ results.push({
13738
+ qualifiedName,
13739
+ oldVersion: lockEntry.version,
13740
+ newVersion: version,
13741
+ status: "up-to-date"
13742
+ });
13743
+ } else if (options2.dryRun) {
13744
+ results.push({
13745
+ qualifiedName,
13746
+ oldVersion: lockEntry.version,
13747
+ newVersion: version,
13748
+ status: "would-update"
13749
+ });
13750
+ } else {
13751
+ results.push({
13752
+ qualifiedName,
13753
+ oldVersion: lockEntry.version,
13754
+ newVersion: version,
13755
+ status: "updated"
13756
+ });
13757
+ updates.push({
13758
+ qualifiedName,
13759
+ component: normalizedManifest,
13760
+ files,
13761
+ newHash,
13762
+ newVersion: version,
13763
+ baseUrl: regConfig.url
13764
+ });
13765
+ }
13766
+ }
13767
+ spin?.succeed(`Checked ${componentsToUpdate.length} component(s)`);
13768
+ if (options2.dryRun) {
13769
+ outputDryRun(results, options2);
13770
+ return;
13771
+ }
13772
+ if (updates.length === 0) {
13773
+ if (!options2.quiet && !options2.json) {
13774
+ logger.info("");
13775
+ logger.success("All components are up to date.");
13776
+ }
13777
+ if (options2.json) {
13778
+ console.log(JSON.stringify({ success: true, updated: [], upToDate: results }, null, 2));
13779
+ }
13780
+ return;
13781
+ }
13782
+ const installSpin = options2.quiet ? null : createSpinner({ text: "Updating components..." });
13783
+ installSpin?.start();
13784
+ for (const update of updates) {
13785
+ for (const file of update.files) {
13786
+ const fileObj = update.component.files.find((f) => f.path === file.path);
13787
+ if (!fileObj)
13788
+ continue;
13789
+ const targetPath = join5(cwd, fileObj.target);
13790
+ const targetDir = dirname3(targetPath);
13791
+ if (!existsSync3(targetDir)) {
13792
+ await mkdir4(targetDir, { recursive: true });
13793
+ }
13794
+ await writeFile3(targetPath, file.content);
13795
+ if (options2.verbose) {
13796
+ logger.info(` \u2713 Updated ${fileObj.target}`);
13797
+ }
13798
+ }
13799
+ const existingEntry = lock.installed[update.qualifiedName];
13800
+ if (!existingEntry) {
13801
+ throw new NotFoundError(`Component '${update.qualifiedName}' not found in lock file.`);
13802
+ }
13803
+ lock.installed[update.qualifiedName] = {
13804
+ registry: existingEntry.registry,
13805
+ version: update.newVersion,
13806
+ hash: update.newHash,
13807
+ files: existingEntry.files,
13808
+ installedAt: existingEntry.installedAt,
13809
+ updatedAt: new Date().toISOString()
13810
+ };
13811
+ }
13812
+ installSpin?.succeed(`Updated ${updates.length} component(s)`);
13813
+ await writeFile3(lockPath, JSON.stringify(lock, null, 2), "utf-8");
13814
+ if (options2.json) {
13815
+ console.log(JSON.stringify({
13816
+ success: true,
13817
+ updated: results.filter((r2) => r2.status === "updated"),
13818
+ upToDate: results.filter((r2) => r2.status === "up-to-date")
13819
+ }, null, 2));
13820
+ } else if (!options2.quiet) {
13821
+ logger.info("");
13822
+ for (const result of results) {
13823
+ if (result.status === "updated") {
13824
+ logger.info(` \u2713 ${result.qualifiedName} (${result.oldVersion} \u2192 ${result.newVersion})`);
13825
+ } else if (result.status === "up-to-date" && options2.verbose) {
13826
+ logger.info(` \u25CB ${result.qualifiedName} (already up to date)`);
13827
+ }
13828
+ }
13829
+ logger.info("");
13830
+ logger.success(`Done! Updated ${updates.length} component(s).`);
13831
+ }
13832
+ } catch (error) {
13833
+ spin?.fail("Failed to check for updates");
13834
+ throw error;
13835
+ }
13836
+ }
13837
+ function parseComponentSpec(spec) {
13838
+ const atIndex = spec.lastIndexOf("@");
13839
+ if (atIndex <= 0) {
13840
+ return { component: spec };
13841
+ }
13842
+ return {
13843
+ component: spec.slice(0, atIndex),
13844
+ version: spec.slice(atIndex + 1)
13845
+ };
13846
+ }
13847
+ function resolveComponentsToUpdate(lock, parsedComponents, options2) {
13848
+ const installedComponents = Object.keys(lock.installed);
13849
+ if (options2.all) {
13850
+ return installedComponents.map((c) => ({ component: c }));
13851
+ }
13852
+ if (options2.registry) {
13853
+ return installedComponents.filter((name) => {
13854
+ const entry = lock.installed[name];
13855
+ return entry?.registry === options2.registry;
13856
+ }).map((c) => ({ component: c }));
13857
+ }
13858
+ const result = [];
13859
+ for (const spec of parsedComponents) {
13860
+ const name = spec.component;
13861
+ if (!name.includes("/")) {
13862
+ const suggestions = installedComponents.filter((installed) => installed.endsWith(`/${name}`));
13863
+ if (suggestions.length === 1) {
13864
+ throw new ValidationError(`Ambiguous component '${name}'. Did you mean '${suggestions[0]}'?`);
13865
+ }
13866
+ if (suggestions.length > 1) {
13867
+ throw new ValidationError(`Ambiguous component '${name}'. Found in multiple registries:
13868
+ ` + suggestions.map((s) => ` - ${s}`).join(`
13869
+ `) + `
13870
+
13871
+ Please use a fully qualified name (registry/component).`);
13872
+ }
13873
+ throw new ValidationError(`Component '${name}' must include a registry prefix (e.g., 'kdco/${name}').`);
13874
+ }
13875
+ if (!lock.installed[name]) {
13876
+ throw new NotFoundError(`Component '${name}' is not installed.
13877
+ Run 'ocx add ${name}' to install it first.`);
13878
+ }
13879
+ result.push(spec);
13880
+ }
13881
+ return result;
13882
+ }
13883
+ function outputDryRun(results, options2) {
13884
+ const wouldUpdate = results.filter((r2) => r2.status === "would-update");
13885
+ const upToDate = results.filter((r2) => r2.status === "up-to-date");
13886
+ if (options2.json) {
13887
+ console.log(JSON.stringify({ dryRun: true, wouldUpdate, upToDate }, null, 2));
13888
+ return;
13889
+ }
13890
+ if (!options2.quiet) {
13891
+ logger.info("");
13892
+ if (wouldUpdate.length > 0) {
13893
+ logger.info("Would update:");
13894
+ for (const result of wouldUpdate) {
13895
+ logger.info(` ${result.qualifiedName} (${result.oldVersion} \u2192 ${result.newVersion})`);
13896
+ }
13897
+ }
13898
+ if (upToDate.length > 0 && options2.verbose) {
13899
+ logger.info("");
13900
+ logger.info("Already up to date:");
13901
+ for (const result of upToDate) {
13902
+ logger.info(` ${result.qualifiedName}`);
13903
+ }
13904
+ }
13905
+ if (wouldUpdate.length > 0) {
13906
+ logger.info("");
13907
+ logger.info("Run without --dry-run to apply changes.");
13908
+ } else {
13909
+ logger.info("All components are up to date.");
13910
+ }
13911
+ }
13912
+ }
13913
+ async function hashContent2(content2) {
13914
+ return createHash2("sha256").update(content2).digest("hex");
13915
+ }
13916
+ async function hashBundle2(files) {
13917
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
13918
+ const manifestParts = [];
13919
+ for (const file of sorted) {
13920
+ const hash = await hashContent2(file.content);
13921
+ manifestParts.push(`${file.path}:${hash}`);
13922
+ }
13923
+ return hashContent2(manifestParts.join(`
13924
+ `));
13925
+ }
13638
13926
  // src/index.ts
13639
- var version = "1.0.14";
13927
+ var version = "1.0.15";
13640
13928
  async function main2() {
13641
13929
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
13642
13930
  registerInitCommand(program2);
13643
13931
  registerAddCommand(program2);
13932
+ registerUpdateCommand(program2);
13644
13933
  registerDiffCommand(program2);
13645
13934
  registerSearchCommand(program2);
13646
13935
  registerRegistryCommand(program2);
@@ -13661,4 +13950,4 @@ export {
13661
13950
  buildRegistry
13662
13951
  };
13663
13952
 
13664
- //# debugId=8679B4448163A59C64756E2164756E21
13953
+ //# debugId=0C10B0E363D77B8C64756E2164756E21