ocx 1.0.21 → 1.1.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 +310 -32
- package/dist/index.js.map +11 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9864,6 +9864,18 @@ function isAbsolutePath(p) {
|
|
|
9864
9864
|
var safeRelativePathSchema = exports_external.string().refine((val) => !val.includes("\x00"), "Path cannot contain null bytes").refine((val) => !val.split(/[/\\]/).some((seg) => seg === ".."), "Path cannot contain '..'").refine((val) => !isAbsolutePath(val), "Path must be relative, not absolute");
|
|
9865
9865
|
|
|
9866
9866
|
// src/schemas/registry.ts
|
|
9867
|
+
var npmSpecifierSchema = exports_external.string().refine((val) => val.startsWith("npm:"), {
|
|
9868
|
+
message: 'npm specifier must start with "npm:" prefix'
|
|
9869
|
+
}).refine((val) => {
|
|
9870
|
+
const remainder = val.slice(4);
|
|
9871
|
+
if (!remainder)
|
|
9872
|
+
return false;
|
|
9873
|
+
if (remainder.includes("..") || remainder.includes("/./"))
|
|
9874
|
+
return false;
|
|
9875
|
+
return true;
|
|
9876
|
+
}, {
|
|
9877
|
+
message: "Invalid npm specifier format"
|
|
9878
|
+
});
|
|
9867
9879
|
var openCodeNameSchema = exports_external.string().min(1, "Name cannot be empty").max(64, "Name cannot exceed 64 characters").regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, {
|
|
9868
9880
|
message: "Must be lowercase alphanumeric with single hyphen separators (e.g., 'my-component', 'my-plugin'). Cannot start/end with hyphen or have consecutive hyphens."
|
|
9869
9881
|
});
|
|
@@ -10420,7 +10432,7 @@ class GhostConfigProvider {
|
|
|
10420
10432
|
// package.json
|
|
10421
10433
|
var package_default = {
|
|
10422
10434
|
name: "ocx",
|
|
10423
|
-
version: "1.
|
|
10435
|
+
version: "1.1.1",
|
|
10424
10436
|
description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
|
|
10425
10437
|
author: "kdcokenny",
|
|
10426
10438
|
license: "MIT",
|
|
@@ -11082,14 +11094,14 @@ var sharedOptions = {
|
|
|
11082
11094
|
cwd: () => new Option("--cwd <path>", "Working directory").default(process.cwd()),
|
|
11083
11095
|
quiet: () => new Option("-q, --quiet", "Suppress output"),
|
|
11084
11096
|
json: () => new Option("--json", "Output as JSON"),
|
|
11085
|
-
|
|
11097
|
+
force: () => new Option("-f, --force", "Skip confirmation prompts"),
|
|
11086
11098
|
verbose: () => new Option("-v, --verbose", "Verbose output")
|
|
11087
11099
|
};
|
|
11088
11100
|
function addCommonOptions(cmd) {
|
|
11089
11101
|
return cmd.addOption(sharedOptions.cwd()).addOption(sharedOptions.quiet()).addOption(sharedOptions.json());
|
|
11090
11102
|
}
|
|
11091
|
-
function
|
|
11092
|
-
return cmd.addOption(sharedOptions.
|
|
11103
|
+
function addForceOption(cmd) {
|
|
11104
|
+
return cmd.addOption(sharedOptions.force());
|
|
11093
11105
|
}
|
|
11094
11106
|
function addVerboseOption(cmd) {
|
|
11095
11107
|
return cmd.addOption(sharedOptions.verbose());
|
|
@@ -12486,11 +12498,154 @@ function warnCompatIssues(registryName, issues) {
|
|
|
12486
12498
|
logger.log(formatCompatWarning(registryName, issue));
|
|
12487
12499
|
}
|
|
12488
12500
|
}
|
|
12501
|
+
// src/utils/npm-registry.ts
|
|
12502
|
+
var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
|
|
12503
|
+
var NPM_FETCH_TIMEOUT_MS = 30000;
|
|
12504
|
+
var NPM_NAME_REGEX = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/;
|
|
12505
|
+
var MAX_NAME_LENGTH = 214;
|
|
12506
|
+
function validateNpmPackageName(name) {
|
|
12507
|
+
if (!name) {
|
|
12508
|
+
throw new ValidationError("npm package name cannot be empty");
|
|
12509
|
+
}
|
|
12510
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
12511
|
+
throw new ValidationError(`npm package name exceeds maximum length of ${MAX_NAME_LENGTH} characters: \`${name}\``);
|
|
12512
|
+
}
|
|
12513
|
+
if (name.includes("..") || name.includes("/./") || name.startsWith("./")) {
|
|
12514
|
+
throw new ValidationError(`Invalid npm package name - path traversal detected: \`${name}\``);
|
|
12515
|
+
}
|
|
12516
|
+
if (!NPM_NAME_REGEX.test(name)) {
|
|
12517
|
+
throw new ValidationError(`Invalid npm package name: \`${name}\`. ` + "Must be lowercase, start with alphanumeric, and contain only letters, numbers, hyphens, dots, or underscores.");
|
|
12518
|
+
}
|
|
12519
|
+
}
|
|
12520
|
+
function parseNpmSpecifier(specifier) {
|
|
12521
|
+
if (!specifier?.trim()) {
|
|
12522
|
+
throw new ValidationError("npm specifier cannot be empty");
|
|
12523
|
+
}
|
|
12524
|
+
const trimmed = specifier.trim();
|
|
12525
|
+
if (!trimmed.startsWith("npm:")) {
|
|
12526
|
+
throw new ValidationError(`Invalid npm specifier: \`${specifier}\`. Must start with \`npm:\` prefix.`);
|
|
12527
|
+
}
|
|
12528
|
+
const remainder = trimmed.slice(4);
|
|
12529
|
+
if (!remainder) {
|
|
12530
|
+
throw new ValidationError(`Invalid npm specifier: \`${specifier}\`. Package name is required.`);
|
|
12531
|
+
}
|
|
12532
|
+
const lastAt = remainder.lastIndexOf("@");
|
|
12533
|
+
let name;
|
|
12534
|
+
let version;
|
|
12535
|
+
if (lastAt > 0) {
|
|
12536
|
+
const beforeAt = remainder.slice(0, lastAt);
|
|
12537
|
+
const afterAt = remainder.slice(lastAt + 1);
|
|
12538
|
+
if (beforeAt.includes("/") || !beforeAt.startsWith("@")) {
|
|
12539
|
+
name = beforeAt;
|
|
12540
|
+
version = afterAt || undefined;
|
|
12541
|
+
} else {
|
|
12542
|
+
throw new ValidationError(`Invalid npm specifier: \`${specifier}\`. Scoped packages must have format @scope/pkg.`);
|
|
12543
|
+
}
|
|
12544
|
+
} else {
|
|
12545
|
+
name = remainder;
|
|
12546
|
+
}
|
|
12547
|
+
validateNpmPackageName(name);
|
|
12548
|
+
return { type: "npm", name, version };
|
|
12549
|
+
}
|
|
12550
|
+
function isNpmSpecifier(input) {
|
|
12551
|
+
return input.trim().startsWith("npm:");
|
|
12552
|
+
}
|
|
12553
|
+
async function validateNpmPackage(packageName) {
|
|
12554
|
+
validateNpmPackageName(packageName);
|
|
12555
|
+
const encodedName = packageName.startsWith("@") ? `@${encodeURIComponent(packageName.slice(1))}` : encodeURIComponent(packageName);
|
|
12556
|
+
const url = `${NPM_REGISTRY_BASE}/${encodedName}`;
|
|
12557
|
+
try {
|
|
12558
|
+
const controller = new AbortController;
|
|
12559
|
+
const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT_MS);
|
|
12560
|
+
const response = await fetch(url, {
|
|
12561
|
+
signal: controller.signal,
|
|
12562
|
+
headers: {
|
|
12563
|
+
Accept: "application/json"
|
|
12564
|
+
}
|
|
12565
|
+
});
|
|
12566
|
+
clearTimeout(timeoutId);
|
|
12567
|
+
if (response.status === 404) {
|
|
12568
|
+
throw new NotFoundError(`npm package \`${packageName}\` not found on registry`);
|
|
12569
|
+
}
|
|
12570
|
+
if (!response.ok) {
|
|
12571
|
+
throw new NetworkError(`Failed to fetch npm package \`${packageName}\`: HTTP ${response.status} ${response.statusText}`);
|
|
12572
|
+
}
|
|
12573
|
+
const data = await response.json();
|
|
12574
|
+
return data;
|
|
12575
|
+
} catch (error) {
|
|
12576
|
+
if (error instanceof NotFoundError || error instanceof NetworkError) {
|
|
12577
|
+
throw error;
|
|
12578
|
+
}
|
|
12579
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
12580
|
+
throw new NetworkError(`Request to npm registry timed out after ${NPM_FETCH_TIMEOUT_MS / 1000}s for package \`${packageName}\``);
|
|
12581
|
+
}
|
|
12582
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12583
|
+
throw new NetworkError(`Failed to fetch npm package \`${packageName}\`: ${message}`);
|
|
12584
|
+
}
|
|
12585
|
+
}
|
|
12586
|
+
function formatPluginEntry(name, version) {
|
|
12587
|
+
return version ? `${name}@${version}` : name;
|
|
12588
|
+
}
|
|
12589
|
+
function validateOpenCodePlugin(packageJson) {
|
|
12590
|
+
const warnings = [];
|
|
12591
|
+
if (packageJson.type !== "module") {
|
|
12592
|
+
throw new ValidationError(`Package \`${packageJson.name}\` is not an ESM module (missing "type": "module" in package.json)`);
|
|
12593
|
+
}
|
|
12594
|
+
const hasMain = Boolean(packageJson.main);
|
|
12595
|
+
const hasExports = packageJson.exports !== undefined;
|
|
12596
|
+
if (!hasMain && !hasExports) {
|
|
12597
|
+
throw new ValidationError(`Package \`${packageJson.name}\` has no entry point (missing "main" or "exports")`);
|
|
12598
|
+
}
|
|
12599
|
+
if (!packageJson.name.includes("opencode")) {
|
|
12600
|
+
warnings.push(`Package name \`${packageJson.name}\` doesn't contain "opencode" - this may not be an OpenCode plugin`);
|
|
12601
|
+
}
|
|
12602
|
+
return { valid: true, warnings };
|
|
12603
|
+
}
|
|
12604
|
+
async function fetchPackageVersion(packageName, version) {
|
|
12605
|
+
const metadata = await validateNpmPackage(packageName);
|
|
12606
|
+
const resolvedVersion = version ?? metadata["dist-tags"].latest;
|
|
12607
|
+
const versionData = metadata.versions[resolvedVersion];
|
|
12608
|
+
if (!versionData) {
|
|
12609
|
+
throw new NotFoundError(`Version \`${resolvedVersion}\` not found for npm package \`${packageName}\``);
|
|
12610
|
+
}
|
|
12611
|
+
return versionData;
|
|
12612
|
+
}
|
|
12613
|
+
function extractPackageName(pluginEntry) {
|
|
12614
|
+
const trimmed = pluginEntry.trim();
|
|
12615
|
+
const lastAt = trimmed.lastIndexOf("@");
|
|
12616
|
+
if (lastAt <= 0) {
|
|
12617
|
+
return trimmed;
|
|
12618
|
+
}
|
|
12619
|
+
const beforeAt = trimmed.slice(0, lastAt);
|
|
12620
|
+
if (beforeAt.includes("/") || !beforeAt.startsWith("@")) {
|
|
12621
|
+
return beforeAt;
|
|
12622
|
+
}
|
|
12623
|
+
return trimmed;
|
|
12624
|
+
}
|
|
12625
|
+
|
|
12489
12626
|
// src/commands/add.ts
|
|
12627
|
+
function parseAddInput(input) {
|
|
12628
|
+
if (!input?.trim()) {
|
|
12629
|
+
throw new ValidationError("Component name cannot be empty");
|
|
12630
|
+
}
|
|
12631
|
+
const trimmed = input.trim();
|
|
12632
|
+
if (isNpmSpecifier(trimmed)) {
|
|
12633
|
+
const parsed = parseNpmSpecifier(trimmed);
|
|
12634
|
+
return { type: "npm", name: parsed.name, version: parsed.version };
|
|
12635
|
+
}
|
|
12636
|
+
if (trimmed.includes("/")) {
|
|
12637
|
+
const { namespace, component } = parseQualifiedComponent(trimmed);
|
|
12638
|
+
return { type: "registry", namespace, component };
|
|
12639
|
+
}
|
|
12640
|
+
return { type: "registry", namespace: "", component: trimmed };
|
|
12641
|
+
}
|
|
12490
12642
|
function registerAddCommand(program2) {
|
|
12491
|
-
const cmd = program2.command("add").description(
|
|
12643
|
+
const cmd = program2.command("add").description(`Add components or npm plugins to your project.
|
|
12644
|
+
|
|
12645
|
+
` + ` Registry components: ocx add namespace/component
|
|
12646
|
+
` + " npm plugins: ocx add npm:package-name[@version]").argument("<components...>", "Components to install (namespace/component or npm:package[@version])").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks").option("--trust", "Skip npm plugin validation (for packages that don't follow conventions)");
|
|
12492
12647
|
addCommonOptions(cmd);
|
|
12493
|
-
|
|
12648
|
+
addForceOption(cmd);
|
|
12494
12649
|
addVerboseOption(cmd);
|
|
12495
12650
|
cmd.action(async (components, options2) => {
|
|
12496
12651
|
try {
|
|
@@ -12502,6 +12657,104 @@ function registerAddCommand(program2) {
|
|
|
12502
12657
|
});
|
|
12503
12658
|
}
|
|
12504
12659
|
async function runAddCore(componentNames, options2, provider) {
|
|
12660
|
+
const cwd = provider.cwd;
|
|
12661
|
+
const parsedInputs = componentNames.map(parseAddInput);
|
|
12662
|
+
const npmInputs = parsedInputs.filter((i) => i.type === "npm");
|
|
12663
|
+
const registryInputs = parsedInputs.filter((i) => i.type === "registry");
|
|
12664
|
+
if (npmInputs.length > 0) {
|
|
12665
|
+
await handleNpmPlugins(npmInputs, options2, cwd);
|
|
12666
|
+
}
|
|
12667
|
+
if (registryInputs.length > 0) {
|
|
12668
|
+
const registryComponentNames = registryInputs.map((i) => i.namespace ? `${i.namespace}/${i.component}` : i.component);
|
|
12669
|
+
await runRegistryAddCore(registryComponentNames, options2, provider);
|
|
12670
|
+
}
|
|
12671
|
+
}
|
|
12672
|
+
async function handleNpmPlugins(inputs, options2, cwd) {
|
|
12673
|
+
const spin = options2.quiet ? null : createSpinner({ text: "Validating npm packages..." });
|
|
12674
|
+
spin?.start();
|
|
12675
|
+
try {
|
|
12676
|
+
const allWarnings = [];
|
|
12677
|
+
for (const input of inputs) {
|
|
12678
|
+
await validateNpmPackage(input.name);
|
|
12679
|
+
if (!options2.trust) {
|
|
12680
|
+
try {
|
|
12681
|
+
const versionData = await fetchPackageVersion(input.name, input.version);
|
|
12682
|
+
const result = validateOpenCodePlugin(versionData);
|
|
12683
|
+
allWarnings.push(...result.warnings);
|
|
12684
|
+
} catch (error) {
|
|
12685
|
+
if (error instanceof ValidationError) {
|
|
12686
|
+
spin?.fail("Plugin validation failed");
|
|
12687
|
+
throw new ValidationError(`${error.message}
|
|
12688
|
+
` + `hint OpenCode plugins must be ESM modules with an entry point
|
|
12689
|
+
` + `hint Use \`--trust\` to add anyway`);
|
|
12690
|
+
}
|
|
12691
|
+
throw error;
|
|
12692
|
+
}
|
|
12693
|
+
}
|
|
12694
|
+
}
|
|
12695
|
+
spin?.succeed(`Validated ${inputs.length} npm package(s)`);
|
|
12696
|
+
if (allWarnings.length > 0 && !options2.quiet) {
|
|
12697
|
+
logger.info("");
|
|
12698
|
+
for (const warning of allWarnings) {
|
|
12699
|
+
logger.warn(warning);
|
|
12700
|
+
}
|
|
12701
|
+
}
|
|
12702
|
+
const existingConfig = await readOpencodeJsonConfig(cwd);
|
|
12703
|
+
const existingPlugins = existingConfig?.config.plugin ?? [];
|
|
12704
|
+
const existingPluginMap = new Map;
|
|
12705
|
+
for (const plugin of existingPlugins) {
|
|
12706
|
+
const name = extractPackageName(plugin);
|
|
12707
|
+
existingPluginMap.set(name, plugin);
|
|
12708
|
+
}
|
|
12709
|
+
const pluginsToAdd = [];
|
|
12710
|
+
const conflicts = [];
|
|
12711
|
+
for (const input of inputs) {
|
|
12712
|
+
const existingEntry = existingPluginMap.get(input.name);
|
|
12713
|
+
if (existingEntry) {
|
|
12714
|
+
if (!options2.force) {
|
|
12715
|
+
conflicts.push(input.name);
|
|
12716
|
+
} else {
|
|
12717
|
+
existingPluginMap.set(input.name, formatPluginEntry(input.name, input.version));
|
|
12718
|
+
}
|
|
12719
|
+
} else {
|
|
12720
|
+
pluginsToAdd.push(formatPluginEntry(input.name, input.version));
|
|
12721
|
+
}
|
|
12722
|
+
}
|
|
12723
|
+
if (conflicts.length > 0) {
|
|
12724
|
+
throw new ConflictError(`Plugin(s) already exist in opencode.json: ${conflicts.join(", ")}.
|
|
12725
|
+
` + "Use --force to replace existing entries.");
|
|
12726
|
+
}
|
|
12727
|
+
const finalPlugins = [...existingPluginMap.values(), ...pluginsToAdd];
|
|
12728
|
+
if (options2.dryRun) {
|
|
12729
|
+
logger.info("");
|
|
12730
|
+
logger.info("Dry run - no changes made");
|
|
12731
|
+
logger.info("");
|
|
12732
|
+
logger.info("Would add npm plugins:");
|
|
12733
|
+
for (const input of inputs) {
|
|
12734
|
+
logger.info(` ${formatPluginEntry(input.name, input.version)}`);
|
|
12735
|
+
}
|
|
12736
|
+
return;
|
|
12737
|
+
}
|
|
12738
|
+
await updateOpencodeJsonConfig(cwd, { plugin: finalPlugins });
|
|
12739
|
+
if (!options2.quiet) {
|
|
12740
|
+
logger.info("");
|
|
12741
|
+
logger.success(`Added ${inputs.length} npm plugin(s) to opencode.json`);
|
|
12742
|
+
for (const input of inputs) {
|
|
12743
|
+
logger.info(` \u2713 ${formatPluginEntry(input.name, input.version)}`);
|
|
12744
|
+
}
|
|
12745
|
+
}
|
|
12746
|
+
if (options2.json) {
|
|
12747
|
+
console.log(JSON.stringify({
|
|
12748
|
+
success: true,
|
|
12749
|
+
plugins: inputs.map((i) => formatPluginEntry(i.name, i.version))
|
|
12750
|
+
}, null, 2));
|
|
12751
|
+
}
|
|
12752
|
+
} catch (error) {
|
|
12753
|
+
spin?.fail("Failed to add npm plugins");
|
|
12754
|
+
throw error;
|
|
12755
|
+
}
|
|
12756
|
+
}
|
|
12757
|
+
async function runRegistryAddCore(componentNames, options2, provider) {
|
|
12505
12758
|
const cwd = provider.cwd;
|
|
12506
12759
|
const lockPath = join2(cwd, "ocx.lock");
|
|
12507
12760
|
const registries = provider.getRegistries();
|
|
@@ -12595,7 +12848,7 @@ async function runAddCore(componentNames, options2, provider) {
|
|
|
12595
12848
|
if (existsSync(targetPath)) {
|
|
12596
12849
|
const existingContent = await Bun.file(targetPath).text();
|
|
12597
12850
|
const incomingContent = file.content.toString("utf-8");
|
|
12598
|
-
if (!isContentIdentical(existingContent, incomingContent) && !options2.
|
|
12851
|
+
if (!isContentIdentical(existingContent, incomingContent) && !options2.force) {
|
|
12599
12852
|
allConflicts.push(componentFile.target);
|
|
12600
12853
|
}
|
|
12601
12854
|
}
|
|
@@ -12609,14 +12862,14 @@ async function runAddCore(componentNames, options2, provider) {
|
|
|
12609
12862
|
}
|
|
12610
12863
|
logger.error("");
|
|
12611
12864
|
logger.error("These files have been modified since installation.");
|
|
12612
|
-
logger.error("Use --
|
|
12613
|
-
throw new ConflictError(`${allConflicts.length} file(s) have conflicts. Use --
|
|
12865
|
+
logger.error("Use --force to overwrite, or review the changes first.");
|
|
12866
|
+
throw new ConflictError(`${allConflicts.length} file(s) have conflicts. Use --force to overwrite.`);
|
|
12614
12867
|
}
|
|
12615
12868
|
const installSpin = options2.quiet ? null : createSpinner({ text: "Installing components..." });
|
|
12616
12869
|
installSpin?.start();
|
|
12617
12870
|
for (const { component, files, computedHash } of componentBundles) {
|
|
12618
12871
|
const installResult = await installComponent(component, files, cwd, {
|
|
12619
|
-
|
|
12872
|
+
force: options2.force,
|
|
12620
12873
|
verbose: options2.verbose
|
|
12621
12874
|
});
|
|
12622
12875
|
if (options2.verbose) {
|
|
@@ -13871,9 +14124,9 @@ Diff for ${res.name}:`));
|
|
|
13871
14124
|
|
|
13872
14125
|
// src/commands/ghost/add.ts
|
|
13873
14126
|
function registerGhostAddCommand(parent) {
|
|
13874
|
-
const cmd = parent.command("add").description("Add components using ghost mode registries").argument("<components...>", "Components to install").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks");
|
|
14127
|
+
const cmd = parent.command("add").description("Add components using ghost mode registries").argument("<components...>", "Components to install").option("--dry-run", "Show what would be installed without making changes").option("--skip-compat-check", "Skip version compatibility checks").option("--trust", "Skip npm plugin validation");
|
|
13875
14128
|
addCommonOptions(cmd);
|
|
13876
|
-
|
|
14129
|
+
addForceOption(cmd);
|
|
13877
14130
|
addVerboseOption(cmd);
|
|
13878
14131
|
cmd.action(async (components, options2) => {
|
|
13879
14132
|
try {
|
|
@@ -13931,12 +14184,9 @@ var DEFAULT_GHOST_CONFIG = `{
|
|
|
13931
14184
|
// This config is used when running commands with \`ocx ghost\` or \`ocx g\`
|
|
13932
14185
|
// Note: OpenCode settings go in ~/.config/ocx/opencode.jsonc (see: ocx ghost opencode --edit)
|
|
13933
14186
|
|
|
13934
|
-
// Component registries
|
|
13935
|
-
"
|
|
13936
|
-
|
|
13937
|
-
"url": "https://registry.opencode.ai"
|
|
13938
|
-
}
|
|
13939
|
-
},
|
|
14187
|
+
// Component registries - add your registries here
|
|
14188
|
+
// Example: "myregistry": { "url": "https://example.com/registry" }
|
|
14189
|
+
"registries": {},
|
|
13940
14190
|
|
|
13941
14191
|
// Where to install components (relative to project root)
|
|
13942
14192
|
"componentPath": "src/components"
|
|
@@ -14079,9 +14329,9 @@ function filterExcludedPaths(excludedPaths, includePatterns, excludePatterns) {
|
|
|
14079
14329
|
|
|
14080
14330
|
// src/utils/symlink-farm.ts
|
|
14081
14331
|
import { randomBytes } from "crypto";
|
|
14082
|
-
import { readdir, rename, rm, stat, symlink } from "fs/promises";
|
|
14332
|
+
import { mkdir as mkdir5, readdir, rename, rm, stat, symlink } from "fs/promises";
|
|
14083
14333
|
import { tmpdir } from "os";
|
|
14084
|
-
import { isAbsolute, join as join6 } from "path";
|
|
14334
|
+
import { dirname as dirname5, isAbsolute, join as join6, relative as relative2 } from "path";
|
|
14085
14335
|
var STALE_SESSION_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
14086
14336
|
var REMOVING_THRESHOLD_MS = 60 * 60 * 1000;
|
|
14087
14337
|
var GHOST_DIR_PREFIX = "ocx-ghost-";
|
|
@@ -14109,6 +14359,32 @@ async function createSymlinkFarm(sourceDir, excludePaths) {
|
|
|
14109
14359
|
throw error;
|
|
14110
14360
|
}
|
|
14111
14361
|
}
|
|
14362
|
+
async function injectGhostFiles(tempDir, sourceDir, injectPaths) {
|
|
14363
|
+
if (!isAbsolute(tempDir)) {
|
|
14364
|
+
throw new Error(`tempDir must be an absolute path, got: ${tempDir}`);
|
|
14365
|
+
}
|
|
14366
|
+
if (!isAbsolute(sourceDir)) {
|
|
14367
|
+
throw new Error(`sourceDir must be an absolute path, got: ${sourceDir}`);
|
|
14368
|
+
}
|
|
14369
|
+
for (const injectPath of injectPaths) {
|
|
14370
|
+
const relativePath = relative2(sourceDir, injectPath);
|
|
14371
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
14372
|
+
throw new Error(`injectPath must be within sourceDir: ${injectPath}`);
|
|
14373
|
+
}
|
|
14374
|
+
const targetPath = join6(tempDir, relativePath);
|
|
14375
|
+
const parentDir = dirname5(targetPath);
|
|
14376
|
+
if (parentDir !== tempDir) {
|
|
14377
|
+
await mkdir5(parentDir, { recursive: true });
|
|
14378
|
+
}
|
|
14379
|
+
try {
|
|
14380
|
+
await symlink(injectPath, targetPath);
|
|
14381
|
+
} catch (err) {
|
|
14382
|
+
if (err.code !== "EEXIST") {
|
|
14383
|
+
throw new Error(`Failed to inject ${injectPath} \u2192 ${targetPath}: ${err.message}`);
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
14386
|
+
}
|
|
14387
|
+
}
|
|
14112
14388
|
async function cleanupSymlinkFarm(tempDir) {
|
|
14113
14389
|
const removingPath = `${tempDir}${REMOVING_SUFFIX}`;
|
|
14114
14390
|
try {
|
|
@@ -14189,6 +14465,8 @@ async function runGhostOpenCode(args, options2) {
|
|
|
14189
14465
|
const ghostConfig = await loadGhostConfig();
|
|
14190
14466
|
const excludePaths = filterExcludedPaths(discoveredPaths, ghostConfig.include, ghostConfig.exclude);
|
|
14191
14467
|
const tempDir = await createSymlinkFarm(cwd, excludePaths);
|
|
14468
|
+
const ghostFiles = await discoverProjectFiles(ghostConfigDir, ghostConfigDir);
|
|
14469
|
+
await injectGhostFiles(tempDir, ghostConfigDir, ghostFiles);
|
|
14192
14470
|
let cleanupDone = false;
|
|
14193
14471
|
const performCleanup = async () => {
|
|
14194
14472
|
if (cleanupDone)
|
|
@@ -14239,7 +14517,7 @@ async function runGhostOpenCode(args, options2) {
|
|
|
14239
14517
|
}
|
|
14240
14518
|
|
|
14241
14519
|
// src/commands/registry.ts
|
|
14242
|
-
async function
|
|
14520
|
+
async function runRegistryAddCore2(url, options2, callbacks) {
|
|
14243
14521
|
if (callbacks.isLocked?.()) {
|
|
14244
14522
|
throw new Error("Registries are locked. Cannot add.");
|
|
14245
14523
|
}
|
|
@@ -14285,7 +14563,7 @@ function registerRegistryCommand(program2) {
|
|
|
14285
14563
|
logger.error("No ocx.jsonc found. Run 'ocx init' first.");
|
|
14286
14564
|
process.exit(1);
|
|
14287
14565
|
}
|
|
14288
|
-
const result = await
|
|
14566
|
+
const result = await runRegistryAddCore2(url, options2, {
|
|
14289
14567
|
getRegistries: () => config.registries,
|
|
14290
14568
|
isLocked: () => config.lockRegistries ?? false,
|
|
14291
14569
|
setRegistry: async (name, regConfig) => {
|
|
@@ -14380,7 +14658,7 @@ function registerGhostRegistryCommand(parent) {
|
|
|
14380
14658
|
try {
|
|
14381
14659
|
await ensureGhostInitialized();
|
|
14382
14660
|
const config = await loadGhostConfig();
|
|
14383
|
-
const result = await
|
|
14661
|
+
const result = await runRegistryAddCore2(url, options2, {
|
|
14384
14662
|
getRegistries: () => config.registries,
|
|
14385
14663
|
setRegistry: async (name, regConfig) => {
|
|
14386
14664
|
config.registries[name] = regConfig;
|
|
@@ -14606,7 +14884,7 @@ function registerGhostCommand(program2) {
|
|
|
14606
14884
|
|
|
14607
14885
|
// src/commands/init.ts
|
|
14608
14886
|
import { existsSync as existsSync2 } from "fs";
|
|
14609
|
-
import { cp, mkdir as
|
|
14887
|
+
import { cp, mkdir as mkdir6, readdir as readdir2, readFile, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
14610
14888
|
import { join as join7 } from "path";
|
|
14611
14889
|
var TEMPLATE_REPO = "kdcokenny/ocx";
|
|
14612
14890
|
var TEMPLATE_PATH = "examples/registry-starter";
|
|
@@ -14684,7 +14962,7 @@ async function runInitRegistry(directory, options2) {
|
|
|
14684
14962
|
if (spin)
|
|
14685
14963
|
spin.text = options2.local ? "Copying template..." : "Fetching template...";
|
|
14686
14964
|
if (options2.local) {
|
|
14687
|
-
await
|
|
14965
|
+
await mkdir6(cwd, { recursive: true });
|
|
14688
14966
|
await copyDir(options2.local, cwd);
|
|
14689
14967
|
} else {
|
|
14690
14968
|
const version = options2.canary ? "main" : await getLatestVersion();
|
|
@@ -14733,7 +15011,7 @@ async function fetchAndExtractTemplate(destDir, version, verbose) {
|
|
|
14733
15011
|
throw new NetworkError(`Failed to fetch template from ${tarballUrl}: ${response.statusText}`);
|
|
14734
15012
|
}
|
|
14735
15013
|
const tempDir = join7(destDir, ".ocx-temp");
|
|
14736
|
-
await
|
|
15014
|
+
await mkdir6(tempDir, { recursive: true });
|
|
14737
15015
|
try {
|
|
14738
15016
|
const tarPath = join7(tempDir, "template.tar.gz");
|
|
14739
15017
|
const arrayBuffer = await response.arrayBuffer();
|
|
@@ -14780,8 +15058,8 @@ async function replacePlaceholders(dir, values) {
|
|
|
14780
15058
|
// src/commands/update.ts
|
|
14781
15059
|
import { createHash as createHash2 } from "crypto";
|
|
14782
15060
|
import { existsSync as existsSync3 } from "fs";
|
|
14783
|
-
import { mkdir as
|
|
14784
|
-
import { dirname as
|
|
15061
|
+
import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
|
|
15062
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
14785
15063
|
function registerUpdateCommand(program2) {
|
|
14786
15064
|
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) => {
|
|
14787
15065
|
try {
|
|
@@ -14919,9 +15197,9 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
14919
15197
|
if (!fileObj)
|
|
14920
15198
|
continue;
|
|
14921
15199
|
const targetPath = join8(cwd, fileObj.target);
|
|
14922
|
-
const targetDir =
|
|
15200
|
+
const targetDir = dirname6(targetPath);
|
|
14923
15201
|
if (!existsSync3(targetDir)) {
|
|
14924
|
-
await
|
|
15202
|
+
await mkdir7(targetDir, { recursive: true });
|
|
14925
15203
|
}
|
|
14926
15204
|
await writeFile4(targetPath, file.content);
|
|
14927
15205
|
if (options2.verbose) {
|
|
@@ -15056,7 +15334,7 @@ async function hashBundle2(files) {
|
|
|
15056
15334
|
`));
|
|
15057
15335
|
}
|
|
15058
15336
|
// src/index.ts
|
|
15059
|
-
var version = "1.
|
|
15337
|
+
var version = "1.1.1";
|
|
15060
15338
|
async function main2() {
|
|
15061
15339
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
15062
15340
|
registerInitCommand(program2);
|
|
@@ -15083,4 +15361,4 @@ export {
|
|
|
15083
15361
|
buildRegistry
|
|
15084
15362
|
};
|
|
15085
15363
|
|
|
15086
|
-
//# debugId=
|
|
15364
|
+
//# debugId=66554E2EE245482F64756E2164756E21
|