@enactprotocol/cli 2.1.7 → 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.
Files changed (47) 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 +1 -1
  4. package/dist/commands/index.d.ts.map +1 -1
  5. package/dist/commands/index.js +1 -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.map +1 -1
  23. package/dist/commands/publish/index.js +14 -7
  24. package/dist/commands/publish/index.js.map +1 -1
  25. package/dist/commands/sign/index.d.ts +2 -1
  26. package/dist/commands/sign/index.d.ts.map +1 -1
  27. package/dist/commands/sign/index.js +75 -17
  28. package/dist/commands/sign/index.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/index.js.map +1 -1
  33. package/package.json +5 -5
  34. package/src/commands/auth/index.ts +1 -1
  35. package/src/commands/index.ts +1 -1
  36. package/src/commands/{get → info}/index.ts +103 -16
  37. package/src/commands/init/index.ts +17 -14
  38. package/src/commands/learn/index.ts +228 -5
  39. package/src/commands/publish/index.ts +14 -7
  40. package/src/commands/sign/index.ts +93 -18
  41. package/src/index.ts +3 -3
  42. package/tests/commands/{get.test.ts → info.test.ts} +35 -33
  43. package/tests/commands/init.test.ts +6 -6
  44. package/tests/commands/learn.test.ts +2 -2
  45. package/tests/e2e.test.ts +1 -1
  46. package/tsconfig.tsbuildinfo +1 -1
  47. /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
@@ -26,7 +26,7 @@ const SUPABASE_ANON_KEY =
26
26
  * Embedded templates (for single-binary compatibility)
27
27
  */
28
28
  const TEMPLATES: Record<string, string> = {
29
- "tool-enact.md": `---
29
+ "tool-skill.md": `---
30
30
  name: {{TOOL_NAME}}
31
31
  description: A simple tool that echoes a greeting
32
32
  version: 0.1.0
@@ -74,7 +74,7 @@ Edit this file to create your own tool:
74
74
 
75
75
  "tool-agents.md": `# Enact Tool Development Guide
76
76
 
77
- Enact tools are containerized, cryptographically-signed executables. Each tool is defined by an \`enact.md\` file (YAML frontmatter + Markdown docs).
77
+ Enact tools are containerized, cryptographically-signed executables. Each tool is defined by a \`SKILL.md\` file (YAML frontmatter + Markdown docs).
78
78
 
79
79
  ## Quick Reference
80
80
 
@@ -85,7 +85,7 @@ Enact tools are containerized, cryptographically-signed executables. Each tool i
85
85
  | Dry run | \`enact run ./ --args '{}' --dry-run\` |
86
86
  | Sign & publish | \`enact sign ./ && enact publish ./\` |
87
87
 
88
- ## enact.md Structure
88
+ ## SKILL.md Structure
89
89
 
90
90
  \`\`\`yaml
91
91
  ---
@@ -225,7 +225,7 @@ Tools run in a container with \`/work\` as the working directory. All source fil
225
225
 
226
226
  ## Secrets
227
227
 
228
- Declare in \`enact.md\`:
228
+ Declare in \`SKILL.md\`:
229
229
  \`\`\`yaml
230
230
  env:
231
231
  API_KEY:
@@ -302,6 +302,7 @@ enact run ./path/to/tool --args '{}' # Run local tool
302
302
  \`\`\`bash
303
303
  enact search "pdf extraction" # Search registry
304
304
  enact get author/category/tool # View tool info
305
+ enact learn author/category/tool # View tool documentation
305
306
  enact install author/category/tool # Add to project (.enact/tools.json)
306
307
  enact install author/category/tool --global # Add globally
307
308
  enact list # List project tools
@@ -314,7 +315,7 @@ enact run tool --args '{}' | jq '.result'
314
315
  \`\`\`
315
316
 
316
317
  ## Creating Local Tools
317
- Create \`tools/<name>/enact.md\` with:
318
+ Create \`tools/<name>/SKILL.md\` with:
318
319
  \`\`\`yaml
319
320
  ---
320
321
  name: my-tool
@@ -345,6 +346,7 @@ This project uses Enact tools — containerized, signed executables you can run
345
346
  \`\`\`bash
346
347
  enact run <tool> --args '{"key": "value"}' # Run a tool
347
348
  enact search "keyword" # Find tools
349
+ enact learn author/tool # View tool documentation
348
350
  enact install author/tool # Install tool
349
351
  enact list # List installed tools
350
352
  \`\`\`
@@ -360,7 +362,7 @@ enact run tool --args '{}' | jq '.data'
360
362
  \`\`\`
361
363
 
362
364
  ## Creating Tools
363
- Create \`enact.md\` in a directory:
365
+ Create \`SKILL.md\` in a directory:
364
366
  \`\`\`yaml
365
367
  ---
366
368
  name: namespace/category/tool
@@ -394,7 +396,7 @@ enact sign ./ && enact publish ./ # Publish
394
396
  \`\`\`
395
397
 
396
398
  ## Secrets
397
- Declare in enact.md, set via CLI:
399
+ Declare in SKILL.md, set via CLI:
398
400
  \`\`\`yaml
399
401
  env:
400
402
  API_KEY: # Declared but not set
@@ -543,9 +545,9 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
543
545
  const isClaudeMode = options.claude;
544
546
  // Default to agent mode if no flag specified
545
547
 
546
- // Handle --tool mode: create enact.md + AGENTS.md for tool development
548
+ // Handle --tool mode: create SKILL.md + AGENTS.md for tool development
547
549
  if (isToolMode) {
548
- const manifestPath = join(targetDir, "enact.md");
550
+ const manifestPath = join(targetDir, "SKILL.md");
549
551
  const agentsPath = join(targetDir, "AGENTS.md");
550
552
 
551
553
  if (existsSync(manifestPath) && !options.force) {
@@ -571,7 +573,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
571
573
 
572
574
  // Load templates with placeholder replacement
573
575
  const replacements = { TOOL_NAME: toolName };
574
- const manifestContent = loadTemplate("tool-enact.md", replacements);
576
+ const manifestContent = loadTemplate("tool-skill.md", replacements);
575
577
  const agentsContent = loadTemplate("tool-agents.md", replacements);
576
578
 
577
579
  // Ensure directory exists
@@ -579,7 +581,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
579
581
  mkdirSync(targetDir, { recursive: true });
580
582
  }
581
583
 
582
- // Write enact.md
584
+ // Write SKILL.md
583
585
  writeFileSync(manifestPath, manifestContent, "utf-8");
584
586
  success(`Created tool manifest: ${manifestPath}`);
585
587
 
@@ -593,7 +595,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
593
595
 
594
596
  info("");
595
597
  info("Next steps:");
596
- info(" 1. Edit enact.md to customize your tool");
598
+ info(" 1. Edit SKILL.md to customize your tool");
597
599
  info(" 2. Run 'enact run ./' to test your tool");
598
600
  info(" 3. Run 'enact publish' to share your tool");
599
601
  return;
@@ -637,7 +639,8 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
637
639
 
638
640
  info("");
639
641
  info("This file helps AI agents understand how to use Enact tools in your project.");
640
- info("Run 'enact search <query>' to find tools, 'enact install <tool>' to add them.");
642
+ info("Run 'enact search <query>' to find tools, 'enact learn <tool>' to view docs,");
643
+ info("and 'enact install <tool>' to add them.");
641
644
  }
642
645
 
643
646
  /**
@@ -649,7 +652,7 @@ export function configureInitCommand(program: Command): void {
649
652
  .description("Initialize Enact in the current directory")
650
653
  .option("-n, --name <name>", "Tool name (default: username/my-tool)")
651
654
  .option("-f, --force", "Overwrite existing files")
652
- .option("--tool", "Create a new Enact tool (enact.md + AGENTS.md)")
655
+ .option("--tool", "Create a new Enact tool (SKILL.md + AGENTS.md)")
653
656
  .option("--agent", "Create AGENTS.md + .enact/tools.json (default)")
654
657
  .option("--claude", "Create CLAUDE.md + .enact/tools.json")
655
658
  .option("-v, --verbose", "Show detailed output")
@@ -3,16 +3,46 @@
3
3
  *
4
4
  * Display the documentation (enact.md) for a tool.
5
5
  * Fetches and displays the raw manifest content for easy reading.
6
+ *
7
+ * Security: For tools fetched from the registry, attestation checks are
8
+ * performed according to the trust policy. This prevents potentially
9
+ * malicious documentation from being displayed to LLMs or users.
6
10
  */
7
11
 
8
- import { createApiClient, getToolInfo, getToolVersion } from "@enactprotocol/api";
9
- import { loadConfig } from "@enactprotocol/shared";
12
+ import {
13
+ type AttestationListResponse,
14
+ createApiClient,
15
+ getAttestationList,
16
+ getToolInfo,
17
+ getToolVersion,
18
+ verifyAllAttestations,
19
+ } from "@enactprotocol/api";
20
+ import {
21
+ getMinimumAttestations,
22
+ getTrustPolicy,
23
+ getTrustedAuditors,
24
+ loadConfig,
25
+ tryResolveTool,
26
+ } from "@enactprotocol/shared";
10
27
  import type { Command } from "commander";
11
28
  import type { CommandContext, GlobalOptions } from "../../types";
12
- import { dim, error, formatError, header, json, newline } from "../../utils";
29
+ import {
30
+ TrustError,
31
+ confirm,
32
+ dim,
33
+ error,
34
+ formatError,
35
+ header,
36
+ info,
37
+ json,
38
+ newline,
39
+ success,
40
+ symbols,
41
+ } from "../../utils";
13
42
 
14
43
  interface LearnOptions extends GlobalOptions {
15
44
  ver?: string;
45
+ local?: boolean;
16
46
  }
17
47
 
18
48
  /**
@@ -21,14 +51,76 @@ interface LearnOptions extends GlobalOptions {
21
51
  async function learnHandler(
22
52
  toolName: string,
23
53
  options: LearnOptions,
24
- _ctx: CommandContext
54
+ ctx: CommandContext
25
55
  ): Promise<void> {
56
+ // First, try to resolve locally (project → user → cache)
57
+ // If the tool is already installed/cached, we trust it
58
+ const resolution = tryResolveTool(toolName, { startDir: ctx.cwd });
59
+
60
+ if (resolution) {
61
+ // Tool is installed locally - read documentation from the manifest file
62
+ if (resolution.manifestPath.endsWith(".md")) {
63
+ const { readFileSync } = await import("node:fs");
64
+ const content = readFileSync(resolution.manifestPath, "utf-8");
65
+
66
+ if (options.json) {
67
+ json({
68
+ name: toolName,
69
+ version: resolution.manifest.version,
70
+ documentation: content,
71
+ source: "local",
72
+ });
73
+ return;
74
+ }
75
+
76
+ header(`${toolName}@${resolution.manifest.version ?? "local"}`);
77
+ dim("(installed locally)");
78
+ newline();
79
+ console.log(content);
80
+ return;
81
+ }
82
+
83
+ // Fallback for non-.md manifests
84
+ if (options.json) {
85
+ json({
86
+ name: toolName,
87
+ version: resolution.manifest.version,
88
+ documentation: resolution.manifest.doc ?? resolution.manifest.description ?? null,
89
+ source: "local",
90
+ });
91
+ return;
92
+ }
93
+
94
+ header(`${toolName}@${resolution.manifest.version ?? "local"}`);
95
+ dim("(installed locally)");
96
+ newline();
97
+ console.log(
98
+ resolution.manifest.doc ?? resolution.manifest.description ?? "No documentation available."
99
+ );
100
+ return;
101
+ }
102
+
103
+ // If --local flag is set, don't fetch from registry
104
+ if (options.local) {
105
+ error(`Tool not found locally: ${toolName}`);
106
+ dim("The tool is not installed. Remove --local to fetch from registry.");
107
+ process.exit(1);
108
+ }
109
+
110
+ // Tool not installed - fetch from registry with attestation checks
26
111
  const config = loadConfig();
27
112
  const registryUrl =
28
113
  process.env.ENACT_REGISTRY_URL ??
29
114
  config.registry?.url ??
30
115
  "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
31
- const authToken = config.registry?.authToken;
116
+
117
+ // Get auth token - use user token if available, otherwise use anon key for public access
118
+ let authToken = config.registry?.authToken ?? process.env.ENACT_AUTH_TOKEN;
119
+ if (!authToken && registryUrl.includes("siikwkfgsmouioodghho.supabase.co")) {
120
+ authToken =
121
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
122
+ }
123
+
32
124
  const client = createApiClient({
33
125
  baseUrl: registryUrl,
34
126
  authToken: authToken,
@@ -42,14 +134,139 @@ async function learnHandler(
42
134
  version = toolInfo.latestVersion;
43
135
  }
44
136
 
137
+ if (!version) {
138
+ error(`No published versions for ${toolName}`);
139
+ process.exit(1);
140
+ }
141
+
45
142
  // Get the version info which includes rawManifest
46
143
  const versionInfo = await getToolVersion(client, toolName, version);
47
144
 
145
+ // ========================================
146
+ // TRUST VERIFICATION - same as run command
147
+ // ========================================
148
+ const trustPolicy = getTrustPolicy();
149
+ const minimumAttestations = getMinimumAttestations();
150
+ const trustedAuditors = getTrustedAuditors();
151
+
152
+ // Fetch attestations from registry
153
+ const attestationsResponse: AttestationListResponse = await getAttestationList(
154
+ client,
155
+ toolName,
156
+ version
157
+ );
158
+ const attestations = attestationsResponse.attestations;
159
+
160
+ if (attestations.length === 0) {
161
+ // No attestations found
162
+ info(`${symbols.warning} Tool ${toolName}@${version} has no attestations.`);
163
+
164
+ if (trustPolicy === "require_attestation") {
165
+ throw new TrustError(
166
+ "Trust policy requires attestations. Cannot display documentation from unverified tools."
167
+ );
168
+ }
169
+ if (ctx.isInteractive && trustPolicy === "prompt") {
170
+ dim("Documentation from unverified tools may contain malicious content.");
171
+ const proceed = await confirm("View documentation from unverified tool?");
172
+ if (!proceed) {
173
+ info("Cancelled.");
174
+ process.exit(0);
175
+ }
176
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
177
+ throw new TrustError(
178
+ "Cannot display documentation from unverified tools in non-interactive mode."
179
+ );
180
+ }
181
+ // trustPolicy === "allow" - continue without prompting
182
+ } else {
183
+ // Verify attestations locally (never trust registry's verification status)
184
+ const verifiedAuditors = await verifyAllAttestations(
185
+ client,
186
+ toolName,
187
+ version,
188
+ versionInfo.bundle.hash ?? ""
189
+ );
190
+
191
+ // Check verified auditors against trust config using provider:identity format
192
+ const trustedVerifiedAuditors = verifiedAuditors
193
+ .filter((auditor) => trustedAuditors.includes(auditor.providerIdentity))
194
+ .map((auditor) => auditor.providerIdentity);
195
+
196
+ if (trustedVerifiedAuditors.length > 0) {
197
+ // Check if we meet minimum attestations threshold
198
+ if (trustedVerifiedAuditors.length < minimumAttestations) {
199
+ info(
200
+ `${symbols.warning} Tool ${toolName}@${version} has ${trustedVerifiedAuditors.length} trusted attestation(s), but ${minimumAttestations} required.`
201
+ );
202
+ dim(`Trusted attestations: ${trustedVerifiedAuditors.join(", ")}`);
203
+
204
+ if (trustPolicy === "require_attestation") {
205
+ throw new TrustError(
206
+ `Trust policy requires at least ${minimumAttestations} attestation(s) from trusted identities.`
207
+ );
208
+ }
209
+ if (ctx.isInteractive && trustPolicy === "prompt") {
210
+ const proceed = await confirm(
211
+ "View documentation with fewer attestations than required?"
212
+ );
213
+ if (!proceed) {
214
+ info("Cancelled.");
215
+ process.exit(0);
216
+ }
217
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
218
+ throw new TrustError(
219
+ "Cannot display documentation without meeting minimum attestation requirement in non-interactive mode."
220
+ );
221
+ }
222
+ // trustPolicy === "allow" - continue without prompting
223
+ } else {
224
+ // Tool meets or exceeds minimum attestations
225
+ if (options.verbose) {
226
+ success(
227
+ `Tool verified by ${trustedVerifiedAuditors.length} trusted identity(ies): ${trustedVerifiedAuditors.join(", ")}`
228
+ );
229
+ }
230
+ }
231
+ } else {
232
+ // Has attestations but none from trusted auditors
233
+ info(
234
+ `${symbols.warning} Tool ${toolName}@${version} has ${verifiedAuditors.length} attestation(s), but none from trusted auditors.`
235
+ );
236
+
237
+ if (trustPolicy === "require_attestation") {
238
+ dim(`Your trusted auditors: ${trustedAuditors.join(", ")}`);
239
+ dim(`Tool attested by: ${verifiedAuditors.map((a) => a.providerIdentity).join(", ")}`);
240
+ throw new TrustError(
241
+ "Trust policy requires attestations from trusted identities. Cannot display documentation."
242
+ );
243
+ }
244
+ if (ctx.isInteractive && trustPolicy === "prompt") {
245
+ dim(`Attested by: ${verifiedAuditors.map((a) => a.providerIdentity).join(", ")}`);
246
+ dim(`Your trusted auditors: ${trustedAuditors.join(", ")}`);
247
+ const proceed = await confirm("View documentation anyway?");
248
+ if (!proceed) {
249
+ info("Cancelled.");
250
+ process.exit(0);
251
+ }
252
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
253
+ throw new TrustError(
254
+ "Cannot display documentation without trusted attestations in non-interactive mode."
255
+ );
256
+ }
257
+ // trustPolicy === "allow" - continue without prompting
258
+ }
259
+ }
260
+
261
+ // ========================================
262
+ // Display documentation (trust verified)
263
+ // ========================================
48
264
  if (options.json) {
49
265
  json({
50
266
  name: toolName,
51
267
  version: versionInfo.version,
52
268
  documentation: versionInfo.rawManifest ?? null,
269
+ source: "registry",
53
270
  });
54
271
  return;
55
272
  }
@@ -65,6 +282,10 @@ async function learnHandler(
65
282
  newline();
66
283
  console.log(versionInfo.rawManifest);
67
284
  } catch (err) {
285
+ if (err instanceof TrustError) {
286
+ error(err.message);
287
+ process.exit(1);
288
+ }
68
289
  if (err instanceof Error) {
69
290
  if (err.message.includes("not_found") || err.message.includes("404")) {
70
291
  error(`Tool not found: ${toolName}`);
@@ -88,7 +309,9 @@ export function configureLearnCommand(program: Command): void {
88
309
  .command("learn <tool>")
89
310
  .description("Display documentation (enact.md) for a tool")
90
311
  .option("--ver <version>", "Show documentation for a specific version")
312
+ .option("--local", "Only show documentation for locally installed tools")
91
313
  .option("--json", "Output as JSON")
314
+ .option("-v, --verbose", "Show detailed output")
92
315
  .action(async (toolName: string, options: LearnOptions) => {
93
316
  const ctx: CommandContext = {
94
317
  cwd: process.cwd(),
@@ -130,9 +130,16 @@ async function createBundleFromDir(toolDir: string): Promise<Uint8Array> {
130
130
  }
131
131
 
132
132
  /**
133
- * Load the raw enact.md file content (full documentation with frontmatter)
133
+ * Load the raw markdown manifest file content (full documentation with frontmatter)
134
+ * Checks for SKILL.md first (preferred), then falls back to enact.md
134
135
  */
135
- function loadEnactMd(toolDir: string): string | undefined {
136
+ function loadRawManifest(toolDir: string): string | undefined {
137
+ // Check SKILL.md first (preferred format)
138
+ const skillMdPath = join(toolDir, "SKILL.md");
139
+ if (existsSync(skillMdPath)) {
140
+ return readFileSync(skillMdPath, "utf-8");
141
+ }
142
+ // Fall back to enact.md
136
143
  const enactMdPath = join(toolDir, "enact.md");
137
144
  if (existsSync(enactMdPath)) {
138
145
  return readFileSync(enactMdPath, "utf-8");
@@ -299,10 +306,10 @@ async function publishHandler(
299
306
  return;
300
307
  }
301
308
 
302
- // Load the full enact.md content (frontmatter + documentation)
303
- const enactMdContent = loadEnactMd(toolDir);
304
- if (enactMdContent) {
305
- info("Found enact.md documentation");
309
+ // Load the full markdown manifest content (SKILL.md or enact.md)
310
+ const rawManifestContent = loadRawManifest(toolDir);
311
+ if (rawManifestContent) {
312
+ info("Found markdown documentation (SKILL.md or enact.md)");
306
313
  }
307
314
 
308
315
  // Create bundle
@@ -318,7 +325,7 @@ async function publishHandler(
318
325
  name: toolName,
319
326
  manifest: manifest as unknown as Record<string, unknown>,
320
327
  bundle,
321
- rawManifest: enactMdContent,
328
+ rawManifest: rawManifestContent,
322
329
  });
323
330
  });
324
331
 
@@ -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)")
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,
@@ -33,7 +33,7 @@ import {
33
33
  } from "./commands";
34
34
  import { error, formatError } from "./utils";
35
35
 
36
- export const version = "2.1.7";
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
- configureGetCommand(program);
66
+ configureInfoCommand(program);
67
67
  configureLearnCommand(program);
68
68
  configurePublishCommand(program);
69
69
  configureAuthCommand(program);