@zoobbe/cli 1.2.0 → 1.2.2

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.
@@ -0,0 +1,281 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const config = require('../lib/config');
4
+ const client = require('../lib/client');
5
+ const { output, success, error } = require('../lib/output');
6
+ const { withSpinner } = require('../utils/spinner');
7
+ const { formatDuration } = require('../utils/format');
8
+
9
+ const analytics = new Command('analytics')
10
+ .alias('an')
11
+ .description('Analytics and reporting commands');
12
+
13
+ analytics
14
+ .command('board <boardId>')
15
+ .description('Show board analytics overview')
16
+ .action(async (boardId) => {
17
+ try {
18
+ const data = await withSpinner('Fetching analytics...', () =>
19
+ client.get(`/analytics/board/${boardId}`)
20
+ );
21
+
22
+ const a = data.analytics || data.data || data;
23
+ console.log();
24
+ console.log(chalk.bold(' Board Analytics'));
25
+ console.log(chalk.bold(' ─────────────────'));
26
+ console.log(chalk.bold(' Total Cards: '), a.totalCards ?? 0);
27
+ console.log(chalk.bold(' Completed: '), a.completedCards ?? 0);
28
+ console.log(chalk.bold(' In Progress: '), a.inProgressCards ?? 0);
29
+ console.log(chalk.bold(' Overdue: '), a.overdueCards ?? 0);
30
+ console.log(chalk.bold(' Members: '), a.totalMembers ?? 0);
31
+ console.log(chalk.bold(' Completion Rate: '), `${a.completionRate ?? 0}%`);
32
+ console.log();
33
+ } catch (err) {
34
+ error(`Failed to fetch analytics: ${err.message}`);
35
+ }
36
+ });
37
+
38
+ analytics
39
+ .command('metrics <boardId>')
40
+ .description('Show board metrics')
41
+ .option('--range <range>', 'Time range (7d|30d|90d)', '30d')
42
+ .option('-f, --format <format>', 'Output format')
43
+ .action(async (boardId, options) => {
44
+ try {
45
+ const data = await withSpinner('Fetching metrics...', () =>
46
+ client.get(`/analytics/board/${boardId}/metrics?range=${options.range}`)
47
+ );
48
+
49
+ const metrics = data.metrics || data.data || data;
50
+ if (Array.isArray(metrics)) {
51
+ const rows = metrics.map(m => ({
52
+ metric: m.name || m.metric || 'N/A',
53
+ value: String(m.value ?? 0),
54
+ change: m.change ? `${m.change > 0 ? '+' : ''}${m.change}%` : '-',
55
+ }));
56
+ output(rows, {
57
+ headers: ['Metric', 'Value', 'Change'],
58
+ format: options.format,
59
+ });
60
+ } else {
61
+ console.log();
62
+ for (const [key, val] of Object.entries(metrics)) {
63
+ console.log(chalk.bold(` ${key}: `), val);
64
+ }
65
+ console.log();
66
+ }
67
+ } catch (err) {
68
+ error(`Failed to fetch metrics: ${err.message}`);
69
+ }
70
+ });
71
+
72
+ analytics
73
+ .command('time <boardId>')
74
+ .description('Show time analytics for a board')
75
+ .option('-f, --format <format>', 'Output format')
76
+ .action(async (boardId, options) => {
77
+ try {
78
+ const data = await withSpinner('Fetching time analytics...', () =>
79
+ client.get(`/analytics/board/${boardId}/time-analytics`)
80
+ );
81
+
82
+ const time = data.analytics || data.data || data;
83
+ if (Array.isArray(time)) {
84
+ const rows = time.map(t => ({
85
+ list: t.list || t.name || 'N/A',
86
+ avgTime: t.avgTime ? formatDuration(t.avgTime) : '-',
87
+ totalCards: String(t.totalCards ?? 0),
88
+ }));
89
+ output(rows, {
90
+ headers: ['List', 'Avg Time', 'Cards'],
91
+ format: options.format,
92
+ });
93
+ } else {
94
+ console.log();
95
+ console.log(chalk.bold(' Avg Completion: '), time.avgCompletion ? formatDuration(time.avgCompletion) : 'N/A');
96
+ console.log(chalk.bold(' Total Tracked: '), time.totalTracked ? formatDuration(time.totalTracked) : '0s');
97
+ console.log();
98
+ }
99
+ } catch (err) {
100
+ error(`Failed to fetch time analytics: ${err.message}`);
101
+ }
102
+ });
103
+
104
+ analytics
105
+ .command('timeline <boardId>')
106
+ .description('Show full timeline for a board')
107
+ .option('-f, --format <format>', 'Output format')
108
+ .action(async (boardId, options) => {
109
+ try {
110
+ const data = await withSpinner('Fetching timeline...', () =>
111
+ client.get(`/analytics/board/${boardId}/full-timeline`)
112
+ );
113
+
114
+ const timeline = data.timeline || data.data || data;
115
+ const rows = (Array.isArray(timeline) ? timeline : []).map(t => ({
116
+ date: t.date || 'N/A',
117
+ created: String(t.created ?? 0),
118
+ completed: String(t.completed ?? 0),
119
+ active: String(t.active ?? 0),
120
+ }));
121
+
122
+ output(rows, {
123
+ headers: ['Date', 'Created', 'Completed', 'Active'],
124
+ format: options.format,
125
+ });
126
+ } catch (err) {
127
+ error(`Failed to fetch timeline: ${err.message}`);
128
+ }
129
+ });
130
+
131
+ analytics
132
+ .command('productivity <boardId>')
133
+ .description('Show user productivity for a board')
134
+ .option('-f, --format <format>', 'Output format')
135
+ .action(async (boardId, options) => {
136
+ try {
137
+ const data = await withSpinner('Fetching productivity...', () =>
138
+ client.get(`/analytics/board/${boardId}/user-productivity`)
139
+ );
140
+
141
+ const users = data.users || data.data || data;
142
+ const rows = (Array.isArray(users) ? users : []).map(u => ({
143
+ user: u.userName || u.name || 'N/A',
144
+ completed: String(u.completedCards ?? 0),
145
+ assigned: String(u.assignedCards ?? 0),
146
+ timeTracked: u.timeTracked ? formatDuration(u.timeTracked) : '-',
147
+ }));
148
+
149
+ output(rows, {
150
+ headers: ['User', 'Completed', 'Assigned', 'Time Tracked'],
151
+ format: options.format,
152
+ });
153
+ } catch (err) {
154
+ error(`Failed to fetch productivity: ${err.message}`);
155
+ }
156
+ });
157
+
158
+ analytics
159
+ .command('workflow <boardId>')
160
+ .description('Show workflow analytics for a board')
161
+ .option('-f, --format <format>', 'Output format')
162
+ .action(async (boardId, options) => {
163
+ try {
164
+ const data = await withSpinner('Fetching workflow analytics...', () =>
165
+ client.get(`/analytics/board/${boardId}/workflow-analytics`)
166
+ );
167
+
168
+ const workflow = data.workflow || data.data || data;
169
+ const rows = (Array.isArray(workflow) ? workflow : []).map(w => ({
170
+ list: w.list || w.name || 'N/A',
171
+ cards: String(w.cardCount ?? 0),
172
+ avgTime: w.avgTime ? formatDuration(w.avgTime) : '-',
173
+ throughput: String(w.throughput ?? 0),
174
+ }));
175
+
176
+ output(rows, {
177
+ headers: ['List', 'Cards', 'Avg Time', 'Throughput'],
178
+ format: options.format,
179
+ });
180
+ } catch (err) {
181
+ error(`Failed to fetch workflow analytics: ${err.message}`);
182
+ }
183
+ });
184
+
185
+ analytics
186
+ .command('workspace')
187
+ .description('Show workspace analytics')
188
+ .action(async () => {
189
+ try {
190
+ const workspaceId = config.get('activeWorkspace');
191
+ if (!workspaceId) return error('No active workspace.');
192
+
193
+ const data = await withSpinner('Fetching workspace analytics...', () =>
194
+ client.get(`/analytics/workspace/${workspaceId}`)
195
+ );
196
+
197
+ const a = data.analytics || data.data || data;
198
+ console.log();
199
+ console.log(chalk.bold(' Workspace Analytics'));
200
+ console.log(chalk.bold(' ─────────────────────'));
201
+ console.log(chalk.bold(' Total Boards: '), a.totalBoards ?? 0);
202
+ console.log(chalk.bold(' Total Cards: '), a.totalCards ?? 0);
203
+ console.log(chalk.bold(' Active Members:'), a.activeMembers ?? 0);
204
+ console.log(chalk.bold(' Completion: '), `${a.completionRate ?? 0}%`);
205
+ console.log();
206
+ } catch (err) {
207
+ error(`Failed to fetch workspace analytics: ${err.message}`);
208
+ }
209
+ });
210
+
211
+ analytics
212
+ .command('user')
213
+ .description('Show your personal analytics')
214
+ .action(async () => {
215
+ try {
216
+ const userId = config.get('userId');
217
+ const data = await withSpinner('Fetching user analytics...', () =>
218
+ client.get(`/analytics/user/${userId}`)
219
+ );
220
+
221
+ const a = data.analytics || data.data || data;
222
+ console.log();
223
+ console.log(chalk.bold(' Your Analytics'));
224
+ console.log(chalk.bold(' ──────────────'));
225
+ console.log(chalk.bold(' Cards Completed: '), a.completedCards ?? 0);
226
+ console.log(chalk.bold(' Cards Assigned: '), a.assignedCards ?? 0);
227
+ console.log(chalk.bold(' Time Tracked: '), a.timeTracked ? formatDuration(a.timeTracked) : '0s');
228
+ console.log(chalk.bold(' Active Boards: '), a.activeBoards ?? 0);
229
+ console.log();
230
+ } catch (err) {
231
+ error(`Failed to fetch user analytics: ${err.message}`);
232
+ }
233
+ });
234
+
235
+ analytics
236
+ .command('trends')
237
+ .description('Show analytics trends')
238
+ .option('-f, --format <format>', 'Output format')
239
+ .action(async (options) => {
240
+ try {
241
+ const data = await withSpinner('Fetching trends...', () =>
242
+ client.get('/analytics/trends')
243
+ );
244
+
245
+ const trends = data.trends || data.data || data;
246
+ const rows = (Array.isArray(trends) ? trends : []).map(t => ({
247
+ period: t.period || t.date || 'N/A',
248
+ cards: String(t.cardsCreated ?? 0),
249
+ completed: String(t.cardsCompleted ?? 0),
250
+ members: String(t.activeMembers ?? 0),
251
+ }));
252
+
253
+ output(rows, {
254
+ headers: ['Period', 'Created', 'Completed', 'Active Members'],
255
+ format: options.format,
256
+ });
257
+ } catch (err) {
258
+ error(`Failed to fetch trends: ${err.message}`);
259
+ }
260
+ });
261
+
262
+ analytics
263
+ .command('export <boardId>')
264
+ .description('Export board analytics')
265
+ .option('-o, --output <file>', 'Output file path', 'analytics-export.json')
266
+ .action(async (boardId, options) => {
267
+ try {
268
+ const data = await withSpinner('Exporting analytics...', () =>
269
+ client.get(`/analytics/export/${boardId}`)
270
+ );
271
+
272
+ const fs = require('fs');
273
+ const exportData = data.export || data.data || data;
274
+ fs.writeFileSync(options.output, JSON.stringify(exportData, null, 2));
275
+ success(`Analytics exported to ${chalk.bold(options.output)}`);
276
+ } catch (err) {
277
+ error(`Failed to export analytics: ${err.message}`);
278
+ }
279
+ });
280
+
281
+ module.exports = analytics;
@@ -0,0 +1,140 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const client = require('../lib/client');
4
+ const { output, success, error, warn } = require('../lib/output');
5
+ const { withSpinner } = require('../utils/spinner');
6
+ const { confirmAction } = require('../utils/prompts');
7
+
8
+ const apiKey = new Command('api-key')
9
+ .alias('ak')
10
+ .description('API key management commands (some operations require session auth)');
11
+
12
+ const AUTH_NOTE = 'Note: API key management may be restricted when authenticated via API key. Use browser session if this fails.';
13
+
14
+ apiKey
15
+ .command('list')
16
+ .alias('ls')
17
+ .description('List your API keys')
18
+ .option('-f, --format <format>', 'Output format')
19
+ .action(async (options) => {
20
+ try {
21
+ const data = await withSpinner('Fetching API keys...', () =>
22
+ client.get('/api-keys')
23
+ );
24
+
25
+ const keys = data.apiKeys || data.data || data;
26
+ const rows = (Array.isArray(keys) ? keys : []).map(k => ({
27
+ name: k.name || 'Unnamed',
28
+ prefix: k.prefix || k.key?.substring(0, 8) || 'N/A',
29
+ created: new Date(k.createdAt).toLocaleDateString(),
30
+ lastUsed: k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'Never',
31
+ id: k._id,
32
+ }));
33
+
34
+ output(rows, {
35
+ headers: ['Name', 'Prefix', 'Created', 'Last Used', 'ID'],
36
+ format: options.format,
37
+ });
38
+ } catch (err) {
39
+ warn(AUTH_NOTE);
40
+ error(`Failed to list API keys: ${err.message}`);
41
+ }
42
+ });
43
+
44
+ apiKey
45
+ .command('create')
46
+ .description('Create a new API key')
47
+ .option('--name <name>', 'Key name (required)')
48
+ .action(async (options) => {
49
+ try {
50
+ if (!options.name) return error('Key name is required. Use --name <name>.');
51
+
52
+ const data = await withSpinner('Creating API key...', () =>
53
+ client.post('/api-keys', { name: options.name })
54
+ );
55
+
56
+ const key = data.apiKey || data.key || data.data || data;
57
+ console.log();
58
+ success(`Created API key: ${chalk.bold(options.name)}`);
59
+ if (key.key || key.token) {
60
+ console.log();
61
+ console.log(chalk.yellow(' Save this key — it won\'t be shown again:'));
62
+ console.log(chalk.bold(` ${key.key || key.token}`));
63
+ console.log();
64
+ }
65
+ } catch (err) {
66
+ warn(AUTH_NOTE);
67
+ error(`Failed to create API key: ${err.message}`);
68
+ }
69
+ });
70
+
71
+ apiKey
72
+ .command('delete <keyId>')
73
+ .description('Delete an API key')
74
+ .option('--force', 'Skip confirmation')
75
+ .action(async (keyId, options) => {
76
+ try {
77
+ if (!options.force) {
78
+ const confirmed = await confirmAction('Delete this API key? Any integrations using it will stop working.');
79
+ if (!confirmed) return;
80
+ }
81
+
82
+ await withSpinner('Deleting API key...', () =>
83
+ client.delete(`/api-keys/${keyId}`)
84
+ );
85
+ success('Deleted API key');
86
+ } catch (err) {
87
+ warn(AUTH_NOTE);
88
+ error(`Failed to delete API key: ${err.message}`);
89
+ }
90
+ });
91
+
92
+ apiKey
93
+ .command('rename <keyId>')
94
+ .description('Rename an API key')
95
+ .option('--name <name>', 'New name (required)')
96
+ .action(async (keyId, options) => {
97
+ try {
98
+ if (!options.name) return error('Name is required. Use --name <name>.');
99
+
100
+ await withSpinner('Renaming API key...', () =>
101
+ client.put(`/api-keys/${keyId}`, { name: options.name })
102
+ );
103
+ success(`Renamed API key to: ${chalk.bold(options.name)}`);
104
+ } catch (err) {
105
+ warn(AUTH_NOTE);
106
+ error(`Failed to rename API key: ${err.message}`);
107
+ }
108
+ });
109
+
110
+ apiKey
111
+ .command('regenerate <keyId>')
112
+ .description('Regenerate an API key')
113
+ .option('--force', 'Skip confirmation')
114
+ .action(async (keyId, options) => {
115
+ try {
116
+ if (!options.force) {
117
+ const confirmed = await confirmAction('Regenerate this API key? The old key will stop working immediately.');
118
+ if (!confirmed) return;
119
+ }
120
+
121
+ const data = await withSpinner('Regenerating API key...', () =>
122
+ client.post(`/api-keys/${keyId}/regenerate`)
123
+ );
124
+
125
+ const key = data.apiKey || data.key || data.data || data;
126
+ console.log();
127
+ success('API key regenerated');
128
+ if (key.key || key.token) {
129
+ console.log();
130
+ console.log(chalk.yellow(' Save this key — it won\'t be shown again:'));
131
+ console.log(chalk.bold(` ${key.key || key.token}`));
132
+ console.log();
133
+ }
134
+ } catch (err) {
135
+ warn(AUTH_NOTE);
136
+ error(`Failed to regenerate API key: ${err.message}`);
137
+ }
138
+ });
139
+
140
+ module.exports = apiKey;
@@ -245,6 +245,8 @@ auth
245
245
  .description('Clear stored credentials')
246
246
  .action(() => {
247
247
  config.set('apiKey', '');
248
+ config.set('apiUrl', 'https://api.zoobbe.com');
249
+ config.set('webUrl', '');
248
250
  config.set('userId', '');
249
251
  config.set('userName', '');
250
252
  config.set('email', '');
@@ -0,0 +1,255 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const client = require('../lib/client');
4
+ const { output, success, error } = require('../lib/output');
5
+ const { withSpinner } = require('../utils/spinner');
6
+ const { confirmAction } = require('../utils/prompts');
7
+ const { formatRelativeTime } = require('../utils/format');
8
+
9
+ const automation = new Command('automation')
10
+ .alias('auto')
11
+ .description('Board automation commands');
12
+
13
+ automation
14
+ .command('list')
15
+ .alias('ls')
16
+ .description('List automations on a board')
17
+ .option('-b, --board <boardId>', 'Board ID (required)')
18
+ .option('-f, --format <format>', 'Output format')
19
+ .action(async (options) => {
20
+ try {
21
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
22
+
23
+ const data = await withSpinner('Fetching automations...', () =>
24
+ client.get(`/boards/${options.board}/automations`)
25
+ );
26
+
27
+ const automations = data.automations || data.data || data;
28
+ const rows = (Array.isArray(automations) ? automations : []).map(a => ({
29
+ name: a.name || a.title || 'Unnamed',
30
+ trigger: a.trigger?.type || a.triggerType || 'N/A',
31
+ action: a.action?.type || a.actionType || 'N/A',
32
+ status: a.enabled ? chalk.green('enabled') : chalk.yellow('disabled'),
33
+ id: a._id,
34
+ }));
35
+
36
+ output(rows, {
37
+ headers: ['Name', 'Trigger', 'Action', 'Status', 'ID'],
38
+ format: options.format,
39
+ });
40
+ } catch (err) {
41
+ error(`Failed to list automations: ${err.message}`);
42
+ }
43
+ });
44
+
45
+ automation
46
+ .command('create')
47
+ .description('Create a new automation (interactive)')
48
+ .option('-b, --board <boardId>', 'Board ID (required)')
49
+ .action(async (options) => {
50
+ try {
51
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
52
+
53
+ const inquirer = require('inquirer');
54
+
55
+ // Fetch available options
56
+ const optionsData = await withSpinner('Fetching automation options...', () =>
57
+ client.get(`/boards/${options.board}/automations/options`)
58
+ );
59
+ const opts = optionsData.options || optionsData.data || optionsData;
60
+
61
+ const triggers = opts.triggers || ['card_created', 'card_moved', 'card_completed', 'due_date_reached'];
62
+ const actions = opts.actions || ['move_card', 'assign_member', 'add_label', 'send_notification'];
63
+
64
+ const answers = await inquirer.prompt([
65
+ {
66
+ type: 'input',
67
+ name: 'name',
68
+ message: 'Automation name:',
69
+ validate: v => v.length > 0 || 'Name is required',
70
+ },
71
+ {
72
+ type: 'list',
73
+ name: 'triggerType',
74
+ message: 'Select trigger:',
75
+ choices: triggers,
76
+ },
77
+ {
78
+ type: 'list',
79
+ name: 'actionType',
80
+ message: 'Select action:',
81
+ choices: actions,
82
+ },
83
+ ]);
84
+
85
+ const data = await withSpinner('Creating automation...', () =>
86
+ client.post(`/boards/${options.board}/automations`, {
87
+ name: answers.name,
88
+ trigger: { type: answers.triggerType },
89
+ action: { type: answers.actionType },
90
+ })
91
+ );
92
+
93
+ const a = data.automation || data.data || data;
94
+ success(`Created automation: ${chalk.bold(answers.name)} (${a._id})`);
95
+ } catch (err) {
96
+ error(`Failed to create automation: ${err.message}`);
97
+ }
98
+ });
99
+
100
+ automation
101
+ .command('update <automationId>')
102
+ .description('Update an automation')
103
+ .option('-b, --board <boardId>', 'Board ID (required)')
104
+ .option('--name <name>', 'New name')
105
+ .action(async (automationId, options) => {
106
+ try {
107
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
108
+
109
+ const body = {};
110
+ if (options.name) body.name = options.name;
111
+ if (Object.keys(body).length === 0) return error('Provide --name.');
112
+
113
+ await withSpinner('Updating automation...', () =>
114
+ client.put(`/boards/${options.board}/automations/${automationId}`, body)
115
+ );
116
+ success(`Updated automation ${automationId}`);
117
+ } catch (err) {
118
+ error(`Failed to update automation: ${err.message}`);
119
+ }
120
+ });
121
+
122
+ automation
123
+ .command('delete <automationId>')
124
+ .description('Delete an automation')
125
+ .option('-b, --board <boardId>', 'Board ID (required)')
126
+ .option('--force', 'Skip confirmation')
127
+ .action(async (automationId, options) => {
128
+ try {
129
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
130
+
131
+ if (!options.force) {
132
+ const confirmed = await confirmAction('Delete this automation?');
133
+ if (!confirmed) return;
134
+ }
135
+
136
+ await withSpinner('Deleting automation...', () =>
137
+ client.delete(`/boards/${options.board}/automations/${automationId}`)
138
+ );
139
+ success('Deleted automation');
140
+ } catch (err) {
141
+ error(`Failed to delete automation: ${err.message}`);
142
+ }
143
+ });
144
+
145
+ automation
146
+ .command('toggle <automationId>')
147
+ .description('Enable or disable an automation')
148
+ .option('-b, --board <boardId>', 'Board ID (required)')
149
+ .action(async (automationId, options) => {
150
+ try {
151
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
152
+
153
+ const data = await withSpinner('Toggling automation...', () =>
154
+ client.patch(`/boards/${options.board}/automations/${automationId}/toggle`)
155
+ );
156
+ const a = data.automation || data.data || data;
157
+ success(a.enabled ? 'Automation enabled' : 'Automation disabled');
158
+ } catch (err) {
159
+ error(`Failed to toggle automation: ${err.message}`);
160
+ }
161
+ });
162
+
163
+ automation
164
+ .command('logs <automationId>')
165
+ .description('Show automation execution logs')
166
+ .option('-b, --board <boardId>', 'Board ID (required)')
167
+ .option('--limit <n>', 'Number of logs', '20')
168
+ .option('-f, --format <format>', 'Output format')
169
+ .action(async (automationId, options) => {
170
+ try {
171
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
172
+
173
+ const data = await withSpinner('Fetching logs...', () =>
174
+ client.get(`/boards/${options.board}/automations/${automationId}/logs`)
175
+ );
176
+
177
+ const logs = data.logs || data.data || data;
178
+ const logArr = Array.isArray(logs) ? logs : [];
179
+ const limited = logArr.slice(0, parseInt(options.limit));
180
+
181
+ const rows = limited.map(l => ({
182
+ status: l.success ? chalk.green('success') : chalk.red('failed'),
183
+ trigger: l.trigger || 'N/A',
184
+ time: formatRelativeTime(l.createdAt || l.executedAt),
185
+ error: l.error || '-',
186
+ }));
187
+
188
+ output(rows, {
189
+ headers: ['Status', 'Trigger', 'Time', 'Error'],
190
+ format: options.format,
191
+ });
192
+ } catch (err) {
193
+ error(`Failed to fetch logs: ${err.message}`);
194
+ }
195
+ });
196
+
197
+ automation
198
+ .command('options')
199
+ .description('Show available automation triggers and actions')
200
+ .option('-b, --board <boardId>', 'Board ID (required)')
201
+ .action(async (options) => {
202
+ try {
203
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
204
+
205
+ const data = await withSpinner('Fetching options...', () =>
206
+ client.get(`/boards/${options.board}/automations/options`)
207
+ );
208
+
209
+ const opts = data.options || data.data || data;
210
+ console.log();
211
+ console.log(chalk.bold(' Available Triggers:'));
212
+ const triggers = opts.triggers || [];
213
+ triggers.forEach(t => console.log(` - ${t.type || t}`));
214
+ console.log();
215
+ console.log(chalk.bold(' Available Actions:'));
216
+ const actions = opts.actions || [];
217
+ actions.forEach(a => console.log(` - ${a.type || a}`));
218
+ console.log();
219
+ } catch (err) {
220
+ error(`Failed to fetch options: ${err.message}`);
221
+ }
222
+ });
223
+
224
+ automation
225
+ .command('info <automationId>')
226
+ .description('Show automation details')
227
+ .option('-b, --board <boardId>', 'Board ID (required)')
228
+ .action(async (automationId, options) => {
229
+ try {
230
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
231
+
232
+ // Fetch all automations and find the one
233
+ const data = await withSpinner('Fetching automation...', () =>
234
+ client.get(`/boards/${options.board}/automations`)
235
+ );
236
+
237
+ const automations = data.automations || data.data || data;
238
+ const match = (Array.isArray(automations) ? automations : []).find(a => a._id === automationId);
239
+
240
+ if (!match) return error(`Automation "${automationId}" not found.`);
241
+
242
+ console.log();
243
+ console.log(chalk.bold(' Name: '), match.name || 'Unnamed');
244
+ console.log(chalk.bold(' ID: '), match._id);
245
+ console.log(chalk.bold(' Trigger: '), match.trigger?.type || 'N/A');
246
+ console.log(chalk.bold(' Action: '), match.action?.type || 'N/A');
247
+ console.log(chalk.bold(' Status: '), match.enabled ? 'Enabled' : 'Disabled');
248
+ console.log(chalk.bold(' Runs: '), match.executionCount ?? 0);
249
+ console.log();
250
+ } catch (err) {
251
+ error(`Failed to get automation info: ${err.message}`);
252
+ }
253
+ });
254
+
255
+ module.exports = automation;