@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/board.js
CHANGED
|
@@ -160,4 +160,299 @@ board
|
|
|
160
160
|
}
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
// ── Extended commands ──
|
|
164
|
+
|
|
165
|
+
const { confirmAction } = require('../utils/prompts');
|
|
166
|
+
const { resolveBoard } = require('../utils/resolve');
|
|
167
|
+
const { formatRelativeTime } = require('../utils/format');
|
|
168
|
+
|
|
169
|
+
board
|
|
170
|
+
.command('delete <nameOrId>')
|
|
171
|
+
.description('Permanently delete a board')
|
|
172
|
+
.option('--force', 'Skip confirmation')
|
|
173
|
+
.action(async (nameOrId, options) => {
|
|
174
|
+
try {
|
|
175
|
+
const workspaceId = config.get('activeWorkspace');
|
|
176
|
+
if (!workspaceId) return error('No active workspace.');
|
|
177
|
+
|
|
178
|
+
const match = await resolveBoard(nameOrId, workspaceId);
|
|
179
|
+
if (!match) return error(`Board "${nameOrId}" not found.`);
|
|
180
|
+
|
|
181
|
+
if (!options.force) {
|
|
182
|
+
const confirmed = await confirmAction(`Permanently delete "${match.title || match.name}" and all its data?`);
|
|
183
|
+
if (!confirmed) return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await withSpinner('Deleting board...', () =>
|
|
187
|
+
client.post(`/boards/delete/${match.shortId}`)
|
|
188
|
+
);
|
|
189
|
+
success(`Deleted board: ${chalk.bold(match.title || match.name)}`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
error(`Failed to delete board: ${err.message}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
board
|
|
196
|
+
.command('restore <nameOrId>')
|
|
197
|
+
.description('Restore an archived board')
|
|
198
|
+
.action(async (nameOrId) => {
|
|
199
|
+
try {
|
|
200
|
+
const workspaceId = config.get('activeWorkspace');
|
|
201
|
+
if (!workspaceId) return error('No active workspace.');
|
|
202
|
+
|
|
203
|
+
const match = await resolveBoard(nameOrId, workspaceId);
|
|
204
|
+
if (!match) return error(`Board "${nameOrId}" not found.`);
|
|
205
|
+
|
|
206
|
+
await withSpinner('Restoring board...', () =>
|
|
207
|
+
client.post(`/boards/archive/${match.shortId}`)
|
|
208
|
+
);
|
|
209
|
+
success(`Restored board: ${chalk.bold(match.title || match.name)}`);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`Failed to restore board: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
board
|
|
216
|
+
.command('update <nameOrId>')
|
|
217
|
+
.description('Update board title or description')
|
|
218
|
+
.option('-t, --title <title>', 'New title')
|
|
219
|
+
.option('-d, --description <desc>', 'New description')
|
|
220
|
+
.action(async (nameOrId, options) => {
|
|
221
|
+
try {
|
|
222
|
+
const body = {};
|
|
223
|
+
if (options.title) body.title = options.title;
|
|
224
|
+
if (options.description) body.description = options.description;
|
|
225
|
+
if (Object.keys(body).length === 0) return error('Provide -t or -d.');
|
|
226
|
+
|
|
227
|
+
await withSpinner('Updating board...', () =>
|
|
228
|
+
client.put(`/boards/${nameOrId}`, body)
|
|
229
|
+
);
|
|
230
|
+
success(`Updated board ${nameOrId}`);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
error(`Failed to update board: ${err.message}`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
board
|
|
237
|
+
.command('members <nameOrId>')
|
|
238
|
+
.description('List board members')
|
|
239
|
+
.option('-f, --format <format>', 'Output format')
|
|
240
|
+
.action(async (nameOrId, options) => {
|
|
241
|
+
try {
|
|
242
|
+
const data = await withSpinner('Fetching members...', () =>
|
|
243
|
+
client.get(`/boards/${nameOrId}/members`)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const members = data.members || data.data || data;
|
|
247
|
+
const rows = (Array.isArray(members) ? members : []).map(m => {
|
|
248
|
+
const user = m.user || m;
|
|
249
|
+
return {
|
|
250
|
+
name: user.userName || user.username || user.name || 'N/A',
|
|
251
|
+
email: user.email || 'N/A',
|
|
252
|
+
role: m.role || 'member',
|
|
253
|
+
id: user._id || user.id || '',
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
output(rows, {
|
|
258
|
+
headers: ['Name', 'Email', 'Role', 'ID'],
|
|
259
|
+
format: options.format,
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
error(`Failed to list members: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
board
|
|
267
|
+
.command('add-member <nameOrId>')
|
|
268
|
+
.description('Add a member to a board')
|
|
269
|
+
.option('-u, --user <userId>', 'User ID (required)')
|
|
270
|
+
.option('--role <role>', 'Role (admin|member|viewer)', 'member')
|
|
271
|
+
.action(async (nameOrId, options) => {
|
|
272
|
+
try {
|
|
273
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
274
|
+
|
|
275
|
+
await withSpinner('Adding member...', () =>
|
|
276
|
+
client.post(`/boards/${nameOrId}/member`, {
|
|
277
|
+
userId: options.user,
|
|
278
|
+
role: options.role,
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
success(`Added member to board ${nameOrId}`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
error(`Failed to add member: ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
board
|
|
288
|
+
.command('remove-member <nameOrId>')
|
|
289
|
+
.description('Remove a member from a board')
|
|
290
|
+
.option('-u, --user <userId>', 'User ID (required)')
|
|
291
|
+
.action(async (nameOrId, options) => {
|
|
292
|
+
try {
|
|
293
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
294
|
+
|
|
295
|
+
await withSpinner('Removing member...', () =>
|
|
296
|
+
client.delete(`/boards/${nameOrId}/member`, { userId: options.user })
|
|
297
|
+
);
|
|
298
|
+
success(`Removed member from board ${nameOrId}`);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
error(`Failed to remove member: ${err.message}`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
board
|
|
305
|
+
.command('invite <nameOrId>')
|
|
306
|
+
.description('Invite a user to a board by email')
|
|
307
|
+
.option('-e, --email <email>', 'Email address (required)')
|
|
308
|
+
.option('--role <role>', 'Role (admin|member|viewer)', 'member')
|
|
309
|
+
.action(async (nameOrId, options) => {
|
|
310
|
+
try {
|
|
311
|
+
if (!options.email) return error('Email is required. Use -e <email>.');
|
|
312
|
+
|
|
313
|
+
await withSpinner('Sending invitation...', () =>
|
|
314
|
+
client.post(`/boards/${nameOrId}/invite`, {
|
|
315
|
+
email: options.email,
|
|
316
|
+
role: options.role,
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
success(`Invitation sent to ${chalk.bold(options.email)}`);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
error(`Failed to invite: ${err.message}`);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
board
|
|
326
|
+
.command('join-link <nameOrId>')
|
|
327
|
+
.description('Generate or manage a board join link')
|
|
328
|
+
.option('--delete', 'Delete the existing join link')
|
|
329
|
+
.action(async (nameOrId, options) => {
|
|
330
|
+
try {
|
|
331
|
+
if (options.delete) {
|
|
332
|
+
await withSpinner('Deleting join link...', () =>
|
|
333
|
+
client.delete(`/boards/${nameOrId}/delete-join-link`)
|
|
334
|
+
);
|
|
335
|
+
success('Join link deleted');
|
|
336
|
+
} else {
|
|
337
|
+
const data = await withSpinner('Generating join link...', () =>
|
|
338
|
+
client.post(`/boards/${nameOrId}/generate-join-link`)
|
|
339
|
+
);
|
|
340
|
+
const link = data.joinLink || data.link || data.data;
|
|
341
|
+
success(`Join link: ${chalk.bold(link)}`);
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
error(`Failed to manage join link: ${err.message}`);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
board
|
|
349
|
+
.command('labels <nameOrId>')
|
|
350
|
+
.description('List labels on a board')
|
|
351
|
+
.option('-f, --format <format>', 'Output format')
|
|
352
|
+
.action(async (nameOrId, options) => {
|
|
353
|
+
try {
|
|
354
|
+
const data = await withSpinner('Fetching labels...', () =>
|
|
355
|
+
client.get(`/boards/${nameOrId}/labels`)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const labels = data.labels || data.data || data;
|
|
359
|
+
const rows = (Array.isArray(labels) ? labels : []).map(l => ({
|
|
360
|
+
text: l.text || '-',
|
|
361
|
+
color: l.color || '-',
|
|
362
|
+
id: l._id,
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
output(rows, {
|
|
366
|
+
headers: ['Text', 'Color', 'ID'],
|
|
367
|
+
format: options.format,
|
|
368
|
+
});
|
|
369
|
+
} catch (err) {
|
|
370
|
+
error(`Failed to list labels: ${err.message}`);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
board
|
|
375
|
+
.command('label-create <nameOrId>')
|
|
376
|
+
.description('Create a label on a board')
|
|
377
|
+
.option('--text <text>', 'Label text')
|
|
378
|
+
.option('--color <color>', 'Label color')
|
|
379
|
+
.action(async (nameOrId, options) => {
|
|
380
|
+
try {
|
|
381
|
+
if (!options.text && !options.color) return error('Provide --text or --color.');
|
|
382
|
+
|
|
383
|
+
const data = await withSpinner('Creating label...', () =>
|
|
384
|
+
client.post(`/boards/${nameOrId}/labels`, {
|
|
385
|
+
text: options.text || '',
|
|
386
|
+
color: options.color || '',
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
const l = data.label || data.data || data;
|
|
390
|
+
success(`Created label: ${chalk.bold(options.text || options.color)} (${l._id})`);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
error(`Failed to create label: ${err.message}`);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
board
|
|
397
|
+
.command('label-delete <nameOrId>')
|
|
398
|
+
.description('Delete a label from a board')
|
|
399
|
+
.option('--label <labelId>', 'Label ID (required)')
|
|
400
|
+
.action(async (nameOrId, options) => {
|
|
401
|
+
try {
|
|
402
|
+
if (!options.label) return error('Label ID is required. Use --label <labelId>.');
|
|
403
|
+
|
|
404
|
+
await withSpinner('Deleting label...', () =>
|
|
405
|
+
client.delete(`/boards/${nameOrId}/labels/${options.label}`)
|
|
406
|
+
);
|
|
407
|
+
success('Deleted label');
|
|
408
|
+
} catch (err) {
|
|
409
|
+
error(`Failed to delete label: ${err.message}`);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
board
|
|
414
|
+
.command('visibility <nameOrId> <level>')
|
|
415
|
+
.description('Change board visibility (Private|Public|Workspace)')
|
|
416
|
+
.action(async (nameOrId, level) => {
|
|
417
|
+
try {
|
|
418
|
+
await withSpinner('Updating visibility...', () =>
|
|
419
|
+
client.put(`/boards/${nameOrId}/visibility`, { visibility: level })
|
|
420
|
+
);
|
|
421
|
+
success(`Board visibility set to ${chalk.bold(level)}`);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
error(`Failed to update visibility: ${err.message}`);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
board
|
|
428
|
+
.command('activities <nameOrId>')
|
|
429
|
+
.description('Show board activity log')
|
|
430
|
+
.option('--limit <n>', 'Number of activities', '20')
|
|
431
|
+
.option('-f, --format <format>', 'Output format')
|
|
432
|
+
.action(async (nameOrId, options) => {
|
|
433
|
+
try {
|
|
434
|
+
const data = await withSpinner('Fetching activities...', () =>
|
|
435
|
+
client.get(`/boards/${nameOrId}/activities`)
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const activities = data.activities || data.data || data;
|
|
439
|
+
const actArr = Array.isArray(activities) ? activities : [];
|
|
440
|
+
const limited = actArr.slice(0, parseInt(options.limit));
|
|
441
|
+
|
|
442
|
+
const rows = limited.map(a => ({
|
|
443
|
+
action: a.action || a.type || 'unknown',
|
|
444
|
+
user: a.user?.userName || a.user?.name || 'N/A',
|
|
445
|
+
target: a.target || a.card?.title || '',
|
|
446
|
+
time: formatRelativeTime(a.createdAt),
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
output(rows, {
|
|
450
|
+
headers: ['Action', 'User', 'Target', 'Time'],
|
|
451
|
+
format: options.format,
|
|
452
|
+
});
|
|
453
|
+
} catch (err) {
|
|
454
|
+
error(`Failed to fetch activities: ${err.message}`);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
163
458
|
module.exports = board;
|
package/src/commands/card.js
CHANGED
|
@@ -255,6 +255,78 @@ card
|
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
card
|
|
259
|
+
.command('label <cardId>')
|
|
260
|
+
.description('Add or remove a label on a card')
|
|
261
|
+
.option('-b, --board <boardId>', 'Board ID (auto-detected from card if not specified)')
|
|
262
|
+
.option('-r, --remove', 'Remove the label instead of adding it')
|
|
263
|
+
.action(async (cardId, options) => {
|
|
264
|
+
try {
|
|
265
|
+
// Fetch card to get board info
|
|
266
|
+
const cardData = await withSpinner('Fetching card...', () =>
|
|
267
|
+
client.get(`/cards/${cardId}`)
|
|
268
|
+
);
|
|
269
|
+
const c = cardData.card || cardData.data || cardData;
|
|
270
|
+
const boardId = options.board || c.boardShortId || c.board;
|
|
271
|
+
|
|
272
|
+
if (!boardId) {
|
|
273
|
+
return error('Could not determine board. Use -b <boardId>.');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Get available labels from the board
|
|
277
|
+
const labelsData = await client.get(`/boards/${boardId}/labels`);
|
|
278
|
+
const labels = labelsData.labels || labelsData.data || labelsData;
|
|
279
|
+
const labelArr = Array.isArray(labels) ? labels : [];
|
|
280
|
+
|
|
281
|
+
if (labelArr.length === 0) {
|
|
282
|
+
return error('No labels found on this board.');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get current card labels
|
|
286
|
+
const cardLabelsData = await client.get(`/cards/${cardId}/labels`);
|
|
287
|
+
const cardLabels = cardLabelsData.labels || cardLabelsData.data || [];
|
|
288
|
+
const cardLabelIds = new Set(cardLabels.map(l => l._id));
|
|
289
|
+
|
|
290
|
+
// Show available labels with status
|
|
291
|
+
console.log();
|
|
292
|
+
console.log(chalk.bold(' Available labels:'));
|
|
293
|
+
labelArr.forEach((l, i) => {
|
|
294
|
+
const active = cardLabelIds.has(l._id) ? chalk.green(' [active]') : '';
|
|
295
|
+
const name = l.text || l.color || 'Unnamed';
|
|
296
|
+
console.log(` ${i + 1}. ${name}${active}`);
|
|
297
|
+
});
|
|
298
|
+
console.log();
|
|
299
|
+
|
|
300
|
+
// Prompt user to pick a label
|
|
301
|
+
const inquirer = require('inquirer');
|
|
302
|
+
const { labelIndex } = await inquirer.prompt([{
|
|
303
|
+
type: 'input',
|
|
304
|
+
name: 'labelIndex',
|
|
305
|
+
message: options.remove ? 'Label # to remove:' : 'Label # to add:',
|
|
306
|
+
validate: v => {
|
|
307
|
+
const n = parseInt(v);
|
|
308
|
+
return (n >= 1 && n <= labelArr.length) ? true : `Enter 1-${labelArr.length}`;
|
|
309
|
+
},
|
|
310
|
+
}]);
|
|
311
|
+
|
|
312
|
+
const label = labelArr[parseInt(labelIndex) - 1];
|
|
313
|
+
|
|
314
|
+
if (options.remove) {
|
|
315
|
+
await withSpinner('Removing label...', () =>
|
|
316
|
+
client.delete(`/cards/${cardId}/labels/${label._id}`)
|
|
317
|
+
);
|
|
318
|
+
success(`Removed label "${label.text || label.color}" from card ${cardId}`);
|
|
319
|
+
} else {
|
|
320
|
+
await withSpinner('Adding label...', () =>
|
|
321
|
+
client.put(`/cards/${cardId}/labels/${label._id}`, { boardId })
|
|
322
|
+
);
|
|
323
|
+
success(`Added label "${label.text || label.color}" to card ${cardId}`);
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
error(`Failed to update label: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
258
330
|
card
|
|
259
331
|
.command('done <cardId>')
|
|
260
332
|
.description('Mark a card as complete')
|
|
@@ -299,4 +371,299 @@ card
|
|
|
299
371
|
}
|
|
300
372
|
});
|
|
301
373
|
|
|
374
|
+
// ── Extended commands ──
|
|
375
|
+
|
|
376
|
+
const { confirmAction } = require('../utils/prompts');
|
|
377
|
+
const { formatRelativeTime } = require('../utils/format');
|
|
378
|
+
|
|
379
|
+
card
|
|
380
|
+
.command('update <cardId>')
|
|
381
|
+
.description('Update card title or description')
|
|
382
|
+
.option('-t, --title <title>', 'New title')
|
|
383
|
+
.option('-d, --description <desc>', 'New description')
|
|
384
|
+
.action(async (cardId, options) => {
|
|
385
|
+
try {
|
|
386
|
+
const body = {};
|
|
387
|
+
if (options.title) body.title = options.title;
|
|
388
|
+
if (options.description) body.description = options.description;
|
|
389
|
+
if (Object.keys(body).length === 0) return error('Provide -t or -d.');
|
|
390
|
+
|
|
391
|
+
await withSpinner('Updating card...', () =>
|
|
392
|
+
client.post(`/cards/update/${cardId}`, body)
|
|
393
|
+
);
|
|
394
|
+
success(`Updated card ${cardId}`);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
error(`Failed to update card: ${err.message}`);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
card
|
|
401
|
+
.command('archive <cardId>')
|
|
402
|
+
.description('Archive a card')
|
|
403
|
+
.action(async (cardId) => {
|
|
404
|
+
try {
|
|
405
|
+
await withSpinner('Archiving card...', () =>
|
|
406
|
+
client.post(`/cards/archive/${cardId}`)
|
|
407
|
+
);
|
|
408
|
+
success(`Archived card ${cardId}`);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
error(`Failed to archive card: ${err.message}`);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
card
|
|
415
|
+
.command('delete <cardId>')
|
|
416
|
+
.description('Permanently delete a card')
|
|
417
|
+
.option('--force', 'Skip confirmation')
|
|
418
|
+
.action(async (cardId, options) => {
|
|
419
|
+
try {
|
|
420
|
+
if (!options.force) {
|
|
421
|
+
const confirmed = await confirmAction('Permanently delete this card and all its data?');
|
|
422
|
+
if (!confirmed) return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await withSpinner('Deleting card...', () =>
|
|
426
|
+
client.post(`/cards/delete/${cardId}`)
|
|
427
|
+
);
|
|
428
|
+
success(`Deleted card ${cardId}`);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
error(`Failed to delete card: ${err.message}`);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
card
|
|
435
|
+
.command('copy <cardId>')
|
|
436
|
+
.description('Copy a card')
|
|
437
|
+
.option('-l, --list <listId>', 'Target list ID (copies to same list if omitted)')
|
|
438
|
+
.action(async (cardId, options) => {
|
|
439
|
+
try {
|
|
440
|
+
const body = {};
|
|
441
|
+
if (options.list) body.targetListId = options.list;
|
|
442
|
+
|
|
443
|
+
const data = await withSpinner('Copying card...', () =>
|
|
444
|
+
client.post(`/cards/${cardId}/copy`, body)
|
|
445
|
+
);
|
|
446
|
+
const c = data.card || data.data || data;
|
|
447
|
+
success(`Copied card to: ${chalk.bold(c.title || 'Copy')} (${c.shortId || c._id})`);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
error(`Failed to copy card: ${err.message}`);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
card
|
|
454
|
+
.command('unassign <cardId>')
|
|
455
|
+
.description('Remove a user from a card')
|
|
456
|
+
.option('-u, --user <userId>', 'User ID to remove (required)')
|
|
457
|
+
.action(async (cardId, options) => {
|
|
458
|
+
try {
|
|
459
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
460
|
+
|
|
461
|
+
await withSpinner('Removing member...', () =>
|
|
462
|
+
client.post(`/cards/${cardId}/removeMember`, { memberId: options.user })
|
|
463
|
+
);
|
|
464
|
+
success(`Removed user from card ${cardId}`);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
error(`Failed to unassign card: ${err.message}`);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
card
|
|
471
|
+
.command('members <cardId>')
|
|
472
|
+
.description('List members assigned to a card')
|
|
473
|
+
.option('-f, --format <format>', 'Output format')
|
|
474
|
+
.action(async (cardId, options) => {
|
|
475
|
+
try {
|
|
476
|
+
const data = await withSpinner('Fetching members...', () =>
|
|
477
|
+
client.get(`/cards/${cardId}/members`)
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const members = data.members || data.data || data;
|
|
481
|
+
const rows = (Array.isArray(members) ? members : []).map(m => {
|
|
482
|
+
const user = m.user || m;
|
|
483
|
+
return {
|
|
484
|
+
name: user.userName || user.username || user.name || 'N/A',
|
|
485
|
+
email: user.email || 'N/A',
|
|
486
|
+
id: user._id || user.id || '',
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
output(rows, {
|
|
491
|
+
headers: ['Name', 'Email', 'ID'],
|
|
492
|
+
format: options.format,
|
|
493
|
+
});
|
|
494
|
+
} catch (err) {
|
|
495
|
+
error(`Failed to list members: ${err.message}`);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
card
|
|
500
|
+
.command('comments <cardId>')
|
|
501
|
+
.description('List comments on a card')
|
|
502
|
+
.option('-f, --format <format>', 'Output format')
|
|
503
|
+
.action(async (cardId, options) => {
|
|
504
|
+
try {
|
|
505
|
+
const data = await withSpinner('Fetching comments...', () =>
|
|
506
|
+
client.get(`/cards/${cardId}/comments`)
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const comments = data.comments || data.data || data;
|
|
510
|
+
const rows = (Array.isArray(comments) ? comments : []).map(c => ({
|
|
511
|
+
user: c.member?.userName || c.user?.userName || 'N/A',
|
|
512
|
+
comment: (c.comment || '').substring(0, 80),
|
|
513
|
+
time: formatRelativeTime(c.createdAt),
|
|
514
|
+
id: c._id,
|
|
515
|
+
}));
|
|
516
|
+
|
|
517
|
+
output(rows, {
|
|
518
|
+
headers: ['User', 'Comment', 'Time', 'ID'],
|
|
519
|
+
format: options.format,
|
|
520
|
+
});
|
|
521
|
+
} catch (err) {
|
|
522
|
+
error(`Failed to list comments: ${err.message}`);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
card
|
|
527
|
+
.command('watch <cardId>')
|
|
528
|
+
.description('Toggle watch on a card')
|
|
529
|
+
.option('--unwatch', 'Unwatch the card')
|
|
530
|
+
.action(async (cardId, options) => {
|
|
531
|
+
try {
|
|
532
|
+
const data = await withSpinner(options.unwatch ? 'Unwatching...' : 'Watching...', () =>
|
|
533
|
+
client.post(`/cards/${cardId}/watch`)
|
|
534
|
+
);
|
|
535
|
+
success(data.message || 'Watch toggled');
|
|
536
|
+
} catch (err) {
|
|
537
|
+
error(`Failed to toggle watch: ${err.message}`);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
card
|
|
542
|
+
.command('attachments <cardId>')
|
|
543
|
+
.description('List attachments on a card')
|
|
544
|
+
.option('-f, --format <format>', 'Output format')
|
|
545
|
+
.action(async (cardId, options) => {
|
|
546
|
+
try {
|
|
547
|
+
const data = await withSpinner('Fetching attachments...', () =>
|
|
548
|
+
client.get(`/cards/attachments/${cardId}`)
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const attachments = data.attachments || data.data || data;
|
|
552
|
+
const rows = (Array.isArray(attachments) ? attachments : []).map(a => ({
|
|
553
|
+
name: a.name || a.filename || 'N/A',
|
|
554
|
+
type: a.type || a.mimetype || '-',
|
|
555
|
+
size: a.size ? `${Math.round(a.size / 1024)}KB` : '-',
|
|
556
|
+
id: a._id,
|
|
557
|
+
}));
|
|
558
|
+
|
|
559
|
+
output(rows, {
|
|
560
|
+
headers: ['Name', 'Type', 'Size', 'ID'],
|
|
561
|
+
format: options.format,
|
|
562
|
+
});
|
|
563
|
+
} catch (err) {
|
|
564
|
+
error(`Failed to list attachments: ${err.message}`);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
card
|
|
569
|
+
.command('attach <cardId> <filePath>')
|
|
570
|
+
.description('Upload an attachment to a card')
|
|
571
|
+
.action(async (cardId, filePath) => {
|
|
572
|
+
try {
|
|
573
|
+
const fs = require('fs');
|
|
574
|
+
if (!fs.existsSync(filePath)) {
|
|
575
|
+
return error(`File not found: ${filePath}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const data = await withSpinner('Uploading attachment...', () =>
|
|
579
|
+
client.upload(`/cards/attachments/${cardId}`, filePath)
|
|
580
|
+
);
|
|
581
|
+
const a = data.attachment || data.data || data;
|
|
582
|
+
success(`Uploaded: ${chalk.bold(a.name || a.filename || filePath)}`);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
error(`Failed to upload attachment: ${err.message}`);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
card
|
|
589
|
+
.command('activities <cardId>')
|
|
590
|
+
.description('Show card activity log')
|
|
591
|
+
.option('--limit <n>', 'Number of activities', '20')
|
|
592
|
+
.option('-f, --format <format>', 'Output format')
|
|
593
|
+
.action(async (cardId, options) => {
|
|
594
|
+
try {
|
|
595
|
+
const data = await withSpinner('Fetching activities...', () =>
|
|
596
|
+
client.get(`/cards/${cardId}/activities`)
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const activities = data.activities || data.data || data;
|
|
600
|
+
const actArr = Array.isArray(activities) ? activities : [];
|
|
601
|
+
const limited = actArr.slice(0, parseInt(options.limit));
|
|
602
|
+
|
|
603
|
+
const rows = limited.map(a => ({
|
|
604
|
+
action: a.action || a.type || 'unknown',
|
|
605
|
+
user: a.user?.userName || a.user?.name || 'N/A',
|
|
606
|
+
detail: (a.detail || a.description || '').substring(0, 60),
|
|
607
|
+
time: formatRelativeTime(a.createdAt),
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
output(rows, {
|
|
611
|
+
headers: ['Action', 'User', 'Detail', 'Time'],
|
|
612
|
+
format: options.format,
|
|
613
|
+
});
|
|
614
|
+
} catch (err) {
|
|
615
|
+
error(`Failed to fetch activities: ${err.message}`);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
card
|
|
620
|
+
.command('due <cardId> <date>')
|
|
621
|
+
.description('Set or remove a due date on a card')
|
|
622
|
+
.option('--remove', 'Remove the due date')
|
|
623
|
+
.action(async (cardId, date, options) => {
|
|
624
|
+
try {
|
|
625
|
+
if (options.remove) {
|
|
626
|
+
await withSpinner('Removing due date...', () =>
|
|
627
|
+
client.delete(`/cards/${cardId}/duedate`)
|
|
628
|
+
);
|
|
629
|
+
success(`Removed due date from card ${cardId}`);
|
|
630
|
+
} else {
|
|
631
|
+
await withSpinner('Setting due date...', () =>
|
|
632
|
+
client.post(`/cards/${cardId}/duedate`, { dueDate: date })
|
|
633
|
+
);
|
|
634
|
+
success(`Set due date to ${chalk.bold(date)} on card ${cardId}`);
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
error(`Failed to update due date: ${err.message}`);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
card
|
|
642
|
+
.command('priority <cardId> <level>')
|
|
643
|
+
.description('Set card priority (low|medium|high|critical)')
|
|
644
|
+
.action(async (cardId, level) => {
|
|
645
|
+
try {
|
|
646
|
+
await withSpinner('Setting priority...', () =>
|
|
647
|
+
client.post(`/cards/${cardId}/priority`, { priority: level })
|
|
648
|
+
);
|
|
649
|
+
success(`Set priority to ${chalk.bold(level)} on card ${cardId}`);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
error(`Failed to set priority: ${err.message}`);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
card
|
|
656
|
+
.command('undone <cardId>')
|
|
657
|
+
.description('Mark a card as incomplete')
|
|
658
|
+
.action(async (cardId) => {
|
|
659
|
+
try {
|
|
660
|
+
await withSpinner('Updating card...', () =>
|
|
661
|
+
client.post(`/cards/update/${cardId}`, { isComplete: false })
|
|
662
|
+
);
|
|
663
|
+
success(`Card ${cardId} marked as incomplete`);
|
|
664
|
+
} catch (err) {
|
|
665
|
+
error(`Failed to update card: ${err.message}`);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
302
669
|
module.exports = card;
|