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.
@@ -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
- })
@@ -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
- })
@@ -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
- }
@@ -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
- })
@@ -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
- }
@@ -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
- })