@enactprotocol/cli 2.1.28 → 2.1.29

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.
Files changed (41) hide show
  1. package/dist/commands/index.d.ts +1 -0
  2. package/dist/commands/index.d.ts.map +1 -1
  3. package/dist/commands/index.js +2 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/commands/init/templates/claude.d.ts +1 -1
  6. package/dist/commands/init/templates/claude.d.ts.map +1 -1
  7. package/dist/commands/init/templates/claude.js +267 -27
  8. package/dist/commands/init/templates/claude.js.map +1 -1
  9. package/dist/commands/init/templates/tool-agents.d.ts +1 -1
  10. package/dist/commands/init/templates/tool-agents.d.ts.map +1 -1
  11. package/dist/commands/init/templates/tool-agents.js +82 -7
  12. package/dist/commands/init/templates/tool-agents.js.map +1 -1
  13. package/dist/commands/learn/index.d.ts.map +1 -1
  14. package/dist/commands/learn/index.js +4 -11
  15. package/dist/commands/learn/index.js.map +1 -1
  16. package/dist/commands/run/index.d.ts.map +1 -1
  17. package/dist/commands/run/index.js +86 -5
  18. package/dist/commands/run/index.js.map +1 -1
  19. package/dist/commands/validate/index.d.ts +11 -0
  20. package/dist/commands/validate/index.d.ts.map +1 -0
  21. package/dist/commands/validate/index.js +299 -0
  22. package/dist/commands/validate/index.js.map +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/types.d.ts +2 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/types.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/commands/index.ts +3 -0
  32. package/src/commands/init/templates/claude.ts +267 -27
  33. package/src/commands/init/templates/tool-agents.ts +82 -7
  34. package/src/commands/run/README.md +17 -0
  35. package/src/commands/run/index.ts +105 -5
  36. package/src/commands/validate/index.ts +344 -0
  37. package/src/index.ts +5 -1
  38. package/src/types.ts +2 -0
  39. package/tests/commands/init.test.ts +1 -1
  40. package/tests/commands/validate.test.ts +81 -0
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -84,6 +84,7 @@ interface RunOptions extends GlobalOptions {
84
84
  verbose?: boolean;
85
85
  output?: string;
86
86
  apply?: boolean;
87
+ debug?: boolean;
87
88
  }
88
89
 
89
90
  /**
@@ -630,6 +631,84 @@ function displayDryRun(
630
631
  newline();
631
632
  }
632
633
 
634
+ /**
635
+ * Display debug information about parameter resolution
636
+ */
637
+ function displayDebugInfo(
638
+ manifest: ToolManifest,
639
+ rawInputs: Record<string, unknown>,
640
+ inputsWithDefaults: Record<string, unknown>,
641
+ finalInputs: Record<string, unknown>,
642
+ env: Record<string, string>,
643
+ command: string[]
644
+ ): void {
645
+ newline();
646
+ info(colors.bold("Debug: Parameter Resolution"));
647
+ newline();
648
+
649
+ // Show schema information
650
+ if (manifest.inputSchema?.properties) {
651
+ info("Schema Properties:");
652
+ const required = new Set(manifest.inputSchema.required || []);
653
+ for (const [name, prop] of Object.entries(manifest.inputSchema.properties)) {
654
+ const propSchema = prop as { type?: string; default?: unknown; description?: string };
655
+ const isRequired = required.has(name);
656
+ const hasDefault = propSchema.default !== undefined;
657
+ const status = isRequired ? colors.error("required") : colors.dim("optional");
658
+ dim(
659
+ ` ${name}: ${propSchema.type || "any"} [${status}]${hasDefault ? ` (default: ${JSON.stringify(propSchema.default)})` : ""}`
660
+ );
661
+ }
662
+ newline();
663
+ }
664
+
665
+ // Show raw inputs (what was provided)
666
+ info("Raw Inputs (provided by user):");
667
+ if (Object.keys(rawInputs).length === 0) {
668
+ dim(" (none)");
669
+ } else {
670
+ for (const [key, value] of Object.entries(rawInputs)) {
671
+ dim(` ${key}: ${JSON.stringify(value)}`);
672
+ }
673
+ }
674
+ newline();
675
+
676
+ // Show inputs after defaults applied
677
+ info("After Defaults Applied:");
678
+ for (const [key, value] of Object.entries(inputsWithDefaults)) {
679
+ const wasDefault = rawInputs[key] === undefined;
680
+ dim(` ${key}: ${JSON.stringify(value)}${wasDefault ? colors.dim(" (default)") : ""}`);
681
+ }
682
+ newline();
683
+
684
+ // Show final inputs (after coercion)
685
+ info("Final Inputs (after validation/coercion):");
686
+ for (const [key, value] of Object.entries(finalInputs)) {
687
+ dim(` ${key}: ${JSON.stringify(value)}`);
688
+ }
689
+ newline();
690
+
691
+ // Show environment variables
692
+ if (Object.keys(env).length > 0) {
693
+ info("Environment Variables:");
694
+ for (const [key, value] of Object.entries(env)) {
695
+ // Mask potentially sensitive values
696
+ const isSensitive =
697
+ key.toLowerCase().includes("secret") ||
698
+ key.toLowerCase().includes("key") ||
699
+ key.toLowerCase().includes("token") ||
700
+ key.toLowerCase().includes("password");
701
+ dim(` ${key}=${isSensitive ? "***" : value}`);
702
+ }
703
+ newline();
704
+ }
705
+
706
+ // Show final command
707
+ info("Final Command:");
708
+ dim(` ${command.join(" ")}`);
709
+ newline();
710
+ }
711
+
633
712
  /**
634
713
  * Display execution result
635
714
  */
@@ -661,15 +740,30 @@ function displayResult(result: ExecutionResult, options: RunOptions): void {
661
740
  } else {
662
741
  error(`Execution failed: ${result.error?.message ?? "Unknown error"}`);
663
742
 
664
- if (result.error?.details) {
743
+ // Show stdout if present (useful for debugging - command may have printed before failing)
744
+ if (result.output?.stdout?.trim()) {
665
745
  newline();
666
- dim(JSON.stringify(result.error.details, null, 2));
746
+ info("stdout:");
747
+ console.log(result.output.stdout);
667
748
  }
668
749
 
669
- if (result.output?.stderr) {
750
+ // Show stderr (the actual error output)
751
+ if (result.output?.stderr?.trim()) {
670
752
  newline();
671
- dim("stderr:");
672
- dim(result.output.stderr);
753
+ error("stderr:");
754
+ console.log(result.output.stderr);
755
+ }
756
+
757
+ // Show additional error details if present (and different from stderr)
758
+ if (result.error?.details) {
759
+ const detailsStr = JSON.stringify(result.error.details, null, 2);
760
+ // Only show if it adds new information (not just duplicating stderr)
761
+ const stderrInDetails = result.error.details.stderr;
762
+ if (!stderrInDetails || stderrInDetails !== result.output?.stderr) {
763
+ newline();
764
+ dim("Additional details:");
765
+ dim(detailsStr);
766
+ }
673
767
  }
674
768
  }
675
769
  }
@@ -908,6 +1002,11 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
908
1002
  }
909
1003
  }
910
1004
 
1005
+ // Debug mode - show detailed parameter resolution info
1006
+ if (options.debug) {
1007
+ displayDebugInfo(manifest, inputs, inputsWithDefaults, finalInputs, envVars, command);
1008
+ }
1009
+
911
1010
  // Dry run mode
912
1011
  if (options.dryRun) {
913
1012
  displayDryRun(
@@ -1060,6 +1159,7 @@ export function configureRunCommand(program: Command): void {
1060
1159
  .option("--local", "Only resolve from local sources")
1061
1160
  .option("-r, --remote", "Skip local resolution and fetch from registry")
1062
1161
  .option("--dry-run", "Show what would be executed without running")
1162
+ .option("--debug", "Show detailed parameter and environment variable resolution")
1063
1163
  .option("-v, --verbose", "Show progress spinners and detailed output")
1064
1164
  .option("--json", "Output result as JSON")
1065
1165
  .action(async (tool: string, options: RunOptions) => {
@@ -0,0 +1,344 @@
1
+ /**
2
+ * enact validate command
3
+ *
4
+ * Validate a SKILL.md file for common issues and best practices.
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { type ToolManifest, tryResolveTool } from "@enactprotocol/shared";
10
+ import type { Command } from "commander";
11
+ import type { CommandContext, GlobalOptions } from "../../types";
12
+ import { colors, dim, error, formatError, info, newline, success, symbols } from "../../utils";
13
+
14
+ interface ValidateOptions extends GlobalOptions {
15
+ fix?: boolean;
16
+ }
17
+
18
+ interface ValidationIssue {
19
+ level: "error" | "warning" | "info";
20
+ message: string;
21
+ suggestion?: string;
22
+ }
23
+
24
+ /**
25
+ * Extract parameter names from a command template
26
+ */
27
+ function extractCommandParams(command: string): string[] {
28
+ const params: string[] = [];
29
+ const regex = /\$\{([^}:]+)(?::[^}]+)?\}/g;
30
+ let match: RegExpExecArray | null;
31
+ match = regex.exec(command);
32
+ while (match !== null) {
33
+ if (match[1] && !params.includes(match[1])) {
34
+ params.push(match[1]);
35
+ }
36
+ match = regex.exec(command);
37
+ }
38
+ return params;
39
+ }
40
+
41
+ /**
42
+ * Validate a tool manifest
43
+ */
44
+ function validateManifest(manifest: ToolManifest, sourceDir: string): ValidationIssue[] {
45
+ const issues: ValidationIssue[] = [];
46
+
47
+ // Check name format
48
+ if (!manifest.name) {
49
+ issues.push({
50
+ level: "error",
51
+ message: "Missing 'name' field",
52
+ suggestion: "Add a name in format: namespace/category/tool",
53
+ });
54
+ } else if (!manifest.name.includes("/")) {
55
+ issues.push({
56
+ level: "warning",
57
+ message: `Name '${manifest.name}' should follow hierarchical format`,
58
+ suggestion: "Use format: namespace/category/tool (e.g., alice/utils/formatter)",
59
+ });
60
+ }
61
+
62
+ // Check version
63
+ if (!manifest.version) {
64
+ issues.push({
65
+ level: "warning",
66
+ message: "Missing 'version' field",
67
+ suggestion: "Add a version using semver (e.g., 1.0.0)",
68
+ });
69
+ }
70
+
71
+ // Check description
72
+ if (!manifest.description) {
73
+ issues.push({
74
+ level: "warning",
75
+ message: "Missing 'description' field",
76
+ suggestion: "Add a clear description for discoverability",
77
+ });
78
+ }
79
+
80
+ // Check base image
81
+ if (manifest.from) {
82
+ if (manifest.from.endsWith(":latest")) {
83
+ issues.push({
84
+ level: "warning",
85
+ message: `Base image '${manifest.from}' uses :latest tag`,
86
+ suggestion: "Pin to a specific version (e.g., python:3.12-slim instead of python:latest)",
87
+ });
88
+ }
89
+ } else if (manifest.command) {
90
+ issues.push({
91
+ level: "info",
92
+ message: "No 'from' field specified, will use alpine:latest",
93
+ suggestion: "Consider specifying a base image for reproducibility",
94
+ });
95
+ }
96
+
97
+ // Check command vs instruction tool
98
+ if (!manifest.command) {
99
+ // Instruction-based tool
100
+ issues.push({
101
+ level: "info",
102
+ message: "No 'command' field - this is an LLM instruction tool",
103
+ });
104
+ } else {
105
+ // Command-based tool - validate parameters
106
+ const commandParams = extractCommandParams(manifest.command);
107
+ const schemaProperties = manifest.inputSchema?.properties
108
+ ? Object.keys(manifest.inputSchema.properties)
109
+ : [];
110
+ const requiredParams = manifest.inputSchema?.required || [];
111
+
112
+ // Check for command params not in schema
113
+ for (const param of commandParams) {
114
+ if (!schemaProperties.includes(param)) {
115
+ issues.push({
116
+ level: "error",
117
+ message: `Command uses \${${param}} but it's not defined in inputSchema.properties`,
118
+ suggestion: `Add '${param}' to inputSchema.properties`,
119
+ });
120
+ }
121
+ }
122
+
123
+ // Check for required params without command usage (potential issue)
124
+ for (const param of requiredParams) {
125
+ if (!commandParams.includes(param)) {
126
+ issues.push({
127
+ level: "info",
128
+ message: `Required parameter '${param}' is not used in command template`,
129
+ suggestion: "This is fine if you access it via environment or files",
130
+ });
131
+ }
132
+ }
133
+
134
+ // Check for optional params without defaults
135
+ for (const prop of schemaProperties) {
136
+ if (!requiredParams.includes(prop)) {
137
+ const propSchema = manifest.inputSchema?.properties?.[prop] as
138
+ | { default?: unknown }
139
+ | undefined;
140
+ if (propSchema?.default === undefined) {
141
+ issues.push({
142
+ level: "warning",
143
+ message: `Optional parameter '${prop}' has no default value`,
144
+ suggestion: "Add a default value or it will be empty string in commands",
145
+ });
146
+ }
147
+ }
148
+ }
149
+
150
+ // Check for double-quoting in command
151
+ if (
152
+ manifest.command.includes("'${") ||
153
+ manifest.command.includes('"${') ||
154
+ (manifest.command.includes("${") &&
155
+ (manifest.command.includes("}'") || manifest.command.includes('}"')))
156
+ ) {
157
+ issues.push({
158
+ level: "warning",
159
+ message: "Command may have manual quotes around parameters",
160
+ suggestion: "Enact auto-quotes parameters. Remove manual quotes around ${param}",
161
+ });
162
+ }
163
+ }
164
+
165
+ // Check timeout
166
+ if (!manifest.timeout && manifest.command) {
167
+ issues.push({
168
+ level: "info",
169
+ message: "No 'timeout' specified, using default (5 minutes)",
170
+ suggestion: "Consider setting an explicit timeout for long-running tools",
171
+ });
172
+ }
173
+
174
+ // Check for common file patterns
175
+ if (manifest.command) {
176
+ // Python tools
177
+ if (manifest.command.includes("python") && manifest.from?.includes("python")) {
178
+ const mainPy = resolve(sourceDir, "main.py");
179
+ if (manifest.command.includes("/workspace/main.py") && !existsSync(mainPy)) {
180
+ issues.push({
181
+ level: "error",
182
+ message: "Command references /workspace/main.py but main.py not found",
183
+ suggestion: "Create main.py or update the command path",
184
+ });
185
+ }
186
+ }
187
+
188
+ // Node tools
189
+ if (manifest.command.includes("node") && manifest.from?.includes("node")) {
190
+ const indexJs = resolve(sourceDir, "index.js");
191
+ const mainJs = resolve(sourceDir, "main.js");
192
+ if (
193
+ manifest.command.includes("/workspace/index.js") &&
194
+ !existsSync(indexJs) &&
195
+ !existsSync(mainJs)
196
+ ) {
197
+ issues.push({
198
+ level: "warning",
199
+ message: "Command references /workspace/index.js but index.js not found",
200
+ suggestion: "Create index.js or update the command path",
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ // Check env declarations
207
+ if (manifest.env) {
208
+ for (const [key, envDef] of Object.entries(manifest.env)) {
209
+ if (envDef.secret && !envDef.description) {
210
+ issues.push({
211
+ level: "info",
212
+ message: `Secret '${key}' has no description`,
213
+ suggestion: "Add a description to help users understand what this secret is for",
214
+ });
215
+ }
216
+ }
217
+ }
218
+
219
+ return issues;
220
+ }
221
+
222
+ /**
223
+ * Display validation results
224
+ */
225
+ function displayResults(issues: ValidationIssue[], toolPath: string): void {
226
+ const errors = issues.filter((i) => i.level === "error");
227
+ const warnings = issues.filter((i) => i.level === "warning");
228
+ const infos = issues.filter((i) => i.level === "info");
229
+
230
+ newline();
231
+
232
+ if (issues.length === 0) {
233
+ success(`${symbols.success} ${toolPath} - No issues found!`);
234
+ return;
235
+ }
236
+
237
+ info(`Validation results for ${toolPath}:`);
238
+ newline();
239
+
240
+ // Display errors
241
+ for (const issue of errors) {
242
+ console.log(` ${colors.error(`${symbols.error} ERROR:`)} ${issue.message}`);
243
+ if (issue.suggestion) {
244
+ dim(` → ${issue.suggestion}`);
245
+ }
246
+ }
247
+
248
+ // Display warnings
249
+ for (const issue of warnings) {
250
+ console.log(` ${colors.warning(`${symbols.warning} WARNING:`)} ${issue.message}`);
251
+ if (issue.suggestion) {
252
+ dim(` → ${issue.suggestion}`);
253
+ }
254
+ }
255
+
256
+ // Display info
257
+ for (const issue of infos) {
258
+ console.log(` ${colors.info(`${symbols.info} INFO:`)} ${issue.message}`);
259
+ if (issue.suggestion) {
260
+ dim(` → ${issue.suggestion}`);
261
+ }
262
+ }
263
+
264
+ newline();
265
+
266
+ // Summary
267
+ const summary: string[] = [];
268
+ if (errors.length > 0) summary.push(`${errors.length} error${errors.length > 1 ? "s" : ""}`);
269
+ if (warnings.length > 0)
270
+ summary.push(`${warnings.length} warning${warnings.length > 1 ? "s" : ""}`);
271
+ if (infos.length > 0) summary.push(`${infos.length} info`);
272
+
273
+ if (errors.length > 0) {
274
+ error(`Found ${summary.join(", ")}`);
275
+ } else if (warnings.length > 0) {
276
+ console.log(colors.warning(`Found ${summary.join(", ")}`));
277
+ } else {
278
+ success(`Found ${summary.join(", ")} - looking good!`);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Validate command handler
284
+ */
285
+ function validateHandler(toolPath: string, _options: ValidateOptions, ctx: CommandContext): void {
286
+ const resolvedPath = resolve(ctx.cwd, toolPath);
287
+
288
+ // Check if path exists
289
+ if (!existsSync(resolvedPath)) {
290
+ error(`Path not found: ${resolvedPath}`);
291
+ process.exit(1);
292
+ }
293
+
294
+ // Resolve the tool
295
+ const resolution = tryResolveTool(resolvedPath);
296
+
297
+ if (!resolution) {
298
+ error(`Could not find SKILL.md in: ${resolvedPath}`);
299
+ dim("Make sure the directory contains a valid SKILL.md file.");
300
+ process.exit(1);
301
+ }
302
+
303
+ // Validate the manifest
304
+ const issues = validateManifest(resolution.manifest, resolution.sourceDir);
305
+
306
+ // Display results
307
+ displayResults(issues, toolPath);
308
+
309
+ // Exit with error if there are errors
310
+ const hasErrors = issues.some((i) => i.level === "error");
311
+ if (hasErrors) {
312
+ process.exit(1);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Configure the validate command
318
+ */
319
+ export function configureValidateCommand(program: Command): void {
320
+ program
321
+ .command("validate")
322
+ .description("Validate a SKILL.md file for common issues")
323
+ .argument("[path]", "Path to tool directory", ".")
324
+ .option("-v, --verbose", "Show detailed output")
325
+ .option("--json", "Output result as JSON")
326
+ .action((toolPath: string, options: ValidateOptions) => {
327
+ const ctx: CommandContext = {
328
+ cwd: process.cwd(),
329
+ options,
330
+ isCI: Boolean(process.env.CI),
331
+ isInteractive: process.stdout.isTTY ?? false,
332
+ };
333
+
334
+ try {
335
+ validateHandler(toolPath, options, ctx);
336
+ } catch (err) {
337
+ error(formatError(err));
338
+ if (options.verbose && err instanceof Error && err.stack) {
339
+ dim(err.stack);
340
+ }
341
+ process.exit(1);
342
+ }
343
+ });
344
+ }
package/src/index.ts CHANGED
@@ -30,12 +30,13 @@ import {
30
30
  configureSignCommand,
31
31
  configureTrustCommand,
32
32
  configureUnyankCommand,
33
+ configureValidateCommand,
33
34
  configureVisibilityCommand,
34
35
  configureYankCommand,
35
36
  } from "./commands";
36
37
  import { error, formatError } from "./utils";
37
38
 
38
- export const version = "2.1.28";
39
+ export const version = "2.1.29";
39
40
 
40
41
  // Export types for external use
41
42
  export type { GlobalOptions, CommandContext } from "./types";
@@ -85,6 +86,9 @@ async function main() {
85
86
  // MCP integration commands
86
87
  configureMcpCommand(program);
87
88
 
89
+ // Validation command
90
+ configureValidateCommand(program);
91
+
88
92
  // Global error handler - handle Commander's help/version exits gracefully
89
93
  program.exitOverride((err) => {
90
94
  // Commander throws errors for help, version, and other "exit" scenarios
package/src/types.ts CHANGED
@@ -16,6 +16,8 @@ export interface GlobalOptions {
16
16
  quiet?: boolean;
17
17
  /** Run without making changes (preview mode) */
18
18
  dryRun?: boolean;
19
+ /** Enable debug mode - show detailed parameter and environment information */
20
+ debug?: boolean;
19
21
  }
20
22
 
21
23
  /**
@@ -565,7 +565,7 @@ describe("init command", () => {
565
565
 
566
566
  // Should be comprehensive but not excessive
567
567
  const lines = content.split("\n").length;
568
- expect(lines).toBeLessThan(250); // Comprehensive guide under 250 lines
568
+ expect(lines).toBeLessThan(350); // Comprehensive guide under 350 lines (includes base image docs, troubleshooting)
569
569
  expect(lines).toBeGreaterThan(100); // But not too sparse
570
570
 
571
571
  // Clean up
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Tests for the validate command
3
+ */
4
+
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { Command } from "commander";
9
+ import { configureValidateCommand } from "../../src/commands/validate";
10
+
11
+ // Test fixtures directory
12
+ const FIXTURES_DIR = join(import.meta.dir, "..", "fixtures", "validate-cmd");
13
+
14
+ describe("validate command", () => {
15
+ beforeAll(() => {
16
+ mkdirSync(FIXTURES_DIR, { recursive: true });
17
+ });
18
+
19
+ afterAll(() => {
20
+ if (existsSync(FIXTURES_DIR)) {
21
+ rmSync(FIXTURES_DIR, { recursive: true, force: true });
22
+ }
23
+ });
24
+
25
+ describe("command configuration", () => {
26
+ test("configures validate command on program", () => {
27
+ const program = new Command();
28
+ configureValidateCommand(program);
29
+
30
+ const validateCmd = program.commands.find((cmd) => cmd.name() === "validate");
31
+ expect(validateCmd).toBeDefined();
32
+ });
33
+
34
+ test("has correct description", () => {
35
+ const program = new Command();
36
+ configureValidateCommand(program);
37
+
38
+ const validateCmd = program.commands.find((cmd) => cmd.name() === "validate");
39
+ expect(validateCmd?.description()).toBe("Validate a SKILL.md file for common issues");
40
+ });
41
+
42
+ test("accepts optional path argument", () => {
43
+ const program = new Command();
44
+ configureValidateCommand(program);
45
+
46
+ const validateCmd = program.commands.find((cmd) => cmd.name() === "validate");
47
+ const args = validateCmd?.registeredArguments ?? [];
48
+ expect(args.length).toBeGreaterThanOrEqual(1);
49
+ expect(args[0]?.name()).toBe("path");
50
+ });
51
+
52
+ test("path argument defaults to current directory", () => {
53
+ const program = new Command();
54
+ configureValidateCommand(program);
55
+
56
+ const validateCmd = program.commands.find((cmd) => cmd.name() === "validate");
57
+ const args = validateCmd?.registeredArguments ?? [];
58
+ expect(args[0]?.defaultValue).toBe(".");
59
+ });
60
+
61
+ test("has --verbose option", () => {
62
+ const program = new Command();
63
+ configureValidateCommand(program);
64
+
65
+ const validateCmd = program.commands.find((cmd) => cmd.name() === "validate");
66
+ const opts = validateCmd?.options ?? [];
67
+ const verboseOpt = opts.find((o) => o.long === "--verbose");
68
+ expect(verboseOpt).toBeDefined();
69
+ });
70
+
71
+ test("has --json option", () => {
72
+ const program = new Command();
73
+ configureValidateCommand(program);
74
+
75
+ const validateCmd = program.commands.find((cmd) => cmd.name() === "validate");
76
+ const opts = validateCmd?.options ?? [];
77
+ const jsonOpt = opts.find((o) => o.long === "--json");
78
+ expect(jsonOpt).toBeDefined();
79
+ });
80
+ });
81
+ });