herdctl 1.3.10 → 1.4.1

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 (46) hide show
  1. package/dist/commands/__tests__/agent.test.d.ts +2 -0
  2. package/dist/commands/__tests__/agent.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/agent.test.js +1461 -0
  4. package/dist/commands/__tests__/agent.test.js.map +1 -0
  5. package/dist/commands/__tests__/init-agent.test.d.ts +2 -0
  6. package/dist/commands/__tests__/init-agent.test.d.ts.map +1 -0
  7. package/dist/commands/__tests__/init-agent.test.js +363 -0
  8. package/dist/commands/__tests__/init-agent.test.js.map +1 -0
  9. package/dist/commands/__tests__/init-fleet.test.d.ts +2 -0
  10. package/dist/commands/__tests__/init-fleet.test.d.ts.map +1 -0
  11. package/dist/commands/__tests__/init-fleet.test.js +154 -0
  12. package/dist/commands/__tests__/init-fleet.test.js.map +1 -0
  13. package/dist/commands/__tests__/init.test.js +43 -213
  14. package/dist/commands/__tests__/init.test.js.map +1 -1
  15. package/dist/commands/agent.d.ts +143 -0
  16. package/dist/commands/agent.d.ts.map +1 -0
  17. package/dist/commands/agent.js +845 -0
  18. package/dist/commands/agent.js.map +1 -0
  19. package/dist/commands/init-agent.d.ts +22 -0
  20. package/dist/commands/init-agent.d.ts.map +1 -0
  21. package/dist/commands/init-agent.js +273 -0
  22. package/dist/commands/init-agent.js.map +1 -0
  23. package/dist/commands/init-fleet.d.ts +13 -0
  24. package/dist/commands/init-fleet.d.ts.map +1 -0
  25. package/dist/commands/init-fleet.js +91 -0
  26. package/dist/commands/init-fleet.js.map +1 -0
  27. package/dist/commands/init-utils.d.ts +8 -0
  28. package/dist/commands/init-utils.d.ts.map +1 -0
  29. package/dist/commands/init-utils.js +24 -0
  30. package/dist/commands/init-utils.js.map +1 -0
  31. package/dist/commands/init.d.ts +9 -9
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +30 -289
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/start.d.ts.map +1 -1
  36. package/dist/commands/start.js +2 -0
  37. package/dist/commands/start.js.map +1 -1
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +131 -8
  41. package/dist/index.js.map +1 -1
  42. package/dist/utils/banner.d.ts +11 -0
  43. package/dist/utils/banner.d.ts.map +1 -0
  44. package/dist/utils/banner.js +47 -0
  45. package/dist/utils/banner.js.map +1 -0
  46. package/package.json +6 -5
@@ -0,0 +1,845 @@
1
+ /**
2
+ * herdctl agent commands
3
+ *
4
+ * Commands for managing installed agents:
5
+ * - herdctl agent add <source> Install an agent from GitHub or local path
6
+ * - herdctl agent list List all discovered agents in the fleet
7
+ * - herdctl agent info <name> Show detailed information about an agent
8
+ * - herdctl agent remove <name> Remove an installed agent
9
+ *
10
+ * The add command orchestrates the full agent installation flow:
11
+ * 1. Parse source specifier (github:user/repo[@ref] or ./local/path)
12
+ * 2. Fetch repository to temp directory
13
+ * 3. Validate agent repository structure
14
+ * 4. Install files to ./agents/<name>/
15
+ * 5. Update herdctl.yaml with agent reference
16
+ * 6. Scan and display required environment variables
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { AGENT_ALREADY_EXISTS,
21
+ // Agent removal
22
+ AGENT_NOT_FOUND, AgentDiscoveryError, AgentInstallError, AgentRemoveError,
23
+ // Fleet config update
24
+ addAgentToFleetConfig, ConfigError, ConfigNotFoundError, createLogger,
25
+ // Agent discovery
26
+ discoverAgents, FleetConfigError,
27
+ // Repository fetching
28
+ fetchRepository, GitHubCloneAuthError, GitHubRepoNotFoundError,
29
+ // Agent info
30
+ getAgentInfo,
31
+ // File installation
32
+ installAgentFiles, isGitHubSource, isLocalSource, LocalPathError,
33
+ // Config loader for fleet-of-fleets support
34
+ loadConfig, NetworkError,
35
+ // Source parsing
36
+ parseSourceSpecifier, RepositoryFetchError, removeAgent, SourceParseError,
37
+ // Environment variable scanning
38
+ scanEnvVariables, stringifySourceSpecifier,
39
+ // Repository validation
40
+ validateRepository, } from "@herdctl/core";
41
+ import { parse as parseYaml } from "yaml";
42
+ const logger = createLogger("cli:agent");
43
+ // =============================================================================
44
+ // Helper Functions
45
+ // =============================================================================
46
+ /**
47
+ * Convert a parsed SourceSpecifier to a FetchSource for the repository fetcher
48
+ */
49
+ function toFetchSource(specifier) {
50
+ if (isGitHubSource(specifier)) {
51
+ return {
52
+ type: "github",
53
+ owner: specifier.owner,
54
+ repo: specifier.repo,
55
+ ref: specifier.ref,
56
+ };
57
+ }
58
+ if (isLocalSource(specifier)) {
59
+ return {
60
+ type: "local",
61
+ path: specifier.path,
62
+ };
63
+ }
64
+ // TypeScript exhaustiveness check
65
+ const _exhaustive = specifier;
66
+ throw new Error(`Unknown source type: ${_exhaustive.type}`);
67
+ }
68
+ /**
69
+ * Convert a parsed SourceSpecifier to an InstallationSource for metadata tracking
70
+ */
71
+ function toInstallationSource(specifier) {
72
+ if (isGitHubSource(specifier)) {
73
+ return {
74
+ type: "github",
75
+ url: `https://github.com/${specifier.owner}/${specifier.repo}`,
76
+ ref: specifier.ref,
77
+ };
78
+ }
79
+ if (isLocalSource(specifier)) {
80
+ return {
81
+ type: "local",
82
+ url: specifier.path,
83
+ };
84
+ }
85
+ // TypeScript exhaustiveness check
86
+ const _exhaustive = specifier;
87
+ throw new Error(`Unknown source type: ${_exhaustive.type}`);
88
+ }
89
+ /**
90
+ * Print validation errors in a user-friendly format
91
+ */
92
+ function printValidationErrors(result) {
93
+ if (result.errors.length > 0) {
94
+ console.log("");
95
+ console.log("Validation errors:");
96
+ for (const error of result.errors) {
97
+ const pathInfo = error.path ? ` (${error.path})` : "";
98
+ console.log(` - ${error.message}${pathInfo}`);
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Print validation warnings in a user-friendly format
104
+ */
105
+ function printValidationWarnings(result) {
106
+ if (result.warnings.length > 0) {
107
+ console.log("");
108
+ console.log("Warnings:");
109
+ for (const warning of result.warnings) {
110
+ const pathInfo = warning.path ? ` (${warning.path})` : "";
111
+ console.log(` - ${warning.message}${pathInfo}`);
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Print environment variables summary
117
+ */
118
+ function printEnvVariables(envResult) {
119
+ if (envResult.variables.length === 0) {
120
+ return;
121
+ }
122
+ console.log("");
123
+ console.log("Environment variables to configure:");
124
+ if (envResult.required.length > 0) {
125
+ console.log(" Required (no defaults):");
126
+ for (const variable of envResult.required) {
127
+ console.log(` ${variable.name}`);
128
+ }
129
+ }
130
+ if (envResult.optional.length > 0) {
131
+ console.log("");
132
+ console.log(" Optional (have defaults):");
133
+ for (const variable of envResult.optional) {
134
+ console.log(` ${variable.name} (default: ${variable.defaultValue})`);
135
+ }
136
+ }
137
+ console.log("");
138
+ console.log("Add these to your .env file, then run: herdctl start");
139
+ }
140
+ /**
141
+ * Handle known error types and print user-friendly messages
142
+ * Returns true if the error was handled, false otherwise
143
+ */
144
+ function handleKnownError(error) {
145
+ if (error instanceof SourceParseError) {
146
+ logger.error(`Invalid source: ${error.message}`);
147
+ return true;
148
+ }
149
+ if (error instanceof GitHubCloneAuthError) {
150
+ logger.error(`Authentication failed: ${error.message}`);
151
+ return true;
152
+ }
153
+ if (error instanceof GitHubRepoNotFoundError) {
154
+ logger.error(`Repository not found: ${error.message}`);
155
+ return true;
156
+ }
157
+ if (error instanceof NetworkError) {
158
+ logger.error(`Network error: ${error.message}`);
159
+ return true;
160
+ }
161
+ if (error instanceof LocalPathError) {
162
+ logger.error(`Local path error: ${error.message}`);
163
+ return true;
164
+ }
165
+ if (error instanceof RepositoryFetchError) {
166
+ logger.error(`Failed to fetch: ${error.message}`);
167
+ return true;
168
+ }
169
+ if (error instanceof AgentInstallError) {
170
+ if (error.code === AGENT_ALREADY_EXISTS) {
171
+ logger.error(`Installation failed: ${error.message}`);
172
+ logger.error("Use --force to overwrite the existing agent.");
173
+ }
174
+ else {
175
+ logger.error(`Installation failed: ${error.message}`);
176
+ }
177
+ return true;
178
+ }
179
+ if (error instanceof FleetConfigError) {
180
+ logger.error(`Config update failed: ${error.message}`);
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ // =============================================================================
186
+ // Main Command
187
+ // =============================================================================
188
+ /**
189
+ * Install an agent from a source specifier
190
+ *
191
+ * Orchestrates the full installation flow:
192
+ * 1. Parse source specifier
193
+ * 2. Fetch repository
194
+ * 3. Validate repository
195
+ * 4. Install files (unless dry-run)
196
+ * 5. Update fleet config (unless dry-run)
197
+ * 6. Scan and display env variables
198
+ * 7. Cleanup temp directory
199
+ *
200
+ * @param source - Source specifier (e.g., "github:user/repo", "./local/path")
201
+ * @param options - Command options
202
+ */
203
+ export async function agentAddCommand(source, options) {
204
+ const cwd = process.cwd();
205
+ const configPath = options.config ? path.resolve(options.config) : path.join(cwd, "herdctl.yaml");
206
+ const { dryRun, force, path: customPath } = options;
207
+ // Determine target base directory
208
+ const targetBaseDir = customPath ? path.dirname(path.resolve(customPath)) : cwd;
209
+ const targetPath = customPath ? path.resolve(customPath) : undefined;
210
+ // ==========================================================================
211
+ // Step 1: Parse source specifier
212
+ // ==========================================================================
213
+ let specifier;
214
+ try {
215
+ specifier = parseSourceSpecifier(source);
216
+ }
217
+ catch (error) {
218
+ handleKnownError(error);
219
+ process.exit(1);
220
+ }
221
+ const sourceStr = stringifySourceSpecifier(specifier);
222
+ console.log(`Fetching ${sourceStr}...`);
223
+ // ==========================================================================
224
+ // Step 2: Fetch repository
225
+ // ==========================================================================
226
+ let fetchResult;
227
+ try {
228
+ const fetchSource = toFetchSource(specifier);
229
+ fetchResult = await fetchRepository(fetchSource);
230
+ }
231
+ catch (error) {
232
+ handleKnownError(error);
233
+ process.exit(1);
234
+ }
235
+ // Ensure cleanup happens even on errors
236
+ try {
237
+ // ==========================================================================
238
+ // Step 3: Validate repository
239
+ // ==========================================================================
240
+ console.log("Validating agent repository...");
241
+ const validationResult = await validateRepository(fetchResult.path);
242
+ // Print warnings regardless of validation result
243
+ printValidationWarnings(validationResult);
244
+ // If there are errors, print them and exit
245
+ if (!validationResult.valid) {
246
+ printValidationErrors(validationResult);
247
+ console.log("");
248
+ logger.error("Validation failed. Cannot install agent.");
249
+ process.exitCode = 1;
250
+ return;
251
+ }
252
+ const agentName = validationResult.agentName;
253
+ // ==========================================================================
254
+ // Step 4: Install files (or describe what would happen)
255
+ // ==========================================================================
256
+ const installSource = toInstallationSource(specifier);
257
+ const effectiveTargetPath = targetPath ?? path.join(cwd, "agents", agentName);
258
+ const relativeInstallPath = path.relative(cwd, effectiveTargetPath);
259
+ if (dryRun) {
260
+ console.log("");
261
+ console.log("Dry run mode - no changes will be made.");
262
+ console.log("");
263
+ console.log(`Would install agent '${agentName}' to ${relativeInstallPath}/`);
264
+ console.log("");
265
+ console.log("Files that would be installed:");
266
+ // List files that would be copied
267
+ const filesToCopy = await listFilesRecursive(fetchResult.path);
268
+ for (const file of filesToCopy) {
269
+ console.log(` ${relativeInstallPath}/${file}`);
270
+ }
271
+ console.log(` ${relativeInstallPath}/workspace/ (created)`);
272
+ console.log("");
273
+ console.log("Config changes:");
274
+ console.log(` herdctl.yaml (would add agent reference)`);
275
+ }
276
+ else {
277
+ console.log(`Installing agent '${agentName}' to ${relativeInstallPath}/...`);
278
+ const installResult = await installAgentFiles({
279
+ sourceDir: fetchResult.path,
280
+ targetBaseDir,
281
+ source: installSource,
282
+ targetPath,
283
+ force,
284
+ });
285
+ // ==========================================================================
286
+ // Step 5: Update fleet config
287
+ // ==========================================================================
288
+ console.log("Updating herdctl.yaml...");
289
+ // Determine the relative path to the agent.yaml for the fleet config
290
+ const agentYamlPath = `./${path.relative(cwd, path.join(installResult.installPath, "agent.yaml"))}`;
291
+ await addAgentToFleetConfig({
292
+ configPath,
293
+ agentPath: agentYamlPath,
294
+ });
295
+ // ==========================================================================
296
+ // Step 6: Scan environment variables
297
+ // ==========================================================================
298
+ const agentYamlFullPath = path.join(installResult.installPath, "agent.yaml");
299
+ const agentYamlContent = fs.readFileSync(agentYamlFullPath, "utf-8");
300
+ const envResult = scanEnvVariables(agentYamlContent);
301
+ // ==========================================================================
302
+ // Print success summary
303
+ // ==========================================================================
304
+ console.log("");
305
+ console.log(`Agent '${agentName}' installed successfully!`);
306
+ console.log("");
307
+ console.log("Files installed:");
308
+ for (const file of installResult.copiedFiles) {
309
+ console.log(` ${relativeInstallPath}/${file}`);
310
+ }
311
+ console.log(` ${relativeInstallPath}/workspace/ (created)`);
312
+ console.log("");
313
+ console.log("Config updated:");
314
+ console.log(" herdctl.yaml (added agent reference)");
315
+ printEnvVariables(envResult);
316
+ }
317
+ }
318
+ catch (error) {
319
+ // Handle known errors from validation, installation, or config update
320
+ if (handleKnownError(error)) {
321
+ process.exitCode = 1;
322
+ return;
323
+ }
324
+ // Re-throw unknown errors
325
+ throw error;
326
+ }
327
+ finally {
328
+ // ==========================================================================
329
+ // Step 7: Cleanup temp directory
330
+ // ==========================================================================
331
+ if (fetchResult) {
332
+ await fetchResult.cleanup();
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * Recursively list files in a directory (excluding .git and node_modules)
338
+ */
339
+ async function listFilesRecursive(dir, relativePath = "") {
340
+ const files = [];
341
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
342
+ const excludedDirs = new Set([".git", "node_modules"]);
343
+ for (const entry of entries) {
344
+ const entryPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
345
+ if (entry.isDirectory()) {
346
+ if (!excludedDirs.has(entry.name)) {
347
+ const subFiles = await listFilesRecursive(path.join(dir, entry.name), entryPath);
348
+ files.push(...subFiles);
349
+ }
350
+ }
351
+ else if (entry.isFile()) {
352
+ files.push(entryPath);
353
+ }
354
+ }
355
+ return files;
356
+ }
357
+ /**
358
+ * Format a date string for display
359
+ */
360
+ function formatDate(isoDate) {
361
+ if (!isoDate) {
362
+ return "-";
363
+ }
364
+ try {
365
+ const date = new Date(isoDate);
366
+ return date.toLocaleDateString("en-US", {
367
+ year: "numeric",
368
+ month: "short",
369
+ day: "numeric",
370
+ });
371
+ }
372
+ catch {
373
+ return "-";
374
+ }
375
+ }
376
+ /**
377
+ * Get source description from agent metadata
378
+ */
379
+ function getSourceDescription(agent) {
380
+ if (!agent.metadata) {
381
+ return "manual";
382
+ }
383
+ const { source } = agent.metadata;
384
+ if (source.type === "github") {
385
+ // Extract owner/repo from URL
386
+ const match = source.url?.match(/github\.com\/([^/]+\/[^/]+)/);
387
+ if (match) {
388
+ return source.ref ? `${match[1]}@${source.ref}` : match[1];
389
+ }
390
+ return source.ref ?? "github";
391
+ }
392
+ if (source.type === "local") {
393
+ return source.url ?? "local";
394
+ }
395
+ return source.type;
396
+ }
397
+ /**
398
+ * Maximum number of agents before switching to summary mode.
399
+ * When total agents exceed this threshold, fleet-level counts are shown
400
+ * instead of individual agent names to avoid overwhelming output.
401
+ */
402
+ export const TREE_AGENT_THRESHOLD = 200;
403
+ /**
404
+ * Build a tree structure from a flat array of resolved agents.
405
+ *
406
+ * Groups agents by their fleetPath hierarchy. Root-level agents (empty fleetPath)
407
+ * go directly under the root node. Sub-fleet agents are nested under their
408
+ * respective fleet nodes.
409
+ */
410
+ export function buildFleetTree(agents, rootName, rootDescription) {
411
+ const root = {
412
+ name: rootName,
413
+ description: rootDescription,
414
+ agents: [],
415
+ children: [],
416
+ };
417
+ for (const agent of agents) {
418
+ if (agent.fleetPath.length === 0) {
419
+ // Root-level agent
420
+ root.agents.push(agent.name);
421
+ }
422
+ else {
423
+ // Walk/create the tree path for this agent
424
+ let current = root;
425
+ for (const fleetName of agent.fleetPath) {
426
+ let child = current.children.find((c) => c.name === fleetName);
427
+ if (!child) {
428
+ child = { name: fleetName, agents: [], children: [] };
429
+ current.children.push(child);
430
+ }
431
+ current = child;
432
+ }
433
+ current.agents.push(agent.name);
434
+ }
435
+ }
436
+ return root;
437
+ }
438
+ /**
439
+ * Render a fleet tree with box-drawing characters.
440
+ *
441
+ * Uses standard Unicode box-drawing characters for the tree structure:
442
+ * - Connector for intermediate items
443
+ * - End connector for last items
444
+ * - Vertical bar for continuing branches
445
+ *
446
+ * @param node - The tree node to render
447
+ * @param prefix - Current indentation prefix
448
+ * @param isLast - Whether this node is the last child of its parent
449
+ * @param isRoot - Whether this is the root node
450
+ * @param summaryMode - When true, show agent counts instead of names
451
+ */
452
+ export function renderFleetTree(node, prefix = "", isLast = true, isRoot = true, summaryMode = false) {
453
+ const lines = [];
454
+ // Render this node's header
455
+ if (isRoot) {
456
+ const desc = node.description ? ` (${node.description})` : "";
457
+ lines.push(`${node.name}${desc}`);
458
+ }
459
+ else {
460
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
461
+ lines.push(`${prefix}${connector}${node.name}`);
462
+ }
463
+ // Determine the prefix for children
464
+ const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
465
+ // Collect all items to render (agents + child fleets)
466
+ const allItems = [];
467
+ // Add child fleets first
468
+ for (const child of node.children) {
469
+ allItems.push({ type: "fleet", node: child });
470
+ }
471
+ // Add agents (or summary)
472
+ if (summaryMode && node.agents.length > 0) {
473
+ allItems.push({ type: "agent", name: `(${node.agents.length} agents)` });
474
+ }
475
+ else {
476
+ for (const agentName of node.agents) {
477
+ allItems.push({ type: "agent", name: agentName });
478
+ }
479
+ }
480
+ // Render each item
481
+ for (let i = 0; i < allItems.length; i++) {
482
+ const item = allItems[i];
483
+ const itemIsLast = i === allItems.length - 1;
484
+ if (item.type === "agent") {
485
+ const connector = itemIsLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
486
+ lines.push(`${childPrefix}${connector}${item.name}`);
487
+ }
488
+ else {
489
+ const subLines = renderFleetTree(item.node, childPrefix, itemIsLast, false, summaryMode);
490
+ lines.push(...subLines);
491
+ }
492
+ }
493
+ return lines;
494
+ }
495
+ /**
496
+ * Check if the fleet config at the given path has sub-fleets defined.
497
+ * Reads the YAML file and checks for a non-empty fleets array.
498
+ */
499
+ function configHasSubFleets(configPath) {
500
+ try {
501
+ const content = fs.readFileSync(configPath, "utf-8");
502
+ const parsed = parseYaml(content);
503
+ return (parsed !== null &&
504
+ typeof parsed === "object" &&
505
+ Array.isArray(parsed.fleets) &&
506
+ parsed.fleets.length > 0);
507
+ }
508
+ catch {
509
+ return false;
510
+ }
511
+ }
512
+ /**
513
+ * List all agents in the fleet
514
+ *
515
+ * Discovers agents from the fleet configuration and displays them.
516
+ * When sub-fleets exist, displays a tree view showing agents grouped by fleet hierarchy.
517
+ * When no sub-fleets exist, displays a flat table (original behavior).
518
+ *
519
+ * If total agents across the hierarchy exceed 200, shows fleet-level summary counts
520
+ * instead of individual agent names.
521
+ *
522
+ * @param options - Command options
523
+ */
524
+ export async function agentListCommand(options) {
525
+ const cwd = process.cwd();
526
+ const configPath = options.config ? path.resolve(options.config) : path.join(cwd, "herdctl.yaml");
527
+ // Check if this fleet has sub-fleets for the tree view
528
+ const hasSubFleets = configHasSubFleets(configPath);
529
+ if (hasSubFleets) {
530
+ // Use loadConfig for full fleet-of-fleets resolution
531
+ try {
532
+ const resolvedConfig = await loadConfig(configPath);
533
+ const allAgents = resolvedConfig.agents;
534
+ if (options.json) {
535
+ // Output the full resolved agents with fleet hierarchy info
536
+ const jsonOutput = allAgents.map((a) => ({
537
+ name: a.name,
538
+ qualifiedName: a.qualifiedName,
539
+ fleetPath: a.fleetPath,
540
+ configPath: a.configPath,
541
+ description: a.description,
542
+ }));
543
+ console.log(JSON.stringify(jsonOutput, null, 2));
544
+ return;
545
+ }
546
+ if (allAgents.length === 0) {
547
+ console.log("No agents found across fleet hierarchy.");
548
+ console.log("");
549
+ console.log("To add an agent, run:");
550
+ console.log(" herdctl agent add <source> Install from GitHub or local path");
551
+ console.log(" herdctl init agent <name> Create a new agent manually");
552
+ return;
553
+ }
554
+ // Build and render the tree
555
+ const rootName = resolvedConfig.fleet.fleet?.name ?? "fleet";
556
+ const rootDescription = resolvedConfig.fleet.fleet?.description;
557
+ const tree = buildFleetTree(allAgents, rootName, rootDescription);
558
+ const summaryMode = allAgents.length > TREE_AGENT_THRESHOLD;
559
+ const treeLines = renderFleetTree(tree, "", true, true, summaryMode);
560
+ console.log("");
561
+ for (const line of treeLines) {
562
+ console.log(line);
563
+ }
564
+ console.log("");
565
+ console.log(`Total: ${allAgents.length} agent${allAgents.length === 1 ? "" : "s"} across fleet hierarchy`);
566
+ }
567
+ catch (error) {
568
+ if (error instanceof ConfigNotFoundError || error instanceof ConfigError) {
569
+ logger.error(`Config error: ${error.message}`);
570
+ process.exit(1);
571
+ }
572
+ throw error;
573
+ }
574
+ }
575
+ else {
576
+ // Original flat table behavior when no sub-fleets
577
+ try {
578
+ const result = await discoverAgents({ configPath });
579
+ if (options.json) {
580
+ console.log(JSON.stringify(result.agents, null, 2));
581
+ return;
582
+ }
583
+ if (result.agents.length === 0) {
584
+ console.log("No agents found in fleet configuration.");
585
+ console.log("");
586
+ console.log("To add an agent, run:");
587
+ console.log(" herdctl agent add <source> Install from GitHub or local path");
588
+ console.log(" herdctl init agent <name> Create a new agent manually");
589
+ return;
590
+ }
591
+ // Print table header
592
+ console.log("");
593
+ console.log("Agents in fleet:");
594
+ console.log("");
595
+ // Calculate column widths
596
+ const nameWidth = Math.max(4, ...result.agents.map((a) => a.name.length));
597
+ const sourceWidth = Math.max(6, ...result.agents.map((a) => getSourceDescription(a).length));
598
+ const versionWidth = Math.max(7, ...result.agents.map((a) => (a.version ?? "-").length));
599
+ const dateWidth = 12;
600
+ const statusWidth = 9;
601
+ // Print header row
602
+ const header = [
603
+ "Name".padEnd(nameWidth),
604
+ "Source".padEnd(sourceWidth),
605
+ "Version".padEnd(versionWidth),
606
+ "Installed".padEnd(dateWidth),
607
+ "Status".padEnd(statusWidth),
608
+ ].join(" ");
609
+ console.log(header);
610
+ console.log("-".repeat(header.length));
611
+ // Print each agent
612
+ for (const agent of result.agents) {
613
+ const row = [
614
+ agent.name.padEnd(nameWidth),
615
+ getSourceDescription(agent).padEnd(sourceWidth),
616
+ (agent.version ?? "-").padEnd(versionWidth),
617
+ formatDate(agent.metadata?.installed_at).padEnd(dateWidth),
618
+ (agent.installed ? "installed" : "manual").padEnd(statusWidth),
619
+ ].join(" ");
620
+ console.log(row);
621
+ }
622
+ console.log("");
623
+ console.log(`Total: ${result.agents.length} agent${result.agents.length === 1 ? "" : "s"}`);
624
+ }
625
+ catch (error) {
626
+ if (error instanceof AgentDiscoveryError) {
627
+ logger.error(`Discovery failed: ${error.message}`);
628
+ process.exit(1);
629
+ }
630
+ throw error;
631
+ }
632
+ }
633
+ }
634
+ /**
635
+ * Get detailed information about a specific agent
636
+ *
637
+ * Shows comprehensive agent information including:
638
+ * - Basic info (name, description, status)
639
+ * - Source and installation details
640
+ * - Environment variables
641
+ * - Schedules
642
+ * - Files in the agent directory
643
+ *
644
+ * @param name - Agent name to look up
645
+ * @param options - Command options
646
+ */
647
+ export async function agentInfoCommand(name, options) {
648
+ const cwd = process.cwd();
649
+ const configPath = options.config ? path.resolve(options.config) : path.join(cwd, "herdctl.yaml");
650
+ try {
651
+ const info = await getAgentInfo({ name, configPath });
652
+ if (!info) {
653
+ logger.error(`Agent '${name}' not found in fleet configuration.`);
654
+ logger.error("Run 'herdctl agent list' to see available agents.");
655
+ process.exit(1);
656
+ }
657
+ if (options.json) {
658
+ console.log(JSON.stringify(info, null, 2));
659
+ return;
660
+ }
661
+ // Print formatted output
662
+ printAgentInfo(info);
663
+ }
664
+ catch (error) {
665
+ if (error instanceof AgentDiscoveryError) {
666
+ logger.error(`Discovery failed: ${error.message}`);
667
+ process.exit(1);
668
+ }
669
+ throw error;
670
+ }
671
+ }
672
+ /**
673
+ * Format and print agent info to console
674
+ */
675
+ function printAgentInfo(info) {
676
+ console.log("");
677
+ console.log(`Agent: ${info.name}`);
678
+ if (info.description) {
679
+ console.log(`Description: ${info.description}`);
680
+ }
681
+ // Status line
682
+ if (info.installed) {
683
+ const sourceType = info.metadata?.source.type ?? "unknown";
684
+ const sourceLabel = sourceType === "github" ? "GitHub" : sourceType === "local" ? "local path" : sourceType;
685
+ console.log(`Status: Installed (via ${sourceLabel})`);
686
+ }
687
+ else {
688
+ console.log("Status: Manual (not installed via herdctl)");
689
+ }
690
+ // Source info for installed agents
691
+ if (info.metadata?.source.url) {
692
+ console.log(`Source: ${info.metadata.source.url}`);
693
+ }
694
+ if (info.version) {
695
+ console.log(`Version: ${info.version}`);
696
+ }
697
+ if (info.metadata?.installed_at) {
698
+ console.log(`Installed: ${info.metadata.installed_at}`);
699
+ }
700
+ // Schedules
701
+ if (info.schedules && Object.keys(info.schedules).length > 0) {
702
+ console.log("");
703
+ console.log("Schedules:");
704
+ for (const [scheduleName, scheduleConfig] of Object.entries(info.schedules)) {
705
+ const scheduleDesc = formatScheduleDescription(scheduleConfig);
706
+ console.log(` ${scheduleName}: ${scheduleDesc}`);
707
+ }
708
+ }
709
+ // Environment variables
710
+ if (info.envVariables) {
711
+ console.log("");
712
+ console.log("Environment Variables:");
713
+ if (info.envVariables.required.length > 0) {
714
+ console.log(" Required:");
715
+ for (const variable of info.envVariables.required) {
716
+ console.log(` ${variable.name}`);
717
+ }
718
+ }
719
+ if (info.envVariables.optional.length > 0) {
720
+ if (info.envVariables.required.length > 0) {
721
+ console.log("");
722
+ }
723
+ console.log(" Optional:");
724
+ for (const variable of info.envVariables.optional) {
725
+ console.log(` ${variable.name} (default: ${variable.defaultValue})`);
726
+ }
727
+ }
728
+ }
729
+ // Files
730
+ if (info.files.length > 0) {
731
+ console.log("");
732
+ console.log("Files:");
733
+ for (const file of info.files) {
734
+ console.log(` ${file}`);
735
+ }
736
+ }
737
+ // Workspace
738
+ console.log("");
739
+ if (info.hasWorkspace) {
740
+ const relativePath = path.relative(process.cwd(), info.path);
741
+ console.log(`Workspace: ${relativePath}/workspace/`);
742
+ }
743
+ else {
744
+ console.log("Workspace: (not created)");
745
+ }
746
+ console.log("");
747
+ }
748
+ /**
749
+ * Format a schedule configuration for display
750
+ */
751
+ function formatScheduleDescription(config) {
752
+ if (!config || typeof config !== "object") {
753
+ return "unknown";
754
+ }
755
+ const scheduleObj = config;
756
+ const scheduleType = (scheduleObj.type ?? scheduleObj.cron) ? "cron" : "unknown";
757
+ if (scheduleType === "cron" && scheduleObj.cron) {
758
+ return `cron (${scheduleObj.cron})`;
759
+ }
760
+ if (scheduleObj.interval) {
761
+ return `interval (${scheduleObj.interval})`;
762
+ }
763
+ return String(scheduleType);
764
+ }
765
+ // =============================================================================
766
+ // Agent Remove Command
767
+ // =============================================================================
768
+ /**
769
+ * Remove an agent from the fleet
770
+ *
771
+ * This command:
772
+ * 1. Finds the agent by name in the fleet configuration
773
+ * 2. Deletes the agent directory (optionally preserving workspace)
774
+ * 3. Removes the agent reference from herdctl.yaml
775
+ * 4. Reports environment variables that were used (for cleanup reference)
776
+ *
777
+ * @param name - Agent name to remove
778
+ * @param options - Command options
779
+ */
780
+ export async function agentRemoveCommand(name, options) {
781
+ const cwd = process.cwd();
782
+ const configPath = options.config ? path.resolve(options.config) : path.join(cwd, "herdctl.yaml");
783
+ const { keepWorkspace = false } = options;
784
+ try {
785
+ console.log(`Removing agent '${name}'...`);
786
+ const result = await removeAgent({
787
+ name,
788
+ configPath,
789
+ keepWorkspace,
790
+ });
791
+ // Print what was removed
792
+ const relativePath = path.relative(cwd, result.removedPath);
793
+ if (result.filesRemoved) {
794
+ if (result.workspacePreserved) {
795
+ console.log(`Deleted ${relativePath}/ (workspace preserved)`);
796
+ }
797
+ else {
798
+ console.log(`Deleted ${relativePath}/`);
799
+ }
800
+ }
801
+ if (result.configUpdated) {
802
+ console.log("Updated herdctl.yaml (removed agent reference)");
803
+ }
804
+ // Print env variables summary if any were found
805
+ if (result.envVariables && result.envVariables.variables.length > 0) {
806
+ console.log("");
807
+ console.log("This agent used the following environment variables:");
808
+ if (result.envVariables.required.length > 0) {
809
+ console.log(" Required:");
810
+ for (const variable of result.envVariables.required) {
811
+ console.log(` ${variable.name}`);
812
+ }
813
+ }
814
+ if (result.envVariables.optional.length > 0) {
815
+ if (result.envVariables.required.length > 0) {
816
+ console.log("");
817
+ }
818
+ console.log(" Optional:");
819
+ for (const variable of result.envVariables.optional) {
820
+ console.log(` ${variable.name} (default: ${variable.defaultValue})`);
821
+ }
822
+ }
823
+ console.log("");
824
+ console.log("You may want to remove these from your .env file.");
825
+ }
826
+ }
827
+ catch (error) {
828
+ if (error instanceof AgentRemoveError) {
829
+ if (error.code === AGENT_NOT_FOUND) {
830
+ logger.error(`Agent '${name}' not found in fleet configuration.`);
831
+ logger.error("Run 'herdctl agent list' to see available agents.");
832
+ }
833
+ else {
834
+ logger.error(`Removal failed: ${error.message}`);
835
+ }
836
+ process.exit(1);
837
+ }
838
+ if (error instanceof AgentDiscoveryError) {
839
+ logger.error(`Discovery failed: ${error.message}`);
840
+ process.exit(1);
841
+ }
842
+ throw error;
843
+ }
844
+ }
845
+ //# sourceMappingURL=agent.js.map