ocx 1.1.0 → 1.2.0

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");
10341
10308
  }
10342
- async function ghostConfigExists() {
10343
- const configPath = getGhostConfigPath();
10344
- const file = Bun.file(configPath);
10345
- return file.exists();
10309
+ function getProfileDir(name) {
10310
+ return path2.join(getProfilesDir(), name);
10346
10311
  }
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);
10312
+ function getProfileGhostConfig(name) {
10313
+ return path2.join(getProfileDir(name), "ghost.jsonc");
10355
10314
  }
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);
10315
+ function getProfileOpencodeConfig(name) {
10316
+ return path2.join(getProfileDir(name), "opencode.jsonc");
10367
10317
  }
10368
- var OPENCODE_CONFIG_FILE_NAME = "opencode.jsonc";
10369
- function getGhostOpencodeConfigPath() {
10370
- return join(getGhostConfigDir(), OPENCODE_CONFIG_FILE_NAME);
10318
+ function getProfileAgents(name) {
10319
+ return path2.join(getProfileDir(name), "AGENTS.md");
10371
10320
  }
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 {};
10321
+ function getCurrentSymlink() {
10322
+ return path2.join(getProfilesDir(), "current");
10323
+ }
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;
10380
10359
  }
10381
- throw err;
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;
10373
+ }
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.0",
10531
+ version: "1.2.0",
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",
@@ -12756,7 +12852,7 @@ async function handleNpmPlugins(inputs, options2, cwd) {
12756
12852
  }
12757
12853
  async function runRegistryAddCore(componentNames, options2, provider) {
12758
12854
  const cwd = provider.cwd;
12759
- const lockPath = join2(cwd, "ocx.lock");
12855
+ const lockPath = join(cwd, "ocx.lock");
12760
12856
  const registries = provider.getRegistries();
12761
12857
  let lock = { lockVersion: 1, installed: {} };
12762
12858
  const existingLock = await readOcxLock(cwd);
@@ -12812,7 +12908,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12812
12908
  }
12813
12909
  const computedHash = await hashBundle(files);
12814
12910
  for (const file of component.files) {
12815
- const targetPath = join2(cwd, file.target);
12911
+ const targetPath = join(cwd, file.target);
12816
12912
  assertPathInside(targetPath, cwd);
12817
12913
  }
12818
12914
  const existingEntry = lock.installed[component.qualifiedName];
@@ -12821,7 +12917,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12821
12917
  throw new IntegrityError(component.qualifiedName, existingEntry.hash, computedHash);
12822
12918
  }
12823
12919
  for (const file of component.files) {
12824
- const targetPath = join2(cwd, file.target);
12920
+ const targetPath = join(cwd, file.target);
12825
12921
  if (existsSync(targetPath)) {
12826
12922
  const conflictingComponent = findComponentByFile(lock, file.target);
12827
12923
  if (conflictingComponent && conflictingComponent !== component.qualifiedName) {
@@ -12844,7 +12940,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12844
12940
  const componentFile = component.files.find((f) => f.path === file.path);
12845
12941
  if (!componentFile)
12846
12942
  continue;
12847
- const targetPath = join2(cwd, componentFile.target);
12943
+ const targetPath = join(cwd, componentFile.target);
12848
12944
  if (existsSync(targetPath)) {
12849
12945
  const existingContent = await Bun.file(targetPath).text();
12850
12946
  const incomingContent = file.content.toString("utf-8");
@@ -12900,9 +12996,9 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12900
12996
  const result = await updateOpencodeJsonConfig(cwd, resolved.opencode);
12901
12997
  if (!options2.quiet && result.changed) {
12902
12998
  if (result.created) {
12903
- logger.info(`Created ${join2(cwd, "opencode.jsonc")}`);
12999
+ logger.info(`Created ${join(cwd, "opencode.jsonc")}`);
12904
13000
  } else {
12905
- logger.info(`Updated ${join2(cwd, "opencode.jsonc")}`);
13001
+ logger.info(`Updated ${join(cwd, "opencode.jsonc")}`);
12906
13002
  }
12907
13003
  }
12908
13004
  }
@@ -12914,7 +13010,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
12914
13010
  try {
12915
13011
  await updateOpencodeDevDependencies(cwd, resolved.npmDependencies, resolved.npmDevDependencies);
12916
13012
  const totalDeps = resolved.npmDependencies.length + resolved.npmDevDependencies.length;
12917
- npmSpin?.succeed(`Added ${totalDeps} dependencies to ${join2(cwd, ".opencode/package.json")}`);
13013
+ npmSpin?.succeed(`Added ${totalDeps} dependencies to ${join(cwd, ".opencode/package.json")}`);
12918
13014
  } catch (error) {
12919
13015
  npmSpin?.fail("Failed to update .opencode/package.json");
12920
13016
  throw error;
@@ -12948,8 +13044,8 @@ async function installComponent(component, files, cwd, _options) {
12948
13044
  const componentFile = component.files.find((f) => f.path === file.path);
12949
13045
  if (!componentFile)
12950
13046
  continue;
12951
- const targetPath = join2(cwd, componentFile.target);
12952
- const targetDir = dirname2(targetPath);
13047
+ const targetPath = join(cwd, componentFile.target);
13048
+ const targetDir = dirname(targetPath);
12953
13049
  if (existsSync(targetPath)) {
12954
13050
  const existingContent = await Bun.file(targetPath).text();
12955
13051
  const incomingContent = file.content.toString("utf-8");
@@ -13038,7 +13134,7 @@ function mergeDevDependencies(existing, newDeps) {
13038
13134
  return { ...existing, devDependencies: merged };
13039
13135
  }
13040
13136
  async function readOpencodePackageJson(opencodeDir) {
13041
- const pkgPath = join2(opencodeDir, "package.json");
13137
+ const pkgPath = join(opencodeDir, "package.json");
13042
13138
  if (!existsSync(pkgPath)) {
13043
13139
  return { ...DEFAULT_PACKAGE_JSON };
13044
13140
  }
@@ -13051,7 +13147,7 @@ async function readOpencodePackageJson(opencodeDir) {
13051
13147
  }
13052
13148
  }
13053
13149
  async function ensureManifestFilesAreTracked(opencodeDir) {
13054
- const gitignorePath = join2(opencodeDir, ".gitignore");
13150
+ const gitignorePath = join(opencodeDir, ".gitignore");
13055
13151
  const filesToTrack = new Set(["package.json", "bun.lock"]);
13056
13152
  const requiredIgnores = ["node_modules"];
13057
13153
  let lines = [];
@@ -13074,12 +13170,12 @@ async function updateOpencodeDevDependencies(cwd, npmDeps, npmDevDeps) {
13074
13170
  const allDepSpecs = [...npmDeps, ...npmDevDeps];
13075
13171
  if (allDepSpecs.length === 0)
13076
13172
  return;
13077
- const opencodeDir = join2(cwd, ".opencode");
13173
+ const opencodeDir = join(cwd, ".opencode");
13078
13174
  await mkdir2(opencodeDir, { recursive: true });
13079
13175
  const parsedDeps = allDepSpecs.map(parseNpmDependency);
13080
13176
  const existing = await readOpencodePackageJson(opencodeDir);
13081
13177
  const updated = mergeDevDependencies(existing, parsedDeps);
13082
- await Bun.write(join2(opencodeDir, "package.json"), `${JSON.stringify(updated, null, 2)}
13178
+ await Bun.write(join(opencodeDir, "package.json"), `${JSON.stringify(updated, null, 2)}
13083
13179
  `);
13084
13180
  await ensureManifestFilesAreTracked(opencodeDir);
13085
13181
  }
@@ -13093,11 +13189,11 @@ function findComponentByFile(lock, filePath) {
13093
13189
  }
13094
13190
 
13095
13191
  // src/commands/build.ts
13096
- import { join as join4, relative } from "path";
13192
+ import { join as join3, relative } from "path";
13097
13193
 
13098
13194
  // src/lib/build-registry.ts
13099
13195
  import { mkdir as mkdir3 } from "fs/promises";
13100
- import { dirname as dirname3, join as join3 } from "path";
13196
+ import { dirname as dirname2, join as join2 } from "path";
13101
13197
  class BuildRegistryError extends Error {
13102
13198
  errors;
13103
13199
  constructor(message, errors3 = []) {
@@ -13108,8 +13204,8 @@ class BuildRegistryError extends Error {
13108
13204
  }
13109
13205
  async function buildRegistry(options2) {
13110
13206
  const { source: sourcePath, out: outPath } = options2;
13111
- const jsoncFile = Bun.file(join3(sourcePath, "registry.jsonc"));
13112
- const jsonFile = Bun.file(join3(sourcePath, "registry.json"));
13207
+ const jsoncFile = Bun.file(join2(sourcePath, "registry.jsonc"));
13208
+ const jsonFile = Bun.file(join2(sourcePath, "registry.json"));
13113
13209
  const jsoncExists = await jsoncFile.exists();
13114
13210
  const jsonExists = await jsonFile.exists();
13115
13211
  if (!jsoncExists && !jsonExists) {
@@ -13125,7 +13221,7 @@ async function buildRegistry(options2) {
13125
13221
  }
13126
13222
  const registry = parseResult.data;
13127
13223
  const validationErrors = [];
13128
- const componentsDir = join3(outPath, "components");
13224
+ const componentsDir = join2(outPath, "components");
13129
13225
  await mkdir3(componentsDir, { recursive: true });
13130
13226
  for (const component of registry.components) {
13131
13227
  const packument = {
@@ -13137,13 +13233,13 @@ async function buildRegistry(options2) {
13137
13233
  latest: registry.version
13138
13234
  }
13139
13235
  };
13140
- const packumentPath = join3(componentsDir, `${component.name}.json`);
13236
+ const packumentPath = join2(componentsDir, `${component.name}.json`);
13141
13237
  await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
13142
13238
  for (const rawFile of component.files) {
13143
13239
  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);
13240
+ const sourceFilePath = join2(sourcePath, "files", file.path);
13241
+ const destFilePath = join2(componentsDir, component.name, file.path);
13242
+ const destFileDir = dirname2(destFilePath);
13147
13243
  if (!await Bun.file(sourceFilePath).exists()) {
13148
13244
  validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`);
13149
13245
  continue;
@@ -13169,11 +13265,11 @@ async function buildRegistry(options2) {
13169
13265
  description: c.description
13170
13266
  }))
13171
13267
  };
13172
- await Bun.write(join3(outPath, "index.json"), JSON.stringify(index, null, 2));
13173
- const wellKnownDir = join3(outPath, ".well-known");
13268
+ await Bun.write(join2(outPath, "index.json"), JSON.stringify(index, null, 2));
13269
+ const wellKnownDir = join2(outPath, ".well-known");
13174
13270
  await mkdir3(wellKnownDir, { recursive: true });
13175
13271
  const discovery = { registry: "/index.json" };
13176
- await Bun.write(join3(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
13272
+ await Bun.write(join2(wellKnownDir, "ocx.json"), JSON.stringify(discovery, null, 2));
13177
13273
  return {
13178
13274
  name: registry.name,
13179
13275
  namespace: registry.namespace,
@@ -13187,8 +13283,8 @@ async function buildRegistry(options2) {
13187
13283
  function registerBuildCommand(program2) {
13188
13284
  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
13285
  try {
13190
- const sourcePath = join4(options2.cwd, path5);
13191
- const outPath = join4(options2.cwd, options2.out);
13286
+ const sourcePath = join3(options2.cwd, path5);
13287
+ const outPath = join3(options2.cwd, options2.out);
13192
13288
  const spinner2 = createSpinner({
13193
13289
  text: "Building registry...",
13194
13290
  quiet: options2.quiet || options2.json
@@ -14143,7 +14239,7 @@ function resolveEditor() {
14143
14239
  return process.env.OCX_EDITOR || process.env.EDITOR || process.env.VISUAL || "vi";
14144
14240
  }
14145
14241
  function registerGhostConfigCommand(parent) {
14146
- const cmd = parent.command("config").description("Open ghost configuration in your editor");
14242
+ 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
14243
  addOutputOptions(cmd).action(async (options2) => {
14148
14244
  try {
14149
14245
  await runGhostConfig(options2);
@@ -14153,13 +14249,15 @@ function registerGhostConfigCommand(parent) {
14153
14249
  });
14154
14250
  }
14155
14251
  async function runGhostConfig(options2) {
14156
- const exists = await ghostConfigExists();
14157
- if (!exists) {
14158
- throw new GhostNotInitializedError;
14252
+ const manager = ProfileManager.create();
14253
+ if (!await manager.isInitialized()) {
14254
+ throw new ProfilesNotInitializedError;
14159
14255
  }
14160
- const configPath = getGhostConfigPath();
14256
+ const profileName = await manager.getCurrent(options2.profile);
14257
+ await manager.get(profileName);
14258
+ const configPath = getProfileGhostConfig(profileName);
14161
14259
  if (options2.json) {
14162
- console.log(JSON.stringify({ success: true, path: configPath }));
14260
+ console.log(JSON.stringify({ success: true, profile: profileName, path: configPath }));
14163
14261
  return;
14164
14262
  }
14165
14263
  const editor = resolveEditor();
@@ -14178,25 +14276,8 @@ async function runGhostConfig(options2) {
14178
14276
  }
14179
14277
 
14180
14278
  // 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 to use
14188
- "registries": {
14189
- "default": {
14190
- "url": "https://registry.opencode.ai"
14191
- }
14192
- },
14193
-
14194
- // Where to install components (relative to project root)
14195
- "componentPath": "src/components"
14196
- }
14197
- `;
14198
14279
  function registerGhostInitCommand(parent) {
14199
- const cmd = parent.command("init").description("Initialize ghost mode with global configuration");
14280
+ const cmd = parent.command("init").description("Initialize ghost mode with profiles");
14200
14281
  addOutputOptions(cmd);
14201
14282
  addVerboseOption(cmd);
14202
14283
  cmd.action(async (options2) => {
@@ -14208,47 +14289,176 @@ function registerGhostInitCommand(parent) {
14208
14289
  });
14209
14290
  }
14210
14291
  async function runGhostInit(options2) {
14211
- const configPath = getGhostConfigPath();
14212
- const configDir = getGhostConfigDir();
14213
- await mkdir4(configDir, { recursive: true });
14214
- try {
14215
- await writeFile2(configPath, DEFAULT_GHOST_CONFIG, { flag: "wx" });
14216
- } catch (err) {
14217
- if (err.code === "EEXIST") {
14218
- throw new GhostAlreadyInitializedError(configPath);
14219
- }
14220
- throw err;
14221
- }
14222
- const opencodeResult = await ensureOpencodeConfig(configDir);
14292
+ const manager = ProfileManager.create();
14293
+ if (await manager.isInitialized()) {
14294
+ const profilesDir2 = getProfilesDir();
14295
+ throw new ProfileExistsError(`Ghost mode already initialized at ${profilesDir2}`);
14296
+ }
14297
+ await manager.initialize();
14298
+ const profilesDir = getProfilesDir();
14299
+ const ghostConfigPath = getProfileGhostConfig("default");
14223
14300
  if (options2.json) {
14224
14301
  console.log(JSON.stringify({
14225
14302
  success: true,
14226
- path: configPath,
14227
- opencodePath: opencodeResult.path,
14228
- opencodeCreated: opencodeResult.created
14303
+ profilesDir,
14304
+ defaultProfile: "default",
14305
+ ghostConfigPath
14229
14306
  }));
14230
14307
  return;
14231
14308
  }
14232
14309
  if (!options2.quiet) {
14233
14310
  logger.success("Ghost mode initialized");
14234
- logger.info(`Created ${configPath}`);
14235
- if (opencodeResult.created) {
14236
- logger.info(`Created ${opencodeResult.path}`);
14237
- }
14311
+ logger.info(`Created ${profilesDir}`);
14312
+ logger.info(`Created profile "default"`);
14238
14313
  logger.info("");
14239
14314
  logger.info("Next steps:");
14240
14315
  logger.info(" 1. Edit your config: ocx ghost config");
14241
14316
  logger.info(" 2. Add registries: ocx ghost registry add <url> --name <name>");
14242
14317
  logger.info(" 3. Add components: ocx ghost add <component>");
14318
+ logger.info(" 4. Create profiles: ocx ghost profile add <name>");
14319
+ }
14320
+ }
14321
+
14322
+ // src/profile/migrate.ts
14323
+ import { chmod, readdir as readdir2, rename as rename2, stat as stat2 } from "fs/promises";
14324
+ import { homedir as homedir2 } from "os";
14325
+ import path5 from "path";
14326
+ function getLegacyConfigDir() {
14327
+ const base = process.env.XDG_CONFIG_HOME || path5.join(homedir2(), ".config");
14328
+ return path5.join(base, "ocx");
14329
+ }
14330
+ async function needsMigration() {
14331
+ const legacyDir = getLegacyConfigDir();
14332
+ const profilesDir = getProfilesDir();
14333
+ try {
14334
+ await stat2(legacyDir);
14335
+ try {
14336
+ await stat2(profilesDir);
14337
+ return false;
14338
+ } catch {
14339
+ return true;
14340
+ }
14341
+ } catch {
14342
+ return false;
14343
+ }
14344
+ }
14345
+ async function migrate(dryRun = false) {
14346
+ const result = {
14347
+ success: false,
14348
+ migratedFiles: [],
14349
+ backupPath: null,
14350
+ errors: []
14351
+ };
14352
+ const legacyDir = getLegacyConfigDir();
14353
+ const profilesDir = getProfilesDir();
14354
+ try {
14355
+ await stat2(legacyDir);
14356
+ } catch {
14357
+ result.errors.push(`No legacy config found at ${legacyDir}`);
14358
+ return result;
14359
+ }
14360
+ try {
14361
+ await stat2(profilesDir);
14362
+ result.errors.push(`Profiles directory already exists at ${profilesDir}`);
14363
+ return result;
14364
+ } catch {}
14365
+ const legacyFiles = await readdir2(legacyDir);
14366
+ const filesToMigrate = legacyFiles.filter((f) => f === "ghost.jsonc" || f === "opencode.jsonc" || f === "AGENTS.md");
14367
+ if (filesToMigrate.length === 0) {
14368
+ result.errors.push("No migratable files found in legacy config");
14369
+ return result;
14370
+ }
14371
+ if (dryRun) {
14372
+ result.migratedFiles = filesToMigrate.map((f) => path5.join(legacyDir, f));
14373
+ result.backupPath = `${legacyDir}.bak`;
14374
+ result.success = true;
14375
+ return result;
14376
+ }
14377
+ const manager = ProfileManager.create();
14378
+ await manager.initialize();
14379
+ const defaultProfileDir = getProfileDir("default");
14380
+ for (const file of filesToMigrate) {
14381
+ const srcPath = path5.join(legacyDir, file);
14382
+ const destPath = path5.join(defaultProfileDir, file);
14383
+ await Bun.write(destPath, Bun.file(srcPath));
14384
+ await chmod(destPath, 384);
14385
+ result.migratedFiles.push(file);
14386
+ }
14387
+ const backupPath = `${legacyDir}.bak`;
14388
+ await rename2(legacyDir, backupPath);
14389
+ result.backupPath = backupPath;
14390
+ result.success = true;
14391
+ return result;
14392
+ }
14393
+
14394
+ // src/commands/ghost/migrate.ts
14395
+ function registerGhostMigrateCommand(parent) {
14396
+ parent.command("migrate").description("Migrate from legacy ~/.config/ocx/ to new profiles system").option("--dry-run", "Preview changes without making them").action(async (options2) => {
14397
+ try {
14398
+ await runMigrate(options2);
14399
+ } catch (error) {
14400
+ handleError(error);
14401
+ }
14402
+ });
14403
+ }
14404
+ async function runMigrate(options2) {
14405
+ const needsMigrationResult = await needsMigration();
14406
+ if (!needsMigrationResult) {
14407
+ const legacyDir = getLegacyConfigDir();
14408
+ const profilesDir = getProfilesDir();
14409
+ console.log("No migration needed.");
14410
+ console.log(` Legacy config: ${legacyDir} (not found)`);
14411
+ console.log(` Profiles: ${profilesDir}`);
14412
+ return;
14413
+ }
14414
+ if (options2.dryRun) {
14415
+ console.log(`Dry run - no changes will be made.
14416
+ `);
14417
+ }
14418
+ const result = await migrate(options2.dryRun);
14419
+ if (!result.success) {
14420
+ console.error("Migration failed:");
14421
+ for (const error of result.errors) {
14422
+ console.error(` - ${error}`);
14423
+ }
14424
+ process.exit(1);
14425
+ }
14426
+ if (options2.dryRun) {
14427
+ console.log("Migration preview:");
14428
+ console.log(`
14429
+ Files to migrate to default profile:`);
14430
+ for (const file of result.migratedFiles) {
14431
+ console.log(` ${file}`);
14432
+ }
14433
+ console.log(`
14434
+ Legacy config will be renamed to:`);
14435
+ console.log(` ${result.backupPath}`);
14436
+ console.log(`
14437
+ Run without --dry-run to perform migration.`);
14438
+ } else {
14439
+ console.log("Migration complete!");
14440
+ console.log(`
14441
+ Migrated to default profile:`);
14442
+ for (const file of result.migratedFiles) {
14443
+ console.log(` ${file}`);
14444
+ }
14445
+ console.log(`
14446
+ Legacy config backed up to:`);
14447
+ console.log(` ${result.backupPath}`);
14448
+ console.log(`
14449
+ Profile location:`);
14450
+ console.log(` ${getProfileDir("default")}`);
14243
14451
  }
14244
14452
  }
14245
14453
 
14246
14454
  // src/commands/ghost/opencode.ts
14247
14455
  import { renameSync, rmSync } from "fs";
14456
+ import { copyFile as copyFilePromise } from "fs/promises";
14457
+ import path6 from "path";
14248
14458
 
14249
14459
  // src/utils/opencode-discovery.ts
14250
14460
  import { exists } from "fs/promises";
14251
- import { dirname as dirname4, join as join5 } from "path";
14461
+ import { dirname as dirname3, join as join4 } from "path";
14252
14462
  var CONFIG_FILES = ["opencode.jsonc", "opencode.json"];
14253
14463
  var RULE_FILES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
14254
14464
  var CONFIG_DIRS = [".opencode"];
@@ -14256,13 +14466,13 @@ async function findUp(target, start, stop) {
14256
14466
  let current = start;
14257
14467
  const result = [];
14258
14468
  while (true) {
14259
- const search = join5(current, target);
14469
+ const search = join4(current, target);
14260
14470
  if (await exists(search).catch(() => false)) {
14261
14471
  result.push(search);
14262
14472
  }
14263
14473
  if (stop === current)
14264
14474
  break;
14265
- const parent = dirname4(current);
14475
+ const parent = dirname3(current);
14266
14476
  if (parent === current)
14267
14477
  break;
14268
14478
  current = parent;
@@ -14274,14 +14484,14 @@ async function* up(options2) {
14274
14484
  let current = start;
14275
14485
  while (true) {
14276
14486
  for (const target of targets) {
14277
- const search = join5(current, target);
14487
+ const search = join4(current, target);
14278
14488
  if (await exists(search).catch(() => false)) {
14279
14489
  yield search;
14280
14490
  }
14281
14491
  }
14282
14492
  if (stop === current)
14283
14493
  break;
14284
- const parent = dirname4(current);
14494
+ const parent = dirname3(current);
14285
14495
  if (parent === current)
14286
14496
  break;
14287
14497
  current = parent;
@@ -14291,14 +14501,14 @@ async function discoverProjectFiles(start, stop) {
14291
14501
  const excluded = new Set;
14292
14502
  for (const file of CONFIG_FILES) {
14293
14503
  const found = await findUp(file, start, stop);
14294
- for (const path5 of found) {
14295
- excluded.add(path5);
14504
+ for (const path6 of found) {
14505
+ excluded.add(path6);
14296
14506
  }
14297
14507
  }
14298
14508
  for (const file of RULE_FILES) {
14299
14509
  const found = await findUp(file, start, stop);
14300
- for (const path5 of found) {
14301
- excluded.add(path5);
14510
+ for (const path6 of found) {
14511
+ excluded.add(path6);
14302
14512
  }
14303
14513
  }
14304
14514
  for await (const dir of up({ targets: CONFIG_DIRS, start, stop })) {
@@ -14319,22 +14529,22 @@ function filterExcludedPaths(excludedPaths, includePatterns, excludePatterns) {
14319
14529
  const includeGlobs = includePatterns.map((p) => new Glob2(p));
14320
14530
  const excludeGlobs = excludePatterns?.map((p) => new Glob2(p)) ?? [];
14321
14531
  const filteredExclusions = new Set;
14322
- for (const path5 of excludedPaths) {
14323
- const matchesInclude = matchesAnyGlob(path5, includeGlobs);
14324
- const matchesExclude = matchesAnyGlob(path5, excludeGlobs);
14532
+ for (const path6 of excludedPaths) {
14533
+ const matchesInclude = matchesAnyGlob(path6, includeGlobs);
14534
+ const matchesExclude = matchesAnyGlob(path6, excludeGlobs);
14325
14535
  if (matchesInclude && !matchesExclude) {
14326
14536
  continue;
14327
14537
  }
14328
- filteredExclusions.add(path5);
14538
+ filteredExclusions.add(path6);
14329
14539
  }
14330
14540
  return filteredExclusions;
14331
14541
  }
14332
14542
 
14333
14543
  // src/utils/symlink-farm.ts
14334
14544
  import { randomBytes } from "crypto";
14335
- import { readdir, rename, rm, stat, symlink } from "fs/promises";
14545
+ import { mkdir as mkdir4, readdir as readdir3, rename as rename3, rm as rm2, stat as stat3, symlink as symlink2 } from "fs/promises";
14336
14546
  import { tmpdir } from "os";
14337
- import { isAbsolute, join as join6 } from "path";
14547
+ import { dirname as dirname4, isAbsolute, join as join5, relative as relative2 } from "path";
14338
14548
  var STALE_SESSION_THRESHOLD_MS = 24 * 60 * 60 * 1000;
14339
14549
  var REMOVING_THRESHOLD_MS = 60 * 60 * 1000;
14340
14550
  var GHOST_DIR_PREFIX = "ocx-ghost-";
@@ -14345,31 +14555,57 @@ async function createSymlinkFarm(sourceDir, excludePaths) {
14345
14555
  throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
14346
14556
  }
14347
14557
  const suffix = randomBytes(4).toString("hex");
14348
- const tempDir = join6(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
14349
- await Bun.write(join6(tempDir, GHOST_MARKER_FILE), "");
14558
+ const tempDir = join5(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
14559
+ await Bun.write(join5(tempDir, GHOST_MARKER_FILE), "");
14350
14560
  try {
14351
- const entries = await readdir(sourceDir, { withFileTypes: true });
14561
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
14352
14562
  for (const entry of entries) {
14353
- const sourcePath = join6(sourceDir, entry.name);
14563
+ const sourcePath = join5(sourceDir, entry.name);
14354
14564
  if (excludePaths.has(sourcePath))
14355
14565
  continue;
14356
- const targetPath = join6(tempDir, entry.name);
14357
- await symlink(sourcePath, targetPath);
14566
+ const targetPath = join5(tempDir, entry.name);
14567
+ await symlink2(sourcePath, targetPath);
14358
14568
  }
14359
14569
  return tempDir;
14360
14570
  } catch (error) {
14361
- await rm(tempDir, { recursive: true, force: true }).catch(() => {});
14571
+ await rm2(tempDir, { recursive: true, force: true }).catch(() => {});
14362
14572
  throw error;
14363
14573
  }
14364
14574
  }
14575
+ async function injectGhostFiles(tempDir, sourceDir, injectPaths) {
14576
+ if (!isAbsolute(tempDir)) {
14577
+ throw new Error(`tempDir must be an absolute path, got: ${tempDir}`);
14578
+ }
14579
+ if (!isAbsolute(sourceDir)) {
14580
+ throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
14581
+ }
14582
+ for (const injectPath of injectPaths) {
14583
+ const relativePath = relative2(sourceDir, injectPath);
14584
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
14585
+ throw new Error(`injectPath must be within sourceDir: ${injectPath}`);
14586
+ }
14587
+ const targetPath = join5(tempDir, relativePath);
14588
+ const parentDir = dirname4(targetPath);
14589
+ if (parentDir !== tempDir) {
14590
+ await mkdir4(parentDir, { recursive: true });
14591
+ }
14592
+ try {
14593
+ await symlink2(injectPath, targetPath);
14594
+ } catch (err) {
14595
+ if (err.code !== "EEXIST") {
14596
+ throw new Error(`Failed to inject ${injectPath} \u2192 ${targetPath}: ${err.message}`);
14597
+ }
14598
+ }
14599
+ }
14600
+ }
14365
14601
  async function cleanupSymlinkFarm(tempDir) {
14366
14602
  const removingPath = `${tempDir}${REMOVING_SUFFIX}`;
14367
14603
  try {
14368
- await rename(tempDir, removingPath);
14604
+ await rename3(tempDir, removingPath);
14369
14605
  } catch {
14370
14606
  return;
14371
14607
  }
14372
- await rm(removingPath, { recursive: true, force: true });
14608
+ await rm2(removingPath, { recursive: true, force: true });
14373
14609
  }
14374
14610
  async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14375
14611
  let cleanedCount = 0;
@@ -14378,19 +14614,19 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14378
14614
  }
14379
14615
  let dirNames;
14380
14616
  try {
14381
- dirNames = await readdir(tempBase);
14617
+ dirNames = await readdir3(tempBase);
14382
14618
  } catch {
14383
14619
  return 0;
14384
14620
  }
14385
14621
  for (const dirName of dirNames) {
14386
- const dirPath = join6(tempBase, dirName);
14622
+ const dirPath = join5(tempBase, dirName);
14387
14623
  const isRemovingDir = dirName.endsWith(REMOVING_SUFFIX);
14388
14624
  const isGhostDir = dirName.startsWith(GHOST_DIR_PREFIX) && !isRemovingDir;
14389
14625
  if (!isRemovingDir && !isGhostDir)
14390
14626
  continue;
14391
14627
  let stats;
14392
14628
  try {
14393
- stats = await stat(dirPath);
14629
+ stats = await stat3(dirPath);
14394
14630
  } catch {
14395
14631
  continue;
14396
14632
  }
@@ -14403,10 +14639,10 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14403
14639
  try {
14404
14640
  if (isGhostDir) {
14405
14641
  const removingPath = `${dirPath}${REMOVING_SUFFIX}`;
14406
- await rename(dirPath, removingPath);
14407
- await rm(removingPath, { recursive: true, force: true });
14642
+ await rename3(dirPath, removingPath);
14643
+ await rm2(removingPath, { recursive: true, force: true });
14408
14644
  } else {
14409
- await rm(dirPath, { recursive: true, force: true });
14645
+ await rm2(dirPath, { recursive: true, force: true });
14410
14646
  }
14411
14647
  cleanedCount++;
14412
14648
  } catch {}
@@ -14416,7 +14652,7 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
14416
14652
 
14417
14653
  // src/commands/ghost/opencode.ts
14418
14654
  function registerGhostOpenCodeCommand(parent) {
14419
- parent.command("opencode").description("Launch OpenCode with ghost mode configuration").addOption(sharedOptions.json()).addOption(sharedOptions.quiet()).allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
14655
+ 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) => {
14420
14656
  try {
14421
14657
  const args = command.args;
14422
14658
  await runGhostOpenCode(args, options2);
@@ -14426,22 +14662,39 @@ function registerGhostOpenCodeCommand(parent) {
14426
14662
  });
14427
14663
  }
14428
14664
  async function runGhostOpenCode(args, options2) {
14429
- if (!await ghostConfigExists()) {
14430
- throw new GhostNotInitializedError;
14665
+ const manager = ProfileManager.create();
14666
+ if (!await manager.isInitialized()) {
14667
+ throw new ProfilesNotInitializedError;
14668
+ }
14669
+ if (!options2.quiet && await needsMigration()) {
14670
+ console.log("Notice: Found legacy config at ~/.config/ocx/");
14671
+ console.log(`Run 'ocx ghost migrate' to upgrade to the new profiles system.
14672
+ `);
14431
14673
  }
14432
14674
  await cleanupOrphanedGhostDirs();
14433
- const openCodeConfig = await loadGhostOpencodeConfig();
14434
- const ghostConfigDir = getGhostConfigDir();
14435
- if (Object.keys(openCodeConfig).length === 0 && !options2.quiet) {
14436
- logger.warn(`No opencode.jsonc found at ${getGhostOpencodeConfigPath()}. Run 'ocx ghost init' first.`);
14675
+ const profileName = await manager.getCurrent(options2.profile);
14676
+ const profile = await manager.get(profileName);
14677
+ const profileDir = getProfileDir(profileName);
14678
+ const profileOpencodePath = getProfileOpencodeConfig(profileName);
14679
+ const profileOpencodeFile = Bun.file(profileOpencodePath);
14680
+ const hasOpencodeConfig = await profileOpencodeFile.exists();
14681
+ if (!hasOpencodeConfig && !options2.quiet) {
14682
+ logger.warn(`No opencode.jsonc found at ${profileOpencodePath}. Create one to customize OpenCode settings.`);
14437
14683
  }
14438
14684
  const cwd = process.cwd();
14439
14685
  const gitContext = await detectGitRepo(cwd);
14440
14686
  const gitRoot = gitContext?.workTree ?? cwd;
14441
14687
  const discoveredPaths = await discoverProjectFiles(cwd, gitRoot);
14442
- const ghostConfig = await loadGhostConfig();
14688
+ const ghostConfig = profile.ghost;
14443
14689
  const excludePaths = filterExcludedPaths(discoveredPaths, ghostConfig.include, ghostConfig.exclude);
14444
14690
  const tempDir = await createSymlinkFarm(cwd, excludePaths);
14691
+ const ghostFiles = await discoverProjectFiles(profileDir, profileDir);
14692
+ await injectGhostFiles(tempDir, profileDir, ghostFiles);
14693
+ if (profile.hasAgents) {
14694
+ const agentsPath = getProfileAgents(profileName);
14695
+ const destAgentsPath = path6.join(tempDir, "AGENTS.md");
14696
+ await copyFilePromise(agentsPath, destAgentsPath);
14697
+ }
14445
14698
  let cleanupDone = false;
14446
14699
  const performCleanup = async () => {
14447
14700
  if (cleanupDone)
@@ -14469,8 +14722,9 @@ async function runGhostOpenCode(args, options2) {
14469
14722
  cwd: tempDir,
14470
14723
  env: {
14471
14724
  ...process.env,
14472
- OPENCODE_CONFIG_CONTENT: JSON.stringify(openCodeConfig),
14473
- OPENCODE_CONFIG_DIR: ghostConfigDir,
14725
+ ...profile.opencode && { OPENCODE_CONFIG_CONTENT: JSON.stringify(profile.opencode) },
14726
+ OPENCODE_CONFIG_DIR: profileDir,
14727
+ OCX_PROFILE: profileName,
14474
14728
  ...gitContext && {
14475
14729
  GIT_WORK_TREE: gitContext.workTree,
14476
14730
  GIT_DIR: gitContext.gitDir
@@ -14491,6 +14745,185 @@ async function runGhostOpenCode(args, options2) {
14491
14745
  }
14492
14746
  }
14493
14747
 
14748
+ // src/commands/ghost/profile/add.ts
14749
+ function registerProfileAddCommand(parent) {
14750
+ parent.command("add <name>").description("Create a new ghost profile").option("--from <profile>", "Clone settings from existing profile").action(async (name, options2) => {
14751
+ try {
14752
+ await runProfileAdd(name, options2);
14753
+ } catch (error) {
14754
+ handleError(error);
14755
+ }
14756
+ });
14757
+ }
14758
+ async function runProfileAdd(name, options2) {
14759
+ const manager = ProfileManager.create();
14760
+ if (options2.from) {
14761
+ const source = await manager.get(options2.from);
14762
+ await manager.add(name);
14763
+ await atomicWrite(getProfileGhostConfig(name), source.ghost);
14764
+ logger.success(`Created profile "${name}" (cloned from "${options2.from}")`);
14765
+ } else {
14766
+ await manager.add(name);
14767
+ logger.success(`Created profile "${name}"`);
14768
+ }
14769
+ }
14770
+
14771
+ // src/commands/ghost/profile/config.ts
14772
+ function registerProfileConfigCommand(parent) {
14773
+ parent.command("config [name]").description("Open profile ghost.jsonc in editor").action(async (name) => {
14774
+ try {
14775
+ await runProfileConfig(name);
14776
+ } catch (error) {
14777
+ handleError(error);
14778
+ }
14779
+ });
14780
+ }
14781
+ async function runProfileConfig(name) {
14782
+ const manager = ProfileManager.create();
14783
+ const profileName = name ?? await manager.getCurrent();
14784
+ await manager.get(profileName);
14785
+ const configPath = getProfileGhostConfig(profileName);
14786
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
14787
+ const proc = Bun.spawn([editor, configPath], {
14788
+ stdin: "inherit",
14789
+ stdout: "inherit",
14790
+ stderr: "inherit"
14791
+ });
14792
+ const exitCode = await proc.exited;
14793
+ if (exitCode !== 0) {
14794
+ throw new Error(`Editor exited with code ${exitCode}`);
14795
+ }
14796
+ }
14797
+
14798
+ // src/commands/ghost/profile/list.ts
14799
+ function registerProfileListCommand(parent) {
14800
+ parent.command("list").alias("ls").description("List all ghost profiles").addOption(sharedOptions.json()).action(async (options2) => {
14801
+ try {
14802
+ await runProfileList(options2);
14803
+ } catch (error) {
14804
+ handleError(error, { json: options2.json });
14805
+ }
14806
+ });
14807
+ }
14808
+ async function runProfileList(options2) {
14809
+ const manager = ProfileManager.create();
14810
+ if (!await manager.isInitialized()) {
14811
+ if (options2.json) {
14812
+ console.log(JSON.stringify({ profiles: [], current: null }));
14813
+ } else {
14814
+ console.log("No profiles found. Run 'ocx ghost init' to create one.");
14815
+ }
14816
+ return;
14817
+ }
14818
+ const profiles = await manager.list();
14819
+ const current = await manager.getCurrent();
14820
+ if (options2.json) {
14821
+ console.log(JSON.stringify({ profiles, current }, null, 2));
14822
+ return;
14823
+ }
14824
+ if (profiles.length === 0) {
14825
+ console.log("No profiles found.");
14826
+ return;
14827
+ }
14828
+ for (const name of profiles) {
14829
+ const marker = name === current ? "* " : " ";
14830
+ console.log(`${marker}${name}`);
14831
+ }
14832
+ }
14833
+
14834
+ // src/commands/ghost/profile/remove.ts
14835
+ 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) => {
14837
+ try {
14838
+ await runProfileRemove(name, options2);
14839
+ } catch (error) {
14840
+ handleError(error);
14841
+ }
14842
+ });
14843
+ }
14844
+ async function runProfileRemove(name, options2) {
14845
+ const manager = ProfileManager.create();
14846
+ if (!await manager.exists(name)) {
14847
+ throw new ProfileNotFoundError(name);
14848
+ }
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);
14860
+ logger.success(`Deleted profile "${name}"`);
14861
+ }
14862
+ function confirmDeletion(name) {
14863
+ const answer = prompt(`Delete profile "${name}"? This cannot be undone. [y/N]`);
14864
+ return answer?.toLowerCase() === "y";
14865
+ }
14866
+
14867
+ // src/commands/ghost/profile/show.ts
14868
+ function registerProfileShowCommand(parent) {
14869
+ parent.command("show [name]").description("Display profile contents").addOption(sharedOptions.json()).action(async (name, options2) => {
14870
+ try {
14871
+ await runProfileShow(name, options2);
14872
+ } catch (error) {
14873
+ handleError(error, { json: options2.json });
14874
+ }
14875
+ });
14876
+ }
14877
+ async function runProfileShow(name, options2) {
14878
+ const manager = ProfileManager.create();
14879
+ const profileName = name ?? await manager.getCurrent();
14880
+ const profile = await manager.get(profileName);
14881
+ if (options2.json) {
14882
+ console.log(JSON.stringify(profile, null, 2));
14883
+ return;
14884
+ }
14885
+ console.log(`Profile: ${profile.name}`);
14886
+ console.log(`
14887
+ Files:`);
14888
+ console.log(` ghost.jsonc: ${getProfileGhostConfig(profileName)}`);
14889
+ if (profile.opencode) {
14890
+ console.log(` opencode.jsonc: ${getProfileOpencodeConfig(profileName)}`);
14891
+ }
14892
+ if (profile.hasAgents) {
14893
+ console.log(` AGENTS.md: ${getProfileAgents(profileName)}`);
14894
+ }
14895
+ console.log(`
14896
+ Ghost Config:`);
14897
+ console.log(JSON.stringify(profile.ghost, null, 2));
14898
+ }
14899
+
14900
+ // src/commands/ghost/profile/use.ts
14901
+ function registerProfileUseCommand(parent) {
14902
+ parent.command("use <name>").description("Set the current ghost profile").action(async (name) => {
14903
+ try {
14904
+ await runProfileUse(name);
14905
+ } catch (error) {
14906
+ handleError(error);
14907
+ }
14908
+ });
14909
+ }
14910
+ async function runProfileUse(name) {
14911
+ const manager = ProfileManager.create();
14912
+ await manager.setCurrent(name);
14913
+ logger.success(`Switched to profile "${name}"`);
14914
+ }
14915
+
14916
+ // src/commands/ghost/profile/index.ts
14917
+ function registerGhostProfileCommand(parent) {
14918
+ const profile = parent.command("profile").alias("p").description("Manage ghost mode profiles");
14919
+ registerProfileListCommand(profile);
14920
+ registerProfileAddCommand(profile);
14921
+ registerProfileRemoveCommand(profile);
14922
+ registerProfileUseCommand(profile);
14923
+ registerProfileShowCommand(profile);
14924
+ registerProfileConfigCommand(profile);
14925
+ }
14926
+
14494
14927
  // src/commands/registry.ts
14495
14928
  async function runRegistryAddCore2(url, options2, callbacks) {
14496
14929
  if (callbacks.isLocked?.()) {
@@ -14619,11 +15052,12 @@ function registerRegistryCommand(program2) {
14619
15052
  }
14620
15053
 
14621
15054
  // src/commands/ghost/registry.ts
14622
- async function ensureGhostInitialized() {
14623
- const exists2 = await ghostConfigExists();
14624
- if (!exists2) {
14625
- throw new GhostNotInitializedError;
15055
+ async function ensureProfilesInitialized() {
15056
+ const manager = ProfileManager.create();
15057
+ if (!await manager.isInitialized()) {
15058
+ throw new ProfilesNotInitializedError;
14626
15059
  }
15060
+ return manager;
14627
15061
  }
14628
15062
  function registerGhostRegistryCommand(parent) {
14629
15063
  const registry = parent.command("registry").description("Manage ghost mode registries");
@@ -14631,13 +15065,14 @@ function registerGhostRegistryCommand(parent) {
14631
15065
  addOutputOptions(addCmd);
14632
15066
  addCmd.action(async (url, options2) => {
14633
15067
  try {
14634
- await ensureGhostInitialized();
14635
- const config = await loadGhostConfig();
15068
+ const manager = await ensureProfilesInitialized();
15069
+ const profileName = await manager.getCurrent();
15070
+ const profile = await manager.get(profileName);
14636
15071
  const result = await runRegistryAddCore2(url, options2, {
14637
- getRegistries: () => config.registries,
15072
+ getRegistries: () => profile.ghost.registries,
14638
15073
  setRegistry: async (name, regConfig) => {
14639
- config.registries[name] = regConfig;
14640
- await saveGhostConfig(config);
15074
+ profile.ghost.registries[name] = regConfig;
15075
+ await atomicWrite(getProfileGhostConfig(profileName), profile.ghost);
14641
15076
  }
14642
15077
  });
14643
15078
  if (options2.json) {
@@ -14657,13 +15092,14 @@ function registerGhostRegistryCommand(parent) {
14657
15092
  addOutputOptions(removeCmd);
14658
15093
  removeCmd.action(async (name, options2) => {
14659
15094
  try {
14660
- await ensureGhostInitialized();
14661
- const config = await loadGhostConfig();
15095
+ const manager = await ensureProfilesInitialized();
15096
+ const profileName = await manager.getCurrent();
15097
+ const profile = await manager.get(profileName);
14662
15098
  const result = await runRegistryRemoveCore(name, {
14663
- getRegistries: () => config.registries,
15099
+ getRegistries: () => profile.ghost.registries,
14664
15100
  removeRegistry: async (regName) => {
14665
- delete config.registries[regName];
14666
- await saveGhostConfig(config);
15101
+ delete profile.ghost.registries[regName];
15102
+ await atomicWrite(getProfileGhostConfig(profileName), profile.ghost);
14667
15103
  }
14668
15104
  });
14669
15105
  if (options2.json) {
@@ -14679,10 +15115,11 @@ function registerGhostRegistryCommand(parent) {
14679
15115
  addOutputOptions(listCmd);
14680
15116
  listCmd.action(async (options2) => {
14681
15117
  try {
14682
- await ensureGhostInitialized();
14683
- const config = await loadGhostConfig();
15118
+ const manager = await ensureProfilesInitialized();
15119
+ const profileName = await manager.getCurrent();
15120
+ const profile = await manager.get(profileName);
14684
15121
  const result = runRegistryListCore({
14685
- getRegistries: () => config.registries
15122
+ getRegistries: () => profile.ghost.registries
14686
15123
  });
14687
15124
  if (options2.json) {
14688
15125
  outputJson({ success: true, data: result });
@@ -14855,12 +15292,14 @@ function registerGhostCommand(program2) {
14855
15292
  registerGhostAddCommand(ghost);
14856
15293
  registerGhostSearchCommand(ghost);
14857
15294
  registerGhostOpenCodeCommand(ghost);
15295
+ registerGhostProfileCommand(ghost);
15296
+ registerGhostMigrateCommand(ghost);
14858
15297
  }
14859
15298
 
14860
15299
  // src/commands/init.ts
14861
15300
  import { existsSync as existsSync2 } from "fs";
14862
- import { cp, mkdir as mkdir5, readdir as readdir2, readFile, rm as rm2, writeFile as writeFile3 } from "fs/promises";
14863
- import { join as join7 } from "path";
15301
+ import { cp, mkdir as mkdir5, readdir as readdir4, readFile, rm as rm3, writeFile as writeFile2 } from "fs/promises";
15302
+ import { join as join6 } from "path";
14864
15303
  var TEMPLATE_REPO = "kdcokenny/ocx";
14865
15304
  var TEMPLATE_PATH = "examples/registry-starter";
14866
15305
  function registerInitCommand(program2) {
@@ -14878,7 +15317,7 @@ function registerInitCommand(program2) {
14878
15317
  }
14879
15318
  async function runInit(options2) {
14880
15319
  const cwd = options2.cwd ?? process.cwd();
14881
- const configPath = join7(cwd, "ocx.jsonc");
15320
+ const configPath = join6(cwd, "ocx.jsonc");
14882
15321
  if (existsSync2(configPath)) {
14883
15322
  throw new ConflictError(`ocx.jsonc already exists at ${configPath}
14884
15323
 
@@ -14894,7 +15333,7 @@ async function runInit(options2) {
14894
15333
  };
14895
15334
  const config = ocxConfigSchema.parse(rawConfig);
14896
15335
  const content2 = JSON.stringify(config, null, 2);
14897
- await writeFile3(configPath, content2, "utf-8");
15336
+ await writeFile2(configPath, content2, "utf-8");
14898
15337
  const opencodeResult = await ensureOpencodeConfig(cwd);
14899
15338
  spin?.succeed("Initialized OCX configuration");
14900
15339
  if (options2.json) {
@@ -14926,7 +15365,7 @@ async function runInitRegistry(directory, options2) {
14926
15365
  if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(namespace)) {
14927
15366
  throw new ValidationError("Invalid namespace format: must start with letter/number, use hyphens only between segments (e.g., 'my-registry')");
14928
15367
  }
14929
- const existingFiles = await readdir2(cwd).catch(() => []);
15368
+ const existingFiles = await readdir4(cwd).catch(() => []);
14930
15369
  const hasVisibleFiles = existingFiles.some((f) => !f.startsWith("."));
14931
15370
  if (hasVisibleFiles && !options2.force) {
14932
15371
  throw new ConflictError("Directory is not empty. Use --force to overwrite existing files.");
@@ -14985,12 +15424,12 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
14985
15424
  if (!response.ok || !response.body) {
14986
15425
  throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
14987
15426
  }
14988
- const tempDir = join7(destDir, ".ocx-temp");
15427
+ const tempDir = join6(destDir, ".ocx-temp");
14989
15428
  await mkdir5(tempDir, { recursive: true });
14990
15429
  try {
14991
- const tarPath = join7(tempDir, "template.tar.gz");
15430
+ const tarPath = join6(tempDir, "template.tar.gz");
14992
15431
  const arrayBuffer = await response.arrayBuffer();
14993
- await writeFile3(tarPath, Buffer.from(arrayBuffer));
15432
+ await writeFile2(tarPath, Buffer.from(arrayBuffer));
14994
15433
  const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
14995
15434
  stdout: "ignore",
14996
15435
  stderr: "pipe"
@@ -15000,15 +15439,15 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
15000
15439
  const stderr = await new Response(proc.stderr).text();
15001
15440
  throw new Error(`Failed to extract template: ${stderr}`);
15002
15441
  }
15003
- const extractedDirs = await readdir2(tempDir);
15442
+ const extractedDirs = await readdir4(tempDir);
15004
15443
  const extractedDir = extractedDirs.find((d) => d.startsWith("ocx-"));
15005
15444
  if (!extractedDir) {
15006
15445
  throw new Error("Failed to find extracted template directory");
15007
15446
  }
15008
- const templateDir = join7(tempDir, extractedDir, TEMPLATE_PATH);
15447
+ const templateDir = join6(tempDir, extractedDir, TEMPLATE_PATH);
15009
15448
  await copyDir(templateDir, destDir);
15010
15449
  } finally {
15011
- await rm2(tempDir, { recursive: true, force: true });
15450
+ await rm3(tempDir, { recursive: true, force: true });
15012
15451
  }
15013
15452
  }
15014
15453
  async function replacePlaceholders(dir, values) {
@@ -15020,21 +15459,21 @@ async function replacePlaceholders(dir, values) {
15020
15459
  "AGENTS.md"
15021
15460
  ];
15022
15461
  for (const file of filesToProcess) {
15023
- const filePath = join7(dir, file);
15462
+ const filePath = join6(dir, file);
15024
15463
  if (!existsSync2(filePath))
15025
15464
  continue;
15026
15465
  let content2 = await readFile(filePath).then((b) => b.toString());
15027
15466
  content2 = content2.replace(/my-registry/g, values.namespace);
15028
15467
  content2 = content2.replace(/Your Name/g, values.author);
15029
- await writeFile3(filePath, content2);
15468
+ await writeFile2(filePath, content2);
15030
15469
  }
15031
15470
  }
15032
15471
 
15033
15472
  // src/commands/update.ts
15034
15473
  import { createHash as createHash2 } from "crypto";
15035
15474
  import { existsSync as existsSync3 } from "fs";
15036
- import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
15037
- import { dirname as dirname5, join as join8 } from "path";
15475
+ import { mkdir as mkdir6, writeFile as writeFile3 } from "fs/promises";
15476
+ import { dirname as dirname5, join as join7 } from "path";
15038
15477
  function registerUpdateCommand(program2) {
15039
15478
  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) => {
15040
15479
  try {
@@ -15046,7 +15485,7 @@ function registerUpdateCommand(program2) {
15046
15485
  }
15047
15486
  async function runUpdate(componentNames, options2) {
15048
15487
  const cwd = options2.cwd ?? process.cwd();
15049
- const lockPath = join8(cwd, "ocx.lock");
15488
+ const lockPath = join7(cwd, "ocx.lock");
15050
15489
  const config = await readOcxConfig(cwd);
15051
15490
  if (!config) {
15052
15491
  throw new ConfigError("No ocx.jsonc found. Run 'ocx init' first.");
@@ -15171,12 +15610,12 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
15171
15610
  const fileObj = update.component.files.find((f) => f.path === file.path);
15172
15611
  if (!fileObj)
15173
15612
  continue;
15174
- const targetPath = join8(cwd, fileObj.target);
15613
+ const targetPath = join7(cwd, fileObj.target);
15175
15614
  const targetDir = dirname5(targetPath);
15176
15615
  if (!existsSync3(targetDir)) {
15177
15616
  await mkdir6(targetDir, { recursive: true });
15178
15617
  }
15179
- await writeFile4(targetPath, file.content);
15618
+ await writeFile3(targetPath, file.content);
15180
15619
  if (options2.verbose) {
15181
15620
  logger.info(` \u2713 Updated ${fileObj.target}`);
15182
15621
  }
@@ -15195,7 +15634,7 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
15195
15634
  };
15196
15635
  }
15197
15636
  installSpin?.succeed(`Updated ${updates.length} component(s)`);
15198
- await writeFile4(lockPath, JSON.stringify(lock, null, 2), "utf-8");
15637
+ await writeFile3(lockPath, JSON.stringify(lock, null, 2), "utf-8");
15199
15638
  if (options2.json) {
15200
15639
  console.log(JSON.stringify({
15201
15640
  success: true,
@@ -15309,7 +15748,7 @@ async function hashBundle2(files) {
15309
15748
  `));
15310
15749
  }
15311
15750
  // src/index.ts
15312
- var version = "1.1.0";
15751
+ var version = "1.2.0";
15313
15752
  async function main2() {
15314
15753
  const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
15315
15754
  registerInitCommand(program2);
@@ -15336,4 +15775,4 @@ export {
15336
15775
  buildRegistry
15337
15776
  };
15338
15777
 
15339
- //# debugId=46A2A0DCEF36C6A464756E2164756E21
15778
+ //# debugId=4A36829C127A179064756E2164756E21