@enactprotocol/cli 1.2.8 → 2.0.0

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 (73) hide show
  1. package/README.md +88 -0
  2. package/package.json +34 -38
  3. package/src/commands/auth/index.ts +940 -0
  4. package/src/commands/cache/index.ts +361 -0
  5. package/src/commands/config/README.md +239 -0
  6. package/src/commands/config/index.ts +164 -0
  7. package/src/commands/env/README.md +197 -0
  8. package/src/commands/env/index.ts +392 -0
  9. package/src/commands/exec/README.md +110 -0
  10. package/src/commands/exec/index.ts +195 -0
  11. package/src/commands/get/index.ts +198 -0
  12. package/src/commands/index.ts +30 -0
  13. package/src/commands/inspect/index.ts +264 -0
  14. package/src/commands/install/README.md +146 -0
  15. package/src/commands/install/index.ts +682 -0
  16. package/src/commands/list/README.md +115 -0
  17. package/src/commands/list/index.ts +138 -0
  18. package/src/commands/publish/index.ts +350 -0
  19. package/src/commands/report/index.ts +366 -0
  20. package/src/commands/run/README.md +124 -0
  21. package/src/commands/run/index.ts +686 -0
  22. package/src/commands/search/index.ts +368 -0
  23. package/src/commands/setup/index.ts +274 -0
  24. package/src/commands/sign/index.ts +652 -0
  25. package/src/commands/trust/README.md +214 -0
  26. package/src/commands/trust/index.ts +453 -0
  27. package/src/commands/unyank/index.ts +107 -0
  28. package/src/commands/yank/index.ts +143 -0
  29. package/src/index.ts +96 -0
  30. package/src/types.ts +81 -0
  31. package/src/utils/errors.ts +409 -0
  32. package/src/utils/exit-codes.ts +159 -0
  33. package/src/utils/ignore.ts +147 -0
  34. package/src/utils/index.ts +107 -0
  35. package/src/utils/output.ts +242 -0
  36. package/src/utils/spinner.ts +214 -0
  37. package/tests/commands/auth.test.ts +217 -0
  38. package/tests/commands/cache.test.ts +286 -0
  39. package/tests/commands/config.test.ts +277 -0
  40. package/tests/commands/env.test.ts +293 -0
  41. package/tests/commands/exec.test.ts +112 -0
  42. package/tests/commands/get.test.ts +179 -0
  43. package/tests/commands/inspect.test.ts +201 -0
  44. package/tests/commands/install-integration.test.ts +343 -0
  45. package/tests/commands/install.test.ts +288 -0
  46. package/tests/commands/list.test.ts +160 -0
  47. package/tests/commands/publish.test.ts +186 -0
  48. package/tests/commands/report.test.ts +194 -0
  49. package/tests/commands/run.test.ts +231 -0
  50. package/tests/commands/search.test.ts +131 -0
  51. package/tests/commands/sign.test.ts +164 -0
  52. package/tests/commands/trust.test.ts +236 -0
  53. package/tests/commands/unyank.test.ts +114 -0
  54. package/tests/commands/yank.test.ts +154 -0
  55. package/tests/e2e.test.ts +554 -0
  56. package/tests/fixtures/calculator/enact.yaml +34 -0
  57. package/tests/fixtures/echo-tool/enact.md +31 -0
  58. package/tests/fixtures/env-tool/enact.yaml +19 -0
  59. package/tests/fixtures/greeter/enact.yaml +18 -0
  60. package/tests/fixtures/invalid-tool/enact.yaml +4 -0
  61. package/tests/index.test.ts +8 -0
  62. package/tests/types.test.ts +84 -0
  63. package/tests/utils/errors.test.ts +303 -0
  64. package/tests/utils/exit-codes.test.ts +189 -0
  65. package/tests/utils/ignore.test.ts +461 -0
  66. package/tests/utils/output.test.ts +126 -0
  67. package/tsconfig.json +17 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/dist/index.js +0 -231410
  70. package/dist/index.js.bak +0 -231409
  71. package/dist/web/static/app.js +0 -663
  72. package/dist/web/static/index.html +0 -117
  73. package/dist/web/static/style.css +0 -291
@@ -0,0 +1,198 @@
1
+ /**
2
+ * enact get command
3
+ *
4
+ * Show detailed information about a tool from the registry.
5
+ */
6
+
7
+ import {
8
+ type ToolInfo,
9
+ type ToolVersionInfo,
10
+ createApiClient,
11
+ getToolInfo,
12
+ getToolVersion,
13
+ } from "@enactprotocol/api";
14
+ import { loadConfig } from "@enactprotocol/shared";
15
+ import type { Command } from "commander";
16
+ import type { CommandContext, GlobalOptions } from "../../types";
17
+ import {
18
+ dim,
19
+ error,
20
+ formatError,
21
+ header,
22
+ info,
23
+ json,
24
+ keyValue,
25
+ newline,
26
+ success,
27
+ } from "../../utils";
28
+
29
+ interface GetOptions extends GlobalOptions {
30
+ version?: string;
31
+ }
32
+
33
+ /**
34
+ * Format a date for display
35
+ */
36
+ function formatDate(date: Date): string {
37
+ return date.toLocaleDateString("en-US", {
38
+ year: "numeric",
39
+ month: "short",
40
+ day: "numeric",
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Display tool info
46
+ */
47
+ function displayToolInfo(tool: ToolInfo, options: GetOptions): void {
48
+ header(tool.name);
49
+ newline();
50
+
51
+ info(tool.description);
52
+ newline();
53
+
54
+ keyValue("Latest Version", tool.latestVersion);
55
+ keyValue("License", tool.license);
56
+ keyValue("Created", formatDate(tool.createdAt));
57
+ keyValue("Updated", formatDate(tool.updatedAt));
58
+
59
+ if (tool.tags.length > 0) {
60
+ keyValue("Tags", tool.tags.join(", "));
61
+ }
62
+
63
+ if (tool.author) {
64
+ keyValue("Author", tool.author.username);
65
+ }
66
+
67
+ newline();
68
+ keyValue("Available Versions", tool.versions.join(", "));
69
+
70
+ if (options.verbose) {
71
+ newline();
72
+ dim("Use --version <ver> to see version-specific details");
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Display version-specific info
78
+ */
79
+ function displayVersionInfo(version: ToolVersionInfo): void {
80
+ header(`${version.name}@${version.version}`);
81
+ newline();
82
+
83
+ info(version.description);
84
+ newline();
85
+
86
+ keyValue("Version", version.version);
87
+ keyValue("License", version.license);
88
+ if (version.bundle) {
89
+ keyValue("Bundle Hash", version.bundle.hash);
90
+ keyValue("Bundle Size", `${(version.bundle.size / 1024).toFixed(1)} KB`);
91
+ }
92
+
93
+ if (version.yanked) {
94
+ newline();
95
+ dim(`⚠ This version is yanked${version.yankReason ? `: ${version.yankReason}` : ""}`);
96
+ if (version.yankReplacement) {
97
+ dim(` Recommended: ${version.yankReplacement}`);
98
+ }
99
+ }
100
+
101
+ if (version.manifest) {
102
+ newline();
103
+ dim("Manifest:");
104
+ console.log(JSON.stringify(version.manifest, null, 2));
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get command handler
110
+ */
111
+ async function getHandler(
112
+ toolName: string,
113
+ options: GetOptions,
114
+ ctx: CommandContext
115
+ ): Promise<void> {
116
+ const config = loadConfig();
117
+ const registryUrl =
118
+ process.env.ENACT_REGISTRY_URL ??
119
+ config.registry?.url ??
120
+ "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
121
+ const authToken = config.registry?.authToken;
122
+ const client = createApiClient({
123
+ baseUrl: registryUrl,
124
+ authToken: authToken,
125
+ });
126
+
127
+ if (ctx.options.verbose) {
128
+ info(`Fetching info for: ${toolName}`);
129
+ }
130
+
131
+ try {
132
+ if (options.version) {
133
+ // Get specific version info
134
+ const versionInfo = await getToolVersion(client, toolName, options.version);
135
+
136
+ if (options.json) {
137
+ json(versionInfo);
138
+ return;
139
+ }
140
+
141
+ displayVersionInfo(versionInfo);
142
+ } else {
143
+ // Get general tool info
144
+ const toolInfo = await getToolInfo(client, toolName);
145
+
146
+ if (options.json) {
147
+ json(toolInfo);
148
+ return;
149
+ }
150
+
151
+ displayToolInfo(toolInfo, options);
152
+ }
153
+
154
+ newline();
155
+ success(`Install with: enact install ${toolName}`);
156
+ } catch (err) {
157
+ if (err instanceof Error) {
158
+ if (err.message.includes("not_found") || err.message.includes("404")) {
159
+ error(`Tool not found: ${toolName}`);
160
+ dim("Check the tool name or search with: enact search <query>");
161
+ process.exit(1);
162
+ }
163
+ if (err.message.includes("fetch")) {
164
+ error("Unable to connect to registry. Check your internet connection.");
165
+ process.exit(1);
166
+ }
167
+ }
168
+ throw err;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Configure the get command
174
+ */
175
+ export function configureGetCommand(program: Command): void {
176
+ program
177
+ .command("get <tool>")
178
+ .alias("info")
179
+ .description("Show detailed information about a tool")
180
+ .option("-V, --version <version>", "Show info for a specific version")
181
+ .option("-v, --verbose", "Show detailed output")
182
+ .option("--json", "Output as JSON")
183
+ .action(async (toolName: string, options: GetOptions) => {
184
+ const ctx: CommandContext = {
185
+ cwd: process.cwd(),
186
+ options,
187
+ isCI: Boolean(process.env.CI),
188
+ isInteractive: process.stdout.isTTY ?? false,
189
+ };
190
+
191
+ try {
192
+ await getHandler(toolName, options, ctx);
193
+ } catch (err) {
194
+ error(formatError(err));
195
+ process.exit(1);
196
+ }
197
+ });
198
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * CLI Commands Index
3
+ *
4
+ * Exports all command configuration functions.
5
+ */
6
+
7
+ export { configureSetupCommand } from "./setup";
8
+ export { configureRunCommand } from "./run";
9
+ export { configureExecCommand } from "./exec";
10
+ export { configureInstallCommand } from "./install";
11
+ export { configureListCommand } from "./list";
12
+ export { configureEnvCommand } from "./env";
13
+ export { configureTrustCommand } from "./trust";
14
+ export { configureConfigCommand } from "./config";
15
+
16
+ // Registry commands (Phase 8)
17
+ export { configureSearchCommand } from "./search";
18
+ export { configureGetCommand } from "./get";
19
+ export { configurePublishCommand } from "./publish";
20
+ export { configureAuthCommand } from "./auth";
21
+ export { configureCacheCommand } from "./cache";
22
+
23
+ // CLI solidification commands (Phase 9)
24
+ export { configureSignCommand } from "./sign";
25
+ export { configureReportCommand } from "./report";
26
+ export { configureInspectCommand } from "./inspect";
27
+
28
+ // API v2 migration commands
29
+ export { configureYankCommand } from "./yank";
30
+ export { configureUnyankCommand } from "./unyank";
@@ -0,0 +1,264 @@
1
+ /**
2
+ * enact inspect command
3
+ *
4
+ * Open a tool's page in the browser for inspection/review.
5
+ * Use --download to download locally for deeper code review.
6
+ * This allows reviewers to examine tool code before trusting it.
7
+ */
8
+
9
+ import { mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { createApiClient, downloadBundle, getToolInfo } from "@enactprotocol/api";
12
+ import { getCacheDir, loadConfig, pathExists } from "@enactprotocol/shared";
13
+ import type { Command } from "commander";
14
+ import type { CommandContext, GlobalOptions } from "../../types";
15
+ import {
16
+ EXIT_FAILURE,
17
+ RegistryError,
18
+ colors,
19
+ confirm,
20
+ dim,
21
+ error,
22
+ formatError,
23
+ handleError,
24
+ info,
25
+ json,
26
+ keyValue,
27
+ newline,
28
+ success,
29
+ symbols,
30
+ withSpinner,
31
+ } from "../../utils";
32
+
33
+ interface InspectOptions extends GlobalOptions {
34
+ output?: string;
35
+ force?: boolean;
36
+ download?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Parse tool@version syntax
41
+ */
42
+ function parseToolSpec(spec: string): { name: string; version: string | undefined } {
43
+ const match = spec.match(/^(@[^@/]+\/[^@]+|[^@]+)(?:@(.+))?$/);
44
+ if (match?.[1]) {
45
+ return {
46
+ name: match[1],
47
+ version: match[2],
48
+ };
49
+ }
50
+ return { name: spec, version: undefined };
51
+ }
52
+
53
+ /**
54
+ * Format bytes to human-readable string
55
+ */
56
+ function formatBytes(bytes: number): string {
57
+ if (bytes === 0) return "0 B";
58
+ const k = 1024;
59
+ const sizes = ["B", "KB", "MB", "GB"];
60
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
61
+ return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
62
+ }
63
+
64
+ /**
65
+ * Extract a tar.gz bundle to a directory
66
+ */
67
+ async function extractBundle(bundleData: ArrayBuffer, destPath: string): Promise<void> {
68
+ const tempFile = join(getCacheDir(), `inspect-${Date.now()}.tar.gz`);
69
+ mkdirSync(dirname(tempFile), { recursive: true });
70
+ writeFileSync(tempFile, Buffer.from(bundleData));
71
+
72
+ mkdirSync(destPath, { recursive: true });
73
+
74
+ const proc = Bun.spawn(["tar", "-xzf", tempFile, "-C", destPath], {
75
+ stdout: "pipe",
76
+ stderr: "pipe",
77
+ });
78
+
79
+ const exitCode = await proc.exited;
80
+
81
+ try {
82
+ unlinkSync(tempFile);
83
+ } catch {
84
+ // Ignore cleanup errors
85
+ }
86
+
87
+ if (exitCode !== 0) {
88
+ const stderr = await new Response(proc.stderr).text();
89
+ throw new Error(`Failed to extract bundle: ${stderr}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Inspect command handler
95
+ */
96
+ async function inspectHandler(
97
+ toolSpec: string,
98
+ options: InspectOptions,
99
+ ctx: CommandContext
100
+ ): Promise<void> {
101
+ const { name: toolName, version } = parseToolSpec(toolSpec);
102
+
103
+ const config = loadConfig();
104
+ const registryUrl = config.registry?.url ?? "https://registry.enact.tools";
105
+
106
+ // Default behavior: open in browser
107
+ // Use --download to download locally instead
108
+ if (!options.download) {
109
+ const toolUrl = version
110
+ ? `${registryUrl}/tools/${toolName}/v/${version}`
111
+ : `${registryUrl}/tools/${toolName}`;
112
+
113
+ info(`Opening ${toolName} in browser...`);
114
+
115
+ // Open URL in default browser
116
+ const openCmd =
117
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
118
+
119
+ const proc = Bun.spawn([openCmd, toolUrl], {
120
+ stdout: "ignore",
121
+ stderr: "ignore",
122
+ });
123
+ await proc.exited;
124
+
125
+ if (options.json) {
126
+ json({ opened: true, url: toolUrl });
127
+ } else {
128
+ success(`Opened: ${toolUrl}`);
129
+ }
130
+ return;
131
+ }
132
+
133
+ const client = createApiClient({
134
+ baseUrl: registryUrl,
135
+ authToken: config.registry?.authToken,
136
+ });
137
+
138
+ // Determine target version
139
+ let targetVersion = version;
140
+
141
+ try {
142
+ if (!targetVersion) {
143
+ const metadata = await withSpinner(
144
+ `Fetching ${toolName} info...`,
145
+ async () => getToolInfo(client, toolName),
146
+ `${symbols.success} Found ${toolName}`
147
+ );
148
+ targetVersion = metadata.latestVersion;
149
+ }
150
+ } catch (err) {
151
+ throw new RegistryError(`Failed to fetch tool info: ${formatError(err)}`);
152
+ }
153
+
154
+ // Determine output directory
155
+ const outputDir = options.output
156
+ ? resolve(ctx.cwd, options.output)
157
+ : resolve(ctx.cwd, toolName.split("/").pop() ?? toolName);
158
+
159
+ // Check if output already exists
160
+ if (pathExists(outputDir) && !options.force) {
161
+ if (ctx.isInteractive) {
162
+ const shouldOverwrite = await confirm(`Directory ${outputDir} already exists. Overwrite?`);
163
+ if (!shouldOverwrite) {
164
+ info("Inspection cancelled.");
165
+ return;
166
+ }
167
+ rmSync(outputDir, { recursive: true, force: true });
168
+ } else {
169
+ error(`Directory ${outputDir} already exists. Use --force to overwrite.`);
170
+ process.exit(EXIT_FAILURE);
171
+ }
172
+ }
173
+
174
+ // Download bundle (no trust verification - this is for inspection)
175
+ let bundleResult: { data: ArrayBuffer; hash: string; size: number };
176
+ try {
177
+ bundleResult = await withSpinner(
178
+ `Downloading ${toolName}@${targetVersion}...`,
179
+ async () =>
180
+ downloadBundle(client, {
181
+ name: toolName,
182
+ version: targetVersion!,
183
+ verify: true,
184
+ }),
185
+ `${symbols.success} Downloaded`
186
+ );
187
+ } catch (err) {
188
+ throw new RegistryError(`Failed to download bundle: ${formatError(err)}`);
189
+ }
190
+
191
+ // Extract to output directory
192
+ try {
193
+ await withSpinner(
194
+ "Extracting...",
195
+ async () => extractBundle(bundleResult.data, outputDir),
196
+ `${symbols.success} Extracted`
197
+ );
198
+ } catch (err) {
199
+ throw new RegistryError(`Failed to extract bundle: ${formatError(err)}`);
200
+ }
201
+
202
+ // Output result
203
+ if (options.json) {
204
+ json({
205
+ inspected: true,
206
+ tool: toolName,
207
+ version: targetVersion,
208
+ location: outputDir,
209
+ hash: bundleResult.hash,
210
+ size: bundleResult.size,
211
+ });
212
+ return;
213
+ }
214
+
215
+ newline();
216
+ keyValue("Tool", toolName);
217
+ keyValue("Version", targetVersion ?? "unknown");
218
+ keyValue("Location", outputDir);
219
+ keyValue("Size", formatBytes(bundleResult.size));
220
+ keyValue("Hash", `${bundleResult.hash.substring(0, 20)}...`);
221
+ newline();
222
+
223
+ success(`Downloaded ${colors.bold(toolName)}@${targetVersion} for inspection`);
224
+ newline();
225
+
226
+ info("Next steps:");
227
+ dim(` cd ${outputDir}`);
228
+ dim(" # Review the code, run security scans, test it");
229
+ dim(" # If it passes review, sign it:");
230
+ dim(" enact sign .");
231
+ dim(" # Or report issues:");
232
+ dim(` enact report ${toolName}@${targetVersion} --reason "description"`);
233
+ }
234
+
235
+ /**
236
+ * Configure the inspect command
237
+ */
238
+ export function configureInspectCommand(program: Command): void {
239
+ program
240
+ .command("inspect <tool[@version]>")
241
+ .description("Open a tool's page in browser for inspection (use --download to save locally)")
242
+ .option("-d, --download", "Download tool locally instead of opening in browser")
243
+ .option(
244
+ "-o, --output <path>",
245
+ "Output directory for download (default: tool name in current directory)"
246
+ )
247
+ .option("-f, --force", "Overwrite existing directory when downloading")
248
+ .option("-v, --verbose", "Show detailed output")
249
+ .option("--json", "Output result as JSON")
250
+ .action(async (toolSpec: string, options: InspectOptions) => {
251
+ const ctx: CommandContext = {
252
+ cwd: process.cwd(),
253
+ options,
254
+ isCI: Boolean(process.env.CI),
255
+ isInteractive: process.stdout.isTTY ?? false,
256
+ };
257
+
258
+ try {
259
+ await inspectHandler(toolSpec, options, ctx);
260
+ } catch (err) {
261
+ handleError(err, options.verbose ? { verbose: true } : undefined);
262
+ }
263
+ });
264
+ }
@@ -0,0 +1,146 @@
1
+ # enact install
2
+
3
+ Install a tool to the project or user directory.
4
+
5
+ ## Synopsis
6
+
7
+ ```bash
8
+ enact install [tool] [options]
9
+ ```
10
+
11
+ ## Description
12
+
13
+ The `install` command copies a tool to either the project's `.enact/tools/` directory (default) or the user's `~/.enact/tools/` directory (with `--global`). This makes the tool available for execution without specifying the full path.
14
+
15
+ ## Arguments
16
+
17
+ | Argument | Description |
18
+ |----------|-------------|
19
+ | `[tool]` | Tool to install. Can be a tool name, path, `.` for current directory, or omitted to install from `tools.json` |
20
+
21
+ ## Options
22
+
23
+ | Option | Description |
24
+ |--------|-------------|
25
+ | `-g, --global` | Install to user directory (`~/.enact/tools/`) instead of project |
26
+ | `-f, --force` | Overwrite existing installation without prompting |
27
+ | `-v, --verbose` | Show detailed output |
28
+ | `--json` | Output result as JSON |
29
+
30
+ ## Installation Scopes
31
+
32
+ ### Project Scope (Default)
33
+
34
+ ```bash
35
+ enact install alice/utils/greeter
36
+ ```
37
+
38
+ Installs to `.enact/tools/alice/utils/greeter/` in your project. This is ideal for:
39
+ - Tools specific to a project
40
+ - Team collaboration (commit `.enact/tools.json`)
41
+ - Reproducible builds
42
+
43
+ ### Global Scope
44
+
45
+ ```bash
46
+ enact install alice/utils/greeter --global
47
+ ```
48
+
49
+ Installs to `~/.enact/tools/alice/utils/greeter/`. This is ideal for:
50
+ - Tools you use across multiple projects
51
+ - Personal utility tools
52
+ - System-wide availability
53
+
54
+ ## Examples
55
+
56
+ ### Install from registry (future)
57
+
58
+ ```bash
59
+ # Install to project
60
+ enact install alice/utils/greeter
61
+
62
+ # Install globally
63
+ enact install alice/utils/greeter --global
64
+ ```
65
+
66
+ ### Install from local path
67
+
68
+ ```bash
69
+ # Install current directory as a tool
70
+ enact install .
71
+
72
+ # Install from a specific path
73
+ enact install ./my-tools/greeter
74
+
75
+ # Install with absolute path
76
+ enact install /path/to/tool
77
+ ```
78
+
79
+ ### Install all project tools
80
+
81
+ ```bash
82
+ # Install all tools defined in .enact/tools.json
83
+ enact install
84
+ ```
85
+
86
+ ### Force reinstall
87
+
88
+ ```bash
89
+ # Overwrite existing installation
90
+ enact install alice/utils/greeter --force
91
+ ```
92
+
93
+ ## Tool Resolution
94
+
95
+ When installing by name, the command searches for the tool in this order:
96
+
97
+ 1. Project tools (`.enact/tools/`)
98
+ 2. User tools (`~/.enact/tools/`)
99
+ 3. Cache (`~/.enact/cache/`)
100
+ 4. Registry (future)
101
+
102
+ ## Directory Structure
103
+
104
+ After installation, tools are organized by their namespaced name:
105
+
106
+ ```
107
+ .enact/tools/
108
+ ├── alice/
109
+ │ └── utils/
110
+ │ └── greeter/
111
+ │ ├── enact.md
112
+ │ └── ...
113
+ └── EnactProtocol/
114
+ └── pdf-extract/
115
+ ├── enact.yaml
116
+ └── ...
117
+ ```
118
+
119
+ ## tools.json
120
+
121
+ The `.enact/tools.json` file tracks project dependencies:
122
+
123
+ ```json
124
+ {
125
+ "tools": {
126
+ "alice/utils/greeter": "^1.0.0",
127
+ "EnactProtocol/pdf-extract": "2.1.0"
128
+ }
129
+ }
130
+ ```
131
+
132
+ Running `enact install` without arguments installs all tools from this file.
133
+
134
+ ## Exit Codes
135
+
136
+ | Code | Description |
137
+ |------|-------------|
138
+ | `0` | Successful installation |
139
+ | `1` | Installation failed |
140
+ | `3` | Tool not found |
141
+
142
+ ## See Also
143
+
144
+ - [enact list](../list/README.md) - List installed tools
145
+ - [enact run](../run/README.md) - Execute tools
146
+ - [enact search](../search/README.md) - Search for tools (future)