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,156 +1,232 @@
1
1
  import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
2
3
  import { getStoredCredentials } from '../../utils/credentials.js';
3
4
  import { createApiClient } from '../../utils/api.js';
4
5
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
6
+ import { addExamples, addNotes } from '../../utils/help.js';
6
7
  import { createSpinner, createTable, theme, formatDate } from '../../utils/ui.js';
7
- import readline from 'readline';
8
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
9
+ import { addYesOption, confirmAction } from '../../utils/confirm.js';
10
+ const SECRET_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
11
+ function validateKey(key) {
12
+ if (!SECRET_KEY_RE.test(key)) {
13
+ throw new CliError(`Invalid secret name "${key}". Use UPPER_SNAKE_CASE: uppercase letters, digits and underscores, starting with a letter.`, 'invalid_secret_key', 'Example: STRIPE_API_KEY');
14
+ }
15
+ }
16
+ function apiFor(options, credentials) {
17
+ return createApiClient({
18
+ apiKey: credentials.apiKey,
19
+ workspace: credentials.workspace,
20
+ zone: credentials.zone,
21
+ dev: options.dev,
22
+ });
23
+ }
24
+ // Reads the whole stdin (piped input) and strips the trailing newline added by echo.
25
+ async function readStdin() {
26
+ const chunks = [];
27
+ for await (const chunk of process.stdin) {
28
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
29
+ }
30
+ return Buffer.concat(chunks).toString('utf-8').replace(/\r?\n$/, '');
31
+ }
32
+ async function resolveSecretValue(key, value) {
33
+ if (value !== undefined)
34
+ return value;
35
+ if (!process.stdin.isTTY) {
36
+ const piped = await readStdin();
37
+ if (!piped) {
38
+ throw new CliError('No secret value received on stdin.', 'missing_secret_value', `Example: echo "$VALUE" | plazbot workers secret set ${key}`);
39
+ }
40
+ return piped;
41
+ }
42
+ if (isJsonMode()) {
43
+ throw new CliError('The secret value is required in --json mode.', 'missing_secret_value', `Pipe it through stdin: echo "$VALUE" | plazbot workers secret set ${key} --json`);
44
+ }
45
+ const { secret } = await inquirer.prompt([{
46
+ type: 'password',
47
+ name: 'secret',
48
+ message: `Value for ${key}:`,
49
+ mask: '*',
50
+ validate: (v) => v.length > 0 || 'The value cannot be empty',
51
+ }]);
52
+ return secret;
53
+ }
8
54
  // ── Comando padre: secret ───────────────────────────────────────────────────
9
55
  export const secretCommand = new Command('secret')
10
- .description('Manage worker secrets (environment variables)')
56
+ .description('Manage worker secrets (available as plz.env.KEY)')
11
57
  .addCommand(secretSetCommand())
12
58
  .addCommand(secretListCommand())
13
59
  .addCommand(secretRemoveCommand());
14
60
  addExamples(secretCommand, [
15
- { description: 'Save (or update) an API key as a secret',
16
- command: 'plazbot workers secret set STRIPE_API_KEY sk_live_xxxxx' },
17
- { description: 'List every secret (values are hidden)',
61
+ { description: 'Save a secret (prompts for the value with hidden input)',
62
+ command: 'plazbot workers secret set STRIPE_API_KEY' },
63
+ { description: 'Save a secret from stdin (scripts/CI)',
64
+ command: 'echo "$STRIPE_KEY" | plazbot workers secret set STRIPE_API_KEY' },
65
+ { description: 'List every secret (values are never shown)',
18
66
  command: 'plazbot workers secret list' },
19
- { description: 'Delete a secret with confirmation prompt',
20
- command: 'plazbot workers secret remove STRIPE_API_KEY' },
21
67
  { description: 'Delete a secret without confirmation',
22
- command: 'plazbot workers secret remove STRIPE_API_KEY -f' },
68
+ command: 'plazbot workers secret remove STRIPE_API_KEY --yes' },
23
69
  ]);
24
70
  // ── secret set ──────────────────────────────────────────────────────────────
25
71
  function secretSetCommand() {
26
- return new Command('set')
72
+ const cmd = new Command('set')
27
73
  .description('Create or update a secret')
28
- .argument('<key>', 'Secret name (e.g.: STRIPE_API_KEY)')
29
- .argument('<value>', 'Secret value')
74
+ .argument('<key>', 'Secret name in UPPER_SNAKE_CASE (e.g. STRIPE_API_KEY)')
75
+ .argument('[value]', 'Secret value (omit it to be prompted, or pipe it through stdin)')
76
+ .addOption(jsonOption())
30
77
  .option('--dev', 'Use development environment', false)
31
78
  .action(async (key, value, options) => {
32
79
  try {
80
+ validateKey(key);
81
+ const secretValue = await resolveSecretValue(key, value);
82
+ if (!secretValue.trim()) {
83
+ throw new CliError('The secret value cannot be empty.', 'missing_secret_value');
84
+ }
33
85
  const credentials = await getStoredCredentials();
34
- const api = createApiClient({
35
- apiKey: credentials.apiKey,
36
- workspace: credentials.workspace,
37
- zone: credentials.zone,
38
- dev: options.dev,
39
- });
86
+ const api = apiFor(options, credentials);
40
87
  const spinner = createSpinner(`Saving secret "${key}"...`);
41
88
  spinner.start();
42
- await api.put('/api/worker/secrets', { key, value });
89
+ try {
90
+ const response = await api.put('/api/worker/secrets', { key, value: secretValue });
91
+ if (response.data?.success === false) {
92
+ throw new CliError(response.data.message || `Could not save secret "${key}".`, 'secret_save_failed');
93
+ }
94
+ }
95
+ catch (error) {
96
+ spinner.stop();
97
+ throw error;
98
+ }
43
99
  spinner.succeed(`Secret "${key}" saved`);
44
- logger.dim(` Available in your workers as plz.env.${key}`);
100
+ if (isJsonMode()) {
101
+ printJson({ key, saved: true });
102
+ return;
103
+ }
104
+ logger.dim(`Available in your workers as plz.env.${key}`);
105
+ if (value !== undefined) {
106
+ logger.dim('Tip: omit the value (or pipe it through stdin) to keep it out of your shell history.');
107
+ }
45
108
  console.log();
46
109
  if (options.dev) {
47
110
  logger.warning('Environment: development');
48
111
  }
49
112
  }
50
113
  catch (error) {
51
- const message = error instanceof Error ? error.message : 'Unknown error';
52
- logger.error(message);
53
- process.exit(1);
114
+ fail(error);
54
115
  }
55
116
  });
117
+ addExamples(cmd, [
118
+ { description: 'Prompt for the value with hidden input (recommended)',
119
+ command: 'plazbot workers secret set STRIPE_API_KEY' },
120
+ { description: 'Read the value from stdin',
121
+ command: 'echo "$STRIPE_KEY" | plazbot workers secret set STRIPE_API_KEY' },
122
+ { description: 'Read the value from a file',
123
+ command: 'plazbot workers secret set GOOGLE_CREDENTIALS < ./service-account.json' },
124
+ { description: 'Pass the value inline (stays in your shell history)',
125
+ command: 'plazbot workers secret set API_BASE_URL https://api.example.com' },
126
+ ]);
127
+ addNotes(cmd, [
128
+ 'Passing the value as an argument leaks it to your shell history and process list; prefer the prompt or stdin.',
129
+ 'Setting an existing key overwrites its value. Values are encrypted and never shown again.',
130
+ ]);
131
+ return cmd;
56
132
  }
57
133
  // ── secret list ─────────────────────────────────────────────────────────────
58
134
  function secretListCommand() {
59
- return new Command('list')
60
- .description('List all secrets in the workspace (without showing values)')
135
+ const cmd = new Command('list')
136
+ .description('List the secrets in the workspace (values are never shown)')
137
+ .addOption(jsonOption())
61
138
  .option('--dev', 'Use development environment', false)
62
139
  .action(async (options) => {
63
140
  try {
64
141
  const credentials = await getStoredCredentials();
65
- const api = createApiClient({
66
- apiKey: credentials.apiKey,
67
- workspace: credentials.workspace,
68
- zone: credentials.zone,
69
- dev: options.dev,
70
- });
142
+ const api = apiFor(options, credentials);
71
143
  const spinner = createSpinner('Fetching secrets...');
72
144
  spinner.start();
73
- const response = await api.get('/api/worker/secrets');
74
- const secrets = response.data;
75
- spinner.stop();
76
- if (!secrets || secrets.length === 0) {
145
+ let secrets;
146
+ try {
147
+ const response = await api.get('/api/worker/secrets');
148
+ secrets = Array.isArray(response.data) ? response.data : [];
149
+ }
150
+ finally {
151
+ spinner.stop();
152
+ }
153
+ if (isJsonMode()) {
154
+ printJson(secrets.map(s => ({ key: s.key, updatedAt: s.updatedAt ?? null })));
155
+ return;
156
+ }
157
+ if (secrets.length === 0) {
77
158
  logger.info('\n No secrets configured');
78
- logger.dim(' Add one: plazbot workers secret set MY_KEY my_value');
159
+ logger.dim('Add one: plazbot workers secret set MY_KEY');
79
160
  console.log();
80
161
  return;
81
162
  }
82
163
  logger.title(`Secrets (${secrets.length})`);
83
- const rows = secrets.map(s => [
84
- s.key,
85
- theme.muted('••••••••'),
86
- formatDate(s.updatedAt),
87
- ]);
88
- console.log(createTable(['Key', 'Value', 'Updated'], rows));
164
+ console.log(createTable(['Key', 'Value', 'Updated'], secrets.map(s => [s.key, theme.muted('••••••••'), s.updatedAt ? formatDate(s.updatedAt) : '—'])));
89
165
  console.log();
90
166
  if (options.dev) {
91
167
  logger.warning('Environment: development');
92
168
  }
93
169
  }
94
170
  catch (error) {
95
- const message = error instanceof Error ? error.message : 'Unknown error';
96
- logger.error(message);
97
- process.exit(1);
171
+ fail(error);
98
172
  }
99
173
  });
174
+ addExamples(cmd, [
175
+ { description: 'List every secret',
176
+ command: 'plazbot workers secret list' },
177
+ { description: 'List secret names as JSON',
178
+ command: 'plazbot workers secret list --json' },
179
+ ]);
180
+ return cmd;
100
181
  }
101
182
  // ── secret remove ───────────────────────────────────────────────────────────
102
183
  function secretRemoveCommand() {
103
- return new Command('remove')
184
+ const cmd = new Command('remove')
104
185
  .description('Delete a secret from the workspace')
105
186
  .argument('<key>', 'Name of the secret to delete')
106
- .option('-f, --force', 'Delete without confirmation', false)
107
- .option('--dev', 'Use development environment', false)
108
- .action(async (key, options) => {
187
+ .addOption(jsonOption())
188
+ .option('--dev', 'Use development environment', false);
189
+ addYesOption(cmd);
190
+ cmd.action(async (key, options) => {
109
191
  try {
110
- const credentials = await getStoredCredentials();
111
- const api = createApiClient({
112
- apiKey: credentials.apiKey,
113
- workspace: credentials.workspace,
114
- zone: credentials.zone,
115
- dev: options.dev,
116
- });
117
- if (!options.force) {
118
- const confirmed = await askConfirmation(` Delete secret "${key}"? (y/N): `);
119
- if (!confirmed) {
120
- logger.info('\n Operation cancelled');
121
- process.exit(0);
122
- }
192
+ const ok = await confirmAction(`Delete secret "${key}"? Workers using plz.env.${key} will stop working.`, options);
193
+ if (!ok) {
194
+ logger.dim('Aborted.');
195
+ process.exit(1);
123
196
  }
197
+ const credentials = await getStoredCredentials();
198
+ const api = apiFor(options, credentials);
124
199
  const spinner = createSpinner(`Deleting secret "${key}"...`);
125
200
  spinner.start();
126
- await api.delete(`/api/worker/secrets/${encodeURIComponent(key)}`);
201
+ try {
202
+ await api.delete(`/api/worker/secrets/${encodeURIComponent(key)}`);
203
+ }
204
+ catch (error) {
205
+ spinner.stop();
206
+ if (error?.response?.status === 404) {
207
+ throw new CliError(`Secret "${key}" not found.`, 'not_found', 'List your secrets with: plazbot workers secret list');
208
+ }
209
+ throw error;
210
+ }
127
211
  spinner.succeed(`Secret "${key}" deleted`);
212
+ if (isJsonMode()) {
213
+ printJson({ key, removed: true });
214
+ return;
215
+ }
128
216
  console.log();
129
217
  if (options.dev) {
130
218
  logger.warning('Environment: development');
131
219
  }
132
220
  }
133
221
  catch (error) {
134
- if (error.response?.status === 404) {
135
- logger.error(`Secret "${key}" not found`);
136
- }
137
- else {
138
- const message = error instanceof Error ? error.message : 'Unknown error';
139
- logger.error(message);
140
- }
141
- process.exit(1);
222
+ fail(error);
142
223
  }
143
224
  });
144
- }
145
- function askConfirmation(question) {
146
- return new Promise((resolve) => {
147
- const rl = readline.createInterface({
148
- input: process.stdin,
149
- output: process.stdout,
150
- });
151
- rl.question(question, (answer) => {
152
- rl.close();
153
- resolve(answer.toLowerCase() === 'y');
154
- });
155
- });
225
+ addExamples(cmd, [
226
+ { description: 'Delete a secret (asks for confirmation)',
227
+ command: 'plazbot workers secret remove STRIPE_API_KEY' },
228
+ { description: 'Delete without confirmation (scripts/CI)',
229
+ command: 'plazbot workers secret remove STRIPE_API_KEY --yes' },
230
+ ]);
231
+ return cmd;
156
232
  }
@@ -1,19 +1,24 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
+ import fs from 'fs/promises';
2
3
  import { getStoredCredentials } from '../../utils/credentials.js';
3
4
  import { createApiClient } from '../../utils/api.js';
4
5
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
6
+ import { addExamples, addNotes } from '../../utils/help.js';
6
7
  import { createSpinner, theme } from '../../utils/ui.js';
8
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
7
9
  export const testCommand = new Command('test')
8
- .description('Run a worker manually to test it')
10
+ .description('Run a deployed worker manually to test it')
9
11
  .argument('<name>', 'Name of the worker to run')
10
- .option('-p, --payload <json>', 'JSON payload for the worker (e.g.: \'{"product":"iPhone"}\')')
11
- .option('-t, --type <type>', 'Worker type (tool, worker, sync, schedule, webhook)')
12
- .option('--contact <id>', 'Contact ID (context for tools)')
13
- .option('--agent <id>', 'Agent ID (context for tools)')
12
+ .option('-d, --data <json>', 'Input data as a JSON object, or @file.json to read it from a file')
13
+ .addOption(new Option('--payload <json>', 'Alias of --data').hideHelp())
14
+ .addOption(new Option('-t, --type <type>', 'Worker type').choices(['tool', 'worker', 'sync', 'schedule', 'webhook']))
15
+ .option('--contact <id>', 'Contact ID passed as context (tools)')
16
+ .option('--agent <id>', 'Agent ID passed as context (tools)')
17
+ .addOption(jsonOption('Print the raw execution result as JSON'))
14
18
  .option('--dev', 'Use development environment', false)
15
19
  .action(async (name, options) => {
16
20
  try {
21
+ const payload = await parseData(options.data ?? options.payload);
17
22
  const credentials = await getStoredCredentials();
18
23
  const api = createApiClient({
19
24
  apiKey: credentials.apiKey,
@@ -21,51 +26,53 @@ export const testCommand = new Command('test')
21
26
  zone: credentials.zone,
22
27
  dev: options.dev,
23
28
  });
24
- // Parsear payload
25
- let payload = {};
26
- if (options.payload) {
27
- try {
28
- payload = JSON.parse(options.payload);
29
- }
30
- catch {
31
- logger.error('Payload is not valid JSON');
32
- logger.dim('Example: --payload \'{"product":"iPhone 15"}\'');
33
- process.exit(1);
34
- }
35
- }
36
29
  logger.title(`Running worker "${name}"`);
37
- console.log();
38
30
  if (Object.keys(payload).length > 0) {
39
- logger.label('Payload', JSON.stringify(payload, null, 2));
40
- console.log();
31
+ logger.label('Data', JSON.stringify(payload));
41
32
  }
42
33
  const spinner = createSpinner('Running...');
43
34
  spinner.start();
44
- const response = await api.post('/api/worker/execute', {
45
- workerName: name,
46
- type: options.type,
47
- payload,
48
- triggerSource: 'cli-test',
49
- agentId: options.agent,
50
- contactId: options.contact,
51
- });
52
- const data = response.data;
53
- const duration = data.duration ?? 0;
54
- spinner.stop();
55
- if (data.success) {
35
+ let data;
36
+ try {
37
+ const response = await api.post('/api/worker/execute', {
38
+ workerName: name,
39
+ type: options.type,
40
+ payload,
41
+ triggerSource: 'cli-test',
42
+ agentId: options.agent,
43
+ contactId: options.contact,
44
+ });
45
+ data = response.data;
46
+ }
47
+ catch (error) {
48
+ if (error?.response?.status === 404) {
49
+ throw new CliError(`Worker "${name}" not found or inactive.`, 'not_found', 'Check it is deployed with: plazbot workers list');
50
+ }
51
+ throw error;
52
+ }
53
+ finally {
54
+ spinner.stop();
55
+ }
56
+ const ok = data?.success === true;
57
+ if (isJsonMode()) {
58
+ printJson(data);
59
+ if (!ok)
60
+ process.exit(1);
61
+ return;
62
+ }
63
+ const duration = data?.duration ?? 0;
64
+ if (ok) {
56
65
  logger.success(`Completed in ${duration}ms`);
57
66
  }
58
67
  else {
59
68
  logger.error(`Failed in ${duration}ms`);
60
69
  }
61
- console.log();
62
- // Mostrar resultado
63
- if (data.result !== undefined && data.result !== null) {
70
+ if (data?.result !== undefined && data?.result !== null) {
71
+ console.log();
64
72
  logger.label('Result', '');
65
73
  const result = data.result;
66
74
  // Si es un ToolResponse, mostrar formateado
67
75
  if (typeof result === 'object' && 'result' in result) {
68
- console.log();
69
76
  logger.label(' result', theme.success(String(result.result)));
70
77
  if (result.data) {
71
78
  logger.label(' data', JSON.stringify(result.data, null, 2));
@@ -74,44 +81,85 @@ export const testCommand = new Command('test')
74
81
  logger.label(' actions', '');
75
82
  for (const action of result.actions) {
76
83
  const a = action;
77
- logger.dim(` - ${a.type}: ${a.value}${a.code ? ` (${a.code})` : ''}`);
84
+ logger.dim(` - ${a.type}: ${a.value}${a.code ? ` (${a.code})` : ''}`);
78
85
  }
79
86
  }
80
87
  }
81
88
  else {
82
- // Resultado generico
83
89
  console.log(JSON.stringify(result, null, 2));
84
90
  }
85
91
  }
86
- // Mostrar error si hubo
87
- if (data.error) {
92
+ const errorMessage = data?.error || (!ok ? data?.message : undefined);
93
+ if (errorMessage) {
88
94
  console.log();
89
- logger.label('Error', theme.error(data.error));
95
+ logger.label('Error', theme.error(errorMessage));
96
+ }
97
+ if (Array.isArray(data?.errors) && data.errors.length > 0) {
98
+ data.errors.forEach(e => logger.dim(`- ${typeof e === 'string' ? e : JSON.stringify(e)}`));
90
99
  }
91
100
  console.log();
92
101
  if (options.dev) {
93
102
  logger.warning('Environment: development');
94
103
  }
104
+ if (!ok)
105
+ process.exit(1);
95
106
  }
96
107
  catch (error) {
97
- if (error.response?.status === 404) {
98
- logger.error(`Worker "${name}" not found or inactive`);
99
- logger.dim('Verify the worker is deployed with: plazbot workers list');
100
- }
101
- else {
102
- const message = error.response?.data?.message || error.message || 'Unknown error';
103
- logger.error(message);
104
- }
105
- process.exit(1);
108
+ fail(error);
106
109
  }
107
110
  });
108
111
  addExamples(testCommand, [
109
- { description: 'Run a worker without payload',
110
- command: 'plazbot workers test my-tool' },
111
- { description: 'Run a worker with a JSON payload',
112
- command: 'plazbot workers test get-stock -p \'{"product":"iPhone 15"}\'' },
113
- { description: 'Run a tool with contact and agent context',
114
- command: 'plazbot workers test my-tool --contact ctc_AbcDef123 --agent agt_AbcDef123' },
115
- { description: 'Run against the local backend',
116
- command: 'plazbot workers test my-tool --dev' },
112
+ { description: 'Run a worker without input data',
113
+ command: 'plazbot workers test get-stock' },
114
+ { description: 'Run a worker with JSON data',
115
+ command: 'plazbot workers test get-stock -d \'{"product":"iPhone 15"}\'' },
116
+ { description: 'Read the input data from a file',
117
+ command: 'plazbot workers test sync-orders -d @./payload.json' },
118
+ { description: 'Run a tool with contact and agent context, raw JSON output',
119
+ command: 'plazbot workers test get-stock --contact ctc_AbcDef123 --agent age_AbcDef123 --json' },
117
120
  ]);
121
+ addNotes(testCommand, [
122
+ 'The worker must be deployed first (plazbot workers deploy). Executions are logged with source "cli-test".',
123
+ 'Exits with code 1 when the execution fails (success: false).',
124
+ ]);
125
+ // Parses --data: inline JSON, @file.json, or @- for stdin. Must be a JSON object.
126
+ async function parseData(raw) {
127
+ if (raw === undefined || raw === '')
128
+ return {};
129
+ let text = raw;
130
+ let source = '--data';
131
+ if (raw.startsWith('@')) {
132
+ const file = raw.slice(1);
133
+ if (!file) {
134
+ throw new CliError('Missing file name after "@".', 'invalid_data', 'Example: -d @./payload.json');
135
+ }
136
+ if (file === '-') {
137
+ const chunks = [];
138
+ for await (const chunk of process.stdin)
139
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
140
+ text = Buffer.concat(chunks).toString('utf-8');
141
+ source = 'stdin';
142
+ }
143
+ else {
144
+ try {
145
+ text = await fs.readFile(file, 'utf-8');
146
+ }
147
+ catch {
148
+ throw new CliError(`Could not read data file: ${file}.`, 'file_not_found', 'Check the path after "@".');
149
+ }
150
+ source = file;
151
+ }
152
+ }
153
+ let parsed;
154
+ try {
155
+ parsed = JSON.parse(text);
156
+ }
157
+ catch (error) {
158
+ const detail = error instanceof Error ? ` (${error.message})` : '';
159
+ throw new CliError(`Invalid JSON in ${source}${detail}.`, 'invalid_json', 'Example: -d \'{"product":"iPhone 15"}\' or -d @./payload.json');
160
+ }
161
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
162
+ throw new CliError(`The data in ${source} must be a JSON object.`, 'invalid_data', 'Example: -d \'{"product":"iPhone 15"}\'');
163
+ }
164
+ return parsed;
165
+ }