@krodak/clickup-cli 0.6.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/LICENSE +21 -0
- package/README.md +395 -0
- package/dist/index.js +1772 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1772 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
function configDir() {
|
|
12
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
13
|
+
if (xdg) return join(xdg, "cu");
|
|
14
|
+
return join(homedir(), ".config", "cu");
|
|
15
|
+
}
|
|
16
|
+
function configPath() {
|
|
17
|
+
return join(configDir(), "config.json");
|
|
18
|
+
}
|
|
19
|
+
function loadConfig() {
|
|
20
|
+
const envToken = process.env.CU_API_TOKEN?.trim();
|
|
21
|
+
const envTeamId = process.env.CU_TEAM_ID?.trim();
|
|
22
|
+
let fileToken;
|
|
23
|
+
let fileTeamId;
|
|
24
|
+
const path = configPath();
|
|
25
|
+
if (fs.existsSync(path)) {
|
|
26
|
+
const raw = fs.readFileSync(path, "utf-8");
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error(`Config file at ${path} contains invalid JSON. Please check the file syntax.`);
|
|
32
|
+
}
|
|
33
|
+
fileToken = parsed.apiToken?.trim();
|
|
34
|
+
fileTeamId = parsed.teamId?.trim();
|
|
35
|
+
}
|
|
36
|
+
const apiToken = envToken || fileToken;
|
|
37
|
+
if (!apiToken) {
|
|
38
|
+
throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cu init");
|
|
39
|
+
}
|
|
40
|
+
if (!apiToken.startsWith("pk_")) {
|
|
41
|
+
throw new Error("Config apiToken must start with pk_. The configured token does not.");
|
|
42
|
+
}
|
|
43
|
+
const teamId = envTeamId || fileTeamId;
|
|
44
|
+
if (!teamId) {
|
|
45
|
+
throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cu init");
|
|
46
|
+
}
|
|
47
|
+
return { apiToken, teamId };
|
|
48
|
+
}
|
|
49
|
+
function loadRawConfig() {
|
|
50
|
+
const path = configPath();
|
|
51
|
+
if (!fs.existsSync(path)) return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(path, "utf-8"));
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getConfigPath() {
|
|
59
|
+
return configPath();
|
|
60
|
+
}
|
|
61
|
+
function writeConfig(config) {
|
|
62
|
+
const dir = configDir();
|
|
63
|
+
if (!fs.existsSync(dir)) {
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
fs.writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/api.ts
|
|
70
|
+
var BASE_URL = "https://api.clickup.com/api/v2";
|
|
71
|
+
var MAX_PAGES = 100;
|
|
72
|
+
var ClickUpClient = class {
|
|
73
|
+
apiToken;
|
|
74
|
+
meCache = null;
|
|
75
|
+
constructor(config) {
|
|
76
|
+
this.apiToken = config.apiToken;
|
|
77
|
+
}
|
|
78
|
+
async request(path, options = {}) {
|
|
79
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
80
|
+
...options,
|
|
81
|
+
signal: AbortSignal.timeout(3e4),
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: this.apiToken,
|
|
84
|
+
...options.body ? { "Content-Type": "application/json" } : {},
|
|
85
|
+
...options.headers
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
data = await res.json();
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
|
|
93
|
+
}
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const raw = data.err ?? data.error ?? data.ECODE ?? res.statusText;
|
|
96
|
+
const errMsg = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
97
|
+
throw new Error(`ClickUp API error ${res.status}: ${errMsg}`);
|
|
98
|
+
}
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
async getMe() {
|
|
102
|
+
if (this.meCache) return this.meCache;
|
|
103
|
+
const data = await this.request("/user");
|
|
104
|
+
this.meCache = data.user;
|
|
105
|
+
return data.user;
|
|
106
|
+
}
|
|
107
|
+
async paginate(buildPath) {
|
|
108
|
+
const allTasks = [];
|
|
109
|
+
let page = 0;
|
|
110
|
+
let lastPage = false;
|
|
111
|
+
while (!lastPage && page < MAX_PAGES) {
|
|
112
|
+
const data = await this.request(buildPath(page));
|
|
113
|
+
const tasks = data.tasks;
|
|
114
|
+
if (!Array.isArray(tasks)) {
|
|
115
|
+
throw new Error(`Unexpected API response: expected tasks array, got ${typeof tasks}`);
|
|
116
|
+
}
|
|
117
|
+
allTasks.push(...tasks);
|
|
118
|
+
lastPage = data.last_page ?? true;
|
|
119
|
+
page++;
|
|
120
|
+
}
|
|
121
|
+
if (page >= MAX_PAGES && !lastPage) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`Warning: reached maximum page limit (${MAX_PAGES}), results may be incomplete
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return allTasks;
|
|
128
|
+
}
|
|
129
|
+
async getMyTasks(teamId, filters = {}) {
|
|
130
|
+
const me = await this.getMe();
|
|
131
|
+
const baseParams = new URLSearchParams({
|
|
132
|
+
subtasks: String(filters.subtasks ?? true)
|
|
133
|
+
});
|
|
134
|
+
baseParams.append("assignees[]", String(me.id));
|
|
135
|
+
for (const s of filters.statuses ?? []) baseParams.append("statuses[]", s);
|
|
136
|
+
for (const id of filters.listIds ?? []) baseParams.append("list_ids[]", id);
|
|
137
|
+
for (const id of filters.spaceIds ?? []) baseParams.append("space_ids[]", id);
|
|
138
|
+
return this.paginate((page) => {
|
|
139
|
+
const params = new URLSearchParams(baseParams);
|
|
140
|
+
params.set("page", String(page));
|
|
141
|
+
return `/team/${teamId}/task?${params.toString()}`;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async updateTask(taskId, options) {
|
|
145
|
+
return this.request(`/task/${taskId}`, {
|
|
146
|
+
method: "PUT",
|
|
147
|
+
body: JSON.stringify(options)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async postComment(taskId, commentText) {
|
|
151
|
+
return this.request(`/task/${taskId}/comment`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify({ comment_text: commentText })
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async getTaskComments(taskId) {
|
|
157
|
+
const data = await this.request(`/task/${taskId}/comment`);
|
|
158
|
+
return data.comments ?? [];
|
|
159
|
+
}
|
|
160
|
+
async getTasksFromList(listId, params = {}) {
|
|
161
|
+
return this.paginate((page) => {
|
|
162
|
+
const qs = new URLSearchParams({ subtasks: "true", page: String(page), ...params }).toString();
|
|
163
|
+
return `/list/${listId}/task?${qs}`;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async getMyTasksFromList(listId) {
|
|
167
|
+
const me = await this.getMe();
|
|
168
|
+
return this.getTasksFromList(listId, { "assignees[]": String(me.id) });
|
|
169
|
+
}
|
|
170
|
+
async getTask(taskId) {
|
|
171
|
+
return this.request(`/task/${taskId}`);
|
|
172
|
+
}
|
|
173
|
+
async updateTaskDescription(taskId, description) {
|
|
174
|
+
return this.updateTask(taskId, { description });
|
|
175
|
+
}
|
|
176
|
+
async createTask(listId, options) {
|
|
177
|
+
return this.request(`/list/${listId}/task`, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
body: JSON.stringify(options)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async getAssignedListIds(teamId) {
|
|
183
|
+
const tasks = await this.getMyTasks(teamId);
|
|
184
|
+
return new Set(tasks.map((t) => t.list.id));
|
|
185
|
+
}
|
|
186
|
+
async getTeams() {
|
|
187
|
+
const data = await this.request("/team");
|
|
188
|
+
return data.teams ?? [];
|
|
189
|
+
}
|
|
190
|
+
async getSpaces(teamId) {
|
|
191
|
+
const data = await this.request(`/team/${teamId}/space?archived=false`);
|
|
192
|
+
return data.spaces ?? [];
|
|
193
|
+
}
|
|
194
|
+
async getLists(spaceId) {
|
|
195
|
+
const data = await this.request(`/space/${spaceId}/list?archived=false`);
|
|
196
|
+
return data.lists ?? [];
|
|
197
|
+
}
|
|
198
|
+
async getFolders(spaceId) {
|
|
199
|
+
const data = await this.request(
|
|
200
|
+
`/space/${spaceId}/folder?archived=false`
|
|
201
|
+
);
|
|
202
|
+
return data.folders ?? [];
|
|
203
|
+
}
|
|
204
|
+
async getFolderLists(folderId) {
|
|
205
|
+
const data = await this.request(`/folder/${folderId}/list?archived=false`);
|
|
206
|
+
return data.lists ?? [];
|
|
207
|
+
}
|
|
208
|
+
async getListViews(listId) {
|
|
209
|
+
return this.request(
|
|
210
|
+
`/list/${listId}/view`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
async getViewTasks(viewId) {
|
|
214
|
+
return this.paginate((page) => `/view/${viewId}/task?page=${page}`);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/output.ts
|
|
219
|
+
import chalk from "chalk";
|
|
220
|
+
function isTTY() {
|
|
221
|
+
return Boolean(process.stdout.isTTY);
|
|
222
|
+
}
|
|
223
|
+
function cell(value, width) {
|
|
224
|
+
if (value.length > width) return value.slice(0, width - 1) + "\u2026";
|
|
225
|
+
return value.padEnd(width);
|
|
226
|
+
}
|
|
227
|
+
function computeWidths(rows, columns) {
|
|
228
|
+
return columns.map((col) => {
|
|
229
|
+
const headerLen = col.label.length;
|
|
230
|
+
const maxDataLen = rows.reduce((max, row) => {
|
|
231
|
+
const val = String(row[col.key] ?? "");
|
|
232
|
+
return Math.max(max, val.length);
|
|
233
|
+
}, 0);
|
|
234
|
+
const natural = Math.max(headerLen, maxDataLen);
|
|
235
|
+
return col.maxWidth ? Math.min(natural, col.maxWidth) : natural;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function formatTable(rows, columns) {
|
|
239
|
+
const widths = computeWidths(rows, columns);
|
|
240
|
+
const header = columns.map((c, i) => cell(c.label, widths[i])).join(" ");
|
|
241
|
+
const divider = "-".repeat(header.replace(/\x1b\[[0-9;]*m/g, "").length);
|
|
242
|
+
const lines = [chalk.bold(header), divider];
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
lines.push(columns.map((c, i) => cell(String(row[c.key] ?? ""), widths[i])).join(" "));
|
|
245
|
+
}
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
var TASK_COLUMNS = [
|
|
249
|
+
{ key: "id", label: "ID" },
|
|
250
|
+
{ key: "name", label: "NAME", maxWidth: 60 },
|
|
251
|
+
{ key: "status", label: "STATUS" },
|
|
252
|
+
{ key: "list", label: "LIST" }
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// src/interactive.ts
|
|
256
|
+
import { execFileSync } from "child_process";
|
|
257
|
+
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
258
|
+
import chalk2 from "chalk";
|
|
259
|
+
function openUrl(url) {
|
|
260
|
+
switch (process.platform) {
|
|
261
|
+
case "darwin":
|
|
262
|
+
execFileSync("open", [url]);
|
|
263
|
+
break;
|
|
264
|
+
case "linux":
|
|
265
|
+
execFileSync("xdg-open", [url]);
|
|
266
|
+
break;
|
|
267
|
+
case "win32":
|
|
268
|
+
execFileSync("cmd", ["/c", "start", "", url]);
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
process.stderr.write(`Cannot open browser on ${process.platform}. Visit: ${url}
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function formatMs(ms) {
|
|
276
|
+
const hours = Math.floor(ms / 36e5);
|
|
277
|
+
const mins = Math.floor(ms % 36e5 / 6e4);
|
|
278
|
+
if (hours > 0 && mins > 0) return `${hours}h ${mins}m`;
|
|
279
|
+
if (hours > 0) return `${hours}h`;
|
|
280
|
+
return `${mins}m`;
|
|
281
|
+
}
|
|
282
|
+
function formatTimestamp(ts) {
|
|
283
|
+
return new Date(Number(ts)).toLocaleDateString("en-US", {
|
|
284
|
+
month: "short",
|
|
285
|
+
day: "numeric",
|
|
286
|
+
year: "numeric"
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function descriptionPreview(text, maxLines = 3) {
|
|
290
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
291
|
+
const preview = lines.slice(0, maxLines);
|
|
292
|
+
const result = preview.map((l) => ` ${chalk2.dim(l.length > 100 ? l.slice(0, 99) + "\u2026" : l)}`).join("\n");
|
|
293
|
+
if (lines.length > maxLines)
|
|
294
|
+
return result + `
|
|
295
|
+
${chalk2.dim(`... (${lines.length - maxLines} more lines)`)}`;
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function formatTaskDetail(task) {
|
|
299
|
+
const lines = [];
|
|
300
|
+
const isInitiative2 = (task.custom_item_id ?? 0) !== 0;
|
|
301
|
+
const typeLabel = isInitiative2 ? "initiative" : "task";
|
|
302
|
+
lines.push(chalk2.bold.underline(task.name));
|
|
303
|
+
lines.push("");
|
|
304
|
+
const fields = [
|
|
305
|
+
["ID", task.id],
|
|
306
|
+
["Status", task.status?.status],
|
|
307
|
+
["Type", typeLabel],
|
|
308
|
+
["List", task.list?.name],
|
|
309
|
+
[
|
|
310
|
+
"Assignees",
|
|
311
|
+
task.assignees?.length ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
312
|
+
],
|
|
313
|
+
["Priority", task.priority?.priority],
|
|
314
|
+
["Start", task.start_date ? formatTimestamp(task.start_date) : void 0],
|
|
315
|
+
["Due", task.due_date ? formatTimestamp(task.due_date) : void 0],
|
|
316
|
+
["Estimate", task.time_estimate ? formatMs(task.time_estimate) : void 0],
|
|
317
|
+
["Tracked", task.time_spent ? formatMs(task.time_spent) : void 0],
|
|
318
|
+
["Tags", task.tags?.length ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
319
|
+
["Parent", task.parent || void 0],
|
|
320
|
+
["URL", task.url]
|
|
321
|
+
];
|
|
322
|
+
const maxLabel = Math.max(...fields.filter(([, v]) => v).map(([k]) => k.length));
|
|
323
|
+
for (const [label, value] of fields) {
|
|
324
|
+
if (!value) continue;
|
|
325
|
+
lines.push(` ${chalk2.bold(label.padEnd(maxLabel + 1))} ${value}`);
|
|
326
|
+
}
|
|
327
|
+
if (task.text_content?.trim()) {
|
|
328
|
+
lines.push("");
|
|
329
|
+
lines.push(descriptionPreview(task.text_content));
|
|
330
|
+
}
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
function formatChoiceName(task) {
|
|
334
|
+
const id = task.id.padEnd(12);
|
|
335
|
+
const name = task.name.length > 50 ? task.name.slice(0, 49) + "\u2026" : task.name.padEnd(50);
|
|
336
|
+
const status = task.status;
|
|
337
|
+
return `${id} ${name} ${chalk2.dim(status)}`;
|
|
338
|
+
}
|
|
339
|
+
async function interactiveTaskPicker(tasks) {
|
|
340
|
+
if (tasks.length === 0) return [];
|
|
341
|
+
const selected = await checkbox({
|
|
342
|
+
message: `${tasks.length} task(s) found. Select to view details / open in browser:`,
|
|
343
|
+
choices: tasks.map((t) => ({
|
|
344
|
+
name: formatChoiceName(t),
|
|
345
|
+
value: t.id
|
|
346
|
+
})),
|
|
347
|
+
pageSize: 20
|
|
348
|
+
});
|
|
349
|
+
return tasks.filter((t) => selected.includes(t.id));
|
|
350
|
+
}
|
|
351
|
+
async function groupedTaskPicker(groups) {
|
|
352
|
+
const allTasks = groups.flatMap((g) => g.tasks);
|
|
353
|
+
const totalCount = allTasks.length;
|
|
354
|
+
if (totalCount === 0) return [];
|
|
355
|
+
const choices = [];
|
|
356
|
+
for (const group of groups) {
|
|
357
|
+
if (group.tasks.length === 0) continue;
|
|
358
|
+
choices.push(new Separator(chalk2.bold(`${group.label} (${group.tasks.length})`)));
|
|
359
|
+
for (const task of group.tasks) {
|
|
360
|
+
choices.push({ name: formatChoiceName(task), value: task.id });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const selected = await checkbox({
|
|
364
|
+
message: `${totalCount} task(s) found. Select to view details / open in browser:`,
|
|
365
|
+
choices,
|
|
366
|
+
pageSize: 20
|
|
367
|
+
});
|
|
368
|
+
return allTasks.filter((t) => selected.includes(t.id));
|
|
369
|
+
}
|
|
370
|
+
async function showDetailsAndOpen(tasks, fetchTask) {
|
|
371
|
+
if (tasks.length === 0) return;
|
|
372
|
+
const separator = chalk2.dim("\u2500".repeat(60));
|
|
373
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
374
|
+
const task = tasks[i];
|
|
375
|
+
if (i > 0) {
|
|
376
|
+
console.log("");
|
|
377
|
+
console.log(separator);
|
|
378
|
+
}
|
|
379
|
+
console.log("");
|
|
380
|
+
if (fetchTask) {
|
|
381
|
+
const full = await fetchTask(task.id);
|
|
382
|
+
console.log(formatTaskDetail(full));
|
|
383
|
+
} else {
|
|
384
|
+
const fallback = {
|
|
385
|
+
id: task.id,
|
|
386
|
+
name: task.name,
|
|
387
|
+
status: { status: task.status, color: "" },
|
|
388
|
+
custom_item_id: task.task_type === "initiative" ? 1 : 0,
|
|
389
|
+
assignees: [],
|
|
390
|
+
url: task.url,
|
|
391
|
+
list: { id: "", name: task.list },
|
|
392
|
+
parent: task.parent
|
|
393
|
+
};
|
|
394
|
+
console.log(formatTaskDetail(fallback));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const urls = tasks.map((t) => t.url);
|
|
398
|
+
console.log("");
|
|
399
|
+
const shouldOpen = await confirm({
|
|
400
|
+
message: `Open ${urls.length} task(s) in browser?`,
|
|
401
|
+
default: true
|
|
402
|
+
});
|
|
403
|
+
if (shouldOpen) {
|
|
404
|
+
for (const url of urls) {
|
|
405
|
+
openUrl(url);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/commands/tasks.ts
|
|
411
|
+
var DONE_PATTERNS = ["done", "complete", "closed"];
|
|
412
|
+
function isDoneStatus(status) {
|
|
413
|
+
const lower = status.toLowerCase();
|
|
414
|
+
return DONE_PATTERNS.some((p) => lower.includes(p));
|
|
415
|
+
}
|
|
416
|
+
function isInitiative(task) {
|
|
417
|
+
return (task.custom_item_id ?? 0) !== 0;
|
|
418
|
+
}
|
|
419
|
+
function summarize(task) {
|
|
420
|
+
return {
|
|
421
|
+
id: task.id,
|
|
422
|
+
name: task.name,
|
|
423
|
+
status: task.status.status,
|
|
424
|
+
task_type: isInitiative(task) ? "initiative" : "task",
|
|
425
|
+
list: task.list.name,
|
|
426
|
+
url: task.url,
|
|
427
|
+
...task.parent ? { parent: task.parent } : {}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function fetchMyTasks(config, opts = {}) {
|
|
431
|
+
const client = new ClickUpClient(config);
|
|
432
|
+
const { typeFilter, name, ...apiFilters } = opts;
|
|
433
|
+
const allTasks = await client.getMyTasks(config.teamId, apiFilters);
|
|
434
|
+
let filtered = typeFilter === "initiative" ? allTasks.filter(isInitiative) : typeFilter === "task" ? allTasks.filter((t) => !isInitiative(t)) : allTasks;
|
|
435
|
+
if (name) {
|
|
436
|
+
const query = name.toLowerCase();
|
|
437
|
+
filtered = filtered.filter((t) => t.name.toLowerCase().includes(query));
|
|
438
|
+
}
|
|
439
|
+
return filtered.map(summarize);
|
|
440
|
+
}
|
|
441
|
+
async function printTasks(tasks, forceJson, config) {
|
|
442
|
+
if (forceJson || !isTTY()) {
|
|
443
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (tasks.length === 0) {
|
|
447
|
+
console.log("No tasks found.");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const fetchTask = config ? (() => {
|
|
451
|
+
const client = new ClickUpClient(config);
|
|
452
|
+
return (id) => client.getTask(id);
|
|
453
|
+
})() : void 0;
|
|
454
|
+
const selected = await interactiveTaskPicker(tasks);
|
|
455
|
+
await showDetailsAndOpen(selected, fetchTask);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/commands/update.ts
|
|
459
|
+
var PRIORITY_MAP = {
|
|
460
|
+
urgent: 1,
|
|
461
|
+
high: 2,
|
|
462
|
+
normal: 3,
|
|
463
|
+
low: 4
|
|
464
|
+
};
|
|
465
|
+
function parsePriority(value) {
|
|
466
|
+
const named = PRIORITY_MAP[value.toLowerCase()];
|
|
467
|
+
if (named !== void 0) return named;
|
|
468
|
+
const num = Number(value);
|
|
469
|
+
if (Number.isInteger(num) && num >= 1 && num <= 4) return num;
|
|
470
|
+
throw new Error("Priority must be urgent, high, normal, low, or 1-4");
|
|
471
|
+
}
|
|
472
|
+
function parseDueDate(value) {
|
|
473
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
474
|
+
throw new Error("Date must be in YYYY-MM-DD format");
|
|
475
|
+
}
|
|
476
|
+
const date = new Date(value);
|
|
477
|
+
if (isNaN(date.getTime())) throw new Error(`Invalid date: ${value}`);
|
|
478
|
+
return date.getTime();
|
|
479
|
+
}
|
|
480
|
+
function parseAssigneeId(value) {
|
|
481
|
+
const id = Number(value);
|
|
482
|
+
if (!Number.isInteger(id)) throw new Error("Assignee must be a numeric user ID");
|
|
483
|
+
return id;
|
|
484
|
+
}
|
|
485
|
+
function buildUpdatePayload(opts) {
|
|
486
|
+
const payload = {};
|
|
487
|
+
if (opts.name !== void 0) payload.name = opts.name;
|
|
488
|
+
if (opts.description !== void 0) payload.description = opts.description;
|
|
489
|
+
if (opts.status !== void 0) payload.status = opts.status;
|
|
490
|
+
if (opts.priority !== void 0) payload.priority = parsePriority(opts.priority);
|
|
491
|
+
if (opts.dueDate !== void 0) {
|
|
492
|
+
payload.due_date = parseDueDate(opts.dueDate);
|
|
493
|
+
payload.due_date_time = false;
|
|
494
|
+
}
|
|
495
|
+
if (opts.assignee !== void 0) {
|
|
496
|
+
payload.assignees = { add: [parseAssigneeId(opts.assignee)] };
|
|
497
|
+
}
|
|
498
|
+
return payload;
|
|
499
|
+
}
|
|
500
|
+
function hasUpdateFields(options) {
|
|
501
|
+
return options.name !== void 0 || options.description !== void 0 || options.status !== void 0 || options.priority !== void 0 || options.due_date !== void 0 || options.assignees !== void 0;
|
|
502
|
+
}
|
|
503
|
+
async function updateTask(config, taskId, options) {
|
|
504
|
+
if (!hasUpdateFields(options))
|
|
505
|
+
throw new Error(
|
|
506
|
+
"Provide at least one of: --name, --description, --status, --priority, --due-date, --assignee"
|
|
507
|
+
);
|
|
508
|
+
const client = new ClickUpClient(config);
|
|
509
|
+
const task = await client.updateTask(taskId, options);
|
|
510
|
+
return { id: task.id, name: task.name };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/commands/create.ts
|
|
514
|
+
async function createTask(config, options) {
|
|
515
|
+
const client = new ClickUpClient(config);
|
|
516
|
+
let listId = options.list;
|
|
517
|
+
if (!listId && options.parent) {
|
|
518
|
+
const parentTask = await client.getTask(options.parent);
|
|
519
|
+
listId = parentTask.list.id;
|
|
520
|
+
}
|
|
521
|
+
if (!listId) {
|
|
522
|
+
throw new Error("Provide --list or --parent (list is auto-detected from parent task)");
|
|
523
|
+
}
|
|
524
|
+
const payload = {
|
|
525
|
+
name: options.name,
|
|
526
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
527
|
+
...options.parent !== void 0 ? { parent: options.parent } : {},
|
|
528
|
+
...options.status !== void 0 ? { status: options.status } : {}
|
|
529
|
+
};
|
|
530
|
+
if (options.priority !== void 0) {
|
|
531
|
+
payload.priority = parsePriority(options.priority);
|
|
532
|
+
}
|
|
533
|
+
if (options.dueDate !== void 0) {
|
|
534
|
+
payload.due_date = parseDueDate(options.dueDate);
|
|
535
|
+
payload.due_date_time = false;
|
|
536
|
+
}
|
|
537
|
+
if (options.assignee !== void 0) {
|
|
538
|
+
payload.assignees = [parseAssigneeId(options.assignee)];
|
|
539
|
+
}
|
|
540
|
+
if (options.tags !== void 0) {
|
|
541
|
+
payload.tags = options.tags.split(",").map((t) => t.trim());
|
|
542
|
+
}
|
|
543
|
+
const task = await client.createTask(listId, payload);
|
|
544
|
+
return { id: task.id, name: task.name, url: task.url };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/commands/get.ts
|
|
548
|
+
async function getTask(config, taskId) {
|
|
549
|
+
const client = new ClickUpClient(config);
|
|
550
|
+
return client.getTask(taskId);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/commands/init.ts
|
|
554
|
+
import { password, select, confirm as confirm2 } from "@inquirer/prompts";
|
|
555
|
+
import fs2 from "fs";
|
|
556
|
+
async function runInitCommand() {
|
|
557
|
+
const configPath3 = getConfigPath();
|
|
558
|
+
if (fs2.existsSync(configPath3)) {
|
|
559
|
+
const overwrite = await confirm2({
|
|
560
|
+
message: `Config already exists at ${configPath3}. Overwrite?`,
|
|
561
|
+
default: false
|
|
562
|
+
});
|
|
563
|
+
if (!overwrite) {
|
|
564
|
+
process.stdout.write("Aborted.\n");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const apiToken = (await password({ message: "ClickUp API token (pk_...):" })).trim();
|
|
569
|
+
if (!apiToken.startsWith("pk_")) throw new Error("Token must start with pk_");
|
|
570
|
+
const client = new ClickUpClient({ apiToken });
|
|
571
|
+
let username;
|
|
572
|
+
try {
|
|
573
|
+
const me = await client.getMe();
|
|
574
|
+
username = me.username;
|
|
575
|
+
} catch (err) {
|
|
576
|
+
throw new Error(`Invalid token: ${err instanceof Error ? err.message : String(err)}`, {
|
|
577
|
+
cause: err
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
process.stdout.write(`Authenticated as @${username}
|
|
581
|
+
`);
|
|
582
|
+
const teams = await client.getTeams();
|
|
583
|
+
if (teams.length === 0) throw new Error("No workspaces found for this token.");
|
|
584
|
+
let teamId;
|
|
585
|
+
if (teams.length === 1) {
|
|
586
|
+
const [team] = teams;
|
|
587
|
+
if (!team) throw new Error("No workspaces found for this token.");
|
|
588
|
+
teamId = team.id;
|
|
589
|
+
process.stdout.write(`Workspace: ${team.name}
|
|
590
|
+
`);
|
|
591
|
+
} else {
|
|
592
|
+
teamId = await select({
|
|
593
|
+
message: "Select workspace:",
|
|
594
|
+
choices: teams.map((t) => ({ name: t.name, value: t.id }))
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
writeConfig({ apiToken, teamId });
|
|
598
|
+
process.stdout.write(`Config written to ${configPath3}
|
|
599
|
+
`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/commands/sprint.ts
|
|
603
|
+
function parseSprintDates(name) {
|
|
604
|
+
const m = name.match(/\((\d{1,2})\/(\d{1,2})\s*[-–]\s*(\d{1,2})\/(\d{1,2})\)/);
|
|
605
|
+
if (!m) return null;
|
|
606
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
607
|
+
const start = new Date(year, Number(m[1]) - 1, Number(m[2]));
|
|
608
|
+
const end = new Date(year, Number(m[3]) - 1, Number(m[4]), 23, 59, 59);
|
|
609
|
+
if (end < start) {
|
|
610
|
+
end.setFullYear(end.getFullYear() + 1);
|
|
611
|
+
}
|
|
612
|
+
return { start, end };
|
|
613
|
+
}
|
|
614
|
+
function findActiveSprintList(lists, today = /* @__PURE__ */ new Date()) {
|
|
615
|
+
if (lists.length === 0) return null;
|
|
616
|
+
for (const list of lists) {
|
|
617
|
+
const dates = parseSprintDates(list.name);
|
|
618
|
+
if (dates && today >= dates.start && today <= dates.end) {
|
|
619
|
+
return list;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return lists[lists.length - 1] ?? null;
|
|
623
|
+
}
|
|
624
|
+
var NOISE_WORDS = /* @__PURE__ */ new Set(["product", "team", "the", "and", "for", "test"]);
|
|
625
|
+
function extractSpaceKeywords(spaceName) {
|
|
626
|
+
return spaceName.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((w) => w.toLowerCase()).filter((w) => w.length >= 3 && !NOISE_WORDS.has(w));
|
|
627
|
+
}
|
|
628
|
+
function findRelatedSpaces(mySpaceIds, allSpaces) {
|
|
629
|
+
const mySpaces = allSpaces.filter((s) => mySpaceIds.has(s.id));
|
|
630
|
+
const keywords = mySpaces.flatMap((s) => extractSpaceKeywords(s.name));
|
|
631
|
+
if (keywords.length === 0) return allSpaces;
|
|
632
|
+
return allSpaces.filter(
|
|
633
|
+
(s) => mySpaceIds.has(s.id) || keywords.some((kw) => s.name.toLowerCase().includes(kw))
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
async function runSprintCommand(config, opts) {
|
|
637
|
+
const client = new ClickUpClient(config);
|
|
638
|
+
process.stderr.write("Detecting active sprint...\n");
|
|
639
|
+
const [myTasks, allSpaces] = await Promise.all([
|
|
640
|
+
client.getMyTasks(config.teamId),
|
|
641
|
+
client.getSpaces(config.teamId)
|
|
642
|
+
]);
|
|
643
|
+
let spaces;
|
|
644
|
+
if (opts.space) {
|
|
645
|
+
spaces = allSpaces.filter(
|
|
646
|
+
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
647
|
+
);
|
|
648
|
+
if (spaces.length === 0) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`No space matching "${opts.space}" found. Use \`cu spaces\` to list available spaces.`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
const mySpaceIds = new Set(
|
|
655
|
+
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
656
|
+
);
|
|
657
|
+
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
658
|
+
}
|
|
659
|
+
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
660
|
+
const sprintFolders = foldersBySpace.flat().filter((f) => f.name.toLowerCase().includes("sprint"));
|
|
661
|
+
const listsByFolder = await Promise.all(
|
|
662
|
+
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
663
|
+
);
|
|
664
|
+
const sprintLists = listsByFolder.flat();
|
|
665
|
+
const activeList = findActiveSprintList(sprintLists);
|
|
666
|
+
if (!activeList) {
|
|
667
|
+
throw new Error('No sprint list found. Ensure sprint folders contain "sprint" in their name.');
|
|
668
|
+
}
|
|
669
|
+
process.stderr.write(`Active sprint: ${activeList.name}
|
|
670
|
+
`);
|
|
671
|
+
const me = await client.getMe();
|
|
672
|
+
const viewData = await client.getListViews(activeList.id);
|
|
673
|
+
const listView = viewData.required_views?.list;
|
|
674
|
+
let sprintTasks;
|
|
675
|
+
if (listView) {
|
|
676
|
+
const allViewTasks = await client.getViewTasks(listView.id);
|
|
677
|
+
sprintTasks = allViewTasks.filter((t) => t.assignees.some((a) => a.id === me.id));
|
|
678
|
+
} else {
|
|
679
|
+
sprintTasks = await client.getMyTasksFromList(activeList.id);
|
|
680
|
+
}
|
|
681
|
+
const filtered = opts.status ? sprintTasks.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase()) : sprintTasks;
|
|
682
|
+
const summaries = filtered.map(summarize);
|
|
683
|
+
await printTasks(summaries, opts.json ?? false, config);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/commands/subtasks.ts
|
|
687
|
+
async function fetchSubtasks(config, taskId) {
|
|
688
|
+
const client = new ClickUpClient(config);
|
|
689
|
+
const parent = await client.getTask(taskId);
|
|
690
|
+
const tasks = await client.getTasksFromList(parent.list.id, { parent: taskId, subtasks: "false" });
|
|
691
|
+
return tasks.map(summarize);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/commands/comment.ts
|
|
695
|
+
async function postComment(config, taskId, text) {
|
|
696
|
+
if (!text.trim()) throw new Error("Comment text cannot be empty");
|
|
697
|
+
const client = new ClickUpClient(config);
|
|
698
|
+
return client.postComment(taskId, text);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/commands/comments.ts
|
|
702
|
+
import chalk3 from "chalk";
|
|
703
|
+
function formatDate(timestamp) {
|
|
704
|
+
return new Date(Number(timestamp)).toLocaleString("en-US", {
|
|
705
|
+
month: "short",
|
|
706
|
+
day: "numeric",
|
|
707
|
+
year: "numeric",
|
|
708
|
+
hour: "numeric",
|
|
709
|
+
minute: "2-digit"
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async function fetchComments(config, taskId) {
|
|
713
|
+
const client = new ClickUpClient(config);
|
|
714
|
+
const comments = await client.getTaskComments(taskId);
|
|
715
|
+
return comments.map((c) => ({
|
|
716
|
+
id: c.id,
|
|
717
|
+
user: c.user.username,
|
|
718
|
+
date: c.date,
|
|
719
|
+
text: c.comment_text
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
function printComments(comments, forceJson) {
|
|
723
|
+
if (forceJson || !isTTY()) {
|
|
724
|
+
console.log(JSON.stringify(comments, null, 2));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (comments.length === 0) {
|
|
728
|
+
console.log("No comments found.");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const separator = chalk3.dim("-".repeat(60));
|
|
732
|
+
for (let i = 0; i < comments.length; i++) {
|
|
733
|
+
const c = comments[i];
|
|
734
|
+
if (i > 0) console.log(separator);
|
|
735
|
+
console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate(c.date))}`);
|
|
736
|
+
console.log(c.text);
|
|
737
|
+
if (i < comments.length - 1) console.log("");
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/commands/lists.ts
|
|
742
|
+
async function fetchLists(config, spaceId, opts = {}) {
|
|
743
|
+
const client = new ClickUpClient(config);
|
|
744
|
+
const results = [];
|
|
745
|
+
const folderlessLists = await client.getLists(spaceId);
|
|
746
|
+
for (const list of folderlessLists) {
|
|
747
|
+
results.push({ id: list.id, name: list.name, folder: "(none)" });
|
|
748
|
+
}
|
|
749
|
+
const folders = await client.getFolders(spaceId);
|
|
750
|
+
const folderListArrays = await Promise.all(folders.map((f) => client.getFolderLists(f.id)));
|
|
751
|
+
for (let i = 0; i < folders.length; i++) {
|
|
752
|
+
const folder = folders[i];
|
|
753
|
+
for (const list of folderListArrays[i]) {
|
|
754
|
+
results.push({ id: list.id, name: list.name, folder: folder.name });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (opts.name) {
|
|
758
|
+
const query = opts.name.toLowerCase();
|
|
759
|
+
return results.filter((l) => l.name.toLowerCase().includes(query));
|
|
760
|
+
}
|
|
761
|
+
return results;
|
|
762
|
+
}
|
|
763
|
+
function printLists(lists, forceJson) {
|
|
764
|
+
if (forceJson || !isTTY()) {
|
|
765
|
+
console.log(JSON.stringify(lists, null, 2));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (lists.length === 0) {
|
|
769
|
+
console.log("No lists found.");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const table = formatTable(lists, [
|
|
773
|
+
{ key: "id", label: "ID" },
|
|
774
|
+
{ key: "name", label: "NAME", maxWidth: 50 },
|
|
775
|
+
{ key: "folder", label: "FOLDER", maxWidth: 30 }
|
|
776
|
+
]);
|
|
777
|
+
console.log(table);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/commands/inbox.ts
|
|
781
|
+
var TIME_PERIODS = [
|
|
782
|
+
{ key: "today", label: "Today" },
|
|
783
|
+
{ key: "yesterday", label: "Yesterday" },
|
|
784
|
+
{ key: "last_7_days", label: "Last 7 days" },
|
|
785
|
+
{ key: "earlier_this_month", label: "Earlier this month" },
|
|
786
|
+
{ key: "last_month", label: "Last month" },
|
|
787
|
+
{ key: "older", label: "Older" }
|
|
788
|
+
];
|
|
789
|
+
function summarizeWithDate(task) {
|
|
790
|
+
return {
|
|
791
|
+
...summarize(task),
|
|
792
|
+
date_updated: task.date_updated ?? "0"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function classifyTimePeriod(timestampMs, now) {
|
|
796
|
+
const todayStart = new Date(now);
|
|
797
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
798
|
+
const yesterdayStart = new Date(todayStart);
|
|
799
|
+
yesterdayStart.setDate(yesterdayStart.getDate() - 1);
|
|
800
|
+
const sevenDaysAgo = new Date(todayStart);
|
|
801
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6);
|
|
802
|
+
const thisMonthStart = new Date(todayStart.getFullYear(), todayStart.getMonth(), 1);
|
|
803
|
+
const lastMonthStart = new Date(todayStart.getFullYear(), todayStart.getMonth() - 1, 1);
|
|
804
|
+
if (timestampMs >= todayStart.getTime()) return "today";
|
|
805
|
+
if (timestampMs >= yesterdayStart.getTime()) return "yesterday";
|
|
806
|
+
if (timestampMs >= sevenDaysAgo.getTime()) return "last_7_days";
|
|
807
|
+
if (timestampMs >= thisMonthStart.getTime()) return "earlier_this_month";
|
|
808
|
+
if (timestampMs >= lastMonthStart.getTime()) return "last_month";
|
|
809
|
+
return "older";
|
|
810
|
+
}
|
|
811
|
+
function groupTasks(tasks, now) {
|
|
812
|
+
const groups = {
|
|
813
|
+
today: [],
|
|
814
|
+
yesterday: [],
|
|
815
|
+
last_7_days: [],
|
|
816
|
+
earlier_this_month: [],
|
|
817
|
+
last_month: [],
|
|
818
|
+
older: []
|
|
819
|
+
};
|
|
820
|
+
for (const task of tasks) {
|
|
821
|
+
const period = classifyTimePeriod(Number(task.date_updated), now);
|
|
822
|
+
groups[period].push(task);
|
|
823
|
+
}
|
|
824
|
+
return groups;
|
|
825
|
+
}
|
|
826
|
+
async function fetchInbox(config, days = 30) {
|
|
827
|
+
const client = new ClickUpClient(config);
|
|
828
|
+
const tasks = await client.getMyTasks(config.teamId, { subtasks: true });
|
|
829
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
830
|
+
return tasks.filter((t) => Number(t.date_updated ?? 0) > cutoff).sort((a, b) => Number(b.date_updated ?? 0) - Number(a.date_updated ?? 0)).map(summarizeWithDate);
|
|
831
|
+
}
|
|
832
|
+
async function printInbox(tasks, forceJson, config) {
|
|
833
|
+
const now = Date.now();
|
|
834
|
+
const groups = groupTasks(tasks, now);
|
|
835
|
+
if (forceJson || !isTTY()) {
|
|
836
|
+
const jsonGroups = {};
|
|
837
|
+
for (const { key } of TIME_PERIODS) {
|
|
838
|
+
if (groups[key].length > 0) {
|
|
839
|
+
jsonGroups[key] = groups[key];
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
console.log(JSON.stringify(jsonGroups, null, 2));
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (tasks.length === 0) {
|
|
846
|
+
console.log("No recently updated tasks.");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const pickerGroups = TIME_PERIODS.filter((def) => groups[def.key].length > 0).map((def) => ({
|
|
850
|
+
label: def.label,
|
|
851
|
+
tasks: groups[def.key]
|
|
852
|
+
}));
|
|
853
|
+
const fetchTask = config ? (() => {
|
|
854
|
+
const client = new ClickUpClient(config);
|
|
855
|
+
return (id) => client.getTask(id);
|
|
856
|
+
})() : void 0;
|
|
857
|
+
const selected = await groupedTaskPicker(pickerGroups);
|
|
858
|
+
await showDetailsAndOpen(selected, fetchTask);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/commands/spaces.ts
|
|
862
|
+
async function listSpaces(config, opts) {
|
|
863
|
+
const client = new ClickUpClient(config);
|
|
864
|
+
let spaces = await client.getSpaces(config.teamId);
|
|
865
|
+
if (opts.name) {
|
|
866
|
+
const lower = opts.name.toLowerCase();
|
|
867
|
+
spaces = spaces.filter((s) => s.name.toLowerCase().includes(lower));
|
|
868
|
+
}
|
|
869
|
+
if (opts.my) {
|
|
870
|
+
const tasks = await client.getMyTasks(config.teamId);
|
|
871
|
+
const mySpaceIds = new Set(
|
|
872
|
+
tasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
873
|
+
);
|
|
874
|
+
spaces = spaces.filter((s) => mySpaceIds.has(s.id));
|
|
875
|
+
}
|
|
876
|
+
if (!opts.json && isTTY()) {
|
|
877
|
+
const table = formatTable(
|
|
878
|
+
spaces.map((s) => ({ id: s.id, name: s.name })),
|
|
879
|
+
[
|
|
880
|
+
{ key: "id", label: "ID" },
|
|
881
|
+
{ key: "name", label: "NAME" }
|
|
882
|
+
]
|
|
883
|
+
);
|
|
884
|
+
console.log(table);
|
|
885
|
+
} else {
|
|
886
|
+
console.log(JSON.stringify(spaces, null, 2));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/commands/assigned.ts
|
|
891
|
+
var CLOSED_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "complete", "completed"]);
|
|
892
|
+
var STATUS_ORDER = [
|
|
893
|
+
"code review",
|
|
894
|
+
"in review",
|
|
895
|
+
"review",
|
|
896
|
+
"in progress",
|
|
897
|
+
"to do",
|
|
898
|
+
"open",
|
|
899
|
+
"needs definition",
|
|
900
|
+
"backlog",
|
|
901
|
+
"blocked"
|
|
902
|
+
];
|
|
903
|
+
function toJsonTask(task, summary) {
|
|
904
|
+
return {
|
|
905
|
+
id: summary.id,
|
|
906
|
+
name: summary.name,
|
|
907
|
+
status: summary.status,
|
|
908
|
+
task_type: summary.task_type,
|
|
909
|
+
list: summary.list,
|
|
910
|
+
url: summary.url,
|
|
911
|
+
priority: task.priority?.priority ?? null,
|
|
912
|
+
due_date: task.due_date ?? null
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function statusSortKey(status) {
|
|
916
|
+
const idx = STATUS_ORDER.indexOf(status.toLowerCase());
|
|
917
|
+
return idx === -1 ? STATUS_ORDER.length : idx;
|
|
918
|
+
}
|
|
919
|
+
function groupByStatus(tasks, includeClosed) {
|
|
920
|
+
const groups = /* @__PURE__ */ new Map();
|
|
921
|
+
for (const task of tasks) {
|
|
922
|
+
const status = task.status.status;
|
|
923
|
+
const key = status.toLowerCase();
|
|
924
|
+
if (!includeClosed && CLOSED_STATUSES.has(key)) continue;
|
|
925
|
+
if (!groups.has(status)) {
|
|
926
|
+
groups.set(status, []);
|
|
927
|
+
}
|
|
928
|
+
groups.get(status).push(task);
|
|
929
|
+
}
|
|
930
|
+
return Array.from(groups.entries()).sort((a, b) => {
|
|
931
|
+
const aIsClosed = CLOSED_STATUSES.has(a[0].toLowerCase());
|
|
932
|
+
const bIsClosed = CLOSED_STATUSES.has(b[0].toLowerCase());
|
|
933
|
+
if (aIsClosed !== bIsClosed) return aIsClosed ? 1 : -1;
|
|
934
|
+
return statusSortKey(a[0]) - statusSortKey(b[0]);
|
|
935
|
+
}).map(([status, tasks2]) => ({ status, tasks: tasks2 }));
|
|
936
|
+
}
|
|
937
|
+
async function runAssignedCommand(config, opts) {
|
|
938
|
+
const client = new ClickUpClient(config);
|
|
939
|
+
const allTasks = await client.getMyTasks(config.teamId);
|
|
940
|
+
const groups = groupByStatus(allTasks, opts.includeClosed ?? false);
|
|
941
|
+
if (opts.json || !isTTY()) {
|
|
942
|
+
const result = {};
|
|
943
|
+
for (const group of groups) {
|
|
944
|
+
result[group.status.toLowerCase()] = group.tasks.map((t) => toJsonTask(t, summarize(t)));
|
|
945
|
+
}
|
|
946
|
+
console.log(JSON.stringify(result, null, 2));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (groups.length === 0) {
|
|
950
|
+
console.log("No tasks found.");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const pickerGroups = groups.map((g) => ({
|
|
954
|
+
label: g.status.toUpperCase(),
|
|
955
|
+
tasks: g.tasks.map(summarize)
|
|
956
|
+
}));
|
|
957
|
+
const selected = await groupedTaskPicker(pickerGroups);
|
|
958
|
+
await showDetailsAndOpen(selected, (id) => client.getTask(id));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/commands/open.ts
|
|
962
|
+
function looksLikeTaskId(query) {
|
|
963
|
+
return /^[a-z0-9]+$/i.test(query) && query.length <= 12;
|
|
964
|
+
}
|
|
965
|
+
async function openTask(config, query, opts = {}) {
|
|
966
|
+
const client = new ClickUpClient(config);
|
|
967
|
+
if (looksLikeTaskId(query)) {
|
|
968
|
+
let task;
|
|
969
|
+
try {
|
|
970
|
+
task = await client.getTask(query);
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
if (task) {
|
|
974
|
+
if (opts.json) {
|
|
975
|
+
console.log(JSON.stringify(task, null, 2));
|
|
976
|
+
} else {
|
|
977
|
+
if (isTTY()) {
|
|
978
|
+
console.log(task.name);
|
|
979
|
+
console.log(task.url);
|
|
980
|
+
}
|
|
981
|
+
openUrl(task.url);
|
|
982
|
+
}
|
|
983
|
+
return task;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const tasks = await fetchMyTasks(config, { name: query });
|
|
987
|
+
if (tasks.length === 0) {
|
|
988
|
+
throw new Error(`No tasks found matching "${query}"`);
|
|
989
|
+
}
|
|
990
|
+
const first = tasks[0];
|
|
991
|
+
if (tasks.length > 1 && isTTY()) {
|
|
992
|
+
console.log(`Found ${tasks.length} matches:`);
|
|
993
|
+
for (const t of tasks) {
|
|
994
|
+
console.log(` ${t.id} ${t.name}`);
|
|
995
|
+
}
|
|
996
|
+
console.log("Opening first match...");
|
|
997
|
+
}
|
|
998
|
+
if (opts.json) {
|
|
999
|
+
const fullTask = await client.getTask(first.id);
|
|
1000
|
+
console.log(JSON.stringify(fullTask, null, 2));
|
|
1001
|
+
return fullTask;
|
|
1002
|
+
}
|
|
1003
|
+
if (isTTY()) {
|
|
1004
|
+
console.log(first.name);
|
|
1005
|
+
console.log(first.url);
|
|
1006
|
+
}
|
|
1007
|
+
openUrl(first.url);
|
|
1008
|
+
return {
|
|
1009
|
+
id: first.id,
|
|
1010
|
+
name: first.name,
|
|
1011
|
+
status: { status: first.status, color: "" },
|
|
1012
|
+
assignees: [],
|
|
1013
|
+
url: first.url,
|
|
1014
|
+
list: { id: "", name: first.list },
|
|
1015
|
+
...first.parent ? { parent: first.parent } : {}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/commands/summary.ts
|
|
1020
|
+
var IN_PROGRESS_PATTERNS = ["in progress", "in review", "doing"];
|
|
1021
|
+
function isCompletedRecently(task, cutoff) {
|
|
1022
|
+
if (!isDoneStatus(task.status.status)) return false;
|
|
1023
|
+
const updated = Number(task.date_updated);
|
|
1024
|
+
return !isNaN(updated) && updated >= cutoff;
|
|
1025
|
+
}
|
|
1026
|
+
function isInProgress(task) {
|
|
1027
|
+
const status = task.status.status.toLowerCase();
|
|
1028
|
+
return IN_PROGRESS_PATTERNS.some((p) => status.includes(p));
|
|
1029
|
+
}
|
|
1030
|
+
function isOverdue(task, now) {
|
|
1031
|
+
if (!task.due_date) return false;
|
|
1032
|
+
const due = Number(task.due_date);
|
|
1033
|
+
return !isNaN(due) && due < now;
|
|
1034
|
+
}
|
|
1035
|
+
function categorizeTasks(tasks, hoursBack) {
|
|
1036
|
+
const now = Date.now();
|
|
1037
|
+
const cutoff = now - hoursBack * 60 * 60 * 1e3;
|
|
1038
|
+
const completed = [];
|
|
1039
|
+
const inProgress = [];
|
|
1040
|
+
const overdue = [];
|
|
1041
|
+
for (const task of tasks) {
|
|
1042
|
+
const done = isDoneStatus(task.status.status);
|
|
1043
|
+
if (done && isCompletedRecently(task, cutoff)) {
|
|
1044
|
+
completed.push(summarize(task));
|
|
1045
|
+
}
|
|
1046
|
+
if (!done && isInProgress(task)) {
|
|
1047
|
+
inProgress.push(summarize(task));
|
|
1048
|
+
}
|
|
1049
|
+
if (!done && isOverdue(task, now)) {
|
|
1050
|
+
overdue.push(summarize(task));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return { completed, inProgress, overdue };
|
|
1054
|
+
}
|
|
1055
|
+
function printSection(label, tasks) {
|
|
1056
|
+
console.log(`
|
|
1057
|
+
${label} (${tasks.length})`);
|
|
1058
|
+
if (tasks.length === 0) {
|
|
1059
|
+
console.log(" None");
|
|
1060
|
+
} else {
|
|
1061
|
+
console.log(formatTable(tasks, TASK_COLUMNS));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function runSummaryCommand(config, opts) {
|
|
1065
|
+
const client = new ClickUpClient(config);
|
|
1066
|
+
const allTasks = await client.getMyTasks(config.teamId);
|
|
1067
|
+
const result = categorizeTasks(allTasks, opts.hours);
|
|
1068
|
+
if (opts.json || !isTTY()) {
|
|
1069
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
printSection("Completed Recently", result.completed);
|
|
1073
|
+
printSection("In Progress", result.inProgress);
|
|
1074
|
+
printSection("Overdue", result.overdue);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/commands/overdue.ts
|
|
1078
|
+
function isOverdue2(task, now) {
|
|
1079
|
+
if (!task.due_date) return false;
|
|
1080
|
+
return Number(task.due_date) < now;
|
|
1081
|
+
}
|
|
1082
|
+
async function fetchOverdueTasks(config) {
|
|
1083
|
+
const client = new ClickUpClient(config);
|
|
1084
|
+
const allTasks = await client.getMyTasks(config.teamId);
|
|
1085
|
+
const now = Date.now();
|
|
1086
|
+
return allTasks.filter((t) => isOverdue2(t, now) && !isDoneStatus(t.status.status)).sort((a, b) => Number(a.due_date) - Number(b.due_date)).map(summarize);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/commands/config.ts
|
|
1090
|
+
var VALID_KEYS = /* @__PURE__ */ new Set(["apiToken", "teamId"]);
|
|
1091
|
+
function assertValidKey(key) {
|
|
1092
|
+
if (!VALID_KEYS.has(key)) {
|
|
1093
|
+
throw new Error(`Unknown config key: ${key}. Valid keys: ${[...VALID_KEYS].join(", ")}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function getConfigValue(key) {
|
|
1097
|
+
assertValidKey(key);
|
|
1098
|
+
const raw = loadRawConfig();
|
|
1099
|
+
const value = raw[key]?.trim();
|
|
1100
|
+
return value || void 0;
|
|
1101
|
+
}
|
|
1102
|
+
function setConfigValue(key, value) {
|
|
1103
|
+
assertValidKey(key);
|
|
1104
|
+
if (key === "apiToken" && !value.startsWith("pk_")) {
|
|
1105
|
+
throw new Error("apiToken must start with pk_");
|
|
1106
|
+
}
|
|
1107
|
+
if (key === "teamId" && value.trim() === "") {
|
|
1108
|
+
throw new Error("teamId must be non-empty");
|
|
1109
|
+
}
|
|
1110
|
+
const raw = loadRawConfig();
|
|
1111
|
+
const merged = {
|
|
1112
|
+
apiToken: raw.apiToken ?? "",
|
|
1113
|
+
teamId: raw.teamId ?? "",
|
|
1114
|
+
...{ [key]: value }
|
|
1115
|
+
};
|
|
1116
|
+
writeConfig(merged);
|
|
1117
|
+
}
|
|
1118
|
+
function configPath2() {
|
|
1119
|
+
return getConfigPath();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/commands/assign.ts
|
|
1123
|
+
async function resolveUserId(client, value) {
|
|
1124
|
+
if (value === "me") {
|
|
1125
|
+
const user = await client.getMe();
|
|
1126
|
+
return user.id;
|
|
1127
|
+
}
|
|
1128
|
+
return parseAssigneeId(value);
|
|
1129
|
+
}
|
|
1130
|
+
async function assignTask(config, taskId, opts) {
|
|
1131
|
+
if (!opts.to && !opts.remove) {
|
|
1132
|
+
throw new Error("Provide at least one of: --to, --remove");
|
|
1133
|
+
}
|
|
1134
|
+
const client = new ClickUpClient(config);
|
|
1135
|
+
const add = [];
|
|
1136
|
+
const rem = [];
|
|
1137
|
+
if (opts.to) {
|
|
1138
|
+
add.push(await resolveUserId(client, opts.to));
|
|
1139
|
+
}
|
|
1140
|
+
if (opts.remove) {
|
|
1141
|
+
rem.push(await resolveUserId(client, opts.remove));
|
|
1142
|
+
}
|
|
1143
|
+
return client.updateTask(taskId, {
|
|
1144
|
+
assignees: {
|
|
1145
|
+
...add.length > 0 ? { add } : {},
|
|
1146
|
+
...rem.length > 0 ? { rem } : {}
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/commands/completion.ts
|
|
1152
|
+
function bashCompletion() {
|
|
1153
|
+
return `_cu_completions() {
|
|
1154
|
+
local cur prev words cword
|
|
1155
|
+
|
|
1156
|
+
if type _init_completion &>/dev/null; then
|
|
1157
|
+
_init_completion || return
|
|
1158
|
+
else
|
|
1159
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1160
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1161
|
+
words=("\${COMP_WORDS[@]}")
|
|
1162
|
+
cword=$COMP_CWORD
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
local commands="init tasks initiatives task update create sprint subtasks comment comments lists spaces inbox assigned open summary overdue assign config completion"
|
|
1166
|
+
|
|
1167
|
+
if [[ $cword -eq 1 ]]; then
|
|
1168
|
+
COMPREPLY=($(compgen -W "$commands --help --version" -- "$cur"))
|
|
1169
|
+
return
|
|
1170
|
+
fi
|
|
1171
|
+
|
|
1172
|
+
local cmd="\${words[1]}"
|
|
1173
|
+
|
|
1174
|
+
case "$prev" in
|
|
1175
|
+
--priority)
|
|
1176
|
+
COMPREPLY=($(compgen -W "urgent high normal low" -- "$cur"))
|
|
1177
|
+
return
|
|
1178
|
+
;;
|
|
1179
|
+
--status)
|
|
1180
|
+
COMPREPLY=($(compgen -W 'open "in progress" "in review" done closed' -- "$cur"))
|
|
1181
|
+
return
|
|
1182
|
+
;;
|
|
1183
|
+
esac
|
|
1184
|
+
|
|
1185
|
+
case "$cmd" in
|
|
1186
|
+
tasks|initiatives)
|
|
1187
|
+
COMPREPLY=($(compgen -W "--status --list --space --name --json" -- "$cur"))
|
|
1188
|
+
;;
|
|
1189
|
+
task)
|
|
1190
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
1191
|
+
;;
|
|
1192
|
+
update)
|
|
1193
|
+
COMPREPLY=($(compgen -W "--name --description --status --priority --due-date --assignee" -- "$cur"))
|
|
1194
|
+
;;
|
|
1195
|
+
create)
|
|
1196
|
+
COMPREPLY=($(compgen -W "--list --name --description --parent --status --priority --due-date --assignee --tags" -- "$cur"))
|
|
1197
|
+
;;
|
|
1198
|
+
sprint)
|
|
1199
|
+
COMPREPLY=($(compgen -W "--status --space --json" -- "$cur"))
|
|
1200
|
+
;;
|
|
1201
|
+
subtasks)
|
|
1202
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
1203
|
+
;;
|
|
1204
|
+
comment)
|
|
1205
|
+
COMPREPLY=($(compgen -W "--message" -- "$cur"))
|
|
1206
|
+
;;
|
|
1207
|
+
comments)
|
|
1208
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
1209
|
+
;;
|
|
1210
|
+
lists)
|
|
1211
|
+
COMPREPLY=($(compgen -W "--name --json" -- "$cur"))
|
|
1212
|
+
;;
|
|
1213
|
+
spaces)
|
|
1214
|
+
COMPREPLY=($(compgen -W "--name --my --json" -- "$cur"))
|
|
1215
|
+
;;
|
|
1216
|
+
inbox)
|
|
1217
|
+
COMPREPLY=($(compgen -W "--json --days" -- "$cur"))
|
|
1218
|
+
;;
|
|
1219
|
+
assigned)
|
|
1220
|
+
COMPREPLY=($(compgen -W "--include-closed --json" -- "$cur"))
|
|
1221
|
+
;;
|
|
1222
|
+
open)
|
|
1223
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
1224
|
+
;;
|
|
1225
|
+
summary)
|
|
1226
|
+
COMPREPLY=($(compgen -W "--hours --json" -- "$cur"))
|
|
1227
|
+
;;
|
|
1228
|
+
overdue)
|
|
1229
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
1230
|
+
;;
|
|
1231
|
+
assign)
|
|
1232
|
+
COMPREPLY=($(compgen -W "--to --remove --json" -- "$cur"))
|
|
1233
|
+
;;
|
|
1234
|
+
config)
|
|
1235
|
+
if [[ $cword -eq 2 ]]; then
|
|
1236
|
+
COMPREPLY=($(compgen -W "get set path" -- "$cur"))
|
|
1237
|
+
elif [[ $cword -eq 3 ]]; then
|
|
1238
|
+
local subcmd="\${words[2]}"
|
|
1239
|
+
case "$subcmd" in
|
|
1240
|
+
get|set)
|
|
1241
|
+
COMPREPLY=($(compgen -W "apiToken teamId" -- "$cur"))
|
|
1242
|
+
;;
|
|
1243
|
+
esac
|
|
1244
|
+
fi
|
|
1245
|
+
;;
|
|
1246
|
+
completion)
|
|
1247
|
+
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
|
|
1248
|
+
;;
|
|
1249
|
+
esac
|
|
1250
|
+
}
|
|
1251
|
+
complete -F _cu_completions cu
|
|
1252
|
+
`;
|
|
1253
|
+
}
|
|
1254
|
+
function zshCompletion() {
|
|
1255
|
+
return `#compdef cu
|
|
1256
|
+
|
|
1257
|
+
_cu() {
|
|
1258
|
+
local -a commands
|
|
1259
|
+
commands=(
|
|
1260
|
+
'init:Set up cu for the first time'
|
|
1261
|
+
'tasks:List tasks assigned to me'
|
|
1262
|
+
'initiatives:List initiatives assigned to me'
|
|
1263
|
+
'task:Get task details'
|
|
1264
|
+
'update:Update a task'
|
|
1265
|
+
'create:Create a new task'
|
|
1266
|
+
'sprint:List my tasks in the current active sprint'
|
|
1267
|
+
'subtasks:List subtasks of a task or initiative'
|
|
1268
|
+
'comment:Post a comment on a task'
|
|
1269
|
+
'comments:List comments on a task'
|
|
1270
|
+
'lists:List all lists in a space'
|
|
1271
|
+
'spaces:List spaces in your workspace'
|
|
1272
|
+
'inbox:Recently updated tasks grouped by time period'
|
|
1273
|
+
'assigned:Show all tasks assigned to me'
|
|
1274
|
+
'open:Open a task in the browser by ID or name'
|
|
1275
|
+
'summary:Daily standup summary'
|
|
1276
|
+
'overdue:List tasks that are past their due date'
|
|
1277
|
+
'assign:Assign or unassign users from a task'
|
|
1278
|
+
'config:Manage CLI configuration'
|
|
1279
|
+
'completion:Output shell completion script'
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
_arguments -C \\
|
|
1283
|
+
'(- *)--help[Show help]' \\
|
|
1284
|
+
'(- *)--version[Show version]' \\
|
|
1285
|
+
'1:command:->command' \\
|
|
1286
|
+
'*::arg:->args'
|
|
1287
|
+
|
|
1288
|
+
case $state in
|
|
1289
|
+
command)
|
|
1290
|
+
_describe 'command' commands
|
|
1291
|
+
;;
|
|
1292
|
+
args)
|
|
1293
|
+
case $words[1] in
|
|
1294
|
+
tasks|initiatives)
|
|
1295
|
+
_arguments \\
|
|
1296
|
+
'--status[Filter by status]:status:(open "in progress" "in review" done closed)' \\
|
|
1297
|
+
'--list[Filter by list ID]:list_id:' \\
|
|
1298
|
+
'--space[Filter by space ID]:space_id:' \\
|
|
1299
|
+
'--name[Filter by name]:query:' \\
|
|
1300
|
+
'--json[Force JSON output]'
|
|
1301
|
+
;;
|
|
1302
|
+
task)
|
|
1303
|
+
_arguments \\
|
|
1304
|
+
'1:task_id:' \\
|
|
1305
|
+
'--json[Force JSON output]'
|
|
1306
|
+
;;
|
|
1307
|
+
update)
|
|
1308
|
+
_arguments \\
|
|
1309
|
+
'1:task_id:' \\
|
|
1310
|
+
'(-n --name)'{-n,--name}'[New task name]:text:' \\
|
|
1311
|
+
'(-d --description)'{-d,--description}'[New description]:text:' \\
|
|
1312
|
+
'(-s --status)'{-s,--status}'[New status]:status:(open "in progress" "in review" done closed)' \\
|
|
1313
|
+
'--priority[Priority level]:priority:(urgent high normal low)' \\
|
|
1314
|
+
'--due-date[Due date]:date:' \\
|
|
1315
|
+
'--assignee[Add assignee]:user_id:'
|
|
1316
|
+
;;
|
|
1317
|
+
create)
|
|
1318
|
+
_arguments \\
|
|
1319
|
+
'(-l --list)'{-l,--list}'[Target list ID]:list_id:' \\
|
|
1320
|
+
'(-n --name)'{-n,--name}'[Task name]:name:' \\
|
|
1321
|
+
'(-d --description)'{-d,--description}'[Task description]:text:' \\
|
|
1322
|
+
'(-p --parent)'{-p,--parent}'[Parent task ID]:task_id:' \\
|
|
1323
|
+
'(-s --status)'{-s,--status}'[Initial status]:status:(open "in progress" "in review" done closed)' \\
|
|
1324
|
+
'--priority[Priority level]:priority:(urgent high normal low)' \\
|
|
1325
|
+
'--due-date[Due date]:date:' \\
|
|
1326
|
+
'--assignee[Assignee user ID]:user_id:' \\
|
|
1327
|
+
'--tags[Comma-separated tag names]:tags:'
|
|
1328
|
+
;;
|
|
1329
|
+
sprint)
|
|
1330
|
+
_arguments \\
|
|
1331
|
+
'--status[Filter by status]:status:(open "in progress" "in review" done closed)' \\
|
|
1332
|
+
'--space[Narrow sprint search to a space]:space:' \\
|
|
1333
|
+
'--json[Force JSON output]'
|
|
1334
|
+
;;
|
|
1335
|
+
subtasks)
|
|
1336
|
+
_arguments \\
|
|
1337
|
+
'1:task_id:' \\
|
|
1338
|
+
'--json[Force JSON output]'
|
|
1339
|
+
;;
|
|
1340
|
+
comment)
|
|
1341
|
+
_arguments \\
|
|
1342
|
+
'1:task_id:' \\
|
|
1343
|
+
'(-m --message)'{-m,--message}'[Comment text]:text:'
|
|
1344
|
+
;;
|
|
1345
|
+
comments)
|
|
1346
|
+
_arguments \\
|
|
1347
|
+
'1:task_id:' \\
|
|
1348
|
+
'--json[Force JSON output]'
|
|
1349
|
+
;;
|
|
1350
|
+
lists)
|
|
1351
|
+
_arguments \\
|
|
1352
|
+
'1:space_id:' \\
|
|
1353
|
+
'--name[Filter by name]:query:' \\
|
|
1354
|
+
'--json[Force JSON output]'
|
|
1355
|
+
;;
|
|
1356
|
+
spaces)
|
|
1357
|
+
_arguments \\
|
|
1358
|
+
'--name[Filter spaces by name]:query:' \\
|
|
1359
|
+
'--my[Show only spaces where I have assigned tasks]' \\
|
|
1360
|
+
'--json[Force JSON output]'
|
|
1361
|
+
;;
|
|
1362
|
+
inbox)
|
|
1363
|
+
_arguments \\
|
|
1364
|
+
'--json[Force JSON output]' \\
|
|
1365
|
+
'--days[Lookback period in days]:days:'
|
|
1366
|
+
;;
|
|
1367
|
+
assigned)
|
|
1368
|
+
_arguments \\
|
|
1369
|
+
'--include-closed[Include done/closed tasks]' \\
|
|
1370
|
+
'--json[Force JSON output]'
|
|
1371
|
+
;;
|
|
1372
|
+
open)
|
|
1373
|
+
_arguments \\
|
|
1374
|
+
'1:query:' \\
|
|
1375
|
+
'--json[Output task JSON instead of opening]'
|
|
1376
|
+
;;
|
|
1377
|
+
summary)
|
|
1378
|
+
_arguments \\
|
|
1379
|
+
'--hours[Completed-tasks lookback in hours]:hours:' \\
|
|
1380
|
+
'--json[Force JSON output]'
|
|
1381
|
+
;;
|
|
1382
|
+
overdue)
|
|
1383
|
+
_arguments \\
|
|
1384
|
+
'--json[Force JSON output]'
|
|
1385
|
+
;;
|
|
1386
|
+
assign)
|
|
1387
|
+
_arguments \\
|
|
1388
|
+
'1:task_id:' \\
|
|
1389
|
+
'--to[Add assignee]:user_id:' \\
|
|
1390
|
+
'--remove[Remove assignee]:user_id:' \\
|
|
1391
|
+
'--json[Force JSON output]'
|
|
1392
|
+
;;
|
|
1393
|
+
config)
|
|
1394
|
+
local -a config_cmds
|
|
1395
|
+
config_cmds=(
|
|
1396
|
+
'get:Print a config value'
|
|
1397
|
+
'set:Set a config value'
|
|
1398
|
+
'path:Print config file path'
|
|
1399
|
+
)
|
|
1400
|
+
_arguments -C \\
|
|
1401
|
+
'1:config command:->config_cmd' \\
|
|
1402
|
+
'*::config_arg:->config_args'
|
|
1403
|
+
case $state in
|
|
1404
|
+
config_cmd)
|
|
1405
|
+
_describe 'config command' config_cmds
|
|
1406
|
+
;;
|
|
1407
|
+
config_args)
|
|
1408
|
+
case $words[1] in
|
|
1409
|
+
get|set)
|
|
1410
|
+
_arguments '1:key:(apiToken teamId)'
|
|
1411
|
+
;;
|
|
1412
|
+
esac
|
|
1413
|
+
;;
|
|
1414
|
+
esac
|
|
1415
|
+
;;
|
|
1416
|
+
completion)
|
|
1417
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
1418
|
+
;;
|
|
1419
|
+
esac
|
|
1420
|
+
;;
|
|
1421
|
+
esac
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
_cu
|
|
1425
|
+
`;
|
|
1426
|
+
}
|
|
1427
|
+
function fishCompletion() {
|
|
1428
|
+
return `# Disable file completions
|
|
1429
|
+
complete -c cu -f
|
|
1430
|
+
|
|
1431
|
+
# Global flags
|
|
1432
|
+
complete -c cu -n __fish_use_subcommand -s h -l help -d 'Show help'
|
|
1433
|
+
complete -c cu -n __fish_use_subcommand -s V -l version -d 'Show version'
|
|
1434
|
+
|
|
1435
|
+
# Commands
|
|
1436
|
+
complete -c cu -n __fish_use_subcommand -a init -d 'Set up cu for the first time'
|
|
1437
|
+
complete -c cu -n __fish_use_subcommand -a tasks -d 'List tasks assigned to me'
|
|
1438
|
+
complete -c cu -n __fish_use_subcommand -a initiatives -d 'List initiatives assigned to me'
|
|
1439
|
+
complete -c cu -n __fish_use_subcommand -a task -d 'Get task details'
|
|
1440
|
+
complete -c cu -n __fish_use_subcommand -a update -d 'Update a task'
|
|
1441
|
+
complete -c cu -n __fish_use_subcommand -a create -d 'Create a new task'
|
|
1442
|
+
complete -c cu -n __fish_use_subcommand -a sprint -d 'List my tasks in the current active sprint'
|
|
1443
|
+
complete -c cu -n __fish_use_subcommand -a subtasks -d 'List subtasks of a task or initiative'
|
|
1444
|
+
complete -c cu -n __fish_use_subcommand -a comment -d 'Post a comment on a task'
|
|
1445
|
+
complete -c cu -n __fish_use_subcommand -a comments -d 'List comments on a task'
|
|
1446
|
+
complete -c cu -n __fish_use_subcommand -a lists -d 'List all lists in a space'
|
|
1447
|
+
complete -c cu -n __fish_use_subcommand -a spaces -d 'List spaces in your workspace'
|
|
1448
|
+
complete -c cu -n __fish_use_subcommand -a inbox -d 'Recently updated tasks grouped by time period'
|
|
1449
|
+
complete -c cu -n __fish_use_subcommand -a assigned -d 'Show all tasks assigned to me'
|
|
1450
|
+
complete -c cu -n __fish_use_subcommand -a open -d 'Open a task in the browser by ID or name'
|
|
1451
|
+
complete -c cu -n __fish_use_subcommand -a summary -d 'Daily standup summary'
|
|
1452
|
+
complete -c cu -n __fish_use_subcommand -a overdue -d 'List tasks that are past their due date'
|
|
1453
|
+
complete -c cu -n __fish_use_subcommand -a assign -d 'Assign or unassign users from a task'
|
|
1454
|
+
complete -c cu -n __fish_use_subcommand -a config -d 'Manage CLI configuration'
|
|
1455
|
+
complete -c cu -n __fish_use_subcommand -a completion -d 'Output shell completion script'
|
|
1456
|
+
|
|
1457
|
+
# tasks / initiatives flags
|
|
1458
|
+
complete -c cu -n '__fish_seen_subcommand_from tasks initiatives' -l status -d 'Filter by status'
|
|
1459
|
+
complete -c cu -n '__fish_seen_subcommand_from tasks initiatives' -l list -d 'Filter by list ID'
|
|
1460
|
+
complete -c cu -n '__fish_seen_subcommand_from tasks initiatives' -l space -d 'Filter by space ID'
|
|
1461
|
+
complete -c cu -n '__fish_seen_subcommand_from tasks initiatives' -l name -d 'Filter by name'
|
|
1462
|
+
complete -c cu -n '__fish_seen_subcommand_from tasks initiatives' -l json -d 'Force JSON output'
|
|
1463
|
+
|
|
1464
|
+
# task flags
|
|
1465
|
+
complete -c cu -n '__fish_seen_subcommand_from task' -l json -d 'Force JSON output'
|
|
1466
|
+
|
|
1467
|
+
# update flags
|
|
1468
|
+
complete -c cu -n '__fish_seen_subcommand_from update' -s n -l name -d 'New task name'
|
|
1469
|
+
complete -c cu -n '__fish_seen_subcommand_from update' -s d -l description -d 'New description'
|
|
1470
|
+
complete -c cu -n '__fish_seen_subcommand_from update' -s s -l status -d 'New status'
|
|
1471
|
+
complete -c cu -n '__fish_seen_subcommand_from update' -l priority -d 'Priority level' -a 'urgent high normal low'
|
|
1472
|
+
complete -c cu -n '__fish_seen_subcommand_from update' -l due-date -d 'Due date'
|
|
1473
|
+
complete -c cu -n '__fish_seen_subcommand_from update' -l assignee -d 'Add assignee'
|
|
1474
|
+
|
|
1475
|
+
# create flags
|
|
1476
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -s l -l list -d 'Target list ID'
|
|
1477
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -s n -l name -d 'Task name'
|
|
1478
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -s d -l description -d 'Task description'
|
|
1479
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -s p -l parent -d 'Parent task ID'
|
|
1480
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -s s -l status -d 'Initial status'
|
|
1481
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -l priority -d 'Priority level' -a 'urgent high normal low'
|
|
1482
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -l due-date -d 'Due date'
|
|
1483
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -l assignee -d 'Assignee user ID'
|
|
1484
|
+
complete -c cu -n '__fish_seen_subcommand_from create' -l tags -d 'Comma-separated tag names'
|
|
1485
|
+
|
|
1486
|
+
# sprint flags
|
|
1487
|
+
complete -c cu -n '__fish_seen_subcommand_from sprint' -l status -d 'Filter by status'
|
|
1488
|
+
complete -c cu -n '__fish_seen_subcommand_from sprint' -l space -d 'Narrow sprint search to a space'
|
|
1489
|
+
complete -c cu -n '__fish_seen_subcommand_from sprint' -l json -d 'Force JSON output'
|
|
1490
|
+
|
|
1491
|
+
# subtasks flags
|
|
1492
|
+
complete -c cu -n '__fish_seen_subcommand_from subtasks' -l json -d 'Force JSON output'
|
|
1493
|
+
|
|
1494
|
+
# comment flags
|
|
1495
|
+
complete -c cu -n '__fish_seen_subcommand_from comment' -s m -l message -d 'Comment text'
|
|
1496
|
+
|
|
1497
|
+
# comments flags
|
|
1498
|
+
complete -c cu -n '__fish_seen_subcommand_from comments' -l json -d 'Force JSON output'
|
|
1499
|
+
|
|
1500
|
+
# lists flags
|
|
1501
|
+
complete -c cu -n '__fish_seen_subcommand_from lists' -l name -d 'Filter by name'
|
|
1502
|
+
complete -c cu -n '__fish_seen_subcommand_from lists' -l json -d 'Force JSON output'
|
|
1503
|
+
|
|
1504
|
+
# spaces flags
|
|
1505
|
+
complete -c cu -n '__fish_seen_subcommand_from spaces' -l name -d 'Filter spaces by name'
|
|
1506
|
+
complete -c cu -n '__fish_seen_subcommand_from spaces' -l my -d 'Show only spaces where I have assigned tasks'
|
|
1507
|
+
complete -c cu -n '__fish_seen_subcommand_from spaces' -l json -d 'Force JSON output'
|
|
1508
|
+
|
|
1509
|
+
# inbox flags
|
|
1510
|
+
complete -c cu -n '__fish_seen_subcommand_from inbox' -l json -d 'Force JSON output'
|
|
1511
|
+
complete -c cu -n '__fish_seen_subcommand_from inbox' -l days -d 'Lookback period in days'
|
|
1512
|
+
|
|
1513
|
+
# assigned flags
|
|
1514
|
+
complete -c cu -n '__fish_seen_subcommand_from assigned' -l include-closed -d 'Include done/closed tasks'
|
|
1515
|
+
complete -c cu -n '__fish_seen_subcommand_from assigned' -l json -d 'Force JSON output'
|
|
1516
|
+
|
|
1517
|
+
# open flags
|
|
1518
|
+
complete -c cu -n '__fish_seen_subcommand_from open' -l json -d 'Output task JSON instead of opening'
|
|
1519
|
+
|
|
1520
|
+
# summary flags
|
|
1521
|
+
complete -c cu -n '__fish_seen_subcommand_from summary' -l hours -d 'Completed-tasks lookback in hours'
|
|
1522
|
+
complete -c cu -n '__fish_seen_subcommand_from summary' -l json -d 'Force JSON output'
|
|
1523
|
+
|
|
1524
|
+
# overdue flags
|
|
1525
|
+
complete -c cu -n '__fish_seen_subcommand_from overdue' -l json -d 'Force JSON output'
|
|
1526
|
+
|
|
1527
|
+
# assign flags
|
|
1528
|
+
complete -c cu -n '__fish_seen_subcommand_from assign' -l to -d 'Add assignee'
|
|
1529
|
+
complete -c cu -n '__fish_seen_subcommand_from assign' -l remove -d 'Remove assignee'
|
|
1530
|
+
complete -c cu -n '__fish_seen_subcommand_from assign' -l json -d 'Force JSON output'
|
|
1531
|
+
|
|
1532
|
+
# config subcommands
|
|
1533
|
+
complete -c cu -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a get -d 'Print a config value'
|
|
1534
|
+
complete -c cu -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a set -d 'Set a config value'
|
|
1535
|
+
complete -c cu -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a path -d 'Print config file path'
|
|
1536
|
+
complete -c cu -n '__fish_seen_subcommand_from get set' -a 'apiToken teamId' -d 'Config key'
|
|
1537
|
+
|
|
1538
|
+
# completion subcommand
|
|
1539
|
+
complete -c cu -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell type'
|
|
1540
|
+
`;
|
|
1541
|
+
}
|
|
1542
|
+
function generateCompletion(shell) {
|
|
1543
|
+
switch (shell) {
|
|
1544
|
+
case "bash":
|
|
1545
|
+
return bashCompletion();
|
|
1546
|
+
case "zsh":
|
|
1547
|
+
return zshCompletion();
|
|
1548
|
+
case "fish":
|
|
1549
|
+
return fishCompletion();
|
|
1550
|
+
default:
|
|
1551
|
+
throw new Error(`Unsupported shell: ${shell}. Supported shells: bash, zsh, fish`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/index.ts
|
|
1556
|
+
var require2 = createRequire(import.meta.url);
|
|
1557
|
+
var { version } = require2("../package.json");
|
|
1558
|
+
function wrapAction(fn) {
|
|
1559
|
+
return (...args) => {
|
|
1560
|
+
fn(...args).catch((err) => {
|
|
1561
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1562
|
+
process.exit(1);
|
|
1563
|
+
});
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
var program = new Command();
|
|
1567
|
+
program.name("cu").description("ClickUp CLI for AI agents").version(version).allowExcessArguments(false);
|
|
1568
|
+
program.command("init").description("Set up cu for the first time").action(
|
|
1569
|
+
wrapAction(async () => {
|
|
1570
|
+
await runInitCommand();
|
|
1571
|
+
})
|
|
1572
|
+
);
|
|
1573
|
+
program.command("tasks").description("List tasks assigned to me").option("--status <status>", 'Filter by status (e.g. "in progress")').option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
|
|
1574
|
+
wrapAction(async (opts) => {
|
|
1575
|
+
const config = loadConfig();
|
|
1576
|
+
const tasks = await fetchMyTasks(config, {
|
|
1577
|
+
typeFilter: "task",
|
|
1578
|
+
statuses: opts.status ? [opts.status] : void 0,
|
|
1579
|
+
listIds: opts.list ? [opts.list] : void 0,
|
|
1580
|
+
spaceIds: opts.space ? [opts.space] : void 0,
|
|
1581
|
+
name: opts.name
|
|
1582
|
+
});
|
|
1583
|
+
await printTasks(tasks, opts.json ?? false, config);
|
|
1584
|
+
})
|
|
1585
|
+
);
|
|
1586
|
+
program.command("initiatives").description("List initiatives assigned to me").option("--status <status>", "Filter by status").option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
|
|
1587
|
+
wrapAction(async (opts) => {
|
|
1588
|
+
const config = loadConfig();
|
|
1589
|
+
const tasks = await fetchMyTasks(config, {
|
|
1590
|
+
typeFilter: "initiative",
|
|
1591
|
+
statuses: opts.status ? [opts.status] : void 0,
|
|
1592
|
+
listIds: opts.list ? [opts.list] : void 0,
|
|
1593
|
+
spaceIds: opts.space ? [opts.space] : void 0,
|
|
1594
|
+
name: opts.name
|
|
1595
|
+
});
|
|
1596
|
+
await printTasks(tasks, opts.json ?? false, config);
|
|
1597
|
+
})
|
|
1598
|
+
);
|
|
1599
|
+
program.command("task <taskId>").description("Get task details").option("--json", "Force JSON output even in terminal").action(
|
|
1600
|
+
wrapAction(async (taskId, opts) => {
|
|
1601
|
+
const config = loadConfig();
|
|
1602
|
+
const result = await getTask(config, taskId);
|
|
1603
|
+
if (opts.json || !isTTY()) {
|
|
1604
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1605
|
+
} else {
|
|
1606
|
+
const lines = [
|
|
1607
|
+
`ID: ${result.id}`,
|
|
1608
|
+
`Name: ${result.name}`,
|
|
1609
|
+
`Status: ${result.status?.status ?? "unknown"}`,
|
|
1610
|
+
`Type: ${(result.custom_item_id ?? 0) !== 0 ? "initiative" : "task"}`,
|
|
1611
|
+
`List: ${result.list?.name ?? "unknown"}`,
|
|
1612
|
+
`URL: ${result.url}`,
|
|
1613
|
+
...result.parent ? [`Parent: ${result.parent}`] : [],
|
|
1614
|
+
...result.description ? ["", result.description] : []
|
|
1615
|
+
];
|
|
1616
|
+
console.log(lines.join("\n"));
|
|
1617
|
+
}
|
|
1618
|
+
})
|
|
1619
|
+
);
|
|
1620
|
+
program.command("update <taskId>").description("Update a task").option("-n, --name <text>", "New task name").option("-d, --description <text>", "New description (markdown supported)").option("-s, --status <status>", 'New status (e.g. "in progress", "done")').option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Add assignee by user ID").action(
|
|
1621
|
+
wrapAction(async (taskId, opts) => {
|
|
1622
|
+
const config = loadConfig();
|
|
1623
|
+
const payload = buildUpdatePayload(opts);
|
|
1624
|
+
const result = await updateTask(config, taskId, payload);
|
|
1625
|
+
if (isTTY()) {
|
|
1626
|
+
console.log(`Updated task ${result.id}: "${result.name}"`);
|
|
1627
|
+
} else {
|
|
1628
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1629
|
+
}
|
|
1630
|
+
})
|
|
1631
|
+
);
|
|
1632
|
+
program.command("create").description("Create a new task").option("-l, --list <listId>", "Target list ID (auto-detected from --parent if omitted)").requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Assignee user ID").option("--tags <tags>", "Comma-separated tag names").action(
|
|
1633
|
+
wrapAction(async (opts) => {
|
|
1634
|
+
const config = loadConfig();
|
|
1635
|
+
const result = await createTask(config, opts);
|
|
1636
|
+
if (isTTY()) {
|
|
1637
|
+
console.log(`Created task ${result.id}: "${result.name}" - ${result.url}`);
|
|
1638
|
+
} else {
|
|
1639
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1640
|
+
}
|
|
1641
|
+
})
|
|
1642
|
+
);
|
|
1643
|
+
program.command("sprint").description("List my tasks in the current active sprint (auto-detected)").option("--status <status>", "Filter by status").option("--space <nameOrId>", "Narrow sprint search to a specific space (partial name or ID)").option("--json", "Force JSON output even in terminal").action(
|
|
1644
|
+
wrapAction(async (opts) => {
|
|
1645
|
+
const config = loadConfig();
|
|
1646
|
+
await runSprintCommand(config, opts);
|
|
1647
|
+
})
|
|
1648
|
+
);
|
|
1649
|
+
program.command("subtasks <taskId>").description("List subtasks of a task or initiative").option("--json", "Force JSON output even in terminal").action(
|
|
1650
|
+
wrapAction(async (taskId, opts) => {
|
|
1651
|
+
const config = loadConfig();
|
|
1652
|
+
const tasks = await fetchSubtasks(config, taskId);
|
|
1653
|
+
await printTasks(tasks, opts.json ?? false, config);
|
|
1654
|
+
})
|
|
1655
|
+
);
|
|
1656
|
+
program.command("comment <taskId>").description("Post a comment on a task").requiredOption("-m, --message <text>", "Comment text").action(
|
|
1657
|
+
wrapAction(async (taskId, opts) => {
|
|
1658
|
+
const config = loadConfig();
|
|
1659
|
+
const result = await postComment(config, taskId, opts.message);
|
|
1660
|
+
if (isTTY()) {
|
|
1661
|
+
console.log(`Comment posted (id: ${result.id})`);
|
|
1662
|
+
} else {
|
|
1663
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1664
|
+
}
|
|
1665
|
+
})
|
|
1666
|
+
);
|
|
1667
|
+
program.command("comments <taskId>").description("List comments on a task").option("--json", "Force JSON output even in terminal").action(
|
|
1668
|
+
wrapAction(async (taskId, opts) => {
|
|
1669
|
+
const config = loadConfig();
|
|
1670
|
+
const comments = await fetchComments(config, taskId);
|
|
1671
|
+
printComments(comments, opts.json ?? false);
|
|
1672
|
+
})
|
|
1673
|
+
);
|
|
1674
|
+
program.command("lists <spaceId>").description("List all lists in a space (including lists inside folders)").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
|
|
1675
|
+
wrapAction(async (spaceId, opts) => {
|
|
1676
|
+
const config = loadConfig();
|
|
1677
|
+
const lists = await fetchLists(config, spaceId, { name: opts.name });
|
|
1678
|
+
printLists(lists, opts.json ?? false);
|
|
1679
|
+
})
|
|
1680
|
+
);
|
|
1681
|
+
program.command("spaces").description("List spaces in your workspace").option("--name <partial>", "Filter spaces by name (case-insensitive contains)").option("--my", "Show only spaces where I have assigned tasks").option("--json", "Force JSON output even in terminal").action(
|
|
1682
|
+
wrapAction(async (opts) => {
|
|
1683
|
+
const config = loadConfig();
|
|
1684
|
+
await listSpaces(config, opts);
|
|
1685
|
+
})
|
|
1686
|
+
);
|
|
1687
|
+
program.command("inbox").description("Recently updated tasks grouped by time period").option("--json", "Force JSON output even in terminal").option("--days <n>", "Lookback period in days", "30").action(
|
|
1688
|
+
wrapAction(async (opts) => {
|
|
1689
|
+
const config = loadConfig();
|
|
1690
|
+
const days = Number(opts.days ?? 30);
|
|
1691
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
1692
|
+
console.error("Error: --days must be a positive number");
|
|
1693
|
+
process.exit(1);
|
|
1694
|
+
}
|
|
1695
|
+
const tasks = await fetchInbox(config, days);
|
|
1696
|
+
await printInbox(tasks, opts.json ?? false, config);
|
|
1697
|
+
})
|
|
1698
|
+
);
|
|
1699
|
+
program.command("assigned").description("Show all tasks assigned to me, grouped by status").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
|
|
1700
|
+
wrapAction(async (opts) => {
|
|
1701
|
+
const config = loadConfig();
|
|
1702
|
+
await runAssignedCommand(config, opts);
|
|
1703
|
+
})
|
|
1704
|
+
);
|
|
1705
|
+
program.command("open <query>").description("Open a task in the browser by ID or name").option("--json", "Output task JSON instead of opening").action(
|
|
1706
|
+
wrapAction(async (query, opts) => {
|
|
1707
|
+
const config = loadConfig();
|
|
1708
|
+
await openTask(config, query, opts);
|
|
1709
|
+
})
|
|
1710
|
+
);
|
|
1711
|
+
program.command("summary").description("Daily standup summary: completed, in-progress, overdue").option("--hours <n>", "Completed-tasks lookback in hours", "24").option("--json", "Force JSON output even in terminal").action(
|
|
1712
|
+
wrapAction(async (opts) => {
|
|
1713
|
+
const config = loadConfig();
|
|
1714
|
+
const hours = Number(opts.hours ?? 24);
|
|
1715
|
+
if (!Number.isFinite(hours) || hours <= 0) {
|
|
1716
|
+
console.error("Error: --hours must be a positive number");
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
await runSummaryCommand(config, { hours, json: opts.json ?? false });
|
|
1720
|
+
})
|
|
1721
|
+
);
|
|
1722
|
+
program.command("overdue").description("List tasks that are past their due date").option("--json", "Force JSON output even in terminal").action(
|
|
1723
|
+
wrapAction(async (opts) => {
|
|
1724
|
+
const config = loadConfig();
|
|
1725
|
+
const tasks = await fetchOverdueTasks(config);
|
|
1726
|
+
await printTasks(tasks, opts.json ?? false, config);
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1729
|
+
program.command("assign <taskId>").description("Assign or unassign users from a task").option("--to <userId>", 'Add assignee (user ID or "me")').option("--remove <userId>", 'Remove assignee (user ID or "me")').option("--json", "Force JSON output even in terminal").action(
|
|
1730
|
+
wrapAction(async (taskId, opts) => {
|
|
1731
|
+
const config = loadConfig();
|
|
1732
|
+
const result = await assignTask(config, taskId, opts);
|
|
1733
|
+
if (opts.json || !isTTY()) {
|
|
1734
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1735
|
+
} else {
|
|
1736
|
+
const parts = [];
|
|
1737
|
+
if (opts.to) parts.push(`Assigned ${opts.to} to task ${taskId}`);
|
|
1738
|
+
if (opts.remove) parts.push(`Removed ${opts.remove} from task ${taskId}`);
|
|
1739
|
+
console.log(parts.join("; "));
|
|
1740
|
+
}
|
|
1741
|
+
})
|
|
1742
|
+
);
|
|
1743
|
+
var configCmd = program.command("config").description("Manage CLI configuration");
|
|
1744
|
+
configCmd.command("get <key>").description("Print a config value").action(
|
|
1745
|
+
wrapAction(async (key) => {
|
|
1746
|
+
const value = getConfigValue(key);
|
|
1747
|
+
if (value !== void 0) {
|
|
1748
|
+
console.log(value);
|
|
1749
|
+
}
|
|
1750
|
+
})
|
|
1751
|
+
);
|
|
1752
|
+
configCmd.command("set <key> <value>").description("Set a config value").action(
|
|
1753
|
+
wrapAction(async (key, value) => {
|
|
1754
|
+
setConfigValue(key, value);
|
|
1755
|
+
})
|
|
1756
|
+
);
|
|
1757
|
+
configCmd.command("path").description("Print config file path").action(
|
|
1758
|
+
wrapAction(async () => {
|
|
1759
|
+
console.log(configPath2());
|
|
1760
|
+
})
|
|
1761
|
+
);
|
|
1762
|
+
program.command("completion <shell>").description("Output shell completion script (bash, zsh, fish)").action(
|
|
1763
|
+
wrapAction(async (shell) => {
|
|
1764
|
+
const script = generateCompletion(shell);
|
|
1765
|
+
process.stdout.write(script);
|
|
1766
|
+
})
|
|
1767
|
+
);
|
|
1768
|
+
process.on("SIGINT", () => {
|
|
1769
|
+
process.stderr.write("\nInterrupted\n");
|
|
1770
|
+
process.exit(130);
|
|
1771
|
+
});
|
|
1772
|
+
program.parse();
|