ocx 1.3.0 → 1.3.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
@@ -10501,7 +10501,8 @@ var ghostConfigSchema = exports_external.object({
10501
10501
  "opencode.json"
10502
10502
  ]).describe("Glob patterns to exclude from the symlink farm"),
10503
10503
  include: exports_external.array(globPatternSchema).default([]).describe("Glob patterns to re-include from excluded set (for power users)"),
10504
- renameWindow: exports_external.boolean().default(true).describe("Set terminal/tmux window name when launching OpenCode")
10504
+ renameWindow: exports_external.boolean().default(true).describe("Set terminal/tmux window name when launching OpenCode"),
10505
+ maxFiles: exports_external.number().int().min(0, "maxFiles must be non-negative").default(1e4).describe("Maximum files to process (0 = unlimited)")
10505
10506
  });
10506
10507
 
10507
10508
  // src/utils/errors.ts
@@ -10511,7 +10512,8 @@ var EXIT_CODES = {
10511
10512
  NOT_FOUND: 66,
10512
10513
  NETWORK: 69,
10513
10514
  CONFIG: 78,
10514
- INTEGRITY: 1
10515
+ INTEGRITY: 1,
10516
+ FILE_LIMIT: 74
10515
10517
  };
10516
10518
 
10517
10519
  class OCXError extends Error {
@@ -10586,6 +10588,17 @@ class GhostConfigError extends OCXError {
10586
10588
  }
10587
10589
  }
10588
10590
 
10591
+ class FileLimitExceededError extends OCXError {
10592
+ count;
10593
+ limit;
10594
+ constructor(count, limit) {
10595
+ super(`File limit exceeded: found ${count} entries (limit: ${limit}). ` + `To fix: add exclude patterns to ghost.jsonc, or set "maxFiles" to a higher value (0 = unlimited).`, "FILE_LIMIT", EXIT_CODES.FILE_LIMIT);
10596
+ this.count = count;
10597
+ this.limit = limit;
10598
+ this.name = "FileLimitExceededError";
10599
+ }
10600
+ }
10601
+
10589
10602
  class ProfileNotFoundError extends OCXError {
10590
10603
  constructor(name) {
10591
10604
  super(`Profile "${name}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
@@ -10686,7 +10699,8 @@ var DEFAULT_GHOST_CONFIG = {
10686
10699
  "opencode.json"
10687
10700
  ],
10688
10701
  include: [],
10689
- renameWindow: true
10702
+ renameWindow: true,
10703
+ maxFiles: 1e4
10690
10704
  };
10691
10705
 
10692
10706
  class ProfileManager {
@@ -10880,7 +10894,7 @@ class GhostConfigProvider {
10880
10894
  // package.json
10881
10895
  var package_default = {
10882
10896
  name: "ocx",
10883
- version: "1.3.0",
10897
+ version: "1.3.2",
10884
10898
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10885
10899
  author: "kdcokenny",
10886
10900
  license: "MIT",
@@ -11020,13 +11034,13 @@ async function fetchFileContent(baseUrl, componentName, filePath) {
11020
11034
  return response.text();
11021
11035
  }
11022
11036
 
11023
- // ../../node_modules/.bun/remeda@2.33.0/node_modules/remeda/dist/lazyDataLastImpl-DtF3cihj.js
11037
+ // ../../node_modules/.bun/remeda@2.33.1/node_modules/remeda/dist/lazyDataLastImpl-DtF3cihj.js
11024
11038
  function e(e2, t, n) {
11025
11039
  let r = (n2) => e2(n2, ...t);
11026
11040
  return n === undefined ? r : Object.assign(r, { lazy: n, lazyArgs: t });
11027
11041
  }
11028
11042
 
11029
- // ../../node_modules/.bun/remeda@2.33.0/node_modules/remeda/dist/purry-GjwKKIlp.js
11043
+ // ../../node_modules/.bun/remeda@2.33.1/node_modules/remeda/dist/purry-GjwKKIlp.js
11030
11044
  function t(t2, n, r) {
11031
11045
  let i = t2.length - n.length;
11032
11046
  if (i === 0)
@@ -11036,7 +11050,7 @@ function t(t2, n, r) {
11036
11050
  throw Error(`Wrong number of arguments`);
11037
11051
  }
11038
11052
 
11039
- // ../../node_modules/.bun/remeda@2.33.0/node_modules/remeda/dist/isPlainObject.js
11053
+ // ../../node_modules/.bun/remeda@2.33.1/node_modules/remeda/dist/isPlainObject.js
11040
11054
  function e2(e3) {
11041
11055
  if (typeof e3 != `object` || !e3)
11042
11056
  return false;
@@ -11044,7 +11058,7 @@ function e2(e3) {
11044
11058
  return t2 === null || t2 === Object.prototype;
11045
11059
  }
11046
11060
 
11047
- // ../../node_modules/.bun/remeda@2.33.0/node_modules/remeda/dist/mergeDeep.js
11061
+ // ../../node_modules/.bun/remeda@2.33.1/node_modules/remeda/dist/mergeDeep.js
11048
11062
  function n(...t2) {
11049
11063
  return t(r, t2);
11050
11064
  }
@@ -13770,7 +13784,7 @@ function registerBuildCommand(program2) {
13770
13784
  });
13771
13785
  }
13772
13786
 
13773
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/base.js
13787
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/base.js
13774
13788
  class Diff {
13775
13789
  diff(oldStr, newStr, options2 = {}) {
13776
13790
  let callback;
@@ -13970,12 +13984,12 @@ class Diff {
13970
13984
  }
13971
13985
  }
13972
13986
 
13973
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/character.js
13987
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/character.js
13974
13988
  class CharacterDiff extends Diff {
13975
13989
  }
13976
13990
  var characterDiff = new CharacterDiff;
13977
13991
 
13978
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/util/string.js
13992
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/util/string.js
13979
13993
  function longestCommonPrefix(str1, str2) {
13980
13994
  let i;
13981
13995
  for (i = 0;i < str1.length && i < str2.length; i++) {
@@ -14071,8 +14085,8 @@ function leadingWs(string) {
14071
14085
  return match ? match[0] : "";
14072
14086
  }
14073
14087
 
14074
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/word.js
14075
- var extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}";
14088
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/word.js
14089
+ var extendedWordChars = "a-zA-Z0-9_\\u{AD}\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}";
14076
14090
  var tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, "ug");
14077
14091
 
14078
14092
  class WordDiff extends Diff {
@@ -14090,7 +14104,15 @@ class WordDiff extends Diff {
14090
14104
  if (segmenter2.resolvedOptions().granularity != "word") {
14091
14105
  throw new Error('The segmenter passed must have a granularity of "word"');
14092
14106
  }
14093
- parts = Array.from(segmenter2.segment(value), (segment) => segment.segment);
14107
+ parts = [];
14108
+ for (const segmentObj of Array.from(segmenter2.segment(value))) {
14109
+ const segment = segmentObj.segment;
14110
+ if (parts.length && /\s/.test(parts[parts.length - 1]) && /\s/.test(segment)) {
14111
+ parts[parts.length - 1] += segment;
14112
+ } else {
14113
+ parts.push(segment);
14114
+ }
14115
+ }
14094
14116
  } else {
14095
14117
  parts = value.match(tokenizeIncludingWhitespace) || [];
14096
14118
  }
@@ -14209,7 +14231,7 @@ class WordsWithSpaceDiff extends Diff {
14209
14231
  }
14210
14232
  var wordsWithSpaceDiff = new WordsWithSpaceDiff;
14211
14233
 
14212
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/line.js
14234
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/line.js
14213
14235
  class LineDiff extends Diff {
14214
14236
  constructor() {
14215
14237
  super(...arguments);
@@ -14262,7 +14284,7 @@ function tokenize(value, options2) {
14262
14284
  return retLines;
14263
14285
  }
14264
14286
 
14265
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/sentence.js
14287
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/sentence.js
14266
14288
  function isSentenceEndPunct(char) {
14267
14289
  return char == "." || char == "!" || char == "?";
14268
14290
  }
@@ -14292,7 +14314,7 @@ class SentenceDiff extends Diff {
14292
14314
  }
14293
14315
  var sentenceDiff = new SentenceDiff;
14294
14316
 
14295
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/css.js
14317
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/css.js
14296
14318
  class CssDiff extends Diff {
14297
14319
  tokenize(value) {
14298
14320
  return value.split(/([{}:;,]|\s+)/);
@@ -14300,7 +14322,7 @@ class CssDiff extends Diff {
14300
14322
  }
14301
14323
  var cssDiff = new CssDiff;
14302
14324
 
14303
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/json.js
14325
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/json.js
14304
14326
  class JsonDiff extends Diff {
14305
14327
  constructor() {
14306
14328
  super(...arguments);
@@ -14369,7 +14391,7 @@ function canonicalize(obj, stack, replacementStack, replacer, key) {
14369
14391
  return canonicalizedObj;
14370
14392
  }
14371
14393
 
14372
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/diff/array.js
14394
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/array.js
14373
14395
  class ArrayDiff extends Diff {
14374
14396
  tokenize(value) {
14375
14397
  return value.slice();
@@ -14383,7 +14405,12 @@ class ArrayDiff extends Diff {
14383
14405
  }
14384
14406
  var arrayDiff = new ArrayDiff;
14385
14407
 
14386
- // ../../node_modules/.bun/diff@8.0.2/node_modules/diff/libesm/patch/create.js
14408
+ // ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/patch/create.js
14409
+ var INCLUDE_HEADERS = {
14410
+ includeIndex: true,
14411
+ includeUnderline: true,
14412
+ includeFileHeaders: true
14413
+ };
14387
14414
  function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options2) {
14388
14415
  let optionsObj;
14389
14416
  if (!options2) {
@@ -14491,18 +14518,28 @@ function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, ne
14491
14518
  };
14492
14519
  }
14493
14520
  }
14494
- function formatPatch(patch) {
14521
+ function formatPatch(patch, headerOptions) {
14522
+ if (!headerOptions) {
14523
+ headerOptions = INCLUDE_HEADERS;
14524
+ }
14495
14525
  if (Array.isArray(patch)) {
14496
- return patch.map(formatPatch).join(`
14526
+ if (patch.length > 1 && !headerOptions.includeFileHeaders) {
14527
+ throw new Error("Cannot omit file headers on a multi-file patch. " + "(The result would be unparseable; how would a tool trying to apply " + "the patch know which changes are to which file?)");
14528
+ }
14529
+ return patch.map((p) => formatPatch(p, headerOptions)).join(`
14497
14530
  `);
14498
14531
  }
14499
14532
  const ret = [];
14500
- if (patch.oldFileName == patch.newFileName) {
14533
+ if (headerOptions.includeIndex && patch.oldFileName == patch.newFileName) {
14501
14534
  ret.push("Index: " + patch.oldFileName);
14502
14535
  }
14503
- ret.push("===================================================================");
14504
- ret.push("--- " + patch.oldFileName + (typeof patch.oldHeader === "undefined" ? "" : "\t" + patch.oldHeader));
14505
- ret.push("+++ " + patch.newFileName + (typeof patch.newHeader === "undefined" ? "" : "\t" + patch.newHeader));
14536
+ if (headerOptions.includeUnderline) {
14537
+ ret.push("===================================================================");
14538
+ }
14539
+ if (headerOptions.includeFileHeaders) {
14540
+ ret.push("--- " + patch.oldFileName + (typeof patch.oldHeader === "undefined" ? "" : "\t" + patch.oldHeader));
14541
+ ret.push("+++ " + patch.newFileName + (typeof patch.newHeader === "undefined" ? "" : "\t" + patch.newHeader));
14542
+ }
14506
14543
  for (let i = 0;i < patch.hunks.length; i++) {
14507
14544
  const hunk = patch.hunks[i];
14508
14545
  if (hunk.oldLines === 0) {
@@ -14529,14 +14566,14 @@ function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader
14529
14566
  if (!patchObj) {
14530
14567
  return;
14531
14568
  }
14532
- return formatPatch(patchObj);
14569
+ return formatPatch(patchObj, options2 === null || options2 === undefined ? undefined : options2.headerOptions);
14533
14570
  } else {
14534
14571
  const { callback } = options2;
14535
14572
  structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, Object.assign(Object.assign({}, options2), { callback: (patchObj) => {
14536
14573
  if (!patchObj) {
14537
14574
  callback(undefined);
14538
14575
  } else {
14539
- callback(formatPatch(patchObj));
14576
+ callback(formatPatch(patchObj, options2.headerOptions));
14540
14577
  }
14541
14578
  } }));
14542
14579
  }
@@ -14762,7 +14799,7 @@ async function runGhostInit(options2) {
14762
14799
  }
14763
14800
 
14764
14801
  // src/commands/ghost/opencode.ts
14765
- import { renameSync, rmSync } from "fs";
14802
+ import { existsSync as existsSync4, renameSync, rmSync, statSync as statSync2 } from "fs";
14766
14803
  import { copyFile, mkdir as mkdir5, readdir as readdir5 } from "fs/promises";
14767
14804
  import path7 from "path";
14768
14805
  var {Glob: Glob3 } = globalThis.Bun;
@@ -16448,7 +16485,12 @@ function createFileSync(tempDir, projectDir, options2) {
16448
16485
  stabilityThreshold: 200,
16449
16486
  pollInterval: 50
16450
16487
  },
16451
- ignored: (filePath) => isExcluded(relative4(tempDir, filePath), gitignore)
16488
+ ignored: (filePath) => {
16489
+ const relativePath = normalizePath2(relative4(tempDir, filePath));
16490
+ if (options2?.overlayFiles?.has(relativePath))
16491
+ return true;
16492
+ return isExcluded(relativePath, gitignore);
16493
+ }
16452
16494
  });
16453
16495
  watcher.on("error", (err) => {
16454
16496
  failures.push({ path: "<watcher>", error: err });
@@ -16534,10 +16576,12 @@ function createFileSync(tempDir, projectDir, options2) {
16534
16576
  }
16535
16577
 
16536
16578
  // src/utils/symlink-farm.ts
16579
+ var import_ignore2 = __toESM(require_ignore(), 1);
16537
16580
  import { randomBytes } from "crypto";
16581
+ import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync, statSync } from "fs";
16538
16582
  import { mkdir as mkdir4, readdir as readdir4, rename as rename2, rm as rm2, stat as stat5, symlink as symlink2 } from "fs/promises";
16539
- import { tmpdir } from "os";
16540
- import { dirname as dirname6, isAbsolute as isAbsolute2, join as join7, relative as relative5 } from "path";
16583
+ import { homedir as homedir2, tmpdir } from "os";
16584
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join7, normalize as normalize2, posix, relative as relative5, sep } from "path";
16541
16585
 
16542
16586
  // src/utils/pattern-filter.ts
16543
16587
  import path5 from "path";
@@ -16647,10 +16691,25 @@ async function createSymlinkFarm(sourceDir, options2) {
16647
16691
  const tempDir = join7(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
16648
16692
  await Bun.write(join7(tempDir, GHOST_MARKER_FILE), "");
16649
16693
  try {
16694
+ const projectDir = options2?.projectDir ?? sourceDir;
16695
+ const gitIgnore = await loadGitIgnoreStack(projectDir);
16650
16696
  const matcher = createPathMatcher(options2?.includePatterns ?? [], options2?.excludePatterns ?? []);
16651
- const plan = await computeSymlinkPlan(sourceDir, sourceDir, matcher);
16652
- await executeSymlinkPlan(plan, sourceDir, tempDir);
16653
- return tempDir;
16697
+ const maxFiles = options2?.maxFiles ?? 1e4;
16698
+ const state = { count: 0, plan: [] };
16699
+ const plan = await computeSymlinkPlan(sourceDir, sourceDir, matcher, state, maxFiles, gitIgnore, projectDir);
16700
+ const symlinkRoots = new Set;
16701
+ await executeSymlinkPlan(plan, sourceDir, tempDir, "", symlinkRoots);
16702
+ for (const op of state.plan) {
16703
+ const relPath = normalizePath3(relative5(sourceDir, op.source));
16704
+ const targetPath = join7(tempDir, relPath);
16705
+ const parentDir = dirname6(targetPath);
16706
+ if (parentDir !== tempDir) {
16707
+ await mkdir4(parentDir, { recursive: true });
16708
+ }
16709
+ await symlink2(op.source, targetPath);
16710
+ symlinkRoots.add(relPath);
16711
+ }
16712
+ return { tempDir, symlinkRoots };
16654
16713
  } catch (error) {
16655
16714
  await rm2(tempDir, { recursive: true, force: true }).catch(() => {});
16656
16715
  throw error;
@@ -16707,7 +16766,7 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
16707
16766
  }
16708
16767
  return cleanedCount;
16709
16768
  }
16710
- async function computeSymlinkPlan(sourceDir, projectRoot, matcher) {
16769
+ async function computeSymlinkPlan(sourceDir, projectRoot, matcher, state, maxFiles, gitIgnore, projectDir) {
16711
16770
  if (!isAbsolute2(sourceDir)) {
16712
16771
  throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
16713
16772
  }
@@ -16716,32 +16775,79 @@ async function computeSymlinkPlan(sourceDir, projectRoot, matcher) {
16716
16775
  files: [],
16717
16776
  partialDirs: new Map
16718
16777
  };
16778
+ let resolvedSource;
16779
+ let resolvedProject;
16780
+ try {
16781
+ resolvedSource = realpathSync(sourceDir);
16782
+ resolvedProject = realpathSync(projectDir);
16783
+ } catch {
16784
+ resolvedSource = sourceDir;
16785
+ resolvedProject = projectDir;
16786
+ }
16787
+ const rel = normalize2(relative5(resolvedProject, resolvedSource));
16788
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute2(rel)) {
16789
+ return plan;
16790
+ }
16791
+ const relativeDirPath = normalizePath3(relative5(resolvedProject, resolvedSource));
16792
+ const nestedGitignorePath = join7(sourceDir, ".gitignore");
16793
+ if (gitIgnore && existsSync3(nestedGitignorePath)) {
16794
+ try {
16795
+ addScopedRules(gitIgnore, readFileSync2(nestedGitignorePath, "utf-8"), relativeDirPath);
16796
+ } catch {}
16797
+ }
16719
16798
  const entries = await readdir4(sourceDir, { withFileTypes: true });
16720
16799
  for (const entry of entries) {
16800
+ if (entry.name === ".git")
16801
+ continue;
16721
16802
  const sourcePath = join7(sourceDir, entry.name);
16722
- const relativePath = normalizeForMatching(sourcePath, projectRoot);
16723
- const disposition = matcher.getDisposition(relativePath);
16803
+ const relativePath = normalizePath3(relative5(projectDir, sourcePath));
16804
+ const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
16805
+ const isGitignored = gitIgnore?.ignores(checkPath) ?? false;
16806
+ const matcherRelativePath = normalizeForMatching(sourcePath, projectRoot);
16807
+ const disposition = matcher.getDisposition(matcherRelativePath);
16724
16808
  if (disposition.type === "excluded") {
16725
16809
  continue;
16726
16810
  }
16727
- if (disposition.type === "included") {
16728
- if (entry.isDirectory()) {
16811
+ if (entry.isDirectory()) {
16812
+ if (isGitignored) {
16813
+ state.plan.push({ type: "directory", source: sourcePath });
16814
+ state.count += 1;
16815
+ if (maxFiles > 0 && state.count > maxFiles) {
16816
+ throw new FileLimitExceededError(state.count, maxFiles);
16817
+ }
16818
+ continue;
16819
+ }
16820
+ if (disposition.type === "included") {
16729
16821
  plan.wholeDirs.push(entry.name);
16730
- } else {
16731
- plan.files.push(entry.name);
16822
+ state.count += 1;
16823
+ if (maxFiles > 0 && state.count > maxFiles) {
16824
+ throw new FileLimitExceededError(state.count, maxFiles);
16825
+ }
16826
+ continue;
16732
16827
  }
16733
- continue;
16734
- }
16735
- if (entry.isDirectory()) {
16736
- const nestedPlan = await computeSymlinkPlan(sourcePath, projectRoot, matcher);
16828
+ const nestedPlan = await computeSymlinkPlan(sourcePath, projectRoot, matcher, state, maxFiles, gitIgnore, projectDir);
16737
16829
  plan.partialDirs.set(entry.name, nestedPlan);
16738
16830
  } else {
16831
+ if (isGitignored)
16832
+ continue;
16833
+ if (disposition.type === "included") {
16834
+ plan.files.push(entry.name);
16835
+ state.count += 1;
16836
+ if (maxFiles > 0 && state.count > maxFiles) {
16837
+ throw new FileLimitExceededError(state.count, maxFiles);
16838
+ }
16839
+ continue;
16840
+ }
16739
16841
  plan.files.push(entry.name);
16842
+ state.count += 1;
16843
+ if (maxFiles > 0 && state.count > maxFiles) {
16844
+ throw new FileLimitExceededError(state.count, maxFiles);
16845
+ }
16740
16846
  }
16741
16847
  }
16742
16848
  return plan;
16743
16849
  }
16744
- async function executeSymlinkPlan(plan, sourceRoot, targetRoot) {
16850
+ async function executeSymlinkPlan(plan, sourceRoot, targetRoot, relativePath = "", symlinkRoots) {
16745
16851
  if (!isAbsolute2(sourceRoot)) {
16746
16852
  throw new Error(`sourceRoot must be an absolute path, got: ${sourceRoot}`);
16747
16853
  }
@@ -16752,18 +16858,99 @@ async function executeSymlinkPlan(plan, sourceRoot, targetRoot) {
16752
16858
  const sourcePath = join7(sourceRoot, dirName);
16753
16859
  const targetPath = join7(targetRoot, dirName);
16754
16860
  await symlink2(sourcePath, targetPath);
16861
+ symlinkRoots?.add(relativePath ? `${relativePath}/${dirName}` : dirName);
16755
16862
  }
16756
16863
  for (const fileName of plan.files) {
16757
16864
  const sourcePath = join7(sourceRoot, fileName);
16758
16865
  const targetPath = join7(targetRoot, fileName);
16759
16866
  await symlink2(sourcePath, targetPath);
16867
+ symlinkRoots?.add(relativePath ? `${relativePath}/${fileName}` : fileName);
16760
16868
  }
16761
16869
  for (const [dirName, nestedPlan] of plan.partialDirs) {
16762
16870
  const sourcePath = join7(sourceRoot, dirName);
16763
16871
  const targetPath = join7(targetRoot, dirName);
16764
16872
  await mkdir4(targetPath, { recursive: true });
16765
- await executeSymlinkPlan(nestedPlan, sourcePath, targetPath);
16873
+ const nestedRelativePath = relativePath ? `${relativePath}/${dirName}` : dirName;
16874
+ await executeSymlinkPlan(nestedPlan, sourcePath, targetPath, nestedRelativePath, symlinkRoots);
16875
+ }
16876
+ }
16877
+ function normalizePath3(p) {
16878
+ return p.replace(/\\/g, "/").replace(/\/$/, "");
16879
+ }
16880
+ function getGitDir(projectDir) {
16881
+ const gitPath = join7(projectDir, ".git");
16882
+ try {
16883
+ const stats = statSync(gitPath);
16884
+ if (stats.isDirectory())
16885
+ return gitPath;
16886
+ if (stats.isFile()) {
16887
+ const content2 = readFileSync2(gitPath, "utf-8").trim();
16888
+ const match = content2.match(/^gitdir:\s*(.+)$/);
16889
+ if (match?.[1]) {
16890
+ const gitdir = match[1];
16891
+ return isAbsolute2(gitdir) ? gitdir : join7(projectDir, gitdir);
16892
+ }
16893
+ }
16894
+ } catch {}
16895
+ return null;
16896
+ }
16897
+ function getGlobalGitignore() {
16898
+ const gitconfigPath = join7(homedir2(), ".gitconfig");
16899
+ try {
16900
+ const content2 = readFileSync2(gitconfigPath, "utf-8");
16901
+ const match = content2.match(/excludesfile\s*=\s*(.+)/i);
16902
+ if (match?.[1]) {
16903
+ const p = match[1].trim().replace(/^~/, homedir2());
16904
+ if (existsSync3(p))
16905
+ return p;
16906
+ }
16907
+ } catch {}
16908
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join7(homedir2(), ".config");
16909
+ const xdgIgnore = join7(xdgConfig, "git", "ignore");
16910
+ return existsSync3(xdgIgnore) ? xdgIgnore : null;
16911
+ }
16912
+ function addScopedRules(ig, content2, subdir) {
16913
+ for (const line of content2.split(/\r?\n/)) {
16914
+ const trimmed = line.trim();
16915
+ if (!trimmed || trimmed.startsWith("#"))
16916
+ continue;
16917
+ let pattern = trimmed;
16918
+ let isNegation = false;
16919
+ if (pattern.startsWith("!")) {
16920
+ isNegation = true;
16921
+ pattern = pattern.slice(1);
16922
+ }
16923
+ if (pattern.startsWith("/")) {
16924
+ pattern = pattern.slice(1);
16925
+ }
16926
+ const scoped = subdir ? posix.join(subdir, pattern) : pattern;
16927
+ ig.add(isNegation ? `!${scoped}` : scoped);
16928
+ }
16929
+ }
16930
+ async function loadGitIgnoreStack(projectDir) {
16931
+ const gitDir = getGitDir(projectDir);
16932
+ if (!gitDir)
16933
+ return null;
16934
+ const ig = import_ignore2.default();
16935
+ const globalPath = getGlobalGitignore();
16936
+ if (globalPath) {
16937
+ try {
16938
+ addScopedRules(ig, readFileSync2(globalPath, "utf-8"), "");
16939
+ } catch {}
16766
16940
  }
16941
+ const excludePath = join7(gitDir, "info", "exclude");
16942
+ try {
16943
+ if (existsSync3(excludePath)) {
16944
+ addScopedRules(ig, readFileSync2(excludePath, "utf-8"), "");
16945
+ }
16946
+ } catch {}
16947
+ const rootGitignore = join7(projectDir, ".gitignore");
16948
+ try {
16949
+ if (existsSync3(rootGitignore)) {
16950
+ addScopedRules(ig, readFileSync2(rootGitignore, "utf-8"), "");
16951
+ }
16952
+ } catch {}
16953
+ return ig;
16767
16954
  }
16768
16955
 
16769
16956
  // src/utils/terminal-title.ts
@@ -16819,11 +17006,77 @@ function formatTerminalName(cwd, profileName, gitInfo) {
16819
17006
  }
16820
17007
 
16821
17008
  // src/commands/ghost/opencode.ts
17009
+ var PROFILE_OVERLAY_ALLOWED = [
17010
+ "opencode.jsonc",
17011
+ "opencode.json",
17012
+ "opencode.yaml",
17013
+ "AGENTS.md",
17014
+ "CLAUDE.md",
17015
+ "CONTEXT.md",
17016
+ /^\.opencode(\/|$)/
17017
+ ];
17018
+ var defaultFs = {
17019
+ existsSync: existsSync4,
17020
+ statSync: statSync2
17021
+ };
17022
+ function resolveProjectPath(args, cwd, fs = defaultFs) {
17023
+ if (args.length === 0) {
17024
+ return { projectDir: cwd, remainingArgs: [], explicitPath: null };
17025
+ }
17026
+ const firstArg = args[0];
17027
+ if (firstArg === undefined) {
17028
+ return { projectDir: cwd, remainingArgs: [], explicitPath: null };
17029
+ }
17030
+ let pathArg;
17031
+ let remainingArgs;
17032
+ if (firstArg === "--") {
17033
+ const secondArg = args[1];
17034
+ if (secondArg === undefined) {
17035
+ return { projectDir: cwd, remainingArgs: [], explicitPath: null };
17036
+ }
17037
+ pathArg = secondArg;
17038
+ remainingArgs = args.slice(2);
17039
+ } else {
17040
+ const potentialPath = isAbsolutePath(firstArg) ? firstArg : path7.resolve(cwd, firstArg);
17041
+ if (!fs.existsSync(potentialPath)) {
17042
+ return { projectDir: cwd, remainingArgs: args, explicitPath: null };
17043
+ }
17044
+ const stat7 = fs.statSync(potentialPath);
17045
+ if (!stat7.isDirectory()) {
17046
+ return { projectDir: cwd, remainingArgs: args, explicitPath: null };
17047
+ }
17048
+ pathArg = firstArg;
17049
+ remainingArgs = args.slice(1);
17050
+ }
17051
+ const projectDir = isAbsolutePath(pathArg) ? pathArg : path7.resolve(cwd, pathArg);
17052
+ if (!fs.existsSync(projectDir)) {
17053
+ throw new NotFoundError(`Project path does not exist: ${pathArg}`);
17054
+ }
17055
+ const stat6 = fs.statSync(projectDir);
17056
+ if (!stat6.isDirectory()) {
17057
+ throw new ValidationError(`Project path is not a directory: ${pathArg}`);
17058
+ }
17059
+ return {
17060
+ projectDir,
17061
+ remainingArgs,
17062
+ explicitPath: pathArg
17063
+ };
17064
+ }
17065
+ function isAllowedOverlayFile(relativePath) {
17066
+ return PROFILE_OVERLAY_ALLOWED.some((pattern) => typeof pattern === "string" ? relativePath === pattern : pattern.test(relativePath));
17067
+ }
17068
+ function isWithinSymlinkRoot(relativePath, symlinkRoots) {
17069
+ for (const root of symlinkRoots) {
17070
+ if (relativePath === root || relativePath.startsWith(`${root}/`)) {
17071
+ return true;
17072
+ }
17073
+ }
17074
+ return false;
17075
+ }
16822
17076
  function registerGhostOpenCodeCommand(parent) {
16823
- parent.command("opencode").description("Launch OpenCode with ghost mode configuration").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").addOption(sharedOptions.json()).addOption(sharedOptions.quiet()).allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
17077
+ parent.command("opencode").description("Launch OpenCode with ghost mode configuration (first arg can be project path)").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").addOption(sharedOptions.json()).addOption(sharedOptions.quiet()).allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
16824
17078
  try {
16825
- const args = command.args;
16826
- await runGhostOpenCode(args, options2);
17079
+ await runGhostOpenCode(command.args, options2);
16827
17080
  } catch (error) {
16828
17081
  handleError(error, { json: options2.json });
16829
17082
  }
@@ -16844,17 +17097,21 @@ async function runGhostOpenCode(args, options2) {
16844
17097
  if (!hasOpencodeConfig && !options2.quiet) {
16845
17098
  logger.warn(`No opencode.jsonc found at ${profileOpencodePath}. Create one to customize OpenCode settings.`);
16846
17099
  }
16847
- const cwd = process.cwd();
16848
- const gitContext = await detectGitRepo(cwd);
17100
+ const { projectDir, remainingArgs, explicitPath } = resolveProjectPath(args, process.cwd());
17101
+ if (explicitPath && !options2.quiet) {
17102
+ logger.info(`Using project directory: ${projectDir}`);
17103
+ }
17104
+ const gitContext = await detectGitRepo(projectDir);
16849
17105
  const ghostConfig = profile.ghost;
16850
- const tempDir = await createSymlinkFarm(cwd, {
17106
+ const { tempDir, symlinkRoots } = await createSymlinkFarm(projectDir, {
16851
17107
  includePatterns: ghostConfig.include,
16852
- excludePatterns: ghostConfig.exclude
17108
+ excludePatterns: ghostConfig.exclude,
17109
+ maxFiles: ghostConfig.maxFiles
16853
17110
  });
16854
- const overlayFiles = await injectProfileOverlay(tempDir, profileDir, ghostConfig.include);
17111
+ const overlayFiles = await injectProfileOverlay(tempDir, profileDir, ghostConfig.include, symlinkRoots);
16855
17112
  let cleanupDone = false;
16856
17113
  let fileSync;
16857
- fileSync = createFileSync(tempDir, cwd, { overlayFiles });
17114
+ fileSync = createFileSync(tempDir, projectDir, { overlayFiles });
16858
17115
  const performCleanup = async () => {
16859
17116
  if (cleanupDone)
16860
17117
  return;
@@ -16885,11 +17142,11 @@ async function runGhostOpenCode(args, options2) {
16885
17142
  process.on("SIGTERM", sigtermHandler);
16886
17143
  if (shouldRename) {
16887
17144
  saveTerminalTitle();
16888
- const gitInfo = await getGitInfo(cwd);
16889
- setTerminalName(formatTerminalName(cwd, profileName, gitInfo));
17145
+ const gitInfo = await getGitInfo(projectDir);
17146
+ setTerminalName(formatTerminalName(projectDir, profileName, gitInfo));
16890
17147
  }
16891
17148
  proc = Bun.spawn({
16892
- cmd: ["opencode", ...args],
17149
+ cmd: ["opencode", ...remainingArgs],
16893
17150
  cwd: tempDir,
16894
17151
  env: {
16895
17152
  ...process.env,
@@ -16942,7 +17199,7 @@ function userExplicitlyIncluded(relativePath, compiledPatterns) {
16942
17199
  return false;
16943
17200
  return compiledPatterns.some((glob) => glob.match(relativePath));
16944
17201
  }
16945
- async function injectProfileOverlay(tempDir, profileDir, includePatterns) {
17202
+ async function injectProfileOverlay(tempDir, profileDir, includePatterns, symlinkRoots) {
16946
17203
  const entries = await readdir5(profileDir, { withFileTypes: true, recursive: true });
16947
17204
  const injectedFiles = new Set;
16948
17205
  const compiledIncludePatterns = includePatterns.map((p) => new Glob3(p));
@@ -16952,11 +17209,17 @@ async function injectProfileOverlay(tempDir, profileDir, includePatterns) {
16952
17209
  continue;
16953
17210
  if (relativePath === ".gitignore")
16954
17211
  continue;
17212
+ if (!isAllowedOverlayFile(relativePath))
17213
+ continue;
17214
+ if (isWithinSymlinkRoot(relativePath, symlinkRoots))
17215
+ continue;
16955
17216
  if (userExplicitlyIncluded(relativePath, compiledIncludePatterns))
16956
17217
  continue;
16957
17218
  if (entry.isDirectory())
16958
17219
  continue;
16959
17220
  const destPath = path7.join(tempDir, relativePath);
17221
+ if (existsSync4(destPath))
17222
+ continue;
16960
17223
  await mkdir5(path7.dirname(destPath), { recursive: true });
16961
17224
  await copyFile(path7.join(entry.parentPath, entry.name), destPath);
16962
17225
  injectedFiles.add(normalizePath2(relativePath));
@@ -17488,850 +17751,864 @@ function registerGhostSearchCommand(parent) {
17488
17751
  });
17489
17752
  }
17490
17753
 
17491
- // src/commands/ghost/index.ts
17492
- function registerGhostCommand(program2) {
17493
- const ghost = program2.command("ghost").alias("g").description("Ghost mode - work without local config files");
17494
- registerGhostInitCommand(ghost);
17495
- registerGhostConfigCommand(ghost);
17496
- registerGhostRegistryCommand(ghost);
17497
- registerGhostAddCommand(ghost);
17498
- registerGhostSearchCommand(ghost);
17499
- registerGhostOpenCodeCommand(ghost);
17500
- registerGhostProfileCommand(ghost);
17501
- }
17502
-
17503
- // src/commands/init.ts
17504
- import { existsSync as existsSync3 } from "fs";
17505
- import { cp, mkdir as mkdir6, readdir as readdir6, readFile, rm as rm3, writeFile as writeFile2 } from "fs/promises";
17506
- import { join as join8 } from "path";
17507
- var TEMPLATE_REPO = "kdcokenny/ocx";
17508
- var TEMPLATE_PATH = "examples/registry-starter";
17509
- function registerInitCommand(program2) {
17510
- program2.command("init [directory]").description("Initialize OCX configuration in your project").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").option("-f, --force", "Overwrite existing files (registry mode only)").action(async (directory, options2) => {
17754
+ // src/commands/update.ts
17755
+ import { createHash as createHash2 } from "crypto";
17756
+ import { existsSync as existsSync5 } from "fs";
17757
+ import { mkdir as mkdir6, writeFile as writeFile2 } from "fs/promises";
17758
+ import { dirname as dirname7, join as join8 } from "path";
17759
+ function registerUpdateCommand(program2) {
17760
+ 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) => {
17511
17761
  try {
17512
- if (options2.registry) {
17513
- await runInitRegistry(directory, options2);
17514
- } else {
17515
- await runInit(options2);
17516
- }
17762
+ await runUpdate(components, options2);
17517
17763
  } catch (error) {
17518
17764
  handleError(error, { json: options2.json });
17519
17765
  }
17520
17766
  });
17521
17767
  }
17522
- async function runInit(options2) {
17768
+ async function runUpdate(componentNames, options2) {
17523
17769
  const cwd = options2.cwd ?? process.cwd();
17524
- const configPath = join8(cwd, "ocx.jsonc");
17525
- if (existsSync3(configPath)) {
17526
- throw new ConflictError(`ocx.jsonc already exists at ${configPath}
17770
+ const provider = await LocalConfigProvider.create(cwd);
17771
+ await runUpdateCore(componentNames, options2, provider);
17772
+ }
17773
+ async function runUpdateCore(componentNames, options2, provider) {
17774
+ const lockPath = join8(provider.cwd, "ocx.lock");
17775
+ const registries = provider.getRegistries();
17776
+ const lock = await readOcxLock(provider.cwd);
17777
+ if (!lock || Object.keys(lock.installed).length === 0) {
17778
+ throw new ValidationError("Nothing installed yet. Run 'ocx add <component>' first.");
17779
+ }
17780
+ const hasComponents = componentNames.length > 0;
17781
+ const hasAll = options2.all === true;
17782
+ const hasRegistry = options2.registry !== undefined;
17783
+ if (!hasComponents && !hasAll && !hasRegistry) {
17784
+ throw new ValidationError(`Specify components, use --all, or use --registry <name>.
17527
17785
 
17528
- ` + `To reset, delete the config and run init again:
17529
- ` + ` rm ${configPath} && ocx init`);
17786
+ ` + `Examples:
17787
+ ` + ` ocx update kdco/agents # Update specific component
17788
+ ` + ` ocx update --all # Update all installed components
17789
+ ` + " ocx update --registry kdco # Update all from a registry");
17530
17790
  }
17531
- const spin = options2.quiet ? null : createSpinner({ text: "Initializing OCX..." });
17532
- spin?.start();
17533
- try {
17534
- const rawConfig = {
17535
- $schema: OCX_SCHEMA_URL,
17536
- registries: {}
17537
- };
17538
- const config = ocxConfigSchema.parse(rawConfig);
17539
- const content2 = JSON.stringify(config, null, 2);
17540
- await writeFile2(configPath, content2, "utf-8");
17541
- const opencodeResult = await ensureOpencodeConfig(cwd);
17542
- spin?.succeed("Initialized OCX configuration");
17543
- if (options2.json) {
17544
- console.log(JSON.stringify({
17545
- success: true,
17546
- path: configPath,
17547
- opencodePath: opencodeResult.path,
17548
- opencodeCreated: opencodeResult.created
17549
- }));
17550
- } else if (!options2.quiet) {
17551
- logger.info(`Created ${configPath}`);
17552
- if (opencodeResult.created) {
17553
- logger.info(`Created ${opencodeResult.path}`);
17554
- }
17555
- logger.info("");
17556
- logger.info("Next steps:");
17557
- logger.info(" 1. Add a registry: ocx registry add <url>");
17558
- logger.info(" 2. Install components: ocx add <component>");
17559
- }
17560
- } catch (error) {
17561
- spin?.fail("Failed to initialize");
17562
- throw error;
17791
+ if (hasAll && hasComponents) {
17792
+ throw new ValidationError(`Cannot specify components with --all.
17793
+ ` + "Use either 'ocx update --all' or 'ocx update <components>'.");
17563
17794
  }
17564
- }
17565
- async function runInitRegistry(directory, options2) {
17566
- const cwd = directory ?? options2.cwd ?? process.cwd();
17567
- const namespace = options2.namespace ?? "my-registry";
17568
- const author = options2.author ?? "Your Name";
17569
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(namespace)) {
17570
- throw new ValidationError("Invalid namespace format: must start with letter/number, use hyphens only between segments (e.g., 'my-registry')");
17795
+ if (hasRegistry && hasComponents) {
17796
+ throw new ValidationError(`Cannot specify components with --registry.
17797
+ ` + "Use either 'ocx update --registry <name>' or 'ocx update <components>'.");
17571
17798
  }
17572
- const existingFiles = await readdir6(cwd).catch(() => []);
17573
- const hasVisibleFiles = existingFiles.some((f) => !f.startsWith("."));
17574
- if (hasVisibleFiles && !options2.force) {
17575
- throw new ConflictError("Directory is not empty. Use --force to overwrite existing files.");
17799
+ if (hasAll && hasRegistry) {
17800
+ throw new ValidationError(`Cannot use --all with --registry.
17801
+ ` + "Use either 'ocx update --all' or 'ocx update --registry <name>'.");
17576
17802
  }
17577
- const spin = options2.quiet ? null : createSpinner({ text: "Scaffolding registry..." });
17578
- spin?.start();
17579
- try {
17580
- if (spin)
17581
- spin.text = options2.local ? "Copying template..." : "Fetching template...";
17582
- if (options2.local) {
17583
- await mkdir6(cwd, { recursive: true });
17584
- await copyDir(options2.local, cwd);
17585
- } else {
17586
- const version = options2.canary ? "main" : await getLatestVersion();
17587
- await fetchAndExtractTemplate(cwd, version, options2.verbose);
17588
- }
17589
- if (spin)
17590
- spin.text = "Configuring project...";
17591
- await replacePlaceholders(cwd, { namespace, author });
17592
- spin?.succeed(`Created registry: ${namespace}`);
17593
- if (options2.json) {
17594
- console.log(JSON.stringify({ success: true, namespace, path: cwd }));
17595
- } else if (!options2.quiet) {
17596
- logger.info("");
17597
- logger.info("Next steps:");
17598
- logger.info(" 1. bun install");
17599
- logger.info(" 2. Edit registry.jsonc with your components");
17600
- logger.info(" 3. bun run build");
17601
- logger.info("");
17602
- logger.info("Deploy to:");
17603
- logger.info(" Cloudflare: bunx wrangler deploy");
17604
- logger.info(" Vercel: vercel");
17605
- logger.info(" Netlify: netlify deploy");
17803
+ const parsedComponents = componentNames.map(parseComponentSpec);
17804
+ for (const spec of parsedComponents) {
17805
+ if (spec.version !== undefined && spec.version === "") {
17806
+ throw new ValidationError(`Invalid version specifier in '${spec.component}@'.` + `
17807
+ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.`);
17606
17808
  }
17607
- } catch (error) {
17608
- spin?.fail("Failed to scaffold registry");
17609
- throw error;
17610
- }
17611
- }
17612
- async function copyDir(src, dest) {
17613
- await cp(src, dest, { recursive: true });
17614
- }
17615
- async function getLatestVersion() {
17616
- const pkgPath = new URL("../../package.json", import.meta.url);
17617
- const pkgContent = await readFile(pkgPath);
17618
- const pkg = JSON.parse(pkgContent.toString());
17619
- return `v${pkg.version}`;
17620
- }
17621
- async function fetchAndExtractTemplate(destDir, version, verbose) {
17622
- const ref = version === "main" ? "heads/main" : `tags/${version}`;
17623
- const tarballUrl = `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
17624
- if (verbose) {
17625
- logger.info(`Fetching ${tarballUrl}`);
17626
17809
  }
17627
- const response = await fetch(tarballUrl);
17628
- if (!response.ok || !response.body) {
17629
- throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
17810
+ const componentsToUpdate = resolveComponentsToUpdate(lock, parsedComponents, options2);
17811
+ if (componentsToUpdate.length === 0) {
17812
+ if (hasRegistry) {
17813
+ throw new NotFoundError(`No installed components from registry '${options2.registry}'.`);
17814
+ }
17815
+ throw new NotFoundError("No matching components found to update.");
17630
17816
  }
17631
- const tempDir = join8(destDir, ".ocx-temp");
17632
- await mkdir6(tempDir, { recursive: true });
17817
+ const spin = options2.quiet ? null : createSpinner({ text: "Checking for updates..." });
17818
+ spin?.start();
17819
+ const results = [];
17820
+ const updates = [];
17633
17821
  try {
17634
- const tarPath = join8(tempDir, "template.tar.gz");
17635
- const arrayBuffer = await response.arrayBuffer();
17636
- await writeFile2(tarPath, Buffer.from(arrayBuffer));
17637
- const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
17638
- stdout: "ignore",
17639
- stderr: "pipe"
17640
- });
17641
- const exitCode = await proc.exited;
17642
- if (exitCode !== 0) {
17643
- const stderr = await new Response(proc.stderr).text();
17644
- throw new Error(`Failed to extract template: ${stderr}`);
17645
- }
17646
- const extractedDirs = await readdir6(tempDir);
17647
- const extractedDir = extractedDirs.find((d) => d.startsWith("ocx-"));
17648
- if (!extractedDir) {
17649
- throw new Error("Failed to find extracted template directory");
17822
+ for (const spec of componentsToUpdate) {
17823
+ const qualifiedName = spec.component;
17824
+ const lockEntry = lock.installed[qualifiedName];
17825
+ if (!lockEntry) {
17826
+ throw new NotFoundError(`Component '${qualifiedName}' not found in lock file.`);
17827
+ }
17828
+ const { namespace, component: componentName } = parseQualifiedComponent(qualifiedName);
17829
+ const regConfig = registries[namespace];
17830
+ if (!regConfig) {
17831
+ throw new ConfigError(`Registry '${namespace}' not configured. Component '${qualifiedName}' cannot be updated.`);
17832
+ }
17833
+ const fetchResult = await fetchComponentVersion(regConfig.url, componentName, spec.version);
17834
+ const manifest = fetchResult.manifest;
17835
+ const version = fetchResult.version;
17836
+ const normalizedManifest = normalizeComponentManifest(manifest);
17837
+ const files = [];
17838
+ for (const file of normalizedManifest.files) {
17839
+ const content2 = await fetchFileContent(regConfig.url, componentName, file.path);
17840
+ files.push({ path: file.path, content: Buffer.from(content2) });
17841
+ }
17842
+ const newHash = await hashBundle2(files);
17843
+ if (newHash === lockEntry.hash) {
17844
+ results.push({
17845
+ qualifiedName,
17846
+ oldVersion: lockEntry.version,
17847
+ newVersion: version,
17848
+ status: "up-to-date"
17849
+ });
17850
+ } else if (options2.dryRun) {
17851
+ results.push({
17852
+ qualifiedName,
17853
+ oldVersion: lockEntry.version,
17854
+ newVersion: version,
17855
+ status: "would-update"
17856
+ });
17857
+ } else {
17858
+ results.push({
17859
+ qualifiedName,
17860
+ oldVersion: lockEntry.version,
17861
+ newVersion: version,
17862
+ status: "updated"
17863
+ });
17864
+ updates.push({
17865
+ qualifiedName,
17866
+ component: normalizedManifest,
17867
+ files,
17868
+ newHash,
17869
+ newVersion: version,
17870
+ baseUrl: regConfig.url
17871
+ });
17872
+ }
17650
17873
  }
17651
- const templateDir = join8(tempDir, extractedDir, TEMPLATE_PATH);
17652
- await copyDir(templateDir, destDir);
17653
- } finally {
17654
- await rm3(tempDir, { recursive: true, force: true });
17655
- }
17656
- }
17657
- async function replacePlaceholders(dir, values) {
17658
- const filesToProcess = [
17659
- "registry.jsonc",
17660
- "package.json",
17661
- "wrangler.jsonc",
17662
- "README.md",
17663
- "AGENTS.md"
17664
- ];
17665
- for (const file of filesToProcess) {
17666
- const filePath = join8(dir, file);
17667
- if (!existsSync3(filePath))
17668
- continue;
17669
- let content2 = await readFile(filePath).then((b) => b.toString());
17670
- content2 = content2.replace(/my-registry/g, values.namespace);
17671
- content2 = content2.replace(/Your Name/g, values.author);
17672
- await writeFile2(filePath, content2);
17673
- }
17674
- }
17675
-
17676
- // src/self-update/version-provider.ts
17677
- class BuildTimeVersionProvider {
17678
- version = "1.3.0";
17679
- }
17680
- var defaultVersionProvider = new BuildTimeVersionProvider;
17681
-
17682
- // src/self-update/check.ts
17683
- var VERSION_CHECK_TIMEOUT_MS = 200;
17684
- var PACKAGE_NAME = "ocx";
17685
- function parseVersion2(v) {
17686
- const [main2 = ""] = v.split("-");
17687
- const parts = main2.split(".");
17688
- const major = parseInt(parts[0] ?? "", 10);
17689
- const minor = parseInt(parts[1] ?? "", 10);
17690
- const patch = parseInt(parts[2] ?? "", 10);
17691
- if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) {
17692
- return null;
17693
- }
17694
- return { major, minor, patch };
17695
- }
17696
- function compareSemver2(a, b) {
17697
- const vA = parseVersion2(a);
17698
- const vB = parseVersion2(b);
17699
- if (!vA || !vB) {
17700
- return null;
17701
- }
17702
- if (vA.major !== vB.major)
17703
- return vA.major - vB.major;
17704
- if (vA.minor !== vB.minor)
17705
- return vA.minor - vB.minor;
17706
- return vA.patch - vB.patch;
17707
- }
17708
- async function checkForUpdate(versionProvider) {
17709
- const provider = versionProvider ?? defaultVersionProvider;
17710
- const current = provider.version || "0.0.0-dev";
17711
- if (current === "0.0.0-dev") {
17712
- return null;
17713
- }
17714
- try {
17715
- const result = await fetchPackageVersion(PACKAGE_NAME, undefined, AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS));
17716
- const latest = result.version;
17717
- const comparison = compareSemver2(latest, current);
17718
- if (comparison === null) {
17719
- return null;
17874
+ spin?.succeed(`Checked ${componentsToUpdate.length} component(s)`);
17875
+ if (options2.dryRun) {
17876
+ outputDryRun(results, options2);
17877
+ return;
17720
17878
  }
17721
- return {
17722
- current,
17723
- latest,
17724
- updateAvailable: comparison > 0
17725
- };
17726
- } catch {
17727
- return null;
17879
+ if (updates.length === 0) {
17880
+ if (!options2.quiet && !options2.json) {
17881
+ logger.info("");
17882
+ logger.success("All components are up to date.");
17883
+ }
17884
+ if (options2.json) {
17885
+ console.log(JSON.stringify({ success: true, updated: [], upToDate: results }, null, 2));
17886
+ }
17887
+ return;
17888
+ }
17889
+ const installSpin = options2.quiet ? null : createSpinner({ text: "Updating components..." });
17890
+ installSpin?.start();
17891
+ for (const update of updates) {
17892
+ for (const file of update.files) {
17893
+ const fileObj = update.component.files.find((f) => f.path === file.path);
17894
+ if (!fileObj)
17895
+ continue;
17896
+ const targetPath = join8(provider.cwd, fileObj.target);
17897
+ const targetDir = dirname7(targetPath);
17898
+ if (!existsSync5(targetDir)) {
17899
+ await mkdir6(targetDir, { recursive: true });
17900
+ }
17901
+ await writeFile2(targetPath, file.content);
17902
+ if (options2.verbose) {
17903
+ logger.info(` \u2713 Updated ${fileObj.target}`);
17904
+ }
17905
+ }
17906
+ const existingEntry = lock.installed[update.qualifiedName];
17907
+ if (!existingEntry) {
17908
+ throw new NotFoundError(`Component '${update.qualifiedName}' not found in lock file.`);
17909
+ }
17910
+ lock.installed[update.qualifiedName] = {
17911
+ registry: existingEntry.registry,
17912
+ version: update.newVersion,
17913
+ hash: update.newHash,
17914
+ files: existingEntry.files,
17915
+ installedAt: existingEntry.installedAt,
17916
+ updatedAt: new Date().toISOString()
17917
+ };
17918
+ }
17919
+ installSpin?.succeed(`Updated ${updates.length} component(s)`);
17920
+ await writeFile2(lockPath, JSON.stringify(lock, null, 2), "utf-8");
17921
+ if (options2.json) {
17922
+ console.log(JSON.stringify({
17923
+ success: true,
17924
+ updated: results.filter((r2) => r2.status === "updated"),
17925
+ upToDate: results.filter((r2) => r2.status === "up-to-date")
17926
+ }, null, 2));
17927
+ } else if (!options2.quiet) {
17928
+ logger.info("");
17929
+ for (const result of results) {
17930
+ if (result.status === "updated") {
17931
+ logger.info(` \u2713 ${result.qualifiedName} (${result.oldVersion} \u2192 ${result.newVersion})`);
17932
+ } else if (result.status === "up-to-date" && options2.verbose) {
17933
+ logger.info(` \u25CB ${result.qualifiedName} (already up to date)`);
17934
+ }
17935
+ }
17936
+ logger.info("");
17937
+ logger.success(`Done! Updated ${updates.length} component(s).`);
17938
+ }
17939
+ } catch (error) {
17940
+ spin?.fail("Failed to check for updates");
17941
+ throw error;
17728
17942
  }
17729
17943
  }
17730
-
17731
- // src/self-update/detect-method.ts
17732
- function parseInstallMethod(input) {
17733
- const VALID_METHODS = ["curl", "npm", "yarn", "pnpm", "bun"];
17734
- const method = VALID_METHODS.find((m) => m === input);
17735
- if (!method) {
17736
- throw new SelfUpdateError(`Invalid install method: "${input}"
17737
- Valid methods: ${VALID_METHODS.join(", ")}`);
17944
+ function parseComponentSpec(spec) {
17945
+ const atIndex = spec.lastIndexOf("@");
17946
+ if (atIndex <= 0) {
17947
+ return { component: spec };
17738
17948
  }
17739
- return method;
17949
+ return {
17950
+ component: spec.slice(0, atIndex),
17951
+ version: spec.slice(atIndex + 1)
17952
+ };
17740
17953
  }
17741
- var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
17742
- var isTempExecution = (path8) => path8.includes("/_npx/") || path8.includes("/.cache/bunx/") || path8.includes("/.pnpm/_temp/");
17743
- var isYarnGlobalInstall = (path8) => path8.includes("/.yarn/global") || path8.includes("/.config/yarn/global");
17744
- var isPnpmGlobalInstall = (path8) => path8.includes("/.pnpm/") || path8.includes("/pnpm/global");
17745
- var isBunGlobalInstall = (path8) => path8.includes("/.bun/bin") || path8.includes("/.bun/install/global");
17746
- var isNpmGlobalInstall = (path8) => path8.includes("/.npm/") || path8.includes("/node_modules/");
17747
- function detectInstallMethod() {
17748
- if (isCompiledBinary()) {
17749
- return "curl";
17954
+ function resolveComponentsToUpdate(lock, parsedComponents, options2) {
17955
+ const installedComponents = Object.keys(lock.installed);
17956
+ if (options2.all) {
17957
+ return installedComponents.map((c) => ({ component: c }));
17750
17958
  }
17751
- const scriptPath = process.argv[1] ?? "";
17752
- if (isTempExecution(scriptPath))
17753
- return "unknown";
17754
- if (isYarnGlobalInstall(scriptPath))
17755
- return "yarn";
17756
- if (isPnpmGlobalInstall(scriptPath))
17757
- return "pnpm";
17758
- if (isBunGlobalInstall(scriptPath))
17759
- return "bun";
17760
- if (isNpmGlobalInstall(scriptPath))
17761
- return "npm";
17762
- const userAgent = process.env.npm_config_user_agent ?? "";
17763
- if (userAgent.includes("yarn"))
17764
- return "yarn";
17765
- if (userAgent.includes("pnpm"))
17766
- return "pnpm";
17767
- if (userAgent.includes("bun"))
17768
- return "bun";
17769
- if (userAgent.includes("npm"))
17770
- return "npm";
17771
- return "unknown";
17772
- }
17773
- function getExecutablePath() {
17774
- if (typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/")) {
17775
- return process.execPath;
17959
+ if (options2.registry) {
17960
+ return installedComponents.filter((name) => {
17961
+ const entry = lock.installed[name];
17962
+ return entry?.registry === options2.registry;
17963
+ }).map((c) => ({ component: c }));
17776
17964
  }
17777
- return process.argv[1] ?? process.execPath;
17778
- }
17965
+ const result = [];
17966
+ for (const spec of parsedComponents) {
17967
+ const name = spec.component;
17968
+ if (!name.includes("/")) {
17969
+ const suggestions = installedComponents.filter((installed) => installed.endsWith(`/${name}`));
17970
+ if (suggestions.length === 1) {
17971
+ throw new ValidationError(`Ambiguous component '${name}'. Did you mean '${suggestions[0]}'?`);
17972
+ }
17973
+ if (suggestions.length > 1) {
17974
+ throw new ValidationError(`Ambiguous component '${name}'. Found in multiple registries:
17975
+ ` + suggestions.map((s) => ` - ${s}`).join(`
17976
+ `) + `
17779
17977
 
17780
- // src/self-update/download.ts
17781
- import { chmodSync, existsSync as existsSync4, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
17782
- var GITHUB_REPO2 = "kdcokenny/ocx";
17783
- var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
17784
- var PLATFORM_MAP = {
17785
- "arm64-darwin": "ocx-darwin-arm64",
17786
- "x64-darwin": "ocx-darwin-x64",
17787
- "arm64-linux": "ocx-linux-arm64",
17788
- "x64-linux": "ocx-linux-x64",
17789
- "x64-win32": "ocx-windows-x64.exe"
17790
- };
17791
- function getDownloadBaseUrl() {
17792
- const envUrl = process.env.OCX_DOWNLOAD_URL;
17793
- if (envUrl) {
17794
- return envUrl.replace(/\/+$/, "");
17795
- }
17796
- return DEFAULT_DOWNLOAD_BASE_URL;
17797
- }
17798
- function getDownloadUrl(version) {
17799
- const platform = `${process.arch}-${process.platform}`;
17800
- const target = PLATFORM_MAP[platform];
17801
- if (!target) {
17802
- const supported = Object.keys(PLATFORM_MAP).join(", ");
17803
- throw new SelfUpdateError(`Unsupported platform: ${platform}
17804
- ` + `Supported platforms: ${supported}`);
17805
- }
17806
- const baseUrl = getDownloadBaseUrl();
17807
- return `${baseUrl}/v${version}/${target}`;
17808
- }
17809
- async function downloadWithProgress(url, dest) {
17810
- const spin = createSpinner({ text: "Downloading update..." });
17811
- spin.start();
17812
- let response;
17813
- try {
17814
- response = await fetch(url, { redirect: "follow" });
17815
- } catch (error) {
17816
- spin.fail("Download failed");
17817
- throw new SelfUpdateError(`Network error: ${error instanceof Error ? error.message : String(error)}`);
17818
- }
17819
- if (!response.ok) {
17820
- spin.fail("Download failed");
17821
- throw new SelfUpdateError(`Failed to download: HTTP ${response.status} ${response.statusText}`);
17822
- }
17823
- if (!response.body) {
17824
- spin.fail("Download failed");
17825
- throw new SelfUpdateError("Download failed: Empty response body");
17826
- }
17827
- const reader = response.body.getReader();
17828
- const writer = Bun.file(dest).writer();
17829
- const total = Number(response.headers.get("content-length") || 0);
17830
- let received = 0;
17831
- try {
17832
- while (true) {
17833
- const { done, value } = await reader.read();
17834
- if (done)
17835
- break;
17836
- await writer.write(value);
17837
- received += value.length;
17838
- if (total > 0) {
17839
- const pct = Math.round(received / total * 100);
17840
- spin.text = `Downloading... ${pct}%`;
17978
+ Please use a fully qualified name (registry/component).`);
17841
17979
  }
17980
+ throw new ValidationError(`Component '${name}' must include a registry prefix (e.g., 'kdco/${name}').`);
17842
17981
  }
17843
- await writer.end();
17844
- spin.succeed("Download complete");
17845
- } catch (error) {
17846
- spin.fail("Download failed");
17847
- await writer.end();
17848
- throw new SelfUpdateError(`Download interrupted: ${error instanceof Error ? error.message : String(error)}`);
17849
- }
17850
- }
17851
- async function downloadToTemp(version) {
17852
- const execPath = getExecutablePath();
17853
- const tempPath = `${execPath}.new.${Date.now()}`;
17854
- const url = getDownloadUrl(version);
17855
- await downloadWithProgress(url, tempPath);
17856
- try {
17857
- chmodSync(tempPath, 493);
17858
- } catch (error) {
17859
- if (existsSync4(tempPath)) {
17860
- unlinkSync2(tempPath);
17982
+ if (!lock.installed[name]) {
17983
+ throw new NotFoundError(`Component '${name}' is not installed.
17984
+ Run 'ocx add ${name}' to install it first.`);
17861
17985
  }
17862
- throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
17986
+ result.push(spec);
17863
17987
  }
17864
- return { tempPath, execPath };
17988
+ return result;
17865
17989
  }
17866
- function atomicReplace(tempPath, execPath) {
17867
- const backupPath = `${execPath}.backup`;
17868
- try {
17869
- if (existsSync4(execPath)) {
17870
- renameSync2(execPath, backupPath);
17871
- }
17872
- renameSync2(tempPath, execPath);
17873
- if (existsSync4(backupPath)) {
17874
- unlinkSync2(backupPath);
17990
+ function outputDryRun(results, options2) {
17991
+ const wouldUpdate = results.filter((r2) => r2.status === "would-update");
17992
+ const upToDate = results.filter((r2) => r2.status === "up-to-date");
17993
+ if (options2.json) {
17994
+ console.log(JSON.stringify({ dryRun: true, wouldUpdate, upToDate }, null, 2));
17995
+ return;
17996
+ }
17997
+ if (!options2.quiet) {
17998
+ logger.info("");
17999
+ if (wouldUpdate.length > 0) {
18000
+ logger.info("Would update:");
18001
+ for (const result of wouldUpdate) {
18002
+ logger.info(` ${result.qualifiedName} (${result.oldVersion} \u2192 ${result.newVersion})`);
18003
+ }
17875
18004
  }
17876
- } catch (error) {
17877
- if (existsSync4(backupPath) && !existsSync4(execPath)) {
17878
- try {
17879
- renameSync2(backupPath, execPath);
17880
- } catch {}
18005
+ if (upToDate.length > 0 && options2.verbose) {
18006
+ logger.info("");
18007
+ logger.info("Already up to date:");
18008
+ for (const result of upToDate) {
18009
+ logger.info(` ${result.qualifiedName}`);
18010
+ }
17881
18011
  }
17882
- if (existsSync4(tempPath)) {
17883
- try {
17884
- unlinkSync2(tempPath);
17885
- } catch {}
18012
+ if (wouldUpdate.length > 0) {
18013
+ logger.info("");
18014
+ logger.info("Run without --dry-run to apply changes.");
18015
+ } else {
18016
+ logger.info("All components are up to date.");
17886
18017
  }
17887
- throw new SelfUpdateError(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
17888
18018
  }
17889
18019
  }
17890
- function cleanupTempFile(tempPath) {
17891
- if (existsSync4(tempPath)) {
17892
- try {
17893
- unlinkSync2(tempPath);
17894
- } catch {}
18020
+ async function hashContent2(content2) {
18021
+ return createHash2("sha256").update(content2).digest("hex");
18022
+ }
18023
+ async function hashBundle2(files) {
18024
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
18025
+ const manifestParts = [];
18026
+ for (const file of sorted) {
18027
+ const hash = await hashContent2(file.content);
18028
+ manifestParts.push(`${file.path}:${hash}`);
17895
18029
  }
18030
+ return hashContent2(manifestParts.join(`
18031
+ `));
17896
18032
  }
17897
18033
 
17898
- // src/self-update/notify.ts
17899
- function notifyUpdate(current, latest) {
17900
- if (!process.stdout.isTTY)
17901
- return;
17902
- console.error(`${kleur_default.cyan("info")}: update available - ${kleur_default.green(latest)} (current: ${kleur_default.dim(current)})`);
17903
- console.error(` run ${kleur_default.cyan("`ocx self update`")} to upgrade`);
17904
- }
17905
- function notifyUpToDate(version) {
17906
- console.error(`${kleur_default.cyan("info")}: ocx unchanged - ${kleur_default.dim(version)}`);
18034
+ // src/commands/ghost/update.ts
18035
+ function registerGhostUpdateCommand(parent) {
18036
+ parent.command("update [components...]").description("Update installed components (use @version suffix to pin)").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) => {
18037
+ try {
18038
+ const provider = await GhostConfigProvider.create(options2.cwd ?? process.cwd());
18039
+ await runUpdateCore(components, options2, provider);
18040
+ } catch (error) {
18041
+ handleError(error, { json: options2.json });
18042
+ }
18043
+ });
17907
18044
  }
17908
- function notifyUpdated(from, to) {
17909
- console.error(` ${kleur_default.green("ocx updated")} - ${to} (from ${from})`);
18045
+
18046
+ // src/commands/ghost/index.ts
18047
+ function registerGhostCommand(program2) {
18048
+ const ghost = program2.command("ghost").alias("g").description("Ghost mode - work without local config files");
18049
+ registerGhostInitCommand(ghost);
18050
+ registerGhostConfigCommand(ghost);
18051
+ registerGhostRegistryCommand(ghost);
18052
+ registerGhostAddCommand(ghost);
18053
+ registerGhostSearchCommand(ghost);
18054
+ registerGhostOpenCodeCommand(ghost);
18055
+ registerGhostProfileCommand(ghost);
18056
+ registerGhostUpdateCommand(ghost);
17910
18057
  }
17911
18058
 
17912
- // src/self-update/verify.ts
17913
- import { createHash as createHash2 } from "crypto";
17914
- var GITHUB_REPO3 = "kdcokenny/ocx";
17915
- function parseSha256Sums(content2) {
17916
- const checksums = new Map;
17917
- for (const line of content2.split(`
17918
- `)) {
17919
- const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
17920
- if (match?.[1] && match[2]) {
17921
- checksums.set(match[2].trim(), match[1].toLowerCase());
18059
+ // src/commands/init.ts
18060
+ import { existsSync as existsSync6 } from "fs";
18061
+ import { cp, mkdir as mkdir7, readdir as readdir6, readFile, rm as rm3, writeFile as writeFile3 } from "fs/promises";
18062
+ import { join as join9 } from "path";
18063
+ var TEMPLATE_REPO = "kdcokenny/ocx";
18064
+ var TEMPLATE_PATH = "examples/registry-starter";
18065
+ function registerInitCommand(program2) {
18066
+ program2.command("init [directory]").description("Initialize OCX configuration in your project").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").option("-f, --force", "Overwrite existing files (registry mode only)").action(async (directory, options2) => {
18067
+ try {
18068
+ if (options2.registry) {
18069
+ await runInitRegistry(directory, options2);
18070
+ } else {
18071
+ await runInit(options2);
18072
+ }
18073
+ } catch (error) {
18074
+ handleError(error, { json: options2.json });
17922
18075
  }
17923
- }
17924
- return checksums;
17925
- }
17926
- function hashContent2(content2) {
17927
- return createHash2("sha256").update(content2).digest("hex");
17928
- }
17929
- async function fetchChecksums(version) {
17930
- const url = `https://github.com/${GITHUB_REPO3}/releases/download/v${version}/SHA256SUMS.txt`;
17931
- const response = await fetch(url);
17932
- if (!response.ok) {
17933
- throw new SelfUpdateError(`Failed to fetch checksums: ${response.status}`);
17934
- }
17935
- const content2 = await response.text();
17936
- return parseSha256Sums(content2);
17937
- }
17938
- async function verifyChecksum(filePath, expectedHash, filename) {
17939
- const file = Bun.file(filePath);
17940
- const content2 = await file.arrayBuffer();
17941
- const actualHash = hashContent2(Buffer.from(content2));
17942
- if (actualHash !== expectedHash) {
17943
- throw new IntegrityError(filename, expectedHash, actualHash);
17944
- }
18076
+ });
17945
18077
  }
18078
+ async function runInit(options2) {
18079
+ const cwd = options2.cwd ?? process.cwd();
18080
+ const configPath = join9(cwd, "ocx.jsonc");
18081
+ if (existsSync6(configPath)) {
18082
+ throw new ConflictError(`ocx.jsonc already exists at ${configPath}
17946
18083
 
17947
- // src/commands/self/update.ts
17948
- var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
17949
- async function updateCommand(options2) {
17950
- const method = options2.method ? parseInstallMethod(options2.method) : detectInstallMethod();
17951
- const result = await checkForUpdate();
17952
- if (!result) {
17953
- throw new SelfUpdateError("Failed to check for updates");
17954
- }
17955
- const { current, latest, updateAvailable } = result;
17956
- if (!updateAvailable && !options2.force) {
17957
- notifyUpToDate(current);
17958
- return;
18084
+ ` + `To reset, delete the config and run init again:
18085
+ ` + ` rm ${configPath} && ocx init`);
17959
18086
  }
17960
- const targetVersion = latest;
17961
- switch (method) {
17962
- case "curl": {
17963
- await updateViaCurl(current, targetVersion);
17964
- break;
17965
- }
17966
- case "npm":
17967
- case "pnpm":
17968
- case "bun":
17969
- case "unknown": {
17970
- await updateViaPackageManager(method, current, targetVersion);
17971
- break;
18087
+ const spin = options2.quiet ? null : createSpinner({ text: "Initializing OCX..." });
18088
+ spin?.start();
18089
+ try {
18090
+ const rawConfig = {
18091
+ $schema: OCX_SCHEMA_URL,
18092
+ registries: {}
18093
+ };
18094
+ const config = ocxConfigSchema.parse(rawConfig);
18095
+ const content2 = JSON.stringify(config, null, 2);
18096
+ await writeFile3(configPath, content2, "utf-8");
18097
+ const opencodeResult = await ensureOpencodeConfig(cwd);
18098
+ spin?.succeed("Initialized OCX configuration");
18099
+ if (options2.json) {
18100
+ console.log(JSON.stringify({
18101
+ success: true,
18102
+ path: configPath,
18103
+ opencodePath: opencodeResult.path,
18104
+ opencodeCreated: opencodeResult.created
18105
+ }));
18106
+ } else if (!options2.quiet) {
18107
+ logger.info(`Created ${configPath}`);
18108
+ if (opencodeResult.created) {
18109
+ logger.info(`Created ${opencodeResult.path}`);
18110
+ }
18111
+ logger.info("");
18112
+ logger.info("Next steps:");
18113
+ logger.info(" 1. Add a registry: ocx registry add <url>");
18114
+ logger.info(" 2. Install components: ocx add <component>");
17972
18115
  }
18116
+ } catch (error) {
18117
+ spin?.fail("Failed to initialize");
18118
+ throw error;
17973
18119
  }
17974
18120
  }
17975
- async function updateViaCurl(current, targetVersion) {
17976
- const url = getDownloadUrl(targetVersion);
17977
- const filename = url.split("/").pop();
17978
- if (!filename) {
17979
- throw new SelfUpdateError("Failed to determine binary filename from download URL");
18121
+ async function runInitRegistry(directory, options2) {
18122
+ const cwd = directory ?? options2.cwd ?? process.cwd();
18123
+ const namespace = options2.namespace ?? "my-registry";
18124
+ const author = options2.author ?? "Your Name";
18125
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(namespace)) {
18126
+ throw new ValidationError("Invalid namespace format: must start with letter/number, use hyphens only between segments (e.g., 'my-registry')");
17980
18127
  }
17981
- const checksums = await fetchChecksums(targetVersion);
17982
- const expectedHash = checksums.get(filename);
17983
- if (!expectedHash) {
17984
- throw new SelfUpdateError(`Security error: No checksum found for ${filename}. Update aborted.`);
18128
+ const existingFiles = await readdir6(cwd).catch(() => []);
18129
+ const hasVisibleFiles = existingFiles.some((f) => !f.startsWith("."));
18130
+ if (hasVisibleFiles && !options2.force) {
18131
+ throw new ConflictError("Directory is not empty. Use --force to overwrite existing files.");
17985
18132
  }
17986
- const { tempPath, execPath } = await downloadToTemp(targetVersion);
18133
+ const spin = options2.quiet ? null : createSpinner({ text: "Scaffolding registry..." });
18134
+ spin?.start();
17987
18135
  try {
17988
- await verifyChecksum(tempPath, expectedHash, filename);
18136
+ if (spin)
18137
+ spin.text = options2.local ? "Copying template..." : "Fetching template...";
18138
+ if (options2.local) {
18139
+ await mkdir7(cwd, { recursive: true });
18140
+ await copyDir(options2.local, cwd);
18141
+ } else {
18142
+ const version = options2.canary ? "main" : await getLatestVersion();
18143
+ await fetchAndExtractTemplate(cwd, version, options2.verbose);
18144
+ }
18145
+ if (spin)
18146
+ spin.text = "Configuring project...";
18147
+ await replacePlaceholders(cwd, { namespace, author });
18148
+ spin?.succeed(`Created registry: ${namespace}`);
18149
+ if (options2.json) {
18150
+ console.log(JSON.stringify({ success: true, namespace, path: cwd }));
18151
+ } else if (!options2.quiet) {
18152
+ logger.info("");
18153
+ logger.info("Next steps:");
18154
+ logger.info(" 1. bun install");
18155
+ logger.info(" 2. Edit registry.jsonc with your components");
18156
+ logger.info(" 3. bun run build");
18157
+ logger.info("");
18158
+ logger.info("Deploy to:");
18159
+ logger.info(" Cloudflare: bunx wrangler deploy");
18160
+ logger.info(" Vercel: vercel");
18161
+ logger.info(" Netlify: netlify deploy");
18162
+ }
17989
18163
  } catch (error) {
17990
- cleanupTempFile(tempPath);
18164
+ spin?.fail("Failed to scaffold registry");
17991
18165
  throw error;
17992
18166
  }
17993
- atomicReplace(tempPath, execPath);
17994
- notifyUpdated(current, targetVersion);
17995
18167
  }
17996
- async function runPackageManager(cmd) {
17997
- const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
17998
- const exitCode = await proc.exited;
17999
- if (exitCode !== 0) {
18000
- const stderr = await new Response(proc.stderr).text();
18001
- throw new SelfUpdateError(`Package manager command failed: ${stderr.trim()}`);
18002
- }
18168
+ async function copyDir(src, dest) {
18169
+ await cp(src, dest, { recursive: true });
18003
18170
  }
18004
- async function updateViaPackageManager(method, current, targetVersion) {
18005
- if (!SEMVER_PATTERN.test(targetVersion)) {
18006
- throw new SelfUpdateError(`Invalid version format: ${targetVersion}`);
18171
+ async function getLatestVersion() {
18172
+ const pkgPath = new URL("../../package.json", import.meta.url);
18173
+ const pkgContent = await readFile(pkgPath);
18174
+ const pkg = JSON.parse(pkgContent.toString());
18175
+ return `v${pkg.version}`;
18176
+ }
18177
+ async function fetchAndExtractTemplate(destDir, version, verbose) {
18178
+ const ref = version === "main" ? "heads/main" : `tags/${version}`;
18179
+ const tarballUrl = `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
18180
+ if (verbose) {
18181
+ logger.info(`Fetching ${tarballUrl}`);
18007
18182
  }
18008
- const spin = createSpinner({ text: `Updating via ${method}...` });
18009
- spin.start();
18183
+ const response = await fetch(tarballUrl);
18184
+ if (!response.ok || !response.body) {
18185
+ throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
18186
+ }
18187
+ const tempDir = join9(destDir, ".ocx-temp");
18188
+ await mkdir7(tempDir, { recursive: true });
18010
18189
  try {
18011
- switch (method) {
18012
- case "npm": {
18013
- await runPackageManager(["npm", "install", "-g", `ocx@${targetVersion}`]);
18014
- break;
18015
- }
18016
- case "yarn": {
18017
- await runPackageManager(["yarn", "global", "add", `ocx@${targetVersion}`]);
18018
- break;
18019
- }
18020
- case "pnpm": {
18021
- await runPackageManager(["pnpm", "install", "-g", `ocx@${targetVersion}`]);
18022
- break;
18023
- }
18024
- case "bun": {
18025
- await runPackageManager(["bun", "install", "-g", `ocx@${targetVersion}`]);
18026
- break;
18027
- }
18028
- case "unknown": {
18029
- throw new SelfUpdateError(`Could not detect install method. Update manually with one of:
18030
- ` + ` npm install -g ocx@latest
18031
- ` + ` pnpm install -g ocx@latest
18032
- ` + " bun install -g ocx@latest");
18033
- }
18190
+ const tarPath = join9(tempDir, "template.tar.gz");
18191
+ const arrayBuffer = await response.arrayBuffer();
18192
+ await writeFile3(tarPath, Buffer.from(arrayBuffer));
18193
+ const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
18194
+ stdout: "ignore",
18195
+ stderr: "pipe"
18196
+ });
18197
+ const exitCode = await proc.exited;
18198
+ if (exitCode !== 0) {
18199
+ const stderr = await new Response(proc.stderr).text();
18200
+ throw new Error(`Failed to extract template: ${stderr}`);
18034
18201
  }
18035
- spin.succeed(`Updated via ${method}`);
18036
- notifyUpdated(current, targetVersion);
18037
- } catch (error) {
18038
- if (error instanceof SelfUpdateError) {
18039
- spin.fail(`Update failed`);
18040
- throw error;
18202
+ const extractedDirs = await readdir6(tempDir);
18203
+ const extractedDir = extractedDirs.find((d) => d.startsWith("ocx-"));
18204
+ if (!extractedDir) {
18205
+ throw new Error("Failed to find extracted template directory");
18041
18206
  }
18042
- spin.fail(`Update failed`);
18043
- throw new SelfUpdateError(`Failed to run ${method}: ${error instanceof Error ? error.message : String(error)}`);
18207
+ const templateDir = join9(tempDir, extractedDir, TEMPLATE_PATH);
18208
+ await copyDir(templateDir, destDir);
18209
+ } finally {
18210
+ await rm3(tempDir, { recursive: true, force: true });
18044
18211
  }
18045
18212
  }
18046
- function registerSelfUpdateCommand(parent) {
18047
- parent.command("update").description("Update OCX to the latest version").option("-f, --force", "Reinstall even if already up to date").option("-m, --method <method>", "Override install method detection (curl|npm|pnpm|bun)").action(wrapAction(async (options2) => {
18048
- await updateCommand(options2);
18049
- }));
18213
+ async function replacePlaceholders(dir, values) {
18214
+ const filesToProcess = [
18215
+ "registry.jsonc",
18216
+ "package.json",
18217
+ "wrangler.jsonc",
18218
+ "README.md",
18219
+ "AGENTS.md"
18220
+ ];
18221
+ for (const file of filesToProcess) {
18222
+ const filePath = join9(dir, file);
18223
+ if (!existsSync6(filePath))
18224
+ continue;
18225
+ let content2 = await readFile(filePath).then((b) => b.toString());
18226
+ content2 = content2.replace(/my-registry/g, values.namespace);
18227
+ content2 = content2.replace(/Your Name/g, values.author);
18228
+ await writeFile3(filePath, content2);
18229
+ }
18050
18230
  }
18051
18231
 
18052
- // src/commands/self/index.ts
18053
- function registerSelfCommand(program2) {
18054
- const self = program2.command("self").description("Manage the OCX CLI");
18055
- registerSelfUpdateCommand(self);
18232
+ // src/self-update/version-provider.ts
18233
+ class BuildTimeVersionProvider {
18234
+ version = "1.3.2";
18235
+ }
18236
+ var defaultVersionProvider = new BuildTimeVersionProvider;
18237
+
18238
+ // src/self-update/check.ts
18239
+ var VERSION_CHECK_TIMEOUT_MS = 200;
18240
+ var PACKAGE_NAME = "ocx";
18241
+ function parseVersion2(v) {
18242
+ const [main2 = ""] = v.split("-");
18243
+ const parts = main2.split(".");
18244
+ const major = parseInt(parts[0] ?? "", 10);
18245
+ const minor = parseInt(parts[1] ?? "", 10);
18246
+ const patch = parseInt(parts[2] ?? "", 10);
18247
+ if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) {
18248
+ return null;
18249
+ }
18250
+ return { major, minor, patch };
18251
+ }
18252
+ function compareSemver2(a, b) {
18253
+ const vA = parseVersion2(a);
18254
+ const vB = parseVersion2(b);
18255
+ if (!vA || !vB) {
18256
+ return null;
18257
+ }
18258
+ if (vA.major !== vB.major)
18259
+ return vA.major - vB.major;
18260
+ if (vA.minor !== vB.minor)
18261
+ return vA.minor - vB.minor;
18262
+ return vA.patch - vB.patch;
18263
+ }
18264
+ async function checkForUpdate(versionProvider) {
18265
+ const provider = versionProvider ?? defaultVersionProvider;
18266
+ const current = provider.version || "0.0.0-dev";
18267
+ if (current === "0.0.0-dev") {
18268
+ return null;
18269
+ }
18270
+ try {
18271
+ const result = await fetchPackageVersion(PACKAGE_NAME, undefined, AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS));
18272
+ const latest = result.version;
18273
+ const comparison = compareSemver2(latest, current);
18274
+ if (comparison === null) {
18275
+ return null;
18276
+ }
18277
+ return {
18278
+ current,
18279
+ latest,
18280
+ updateAvailable: comparison > 0
18281
+ };
18282
+ } catch {
18283
+ return null;
18284
+ }
18056
18285
  }
18057
18286
 
18058
- // src/commands/update.ts
18059
- import { createHash as createHash3 } from "crypto";
18060
- import { existsSync as existsSync5 } from "fs";
18061
- import { mkdir as mkdir7, writeFile as writeFile3 } from "fs/promises";
18062
- import { dirname as dirname7, join as join9 } from "path";
18063
- function registerUpdateCommand(program2) {
18064
- 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) => {
18065
- try {
18066
- await runUpdate(components, options2);
18067
- } catch (error) {
18068
- handleError(error, { json: options2.json });
18069
- }
18070
- });
18287
+ // src/self-update/detect-method.ts
18288
+ function parseInstallMethod(input) {
18289
+ const VALID_METHODS = ["curl", "npm", "yarn", "pnpm", "bun"];
18290
+ const method = VALID_METHODS.find((m) => m === input);
18291
+ if (!method) {
18292
+ throw new SelfUpdateError(`Invalid install method: "${input}"
18293
+ Valid methods: ${VALID_METHODS.join(", ")}`);
18294
+ }
18295
+ return method;
18071
18296
  }
18072
- async function runUpdate(componentNames, options2) {
18073
- const cwd = options2.cwd ?? process.cwd();
18074
- const lockPath = join9(cwd, "ocx.lock");
18075
- const config = await readOcxConfig(cwd);
18076
- if (!config) {
18077
- throw new ConfigError("No ocx.jsonc found. Run 'ocx init' first.");
18297
+ var isCompiledBinary = () => Bun.main.startsWith("/$bunfs/");
18298
+ var isTempExecution = (path8) => path8.includes("/_npx/") || path8.includes("/.cache/bunx/") || path8.includes("/.pnpm/_temp/");
18299
+ var isYarnGlobalInstall = (path8) => path8.includes("/.yarn/global") || path8.includes("/.config/yarn/global");
18300
+ var isPnpmGlobalInstall = (path8) => path8.includes("/.pnpm/") || path8.includes("/pnpm/global");
18301
+ var isBunGlobalInstall = (path8) => path8.includes("/.bun/bin") || path8.includes("/.bun/install/global");
18302
+ var isNpmGlobalInstall = (path8) => path8.includes("/.npm/") || path8.includes("/node_modules/");
18303
+ function detectInstallMethod() {
18304
+ if (isCompiledBinary()) {
18305
+ return "curl";
18078
18306
  }
18079
- const lock = await readOcxLock(cwd);
18080
- if (!lock || Object.keys(lock.installed).length === 0) {
18081
- throw new ConfigError("Nothing installed yet. Run 'ocx add <component>' first.");
18307
+ const scriptPath = process.argv[1] ?? "";
18308
+ if (isTempExecution(scriptPath))
18309
+ return "unknown";
18310
+ if (isYarnGlobalInstall(scriptPath))
18311
+ return "yarn";
18312
+ if (isPnpmGlobalInstall(scriptPath))
18313
+ return "pnpm";
18314
+ if (isBunGlobalInstall(scriptPath))
18315
+ return "bun";
18316
+ if (isNpmGlobalInstall(scriptPath))
18317
+ return "npm";
18318
+ const userAgent = process.env.npm_config_user_agent ?? "";
18319
+ if (userAgent.includes("yarn"))
18320
+ return "yarn";
18321
+ if (userAgent.includes("pnpm"))
18322
+ return "pnpm";
18323
+ if (userAgent.includes("bun"))
18324
+ return "bun";
18325
+ if (userAgent.includes("npm"))
18326
+ return "npm";
18327
+ return "unknown";
18328
+ }
18329
+ function getExecutablePath() {
18330
+ if (typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/")) {
18331
+ return process.execPath;
18082
18332
  }
18083
- const hasComponents = componentNames.length > 0;
18084
- const hasAll = options2.all === true;
18085
- const hasRegistry = options2.registry !== undefined;
18086
- if (!hasComponents && !hasAll && !hasRegistry) {
18087
- throw new ValidationError(`Specify components, use --all, or use --registry <name>.
18333
+ return process.argv[1] ?? process.execPath;
18334
+ }
18088
18335
 
18089
- ` + `Examples:
18090
- ` + ` ocx update kdco/agents # Update specific component
18091
- ` + ` ocx update --all # Update all installed components
18092
- ` + " ocx update --registry kdco # Update all from a registry");
18093
- }
18094
- if (hasAll && hasComponents) {
18095
- throw new ValidationError(`Cannot specify components with --all.
18096
- ` + "Use either 'ocx update --all' or 'ocx update <components>'.");
18336
+ // src/self-update/download.ts
18337
+ import { chmodSync, existsSync as existsSync7, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
18338
+ var GITHUB_REPO2 = "kdcokenny/ocx";
18339
+ var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
18340
+ var PLATFORM_MAP = {
18341
+ "arm64-darwin": "ocx-darwin-arm64",
18342
+ "x64-darwin": "ocx-darwin-x64",
18343
+ "arm64-linux": "ocx-linux-arm64",
18344
+ "x64-linux": "ocx-linux-x64",
18345
+ "x64-win32": "ocx-windows-x64.exe"
18346
+ };
18347
+ function getDownloadBaseUrl() {
18348
+ const envUrl = process.env.OCX_DOWNLOAD_URL;
18349
+ if (envUrl) {
18350
+ return envUrl.replace(/\/+$/, "");
18097
18351
  }
18098
- if (hasRegistry && hasComponents) {
18099
- throw new ValidationError(`Cannot specify components with --registry.
18100
- ` + "Use either 'ocx update --registry <name>' or 'ocx update <components>'.");
18352
+ return DEFAULT_DOWNLOAD_BASE_URL;
18353
+ }
18354
+ function getDownloadUrl(version) {
18355
+ const platform = `${process.arch}-${process.platform}`;
18356
+ const target = PLATFORM_MAP[platform];
18357
+ if (!target) {
18358
+ const supported = Object.keys(PLATFORM_MAP).join(", ");
18359
+ throw new SelfUpdateError(`Unsupported platform: ${platform}
18360
+ ` + `Supported platforms: ${supported}`);
18101
18361
  }
18102
- if (hasAll && hasRegistry) {
18103
- throw new ValidationError(`Cannot use --all with --registry.
18104
- ` + "Use either 'ocx update --all' or 'ocx update --registry <name>'.");
18362
+ const baseUrl = getDownloadBaseUrl();
18363
+ return `${baseUrl}/v${version}/${target}`;
18364
+ }
18365
+ async function downloadWithProgress(url, dest) {
18366
+ const spin = createSpinner({ text: "Downloading update..." });
18367
+ spin.start();
18368
+ let response;
18369
+ try {
18370
+ response = await fetch(url, { redirect: "follow" });
18371
+ } catch (error) {
18372
+ spin.fail("Download failed");
18373
+ throw new SelfUpdateError(`Network error: ${error instanceof Error ? error.message : String(error)}`);
18105
18374
  }
18106
- const parsedComponents = componentNames.map(parseComponentSpec);
18107
- for (const spec of parsedComponents) {
18108
- if (spec.version !== undefined && spec.version === "") {
18109
- throw new ValidationError(`Invalid version specifier in '${spec.component}@'.` + `
18110
- Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.`);
18111
- }
18375
+ if (!response.ok) {
18376
+ spin.fail("Download failed");
18377
+ throw new SelfUpdateError(`Failed to download: HTTP ${response.status} ${response.statusText}`);
18112
18378
  }
18113
- const componentsToUpdate = resolveComponentsToUpdate(lock, parsedComponents, options2);
18114
- if (componentsToUpdate.length === 0) {
18115
- if (hasRegistry) {
18116
- throw new NotFoundError(`No installed components from registry '${options2.registry}'.`);
18117
- }
18118
- throw new NotFoundError("No matching components found to update.");
18379
+ if (!response.body) {
18380
+ spin.fail("Download failed");
18381
+ throw new SelfUpdateError("Download failed: Empty response body");
18119
18382
  }
18120
- const spin = options2.quiet ? null : createSpinner({ text: "Checking for updates..." });
18121
- spin?.start();
18122
- const results = [];
18123
- const updates = [];
18383
+ const reader = response.body.getReader();
18384
+ const writer = Bun.file(dest).writer();
18385
+ const total = Number(response.headers.get("content-length") || 0);
18386
+ let received = 0;
18124
18387
  try {
18125
- for (const spec of componentsToUpdate) {
18126
- const qualifiedName = spec.component;
18127
- const lockEntry = lock.installed[qualifiedName];
18128
- if (!lockEntry) {
18129
- throw new NotFoundError(`Component '${qualifiedName}' not found in lock file.`);
18130
- }
18131
- const { namespace, component: componentName } = parseQualifiedComponent(qualifiedName);
18132
- const regConfig = config.registries[namespace];
18133
- if (!regConfig) {
18134
- throw new ConfigError(`Registry '${namespace}' not configured. Component '${qualifiedName}' cannot be updated.`);
18135
- }
18136
- const fetchResult = await fetchComponentVersion(regConfig.url, componentName, spec.version);
18137
- const manifest = fetchResult.manifest;
18138
- const version = fetchResult.version;
18139
- const normalizedManifest = normalizeComponentManifest(manifest);
18140
- const files = [];
18141
- for (const file of normalizedManifest.files) {
18142
- const content2 = await fetchFileContent(regConfig.url, componentName, file.path);
18143
- files.push({ path: file.path, content: Buffer.from(content2) });
18144
- }
18145
- const newHash = await hashBundle2(files);
18146
- if (newHash === lockEntry.hash) {
18147
- results.push({
18148
- qualifiedName,
18149
- oldVersion: lockEntry.version,
18150
- newVersion: version,
18151
- status: "up-to-date"
18152
- });
18153
- } else if (options2.dryRun) {
18154
- results.push({
18155
- qualifiedName,
18156
- oldVersion: lockEntry.version,
18157
- newVersion: version,
18158
- status: "would-update"
18159
- });
18160
- } else {
18161
- results.push({
18162
- qualifiedName,
18163
- oldVersion: lockEntry.version,
18164
- newVersion: version,
18165
- status: "updated"
18166
- });
18167
- updates.push({
18168
- qualifiedName,
18169
- component: normalizedManifest,
18170
- files,
18171
- newHash,
18172
- newVersion: version,
18173
- baseUrl: regConfig.url
18174
- });
18175
- }
18176
- }
18177
- spin?.succeed(`Checked ${componentsToUpdate.length} component(s)`);
18178
- if (options2.dryRun) {
18179
- outputDryRun(results, options2);
18180
- return;
18181
- }
18182
- if (updates.length === 0) {
18183
- if (!options2.quiet && !options2.json) {
18184
- logger.info("");
18185
- logger.success("All components are up to date.");
18186
- }
18187
- if (options2.json) {
18188
- console.log(JSON.stringify({ success: true, updated: [], upToDate: results }, null, 2));
18388
+ while (true) {
18389
+ const { done, value } = await reader.read();
18390
+ if (done)
18391
+ break;
18392
+ await writer.write(value);
18393
+ received += value.length;
18394
+ if (total > 0) {
18395
+ const pct = Math.round(received / total * 100);
18396
+ spin.text = `Downloading... ${pct}%`;
18189
18397
  }
18190
- return;
18191
18398
  }
18192
- const installSpin = options2.quiet ? null : createSpinner({ text: "Updating components..." });
18193
- installSpin?.start();
18194
- for (const update of updates) {
18195
- for (const file of update.files) {
18196
- const fileObj = update.component.files.find((f) => f.path === file.path);
18197
- if (!fileObj)
18198
- continue;
18199
- const targetPath = join9(cwd, fileObj.target);
18200
- const targetDir = dirname7(targetPath);
18201
- if (!existsSync5(targetDir)) {
18202
- await mkdir7(targetDir, { recursive: true });
18203
- }
18204
- await writeFile3(targetPath, file.content);
18205
- if (options2.verbose) {
18206
- logger.info(` \u2713 Updated ${fileObj.target}`);
18207
- }
18208
- }
18209
- const existingEntry = lock.installed[update.qualifiedName];
18210
- if (!existingEntry) {
18211
- throw new NotFoundError(`Component '${update.qualifiedName}' not found in lock file.`);
18212
- }
18213
- lock.installed[update.qualifiedName] = {
18214
- registry: existingEntry.registry,
18215
- version: update.newVersion,
18216
- hash: update.newHash,
18217
- files: existingEntry.files,
18218
- installedAt: existingEntry.installedAt,
18219
- updatedAt: new Date().toISOString()
18220
- };
18399
+ await writer.end();
18400
+ spin.succeed("Download complete");
18401
+ } catch (error) {
18402
+ spin.fail("Download failed");
18403
+ await writer.end();
18404
+ throw new SelfUpdateError(`Download interrupted: ${error instanceof Error ? error.message : String(error)}`);
18405
+ }
18406
+ }
18407
+ async function downloadToTemp(version) {
18408
+ const execPath = getExecutablePath();
18409
+ const tempPath = `${execPath}.new.${Date.now()}`;
18410
+ const url = getDownloadUrl(version);
18411
+ await downloadWithProgress(url, tempPath);
18412
+ try {
18413
+ chmodSync(tempPath, 493);
18414
+ } catch (error) {
18415
+ if (existsSync7(tempPath)) {
18416
+ unlinkSync2(tempPath);
18221
18417
  }
18222
- installSpin?.succeed(`Updated ${updates.length} component(s)`);
18223
- await writeFile3(lockPath, JSON.stringify(lock, null, 2), "utf-8");
18224
- if (options2.json) {
18225
- console.log(JSON.stringify({
18226
- success: true,
18227
- updated: results.filter((r2) => r2.status === "updated"),
18228
- upToDate: results.filter((r2) => r2.status === "up-to-date")
18229
- }, null, 2));
18230
- } else if (!options2.quiet) {
18231
- logger.info("");
18232
- for (const result of results) {
18233
- if (result.status === "updated") {
18234
- logger.info(` \u2713 ${result.qualifiedName} (${result.oldVersion} \u2192 ${result.newVersion})`);
18235
- } else if (result.status === "up-to-date" && options2.verbose) {
18236
- logger.info(` \u25CB ${result.qualifiedName} (already up to date)`);
18237
- }
18238
- }
18239
- logger.info("");
18240
- logger.success(`Done! Updated ${updates.length} component(s).`);
18418
+ throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
18419
+ }
18420
+ return { tempPath, execPath };
18421
+ }
18422
+ function atomicReplace(tempPath, execPath) {
18423
+ const backupPath = `${execPath}.backup`;
18424
+ try {
18425
+ if (existsSync7(execPath)) {
18426
+ renameSync2(execPath, backupPath);
18427
+ }
18428
+ renameSync2(tempPath, execPath);
18429
+ if (existsSync7(backupPath)) {
18430
+ unlinkSync2(backupPath);
18241
18431
  }
18242
18432
  } catch (error) {
18243
- spin?.fail("Failed to check for updates");
18244
- throw error;
18433
+ if (existsSync7(backupPath) && !existsSync7(execPath)) {
18434
+ try {
18435
+ renameSync2(backupPath, execPath);
18436
+ } catch {}
18437
+ }
18438
+ if (existsSync7(tempPath)) {
18439
+ try {
18440
+ unlinkSync2(tempPath);
18441
+ } catch {}
18442
+ }
18443
+ throw new SelfUpdateError(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
18245
18444
  }
18246
18445
  }
18247
- function parseComponentSpec(spec) {
18248
- const atIndex = spec.lastIndexOf("@");
18249
- if (atIndex <= 0) {
18250
- return { component: spec };
18446
+ function cleanupTempFile(tempPath) {
18447
+ if (existsSync7(tempPath)) {
18448
+ try {
18449
+ unlinkSync2(tempPath);
18450
+ } catch {}
18251
18451
  }
18252
- return {
18253
- component: spec.slice(0, atIndex),
18254
- version: spec.slice(atIndex + 1)
18255
- };
18256
18452
  }
18257
- function resolveComponentsToUpdate(lock, parsedComponents, options2) {
18258
- const installedComponents = Object.keys(lock.installed);
18259
- if (options2.all) {
18260
- return installedComponents.map((c) => ({ component: c }));
18453
+
18454
+ // src/self-update/notify.ts
18455
+ function notifyUpdate(current, latest) {
18456
+ if (!process.stdout.isTTY)
18457
+ return;
18458
+ console.error(`${kleur_default.cyan("info")}: update available - ${kleur_default.green(latest)} (current: ${kleur_default.dim(current)})`);
18459
+ console.error(` run ${kleur_default.cyan("`ocx self update`")} to upgrade`);
18460
+ }
18461
+ function notifyUpToDate(version) {
18462
+ console.error(`${kleur_default.cyan("info")}: ocx unchanged - ${kleur_default.dim(version)}`);
18463
+ }
18464
+ function notifyUpdated(from, to) {
18465
+ console.error(` ${kleur_default.green("ocx updated")} - ${to} (from ${from})`);
18466
+ }
18467
+
18468
+ // src/self-update/verify.ts
18469
+ import { createHash as createHash3 } from "crypto";
18470
+ var GITHUB_REPO3 = "kdcokenny/ocx";
18471
+ function parseSha256Sums(content2) {
18472
+ const checksums = new Map;
18473
+ for (const line of content2.split(`
18474
+ `)) {
18475
+ const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
18476
+ if (match?.[1] && match[2]) {
18477
+ checksums.set(match[2].trim(), match[1].toLowerCase());
18478
+ }
18261
18479
  }
18262
- if (options2.registry) {
18263
- return installedComponents.filter((name) => {
18264
- const entry = lock.installed[name];
18265
- return entry?.registry === options2.registry;
18266
- }).map((c) => ({ component: c }));
18480
+ return checksums;
18481
+ }
18482
+ function hashContent3(content2) {
18483
+ return createHash3("sha256").update(content2).digest("hex");
18484
+ }
18485
+ async function fetchChecksums(version) {
18486
+ const url = `https://github.com/${GITHUB_REPO3}/releases/download/v${version}/SHA256SUMS.txt`;
18487
+ const response = await fetch(url);
18488
+ if (!response.ok) {
18489
+ throw new SelfUpdateError(`Failed to fetch checksums: ${response.status}`);
18267
18490
  }
18268
- const result = [];
18269
- for (const spec of parsedComponents) {
18270
- const name = spec.component;
18271
- if (!name.includes("/")) {
18272
- const suggestions = installedComponents.filter((installed) => installed.endsWith(`/${name}`));
18273
- if (suggestions.length === 1) {
18274
- throw new ValidationError(`Ambiguous component '${name}'. Did you mean '${suggestions[0]}'?`);
18275
- }
18276
- if (suggestions.length > 1) {
18277
- throw new ValidationError(`Ambiguous component '${name}'. Found in multiple registries:
18278
- ` + suggestions.map((s) => ` - ${s}`).join(`
18279
- `) + `
18491
+ const content2 = await response.text();
18492
+ return parseSha256Sums(content2);
18493
+ }
18494
+ async function verifyChecksum(filePath, expectedHash, filename) {
18495
+ const file = Bun.file(filePath);
18496
+ const content2 = await file.arrayBuffer();
18497
+ const actualHash = hashContent3(Buffer.from(content2));
18498
+ if (actualHash !== expectedHash) {
18499
+ throw new IntegrityError(filename, expectedHash, actualHash);
18500
+ }
18501
+ }
18280
18502
 
18281
- Please use a fully qualified name (registry/component).`);
18282
- }
18283
- throw new ValidationError(`Component '${name}' must include a registry prefix (e.g., 'kdco/${name}').`);
18503
+ // src/commands/self/update.ts
18504
+ var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
18505
+ async function updateCommand(options2) {
18506
+ const method = options2.method ? parseInstallMethod(options2.method) : detectInstallMethod();
18507
+ const result = await checkForUpdate();
18508
+ if (!result) {
18509
+ throw new SelfUpdateError("Failed to check for updates");
18510
+ }
18511
+ const { current, latest, updateAvailable } = result;
18512
+ if (!updateAvailable && !options2.force) {
18513
+ notifyUpToDate(current);
18514
+ return;
18515
+ }
18516
+ const targetVersion = latest;
18517
+ switch (method) {
18518
+ case "curl": {
18519
+ await updateViaCurl(current, targetVersion);
18520
+ break;
18284
18521
  }
18285
- if (!lock.installed[name]) {
18286
- throw new NotFoundError(`Component '${name}' is not installed.
18287
- Run 'ocx add ${name}' to install it first.`);
18522
+ case "npm":
18523
+ case "pnpm":
18524
+ case "bun":
18525
+ case "unknown": {
18526
+ await updateViaPackageManager(method, current, targetVersion);
18527
+ break;
18288
18528
  }
18289
- result.push(spec);
18290
18529
  }
18291
- return result;
18292
18530
  }
18293
- function outputDryRun(results, options2) {
18294
- const wouldUpdate = results.filter((r2) => r2.status === "would-update");
18295
- const upToDate = results.filter((r2) => r2.status === "up-to-date");
18296
- if (options2.json) {
18297
- console.log(JSON.stringify({ dryRun: true, wouldUpdate, upToDate }, null, 2));
18298
- return;
18531
+ async function updateViaCurl(current, targetVersion) {
18532
+ const url = getDownloadUrl(targetVersion);
18533
+ const filename = url.split("/").pop();
18534
+ if (!filename) {
18535
+ throw new SelfUpdateError("Failed to determine binary filename from download URL");
18299
18536
  }
18300
- if (!options2.quiet) {
18301
- logger.info("");
18302
- if (wouldUpdate.length > 0) {
18303
- logger.info("Would update:");
18304
- for (const result of wouldUpdate) {
18305
- logger.info(` ${result.qualifiedName} (${result.oldVersion} \u2192 ${result.newVersion})`);
18537
+ const checksums = await fetchChecksums(targetVersion);
18538
+ const expectedHash = checksums.get(filename);
18539
+ if (!expectedHash) {
18540
+ throw new SelfUpdateError(`Security error: No checksum found for ${filename}. Update aborted.`);
18541
+ }
18542
+ const { tempPath, execPath } = await downloadToTemp(targetVersion);
18543
+ try {
18544
+ await verifyChecksum(tempPath, expectedHash, filename);
18545
+ } catch (error) {
18546
+ cleanupTempFile(tempPath);
18547
+ throw error;
18548
+ }
18549
+ atomicReplace(tempPath, execPath);
18550
+ notifyUpdated(current, targetVersion);
18551
+ }
18552
+ async function runPackageManager(cmd) {
18553
+ const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
18554
+ const exitCode = await proc.exited;
18555
+ if (exitCode !== 0) {
18556
+ const stderr = await new Response(proc.stderr).text();
18557
+ throw new SelfUpdateError(`Package manager command failed: ${stderr.trim()}`);
18558
+ }
18559
+ }
18560
+ async function updateViaPackageManager(method, current, targetVersion) {
18561
+ if (!SEMVER_PATTERN.test(targetVersion)) {
18562
+ throw new SelfUpdateError(`Invalid version format: ${targetVersion}`);
18563
+ }
18564
+ const spin = createSpinner({ text: `Updating via ${method}...` });
18565
+ spin.start();
18566
+ try {
18567
+ switch (method) {
18568
+ case "npm": {
18569
+ await runPackageManager(["npm", "install", "-g", `ocx@${targetVersion}`]);
18570
+ break;
18306
18571
  }
18307
- }
18308
- if (upToDate.length > 0 && options2.verbose) {
18309
- logger.info("");
18310
- logger.info("Already up to date:");
18311
- for (const result of upToDate) {
18312
- logger.info(` ${result.qualifiedName}`);
18572
+ case "yarn": {
18573
+ await runPackageManager(["yarn", "global", "add", `ocx@${targetVersion}`]);
18574
+ break;
18575
+ }
18576
+ case "pnpm": {
18577
+ await runPackageManager(["pnpm", "install", "-g", `ocx@${targetVersion}`]);
18578
+ break;
18579
+ }
18580
+ case "bun": {
18581
+ await runPackageManager(["bun", "install", "-g", `ocx@${targetVersion}`]);
18582
+ break;
18583
+ }
18584
+ case "unknown": {
18585
+ throw new SelfUpdateError(`Could not detect install method. Update manually with one of:
18586
+ ` + ` npm install -g ocx@latest
18587
+ ` + ` pnpm install -g ocx@latest
18588
+ ` + " bun install -g ocx@latest");
18313
18589
  }
18314
18590
  }
18315
- if (wouldUpdate.length > 0) {
18316
- logger.info("");
18317
- logger.info("Run without --dry-run to apply changes.");
18318
- } else {
18319
- logger.info("All components are up to date.");
18591
+ spin.succeed(`Updated via ${method}`);
18592
+ notifyUpdated(current, targetVersion);
18593
+ } catch (error) {
18594
+ if (error instanceof SelfUpdateError) {
18595
+ spin.fail(`Update failed`);
18596
+ throw error;
18320
18597
  }
18598
+ spin.fail(`Update failed`);
18599
+ throw new SelfUpdateError(`Failed to run ${method}: ${error instanceof Error ? error.message : String(error)}`);
18321
18600
  }
18322
18601
  }
18323
- async function hashContent3(content2) {
18324
- return createHash3("sha256").update(content2).digest("hex");
18602
+ function registerSelfUpdateCommand(parent) {
18603
+ parent.command("update").description("Update OCX to the latest version").option("-f, --force", "Reinstall even if already up to date").option("-m, --method <method>", "Override install method detection (curl|npm|pnpm|bun)").action(wrapAction(async (options2) => {
18604
+ await updateCommand(options2);
18605
+ }));
18325
18606
  }
18326
- async function hashBundle2(files) {
18327
- const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
18328
- const manifestParts = [];
18329
- for (const file of sorted) {
18330
- const hash = await hashContent3(file.content);
18331
- manifestParts.push(`${file.path}:${hash}`);
18332
- }
18333
- return hashContent3(manifestParts.join(`
18334
- `));
18607
+
18608
+ // src/commands/self/index.ts
18609
+ function registerSelfCommand(program2) {
18610
+ const self = program2.command("self").description("Manage the OCX CLI");
18611
+ registerSelfUpdateCommand(self);
18335
18612
  }
18336
18613
 
18337
18614
  // src/self-update/index.ts
@@ -18362,7 +18639,7 @@ function registerUpdateCheckHook(program2) {
18362
18639
  });
18363
18640
  }
18364
18641
  // src/index.ts
18365
- var version = "1.3.0";
18642
+ var version = "1.3.2";
18366
18643
  async function main2() {
18367
18644
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
18368
18645
  registerInitCommand(program2);
@@ -18391,4 +18668,4 @@ export {
18391
18668
  buildRegistry
18392
18669
  };
18393
18670
 
18394
- //# debugId=FE5CBBF0E272CBE164756E2164756E21
18671
+ //# debugId=B7C7102D18EED5CE64756E2164756E21