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,268 +0,0 @@
1
- /**
2
- * Registry & Component Schemas
3
- *
4
- * Zod schemas with fail-fast validation following the 5 Laws of Elegant Defense.
5
- * All validation errors include clear messages with examples of valid input.
6
- */
7
-
8
- import { z } from "zod"
9
-
10
- // =============================================================================
11
- // OPENCODE NAMING CONSTRAINTS (from OpenCode docs)
12
- // =============================================================================
13
-
14
- /**
15
- * OpenCode name schema following official constraints:
16
- * - 1-64 characters
17
- * - Lowercase alphanumeric with single hyphen separators
18
- * - Cannot start or end with hyphen
19
- * - Cannot contain consecutive hyphens
20
- *
21
- * Regex: ^[a-z0-9]+(-[a-z0-9]+)*$
22
- */
23
- export const openCodeNameSchema = z
24
- .string()
25
- .min(1, "Name cannot be empty")
26
- .max(64, "Name cannot exceed 64 characters")
27
- .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, {
28
- message:
29
- "Must be lowercase alphanumeric with single hyphen separators (e.g., 'my-component', 'kdco-plan'). Cannot start/end with hyphen or have consecutive hyphens.",
30
- })
31
-
32
- /**
33
- * Creates a component name schema that enforces registry prefix.
34
- * All components in a registry must start with the registry's prefix.
35
- */
36
- export const createComponentNameSchema = (prefix: string) =>
37
- openCodeNameSchema.refine((name) => name.startsWith(`${prefix}-`), {
38
- message: `Component name must start with registry prefix "${prefix}-" (e.g., "${prefix}-my-component")`,
39
- })
40
-
41
- // =============================================================================
42
- // FILE TARGET SCHEMAS
43
- // =============================================================================
44
-
45
- /**
46
- * Valid component types and their target directories
47
- */
48
- export const COMPONENT_TYPE_DIRS = {
49
- "ocx:agent": "agent",
50
- "ocx:skill": "skill",
51
- "ocx:plugin": "plugin",
52
- "ocx:command": "command",
53
- "ocx:tool": "tool",
54
- "ocx:bundle": null, // Bundles don't have a target directory
55
- } as const
56
-
57
- export const componentTypeSchema = z.enum([
58
- "ocx:agent",
59
- "ocx:skill",
60
- "ocx:plugin",
61
- "ocx:command",
62
- "ocx:tool",
63
- "ocx:bundle",
64
- ])
65
-
66
- export type ComponentType = z.infer<typeof componentTypeSchema>
67
-
68
- /**
69
- * Target path must be inside .opencode/ with valid subdirectory
70
- */
71
- export const targetPathSchema = z
72
- .string()
73
- .refine((path) => path.startsWith(".opencode/"), {
74
- message: 'Target path must start with ".opencode/"',
75
- })
76
- .refine(
77
- (path) => {
78
- const parts = path.split("/")
79
- if (parts.length < 2) return false
80
- const dir = parts[1]
81
- return ["agent", "skill", "plugin", "command", "tool", "philosophy"].includes(dir)
82
- },
83
- {
84
- message:
85
- 'Target must be in a valid directory: ".opencode/{agent|skill|plugin|command|tool|philosophy}/..."',
86
- },
87
- )
88
-
89
- /**
90
- * Skill-specific target validation.
91
- * Skills must be in: .opencode/skill/<name>/SKILL.md
92
- */
93
- export const skillTargetSchema = z
94
- .string()
95
- .regex(/^\.opencode\/skill\/[a-z0-9]+(-[a-z0-9]+)*\/SKILL\.md$/, {
96
- message:
97
- 'Skill target must match pattern ".opencode/skill/<name>/SKILL.md" where name follows OpenCode naming rules',
98
- })
99
-
100
- // =============================================================================
101
- // MCP SERVER SCHEMA
102
- // =============================================================================
103
-
104
- export const mcpServerSchema = z
105
- .object({
106
- type: z.enum(["remote", "local"]),
107
- url: z.string().url().optional(),
108
- command: z.array(z.string()).optional(),
109
- headers: z.record(z.string()).optional(),
110
- enabled: z.boolean().default(true),
111
- })
112
- .refine(
113
- (data) => {
114
- if (data.type === "remote" && !data.url) {
115
- return false
116
- }
117
- if (data.type === "local" && !data.command) {
118
- return false
119
- }
120
- return true
121
- },
122
- {
123
- message: "Remote MCP servers require 'url', local servers require 'command'",
124
- },
125
- )
126
-
127
- export type McpServer = z.infer<typeof mcpServerSchema>
128
-
129
- // =============================================================================
130
- // COMPONENT FILE SCHEMA
131
- // =============================================================================
132
-
133
- export const componentFileSchema = z.object({
134
- /** Source path in registry */
135
- path: z.string().min(1, "File path cannot be empty"),
136
- /** Target path in .opencode/ */
137
- target: targetPathSchema,
138
- })
139
-
140
- export type ComponentFile = z.infer<typeof componentFileSchema>
141
-
142
- // =============================================================================
143
- // COMPONENT MANIFEST SCHEMA
144
- // =============================================================================
145
-
146
- export const componentManifestSchema = z.object({
147
- /** Component name (must include registry prefix) */
148
- name: openCodeNameSchema,
149
-
150
- /** Component type */
151
- type: componentTypeSchema,
152
-
153
- /** Human-readable description */
154
- description: z.string().min(1).max(1024),
155
-
156
- /** Files to install */
157
- files: z.array(componentFileSchema),
158
-
159
- /** Dependencies on other components */
160
- dependencies: z.array(openCodeNameSchema).default([]),
161
-
162
- /** MCP servers this component needs */
163
- mcpServers: z.record(mcpServerSchema).optional(),
164
-
165
- /** Scope MCP servers to this agent only? Default: "agent" */
166
- mcpScope: z.enum(["agent", "global"]).default("agent"),
167
- })
168
-
169
- export type ComponentManifest = z.infer<typeof componentManifestSchema>
170
-
171
- // =============================================================================
172
- // REGISTRY SCHEMA
173
- // =============================================================================
174
-
175
- /**
176
- * Semver regex for version validation
177
- */
178
- const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/
179
-
180
- export const registrySchema = z
181
- .object({
182
- /** Registry name */
183
- name: z.string().min(1, "Registry name cannot be empty"),
184
-
185
- /** Registry prefix - REQUIRED, all components must use this */
186
- prefix: openCodeNameSchema,
187
-
188
- /** Registry version (semver) */
189
- version: z.string().regex(semverRegex, {
190
- message: "Version must be valid semver (e.g., '1.0.0', '2.1.0-beta.1')",
191
- }),
192
-
193
- /** Registry author */
194
- author: z.string().min(1, "Author cannot be empty"),
195
-
196
- /** Components in this registry */
197
- components: z.array(componentManifestSchema),
198
- })
199
- .refine(
200
- (data) => {
201
- // All component names must start with the registry prefix
202
- return data.components.every((c) => c.name.startsWith(`${data.prefix}-`))
203
- },
204
- {
205
- message: "All component names must start with the registry prefix",
206
- },
207
- )
208
- .refine(
209
- (data) => {
210
- // All dependencies must exist within the registry
211
- const componentNames = new Set(data.components.map((c) => c.name))
212
- for (const component of data.components) {
213
- for (const dep of component.dependencies) {
214
- if (!componentNames.has(dep)) {
215
- return false
216
- }
217
- }
218
- }
219
- return true
220
- },
221
- {
222
- message: "All dependencies must reference components that exist in the registry",
223
- },
224
- )
225
-
226
- export type Registry = z.infer<typeof registrySchema>
227
-
228
- // =============================================================================
229
- // PACKUMENT SCHEMA (npm-style versioned component)
230
- // =============================================================================
231
-
232
- export const packumentSchema = z.object({
233
- /** Component name */
234
- name: openCodeNameSchema,
235
-
236
- /** Latest version */
237
- "dist-tags": z.object({
238
- latest: z.string(),
239
- }),
240
-
241
- /** All versions */
242
- versions: z.record(componentManifestSchema),
243
- })
244
-
245
- export type Packument = z.infer<typeof packumentSchema>
246
-
247
- // =============================================================================
248
- // REGISTRY INDEX SCHEMA
249
- // =============================================================================
250
-
251
- export const registryIndexSchema = z.object({
252
- /** Registry metadata */
253
- name: z.string(),
254
- prefix: openCodeNameSchema,
255
- version: z.string(),
256
- author: z.string(),
257
-
258
- /** Component summaries for search */
259
- components: z.array(
260
- z.object({
261
- name: openCodeNameSchema,
262
- type: componentTypeSchema,
263
- description: z.string(),
264
- }),
265
- ),
266
- })
267
-
268
- export type RegistryIndex = z.infer<typeof registryIndexSchema>
package/src/utils/env.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * Environment detection utilities
3
- * Detects CI, TTY, color support for proper output handling
4
- */
5
-
6
- /** Running in CI environment */
7
- export const isCI = Boolean(
8
- process.env.CI ||
9
- process.env.GITHUB_ACTIONS ||
10
- process.env.GITLAB_CI ||
11
- process.env.CIRCLECI ||
12
- process.env.JENKINS_URL ||
13
- process.env.BUILDKITE,
14
- )
15
-
16
- /** Running in interactive terminal */
17
- export const isTTY = Boolean(process.stdout.isTTY && !isCI)
18
-
19
- /** Terminal supports colors */
20
- export const supportsColor = Boolean(
21
- isTTY && process.env.FORCE_COLOR !== "0" && process.env.NO_COLOR === undefined,
22
- )
23
-
24
- /** Get terminal width, default to 80 */
25
- export function getTerminalWidth(): number {
26
- return process.stdout.columns || 80
27
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Custom error classes with error codes
3
- * Following fail-fast philosophy - clear, actionable errors
4
- */
5
-
6
- export type ErrorCode =
7
- | "NOT_FOUND"
8
- | "NETWORK_ERROR"
9
- | "CONFIG_ERROR"
10
- | "VALIDATION_ERROR"
11
- | "CONFLICT"
12
- | "PERMISSION_ERROR"
13
- | "INTEGRITY_ERROR"
14
-
15
- export const EXIT_CODES = {
16
- SUCCESS: 0,
17
- GENERAL: 1,
18
- NOT_FOUND: 66,
19
- NETWORK: 69,
20
- CONFIG: 78,
21
- INTEGRITY: 1, // Exit code for integrity failures
22
- } as const
23
-
24
- export class OCXError extends Error {
25
- constructor(
26
- message: string,
27
- public readonly code: ErrorCode,
28
- public readonly exitCode: number = EXIT_CODES.GENERAL,
29
- ) {
30
- super(message)
31
- this.name = "OCXError"
32
- }
33
- }
34
-
35
- export class NotFoundError extends OCXError {
36
- constructor(message: string) {
37
- super(message, "NOT_FOUND", EXIT_CODES.NOT_FOUND)
38
- this.name = "NotFoundError"
39
- }
40
- }
41
-
42
- export class NetworkError extends OCXError {
43
- constructor(message: string) {
44
- super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK)
45
- this.name = "NetworkError"
46
- }
47
- }
48
-
49
- export class ConfigError extends OCXError {
50
- constructor(message: string) {
51
- super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG)
52
- this.name = "ConfigError"
53
- }
54
- }
55
-
56
- export class ValidationError extends OCXError {
57
- constructor(message: string) {
58
- super(message, "VALIDATION_ERROR", EXIT_CODES.GENERAL)
59
- this.name = "ValidationError"
60
- }
61
- }
62
-
63
- export class ConflictError extends OCXError {
64
- constructor(message: string) {
65
- super(message, "CONFLICT", EXIT_CODES.GENERAL)
66
- this.name = "ConflictError"
67
- }
68
- }
69
-
70
- export class IntegrityError extends OCXError {
71
- constructor(component: string, expected: string, found: string) {
72
- const message =
73
- `Integrity verification failed for "${component}"\n` +
74
- ` Expected: ${expected}\n` +
75
- ` Found: ${found}\n\n` +
76
- `The registry content has changed since this component was locked.\n` +
77
- `This could indicate tampering or an unauthorized update.`
78
- super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY)
79
- this.name = "IntegrityError"
80
- }
81
- }
@@ -1,108 +0,0 @@
1
- /**
2
- * Error handler for CLI commands
3
- * Converts errors to user-friendly output with proper exit codes
4
- */
5
-
6
- import { ZodError } from "zod"
7
-
8
- import { OCXError, EXIT_CODES } from "./errors"
9
- import { logger } from "./logger"
10
-
11
- export interface HandleErrorOptions {
12
- json?: boolean
13
- }
14
-
15
- /**
16
- * Handle errors consistently across all commands
17
- * Fail-fast: exit immediately with appropriate code
18
- */
19
- export function handleError(error: unknown, options: HandleErrorOptions = {}): never {
20
- // JSON mode: structured output
21
- if (options.json) {
22
- const output = formatErrorAsJson(error)
23
- console.log(JSON.stringify(output, null, 2))
24
- process.exit(output.exitCode)
25
- }
26
-
27
- // OCX errors: known errors with codes
28
- if (error instanceof OCXError) {
29
- logger.error(error.message)
30
- process.exit(error.exitCode)
31
- }
32
-
33
- // Zod validation errors: format nicely
34
- if (error instanceof ZodError) {
35
- logger.error("Validation failed:")
36
- for (const issue of error.issues) {
37
- const path = issue.path.join(".")
38
- logger.error(` ${path}: ${issue.message}`)
39
- }
40
- process.exit(EXIT_CODES.CONFIG)
41
- }
42
-
43
- // Unknown errors
44
- if (error instanceof Error) {
45
- logger.error(error.message)
46
- if (process.env.DEBUG) {
47
- console.error(error.stack)
48
- }
49
- } else {
50
- logger.error("An unknown error occurred")
51
- }
52
-
53
- process.exit(EXIT_CODES.GENERAL)
54
- }
55
-
56
- interface JsonErrorOutput {
57
- success: false
58
- error: {
59
- code: string
60
- message: string
61
- }
62
- exitCode: number
63
- meta: {
64
- timestamp: string
65
- }
66
- }
67
-
68
- function formatErrorAsJson(error: unknown): JsonErrorOutput {
69
- if (error instanceof OCXError) {
70
- return {
71
- success: false,
72
- error: {
73
- code: error.code,
74
- message: error.message,
75
- },
76
- exitCode: error.exitCode,
77
- meta: {
78
- timestamp: new Date().toISOString(),
79
- },
80
- }
81
- }
82
-
83
- if (error instanceof ZodError) {
84
- return {
85
- success: false,
86
- error: {
87
- code: "VALIDATION_ERROR",
88
- message: error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "),
89
- },
90
- exitCode: EXIT_CODES.CONFIG,
91
- meta: {
92
- timestamp: new Date().toISOString(),
93
- },
94
- }
95
- }
96
-
97
- return {
98
- success: false,
99
- error: {
100
- code: "UNKNOWN_ERROR",
101
- message: error instanceof Error ? error.message : "An unknown error occurred",
102
- },
103
- exitCode: EXIT_CODES.GENERAL,
104
- meta: {
105
- timestamp: new Date().toISOString(),
106
- },
107
- }
108
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Core utilities for OCX CLI
3
- */
4
-
5
- export * from "./env.js"
6
- export * from "./errors.js"
7
- export * from "./logger.js"
8
- export * from "./spinner.js"
9
- export * from "./handle-error.js"
10
- export * from "./json-output.js"
@@ -1,107 +0,0 @@
1
- /**
2
- * JSON output utilities for CI/CD integration
3
- * Following GitHub CLI patterns for consistent --json flag handling
4
- */
5
-
6
- import { type ErrorCode, EXIT_CODES, OCXError } from "./errors.js"
7
-
8
- // JSON response envelope
9
- export interface JsonResponse<T = unknown> {
10
- success: boolean
11
- data?: T
12
- error?: {
13
- code: ErrorCode
14
- message: string
15
- }
16
- meta?: {
17
- timestamp: string
18
- version: string
19
- }
20
- }
21
-
22
- // Global JSON mode state
23
- let jsonMode = false
24
-
25
- export function setJsonMode(enabled: boolean): void {
26
- jsonMode = enabled
27
- }
28
-
29
- export function isJsonMode(): boolean {
30
- return jsonMode
31
- }
32
-
33
- /**
34
- * Output data as JSON
35
- */
36
- export function outputJson(data: any): void {
37
- console.log(JSON.stringify(data, null, 2))
38
- }
39
-
40
- /**
41
- * Output success response
42
- */
43
- export function outputSuccess<T>(data: T): void {
44
- const response: JsonResponse<T> = {
45
- success: true,
46
- data,
47
- meta: {
48
- timestamp: new Date().toISOString(),
49
- version: "0.1.0",
50
- },
51
- }
52
- outputJson(response)
53
- }
54
-
55
- /**
56
- * Output error response
57
- */
58
- export function outputError(code: ErrorCode, message: string): void {
59
- const response: JsonResponse = {
60
- success: false,
61
- error: { code, message },
62
- meta: {
63
- timestamp: new Date().toISOString(),
64
- version: "0.1.0",
65
- },
66
- }
67
- outputJson(response)
68
- }
69
-
70
- /**
71
- * Get exit code for error code
72
- */
73
- export function getExitCode(code: ErrorCode): number {
74
- switch (code) {
75
- case "NOT_FOUND":
76
- return EXIT_CODES.NOT_FOUND
77
- case "NETWORK_ERROR":
78
- return EXIT_CODES.NETWORK
79
- case "CONFIG_ERROR":
80
- case "VALIDATION_ERROR":
81
- return EXIT_CODES.CONFIG
82
- default:
83
- return EXIT_CODES.GENERAL
84
- }
85
- }
86
-
87
- /**
88
- * Wrap a command handler to support JSON output mode
89
- */
90
- export function withJsonOutput<T extends (...args: unknown[]) => Promise<void>>(handler: T): T {
91
- return (async (...args: unknown[]) => {
92
- try {
93
- await handler(...args)
94
- } catch (error) {
95
- if (isJsonMode()) {
96
- if (error instanceof OCXError) {
97
- outputError(error.code, error.message)
98
- process.exit(error.exitCode)
99
- }
100
- const message = error instanceof Error ? error.message : String(error)
101
- outputError("VALIDATION_ERROR", message)
102
- process.exit(EXIT_CODES.GENERAL)
103
- }
104
- throw error
105
- }
106
- }) as T
107
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Logger utility with quiet/verbose modes
3
- * Inspired by ShadCN's logger pattern
4
- */
5
-
6
- import kleur from "kleur"
7
- import { supportsColor } from "./env"
8
-
9
- // Disable colors if not supported
10
- if (!supportsColor) {
11
- kleur.enabled = false
12
- }
13
-
14
- export interface LoggerOptions {
15
- quiet?: boolean
16
- verbose?: boolean
17
- }
18
-
19
- let options: LoggerOptions = {}
20
-
21
- export function setLoggerOptions(opts: LoggerOptions): void {
22
- options = opts
23
- }
24
-
25
- export const logger = {
26
- info(...args: unknown[]): void {
27
- if (options.quiet) return
28
- console.log(kleur.blue("info"), ...args)
29
- },
30
-
31
- success(...args: unknown[]): void {
32
- if (options.quiet) return
33
- console.log(kleur.green("✓"), ...args)
34
- },
35
-
36
- warn(...args: unknown[]): void {
37
- if (options.quiet) return
38
- console.warn(kleur.yellow("warn"), ...args)
39
- },
40
-
41
- error(...args: unknown[]): void {
42
- // Errors are always shown
43
- console.error(kleur.red("error"), ...args)
44
- },
45
-
46
- debug(...args: unknown[]): void {
47
- if (!options.verbose) return
48
- console.log(kleur.gray("debug"), ...args)
49
- },
50
-
51
- log(...args: unknown[]): void {
52
- if (options.quiet) return
53
- console.log(...args)
54
- },
55
-
56
- /** Print a blank line */
57
- break(): void {
58
- if (options.quiet) return
59
- console.log("")
60
- },
61
- }
62
-
63
- /** Highlight text with color */
64
- export const highlight = {
65
- component: (text: string) => kleur.cyan(text),
66
- path: (text: string) => kleur.green(text),
67
- command: (text: string) => kleur.yellow(text),
68
- url: (text: string) => kleur.blue().underline(text),
69
- error: (text: string) => kleur.red(text),
70
- dim: (text: string) => kleur.gray(text),
71
- bold: (text: string) => kleur.bold(text),
72
- }