@zoobbe/cli 1.2.0 → 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 +205 -19
- 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 +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/utils/format.js +39 -0
- package/src/utils/prompts.js +40 -0
- package/src/utils/resolve.js +67 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const client = require('../lib/client');
|
|
4
|
+
const { output, success, error, warn } = require('../lib/output');
|
|
5
|
+
const { withSpinner } = require('../utils/spinner');
|
|
6
|
+
const { confirmAction } = require('../utils/prompts');
|
|
7
|
+
|
|
8
|
+
const apiKey = new Command('api-key')
|
|
9
|
+
.alias('ak')
|
|
10
|
+
.description('API key management commands (some operations require session auth)');
|
|
11
|
+
|
|
12
|
+
const AUTH_NOTE = 'Note: API key management may be restricted when authenticated via API key. Use browser session if this fails.';
|
|
13
|
+
|
|
14
|
+
apiKey
|
|
15
|
+
.command('list')
|
|
16
|
+
.alias('ls')
|
|
17
|
+
.description('List your API keys')
|
|
18
|
+
.option('-f, --format <format>', 'Output format')
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
try {
|
|
21
|
+
const data = await withSpinner('Fetching API keys...', () =>
|
|
22
|
+
client.get('/api-keys')
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const keys = data.apiKeys || data.data || data;
|
|
26
|
+
const rows = (Array.isArray(keys) ? keys : []).map(k => ({
|
|
27
|
+
name: k.name || 'Unnamed',
|
|
28
|
+
prefix: k.prefix || k.key?.substring(0, 8) || 'N/A',
|
|
29
|
+
created: new Date(k.createdAt).toLocaleDateString(),
|
|
30
|
+
lastUsed: k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'Never',
|
|
31
|
+
id: k._id,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
output(rows, {
|
|
35
|
+
headers: ['Name', 'Prefix', 'Created', 'Last Used', 'ID'],
|
|
36
|
+
format: options.format,
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
warn(AUTH_NOTE);
|
|
40
|
+
error(`Failed to list API keys: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
apiKey
|
|
45
|
+
.command('create')
|
|
46
|
+
.description('Create a new API key')
|
|
47
|
+
.option('--name <name>', 'Key name (required)')
|
|
48
|
+
.action(async (options) => {
|
|
49
|
+
try {
|
|
50
|
+
if (!options.name) return error('Key name is required. Use --name <name>.');
|
|
51
|
+
|
|
52
|
+
const data = await withSpinner('Creating API key...', () =>
|
|
53
|
+
client.post('/api-keys', { name: options.name })
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const key = data.apiKey || data.key || data.data || data;
|
|
57
|
+
console.log();
|
|
58
|
+
success(`Created API key: ${chalk.bold(options.name)}`);
|
|
59
|
+
if (key.key || key.token) {
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(chalk.yellow(' Save this key — it won\'t be shown again:'));
|
|
62
|
+
console.log(chalk.bold(` ${key.key || key.token}`));
|
|
63
|
+
console.log();
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
warn(AUTH_NOTE);
|
|
67
|
+
error(`Failed to create API key: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
apiKey
|
|
72
|
+
.command('delete <keyId>')
|
|
73
|
+
.description('Delete an API key')
|
|
74
|
+
.option('--force', 'Skip confirmation')
|
|
75
|
+
.action(async (keyId, options) => {
|
|
76
|
+
try {
|
|
77
|
+
if (!options.force) {
|
|
78
|
+
const confirmed = await confirmAction('Delete this API key? Any integrations using it will stop working.');
|
|
79
|
+
if (!confirmed) return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await withSpinner('Deleting API key...', () =>
|
|
83
|
+
client.delete(`/api-keys/${keyId}`)
|
|
84
|
+
);
|
|
85
|
+
success('Deleted API key');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
warn(AUTH_NOTE);
|
|
88
|
+
error(`Failed to delete API key: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
apiKey
|
|
93
|
+
.command('rename <keyId>')
|
|
94
|
+
.description('Rename an API key')
|
|
95
|
+
.option('--name <name>', 'New name (required)')
|
|
96
|
+
.action(async (keyId, options) => {
|
|
97
|
+
try {
|
|
98
|
+
if (!options.name) return error('Name is required. Use --name <name>.');
|
|
99
|
+
|
|
100
|
+
await withSpinner('Renaming API key...', () =>
|
|
101
|
+
client.put(`/api-keys/${keyId}`, { name: options.name })
|
|
102
|
+
);
|
|
103
|
+
success(`Renamed API key to: ${chalk.bold(options.name)}`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
warn(AUTH_NOTE);
|
|
106
|
+
error(`Failed to rename API key: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
apiKey
|
|
111
|
+
.command('regenerate <keyId>')
|
|
112
|
+
.description('Regenerate an API key')
|
|
113
|
+
.option('--force', 'Skip confirmation')
|
|
114
|
+
.action(async (keyId, options) => {
|
|
115
|
+
try {
|
|
116
|
+
if (!options.force) {
|
|
117
|
+
const confirmed = await confirmAction('Regenerate this API key? The old key will stop working immediately.');
|
|
118
|
+
if (!confirmed) return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const data = await withSpinner('Regenerating API key...', () =>
|
|
122
|
+
client.post(`/api-keys/${keyId}/regenerate`)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const key = data.apiKey || data.key || data.data || data;
|
|
126
|
+
console.log();
|
|
127
|
+
success('API key regenerated');
|
|
128
|
+
if (key.key || key.token) {
|
|
129
|
+
console.log();
|
|
130
|
+
console.log(chalk.yellow(' Save this key — it won\'t be shown again:'));
|
|
131
|
+
console.log(chalk.bold(` ${key.key || key.token}`));
|
|
132
|
+
console.log();
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
warn(AUTH_NOTE);
|
|
136
|
+
error(`Failed to regenerate API key: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
module.exports = apiKey;
|
package/src/commands/auth.js
CHANGED
|
@@ -245,6 +245,8 @@ auth
|
|
|
245
245
|
.description('Clear stored credentials')
|
|
246
246
|
.action(() => {
|
|
247
247
|
config.set('apiKey', '');
|
|
248
|
+
config.set('apiUrl', 'https://api.zoobbe.com');
|
|
249
|
+
config.set('webUrl', '');
|
|
248
250
|
config.set('userId', '');
|
|
249
251
|
config.set('userName', '');
|
|
250
252
|
config.set('email', '');
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const client = require('../lib/client');
|
|
4
|
+
const { output, success, error } = require('../lib/output');
|
|
5
|
+
const { withSpinner } = require('../utils/spinner');
|
|
6
|
+
const { confirmAction } = require('../utils/prompts');
|
|
7
|
+
const { formatRelativeTime } = require('../utils/format');
|
|
8
|
+
|
|
9
|
+
const automation = new Command('automation')
|
|
10
|
+
.alias('auto')
|
|
11
|
+
.description('Board automation commands');
|
|
12
|
+
|
|
13
|
+
automation
|
|
14
|
+
.command('list')
|
|
15
|
+
.alias('ls')
|
|
16
|
+
.description('List automations on a board')
|
|
17
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
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 automations...', () =>
|
|
24
|
+
client.get(`/boards/${options.board}/automations`)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const automations = data.automations || data.data || data;
|
|
28
|
+
const rows = (Array.isArray(automations) ? automations : []).map(a => ({
|
|
29
|
+
name: a.name || a.title || 'Unnamed',
|
|
30
|
+
trigger: a.trigger?.type || a.triggerType || 'N/A',
|
|
31
|
+
action: a.action?.type || a.actionType || 'N/A',
|
|
32
|
+
status: a.enabled ? chalk.green('enabled') : chalk.yellow('disabled'),
|
|
33
|
+
id: a._id,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
output(rows, {
|
|
37
|
+
headers: ['Name', 'Trigger', 'Action', 'Status', 'ID'],
|
|
38
|
+
format: options.format,
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
error(`Failed to list automations: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
automation
|
|
46
|
+
.command('create')
|
|
47
|
+
.description('Create a new automation (interactive)')
|
|
48
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
try {
|
|
51
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
52
|
+
|
|
53
|
+
const inquirer = require('inquirer');
|
|
54
|
+
|
|
55
|
+
// Fetch available options
|
|
56
|
+
const optionsData = await withSpinner('Fetching automation options...', () =>
|
|
57
|
+
client.get(`/boards/${options.board}/automations/options`)
|
|
58
|
+
);
|
|
59
|
+
const opts = optionsData.options || optionsData.data || optionsData;
|
|
60
|
+
|
|
61
|
+
const triggers = opts.triggers || ['card_created', 'card_moved', 'card_completed', 'due_date_reached'];
|
|
62
|
+
const actions = opts.actions || ['move_card', 'assign_member', 'add_label', 'send_notification'];
|
|
63
|
+
|
|
64
|
+
const answers = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: 'input',
|
|
67
|
+
name: 'name',
|
|
68
|
+
message: 'Automation name:',
|
|
69
|
+
validate: v => v.length > 0 || 'Name is required',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'list',
|
|
73
|
+
name: 'triggerType',
|
|
74
|
+
message: 'Select trigger:',
|
|
75
|
+
choices: triggers,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'list',
|
|
79
|
+
name: 'actionType',
|
|
80
|
+
message: 'Select action:',
|
|
81
|
+
choices: actions,
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const data = await withSpinner('Creating automation...', () =>
|
|
86
|
+
client.post(`/boards/${options.board}/automations`, {
|
|
87
|
+
name: answers.name,
|
|
88
|
+
trigger: { type: answers.triggerType },
|
|
89
|
+
action: { type: answers.actionType },
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const a = data.automation || data.data || data;
|
|
94
|
+
success(`Created automation: ${chalk.bold(answers.name)} (${a._id})`);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
error(`Failed to create automation: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
automation
|
|
101
|
+
.command('update <automationId>')
|
|
102
|
+
.description('Update an automation')
|
|
103
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
104
|
+
.option('--name <name>', 'New name')
|
|
105
|
+
.action(async (automationId, options) => {
|
|
106
|
+
try {
|
|
107
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
108
|
+
|
|
109
|
+
const body = {};
|
|
110
|
+
if (options.name) body.name = options.name;
|
|
111
|
+
if (Object.keys(body).length === 0) return error('Provide --name.');
|
|
112
|
+
|
|
113
|
+
await withSpinner('Updating automation...', () =>
|
|
114
|
+
client.put(`/boards/${options.board}/automations/${automationId}`, body)
|
|
115
|
+
);
|
|
116
|
+
success(`Updated automation ${automationId}`);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
error(`Failed to update automation: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
automation
|
|
123
|
+
.command('delete <automationId>')
|
|
124
|
+
.description('Delete an automation')
|
|
125
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
126
|
+
.option('--force', 'Skip confirmation')
|
|
127
|
+
.action(async (automationId, options) => {
|
|
128
|
+
try {
|
|
129
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
130
|
+
|
|
131
|
+
if (!options.force) {
|
|
132
|
+
const confirmed = await confirmAction('Delete this automation?');
|
|
133
|
+
if (!confirmed) return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await withSpinner('Deleting automation...', () =>
|
|
137
|
+
client.delete(`/boards/${options.board}/automations/${automationId}`)
|
|
138
|
+
);
|
|
139
|
+
success('Deleted automation');
|
|
140
|
+
} catch (err) {
|
|
141
|
+
error(`Failed to delete automation: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
automation
|
|
146
|
+
.command('toggle <automationId>')
|
|
147
|
+
.description('Enable or disable an automation')
|
|
148
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
149
|
+
.action(async (automationId, options) => {
|
|
150
|
+
try {
|
|
151
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
152
|
+
|
|
153
|
+
const data = await withSpinner('Toggling automation...', () =>
|
|
154
|
+
client.patch(`/boards/${options.board}/automations/${automationId}/toggle`)
|
|
155
|
+
);
|
|
156
|
+
const a = data.automation || data.data || data;
|
|
157
|
+
success(a.enabled ? 'Automation enabled' : 'Automation disabled');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
error(`Failed to toggle automation: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
automation
|
|
164
|
+
.command('logs <automationId>')
|
|
165
|
+
.description('Show automation execution logs')
|
|
166
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
167
|
+
.option('--limit <n>', 'Number of logs', '20')
|
|
168
|
+
.option('-f, --format <format>', 'Output format')
|
|
169
|
+
.action(async (automationId, options) => {
|
|
170
|
+
try {
|
|
171
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
172
|
+
|
|
173
|
+
const data = await withSpinner('Fetching logs...', () =>
|
|
174
|
+
client.get(`/boards/${options.board}/automations/${automationId}/logs`)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const logs = data.logs || data.data || data;
|
|
178
|
+
const logArr = Array.isArray(logs) ? logs : [];
|
|
179
|
+
const limited = logArr.slice(0, parseInt(options.limit));
|
|
180
|
+
|
|
181
|
+
const rows = limited.map(l => ({
|
|
182
|
+
status: l.success ? chalk.green('success') : chalk.red('failed'),
|
|
183
|
+
trigger: l.trigger || 'N/A',
|
|
184
|
+
time: formatRelativeTime(l.createdAt || l.executedAt),
|
|
185
|
+
error: l.error || '-',
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
output(rows, {
|
|
189
|
+
headers: ['Status', 'Trigger', 'Time', 'Error'],
|
|
190
|
+
format: options.format,
|
|
191
|
+
});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
error(`Failed to fetch logs: ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
automation
|
|
198
|
+
.command('options')
|
|
199
|
+
.description('Show available automation triggers and actions')
|
|
200
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
201
|
+
.action(async (options) => {
|
|
202
|
+
try {
|
|
203
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
204
|
+
|
|
205
|
+
const data = await withSpinner('Fetching options...', () =>
|
|
206
|
+
client.get(`/boards/${options.board}/automations/options`)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const opts = data.options || data.data || data;
|
|
210
|
+
console.log();
|
|
211
|
+
console.log(chalk.bold(' Available Triggers:'));
|
|
212
|
+
const triggers = opts.triggers || [];
|
|
213
|
+
triggers.forEach(t => console.log(` - ${t.type || t}`));
|
|
214
|
+
console.log();
|
|
215
|
+
console.log(chalk.bold(' Available Actions:'));
|
|
216
|
+
const actions = opts.actions || [];
|
|
217
|
+
actions.forEach(a => console.log(` - ${a.type || a}`));
|
|
218
|
+
console.log();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
error(`Failed to fetch options: ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
automation
|
|
225
|
+
.command('info <automationId>')
|
|
226
|
+
.description('Show automation details')
|
|
227
|
+
.option('-b, --board <boardId>', 'Board ID (required)')
|
|
228
|
+
.action(async (automationId, options) => {
|
|
229
|
+
try {
|
|
230
|
+
if (!options.board) return error('Board ID is required. Use -b <boardId>.');
|
|
231
|
+
|
|
232
|
+
// Fetch all automations and find the one
|
|
233
|
+
const data = await withSpinner('Fetching automation...', () =>
|
|
234
|
+
client.get(`/boards/${options.board}/automations`)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const automations = data.automations || data.data || data;
|
|
238
|
+
const match = (Array.isArray(automations) ? automations : []).find(a => a._id === automationId);
|
|
239
|
+
|
|
240
|
+
if (!match) return error(`Automation "${automationId}" not found.`);
|
|
241
|
+
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(chalk.bold(' Name: '), match.name || 'Unnamed');
|
|
244
|
+
console.log(chalk.bold(' ID: '), match._id);
|
|
245
|
+
console.log(chalk.bold(' Trigger: '), match.trigger?.type || 'N/A');
|
|
246
|
+
console.log(chalk.bold(' Action: '), match.action?.type || 'N/A');
|
|
247
|
+
console.log(chalk.bold(' Status: '), match.enabled ? 'Enabled' : 'Disabled');
|
|
248
|
+
console.log(chalk.bold(' Runs: '), match.executionCount ?? 0);
|
|
249
|
+
console.log();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
error(`Failed to get automation info: ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
module.exports = automation;
|
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;
|