ocx 1.2.0 → 1.2.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
@@ -10528,7 +10528,7 @@ class GhostConfigProvider {
10528
10528
  // package.json
10529
10529
  var package_default = {
10530
10530
  name: "ocx",
10531
- version: "1.2.0",
10531
+ version: "1.2.2",
10532
10532
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10533
10533
  author: "kdcokenny",
10534
10534
  license: "MIT",
@@ -10921,14 +10921,15 @@ var isCI = Boolean(process.env.CI || process.env.GITHUB_ACTIONS || process.env.G
10921
10921
  var isTTY = Boolean(process.stdout.isTTY && !isCI);
10922
10922
  var supportsColor = Boolean(isTTY && process.env.FORCE_COLOR !== "0" && process.env.NO_COLOR === undefined);
10923
10923
  // src/utils/git-context.ts
10924
- import { resolve } from "path";
10924
+ import { basename, resolve } from "path";
10925
+ function getGitEnv() {
10926
+ const { GIT_DIR: _, GIT_WORK_TREE: __, ...cleanEnv } = process.env;
10927
+ return cleanEnv;
10928
+ }
10925
10929
  async function detectGitRepo(cwd) {
10926
- const cleanEnv = { ...process.env };
10927
- delete cleanEnv.GIT_DIR;
10928
- delete cleanEnv.GIT_WORK_TREE;
10929
10930
  const gitDirProc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
10930
10931
  cwd,
10931
- env: cleanEnv,
10932
+ env: getGitEnv(),
10932
10933
  stdout: "pipe",
10933
10934
  stderr: "pipe"
10934
10935
  });
@@ -10943,7 +10944,7 @@ async function detectGitRepo(cwd) {
10943
10944
  }
10944
10945
  const workTreeProc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
10945
10946
  cwd,
10946
- env: cleanEnv,
10947
+ env: getGitEnv(),
10947
10948
  stdout: "pipe",
10948
10949
  stderr: "pipe"
10949
10950
  });
@@ -10959,6 +10960,73 @@ async function detectGitRepo(cwd) {
10959
10960
  const gitDir = resolve(cwd, gitDirRaw);
10960
10961
  return { gitDir, workTree };
10961
10962
  }
10963
+ async function getBranch(cwd) {
10964
+ const symbolicProc = Bun.spawn(["git", "symbolic-ref", "--short", "HEAD"], {
10965
+ cwd,
10966
+ env: getGitEnv(),
10967
+ stdout: "pipe",
10968
+ stderr: "pipe"
10969
+ });
10970
+ const symbolicExitCode = await symbolicProc.exited;
10971
+ if (symbolicExitCode === 0) {
10972
+ const output = await new Response(symbolicProc.stdout).text();
10973
+ const branch = output.trim();
10974
+ if (branch) {
10975
+ return branch;
10976
+ }
10977
+ }
10978
+ const tagProc = Bun.spawn(["git", "describe", "--tags", "--exact-match", "HEAD"], {
10979
+ cwd,
10980
+ env: getGitEnv(),
10981
+ stdout: "pipe",
10982
+ stderr: "pipe"
10983
+ });
10984
+ const tagExitCode = await tagProc.exited;
10985
+ if (tagExitCode === 0) {
10986
+ const output = await new Response(tagProc.stdout).text();
10987
+ const tag = output.trim();
10988
+ if (tag) {
10989
+ return tag;
10990
+ }
10991
+ }
10992
+ const hashProc = Bun.spawn(["git", "rev-parse", "--short", "HEAD"], {
10993
+ cwd,
10994
+ env: getGitEnv(),
10995
+ stdout: "pipe",
10996
+ stderr: "pipe"
10997
+ });
10998
+ const hashExitCode = await hashProc.exited;
10999
+ if (hashExitCode === 0) {
11000
+ const output = await new Response(hashProc.stdout).text();
11001
+ const hash = output.trim();
11002
+ if (hash) {
11003
+ return hash;
11004
+ }
11005
+ }
11006
+ return null;
11007
+ }
11008
+ async function getRepoName(cwd) {
11009
+ const proc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
11010
+ cwd,
11011
+ env: getGitEnv(),
11012
+ stdout: "pipe",
11013
+ stderr: "pipe"
11014
+ });
11015
+ const exitCode = await proc.exited;
11016
+ if (exitCode !== 0) {
11017
+ return null;
11018
+ }
11019
+ const output = await new Response(proc.stdout).text();
11020
+ const rootPath = output.trim();
11021
+ if (!rootPath) {
11022
+ return null;
11023
+ }
11024
+ return basename(rootPath);
11025
+ }
11026
+ async function getGitInfo(cwd) {
11027
+ const [repoName, branch] = await Promise.all([getRepoName(cwd), getBranch(cwd)]);
11028
+ return { repoName, branch };
11029
+ }
10962
11030
  // ../../node_modules/.bun/kleur@4.1.5/node_modules/kleur/index.mjs
10963
11031
  var FORCE_COLOR;
10964
11032
  var NODE_DISABLE_COLORS;
@@ -14454,7 +14522,7 @@ Profile location:`);
14454
14522
  // src/commands/ghost/opencode.ts
14455
14523
  import { renameSync, rmSync } from "fs";
14456
14524
  import { copyFile as copyFilePromise } from "fs/promises";
14457
- import path6 from "path";
14525
+ import path8 from "path";
14458
14526
 
14459
14527
  // src/utils/opencode-discovery.ts
14460
14528
  import { exists } from "fs/promises";
@@ -14517,40 +14585,73 @@ async function discoverProjectFiles(start, stop) {
14517
14585
  return excluded;
14518
14586
  }
14519
14587
 
14588
+ // src/utils/symlink-farm.ts
14589
+ import { randomBytes } from "crypto";
14590
+ import { mkdir as mkdir4, readdir as readdir3, rename as rename3, rm as rm2, stat as stat3, symlink as symlink2 } from "fs/promises";
14591
+ import { tmpdir } from "os";
14592
+ import { dirname as dirname4, isAbsolute, join as join5, relative as relative2 } from "path";
14593
+
14520
14594
  // src/utils/pattern-filter.ts
14595
+ import path6 from "path";
14521
14596
  var {Glob: Glob2 } = globalThis.Bun;
14522
- function matchesAnyGlob(filePath, globs) {
14523
- return globs.some((g) => g.match(filePath));
14524
- }
14525
- function filterExcludedPaths(excludedPaths, includePatterns, excludePatterns) {
14526
- if (!includePatterns || includePatterns.length === 0) {
14527
- return new Set(excludedPaths);
14528
- }
14529
- const includeGlobs = includePatterns.map((p) => new Glob2(p));
14530
- const excludeGlobs = excludePatterns?.map((p) => new Glob2(p)) ?? [];
14531
- const filteredExclusions = new Set;
14532
- for (const path6 of excludedPaths) {
14533
- const matchesInclude = matchesAnyGlob(path6, includeGlobs);
14534
- const matchesExclude = matchesAnyGlob(path6, excludeGlobs);
14535
- if (matchesInclude && !matchesExclude) {
14536
- continue;
14597
+ function normalizeForMatching(absolutePath, projectRoot) {
14598
+ const relativePath = path6.relative(projectRoot, absolutePath);
14599
+ return relativePath.split(path6.sep).join("/").replace(/^\.\//, "");
14600
+ }
14601
+ class PathMatcher {
14602
+ includeGlobs;
14603
+ excludeGlobs;
14604
+ includePatterns;
14605
+ constructor(includePatterns = [], excludePatterns = []) {
14606
+ this.includePatterns = includePatterns;
14607
+ this.includeGlobs = includePatterns.map((p) => ({ pattern: p, glob: new Glob2(p) }));
14608
+ this.excludeGlobs = excludePatterns.map((p) => ({ pattern: p, glob: new Glob2(p) }));
14609
+ }
14610
+ getDisposition(relativePath) {
14611
+ if (this.excludeGlobs.some((g) => g.glob.match(relativePath))) {
14612
+ return { type: "excluded" };
14613
+ }
14614
+ if (this.includeGlobs.some((g) => g.glob.match(relativePath))) {
14615
+ return { type: "included" };
14616
+ }
14617
+ const patternsInsideDir = this.includePatterns.filter((pattern) => {
14618
+ if (pattern.startsWith(`${relativePath}/`))
14619
+ return true;
14620
+ if (pattern.startsWith("**/"))
14621
+ return true;
14622
+ return false;
14623
+ });
14624
+ if (patternsInsideDir.length > 0) {
14625
+ return { type: "partial", patterns: patternsInsideDir };
14537
14626
  }
14538
- filteredExclusions.add(path6);
14627
+ if (this.includePatterns.length > 0) {
14628
+ return { type: "excluded" };
14629
+ }
14630
+ return { type: "included" };
14631
+ }
14632
+ targetsInside(dirPath) {
14633
+ const normalizedDir = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
14634
+ return this.includePatterns.some((p) => p.startsWith(normalizedDir));
14635
+ }
14636
+ getInnerPatterns(dirPath) {
14637
+ const normalizedDir = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
14638
+ return this.includePatterns.filter((p) => p.startsWith(normalizedDir));
14539
14639
  }
14540
- return filteredExclusions;
14640
+ hasIncludePatterns() {
14641
+ return this.includePatterns.length > 0;
14642
+ }
14643
+ }
14644
+ function createPathMatcher(includePatterns = [], excludePatterns = []) {
14645
+ return new PathMatcher(includePatterns, excludePatterns);
14541
14646
  }
14542
14647
 
14543
14648
  // src/utils/symlink-farm.ts
14544
- import { randomBytes } from "crypto";
14545
- import { mkdir as mkdir4, readdir as readdir3, rename as rename3, rm as rm2, stat as stat3, symlink as symlink2 } from "fs/promises";
14546
- import { tmpdir } from "os";
14547
- import { dirname as dirname4, isAbsolute, join as join5, relative as relative2 } from "path";
14548
14649
  var STALE_SESSION_THRESHOLD_MS = 24 * 60 * 60 * 1000;
14549
14650
  var REMOVING_THRESHOLD_MS = 60 * 60 * 1000;
14550
14651
  var GHOST_DIR_PREFIX = "ocx-ghost-";
14551
14652
  var REMOVING_SUFFIX = "-removing";
14552
14653
  var GHOST_MARKER_FILE = ".ocx-ghost-marker";
14553
- async function createSymlinkFarm(sourceDir, excludePaths) {
14654
+ async function createSymlinkFarm(sourceDir, excludePaths, options2) {
14554
14655
  if (!isAbsolute(sourceDir)) {
14555
14656
  throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
14556
14657
  }
@@ -14558,14 +14659,9 @@ async function createSymlinkFarm(sourceDir, excludePaths) {
14558
14659
  const tempDir = join5(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
14559
14660
  await Bun.write(join5(tempDir, GHOST_MARKER_FILE), "");
14560
14661
  try {
14561
- const entries = await readdir3(sourceDir, { withFileTypes: true });
14562
- for (const entry of entries) {
14563
- const sourcePath = join5(sourceDir, entry.name);
14564
- if (excludePaths.has(sourcePath))
14565
- continue;
14566
- const targetPath = join5(tempDir, entry.name);
14567
- await symlink2(sourcePath, targetPath);
14568
- }
14662
+ const matcher = createPathMatcher(options2?.includePatterns ?? [], options2?.excludePatterns ?? []);
14663
+ const plan = await computeSymlinkPlan(sourceDir, sourceDir, excludePaths, matcher);
14664
+ await executeSymlinkPlan(plan, sourceDir, tempDir);
14569
14665
  return tempDir;
14570
14666
  } catch (error) {
14571
14667
  await rm2(tempDir, { recursive: true, force: true }).catch(() => {});
@@ -14649,6 +14745,133 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14649
14745
  }
14650
14746
  return cleanedCount;
14651
14747
  }
14748
+ async function handleExcludedEntry(isDirectory, disposition, computeNestedPlan) {
14749
+ if (disposition.type === "excluded") {
14750
+ return { action: "skip" };
14751
+ }
14752
+ if (disposition.type === "included") {
14753
+ return { action: "includeWhole" };
14754
+ }
14755
+ if (isDirectory) {
14756
+ const nestedPlan = await computeNestedPlan();
14757
+ return { action: "partial", nestedPlan };
14758
+ }
14759
+ return { action: "skip" };
14760
+ }
14761
+ async function computeSymlinkPlan(sourceDir, projectRoot, excludedPaths, matcher, insideExcludedTree = false) {
14762
+ if (!isAbsolute(sourceDir)) {
14763
+ throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
14764
+ }
14765
+ const plan = {
14766
+ wholeDirs: [],
14767
+ files: [],
14768
+ partialDirs: new Map
14769
+ };
14770
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
14771
+ for (const entry of entries) {
14772
+ const sourcePath = join5(sourceDir, entry.name);
14773
+ const relativePath = normalizeForMatching(sourcePath, projectRoot);
14774
+ if (insideExcludedTree) {
14775
+ const disposition = matcher.getDisposition(relativePath);
14776
+ const result = await handleExcludedEntry(entry.isDirectory(), disposition, () => computeSymlinkPlan(sourcePath, projectRoot, excludedPaths, matcher, true));
14777
+ if (result.action === "skip") {
14778
+ continue;
14779
+ }
14780
+ if (result.action === "includeWhole") {
14781
+ if (entry.isDirectory()) {
14782
+ plan.wholeDirs.push(entry.name);
14783
+ } else {
14784
+ plan.files.push(entry.name);
14785
+ }
14786
+ continue;
14787
+ }
14788
+ plan.partialDirs.set(entry.name, result.nestedPlan);
14789
+ continue;
14790
+ }
14791
+ if (excludedPaths.has(sourcePath)) {
14792
+ if (!matcher.hasIncludePatterns()) {
14793
+ continue;
14794
+ }
14795
+ const disposition = matcher.getDisposition(relativePath);
14796
+ const result = await handleExcludedEntry(entry.isDirectory(), disposition, () => computeSymlinkPlan(sourcePath, projectRoot, excludedPaths, matcher, true));
14797
+ if (result.action === "skip") {
14798
+ continue;
14799
+ }
14800
+ if (result.action === "includeWhole") {
14801
+ if (entry.isDirectory()) {
14802
+ plan.wholeDirs.push(entry.name);
14803
+ } else {
14804
+ plan.files.push(entry.name);
14805
+ }
14806
+ continue;
14807
+ }
14808
+ plan.partialDirs.set(entry.name, result.nestedPlan);
14809
+ continue;
14810
+ }
14811
+ if (entry.isDirectory()) {
14812
+ plan.wholeDirs.push(entry.name);
14813
+ } else {
14814
+ plan.files.push(entry.name);
14815
+ }
14816
+ }
14817
+ return plan;
14818
+ }
14819
+ async function executeSymlinkPlan(plan, sourceRoot, targetRoot) {
14820
+ if (!isAbsolute(sourceRoot)) {
14821
+ throw new Error(`sourceRoot must be an absolute path, got: ${sourceRoot}`);
14822
+ }
14823
+ if (!isAbsolute(targetRoot)) {
14824
+ throw new Error(`targetRoot must be an absolute path, got: ${targetRoot}`);
14825
+ }
14826
+ for (const dirName of plan.wholeDirs) {
14827
+ const sourcePath = join5(sourceRoot, dirName);
14828
+ const targetPath = join5(targetRoot, dirName);
14829
+ await symlink2(sourcePath, targetPath);
14830
+ }
14831
+ for (const fileName of plan.files) {
14832
+ const sourcePath = join5(sourceRoot, fileName);
14833
+ const targetPath = join5(targetRoot, fileName);
14834
+ await symlink2(sourcePath, targetPath);
14835
+ }
14836
+ for (const [dirName, nestedPlan] of plan.partialDirs) {
14837
+ const sourcePath = join5(sourceRoot, dirName);
14838
+ const targetPath = join5(targetRoot, dirName);
14839
+ await mkdir4(targetPath, { recursive: true });
14840
+ await executeSymlinkPlan(nestedPlan, sourcePath, targetPath);
14841
+ }
14842
+ }
14843
+
14844
+ // src/utils/terminal-title.ts
14845
+ import path7 from "path";
14846
+ var MAX_BRANCH_LENGTH = 20;
14847
+ function isInsideTmux() {
14848
+ return Boolean(process.env.TMUX);
14849
+ }
14850
+ function setTmuxWindowName(name) {
14851
+ if (!isInsideTmux()) {
14852
+ return;
14853
+ }
14854
+ Bun.spawnSync(["tmux", "rename-window", name]);
14855
+ Bun.spawnSync(["tmux", "set-window-option", "automatic-rename", "off"]);
14856
+ }
14857
+ function setTerminalTitle(title) {
14858
+ if (!isTTY) {
14859
+ return;
14860
+ }
14861
+ process.stdout.write(`\x1B]0;${title}\x07`);
14862
+ }
14863
+ function setTerminalName(name) {
14864
+ setTmuxWindowName(name);
14865
+ setTerminalTitle(name);
14866
+ }
14867
+ function formatTerminalName(cwd, profileName, gitInfo) {
14868
+ const repoName = gitInfo.repoName ?? path7.basename(cwd);
14869
+ if (!gitInfo.branch) {
14870
+ return `ghost[${profileName}]:${repoName}`;
14871
+ }
14872
+ const branch = gitInfo.branch.length > MAX_BRANCH_LENGTH ? `${gitInfo.branch.slice(0, MAX_BRANCH_LENGTH - 3)}...` : gitInfo.branch;
14873
+ return `ghost[${profileName}]:${repoName}/${branch}`;
14874
+ }
14652
14875
 
14653
14876
  // src/commands/ghost/opencode.ts
14654
14877
  function registerGhostOpenCodeCommand(parent) {
@@ -14686,13 +14909,15 @@ async function runGhostOpenCode(args, options2) {
14686
14909
  const gitRoot = gitContext?.workTree ?? cwd;
14687
14910
  const discoveredPaths = await discoverProjectFiles(cwd, gitRoot);
14688
14911
  const ghostConfig = profile.ghost;
14689
- const excludePaths = filterExcludedPaths(discoveredPaths, ghostConfig.include, ghostConfig.exclude);
14690
- const tempDir = await createSymlinkFarm(cwd, excludePaths);
14912
+ const tempDir = await createSymlinkFarm(cwd, discoveredPaths, {
14913
+ includePatterns: ghostConfig.include,
14914
+ excludePatterns: ghostConfig.exclude
14915
+ });
14691
14916
  const ghostFiles = await discoverProjectFiles(profileDir, profileDir);
14692
14917
  await injectGhostFiles(tempDir, profileDir, ghostFiles);
14693
14918
  if (profile.hasAgents) {
14694
14919
  const agentsPath = getProfileAgents(profileName);
14695
- const destAgentsPath = path6.join(tempDir, "AGENTS.md");
14920
+ const destAgentsPath = path8.join(tempDir, "AGENTS.md");
14696
14921
  await copyFilePromise(agentsPath, destAgentsPath);
14697
14922
  }
14698
14923
  let cleanupDone = false;
@@ -14717,6 +14942,8 @@ async function runGhostOpenCode(args, options2) {
14717
14942
  const sigtermHandler = () => proc?.kill("SIGTERM");
14718
14943
  process.on("SIGINT", sigintHandler);
14719
14944
  process.on("SIGTERM", sigtermHandler);
14945
+ const gitInfo = await getGitInfo(cwd);
14946
+ setTerminalName(formatTerminalName(cwd, profileName, gitInfo));
14720
14947
  proc = Bun.spawn({
14721
14948
  cmd: ["opencode", ...args],
14722
14949
  cwd: tempDir,
@@ -14833,7 +15060,7 @@ async function runProfileList(options2) {
14833
15060
 
14834
15061
  // src/commands/ghost/profile/remove.ts
14835
15062
  function registerProfileRemoveCommand(parent) {
14836
- parent.command("remove <name>").alias("rm").description("Delete a ghost profile").option("-f, --force", "Skip confirmation and allow deleting current profile").action(async (name, options2) => {
15063
+ parent.command("remove <name>").alias("rm").description("Delete a ghost profile").option("-f, --force", "Allow deleting current profile").action(async (name, options2) => {
14837
15064
  try {
14838
15065
  await runProfileRemove(name, options2);
14839
15066
  } catch (error) {
@@ -14846,23 +15073,9 @@ async function runProfileRemove(name, options2) {
14846
15073
  if (!await manager.exists(name)) {
14847
15074
  throw new ProfileNotFoundError(name);
14848
15075
  }
14849
- if (!options2.force) {
14850
- if (!isTTY) {
14851
- throw new ValidationError("Cannot confirm deletion in non-interactive mode. Use --force to delete without confirmation.");
14852
- }
14853
- const confirmed = confirmDeletion(name);
14854
- if (!confirmed) {
14855
- console.log("Aborted.");
14856
- return;
14857
- }
14858
- }
14859
- await manager.remove(name, options2.force);
15076
+ await manager.remove(name, options2.force ?? false);
14860
15077
  logger.success(`Deleted profile "${name}"`);
14861
15078
  }
14862
- function confirmDeletion(name) {
14863
- const answer = prompt(`Delete profile "${name}"? This cannot be undone. [y/N]`);
14864
- return answer?.toLowerCase() === "y";
14865
- }
14866
15079
 
14867
15080
  // src/commands/ghost/profile/show.ts
14868
15081
  function registerProfileShowCommand(parent) {
@@ -15748,7 +15961,7 @@ async function hashBundle2(files) {
15748
15961
  `));
15749
15962
  }
15750
15963
  // src/index.ts
15751
- var version = "1.2.0";
15964
+ var version = "1.2.2";
15752
15965
  async function main2() {
15753
15966
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
15754
15967
  registerInitCommand(program2);
@@ -15775,4 +15988,4 @@ export {
15775
15988
  buildRegistry
15776
15989
  };
15777
15990
 
15778
- //# debugId=4A36829C127A179064756E2164756E21
15991
+ //# debugId=DCBED805AED28C6C64756E2164756E21