@enactprotocol/shared 2.2.4 → 2.3.4
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/README.md +1 -18
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -6
- package/dist/config.js.map +1 -1
- package/dist/execution/action-command.d.ts +131 -0
- package/dist/execution/action-command.d.ts.map +1 -0
- package/dist/execution/action-command.js +300 -0
- package/dist/execution/action-command.js.map +1 -0
- package/dist/execution/command.d.ts +8 -8
- package/dist/execution/command.js +6 -6
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +2 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/types.d.ts +5 -2
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/execution/types.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest/actions-loader.d.ts +29 -0
- package/dist/manifest/actions-loader.d.ts.map +1 -0
- package/dist/manifest/actions-loader.js +34 -0
- package/dist/manifest/actions-loader.js.map +1 -0
- package/dist/manifest/actions-parser.d.ts +69 -0
- package/dist/manifest/actions-parser.d.ts.map +1 -0
- package/dist/manifest/actions-parser.js +265 -0
- package/dist/manifest/actions-parser.js.map +1 -0
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +4 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/loader.d.ts +7 -2
- package/dist/manifest/loader.d.ts.map +1 -1
- package/dist/manifest/loader.js +71 -4
- package/dist/manifest/loader.js.map +1 -1
- package/dist/manifest/parser.d.ts +1 -0
- package/dist/manifest/parser.d.ts.map +1 -1
- package/dist/manifest/parser.js +1 -0
- package/dist/manifest/parser.js.map +1 -1
- package/dist/manifest/scripts.d.ts +19 -0
- package/dist/manifest/scripts.d.ts.map +1 -0
- package/dist/manifest/scripts.js +102 -0
- package/dist/manifest/scripts.js.map +1 -0
- package/dist/manifest/validator.d.ts +1 -8
- package/dist/manifest/validator.d.ts.map +1 -1
- package/dist/manifest/validator.js +14 -13
- package/dist/manifest/validator.js.map +1 -1
- package/dist/mcp-registry.js +5 -5
- package/dist/mcp-registry.js.map +1 -1
- package/dist/paths.d.ts +9 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -3
- package/dist/paths.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -5
- package/dist/registry.js.map +1 -1
- package/dist/resolver.d.ts +55 -4
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +133 -75
- package/dist/resolver.js.map +1 -1
- package/dist/types/actions.d.ts +194 -0
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/actions.js +32 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/manifest.d.ts +50 -5
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/manifest.js +10 -2
- package/dist/types/manifest.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +48 -6
- package/src/execution/action-command.ts +417 -0
- package/src/execution/command.ts +8 -8
- package/src/execution/index.ts +17 -0
- package/src/execution/types.ts +13 -2
- package/src/index.ts +37 -0
- package/src/manifest/actions-loader.ts +49 -0
- package/src/manifest/index.ts +12 -0
- package/src/manifest/loader.ts +77 -4
- package/src/manifest/parser.ts +1 -0
- package/src/manifest/scripts.ts +116 -0
- package/src/manifest/validator.ts +15 -14
- package/src/mcp-registry.ts +5 -5
- package/src/paths.ts +13 -3
- package/src/registry.ts +5 -5
- package/src/resolver.ts +172 -77
- package/src/types/actions.ts +223 -0
- package/src/types/index.ts +11 -0
- package/src/types/manifest.ts +67 -6
- package/tests/action-command.test.ts +249 -0
- package/tests/config-normalization.test.ts +279 -0
- package/tests/config.test.ts +4 -1
- package/tests/effective-input-schema.test.ts +86 -0
- package/tests/fixtures/valid-tool.md +5 -12
- package/tests/fixtures/valid-tool.yaml +3 -10
- package/tests/hooks.test.ts +177 -0
- package/tests/manifest/loader.test.ts +34 -20
- package/tests/manifest/parser.test.ts +11 -15
- package/tests/manifest/validator.test.ts +7 -17
- package/tests/manifest-types.test.ts +9 -11
- package/tests/paths.test.ts +11 -4
- package/tests/registry.test.ts +12 -11
- package/tests/resolver.test.ts +11 -7
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for getEffectiveInputSchema and DEFAULT_INPUT_SCHEMA.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { DEFAULT_INPUT_SCHEMA, getEffectiveInputSchema } from "../src/types/actions";
|
|
7
|
+
import type { Action } from "../src/types/actions";
|
|
8
|
+
|
|
9
|
+
describe("DEFAULT_INPUT_SCHEMA", () => {
|
|
10
|
+
test("is an object type schema", () => {
|
|
11
|
+
expect(DEFAULT_INPUT_SCHEMA.type).toBe("object");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("has empty properties", () => {
|
|
15
|
+
expect(DEFAULT_INPUT_SCHEMA.properties).toEqual({});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("has no required fields", () => {
|
|
19
|
+
expect(DEFAULT_INPUT_SCHEMA.required).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("getEffectiveInputSchema", () => {
|
|
24
|
+
test("returns action inputSchema when provided", () => {
|
|
25
|
+
const action: Action = {
|
|
26
|
+
description: "test",
|
|
27
|
+
command: ["echo"],
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: { name: { type: "string" } },
|
|
31
|
+
required: ["name"],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const schema = getEffectiveInputSchema(action);
|
|
36
|
+
expect(schema).toBe(action.inputSchema!);
|
|
37
|
+
expect(schema.required).toEqual(["name"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns DEFAULT_INPUT_SCHEMA when no inputSchema", () => {
|
|
41
|
+
const action: Action = {
|
|
42
|
+
description: "test",
|
|
43
|
+
command: "echo hello",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const schema = getEffectiveInputSchema(action);
|
|
47
|
+
expect(schema).toBe(DEFAULT_INPUT_SCHEMA);
|
|
48
|
+
expect(schema.type).toBe("object");
|
|
49
|
+
expect(schema.properties).toEqual({});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns DEFAULT_INPUT_SCHEMA when inputSchema is undefined", () => {
|
|
53
|
+
const action = {
|
|
54
|
+
description: "test",
|
|
55
|
+
command: ["echo", "hello"],
|
|
56
|
+
} as Action;
|
|
57
|
+
|
|
58
|
+
const schema = getEffectiveInputSchema(action);
|
|
59
|
+
expect(schema).toBe(DEFAULT_INPUT_SCHEMA);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("preserves complex inputSchema with nested properties", () => {
|
|
63
|
+
const action: Action = {
|
|
64
|
+
description: "test",
|
|
65
|
+
command: ["cmd"],
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
config: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
depth: { type: "number", default: 3 },
|
|
73
|
+
format: { type: "string", enum: ["json", "csv"] },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["config"],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const schema = getEffectiveInputSchema(action);
|
|
82
|
+
expect(schema.required).toEqual(["config"]);
|
|
83
|
+
const props = schema.properties as Record<string, { type: string }>;
|
|
84
|
+
expect(props.config!.type).toBe("object");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
enact: "2.0.0"
|
|
3
|
-
name: acme/
|
|
3
|
+
name: acme/analyzer
|
|
4
4
|
description: Analyzes documentation for completeness
|
|
5
5
|
version: "1.2.0"
|
|
6
6
|
from: python:3.11-slim
|
|
7
|
-
command: "python analyze.py ${file}"
|
|
8
7
|
timeout: 5m
|
|
9
8
|
license: Apache-2.0
|
|
10
9
|
tags:
|
|
11
10
|
- documentation
|
|
12
11
|
- analysis
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
properties:
|
|
16
|
-
file:
|
|
17
|
-
type: string
|
|
18
|
-
description: Path to the documentation file
|
|
19
|
-
required:
|
|
20
|
-
- file
|
|
12
|
+
scripts:
|
|
13
|
+
analyze: "python analyze.py {{file}}"
|
|
21
14
|
outputSchema:
|
|
22
15
|
type: object
|
|
23
16
|
properties:
|
|
@@ -39,7 +32,7 @@ Provide a path to a documentation file and get a quality score along with
|
|
|
39
32
|
any issues found.
|
|
40
33
|
|
|
41
34
|
```bash
|
|
42
|
-
enact run acme/
|
|
35
|
+
enact run acme/analyzer --file README.md
|
|
43
36
|
```
|
|
44
37
|
|
|
45
38
|
## Output
|
|
@@ -54,7 +47,7 @@ The tool returns a JSON object with:
|
|
|
54
47
|
### Basic Analysis
|
|
55
48
|
|
|
56
49
|
```bash
|
|
57
|
-
enact run acme/
|
|
50
|
+
enact run acme/analyzer --file docs/api.md
|
|
58
51
|
```
|
|
59
52
|
|
|
60
53
|
### Integration with CI
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
enact: "2.0.0"
|
|
2
|
-
name: acme/
|
|
2
|
+
name: acme/greeter
|
|
3
3
|
description: Greets users by name
|
|
4
4
|
version: "1.0.0"
|
|
5
5
|
from: node:18-alpine
|
|
6
|
-
command: "echo 'Hello ${name}!'"
|
|
7
6
|
timeout: 30s
|
|
8
7
|
license: MIT
|
|
9
8
|
tags:
|
|
10
9
|
- greeting
|
|
11
10
|
- utility
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
properties:
|
|
15
|
-
name:
|
|
16
|
-
type: string
|
|
17
|
-
description: The name to greet
|
|
18
|
-
required:
|
|
19
|
-
- name
|
|
11
|
+
scripts:
|
|
12
|
+
greet: "echo 'Hello {{name}}!'"
|
|
20
13
|
outputSchema:
|
|
21
14
|
type: object
|
|
22
15
|
properties:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ToolHooks type and postinstall hook behavior.
|
|
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 type { ToolHooks, ToolManifest } from "../src/types/manifest";
|
|
9
|
+
|
|
10
|
+
const FIXTURES_DIR = join(import.meta.dir, "fixtures", "hooks-test");
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
mkdirSync(FIXTURES_DIR, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
if (existsSync(FIXTURES_DIR)) {
|
|
18
|
+
rmSync(FIXTURES_DIR, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("ToolHooks type", () => {
|
|
23
|
+
test("manifest accepts hooks with postinstall string", () => {
|
|
24
|
+
const manifest: ToolManifest = {
|
|
25
|
+
name: "@test/with-hook",
|
|
26
|
+
description: "A tool with a postinstall hook",
|
|
27
|
+
hooks: {
|
|
28
|
+
postinstall: "npm install",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
expect(manifest.hooks?.postinstall).toBe("npm install");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("manifest accepts hooks with postinstall array", () => {
|
|
35
|
+
const manifest: ToolManifest = {
|
|
36
|
+
name: "@test/with-hooks",
|
|
37
|
+
description: "A tool with multiple postinstall commands",
|
|
38
|
+
hooks: {
|
|
39
|
+
postinstall: ["npm install", "npm run build"],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
expect(manifest.hooks?.postinstall).toEqual(["npm install", "npm run build"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("manifest works without hooks", () => {
|
|
46
|
+
const manifest: ToolManifest = {
|
|
47
|
+
name: "@test/no-hooks",
|
|
48
|
+
description: "A tool without hooks",
|
|
49
|
+
};
|
|
50
|
+
expect(manifest.hooks).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("hooks field can be empty object", () => {
|
|
54
|
+
const hooks: ToolHooks = {};
|
|
55
|
+
expect(hooks.postinstall).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("postinstall hook execution", () => {
|
|
60
|
+
test("single command creates expected side-effect", async () => {
|
|
61
|
+
const toolDir = join(FIXTURES_DIR, "single-cmd");
|
|
62
|
+
mkdirSync(toolDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const markerFile = join(toolDir, "postinstall-ran.txt");
|
|
65
|
+
const cmd = `echo "hook executed" > postinstall-ran.txt`;
|
|
66
|
+
|
|
67
|
+
const proc = Bun.spawn(["sh", "-c", cmd], {
|
|
68
|
+
cwd: toolDir,
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "pipe",
|
|
71
|
+
});
|
|
72
|
+
await proc.exited;
|
|
73
|
+
|
|
74
|
+
expect(existsSync(markerFile)).toBe(true);
|
|
75
|
+
const content = await Bun.file(markerFile).text();
|
|
76
|
+
expect(content.trim()).toBe("hook executed");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("multiple commands run sequentially", async () => {
|
|
80
|
+
const toolDir = join(FIXTURES_DIR, "multi-cmd");
|
|
81
|
+
mkdirSync(toolDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
const commands = [
|
|
84
|
+
'echo "step1" > step1.txt',
|
|
85
|
+
'echo "step2" > step2.txt',
|
|
86
|
+
"cat step1.txt step2.txt > combined.txt",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const cmd of commands) {
|
|
90
|
+
const proc = Bun.spawn(["sh", "-c", cmd], {
|
|
91
|
+
cwd: toolDir,
|
|
92
|
+
stdout: "pipe",
|
|
93
|
+
stderr: "pipe",
|
|
94
|
+
});
|
|
95
|
+
const exitCode = await proc.exited;
|
|
96
|
+
expect(exitCode).toBe(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const combined = await Bun.file(join(toolDir, "combined.txt")).text();
|
|
100
|
+
expect(combined).toContain("step1");
|
|
101
|
+
expect(combined).toContain("step2");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("failing command returns non-zero exit code", async () => {
|
|
105
|
+
const toolDir = join(FIXTURES_DIR, "fail-cmd");
|
|
106
|
+
mkdirSync(toolDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
const proc = Bun.spawn(["sh", "-c", "exit 1"], {
|
|
109
|
+
cwd: toolDir,
|
|
110
|
+
stdout: "pipe",
|
|
111
|
+
stderr: "pipe",
|
|
112
|
+
});
|
|
113
|
+
const exitCode = await proc.exited;
|
|
114
|
+
|
|
115
|
+
expect(exitCode).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("command runs in the tool directory (cwd)", async () => {
|
|
119
|
+
const toolDir = join(FIXTURES_DIR, "cwd-check");
|
|
120
|
+
mkdirSync(toolDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
const proc = Bun.spawn(["sh", "-c", "pwd"], {
|
|
123
|
+
cwd: toolDir,
|
|
124
|
+
stdout: "pipe",
|
|
125
|
+
stderr: "pipe",
|
|
126
|
+
});
|
|
127
|
+
await proc.exited;
|
|
128
|
+
|
|
129
|
+
const output = await new Response(proc.stdout).text();
|
|
130
|
+
expect(output.trim()).toBe(toolDir);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("command inherits environment variables", async () => {
|
|
134
|
+
const toolDir = join(FIXTURES_DIR, "env-check");
|
|
135
|
+
mkdirSync(toolDir, { recursive: true });
|
|
136
|
+
|
|
137
|
+
const proc = Bun.spawn(["sh", "-c", "echo $HOME"], {
|
|
138
|
+
cwd: toolDir,
|
|
139
|
+
stdout: "pipe",
|
|
140
|
+
stderr: "pipe",
|
|
141
|
+
env: { ...process.env },
|
|
142
|
+
});
|
|
143
|
+
await proc.exited;
|
|
144
|
+
|
|
145
|
+
const output = await new Response(proc.stdout).text();
|
|
146
|
+
expect(output.trim()).toBeTruthy();
|
|
147
|
+
expect(output.trim()).toBe(process.env.HOME!);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("manifest with hooks serialization", () => {
|
|
152
|
+
test("hooks survive JSON round-trip (single command)", () => {
|
|
153
|
+
const manifest: ToolManifest = {
|
|
154
|
+
name: "@test/roundtrip",
|
|
155
|
+
description: "test",
|
|
156
|
+
hooks: { postinstall: "make build" },
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const json = JSON.stringify(manifest);
|
|
160
|
+
const parsed = JSON.parse(json) as ToolManifest;
|
|
161
|
+
|
|
162
|
+
expect(parsed.hooks?.postinstall).toBe("make build");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("hooks survive JSON round-trip (command array)", () => {
|
|
166
|
+
const manifest: ToolManifest = {
|
|
167
|
+
name: "@test/roundtrip-array",
|
|
168
|
+
description: "test",
|
|
169
|
+
hooks: { postinstall: ["npm ci", "npm run build"] },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const json = JSON.stringify(manifest);
|
|
173
|
+
const parsed = JSON.parse(json) as ToolManifest;
|
|
174
|
+
|
|
175
|
+
expect(parsed.hooks?.postinstall).toEqual(["npm ci", "npm run build"]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -34,7 +34,7 @@ describe("manifest loader", () => {
|
|
|
34
34
|
const filePath = join(FIXTURES_DIR, "valid-tool.yaml");
|
|
35
35
|
const result = loadManifest(filePath);
|
|
36
36
|
|
|
37
|
-
expect(result.manifest.name).toBe("acme/
|
|
37
|
+
expect(result.manifest.name).toBe("acme/greeter");
|
|
38
38
|
expect(result.manifest.description).toBe("Greets users by name");
|
|
39
39
|
expect(result.manifest.version).toBe("1.0.0");
|
|
40
40
|
expect(result.format).toBe("yaml");
|
|
@@ -46,7 +46,7 @@ describe("manifest loader", () => {
|
|
|
46
46
|
const filePath = join(FIXTURES_DIR, "valid-tool.md");
|
|
47
47
|
const result = loadManifest(filePath);
|
|
48
48
|
|
|
49
|
-
expect(result.manifest.name).toBe("acme/
|
|
49
|
+
expect(result.manifest.name).toBe("acme/analyzer");
|
|
50
50
|
expect(result.manifest.description).toBe("Analyzes documentation for completeness");
|
|
51
51
|
expect(result.format).toBe("md");
|
|
52
52
|
expect(result.body).toBeDefined();
|
|
@@ -104,11 +104,11 @@ description: A minimal tool
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
describe("loadManifestFromDir", () => {
|
|
107
|
-
test("loads manifest from directory with
|
|
107
|
+
test("loads manifest from directory with skill.yaml", () => {
|
|
108
108
|
const testDir = join(TEST_DIR, "yaml-dir");
|
|
109
109
|
mkdirSync(testDir, { recursive: true });
|
|
110
110
|
writeFileSync(
|
|
111
|
-
join(testDir, "
|
|
111
|
+
join(testDir, "skill.yaml"),
|
|
112
112
|
`
|
|
113
113
|
name: test/yaml
|
|
114
114
|
description: Test YAML manifest
|
|
@@ -120,6 +120,22 @@ description: Test YAML manifest
|
|
|
120
120
|
expect(result.manifest.name).toBe("test/yaml");
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
test("loads manifest from directory with legacy enact.yaml", () => {
|
|
124
|
+
const testDir = join(TEST_DIR, "legacy-yaml-dir");
|
|
125
|
+
mkdirSync(testDir, { recursive: true });
|
|
126
|
+
writeFileSync(
|
|
127
|
+
join(testDir, "enact.yaml"),
|
|
128
|
+
`
|
|
129
|
+
name: test/legacy-yaml
|
|
130
|
+
description: Test legacy YAML manifest
|
|
131
|
+
`,
|
|
132
|
+
"utf-8"
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = loadManifestFromDir(testDir);
|
|
136
|
+
expect(result.manifest.name).toBe("test/legacy-yaml");
|
|
137
|
+
});
|
|
138
|
+
|
|
123
139
|
test("loads manifest from directory with enact.md", () => {
|
|
124
140
|
const testDir = join(TEST_DIR, "md-dir");
|
|
125
141
|
mkdirSync(testDir, { recursive: true });
|
|
@@ -140,31 +156,29 @@ description: Test Markdown manifest
|
|
|
140
156
|
expect(result.format).toBe("md");
|
|
141
157
|
});
|
|
142
158
|
|
|
143
|
-
test("prefers
|
|
159
|
+
test("prefers skill.yaml over legacy enact.yaml", () => {
|
|
144
160
|
const testDir = join(TEST_DIR, "both-dir");
|
|
145
161
|
mkdirSync(testDir, { recursive: true });
|
|
146
162
|
|
|
147
|
-
// Create both files
|
|
148
163
|
writeFileSync(
|
|
149
|
-
join(testDir, "
|
|
150
|
-
|
|
151
|
-
name: test/
|
|
152
|
-
description:
|
|
153
|
-
---
|
|
164
|
+
join(testDir, "skill.yaml"),
|
|
165
|
+
`
|
|
166
|
+
name: test/skill-preferred
|
|
167
|
+
description: Skill version
|
|
154
168
|
`,
|
|
155
169
|
"utf-8"
|
|
156
170
|
);
|
|
157
171
|
writeFileSync(
|
|
158
172
|
join(testDir, "enact.yaml"),
|
|
159
173
|
`
|
|
160
|
-
name: test/
|
|
161
|
-
description:
|
|
174
|
+
name: test/enact-version
|
|
175
|
+
description: Legacy version
|
|
162
176
|
`,
|
|
163
177
|
"utf-8"
|
|
164
178
|
);
|
|
165
179
|
|
|
166
180
|
const result = loadManifestFromDir(testDir);
|
|
167
|
-
expect(result.manifest.name).toBe("test/
|
|
181
|
+
expect(result.manifest.name).toBe("test/skill-preferred");
|
|
168
182
|
});
|
|
169
183
|
|
|
170
184
|
test("throws for directory without manifest", () => {
|
|
@@ -177,13 +191,13 @@ description: YAML version
|
|
|
177
191
|
});
|
|
178
192
|
|
|
179
193
|
describe("findManifestFile", () => {
|
|
180
|
-
test("finds
|
|
194
|
+
test("finds skill.yaml", () => {
|
|
181
195
|
const testDir = join(TEST_DIR, "find-yaml");
|
|
182
196
|
mkdirSync(testDir, { recursive: true });
|
|
183
|
-
writeFileSync(join(testDir, "
|
|
197
|
+
writeFileSync(join(testDir, "skill.yaml"), "name: test/find", "utf-8");
|
|
184
198
|
|
|
185
199
|
const result = findManifestFile(testDir);
|
|
186
|
-
expect(result).toBe(join(testDir, "
|
|
200
|
+
expect(result).toBe(join(testDir, "skill.yaml"));
|
|
187
201
|
});
|
|
188
202
|
|
|
189
203
|
test("finds enact.md", () => {
|
|
@@ -208,7 +222,7 @@ description: YAML version
|
|
|
208
222
|
test("returns true when manifest exists", () => {
|
|
209
223
|
const testDir = join(TEST_DIR, "has-manifest");
|
|
210
224
|
mkdirSync(testDir, { recursive: true });
|
|
211
|
-
writeFileSync(join(testDir, "
|
|
225
|
+
writeFileSync(join(testDir, "skill.yaml"), "name: test/has", "utf-8");
|
|
212
226
|
|
|
213
227
|
expect(hasManifest(testDir)).toBe(true);
|
|
214
228
|
});
|
|
@@ -227,7 +241,7 @@ description: YAML version
|
|
|
227
241
|
const result = tryLoadManifest(filePath);
|
|
228
242
|
|
|
229
243
|
expect(result).not.toBeNull();
|
|
230
|
-
expect(result?.manifest.name).toBe("acme/
|
|
244
|
+
expect(result?.manifest.name).toBe("acme/greeter");
|
|
231
245
|
});
|
|
232
246
|
|
|
233
247
|
test("returns null on failure", () => {
|
|
@@ -250,7 +264,7 @@ description: YAML version
|
|
|
250
264
|
const testDir = join(TEST_DIR, "try-success");
|
|
251
265
|
mkdirSync(testDir, { recursive: true });
|
|
252
266
|
writeFileSync(
|
|
253
|
-
join(testDir, "
|
|
267
|
+
join(testDir, "skill.yaml"),
|
|
254
268
|
`
|
|
255
269
|
name: test/try
|
|
256
270
|
description: Test try loading
|
|
@@ -25,15 +25,15 @@ version: "1.0.0"
|
|
|
25
25
|
test("parses nested YAML", () => {
|
|
26
26
|
const yaml = `
|
|
27
27
|
name: org/tool
|
|
28
|
-
|
|
28
|
+
outputSchema:
|
|
29
29
|
type: object
|
|
30
30
|
properties:
|
|
31
31
|
name:
|
|
32
32
|
type: string
|
|
33
33
|
`;
|
|
34
34
|
const result = parseYaml(yaml);
|
|
35
|
-
expect(result.
|
|
36
|
-
const schema = result.
|
|
35
|
+
expect(result.outputSchema).toBeDefined();
|
|
36
|
+
const schema = result.outputSchema as Record<string, unknown>;
|
|
37
37
|
expect(schema.type).toBe("object");
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -169,27 +169,21 @@ description: A test tool
|
|
|
169
169
|
test("parses complete YAML manifest", () => {
|
|
170
170
|
const yaml = `
|
|
171
171
|
enact: "2.0.0"
|
|
172
|
-
name: acme/
|
|
172
|
+
name: acme/greeter
|
|
173
173
|
description: Greets users
|
|
174
174
|
version: "1.0.0"
|
|
175
175
|
from: node:18-alpine
|
|
176
|
-
command: "echo 'Hello \${name}'"
|
|
177
176
|
timeout: 30s
|
|
178
177
|
license: MIT
|
|
179
178
|
tags:
|
|
180
179
|
- greeting
|
|
181
180
|
- utility
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
properties:
|
|
185
|
-
name:
|
|
186
|
-
type: string
|
|
187
|
-
required:
|
|
188
|
-
- name
|
|
181
|
+
scripts:
|
|
182
|
+
greet: "echo 'Hello {{name}}'"
|
|
189
183
|
`;
|
|
190
184
|
const result = parseManifest(yaml, "yaml");
|
|
191
185
|
expect(result.manifest.enact).toBe("2.0.0");
|
|
192
|
-
expect(result.manifest.name).toBe("acme/
|
|
186
|
+
expect(result.manifest.name).toBe("acme/greeter");
|
|
193
187
|
expect(result.manifest.from).toBe("node:18-alpine");
|
|
194
188
|
expect(result.manifest.tags).toContain("greeting");
|
|
195
189
|
});
|
|
@@ -266,12 +260,14 @@ Body here.
|
|
|
266
260
|
|
|
267
261
|
describe("detectFormat", () => {
|
|
268
262
|
test("detects .yaml extension", () => {
|
|
263
|
+
expect(detectFormat("skill.yaml")).toBe("yaml");
|
|
269
264
|
expect(detectFormat("enact.yaml")).toBe("yaml");
|
|
270
265
|
expect(detectFormat("tool.yaml")).toBe("yaml");
|
|
271
|
-
expect(detectFormat("/path/to/
|
|
266
|
+
expect(detectFormat("/path/to/skill.yaml")).toBe("yaml");
|
|
272
267
|
});
|
|
273
268
|
|
|
274
269
|
test("detects .yml extension", () => {
|
|
270
|
+
expect(detectFormat("skill.yml")).toBe("yaml");
|
|
275
271
|
expect(detectFormat("enact.yml")).toBe("yaml");
|
|
276
272
|
expect(detectFormat("/path/to/tool.yml")).toBe("yaml");
|
|
277
273
|
});
|
|
@@ -302,7 +298,7 @@ Body here.
|
|
|
302
298
|
name: org/tool
|
|
303
299
|
description: Test
|
|
304
300
|
`;
|
|
305
|
-
const result = parseManifestAuto(yaml, "
|
|
301
|
+
const result = parseManifestAuto(yaml, "skill.yaml");
|
|
306
302
|
expect(result.manifest.name).toBe("org/tool");
|
|
307
303
|
expect(result.format).toBe("yaml");
|
|
308
304
|
});
|
|
@@ -24,7 +24,7 @@ describe("manifest validator", () => {
|
|
|
24
24
|
test("validates complete manifest", () => {
|
|
25
25
|
const manifest = {
|
|
26
26
|
enact: "2.0.0",
|
|
27
|
-
name: "acme/
|
|
27
|
+
name: "acme/greeter",
|
|
28
28
|
description: "Greets users by name",
|
|
29
29
|
version: "1.0.0",
|
|
30
30
|
from: "node:18-alpine",
|
|
@@ -32,12 +32,8 @@ describe("manifest validator", () => {
|
|
|
32
32
|
timeout: "30s",
|
|
33
33
|
license: "MIT",
|
|
34
34
|
tags: ["greeting", "utility"],
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
properties: {
|
|
38
|
-
name: { type: "string" },
|
|
39
|
-
},
|
|
40
|
-
required: ["name"],
|
|
35
|
+
scripts: {
|
|
36
|
+
greet: "echo 'Hello {{name}}'",
|
|
41
37
|
},
|
|
42
38
|
outputSchema: {
|
|
43
39
|
type: "object",
|
|
@@ -222,13 +218,7 @@ describe("manifest validator", () => {
|
|
|
222
218
|
});
|
|
223
219
|
|
|
224
220
|
test("accepts valid tool name formats", () => {
|
|
225
|
-
const names = [
|
|
226
|
-
"org/tool",
|
|
227
|
-
"acme/utils/greeter",
|
|
228
|
-
"my-org/category/sub-category/tool-name",
|
|
229
|
-
"a/b",
|
|
230
|
-
"org_name/tool_name",
|
|
231
|
-
];
|
|
221
|
+
const names = ["org/tool", "acme/greeter", "@my-org/tool-name", "a/b", "org_name/tool_name"];
|
|
232
222
|
|
|
233
223
|
for (const name of names) {
|
|
234
224
|
const manifest = { name, description: "Test" };
|
|
@@ -405,7 +395,7 @@ describe("manifest validator", () => {
|
|
|
405
395
|
describe("isValidToolName", () => {
|
|
406
396
|
test("returns true for valid names", () => {
|
|
407
397
|
expect(isValidToolName("org/tool")).toBe(true);
|
|
408
|
-
expect(isValidToolName("acme/
|
|
398
|
+
expect(isValidToolName("@acme/greeter")).toBe(true);
|
|
409
399
|
expect(isValidToolName("my-org/my-tool")).toBe(true);
|
|
410
400
|
expect(isValidToolName("org_name/tool_name")).toBe(true);
|
|
411
401
|
});
|
|
@@ -425,9 +415,9 @@ describe("manifest validator", () => {
|
|
|
425
415
|
expect(isValidLocalToolName("simple")).toBe(true);
|
|
426
416
|
});
|
|
427
417
|
|
|
428
|
-
test("returns true for
|
|
418
|
+
test("returns true for scoped names", () => {
|
|
429
419
|
expect(isValidLocalToolName("org/tool")).toBe(true);
|
|
430
|
-
expect(isValidLocalToolName("acme/
|
|
420
|
+
expect(isValidLocalToolName("@acme/greeter")).toBe(true);
|
|
431
421
|
});
|
|
432
422
|
|
|
433
423
|
test("returns false for invalid names", () => {
|
|
@@ -29,7 +29,7 @@ describe("manifest types", () => {
|
|
|
29
29
|
|
|
30
30
|
test("accepts full manifest with all fields", () => {
|
|
31
31
|
const manifest: ToolManifest = {
|
|
32
|
-
name: "acme/
|
|
32
|
+
name: "acme/greeter",
|
|
33
33
|
description: "Greets users by name",
|
|
34
34
|
enact: "2.0.0",
|
|
35
35
|
version: "1.0.0",
|
|
@@ -38,12 +38,8 @@ describe("manifest types", () => {
|
|
|
38
38
|
timeout: "30s",
|
|
39
39
|
license: "MIT",
|
|
40
40
|
tags: ["greeting", "utility"],
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
properties: {
|
|
44
|
-
name: { type: "string" },
|
|
45
|
-
},
|
|
46
|
-
required: ["name"],
|
|
41
|
+
scripts: {
|
|
42
|
+
greet: "echo 'Hello {{name}}'",
|
|
47
43
|
},
|
|
48
44
|
outputSchema: {
|
|
49
45
|
type: "object",
|
|
@@ -90,7 +86,7 @@ describe("manifest types", () => {
|
|
|
90
86
|
"x-custom-field": "custom value",
|
|
91
87
|
};
|
|
92
88
|
|
|
93
|
-
expect(manifest.name).toBe("acme/
|
|
89
|
+
expect(manifest.name).toBe("acme/greeter");
|
|
94
90
|
expect(manifest.enact).toBe("2.0.0");
|
|
95
91
|
expect(manifest.env?.API_KEY?.secret).toBe(true);
|
|
96
92
|
expect(manifest.annotations?.readOnlyHint).toBe(true);
|
|
@@ -314,7 +310,7 @@ describe("manifest types", () => {
|
|
|
314
310
|
},
|
|
315
311
|
sourceDir: "/home/user/.enact/tools/org/tool",
|
|
316
312
|
location: "user",
|
|
317
|
-
manifestPath: "/home/user/.enact/tools/org/tool/
|
|
313
|
+
manifestPath: "/home/user/.enact/tools/org/tool/skill.yaml",
|
|
318
314
|
version: "1.0.0",
|
|
319
315
|
};
|
|
320
316
|
|
|
@@ -336,7 +332,7 @@ describe("manifest types", () => {
|
|
|
336
332
|
manifest: { name: "test", description: "test" },
|
|
337
333
|
sourceDir: "/test",
|
|
338
334
|
location: loc,
|
|
339
|
-
manifestPath: "/test/
|
|
335
|
+
manifestPath: "/test/skill.yaml",
|
|
340
336
|
};
|
|
341
337
|
expect(resolution.location).toBe(loc);
|
|
342
338
|
}
|
|
@@ -346,10 +342,12 @@ describe("manifest types", () => {
|
|
|
346
342
|
describe("constants", () => {
|
|
347
343
|
test("MANIFEST_FILES contains expected files", () => {
|
|
348
344
|
expect(MANIFEST_FILES).toContain("SKILL.md");
|
|
345
|
+
expect(MANIFEST_FILES).toContain("skill.yaml");
|
|
346
|
+
expect(MANIFEST_FILES).toContain("skill.yml");
|
|
349
347
|
expect(MANIFEST_FILES).toContain("enact.md");
|
|
350
348
|
expect(MANIFEST_FILES).toContain("enact.yaml");
|
|
351
349
|
expect(MANIFEST_FILES).toContain("enact.yml");
|
|
352
|
-
expect(MANIFEST_FILES.length).toBe(
|
|
350
|
+
expect(MANIFEST_FILES.length).toBe(6);
|
|
353
351
|
});
|
|
354
352
|
|
|
355
353
|
test("PACKAGE_MANIFEST_FILE is correct", () => {
|