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 +713 -274
- package/dist/index.js.map +24 -12
- package/package.json +1 -1
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
|
|
4514
|
+
import { dirname, join } from "path";
|
|
4515
4515
|
|
|
4516
|
-
// src/
|
|
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
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
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
|
|
10288
|
-
constructor(
|
|
10289
|
-
|
|
10290
|
-
|
|
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
|
|
10300
|
-
constructor(
|
|
10301
|
-
super(
|
|
10302
|
-
this.name = "
|
|
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
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
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
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
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
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
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
|
|
10333
|
-
const
|
|
10334
|
-
|
|
10335
|
-
|
|
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
|
-
|
|
10340
|
-
|
|
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
|
-
|
|
10343
|
-
|
|
10344
|
-
const file = Bun.file(configPath);
|
|
10345
|
-
return file.exists();
|
|
10309
|
+
function getProfileDir(name) {
|
|
10310
|
+
return path2.join(getProfilesDir(), name);
|
|
10346
10311
|
}
|
|
10347
|
-
|
|
10348
|
-
|
|
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
|
-
|
|
10357
|
-
|
|
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
|
-
|
|
10369
|
-
|
|
10370
|
-
return join(getGhostConfigDir(), OPENCODE_CONFIG_FILE_NAME);
|
|
10318
|
+
function getProfileAgents(name) {
|
|
10319
|
+
return path2.join(getProfileDir(name), "AGENTS.md");
|
|
10371
10320
|
}
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
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
|
-
|
|
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
|
|
10420
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
12999
|
+
logger.info(`Created ${join(cwd, "opencode.jsonc")}`);
|
|
12904
13000
|
} else {
|
|
12905
|
-
logger.info(`Updated ${
|
|
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 ${
|
|
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 =
|
|
12952
|
-
const targetDir =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
13112
|
-
const jsonFile = Bun.file(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
13145
|
-
const destFilePath =
|
|
13146
|
-
const destFileDir =
|
|
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(
|
|
13173
|
-
const wellKnownDir =
|
|
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(
|
|
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 =
|
|
13191
|
-
const outPath =
|
|
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
|
|
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
|
|
14157
|
-
if (!
|
|
14158
|
-
throw new
|
|
14252
|
+
const manager = ProfileManager.create();
|
|
14253
|
+
if (!await manager.isInitialized()) {
|
|
14254
|
+
throw new ProfilesNotInitializedError;
|
|
14159
14255
|
}
|
|
14160
|
-
const
|
|
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
|
|
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
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
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
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
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 ${
|
|
14235
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
14295
|
-
excluded.add(
|
|
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
|
|
14301
|
-
excluded.add(
|
|
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
|
|
14323
|
-
const matchesInclude = matchesAnyGlob(
|
|
14324
|
-
const matchesExclude = matchesAnyGlob(
|
|
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(
|
|
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
|
|
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 =
|
|
14349
|
-
await Bun.write(
|
|
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
|
|
14561
|
+
const entries = await readdir3(sourceDir, { withFileTypes: true });
|
|
14352
14562
|
for (const entry of entries) {
|
|
14353
|
-
const sourcePath =
|
|
14563
|
+
const sourcePath = join5(sourceDir, entry.name);
|
|
14354
14564
|
if (excludePaths.has(sourcePath))
|
|
14355
14565
|
continue;
|
|
14356
|
-
const targetPath =
|
|
14357
|
-
await
|
|
14566
|
+
const targetPath = join5(tempDir, entry.name);
|
|
14567
|
+
await symlink2(sourcePath, targetPath);
|
|
14358
14568
|
}
|
|
14359
14569
|
return tempDir;
|
|
14360
14570
|
} catch (error) {
|
|
14361
|
-
await
|
|
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
|
|
14604
|
+
await rename3(tempDir, removingPath);
|
|
14369
14605
|
} catch {
|
|
14370
14606
|
return;
|
|
14371
14607
|
}
|
|
14372
|
-
await
|
|
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
|
|
14617
|
+
dirNames = await readdir3(tempBase);
|
|
14382
14618
|
} catch {
|
|
14383
14619
|
return 0;
|
|
14384
14620
|
}
|
|
14385
14621
|
for (const dirName of dirNames) {
|
|
14386
|
-
const dirPath =
|
|
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
|
|
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
|
|
14407
|
-
await
|
|
14642
|
+
await rename3(dirPath, removingPath);
|
|
14643
|
+
await rm2(removingPath, { recursive: true, force: true });
|
|
14408
14644
|
} else {
|
|
14409
|
-
await
|
|
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
|
-
|
|
14430
|
-
|
|
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
|
|
14434
|
-
const
|
|
14435
|
-
|
|
14436
|
-
|
|
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 =
|
|
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(
|
|
14473
|
-
OPENCODE_CONFIG_DIR:
|
|
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
|
|
14623
|
-
const
|
|
14624
|
-
if (!
|
|
14625
|
-
throw new
|
|
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
|
|
14635
|
-
const
|
|
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: () =>
|
|
15072
|
+
getRegistries: () => profile.ghost.registries,
|
|
14638
15073
|
setRegistry: async (name, regConfig) => {
|
|
14639
|
-
|
|
14640
|
-
await
|
|
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
|
|
14661
|
-
const
|
|
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: () =>
|
|
15099
|
+
getRegistries: () => profile.ghost.registries,
|
|
14664
15100
|
removeRegistry: async (regName) => {
|
|
14665
|
-
delete
|
|
14666
|
-
await
|
|
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
|
|
14683
|
-
const
|
|
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: () =>
|
|
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
|
|
14863
|
-
import { join as
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
15427
|
+
const tempDir = join6(destDir, ".ocx-temp");
|
|
14989
15428
|
await mkdir5(tempDir, { recursive: true });
|
|
14990
15429
|
try {
|
|
14991
|
-
const tarPath =
|
|
15430
|
+
const tarPath = join6(tempDir, "template.tar.gz");
|
|
14992
15431
|
const arrayBuffer = await response.arrayBuffer();
|
|
14993
|
-
await
|
|
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
|
|
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 =
|
|
15447
|
+
const templateDir = join6(tempDir, extractedDir, TEMPLATE_PATH);
|
|
15009
15448
|
await copyDir(templateDir, destDir);
|
|
15010
15449
|
} finally {
|
|
15011
|
-
await
|
|
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 =
|
|
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
|
|
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
|
|
15037
|
-
import { dirname as dirname5, join as
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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=
|
|
15778
|
+
//# debugId=4A36829C127A179064756E2164756E21
|