ocx 1.0.20 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +344 -56
- package/dist/index.js.map +12 -11
- 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.0
|
|
10435
|
+
version: "1.1.0",
|
|
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",
|
|
@@ -10691,6 +10703,7 @@ async function resolveDependencies(registries, componentNames) {
|
|
|
10691
10703
|
}
|
|
10692
10704
|
|
|
10693
10705
|
// src/updaters/update-opencode-config.ts
|
|
10706
|
+
import path3 from "path";
|
|
10694
10707
|
var JSONC_OPTIONS = {
|
|
10695
10708
|
formattingOptions: {
|
|
10696
10709
|
tabSize: 2,
|
|
@@ -10699,9 +10712,28 @@ var JSONC_OPTIONS = {
|
|
|
10699
10712
|
`
|
|
10700
10713
|
}
|
|
10701
10714
|
};
|
|
10715
|
+
var OPENCODE_CONFIG_TEMPLATE = `{
|
|
10716
|
+
"$schema": "https://opencode.ai/config.json"
|
|
10717
|
+
// Add MCP servers, tools, plugins here
|
|
10718
|
+
}
|
|
10719
|
+
`;
|
|
10720
|
+
async function ensureOpencodeConfig(cwd) {
|
|
10721
|
+
const jsoncPath = path3.join(cwd, "opencode.jsonc");
|
|
10722
|
+
const jsonPath = path3.join(cwd, "opencode.json");
|
|
10723
|
+
const jsoncFile = Bun.file(jsoncPath);
|
|
10724
|
+
if (await jsoncFile.exists()) {
|
|
10725
|
+
return { path: jsoncPath, created: false };
|
|
10726
|
+
}
|
|
10727
|
+
const jsonFile = Bun.file(jsonPath);
|
|
10728
|
+
if (await jsonFile.exists()) {
|
|
10729
|
+
return { path: jsonPath, created: false };
|
|
10730
|
+
}
|
|
10731
|
+
await Bun.write(jsoncPath, OPENCODE_CONFIG_TEMPLATE);
|
|
10732
|
+
return { path: jsoncPath, created: true };
|
|
10733
|
+
}
|
|
10702
10734
|
async function readOpencodeJsonConfig(cwd) {
|
|
10703
|
-
const jsonPath =
|
|
10704
|
-
const jsoncPath =
|
|
10735
|
+
const jsonPath = path3.join(cwd, "opencode.json");
|
|
10736
|
+
const jsoncPath = path3.join(cwd, "opencode.jsonc");
|
|
10705
10737
|
for (const configPath of [jsoncPath, jsonPath]) {
|
|
10706
10738
|
const file = Bun.file(configPath);
|
|
10707
10739
|
if (await file.exists()) {
|
|
@@ -10715,13 +10747,13 @@ async function readOpencodeJsonConfig(cwd) {
|
|
|
10715
10747
|
}
|
|
10716
10748
|
return null;
|
|
10717
10749
|
}
|
|
10718
|
-
async function writeOpencodeJsonConfig(
|
|
10719
|
-
await Bun.write(
|
|
10750
|
+
async function writeOpencodeJsonConfig(path4, content) {
|
|
10751
|
+
await Bun.write(path4, content);
|
|
10720
10752
|
}
|
|
10721
|
-
function getValueAtPath(content,
|
|
10753
|
+
function getValueAtPath(content, path4) {
|
|
10722
10754
|
const parsed = parse2(content, [], { allowTrailingComma: true });
|
|
10723
10755
|
let current = parsed;
|
|
10724
|
-
for (const segment of
|
|
10756
|
+
for (const segment of path4) {
|
|
10725
10757
|
if (current === null || current === undefined)
|
|
10726
10758
|
return;
|
|
10727
10759
|
if (typeof current !== "object")
|
|
@@ -10730,27 +10762,27 @@ function getValueAtPath(content, path3) {
|
|
|
10730
10762
|
}
|
|
10731
10763
|
return current;
|
|
10732
10764
|
}
|
|
10733
|
-
function applyValueAtPath(content,
|
|
10765
|
+
function applyValueAtPath(content, path4, value) {
|
|
10734
10766
|
if (value === null || value === undefined) {
|
|
10735
10767
|
return content;
|
|
10736
10768
|
}
|
|
10737
10769
|
if (typeof value === "object" && !Array.isArray(value)) {
|
|
10738
|
-
const existingValue = getValueAtPath(content,
|
|
10770
|
+
const existingValue = getValueAtPath(content, path4);
|
|
10739
10771
|
if (existingValue !== undefined && (existingValue === null || typeof existingValue !== "object")) {
|
|
10740
|
-
const edits2 = modify(content,
|
|
10772
|
+
const edits2 = modify(content, path4, value, JSONC_OPTIONS);
|
|
10741
10773
|
return applyEdits(content, edits2);
|
|
10742
10774
|
}
|
|
10743
10775
|
let updatedContent = content;
|
|
10744
10776
|
for (const [key, val] of Object.entries(value)) {
|
|
10745
|
-
updatedContent = applyValueAtPath(updatedContent, [...
|
|
10777
|
+
updatedContent = applyValueAtPath(updatedContent, [...path4, key], val);
|
|
10746
10778
|
}
|
|
10747
10779
|
return updatedContent;
|
|
10748
10780
|
}
|
|
10749
10781
|
if (Array.isArray(value)) {
|
|
10750
|
-
const edits2 = modify(content,
|
|
10782
|
+
const edits2 = modify(content, path4, value, JSONC_OPTIONS);
|
|
10751
10783
|
return applyEdits(content, edits2);
|
|
10752
10784
|
}
|
|
10753
|
-
const edits = modify(content,
|
|
10785
|
+
const edits = modify(content, path4, value, JSONC_OPTIONS);
|
|
10754
10786
|
return applyEdits(content, edits);
|
|
10755
10787
|
}
|
|
10756
10788
|
async function updateOpencodeJsonConfig(cwd, opencode) {
|
|
@@ -10764,7 +10796,7 @@ async function updateOpencodeJsonConfig(cwd, opencode) {
|
|
|
10764
10796
|
} else {
|
|
10765
10797
|
const config = { $schema: "https://opencode.ai/config.json" };
|
|
10766
10798
|
content = JSON.stringify(config, null, "\t");
|
|
10767
|
-
configPath =
|
|
10799
|
+
configPath = path3.join(cwd, "opencode.jsonc");
|
|
10768
10800
|
created = true;
|
|
10769
10801
|
}
|
|
10770
10802
|
const originalContent = content;
|
|
@@ -10983,8 +11015,8 @@ function handleError(error, options2 = {}) {
|
|
|
10983
11015
|
if (error instanceof ZodError) {
|
|
10984
11016
|
logger.error("Validation failed:");
|
|
10985
11017
|
for (const issue of error.issues) {
|
|
10986
|
-
const
|
|
10987
|
-
logger.error(` ${
|
|
11018
|
+
const path4 = issue.path.join(".");
|
|
11019
|
+
logger.error(` ${path4}: ${issue.message}`);
|
|
10988
11020
|
}
|
|
10989
11021
|
process.exit(EXIT_CODES.CONFIG);
|
|
10990
11022
|
}
|
|
@@ -11042,14 +11074,14 @@ function outputJson(data) {
|
|
|
11042
11074
|
console.log(JSON.stringify(data, null, 2));
|
|
11043
11075
|
}
|
|
11044
11076
|
// src/utils/path-safety.ts
|
|
11045
|
-
import
|
|
11077
|
+
import path4 from "path";
|
|
11046
11078
|
function isPathInside(childPath, parentPath) {
|
|
11047
|
-
const resolvedChild =
|
|
11048
|
-
const resolvedParent =
|
|
11079
|
+
const resolvedChild = path4.resolve(childPath);
|
|
11080
|
+
const resolvedParent = path4.resolve(parentPath);
|
|
11049
11081
|
if (resolvedChild === resolvedParent) {
|
|
11050
11082
|
return true;
|
|
11051
11083
|
}
|
|
11052
|
-
const relative =
|
|
11084
|
+
const relative = path4.relative(resolvedParent, resolvedChild);
|
|
11053
11085
|
return !!relative && !relative.startsWith("..") && !isAbsolutePath(relative);
|
|
11054
11086
|
}
|
|
11055
11087
|
function assertPathInside(childPath, parentPath) {
|
|
@@ -11062,14 +11094,14 @@ var sharedOptions = {
|
|
|
11062
11094
|
cwd: () => new Option("--cwd <path>", "Working directory").default(process.cwd()),
|
|
11063
11095
|
quiet: () => new Option("-q, --quiet", "Suppress output"),
|
|
11064
11096
|
json: () => new Option("--json", "Output as JSON"),
|
|
11065
|
-
|
|
11097
|
+
force: () => new Option("-f, --force", "Skip confirmation prompts"),
|
|
11066
11098
|
verbose: () => new Option("-v, --verbose", "Verbose output")
|
|
11067
11099
|
};
|
|
11068
11100
|
function addCommonOptions(cmd) {
|
|
11069
11101
|
return cmd.addOption(sharedOptions.cwd()).addOption(sharedOptions.quiet()).addOption(sharedOptions.json());
|
|
11070
11102
|
}
|
|
11071
|
-
function
|
|
11072
|
-
return cmd.addOption(sharedOptions.
|
|
11103
|
+
function addForceOption(cmd) {
|
|
11104
|
+
return cmd.addOption(sharedOptions.force());
|
|
11073
11105
|
}
|
|
11074
11106
|
function addVerboseOption(cmd) {
|
|
11075
11107
|
return cmd.addOption(sharedOptions.verbose());
|
|
@@ -12466,11 +12498,154 @@ function warnCompatIssues(registryName, issues) {
|
|
|
12466
12498
|
logger.log(formatCompatWarning(registryName, issue));
|
|
12467
12499
|
}
|
|
12468
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
|
+
|
|
12469
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
|
+
}
|
|
12470
12642
|
function registerAddCommand(program2) {
|
|
12471
|
-
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)");
|
|
12472
12647
|
addCommonOptions(cmd);
|
|
12473
|
-
|
|
12648
|
+
addForceOption(cmd);
|
|
12474
12649
|
addVerboseOption(cmd);
|
|
12475
12650
|
cmd.action(async (components, options2) => {
|
|
12476
12651
|
try {
|
|
@@ -12482,6 +12657,104 @@ function registerAddCommand(program2) {
|
|
|
12482
12657
|
});
|
|
12483
12658
|
}
|
|
12484
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) {
|
|
12485
12758
|
const cwd = provider.cwd;
|
|
12486
12759
|
const lockPath = join2(cwd, "ocx.lock");
|
|
12487
12760
|
const registries = provider.getRegistries();
|
|
@@ -12575,7 +12848,7 @@ async function runAddCore(componentNames, options2, provider) {
|
|
|
12575
12848
|
if (existsSync(targetPath)) {
|
|
12576
12849
|
const existingContent = await Bun.file(targetPath).text();
|
|
12577
12850
|
const incomingContent = file.content.toString("utf-8");
|
|
12578
|
-
if (!isContentIdentical(existingContent, incomingContent) && !options2.
|
|
12851
|
+
if (!isContentIdentical(existingContent, incomingContent) && !options2.force) {
|
|
12579
12852
|
allConflicts.push(componentFile.target);
|
|
12580
12853
|
}
|
|
12581
12854
|
}
|
|
@@ -12589,14 +12862,14 @@ async function runAddCore(componentNames, options2, provider) {
|
|
|
12589
12862
|
}
|
|
12590
12863
|
logger.error("");
|
|
12591
12864
|
logger.error("These files have been modified since installation.");
|
|
12592
|
-
logger.error("Use --
|
|
12593
|
-
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.`);
|
|
12594
12867
|
}
|
|
12595
12868
|
const installSpin = options2.quiet ? null : createSpinner({ text: "Installing components..." });
|
|
12596
12869
|
installSpin?.start();
|
|
12597
12870
|
for (const { component, files, computedHash } of componentBundles) {
|
|
12598
12871
|
const installResult = await installComponent(component, files, cwd, {
|
|
12599
|
-
|
|
12872
|
+
force: options2.force,
|
|
12600
12873
|
verbose: options2.verbose
|
|
12601
12874
|
});
|
|
12602
12875
|
if (options2.verbose) {
|
|
@@ -12912,9 +13185,9 @@ async function buildRegistry(options2) {
|
|
|
12912
13185
|
|
|
12913
13186
|
// src/commands/build.ts
|
|
12914
13187
|
function registerBuildCommand(program2) {
|
|
12915
|
-
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 (
|
|
13188
|
+
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) => {
|
|
12916
13189
|
try {
|
|
12917
|
-
const sourcePath = join4(options2.cwd,
|
|
13190
|
+
const sourcePath = join4(options2.cwd, path5);
|
|
12918
13191
|
const outPath = join4(options2.cwd, options2.out);
|
|
12919
13192
|
const spinner2 = createSpinner({
|
|
12920
13193
|
text: "Building registry...",
|
|
@@ -13059,16 +13332,16 @@ class Diff {
|
|
|
13059
13332
|
}
|
|
13060
13333
|
}
|
|
13061
13334
|
}
|
|
13062
|
-
addToPath(
|
|
13063
|
-
const last =
|
|
13335
|
+
addToPath(path5, added, removed, oldPosInc, options2) {
|
|
13336
|
+
const last = path5.lastComponent;
|
|
13064
13337
|
if (last && !options2.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
13065
13338
|
return {
|
|
13066
|
-
oldPos:
|
|
13339
|
+
oldPos: path5.oldPos + oldPosInc,
|
|
13067
13340
|
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
13068
13341
|
};
|
|
13069
13342
|
} else {
|
|
13070
13343
|
return {
|
|
13071
|
-
oldPos:
|
|
13344
|
+
oldPos: path5.oldPos + oldPosInc,
|
|
13072
13345
|
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
13073
13346
|
};
|
|
13074
13347
|
}
|
|
@@ -13851,9 +14124,9 @@ Diff for ${res.name}:`));
|
|
|
13851
14124
|
|
|
13852
14125
|
// src/commands/ghost/add.ts
|
|
13853
14126
|
function registerGhostAddCommand(parent) {
|
|
13854
|
-
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");
|
|
13855
14128
|
addCommonOptions(cmd);
|
|
13856
|
-
|
|
14129
|
+
addForceOption(cmd);
|
|
13857
14130
|
addVerboseOption(cmd);
|
|
13858
14131
|
cmd.action(async (components, options2) => {
|
|
13859
14132
|
try {
|
|
@@ -13946,13 +14219,22 @@ async function runGhostInit(options2) {
|
|
|
13946
14219
|
}
|
|
13947
14220
|
throw err;
|
|
13948
14221
|
}
|
|
14222
|
+
const opencodeResult = await ensureOpencodeConfig(configDir);
|
|
13949
14223
|
if (options2.json) {
|
|
13950
|
-
console.log(JSON.stringify({
|
|
14224
|
+
console.log(JSON.stringify({
|
|
14225
|
+
success: true,
|
|
14226
|
+
path: configPath,
|
|
14227
|
+
opencodePath: opencodeResult.path,
|
|
14228
|
+
opencodeCreated: opencodeResult.created
|
|
14229
|
+
}));
|
|
13951
14230
|
return;
|
|
13952
14231
|
}
|
|
13953
14232
|
if (!options2.quiet) {
|
|
13954
14233
|
logger.success("Ghost mode initialized");
|
|
13955
14234
|
logger.info(`Created ${configPath}`);
|
|
14235
|
+
if (opencodeResult.created) {
|
|
14236
|
+
logger.info(`Created ${opencodeResult.path}`);
|
|
14237
|
+
}
|
|
13956
14238
|
logger.info("");
|
|
13957
14239
|
logger.info("Next steps:");
|
|
13958
14240
|
logger.info(" 1. Edit your config: ocx ghost config");
|
|
@@ -14009,14 +14291,14 @@ async function discoverProjectFiles(start, stop) {
|
|
|
14009
14291
|
const excluded = new Set;
|
|
14010
14292
|
for (const file of CONFIG_FILES) {
|
|
14011
14293
|
const found = await findUp(file, start, stop);
|
|
14012
|
-
for (const
|
|
14013
|
-
excluded.add(
|
|
14294
|
+
for (const path5 of found) {
|
|
14295
|
+
excluded.add(path5);
|
|
14014
14296
|
}
|
|
14015
14297
|
}
|
|
14016
14298
|
for (const file of RULE_FILES) {
|
|
14017
14299
|
const found = await findUp(file, start, stop);
|
|
14018
|
-
for (const
|
|
14019
|
-
excluded.add(
|
|
14300
|
+
for (const path5 of found) {
|
|
14301
|
+
excluded.add(path5);
|
|
14020
14302
|
}
|
|
14021
14303
|
}
|
|
14022
14304
|
for await (const dir of up({ targets: CONFIG_DIRS, start, stop })) {
|
|
@@ -14037,13 +14319,13 @@ function filterExcludedPaths(excludedPaths, includePatterns, excludePatterns) {
|
|
|
14037
14319
|
const includeGlobs = includePatterns.map((p) => new Glob2(p));
|
|
14038
14320
|
const excludeGlobs = excludePatterns?.map((p) => new Glob2(p)) ?? [];
|
|
14039
14321
|
const filteredExclusions = new Set;
|
|
14040
|
-
for (const
|
|
14041
|
-
const matchesInclude = matchesAnyGlob(
|
|
14042
|
-
const matchesExclude = matchesAnyGlob(
|
|
14322
|
+
for (const path5 of excludedPaths) {
|
|
14323
|
+
const matchesInclude = matchesAnyGlob(path5, includeGlobs);
|
|
14324
|
+
const matchesExclude = matchesAnyGlob(path5, excludeGlobs);
|
|
14043
14325
|
if (matchesInclude && !matchesExclude) {
|
|
14044
14326
|
continue;
|
|
14045
14327
|
}
|
|
14046
|
-
filteredExclusions.add(
|
|
14328
|
+
filteredExclusions.add(path5);
|
|
14047
14329
|
}
|
|
14048
14330
|
return filteredExclusions;
|
|
14049
14331
|
}
|
|
@@ -14151,7 +14433,7 @@ async function runGhostOpenCode(args, options2) {
|
|
|
14151
14433
|
const openCodeConfig = await loadGhostOpencodeConfig();
|
|
14152
14434
|
const ghostConfigDir = getGhostConfigDir();
|
|
14153
14435
|
if (Object.keys(openCodeConfig).length === 0 && !options2.quiet) {
|
|
14154
|
-
logger.warn(`No opencode.jsonc found at ${getGhostOpencodeConfigPath()}. Run 'ocx ghost
|
|
14436
|
+
logger.warn(`No opencode.jsonc found at ${getGhostOpencodeConfigPath()}. Run 'ocx ghost init' first.`);
|
|
14155
14437
|
}
|
|
14156
14438
|
const cwd = process.cwd();
|
|
14157
14439
|
const gitContext = await detectGitRepo(cwd);
|
|
@@ -14210,7 +14492,7 @@ async function runGhostOpenCode(args, options2) {
|
|
|
14210
14492
|
}
|
|
14211
14493
|
|
|
14212
14494
|
// src/commands/registry.ts
|
|
14213
|
-
async function
|
|
14495
|
+
async function runRegistryAddCore2(url, options2, callbacks) {
|
|
14214
14496
|
if (callbacks.isLocked?.()) {
|
|
14215
14497
|
throw new Error("Registries are locked. Cannot add.");
|
|
14216
14498
|
}
|
|
@@ -14256,7 +14538,7 @@ function registerRegistryCommand(program2) {
|
|
|
14256
14538
|
logger.error("No ocx.jsonc found. Run 'ocx init' first.");
|
|
14257
14539
|
process.exit(1);
|
|
14258
14540
|
}
|
|
14259
|
-
const result = await
|
|
14541
|
+
const result = await runRegistryAddCore2(url, options2, {
|
|
14260
14542
|
getRegistries: () => config.registries,
|
|
14261
14543
|
isLocked: () => config.lockRegistries ?? false,
|
|
14262
14544
|
setRegistry: async (name, regConfig) => {
|
|
@@ -14351,7 +14633,7 @@ function registerGhostRegistryCommand(parent) {
|
|
|
14351
14633
|
try {
|
|
14352
14634
|
await ensureGhostInitialized();
|
|
14353
14635
|
const config = await loadGhostConfig();
|
|
14354
|
-
const result = await
|
|
14636
|
+
const result = await runRegistryAddCore2(url, options2, {
|
|
14355
14637
|
getRegistries: () => config.registries,
|
|
14356
14638
|
setRegistry: async (name, regConfig) => {
|
|
14357
14639
|
config.registries[name] = regConfig;
|
|
@@ -14613,14 +14895,20 @@ async function runInit(options2) {
|
|
|
14613
14895
|
const config = ocxConfigSchema.parse(rawConfig);
|
|
14614
14896
|
const content2 = JSON.stringify(config, null, 2);
|
|
14615
14897
|
await writeFile3(configPath, content2, "utf-8");
|
|
14616
|
-
|
|
14617
|
-
logger.success("Initialized OCX configuration");
|
|
14618
|
-
}
|
|
14898
|
+
const opencodeResult = await ensureOpencodeConfig(cwd);
|
|
14619
14899
|
spin?.succeed("Initialized OCX configuration");
|
|
14620
14900
|
if (options2.json) {
|
|
14621
|
-
console.log(JSON.stringify({
|
|
14901
|
+
console.log(JSON.stringify({
|
|
14902
|
+
success: true,
|
|
14903
|
+
path: configPath,
|
|
14904
|
+
opencodePath: opencodeResult.path,
|
|
14905
|
+
opencodeCreated: opencodeResult.created
|
|
14906
|
+
}));
|
|
14622
14907
|
} else if (!options2.quiet) {
|
|
14623
14908
|
logger.info(`Created ${configPath}`);
|
|
14909
|
+
if (opencodeResult.created) {
|
|
14910
|
+
logger.info(`Created ${opencodeResult.path}`);
|
|
14911
|
+
}
|
|
14624
14912
|
logger.info("");
|
|
14625
14913
|
logger.info("Next steps:");
|
|
14626
14914
|
logger.info(" 1. Add a registry: ocx registry add <url>");
|
|
@@ -15021,7 +15309,7 @@ async function hashBundle2(files) {
|
|
|
15021
15309
|
`));
|
|
15022
15310
|
}
|
|
15023
15311
|
// src/index.ts
|
|
15024
|
-
var version = "1.0
|
|
15312
|
+
var version = "1.1.0";
|
|
15025
15313
|
async function main2() {
|
|
15026
15314
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
15027
15315
|
registerInitCommand(program2);
|
|
@@ -15048,4 +15336,4 @@ export {
|
|
|
15048
15336
|
buildRegistry
|
|
15049
15337
|
};
|
|
15050
15338
|
|
|
15051
|
-
//# debugId=
|
|
15339
|
+
//# debugId=46A2A0DCEF36C6A464756E2164756E21
|