plazbot-cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +67 -111
  2. package/dist/cli.js +26 -11
  3. package/dist/commands/agent/ai-config.js +264 -63
  4. package/dist/commands/agent/chat.js +16 -11
  5. package/dist/commands/agent/copy.js +49 -13
  6. package/dist/commands/agent/create.js +62 -17
  7. package/dist/commands/agent/delete.js +50 -39
  8. package/dist/commands/agent/enable-widget.js +47 -27
  9. package/dist/commands/agent/export.js +179 -88
  10. package/dist/commands/agent/files.js +155 -73
  11. package/dist/commands/agent/get.js +24 -16
  12. package/dist/commands/agent/index.js +12 -10
  13. package/dist/commands/agent/list.js +20 -11
  14. package/dist/commands/agent/monitor.js +101 -53
  15. package/dist/commands/agent/on-message.js +55 -32
  16. package/dist/commands/agent/set.js +230 -82
  17. package/dist/commands/agent/templates.js +259 -156
  18. package/dist/commands/agent/tools.js +356 -123
  19. package/dist/commands/agent/update.js +69 -28
  20. package/dist/commands/agent/validate.js +72 -40
  21. package/dist/commands/agent/wizard.js +240 -329
  22. package/dist/commands/auth/browser-login.js +37 -31
  23. package/dist/commands/auth/login.js +49 -45
  24. package/dist/commands/auth/logout.js +20 -11
  25. package/dist/commands/auth/status.js +36 -12
  26. package/dist/commands/auth/workspace.js +61 -27
  27. package/dist/commands/portal/add-agent.js +106 -45
  28. package/dist/commands/portal/add-link.js +25 -28
  29. package/dist/commands/portal/clear-links.js +39 -19
  30. package/dist/commands/portal/create.js +66 -37
  31. package/dist/commands/portal/delete.js +40 -45
  32. package/dist/commands/portal/get.js +21 -54
  33. package/dist/commands/portal/index.js +12 -12
  34. package/dist/commands/portal/list.js +34 -52
  35. package/dist/commands/portal/shared.js +92 -0
  36. package/dist/commands/portal/update.js +100 -70
  37. package/dist/commands/whatsapp/broadcast.js +169 -77
  38. package/dist/commands/whatsapp/channels.js +167 -65
  39. package/dist/commands/whatsapp/chat.js +45 -46
  40. package/dist/commands/whatsapp/connect-flow.js +16 -15
  41. package/dist/commands/whatsapp/connect.js +36 -21
  42. package/dist/commands/whatsapp/delete-webhook.js +42 -25
  43. package/dist/commands/whatsapp/index.js +15 -16
  44. package/dist/commands/whatsapp/register-webhook.js +43 -26
  45. package/dist/commands/whatsapp/send-template.js +62 -61
  46. package/dist/commands/whatsapp/send.js +46 -33
  47. package/dist/commands/whatsapp/setup.js +62 -52
  48. package/dist/commands/whatsapp/shared.js +75 -0
  49. package/dist/commands/whatsapp/widget.js +82 -45
  50. package/dist/commands/workers/deploy.js +80 -58
  51. package/dist/commands/workers/index.js +8 -8
  52. package/dist/commands/workers/list.js +42 -20
  53. package/dist/commands/workers/logs.js +139 -36
  54. package/dist/commands/workers/remove.js +45 -47
  55. package/dist/commands/workers/secret.js +159 -83
  56. package/dist/commands/workers/test.js +108 -60
  57. package/dist/schemas/agent.config.schema.json +159 -8
  58. package/dist/studio/api/sseClient.js +1 -1
  59. package/dist/studio/api/studioApi.js +8 -0
  60. package/dist/studio/components/App.js +12 -0
  61. package/dist/studio/components/Footer.js +1 -1
  62. package/dist/studio/index.js +77 -14
  63. package/dist/studio/runOneShot.js +62 -23
  64. package/dist/studio/runRepl.js +5 -5
  65. package/dist/studio/slash/handlers.js +11 -8
  66. package/dist/studio/state/store.js +1 -1
  67. package/dist/utils/agent-errors.js +14 -11
  68. package/dist/utils/agent-patch.js +60 -0
  69. package/dist/utils/banner.js +9 -5
  70. package/dist/utils/confirm.js +31 -0
  71. package/dist/utils/credentials.js +13 -3
  72. package/dist/utils/logger.js +13 -10
  73. package/dist/utils/output.js +78 -0
  74. package/dist/utils/phone.js +22 -0
  75. package/dist/utils/ui.js +14 -11
  76. package/package.json +2 -2
@@ -1,68 +1,105 @@
1
1
  import { Command } from 'commander';
2
- import { Agent } from 'plazbot';
3
- import { getStoredCredentials } from '../../utils/credentials.js';
4
2
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
3
+ import { addExamples, addNotes } from '../../utils/help.js';
6
4
  import { createSpinner } from '../../utils/ui.js';
5
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
6
+ import { phoneOption, normalizePhone } from '../../utils/phone.js';
7
+ import { fetchFullAgent, getAgentClient, updateAgentMerged } from './shared.js';
7
8
  const widgetGroup = new Command('widget')
8
- .description('Manage WhatsApp widget');
9
+ .description('Show or hide the WhatsApp click-to-chat button on an agent web widget');
9
10
  addExamples(widgetGroup, [
10
- { description: 'Enable the WhatsApp click-to-chat widget on an agent',
11
- command: 'plazbot whatsapp widget enable agt_AbcDef123 -u https://wa.me/51912345678' },
12
- { description: 'Disable the widget',
13
- command: 'plazbot whatsapp widget disable agt_AbcDef123' },
11
+ { description: 'Enable the WhatsApp button for a number',
12
+ command: 'plazbot whatsapp widget enable age_AbcDef123 -p +51912345678' },
13
+ { description: 'Disable it',
14
+ command: 'plazbot whatsapp widget disable age_AbcDef123' },
14
15
  ]);
15
16
  widgetGroup.command('enable')
16
- .description('Enable the WhatsApp widget on an agent')
17
- .argument('<agentId>', 'Agent ID')
18
- .requiredOption('-u, --url <url>', 'WhatsApp URL (e.g.: https://wa.me/51912345678)')
17
+ .description('Enable the WhatsApp button on an agent web widget')
18
+ .argument('<agentId>', 'Agent ID (e.g. age_AbcDef123)')
19
+ .addOption(phoneOption('WhatsApp number the button opens, e.g. +51912345678 (builds https://wa.me/<number>)'))
20
+ .option('-u, --url <url>', 'Custom click-to-chat URL instead of --phone (e.g. https://wa.me/51912345678?text=Hi)')
21
+ .addOption(jsonOption())
19
22
  .option('--dev', 'Use development environment', false)
20
23
  .action(async (agentId, options) => {
21
24
  try {
22
- const credentials = await getStoredCredentials();
23
- const agent = new Agent({
24
- workspaceId: credentials.workspace,
25
- apiKey: credentials.apiKey,
26
- zone: credentials.zone,
27
- ...(options.dev && { customUrl: "http://localhost:5090" })
28
- });
29
- const spinner = createSpinner('Enabling WhatsApp widget...');
30
- spinner.start();
31
- await agent.updateAgent(agentId, {
32
- enableWhatsappWidget: true,
33
- urlWhatsappWidget: options.url,
34
- });
35
- spinner.succeed('WhatsApp widget enabled');
36
- logger.label('URL', options.url);
25
+ if (options.phone && options.url) {
26
+ throw new CliError('Use either --phone or --url, not both.', 'conflicting_options');
27
+ }
28
+ let url;
29
+ if (options.phone) {
30
+ url = `https://wa.me/${normalizePhone(options.phone)}`;
31
+ }
32
+ else if (options.url) {
33
+ url = options.url.trim();
34
+ if (!/^https?:\/\/\S+$/i.test(url)) {
35
+ throw new CliError(`Invalid URL "${options.url}".`, 'invalid_url', 'Use a full URL, e.g. https://wa.me/51912345678');
36
+ }
37
+ }
38
+ else {
39
+ throw new CliError('Missing WhatsApp number.', 'missing_phone', 'Pass --phone +51912345678, or a custom link with --url <url>.');
40
+ }
41
+ const { agent, credentials } = await getAgentClient(options.dev);
42
+ const spinner = createSpinner('Enabling WhatsApp button...').start();
43
+ try {
44
+ const current = await fetchFullAgent(agent, agentId);
45
+ await updateAgentMerged(credentials, options.dev, agentId, current, { enableWhatsappWidget: true, urlWhatsappWidget: url });
46
+ }
47
+ catch (error) {
48
+ spinner.stop();
49
+ throw error;
50
+ }
51
+ spinner.succeed('WhatsApp button enabled');
52
+ if (isJsonMode())
53
+ printJson({ id: agentId, enableWhatsappWidget: true, url });
54
+ else
55
+ logger.label('URL', url);
37
56
  }
38
57
  catch (error) {
39
- const message = error instanceof Error ? error.message : 'Unknown error';
40
- logger.error(message);
41
- process.exit(1);
58
+ fail(error);
42
59
  }
43
60
  });
44
61
  widgetGroup.command('disable')
45
- .description('Disable the WhatsApp widget on an agent')
46
- .argument('<agentId>', 'Agent ID')
62
+ .description('Disable the WhatsApp button on an agent web widget')
63
+ .argument('<agentId>', 'Agent ID (e.g. age_AbcDef123)')
64
+ .addOption(jsonOption())
47
65
  .option('--dev', 'Use development environment', false)
48
66
  .action(async (agentId, options) => {
49
67
  try {
50
- const credentials = await getStoredCredentials();
51
- const agent = new Agent({
52
- workspaceId: credentials.workspace,
53
- apiKey: credentials.apiKey,
54
- zone: credentials.zone,
55
- ...(options.dev && { customUrl: "http://localhost:5090" })
56
- });
57
- const spinner = createSpinner('Disabling WhatsApp widget...');
58
- spinner.start();
59
- await agent.updateAgent(agentId, { enableWhatsappWidget: false });
60
- spinner.succeed('WhatsApp widget disabled');
68
+ const { agent, credentials } = await getAgentClient(options.dev);
69
+ const spinner = createSpinner('Disabling WhatsApp button...').start();
70
+ try {
71
+ const current = await fetchFullAgent(agent, agentId);
72
+ await updateAgentMerged(credentials, options.dev, agentId, current, { enableWhatsappWidget: false });
73
+ }
74
+ catch (error) {
75
+ spinner.stop();
76
+ throw error;
77
+ }
78
+ spinner.succeed('WhatsApp button disabled');
79
+ if (isJsonMode())
80
+ printJson({ id: agentId, enableWhatsappWidget: false });
61
81
  }
62
82
  catch (error) {
63
- const message = error instanceof Error ? error.message : 'Unknown error';
64
- logger.error(message);
65
- process.exit(1);
83
+ fail(error);
66
84
  }
67
85
  });
86
+ const enableCmd = widgetGroup.commands.find(c => c.name() === 'enable');
87
+ const disableCmd = widgetGroup.commands.find(c => c.name() === 'disable');
88
+ addExamples(enableCmd, [
89
+ { description: 'Button that opens a chat with your number',
90
+ command: 'plazbot whatsapp widget enable age_AbcDef123 -p +51912345678' },
91
+ { description: 'Custom link with a pre-filled message',
92
+ command: 'plazbot whatsapp widget enable age_AbcDef123 -u "https://wa.me/51912345678?text=Hi"' },
93
+ { description: 'Machine-readable output',
94
+ command: 'plazbot whatsapp widget enable age_AbcDef123 -p +51912345678 --json' },
95
+ ]);
96
+ addNotes(enableCmd, [
97
+ 'Pass one of --phone or --url. Only the widget fields change; the rest of the agent is kept.',
98
+ ]);
99
+ addExamples(disableCmd, [
100
+ { description: 'Hide the WhatsApp button',
101
+ command: 'plazbot whatsapp widget disable age_AbcDef123' },
102
+ { description: 'Machine-readable output',
103
+ command: 'plazbot whatsapp widget disable age_AbcDef123 --json' },
104
+ ]);
68
105
  export const widgetCommand = widgetGroup;
@@ -1,28 +1,31 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
2
  import fs from 'fs/promises';
3
3
  import path from 'path';
4
4
  import { getStoredCredentials } from '../../utils/credentials.js';
5
5
  import { createApiClient } from '../../utils/api.js';
6
6
  import { logger } from '../../utils/logger.js';
7
- import { addExamples } from '../../utils/help.js';
7
+ import { addExamples, addNotes } from '../../utils/help.js';
8
8
  import { createSpinner, createTable, theme } from '../../utils/ui.js';
9
+ import { jsonOption, isJsonMode, printJson, fail, CliError, describeError } from '../../utils/output.js';
9
10
  export const deployCommand = new Command('deploy')
10
- .description('Deploy a worker to the Plazbot workspace')
11
- .argument('[file]', 'Worker TypeScript file (e.g.: ./workers/my-tool.ts)')
12
- .option('-n, --name <name>', 'Worker name (overrides the one in the file)')
13
- .option('-d, --description <desc>', 'Worker description')
11
+ .description('Deploy a worker file, or every worker in ./workers')
12
+ .argument('[file]', 'Worker TypeScript file (default: every *.ts file in ./workers)')
13
+ .option('-n, --name <name>', 'Worker name, overrides the one in the file (single file only)')
14
+ .option('--description <text>', 'Worker description (single file only)')
15
+ .addOption(new Option('-d <text>', 'Alias of --description').hideHelp())
16
+ .addOption(jsonOption())
14
17
  .option('--dev', 'Use development environment', false)
15
18
  .action(async (file, options) => {
16
19
  try {
17
- const credentials = await getStoredCredentials();
18
- // Buscar archivos de workers
20
+ const description = options.description ?? options.d;
19
21
  const files = await resolveWorkerFiles(file);
20
22
  if (files.length === 0) {
21
- logger.error('No worker files found to deploy');
22
- logger.dim('Usage: plazbot workers deploy ./workers/my-tool.ts');
23
- logger.dim(' or: plazbot workers deploy (searches ./workers/ automatically)');
24
- process.exit(1);
23
+ throw new CliError('No worker files found to deploy.', 'no_worker_files', 'Pass a file (plazbot workers deploy ./workers/my-tool.ts) or add *.ts files to ./workers.');
24
+ }
25
+ if (files.length > 1 && (options.name || description)) {
26
+ throw new CliError(`--name and --description only apply to a single file, but ${files.length} files were found in ./workers.`, 'name_with_multiple_files', 'Deploy one file at a time: plazbot workers deploy ./workers/my-tool.ts -n "my-tool"');
25
27
  }
28
+ const credentials = await getStoredCredentials();
26
29
  const api = createApiClient({
27
30
  apiKey: credentials.apiKey,
28
31
  workspace: credentials.workspace,
@@ -30,103 +33,122 @@ export const deployCommand = new Command('deploy')
30
33
  dev: options.dev,
31
34
  });
32
35
  logger.title(`Deploying ${files.length} worker${files.length > 1 ? 's' : ''}`);
33
- console.log();
34
36
  const results = [];
35
37
  for (const filePath of files) {
36
38
  const fileName = path.basename(filePath);
37
39
  const spinner = createSpinner(`Deploying ${fileName}...`);
38
40
  spinner.start();
39
41
  try {
40
- // Leer archivo fuente
41
42
  const sourceCode = await fs.readFile(filePath, 'utf-8');
42
- // Enviar al API
43
43
  const response = await api.post('/api/worker/deploy', {
44
44
  sourceCode,
45
45
  fileName,
46
46
  name: files.length === 1 ? options.name : undefined,
47
- description: files.length === 1 ? options.description : undefined,
47
+ description: files.length === 1 ? description : undefined,
48
48
  });
49
49
  const data = response.data;
50
+ if (data?.success === false || !data?.name) {
51
+ throw new CliError(data?.message || 'The API did not return the deployed worker.', 'deploy_failed');
52
+ }
50
53
  spinner.succeed(`${fileName} → ${data.name} (${data.type})`);
51
- results.push({ file: fileName, result: data });
54
+ results.push({
55
+ file: fileName,
56
+ name: data.name,
57
+ type: data.type,
58
+ status: 'deployed',
59
+ workerStatus: data.status,
60
+ webhookUrl: data.webhookUrl || undefined,
61
+ cronExpression: data.cronExpression || undefined,
62
+ });
52
63
  }
53
64
  catch (error) {
54
- const msg = error.response?.data?.message || error.message || 'Unknown error';
55
- spinner.fail(`${fileName}: ${msg}`);
56
- results.push({ file: fileName, error: msg });
65
+ const { message } = describeError(error);
66
+ spinner.fail(`${fileName}: ${message}`);
67
+ results.push({ file: fileName, status: 'failed', error: message });
57
68
  }
58
69
  }
59
- // Resumen
60
- console.log();
61
- const success = results.filter(r => r.result);
62
- const failed = results.filter(r => r.error);
70
+ const success = results.filter(r => r.status === 'deployed');
71
+ const failed = results.filter(r => r.status === 'failed');
72
+ if (isJsonMode()) {
73
+ printJson(results);
74
+ if (failed.length > 0)
75
+ process.exit(1);
76
+ return;
77
+ }
63
78
  if (success.length > 0) {
64
79
  logger.success(`${success.length} worker${success.length > 1 ? 's' : ''} deployed`);
65
- const rows = success.map(r => [
66
- r.result.name,
67
- r.result.type,
68
- r.result.status,
69
- r.result.webhookUrl || r.result.cronExpression || '—',
70
- ]);
71
- console.log(createTable(['Name', 'Type', 'Status', 'URL/Cron'], rows));
80
+ console.log(createTable(['File', 'Name', 'Type', 'Status', 'URL/Cron'], success.map(r => [
81
+ r.file,
82
+ r.name,
83
+ r.type || '—',
84
+ r.workerStatus || '—',
85
+ r.webhookUrl || r.cronExpression || '—',
86
+ ])));
72
87
  }
73
- if (failed.length > 0) {
74
- logger.warning(`${failed.length} worker${failed.length > 1 ? 's' : ''} failed`);
75
- failed.forEach(f => logger.dim(` ${f.file}: ${f.error}`));
76
- }
77
- // Mostrar webhook URLs si hay
78
- const webhooks = success.filter(r => r.result.webhookUrl);
88
+ const webhooks = success.filter(r => r.webhookUrl);
79
89
  if (webhooks.length > 0) {
80
- console.log();
81
90
  logger.title('Webhook URLs');
82
- webhooks.forEach(w => {
83
- logger.label(w.result.name, theme.secondary(w.result.webhookUrl));
84
- });
91
+ webhooks.forEach(w => logger.label(w.name, theme.secondary(w.webhookUrl)));
92
+ }
93
+ if (failed.length > 0) {
94
+ logger.warning(`${failed.length} of ${results.length} worker${results.length > 1 ? 's' : ''} failed to deploy`);
95
+ failed.forEach(f => logger.dim(` ${f.file}: ${f.error}`));
85
96
  }
86
97
  console.log();
87
98
  if (options.dev) {
88
99
  logger.warning('Environment: development');
89
100
  }
101
+ if (failed.length > 0)
102
+ process.exit(1);
90
103
  }
91
104
  catch (error) {
92
- const message = error instanceof Error ? error.message : 'Unknown error';
93
- logger.error(message);
94
- process.exit(1);
105
+ fail(error);
95
106
  }
96
107
  });
97
108
  addExamples(deployCommand, [
98
109
  { description: 'Deploy a single worker file',
99
- command: 'plazbot workers deploy ./workers/my-tool.ts' },
100
- { description: 'Deploy every TypeScript file inside ./workers automatically',
110
+ command: 'plazbot workers deploy ./workers/get-stock.ts' },
111
+ { description: 'Deploy every *.ts file inside ./workers',
101
112
  command: 'plazbot workers deploy' },
102
113
  { description: 'Deploy with a custom name and description',
103
- command: 'plazbot workers deploy ./workers/cron.ts -n "Daily Report" -d "Generates daily KPIs"' },
104
- { description: 'Deploy against the local backend',
105
- command: 'plazbot workers deploy ./workers/test.ts --dev' },
114
+ command: 'plazbot workers deploy ./workers/cron.ts -n "daily-report" --description "Generates daily KPIs"' },
115
+ { description: 'Deploy everything and get per-file results as JSON (CI)',
116
+ command: 'plazbot workers deploy --json' },
117
+ ]);
118
+ addNotes(deployCommand, [
119
+ 'Without [file], every *.ts file in ./workers is deployed (*.d.ts files are skipped).',
120
+ '--name and --description only work when deploying a single file.',
121
+ 'Only the single source file is uploaded: relative imports and npm dependencies are not bundled.',
122
+ 'The TypeScript is compiled on the Plazbot server; compile errors show up as a failed file.',
123
+ 'Exits with code 1 if any file fails to deploy.',
106
124
  ]);
107
125
  // Resuelve los archivos a desplegar
108
126
  async function resolveWorkerFiles(file) {
109
127
  if (file) {
110
- // Archivo especifico
111
128
  const resolved = path.resolve(file);
129
+ let isFile = false;
112
130
  try {
113
- await fs.access(resolved);
114
- return [resolved];
131
+ isFile = (await fs.stat(resolved)).isFile();
115
132
  }
116
133
  catch {
117
- throw new Error(`File not found: ${file}`);
134
+ isFile = false;
135
+ }
136
+ if (!isFile) {
137
+ throw new CliError(`File not found: ${file}.`, 'file_not_found', 'Check the path to your worker file.');
118
138
  }
139
+ return [resolved];
119
140
  }
120
141
  // Buscar en ./workers/ automaticamente
121
142
  const workersDir = path.resolve('workers');
143
+ let entries;
122
144
  try {
123
- const entries = await fs.readdir(workersDir);
124
- const tsFiles = entries
125
- .filter(e => e.endsWith('.ts') && !e.endsWith('.d.ts'))
126
- .map(e => path.join(workersDir, e));
127
- return tsFiles;
145
+ entries = await fs.readdir(workersDir);
128
146
  }
129
147
  catch {
130
148
  return [];
131
149
  }
150
+ return entries
151
+ .filter(e => e.endsWith('.ts') && !e.endsWith('.d.ts'))
152
+ .sort()
153
+ .map(e => path.join(workersDir, e));
132
154
  }
@@ -7,7 +7,7 @@ import { removeCommand } from './remove.js';
7
7
  import { secretCommand } from './secret.js';
8
8
  import { testCommand } from './test.js';
9
9
  export const workersCommands = new Command('workers')
10
- .description('Commands to manage workers (tools, syncs, schedules, webhooks)')
10
+ .description('Manage workers (tools, syncs, schedules, webhooks)')
11
11
  .addCommand(deployCommand)
12
12
  .addCommand(listCommand)
13
13
  .addCommand(logsCommand)
@@ -15,14 +15,14 @@ export const workersCommands = new Command('workers')
15
15
  .addCommand(secretCommand)
16
16
  .addCommand(testCommand);
17
17
  addExamples(workersCommands, [
18
- { description: 'Deploy every worker file in ./workers automatically',
18
+ { description: 'Deploy every worker file in ./workers',
19
19
  command: 'plazbot workers deploy' },
20
20
  { description: 'List active workers',
21
21
  command: 'plazbot workers list -s active' },
22
- { description: 'Run a worker with a JSON payload',
23
- command: 'plazbot workers test my-tool -p \'{"product":"iPhone 15"}\'' },
24
- { description: 'Save a secret for workers (plz.env.KEY)',
25
- command: 'plazbot workers secret set STRIPE_API_KEY sk_live_xxxxx' },
26
- { description: 'View the last 100 executions of a worker',
27
- command: 'plazbot workers logs my-tool -l 100' },
22
+ { description: 'Run a worker with JSON data',
23
+ command: 'plazbot workers test get-stock -d \'{"product":"iPhone 15"}\'' },
24
+ { description: 'Save a secret from stdin (available as plz.env.STRIPE_API_KEY)',
25
+ command: 'echo "$STRIPE_KEY" | plazbot workers secret set STRIPE_API_KEY' },
26
+ { description: 'Follow the executions of a worker in real time',
27
+ command: 'plazbot workers logs get-stock --follow' },
28
28
  ]);
@@ -1,13 +1,17 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
2
  import { getStoredCredentials } from '../../utils/credentials.js';
3
3
  import { createApiClient } from '../../utils/api.js';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  import { addExamples } from '../../utils/help.js';
6
6
  import { createSpinner, createTable, theme, formatDate } from '../../utils/ui.js';
7
+ import { jsonOption, isJsonMode, printJson, fail } from '../../utils/output.js';
8
+ const WORKER_TYPES = ['tool', 'worker', 'sync', 'schedule', 'webhook'];
9
+ const WORKER_STATUSES = ['active', 'inactive', 'error'];
7
10
  export const listCommand = new Command('list')
8
- .description('List all workers in the workspace')
9
- .option('-t, --type <type>', 'Filter by type (tool, worker, sync, schedule, webhook)')
10
- .option('-s, --status <status>', 'Filter by status (active, inactive, error)')
11
+ .description('List the workers in the workspace')
12
+ .addOption(new Option('-t, --type <type>', 'Filter by type').choices(WORKER_TYPES))
13
+ .addOption(new Option('-s, --status <status>', 'Filter by status').choices(WORKER_STATUSES))
14
+ .addOption(jsonOption())
11
15
  .option('--dev', 'Use development environment', false)
12
16
  .action(async (options) => {
13
17
  try {
@@ -25,12 +29,36 @@ export const listCommand = new Command('list')
25
29
  params.type = options.type;
26
30
  if (options.status)
27
31
  params.status = options.status;
28
- const response = await api.get('/api/worker', { params });
29
- const workers = response.data;
30
- spinner.stop();
31
- if (!workers || workers.length === 0) {
32
+ let workers;
33
+ try {
34
+ const response = await api.get('/api/worker', { params });
35
+ workers = Array.isArray(response.data) ? response.data : [];
36
+ }
37
+ finally {
38
+ spinner.stop();
39
+ }
40
+ if (isJsonMode()) {
41
+ printJson(workers.map(w => ({
42
+ id: w.id,
43
+ name: w.name,
44
+ type: w.type,
45
+ status: w.status,
46
+ description: w.description ?? null,
47
+ cronExpression: w.cronExpression ?? null,
48
+ webhookUrl: w.webhookUrl ?? null,
49
+ executionCount: w.executionCount ?? 0,
50
+ errorCount: w.errorCount ?? 0,
51
+ avgDurationMs: w.avgDurationMs ?? 0,
52
+ lastExecutedAt: w.lastExecutedAt ?? null,
53
+ lastError: w.lastError ?? null,
54
+ createdAt: w.createdAt,
55
+ updatedAt: w.updatedAt,
56
+ })));
57
+ return;
58
+ }
59
+ if (workers.length === 0) {
32
60
  logger.info('\n No workers found in this workspace');
33
- logger.dim(' Deploy your first worker: plazbot workers deploy ./workers/my-tool.ts');
61
+ logger.dim('Deploy your first worker: plazbot workers deploy ./workers/my-tool.ts');
34
62
  console.log();
35
63
  return;
36
64
  }
@@ -41,20 +69,16 @@ export const listCommand = new Command('list')
41
69
  w.status === 'active' ? theme.success('active')
42
70
  : w.status === 'error' ? theme.error('error')
43
71
  : theme.muted('inactive'),
44
- `${w.executionCount}`,
72
+ `${w.executionCount ?? 0}`,
45
73
  w.errorCount > 0 ? theme.error(`${w.errorCount}`) : theme.muted('0'),
46
74
  w.avgDurationMs > 0 ? `${w.avgDurationMs}ms` : '—',
47
75
  w.lastExecutedAt ? formatDate(w.lastExecutedAt) : '—',
48
76
  ]);
49
77
  console.log(createTable(['Name', 'Type', 'Status', 'Executions', 'Errors', 'Duration', 'Last execution'], rows));
50
- // Mostrar workers con error
51
78
  const errored = workers.filter(w => w.status === 'error');
52
79
  if (errored.length > 0) {
53
- console.log();
54
80
  logger.warning('Workers with errors:');
55
- errored.forEach(w => {
56
- logger.dim(` ${w.name}: ${w.lastError || 'no details'}`);
57
- });
81
+ errored.forEach(w => logger.dim(` ${w.name}: ${w.lastError || 'no details'}`));
58
82
  }
59
83
  console.log();
60
84
  if (options.dev) {
@@ -62,9 +86,7 @@ export const listCommand = new Command('list')
62
86
  }
63
87
  }
64
88
  catch (error) {
65
- const message = error instanceof Error ? error.message : 'Unknown error';
66
- logger.error(message);
67
- process.exit(1);
89
+ fail(error);
68
90
  }
69
91
  });
70
92
  addExamples(listCommand, [
@@ -74,8 +96,8 @@ addExamples(listCommand, [
74
96
  command: 'plazbot workers list -t schedule' },
75
97
  { description: 'Show only workers with errors',
76
98
  command: 'plazbot workers list -s error' },
77
- { description: 'Filter active webhooks',
78
- command: 'plazbot workers list -t webhook -s active' },
99
+ { description: 'Get active webhooks as JSON',
100
+ command: 'plazbot workers list -t webhook -s active --json' },
79
101
  ]);
80
102
  function typeLabel(type) {
81
103
  const labels = {