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.
- package/dist/commands/__tests__/agent.test.d.ts +2 -0
- package/dist/commands/__tests__/agent.test.d.ts.map +1 -0
- package/dist/commands/__tests__/agent.test.js +1461 -0
- package/dist/commands/__tests__/agent.test.js.map +1 -0
- package/dist/commands/__tests__/init-agent.test.d.ts +2 -0
- package/dist/commands/__tests__/init-agent.test.d.ts.map +1 -0
- package/dist/commands/__tests__/init-agent.test.js +363 -0
- package/dist/commands/__tests__/init-agent.test.js.map +1 -0
- package/dist/commands/__tests__/init-fleet.test.d.ts +2 -0
- package/dist/commands/__tests__/init-fleet.test.d.ts.map +1 -0
- package/dist/commands/__tests__/init-fleet.test.js +154 -0
- package/dist/commands/__tests__/init-fleet.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.js +43 -213
- package/dist/commands/__tests__/init.test.js.map +1 -1
- package/dist/commands/agent.d.ts +143 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +845 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/init-agent.d.ts +22 -0
- package/dist/commands/init-agent.d.ts.map +1 -0
- package/dist/commands/init-agent.js +273 -0
- package/dist/commands/init-agent.js.map +1 -0
- package/dist/commands/init-fleet.d.ts +13 -0
- package/dist/commands/init-fleet.d.ts.map +1 -0
- package/dist/commands/init-fleet.js +91 -0
- package/dist/commands/init-fleet.js.map +1 -0
- package/dist/commands/init-utils.d.ts +8 -0
- package/dist/commands/init-utils.d.ts.map +1 -0
- package/dist/commands/init-utils.js +24 -0
- package/dist/commands/init-utils.js.map +1 -0
- package/dist/commands/init.d.ts +9 -9
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +30 -289
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +2 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -8
- package/dist/index.js.map +1 -1
- package/dist/utils/banner.d.ts +11 -0
- package/dist/utils/banner.d.ts.map +1 -0
- package/dist/utils/banner.js +47 -0
- package/dist/utils/banner.js.map +1 -0
- 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
|