@herdctl/core 5.4.3 → 5.6.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 (148) hide show
  1. package/dist/config/__tests__/schema.test.js +19 -0
  2. package/dist/config/__tests__/schema.test.js.map +1 -1
  3. package/dist/config/index.d.ts +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +3 -1
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/config/schema.d.ts +10 -18
  8. package/dist/config/schema.d.ts.map +1 -1
  9. package/dist/config/schema.js +4 -2
  10. package/dist/config/schema.js.map +1 -1
  11. package/dist/distribution/__tests__/agent-discovery.test.d.ts +7 -0
  12. package/dist/distribution/__tests__/agent-discovery.test.d.ts.map +1 -0
  13. package/dist/distribution/__tests__/agent-discovery.test.js +443 -0
  14. package/dist/distribution/__tests__/agent-discovery.test.js.map +1 -0
  15. package/dist/distribution/__tests__/agent-info.test.d.ts +7 -0
  16. package/dist/distribution/__tests__/agent-info.test.d.ts.map +1 -0
  17. package/dist/distribution/__tests__/agent-info.test.js +568 -0
  18. package/dist/distribution/__tests__/agent-info.test.js.map +1 -0
  19. package/dist/distribution/__tests__/agent-remover.test.d.ts +7 -0
  20. package/dist/distribution/__tests__/agent-remover.test.d.ts.map +1 -0
  21. package/dist/distribution/__tests__/agent-remover.test.js +498 -0
  22. package/dist/distribution/__tests__/agent-remover.test.js.map +1 -0
  23. package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts +5 -0
  24. package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts.map +1 -0
  25. package/dist/distribution/__tests__/agent-repo-metadata.test.js +500 -0
  26. package/dist/distribution/__tests__/agent-repo-metadata.test.js.map +1 -0
  27. package/dist/distribution/__tests__/env-scanner.test.d.ts +5 -0
  28. package/dist/distribution/__tests__/env-scanner.test.d.ts.map +1 -0
  29. package/dist/distribution/__tests__/env-scanner.test.js +576 -0
  30. package/dist/distribution/__tests__/env-scanner.test.js.map +1 -0
  31. package/dist/distribution/__tests__/file-installer.test.d.ts +7 -0
  32. package/dist/distribution/__tests__/file-installer.test.d.ts.map +1 -0
  33. package/dist/distribution/__tests__/file-installer.test.js +714 -0
  34. package/dist/distribution/__tests__/file-installer.test.js.map +1 -0
  35. package/dist/distribution/__tests__/fleet-config-updater.test.d.ts +7 -0
  36. package/dist/distribution/__tests__/fleet-config-updater.test.d.ts.map +1 -0
  37. package/dist/distribution/__tests__/fleet-config-updater.test.js +531 -0
  38. package/dist/distribution/__tests__/fleet-config-updater.test.js.map +1 -0
  39. package/dist/distribution/__tests__/installation-metadata.test.d.ts +2 -0
  40. package/dist/distribution/__tests__/installation-metadata.test.d.ts.map +1 -0
  41. package/dist/distribution/__tests__/installation-metadata.test.js +292 -0
  42. package/dist/distribution/__tests__/installation-metadata.test.js.map +1 -0
  43. package/dist/distribution/__tests__/integration.test.d.ts +10 -0
  44. package/dist/distribution/__tests__/integration.test.d.ts.map +1 -0
  45. package/dist/distribution/__tests__/integration.test.js +522 -0
  46. package/dist/distribution/__tests__/integration.test.js.map +1 -0
  47. package/dist/distribution/__tests__/repository-fetcher.test.d.ts +5 -0
  48. package/dist/distribution/__tests__/repository-fetcher.test.d.ts.map +1 -0
  49. package/dist/distribution/__tests__/repository-fetcher.test.js +386 -0
  50. package/dist/distribution/__tests__/repository-fetcher.test.js.map +1 -0
  51. package/dist/distribution/__tests__/repository-validator.test.d.ts +7 -0
  52. package/dist/distribution/__tests__/repository-validator.test.d.ts.map +1 -0
  53. package/dist/distribution/__tests__/repository-validator.test.js +447 -0
  54. package/dist/distribution/__tests__/repository-validator.test.js.map +1 -0
  55. package/dist/distribution/__tests__/source-specifier.test.d.ts +5 -0
  56. package/dist/distribution/__tests__/source-specifier.test.d.ts.map +1 -0
  57. package/dist/distribution/__tests__/source-specifier.test.js +533 -0
  58. package/dist/distribution/__tests__/source-specifier.test.js.map +1 -0
  59. package/dist/distribution/agent-discovery.d.ts +81 -0
  60. package/dist/distribution/agent-discovery.d.ts.map +1 -0
  61. package/dist/distribution/agent-discovery.js +264 -0
  62. package/dist/distribution/agent-discovery.js.map +1 -0
  63. package/dist/distribution/agent-info.d.ts +86 -0
  64. package/dist/distribution/agent-info.d.ts.map +1 -0
  65. package/dist/distribution/agent-info.js +225 -0
  66. package/dist/distribution/agent-info.js.map +1 -0
  67. package/dist/distribution/agent-remover.d.ts +83 -0
  68. package/dist/distribution/agent-remover.d.ts.map +1 -0
  69. package/dist/distribution/agent-remover.js +222 -0
  70. package/dist/distribution/agent-remover.js.map +1 -0
  71. package/dist/distribution/agent-repo-metadata.d.ts +181 -0
  72. package/dist/distribution/agent-repo-metadata.d.ts.map +1 -0
  73. package/dist/distribution/agent-repo-metadata.js +143 -0
  74. package/dist/distribution/agent-repo-metadata.js.map +1 -0
  75. package/dist/distribution/env-scanner.d.ts +78 -0
  76. package/dist/distribution/env-scanner.d.ts.map +1 -0
  77. package/dist/distribution/env-scanner.js +144 -0
  78. package/dist/distribution/env-scanner.js.map +1 -0
  79. package/dist/distribution/file-installer.d.ts +80 -0
  80. package/dist/distribution/file-installer.d.ts.map +1 -0
  81. package/dist/distribution/file-installer.js +268 -0
  82. package/dist/distribution/file-installer.js.map +1 -0
  83. package/dist/distribution/fleet-config-updater.d.ts +96 -0
  84. package/dist/distribution/fleet-config-updater.d.ts.map +1 -0
  85. package/dist/distribution/fleet-config-updater.js +266 -0
  86. package/dist/distribution/fleet-config-updater.js.map +1 -0
  87. package/dist/distribution/index.d.ts +23 -0
  88. package/dist/distribution/index.d.ts.map +1 -0
  89. package/dist/distribution/index.js +42 -0
  90. package/dist/distribution/index.js.map +1 -0
  91. package/dist/distribution/installation-metadata.d.ts +191 -0
  92. package/dist/distribution/installation-metadata.d.ts.map +1 -0
  93. package/dist/distribution/installation-metadata.js +100 -0
  94. package/dist/distribution/installation-metadata.js.map +1 -0
  95. package/dist/distribution/repository-fetcher.d.ts +104 -0
  96. package/dist/distribution/repository-fetcher.d.ts.map +1 -0
  97. package/dist/distribution/repository-fetcher.js +246 -0
  98. package/dist/distribution/repository-fetcher.js.map +1 -0
  99. package/dist/distribution/repository-validator.d.ts +86 -0
  100. package/dist/distribution/repository-validator.d.ts.map +1 -0
  101. package/dist/distribution/repository-validator.js +296 -0
  102. package/dist/distribution/repository-validator.js.map +1 -0
  103. package/dist/distribution/source-specifier.d.ts +106 -0
  104. package/dist/distribution/source-specifier.d.ts.map +1 -0
  105. package/dist/distribution/source-specifier.js +247 -0
  106. package/dist/distribution/source-specifier.js.map +1 -0
  107. package/dist/fleet-manager/errors.d.ts +15 -0
  108. package/dist/fleet-manager/errors.d.ts.map +1 -1
  109. package/dist/fleet-manager/errors.js +16 -0
  110. package/dist/fleet-manager/errors.js.map +1 -1
  111. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  112. package/dist/fleet-manager/fleet-manager.js +32 -9
  113. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  114. package/dist/hooks/types.d.ts +2 -3
  115. package/dist/hooks/types.d.ts.map +1 -1
  116. package/dist/index.d.ts +1 -0
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +2 -0
  119. package/dist/index.js.map +1 -1
  120. package/dist/runner/job-executor.js +10 -10
  121. package/dist/runner/job-executor.js.map +1 -1
  122. package/dist/runner/message-processor.d.ts.map +1 -1
  123. package/dist/runner/message-processor.js +7 -2
  124. package/dist/runner/message-processor.js.map +1 -1
  125. package/dist/runner/runtime/cli-runtime.d.ts +3 -2
  126. package/dist/runner/runtime/cli-runtime.d.ts.map +1 -1
  127. package/dist/runner/runtime/cli-session-path.d.ts +0 -20
  128. package/dist/runner/runtime/cli-session-path.d.ts.map +1 -1
  129. package/dist/runner/runtime/cli-session-path.js +1 -1
  130. package/dist/runner/runtime/cli-session-path.js.map +1 -1
  131. package/dist/runner/runtime/container-manager.js +1 -1
  132. package/dist/runner/runtime/container-manager.js.map +1 -1
  133. package/dist/runner/runtime/container-runner.d.ts.map +1 -1
  134. package/dist/runner/runtime/container-runner.js +5 -1
  135. package/dist/runner/runtime/container-runner.js.map +1 -1
  136. package/dist/runner/runtime/factory.d.ts +2 -1
  137. package/dist/runner/runtime/factory.d.ts.map +1 -1
  138. package/dist/runner/runtime/index.d.ts +4 -10
  139. package/dist/runner/runtime/index.d.ts.map +1 -1
  140. package/dist/runner/runtime/index.js +4 -13
  141. package/dist/runner/runtime/index.js.map +1 -1
  142. package/dist/scheduler/errors.d.ts +15 -0
  143. package/dist/scheduler/errors.d.ts.map +1 -1
  144. package/dist/state/schemas/session-info.d.ts +2 -1
  145. package/dist/state/schemas/session-info.d.ts.map +1 -1
  146. package/dist/state/schemas/session-info.js +1 -1
  147. package/dist/state/schemas/session-info.js.map +1 -1
  148. package/package.json +1 -1
@@ -0,0 +1,714 @@
1
+ /**
2
+ * Tests for file installer
3
+ *
4
+ * Uses real file I/O with temporary directories to test installation logic.
5
+ */
6
+ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
+ import { AGENT_ALREADY_EXISTS, AgentInstallError, INVALID_AGENT_NAME, INVALID_AGENT_YAML, installAgentFiles, MISSING_AGENT_YAML, } from "../file-installer.js";
11
+ // =============================================================================
12
+ // Test Setup
13
+ // =============================================================================
14
+ describe("installAgentFiles", () => {
15
+ let sourceDir;
16
+ let targetDir;
17
+ beforeEach(async () => {
18
+ // Create fresh temp directories for each test
19
+ sourceDir = await mkdtemp(join(tmpdir(), "herdctl-installer-source-"));
20
+ targetDir = await mkdtemp(join(tmpdir(), "herdctl-installer-target-"));
21
+ });
22
+ afterEach(async () => {
23
+ // Clean up temp directories
24
+ await rm(sourceDir, { recursive: true, force: true });
25
+ await rm(targetDir, { recursive: true, force: true });
26
+ });
27
+ // ===========================================================================
28
+ // Helper Functions
29
+ // ===========================================================================
30
+ /**
31
+ * Minimal valid agent.yaml content
32
+ */
33
+ const minimalAgentYaml = `
34
+ name: test-agent
35
+ runtime: cli
36
+ `;
37
+ /**
38
+ * Standard source for metadata tracking
39
+ */
40
+ const standardSource = {
41
+ type: "github",
42
+ url: "https://github.com/user/test-agent",
43
+ ref: "v1.0.0",
44
+ };
45
+ /**
46
+ * Helper to check if a path exists
47
+ */
48
+ async function pathExists(filePath) {
49
+ try {
50
+ await access(filePath);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Helper to read JSON file
59
+ */
60
+ async function readJsonFile(filePath) {
61
+ const content = await readFile(filePath, "utf-8");
62
+ return JSON.parse(content);
63
+ }
64
+ // ===========================================================================
65
+ // Successful Installation Tests
66
+ // ===========================================================================
67
+ describe("successful installation", () => {
68
+ it("installs agent from source directory", async () => {
69
+ // Setup source directory with minimal agent.yaml
70
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
71
+ const result = await installAgentFiles({
72
+ sourceDir,
73
+ targetBaseDir: targetDir,
74
+ source: standardSource,
75
+ });
76
+ expect(result.agentName).toBe("test-agent");
77
+ expect(result.installPath).toBe(join(targetDir, "agents", "test-agent"));
78
+ expect(result.copiedFiles).toContain("agent.yaml");
79
+ });
80
+ it("creates workspace directory", async () => {
81
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
82
+ const result = await installAgentFiles({
83
+ sourceDir,
84
+ targetBaseDir: targetDir,
85
+ source: standardSource,
86
+ });
87
+ const workspacePath = join(result.installPath, "workspace");
88
+ expect(await pathExists(workspacePath)).toBe(true);
89
+ });
90
+ it("writes valid metadata.json", async () => {
91
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
92
+ const result = await installAgentFiles({
93
+ sourceDir,
94
+ targetBaseDir: targetDir,
95
+ source: standardSource,
96
+ });
97
+ const metadataPath = join(result.installPath, "metadata.json");
98
+ expect(await pathExists(metadataPath)).toBe(true);
99
+ const metadata = await readJsonFile(metadataPath);
100
+ expect(metadata.source.type).toBe("github");
101
+ expect(metadata.source.url).toBe("https://github.com/user/test-agent");
102
+ expect(metadata.source.ref).toBe("v1.0.0");
103
+ expect(metadata.installed_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
104
+ expect(metadata.installed_by).toMatch(/^herdctl@/);
105
+ });
106
+ it("excludes .git directory", async () => {
107
+ // Setup source with .git directory
108
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
109
+ await mkdir(join(sourceDir, ".git"), { recursive: true });
110
+ await writeFile(join(sourceDir, ".git", "config"), "git config");
111
+ await writeFile(join(sourceDir, ".git", "HEAD"), "ref: refs/heads/main");
112
+ const result = await installAgentFiles({
113
+ sourceDir,
114
+ targetBaseDir: targetDir,
115
+ source: standardSource,
116
+ });
117
+ // .git should not exist in target
118
+ expect(await pathExists(join(result.installPath, ".git"))).toBe(false);
119
+ // agent.yaml should exist
120
+ expect(await pathExists(join(result.installPath, "agent.yaml"))).toBe(true);
121
+ // copiedFiles should not include .git entries
122
+ expect(result.copiedFiles.some((f) => f.includes(".git"))).toBe(false);
123
+ });
124
+ it("excludes node_modules directory", async () => {
125
+ // Setup source with node_modules directory
126
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
127
+ await mkdir(join(sourceDir, "node_modules", "some-package"), { recursive: true });
128
+ await writeFile(join(sourceDir, "node_modules", "some-package", "index.js"), "module.exports = {};");
129
+ const result = await installAgentFiles({
130
+ sourceDir,
131
+ targetBaseDir: targetDir,
132
+ source: standardSource,
133
+ });
134
+ // node_modules should not exist in target
135
+ expect(await pathExists(join(result.installPath, "node_modules"))).toBe(false);
136
+ // copiedFiles should not include node_modules entries
137
+ expect(result.copiedFiles.some((f) => f.includes("node_modules"))).toBe(false);
138
+ });
139
+ it("handles nested directory structures", async () => {
140
+ // Setup source with nested directories
141
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
142
+ await writeFile(join(sourceDir, "CLAUDE.md"), "# Agent");
143
+ await mkdir(join(sourceDir, "knowledge"), { recursive: true });
144
+ await writeFile(join(sourceDir, "knowledge", "guide.md"), "# Guide");
145
+ await mkdir(join(sourceDir, "knowledge", "deep"), { recursive: true });
146
+ await writeFile(join(sourceDir, "knowledge", "deep", "nested.md"), "# Nested");
147
+ const result = await installAgentFiles({
148
+ sourceDir,
149
+ targetBaseDir: targetDir,
150
+ source: standardSource,
151
+ });
152
+ // All files should be copied
153
+ expect(await pathExists(join(result.installPath, "agent.yaml"))).toBe(true);
154
+ expect(await pathExists(join(result.installPath, "CLAUDE.md"))).toBe(true);
155
+ expect(await pathExists(join(result.installPath, "knowledge", "guide.md"))).toBe(true);
156
+ expect(await pathExists(join(result.installPath, "knowledge", "deep", "nested.md"))).toBe(true);
157
+ // copiedFiles should include all relative paths
158
+ expect(result.copiedFiles).toContain("agent.yaml");
159
+ expect(result.copiedFiles).toContain("CLAUDE.md");
160
+ expect(result.copiedFiles).toContain(join("knowledge", "guide.md"));
161
+ expect(result.copiedFiles).toContain(join("knowledge", "deep", "nested.md"));
162
+ });
163
+ it("works with --path override option", async () => {
164
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
165
+ const customPath = join(targetDir, "custom", "location", "my-agent");
166
+ const result = await installAgentFiles({
167
+ sourceDir,
168
+ targetBaseDir: targetDir,
169
+ source: standardSource,
170
+ targetPath: customPath,
171
+ });
172
+ expect(result.installPath).toBe(customPath);
173
+ expect(await pathExists(join(customPath, "agent.yaml"))).toBe(true);
174
+ expect(await pathExists(join(customPath, "workspace"))).toBe(true);
175
+ expect(await pathExists(join(customPath, "metadata.json"))).toBe(true);
176
+ });
177
+ it("returns correct copiedFiles list", async () => {
178
+ // Setup source with multiple files
179
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
180
+ await writeFile(join(sourceDir, "CLAUDE.md"), "# Identity");
181
+ await writeFile(join(sourceDir, "README.md"), "# Docs");
182
+ await writeFile(join(sourceDir, "herdctl.json"), "{}");
183
+ const result = await installAgentFiles({
184
+ sourceDir,
185
+ targetBaseDir: targetDir,
186
+ source: standardSource,
187
+ });
188
+ expect(result.copiedFiles).toHaveLength(4);
189
+ expect(result.copiedFiles).toContain("agent.yaml");
190
+ expect(result.copiedFiles).toContain("CLAUDE.md");
191
+ expect(result.copiedFiles).toContain("README.md");
192
+ expect(result.copiedFiles).toContain("herdctl.json");
193
+ });
194
+ it("handles local source type in metadata", async () => {
195
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
196
+ const localSource = {
197
+ type: "local",
198
+ url: "/path/to/local/agent",
199
+ };
200
+ const result = await installAgentFiles({
201
+ sourceDir,
202
+ targetBaseDir: targetDir,
203
+ source: localSource,
204
+ });
205
+ const metadata = await readJsonFile(join(result.installPath, "metadata.json"));
206
+ expect(metadata.source.type).toBe("local");
207
+ expect(metadata.source.url).toBe("/path/to/local/agent");
208
+ });
209
+ });
210
+ // ===========================================================================
211
+ // Error: Agent Already Exists
212
+ // ===========================================================================
213
+ describe("agent already exists error", () => {
214
+ it("errors when target already exists", async () => {
215
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
216
+ // Create existing agent directory
217
+ const existingPath = join(targetDir, "agents", "test-agent");
218
+ await mkdir(existingPath, { recursive: true });
219
+ await writeFile(join(existingPath, "agent.yaml"), minimalAgentYaml);
220
+ await expect(installAgentFiles({
221
+ sourceDir,
222
+ targetBaseDir: targetDir,
223
+ source: standardSource,
224
+ })).rejects.toThrow(AgentInstallError);
225
+ try {
226
+ await installAgentFiles({
227
+ sourceDir,
228
+ targetBaseDir: targetDir,
229
+ source: standardSource,
230
+ });
231
+ }
232
+ catch (err) {
233
+ const error = err;
234
+ expect(error.code).toBe(AGENT_ALREADY_EXISTS);
235
+ expect(error.message).toContain("test-agent");
236
+ expect(error.message).toContain("already exists");
237
+ }
238
+ });
239
+ it("errors even if target is an empty directory", async () => {
240
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
241
+ // Create existing empty directory
242
+ const existingPath = join(targetDir, "agents", "test-agent");
243
+ await mkdir(existingPath, { recursive: true });
244
+ await expect(installAgentFiles({
245
+ sourceDir,
246
+ targetBaseDir: targetDir,
247
+ source: standardSource,
248
+ })).rejects.toThrow(AgentInstallError);
249
+ });
250
+ });
251
+ // ===========================================================================
252
+ // Error: Invalid Agent Name
253
+ // ===========================================================================
254
+ describe("invalid agent name error", () => {
255
+ it("errors on agent name with spaces", async () => {
256
+ const invalidNameYaml = `
257
+ name: "invalid name with spaces"
258
+ runtime: cli
259
+ `;
260
+ await writeFile(join(sourceDir, "agent.yaml"), invalidNameYaml);
261
+ await expect(installAgentFiles({
262
+ sourceDir,
263
+ targetBaseDir: targetDir,
264
+ source: standardSource,
265
+ })).rejects.toThrow(AgentInstallError);
266
+ try {
267
+ await installAgentFiles({
268
+ sourceDir,
269
+ targetBaseDir: targetDir,
270
+ source: standardSource,
271
+ });
272
+ }
273
+ catch (err) {
274
+ const error = err;
275
+ expect(error.code).toBe(INVALID_AGENT_NAME);
276
+ expect(error.message).toContain("Invalid agent name");
277
+ }
278
+ });
279
+ it("errors on agent name starting with hyphen", async () => {
280
+ const invalidNameYaml = `
281
+ name: "-starts-with-hyphen"
282
+ runtime: cli
283
+ `;
284
+ await writeFile(join(sourceDir, "agent.yaml"), invalidNameYaml);
285
+ await expect(installAgentFiles({
286
+ sourceDir,
287
+ targetBaseDir: targetDir,
288
+ source: standardSource,
289
+ })).rejects.toThrow(AgentInstallError);
290
+ try {
291
+ await installAgentFiles({
292
+ sourceDir,
293
+ targetBaseDir: targetDir,
294
+ source: standardSource,
295
+ });
296
+ }
297
+ catch (err) {
298
+ const error = err;
299
+ expect(error.code).toBe(INVALID_AGENT_NAME);
300
+ }
301
+ });
302
+ it("errors on agent name with special characters", async () => {
303
+ const invalidNameYaml = `
304
+ name: "agent@name.with.dots"
305
+ runtime: cli
306
+ `;
307
+ await writeFile(join(sourceDir, "agent.yaml"), invalidNameYaml);
308
+ await expect(installAgentFiles({
309
+ sourceDir,
310
+ targetBaseDir: targetDir,
311
+ source: standardSource,
312
+ })).rejects.toThrow(AgentInstallError);
313
+ try {
314
+ await installAgentFiles({
315
+ sourceDir,
316
+ targetBaseDir: targetDir,
317
+ source: standardSource,
318
+ });
319
+ }
320
+ catch (err) {
321
+ const error = err;
322
+ expect(error.code).toBe(INVALID_AGENT_NAME);
323
+ }
324
+ });
325
+ it("errors on agent name with path traversal", async () => {
326
+ const invalidNameYaml = `
327
+ name: "../../../etc/passwd"
328
+ runtime: cli
329
+ `;
330
+ await writeFile(join(sourceDir, "agent.yaml"), invalidNameYaml);
331
+ await expect(installAgentFiles({
332
+ sourceDir,
333
+ targetBaseDir: targetDir,
334
+ source: standardSource,
335
+ })).rejects.toThrow(AgentInstallError);
336
+ try {
337
+ await installAgentFiles({
338
+ sourceDir,
339
+ targetBaseDir: targetDir,
340
+ source: standardSource,
341
+ });
342
+ }
343
+ catch (err) {
344
+ const error = err;
345
+ expect(error.code).toBe(INVALID_AGENT_NAME);
346
+ }
347
+ });
348
+ it("allows valid agent names with underscores and hyphens", async () => {
349
+ const validNameYaml = `
350
+ name: my-agent_v2
351
+ runtime: cli
352
+ `;
353
+ await writeFile(join(sourceDir, "agent.yaml"), validNameYaml);
354
+ const result = await installAgentFiles({
355
+ sourceDir,
356
+ targetBaseDir: targetDir,
357
+ source: standardSource,
358
+ });
359
+ expect(result.agentName).toBe("my-agent_v2");
360
+ });
361
+ it("allows valid agent names starting with number", async () => {
362
+ const validNameYaml = `
363
+ name: 2fast2furious
364
+ runtime: cli
365
+ `;
366
+ await writeFile(join(sourceDir, "agent.yaml"), validNameYaml);
367
+ const result = await installAgentFiles({
368
+ sourceDir,
369
+ targetBaseDir: targetDir,
370
+ source: standardSource,
371
+ });
372
+ expect(result.agentName).toBe("2fast2furious");
373
+ });
374
+ });
375
+ // ===========================================================================
376
+ // Error: Missing agent.yaml
377
+ // ===========================================================================
378
+ describe("missing agent.yaml error", () => {
379
+ it("errors on missing agent.yaml", async () => {
380
+ // Don't create agent.yaml - empty source directory
381
+ await expect(installAgentFiles({
382
+ sourceDir,
383
+ targetBaseDir: targetDir,
384
+ source: standardSource,
385
+ })).rejects.toThrow(AgentInstallError);
386
+ try {
387
+ await installAgentFiles({
388
+ sourceDir,
389
+ targetBaseDir: targetDir,
390
+ source: standardSource,
391
+ });
392
+ }
393
+ catch (err) {
394
+ const error = err;
395
+ expect(error.code).toBe(MISSING_AGENT_YAML);
396
+ expect(error.message).toContain("agent.yaml not found");
397
+ }
398
+ });
399
+ it("errors when only other files exist", async () => {
400
+ // Create other files but not agent.yaml
401
+ await writeFile(join(sourceDir, "README.md"), "# Docs");
402
+ await writeFile(join(sourceDir, "CLAUDE.md"), "# Identity");
403
+ await expect(installAgentFiles({
404
+ sourceDir,
405
+ targetBaseDir: targetDir,
406
+ source: standardSource,
407
+ })).rejects.toThrow(AgentInstallError);
408
+ try {
409
+ await installAgentFiles({
410
+ sourceDir,
411
+ targetBaseDir: targetDir,
412
+ source: standardSource,
413
+ });
414
+ }
415
+ catch (err) {
416
+ const error = err;
417
+ expect(error.code).toBe(MISSING_AGENT_YAML);
418
+ }
419
+ });
420
+ });
421
+ // ===========================================================================
422
+ // Error: Invalid agent.yaml
423
+ // ===========================================================================
424
+ describe("invalid agent.yaml error", () => {
425
+ it("errors on invalid YAML syntax", async () => {
426
+ const invalidYaml = `
427
+ name: test-agent
428
+ bad-indent: invalid
429
+ nested: wrong
430
+ `;
431
+ await writeFile(join(sourceDir, "agent.yaml"), invalidYaml);
432
+ await expect(installAgentFiles({
433
+ sourceDir,
434
+ targetBaseDir: targetDir,
435
+ source: standardSource,
436
+ })).rejects.toThrow(AgentInstallError);
437
+ try {
438
+ await installAgentFiles({
439
+ sourceDir,
440
+ targetBaseDir: targetDir,
441
+ source: standardSource,
442
+ });
443
+ }
444
+ catch (err) {
445
+ const error = err;
446
+ expect(error.code).toBe(INVALID_AGENT_YAML);
447
+ expect(error.message).toContain("Invalid YAML syntax");
448
+ }
449
+ });
450
+ it("errors on empty agent.yaml", async () => {
451
+ await writeFile(join(sourceDir, "agent.yaml"), "");
452
+ await expect(installAgentFiles({
453
+ sourceDir,
454
+ targetBaseDir: targetDir,
455
+ source: standardSource,
456
+ })).rejects.toThrow(AgentInstallError);
457
+ try {
458
+ await installAgentFiles({
459
+ sourceDir,
460
+ targetBaseDir: targetDir,
461
+ source: standardSource,
462
+ });
463
+ }
464
+ catch (err) {
465
+ const error = err;
466
+ expect(error.code).toBe(INVALID_AGENT_YAML);
467
+ }
468
+ });
469
+ it("errors on agent.yaml without name field", async () => {
470
+ const noNameYaml = `
471
+ description: An agent without a name
472
+ runtime: cli
473
+ `;
474
+ await writeFile(join(sourceDir, "agent.yaml"), noNameYaml);
475
+ await expect(installAgentFiles({
476
+ sourceDir,
477
+ targetBaseDir: targetDir,
478
+ source: standardSource,
479
+ })).rejects.toThrow(AgentInstallError);
480
+ try {
481
+ await installAgentFiles({
482
+ sourceDir,
483
+ targetBaseDir: targetDir,
484
+ source: standardSource,
485
+ });
486
+ }
487
+ catch (err) {
488
+ const error = err;
489
+ expect(error.code).toBe(INVALID_AGENT_YAML);
490
+ expect(error.message).toContain("name");
491
+ }
492
+ });
493
+ it("errors on agent.yaml with empty name", async () => {
494
+ const emptyNameYaml = `
495
+ name: ""
496
+ runtime: cli
497
+ `;
498
+ await writeFile(join(sourceDir, "agent.yaml"), emptyNameYaml);
499
+ await expect(installAgentFiles({
500
+ sourceDir,
501
+ targetBaseDir: targetDir,
502
+ source: standardSource,
503
+ })).rejects.toThrow(AgentInstallError);
504
+ try {
505
+ await installAgentFiles({
506
+ sourceDir,
507
+ targetBaseDir: targetDir,
508
+ source: standardSource,
509
+ });
510
+ }
511
+ catch (err) {
512
+ const error = err;
513
+ expect(error.code).toBe(INVALID_AGENT_YAML);
514
+ }
515
+ });
516
+ it("errors on agent.yaml with non-string name", async () => {
517
+ const nonStringNameYaml = `
518
+ name: 123
519
+ runtime: cli
520
+ `;
521
+ await writeFile(join(sourceDir, "agent.yaml"), nonStringNameYaml);
522
+ await expect(installAgentFiles({
523
+ sourceDir,
524
+ targetBaseDir: targetDir,
525
+ source: standardSource,
526
+ })).rejects.toThrow(AgentInstallError);
527
+ try {
528
+ await installAgentFiles({
529
+ sourceDir,
530
+ targetBaseDir: targetDir,
531
+ source: standardSource,
532
+ });
533
+ }
534
+ catch (err) {
535
+ const error = err;
536
+ expect(error.code).toBe(INVALID_AGENT_YAML);
537
+ }
538
+ });
539
+ it("errors on agent.yaml that is just a string", async () => {
540
+ await writeFile(join(sourceDir, "agent.yaml"), "just a string");
541
+ await expect(installAgentFiles({
542
+ sourceDir,
543
+ targetBaseDir: targetDir,
544
+ source: standardSource,
545
+ })).rejects.toThrow(AgentInstallError);
546
+ try {
547
+ await installAgentFiles({
548
+ sourceDir,
549
+ targetBaseDir: targetDir,
550
+ source: standardSource,
551
+ });
552
+ }
553
+ catch (err) {
554
+ const error = err;
555
+ expect(error.code).toBe(INVALID_AGENT_YAML);
556
+ }
557
+ });
558
+ it("errors on agent.yaml with only comments", async () => {
559
+ await writeFile(join(sourceDir, "agent.yaml"), "# Just a comment\n# Another comment");
560
+ await expect(installAgentFiles({
561
+ sourceDir,
562
+ targetBaseDir: targetDir,
563
+ source: standardSource,
564
+ })).rejects.toThrow(AgentInstallError);
565
+ try {
566
+ await installAgentFiles({
567
+ sourceDir,
568
+ targetBaseDir: targetDir,
569
+ source: standardSource,
570
+ });
571
+ }
572
+ catch (err) {
573
+ const error = err;
574
+ expect(error.code).toBe(INVALID_AGENT_YAML);
575
+ }
576
+ });
577
+ });
578
+ // ===========================================================================
579
+ // Edge Cases
580
+ // ===========================================================================
581
+ describe("edge cases", () => {
582
+ it("handles source with both .git and node_modules", async () => {
583
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
584
+ await mkdir(join(sourceDir, ".git", "objects"), { recursive: true });
585
+ await mkdir(join(sourceDir, "node_modules", "pkg"), { recursive: true });
586
+ await mkdir(join(sourceDir, "src"), { recursive: true });
587
+ await writeFile(join(sourceDir, ".git", "objects", "abc123"), "blob");
588
+ await writeFile(join(sourceDir, "node_modules", "pkg", "index.js"), "code");
589
+ await writeFile(join(sourceDir, "src", "main.ts"), "export default {}");
590
+ const result = await installAgentFiles({
591
+ sourceDir,
592
+ targetBaseDir: targetDir,
593
+ source: standardSource,
594
+ });
595
+ // Excluded directories should not exist
596
+ expect(await pathExists(join(result.installPath, ".git"))).toBe(false);
597
+ expect(await pathExists(join(result.installPath, "node_modules"))).toBe(false);
598
+ // Other files should exist
599
+ expect(await pathExists(join(result.installPath, "agent.yaml"))).toBe(true);
600
+ expect(await pathExists(join(result.installPath, "src", "main.ts"))).toBe(true);
601
+ });
602
+ it("handles deeply nested excluded directories", async () => {
603
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
604
+ await mkdir(join(sourceDir, "project", ".git", "hooks"), { recursive: true });
605
+ await writeFile(join(sourceDir, "project", ".git", "hooks", "pre-commit"), "#!/bin/sh");
606
+ await mkdir(join(sourceDir, "project", "lib"), { recursive: true });
607
+ await writeFile(join(sourceDir, "project", "lib", "utils.ts"), "code");
608
+ const result = await installAgentFiles({
609
+ sourceDir,
610
+ targetBaseDir: targetDir,
611
+ source: standardSource,
612
+ });
613
+ // .git in nested project should still be excluded
614
+ expect(await pathExists(join(result.installPath, "project", ".git"))).toBe(false);
615
+ // Other nested files should exist
616
+ expect(await pathExists(join(result.installPath, "project", "lib", "utils.ts"))).toBe(true);
617
+ });
618
+ it("preserves file contents after copy", async () => {
619
+ const yamlContent = `
620
+ name: test-agent
621
+ description: "A test agent for content verification"
622
+ runtime: cli
623
+ `;
624
+ const readmeContent = "# Test Agent\n\nThis is the readme content.";
625
+ const codeContent = 'export const VERSION = "1.0.0";';
626
+ await writeFile(join(sourceDir, "agent.yaml"), yamlContent);
627
+ await writeFile(join(sourceDir, "README.md"), readmeContent);
628
+ await mkdir(join(sourceDir, "src"), { recursive: true });
629
+ await writeFile(join(sourceDir, "src", "index.ts"), codeContent);
630
+ const result = await installAgentFiles({
631
+ sourceDir,
632
+ targetBaseDir: targetDir,
633
+ source: standardSource,
634
+ });
635
+ // Verify content is preserved
636
+ const copiedYaml = await readFile(join(result.installPath, "agent.yaml"), "utf-8");
637
+ const copiedReadme = await readFile(join(result.installPath, "README.md"), "utf-8");
638
+ const copiedCode = await readFile(join(result.installPath, "src", "index.ts"), "utf-8");
639
+ expect(copiedYaml).toBe(yamlContent);
640
+ expect(copiedReadme).toBe(readmeContent);
641
+ expect(copiedCode).toBe(codeContent);
642
+ });
643
+ it("handles agent name that is just numbers", async () => {
644
+ const numbersNameYaml = `
645
+ name: "123456"
646
+ runtime: cli
647
+ `;
648
+ await writeFile(join(sourceDir, "agent.yaml"), numbersNameYaml);
649
+ const result = await installAgentFiles({
650
+ sourceDir,
651
+ targetBaseDir: targetDir,
652
+ source: standardSource,
653
+ });
654
+ expect(result.agentName).toBe("123456");
655
+ expect(result.installPath).toBe(join(targetDir, "agents", "123456"));
656
+ });
657
+ it("handles whitespace in agent.yaml name (after trim)", async () => {
658
+ // YAML will preserve the string including whitespace
659
+ const whitespaceNameYaml = `
660
+ name: " spaced "
661
+ runtime: cli
662
+ `;
663
+ await writeFile(join(sourceDir, "agent.yaml"), whitespaceNameYaml);
664
+ // The name with spaces should fail validation
665
+ await expect(installAgentFiles({
666
+ sourceDir,
667
+ targetBaseDir: targetDir,
668
+ source: standardSource,
669
+ })).rejects.toThrow(AgentInstallError);
670
+ });
671
+ });
672
+ // ===========================================================================
673
+ // Metadata Content Tests
674
+ // ===========================================================================
675
+ describe("metadata content", () => {
676
+ it("includes all source fields in metadata", async () => {
677
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
678
+ const fullSource = {
679
+ type: "github",
680
+ url: "https://github.com/org/repo",
681
+ ref: "v2.0.0",
682
+ version: "2.0.0",
683
+ };
684
+ const result = await installAgentFiles({
685
+ sourceDir,
686
+ targetBaseDir: targetDir,
687
+ source: fullSource,
688
+ });
689
+ const metadata = await readJsonFile(join(result.installPath, "metadata.json"));
690
+ expect(metadata.source.type).toBe("github");
691
+ expect(metadata.source.url).toBe("https://github.com/org/repo");
692
+ expect(metadata.source.ref).toBe("v2.0.0");
693
+ expect(metadata.source.version).toBe("2.0.0");
694
+ });
695
+ it("generates valid ISO 8601 timestamp", async () => {
696
+ await writeFile(join(sourceDir, "agent.yaml"), minimalAgentYaml);
697
+ const result = await installAgentFiles({
698
+ sourceDir,
699
+ targetBaseDir: targetDir,
700
+ source: standardSource,
701
+ });
702
+ const metadata = await readJsonFile(join(result.installPath, "metadata.json"));
703
+ // Check it's a valid ISO 8601 timestamp
704
+ const timestamp = new Date(metadata.installed_at);
705
+ expect(timestamp).toBeInstanceOf(Date);
706
+ expect(Number.isNaN(timestamp.getTime())).toBe(false);
707
+ // Check it's recent (within last minute)
708
+ const now = Date.now();
709
+ const installedTime = timestamp.getTime();
710
+ expect(now - installedTime).toBeLessThan(60000);
711
+ });
712
+ });
713
+ });
714
+ //# sourceMappingURL=file-installer.test.js.map