@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.
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init/templates/claude.d.ts +1 -1
- package/dist/commands/init/templates/claude.d.ts.map +1 -1
- package/dist/commands/init/templates/claude.js +267 -27
- package/dist/commands/init/templates/claude.js.map +1 -1
- package/dist/commands/init/templates/tool-agents.d.ts +1 -1
- package/dist/commands/init/templates/tool-agents.d.ts.map +1 -1
- package/dist/commands/init/templates/tool-agents.js +82 -7
- package/dist/commands/init/templates/tool-agents.js.map +1 -1
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +4 -11
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +86 -5
- package/dist/commands/run/index.js.map +1 -1
- package/dist/commands/validate/index.d.ts +11 -0
- package/dist/commands/validate/index.d.ts.map +1 -0
- package/dist/commands/validate/index.js +299 -0
- package/dist/commands/validate/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/index.ts +3 -0
- package/src/commands/init/templates/claude.ts +267 -27
- package/src/commands/init/templates/tool-agents.ts +82 -7
- package/src/commands/run/README.md +17 -0
- package/src/commands/run/index.ts +105 -5
- package/src/commands/validate/index.ts +344 -0
- package/src/index.ts +5 -1
- package/src/types.ts +2 -0
- package/tests/commands/init.test.ts +1 -1
- package/tests/commands/validate.test.ts +81 -0
- 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 (
|
|
743
|
+
// Show stdout if present (useful for debugging - command may have printed before failing)
|
|
744
|
+
if (result.output?.stdout?.trim()) {
|
|
665
745
|
newline();
|
|
666
|
-
|
|
746
|
+
info("stdout:");
|
|
747
|
+
console.log(result.output.stdout);
|
|
667
748
|
}
|
|
668
749
|
|
|
669
|
-
|
|
750
|
+
// Show stderr (the actual error output)
|
|
751
|
+
if (result.output?.stderr?.trim()) {
|
|
670
752
|
newline();
|
|
671
|
-
|
|
672
|
-
|
|
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.
|
|
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
|
@@ -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(
|
|
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
|
+
});
|