@krodak/clickup-cli 1.19.4 → 1.20.0
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 +19 -0
- package/dist/index.js +2146 -157
- package/package.json +1 -1
- package/skills/clickup-cli/SKILL.md +60 -3
- package/dist/chunk-HCGKTH6V.js +0 -2063
- package/dist/sprint-NLMMC4RB.js +0 -19
package/dist/index.js
CHANGED
|
@@ -1,55 +1,4 @@
|
|
|
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-HCGKTH6V.js";
|
|
53
2
|
|
|
54
3
|
// src/index.ts
|
|
55
4
|
import { realpathSync as realpathSync2 } from "fs";
|
|
@@ -58,6 +7,1783 @@ import { Command } from "commander";
|
|
|
58
7
|
import { createRequire } from "module";
|
|
59
8
|
import { fileURLToPath } from "url";
|
|
60
9
|
|
|
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 requestV3Array(path) {
|
|
130
|
+
const res = await fetch(`${BASE_URL_V3}${path}`, {
|
|
131
|
+
signal: AbortSignal.timeout(3e4),
|
|
132
|
+
headers: { Authorization: this.apiToken }
|
|
133
|
+
});
|
|
134
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
throw new Error(`ClickUp API error ${res.status}: ${res.statusText}`);
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = await res.json();
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
145
|
+
}
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
let errMsg = res.statusText;
|
|
148
|
+
if (isRecord(parsed)) {
|
|
149
|
+
const raw = parsed.err ?? parsed.error ?? parsed.ECODE;
|
|
150
|
+
if (typeof raw === "string") errMsg = raw;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`ClickUp API error ${res.status}: ${errMsg}`);
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(parsed)) {
|
|
155
|
+
throw new Error("Unexpected API response: expected JSON array");
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
async getMe() {
|
|
160
|
+
if (this.meCache) return this.meCache;
|
|
161
|
+
const data = await this.request(
|
|
162
|
+
"/user"
|
|
163
|
+
);
|
|
164
|
+
const user = expectRecordField(data, "user", "user");
|
|
165
|
+
const timezone = typeof user.timezone === "string" && user.timezone ? user.timezone : void 0;
|
|
166
|
+
this.meCache = {
|
|
167
|
+
id: expectNumericField(user, "id", "user"),
|
|
168
|
+
username: expectStringField(user, "username", "user"),
|
|
169
|
+
...timezone ? { timezone } : {}
|
|
170
|
+
};
|
|
171
|
+
return this.meCache;
|
|
172
|
+
}
|
|
173
|
+
async getUserTimezone() {
|
|
174
|
+
const me = await this.getMe();
|
|
175
|
+
return me.timezone;
|
|
176
|
+
}
|
|
177
|
+
async paginate(buildPath) {
|
|
178
|
+
const allTasks = [];
|
|
179
|
+
let page = 0;
|
|
180
|
+
let lastPage = false;
|
|
181
|
+
while (!lastPage && page < MAX_PAGES) {
|
|
182
|
+
const data = await this.request(buildPath(page));
|
|
183
|
+
const taskPage = expectPaginatedCollectionField(
|
|
184
|
+
data,
|
|
185
|
+
"tasks",
|
|
186
|
+
"task page"
|
|
187
|
+
);
|
|
188
|
+
allTasks.push(...taskPage.items);
|
|
189
|
+
lastPage = taskPage.lastPage;
|
|
190
|
+
page++;
|
|
191
|
+
}
|
|
192
|
+
if (page >= MAX_PAGES && !lastPage) {
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
`Warning: reached maximum page limit (${MAX_PAGES}), results may be incomplete
|
|
195
|
+
`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return allTasks;
|
|
199
|
+
}
|
|
200
|
+
async getMyTasks(teamId, filters = {}) {
|
|
201
|
+
const baseParams = new URLSearchParams({
|
|
202
|
+
subtasks: String(filters.subtasks ?? true)
|
|
203
|
+
});
|
|
204
|
+
if (filters.includeClosed) baseParams.set("include_closed", "true");
|
|
205
|
+
if (!filters.all) {
|
|
206
|
+
const me = await this.getMe();
|
|
207
|
+
baseParams.append("assignees[]", String(me.id));
|
|
208
|
+
}
|
|
209
|
+
if (filters.assignees) {
|
|
210
|
+
for (const id of filters.assignees) baseParams.append("assignees[]", String(id));
|
|
211
|
+
}
|
|
212
|
+
for (const s of filters.statuses ?? []) baseParams.append("statuses[]", s);
|
|
213
|
+
for (const id of filters.listIds ?? []) baseParams.append("list_ids[]", id);
|
|
214
|
+
for (const id of filters.spaceIds ?? []) baseParams.append("space_ids[]", id);
|
|
215
|
+
for (const tag of filters.tags ?? []) baseParams.append("tags[]", tag);
|
|
216
|
+
if (filters.dueDateGt) baseParams.set("due_date_gt", String(filters.dueDateGt));
|
|
217
|
+
if (filters.dueDateLt) baseParams.set("due_date_lt", String(filters.dueDateLt));
|
|
218
|
+
if (filters.dateCreatedGt) baseParams.set("date_created_gt", String(filters.dateCreatedGt));
|
|
219
|
+
if (filters.dateCreatedLt) baseParams.set("date_created_lt", String(filters.dateCreatedLt));
|
|
220
|
+
if (filters.dateUpdatedGt) baseParams.set("date_updated_gt", String(filters.dateUpdatedGt));
|
|
221
|
+
if (filters.dateUpdatedLt) baseParams.set("date_updated_lt", String(filters.dateUpdatedLt));
|
|
222
|
+
if (filters.customFields?.length) {
|
|
223
|
+
baseParams.set("custom_fields", JSON.stringify(filters.customFields));
|
|
224
|
+
}
|
|
225
|
+
return this.paginate((page) => {
|
|
226
|
+
const params = new URLSearchParams(baseParams);
|
|
227
|
+
params.set("page", String(page));
|
|
228
|
+
return `/team/${teamId}/task?${params.toString()}`;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async updateTask(taskId, options) {
|
|
232
|
+
return this.request(this.taskPath(taskId), {
|
|
233
|
+
method: "PUT",
|
|
234
|
+
body: JSON.stringify(options)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async postComment(taskId, commentText, notifyAll) {
|
|
238
|
+
const body = { comment_text: commentText };
|
|
239
|
+
if (notifyAll) body.notify_all = true;
|
|
240
|
+
return this.request(this.taskPath(taskId, "/comment"), {
|
|
241
|
+
method: "POST",
|
|
242
|
+
body: JSON.stringify(body)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async getTaskComments(taskId) {
|
|
246
|
+
const data = await this.request(this.taskPath(taskId, "/comment"));
|
|
247
|
+
return readCollectionField(
|
|
248
|
+
data,
|
|
249
|
+
"comments",
|
|
250
|
+
"task comments"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
async getTasksFromList(listId, params = {}, options = {}) {
|
|
254
|
+
return this.paginate((page) => {
|
|
255
|
+
const base = { subtasks: "true", page: String(page), ...params };
|
|
256
|
+
if (options.includeClosed) base["include_closed"] = "true";
|
|
257
|
+
const qs = new URLSearchParams(base).toString();
|
|
258
|
+
return `/list/${listId}/task?${qs}`;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async getTask(taskId) {
|
|
262
|
+
return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
|
|
263
|
+
}
|
|
264
|
+
async getTimeInStatus(taskId) {
|
|
265
|
+
return this.request(this.taskPath(taskId, "/time_in_status"));
|
|
266
|
+
}
|
|
267
|
+
async createTask(listId, options) {
|
|
268
|
+
return this.request(`/list/${listId}/task`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
body: JSON.stringify(options)
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async getTeams() {
|
|
274
|
+
const data = await this.request("/team");
|
|
275
|
+
return readCollectionField(data, "teams", "teams");
|
|
276
|
+
}
|
|
277
|
+
async getSpaceWithStatuses(spaceId) {
|
|
278
|
+
return this.request(`/space/${spaceId}`);
|
|
279
|
+
}
|
|
280
|
+
async getListWithStatuses(listId) {
|
|
281
|
+
return this.request(`/list/${listId}`);
|
|
282
|
+
}
|
|
283
|
+
async createSpace(teamId, name) {
|
|
284
|
+
return this.request(`/team/${teamId}/space`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
body: JSON.stringify({ name, multiple_assignees: true })
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async getSpaces(teamId) {
|
|
290
|
+
const data = await this.request(`/team/${teamId}/space?archived=false`);
|
|
291
|
+
return readCollectionField(data, "spaces", "spaces");
|
|
292
|
+
}
|
|
293
|
+
async getCustomTaskTypes(teamId) {
|
|
294
|
+
const data = await this.request(
|
|
295
|
+
`/team/${teamId}/custom_item`
|
|
296
|
+
);
|
|
297
|
+
return readCollectionField(
|
|
298
|
+
data,
|
|
299
|
+
"custom_items",
|
|
300
|
+
"custom task types"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
async createList(spaceId, name) {
|
|
304
|
+
return this.request(`/space/${spaceId}/list`, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
body: JSON.stringify({ name })
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async createFolderList(folderId, name) {
|
|
310
|
+
return this.request(`/folder/${folderId}/list`, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: JSON.stringify({ name })
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async updateList(listId, payload) {
|
|
316
|
+
return this.request(`/list/${listId}`, {
|
|
317
|
+
method: "PUT",
|
|
318
|
+
body: JSON.stringify(payload)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async createFolder(spaceId, name) {
|
|
322
|
+
return this.request(`/space/${spaceId}/folder`, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
body: JSON.stringify({ name })
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async getLists(spaceId) {
|
|
328
|
+
const data = await this.request(`/space/${spaceId}/list?archived=false`);
|
|
329
|
+
return readCollectionField(data, "lists", "space lists");
|
|
330
|
+
}
|
|
331
|
+
async getFolders(spaceId) {
|
|
332
|
+
const data = await this.request(
|
|
333
|
+
`/space/${spaceId}/folder?archived=false`
|
|
334
|
+
);
|
|
335
|
+
return readCollectionField(data, "folders", "space folders");
|
|
336
|
+
}
|
|
337
|
+
async getFolderLists(folderId) {
|
|
338
|
+
const data = await this.request(`/folder/${folderId}/list?archived=false`);
|
|
339
|
+
return readCollectionField(data, "lists", "folder lists");
|
|
340
|
+
}
|
|
341
|
+
async getListViews(listId) {
|
|
342
|
+
return this.request(`/list/${listId}/view`);
|
|
343
|
+
}
|
|
344
|
+
async getSpaceViews(spaceId) {
|
|
345
|
+
const data = await this.request(`/space/${spaceId}/view`);
|
|
346
|
+
return readCollectionField(data, "views", "views");
|
|
347
|
+
}
|
|
348
|
+
async getFolderViews(folderId) {
|
|
349
|
+
const data = await this.request(`/folder/${folderId}/view`);
|
|
350
|
+
return readCollectionField(data, "views", "views");
|
|
351
|
+
}
|
|
352
|
+
async getWorkspaceViews(teamId) {
|
|
353
|
+
const data = await this.request(`/team/${teamId}/view`);
|
|
354
|
+
return readCollectionField(data, "views", "views");
|
|
355
|
+
}
|
|
356
|
+
async getViewTasks(viewId) {
|
|
357
|
+
return this.paginate((page) => `/view/${viewId}/task?page=${page}`);
|
|
358
|
+
}
|
|
359
|
+
async getView(viewId) {
|
|
360
|
+
const data = await this.request(`/view/${viewId}`);
|
|
361
|
+
return expectRecordField(data, "view", "view");
|
|
362
|
+
}
|
|
363
|
+
async createListView(listId, payload) {
|
|
364
|
+
const data = await this.request(`/list/${listId}/view`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
body: JSON.stringify(payload)
|
|
367
|
+
});
|
|
368
|
+
return expectRecordField(data, "view", "view");
|
|
369
|
+
}
|
|
370
|
+
async updateView(viewId, payload) {
|
|
371
|
+
const data = await this.request(`/view/${viewId}`, {
|
|
372
|
+
method: "PUT",
|
|
373
|
+
body: JSON.stringify(payload)
|
|
374
|
+
});
|
|
375
|
+
return expectRecordField(data, "view", "view");
|
|
376
|
+
}
|
|
377
|
+
async deleteView(viewId) {
|
|
378
|
+
await this.request(`/view/${viewId}`, { method: "DELETE" });
|
|
379
|
+
}
|
|
380
|
+
async getListTemplates(teamId) {
|
|
381
|
+
const data = await this.request(`/team/${teamId}/list_template`);
|
|
382
|
+
return readCollectionField(
|
|
383
|
+
data,
|
|
384
|
+
"templates",
|
|
385
|
+
"list templates"
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
async getFolderTemplates(teamId) {
|
|
389
|
+
const data = await this.request(
|
|
390
|
+
`/team/${teamId}/folder_template`
|
|
391
|
+
);
|
|
392
|
+
return readCollectionField(
|
|
393
|
+
data,
|
|
394
|
+
"templates",
|
|
395
|
+
"folder templates"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
async createListFromTemplate(containerId, templateId, name, containerType) {
|
|
399
|
+
return this.request(
|
|
400
|
+
`/${containerType}/${containerId}/list_template/${templateId}`,
|
|
401
|
+
{ method: "POST", body: JSON.stringify({ name }) }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
async addTaskToList(taskId, listId) {
|
|
405
|
+
await this.request(`/list/${listId}/task/${taskId}`, { method: "POST" });
|
|
406
|
+
}
|
|
407
|
+
async removeTaskFromList(taskId, listId) {
|
|
408
|
+
await this.request(`/list/${listId}/task/${taskId}`, { method: "DELETE" });
|
|
409
|
+
}
|
|
410
|
+
async setCustomFieldValue(taskId, fieldId, value) {
|
|
411
|
+
await this.request(this.taskPath(taskId, `/field/${fieldId}`), {
|
|
412
|
+
method: "POST",
|
|
413
|
+
body: JSON.stringify({ value })
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async removeCustomFieldValue(taskId, fieldId) {
|
|
417
|
+
await this.request(this.taskPath(taskId, `/field/${fieldId}`), { method: "DELETE" });
|
|
418
|
+
}
|
|
419
|
+
async deleteTask(taskId) {
|
|
420
|
+
await this.request(this.taskPath(taskId), { method: "DELETE" });
|
|
421
|
+
}
|
|
422
|
+
async addTagToTask(taskId, tagName) {
|
|
423
|
+
await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
|
|
424
|
+
method: "POST"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async removeTagFromTask(taskId, tagName) {
|
|
428
|
+
await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
|
|
429
|
+
method: "DELETE"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async addDependency(taskId, opts) {
|
|
433
|
+
const body = {};
|
|
434
|
+
if (opts.dependsOn) body.depends_on = opts.dependsOn;
|
|
435
|
+
if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
|
|
436
|
+
await this.request(this.taskPath(taskId, "/dependency"), {
|
|
437
|
+
method: "POST",
|
|
438
|
+
body: JSON.stringify(body)
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async deleteDependency(taskId, opts) {
|
|
442
|
+
const params = new URLSearchParams();
|
|
443
|
+
if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
|
|
444
|
+
if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
|
|
445
|
+
await this.request(this.taskPath(taskId, `/dependency?${params.toString()}`), {
|
|
446
|
+
method: "DELETE"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
async updateComment(commentId, text, resolved) {
|
|
450
|
+
const body = { comment_text: text };
|
|
451
|
+
if (resolved !== void 0) body.resolved = resolved;
|
|
452
|
+
await this.request(`/comment/${commentId}`, {
|
|
453
|
+
method: "PUT",
|
|
454
|
+
body: JSON.stringify(body)
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
async deleteComment(commentId) {
|
|
458
|
+
await this.request(`/comment/${commentId}`, { method: "DELETE" });
|
|
459
|
+
}
|
|
460
|
+
async getThreadedComments(commentId) {
|
|
461
|
+
const data = await this.request(`/comment/${commentId}/reply`);
|
|
462
|
+
return readCollectionField(
|
|
463
|
+
data,
|
|
464
|
+
"comments",
|
|
465
|
+
"threaded comments"
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
async createThreadedComment(commentId, text, notifyAll) {
|
|
469
|
+
const body = { comment_text: text };
|
|
470
|
+
if (notifyAll) body.notify_all = true;
|
|
471
|
+
await this.request(`/comment/${commentId}/reply`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
body: JSON.stringify(body)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async addTaskLink(taskId, linksTo) {
|
|
477
|
+
await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
|
|
478
|
+
method: "POST"
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
async deleteTaskLink(taskId, linksTo) {
|
|
482
|
+
await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
|
|
483
|
+
method: "DELETE"
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
async getListCustomFields(listId) {
|
|
487
|
+
const data = await this.request(`/list/${listId}/field`);
|
|
488
|
+
return readCollectionField(
|
|
489
|
+
data,
|
|
490
|
+
"fields",
|
|
491
|
+
"list custom fields"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
async createChecklist(taskId, name) {
|
|
495
|
+
const data = await this.request(this.taskPath(taskId, "/checklist"), {
|
|
496
|
+
method: "POST",
|
|
497
|
+
body: JSON.stringify({ name })
|
|
498
|
+
});
|
|
499
|
+
return expectRecordField(
|
|
500
|
+
data,
|
|
501
|
+
"checklist",
|
|
502
|
+
"checklist"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
async deleteChecklist(checklistId) {
|
|
506
|
+
await this.request(`/checklist/${checklistId}`, { method: "DELETE" });
|
|
507
|
+
}
|
|
508
|
+
async createChecklistItem(checklistId, name) {
|
|
509
|
+
const data = await this.request(
|
|
510
|
+
`/checklist/${checklistId}/checklist_item`,
|
|
511
|
+
{ method: "POST", body: JSON.stringify({ name }) }
|
|
512
|
+
);
|
|
513
|
+
return expectRecordField(
|
|
514
|
+
data,
|
|
515
|
+
"checklist",
|
|
516
|
+
"checklist"
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
async editChecklistItem(checklistId, checklistItemId, updates) {
|
|
520
|
+
const data = await this.request(
|
|
521
|
+
`/checklist/${checklistId}/checklist_item/${checklistItemId}`,
|
|
522
|
+
{ method: "PUT", body: JSON.stringify(updates) }
|
|
523
|
+
);
|
|
524
|
+
return expectRecordField(
|
|
525
|
+
data,
|
|
526
|
+
"checklist",
|
|
527
|
+
"checklist"
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
async deleteChecklistItem(checklistId, checklistItemId) {
|
|
531
|
+
await this.request(
|
|
532
|
+
`/checklist/${checklistId}/checklist_item/${checklistItemId}`,
|
|
533
|
+
{ method: "DELETE" }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
async startTimeEntry(teamId, taskId, description) {
|
|
537
|
+
const body = {
|
|
538
|
+
tid: taskId,
|
|
539
|
+
start: Date.now(),
|
|
540
|
+
duration: -1
|
|
541
|
+
};
|
|
542
|
+
if (description) body.description = description;
|
|
543
|
+
const data = await this.request(
|
|
544
|
+
`/team/${teamId}/time_entries/start${this.customIdQueryParams(taskId)}`,
|
|
545
|
+
{
|
|
546
|
+
method: "POST",
|
|
547
|
+
body: JSON.stringify(body)
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
return data.data;
|
|
551
|
+
}
|
|
552
|
+
async stopTimeEntry(teamId) {
|
|
553
|
+
const data = await this.request(`/team/${teamId}/time_entries/stop`, {
|
|
554
|
+
method: "POST"
|
|
555
|
+
});
|
|
556
|
+
return data.data;
|
|
557
|
+
}
|
|
558
|
+
async getRunningTimeEntry(teamId) {
|
|
559
|
+
const data = await this.request(
|
|
560
|
+
`/team/${teamId}/time_entries/current`
|
|
561
|
+
);
|
|
562
|
+
return data.data ?? null;
|
|
563
|
+
}
|
|
564
|
+
async createTimeEntry(teamId, taskId, duration, opts) {
|
|
565
|
+
const start = opts?.start ?? Date.now() - duration;
|
|
566
|
+
const body = {
|
|
567
|
+
tid: taskId,
|
|
568
|
+
start,
|
|
569
|
+
duration
|
|
570
|
+
};
|
|
571
|
+
if (opts?.description) body.description = opts.description;
|
|
572
|
+
const data = await this.request(
|
|
573
|
+
`/team/${teamId}/time_entries${this.customIdQueryParams(taskId)}`,
|
|
574
|
+
{
|
|
575
|
+
method: "POST",
|
|
576
|
+
body: JSON.stringify(body)
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
return data.data;
|
|
580
|
+
}
|
|
581
|
+
async getTimeEntries(teamId, opts) {
|
|
582
|
+
const params = new URLSearchParams();
|
|
583
|
+
if (opts?.startDate != null) params.set("start_date", String(opts.startDate));
|
|
584
|
+
if (opts?.endDate != null) params.set("end_date", String(opts.endDate));
|
|
585
|
+
if (opts?.spaceId) params.set("space_id", opts.spaceId);
|
|
586
|
+
if (opts?.listId) params.set("list_id", opts.listId);
|
|
587
|
+
if (opts?.assigneeId) params.set("assignee", opts.assigneeId);
|
|
588
|
+
const query = params.toString();
|
|
589
|
+
const url = `/team/${teamId}/time_entries${query ? `?${query}` : ""}`;
|
|
590
|
+
const data = await this.request(url);
|
|
591
|
+
const entries = readCollectionField(
|
|
592
|
+
data,
|
|
593
|
+
"data",
|
|
594
|
+
"time entries"
|
|
595
|
+
);
|
|
596
|
+
if (opts?.taskId) {
|
|
597
|
+
return entries.filter((e) => e.task?.id === opts.taskId);
|
|
598
|
+
}
|
|
599
|
+
return entries;
|
|
600
|
+
}
|
|
601
|
+
async updateTimeEntry(teamId, timeEntryId, updates) {
|
|
602
|
+
const data = await this.request(
|
|
603
|
+
`/team/${teamId}/time_entries/${timeEntryId}`,
|
|
604
|
+
{ method: "PUT", body: JSON.stringify(updates) }
|
|
605
|
+
);
|
|
606
|
+
return data.data;
|
|
607
|
+
}
|
|
608
|
+
async getSpaceTags(spaceId) {
|
|
609
|
+
const data = await this.request(`/space/${spaceId}/tag`);
|
|
610
|
+
return readCollectionField(data, "tags", "space tags");
|
|
611
|
+
}
|
|
612
|
+
async createSpaceTag(spaceId, name, fg, bg) {
|
|
613
|
+
await this.request(`/space/${spaceId}/tag`, {
|
|
614
|
+
method: "POST",
|
|
615
|
+
body: JSON.stringify({
|
|
616
|
+
tag: { name, tag_fg: fg ?? "#000000", tag_bg: bg ?? "#04A9F4" }
|
|
617
|
+
})
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async deleteSpaceTag(spaceId, tagName) {
|
|
621
|
+
await this.request(
|
|
622
|
+
`/space/${spaceId}/tag/${encodeURIComponent(tagName)}`,
|
|
623
|
+
{ method: "DELETE" }
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
async getWorkspaceMembers(teamId) {
|
|
627
|
+
const data = await this.request("/team");
|
|
628
|
+
const team = readCollectionField(
|
|
629
|
+
data,
|
|
630
|
+
"teams",
|
|
631
|
+
"workspace members"
|
|
632
|
+
).find((t) => t.id === teamId);
|
|
633
|
+
return team?.members?.map((m) => m.user) ?? [];
|
|
634
|
+
}
|
|
635
|
+
async deleteTimeEntry(teamId, timeEntryId) {
|
|
636
|
+
await this.request(`/team/${teamId}/time_entries/${timeEntryId}`, {
|
|
637
|
+
method: "DELETE"
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
async createTaskAttachment(taskId, filePath) {
|
|
641
|
+
const { readFile } = await import("fs/promises");
|
|
642
|
+
const { basename: basename2 } = await import("path");
|
|
643
|
+
const fileBuffer = await readFile(filePath);
|
|
644
|
+
const fileName = basename2(filePath);
|
|
645
|
+
const formData = new FormData();
|
|
646
|
+
formData.append("attachment", new Blob([fileBuffer]), fileName);
|
|
647
|
+
const res = await fetch(`${BASE_URL}${this.taskPath(taskId, "/attachment")}`, {
|
|
648
|
+
method: "POST",
|
|
649
|
+
headers: { Authorization: this.apiToken },
|
|
650
|
+
body: formData,
|
|
651
|
+
signal: AbortSignal.timeout(6e4)
|
|
652
|
+
});
|
|
653
|
+
if (!res.ok) {
|
|
654
|
+
let msg;
|
|
655
|
+
try {
|
|
656
|
+
const data2 = await res.json();
|
|
657
|
+
msg = data2.err ?? `HTTP ${res.status}`;
|
|
658
|
+
} catch {
|
|
659
|
+
msg = `HTTP ${res.status}`;
|
|
660
|
+
}
|
|
661
|
+
throw new Error(`ClickUp API error ${res.status}: ${msg}`);
|
|
662
|
+
}
|
|
663
|
+
let data;
|
|
664
|
+
try {
|
|
665
|
+
data = await res.json();
|
|
666
|
+
} catch {
|
|
667
|
+
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
668
|
+
}
|
|
669
|
+
return data;
|
|
670
|
+
}
|
|
671
|
+
async getDocs(workspaceId) {
|
|
672
|
+
const data = await this.requestV3(`/workspaces/${workspaceId}/docs`);
|
|
673
|
+
return readCollectionField(data, "docs", "docs");
|
|
674
|
+
}
|
|
675
|
+
async getDocPage(workspaceId, docId, pageId) {
|
|
676
|
+
return this.requestV3(
|
|
677
|
+
`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}?content_format=text/md`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
async createDoc(workspaceId, title, content, parentId) {
|
|
681
|
+
const body = { title };
|
|
682
|
+
if (content) body.content = content;
|
|
683
|
+
if (parentId) {
|
|
684
|
+
body.parent_id = parentId;
|
|
685
|
+
body.parent_type = "doc";
|
|
686
|
+
}
|
|
687
|
+
return this.requestV3(`/workspaces/${workspaceId}/docs`, {
|
|
688
|
+
method: "POST",
|
|
689
|
+
body: JSON.stringify(body)
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
async createDocPage(workspaceId, docId, name, content, parentPageId) {
|
|
693
|
+
const body = { name, content_format: "text/md" };
|
|
694
|
+
if (content) body.content = content;
|
|
695
|
+
if (parentPageId) body.parent_page_id = parentPageId;
|
|
696
|
+
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages`, {
|
|
697
|
+
method: "POST",
|
|
698
|
+
body: JSON.stringify(body)
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async editDocPage(workspaceId, docId, pageId, updates) {
|
|
702
|
+
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`, {
|
|
703
|
+
method: "PUT",
|
|
704
|
+
body: JSON.stringify(updates)
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async getDoc(workspaceId, docId) {
|
|
708
|
+
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`);
|
|
709
|
+
}
|
|
710
|
+
async getDocPageListing(workspaceId, docId) {
|
|
711
|
+
return this.requestV3Array(`/workspaces/${workspaceId}/docs/${docId}/pages`);
|
|
712
|
+
}
|
|
713
|
+
async getDocPages(workspaceId, docId) {
|
|
714
|
+
return this.requestV3Array(
|
|
715
|
+
`/workspaces/${workspaceId}/docs/${docId}/pages?content_format=text/md`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
async getGoals(teamId) {
|
|
719
|
+
const data = await this.request(`/team/${teamId}/goal`);
|
|
720
|
+
return readCollectionField(data, "goals", "goals");
|
|
721
|
+
}
|
|
722
|
+
async createGoal(teamId, name, opts) {
|
|
723
|
+
const body = { name, multiple_owners: true };
|
|
724
|
+
if (opts?.description) body.description = opts.description;
|
|
725
|
+
if (opts?.dueDate != null) body.due_date = opts.dueDate;
|
|
726
|
+
if (opts?.color) body.color = opts.color;
|
|
727
|
+
const data = await this.request(`/team/${teamId}/goal`, {
|
|
728
|
+
method: "POST",
|
|
729
|
+
body: JSON.stringify(body)
|
|
730
|
+
});
|
|
731
|
+
return data.goal;
|
|
732
|
+
}
|
|
733
|
+
async updateGoal(goalId, updates) {
|
|
734
|
+
const data = await this.request(`/goal/${goalId}`, {
|
|
735
|
+
method: "PUT",
|
|
736
|
+
body: JSON.stringify(updates)
|
|
737
|
+
});
|
|
738
|
+
return data.goal;
|
|
739
|
+
}
|
|
740
|
+
async getKeyResults(goalId) {
|
|
741
|
+
const data = await this.request(`/goal/${goalId}`);
|
|
742
|
+
return data.goal?.key_results ?? [];
|
|
743
|
+
}
|
|
744
|
+
async createKeyResult(goalId, name, type, stepsEnd) {
|
|
745
|
+
const data = await this.request(`/goal/${goalId}/key_result`, {
|
|
746
|
+
method: "POST",
|
|
747
|
+
body: JSON.stringify({
|
|
748
|
+
name,
|
|
749
|
+
type,
|
|
750
|
+
steps_start: 0,
|
|
751
|
+
steps_end: stepsEnd,
|
|
752
|
+
unit: type === "number" ? "items" : "%"
|
|
753
|
+
})
|
|
754
|
+
});
|
|
755
|
+
return data.key_result;
|
|
756
|
+
}
|
|
757
|
+
async updateKeyResult(keyResultId, updates) {
|
|
758
|
+
const data = await this.request(`/key_result/${keyResultId}`, {
|
|
759
|
+
method: "PUT",
|
|
760
|
+
body: JSON.stringify(updates)
|
|
761
|
+
});
|
|
762
|
+
return data.key_result;
|
|
763
|
+
}
|
|
764
|
+
async deleteGoal(goalId) {
|
|
765
|
+
await this.request(`/goal/${goalId}`, { method: "DELETE" });
|
|
766
|
+
}
|
|
767
|
+
async deleteKeyResult(keyResultId) {
|
|
768
|
+
await this.request(`/key_result/${keyResultId}`, { method: "DELETE" });
|
|
769
|
+
}
|
|
770
|
+
async deleteDoc(workspaceId, docId) {
|
|
771
|
+
await this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`, {
|
|
772
|
+
method: "DELETE"
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async deleteDocPage(workspaceId, docId, pageId) {
|
|
776
|
+
await this.requestV3(
|
|
777
|
+
`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`,
|
|
778
|
+
{ method: "DELETE" }
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
async updateSpaceTag(spaceId, tagName, updates) {
|
|
782
|
+
await this.request(
|
|
783
|
+
`/space/${spaceId}/tag/${encodeURIComponent(tagName)}`,
|
|
784
|
+
{
|
|
785
|
+
method: "PUT",
|
|
786
|
+
body: JSON.stringify({
|
|
787
|
+
tag: {
|
|
788
|
+
name: updates.name,
|
|
789
|
+
tag_fg: updates.tag_fg ?? "#000000",
|
|
790
|
+
tag_bg: updates.tag_bg ?? "#04A9F4"
|
|
791
|
+
}
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
async getTaskTemplates(teamId) {
|
|
797
|
+
const data = await this.request(
|
|
798
|
+
`/team/${teamId}/taskTemplate?page=0`
|
|
799
|
+
);
|
|
800
|
+
return readCollectionField(
|
|
801
|
+
data,
|
|
802
|
+
"templates",
|
|
803
|
+
"task templates"
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
async createTaskFromTemplate(listId, templateId, name) {
|
|
807
|
+
return this.request(`/list/${listId}/taskTemplate/${templateId}`, {
|
|
808
|
+
method: "POST",
|
|
809
|
+
body: JSON.stringify({ name })
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async createCustomField(teamId, name, type, opts) {
|
|
813
|
+
const typeConfig = {};
|
|
814
|
+
if (opts?.options?.length) {
|
|
815
|
+
typeConfig.options = opts.options.map((optName, i) => ({
|
|
816
|
+
name: optName,
|
|
817
|
+
orderindex: i
|
|
818
|
+
}));
|
|
819
|
+
}
|
|
820
|
+
const body = {
|
|
821
|
+
name,
|
|
822
|
+
type,
|
|
823
|
+
type_config: typeConfig,
|
|
824
|
+
description: opts?.description ?? "",
|
|
825
|
+
required: opts?.required ?? false,
|
|
826
|
+
pinned: false,
|
|
827
|
+
hide_from_guests: false,
|
|
828
|
+
required_on_subtasks: false,
|
|
829
|
+
private: false,
|
|
830
|
+
permission_level: null,
|
|
831
|
+
members: [],
|
|
832
|
+
groups: []
|
|
833
|
+
};
|
|
834
|
+
const data = await this.request(
|
|
835
|
+
`/field?workspace_id=${teamId}`,
|
|
836
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
837
|
+
);
|
|
838
|
+
return data.data;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// src/config.ts
|
|
843
|
+
import fs from "fs";
|
|
844
|
+
import { homedir } from "os";
|
|
845
|
+
import { join } from "path";
|
|
846
|
+
function isRecord2(value) {
|
|
847
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
848
|
+
}
|
|
849
|
+
function readConfigString(parsed, key, path, strict) {
|
|
850
|
+
const value = parsed[key];
|
|
851
|
+
if (value === void 0) return void 0;
|
|
852
|
+
if (typeof value !== "string") {
|
|
853
|
+
if (strict) {
|
|
854
|
+
throw new Error(`Config field ${key} must be a string in ${path}.`);
|
|
855
|
+
}
|
|
856
|
+
return void 0;
|
|
857
|
+
}
|
|
858
|
+
const trimmed = value.trim();
|
|
859
|
+
return trimmed || void 0;
|
|
860
|
+
}
|
|
861
|
+
function parseConfigFile(raw, path, strictFields, strictRoot = strictFields) {
|
|
862
|
+
let parsed;
|
|
863
|
+
try {
|
|
864
|
+
parsed = JSON.parse(raw);
|
|
865
|
+
} catch {
|
|
866
|
+
if (strictRoot) {
|
|
867
|
+
throw new Error(`Config file at ${path} contains invalid JSON. Please check the file syntax.`);
|
|
868
|
+
}
|
|
869
|
+
return {};
|
|
870
|
+
}
|
|
871
|
+
if (!isRecord2(parsed)) {
|
|
872
|
+
if (strictRoot) {
|
|
873
|
+
throw new Error(`Config file at ${path} must contain a JSON object.`);
|
|
874
|
+
}
|
|
875
|
+
return {};
|
|
876
|
+
}
|
|
877
|
+
const apiToken = readConfigString(parsed, "apiToken", path, strictFields);
|
|
878
|
+
const teamId = readConfigString(parsed, "teamId", path, strictFields);
|
|
879
|
+
const sprintFolderId = readConfigString(parsed, "sprintFolderId", path, strictFields);
|
|
880
|
+
return {
|
|
881
|
+
...apiToken ? { apiToken } : {},
|
|
882
|
+
...teamId ? { teamId } : {},
|
|
883
|
+
...sprintFolderId ? { sprintFolderId } : {}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function trimConfigValue(value) {
|
|
887
|
+
const trimmed = value?.trim();
|
|
888
|
+
return trimmed || void 0;
|
|
889
|
+
}
|
|
890
|
+
function configDir() {
|
|
891
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
892
|
+
if (xdg) return join(xdg, "cup");
|
|
893
|
+
return join(homedir(), ".config", "cup");
|
|
894
|
+
}
|
|
895
|
+
function legacyConfigDir() {
|
|
896
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
897
|
+
if (xdg) return join(xdg, "cu");
|
|
898
|
+
return join(homedir(), ".config", "cu");
|
|
899
|
+
}
|
|
900
|
+
var migrationChecked = false;
|
|
901
|
+
function migrateFromLegacy() {
|
|
902
|
+
if (migrationChecked) return;
|
|
903
|
+
migrationChecked = true;
|
|
904
|
+
const legacy = legacyConfigDir();
|
|
905
|
+
const current = configDir();
|
|
906
|
+
if (fs.existsSync(join(legacy, "config.json")) && !fs.existsSync(join(current, "config.json"))) {
|
|
907
|
+
fs.mkdirSync(current, { recursive: true, mode: 448 });
|
|
908
|
+
fs.copyFileSync(join(legacy, "config.json"), join(current, "config.json"));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function configPath() {
|
|
912
|
+
return join(configDir(), "config.json");
|
|
913
|
+
}
|
|
914
|
+
function migrateToMultiProfile(parsed, filePath) {
|
|
915
|
+
if (typeof parsed.apiToken === "string" && !parsed.profiles) {
|
|
916
|
+
const profile = {};
|
|
917
|
+
const token = trimConfigValue(parsed.apiToken);
|
|
918
|
+
if (token) profile.apiToken = token;
|
|
919
|
+
const team = typeof parsed.teamId === "string" ? trimConfigValue(parsed.teamId) : void 0;
|
|
920
|
+
if (team) profile.teamId = team;
|
|
921
|
+
const sprint = typeof parsed.sprintFolderId === "string" ? trimConfigValue(parsed.sprintFolderId) : void 0;
|
|
922
|
+
if (sprint) profile.sprintFolderId = sprint;
|
|
923
|
+
const migrated = {
|
|
924
|
+
defaultProfile: "default",
|
|
925
|
+
profiles: { default: profile }
|
|
926
|
+
};
|
|
927
|
+
const dir = configDir();
|
|
928
|
+
if (!fs.existsSync(dir)) {
|
|
929
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
930
|
+
}
|
|
931
|
+
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(migrated, null, 2) + "\n", {
|
|
932
|
+
encoding: "utf-8",
|
|
933
|
+
mode: 384
|
|
934
|
+
});
|
|
935
|
+
return migrated;
|
|
936
|
+
}
|
|
937
|
+
if (isRecord2(parsed.profiles)) {
|
|
938
|
+
const profiles = {};
|
|
939
|
+
for (const [name, value] of Object.entries(parsed.profiles)) {
|
|
940
|
+
if (isRecord2(value)) {
|
|
941
|
+
const p = {};
|
|
942
|
+
if (typeof value.apiToken === "string" && value.apiToken.trim())
|
|
943
|
+
p.apiToken = value.apiToken.trim();
|
|
944
|
+
if (typeof value.teamId === "string" && value.teamId.trim()) p.teamId = value.teamId.trim();
|
|
945
|
+
if (typeof value.sprintFolderId === "string" && value.sprintFolderId.trim())
|
|
946
|
+
p.sprintFolderId = value.sprintFolderId.trim();
|
|
947
|
+
if (isRecord2(value.filters)) p.filters = value.filters;
|
|
948
|
+
if (isRecord2(value.favorites)) p.favorites = value.favorites;
|
|
949
|
+
profiles[name] = p;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
defaultProfile: typeof parsed.defaultProfile === "string" ? parsed.defaultProfile : "",
|
|
954
|
+
profiles
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
throw new Error(`Config file at ${filePath} has unrecognized format.`);
|
|
958
|
+
}
|
|
959
|
+
function parseRawConfig(filePath) {
|
|
960
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
961
|
+
let parsed;
|
|
962
|
+
try {
|
|
963
|
+
parsed = JSON.parse(raw);
|
|
964
|
+
} catch {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`Config file at ${filePath} contains invalid JSON. Please check the file syntax.`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (!isRecord2(parsed)) {
|
|
970
|
+
throw new Error(`Config file at ${filePath} must contain a JSON object.`);
|
|
971
|
+
}
|
|
972
|
+
return { parsed, raw };
|
|
973
|
+
}
|
|
974
|
+
function isOldFormat(parsed) {
|
|
975
|
+
return typeof parsed.apiToken === "string" && !parsed.profiles;
|
|
976
|
+
}
|
|
977
|
+
function loadConfig(profileName) {
|
|
978
|
+
migrateFromLegacy();
|
|
979
|
+
const envToken = process.env.CU_API_TOKEN?.trim();
|
|
980
|
+
const envTeamId = process.env.CU_TEAM_ID?.trim();
|
|
981
|
+
if (envToken && envTeamId) {
|
|
982
|
+
if (!envToken.startsWith("pk_")) {
|
|
983
|
+
throw new Error("CU_API_TOKEN must start with pk_.");
|
|
984
|
+
}
|
|
985
|
+
return { apiToken: envToken, teamId: envTeamId };
|
|
986
|
+
}
|
|
987
|
+
const path = configPath();
|
|
988
|
+
if (!fs.existsSync(path)) {
|
|
989
|
+
if (envToken || envTeamId) {
|
|
990
|
+
throw new Error("Both CU_API_TOKEN and CU_TEAM_ID must be set, or run: cup init");
|
|
991
|
+
}
|
|
992
|
+
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
|
|
993
|
+
}
|
|
994
|
+
const { parsed } = parseRawConfig(path);
|
|
995
|
+
if (isOldFormat(parsed)) {
|
|
996
|
+
const fileConfig = parseConfigFile(JSON.stringify(parsed), path, true);
|
|
997
|
+
const apiToken2 = envToken ?? fileConfig.apiToken;
|
|
998
|
+
if (!apiToken2) {
|
|
999
|
+
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
|
|
1000
|
+
}
|
|
1001
|
+
if (!apiToken2.startsWith("pk_")) {
|
|
1002
|
+
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
1003
|
+
}
|
|
1004
|
+
const teamId2 = envTeamId ?? fileConfig.teamId;
|
|
1005
|
+
if (!teamId2) {
|
|
1006
|
+
throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cup init");
|
|
1007
|
+
}
|
|
1008
|
+
migrateToMultiProfile(parsed, path);
|
|
1009
|
+
return {
|
|
1010
|
+
apiToken: apiToken2,
|
|
1011
|
+
teamId: teamId2,
|
|
1012
|
+
...fileConfig.sprintFolderId ? { sprintFolderId: fileConfig.sprintFolderId } : {}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
const multi = loadMultiProfileConfig();
|
|
1016
|
+
const resolvedProfile = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1017
|
+
if (!resolvedProfile) {
|
|
1018
|
+
throw new Error("No default profile set. Run: cup profile use <name>");
|
|
1019
|
+
}
|
|
1020
|
+
const profile = multi.profiles[resolvedProfile];
|
|
1021
|
+
if (!profile) {
|
|
1022
|
+
const available = Object.keys(multi.profiles).join(", ");
|
|
1023
|
+
throw new Error(`Profile "${resolvedProfile}" not found. Available: ${available}`);
|
|
1024
|
+
}
|
|
1025
|
+
const apiToken = envToken ?? profile.apiToken?.trim();
|
|
1026
|
+
if (!apiToken) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`Profile "${resolvedProfile}" missing apiToken. Run: cup profile add ${resolvedProfile}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (!apiToken.startsWith("pk_")) {
|
|
1032
|
+
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
1033
|
+
}
|
|
1034
|
+
const teamId = envTeamId ?? profile.teamId?.trim();
|
|
1035
|
+
if (!teamId) {
|
|
1036
|
+
throw new Error(`Profile "${resolvedProfile}" missing teamId.`);
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
apiToken,
|
|
1040
|
+
teamId,
|
|
1041
|
+
...profile.sprintFolderId ? { sprintFolderId: profile.sprintFolderId } : {}
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function loadMultiProfileConfig() {
|
|
1045
|
+
migrateFromLegacy();
|
|
1046
|
+
const path = configPath();
|
|
1047
|
+
if (!fs.existsSync(path)) {
|
|
1048
|
+
return { defaultProfile: "", profiles: {} };
|
|
1049
|
+
}
|
|
1050
|
+
let parsed;
|
|
1051
|
+
try {
|
|
1052
|
+
const raw = fs.readFileSync(path, "utf-8");
|
|
1053
|
+
parsed = JSON.parse(raw);
|
|
1054
|
+
} catch {
|
|
1055
|
+
return { defaultProfile: "", profiles: {} };
|
|
1056
|
+
}
|
|
1057
|
+
if (!isRecord2(parsed)) return { defaultProfile: "", profiles: {} };
|
|
1058
|
+
if (isOldFormat(parsed)) {
|
|
1059
|
+
return migrateToMultiProfile(parsed, path);
|
|
1060
|
+
}
|
|
1061
|
+
return migrateToMultiProfile(parsed, path);
|
|
1062
|
+
}
|
|
1063
|
+
function saveMultiProfileConfig(config) {
|
|
1064
|
+
const dir = configDir();
|
|
1065
|
+
if (!fs.existsSync(dir)) {
|
|
1066
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1067
|
+
}
|
|
1068
|
+
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", {
|
|
1069
|
+
encoding: "utf-8",
|
|
1070
|
+
mode: 384
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function addProfile(name, profile) {
|
|
1074
|
+
const multi = loadMultiProfileConfig();
|
|
1075
|
+
multi.profiles[name] = profile;
|
|
1076
|
+
if (!multi.defaultProfile) multi.defaultProfile = name;
|
|
1077
|
+
saveMultiProfileConfig(multi);
|
|
1078
|
+
}
|
|
1079
|
+
function removeProfile(name) {
|
|
1080
|
+
const multi = loadMultiProfileConfig();
|
|
1081
|
+
if (!multi.profiles[name]) {
|
|
1082
|
+
throw new Error(`Profile "${name}" not found.`);
|
|
1083
|
+
}
|
|
1084
|
+
const keys = Object.keys(multi.profiles);
|
|
1085
|
+
if (keys.length <= 1) {
|
|
1086
|
+
throw new Error("Cannot remove the last profile.");
|
|
1087
|
+
}
|
|
1088
|
+
delete multi.profiles[name];
|
|
1089
|
+
if (multi.defaultProfile === name) {
|
|
1090
|
+
multi.defaultProfile = Object.keys(multi.profiles)[0] ?? "";
|
|
1091
|
+
}
|
|
1092
|
+
saveMultiProfileConfig(multi);
|
|
1093
|
+
}
|
|
1094
|
+
function setDefaultProfile(name) {
|
|
1095
|
+
const multi = loadMultiProfileConfig();
|
|
1096
|
+
if (!multi.profiles[name]) {
|
|
1097
|
+
const available = Object.keys(multi.profiles).join(", ");
|
|
1098
|
+
throw new Error(`Profile "${name}" not found. Available: ${available}`);
|
|
1099
|
+
}
|
|
1100
|
+
multi.defaultProfile = name;
|
|
1101
|
+
saveMultiProfileConfig(multi);
|
|
1102
|
+
}
|
|
1103
|
+
function listProfiles() {
|
|
1104
|
+
const multi = loadMultiProfileConfig();
|
|
1105
|
+
return Object.entries(multi.profiles).map(([name, profile]) => ({
|
|
1106
|
+
name,
|
|
1107
|
+
isDefault: name === multi.defaultProfile,
|
|
1108
|
+
teamId: profile.teamId
|
|
1109
|
+
}));
|
|
1110
|
+
}
|
|
1111
|
+
function loadRawConfig(profileName) {
|
|
1112
|
+
migrateFromLegacy();
|
|
1113
|
+
const path = configPath();
|
|
1114
|
+
if (!fs.existsSync(path)) return {};
|
|
1115
|
+
let parsed;
|
|
1116
|
+
try {
|
|
1117
|
+
const raw = fs.readFileSync(path, "utf-8");
|
|
1118
|
+
parsed = JSON.parse(raw);
|
|
1119
|
+
} catch {
|
|
1120
|
+
return {};
|
|
1121
|
+
}
|
|
1122
|
+
if (!isRecord2(parsed)) {
|
|
1123
|
+
throw new Error(`Config file at ${path} must contain a JSON object.`);
|
|
1124
|
+
}
|
|
1125
|
+
if (isOldFormat(parsed)) {
|
|
1126
|
+
return parseConfigFile(JSON.stringify(parsed), path, false, true);
|
|
1127
|
+
}
|
|
1128
|
+
const multi = migrateToMultiProfile(parsed, path);
|
|
1129
|
+
const name = profileName || multi.defaultProfile || "default";
|
|
1130
|
+
return multi.profiles[name] ?? {};
|
|
1131
|
+
}
|
|
1132
|
+
function getConfigPath() {
|
|
1133
|
+
migrateFromLegacy();
|
|
1134
|
+
return configPath();
|
|
1135
|
+
}
|
|
1136
|
+
function getFilters(profileName) {
|
|
1137
|
+
const multi = loadMultiProfileConfig();
|
|
1138
|
+
const name = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1139
|
+
const profile = name ? multi.profiles[name] ?? {} : {};
|
|
1140
|
+
return profile.filters ?? {};
|
|
1141
|
+
}
|
|
1142
|
+
function saveFilter(name, entry, profileName) {
|
|
1143
|
+
const multi = loadMultiProfileConfig();
|
|
1144
|
+
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1145
|
+
const profile = multi.profiles[pName] ?? {};
|
|
1146
|
+
const filters = { ...profile.filters ?? {}, [name]: entry };
|
|
1147
|
+
multi.profiles[pName] = { ...profile, filters };
|
|
1148
|
+
if (!multi.defaultProfile) multi.defaultProfile = pName;
|
|
1149
|
+
saveMultiProfileConfig(multi);
|
|
1150
|
+
}
|
|
1151
|
+
function deleteFilter(name, profileName) {
|
|
1152
|
+
const multi = loadMultiProfileConfig();
|
|
1153
|
+
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1154
|
+
const profile = multi.profiles[pName] ?? {};
|
|
1155
|
+
const filters = { ...profile.filters ?? {} };
|
|
1156
|
+
if (!(name in filters)) {
|
|
1157
|
+
throw new Error(`Filter "${name}" not found.`);
|
|
1158
|
+
}
|
|
1159
|
+
delete filters[name];
|
|
1160
|
+
multi.profiles[pName] = { ...profile, filters };
|
|
1161
|
+
saveMultiProfileConfig(multi);
|
|
1162
|
+
}
|
|
1163
|
+
function getFavorites(profileName) {
|
|
1164
|
+
const multi = loadMultiProfileConfig();
|
|
1165
|
+
const name = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1166
|
+
const profile = name ? multi.profiles[name] ?? {} : {};
|
|
1167
|
+
return profile.favorites ?? {};
|
|
1168
|
+
}
|
|
1169
|
+
function saveFavorite(alias, entry, profileName) {
|
|
1170
|
+
const multi = loadMultiProfileConfig();
|
|
1171
|
+
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1172
|
+
const profile = multi.profiles[pName] ?? {};
|
|
1173
|
+
const favorites = { ...profile.favorites ?? {}, [alias]: entry };
|
|
1174
|
+
multi.profiles[pName] = { ...profile, favorites };
|
|
1175
|
+
if (!multi.defaultProfile) multi.defaultProfile = pName;
|
|
1176
|
+
saveMultiProfileConfig(multi);
|
|
1177
|
+
}
|
|
1178
|
+
function deleteFavorite(alias, profileName) {
|
|
1179
|
+
const multi = loadMultiProfileConfig();
|
|
1180
|
+
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1181
|
+
const profile = multi.profiles[pName] ?? {};
|
|
1182
|
+
const favorites = { ...profile.favorites ?? {} };
|
|
1183
|
+
if (!(alias in favorites)) {
|
|
1184
|
+
throw new Error(`Favorite "${alias}" not found.`);
|
|
1185
|
+
}
|
|
1186
|
+
delete favorites[alias];
|
|
1187
|
+
multi.profiles[pName] = { ...profile, favorites };
|
|
1188
|
+
saveMultiProfileConfig(multi);
|
|
1189
|
+
}
|
|
1190
|
+
function writeConfig(config, profileName) {
|
|
1191
|
+
const multi = loadMultiProfileConfig();
|
|
1192
|
+
const name = profileName || multi.defaultProfile || "default";
|
|
1193
|
+
const apiToken = trimConfigValue(config.apiToken) ?? void 0;
|
|
1194
|
+
const teamId = trimConfigValue(config.teamId) ?? void 0;
|
|
1195
|
+
const sprintFolderId = trimConfigValue(config.sprintFolderId);
|
|
1196
|
+
const normalizedConfig = {
|
|
1197
|
+
...apiToken ? { apiToken } : {},
|
|
1198
|
+
...teamId ? { teamId } : {},
|
|
1199
|
+
...sprintFolderId ? { sprintFolderId } : {}
|
|
1200
|
+
};
|
|
1201
|
+
multi.profiles[name] = {
|
|
1202
|
+
...multi.profiles[name],
|
|
1203
|
+
...normalizedConfig
|
|
1204
|
+
};
|
|
1205
|
+
if (!multi.defaultProfile) multi.defaultProfile = name;
|
|
1206
|
+
saveMultiProfileConfig(multi);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/date.ts
|
|
1210
|
+
function formatDate(ms) {
|
|
1211
|
+
return new Date(Number(ms)).toLocaleDateString("en-US", {
|
|
1212
|
+
month: "short",
|
|
1213
|
+
day: "numeric",
|
|
1214
|
+
year: "numeric"
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
function formatTimestamp(ms) {
|
|
1218
|
+
return new Date(Number(ms)).toLocaleString("en-US", {
|
|
1219
|
+
month: "short",
|
|
1220
|
+
day: "numeric",
|
|
1221
|
+
hour: "numeric",
|
|
1222
|
+
minute: "2-digit"
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function formatDuration(ms) {
|
|
1226
|
+
const totalMinutes = Math.round(Math.abs(ms) / 6e4);
|
|
1227
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1228
|
+
const minutes = totalMinutes % 60;
|
|
1229
|
+
if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
|
|
1230
|
+
if (hours > 0) return `${hours}h`;
|
|
1231
|
+
return `${minutes}m`;
|
|
1232
|
+
}
|
|
1233
|
+
function formatLongDuration(ms) {
|
|
1234
|
+
const totalMinutes = Math.round(Math.abs(ms) / 6e4);
|
|
1235
|
+
if (totalMinutes === 0) return "< 1m";
|
|
1236
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
1237
|
+
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
1238
|
+
const minutes = totalMinutes % 60;
|
|
1239
|
+
const parts = [];
|
|
1240
|
+
if (days > 0) parts.push(`${days}d`);
|
|
1241
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
1242
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
1243
|
+
return parts.join(" ");
|
|
1244
|
+
}
|
|
1245
|
+
function formatDateISO(ms) {
|
|
1246
|
+
const d = new Date(Number(ms));
|
|
1247
|
+
const year = d.getUTCFullYear();
|
|
1248
|
+
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
1249
|
+
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
1250
|
+
return `${year}-${month}-${day}`;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/output.ts
|
|
1254
|
+
import chalk from "chalk";
|
|
1255
|
+
function isTTY() {
|
|
1256
|
+
return Boolean(process.stdout.isTTY);
|
|
1257
|
+
}
|
|
1258
|
+
function shouldOutputJson(forceJson) {
|
|
1259
|
+
if (forceJson) return true;
|
|
1260
|
+
if (process.env["CU_OUTPUT"] === "json") return true;
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
function cell(value, width) {
|
|
1264
|
+
if (value.length > width) return value.slice(0, width - 1) + "\u2026";
|
|
1265
|
+
return value.padEnd(width);
|
|
1266
|
+
}
|
|
1267
|
+
function computeWidths(rows, columns) {
|
|
1268
|
+
return columns.map((col) => {
|
|
1269
|
+
const headerLen = col.label.length;
|
|
1270
|
+
const maxDataLen = rows.reduce((max, row) => {
|
|
1271
|
+
const val = String(row[col.key] ?? "");
|
|
1272
|
+
return Math.max(max, val.length);
|
|
1273
|
+
}, 0);
|
|
1274
|
+
const natural = Math.max(headerLen, maxDataLen);
|
|
1275
|
+
return col.maxWidth ? Math.min(natural, col.maxWidth) : natural;
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
function formatTable(rows, columns) {
|
|
1279
|
+
const widths = computeWidths(rows, columns);
|
|
1280
|
+
const header = columns.map((c, i) => cell(c.label, widths[i])).join(" ");
|
|
1281
|
+
const divider = chalk.dim("-".repeat(header.replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
1282
|
+
const lines = [chalk.bold(header), divider];
|
|
1283
|
+
for (const row of rows) {
|
|
1284
|
+
lines.push(
|
|
1285
|
+
columns.map((c, i) => {
|
|
1286
|
+
const raw = String(row[c.key] ?? "");
|
|
1287
|
+
const width = widths[i];
|
|
1288
|
+
const truncated = raw.length > width ? raw.slice(0, width - 1) + "\u2026" : raw;
|
|
1289
|
+
const padding = " ".repeat(Math.max(0, width - truncated.length));
|
|
1290
|
+
return c.format ? c.format(truncated, row) + padding : truncated + padding;
|
|
1291
|
+
}).join(" ")
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
return lines.join("\n");
|
|
1295
|
+
}
|
|
1296
|
+
function colorStatus(status) {
|
|
1297
|
+
const lower = status.toLowerCase();
|
|
1298
|
+
if (lower.includes("done") || lower.includes("complete") || lower.includes("closed"))
|
|
1299
|
+
return chalk.green(status);
|
|
1300
|
+
if (lower.includes("progress") || lower.includes("review") || lower.includes("active"))
|
|
1301
|
+
return chalk.yellow(status);
|
|
1302
|
+
if (lower.includes("block") || lower.includes("stuck")) return chalk.red(status);
|
|
1303
|
+
return chalk.dim(status);
|
|
1304
|
+
}
|
|
1305
|
+
function colorPriority(priority) {
|
|
1306
|
+
const lower = priority.toLowerCase();
|
|
1307
|
+
if (lower === "urgent") return chalk.red(priority);
|
|
1308
|
+
if (lower === "high") return chalk.yellow(priority);
|
|
1309
|
+
if (lower === "normal") return priority;
|
|
1310
|
+
if (lower === "low") return chalk.dim(priority);
|
|
1311
|
+
return priority;
|
|
1312
|
+
}
|
|
1313
|
+
function colorDueDate(dateStr, rawTimestamp) {
|
|
1314
|
+
if (!dateStr) return dateStr;
|
|
1315
|
+
if (rawTimestamp) {
|
|
1316
|
+
const ts = Number(rawTimestamp);
|
|
1317
|
+
if (Number.isFinite(ts) && ts < Date.now()) return chalk.red(dateStr);
|
|
1318
|
+
}
|
|
1319
|
+
return dateStr;
|
|
1320
|
+
}
|
|
1321
|
+
var TASK_COLUMNS = [
|
|
1322
|
+
{ key: "id", label: "ID" },
|
|
1323
|
+
{ key: "name", label: "NAME", maxWidth: 60 },
|
|
1324
|
+
{ key: "status", label: "STATUS", maxWidth: 20, format: (v) => colorStatus(v) },
|
|
1325
|
+
{ key: "priority", label: "PRIORITY", maxWidth: 10, format: (v) => colorPriority(v) },
|
|
1326
|
+
{
|
|
1327
|
+
key: "due_date",
|
|
1328
|
+
label: "DUE",
|
|
1329
|
+
maxWidth: 15,
|
|
1330
|
+
format: (v, row) => v ? colorDueDate(v, row.dueRaw) : ""
|
|
1331
|
+
},
|
|
1332
|
+
{ key: "list", label: "LIST" }
|
|
1333
|
+
];
|
|
1334
|
+
|
|
1335
|
+
// src/markdown.ts
|
|
1336
|
+
function escapeCell(value) {
|
|
1337
|
+
return value.replace(/\|/g, "\\|");
|
|
1338
|
+
}
|
|
1339
|
+
function formatMarkdownTable(rows, columns) {
|
|
1340
|
+
const header = "| " + columns.map((c) => c.label).join(" | ") + " |";
|
|
1341
|
+
const divider = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1342
|
+
const lines = [header, divider];
|
|
1343
|
+
for (const row of rows) {
|
|
1344
|
+
const cells = columns.map((c) => escapeCell(String(row[c.key] ?? "")));
|
|
1345
|
+
lines.push("| " + cells.join(" | ") + " |");
|
|
1346
|
+
}
|
|
1347
|
+
return lines.join("\n");
|
|
1348
|
+
}
|
|
1349
|
+
var TASK_MD_COLUMNS = [
|
|
1350
|
+
{ key: "id", label: "ID" },
|
|
1351
|
+
{ key: "name", label: "Name" },
|
|
1352
|
+
{ key: "status", label: "Status" },
|
|
1353
|
+
{ key: "priority", label: "Priority" },
|
|
1354
|
+
{ key: "due_date", label: "Due" },
|
|
1355
|
+
{ key: "list", label: "List" }
|
|
1356
|
+
];
|
|
1357
|
+
function formatTasksMarkdown(tasks) {
|
|
1358
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
1359
|
+
return formatMarkdownTable(tasks, TASK_MD_COLUMNS);
|
|
1360
|
+
}
|
|
1361
|
+
function formatCommentsMarkdown(comments) {
|
|
1362
|
+
if (comments.length === 0) return "No comments found.";
|
|
1363
|
+
return comments.map((c) => `**${c.user}** (${formatDateISO(c.date)})
|
|
1364
|
+
|
|
1365
|
+
${c.text}`).join("\n\n---\n\n");
|
|
1366
|
+
}
|
|
1367
|
+
var LIST_MD_COLUMNS = [
|
|
1368
|
+
{ key: "id", label: "ID" },
|
|
1369
|
+
{ key: "name", label: "Name" },
|
|
1370
|
+
{ key: "folder", label: "Folder" }
|
|
1371
|
+
];
|
|
1372
|
+
function formatListsMarkdown(lists) {
|
|
1373
|
+
if (lists.length === 0) return "No lists found.";
|
|
1374
|
+
return formatMarkdownTable(lists, LIST_MD_COLUMNS);
|
|
1375
|
+
}
|
|
1376
|
+
var SPACE_MD_COLUMNS = [
|
|
1377
|
+
{ key: "id", label: "ID" },
|
|
1378
|
+
{ key: "name", label: "Name" }
|
|
1379
|
+
];
|
|
1380
|
+
function formatSpacesMarkdown(spaces) {
|
|
1381
|
+
if (spaces.length === 0) return "No spaces found.";
|
|
1382
|
+
return formatMarkdownTable(spaces, SPACE_MD_COLUMNS);
|
|
1383
|
+
}
|
|
1384
|
+
function formatGroupedTasksMarkdown(groups) {
|
|
1385
|
+
const sections = groups.filter((g) => g.tasks.length > 0).map((g) => `## ${g.label}
|
|
1386
|
+
|
|
1387
|
+
${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
|
|
1388
|
+
if (sections.length === 0) return "No tasks found.";
|
|
1389
|
+
return sections.join("\n\n");
|
|
1390
|
+
}
|
|
1391
|
+
function formatTaskDetailMarkdown(task) {
|
|
1392
|
+
const lines = [`# ${task.name}`, ""];
|
|
1393
|
+
const isInitiative = (task.custom_item_id ?? 0) !== 0;
|
|
1394
|
+
const fields = [
|
|
1395
|
+
["ID", task.id],
|
|
1396
|
+
["Status", task.status.status],
|
|
1397
|
+
["Type", isInitiative ? "initiative" : "task"],
|
|
1398
|
+
["List", task.list.name],
|
|
1399
|
+
["URL", task.url],
|
|
1400
|
+
[
|
|
1401
|
+
"Assignees",
|
|
1402
|
+
task.assignees.length > 0 ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
1403
|
+
],
|
|
1404
|
+
["Priority", task.priority?.priority],
|
|
1405
|
+
["Parent", task.parent ?? void 0],
|
|
1406
|
+
["Start Date", task.start_date ? formatDateISO(task.start_date) : void 0],
|
|
1407
|
+
["Due Date", task.due_date ? formatDateISO(task.due_date) : void 0],
|
|
1408
|
+
[
|
|
1409
|
+
"Time Estimate",
|
|
1410
|
+
task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
|
|
1411
|
+
],
|
|
1412
|
+
[
|
|
1413
|
+
"Time Spent",
|
|
1414
|
+
task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
|
|
1415
|
+
],
|
|
1416
|
+
["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
1417
|
+
[
|
|
1418
|
+
"Lists",
|
|
1419
|
+
task.locations && task.locations.length > 0 ? task.locations.map((l) => l.name).join(", ") : void 0
|
|
1420
|
+
],
|
|
1421
|
+
["Created", task.date_created ? formatDateISO(task.date_created) : void 0],
|
|
1422
|
+
["Updated", task.date_updated ? formatDateISO(task.date_updated) : void 0]
|
|
1423
|
+
];
|
|
1424
|
+
for (const [label, value] of fields) {
|
|
1425
|
+
if (value != null && value !== "") {
|
|
1426
|
+
lines.push(`**${label}:** ${value}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
const descriptionContent = task.markdown_content ?? task.description;
|
|
1430
|
+
if (descriptionContent) {
|
|
1431
|
+
lines.push("", "## Description", "", descriptionContent);
|
|
1432
|
+
}
|
|
1433
|
+
if (task.checklists?.length) {
|
|
1434
|
+
lines.push("", "## Checklists", "");
|
|
1435
|
+
for (const cl of task.checklists) {
|
|
1436
|
+
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
1437
|
+
lines.push(`### ${cl.name} (${resolved}/${cl.items.length})`, "");
|
|
1438
|
+
for (const item of cl.items) {
|
|
1439
|
+
lines.push(`- [${item.resolved ? "x" : " "}] ${item.name}`);
|
|
1440
|
+
}
|
|
1441
|
+
lines.push("");
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (task.attachments?.length) {
|
|
1445
|
+
lines.push("", "## Attachments", "");
|
|
1446
|
+
for (const att of task.attachments) {
|
|
1447
|
+
lines.push(`- [${att.title}](${att.url})`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (task.dependencies?.length) {
|
|
1451
|
+
lines.push("", "## Dependencies", "");
|
|
1452
|
+
for (const dep of task.dependencies) {
|
|
1453
|
+
const direction = dep.depends_on === task.id ? "blocks" : "depends on";
|
|
1454
|
+
const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
|
|
1455
|
+
lines.push(`- ${direction} ${otherId}`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (task.linked_tasks?.length) {
|
|
1459
|
+
lines.push("", "## Linked Tasks", "");
|
|
1460
|
+
for (const lt of task.linked_tasks) {
|
|
1461
|
+
lines.push(`- ${lt.task_id}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return lines.join("\n");
|
|
1465
|
+
}
|
|
1466
|
+
function formatUpdateConfirmation(id, name) {
|
|
1467
|
+
return `Updated task ${id}: "${name}"`;
|
|
1468
|
+
}
|
|
1469
|
+
function formatCreateConfirmation(id, name, url) {
|
|
1470
|
+
return `Created task ${id}: "${name}" - ${url}`;
|
|
1471
|
+
}
|
|
1472
|
+
function formatCommentConfirmation(id) {
|
|
1473
|
+
return `Comment posted (id: ${id})`;
|
|
1474
|
+
}
|
|
1475
|
+
function formatAssignConfirmation(taskId, opts) {
|
|
1476
|
+
const parts = [];
|
|
1477
|
+
if (opts.to) parts.push(`Assigned ${opts.to} to ${taskId}`);
|
|
1478
|
+
if (opts.remove) parts.push(`Removed ${opts.remove} from ${taskId}`);
|
|
1479
|
+
return parts.join("; ");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/interactive.ts
|
|
1483
|
+
import { execFileSync } from "child_process";
|
|
1484
|
+
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
1485
|
+
import chalk2 from "chalk";
|
|
1486
|
+
function openUrl(url) {
|
|
1487
|
+
switch (process.platform) {
|
|
1488
|
+
case "darwin":
|
|
1489
|
+
execFileSync("open", [url]);
|
|
1490
|
+
break;
|
|
1491
|
+
case "linux":
|
|
1492
|
+
execFileSync("xdg-open", [url]);
|
|
1493
|
+
break;
|
|
1494
|
+
case "win32":
|
|
1495
|
+
execFileSync("cmd", ["/c", "start", "", url]);
|
|
1496
|
+
break;
|
|
1497
|
+
default:
|
|
1498
|
+
process.stderr.write(`Cannot open browser on ${process.platform}. Visit: ${url}
|
|
1499
|
+
`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function descriptionPreview(text, maxLines = 3) {
|
|
1503
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
1504
|
+
const preview = lines.slice(0, maxLines);
|
|
1505
|
+
const result = preview.map((l) => ` ${chalk2.dim(l.length > 100 ? l.slice(0, 99) + "\u2026" : l)}`).join("\n");
|
|
1506
|
+
if (lines.length > maxLines)
|
|
1507
|
+
return result + `
|
|
1508
|
+
${chalk2.dim(`... (${lines.length - maxLines} more lines)`)}`;
|
|
1509
|
+
return result;
|
|
1510
|
+
}
|
|
1511
|
+
function stringifyFieldValue(value) {
|
|
1512
|
+
if (typeof value === "string") return value;
|
|
1513
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1514
|
+
return JSON.stringify(value);
|
|
1515
|
+
}
|
|
1516
|
+
function formatCustomFieldValue(field) {
|
|
1517
|
+
if (field.value === null || field.value === void 0) return null;
|
|
1518
|
+
const options = field.type_config?.options;
|
|
1519
|
+
switch (field.type) {
|
|
1520
|
+
case "drop_down": {
|
|
1521
|
+
if (!options) return stringifyFieldValue(field.value);
|
|
1522
|
+
const match = options.find((o) => o.id === Number(field.value));
|
|
1523
|
+
return match?.name ?? stringifyFieldValue(field.value);
|
|
1524
|
+
}
|
|
1525
|
+
case "labels": {
|
|
1526
|
+
if (!Array.isArray(field.value) || !options) return stringifyFieldValue(field.value);
|
|
1527
|
+
const names = field.value.map((id) => options.find((o) => o.id === id)?.name).filter((n) => n !== void 0);
|
|
1528
|
+
return names.length > 0 ? names.join(", ") : null;
|
|
1529
|
+
}
|
|
1530
|
+
case "date": {
|
|
1531
|
+
const ts = Number(field.value);
|
|
1532
|
+
if (!Number.isFinite(ts)) return stringifyFieldValue(field.value);
|
|
1533
|
+
return formatDate(String(ts));
|
|
1534
|
+
}
|
|
1535
|
+
case "checkbox":
|
|
1536
|
+
return field.value === true || field.value === "true" ? "Yes" : "No";
|
|
1537
|
+
default:
|
|
1538
|
+
return stringifyFieldValue(field.value);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function formatTaskDetail(task) {
|
|
1542
|
+
const lines = [];
|
|
1543
|
+
const isInitiative = (task.custom_item_id ?? 0) !== 0;
|
|
1544
|
+
const typeLabel = isInitiative ? "initiative" : "task";
|
|
1545
|
+
lines.push(chalk2.bold.underline(task.name));
|
|
1546
|
+
lines.push("");
|
|
1547
|
+
const fields = [
|
|
1548
|
+
["ID", task.id],
|
|
1549
|
+
["Status", task.status?.status ? colorStatus(task.status.status) : void 0],
|
|
1550
|
+
["Type", typeLabel],
|
|
1551
|
+
["List", task.list?.name],
|
|
1552
|
+
[
|
|
1553
|
+
"Assignees",
|
|
1554
|
+
task.assignees?.length ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
1555
|
+
],
|
|
1556
|
+
["Priority", task.priority?.priority ? colorPriority(task.priority.priority) : void 0],
|
|
1557
|
+
["Start", task.start_date ? formatDate(task.start_date) : void 0],
|
|
1558
|
+
["Due", task.due_date ? colorDueDate(formatDate(task.due_date), task.due_date) : void 0],
|
|
1559
|
+
["Estimate", task.time_estimate ? formatDuration(task.time_estimate) : void 0],
|
|
1560
|
+
["Tracked", task.time_spent ? formatDuration(task.time_spent) : void 0],
|
|
1561
|
+
["Tags", task.tags?.length ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
1562
|
+
["Lists", task.locations?.length ? task.locations.map((l) => l.name).join(", ") : void 0],
|
|
1563
|
+
["Parent", task.parent || void 0],
|
|
1564
|
+
["URL", task.url]
|
|
1565
|
+
];
|
|
1566
|
+
const maxLabel = Math.max(...fields.filter(([, v]) => v).map(([k]) => k.length));
|
|
1567
|
+
for (const [label, value] of fields) {
|
|
1568
|
+
if (!value) continue;
|
|
1569
|
+
lines.push(` ${chalk2.bold(label.padEnd(maxLabel + 1))} ${value}`);
|
|
1570
|
+
}
|
|
1571
|
+
if (task.custom_fields?.length) {
|
|
1572
|
+
const formatted = task.custom_fields.map((f) => [f.name, formatCustomFieldValue(f)]).filter((pair) => pair[1] !== null);
|
|
1573
|
+
if (formatted.length > 0) {
|
|
1574
|
+
lines.push("");
|
|
1575
|
+
lines.push(chalk2.bold("Custom Fields"));
|
|
1576
|
+
for (const [name, value] of formatted) {
|
|
1577
|
+
lines.push(` ${chalk2.bold(name)} ${value}`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (task.checklists?.length) {
|
|
1582
|
+
lines.push("");
|
|
1583
|
+
lines.push(chalk2.bold("Checklists"));
|
|
1584
|
+
for (const cl of task.checklists) {
|
|
1585
|
+
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
1586
|
+
lines.push(` ${chalk2.bold(cl.name)} (${resolved}/${cl.items.length})`);
|
|
1587
|
+
for (const item of cl.items) {
|
|
1588
|
+
const check = item.resolved ? chalk2.green("[x]") : chalk2.dim("[ ]");
|
|
1589
|
+
lines.push(` ${check} ${item.name}`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (task.attachments?.length) {
|
|
1594
|
+
lines.push("");
|
|
1595
|
+
lines.push(chalk2.bold("Attachments"));
|
|
1596
|
+
for (const att of task.attachments) {
|
|
1597
|
+
lines.push(` ${att.title} ${chalk2.dim(att.url)}`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (task.dependencies?.length) {
|
|
1601
|
+
lines.push("");
|
|
1602
|
+
lines.push(chalk2.bold("Dependencies"));
|
|
1603
|
+
for (const dep of task.dependencies) {
|
|
1604
|
+
const direction = dep.depends_on === task.id ? "blocks" : "depends on";
|
|
1605
|
+
const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
|
|
1606
|
+
lines.push(` ${direction} ${chalk2.dim(otherId)}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (task.linked_tasks?.length) {
|
|
1610
|
+
lines.push("");
|
|
1611
|
+
lines.push(chalk2.bold("Linked Tasks"));
|
|
1612
|
+
for (const lt of task.linked_tasks) {
|
|
1613
|
+
lines.push(` ${chalk2.dim(lt.task_id)}`);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (task.text_content?.trim()) {
|
|
1617
|
+
lines.push("");
|
|
1618
|
+
lines.push(descriptionPreview(task.text_content));
|
|
1619
|
+
}
|
|
1620
|
+
return lines.join("\n");
|
|
1621
|
+
}
|
|
1622
|
+
function formatChoiceName(task) {
|
|
1623
|
+
const id = task.id.padEnd(12);
|
|
1624
|
+
const name = task.name.length > 50 ? task.name.slice(0, 49) + "\u2026" : task.name.padEnd(50);
|
|
1625
|
+
const status = colorStatus(task.status);
|
|
1626
|
+
const priority = task.priority !== "none" ? colorPriority(task.priority) : "";
|
|
1627
|
+
return `${id} ${name} ${status}${priority ? " " + priority : ""}`;
|
|
1628
|
+
}
|
|
1629
|
+
async function interactiveTaskPicker(tasks) {
|
|
1630
|
+
if (tasks.length === 0) return [];
|
|
1631
|
+
const selected = await checkbox({
|
|
1632
|
+
message: `${tasks.length} task(s) found. Select to view details / open in browser:`,
|
|
1633
|
+
choices: tasks.map((t) => ({
|
|
1634
|
+
name: formatChoiceName(t),
|
|
1635
|
+
value: t.id
|
|
1636
|
+
})),
|
|
1637
|
+
pageSize: 20
|
|
1638
|
+
});
|
|
1639
|
+
return tasks.filter((t) => selected.includes(t.id));
|
|
1640
|
+
}
|
|
1641
|
+
async function groupedTaskPicker(groups) {
|
|
1642
|
+
const allTasks = groups.flatMap((g) => g.tasks);
|
|
1643
|
+
const totalCount = allTasks.length;
|
|
1644
|
+
if (totalCount === 0) return [];
|
|
1645
|
+
const choices = [];
|
|
1646
|
+
for (const group of groups) {
|
|
1647
|
+
if (group.tasks.length === 0) continue;
|
|
1648
|
+
choices.push(new Separator(chalk2.bold(`${group.label} (${group.tasks.length})`)));
|
|
1649
|
+
for (const task of group.tasks) {
|
|
1650
|
+
choices.push({ name: formatChoiceName(task), value: task.id });
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const selected = await checkbox({
|
|
1654
|
+
message: `${totalCount} task(s) found. Select to view details / open in browser:`,
|
|
1655
|
+
choices,
|
|
1656
|
+
pageSize: 20
|
|
1657
|
+
});
|
|
1658
|
+
return allTasks.filter((t) => selected.includes(t.id));
|
|
1659
|
+
}
|
|
1660
|
+
async function showDetailsAndOpen(tasks, fetchTask) {
|
|
1661
|
+
if (tasks.length === 0) return;
|
|
1662
|
+
const separator = chalk2.dim("\u2500".repeat(60));
|
|
1663
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
1664
|
+
const task = tasks[i];
|
|
1665
|
+
if (i > 0) {
|
|
1666
|
+
console.log("");
|
|
1667
|
+
console.log(separator);
|
|
1668
|
+
}
|
|
1669
|
+
console.log("");
|
|
1670
|
+
if (fetchTask) {
|
|
1671
|
+
const full = await fetchTask(task.id);
|
|
1672
|
+
console.log(formatTaskDetail(full));
|
|
1673
|
+
} else {
|
|
1674
|
+
const fallback = {
|
|
1675
|
+
id: task.id,
|
|
1676
|
+
name: task.name,
|
|
1677
|
+
status: { status: task.status, color: "" },
|
|
1678
|
+
custom_item_id: task.task_type === "initiative" ? 1 : 0,
|
|
1679
|
+
assignees: [],
|
|
1680
|
+
url: task.url,
|
|
1681
|
+
list: { id: "", name: task.list },
|
|
1682
|
+
parent: task.parent
|
|
1683
|
+
};
|
|
1684
|
+
console.log(formatTaskDetail(fallback));
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const urls = tasks.map((t) => t.url);
|
|
1688
|
+
console.log("");
|
|
1689
|
+
const shouldOpen = await confirm({
|
|
1690
|
+
message: `Open ${urls.length} task(s) in browser?`,
|
|
1691
|
+
default: true
|
|
1692
|
+
});
|
|
1693
|
+
if (shouldOpen) {
|
|
1694
|
+
for (const url of urls) {
|
|
1695
|
+
openUrl(url);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/commands/tasks.ts
|
|
1701
|
+
var DONE_PATTERNS = ["done", "complete", "closed"];
|
|
1702
|
+
function isDoneStatus(status) {
|
|
1703
|
+
const lower = status.toLowerCase();
|
|
1704
|
+
return DONE_PATTERNS.some((p) => lower.includes(p));
|
|
1705
|
+
}
|
|
1706
|
+
function formatDueDate(ms) {
|
|
1707
|
+
if (!ms) return "";
|
|
1708
|
+
return formatDate(ms);
|
|
1709
|
+
}
|
|
1710
|
+
function resolveTaskType(task, typeMap) {
|
|
1711
|
+
const id = task.custom_item_id ?? 0;
|
|
1712
|
+
if (id === 0) return "task";
|
|
1713
|
+
return typeMap.get(id) ?? `type_${id}`;
|
|
1714
|
+
}
|
|
1715
|
+
function summarize(task, typeMap) {
|
|
1716
|
+
return {
|
|
1717
|
+
id: task.id,
|
|
1718
|
+
name: task.name,
|
|
1719
|
+
status: task.status.status,
|
|
1720
|
+
task_type: resolveTaskType(task, typeMap ?? /* @__PURE__ */ new Map()),
|
|
1721
|
+
priority: task.priority?.priority ?? "none",
|
|
1722
|
+
due_date: formatDueDate(task.due_date),
|
|
1723
|
+
...task.due_date ? { dueRaw: task.due_date } : {},
|
|
1724
|
+
list: task.list.name,
|
|
1725
|
+
url: task.url,
|
|
1726
|
+
...task.parent ? { parent: task.parent } : {}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function buildTypeMap(types) {
|
|
1730
|
+
const map = /* @__PURE__ */ new Map();
|
|
1731
|
+
for (const t of types) {
|
|
1732
|
+
map.set(t.id, t.name);
|
|
1733
|
+
}
|
|
1734
|
+
return map;
|
|
1735
|
+
}
|
|
1736
|
+
function resolveTypeFilter(typeFilter, typeMap) {
|
|
1737
|
+
if (typeFilter === "task") return 0;
|
|
1738
|
+
const asNum = Number(typeFilter);
|
|
1739
|
+
if (Number.isFinite(asNum)) return asNum;
|
|
1740
|
+
const lower = typeFilter.toLowerCase();
|
|
1741
|
+
for (const [id, name] of typeMap) {
|
|
1742
|
+
if (name.toLowerCase() === lower) return id;
|
|
1743
|
+
}
|
|
1744
|
+
const available = ["task", ...Array.from(typeMap.values())].join(", ");
|
|
1745
|
+
throw new Error(`Unknown task type "${typeFilter}". Available types: ${available}`);
|
|
1746
|
+
}
|
|
1747
|
+
async function fetchMyTasks(config, opts = {}) {
|
|
1748
|
+
const client = new ClickUpClient(config);
|
|
1749
|
+
const { typeFilter, name, ...apiFilters } = opts;
|
|
1750
|
+
const [allTasks, customTypes] = await Promise.all([
|
|
1751
|
+
client.getMyTasks(config.teamId, apiFilters),
|
|
1752
|
+
client.getCustomTaskTypes(config.teamId)
|
|
1753
|
+
]);
|
|
1754
|
+
const typeMap = buildTypeMap(customTypes);
|
|
1755
|
+
let filtered = allTasks;
|
|
1756
|
+
if (typeFilter) {
|
|
1757
|
+
const targetId = resolveTypeFilter(typeFilter, typeMap);
|
|
1758
|
+
filtered = allTasks.filter((t) => (t.custom_item_id ?? 0) === targetId);
|
|
1759
|
+
}
|
|
1760
|
+
if (name) {
|
|
1761
|
+
const query = name.toLowerCase();
|
|
1762
|
+
filtered = filtered.filter((t) => t.name.toLowerCase().includes(query));
|
|
1763
|
+
}
|
|
1764
|
+
return filtered.map((t) => summarize(t, typeMap));
|
|
1765
|
+
}
|
|
1766
|
+
async function printTasks(tasks, forceJson, config) {
|
|
1767
|
+
if (shouldOutputJson(forceJson)) {
|
|
1768
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (!isTTY()) {
|
|
1772
|
+
console.log(formatTasksMarkdown(tasks));
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (tasks.length === 0) {
|
|
1776
|
+
console.log("No tasks found.");
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const fetchTask = config ? (() => {
|
|
1780
|
+
const client = new ClickUpClient(config);
|
|
1781
|
+
return (id) => client.getTask(id);
|
|
1782
|
+
})() : void 0;
|
|
1783
|
+
const selected = await interactiveTaskPicker(tasks);
|
|
1784
|
+
await showDetailsAndOpen(selected, fetchTask);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
61
1787
|
// src/status.ts
|
|
62
1788
|
function matchStatus(input, statuses) {
|
|
63
1789
|
if (!input) return null;
|
|
@@ -286,13 +2012,36 @@ async function getTask(config, taskId) {
|
|
|
286
2012
|
}
|
|
287
2013
|
|
|
288
2014
|
// src/commands/init.ts
|
|
289
|
-
import { password, select, confirm } from "@inquirer/prompts";
|
|
290
|
-
import
|
|
291
|
-
async function runInitCommand() {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
2015
|
+
import { password, select, confirm as confirm2 } from "@inquirer/prompts";
|
|
2016
|
+
import fs2 from "fs";
|
|
2017
|
+
async function runInitCommand(opts) {
|
|
2018
|
+
if (opts?.token && opts?.team) {
|
|
2019
|
+
const apiToken2 = opts.token.trim();
|
|
2020
|
+
if (!apiToken2.startsWith("pk_")) throw new Error("Token must start with pk_");
|
|
2021
|
+
const client2 = new ClickUpClient({ apiToken: apiToken2 });
|
|
2022
|
+
let username2;
|
|
2023
|
+
try {
|
|
2024
|
+
const me = await client2.getMe();
|
|
2025
|
+
username2 = me.username;
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
throw new Error(`Invalid token: ${err instanceof Error ? err.message : String(err)}`, {
|
|
2028
|
+
cause: err
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
process.stdout.write(`Authenticated as @${username2}
|
|
2032
|
+
`);
|
|
2033
|
+
writeConfig({ apiToken: apiToken2, teamId: opts.team });
|
|
2034
|
+
process.stdout.write(`Config written to ${getConfigPath()}
|
|
2035
|
+
`);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (opts?.token || opts?.team) {
|
|
2039
|
+
throw new Error("Both --token and --team are required for non-interactive setup");
|
|
2040
|
+
}
|
|
2041
|
+
const configPath3 = getConfigPath();
|
|
2042
|
+
if (fs2.existsSync(configPath3)) {
|
|
2043
|
+
const overwrite = await confirm2({
|
|
2044
|
+
message: `Config already exists at ${configPath3}. Overwrite?`,
|
|
296
2045
|
default: false
|
|
297
2046
|
});
|
|
298
2047
|
if (!overwrite) {
|
|
@@ -330,19 +2079,240 @@ async function runInitCommand() {
|
|
|
330
2079
|
});
|
|
331
2080
|
}
|
|
332
2081
|
writeConfig({ apiToken, teamId });
|
|
333
|
-
process.stdout.write(`Config written to ${
|
|
2082
|
+
process.stdout.write(`Config written to ${configPath3}
|
|
2083
|
+
`);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/commands/sprint.ts
|
|
2087
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
2088
|
+
var SPRINT_KEYWORDS = ["sprint", "iteration", "cycle", "scrum"];
|
|
2089
|
+
function parseUSDateRange(name) {
|
|
2090
|
+
const m = name.match(/\((\d{1,2})\/(\d{1,2})\s*[-–]\s*(\d{1,2})\/(\d{1,2})\)/);
|
|
2091
|
+
if (!m) return null;
|
|
2092
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2093
|
+
const start = new Date(year, Number(m[1]) - 1, Number(m[2]));
|
|
2094
|
+
const end = new Date(year, Number(m[3]) - 1, Number(m[4]), 23, 59, 59);
|
|
2095
|
+
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
2096
|
+
return { start, end };
|
|
2097
|
+
}
|
|
2098
|
+
function parseISODateRange(name) {
|
|
2099
|
+
const m = name.match(/\((\d{4}-\d{2}-\d{2})\s*[-–]\s*(\d{4}-\d{2}-\d{2})\)/);
|
|
2100
|
+
if (!m) return null;
|
|
2101
|
+
const [sy, sm, sd] = m[1].split("-").map(Number);
|
|
2102
|
+
const [ey, em, ed] = m[2].split("-").map(Number);
|
|
2103
|
+
const start = new Date(sy, sm - 1, sd);
|
|
2104
|
+
const end = new Date(ey, em - 1, ed, 23, 59, 59);
|
|
2105
|
+
return { start, end };
|
|
2106
|
+
}
|
|
2107
|
+
function parseMonthDayRange(name) {
|
|
2108
|
+
const months = {
|
|
2109
|
+
jan: 0,
|
|
2110
|
+
feb: 1,
|
|
2111
|
+
mar: 2,
|
|
2112
|
+
apr: 3,
|
|
2113
|
+
may: 4,
|
|
2114
|
+
jun: 5,
|
|
2115
|
+
jul: 6,
|
|
2116
|
+
aug: 7,
|
|
2117
|
+
sep: 8,
|
|
2118
|
+
oct: 9,
|
|
2119
|
+
nov: 10,
|
|
2120
|
+
dec: 11
|
|
2121
|
+
};
|
|
2122
|
+
const m = name.match(/\(([A-Za-z]{3})\s+(\d{1,2})\s*[-–]\s*([A-Za-z]{3})\s+(\d{1,2})\)/);
|
|
2123
|
+
if (!m) return null;
|
|
2124
|
+
const sm = months[m[1].toLowerCase()];
|
|
2125
|
+
const em = months[m[3].toLowerCase()];
|
|
2126
|
+
if (sm === void 0 || em === void 0) return null;
|
|
2127
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2128
|
+
const start = new Date(year, sm, Number(m[2]));
|
|
2129
|
+
const end = new Date(year, em, Number(m[4]), 23, 59, 59);
|
|
2130
|
+
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
2131
|
+
return { start, end };
|
|
2132
|
+
}
|
|
2133
|
+
function parseEuropeanDateRange(name) {
|
|
2134
|
+
const m = name.match(/\((\d{1,2})\.(\d{1,2})\s*[-–]\s*(\d{1,2})\.(\d{1,2})\)/);
|
|
2135
|
+
if (!m) return null;
|
|
2136
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2137
|
+
const start = new Date(year, Number(m[2]) - 1, Number(m[1]));
|
|
2138
|
+
const end = new Date(year, Number(m[4]) - 1, Number(m[3]), 23, 59, 59);
|
|
2139
|
+
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
2140
|
+
return { start, end };
|
|
2141
|
+
}
|
|
2142
|
+
function parseSprintDates(name) {
|
|
2143
|
+
return parseUSDateRange(name) ?? parseISODateRange(name) ?? parseMonthDayRange(name) ?? parseEuropeanDateRange(name);
|
|
2144
|
+
}
|
|
2145
|
+
function findActiveSprintList(lists, today = /* @__PURE__ */ new Date()) {
|
|
2146
|
+
if (lists.length === 0) return null;
|
|
2147
|
+
for (const list of lists) {
|
|
2148
|
+
const dates = parseSprintDates(list.name);
|
|
2149
|
+
if (dates && today >= dates.start && today <= dates.end) return list;
|
|
2150
|
+
}
|
|
2151
|
+
for (const list of lists) {
|
|
2152
|
+
if (list.start_date && list.due_date) {
|
|
2153
|
+
const start = new Date(Number(list.start_date));
|
|
2154
|
+
const end = new Date(Number(list.due_date));
|
|
2155
|
+
if (today >= start && today <= end) return list;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return lists[lists.length - 1] ?? null;
|
|
2159
|
+
}
|
|
2160
|
+
var NOISE_WORDS = /* @__PURE__ */ new Set(["product", "team", "the", "and", "for", "test"]);
|
|
2161
|
+
function extractSpaceKeywords(spaceName) {
|
|
2162
|
+
return spaceName.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((w) => w.toLowerCase()).filter((w) => w.length >= 3 && !NOISE_WORDS.has(w));
|
|
2163
|
+
}
|
|
2164
|
+
function findRelatedSpaces(mySpaceIds, allSpaces) {
|
|
2165
|
+
const mySpaces = allSpaces.filter((s) => mySpaceIds.has(s.id));
|
|
2166
|
+
const keywords = mySpaces.flatMap((s) => extractSpaceKeywords(s.name));
|
|
2167
|
+
if (keywords.length === 0) return allSpaces;
|
|
2168
|
+
return allSpaces.filter(
|
|
2169
|
+
(s) => mySpaceIds.has(s.id) || keywords.some((kw) => s.name.toLowerCase().includes(kw))
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
function resolveSprintFolderId(config, opts) {
|
|
2173
|
+
const folderId = opts?.folder ?? config.sprintFolderId;
|
|
2174
|
+
if (folderId) return folderId;
|
|
2175
|
+
const favorites = getFavorites();
|
|
2176
|
+
const favoriteFolderIds = Object.values(favorites).filter((f) => f.type === "sprint-folder").map((f) => f.id);
|
|
2177
|
+
return favoriteFolderIds[0];
|
|
2178
|
+
}
|
|
2179
|
+
async function resolveActiveSprintListId(config, opts) {
|
|
2180
|
+
const client = new ClickUpClient(config);
|
|
2181
|
+
const folderId = resolveSprintFolderId(config, opts);
|
|
2182
|
+
let sprintLists;
|
|
2183
|
+
if (folderId) {
|
|
2184
|
+
sprintLists = await client.getFolderLists(folderId);
|
|
2185
|
+
} else {
|
|
2186
|
+
const [myTasks, allSpaces] = await Promise.all([
|
|
2187
|
+
client.getMyTasks(config.teamId),
|
|
2188
|
+
client.getSpaces(config.teamId)
|
|
2189
|
+
]);
|
|
2190
|
+
let spaces;
|
|
2191
|
+
if (opts?.space) {
|
|
2192
|
+
spaces = allSpaces.filter(
|
|
2193
|
+
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
2194
|
+
);
|
|
2195
|
+
if (spaces.length === 0) {
|
|
2196
|
+
throw new Error(`No space matching "${opts.space}" found.`);
|
|
2197
|
+
}
|
|
2198
|
+
} else {
|
|
2199
|
+
const mySpaceIds = new Set(
|
|
2200
|
+
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
2201
|
+
);
|
|
2202
|
+
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
2203
|
+
}
|
|
2204
|
+
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
2205
|
+
const sprintFolders = foldersBySpace.flat().filter((f) => {
|
|
2206
|
+
const lower = f.name.toLowerCase();
|
|
2207
|
+
return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
2208
|
+
});
|
|
2209
|
+
const listsByFolder = await Promise.all(
|
|
2210
|
+
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
2211
|
+
);
|
|
2212
|
+
sprintLists = listsByFolder.flat();
|
|
2213
|
+
}
|
|
2214
|
+
const activeList = findActiveSprintList(sprintLists);
|
|
2215
|
+
if (!activeList) {
|
|
2216
|
+
throw new Error(
|
|
2217
|
+
'No active sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
return activeList.id;
|
|
2221
|
+
}
|
|
2222
|
+
async function runSprintCommand(config, opts) {
|
|
2223
|
+
const client = new ClickUpClient(config);
|
|
2224
|
+
process.stderr.write("Detecting active sprint...\n");
|
|
2225
|
+
const folderId = resolveSprintFolderId(config, opts);
|
|
2226
|
+
const [myTasks, allSpaces, customTypes] = await Promise.all([
|
|
2227
|
+
client.getMyTasks(config.teamId),
|
|
2228
|
+
folderId ? Promise.resolve([]) : client.getSpaces(config.teamId),
|
|
2229
|
+
client.getCustomTaskTypes(config.teamId)
|
|
2230
|
+
]);
|
|
2231
|
+
const typeMap = buildTypeMap(customTypes);
|
|
2232
|
+
let sprintLists;
|
|
2233
|
+
if (folderId) {
|
|
2234
|
+
sprintLists = await client.getFolderLists(folderId);
|
|
2235
|
+
} else {
|
|
2236
|
+
let spaces;
|
|
2237
|
+
if (opts.space) {
|
|
2238
|
+
spaces = allSpaces.filter(
|
|
2239
|
+
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
2240
|
+
);
|
|
2241
|
+
if (spaces.length === 0) {
|
|
2242
|
+
throw new Error(
|
|
2243
|
+
`No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
} else {
|
|
2247
|
+
const mySpaceIds = new Set(
|
|
2248
|
+
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
2249
|
+
);
|
|
2250
|
+
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
2251
|
+
}
|
|
2252
|
+
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
2253
|
+
const sprintFolders = foldersBySpace.flat().filter((f) => {
|
|
2254
|
+
const lower = f.name.toLowerCase();
|
|
2255
|
+
return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
2256
|
+
});
|
|
2257
|
+
const listsByFolder = await Promise.all(
|
|
2258
|
+
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
2259
|
+
);
|
|
2260
|
+
sprintLists = listsByFolder.flat();
|
|
2261
|
+
}
|
|
2262
|
+
let activeList = findActiveSprintList(sprintLists);
|
|
2263
|
+
if (!activeList && sprintLists.length > 1 && isTTY()) {
|
|
2264
|
+
const choice = await select2({
|
|
2265
|
+
message: "Multiple sprint lists found. Which one?",
|
|
2266
|
+
choices: sprintLists.map((l) => ({
|
|
2267
|
+
name: `${l.name} (${l.id})`,
|
|
2268
|
+
value: l
|
|
2269
|
+
}))
|
|
2270
|
+
});
|
|
2271
|
+
activeList = choice;
|
|
2272
|
+
}
|
|
2273
|
+
if (!activeList && sprintLists.length > 1) {
|
|
2274
|
+
process.stderr.write(
|
|
2275
|
+
`Multiple sprint lists found:
|
|
2276
|
+
${sprintLists.map((l) => ` - ${l.name} (${l.id})`).join("\n")}
|
|
2277
|
+
Using: ${sprintLists[sprintLists.length - 1].name}
|
|
2278
|
+
`
|
|
2279
|
+
);
|
|
2280
|
+
activeList = sprintLists[sprintLists.length - 1] ?? null;
|
|
2281
|
+
}
|
|
2282
|
+
if (!activeList) {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
'No sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
process.stderr.write(`Active sprint: ${activeList.name}
|
|
334
2288
|
`);
|
|
2289
|
+
const me = await client.getMe();
|
|
2290
|
+
const viewData = await client.getListViews(activeList.id);
|
|
2291
|
+
const listView = viewData.required_views?.list;
|
|
2292
|
+
let allTasks;
|
|
2293
|
+
if (listView) {
|
|
2294
|
+
allTasks = await client.getViewTasks(listView.id);
|
|
2295
|
+
} else {
|
|
2296
|
+
allTasks = await client.getTasksFromList(activeList.id);
|
|
2297
|
+
}
|
|
2298
|
+
let sprintTasks = allTasks.filter((t) => t.assignees.some((a) => Number(a.id) === me.id));
|
|
2299
|
+
if (!opts.includeClosed) {
|
|
2300
|
+
sprintTasks = sprintTasks.filter((t) => !isDoneStatus(t.status.status));
|
|
2301
|
+
}
|
|
2302
|
+
const filtered = opts.status ? sprintTasks.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase()) : sprintTasks;
|
|
2303
|
+
const summaries = filtered.map((t) => summarize(t, typeMap));
|
|
2304
|
+
await printTasks(summaries, opts.json ?? false, config);
|
|
335
2305
|
}
|
|
336
2306
|
|
|
337
2307
|
// src/commands/sprints.ts
|
|
338
|
-
import
|
|
2308
|
+
import chalk3 from "chalk";
|
|
339
2309
|
var SPRINT_COLUMNS = [
|
|
340
2310
|
{ key: "id", label: "ID" },
|
|
341
2311
|
{
|
|
342
2312
|
key: "sprint",
|
|
343
2313
|
label: "SPRINT",
|
|
344
2314
|
maxWidth: 60,
|
|
345
|
-
format: (v, row) => row.active ?
|
|
2315
|
+
format: (v, row) => row.active ? chalk3.green(v) : v
|
|
346
2316
|
},
|
|
347
2317
|
{ key: "dates", label: "DATES" }
|
|
348
2318
|
];
|
|
@@ -468,7 +2438,7 @@ async function postComment(config, taskId, text, notifyAll) {
|
|
|
468
2438
|
}
|
|
469
2439
|
|
|
470
2440
|
// src/commands/comments.ts
|
|
471
|
-
import
|
|
2441
|
+
import chalk4 from "chalk";
|
|
472
2442
|
async function fetchComments(config, taskId) {
|
|
473
2443
|
const client = new ClickUpClient(config);
|
|
474
2444
|
const comments = await client.getTaskComments(taskId);
|
|
@@ -492,11 +2462,11 @@ function printComments(comments, forceJson) {
|
|
|
492
2462
|
console.log("No comments found.");
|
|
493
2463
|
return;
|
|
494
2464
|
}
|
|
495
|
-
const separator =
|
|
2465
|
+
const separator = chalk4.dim("-".repeat(60));
|
|
496
2466
|
for (let i = 0; i < comments.length; i++) {
|
|
497
2467
|
const c = comments[i];
|
|
498
2468
|
if (i > 0) console.log(separator);
|
|
499
|
-
console.log(`${
|
|
2469
|
+
console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatTimestamp(c.date))}`);
|
|
500
2470
|
console.log(c.text);
|
|
501
2471
|
if (i < comments.length - 1) console.log("");
|
|
502
2472
|
}
|
|
@@ -806,7 +2776,7 @@ async function openTask(config, query, opts = {}) {
|
|
|
806
2776
|
}
|
|
807
2777
|
|
|
808
2778
|
// src/commands/summary.ts
|
|
809
|
-
import
|
|
2779
|
+
import chalk5 from "chalk";
|
|
810
2780
|
var IN_PROGRESS_PATTERNS = ["in progress", "in review", "code review", "doing"];
|
|
811
2781
|
function isCompletedRecently(task, cutoff) {
|
|
812
2782
|
if (!isDoneStatus(task.status.status)) return false;
|
|
@@ -844,9 +2814,9 @@ function categorizeTasks(tasks, hoursBack, typeMap) {
|
|
|
844
2814
|
}
|
|
845
2815
|
function colorSectionLabel(label) {
|
|
846
2816
|
const lower = label.toLowerCase();
|
|
847
|
-
if (lower.includes("completed")) return
|
|
848
|
-
if (lower.includes("progress")) return
|
|
849
|
-
if (lower.includes("overdue")) return
|
|
2817
|
+
if (lower.includes("completed")) return chalk5.green(label);
|
|
2818
|
+
if (lower.includes("progress")) return chalk5.yellow(label);
|
|
2819
|
+
if (lower.includes("overdue")) return chalk5.red(label);
|
|
850
2820
|
return label;
|
|
851
2821
|
}
|
|
852
2822
|
function printSection(label, tasks) {
|
|
@@ -948,7 +2918,7 @@ function setConfigValue(key, value, profileName) {
|
|
|
948
2918
|
}
|
|
949
2919
|
writeConfig(merged, profileName);
|
|
950
2920
|
}
|
|
951
|
-
function
|
|
2921
|
+
function configPath2() {
|
|
952
2922
|
return getConfigPath();
|
|
953
2923
|
}
|
|
954
2924
|
|
|
@@ -975,7 +2945,7 @@ async function assignTask(config, taskId, opts) {
|
|
|
975
2945
|
}
|
|
976
2946
|
|
|
977
2947
|
// src/commands/activity.ts
|
|
978
|
-
import
|
|
2948
|
+
import chalk6 from "chalk";
|
|
979
2949
|
async function fetchActivity(config, taskId) {
|
|
980
2950
|
const client = new ClickUpClient(config);
|
|
981
2951
|
const [task, rawComments] = await Promise.all([
|
|
@@ -1007,8 +2977,8 @@ ${commentsMd}`);
|
|
|
1007
2977
|
}
|
|
1008
2978
|
console.log(formatTaskDetail(result.task));
|
|
1009
2979
|
console.log("");
|
|
1010
|
-
console.log(
|
|
1011
|
-
console.log(
|
|
2980
|
+
console.log(chalk6.bold("Comments"));
|
|
2981
|
+
console.log(chalk6.dim("-".repeat(60)));
|
|
1012
2982
|
if (result.comments.length === 0) {
|
|
1013
2983
|
console.log("No comments.");
|
|
1014
2984
|
return;
|
|
@@ -1017,15 +2987,15 @@ ${commentsMd}`);
|
|
|
1017
2987
|
const c = result.comments[i];
|
|
1018
2988
|
if (i > 0) {
|
|
1019
2989
|
console.log("");
|
|
1020
|
-
console.log(
|
|
2990
|
+
console.log(chalk6.dim("-".repeat(60)));
|
|
1021
2991
|
}
|
|
1022
|
-
console.log(`${
|
|
2992
|
+
console.log(`${chalk6.bold(c.user)} ${chalk6.dim(formatTimestamp(c.date))}`);
|
|
1023
2993
|
console.log(c.text);
|
|
1024
2994
|
}
|
|
1025
2995
|
}
|
|
1026
2996
|
|
|
1027
2997
|
// src/commands/time-in-status.ts
|
|
1028
|
-
import
|
|
2998
|
+
import chalk7 from "chalk";
|
|
1029
2999
|
function transformResponse(taskId, data) {
|
|
1030
3000
|
const entries = [];
|
|
1031
3001
|
for (const entry of data.status_history ?? []) {
|
|
@@ -1095,11 +3065,11 @@ function printTimeInStatus(result, forceJson) {
|
|
|
1095
3065
|
const columns = [
|
|
1096
3066
|
{ key: "status", label: "STATUS", maxWidth: 25, format: (v) => colorStatus(v) },
|
|
1097
3067
|
{ key: "duration", label: "DURATION" },
|
|
1098
|
-
{ key: "current", label: "", format: (v) => v ?
|
|
3068
|
+
{ key: "current", label: "", format: (v) => v ? chalk7.green("\u25C0") : "" }
|
|
1099
3069
|
];
|
|
1100
3070
|
console.log(formatTable(rows, columns));
|
|
1101
3071
|
console.log("");
|
|
1102
|
-
console.log(`${
|
|
3072
|
+
console.log(`${chalk7.bold("Total:")} ${result.total}`);
|
|
1103
3073
|
}
|
|
1104
3074
|
|
|
1105
3075
|
// src/commands/metadata.ts
|
|
@@ -1107,6 +3077,7 @@ var commandMetadata = [
|
|
|
1107
3077
|
{
|
|
1108
3078
|
name: "init",
|
|
1109
3079
|
description: "Set up cup for the first time",
|
|
3080
|
+
flags: ["--token", "--team"],
|
|
1110
3081
|
quickReference: [{ section: "setup", usage: "init", description: "First-time setup wizard" }]
|
|
1111
3082
|
},
|
|
1112
3083
|
{
|
|
@@ -1244,12 +3215,12 @@ var commandMetadata = [
|
|
|
1244
3215
|
},
|
|
1245
3216
|
{
|
|
1246
3217
|
name: "comment-delete",
|
|
1247
|
-
description: "Delete a comment",
|
|
1248
|
-
flags: ["--mine", "--match", "--json"],
|
|
3218
|
+
description: "Delete a comment by ID, or use --task with --mine to find and delete your comment",
|
|
3219
|
+
flags: ["--task", "--mine", "--match", "--json"],
|
|
1249
3220
|
quickReference: [
|
|
1250
3221
|
{
|
|
1251
3222
|
section: "write",
|
|
1252
|
-
usage: "comment-delete
|
|
3223
|
+
usage: "comment-delete [commentId]",
|
|
1253
3224
|
description: "Delete a comment"
|
|
1254
3225
|
}
|
|
1255
3226
|
]
|
|
@@ -2483,8 +4454,9 @@ ${renderZshTopLevelCommands(name)}
|
|
|
2483
4454
|
comment-delete)
|
|
2484
4455
|
_arguments \\
|
|
2485
4456
|
'1:comment_id:' \\
|
|
4457
|
+
'--task[Task to search for your comment (requires --mine)]:task_id:' \\
|
|
2486
4458
|
'--mine[Delete one of my comments from the specified task]' \\
|
|
2487
|
-
'--match[Only match comments containing this text]:text:' \\
|
|
4459
|
+
'--match[Only match comments containing this text (requires --mine)]:text:' \\
|
|
2488
4460
|
'--json[Force JSON output]'
|
|
2489
4461
|
;;
|
|
2490
4462
|
replies)
|
|
@@ -3014,18 +4986,18 @@ function generateCompletion(shell, name = "cup") {
|
|
|
3014
4986
|
|
|
3015
4987
|
// src/commands/skill.ts
|
|
3016
4988
|
import { readFileSync, realpathSync, mkdirSync, copyFileSync, existsSync } from "fs";
|
|
3017
|
-
import { join, dirname } from "path";
|
|
3018
|
-
import { homedir } from "os";
|
|
3019
|
-
import
|
|
4989
|
+
import { join as join2, dirname } from "path";
|
|
4990
|
+
import { homedir as homedir2 } from "os";
|
|
4991
|
+
import chalk8 from "chalk";
|
|
3020
4992
|
function skillPath() {
|
|
3021
4993
|
if (!process.argv[1]) {
|
|
3022
4994
|
throw new Error("Cannot determine install path. Run with: cup skill");
|
|
3023
4995
|
}
|
|
3024
4996
|
const entryPoint = realpathSync(process.argv[1]);
|
|
3025
4997
|
const packageRoot = dirname(dirname(entryPoint));
|
|
3026
|
-
const candidate =
|
|
4998
|
+
const candidate = join2(packageRoot, "skills", "clickup-cli", "SKILL.md");
|
|
3027
4999
|
if (existsSync(candidate)) return candidate;
|
|
3028
|
-
const altCandidate =
|
|
5000
|
+
const altCandidate = join2(dirname(entryPoint), "..", "skills", "clickup-cli", "SKILL.md");
|
|
3029
5001
|
if (existsSync(altCandidate)) return altCandidate;
|
|
3030
5002
|
throw new Error("SKILL.md not found. Reinstall with: npm install -g @krodak/clickup-cli");
|
|
3031
5003
|
}
|
|
@@ -3033,11 +5005,11 @@ function printSkill() {
|
|
|
3033
5005
|
return readFileSync(skillPath(), "utf-8");
|
|
3034
5006
|
}
|
|
3035
5007
|
function getAgentTargets() {
|
|
3036
|
-
const home =
|
|
5008
|
+
const home = homedir2();
|
|
3037
5009
|
const targets = [
|
|
3038
|
-
{ name: "Claude Code", dir:
|
|
3039
|
-
{ name: "Codex", dir:
|
|
3040
|
-
{ name: "OpenCode", dir:
|
|
5010
|
+
{ name: "Claude Code", dir: join2(home, ".claude", "skills", "clickup") },
|
|
5011
|
+
{ name: "Codex", dir: join2(home, ".agents", "skills", "clickup") },
|
|
5012
|
+
{ name: "OpenCode", dir: join2(home, ".config", "opencode", "skills", "clickup") }
|
|
3041
5013
|
];
|
|
3042
5014
|
return targets.map((t) => ({
|
|
3043
5015
|
...t,
|
|
@@ -3049,11 +5021,11 @@ async function installSkillInteractive() {
|
|
|
3049
5021
|
const source = skillPath();
|
|
3050
5022
|
const installed = [];
|
|
3051
5023
|
if (isTTY()) {
|
|
3052
|
-
const { checkbox } = await import("@inquirer/prompts");
|
|
3053
|
-
const selected = await
|
|
5024
|
+
const { checkbox: checkbox2 } = await import("@inquirer/prompts");
|
|
5025
|
+
const selected = await checkbox2({
|
|
3054
5026
|
message: "Install skill for which agents?",
|
|
3055
5027
|
choices: targets.map((t) => ({
|
|
3056
|
-
name: `${t.name}${t.detected ?
|
|
5028
|
+
name: `${t.name}${t.detected ? chalk8.dim(" (detected)") : ""}`,
|
|
3057
5029
|
value: t.name,
|
|
3058
5030
|
checked: t.detected
|
|
3059
5031
|
}))
|
|
@@ -3065,7 +5037,7 @@ async function installSkillInteractive() {
|
|
|
3065
5037
|
const target = targets.find((t) => t.name === name);
|
|
3066
5038
|
if (!target) continue;
|
|
3067
5039
|
if (!existsSync(target.dir)) mkdirSync(target.dir, { recursive: true });
|
|
3068
|
-
const dest =
|
|
5040
|
+
const dest = join2(target.dir, "SKILL.md");
|
|
3069
5041
|
copyFileSync(source, dest);
|
|
3070
5042
|
installed.push(`${target.name}: ${dest}`);
|
|
3071
5043
|
}
|
|
@@ -3078,7 +5050,7 @@ async function installSkillInteractive() {
|
|
|
3078
5050
|
}
|
|
3079
5051
|
for (const target of detected) {
|
|
3080
5052
|
if (!existsSync(target.dir)) mkdirSync(target.dir, { recursive: true });
|
|
3081
|
-
const dest =
|
|
5053
|
+
const dest = join2(target.dir, "SKILL.md");
|
|
3082
5054
|
copyFileSync(source, dest);
|
|
3083
5055
|
installed.push(`${target.name}: ${dest}`);
|
|
3084
5056
|
}
|
|
@@ -3298,8 +5270,8 @@ async function deleteTaskCommand(config, taskId, opts) {
|
|
|
3298
5270
|
throw new Error("Destructive operation requires --confirm flag in non-interactive mode");
|
|
3299
5271
|
}
|
|
3300
5272
|
const task = await client.getTask(taskId);
|
|
3301
|
-
const { confirm:
|
|
3302
|
-
const confirmed = await
|
|
5273
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
5274
|
+
const confirmed = await confirm3({
|
|
3303
5275
|
message: `Delete task "${task.name}" (${task.id})? This cannot be undone.`,
|
|
3304
5276
|
default: false
|
|
3305
5277
|
});
|
|
@@ -3319,8 +5291,8 @@ async function archiveTaskCommand(config, taskId, opts) {
|
|
|
3319
5291
|
throw new Error(`Destructive operation requires --confirm flag in non-interactive mode`);
|
|
3320
5292
|
}
|
|
3321
5293
|
const task = await client.getTask(taskId);
|
|
3322
|
-
const { confirm:
|
|
3323
|
-
const confirmed = await
|
|
5294
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
5295
|
+
const confirmed = await confirm3({
|
|
3324
5296
|
message: `${opts.unarchive ? "Unarchive" : "Archive"} task "${task.name}" (${task.id})?`,
|
|
3325
5297
|
default: false
|
|
3326
5298
|
});
|
|
@@ -3371,7 +5343,7 @@ async function manageTags(config, taskId, opts) {
|
|
|
3371
5343
|
}
|
|
3372
5344
|
|
|
3373
5345
|
// src/commands/checklist.ts
|
|
3374
|
-
import
|
|
5346
|
+
import chalk9 from "chalk";
|
|
3375
5347
|
async function viewChecklists(config, taskId) {
|
|
3376
5348
|
const client = new ClickUpClient(config);
|
|
3377
5349
|
const task = await client.getTask(taskId);
|
|
@@ -3404,14 +5376,14 @@ function formatChecklists(checklists) {
|
|
|
3404
5376
|
const lines = [];
|
|
3405
5377
|
for (const cl of checklists) {
|
|
3406
5378
|
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
3407
|
-
lines.push(
|
|
3408
|
-
lines.push(
|
|
5379
|
+
lines.push(chalk9.bold(`${cl.name} (${resolved}/${cl.items.length})`));
|
|
5380
|
+
lines.push(chalk9.dim(` ID: ${cl.id}`));
|
|
3409
5381
|
for (const item of cl.items) {
|
|
3410
|
-
const check = item.resolved ?
|
|
3411
|
-
const name = item.resolved ?
|
|
3412
|
-
const assignee = item.assignee ?
|
|
5382
|
+
const check = item.resolved ? chalk9.green("[x]") : chalk9.dim("[ ]");
|
|
5383
|
+
const name = item.resolved ? chalk9.dim(item.name) : item.name;
|
|
5384
|
+
const assignee = item.assignee ? chalk9.dim(` @${item.assignee.username}`) : "";
|
|
3413
5385
|
lines.push(` ${check} ${name}${assignee}`);
|
|
3414
|
-
lines.push(
|
|
5386
|
+
lines.push(chalk9.dim(` item-id: ${item.id}`));
|
|
3415
5387
|
}
|
|
3416
5388
|
}
|
|
3417
5389
|
return lines.join("\n");
|
|
@@ -3470,7 +5442,7 @@ async function deleteCommentByTaskSelection(config, taskId, options) {
|
|
|
3470
5442
|
}
|
|
3471
5443
|
|
|
3472
5444
|
// src/commands/replies.ts
|
|
3473
|
-
import
|
|
5445
|
+
import chalk10 from "chalk";
|
|
3474
5446
|
async function getReplies(config, commentId) {
|
|
3475
5447
|
const client = new ClickUpClient(config);
|
|
3476
5448
|
return client.getThreadedComments(commentId);
|
|
@@ -3485,7 +5457,7 @@ function formatReplies(replies) {
|
|
|
3485
5457
|
return replies.map((r) => {
|
|
3486
5458
|
const user = r.user?.username ?? "Unknown";
|
|
3487
5459
|
const date = formatTimestamp(Number(r.date));
|
|
3488
|
-
return `${
|
|
5460
|
+
return `${chalk10.bold(user)} ${chalk10.dim(date)}
|
|
3489
5461
|
${r.comment_text}`;
|
|
3490
5462
|
}).join("\n\n");
|
|
3491
5463
|
}
|
|
@@ -3548,7 +5520,7 @@ function formatDocsMarkdown(docs) {
|
|
|
3548
5520
|
}
|
|
3549
5521
|
|
|
3550
5522
|
// src/commands/doc.ts
|
|
3551
|
-
import
|
|
5523
|
+
import chalk11 from "chalk";
|
|
3552
5524
|
async function getDocInfo(config, docId) {
|
|
3553
5525
|
const client = new ClickUpClient(config);
|
|
3554
5526
|
const [doc, pages] = await Promise.all([
|
|
@@ -3560,14 +5532,14 @@ async function getDocInfo(config, docId) {
|
|
|
3560
5532
|
function formatDocInfo(doc, pages, indent = 0) {
|
|
3561
5533
|
const lines = [];
|
|
3562
5534
|
if (indent === 0) {
|
|
3563
|
-
lines.push(`${
|
|
5535
|
+
lines.push(`${chalk11.bold(doc.name)} ${chalk11.dim(doc.id)}`);
|
|
3564
5536
|
if (pages.length === 0) {
|
|
3565
5537
|
lines.push(" (no pages)");
|
|
3566
5538
|
}
|
|
3567
5539
|
}
|
|
3568
5540
|
for (const page of pages) {
|
|
3569
5541
|
const prefix = " ".repeat(indent + 1);
|
|
3570
|
-
lines.push(`${prefix}${page.name} ${
|
|
5542
|
+
lines.push(`${prefix}${page.name} ${chalk11.dim(page.id)}`);
|
|
3571
5543
|
if (page.pages && page.pages.length > 0) {
|
|
3572
5544
|
lines.push(formatDocInfo(doc, page.pages, indent + 1));
|
|
3573
5545
|
}
|
|
@@ -3646,7 +5618,7 @@ async function deleteDocPage(config, docId, pageId) {
|
|
|
3646
5618
|
}
|
|
3647
5619
|
|
|
3648
5620
|
// src/commands/folders.ts
|
|
3649
|
-
import
|
|
5621
|
+
import chalk12 from "chalk";
|
|
3650
5622
|
async function listFolders(config, spaceId, nameFilter) {
|
|
3651
5623
|
const client = new ClickUpClient(config);
|
|
3652
5624
|
const folders = await client.getFolders(spaceId);
|
|
@@ -3665,9 +5637,9 @@ async function listFolders(config, spaceId, nameFilter) {
|
|
|
3665
5637
|
function formatFolders(folders) {
|
|
3666
5638
|
if (folders.length === 0) return "No folders found";
|
|
3667
5639
|
return folders.map((f) => {
|
|
3668
|
-
const header = `${
|
|
5640
|
+
const header = `${chalk12.bold(f.name)} ${chalk12.dim(f.id)}`;
|
|
3669
5641
|
if (f.lists.length === 0) return header;
|
|
3670
|
-
const listLines = f.lists.map((l) => ` ${
|
|
5642
|
+
const listLines = f.lists.map((l) => ` ${chalk12.dim(">")} ${l.name} ${chalk12.dim(l.id)}`);
|
|
3671
5643
|
return [header, ...listLines].join("\n");
|
|
3672
5644
|
}).join("\n\n");
|
|
3673
5645
|
}
|
|
@@ -3682,13 +5654,13 @@ function formatFoldersMarkdown(folders) {
|
|
|
3682
5654
|
}
|
|
3683
5655
|
|
|
3684
5656
|
// src/commands/time.ts
|
|
3685
|
-
import
|
|
5657
|
+
import chalk13 from "chalk";
|
|
3686
5658
|
var TIME_COLUMNS = [
|
|
3687
5659
|
{ key: "task", label: "Task", maxWidth: 35 },
|
|
3688
5660
|
{ key: "duration", label: "Duration", maxWidth: 10 },
|
|
3689
5661
|
{ key: "date", label: "Date", maxWidth: 20 },
|
|
3690
5662
|
{ key: "description", label: "Description", maxWidth: 30 },
|
|
3691
|
-
{ key: "status", label: "", maxWidth: 10, format: (v) => v === "RUNNING" ?
|
|
5663
|
+
{ key: "status", label: "", maxWidth: 10, format: (v) => v === "RUNNING" ? chalk13.green(v) : "" }
|
|
3692
5664
|
];
|
|
3693
5665
|
async function startTimer(config, taskId, description) {
|
|
3694
5666
|
const client = new ClickUpClient(config);
|
|
@@ -3784,7 +5756,7 @@ function formatTimeEntriesMarkdown(entries) {
|
|
|
3784
5756
|
}
|
|
3785
5757
|
|
|
3786
5758
|
// src/commands/tags.ts
|
|
3787
|
-
import
|
|
5759
|
+
import chalk14 from "chalk";
|
|
3788
5760
|
var TAG_COLUMNS = [
|
|
3789
5761
|
{ key: "name", label: "Name", maxWidth: 40 },
|
|
3790
5762
|
{ key: "fg", label: "FG", maxWidth: 10 },
|
|
@@ -3817,13 +5789,13 @@ function formatTags(tags) {
|
|
|
3817
5789
|
if (tags.length === 0) return "No tags found";
|
|
3818
5790
|
if (isTTY()) {
|
|
3819
5791
|
const rows = tags.map((t) => ({
|
|
3820
|
-
name: t.tag_bg ?
|
|
5792
|
+
name: t.tag_bg ? chalk14.bgHex(t.tag_bg).hex(t.tag_fg || "#ffffff")(` ${t.name} `) : chalk14.bold(t.name),
|
|
3821
5793
|
fg: t.tag_fg || "",
|
|
3822
5794
|
bg: t.tag_bg || ""
|
|
3823
5795
|
}));
|
|
3824
5796
|
return formatTable(rows, TAG_COLUMNS);
|
|
3825
5797
|
}
|
|
3826
|
-
return tags.map((t) =>
|
|
5798
|
+
return tags.map((t) => chalk14.bold(t.name)).join(", ");
|
|
3827
5799
|
}
|
|
3828
5800
|
function formatTagsMarkdown(tags) {
|
|
3829
5801
|
if (tags.length === 0) return "No tags found";
|
|
@@ -3858,7 +5830,7 @@ function formatMembersMarkdown(members) {
|
|
|
3858
5830
|
}
|
|
3859
5831
|
|
|
3860
5832
|
// src/commands/fields.ts
|
|
3861
|
-
import
|
|
5833
|
+
import chalk15 from "chalk";
|
|
3862
5834
|
var FIELD_COLUMNS = [
|
|
3863
5835
|
{ key: "id", label: "ID", maxWidth: 20 },
|
|
3864
5836
|
{ key: "name", label: "Name", maxWidth: 30 },
|
|
@@ -3867,7 +5839,7 @@ var FIELD_COLUMNS = [
|
|
|
3867
5839
|
key: "required",
|
|
3868
5840
|
label: "Required",
|
|
3869
5841
|
maxWidth: 10,
|
|
3870
|
-
format: (v) => v === "yes" ?
|
|
5842
|
+
format: (v) => v === "yes" ? chalk15.yellow(v) : chalk15.dim(v)
|
|
3871
5843
|
},
|
|
3872
5844
|
{ key: "options", label: "Options", maxWidth: 40 }
|
|
3873
5845
|
];
|
|
@@ -3974,13 +5946,13 @@ async function bulkTag(config, tagName, taskIds, action) {
|
|
|
3974
5946
|
}
|
|
3975
5947
|
|
|
3976
5948
|
// src/commands/goals.ts
|
|
3977
|
-
import
|
|
5949
|
+
import chalk16 from "chalk";
|
|
3978
5950
|
function colorProgress(value) {
|
|
3979
5951
|
const num = parseInt(value, 10);
|
|
3980
5952
|
if (isNaN(num)) return value;
|
|
3981
|
-
if (num >= 75) return
|
|
3982
|
-
if (num >= 25) return
|
|
3983
|
-
return
|
|
5953
|
+
if (num >= 75) return chalk16.green(value);
|
|
5954
|
+
if (num >= 25) return chalk16.yellow(value);
|
|
5955
|
+
return chalk16.red(value);
|
|
3984
5956
|
}
|
|
3985
5957
|
var GOAL_COLUMNS = [
|
|
3986
5958
|
{ key: "id", label: "ID", maxWidth: 15 },
|
|
@@ -4074,14 +6046,14 @@ function formatKeyResultsMarkdown(keyResults) {
|
|
|
4074
6046
|
}
|
|
4075
6047
|
|
|
4076
6048
|
// src/commands/task-types.ts
|
|
4077
|
-
import
|
|
6049
|
+
import chalk17 from "chalk";
|
|
4078
6050
|
async function listTaskTypes(config) {
|
|
4079
6051
|
const client = new ClickUpClient(config);
|
|
4080
6052
|
return client.getCustomTaskTypes(config.teamId);
|
|
4081
6053
|
}
|
|
4082
6054
|
function formatTaskTypes(types) {
|
|
4083
6055
|
if (types.length === 0) return "No custom task types";
|
|
4084
|
-
return types.map((t) => `${
|
|
6056
|
+
return types.map((t) => `${chalk17.bold(t.name)} ${chalk17.dim(`(${t.id})`)}`).join("\n");
|
|
4085
6057
|
}
|
|
4086
6058
|
function formatTaskTypesMarkdown(types) {
|
|
4087
6059
|
if (types.length === 0) return "No custom task types";
|
|
@@ -4089,14 +6061,14 @@ function formatTaskTypesMarkdown(types) {
|
|
|
4089
6061
|
}
|
|
4090
6062
|
|
|
4091
6063
|
// src/commands/templates.ts
|
|
4092
|
-
import
|
|
6064
|
+
import chalk18 from "chalk";
|
|
4093
6065
|
async function listTemplates(config) {
|
|
4094
6066
|
const client = new ClickUpClient(config);
|
|
4095
6067
|
return client.getTaskTemplates(config.teamId);
|
|
4096
6068
|
}
|
|
4097
6069
|
function formatTemplates(templates) {
|
|
4098
6070
|
if (templates.length === 0) return "No task templates";
|
|
4099
|
-
return templates.map((t) => `${
|
|
6071
|
+
return templates.map((t) => `${chalk18.bold(t.name)} ${chalk18.dim(`(${t.id})`)}`).join("\n");
|
|
4100
6072
|
}
|
|
4101
6073
|
function formatTemplatesMarkdown(templates) {
|
|
4102
6074
|
if (templates.length === 0) return "No task templates";
|
|
@@ -4104,14 +6076,14 @@ function formatTemplatesMarkdown(templates) {
|
|
|
4104
6076
|
}
|
|
4105
6077
|
|
|
4106
6078
|
// src/commands/list-templates.ts
|
|
4107
|
-
import
|
|
6079
|
+
import chalk19 from "chalk";
|
|
4108
6080
|
async function listListTemplates(config) {
|
|
4109
6081
|
const client = new ClickUpClient(config);
|
|
4110
6082
|
return client.getListTemplates(config.teamId);
|
|
4111
6083
|
}
|
|
4112
6084
|
function formatListTemplates(templates) {
|
|
4113
6085
|
if (templates.length === 0) return "No list templates";
|
|
4114
|
-
return templates.map((t) => `${
|
|
6086
|
+
return templates.map((t) => `${chalk19.bold(t.name)} ${chalk19.dim(`(${t.id})`)}`).join("\n");
|
|
4115
6087
|
}
|
|
4116
6088
|
function formatListTemplatesMarkdown(templates) {
|
|
4117
6089
|
if (templates.length === 0) return "No list templates";
|
|
@@ -4119,14 +6091,14 @@ function formatListTemplatesMarkdown(templates) {
|
|
|
4119
6091
|
}
|
|
4120
6092
|
|
|
4121
6093
|
// src/commands/folder-templates.ts
|
|
4122
|
-
import
|
|
6094
|
+
import chalk20 from "chalk";
|
|
4123
6095
|
async function listFolderTemplates(config) {
|
|
4124
6096
|
const client = new ClickUpClient(config);
|
|
4125
6097
|
return client.getFolderTemplates(config.teamId);
|
|
4126
6098
|
}
|
|
4127
6099
|
function formatFolderTemplates(templates) {
|
|
4128
6100
|
if (templates.length === 0) return "No folder templates";
|
|
4129
|
-
return templates.map((t) => `${
|
|
6101
|
+
return templates.map((t) => `${chalk20.bold(t.name)} ${chalk20.dim(`(${t.id})`)}`).join("\n");
|
|
4130
6102
|
}
|
|
4131
6103
|
function formatFolderTemplatesMarkdown(templates) {
|
|
4132
6104
|
if (templates.length === 0) return "No folder templates";
|
|
@@ -4149,7 +6121,7 @@ async function createListFromTemplate(config, name, opts) {
|
|
|
4149
6121
|
}
|
|
4150
6122
|
|
|
4151
6123
|
// src/commands/views.ts
|
|
4152
|
-
import
|
|
6124
|
+
import chalk21 from "chalk";
|
|
4153
6125
|
async function listViews(config, id, container = "list") {
|
|
4154
6126
|
const client = new ClickUpClient(config);
|
|
4155
6127
|
if (container === "space") return client.getSpaceViews(id);
|
|
@@ -4160,7 +6132,7 @@ async function listViews(config, id, container = "list") {
|
|
|
4160
6132
|
}
|
|
4161
6133
|
function formatViews(views) {
|
|
4162
6134
|
if (views.length === 0) return "No views";
|
|
4163
|
-
return views.map((v) => `${
|
|
6135
|
+
return views.map((v) => `${chalk21.bold(v.name)} ${chalk21.dim(`(${v.id})`)} ${chalk21.dim(v.type)}`).join("\n");
|
|
4164
6136
|
}
|
|
4165
6137
|
function formatViewsMarkdown(views) {
|
|
4166
6138
|
if (views.length === 0) return "No views";
|
|
@@ -4168,20 +6140,20 @@ function formatViewsMarkdown(views) {
|
|
|
4168
6140
|
}
|
|
4169
6141
|
|
|
4170
6142
|
// src/commands/view.ts
|
|
4171
|
-
import
|
|
6143
|
+
import chalk22 from "chalk";
|
|
4172
6144
|
async function getView(config, viewId) {
|
|
4173
6145
|
const client = new ClickUpClient(config);
|
|
4174
6146
|
return client.getView(viewId);
|
|
4175
6147
|
}
|
|
4176
6148
|
function formatView(view) {
|
|
4177
6149
|
const lines = [];
|
|
4178
|
-
lines.push(
|
|
6150
|
+
lines.push(chalk22.bold.underline(view.name));
|
|
4179
6151
|
lines.push("");
|
|
4180
|
-
lines.push(` ${
|
|
4181
|
-
lines.push(` ${
|
|
4182
|
-
if (view.visibility) lines.push(` ${
|
|
4183
|
-
if (view.date_created) lines.push(` ${
|
|
4184
|
-
if (view.protected !== void 0) lines.push(` ${
|
|
6152
|
+
lines.push(` ${chalk22.bold("ID")} ${view.id}`);
|
|
6153
|
+
lines.push(` ${chalk22.bold("Type")} ${view.type}`);
|
|
6154
|
+
if (view.visibility) lines.push(` ${chalk22.bold("Visibility")} ${view.visibility}`);
|
|
6155
|
+
if (view.date_created) lines.push(` ${chalk22.bold("Created")} ${formatDate(view.date_created)}`);
|
|
6156
|
+
if (view.protected !== void 0) lines.push(` ${chalk22.bold("Protected")} ${view.protected}`);
|
|
4185
6157
|
return lines.join("\n");
|
|
4186
6158
|
}
|
|
4187
6159
|
function formatViewMarkdown(view) {
|
|
@@ -4265,8 +6237,8 @@ async function deleteViewCommand(config, viewId, opts) {
|
|
|
4265
6237
|
throw new Error("Destructive operation requires --confirm flag in non-interactive mode");
|
|
4266
6238
|
}
|
|
4267
6239
|
const view = await client.getView(viewId);
|
|
4268
|
-
const { confirm:
|
|
4269
|
-
const confirmed = await
|
|
6240
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
6241
|
+
const confirmed = await confirm3({
|
|
4270
6242
|
message: `Delete view "${view.name}" (${viewId})? This cannot be undone.`,
|
|
4271
6243
|
default: false
|
|
4272
6244
|
});
|
|
@@ -4473,9 +6445,9 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
4473
6445
|
function getProfileName() {
|
|
4474
6446
|
return program.opts().profile;
|
|
4475
6447
|
}
|
|
4476
|
-
program.command("init").description(`Set up ${programName} for the first time`).action(
|
|
4477
|
-
wrapAction(async () => {
|
|
4478
|
-
await runInitCommand();
|
|
6448
|
+
program.command("init").description(`Set up ${programName} for the first time`).option("--token <token>", "API token (pk_...) for non-interactive setup").option("--team <teamId>", "Workspace/team ID for non-interactive setup").action(
|
|
6449
|
+
wrapAction(async (opts) => {
|
|
6450
|
+
await runInitCommand(opts);
|
|
4479
6451
|
})
|
|
4480
6452
|
);
|
|
4481
6453
|
program.command("auth").description("Validate API token and show current user").option("--json", "Force JSON output even in terminal").action(
|
|
@@ -4625,7 +6597,6 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
4625
6597
|
wrapAction(async (opts) => {
|
|
4626
6598
|
const config = loadConfig(getProfileName());
|
|
4627
6599
|
if (opts.list === "sprint:current") {
|
|
4628
|
-
const { resolveActiveSprintListId } = await import("./sprint-NLMMC4RB.js");
|
|
4629
6600
|
opts.list = await resolveActiveSprintListId(config);
|
|
4630
6601
|
}
|
|
4631
6602
|
if (opts.assignee === "me") {
|
|
@@ -4707,14 +6678,33 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
4707
6678
|
}
|
|
4708
6679
|
)
|
|
4709
6680
|
);
|
|
4710
|
-
program.command("comment-delete
|
|
6681
|
+
program.command("comment-delete [commentId]").description(
|
|
6682
|
+
"Delete a comment by ID, or use --task with --mine to find and delete your comment"
|
|
6683
|
+
).option("--task <taskId>", "Task to search for your comment (requires --mine)").option("--mine", "Delete one of my comments from the specified task").option("--match <text>", "Only match comments containing this text (requires --mine)").option("--json", "Force JSON output even in terminal").action(
|
|
4711
6684
|
wrapAction(
|
|
4712
6685
|
async (commentId, opts) => {
|
|
6686
|
+
if (opts.mine && !opts.task) {
|
|
6687
|
+
throw new Error("--mine requires --task <taskId>");
|
|
6688
|
+
}
|
|
6689
|
+
if (opts.match && !opts.mine) {
|
|
6690
|
+
throw new Error("--match requires --mine");
|
|
6691
|
+
}
|
|
4713
6692
|
const config = loadConfig(getProfileName());
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
6693
|
+
let result;
|
|
6694
|
+
if (opts.task) {
|
|
6695
|
+
if (!opts.mine) {
|
|
6696
|
+
throw new Error("--task requires --mine");
|
|
6697
|
+
}
|
|
6698
|
+
result = await deleteCommentByTaskSelection(config, opts.task, {
|
|
6699
|
+
mine: opts.mine,
|
|
6700
|
+
match: opts.match
|
|
6701
|
+
});
|
|
6702
|
+
} else if (commentId) {
|
|
6703
|
+
await deleteComment(config, commentId);
|
|
6704
|
+
result = { commentId };
|
|
6705
|
+
} else {
|
|
6706
|
+
throw new Error("Provide a comment ID or use --task <taskId> --mine");
|
|
6707
|
+
}
|
|
4718
6708
|
if (shouldOutputJson(opts.json ?? false)) {
|
|
4719
6709
|
console.log(JSON.stringify({ success: true, ...result }, null, 2));
|
|
4720
6710
|
} else {
|
|
@@ -4946,7 +6936,6 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
4946
6936
|
wrapAction(async (taskId, opts) => {
|
|
4947
6937
|
const config = loadConfig(getProfileName());
|
|
4948
6938
|
if (opts.to === "sprint:current") {
|
|
4949
|
-
const { resolveActiveSprintListId } = await import("./sprint-NLMMC4RB.js");
|
|
4950
6939
|
opts.to = await resolveActiveSprintListId(config);
|
|
4951
6940
|
}
|
|
4952
6941
|
const message = await moveTask(config, taskId, opts);
|
|
@@ -5988,7 +7977,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
5988
7977
|
);
|
|
5989
7978
|
profileCmd.command("add <name>").description("Add a new profile").action(
|
|
5990
7979
|
wrapAction(async (name) => {
|
|
5991
|
-
const { password: password2, select:
|
|
7980
|
+
const { password: password2, select: select3 } = await import("@inquirer/prompts");
|
|
5992
7981
|
const apiToken = (await password2({ message: "ClickUp API token (pk_...):" })).trim();
|
|
5993
7982
|
if (!apiToken.startsWith("pk_")) throw new Error("Token must start with pk_");
|
|
5994
7983
|
const client = new ClickUpClient({ apiToken });
|
|
@@ -6003,7 +7992,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
6003
7992
|
process.stdout.write(`Workspace: ${teams[0].name}
|
|
6004
7993
|
`);
|
|
6005
7994
|
} else {
|
|
6006
|
-
teamId = await
|
|
7995
|
+
teamId = await select3({
|
|
6007
7996
|
message: "Select workspace:",
|
|
6008
7997
|
choices: teams.map((t) => ({ name: t.name, value: t.id }))
|
|
6009
7998
|
});
|
|
@@ -6041,7 +8030,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
|
|
|
6041
8030
|
);
|
|
6042
8031
|
configCmd.command("path").description("Print config file path").action(
|
|
6043
8032
|
wrapAction(async () => {
|
|
6044
|
-
console.log(
|
|
8033
|
+
console.log(configPath2());
|
|
6045
8034
|
})
|
|
6046
8035
|
);
|
|
6047
8036
|
program.command("completion <shell>").description("Output shell completion script (bash, zsh, fish)").action(
|