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/src/utils/spinner.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spinner utility for async operations
|
|
3
|
-
* Uses ora, disabled in CI/non-TTY environments
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import ora, { type Ora } from "ora"
|
|
7
|
-
import { isCI, isTTY } from "./env"
|
|
8
|
-
|
|
9
|
-
export interface SpinnerOptions {
|
|
10
|
-
text: string
|
|
11
|
-
quiet?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Create a spinner that works in TTY, falls back gracefully in CI
|
|
16
|
-
*/
|
|
17
|
-
export function createSpinner(options: SpinnerOptions): Ora {
|
|
18
|
-
const shouldSpin = isTTY && !options.quiet
|
|
19
|
-
|
|
20
|
-
const spinner = ora({
|
|
21
|
-
text: options.text,
|
|
22
|
-
isSilent: !shouldSpin,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
return spinner
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Run an async function with a spinner
|
|
30
|
-
*/
|
|
31
|
-
export async function withSpinner<T>(
|
|
32
|
-
options: SpinnerOptions,
|
|
33
|
-
fn: (spinner: Ora) => Promise<T>,
|
|
34
|
-
): Promise<T> {
|
|
35
|
-
const spinner = createSpinner(options)
|
|
36
|
-
spinner.start()
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const result = await fn(spinner)
|
|
40
|
-
spinner.succeed()
|
|
41
|
-
return result
|
|
42
|
-
} catch (error) {
|
|
43
|
-
spinner.fail()
|
|
44
|
-
throw error
|
|
45
|
-
}
|
|
46
|
-
}
|
package/tests/add.test.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll, afterEach } from "bun:test"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { createTempDir, cleanupTempDir, runCLI, stripJsonc } from "./helpers"
|
|
6
|
-
import { startMockRegistry, type MockRegistry } from "./mock-registry"
|
|
7
|
-
|
|
8
|
-
describe("ocx add", () => {
|
|
9
|
-
let testDir: string
|
|
10
|
-
let registry: MockRegistry
|
|
11
|
-
|
|
12
|
-
beforeAll(() => {
|
|
13
|
-
registry = startMockRegistry()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
afterAll(() => {
|
|
17
|
-
registry.stop()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
if (testDir) {
|
|
22
|
-
await cleanupTempDir(testDir)
|
|
23
|
-
}
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it("should fail if not initialized", async () => {
|
|
27
|
-
testDir = await createTempDir("add-no-init")
|
|
28
|
-
const { exitCode, output } = await runCLI(["add", "test-comp"], testDir)
|
|
29
|
-
expect(exitCode).not.toBe(0)
|
|
30
|
-
expect(output).toContain("Run 'ocx init' first")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it("should install a component and its dependencies", async () => {
|
|
34
|
-
testDir = await createTempDir("add-basic")
|
|
35
|
-
|
|
36
|
-
// Init and add registry
|
|
37
|
-
await runCLI(["init", "--yes"], testDir)
|
|
38
|
-
|
|
39
|
-
// Manually add registry to config since 'ocx registry add' might be flaky in parallel tests
|
|
40
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
41
|
-
const config = JSON.parse(stripJsonc(await readFile(configPath, "utf-8")))
|
|
42
|
-
config.registries = {
|
|
43
|
-
test: { url: registry.url },
|
|
44
|
-
}
|
|
45
|
-
await writeFile(configPath, JSON.stringify(config, null, 2))
|
|
46
|
-
|
|
47
|
-
// Install agent which depends on skill which depends on plugin
|
|
48
|
-
const { exitCode, output } = await runCLI(["add", "kdco-test-agent", "--yes"], testDir)
|
|
49
|
-
|
|
50
|
-
if (exitCode !== 0) {
|
|
51
|
-
console.log(output)
|
|
52
|
-
}
|
|
53
|
-
expect(exitCode).toBe(0)
|
|
54
|
-
|
|
55
|
-
// Verify files
|
|
56
|
-
expect(existsSync(join(testDir, ".opencode/agent/kdco-test-agent.md"))).toBe(true)
|
|
57
|
-
expect(existsSync(join(testDir, ".opencode/skill/kdco-test-skill/SKILL.md"))).toBe(true)
|
|
58
|
-
expect(existsSync(join(testDir, ".opencode/plugin/kdco-test-plugin.ts"))).toBe(true)
|
|
59
|
-
|
|
60
|
-
// Verify lock file
|
|
61
|
-
const lockPath = join(testDir, "ocx.lock")
|
|
62
|
-
expect(existsSync(lockPath)).toBe(true)
|
|
63
|
-
const lock = JSON.parse(stripJsonc(await readFile(lockPath, "utf-8")))
|
|
64
|
-
expect(lock.installed["kdco-test-agent"]).toBeDefined()
|
|
65
|
-
expect(lock.installed["kdco-test-skill"]).toBeDefined()
|
|
66
|
-
expect(lock.installed["kdco-test-plugin"]).toBeDefined()
|
|
67
|
-
|
|
68
|
-
// Verify opencode.json patching
|
|
69
|
-
const opencodePath = join(testDir, "opencode.json")
|
|
70
|
-
expect(existsSync(opencodePath)).toBe(true)
|
|
71
|
-
const opencode = JSON.parse(await readFile(opencodePath, "utf-8"))
|
|
72
|
-
expect(opencode.mcp["test-mcp"]).toBeDefined()
|
|
73
|
-
expect(opencode.mcp["test-mcp"].url).toBe("https://mcp.test.com")
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it("should fail if integrity check fails", async () => {
|
|
77
|
-
testDir = await createTempDir("add-integrity-fail")
|
|
78
|
-
|
|
79
|
-
// Init and add registry
|
|
80
|
-
await runCLI(["init", "--yes"], testDir)
|
|
81
|
-
|
|
82
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
83
|
-
const config = JSON.parse(stripJsonc(await readFile(configPath, "utf-8")))
|
|
84
|
-
config.registries = {
|
|
85
|
-
test: { url: registry.url },
|
|
86
|
-
}
|
|
87
|
-
await writeFile(configPath, JSON.stringify(config, null, 2))
|
|
88
|
-
|
|
89
|
-
// 1. Install normally to create lock entry
|
|
90
|
-
await runCLI(["add", "kdco-test-plugin", "--yes"], testDir)
|
|
91
|
-
|
|
92
|
-
// 2. Tamper with the registry content
|
|
93
|
-
registry.setFileContent("kdco-test-plugin", "index.ts", "TAMPERED CONTENT")
|
|
94
|
-
|
|
95
|
-
// 3. Try to add again (should fail integrity check)
|
|
96
|
-
const { exitCode, output } = await runCLI(["add", "kdco-test-plugin", "--yes"], testDir)
|
|
97
|
-
|
|
98
|
-
expect(exitCode).not.toBe(0)
|
|
99
|
-
expect(output).toContain("Integrity verification failed")
|
|
100
|
-
expect(output).toContain("The registry content has changed since this component was locked")
|
|
101
|
-
})
|
|
102
|
-
})
|
package/tests/build.test.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { createTempDir, cleanupTempDir, runCLI, stripJsonc } from "./helpers"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { existsSync } from "node:fs"
|
|
5
|
-
import { mkdir, writeFile, readFile } from "node:fs/promises"
|
|
6
|
-
|
|
7
|
-
describe("ocx build", () => {
|
|
8
|
-
let testDir: string
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
testDir = await createTempDir("build-test")
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await cleanupTempDir(testDir)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it("should build a valid registry from source", async () => {
|
|
19
|
-
// Create registry source
|
|
20
|
-
const sourceDir = join(testDir, "registry")
|
|
21
|
-
await mkdir(sourceDir, { recursive: true })
|
|
22
|
-
|
|
23
|
-
const registryJson = {
|
|
24
|
-
name: "Test Registry",
|
|
25
|
-
prefix: "kdco",
|
|
26
|
-
version: "1.0.0",
|
|
27
|
-
author: "Test Author",
|
|
28
|
-
components: [
|
|
29
|
-
{
|
|
30
|
-
name: "kdco-comp-1",
|
|
31
|
-
type: "ocx:plugin",
|
|
32
|
-
description: "Test component 1",
|
|
33
|
-
files: [{ path: "index.ts", target: ".opencode/plugin/kdco-comp-1.ts" }],
|
|
34
|
-
dependencies: [],
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: "kdco-comp-2",
|
|
38
|
-
type: "ocx:agent",
|
|
39
|
-
description: "Test component 2",
|
|
40
|
-
files: [{ path: "agent.md", target: ".opencode/agent/kdco-comp-2.md" }],
|
|
41
|
-
dependencies: ["kdco-comp-1"],
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
await writeFile(join(sourceDir, "registry.json"), JSON.stringify(registryJson, null, 2))
|
|
47
|
-
|
|
48
|
-
// Create the files directory and source files
|
|
49
|
-
const filesDir = join(sourceDir, "files")
|
|
50
|
-
await mkdir(filesDir, { recursive: true })
|
|
51
|
-
await writeFile(join(filesDir, "index.ts"), "// Test plugin content")
|
|
52
|
-
await writeFile(join(filesDir, "agent.md"), "# Test agent content")
|
|
53
|
-
|
|
54
|
-
// Run build
|
|
55
|
-
const outDir = "dist"
|
|
56
|
-
const { exitCode, output } = await runCLI(["build", "registry", "--out", outDir], testDir)
|
|
57
|
-
|
|
58
|
-
if (exitCode !== 0) {
|
|
59
|
-
console.log(output)
|
|
60
|
-
}
|
|
61
|
-
expect(exitCode).toBe(0)
|
|
62
|
-
expect(output).toContain("Built 2 components")
|
|
63
|
-
|
|
64
|
-
// Verify output files
|
|
65
|
-
const fullOutDir = join(testDir, outDir)
|
|
66
|
-
expect(existsSync(join(fullOutDir, "index.json"))).toBe(true)
|
|
67
|
-
expect(existsSync(join(fullOutDir, "components", "kdco-comp-1.json"))).toBe(true)
|
|
68
|
-
expect(existsSync(join(fullOutDir, "components", "kdco-comp-2.json"))).toBe(true)
|
|
69
|
-
|
|
70
|
-
// Verify index.json content
|
|
71
|
-
const index = JSON.parse(await readFile(join(fullOutDir, "index.json"), "utf-8"))
|
|
72
|
-
expect(index.name).toBe("Test Registry")
|
|
73
|
-
expect(index.components.length).toBe(2)
|
|
74
|
-
expect(index.components[0].name).toBe("kdco-comp-1")
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it("should fail if component prefix is missing", async () => {
|
|
78
|
-
const sourceDir = join(testDir, "registry-invalid")
|
|
79
|
-
await mkdir(sourceDir, { recursive: true })
|
|
80
|
-
|
|
81
|
-
const registryJson = {
|
|
82
|
-
name: "Invalid Registry",
|
|
83
|
-
prefix: "kdco",
|
|
84
|
-
version: "1.0.0",
|
|
85
|
-
author: "Test Author",
|
|
86
|
-
components: [
|
|
87
|
-
{
|
|
88
|
-
name: "wrong-prefix",
|
|
89
|
-
type: "ocx:plugin",
|
|
90
|
-
description: "Invalid component",
|
|
91
|
-
files: [{ path: "index.ts", target: ".opencode/plugin/wrong-prefix.ts" }],
|
|
92
|
-
dependencies: [],
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await writeFile(join(sourceDir, "registry.json"), JSON.stringify(registryJson, null, 2))
|
|
98
|
-
|
|
99
|
-
const { exitCode, output } = await runCLI(["build", "registry-invalid"], testDir)
|
|
100
|
-
|
|
101
|
-
expect(exitCode).not.toBe(0)
|
|
102
|
-
// Match the actual Zod error message
|
|
103
|
-
expect(output).toContain("All component names must start with the registry prefix")
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it("should fail on missing dependencies", async () => {
|
|
107
|
-
const sourceDir = join(testDir, "registry-missing-dep")
|
|
108
|
-
await mkdir(sourceDir, { recursive: true })
|
|
109
|
-
|
|
110
|
-
const registryJson = {
|
|
111
|
-
name: "Missing Dep Registry",
|
|
112
|
-
prefix: "kdco",
|
|
113
|
-
version: "1.0.0",
|
|
114
|
-
author: "Test Author",
|
|
115
|
-
components: [
|
|
116
|
-
{
|
|
117
|
-
name: "kdco-comp",
|
|
118
|
-
type: "ocx:plugin",
|
|
119
|
-
description: "Component with missing dep",
|
|
120
|
-
files: [{ path: "index.ts", target: ".opencode/plugin/kdco-comp.ts" }],
|
|
121
|
-
dependencies: ["kdco-non-existent"],
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
await writeFile(join(sourceDir, "registry.json"), JSON.stringify(registryJson, null, 2))
|
|
127
|
-
|
|
128
|
-
const { exitCode, output } = await runCLI(["build", "registry-missing-dep"], testDir)
|
|
129
|
-
|
|
130
|
-
expect(exitCode).not.toBe(0)
|
|
131
|
-
// Match the actual Zod error message
|
|
132
|
-
expect(output).toContain(
|
|
133
|
-
"All dependencies must reference components that exist in the registry",
|
|
134
|
-
)
|
|
135
|
-
})
|
|
136
|
-
})
|
package/tests/diff.test.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { createTempDir, cleanupTempDir, runCLI } from "./helpers"
|
|
3
|
-
import { startMockRegistry, type MockRegistry } from "./mock-registry"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { writeFile, mkdir } from "node:fs/promises"
|
|
6
|
-
|
|
7
|
-
describe("ocx diff", () => {
|
|
8
|
-
let testDir: string
|
|
9
|
-
let registry: MockRegistry
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
testDir = await createTempDir("diff-test")
|
|
13
|
-
registry = startMockRegistry()
|
|
14
|
-
await runCLI(["init", "--yes"], testDir)
|
|
15
|
-
await runCLI(["registry", "add", registry.url, "--name", "test-reg"], testDir)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
afterEach(async () => {
|
|
19
|
-
registry.stop()
|
|
20
|
-
await cleanupTempDir(testDir)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("should show no changes when local matches upstream", async () => {
|
|
24
|
-
// Mock an install
|
|
25
|
-
await runCLI(["add", "kdco-test-plugin", "--yes"], testDir)
|
|
26
|
-
|
|
27
|
-
const { exitCode, output } = await runCLI(["diff", "kdco-test-plugin"], testDir)
|
|
28
|
-
|
|
29
|
-
expect(exitCode).toBe(0)
|
|
30
|
-
expect(output).toContain("No changes")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it("should detect changes when local file is modified", async () => {
|
|
34
|
-
// Mock an install
|
|
35
|
-
await runCLI(["add", "kdco-test-plugin", "--yes"], testDir)
|
|
36
|
-
|
|
37
|
-
// Modify the local file
|
|
38
|
-
const pluginPath = join(testDir, ".opencode/plugin/kdco-test-plugin.ts")
|
|
39
|
-
await writeFile(pluginPath, "console.log('modified')")
|
|
40
|
-
|
|
41
|
-
const { exitCode, output } = await runCLI(["diff", "kdco-test-plugin"], testDir)
|
|
42
|
-
|
|
43
|
-
expect(exitCode).toBe(0)
|
|
44
|
-
expect(output).toContain("Diff for kdco-test-plugin")
|
|
45
|
-
expect(output).toContain("+console.log('modified')")
|
|
46
|
-
})
|
|
47
|
-
})
|
package/tests/helpers.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path"
|
|
2
|
-
import { mkdir, rm } from "node:fs/promises"
|
|
3
|
-
import { existsSync } from "node:fs"
|
|
4
|
-
|
|
5
|
-
export interface CLIResult {
|
|
6
|
-
stdout: string
|
|
7
|
-
stderr: string
|
|
8
|
-
output: string
|
|
9
|
-
exitCode: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function createTempDir(prefix: string): Promise<string> {
|
|
13
|
-
const path = join(
|
|
14
|
-
process.cwd(),
|
|
15
|
-
"tests/fixtures",
|
|
16
|
-
`tmp-${prefix}-${Math.random().toString(36).slice(2)}`,
|
|
17
|
-
)
|
|
18
|
-
await mkdir(path, { recursive: true })
|
|
19
|
-
return path
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function cleanupTempDir(path: string): Promise<void> {
|
|
23
|
-
if (existsSync(path)) {
|
|
24
|
-
await rm(path, { recursive: true, force: true })
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Strips JSONC comments for parsing in tests
|
|
30
|
-
* Minimal version that avoids breaking URLs
|
|
31
|
-
*/
|
|
32
|
-
export function stripJsonc(content: string): string {
|
|
33
|
-
return content
|
|
34
|
-
.split("\n")
|
|
35
|
-
.map((line) => {
|
|
36
|
-
const trimmed = line.trim()
|
|
37
|
-
if (trimmed.startsWith("//")) return ""
|
|
38
|
-
// This is still naive but better for our tests which don't use complex JSONC
|
|
39
|
-
return line
|
|
40
|
-
})
|
|
41
|
-
.join("\n")
|
|
42
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function runCLI(args: string[], cwd: string): Promise<CLIResult> {
|
|
46
|
-
const indexPath = join(process.cwd(), "packages/cli/src/index.ts")
|
|
47
|
-
|
|
48
|
-
const proc = Bun.spawn(["bun", "run", indexPath, ...args], {
|
|
49
|
-
cwd,
|
|
50
|
-
env: {
|
|
51
|
-
...process.env,
|
|
52
|
-
FORCE_COLOR: "0",
|
|
53
|
-
},
|
|
54
|
-
stdout: "pipe",
|
|
55
|
-
stderr: "pipe",
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const stdout = await new Response(proc.stdout).text()
|
|
59
|
-
const stderr = await new Response(proc.stderr).text()
|
|
60
|
-
const exitCode = await proc.exited
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
stdout,
|
|
64
|
-
stderr,
|
|
65
|
-
output: stdout + stderr,
|
|
66
|
-
exitCode,
|
|
67
|
-
}
|
|
68
|
-
}
|
package/tests/init.test.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "bun:test"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
3
|
-
import { readFile } from "node:fs/promises"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { createTempDir, cleanupTempDir, runCLI, stripJsonc } from "./helpers"
|
|
6
|
-
|
|
7
|
-
describe("ocx init", () => {
|
|
8
|
-
let testDir: string
|
|
9
|
-
|
|
10
|
-
afterEach(async () => {
|
|
11
|
-
if (testDir) {
|
|
12
|
-
await cleanupTempDir(testDir)
|
|
13
|
-
}
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it("should create ocx.jsonc with default config", async () => {
|
|
17
|
-
testDir = await createTempDir("init-basic")
|
|
18
|
-
const { exitCode, output } = await runCLI(["init", "--yes"], testDir)
|
|
19
|
-
|
|
20
|
-
expect(exitCode).toBe(0)
|
|
21
|
-
// Success message from logger.success
|
|
22
|
-
expect(output).toContain("Initialized OCX configuration")
|
|
23
|
-
|
|
24
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
25
|
-
expect(existsSync(configPath)).toBe(true)
|
|
26
|
-
|
|
27
|
-
const content = await readFile(configPath, "utf-8")
|
|
28
|
-
const config = JSON.parse(stripJsonc(content))
|
|
29
|
-
expect(config.registries).toBeDefined()
|
|
30
|
-
expect(config.lockRegistries).toBe(false)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it("should warn if ocx.jsonc already exists", async () => {
|
|
34
|
-
testDir = await createTempDir("init-exists")
|
|
35
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
36
|
-
await Bun.write(configPath, "{}")
|
|
37
|
-
|
|
38
|
-
const { exitCode, output } = await runCLI(["init"], testDir)
|
|
39
|
-
expect(exitCode).toBe(0)
|
|
40
|
-
expect(output).toContain("ocx.jsonc already exists")
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it("should output JSON when requested", async () => {
|
|
44
|
-
testDir = await createTempDir("init-json")
|
|
45
|
-
const { exitCode, output } = await runCLI(["init", "--yes", "--json"], testDir)
|
|
46
|
-
|
|
47
|
-
expect(exitCode).toBe(0)
|
|
48
|
-
const json = JSON.parse(output)
|
|
49
|
-
expect(json.success).toBe(true)
|
|
50
|
-
expect(json.path).toContain("ocx.jsonc")
|
|
51
|
-
})
|
|
52
|
-
})
|
package/tests/mock-registry.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { type Server } from "bun"
|
|
2
|
-
|
|
3
|
-
export interface MockRegistry {
|
|
4
|
-
server: Server
|
|
5
|
-
url: string
|
|
6
|
-
stop: () => void
|
|
7
|
-
setFileContent: (componentName: string, fileName: string, content: string) => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Start a mock HTTP registry server for testing
|
|
12
|
-
*/
|
|
13
|
-
export function startMockRegistry(): MockRegistry {
|
|
14
|
-
const customFiles = new Map<string, string>()
|
|
15
|
-
|
|
16
|
-
const components = {
|
|
17
|
-
"kdco-test-plugin": {
|
|
18
|
-
name: "kdco-test-plugin",
|
|
19
|
-
type: "ocx:plugin",
|
|
20
|
-
description: "A test plugin",
|
|
21
|
-
files: [{ path: "index.ts", target: ".opencode/plugin/kdco-test-plugin.ts" }],
|
|
22
|
-
dependencies: [],
|
|
23
|
-
},
|
|
24
|
-
"kdco-test-skill": {
|
|
25
|
-
name: "kdco-test-skill",
|
|
26
|
-
type: "ocx:skill",
|
|
27
|
-
description: "A test skill",
|
|
28
|
-
files: [{ path: "SKILL.md", target: ".opencode/skill/kdco-test-skill/SKILL.md" }],
|
|
29
|
-
dependencies: ["kdco-test-plugin"],
|
|
30
|
-
},
|
|
31
|
-
"kdco-test-agent": {
|
|
32
|
-
name: "kdco-test-agent",
|
|
33
|
-
type: "ocx:agent",
|
|
34
|
-
description: "A test agent",
|
|
35
|
-
files: [{ path: "agent.md", target: ".opencode/agent/kdco-test-agent.md" }],
|
|
36
|
-
dependencies: ["kdco-test-skill"],
|
|
37
|
-
mcpServers: {
|
|
38
|
-
"test-mcp": {
|
|
39
|
-
type: "remote",
|
|
40
|
-
url: "https://mcp.test.com",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const server = Bun.serve({
|
|
47
|
-
port: 0, // Random port
|
|
48
|
-
fetch(req) {
|
|
49
|
-
const url = new URL(req.url)
|
|
50
|
-
const path = url.pathname
|
|
51
|
-
|
|
52
|
-
if (path === "/index.json") {
|
|
53
|
-
return Response.json({
|
|
54
|
-
name: "Test Registry",
|
|
55
|
-
prefix: "kdco",
|
|
56
|
-
version: "1.0.0",
|
|
57
|
-
author: "Test Author",
|
|
58
|
-
components: Object.values(components).map((c) => ({
|
|
59
|
-
name: c.name,
|
|
60
|
-
type: c.type,
|
|
61
|
-
description: c.description,
|
|
62
|
-
})),
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const componentMatch = path.match(/^\/components\/(.+)\.json$/)
|
|
67
|
-
if (componentMatch) {
|
|
68
|
-
const name = componentMatch[1]
|
|
69
|
-
const component = components[name as keyof typeof components]
|
|
70
|
-
if (component) {
|
|
71
|
-
return Response.json({
|
|
72
|
-
name: component.name,
|
|
73
|
-
"dist-tags": {
|
|
74
|
-
latest: "1.0.0",
|
|
75
|
-
},
|
|
76
|
-
versions: {
|
|
77
|
-
"1.0.0": component,
|
|
78
|
-
},
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const fileMatch = path.match(/^\/components\/(.+)\/(.+)$/)
|
|
84
|
-
if (fileMatch) {
|
|
85
|
-
const [, name, filePath] = fileMatch
|
|
86
|
-
const customKey = `${name}:${filePath}`
|
|
87
|
-
if (customFiles.has(customKey)) {
|
|
88
|
-
return new Response(customFiles.get(customKey))
|
|
89
|
-
}
|
|
90
|
-
return new Response(`Content of ${filePath} for ${name}`)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return new Response("Not Found", { status: 404 })
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
server,
|
|
99
|
-
url: `http://localhost:${server.port}`,
|
|
100
|
-
stop: () => server.stop(),
|
|
101
|
-
setFileContent: (componentName: string, fileName: string, content: string) => {
|
|
102
|
-
customFiles.set(`${componentName}:${fileName}`, content)
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
}
|
package/tests/registry.test.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { createTempDir, cleanupTempDir, runCLI, stripJsonc } from "./helpers"
|
|
3
|
-
import { startMockRegistry, type MockRegistry } from "./mock-registry"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { existsSync } from "node:fs"
|
|
6
|
-
|
|
7
|
-
describe("ocx registry", () => {
|
|
8
|
-
let testDir: string
|
|
9
|
-
let registry: MockRegistry
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
testDir = await createTempDir("registry-test")
|
|
13
|
-
registry = startMockRegistry()
|
|
14
|
-
await runCLI(["init", "--yes"], testDir)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(async () => {
|
|
18
|
-
registry.stop()
|
|
19
|
-
await cleanupTempDir(testDir)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it("should add a registry", async () => {
|
|
23
|
-
const { exitCode, output } = await runCLI(
|
|
24
|
-
["registry", "add", registry.url, "--name", "test-reg"],
|
|
25
|
-
testDir,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
if (exitCode !== 0) {
|
|
29
|
-
console.log(output)
|
|
30
|
-
}
|
|
31
|
-
expect(exitCode).toBe(0)
|
|
32
|
-
expect(output).toContain("Added registry: test-reg")
|
|
33
|
-
|
|
34
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
35
|
-
const configContent = await Bun.file(configPath).text()
|
|
36
|
-
const config = JSON.parse(stripJsonc(configContent))
|
|
37
|
-
expect(config.registries["test-reg"]).toBeDefined()
|
|
38
|
-
expect(config.registries["test-reg"].url).toBe(registry.url)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it("should list configured registries", async () => {
|
|
42
|
-
await runCLI(["registry", "add", registry.url, "--name", "test-reg"], testDir)
|
|
43
|
-
|
|
44
|
-
const { exitCode, output } = await runCLI(["registry", "list"], testDir)
|
|
45
|
-
|
|
46
|
-
expect(exitCode).toBe(0)
|
|
47
|
-
expect(output).toContain("test-reg")
|
|
48
|
-
expect(output).toContain(registry.url)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it("should remove a registry", async () => {
|
|
52
|
-
await runCLI(["registry", "add", registry.url, "--name", "test-reg"], testDir)
|
|
53
|
-
|
|
54
|
-
const { exitCode, output } = await runCLI(["registry", "remove", "test-reg"], testDir)
|
|
55
|
-
|
|
56
|
-
expect(exitCode).toBe(0)
|
|
57
|
-
expect(output).toContain("Removed registry: test-reg")
|
|
58
|
-
|
|
59
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
60
|
-
const configContent = await Bun.file(configPath).text()
|
|
61
|
-
const config = JSON.parse(stripJsonc(configContent))
|
|
62
|
-
expect(config.registries["test-reg"]).toBeUndefined()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it("should fail if adding to locked registries", async () => {
|
|
66
|
-
// Manually lock registries
|
|
67
|
-
const configPath = join(testDir, "ocx.jsonc")
|
|
68
|
-
const configContent = await Bun.file(configPath).text()
|
|
69
|
-
const config = JSON.parse(stripJsonc(configContent))
|
|
70
|
-
config.lockRegistries = true
|
|
71
|
-
await Bun.write(configPath, JSON.stringify(config, null, 2))
|
|
72
|
-
|
|
73
|
-
const { exitCode, output } = await runCLI(["registry", "add", "http://example.com"], testDir)
|
|
74
|
-
|
|
75
|
-
expect(exitCode).not.toBe(0)
|
|
76
|
-
expect(output).toContain("Registries are locked")
|
|
77
|
-
})
|
|
78
|
-
})
|