clickup-agent-cli 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +5 -5
- package/dist/clickup.js +936 -241
- package/package.json +13 -4
- package/skills/clickup/SKILL.md +38 -3
- package/skills/clickup-fields/SKILL.md +13 -2
- package/skills/clickup-rollout/SKILL.md +111 -0
- package/skills/clickup-spaces/SKILL.md +3 -0
- package/skills/clickup-tasks/SKILL.md +15 -2
- package/skills/clickup-templates/SKILL.md +76 -0
- package/skills/clickup-time/SKILL.md +3 -2
package/dist/clickup.js
CHANGED
|
@@ -31,23 +31,24 @@ function parseApiError(responseBody, status) {
|
|
|
31
31
|
}
|
|
32
32
|
function mapToExitCode(error) {
|
|
33
33
|
if (!(error instanceof ClickUpError)) {
|
|
34
|
-
return
|
|
34
|
+
return EXIT_CODES.GENERAL_ERROR;
|
|
35
35
|
}
|
|
36
36
|
switch (error.status) {
|
|
37
|
+
case 0:
|
|
38
|
+
return EXIT_CODES.NETWORK_ERROR;
|
|
37
39
|
case 401:
|
|
38
|
-
return
|
|
40
|
+
return EXIT_CODES.AUTH_FAILURE;
|
|
39
41
|
case 403:
|
|
40
|
-
return
|
|
42
|
+
return EXIT_CODES.PERMISSION_DENIED;
|
|
41
43
|
case 404:
|
|
42
|
-
return
|
|
44
|
+
return EXIT_CODES.NOT_FOUND;
|
|
43
45
|
case 429:
|
|
44
|
-
return
|
|
46
|
+
return EXIT_CODES.RATE_LIMITED;
|
|
45
47
|
default:
|
|
46
|
-
|
|
47
|
-
return 1;
|
|
48
|
+
return EXIT_CODES.GENERAL_ERROR;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES;
|
|
51
|
+
var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES, DryRunComplete;
|
|
51
52
|
var init_errors = __esm({
|
|
52
53
|
"src/errors.ts"() {
|
|
53
54
|
"use strict";
|
|
@@ -85,6 +86,12 @@ var init_errors = __esm({
|
|
|
85
86
|
RATE_LIMITED: 6,
|
|
86
87
|
NETWORK_ERROR: 7
|
|
87
88
|
};
|
|
89
|
+
DryRunComplete = class extends Error {
|
|
90
|
+
constructor() {
|
|
91
|
+
super("Dry run complete");
|
|
92
|
+
this.name = "DryRunComplete";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
88
95
|
}
|
|
89
96
|
});
|
|
90
97
|
|
|
@@ -140,6 +147,41 @@ var init_client = __esm({
|
|
|
140
147
|
async delete(path, body) {
|
|
141
148
|
return this.request("DELETE", path, body);
|
|
142
149
|
}
|
|
150
|
+
async downloadUrl(url) {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
153
|
+
const headers = {};
|
|
154
|
+
try {
|
|
155
|
+
const { hostname } = new URL(url);
|
|
156
|
+
if (hostname === "clickup.com" || hostname.endsWith(".clickup.com")) {
|
|
157
|
+
headers["Authorization"] = this.token;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
throw new ClickUpError(`Invalid download URL: ${url}`, 0, void 0, void 0);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(url, {
|
|
165
|
+
headers,
|
|
166
|
+
signal: controller.signal
|
|
167
|
+
});
|
|
168
|
+
clearTimeout(timeoutId);
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const body = await this.safeJson(response);
|
|
171
|
+
throw parseApiError(body, response.status);
|
|
172
|
+
}
|
|
173
|
+
return response.arrayBuffer();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
clearTimeout(timeoutId);
|
|
176
|
+
if (error instanceof ClickUpError) throw error;
|
|
177
|
+
throw new ClickUpError(
|
|
178
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
179
|
+
0,
|
|
180
|
+
void 0,
|
|
181
|
+
void 0
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
143
185
|
async upload(path, filePath, filename) {
|
|
144
186
|
const fileBuffer = readFileSync(filePath);
|
|
145
187
|
const resolvedFilename = filename ?? basename(filePath);
|
|
@@ -148,7 +190,7 @@ var init_client = __esm({
|
|
|
148
190
|
const url = `${this.baseUrl}${path}`;
|
|
149
191
|
if (this.dryRun) {
|
|
150
192
|
this.logDryRun("POST", url, "[multipart form data]");
|
|
151
|
-
|
|
193
|
+
throw new DryRunComplete();
|
|
152
194
|
}
|
|
153
195
|
const controller = new AbortController();
|
|
154
196
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -193,7 +235,7 @@ var init_client = __esm({
|
|
|
193
235
|
}
|
|
194
236
|
if (this.dryRun) {
|
|
195
237
|
this.logDryRun(method, url, body);
|
|
196
|
-
|
|
238
|
+
throw new DryRunComplete();
|
|
197
239
|
}
|
|
198
240
|
let lastError;
|
|
199
241
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
@@ -240,17 +282,6 @@ Headers: ${JSON.stringify(redactedHeaders)}
|
|
|
240
282
|
process.stderr.write(`[${method}] ${path} ${response.status} ${response.statusText} (${elapsed}ms)
|
|
241
283
|
`);
|
|
242
284
|
}
|
|
243
|
-
const remaining = response.headers.get("X-RateLimit-Remaining");
|
|
244
|
-
const reset = response.headers.get("X-RateLimit-Reset");
|
|
245
|
-
if (remaining === "0" && reset) {
|
|
246
|
-
const resetTime = parseInt(reset, 10) * 1e3;
|
|
247
|
-
const waitMs = Math.max(0, resetTime - Date.now());
|
|
248
|
-
if (waitMs > 0) {
|
|
249
|
-
process.stderr.write(`Rate limited. Waiting ${Math.ceil(waitMs / 1e3)}s...
|
|
250
|
-
`);
|
|
251
|
-
await this.sleep(waitMs);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
285
|
if (!response.ok) {
|
|
255
286
|
const responseBody = await this.safeJson(response);
|
|
256
287
|
const error = parseApiError(responseBody, response.status);
|
|
@@ -262,6 +293,18 @@ Headers: ${JSON.stringify(redactedHeaders)}
|
|
|
262
293
|
throw error;
|
|
263
294
|
}
|
|
264
295
|
if (RETRYABLE_STATUSES.has(response.status) && attempt < this.maxRetries) {
|
|
296
|
+
if (response.status === 429) {
|
|
297
|
+
const reset = response.headers.get("X-RateLimit-Reset");
|
|
298
|
+
if (reset) {
|
|
299
|
+
const resetTime = parseInt(reset, 10) * 1e3;
|
|
300
|
+
const waitMs = Math.min(Math.max(0, resetTime - Date.now()), 6e4);
|
|
301
|
+
if (waitMs > 0) {
|
|
302
|
+
process.stderr.write(`Rate limited. Waiting ${Math.ceil(waitMs / 1e3)}s...
|
|
303
|
+
`);
|
|
304
|
+
await this.sleep(waitMs);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
265
308
|
lastError = error;
|
|
266
309
|
continue;
|
|
267
310
|
}
|
|
@@ -321,61 +364,152 @@ Headers: ${JSON.stringify(redactedHeaders)}
|
|
|
321
364
|
}
|
|
322
365
|
}
|
|
323
366
|
sleep(ms) {
|
|
324
|
-
return new Promise((
|
|
367
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
325
368
|
}
|
|
326
369
|
};
|
|
327
370
|
}
|
|
328
371
|
});
|
|
329
372
|
|
|
330
373
|
// src/config.ts
|
|
374
|
+
import { readFileSync as readFileSync2, chmodSync, existsSync } from "fs";
|
|
331
375
|
import Conf from "conf";
|
|
332
376
|
function isValidConfigKey(key) {
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
377
|
+
return ALL_CONFIG_KEYS.includes(key);
|
|
378
|
+
}
|
|
379
|
+
function hardenConfigFile() {
|
|
380
|
+
try {
|
|
381
|
+
if (existsSync(config.path)) chmodSync(config.path, 384);
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function setProfileOverride(nameOrKey) {
|
|
386
|
+
_profileOverride = nameOrKey ? findProfileKey(nameOrKey) : void 0;
|
|
387
|
+
}
|
|
388
|
+
function getActiveProfileKey() {
|
|
389
|
+
if (_profileOverride) return _profileOverride;
|
|
390
|
+
const active = config.get("active_profile");
|
|
391
|
+
return active ?? "default";
|
|
392
|
+
}
|
|
393
|
+
function getProfiles() {
|
|
394
|
+
const stored = config.get("profiles");
|
|
395
|
+
return stored ?? {};
|
|
396
|
+
}
|
|
397
|
+
function getProfile(key) {
|
|
398
|
+
return getProfiles()[key];
|
|
399
|
+
}
|
|
400
|
+
function setProfile(key, profile) {
|
|
401
|
+
const profiles = getProfiles();
|
|
402
|
+
profiles[key] = profile;
|
|
403
|
+
config.set("profiles", profiles);
|
|
404
|
+
hardenConfigFile();
|
|
405
|
+
}
|
|
406
|
+
function deleteProfile(key) {
|
|
407
|
+
const profiles = getProfiles();
|
|
408
|
+
delete profiles[key];
|
|
409
|
+
config.set("profiles", profiles);
|
|
410
|
+
}
|
|
411
|
+
function slugifyWorkspaceName(name) {
|
|
412
|
+
return name.toLowerCase().replace(/'/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
413
|
+
}
|
|
414
|
+
function findProfileKey(nameOrKey) {
|
|
415
|
+
const profiles = getProfiles();
|
|
416
|
+
if (profiles[nameOrKey] !== void 0) return nameOrKey;
|
|
417
|
+
const lower = nameOrKey.toLowerCase();
|
|
418
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
419
|
+
if (profile.workspace_name?.toLowerCase() === lower) return key;
|
|
420
|
+
if (profile.nickname?.toLowerCase() === lower) return key;
|
|
421
|
+
}
|
|
422
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
423
|
+
if (profile.workspace_name?.toLowerCase().includes(lower)) return key;
|
|
424
|
+
if (profile.nickname?.toLowerCase().includes(lower)) return key;
|
|
425
|
+
}
|
|
426
|
+
return nameOrKey;
|
|
427
|
+
}
|
|
428
|
+
function migrateConfig() {
|
|
429
|
+
const legacyToken = config.get("token");
|
|
430
|
+
const legacyWorkspaceId = config.get("workspace_id");
|
|
431
|
+
if (!legacyToken && !legacyWorkspaceId) return;
|
|
432
|
+
const profiles = getProfiles();
|
|
433
|
+
if (!profiles["default"]) {
|
|
434
|
+
const defaultProfile = {};
|
|
435
|
+
if (legacyToken) defaultProfile.token = legacyToken;
|
|
436
|
+
if (legacyWorkspaceId) defaultProfile.workspace_id = legacyWorkspaceId;
|
|
437
|
+
setProfile("default", defaultProfile);
|
|
438
|
+
config.delete("token");
|
|
439
|
+
config.delete("workspace_id");
|
|
440
|
+
}
|
|
342
441
|
}
|
|
343
|
-
function resolveToken(flagValue) {
|
|
344
|
-
|
|
442
|
+
function resolveToken(flagValue, tokenFilePath) {
|
|
443
|
+
if (flagValue) return flagValue;
|
|
444
|
+
if (tokenFilePath) {
|
|
445
|
+
let content;
|
|
446
|
+
try {
|
|
447
|
+
content = readFileSync2(tokenFilePath, "utf8").trim();
|
|
448
|
+
} catch (error) {
|
|
449
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
450
|
+
throw new Error(`Cannot read token file "${tokenFilePath}": ${reason}`);
|
|
451
|
+
}
|
|
452
|
+
if (!content) {
|
|
453
|
+
throw new Error(`Token file "${tokenFilePath}" is empty`);
|
|
454
|
+
}
|
|
455
|
+
return content;
|
|
456
|
+
}
|
|
457
|
+
const envVal = process.env["CLICKUP_API_TOKEN"];
|
|
458
|
+
if (envVal) return envVal;
|
|
459
|
+
const profileKey = getActiveProfileKey();
|
|
460
|
+
const profiles = getProfiles();
|
|
461
|
+
const activeProfile = profiles[profileKey];
|
|
462
|
+
if (activeProfile?.token) return activeProfile.token;
|
|
463
|
+
const legacyToken = config.get("token");
|
|
464
|
+
if (legacyToken) return legacyToken;
|
|
465
|
+
return void 0;
|
|
345
466
|
}
|
|
346
467
|
function resolveWorkspaceId(flagValue) {
|
|
347
|
-
|
|
468
|
+
if (flagValue) return flagValue;
|
|
469
|
+
const envVal = process.env["CLICKUP_WORKSPACE_ID"];
|
|
470
|
+
if (envVal) return envVal;
|
|
471
|
+
const profileKey = getActiveProfileKey();
|
|
472
|
+
const profiles = getProfiles();
|
|
473
|
+
const activeProfile = profiles[profileKey];
|
|
474
|
+
if (activeProfile?.workspace_id) return activeProfile.workspace_id;
|
|
475
|
+
const withWorkspace = Object.values(profiles).filter((p) => p.workspace_id);
|
|
476
|
+
if (withWorkspace.length === 1) return withWorkspace[0].workspace_id;
|
|
477
|
+
const legacyId = config.get("workspace_id");
|
|
478
|
+
if (legacyId) return legacyId;
|
|
479
|
+
return void 0;
|
|
348
480
|
}
|
|
349
481
|
function resolveOutputFormat(flagValue) {
|
|
350
|
-
|
|
351
|
-
|
|
482
|
+
if (flagValue) return flagValue;
|
|
483
|
+
const envVal = process.env["CLICKUP_OUTPUT_FORMAT"];
|
|
484
|
+
if (envVal) return envVal;
|
|
485
|
+
const stored = config.get("output_format");
|
|
486
|
+
if (stored) return stored;
|
|
352
487
|
return process.stdout.isTTY ? "table" : "json";
|
|
353
488
|
}
|
|
354
|
-
var
|
|
489
|
+
var ALL_CONFIG_KEYS, VALID_CONFIG_KEYS, config, _profileOverride;
|
|
355
490
|
var init_config = __esm({
|
|
356
491
|
"src/config.ts"() {
|
|
357
492
|
"use strict";
|
|
358
|
-
|
|
359
|
-
VALID_CONFIG_KEYS =
|
|
493
|
+
ALL_CONFIG_KEYS = ["token", "workspace_id", "output_format", "color", "page_size", "timezone", "active_profile"];
|
|
494
|
+
VALID_CONFIG_KEYS = ALL_CONFIG_KEYS;
|
|
360
495
|
config = new Conf({
|
|
361
496
|
projectName: "clickup-cli",
|
|
362
497
|
schema: {
|
|
363
498
|
token: { type: "string" },
|
|
364
499
|
workspace_id: { type: "string" },
|
|
365
|
-
output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id"] },
|
|
500
|
+
output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id", "md"] },
|
|
366
501
|
color: { type: "boolean", default: true },
|
|
367
502
|
page_size: { type: "number", default: 100 },
|
|
368
|
-
timezone: { type: "string" }
|
|
503
|
+
timezone: { type: "string" },
|
|
504
|
+
active_profile: { type: "string" },
|
|
505
|
+
profiles: { type: "object" }
|
|
369
506
|
}
|
|
370
507
|
});
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
page_size: "CLICKUP_PAGE_SIZE",
|
|
377
|
-
timezone: "CLICKUP_TIMEZONE"
|
|
378
|
-
};
|
|
508
|
+
hardenConfigFile();
|
|
509
|
+
try {
|
|
510
|
+
migrateConfig();
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
379
513
|
}
|
|
380
514
|
});
|
|
381
515
|
|
|
@@ -387,22 +521,29 @@ __export(auth_exports, {
|
|
|
387
521
|
oauthLogin: () => oauthLogin
|
|
388
522
|
});
|
|
389
523
|
import { createServer } from "http";
|
|
390
|
-
import { randomBytes, createHash } from "crypto";
|
|
524
|
+
import { randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
391
525
|
function generateCodeVerifier() {
|
|
392
526
|
return randomBytes(48).toString("base64url");
|
|
393
527
|
}
|
|
394
528
|
function generateCodeChallenge(verifier) {
|
|
395
529
|
return createHash("sha256").update(verifier).digest("base64url");
|
|
396
530
|
}
|
|
397
|
-
function buildAuthorizeUrl(clientId, codeChallenge) {
|
|
531
|
+
function buildAuthorizeUrl(clientId, codeChallenge, state) {
|
|
398
532
|
const params = new URLSearchParams({
|
|
399
533
|
client_id: clientId,
|
|
400
534
|
redirect_uri: REDIRECT_URI,
|
|
401
535
|
code_challenge: codeChallenge,
|
|
402
|
-
code_challenge_method: "S256"
|
|
536
|
+
code_challenge_method: "S256",
|
|
537
|
+
state
|
|
403
538
|
});
|
|
404
539
|
return `${CLICKUP_AUTHORIZE_URL}?${params.toString()}`;
|
|
405
540
|
}
|
|
541
|
+
function statesMatch(expected, received) {
|
|
542
|
+
const a = Buffer.from(expected);
|
|
543
|
+
const b = Buffer.from(received);
|
|
544
|
+
if (a.length !== b.length) return false;
|
|
545
|
+
return timingSafeEqual(a, b);
|
|
546
|
+
}
|
|
406
547
|
async function exchangeCodeForToken(code, clientId, clientSecret, codeVerifier) {
|
|
407
548
|
const response = await fetch(CLICKUP_TOKEN_URL, {
|
|
408
549
|
method: "POST",
|
|
@@ -425,8 +566,8 @@ async function exchangeCodeForToken(code, clientId, clientSecret, codeVerifier)
|
|
|
425
566
|
}
|
|
426
567
|
return token;
|
|
427
568
|
}
|
|
428
|
-
function waitForCallback(server) {
|
|
429
|
-
return new Promise((
|
|
569
|
+
function waitForCallback(server, expectedState) {
|
|
570
|
+
return new Promise((resolve2, reject) => {
|
|
430
571
|
const timeout = setTimeout(() => {
|
|
431
572
|
server.close();
|
|
432
573
|
reject(new Error("OAuth callback timed out after 120 seconds"));
|
|
@@ -440,6 +581,7 @@ function waitForCallback(server) {
|
|
|
440
581
|
}
|
|
441
582
|
const code = url.searchParams.get("code");
|
|
442
583
|
const error = url.searchParams.get("error");
|
|
584
|
+
const state = url.searchParams.get("state");
|
|
443
585
|
if (error) {
|
|
444
586
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
445
587
|
res.end("<html><body><h1>Authentication failed</h1><p>You can close this tab.</p></body></html>");
|
|
@@ -448,6 +590,11 @@ function waitForCallback(server) {
|
|
|
448
590
|
reject(new Error(`OAuth error: ${error}`));
|
|
449
591
|
return;
|
|
450
592
|
}
|
|
593
|
+
if (!state || !statesMatch(expectedState, state)) {
|
|
594
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
595
|
+
res.end("<html><body><h1>Invalid request</h1><p>State mismatch. Please retry the login.</p></body></html>");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
451
598
|
if (!code) {
|
|
452
599
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
453
600
|
res.end("<html><body><h1>Missing code</h1><p>No authorization code received.</p></body></html>");
|
|
@@ -457,24 +604,28 @@ function waitForCallback(server) {
|
|
|
457
604
|
res.end("<html><body><h1>Authenticated</h1><p>You can close this tab and return to the terminal.</p></body></html>");
|
|
458
605
|
clearTimeout(timeout);
|
|
459
606
|
server.close();
|
|
460
|
-
|
|
607
|
+
resolve2(code);
|
|
461
608
|
});
|
|
462
609
|
});
|
|
463
610
|
}
|
|
464
611
|
async function openBrowser(url) {
|
|
465
612
|
const { platform } = process;
|
|
613
|
+
const { execFile } = await import("child_process");
|
|
466
614
|
let command;
|
|
615
|
+
let args;
|
|
467
616
|
if (platform === "darwin") {
|
|
468
617
|
command = "open";
|
|
618
|
+
args = [url];
|
|
469
619
|
} else if (platform === "win32") {
|
|
470
|
-
command = "
|
|
620
|
+
command = "cmd";
|
|
621
|
+
args = ["/c", "start", "", url];
|
|
471
622
|
} else {
|
|
472
623
|
command = "xdg-open";
|
|
624
|
+
args = [url];
|
|
473
625
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
resolve();
|
|
626
|
+
return new Promise((resolve2) => {
|
|
627
|
+
execFile(command, args, () => {
|
|
628
|
+
resolve2();
|
|
478
629
|
});
|
|
479
630
|
});
|
|
480
631
|
}
|
|
@@ -493,22 +644,24 @@ async function oauthLogin() {
|
|
|
493
644
|
}
|
|
494
645
|
const codeVerifier = generateCodeVerifier();
|
|
495
646
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
647
|
+
const state = randomBytes(24).toString("base64url");
|
|
496
648
|
const server = createServer();
|
|
497
|
-
await new Promise((
|
|
498
|
-
server.listen(CALLBACK_PORT, () =>
|
|
649
|
+
await new Promise((resolve2, reject) => {
|
|
650
|
+
server.listen(CALLBACK_PORT, "127.0.0.1", () => resolve2());
|
|
499
651
|
server.on("error", reject);
|
|
500
652
|
});
|
|
501
|
-
const authorizeUrl = buildAuthorizeUrl(clientId, codeChallenge);
|
|
653
|
+
const authorizeUrl = buildAuthorizeUrl(clientId, codeChallenge, state);
|
|
502
654
|
process.stderr.write(`Opening browser for authentication...
|
|
503
655
|
`);
|
|
504
656
|
process.stderr.write(`If the browser does not open, visit:
|
|
505
657
|
${authorizeUrl}
|
|
506
658
|
`);
|
|
507
659
|
await openBrowser(authorizeUrl);
|
|
508
|
-
const code = await waitForCallback(server);
|
|
660
|
+
const code = await waitForCallback(server, state);
|
|
509
661
|
process.stderr.write("Exchanging code for token...\n");
|
|
510
662
|
const token = await exchangeCodeForToken(code, clientId, clientSecret, codeVerifier);
|
|
511
|
-
|
|
663
|
+
const profileKey = getActiveProfileKey();
|
|
664
|
+
setProfile(profileKey, { ...getProfile(profileKey), token });
|
|
512
665
|
return token;
|
|
513
666
|
}
|
|
514
667
|
var CLICKUP_AUTHORIZE_URL, CLICKUP_TOKEN_URL, CALLBACK_PORT, CALLBACK_PATH, REDIRECT_URI;
|
|
@@ -528,13 +681,14 @@ var init_auth = __esm({
|
|
|
528
681
|
init_client();
|
|
529
682
|
init_errors();
|
|
530
683
|
init_config();
|
|
531
|
-
import { Command } from "commander";
|
|
684
|
+
import { Command, Option } from "commander";
|
|
532
685
|
|
|
533
686
|
// src/commands/auth-cmd.ts
|
|
534
687
|
init_config();
|
|
688
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
535
689
|
function registerAuthCommands(program, getClient) {
|
|
536
690
|
const auth = program.command("auth").description("Manage authentication");
|
|
537
|
-
auth.command("login").description("Authenticate with ClickUp").option("--token <token>", "Personal API token").option("--oauth", "Use OAuth2 PKCE browser flow (requires CLICKUP_CLIENT_ID and CLICKUP_CLIENT_SECRET)").action(async (opts) => {
|
|
691
|
+
auth.command("login").description("Authenticate with ClickUp").option("--token <token>", "Personal API token").option("--token-file <path>", "Read token from this file path").option("--oauth", "Use OAuth2 PKCE browser flow (requires CLICKUP_CLIENT_ID and CLICKUP_CLIENT_SECRET)").action(async (opts) => {
|
|
538
692
|
if (opts.oauth) {
|
|
539
693
|
const { oauthLogin: oauthLogin2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
540
694
|
const token2 = await oauthLogin2();
|
|
@@ -550,9 +704,19 @@ function registerAuthCommands(program, getClient) {
|
|
|
550
704
|
return;
|
|
551
705
|
}
|
|
552
706
|
let token = opts.token;
|
|
707
|
+
if (!token && opts.tokenFile) {
|
|
708
|
+
try {
|
|
709
|
+
token = readFileSync3(opts.tokenFile, "utf8").trim();
|
|
710
|
+
} catch {
|
|
711
|
+
process.stderr.write(`Error: Could not read token file: ${opts.tokenFile}
|
|
712
|
+
`);
|
|
713
|
+
process.exit(2);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
553
717
|
if (!token) {
|
|
554
718
|
if (!process.stdin.isTTY) {
|
|
555
|
-
process.stderr.write("Error: No token provided. Use --token <token
|
|
719
|
+
process.stderr.write("Error: No token provided. Use --token <token>, --token-file <path>, or --oauth in non-interactive mode.\n");
|
|
556
720
|
process.exit(2);
|
|
557
721
|
return;
|
|
558
722
|
}
|
|
@@ -571,15 +735,29 @@ function registerAuthCommands(program, getClient) {
|
|
|
571
735
|
const client = new ClientClass({ token });
|
|
572
736
|
try {
|
|
573
737
|
const userData = await client.get("/user");
|
|
574
|
-
|
|
738
|
+
const profileKey = getActiveProfileKey();
|
|
739
|
+
const profiles = getProfiles();
|
|
740
|
+
const existing = profiles[profileKey] ?? {};
|
|
741
|
+
setProfile(profileKey, { ...existing, token });
|
|
575
742
|
process.stdout.write(`Authenticated as ${userData.user.username} (${userData.user.email})
|
|
576
743
|
`);
|
|
744
|
+
const profile = getProfiles()[profileKey];
|
|
745
|
+
if (!profile?.workspace_id) {
|
|
746
|
+
process.stdout.write("Run: clickup workspace setup -- to configure your workspace.\n");
|
|
747
|
+
}
|
|
577
748
|
} catch {
|
|
578
749
|
process.stderr.write("Error: Invalid token. Authentication failed.\n");
|
|
579
750
|
process.exit(3);
|
|
580
751
|
}
|
|
581
752
|
});
|
|
582
753
|
auth.command("logout").description("Remove stored credentials").action(() => {
|
|
754
|
+
const profileKey = getActiveProfileKey();
|
|
755
|
+
const profiles = getProfiles();
|
|
756
|
+
const existing = profiles[profileKey];
|
|
757
|
+
if (existing) {
|
|
758
|
+
const { token: _removed, ...profileWithoutToken } = existing;
|
|
759
|
+
setProfile(profileKey, profileWithoutToken);
|
|
760
|
+
}
|
|
583
761
|
config.delete("token");
|
|
584
762
|
process.stdout.write("Logged out. Token removed.\n");
|
|
585
763
|
});
|
|
@@ -592,9 +770,17 @@ function registerAuthCommands(program, getClient) {
|
|
|
592
770
|
const prefix = token.slice(0, 8);
|
|
593
771
|
process.stdout.write(`Token: ${prefix}...
|
|
594
772
|
`);
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
773
|
+
const profileKey = getActiveProfileKey();
|
|
774
|
+
process.stdout.write(`Profile: ${profileKey}
|
|
775
|
+
`);
|
|
776
|
+
const profiles = getProfiles();
|
|
777
|
+
const activeProfile = profiles[profileKey];
|
|
778
|
+
if (activeProfile?.workspace_id) {
|
|
779
|
+
process.stdout.write(`Workspace: ${activeProfile.workspace_name ?? activeProfile.workspace_id}
|
|
780
|
+
`);
|
|
781
|
+
} else {
|
|
782
|
+
const legacyId = config.get("workspace_id");
|
|
783
|
+
if (legacyId) process.stdout.write(`Workspace: ${legacyId}
|
|
598
784
|
`);
|
|
599
785
|
}
|
|
600
786
|
try {
|
|
@@ -620,7 +806,7 @@ function registerAuthCommands(program, getClient) {
|
|
|
620
806
|
|
|
621
807
|
// src/commands/config-cmd.ts
|
|
622
808
|
init_config();
|
|
623
|
-
function registerConfigCommands(program) {
|
|
809
|
+
function registerConfigCommands(program, getClient) {
|
|
624
810
|
const configCmd = program.command("config").description("Manage CLI configuration");
|
|
625
811
|
configCmd.command("set").description("Set a configuration value").argument("<key>", "Config key").argument("<value>", "Config value").action((key, value) => {
|
|
626
812
|
if (!isValidConfigKey(key)) {
|
|
@@ -639,10 +825,20 @@ function registerConfigCommands(program) {
|
|
|
639
825
|
return;
|
|
640
826
|
}
|
|
641
827
|
config.set(key, num);
|
|
828
|
+
} else if (key === "output_format") {
|
|
829
|
+
const valid = ["table", "json", "csv", "tsv", "quiet", "id", "md"];
|
|
830
|
+
if (!valid.includes(value)) {
|
|
831
|
+
process.stderr.write(`Error: output_format must be one of: ${valid.join(", ")}
|
|
832
|
+
`);
|
|
833
|
+
process.exit(2);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
config.set(key, value);
|
|
642
837
|
} else {
|
|
643
838
|
config.set(key, value);
|
|
644
839
|
}
|
|
645
|
-
|
|
840
|
+
const shown = key === "token" ? `${value.slice(0, 8)}...` : value;
|
|
841
|
+
process.stdout.write(`Set ${key} = ${shown}
|
|
646
842
|
`);
|
|
647
843
|
});
|
|
648
844
|
configCmd.command("get").description("Get a configuration value").argument("<key>", "Config key").action((key) => {
|
|
@@ -666,7 +862,8 @@ function registerConfigCommands(program) {
|
|
|
666
862
|
for (const key of keys) {
|
|
667
863
|
const value = store[key];
|
|
668
864
|
if (value !== void 0) {
|
|
669
|
-
|
|
865
|
+
const shown = key === "token" ? `${String(value).slice(0, 8)}...` : value;
|
|
866
|
+
process.stdout.write(`${key} = ${shown}
|
|
670
867
|
`);
|
|
671
868
|
}
|
|
672
869
|
}
|
|
@@ -694,6 +891,141 @@ function registerConfigCommands(program) {
|
|
|
694
891
|
configCmd.command("path").description("Print the config file path").action(() => {
|
|
695
892
|
process.stdout.write(config.path + "\n");
|
|
696
893
|
});
|
|
894
|
+
configCmd.command("validate").description("Verify the stored token works and show current identity").action(async () => {
|
|
895
|
+
const client = getClient();
|
|
896
|
+
let user;
|
|
897
|
+
try {
|
|
898
|
+
const userData = await client.get("/user");
|
|
899
|
+
user = userData.user;
|
|
900
|
+
} catch (err) {
|
|
901
|
+
process.stderr.write(`Error: Token validation failed. Run: clickup auth login
|
|
902
|
+
`);
|
|
903
|
+
process.exit(3);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
let workspaceName = "(none)";
|
|
907
|
+
let workspaceId = "(none)";
|
|
908
|
+
try {
|
|
909
|
+
const teamsData = await client.get("/team");
|
|
910
|
+
const activeKey = getActiveProfileKey();
|
|
911
|
+
const profiles = getProfiles();
|
|
912
|
+
const activeProfile = profiles[activeKey];
|
|
913
|
+
if (activeProfile?.workspace_id) {
|
|
914
|
+
const match = teamsData.teams.find((t) => t.id === activeProfile.workspace_id);
|
|
915
|
+
workspaceName = match?.name ?? activeProfile.workspace_name ?? activeProfile.workspace_id;
|
|
916
|
+
workspaceId = activeProfile.workspace_id;
|
|
917
|
+
} else if (teamsData.teams.length > 0) {
|
|
918
|
+
workspaceName = teamsData.teams.map((t) => t.name).join(", ");
|
|
919
|
+
workspaceId = teamsData.teams.map((t) => t.id).join(", ");
|
|
920
|
+
}
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
process.stdout.write(`User: ${user.username} (${user.email})
|
|
924
|
+
`);
|
|
925
|
+
process.stdout.write(`Active profile: ${getActiveProfileKey()}
|
|
926
|
+
`);
|
|
927
|
+
process.stdout.write(`Workspace: ${workspaceName}
|
|
928
|
+
`);
|
|
929
|
+
process.stdout.write(`Workspace ID: ${workspaceId}
|
|
930
|
+
`);
|
|
931
|
+
process.stdout.write(`Token status: Valid
|
|
932
|
+
`);
|
|
933
|
+
});
|
|
934
|
+
const profileCmd = configCmd.command("profile").description("Manage named profiles");
|
|
935
|
+
profileCmd.command("list").description("List all profiles").action(() => {
|
|
936
|
+
const profiles = getProfiles();
|
|
937
|
+
const activeKey = getActiveProfileKey();
|
|
938
|
+
const keys = Object.keys(profiles);
|
|
939
|
+
if (keys.length === 0) {
|
|
940
|
+
process.stdout.write("No profiles configured. Run: clickup auth login\n");
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const col = (s, w) => s.padEnd(w).slice(0, w);
|
|
944
|
+
process.stdout.write(
|
|
945
|
+
`${col("Key", 20)} ${col("Workspace Name", 30)} ${col("Nickname", 15)} ${col("Workspace ID", 16)} Active
|
|
946
|
+
`
|
|
947
|
+
);
|
|
948
|
+
process.stdout.write(`${"-".repeat(20)} ${"-".repeat(30)} ${"-".repeat(15)} ${"-".repeat(16)} ------
|
|
949
|
+
`);
|
|
950
|
+
for (const key of keys) {
|
|
951
|
+
const p = profiles[key];
|
|
952
|
+
const active = key === activeKey ? "*" : "";
|
|
953
|
+
process.stdout.write(
|
|
954
|
+
`${col(key, 20)} ${col(p.workspace_name ?? "", 30)} ${col(p.nickname ?? "", 15)} ${col(p.workspace_id ?? "", 16)} ${active}
|
|
955
|
+
`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
profileCmd.command("create").description("Create an empty profile").argument("<key>", "Profile key").action((key) => {
|
|
960
|
+
const profiles = getProfiles();
|
|
961
|
+
if (profiles[key]) {
|
|
962
|
+
process.stderr.write(`Error: Profile "${key}" already exists.
|
|
963
|
+
`);
|
|
964
|
+
process.exit(2);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
setProfile(key, {});
|
|
968
|
+
process.stdout.write(`Profile "${key}" created.
|
|
969
|
+
`);
|
|
970
|
+
});
|
|
971
|
+
profileCmd.command("delete").description("Delete a profile").argument("<key>", "Profile key").option("--confirm", "Skip confirmation prompt").action(async (key, opts) => {
|
|
972
|
+
const profiles = getProfiles();
|
|
973
|
+
if (!profiles[key]) {
|
|
974
|
+
process.stderr.write(`Error: Profile "${key}" not found.
|
|
975
|
+
`);
|
|
976
|
+
process.exit(2);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const isActive = key === getActiveProfileKey();
|
|
980
|
+
if (!opts.confirm) {
|
|
981
|
+
if (!process.stdin.isTTY) {
|
|
982
|
+
process.stderr.write("Error: Add --confirm to delete a profile in non-interactive mode.\n");
|
|
983
|
+
process.exit(2);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
987
|
+
const answer = await confirm({
|
|
988
|
+
message: isActive ? `"${key}" is the active profile. Delete it?` : `Delete profile "${key}"?`,
|
|
989
|
+
default: false
|
|
990
|
+
});
|
|
991
|
+
if (!answer) {
|
|
992
|
+
process.stdout.write("Cancelled.\n");
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
deleteProfile(key);
|
|
997
|
+
if (isActive) {
|
|
998
|
+
config.delete("active_profile");
|
|
999
|
+
}
|
|
1000
|
+
process.stdout.write(`Profile "${key}" deleted.
|
|
1001
|
+
`);
|
|
1002
|
+
});
|
|
1003
|
+
profileCmd.command("use").description("Switch the active profile").argument("<key>", "Profile key, workspace name, or nickname").action((nameOrKey) => {
|
|
1004
|
+
const profiles = getProfiles();
|
|
1005
|
+
const resolvedKey = findProfileKey(nameOrKey);
|
|
1006
|
+
if (!profiles[resolvedKey]) {
|
|
1007
|
+
process.stderr.write(`Error: Profile "${nameOrKey}" not found.
|
|
1008
|
+
`);
|
|
1009
|
+
process.exit(2);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
config.set("active_profile", resolvedKey);
|
|
1013
|
+
process.stdout.write(`Active profile: ${resolvedKey}
|
|
1014
|
+
`);
|
|
1015
|
+
});
|
|
1016
|
+
profileCmd.command("nickname").description("Set a short nickname for a profile").argument("<key>", "Profile key").argument("<nickname>", "Short nickname").action((key, nickname) => {
|
|
1017
|
+
const profiles = getProfiles();
|
|
1018
|
+
const profile = profiles[key];
|
|
1019
|
+
if (!profile) {
|
|
1020
|
+
process.stderr.write(`Error: Profile "${key}" not found.
|
|
1021
|
+
`);
|
|
1022
|
+
process.exit(2);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
setProfile(key, { ...profile, nickname });
|
|
1026
|
+
process.stdout.write(`Nickname "${nickname}" set for profile "${key}".
|
|
1027
|
+
`);
|
|
1028
|
+
});
|
|
697
1029
|
}
|
|
698
1030
|
|
|
699
1031
|
// src/commands/workspace.ts
|
|
@@ -772,18 +1104,23 @@ function applySort(rows, sort) {
|
|
|
772
1104
|
return desc ? -result : result;
|
|
773
1105
|
});
|
|
774
1106
|
}
|
|
1107
|
+
var CONTROL_CHARS_RE = new RegExp("[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f]", "g");
|
|
1108
|
+
function sanitize(str) {
|
|
1109
|
+
return str.replace(/\r?\n/g, " ").replace(CONTROL_CHARS_RE, "");
|
|
1110
|
+
}
|
|
775
1111
|
function getValue(row, key) {
|
|
776
1112
|
if (row && typeof row === "object") {
|
|
777
1113
|
const record = row;
|
|
778
1114
|
const val = record[key];
|
|
779
1115
|
if (val === null || val === void 0) return "";
|
|
780
|
-
if (typeof val === "object") return JSON.stringify(val);
|
|
781
|
-
return String(val);
|
|
1116
|
+
if (typeof val === "object") return sanitize(JSON.stringify(val));
|
|
1117
|
+
return sanitize(String(val));
|
|
782
1118
|
}
|
|
783
1119
|
return "";
|
|
784
1120
|
}
|
|
785
1121
|
function truncate(str, width) {
|
|
786
1122
|
if (str.length <= width) return str;
|
|
1123
|
+
if (width <= 3) return str.slice(0, Math.max(0, width));
|
|
787
1124
|
return str.slice(0, width - 3) + "...";
|
|
788
1125
|
}
|
|
789
1126
|
function printTable(rows, columns, opts) {
|
|
@@ -813,7 +1150,10 @@ function printDelimited(rows, columns, delimiter, opts) {
|
|
|
813
1150
|
}
|
|
814
1151
|
for (const row of rows) {
|
|
815
1152
|
const values = columns.map((col) => {
|
|
816
|
-
|
|
1153
|
+
let val = getValue(row, col.key);
|
|
1154
|
+
if (delimiter === "," && /^[=+\-@\t\r]/.test(val)) {
|
|
1155
|
+
val = `'${val}`;
|
|
1156
|
+
}
|
|
817
1157
|
if (delimiter === "," && (val.includes(",") || val.includes('"') || val.includes("\n"))) {
|
|
818
1158
|
return `"${val.replace(/"/g, '""')}"`;
|
|
819
1159
|
}
|
|
@@ -869,6 +1209,7 @@ function getFields(resource, action) {
|
|
|
869
1209
|
}
|
|
870
1210
|
|
|
871
1211
|
// src/commands/workspace.ts
|
|
1212
|
+
registerSchema("workspace", "setup", "Configure workspace -- fetches workspaces and saves profile", []);
|
|
872
1213
|
registerSchema("workspace", "list", "List all workspaces accessible to the authenticated user", []);
|
|
873
1214
|
registerSchema("workspace", "get", "Get workspace details", [
|
|
874
1215
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" }
|
|
@@ -912,6 +1253,49 @@ function requireWorkspaceId(program) {
|
|
|
912
1253
|
}
|
|
913
1254
|
function registerWorkspaceCommands(program, getClient) {
|
|
914
1255
|
const workspace = program.command("workspace").description("Manage workspaces");
|
|
1256
|
+
workspace.command("setup").description("Configure workspace for the active profile").action(async () => {
|
|
1257
|
+
const client = getClient();
|
|
1258
|
+
const data = await client.get("/team");
|
|
1259
|
+
const teams = data.teams;
|
|
1260
|
+
if (teams.length === 0) {
|
|
1261
|
+
process.stderr.write("Error: No workspaces found for this token.\n");
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
let selected;
|
|
1266
|
+
if (teams.length === 1) {
|
|
1267
|
+
selected = teams[0];
|
|
1268
|
+
} else {
|
|
1269
|
+
if (!process.stdin.isTTY) {
|
|
1270
|
+
process.stderr.write("Error: Multiple workspaces found. Use --workspace-id or run in interactive mode.\n");
|
|
1271
|
+
process.exit(2);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const { select } = await import("@inquirer/prompts");
|
|
1275
|
+
const selectedId = await select({
|
|
1276
|
+
message: "Select workspace:",
|
|
1277
|
+
choices: teams.map((t) => ({ name: t.name, value: t.id }))
|
|
1278
|
+
});
|
|
1279
|
+
selected = teams.find((t) => t.id === selectedId);
|
|
1280
|
+
}
|
|
1281
|
+
const profileKey = getActiveProfileKey();
|
|
1282
|
+
const profiles = getProfiles();
|
|
1283
|
+
const existing = profiles[profileKey] ?? {};
|
|
1284
|
+
const slugKey = slugifyWorkspaceName(selected.name);
|
|
1285
|
+
setProfile(profileKey, {
|
|
1286
|
+
...existing,
|
|
1287
|
+
workspace_id: selected.id,
|
|
1288
|
+
workspace_name: selected.name
|
|
1289
|
+
});
|
|
1290
|
+
process.stdout.write(
|
|
1291
|
+
`Workspace "${selected.name}" saved as profile "${profileKey}".
|
|
1292
|
+
`
|
|
1293
|
+
);
|
|
1294
|
+
if (slugKey !== profileKey) {
|
|
1295
|
+
process.stdout.write(`Use --profile "${selected.name}" or --profile ${profileKey}
|
|
1296
|
+
`);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
915
1299
|
workspace.command("list").description("List all workspaces").action(async () => {
|
|
916
1300
|
const client = getClient();
|
|
917
1301
|
const data = await client.get("/team");
|
|
@@ -1160,6 +1544,124 @@ function registerFolderCommands(program, getClient) {
|
|
|
1160
1544
|
});
|
|
1161
1545
|
}
|
|
1162
1546
|
|
|
1547
|
+
// src/dates.ts
|
|
1548
|
+
var RELATIVE_OFFSET_RE = /^([+-]\d+)([dwmh])$/;
|
|
1549
|
+
var UNIX_MS_RE = /^\d{13,}$/;
|
|
1550
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1551
|
+
var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
1552
|
+
var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
1553
|
+
function parseDate(input) {
|
|
1554
|
+
const raw = input.trim();
|
|
1555
|
+
const lower = raw.toLowerCase();
|
|
1556
|
+
if (UNIX_MS_RE.test(raw)) {
|
|
1557
|
+
return parseInt(raw, 10);
|
|
1558
|
+
}
|
|
1559
|
+
const now = /* @__PURE__ */ new Date();
|
|
1560
|
+
switch (lower) {
|
|
1561
|
+
case "today": {
|
|
1562
|
+
return startOfDay(now).getTime();
|
|
1563
|
+
}
|
|
1564
|
+
case "tomorrow": {
|
|
1565
|
+
const d = startOfDay(now);
|
|
1566
|
+
d.setDate(d.getDate() + 1);
|
|
1567
|
+
return d.getTime();
|
|
1568
|
+
}
|
|
1569
|
+
case "yesterday": {
|
|
1570
|
+
const d = startOfDay(now);
|
|
1571
|
+
d.setDate(d.getDate() - 1);
|
|
1572
|
+
return d.getTime();
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const offsetMatch = RELATIVE_OFFSET_RE.exec(lower);
|
|
1576
|
+
if (offsetMatch) {
|
|
1577
|
+
const amount = parseInt(offsetMatch[1], 10);
|
|
1578
|
+
const unit = offsetMatch[2];
|
|
1579
|
+
const ms = unitToMs(unit, amount);
|
|
1580
|
+
return now.getTime() + ms;
|
|
1581
|
+
}
|
|
1582
|
+
if (lower.startsWith("next ")) {
|
|
1583
|
+
const dayName = lower.slice(5);
|
|
1584
|
+
const dayIndex = DAY_NAMES.indexOf(dayName);
|
|
1585
|
+
if (dayIndex !== -1) {
|
|
1586
|
+
const today = now.getDay();
|
|
1587
|
+
let daysAhead = dayIndex - today;
|
|
1588
|
+
if (daysAhead <= 0) daysAhead += 7;
|
|
1589
|
+
const target = startOfDay(now);
|
|
1590
|
+
target.setDate(target.getDate() + daysAhead);
|
|
1591
|
+
return target.getTime();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (ISO_DATE_RE.test(raw)) {
|
|
1595
|
+
const d = /* @__PURE__ */ new Date(raw + "T00:00:00");
|
|
1596
|
+
if (!isNaN(d.getTime())) return d.getTime();
|
|
1597
|
+
}
|
|
1598
|
+
if (ISO_DATETIME_RE.test(raw)) {
|
|
1599
|
+
const d = new Date(raw);
|
|
1600
|
+
if (!isNaN(d.getTime())) return d.getTime();
|
|
1601
|
+
}
|
|
1602
|
+
throw new Error(`Unable to parse date: "${input}"`);
|
|
1603
|
+
}
|
|
1604
|
+
function startOfDay(date) {
|
|
1605
|
+
const d = new Date(date);
|
|
1606
|
+
d.setHours(0, 0, 0, 0);
|
|
1607
|
+
return d;
|
|
1608
|
+
}
|
|
1609
|
+
function unitToMs(unit, amount) {
|
|
1610
|
+
switch (unit) {
|
|
1611
|
+
case "m":
|
|
1612
|
+
return amount * 60 * 1e3;
|
|
1613
|
+
case "h":
|
|
1614
|
+
return amount * 60 * 60 * 1e3;
|
|
1615
|
+
case "d":
|
|
1616
|
+
return amount * 24 * 60 * 60 * 1e3;
|
|
1617
|
+
case "w":
|
|
1618
|
+
return amount * 7 * 24 * 60 * 60 * 1e3;
|
|
1619
|
+
default:
|
|
1620
|
+
return 0;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/parse.ts
|
|
1625
|
+
function fail(message) {
|
|
1626
|
+
process.stderr.write(`Error: ${message}
|
|
1627
|
+
`);
|
|
1628
|
+
process.exit(2);
|
|
1629
|
+
}
|
|
1630
|
+
function parseIntStrict(value, flag) {
|
|
1631
|
+
const num = parseInt(value, 10);
|
|
1632
|
+
if (isNaN(num)) fail(`${flag} must be a number, got "${value}"`);
|
|
1633
|
+
return num;
|
|
1634
|
+
}
|
|
1635
|
+
function parseFloatStrict(value, flag) {
|
|
1636
|
+
const num = parseFloat(value);
|
|
1637
|
+
if (isNaN(num)) fail(`${flag} must be a number, got "${value}"`);
|
|
1638
|
+
return num;
|
|
1639
|
+
}
|
|
1640
|
+
function parseDateStrict(value, flag) {
|
|
1641
|
+
try {
|
|
1642
|
+
return parseDate(value);
|
|
1643
|
+
} catch {
|
|
1644
|
+
fail(`${flag} must be a date (ISO 8601, relative like +3d, or Unix ms), got "${value}"`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function parseBoolStrict(value, flag) {
|
|
1648
|
+
if (value === "true") return true;
|
|
1649
|
+
if (value === "false") return false;
|
|
1650
|
+
fail(`${flag} must be "true" or "false", got "${value}"`);
|
|
1651
|
+
}
|
|
1652
|
+
function intArg(flag) {
|
|
1653
|
+
return (value) => parseIntStrict(value, flag);
|
|
1654
|
+
}
|
|
1655
|
+
function enumIntArg(flag, allowed) {
|
|
1656
|
+
return (value) => {
|
|
1657
|
+
const num = parseInt(value, 10);
|
|
1658
|
+
if (isNaN(num) || !allowed.includes(num)) {
|
|
1659
|
+
fail(`${flag} must be one of: ${allowed.join(", ")}`);
|
|
1660
|
+
}
|
|
1661
|
+
return num;
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1163
1665
|
// src/commands/list.ts
|
|
1164
1666
|
registerSchema("list", "list", "List lists in a folder", [
|
|
1165
1667
|
{ flag: "--folder-id", type: "string", required: true, description: "Folder ID" },
|
|
@@ -1232,11 +1734,11 @@ function registerListCommands(program, getClient) {
|
|
|
1232
1734
|
const data = await client.get(`/list/${listId}`);
|
|
1233
1735
|
formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
|
|
1234
1736
|
});
|
|
1235
|
-
list.command("create").description("Create a new list in a folder").requiredOption("--folder-id <id>", "Folder ID").requiredOption("--name <name>", "List name").option("--content <desc>", "List description").option("--due-date <ts>", "Due date (Unix ms)").option("--priority <n>", "Priority (1-4)",
|
|
1737
|
+
list.command("create").description("Create a new list in a folder").requiredOption("--folder-id <id>", "Folder ID").requiredOption("--name <name>", "List name").option("--content <desc>", "List description").option("--due-date <ts>", "Due date (Unix ms)").option("--priority <n>", "Priority (1-4)", enumIntArg("--priority", [1, 2, 3, 4])).option("--status <s>", "Default status").action(async (opts) => {
|
|
1236
1738
|
const client = getClient();
|
|
1237
1739
|
const body = { name: opts.name };
|
|
1238
1740
|
if (opts.content !== void 0) body["content"] = opts.content;
|
|
1239
|
-
if (opts.dueDate !== void 0) body["due_date"] =
|
|
1741
|
+
if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
|
|
1240
1742
|
if (opts.priority !== void 0) body["priority"] = opts.priority;
|
|
1241
1743
|
if (opts.status !== void 0) body["status"] = opts.status;
|
|
1242
1744
|
const data = await client.post(`/folder/${opts.folderId}/list`, body);
|
|
@@ -1254,7 +1756,7 @@ function registerListCommands(program, getClient) {
|
|
|
1254
1756
|
const body = {};
|
|
1255
1757
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1256
1758
|
if (opts.content !== void 0) body["content"] = opts.content;
|
|
1257
|
-
if (opts.dueDate !== void 0) body["due_date"] =
|
|
1759
|
+
if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
|
|
1258
1760
|
if (opts.unsetStatus) body["unset_status"] = true;
|
|
1259
1761
|
const data = await client.put(`/list/${listId}`, body);
|
|
1260
1762
|
formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
|
|
@@ -1294,6 +1796,116 @@ function registerListCommands(program, getClient) {
|
|
|
1294
1796
|
|
|
1295
1797
|
// src/commands/task.ts
|
|
1296
1798
|
init_config();
|
|
1799
|
+
|
|
1800
|
+
// src/commands/task-bulk.ts
|
|
1801
|
+
registerSchema("task", "bulk-update", "Apply the same update to multiple tasks", [
|
|
1802
|
+
{ flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
|
|
1803
|
+
{ flag: "--name", type: "string", required: false, description: "New task name" },
|
|
1804
|
+
{ flag: "--description", type: "string", required: false, description: "New description" },
|
|
1805
|
+
{ flag: "--status", type: "string", required: false, description: "New status" },
|
|
1806
|
+
{ flag: "--priority", type: "string", required: false, description: "New priority (1-4 or urgent/high/normal/low)" }
|
|
1807
|
+
]);
|
|
1808
|
+
registerSchema("task", "bulk-delete", "Delete multiple tasks", [
|
|
1809
|
+
{ flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
|
|
1810
|
+
{ flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
|
|
1811
|
+
]);
|
|
1812
|
+
async function runConcurrent(tasks, limit) {
|
|
1813
|
+
const results = new Array(tasks.length);
|
|
1814
|
+
let idx = 0;
|
|
1815
|
+
async function worker() {
|
|
1816
|
+
while (idx < tasks.length) {
|
|
1817
|
+
const current = idx++;
|
|
1818
|
+
try {
|
|
1819
|
+
results[current] = await tasks[current]();
|
|
1820
|
+
} catch (e) {
|
|
1821
|
+
results[current] = e instanceof Error ? e : new Error(String(e));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
|
|
1826
|
+
return results;
|
|
1827
|
+
}
|
|
1828
|
+
function registerTaskBulkCommands(task, program, getClient) {
|
|
1829
|
+
task.command("bulk-time-in-status").description("Get time-in-status for multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).action(async (opts) => {
|
|
1830
|
+
const client = getClient();
|
|
1831
|
+
const taskIds = opts.taskId;
|
|
1832
|
+
const results = [];
|
|
1833
|
+
for (const id of taskIds) {
|
|
1834
|
+
const data = await client.get(`/task/${id}/time_in_status`);
|
|
1835
|
+
const entries = data.status_history ?? [];
|
|
1836
|
+
if (data.current_status) entries.push(data.current_status);
|
|
1837
|
+
results.push({ task_id: id, statuses: entries });
|
|
1838
|
+
}
|
|
1839
|
+
formatOutput(results, [
|
|
1840
|
+
{ key: "task_id", header: "Task ID", width: 14 },
|
|
1841
|
+
{ key: "statuses", header: "Statuses", width: 50 }
|
|
1842
|
+
], getOutputOptions(program));
|
|
1843
|
+
});
|
|
1844
|
+
task.command("bulk-update").description("Apply the same update to multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", intArg("--time-estimate")).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).action(async (opts) => {
|
|
1845
|
+
const client = getClient();
|
|
1846
|
+
const taskIds = opts.taskId;
|
|
1847
|
+
const body = {};
|
|
1848
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1849
|
+
if (opts.description !== void 0) body["description"] = opts.description;
|
|
1850
|
+
if (opts.status !== void 0) body["status"] = opts.status;
|
|
1851
|
+
if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
|
|
1852
|
+
if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
|
|
1853
|
+
if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
|
|
1854
|
+
if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
|
|
1855
|
+
const addIds = opts.assigneeAdd;
|
|
1856
|
+
const remIds = opts.assigneeRemove;
|
|
1857
|
+
if (addIds.length || remIds.length) {
|
|
1858
|
+
body["assignees"] = {
|
|
1859
|
+
add: addIds.map((a) => parseIntStrict(a, "--assignee-add")),
|
|
1860
|
+
rem: remIds.map((a) => parseIntStrict(a, "--assignee-remove"))
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
const tasks = taskIds.map((id) => async () => {
|
|
1864
|
+
const data = await client.put(`/task/${id}`, body);
|
|
1865
|
+
return { task_id: id, name: data["name"], result: "ok" };
|
|
1866
|
+
});
|
|
1867
|
+
const results = await runConcurrent(tasks, 3);
|
|
1868
|
+
const rows = results.map(
|
|
1869
|
+
(r, i) => r instanceof Error ? { task_id: taskIds[i], name: "", result: r.message } : r
|
|
1870
|
+
);
|
|
1871
|
+
formatOutput(rows, [
|
|
1872
|
+
{ key: "task_id", header: "Task ID", width: 14 },
|
|
1873
|
+
{ key: "name", header: "Name", width: 30 },
|
|
1874
|
+
{ key: "result", header: "Result", width: 20 }
|
|
1875
|
+
], getOutputOptions(program));
|
|
1876
|
+
});
|
|
1877
|
+
task.command("bulk-delete").description("Delete multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--confirm", "Skip confirmation prompt").action(async (opts) => {
|
|
1878
|
+
const client = getClient();
|
|
1879
|
+
const taskIds = opts.taskId;
|
|
1880
|
+
if (!opts.confirm) {
|
|
1881
|
+
if (!process.stdin.isTTY) {
|
|
1882
|
+
process.stderr.write("Error: Use --confirm to bulk delete in non-interactive mode.\n");
|
|
1883
|
+
process.exit(2);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
1887
|
+
const yes = await confirm({ message: `Delete ${taskIds.length} task(s)?` });
|
|
1888
|
+
if (!yes) {
|
|
1889
|
+
process.stdout.write("Cancelled.\n");
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const tasks = taskIds.map((id) => async () => {
|
|
1894
|
+
await client.delete(`/task/${id}`);
|
|
1895
|
+
return { task_id: id, result: "deleted" };
|
|
1896
|
+
});
|
|
1897
|
+
const results = await runConcurrent(tasks, 3);
|
|
1898
|
+
const rows = results.map(
|
|
1899
|
+
(r, i) => r instanceof Error ? { task_id: taskIds[i], result: r.message } : r
|
|
1900
|
+
);
|
|
1901
|
+
formatOutput(rows, [
|
|
1902
|
+
{ key: "task_id", header: "Task ID", width: 14 },
|
|
1903
|
+
{ key: "result", header: "Result", width: 20 }
|
|
1904
|
+
], getOutputOptions(program));
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/commands/task.ts
|
|
1297
1909
|
registerSchema("task", "list", "List tasks in a list", [
|
|
1298
1910
|
{ flag: "--list-id", type: "string", required: true, description: "List ID" },
|
|
1299
1911
|
{ flag: "--archived", type: "boolean", required: false, description: "Include archived tasks" },
|
|
@@ -1377,6 +1989,26 @@ function requireWorkspaceId3(program) {
|
|
|
1377
1989
|
function collect(value, previous) {
|
|
1378
1990
|
return previous.concat([value]);
|
|
1379
1991
|
}
|
|
1992
|
+
var PRIORITY_MAP = {
|
|
1993
|
+
urgent: 1,
|
|
1994
|
+
high: 2,
|
|
1995
|
+
normal: 3,
|
|
1996
|
+
low: 4
|
|
1997
|
+
};
|
|
1998
|
+
function parsePriority(value) {
|
|
1999
|
+
const lower = value.toLowerCase();
|
|
2000
|
+
if (PRIORITY_MAP[lower] !== void 0) return PRIORITY_MAP[lower];
|
|
2001
|
+
const num = parseInt(value, 10);
|
|
2002
|
+
if (!isNaN(num) && num >= 1 && num <= 4) return num;
|
|
2003
|
+
fail("--priority must be 1-4 or urgent/high/normal/low");
|
|
2004
|
+
}
|
|
2005
|
+
function parseCustomFieldFilters(filters) {
|
|
2006
|
+
try {
|
|
2007
|
+
return JSON.stringify(filters.map((f) => JSON.parse(f)));
|
|
2008
|
+
} catch {
|
|
2009
|
+
fail("--custom-field filters must be valid JSON");
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1380
2012
|
function buildTaskListParams(opts) {
|
|
1381
2013
|
const params = {};
|
|
1382
2014
|
if (opts.archived) params["archived"] = "true";
|
|
@@ -1398,20 +2030,18 @@ function buildTaskListParams(opts) {
|
|
|
1398
2030
|
const tags = opts.tag;
|
|
1399
2031
|
if (tags?.length) params["tags[]"] = tags;
|
|
1400
2032
|
const customFields = opts.customField;
|
|
1401
|
-
if (customFields?.length)
|
|
1402
|
-
for (const cf of customFields) params["custom_fields"] = JSON.stringify(customFields.map((f) => JSON.parse(f)));
|
|
1403
|
-
}
|
|
2033
|
+
if (customFields?.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
|
|
1404
2034
|
return params;
|
|
1405
2035
|
}
|
|
1406
2036
|
function registerTaskCommands(program, getClient) {
|
|
1407
2037
|
const task = program.command("task").description("Manage tasks");
|
|
1408
|
-
task.command("list").description("List tasks in a list").requiredOption("--list-id <id>", "List ID").option("--archived", "Include archived tasks").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks in results").option("--page <n>", "Page number (0-indexed)",
|
|
2038
|
+
task.command("list").description("List tasks in a list").requiredOption("--list-id <id>", "List ID").option("--archived", "Include archived tasks").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks in results").option("--page <n>", "Page number (0-indexed)", intArg("--page")).option("--status <s>", "Filter by status (repeatable)", collect, []).option("--assignee <id>", "Filter by assignee ID (repeatable)", collect, []).option("--tag <name>", "Filter by tag name (repeatable)", collect, []).option("--due-date-gt <ts>", "Tasks due after timestamp").option("--due-date-lt <ts>", "Tasks due before timestamp").option("--date-created-gt <ts>", "Tasks created after timestamp").option("--date-created-lt <ts>", "Tasks created before timestamp").option("--date-updated-gt <ts>", "Tasks updated after timestamp").option("--date-updated-lt <ts>", "Tasks updated before timestamp").option("--custom-field <json>", "Custom field filter as JSON (repeatable)", collect, []).option("--order-by <field>", "Sort by field (id|created|updated|due_date)").option("--reverse", "Reverse sort order").action(async (opts) => {
|
|
1409
2039
|
const client = getClient();
|
|
1410
2040
|
const params = buildTaskListParams(opts);
|
|
1411
2041
|
const data = await client.get(`/list/${opts.listId}/task`, params);
|
|
1412
2042
|
formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
|
|
1413
2043
|
});
|
|
1414
|
-
task.command("search").description("Search tasks across a workspace").option("--query <text>", "Full-text search query").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks").option("--page <n>", "Page number (0-indexed)",
|
|
2044
|
+
task.command("search").description("Search tasks across a workspace").option("--query <text>", "Full-text search query").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks").option("--page <n>", "Page number (0-indexed)", intArg("--page")).option("--status <s>", "Filter by status (repeatable)", collect, []).option("--assignee <id>", "Filter by assignee ID (repeatable)", collect, []).option("--tag <name>", "Filter by tag name (repeatable)", collect, []).option("--priority <n>", "Filter by priority (repeatable)", collect, []).option("--list-id <id>", "Scope to list IDs (repeatable)", collect, []).option("--folder-id <id>", "Scope to folder IDs (repeatable)", collect, []).option("--space-id <id>", "Scope to space IDs (repeatable)", collect, []).option("--project-id <id>", "Scope to project IDs (repeatable)", collect, []).option("--due-date-gt <ts>", "Tasks due after timestamp").option("--due-date-lt <ts>", "Tasks due before timestamp").option("--date-created-gt <ts>", "Tasks created after timestamp").option("--date-created-lt <ts>", "Tasks created before timestamp").option("--custom-field <json>", "Custom field filter as JSON (repeatable)", collect, []).option("--order-by <field>", "Sort by field (id|created|updated|due_date)").option("--reverse", "Reverse sort order").action(async (opts) => {
|
|
1415
2045
|
const workspaceId = requireWorkspaceId3(program);
|
|
1416
2046
|
if (!workspaceId) return;
|
|
1417
2047
|
const client = getClient();
|
|
@@ -1443,7 +2073,7 @@ function registerTaskCommands(program, getClient) {
|
|
|
1443
2073
|
const projectIds = opts.projectId;
|
|
1444
2074
|
if (projectIds.length) params["project_ids[]"] = projectIds;
|
|
1445
2075
|
const customFields = opts.customField;
|
|
1446
|
-
if (customFields.length) params["custom_fields"] =
|
|
2076
|
+
if (customFields.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
|
|
1447
2077
|
const data = await client.get(`/team/${workspaceId}/task`, params);
|
|
1448
2078
|
formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
|
|
1449
2079
|
});
|
|
@@ -1455,17 +2085,17 @@ function registerTaskCommands(program, getClient) {
|
|
|
1455
2085
|
const data = await client.get(`/task/${taskId}`, params);
|
|
1456
2086
|
formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
|
|
1457
2087
|
});
|
|
1458
|
-
task.command("create").description("Create a new task").requiredOption("--list-id <id>", "List ID").requiredOption("--name <name>", "Task name").option("--description <desc>", "Plain text description").option("--markdown-description <md>", "Markdown description (overrides --description)").option("--status <s>", "Initial status").option("--priority <n>", "Priority (1
|
|
2088
|
+
task.command("create").description("Create a new task").requiredOption("--list-id <id>", "List ID").requiredOption("--name <name>", "Task name").option("--description <desc>", "Plain text description").option("--markdown-description <md>", "Markdown description (overrides --description)").option("--status <s>", "Initial status").option("--priority <n>", "Priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "Due date (Unix ms)").option("--start-date <date>", "Start date (Unix ms)").option("--assignee <id>", "Assignee user ID (repeatable)", collect, []).option("--tag <name>", "Tag name (repeatable)", collect, []).option("--time-estimate <ms>", "Time estimate in milliseconds", intArg("--time-estimate")).option("--notify-all", "Notify all assignees and watchers").option("--parent <task-id>", "Parent task ID (creates subtask)").option("--links-to <task-id>", "Link to another task").option("--custom-field <id=value>", "Set custom field (repeatable)", collect, []).option("--check-required-custom-fields", "Reject if required custom fields are missing").action(async (opts) => {
|
|
1459
2089
|
const client = getClient();
|
|
1460
2090
|
const body = { name: opts.name };
|
|
1461
2091
|
if (opts.markdownDescription !== void 0) body["markdown_description"] = opts.markdownDescription;
|
|
1462
2092
|
else if (opts.description !== void 0) body["description"] = opts.description;
|
|
1463
2093
|
if (opts.status !== void 0) body["status"] = opts.status;
|
|
1464
|
-
if (opts.priority !== void 0) body["priority"] = opts.priority;
|
|
1465
|
-
if (opts.dueDate !== void 0) body["due_date"] =
|
|
1466
|
-
if (opts.startDate !== void 0) body["start_date"] =
|
|
2094
|
+
if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
|
|
2095
|
+
if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
|
|
2096
|
+
if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
|
|
1467
2097
|
const assignees = opts.assignee;
|
|
1468
|
-
if (assignees.length) body["assignees"] = assignees.map((a) =>
|
|
2098
|
+
if (assignees.length) body["assignees"] = assignees.map((a) => parseIntStrict(a, "--assignee"));
|
|
1469
2099
|
const tags = opts.tag;
|
|
1470
2100
|
if (tags.length) body["tags"] = tags;
|
|
1471
2101
|
if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
|
|
@@ -1476,7 +2106,7 @@ function registerTaskCommands(program, getClient) {
|
|
|
1476
2106
|
if (customFields.length) {
|
|
1477
2107
|
body["custom_fields"] = customFields.map((cf) => {
|
|
1478
2108
|
const eqIdx = cf.indexOf("=");
|
|
1479
|
-
if (eqIdx === -1)
|
|
2109
|
+
if (eqIdx === -1) fail(`Invalid custom field format: ${cf}. Expected: <id>=<value>`);
|
|
1480
2110
|
const id = cf.slice(0, eqIdx);
|
|
1481
2111
|
let value = cf.slice(eqIdx + 1);
|
|
1482
2112
|
try {
|
|
@@ -1490,22 +2120,22 @@ function registerTaskCommands(program, getClient) {
|
|
|
1490
2120
|
const data = await client.post(`/list/${opts.listId}/task`, body);
|
|
1491
2121
|
formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
|
|
1492
2122
|
});
|
|
1493
|
-
task.command("update").description("Update a task").argument("<task-id>", "Task ID").option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4)"
|
|
2123
|
+
task.command("update").description("Update a task").argument("<task-id>", "Task ID").option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", intArg("--time-estimate")).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).option("--archived <bool>", "Archive or unarchive").action(async (taskId, opts) => {
|
|
1494
2124
|
const client = getClient();
|
|
1495
2125
|
const body = {};
|
|
1496
2126
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1497
2127
|
if (opts.description !== void 0) body["description"] = opts.description;
|
|
1498
2128
|
if (opts.status !== void 0) body["status"] = opts.status;
|
|
1499
|
-
if (opts.priority !== void 0) body["priority"] = opts.priority;
|
|
1500
|
-
if (opts.dueDate !== void 0) body["due_date"] =
|
|
1501
|
-
if (opts.startDate !== void 0) body["start_date"] =
|
|
2129
|
+
if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
|
|
2130
|
+
if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
|
|
2131
|
+
if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
|
|
1502
2132
|
if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
|
|
1503
2133
|
const addIds = opts.assigneeAdd;
|
|
1504
2134
|
const remIds = opts.assigneeRemove;
|
|
1505
2135
|
if (addIds.length || remIds.length) {
|
|
1506
2136
|
body["assignees"] = {
|
|
1507
|
-
add: addIds.map((a) =>
|
|
1508
|
-
rem: remIds.map((a) =>
|
|
2137
|
+
add: addIds.map((a) => parseIntStrict(a, "--assignee-add")),
|
|
2138
|
+
rem: remIds.map((a) => parseIntStrict(a, "--assignee-remove"))
|
|
1509
2139
|
};
|
|
1510
2140
|
}
|
|
1511
2141
|
if (opts.archived !== void 0) body["archived"] = opts.archived === "true";
|
|
@@ -1538,21 +2168,7 @@ function registerTaskCommands(program, getClient) {
|
|
|
1538
2168
|
if (data.current_status) entries.push(data.current_status);
|
|
1539
2169
|
formatOutput(entries, TIME_IN_STATUS_COLUMNS, getOutputOptions(program));
|
|
1540
2170
|
});
|
|
1541
|
-
|
|
1542
|
-
const client = getClient();
|
|
1543
|
-
const taskIds = opts.taskId;
|
|
1544
|
-
const results = [];
|
|
1545
|
-
for (const id of taskIds) {
|
|
1546
|
-
const data = await client.get(`/task/${id}/time_in_status`);
|
|
1547
|
-
const entries = data.status_history ?? [];
|
|
1548
|
-
if (data.current_status) entries.push(data.current_status);
|
|
1549
|
-
results.push({ task_id: id, statuses: entries });
|
|
1550
|
-
}
|
|
1551
|
-
formatOutput(results, [
|
|
1552
|
-
{ key: "task_id", header: "Task ID", width: 14 },
|
|
1553
|
-
{ key: "statuses", header: "Statuses", width: 50 }
|
|
1554
|
-
], getOutputOptions(program));
|
|
1555
|
-
});
|
|
2171
|
+
registerTaskBulkCommands(task, program, getClient);
|
|
1556
2172
|
}
|
|
1557
2173
|
|
|
1558
2174
|
// src/commands/checklist.ts
|
|
@@ -1600,7 +2216,7 @@ function registerChecklistCommands(program, getClient) {
|
|
|
1600
2216
|
const data = await client.post(`/task/${opts.taskId}/checklist`, { name: opts.name });
|
|
1601
2217
|
formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
|
|
1602
2218
|
});
|
|
1603
|
-
checklist.command("update").description("Update a checklist").argument("<checklist-id>", "Checklist ID").option("--name <name>", "New checklist name").option("--position <n>", "Position (0-indexed)",
|
|
2219
|
+
checklist.command("update").description("Update a checklist").argument("<checklist-id>", "Checklist ID").option("--name <name>", "New checklist name").option("--position <n>", "Position (0-indexed)", intArg("--position")).action(async (checklistId, opts) => {
|
|
1604
2220
|
const client = getClient();
|
|
1605
2221
|
const body = {};
|
|
1606
2222
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
@@ -1630,7 +2246,7 @@ function registerChecklistCommands(program, getClient) {
|
|
|
1630
2246
|
checklist.command("add-item").description("Add an item to a checklist").argument("<checklist-id>", "Checklist ID").requiredOption("--name <name>", "Item name").option("--assignee <id>", "Assignee user ID").option("--resolved <bool>", "Mark as resolved").option("--parent <item-id>", "Parent item ID").action(async (checklistId, opts) => {
|
|
1631
2247
|
const client = getClient();
|
|
1632
2248
|
const body = { name: opts.name };
|
|
1633
|
-
if (opts.assignee !== void 0) body["assignee"] =
|
|
2249
|
+
if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
|
|
1634
2250
|
if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
|
|
1635
2251
|
if (opts.parent !== void 0) body["parent"] = opts.parent;
|
|
1636
2252
|
const data = await client.post(`/checklist/${checklistId}/checklist_item`, body);
|
|
@@ -1641,7 +2257,7 @@ function registerChecklistCommands(program, getClient) {
|
|
|
1641
2257
|
const body = {};
|
|
1642
2258
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1643
2259
|
if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
|
|
1644
|
-
if (opts.assignee !== void 0) body["assignee"] =
|
|
2260
|
+
if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
|
|
1645
2261
|
if (opts.parent !== void 0) body["parent"] = opts.parent;
|
|
1646
2262
|
const data = await client.put(`/checklist/${checklistId}/checklist_item/${opts.itemId}`, body);
|
|
1647
2263
|
formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
|
|
@@ -1668,6 +2284,7 @@ function registerChecklistCommands(program, getClient) {
|
|
|
1668
2284
|
}
|
|
1669
2285
|
|
|
1670
2286
|
// src/commands/custom-field.ts
|
|
2287
|
+
init_config();
|
|
1671
2288
|
registerSchema("field", "list", "List custom fields for a list, folder, space, or workspace", [
|
|
1672
2289
|
{ flag: "--list-id", type: "string", required: false, description: "List ID" },
|
|
1673
2290
|
{ flag: "--folder-id", type: "string", required: false, description: "Folder ID" },
|
|
@@ -1681,7 +2298,8 @@ registerSchema("field", "set", "Set a custom field value on a task", [
|
|
|
1681
2298
|
]);
|
|
1682
2299
|
registerSchema("field", "remove", "Remove a custom field value from a task", [
|
|
1683
2300
|
{ flag: "--task-id", type: "string", required: true, description: "Task ID" },
|
|
1684
|
-
{ flag: "--field-id", type: "string", required: true, description: "Field ID" }
|
|
2301
|
+
{ flag: "--field-id", type: "string", required: true, description: "Field ID" },
|
|
2302
|
+
{ flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
|
|
1685
2303
|
]);
|
|
1686
2304
|
var FIELD_COLUMNS = [
|
|
1687
2305
|
{ key: "id", header: "ID", width: 20 },
|
|
@@ -1694,7 +2312,7 @@ function registerFieldCommands(program, getClient) {
|
|
|
1694
2312
|
const field = program.command("field").description("Manage custom fields");
|
|
1695
2313
|
field.command("list").description("List custom fields (provide one ID flag)").option("--list-id <id>", "List ID").option("--folder-id <id>", "Folder ID").option("--space-id <id>", "Space ID").action(async (opts) => {
|
|
1696
2314
|
const client = getClient();
|
|
1697
|
-
const globalWorkspaceId = program.opts()["workspaceId"];
|
|
2315
|
+
const globalWorkspaceId = resolveWorkspaceId(program.opts()["workspaceId"]);
|
|
1698
2316
|
let endpoint;
|
|
1699
2317
|
if (opts.listId) {
|
|
1700
2318
|
endpoint = `/list/${opts.listId}/field`;
|
|
@@ -1722,8 +2340,21 @@ function registerFieldCommands(program, getClient) {
|
|
|
1722
2340
|
const data = await client.post(`/task/${opts.taskId}/field/${opts.fieldId}`, { value: parsedValue });
|
|
1723
2341
|
formatOutput(data, FIELD_COLUMNS, getOutputOptions(program));
|
|
1724
2342
|
});
|
|
1725
|
-
field.command("remove").description("Remove a custom field value from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--field-id <fid>", "Field ID").action(async (opts) => {
|
|
2343
|
+
field.command("remove").description("Remove a custom field value from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--field-id <fid>", "Field ID").option("--confirm", "Skip confirmation prompt").action(async (opts) => {
|
|
1726
2344
|
const client = getClient();
|
|
2345
|
+
if (!opts.confirm) {
|
|
2346
|
+
if (!process.stdin.isTTY) {
|
|
2347
|
+
process.stderr.write("Error: Use --confirm to remove in non-interactive mode.\n");
|
|
2348
|
+
process.exit(2);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
2352
|
+
const yes = await confirm({ message: `Remove field ${opts.fieldId} value from task ${opts.taskId}?` });
|
|
2353
|
+
if (!yes) {
|
|
2354
|
+
process.stdout.write("Cancelled.\n");
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
1727
2358
|
await client.delete(`/task/${opts.taskId}/field/${opts.fieldId}`);
|
|
1728
2359
|
process.stdout.write(`Removed field ${opts.fieldId} from task ${opts.taskId}
|
|
1729
2360
|
`);
|
|
@@ -1900,11 +2531,22 @@ function registerRelationCommands(program, getClient) {
|
|
|
1900
2531
|
}
|
|
1901
2532
|
|
|
1902
2533
|
// src/commands/attachment.ts
|
|
2534
|
+
import { writeFileSync, existsSync as existsSync2 } from "fs";
|
|
2535
|
+
import { join, basename as basename2, resolve } from "path";
|
|
1903
2536
|
registerSchema("attachment", "upload", "Upload a file to a task", [
|
|
1904
2537
|
{ flag: "--task-id", type: "string", required: true, description: "Task ID" },
|
|
1905
2538
|
{ flag: "--file", type: "string", required: true, description: "Local file path" },
|
|
1906
2539
|
{ flag: "--filename", type: "string", required: false, description: "Override display filename" }
|
|
1907
2540
|
]);
|
|
2541
|
+
registerSchema("attachment", "list", "List attachments on a task", [
|
|
2542
|
+
{ flag: "--task-id", type: "string", required: true, description: "Task ID" }
|
|
2543
|
+
]);
|
|
2544
|
+
registerSchema("attachment", "download", "Download an attachment from a task", [
|
|
2545
|
+
{ flag: "--task-id", type: "string", required: true, description: "Task ID" },
|
|
2546
|
+
{ flag: "--attachment-id", type: "string", required: true, description: "Attachment ID" },
|
|
2547
|
+
{ flag: "--output", type: "string", required: false, description: "Output file path (default: ./attachment-<id>-<title>)" },
|
|
2548
|
+
{ flag: "--force", type: "boolean", required: false, description: "Overwrite the output file if it exists" }
|
|
2549
|
+
]);
|
|
1908
2550
|
var ATTACHMENT_COLUMNS = [
|
|
1909
2551
|
{ key: "id", header: "ID", width: 20 },
|
|
1910
2552
|
{ key: "title", header: "Title", width: 30 },
|
|
@@ -1919,6 +2561,42 @@ function registerAttachmentCommands(program, getClient) {
|
|
|
1919
2561
|
const data = await client.upload(`/task/${opts.taskId}/attachment`, opts.file, opts.filename);
|
|
1920
2562
|
formatOutput(data, ATTACHMENT_COLUMNS, getOutputOptions(program));
|
|
1921
2563
|
});
|
|
2564
|
+
attachment.command("list").description("List attachments on a task").requiredOption("--task-id <id>", "Task ID").action(async (opts) => {
|
|
2565
|
+
const client = getClient();
|
|
2566
|
+
const data = await client.get(`/task/${opts.taskId}`);
|
|
2567
|
+
const attachments = data["attachments"] ?? [];
|
|
2568
|
+
formatOutput(attachments, ATTACHMENT_COLUMNS, getOutputOptions(program));
|
|
2569
|
+
});
|
|
2570
|
+
attachment.command("download").description("Download an attachment from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--attachment-id <id>", "Attachment ID").option("--output <path>", "Output file path").option("--force", "Overwrite the output file if it exists").action(async (opts) => {
|
|
2571
|
+
const { default: ora } = await import("ora");
|
|
2572
|
+
const client = getClient();
|
|
2573
|
+
const data = await client.get(`/task/${opts.taskId}`);
|
|
2574
|
+
const attachments = data["attachments"] ?? [];
|
|
2575
|
+
const attachment2 = attachments.find((a) => a.id === opts.attachmentId);
|
|
2576
|
+
if (!attachment2) {
|
|
2577
|
+
process.stderr.write(`Error: Attachment "${opts.attachmentId}" not found on task "${opts.taskId}".
|
|
2578
|
+
`);
|
|
2579
|
+
process.exit(4);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const safeTitle = basename2(attachment2.title.replace(/[/\\]/g, "_")) || "file";
|
|
2583
|
+
const outputPath = opts.output ? resolve(opts.output) : join(process.cwd(), `attachment-${attachment2.id}-${safeTitle}`);
|
|
2584
|
+
if (existsSync2(outputPath) && !opts.force) {
|
|
2585
|
+
process.stderr.write(`Error: "${outputPath}" already exists. Use --force to overwrite or --output to pick another path.
|
|
2586
|
+
`);
|
|
2587
|
+
process.exit(2);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const spinner = ora(`Downloading ${safeTitle}...`).start();
|
|
2591
|
+
try {
|
|
2592
|
+
const buffer = await client.downloadUrl(attachment2.url);
|
|
2593
|
+
writeFileSync(outputPath, Buffer.from(buffer));
|
|
2594
|
+
spinner.succeed(`Downloaded to ${outputPath}`);
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
spinner.fail("Download failed");
|
|
2597
|
+
throw err;
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
1922
2600
|
}
|
|
1923
2601
|
|
|
1924
2602
|
// src/commands/comment.ts
|
|
@@ -2005,7 +2683,7 @@ function registerCommentCommands(program, getClient) {
|
|
|
2005
2683
|
if (!parent) return;
|
|
2006
2684
|
const client = getClient();
|
|
2007
2685
|
const body = { comment_text: opts.text };
|
|
2008
|
-
if (opts.assignee !== void 0) body["assignee"] =
|
|
2686
|
+
if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
|
|
2009
2687
|
if (opts.notifyAll) body["notify_all"] = true;
|
|
2010
2688
|
const data = await client.post(`/${parent.type}/${parent.id}/comment`, body);
|
|
2011
2689
|
process.stdout.write(`Created comment ${data["id"] ?? ""}
|
|
@@ -2014,7 +2692,7 @@ function registerCommentCommands(program, getClient) {
|
|
|
2014
2692
|
comment.command("update").description("Update a comment").argument("<comment-id>", "Comment ID").requiredOption("--text <text>", "New comment text").option("--assignee <id>", "Assignee user ID").option("--resolved <bool>", "Mark as resolved (true/false)").action(async (commentId, opts) => {
|
|
2015
2693
|
const client = getClient();
|
|
2016
2694
|
const body = { comment_text: opts.text };
|
|
2017
|
-
if (opts.assignee !== void 0) body["assignee"] =
|
|
2695
|
+
if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
|
|
2018
2696
|
if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
|
|
2019
2697
|
await client.put(`/comment/${commentId}`, body);
|
|
2020
2698
|
process.stdout.write(`Updated comment ${commentId}
|
|
@@ -2047,7 +2725,7 @@ function registerCommentCommands(program, getClient) {
|
|
|
2047
2725
|
comment.command("reply").description("Reply to a comment (threaded)").argument("<comment-id>", "Comment ID").requiredOption("--text <text>", "Reply text").option("--assignee <id>", "Assignee user ID").option("--notify-all", "Notify all watchers").action(async (commentId, opts) => {
|
|
2048
2726
|
const client = getClient();
|
|
2049
2727
|
const body = { comment_text: opts.text };
|
|
2050
|
-
if (opts.assignee !== void 0) body["assignee"] =
|
|
2728
|
+
if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
|
|
2051
2729
|
if (opts.notifyAll) body["notify_all"] = true;
|
|
2052
2730
|
const data = await client.post(`/comment/${commentId}/thread`, body);
|
|
2053
2731
|
process.stdout.write(`Created reply ${data["id"] ?? ""}
|
|
@@ -2057,85 +2735,6 @@ function registerCommentCommands(program, getClient) {
|
|
|
2057
2735
|
|
|
2058
2736
|
// src/commands/time-tracking.ts
|
|
2059
2737
|
init_config();
|
|
2060
|
-
|
|
2061
|
-
// src/dates.ts
|
|
2062
|
-
var RELATIVE_OFFSET_RE = /^([+-]\d+)([dwmh])$/;
|
|
2063
|
-
var UNIX_MS_RE = /^\d{13,}$/;
|
|
2064
|
-
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2065
|
-
var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
2066
|
-
var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
2067
|
-
function parseDate(input) {
|
|
2068
|
-
const raw = input.trim();
|
|
2069
|
-
const lower = raw.toLowerCase();
|
|
2070
|
-
if (UNIX_MS_RE.test(raw)) {
|
|
2071
|
-
return parseInt(raw, 10);
|
|
2072
|
-
}
|
|
2073
|
-
const now = /* @__PURE__ */ new Date();
|
|
2074
|
-
switch (lower) {
|
|
2075
|
-
case "today": {
|
|
2076
|
-
return startOfDay(now).getTime();
|
|
2077
|
-
}
|
|
2078
|
-
case "tomorrow": {
|
|
2079
|
-
const d = startOfDay(now);
|
|
2080
|
-
d.setDate(d.getDate() + 1);
|
|
2081
|
-
return d.getTime();
|
|
2082
|
-
}
|
|
2083
|
-
case "yesterday": {
|
|
2084
|
-
const d = startOfDay(now);
|
|
2085
|
-
d.setDate(d.getDate() - 1);
|
|
2086
|
-
return d.getTime();
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
const offsetMatch = RELATIVE_OFFSET_RE.exec(lower);
|
|
2090
|
-
if (offsetMatch) {
|
|
2091
|
-
const amount = parseInt(offsetMatch[1], 10);
|
|
2092
|
-
const unit = offsetMatch[2];
|
|
2093
|
-
const ms = unitToMs(unit, amount);
|
|
2094
|
-
return now.getTime() + ms;
|
|
2095
|
-
}
|
|
2096
|
-
if (lower.startsWith("next ")) {
|
|
2097
|
-
const dayName = lower.slice(5);
|
|
2098
|
-
const dayIndex = DAY_NAMES.indexOf(dayName);
|
|
2099
|
-
if (dayIndex !== -1) {
|
|
2100
|
-
const today = now.getDay();
|
|
2101
|
-
let daysAhead = dayIndex - today;
|
|
2102
|
-
if (daysAhead <= 0) daysAhead += 7;
|
|
2103
|
-
const target = startOfDay(now);
|
|
2104
|
-
target.setDate(target.getDate() + daysAhead);
|
|
2105
|
-
return target.getTime();
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
if (ISO_DATE_RE.test(raw)) {
|
|
2109
|
-
const d = /* @__PURE__ */ new Date(raw + "T00:00:00");
|
|
2110
|
-
if (!isNaN(d.getTime())) return d.getTime();
|
|
2111
|
-
}
|
|
2112
|
-
if (ISO_DATETIME_RE.test(raw)) {
|
|
2113
|
-
const d = new Date(raw);
|
|
2114
|
-
if (!isNaN(d.getTime())) return d.getTime();
|
|
2115
|
-
}
|
|
2116
|
-
throw new Error(`Unable to parse date: "${input}"`);
|
|
2117
|
-
}
|
|
2118
|
-
function startOfDay(date) {
|
|
2119
|
-
const d = new Date(date);
|
|
2120
|
-
d.setHours(0, 0, 0, 0);
|
|
2121
|
-
return d;
|
|
2122
|
-
}
|
|
2123
|
-
function unitToMs(unit, amount) {
|
|
2124
|
-
switch (unit) {
|
|
2125
|
-
case "m":
|
|
2126
|
-
return amount * 60 * 1e3;
|
|
2127
|
-
case "h":
|
|
2128
|
-
return amount * 60 * 60 * 1e3;
|
|
2129
|
-
case "d":
|
|
2130
|
-
return amount * 24 * 60 * 60 * 1e3;
|
|
2131
|
-
case "w":
|
|
2132
|
-
return amount * 7 * 24 * 60 * 60 * 1e3;
|
|
2133
|
-
default:
|
|
2134
|
-
return 0;
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
// src/commands/time-tracking.ts
|
|
2139
2738
|
registerSchema("time", "list", "List time entries for a task or workspace", [
|
|
2140
2739
|
{ flag: "--task-id", type: "string", required: false, description: "Task ID (or use --workspace-id for workspace-wide)" },
|
|
2141
2740
|
{ flag: "--workspace-id", type: "string", required: false, description: "Workspace ID" },
|
|
@@ -2166,7 +2765,8 @@ registerSchema("time", "update", "Update a time entry", [
|
|
|
2166
2765
|
]);
|
|
2167
2766
|
registerSchema("time", "delete", "Delete a time entry", [
|
|
2168
2767
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
|
|
2169
|
-
{ flag: "<timer-id>", type: "string", required: true, description: "Time entry ID" }
|
|
2768
|
+
{ flag: "<timer-id>", type: "string", required: true, description: "Time entry ID" },
|
|
2769
|
+
{ flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
|
|
2170
2770
|
]);
|
|
2171
2771
|
registerSchema("time", "running", "Get current running timer", [
|
|
2172
2772
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
|
|
@@ -2254,12 +2854,12 @@ function registerTimeTrackingCommands(program, getClient) {
|
|
|
2254
2854
|
time.command("create").description("Create a time entry on a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--duration <ms>", "Duration in milliseconds").requiredOption("--start <ts>", "Start time (Unix ms or date string)").option("--description <desc>", "Description").option("--assignee <id>", "Assignee user ID").option("--billable <bool>", "Billable (true/false)").option("--tag <name>", "Tag name (repeatable)", collect2, []).action(async (opts) => {
|
|
2255
2855
|
const client = getClient();
|
|
2256
2856
|
const body = {
|
|
2257
|
-
duration:
|
|
2857
|
+
duration: parseIntStrict(opts.duration, "--duration"),
|
|
2258
2858
|
start: String(parseDate(opts.start))
|
|
2259
2859
|
};
|
|
2260
2860
|
if (opts.description !== void 0) body["description"] = opts.description;
|
|
2261
|
-
if (opts.assignee !== void 0) body["assignee"] =
|
|
2262
|
-
if (opts.billable !== void 0) body["billable"] = opts.billable
|
|
2861
|
+
if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
|
|
2862
|
+
if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
|
|
2263
2863
|
if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
|
|
2264
2864
|
const data = await client.post(`/task/${opts.taskId}/time`, body);
|
|
2265
2865
|
process.stdout.write(`Created time entry ${data.data?.id ?? ""}
|
|
@@ -2271,9 +2871,9 @@ function registerTimeTrackingCommands(program, getClient) {
|
|
|
2271
2871
|
const client = getClient();
|
|
2272
2872
|
const body = {};
|
|
2273
2873
|
if (opts.description !== void 0) body["description"] = opts.description;
|
|
2274
|
-
if (opts.duration !== void 0) body["duration"] =
|
|
2874
|
+
if (opts.duration !== void 0) body["duration"] = parseIntStrict(opts.duration, "--duration");
|
|
2275
2875
|
if (opts.start !== void 0) body["start"] = String(parseDate(opts.start));
|
|
2276
|
-
if (opts.billable !== void 0) body["billable"] = opts.billable
|
|
2876
|
+
if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
|
|
2277
2877
|
if (opts.tag.length) {
|
|
2278
2878
|
body["tags"] = opts.tag.map((t) => ({ name: t }));
|
|
2279
2879
|
if (opts.tagAction) body["tag_action"] = opts.tagAction;
|
|
@@ -2282,10 +2882,23 @@ function registerTimeTrackingCommands(program, getClient) {
|
|
|
2282
2882
|
process.stdout.write(`Updated time entry ${timerId}
|
|
2283
2883
|
`);
|
|
2284
2884
|
});
|
|
2285
|
-
time.command("delete").description("Delete a time entry").argument("<timer-id>", "Time entry ID").action(async (timerId) => {
|
|
2885
|
+
time.command("delete").description("Delete a time entry").argument("<timer-id>", "Time entry ID").option("--confirm", "Skip confirmation prompt").action(async (timerId, opts) => {
|
|
2286
2886
|
const workspaceId = requireWorkspaceId4(program);
|
|
2287
2887
|
if (!workspaceId) return;
|
|
2288
2888
|
const client = getClient();
|
|
2889
|
+
if (!opts.confirm) {
|
|
2890
|
+
if (!process.stdin.isTTY) {
|
|
2891
|
+
process.stderr.write("Error: Use --confirm to delete in non-interactive mode.\n");
|
|
2892
|
+
process.exit(2);
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
2896
|
+
const yes = await confirm({ message: `Delete time entry ${timerId}?` });
|
|
2897
|
+
if (!yes) {
|
|
2898
|
+
process.stdout.write("Cancelled.\n");
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2289
2902
|
await client.delete(`/team/${workspaceId}/time_entries/${timerId}`);
|
|
2290
2903
|
process.stdout.write(`Deleted time entry ${timerId}
|
|
2291
2904
|
`);
|
|
@@ -2316,7 +2929,7 @@ function registerTimeTrackingCommands(program, getClient) {
|
|
|
2316
2929
|
const client = getClient();
|
|
2317
2930
|
const body = { tid: opts.taskId };
|
|
2318
2931
|
if (opts.description !== void 0) body["description"] = opts.description;
|
|
2319
|
-
if (opts.billable !== void 0) body["billable"] = opts.billable
|
|
2932
|
+
if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
|
|
2320
2933
|
if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
|
|
2321
2934
|
const data = await client.post(`/team/${workspaceId}/time_entries/start`, body);
|
|
2322
2935
|
process.stdout.write(`Started timer ${data.data?.id ?? ""}
|
|
@@ -2481,7 +3094,7 @@ function registerGoalCommands(program, getClient) {
|
|
|
2481
3094
|
if (opts.dueDate !== void 0) body["due_date"] = opts.dueDate;
|
|
2482
3095
|
if (opts.description !== void 0) body["description"] = opts.description;
|
|
2483
3096
|
if (opts.multipleOwners) body["multiple_owners"] = true;
|
|
2484
|
-
if (opts.owner.length) body["owners"] = opts.owner.map((id) =>
|
|
3097
|
+
if (opts.owner.length) body["owners"] = opts.owner.map((id) => parseIntStrict(id, "--owner"));
|
|
2485
3098
|
if (opts.color !== void 0) body["color"] = opts.color;
|
|
2486
3099
|
const data = await client.post(`/team/${workspaceId}/goal`, body);
|
|
2487
3100
|
process.stdout.write(`Created goal ${data.goal?.id ?? ""}
|
|
@@ -2520,8 +3133,8 @@ function registerGoalCommands(program, getClient) {
|
|
|
2520
3133
|
goal.command("add-key-result").description("Add a key result to a goal").argument("<goal-id>", "Goal ID").requiredOption("--name <name>", "Key result name").requiredOption("--type <type>", "Type (number, currency, boolean, percentage, automatic)").option("--steps-start <n>", "Starting value").option("--steps-end <n>", "Target value").option("--unit <unit>", "Unit label").option("--task-ids <id>", "Task ID (repeatable, for automatic type)", collect3, []).option("--list-ids <id>", "List ID (repeatable, for automatic type)", collect3, []).action(async (goalId, opts) => {
|
|
2521
3134
|
const client = getClient();
|
|
2522
3135
|
const body = { name: opts.name, type: opts.type };
|
|
2523
|
-
if (opts.stepsStart !== void 0) body["steps_start"] =
|
|
2524
|
-
if (opts.stepsEnd !== void 0) body["steps_end"] =
|
|
3136
|
+
if (opts.stepsStart !== void 0) body["steps_start"] = parseFloatStrict(opts.stepsStart, "--steps-start");
|
|
3137
|
+
if (opts.stepsEnd !== void 0) body["steps_end"] = parseFloatStrict(opts.stepsEnd, "--steps-end");
|
|
2525
3138
|
if (opts.unit !== void 0) body["unit"] = opts.unit;
|
|
2526
3139
|
if (opts.taskIds.length) body["task_ids"] = opts.taskIds;
|
|
2527
3140
|
if (opts.listIds.length) body["list_ids"] = opts.listIds;
|
|
@@ -2533,7 +3146,7 @@ function registerGoalCommands(program, getClient) {
|
|
|
2533
3146
|
const client = getClient();
|
|
2534
3147
|
const body = {};
|
|
2535
3148
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
2536
|
-
if (opts.stepsCurrent !== void 0) body["steps_current"] =
|
|
3149
|
+
if (opts.stepsCurrent !== void 0) body["steps_current"] = parseFloatStrict(opts.stepsCurrent, "--steps-current");
|
|
2537
3150
|
if (opts.note !== void 0) body["note"] = opts.note;
|
|
2538
3151
|
await client.put(`/key_result/${keyResultId}`, body);
|
|
2539
3152
|
process.stdout.write(`Updated key result ${keyResultId}
|
|
@@ -2561,6 +3174,7 @@ function registerGoalCommands(program, getClient) {
|
|
|
2561
3174
|
}
|
|
2562
3175
|
|
|
2563
3176
|
// src/commands/view.ts
|
|
3177
|
+
init_config();
|
|
2564
3178
|
registerSchema("view", "list", "List views for a workspace, space, folder, or list", [
|
|
2565
3179
|
{ flag: "--workspace-id", type: "string", required: false, description: "Workspace ID (provide one parent)" },
|
|
2566
3180
|
{ flag: "--space-id", type: "string", required: false, description: "Space ID (provide one parent)" },
|
|
@@ -2613,6 +3227,10 @@ function resolveViewParent(opts, program) {
|
|
|
2613
3227
|
if (opts.spaceId) parents.push({ segment: "space", id: opts.spaceId });
|
|
2614
3228
|
if (opts.folderId) parents.push({ segment: "folder", id: opts.folderId });
|
|
2615
3229
|
if (opts.listId) parents.push({ segment: "list", id: opts.listId });
|
|
3230
|
+
if (parents.length === 0) {
|
|
3231
|
+
const resolvedWsId = resolveWorkspaceId(void 0);
|
|
3232
|
+
if (resolvedWsId) parents.push({ segment: "team", id: resolvedWsId });
|
|
3233
|
+
}
|
|
2616
3234
|
if (parents.length === 0) {
|
|
2617
3235
|
process.stderr.write("Error: Provide one of --workspace-id, --space-id, --folder-id, or --list-id.\n");
|
|
2618
3236
|
process.exit(2);
|
|
@@ -2816,6 +3434,11 @@ function registerWebhookCommands(program, getClient) {
|
|
|
2816
3434
|
`);
|
|
2817
3435
|
});
|
|
2818
3436
|
webhook.command("update").description("Update a webhook").argument("<webhook-id>", "Webhook ID").option("--endpoint <url>", "New endpoint URL").option("--event <event>", "Event to subscribe to (repeatable, replaces existing)", collect4, []).option("--status <status>", "Status (active or inactive)").action(async (webhookId, opts) => {
|
|
3437
|
+
if (opts.status !== void 0 && opts.status !== "active" && opts.status !== "inactive") {
|
|
3438
|
+
process.stderr.write('Error: --status must be "active" or "inactive".\n');
|
|
3439
|
+
process.exit(2);
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
2819
3442
|
const client = getClient();
|
|
2820
3443
|
const body = {};
|
|
2821
3444
|
if (opts.endpoint !== void 0) body["endpoint"] = opts.endpoint;
|
|
@@ -2917,7 +3540,7 @@ function registerUserCommands(program, getClient) {
|
|
|
2917
3540
|
const body = {};
|
|
2918
3541
|
if (opts.username !== void 0) body["username"] = opts.username;
|
|
2919
3542
|
if (opts.admin !== void 0) body["admin"] = opts.admin === "true";
|
|
2920
|
-
if (opts.customRoleId !== void 0) body["custom_role_id"] =
|
|
3543
|
+
if (opts.customRoleId !== void 0) body["custom_role_id"] = parseIntStrict(opts.customRoleId, "--custom-role-id");
|
|
2921
3544
|
await client.put(`/team/${workspaceId}/user/${userId}`, body);
|
|
2922
3545
|
process.stdout.write(`Updated user ${userId}
|
|
2923
3546
|
`);
|
|
@@ -3006,7 +3629,7 @@ function registerGroupCommands(program, getClient) {
|
|
|
3006
3629
|
const client = getClient();
|
|
3007
3630
|
const body = { name: opts.name };
|
|
3008
3631
|
if (opts.memberId.length) {
|
|
3009
|
-
body["members"] = opts.memberId.map((id) => ({ id:
|
|
3632
|
+
body["members"] = opts.memberId.map((id) => ({ id: parseIntStrict(id, "--member-id") }));
|
|
3010
3633
|
}
|
|
3011
3634
|
const data = await client.post(`/team/${workspaceId}/group`, body);
|
|
3012
3635
|
process.stdout.write(`Created group ${data["id"] ?? ""}
|
|
@@ -3017,8 +3640,8 @@ function registerGroupCommands(program, getClient) {
|
|
|
3017
3640
|
const body = {};
|
|
3018
3641
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
3019
3642
|
const members = {};
|
|
3020
|
-
if (opts.addMember.length) members["add"] = opts.addMember.map((id) => ({ id:
|
|
3021
|
-
if (opts.removeMember.length) members["rem"] = opts.removeMember.map((id) => ({ id:
|
|
3643
|
+
if (opts.addMember.length) members["add"] = opts.addMember.map((id) => ({ id: parseIntStrict(id, "--add-member") }));
|
|
3644
|
+
if (opts.removeMember.length) members["rem"] = opts.removeMember.map((id) => ({ id: parseIntStrict(id, "--remove-member") }));
|
|
3022
3645
|
if (Object.keys(members).length) body["members"] = members;
|
|
3023
3646
|
await client.put(`/group/${groupId}`, body);
|
|
3024
3647
|
process.stdout.write(`Updated group ${groupId}
|
|
@@ -3069,6 +3692,15 @@ function parseBool(val) {
|
|
|
3069
3692
|
if (val === void 0) return void 0;
|
|
3070
3693
|
return val === "true";
|
|
3071
3694
|
}
|
|
3695
|
+
var PERMISSION_LEVELS = ["read", "comment", "edit", "create"];
|
|
3696
|
+
function requirePermission(level) {
|
|
3697
|
+
if (!PERMISSION_LEVELS.includes(level)) {
|
|
3698
|
+
process.stderr.write(`Error: --permission must be one of: ${PERMISSION_LEVELS.join(", ")}
|
|
3699
|
+
`);
|
|
3700
|
+
process.exit(2);
|
|
3701
|
+
}
|
|
3702
|
+
return level;
|
|
3703
|
+
}
|
|
3072
3704
|
registerSchema("guest", "invite", "Invite a guest to a workspace", [
|
|
3073
3705
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
|
|
3074
3706
|
{ flag: "--email", type: "string", required: true, description: "Email address to invite" },
|
|
@@ -3183,7 +3815,7 @@ function registerGuestCommands(program, getClient) {
|
|
|
3183
3815
|
});
|
|
3184
3816
|
guest.command("add-to-task").description("Add a guest to a task").argument("<guest-id>", "Guest ID").requiredOption("--task-id <id>", "Task ID").requiredOption("--permission <level>", "Permission level (read, comment, edit, create)").action(async (guestId, opts) => {
|
|
3185
3817
|
const client = getClient();
|
|
3186
|
-
await client.post(`/task/${opts.taskId}/guest/${guestId}`, { permission_level: opts.permission });
|
|
3818
|
+
await client.post(`/task/${opts.taskId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
|
|
3187
3819
|
process.stdout.write(`Added guest ${guestId} to task ${opts.taskId}
|
|
3188
3820
|
`);
|
|
3189
3821
|
});
|
|
@@ -3195,7 +3827,7 @@ function registerGuestCommands(program, getClient) {
|
|
|
3195
3827
|
});
|
|
3196
3828
|
guest.command("add-to-list").description("Add a guest to a list").argument("<guest-id>", "Guest ID").requiredOption("--list-id <id>", "List ID").requiredOption("--permission <level>", "Permission level (read, comment, edit, create)").action(async (guestId, opts) => {
|
|
3197
3829
|
const client = getClient();
|
|
3198
|
-
await client.post(`/list/${opts.listId}/guest/${guestId}`, { permission_level: opts.permission });
|
|
3830
|
+
await client.post(`/list/${opts.listId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
|
|
3199
3831
|
process.stdout.write(`Added guest ${guestId} to list ${opts.listId}
|
|
3200
3832
|
`);
|
|
3201
3833
|
});
|
|
@@ -3207,7 +3839,7 @@ function registerGuestCommands(program, getClient) {
|
|
|
3207
3839
|
});
|
|
3208
3840
|
guest.command("add-to-folder").description("Add a guest to a folder").argument("<guest-id>", "Guest ID").requiredOption("--folder-id <id>", "Folder ID").requiredOption("--permission <level>", "Permission level (read, comment, edit, create)").action(async (guestId, opts) => {
|
|
3209
3841
|
const client = getClient();
|
|
3210
|
-
await client.post(`/folder/${opts.folderId}/guest/${guestId}`, { permission_level: opts.permission });
|
|
3842
|
+
await client.post(`/folder/${opts.folderId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
|
|
3211
3843
|
process.stdout.write(`Added guest ${guestId} to folder ${opts.folderId}
|
|
3212
3844
|
`);
|
|
3213
3845
|
});
|
|
@@ -3373,9 +4005,25 @@ registerSchema("template", "list", "List task templates in a workspace", [
|
|
|
3373
4005
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
|
|
3374
4006
|
{ flag: "--page", type: "integer", required: false, description: "Page number (starts at 0)" }
|
|
3375
4007
|
]);
|
|
4008
|
+
registerSchema("template", "apply-task", "Create a task from a task template", [
|
|
4009
|
+
{ flag: "--list-id", type: "string", required: true, description: "List to create the task in" },
|
|
4010
|
+
{ flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
|
|
4011
|
+
{ flag: "--name", type: "string", required: false, description: "Override the template name" }
|
|
4012
|
+
]);
|
|
4013
|
+
registerSchema("template", "apply-list", "Create a list from a list template", [
|
|
4014
|
+
{ flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
|
|
4015
|
+
{ flag: "--folder-id", type: "string", required: false, description: "Folder to create the list in (use this or --space-id)" },
|
|
4016
|
+
{ flag: "--space-id", type: "string", required: false, description: "Space to create the list in (use this or --folder-id)" },
|
|
4017
|
+
{ flag: "--name", type: "string", required: false, description: "Override the template name" }
|
|
4018
|
+
]);
|
|
4019
|
+
registerSchema("template", "apply-folder", "Create a folder from a folder template", [
|
|
4020
|
+
{ flag: "--space-id", type: "string", required: true, description: "Space to create the folder in" },
|
|
4021
|
+
{ flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
|
|
4022
|
+
{ flag: "--name", type: "string", required: false, description: "Override the template name" }
|
|
4023
|
+
]);
|
|
3376
4024
|
function registerTemplateCommands(program, getClient) {
|
|
3377
4025
|
const template = program.command("template").description("Manage task templates");
|
|
3378
|
-
template.command("list").description("List task templates").option("--page <n>", "Page number (starts at 0)",
|
|
4026
|
+
template.command("list").description("List task templates").option("--page <n>", "Page number (starts at 0)", intArg("--page")).action(async (opts) => {
|
|
3379
4027
|
const workspaceId = requireWorkspaceId11(program);
|
|
3380
4028
|
if (!workspaceId) return;
|
|
3381
4029
|
const client = getClient();
|
|
@@ -3384,6 +4032,39 @@ function registerTemplateCommands(program, getClient) {
|
|
|
3384
4032
|
const data = await client.get(`/team/${workspaceId}/taskTemplate`, params);
|
|
3385
4033
|
formatOutput(data.templates, TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
3386
4034
|
});
|
|
4035
|
+
template.command("apply-task").description("Create a task from a task template").requiredOption("--list-id <id>", "List to create the task in").requiredOption("--template-id <id>", "Template ID to apply").option("--name <name>", "Override the template name").action(async (opts) => {
|
|
4036
|
+
const client = getClient();
|
|
4037
|
+
const body = {};
|
|
4038
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
4039
|
+
const data = await client.post(
|
|
4040
|
+
`/list/${opts.listId}/taskTemplate/${opts.templateId}`,
|
|
4041
|
+
body
|
|
4042
|
+
);
|
|
4043
|
+
formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
4044
|
+
});
|
|
4045
|
+
template.command("apply-list").description("Create a list from a list template").requiredOption("--template-id <id>", "Template ID to apply").option("--folder-id <id>", "Folder to create the list in").option("--space-id <id>", "Space to create the list in (folderless)").option("--name <name>", "Override the template name").action(async (opts) => {
|
|
4046
|
+
if (!opts.folderId && !opts.spaceId) {
|
|
4047
|
+
process.stderr.write("Error: Provide either --folder-id or --space-id\n");
|
|
4048
|
+
process.exit(2);
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
const client = getClient();
|
|
4052
|
+
const body = {};
|
|
4053
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
4054
|
+
const path = opts.folderId ? `/folder/${opts.folderId}/listTemplate/${opts.templateId}` : `/space/${opts.spaceId}/listTemplate/${opts.templateId}`;
|
|
4055
|
+
const data = await client.post(path, body);
|
|
4056
|
+
formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
4057
|
+
});
|
|
4058
|
+
template.command("apply-folder").description("Create a folder from a folder template").requiredOption("--space-id <id>", "Space to create the folder in").requiredOption("--template-id <id>", "Template ID to apply").option("--name <name>", "Override the template name").action(async (opts) => {
|
|
4059
|
+
const client = getClient();
|
|
4060
|
+
const body = {};
|
|
4061
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
4062
|
+
const data = await client.post(
|
|
4063
|
+
`/space/${opts.spaceId}/folderTemplate/${opts.templateId}`,
|
|
4064
|
+
body
|
|
4065
|
+
);
|
|
4066
|
+
formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
4067
|
+
});
|
|
3387
4068
|
}
|
|
3388
4069
|
|
|
3389
4070
|
// src/commands/task-type.ts
|
|
@@ -3631,7 +4312,7 @@ function registerDocCommands(program, getClient) {
|
|
|
3631
4312
|
const data = await client.get(`/v3/workspaces/${workspaceId}/docs/${opts.docId}`);
|
|
3632
4313
|
formatOutput(data, DOC_COLUMNS, getOutputOptions(program));
|
|
3633
4314
|
});
|
|
3634
|
-
doc.command("create").description("Create a doc").requiredOption("--name <name>", "Doc name").option("--parent-id <id>", "Parent ID").option("--parent-type <type>", "Parent type (4=space, 5=folder, 6=list, 7=task)",
|
|
4315
|
+
doc.command("create").description("Create a doc").requiredOption("--name <name>", "Doc name").option("--parent-id <id>", "Parent ID").option("--parent-type <type>", "Parent type (4=space, 5=folder, 6=list, 7=task)", enumIntArg("--parent-type", [4, 5, 6, 7])).option("--visibility <visibility>", "Visibility (private, workspace)").action(async (opts) => {
|
|
3635
4316
|
const workspaceId = requireWorkspaceId14(program);
|
|
3636
4317
|
if (!workspaceId) return;
|
|
3637
4318
|
const client = getClient();
|
|
@@ -3729,8 +4410,8 @@ function registerDocCommands(program, getClient) {
|
|
|
3729
4410
|
}
|
|
3730
4411
|
|
|
3731
4412
|
// src/commands/skill-cmd.ts
|
|
3732
|
-
import { readdirSync, readFileSync as
|
|
3733
|
-
import { join, dirname } from "path";
|
|
4413
|
+
import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
4414
|
+
import { join as join2, dirname } from "path";
|
|
3734
4415
|
import { fileURLToPath } from "url";
|
|
3735
4416
|
var SKILL_COLUMNS = [
|
|
3736
4417
|
{ key: "name", header: "Name", width: 28 },
|
|
@@ -3746,10 +4427,10 @@ registerSchema("skill", "path", "Print the file system path to a skill directory
|
|
|
3746
4427
|
]);
|
|
3747
4428
|
function findSkillsDir() {
|
|
3748
4429
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
3749
|
-
const bundledDir =
|
|
3750
|
-
if (
|
|
3751
|
-
const projectDir =
|
|
3752
|
-
if (
|
|
4430
|
+
const bundledDir = join2(thisDir, "..", "skills");
|
|
4431
|
+
if (existsSync3(bundledDir)) return bundledDir;
|
|
4432
|
+
const projectDir = join2(thisDir, "..", "..", "skills");
|
|
4433
|
+
if (existsSync3(projectDir)) return projectDir;
|
|
3753
4434
|
return void 0;
|
|
3754
4435
|
}
|
|
3755
4436
|
function parseFrontmatter(content) {
|
|
@@ -3789,16 +4470,16 @@ function loadSkills(skillsDir) {
|
|
|
3789
4470
|
return skills;
|
|
3790
4471
|
}
|
|
3791
4472
|
for (const entry of entries) {
|
|
3792
|
-
const skillFile =
|
|
3793
|
-
if (!
|
|
4473
|
+
const skillFile = join2(skillsDir, entry, "SKILL.md");
|
|
4474
|
+
if (!existsSync3(skillFile)) continue;
|
|
3794
4475
|
try {
|
|
3795
|
-
const content =
|
|
4476
|
+
const content = readFileSync4(skillFile, "utf-8");
|
|
3796
4477
|
const { frontmatter } = parseFrontmatter(content);
|
|
3797
4478
|
skills.push({
|
|
3798
4479
|
name: frontmatter["name"] || entry,
|
|
3799
4480
|
description: (frontmatter["description"] || "").slice(0, 100),
|
|
3800
4481
|
type: classifySkill(frontmatter),
|
|
3801
|
-
path:
|
|
4482
|
+
path: join2(skillsDir, entry),
|
|
3802
4483
|
frontmatter
|
|
3803
4484
|
});
|
|
3804
4485
|
} catch {
|
|
@@ -3813,11 +4494,11 @@ function loadSkills(skillsDir) {
|
|
|
3813
4494
|
});
|
|
3814
4495
|
}
|
|
3815
4496
|
function findSkill(skillsDir, name) {
|
|
3816
|
-
const skillDir =
|
|
3817
|
-
const skillFile =
|
|
3818
|
-
if (!
|
|
4497
|
+
const skillDir = join2(skillsDir, name);
|
|
4498
|
+
const skillFile = join2(skillDir, "SKILL.md");
|
|
4499
|
+
if (!existsSync3(skillFile)) return void 0;
|
|
3819
4500
|
try {
|
|
3820
|
-
const content =
|
|
4501
|
+
const content = readFileSync4(skillFile, "utf-8");
|
|
3821
4502
|
return { skillDir, content };
|
|
3822
4503
|
} catch {
|
|
3823
4504
|
return void 0;
|
|
@@ -3945,15 +4626,20 @@ function registerChatCommands(program, getClient) {
|
|
|
3945
4626
|
}
|
|
3946
4627
|
|
|
3947
4628
|
// src/cli.ts
|
|
3948
|
-
var VERSION = "0.
|
|
4629
|
+
var VERSION = "0.4.0";
|
|
3949
4630
|
function createProgram() {
|
|
3950
4631
|
const program = new Command();
|
|
3951
|
-
program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").option("--
|
|
4632
|
+
program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").option("--token-file <path>", "Read API token from this file path").option("--profile <name>", "Profile to use (key, workspace name, or nickname)").option("--workspace-id <id>", "Workspace ID").addOption(
|
|
4633
|
+
new Option("--format <format>", "Output format").choices(["table", "json", "csv", "tsv", "quiet", "id", "md"])
|
|
4634
|
+
).option("--no-color", "Disable colors").option("--no-header", "Omit column headers").option("--fields <fields>", "Show only specified fields (comma-separated)").option("--filter <filter>", "Client-side filter (key=value)").option("--sort <sort>", "Sort by field (field[:asc|:desc])").option("--limit <n>", "Limit results", intArg("--limit")).option("--verbose", "Show request details").option("--debug", "Full debug output").option("--dry-run", "Print what would be sent without executing");
|
|
3952
4635
|
return program;
|
|
3953
4636
|
}
|
|
3954
4637
|
function createClient(program) {
|
|
3955
4638
|
const globalOpts = program.opts();
|
|
3956
|
-
const token = resolveToken(
|
|
4639
|
+
const token = resolveToken(
|
|
4640
|
+
globalOpts["token"],
|
|
4641
|
+
globalOpts["tokenFile"]
|
|
4642
|
+
);
|
|
3957
4643
|
if (!token) {
|
|
3958
4644
|
process.stderr.write("Error: No API token found. Run: clickup auth login\n");
|
|
3959
4645
|
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
@@ -3982,9 +4668,15 @@ function getOutputOptions(program) {
|
|
|
3982
4668
|
return result;
|
|
3983
4669
|
}
|
|
3984
4670
|
function run() {
|
|
4671
|
+
process.stdout.on("error", (err) => {
|
|
4672
|
+
if (err.code === "EPIPE") process.exit(EXIT_CODES.SUCCESS);
|
|
4673
|
+
});
|
|
3985
4674
|
const program = createProgram();
|
|
4675
|
+
program.hook("preAction", () => {
|
|
4676
|
+
setProfileOverride(program.opts()["profile"]);
|
|
4677
|
+
});
|
|
3986
4678
|
registerAuthCommands(program, () => createClient(program));
|
|
3987
|
-
registerConfigCommands(program);
|
|
4679
|
+
registerConfigCommands(program, () => createClient(program));
|
|
3988
4680
|
registerWorkspaceCommands(program, () => createClient(program));
|
|
3989
4681
|
registerSpaceCommands(program, () => createClient(program));
|
|
3990
4682
|
registerFolderCommands(program, () => createClient(program));
|
|
@@ -4014,6 +4706,9 @@ function run() {
|
|
|
4014
4706
|
registerSchemaCommands(program);
|
|
4015
4707
|
registerSkillCommands(program);
|
|
4016
4708
|
program.parseAsync(process.argv).catch((error) => {
|
|
4709
|
+
if (error instanceof DryRunComplete) {
|
|
4710
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
4711
|
+
}
|
|
4017
4712
|
if (error instanceof ClickUpError) {
|
|
4018
4713
|
process.stderr.write(`Error: ${error.message}
|
|
4019
4714
|
`);
|