@enactprotocol/cli 2.1.10 → 2.1.15

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/index.d.ts +1 -0
  2. package/dist/commands/index.d.ts.map +1 -1
  3. package/dist/commands/index.js +2 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/commands/init/index.d.ts.map +1 -1
  6. package/dist/commands/init/index.js +6 -377
  7. package/dist/commands/init/index.js.map +1 -1
  8. package/dist/commands/init/templates/agent-agents.d.ts +5 -0
  9. package/dist/commands/init/templates/agent-agents.d.ts.map +1 -0
  10. package/dist/commands/init/templates/agent-agents.js +53 -0
  11. package/dist/commands/init/templates/agent-agents.js.map +1 -0
  12. package/dist/commands/init/templates/claude.d.ts +5 -0
  13. package/dist/commands/init/templates/claude.d.ts.map +1 -0
  14. package/dist/commands/init/templates/claude.js +71 -0
  15. package/dist/commands/init/templates/claude.js.map +1 -0
  16. package/dist/commands/init/templates/index.d.ts +8 -0
  17. package/dist/commands/init/templates/index.d.ts.map +1 -0
  18. package/dist/commands/init/templates/index.js +8 -0
  19. package/dist/commands/init/templates/index.js.map +1 -0
  20. package/dist/commands/init/templates/tool-agents.d.ts +5 -0
  21. package/dist/commands/init/templates/tool-agents.d.ts.map +1 -0
  22. package/dist/commands/init/templates/tool-agents.js +219 -0
  23. package/dist/commands/init/templates/tool-agents.js.map +1 -0
  24. package/dist/commands/init/templates/tool-skill.d.ts +5 -0
  25. package/dist/commands/init/templates/tool-skill.d.ts.map +1 -0
  26. package/dist/commands/init/templates/tool-skill.js +76 -0
  27. package/dist/commands/init/templates/tool-skill.js.map +1 -0
  28. package/dist/commands/publish/index.d.ts +2 -0
  29. package/dist/commands/publish/index.d.ts.map +1 -1
  30. package/dist/commands/publish/index.js +18 -1
  31. package/dist/commands/publish/index.js.map +1 -1
  32. package/dist/commands/visibility/index.d.ts +11 -0
  33. package/dist/commands/visibility/index.d.ts.map +1 -0
  34. package/dist/commands/visibility/index.js +117 -0
  35. package/dist/commands/visibility/index.js.map +1 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -2
  39. package/dist/index.js.map +1 -1
  40. package/package.json +5 -5
  41. package/src/commands/index.ts +3 -0
  42. package/src/commands/init/index.ts +11 -380
  43. package/src/commands/init/templates/{agent-agents.md → agent-agents.ts} +20 -15
  44. package/src/commands/init/templates/{claude.md → claude.ts} +24 -19
  45. package/src/commands/init/templates/index.ts +7 -0
  46. package/src/commands/init/templates/tool-agents.ts +218 -0
  47. package/src/commands/init/templates/tool-skill.ts +75 -0
  48. package/src/commands/publish/index.ts +23 -1
  49. package/src/commands/visibility/index.ts +154 -0
  50. package/src/index.ts +5 -1
  51. package/tests/commands/publish.test.ts +50 -0
  52. package/tests/commands/visibility.test.ts +156 -0
  53. package/tsconfig.tsbuildinfo +1 -1
  54. package/src/commands/init/templates/tool-agents.md +0 -56
  55. package/src/commands/init/templates/tool-enact.md +0 -44
@@ -0,0 +1,218 @@
1
+ /**
2
+ * AGENTS.md template for tool development
3
+ */
4
+ export const toolAgentsTemplate = `# Enact Tool Development Guide
5
+
6
+ Enact tools are containerized, cryptographically-signed executables. Each tool is defined by a \`SKILL.md\` file (YAML frontmatter + Markdown docs).
7
+
8
+ ## Quick Reference
9
+
10
+ | Task | Command |
11
+ |------|---------|
12
+ | Run with JSON | \`enact run ./ --args '{"key": "value"}'\` |
13
+ | Run from file | \`enact run ./ --input-file inputs.json\` |
14
+ | Dry run | \`enact run ./ --args '{}' --dry-run\` |
15
+ | Sign & publish | \`enact sign ./ && enact publish ./\` |
16
+
17
+ ## SKILL.md Structure
18
+
19
+ \`\`\`yaml
20
+ ---
21
+ name: {{TOOL_NAME}}
22
+ description: What the tool does
23
+ version: 1.0.0
24
+ enact: "2.0.0"
25
+
26
+ from: python:3.12-slim # Docker image (pin versions, not :latest)
27
+ build: pip install requests # Build steps (cached by Dagger)
28
+ command: python /work/main.py \${input}
29
+ timeout: 30s
30
+
31
+ inputSchema:
32
+ type: object
33
+ properties:
34
+ input:
35
+ type: string
36
+ description: "Input to process"
37
+ required: [input]
38
+
39
+ outputSchema:
40
+ type: object
41
+ properties:
42
+ result:
43
+ type: string
44
+
45
+ env:
46
+ API_KEY:
47
+ description: "External API key"
48
+ secret: true # Set via: enact env set API_KEY --secret
49
+ ---
50
+ # Tool Name
51
+ Documentation here.
52
+ \`\`\`
53
+
54
+ ## Field Reference
55
+
56
+ | Field | Description |
57
+ |-------|-------------|
58
+ | \`name\` | Hierarchical ID: \`org/category/tool\` |
59
+ | \`description\` | What the tool does |
60
+ | \`version\` | Semver version |
61
+ | \`from\` | Docker image |
62
+ | \`build\` | Build commands (string or array, cached) |
63
+ | \`command\` | Shell command with \`\${param}\` substitution |
64
+ | \`timeout\` | Max execution time (e.g., "30s", "5m") |
65
+ | \`inputSchema\` | JSON Schema for inputs |
66
+ | \`outputSchema\` | JSON Schema for outputs |
67
+ | \`env\` | Environment variables and secrets |
68
+
69
+ ## Parameter Substitution
70
+
71
+ Enact auto-quotes parameters. **Never manually quote:**
72
+
73
+ \`\`\`yaml
74
+ # WRONG - causes double-quoting
75
+ command: python /work/main.py "\${input}"
76
+
77
+ # RIGHT - Enact handles quoting
78
+ command: python /work/main.py \${input}
79
+ \`\`\`
80
+
81
+ **Optional params:** Missing optional params become empty strings. Always provide defaults:
82
+ \`\`\`yaml
83
+ inputSchema:
84
+ properties:
85
+ greeting:
86
+ type: string
87
+ default: "Hello" # Recommended for optional params
88
+ \`\`\`
89
+
90
+ Or handle empty in shell:
91
+ \`\`\`yaml
92
+ command: "echo \${greeting:-Hello} \${name}"
93
+ \`\`\`
94
+
95
+ Modifiers:
96
+ - \`\${param}\` — auto-quoted (handles spaces, JSON, special chars)
97
+ - \`\${param:raw}\` — raw, no quoting (use carefully)
98
+
99
+ ## Output
100
+
101
+ Output valid JSON to stdout when \`outputSchema\` is defined:
102
+
103
+ \`\`\`python
104
+ import json, sys
105
+
106
+ try:
107
+ result = do_work()
108
+ print(json.dumps({"status": "success", "result": result}))
109
+ except Exception as e:
110
+ print(json.dumps({"status": "error", "message": str(e)}))
111
+ sys.exit(1) # non-zero = error
112
+ \`\`\`
113
+
114
+ ## Build Steps by Language
115
+
116
+ **Python:**
117
+ \`\`\`yaml
118
+ from: python:3.12-slim
119
+ build: pip install requests pandas
120
+ \`\`\`
121
+
122
+ **Node.js:**
123
+ \`\`\`yaml
124
+ from: node:20-alpine
125
+ build:
126
+ - npm install
127
+ - npm run build
128
+ \`\`\`
129
+
130
+ **Rust:**
131
+ \`\`\`yaml
132
+ from: rust:1.83-slim
133
+ build: rustc /work/main.rs -o /work/tool
134
+ command: /work/tool \${input}
135
+ \`\`\`
136
+
137
+ **Go:**
138
+ \`\`\`yaml
139
+ from: golang:1.22-alpine
140
+ build: cd /work && go build -o tool main.go
141
+ command: /work/tool \${input}
142
+ \`\`\`
143
+
144
+ **System packages:**
145
+ \`\`\`yaml
146
+ build: apt-get update && apt-get install -y libfoo-dev
147
+ \`\`\`
148
+
149
+ Build steps are cached — first run slow, subsequent runs instant.
150
+
151
+ ## File Access
152
+
153
+ Tools run in a container with \`/work\` as the working directory. All source files are copied there.
154
+
155
+ ## Secrets
156
+
157
+ Declare in \`SKILL.md\`:
158
+ \`\`\`yaml
159
+ env:
160
+ API_KEY:
161
+ description: "API key for service"
162
+ secret: true
163
+ \`\`\`
164
+
165
+ Set before running:
166
+ \`\`\`bash
167
+ enact env set API_KEY --secret --namespace {{TOOL_NAME}}
168
+ \`\`\`
169
+
170
+ Access in code:
171
+ \`\`\`python
172
+ import os
173
+ api_key = os.environ.get('API_KEY')
174
+ \`\`\`
175
+
176
+ ## LLM Instruction Tools
177
+
178
+ Tools without a \`command\` field are interpreted by LLMs:
179
+
180
+ \`\`\`yaml
181
+ ---
182
+ name: myorg/ai/reviewer
183
+ description: AI-powered code review
184
+ inputSchema:
185
+ type: object
186
+ properties:
187
+ code: { type: string }
188
+ required: [code]
189
+ outputSchema:
190
+ type: object
191
+ properties:
192
+ issues: { type: array }
193
+ score: { type: number }
194
+ ---
195
+ # Code Reviewer
196
+
197
+ You are a senior engineer. Review the code for bugs, style, and security.
198
+ Return JSON: {"issues": [...], "score": 75}
199
+ \`\`\`
200
+
201
+ ## Publishing Checklist
202
+
203
+ - [ ] \`name\` follows \`namespace/category/tool\` pattern
204
+ - [ ] \`version\` set (semver)
205
+ - [ ] \`description\` is clear and searchable
206
+ - [ ] \`inputSchema\` / \`outputSchema\` defined
207
+ - [ ] \`from\` uses pinned image version
208
+ - [ ] \`timeout\` set appropriately
209
+ - [ ] Tool tested locally with \`enact run ./\`
210
+
211
+ ## Troubleshooting
212
+
213
+ \`\`\`bash
214
+ enact run ./ --args '{"x": "y"}' --verbose # Verbose output
215
+ enact run ./ --args '{}' --dry-run # Preview command
216
+ enact list # List installed tools
217
+ \`\`\`
218
+ `;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * SKILL.md template for new tool creation
3
+ */
4
+ export const toolSkillTemplate = `---
5
+ name: {{TOOL_NAME}}
6
+ description: A simple tool that echoes a greeting
7
+ version: 0.1.0
8
+ enact: "2.0"
9
+
10
+ from: python:3.12-slim
11
+
12
+ # Install dependencies (cached by Dagger)
13
+ build: |
14
+ pip install requests
15
+
16
+ # Environment variables (optional)
17
+ # env:
18
+ # API_KEY:
19
+ # secret: true
20
+ # description: "Your API key"
21
+
22
+ inputSchema:
23
+ type: object
24
+ properties:
25
+ name:
26
+ type: string
27
+ description: Name to greet
28
+ default: World
29
+ required: []
30
+
31
+ command: |
32
+ echo "Hello, \${name}!"
33
+ ---
34
+
35
+ # {{TOOL_NAME}}
36
+
37
+ A simple greeting tool created with \`enact init\`.
38
+
39
+ ## Usage
40
+
41
+ \`\`\`bash
42
+ enact run ./ --args '{"name": "Alice"}'
43
+ \`\`\`
44
+
45
+ ## Customization
46
+
47
+ Edit this file to create your own tool:
48
+
49
+ 1. Update the \`name\` and \`description\` in the frontmatter
50
+ 2. Change the \`from\` image to match your runtime (python, node, rust, etc.)
51
+ 3. Add dependencies in the \`build\` section (pip install, npm install, etc.)
52
+ 4. Uncomment and configure \`env\` for secrets/API keys
53
+ 5. Modify the \`inputSchema\` to define your tool's inputs
54
+ 6. Change the \`command\` to run your script or shell commands
55
+ 7. Update this documentation section
56
+
57
+ ## Environment Variables
58
+
59
+ To use secrets, uncomment the \`env\` section above, then set the value:
60
+
61
+ \`\`\`bash
62
+ enact env set API_KEY --secret --namespace {{TOOL_NAME}}
63
+ \`\`\`
64
+
65
+ Access in your code:
66
+ \`\`\`python
67
+ import os
68
+ api_key = os.environ.get('API_KEY')
69
+ \`\`\`
70
+
71
+ ## Learn More
72
+
73
+ - [Enact Documentation](https://enact.dev/docs)
74
+ - [Tool Manifest Reference](https://enact.dev/docs/manifest)
75
+ `;
@@ -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
  /**
@@ -203,10 +208,18 @@ async function publishHandler(
203
208
  header(`Publishing ${toolName}@${version}`);
204
209
  newline();
205
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
+
206
218
  // Show what we're publishing
207
219
  keyValue("Name", toolName);
208
220
  keyValue("Version", version);
209
221
  keyValue("Description", manifest.description);
222
+ keyValue("Visibility", visibility);
210
223
  if (manifest.tags && manifest.tags.length > 0) {
211
224
  keyValue("Tags", manifest.tags.join(", "));
212
225
  }
@@ -290,6 +303,7 @@ async function publishHandler(
290
303
  info("Would publish to registry:");
291
304
  keyValue("Tool", toolName);
292
305
  keyValue("Version", version);
306
+ keyValue("Visibility", visibility);
293
307
  keyValue("Source", toolDir);
294
308
 
295
309
  // Show files that would be bundled
@@ -326,6 +340,7 @@ async function publishHandler(
326
340
  manifest: manifest as unknown as Record<string, unknown>,
327
341
  bundle,
328
342
  rawManifest: rawManifestContent,
343
+ visibility,
329
344
  });
330
345
  });
331
346
 
@@ -337,10 +352,15 @@ async function publishHandler(
337
352
 
338
353
  // Success output
339
354
  newline();
340
- success(`Published ${result.name}@${result.version}`);
355
+ success(`Published ${result.name}@${result.version} (${visibility})`);
341
356
  keyValue("Bundle Hash", result.bundleHash);
342
357
  keyValue("Published At", result.publishedAt.toISOString());
343
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
+ }
344
364
  dim(`Install with: enact install ${toolName}`);
345
365
  }
346
366
 
@@ -356,6 +376,8 @@ export function configurePublishCommand(program: Command): void {
356
376
  .option("-v, --verbose", "Show detailed output")
357
377
  .option("--skip-auth", "Skip authentication (for local development)")
358
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)")
359
381
  .action(async (pathArg: string | undefined, options: PublishOptions) => {
360
382
  const resolvedPath = pathArg ?? ".";
361
383
  const ctx: CommandContext = {
@@ -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
@@ -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.10";
37
+ export const version = "2.1.15";
37
38
 
38
39
  // Export types for external use
39
40
  export type { GlobalOptions, CommandContext } from "./types";
@@ -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
@@ -54,6 +54,56 @@ describe("publish command", () => {
54
54
  const jsonOpt = opts.find((o) => o.long === "--json");
55
55
  expect(jsonOpt).toBeDefined();
56
56
  });
57
+
58
+ test("has --public option", () => {
59
+ const program = new Command();
60
+ configurePublishCommand(program);
61
+
62
+ const publishCmd = program.commands.find((cmd) => cmd.name() === "publish");
63
+ const opts = publishCmd?.options ?? [];
64
+ const publicOpt = opts.find((o) => o.long === "--public");
65
+ expect(publicOpt).toBeDefined();
66
+ });
67
+
68
+ test("has --unlisted option", () => {
69
+ const program = new Command();
70
+ configurePublishCommand(program);
71
+
72
+ const publishCmd = program.commands.find((cmd) => cmd.name() === "publish");
73
+ const opts = publishCmd?.options ?? [];
74
+ const unlistedOpt = opts.find((o) => o.long === "--unlisted");
75
+ expect(unlistedOpt).toBeDefined();
76
+ });
77
+ });
78
+
79
+ describe("visibility determination", () => {
80
+ type ToolVisibility = "public" | "private" | "unlisted";
81
+
82
+ // Mirrors the logic in publish command
83
+ const determineVisibility = (options: {
84
+ public?: boolean;
85
+ unlisted?: boolean;
86
+ }): ToolVisibility => {
87
+ if (options.public) return "public";
88
+ if (options.unlisted) return "unlisted";
89
+ return "private"; // Default is private
90
+ };
91
+
92
+ test("defaults to private visibility", () => {
93
+ expect(determineVisibility({})).toBe("private");
94
+ });
95
+
96
+ test("--public flag sets public visibility", () => {
97
+ expect(determineVisibility({ public: true })).toBe("public");
98
+ });
99
+
100
+ test("--unlisted flag sets unlisted visibility", () => {
101
+ expect(determineVisibility({ unlisted: true })).toBe("unlisted");
102
+ });
103
+
104
+ test("--public takes precedence over --unlisted", () => {
105
+ expect(determineVisibility({ public: true, unlisted: true })).toBe("public");
106
+ });
57
107
  });
58
108
 
59
109
  describe("manifest file detection", () => {