@enactprotocol/cli 2.1.7 → 2.1.14

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.
Files changed (55) hide show
  1. package/dist/commands/auth/index.js +1 -1
  2. package/dist/commands/auth/index.js.map +1 -1
  3. package/dist/commands/index.d.ts +2 -1
  4. package/dist/commands/index.d.ts.map +1 -1
  5. package/dist/commands/index.js +3 -1
  6. package/dist/commands/index.js.map +1 -1
  7. package/dist/commands/info/index.d.ts +11 -0
  8. package/dist/commands/info/index.d.ts.map +1 -0
  9. package/dist/commands/info/index.js +232 -0
  10. package/dist/commands/info/index.js.map +1 -0
  11. package/dist/commands/init/index.d.ts.map +1 -1
  12. package/dist/commands/init/index.js +17 -14
  13. package/dist/commands/init/index.js.map +1 -1
  14. package/dist/commands/learn/index.d.ts +4 -0
  15. package/dist/commands/learn/index.d.ts.map +1 -1
  16. package/dist/commands/learn/index.js +159 -5
  17. package/dist/commands/learn/index.js.map +1 -1
  18. package/dist/commands/mcp/index.d.ts +20 -0
  19. package/dist/commands/mcp/index.d.ts.map +1 -0
  20. package/dist/commands/mcp/index.js +460 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/publish/index.d.ts +2 -0
  23. package/dist/commands/publish/index.d.ts.map +1 -1
  24. package/dist/commands/publish/index.js +32 -8
  25. package/dist/commands/publish/index.js.map +1 -1
  26. package/dist/commands/sign/index.d.ts +2 -1
  27. package/dist/commands/sign/index.d.ts.map +1 -1
  28. package/dist/commands/sign/index.js +75 -17
  29. package/dist/commands/sign/index.js.map +1 -1
  30. package/dist/commands/visibility/index.d.ts +11 -0
  31. package/dist/commands/visibility/index.d.ts.map +1 -0
  32. package/dist/commands/visibility/index.js +117 -0
  33. package/dist/commands/visibility/index.js.map +1 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -3
  37. package/dist/index.js.map +1 -1
  38. package/package.json +5 -5
  39. package/src/commands/auth/index.ts +1 -1
  40. package/src/commands/index.ts +4 -1
  41. package/src/commands/{get → info}/index.ts +103 -16
  42. package/src/commands/init/index.ts +17 -14
  43. package/src/commands/learn/index.ts +228 -5
  44. package/src/commands/publish/index.ts +37 -8
  45. package/src/commands/sign/index.ts +93 -18
  46. package/src/commands/visibility/index.ts +154 -0
  47. package/src/index.ts +7 -3
  48. package/tests/commands/{get.test.ts → info.test.ts} +35 -33
  49. package/tests/commands/init.test.ts +6 -6
  50. package/tests/commands/learn.test.ts +2 -2
  51. package/tests/commands/publish.test.ts +50 -0
  52. package/tests/commands/visibility.test.ts +156 -0
  53. package/tests/e2e.test.ts +1 -1
  54. package/tsconfig.tsbuildinfo +1 -1
  55. /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
@@ -39,10 +39,15 @@ import { loadGitignore, shouldIgnore } from "../../utils/ignore";
39
39
  const AUTH_NAMESPACE = "enact:auth";
40
40
  const ACCESS_TOKEN_KEY = "access_token";
41
41
 
42
+ /** Tool visibility levels */
43
+ export type ToolVisibility = "public" | "private" | "unlisted";
44
+
42
45
  interface PublishOptions extends GlobalOptions {
43
46
  dryRun?: boolean;
44
47
  tag?: string;
45
48
  skipAuth?: boolean;
49
+ public?: boolean;
50
+ unlisted?: boolean;
46
51
  }
47
52
 
48
53
  /**
@@ -130,9 +135,16 @@ async function createBundleFromDir(toolDir: string): Promise<Uint8Array> {
130
135
  }
131
136
 
132
137
  /**
133
- * Load the raw enact.md file content (full documentation with frontmatter)
138
+ * Load the raw markdown manifest file content (full documentation with frontmatter)
139
+ * Checks for SKILL.md first (preferred), then falls back to enact.md
134
140
  */
135
- function loadEnactMd(toolDir: string): string | undefined {
141
+ function loadRawManifest(toolDir: string): string | undefined {
142
+ // Check SKILL.md first (preferred format)
143
+ const skillMdPath = join(toolDir, "SKILL.md");
144
+ if (existsSync(skillMdPath)) {
145
+ return readFileSync(skillMdPath, "utf-8");
146
+ }
147
+ // Fall back to enact.md
136
148
  const enactMdPath = join(toolDir, "enact.md");
137
149
  if (existsSync(enactMdPath)) {
138
150
  return readFileSync(enactMdPath, "utf-8");
@@ -196,10 +208,18 @@ async function publishHandler(
196
208
  header(`Publishing ${toolName}@${version}`);
197
209
  newline();
198
210
 
211
+ // Determine visibility (private by default for security)
212
+ const visibility: ToolVisibility = options.public
213
+ ? "public"
214
+ : options.unlisted
215
+ ? "unlisted"
216
+ : "private";
217
+
199
218
  // Show what we're publishing
200
219
  keyValue("Name", toolName);
201
220
  keyValue("Version", version);
202
221
  keyValue("Description", manifest.description);
222
+ keyValue("Visibility", visibility);
203
223
  if (manifest.tags && manifest.tags.length > 0) {
204
224
  keyValue("Tags", manifest.tags.join(", "));
205
225
  }
@@ -283,6 +303,7 @@ async function publishHandler(
283
303
  info("Would publish to registry:");
284
304
  keyValue("Tool", toolName);
285
305
  keyValue("Version", version);
306
+ keyValue("Visibility", visibility);
286
307
  keyValue("Source", toolDir);
287
308
 
288
309
  // Show files that would be bundled
@@ -299,10 +320,10 @@ async function publishHandler(
299
320
  return;
300
321
  }
301
322
 
302
- // Load the full enact.md content (frontmatter + documentation)
303
- const enactMdContent = loadEnactMd(toolDir);
304
- if (enactMdContent) {
305
- info("Found enact.md documentation");
323
+ // Load the full markdown manifest content (SKILL.md or enact.md)
324
+ const rawManifestContent = loadRawManifest(toolDir);
325
+ if (rawManifestContent) {
326
+ info("Found markdown documentation (SKILL.md or enact.md)");
306
327
  }
307
328
 
308
329
  // Create bundle
@@ -318,7 +339,8 @@ async function publishHandler(
318
339
  name: toolName,
319
340
  manifest: manifest as unknown as Record<string, unknown>,
320
341
  bundle,
321
- rawManifest: enactMdContent,
342
+ rawManifest: rawManifestContent,
343
+ visibility,
322
344
  });
323
345
  });
324
346
 
@@ -330,10 +352,15 @@ async function publishHandler(
330
352
 
331
353
  // Success output
332
354
  newline();
333
- success(`Published ${result.name}@${result.version}`);
355
+ success(`Published ${result.name}@${result.version} (${visibility})`);
334
356
  keyValue("Bundle Hash", result.bundleHash);
335
357
  keyValue("Published At", result.publishedAt.toISOString());
336
358
  newline();
359
+ if (visibility === "private") {
360
+ dim("This tool is private - only you can access it.");
361
+ } else if (visibility === "unlisted") {
362
+ dim("This tool is unlisted - accessible via direct link, not searchable.");
363
+ }
337
364
  dim(`Install with: enact install ${toolName}`);
338
365
  }
339
366
 
@@ -349,6 +376,8 @@ export function configurePublishCommand(program: Command): void {
349
376
  .option("-v, --verbose", "Show detailed output")
350
377
  .option("--skip-auth", "Skip authentication (for local development)")
351
378
  .option("--json", "Output as JSON")
379
+ .option("--public", "Publish as public (searchable by everyone)")
380
+ .option("--unlisted", "Publish as unlisted (accessible via direct link, not searchable)")
352
381
  .action(async (pathArg: string | undefined, options: PublishOptions) => {
353
382
  const resolvedPath = pathArg ?? ".";
354
383
  const ctx: CommandContext = {
@@ -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@1.0.0
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 { createApiClient, getToolVersion, submitAttestationToRegistry } from "@enactprotocol/api";
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
- * Returns null if not a valid remote reference
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 @ for version
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 || atIndex === 0) {
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
- // Must have at least one / in the name (author/tool)
85
- if (!name.includes("/") || !version) {
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
- _ctx: CommandContext
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}@${toolRef.version} from registry...`);
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, toolRef.version);
391
+ toolInfo = await getToolVersion(client, toolRef.name, targetVersion);
317
392
  } catch (err) {
318
- error(`Tool not found: ${toolRef.name}@${toolRef.version}`);
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 (_ctx.isInteractive) {
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 (_ctx.isInteractive && !options.json) {
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, _ctx.isInteractive, issuer);
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)")
@@ -0,0 +1,154 @@
1
+ /**
2
+ * enact visibility command
3
+ *
4
+ * Change the visibility of a published tool.
5
+ */
6
+
7
+ import { getSecret } from "@enactprotocol/secrets";
8
+ import { loadConfig } from "@enactprotocol/shared";
9
+ import type { Command } from "commander";
10
+ import type { CommandContext, GlobalOptions } from "../../types";
11
+ import {
12
+ dim,
13
+ error,
14
+ extractNamespace,
15
+ formatError,
16
+ getCurrentUsername,
17
+ header,
18
+ info,
19
+ json,
20
+ newline,
21
+ success,
22
+ } from "../../utils";
23
+
24
+ /** Auth namespace for token storage */
25
+ const AUTH_NAMESPACE = "enact:auth";
26
+ const ACCESS_TOKEN_KEY = "access_token";
27
+
28
+ /** Valid visibility levels */
29
+ const VALID_VISIBILITIES = ["public", "private", "unlisted"] as const;
30
+ type Visibility = (typeof VALID_VISIBILITIES)[number];
31
+
32
+ interface VisibilityOptions extends GlobalOptions {
33
+ json?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Visibility command handler
38
+ */
39
+ async function visibilityHandler(
40
+ tool: string,
41
+ visibility: string,
42
+ options: VisibilityOptions,
43
+ _ctx: CommandContext
44
+ ): Promise<void> {
45
+ // Validate visibility value
46
+ if (!VALID_VISIBILITIES.includes(visibility as Visibility)) {
47
+ error(`Invalid visibility: ${visibility}`);
48
+ newline();
49
+ dim(`Valid values: ${VALID_VISIBILITIES.join(", ")}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ header(`Changing visibility for ${tool}`);
54
+ newline();
55
+
56
+ // Pre-flight namespace check
57
+ const currentUsername = await getCurrentUsername();
58
+ if (currentUsername) {
59
+ const toolNamespace = extractNamespace(tool);
60
+ if (toolNamespace !== currentUsername) {
61
+ error(
62
+ `Namespace mismatch: Tool namespace "${toolNamespace}" does not match your username "${currentUsername}".`
63
+ );
64
+ newline();
65
+ dim("You can only change visibility for your own tools.");
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ // Get registry URL from config or environment
71
+ const config = loadConfig();
72
+ const registryUrl =
73
+ process.env.ENACT_REGISTRY_URL ??
74
+ config.registry?.url ??
75
+ "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
76
+
77
+ // Get auth token
78
+ let authToken = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
79
+ if (!authToken) {
80
+ authToken = config.registry?.authToken ?? process.env.ENACT_AUTH_TOKEN ?? null;
81
+ }
82
+ if (!authToken) {
83
+ error("Not authenticated. Please run: enact auth login");
84
+ process.exit(1);
85
+ }
86
+
87
+ // Make the API request to change visibility
88
+ const response = await fetch(`${registryUrl}/tools/${tool}/visibility`, {
89
+ method: "PATCH",
90
+ headers: {
91
+ Authorization: `Bearer ${authToken}`,
92
+ "Content-Type": "application/json",
93
+ },
94
+ body: JSON.stringify({ visibility }),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const errorText = await response.text();
99
+ try {
100
+ const errorJson = JSON.parse(errorText);
101
+ error(`Failed to change visibility: ${errorJson.error?.message ?? errorText}`);
102
+ } catch {
103
+ error(`Failed to change visibility: ${errorText}`);
104
+ }
105
+ process.exit(1);
106
+ }
107
+
108
+ // Parse response (we don't use it but need to consume the body)
109
+ await response.json();
110
+
111
+ // JSON output
112
+ if (options.json) {
113
+ json({ tool, visibility, success: true });
114
+ return;
115
+ }
116
+
117
+ // Success output
118
+ success(`${tool} is now ${visibility}`);
119
+ newline();
120
+
121
+ if (visibility === "private") {
122
+ info("This tool is now private - only you can access it.");
123
+ } else if (visibility === "unlisted") {
124
+ info("This tool is now unlisted - accessible via direct link, but not searchable.");
125
+ } else {
126
+ info("This tool is now public - anyone can find and install it.");
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Configure the visibility command
132
+ */
133
+ export function configureVisibilityCommand(program: Command): void {
134
+ program
135
+ .command("visibility <tool> <visibility>")
136
+ .description("Change tool visibility (public, private, or unlisted)")
137
+ .option("--json", "Output as JSON")
138
+ .option("-v, --verbose", "Show detailed output")
139
+ .action(async (tool: string, visibility: string, options: VisibilityOptions) => {
140
+ const ctx: CommandContext = {
141
+ cwd: process.cwd(),
142
+ options,
143
+ isCI: Boolean(process.env.CI),
144
+ isInteractive: process.stdout.isTTY ?? false,
145
+ };
146
+
147
+ try {
148
+ await visibilityHandler(tool, visibility, options, ctx);
149
+ } catch (err) {
150
+ error(formatError(err));
151
+ process.exit(1);
152
+ }
153
+ });
154
+ }
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  configureConfigCommand,
16
16
  configureEnvCommand,
17
17
  configureExecCommand,
18
- configureGetCommand,
18
+ configureInfoCommand,
19
19
  configureInitCommand,
20
20
  configureInspectCommand,
21
21
  configureInstallCommand,
@@ -29,11 +29,12 @@ import {
29
29
  configureSignCommand,
30
30
  configureTrustCommand,
31
31
  configureUnyankCommand,
32
+ configureVisibilityCommand,
32
33
  configureYankCommand,
33
34
  } from "./commands";
34
35
  import { error, formatError } from "./utils";
35
36
 
36
- export const version = "2.1.7";
37
+ export const version = "2.1.14";
37
38
 
38
39
  // Export types for external use
39
40
  export type { GlobalOptions, CommandContext } from "./types";
@@ -63,7 +64,7 @@ async function main() {
63
64
 
64
65
  // Registry commands (Phase 8)
65
66
  configureSearchCommand(program);
66
- configureGetCommand(program);
67
+ configureInfoCommand(program);
67
68
  configureLearnCommand(program);
68
69
  configurePublishCommand(program);
69
70
  configureAuthCommand(program);
@@ -78,6 +79,9 @@ async function main() {
78
79
  configureYankCommand(program);
79
80
  configureUnyankCommand(program);
80
81
 
82
+ // Private tools - visibility management
83
+ configureVisibilityCommand(program);
84
+
81
85
  // Global error handler - handle Commander's help/version exits gracefully
82
86
  program.exitOverride((err) => {
83
87
  // Commander throws errors for help, version, and other "exit" scenarios
@@ -1,64 +1,66 @@
1
1
  /**
2
- * Tests for the get command
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 { configureGetCommand } from "../../src/commands/get";
8
+ import { configureInfoCommand } from "../../src/commands/info";
9
9
 
10
- describe("get command", () => {
10
+ describe("info command", () => {
11
11
  describe("command configuration", () => {
12
- test("configures get command on program", () => {
12
+ test("configures info command on program", () => {
13
13
  const program = new Command();
14
- configureGetCommand(program);
14
+ configureInfoCommand(program);
15
15
 
16
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
17
- expect(getCmd).toBeDefined();
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
- configureGetCommand(program);
22
+ configureInfoCommand(program);
23
23
 
24
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
25
- expect(getCmd?.description()).toBe("Show detailed information about a tool");
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 info as alias", () => {
30
+ test("has get as alias", () => {
29
31
  const program = new Command();
30
- configureGetCommand(program);
32
+ configureInfoCommand(program);
31
33
 
32
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
33
- expect(getCmd?.aliases()).toContain("info");
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
- configureGetCommand(program);
40
+ configureInfoCommand(program);
39
41
 
40
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
41
- const args = getCmd?.registeredArguments ?? [];
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
- configureGetCommand(program);
50
+ configureInfoCommand(program);
49
51
 
50
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
51
- const opts = getCmd?.options ?? [];
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
- configureGetCommand(program);
60
+ configureInfoCommand(program);
59
61
 
60
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
61
- const opts = getCmd?.options ?? [];
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
- configureGetCommand(program);
72
+ configureInfoCommand(program);
71
73
 
72
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
73
- const opts = getCmd?.options ?? [];
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
- configureGetCommand(program);
82
+ configureInfoCommand(program);
81
83
 
82
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
83
- const opts = getCmd?.options ?? [];
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 get command", () => {
254
+ test("verbose option is available on info command", () => {
253
255
  const program = new Command();
254
- configureGetCommand(program);
256
+ configureInfoCommand(program);
255
257
 
256
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
257
- const opts = getCmd?.options ?? [];
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");
@@ -159,7 +159,7 @@ describe("init command", () => {
159
159
  expect(content).toEqual({ tools: {} });
160
160
  });
161
161
 
162
- test("--tool mode creates enact.md", async () => {
162
+ test("--tool mode creates SKILL.md", async () => {
163
163
  const program = new Command();
164
164
  program.exitOverride(); // Prevent process.exit
165
165
  configureInitCommand(program);
@@ -176,7 +176,7 @@ describe("init command", () => {
176
176
  process.chdir(originalCwd);
177
177
  }
178
178
 
179
- const manifestPath = join(testDir, "enact.md");
179
+ const manifestPath = join(testDir, "SKILL.md");
180
180
  expect(existsSync(manifestPath)).toBe(true);
181
181
 
182
182
  const content = readFileSync(manifestPath, "utf-8");
@@ -206,7 +206,7 @@ describe("init command", () => {
206
206
 
207
207
  const content = readFileSync(agentsPath, "utf-8");
208
208
  expect(content).toContain("enact run");
209
- expect(content).toContain("enact.md");
209
+ expect(content).toContain("SKILL.md");
210
210
  expect(content).toContain("Parameter Substitution");
211
211
  });
212
212
 
@@ -395,7 +395,7 @@ describe("init command", () => {
395
395
  expect(existsSync(toolsJsonPath)).toBe(false);
396
396
  });
397
397
 
398
- test("enact.md contains valid YAML frontmatter", async () => {
398
+ test("SKILL.md contains valid YAML frontmatter", async () => {
399
399
  const program = new Command();
400
400
  program.exitOverride();
401
401
  configureInitCommand(program);
@@ -418,7 +418,7 @@ describe("init command", () => {
418
418
  process.chdir(originalCwd);
419
419
  }
420
420
 
421
- const content = readFileSync(join(testDir, "enact.md"), "utf-8");
421
+ const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
422
422
 
423
423
  // Check frontmatter structure
424
424
  expect(content.startsWith("---")).toBe(true);
@@ -522,7 +522,7 @@ describe("init command", () => {
522
522
  process.chdir(originalCwd);
523
523
  }
524
524
 
525
- const content = readFileSync(join(testDir, "enact.md"), "utf-8");
525
+ const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
526
526
 
527
527
  // Required fields per spec
528
528
  expect(content).toContain("name:");