ocx 1.1.1 → 1.2.1

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
@@ -4511,12 +4511,10 @@ var {
4511
4511
  import { createHash } from "crypto";
4512
4512
  import { existsSync } from "fs";
4513
4513
  import { mkdir as mkdir2, writeFile } from "fs/promises";
4514
- import { dirname as dirname2, join as join2 } from "path";
4514
+ import { dirname, join } from "path";
4515
4515
 
4516
- // src/ghost/config.ts
4517
- import { mkdir } from "fs/promises";
4518
- import { homedir } from "os";
4519
- import path2, { dirname, join } from "path";
4516
+ // src/profile/manager.ts
4517
+ import { mkdir, readdir, readlink, rm, stat } from "fs/promises";
4520
4518
 
4521
4519
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
4522
4520
  function createScanner(text, ignoreTrivia = false) {
@@ -5817,43 +5815,6 @@ var ParseErrorCode;
5817
5815
  ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
5818
5816
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
5819
5817
  })(ParseErrorCode || (ParseErrorCode = {}));
5820
- function printParseErrorCode(code) {
5821
- switch (code) {
5822
- case 1:
5823
- return "InvalidSymbol";
5824
- case 2:
5825
- return "InvalidNumberFormat";
5826
- case 3:
5827
- return "PropertyNameExpected";
5828
- case 4:
5829
- return "ValueExpected";
5830
- case 5:
5831
- return "ColonExpected";
5832
- case 6:
5833
- return "CommaExpected";
5834
- case 7:
5835
- return "CloseBraceExpected";
5836
- case 8:
5837
- return "CloseBracketExpected";
5838
- case 9:
5839
- return "EndOfFileExpected";
5840
- case 10:
5841
- return "InvalidCommentToken";
5842
- case 11:
5843
- return "UnexpectedEndOfComment";
5844
- case 12:
5845
- return "UnexpectedEndOfString";
5846
- case 13:
5847
- return "UnexpectedEndOfNumber";
5848
- case 14:
5849
- return "InvalidUnicode";
5850
- case 15:
5851
- return "InvalidEscapeCharacter";
5852
- case 16:
5853
- return "InvalidCharacter";
5854
- }
5855
- return "<unknown ParseErrorCode>";
5856
- }
5857
5818
  function modify(text, path, value, options) {
5858
5819
  return setProperty(text, path, value, options);
5859
5820
  }
@@ -10276,109 +10237,242 @@ class IntegrityError extends OCXError {
10276
10237
  this.name = "IntegrityError";
10277
10238
  }
10278
10239
  }
10279
-
10280
- class GhostNotInitializedError extends OCXError {
10281
- constructor() {
10282
- super("Ghost mode not initialized. Run `ocx ghost init` first.", "CONFIG_ERROR", EXIT_CODES.CONFIG);
10283
- this.name = "GhostNotInitializedError";
10240
+ class GhostConfigError extends OCXError {
10241
+ constructor(message) {
10242
+ super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
10243
+ this.name = "GhostConfigError";
10284
10244
  }
10285
10245
  }
10286
10246
 
10287
- class GhostAlreadyInitializedError extends OCXError {
10288
- constructor(configPath) {
10289
- const path2 = configPath ?? "~/.config/ocx/ghost.jsonc";
10290
- super(`Ghost mode already initialized.
10291
- ` + `Config: ${path2}
10292
-
10293
- ` + `To reset, delete the config and run init again:
10294
- ` + ` rm ${path2} && ocx ghost init`, "CONFLICT", EXIT_CODES.GENERAL);
10295
- this.name = "GhostAlreadyInitializedError";
10247
+ class ProfileNotFoundError extends OCXError {
10248
+ constructor(name) {
10249
+ super(`Profile "${name}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
10250
+ this.name = "ProfileNotFoundError";
10296
10251
  }
10297
10252
  }
10298
10253
 
10299
- class GhostConfigError extends OCXError {
10300
- constructor(message) {
10301
- super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
10302
- this.name = "GhostConfigError";
10254
+ class ProfileExistsError extends OCXError {
10255
+ constructor(name) {
10256
+ super(`Profile "${name}" already exists`, "CONFLICT", EXIT_CODES.GENERAL);
10257
+ this.name = "ProfileExistsError";
10303
10258
  }
10304
10259
  }
10305
10260
 
10306
- // src/ghost/config.ts
10307
- var CONFIG_DIR_NAME = "ocx";
10308
- var CONFIG_FILE_NAME = "ghost.jsonc";
10309
- function formatZodError(error) {
10310
- return error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join(`
10311
- `);
10261
+ class InvalidProfileNameError extends OCXError {
10262
+ constructor(name, reason) {
10263
+ super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
10264
+ this.name = "InvalidProfileNameError";
10265
+ }
10312
10266
  }
10313
- function parseRawJsonc(filePath, content) {
10314
- const errors2 = [];
10315
- const raw = parse2(content, errors2, { allowTrailingComma: true });
10316
- const firstError = errors2[0];
10317
- if (firstError) {
10318
- throw new GhostConfigError(`Invalid JSON in ${filePath}:
10319
- Offset ${firstError.offset}: ${printParseErrorCode(firstError.error)}`);
10267
+
10268
+ class ProfilesNotInitializedError extends OCXError {
10269
+ constructor() {
10270
+ super("Ghost profiles not initialized. Run 'ocx ghost init' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
10271
+ this.name = "ProfilesNotInitializedError";
10320
10272
  }
10321
- return raw;
10322
10273
  }
10323
- function parseJsoncFile(filePath, content, schema) {
10324
- const raw = parseRawJsonc(filePath, content);
10325
- const result = schema.safeParse(raw);
10326
- if (!result.success) {
10327
- throw new GhostConfigError(`Invalid config in ${filePath}:
10328
- ${formatZodError(result.error)}`);
10274
+
10275
+ // src/profile/atomic.ts
10276
+ import { rename, symlink, unlink } from "fs/promises";
10277
+ async function atomicWrite(filePath, data) {
10278
+ const tempPath = `${filePath}.tmp.${process.pid}`;
10279
+ try {
10280
+ await Bun.write(tempPath, JSON.stringify(data, null, "\t"), { mode: 384 });
10281
+ await rename(tempPath, filePath);
10282
+ } catch (error) {
10283
+ try {
10284
+ await unlink(tempPath);
10285
+ } catch {}
10286
+ throw error;
10329
10287
  }
10330
- return result.data;
10331
10288
  }
10332
- function getGhostConfigDir() {
10333
- const xdgConfigHome = process.env.XDG_CONFIG_HOME;
10334
- if (xdgConfigHome && isAbsolutePath(xdgConfigHome)) {
10335
- return path2.join(xdgConfigHome, CONFIG_DIR_NAME);
10289
+ async function atomicSymlink(target, linkPath) {
10290
+ const tempLink = `${linkPath}.tmp.${process.pid}`;
10291
+ try {
10292
+ await symlink(target, tempLink);
10293
+ await rename(tempLink, linkPath);
10294
+ } catch (error) {
10295
+ try {
10296
+ await unlink(tempLink);
10297
+ } catch {}
10298
+ throw error;
10336
10299
  }
10337
- return path2.join(homedir(), ".config", CONFIG_DIR_NAME);
10338
10300
  }
10339
- function getGhostConfigPath() {
10340
- return join(getGhostConfigDir(), CONFIG_FILE_NAME);
10301
+
10302
+ // src/profile/paths.ts
10303
+ import { homedir } from "os";
10304
+ import path2 from "path";
10305
+ function getProfilesDir() {
10306
+ const base = process.env.XDG_CONFIG_HOME || path2.join(homedir(), ".config");
10307
+ return path2.join(base, "opencode", "profiles");
10308
+ }
10309
+ function getProfileDir(name) {
10310
+ return path2.join(getProfilesDir(), name);
10341
10311
  }
10342
- async function ghostConfigExists() {
10343
- const configPath = getGhostConfigPath();
10344
- const file = Bun.file(configPath);
10345
- return file.exists();
10312
+ function getProfileGhostConfig(name) {
10313
+ return path2.join(getProfileDir(name), "ghost.jsonc");
10346
10314
  }
10347
- async function loadGhostConfig() {
10348
- const configPath = getGhostConfigPath();
10349
- const file = Bun.file(configPath);
10350
- if (!await file.exists()) {
10351
- throw new GhostNotInitializedError;
10352
- }
10353
- const content = await file.text();
10354
- return parseJsoncFile(configPath, content, ghostConfigSchema);
10315
+ function getProfileOpencodeConfig(name) {
10316
+ return path2.join(getProfileDir(name), "opencode.jsonc");
10355
10317
  }
10356
- async function saveGhostConfig(config) {
10357
- const configPath = getGhostConfigPath();
10358
- const configDir = dirname(configPath);
10359
- await mkdir(configDir, { recursive: true });
10360
- const result = ghostConfigSchema.safeParse(config);
10361
- if (!result.success) {
10362
- throw new GhostConfigError(`Invalid config:
10363
- ${formatZodError(result.error)}`);
10364
- }
10365
- const content = JSON.stringify(result.data, null, 2);
10366
- await Bun.write(configPath, content);
10318
+ function getProfileAgents(name) {
10319
+ return path2.join(getProfileDir(name), "AGENTS.md");
10367
10320
  }
10368
- var OPENCODE_CONFIG_FILE_NAME = "opencode.jsonc";
10369
- function getGhostOpencodeConfigPath() {
10370
- return join(getGhostConfigDir(), OPENCODE_CONFIG_FILE_NAME);
10321
+ function getCurrentSymlink() {
10322
+ return path2.join(getProfilesDir(), "current");
10371
10323
  }
10372
- async function loadGhostOpencodeConfig() {
10373
- const configPath = getGhostOpencodeConfigPath();
10374
- try {
10375
- const content = await Bun.file(configPath).text();
10376
- return parseRawJsonc(configPath, content);
10377
- } catch (err) {
10378
- if (err.code === "ENOENT") {
10379
- return {};
10324
+
10325
+ // src/profile/schema.ts
10326
+ var profileNameSchema = exports_external.string().min(1, "Profile name is required").max(32, "Profile name must be 32 characters or less").regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, "Profile name must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens");
10327
+ var profileSchema = exports_external.object({
10328
+ name: profileNameSchema,
10329
+ ghost: ghostConfigSchema,
10330
+ opencode: exports_external.record(exports_external.unknown()).optional(),
10331
+ hasAgents: exports_external.boolean()
10332
+ });
10333
+
10334
+ // src/profile/manager.ts
10335
+ var DEFAULT_GHOST_CONFIG = {
10336
+ $schema: "https://ocx.kdco.dev/schemas/ghost.json",
10337
+ registries: {}
10338
+ };
10339
+
10340
+ class ProfileManager {
10341
+ profilesDir;
10342
+ constructor(profilesDir) {
10343
+ this.profilesDir = profilesDir;
10344
+ }
10345
+ static create() {
10346
+ return new ProfileManager(getProfilesDir());
10347
+ }
10348
+ async isInitialized() {
10349
+ try {
10350
+ const stats = await stat(this.profilesDir);
10351
+ return stats.isDirectory();
10352
+ } catch {
10353
+ return false;
10354
+ }
10355
+ }
10356
+ async ensureInitialized() {
10357
+ if (!await this.isInitialized()) {
10358
+ throw new ProfilesNotInitializedError;
10359
+ }
10360
+ }
10361
+ async list() {
10362
+ await this.ensureInitialized();
10363
+ const entries = await readdir(this.profilesDir, { withFileTypes: true });
10364
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "current").map((e) => e.name).sort();
10365
+ }
10366
+ async exists(name) {
10367
+ const dir = getProfileDir(name);
10368
+ try {
10369
+ const stats = await stat(dir);
10370
+ return stats.isDirectory();
10371
+ } catch {
10372
+ return false;
10380
10373
  }
10381
- throw err;
10374
+ }
10375
+ async get(name) {
10376
+ if (!await this.exists(name)) {
10377
+ throw new ProfileNotFoundError(name);
10378
+ }
10379
+ const ghostPath = getProfileGhostConfig(name);
10380
+ const ghostFile = Bun.file(ghostPath);
10381
+ if (!await ghostFile.exists()) {
10382
+ throw new GhostConfigError(`Profile "${name}" is missing ghost.jsonc. Expected at: ${ghostPath}`);
10383
+ }
10384
+ const ghostContent = await ghostFile.text();
10385
+ const ghostRaw = parse2(ghostContent);
10386
+ const ghost = ghostConfigSchema.parse(ghostRaw);
10387
+ const opencodePath = getProfileOpencodeConfig(name);
10388
+ const opencodeFile = Bun.file(opencodePath);
10389
+ let opencode;
10390
+ if (await opencodeFile.exists()) {
10391
+ const opencodeContent = await opencodeFile.text();
10392
+ opencode = parse2(opencodeContent);
10393
+ }
10394
+ const agentsPath = getProfileAgents(name);
10395
+ const agentsFile = Bun.file(agentsPath);
10396
+ const hasAgents = await agentsFile.exists();
10397
+ return {
10398
+ name,
10399
+ ghost,
10400
+ opencode,
10401
+ hasAgents
10402
+ };
10403
+ }
10404
+ async add(name) {
10405
+ const result = profileNameSchema.safeParse(name);
10406
+ if (!result.success) {
10407
+ throw new InvalidProfileNameError(name, result.error.errors[0]?.message ?? "Invalid name");
10408
+ }
10409
+ if (await this.exists(name)) {
10410
+ throw new ProfileExistsError(name);
10411
+ }
10412
+ const dir = getProfileDir(name);
10413
+ await mkdir(dir, { recursive: true, mode: 448 });
10414
+ const ghostPath = getProfileGhostConfig(name);
10415
+ await atomicWrite(ghostPath, DEFAULT_GHOST_CONFIG);
10416
+ }
10417
+ async remove(name, force = false) {
10418
+ if (!await this.exists(name)) {
10419
+ throw new ProfileNotFoundError(name);
10420
+ }
10421
+ const current = await this.getCurrent();
10422
+ const isCurrentProfile = current === name;
10423
+ const profiles = await this.list();
10424
+ if (isCurrentProfile && !force) {
10425
+ throw new Error(`Cannot delete current profile "${name}". Use --force to override.`);
10426
+ }
10427
+ if (profiles.length <= 1) {
10428
+ throw new Error("Cannot delete the last profile. At least one profile must exist.");
10429
+ }
10430
+ const remaining = profiles.filter((p) => p !== name);
10431
+ const dir = getProfileDir(name);
10432
+ await rm(dir, { recursive: true });
10433
+ if (isCurrentProfile && remaining.length > 0) {
10434
+ await this.setCurrent(remaining[0]);
10435
+ }
10436
+ }
10437
+ async getCurrent(override) {
10438
+ if (override) {
10439
+ if (!await this.exists(override)) {
10440
+ throw new ProfileNotFoundError(override);
10441
+ }
10442
+ return override;
10443
+ }
10444
+ const envProfile = process.env.OCX_PROFILE;
10445
+ if (envProfile) {
10446
+ if (!await this.exists(envProfile)) {
10447
+ throw new ProfileNotFoundError(envProfile);
10448
+ }
10449
+ return envProfile;
10450
+ }
10451
+ await this.ensureInitialized();
10452
+ const linkPath = getCurrentSymlink();
10453
+ try {
10454
+ const target = await readlink(linkPath);
10455
+ return target;
10456
+ } catch {
10457
+ const profiles = await this.list();
10458
+ const firstProfile = profiles[0];
10459
+ if (!firstProfile) {
10460
+ throw new ProfilesNotInitializedError;
10461
+ }
10462
+ return firstProfile;
10463
+ }
10464
+ }
10465
+ async setCurrent(name) {
10466
+ if (!await this.exists(name)) {
10467
+ throw new ProfileNotFoundError(name);
10468
+ }
10469
+ const linkPath = getCurrentSymlink();
10470
+ await atomicSymlink(name, linkPath);
10471
+ }
10472
+ async initialize() {
10473
+ await mkdir(this.profilesDir, { recursive: true, mode: 448 });
10474
+ await this.add("default");
10475
+ await this.setCurrent("default");
10382
10476
  }
10383
10477
  }
10384
10478
 
@@ -10416,8 +10510,10 @@ class GhostConfigProvider {
10416
10510
  this.config = config;
10417
10511
  }
10418
10512
  static async create(_cwd) {
10419
- const config = await loadGhostConfig();
10420
- return new GhostConfigProvider(getGhostConfigDir(), config);
10513
+ const manager = ProfileManager.create();
10514
+ const profileName = await manager.getCurrent();
10515
+ const profile = await manager.get(profileName);
10516
+ return new GhostConfigProvider(getProfileDir(profileName), profile.ghost);
10421
10517
  }
10422
10518
  getRegistries() {
10423
10519
  return this.config.registries;
@@ -10432,7 +10528,7 @@ class GhostConfigProvider {
10432
10528
  // package.json
10433
10529
  var package_default = {
10434
10530
  name: "ocx",
10435
- version: "1.1.1",
10531
+ version: "1.2.1",
10436
10532
  description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
10437
10533
  author: "kdcokenny",
10438
10534
  license: "MIT",
@@ -10825,14 +10921,15 @@ var isCI = Boolean(process.env.CI || process.env.GITHUB_ACTIONS || process.env.G
10825
10921
  var isTTY = Boolean(process.stdout.isTTY && !isCI);
10826
10922
  var supportsColor = Boolean(isTTY && process.env.FORCE_COLOR !== "0" && process.env.NO_COLOR === undefined);
10827
10923
  // src/utils/git-context.ts
10828
- 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
+ }
10829
10929
  async function detectGitRepo(cwd) {
10830
- const cleanEnv = { ...process.env };
10831
- delete cleanEnv.GIT_DIR;
10832
- delete cleanEnv.GIT_WORK_TREE;
10833
10930
  const gitDirProc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
10834
10931
  cwd,
10835
- env: cleanEnv,
10932
+ env: getGitEnv(),
10836
10933
  stdout: "pipe",
10837
10934
  stderr: "pipe"
10838
10935
  });
@@ -10847,7 +10944,7 @@ async function detectGitRepo(cwd) {
10847
10944
  }
10848
10945
  const workTreeProc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
10849
10946
  cwd,
10850
- env: cleanEnv,
10947
+ env: getGitEnv(),
10851
10948
  stdout: "pipe",
10852
10949
  stderr: "pipe"
10853
10950
  });
@@ -10863,6 +10960,73 @@ async function detectGitRepo(cwd) {
10863
10960
  const gitDir = resolve(cwd, gitDirRaw);
10864
10961
  return { gitDir, workTree };
10865
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
+ }
10866
11030
  // ../../node_modules/.bun/kleur@4.1.5/node_modules/kleur/index.mjs
10867
11031
  var FORCE_COLOR;
10868
11032
  var NODE_DISABLE_COLORS;
@@ -12756,7 +12920,7 @@ async function handleNpmPlugins(inputs, options2, cwd) {
12756
12920
  }
12757
12921
  async function runRegistryAddCore(componentNames, options2, provider) {
12758
12922
  const cwd = provider.cwd;
12759
- const lockPath = join2(cwd, "ocx.lock");
12923
+ const lockPath = join(cwd, "ocx.lock");
12760
12924
  const registries = provider.getRegistries();
12761
12925
  let lock = { lockVersion: 1, installed: {} };
12762
12926
  const existingLock = await readOcxLock(cwd);
@@ -12812,7 +12976,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12812
12976
  }
12813
12977
  const computedHash = await hashBundle(files);
12814
12978
  for (const file of component.files) {
12815
- const targetPath = join2(cwd, file.target);
12979
+ const targetPath = join(cwd, file.target);
12816
12980
  assertPathInside(targetPath, cwd);
12817
12981
  }
12818
12982
  const existingEntry = lock.installed[component.qualifiedName];
@@ -12821,7 +12985,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12821
12985
  throw new IntegrityError(component.qualifiedName, existingEntry.hash, computedHash);
12822
12986
  }
12823
12987
  for (const file of component.files) {
12824
- const targetPath = join2(cwd, file.target);
12988
+ const targetPath = join(cwd, file.target);
12825
12989
  if (existsSync(targetPath)) {
12826
12990
  const conflictingComponent = findComponentByFile(lock, file.target);
12827
12991
  if (conflictingComponent && conflictingComponent !== component.qualifiedName) {
@@ -12844,7 +13008,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12844
13008
  const componentFile = component.files.find((f) => f.path === file.path);
12845
13009
  if (!componentFile)
12846
13010
  continue;
12847
- const targetPath = join2(cwd, componentFile.target);
13011
+ const targetPath = join(cwd, componentFile.target);
12848
13012
  if (existsSync(targetPath)) {
12849
13013
  const existingContent = await Bun.file(targetPath).text();
12850
13014
  const incomingContent = file.content.toString("utf-8");
@@ -12900,9 +13064,9 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12900
13064
  const result = await updateOpencodeJsonConfig(cwd, resolved.opencode);
12901
13065
  if (!options2.quiet && result.changed) {
12902
13066
  if (result.created) {
12903
- logger.info(`Created ${join2(cwd, "opencode.jsonc")}`);
13067
+ logger.info(`Created ${join(cwd, "opencode.jsonc")}`);
12904
13068
  } else {
12905
- logger.info(`Updated ${join2(cwd, "opencode.jsonc")}`);
13069
+ logger.info(`Updated ${join(cwd, "opencode.jsonc")}`);
12906
13070
  }
12907
13071
  }
12908
13072
  }
@@ -12914,7 +13078,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12914
13078
  try {
12915
13079
  await updateOpencodeDevDependencies(cwd, resolved.npmDependencies, resolved.npmDevDependencies);
12916
13080
  const totalDeps = resolved.npmDependencies.length + resolved.npmDevDependencies.length;
12917
- npmSpin?.succeed(`Added ${totalDeps} dependencies to ${join2(cwd, ".opencode/package.json")}`);
13081
+ npmSpin?.succeed(`Added ${totalDeps} dependencies to ${join(cwd, ".opencode/package.json")}`);
12918
13082
  } catch (error) {
12919
13083
  npmSpin?.fail("Failed to update .opencode/package.json");
12920
13084
  throw error;
@@ -12948,8 +13112,8 @@ async function installComponent(component, files, cwd, _options) {
12948
13112
  const componentFile = component.files.find((f) => f.path === file.path);
12949
13113
  if (!componentFile)
12950
13114
  continue;
12951
- const targetPath = join2(cwd, componentFile.target);
12952
- const targetDir = dirname2(targetPath);
13115
+ const targetPath = join(cwd, componentFile.target);
13116
+ const targetDir = dirname(targetPath);
12953
13117
  if (existsSync(targetPath)) {
12954
13118
  const existingContent = await Bun.file(targetPath).text();
12955
13119
  const incomingContent = file.content.toString("utf-8");
@@ -13038,7 +13202,7 @@ function mergeDevDependencies(existing, newDeps) {
13038
13202
  return { ...existing, devDependencies: merged };
13039
13203
  }
13040
13204
  async function readOpencodePackageJson(opencodeDir) {
13041
- const pkgPath = join2(opencodeDir, "package.json");
13205
+ const pkgPath = join(opencodeDir, "package.json");
13042
13206
  if (!existsSync(pkgPath)) {
13043
13207
  return { ...DEFAULT_PACKAGE_JSON };
13044
13208
  }
@@ -13051,7 +13215,7 @@ async function readOpencodePackageJson(opencodeDir) {
13051
13215
  }
13052
13216
  }
13053
13217
  async function ensureManifestFilesAreTracked(opencodeDir) {
13054
- const gitignorePath = join2(opencodeDir, ".gitignore");
13218
+ const gitignorePath = join(opencodeDir, ".gitignore");
13055
13219
  const filesToTrack = new Set(["package.json", "bun.lock"]);
13056
13220
  const requiredIgnores = ["node_modules"];
13057
13221
  let lines = [];
@@ -13074,12 +13238,12 @@ async function updateOpencodeDevDependencies(cwd, npmDeps, npmDevDeps) {
13074
13238
  const allDepSpecs = [...npmDeps, ...npmDevDeps];
13075
13239
  if (allDepSpecs.length === 0)
13076
13240
  return;
13077
- const opencodeDir = join2(cwd, ".opencode");
13241
+ const opencodeDir = join(cwd, ".opencode");
13078
13242
  await mkdir2(opencodeDir, { recursive: true });
13079
13243
  const parsedDeps = allDepSpecs.map(parseNpmDependency);
13080
13244
  const existing = await readOpencodePackageJson(opencodeDir);
13081
13245
  const updated = mergeDevDependencies(existing, parsedDeps);
13082
- await Bun.write(join2(opencodeDir, "package.json"), `${JSON.stringify(updated, null, 2)}
13246
+ await Bun.write(join(opencodeDir, "package.json"), `${JSON.stringify(updated, null, 2)}
13083
13247
  `);
13084
13248
  await ensureManifestFilesAreTracked(opencodeDir);
13085
13249
  }
@@ -13093,11 +13257,11 @@ function findComponentByFile(lock, filePath) {
13093
13257
  }
13094
13258
 
13095
13259
  // src/commands/build.ts
13096
- import { join as join4, relative } from "path";
13260
+ import { join as join3, relative } from "path";
13097
13261
 
13098
13262
  // src/lib/build-registry.ts
13099
13263
  import { mkdir as mkdir3 } from "fs/promises";
13100
- import { dirname as dirname3, join as join3 } from "path";
13264
+ import { dirname as dirname2, join as join2 } from "path";
13101
13265
  class BuildRegistryError extends Error {
13102
13266
  errors;
13103
13267
  constructor(message, errors3 = []) {
@@ -13108,8 +13272,8 @@ class BuildRegistryError extends Error {
13108
13272
  }
13109
13273
  async function buildRegistry(options2) {
13110
13274
  const { source: sourcePath, out: outPath } = options2;
13111
- const jsoncFile = Bun.file(join3(sourcePath, "registry.jsonc"));
13112
- const jsonFile = Bun.file(join3(sourcePath, "registry.json"));
13275
+ const jsoncFile = Bun.file(join2(sourcePath, "registry.jsonc"));
13276
+ const jsonFile = Bun.file(join2(sourcePath, "registry.json"));
13113
13277
  const jsoncExists = await jsoncFile.exists();
13114
13278
  const jsonExists = await jsonFile.exists();
13115
13279
  if (!jsoncExists && !jsonExists) {
@@ -13125,7 +13289,7 @@ async function buildRegistry(options2) {
13125
13289
  }
13126
13290
  const registry = parseResult.data;
13127
13291
  const validationErrors = [];
13128
- const componentsDir = join3(outPath, "components");
13292
+ const componentsDir = join2(outPath, "components");
13129
13293
  await mkdir3(componentsDir, { recursive: true });
13130
13294
  for (const component of registry.components) {
13131
13295
  const packument = {
@@ -13137,13 +13301,13 @@ async function buildRegistry(options2) {
13137
13301
  latest: registry.version
13138
13302
  }
13139
13303
  };
13140
- const packumentPath = join3(componentsDir, `${component.name}.json`);
13304
+ const packumentPath = join2(componentsDir, `${component.name}.json`);
13141
13305
  await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
13142
13306
  for (const rawFile of component.files) {
13143
13307
  const file = normalizeFile(rawFile);
13144
- const sourceFilePath = join3(sourcePath, "files", file.path);
13145
- const destFilePath = join3(componentsDir, component.name, file.path);
13146
- const destFileDir = dirname3(destFilePath);
13308
+ const sourceFilePath = join2(sourcePath, "files", file.path);
13309
+ const destFilePath = join2(componentsDir, component.name, file.path);
13310
+ const destFileDir = dirname2(destFilePath);
13147
13311
  if (!await Bun.file(sourceFilePath).exists()) {
13148
13312
  validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`);
13149
13313
  continue;
@@ -13169,11 +13333,11 @@ async function buildRegistry(options2) {
13169
13333
  description: c.description
13170
13334
  }))
13171
13335
  };
13172
- await Bun.write(join3(outPath, "index.json"), JSON.stringify(index, null, 2));
13173
- const wellKnownDir = join3(outPath, ".well-known");
13336
+ await Bun.write(join2(outPath, "index.json"), JSON.stringify(index, null, 2));
13337
+ const wellKnownDir = join2(outPath, ".well-known");
13174
13338
  await mkdir3(wellKnownDir, { recursive: true });
13175
13339
  const discovery = { registry: "/index.json" };
13176
- await Bun.write(join3(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
13340
+ await Bun.write(join2(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
13177
13341
  return {
13178
13342
  name: registry.name,
13179
13343
  namespace: registry.namespace,
@@ -13187,8 +13351,8 @@ async function buildRegistry(options2) {
13187
13351
  function registerBuildCommand(program2) {
13188
13352
  program2.command("build").description("Build a registry from source (for registry authors)").argument("[path]", "Registry source directory", ".").option("--out <dir>", "Output directory", "./dist").option("--cwd <path>", "Working directory", process.cwd()).option("--json", "Output as JSON", false).option("-q, --quiet", "Suppress output", false).action(async (path5, options2) => {
13189
13353
  try {
13190
- const sourcePath = join4(options2.cwd, path5);
13191
- const outPath = join4(options2.cwd, options2.out);
13354
+ const sourcePath = join3(options2.cwd, path5);
13355
+ const outPath = join3(options2.cwd, options2.out);
13192
13356
  const spinner2 = createSpinner({
13193
13357
  text: "Building registry...",
13194
13358
  quiet: options2.quiet || options2.json
@@ -14143,7 +14307,7 @@ function resolveEditor() {
14143
14307
  return process.env.OCX_EDITOR || process.env.EDITOR || process.env.VISUAL || "vi";
14144
14308
  }
14145
14309
  function registerGhostConfigCommand(parent) {
14146
- const cmd = parent.command("config").description("Open ghost configuration in your editor");
14310
+ const cmd = parent.command("config").description("Open current profile's ghost.jsonc in your editor").option("-p, --profile <name>", "Open a specific profile's config");
14147
14311
  addOutputOptions(cmd).action(async (options2) => {
14148
14312
  try {
14149
14313
  await runGhostConfig(options2);
@@ -14153,13 +14317,15 @@ function registerGhostConfigCommand(parent) {
14153
14317
  });
14154
14318
  }
14155
14319
  async function runGhostConfig(options2) {
14156
- const exists = await ghostConfigExists();
14157
- if (!exists) {
14158
- throw new GhostNotInitializedError;
14320
+ const manager = ProfileManager.create();
14321
+ if (!await manager.isInitialized()) {
14322
+ throw new ProfilesNotInitializedError;
14159
14323
  }
14160
- const configPath = getGhostConfigPath();
14324
+ const profileName = await manager.getCurrent(options2.profile);
14325
+ await manager.get(profileName);
14326
+ const configPath = getProfileGhostConfig(profileName);
14161
14327
  if (options2.json) {
14162
- console.log(JSON.stringify({ success: true, path: configPath }));
14328
+ console.log(JSON.stringify({ success: true, profile: profileName, path: configPath }));
14163
14329
  return;
14164
14330
  }
14165
14331
  const editor = resolveEditor();
@@ -14178,22 +14344,8 @@ async function runGhostConfig(options2) {
14178
14344
  }
14179
14345
 
14180
14346
  // src/commands/ghost/init.ts
14181
- import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
14182
- var DEFAULT_GHOST_CONFIG = `{
14183
- // OCX Ghost Mode Configuration
14184
- // This config is used when running commands with \`ocx ghost\` or \`ocx g\`
14185
- // Note: OpenCode settings go in ~/.config/ocx/opencode.jsonc (see: ocx ghost opencode --edit)
14186
-
14187
- // Component registries - add your registries here
14188
- // Example: "myregistry": { "url": "https://example.com/registry" }
14189
- "registries": {},
14190
-
14191
- // Where to install components (relative to project root)
14192
- "componentPath": "src/components"
14193
- }
14194
- `;
14195
14347
  function registerGhostInitCommand(parent) {
14196
- const cmd = parent.command("init").description("Initialize ghost mode with global configuration");
14348
+ const cmd = parent.command("init").description("Initialize ghost mode with profiles");
14197
14349
  addOutputOptions(cmd);
14198
14350
  addVerboseOption(cmd);
14199
14351
  cmd.action(async (options2) => {
@@ -14205,47 +14357,176 @@ function registerGhostInitCommand(parent) {
14205
14357
  });
14206
14358
  }
14207
14359
  async function runGhostInit(options2) {
14208
- const configPath = getGhostConfigPath();
14209
- const configDir = getGhostConfigDir();
14210
- await mkdir4(configDir, { recursive: true });
14211
- try {
14212
- await writeFile2(configPath, DEFAULT_GHOST_CONFIG, { flag: "wx" });
14213
- } catch (err) {
14214
- if (err.code === "EEXIST") {
14215
- throw new GhostAlreadyInitializedError(configPath);
14216
- }
14217
- throw err;
14218
- }
14219
- const opencodeResult = await ensureOpencodeConfig(configDir);
14360
+ const manager = ProfileManager.create();
14361
+ if (await manager.isInitialized()) {
14362
+ const profilesDir2 = getProfilesDir();
14363
+ throw new ProfileExistsError(`Ghost mode already initialized at ${profilesDir2}`);
14364
+ }
14365
+ await manager.initialize();
14366
+ const profilesDir = getProfilesDir();
14367
+ const ghostConfigPath = getProfileGhostConfig("default");
14220
14368
  if (options2.json) {
14221
14369
  console.log(JSON.stringify({
14222
14370
  success: true,
14223
- path: configPath,
14224
- opencodePath: opencodeResult.path,
14225
- opencodeCreated: opencodeResult.created
14371
+ profilesDir,
14372
+ defaultProfile: "default",
14373
+ ghostConfigPath
14226
14374
  }));
14227
14375
  return;
14228
14376
  }
14229
14377
  if (!options2.quiet) {
14230
14378
  logger.success("Ghost mode initialized");
14231
- logger.info(`Created ${configPath}`);
14232
- if (opencodeResult.created) {
14233
- logger.info(`Created ${opencodeResult.path}`);
14234
- }
14379
+ logger.info(`Created ${profilesDir}`);
14380
+ logger.info(`Created profile "default"`);
14235
14381
  logger.info("");
14236
14382
  logger.info("Next steps:");
14237
14383
  logger.info(" 1. Edit your config: ocx ghost config");
14238
14384
  logger.info(" 2. Add registries: ocx ghost registry add <url> --name <name>");
14239
14385
  logger.info(" 3. Add components: ocx ghost add <component>");
14386
+ logger.info(" 4. Create profiles: ocx ghost profile add <name>");
14387
+ }
14388
+ }
14389
+
14390
+ // src/profile/migrate.ts
14391
+ import { chmod, readdir as readdir2, rename as rename2, stat as stat2 } from "fs/promises";
14392
+ import { homedir as homedir2 } from "os";
14393
+ import path5 from "path";
14394
+ function getLegacyConfigDir() {
14395
+ const base = process.env.XDG_CONFIG_HOME || path5.join(homedir2(), ".config");
14396
+ return path5.join(base, "ocx");
14397
+ }
14398
+ async function needsMigration() {
14399
+ const legacyDir = getLegacyConfigDir();
14400
+ const profilesDir = getProfilesDir();
14401
+ try {
14402
+ await stat2(legacyDir);
14403
+ try {
14404
+ await stat2(profilesDir);
14405
+ return false;
14406
+ } catch {
14407
+ return true;
14408
+ }
14409
+ } catch {
14410
+ return false;
14411
+ }
14412
+ }
14413
+ async function migrate(dryRun = false) {
14414
+ const result = {
14415
+ success: false,
14416
+ migratedFiles: [],
14417
+ backupPath: null,
14418
+ errors: []
14419
+ };
14420
+ const legacyDir = getLegacyConfigDir();
14421
+ const profilesDir = getProfilesDir();
14422
+ try {
14423
+ await stat2(legacyDir);
14424
+ } catch {
14425
+ result.errors.push(`No legacy config found at ${legacyDir}`);
14426
+ return result;
14427
+ }
14428
+ try {
14429
+ await stat2(profilesDir);
14430
+ result.errors.push(`Profiles directory already exists at ${profilesDir}`);
14431
+ return result;
14432
+ } catch {}
14433
+ const legacyFiles = await readdir2(legacyDir);
14434
+ const filesToMigrate = legacyFiles.filter((f) => f === "ghost.jsonc" || f === "opencode.jsonc" || f === "AGENTS.md");
14435
+ if (filesToMigrate.length === 0) {
14436
+ result.errors.push("No migratable files found in legacy config");
14437
+ return result;
14438
+ }
14439
+ if (dryRun) {
14440
+ result.migratedFiles = filesToMigrate.map((f) => path5.join(legacyDir, f));
14441
+ result.backupPath = `${legacyDir}.bak`;
14442
+ result.success = true;
14443
+ return result;
14444
+ }
14445
+ const manager = ProfileManager.create();
14446
+ await manager.initialize();
14447
+ const defaultProfileDir = getProfileDir("default");
14448
+ for (const file of filesToMigrate) {
14449
+ const srcPath = path5.join(legacyDir, file);
14450
+ const destPath = path5.join(defaultProfileDir, file);
14451
+ await Bun.write(destPath, Bun.file(srcPath));
14452
+ await chmod(destPath, 384);
14453
+ result.migratedFiles.push(file);
14454
+ }
14455
+ const backupPath = `${legacyDir}.bak`;
14456
+ await rename2(legacyDir, backupPath);
14457
+ result.backupPath = backupPath;
14458
+ result.success = true;
14459
+ return result;
14460
+ }
14461
+
14462
+ // src/commands/ghost/migrate.ts
14463
+ function registerGhostMigrateCommand(parent) {
14464
+ parent.command("migrate").description("Migrate from legacy ~/.config/ocx/ to new profiles system").option("--dry-run", "Preview changes without making them").action(async (options2) => {
14465
+ try {
14466
+ await runMigrate(options2);
14467
+ } catch (error) {
14468
+ handleError(error);
14469
+ }
14470
+ });
14471
+ }
14472
+ async function runMigrate(options2) {
14473
+ const needsMigrationResult = await needsMigration();
14474
+ if (!needsMigrationResult) {
14475
+ const legacyDir = getLegacyConfigDir();
14476
+ const profilesDir = getProfilesDir();
14477
+ console.log("No migration needed.");
14478
+ console.log(` Legacy config: ${legacyDir} (not found)`);
14479
+ console.log(` Profiles: ${profilesDir}`);
14480
+ return;
14481
+ }
14482
+ if (options2.dryRun) {
14483
+ console.log(`Dry run - no changes will be made.
14484
+ `);
14485
+ }
14486
+ const result = await migrate(options2.dryRun);
14487
+ if (!result.success) {
14488
+ console.error("Migration failed:");
14489
+ for (const error of result.errors) {
14490
+ console.error(` - ${error}`);
14491
+ }
14492
+ process.exit(1);
14493
+ }
14494
+ if (options2.dryRun) {
14495
+ console.log("Migration preview:");
14496
+ console.log(`
14497
+ Files to migrate to default profile:`);
14498
+ for (const file of result.migratedFiles) {
14499
+ console.log(` ${file}`);
14500
+ }
14501
+ console.log(`
14502
+ Legacy config will be renamed to:`);
14503
+ console.log(` ${result.backupPath}`);
14504
+ console.log(`
14505
+ Run without --dry-run to perform migration.`);
14506
+ } else {
14507
+ console.log("Migration complete!");
14508
+ console.log(`
14509
+ Migrated to default profile:`);
14510
+ for (const file of result.migratedFiles) {
14511
+ console.log(` ${file}`);
14512
+ }
14513
+ console.log(`
14514
+ Legacy config backed up to:`);
14515
+ console.log(` ${result.backupPath}`);
14516
+ console.log(`
14517
+ Profile location:`);
14518
+ console.log(` ${getProfileDir("default")}`);
14240
14519
  }
14241
14520
  }
14242
14521
 
14243
14522
  // src/commands/ghost/opencode.ts
14244
14523
  import { renameSync, rmSync } from "fs";
14524
+ import { copyFile as copyFilePromise } from "fs/promises";
14525
+ import path7 from "path";
14245
14526
 
14246
14527
  // src/utils/opencode-discovery.ts
14247
14528
  import { exists } from "fs/promises";
14248
- import { dirname as dirname4, join as join5 } from "path";
14529
+ import { dirname as dirname3, join as join4 } from "path";
14249
14530
  var CONFIG_FILES = ["opencode.jsonc", "opencode.json"];
14250
14531
  var RULE_FILES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
14251
14532
  var CONFIG_DIRS = [".opencode"];
@@ -14253,13 +14534,13 @@ async function findUp(target, start, stop) {
14253
14534
  let current = start;
14254
14535
  const result = [];
14255
14536
  while (true) {
14256
- const search = join5(current, target);
14537
+ const search = join4(current, target);
14257
14538
  if (await exists(search).catch(() => false)) {
14258
14539
  result.push(search);
14259
14540
  }
14260
14541
  if (stop === current)
14261
14542
  break;
14262
- const parent = dirname4(current);
14543
+ const parent = dirname3(current);
14263
14544
  if (parent === current)
14264
14545
  break;
14265
14546
  current = parent;
@@ -14271,14 +14552,14 @@ async function* up(options2) {
14271
14552
  let current = start;
14272
14553
  while (true) {
14273
14554
  for (const target of targets) {
14274
- const search = join5(current, target);
14555
+ const search = join4(current, target);
14275
14556
  if (await exists(search).catch(() => false)) {
14276
14557
  yield search;
14277
14558
  }
14278
14559
  }
14279
14560
  if (stop === current)
14280
14561
  break;
14281
- const parent = dirname4(current);
14562
+ const parent = dirname3(current);
14282
14563
  if (parent === current)
14283
14564
  break;
14284
14565
  current = parent;
@@ -14288,14 +14569,14 @@ async function discoverProjectFiles(start, stop) {
14288
14569
  const excluded = new Set;
14289
14570
  for (const file of CONFIG_FILES) {
14290
14571
  const found = await findUp(file, start, stop);
14291
- for (const path5 of found) {
14292
- excluded.add(path5);
14572
+ for (const path6 of found) {
14573
+ excluded.add(path6);
14293
14574
  }
14294
14575
  }
14295
14576
  for (const file of RULE_FILES) {
14296
14577
  const found = await findUp(file, start, stop);
14297
- for (const path5 of found) {
14298
- excluded.add(path5);
14578
+ for (const path6 of found) {
14579
+ excluded.add(path6);
14299
14580
  }
14300
14581
  }
14301
14582
  for await (const dir of up({ targets: CONFIG_DIRS, start, stop })) {
@@ -14316,22 +14597,22 @@ function filterExcludedPaths(excludedPaths, includePatterns, excludePatterns) {
14316
14597
  const includeGlobs = includePatterns.map((p) => new Glob2(p));
14317
14598
  const excludeGlobs = excludePatterns?.map((p) => new Glob2(p)) ?? [];
14318
14599
  const filteredExclusions = new Set;
14319
- for (const path5 of excludedPaths) {
14320
- const matchesInclude = matchesAnyGlob(path5, includeGlobs);
14321
- const matchesExclude = matchesAnyGlob(path5, excludeGlobs);
14600
+ for (const path6 of excludedPaths) {
14601
+ const matchesInclude = matchesAnyGlob(path6, includeGlobs);
14602
+ const matchesExclude = matchesAnyGlob(path6, excludeGlobs);
14322
14603
  if (matchesInclude && !matchesExclude) {
14323
14604
  continue;
14324
14605
  }
14325
- filteredExclusions.add(path5);
14606
+ filteredExclusions.add(path6);
14326
14607
  }
14327
14608
  return filteredExclusions;
14328
14609
  }
14329
14610
 
14330
14611
  // src/utils/symlink-farm.ts
14331
14612
  import { randomBytes } from "crypto";
14332
- import { mkdir as mkdir5, readdir, rename, rm, stat, symlink } from "fs/promises";
14613
+ import { mkdir as mkdir4, readdir as readdir3, rename as rename3, rm as rm2, stat as stat3, symlink as symlink2 } from "fs/promises";
14333
14614
  import { tmpdir } from "os";
14334
- import { dirname as dirname5, isAbsolute, join as join6, relative as relative2 } from "path";
14615
+ import { dirname as dirname4, isAbsolute, join as join5, relative as relative2 } from "path";
14335
14616
  var STALE_SESSION_THRESHOLD_MS = 24 * 60 * 60 * 1000;
14336
14617
  var REMOVING_THRESHOLD_MS = 60 * 60 * 1000;
14337
14618
  var GHOST_DIR_PREFIX = "ocx-ghost-";
@@ -14342,20 +14623,20 @@ async function createSymlinkFarm(sourceDir, excludePaths) {
14342
14623
  throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
14343
14624
  }
14344
14625
  const suffix = randomBytes(4).toString("hex");
14345
- const tempDir = join6(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
14346
- await Bun.write(join6(tempDir, GHOST_MARKER_FILE), "");
14626
+ const tempDir = join5(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
14627
+ await Bun.write(join5(tempDir, GHOST_MARKER_FILE), "");
14347
14628
  try {
14348
- const entries = await readdir(sourceDir, { withFileTypes: true });
14629
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
14349
14630
  for (const entry of entries) {
14350
- const sourcePath = join6(sourceDir, entry.name);
14631
+ const sourcePath = join5(sourceDir, entry.name);
14351
14632
  if (excludePaths.has(sourcePath))
14352
14633
  continue;
14353
- const targetPath = join6(tempDir, entry.name);
14354
- await symlink(sourcePath, targetPath);
14634
+ const targetPath = join5(tempDir, entry.name);
14635
+ await symlink2(sourcePath, targetPath);
14355
14636
  }
14356
14637
  return tempDir;
14357
14638
  } catch (error) {
14358
- await rm(tempDir, { recursive: true, force: true }).catch(() => {});
14639
+ await rm2(tempDir, { recursive: true, force: true }).catch(() => {});
14359
14640
  throw error;
14360
14641
  }
14361
14642
  }
@@ -14371,13 +14652,13 @@ async function injectGhostFiles(tempDir, sourceDir, injectPaths) {
14371
14652
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
14372
14653
  throw new Error(`injectPath must be within sourceDir: ${injectPath}`);
14373
14654
  }
14374
- const targetPath = join6(tempDir, relativePath);
14375
- const parentDir = dirname5(targetPath);
14655
+ const targetPath = join5(tempDir, relativePath);
14656
+ const parentDir = dirname4(targetPath);
14376
14657
  if (parentDir !== tempDir) {
14377
- await mkdir5(parentDir, { recursive: true });
14658
+ await mkdir4(parentDir, { recursive: true });
14378
14659
  }
14379
14660
  try {
14380
- await symlink(injectPath, targetPath);
14661
+ await symlink2(injectPath, targetPath);
14381
14662
  } catch (err) {
14382
14663
  if (err.code !== "EEXIST") {
14383
14664
  throw new Error(`Failed to inject ${injectPath} \u2192 ${targetPath}: ${err.message}`);
@@ -14388,11 +14669,11 @@ async function injectGhostFiles(tempDir, sourceDir, injectPaths) {
14388
14669
  async function cleanupSymlinkFarm(tempDir) {
14389
14670
  const removingPath = `${tempDir}${REMOVING_SUFFIX}`;
14390
14671
  try {
14391
- await rename(tempDir, removingPath);
14672
+ await rename3(tempDir, removingPath);
14392
14673
  } catch {
14393
14674
  return;
14394
14675
  }
14395
- await rm(removingPath, { recursive: true, force: true });
14676
+ await rm2(removingPath, { recursive: true, force: true });
14396
14677
  }
14397
14678
  async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14398
14679
  let cleanedCount = 0;
@@ -14401,19 +14682,19 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14401
14682
  }
14402
14683
  let dirNames;
14403
14684
  try {
14404
- dirNames = await readdir(tempBase);
14685
+ dirNames = await readdir3(tempBase);
14405
14686
  } catch {
14406
14687
  return 0;
14407
14688
  }
14408
14689
  for (const dirName of dirNames) {
14409
- const dirPath = join6(tempBase, dirName);
14690
+ const dirPath = join5(tempBase, dirName);
14410
14691
  const isRemovingDir = dirName.endsWith(REMOVING_SUFFIX);
14411
14692
  const isGhostDir = dirName.startsWith(GHOST_DIR_PREFIX) && !isRemovingDir;
14412
14693
  if (!isRemovingDir && !isGhostDir)
14413
14694
  continue;
14414
14695
  let stats;
14415
14696
  try {
14416
- stats = await stat(dirPath);
14697
+ stats = await stat3(dirPath);
14417
14698
  } catch {
14418
14699
  continue;
14419
14700
  }
@@ -14426,10 +14707,10 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14426
14707
  try {
14427
14708
  if (isGhostDir) {
14428
14709
  const removingPath = `${dirPath}${REMOVING_SUFFIX}`;
14429
- await rename(dirPath, removingPath);
14430
- await rm(removingPath, { recursive: true, force: true });
14710
+ await rename3(dirPath, removingPath);
14711
+ await rm2(removingPath, { recursive: true, force: true });
14431
14712
  } else {
14432
- await rm(dirPath, { recursive: true, force: true });
14713
+ await rm2(dirPath, { recursive: true, force: true });
14433
14714
  }
14434
14715
  cleanedCount++;
14435
14716
  } catch {}
@@ -14437,9 +14718,41 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14437
14718
  return cleanedCount;
14438
14719
  }
14439
14720
 
14721
+ // src/utils/terminal-title.ts
14722
+ import path6 from "path";
14723
+ var MAX_BRANCH_LENGTH = 20;
14724
+ function isInsideTmux() {
14725
+ return Boolean(process.env.TMUX);
14726
+ }
14727
+ function setTmuxWindowName(name) {
14728
+ if (!isInsideTmux()) {
14729
+ return;
14730
+ }
14731
+ Bun.spawnSync(["tmux", "rename-window", name]);
14732
+ Bun.spawnSync(["tmux", "set-window-option", "automatic-rename", "off"]);
14733
+ }
14734
+ function setTerminalTitle(title) {
14735
+ if (!isTTY) {
14736
+ return;
14737
+ }
14738
+ process.stdout.write(`\x1B]0;${title}\x07`);
14739
+ }
14740
+ function setTerminalName(name) {
14741
+ setTmuxWindowName(name);
14742
+ setTerminalTitle(name);
14743
+ }
14744
+ function formatTerminalName(cwd, profileName, gitInfo) {
14745
+ const repoName = gitInfo.repoName ?? path6.basename(cwd);
14746
+ if (!gitInfo.branch) {
14747
+ return `ghost[${profileName}]:${repoName}`;
14748
+ }
14749
+ const branch = gitInfo.branch.length > MAX_BRANCH_LENGTH ? `${gitInfo.branch.slice(0, MAX_BRANCH_LENGTH - 3)}...` : gitInfo.branch;
14750
+ return `ghost[${profileName}]:${repoName}/${branch}`;
14751
+ }
14752
+
14440
14753
  // src/commands/ghost/opencode.ts
14441
14754
  function registerGhostOpenCodeCommand(parent) {
14442
- parent.command("opencode").description("Launch OpenCode with ghost mode configuration").addOption(sharedOptions.json()).addOption(sharedOptions.quiet()).allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
14755
+ parent.command("opencode").description("Launch OpenCode with ghost mode configuration").option("-p, --profile <name>", "Use specific profile").addOption(sharedOptions.json()).addOption(sharedOptions.quiet()).allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
14443
14756
  try {
14444
14757
  const args = command.args;
14445
14758
  await runGhostOpenCode(args, options2);
@@ -14449,24 +14762,39 @@ function registerGhostOpenCodeCommand(parent) {
14449
14762
  });
14450
14763
  }
14451
14764
  async function runGhostOpenCode(args, options2) {
14452
- if (!await ghostConfigExists()) {
14453
- throw new GhostNotInitializedError;
14765
+ const manager = ProfileManager.create();
14766
+ if (!await manager.isInitialized()) {
14767
+ throw new ProfilesNotInitializedError;
14768
+ }
14769
+ if (!options2.quiet && await needsMigration()) {
14770
+ console.log("Notice: Found legacy config at ~/.config/ocx/");
14771
+ console.log(`Run 'ocx ghost migrate' to upgrade to the new profiles system.
14772
+ `);
14454
14773
  }
14455
14774
  await cleanupOrphanedGhostDirs();
14456
- const openCodeConfig = await loadGhostOpencodeConfig();
14457
- const ghostConfigDir = getGhostConfigDir();
14458
- if (Object.keys(openCodeConfig).length === 0 && !options2.quiet) {
14459
- logger.warn(`No opencode.jsonc found at ${getGhostOpencodeConfigPath()}. Run 'ocx ghost init' first.`);
14775
+ const profileName = await manager.getCurrent(options2.profile);
14776
+ const profile = await manager.get(profileName);
14777
+ const profileDir = getProfileDir(profileName);
14778
+ const profileOpencodePath = getProfileOpencodeConfig(profileName);
14779
+ const profileOpencodeFile = Bun.file(profileOpencodePath);
14780
+ const hasOpencodeConfig = await profileOpencodeFile.exists();
14781
+ if (!hasOpencodeConfig && !options2.quiet) {
14782
+ logger.warn(`No opencode.jsonc found at ${profileOpencodePath}. Create one to customize OpenCode settings.`);
14460
14783
  }
14461
14784
  const cwd = process.cwd();
14462
14785
  const gitContext = await detectGitRepo(cwd);
14463
14786
  const gitRoot = gitContext?.workTree ?? cwd;
14464
14787
  const discoveredPaths = await discoverProjectFiles(cwd, gitRoot);
14465
- const ghostConfig = await loadGhostConfig();
14788
+ const ghostConfig = profile.ghost;
14466
14789
  const excludePaths = filterExcludedPaths(discoveredPaths, ghostConfig.include, ghostConfig.exclude);
14467
14790
  const tempDir = await createSymlinkFarm(cwd, excludePaths);
14468
- const ghostFiles = await discoverProjectFiles(ghostConfigDir, ghostConfigDir);
14469
- await injectGhostFiles(tempDir, ghostConfigDir, ghostFiles);
14791
+ const ghostFiles = await discoverProjectFiles(profileDir, profileDir);
14792
+ await injectGhostFiles(tempDir, profileDir, ghostFiles);
14793
+ if (profile.hasAgents) {
14794
+ const agentsPath = getProfileAgents(profileName);
14795
+ const destAgentsPath = path7.join(tempDir, "AGENTS.md");
14796
+ await copyFilePromise(agentsPath, destAgentsPath);
14797
+ }
14470
14798
  let cleanupDone = false;
14471
14799
  const performCleanup = async () => {
14472
14800
  if (cleanupDone)
@@ -14489,13 +14817,16 @@ async function runGhostOpenCode(args, options2) {
14489
14817
  const sigtermHandler = () => proc?.kill("SIGTERM");
14490
14818
  process.on("SIGINT", sigintHandler);
14491
14819
  process.on("SIGTERM", sigtermHandler);
14820
+ const gitInfo = await getGitInfo(cwd);
14821
+ setTerminalName(formatTerminalName(cwd, profileName, gitInfo));
14492
14822
  proc = Bun.spawn({
14493
14823
  cmd: ["opencode", ...args],
14494
14824
  cwd: tempDir,
14495
14825
  env: {
14496
14826
  ...process.env,
14497
- OPENCODE_CONFIG_CONTENT: JSON.stringify(openCodeConfig),
14498
- OPENCODE_CONFIG_DIR: ghostConfigDir,
14827
+ ...profile.opencode && { OPENCODE_CONFIG_CONTENT: JSON.stringify(profile.opencode) },
14828
+ OPENCODE_CONFIG_DIR: profileDir,
14829
+ OCX_PROFILE: profileName,
14499
14830
  ...gitContext && {
14500
14831
  GIT_WORK_TREE: gitContext.workTree,
14501
14832
  GIT_DIR: gitContext.gitDir
@@ -14516,6 +14847,171 @@ async function runGhostOpenCode(args, options2) {
14516
14847
  }
14517
14848
  }
14518
14849
 
14850
+ // src/commands/ghost/profile/add.ts
14851
+ function registerProfileAddCommand(parent) {
14852
+ parent.command("add <name>").description("Create a new ghost profile").option("--from <profile>", "Clone settings from existing profile").action(async (name, options2) => {
14853
+ try {
14854
+ await runProfileAdd(name, options2);
14855
+ } catch (error) {
14856
+ handleError(error);
14857
+ }
14858
+ });
14859
+ }
14860
+ async function runProfileAdd(name, options2) {
14861
+ const manager = ProfileManager.create();
14862
+ if (options2.from) {
14863
+ const source = await manager.get(options2.from);
14864
+ await manager.add(name);
14865
+ await atomicWrite(getProfileGhostConfig(name), source.ghost);
14866
+ logger.success(`Created profile "${name}" (cloned from "${options2.from}")`);
14867
+ } else {
14868
+ await manager.add(name);
14869
+ logger.success(`Created profile "${name}"`);
14870
+ }
14871
+ }
14872
+
14873
+ // src/commands/ghost/profile/config.ts
14874
+ function registerProfileConfigCommand(parent) {
14875
+ parent.command("config [name]").description("Open profile ghost.jsonc in editor").action(async (name) => {
14876
+ try {
14877
+ await runProfileConfig(name);
14878
+ } catch (error) {
14879
+ handleError(error);
14880
+ }
14881
+ });
14882
+ }
14883
+ async function runProfileConfig(name) {
14884
+ const manager = ProfileManager.create();
14885
+ const profileName = name ?? await manager.getCurrent();
14886
+ await manager.get(profileName);
14887
+ const configPath = getProfileGhostConfig(profileName);
14888
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
14889
+ const proc = Bun.spawn([editor, configPath], {
14890
+ stdin: "inherit",
14891
+ stdout: "inherit",
14892
+ stderr: "inherit"
14893
+ });
14894
+ const exitCode = await proc.exited;
14895
+ if (exitCode !== 0) {
14896
+ throw new Error(`Editor exited with code ${exitCode}`);
14897
+ }
14898
+ }
14899
+
14900
+ // src/commands/ghost/profile/list.ts
14901
+ function registerProfileListCommand(parent) {
14902
+ parent.command("list").alias("ls").description("List all ghost profiles").addOption(sharedOptions.json()).action(async (options2) => {
14903
+ try {
14904
+ await runProfileList(options2);
14905
+ } catch (error) {
14906
+ handleError(error, { json: options2.json });
14907
+ }
14908
+ });
14909
+ }
14910
+ async function runProfileList(options2) {
14911
+ const manager = ProfileManager.create();
14912
+ if (!await manager.isInitialized()) {
14913
+ if (options2.json) {
14914
+ console.log(JSON.stringify({ profiles: [], current: null }));
14915
+ } else {
14916
+ console.log("No profiles found. Run 'ocx ghost init' to create one.");
14917
+ }
14918
+ return;
14919
+ }
14920
+ const profiles = await manager.list();
14921
+ const current = await manager.getCurrent();
14922
+ if (options2.json) {
14923
+ console.log(JSON.stringify({ profiles, current }, null, 2));
14924
+ return;
14925
+ }
14926
+ if (profiles.length === 0) {
14927
+ console.log("No profiles found.");
14928
+ return;
14929
+ }
14930
+ for (const name of profiles) {
14931
+ const marker = name === current ? "* " : " ";
14932
+ console.log(`${marker}${name}`);
14933
+ }
14934
+ }
14935
+
14936
+ // src/commands/ghost/profile/remove.ts
14937
+ function registerProfileRemoveCommand(parent) {
14938
+ parent.command("remove <name>").alias("rm").description("Delete a ghost profile").option("-f, --force", "Allow deleting current profile").action(async (name, options2) => {
14939
+ try {
14940
+ await runProfileRemove(name, options2);
14941
+ } catch (error) {
14942
+ handleError(error);
14943
+ }
14944
+ });
14945
+ }
14946
+ async function runProfileRemove(name, options2) {
14947
+ const manager = ProfileManager.create();
14948
+ if (!await manager.exists(name)) {
14949
+ throw new ProfileNotFoundError(name);
14950
+ }
14951
+ await manager.remove(name, options2.force ?? false);
14952
+ logger.success(`Deleted profile "${name}"`);
14953
+ }
14954
+
14955
+ // src/commands/ghost/profile/show.ts
14956
+ function registerProfileShowCommand(parent) {
14957
+ parent.command("show [name]").description("Display profile contents").addOption(sharedOptions.json()).action(async (name, options2) => {
14958
+ try {
14959
+ await runProfileShow(name, options2);
14960
+ } catch (error) {
14961
+ handleError(error, { json: options2.json });
14962
+ }
14963
+ });
14964
+ }
14965
+ async function runProfileShow(name, options2) {
14966
+ const manager = ProfileManager.create();
14967
+ const profileName = name ?? await manager.getCurrent();
14968
+ const profile = await manager.get(profileName);
14969
+ if (options2.json) {
14970
+ console.log(JSON.stringify(profile, null, 2));
14971
+ return;
14972
+ }
14973
+ console.log(`Profile: ${profile.name}`);
14974
+ console.log(`
14975
+ Files:`);
14976
+ console.log(` ghost.jsonc: ${getProfileGhostConfig(profileName)}`);
14977
+ if (profile.opencode) {
14978
+ console.log(` opencode.jsonc: ${getProfileOpencodeConfig(profileName)}`);
14979
+ }
14980
+ if (profile.hasAgents) {
14981
+ console.log(` AGENTS.md: ${getProfileAgents(profileName)}`);
14982
+ }
14983
+ console.log(`
14984
+ Ghost Config:`);
14985
+ console.log(JSON.stringify(profile.ghost, null, 2));
14986
+ }
14987
+
14988
+ // src/commands/ghost/profile/use.ts
14989
+ function registerProfileUseCommand(parent) {
14990
+ parent.command("use <name>").description("Set the current ghost profile").action(async (name) => {
14991
+ try {
14992
+ await runProfileUse(name);
14993
+ } catch (error) {
14994
+ handleError(error);
14995
+ }
14996
+ });
14997
+ }
14998
+ async function runProfileUse(name) {
14999
+ const manager = ProfileManager.create();
15000
+ await manager.setCurrent(name);
15001
+ logger.success(`Switched to profile "${name}"`);
15002
+ }
15003
+
15004
+ // src/commands/ghost/profile/index.ts
15005
+ function registerGhostProfileCommand(parent) {
15006
+ const profile = parent.command("profile").alias("p").description("Manage ghost mode profiles");
15007
+ registerProfileListCommand(profile);
15008
+ registerProfileAddCommand(profile);
15009
+ registerProfileRemoveCommand(profile);
15010
+ registerProfileUseCommand(profile);
15011
+ registerProfileShowCommand(profile);
15012
+ registerProfileConfigCommand(profile);
15013
+ }
15014
+
14519
15015
  // src/commands/registry.ts
14520
15016
  async function runRegistryAddCore2(url, options2, callbacks) {
14521
15017
  if (callbacks.isLocked?.()) {
@@ -14644,11 +15140,12 @@ function registerRegistryCommand(program2) {
14644
15140
  }
14645
15141
 
14646
15142
  // src/commands/ghost/registry.ts
14647
- async function ensureGhostInitialized() {
14648
- const exists2 = await ghostConfigExists();
14649
- if (!exists2) {
14650
- throw new GhostNotInitializedError;
15143
+ async function ensureProfilesInitialized() {
15144
+ const manager = ProfileManager.create();
15145
+ if (!await manager.isInitialized()) {
15146
+ throw new ProfilesNotInitializedError;
14651
15147
  }
15148
+ return manager;
14652
15149
  }
14653
15150
  function registerGhostRegistryCommand(parent) {
14654
15151
  const registry = parent.command("registry").description("Manage ghost mode registries");
@@ -14656,13 +15153,14 @@ function registerGhostRegistryCommand(parent) {
14656
15153
  addOutputOptions(addCmd);
14657
15154
  addCmd.action(async (url, options2) => {
14658
15155
  try {
14659
- await ensureGhostInitialized();
14660
- const config = await loadGhostConfig();
15156
+ const manager = await ensureProfilesInitialized();
15157
+ const profileName = await manager.getCurrent();
15158
+ const profile = await manager.get(profileName);
14661
15159
  const result = await runRegistryAddCore2(url, options2, {
14662
- getRegistries: () => config.registries,
15160
+ getRegistries: () => profile.ghost.registries,
14663
15161
  setRegistry: async (name, regConfig) => {
14664
- config.registries[name] = regConfig;
14665
- await saveGhostConfig(config);
15162
+ profile.ghost.registries[name] = regConfig;
15163
+ await atomicWrite(getProfileGhostConfig(profileName), profile.ghost);
14666
15164
  }
14667
15165
  });
14668
15166
  if (options2.json) {
@@ -14682,13 +15180,14 @@ function registerGhostRegistryCommand(parent) {
14682
15180
  addOutputOptions(removeCmd);
14683
15181
  removeCmd.action(async (name, options2) => {
14684
15182
  try {
14685
- await ensureGhostInitialized();
14686
- const config = await loadGhostConfig();
15183
+ const manager = await ensureProfilesInitialized();
15184
+ const profileName = await manager.getCurrent();
15185
+ const profile = await manager.get(profileName);
14687
15186
  const result = await runRegistryRemoveCore(name, {
14688
- getRegistries: () => config.registries,
15187
+ getRegistries: () => profile.ghost.registries,
14689
15188
  removeRegistry: async (regName) => {
14690
- delete config.registries[regName];
14691
- await saveGhostConfig(config);
15189
+ delete profile.ghost.registries[regName];
15190
+ await atomicWrite(getProfileGhostConfig(profileName), profile.ghost);
14692
15191
  }
14693
15192
  });
14694
15193
  if (options2.json) {
@@ -14704,10 +15203,11 @@ function registerGhostRegistryCommand(parent) {
14704
15203
  addOutputOptions(listCmd);
14705
15204
  listCmd.action(async (options2) => {
14706
15205
  try {
14707
- await ensureGhostInitialized();
14708
- const config = await loadGhostConfig();
15206
+ const manager = await ensureProfilesInitialized();
15207
+ const profileName = await manager.getCurrent();
15208
+ const profile = await manager.get(profileName);
14709
15209
  const result = runRegistryListCore({
14710
- getRegistries: () => config.registries
15210
+ getRegistries: () => profile.ghost.registries
14711
15211
  });
14712
15212
  if (options2.json) {
14713
15213
  outputJson({ success: true, data: result });
@@ -14880,12 +15380,14 @@ function registerGhostCommand(program2) {
14880
15380
  registerGhostAddCommand(ghost);
14881
15381
  registerGhostSearchCommand(ghost);
14882
15382
  registerGhostOpenCodeCommand(ghost);
15383
+ registerGhostProfileCommand(ghost);
15384
+ registerGhostMigrateCommand(ghost);
14883
15385
  }
14884
15386
 
14885
15387
  // src/commands/init.ts
14886
15388
  import { existsSync as existsSync2 } from "fs";
14887
- import { cp, mkdir as mkdir6, readdir as readdir2, readFile, rm as rm2, writeFile as writeFile3 } from "fs/promises";
14888
- import { join as join7 } from "path";
15389
+ import { cp, mkdir as mkdir5, readdir as readdir4, readFile, rm as rm3, writeFile as writeFile2 } from "fs/promises";
15390
+ import { join as join6 } from "path";
14889
15391
  var TEMPLATE_REPO = "kdcokenny/ocx";
14890
15392
  var TEMPLATE_PATH = "examples/registry-starter";
14891
15393
  function registerInitCommand(program2) {
@@ -14903,7 +15405,7 @@ function registerInitCommand(program2) {
14903
15405
  }
14904
15406
  async function runInit(options2) {
14905
15407
  const cwd = options2.cwd ?? process.cwd();
14906
- const configPath = join7(cwd, "ocx.jsonc");
15408
+ const configPath = join6(cwd, "ocx.jsonc");
14907
15409
  if (existsSync2(configPath)) {
14908
15410
  throw new ConflictError(`ocx.jsonc already exists at ${configPath}
14909
15411
 
@@ -14919,7 +15421,7 @@ async function runInit(options2) {
14919
15421
  };
14920
15422
  const config = ocxConfigSchema.parse(rawConfig);
14921
15423
  const content2 = JSON.stringify(config, null, 2);
14922
- await writeFile3(configPath, content2, "utf-8");
15424
+ await writeFile2(configPath, content2, "utf-8");
14923
15425
  const opencodeResult = await ensureOpencodeConfig(cwd);
14924
15426
  spin?.succeed("Initialized OCX configuration");
14925
15427
  if (options2.json) {
@@ -14951,7 +15453,7 @@ async function runInitRegistry(directory, options2) {
14951
15453
  if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(namespace)) {
14952
15454
  throw new ValidationError("Invalid namespace format: must start with letter/number, use hyphens only between segments (e.g., 'my-registry')");
14953
15455
  }
14954
- const existingFiles = await readdir2(cwd).catch(() => []);
15456
+ const existingFiles = await readdir4(cwd).catch(() => []);
14955
15457
  const hasVisibleFiles = existingFiles.some((f) => !f.startsWith("."));
14956
15458
  if (hasVisibleFiles && !options2.force) {
14957
15459
  throw new ConflictError("Directory is not empty. Use --force to overwrite existing files.");
@@ -14962,7 +15464,7 @@ async function runInitRegistry(directory, options2) {
14962
15464
  if (spin)
14963
15465
  spin.text = options2.local ? "Copying template..." : "Fetching template...";
14964
15466
  if (options2.local) {
14965
- await mkdir6(cwd, { recursive: true });
15467
+ await mkdir5(cwd, { recursive: true });
14966
15468
  await copyDir(options2.local, cwd);
14967
15469
  } else {
14968
15470
  const version = options2.canary ? "main" : await getLatestVersion();
@@ -15010,12 +15512,12 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
15010
15512
  if (!response.ok || !response.body) {
15011
15513
  throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
15012
15514
  }
15013
- const tempDir = join7(destDir, ".ocx-temp");
15014
- await mkdir6(tempDir, { recursive: true });
15515
+ const tempDir = join6(destDir, ".ocx-temp");
15516
+ await mkdir5(tempDir, { recursive: true });
15015
15517
  try {
15016
- const tarPath = join7(tempDir, "template.tar.gz");
15518
+ const tarPath = join6(tempDir, "template.tar.gz");
15017
15519
  const arrayBuffer = await response.arrayBuffer();
15018
- await writeFile3(tarPath, Buffer.from(arrayBuffer));
15520
+ await writeFile2(tarPath, Buffer.from(arrayBuffer));
15019
15521
  const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
15020
15522
  stdout: "ignore",
15021
15523
  stderr: "pipe"
@@ -15025,15 +15527,15 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
15025
15527
  const stderr = await new Response(proc.stderr).text();
15026
15528
  throw new Error(`Failed to extract template: ${stderr}`);
15027
15529
  }
15028
- const extractedDirs = await readdir2(tempDir);
15530
+ const extractedDirs = await readdir4(tempDir);
15029
15531
  const extractedDir = extractedDirs.find((d) => d.startsWith("ocx-"));
15030
15532
  if (!extractedDir) {
15031
15533
  throw new Error("Failed to find extracted template directory");
15032
15534
  }
15033
- const templateDir = join7(tempDir, extractedDir, TEMPLATE_PATH);
15535
+ const templateDir = join6(tempDir, extractedDir, TEMPLATE_PATH);
15034
15536
  await copyDir(templateDir, destDir);
15035
15537
  } finally {
15036
- await rm2(tempDir, { recursive: true, force: true });
15538
+ await rm3(tempDir, { recursive: true, force: true });
15037
15539
  }
15038
15540
  }
15039
15541
  async function replacePlaceholders(dir, values) {
@@ -15045,21 +15547,21 @@ async function replacePlaceholders(dir, values) {
15045
15547
  "AGENTS.md"
15046
15548
  ];
15047
15549
  for (const file of filesToProcess) {
15048
- const filePath = join7(dir, file);
15550
+ const filePath = join6(dir, file);
15049
15551
  if (!existsSync2(filePath))
15050
15552
  continue;
15051
15553
  let content2 = await readFile(filePath).then((b) => b.toString());
15052
15554
  content2 = content2.replace(/my-registry/g, values.namespace);
15053
15555
  content2 = content2.replace(/Your Name/g, values.author);
15054
- await writeFile3(filePath, content2);
15556
+ await writeFile2(filePath, content2);
15055
15557
  }
15056
15558
  }
15057
15559
 
15058
15560
  // src/commands/update.ts
15059
15561
  import { createHash as createHash2 } from "crypto";
15060
15562
  import { existsSync as existsSync3 } from "fs";
15061
- import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
15062
- import { dirname as dirname6, join as join8 } from "path";
15563
+ import { mkdir as mkdir6, writeFile as writeFile3 } from "fs/promises";
15564
+ import { dirname as dirname5, join as join7 } from "path";
15063
15565
  function registerUpdateCommand(program2) {
15064
15566
  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) => {
15065
15567
  try {
@@ -15071,7 +15573,7 @@ function registerUpdateCommand(program2) {
15071
15573
  }
15072
15574
  async function runUpdate(componentNames, options2) {
15073
15575
  const cwd = options2.cwd ?? process.cwd();
15074
- const lockPath = join8(cwd, "ocx.lock");
15576
+ const lockPath = join7(cwd, "ocx.lock");
15075
15577
  const config = await readOcxConfig(cwd);
15076
15578
  if (!config) {
15077
15579
  throw new ConfigError("No ocx.jsonc found. Run 'ocx init' first.");
@@ -15196,12 +15698,12 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
15196
15698
  const fileObj = update.component.files.find((f) => f.path === file.path);
15197
15699
  if (!fileObj)
15198
15700
  continue;
15199
- const targetPath = join8(cwd, fileObj.target);
15200
- const targetDir = dirname6(targetPath);
15701
+ const targetPath = join7(cwd, fileObj.target);
15702
+ const targetDir = dirname5(targetPath);
15201
15703
  if (!existsSync3(targetDir)) {
15202
- await mkdir7(targetDir, { recursive: true });
15704
+ await mkdir6(targetDir, { recursive: true });
15203
15705
  }
15204
- await writeFile4(targetPath, file.content);
15706
+ await writeFile3(targetPath, file.content);
15205
15707
  if (options2.verbose) {
15206
15708
  logger.info(` \u2713 Updated ${fileObj.target}`);
15207
15709
  }
@@ -15220,7 +15722,7 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
15220
15722
  };
15221
15723
  }
15222
15724
  installSpin?.succeed(`Updated ${updates.length} component(s)`);
15223
- await writeFile4(lockPath, JSON.stringify(lock, null, 2), "utf-8");
15725
+ await writeFile3(lockPath, JSON.stringify(lock, null, 2), "utf-8");
15224
15726
  if (options2.json) {
15225
15727
  console.log(JSON.stringify({
15226
15728
  success: true,
@@ -15334,7 +15836,7 @@ async function hashBundle2(files) {
15334
15836
  `));
15335
15837
  }
15336
15838
  // src/index.ts
15337
- var version = "1.1.1";
15839
+ var version = "1.2.1";
15338
15840
  async function main2() {
15339
15841
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
15340
15842
  registerInitCommand(program2);
@@ -15361,4 +15863,4 @@ export {
15361
15863
  buildRegistry
15362
15864
  };
15363
15865
 
15364
- //# debugId=66554E2EE245482F64756E2164756E21
15866
+ //# debugId=FF19E8C89476382D64756E2164756E21