@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
|
@@ -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;
|
package/src/commands/page.js
CHANGED
|
@@ -67,8 +67,7 @@ page
|
|
|
67
67
|
.action(async (pageId) => {
|
|
68
68
|
try {
|
|
69
69
|
const open = require('open');
|
|
70
|
-
const
|
|
71
|
-
const baseUrl = apiUrl.replace('api.', '');
|
|
70
|
+
const baseUrl = config.getWebUrl();
|
|
72
71
|
await open(`${baseUrl}/page/${pageId}`);
|
|
73
72
|
success('Opened page in browser');
|
|
74
73
|
} catch (err) {
|
|
@@ -99,4 +98,257 @@ page
|
|
|
99
98
|
}
|
|
100
99
|
});
|
|
101
100
|
|
|
101
|
+
// ── Extended commands ──
|
|
102
|
+
|
|
103
|
+
const { confirmAction } = require('../utils/prompts');
|
|
104
|
+
|
|
105
|
+
page
|
|
106
|
+
.command('update <pageId>')
|
|
107
|
+
.description('Update a page')
|
|
108
|
+
.option('-t, --title <title>', 'New title')
|
|
109
|
+
.option('--icon <icon>', 'Page icon/emoji')
|
|
110
|
+
.action(async (pageId, options) => {
|
|
111
|
+
try {
|
|
112
|
+
const body = {};
|
|
113
|
+
if (options.title) body.title = options.title;
|
|
114
|
+
if (options.icon) body.icon = options.icon;
|
|
115
|
+
if (Object.keys(body).length === 0) return error('Provide -t or --icon.');
|
|
116
|
+
|
|
117
|
+
await withSpinner('Updating page...', () =>
|
|
118
|
+
client.put(`/pages/${pageId}`, body)
|
|
119
|
+
);
|
|
120
|
+
success(`Updated page ${pageId}`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
error(`Failed to update page: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
page
|
|
127
|
+
.command('delete <pageId>')
|
|
128
|
+
.description('Delete a page')
|
|
129
|
+
.option('--permanent', 'Permanently delete (cannot be restored)')
|
|
130
|
+
.option('--force', 'Skip confirmation')
|
|
131
|
+
.action(async (pageId, options) => {
|
|
132
|
+
try {
|
|
133
|
+
if (!options.force) {
|
|
134
|
+
const msg = options.permanent
|
|
135
|
+
? 'Permanently delete this page? This cannot be undone.'
|
|
136
|
+
: 'Delete this page?';
|
|
137
|
+
const confirmed = await confirmAction(msg);
|
|
138
|
+
if (!confirmed) return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const url = options.permanent ? `/pages/${pageId}/permanent` : `/pages/${pageId}`;
|
|
142
|
+
await withSpinner('Deleting page...', () =>
|
|
143
|
+
client.delete(url)
|
|
144
|
+
);
|
|
145
|
+
success(`Deleted page ${pageId}`);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
error(`Failed to delete page: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
page
|
|
152
|
+
.command('archive <pageId>')
|
|
153
|
+
.description('Archive a page')
|
|
154
|
+
.action(async (pageId) => {
|
|
155
|
+
try {
|
|
156
|
+
await withSpinner('Archiving page...', () =>
|
|
157
|
+
client.post(`/pages/${pageId}/archive`)
|
|
158
|
+
);
|
|
159
|
+
success(`Archived page ${pageId}`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
error(`Failed to archive page: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
page
|
|
166
|
+
.command('restore <pageId>')
|
|
167
|
+
.description('Restore an archived page')
|
|
168
|
+
.action(async (pageId) => {
|
|
169
|
+
try {
|
|
170
|
+
await withSpinner('Restoring page...', () =>
|
|
171
|
+
client.post(`/pages/${pageId}/restore`)
|
|
172
|
+
);
|
|
173
|
+
success(`Restored page ${pageId}`);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
error(`Failed to restore page: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
page
|
|
180
|
+
.command('share <pageId>')
|
|
181
|
+
.description('Manage page sharing')
|
|
182
|
+
.option('--public', 'Make page publicly accessible')
|
|
183
|
+
.option('--revoke', 'Revoke public access')
|
|
184
|
+
.option('-u, --user <userId>', 'Share with a specific user')
|
|
185
|
+
.option('--role <role>', 'Role for shared user (editor|viewer)', 'viewer')
|
|
186
|
+
.action(async (pageId, options) => {
|
|
187
|
+
try {
|
|
188
|
+
if (options.revoke) {
|
|
189
|
+
await withSpinner('Revoking share...', () =>
|
|
190
|
+
client.post(`/pages/${pageId}/share/revoke`)
|
|
191
|
+
);
|
|
192
|
+
success('Public sharing revoked');
|
|
193
|
+
} else if (options.public) {
|
|
194
|
+
const data = await withSpinner('Enabling public share...', () =>
|
|
195
|
+
client.post(`/pages/${pageId}/share/public`)
|
|
196
|
+
);
|
|
197
|
+
const link = data.shareLink || data.link || data.data;
|
|
198
|
+
success(`Page shared publicly${link ? `: ${chalk.bold(link)}` : ''}`);
|
|
199
|
+
} else if (options.user) {
|
|
200
|
+
await withSpinner('Sharing with user...', () =>
|
|
201
|
+
client.post(`/pages/${pageId}/share/user`, {
|
|
202
|
+
userId: options.user,
|
|
203
|
+
role: options.role,
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
success(`Shared page with user (${options.role})`);
|
|
207
|
+
} else {
|
|
208
|
+
return error('Provide --public, --revoke, or -u <userId>.');
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`Failed to share page: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
page
|
|
216
|
+
.command('members <pageId>')
|
|
217
|
+
.description('List page members')
|
|
218
|
+
.option('-f, --format <format>', 'Output format')
|
|
219
|
+
.action(async (pageId, options) => {
|
|
220
|
+
try {
|
|
221
|
+
const data = await withSpinner('Fetching members...', () =>
|
|
222
|
+
client.get(`/pages/${pageId}/members`)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const members = data.members || data.data || data;
|
|
226
|
+
const rows = (Array.isArray(members) ? members : []).map(m => {
|
|
227
|
+
const user = m.user || m;
|
|
228
|
+
return {
|
|
229
|
+
name: user.userName || user.username || user.name || 'N/A',
|
|
230
|
+
email: user.email || 'N/A',
|
|
231
|
+
role: m.role || 'viewer',
|
|
232
|
+
id: user._id || user.id || '',
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
output(rows, {
|
|
237
|
+
headers: ['Name', 'Email', 'Role', 'ID'],
|
|
238
|
+
format: options.format,
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
error(`Failed to list members: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
page
|
|
246
|
+
.command('add-member <pageId>')
|
|
247
|
+
.description('Add a member to a page')
|
|
248
|
+
.option('-u, --user <userId>', 'User ID (required)')
|
|
249
|
+
.option('--role <role>', 'Role (editor|viewer)', 'viewer')
|
|
250
|
+
.action(async (pageId, options) => {
|
|
251
|
+
try {
|
|
252
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
253
|
+
|
|
254
|
+
await withSpinner('Adding member...', () =>
|
|
255
|
+
client.post(`/pages/${pageId}/members`, {
|
|
256
|
+
userId: options.user,
|
|
257
|
+
role: options.role,
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
success(`Added member to page ${pageId}`);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
error(`Failed to add member: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
page
|
|
267
|
+
.command('remove-member <pageId>')
|
|
268
|
+
.description('Remove a member from a page')
|
|
269
|
+
.option('-u, --user <userId>', 'User/member ID (required)')
|
|
270
|
+
.action(async (pageId, options) => {
|
|
271
|
+
try {
|
|
272
|
+
if (!options.user) return error('User ID is required. Use -u <userId>.');
|
|
273
|
+
|
|
274
|
+
await withSpinner('Removing member...', () =>
|
|
275
|
+
client.delete(`/pages/${pageId}/members/${options.user}`)
|
|
276
|
+
);
|
|
277
|
+
success('Removed member from page');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
error(`Failed to remove member: ${err.message}`);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
page
|
|
284
|
+
.command('comments <pageId>')
|
|
285
|
+
.description('List comments on a page')
|
|
286
|
+
.option('-f, --format <format>', 'Output format')
|
|
287
|
+
.action(async (pageId, options) => {
|
|
288
|
+
try {
|
|
289
|
+
const data = await withSpinner('Fetching comments...', () =>
|
|
290
|
+
client.get(`/pages/${pageId}/comments`)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const { formatRelativeTime } = require('../utils/format');
|
|
294
|
+
const comments = data.comments || data.data || data;
|
|
295
|
+
const rows = (Array.isArray(comments) ? comments : []).map(c => ({
|
|
296
|
+
user: c.user?.userName || c.member?.userName || 'N/A',
|
|
297
|
+
comment: (c.comment || c.text || '').substring(0, 80),
|
|
298
|
+
time: formatRelativeTime(c.createdAt),
|
|
299
|
+
id: c._id,
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
output(rows, {
|
|
303
|
+
headers: ['User', 'Comment', 'Time', 'ID'],
|
|
304
|
+
format: options.format,
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
error(`Failed to list comments: ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
page
|
|
312
|
+
.command('comment <pageId> <text>')
|
|
313
|
+
.description('Add a comment to a page')
|
|
314
|
+
.action(async (pageId, text) => {
|
|
315
|
+
try {
|
|
316
|
+
await withSpinner('Adding comment...', () =>
|
|
317
|
+
client.post(`/pages/${pageId}/comments`, { comment: text })
|
|
318
|
+
);
|
|
319
|
+
success('Comment added');
|
|
320
|
+
} catch (err) {
|
|
321
|
+
error(`Failed to add comment: ${err.message}`);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
page
|
|
326
|
+
.command('duplicate <pageId>')
|
|
327
|
+
.description('Duplicate a page')
|
|
328
|
+
.action(async (pageId) => {
|
|
329
|
+
try {
|
|
330
|
+
const data = await withSpinner('Duplicating page...', () =>
|
|
331
|
+
client.post(`/pages/${pageId}/duplicate`)
|
|
332
|
+
);
|
|
333
|
+
const p = data.page || data.data || data;
|
|
334
|
+
success(`Duplicated page: ${chalk.bold(p.title || 'Copy')} (${p.shortId || p._id})`);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
error(`Failed to duplicate page: ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
page
|
|
341
|
+
.command('favorite <pageId>')
|
|
342
|
+
.description('Toggle favorite on a page')
|
|
343
|
+
.action(async (pageId) => {
|
|
344
|
+
try {
|
|
345
|
+
const data = await withSpinner('Toggling favorite...', () =>
|
|
346
|
+
client.post(`/pages/${pageId}/favorite`)
|
|
347
|
+
);
|
|
348
|
+
success(data.message || 'Favorite toggled');
|
|
349
|
+
} catch (err) {
|
|
350
|
+
error(`Failed to toggle favorite: ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
102
354
|
module.exports = page;
|