@enactprotocol/shared 2.1.23 → 2.1.24
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/manifest/index.d.ts +2 -2
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/loader.d.ts +14 -4
- package/dist/manifest/loader.d.ts.map +1 -1
- package/dist/manifest/loader.js +12 -8
- package/dist/manifest/loader.js.map +1 -1
- package/dist/manifest/validator.d.ts +21 -4
- package/dist/manifest/validator.d.ts.map +1 -1
- package/dist/manifest/validator.js +72 -47
- package/dist/manifest/validator.js.map +1 -1
- package/dist/resolver.d.ts +3 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +12 -5
- package/dist/resolver.js.map +1 -1
- package/package.json +2 -2
- package/src/manifest/index.ts +3 -0
- package/src/manifest/loader.ts +29 -9
- package/src/manifest/validator.ts +102 -57
- package/src/resolver.ts +18 -6
- package/tests/manifest/validator.test.ts +77 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/manifest/loader.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { basename, join } from "node:path";
|
|
|
9
9
|
import type { ParsedManifest, ToolManifest, ValidationResult } from "../types/manifest";
|
|
10
10
|
import { MANIFEST_FILES } from "../types/manifest";
|
|
11
11
|
import { ManifestParseError, parseManifestAuto } from "./parser";
|
|
12
|
-
import { validateManifest } from "./validator";
|
|
12
|
+
import { type ValidateManifestOptions, validateManifest } from "./validator";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Error thrown when loading a manifest fails
|
|
@@ -41,14 +41,22 @@ export interface LoadedManifest {
|
|
|
41
41
|
warnings?: ValidationResult["warnings"];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Options for loading a manifest
|
|
46
|
+
*/
|
|
47
|
+
export interface LoadManifestOptions extends ValidateManifestOptions {
|
|
48
|
+
// Inherits allowSimpleNames from ValidateManifestOptions
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* Load a manifest from a file path
|
|
46
53
|
*
|
|
47
54
|
* @param filePath - Path to the manifest file (SKILL.md, enact.md, enact.yaml, or enact.yml)
|
|
55
|
+
* @param options - Options for loading and validation
|
|
48
56
|
* @returns LoadedManifest with validated manifest and metadata
|
|
49
57
|
* @throws ManifestLoadError if file doesn't exist, parse fails, or validation fails
|
|
50
58
|
*/
|
|
51
|
-
export function loadManifest(filePath: string): LoadedManifest {
|
|
59
|
+
export function loadManifest(filePath: string, options: LoadManifestOptions = {}): LoadedManifest {
|
|
52
60
|
// Check file exists
|
|
53
61
|
if (!existsSync(filePath)) {
|
|
54
62
|
throw new ManifestLoadError(`Manifest file not found: ${filePath}`, filePath);
|
|
@@ -82,7 +90,7 @@ export function loadManifest(filePath: string): LoadedManifest {
|
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
// Validate the manifest
|
|
85
|
-
const validation = validateManifest(parsed.manifest);
|
|
93
|
+
const validation = validateManifest(parsed.manifest, options);
|
|
86
94
|
|
|
87
95
|
if (!validation.valid) {
|
|
88
96
|
const errorMessages =
|
|
@@ -114,15 +122,19 @@ export function loadManifest(filePath: string): LoadedManifest {
|
|
|
114
122
|
* Searches for SKILL.md, enact.md, enact.yaml, or enact.yml in the given directory
|
|
115
123
|
*
|
|
116
124
|
* @param dir - Directory to search for manifest
|
|
125
|
+
* @param options - Options for loading and validation
|
|
117
126
|
* @returns LoadedManifest if found
|
|
118
127
|
* @throws ManifestLoadError if no manifest found or loading fails
|
|
119
128
|
*/
|
|
120
|
-
export function loadManifestFromDir(
|
|
129
|
+
export function loadManifestFromDir(
|
|
130
|
+
dir: string,
|
|
131
|
+
options: LoadManifestOptions = {}
|
|
132
|
+
): LoadedManifest {
|
|
121
133
|
// Try each manifest filename in order of preference
|
|
122
134
|
for (const filename of MANIFEST_FILES) {
|
|
123
135
|
const filePath = join(dir, filename);
|
|
124
136
|
if (existsSync(filePath)) {
|
|
125
|
-
return loadManifest(filePath);
|
|
137
|
+
return loadManifest(filePath, options);
|
|
126
138
|
}
|
|
127
139
|
}
|
|
128
140
|
|
|
@@ -162,11 +174,15 @@ export function hasManifest(dir: string): boolean {
|
|
|
162
174
|
* Try to load a manifest, returning null instead of throwing
|
|
163
175
|
*
|
|
164
176
|
* @param filePath - Path to the manifest file
|
|
177
|
+
* @param options - Options for loading and validation
|
|
165
178
|
* @returns LoadedManifest or null if loading fails
|
|
166
179
|
*/
|
|
167
|
-
export function tryLoadManifest(
|
|
180
|
+
export function tryLoadManifest(
|
|
181
|
+
filePath: string,
|
|
182
|
+
options: LoadManifestOptions = {}
|
|
183
|
+
): LoadedManifest | null {
|
|
168
184
|
try {
|
|
169
|
-
return loadManifest(filePath);
|
|
185
|
+
return loadManifest(filePath, options);
|
|
170
186
|
} catch {
|
|
171
187
|
return null;
|
|
172
188
|
}
|
|
@@ -176,11 +192,15 @@ export function tryLoadManifest(filePath: string): LoadedManifest | null {
|
|
|
176
192
|
* Try to load a manifest from a directory, returning null instead of throwing
|
|
177
193
|
*
|
|
178
194
|
* @param dir - Directory to search
|
|
195
|
+
* @param options - Options for loading and validation
|
|
179
196
|
* @returns LoadedManifest or null if no manifest found or loading fails
|
|
180
197
|
*/
|
|
181
|
-
export function tryLoadManifestFromDir(
|
|
198
|
+
export function tryLoadManifestFromDir(
|
|
199
|
+
dir: string,
|
|
200
|
+
options: LoadManifestOptions = {}
|
|
201
|
+
): LoadedManifest | null {
|
|
182
202
|
try {
|
|
183
|
-
return loadManifestFromDir(dir);
|
|
203
|
+
return loadManifestFromDir(dir, options);
|
|
184
204
|
} catch {
|
|
185
205
|
return null;
|
|
186
206
|
}
|
|
@@ -83,68 +83,85 @@ const SEMVER_REGEX =
|
|
|
83
83
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* Tool name regex - hierarchical path format
|
|
86
|
+
* Tool name regex - hierarchical path format (required for publishing)
|
|
87
87
|
*/
|
|
88
88
|
const TOOL_NAME_REGEX = /^[a-z0-9_-]+(?:\/[a-z0-9_-]+)+$/;
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Tool name regex - simple format (allowed for local tools)
|
|
92
|
+
* Allows both hierarchical (org/tool) and simple (my-tool) names
|
|
93
|
+
*/
|
|
94
|
+
const TOOL_NAME_REGEX_LOCAL = /^[a-z0-9_-]+(?:\/[a-z0-9_-]+)*$/;
|
|
95
|
+
|
|
90
96
|
/**
|
|
91
97
|
* Go duration regex (used for timeout)
|
|
92
98
|
*/
|
|
93
99
|
const GO_DURATION_REGEX = /^(\d+)(ns|us|µs|ms|s|m|h)$/;
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
|
-
*
|
|
102
|
+
* Create a tool manifest schema with configurable name validation
|
|
97
103
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
name
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
104
|
+
function createToolManifestSchema(allowSimpleNames: boolean) {
|
|
105
|
+
const nameRegex = allowSimpleNames ? TOOL_NAME_REGEX_LOCAL : TOOL_NAME_REGEX;
|
|
106
|
+
const nameMessage = allowSimpleNames
|
|
107
|
+
? "Tool name must contain only lowercase letters, numbers, hyphens, and underscores"
|
|
108
|
+
: "Tool name must be hierarchical path format (e.g., 'org/tool' or 'org/category/tool')";
|
|
109
|
+
|
|
110
|
+
return z
|
|
111
|
+
.object({
|
|
112
|
+
// Required fields
|
|
113
|
+
name: z.string().min(1, "Tool name is required").regex(nameRegex, nameMessage),
|
|
114
|
+
|
|
115
|
+
description: z
|
|
116
|
+
.string()
|
|
117
|
+
.min(1, "Description is required")
|
|
118
|
+
.max(500, "Description should be 500 characters or less"),
|
|
119
|
+
|
|
120
|
+
// Recommended fields
|
|
121
|
+
enact: z.string().optional(),
|
|
122
|
+
version: z
|
|
123
|
+
.string()
|
|
124
|
+
.regex(SEMVER_REGEX, "Version must be valid semver (e.g., '1.0.0')")
|
|
125
|
+
.optional(),
|
|
126
|
+
from: z.string().optional(),
|
|
127
|
+
command: z.string().optional(),
|
|
128
|
+
timeout: z
|
|
129
|
+
.string()
|
|
130
|
+
.regex(GO_DURATION_REGEX, "Timeout must be Go duration format (e.g., '30s', '5m', '1h')")
|
|
131
|
+
.optional(),
|
|
132
|
+
license: z.string().optional(),
|
|
133
|
+
tags: z.array(z.string()).optional(),
|
|
134
|
+
|
|
135
|
+
// Schema fields
|
|
136
|
+
inputSchema: JsonSchemaSchema.optional(),
|
|
137
|
+
outputSchema: JsonSchemaSchema.optional(),
|
|
138
|
+
|
|
139
|
+
// Environment variables
|
|
140
|
+
env: z.record(z.string(), EnvVariableSchema).optional(),
|
|
141
|
+
|
|
142
|
+
// Behavior & Resources
|
|
143
|
+
annotations: ToolAnnotationsSchema.optional(),
|
|
144
|
+
resources: ResourceRequirementsSchema.optional(),
|
|
145
|
+
|
|
146
|
+
// Documentation
|
|
147
|
+
doc: z.string().optional(),
|
|
148
|
+
authors: z.array(AuthorSchema).optional(),
|
|
149
|
+
|
|
150
|
+
// Testing
|
|
151
|
+
examples: z.array(ToolExampleSchema).optional(),
|
|
152
|
+
})
|
|
153
|
+
.passthrough(); // Allow x-* custom fields
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Complete tool manifest schema (strict - requires hierarchical names)
|
|
158
|
+
*/
|
|
159
|
+
const ToolManifestSchema = createToolManifestSchema(false);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Local tool manifest schema (relaxed - allows simple names)
|
|
163
|
+
*/
|
|
164
|
+
const ToolManifestSchemaLocal = createToolManifestSchema(true);
|
|
148
165
|
|
|
149
166
|
// ==================== Validation Functions ====================
|
|
150
167
|
|
|
@@ -239,14 +256,31 @@ function generateWarnings(manifest: ToolManifest): ValidationWarning[] {
|
|
|
239
256
|
return warnings;
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Options for manifest validation
|
|
261
|
+
*/
|
|
262
|
+
export interface ValidateManifestOptions {
|
|
263
|
+
/**
|
|
264
|
+
* Allow simple tool names without hierarchy (e.g., "my-tool" instead of "org/my-tool").
|
|
265
|
+
* Use this for local tools that won't be published.
|
|
266
|
+
* @default false
|
|
267
|
+
*/
|
|
268
|
+
allowSimpleNames?: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
242
271
|
/**
|
|
243
272
|
* Validate a tool manifest
|
|
244
273
|
*
|
|
245
274
|
* @param manifest - The manifest to validate (parsed but unvalidated)
|
|
275
|
+
* @param options - Validation options
|
|
246
276
|
* @returns ValidationResult with valid flag, errors, and warnings
|
|
247
277
|
*/
|
|
248
|
-
export function validateManifest(
|
|
249
|
-
|
|
278
|
+
export function validateManifest(
|
|
279
|
+
manifest: unknown,
|
|
280
|
+
options: ValidateManifestOptions = {}
|
|
281
|
+
): ValidationResult {
|
|
282
|
+
const schema = options.allowSimpleNames ? ToolManifestSchemaLocal : ToolManifestSchema;
|
|
283
|
+
const result = schema.safeParse(manifest);
|
|
250
284
|
|
|
251
285
|
if (!result.success) {
|
|
252
286
|
return {
|
|
@@ -270,11 +304,15 @@ export function validateManifest(manifest: unknown): ValidationResult {
|
|
|
270
304
|
* Throws if validation fails
|
|
271
305
|
*
|
|
272
306
|
* @param manifest - The manifest to validate
|
|
307
|
+
* @param options - Validation options
|
|
273
308
|
* @returns The validated ToolManifest
|
|
274
309
|
* @throws Error if validation fails
|
|
275
310
|
*/
|
|
276
|
-
export function validateManifestStrict(
|
|
277
|
-
|
|
311
|
+
export function validateManifestStrict(
|
|
312
|
+
manifest: unknown,
|
|
313
|
+
options: ValidateManifestOptions = {}
|
|
314
|
+
): ToolManifest {
|
|
315
|
+
const result = validateManifest(manifest, options);
|
|
278
316
|
|
|
279
317
|
if (!result.valid) {
|
|
280
318
|
const errorMessages = result.errors?.map((e) => `${e.path}: ${e.message}`).join(", ");
|
|
@@ -285,12 +323,19 @@ export function validateManifestStrict(manifest: unknown): ToolManifest {
|
|
|
285
323
|
}
|
|
286
324
|
|
|
287
325
|
/**
|
|
288
|
-
* Check if a string is a valid tool name
|
|
326
|
+
* Check if a string is a valid tool name (hierarchical format for publishing)
|
|
289
327
|
*/
|
|
290
328
|
export function isValidToolName(name: string): boolean {
|
|
291
329
|
return TOOL_NAME_REGEX.test(name);
|
|
292
330
|
}
|
|
293
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Check if a string is a valid local tool name (allows simple names)
|
|
334
|
+
*/
|
|
335
|
+
export function isValidLocalToolName(name: string): boolean {
|
|
336
|
+
return TOOL_NAME_REGEX_LOCAL.test(name);
|
|
337
|
+
}
|
|
338
|
+
|
|
294
339
|
/**
|
|
295
340
|
* Check if a string is a valid semver version
|
|
296
341
|
*/
|
package/src/resolver.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readdirSync } from "node:fs";
|
|
12
12
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
|
-
import { findManifestFile, loadManifest } from "./manifest/loader";
|
|
13
|
+
import { type LoadManifestOptions, findManifestFile, loadManifest } from "./manifest/loader";
|
|
14
14
|
import { getCacheDir, getProjectEnactDir } from "./paths";
|
|
15
15
|
import { getInstalledVersion, getToolCachePath } from "./registry";
|
|
16
16
|
import type { ToolLocation, ToolResolution } from "./types/manifest";
|
|
@@ -73,9 +73,14 @@ export function getToolPath(toolsDir: string, toolName: string): string {
|
|
|
73
73
|
*
|
|
74
74
|
* @param dir - Directory to check
|
|
75
75
|
* @param location - The location type for metadata
|
|
76
|
+
* @param options - Options for loading the manifest
|
|
76
77
|
* @returns ToolResolution or null if not found/invalid
|
|
77
78
|
*/
|
|
78
|
-
function tryLoadFromDir(
|
|
79
|
+
function tryLoadFromDir(
|
|
80
|
+
dir: string,
|
|
81
|
+
location: ToolLocation,
|
|
82
|
+
options: LoadManifestOptions = {}
|
|
83
|
+
): ToolResolution | null {
|
|
79
84
|
if (!existsSync(dir)) {
|
|
80
85
|
return null;
|
|
81
86
|
}
|
|
@@ -86,7 +91,7 @@ function tryLoadFromDir(dir: string, location: ToolLocation): ToolResolution | n
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
try {
|
|
89
|
-
const loaded = loadManifest(manifestPath);
|
|
94
|
+
const loaded = loadManifest(manifestPath, options);
|
|
90
95
|
return {
|
|
91
96
|
manifest: loaded.manifest,
|
|
92
97
|
sourceDir: dir,
|
|
@@ -103,6 +108,9 @@ function tryLoadFromDir(dir: string, location: ToolLocation): ToolResolution | n
|
|
|
103
108
|
/**
|
|
104
109
|
* Resolve a tool from a file path
|
|
105
110
|
*
|
|
111
|
+
* Local/file tools are allowed to have simple names (without hierarchy)
|
|
112
|
+
* since they don't need to be published.
|
|
113
|
+
*
|
|
106
114
|
* @param filePath - Path to manifest file or directory containing manifest
|
|
107
115
|
* @returns ToolResolution
|
|
108
116
|
* @throws ToolResolveError if not found
|
|
@@ -110,6 +118,9 @@ function tryLoadFromDir(dir: string, location: ToolLocation): ToolResolution | n
|
|
|
110
118
|
export function resolveToolFromPath(filePath: string): ToolResolution {
|
|
111
119
|
const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
112
120
|
|
|
121
|
+
// Local tools can have simple names (no hierarchy required)
|
|
122
|
+
const localOptions: LoadManifestOptions = { allowSimpleNames: true };
|
|
123
|
+
|
|
113
124
|
// Check if it's a manifest file directly
|
|
114
125
|
if (
|
|
115
126
|
absolutePath.endsWith(".yaml") ||
|
|
@@ -120,7 +131,7 @@ export function resolveToolFromPath(filePath: string): ToolResolution {
|
|
|
120
131
|
throw new ToolResolveError(`Manifest file not found: ${absolutePath}`, filePath);
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
const loaded = loadManifest(absolutePath);
|
|
134
|
+
const loaded = loadManifest(absolutePath, localOptions);
|
|
124
135
|
return {
|
|
125
136
|
manifest: loaded.manifest,
|
|
126
137
|
sourceDir: dirname(absolutePath),
|
|
@@ -131,7 +142,7 @@ export function resolveToolFromPath(filePath: string): ToolResolution {
|
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
// Treat as directory
|
|
134
|
-
const result = tryLoadFromDir(absolutePath, "file");
|
|
145
|
+
const result = tryLoadFromDir(absolutePath, "file", localOptions);
|
|
135
146
|
if (result) {
|
|
136
147
|
return result;
|
|
137
148
|
}
|
|
@@ -298,7 +309,8 @@ export function resolveToolAuto(
|
|
|
298
309
|
|
|
299
310
|
// Check if the path exists as-is (could be a relative directory without ./)
|
|
300
311
|
if (existsSync(toolNameOrPath)) {
|
|
301
|
-
|
|
312
|
+
// Local tools can have simple names (no hierarchy required)
|
|
313
|
+
const result = tryLoadFromDir(resolve(toolNameOrPath), "file", { allowSimpleNames: true });
|
|
302
314
|
if (result) {
|
|
303
315
|
return result;
|
|
304
316
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
isValidLocalToolName,
|
|
3
4
|
isValidTimeout,
|
|
4
5
|
isValidToolName,
|
|
5
6
|
isValidVersion,
|
|
@@ -237,6 +238,63 @@ describe("manifest validator", () => {
|
|
|
237
238
|
});
|
|
238
239
|
});
|
|
239
240
|
|
|
241
|
+
describe("allowSimpleNames option", () => {
|
|
242
|
+
test("rejects simple names by default", () => {
|
|
243
|
+
const manifest = {
|
|
244
|
+
name: "my-tool", // No slash
|
|
245
|
+
description: "A local tool",
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = validateManifest(manifest);
|
|
249
|
+
expect(result.valid).toBe(false);
|
|
250
|
+
expect(result.errors?.some((e) => e.path === "name")).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("accepts simple names with allowSimpleNames option", () => {
|
|
254
|
+
const manifest = {
|
|
255
|
+
name: "my-tool",
|
|
256
|
+
description: "A local tool",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = validateManifest(manifest, { allowSimpleNames: true });
|
|
260
|
+
expect(result.valid).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("accepts hierarchical names with allowSimpleNames option", () => {
|
|
264
|
+
const manifest = {
|
|
265
|
+
name: "org/tool",
|
|
266
|
+
description: "A published tool",
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = validateManifest(manifest, { allowSimpleNames: true });
|
|
270
|
+
expect(result.valid).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("still rejects invalid characters with allowSimpleNames", () => {
|
|
274
|
+
const manifest = {
|
|
275
|
+
name: "My-Tool", // Uppercase not allowed
|
|
276
|
+
description: "Invalid tool name",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const result = validateManifest(manifest, { allowSimpleNames: true });
|
|
280
|
+
expect(result.valid).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("validateManifestStrict respects allowSimpleNames option", () => {
|
|
284
|
+
const manifest = {
|
|
285
|
+
name: "local-tool",
|
|
286
|
+
description: "A local tool",
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Should throw without option
|
|
290
|
+
expect(() => validateManifestStrict(manifest)).toThrow();
|
|
291
|
+
|
|
292
|
+
// Should succeed with option
|
|
293
|
+
const result = validateManifestStrict(manifest, { allowSimpleNames: true });
|
|
294
|
+
expect(result.name).toBe("local-tool");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
240
298
|
describe("warnings", () => {
|
|
241
299
|
test("warns about missing recommended fields", () => {
|
|
242
300
|
const manifest = {
|
|
@@ -360,6 +418,25 @@ describe("manifest validator", () => {
|
|
|
360
418
|
});
|
|
361
419
|
});
|
|
362
420
|
|
|
421
|
+
describe("isValidLocalToolName", () => {
|
|
422
|
+
test("returns true for simple names (no hierarchy)", () => {
|
|
423
|
+
expect(isValidLocalToolName("my-tool")).toBe(true);
|
|
424
|
+
expect(isValidLocalToolName("tool_name")).toBe(true);
|
|
425
|
+
expect(isValidLocalToolName("simple")).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("returns true for hierarchical names", () => {
|
|
429
|
+
expect(isValidLocalToolName("org/tool")).toBe(true);
|
|
430
|
+
expect(isValidLocalToolName("acme/utils/greeter")).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("returns false for invalid names", () => {
|
|
434
|
+
expect(isValidLocalToolName("My-Tool")).toBe(false); // Uppercase
|
|
435
|
+
expect(isValidLocalToolName("tool name")).toBe(false); // Space
|
|
436
|
+
expect(isValidLocalToolName("")).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
363
440
|
describe("isValidVersion", () => {
|
|
364
441
|
test("returns true for valid semver", () => {
|
|
365
442
|
expect(isValidVersion("1.0.0")).toBe(true);
|