@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.
- package/README.md +243 -19
- package/bin/zoobbe-mcp +5 -0
- package/package.json +6 -3
- package/src/commands/activity.js +102 -0
- package/src/commands/analytics.js +281 -0
- package/src/commands/api-key.js +140 -0
- package/src/commands/auth.js +2 -0
- package/src/commands/automation.js +255 -0
- package/src/commands/board.js +295 -0
- package/src/commands/card.js +367 -0
- package/src/commands/checklist.js +173 -0
- package/src/commands/import.js +101 -0
- package/src/commands/list.js +213 -0
- package/src/commands/notification.js +92 -0
- package/src/commands/page.js +253 -0
- package/src/commands/timer.js +234 -0
- package/src/commands/webhook.js +141 -0
- package/src/commands/workspace.js +174 -0
- package/src/index.js +10 -0
- package/src/lib/client.js +42 -2
- package/src/mcp-server.js +797 -0
- package/src/utils/format.js +39 -0
- package/src/utils/prompts.js +40 -0
- package/src/utils/resolve.js +67 -0
package/src/commands/page.js
CHANGED
|
@@ -98,4 +98,257 @@ page
|
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
// ── Extended commands ──
|
|
102
|
+
|
|
103
|
+
const { confirmAction } = require('../utils/prompts');
|
|
104
|
+
|
|
105
|
+
page
|
|
106
|
+
.command('update <pageId>')
|
|
107
|
+
.description('Update a page')
|
|
108
|
+
.option('-t, --title <title>', 'New title')
|
|
109
|
+
.option('--icon <icon>', 'Page icon/emoji')
|
|
110
|
+
.action(async (pageId, options) => {
|
|
111
|
+
try {
|
|
112
|
+
const body = {};
|
|
113
|
+
if (options.title) body.title = options.title;
|
|
114
|
+
if (options.icon) body.icon = options.icon;
|
|
115
|
+
if (Object.keys(body).length === 0) return error('Provide -t or --icon.');
|
|
116
|
+
|
|
117
|
+
await withSpinner('Updating page...', () =>
|
|
118
|
+
client.put(`/pages/${pageId}`, body)
|
|
119
|
+
);
|
|
120
|
+
success(`Updated page ${pageId}`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
error(`Failed to update page: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
page
|
|
127
|
+
.command('delete <pageId>')
|
|
128
|
+
.description('Delete a page')
|
|
129
|
+
.option('--permanent', 'Permanently delete (cannot be restored)')
|
|
130
|
+
.option('--force', 'Skip confirmation')
|
|
131
|
+
.action(async (pageId, options) => {
|
|
132
|
+
try {
|
|
133
|
+
if (!options.force) {
|
|
134
|
+
const msg = options.permanent
|
|
135
|
+
? 'Permanently delete this page? This cannot be undone.'
|
|
136
|
+
: 'Delete this page?';
|
|
137
|
+
const confirmed = await confirmAction(msg);
|
|
138
|
+
if (!confirmed) return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const url = options.permanent ? `/pages/${pageId}/permanent` : `/pages/${pageId}`;
|
|
142
|
+
await withSpinner('Deleting page...', () =>
|
|
143
|
+
client.delete(url)
|
|
144
|
+
);
|
|
145
|
+
success(`Deleted page ${pageId}`);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
error(`Failed to delete page: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
page
|
|
152
|
+
.command('archive <pageId>')
|
|
153
|
+
.description('Archive a page')
|
|
154
|
+
.action(async (pageId) => {
|
|
155
|
+
try {
|
|
156
|
+
await withSpinner('Archiving page...', () =>
|
|
157
|
+
client.post(`/pages/${pageId}/archive`)
|
|
158
|
+
);
|
|
159
|
+
success(`Archived page ${pageId}`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
error(`Failed to archive page: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
page
|
|
166
|
+
.command('restore <pageId>')
|
|
167
|
+
.description('Restore an archived page')
|
|
168
|
+
.action(async (pageId) => {
|
|
169
|
+
try {
|
|
170
|
+
await withSpinner('Restoring page...', () =>
|
|
171
|
+
client.post(`/pages/${pageId}/restore`)
|
|
172
|
+
);
|
|
173
|
+
success(`Restored page ${pageId}`);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
error(`Failed to restore page: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
page
|
|
180
|
+
.command('share <pageId>')
|
|
181
|
+
.description('Manage page sharing')
|
|
182
|
+
.option('--public', 'Make page publicly accessible')
|
|
183
|
+
.option('--revoke', 'Revoke public access')
|
|
184
|
+
.option('-u, --user <userId>', 'Share with a specific user')
|
|
185
|
+
.option('--role <role>', 'Role for shared user (editor|viewer)', 'viewer')
|
|
186
|
+
.action(async (pageId, options) => {
|
|
187
|
+
try {
|
|
188
|
+
if (options.revoke) {
|
|
189
|
+
await withSpinner('Revoking share...', () =>
|
|
190
|
+
client.post(`/pages/${pageId}/share/revoke`)
|
|
191
|
+
);
|
|
192
|
+
success('Public sharing revoked');
|
|
193
|
+
} else if (options.public) {
|
|
194
|
+
const data = await withSpinner('Enabling public share...', () =>
|
|
195
|
+
client.post(`/pages/${pageId}/share/public`)
|
|
196
|
+
);
|
|
197
|
+
const link = data.shareLink || data.link || data.data;
|
|
198
|
+
success(`Page shared publicly${link ? `: ${chalk.bold(link)}` : ''}`);
|
|
199
|
+
} else if (options.user) {
|
|
200
|
+
await withSpinner('Sharing with user...', () =>
|
|
201
|
+
client.post(`/pages/${pageId}/share/user`, {
|
|
202
|
+
userId: options.user,
|
|
203
|
+
role: options.role,
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
success(`Shared page with user (${options.role})`);
|
|
207
|
+
} else {
|
|
208
|
+
return error('Provide --public, --revoke, or -u <userId>.');
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`Failed to share page: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
page
|
|
216
|
+
.command('members <pageId>')
|
|
217
|
+
.description('List page members')
|
|
218
|
+
.option('-f, --format <format>', 'Output format')
|
|
219
|
+
.action(async (pageId, options) => {
|
|
220
|
+
try {
|
|
221
|
+
const data = await withSpinner('Fetching members...', () =>
|
|
222
|
+
client.get(`/pages/${pageId}/members`)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const members = data.members || data.data || data;
|
|
226
|
+
const rows = (Array.isArray(members) ? members : []).map(m => {
|
|
227
|
+
const user = m.user || m;
|
|
228
|
+
return {
|
|
229
|
+
name: user.userName || user.username || user.name || 'N/A',
|
|
230
|
+
email: user.email || 'N/A',
|
|
231
|
+
role: m.role || 'viewer',
|
|
232
|
+
id: user._id || user.id || '',
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
output(rows, {
|
|
237
|
+
headers: ['Name', 'Email', 'Role', 'ID'],
|
|
238
|
+
format: options.format,
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
error(`Failed to list members: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
page
|
|
246
|
+
.command('add-member <pageId>')
|
|
247
|
+
.description('Add a member to a page')
|
|
248
|
+
.option('-u, --user <userId>', 'User ID (required)')
|
|
249
|
+
.option('--role <role>', 'Role (editor|viewer)', 'viewer')
|
|
250
|
+
.action(async (pageId, options) => {
|
|
251
|
+
try {
|
|
252
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
253
|
+
|
|
254
|
+
await withSpinner('Adding member...', () =>
|
|
255
|
+
client.post(`/pages/${pageId}/members`, {
|
|
256
|
+
userId: options.user,
|
|
257
|
+
role: options.role,
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
success(`Added member to page ${pageId}`);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
error(`Failed to add member: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
page
|
|
267
|
+
.command('remove-member <pageId>')
|
|
268
|
+
.description('Remove a member from a page')
|
|
269
|
+
.option('-u, --user <userId>', 'User/member ID (required)')
|
|
270
|
+
.action(async (pageId, options) => {
|
|
271
|
+
try {
|
|
272
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
273
|
+
|
|
274
|
+
await withSpinner('Removing member...', () =>
|
|
275
|
+
client.delete(`/pages/${pageId}/members/${options.user}`)
|
|
276
|
+
);
|
|
277
|
+
success('Removed member from page');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
error(`Failed to remove member: ${err.message}`);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
page
|
|
284
|
+
.command('comments <pageId>')
|
|
285
|
+
.description('List comments on a page')
|
|
286
|
+
.option('-f, --format <format>', 'Output format')
|
|
287
|
+
.action(async (pageId, options) => {
|
|
288
|
+
try {
|
|
289
|
+
const data = await withSpinner('Fetching comments...', () =>
|
|
290
|
+
client.get(`/pages/${pageId}/comments`)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const { formatRelativeTime } = require('../utils/format');
|
|
294
|
+
const comments = data.comments || data.data || data;
|
|
295
|
+
const rows = (Array.isArray(comments) ? comments : []).map(c => ({
|
|
296
|
+
user: c.user?.userName || c.member?.userName || 'N/A',
|
|
297
|
+
comment: (c.comment || c.text || '').substring(0, 80),
|
|
298
|
+
time: formatRelativeTime(c.createdAt),
|
|
299
|
+
id: c._id,
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
output(rows, {
|
|
303
|
+
headers: ['User', 'Comment', 'Time', 'ID'],
|
|
304
|
+
format: options.format,
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
error(`Failed to list comments: ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
page
|
|
312
|
+
.command('comment <pageId> <text>')
|
|
313
|
+
.description('Add a comment to a page')
|
|
314
|
+
.action(async (pageId, text) => {
|
|
315
|
+
try {
|
|
316
|
+
await withSpinner('Adding comment...', () =>
|
|
317
|
+
client.post(`/pages/${pageId}/comments`, { comment: text })
|
|
318
|
+
);
|
|
319
|
+
success('Comment added');
|
|
320
|
+
} catch (err) {
|
|
321
|
+
error(`Failed to add comment: ${err.message}`);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
page
|
|
326
|
+
.command('duplicate <pageId>')
|
|
327
|
+
.description('Duplicate a page')
|
|
328
|
+
.action(async (pageId) => {
|
|
329
|
+
try {
|
|
330
|
+
const data = await withSpinner('Duplicating page...', () =>
|
|
331
|
+
client.post(`/pages/${pageId}/duplicate`)
|
|
332
|
+
);
|
|
333
|
+
const p = data.page || data.data || data;
|
|
334
|
+
success(`Duplicated page: ${chalk.bold(p.title || 'Copy')} (${p.shortId || p._id})`);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
error(`Failed to duplicate page: ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
page
|
|
341
|
+
.command('favorite <pageId>')
|
|
342
|
+
.description('Toggle favorite on a page')
|
|
343
|
+
.action(async (pageId) => {
|
|
344
|
+
try {
|
|
345
|
+
const data = await withSpinner('Toggling favorite...', () =>
|
|
346
|
+
client.post(`/pages/${pageId}/favorite`)
|
|
347
|
+
);
|
|
348
|
+
success(data.message || 'Favorite toggled');
|
|
349
|
+
} catch (err) {
|
|
350
|
+
error(`Failed to toggle favorite: ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
101
354
|
module.exports = page;
|
|
@@ -0,0 +1,234 @@
|
|
|
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 { formatDuration, formatRelativeTime } = require('../utils/format');
|
|
7
|
+
|
|
8
|
+
const timer = new Command('timer')
|
|
9
|
+
.alias('t')
|
|
10
|
+
.description('Card timer management commands');
|
|
11
|
+
|
|
12
|
+
timer
|
|
13
|
+
.command('start <cardId>')
|
|
14
|
+
.description('Start a timer on a card')
|
|
15
|
+
.option('--mode <mode>', 'Timer mode (stopwatch|pomodoro)', 'stopwatch')
|
|
16
|
+
.action(async (cardId, options) => {
|
|
17
|
+
try {
|
|
18
|
+
await withSpinner('Starting timer...', () =>
|
|
19
|
+
client.post(`/timers/cards/${cardId}/timer/start`, { mode: options.mode })
|
|
20
|
+
);
|
|
21
|
+
success(`Timer started on card ${cardId} (${options.mode} mode)`);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
error(`Failed to start timer: ${err.message}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
timer
|
|
28
|
+
.command('pause <cardId>')
|
|
29
|
+
.description('Pause a running timer')
|
|
30
|
+
.action(async (cardId) => {
|
|
31
|
+
try {
|
|
32
|
+
await withSpinner('Pausing timer...', () =>
|
|
33
|
+
client.post(`/timers/cards/${cardId}/timer/pause`)
|
|
34
|
+
);
|
|
35
|
+
success('Timer paused');
|
|
36
|
+
} catch (err) {
|
|
37
|
+
error(`Failed to pause timer: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
timer
|
|
42
|
+
.command('resume <cardId>')
|
|
43
|
+
.description('Resume a paused timer')
|
|
44
|
+
.action(async (cardId) => {
|
|
45
|
+
try {
|
|
46
|
+
await withSpinner('Resuming timer...', () =>
|
|
47
|
+
client.post(`/timers/cards/${cardId}/timer/resume`)
|
|
48
|
+
);
|
|
49
|
+
success('Timer resumed');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
error(`Failed to resume timer: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
timer
|
|
56
|
+
.command('stop <cardId>')
|
|
57
|
+
.description('Stop a timer and save the session')
|
|
58
|
+
.action(async (cardId) => {
|
|
59
|
+
try {
|
|
60
|
+
const data = await withSpinner('Stopping timer...', () =>
|
|
61
|
+
client.post(`/timers/cards/${cardId}/timer/stop`)
|
|
62
|
+
);
|
|
63
|
+
const duration = data.duration || data.elapsed;
|
|
64
|
+
success(`Timer stopped${duration ? ` — ${formatDuration(duration)}` : ''}`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
error(`Failed to stop timer: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
timer
|
|
71
|
+
.command('reset <cardId>')
|
|
72
|
+
.description('Reset a timer')
|
|
73
|
+
.action(async (cardId) => {
|
|
74
|
+
try {
|
|
75
|
+
await withSpinner('Resetting timer...', () =>
|
|
76
|
+
client.post(`/timers/cards/${cardId}/timer/reset`)
|
|
77
|
+
);
|
|
78
|
+
success('Timer reset');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
error(`Failed to reset timer: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
timer
|
|
85
|
+
.command('mode <cardId> <mode>')
|
|
86
|
+
.description('Switch timer mode (stopwatch|pomodoro)')
|
|
87
|
+
.action(async (cardId, mode) => {
|
|
88
|
+
try {
|
|
89
|
+
await withSpinner('Switching mode...', () =>
|
|
90
|
+
client.post(`/timers/cards/${cardId}/timer/mode`, { mode })
|
|
91
|
+
);
|
|
92
|
+
success(`Timer mode set to ${chalk.bold(mode)}`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
error(`Failed to switch mode: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
timer
|
|
99
|
+
.command('get <cardId>')
|
|
100
|
+
.description('Get current timer state for a card')
|
|
101
|
+
.action(async (cardId) => {
|
|
102
|
+
try {
|
|
103
|
+
const data = await withSpinner('Fetching timer...', () =>
|
|
104
|
+
client.get(`/timers/cards/${cardId}/timer`)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const t = data.timer || data.data || data;
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.bold(' Status: '), t.status || t.state || 'none');
|
|
110
|
+
console.log(chalk.bold(' Mode: '), t.mode || 'stopwatch');
|
|
111
|
+
console.log(chalk.bold(' Elapsed: '), t.elapsed ? formatDuration(t.elapsed) : '0s');
|
|
112
|
+
if (t.startedAt) {
|
|
113
|
+
console.log(chalk.bold(' Started: '), formatRelativeTime(t.startedAt));
|
|
114
|
+
}
|
|
115
|
+
console.log();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
error(`Failed to get timer: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
timer
|
|
122
|
+
.command('sessions <cardId>')
|
|
123
|
+
.description('List timer sessions for a card')
|
|
124
|
+
.option('-f, --format <format>', 'Output format')
|
|
125
|
+
.action(async (cardId, options) => {
|
|
126
|
+
try {
|
|
127
|
+
const data = await withSpinner('Fetching sessions...', () =>
|
|
128
|
+
client.get(`/timers/cards/${cardId}/timer/sessions`)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const sessions = data.sessions || data.data || data;
|
|
132
|
+
const rows = (Array.isArray(sessions) ? sessions : []).map(s => ({
|
|
133
|
+
date: s.startedAt ? new Date(s.startedAt).toLocaleDateString() : '-',
|
|
134
|
+
duration: s.duration ? formatDuration(s.duration) : '-',
|
|
135
|
+
mode: s.mode || 'stopwatch',
|
|
136
|
+
id: s._id,
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
output(rows, {
|
|
140
|
+
headers: ['Date', 'Duration', 'Mode', 'ID'],
|
|
141
|
+
format: options.format,
|
|
142
|
+
});
|
|
143
|
+
} catch (err) {
|
|
144
|
+
error(`Failed to list sessions: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
timer
|
|
149
|
+
.command('status')
|
|
150
|
+
.description('Show overall timer status')
|
|
151
|
+
.action(async () => {
|
|
152
|
+
try {
|
|
153
|
+
const data = await withSpinner('Fetching status...', () =>
|
|
154
|
+
client.get('/timers/status')
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const status = data.status || data.data || data;
|
|
158
|
+
console.log();
|
|
159
|
+
if (status.activeCard) {
|
|
160
|
+
console.log(chalk.bold(' Active Card: '), status.activeCard.title || status.activeCard);
|
|
161
|
+
console.log(chalk.bold(' Elapsed: '), status.elapsed ? formatDuration(status.elapsed) : '0s');
|
|
162
|
+
console.log(chalk.bold(' Mode: '), status.mode || 'stopwatch');
|
|
163
|
+
} else {
|
|
164
|
+
console.log(' No active timer');
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
error(`Failed to get status: ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
timer
|
|
173
|
+
.command('running')
|
|
174
|
+
.description('Show all running timers')
|
|
175
|
+
.option('-f, --format <format>', 'Output format')
|
|
176
|
+
.action(async (options) => {
|
|
177
|
+
try {
|
|
178
|
+
const data = await withSpinner('Fetching running timers...', () =>
|
|
179
|
+
client.get('/timers/running')
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const timers = data.timers || data.data || data;
|
|
183
|
+
const rows = (Array.isArray(timers) ? timers : []).map(t => ({
|
|
184
|
+
card: t.card?.title || t.cardId || 'N/A',
|
|
185
|
+
elapsed: t.elapsed ? formatDuration(t.elapsed) : '-',
|
|
186
|
+
mode: t.mode || 'stopwatch',
|
|
187
|
+
started: t.startedAt ? formatRelativeTime(t.startedAt) : '-',
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
output(rows, {
|
|
191
|
+
headers: ['Card', 'Elapsed', 'Mode', 'Started'],
|
|
192
|
+
format: options.format,
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
error(`Failed to list running timers: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
timer
|
|
200
|
+
.command('stats')
|
|
201
|
+
.description('Show timer statistics')
|
|
202
|
+
.action(async () => {
|
|
203
|
+
try {
|
|
204
|
+
const data = await withSpinner('Fetching stats...', () =>
|
|
205
|
+
client.get('/timers/sessions/stats')
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const stats = data.stats || data.data || data;
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(chalk.bold(' Total Sessions: '), stats.totalSessions || 0);
|
|
211
|
+
console.log(chalk.bold(' Total Time: '), stats.totalTime ? formatDuration(stats.totalTime) : '0s');
|
|
212
|
+
console.log(chalk.bold(' Avg Session: '), stats.avgDuration ? formatDuration(stats.avgDuration) : '0s');
|
|
213
|
+
console.log(chalk.bold(' This Week: '), stats.thisWeek ? formatDuration(stats.thisWeek) : '0s');
|
|
214
|
+
console.log();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
error(`Failed to get stats: ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
timer
|
|
221
|
+
.command('delete <cardId>')
|
|
222
|
+
.description('Delete all timer data for a card')
|
|
223
|
+
.action(async (cardId) => {
|
|
224
|
+
try {
|
|
225
|
+
await withSpinner('Deleting timer...', () =>
|
|
226
|
+
client.delete(`/timers/card/${cardId}`)
|
|
227
|
+
);
|
|
228
|
+
success('Timer data deleted');
|
|
229
|
+
} catch (err) {
|
|
230
|
+
error(`Failed to delete timer: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
module.exports = timer;
|
|
@@ -0,0 +1,141 @@
|
|
|
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 webhook = new Command('webhook')
|
|
10
|
+
.alias('wh')
|
|
11
|
+
.description('Webhook management commands');
|
|
12
|
+
|
|
13
|
+
webhook
|
|
14
|
+
.command('list')
|
|
15
|
+
.alias('ls')
|
|
16
|
+
.description('List webhooks')
|
|
17
|
+
.option('-f, --format <format>', 'Output format')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const data = await withSpinner('Fetching webhooks...', () =>
|
|
21
|
+
client.get('/webhooks')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const webhooks = data.webhooks || data.data || data;
|
|
25
|
+
const rows = (Array.isArray(webhooks) ? webhooks : []).map(w => ({
|
|
26
|
+
name: w.name || w.description || 'Unnamed',
|
|
27
|
+
url: (w.url || '').substring(0, 50),
|
|
28
|
+
events: (w.events || []).join(', ') || 'all',
|
|
29
|
+
status: w.active !== false ? chalk.green('active') : chalk.yellow('inactive'),
|
|
30
|
+
id: w._id,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
output(rows, {
|
|
34
|
+
headers: ['Name', 'URL', 'Events', 'Status', 'ID'],
|
|
35
|
+
format: options.format,
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
error(`Failed to list webhooks: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
webhook
|
|
43
|
+
.command('create')
|
|
44
|
+
.description('Create a webhook')
|
|
45
|
+
.option('--url <url>', 'Webhook URL (required)')
|
|
46
|
+
.option('--events <events>', 'Comma-separated event types')
|
|
47
|
+
.option('--name <name>', 'Webhook name')
|
|
48
|
+
.action(async (options) => {
|
|
49
|
+
try {
|
|
50
|
+
if (!options.url) return error('URL is required. Use --url <url>.');
|
|
51
|
+
|
|
52
|
+
const body = { url: options.url };
|
|
53
|
+
if (options.name) body.name = options.name;
|
|
54
|
+
if (options.events) body.events = options.events.split(',').map(e => e.trim());
|
|
55
|
+
|
|
56
|
+
const data = await withSpinner('Creating webhook...', () =>
|
|
57
|
+
client.post('/webhooks', body)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const w = data.webhook || data.data || data;
|
|
61
|
+
success(`Created webhook: ${chalk.bold(options.name || options.url)} (${w._id})`);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
error(`Failed to create webhook: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
webhook
|
|
68
|
+
.command('update <webhookId>')
|
|
69
|
+
.description('Update a webhook')
|
|
70
|
+
.option('--url <url>', 'New URL')
|
|
71
|
+
.option('--events <events>', 'Comma-separated event types')
|
|
72
|
+
.option('--name <name>', 'New name')
|
|
73
|
+
.action(async (webhookId, options) => {
|
|
74
|
+
try {
|
|
75
|
+
const body = {};
|
|
76
|
+
if (options.url) body.url = options.url;
|
|
77
|
+
if (options.name) body.name = options.name;
|
|
78
|
+
if (options.events) body.events = options.events.split(',').map(e => e.trim());
|
|
79
|
+
if (Object.keys(body).length === 0) return error('Provide --url, --events, or --name.');
|
|
80
|
+
|
|
81
|
+
await withSpinner('Updating webhook...', () =>
|
|
82
|
+
client.put(`/webhooks/${webhookId}`, body)
|
|
83
|
+
);
|
|
84
|
+
success(`Updated webhook ${webhookId}`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
error(`Failed to update webhook: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
webhook
|
|
91
|
+
.command('delete <webhookId>')
|
|
92
|
+
.description('Delete a webhook')
|
|
93
|
+
.option('--force', 'Skip confirmation')
|
|
94
|
+
.action(async (webhookId, options) => {
|
|
95
|
+
try {
|
|
96
|
+
if (!options.force) {
|
|
97
|
+
const confirmed = await confirmAction('Delete this webhook?');
|
|
98
|
+
if (!confirmed) return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await withSpinner('Deleting webhook...', () =>
|
|
102
|
+
client.delete(`/webhooks/${webhookId}`)
|
|
103
|
+
);
|
|
104
|
+
success('Deleted webhook');
|
|
105
|
+
} catch (err) {
|
|
106
|
+
error(`Failed to delete webhook: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
webhook
|
|
111
|
+
.command('logs <webhookId>')
|
|
112
|
+
.description('Show webhook delivery logs')
|
|
113
|
+
.option('--limit <n>', 'Number of logs', '20')
|
|
114
|
+
.option('-f, --format <format>', 'Output format')
|
|
115
|
+
.action(async (webhookId, options) => {
|
|
116
|
+
try {
|
|
117
|
+
const data = await withSpinner('Fetching delivery logs...', () =>
|
|
118
|
+
client.get(`/webhooks/${webhookId}/deliveries`)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const deliveries = data.deliveries || data.data || data;
|
|
122
|
+
const delArr = Array.isArray(deliveries) ? deliveries : [];
|
|
123
|
+
const limited = delArr.slice(0, parseInt(options.limit));
|
|
124
|
+
|
|
125
|
+
const rows = limited.map(d => ({
|
|
126
|
+
event: d.event || d.type || 'N/A',
|
|
127
|
+
status: d.statusCode ? (d.statusCode < 400 ? chalk.green(d.statusCode) : chalk.red(d.statusCode)) : '-',
|
|
128
|
+
response: (d.response || d.body || '').substring(0, 40) || '-',
|
|
129
|
+
time: formatRelativeTime(d.createdAt || d.deliveredAt),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
output(rows, {
|
|
133
|
+
headers: ['Event', 'Status', 'Response', 'Time'],
|
|
134
|
+
format: options.format,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
error(`Failed to fetch delivery logs: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
module.exports = webhook;
|