ocx 1.0.13 → 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),
@@ -13266,7 +13275,7 @@ Diff for ${res.name}:`));
13266
13275
 
13267
13276
  // src/commands/init.ts
13268
13277
  import { existsSync as existsSync2 } from "fs";
13269
- import { writeFile as writeFile2 } from "fs/promises";
13278
+ import { cp, mkdir as mkdir3, readdir, readFile, rm, writeFile as writeFile2 } from "fs/promises";
13270
13279
  import { join as join4 } from "path";
13271
13280
 
13272
13281
  // src/constants.ts
@@ -13274,10 +13283,16 @@ var OCX_DOMAIN = "ocx.kdco.dev";
13274
13283
  var OCX_SCHEMA_URL = `https://${OCX_DOMAIN}/schema.json`;
13275
13284
 
13276
13285
  // src/commands/init.ts
13286
+ var TEMPLATE_REPO = "kdcokenny/ocx";
13287
+ var TEMPLATE_PATH = "examples/registry-starter";
13277
13288
  function registerInitCommand(program2) {
13278
- program2.command("init").description("Initialize OCX configuration in your project").option("-y, --yes", "Skip prompts and use defaults").option("--cwd <path>", "Working directory", process.cwd()).option("-q, --quiet", "Suppress output").option("-v, --verbose", "Verbose output").option("--json", "Output as JSON").action(async (options2) => {
13289
+ program2.command("init [directory]").description("Initialize OCX configuration in your project").option("-y, --yes", "Skip prompts and use defaults").option("--cwd <path>", "Working directory", process.cwd()).option("-q, --quiet", "Suppress output").option("-v, --verbose", "Verbose output").option("--json", "Output as JSON").option("--registry", "Scaffold a new OCX registry project").option("--namespace <name>", "Registry namespace (e.g., my-org)").option("--author <name>", "Author name for the registry").option("--canary", "Use canary (main branch) instead of latest release").option("--local <path>", "Use local template directory instead of fetching").action(async (directory, options2) => {
13279
13290
  try {
13280
- await runInit(options2);
13291
+ if (options2.registry) {
13292
+ await runInitRegistry(directory, options2);
13293
+ } else {
13294
+ await runInit(options2);
13295
+ }
13281
13296
  } catch (error) {
13282
13297
  handleError(error, { json: options2.json });
13283
13298
  }
@@ -13322,6 +13337,116 @@ async function runInit(options2) {
13322
13337
  throw error;
13323
13338
  }
13324
13339
  }
13340
+ async function runInitRegistry(directory, options2) {
13341
+ const cwd = directory ?? options2.cwd ?? process.cwd();
13342
+ const namespace = options2.namespace ?? "my-registry";
13343
+ const author = options2.author ?? "Your Name";
13344
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(namespace)) {
13345
+ throw new ValidationError("Invalid namespace format: must start with letter/number, use hyphens only between segments (e.g., 'my-registry')");
13346
+ }
13347
+ const existingFiles = await readdir(cwd).catch(() => []);
13348
+ const hasVisibleFiles = existingFiles.some((f) => !f.startsWith("."));
13349
+ if (hasVisibleFiles && !options2.yes) {
13350
+ throw new ConflictError("Directory is not empty. Use --yes to overwrite existing files.");
13351
+ }
13352
+ const spin = options2.quiet ? null : createSpinner({ text: "Scaffolding registry..." });
13353
+ spin?.start();
13354
+ try {
13355
+ if (spin)
13356
+ spin.text = options2.local ? "Copying template..." : "Fetching template...";
13357
+ if (options2.local) {
13358
+ await mkdir3(cwd, { recursive: true });
13359
+ await copyDir(options2.local, cwd);
13360
+ } else {
13361
+ const version = options2.canary ? "main" : await getLatestVersion();
13362
+ await fetchAndExtractTemplate(cwd, version, options2.verbose);
13363
+ }
13364
+ if (spin)
13365
+ spin.text = "Configuring project...";
13366
+ await replacePlaceholders(cwd, { namespace, author });
13367
+ spin?.succeed(`Created registry: ${namespace}`);
13368
+ if (options2.json) {
13369
+ console.log(JSON.stringify({ success: true, namespace, path: cwd }));
13370
+ } else if (!options2.quiet) {
13371
+ logger.info("");
13372
+ logger.info("Next steps:");
13373
+ logger.info(" 1. bun install");
13374
+ logger.info(" 2. Edit registry.json with your components");
13375
+ logger.info(" 3. bun run build");
13376
+ logger.info("");
13377
+ logger.info("Deploy to:");
13378
+ logger.info(" Cloudflare: bunx wrangler deploy");
13379
+ logger.info(" Vercel: vercel");
13380
+ logger.info(" Netlify: netlify deploy");
13381
+ }
13382
+ } catch (error) {
13383
+ spin?.fail("Failed to scaffold registry");
13384
+ throw error;
13385
+ }
13386
+ }
13387
+ async function copyDir(src, dest) {
13388
+ await cp(src, dest, { recursive: true });
13389
+ }
13390
+ async function getLatestVersion() {
13391
+ const pkgPath = new URL("../../package.json", import.meta.url);
13392
+ const pkgContent = await readFile(pkgPath);
13393
+ const pkg = JSON.parse(pkgContent.toString());
13394
+ return `v${pkg.version}`;
13395
+ }
13396
+ async function fetchAndExtractTemplate(destDir, version, verbose) {
13397
+ const ref = version === "main" ? "heads/main" : `tags/${version}`;
13398
+ const tarballUrl = `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
13399
+ if (verbose) {
13400
+ logger.info(`Fetching ${tarballUrl}`);
13401
+ }
13402
+ const response = await fetch(tarballUrl);
13403
+ if (!response.ok || !response.body) {
13404
+ throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
13405
+ }
13406
+ const tempDir = join4(destDir, ".ocx-temp");
13407
+ await mkdir3(tempDir, { recursive: true });
13408
+ try {
13409
+ const tarPath = join4(tempDir, "template.tar.gz");
13410
+ const arrayBuffer = await response.arrayBuffer();
13411
+ await writeFile2(tarPath, Buffer.from(arrayBuffer));
13412
+ const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
13413
+ stdout: "ignore",
13414
+ stderr: "pipe"
13415
+ });
13416
+ const exitCode = await proc.exited;
13417
+ if (exitCode !== 0) {
13418
+ const stderr = await new Response(proc.stderr).text();
13419
+ throw new Error(`Failed to extract template: ${stderr}`);
13420
+ }
13421
+ const extractedDirs = await readdir(tempDir);
13422
+ const extractedDir = extractedDirs.find((d) => d.startsWith("ocx-"));
13423
+ if (!extractedDir) {
13424
+ throw new Error("Failed to find extracted template directory");
13425
+ }
13426
+ const templateDir = join4(tempDir, extractedDir, TEMPLATE_PATH);
13427
+ await copyDir(templateDir, destDir);
13428
+ } finally {
13429
+ await rm(tempDir, { recursive: true, force: true });
13430
+ }
13431
+ }
13432
+ async function replacePlaceholders(dir, values) {
13433
+ const filesToProcess = [
13434
+ "registry.json",
13435
+ "package.json",
13436
+ "wrangler.jsonc",
13437
+ "README.md",
13438
+ "AGENTS.md"
13439
+ ];
13440
+ for (const file of filesToProcess) {
13441
+ const filePath = join4(dir, file);
13442
+ if (!existsSync2(filePath))
13443
+ continue;
13444
+ let content2 = await readFile(filePath).then((b) => b.toString());
13445
+ content2 = content2.replace(/my-registry/g, values.namespace);
13446
+ content2 = content2.replace(/Your Name/g, values.author);
13447
+ await writeFile2(filePath, content2);
13448
+ }
13449
+ }
13325
13450
 
13326
13451
  // src/commands/registry.ts
13327
13452
  function registerRegistryCommand(program2) {
@@ -13519,12 +13644,292 @@ function registerSearchCommand(program2) {
13519
13644
  }
13520
13645
  });
13521
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
+ }
13522
13926
  // src/index.ts
13523
- var version = "1.0.13";
13927
+ var version = "1.0.15";
13524
13928
  async function main2() {
13525
13929
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
13526
13930
  registerInitCommand(program2);
13527
13931
  registerAddCommand(program2);
13932
+ registerUpdateCommand(program2);
13528
13933
  registerDiffCommand(program2);
13529
13934
  registerSearchCommand(program2);
13530
13935
  registerRegistryCommand(program2);
@@ -13545,4 +13950,4 @@ export {
13545
13950
  buildRegistry
13546
13951
  };
13547
13952
 
13548
- //# debugId=2D4B3AD90B25BFD964756E2164756E21
13953
+ //# debugId=0C10B0E363D77B8C64756E2164756E21