Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
import ora, { type Ora } from 'ora';
import { runDeploy, runLogin, runLogout } from './deploy-command.js';
import { runDestroy } from './destroy-command.js';
import { runDeploymentList } from './list-command.js';
import { runDeploymentList, runDeploymentLogs } from './list-command.js';
import {
startLaunchMetadataRecording,
type LaunchMetadataRun
Expand Down Expand Up @@ -206,6 +206,7 @@ Commands:
--input KEY=value override a declared persona input
(repeat for multiple)
deployments list List deployed cloud agents in the active workspace.
deployments logs Show structured logs for a deployed cloud agent.
destroy <persona-or-agent-id> [flags]
Tear down a deployed agent: cancel all schedules and
mark the agent as destroyed in the workspace. Accepts
Expand Down Expand Up @@ -3805,14 +3806,18 @@ export async function main(): Promise<void> {
if (subcommand === 'deployments') {
const [action, ...extra] = rest;
if (!action || action === '-h' || action === '--help') {
process.stdout.write('Usage: agentworkforce deployments list [flags]\n');
process.stdout.write('Usage: agentworkforce deployments <list|logs> [flags]\n');
process.exit(action ? 0 : 1);
}
if (action !== 'list') {
die(`deployments: unknown action "${action}". Expected: list`);
if (action === 'list') {
await runDeploymentList(extra);
return;
}
await runDeploymentList(extra);
return;
if (action === 'logs') {
await runDeploymentLogs(extra);
return;
}
die(`deployments: unknown action "${action}". Expected: list, logs`);
}

if (subcommand === 'destroy') {
Expand Down
73 changes: 69 additions & 4 deletions packages/cli/src/list-command.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { formatDeploymentsTable, parseDeploymentListArgs } from './list-command.js';
import {
formatDeploymentLogEntries,
formatDeploymentsTable,
parseDeploymentListArgs,
parseDeploymentLogsArgs,
tailLogEntriesFromNewestFiles
} from './list-command.js';

test('parseDeploymentListArgs accepts deployment list filters', () => {
assert.deepEqual(
Expand Down Expand Up @@ -30,7 +36,8 @@ test('formatDeploymentsTable renders agent rows', () => {
const out = formatDeploymentsTable([
{
agentId: 'b2f111111111111111111111e8c2',
personaId: 'weekly-digest',
personaId: '7133e815-8c84-5d05-a08b-e434006b11ac',
personaSlug: 'weekly-digest',
deployedName: 'Weekly Digest',
status: 'active',
createdAt: '2026-05-13T09:11:00.000Z',
Expand All @@ -39,8 +46,66 @@ test('formatDeploymentsTable renders agent rows', () => {
deployedByUserId: 'user-1'
}
]);
assert.match(out, /agentId\s+persona\s+status\s+deployed\s+lastUsed/);
assert.match(out, /name\s+status\s+deployed\s+lastUsed\s+agentId/);
assert.match(out, /b2f1\.\.\.e8c2/);
assert.match(out, /weekly-digest/);
assert.match(out, /Weekly Digest/);
assert.doesNotMatch(out, /7133e815/);
assert.match(out, /2026-05-13 09:11 UTC/);
});

test('parseDeploymentLogsArgs accepts selector and log flags', () => {
assert.deepEqual(
parseDeploymentLogsArgs([
'Weekly Digest',
'--workspace=ws-1',
'--path',
'/_logs/ws-1/2026-05-19.jsonl',
'--tail',
'25',
'--cloud-url',
'https://cloud.example.test',
'--json',
'--no-prompt'
]),
{
selector: 'Weekly Digest',
workspace: 'ws-1',
path: '/_logs/ws-1/2026-05-19.jsonl',
tail: 25,
cloudUrl: 'https://cloud.example.test',
json: true,
noPrompt: true
}
);
});

test('formatDeploymentLogEntries renders structured log rows', () => {
const out = formatDeploymentLogEntries([
{
ts: '2026-05-19T13:00:00.000Z',
level: 'info',
agentId: 'agent-1',
msg: 'handled event'
}
]);
assert.match(out, /2026-05-19T13:00:00.000Z\s+INFO\s+agent-1\s+handled event/);
});

test('tailLogEntriesFromNewestFiles keeps the newest entries across files', () => {
const entries = tailLogEntriesFromNewestFiles(
[
[
{ ts: '2026-05-19T13:00:00.000Z', msg: 'newer-a' },
{ ts: '2026-05-19T14:00:00.000Z', msg: 'newer-b' },
{ ts: '2026-05-19T15:00:00.000Z', msg: 'newer-c' }
],
[
{ ts: '2026-05-18T10:00:00.000Z', msg: 'older-a' },
{ ts: '2026-05-18T11:00:00.000Z', msg: 'older-b' }
]
],
3
);

assert.deepEqual(entries.map((entry) => entry.msg), ['newer-a', 'newer-b', 'newer-c']);
});
Loading
Loading