@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.
- package/README.md +232 -34
- package/package.json +1 -1
- 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 +199 -30
- package/src/commands/automation.js +255 -0
- package/src/commands/board.js +296 -2
- 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 +254 -2
- package/src/commands/timer.js +234 -0
- package/src/commands/webhook.js +141 -0
- package/src/commands/workspace.js +177 -2
- package/src/index.js +10 -0
- package/src/lib/client.js +45 -4
- package/src/lib/config.js +21 -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/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;
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
|
|
8
|
+
const checklist = new Command('checklist')
|
|
9
|
+
.alias('cl')
|
|
10
|
+
.description('Card checklist management commands');
|
|
11
|
+
|
|
12
|
+
checklist
|
|
13
|
+
.command('list <cardId>')
|
|
14
|
+
.alias('ls')
|
|
15
|
+
.description('List checklists on a card')
|
|
16
|
+
.option('-f, --format <format>', 'Output format')
|
|
17
|
+
.action(async (cardId, options) => {
|
|
18
|
+
try {
|
|
19
|
+
const data = await withSpinner('Fetching checklists...', () =>
|
|
20
|
+
client.get(`/cards/${cardId}/checklists`)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const checklists = data.checklists || data.data || data;
|
|
24
|
+
const clArr = Array.isArray(checklists) ? checklists : [];
|
|
25
|
+
|
|
26
|
+
if (clArr.length === 0) {
|
|
27
|
+
return error('No checklists on this card.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Display checklists with items
|
|
31
|
+
for (const cl of clArr) {
|
|
32
|
+
const items = cl.items || [];
|
|
33
|
+
const done = items.filter(i => i.checked).length;
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(` ${chalk.bold(cl.title)} (${cl._id}) — ${done}/${items.length} done`);
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const check = item.checked ? chalk.green('✓') : chalk.gray('○');
|
|
38
|
+
console.log(` ${check} ${item.title} ${chalk.gray(`(${item._id})`)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
error(`Failed to list checklists: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
checklist
|
|
48
|
+
.command('add <cardId> <title>')
|
|
49
|
+
.description('Add a checklist to a card')
|
|
50
|
+
.action(async (cardId, title) => {
|
|
51
|
+
try {
|
|
52
|
+
const data = await withSpinner('Adding checklist...', () =>
|
|
53
|
+
client.post(`/cards/${cardId}/checklists`, { title })
|
|
54
|
+
);
|
|
55
|
+
const cl = data.checklist || data.data || data;
|
|
56
|
+
success(`Added checklist: ${chalk.bold(title)} (${cl._id})`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
error(`Failed to add checklist: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
checklist
|
|
63
|
+
.command('rename <cardId> <checklistId>')
|
|
64
|
+
.description('Rename a checklist')
|
|
65
|
+
.option('-t, --title <title>', 'New title (required)')
|
|
66
|
+
.action(async (cardId, checklistId, options) => {
|
|
67
|
+
try {
|
|
68
|
+
if (!options.title) return error('Title is required. Use -t <title>.');
|
|
69
|
+
|
|
70
|
+
await withSpinner('Renaming checklist...', () =>
|
|
71
|
+
client.put(`/cards/${cardId}/checklists/${checklistId}`, { title: options.title })
|
|
72
|
+
);
|
|
73
|
+
success(`Renamed checklist to: ${chalk.bold(options.title)}`);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
error(`Failed to rename checklist: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
checklist
|
|
80
|
+
.command('delete <cardId> <checklistId>')
|
|
81
|
+
.description('Delete a checklist')
|
|
82
|
+
.option('--force', 'Skip confirmation')
|
|
83
|
+
.action(async (cardId, checklistId, options) => {
|
|
84
|
+
try {
|
|
85
|
+
if (!options.force) {
|
|
86
|
+
const confirmed = await confirmAction('Delete this checklist and all its items?');
|
|
87
|
+
if (!confirmed) return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await withSpinner('Deleting checklist...', () =>
|
|
91
|
+
client.delete(`/cards/${cardId}/checklists/${checklistId}`)
|
|
92
|
+
);
|
|
93
|
+
success(`Deleted checklist ${checklistId}`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
error(`Failed to delete checklist: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
checklist
|
|
100
|
+
.command('add-item <cardId> <checklistId> <title>')
|
|
101
|
+
.description('Add an item to a checklist')
|
|
102
|
+
.action(async (cardId, checklistId, title) => {
|
|
103
|
+
try {
|
|
104
|
+
await withSpinner('Adding item...', () =>
|
|
105
|
+
client.post(`/cards/${cardId}/checklists/${checklistId}/items`, { title })
|
|
106
|
+
);
|
|
107
|
+
success(`Added item: ${chalk.bold(title)}`);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
error(`Failed to add item: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
checklist
|
|
114
|
+
.command('check <cardId> <checklistId> <itemId>')
|
|
115
|
+
.description('Toggle an item as complete/incomplete')
|
|
116
|
+
.action(async (cardId, checklistId, itemId) => {
|
|
117
|
+
try {
|
|
118
|
+
// Fetch current state to toggle
|
|
119
|
+
const data = await client.get(`/cards/${cardId}/checklists`);
|
|
120
|
+
const checklists = data.checklists || data.data || data;
|
|
121
|
+
let currentChecked = false;
|
|
122
|
+
for (const cl of (Array.isArray(checklists) ? checklists : [])) {
|
|
123
|
+
if (cl._id === checklistId) {
|
|
124
|
+
const item = (cl.items || []).find(i => i._id === itemId);
|
|
125
|
+
if (item) currentChecked = item.checked;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await withSpinner('Toggling item...', () =>
|
|
130
|
+
client.put(`/cards/${cardId}/checklists/${checklistId}/items/${itemId}`, {
|
|
131
|
+
checked: !currentChecked,
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
success(!currentChecked ? 'Item marked as complete' : 'Item marked as incomplete');
|
|
135
|
+
} catch (err) {
|
|
136
|
+
error(`Failed to toggle item: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
checklist
|
|
141
|
+
.command('update-item <cardId> <checklistId> <itemId>')
|
|
142
|
+
.description('Update a checklist item title')
|
|
143
|
+
.option('-t, --title <title>', 'New title (required)')
|
|
144
|
+
.action(async (cardId, checklistId, itemId, options) => {
|
|
145
|
+
try {
|
|
146
|
+
if (!options.title) return error('Title is required. Use -t <title>.');
|
|
147
|
+
|
|
148
|
+
await withSpinner('Updating item...', () =>
|
|
149
|
+
client.put(`/cards/${cardId}/checklists/${checklistId}/items/${itemId}`, {
|
|
150
|
+
title: options.title,
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
success(`Updated item to: ${chalk.bold(options.title)}`);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
error(`Failed to update item: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
checklist
|
|
160
|
+
.command('delete-item <cardId> <checklistId> <itemId>')
|
|
161
|
+
.description('Delete a checklist item')
|
|
162
|
+
.action(async (cardId, checklistId, itemId) => {
|
|
163
|
+
try {
|
|
164
|
+
await withSpinner('Deleting item...', () =>
|
|
165
|
+
client.delete(`/cards/${cardId}/checklists/${checklistId}/items/${itemId}`)
|
|
166
|
+
);
|
|
167
|
+
success('Deleted checklist item');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
error(`Failed to delete item: ${err.message}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
module.exports = checklist;
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { formatRelativeTime } = require('../utils/format');
|
|
8
|
+
|
|
9
|
+
const importCmd = new Command('import')
|
|
10
|
+
.description('Import data from external services');
|
|
11
|
+
|
|
12
|
+
importCmd
|
|
13
|
+
.command('trello')
|
|
14
|
+
.description('Import boards from Trello')
|
|
15
|
+
.option('--key <key>', 'Trello API key (required)')
|
|
16
|
+
.option('--token <token>', 'Trello token (required)')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
if (!options.key || !options.token) {
|
|
20
|
+
return error('Both --key and --token are required for Trello import.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const workspaceId = config.get('activeWorkspace');
|
|
24
|
+
if (!workspaceId) return error('No active workspace.');
|
|
25
|
+
|
|
26
|
+
const data = await withSpinner('Importing from Trello...', () =>
|
|
27
|
+
client.post('/trello/import', {
|
|
28
|
+
trelloKey: options.key,
|
|
29
|
+
trelloToken: options.token,
|
|
30
|
+
workspaceId,
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const job = data.job || data.data || data;
|
|
35
|
+
success(`Import started: Job ${chalk.bold(job._id || job.jobId || 'created')}`);
|
|
36
|
+
if (job._id || job.jobId) {
|
|
37
|
+
console.log(` Track progress: zoobbe import status ${job._id || job.jobId}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
error(`Failed to import from Trello: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
importCmd
|
|
45
|
+
.command('status <jobId>')
|
|
46
|
+
.description('Check import job status')
|
|
47
|
+
.action(async (jobId) => {
|
|
48
|
+
try {
|
|
49
|
+
const data = await withSpinner('Fetching job status...', () =>
|
|
50
|
+
client.get(`/import/jobs/${jobId}`)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const job = data.job || data.data || data;
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(chalk.bold(' Job ID: '), job._id || jobId);
|
|
56
|
+
console.log(chalk.bold(' Status: '), job.status || 'unknown');
|
|
57
|
+
console.log(chalk.bold(' Progress: '), `${job.progress ?? 0}%`);
|
|
58
|
+
if (job.boardsImported) {
|
|
59
|
+
console.log(chalk.bold(' Boards: '), job.boardsImported);
|
|
60
|
+
}
|
|
61
|
+
if (job.cardsImported) {
|
|
62
|
+
console.log(chalk.bold(' Cards: '), job.cardsImported);
|
|
63
|
+
}
|
|
64
|
+
if (job.error) {
|
|
65
|
+
console.log(chalk.bold(' Error: '), chalk.red(job.error));
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
error(`Failed to get job status: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
importCmd
|
|
74
|
+
.command('jobs')
|
|
75
|
+
.description('List import jobs')
|
|
76
|
+
.option('-f, --format <format>', 'Output format')
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
try {
|
|
79
|
+
const data = await withSpinner('Fetching jobs...', () =>
|
|
80
|
+
client.get('/import/jobs')
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const jobs = data.jobs || data.data || data;
|
|
84
|
+
const rows = (Array.isArray(jobs) ? jobs : []).map(j => ({
|
|
85
|
+
source: j.source || 'trello',
|
|
86
|
+
status: j.status || 'unknown',
|
|
87
|
+
progress: `${j.progress ?? 0}%`,
|
|
88
|
+
created: formatRelativeTime(j.createdAt),
|
|
89
|
+
id: j._id,
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
output(rows, {
|
|
93
|
+
headers: ['Source', 'Status', 'Progress', 'Created', 'ID'],
|
|
94
|
+
format: options.format,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
error(`Failed to list jobs: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
module.exports = importCmd;
|