@harness-engineering/cli 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.
@@ -0,0 +1,265 @@
1
+ // src/commands/create-skill.ts
2
+ import { Command } from "commander";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ import YAML from "yaml";
6
+
7
+ // src/skill/schema.ts
8
+ import { z } from "zod";
9
+ var SkillPhaseSchema = z.object({
10
+ name: z.string(),
11
+ description: z.string(),
12
+ required: z.boolean().default(true)
13
+ });
14
+ var SkillCliSchema = z.object({
15
+ command: z.string(),
16
+ args: z.array(
17
+ z.object({
18
+ name: z.string(),
19
+ description: z.string(),
20
+ required: z.boolean().default(false)
21
+ })
22
+ ).default([])
23
+ });
24
+ var SkillMcpSchema = z.object({
25
+ tool: z.string(),
26
+ input: z.record(z.string())
27
+ });
28
+ var SkillStateSchema = z.object({
29
+ persistent: z.boolean().default(false),
30
+ files: z.array(z.string()).default([])
31
+ });
32
+ var ALLOWED_TRIGGERS = [
33
+ "manual",
34
+ "on_pr",
35
+ "on_commit",
36
+ "on_new_feature",
37
+ "on_bug_fix",
38
+ "on_refactor",
39
+ "on_project_init",
40
+ "on_review"
41
+ ];
42
+ var ALLOWED_PLATFORMS = ["claude-code", "gemini-cli"];
43
+ var ALLOWED_COGNITIVE_MODES = [
44
+ "adversarial-reviewer",
45
+ "constructive-architect",
46
+ "meticulous-implementer",
47
+ "diagnostic-investigator",
48
+ "advisory-guide",
49
+ "meticulous-verifier"
50
+ ];
51
+ var SkillMetadataSchema = z.object({
52
+ name: z.string().regex(/^[a-z][a-z0-9-]*$/, "Name must be lowercase with hyphens"),
53
+ version: z.string().regex(/^\d+\.\d+\.\d+$/, "Version must be semver format"),
54
+ description: z.string(),
55
+ cognitive_mode: z.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "Cognitive mode must be kebab-case").optional(),
56
+ triggers: z.array(z.enum(ALLOWED_TRIGGERS)),
57
+ platforms: z.array(z.enum(ALLOWED_PLATFORMS)),
58
+ tools: z.array(z.string()),
59
+ cli: SkillCliSchema.optional(),
60
+ mcp: SkillMcpSchema.optional(),
61
+ type: z.enum(["rigid", "flexible"]),
62
+ phases: z.array(SkillPhaseSchema).optional(),
63
+ state: SkillStateSchema.default({}),
64
+ depends_on: z.array(z.string()).default([])
65
+ });
66
+
67
+ // src/output/logger.ts
68
+ import chalk from "chalk";
69
+ var logger = {
70
+ info: (message) => console.log(chalk.blue("i"), message),
71
+ success: (message) => console.log(chalk.green("v"), message),
72
+ warn: (message) => console.log(chalk.yellow("!"), message),
73
+ error: (message) => console.error(chalk.red("x"), message),
74
+ dim: (message) => console.log(chalk.dim(message)),
75
+ // For JSON output mode
76
+ raw: (data) => console.log(JSON.stringify(data, null, 2))
77
+ };
78
+
79
+ // src/utils/errors.ts
80
+ var ExitCode = {
81
+ SUCCESS: 0,
82
+ VALIDATION_FAILED: 1,
83
+ ERROR: 2
84
+ };
85
+ var CLIError = class extends Error {
86
+ exitCode;
87
+ constructor(message, exitCode = ExitCode.ERROR) {
88
+ super(message);
89
+ this.name = "CLIError";
90
+ this.exitCode = exitCode;
91
+ }
92
+ };
93
+ function formatError(error) {
94
+ if (error instanceof CLIError) {
95
+ return `Error: ${error.message}`;
96
+ }
97
+ if (error instanceof Error) {
98
+ return `Error: ${error.message}`;
99
+ }
100
+ return `Error: ${String(error)}`;
101
+ }
102
+ function handleError(error) {
103
+ const message = formatError(error);
104
+ console.error(message);
105
+ const exitCode = error instanceof CLIError ? error.exitCode : ExitCode.ERROR;
106
+ process.exit(exitCode);
107
+ }
108
+
109
+ // src/commands/create-skill.ts
110
+ var KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
111
+ function buildSkillYaml(opts) {
112
+ const doc = {
113
+ name: opts.name,
114
+ version: "0.1.0",
115
+ description: opts.description,
116
+ cognitive_mode: opts.cognitiveMode ?? "constructive-architect",
117
+ triggers: ["manual"],
118
+ platforms: ["claude-code"],
119
+ tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
120
+ type: "flexible",
121
+ state: {
122
+ persistent: false,
123
+ files: []
124
+ },
125
+ depends_on: []
126
+ };
127
+ return doc;
128
+ }
129
+ function buildSkillMd(opts) {
130
+ const mode = opts.cognitiveMode ?? "constructive-architect";
131
+ const readsSection = opts.reads && opts.reads.length > 0 ? opts.reads.map((r) => `- \`${r}\``).join("\n") : "- _No read patterns specified_";
132
+ const producesLine = opts.produces ? `- \`${opts.produces}\`` : "- _No output specified_";
133
+ const preChecksSection = opts.preChecks && opts.preChecks.length > 0 ? opts.preChecks.map((c) => `- \`${c}\``).join("\n") : "- _None_";
134
+ const postChecksSection = opts.postChecks && opts.postChecks.length > 0 ? opts.postChecks.map((c) => `- \`${c}\``).join("\n") : "- _None_";
135
+ return `# ${opts.name}
136
+
137
+ > Cognitive Mode: ${mode}
138
+
139
+ ${opts.description}
140
+
141
+ ## When to Use
142
+
143
+ - [Describe when this skill should be invoked]
144
+ - [Describe the trigger conditions]
145
+
146
+ ## Context Assembly
147
+
148
+ ### Reads
149
+ ${readsSection}
150
+
151
+ ### Produces
152
+ ${producesLine}
153
+
154
+ ## Deterministic Checks
155
+
156
+ ### Pre-checks
157
+ ${preChecksSection}
158
+
159
+ ### Post-checks
160
+ ${postChecksSection}
161
+
162
+ ## Process
163
+
164
+ 1. [Describe the step-by-step process]
165
+ 2. [Add additional steps as needed]
166
+
167
+ ## Harness Integration
168
+
169
+ This skill integrates with the harness engineering workflow. It can be invoked via:
170
+
171
+ \`\`\`bash
172
+ harness skill run ${opts.name}
173
+ \`\`\`
174
+
175
+ ## Success Criteria
176
+
177
+ - [Define what a successful execution looks like]
178
+ - [Add measurable criteria]
179
+
180
+ ## Examples
181
+
182
+ \`\`\`bash
183
+ harness skill run ${opts.name}
184
+ \`\`\`
185
+ `;
186
+ }
187
+ function generateSkillFiles(opts) {
188
+ if (!KEBAB_CASE_RE.test(opts.name)) {
189
+ throw new CLIError(
190
+ `Invalid skill name "${opts.name}". Must be kebab-case (e.g., my-skill).`,
191
+ ExitCode.VALIDATION_FAILED
192
+ );
193
+ }
194
+ if (opts.cognitiveMode && !ALLOWED_COGNITIVE_MODES.includes(opts.cognitiveMode)) {
195
+ throw new CLIError(
196
+ `Invalid cognitive mode "${opts.cognitiveMode}". Allowed: ${ALLOWED_COGNITIVE_MODES.join(", ")}`,
197
+ ExitCode.VALIDATION_FAILED
198
+ );
199
+ }
200
+ const baseDir = opts.outputDir ?? path.join(process.cwd(), "agents", "skills", "claude-code");
201
+ const skillDir = path.join(baseDir, opts.name);
202
+ if (fs.existsSync(skillDir)) {
203
+ throw new CLIError(`Skill directory already exists: ${skillDir}`, ExitCode.VALIDATION_FAILED);
204
+ }
205
+ fs.mkdirSync(skillDir, { recursive: true });
206
+ const skillYaml = buildSkillYaml(opts);
207
+ const skillYamlPath = path.join(skillDir, "skill.yaml");
208
+ fs.writeFileSync(skillYamlPath, YAML.stringify(skillYaml));
209
+ const skillMd = buildSkillMd(opts);
210
+ const skillMdPath = path.join(skillDir, "SKILL.md");
211
+ fs.writeFileSync(skillMdPath, skillMd);
212
+ return { skillYamlPath, skillMdPath };
213
+ }
214
+ function createCreateSkillCommand() {
215
+ const command = new Command("create-skill").description("Scaffold a new skill with skill.yaml and SKILL.md").requiredOption("--name <name>", "Skill name (kebab-case)").requiredOption("--description <desc>", "Skill description").option(
216
+ "--cognitive-mode <mode>",
217
+ `Cognitive mode (${ALLOWED_COGNITIVE_MODES.join(", ")})`,
218
+ "constructive-architect"
219
+ ).option("--reads <patterns...>", "File patterns the skill reads").option("--produces <output>", "What the skill produces").option("--pre-checks <commands...>", "Pre-check commands").option("--post-checks <commands...>", "Post-check commands").action(async (opts, cmd) => {
220
+ const globalOpts = cmd.optsWithGlobals();
221
+ try {
222
+ const result = generateSkillFiles({
223
+ name: opts.name,
224
+ description: opts.description,
225
+ cognitiveMode: opts.cognitiveMode,
226
+ reads: opts.reads,
227
+ produces: opts.produces,
228
+ preChecks: opts.preChecks,
229
+ postChecks: opts.postChecks
230
+ });
231
+ if (!globalOpts.quiet) {
232
+ logger.success(`Created skill "${opts.name}"`);
233
+ logger.info(` ${result.skillYamlPath}`);
234
+ logger.info(` ${result.skillMdPath}`);
235
+ }
236
+ if (globalOpts.json) {
237
+ logger.raw({
238
+ name: opts.name,
239
+ files: [result.skillYamlPath, result.skillMdPath]
240
+ });
241
+ }
242
+ } catch (error) {
243
+ if (error instanceof CLIError) {
244
+ if (globalOpts.json) {
245
+ console.log(JSON.stringify({ error: error.message }));
246
+ } else {
247
+ logger.error(error.message);
248
+ }
249
+ process.exit(error.exitCode);
250
+ }
251
+ throw error;
252
+ }
253
+ });
254
+ return command;
255
+ }
256
+
257
+ export {
258
+ ExitCode,
259
+ CLIError,
260
+ handleError,
261
+ logger,
262
+ SkillMetadataSchema,
263
+ generateSkillFiles,
264
+ createCreateSkillCommand
265
+ };
@@ -0,0 +1,226 @@
1
+ // src/index.ts
2
+ import { Command } from "commander";
3
+ import { VERSION } from "@harness-engineering/core";
4
+
5
+ // src/utils/errors.ts
6
+ var ExitCode = {
7
+ SUCCESS: 0,
8
+ VALIDATION_FAILED: 1,
9
+ ERROR: 2
10
+ };
11
+ var CLIError = class extends Error {
12
+ exitCode;
13
+ constructor(message, exitCode = ExitCode.ERROR) {
14
+ super(message);
15
+ this.name = "CLIError";
16
+ this.exitCode = exitCode;
17
+ }
18
+ };
19
+ function formatError(error) {
20
+ if (error instanceof CLIError) {
21
+ return `Error: ${error.message}`;
22
+ }
23
+ if (error instanceof Error) {
24
+ return `Error: ${error.message}`;
25
+ }
26
+ return `Error: ${String(error)}`;
27
+ }
28
+ function handleError(error) {
29
+ const message = formatError(error);
30
+ console.error(message);
31
+ const exitCode = error instanceof CLIError ? error.exitCode : ExitCode.ERROR;
32
+ process.exit(exitCode);
33
+ }
34
+
35
+ // src/output/formatter.ts
36
+ import chalk from "chalk";
37
+ var OutputMode = {
38
+ JSON: "json",
39
+ TEXT: "text",
40
+ QUIET: "quiet",
41
+ VERBOSE: "verbose"
42
+ };
43
+ var OutputFormatter = class {
44
+ constructor(mode = OutputMode.TEXT) {
45
+ this.mode = mode;
46
+ }
47
+ /**
48
+ * Format raw data (for JSON mode)
49
+ */
50
+ format(data) {
51
+ if (this.mode === OutputMode.JSON) {
52
+ return JSON.stringify(data, null, 2);
53
+ }
54
+ return String(data);
55
+ }
56
+ /**
57
+ * Format validation result
58
+ */
59
+ formatValidation(result) {
60
+ if (this.mode === OutputMode.JSON) {
61
+ return JSON.stringify(result, null, 2);
62
+ }
63
+ if (this.mode === OutputMode.QUIET) {
64
+ if (result.valid) return "";
65
+ return result.issues.map((i) => `${i.file ?? ""}: ${i.message}`).join("\n");
66
+ }
67
+ const lines = [];
68
+ if (result.valid) {
69
+ lines.push(chalk.green("v validation passed"));
70
+ } else {
71
+ lines.push(chalk.red(`x Validation failed (${result.issues.length} issues)`));
72
+ lines.push("");
73
+ for (const issue of result.issues) {
74
+ const location = issue.file ? issue.line ? `${issue.file}:${issue.line}` : issue.file : "unknown";
75
+ lines.push(` ${chalk.yellow("*")} ${chalk.dim(location)}`);
76
+ lines.push(` ${issue.message}`);
77
+ if (issue.suggestion && this.mode === OutputMode.VERBOSE) {
78
+ lines.push(` ${chalk.dim("->")} ${issue.suggestion}`);
79
+ }
80
+ }
81
+ }
82
+ return lines.join("\n");
83
+ }
84
+ /**
85
+ * Format a summary line
86
+ */
87
+ formatSummary(label, value, success) {
88
+ if (this.mode === OutputMode.JSON || this.mode === OutputMode.QUIET) {
89
+ return "";
90
+ }
91
+ const icon = success ? chalk.green("v") : chalk.red("x");
92
+ return `${icon} ${label}: ${value}`;
93
+ }
94
+ };
95
+
96
+ // src/output/logger.ts
97
+ import chalk2 from "chalk";
98
+ var logger = {
99
+ info: (message) => console.log(chalk2.blue("i"), message),
100
+ success: (message) => console.log(chalk2.green("v"), message),
101
+ warn: (message) => console.log(chalk2.yellow("!"), message),
102
+ error: (message) => console.error(chalk2.red("x"), message),
103
+ dim: (message) => console.log(chalk2.dim(message)),
104
+ // For JSON output mode
105
+ raw: (data) => console.log(JSON.stringify(data, null, 2))
106
+ };
107
+
108
+ // src/config/loader.ts
109
+ import * as fs from "fs";
110
+ import * as path from "path";
111
+ import { Ok, Err } from "@harness-engineering/core";
112
+
113
+ // src/config/schema.ts
114
+ import { z } from "zod";
115
+ var LayerSchema = z.object({
116
+ name: z.string(),
117
+ pattern: z.string(),
118
+ allowedDependencies: z.array(z.string())
119
+ });
120
+ var ForbiddenImportSchema = z.object({
121
+ from: z.string(),
122
+ disallow: z.array(z.string()),
123
+ message: z.string().optional()
124
+ });
125
+ var BoundaryConfigSchema = z.object({
126
+ requireSchema: z.array(z.string())
127
+ });
128
+ var AgentConfigSchema = z.object({
129
+ executor: z.enum(["subprocess", "cloud", "noop"]).default("subprocess"),
130
+ timeout: z.number().default(3e5),
131
+ skills: z.array(z.string()).optional()
132
+ });
133
+ var EntropyConfigSchema = z.object({
134
+ excludePatterns: z.array(z.string()).default(["**/node_modules/**", "**/*.test.ts"]),
135
+ autoFix: z.boolean().default(false)
136
+ });
137
+ var HarnessConfigSchema = z.object({
138
+ version: z.literal(1),
139
+ name: z.string().optional(),
140
+ rootDir: z.string().default("."),
141
+ layers: z.array(LayerSchema).optional(),
142
+ forbiddenImports: z.array(ForbiddenImportSchema).optional(),
143
+ boundaries: BoundaryConfigSchema.optional(),
144
+ agentsMapPath: z.string().default("./AGENTS.md"),
145
+ docsDir: z.string().default("./docs"),
146
+ agent: AgentConfigSchema.optional(),
147
+ entropy: EntropyConfigSchema.optional()
148
+ });
149
+
150
+ // src/config/loader.ts
151
+ var CONFIG_FILENAMES = ["harness.config.json"];
152
+ function findConfigFile(startDir = process.cwd()) {
153
+ let currentDir = path.resolve(startDir);
154
+ const root = path.parse(currentDir).root;
155
+ while (currentDir !== root) {
156
+ for (const filename of CONFIG_FILENAMES) {
157
+ const configPath = path.join(currentDir, filename);
158
+ if (fs.existsSync(configPath)) {
159
+ return Ok(configPath);
160
+ }
161
+ }
162
+ currentDir = path.dirname(currentDir);
163
+ }
164
+ return Err(new CLIError(
165
+ 'No harness.config.json found. Run "harness init" to create one.',
166
+ ExitCode.ERROR
167
+ ));
168
+ }
169
+ function loadConfig(configPath) {
170
+ if (!fs.existsSync(configPath)) {
171
+ return Err(new CLIError(
172
+ `Config file not found: ${configPath}`,
173
+ ExitCode.ERROR
174
+ ));
175
+ }
176
+ let rawConfig;
177
+ try {
178
+ const content = fs.readFileSync(configPath, "utf-8");
179
+ rawConfig = JSON.parse(content);
180
+ } catch (error) {
181
+ return Err(new CLIError(
182
+ `Failed to parse config: ${error instanceof Error ? error.message : "Unknown error"}`,
183
+ ExitCode.ERROR
184
+ ));
185
+ }
186
+ const parsed = HarnessConfigSchema.safeParse(rawConfig);
187
+ if (!parsed.success) {
188
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
189
+ return Err(new CLIError(
190
+ `Invalid config:
191
+ ${issues}`,
192
+ ExitCode.ERROR
193
+ ));
194
+ }
195
+ return Ok(parsed.data);
196
+ }
197
+ function resolveConfig(configPath) {
198
+ if (configPath) {
199
+ return loadConfig(configPath);
200
+ }
201
+ const findResult = findConfigFile();
202
+ if (!findResult.ok) {
203
+ return findResult;
204
+ }
205
+ return loadConfig(findResult.value);
206
+ }
207
+
208
+ // src/index.ts
209
+ function createProgram() {
210
+ const program = new Command();
211
+ program.name("harness").description("CLI for Harness Engineering toolkit").version(VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
212
+ return program;
213
+ }
214
+
215
+ export {
216
+ ExitCode,
217
+ CLIError,
218
+ handleError,
219
+ OutputMode,
220
+ OutputFormatter,
221
+ logger,
222
+ findConfigFile,
223
+ loadConfig,
224
+ resolveConfig,
225
+ createProgram
226
+ };