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