ocx 0.1.1 → 1.0.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/package.json CHANGED
@@ -1,29 +1,63 @@
1
1
  {
2
- "name": "ocx",
3
- "version": "0.1.1",
4
- "description": "OCX CLI - Install OpenCode extensions",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "bin": {
8
- "ocx": "./dist/index.js"
9
- },
10
- "scripts": {
11
- "build": "bun run scripts/build.ts",
12
- "build:binary": "bun run scripts/build-binary.ts",
13
- "dev": "bun run src/index.ts",
14
- "typecheck": "tsc --noEmit",
15
- "test": "bun test"
16
- },
17
- "dependencies": {
18
- "commander": "^14.0.0",
19
- "diff": "^8.0.0",
20
- "fuzzysort": "^3.1.0",
21
- "kleur": "^4.1.5",
22
- "ora": "^8.2.0",
23
- "zod": "^3.24.0"
24
- },
25
- "devDependencies": {
26
- "@types/bun": "latest",
27
- "@types/diff": "^8.0.0"
28
- }
2
+ "name": "ocx",
3
+ "version": "1.0.1",
4
+ "description": "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
5
+ "author": "kdcokenny",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "bin": {
10
+ "ocx": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist/index.js",
14
+ "dist/index.js.map"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/kdcokenny/ocx.git"
22
+ },
23
+ "homepage": "https://github.com/kdcokenny/ocx#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/kdcokenny/ocx/issues"
26
+ },
27
+ "keywords": [
28
+ "opencode",
29
+ "cli",
30
+ "extensions",
31
+ "agents",
32
+ "plugins",
33
+ "mcp",
34
+ "shadcn",
35
+ "registry"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "build": "bun run scripts/build.ts",
42
+ "build:binary": "bun run scripts/build-binary.ts",
43
+ "check": "bun check:biome && bun check:types",
44
+ "check:biome": "biome check .",
45
+ "check:types": "tsc --noEmit",
46
+ "dev": "bun run src/index.ts",
47
+ "prepublishOnly": "bun run build",
48
+ "test": "bun test"
49
+ },
50
+ "dependencies": {
51
+ "commander": "^14.0.0",
52
+ "diff": "^8.0.0",
53
+ "fuzzysort": "^3.1.0",
54
+ "kleur": "^4.1.5",
55
+ "ora": "^8.2.0",
56
+ "strip-json-comments": "^5.0.3",
57
+ "zod": "^3.24.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/bun": "latest",
61
+ "@types/diff": "^8.0.0"
62
+ }
29
63
  }
Binary file
@@ -1,96 +0,0 @@
1
- /**
2
- * Build binary script for OCX CLI
3
- * Creates standalone executables for multiple platforms
4
- */
5
-
6
- export {}
7
-
8
- import { readFileSync } from "node:fs"
9
- import { join } from "node:path"
10
-
11
- const pkg = JSON.parse(readFileSync("./package.json", "utf-8"))
12
- const version = pkg.version
13
-
14
- const targets = [
15
- "bun-darwin-arm64",
16
- "bun-darwin-x64",
17
- "bun-linux-x64",
18
- "bun-linux-arm64",
19
- "bun-windows-x64-baseline",
20
- ] as const
21
-
22
- type Target = (typeof targets)[number]
23
-
24
- const outDir = "./dist/bin"
25
-
26
- async function buildBinary(target: Target) {
27
- const ext = target.includes("windows") ? ".exe" : ""
28
- const outfile = join(outDir, `ocx-${target.replace("bun-", "")}${ext}`)
29
-
30
- console.log(`Building ${target}...`)
31
-
32
- // Use bun's compile for the actual binary
33
- const proc = Bun.spawn([
34
- "bun",
35
- "build",
36
- "--compile",
37
- `--target=${target}`,
38
- "--minify",
39
- `--outfile=${outfile}`,
40
- "./src/index.ts",
41
- ])
42
-
43
- await proc.exited
44
-
45
- if (proc.exitCode !== 0) {
46
- console.error(`Failed to compile binary for ${target}`)
47
- process.exit(1)
48
- }
49
-
50
- console.log(`✓ ${outfile}`)
51
- }
52
-
53
- // Parse args
54
- const args = process.argv.slice(2)
55
- const targetArg = args.find((a) => a.startsWith("--target="))
56
- const allFlag = args.includes("--all")
57
-
58
- if (allFlag) {
59
- // Build all targets
60
- for (const target of targets) {
61
- await buildBinary(target)
62
- }
63
- } else if (targetArg) {
64
- // Build specific target
65
- const target = targetArg.replace("--target=", "") as Target
66
- if (!targets.includes(target)) {
67
- console.error(`Invalid target: ${target}`)
68
- console.error(`Valid targets: ${targets.join(", ")}`)
69
- process.exit(1)
70
- }
71
- await buildBinary(target)
72
- } else {
73
- // Default: build for current platform
74
- const platform = process.platform
75
- const arch = process.arch
76
-
77
- let target: Target
78
- if (platform === "darwin" && arch === "arm64") {
79
- target = "bun-darwin-arm64"
80
- } else if (platform === "darwin") {
81
- target = "bun-darwin-x64"
82
- } else if (platform === "linux" && arch === "arm64") {
83
- target = "bun-linux-arm64"
84
- } else if (platform === "linux") {
85
- target = "bun-linux-x64"
86
- } else if (platform === "win32") {
87
- target = "bun-windows-x64-baseline"
88
- } else {
89
- console.error(`Unsupported platform: ${platform}-${arch}`)
90
- process.exit(1)
91
- }
92
-
93
- await buildBinary(target)
94
- }
95
-
96
- console.log("\n✓ Binary build complete")
package/scripts/build.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Build script for OCX CLI
3
- * Compiles TypeScript to JavaScript
4
- */
5
-
6
- export {}
7
-
8
- await Bun.build({
9
- entrypoints: ["./src/index.ts"],
10
- outdir: "./dist",
11
- target: "bun",
12
- format: "esm",
13
- minify: false,
14
- sourcemap: "external",
15
- })
16
-
17
- console.log("✓ Build complete: ./dist/index.js")
@@ -1,65 +0,0 @@
1
- #!/bin/bash
2
- # OCX Installer
3
-
4
- set -e
5
-
6
- REPO="kdcokenny/ocx"
7
- GITHUB_URL="https://github.com/$REPO"
8
-
9
- # Detect OS
10
- OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
11
- case "$OS" in
12
- darwin) PLATFORM="darwin" ;;
13
- linux) PLATFORM="linux" ;;
14
- *) echo "Unsupported OS: $OS"; exit 1 ;;
15
- esac
16
-
17
- # Detect Architecture
18
- ARCH="$(uname -m)"
19
- case "$ARCH" in
20
- arm64|aarch64) ARCH="arm64" ;;
21
- x86_64|amd64) ARCH="x64" ;;
22
- *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
23
- esac
24
-
25
- # Construct binary name
26
- BINARY_NAME="ocx-$PLATFORM-$ARCH"
27
- if [ "$PLATFORM" = "windows" ]; then
28
- BINARY_NAME="ocx-windows-x64-baseline.exe"
29
- fi
30
-
31
- # Get latest release version
32
- LATEST_TAG=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
33
-
34
- if [ -z "$LATEST_TAG" ]; then
35
- echo "Could not find latest release for $REPO"
36
- exit 1
37
- fi
38
-
39
- DOWNLOAD_URL="$GITHUB_URL/releases/download/$LATEST_TAG/$BINARY_NAME"
40
-
41
- echo "Downloading OCX $LATEST_TAG for $PLATFORM-$ARCH..."
42
- echo "URL: $DOWNLOAD_URL"
43
-
44
- # Create temp directory
45
- TMP_DIR=$(mktemp -d)
46
- trap 'rm -rf "$TMP_DIR"' EXIT
47
-
48
- # Download
49
- curl -L "$DOWNLOAD_URL" -o "$TMP_DIR/ocx"
50
-
51
- # Installation path
52
- INSTALL_DIR="/usr/local/bin"
53
- if [ ! -w "$INSTALL_DIR" ]; then
54
- INSTALL_DIR="$HOME/.local/bin"
55
- mkdir -p "$INSTALL_DIR"
56
- echo "Warning: /usr/local/bin is not writable. Installing to $INSTALL_DIR instead."
57
- echo "Make sure $INSTALL_DIR is in your PATH."
58
- fi
59
-
60
- # Move to install dir
61
- chmod +x "$TMP_DIR/ocx"
62
- mv "$TMP_DIR/ocx" "$INSTALL_DIR/ocx"
63
-
64
- echo "OCX has been installed to $INSTALL_DIR/ocx"
65
- echo "Run 'ocx --version' to verify installation."
@@ -1,229 +0,0 @@
1
- /**
2
- * OCX CLI - add command
3
- * Install components from registries
4
- */
5
-
6
- import { existsSync } from "node:fs"
7
- import { mkdir, readFile, writeFile } from "node:fs/promises"
8
- import { dirname, join } from "node:path"
9
- import { createHash } from "node:crypto"
10
- import type { Command } from "commander"
11
- import { ocxConfigSchema, ocxLockSchema, type OcxLock } from "../schemas/config.js"
12
- import type { ComponentManifest } from "../schemas/registry.js"
13
- import { fetchRegistryIndex, fetchFileContent } from "../registry/fetcher.js"
14
- import { resolveDependencies, type ResolvedDependencies } from "../registry/resolver.js"
15
- import { updateOpencodeConfig } from "../registry/opencode-config.js"
16
- import { logger, createSpinner, handleError } from "../utils/index.js"
17
- import { ConfigError, IntegrityError } from "../utils/errors.js"
18
-
19
- interface AddOptions {
20
- yes?: boolean
21
- dryRun?: boolean
22
- cwd?: string
23
- quiet?: boolean
24
- verbose?: boolean
25
- json?: boolean
26
- }
27
-
28
- export function registerAddCommand(program: Command): void {
29
- program
30
- .command("add")
31
- .description("Add components to your project")
32
- .argument("<components...>", "Components to install")
33
- .option("-y, --yes", "Skip prompts")
34
- .option("--dry-run", "Show what would be installed without making changes")
35
- .option("--cwd <path>", "Working directory", process.cwd())
36
- .option("-q, --quiet", "Suppress output")
37
- .option("-v, --verbose", "Verbose output")
38
- .option("--json", "Output as JSON")
39
- .action(async (components: string[], options: AddOptions) => {
40
- try {
41
- await runAdd(components, options)
42
- } catch (error) {
43
- handleError(error, { json: options.json })
44
- }
45
- })
46
- }
47
-
48
- async function runAdd(componentNames: string[], options: AddOptions): Promise<void> {
49
- const cwd = options.cwd ?? process.cwd()
50
- const configPath = join(cwd, "ocx.jsonc")
51
- const lockPath = join(cwd, "ocx.lock")
52
-
53
- // Load config
54
- if (!existsSync(configPath)) {
55
- throw new ConfigError("No ocx.jsonc found. Run 'ocx init' first.")
56
- }
57
-
58
- const configContent = await readFile(configPath, "utf-8")
59
- const config = ocxConfigSchema.parse(JSON.parse(configContent))
60
-
61
- // Load or create lock
62
- let lock: OcxLock = { lockVersion: 1, installed: {} }
63
- if (existsSync(lockPath)) {
64
- const lockContent = await readFile(lockPath, "utf-8")
65
- lock = ocxLockSchema.parse(JSON.parse(lockContent))
66
- }
67
-
68
- const spin = options.quiet ? null : createSpinner({ text: "Resolving dependencies..." })
69
- spin?.start()
70
-
71
- try {
72
- // Resolve all dependencies across all configured registries
73
- const resolved = await resolveDependencies(config.registries, componentNames)
74
-
75
- spin?.succeed(`Resolved ${resolved.components.length} components`)
76
-
77
- if (options.verbose) {
78
- logger.info("Install order:")
79
- for (const name of resolved.installOrder) {
80
- logger.info(` - ${name}`)
81
- }
82
- }
83
-
84
- if (options.dryRun) {
85
- logger.info("")
86
- logger.info("Dry run - no changes made")
87
- logResolved(resolved)
88
- return
89
- }
90
-
91
- // Install components
92
- const installSpin = options.quiet ? null : createSpinner({ text: "Installing components..." })
93
- installSpin?.start()
94
-
95
- for (const component of resolved.components) {
96
- // Fetch component files and compute bundle hash
97
- const files: { path: string; content: Buffer }[] = []
98
- for (const file of component.files) {
99
- const content = await fetchFileContent(component.baseUrl, component.name, file.path)
100
- files.push({ path: file.path, content: Buffer.from(content) })
101
- }
102
-
103
- const computedHash = await hashBundle(files)
104
-
105
- // Verify integrity if already in lock
106
- const existingEntry = lock.installed[component.name]
107
- if (existingEntry && existingEntry.hash !== computedHash) {
108
- throw new IntegrityError(component.name, existingEntry.hash, computedHash)
109
- }
110
-
111
- // Install components
112
- await installComponent(component, files, cwd)
113
-
114
- // Fetch registry index to get version for lockfile
115
- const index = await fetchRegistryIndex(component.baseUrl)
116
-
117
- // Update lock
118
- lock.installed[component.name] = {
119
- registry: component.registryName,
120
- version: index.version,
121
- hash: computedHash,
122
- target: getTargetPath(component),
123
- installedAt: new Date().toISOString(),
124
- }
125
- }
126
-
127
- installSpin?.succeed(`Installed ${resolved.components.length} components`)
128
-
129
- // Apply opencode.json changes
130
- if (Object.keys(resolved.mcpServers).length > 0) {
131
- const result = await updateOpencodeConfig(cwd, {
132
- mcpServers: resolved.mcpServers,
133
- })
134
-
135
- if (result.mcpSkipped.length > 0 && !options.quiet) {
136
- for (const name of result.mcpSkipped) {
137
- logger.warn(`MCP server "${name}" already configured, skipped`)
138
- }
139
- }
140
-
141
- if (!options.quiet && result.mcpAdded.length > 0) {
142
- logger.info(`Configured ${result.mcpAdded.length} MCP servers`)
143
- }
144
- }
145
-
146
- // Save lock file
147
- await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8")
148
-
149
- if (options.json) {
150
- console.log(
151
- JSON.stringify(
152
- {
153
- success: true,
154
- installed: resolved.installOrder,
155
- mcpServers: Object.keys(resolved.mcpServers),
156
- },
157
- null,
158
- 2,
159
- ),
160
- )
161
- } else if (!options.quiet) {
162
- logger.info("")
163
- logger.success(`Done! Installed ${resolved.components.length} components.`)
164
- }
165
- } catch (error) {
166
- spin?.fail("Failed to resolve dependencies")
167
- throw error
168
- }
169
- }
170
-
171
- async function installComponent(
172
- component: ComponentManifest,
173
- files: { path: string; content: Buffer }[],
174
- cwd: string,
175
- ): Promise<void> {
176
- for (const file of files) {
177
- const componentFile = component.files.find((f) => f.path === file.path)
178
- if (!componentFile) continue
179
-
180
- const targetPath = join(cwd, componentFile.target)
181
- const targetDir = dirname(targetPath)
182
-
183
- // Create directory if needed
184
- if (!existsSync(targetDir)) {
185
- await mkdir(targetDir, { recursive: true })
186
- }
187
-
188
- await writeFile(targetPath, file.content)
189
- }
190
- }
191
-
192
- function getTargetPath(component: ComponentManifest): string {
193
- return component.files[0]?.target ?? `.opencode/${component.type}/${component.name}`
194
- }
195
-
196
- async function hashContent(content: string | Buffer): Promise<string> {
197
- return createHash("sha256").update(content).digest("hex")
198
- }
199
-
200
- async function hashBundle(files: { path: string; content: Buffer }[]): Promise<string> {
201
- // Sort files for deterministic hashing
202
- const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path))
203
-
204
- // Create a manifest of file hashes
205
- const manifestParts: string[] = []
206
- for (const file of sorted) {
207
- const hash = await hashContent(file.content)
208
- manifestParts.push(`${file.path}:${hash}`)
209
- }
210
-
211
- // Hash the manifest itself
212
- return hashContent(manifestParts.join("\n"))
213
- }
214
-
215
- function logResolved(resolved: ResolvedDependencies): void {
216
- logger.info("")
217
- logger.info("Would install:")
218
- for (const component of resolved.components) {
219
- logger.info(` ${component.name} (${component.type}) from ${component.registryName}`)
220
- }
221
-
222
- if (Object.keys(resolved.mcpServers).length > 0) {
223
- logger.info("")
224
- logger.info("Would configure MCP servers:")
225
- for (const name of Object.keys(resolved.mcpServers)) {
226
- logger.info(` ${name}`)
227
- }
228
- }
229
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * Build Command (for Registry Authors)
3
- *
4
- * Validate and build a registry from source.
5
- */
6
-
7
- import { Command } from "commander"
8
- import { join, relative, dirname } from "node:path"
9
- import { mkdir } from "node:fs/promises"
10
- import { existsSync } from "node:fs"
11
- import kleur from "kleur"
12
- import { registrySchema, componentManifestSchema } from "../schemas/registry.js"
13
- import { logger, handleError, outputJson, createSpinner } from "../utils/index.js"
14
-
15
- interface BuildOptions {
16
- cwd: string
17
- out: string
18
- json: boolean
19
- quiet: boolean
20
- }
21
-
22
- export function registerBuildCommand(program: Command): void {
23
- program
24
- .command("build")
25
- .description("Build a registry from source (for registry authors)")
26
- .argument("[path]", "Registry source directory", ".")
27
- .option("--out <dir>", "Output directory", "./dist")
28
- .option("--cwd <path>", "Working directory", process.cwd())
29
- .option("--json", "Output as JSON", false)
30
- .option("-q, --quiet", "Suppress output", false)
31
- .action(async (path: string, options: BuildOptions) => {
32
- try {
33
- const sourcePath = join(options.cwd, path)
34
- const outPath = join(options.cwd, options.out)
35
-
36
- const spinner = createSpinner({ text: "Building registry...", quiet: options.quiet || options.json })
37
- if (!options.json) spinner.start()
38
-
39
- // Read registry.json from source
40
- const registryFile = Bun.file(join(sourcePath, "registry.json"))
41
- if (!(await registryFile.exists())) {
42
- if (!options.json) spinner.fail("No registry.json found in source directory")
43
- process.exit(1)
44
- }
45
-
46
- const registryData = await registryFile.json()
47
-
48
- // Validate registry schema
49
- const parseResult = registrySchema.safeParse(registryData)
50
- if (!parseResult.success) {
51
- if (!options.json) {
52
- spinner.fail("Registry validation failed")
53
- const errors = parseResult.error.errors.map(e => ` ${e.path.join(".")}: ${e.message}`)
54
- for (const err of errors) {
55
- console.log(kleur.red(err))
56
- }
57
- }
58
- process.exit(1)
59
- }
60
-
61
- const registry = parseResult.data
62
- const validationErrors: string[] = []
63
-
64
- // Create output directory structure
65
- const componentsDir = join(outPath, "components")
66
- await mkdir(componentsDir, { recursive: true })
67
-
68
- // Generate packument and copy files for each component
69
- for (const component of registry.components) {
70
- const packument = {
71
- name: component.name,
72
- versions: {
73
- [registry.version]: component,
74
- },
75
- "dist-tags": {
76
- latest: registry.version,
77
- },
78
- }
79
-
80
- // Write manifest to components/[name].json
81
- const packumentPath = join(componentsDir, `${component.name}.json`)
82
- await Bun.write(packumentPath, JSON.stringify(packument, null, 2))
83
-
84
- // Copy files to components/[name]/[path]
85
- for (const file of component.files) {
86
- const sourceFilePath = join(sourcePath, "files", file.path)
87
- const destFilePath = join(componentsDir, component.name, file.path)
88
- const destFileDir = dirname(destFilePath)
89
-
90
- if (!(await Bun.file(sourceFilePath).exists())) {
91
- validationErrors.push(`${component.name}: Source file not found at ${sourceFilePath}`)
92
- continue
93
- }
94
-
95
- await mkdir(destFileDir, { recursive: true })
96
- const sourceFile = Bun.file(sourceFilePath)
97
- await Bun.write(destFilePath, sourceFile)
98
- }
99
- }
100
-
101
- // Fail fast if source files were missing during copy
102
- if (validationErrors.length > 0) {
103
- if (!options.json) {
104
- spinner.fail(`Build failed with ${validationErrors.length} errors`)
105
- for (const err of validationErrors) {
106
- console.log(kleur.red(` ${err}`))
107
- }
108
- }
109
- process.exit(1)
110
- }
111
-
112
- // Generate index.json at the root
113
- const index = {
114
- name: registry.name,
115
- prefix: registry.prefix,
116
- version: registry.version,
117
- author: registry.author,
118
- components: registry.components.map(c => ({
119
- name: c.name,
120
- type: c.type,
121
- description: c.description,
122
- })),
123
- }
124
-
125
- await Bun.write(join(outPath, "index.json"), JSON.stringify(index, null, 2))
126
-
127
- if (!options.json) {
128
- const msg = `Built ${registry.components.length} components to ${relative(options.cwd, outPath)}`
129
- spinner.succeed(msg)
130
- if (process.env.NODE_ENV === "test" || !process.stdout.isTTY) {
131
- logger.success(`Built ${registry.components.length} components`)
132
- }
133
- }
134
-
135
- if (options.json) {
136
- outputJson({
137
- success: true,
138
- data: {
139
- name: registry.name,
140
- version: registry.version,
141
- components: registry.components.length,
142
- output: outPath,
143
- },
144
- })
145
- }
146
- } catch (error) {
147
- handleError(error, { json: options.json })
148
- }
149
- })
150
- }