@enactprotocol/cli 1.2.13 → 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.
- package/README.md +88 -0
- package/package.json +34 -38
- package/src/commands/auth/index.ts +940 -0
- package/src/commands/cache/index.ts +361 -0
- package/src/commands/config/README.md +239 -0
- package/src/commands/config/index.ts +164 -0
- package/src/commands/env/README.md +197 -0
- package/src/commands/env/index.ts +392 -0
- package/src/commands/exec/README.md +110 -0
- package/src/commands/exec/index.ts +195 -0
- package/src/commands/get/index.ts +198 -0
- package/src/commands/index.ts +30 -0
- package/src/commands/inspect/index.ts +264 -0
- package/src/commands/install/README.md +146 -0
- package/src/commands/install/index.ts +682 -0
- package/src/commands/list/README.md +115 -0
- package/src/commands/list/index.ts +138 -0
- package/src/commands/publish/index.ts +350 -0
- package/src/commands/report/index.ts +366 -0
- package/src/commands/run/README.md +124 -0
- package/src/commands/run/index.ts +686 -0
- package/src/commands/search/index.ts +368 -0
- package/src/commands/setup/index.ts +274 -0
- package/src/commands/sign/index.ts +652 -0
- package/src/commands/trust/README.md +214 -0
- package/src/commands/trust/index.ts +453 -0
- package/src/commands/unyank/index.ts +107 -0
- package/src/commands/yank/index.ts +143 -0
- package/src/index.ts +96 -0
- package/src/types.ts +81 -0
- package/src/utils/errors.ts +409 -0
- package/src/utils/exit-codes.ts +159 -0
- package/src/utils/ignore.ts +147 -0
- package/src/utils/index.ts +107 -0
- package/src/utils/output.ts +242 -0
- package/src/utils/spinner.ts +214 -0
- package/tests/commands/auth.test.ts +217 -0
- package/tests/commands/cache.test.ts +286 -0
- package/tests/commands/config.test.ts +277 -0
- package/tests/commands/env.test.ts +293 -0
- package/tests/commands/exec.test.ts +112 -0
- package/tests/commands/get.test.ts +179 -0
- package/tests/commands/inspect.test.ts +201 -0
- package/tests/commands/install-integration.test.ts +343 -0
- package/tests/commands/install.test.ts +288 -0
- package/tests/commands/list.test.ts +160 -0
- package/tests/commands/publish.test.ts +186 -0
- package/tests/commands/report.test.ts +194 -0
- package/tests/commands/run.test.ts +231 -0
- package/tests/commands/search.test.ts +131 -0
- package/tests/commands/sign.test.ts +164 -0
- package/tests/commands/trust.test.ts +236 -0
- package/tests/commands/unyank.test.ts +114 -0
- package/tests/commands/yank.test.ts +154 -0
- package/tests/e2e.test.ts +554 -0
- package/tests/fixtures/calculator/enact.yaml +34 -0
- package/tests/fixtures/echo-tool/enact.md +31 -0
- package/tests/fixtures/env-tool/enact.yaml +19 -0
- package/tests/fixtures/greeter/enact.yaml +18 -0
- package/tests/fixtures/invalid-tool/enact.yaml +4 -0
- package/tests/index.test.ts +8 -0
- package/tests/types.test.ts +84 -0
- package/tests/utils/errors.test.ts +303 -0
- package/tests/utils/exit-codes.test.ts +189 -0
- package/tests/utils/ignore.test.ts +461 -0
- package/tests/utils/output.test.ts +126 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/index.js +0 -231612
- package/dist/index.js.bak +0 -231611
- package/dist/web/static/app.js +0 -663
- package/dist/web/static/index.html +0 -117
- package/dist/web/static/style.css +0 -291
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for install command
|
|
3
|
+
*
|
|
4
|
+
* Tests actual installation flows using local tools.
|
|
5
|
+
* These tests create real files and directories to verify the full installation process.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
// Test fixtures
|
|
13
|
+
const TEST_BASE = join(import.meta.dir, "..", "fixtures", "install-integration");
|
|
14
|
+
const TEST_PROJECT = join(TEST_BASE, "test-project");
|
|
15
|
+
const TEST_TOOL_SRC = join(TEST_BASE, "source-tool");
|
|
16
|
+
const TEST_GLOBAL_HOME = join(TEST_BASE, "fake-home");
|
|
17
|
+
|
|
18
|
+
// Create a sample tool manifest for testing
|
|
19
|
+
const SAMPLE_MANIFEST = `
|
|
20
|
+
name: test/sample-tool
|
|
21
|
+
version: 1.0.0
|
|
22
|
+
description: A sample tool for testing installation
|
|
23
|
+
command: echo "Hello from sample-tool!"
|
|
24
|
+
tags:
|
|
25
|
+
- test
|
|
26
|
+
- sample
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const SAMPLE_MANIFEST_V2 = `
|
|
30
|
+
name: test/sample-tool
|
|
31
|
+
version: 2.0.0
|
|
32
|
+
description: A sample tool for testing installation (v2)
|
|
33
|
+
command: echo "Hello from sample-tool v2!"
|
|
34
|
+
tags:
|
|
35
|
+
- test
|
|
36
|
+
- sample
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
describe("install integration", () => {
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
// Create test directories
|
|
42
|
+
mkdirSync(TEST_BASE, { recursive: true });
|
|
43
|
+
mkdirSync(TEST_PROJECT, { recursive: true });
|
|
44
|
+
mkdirSync(join(TEST_PROJECT, ".enact"), { recursive: true });
|
|
45
|
+
mkdirSync(TEST_TOOL_SRC, { recursive: true });
|
|
46
|
+
mkdirSync(join(TEST_GLOBAL_HOME, ".enact", "cache"), { recursive: true });
|
|
47
|
+
|
|
48
|
+
// Create sample tool source
|
|
49
|
+
writeFileSync(join(TEST_TOOL_SRC, "enact.yaml"), SAMPLE_MANIFEST);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
// Clean project tools before each test
|
|
54
|
+
const projectToolsDir = join(TEST_PROJECT, ".enact", "tools");
|
|
55
|
+
if (existsSync(projectToolsDir)) {
|
|
56
|
+
rmSync(projectToolsDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Clean fake global tools.json
|
|
60
|
+
const globalToolsJson = join(TEST_GLOBAL_HOME, ".enact", "tools.json");
|
|
61
|
+
if (existsSync(globalToolsJson)) {
|
|
62
|
+
rmSync(globalToolsJson);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterAll(() => {
|
|
67
|
+
// Clean up test directories
|
|
68
|
+
if (existsSync(TEST_BASE)) {
|
|
69
|
+
rmSync(TEST_BASE, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("local path installation", () => {
|
|
74
|
+
test("installs tool from local path to project .enact/tools/", async () => {
|
|
75
|
+
// This test simulates what happens when you run `enact install ./source-tool`
|
|
76
|
+
|
|
77
|
+
// Import the shared functions we need
|
|
78
|
+
const { loadManifestFromDir, toolNameToPath } = await import("@enactprotocol/shared");
|
|
79
|
+
|
|
80
|
+
// Verify source tool exists
|
|
81
|
+
const loaded = loadManifestFromDir(TEST_TOOL_SRC);
|
|
82
|
+
expect(loaded).not.toBeNull();
|
|
83
|
+
expect(loaded?.manifest.name).toBe("test/sample-tool");
|
|
84
|
+
|
|
85
|
+
// Simulate the copy operation the install command performs
|
|
86
|
+
const destPath = join(TEST_PROJECT, ".enact", "tools", toolNameToPath("test/sample-tool"));
|
|
87
|
+
mkdirSync(destPath, { recursive: true });
|
|
88
|
+
|
|
89
|
+
// Copy the manifest
|
|
90
|
+
const { cpSync } = await import("node:fs");
|
|
91
|
+
cpSync(TEST_TOOL_SRC, destPath, { recursive: true });
|
|
92
|
+
|
|
93
|
+
// Verify installation
|
|
94
|
+
expect(existsSync(destPath)).toBe(true);
|
|
95
|
+
expect(existsSync(join(destPath, "enact.yaml"))).toBe(true);
|
|
96
|
+
|
|
97
|
+
// Verify manifest can be loaded from destination
|
|
98
|
+
const installedManifest = loadManifestFromDir(destPath);
|
|
99
|
+
expect(installedManifest).not.toBeNull();
|
|
100
|
+
expect(installedManifest?.manifest.name).toBe("test/sample-tool");
|
|
101
|
+
expect(installedManifest?.manifest.version).toBe("1.0.0");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("overwrites existing project tool when force is used", async () => {
|
|
105
|
+
const { loadManifestFromDir, toolNameToPath } = await import("@enactprotocol/shared");
|
|
106
|
+
const { cpSync } = await import("node:fs");
|
|
107
|
+
|
|
108
|
+
const destPath = join(TEST_PROJECT, ".enact", "tools", toolNameToPath("test/sample-tool"));
|
|
109
|
+
|
|
110
|
+
// First installation
|
|
111
|
+
mkdirSync(destPath, { recursive: true });
|
|
112
|
+
cpSync(TEST_TOOL_SRC, destPath, { recursive: true });
|
|
113
|
+
|
|
114
|
+
// Verify v1 is installed
|
|
115
|
+
let installed = loadManifestFromDir(destPath);
|
|
116
|
+
expect(installed?.manifest.version).toBe("1.0.0");
|
|
117
|
+
|
|
118
|
+
// Create v2 source
|
|
119
|
+
const v2Source = join(TEST_BASE, "source-tool-v2");
|
|
120
|
+
mkdirSync(v2Source, { recursive: true });
|
|
121
|
+
writeFileSync(join(v2Source, "enact.yaml"), SAMPLE_MANIFEST_V2);
|
|
122
|
+
|
|
123
|
+
// Simulate force overwrite
|
|
124
|
+
rmSync(destPath, { recursive: true, force: true });
|
|
125
|
+
mkdirSync(destPath, { recursive: true });
|
|
126
|
+
cpSync(v2Source, destPath, { recursive: true });
|
|
127
|
+
|
|
128
|
+
// Verify v2 is now installed
|
|
129
|
+
installed = loadManifestFromDir(destPath);
|
|
130
|
+
expect(installed?.manifest.version).toBe("2.0.0");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("global installation via tools.json", () => {
|
|
135
|
+
test("global install updates tools.json registry", async () => {
|
|
136
|
+
const { addToolToRegistry, getInstalledVersion, isToolInstalled, loadToolsRegistry } =
|
|
137
|
+
await import("@enactprotocol/shared");
|
|
138
|
+
|
|
139
|
+
// Simulate global installation by adding to registry
|
|
140
|
+
// Note: In real usage, this writes to ~/.enact/tools.json
|
|
141
|
+
// For testing, we verify the registry functions work correctly
|
|
142
|
+
|
|
143
|
+
// Add tool to registry (simulating what install command does)
|
|
144
|
+
addToolToRegistry("test/sample-tool", "1.0.0", "project", TEST_PROJECT);
|
|
145
|
+
|
|
146
|
+
// Verify it's registered
|
|
147
|
+
expect(isToolInstalled("test/sample-tool", "project", TEST_PROJECT)).toBe(true);
|
|
148
|
+
expect(getInstalledVersion("test/sample-tool", "project", TEST_PROJECT)).toBe("1.0.0");
|
|
149
|
+
|
|
150
|
+
// Load and verify registry directly
|
|
151
|
+
const registry = loadToolsRegistry("project", TEST_PROJECT);
|
|
152
|
+
expect(registry.tools["test/sample-tool"]).toBe("1.0.0");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("global install updates version when reinstalling", async () => {
|
|
156
|
+
const { addToolToRegistry, getInstalledVersion } = await import("@enactprotocol/shared");
|
|
157
|
+
|
|
158
|
+
// Install v1
|
|
159
|
+
addToolToRegistry("test/sample-tool", "1.0.0", "project", TEST_PROJECT);
|
|
160
|
+
expect(getInstalledVersion("test/sample-tool", "project", TEST_PROJECT)).toBe("1.0.0");
|
|
161
|
+
|
|
162
|
+
// "Upgrade" to v2
|
|
163
|
+
addToolToRegistry("test/sample-tool", "2.0.0", "project", TEST_PROJECT);
|
|
164
|
+
expect(getInstalledVersion("test/sample-tool", "project", TEST_PROJECT)).toBe("2.0.0");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("global install extracts to cache path", async () => {
|
|
168
|
+
const { getToolCachePath } = await import("@enactprotocol/shared");
|
|
169
|
+
|
|
170
|
+
// Verify cache path structure
|
|
171
|
+
const cachePath = getToolCachePath("test/sample-tool", "1.0.0");
|
|
172
|
+
expect(cachePath).toContain(".enact");
|
|
173
|
+
expect(cachePath).toContain("cache");
|
|
174
|
+
expect(cachePath).toContain("test/sample-tool");
|
|
175
|
+
expect(cachePath).toContain("v1.0.0");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("listInstalledTools returns installed tools from registry", async () => {
|
|
179
|
+
const { addToolToRegistry, listInstalledTools, saveToolsRegistry } = await import(
|
|
180
|
+
"@enactprotocol/shared"
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Clear any existing tools first
|
|
184
|
+
saveToolsRegistry({ tools: {} }, "project", TEST_PROJECT);
|
|
185
|
+
|
|
186
|
+
// Add multiple tools
|
|
187
|
+
addToolToRegistry("alice/tool-a", "1.0.0", "project", TEST_PROJECT);
|
|
188
|
+
addToolToRegistry("bob/tool-b", "2.0.0", "project", TEST_PROJECT);
|
|
189
|
+
addToolToRegistry("charlie/tool-c", "3.0.0", "project", TEST_PROJECT);
|
|
190
|
+
|
|
191
|
+
// List all installed
|
|
192
|
+
const tools = listInstalledTools("project", TEST_PROJECT);
|
|
193
|
+
|
|
194
|
+
expect(tools.length).toBe(3);
|
|
195
|
+
expect(tools.find((t) => t.name === "alice/tool-a")).toBeTruthy();
|
|
196
|
+
expect(tools.find((t) => t.name === "bob/tool-b")).toBeTruthy();
|
|
197
|
+
expect(tools.find((t) => t.name === "charlie/tool-c")).toBeTruthy();
|
|
198
|
+
|
|
199
|
+
// Verify versions
|
|
200
|
+
expect(tools.find((t) => t.name === "alice/tool-a")?.version).toBe("1.0.0");
|
|
201
|
+
expect(tools.find((t) => t.name === "bob/tool-b")?.version).toBe("2.0.0");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("registry integration scenarios", () => {
|
|
206
|
+
test("removeToolFromRegistry removes tool from tools.json", async () => {
|
|
207
|
+
const { addToolToRegistry, isToolInstalled, removeToolFromRegistry } = await import(
|
|
208
|
+
"@enactprotocol/shared"
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Add then remove
|
|
212
|
+
addToolToRegistry("test/to-remove", "1.0.0", "project", TEST_PROJECT);
|
|
213
|
+
expect(isToolInstalled("test/to-remove", "project", TEST_PROJECT)).toBe(true);
|
|
214
|
+
|
|
215
|
+
const removed = removeToolFromRegistry("test/to-remove", "project", TEST_PROJECT);
|
|
216
|
+
expect(removed).toBe(true);
|
|
217
|
+
expect(isToolInstalled("test/to-remove", "project", TEST_PROJECT)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("tools.json file format is correct", async () => {
|
|
221
|
+
const { addToolToRegistry, getToolsJsonPath } = await import("@enactprotocol/shared");
|
|
222
|
+
|
|
223
|
+
addToolToRegistry("org/namespace/tool", "1.2.3", "project", TEST_PROJECT);
|
|
224
|
+
|
|
225
|
+
const jsonPath = getToolsJsonPath("project", TEST_PROJECT);
|
|
226
|
+
expect(jsonPath).not.toBeNull();
|
|
227
|
+
|
|
228
|
+
const content = readFileSync(jsonPath!, "utf-8");
|
|
229
|
+
const parsed = JSON.parse(content);
|
|
230
|
+
|
|
231
|
+
// Verify structure
|
|
232
|
+
expect(parsed).toHaveProperty("tools");
|
|
233
|
+
expect(typeof parsed.tools).toBe("object");
|
|
234
|
+
expect(parsed.tools["org/namespace/tool"]).toBe("1.2.3");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("tool resolution", () => {
|
|
239
|
+
test("resolver finds tools from registry", async () => {
|
|
240
|
+
// Setup: Create a cached tool with manifest
|
|
241
|
+
const cachePath = join(TEST_GLOBAL_HOME, ".enact", "cache", "test", "cached-tool", "v1.0.0");
|
|
242
|
+
mkdirSync(cachePath, { recursive: true });
|
|
243
|
+
writeFileSync(
|
|
244
|
+
join(cachePath, "enact.yaml"),
|
|
245
|
+
`
|
|
246
|
+
name: test/cached-tool
|
|
247
|
+
version: 1.0.0
|
|
248
|
+
description: A cached tool
|
|
249
|
+
command: echo "cached"
|
|
250
|
+
`
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// The resolver should be able to find this tool once it's registered
|
|
254
|
+
// Note: Full resolver testing is in resolver.test.ts
|
|
255
|
+
expect(existsSync(join(cachePath, "enact.yaml"))).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("tools.json edge cases", () => {
|
|
261
|
+
const EDGE_TEST_DIR = join(TEST_BASE, "edge-cases");
|
|
262
|
+
|
|
263
|
+
beforeAll(() => {
|
|
264
|
+
mkdirSync(join(EDGE_TEST_DIR, ".enact"), { recursive: true });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
afterAll(() => {
|
|
268
|
+
if (existsSync(EDGE_TEST_DIR)) {
|
|
269
|
+
rmSync(EDGE_TEST_DIR, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
// Ensure .enact directory exists
|
|
275
|
+
mkdirSync(join(EDGE_TEST_DIR, ".enact"), { recursive: true });
|
|
276
|
+
|
|
277
|
+
const jsonPath = join(EDGE_TEST_DIR, ".enact", "tools.json");
|
|
278
|
+
if (existsSync(jsonPath)) {
|
|
279
|
+
rmSync(jsonPath);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("handles tool names with special characters", async () => {
|
|
284
|
+
const { addToolToRegistry, getInstalledVersion } = await import("@enactprotocol/shared");
|
|
285
|
+
|
|
286
|
+
// Various tool name formats
|
|
287
|
+
addToolToRegistry("org/ns/my-tool", "1.0.0", "project", EDGE_TEST_DIR);
|
|
288
|
+
addToolToRegistry("org/ns/my_tool", "2.0.0", "project", EDGE_TEST_DIR);
|
|
289
|
+
addToolToRegistry("org123/ns456/tool789", "3.0.0", "project", EDGE_TEST_DIR);
|
|
290
|
+
|
|
291
|
+
expect(getInstalledVersion("org/ns/my-tool", "project", EDGE_TEST_DIR)).toBe("1.0.0");
|
|
292
|
+
expect(getInstalledVersion("org/ns/my_tool", "project", EDGE_TEST_DIR)).toBe("2.0.0");
|
|
293
|
+
expect(getInstalledVersion("org123/ns456/tool789", "project", EDGE_TEST_DIR)).toBe("3.0.0");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("handles version formats correctly", async () => {
|
|
297
|
+
const { addToolToRegistry, getInstalledVersion } = await import("@enactprotocol/shared");
|
|
298
|
+
|
|
299
|
+
// Various version formats
|
|
300
|
+
addToolToRegistry("test/semver", "1.2.3", "project", EDGE_TEST_DIR);
|
|
301
|
+
addToolToRegistry("test/prerelease", "2.0.0-beta.1", "project", EDGE_TEST_DIR);
|
|
302
|
+
addToolToRegistry("test/build", "3.0.0+build.123", "project", EDGE_TEST_DIR);
|
|
303
|
+
|
|
304
|
+
expect(getInstalledVersion("test/semver", "project", EDGE_TEST_DIR)).toBe("1.2.3");
|
|
305
|
+
expect(getInstalledVersion("test/prerelease", "project", EDGE_TEST_DIR)).toBe("2.0.0-beta.1");
|
|
306
|
+
expect(getInstalledVersion("test/build", "project", EDGE_TEST_DIR)).toBe("3.0.0+build.123");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("handles concurrent writes gracefully", async () => {
|
|
310
|
+
const { addToolToRegistry, loadToolsRegistry } = await import("@enactprotocol/shared");
|
|
311
|
+
|
|
312
|
+
// Simulate rapid concurrent writes
|
|
313
|
+
const promises = [];
|
|
314
|
+
for (let i = 0; i < 10; i++) {
|
|
315
|
+
promises.push(
|
|
316
|
+
Promise.resolve().then(() => {
|
|
317
|
+
addToolToRegistry(`test/concurrent-${i}`, `${i}.0.0`, "project", EDGE_TEST_DIR);
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await Promise.all(promises);
|
|
323
|
+
|
|
324
|
+
// All tools should be registered (though order may vary)
|
|
325
|
+
const registry = loadToolsRegistry("project", EDGE_TEST_DIR);
|
|
326
|
+
const keys = Object.keys(registry.tools);
|
|
327
|
+
|
|
328
|
+
// Due to race conditions, we may not have all 10
|
|
329
|
+
// but we should have at least some and they should be valid
|
|
330
|
+
expect(keys.length).toBeGreaterThan(0);
|
|
331
|
+
for (const key of keys) {
|
|
332
|
+
expect(key).toMatch(/^test\/concurrent-\d$/);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("empty registry returns empty tools object", async () => {
|
|
337
|
+
const { loadToolsRegistry } = await import("@enactprotocol/shared");
|
|
338
|
+
|
|
339
|
+
// No tools.json exists
|
|
340
|
+
const registry = loadToolsRegistry("project", EDGE_TEST_DIR);
|
|
341
|
+
expect(registry.tools).toEqual({});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the install command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { configureInstallCommand } from "../../src/commands/install";
|
|
10
|
+
|
|
11
|
+
// Test fixtures directory
|
|
12
|
+
const FIXTURES_DIR = join(import.meta.dir, "..", "fixtures", "install-cmd");
|
|
13
|
+
|
|
14
|
+
describe("install command", () => {
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
mkdirSync(FIXTURES_DIR, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Clean fixtures for each test
|
|
21
|
+
if (existsSync(FIXTURES_DIR)) {
|
|
22
|
+
rmSync(FIXTURES_DIR, { recursive: true, force: true });
|
|
23
|
+
mkdirSync(FIXTURES_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
if (existsSync(FIXTURES_DIR)) {
|
|
29
|
+
rmSync(FIXTURES_DIR, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("command configuration", () => {
|
|
34
|
+
test("configures install command on program", () => {
|
|
35
|
+
const program = new Command();
|
|
36
|
+
configureInstallCommand(program);
|
|
37
|
+
|
|
38
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
39
|
+
expect(installCmd).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("has correct description", () => {
|
|
43
|
+
const program = new Command();
|
|
44
|
+
configureInstallCommand(program);
|
|
45
|
+
|
|
46
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
47
|
+
expect(installCmd?.description()).toBe("Install a tool to the project or globally");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("has 'i' as alias", () => {
|
|
51
|
+
const program = new Command();
|
|
52
|
+
configureInstallCommand(program);
|
|
53
|
+
|
|
54
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
55
|
+
const aliases = installCmd?.aliases() ?? [];
|
|
56
|
+
expect(aliases).toContain("i");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("tool argument is optional", () => {
|
|
60
|
+
const program = new Command();
|
|
61
|
+
configureInstallCommand(program);
|
|
62
|
+
|
|
63
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
64
|
+
const args = installCmd?.registeredArguments ?? [];
|
|
65
|
+
// Optional argument is wrapped in brackets in Commander
|
|
66
|
+
expect(args.length).toBe(1);
|
|
67
|
+
expect(args[0]?.name()).toBe("tool");
|
|
68
|
+
expect(args[0]?.required).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("has --global option", () => {
|
|
72
|
+
const program = new Command();
|
|
73
|
+
configureInstallCommand(program);
|
|
74
|
+
|
|
75
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
76
|
+
const opts = installCmd?.options ?? [];
|
|
77
|
+
const globalOpt = opts.find((o) => o.long === "--global");
|
|
78
|
+
expect(globalOpt).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("has -g short option for global", () => {
|
|
82
|
+
const program = new Command();
|
|
83
|
+
configureInstallCommand(program);
|
|
84
|
+
|
|
85
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
86
|
+
const opts = installCmd?.options ?? [];
|
|
87
|
+
const globalOpt = opts.find((o) => o.short === "-g");
|
|
88
|
+
expect(globalOpt).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("has --force option", () => {
|
|
92
|
+
const program = new Command();
|
|
93
|
+
configureInstallCommand(program);
|
|
94
|
+
|
|
95
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
96
|
+
const opts = installCmd?.options ?? [];
|
|
97
|
+
const forceOpt = opts.find((o) => o.long === "--force");
|
|
98
|
+
expect(forceOpt).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("has --verbose option", () => {
|
|
102
|
+
const program = new Command();
|
|
103
|
+
configureInstallCommand(program);
|
|
104
|
+
|
|
105
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
106
|
+
const opts = installCmd?.options ?? [];
|
|
107
|
+
const verboseOpt = opts.find((o) => o.long === "--verbose");
|
|
108
|
+
expect(verboseOpt).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("has --json option", () => {
|
|
112
|
+
const program = new Command();
|
|
113
|
+
configureInstallCommand(program);
|
|
114
|
+
|
|
115
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
116
|
+
const opts = installCmd?.options ?? [];
|
|
117
|
+
const jsonOpt = opts.find((o) => o.long === "--json");
|
|
118
|
+
expect(jsonOpt).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("has --verify option", () => {
|
|
122
|
+
const program = new Command();
|
|
123
|
+
configureInstallCommand(program);
|
|
124
|
+
|
|
125
|
+
const installCmd = program.commands.find((cmd) => cmd.name() === "install");
|
|
126
|
+
const opts = installCmd?.options ?? [];
|
|
127
|
+
const verifyOpt = opts.find((o) => o.long === "--verify");
|
|
128
|
+
// --verify is no longer optional, verification is always required
|
|
129
|
+
expect(verifyOpt).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("path detection", () => {
|
|
134
|
+
// Helper function that mirrors the command's path detection logic
|
|
135
|
+
const isToolPath = (tool: string): boolean => {
|
|
136
|
+
return tool === "." || tool.startsWith("./") || tool.startsWith("/") || tool.startsWith("..");
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
test("recognizes current directory (.)", () => {
|
|
140
|
+
expect(isToolPath(".")).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("recognizes relative path (./)", () => {
|
|
144
|
+
expect(isToolPath("./my-tool")).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("recognizes absolute path (/)", () => {
|
|
148
|
+
expect(isToolPath("/Users/test/my-tool")).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("recognizes parent relative path (..)", () => {
|
|
152
|
+
expect(isToolPath("../my-tool")).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("does not recognize tool name as path", () => {
|
|
156
|
+
expect(isToolPath("alice/utils/greeter")).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("does not recognize simple name as path", () => {
|
|
160
|
+
expect(isToolPath("my-tool")).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("registry tool detection", () => {
|
|
165
|
+
// Helper function that mirrors the command's registry tool detection
|
|
166
|
+
const isRegistryTool = (toolName: string): boolean => {
|
|
167
|
+
const parts = toolName.split("/");
|
|
168
|
+
return parts.length >= 3 && !toolName.startsWith(".") && !toolName.startsWith("/");
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
test("recognizes registry tool format (owner/namespace/tool)", () => {
|
|
172
|
+
expect(isRegistryTool("alice/utils/greeter")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("recognizes registry tool with more parts", () => {
|
|
176
|
+
expect(isRegistryTool("acme/internal/tools/formatter")).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("rejects simple tool name", () => {
|
|
180
|
+
expect(isRegistryTool("greeter")).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("rejects owner/tool format (needs namespace)", () => {
|
|
184
|
+
expect(isRegistryTool("alice/greeter")).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("rejects path-like patterns", () => {
|
|
188
|
+
expect(isRegistryTool("./alice/utils/greeter")).toBe(false);
|
|
189
|
+
expect(isRegistryTool("/alice/utils/greeter")).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("tool spec parsing", () => {
|
|
194
|
+
// Helper function that mirrors the command's parseToolSpec
|
|
195
|
+
const parseToolSpec = (spec: string): { name: string; version: string | undefined } => {
|
|
196
|
+
const match = spec.match(/^(@[^@/]+\/[^@]+|[^@]+)(?:@(.+))?$/);
|
|
197
|
+
if (match?.[1]) {
|
|
198
|
+
return { name: match[1], version: match[2] };
|
|
199
|
+
}
|
|
200
|
+
return { name: spec, version: undefined };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
test("parses tool name without version", () => {
|
|
204
|
+
const result = parseToolSpec("alice/utils/greeter");
|
|
205
|
+
expect(result.name).toBe("alice/utils/greeter");
|
|
206
|
+
expect(result.version).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("parses tool name with version", () => {
|
|
210
|
+
const result = parseToolSpec("alice/utils/greeter@1.0.0");
|
|
211
|
+
expect(result.name).toBe("alice/utils/greeter");
|
|
212
|
+
expect(result.version).toBe("1.0.0");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("parses tool name with semver prerelease", () => {
|
|
216
|
+
const result = parseToolSpec("alice/utils/greeter@2.0.0-beta.1");
|
|
217
|
+
expect(result.name).toBe("alice/utils/greeter");
|
|
218
|
+
expect(result.version).toBe("2.0.0-beta.1");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("parses scoped package without version", () => {
|
|
222
|
+
const result = parseToolSpec("@scope/package");
|
|
223
|
+
expect(result.name).toBe("@scope/package");
|
|
224
|
+
expect(result.version).toBeUndefined();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("parses scoped package with version", () => {
|
|
228
|
+
const result = parseToolSpec("@scope/package@3.0.0");
|
|
229
|
+
expect(result.name).toBe("@scope/package");
|
|
230
|
+
expect(result.version).toBe("3.0.0");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("handles latest tag as version", () => {
|
|
234
|
+
const result = parseToolSpec("alice/utils/greeter@latest");
|
|
235
|
+
expect(result.name).toBe("alice/utils/greeter");
|
|
236
|
+
expect(result.version).toBe("latest");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("installation scenarios", () => {
|
|
241
|
+
test("project install should use .enact/tools/", () => {
|
|
242
|
+
const cwd = "/project";
|
|
243
|
+
const toolName = "alice/utils/greeter";
|
|
244
|
+
const expectedPath = join(cwd, ".enact", "tools", toolName.replace(/\//g, "/"));
|
|
245
|
+
expect(expectedPath).toContain(".enact/tools");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("global install should use ~/.enact/tools/", () => {
|
|
249
|
+
const home = process.env.HOME ?? "/home/user";
|
|
250
|
+
const toolName = "alice/utils/greeter";
|
|
251
|
+
const expectedPath = join(home, ".enact", "tools", toolName.replace(/\//g, "/"));
|
|
252
|
+
expect(expectedPath).toContain(".enact/tools");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("bytes formatting", () => {
|
|
257
|
+
// Helper function that mirrors the command's formatBytes
|
|
258
|
+
const formatBytes = (bytes: number): string => {
|
|
259
|
+
if (bytes === 0) return "0 B";
|
|
260
|
+
const k = 1024;
|
|
261
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
262
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
263
|
+
return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
test("formats zero bytes", () => {
|
|
267
|
+
expect(formatBytes(0)).toBe("0 B");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("formats bytes", () => {
|
|
271
|
+
expect(formatBytes(500)).toBe("500.0 B");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("formats kilobytes", () => {
|
|
275
|
+
expect(formatBytes(1024)).toBe("1.0 KB");
|
|
276
|
+
expect(formatBytes(2048)).toBe("2.0 KB");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("formats megabytes", () => {
|
|
280
|
+
expect(formatBytes(1024 * 1024)).toBe("1.0 MB");
|
|
281
|
+
expect(formatBytes(5 * 1024 * 1024)).toBe("5.0 MB");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("formats gigabytes", () => {
|
|
285
|
+
expect(formatBytes(1024 * 1024 * 1024)).toBe("1.0 GB");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|