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/schemas/registry.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/errors.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/index.ts
DELETED
package/src/utils/json-output.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/logger.ts
DELETED
|
@@ -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
|
-
}
|