ocx 1.1.1 → 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 +695 -281
- package/dist/index.js.map +23 -11
- 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;
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
async list() {
|
|
10362
|
+
await this.ensureInitialized();
|
|
10363
|
+
const entries = await readdir(this.profilesDir, { withFileTypes: true });
|
|
10364
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "current").map((e) => e.name).sort();
|
|
10365
|
+
}
|
|
10366
|
+
async exists(name) {
|
|
10367
|
+
const dir = getProfileDir(name);
|
|
10368
|
+
try {
|
|
10369
|
+
const stats = await stat(dir);
|
|
10370
|
+
return stats.isDirectory();
|
|
10371
|
+
} catch {
|
|
10372
|
+
return false;
|
|
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);
|
|
10380
10468
|
}
|
|
10381
|
-
|
|
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,22 +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 - add your registries here
|
|
14188
|
-
// Example: "myregistry": { "url": "https://example.com/registry" }
|
|
14189
|
-
"registries": {},
|
|
14190
|
-
|
|
14191
|
-
// Where to install components (relative to project root)
|
|
14192
|
-
"componentPath": "src/components"
|
|
14193
|
-
}
|
|
14194
|
-
`;
|
|
14195
14279
|
function registerGhostInitCommand(parent) {
|
|
14196
|
-
const cmd = parent.command("init").description("Initialize ghost mode with
|
|
14280
|
+
const cmd = parent.command("init").description("Initialize ghost mode with profiles");
|
|
14197
14281
|
addOutputOptions(cmd);
|
|
14198
14282
|
addVerboseOption(cmd);
|
|
14199
14283
|
cmd.action(async (options2) => {
|
|
@@ -14205,47 +14289,176 @@ function registerGhostInitCommand(parent) {
|
|
|
14205
14289
|
});
|
|
14206
14290
|
}
|
|
14207
14291
|
async function runGhostInit(options2) {
|
|
14208
|
-
const
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
}
|
|
14217
|
-
throw err;
|
|
14218
|
-
}
|
|
14219
|
-
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");
|
|
14220
14300
|
if (options2.json) {
|
|
14221
14301
|
console.log(JSON.stringify({
|
|
14222
14302
|
success: true,
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14303
|
+
profilesDir,
|
|
14304
|
+
defaultProfile: "default",
|
|
14305
|
+
ghostConfigPath
|
|
14226
14306
|
}));
|
|
14227
14307
|
return;
|
|
14228
14308
|
}
|
|
14229
14309
|
if (!options2.quiet) {
|
|
14230
14310
|
logger.success("Ghost mode initialized");
|
|
14231
|
-
logger.info(`Created ${
|
|
14232
|
-
|
|
14233
|
-
logger.info(`Created ${opencodeResult.path}`);
|
|
14234
|
-
}
|
|
14311
|
+
logger.info(`Created ${profilesDir}`);
|
|
14312
|
+
logger.info(`Created profile "default"`);
|
|
14235
14313
|
logger.info("");
|
|
14236
14314
|
logger.info("Next steps:");
|
|
14237
14315
|
logger.info(" 1. Edit your config: ocx ghost config");
|
|
14238
14316
|
logger.info(" 2. Add registries: ocx ghost registry add <url> --name <name>");
|
|
14239
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")}`);
|
|
14240
14451
|
}
|
|
14241
14452
|
}
|
|
14242
14453
|
|
|
14243
14454
|
// src/commands/ghost/opencode.ts
|
|
14244
14455
|
import { renameSync, rmSync } from "fs";
|
|
14456
|
+
import { copyFile as copyFilePromise } from "fs/promises";
|
|
14457
|
+
import path6 from "path";
|
|
14245
14458
|
|
|
14246
14459
|
// src/utils/opencode-discovery.ts
|
|
14247
14460
|
import { exists } from "fs/promises";
|
|
14248
|
-
import { dirname as
|
|
14461
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
14249
14462
|
var CONFIG_FILES = ["opencode.jsonc", "opencode.json"];
|
|
14250
14463
|
var RULE_FILES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
|
|
14251
14464
|
var CONFIG_DIRS = [".opencode"];
|
|
@@ -14253,13 +14466,13 @@ async function findUp(target, start, stop) {
|
|
|
14253
14466
|
let current = start;
|
|
14254
14467
|
const result = [];
|
|
14255
14468
|
while (true) {
|
|
14256
|
-
const search =
|
|
14469
|
+
const search = join4(current, target);
|
|
14257
14470
|
if (await exists(search).catch(() => false)) {
|
|
14258
14471
|
result.push(search);
|
|
14259
14472
|
}
|
|
14260
14473
|
if (stop === current)
|
|
14261
14474
|
break;
|
|
14262
|
-
const parent =
|
|
14475
|
+
const parent = dirname3(current);
|
|
14263
14476
|
if (parent === current)
|
|
14264
14477
|
break;
|
|
14265
14478
|
current = parent;
|
|
@@ -14271,14 +14484,14 @@ async function* up(options2) {
|
|
|
14271
14484
|
let current = start;
|
|
14272
14485
|
while (true) {
|
|
14273
14486
|
for (const target of targets) {
|
|
14274
|
-
const search =
|
|
14487
|
+
const search = join4(current, target);
|
|
14275
14488
|
if (await exists(search).catch(() => false)) {
|
|
14276
14489
|
yield search;
|
|
14277
14490
|
}
|
|
14278
14491
|
}
|
|
14279
14492
|
if (stop === current)
|
|
14280
14493
|
break;
|
|
14281
|
-
const parent =
|
|
14494
|
+
const parent = dirname3(current);
|
|
14282
14495
|
if (parent === current)
|
|
14283
14496
|
break;
|
|
14284
14497
|
current = parent;
|
|
@@ -14288,14 +14501,14 @@ async function discoverProjectFiles(start, stop) {
|
|
|
14288
14501
|
const excluded = new Set;
|
|
14289
14502
|
for (const file of CONFIG_FILES) {
|
|
14290
14503
|
const found = await findUp(file, start, stop);
|
|
14291
|
-
for (const
|
|
14292
|
-
excluded.add(
|
|
14504
|
+
for (const path6 of found) {
|
|
14505
|
+
excluded.add(path6);
|
|
14293
14506
|
}
|
|
14294
14507
|
}
|
|
14295
14508
|
for (const file of RULE_FILES) {
|
|
14296
14509
|
const found = await findUp(file, start, stop);
|
|
14297
|
-
for (const
|
|
14298
|
-
excluded.add(
|
|
14510
|
+
for (const path6 of found) {
|
|
14511
|
+
excluded.add(path6);
|
|
14299
14512
|
}
|
|
14300
14513
|
}
|
|
14301
14514
|
for await (const dir of up({ targets: CONFIG_DIRS, start, stop })) {
|
|
@@ -14316,22 +14529,22 @@ function filterExcludedPaths(excludedPaths, includePatterns, excludePatterns) {
|
|
|
14316
14529
|
const includeGlobs = includePatterns.map((p) => new Glob2(p));
|
|
14317
14530
|
const excludeGlobs = excludePatterns?.map((p) => new Glob2(p)) ?? [];
|
|
14318
14531
|
const filteredExclusions = new Set;
|
|
14319
|
-
for (const
|
|
14320
|
-
const matchesInclude = matchesAnyGlob(
|
|
14321
|
-
const matchesExclude = matchesAnyGlob(
|
|
14532
|
+
for (const path6 of excludedPaths) {
|
|
14533
|
+
const matchesInclude = matchesAnyGlob(path6, includeGlobs);
|
|
14534
|
+
const matchesExclude = matchesAnyGlob(path6, excludeGlobs);
|
|
14322
14535
|
if (matchesInclude && !matchesExclude) {
|
|
14323
14536
|
continue;
|
|
14324
14537
|
}
|
|
14325
|
-
filteredExclusions.add(
|
|
14538
|
+
filteredExclusions.add(path6);
|
|
14326
14539
|
}
|
|
14327
14540
|
return filteredExclusions;
|
|
14328
14541
|
}
|
|
14329
14542
|
|
|
14330
14543
|
// src/utils/symlink-farm.ts
|
|
14331
14544
|
import { randomBytes } from "crypto";
|
|
14332
|
-
import { mkdir as
|
|
14545
|
+
import { mkdir as mkdir4, readdir as readdir3, rename as rename3, rm as rm2, stat as stat3, symlink as symlink2 } from "fs/promises";
|
|
14333
14546
|
import { tmpdir } from "os";
|
|
14334
|
-
import { dirname as
|
|
14547
|
+
import { dirname as dirname4, isAbsolute, join as join5, relative as relative2 } from "path";
|
|
14335
14548
|
var STALE_SESSION_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
14336
14549
|
var REMOVING_THRESHOLD_MS = 60 * 60 * 1000;
|
|
14337
14550
|
var GHOST_DIR_PREFIX = "ocx-ghost-";
|
|
@@ -14342,20 +14555,20 @@ async function createSymlinkFarm(sourceDir, excludePaths) {
|
|
|
14342
14555
|
throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
|
|
14343
14556
|
}
|
|
14344
14557
|
const suffix = randomBytes(4).toString("hex");
|
|
14345
|
-
const tempDir =
|
|
14346
|
-
await Bun.write(
|
|
14558
|
+
const tempDir = join5(tmpdir(), `${GHOST_DIR_PREFIX}${suffix}`);
|
|
14559
|
+
await Bun.write(join5(tempDir, GHOST_MARKER_FILE), "");
|
|
14347
14560
|
try {
|
|
14348
|
-
const entries = await
|
|
14561
|
+
const entries = await readdir3(sourceDir, { withFileTypes: true });
|
|
14349
14562
|
for (const entry of entries) {
|
|
14350
|
-
const sourcePath =
|
|
14563
|
+
const sourcePath = join5(sourceDir, entry.name);
|
|
14351
14564
|
if (excludePaths.has(sourcePath))
|
|
14352
14565
|
continue;
|
|
14353
|
-
const targetPath =
|
|
14354
|
-
await
|
|
14566
|
+
const targetPath = join5(tempDir, entry.name);
|
|
14567
|
+
await symlink2(sourcePath, targetPath);
|
|
14355
14568
|
}
|
|
14356
14569
|
return tempDir;
|
|
14357
14570
|
} catch (error) {
|
|
14358
|
-
await
|
|
14571
|
+
await rm2(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
14359
14572
|
throw error;
|
|
14360
14573
|
}
|
|
14361
14574
|
}
|
|
@@ -14371,13 +14584,13 @@ async function injectGhostFiles(tempDir, sourceDir, injectPaths) {
|
|
|
14371
14584
|
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
14372
14585
|
throw new Error(`injectPath must be within sourceDir: ${injectPath}`);
|
|
14373
14586
|
}
|
|
14374
|
-
const targetPath =
|
|
14375
|
-
const parentDir =
|
|
14587
|
+
const targetPath = join5(tempDir, relativePath);
|
|
14588
|
+
const parentDir = dirname4(targetPath);
|
|
14376
14589
|
if (parentDir !== tempDir) {
|
|
14377
|
-
await
|
|
14590
|
+
await mkdir4(parentDir, { recursive: true });
|
|
14378
14591
|
}
|
|
14379
14592
|
try {
|
|
14380
|
-
await
|
|
14593
|
+
await symlink2(injectPath, targetPath);
|
|
14381
14594
|
} catch (err) {
|
|
14382
14595
|
if (err.code !== "EEXIST") {
|
|
14383
14596
|
throw new Error(`Failed to inject ${injectPath} \u2192 ${targetPath}: ${err.message}`);
|
|
@@ -14388,11 +14601,11 @@ async function injectGhostFiles(tempDir, sourceDir, injectPaths) {
|
|
|
14388
14601
|
async function cleanupSymlinkFarm(tempDir) {
|
|
14389
14602
|
const removingPath = `${tempDir}${REMOVING_SUFFIX}`;
|
|
14390
14603
|
try {
|
|
14391
|
-
await
|
|
14604
|
+
await rename3(tempDir, removingPath);
|
|
14392
14605
|
} catch {
|
|
14393
14606
|
return;
|
|
14394
14607
|
}
|
|
14395
|
-
await
|
|
14608
|
+
await rm2(removingPath, { recursive: true, force: true });
|
|
14396
14609
|
}
|
|
14397
14610
|
async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
|
|
14398
14611
|
let cleanedCount = 0;
|
|
@@ -14401,19 +14614,19 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
|
|
|
14401
14614
|
}
|
|
14402
14615
|
let dirNames;
|
|
14403
14616
|
try {
|
|
14404
|
-
dirNames = await
|
|
14617
|
+
dirNames = await readdir3(tempBase);
|
|
14405
14618
|
} catch {
|
|
14406
14619
|
return 0;
|
|
14407
14620
|
}
|
|
14408
14621
|
for (const dirName of dirNames) {
|
|
14409
|
-
const dirPath =
|
|
14622
|
+
const dirPath = join5(tempBase, dirName);
|
|
14410
14623
|
const isRemovingDir = dirName.endsWith(REMOVING_SUFFIX);
|
|
14411
14624
|
const isGhostDir = dirName.startsWith(GHOST_DIR_PREFIX) && !isRemovingDir;
|
|
14412
14625
|
if (!isRemovingDir && !isGhostDir)
|
|
14413
14626
|
continue;
|
|
14414
14627
|
let stats;
|
|
14415
14628
|
try {
|
|
14416
|
-
stats = await
|
|
14629
|
+
stats = await stat3(dirPath);
|
|
14417
14630
|
} catch {
|
|
14418
14631
|
continue;
|
|
14419
14632
|
}
|
|
@@ -14426,10 +14639,10 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
|
|
|
14426
14639
|
try {
|
|
14427
14640
|
if (isGhostDir) {
|
|
14428
14641
|
const removingPath = `${dirPath}${REMOVING_SUFFIX}`;
|
|
14429
|
-
await
|
|
14430
|
-
await
|
|
14642
|
+
await rename3(dirPath, removingPath);
|
|
14643
|
+
await rm2(removingPath, { recursive: true, force: true });
|
|
14431
14644
|
} else {
|
|
14432
|
-
await
|
|
14645
|
+
await rm2(dirPath, { recursive: true, force: true });
|
|
14433
14646
|
}
|
|
14434
14647
|
cleanedCount++;
|
|
14435
14648
|
} catch {}
|
|
@@ -14439,7 +14652,7 @@ async function cleanupOrphanedGhostDirs(tempBase = tmpdir()) {
|
|
|
14439
14652
|
|
|
14440
14653
|
// src/commands/ghost/opencode.ts
|
|
14441
14654
|
function registerGhostOpenCodeCommand(parent) {
|
|
14442
|
-
parent.command("opencode").description("Launch OpenCode with ghost mode configuration").addOption(sharedOptions.json()).addOption(sharedOptions.quiet()).allowUnknownOption().allowExcessArguments(true).action(async (options2, command) => {
|
|
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) => {
|
|
14443
14656
|
try {
|
|
14444
14657
|
const args = command.args;
|
|
14445
14658
|
await runGhostOpenCode(args, options2);
|
|
@@ -14449,24 +14662,39 @@ function registerGhostOpenCodeCommand(parent) {
|
|
|
14449
14662
|
});
|
|
14450
14663
|
}
|
|
14451
14664
|
async function runGhostOpenCode(args, options2) {
|
|
14452
|
-
|
|
14453
|
-
|
|
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
|
+
`);
|
|
14454
14673
|
}
|
|
14455
14674
|
await cleanupOrphanedGhostDirs();
|
|
14456
|
-
const
|
|
14457
|
-
const
|
|
14458
|
-
|
|
14459
|
-
|
|
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.`);
|
|
14460
14683
|
}
|
|
14461
14684
|
const cwd = process.cwd();
|
|
14462
14685
|
const gitContext = await detectGitRepo(cwd);
|
|
14463
14686
|
const gitRoot = gitContext?.workTree ?? cwd;
|
|
14464
14687
|
const discoveredPaths = await discoverProjectFiles(cwd, gitRoot);
|
|
14465
|
-
const ghostConfig =
|
|
14688
|
+
const ghostConfig = profile.ghost;
|
|
14466
14689
|
const excludePaths = filterExcludedPaths(discoveredPaths, ghostConfig.include, ghostConfig.exclude);
|
|
14467
14690
|
const tempDir = await createSymlinkFarm(cwd, excludePaths);
|
|
14468
|
-
const ghostFiles = await discoverProjectFiles(
|
|
14469
|
-
await injectGhostFiles(tempDir,
|
|
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
|
+
}
|
|
14470
14698
|
let cleanupDone = false;
|
|
14471
14699
|
const performCleanup = async () => {
|
|
14472
14700
|
if (cleanupDone)
|
|
@@ -14494,8 +14722,9 @@ async function runGhostOpenCode(args, options2) {
|
|
|
14494
14722
|
cwd: tempDir,
|
|
14495
14723
|
env: {
|
|
14496
14724
|
...process.env,
|
|
14497
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify(
|
|
14498
|
-
OPENCODE_CONFIG_DIR:
|
|
14725
|
+
...profile.opencode && { OPENCODE_CONFIG_CONTENT: JSON.stringify(profile.opencode) },
|
|
14726
|
+
OPENCODE_CONFIG_DIR: profileDir,
|
|
14727
|
+
OCX_PROFILE: profileName,
|
|
14499
14728
|
...gitContext && {
|
|
14500
14729
|
GIT_WORK_TREE: gitContext.workTree,
|
|
14501
14730
|
GIT_DIR: gitContext.gitDir
|
|
@@ -14516,6 +14745,185 @@ async function runGhostOpenCode(args, options2) {
|
|
|
14516
14745
|
}
|
|
14517
14746
|
}
|
|
14518
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
|
+
|
|
14519
14927
|
// src/commands/registry.ts
|
|
14520
14928
|
async function runRegistryAddCore2(url, options2, callbacks) {
|
|
14521
14929
|
if (callbacks.isLocked?.()) {
|
|
@@ -14644,11 +15052,12 @@ function registerRegistryCommand(program2) {
|
|
|
14644
15052
|
}
|
|
14645
15053
|
|
|
14646
15054
|
// src/commands/ghost/registry.ts
|
|
14647
|
-
async function
|
|
14648
|
-
const
|
|
14649
|
-
if (!
|
|
14650
|
-
throw new
|
|
15055
|
+
async function ensureProfilesInitialized() {
|
|
15056
|
+
const manager = ProfileManager.create();
|
|
15057
|
+
if (!await manager.isInitialized()) {
|
|
15058
|
+
throw new ProfilesNotInitializedError;
|
|
14651
15059
|
}
|
|
15060
|
+
return manager;
|
|
14652
15061
|
}
|
|
14653
15062
|
function registerGhostRegistryCommand(parent) {
|
|
14654
15063
|
const registry = parent.command("registry").description("Manage ghost mode registries");
|
|
@@ -14656,13 +15065,14 @@ function registerGhostRegistryCommand(parent) {
|
|
|
14656
15065
|
addOutputOptions(addCmd);
|
|
14657
15066
|
addCmd.action(async (url, options2) => {
|
|
14658
15067
|
try {
|
|
14659
|
-
await
|
|
14660
|
-
const
|
|
15068
|
+
const manager = await ensureProfilesInitialized();
|
|
15069
|
+
const profileName = await manager.getCurrent();
|
|
15070
|
+
const profile = await manager.get(profileName);
|
|
14661
15071
|
const result = await runRegistryAddCore2(url, options2, {
|
|
14662
|
-
getRegistries: () =>
|
|
15072
|
+
getRegistries: () => profile.ghost.registries,
|
|
14663
15073
|
setRegistry: async (name, regConfig) => {
|
|
14664
|
-
|
|
14665
|
-
await
|
|
15074
|
+
profile.ghost.registries[name] = regConfig;
|
|
15075
|
+
await atomicWrite(getProfileGhostConfig(profileName), profile.ghost);
|
|
14666
15076
|
}
|
|
14667
15077
|
});
|
|
14668
15078
|
if (options2.json) {
|
|
@@ -14682,13 +15092,14 @@ function registerGhostRegistryCommand(parent) {
|
|
|
14682
15092
|
addOutputOptions(removeCmd);
|
|
14683
15093
|
removeCmd.action(async (name, options2) => {
|
|
14684
15094
|
try {
|
|
14685
|
-
await
|
|
14686
|
-
const
|
|
15095
|
+
const manager = await ensureProfilesInitialized();
|
|
15096
|
+
const profileName = await manager.getCurrent();
|
|
15097
|
+
const profile = await manager.get(profileName);
|
|
14687
15098
|
const result = await runRegistryRemoveCore(name, {
|
|
14688
|
-
getRegistries: () =>
|
|
15099
|
+
getRegistries: () => profile.ghost.registries,
|
|
14689
15100
|
removeRegistry: async (regName) => {
|
|
14690
|
-
delete
|
|
14691
|
-
await
|
|
15101
|
+
delete profile.ghost.registries[regName];
|
|
15102
|
+
await atomicWrite(getProfileGhostConfig(profileName), profile.ghost);
|
|
14692
15103
|
}
|
|
14693
15104
|
});
|
|
14694
15105
|
if (options2.json) {
|
|
@@ -14704,10 +15115,11 @@ function registerGhostRegistryCommand(parent) {
|
|
|
14704
15115
|
addOutputOptions(listCmd);
|
|
14705
15116
|
listCmd.action(async (options2) => {
|
|
14706
15117
|
try {
|
|
14707
|
-
await
|
|
14708
|
-
const
|
|
15118
|
+
const manager = await ensureProfilesInitialized();
|
|
15119
|
+
const profileName = await manager.getCurrent();
|
|
15120
|
+
const profile = await manager.get(profileName);
|
|
14709
15121
|
const result = runRegistryListCore({
|
|
14710
|
-
getRegistries: () =>
|
|
15122
|
+
getRegistries: () => profile.ghost.registries
|
|
14711
15123
|
});
|
|
14712
15124
|
if (options2.json) {
|
|
14713
15125
|
outputJson({ success: true, data: result });
|
|
@@ -14880,12 +15292,14 @@ function registerGhostCommand(program2) {
|
|
|
14880
15292
|
registerGhostAddCommand(ghost);
|
|
14881
15293
|
registerGhostSearchCommand(ghost);
|
|
14882
15294
|
registerGhostOpenCodeCommand(ghost);
|
|
15295
|
+
registerGhostProfileCommand(ghost);
|
|
15296
|
+
registerGhostMigrateCommand(ghost);
|
|
14883
15297
|
}
|
|
14884
15298
|
|
|
14885
15299
|
// src/commands/init.ts
|
|
14886
15300
|
import { existsSync as existsSync2 } from "fs";
|
|
14887
|
-
import { cp, mkdir as
|
|
14888
|
-
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";
|
|
14889
15303
|
var TEMPLATE_REPO = "kdcokenny/ocx";
|
|
14890
15304
|
var TEMPLATE_PATH = "examples/registry-starter";
|
|
14891
15305
|
function registerInitCommand(program2) {
|
|
@@ -14903,7 +15317,7 @@ function registerInitCommand(program2) {
|
|
|
14903
15317
|
}
|
|
14904
15318
|
async function runInit(options2) {
|
|
14905
15319
|
const cwd = options2.cwd ?? process.cwd();
|
|
14906
|
-
const configPath =
|
|
15320
|
+
const configPath = join6(cwd, "ocx.jsonc");
|
|
14907
15321
|
if (existsSync2(configPath)) {
|
|
14908
15322
|
throw new ConflictError(`ocx.jsonc already exists at ${configPath}
|
|
14909
15323
|
|
|
@@ -14919,7 +15333,7 @@ async function runInit(options2) {
|
|
|
14919
15333
|
};
|
|
14920
15334
|
const config = ocxConfigSchema.parse(rawConfig);
|
|
14921
15335
|
const content2 = JSON.stringify(config, null, 2);
|
|
14922
|
-
await
|
|
15336
|
+
await writeFile2(configPath, content2, "utf-8");
|
|
14923
15337
|
const opencodeResult = await ensureOpencodeConfig(cwd);
|
|
14924
15338
|
spin?.succeed("Initialized OCX configuration");
|
|
14925
15339
|
if (options2.json) {
|
|
@@ -14951,7 +15365,7 @@ async function runInitRegistry(directory, options2) {
|
|
|
14951
15365
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(namespace)) {
|
|
14952
15366
|
throw new ValidationError("Invalid namespace format: must start with letter/number, use hyphens only between segments (e.g., 'my-registry')");
|
|
14953
15367
|
}
|
|
14954
|
-
const existingFiles = await
|
|
15368
|
+
const existingFiles = await readdir4(cwd).catch(() => []);
|
|
14955
15369
|
const hasVisibleFiles = existingFiles.some((f) => !f.startsWith("."));
|
|
14956
15370
|
if (hasVisibleFiles && !options2.force) {
|
|
14957
15371
|
throw new ConflictError("Directory is not empty. Use --force to overwrite existing files.");
|
|
@@ -14962,7 +15376,7 @@ async function runInitRegistry(directory, options2) {
|
|
|
14962
15376
|
if (spin)
|
|
14963
15377
|
spin.text = options2.local ? "Copying template..." : "Fetching template...";
|
|
14964
15378
|
if (options2.local) {
|
|
14965
|
-
await
|
|
15379
|
+
await mkdir5(cwd, { recursive: true });
|
|
14966
15380
|
await copyDir(options2.local, cwd);
|
|
14967
15381
|
} else {
|
|
14968
15382
|
const version = options2.canary ? "main" : await getLatestVersion();
|
|
@@ -15010,12 +15424,12 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
|
|
|
15010
15424
|
if (!response.ok || !response.body) {
|
|
15011
15425
|
throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
|
|
15012
15426
|
}
|
|
15013
|
-
const tempDir =
|
|
15014
|
-
await
|
|
15427
|
+
const tempDir = join6(destDir, ".ocx-temp");
|
|
15428
|
+
await mkdir5(tempDir, { recursive: true });
|
|
15015
15429
|
try {
|
|
15016
|
-
const tarPath =
|
|
15430
|
+
const tarPath = join6(tempDir, "template.tar.gz");
|
|
15017
15431
|
const arrayBuffer = await response.arrayBuffer();
|
|
15018
|
-
await
|
|
15432
|
+
await writeFile2(tarPath, Buffer.from(arrayBuffer));
|
|
15019
15433
|
const proc = Bun.spawn(["tar", "-xzf", tarPath, "-C", tempDir], {
|
|
15020
15434
|
stdout: "ignore",
|
|
15021
15435
|
stderr: "pipe"
|
|
@@ -15025,15 +15439,15 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
|
|
|
15025
15439
|
const stderr = await new Response(proc.stderr).text();
|
|
15026
15440
|
throw new Error(`Failed to extract template: ${stderr}`);
|
|
15027
15441
|
}
|
|
15028
|
-
const extractedDirs = await
|
|
15442
|
+
const extractedDirs = await readdir4(tempDir);
|
|
15029
15443
|
const extractedDir = extractedDirs.find((d) => d.startsWith("ocx-"));
|
|
15030
15444
|
if (!extractedDir) {
|
|
15031
15445
|
throw new Error("Failed to find extracted template directory");
|
|
15032
15446
|
}
|
|
15033
|
-
const templateDir =
|
|
15447
|
+
const templateDir = join6(tempDir, extractedDir, TEMPLATE_PATH);
|
|
15034
15448
|
await copyDir(templateDir, destDir);
|
|
15035
15449
|
} finally {
|
|
15036
|
-
await
|
|
15450
|
+
await rm3(tempDir, { recursive: true, force: true });
|
|
15037
15451
|
}
|
|
15038
15452
|
}
|
|
15039
15453
|
async function replacePlaceholders(dir, values) {
|
|
@@ -15045,21 +15459,21 @@ async function replacePlaceholders(dir, values) {
|
|
|
15045
15459
|
"AGENTS.md"
|
|
15046
15460
|
];
|
|
15047
15461
|
for (const file of filesToProcess) {
|
|
15048
|
-
const filePath =
|
|
15462
|
+
const filePath = join6(dir, file);
|
|
15049
15463
|
if (!existsSync2(filePath))
|
|
15050
15464
|
continue;
|
|
15051
15465
|
let content2 = await readFile(filePath).then((b) => b.toString());
|
|
15052
15466
|
content2 = content2.replace(/my-registry/g, values.namespace);
|
|
15053
15467
|
content2 = content2.replace(/Your Name/g, values.author);
|
|
15054
|
-
await
|
|
15468
|
+
await writeFile2(filePath, content2);
|
|
15055
15469
|
}
|
|
15056
15470
|
}
|
|
15057
15471
|
|
|
15058
15472
|
// src/commands/update.ts
|
|
15059
15473
|
import { createHash as createHash2 } from "crypto";
|
|
15060
15474
|
import { existsSync as existsSync3 } from "fs";
|
|
15061
|
-
import { mkdir as
|
|
15062
|
-
import { dirname as
|
|
15475
|
+
import { mkdir as mkdir6, writeFile as writeFile3 } from "fs/promises";
|
|
15476
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
15063
15477
|
function registerUpdateCommand(program2) {
|
|
15064
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) => {
|
|
15065
15479
|
try {
|
|
@@ -15071,7 +15485,7 @@ function registerUpdateCommand(program2) {
|
|
|
15071
15485
|
}
|
|
15072
15486
|
async function runUpdate(componentNames, options2) {
|
|
15073
15487
|
const cwd = options2.cwd ?? process.cwd();
|
|
15074
|
-
const lockPath =
|
|
15488
|
+
const lockPath = join7(cwd, "ocx.lock");
|
|
15075
15489
|
const config = await readOcxConfig(cwd);
|
|
15076
15490
|
if (!config) {
|
|
15077
15491
|
throw new ConfigError("No ocx.jsonc found. Run 'ocx init' first.");
|
|
@@ -15196,12 +15610,12 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
15196
15610
|
const fileObj = update.component.files.find((f) => f.path === file.path);
|
|
15197
15611
|
if (!fileObj)
|
|
15198
15612
|
continue;
|
|
15199
|
-
const targetPath =
|
|
15200
|
-
const targetDir =
|
|
15613
|
+
const targetPath = join7(cwd, fileObj.target);
|
|
15614
|
+
const targetDir = dirname5(targetPath);
|
|
15201
15615
|
if (!existsSync3(targetDir)) {
|
|
15202
|
-
await
|
|
15616
|
+
await mkdir6(targetDir, { recursive: true });
|
|
15203
15617
|
}
|
|
15204
|
-
await
|
|
15618
|
+
await writeFile3(targetPath, file.content);
|
|
15205
15619
|
if (options2.verbose) {
|
|
15206
15620
|
logger.info(` \u2713 Updated ${fileObj.target}`);
|
|
15207
15621
|
}
|
|
@@ -15220,7 +15634,7 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
15220
15634
|
};
|
|
15221
15635
|
}
|
|
15222
15636
|
installSpin?.succeed(`Updated ${updates.length} component(s)`);
|
|
15223
|
-
await
|
|
15637
|
+
await writeFile3(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
15224
15638
|
if (options2.json) {
|
|
15225
15639
|
console.log(JSON.stringify({
|
|
15226
15640
|
success: true,
|
|
@@ -15334,7 +15748,7 @@ async function hashBundle2(files) {
|
|
|
15334
15748
|
`));
|
|
15335
15749
|
}
|
|
15336
15750
|
// src/index.ts
|
|
15337
|
-
var version = "1.
|
|
15751
|
+
var version = "1.2.0";
|
|
15338
15752
|
async function main2() {
|
|
15339
15753
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
15340
15754
|
registerInitCommand(program2);
|
|
@@ -15361,4 +15775,4 @@ export {
|
|
|
15361
15775
|
buildRegistry
|
|
15362
15776
|
};
|
|
15363
15777
|
|
|
15364
|
-
//# debugId=
|
|
15778
|
+
//# debugId=4A36829C127A179064756E2164756E21
|