@krodak/clickup-cli 1.19.3 → 1.19.5
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/dist/index.js +2127 -156
- package/package.json +1 -1
- package/skills/clickup-cli/SKILL.md +1 -1
- package/dist/chunk-HCGKTH6V.js +0 -2063
- package/dist/sprint-NLMMC4RB.js +0 -19
package/dist/chunk-HCGKTH6V.js
DELETED
|
@@ -1,2063 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/commands/sprint.ts
|
|
4
|
-
import { select } from "@inquirer/prompts";
|
|
5
|
-
|
|
6
|
-
// src/api.ts
|
|
7
|
-
var BASE_URL = "https://api.clickup.com/api/v2";
|
|
8
|
-
var BASE_URL_V3 = "https://api.clickup.com/api/v3";
|
|
9
|
-
var MAX_PAGES = 100;
|
|
10
|
-
function isRecord(value) {
|
|
11
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
-
}
|
|
13
|
-
function expectRecord(value, context) {
|
|
14
|
-
if (!isRecord(value)) {
|
|
15
|
-
throw new Error(`Unexpected API response: expected ${context} object`);
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
function expectRecordField(data, key, context) {
|
|
20
|
-
return expectRecord(data[key], context);
|
|
21
|
-
}
|
|
22
|
-
function expectNumericField(data, key, context) {
|
|
23
|
-
const value = Number(data[key]);
|
|
24
|
-
if (!Number.isInteger(value)) {
|
|
25
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be numeric`);
|
|
26
|
-
}
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
function expectStringField(data, key, context) {
|
|
30
|
-
const value = data[key];
|
|
31
|
-
if (typeof value !== "string") {
|
|
32
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be a string`);
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
function expectArrayField(data, key, context) {
|
|
37
|
-
const value = data[key];
|
|
38
|
-
if (!Array.isArray(value)) {
|
|
39
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be an array`);
|
|
40
|
-
}
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
function readCollectionField(data, key, context) {
|
|
44
|
-
if (data[key] === void 0) return [];
|
|
45
|
-
return expectArrayField(data, key, context);
|
|
46
|
-
}
|
|
47
|
-
function expectBooleanField(data, key, context) {
|
|
48
|
-
const value = data[key];
|
|
49
|
-
if (typeof value !== "boolean") {
|
|
50
|
-
throw new Error(`Unexpected API response: expected ${context}.${key} to be a boolean`);
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
function expectPaginatedCollectionField(data, key, context) {
|
|
55
|
-
const items = data[key];
|
|
56
|
-
if (!Array.isArray(items)) {
|
|
57
|
-
throw new Error(`Unexpected API response: expected ${key} array`);
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
items,
|
|
61
|
-
lastPage: expectBooleanField(data, "last_page", context)
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function isCustomTaskId(id) {
|
|
65
|
-
return /^[A-Z]+-\d+$/i.test(id);
|
|
66
|
-
}
|
|
67
|
-
var ClickUpClient = class {
|
|
68
|
-
apiToken;
|
|
69
|
-
teamId;
|
|
70
|
-
meCache = null;
|
|
71
|
-
constructor(config) {
|
|
72
|
-
this.apiToken = config.apiToken;
|
|
73
|
-
this.teamId = config.teamId;
|
|
74
|
-
}
|
|
75
|
-
taskPath(taskId, suffix = "") {
|
|
76
|
-
const base = `/task/${taskId}${suffix}`;
|
|
77
|
-
if (isCustomTaskId(taskId) && this.teamId) {
|
|
78
|
-
const sep = base.includes("?") ? "&" : "?";
|
|
79
|
-
return `${base}${sep}custom_task_ids=true&team_id=${this.teamId}`;
|
|
80
|
-
}
|
|
81
|
-
return base;
|
|
82
|
-
}
|
|
83
|
-
customIdQueryParams(taskId) {
|
|
84
|
-
if (isCustomTaskId(taskId) && this.teamId) {
|
|
85
|
-
return `?custom_task_ids=true&team_id=${this.teamId}`;
|
|
86
|
-
}
|
|
87
|
-
return "";
|
|
88
|
-
}
|
|
89
|
-
async _fetch(baseUrl, path, options = {}) {
|
|
90
|
-
const res = await fetch(`${baseUrl}${path}`, {
|
|
91
|
-
...options,
|
|
92
|
-
signal: AbortSignal.timeout(3e4),
|
|
93
|
-
headers: {
|
|
94
|
-
Authorization: this.apiToken,
|
|
95
|
-
...options.body ? { "Content-Type": "application/json" } : {},
|
|
96
|
-
...options.headers
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
100
|
-
if (!res.ok) {
|
|
101
|
-
throw new Error(`ClickUp API error ${res.status}: ${res.statusText}`);
|
|
102
|
-
}
|
|
103
|
-
return {};
|
|
104
|
-
}
|
|
105
|
-
let parsed;
|
|
106
|
-
try {
|
|
107
|
-
parsed = await res.json();
|
|
108
|
-
} catch {
|
|
109
|
-
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
110
|
-
}
|
|
111
|
-
const data = expectRecord(parsed, "JSON");
|
|
112
|
-
if (!res.ok) {
|
|
113
|
-
const raw = data.err ?? data.error ?? data.ECODE ?? res.statusText;
|
|
114
|
-
const errMsg = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
115
|
-
throw new Error(`ClickUp API error ${res.status}: ${errMsg}`);
|
|
116
|
-
}
|
|
117
|
-
return data;
|
|
118
|
-
}
|
|
119
|
-
async request(path, options = {}) {
|
|
120
|
-
return this._fetch(BASE_URL, path, options);
|
|
121
|
-
}
|
|
122
|
-
async requestV3(path, options = {}) {
|
|
123
|
-
return this._fetch(BASE_URL_V3, path, options);
|
|
124
|
-
}
|
|
125
|
-
async requestV3Array(path) {
|
|
126
|
-
const res = await fetch(`${BASE_URL_V3}${path}`, {
|
|
127
|
-
signal: AbortSignal.timeout(3e4),
|
|
128
|
-
headers: { Authorization: this.apiToken }
|
|
129
|
-
});
|
|
130
|
-
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
131
|
-
if (!res.ok) {
|
|
132
|
-
throw new Error(`ClickUp API error ${res.status}: ${res.statusText}`);
|
|
133
|
-
}
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
let parsed;
|
|
137
|
-
try {
|
|
138
|
-
parsed = await res.json();
|
|
139
|
-
} catch {
|
|
140
|
-
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
141
|
-
}
|
|
142
|
-
if (!res.ok) {
|
|
143
|
-
let errMsg = res.statusText;
|
|
144
|
-
if (isRecord(parsed)) {
|
|
145
|
-
const raw = parsed.err ?? parsed.error ?? parsed.ECODE;
|
|
146
|
-
if (typeof raw === "string") errMsg = raw;
|
|
147
|
-
}
|
|
148
|
-
throw new Error(`ClickUp API error ${res.status}: ${errMsg}`);
|
|
149
|
-
}
|
|
150
|
-
if (!Array.isArray(parsed)) {
|
|
151
|
-
throw new Error("Unexpected API response: expected JSON array");
|
|
152
|
-
}
|
|
153
|
-
return parsed;
|
|
154
|
-
}
|
|
155
|
-
async getMe() {
|
|
156
|
-
if (this.meCache) return this.meCache;
|
|
157
|
-
const data = await this.request(
|
|
158
|
-
"/user"
|
|
159
|
-
);
|
|
160
|
-
const user = expectRecordField(data, "user", "user");
|
|
161
|
-
const timezone = typeof user.timezone === "string" && user.timezone ? user.timezone : void 0;
|
|
162
|
-
this.meCache = {
|
|
163
|
-
id: expectNumericField(user, "id", "user"),
|
|
164
|
-
username: expectStringField(user, "username", "user"),
|
|
165
|
-
...timezone ? { timezone } : {}
|
|
166
|
-
};
|
|
167
|
-
return this.meCache;
|
|
168
|
-
}
|
|
169
|
-
async getUserTimezone() {
|
|
170
|
-
const me = await this.getMe();
|
|
171
|
-
return me.timezone;
|
|
172
|
-
}
|
|
173
|
-
async paginate(buildPath) {
|
|
174
|
-
const allTasks = [];
|
|
175
|
-
let page = 0;
|
|
176
|
-
let lastPage = false;
|
|
177
|
-
while (!lastPage && page < MAX_PAGES) {
|
|
178
|
-
const data = await this.request(buildPath(page));
|
|
179
|
-
const taskPage = expectPaginatedCollectionField(
|
|
180
|
-
data,
|
|
181
|
-
"tasks",
|
|
182
|
-
"task page"
|
|
183
|
-
);
|
|
184
|
-
allTasks.push(...taskPage.items);
|
|
185
|
-
lastPage = taskPage.lastPage;
|
|
186
|
-
page++;
|
|
187
|
-
}
|
|
188
|
-
if (page >= MAX_PAGES && !lastPage) {
|
|
189
|
-
process.stderr.write(
|
|
190
|
-
`Warning: reached maximum page limit (${MAX_PAGES}), results may be incomplete
|
|
191
|
-
`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
return allTasks;
|
|
195
|
-
}
|
|
196
|
-
async getMyTasks(teamId, filters = {}) {
|
|
197
|
-
const baseParams = new URLSearchParams({
|
|
198
|
-
subtasks: String(filters.subtasks ?? true)
|
|
199
|
-
});
|
|
200
|
-
if (filters.includeClosed) baseParams.set("include_closed", "true");
|
|
201
|
-
if (!filters.all) {
|
|
202
|
-
const me = await this.getMe();
|
|
203
|
-
baseParams.append("assignees[]", String(me.id));
|
|
204
|
-
}
|
|
205
|
-
if (filters.assignees) {
|
|
206
|
-
for (const id of filters.assignees) baseParams.append("assignees[]", String(id));
|
|
207
|
-
}
|
|
208
|
-
for (const s of filters.statuses ?? []) baseParams.append("statuses[]", s);
|
|
209
|
-
for (const id of filters.listIds ?? []) baseParams.append("list_ids[]", id);
|
|
210
|
-
for (const id of filters.spaceIds ?? []) baseParams.append("space_ids[]", id);
|
|
211
|
-
for (const tag of filters.tags ?? []) baseParams.append("tags[]", tag);
|
|
212
|
-
if (filters.dueDateGt) baseParams.set("due_date_gt", String(filters.dueDateGt));
|
|
213
|
-
if (filters.dueDateLt) baseParams.set("due_date_lt", String(filters.dueDateLt));
|
|
214
|
-
if (filters.dateCreatedGt) baseParams.set("date_created_gt", String(filters.dateCreatedGt));
|
|
215
|
-
if (filters.dateCreatedLt) baseParams.set("date_created_lt", String(filters.dateCreatedLt));
|
|
216
|
-
if (filters.dateUpdatedGt) baseParams.set("date_updated_gt", String(filters.dateUpdatedGt));
|
|
217
|
-
if (filters.dateUpdatedLt) baseParams.set("date_updated_lt", String(filters.dateUpdatedLt));
|
|
218
|
-
if (filters.customFields?.length) {
|
|
219
|
-
baseParams.set("custom_fields", JSON.stringify(filters.customFields));
|
|
220
|
-
}
|
|
221
|
-
return this.paginate((page) => {
|
|
222
|
-
const params = new URLSearchParams(baseParams);
|
|
223
|
-
params.set("page", String(page));
|
|
224
|
-
return `/team/${teamId}/task?${params.toString()}`;
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
async updateTask(taskId, options) {
|
|
228
|
-
return this.request(this.taskPath(taskId), {
|
|
229
|
-
method: "PUT",
|
|
230
|
-
body: JSON.stringify(options)
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
async postComment(taskId, commentText, notifyAll) {
|
|
234
|
-
const body = { comment_text: commentText };
|
|
235
|
-
if (notifyAll) body.notify_all = true;
|
|
236
|
-
return this.request(this.taskPath(taskId, "/comment"), {
|
|
237
|
-
method: "POST",
|
|
238
|
-
body: JSON.stringify(body)
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
async getTaskComments(taskId) {
|
|
242
|
-
const data = await this.request(this.taskPath(taskId, "/comment"));
|
|
243
|
-
return readCollectionField(
|
|
244
|
-
data,
|
|
245
|
-
"comments",
|
|
246
|
-
"task comments"
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
async getTasksFromList(listId, params = {}, options = {}) {
|
|
250
|
-
return this.paginate((page) => {
|
|
251
|
-
const base = { subtasks: "true", page: String(page), ...params };
|
|
252
|
-
if (options.includeClosed) base["include_closed"] = "true";
|
|
253
|
-
const qs = new URLSearchParams(base).toString();
|
|
254
|
-
return `/list/${listId}/task?${qs}`;
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
async getTask(taskId) {
|
|
258
|
-
return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
|
|
259
|
-
}
|
|
260
|
-
async getTimeInStatus(taskId) {
|
|
261
|
-
return this.request(this.taskPath(taskId, "/time_in_status"));
|
|
262
|
-
}
|
|
263
|
-
async createTask(listId, options) {
|
|
264
|
-
return this.request(`/list/${listId}/task`, {
|
|
265
|
-
method: "POST",
|
|
266
|
-
body: JSON.stringify(options)
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
async getTeams() {
|
|
270
|
-
const data = await this.request("/team");
|
|
271
|
-
return readCollectionField(data, "teams", "teams");
|
|
272
|
-
}
|
|
273
|
-
async getSpaceWithStatuses(spaceId) {
|
|
274
|
-
return this.request(`/space/${spaceId}`);
|
|
275
|
-
}
|
|
276
|
-
async getListWithStatuses(listId) {
|
|
277
|
-
return this.request(`/list/${listId}`);
|
|
278
|
-
}
|
|
279
|
-
async createSpace(teamId, name) {
|
|
280
|
-
return this.request(`/team/${teamId}/space`, {
|
|
281
|
-
method: "POST",
|
|
282
|
-
body: JSON.stringify({ name, multiple_assignees: true })
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
async getSpaces(teamId) {
|
|
286
|
-
const data = await this.request(`/team/${teamId}/space?archived=false`);
|
|
287
|
-
return readCollectionField(data, "spaces", "spaces");
|
|
288
|
-
}
|
|
289
|
-
async getCustomTaskTypes(teamId) {
|
|
290
|
-
const data = await this.request(
|
|
291
|
-
`/team/${teamId}/custom_item`
|
|
292
|
-
);
|
|
293
|
-
return readCollectionField(
|
|
294
|
-
data,
|
|
295
|
-
"custom_items",
|
|
296
|
-
"custom task types"
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
async createList(spaceId, name) {
|
|
300
|
-
return this.request(`/space/${spaceId}/list`, {
|
|
301
|
-
method: "POST",
|
|
302
|
-
body: JSON.stringify({ name })
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
async createFolderList(folderId, name) {
|
|
306
|
-
return this.request(`/folder/${folderId}/list`, {
|
|
307
|
-
method: "POST",
|
|
308
|
-
body: JSON.stringify({ name })
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
async updateList(listId, payload) {
|
|
312
|
-
return this.request(`/list/${listId}`, {
|
|
313
|
-
method: "PUT",
|
|
314
|
-
body: JSON.stringify(payload)
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
async createFolder(spaceId, name) {
|
|
318
|
-
return this.request(`/space/${spaceId}/folder`, {
|
|
319
|
-
method: "POST",
|
|
320
|
-
body: JSON.stringify({ name })
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
async getLists(spaceId) {
|
|
324
|
-
const data = await this.request(`/space/${spaceId}/list?archived=false`);
|
|
325
|
-
return readCollectionField(data, "lists", "space lists");
|
|
326
|
-
}
|
|
327
|
-
async getFolders(spaceId) {
|
|
328
|
-
const data = await this.request(
|
|
329
|
-
`/space/${spaceId}/folder?archived=false`
|
|
330
|
-
);
|
|
331
|
-
return readCollectionField(data, "folders", "space folders");
|
|
332
|
-
}
|
|
333
|
-
async getFolderLists(folderId) {
|
|
334
|
-
const data = await this.request(`/folder/${folderId}/list?archived=false`);
|
|
335
|
-
return readCollectionField(data, "lists", "folder lists");
|
|
336
|
-
}
|
|
337
|
-
async getListViews(listId) {
|
|
338
|
-
return this.request(`/list/${listId}/view`);
|
|
339
|
-
}
|
|
340
|
-
async getSpaceViews(spaceId) {
|
|
341
|
-
const data = await this.request(`/space/${spaceId}/view`);
|
|
342
|
-
return readCollectionField(data, "views", "views");
|
|
343
|
-
}
|
|
344
|
-
async getFolderViews(folderId) {
|
|
345
|
-
const data = await this.request(`/folder/${folderId}/view`);
|
|
346
|
-
return readCollectionField(data, "views", "views");
|
|
347
|
-
}
|
|
348
|
-
async getWorkspaceViews(teamId) {
|
|
349
|
-
const data = await this.request(`/team/${teamId}/view`);
|
|
350
|
-
return readCollectionField(data, "views", "views");
|
|
351
|
-
}
|
|
352
|
-
async getViewTasks(viewId) {
|
|
353
|
-
return this.paginate((page) => `/view/${viewId}/task?page=${page}`);
|
|
354
|
-
}
|
|
355
|
-
async getView(viewId) {
|
|
356
|
-
const data = await this.request(`/view/${viewId}`);
|
|
357
|
-
return expectRecordField(data, "view", "view");
|
|
358
|
-
}
|
|
359
|
-
async createListView(listId, payload) {
|
|
360
|
-
const data = await this.request(`/list/${listId}/view`, {
|
|
361
|
-
method: "POST",
|
|
362
|
-
body: JSON.stringify(payload)
|
|
363
|
-
});
|
|
364
|
-
return expectRecordField(data, "view", "view");
|
|
365
|
-
}
|
|
366
|
-
async updateView(viewId, payload) {
|
|
367
|
-
const data = await this.request(`/view/${viewId}`, {
|
|
368
|
-
method: "PUT",
|
|
369
|
-
body: JSON.stringify(payload)
|
|
370
|
-
});
|
|
371
|
-
return expectRecordField(data, "view", "view");
|
|
372
|
-
}
|
|
373
|
-
async deleteView(viewId) {
|
|
374
|
-
await this.request(`/view/${viewId}`, { method: "DELETE" });
|
|
375
|
-
}
|
|
376
|
-
async getListTemplates(teamId) {
|
|
377
|
-
const data = await this.request(`/team/${teamId}/list_template`);
|
|
378
|
-
return readCollectionField(
|
|
379
|
-
data,
|
|
380
|
-
"templates",
|
|
381
|
-
"list templates"
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
async getFolderTemplates(teamId) {
|
|
385
|
-
const data = await this.request(
|
|
386
|
-
`/team/${teamId}/folder_template`
|
|
387
|
-
);
|
|
388
|
-
return readCollectionField(
|
|
389
|
-
data,
|
|
390
|
-
"templates",
|
|
391
|
-
"folder templates"
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
async createListFromTemplate(containerId, templateId, name, containerType) {
|
|
395
|
-
return this.request(
|
|
396
|
-
`/${containerType}/${containerId}/list_template/${templateId}`,
|
|
397
|
-
{ method: "POST", body: JSON.stringify({ name }) }
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
async addTaskToList(taskId, listId) {
|
|
401
|
-
await this.request(`/list/${listId}/task/${taskId}`, { method: "POST" });
|
|
402
|
-
}
|
|
403
|
-
async removeTaskFromList(taskId, listId) {
|
|
404
|
-
await this.request(`/list/${listId}/task/${taskId}`, { method: "DELETE" });
|
|
405
|
-
}
|
|
406
|
-
async setCustomFieldValue(taskId, fieldId, value) {
|
|
407
|
-
await this.request(this.taskPath(taskId, `/field/${fieldId}`), {
|
|
408
|
-
method: "POST",
|
|
409
|
-
body: JSON.stringify({ value })
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
async removeCustomFieldValue(taskId, fieldId) {
|
|
413
|
-
await this.request(this.taskPath(taskId, `/field/${fieldId}`), { method: "DELETE" });
|
|
414
|
-
}
|
|
415
|
-
async deleteTask(taskId) {
|
|
416
|
-
await this.request(this.taskPath(taskId), { method: "DELETE" });
|
|
417
|
-
}
|
|
418
|
-
async addTagToTask(taskId, tagName) {
|
|
419
|
-
await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
|
|
420
|
-
method: "POST"
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
async removeTagFromTask(taskId, tagName) {
|
|
424
|
-
await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
|
|
425
|
-
method: "DELETE"
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
async addDependency(taskId, opts) {
|
|
429
|
-
const body = {};
|
|
430
|
-
if (opts.dependsOn) body.depends_on = opts.dependsOn;
|
|
431
|
-
if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
|
|
432
|
-
await this.request(this.taskPath(taskId, "/dependency"), {
|
|
433
|
-
method: "POST",
|
|
434
|
-
body: JSON.stringify(body)
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
async deleteDependency(taskId, opts) {
|
|
438
|
-
const params = new URLSearchParams();
|
|
439
|
-
if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
|
|
440
|
-
if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
|
|
441
|
-
await this.request(this.taskPath(taskId, `/dependency?${params.toString()}`), {
|
|
442
|
-
method: "DELETE"
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
async updateComment(commentId, text, resolved) {
|
|
446
|
-
const body = { comment_text: text };
|
|
447
|
-
if (resolved !== void 0) body.resolved = resolved;
|
|
448
|
-
await this.request(`/comment/${commentId}`, {
|
|
449
|
-
method: "PUT",
|
|
450
|
-
body: JSON.stringify(body)
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
async deleteComment(commentId) {
|
|
454
|
-
await this.request(`/comment/${commentId}`, { method: "DELETE" });
|
|
455
|
-
}
|
|
456
|
-
async getThreadedComments(commentId) {
|
|
457
|
-
const data = await this.request(`/comment/${commentId}/reply`);
|
|
458
|
-
return readCollectionField(
|
|
459
|
-
data,
|
|
460
|
-
"comments",
|
|
461
|
-
"threaded comments"
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
async createThreadedComment(commentId, text, notifyAll) {
|
|
465
|
-
const body = { comment_text: text };
|
|
466
|
-
if (notifyAll) body.notify_all = true;
|
|
467
|
-
await this.request(`/comment/${commentId}/reply`, {
|
|
468
|
-
method: "POST",
|
|
469
|
-
body: JSON.stringify(body)
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
async addTaskLink(taskId, linksTo) {
|
|
473
|
-
await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
|
|
474
|
-
method: "POST"
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
async deleteTaskLink(taskId, linksTo) {
|
|
478
|
-
await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
|
|
479
|
-
method: "DELETE"
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
async getListCustomFields(listId) {
|
|
483
|
-
const data = await this.request(`/list/${listId}/field`);
|
|
484
|
-
return readCollectionField(
|
|
485
|
-
data,
|
|
486
|
-
"fields",
|
|
487
|
-
"list custom fields"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
async createChecklist(taskId, name) {
|
|
491
|
-
const data = await this.request(this.taskPath(taskId, "/checklist"), {
|
|
492
|
-
method: "POST",
|
|
493
|
-
body: JSON.stringify({ name })
|
|
494
|
-
});
|
|
495
|
-
return expectRecordField(
|
|
496
|
-
data,
|
|
497
|
-
"checklist",
|
|
498
|
-
"checklist"
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
async deleteChecklist(checklistId) {
|
|
502
|
-
await this.request(`/checklist/${checklistId}`, { method: "DELETE" });
|
|
503
|
-
}
|
|
504
|
-
async createChecklistItem(checklistId, name) {
|
|
505
|
-
const data = await this.request(
|
|
506
|
-
`/checklist/${checklistId}/checklist_item`,
|
|
507
|
-
{ method: "POST", body: JSON.stringify({ name }) }
|
|
508
|
-
);
|
|
509
|
-
return expectRecordField(
|
|
510
|
-
data,
|
|
511
|
-
"checklist",
|
|
512
|
-
"checklist"
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
async editChecklistItem(checklistId, checklistItemId, updates) {
|
|
516
|
-
const data = await this.request(
|
|
517
|
-
`/checklist/${checklistId}/checklist_item/${checklistItemId}`,
|
|
518
|
-
{ method: "PUT", body: JSON.stringify(updates) }
|
|
519
|
-
);
|
|
520
|
-
return expectRecordField(
|
|
521
|
-
data,
|
|
522
|
-
"checklist",
|
|
523
|
-
"checklist"
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
async deleteChecklistItem(checklistId, checklistItemId) {
|
|
527
|
-
await this.request(
|
|
528
|
-
`/checklist/${checklistId}/checklist_item/${checklistItemId}`,
|
|
529
|
-
{ method: "DELETE" }
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
async startTimeEntry(teamId, taskId, description) {
|
|
533
|
-
const body = {
|
|
534
|
-
tid: taskId,
|
|
535
|
-
start: Date.now(),
|
|
536
|
-
duration: -1
|
|
537
|
-
};
|
|
538
|
-
if (description) body.description = description;
|
|
539
|
-
const data = await this.request(
|
|
540
|
-
`/team/${teamId}/time_entries/start${this.customIdQueryParams(taskId)}`,
|
|
541
|
-
{
|
|
542
|
-
method: "POST",
|
|
543
|
-
body: JSON.stringify(body)
|
|
544
|
-
}
|
|
545
|
-
);
|
|
546
|
-
return data.data;
|
|
547
|
-
}
|
|
548
|
-
async stopTimeEntry(teamId) {
|
|
549
|
-
const data = await this.request(`/team/${teamId}/time_entries/stop`, {
|
|
550
|
-
method: "POST"
|
|
551
|
-
});
|
|
552
|
-
return data.data;
|
|
553
|
-
}
|
|
554
|
-
async getRunningTimeEntry(teamId) {
|
|
555
|
-
const data = await this.request(
|
|
556
|
-
`/team/${teamId}/time_entries/current`
|
|
557
|
-
);
|
|
558
|
-
return data.data ?? null;
|
|
559
|
-
}
|
|
560
|
-
async createTimeEntry(teamId, taskId, duration, opts) {
|
|
561
|
-
const start = opts?.start ?? Date.now() - duration;
|
|
562
|
-
const body = {
|
|
563
|
-
tid: taskId,
|
|
564
|
-
start,
|
|
565
|
-
duration
|
|
566
|
-
};
|
|
567
|
-
if (opts?.description) body.description = opts.description;
|
|
568
|
-
const data = await this.request(
|
|
569
|
-
`/team/${teamId}/time_entries${this.customIdQueryParams(taskId)}`,
|
|
570
|
-
{
|
|
571
|
-
method: "POST",
|
|
572
|
-
body: JSON.stringify(body)
|
|
573
|
-
}
|
|
574
|
-
);
|
|
575
|
-
return data.data;
|
|
576
|
-
}
|
|
577
|
-
async getTimeEntries(teamId, opts) {
|
|
578
|
-
const params = new URLSearchParams();
|
|
579
|
-
if (opts?.startDate != null) params.set("start_date", String(opts.startDate));
|
|
580
|
-
if (opts?.endDate != null) params.set("end_date", String(opts.endDate));
|
|
581
|
-
if (opts?.spaceId) params.set("space_id", opts.spaceId);
|
|
582
|
-
if (opts?.listId) params.set("list_id", opts.listId);
|
|
583
|
-
if (opts?.assigneeId) params.set("assignee", opts.assigneeId);
|
|
584
|
-
const query = params.toString();
|
|
585
|
-
const url = `/team/${teamId}/time_entries${query ? `?${query}` : ""}`;
|
|
586
|
-
const data = await this.request(url);
|
|
587
|
-
const entries = readCollectionField(
|
|
588
|
-
data,
|
|
589
|
-
"data",
|
|
590
|
-
"time entries"
|
|
591
|
-
);
|
|
592
|
-
if (opts?.taskId) {
|
|
593
|
-
return entries.filter((e) => e.task?.id === opts.taskId);
|
|
594
|
-
}
|
|
595
|
-
return entries;
|
|
596
|
-
}
|
|
597
|
-
async updateTimeEntry(teamId, timeEntryId, updates) {
|
|
598
|
-
const data = await this.request(
|
|
599
|
-
`/team/${teamId}/time_entries/${timeEntryId}`,
|
|
600
|
-
{ method: "PUT", body: JSON.stringify(updates) }
|
|
601
|
-
);
|
|
602
|
-
return data.data;
|
|
603
|
-
}
|
|
604
|
-
async getSpaceTags(spaceId) {
|
|
605
|
-
const data = await this.request(`/space/${spaceId}/tag`);
|
|
606
|
-
return readCollectionField(data, "tags", "space tags");
|
|
607
|
-
}
|
|
608
|
-
async createSpaceTag(spaceId, name, fg, bg) {
|
|
609
|
-
await this.request(`/space/${spaceId}/tag`, {
|
|
610
|
-
method: "POST",
|
|
611
|
-
body: JSON.stringify({
|
|
612
|
-
tag: { name, tag_fg: fg ?? "#000000", tag_bg: bg ?? "#04A9F4" }
|
|
613
|
-
})
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
async deleteSpaceTag(spaceId, tagName) {
|
|
617
|
-
await this.request(
|
|
618
|
-
`/space/${spaceId}/tag/${encodeURIComponent(tagName)}`,
|
|
619
|
-
{ method: "DELETE" }
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
async getWorkspaceMembers(teamId) {
|
|
623
|
-
const data = await this.request("/team");
|
|
624
|
-
const team = readCollectionField(
|
|
625
|
-
data,
|
|
626
|
-
"teams",
|
|
627
|
-
"workspace members"
|
|
628
|
-
).find((t) => t.id === teamId);
|
|
629
|
-
return team?.members?.map((m) => m.user) ?? [];
|
|
630
|
-
}
|
|
631
|
-
async deleteTimeEntry(teamId, timeEntryId) {
|
|
632
|
-
await this.request(`/team/${teamId}/time_entries/${timeEntryId}`, {
|
|
633
|
-
method: "DELETE"
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
async createTaskAttachment(taskId, filePath) {
|
|
637
|
-
const { readFile } = await import("fs/promises");
|
|
638
|
-
const { basename } = await import("path");
|
|
639
|
-
const fileBuffer = await readFile(filePath);
|
|
640
|
-
const fileName = basename(filePath);
|
|
641
|
-
const formData = new FormData();
|
|
642
|
-
formData.append("attachment", new Blob([fileBuffer]), fileName);
|
|
643
|
-
const res = await fetch(`${BASE_URL}${this.taskPath(taskId, "/attachment")}`, {
|
|
644
|
-
method: "POST",
|
|
645
|
-
headers: { Authorization: this.apiToken },
|
|
646
|
-
body: formData,
|
|
647
|
-
signal: AbortSignal.timeout(6e4)
|
|
648
|
-
});
|
|
649
|
-
if (!res.ok) {
|
|
650
|
-
let msg;
|
|
651
|
-
try {
|
|
652
|
-
const data2 = await res.json();
|
|
653
|
-
msg = data2.err ?? `HTTP ${res.status}`;
|
|
654
|
-
} catch {
|
|
655
|
-
msg = `HTTP ${res.status}`;
|
|
656
|
-
}
|
|
657
|
-
throw new Error(`ClickUp API error ${res.status}: ${msg}`);
|
|
658
|
-
}
|
|
659
|
-
let data;
|
|
660
|
-
try {
|
|
661
|
-
data = await res.json();
|
|
662
|
-
} catch {
|
|
663
|
-
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
664
|
-
}
|
|
665
|
-
return data;
|
|
666
|
-
}
|
|
667
|
-
async getDocs(workspaceId) {
|
|
668
|
-
const data = await this.requestV3(`/workspaces/${workspaceId}/docs`);
|
|
669
|
-
return readCollectionField(data, "docs", "docs");
|
|
670
|
-
}
|
|
671
|
-
async getDocPage(workspaceId, docId, pageId) {
|
|
672
|
-
return this.requestV3(
|
|
673
|
-
`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}?content_format=text/md`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
async createDoc(workspaceId, title, content, parentId) {
|
|
677
|
-
const body = { title };
|
|
678
|
-
if (content) body.content = content;
|
|
679
|
-
if (parentId) {
|
|
680
|
-
body.parent_id = parentId;
|
|
681
|
-
body.parent_type = "doc";
|
|
682
|
-
}
|
|
683
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs`, {
|
|
684
|
-
method: "POST",
|
|
685
|
-
body: JSON.stringify(body)
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
async createDocPage(workspaceId, docId, name, content, parentPageId) {
|
|
689
|
-
const body = { name, content_format: "text/md" };
|
|
690
|
-
if (content) body.content = content;
|
|
691
|
-
if (parentPageId) body.parent_page_id = parentPageId;
|
|
692
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages`, {
|
|
693
|
-
method: "POST",
|
|
694
|
-
body: JSON.stringify(body)
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
async editDocPage(workspaceId, docId, pageId, updates) {
|
|
698
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`, {
|
|
699
|
-
method: "PUT",
|
|
700
|
-
body: JSON.stringify(updates)
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
async getDoc(workspaceId, docId) {
|
|
704
|
-
return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`);
|
|
705
|
-
}
|
|
706
|
-
async getDocPageListing(workspaceId, docId) {
|
|
707
|
-
return this.requestV3Array(`/workspaces/${workspaceId}/docs/${docId}/pages`);
|
|
708
|
-
}
|
|
709
|
-
async getDocPages(workspaceId, docId) {
|
|
710
|
-
return this.requestV3Array(
|
|
711
|
-
`/workspaces/${workspaceId}/docs/${docId}/pages?content_format=text/md`
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
async getGoals(teamId) {
|
|
715
|
-
const data = await this.request(`/team/${teamId}/goal`);
|
|
716
|
-
return readCollectionField(data, "goals", "goals");
|
|
717
|
-
}
|
|
718
|
-
async createGoal(teamId, name, opts) {
|
|
719
|
-
const body = { name, multiple_owners: true };
|
|
720
|
-
if (opts?.description) body.description = opts.description;
|
|
721
|
-
if (opts?.dueDate != null) body.due_date = opts.dueDate;
|
|
722
|
-
if (opts?.color) body.color = opts.color;
|
|
723
|
-
const data = await this.request(`/team/${teamId}/goal`, {
|
|
724
|
-
method: "POST",
|
|
725
|
-
body: JSON.stringify(body)
|
|
726
|
-
});
|
|
727
|
-
return data.goal;
|
|
728
|
-
}
|
|
729
|
-
async updateGoal(goalId, updates) {
|
|
730
|
-
const data = await this.request(`/goal/${goalId}`, {
|
|
731
|
-
method: "PUT",
|
|
732
|
-
body: JSON.stringify(updates)
|
|
733
|
-
});
|
|
734
|
-
return data.goal;
|
|
735
|
-
}
|
|
736
|
-
async getKeyResults(goalId) {
|
|
737
|
-
const data = await this.request(`/goal/${goalId}`);
|
|
738
|
-
return data.goal?.key_results ?? [];
|
|
739
|
-
}
|
|
740
|
-
async createKeyResult(goalId, name, type, stepsEnd) {
|
|
741
|
-
const data = await this.request(`/goal/${goalId}/key_result`, {
|
|
742
|
-
method: "POST",
|
|
743
|
-
body: JSON.stringify({
|
|
744
|
-
name,
|
|
745
|
-
type,
|
|
746
|
-
steps_start: 0,
|
|
747
|
-
steps_end: stepsEnd,
|
|
748
|
-
unit: type === "number" ? "items" : "%"
|
|
749
|
-
})
|
|
750
|
-
});
|
|
751
|
-
return data.key_result;
|
|
752
|
-
}
|
|
753
|
-
async updateKeyResult(keyResultId, updates) {
|
|
754
|
-
const data = await this.request(`/key_result/${keyResultId}`, {
|
|
755
|
-
method: "PUT",
|
|
756
|
-
body: JSON.stringify(updates)
|
|
757
|
-
});
|
|
758
|
-
return data.key_result;
|
|
759
|
-
}
|
|
760
|
-
async deleteGoal(goalId) {
|
|
761
|
-
await this.request(`/goal/${goalId}`, { method: "DELETE" });
|
|
762
|
-
}
|
|
763
|
-
async deleteKeyResult(keyResultId) {
|
|
764
|
-
await this.request(`/key_result/${keyResultId}`, { method: "DELETE" });
|
|
765
|
-
}
|
|
766
|
-
async deleteDoc(workspaceId, docId) {
|
|
767
|
-
await this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`, {
|
|
768
|
-
method: "DELETE"
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
async deleteDocPage(workspaceId, docId, pageId) {
|
|
772
|
-
await this.requestV3(
|
|
773
|
-
`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`,
|
|
774
|
-
{ method: "DELETE" }
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
async updateSpaceTag(spaceId, tagName, updates) {
|
|
778
|
-
await this.request(
|
|
779
|
-
`/space/${spaceId}/tag/${encodeURIComponent(tagName)}`,
|
|
780
|
-
{
|
|
781
|
-
method: "PUT",
|
|
782
|
-
body: JSON.stringify({
|
|
783
|
-
tag: {
|
|
784
|
-
name: updates.name,
|
|
785
|
-
tag_fg: updates.tag_fg ?? "#000000",
|
|
786
|
-
tag_bg: updates.tag_bg ?? "#04A9F4"
|
|
787
|
-
}
|
|
788
|
-
})
|
|
789
|
-
}
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
async getTaskTemplates(teamId) {
|
|
793
|
-
const data = await this.request(
|
|
794
|
-
`/team/${teamId}/taskTemplate?page=0`
|
|
795
|
-
);
|
|
796
|
-
return readCollectionField(
|
|
797
|
-
data,
|
|
798
|
-
"templates",
|
|
799
|
-
"task templates"
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
async createTaskFromTemplate(listId, templateId, name) {
|
|
803
|
-
return this.request(`/list/${listId}/taskTemplate/${templateId}`, {
|
|
804
|
-
method: "POST",
|
|
805
|
-
body: JSON.stringify({ name })
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
async createCustomField(teamId, name, type, opts) {
|
|
809
|
-
const typeConfig = {};
|
|
810
|
-
if (opts?.options?.length) {
|
|
811
|
-
typeConfig.options = opts.options.map((optName, i) => ({
|
|
812
|
-
name: optName,
|
|
813
|
-
orderindex: i
|
|
814
|
-
}));
|
|
815
|
-
}
|
|
816
|
-
const body = {
|
|
817
|
-
name,
|
|
818
|
-
type,
|
|
819
|
-
type_config: typeConfig,
|
|
820
|
-
description: opts?.description ?? "",
|
|
821
|
-
required: opts?.required ?? false,
|
|
822
|
-
pinned: false,
|
|
823
|
-
hide_from_guests: false,
|
|
824
|
-
required_on_subtasks: false,
|
|
825
|
-
private: false,
|
|
826
|
-
permission_level: null,
|
|
827
|
-
members: [],
|
|
828
|
-
groups: []
|
|
829
|
-
};
|
|
830
|
-
const data = await this.request(
|
|
831
|
-
`/field?workspace_id=${teamId}`,
|
|
832
|
-
{ method: "POST", body: JSON.stringify(body) }
|
|
833
|
-
);
|
|
834
|
-
return data.data;
|
|
835
|
-
}
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
// src/config.ts
|
|
839
|
-
import fs from "fs";
|
|
840
|
-
import { homedir } from "os";
|
|
841
|
-
import { join } from "path";
|
|
842
|
-
function isRecord2(value) {
|
|
843
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
844
|
-
}
|
|
845
|
-
function readConfigString(parsed, key, path, strict) {
|
|
846
|
-
const value = parsed[key];
|
|
847
|
-
if (value === void 0) return void 0;
|
|
848
|
-
if (typeof value !== "string") {
|
|
849
|
-
if (strict) {
|
|
850
|
-
throw new Error(`Config field ${key} must be a string in ${path}.`);
|
|
851
|
-
}
|
|
852
|
-
return void 0;
|
|
853
|
-
}
|
|
854
|
-
const trimmed = value.trim();
|
|
855
|
-
return trimmed || void 0;
|
|
856
|
-
}
|
|
857
|
-
function parseConfigFile(raw, path, strictFields, strictRoot = strictFields) {
|
|
858
|
-
let parsed;
|
|
859
|
-
try {
|
|
860
|
-
parsed = JSON.parse(raw);
|
|
861
|
-
} catch {
|
|
862
|
-
if (strictRoot) {
|
|
863
|
-
throw new Error(`Config file at ${path} contains invalid JSON. Please check the file syntax.`);
|
|
864
|
-
}
|
|
865
|
-
return {};
|
|
866
|
-
}
|
|
867
|
-
if (!isRecord2(parsed)) {
|
|
868
|
-
if (strictRoot) {
|
|
869
|
-
throw new Error(`Config file at ${path} must contain a JSON object.`);
|
|
870
|
-
}
|
|
871
|
-
return {};
|
|
872
|
-
}
|
|
873
|
-
const apiToken = readConfigString(parsed, "apiToken", path, strictFields);
|
|
874
|
-
const teamId = readConfigString(parsed, "teamId", path, strictFields);
|
|
875
|
-
const sprintFolderId = readConfigString(parsed, "sprintFolderId", path, strictFields);
|
|
876
|
-
return {
|
|
877
|
-
...apiToken ? { apiToken } : {},
|
|
878
|
-
...teamId ? { teamId } : {},
|
|
879
|
-
...sprintFolderId ? { sprintFolderId } : {}
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
function trimConfigValue(value) {
|
|
883
|
-
const trimmed = value?.trim();
|
|
884
|
-
return trimmed || void 0;
|
|
885
|
-
}
|
|
886
|
-
function configDir() {
|
|
887
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
888
|
-
if (xdg) return join(xdg, "cup");
|
|
889
|
-
return join(homedir(), ".config", "cup");
|
|
890
|
-
}
|
|
891
|
-
function legacyConfigDir() {
|
|
892
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
893
|
-
if (xdg) return join(xdg, "cu");
|
|
894
|
-
return join(homedir(), ".config", "cu");
|
|
895
|
-
}
|
|
896
|
-
var migrationChecked = false;
|
|
897
|
-
function migrateFromLegacy() {
|
|
898
|
-
if (migrationChecked) return;
|
|
899
|
-
migrationChecked = true;
|
|
900
|
-
const legacy = legacyConfigDir();
|
|
901
|
-
const current = configDir();
|
|
902
|
-
if (fs.existsSync(join(legacy, "config.json")) && !fs.existsSync(join(current, "config.json"))) {
|
|
903
|
-
fs.mkdirSync(current, { recursive: true, mode: 448 });
|
|
904
|
-
fs.copyFileSync(join(legacy, "config.json"), join(current, "config.json"));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
function configPath() {
|
|
908
|
-
return join(configDir(), "config.json");
|
|
909
|
-
}
|
|
910
|
-
function migrateToMultiProfile(parsed, filePath) {
|
|
911
|
-
if (typeof parsed.apiToken === "string" && !parsed.profiles) {
|
|
912
|
-
const profile = {};
|
|
913
|
-
const token = trimConfigValue(parsed.apiToken);
|
|
914
|
-
if (token) profile.apiToken = token;
|
|
915
|
-
const team = typeof parsed.teamId === "string" ? trimConfigValue(parsed.teamId) : void 0;
|
|
916
|
-
if (team) profile.teamId = team;
|
|
917
|
-
const sprint = typeof parsed.sprintFolderId === "string" ? trimConfigValue(parsed.sprintFolderId) : void 0;
|
|
918
|
-
if (sprint) profile.sprintFolderId = sprint;
|
|
919
|
-
const migrated = {
|
|
920
|
-
defaultProfile: "default",
|
|
921
|
-
profiles: { default: profile }
|
|
922
|
-
};
|
|
923
|
-
const dir = configDir();
|
|
924
|
-
if (!fs.existsSync(dir)) {
|
|
925
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
926
|
-
}
|
|
927
|
-
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(migrated, null, 2) + "\n", {
|
|
928
|
-
encoding: "utf-8",
|
|
929
|
-
mode: 384
|
|
930
|
-
});
|
|
931
|
-
return migrated;
|
|
932
|
-
}
|
|
933
|
-
if (isRecord2(parsed.profiles)) {
|
|
934
|
-
const profiles = {};
|
|
935
|
-
for (const [name, value] of Object.entries(parsed.profiles)) {
|
|
936
|
-
if (isRecord2(value)) {
|
|
937
|
-
const p = {};
|
|
938
|
-
if (typeof value.apiToken === "string" && value.apiToken.trim())
|
|
939
|
-
p.apiToken = value.apiToken.trim();
|
|
940
|
-
if (typeof value.teamId === "string" && value.teamId.trim()) p.teamId = value.teamId.trim();
|
|
941
|
-
if (typeof value.sprintFolderId === "string" && value.sprintFolderId.trim())
|
|
942
|
-
p.sprintFolderId = value.sprintFolderId.trim();
|
|
943
|
-
if (isRecord2(value.filters)) p.filters = value.filters;
|
|
944
|
-
if (isRecord2(value.favorites)) p.favorites = value.favorites;
|
|
945
|
-
profiles[name] = p;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
return {
|
|
949
|
-
defaultProfile: typeof parsed.defaultProfile === "string" ? parsed.defaultProfile : "",
|
|
950
|
-
profiles
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
throw new Error(`Config file at ${filePath} has unrecognized format.`);
|
|
954
|
-
}
|
|
955
|
-
function parseRawConfig(filePath) {
|
|
956
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
957
|
-
let parsed;
|
|
958
|
-
try {
|
|
959
|
-
parsed = JSON.parse(raw);
|
|
960
|
-
} catch {
|
|
961
|
-
throw new Error(
|
|
962
|
-
`Config file at ${filePath} contains invalid JSON. Please check the file syntax.`
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
if (!isRecord2(parsed)) {
|
|
966
|
-
throw new Error(`Config file at ${filePath} must contain a JSON object.`);
|
|
967
|
-
}
|
|
968
|
-
return { parsed, raw };
|
|
969
|
-
}
|
|
970
|
-
function isOldFormat(parsed) {
|
|
971
|
-
return typeof parsed.apiToken === "string" && !parsed.profiles;
|
|
972
|
-
}
|
|
973
|
-
function loadConfig(profileName) {
|
|
974
|
-
migrateFromLegacy();
|
|
975
|
-
const envToken = process.env.CU_API_TOKEN?.trim();
|
|
976
|
-
const envTeamId = process.env.CU_TEAM_ID?.trim();
|
|
977
|
-
if (envToken && envTeamId) {
|
|
978
|
-
if (!envToken.startsWith("pk_")) {
|
|
979
|
-
throw new Error("CU_API_TOKEN must start with pk_.");
|
|
980
|
-
}
|
|
981
|
-
return { apiToken: envToken, teamId: envTeamId };
|
|
982
|
-
}
|
|
983
|
-
const path = configPath();
|
|
984
|
-
if (!fs.existsSync(path)) {
|
|
985
|
-
if (envToken || envTeamId) {
|
|
986
|
-
throw new Error("Both CU_API_TOKEN and CU_TEAM_ID must be set, or run: cup init");
|
|
987
|
-
}
|
|
988
|
-
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
|
|
989
|
-
}
|
|
990
|
-
const { parsed } = parseRawConfig(path);
|
|
991
|
-
if (isOldFormat(parsed)) {
|
|
992
|
-
const fileConfig = parseConfigFile(JSON.stringify(parsed), path, true);
|
|
993
|
-
const apiToken2 = envToken ?? fileConfig.apiToken;
|
|
994
|
-
if (!apiToken2) {
|
|
995
|
-
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
|
|
996
|
-
}
|
|
997
|
-
if (!apiToken2.startsWith("pk_")) {
|
|
998
|
-
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
999
|
-
}
|
|
1000
|
-
const teamId2 = envTeamId ?? fileConfig.teamId;
|
|
1001
|
-
if (!teamId2) {
|
|
1002
|
-
throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cup init");
|
|
1003
|
-
}
|
|
1004
|
-
migrateToMultiProfile(parsed, path);
|
|
1005
|
-
return {
|
|
1006
|
-
apiToken: apiToken2,
|
|
1007
|
-
teamId: teamId2,
|
|
1008
|
-
...fileConfig.sprintFolderId ? { sprintFolderId: fileConfig.sprintFolderId } : {}
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
const multi = loadMultiProfileConfig();
|
|
1012
|
-
const resolvedProfile = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1013
|
-
if (!resolvedProfile) {
|
|
1014
|
-
throw new Error("No default profile set. Run: cup profile use <name>");
|
|
1015
|
-
}
|
|
1016
|
-
const profile = multi.profiles[resolvedProfile];
|
|
1017
|
-
if (!profile) {
|
|
1018
|
-
const available = Object.keys(multi.profiles).join(", ");
|
|
1019
|
-
throw new Error(`Profile "${resolvedProfile}" not found. Available: ${available}`);
|
|
1020
|
-
}
|
|
1021
|
-
const apiToken = envToken ?? profile.apiToken?.trim();
|
|
1022
|
-
if (!apiToken) {
|
|
1023
|
-
throw new Error(
|
|
1024
|
-
`Profile "${resolvedProfile}" missing apiToken. Run: cup profile add ${resolvedProfile}`
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
if (!apiToken.startsWith("pk_")) {
|
|
1028
|
-
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
1029
|
-
}
|
|
1030
|
-
const teamId = envTeamId ?? profile.teamId?.trim();
|
|
1031
|
-
if (!teamId) {
|
|
1032
|
-
throw new Error(`Profile "${resolvedProfile}" missing teamId.`);
|
|
1033
|
-
}
|
|
1034
|
-
return {
|
|
1035
|
-
apiToken,
|
|
1036
|
-
teamId,
|
|
1037
|
-
...profile.sprintFolderId ? { sprintFolderId: profile.sprintFolderId } : {}
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
function loadMultiProfileConfig() {
|
|
1041
|
-
migrateFromLegacy();
|
|
1042
|
-
const path = configPath();
|
|
1043
|
-
if (!fs.existsSync(path)) {
|
|
1044
|
-
return { defaultProfile: "", profiles: {} };
|
|
1045
|
-
}
|
|
1046
|
-
let parsed;
|
|
1047
|
-
try {
|
|
1048
|
-
const raw = fs.readFileSync(path, "utf-8");
|
|
1049
|
-
parsed = JSON.parse(raw);
|
|
1050
|
-
} catch {
|
|
1051
|
-
return { defaultProfile: "", profiles: {} };
|
|
1052
|
-
}
|
|
1053
|
-
if (!isRecord2(parsed)) return { defaultProfile: "", profiles: {} };
|
|
1054
|
-
if (isOldFormat(parsed)) {
|
|
1055
|
-
return migrateToMultiProfile(parsed, path);
|
|
1056
|
-
}
|
|
1057
|
-
return migrateToMultiProfile(parsed, path);
|
|
1058
|
-
}
|
|
1059
|
-
function saveMultiProfileConfig(config) {
|
|
1060
|
-
const dir = configDir();
|
|
1061
|
-
if (!fs.existsSync(dir)) {
|
|
1062
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1063
|
-
}
|
|
1064
|
-
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", {
|
|
1065
|
-
encoding: "utf-8",
|
|
1066
|
-
mode: 384
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
function addProfile(name, profile) {
|
|
1070
|
-
const multi = loadMultiProfileConfig();
|
|
1071
|
-
multi.profiles[name] = profile;
|
|
1072
|
-
if (!multi.defaultProfile) multi.defaultProfile = name;
|
|
1073
|
-
saveMultiProfileConfig(multi);
|
|
1074
|
-
}
|
|
1075
|
-
function removeProfile(name) {
|
|
1076
|
-
const multi = loadMultiProfileConfig();
|
|
1077
|
-
if (!multi.profiles[name]) {
|
|
1078
|
-
throw new Error(`Profile "${name}" not found.`);
|
|
1079
|
-
}
|
|
1080
|
-
const keys = Object.keys(multi.profiles);
|
|
1081
|
-
if (keys.length <= 1) {
|
|
1082
|
-
throw new Error("Cannot remove the last profile.");
|
|
1083
|
-
}
|
|
1084
|
-
delete multi.profiles[name];
|
|
1085
|
-
if (multi.defaultProfile === name) {
|
|
1086
|
-
multi.defaultProfile = Object.keys(multi.profiles)[0] ?? "";
|
|
1087
|
-
}
|
|
1088
|
-
saveMultiProfileConfig(multi);
|
|
1089
|
-
}
|
|
1090
|
-
function setDefaultProfile(name) {
|
|
1091
|
-
const multi = loadMultiProfileConfig();
|
|
1092
|
-
if (!multi.profiles[name]) {
|
|
1093
|
-
const available = Object.keys(multi.profiles).join(", ");
|
|
1094
|
-
throw new Error(`Profile "${name}" not found. Available: ${available}`);
|
|
1095
|
-
}
|
|
1096
|
-
multi.defaultProfile = name;
|
|
1097
|
-
saveMultiProfileConfig(multi);
|
|
1098
|
-
}
|
|
1099
|
-
function listProfiles() {
|
|
1100
|
-
const multi = loadMultiProfileConfig();
|
|
1101
|
-
return Object.entries(multi.profiles).map(([name, profile]) => ({
|
|
1102
|
-
name,
|
|
1103
|
-
isDefault: name === multi.defaultProfile,
|
|
1104
|
-
teamId: profile.teamId
|
|
1105
|
-
}));
|
|
1106
|
-
}
|
|
1107
|
-
function loadRawConfig(profileName) {
|
|
1108
|
-
migrateFromLegacy();
|
|
1109
|
-
const path = configPath();
|
|
1110
|
-
if (!fs.existsSync(path)) return {};
|
|
1111
|
-
let parsed;
|
|
1112
|
-
try {
|
|
1113
|
-
const raw = fs.readFileSync(path, "utf-8");
|
|
1114
|
-
parsed = JSON.parse(raw);
|
|
1115
|
-
} catch {
|
|
1116
|
-
return {};
|
|
1117
|
-
}
|
|
1118
|
-
if (!isRecord2(parsed)) {
|
|
1119
|
-
throw new Error(`Config file at ${path} must contain a JSON object.`);
|
|
1120
|
-
}
|
|
1121
|
-
if (isOldFormat(parsed)) {
|
|
1122
|
-
return parseConfigFile(JSON.stringify(parsed), path, false, true);
|
|
1123
|
-
}
|
|
1124
|
-
const multi = migrateToMultiProfile(parsed, path);
|
|
1125
|
-
const name = profileName || multi.defaultProfile || "default";
|
|
1126
|
-
return multi.profiles[name] ?? {};
|
|
1127
|
-
}
|
|
1128
|
-
function getConfigPath() {
|
|
1129
|
-
migrateFromLegacy();
|
|
1130
|
-
return configPath();
|
|
1131
|
-
}
|
|
1132
|
-
function getFilters(profileName) {
|
|
1133
|
-
const multi = loadMultiProfileConfig();
|
|
1134
|
-
const name = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1135
|
-
const profile = name ? multi.profiles[name] ?? {} : {};
|
|
1136
|
-
return profile.filters ?? {};
|
|
1137
|
-
}
|
|
1138
|
-
function saveFilter(name, entry, profileName) {
|
|
1139
|
-
const multi = loadMultiProfileConfig();
|
|
1140
|
-
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1141
|
-
const profile = multi.profiles[pName] ?? {};
|
|
1142
|
-
const filters = { ...profile.filters ?? {}, [name]: entry };
|
|
1143
|
-
multi.profiles[pName] = { ...profile, filters };
|
|
1144
|
-
if (!multi.defaultProfile) multi.defaultProfile = pName;
|
|
1145
|
-
saveMultiProfileConfig(multi);
|
|
1146
|
-
}
|
|
1147
|
-
function deleteFilter(name, profileName) {
|
|
1148
|
-
const multi = loadMultiProfileConfig();
|
|
1149
|
-
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1150
|
-
const profile = multi.profiles[pName] ?? {};
|
|
1151
|
-
const filters = { ...profile.filters ?? {} };
|
|
1152
|
-
if (!(name in filters)) {
|
|
1153
|
-
throw new Error(`Filter "${name}" not found.`);
|
|
1154
|
-
}
|
|
1155
|
-
delete filters[name];
|
|
1156
|
-
multi.profiles[pName] = { ...profile, filters };
|
|
1157
|
-
saveMultiProfileConfig(multi);
|
|
1158
|
-
}
|
|
1159
|
-
function getFavorites(profileName) {
|
|
1160
|
-
const multi = loadMultiProfileConfig();
|
|
1161
|
-
const name = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
|
|
1162
|
-
const profile = name ? multi.profiles[name] ?? {} : {};
|
|
1163
|
-
return profile.favorites ?? {};
|
|
1164
|
-
}
|
|
1165
|
-
function saveFavorite(alias, entry, profileName) {
|
|
1166
|
-
const multi = loadMultiProfileConfig();
|
|
1167
|
-
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1168
|
-
const profile = multi.profiles[pName] ?? {};
|
|
1169
|
-
const favorites = { ...profile.favorites ?? {}, [alias]: entry };
|
|
1170
|
-
multi.profiles[pName] = { ...profile, favorites };
|
|
1171
|
-
if (!multi.defaultProfile) multi.defaultProfile = pName;
|
|
1172
|
-
saveMultiProfileConfig(multi);
|
|
1173
|
-
}
|
|
1174
|
-
function deleteFavorite(alias, profileName) {
|
|
1175
|
-
const multi = loadMultiProfileConfig();
|
|
1176
|
-
const pName = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile ?? "default";
|
|
1177
|
-
const profile = multi.profiles[pName] ?? {};
|
|
1178
|
-
const favorites = { ...profile.favorites ?? {} };
|
|
1179
|
-
if (!(alias in favorites)) {
|
|
1180
|
-
throw new Error(`Favorite "${alias}" not found.`);
|
|
1181
|
-
}
|
|
1182
|
-
delete favorites[alias];
|
|
1183
|
-
multi.profiles[pName] = { ...profile, favorites };
|
|
1184
|
-
saveMultiProfileConfig(multi);
|
|
1185
|
-
}
|
|
1186
|
-
function writeConfig(config, profileName) {
|
|
1187
|
-
const multi = loadMultiProfileConfig();
|
|
1188
|
-
const name = profileName || multi.defaultProfile || "default";
|
|
1189
|
-
const apiToken = trimConfigValue(config.apiToken) ?? void 0;
|
|
1190
|
-
const teamId = trimConfigValue(config.teamId) ?? void 0;
|
|
1191
|
-
const sprintFolderId = trimConfigValue(config.sprintFolderId);
|
|
1192
|
-
const normalizedConfig = {
|
|
1193
|
-
...apiToken ? { apiToken } : {},
|
|
1194
|
-
...teamId ? { teamId } : {},
|
|
1195
|
-
...sprintFolderId ? { sprintFolderId } : {}
|
|
1196
|
-
};
|
|
1197
|
-
multi.profiles[name] = {
|
|
1198
|
-
...multi.profiles[name],
|
|
1199
|
-
...normalizedConfig
|
|
1200
|
-
};
|
|
1201
|
-
if (!multi.defaultProfile) multi.defaultProfile = name;
|
|
1202
|
-
saveMultiProfileConfig(multi);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/output.ts
|
|
1206
|
-
import chalk from "chalk";
|
|
1207
|
-
function isTTY() {
|
|
1208
|
-
return Boolean(process.stdout.isTTY);
|
|
1209
|
-
}
|
|
1210
|
-
function shouldOutputJson(forceJson) {
|
|
1211
|
-
if (forceJson) return true;
|
|
1212
|
-
if (process.env["CU_OUTPUT"] === "json") return true;
|
|
1213
|
-
return false;
|
|
1214
|
-
}
|
|
1215
|
-
function cell(value, width) {
|
|
1216
|
-
if (value.length > width) return value.slice(0, width - 1) + "\u2026";
|
|
1217
|
-
return value.padEnd(width);
|
|
1218
|
-
}
|
|
1219
|
-
function computeWidths(rows, columns) {
|
|
1220
|
-
return columns.map((col) => {
|
|
1221
|
-
const headerLen = col.label.length;
|
|
1222
|
-
const maxDataLen = rows.reduce((max, row) => {
|
|
1223
|
-
const val = String(row[col.key] ?? "");
|
|
1224
|
-
return Math.max(max, val.length);
|
|
1225
|
-
}, 0);
|
|
1226
|
-
const natural = Math.max(headerLen, maxDataLen);
|
|
1227
|
-
return col.maxWidth ? Math.min(natural, col.maxWidth) : natural;
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
function formatTable(rows, columns) {
|
|
1231
|
-
const widths = computeWidths(rows, columns);
|
|
1232
|
-
const header = columns.map((c, i) => cell(c.label, widths[i])).join(" ");
|
|
1233
|
-
const divider = chalk.dim("-".repeat(header.replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
1234
|
-
const lines = [chalk.bold(header), divider];
|
|
1235
|
-
for (const row of rows) {
|
|
1236
|
-
lines.push(
|
|
1237
|
-
columns.map((c, i) => {
|
|
1238
|
-
const raw = String(row[c.key] ?? "");
|
|
1239
|
-
const width = widths[i];
|
|
1240
|
-
const truncated = raw.length > width ? raw.slice(0, width - 1) + "\u2026" : raw;
|
|
1241
|
-
const padding = " ".repeat(Math.max(0, width - truncated.length));
|
|
1242
|
-
return c.format ? c.format(truncated, row) + padding : truncated + padding;
|
|
1243
|
-
}).join(" ")
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
return lines.join("\n");
|
|
1247
|
-
}
|
|
1248
|
-
function colorStatus(status) {
|
|
1249
|
-
const lower = status.toLowerCase();
|
|
1250
|
-
if (lower.includes("done") || lower.includes("complete") || lower.includes("closed"))
|
|
1251
|
-
return chalk.green(status);
|
|
1252
|
-
if (lower.includes("progress") || lower.includes("review") || lower.includes("active"))
|
|
1253
|
-
return chalk.yellow(status);
|
|
1254
|
-
if (lower.includes("block") || lower.includes("stuck")) return chalk.red(status);
|
|
1255
|
-
return chalk.dim(status);
|
|
1256
|
-
}
|
|
1257
|
-
function colorPriority(priority) {
|
|
1258
|
-
const lower = priority.toLowerCase();
|
|
1259
|
-
if (lower === "urgent") return chalk.red(priority);
|
|
1260
|
-
if (lower === "high") return chalk.yellow(priority);
|
|
1261
|
-
if (lower === "normal") return priority;
|
|
1262
|
-
if (lower === "low") return chalk.dim(priority);
|
|
1263
|
-
return priority;
|
|
1264
|
-
}
|
|
1265
|
-
function colorDueDate(dateStr, rawTimestamp) {
|
|
1266
|
-
if (!dateStr) return dateStr;
|
|
1267
|
-
if (rawTimestamp) {
|
|
1268
|
-
const ts = Number(rawTimestamp);
|
|
1269
|
-
if (Number.isFinite(ts) && ts < Date.now()) return chalk.red(dateStr);
|
|
1270
|
-
}
|
|
1271
|
-
return dateStr;
|
|
1272
|
-
}
|
|
1273
|
-
var TASK_COLUMNS = [
|
|
1274
|
-
{ key: "id", label: "ID" },
|
|
1275
|
-
{ key: "name", label: "NAME", maxWidth: 60 },
|
|
1276
|
-
{ key: "status", label: "STATUS", maxWidth: 20, format: (v) => colorStatus(v) },
|
|
1277
|
-
{ key: "priority", label: "PRIORITY", maxWidth: 10, format: (v) => colorPriority(v) },
|
|
1278
|
-
{
|
|
1279
|
-
key: "due_date",
|
|
1280
|
-
label: "DUE",
|
|
1281
|
-
maxWidth: 15,
|
|
1282
|
-
format: (v, row) => v ? colorDueDate(v, row.dueRaw) : ""
|
|
1283
|
-
},
|
|
1284
|
-
{ key: "list", label: "LIST" }
|
|
1285
|
-
];
|
|
1286
|
-
|
|
1287
|
-
// src/date.ts
|
|
1288
|
-
function formatDate(ms) {
|
|
1289
|
-
return new Date(Number(ms)).toLocaleDateString("en-US", {
|
|
1290
|
-
month: "short",
|
|
1291
|
-
day: "numeric",
|
|
1292
|
-
year: "numeric"
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
function formatTimestamp(ms) {
|
|
1296
|
-
return new Date(Number(ms)).toLocaleString("en-US", {
|
|
1297
|
-
month: "short",
|
|
1298
|
-
day: "numeric",
|
|
1299
|
-
hour: "numeric",
|
|
1300
|
-
minute: "2-digit"
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
function formatDuration(ms) {
|
|
1304
|
-
const totalMinutes = Math.round(Math.abs(ms) / 6e4);
|
|
1305
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
1306
|
-
const minutes = totalMinutes % 60;
|
|
1307
|
-
if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
|
|
1308
|
-
if (hours > 0) return `${hours}h`;
|
|
1309
|
-
return `${minutes}m`;
|
|
1310
|
-
}
|
|
1311
|
-
function formatLongDuration(ms) {
|
|
1312
|
-
const totalMinutes = Math.round(Math.abs(ms) / 6e4);
|
|
1313
|
-
if (totalMinutes === 0) return "< 1m";
|
|
1314
|
-
const days = Math.floor(totalMinutes / 1440);
|
|
1315
|
-
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
1316
|
-
const minutes = totalMinutes % 60;
|
|
1317
|
-
const parts = [];
|
|
1318
|
-
if (days > 0) parts.push(`${days}d`);
|
|
1319
|
-
if (hours > 0) parts.push(`${hours}h`);
|
|
1320
|
-
if (minutes > 0) parts.push(`${minutes}m`);
|
|
1321
|
-
return parts.join(" ");
|
|
1322
|
-
}
|
|
1323
|
-
function formatDateISO(ms) {
|
|
1324
|
-
const d = new Date(Number(ms));
|
|
1325
|
-
const year = d.getUTCFullYear();
|
|
1326
|
-
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
1327
|
-
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
1328
|
-
return `${year}-${month}-${day}`;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// src/markdown.ts
|
|
1332
|
-
function escapeCell(value) {
|
|
1333
|
-
return value.replace(/\|/g, "\\|");
|
|
1334
|
-
}
|
|
1335
|
-
function formatMarkdownTable(rows, columns) {
|
|
1336
|
-
const header = "| " + columns.map((c) => c.label).join(" | ") + " |";
|
|
1337
|
-
const divider = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1338
|
-
const lines = [header, divider];
|
|
1339
|
-
for (const row of rows) {
|
|
1340
|
-
const cells = columns.map((c) => escapeCell(String(row[c.key] ?? "")));
|
|
1341
|
-
lines.push("| " + cells.join(" | ") + " |");
|
|
1342
|
-
}
|
|
1343
|
-
return lines.join("\n");
|
|
1344
|
-
}
|
|
1345
|
-
var TASK_MD_COLUMNS = [
|
|
1346
|
-
{ key: "id", label: "ID" },
|
|
1347
|
-
{ key: "name", label: "Name" },
|
|
1348
|
-
{ key: "status", label: "Status" },
|
|
1349
|
-
{ key: "priority", label: "Priority" },
|
|
1350
|
-
{ key: "due_date", label: "Due" },
|
|
1351
|
-
{ key: "list", label: "List" }
|
|
1352
|
-
];
|
|
1353
|
-
function formatTasksMarkdown(tasks) {
|
|
1354
|
-
if (tasks.length === 0) return "No tasks found.";
|
|
1355
|
-
return formatMarkdownTable(tasks, TASK_MD_COLUMNS);
|
|
1356
|
-
}
|
|
1357
|
-
function formatCommentsMarkdown(comments) {
|
|
1358
|
-
if (comments.length === 0) return "No comments found.";
|
|
1359
|
-
return comments.map((c) => `**${c.user}** (${formatDateISO(c.date)})
|
|
1360
|
-
|
|
1361
|
-
${c.text}`).join("\n\n---\n\n");
|
|
1362
|
-
}
|
|
1363
|
-
var LIST_MD_COLUMNS = [
|
|
1364
|
-
{ key: "id", label: "ID" },
|
|
1365
|
-
{ key: "name", label: "Name" },
|
|
1366
|
-
{ key: "folder", label: "Folder" }
|
|
1367
|
-
];
|
|
1368
|
-
function formatListsMarkdown(lists) {
|
|
1369
|
-
if (lists.length === 0) return "No lists found.";
|
|
1370
|
-
return formatMarkdownTable(lists, LIST_MD_COLUMNS);
|
|
1371
|
-
}
|
|
1372
|
-
var SPACE_MD_COLUMNS = [
|
|
1373
|
-
{ key: "id", label: "ID" },
|
|
1374
|
-
{ key: "name", label: "Name" }
|
|
1375
|
-
];
|
|
1376
|
-
function formatSpacesMarkdown(spaces) {
|
|
1377
|
-
if (spaces.length === 0) return "No spaces found.";
|
|
1378
|
-
return formatMarkdownTable(spaces, SPACE_MD_COLUMNS);
|
|
1379
|
-
}
|
|
1380
|
-
function formatGroupedTasksMarkdown(groups) {
|
|
1381
|
-
const sections = groups.filter((g) => g.tasks.length > 0).map((g) => `## ${g.label}
|
|
1382
|
-
|
|
1383
|
-
${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
|
|
1384
|
-
if (sections.length === 0) return "No tasks found.";
|
|
1385
|
-
return sections.join("\n\n");
|
|
1386
|
-
}
|
|
1387
|
-
function formatTaskDetailMarkdown(task) {
|
|
1388
|
-
const lines = [`# ${task.name}`, ""];
|
|
1389
|
-
const isInitiative = (task.custom_item_id ?? 0) !== 0;
|
|
1390
|
-
const fields = [
|
|
1391
|
-
["ID", task.id],
|
|
1392
|
-
["Status", task.status.status],
|
|
1393
|
-
["Type", isInitiative ? "initiative" : "task"],
|
|
1394
|
-
["List", task.list.name],
|
|
1395
|
-
["URL", task.url],
|
|
1396
|
-
[
|
|
1397
|
-
"Assignees",
|
|
1398
|
-
task.assignees.length > 0 ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
1399
|
-
],
|
|
1400
|
-
["Priority", task.priority?.priority],
|
|
1401
|
-
["Parent", task.parent ?? void 0],
|
|
1402
|
-
["Start Date", task.start_date ? formatDateISO(task.start_date) : void 0],
|
|
1403
|
-
["Due Date", task.due_date ? formatDateISO(task.due_date) : void 0],
|
|
1404
|
-
[
|
|
1405
|
-
"Time Estimate",
|
|
1406
|
-
task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
|
|
1407
|
-
],
|
|
1408
|
-
[
|
|
1409
|
-
"Time Spent",
|
|
1410
|
-
task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
|
|
1411
|
-
],
|
|
1412
|
-
["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
1413
|
-
[
|
|
1414
|
-
"Lists",
|
|
1415
|
-
task.locations && task.locations.length > 0 ? task.locations.map((l) => l.name).join(", ") : void 0
|
|
1416
|
-
],
|
|
1417
|
-
["Created", task.date_created ? formatDateISO(task.date_created) : void 0],
|
|
1418
|
-
["Updated", task.date_updated ? formatDateISO(task.date_updated) : void 0]
|
|
1419
|
-
];
|
|
1420
|
-
for (const [label, value] of fields) {
|
|
1421
|
-
if (value != null && value !== "") {
|
|
1422
|
-
lines.push(`**${label}:** ${value}`);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
const descriptionContent = task.markdown_content ?? task.description;
|
|
1426
|
-
if (descriptionContent) {
|
|
1427
|
-
lines.push("", "## Description", "", descriptionContent);
|
|
1428
|
-
}
|
|
1429
|
-
if (task.checklists?.length) {
|
|
1430
|
-
lines.push("", "## Checklists", "");
|
|
1431
|
-
for (const cl of task.checklists) {
|
|
1432
|
-
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
1433
|
-
lines.push(`### ${cl.name} (${resolved}/${cl.items.length})`, "");
|
|
1434
|
-
for (const item of cl.items) {
|
|
1435
|
-
lines.push(`- [${item.resolved ? "x" : " "}] ${item.name}`);
|
|
1436
|
-
}
|
|
1437
|
-
lines.push("");
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
if (task.attachments?.length) {
|
|
1441
|
-
lines.push("", "## Attachments", "");
|
|
1442
|
-
for (const att of task.attachments) {
|
|
1443
|
-
lines.push(`- [${att.title}](${att.url})`);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
if (task.dependencies?.length) {
|
|
1447
|
-
lines.push("", "## Dependencies", "");
|
|
1448
|
-
for (const dep of task.dependencies) {
|
|
1449
|
-
const direction = dep.depends_on === task.id ? "blocks" : "depends on";
|
|
1450
|
-
const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
|
|
1451
|
-
lines.push(`- ${direction} ${otherId}`);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
if (task.linked_tasks?.length) {
|
|
1455
|
-
lines.push("", "## Linked Tasks", "");
|
|
1456
|
-
for (const lt of task.linked_tasks) {
|
|
1457
|
-
lines.push(`- ${lt.task_id}`);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
return lines.join("\n");
|
|
1461
|
-
}
|
|
1462
|
-
function formatUpdateConfirmation(id, name) {
|
|
1463
|
-
return `Updated task ${id}: "${name}"`;
|
|
1464
|
-
}
|
|
1465
|
-
function formatCreateConfirmation(id, name, url) {
|
|
1466
|
-
return `Created task ${id}: "${name}" - ${url}`;
|
|
1467
|
-
}
|
|
1468
|
-
function formatCommentConfirmation(id) {
|
|
1469
|
-
return `Comment posted (id: ${id})`;
|
|
1470
|
-
}
|
|
1471
|
-
function formatAssignConfirmation(taskId, opts) {
|
|
1472
|
-
const parts = [];
|
|
1473
|
-
if (opts.to) parts.push(`Assigned ${opts.to} to ${taskId}`);
|
|
1474
|
-
if (opts.remove) parts.push(`Removed ${opts.remove} from ${taskId}`);
|
|
1475
|
-
return parts.join("; ");
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// src/interactive.ts
|
|
1479
|
-
import { execFileSync } from "child_process";
|
|
1480
|
-
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
1481
|
-
import chalk2 from "chalk";
|
|
1482
|
-
function openUrl(url) {
|
|
1483
|
-
switch (process.platform) {
|
|
1484
|
-
case "darwin":
|
|
1485
|
-
execFileSync("open", [url]);
|
|
1486
|
-
break;
|
|
1487
|
-
case "linux":
|
|
1488
|
-
execFileSync("xdg-open", [url]);
|
|
1489
|
-
break;
|
|
1490
|
-
case "win32":
|
|
1491
|
-
execFileSync("cmd", ["/c", "start", "", url]);
|
|
1492
|
-
break;
|
|
1493
|
-
default:
|
|
1494
|
-
process.stderr.write(`Cannot open browser on ${process.platform}. Visit: ${url}
|
|
1495
|
-
`);
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
function descriptionPreview(text, maxLines = 3) {
|
|
1499
|
-
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
1500
|
-
const preview = lines.slice(0, maxLines);
|
|
1501
|
-
const result = preview.map((l) => ` ${chalk2.dim(l.length > 100 ? l.slice(0, 99) + "\u2026" : l)}`).join("\n");
|
|
1502
|
-
if (lines.length > maxLines)
|
|
1503
|
-
return result + `
|
|
1504
|
-
${chalk2.dim(`... (${lines.length - maxLines} more lines)`)}`;
|
|
1505
|
-
return result;
|
|
1506
|
-
}
|
|
1507
|
-
function stringifyFieldValue(value) {
|
|
1508
|
-
if (typeof value === "string") return value;
|
|
1509
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1510
|
-
return JSON.stringify(value);
|
|
1511
|
-
}
|
|
1512
|
-
function formatCustomFieldValue(field) {
|
|
1513
|
-
if (field.value === null || field.value === void 0) return null;
|
|
1514
|
-
const options = field.type_config?.options;
|
|
1515
|
-
switch (field.type) {
|
|
1516
|
-
case "drop_down": {
|
|
1517
|
-
if (!options) return stringifyFieldValue(field.value);
|
|
1518
|
-
const match = options.find((o) => o.id === Number(field.value));
|
|
1519
|
-
return match?.name ?? stringifyFieldValue(field.value);
|
|
1520
|
-
}
|
|
1521
|
-
case "labels": {
|
|
1522
|
-
if (!Array.isArray(field.value) || !options) return stringifyFieldValue(field.value);
|
|
1523
|
-
const names = field.value.map((id) => options.find((o) => o.id === id)?.name).filter((n) => n !== void 0);
|
|
1524
|
-
return names.length > 0 ? names.join(", ") : null;
|
|
1525
|
-
}
|
|
1526
|
-
case "date": {
|
|
1527
|
-
const ts = Number(field.value);
|
|
1528
|
-
if (!Number.isFinite(ts)) return stringifyFieldValue(field.value);
|
|
1529
|
-
return formatDate(String(ts));
|
|
1530
|
-
}
|
|
1531
|
-
case "checkbox":
|
|
1532
|
-
return field.value === true || field.value === "true" ? "Yes" : "No";
|
|
1533
|
-
default:
|
|
1534
|
-
return stringifyFieldValue(field.value);
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
function formatTaskDetail(task) {
|
|
1538
|
-
const lines = [];
|
|
1539
|
-
const isInitiative = (task.custom_item_id ?? 0) !== 0;
|
|
1540
|
-
const typeLabel = isInitiative ? "initiative" : "task";
|
|
1541
|
-
lines.push(chalk2.bold.underline(task.name));
|
|
1542
|
-
lines.push("");
|
|
1543
|
-
const fields = [
|
|
1544
|
-
["ID", task.id],
|
|
1545
|
-
["Status", task.status?.status ? colorStatus(task.status.status) : void 0],
|
|
1546
|
-
["Type", typeLabel],
|
|
1547
|
-
["List", task.list?.name],
|
|
1548
|
-
[
|
|
1549
|
-
"Assignees",
|
|
1550
|
-
task.assignees?.length ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
1551
|
-
],
|
|
1552
|
-
["Priority", task.priority?.priority ? colorPriority(task.priority.priority) : void 0],
|
|
1553
|
-
["Start", task.start_date ? formatDate(task.start_date) : void 0],
|
|
1554
|
-
["Due", task.due_date ? colorDueDate(formatDate(task.due_date), task.due_date) : void 0],
|
|
1555
|
-
["Estimate", task.time_estimate ? formatDuration(task.time_estimate) : void 0],
|
|
1556
|
-
["Tracked", task.time_spent ? formatDuration(task.time_spent) : void 0],
|
|
1557
|
-
["Tags", task.tags?.length ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
1558
|
-
["Lists", task.locations?.length ? task.locations.map((l) => l.name).join(", ") : void 0],
|
|
1559
|
-
["Parent", task.parent || void 0],
|
|
1560
|
-
["URL", task.url]
|
|
1561
|
-
];
|
|
1562
|
-
const maxLabel = Math.max(...fields.filter(([, v]) => v).map(([k]) => k.length));
|
|
1563
|
-
for (const [label, value] of fields) {
|
|
1564
|
-
if (!value) continue;
|
|
1565
|
-
lines.push(` ${chalk2.bold(label.padEnd(maxLabel + 1))} ${value}`);
|
|
1566
|
-
}
|
|
1567
|
-
if (task.custom_fields?.length) {
|
|
1568
|
-
const formatted = task.custom_fields.map((f) => [f.name, formatCustomFieldValue(f)]).filter((pair) => pair[1] !== null);
|
|
1569
|
-
if (formatted.length > 0) {
|
|
1570
|
-
lines.push("");
|
|
1571
|
-
lines.push(chalk2.bold("Custom Fields"));
|
|
1572
|
-
for (const [name, value] of formatted) {
|
|
1573
|
-
lines.push(` ${chalk2.bold(name)} ${value}`);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
if (task.checklists?.length) {
|
|
1578
|
-
lines.push("");
|
|
1579
|
-
lines.push(chalk2.bold("Checklists"));
|
|
1580
|
-
for (const cl of task.checklists) {
|
|
1581
|
-
const resolved = cl.items.filter((i) => i.resolved).length;
|
|
1582
|
-
lines.push(` ${chalk2.bold(cl.name)} (${resolved}/${cl.items.length})`);
|
|
1583
|
-
for (const item of cl.items) {
|
|
1584
|
-
const check = item.resolved ? chalk2.green("[x]") : chalk2.dim("[ ]");
|
|
1585
|
-
lines.push(` ${check} ${item.name}`);
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
if (task.attachments?.length) {
|
|
1590
|
-
lines.push("");
|
|
1591
|
-
lines.push(chalk2.bold("Attachments"));
|
|
1592
|
-
for (const att of task.attachments) {
|
|
1593
|
-
lines.push(` ${att.title} ${chalk2.dim(att.url)}`);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
if (task.dependencies?.length) {
|
|
1597
|
-
lines.push("");
|
|
1598
|
-
lines.push(chalk2.bold("Dependencies"));
|
|
1599
|
-
for (const dep of task.dependencies) {
|
|
1600
|
-
const direction = dep.depends_on === task.id ? "blocks" : "depends on";
|
|
1601
|
-
const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
|
|
1602
|
-
lines.push(` ${direction} ${chalk2.dim(otherId)}`);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
if (task.linked_tasks?.length) {
|
|
1606
|
-
lines.push("");
|
|
1607
|
-
lines.push(chalk2.bold("Linked Tasks"));
|
|
1608
|
-
for (const lt of task.linked_tasks) {
|
|
1609
|
-
lines.push(` ${chalk2.dim(lt.task_id)}`);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
if (task.text_content?.trim()) {
|
|
1613
|
-
lines.push("");
|
|
1614
|
-
lines.push(descriptionPreview(task.text_content));
|
|
1615
|
-
}
|
|
1616
|
-
return lines.join("\n");
|
|
1617
|
-
}
|
|
1618
|
-
function formatChoiceName(task) {
|
|
1619
|
-
const id = task.id.padEnd(12);
|
|
1620
|
-
const name = task.name.length > 50 ? task.name.slice(0, 49) + "\u2026" : task.name.padEnd(50);
|
|
1621
|
-
const status = colorStatus(task.status);
|
|
1622
|
-
const priority = task.priority !== "none" ? colorPriority(task.priority) : "";
|
|
1623
|
-
return `${id} ${name} ${status}${priority ? " " + priority : ""}`;
|
|
1624
|
-
}
|
|
1625
|
-
async function interactiveTaskPicker(tasks) {
|
|
1626
|
-
if (tasks.length === 0) return [];
|
|
1627
|
-
const selected = await checkbox({
|
|
1628
|
-
message: `${tasks.length} task(s) found. Select to view details / open in browser:`,
|
|
1629
|
-
choices: tasks.map((t) => ({
|
|
1630
|
-
name: formatChoiceName(t),
|
|
1631
|
-
value: t.id
|
|
1632
|
-
})),
|
|
1633
|
-
pageSize: 20
|
|
1634
|
-
});
|
|
1635
|
-
return tasks.filter((t) => selected.includes(t.id));
|
|
1636
|
-
}
|
|
1637
|
-
async function groupedTaskPicker(groups) {
|
|
1638
|
-
const allTasks = groups.flatMap((g) => g.tasks);
|
|
1639
|
-
const totalCount = allTasks.length;
|
|
1640
|
-
if (totalCount === 0) return [];
|
|
1641
|
-
const choices = [];
|
|
1642
|
-
for (const group of groups) {
|
|
1643
|
-
if (group.tasks.length === 0) continue;
|
|
1644
|
-
choices.push(new Separator(chalk2.bold(`${group.label} (${group.tasks.length})`)));
|
|
1645
|
-
for (const task of group.tasks) {
|
|
1646
|
-
choices.push({ name: formatChoiceName(task), value: task.id });
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
const selected = await checkbox({
|
|
1650
|
-
message: `${totalCount} task(s) found. Select to view details / open in browser:`,
|
|
1651
|
-
choices,
|
|
1652
|
-
pageSize: 20
|
|
1653
|
-
});
|
|
1654
|
-
return allTasks.filter((t) => selected.includes(t.id));
|
|
1655
|
-
}
|
|
1656
|
-
async function showDetailsAndOpen(tasks, fetchTask) {
|
|
1657
|
-
if (tasks.length === 0) return;
|
|
1658
|
-
const separator = chalk2.dim("\u2500".repeat(60));
|
|
1659
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
1660
|
-
const task = tasks[i];
|
|
1661
|
-
if (i > 0) {
|
|
1662
|
-
console.log("");
|
|
1663
|
-
console.log(separator);
|
|
1664
|
-
}
|
|
1665
|
-
console.log("");
|
|
1666
|
-
if (fetchTask) {
|
|
1667
|
-
const full = await fetchTask(task.id);
|
|
1668
|
-
console.log(formatTaskDetail(full));
|
|
1669
|
-
} else {
|
|
1670
|
-
const fallback = {
|
|
1671
|
-
id: task.id,
|
|
1672
|
-
name: task.name,
|
|
1673
|
-
status: { status: task.status, color: "" },
|
|
1674
|
-
custom_item_id: task.task_type === "initiative" ? 1 : 0,
|
|
1675
|
-
assignees: [],
|
|
1676
|
-
url: task.url,
|
|
1677
|
-
list: { id: "", name: task.list },
|
|
1678
|
-
parent: task.parent
|
|
1679
|
-
};
|
|
1680
|
-
console.log(formatTaskDetail(fallback));
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
const urls = tasks.map((t) => t.url);
|
|
1684
|
-
console.log("");
|
|
1685
|
-
const shouldOpen = await confirm({
|
|
1686
|
-
message: `Open ${urls.length} task(s) in browser?`,
|
|
1687
|
-
default: true
|
|
1688
|
-
});
|
|
1689
|
-
if (shouldOpen) {
|
|
1690
|
-
for (const url of urls) {
|
|
1691
|
-
openUrl(url);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// src/commands/tasks.ts
|
|
1697
|
-
var DONE_PATTERNS = ["done", "complete", "closed"];
|
|
1698
|
-
function isDoneStatus(status) {
|
|
1699
|
-
const lower = status.toLowerCase();
|
|
1700
|
-
return DONE_PATTERNS.some((p) => lower.includes(p));
|
|
1701
|
-
}
|
|
1702
|
-
function formatDueDate(ms) {
|
|
1703
|
-
if (!ms) return "";
|
|
1704
|
-
return formatDate(ms);
|
|
1705
|
-
}
|
|
1706
|
-
function resolveTaskType(task, typeMap) {
|
|
1707
|
-
const id = task.custom_item_id ?? 0;
|
|
1708
|
-
if (id === 0) return "task";
|
|
1709
|
-
return typeMap.get(id) ?? `type_${id}`;
|
|
1710
|
-
}
|
|
1711
|
-
function summarize(task, typeMap) {
|
|
1712
|
-
return {
|
|
1713
|
-
id: task.id,
|
|
1714
|
-
name: task.name,
|
|
1715
|
-
status: task.status.status,
|
|
1716
|
-
task_type: resolveTaskType(task, typeMap ?? /* @__PURE__ */ new Map()),
|
|
1717
|
-
priority: task.priority?.priority ?? "none",
|
|
1718
|
-
due_date: formatDueDate(task.due_date),
|
|
1719
|
-
...task.due_date ? { dueRaw: task.due_date } : {},
|
|
1720
|
-
list: task.list.name,
|
|
1721
|
-
url: task.url,
|
|
1722
|
-
...task.parent ? { parent: task.parent } : {}
|
|
1723
|
-
};
|
|
1724
|
-
}
|
|
1725
|
-
function buildTypeMap(types) {
|
|
1726
|
-
const map = /* @__PURE__ */ new Map();
|
|
1727
|
-
for (const t of types) {
|
|
1728
|
-
map.set(t.id, t.name);
|
|
1729
|
-
}
|
|
1730
|
-
return map;
|
|
1731
|
-
}
|
|
1732
|
-
function resolveTypeFilter(typeFilter, typeMap) {
|
|
1733
|
-
if (typeFilter === "task") return 0;
|
|
1734
|
-
const asNum = Number(typeFilter);
|
|
1735
|
-
if (Number.isFinite(asNum)) return asNum;
|
|
1736
|
-
const lower = typeFilter.toLowerCase();
|
|
1737
|
-
for (const [id, name] of typeMap) {
|
|
1738
|
-
if (name.toLowerCase() === lower) return id;
|
|
1739
|
-
}
|
|
1740
|
-
const available = ["task", ...Array.from(typeMap.values())].join(", ");
|
|
1741
|
-
throw new Error(`Unknown task type "${typeFilter}". Available types: ${available}`);
|
|
1742
|
-
}
|
|
1743
|
-
async function fetchMyTasks(config, opts = {}) {
|
|
1744
|
-
const client = new ClickUpClient(config);
|
|
1745
|
-
const { typeFilter, name, ...apiFilters } = opts;
|
|
1746
|
-
const [allTasks, customTypes] = await Promise.all([
|
|
1747
|
-
client.getMyTasks(config.teamId, apiFilters),
|
|
1748
|
-
client.getCustomTaskTypes(config.teamId)
|
|
1749
|
-
]);
|
|
1750
|
-
const typeMap = buildTypeMap(customTypes);
|
|
1751
|
-
let filtered = allTasks;
|
|
1752
|
-
if (typeFilter) {
|
|
1753
|
-
const targetId = resolveTypeFilter(typeFilter, typeMap);
|
|
1754
|
-
filtered = allTasks.filter((t) => (t.custom_item_id ?? 0) === targetId);
|
|
1755
|
-
}
|
|
1756
|
-
if (name) {
|
|
1757
|
-
const query = name.toLowerCase();
|
|
1758
|
-
filtered = filtered.filter((t) => t.name.toLowerCase().includes(query));
|
|
1759
|
-
}
|
|
1760
|
-
return filtered.map((t) => summarize(t, typeMap));
|
|
1761
|
-
}
|
|
1762
|
-
async function printTasks(tasks, forceJson, config) {
|
|
1763
|
-
if (shouldOutputJson(forceJson)) {
|
|
1764
|
-
console.log(JSON.stringify(tasks, null, 2));
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
if (!isTTY()) {
|
|
1768
|
-
console.log(formatTasksMarkdown(tasks));
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1771
|
-
if (tasks.length === 0) {
|
|
1772
|
-
console.log("No tasks found.");
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
const fetchTask = config ? (() => {
|
|
1776
|
-
const client = new ClickUpClient(config);
|
|
1777
|
-
return (id) => client.getTask(id);
|
|
1778
|
-
})() : void 0;
|
|
1779
|
-
const selected = await interactiveTaskPicker(tasks);
|
|
1780
|
-
await showDetailsAndOpen(selected, fetchTask);
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
// src/commands/sprint.ts
|
|
1784
|
-
var SPRINT_KEYWORDS = ["sprint", "iteration", "cycle", "scrum"];
|
|
1785
|
-
function parseUSDateRange(name) {
|
|
1786
|
-
const m = name.match(/\((\d{1,2})\/(\d{1,2})\s*[-–]\s*(\d{1,2})\/(\d{1,2})\)/);
|
|
1787
|
-
if (!m) return null;
|
|
1788
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1789
|
-
const start = new Date(year, Number(m[1]) - 1, Number(m[2]));
|
|
1790
|
-
const end = new Date(year, Number(m[3]) - 1, Number(m[4]), 23, 59, 59);
|
|
1791
|
-
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
1792
|
-
return { start, end };
|
|
1793
|
-
}
|
|
1794
|
-
function parseISODateRange(name) {
|
|
1795
|
-
const m = name.match(/\((\d{4}-\d{2}-\d{2})\s*[-–]\s*(\d{4}-\d{2}-\d{2})\)/);
|
|
1796
|
-
if (!m) return null;
|
|
1797
|
-
const [sy, sm, sd] = m[1].split("-").map(Number);
|
|
1798
|
-
const [ey, em, ed] = m[2].split("-").map(Number);
|
|
1799
|
-
const start = new Date(sy, sm - 1, sd);
|
|
1800
|
-
const end = new Date(ey, em - 1, ed, 23, 59, 59);
|
|
1801
|
-
return { start, end };
|
|
1802
|
-
}
|
|
1803
|
-
function parseMonthDayRange(name) {
|
|
1804
|
-
const months = {
|
|
1805
|
-
jan: 0,
|
|
1806
|
-
feb: 1,
|
|
1807
|
-
mar: 2,
|
|
1808
|
-
apr: 3,
|
|
1809
|
-
may: 4,
|
|
1810
|
-
jun: 5,
|
|
1811
|
-
jul: 6,
|
|
1812
|
-
aug: 7,
|
|
1813
|
-
sep: 8,
|
|
1814
|
-
oct: 9,
|
|
1815
|
-
nov: 10,
|
|
1816
|
-
dec: 11
|
|
1817
|
-
};
|
|
1818
|
-
const m = name.match(/\(([A-Za-z]{3})\s+(\d{1,2})\s*[-–]\s*([A-Za-z]{3})\s+(\d{1,2})\)/);
|
|
1819
|
-
if (!m) return null;
|
|
1820
|
-
const sm = months[m[1].toLowerCase()];
|
|
1821
|
-
const em = months[m[3].toLowerCase()];
|
|
1822
|
-
if (sm === void 0 || em === void 0) return null;
|
|
1823
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1824
|
-
const start = new Date(year, sm, Number(m[2]));
|
|
1825
|
-
const end = new Date(year, em, Number(m[4]), 23, 59, 59);
|
|
1826
|
-
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
1827
|
-
return { start, end };
|
|
1828
|
-
}
|
|
1829
|
-
function parseEuropeanDateRange(name) {
|
|
1830
|
-
const m = name.match(/\((\d{1,2})\.(\d{1,2})\s*[-–]\s*(\d{1,2})\.(\d{1,2})\)/);
|
|
1831
|
-
if (!m) return null;
|
|
1832
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1833
|
-
const start = new Date(year, Number(m[2]) - 1, Number(m[1]));
|
|
1834
|
-
const end = new Date(year, Number(m[4]) - 1, Number(m[3]), 23, 59, 59);
|
|
1835
|
-
if (end < start) end.setFullYear(end.getFullYear() + 1);
|
|
1836
|
-
return { start, end };
|
|
1837
|
-
}
|
|
1838
|
-
function parseSprintDates(name) {
|
|
1839
|
-
return parseUSDateRange(name) ?? parseISODateRange(name) ?? parseMonthDayRange(name) ?? parseEuropeanDateRange(name);
|
|
1840
|
-
}
|
|
1841
|
-
function findActiveSprintList(lists, today = /* @__PURE__ */ new Date()) {
|
|
1842
|
-
if (lists.length === 0) return null;
|
|
1843
|
-
for (const list of lists) {
|
|
1844
|
-
const dates = parseSprintDates(list.name);
|
|
1845
|
-
if (dates && today >= dates.start && today <= dates.end) return list;
|
|
1846
|
-
}
|
|
1847
|
-
for (const list of lists) {
|
|
1848
|
-
if (list.start_date && list.due_date) {
|
|
1849
|
-
const start = new Date(Number(list.start_date));
|
|
1850
|
-
const end = new Date(Number(list.due_date));
|
|
1851
|
-
if (today >= start && today <= end) return list;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
return lists[lists.length - 1] ?? null;
|
|
1855
|
-
}
|
|
1856
|
-
var NOISE_WORDS = /* @__PURE__ */ new Set(["product", "team", "the", "and", "for", "test"]);
|
|
1857
|
-
function extractSpaceKeywords(spaceName) {
|
|
1858
|
-
return spaceName.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((w) => w.toLowerCase()).filter((w) => w.length >= 3 && !NOISE_WORDS.has(w));
|
|
1859
|
-
}
|
|
1860
|
-
function findRelatedSpaces(mySpaceIds, allSpaces) {
|
|
1861
|
-
const mySpaces = allSpaces.filter((s) => mySpaceIds.has(s.id));
|
|
1862
|
-
const keywords = mySpaces.flatMap((s) => extractSpaceKeywords(s.name));
|
|
1863
|
-
if (keywords.length === 0) return allSpaces;
|
|
1864
|
-
return allSpaces.filter(
|
|
1865
|
-
(s) => mySpaceIds.has(s.id) || keywords.some((kw) => s.name.toLowerCase().includes(kw))
|
|
1866
|
-
);
|
|
1867
|
-
}
|
|
1868
|
-
async function resolveActiveSprintListId(config, opts) {
|
|
1869
|
-
const client = new ClickUpClient(config);
|
|
1870
|
-
let folderId = opts?.folder ?? config.sprintFolderId;
|
|
1871
|
-
if (!folderId) {
|
|
1872
|
-
const favorites = getFavorites();
|
|
1873
|
-
const favoriteFolderIds = Object.values(favorites).filter((f) => f.type === "sprint-folder").map((f) => f.id);
|
|
1874
|
-
if (favoriteFolderIds.length > 0) {
|
|
1875
|
-
folderId = favoriteFolderIds[0];
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
let sprintLists;
|
|
1879
|
-
if (folderId) {
|
|
1880
|
-
sprintLists = await client.getFolderLists(folderId);
|
|
1881
|
-
} else {
|
|
1882
|
-
const [myTasks, allSpaces] = await Promise.all([
|
|
1883
|
-
client.getMyTasks(config.teamId),
|
|
1884
|
-
client.getSpaces(config.teamId)
|
|
1885
|
-
]);
|
|
1886
|
-
let spaces;
|
|
1887
|
-
if (opts?.space) {
|
|
1888
|
-
spaces = allSpaces.filter(
|
|
1889
|
-
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
1890
|
-
);
|
|
1891
|
-
if (spaces.length === 0) {
|
|
1892
|
-
throw new Error(`No space matching "${opts.space}" found.`);
|
|
1893
|
-
}
|
|
1894
|
-
} else {
|
|
1895
|
-
const mySpaceIds = new Set(
|
|
1896
|
-
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
1897
|
-
);
|
|
1898
|
-
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
1899
|
-
}
|
|
1900
|
-
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
1901
|
-
const sprintFolders = foldersBySpace.flat().filter((f) => {
|
|
1902
|
-
const lower = f.name.toLowerCase();
|
|
1903
|
-
return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
1904
|
-
});
|
|
1905
|
-
const listsByFolder = await Promise.all(
|
|
1906
|
-
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
1907
|
-
);
|
|
1908
|
-
sprintLists = listsByFolder.flat();
|
|
1909
|
-
}
|
|
1910
|
-
const activeList = findActiveSprintList(sprintLists);
|
|
1911
|
-
if (!activeList) {
|
|
1912
|
-
throw new Error(
|
|
1913
|
-
'No active sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
|
|
1914
|
-
);
|
|
1915
|
-
}
|
|
1916
|
-
return activeList.id;
|
|
1917
|
-
}
|
|
1918
|
-
async function runSprintCommand(config, opts) {
|
|
1919
|
-
const client = new ClickUpClient(config);
|
|
1920
|
-
process.stderr.write("Detecting active sprint...\n");
|
|
1921
|
-
let folderId = opts.folder ?? config.sprintFolderId;
|
|
1922
|
-
if (!folderId) {
|
|
1923
|
-
const favorites = getFavorites();
|
|
1924
|
-
const favoriteFolderIds = Object.values(favorites).filter((f) => f.type === "sprint-folder").map((f) => f.id);
|
|
1925
|
-
if (favoriteFolderIds.length > 0) {
|
|
1926
|
-
folderId = favoriteFolderIds[0];
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
const [myTasks, allSpaces, customTypes] = await Promise.all([
|
|
1930
|
-
client.getMyTasks(config.teamId),
|
|
1931
|
-
folderId ? Promise.resolve([]) : client.getSpaces(config.teamId),
|
|
1932
|
-
client.getCustomTaskTypes(config.teamId)
|
|
1933
|
-
]);
|
|
1934
|
-
const typeMap = buildTypeMap(customTypes);
|
|
1935
|
-
let sprintLists;
|
|
1936
|
-
if (folderId) {
|
|
1937
|
-
sprintLists = await client.getFolderLists(folderId);
|
|
1938
|
-
} else {
|
|
1939
|
-
let spaces;
|
|
1940
|
-
if (opts.space) {
|
|
1941
|
-
spaces = allSpaces.filter(
|
|
1942
|
-
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
1943
|
-
);
|
|
1944
|
-
if (spaces.length === 0) {
|
|
1945
|
-
throw new Error(
|
|
1946
|
-
`No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
|
|
1947
|
-
);
|
|
1948
|
-
}
|
|
1949
|
-
} else {
|
|
1950
|
-
const mySpaceIds = new Set(
|
|
1951
|
-
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
1952
|
-
);
|
|
1953
|
-
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
1954
|
-
}
|
|
1955
|
-
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
1956
|
-
const sprintFolders = foldersBySpace.flat().filter((f) => {
|
|
1957
|
-
const lower = f.name.toLowerCase();
|
|
1958
|
-
return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
1959
|
-
});
|
|
1960
|
-
const listsByFolder = await Promise.all(
|
|
1961
|
-
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
1962
|
-
);
|
|
1963
|
-
sprintLists = listsByFolder.flat();
|
|
1964
|
-
}
|
|
1965
|
-
let activeList = findActiveSprintList(sprintLists);
|
|
1966
|
-
if (!activeList && sprintLists.length > 1 && isTTY()) {
|
|
1967
|
-
const choice = await select({
|
|
1968
|
-
message: "Multiple sprint lists found. Which one?",
|
|
1969
|
-
choices: sprintLists.map((l) => ({
|
|
1970
|
-
name: `${l.name} (${l.id})`,
|
|
1971
|
-
value: l
|
|
1972
|
-
}))
|
|
1973
|
-
});
|
|
1974
|
-
activeList = choice;
|
|
1975
|
-
}
|
|
1976
|
-
if (!activeList && sprintLists.length > 1) {
|
|
1977
|
-
process.stderr.write(
|
|
1978
|
-
`Multiple sprint lists found:
|
|
1979
|
-
${sprintLists.map((l) => ` - ${l.name} (${l.id})`).join("\n")}
|
|
1980
|
-
Using: ${sprintLists[sprintLists.length - 1].name}
|
|
1981
|
-
`
|
|
1982
|
-
);
|
|
1983
|
-
activeList = sprintLists[sprintLists.length - 1] ?? null;
|
|
1984
|
-
}
|
|
1985
|
-
if (!activeList) {
|
|
1986
|
-
throw new Error(
|
|
1987
|
-
'No sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
|
|
1988
|
-
);
|
|
1989
|
-
}
|
|
1990
|
-
process.stderr.write(`Active sprint: ${activeList.name}
|
|
1991
|
-
`);
|
|
1992
|
-
const me = await client.getMe();
|
|
1993
|
-
const viewData = await client.getListViews(activeList.id);
|
|
1994
|
-
const listView = viewData.required_views?.list;
|
|
1995
|
-
let allTasks;
|
|
1996
|
-
if (listView) {
|
|
1997
|
-
allTasks = await client.getViewTasks(listView.id);
|
|
1998
|
-
} else {
|
|
1999
|
-
allTasks = await client.getTasksFromList(activeList.id);
|
|
2000
|
-
}
|
|
2001
|
-
let sprintTasks = allTasks.filter((t) => t.assignees.some((a) => Number(a.id) === me.id));
|
|
2002
|
-
if (!opts.includeClosed) {
|
|
2003
|
-
sprintTasks = sprintTasks.filter((t) => !isDoneStatus(t.status.status));
|
|
2004
|
-
}
|
|
2005
|
-
const filtered = opts.status ? sprintTasks.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase()) : sprintTasks;
|
|
2006
|
-
const summaries = filtered.map((t) => summarize(t, typeMap));
|
|
2007
|
-
await printTasks(summaries, opts.json ?? false, config);
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
export {
|
|
2011
|
-
isCustomTaskId,
|
|
2012
|
-
ClickUpClient,
|
|
2013
|
-
loadConfig,
|
|
2014
|
-
addProfile,
|
|
2015
|
-
removeProfile,
|
|
2016
|
-
setDefaultProfile,
|
|
2017
|
-
listProfiles,
|
|
2018
|
-
loadRawConfig,
|
|
2019
|
-
getConfigPath,
|
|
2020
|
-
getFilters,
|
|
2021
|
-
saveFilter,
|
|
2022
|
-
deleteFilter,
|
|
2023
|
-
getFavorites,
|
|
2024
|
-
saveFavorite,
|
|
2025
|
-
deleteFavorite,
|
|
2026
|
-
writeConfig,
|
|
2027
|
-
formatDate,
|
|
2028
|
-
formatTimestamp,
|
|
2029
|
-
formatDuration,
|
|
2030
|
-
formatLongDuration,
|
|
2031
|
-
formatDateISO,
|
|
2032
|
-
isTTY,
|
|
2033
|
-
shouldOutputJson,
|
|
2034
|
-
formatTable,
|
|
2035
|
-
colorStatus,
|
|
2036
|
-
TASK_COLUMNS,
|
|
2037
|
-
formatMarkdownTable,
|
|
2038
|
-
formatCommentsMarkdown,
|
|
2039
|
-
formatListsMarkdown,
|
|
2040
|
-
formatSpacesMarkdown,
|
|
2041
|
-
formatGroupedTasksMarkdown,
|
|
2042
|
-
formatTaskDetailMarkdown,
|
|
2043
|
-
formatUpdateConfirmation,
|
|
2044
|
-
formatCreateConfirmation,
|
|
2045
|
-
formatCommentConfirmation,
|
|
2046
|
-
formatAssignConfirmation,
|
|
2047
|
-
openUrl,
|
|
2048
|
-
formatTaskDetail,
|
|
2049
|
-
groupedTaskPicker,
|
|
2050
|
-
showDetailsAndOpen,
|
|
2051
|
-
isDoneStatus,
|
|
2052
|
-
summarize,
|
|
2053
|
-
buildTypeMap,
|
|
2054
|
-
fetchMyTasks,
|
|
2055
|
-
printTasks,
|
|
2056
|
-
SPRINT_KEYWORDS,
|
|
2057
|
-
parseSprintDates,
|
|
2058
|
-
findActiveSprintList,
|
|
2059
|
-
extractSpaceKeywords,
|
|
2060
|
-
findRelatedSpaces,
|
|
2061
|
-
resolveActiveSprintListId,
|
|
2062
|
-
runSprintCommand
|
|
2063
|
-
};
|