@enactprotocol/cli 2.1.24 → 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 +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -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 +268 -28
- 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 +90 -15
- package/dist/commands/init/templates/tool-agents.js.map +1 -1
- package/dist/commands/install/index.d.ts.map +1 -1
- package/dist/commands/install/index.js +9 -1
- package/dist/commands/install/index.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/mcp/index.d.ts.map +1 -1
- package/dist/commands/mcp/index.js +204 -53
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +380 -39
- 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 +6 -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/dist/utils/errors.d.ts +8 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +13 -2
- package/dist/utils/errors.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/index.ts +5 -0
- package/src/commands/init/templates/claude.ts +268 -28
- package/src/commands/init/templates/tool-agents.ts +90 -15
- package/src/commands/install/index.ts +11 -0
- package/src/commands/learn/index.ts +6 -11
- package/src/commands/mcp/index.ts +768 -0
- package/src/commands/run/README.md +68 -1
- package/src/commands/run/index.ts +475 -35
- package/src/commands/validate/index.ts +344 -0
- package/src/index.ts +8 -1
- package/src/types.ts +2 -0
- package/src/utils/errors.ts +26 -6
- package/tests/commands/init.test.ts +2 -2
- package/tests/commands/run.test.ts +260 -0
- package/tests/commands/validate.test.ts +81 -0
- package/tests/utils/errors.test.ts +36 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
configureInstallCommand,
|
|
22
22
|
configureLearnCommand,
|
|
23
23
|
configureListCommand,
|
|
24
|
+
configureMcpCommand,
|
|
24
25
|
configurePublishCommand,
|
|
25
26
|
configureReportCommand,
|
|
26
27
|
configureRunCommand,
|
|
@@ -29,12 +30,13 @@ import {
|
|
|
29
30
|
configureSignCommand,
|
|
30
31
|
configureTrustCommand,
|
|
31
32
|
configureUnyankCommand,
|
|
33
|
+
configureValidateCommand,
|
|
32
34
|
configureVisibilityCommand,
|
|
33
35
|
configureYankCommand,
|
|
34
36
|
} from "./commands";
|
|
35
37
|
import { error, formatError } from "./utils";
|
|
36
38
|
|
|
37
|
-
export const version = "2.1.
|
|
39
|
+
export const version = "2.1.29";
|
|
38
40
|
|
|
39
41
|
// Export types for external use
|
|
40
42
|
export type { GlobalOptions, CommandContext } from "./types";
|
|
@@ -81,6 +83,11 @@ async function main() {
|
|
|
81
83
|
|
|
82
84
|
// Private tools - visibility management
|
|
83
85
|
configureVisibilityCommand(program);
|
|
86
|
+
// MCP integration commands
|
|
87
|
+
configureMcpCommand(program);
|
|
88
|
+
|
|
89
|
+
// Validation command
|
|
90
|
+
configureValidateCommand(program);
|
|
84
91
|
|
|
85
92
|
// Global error handler - handle Commander's help/version exits gracefully
|
|
86
93
|
program.exitOverride((err) => {
|
package/src/types.ts
CHANGED
package/src/utils/errors.ts
CHANGED
|
@@ -45,12 +45,32 @@ export class CliError extends Error {
|
|
|
45
45
|
* Tool not found error
|
|
46
46
|
*/
|
|
47
47
|
export class ToolNotFoundError extends CliError {
|
|
48
|
-
constructor(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
constructor(
|
|
49
|
+
toolName: string,
|
|
50
|
+
options?: {
|
|
51
|
+
/** Additional context about why the tool wasn't found */
|
|
52
|
+
reason?: string;
|
|
53
|
+
/** Locations that were searched */
|
|
54
|
+
searchedLocations?: string[];
|
|
55
|
+
/** Whether --local flag was set */
|
|
56
|
+
localOnly?: boolean;
|
|
57
|
+
}
|
|
58
|
+
) {
|
|
59
|
+
let message = `Tool not found: ${toolName}`;
|
|
60
|
+
if (options?.reason) {
|
|
61
|
+
message += `\n${options.reason}`;
|
|
62
|
+
}
|
|
63
|
+
if (options?.searchedLocations && options.searchedLocations.length > 0) {
|
|
64
|
+
message += `\nSearched locations:\n${options.searchedLocations.map((l) => ` - ${l}`).join("\n")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let suggestion =
|
|
68
|
+
"Check the tool name or provide a path to a local tool.\nFor registry tools, use the format: owner/namespace/tool[@version]";
|
|
69
|
+
if (options?.localOnly) {
|
|
70
|
+
suggestion = "Remove --local flag to search the registry, or check the tool path.";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
super(message, EXIT_TOOL_NOT_FOUND, suggestion);
|
|
54
74
|
this.name = "ToolNotFoundError";
|
|
55
75
|
}
|
|
56
76
|
}
|
|
@@ -456,7 +456,7 @@ describe("init command", () => {
|
|
|
456
456
|
expect(content).toContain("enact publish");
|
|
457
457
|
expect(content).toContain("${param}");
|
|
458
458
|
expect(content).toContain("build:");
|
|
459
|
-
expect(content).toContain("/
|
|
459
|
+
expect(content).toContain("/workspace");
|
|
460
460
|
});
|
|
461
461
|
|
|
462
462
|
test("AGENTS.md for agent projects contains usage instructions", async () => {
|
|
@@ -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
|