ocx 0.1.1 → 1.0.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 +1903 -1900
- package/dist/index.js.map +20 -20
- package/package.json +60 -27
- package/dist/bin/ocx-darwin-arm64 +0 -0
- package/scripts/build-binary.ts +0 -96
- package/scripts/build.ts +0 -17
- package/scripts/install.sh +0 -65
- package/src/commands/add.ts +0 -229
- package/src/commands/build.ts +0 -150
- package/src/commands/diff.ts +0 -139
- package/src/commands/init.ts +0 -90
- package/src/commands/registry.ts +0 -153
- package/src/commands/search.ts +0 -159
- package/src/constants.ts +0 -18
- package/src/index.ts +0 -42
- package/src/registry/fetcher.ts +0 -168
- package/src/registry/index.ts +0 -2
- package/src/registry/opencode-config.ts +0 -182
- package/src/registry/resolver.ts +0 -127
- package/src/schemas/config.ts +0 -207
- package/src/schemas/index.ts +0 -6
- package/src/schemas/registry.ts +0 -268
- package/src/utils/env.ts +0 -27
- package/src/utils/errors.ts +0 -81
- package/src/utils/handle-error.ts +0 -108
- package/src/utils/index.ts +0 -10
- package/src/utils/json-output.ts +0 -107
- package/src/utils/logger.ts +0 -72
- package/src/utils/spinner.ts +0 -46
- package/tests/add.test.ts +0 -102
- package/tests/build.test.ts +0 -136
- package/tests/diff.test.ts +0 -47
- package/tests/helpers.ts +0 -68
- package/tests/init.test.ts +0 -52
- package/tests/mock-registry.ts +0 -105
- package/tests/registry.test.ts +0 -78
- package/tests/search.test.ts +0 -64
- package/tsconfig.json +0 -15
package/package.json
CHANGED
|
@@ -1,29 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
2
|
+
"name": "ocx",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"zod": "^3.24.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/bun": "latest",
|
|
60
|
+
"@types/diff": "^8.0.0"
|
|
61
|
+
}
|
|
29
62
|
}
|
|
Binary file
|
package/scripts/build-binary.ts
DELETED
|
@@ -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")
|
package/scripts/install.sh
DELETED
|
@@ -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."
|
package/src/commands/add.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/build.ts
DELETED
|
@@ -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
|
-
}
|