@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.
@@ -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;