@enactprotocol/cli 2.1.6 → 2.1.10
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/auth/index.js +1 -1
- package/dist/commands/auth/index.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/info/index.d.ts +11 -0
- package/dist/commands/info/index.d.ts.map +1 -0
- package/dist/commands/info/index.js +232 -0
- package/dist/commands/info/index.js.map +1 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +92 -61
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts +4 -0
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +159 -5
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/mcp/index.d.ts +20 -0
- package/dist/commands/mcp/index.d.ts.map +1 -0
- package/dist/commands/mcp/index.js +460 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/publish/index.d.ts.map +1 -1
- package/dist/commands/publish/index.js +14 -7
- package/dist/commands/publish/index.js.map +1 -1
- package/dist/commands/sign/index.d.ts +2 -1
- package/dist/commands/sign/index.d.ts.map +1 -1
- package/dist/commands/sign/index.js +75 -17
- package/dist/commands/sign/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/auth/index.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/{get → info}/index.ts +103 -16
- package/src/commands/init/index.ts +104 -65
- package/src/commands/learn/index.ts +228 -5
- package/src/commands/publish/index.ts +14 -7
- package/src/commands/sign/index.ts +93 -18
- package/src/index.ts +3 -3
- package/tests/commands/{get.test.ts → info.test.ts} +35 -33
- package/tests/commands/init.test.ts +204 -17
- package/tests/commands/learn.test.ts +2 -2
- package/tests/e2e.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Supports both local paths and remote tool references:
|
|
9
9
|
* - Local: enact sign ./my-tool
|
|
10
|
-
* - Remote: enact sign author/tool
|
|
10
|
+
* - Remote: enact sign author/tool (prompts for version)
|
|
11
|
+
* - Remote: enact sign author/tool@1.0.0 (specific version)
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { dirname, join, resolve } from "node:path";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createApiClient,
|
|
18
|
+
getToolInfo,
|
|
19
|
+
getToolVersion,
|
|
20
|
+
submitAttestationToRegistry,
|
|
21
|
+
} from "@enactprotocol/api";
|
|
16
22
|
import { getSecret } from "@enactprotocol/secrets";
|
|
17
23
|
import {
|
|
18
24
|
addTrustedAuditor,
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
json,
|
|
43
49
|
keyValue,
|
|
44
50
|
newline,
|
|
51
|
+
select,
|
|
45
52
|
success,
|
|
46
53
|
symbols,
|
|
47
54
|
warning,
|
|
@@ -63,26 +70,37 @@ interface SignOptions extends GlobalOptions {
|
|
|
63
70
|
const DEFAULT_BUNDLE_FILENAME = ".sigstore-bundle.json";
|
|
64
71
|
|
|
65
72
|
/**
|
|
66
|
-
* Parse a remote tool reference like "author/tool@1.0.0"
|
|
67
|
-
*
|
|
73
|
+
* Parse a remote tool reference like "author/tool@1.0.0" or "author/tool"
|
|
74
|
+
* Version is optional - if not provided, will prompt user to select
|
|
75
|
+
* Returns null if not a valid remote reference (i.e., looks like a local path)
|
|
68
76
|
*/
|
|
69
|
-
function parseRemoteToolRef(ref: string): { name: string; version: string } | null {
|
|
77
|
+
function parseRemoteToolRef(ref: string): { name: string; version: string | undefined } | null {
|
|
70
78
|
// Remote refs look like: author/tool@version or org/author/tool@version
|
|
71
|
-
// They don't start with . or / and contain
|
|
79
|
+
// They don't start with . or / and must contain at least one /
|
|
72
80
|
if (ref.startsWith(".") || ref.startsWith("/") || ref.startsWith("~")) {
|
|
73
81
|
return null;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
// Must have at least one / in the name (author/tool format)
|
|
85
|
+
if (!ref.includes("/")) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
const atIndex = ref.lastIndexOf("@");
|
|
77
|
-
if (atIndex === -1
|
|
90
|
+
if (atIndex === -1) {
|
|
91
|
+
// No version specified - that's OK, we'll prompt for it
|
|
92
|
+
return { name: ref, version: undefined };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (atIndex === 0) {
|
|
78
96
|
return null;
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
const name = ref.substring(0, atIndex);
|
|
82
100
|
const version = ref.substring(atIndex + 1);
|
|
83
101
|
|
|
84
|
-
//
|
|
85
|
-
if (!
|
|
102
|
+
// Version after @ must not be empty
|
|
103
|
+
if (!version) {
|
|
86
104
|
return null;
|
|
87
105
|
}
|
|
88
106
|
|
|
@@ -276,9 +294,9 @@ function displayResult(
|
|
|
276
294
|
* Sign a remote tool from the registry
|
|
277
295
|
*/
|
|
278
296
|
async function signRemoteTool(
|
|
279
|
-
toolRef: { name: string; version: string },
|
|
297
|
+
toolRef: { name: string; version: string | undefined },
|
|
280
298
|
options: SignOptions,
|
|
281
|
-
|
|
299
|
+
ctx: CommandContext
|
|
282
300
|
): Promise<void> {
|
|
283
301
|
const config = loadConfig();
|
|
284
302
|
const registryUrl =
|
|
@@ -308,14 +326,71 @@ async function signRemoteTool(
|
|
|
308
326
|
newline();
|
|
309
327
|
}
|
|
310
328
|
|
|
329
|
+
// Resolve version - prompt if not provided
|
|
330
|
+
let targetVersion = toolRef.version;
|
|
331
|
+
|
|
332
|
+
if (!targetVersion) {
|
|
333
|
+
// Fetch tool info to get available versions
|
|
334
|
+
info(`Fetching versions for ${toolRef.name}...`);
|
|
335
|
+
|
|
336
|
+
let toolMetadata: Awaited<ReturnType<typeof getToolInfo>>;
|
|
337
|
+
try {
|
|
338
|
+
toolMetadata = await getToolInfo(client, toolRef.name);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
error(`Tool not found: ${toolRef.name}`);
|
|
341
|
+
if (err instanceof Error) {
|
|
342
|
+
dim(` ${err.message}`);
|
|
343
|
+
}
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (toolMetadata.versions.length === 0) {
|
|
348
|
+
error(`No published versions found for ${toolRef.name}`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Filter out yanked versions for selection (unless there are no non-yanked versions)
|
|
353
|
+
const availableVersions = toolMetadata.versions.filter((v) => !v.yanked);
|
|
354
|
+
const versionsToShow = availableVersions.length > 0 ? availableVersions : toolMetadata.versions;
|
|
355
|
+
|
|
356
|
+
if (ctx.isInteractive) {
|
|
357
|
+
// Prompt user to select a version
|
|
358
|
+
newline();
|
|
359
|
+
const selectedVersion = await select(
|
|
360
|
+
"Select a version to sign:",
|
|
361
|
+
versionsToShow.map((v) => {
|
|
362
|
+
const option: { value: string; label: string; hint?: string } = {
|
|
363
|
+
value: v.version,
|
|
364
|
+
label: v.version + (v.version === toolMetadata.latestVersion ? " (latest)" : ""),
|
|
365
|
+
};
|
|
366
|
+
if (v.yanked) {
|
|
367
|
+
option.hint = "yanked";
|
|
368
|
+
}
|
|
369
|
+
return option;
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (!selectedVersion) {
|
|
374
|
+
info("Signing cancelled");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
targetVersion = selectedVersion;
|
|
379
|
+
} else {
|
|
380
|
+
// Non-interactive: use latest version
|
|
381
|
+
targetVersion = toolMetadata.latestVersion;
|
|
382
|
+
info(`Using latest version: ${targetVersion}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
311
386
|
// Fetch tool info from registry
|
|
312
|
-
info(`Fetching ${toolRef.name}@${
|
|
387
|
+
info(`Fetching ${toolRef.name}@${targetVersion} from registry...`);
|
|
313
388
|
|
|
314
389
|
let toolInfo: Awaited<ReturnType<typeof getToolVersion>>;
|
|
315
390
|
try {
|
|
316
|
-
toolInfo = await getToolVersion(client, toolRef.name,
|
|
391
|
+
toolInfo = await getToolVersion(client, toolRef.name, targetVersion);
|
|
317
392
|
} catch (err) {
|
|
318
|
-
error(`Tool not found: ${toolRef.name}@${
|
|
393
|
+
error(`Tool not found: ${toolRef.name}@${targetVersion}`);
|
|
319
394
|
if (err instanceof Error) {
|
|
320
395
|
dim(` ${err.message}`);
|
|
321
396
|
}
|
|
@@ -357,7 +432,7 @@ async function signRemoteTool(
|
|
|
357
432
|
}
|
|
358
433
|
|
|
359
434
|
// Confirm signing
|
|
360
|
-
if (
|
|
435
|
+
if (ctx.isInteractive) {
|
|
361
436
|
newline();
|
|
362
437
|
const shouldSign = await confirm(
|
|
363
438
|
`Sign ${toolInfo.name}@${toolInfo.version} with your identity?`,
|
|
@@ -466,10 +541,10 @@ async function signRemoteTool(
|
|
|
466
541
|
}
|
|
467
542
|
|
|
468
543
|
// Prompt to add to trust list - extract issuer from bundle for correct identity format
|
|
469
|
-
if (
|
|
544
|
+
if (ctx.isInteractive && !options.json) {
|
|
470
545
|
const certificate = extractCertificateFromBundle(result.bundle);
|
|
471
546
|
const issuer = certificate?.identity?.issuer;
|
|
472
|
-
await promptAddToTrustList(attestationResult.auditor,
|
|
547
|
+
await promptAddToTrustList(attestationResult.auditor, ctx.isInteractive, issuer);
|
|
473
548
|
}
|
|
474
549
|
|
|
475
550
|
if (options.json) {
|
|
@@ -719,7 +794,7 @@ export function configureSignCommand(program: Command): void {
|
|
|
719
794
|
.description("Cryptographically sign a tool and submit attestation to registry")
|
|
720
795
|
.argument(
|
|
721
796
|
"<path>",
|
|
722
|
-
"Path to tool directory, manifest file, or remote tool (author/tool@version)"
|
|
797
|
+
"Path to tool directory, manifest file, or remote tool (author/tool or author/tool@version)"
|
|
723
798
|
)
|
|
724
799
|
.option("-i, --identity <email>", "Sign with specific identity (uses OAuth)")
|
|
725
800
|
.option("-o, --output <path>", "Output path for signature bundle (local only)")
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
configureConfigCommand,
|
|
16
16
|
configureEnvCommand,
|
|
17
17
|
configureExecCommand,
|
|
18
|
-
|
|
18
|
+
configureInfoCommand,
|
|
19
19
|
configureInitCommand,
|
|
20
20
|
configureInspectCommand,
|
|
21
21
|
configureInstallCommand,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "./commands";
|
|
34
34
|
import { error, formatError } from "./utils";
|
|
35
35
|
|
|
36
|
-
export const version = "2.1.
|
|
36
|
+
export const version = "2.1.10";
|
|
37
37
|
|
|
38
38
|
// Export types for external use
|
|
39
39
|
export type { GlobalOptions, CommandContext } from "./types";
|
|
@@ -63,7 +63,7 @@ async function main() {
|
|
|
63
63
|
|
|
64
64
|
// Registry commands (Phase 8)
|
|
65
65
|
configureSearchCommand(program);
|
|
66
|
-
|
|
66
|
+
configureInfoCommand(program);
|
|
67
67
|
configureLearnCommand(program);
|
|
68
68
|
configurePublishCommand(program);
|
|
69
69
|
configureAuthCommand(program);
|
|
@@ -1,64 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the
|
|
2
|
+
* Tests for the info command
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import type { ToolVersionInfo } from "@enactprotocol/api";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import {
|
|
8
|
+
import { configureInfoCommand } from "../../src/commands/info";
|
|
9
9
|
|
|
10
|
-
describe("
|
|
10
|
+
describe("info command", () => {
|
|
11
11
|
describe("command configuration", () => {
|
|
12
|
-
test("configures
|
|
12
|
+
test("configures info command on program", () => {
|
|
13
13
|
const program = new Command();
|
|
14
|
-
|
|
14
|
+
configureInfoCommand(program);
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
expect(
|
|
16
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
17
|
+
expect(infoCmd).toBeDefined();
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
test("has correct description", () => {
|
|
21
21
|
const program = new Command();
|
|
22
|
-
|
|
22
|
+
configureInfoCommand(program);
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
expect(
|
|
24
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
25
|
+
expect(infoCmd?.description()).toBe(
|
|
26
|
+
"Show detailed information about a tool (local path or registry)"
|
|
27
|
+
);
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
test("has
|
|
30
|
+
test("has get as alias", () => {
|
|
29
31
|
const program = new Command();
|
|
30
|
-
|
|
32
|
+
configureInfoCommand(program);
|
|
31
33
|
|
|
32
|
-
const
|
|
33
|
-
expect(
|
|
34
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
35
|
+
expect(infoCmd?.aliases()).toContain("get");
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
test("accepts tool argument", () => {
|
|
37
39
|
const program = new Command();
|
|
38
|
-
|
|
40
|
+
configureInfoCommand(program);
|
|
39
41
|
|
|
40
|
-
const
|
|
41
|
-
const args =
|
|
42
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
43
|
+
const args = infoCmd?.registeredArguments ?? [];
|
|
42
44
|
expect(args.length).toBeGreaterThan(0);
|
|
43
45
|
expect(args[0]?.name()).toBe("tool");
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
test("has --ver option for specifying version", () => {
|
|
47
49
|
const program = new Command();
|
|
48
|
-
|
|
50
|
+
configureInfoCommand(program);
|
|
49
51
|
|
|
50
|
-
const
|
|
51
|
-
const opts =
|
|
52
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
53
|
+
const opts = infoCmd?.options ?? [];
|
|
52
54
|
const verOpt = opts.find((o) => o.long === "--ver");
|
|
53
55
|
expect(verOpt).toBeDefined();
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
test("has -v short option for verbose (not version)", () => {
|
|
57
59
|
const program = new Command();
|
|
58
|
-
|
|
60
|
+
configureInfoCommand(program);
|
|
59
61
|
|
|
60
|
-
const
|
|
61
|
-
const opts =
|
|
62
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
63
|
+
const opts = infoCmd?.options ?? [];
|
|
62
64
|
// -v is for verbose, not version (--ver is for version)
|
|
63
65
|
const verboseOpt = opts.find((o) => o.short === "-v");
|
|
64
66
|
expect(verboseOpt).toBeDefined();
|
|
@@ -67,20 +69,20 @@ describe("get command", () => {
|
|
|
67
69
|
|
|
68
70
|
test("has --json option", () => {
|
|
69
71
|
const program = new Command();
|
|
70
|
-
|
|
72
|
+
configureInfoCommand(program);
|
|
71
73
|
|
|
72
|
-
const
|
|
73
|
-
const opts =
|
|
74
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
75
|
+
const opts = infoCmd?.options ?? [];
|
|
74
76
|
const jsonOpt = opts.find((o) => o.long === "--json");
|
|
75
77
|
expect(jsonOpt).toBeDefined();
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test("has --verbose option", () => {
|
|
79
81
|
const program = new Command();
|
|
80
|
-
|
|
82
|
+
configureInfoCommand(program);
|
|
81
83
|
|
|
82
|
-
const
|
|
83
|
-
const opts =
|
|
84
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
85
|
+
const opts = infoCmd?.options ?? [];
|
|
84
86
|
const verboseOpt = opts.find((o) => o.long === "--verbose");
|
|
85
87
|
expect(verboseOpt).toBeDefined();
|
|
86
88
|
});
|
|
@@ -249,12 +251,12 @@ Documentation here.`;
|
|
|
249
251
|
expect(enactMdContent).toContain("Documentation here.");
|
|
250
252
|
});
|
|
251
253
|
|
|
252
|
-
test("verbose option is available on
|
|
254
|
+
test("verbose option is available on info command", () => {
|
|
253
255
|
const program = new Command();
|
|
254
|
-
|
|
256
|
+
configureInfoCommand(program);
|
|
255
257
|
|
|
256
|
-
const
|
|
257
|
-
const opts =
|
|
258
|
+
const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
|
|
259
|
+
const opts = infoCmd?.options ?? [];
|
|
258
260
|
|
|
259
261
|
// Check both short and long form exist
|
|
260
262
|
const verboseOpt = opts.find((o) => o.long === "--verbose");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { configureInitCommand } from "../../src/commands/init";
|
|
@@ -107,7 +107,59 @@ describe("init command", () => {
|
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test("default mode creates
|
|
110
|
+
test("default mode creates AGENTS.md for tool consumers", async () => {
|
|
111
|
+
const program = new Command();
|
|
112
|
+
program.exitOverride();
|
|
113
|
+
configureInitCommand(program);
|
|
114
|
+
|
|
115
|
+
const originalCwd = process.cwd();
|
|
116
|
+
process.chdir(testDir);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await program.parseAsync(["node", "test", "init"]);
|
|
120
|
+
} catch {
|
|
121
|
+
// Command may throw due to exitOverride
|
|
122
|
+
} finally {
|
|
123
|
+
process.chdir(originalCwd);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const agentsPath = join(testDir, "AGENTS.md");
|
|
127
|
+
expect(existsSync(agentsPath)).toBe(true);
|
|
128
|
+
|
|
129
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
130
|
+
expect(content).toContain("enact search");
|
|
131
|
+
expect(content).toContain("enact install");
|
|
132
|
+
expect(content).toContain("Finding & Installing Tools");
|
|
133
|
+
|
|
134
|
+
// Should NOT create enact.md in default mode
|
|
135
|
+
const manifestPath = join(testDir, "enact.md");
|
|
136
|
+
expect(existsSync(manifestPath)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("default mode creates .enact/tools.json", async () => {
|
|
140
|
+
const program = new Command();
|
|
141
|
+
program.exitOverride();
|
|
142
|
+
configureInitCommand(program);
|
|
143
|
+
|
|
144
|
+
const originalCwd = process.cwd();
|
|
145
|
+
process.chdir(testDir);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await program.parseAsync(["node", "test", "init"]);
|
|
149
|
+
} catch {
|
|
150
|
+
// Command may throw due to exitOverride
|
|
151
|
+
} finally {
|
|
152
|
+
process.chdir(originalCwd);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const toolsJsonPath = join(testDir, ".enact", "tools.json");
|
|
156
|
+
expect(existsSync(toolsJsonPath)).toBe(true);
|
|
157
|
+
|
|
158
|
+
const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
|
|
159
|
+
expect(content).toEqual({ tools: {} });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("--tool mode creates SKILL.md", async () => {
|
|
111
163
|
const program = new Command();
|
|
112
164
|
program.exitOverride(); // Prevent process.exit
|
|
113
165
|
configureInitCommand(program);
|
|
@@ -117,14 +169,14 @@ describe("init command", () => {
|
|
|
117
169
|
process.chdir(testDir);
|
|
118
170
|
|
|
119
171
|
try {
|
|
120
|
-
await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
|
|
172
|
+
await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/my-tool"]);
|
|
121
173
|
} catch {
|
|
122
174
|
// Command may throw due to exitOverride
|
|
123
175
|
} finally {
|
|
124
176
|
process.chdir(originalCwd);
|
|
125
177
|
}
|
|
126
178
|
|
|
127
|
-
const manifestPath = join(testDir, "
|
|
179
|
+
const manifestPath = join(testDir, "SKILL.md");
|
|
128
180
|
expect(existsSync(manifestPath)).toBe(true);
|
|
129
181
|
|
|
130
182
|
const content = readFileSync(manifestPath, "utf-8");
|
|
@@ -133,7 +185,7 @@ describe("init command", () => {
|
|
|
133
185
|
expect(content).toContain("command:");
|
|
134
186
|
});
|
|
135
187
|
|
|
136
|
-
test("
|
|
188
|
+
test("--tool mode creates AGENTS.md for tool development", async () => {
|
|
137
189
|
const program = new Command();
|
|
138
190
|
program.exitOverride();
|
|
139
191
|
configureInitCommand(program);
|
|
@@ -142,7 +194,7 @@ describe("init command", () => {
|
|
|
142
194
|
process.chdir(testDir);
|
|
143
195
|
|
|
144
196
|
try {
|
|
145
|
-
await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
|
|
197
|
+
await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/my-tool"]);
|
|
146
198
|
} catch {
|
|
147
199
|
// Command may throw due to exitOverride
|
|
148
200
|
} finally {
|
|
@@ -154,7 +206,7 @@ describe("init command", () => {
|
|
|
154
206
|
|
|
155
207
|
const content = readFileSync(agentsPath, "utf-8");
|
|
156
208
|
expect(content).toContain("enact run");
|
|
157
|
-
expect(content).toContain("
|
|
209
|
+
expect(content).toContain("SKILL.md");
|
|
158
210
|
expect(content).toContain("Parameter Substitution");
|
|
159
211
|
});
|
|
160
212
|
|
|
@@ -215,7 +267,7 @@ describe("init command", () => {
|
|
|
215
267
|
expect(existsSync(join(testDir, "AGENTS.md"))).toBe(false);
|
|
216
268
|
});
|
|
217
269
|
|
|
218
|
-
test("
|
|
270
|
+
test("--agent mode creates .enact/tools.json", async () => {
|
|
219
271
|
const program = new Command();
|
|
220
272
|
program.exitOverride();
|
|
221
273
|
configureInitCommand(program);
|
|
@@ -224,14 +276,149 @@ describe("init command", () => {
|
|
|
224
276
|
process.chdir(testDir);
|
|
225
277
|
|
|
226
278
|
try {
|
|
227
|
-
await program.parseAsync(["node", "test", "init", "--
|
|
279
|
+
await program.parseAsync(["node", "test", "init", "--agent"]);
|
|
228
280
|
} catch {
|
|
229
281
|
// Command may throw due to exitOverride
|
|
230
282
|
} finally {
|
|
231
283
|
process.chdir(originalCwd);
|
|
232
284
|
}
|
|
233
285
|
|
|
234
|
-
const
|
|
286
|
+
const toolsJsonPath = join(testDir, ".enact", "tools.json");
|
|
287
|
+
expect(existsSync(toolsJsonPath)).toBe(true);
|
|
288
|
+
|
|
289
|
+
const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
|
|
290
|
+
expect(content).toEqual({ tools: {} });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("--claude mode creates .enact/tools.json", async () => {
|
|
294
|
+
const program = new Command();
|
|
295
|
+
program.exitOverride();
|
|
296
|
+
configureInitCommand(program);
|
|
297
|
+
|
|
298
|
+
const originalCwd = process.cwd();
|
|
299
|
+
process.chdir(testDir);
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await program.parseAsync(["node", "test", "init", "--claude"]);
|
|
303
|
+
} catch {
|
|
304
|
+
// Command may throw due to exitOverride
|
|
305
|
+
} finally {
|
|
306
|
+
process.chdir(originalCwd);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const toolsJsonPath = join(testDir, ".enact", "tools.json");
|
|
310
|
+
expect(existsSync(toolsJsonPath)).toBe(true);
|
|
311
|
+
|
|
312
|
+
const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
|
|
313
|
+
expect(content).toEqual({ tools: {} });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("--agent mode with --force overwrites existing .enact/tools.json", async () => {
|
|
317
|
+
// Create existing .enact/tools.json with some content
|
|
318
|
+
const enactDir = join(testDir, ".enact");
|
|
319
|
+
mkdirSync(enactDir, { recursive: true });
|
|
320
|
+
const toolsJsonPath = join(enactDir, "tools.json");
|
|
321
|
+
const existingContent = { tools: { "some/tool": "1.0.0" } };
|
|
322
|
+
writeFileSync(toolsJsonPath, JSON.stringify(existingContent));
|
|
323
|
+
|
|
324
|
+
const program = new Command();
|
|
325
|
+
program.exitOverride();
|
|
326
|
+
configureInitCommand(program);
|
|
327
|
+
|
|
328
|
+
const originalCwd = process.cwd();
|
|
329
|
+
process.chdir(testDir);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
await program.parseAsync(["node", "test", "init", "--agent", "--force"]);
|
|
333
|
+
} catch {
|
|
334
|
+
// Command may throw due to exitOverride
|
|
335
|
+
} finally {
|
|
336
|
+
process.chdir(originalCwd);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
|
|
340
|
+
expect(content).toEqual({ tools: {} });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("--agent mode preserves existing .enact/tools.json without --force", async () => {
|
|
344
|
+
// Create existing .enact/tools.json with some content
|
|
345
|
+
const enactDir = join(testDir, ".enact");
|
|
346
|
+
mkdirSync(enactDir, { recursive: true });
|
|
347
|
+
const toolsJsonPath = join(enactDir, "tools.json");
|
|
348
|
+
const existingContent = { tools: { "some/tool": "1.0.0" } };
|
|
349
|
+
writeFileSync(toolsJsonPath, JSON.stringify(existingContent));
|
|
350
|
+
|
|
351
|
+
// Also create AGENTS.md so the command doesn't fail early
|
|
352
|
+
writeFileSync(join(testDir, "AGENTS.md"), "existing");
|
|
353
|
+
|
|
354
|
+
const program = new Command();
|
|
355
|
+
program.exitOverride();
|
|
356
|
+
configureInitCommand(program);
|
|
357
|
+
|
|
358
|
+
const originalCwd = process.cwd();
|
|
359
|
+
process.chdir(testDir);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Without --force, AGENTS.md check will fail and return early
|
|
363
|
+
// So we need to test with --force on AGENTS.md but not tools.json
|
|
364
|
+
// Actually the --force flag applies to both, so let's just verify
|
|
365
|
+
// tools.json is preserved when it exists and no --force
|
|
366
|
+
await program.parseAsync(["node", "test", "init", "--agent"]);
|
|
367
|
+
} catch {
|
|
368
|
+
// Command may throw due to exitOverride or warning about existing file
|
|
369
|
+
} finally {
|
|
370
|
+
process.chdir(originalCwd);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// tools.json should be preserved since AGENTS.md existed and no --force was used
|
|
374
|
+
const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
|
|
375
|
+
expect(content).toEqual(existingContent);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("--tool mode does NOT create .enact/tools.json", async () => {
|
|
379
|
+
const program = new Command();
|
|
380
|
+
program.exitOverride();
|
|
381
|
+
configureInitCommand(program);
|
|
382
|
+
|
|
383
|
+
const originalCwd = process.cwd();
|
|
384
|
+
process.chdir(testDir);
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/my-tool"]);
|
|
388
|
+
} catch {
|
|
389
|
+
// Command may throw due to exitOverride
|
|
390
|
+
} finally {
|
|
391
|
+
process.chdir(originalCwd);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const toolsJsonPath = join(testDir, ".enact", "tools.json");
|
|
395
|
+
expect(existsSync(toolsJsonPath)).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("SKILL.md contains valid YAML frontmatter", async () => {
|
|
399
|
+
const program = new Command();
|
|
400
|
+
program.exitOverride();
|
|
401
|
+
configureInitCommand(program);
|
|
402
|
+
|
|
403
|
+
const originalCwd = process.cwd();
|
|
404
|
+
process.chdir(testDir);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await program.parseAsync([
|
|
408
|
+
"node",
|
|
409
|
+
"test",
|
|
410
|
+
"init",
|
|
411
|
+
"--tool",
|
|
412
|
+
"--name",
|
|
413
|
+
"myorg/utils/greeter",
|
|
414
|
+
]);
|
|
415
|
+
} catch {
|
|
416
|
+
// Command may throw due to exitOverride
|
|
417
|
+
} finally {
|
|
418
|
+
process.chdir(originalCwd);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
|
|
235
422
|
|
|
236
423
|
// Check frontmatter structure
|
|
237
424
|
expect(content.startsWith("---")).toBe(true);
|
|
@@ -255,7 +442,7 @@ describe("init command", () => {
|
|
|
255
442
|
process.chdir(testDir);
|
|
256
443
|
|
|
257
444
|
try {
|
|
258
|
-
await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
|
|
445
|
+
await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/tool"]);
|
|
259
446
|
} catch {
|
|
260
447
|
// Command may throw due to exitOverride
|
|
261
448
|
} finally {
|
|
@@ -299,16 +486,16 @@ describe("init command", () => {
|
|
|
299
486
|
});
|
|
300
487
|
|
|
301
488
|
describe("option conflicts", () => {
|
|
302
|
-
test("--
|
|
489
|
+
test("--agent is the default when no mode specified", () => {
|
|
303
490
|
const program = new Command();
|
|
304
491
|
configureInitCommand(program);
|
|
305
492
|
|
|
306
493
|
const initCmd = program.commands.find((cmd) => cmd.name() === "init");
|
|
307
494
|
const opts = initCmd?.options ?? [];
|
|
308
|
-
const
|
|
495
|
+
const agentOpt = opts.find((o) => o.long === "--agent");
|
|
309
496
|
|
|
310
497
|
// Description should indicate it's the default
|
|
311
|
-
expect(
|
|
498
|
+
expect(agentOpt?.description).toContain("default");
|
|
312
499
|
});
|
|
313
500
|
});
|
|
314
501
|
|
|
@@ -328,14 +515,14 @@ describe("init command", () => {
|
|
|
328
515
|
process.chdir(testDir);
|
|
329
516
|
|
|
330
517
|
try {
|
|
331
|
-
await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
|
|
518
|
+
await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/tool"]);
|
|
332
519
|
} catch {
|
|
333
520
|
// Command may throw due to exitOverride
|
|
334
521
|
} finally {
|
|
335
522
|
process.chdir(originalCwd);
|
|
336
523
|
}
|
|
337
524
|
|
|
338
|
-
const content = readFileSync(join(testDir, "
|
|
525
|
+
const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
|
|
339
526
|
|
|
340
527
|
// Required fields per spec
|
|
341
528
|
expect(content).toContain("name:");
|
|
@@ -367,7 +554,7 @@ describe("init command", () => {
|
|
|
367
554
|
process.chdir(testDir);
|
|
368
555
|
|
|
369
556
|
try {
|
|
370
|
-
await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
|
|
557
|
+
await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/tool"]);
|
|
371
558
|
} catch {
|
|
372
559
|
// Command may throw due to exitOverride
|
|
373
560
|
} finally {
|