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