@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.
- package/dist/commands/auth/index.js +1 -1
- package/dist/commands/auth/index.js.map +1 -1
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +3 -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 +17 -14
- 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 +2 -0
- package/dist/commands/publish/index.d.ts.map +1 -1
- package/dist/commands/publish/index.js +32 -8
- 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/commands/visibility/index.d.ts +11 -0
- package/dist/commands/visibility/index.d.ts.map +1 -0
- package/dist/commands/visibility/index.js +117 -0
- package/dist/commands/visibility/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- 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 +4 -1
- package/src/commands/{get → info}/index.ts +103 -16
- package/src/commands/init/index.ts +17 -14
- package/src/commands/learn/index.ts +228 -5
- package/src/commands/publish/index.ts +37 -8
- package/src/commands/sign/index.ts +93 -18
- package/src/commands/visibility/index.ts +154 -0
- package/src/index.ts +7 -3
- package/tests/commands/{get.test.ts → info.test.ts} +35 -33
- package/tests/commands/init.test.ts +6 -6
- package/tests/commands/learn.test.ts +2 -2
- package/tests/commands/publish.test.ts +50 -0
- package/tests/commands/visibility.test.ts +156 -0
- package/tests/e2e.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- /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
|
|
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
|
|
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
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
info("Found enact.md
|
|
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:
|
|
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
|
|
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)")
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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");
|
|
@@ -159,7 +159,7 @@ describe("init command", () => {
|
|
|
159
159
|
expect(content).toEqual({ tools: {} });
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
test("--tool mode creates
|
|
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, "
|
|
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("
|
|
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("
|
|
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, "
|
|
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, "
|
|
525
|
+
const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
|
|
526
526
|
|
|
527
527
|
// Required fields per spec
|
|
528
528
|
expect(content).toContain("name:");
|