@eve-horizon/cli 0.2.12 → 0.2.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eve-horizon/cli",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "Eve Horizon CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,14 +24,15 @@
24
24
  "node": ">=22.0.0"
25
25
  },
26
26
  "dependencies": {
27
- "yaml": "^2.5.1",
28
- "@eve/shared": "0.0.1"
27
+ "yaml": "^2.5.1"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@types/node": "^22.0.0",
32
- "typescript": "^5.7.0"
31
+ "esbuild": "^0.27.3",
32
+ "typescript": "^5.7.0",
33
+ "@eve/shared": "0.0.1"
33
34
  },
34
35
  "scripts": {
35
- "build": "tsc -p tsconfig.json"
36
+ "build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node22 --outfile=dist/index.js --format=cjs --external:yaml"
36
37
  }
37
38
  }
@@ -1,81 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleAdmin = handleAdmin;
4
- const args_1 = require("../lib/args");
5
- const client_1 = require("../lib/client");
6
- const output_1 = require("../lib/output");
7
- async function handleAdmin(subcommand, positionals, flags, context) {
8
- const json = Boolean(flags.json);
9
- switch (subcommand) {
10
- case 'invite': {
11
- const githubUsername = (0, args_1.getStringFlag)(flags, ['github']);
12
- const email = (0, args_1.getStringFlag)(flags, ['email']);
13
- const role = (0, args_1.getStringFlag)(flags, ['role']) ?? 'member';
14
- const orgId = (0, args_1.getStringFlag)(flags, ['org']) ?? context.orgId;
15
- if (!email) {
16
- throw new Error('Usage: eve admin invite --email <email> [--github <username>] [--role <role>] [--org <org_id>]');
17
- }
18
- if (!['owner', 'admin', 'member'].includes(role)) {
19
- throw new Error(`Invalid role: ${role}. Must be one of: owner, admin, member`);
20
- }
21
- if (!orgId) {
22
- throw new Error('No org specified. Use --org <org_id> or set a default org in your profile.');
23
- }
24
- const results = {
25
- keys_registered: 0,
26
- identities: [],
27
- };
28
- // Add user to org first - this creates the user if they don't exist
29
- if (orgId) {
30
- const membership = await (0, client_1.requestJson)(context, `/orgs/${orgId}/members`, {
31
- method: 'POST',
32
- body: {
33
- email,
34
- role,
35
- },
36
- });
37
- results.membership = membership;
38
- }
39
- // Fetch and register GitHub SSH keys if username provided
40
- // Now that user exists (created via org membership), identity registration will work
41
- if (githubUsername) {
42
- const keys = await fetchGitHubKeys(githubUsername);
43
- if (keys.length === 0) {
44
- throw new Error(`No SSH keys found for GitHub user: ${githubUsername}`);
45
- }
46
- for (const publicKey of keys) {
47
- const identity = await (0, client_1.requestJson)(context, '/auth/identities', {
48
- method: 'POST',
49
- body: {
50
- email,
51
- public_key: publicKey,
52
- label: `github-${githubUsername}`,
53
- },
54
- });
55
- results.identities.push(identity);
56
- results.keys_registered += 1;
57
- }
58
- }
59
- const summary = [
60
- `Invited ${email}`,
61
- results.keys_registered > 0 ? `${results.keys_registered} SSH key(s) registered` : null,
62
- results.membership ? `Added to ${orgId} as ${role}` : null,
63
- ].filter(Boolean).join(', ');
64
- (0, output_1.outputJson)(results, json, `+ ${summary}`);
65
- return;
66
- }
67
- default:
68
- throw new Error('Usage: eve admin <invite>');
69
- }
70
- }
71
- async function fetchGitHubKeys(username) {
72
- const response = await fetch(`https://github.com/${username}.keys`);
73
- if (!response.ok) {
74
- if (response.status === 404) {
75
- throw new Error(`GitHub user not found: ${username}`);
76
- }
77
- throw new Error(`Failed to fetch GitHub keys: HTTP ${response.status}`);
78
- }
79
- const text = await response.text();
80
- return text.trim().split('\n').filter(k => k.length > 0);
81
- }
@@ -1,445 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleAgents = handleAgents;
4
- const node_child_process_1 = require("node:child_process");
5
- const node_fs_1 = require("node:fs");
6
- const node_path_1 = require("node:path");
7
- const yaml_1 = require("yaml");
8
- const shared_1 = require("@eve/shared");
9
- const harness_capabilities_js_1 = require("../lib/harness-capabilities.js");
10
- const args_1 = require("../lib/args");
11
- const client_1 = require("../lib/client");
12
- const output_1 = require("../lib/output");
13
- const git_js_1 = require("../lib/git.js");
14
- function readYamlFile(filePath) {
15
- const raw = (0, node_fs_1.readFileSync)(filePath, 'utf-8');
16
- const parsed = (0, yaml_1.parse)(raw);
17
- if (!parsed || typeof parsed !== 'object') {
18
- throw new Error(`Invalid YAML in ${filePath}`);
19
- }
20
- return parsed;
21
- }
22
- function resolveAgentsConfigPaths(repoRoot, manifest) {
23
- const xEve = manifest['x-eve'] ||
24
- manifest['x_eve'] ||
25
- {};
26
- const agentsBlock = xEve['agents'] || {};
27
- const chatBlock = xEve['chat'] || {};
28
- const manifestChat = manifest['chat'] || {};
29
- const agentsPath = (0, node_path_1.resolve)(repoRoot, pickString(agentsBlock.config_path) ?? 'agents/agents.yaml');
30
- const teamsPath = (0, node_path_1.resolve)(repoRoot, pickString(agentsBlock.teams_path) ?? 'agents/teams.yaml');
31
- const chatPath = (0, node_path_1.resolve)(repoRoot, pickString(chatBlock.config_path) ?? pickString(manifestChat.config_path) ?? 'agents/chat.yaml');
32
- return { agentsPath, teamsPath, chatPath };
33
- }
34
- function pickString(value) {
35
- return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
36
- }
37
- function ensureFileExists(path, label) {
38
- if (!(0, node_fs_1.existsSync)(path)) {
39
- throw new Error(`Missing ${label} at ${path}. Update manifest config_path or add the file.`);
40
- }
41
- return path;
42
- }
43
- function isLocalApiUrl(apiUrl) {
44
- try {
45
- const url = new URL(apiUrl);
46
- const host = url.hostname.toLowerCase();
47
- return (host === 'localhost' ||
48
- host === '127.0.0.1' ||
49
- host === '0.0.0.0' ||
50
- host.endsWith('.lvh.me') ||
51
- host.endsWith('.localhost'));
52
- }
53
- catch {
54
- return false;
55
- }
56
- }
57
- function loadAgentsConfig(repoRoot) {
58
- const eveDir = (0, node_path_1.join)(repoRoot, '.eve');
59
- const manifestPath = (0, node_path_1.join)(eveDir, 'manifest.yaml');
60
- if ((0, node_fs_1.existsSync)(manifestPath)) {
61
- const manifest = readYamlFile(manifestPath);
62
- const xEve = manifest['x-eve'] ||
63
- manifest['x_eve'] ||
64
- {};
65
- const policy = xEve['agents'] || null;
66
- const defaults = xEve['defaults'] || null;
67
- return { source: { type: 'manifest', path: manifestPath }, policy, manifest_defaults: defaults };
68
- }
69
- return { source: { type: 'none' }, policy: null };
70
- }
71
- async function resolvePacksAndMerge(repoRoot, manifest, projectSlug) {
72
- const xEve = (manifest['x-eve'] ?? manifest['x_eve']);
73
- const packs = (xEve?.packs ?? []);
74
- if (packs.length === 0)
75
- return null;
76
- console.log(`Resolving ${packs.length} pack(s)...`);
77
- // 1. Resolve all packs
78
- const resolvedPacks = [];
79
- for (const entry of packs) {
80
- const resolved = await (0, shared_1.resolvePack)(entry, projectSlug, repoRoot);
81
- resolvedPacks.push(resolved);
82
- console.log(` ✓ ${resolved.id} (${resolved.skillPaths.length} skills)`);
83
- }
84
- // 2. Check for agent ID collisions across packs
85
- const seenAgentIds = new Map();
86
- for (const pack of resolvedPacks) {
87
- for (const agentId of Object.keys(pack.agents)) {
88
- if (seenAgentIds.has(agentId)) {
89
- throw new Error(`Agent ID collision: "${agentId}" defined in both pack "${seenAgentIds.get(agentId)}" and "${pack.id}". ` +
90
- `Agent IDs must be unique across all packs.`);
91
- }
92
- seenAgentIds.set(agentId, pack.id);
93
- }
94
- }
95
- // 3. Merge pack bases in listed order
96
- let mergedAgents = {};
97
- let mergedTeams = {};
98
- let mergedChat = { routes: [] };
99
- const xEveFragments = [];
100
- for (const pack of resolvedPacks) {
101
- mergedAgents = (0, shared_1.mergeMapConfig)(mergedAgents, pack.agents);
102
- mergedTeams = (0, shared_1.mergeMapConfig)(mergedTeams, pack.teams);
103
- if (pack.chat) {
104
- mergedChat = (0, shared_1.mergeChatConfig)(mergedChat, pack.chat);
105
- }
106
- if (pack.xEve) {
107
- xEveFragments.push(pack.xEve);
108
- }
109
- }
110
- // 4. Load project overlay files and merge on top
111
- const configPaths = resolveAgentsConfigPaths(repoRoot, manifest);
112
- if ((0, node_fs_1.existsSync)(configPaths.agentsPath)) {
113
- const projectAgents = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(configPaths.agentsPath, 'utf-8')) ?? {};
114
- mergedAgents = (0, shared_1.mergeMapConfig)(mergedAgents, projectAgents);
115
- }
116
- if ((0, node_fs_1.existsSync)(configPaths.teamsPath)) {
117
- const projectTeams = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(configPaths.teamsPath, 'utf-8')) ?? {};
118
- mergedTeams = (0, shared_1.mergeMapConfig)(mergedTeams, projectTeams);
119
- }
120
- if ((0, node_fs_1.existsSync)(configPaths.chatPath)) {
121
- const projectChat = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(configPaths.chatPath, 'utf-8')) ?? {};
122
- mergedChat = (0, shared_1.mergeChatConfig)(mergedChat, projectChat);
123
- }
124
- // 5. Merge x-eve (strip packs + install_agents from project overlay)
125
- const projectXEve = (xEve ?? {});
126
- const { packs: _p, install_agents: _ia, ...projectXEveRest } = projectXEve;
127
- (0, shared_1.mergeXEve)(xEveFragments, projectXEveRest);
128
- // 6. Validate effective config
129
- validateEffectiveConfig(mergedAgents, mergedTeams, mergedChat);
130
- // 7. Write lockfile
131
- const lockfile = {
132
- resolved_at: new Date().toISOString(),
133
- project_slug: projectSlug,
134
- packs: resolvedPacks.map((p) => ({
135
- id: p.id,
136
- source: p.source,
137
- ref: p.ref,
138
- pack_version: 1,
139
- })),
140
- effective: {
141
- agents_count: Object.keys((0, shared_1.extractInnerMap)(mergedAgents, 'agents')).length,
142
- teams_count: Object.keys((0, shared_1.extractInnerMap)(mergedTeams, 'teams')).length,
143
- routes_count: (mergedChat.routes ?? []).length,
144
- profiles_count: 0,
145
- agents_hash: simpleHash(JSON.stringify(mergedAgents)),
146
- teams_hash: simpleHash(JSON.stringify(mergedTeams)),
147
- chat_hash: simpleHash(JSON.stringify(mergedChat)),
148
- },
149
- };
150
- const eveDir = (0, node_path_1.join)(repoRoot, '.eve');
151
- (0, node_fs_1.mkdirSync)(eveDir, { recursive: true });
152
- const lockfilePath = (0, node_path_1.join)(eveDir, 'packs.lock.yaml');
153
- (0, node_fs_1.writeFileSync)(lockfilePath, (0, yaml_1.stringify)(lockfile), 'utf-8');
154
- console.log(` ✓ Lockfile written: .eve/packs.lock.yaml`);
155
- // 8. Return effective config as YAML strings
156
- const packRefs = resolvedPacks.map((p) => ({ id: p.id, source: p.source, ref: p.ref }));
157
- return {
158
- agentsYaml: (0, yaml_1.stringify)(mergedAgents),
159
- teamsYaml: (0, yaml_1.stringify)(mergedTeams),
160
- chatYaml: (0, yaml_1.stringify)(mergedChat),
161
- packRefs,
162
- };
163
- }
164
- function validateEffectiveConfig(agents, teams, chat) {
165
- // Extract inner maps (handles both nested `{ version: 1, agents: { ... } }` and flat formats)
166
- const agentsMap = (0, shared_1.extractInnerMap)(agents, 'agents');
167
- const teamsMap = (0, shared_1.extractInnerMap)(teams, 'teams');
168
- // Check slug uniqueness
169
- const slugs = new Set();
170
- for (const [, entry] of Object.entries(agentsMap)) {
171
- if (typeof entry !== 'object' || entry === null)
172
- continue;
173
- const agent = entry;
174
- const slug = agent.slug;
175
- if (slug) {
176
- if (slugs.has(slug)) {
177
- throw new Error(`Duplicate agent slug "${slug}" in effective config. Slugs must be unique.`);
178
- }
179
- slugs.add(slug);
180
- }
181
- }
182
- // Check team references
183
- const agentIds = new Set(Object.keys(agentsMap));
184
- for (const [teamId, entry] of Object.entries(teamsMap)) {
185
- if (typeof entry !== 'object' || entry === null)
186
- continue;
187
- const team = entry;
188
- const lead = team.lead;
189
- if (lead && !agentIds.has(lead)) {
190
- throw new Error(`Team "${teamId}" references unknown agent "${lead}" as lead.`);
191
- }
192
- const members = (team.members ?? []);
193
- for (const member of members) {
194
- if (!agentIds.has(member)) {
195
- throw new Error(`Team "${teamId}" references unknown agent "${member}" as member.`);
196
- }
197
- }
198
- }
199
- // Check route targets (supports "team:name" and "agent:name" prefixes)
200
- const teamIds = new Set(Object.keys(teamsMap));
201
- const routes = (chat.routes ?? []);
202
- for (const route of routes) {
203
- if (!route.target)
204
- continue;
205
- let targetId = route.target;
206
- if (targetId.startsWith('team:')) {
207
- targetId = targetId.slice(5);
208
- if (!teamIds.has(targetId)) {
209
- throw new Error(`Route "${route.id}" targets unknown team "${route.target}".`);
210
- }
211
- }
212
- else if (targetId.startsWith('agent:')) {
213
- targetId = targetId.slice(6);
214
- if (!agentIds.has(targetId)) {
215
- throw new Error(`Route "${route.id}" targets unknown agent "${route.target}".`);
216
- }
217
- }
218
- else if (!agentIds.has(targetId) && !teamIds.has(targetId)) {
219
- throw new Error(`Route "${route.id}" targets unknown agent/team "${route.target}".`);
220
- }
221
- }
222
- }
223
- function simpleHash(input) {
224
- let hash = 0;
225
- for (let i = 0; i < input.length; i++) {
226
- const char = input.charCodeAt(i);
227
- hash = ((hash << 5) - hash) + char;
228
- hash = hash & hash; // Convert to 32bit integer
229
- }
230
- return Math.abs(hash).toString(16).padStart(8, '0');
231
- }
232
- async function handleAgents(subcommand, positionals, flags, context) {
233
- const command = subcommand ?? 'config';
234
- const json = Boolean(flags.json);
235
- const includeHarnesses = !((0, args_1.getBooleanFlag)(flags, ['no-harnesses']) ?? false);
236
- const repoRoot = (0, node_path_1.resolve)((0, args_1.getStringFlag)(flags, ['repo-dir', 'repo_dir', 'dir', 'path']) ?? process.cwd());
237
- switch (command) {
238
- case 'config': {
239
- const result = loadAgentsConfig(repoRoot);
240
- const response = {
241
- repo_root: repoRoot,
242
- source: result.source,
243
- policy: result.policy,
244
- };
245
- if (result.manifest_defaults) {
246
- response.manifest_defaults = result.manifest_defaults;
247
- }
248
- if (includeHarnesses) {
249
- const harnesses = await (0, client_1.requestJson)(context, '/harnesses');
250
- response.harnesses = harnesses.data;
251
- response.capabilities = harness_capabilities_js_1.HARNESS_CAPABILITIES;
252
- }
253
- if (json) {
254
- (0, output_1.outputJson)(response, json);
255
- return;
256
- }
257
- console.log(`Agents config source: ${result.source.type}`);
258
- if ('path' in result.source) {
259
- console.log(`Path: ${result.source.path}`);
260
- }
261
- if (!result.policy) {
262
- console.log('No policy found. Add x-eve.agents to .eve/manifest.yaml.');
263
- }
264
- else {
265
- const profiles = result.policy.profiles || {};
266
- const profileNames = Object.keys(profiles);
267
- console.log(`Profiles: ${profileNames.length ? profileNames.join(', ') : 'none'}`);
268
- }
269
- if (includeHarnesses) {
270
- const harnesses = response.harnesses;
271
- if (harnesses?.length) {
272
- const ready = harnesses.filter((h) => h.auth.available).length;
273
- console.log(`Harnesses: ${harnesses.length} (${ready} ready)`);
274
- }
275
- }
276
- return;
277
- }
278
- case 'sync': {
279
- const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
280
- if (!projectId) {
281
- throw new Error('Missing project id. Provide --project or set a profile default.');
282
- }
283
- const ref = (0, args_1.getStringFlag)(flags, ['ref']);
284
- const local = (0, args_1.getBooleanFlag)(flags, ['local']) ?? false;
285
- const allowDirty = (0, args_1.getBooleanFlag)(flags, ['allow-dirty', 'allow_dirty']) ?? false;
286
- const forceNonlocal = (0, args_1.getBooleanFlag)(flags, ['force-nonlocal', 'force_nonlocal']) ?? false;
287
- if (local && ref) {
288
- throw new Error('Use either --local or --ref, not both.');
289
- }
290
- if (!local && !ref) {
291
- throw new Error('Missing --ref. Use --ref <sha|branch> or --local for dev sync.');
292
- }
293
- if (local && !forceNonlocal && !isLocalApiUrl(context.apiUrl)) {
294
- throw new Error(`--local sync is only allowed for local API URLs (localhost or *.lvh.me). ` +
295
- `Current API: ${context.apiUrl}. Use --force-nonlocal to override.`);
296
- }
297
- const gitRoot = (0, git_js_1.getGitRoot)(repoRoot);
298
- if (!gitRoot) {
299
- throw new Error('Not a git repository. Run from the repo root or pass --repo-dir <path>.');
300
- }
301
- const dirty = (0, git_js_1.isGitDirty)(gitRoot);
302
- if (dirty && !allowDirty) {
303
- throw new Error('Working tree is dirty. Commit changes or pass --allow-dirty to sync anyway.');
304
- }
305
- const manifestPath = (0, node_path_1.join)(repoRoot, '.eve', 'manifest.yaml');
306
- if (!(0, node_fs_1.existsSync)(manifestPath)) {
307
- throw new Error(`Missing manifest at ${manifestPath}. Expected .eve/manifest.yaml.`);
308
- }
309
- const manifest = readYamlFile(manifestPath);
310
- // Pack resolution: if packs exist, resolve and merge before posting
311
- const projectSlug = manifest.project ?? projectId.replace(/^proj_/, '');
312
- const packResult = await resolvePacksAndMerge(repoRoot, manifest, projectSlug);
313
- let agentsYaml;
314
- let teamsYaml;
315
- let chatYaml;
316
- let packRefs;
317
- if (packResult) {
318
- agentsYaml = packResult.agentsYaml;
319
- teamsYaml = packResult.teamsYaml;
320
- chatYaml = packResult.chatYaml;
321
- packRefs = packResult.packRefs;
322
- }
323
- else {
324
- const configPaths = resolveAgentsConfigPaths(repoRoot, manifest);
325
- agentsYaml = (0, node_fs_1.readFileSync)(ensureFileExists(configPaths.agentsPath, 'agents config'), 'utf-8');
326
- teamsYaml = (0, node_fs_1.readFileSync)(ensureFileExists(configPaths.teamsPath, 'teams config'), 'utf-8');
327
- chatYaml = (0, node_fs_1.readFileSync)(ensureFileExists(configPaths.chatPath, 'chat config'), 'utf-8');
328
- }
329
- let gitSha;
330
- let branch;
331
- let gitRef = ref ?? 'local';
332
- if (ref) {
333
- gitSha = await (0, git_js_1.resolveGitRef)(context, projectId, ref, repoRoot);
334
- branch = (0, git_js_1.resolveGitBranch)(gitRoot, ref) ?? undefined;
335
- }
336
- else {
337
- gitSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd: gitRoot, encoding: 'utf-8' }).trim();
338
- branch = (0, git_js_1.getGitBranch)(gitRoot) ?? undefined;
339
- }
340
- if (dirty) {
341
- gitRef = `dirty:${gitRef}`;
342
- }
343
- const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/agents/sync`, {
344
- method: 'POST',
345
- body: {
346
- agents_yaml: agentsYaml,
347
- teams_yaml: teamsYaml,
348
- chat_yaml: chatYaml,
349
- git_sha: gitSha,
350
- branch,
351
- git_ref: gitRef,
352
- pack_refs: packRefs,
353
- },
354
- });
355
- if (json) {
356
- (0, output_1.outputJson)(response, json);
357
- return;
358
- }
359
- console.log(`✓ Agents config synced to ${projectId}`);
360
- if (packRefs) {
361
- console.log(` Packs: ${packRefs.map((p) => p.id).join(', ')}`);
362
- console.log(` Source: merged (packs + project overlay)`);
363
- }
364
- else {
365
- const configPaths = resolveAgentsConfigPaths(repoRoot, manifest);
366
- console.log(` Agents: ${configPaths.agentsPath}`);
367
- console.log(` Teams: ${configPaths.teamsPath}`);
368
- console.log(` Chat: ${configPaths.chatPath}`);
369
- }
370
- if (gitSha)
371
- console.log(` Git SHA: ${gitSha.substring(0, 8)}`);
372
- if (branch)
373
- console.log(` Branch: ${branch}`);
374
- if (dirty)
375
- console.log(' Warning: working tree dirty — marked non-deployable');
376
- return;
377
- }
378
- case 'runtime-status': {
379
- const orgId = (0, args_1.getStringFlag)(flags, ['org']) ?? context.orgId;
380
- if (!orgId) {
381
- throw new Error('Missing org id. Provide --org or set a profile default.');
382
- }
383
- const response = await (0, client_1.requestJson)(context, `/orgs/${orgId}/agent-runtime/status`);
384
- if (json) {
385
- (0, output_1.outputJson)(response, json);
386
- return;
387
- }
388
- formatAgentRuntimeStatus(response, orgId);
389
- return;
390
- }
391
- default:
392
- throw new Error('Usage: eve agents <config|sync|runtime-status>');
393
- }
394
- }
395
- function formatAgentRuntimeStatus(response, orgId) {
396
- console.log(`Agent Runtime Status: ${orgId}`);
397
- if (response.pods.length === 0) {
398
- console.log('');
399
- console.log('No agent runtime pods found.');
400
- return;
401
- }
402
- const nameWidth = Math.max(7, ...response.pods.map((pod) => pod.pod_name.length));
403
- const statusWidth = Math.max(6, ...response.pods.map((pod) => pod.status.length));
404
- const capacityWidth = Math.max(8, ...response.pods.map((pod) => String(pod.capacity).length));
405
- const ageWidth = Math.max(3, ...response.pods.map((pod) => formatAgeSeconds(pod.last_heartbeat_at).length));
406
- console.log('');
407
- const header = [
408
- padRight('Pod', nameWidth),
409
- padRight('Status', statusWidth),
410
- padRight('Capacity', capacityWidth),
411
- padRight('Age', ageWidth),
412
- 'Last Heartbeat',
413
- ].join(' ');
414
- console.log(header);
415
- console.log('-'.repeat(header.length));
416
- for (const pod of response.pods) {
417
- const age = formatAgeSeconds(pod.last_heartbeat_at);
418
- console.log([
419
- padRight(pod.pod_name, nameWidth),
420
- padRight(pod.status, statusWidth),
421
- padRight(String(pod.capacity), capacityWidth),
422
- padRight(age, ageWidth),
423
- pod.last_heartbeat_at,
424
- ].join(' '));
425
- }
426
- }
427
- function padRight(value, width) {
428
- return value.length >= width ? value : `${value}${' '.repeat(width - value.length)}`;
429
- }
430
- function formatAgeSeconds(isoDate) {
431
- const timestamp = Date.parse(isoDate);
432
- if (!Number.isFinite(timestamp)) {
433
- return '-';
434
- }
435
- const diffSeconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
436
- if (diffSeconds < 60) {
437
- return `${diffSeconds}s`;
438
- }
439
- const diffMinutes = Math.floor(diffSeconds / 60);
440
- if (diffMinutes < 60) {
441
- return `${diffMinutes}m`;
442
- }
443
- const diffHours = Math.floor(diffMinutes / 60);
444
- return `${diffHours}h`;
445
- }