@krodak/clickup-cli 1.18.1 → 1.19.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/.claude-plugin/plugin.json +1 -1
- package/README.md +50 -19
- package/dist/chunk-3VDSLYDJ.js +2041 -0
- package/dist/index.js +472 -1990
- package/dist/sprint-5VYRQK6Z.js +19 -0
- package/package.json +1 -1
- package/skills/clickup-cli/SKILL.md +106 -89
package/dist/index.js
CHANGED
|
@@ -1,4 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ClickUpClient,
|
|
4
|
+
SPRINT_KEYWORDS,
|
|
5
|
+
TASK_COLUMNS,
|
|
6
|
+
addProfile,
|
|
7
|
+
buildTypeMap,
|
|
8
|
+
colorStatus,
|
|
9
|
+
deleteFavorite,
|
|
10
|
+
deleteFilter,
|
|
11
|
+
fetchMyTasks,
|
|
12
|
+
findRelatedSpaces,
|
|
13
|
+
formatAssignConfirmation,
|
|
14
|
+
formatCommentConfirmation,
|
|
15
|
+
formatCommentsMarkdown,
|
|
16
|
+
formatCreateConfirmation,
|
|
17
|
+
formatDate,
|
|
18
|
+
formatDateISO,
|
|
19
|
+
formatDuration,
|
|
20
|
+
formatGroupedTasksMarkdown,
|
|
21
|
+
formatListsMarkdown,
|
|
22
|
+
formatLongDuration,
|
|
23
|
+
formatMarkdownTable,
|
|
24
|
+
formatSpacesMarkdown,
|
|
25
|
+
formatTable,
|
|
26
|
+
formatTaskDetail,
|
|
27
|
+
formatTaskDetailMarkdown,
|
|
28
|
+
formatTimestamp,
|
|
29
|
+
formatUpdateConfirmation,
|
|
30
|
+
getConfigPath,
|
|
31
|
+
getFavorites,
|
|
32
|
+
getFilters,
|
|
33
|
+
groupedTaskPicker,
|
|
34
|
+
isCustomTaskId,
|
|
35
|
+
isDoneStatus,
|
|
36
|
+
isTTY,
|
|
37
|
+
listProfiles,
|
|
38
|
+
loadConfig,
|
|
39
|
+
loadRawConfig,
|
|
40
|
+
openUrl,
|
|
41
|
+
parseSprintDates,
|
|
42
|
+
printTasks,
|
|
43
|
+
removeProfile,
|
|
44
|
+
runSprintCommand,
|
|
45
|
+
saveFavorite,
|
|
46
|
+
saveFilter,
|
|
47
|
+
setDefaultProfile,
|
|
48
|
+
shouldOutputJson,
|
|
49
|
+
showDetailsAndOpen,
|
|
50
|
+
summarize,
|
|
51
|
+
writeConfig
|
|
52
|
+
} from "./chunk-3VDSLYDJ.js";
|
|
2
53
|
|
|
3
54
|
// src/index.ts
|
|
4
55
|
import { realpathSync as realpathSync2 } from "fs";
|
|
@@ -7,1718 +58,6 @@ import { Command } from "commander";
|
|
|
7
58
|
import { createRequire } from "module";
|
|
8
59
|
import { fileURLToPath } from "url";
|
|
9
60
|
|
|
10
|
-
// src/api.ts
|
|
11
|
-
var BASE_URL = "https://api.clickup.com/api/v2";
|
|
12
|
-
var BASE_URL_V3 = "https://api.clickup.com/api/v3";
|
|
13
|
-
var MAX_PAGES = 100;
|
|
14
|
-
function isRecord(value) {
|
|
15
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
-
}
|
|
17
|
-
function expectRecord(value, context) {
|
|
18
|
-
if (!isRecord(value)) {
|
|
19
|
-
throw new Error(`Unexpected API response: expected ${context} object`);
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
function expectRecordField(data, key, context) {
|
|
24
|
-
return expectRecord(data[key], context);
|
|
25
|
-
}
|
|
26
|
-
function expectNumericField(data, key, context) {
|
|
27
|
-
const value = Number(data[key]);
|
|
28
|
-
if (!Number.isInteger(value)) {
|
|
29
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be numeric`);
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
}
|
|
33
|
-
function expectStringField(data, key, context) {
|
|
34
|
-
const value = data[key];
|
|
35
|
-
if (typeof value !== "string") {
|
|
36
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be a string`);
|
|
37
|
-
}
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
function expectArrayField(data, key, context) {
|
|
41
|
-
const value = data[key];
|
|
42
|
-
if (!Array.isArray(value)) {
|
|
43
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be an array`);
|
|
44
|
-
}
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
function readCollectionField(data, key, context) {
|
|
48
|
-
if (data[key] === void 0) return [];
|
|
49
|
-
return expectArrayField(data, key, context);
|
|
50
|
-
}
|
|
51
|
-
function expectBooleanField(data, key, context) {
|
|
52
|
-
const value = data[key];
|
|
53
|
-
if (typeof value !== "boolean") {
|
|
54
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be a boolean`);
|
|
55
|
-
}
|
|
56
|
-
return value;
|
|
57
|
-
}
|
|
58
|
-
function expectPaginatedCollectionField(data, key, context) {
|
|
59
|
-
const items = data[key];
|
|
60
|
-
if (!Array.isArray(items)) {
|
|
61
|
-
throw new Error(`Unexpected API response: expected ${key} array`);
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
items,
|
|
65
|
-
lastPage: expectBooleanField(data, "last_page", context)
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function isCustomTaskId(id) {
|
|
69
|
-
return /^[A-Z]+-\d+$/i.test(id);
|
|
70
|
-
}
|
|
71
|
-
var ClickUpClient = class {
|
|
72
|
-
apiToken;
|
|
73
|
-
teamId;
|
|
74
|
-
meCache = null;
|
|
75
|
-
constructor(config) {
|
|
76
|
-
this.apiToken = config.apiToken;
|
|
77
|
-
this.teamId = config.teamId;
|
|
78
|
-
}
|
|
79
|
-
taskPath(taskId, suffix = "") {
|
|
80
|
-
const base = `/task/${taskId}${suffix}`;
|
|
81
|
-
if (isCustomTaskId(taskId) && this.teamId) {
|
|
82
|
-
const sep = base.includes("?") ? "&" : "?";
|
|
83
|
-
return `${base}${sep}custom_task_ids=true&team_id=${this.teamId}`;
|
|
84
|
-
}
|
|
85
|
-
return base;
|
|
86
|
-
}
|
|
87
|
-
customIdQueryParams(taskId) {
|
|
88
|
-
if (isCustomTaskId(taskId) && this.teamId) {
|
|
89
|
-
return `?custom_task_ids=true&team_id=${this.teamId}`;
|
|
90
|
-
}
|
|
91
|
-
return "";
|
|
92
|
-
}
|
|
93
|
-
async _fetch(baseUrl, path, options = {}) {
|
|
94
|
-
const res = await fetch(`${baseUrl}${path}`, {
|
|
95
|
-
...options,
|
|
96
|
-
signal: AbortSignal.timeout(3e4),
|
|
97
|
-
headers: {
|
|
98
|
-
Authorization: this.apiToken,
|
|
99
|
-
...options.body ? { "Content-Type": "application/json" } : {},
|
|
100
|
-
...options.headers
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
104
|
-
if (!res.ok) {
|
|
105
|
-
throw new Error(`ClickUp API error ${res.status}: ${res.statusText}`);
|
|
106
|
-
}
|
|
107
|
-
return {};
|
|
108
|
-
}
|
|
109
|
-
let parsed;
|
|
110
|
-
try {
|
|
111
|
-
parsed = await res.json();
|
|
112
|
-
} catch {
|
|
113
|
-
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
114
|
-
}
|
|
115
|
-
const data = expectRecord(parsed, "JSON");
|
|
116
|
-
if (!res.ok) {
|
|
117
|
-
const raw = data.err ?? data.error ?? data.ECODE ?? res.statusText;
|
|
118
|
-
const errMsg = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
119
|
-
throw new Error(`ClickUp API error ${res.status}: ${errMsg}`);
|
|
120
|
-
}
|
|
121
|
-
return data;
|
|
122
|
-
}
|
|
123
|
-
async request(path, options = {}) {
|
|
124
|
-
return this._fetch(BASE_URL, path, options);
|
|
125
|
-
}
|
|
126
|
-
async requestV3(path, options = {}) {
|
|
127
|
-
return this._fetch(BASE_URL_V3, path, options);
|
|
128
|
-
}
|
|
129
|
-
async getMe() {
|
|
130
|
-
if (this.meCache) return this.meCache;
|
|
131
|
-
const data = await this.request(
|
|
132
|
-
"/user"
|
|
133
|
-
);
|
|
134
|
-
const user = expectRecordField(data, "user", "user");
|
|
135
|
-
const timezone = typeof user.timezone === "string" && user.timezone ? user.timezone : void 0;
|
|
136
|
-
this.meCache = {
|
|
137
|
-
id: expectNumericField(user, "id", "user"),
|
|
138
|
-
username: expectStringField(user, "username", "user"),
|
|
139
|
-
...timezone ? { timezone } : {}
|
|
140
|
-
};
|
|
141
|
-
return this.meCache;
|
|
142
|
-
}
|
|
143
|
-
async getUserTimezone() {
|
|
144
|
-
const me = await this.getMe();
|
|
145
|
-
return me.timezone;
|
|
146
|
-
}
|
|
147
|
-
async paginate(buildPath) {
|
|
148
|
-
const allTasks = [];
|
|
149
|
-
let page = 0;
|
|
150
|
-
let lastPage = false;
|
|
151
|
-
while (!lastPage && page < MAX_PAGES) {
|
|
152
|
-
const data = await this.request(buildPath(page));
|
|
153
|
-
const taskPage = expectPaginatedCollectionField(
|
|
154
|
-
data,
|
|
155
|
-
"tasks",
|
|
156
|
-
"task page"
|
|
157
|
-
);
|
|
158
|
-
allTasks.push(...taskPage.items);
|
|
159
|
-
lastPage = taskPage.lastPage;
|
|
160
|
-
page++;
|
|
161
|
-
}
|
|
162
|
-
if (page >= MAX_PAGES && !lastPage) {
|
|
163
|
-
process.stderr.write(
|
|
164
|
-
`Warning: reached maximum page limit (${MAX_PAGES}), results may be incomplete
|
|
165
|
-
`
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
return allTasks;
|
|
169
|
-
}
|
|
170
|
-
async getMyTasks(teamId, filters = {}) {
|
|
171
|
-
const baseParams = new URLSearchParams({
|
|
172
|
-
subtasks: String(filters.subtasks ?? true)
|
|
173
|
-
});
|
|
174
|
-
if (filters.includeClosed) baseParams.set("include_closed", "true");
|
|
175
|
-
if (!filters.all) {
|
|
176
|
-
const me = await this.getMe();
|
|
177
|
-
baseParams.append("assignees[]", String(me.id));
|
|
178
|
-
}
|
|
179
|
-
if (filters.assignees) {
|
|
180
|
-
for (const id of filters.assignees) baseParams.append("assignees[]", String(id));
|
|
181
|
-
}
|
|
182
|
-
for (const s of filters.statuses ?? []) baseParams.append("statuses[]", s);
|
|
183
|
-
for (const id of filters.listIds ?? []) baseParams.append("list_ids[]", id);
|
|
184
|
-
for (const id of filters.spaceIds ?? []) baseParams.append("space_ids[]", id);
|
|
185
|
-
for (const tag of filters.tags ?? []) baseParams.append("tags[]", tag);
|
|
186
|
-
if (filters.dueDateGt) baseParams.set("due_date_gt", String(filters.dueDateGt));
|
|
187
|
-
if (filters.dueDateLt) baseParams.set("due_date_lt", String(filters.dueDateLt));
|
|
188
|
-
if (filters.dateCreatedGt) baseParams.set("date_created_gt", String(filters.dateCreatedGt));
|
|
189
|
-
if (filters.dateCreatedLt) baseParams.set("date_created_lt", String(filters.dateCreatedLt));
|
|
190
|
-
if (filters.dateUpdatedGt) baseParams.set("date_updated_gt", String(filters.dateUpdatedGt));
|
|
191
|
-
if (filters.dateUpdatedLt) baseParams.set("date_updated_lt", String(filters.dateUpdatedLt));
|
|
192
|
-
if (filters.customFields?.length) {
|
|
193
|
-
baseParams.set("custom_fields", JSON.stringify(filters.customFields));
|
|
194
|
-
}
|
|
195
|
-
return this.paginate((page) => {
|
|
196
|
-
const params = new URLSearchParams(baseParams);
|
|
197
|
-
params.set("page", String(page));
|
|
198
|
-
return `/team/${teamId}/task?${params.toString()}`;
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
async updateTask(taskId, options) {
|
|
202
|
-
return this.request(this.taskPath(taskId), {
|
|
203
|
-
method: "PUT",
|
|
204
|
-
body: JSON.stringify(options)
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
async postComment(taskId, commentText, notifyAll) {
|
|
208
|
-
const body = { comment_text: commentText };
|
|
209
|
-
if (notifyAll) body.notify_all = true;
|
|
210
|
-
return this.request(this.taskPath(taskId, "/comment"), {
|
|
211
|
-
method: "POST",
|
|
212
|
-
body: JSON.stringify(body)
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
async getTaskComments(taskId) {
|
|
216
|
-
const data = await this.request(this.taskPath(taskId, "/comment"));
|
|
217
|
-
return readCollectionField(
|
|
218
|
-
data,
|
|
219
|
-
"comments",
|
|
220
|
-
"task comments"
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
async getTasksFromList(listId, params = {}, options = {}) {
|
|
224
|
-
return this.paginate((page) => {
|
|
225
|
-
const base = { subtasks: "true", page: String(page), ...params };
|
|
226
|
-
if (options.includeClosed) base["include_closed"] = "true";
|
|
227
|
-
const qs = new URLSearchParams(base).toString();
|
|
228
|
-
return `/list/${listId}/task?${qs}`;
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
async getTask(taskId) {
|
|
232
|
-
return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
|
|
233
|
-
}
|
|
234
|
-
async createTask(listId, options) {
|
|
235
|
-
return this.request(`/list/${listId}/task`, {
|
|
236
|
-
method: "POST",
|
|
237
|
-
body: JSON.stringify(options)
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
async getTeams() {
|
|
241
|
-
const data = await this.request("/team");
|
|
242
|
-
return readCollectionField(data, "teams", "teams");
|
|
243
|
-
}
|
|
244
|
-
async getSpaceWithStatuses(spaceId) {
|
|
245
|
-
return this.request(`/space/${spaceId}`);
|
|
246
|
-
}
|
|
247
|
-
async getListWithStatuses(listId) {
|
|
248
|
-
return this.request(`/list/${listId}`);
|
|
249
|
-
}
|
|
250
|
-
async createSpace(teamId, name) {
|
|
251
|
-
return this.request(`/team/${teamId}/space`, {
|
|
252
|
-
method: "POST",
|
|
253
|
-
body: JSON.stringify({ name, multiple_assignees: true })
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
async getSpaces(teamId) {
|
|
257
|
-
const data = await this.request(`/team/${teamId}/space?archived=false`);
|
|
258
|
-
return readCollectionField(data, "spaces", "spaces");
|
|
259
|
-
}
|
|
260
|
-
async getCustomTaskTypes(teamId) {
|
|
261
|
-
const data = await this.request(
|
|
262
|
-
`/team/${teamId}/custom_item`
|
|
263
|
-
);
|
|
264
|
-
return readCollectionField(
|
|
265
|
-
data,
|
|
266
|
-
"custom_items",
|
|
267
|
-
"custom task types"
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
async createList(spaceId, name) {
|
|
271
|
-
return this.request(`/space/${spaceId}/list`, {
|
|
272
|
-
method: "POST",
|
|
273
|
-
body: JSON.stringify({ name })
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
async createFolderList(folderId, name) {
|
|
277
|
-
return this.request(`/folder/${folderId}/list`, {
|
|
278
|
-
method: "POST",
|
|
279
|
-
body: JSON.stringify({ name })
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
async updateList(listId, payload) {
|
|
283
|
-
return this.request(`/list/${listId}`, {
|
|
284
|
-
method: "PUT",
|
|
285
|
-
body: JSON.stringify(payload)
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
async createFolder(spaceId, name) {
|
|
289
|
-
return this.request(`/space/${spaceId}/folder`, {
|
|
290
|
-
method: "POST",
|
|
291
|
-
body: JSON.stringify({ name })
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
async getLists(spaceId) {
|
|
295
|
-
const data = await this.request(`/space/${spaceId}/list?archived=false`);
|
|
296
|
-
return readCollectionField(data, "lists", "space lists");
|
|
297
|
-
}
|
|
298
|
-
async getFolders(spaceId) {
|
|
299
|
-
const data = await this.request(
|
|
300
|
-
`/space/${spaceId}/folder?archived=false`
|
|
301
|
-
);
|
|
302
|
-
return readCollectionField(data, "folders", "space folders");
|
|
303
|
-
}
|
|
304
|
-
async getFolderLists(folderId) {
|
|
305
|
-
const data = await this.request(`/folder/${folderId}/list?archived=false`);
|
|
306
|
-
return readCollectionField(data, "lists", "folder lists");
|
|
307
|
-
}
|
|
308
|
-
async getListViews(listId) {
|
|
309
|
-
return this.request(`/list/${listId}/view`);
|
|
310
|
-
}
|
|
311
|
-
async getSpaceViews(spaceId) {
|
|
312
|
-
const data = await this.request(`/space/${spaceId}/view`);
|
|
313
|
-
return readCollectionField(data, "views", "views");
|
|
314
|
-
}
|
|
315
|
-
async getFolderViews(folderId) {
|
|
316
|
-
const data = await this.request(`/folder/${folderId}/view`);
|
|
317
|
-
return readCollectionField(data, "views", "views");
|
|
318
|
-
}
|
|
319
|
-
async getWorkspaceViews(teamId) {
|
|
320
|
-
const data = await this.request(`/team/${teamId}/view`);
|
|
321
|
-
return readCollectionField(data, "views", "views");
|
|
322
|
-
}
|
|
323
|
-
async getViewTasks(viewId) {
|
|
324
|
-
return this.paginate((page) => `/view/${viewId}/task?page=${page}`);
|
|
325
|
-
}
|
|
326
|
-
async getView(viewId) {
|
|
327
|
-
const data = await this.request(`/view/${viewId}`);
|
|
328
|
-
return expectRecordField(data, "view", "view");
|
|
329
|
-
}
|
|
330
|
-
async createListView(listId, payload) {
|
|
331
|
-
const data = await this.request(`/list/${listId}/view`, {
|
|
332
|
-
method: "POST",
|
|
333
|
-
body: JSON.stringify(payload)
|
|
334
|
-
});
|
|
335
|
-
return expectRecordField(data, "view", "view");
|
|
336
|
-
}
|
|
337
|
-
async updateView(viewId, payload) {
|
|
338
|
-
const data = await this.request(`/view/${viewId}`, {
|
|
339
|
-
method: "PUT",
|
|
340
|
-
body: JSON.stringify(payload)
|
|
341
|
-
});
|
|
342
|
-
return expectRecordField(data, "view", "view");
|
|
343
|
-
}
|
|
344
|
-
async deleteView(viewId) {
|
|
345
|
-
await this.request(`/view/${viewId}`, { method: "DELETE" });
|
|
346
|
-
}
|
|
347
|
-
async getListTemplates(teamId) {
|
|
348
|
-
const data = await this.request(`/team/${teamId}/list_template`);
|
|
349
|
-
return readCollectionField(
|
|
350
|
-
data,
|
|
351
|
-
"templates",
|
|
352
|
-
"list templates"
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
async getFolderTemplates(teamId) {
|
|
356
|
-
const data = await this.request(
|
|
357
|
-
`/team/${teamId}/folder_template`
|
|
358
|
-
);
|
|
359
|
-
return readCollectionField(
|
|
360
|
-
data,
|
|
361
|
-
"templates",
|
|
362
|
-
"folder templates"
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
async createListFromTemplate(containerId, templateId, name, containerType) {
|
|
366
|
-
return this.request(
|
|
367
|
-
`/${containerType}/${containerId}/list_template/${templateId}`,
|
|
368
|
-
{ method: "POST", body: JSON.stringify({ name }) }
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
async addTaskToList(taskId, listId) {
|
|
372
|
-
await this.request(`/list/${listId}/task/${taskId}`, { method: "POST" });
|
|
373
|
-
}
|
|
374
|
-
async removeTaskFromList(taskId, listId) {
|
|
375
|
-
await this.request(`/list/${listId}/task/${taskId}`, { method: "DELETE" });
|
|
376
|
-
}
|
|
377
|
-
async setCustomFieldValue(taskId, fieldId, value) {
|
|
378
|
-
await this.request(this.taskPath(taskId, `/field/${fieldId}`), {
|
|
379
|
-
method: "POST",
|
|
380
|
-
body: JSON.stringify({ value })
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
async removeCustomFieldValue(taskId, fieldId) {
|
|
384
|
-
await this.request(this.taskPath(taskId, `/field/${fieldId}`), { method: "DELETE" });
|
|
385
|
-
}
|
|
386
|
-
async deleteTask(taskId) {
|
|
387
|
-
await this.request(this.taskPath(taskId), { method: "DELETE" });
|
|
388
|
-
}
|
|
389
|
-
async addTagToTask(taskId, tagName) {
|
|
390
|
-
await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
|
|
391
|
-
method: "POST"
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
async removeTagFromTask(taskId, tagName) {
|
|
395
|
-
await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
|
|
396
|
-
method: "DELETE"
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
async addDependency(taskId, opts) {
|
|
400
|
-
const body = {};
|
|
401
|
-
if (opts.dependsOn) body.depends_on = opts.dependsOn;
|
|
402
|
-
if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
|
|
403
|
-
await this.request(this.taskPath(taskId, "/dependency"), {
|
|
404
|
-
method: "POST",
|
|
405
|
-
body: JSON.stringify(body)
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
async deleteDependency(taskId, opts) {
|
|
409
|
-
const params = new URLSearchParams();
|
|
410
|
-
if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
|
|
411
|
-
if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
|
|
412
|
-
await this.request(this.taskPath(taskId, `/dependency?${params.toString()}`), {
|
|
413
|
-
method: "DELETE"
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
async updateComment(commentId, text, resolved) {
|
|
417
|
-
const body = { comment_text: text };
|
|
418
|
-
if (resolved !== void 0) body.resolved = resolved;
|
|
419
|
-
await this.request(`/comment/${commentId}`, {
|
|
420
|
-
method: "PUT",
|
|
421
|
-
body: JSON.stringify(body)
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
async deleteComment(commentId) {
|
|
425
|
-
await this.request(`/comment/${commentId}`, { method: "DELETE" });
|
|
426
|
-
}
|
|
427
|
-
async getThreadedComments(commentId) {
|
|
428
|
-
const data = await this.request(`/comment/${commentId}/reply`);
|
|
429
|
-
return readCollectionField(
|
|
430
|
-
data,
|
|
431
|
-
"comments",
|
|
432
|
-
"threaded comments"
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
async createThreadedComment(commentId, text, notifyAll) {
|
|
436
|
-
const body = { comment_text: text };
|
|
437
|
-
if (notifyAll) body.notify_all = true;
|
|
438
|
-
await this.request(`/comment/${commentId}/reply`, {
|
|
439
|
-
method: "POST",
|
|
440
|
-
body: JSON.stringify(body)
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
async addTaskLink(taskId, linksTo) {
|
|
444
|
-
await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
|
|
445
|
-
method: "POST"
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
async deleteTaskLink(taskId, linksTo) {
|
|
449
|
-
await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
|
|
450
|
-
method: "DELETE"
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
async getListCustomFields(listId) {
|
|
454
|
-
const data = await this.request(`/list/${listId}/field`);
|
|
455
|
-
return readCollectionField(
|
|
456
|
-
data,
|
|
457
|
-
"fields",
|
|
458
|
-
"list custom fields"
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
async createChecklist(taskId, name) {
|
|
462
|
-
const data = await this.request(this.taskPath(taskId, "/checklist"), {
|
|
463
|
-
method: "POST",
|
|
464
|
-
body: JSON.stringify({ name })
|
|
465
|
-
});
|
|
466
|
-
return expectRecordField(
|
|
467
|
-
data,
|
|
468
|
-
"checklist",
|
|
469
|
-
"checklist"
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
async deleteChecklist(checklistId) {
|
|
473
|
-
await this.request(`/checklist/${checklistId}`, { method: "DELETE" });
|
|
474
|
-
}
|
|
475
|
-
async createChecklistItem(checklistId, name) {
|
|
476
|
-
const data = await this.request(
|
|
477
|
-
`/checklist/${checklistId}/checklist_item`,
|
|
478
|
-
{ method: "POST", body: JSON.stringify({ name }) }
|
|
479
|
-
);
|
|
480
|
-
return expectRecordField(
|
|
481
|
-
data,
|
|
482
|
-
"checklist",
|
|
483
|
-
"checklist"
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
async editChecklistItem(checklistId, checklistItemId, updates) {
|
|
487
|
-
const data = await this.request(
|
|
488
|
-
`/checklist/${checklistId}/checklist_item/${checklistItemId}`,
|
|
489
|
-
{ method: "PUT", body: JSON.stringify(updates) }
|
|
490
|
-
);
|
|
491
|
-
return expectRecordField(
|
|
492
|
-
data,
|
|
493
|
-
"checklist",
|
|
494
|
-
"checklist"
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
async deleteChecklistItem(checklistId, checklistItemId) {
|
|
498
|
-
await this.request(
|
|
499
|
-
`/checklist/${checklistId}/checklist_item/${checklistItemId}`,
|
|
500
|
-
{ method: "DELETE" }
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
async startTimeEntry(teamId, taskId, description) {
|
|
504
|
-
const body = {
|
|
505
|
-
tid: taskId,
|
|
506
|
-
start: Date.now(),
|
|
507
|
-
duration: -1
|
|
508
|
-
};
|
|
509
|
-
if (description) body.description = description;
|
|
510
|
-
const data = await this.request(
|
|
511
|
-
`/team/${teamId}/time_entries/start${this.customIdQueryParams(taskId)}`,
|
|
512
|
-
{
|
|
513
|
-
method: "POST",
|
|
514
|
-
body: JSON.stringify(body)
|
|
515
|
-
}
|
|
516
|
-
);
|
|
517
|
-
return data.data;
|
|
518
|
-
}
|
|
519
|
-
async stopTimeEntry(teamId) {
|
|
520
|
-
const data = await this.request(`/team/${teamId}/time_entries/stop`, {
|
|
521
|
-
method: "POST"
|
|
522
|
-
});
|
|
523
|
-
return data.data;
|
|
524
|
-
}
|
|
525
|
-
async getRunningTimeEntry(teamId) {
|
|
526
|
-
const data = await this.request(
|
|
527
|
-
`/team/${teamId}/time_entries/current`
|
|
528
|
-
);
|
|
529
|
-
return data.data ?? null;
|
|
530
|
-
}
|
|
531
|
-
async createTimeEntry(teamId, taskId, duration, opts) {
|
|
532
|
-
const start = opts?.start ?? Date.now() - duration;
|
|
533
|
-
const body = {
|
|
534
|
-
tid: taskId,
|
|
535
|
-
start,
|
|
536
|
-
duration
|
|
537
|
-
};
|
|
538
|
-
if (opts?.description) body.description = opts.description;
|
|
539
|
-
const data = await this.request(
|
|
540
|
-
`/team/${teamId}/time_entries${this.customIdQueryParams(taskId)}`,
|
|
541
|
-
{
|
|
542
|
-
method: "POST",
|
|
543
|
-
body: JSON.stringify(body)
|
|
544
|
-
}
|
|
545
|
-
);
|
|
546
|
-
return data.data;
|
|
547
|
-
}
|
|
548
|
-
async getTimeEntries(teamId, opts) {
|
|
549
|
-
const params = new URLSearchParams();
|
|
550
|
-
if (opts?.startDate != null) params.set("start_date", String(opts.startDate));
|
|
551
|
-
if (opts?.endDate != null) params.set("end_date", String(opts.endDate));
|
|
552
|
-
if (opts?.spaceId) params.set("space_id", opts.spaceId);
|
|
553
|
-
if (opts?.listId) params.set("list_id", opts.listId);
|
|
554
|
-
if (opts?.assigneeId) params.set("assignee", opts.assigneeId);
|
|
555
|
-
const query = params.toString();
|
|
556
|
-
const url = `/team/${teamId}/time_entries${query ? `?${query}` : ""}`;
|
|
557
|
-
const data = await this.request(url);
|
|
558
|
-
const entries = readCollectionField(
|
|
559
|
-
data,
|
|
560
|
-
"data",
|
|
561
|
-
"time entries"
|
|
562
|
-
);
|
|
563
|
-
if (opts?.taskId) {
|
|
564
|
-
return entries.filter((e) => e.task?.id === opts.taskId);
|
|
565
|
-
}
|
|
566
|
-
return entries;
|
|
567
|
-
}
|
|
568
|
-
async updateTimeEntry(teamId, timeEntryId, updates) {
|
|
569
|
-
const data = await this.request(
|
|
570
|
-
`/team/${teamId}/time_entries/${timeEntryId}`,
|
|
571
|
-
{ method: "PUT", body: JSON.stringify(updates) }
|
|
572
|
-
);
|
|
573
|
-
return data.data;
|
|
574
|
-
}
|
|
575
|
-
async getSpaceTags(spaceId) {
|
|
576
|
-
const data = await this.request(`/space/${spaceId}/tag`);
|
|
577
|
-
return readCollectionField(data, "tags", "space tags");
|
|
578
|
-
}
|
|
579
|
-
async createSpaceTag(spaceId, name, fg, bg) {
|
|
580
|
-
await this.request(`/space/${spaceId}/tag`, {
|
|
581
|
-
method: "POST",
|
|
582
|
-
body: JSON.stringify({
|
|
583
|
-
tag: { name, tag_fg: fg ?? "#000000", tag_bg: bg ?? "#04A9F4" }
|
|
584
|
-
})
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
async deleteSpaceTag(spaceId, tagName) {
|
|
588
|
-
await this.request(
|
|
589
|
-
`/space/${spaceId}/tag/${encodeURIComponent(tagName)}`,
|
|
590
|
-
{ method: "DELETE" }
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
async getWorkspaceMembers(teamId) {
|
|
594
|
-
const data = await this.request("/team");
|
|
595
|
-
const team = readCollectionField(
|
|
596
|
-
data,
|
|
597
|
-
"teams",
|
|
598
|
-
"workspace members"
|
|
599
|
-
).find((t) => t.id === teamId);
|
|
600
|
-
return team?.members?.map((m) => m.user) ?? [];
|
|
601
|
-
}
|
|
602
|
-
async deleteTimeEntry(teamId, timeEntryId) {
|
|
603
|
-
await this.request(`/team/${teamId}/time_entries/${timeEntryId}`, {
|
|
604
|
-
method: "DELETE"
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
async createTaskAttachment(taskId, filePath) {
|
|
608
|
-
const { readFile } = await import("fs/promises");
|
|
609
|
-
const { basename: basename2 } = await import("path");
|
|
610
|
-
const fileBuffer = await readFile(filePath);
|
|
611
|
-
const fileName = basename2(filePath);
|
|
612
|
-
const formData = new FormData();
|
|
613
|
-
formData.append("attachment", new Blob([fileBuffer]), fileName);
|
|
614
|
-
const res = await fetch(`${BASE_URL}${this.taskPath(taskId, "/attachment")}`, {
|
|
615
|
-
method: "POST",
|
|
616
|
-
headers: { Authorization: this.apiToken },
|
|
617
|
-
body: formData,
|
|
618
|
-
signal: AbortSignal.timeout(6e4)
|
|
619
|
-
});
|
|
620
|
-
if (!res.ok) {
|
|
621
|
-
let msg;
|
|
622
|
-
try {
|
|
623
|
-
const data2 = await res.json();
|
|
624
|
-
msg = data2.err ?? `HTTP ${res.status}`;
|
|
625
|
-
} catch {
|
|
626
|
-
msg = `HTTP ${res.status}`;
|
|
627
|
-
}
|
|
628
|
-
throw new Error(`ClickUp API error ${res.status}: ${msg}`);
|
|
629
|
-
}
|
|
630
|
-
let data;
|
|
631
|
-
try {
|
|
632
|
-
data = await res.json();
|
|
633
|
-
} catch {
|
|
634
|
-
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
635
|
-
}
|
|
636
|
-
return data;
|
|
637
|
-
}
|
|
638
|
-
async getDocs(workspaceId) {
|
|
639
|
-
const data = await this.requestV3(`/workspaces/${workspaceId}/docs`);
|
|
640
|
-
return readCollectionField(data, "docs", "docs");
|
|
641
|
-
}
|
|
642
|
-
async getDocPage(workspaceId, docId, pageId) {
|
|
643
|
-
return this.requestV3(
|
|
644
|
-
`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}?content_format=text/md`
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
async createDoc(workspaceId, title, content, parentId) {
|
|
648
|
-
const body = { title };
|
|
649
|
-
if (content) body.content = content;
|
|
650
|
-
if (parentId) {
|
|
651
|
-
body.parent_id = parentId;
|
|
652
|
-
body.parent_type = "doc";
|
|
653
|
-
}
|
|
654
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs`, {
|
|
655
|
-
method: "POST",
|
|
656
|
-
body: JSON.stringify(body)
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
async createDocPage(workspaceId, docId, name, content, parentPageId) {
|
|
660
|
-
const body = { name, content_format: "text/md" };
|
|
661
|
-
if (content) body.content = content;
|
|
662
|
-
if (parentPageId) body.parent_page_id = parentPageId;
|
|
663
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages`, {
|
|
664
|
-
method: "POST",
|
|
665
|
-
body: JSON.stringify(body)
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
async editDocPage(workspaceId, docId, pageId, updates) {
|
|
669
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`, {
|
|
670
|
-
method: "PUT",
|
|
671
|
-
body: JSON.stringify(updates)
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
async getDoc(workspaceId, docId) {
|
|
675
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`);
|
|
676
|
-
}
|
|
677
|
-
async getDocPageListing(workspaceId, docId) {
|
|
678
|
-
const data = await this.requestV3(
|
|
679
|
-
`/workspaces/${workspaceId}/docs/${docId}/pagelisting`
|
|
680
|
-
);
|
|
681
|
-
return readCollectionField(
|
|
682
|
-
data,
|
|
683
|
-
"pages",
|
|
684
|
-
"doc page listing"
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
async getDocPages(workspaceId, docId) {
|
|
688
|
-
const data = await this.requestV3(
|
|
689
|
-
`/workspaces/${workspaceId}/docs/${docId}/pages?content_format=text/md`
|
|
690
|
-
);
|
|
691
|
-
return readCollectionField(data, "pages", "doc pages");
|
|
692
|
-
}
|
|
693
|
-
async getGoals(teamId) {
|
|
694
|
-
const data = await this.request(`/team/${teamId}/goal`);
|
|
695
|
-
return readCollectionField(data, "goals", "goals");
|
|
696
|
-
}
|
|
697
|
-
async createGoal(teamId, name, opts) {
|
|
698
|
-
const body = { name, multiple_owners: true };
|
|
699
|
-
if (opts?.description) body.description = opts.description;
|
|
700
|
-
if (opts?.dueDate != null) body.due_date = opts.dueDate;
|
|
701
|
-
if (opts?.color) body.color = opts.color;
|
|
702
|
-
const data = await this.request(`/team/${teamId}/goal`, {
|
|
703
|
-
method: "POST",
|
|
704
|
-
body: JSON.stringify(body)
|
|
705
|
-
});
|
|
706
|
-
return data.goal;
|
|
707
|
-
}
|
|
708
|
-
async updateGoal(goalId, updates) {
|
|
709
|
-
const data = await this.request(`/goal/${goalId}`, {
|
|
710
|
-
method: "PUT",
|
|
711
|
-
body: JSON.stringify(updates)
|
|
712
|
-
});
|
|
713
|
-
return data.goal;
|
|
714
|
-
}
|
|
715
|
-
async getKeyResults(goalId) {
|
|
716
|
-
const data = await this.request(`/goal/${goalId}`);
|
|
717
|
-
return data.goal?.key_results ?? [];
|
|
718
|
-
}
|
|
719
|
-
async createKeyResult(goalId, name, type, stepsEnd) {
|
|
720
|
-
const data = await this.request(`/goal/${goalId}/key_result`, {
|
|
721
|
-
method: "POST",
|
|
722
|
-
body: JSON.stringify({
|
|
723
|
-
name,
|
|
724
|
-
type,
|
|
725
|
-
steps_start: 0,
|
|
726
|
-
steps_end: stepsEnd,
|
|
727
|
-
unit: type === "number" ? "items" : "%"
|
|
728
|
-
})
|
|
729
|
-
});
|
|
730
|
-
return data.key_result;
|
|
731
|
-
}
|
|
732
|
-
async updateKeyResult(keyResultId, updates) {
|
|
733
|
-
const data = await this.request(`/key_result/${keyResultId}`, {
|
|
734
|
-
method: "PUT",
|
|
735
|
-
body: JSON.stringify(updates)
|
|
736
|
-
});
|
|
737
|
-
return data.key_result;
|
|
738
|
-
}
|
|
739
|
-
async deleteGoal(goalId) {
|
|
740
|
-
await this.request(`/goal/${goalId}`, { method: "DELETE" });
|
|
741
|
-
}
|
|
742
|
-
async deleteKeyResult(keyResultId) {
|
|
743
|
-
await this.request(`/key_result/${keyResultId}`, { method: "DELETE" });
|
|
744
|
-
}
|
|
745
|
-
async deleteDoc(workspaceId, docId) {
|
|
746
|
-
await this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`, {
|
|
747
|
-
method: "DELETE"
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
async deleteDocPage(workspaceId, docId, pageId) {
|
|
751
|
-
await this.requestV3(
|
|
752
|
-
`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`,
|
|
753
|
-
{ method: "DELETE" }
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
async updateSpaceTag(spaceId, tagName, updates) {
|
|
757
|
-
await this.request(
|
|
758
|
-
`/space/${spaceId}/tag/${encodeURIComponent(tagName)}`,
|
|
759
|
-
{
|
|
760
|
-
method: "PUT",
|
|
761
|
-
body: JSON.stringify({
|
|
762
|
-
tag: {
|
|
763
|
-
name: updates.name,
|
|
764
|
-
tag_fg: updates.tag_fg ?? "#000000",
|
|
765
|
-
tag_bg: updates.tag_bg ?? "#04A9F4"
|
|
766
|
-
}
|
|
767
|
-
})
|
|
768
|
-
}
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
async getTaskTemplates(teamId) {
|
|
772
|
-
const data = await this.request(
|
|
773
|
-
`/team/${teamId}/taskTemplate?page=0`
|
|
774
|
-
);
|
|
775
|
-
return readCollectionField(
|
|
776
|
-
data,
|
|
777
|
-
"templates",
|
|
778
|
-
"task templates"
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
async createTaskFromTemplate(listId, templateId, name) {
|
|
782
|
-
return this.request(`/list/${listId}/taskTemplate/${templateId}`, {
|
|
783
|
-
method: "POST",
|
|
784
|
-
body: JSON.stringify({ name })
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
async createCustomField(teamId, name, type, opts) {
|
|
788
|
-
const typeConfig = {};
|
|
789
|
-
if (opts?.options?.length) {
|
|
790
|
-
typeConfig.options = opts.options.map((optName, i) => ({
|
|
791
|
-
name: optName,
|
|
792
|
-
orderindex: i
|
|
793
|
-
}));
|
|
794
|
-
}
|
|
795
|
-
const body = {
|
|
796
|
-
name,
|
|
797
|
-
type,
|
|
798
|
-
type_config: typeConfig,
|
|
799
|
-
description: opts?.description ?? "",
|
|
800
|
-
required: opts?.required ?? false,
|
|
801
|
-
pinned: false,
|
|
802
|
-
hide_from_guests: false,
|
|
803
|
-
required_on_subtasks: false,
|
|
804
|
-
private: false,
|
|
805
|
-
permission_level: null,
|
|
806
|
-
members: [],
|
|
807
|
-
groups: []
|
|
808
|
-
};
|
|
809
|
-
const data = await this.request(
|
|
810
|
-
`/field?workspace_id=${teamId}`,
|
|
811
|
-
{ method: "POST", body: JSON.stringify(body) }
|
|
812
|
-
);
|
|
813
|
-
return data.data;
|
|
814
|
-
}
|
|
815
|
-
};
|
|
816
|
-
|
|
817
|
-
// src/config.ts
|
|
818
|
-
import fs from "fs";
|
|
819
|
-
import { homedir } from "os";
|
|
820
|
-
import { join } from "path";
|
|
821
|
-
function isRecord2(value) {
|
|
822
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
823
|
-
}
|
|
824
|
-
function readConfigString(parsed, key, path, strict) {
|
|
825
|
-
const value = parsed[key];
|
|
826
|
-
if (value === void 0) return void 0;
|
|
827
|
-
if (typeof value !== "string") {
|
|
828
|
-
if (strict) {
|
|
829
|
-
throw new Error(`Config field ${key} must be a string in ${path}.`);
|
|
830
|
-
}
|
|
831
|
-
return void 0;
|
|
832
|
-
}
|
|
833
|
-
const trimmed = value.trim();
|
|
834
|
-
return trimmed || void 0;
|
|
835
|
-
}
|
|
836
|
-
function parseConfigFile(raw, path, strictFields, strictRoot = strictFields) {
|
|
837
|
-
let parsed;
|
|
838
|
-
try {
|
|
839
|
-
parsed = JSON.parse(raw);
|
|
840
|
-
} catch {
|
|
841
|
-
if (strictRoot) {
|
|
842
|
-
throw new Error(`Config file at ${path} contains invalid JSON. Please check the file syntax.`);
|
|
843
|
-
}
|
|
844
|
-
return {};
|
|
845
|
-
}
|
|
846
|
-
if (!isRecord2(parsed)) {
|
|
847
|
-
if (strictRoot) {
|
|
848
|
-
throw new Error(`Config file at ${path} must contain a JSON object.`);
|
|
849
|
-
}
|
|
850
|
-
return {};
|
|
851
|
-
}
|
|
852
|
-
const apiToken = readConfigString(parsed, "apiToken", path, strictFields);
|
|
853
|
-
const teamId = readConfigString(parsed, "teamId", path, strictFields);
|
|
854
|
-
const sprintFolderId = readConfigString(parsed, "sprintFolderId", path, strictFields);
|
|
855
|
-
return {
|
|
856
|
-
...apiToken ? { apiToken } : {},
|
|
857
|
-
...teamId ? { teamId } : {},
|
|
858
|
-
...sprintFolderId ? { sprintFolderId } : {}
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
function trimConfigValue(value) {
|
|
862
|
-
const trimmed = value?.trim();
|
|
863
|
-
return trimmed || void 0;
|
|
864
|
-
}
|
|
865
|
-
function configDir() {
|
|
866
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
867
|
-
if (xdg) return join(xdg, "cup");
|
|
868
|
-
return join(homedir(), ".config", "cup");
|
|
869
|
-
}
|
|
870
|
-
function legacyConfigDir() {
|
|
871
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
872
|
-
if (xdg) return join(xdg, "cu");
|
|
873
|
-
return join(homedir(), ".config", "cu");
|
|
874
|
-
}
|
|
875
|
-
var migrationChecked = false;
|
|
876
|
-
function migrateFromLegacy() {
|
|
877
|
-
if (migrationChecked) return;
|
|
878
|
-
migrationChecked = true;
|
|
879
|
-
const legacy = legacyConfigDir();
|
|
880
|
-
const current = configDir();
|
|
881
|
-
if (fs.existsSync(join(legacy, "config.json")) && !fs.existsSync(join(current, "config.json"))) {
|
|
882
|
-
fs.mkdirSync(current, { recursive: true, mode: 448 });
|
|
883
|
-
fs.copyFileSync(join(legacy, "config.json"), join(current, "config.json"));
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
function configPath() {
|
|
887
|
-
return join(configDir(), "config.json");
|
|
888
|
-
}
|
|
889
|
-
function migrateToMultiProfile(parsed, filePath) {
|
|
890
|
-
if (typeof parsed.apiToken === "string" && !parsed.profiles) {
|
|
891
|
-
const profile = {};
|
|
892
|
-
const token = trimConfigValue(parsed.apiToken);
|
|
893
|
-
if (token) profile.apiToken = token;
|
|
894
|
-
const team = typeof parsed.teamId === "string" ? trimConfigValue(parsed.teamId) : void 0;
|
|
895
|
-
if (team) profile.teamId = team;
|
|
896
|
-
const sprint = typeof parsed.sprintFolderId === "string" ? trimConfigValue(parsed.sprintFolderId) : void 0;
|
|
897
|
-
if (sprint) profile.sprintFolderId = sprint;
|
|
898
|
-
const migrated = {
|
|
899
|
-
defaultProfile: "default",
|
|
900
|
-
profiles: { default: profile }
|
|
901
|
-
};
|
|
902
|
-
const dir = configDir();
|
|
903
|
-
if (!fs.existsSync(dir)) {
|
|
904
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
905
|
-
}
|
|
906
|
-
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(migrated, null, 2) + "\n", {
|
|
907
|
-
encoding: "utf-8",
|
|
908
|
-
mode: 384
|
|
909
|
-
});
|
|
910
|
-
return migrated;
|
|
911
|
-
}
|
|
912
|
-
if (isRecord2(parsed.profiles)) {
|
|
913
|
-
const profiles = {};
|
|
914
|
-
for (const [name, value] of Object.entries(parsed.profiles)) {
|
|
915
|
-
if (isRecord2(value)) {
|
|
916
|
-
const p = {};
|
|
917
|
-
if (typeof value.apiToken === "string" && value.apiToken.trim())
|
|
918
|
-
p.apiToken = value.apiToken.trim();
|
|
919
|
-
if (typeof value.teamId === "string" && value.teamId.trim()) p.teamId = value.teamId.trim();
|
|
920
|
-
if (typeof value.sprintFolderId === "string" && value.sprintFolderId.trim())
|
|
921
|
-
p.sprintFolderId = value.sprintFolderId.trim();
|
|
922
|
-
if (isRecord2(value.filters)) p.filters = value.filters;
|
|
923
|
-
profiles[name] = p;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return {
|
|
927
|
-
defaultProfile: typeof parsed.defaultProfile === "string" ? parsed.defaultProfile : "",
|
|
928
|
-
profiles
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
throw new Error(`Config file at ${filePath} has unrecognized format.`);
|
|
932
|
-
}
|
|
933
|
-
function parseRawConfig(filePath) {
|
|
934
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
935
|
-
let parsed;
|
|
936
|
-
try {
|
|
937
|
-
parsed = JSON.parse(raw);
|
|
938
|
-
} catch {
|
|
939
|
-
throw new Error(
|
|
940
|
-
`Config file at ${filePath} contains invalid JSON. Please check the file syntax.`
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
if (!isRecord2(parsed)) {
|
|
944
|
-
throw new Error(`Config file at ${filePath} must contain a JSON object.`);
|
|
945
|
-
}
|
|
946
|
-
return { parsed, raw };
|
|
947
|
-
}
|
|
948
|
-
function isOldFormat(parsed) {
|
|
949
|
-
return typeof parsed.apiToken === "string" && !parsed.profiles;
|
|
950
|
-
}
|
|
951
|
-
function loadConfig(profileName) {
|
|
952
|
-
migrateFromLegacy();
|
|
953
|
-
const envToken = process.env.CU_API_TOKEN?.trim();
|
|
954
|
-
const envTeamId = process.env.CU_TEAM_ID?.trim();
|
|
955
|
-
if (envToken && envTeamId) {
|
|
956
|
-
if (!envToken.startsWith("pk_")) {
|
|
957
|
-
throw new Error("CU_API_TOKEN must start with pk_.");
|
|
958
|
-
}
|
|
959
|
-
return { apiToken: envToken, teamId: envTeamId };
|
|
960
|
-
}
|
|
961
|
-
const path = configPath();
|
|
962
|
-
if (!fs.existsSync(path)) {
|
|
963
|
-
if (envToken || envTeamId) {
|
|
964
|
-
throw new Error("Both CU_API_TOKEN and CU_TEAM_ID must be set, or run: cup init");
|
|
965
|
-
}
|
|
966
|
-
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
|
|
967
|
-
}
|
|
968
|
-
const { parsed } = parseRawConfig(path);
|
|
969
|
-
if (isOldFormat(parsed)) {
|
|
970
|
-
const fileConfig = parseConfigFile(JSON.stringify(parsed), path, true);
|
|
971
|
-
const apiToken2 = envToken ?? fileConfig.apiToken;
|
|
972
|
-
if (!apiToken2) {
|
|
973
|
-
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
|
|
974
|
-
}
|
|
975
|
-
if (!apiToken2.startsWith("pk_")) {
|
|
976
|
-
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
977
|
-
}
|
|
978
|
-
const teamId2 = envTeamId ?? fileConfig.teamId;
|
|
979
|
-
if (!teamId2) {
|
|
980
|
-
throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cup init");
|
|
981
|
-
}
|
|
982
|
-
migrateToMultiProfile(parsed, path);
|
|
983
|
-
return {
|
|
984
|
-
apiToken: apiToken2,
|
|
985
|
-
teamId: teamId2,
|
|
986
|
-
...fileConfig.sprintFolderId ? { sprintFolderId: fileConfig.sprintFolderId } : {}
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
const multi = loadMultiProfileConfig();
|
|
990
|
-
const resolvedProfile = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
991
|
-
if (!resolvedProfile) {
|
|
992
|
-
throw new Error("No default profile set. Run: cup profile use <name>");
|
|
993
|
-
}
|
|
994
|
-
const profile = multi.profiles[resolvedProfile];
|
|
995
|
-
if (!profile) {
|
|
996
|
-
const available = Object.keys(multi.profiles).join(", ");
|
|
997
|
-
throw new Error(`Profile "${resolvedProfile}" not found. Available: ${available}`);
|
|
998
|
-
}
|
|
999
|
-
const apiToken = envToken ?? profile.apiToken?.trim();
|
|
1000
|
-
if (!apiToken) {
|
|
1001
|
-
throw new Error(
|
|
1002
|
-
`Profile "${resolvedProfile}" missing apiToken. Run: cup profile add ${resolvedProfile}`
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
if (!apiToken.startsWith("pk_")) {
|
|
1006
|
-
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
1007
|
-
}
|
|
1008
|
-
const teamId = envTeamId ?? profile.teamId?.trim();
|
|
1009
|
-
if (!teamId) {
|
|
1010
|
-
throw new Error(`Profile "${resolvedProfile}" missing teamId.`);
|
|
1011
|
-
}
|
|
1012
|
-
return {
|
|
1013
|
-
apiToken,
|
|
1014
|
-
teamId,
|
|
1015
|
-
...profile.sprintFolderId ? { sprintFolderId: profile.sprintFolderId } : {}
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
function loadMultiProfileConfig() {
|
|
1019
|
-
migrateFromLegacy();
|
|
1020
|
-
const path = configPath();
|
|
1021
|
-
if (!fs.existsSync(path)) {
|
|
1022
|
-
return { defaultProfile: "", profiles: {} };
|
|
1023
|
-
}
|
|
1024
|
-
let parsed;
|
|
1025
|
-
try {
|
|
1026
|
-
const raw = fs.readFileSync(path, "utf-8");
|
|
1027
|
-
parsed = JSON.parse(raw);
|
|
1028
|
-
} catch {
|
|
1029
|
-
return { defaultProfile: "", profiles: {} };
|
|
1030
|
-
}
|
|
1031
|
-
if (!isRecord2(parsed)) return { defaultProfile: "", profiles: {} };
|
|
1032
|
-
if (isOldFormat(parsed)) {
|
|
1033
|
-
return migrateToMultiProfile(parsed, path);
|
|
1034
|
-
}
|
|
1035
|
-
return migrateToMultiProfile(parsed, path);
|
|
1036
|
-
}
|
|
1037
|
-
function saveMultiProfileConfig(config) {
|
|
1038
|
-
const dir = configDir();
|
|
1039
|
-
if (!fs.existsSync(dir)) {
|
|
1040
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1041
|
-
}
|
|
1042
|
-
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", {
|
|
1043
|
-
encoding: "utf-8",
|
|
1044
|
-
mode: 384
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
function addProfile(name, profile) {
|
|
1048
|
-
const multi = loadMultiProfileConfig();
|
|
1049
|
-
multi.profiles[name] = profile;
|
|
1050
|
-
if (!multi.defaultProfile) multi.defaultProfile = name;
|
|
1051
|
-
saveMultiProfileConfig(multi);
|
|
1052
|
-
}
|
|
1053
|
-
function removeProfile(name) {
|
|
1054
|
-
const multi = loadMultiProfileConfig();
|
|
1055
|
-
if (!multi.profiles[name]) {
|
|
1056
|
-
throw new Error(`Profile "${name}" not found.`);
|
|
1057
|
-
}
|
|
1058
|
-
const keys = Object.keys(multi.profiles);
|
|
1059
|
-
if (keys.length <= 1) {
|
|
1060
|
-
throw new Error("Cannot remove the last profile.");
|
|
1061
|
-
}
|
|
1062
|
-
delete multi.profiles[name];
|
|
1063
|
-
if (multi.defaultProfile === name) {
|
|
1064
|
-
multi.defaultProfile = Object.keys(multi.profiles)[0] ?? "";
|
|
1065
|
-
}
|
|
1066
|
-
saveMultiProfileConfig(multi);
|
|
1067
|
-
}
|
|
1068
|
-
function setDefaultProfile(name) {
|
|
1069
|
-
const multi = loadMultiProfileConfig();
|
|
1070
|
-
if (!multi.profiles[name]) {
|
|
1071
|
-
const available = Object.keys(multi.profiles).join(", ");
|
|
1072
|
-
throw new Error(`Profile "${name}" not found. Available: ${available}`);
|
|
1073
|
-
}
|
|
1074
|
-
multi.defaultProfile = name;
|
|
1075
|
-
saveMultiProfileConfig(multi);
|
|
1076
|
-
}
|
|
1077
|
-
function listProfiles() {
|
|
1078
|
-
const multi = loadMultiProfileConfig();
|
|
1079
|
-
return Object.entries(multi.profiles).map(([name, profile]) => ({
|
|
1080
|
-
name,
|
|
1081
|
-
isDefault: name === multi.defaultProfile,
|
|
1082
|
-
teamId: profile.teamId
|
|
1083
|
-
}));
|
|
1084
|
-
}
|
|
1085
|
-
function loadRawConfig(profileName) {
|
|
1086
|
-
migrateFromLegacy();
|
|
1087
|
-
const path = configPath();
|
|
1088
|
-
if (!fs.existsSync(path)) return {};
|
|
1089
|
-
let parsed;
|
|
1090
|
-
try {
|
|
1091
|
-
const raw = fs.readFileSync(path, "utf-8");
|
|
1092
|
-
parsed = JSON.parse(raw);
|
|
1093
|
-
} catch {
|
|
1094
|
-
return {};
|
|
1095
|
-
}
|
|
1096
|
-
if (!isRecord2(parsed)) {
|
|
1097
|
-
throw new Error(`Config file at ${path} must contain a JSON object.`);
|
|
1098
|
-
}
|
|
1099
|
-
if (isOldFormat(parsed)) {
|
|
1100
|
-
return parseConfigFile(JSON.stringify(parsed), path, false, true);
|
|
1101
|
-
}
|
|
1102
|
-
const multi = migrateToMultiProfile(parsed, path);
|
|
1103
|
-
const name = profileName || multi.defaultProfile || "default";
|
|
1104
|
-
return multi.profiles[name] ?? {};
|
|
1105
|
-
}
|
|
1106
|
-
function getConfigPath() {
|
|
1107
|
-
migrateFromLegacy();
|
|
1108
|
-
return configPath();
|
|
1109
|
-
}
|
|
1110
|
-
function getFilters(profileName) {
|
|
1111
|
-
const multi = loadMultiProfileConfig();
|
|
1112
|
-
const name = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1113
|
-
const profile = name ? multi.profiles[name] ?? {} : {};
|
|
1114
|
-
return profile.filters ?? {};
|
|
1115
|
-
}
|
|
1116
|
-
function saveFilter(name, entry, profileName) {
|
|
1117
|
-
const multi = loadMultiProfileConfig();
|
|
1118
|
-
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1119
|
-
const profile = multi.profiles[pName] ?? {};
|
|
1120
|
-
const filters = { ...profile.filters ?? {}, [name]: entry };
|
|
1121
|
-
multi.profiles[pName] = { ...profile, filters };
|
|
1122
|
-
if (!multi.defaultProfile) multi.defaultProfile = pName;
|
|
1123
|
-
saveMultiProfileConfig(multi);
|
|
1124
|
-
}
|
|
1125
|
-
function deleteFilter(name, profileName) {
|
|
1126
|
-
const multi = loadMultiProfileConfig();
|
|
1127
|
-
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1128
|
-
const profile = multi.profiles[pName] ?? {};
|
|
1129
|
-
const filters = { ...profile.filters ?? {} };
|
|
1130
|
-
if (!(name in filters)) {
|
|
1131
|
-
throw new Error(`Filter "${name}" not found.`);
|
|
1132
|
-
}
|
|
1133
|
-
delete filters[name];
|
|
1134
|
-
multi.profiles[pName] = { ...profile, filters };
|
|
1135
|
-
saveMultiProfileConfig(multi);
|
|
1136
|
-
}
|
|
1137
|
-
function writeConfig(config, profileName) {
|
|
1138
|
-
const multi = loadMultiProfileConfig();
|
|
1139
|
-
const name = profileName || multi.defaultProfile || "default";
|
|
1140
|
-
const apiToken = trimConfigValue(config.apiToken) ?? void 0;
|
|
1141
|
-
const teamId = trimConfigValue(config.teamId) ?? void 0;
|
|
1142
|
-
const sprintFolderId = trimConfigValue(config.sprintFolderId);
|
|
1143
|
-
const normalizedConfig = {
|
|
1144
|
-
...apiToken ? { apiToken } : {},
|
|
1145
|
-
...teamId ? { teamId } : {},
|
|
1146
|
-
...sprintFolderId ? { sprintFolderId } : {}
|
|
1147
|
-
};
|
|
1148
|
-
multi.profiles[name] = {
|
|
1149
|
-
...multi.profiles[name],
|
|
1150
|
-
...normalizedConfig
|
|
1151
|
-
};
|
|
1152
|
-
if (!multi.defaultProfile) multi.defaultProfile = name;
|
|
1153
|
-
saveMultiProfileConfig(multi);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// src/date.ts
|
|
1157
|
-
function formatDate(ms) {
|
|
1158
|
-
return new Date(Number(ms)).toLocaleDateString("en-US", {
|
|
1159
|
-
month: "short",
|
|
1160
|
-
day: "numeric",
|
|
1161
|
-
year: "numeric"
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
function formatTimestamp(ms) {
|
|
1165
|
-
return new Date(Number(ms)).toLocaleString("en-US", {
|
|
1166
|
-
month: "short",
|
|
1167
|
-
day: "numeric",
|
|
1168
|
-
hour: "numeric",
|
|
1169
|
-
minute: "2-digit"
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
function formatDuration(ms) {
|
|
1173
|
-
const totalMinutes = Math.round(Math.abs(ms) / 6e4);
|
|
1174
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
1175
|
-
const minutes = totalMinutes % 60;
|
|
1176
|
-
if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
|
|
1177
|
-
if (hours > 0) return `${hours}h`;
|
|
1178
|
-
return `${minutes}m`;
|
|
1179
|
-
}
|
|
1180
|
-
function formatDateISO(ms) {
|
|
1181
|
-
const d = new Date(Number(ms));
|
|
1182
|
-
const year = d.getUTCFullYear();
|
|
1183
|
-
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
1184
|
-
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
1185
|
-
return `${year}-${month}-${day}`;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// src/output.ts
|
|
1189
|
-
import chalk from "chalk";
|
|
1190
|
-
function isTTY() {
|
|
1191
|
-
return Boolean(process.stdout.isTTY);
|
|
1192
|
-
}
|
|
1193
|
-
function shouldOutputJson(forceJson) {
|
|
1194
|
-
if (forceJson) return true;
|
|
1195
|
-
if (process.env["CU_OUTPUT"] === "json") return true;
|
|
1196
|
-
return false;
|
|
1197
|
-
}
|
|
1198
|
-
function cell(value, width) {
|
|
1199
|
-
if (value.length > width) return value.slice(0, width - 1) + "\u2026";
|
|
1200
|
-
return value.padEnd(width);
|
|
1201
|
-
}
|
|
1202
|
-
function computeWidths(rows, columns) {
|
|
1203
|
-
return columns.map((col) => {
|
|
1204
|
-
const headerLen = col.label.length;
|
|
1205
|
-
const maxDataLen = rows.reduce((max, row) => {
|
|
1206
|
-
const val = String(row[col.key] ?? "");
|
|
1207
|
-
return Math.max(max, val.length);
|
|
1208
|
-
}, 0);
|
|
1209
|
-
const natural = Math.max(headerLen, maxDataLen);
|
|
1210
|
-
return col.maxWidth ? Math.min(natural, col.maxWidth) : natural;
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
function formatTable(rows, columns) {
|
|
1214
|
-
const widths = computeWidths(rows, columns);
|
|
1215
|
-
const header = columns.map((c, i) => cell(c.label, widths[i])).join(" ");
|
|
1216
|
-
const divider = chalk.dim("-".repeat(header.replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
1217
|
-
const lines = [chalk.bold(header), divider];
|
|
1218
|
-
for (const row of rows) {
|
|
1219
|
-
lines.push(
|
|
1220
|
-
columns.map((c, i) => {
|
|
1221
|
-
const raw = String(row[c.key] ?? "");
|
|
1222
|
-
const width = widths[i];
|
|
1223
|
-
const truncated = raw.length > width ? raw.slice(0, width - 1) + "\u2026" : raw;
|
|
1224
|
-
const padding = " ".repeat(Math.max(0, width - truncated.length));
|
|
1225
|
-
return c.format ? c.format(truncated, row) + padding : truncated + padding;
|
|
1226
|
-
}).join(" ")
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
return lines.join("\n");
|
|
1230
|
-
}
|
|
1231
|
-
function colorStatus(status) {
|
|
1232
|
-
const lower = status.toLowerCase();
|
|
1233
|
-
if (lower.includes("done") || lower.includes("complete") || lower.includes("closed"))
|
|
1234
|
-
return chalk.green(status);
|
|
1235
|
-
if (lower.includes("progress") || lower.includes("review") || lower.includes("active"))
|
|
1236
|
-
return chalk.yellow(status);
|
|
1237
|
-
if (lower.includes("block") || lower.includes("stuck")) return chalk.red(status);
|
|
1238
|
-
return chalk.dim(status);
|
|
1239
|
-
}
|
|
1240
|
-
function colorPriority(priority) {
|
|
1241
|
-
const lower = priority.toLowerCase();
|
|
1242
|
-
if (lower === "urgent") return chalk.red(priority);
|
|
1243
|
-
if (lower === "high") return chalk.yellow(priority);
|
|
1244
|
-
if (lower === "normal") return priority;
|
|
1245
|
-
if (lower === "low") return chalk.dim(priority);
|
|
1246
|
-
return priority;
|
|
1247
|
-
}
|
|
1248
|
-
function colorDueDate(dateStr, rawTimestamp) {
|
|
1249
|
-
if (!dateStr) return dateStr;
|
|
1250
|
-
if (rawTimestamp) {
|
|
1251
|
-
const ts = Number(rawTimestamp);
|
|
1252
|
-
if (Number.isFinite(ts) && ts < Date.now()) return chalk.red(dateStr);
|
|
1253
|
-
}
|
|
1254
|
-
return dateStr;
|
|
1255
|
-
}
|
|
1256
|
-
var TASK_COLUMNS = [
|
|
1257
|
-
{ key: "id", label: "ID" },
|
|
1258
|
-
{ key: "name", label: "NAME", maxWidth: 60 },
|
|
1259
|
-
{ key: "status", label: "STATUS", maxWidth: 20, format: (v) => colorStatus(v) },
|
|
1260
|
-
{ key: "priority", label: "PRIORITY", maxWidth: 10, format: (v) => colorPriority(v) },
|
|
1261
|
-
{
|
|
1262
|
-
key: "due_date",
|
|
1263
|
-
label: "DUE",
|
|
1264
|
-
maxWidth: 15,
|
|
1265
|
-
format: (v, row) => v ? colorDueDate(v, row.dueRaw) : ""
|
|
1266
|
-
},
|
|
1267
|
-
{ key: "list", label: "LIST" }
|
|
1268
|
-
];
|
|
1269
|
-
|
|
1270
|
-
// src/markdown.ts
|
|
1271
|
-
function escapeCell(value) {
|
|
1272
|
-
return value.replace(/\|/g, "\\|");
|
|
1273
|
-
}
|
|
1274
|
-
function formatMarkdownTable(rows, columns) {
|
|
1275
|
-
const header = "| " + columns.map((c) => c.label).join(" | ") + " |";
|
|
1276
|
-
const divider = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1277
|
-
const lines = [header, divider];
|
|
1278
|
-
for (const row of rows) {
|
|
1279
|
-
const cells = columns.map((c) => escapeCell(String(row[c.key] ?? "")));
|
|
1280
|
-
lines.push("| " + cells.join(" | ") + " |");
|
|
1281
|
-
}
|
|
1282
|
-
return lines.join("\n");
|
|
1283
|
-
}
|
|
1284
|
-
var TASK_MD_COLUMNS = [
|
|
1285
|
-
{ key: "id", label: "ID" },
|
|
1286
|
-
{ key: "name", label: "Name" },
|
|
1287
|
-
{ key: "status", label: "Status" },
|
|
1288
|
-
{ key: "priority", label: "Priority" },
|
|
1289
|
-
{ key: "due_date", label: "Due" },
|
|
1290
|
-
{ key: "list", label: "List" }
|
|
1291
|
-
];
|
|
1292
|
-
function formatTasksMarkdown(tasks) {
|
|
1293
|
-
if (tasks.length === 0) return "No tasks found.";
|
|
1294
|
-
return formatMarkdownTable(tasks, TASK_MD_COLUMNS);
|
|
1295
|
-
}
|
|
1296
|
-
function formatCommentsMarkdown(comments) {
|
|
1297
|
-
if (comments.length === 0) return "No comments found.";
|
|
1298
|
-
return comments.map((c) => `**${c.user}** (${formatDateISO(c.date)})
|
|
1299
|
-
|
|
1300
|
-
${c.text}`).join("\n\n---\n\n");
|
|
1301
|
-
}
|
|
1302
|
-
var LIST_MD_COLUMNS = [
|
|
1303
|
-
{ key: "id", label: "ID" },
|
|
1304
|
-
{ key: "name", label: "Name" },
|
|
1305
|
-
{ key: "folder", label: "Folder" }
|
|
1306
|
-
];
|
|
1307
|
-
function formatListsMarkdown(lists) {
|
|
1308
|
-
if (lists.length === 0) return "No lists found.";
|
|
1309
|
-
return formatMarkdownTable(lists, LIST_MD_COLUMNS);
|
|
1310
|
-
}
|
|
1311
|
-
var SPACE_MD_COLUMNS = [
|
|
1312
|
-
{ key: "id", label: "ID" },
|
|
1313
|
-
{ key: "name", label: "Name" }
|
|
1314
|
-
];
|
|
1315
|
-
function formatSpacesMarkdown(spaces) {
|
|
1316
|
-
if (spaces.length === 0) return "No spaces found.";
|
|
1317
|
-
return formatMarkdownTable(spaces, SPACE_MD_COLUMNS);
|
|
1318
|
-
}
|
|
1319
|
-
function formatGroupedTasksMarkdown(groups) {
|
|
1320
|
-
const sections = groups.filter((g) => g.tasks.length > 0).map((g) => `## ${g.label}
|
|
1321
|
-
|
|
1322
|
-
${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
|
|
1323
|
-
if (sections.length === 0) return "No tasks found.";
|
|
1324
|
-
return sections.join("\n\n");
|
|
1325
|
-
}
|
|
1326
|
-
function formatTaskDetailMarkdown(task) {
|
|
1327
|
-
const lines = [`# ${task.name}`, ""];
|
|
1328
|
-
const isInitiative = (task.custom_item_id ?? 0) !== 0;
|
|
1329
|
-
const fields = [
|
|
1330
|
-
["ID", task.id],
|
|
1331
|
-
["Status", task.status.status],
|
|
1332
|
-
["Type", isInitiative ? "initiative" : "task"],
|
|
1333
|
-
["List", task.list.name],
|
|
1334
|
-
["URL", task.url],
|
|
1335
|
-
[
|
|
1336
|
-
"Assignees",
|
|
1337
|
-
task.assignees.length > 0 ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
1338
|
-
],
|
|
1339
|
-
["Priority", task.priority?.priority],
|
|
1340
|
-
["Parent", task.parent ?? void 0],
|
|
1341
|
-
["Start Date", task.start_date ? formatDateISO(task.start_date) : void 0],
|
|
1342
|
-
["Due Date", task.due_date ? formatDateISO(task.due_date) : void 0],
|
|
1343
|
-
[
|
|
1344
|
-
"Time Estimate",
|
|
1345
|
-
task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
|
|
1346
|
-
],
|
|
1347
|
-
[
|
|
1348
|
-
"Time Spent",
|
|
1349
|
-
task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
|
|
1350
|
-
],
|
|
1351
|
-
["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
1352
|
-
[
|
|
1353
|
-
"Lists",
|
|
1354
|
-
task.locations && task.locations.length > 0 ? task.locations.map((l) => l.name).join(", ") : void 0
|
|
1355
|
-
],
|
|
1356
|
-
["Created", task.date_created ? formatDateISO(task.date_created) : void 0],
|
|
1357
|
-
["Updated", task.date_updated ? formatDateISO(task.date_updated) : void 0]
|
|
1358
|
-
];
|
|
1359
|
-
for (const [label, value] of fields) {
|
|
1360
|
-
if (value != null && value !== "") {
|
|
1361
|
-
lines.push(`**${label}:** ${value}`);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
const descriptionContent = task.markdown_content ?? task.description;
|
|
1365
|
-
if (descriptionContent) {
|
|
1366
|
-
lines.push("", "## Description", "", descriptionContent);
|
|
1367
|
-
}
|
|
1368
|
-
if (task.checklists?.length) {
|
|
1369
|
-
lines.push("", "## Checklists", "");
|
|
1370
|
-
for (const cl of task.checklists) {
|
|
1371
|
-
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
1372
|
-
lines.push(`### ${cl.name} (${resolved}/${cl.items.length})`, "");
|
|
1373
|
-
for (const item of cl.items) {
|
|
1374
|
-
lines.push(`- [${item.resolved ? "x" : " "}] ${item.name}`);
|
|
1375
|
-
}
|
|
1376
|
-
lines.push("");
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
if (task.attachments?.length) {
|
|
1380
|
-
lines.push("", "## Attachments", "");
|
|
1381
|
-
for (const att of task.attachments) {
|
|
1382
|
-
lines.push(`- [${att.title}](${att.url})`);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
if (task.dependencies?.length) {
|
|
1386
|
-
lines.push("", "## Dependencies", "");
|
|
1387
|
-
for (const dep of task.dependencies) {
|
|
1388
|
-
const direction = dep.depends_on === task.id ? "blocks" : "depends on";
|
|
1389
|
-
const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
|
|
1390
|
-
lines.push(`- ${direction} ${otherId}`);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
if (task.linked_tasks?.length) {
|
|
1394
|
-
lines.push("", "## Linked Tasks", "");
|
|
1395
|
-
for (const lt of task.linked_tasks) {
|
|
1396
|
-
lines.push(`- ${lt.task_id}`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
return lines.join("\n");
|
|
1400
|
-
}
|
|
1401
|
-
function formatUpdateConfirmation(id, name) {
|
|
1402
|
-
return `Updated task ${id}: "${name}"`;
|
|
1403
|
-
}
|
|
1404
|
-
function formatCreateConfirmation(id, name, url) {
|
|
1405
|
-
return `Created task ${id}: "${name}" - ${url}`;
|
|
1406
|
-
}
|
|
1407
|
-
function formatCommentConfirmation(id) {
|
|
1408
|
-
return `Comment posted (id: ${id})`;
|
|
1409
|
-
}
|
|
1410
|
-
function formatAssignConfirmation(taskId, opts) {
|
|
1411
|
-
const parts = [];
|
|
1412
|
-
if (opts.to) parts.push(`Assigned ${opts.to} to ${taskId}`);
|
|
1413
|
-
if (opts.remove) parts.push(`Removed ${opts.remove} from ${taskId}`);
|
|
1414
|
-
return parts.join("; ");
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// src/interactive.ts
|
|
1418
|
-
import { execFileSync } from "child_process";
|
|
1419
|
-
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
1420
|
-
import chalk2 from "chalk";
|
|
1421
|
-
function openUrl(url) {
|
|
1422
|
-
switch (process.platform) {
|
|
1423
|
-
case "darwin":
|
|
1424
|
-
execFileSync("open", [url]);
|
|
1425
|
-
break;
|
|
1426
|
-
case "linux":
|
|
1427
|
-
execFileSync("xdg-open", [url]);
|
|
1428
|
-
break;
|
|
1429
|
-
case "win32":
|
|
1430
|
-
execFileSync("cmd", ["/c", "start", "", url]);
|
|
1431
|
-
break;
|
|
1432
|
-
default:
|
|
1433
|
-
process.stderr.write(`Cannot open browser on ${process.platform}. Visit: ${url}
|
|
1434
|
-
`);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
function descriptionPreview(text, maxLines = 3) {
|
|
1438
|
-
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
1439
|
-
const preview = lines.slice(0, maxLines);
|
|
1440
|
-
const result = preview.map((l) => ` ${chalk2.dim(l.length > 100 ? l.slice(0, 99) + "\u2026" : l)}`).join("\n");
|
|
1441
|
-
if (lines.length > maxLines)
|
|
1442
|
-
return result + `
|
|
1443
|
-
${chalk2.dim(`... (${lines.length - maxLines} more lines)`)}`;
|
|
1444
|
-
return result;
|
|
1445
|
-
}
|
|
1446
|
-
function stringifyFieldValue(value) {
|
|
1447
|
-
if (typeof value === "string") return value;
|
|
1448
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1449
|
-
return JSON.stringify(value);
|
|
1450
|
-
}
|
|
1451
|
-
function formatCustomFieldValue(field) {
|
|
1452
|
-
if (field.value === null || field.value === void 0) return null;
|
|
1453
|
-
const options = field.type_config?.options;
|
|
1454
|
-
switch (field.type) {
|
|
1455
|
-
case "drop_down": {
|
|
1456
|
-
if (!options) return stringifyFieldValue(field.value);
|
|
1457
|
-
const match = options.find((o) => o.id === Number(field.value));
|
|
1458
|
-
return match?.name ?? stringifyFieldValue(field.value);
|
|
1459
|
-
}
|
|
1460
|
-
case "labels": {
|
|
1461
|
-
if (!Array.isArray(field.value) || !options) return stringifyFieldValue(field.value);
|
|
1462
|
-
const names = field.value.map((id) => options.find((o) => o.id === id)?.name).filter((n) => n !== void 0);
|
|
1463
|
-
return names.length > 0 ? names.join(", ") : null;
|
|
1464
|
-
}
|
|
1465
|
-
case "date": {
|
|
1466
|
-
const ts = Number(field.value);
|
|
1467
|
-
if (!Number.isFinite(ts)) return stringifyFieldValue(field.value);
|
|
1468
|
-
return formatDate(String(ts));
|
|
1469
|
-
}
|
|
1470
|
-
case "checkbox":
|
|
1471
|
-
return field.value === true || field.value === "true" ? "Yes" : "No";
|
|
1472
|
-
default:
|
|
1473
|
-
return stringifyFieldValue(field.value);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
function formatTaskDetail(task) {
|
|
1477
|
-
const lines = [];
|
|
1478
|
-
const isInitiative = (task.custom_item_id ?? 0) !== 0;
|
|
1479
|
-
const typeLabel = isInitiative ? "initiative" : "task";
|
|
1480
|
-
lines.push(chalk2.bold.underline(task.name));
|
|
1481
|
-
lines.push("");
|
|
1482
|
-
const fields = [
|
|
1483
|
-
["ID", task.id],
|
|
1484
|
-
["Status", task.status?.status ? colorStatus(task.status.status) : void 0],
|
|
1485
|
-
["Type", typeLabel],
|
|
1486
|
-
["List", task.list?.name],
|
|
1487
|
-
[
|
|
1488
|
-
"Assignees",
|
|
1489
|
-
task.assignees?.length ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
1490
|
-
],
|
|
1491
|
-
["Priority", task.priority?.priority ? colorPriority(task.priority.priority) : void 0],
|
|
1492
|
-
["Start", task.start_date ? formatDate(task.start_date) : void 0],
|
|
1493
|
-
["Due", task.due_date ? colorDueDate(formatDate(task.due_date), task.due_date) : void 0],
|
|
1494
|
-
["Estimate", task.time_estimate ? formatDuration(task.time_estimate) : void 0],
|
|
1495
|
-
["Tracked", task.time_spent ? formatDuration(task.time_spent) : void 0],
|
|
1496
|
-
["Tags", task.tags?.length ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
1497
|
-
["Lists", task.locations?.length ? task.locations.map((l) => l.name).join(", ") : void 0],
|
|
1498
|
-
["Parent", task.parent || void 0],
|
|
1499
|
-
["URL", task.url]
|
|
1500
|
-
];
|
|
1501
|
-
const maxLabel = Math.max(...fields.filter(([, v]) => v).map(([k]) => k.length));
|
|
1502
|
-
for (const [label, value] of fields) {
|
|
1503
|
-
if (!value) continue;
|
|
1504
|
-
lines.push(` ${chalk2.bold(label.padEnd(maxLabel + 1))} ${value}`);
|
|
1505
|
-
}
|
|
1506
|
-
if (task.custom_fields?.length) {
|
|
1507
|
-
const formatted = task.custom_fields.map((f) => [f.name, formatCustomFieldValue(f)]).filter((pair) => pair[1] !== null);
|
|
1508
|
-
if (formatted.length > 0) {
|
|
1509
|
-
lines.push("");
|
|
1510
|
-
lines.push(chalk2.bold("Custom Fields"));
|
|
1511
|
-
for (const [name, value] of formatted) {
|
|
1512
|
-
lines.push(` ${chalk2.bold(name)} ${value}`);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
if (task.checklists?.length) {
|
|
1517
|
-
lines.push("");
|
|
1518
|
-
lines.push(chalk2.bold("Checklists"));
|
|
1519
|
-
for (const cl of task.checklists) {
|
|
1520
|
-
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
1521
|
-
lines.push(` ${chalk2.bold(cl.name)} (${resolved}/${cl.items.length})`);
|
|
1522
|
-
for (const item of cl.items) {
|
|
1523
|
-
const check = item.resolved ? chalk2.green("[x]") : chalk2.dim("[ ]");
|
|
1524
|
-
lines.push(` ${check} ${item.name}`);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
if (task.attachments?.length) {
|
|
1529
|
-
lines.push("");
|
|
1530
|
-
lines.push(chalk2.bold("Attachments"));
|
|
1531
|
-
for (const att of task.attachments) {
|
|
1532
|
-
lines.push(` ${att.title} ${chalk2.dim(att.url)}`);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
if (task.dependencies?.length) {
|
|
1536
|
-
lines.push("");
|
|
1537
|
-
lines.push(chalk2.bold("Dependencies"));
|
|
1538
|
-
for (const dep of task.dependencies) {
|
|
1539
|
-
const direction = dep.depends_on === task.id ? "blocks" : "depends on";
|
|
1540
|
-
const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
|
|
1541
|
-
lines.push(` ${direction} ${chalk2.dim(otherId)}`);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
if (task.linked_tasks?.length) {
|
|
1545
|
-
lines.push("");
|
|
1546
|
-
lines.push(chalk2.bold("Linked Tasks"));
|
|
1547
|
-
for (const lt of task.linked_tasks) {
|
|
1548
|
-
lines.push(` ${chalk2.dim(lt.task_id)}`);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
if (task.text_content?.trim()) {
|
|
1552
|
-
lines.push("");
|
|
1553
|
-
lines.push(descriptionPreview(task.text_content));
|
|
1554
|
-
}
|
|
1555
|
-
return lines.join("\n");
|
|
1556
|
-
}
|
|
1557
|
-
function formatChoiceName(task) {
|
|
1558
|
-
const id = task.id.padEnd(12);
|
|
1559
|
-
const name = task.name.length > 50 ? task.name.slice(0, 49) + "\u2026" : task.name.padEnd(50);
|
|
1560
|
-
const status = colorStatus(task.status);
|
|
1561
|
-
const priority = task.priority !== "none" ? colorPriority(task.priority) : "";
|
|
1562
|
-
return `${id} ${name} ${status}${priority ? " " + priority : ""}`;
|
|
1563
|
-
}
|
|
1564
|
-
async function interactiveTaskPicker(tasks) {
|
|
1565
|
-
if (tasks.length === 0) return [];
|
|
1566
|
-
const selected = await checkbox({
|
|
1567
|
-
message: `${tasks.length} task(s) found. Select to view details / open in browser:`,
|
|
1568
|
-
choices: tasks.map((t) => ({
|
|
1569
|
-
name: formatChoiceName(t),
|
|
1570
|
-
value: t.id
|
|
1571
|
-
})),
|
|
1572
|
-
pageSize: 20
|
|
1573
|
-
});
|
|
1574
|
-
return tasks.filter((t) => selected.includes(t.id));
|
|
1575
|
-
}
|
|
1576
|
-
async function groupedTaskPicker(groups) {
|
|
1577
|
-
const allTasks = groups.flatMap((g) => g.tasks);
|
|
1578
|
-
const totalCount = allTasks.length;
|
|
1579
|
-
if (totalCount === 0) return [];
|
|
1580
|
-
const choices = [];
|
|
1581
|
-
for (const group of groups) {
|
|
1582
|
-
if (group.tasks.length === 0) continue;
|
|
1583
|
-
choices.push(new Separator(chalk2.bold(`${group.label} (${group.tasks.length})`)));
|
|
1584
|
-
for (const task of group.tasks) {
|
|
1585
|
-
choices.push({ name: formatChoiceName(task), value: task.id });
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
const selected = await checkbox({
|
|
1589
|
-
message: `${totalCount} task(s) found. Select to view details / open in browser:`,
|
|
1590
|
-
choices,
|
|
1591
|
-
pageSize: 20
|
|
1592
|
-
});
|
|
1593
|
-
return allTasks.filter((t) => selected.includes(t.id));
|
|
1594
|
-
}
|
|
1595
|
-
async function showDetailsAndOpen(tasks, fetchTask) {
|
|
1596
|
-
if (tasks.length === 0) return;
|
|
1597
|
-
const separator = chalk2.dim("\u2500".repeat(60));
|
|
1598
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
1599
|
-
const task = tasks[i];
|
|
1600
|
-
if (i > 0) {
|
|
1601
|
-
console.log("");
|
|
1602
|
-
console.log(separator);
|
|
1603
|
-
}
|
|
1604
|
-
console.log("");
|
|
1605
|
-
if (fetchTask) {
|
|
1606
|
-
const full = await fetchTask(task.id);
|
|
1607
|
-
console.log(formatTaskDetail(full));
|
|
1608
|
-
} else {
|
|
1609
|
-
const fallback = {
|
|
1610
|
-
id: task.id,
|
|
1611
|
-
name: task.name,
|
|
1612
|
-
status: { status: task.status, color: "" },
|
|
1613
|
-
custom_item_id: task.task_type === "initiative" ? 1 : 0,
|
|
1614
|
-
assignees: [],
|
|
1615
|
-
url: task.url,
|
|
1616
|
-
list: { id: "", name: task.list },
|
|
1617
|
-
parent: task.parent
|
|
1618
|
-
};
|
|
1619
|
-
console.log(formatTaskDetail(fallback));
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
const urls = tasks.map((t) => t.url);
|
|
1623
|
-
console.log("");
|
|
1624
|
-
const shouldOpen = await confirm({
|
|
1625
|
-
message: `Open ${urls.length} task(s) in browser?`,
|
|
1626
|
-
default: true
|
|
1627
|
-
});
|
|
1628
|
-
if (shouldOpen) {
|
|
1629
|
-
for (const url of urls) {
|
|
1630
|
-
openUrl(url);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// src/commands/tasks.ts
|
|
1636
|
-
var DONE_PATTERNS = ["done", "complete", "closed"];
|
|
1637
|
-
function isDoneStatus(status) {
|
|
1638
|
-
const lower = status.toLowerCase();
|
|
1639
|
-
return DONE_PATTERNS.some((p) => lower.includes(p));
|
|
1640
|
-
}
|
|
1641
|
-
function formatDueDate(ms) {
|
|
1642
|
-
if (!ms) return "";
|
|
1643
|
-
return formatDate(ms);
|
|
1644
|
-
}
|
|
1645
|
-
function resolveTaskType(task, typeMap) {
|
|
1646
|
-
const id = task.custom_item_id ?? 0;
|
|
1647
|
-
if (id === 0) return "task";
|
|
1648
|
-
return typeMap.get(id) ?? `type_${id}`;
|
|
1649
|
-
}
|
|
1650
|
-
function summarize(task, typeMap) {
|
|
1651
|
-
return {
|
|
1652
|
-
id: task.id,
|
|
1653
|
-
name: task.name,
|
|
1654
|
-
status: task.status.status,
|
|
1655
|
-
task_type: resolveTaskType(task, typeMap ?? /* @__PURE__ */ new Map()),
|
|
1656
|
-
priority: task.priority?.priority ?? "none",
|
|
1657
|
-
due_date: formatDueDate(task.due_date),
|
|
1658
|
-
...task.due_date ? { dueRaw: task.due_date } : {},
|
|
1659
|
-
list: task.list.name,
|
|
1660
|
-
url: task.url,
|
|
1661
|
-
...task.parent ? { parent: task.parent } : {}
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
function buildTypeMap(types) {
|
|
1665
|
-
const map = /* @__PURE__ */ new Map();
|
|
1666
|
-
for (const t of types) {
|
|
1667
|
-
map.set(t.id, t.name);
|
|
1668
|
-
}
|
|
1669
|
-
return map;
|
|
1670
|
-
}
|
|
1671
|
-
function resolveTypeFilter(typeFilter, typeMap) {
|
|
1672
|
-
if (typeFilter === "task") return 0;
|
|
1673
|
-
const asNum = Number(typeFilter);
|
|
1674
|
-
if (Number.isFinite(asNum)) return asNum;
|
|
1675
|
-
const lower = typeFilter.toLowerCase();
|
|
1676
|
-
for (const [id, name] of typeMap) {
|
|
1677
|
-
if (name.toLowerCase() === lower) return id;
|
|
1678
|
-
}
|
|
1679
|
-
const available = ["task", ...Array.from(typeMap.values())].join(", ");
|
|
1680
|
-
throw new Error(`Unknown task type "${typeFilter}". Available types: ${available}`);
|
|
1681
|
-
}
|
|
1682
|
-
async function fetchMyTasks(config, opts = {}) {
|
|
1683
|
-
const client = new ClickUpClient(config);
|
|
1684
|
-
const { typeFilter, name, ...apiFilters } = opts;
|
|
1685
|
-
const [allTasks, customTypes] = await Promise.all([
|
|
1686
|
-
client.getMyTasks(config.teamId, apiFilters),
|
|
1687
|
-
client.getCustomTaskTypes(config.teamId)
|
|
1688
|
-
]);
|
|
1689
|
-
const typeMap = buildTypeMap(customTypes);
|
|
1690
|
-
let filtered = allTasks;
|
|
1691
|
-
if (typeFilter) {
|
|
1692
|
-
const targetId = resolveTypeFilter(typeFilter, typeMap);
|
|
1693
|
-
filtered = allTasks.filter((t) => (t.custom_item_id ?? 0) === targetId);
|
|
1694
|
-
}
|
|
1695
|
-
if (name) {
|
|
1696
|
-
const query = name.toLowerCase();
|
|
1697
|
-
filtered = filtered.filter((t) => t.name.toLowerCase().includes(query));
|
|
1698
|
-
}
|
|
1699
|
-
return filtered.map((t) => summarize(t, typeMap));
|
|
1700
|
-
}
|
|
1701
|
-
async function printTasks(tasks, forceJson, config) {
|
|
1702
|
-
if (shouldOutputJson(forceJson)) {
|
|
1703
|
-
console.log(JSON.stringify(tasks, null, 2));
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
if (!isTTY()) {
|
|
1707
|
-
console.log(formatTasksMarkdown(tasks));
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
if (tasks.length === 0) {
|
|
1711
|
-
console.log("No tasks found.");
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
const fetchTask = config ? (() => {
|
|
1715
|
-
const client = new ClickUpClient(config);
|
|
1716
|
-
return (id) => client.getTask(id);
|
|
1717
|
-
})() : void 0;
|
|
1718
|
-
const selected = await interactiveTaskPicker(tasks);
|
|
1719
|
-
await showDetailsAndOpen(selected, fetchTask);
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
61
|
// src/status.ts
|
|
1723
62
|
function matchStatus(input, statuses) {
|
|
1724
63
|
if (!input) return null;
|
|
@@ -1947,13 +286,13 @@ async function getTask(config, taskId) {
|
|
|
1947
286
|
}
|
|
1948
287
|
|
|
1949
288
|
// src/commands/init.ts
|
|
1950
|
-
import { password, select, confirm
|
|
1951
|
-
import
|
|
289
|
+
import { password, select, confirm } from "@inquirer/prompts";
|
|
290
|
+
import fs from "fs";
|
|
1952
291
|
async function runInitCommand() {
|
|
1953
|
-
const
|
|
1954
|
-
if (
|
|
1955
|
-
const overwrite = await
|
|
1956
|
-
message: `Config already exists at ${
|
|
292
|
+
const configPath2 = getConfigPath();
|
|
293
|
+
if (fs.existsSync(configPath2)) {
|
|
294
|
+
const overwrite = await confirm({
|
|
295
|
+
message: `Config already exists at ${configPath2}. Overwrite?`,
|
|
1957
296
|
default: false
|
|
1958
297
|
});
|
|
1959
298
|
if (!overwrite) {
|
|
@@ -1991,190 +330,19 @@ async function runInitCommand() {
|
|
|
1991
330
|
});
|
|
1992
331
|
}
|
|
1993
332
|
writeConfig({ apiToken, teamId });
|
|
1994
|
-
process.stdout.write(`Config written to ${
|
|
1995
|
-
`);
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
// src/commands/sprint.ts
|
|
1999
|
-
import { select as select2 } from "@inquirer/prompts";
|
|
2000
|
-
var SPRINT_KEYWORDS = ["sprint", "iteration", "cycle", "scrum"];
|
|
2001
|
-
function parseUSDateRange(name) {
|
|
2002
|
-
const m = name.match(/\((\d{1,2})\/(\d{1,2})\s*[-–]\s*(\d{1,2})\/(\d{1,2})\)/);
|
|
2003
|
-
if (!m) return null;
|
|
2004
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2005
|
-
const start = new Date(year, Number(m[1]) - 1, Number(m[2]));
|
|
2006
|
-
const end = new Date(year, Number(m[3]) - 1, Number(m[4]), 23, 59, 59);
|
|
2007
|
-
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
2008
|
-
return { start, end };
|
|
2009
|
-
}
|
|
2010
|
-
function parseISODateRange(name) {
|
|
2011
|
-
const m = name.match(/\((\d{4}-\d{2}-\d{2})\s*[-–]\s*(\d{4}-\d{2}-\d{2})\)/);
|
|
2012
|
-
if (!m) return null;
|
|
2013
|
-
const [sy, sm, sd] = m[1].split("-").map(Number);
|
|
2014
|
-
const [ey, em, ed] = m[2].split("-").map(Number);
|
|
2015
|
-
const start = new Date(sy, sm - 1, sd);
|
|
2016
|
-
const end = new Date(ey, em - 1, ed, 23, 59, 59);
|
|
2017
|
-
return { start, end };
|
|
2018
|
-
}
|
|
2019
|
-
function parseMonthDayRange(name) {
|
|
2020
|
-
const months = {
|
|
2021
|
-
jan: 0,
|
|
2022
|
-
feb: 1,
|
|
2023
|
-
mar: 2,
|
|
2024
|
-
apr: 3,
|
|
2025
|
-
may: 4,
|
|
2026
|
-
jun: 5,
|
|
2027
|
-
jul: 6,
|
|
2028
|
-
aug: 7,
|
|
2029
|
-
sep: 8,
|
|
2030
|
-
oct: 9,
|
|
2031
|
-
nov: 10,
|
|
2032
|
-
dec: 11
|
|
2033
|
-
};
|
|
2034
|
-
const m = name.match(/\(([A-Za-z]{3})\s+(\d{1,2})\s*[-–]\s*([A-Za-z]{3})\s+(\d{1,2})\)/);
|
|
2035
|
-
if (!m) return null;
|
|
2036
|
-
const sm = months[m[1].toLowerCase()];
|
|
2037
|
-
const em = months[m[3].toLowerCase()];
|
|
2038
|
-
if (sm === void 0 || em === void 0) return null;
|
|
2039
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2040
|
-
const start = new Date(year, sm, Number(m[2]));
|
|
2041
|
-
const end = new Date(year, em, Number(m[4]), 23, 59, 59);
|
|
2042
|
-
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
2043
|
-
return { start, end };
|
|
2044
|
-
}
|
|
2045
|
-
function parseEuropeanDateRange(name) {
|
|
2046
|
-
const m = name.match(/\((\d{1,2})\.(\d{1,2})\s*[-–]\s*(\d{1,2})\.(\d{1,2})\)/);
|
|
2047
|
-
if (!m) return null;
|
|
2048
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2049
|
-
const start = new Date(year, Number(m[2]) - 1, Number(m[1]));
|
|
2050
|
-
const end = new Date(year, Number(m[4]) - 1, Number(m[3]), 23, 59, 59);
|
|
2051
|
-
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
2052
|
-
return { start, end };
|
|
2053
|
-
}
|
|
2054
|
-
function parseSprintDates(name) {
|
|
2055
|
-
return parseUSDateRange(name) ?? parseISODateRange(name) ?? parseMonthDayRange(name) ?? parseEuropeanDateRange(name);
|
|
2056
|
-
}
|
|
2057
|
-
function findActiveSprintList(lists, today = /* @__PURE__ */ new Date()) {
|
|
2058
|
-
if (lists.length === 0) return null;
|
|
2059
|
-
for (const list of lists) {
|
|
2060
|
-
const dates = parseSprintDates(list.name);
|
|
2061
|
-
if (dates && today >= dates.start && today <= dates.end) return list;
|
|
2062
|
-
}
|
|
2063
|
-
for (const list of lists) {
|
|
2064
|
-
if (list.start_date && list.due_date) {
|
|
2065
|
-
const start = new Date(Number(list.start_date));
|
|
2066
|
-
const end = new Date(Number(list.due_date));
|
|
2067
|
-
if (today >= start && today <= end) return list;
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
return lists[lists.length - 1] ?? null;
|
|
2071
|
-
}
|
|
2072
|
-
var NOISE_WORDS = /* @__PURE__ */ new Set(["product", "team", "the", "and", "for", "test"]);
|
|
2073
|
-
function extractSpaceKeywords(spaceName) {
|
|
2074
|
-
return spaceName.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((w) => w.toLowerCase()).filter((w) => w.length >= 3 && !NOISE_WORDS.has(w));
|
|
2075
|
-
}
|
|
2076
|
-
function findRelatedSpaces(mySpaceIds, allSpaces) {
|
|
2077
|
-
const mySpaces = allSpaces.filter((s) => mySpaceIds.has(s.id));
|
|
2078
|
-
const keywords = mySpaces.flatMap((s) => extractSpaceKeywords(s.name));
|
|
2079
|
-
if (keywords.length === 0) return allSpaces;
|
|
2080
|
-
return allSpaces.filter(
|
|
2081
|
-
(s) => mySpaceIds.has(s.id) || keywords.some((kw) => s.name.toLowerCase().includes(kw))
|
|
2082
|
-
);
|
|
2083
|
-
}
|
|
2084
|
-
async function runSprintCommand(config, opts) {
|
|
2085
|
-
const client = new ClickUpClient(config);
|
|
2086
|
-
process.stderr.write("Detecting active sprint...\n");
|
|
2087
|
-
const folderId = opts.folder ?? config.sprintFolderId;
|
|
2088
|
-
const [myTasks, allSpaces, customTypes] = await Promise.all([
|
|
2089
|
-
client.getMyTasks(config.teamId),
|
|
2090
|
-
folderId ? Promise.resolve([]) : client.getSpaces(config.teamId),
|
|
2091
|
-
client.getCustomTaskTypes(config.teamId)
|
|
2092
|
-
]);
|
|
2093
|
-
const typeMap = buildTypeMap(customTypes);
|
|
2094
|
-
let sprintLists;
|
|
2095
|
-
if (folderId) {
|
|
2096
|
-
sprintLists = await client.getFolderLists(folderId);
|
|
2097
|
-
} else {
|
|
2098
|
-
let spaces;
|
|
2099
|
-
if (opts.space) {
|
|
2100
|
-
spaces = allSpaces.filter(
|
|
2101
|
-
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
2102
|
-
);
|
|
2103
|
-
if (spaces.length === 0) {
|
|
2104
|
-
throw new Error(
|
|
2105
|
-
`No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
|
|
2106
|
-
);
|
|
2107
|
-
}
|
|
2108
|
-
} else {
|
|
2109
|
-
const mySpaceIds = new Set(
|
|
2110
|
-
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
2111
|
-
);
|
|
2112
|
-
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
2113
|
-
}
|
|
2114
|
-
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
2115
|
-
const sprintFolders = foldersBySpace.flat().filter((f) => {
|
|
2116
|
-
const lower = f.name.toLowerCase();
|
|
2117
|
-
return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
2118
|
-
});
|
|
2119
|
-
const listsByFolder = await Promise.all(
|
|
2120
|
-
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
2121
|
-
);
|
|
2122
|
-
sprintLists = listsByFolder.flat();
|
|
2123
|
-
}
|
|
2124
|
-
let activeList = findActiveSprintList(sprintLists);
|
|
2125
|
-
if (!activeList && sprintLists.length > 1 && isTTY()) {
|
|
2126
|
-
const choice = await select2({
|
|
2127
|
-
message: "Multiple sprint lists found. Which one?",
|
|
2128
|
-
choices: sprintLists.map((l) => ({
|
|
2129
|
-
name: `${l.name} (${l.id})`,
|
|
2130
|
-
value: l
|
|
2131
|
-
}))
|
|
2132
|
-
});
|
|
2133
|
-
activeList = choice;
|
|
2134
|
-
}
|
|
2135
|
-
if (!activeList && sprintLists.length > 1) {
|
|
2136
|
-
process.stderr.write(
|
|
2137
|
-
`Multiple sprint lists found:
|
|
2138
|
-
${sprintLists.map((l) => ` - ${l.name} (${l.id})`).join("\n")}
|
|
2139
|
-
Using: ${sprintLists[sprintLists.length - 1].name}
|
|
2140
|
-
`
|
|
2141
|
-
);
|
|
2142
|
-
activeList = sprintLists[sprintLists.length - 1] ?? null;
|
|
2143
|
-
}
|
|
2144
|
-
if (!activeList) {
|
|
2145
|
-
throw new Error(
|
|
2146
|
-
'No sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
|
|
2147
|
-
);
|
|
2148
|
-
}
|
|
2149
|
-
process.stderr.write(`Active sprint: ${activeList.name}
|
|
333
|
+
process.stdout.write(`Config written to ${configPath2}
|
|
2150
334
|
`);
|
|
2151
|
-
const me = await client.getMe();
|
|
2152
|
-
const viewData = await client.getListViews(activeList.id);
|
|
2153
|
-
const listView = viewData.required_views?.list;
|
|
2154
|
-
let allTasks;
|
|
2155
|
-
if (listView) {
|
|
2156
|
-
allTasks = await client.getViewTasks(listView.id);
|
|
2157
|
-
} else {
|
|
2158
|
-
allTasks = await client.getTasksFromList(activeList.id);
|
|
2159
|
-
}
|
|
2160
|
-
let sprintTasks = allTasks.filter((t) => t.assignees.some((a) => Number(a.id) === me.id));
|
|
2161
|
-
if (!opts.includeClosed) {
|
|
2162
|
-
sprintTasks = sprintTasks.filter((t) => !isDoneStatus(t.status.status));
|
|
2163
|
-
}
|
|
2164
|
-
const filtered = opts.status ? sprintTasks.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase()) : sprintTasks;
|
|
2165
|
-
const summaries = filtered.map((t) => summarize(t, typeMap));
|
|
2166
|
-
await printTasks(summaries, opts.json ?? false, config);
|
|
2167
335
|
}
|
|
2168
336
|
|
|
2169
337
|
// src/commands/sprints.ts
|
|
2170
|
-
import
|
|
338
|
+
import chalk from "chalk";
|
|
2171
339
|
var SPRINT_COLUMNS = [
|
|
2172
340
|
{ key: "id", label: "ID" },
|
|
2173
341
|
{
|
|
2174
342
|
key: "sprint",
|
|
2175
343
|
label: "SPRINT",
|
|
2176
344
|
maxWidth: 60,
|
|
2177
|
-
format: (v, row) => row.active ?
|
|
345
|
+
format: (v, row) => row.active ? chalk.green(v) : v
|
|
2178
346
|
},
|
|
2179
347
|
{ key: "dates", label: "DATES" }
|
|
2180
348
|
];
|
|
@@ -2217,11 +385,15 @@ async function listSprints(config, opts = {}) {
|
|
|
2217
385
|
);
|
|
2218
386
|
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
2219
387
|
}
|
|
388
|
+
const favorites = getFavorites();
|
|
389
|
+
const favoriteFolderIds = Object.values(favorites).filter((f) => f.type === "sprint-folder").map((f) => f.id);
|
|
2220
390
|
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
2221
391
|
const sprintFolders = foldersBySpace.flat().filter((f) => {
|
|
2222
392
|
const lower = f.name.toLowerCase();
|
|
2223
393
|
return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
2224
394
|
});
|
|
395
|
+
const detectedFolderIds = new Set(sprintFolders.map((f) => f.id));
|
|
396
|
+
const extraFavoriteIds = favoriteFolderIds.filter((id) => !detectedFolderIds.has(id));
|
|
2225
397
|
const today = /* @__PURE__ */ new Date();
|
|
2226
398
|
const allSprints = [];
|
|
2227
399
|
const listsByFolder = await Promise.all(
|
|
@@ -2232,6 +404,16 @@ async function listSprints(config, opts = {}) {
|
|
|
2232
404
|
const lists = listsByFolder[i];
|
|
2233
405
|
allSprints.push(...buildSprintInfos(lists, folder.name, today));
|
|
2234
406
|
}
|
|
407
|
+
const extraListsByFolder = await Promise.all(
|
|
408
|
+
extraFavoriteIds.map((id) => client.getFolderLists(id))
|
|
409
|
+
);
|
|
410
|
+
for (let i = 0; i < extraFavoriteIds.length; i++) {
|
|
411
|
+
const lists = extraListsByFolder[i];
|
|
412
|
+
const alias = Object.entries(favorites).find(
|
|
413
|
+
([_, f]) => f.type === "sprint-folder" && f.id === extraFavoriteIds[i]
|
|
414
|
+
)?.[1]?.name ?? `Folder ${extraFavoriteIds[i]}`;
|
|
415
|
+
allSprints.push(...buildSprintInfos(lists, alias, today));
|
|
416
|
+
}
|
|
2235
417
|
if (shouldOutputJson(opts.json ?? false)) {
|
|
2236
418
|
console.log(JSON.stringify(allSprints, null, 2));
|
|
2237
419
|
return;
|
|
@@ -2286,7 +468,7 @@ async function postComment(config, taskId, text, notifyAll) {
|
|
|
2286
468
|
}
|
|
2287
469
|
|
|
2288
470
|
// src/commands/comments.ts
|
|
2289
|
-
import
|
|
471
|
+
import chalk2 from "chalk";
|
|
2290
472
|
async function fetchComments(config, taskId) {
|
|
2291
473
|
const client = new ClickUpClient(config);
|
|
2292
474
|
const comments = await client.getTaskComments(taskId);
|
|
@@ -2310,11 +492,11 @@ function printComments(comments, forceJson) {
|
|
|
2310
492
|
console.log("No comments found.");
|
|
2311
493
|
return;
|
|
2312
494
|
}
|
|
2313
|
-
const separator =
|
|
495
|
+
const separator = chalk2.dim("-".repeat(60));
|
|
2314
496
|
for (let i = 0; i < comments.length; i++) {
|
|
2315
497
|
const c = comments[i];
|
|
2316
498
|
if (i > 0) console.log(separator);
|
|
2317
|
-
console.log(`${
|
|
499
|
+
console.log(`${chalk2.bold(c.user)} ${chalk2.dim(formatTimestamp(c.date))}`);
|
|
2318
500
|
console.log(c.text);
|
|
2319
501
|
if (i < comments.length - 1) console.log("");
|
|
2320
502
|
}
|
|
@@ -2624,7 +806,7 @@ async function openTask(config, query, opts = {}) {
|
|
|
2624
806
|
}
|
|
2625
807
|
|
|
2626
808
|
// src/commands/summary.ts
|
|
2627
|
-
import
|
|
809
|
+
import chalk3 from "chalk";
|
|
2628
810
|
var IN_PROGRESS_PATTERNS = ["in progress", "in review", "code review", "doing"];
|
|
2629
811
|
function isCompletedRecently(task, cutoff) {
|
|
2630
812
|
if (!isDoneStatus(task.status.status)) return false;
|
|
@@ -2662,9 +844,9 @@ function categorizeTasks(tasks, hoursBack, typeMap) {
|
|
|
2662
844
|
}
|
|
2663
845
|
function colorSectionLabel(label) {
|
|
2664
846
|
const lower = label.toLowerCase();
|
|
2665
|
-
if (lower.includes("completed")) return
|
|
2666
|
-
if (lower.includes("progress")) return
|
|
2667
|
-
if (lower.includes("overdue")) return
|
|
847
|
+
if (lower.includes("completed")) return chalk3.green(label);
|
|
848
|
+
if (lower.includes("progress")) return chalk3.yellow(label);
|
|
849
|
+
if (lower.includes("overdue")) return chalk3.red(label);
|
|
2668
850
|
return label;
|
|
2669
851
|
}
|
|
2670
852
|
function printSection(label, tasks) {
|
|
@@ -2766,7 +948,7 @@ function setConfigValue(key, value, profileName) {
|
|
|
2766
948
|
}
|
|
2767
949
|
writeConfig(merged, profileName);
|
|
2768
950
|
}
|
|
2769
|
-
function
|
|
951
|
+
function configPath() {
|
|
2770
952
|
return getConfigPath();
|
|
2771
953
|
}
|
|
2772
954
|
|
|
@@ -2793,7 +975,7 @@ async function assignTask(config, taskId, opts) {
|
|
|
2793
975
|
}
|
|
2794
976
|
|
|
2795
977
|
// src/commands/activity.ts
|
|
2796
|
-
import
|
|
978
|
+
import chalk4 from "chalk";
|
|
2797
979
|
async function fetchActivity(config, taskId) {
|
|
2798
980
|
const client = new ClickUpClient(config);
|
|
2799
981
|
const [task, rawComments] = await Promise.all([
|
|
@@ -2825,8 +1007,8 @@ ${commentsMd}`);
|
|
|
2825
1007
|
}
|
|
2826
1008
|
console.log(formatTaskDetail(result.task));
|
|
2827
1009
|
console.log("");
|
|
2828
|
-
console.log(
|
|
2829
|
-
console.log(
|
|
1010
|
+
console.log(chalk4.bold("Comments"));
|
|
1011
|
+
console.log(chalk4.dim("-".repeat(60)));
|
|
2830
1012
|
if (result.comments.length === 0) {
|
|
2831
1013
|
console.log("No comments.");
|
|
2832
1014
|
return;
|
|
@@ -2835,13 +1017,90 @@ ${commentsMd}`);
|
|
|
2835
1017
|
const c = result.comments[i];
|
|
2836
1018
|
if (i > 0) {
|
|
2837
1019
|
console.log("");
|
|
2838
|
-
console.log(
|
|
1020
|
+
console.log(chalk4.dim("-".repeat(60)));
|
|
2839
1021
|
}
|
|
2840
|
-
console.log(`${
|
|
1022
|
+
console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatTimestamp(c.date))}`);
|
|
2841
1023
|
console.log(c.text);
|
|
2842
1024
|
}
|
|
2843
1025
|
}
|
|
2844
1026
|
|
|
1027
|
+
// src/commands/time-in-status.ts
|
|
1028
|
+
import chalk5 from "chalk";
|
|
1029
|
+
function transformResponse(taskId, data) {
|
|
1030
|
+
const entries = [];
|
|
1031
|
+
for (const entry of data.status_history ?? []) {
|
|
1032
|
+
const ms = (entry.total_time?.by_minute ?? 0) * 6e4;
|
|
1033
|
+
entries.push({
|
|
1034
|
+
status: entry.status,
|
|
1035
|
+
duration: formatLongDuration(ms),
|
|
1036
|
+
durationMs: ms,
|
|
1037
|
+
current: false
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
if (data.current_status) {
|
|
1041
|
+
const ms = (data.current_status.total_time?.by_minute ?? 0) * 6e4;
|
|
1042
|
+
const existing = entries.find((e) => e.status === data.current_status.status);
|
|
1043
|
+
if (existing) {
|
|
1044
|
+
existing.current = true;
|
|
1045
|
+
} else {
|
|
1046
|
+
entries.push({
|
|
1047
|
+
status: data.current_status.status,
|
|
1048
|
+
duration: formatLongDuration(ms),
|
|
1049
|
+
durationMs: ms,
|
|
1050
|
+
current: true
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const totalMs = entries.reduce((sum, e) => sum + e.durationMs, 0);
|
|
1055
|
+
return { taskId, statuses: entries, totalMs, total: formatLongDuration(totalMs) };
|
|
1056
|
+
}
|
|
1057
|
+
async function fetchTimeInStatus(config, taskId) {
|
|
1058
|
+
const client = new ClickUpClient(config);
|
|
1059
|
+
let data;
|
|
1060
|
+
try {
|
|
1061
|
+
data = await client.getTimeInStatus(taskId);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
if (err instanceof Error && /No data for TIS/i.test(err.message)) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
'The "Time in Status" ClickApp is not enabled for this workspace.\nEnable it in ClickUp: Space Settings \u2192 ClickApps \u2192 Time in Status'
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
throw err;
|
|
1069
|
+
}
|
|
1070
|
+
return transformResponse(taskId, data);
|
|
1071
|
+
}
|
|
1072
|
+
function printTimeInStatus(result, forceJson) {
|
|
1073
|
+
if (shouldOutputJson(forceJson)) {
|
|
1074
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const rows = result.statuses.map((s) => ({
|
|
1078
|
+
status: s.status,
|
|
1079
|
+
duration: s.duration,
|
|
1080
|
+
current: s.current ? "*" : ""
|
|
1081
|
+
}));
|
|
1082
|
+
if (!isTTY()) {
|
|
1083
|
+
const mdColumns = [
|
|
1084
|
+
{ key: "status", label: "Status" },
|
|
1085
|
+
{ key: "duration", label: "Duration" },
|
|
1086
|
+
{ key: "current", label: "Current" }
|
|
1087
|
+
];
|
|
1088
|
+
const table = formatMarkdownTable(rows, mdColumns);
|
|
1089
|
+
console.log(`${table}
|
|
1090
|
+
|
|
1091
|
+
**Total:** ${result.total}`);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const columns = [
|
|
1095
|
+
{ key: "status", label: "STATUS", maxWidth: 25, format: (v) => colorStatus(v) },
|
|
1096
|
+
{ key: "duration", label: "DURATION" },
|
|
1097
|
+
{ key: "current", label: "", format: (v) => v ? chalk5.green("\u25C0") : "" }
|
|
1098
|
+
];
|
|
1099
|
+
console.log(formatTable(rows, columns));
|
|
1100
|
+
console.log("");
|
|
1101
|
+
console.log(`${chalk5.bold("Total:")} ${result.total}`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
2845
1104
|
// src/commands/metadata.ts
|
|
2846
1105
|
var commandMetadata = [
|
|
2847
1106
|
{
|
|
@@ -2985,7 +1244,7 @@ var commandMetadata = [
|
|
|
2985
1244
|
{
|
|
2986
1245
|
name: "comment-delete",
|
|
2987
1246
|
description: "Delete a comment",
|
|
2988
|
-
flags: ["--json"],
|
|
1247
|
+
flags: ["--mine", "--match", "--json"],
|
|
2989
1248
|
quickReference: [
|
|
2990
1249
|
{
|
|
2991
1250
|
section: "write",
|
|
@@ -3275,6 +1534,18 @@ var commandMetadata = [
|
|
|
3275
1534
|
{ section: "write", usage: "time delete <timeEntryId>", description: "Delete a time entry" }
|
|
3276
1535
|
]
|
|
3277
1536
|
},
|
|
1537
|
+
{
|
|
1538
|
+
name: "time-in-status",
|
|
1539
|
+
description: "Show how long a task has been in each status",
|
|
1540
|
+
flags: ["--json"],
|
|
1541
|
+
quickReference: [
|
|
1542
|
+
{
|
|
1543
|
+
section: "read",
|
|
1544
|
+
usage: "time-in-status <taskId>",
|
|
1545
|
+
description: "Show how long a task has been in each status"
|
|
1546
|
+
}
|
|
1547
|
+
]
|
|
1548
|
+
},
|
|
3278
1549
|
{
|
|
3279
1550
|
name: "docs",
|
|
3280
1551
|
description: "List workspace docs (optionally filter by name)",
|
|
@@ -3648,6 +1919,23 @@ var commandMetadata = [
|
|
|
3648
1919
|
{ section: "read", usage: "filter run <name>", description: "Run a saved shortcut" }
|
|
3649
1920
|
]
|
|
3650
1921
|
},
|
|
1922
|
+
{
|
|
1923
|
+
name: "favorite",
|
|
1924
|
+
description: "Manage local favorites (sprint folders, spaces, lists, etc.)",
|
|
1925
|
+
quickReference: [
|
|
1926
|
+
{
|
|
1927
|
+
section: "configuration",
|
|
1928
|
+
usage: "favorite add <type> <id> [alias]",
|
|
1929
|
+
description: "Add a favorite"
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
section: "configuration",
|
|
1933
|
+
usage: "favorite remove <alias>",
|
|
1934
|
+
description: "Remove a favorite"
|
|
1935
|
+
},
|
|
1936
|
+
{ section: "read", usage: "favorite list", description: "List saved favorites" }
|
|
1937
|
+
]
|
|
1938
|
+
},
|
|
3651
1939
|
{
|
|
3652
1940
|
name: "profile",
|
|
3653
1941
|
description: "Manage profiles",
|
|
@@ -3716,6 +2004,7 @@ var bashSpecialCaseCommands = /* @__PURE__ */ new Set([
|
|
|
3716
2004
|
"time",
|
|
3717
2005
|
"bulk",
|
|
3718
2006
|
"filter",
|
|
2007
|
+
"favorite",
|
|
3719
2008
|
"config",
|
|
3720
2009
|
"profile",
|
|
3721
2010
|
"completion"
|
|
@@ -3812,6 +2101,18 @@ ${renderBashCommandCases()}
|
|
|
3812
2101
|
COMPREPLY=($(compgen -W "status assign due-date tag" -- "$cur"))
|
|
3813
2102
|
fi
|
|
3814
2103
|
;;
|
|
2104
|
+
favorite)
|
|
2105
|
+
if [[ $cword -eq 2 ]]; then
|
|
2106
|
+
COMPREPLY=($(compgen -W "add remove list" -- "$cur"))
|
|
2107
|
+
elif [[ $cword -eq 3 ]]; then
|
|
2108
|
+
local subcmd="\${words[2]}"
|
|
2109
|
+
case "$subcmd" in
|
|
2110
|
+
add)
|
|
2111
|
+
COMPREPLY=($(compgen -W "sprint-folder space list folder view task" -- "$cur"))
|
|
2112
|
+
;;
|
|
2113
|
+
esac
|
|
2114
|
+
fi
|
|
2115
|
+
;;
|
|
3815
2116
|
profile)
|
|
3816
2117
|
if [[ $cword -eq 2 ]]; then
|
|
3817
2118
|
COMPREPLY=($(compgen -W "list add remove use" -- "$cur"))
|
|
@@ -4181,6 +2482,8 @@ ${renderZshTopLevelCommands(name)}
|
|
|
4181
2482
|
comment-delete)
|
|
4182
2483
|
_arguments \\
|
|
4183
2484
|
'1:comment_id:' \\
|
|
2485
|
+
'--mine[Delete one of my comments from the specified task]' \\
|
|
2486
|
+
'--match[Only match comments containing this text]:text:' \\
|
|
4184
2487
|
'--json[Force JSON output]'
|
|
4185
2488
|
;;
|
|
4186
2489
|
replies)
|
|
@@ -4469,6 +2772,40 @@ ${renderZshTopLevelCommands(name)}
|
|
|
4469
2772
|
'(-c --content)'{-c,--content}'[New page content]:text:' \\
|
|
4470
2773
|
'--json[Force JSON output]'
|
|
4471
2774
|
;;
|
|
2775
|
+
favorite)
|
|
2776
|
+
local -a favorite_cmds
|
|
2777
|
+
favorite_cmds=(
|
|
2778
|
+
'add:Add a favorite (types: sprint-folder, space, list, folder, view, task)'
|
|
2779
|
+
'remove:Remove a favorite by alias'
|
|
2780
|
+
'list:List saved favorites'
|
|
2781
|
+
)
|
|
2782
|
+
_arguments -C \\
|
|
2783
|
+
'1:favorite command:->favorite_cmd' \\
|
|
2784
|
+
'*::favorite_arg:->favorite_args'
|
|
2785
|
+
case $state in
|
|
2786
|
+
favorite_cmd)
|
|
2787
|
+
_describe 'favorite command' favorite_cmds
|
|
2788
|
+
;;
|
|
2789
|
+
favorite_args)
|
|
2790
|
+
case $words[1] in
|
|
2791
|
+
add)
|
|
2792
|
+
_arguments \\
|
|
2793
|
+
'1:type:(sprint-folder space list folder view task)' \\
|
|
2794
|
+
'2:id:' \\
|
|
2795
|
+
'3:alias:' \\
|
|
2796
|
+
'(-n --name)'{-n,--name}'[Display name]:name:' \\
|
|
2797
|
+
'--json[Force JSON output]'
|
|
2798
|
+
;;
|
|
2799
|
+
remove)
|
|
2800
|
+
_arguments '1:alias:' '--json[Force JSON output]'
|
|
2801
|
+
;;
|
|
2802
|
+
list)
|
|
2803
|
+
_arguments '--type[Filter by entity type]:type:(sprint-folder space list folder view task)' '--json[Force JSON output]'
|
|
2804
|
+
;;
|
|
2805
|
+
esac
|
|
2806
|
+
;;
|
|
2807
|
+
esac
|
|
2808
|
+
;;
|
|
4472
2809
|
filter)
|
|
4473
2810
|
local -a filter_cmds
|
|
4474
2811
|
filter_cmds=(
|
|
@@ -4651,6 +2988,14 @@ complete -c ${name} -n '__fish_seen_subcommand_from filter; and not __fish_seen_
|
|
|
4651
2988
|
complete -c ${name} -n '__fish_seen_subcommand_from filter; and not __fish_seen_subcommand_from save run list delete show' -a show -d 'Show details of a saved shortcut'
|
|
4652
2989
|
complete -c ${name} -n '__fish_seen_subcommand_from save run list delete show; and __fish_seen_subcommand_from filter' -l json -d 'Force JSON output'
|
|
4653
2990
|
complete -c ${name} -n '__fish_seen_subcommand_from save; and __fish_seen_subcommand_from filter' -s d -l description -d 'Filter description'
|
|
2991
|
+
|
|
2992
|
+
complete -c ${name} -n '__fish_seen_subcommand_from favorite; and not __fish_seen_subcommand_from add remove list' -a add -d 'Add a favorite'
|
|
2993
|
+
complete -c ${name} -n '__fish_seen_subcommand_from favorite; and not __fish_seen_subcommand_from add remove list' -a remove -d 'Remove a favorite'
|
|
2994
|
+
complete -c ${name} -n '__fish_seen_subcommand_from favorite; and not __fish_seen_subcommand_from add remove list' -a list -d 'List saved favorites'
|
|
2995
|
+
complete -c ${name} -n '__fish_seen_subcommand_from add; and __fish_seen_subcommand_from favorite' -a 'sprint-folder space list folder view task' -d 'Entity type'
|
|
2996
|
+
complete -c ${name} -n '__fish_seen_subcommand_from add remove list; and __fish_seen_subcommand_from favorite' -l json -d 'Force JSON output'
|
|
2997
|
+
complete -c ${name} -n '__fish_seen_subcommand_from add; and __fish_seen_subcommand_from favorite' -s n -l name -d 'Display name'
|
|
2998
|
+
complete -c ${name} -n '__fish_seen_subcommand_from list; and __fish_seen_subcommand_from favorite' -l type -d 'Filter by entity type'
|
|
4654
2999
|
`;
|
|
4655
3000
|
}
|
|
4656
3001
|
function generateCompletion(shell, name = "cup") {
|
|
@@ -4668,15 +3013,18 @@ function generateCompletion(shell, name = "cup") {
|
|
|
4668
3013
|
|
|
4669
3014
|
// src/commands/skill.ts
|
|
4670
3015
|
import { readFileSync, realpathSync, mkdirSync, copyFileSync, existsSync } from "fs";
|
|
4671
|
-
import { join
|
|
4672
|
-
import { homedir
|
|
4673
|
-
import
|
|
3016
|
+
import { join, dirname } from "path";
|
|
3017
|
+
import { homedir } from "os";
|
|
3018
|
+
import chalk6 from "chalk";
|
|
4674
3019
|
function skillPath() {
|
|
4675
|
-
|
|
3020
|
+
if (!process.argv[1]) {
|
|
3021
|
+
throw new Error("Cannot determine install path. Run with: cup skill");
|
|
3022
|
+
}
|
|
3023
|
+
const entryPoint = realpathSync(process.argv[1]);
|
|
4676
3024
|
const packageRoot = dirname(dirname(entryPoint));
|
|
4677
|
-
const candidate =
|
|
3025
|
+
const candidate = join(packageRoot, "skills", "clickup-cli", "SKILL.md");
|
|
4678
3026
|
if (existsSync(candidate)) return candidate;
|
|
4679
|
-
const altCandidate =
|
|
3027
|
+
const altCandidate = join(dirname(entryPoint), "..", "skills", "clickup-cli", "SKILL.md");
|
|
4680
3028
|
if (existsSync(altCandidate)) return altCandidate;
|
|
4681
3029
|
throw new Error("SKILL.md not found. Reinstall with: npm install -g @krodak/clickup-cli");
|
|
4682
3030
|
}
|
|
@@ -4684,11 +3032,11 @@ function printSkill() {
|
|
|
4684
3032
|
return readFileSync(skillPath(), "utf-8");
|
|
4685
3033
|
}
|
|
4686
3034
|
function getAgentTargets() {
|
|
4687
|
-
const home =
|
|
3035
|
+
const home = homedir();
|
|
4688
3036
|
const targets = [
|
|
4689
|
-
{ name: "Claude Code", dir:
|
|
4690
|
-
{ name: "Codex", dir:
|
|
4691
|
-
{ name: "OpenCode", dir:
|
|
3037
|
+
{ name: "Claude Code", dir: join(home, ".claude", "skills", "clickup") },
|
|
3038
|
+
{ name: "Codex", dir: join(home, ".agents", "skills", "clickup") },
|
|
3039
|
+
{ name: "OpenCode", dir: join(home, ".config", "opencode", "skills", "clickup") }
|
|
4692
3040
|
];
|
|
4693
3041
|
return targets.map((t) => ({
|
|
4694
3042
|
...t,
|
|
@@ -4700,12 +3048,12 @@ async function installSkillInteractive() {
|
|
|
4700
3048
|
const source = skillPath();
|
|
4701
3049
|
const installed = [];
|
|
4702
3050
|
if (isTTY()) {
|
|
4703
|
-
const { checkbox
|
|
3051
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
4704
3052
|
const preselected = targets.filter((t) => t.detected).map((t) => t.name);
|
|
4705
|
-
const selected = await
|
|
3053
|
+
const selected = await checkbox({
|
|
4706
3054
|
message: "Install skill for which agents?",
|
|
4707
3055
|
choices: targets.map((t) => ({
|
|
4708
|
-
name: `${t.name}${t.detected ?
|
|
3056
|
+
name: `${t.name}${t.detected ? chalk6.dim(" (detected)") : ""}`,
|
|
4709
3057
|
value: t.name,
|
|
4710
3058
|
checked: t.detected
|
|
4711
3059
|
}))
|
|
@@ -4715,25 +3063,25 @@ async function installSkillInteractive() {
|
|
|
4715
3063
|
}
|
|
4716
3064
|
for (const name of selected) {
|
|
4717
3065
|
const target = targets.find((t) => t.name === name);
|
|
3066
|
+
if (!target) continue;
|
|
4718
3067
|
if (!existsSync(target.dir)) mkdirSync(target.dir, { recursive: true });
|
|
4719
|
-
const dest =
|
|
3068
|
+
const dest = join(target.dir, "SKILL.md");
|
|
4720
3069
|
copyFileSync(source, dest);
|
|
4721
3070
|
installed.push(`${target.name}: ${dest}`);
|
|
4722
3071
|
}
|
|
4723
3072
|
} else {
|
|
4724
|
-
|
|
3073
|
+
const detected = targets.filter((t) => t.detected);
|
|
3074
|
+
if (detected.length === 0) {
|
|
3075
|
+
throw new Error(
|
|
3076
|
+
"No agents detected. Use --path to specify install location:\n cup skill --path ~/.claude/skills/clickup/SKILL.md"
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
for (const target of detected) {
|
|
4725
3080
|
if (!existsSync(target.dir)) mkdirSync(target.dir, { recursive: true });
|
|
4726
|
-
const dest =
|
|
3081
|
+
const dest = join(target.dir, "SKILL.md");
|
|
4727
3082
|
copyFileSync(source, dest);
|
|
4728
3083
|
installed.push(`${target.name}: ${dest}`);
|
|
4729
3084
|
}
|
|
4730
|
-
if (installed.length === 0) {
|
|
4731
|
-
const fallback = targets[0];
|
|
4732
|
-
mkdirSync(fallback.dir, { recursive: true });
|
|
4733
|
-
const dest = join2(fallback.dir, "SKILL.md");
|
|
4734
|
-
copyFileSync(source, dest);
|
|
4735
|
-
installed.push(`${fallback.name}: ${dest}`);
|
|
4736
|
-
}
|
|
4737
3085
|
}
|
|
4738
3086
|
return installed;
|
|
4739
3087
|
}
|
|
@@ -4950,8 +3298,8 @@ async function deleteTaskCommand(config, taskId, opts) {
|
|
|
4950
3298
|
throw new Error("Destructive operation requires --confirm flag in non-interactive mode");
|
|
4951
3299
|
}
|
|
4952
3300
|
const task = await client.getTask(taskId);
|
|
4953
|
-
const { confirm:
|
|
4954
|
-
const confirmed = await
|
|
3301
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
3302
|
+
const confirmed = await confirm2({
|
|
4955
3303
|
message: `Delete task "${task.name}" (${task.id})? This cannot be undone.`,
|
|
4956
3304
|
default: false
|
|
4957
3305
|
});
|
|
@@ -4971,8 +3319,8 @@ async function archiveTaskCommand(config, taskId, opts) {
|
|
|
4971
3319
|
throw new Error(`Destructive operation requires --confirm flag in non-interactive mode`);
|
|
4972
3320
|
}
|
|
4973
3321
|
const task = await client.getTask(taskId);
|
|
4974
|
-
const { confirm:
|
|
4975
|
-
const confirmed = await
|
|
3322
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
3323
|
+
const confirmed = await confirm2({
|
|
4976
3324
|
message: `${opts.unarchive ? "Unarchive" : "Archive"} task "${task.name}" (${task.id})?`,
|
|
4977
3325
|
default: false
|
|
4978
3326
|
});
|
|
@@ -5023,7 +3371,7 @@ async function manageTags(config, taskId, opts) {
|
|
|
5023
3371
|
}
|
|
5024
3372
|
|
|
5025
3373
|
// src/commands/checklist.ts
|
|
5026
|
-
import
|
|
3374
|
+
import chalk7 from "chalk";
|
|
5027
3375
|
async function viewChecklists(config, taskId) {
|
|
5028
3376
|
const client = new ClickUpClient(config);
|
|
5029
3377
|
const task = await client.getTask(taskId);
|
|
@@ -5056,14 +3404,14 @@ function formatChecklists(checklists) {
|
|
|
5056
3404
|
const lines = [];
|
|
5057
3405
|
for (const cl of checklists) {
|
|
5058
3406
|
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
5059
|
-
lines.push(
|
|
5060
|
-
lines.push(
|
|
3407
|
+
lines.push(chalk7.bold(`${cl.name} (${resolved}/${cl.items.length})`));
|
|
3408
|
+
lines.push(chalk7.dim(` ID: ${cl.id}`));
|
|
5061
3409
|
for (const item of cl.items) {
|
|
5062
|
-
const check = item.resolved ?
|
|
5063
|
-
const name = item.resolved ?
|
|
5064
|
-
const assignee = item.assignee ?
|
|
3410
|
+
const check = item.resolved ? chalk7.green("[x]") : chalk7.dim("[ ]");
|
|
3411
|
+
const name = item.resolved ? chalk7.dim(item.name) : item.name;
|
|
3412
|
+
const assignee = item.assignee ? chalk7.dim(` @${item.assignee.username}`) : "";
|
|
5065
3413
|
lines.push(` ${check} ${name}${assignee}`);
|
|
5066
|
-
lines.push(
|
|
3414
|
+
lines.push(chalk7.dim(` item-id: ${item.id}`));
|
|
5067
3415
|
}
|
|
5068
3416
|
}
|
|
5069
3417
|
return lines.join("\n");
|
|
@@ -5095,9 +3443,34 @@ async function deleteComment(config, commentId) {
|
|
|
5095
3443
|
const client = new ClickUpClient(config);
|
|
5096
3444
|
await client.deleteComment(commentId);
|
|
5097
3445
|
}
|
|
3446
|
+
function matchesCommentText(commentText, match) {
|
|
3447
|
+
if (!match) return true;
|
|
3448
|
+
return commentText.toLowerCase().includes(match.toLowerCase());
|
|
3449
|
+
}
|
|
3450
|
+
async function deleteCommentByTaskSelection(config, taskId, options) {
|
|
3451
|
+
if (!options.mine) {
|
|
3452
|
+
throw new Error("Task-scoped comment deletion requires --mine");
|
|
3453
|
+
}
|
|
3454
|
+
const client = new ClickUpClient(config);
|
|
3455
|
+
const me = await client.getMe();
|
|
3456
|
+
const comments = await client.getTaskComments(taskId);
|
|
3457
|
+
const matches = comments.filter((comment2) => {
|
|
3458
|
+
const authorId = "id" in comment2.user ? comment2.user.id : void 0;
|
|
3459
|
+
return authorId === me.id && matchesCommentText(comment2.comment_text, options.match);
|
|
3460
|
+
});
|
|
3461
|
+
if (matches.length === 0) {
|
|
3462
|
+
throw new Error("No matching comments found for the current user");
|
|
3463
|
+
}
|
|
3464
|
+
if (matches.length > 1) {
|
|
3465
|
+
throw new Error("Multiple matching comments found - refine with --match or use a comment ID");
|
|
3466
|
+
}
|
|
3467
|
+
const comment = matches[0];
|
|
3468
|
+
await client.deleteComment(comment.id);
|
|
3469
|
+
return { commentId: comment.id, taskId };
|
|
3470
|
+
}
|
|
5098
3471
|
|
|
5099
3472
|
// src/commands/replies.ts
|
|
5100
|
-
import
|
|
3473
|
+
import chalk8 from "chalk";
|
|
5101
3474
|
async function getReplies(config, commentId) {
|
|
5102
3475
|
const client = new ClickUpClient(config);
|
|
5103
3476
|
return client.getThreadedComments(commentId);
|
|
@@ -5112,7 +3485,7 @@ function formatReplies(replies) {
|
|
|
5112
3485
|
return replies.map((r) => {
|
|
5113
3486
|
const user = r.user?.username ?? "Unknown";
|
|
5114
3487
|
const date = formatTimestamp(Number(r.date));
|
|
5115
|
-
return `${
|
|
3488
|
+
return `${chalk8.bold(user)} ${chalk8.dim(date)}
|
|
5116
3489
|
${r.comment_text}`;
|
|
5117
3490
|
}).join("\n\n");
|
|
5118
3491
|
}
|
|
@@ -5175,7 +3548,7 @@ function formatDocsMarkdown(docs) {
|
|
|
5175
3548
|
}
|
|
5176
3549
|
|
|
5177
3550
|
// src/commands/doc.ts
|
|
5178
|
-
import
|
|
3551
|
+
import chalk9 from "chalk";
|
|
5179
3552
|
async function getDocInfo(config, docId) {
|
|
5180
3553
|
const client = new ClickUpClient(config);
|
|
5181
3554
|
const [doc, pages] = await Promise.all([
|
|
@@ -5187,14 +3560,14 @@ async function getDocInfo(config, docId) {
|
|
|
5187
3560
|
function formatDocInfo(doc, pages, indent = 0) {
|
|
5188
3561
|
const lines = [];
|
|
5189
3562
|
if (indent === 0) {
|
|
5190
|
-
lines.push(`${
|
|
3563
|
+
lines.push(`${chalk9.bold(doc.name)} ${chalk9.dim(doc.id)}`);
|
|
5191
3564
|
if (pages.length === 0) {
|
|
5192
3565
|
lines.push(" (no pages)");
|
|
5193
3566
|
}
|
|
5194
3567
|
}
|
|
5195
3568
|
for (const page of pages) {
|
|
5196
3569
|
const prefix = " ".repeat(indent + 1);
|
|
5197
|
-
lines.push(`${prefix}${page.name} ${
|
|
3570
|
+
lines.push(`${prefix}${page.name} ${chalk9.dim(page.id)}`);
|
|
5198
3571
|
if (page.pages && page.pages.length > 0) {
|
|
5199
3572
|
lines.push(formatDocInfo(doc, page.pages, indent + 1));
|
|
5200
3573
|
}
|
|
@@ -5273,7 +3646,7 @@ async function deleteDocPage(config, docId, pageId) {
|
|
|
5273
3646
|
}
|
|
5274
3647
|
|
|
5275
3648
|
// src/commands/folders.ts
|
|
5276
|
-
import
|
|
3649
|
+
import chalk10 from "chalk";
|
|
5277
3650
|
async function listFolders(config, spaceId, nameFilter) {
|
|
5278
3651
|
const client = new ClickUpClient(config);
|
|
5279
3652
|
const folders = await client.getFolders(spaceId);
|
|
@@ -5292,9 +3665,9 @@ async function listFolders(config, spaceId, nameFilter) {
|
|
|
5292
3665
|
function formatFolders(folders) {
|
|
5293
3666
|
if (folders.length === 0) return "No folders found";
|
|
5294
3667
|
return folders.map((f) => {
|
|
5295
|
-
const header = `${
|
|
3668
|
+
const header = `${chalk10.bold(f.name)} ${chalk10.dim(f.id)}`;
|
|
5296
3669
|
if (f.lists.length === 0) return header;
|
|
5297
|
-
const listLines = f.lists.map((l) => ` ${
|
|
3670
|
+
const listLines = f.lists.map((l) => ` ${chalk10.dim(">")} ${l.name} ${chalk10.dim(l.id)}`);
|
|
5298
3671
|
return [header, ...listLines].join("\n");
|
|
5299
3672
|
}).join("\n\n");
|
|
5300
3673
|
}
|
|
@@ -5309,13 +3682,13 @@ function formatFoldersMarkdown(folders) {
|
|
|
5309
3682
|
}
|
|
5310
3683
|
|
|
5311
3684
|
// src/commands/time.ts
|
|
5312
|
-
import
|
|
3685
|
+
import chalk11 from "chalk";
|
|
5313
3686
|
var TIME_COLUMNS = [
|
|
5314
3687
|
{ key: "task", label: "Task", maxWidth: 35 },
|
|
5315
3688
|
{ key: "duration", label: "Duration", maxWidth: 10 },
|
|
5316
3689
|
{ key: "date", label: "Date", maxWidth: 20 },
|
|
5317
3690
|
{ key: "description", label: "Description", maxWidth: 30 },
|
|
5318
|
-
{ key: "status", label: "", maxWidth: 10, format: (v) => v === "RUNNING" ?
|
|
3691
|
+
{ key: "status", label: "", maxWidth: 10, format: (v) => v === "RUNNING" ? chalk11.green(v) : "" }
|
|
5319
3692
|
];
|
|
5320
3693
|
async function startTimer(config, taskId, description) {
|
|
5321
3694
|
const client = new ClickUpClient(config);
|
|
@@ -5411,7 +3784,7 @@ function formatTimeEntriesMarkdown(entries) {
|
|
|
5411
3784
|
}
|
|
5412
3785
|
|
|
5413
3786
|
// src/commands/tags.ts
|
|
5414
|
-
import
|
|
3787
|
+
import chalk12 from "chalk";
|
|
5415
3788
|
var TAG_COLUMNS = [
|
|
5416
3789
|
{ key: "name", label: "Name", maxWidth: 40 },
|
|
5417
3790
|
{ key: "fg", label: "FG", maxWidth: 10 },
|
|
@@ -5444,13 +3817,13 @@ function formatTags(tags) {
|
|
|
5444
3817
|
if (tags.length === 0) return "No tags found";
|
|
5445
3818
|
if (isTTY()) {
|
|
5446
3819
|
const rows = tags.map((t) => ({
|
|
5447
|
-
name: t.tag_bg ?
|
|
3820
|
+
name: t.tag_bg ? chalk12.bgHex(t.tag_bg).hex(t.tag_fg || "#ffffff")(` ${t.name} `) : chalk12.bold(t.name),
|
|
5448
3821
|
fg: t.tag_fg || "",
|
|
5449
3822
|
bg: t.tag_bg || ""
|
|
5450
3823
|
}));
|
|
5451
3824
|
return formatTable(rows, TAG_COLUMNS);
|
|
5452
3825
|
}
|
|
5453
|
-
return tags.map((t) =>
|
|
3826
|
+
return tags.map((t) => chalk12.bold(t.name)).join(", ");
|
|
5454
3827
|
}
|
|
5455
3828
|
function formatTagsMarkdown(tags) {
|
|
5456
3829
|
if (tags.length === 0) return "No tags found";
|
|
@@ -5485,7 +3858,7 @@ function formatMembersMarkdown(members) {
|
|
|
5485
3858
|
}
|
|
5486
3859
|
|
|
5487
3860
|
// src/commands/fields.ts
|
|
5488
|
-
import
|
|
3861
|
+
import chalk13 from "chalk";
|
|
5489
3862
|
var FIELD_COLUMNS = [
|
|
5490
3863
|
{ key: "id", label: "ID", maxWidth: 20 },
|
|
5491
3864
|
{ key: "name", label: "Name", maxWidth: 30 },
|
|
@@ -5494,7 +3867,7 @@ var FIELD_COLUMNS = [
|
|
|
5494
3867
|
key: "required",
|
|
5495
3868
|
label: "Required",
|
|
5496
3869
|
maxWidth: 10,
|
|
5497
|
-
format: (v) => v === "yes" ?
|
|
3870
|
+
format: (v) => v === "yes" ? chalk13.yellow(v) : chalk13.dim(v)
|
|
5498
3871
|
},
|
|
5499
3872
|
{ key: "options", label: "Options", maxWidth: 40 }
|
|
5500
3873
|
];
|
|
@@ -5601,13 +3974,13 @@ async function bulkTag(config, tagName, taskIds, action) {
|
|
|
5601
3974
|
}
|
|
5602
3975
|
|
|
5603
3976
|
// src/commands/goals.ts
|
|
5604
|
-
import
|
|
3977
|
+
import chalk14 from "chalk";
|
|
5605
3978
|
function colorProgress(value) {
|
|
5606
3979
|
const num = parseInt(value, 10);
|
|
5607
3980
|
if (isNaN(num)) return value;
|
|
5608
|
-
if (num >= 75) return
|
|
5609
|
-
if (num >= 25) return
|
|
5610
|
-
return
|
|
3981
|
+
if (num >= 75) return chalk14.green(value);
|
|
3982
|
+
if (num >= 25) return chalk14.yellow(value);
|
|
3983
|
+
return chalk14.red(value);
|
|
5611
3984
|
}
|
|
5612
3985
|
var GOAL_COLUMNS = [
|
|
5613
3986
|
{ key: "id", label: "ID", maxWidth: 15 },
|
|
@@ -5701,14 +4074,14 @@ function formatKeyResultsMarkdown(keyResults) {
|
|
|
5701
4074
|
}
|
|
5702
4075
|
|
|
5703
4076
|
// src/commands/task-types.ts
|
|
5704
|
-
import
|
|
4077
|
+
import chalk15 from "chalk";
|
|
5705
4078
|
async function listTaskTypes(config) {
|
|
5706
4079
|
const client = new ClickUpClient(config);
|
|
5707
4080
|
return client.getCustomTaskTypes(config.teamId);
|
|
5708
4081
|
}
|
|
5709
4082
|
function formatTaskTypes(types) {
|
|
5710
4083
|
if (types.length === 0) return "No custom task types";
|
|
5711
|
-
return types.map((t) => `${
|
|
4084
|
+
return types.map((t) => `${chalk15.bold(t.name)} ${chalk15.dim(`(${t.id})`)}`).join("\n");
|
|
5712
4085
|
}
|
|
5713
4086
|
function formatTaskTypesMarkdown(types) {
|
|
5714
4087
|
if (types.length === 0) return "No custom task types";
|
|
@@ -5716,14 +4089,14 @@ function formatTaskTypesMarkdown(types) {
|
|
|
5716
4089
|
}
|
|
5717
4090
|
|
|
5718
4091
|
// src/commands/templates.ts
|
|
5719
|
-
import
|
|
4092
|
+
import chalk16 from "chalk";
|
|
5720
4093
|
async function listTemplates(config) {
|
|
5721
4094
|
const client = new ClickUpClient(config);
|
|
5722
4095
|
return client.getTaskTemplates(config.teamId);
|
|
5723
4096
|
}
|
|
5724
4097
|
function formatTemplates(templates) {
|
|
5725
4098
|
if (templates.length === 0) return "No task templates";
|
|
5726
|
-
return templates.map((t) => `${
|
|
4099
|
+
return templates.map((t) => `${chalk16.bold(t.name)} ${chalk16.dim(`(${t.id})`)}`).join("\n");
|
|
5727
4100
|
}
|
|
5728
4101
|
function formatTemplatesMarkdown(templates) {
|
|
5729
4102
|
if (templates.length === 0) return "No task templates";
|
|
@@ -5731,14 +4104,14 @@ function formatTemplatesMarkdown(templates) {
|
|
|
5731
4104
|
}
|
|
5732
4105
|
|
|
5733
4106
|
// src/commands/list-templates.ts
|
|
5734
|
-
import
|
|
4107
|
+
import chalk17 from "chalk";
|
|
5735
4108
|
async function listListTemplates(config) {
|
|
5736
4109
|
const client = new ClickUpClient(config);
|
|
5737
4110
|
return client.getListTemplates(config.teamId);
|
|
5738
4111
|
}
|
|
5739
4112
|
function formatListTemplates(templates) {
|
|
5740
4113
|
if (templates.length === 0) return "No list templates";
|
|
5741
|
-
return templates.map((t) => `${
|
|
4114
|
+
return templates.map((t) => `${chalk17.bold(t.name)} ${chalk17.dim(`(${t.id})`)}`).join("\n");
|
|
5742
4115
|
}
|
|
5743
4116
|
function formatListTemplatesMarkdown(templates) {
|
|
5744
4117
|
if (templates.length === 0) return "No list templates";
|
|
@@ -5746,14 +4119,14 @@ function formatListTemplatesMarkdown(templates) {
|
|
|
5746
4119
|
}
|
|
5747
4120
|
|
|
5748
4121
|
// src/commands/folder-templates.ts
|
|
5749
|
-
import
|
|
4122
|
+
import chalk18 from "chalk";
|
|
5750
4123
|
async function listFolderTemplates(config) {
|
|
5751
4124
|
const client = new ClickUpClient(config);
|
|
5752
4125
|
return client.getFolderTemplates(config.teamId);
|
|
5753
4126
|
}
|
|
5754
4127
|
function formatFolderTemplates(templates) {
|
|
5755
4128
|
if (templates.length === 0) return "No folder templates";
|
|
5756
|
-
return templates.map((t) => `${
|
|
4129
|
+
return templates.map((t) => `${chalk18.bold(t.name)} ${chalk18.dim(`(${t.id})`)}`).join("\n");
|
|
5757
4130
|
}
|
|
5758
4131
|
function formatFolderTemplatesMarkdown(templates) {
|
|
5759
4132
|
if (templates.length === 0) return "No folder templates";
|
|
@@ -5776,7 +4149,7 @@ async function createListFromTemplate(config, name, opts) {
|
|
|
5776
4149
|
}
|
|
5777
4150
|
|
|
5778
4151
|
// src/commands/views.ts
|
|
5779
|
-
import
|
|
4152
|
+
import chalk19 from "chalk";
|
|
5780
4153
|
async function listViews(config, id, container = "list") {
|
|
5781
4154
|
const client = new ClickUpClient(config);
|
|
5782
4155
|
if (container === "space") return client.getSpaceViews(id);
|
|
@@ -5787,7 +4160,7 @@ async function listViews(config, id, container = "list") {
|
|
|
5787
4160
|
}
|
|
5788
4161
|
function formatViews(views) {
|
|
5789
4162
|
if (views.length === 0) return "No views";
|
|
5790
|
-
return views.map((v) => `${
|
|
4163
|
+
return views.map((v) => `${chalk19.bold(v.name)} ${chalk19.dim(`(${v.id})`)} ${chalk19.dim(v.type)}`).join("\n");
|
|
5791
4164
|
}
|
|
5792
4165
|
function formatViewsMarkdown(views) {
|
|
5793
4166
|
if (views.length === 0) return "No views";
|
|
@@ -5795,20 +4168,20 @@ function formatViewsMarkdown(views) {
|
|
|
5795
4168
|
}
|
|
5796
4169
|
|
|
5797
4170
|
// src/commands/view.ts
|
|
5798
|
-
import
|
|
4171
|
+
import chalk20 from "chalk";
|
|
5799
4172
|
async function getView(config, viewId) {
|
|
5800
4173
|
const client = new ClickUpClient(config);
|
|
5801
4174
|
return client.getView(viewId);
|
|
5802
4175
|
}
|
|
5803
4176
|
function formatView(view) {
|
|
5804
4177
|
const lines = [];
|
|
5805
|
-
lines.push(
|
|
4178
|
+
lines.push(chalk20.bold.underline(view.name));
|
|
5806
4179
|
lines.push("");
|
|
5807
|
-
lines.push(` ${
|
|
5808
|
-
lines.push(` ${
|
|
5809
|
-
if (view.visibility) lines.push(` ${
|
|
5810
|
-
if (view.date_created) lines.push(` ${
|
|
5811
|
-
if (view.protected !== void 0) lines.push(` ${
|
|
4180
|
+
lines.push(` ${chalk20.bold("ID")} ${view.id}`);
|
|
4181
|
+
lines.push(` ${chalk20.bold("Type")} ${view.type}`);
|
|
4182
|
+
if (view.visibility) lines.push(` ${chalk20.bold("Visibility")} ${view.visibility}`);
|
|
4183
|
+
if (view.date_created) lines.push(` ${chalk20.bold("Created")} ${formatDate(view.date_created)}`);
|
|
4184
|
+
if (view.protected !== void 0) lines.push(` ${chalk20.bold("Protected")} ${view.protected}`);
|
|
5812
4185
|
return lines.join("\n");
|
|
5813
4186
|
}
|
|
5814
4187
|
function formatViewMarkdown(view) {
|
|
@@ -5892,8 +4265,8 @@ async function deleteViewCommand(config, viewId, opts) {
|
|
|
5892
4265
|
throw new Error("Destructive operation requires --confirm flag in non-interactive mode");
|
|
5893
4266
|
}
|
|
5894
4267
|
const view = await client.getView(viewId);
|
|
5895
|
-
const { confirm:
|
|
5896
|
-
const confirmed = await
|
|
4268
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
4269
|
+
const confirmed = await confirm2({
|
|
5897
4270
|
message: `Delete view "${view.name}" (${viewId})? This cannot be undone.`,
|
|
5898
4271
|
default: false
|
|
5899
4272
|
});
|
|
@@ -6032,6 +4405,50 @@ async function copyStatusesFrom(client, sourceId) {
|
|
|
6032
4405
|
}
|
|
6033
4406
|
}
|
|
6034
4407
|
|
|
4408
|
+
// src/commands/favorite.ts
|
|
4409
|
+
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
4410
|
+
"sprint-folder",
|
|
4411
|
+
"space",
|
|
4412
|
+
"list",
|
|
4413
|
+
"folder",
|
|
4414
|
+
"view",
|
|
4415
|
+
"task"
|
|
4416
|
+
]);
|
|
4417
|
+
function validateFavoriteType(type) {
|
|
4418
|
+
if (!VALID_TYPES.has(type)) {
|
|
4419
|
+
throw new Error(`Invalid favorite type: "${type}". Valid types: ${[...VALID_TYPES].join(", ")}`);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
function slugify(value) {
|
|
4423
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4424
|
+
}
|
|
4425
|
+
var FAVORITE_COLUMNS = [
|
|
4426
|
+
{ key: "alias", label: "ALIAS", maxWidth: 30 },
|
|
4427
|
+
{ key: "type", label: "TYPE", maxWidth: 20 },
|
|
4428
|
+
{ key: "id", label: "ID", maxWidth: 20 },
|
|
4429
|
+
{ key: "name", label: "NAME", maxWidth: 40 }
|
|
4430
|
+
];
|
|
4431
|
+
function formatFavoritesTable(favorites) {
|
|
4432
|
+
const entries = Object.entries(favorites);
|
|
4433
|
+
if (entries.length === 0) return "No favorites saved";
|
|
4434
|
+
const rows = entries.map(([alias, entry]) => ({
|
|
4435
|
+
alias,
|
|
4436
|
+
type: entry.type,
|
|
4437
|
+
id: entry.id,
|
|
4438
|
+
name: entry.name ?? ""
|
|
4439
|
+
}));
|
|
4440
|
+
return formatTable(rows, FAVORITE_COLUMNS);
|
|
4441
|
+
}
|
|
4442
|
+
function formatFavoritesMarkdown(favorites) {
|
|
4443
|
+
const entries = Object.entries(favorites);
|
|
4444
|
+
if (entries.length === 0) return "No favorites saved";
|
|
4445
|
+
const lines = ["| Alias | Type | ID | Name |", "| --- | --- | --- | --- |"];
|
|
4446
|
+
for (const [alias, entry] of entries) {
|
|
4447
|
+
lines.push(`| ${alias} | ${entry.type} | ${entry.id} | ${entry.name ?? ""} |`);
|
|
4448
|
+
}
|
|
4449
|
+
return lines.join("\n");
|
|
4450
|
+
}
|
|
4451
|
+
|
|
6035
4452
|
// src/index.ts
|
|
6036
4453
|
var require2 = createRequire(import.meta.url);
|
|
6037
4454
|
var { version } = require2("../package.json");
|
|
@@ -6201,6 +4618,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
6201
4618
|
program.command("create").description("Create a new task").option("-l, --list <listId>", "Target list ID (auto-detected from --parent if omitted)").requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description (markdown supported)").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--assignee <userId>", 'Assignee user ID or "me"').option("--tags <tags>", "Comma-separated tag names").option("--custom-item-id <id>", "Custom task type ID (use to create initiatives)").option("--time-estimate <duration>", 'Time estimate (e.g. "2h", "30m", "1h30m")').option("--template <id>", "Create from a task template").option("--json", "Force JSON output even in terminal").action(
|
|
6202
4619
|
wrapAction(async (opts) => {
|
|
6203
4620
|
const config = loadConfig(getProfileName());
|
|
4621
|
+
if (opts.list === "sprint:current") {
|
|
4622
|
+
const { resolveActiveSprintListId } = await import("./sprint-5VYRQK6Z.js");
|
|
4623
|
+
opts.list = await resolveActiveSprintListId(config);
|
|
4624
|
+
}
|
|
6204
4625
|
if (opts.assignee === "me") {
|
|
6205
4626
|
const client = new ClickUpClient(config);
|
|
6206
4627
|
opts.assignee = String(await resolveAssigneeId(client, "me"));
|
|
@@ -6280,16 +4701,23 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
6280
4701
|
}
|
|
6281
4702
|
)
|
|
6282
4703
|
);
|
|
6283
|
-
program.command("comment-delete <commentId>").description("Delete a comment").option("--json", "Force JSON output even in terminal").action(
|
|
6284
|
-
wrapAction(
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
4704
|
+
program.command("comment-delete <commentId>").description("Delete a comment").option("--mine", "Delete one of my comments from the specified task instead of by comment ID").option("--match <text>", "Only match my task comments containing this text").option("--json", "Force JSON output even in terminal").action(
|
|
4705
|
+
wrapAction(
|
|
4706
|
+
async (commentId, opts) => {
|
|
4707
|
+
const config = loadConfig(getProfileName());
|
|
4708
|
+
const result = opts.mine || opts.match ? await deleteCommentByTaskSelection(config, commentId, {
|
|
4709
|
+
mine: opts.mine,
|
|
4710
|
+
match: opts.match
|
|
4711
|
+
}) : (await deleteComment(config, commentId), { commentId });
|
|
4712
|
+
if (shouldOutputJson(opts.json ?? false)) {
|
|
4713
|
+
console.log(JSON.stringify({ success: true, ...result }, null, 2));
|
|
4714
|
+
} else {
|
|
4715
|
+
console.log(
|
|
4716
|
+
result.taskId ? `Deleted comment ${result.commentId} from task ${result.taskId}` : `Deleted comment ${result.commentId}`
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
6291
4719
|
}
|
|
6292
|
-
|
|
4720
|
+
)
|
|
6293
4721
|
);
|
|
6294
4722
|
program.command("replies <commentId>").description("List threaded replies on a comment").option("--json", "Force JSON output even in terminal").action(
|
|
6295
4723
|
wrapAction(async (commentId, opts) => {
|
|
@@ -6324,6 +4752,13 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
6324
4752
|
printActivity(result, opts.json ?? false);
|
|
6325
4753
|
})
|
|
6326
4754
|
);
|
|
4755
|
+
program.command("time-in-status <taskId>").description("Show how long a task has been in each status").option("--json", "Force JSON output even in terminal").action(
|
|
4756
|
+
wrapAction(async (taskId, opts) => {
|
|
4757
|
+
const config = loadConfig(getProfileName());
|
|
4758
|
+
const result = await fetchTimeInStatus(config, taskId);
|
|
4759
|
+
printTimeInStatus(result, opts.json ?? false);
|
|
4760
|
+
})
|
|
4761
|
+
);
|
|
6327
4762
|
program.command("lists <spaceId>").description("List all lists in a space (including lists inside folders)").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
|
|
6328
4763
|
wrapAction(async (spaceId, opts) => {
|
|
6329
4764
|
const config = loadConfig(getProfileName());
|
|
@@ -6504,6 +4939,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
6504
4939
|
program.command("move <taskId>").description("Add or remove a task from a list").option("--to <listId>", "Add task to this list").option("--remove <listId>", "Remove task from this list").option("--json", "Force JSON output even in terminal").action(
|
|
6505
4940
|
wrapAction(async (taskId, opts) => {
|
|
6506
4941
|
const config = loadConfig(getProfileName());
|
|
4942
|
+
if (opts.to === "sprint:current") {
|
|
4943
|
+
const { resolveActiveSprintListId } = await import("./sprint-5VYRQK6Z.js");
|
|
4944
|
+
opts.to = await resolveActiveSprintListId(config);
|
|
4945
|
+
}
|
|
6507
4946
|
const message = await moveTask(config, taskId, opts);
|
|
6508
4947
|
if (shouldOutputJson(opts.json ?? false)) {
|
|
6509
4948
|
console.log(
|
|
@@ -7482,6 +5921,49 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
7482
5921
|
}
|
|
7483
5922
|
})
|
|
7484
5923
|
);
|
|
5924
|
+
const favoriteCmd = program.command("favorite").description("Manage local favorites (sprint folders, spaces, lists, etc.)");
|
|
5925
|
+
favoriteCmd.command("add <type> <id> [alias]").description("Add a favorite (types: sprint-folder, space, list, folder, view, task)").option("-n, --name <name>", "Display name for the favorite").option("--json", "Force JSON output even in terminal").action(
|
|
5926
|
+
wrapAction(
|
|
5927
|
+
async (type, id, alias, opts) => {
|
|
5928
|
+
validateFavoriteType(type);
|
|
5929
|
+
const resolvedAlias = alias ?? slugify(opts.name ?? id);
|
|
5930
|
+
const entry = { type, id, ...opts.name ? { name: opts.name } : {} };
|
|
5931
|
+
saveFavorite(resolvedAlias, entry, getProfileName());
|
|
5932
|
+
if (shouldOutputJson(opts.json ?? false)) {
|
|
5933
|
+
console.log(JSON.stringify({ alias: resolvedAlias, ...entry }, null, 2));
|
|
5934
|
+
} else {
|
|
5935
|
+
console.log(`Added favorite "${resolvedAlias}" (${type} ${id})`);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
)
|
|
5939
|
+
);
|
|
5940
|
+
favoriteCmd.command("remove <alias>").description("Remove a favorite by alias").option("--json", "Force JSON output even in terminal").action(
|
|
5941
|
+
wrapAction(async (alias, opts) => {
|
|
5942
|
+
deleteFavorite(alias, getProfileName());
|
|
5943
|
+
if (shouldOutputJson(opts.json ?? false)) {
|
|
5944
|
+
console.log(JSON.stringify({ success: true, alias }, null, 2));
|
|
5945
|
+
} else {
|
|
5946
|
+
console.log(`Removed favorite "${alias}"`);
|
|
5947
|
+
}
|
|
5948
|
+
})
|
|
5949
|
+
);
|
|
5950
|
+
favoriteCmd.command("list").description("List saved favorites").option("--type <type>", "Filter by entity type").option("--json", "Force JSON output even in terminal").action(
|
|
5951
|
+
wrapAction(async (opts) => {
|
|
5952
|
+
let favorites = getFavorites(getProfileName());
|
|
5953
|
+
if (opts.type) {
|
|
5954
|
+
favorites = Object.fromEntries(
|
|
5955
|
+
Object.entries(favorites).filter(([, entry]) => entry.type === opts.type)
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
if (shouldOutputJson(opts.json ?? false)) {
|
|
5959
|
+
console.log(JSON.stringify(favorites, null, 2));
|
|
5960
|
+
} else if (isTTY()) {
|
|
5961
|
+
console.log(formatFavoritesTable(favorites));
|
|
5962
|
+
} else {
|
|
5963
|
+
console.log(formatFavoritesMarkdown(favorites));
|
|
5964
|
+
}
|
|
5965
|
+
})
|
|
5966
|
+
);
|
|
7485
5967
|
const profileCmd = program.command("profile").description("Manage profiles");
|
|
7486
5968
|
profileCmd.command("list").description("List all profiles").option("--json", "Force JSON output even in terminal").action(
|
|
7487
5969
|
wrapAction(async (opts) => {
|
|
@@ -7500,7 +5982,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
7500
5982
|
);
|
|
7501
5983
|
profileCmd.command("add <name>").description("Add a new profile").action(
|
|
7502
5984
|
wrapAction(async (name) => {
|
|
7503
|
-
const { password: password2, select:
|
|
5985
|
+
const { password: password2, select: select2 } = await import("@inquirer/prompts");
|
|
7504
5986
|
const apiToken = (await password2({ message: "ClickUp API token (pk_...):" })).trim();
|
|
7505
5987
|
if (!apiToken.startsWith("pk_")) throw new Error("Token must start with pk_");
|
|
7506
5988
|
const client = new ClickUpClient({ apiToken });
|
|
@@ -7515,7 +5997,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
7515
5997
|
process.stdout.write(`Workspace: ${teams[0].name}
|
|
7516
5998
|
`);
|
|
7517
5999
|
} else {
|
|
7518
|
-
teamId = await
|
|
6000
|
+
teamId = await select2({
|
|
7519
6001
|
message: "Select workspace:",
|
|
7520
6002
|
choices: teams.map((t) => ({ name: t.name, value: t.id }))
|
|
7521
6003
|
});
|
|
@@ -7553,7 +6035,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
7553
6035
|
);
|
|
7554
6036
|
configCmd.command("path").description("Print config file path").action(
|
|
7555
6037
|
wrapAction(async () => {
|
|
7556
|
-
console.log(
|
|
6038
|
+
console.log(configPath());
|
|
7557
6039
|
})
|
|
7558
6040
|
);
|
|
7559
6041
|
program.command("completion <shell>").description("Output shell completion script (bash, zsh, fish)").action(
|