@zoobbe/cli 1.1.1 → 1.2.1

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,213 @@
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 { confirmAction } = require('../utils/prompts');
8
+
9
+ const list = new Command('list')
10
+ .alias('l')
11
+ .description('Action list (column) management commands');
12
+
13
+ list
14
+ .command('ls')
15
+ .description('List all lists on a board')
16
+ .option('-b, --board <boardId>', 'Board ID (required)')
17
+ .option('-a, --all', 'Include archived lists')
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 lists...', () =>
24
+ client.get(`/${options.board}/actionLists/`)
25
+ );
26
+
27
+ let lists = data.lists || data.data || data;
28
+ lists = Array.isArray(lists) ? lists : Object.values(lists);
29
+ if (!options.all) {
30
+ lists = lists.filter(l => !l.archived);
31
+ }
32
+
33
+ const rows = lists.map(l => ({
34
+ title: l.title || 'Untitled',
35
+ id: l._id,
36
+ cards: String(l.cards?.length || 0),
37
+ color: l.color || '-',
38
+ status: l.archived ? chalk.yellow('archived') : chalk.green('active'),
39
+ }));
40
+
41
+ output(rows, {
42
+ headers: ['Title', 'ID', 'Cards', 'Color', 'Status'],
43
+ format: options.format,
44
+ });
45
+ } catch (err) {
46
+ error(`Failed to list lists: ${err.message}`);
47
+ }
48
+ });
49
+
50
+ list
51
+ .command('create <title>')
52
+ .description('Create a new list on a board')
53
+ .option('-b, --board <boardId>', 'Board ID (required)')
54
+ .action(async (title, options) => {
55
+ try {
56
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
57
+
58
+ const data = await withSpinner('Creating list...', () =>
59
+ client.post('/actionLists', { title, boardId: options.board })
60
+ );
61
+
62
+ const l = data.actionList || data.data || data;
63
+ success(`Created list: ${chalk.bold(title)} (${l._id})`);
64
+ } catch (err) {
65
+ error(`Failed to create list: ${err.message}`);
66
+ }
67
+ });
68
+
69
+ list
70
+ .command('update <listId>')
71
+ .description('Update a list')
72
+ .option('-t, --title <title>', 'New title')
73
+ .option('--color <color>', 'List color (hex)')
74
+ .option('--text-color <color>', 'Text color (hex)')
75
+ .action(async (listId, options) => {
76
+ try {
77
+ const body = {};
78
+ if (options.title) body.title = options.title;
79
+ if (options.color) body.color = options.color;
80
+ if (options.textColor) body.textColor = options.textColor;
81
+
82
+ if (Object.keys(body).length === 0) {
83
+ return error('Provide at least one option: -t, --color, or --text-color.');
84
+ }
85
+
86
+ await withSpinner('Updating list...', () =>
87
+ client.put(`/actionLists/${listId}`, body)
88
+ );
89
+ success(`Updated list ${listId}`);
90
+ } catch (err) {
91
+ error(`Failed to update list: ${err.message}`);
92
+ }
93
+ });
94
+
95
+ list
96
+ .command('delete <listId>')
97
+ .description('Delete a list and all its cards')
98
+ .option('--force', 'Skip confirmation')
99
+ .action(async (listId, options) => {
100
+ try {
101
+ if (!options.force) {
102
+ const confirmed = await confirmAction('This will delete the list and all its cards. Continue?');
103
+ if (!confirmed) return;
104
+ }
105
+
106
+ await withSpinner('Deleting list...', () =>
107
+ client.delete(`/actionLists/${listId}`)
108
+ );
109
+ success(`Deleted list ${listId}`);
110
+ } catch (err) {
111
+ error(`Failed to delete list: ${err.message}`);
112
+ }
113
+ });
114
+
115
+ list
116
+ .command('archive <listId>')
117
+ .description('Archive or unarchive a list')
118
+ .option('--unarchive', 'Unarchive the list')
119
+ .action(async (listId, options) => {
120
+ try {
121
+ const archived = !options.unarchive;
122
+ await withSpinner(archived ? 'Archiving list...' : 'Unarchiving list...', () =>
123
+ client.put(`/actionLists/archive/${listId}`, { archivedStatus: archived })
124
+ );
125
+ success(archived ? `Archived list ${listId}` : `Unarchived list ${listId}`);
126
+ } catch (err) {
127
+ error(`Failed to archive list: ${err.message}`);
128
+ }
129
+ });
130
+
131
+ list
132
+ .command('archive-cards <listId>')
133
+ .description('Archive all cards in a list')
134
+ .option('--force', 'Skip confirmation')
135
+ .action(async (listId, options) => {
136
+ try {
137
+ if (!options.force) {
138
+ const confirmed = await confirmAction('Archive all cards in this list?');
139
+ if (!confirmed) return;
140
+ }
141
+
142
+ await withSpinner('Archiving cards...', () =>
143
+ client.post(`/actionLists/${listId}/cards/archived`)
144
+ );
145
+ success(`Archived all cards in list ${listId}`);
146
+ } catch (err) {
147
+ error(`Failed to archive cards: ${err.message}`);
148
+ }
149
+ });
150
+
151
+ list
152
+ .command('copy <listId>')
153
+ .description('Copy a list with all its cards')
154
+ .option('-t, --title <title>', 'Title for the copy')
155
+ .action(async (listId, options) => {
156
+ try {
157
+ const body = {};
158
+ if (options.title) body.newTitle = options.title;
159
+
160
+ const data = await withSpinner('Copying list...', () =>
161
+ client.post(`/actionLists/${listId}/copy`, body)
162
+ );
163
+ const copied = data.copiedList || data.data || data;
164
+ success(`Copied list to: ${chalk.bold(copied.title || 'Copy')}`);
165
+ } catch (err) {
166
+ error(`Failed to copy list: ${err.message}`);
167
+ }
168
+ });
169
+
170
+ list
171
+ .command('watch <listId>')
172
+ .description('Toggle watch on a list')
173
+ .action(async (listId) => {
174
+ try {
175
+ const data = await withSpinner('Toggling watch...', () =>
176
+ client.post(`/actionLists/${listId}/watch`)
177
+ );
178
+ success(data.message || 'Watch toggled');
179
+ } catch (err) {
180
+ error(`Failed to toggle watch: ${err.message}`);
181
+ }
182
+ });
183
+
184
+ list
185
+ .command('archived')
186
+ .description('Show archived lists for a board')
187
+ .option('-b, --board <boardId>', 'Board ID (required)')
188
+ .option('-f, --format <format>', 'Output format')
189
+ .action(async (options) => {
190
+ try {
191
+ if (!options.board) return error('Board ID is required. Use -b <boardId>.');
192
+
193
+ const data = await withSpinner('Fetching archived lists...', () =>
194
+ client.get(`/${options.board}/lists/archived/`)
195
+ );
196
+
197
+ const lists = data.lists || data.data || data;
198
+ const rows = (Array.isArray(lists) ? lists : []).map(l => ({
199
+ title: l.title || 'Untitled',
200
+ id: l._id,
201
+ cards: String(l.cards?.length || 0),
202
+ }));
203
+
204
+ output(rows, {
205
+ headers: ['Title', 'ID', 'Cards'],
206
+ format: options.format,
207
+ });
208
+ } catch (err) {
209
+ error(`Failed to fetch archived lists: ${err.message}`);
210
+ }
211
+ });
212
+
213
+ module.exports = list;
@@ -0,0 +1,92 @@
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 { formatRelativeTime } = require('../utils/format');
7
+
8
+ const notification = new Command('notification')
9
+ .alias('n')
10
+ .description('Notification management commands');
11
+
12
+ notification
13
+ .command('list')
14
+ .alias('ls')
15
+ .description('List notifications')
16
+ .option('--limit <n>', 'Number of notifications', '20')
17
+ .option('--unread', 'Show only unread notifications')
18
+ .option('-f, --format <format>', 'Output format')
19
+ .action(async (options) => {
20
+ try {
21
+ let url = `/notifications?limit=${options.limit}`;
22
+ if (options.unread) url += '&unread=true';
23
+
24
+ const data = await withSpinner('Fetching notifications...', () =>
25
+ client.get(url)
26
+ );
27
+
28
+ const notifications = data.notifications || data.data || data;
29
+ const rows = (Array.isArray(notifications) ? notifications : []).map(n => ({
30
+ type: n.type || n.action || 'notification',
31
+ message: (n.message || n.text || '').substring(0, 60),
32
+ read: n.read ? chalk.gray('read') : chalk.blue('unread'),
33
+ time: formatRelativeTime(n.createdAt),
34
+ id: n._id,
35
+ }));
36
+
37
+ output(rows, {
38
+ headers: ['Type', 'Message', 'Status', 'Time', 'ID'],
39
+ format: options.format,
40
+ });
41
+ } catch (err) {
42
+ error(`Failed to list notifications: ${err.message}`);
43
+ }
44
+ });
45
+
46
+ notification
47
+ .command('count')
48
+ .description('Show unread notification count')
49
+ .action(async () => {
50
+ try {
51
+ const data = await withSpinner('Fetching count...', () =>
52
+ client.get('/notifications/count')
53
+ );
54
+
55
+ const count = data.count ?? data.unread ?? data.data ?? 0;
56
+ console.log();
57
+ console.log(` ${chalk.bold('Unread notifications:')} ${count}`);
58
+ console.log();
59
+ } catch (err) {
60
+ error(`Failed to get notification count: ${err.message}`);
61
+ }
62
+ });
63
+
64
+ notification
65
+ .command('read <notificationId>')
66
+ .description('Mark a notification as read')
67
+ .action(async (notificationId) => {
68
+ try {
69
+ await withSpinner('Marking as read...', () =>
70
+ client.put(`/notifications/${notificationId}`)
71
+ );
72
+ success('Notification marked as read');
73
+ } catch (err) {
74
+ error(`Failed to mark notification: ${err.message}`);
75
+ }
76
+ });
77
+
78
+ notification
79
+ .command('read-all')
80
+ .description('Mark all notifications as read')
81
+ .action(async () => {
82
+ try {
83
+ await withSpinner('Marking all as read...', () =>
84
+ client.put('/notifications/mark-all-read')
85
+ );
86
+ success('All notifications marked as read');
87
+ } catch (err) {
88
+ error(`Failed to mark all: ${err.message}`);
89
+ }
90
+ });
91
+
92
+ module.exports = notification;
@@ -67,8 +67,7 @@ page
67
67
  .action(async (pageId) => {
68
68
  try {
69
69
  const open = require('open');
70
- const apiUrl = config.get('apiUrl');
71
- const baseUrl = apiUrl.replace('api.', '');
70
+ const baseUrl = config.getWebUrl();
72
71
  await open(`${baseUrl}/page/${pageId}`);
73
72
  success('Opened page in browser');
74
73
  } catch (err) {
@@ -99,4 +98,257 @@ page
99
98
  }
100
99
  });
101
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
+
102
354
  module.exports = page;