ocx 0.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/bin/ocx-darwin-arm64 +0 -0
- package/dist/index.js +11857 -0
- package/dist/index.js.map +78 -0
- package/package.json +29 -0
- package/scripts/build-binary.ts +96 -0
- package/scripts/build.ts +17 -0
- package/scripts/install.sh +65 -0
- package/src/commands/add.ts +229 -0
- package/src/commands/build.ts +150 -0
- package/src/commands/diff.ts +139 -0
- package/src/commands/init.ts +90 -0
- package/src/commands/registry.ts +153 -0
- package/src/commands/search.ts +159 -0
- package/src/constants.ts +18 -0
- package/src/index.ts +42 -0
- package/src/registry/fetcher.ts +168 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/opencode-config.ts +182 -0
- package/src/registry/resolver.ts +127 -0
- package/src/schemas/config.ts +207 -0
- package/src/schemas/index.ts +6 -0
- package/src/schemas/registry.ts +268 -0
- package/src/utils/env.ts +27 -0
- package/src/utils/errors.ts +81 -0
- package/src/utils/handle-error.ts +108 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/json-output.ts +107 -0
- package/src/utils/logger.ts +72 -0
- package/src/utils/spinner.ts +46 -0
- package/tests/add.test.ts +102 -0
- package/tests/build.test.ts +136 -0
- package/tests/diff.test.ts +47 -0
- package/tests/helpers.ts +68 -0
- package/tests/init.test.ts +52 -0
- package/tests/mock-registry.ts +105 -0
- package/tests/registry.test.ts +78 -0
- package/tests/search.test.ts +64 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Command
|
|
3
|
+
*
|
|
4
|
+
* Compare installed components with upstream registry versions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from "commander"
|
|
8
|
+
import * as Diff from "diff"
|
|
9
|
+
import kleur from "kleur"
|
|
10
|
+
import { readOcxLock, readOcxConfig } from "../schemas/config.js"
|
|
11
|
+
import { fetchComponent, fetchFileContent } from "../registry/fetcher.js"
|
|
12
|
+
import { logger, handleError, outputJson } from "../utils/index.js"
|
|
13
|
+
|
|
14
|
+
interface DiffOptions {
|
|
15
|
+
cwd: string
|
|
16
|
+
json: boolean
|
|
17
|
+
quiet: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerDiffCommand(program: Command): void {
|
|
21
|
+
program
|
|
22
|
+
.command("diff")
|
|
23
|
+
.description("Compare installed components with upstream")
|
|
24
|
+
.argument("[component]", "Component to diff (optional, diffs all if omitted)")
|
|
25
|
+
.option("--cwd <path>", "Working directory", process.cwd())
|
|
26
|
+
.option("--json", "Output as JSON", false)
|
|
27
|
+
.option("-q, --quiet", "Suppress output", false)
|
|
28
|
+
.action(async (component: string | undefined, options: DiffOptions) => {
|
|
29
|
+
try {
|
|
30
|
+
const lock = await readOcxLock(options.cwd)
|
|
31
|
+
if (!lock) {
|
|
32
|
+
if (options.json) {
|
|
33
|
+
outputJson({
|
|
34
|
+
success: false,
|
|
35
|
+
error: { code: "NOT_FOUND", message: "No ocx.lock found" },
|
|
36
|
+
})
|
|
37
|
+
} else {
|
|
38
|
+
logger.warn("No ocx.lock found. Run 'ocx add' first.")
|
|
39
|
+
}
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const config = await readOcxConfig(options.cwd)
|
|
44
|
+
if (!config) {
|
|
45
|
+
if (options.json) {
|
|
46
|
+
outputJson({
|
|
47
|
+
success: false,
|
|
48
|
+
error: { code: "NOT_FOUND", message: "No ocx.jsonc found" },
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
logger.warn("No ocx.jsonc found. Run 'ocx init' first.")
|
|
52
|
+
}
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const componentNames = component ? [component] : Object.keys(lock.installed)
|
|
57
|
+
|
|
58
|
+
if (componentNames.length === 0) {
|
|
59
|
+
if (options.json) {
|
|
60
|
+
outputJson({ success: true, data: { diffs: [] } })
|
|
61
|
+
} else {
|
|
62
|
+
logger.info("No components installed.")
|
|
63
|
+
}
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const results: Array<{ name: string; hasChanges: boolean; diff?: string }> = []
|
|
68
|
+
|
|
69
|
+
for (const name of componentNames) {
|
|
70
|
+
const installed = lock.installed[name]
|
|
71
|
+
if (!installed) {
|
|
72
|
+
if (component) {
|
|
73
|
+
logger.warn(`Component '${name}' not found in lockfile.`)
|
|
74
|
+
}
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Read local file
|
|
79
|
+
const localPath = `${options.cwd}/${installed.target}`
|
|
80
|
+
const localFile = Bun.file(localPath)
|
|
81
|
+
if (!(await localFile.exists())) {
|
|
82
|
+
results.push({ name, hasChanges: true, diff: "Local file missing" })
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
const localContent = await localFile.text()
|
|
86
|
+
|
|
87
|
+
// Fetch upstream
|
|
88
|
+
const registryConfig = config.registries[installed.registry]
|
|
89
|
+
if (!registryConfig) {
|
|
90
|
+
logger.warn(`Registry '${installed.registry}' not configured for component '${name}'.`)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const upstream = await fetchComponent(registryConfig.url, name)
|
|
96
|
+
|
|
97
|
+
// Assume first file for simplicity in this MVP
|
|
98
|
+
// In a full implementation we'd diff all files in the component
|
|
99
|
+
const upstreamFile = upstream.files[0]
|
|
100
|
+
if (!upstreamFile) {
|
|
101
|
+
results.push({ name, hasChanges: false })
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fetch actual content from registry
|
|
106
|
+
const upstreamContent = await fetchFileContent(
|
|
107
|
+
registryConfig.url,
|
|
108
|
+
name,
|
|
109
|
+
upstreamFile.path,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (localContent === upstreamContent) {
|
|
113
|
+
results.push({ name, hasChanges: false })
|
|
114
|
+
} else {
|
|
115
|
+
const patch = Diff.createPatch(name, upstreamContent, localContent)
|
|
116
|
+
results.push({ name, hasChanges: true, diff: patch })
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logger.warn(`Could not fetch upstream for ${name}: ${String(err)}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.json) {
|
|
124
|
+
outputJson({ success: true, data: { diffs: results } })
|
|
125
|
+
} else {
|
|
126
|
+
for (const res of results) {
|
|
127
|
+
if (res.hasChanges) {
|
|
128
|
+
console.log(kleur.yellow(`\nDiff for ${res.name}:`))
|
|
129
|
+
console.log(res.diff || "Changes detected (no diff available)")
|
|
130
|
+
} else if (!options.quiet) {
|
|
131
|
+
logger.success(`${res.name}: No changes`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
handleError(error, { json: options.json })
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCX CLI - init command
|
|
3
|
+
* Initialize OCX configuration in a project
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs"
|
|
7
|
+
import { writeFile } from "node:fs/promises"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import { Command } from "commander"
|
|
10
|
+
import { OCX_SCHEMA_URL } from "../constants.js"
|
|
11
|
+
import { ocxConfigSchema } from "../schemas/config.js"
|
|
12
|
+
import { logger, createSpinner, handleError } from "../utils/index.js"
|
|
13
|
+
|
|
14
|
+
interface InitOptions {
|
|
15
|
+
yes?: boolean
|
|
16
|
+
cwd?: string
|
|
17
|
+
quiet?: boolean
|
|
18
|
+
verbose?: boolean
|
|
19
|
+
json?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerInitCommand(program: Command): void {
|
|
23
|
+
program
|
|
24
|
+
.command("init")
|
|
25
|
+
.description("Initialize OCX configuration in your project")
|
|
26
|
+
.option("-y, --yes", "Skip prompts and use defaults")
|
|
27
|
+
.option("--cwd <path>", "Working directory", process.cwd())
|
|
28
|
+
.option("-q, --quiet", "Suppress output")
|
|
29
|
+
.option("-v, --verbose", "Verbose output")
|
|
30
|
+
.option("--json", "Output as JSON")
|
|
31
|
+
.action(async (options: InitOptions) => {
|
|
32
|
+
try {
|
|
33
|
+
await runInit(options)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
handleError(error, { json: options.json })
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runInit(options: InitOptions): Promise<void> {
|
|
41
|
+
const cwd = options.cwd ?? process.cwd()
|
|
42
|
+
const configPath = join(cwd, "ocx.jsonc")
|
|
43
|
+
|
|
44
|
+
// Check for existing config
|
|
45
|
+
if (existsSync(configPath)) {
|
|
46
|
+
if (!options.yes) {
|
|
47
|
+
logger.warn("ocx.jsonc already exists")
|
|
48
|
+
logger.info("Use --yes to overwrite")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
logger.info("Overwriting existing ocx.jsonc")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const spin = options.quiet ? null : createSpinner({ text: "Initializing OCX..." })
|
|
55
|
+
spin?.start()
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Create minimal config - schema will apply defaults
|
|
59
|
+
const rawConfig = {
|
|
60
|
+
$schema: OCX_SCHEMA_URL,
|
|
61
|
+
registries: {},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate with schema (applies defaults)
|
|
65
|
+
const config = ocxConfigSchema.parse(rawConfig)
|
|
66
|
+
|
|
67
|
+
// Write config file
|
|
68
|
+
const content = JSON.stringify(config, null, 2)
|
|
69
|
+
await writeFile(configPath, content, "utf-8")
|
|
70
|
+
|
|
71
|
+
if (!options.quiet && !options.json) {
|
|
72
|
+
logger.success("Initialized OCX configuration")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
spin?.succeed("Initialized OCX configuration")
|
|
76
|
+
|
|
77
|
+
if (options.json) {
|
|
78
|
+
console.log(JSON.stringify({ success: true, path: configPath }))
|
|
79
|
+
} else if (!options.quiet) {
|
|
80
|
+
logger.info(`Created ${configPath}`)
|
|
81
|
+
logger.info("")
|
|
82
|
+
logger.info("Next steps:")
|
|
83
|
+
logger.info(" 1. Add a registry: ocx registry add <url>")
|
|
84
|
+
logger.info(" 2. Install components: ocx add <component>")
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
spin?.fail("Failed to initialize")
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry Command
|
|
3
|
+
*
|
|
4
|
+
* Manage configured registries.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from "commander"
|
|
8
|
+
import kleur from "kleur"
|
|
9
|
+
import { readOcxConfig, writeOcxConfig, type OcxConfig } from "../schemas/config.js"
|
|
10
|
+
import { logger, handleError, outputJson } from "../utils/index.js"
|
|
11
|
+
|
|
12
|
+
interface RegistryOptions {
|
|
13
|
+
cwd: string
|
|
14
|
+
json: boolean
|
|
15
|
+
quiet: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerRegistryCommand(program: Command): void {
|
|
19
|
+
const registry = program
|
|
20
|
+
.command("registry")
|
|
21
|
+
.description("Manage registries")
|
|
22
|
+
|
|
23
|
+
// registry add
|
|
24
|
+
registry
|
|
25
|
+
.command("add")
|
|
26
|
+
.description("Add a registry")
|
|
27
|
+
.argument("<url>", "Registry URL")
|
|
28
|
+
.option("--name <name>", "Registry alias (defaults to hostname)")
|
|
29
|
+
.option("--version <version>", "Pin to specific version")
|
|
30
|
+
.option("--cwd <path>", "Working directory", process.cwd())
|
|
31
|
+
.option("--json", "Output as JSON", false)
|
|
32
|
+
.option("-q, --quiet", "Suppress output", false)
|
|
33
|
+
.action(async (url: string, options: RegistryOptions & { name?: string; version?: string }) => {
|
|
34
|
+
try {
|
|
35
|
+
let config = await readOcxConfig(options.cwd)
|
|
36
|
+
if (!config) {
|
|
37
|
+
logger.error("No ocx.jsonc found. Run 'ocx init' first.")
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (config.lockRegistries) {
|
|
42
|
+
logger.error("Registries are locked. Cannot add.")
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Derive name from URL if not provided
|
|
47
|
+
const name = options.name || new URL(url).hostname.replace(/\./g, "-")
|
|
48
|
+
|
|
49
|
+
if (config.registries[name]) {
|
|
50
|
+
logger.warn(`Registry '${name}' already exists. Use a different name.`)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
config.registries[name] = {
|
|
55
|
+
url,
|
|
56
|
+
version: options.version,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await writeOcxConfig(options.cwd, config)
|
|
60
|
+
|
|
61
|
+
if (options.json) {
|
|
62
|
+
outputJson({ success: true, data: { name, url } })
|
|
63
|
+
} else {
|
|
64
|
+
logger.success(`Added registry: ${name} -> ${url}`)
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
handleError(error)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// registry remove
|
|
72
|
+
registry
|
|
73
|
+
.command("remove")
|
|
74
|
+
.description("Remove a registry")
|
|
75
|
+
.argument("<name>", "Registry name")
|
|
76
|
+
.option("--cwd <path>", "Working directory", process.cwd())
|
|
77
|
+
.option("--json", "Output as JSON", false)
|
|
78
|
+
.option("-q, --quiet", "Suppress output", false)
|
|
79
|
+
.action(async (name: string, options: RegistryOptions) => {
|
|
80
|
+
try {
|
|
81
|
+
let config = await readOcxConfig(options.cwd)
|
|
82
|
+
if (!config) {
|
|
83
|
+
logger.error("No ocx.jsonc found. Run 'ocx init' first.")
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (config.lockRegistries) {
|
|
88
|
+
logger.error("Registries are locked. Cannot remove.")
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!config.registries[name]) {
|
|
93
|
+
logger.warn(`Registry '${name}' not found.`)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
delete config.registries[name]
|
|
98
|
+
await writeOcxConfig(options.cwd, config)
|
|
99
|
+
|
|
100
|
+
if (options.json) {
|
|
101
|
+
outputJson({ success: true, data: { removed: name } })
|
|
102
|
+
} else {
|
|
103
|
+
logger.success(`Removed registry: ${name}`)
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
handleError(error)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// registry list
|
|
111
|
+
registry
|
|
112
|
+
.command("list")
|
|
113
|
+
.description("List configured registries")
|
|
114
|
+
.option("--cwd <path>", "Working directory", process.cwd())
|
|
115
|
+
.option("--json", "Output as JSON", false)
|
|
116
|
+
.option("-q, --quiet", "Suppress output", false)
|
|
117
|
+
.action(async (options: RegistryOptions) => {
|
|
118
|
+
try {
|
|
119
|
+
const config = await readOcxConfig(options.cwd)
|
|
120
|
+
if (!config) {
|
|
121
|
+
logger.warn("No ocx.jsonc found. Run 'ocx init' first.")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const registries = Object.entries(config.registries).map(([name, cfg]) => ({
|
|
126
|
+
name,
|
|
127
|
+
url: cfg.url,
|
|
128
|
+
version: cfg.version || "latest",
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
if (options.json) {
|
|
132
|
+
outputJson({
|
|
133
|
+
success: true,
|
|
134
|
+
data: {
|
|
135
|
+
registries,
|
|
136
|
+
locked: config.lockRegistries,
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
} else {
|
|
140
|
+
if (registries.length === 0) {
|
|
141
|
+
logger.info("No registries configured.")
|
|
142
|
+
} else {
|
|
143
|
+
logger.info(`Configured registries${config.lockRegistries ? kleur.yellow(" (locked)") : ""}:`)
|
|
144
|
+
for (const reg of registries) {
|
|
145
|
+
console.log(` ${kleur.cyan(reg.name)}: ${reg.url} ${kleur.dim(`(${reg.version})`)}`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
handleError(error)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search/List Command
|
|
3
|
+
*
|
|
4
|
+
* Search for components across registries or list installed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from "commander"
|
|
8
|
+
import kleur from "kleur"
|
|
9
|
+
import fuzzysort from "fuzzysort"
|
|
10
|
+
import { readOcxConfig, readOcxLock } from "../schemas/config.js"
|
|
11
|
+
import { fetchRegistryIndex } from "../registry/fetcher.js"
|
|
12
|
+
import { logger, handleError, outputJson, createSpinner } from "../utils/index.js"
|
|
13
|
+
|
|
14
|
+
interface SearchOptions {
|
|
15
|
+
cwd: string
|
|
16
|
+
json: boolean
|
|
17
|
+
quiet: boolean
|
|
18
|
+
verbose: boolean
|
|
19
|
+
installed: boolean
|
|
20
|
+
limit: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerSearchCommand(program: Command): void {
|
|
24
|
+
program
|
|
25
|
+
.command("search")
|
|
26
|
+
.alias("list")
|
|
27
|
+
.description("Search for components across registries or list installed")
|
|
28
|
+
.argument("[query]", "Search query")
|
|
29
|
+
.option("--cwd <path>", "Working directory", process.cwd())
|
|
30
|
+
.option("--json", "Output as JSON", false)
|
|
31
|
+
.option("-q, --quiet", "Suppress output", false)
|
|
32
|
+
.option("-v, --verbose", "Verbose output", false)
|
|
33
|
+
.option("-i, --installed", "List installed components only", false)
|
|
34
|
+
.option("-l, --limit <n>", "Limit results", "20")
|
|
35
|
+
.action(async (query: string | undefined, options: SearchOptions) => {
|
|
36
|
+
try {
|
|
37
|
+
const limit = parseInt(String(options.limit), 10)
|
|
38
|
+
|
|
39
|
+
// List installed only
|
|
40
|
+
if (options.installed) {
|
|
41
|
+
const lock = await readOcxLock(options.cwd)
|
|
42
|
+
if (!lock) {
|
|
43
|
+
if (options.json) {
|
|
44
|
+
outputJson({ success: true, data: { components: [] } })
|
|
45
|
+
} else {
|
|
46
|
+
logger.info("No components installed.")
|
|
47
|
+
}
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const installed = Object.entries(lock.installed).map(([name, info]) => ({
|
|
52
|
+
name,
|
|
53
|
+
registry: info.registry,
|
|
54
|
+
version: info.version,
|
|
55
|
+
installedAt: info.installedAt,
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
if (options.json) {
|
|
59
|
+
outputJson({ success: true, data: { components: installed } })
|
|
60
|
+
} else {
|
|
61
|
+
logger.info(`Installed components (${installed.length}):`)
|
|
62
|
+
for (const comp of installed) {
|
|
63
|
+
console.log(
|
|
64
|
+
` ${kleur.cyan(comp.name)} ${kleur.dim(`v${comp.version}`)} from ${comp.registry}`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Search across registries
|
|
72
|
+
const config = await readOcxConfig(options.cwd)
|
|
73
|
+
if (!config) {
|
|
74
|
+
logger.warn("No ocx.jsonc found. Run 'ocx init' first.")
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.verbose) {
|
|
79
|
+
logger.info(`Searching in ${Object.keys(config.registries).length} registries...`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allComponents: Array<{
|
|
83
|
+
name: string
|
|
84
|
+
description: string
|
|
85
|
+
type: string
|
|
86
|
+
registry: string
|
|
87
|
+
}> = []
|
|
88
|
+
|
|
89
|
+
const spinner = createSpinner({
|
|
90
|
+
text: "Searching registries...",
|
|
91
|
+
quiet: options.quiet || options.verbose,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
if (!options.json && !options.verbose) {
|
|
95
|
+
spinner.start()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const [registryName, registryConfig] of Object.entries(config.registries)) {
|
|
99
|
+
try {
|
|
100
|
+
if (options.verbose) {
|
|
101
|
+
logger.info(`Fetching index from ${registryName} (${registryConfig.url})...`)
|
|
102
|
+
}
|
|
103
|
+
const index = await fetchRegistryIndex(registryConfig.url)
|
|
104
|
+
if (options.verbose) {
|
|
105
|
+
logger.info(`Found ${index.components.length} components in ${registryName}`)
|
|
106
|
+
}
|
|
107
|
+
for (const comp of index.components) {
|
|
108
|
+
allComponents.push({
|
|
109
|
+
name: comp.name,
|
|
110
|
+
description: comp.description,
|
|
111
|
+
type: comp.type,
|
|
112
|
+
registry: registryName,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (options.verbose) {
|
|
117
|
+
logger.warn(
|
|
118
|
+
`Failed to fetch registry ${registryName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
// Skip failed registries
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!options.json && !options.verbose) {
|
|
126
|
+
spinner.stop()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Filter by query if provided
|
|
130
|
+
let results = allComponents
|
|
131
|
+
if (query) {
|
|
132
|
+
const fuzzyResults = fuzzysort.go(query, allComponents, {
|
|
133
|
+
keys: ["name", "description"],
|
|
134
|
+
limit,
|
|
135
|
+
})
|
|
136
|
+
results = fuzzyResults.map((r) => r.obj)
|
|
137
|
+
} else {
|
|
138
|
+
results = results.slice(0, limit)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.json) {
|
|
142
|
+
outputJson({ success: true, data: { components: results } })
|
|
143
|
+
} else {
|
|
144
|
+
if (results.length === 0) {
|
|
145
|
+
logger.info("No components found.")
|
|
146
|
+
} else {
|
|
147
|
+
logger.info(`Found ${results.length} components:`)
|
|
148
|
+
for (const comp of results) {
|
|
149
|
+
console.log(
|
|
150
|
+
` ${kleur.cyan(comp.name)} ${kleur.dim(`(${comp.type})`)} - ${comp.description}`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
handleError(error, { json: options.json })
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCX URL Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized URL definitions to avoid hardcoding throughout the codebase.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Base domains
|
|
8
|
+
export const OCX_DOMAIN = "ocx.kdco.dev"
|
|
9
|
+
export const GITHUB_REPO = "kdcokenny/ocx"
|
|
10
|
+
|
|
11
|
+
// OCX URLs
|
|
12
|
+
export const OCX_SCHEMA_URL = `https://${OCX_DOMAIN}/schema.json`
|
|
13
|
+
export const OCX_LOCK_SCHEMA_URL = `https://${OCX_DOMAIN}/lock.schema.json`
|
|
14
|
+
export const OCX_INSTALL_URL = `https://${OCX_DOMAIN}/install.sh`
|
|
15
|
+
|
|
16
|
+
// GitHub URLs
|
|
17
|
+
export const GITHUB_RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases`
|
|
18
|
+
export const GITHUB_RAW_URL = `https://raw.githubusercontent.com/${GITHUB_REPO}/main`
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* OCX CLI - OpenCode Extensions
|
|
4
|
+
*
|
|
5
|
+
* A ShadCN-style CLI for installing agents, skills, plugins, and commands
|
|
6
|
+
* into OpenCode projects.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from "commander"
|
|
10
|
+
import { registerInitCommand } from "./commands/init.js"
|
|
11
|
+
import { registerAddCommand } from "./commands/add.js"
|
|
12
|
+
import { registerDiffCommand } from "./commands/diff.js"
|
|
13
|
+
import { registerSearchCommand } from "./commands/search.js"
|
|
14
|
+
import { registerRegistryCommand } from "./commands/registry.js"
|
|
15
|
+
import { registerBuildCommand } from "./commands/build.js"
|
|
16
|
+
import { handleError } from "./utils/index.js"
|
|
17
|
+
|
|
18
|
+
// Version injected at build time
|
|
19
|
+
declare const __VERSION__: string
|
|
20
|
+
const version = typeof __VERSION__ !== "undefined" ? __VERSION__ : "0.0.0-dev"
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const program = new Command()
|
|
24
|
+
.name("ocx")
|
|
25
|
+
.description("OpenCode Extensions - Install agents, skills, plugins, and commands")
|
|
26
|
+
.version(version)
|
|
27
|
+
|
|
28
|
+
// Register all commands using the registration pattern
|
|
29
|
+
registerInitCommand(program)
|
|
30
|
+
registerAddCommand(program)
|
|
31
|
+
registerDiffCommand(program)
|
|
32
|
+
registerSearchCommand(program)
|
|
33
|
+
registerRegistryCommand(program)
|
|
34
|
+
registerBuildCommand(program)
|
|
35
|
+
|
|
36
|
+
// Parse and handle errors
|
|
37
|
+
await program.parseAsync(process.argv)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main().catch((err) => {
|
|
41
|
+
handleError(err)
|
|
42
|
+
})
|