@krodak/clickup-cli 1.44.0 → 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ backfillAttachments,
4
+ formatDate,
5
+ formatDateISO,
6
+ formatDuration,
7
+ formatLongDuration,
8
+ formatTimestamp,
9
+ readBundleData,
10
+ renderBundleMarkdown,
11
+ writeBundleData
12
+ } from "./chunk-OPPKVJ6P.js";
2
13
 
3
14
  // src/index.ts
4
15
  import { realpathSync as realpathSync2 } from "fs";
5
- import { basename, resolve } from "path";
16
+ import { basename, resolve as resolve2 } from "path";
6
17
  import { Command } from "commander";
7
18
  import { createRequire } from "module";
8
19
  import { fileURLToPath } from "url";
@@ -81,10 +92,12 @@ function normalizeViewId(input) {
81
92
  var ClickUpClient = class {
82
93
  apiToken;
83
94
  teamId;
95
+ rateLimiter;
84
96
  meCache = null;
85
97
  constructor(config) {
86
98
  this.apiToken = config.apiToken;
87
99
  this.teamId = config.teamId;
100
+ this.rateLimiter = config.rateLimiter;
88
101
  }
89
102
  taskPath(taskId, suffix = "") {
90
103
  const normalized = normalizeTaskId(taskId);
@@ -102,7 +115,7 @@ var ClickUpClient = class {
102
115
  return "";
103
116
  }
104
117
  sleep(ms) {
105
- return new Promise((resolve2) => setTimeout(resolve2, ms));
118
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
106
119
  }
107
120
  retryDelayMs(res, attempt) {
108
121
  const retryAfter = res.headers.get("retry-after");
@@ -118,9 +131,11 @@ var ClickUpClient = class {
118
131
  const maxRetries = 3;
119
132
  let attempt = 0;
120
133
  for (; ; ) {
134
+ await this.rateLimiter?.acquire();
121
135
  const res = await fetch(url, { ...init, signal: AbortSignal.timeout(3e4) });
122
136
  const retryable = res.status === 429 || res.status === 502 || res.status === 503 || res.status === 504;
123
137
  if (!retryable || attempt >= maxRetries) return res;
138
+ if (res.status === 429) this.rateLimiter?.penalize();
124
139
  attempt++;
125
140
  const delayMs = this.retryDelayMs(res, attempt);
126
141
  process.stderr.write(
@@ -236,6 +251,7 @@ var ClickUpClient = class {
236
251
  subtasks: String(filters.subtasks ?? true)
237
252
  });
238
253
  if (filters.includeClosed) baseParams.set("include_closed", "true");
254
+ if (filters.archived) baseParams.set("archived", "true");
239
255
  if (!filters.all) {
240
256
  const me = await this.getMe();
241
257
  baseParams.append("assignees[]", String(me.id));
@@ -280,10 +296,40 @@ var ClickUpClient = class {
280
296
  const data = await this.request(this.taskPath(taskId, "/comment"));
281
297
  return readCollectionField(data, "comments", "task comments");
282
298
  }
299
+ /**
300
+ * Every comment on a task. ClickUp returns 25 per call and pages with
301
+ * `start` (the oldest returned comment's date) + `start_id`; the cursor
302
+ * comment is repeated on the next page, so results are deduped by id.
303
+ */
304
+ async getAllTaskComments(taskId) {
305
+ const PAGE_SIZE = 25;
306
+ const seen = /* @__PURE__ */ new Set();
307
+ const all = [];
308
+ let cursor;
309
+ for (; ; ) {
310
+ const qs = cursor ? `?start=${encodeURIComponent(cursor.start)}&start_id=${encodeURIComponent(cursor.startId)}` : "";
311
+ const data = await this.request(
312
+ this.taskPath(taskId, `/comment${qs}`)
313
+ );
314
+ const page = readCollectionField(data, "comments", "task comments");
315
+ let added = 0;
316
+ for (const c of page) {
317
+ if (seen.has(c.id)) continue;
318
+ seen.add(c.id);
319
+ all.push(c);
320
+ added++;
321
+ }
322
+ if (page.length < PAGE_SIZE || added === 0) break;
323
+ const last = page[page.length - 1];
324
+ cursor = { start: last.date, startId: last.id };
325
+ }
326
+ return all;
327
+ }
283
328
  async getTasksFromList(listId, params = {}, options = {}) {
284
329
  return this.paginate((page) => {
285
330
  const base = { subtasks: "true", page: String(page), ...params };
286
331
  if (options.includeClosed) base["include_closed"] = "true";
332
+ if (options.archived) base["archived"] = "true";
287
333
  const qs = new URLSearchParams(base).toString();
288
334
  return `/list/${listId}/task?${qs}`;
289
335
  });
@@ -291,6 +337,12 @@ var ClickUpClient = class {
291
337
  async getTask(taskId) {
292
338
  return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
293
339
  }
340
+ /** Full task for archival: markdown description plus the direct subtask list. */
341
+ async getTaskForExport(taskId) {
342
+ return this.request(
343
+ this.taskPath(taskId, "?include_markdown_description=true&include_subtasks=true")
344
+ );
345
+ }
294
346
  /**
295
347
  * Resolve any accepted task-id form to a native ClickUp task id.
296
348
  * - Task URLs are reduced to their id segment.
@@ -764,6 +816,23 @@ var ClickUpClient = class {
764
816
  const data = await this.requestV3(`/workspaces/${workspaceId}/docs`);
765
817
  return readCollectionField(data, "docs", "docs");
766
818
  }
819
+ /** Every doc in the workspace, following v3 `next_cursor` pagination. */
820
+ async getAllDocs(workspaceId, options = {}) {
821
+ const all = [];
822
+ let cursor;
823
+ for (; ; ) {
824
+ const params = new URLSearchParams({ limit: "50" });
825
+ if (options.archived) params.set("archived", "true");
826
+ if (cursor) params.set("next_cursor", cursor);
827
+ const data = await this.requestV3(
828
+ `/workspaces/${workspaceId}/docs?${params.toString()}`
829
+ );
830
+ all.push(...readCollectionField(data, "docs", "docs"));
831
+ if (!data.next_cursor) break;
832
+ cursor = data.next_cursor;
833
+ }
834
+ return all;
835
+ }
767
836
  async getDocPage(workspaceId, docId, pageId) {
768
837
  return this.requestV3(
769
838
  `/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}?content_format=text/md`
@@ -1562,50 +1631,6 @@ function writeConfig(config, profileName) {
1562
1631
  saveMultiProfileConfig(multi);
1563
1632
  }
1564
1633
 
1565
- // src/date.ts
1566
- function formatDate(ms) {
1567
- return new Date(Number(ms)).toLocaleDateString("en-US", {
1568
- month: "short",
1569
- day: "numeric",
1570
- year: "numeric"
1571
- });
1572
- }
1573
- function formatTimestamp(ms) {
1574
- return new Date(Number(ms)).toLocaleString("en-US", {
1575
- month: "short",
1576
- day: "numeric",
1577
- hour: "numeric",
1578
- minute: "2-digit"
1579
- });
1580
- }
1581
- function formatDuration(ms) {
1582
- const totalMinutes = Math.round(Math.abs(ms) / 6e4);
1583
- const hours = Math.floor(totalMinutes / 60);
1584
- const minutes = totalMinutes % 60;
1585
- if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
1586
- if (hours > 0) return `${hours}h`;
1587
- return `${minutes}m`;
1588
- }
1589
- function formatLongDuration(ms) {
1590
- const totalMinutes = Math.round(Math.abs(ms) / 6e4);
1591
- if (totalMinutes === 0) return "< 1m";
1592
- const days = Math.floor(totalMinutes / 1440);
1593
- const hours = Math.floor(totalMinutes % 1440 / 60);
1594
- const minutes = totalMinutes % 60;
1595
- const parts = [];
1596
- if (days > 0) parts.push(`${days}d`);
1597
- if (hours > 0) parts.push(`${hours}h`);
1598
- if (minutes > 0) parts.push(`${minutes}m`);
1599
- return parts.join(" ");
1600
- }
1601
- function formatDateISO(ms) {
1602
- const d = new Date(Number(ms));
1603
- const year = d.getUTCFullYear();
1604
- const month = String(d.getUTCMonth() + 1).padStart(2, "0");
1605
- const day = String(d.getUTCDate()).padStart(2, "0");
1606
- return `${year}-${month}-${day}`;
1607
- }
1608
-
1609
1634
  // src/output.ts
1610
1635
  import chalk from "chalk";
1611
1636
  function isTTY() {
@@ -4280,6 +4305,42 @@ var commandMetadata = [
4280
4305
  }
4281
4306
  ]
4282
4307
  },
4308
+ {
4309
+ name: "export",
4310
+ description: "Export tasks and docs to a local archive (lossless JSON + rendered markdown)",
4311
+ quickReference: [
4312
+ {
4313
+ section: "read",
4314
+ usage: "export user <userRef>",
4315
+ description: "Export all tasks assigned to a user (me, id, email; incl. closed, archived)"
4316
+ },
4317
+ {
4318
+ section: "read",
4319
+ usage: "export team <spaceRef>",
4320
+ description: "Export every list in a space (id or name), incl. archived"
4321
+ },
4322
+ {
4323
+ section: "read",
4324
+ usage: "export roadmap <listId>",
4325
+ description: "Export a list with initiatives grouped (--item-id) and subtask trees"
4326
+ },
4327
+ {
4328
+ section: "read",
4329
+ usage: "export initiatives <listId>",
4330
+ description: "Export only initiative-typed tasks (--item-id) plus their subtask trees"
4331
+ },
4332
+ {
4333
+ section: "read",
4334
+ usage: "export docs",
4335
+ description: "Export every workspace doc as markdown page trees"
4336
+ },
4337
+ {
4338
+ section: "read",
4339
+ usage: "export all",
4340
+ description: "Export the whole workspace (plans, confirms; --yes for non-interactive)"
4341
+ }
4342
+ ]
4343
+ },
4283
4344
  {
4284
4345
  name: "attach-get",
4285
4346
  description: "Download task attachment(s) by ID or title",
@@ -6371,8 +6432,8 @@ async function installSkillInteractive() {
6371
6432
  const source = skillPath();
6372
6433
  const installed = [];
6373
6434
  if (isTTY()) {
6374
- const { checkbox: checkbox2 } = await import("@inquirer/prompts");
6375
- const selected = await checkbox2({
6435
+ const { checkbox: checkbox3 } = await import("@inquirer/prompts");
6436
+ const selected = await checkbox3({
6376
6437
  message: "Install skill for which agents?",
6377
6438
  choices: targets.map((t) => ({
6378
6439
  name: `${t.name}${t.detected ? chalk8.dim(" (detected)") : ""}`,
@@ -7163,7 +7224,7 @@ async function downloadAttachment(attachment, targetPath, force) {
7163
7224
  return { title: attachment.title, path: targetPath, size: buffer.length };
7164
7225
  }
7165
7226
  async function attachGet(config, taskId, selector, opts) {
7166
- const { resolve: resolve2 } = await import("path");
7227
+ const { resolve: resolve3 } = await import("path");
7167
7228
  const client = new ClickUpClient(config);
7168
7229
  const attachments = await client.getTaskAttachments(taskId);
7169
7230
  const selected = selectAttachments(attachments, selector, opts.all ?? false);
@@ -7171,16 +7232,1086 @@ async function attachGet(config, taskId, selector, opts) {
7171
7232
  for (const att of selected) {
7172
7233
  let targetPath;
7173
7234
  if (opts.output && !opts.all) {
7174
- targetPath = resolve2(opts.output);
7235
+ targetPath = resolve3(opts.output);
7175
7236
  } else {
7176
7237
  const dir = opts.dir ?? ".";
7177
- targetPath = resolve2(dir, sanitizeFilename(att.title));
7238
+ targetPath = resolve3(dir, sanitizeFilename(att.title));
7178
7239
  }
7179
7240
  results.push(await downloadAttachment(att, targetPath, opts.force ?? false));
7180
7241
  }
7181
7242
  return results;
7182
7243
  }
7183
7244
 
7245
+ // src/commands/export.ts
7246
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
7247
+ import { join as join6, resolve } from "path";
7248
+
7249
+ // src/export/discover.ts
7250
+ function slug(name) {
7251
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "unnamed";
7252
+ }
7253
+ async function resolveUserRef(client, teamId, ref) {
7254
+ if (ref === "me") {
7255
+ const me = await client.getMe();
7256
+ return { id: me.id, username: me.username };
7257
+ }
7258
+ const members = await client.getWorkspaceMembers(teamId);
7259
+ const needle = ref.trim().toLowerCase();
7260
+ const match = members.find(
7261
+ (m) => String(m.id) === needle || m.username?.toLowerCase() === needle || m.email?.toLowerCase() === needle
7262
+ );
7263
+ if (!match) {
7264
+ const available = members.map((m) => m.username ?? m.email ?? String(m.id)).join(", ");
7265
+ throw new Error(`User "${ref}" not found. Available: ${available}`);
7266
+ }
7267
+ return { id: match.id, username: match.username ?? match.email ?? String(match.id) };
7268
+ }
7269
+ async function workspaceInfo(client, teamId) {
7270
+ const teams = await client.getTeams();
7271
+ const team = teams.find((t) => t.id === teamId);
7272
+ return { id: teamId, name: team?.name ?? teamId };
7273
+ }
7274
+ function toDiscovered(task) {
7275
+ return {
7276
+ id: task.id,
7277
+ listId: task.list.id,
7278
+ initiative: (task.custom_item_id ?? 0) !== 0
7279
+ };
7280
+ }
7281
+ function dedupe(tasks) {
7282
+ const seen = /* @__PURE__ */ new Set();
7283
+ return tasks.filter((t) => seen.has(t.id) ? false : (seen.add(t.id), true));
7284
+ }
7285
+ async function discoverUserTasks(client, teamId, userRef) {
7286
+ const user = await resolveUserRef(client, teamId, userRef);
7287
+ const base = { all: true, assignees: [user.id], includeClosed: true, subtasks: true };
7288
+ const [active, archived] = await Promise.all([
7289
+ client.getMyTasks(teamId, base),
7290
+ client.getMyTasks(teamId, { ...base, archived: true })
7291
+ ]);
7292
+ const all = dedupe([...active, ...archived]);
7293
+ return {
7294
+ slice: { name: `user-${slug(user.username)}`, kind: "user", scope: String(user.id) },
7295
+ tasks: all.map(toDiscovered),
7296
+ workspace: await workspaceInfo(client, teamId),
7297
+ tasksById: Object.fromEntries(all.map((t) => [t.id, t]))
7298
+ };
7299
+ }
7300
+ async function resolveSpaceRef(client, teamId, ref) {
7301
+ const spaces = await client.getSpaces(teamId);
7302
+ const needle = ref.trim().toLowerCase();
7303
+ const match = spaces.find((s) => s.id === ref.trim() || s.name.toLowerCase() === needle);
7304
+ if (!match) {
7305
+ throw new Error(`Space "${ref}" not found. Available: ${spaces.map((s) => s.name).join(", ")}`);
7306
+ }
7307
+ return { id: match.id, name: match.name };
7308
+ }
7309
+ async function listTasks(client, listId) {
7310
+ const [active, archived] = await Promise.all([
7311
+ client.getTasksFromList(listId, {}, { includeClosed: true }),
7312
+ client.getTasksFromList(listId, {}, { includeClosed: true, archived: true })
7313
+ ]);
7314
+ return dedupe([...active, ...archived]);
7315
+ }
7316
+ async function union(active, archived) {
7317
+ const [a, b] = await Promise.all([active, archived]);
7318
+ const seen = /* @__PURE__ */ new Set();
7319
+ return [...a, ...b].filter((x) => seen.has(x.id) ? false : (seen.add(x.id), true));
7320
+ }
7321
+ async function walkSpace(client, spaceId, spaceName) {
7322
+ const [lists, folders] = await Promise.all([
7323
+ union(client.getLists(spaceId, false), client.getLists(spaceId, true)),
7324
+ union(client.getFolders(spaceId, false), client.getFolders(spaceId, true))
7325
+ ]);
7326
+ const folderEntries = await Promise.all(
7327
+ folders.map(async (f) => ({
7328
+ id: f.id,
7329
+ name: f.name,
7330
+ lists: (await union(client.getFolderLists(f.id, false), client.getFolderLists(f.id, true))).map((l) => ({ id: l.id, name: l.name }))
7331
+ }))
7332
+ );
7333
+ const hierarchy = {
7334
+ space: { id: spaceId, name: spaceName },
7335
+ folders: folderEntries,
7336
+ lists: lists.map((l) => ({ id: l.id, name: l.name }))
7337
+ };
7338
+ return hierarchy;
7339
+ }
7340
+ async function discoverTeamTasks(client, teamId, spaceRef) {
7341
+ const space = await resolveSpaceRef(client, teamId, spaceRef);
7342
+ const hierarchy = await walkSpace(client, space.id, space.name);
7343
+ const listIds = [
7344
+ ...hierarchy.lists.map((l) => l.id),
7345
+ ...hierarchy.folders.flatMap((f) => f.lists.map((l) => l.id))
7346
+ ];
7347
+ const perList = await Promise.all(listIds.map((id) => listTasks(client, id)));
7348
+ const all = dedupe(perList.flat());
7349
+ return {
7350
+ slice: { name: `team-${slug(space.name)}`, kind: "team", scope: space.id },
7351
+ tasks: all.map(toDiscovered),
7352
+ workspace: await workspaceInfo(client, teamId),
7353
+ tasksById: Object.fromEntries(all.map((t) => [t.id, t])),
7354
+ hierarchy
7355
+ };
7356
+ }
7357
+ async function discoverListTasks(client, teamId, listId, opts) {
7358
+ const list = await client.getListWithStatuses(listId);
7359
+ let all = await listTasks(client, listId);
7360
+ if (opts.kind === "initiatives") {
7361
+ const itemId = opts.initiativeItemId;
7362
+ if (itemId === void 0) throw new Error("initiatives export requires an initiative item id");
7363
+ const matching = all.filter((t) => t.custom_item_id === itemId);
7364
+ if (matching.length === 0) {
7365
+ const found = [...new Set(all.map((t) => t.custom_item_id ?? 0).filter((id) => id !== 0))].sort((a, b) => a - b).join(", ");
7366
+ throw new Error(
7367
+ `No tasks with custom item id ${itemId} in list "${list.name}" (custom item ids found: ${found || "none"})`
7368
+ );
7369
+ }
7370
+ all = matching;
7371
+ }
7372
+ return {
7373
+ slice: { name: `${opts.kind}-${slug(list.name)}`, kind: opts.kind, scope: listId },
7374
+ tasks: all.map(toDiscovered),
7375
+ workspace: await workspaceInfo(client, teamId),
7376
+ tasksById: Object.fromEntries(all.map((t) => [t.id, t])),
7377
+ list: { id: list.id, name: list.name }
7378
+ };
7379
+ }
7380
+
7381
+ // src/util/batch.ts
7382
+ async function runInBatches(items, concurrency, fn) {
7383
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
7384
+ throw new Error(`concurrency must be a positive integer, got ${concurrency}`);
7385
+ }
7386
+ const results = [];
7387
+ for (let i = 0; i < items.length; i += concurrency) {
7388
+ const batch = items.slice(i, i + concurrency);
7389
+ const settled = await Promise.allSettled(batch.map(fn));
7390
+ settled.forEach((res, idx) => {
7391
+ const item = batch[idx];
7392
+ if (res.status === "fulfilled") {
7393
+ results.push({ item, ok: true, result: res.value });
7394
+ } else {
7395
+ const error = res.reason instanceof Error ? res.reason : new Error(String(res.reason), { cause: res.reason });
7396
+ results.push({ item, ok: false, error });
7397
+ }
7398
+ });
7399
+ }
7400
+ return results;
7401
+ }
7402
+
7403
+ // src/export/bundle.ts
7404
+ function replyCount(c) {
7405
+ const n = c.reply_count;
7406
+ return typeof n === "number" ? n : 0;
7407
+ }
7408
+ async function fetchTaskBundle(client, taskId) {
7409
+ const [task, rawComments] = await Promise.all([
7410
+ client.getTaskForExport(taskId),
7411
+ client.getAllTaskComments(taskId)
7412
+ ]);
7413
+ const comments = await Promise.all(
7414
+ rawComments.map(async (c) => ({
7415
+ ...c,
7416
+ replies: replyCount(c) > 0 ? await client.getThreadedComments(c.id) : []
7417
+ }))
7418
+ );
7419
+ return {
7420
+ task,
7421
+ comments,
7422
+ attachments: task.attachments ?? [],
7423
+ subtaskIds: (task.subtasks ?? []).map((s) => s.id),
7424
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
7425
+ };
7426
+ }
7427
+
7428
+ // src/export/manifest.ts
7429
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
7430
+ import { join as join3 } from "path";
7431
+ var MANIFEST_VERSION = 1;
7432
+ var FILE = "manifest.json";
7433
+ function emptyManifest() {
7434
+ return { version: MANIFEST_VERSION, workspace: void 0, tasks: {}, docs: {}, slices: {} };
7435
+ }
7436
+ function loadManifest(root) {
7437
+ const path = join3(root, FILE);
7438
+ if (!existsSync2(path)) return emptyManifest();
7439
+ let parsed;
7440
+ try {
7441
+ parsed = JSON.parse(readFileSync3(path, "utf8"));
7442
+ } catch (err) {
7443
+ throw new Error(`Cannot parse ${path}: ${err.message}`, { cause: err });
7444
+ }
7445
+ const m = parsed;
7446
+ if (m.version !== MANIFEST_VERSION) {
7447
+ throw new Error(
7448
+ `Unsupported manifest version ${String(m.version)} in ${path} (expected ${MANIFEST_VERSION})`
7449
+ );
7450
+ }
7451
+ return {
7452
+ version: MANIFEST_VERSION,
7453
+ workspace: m.workspace ?? void 0,
7454
+ tasks: m.tasks ?? {},
7455
+ docs: m.docs ?? {},
7456
+ slices: m.slices ?? {}
7457
+ };
7458
+ }
7459
+ function saveManifest(root, manifest) {
7460
+ mkdirSync2(root, { recursive: true });
7461
+ const path = join3(root, FILE);
7462
+ const tmp = `${path}.tmp`;
7463
+ writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
7464
+ renameSync(tmp, path);
7465
+ }
7466
+
7467
+ // src/util/plural.ts
7468
+ function plural(n, singular, pluralForm = `${singular}s`) {
7469
+ return `${n} ${n === 1 ? singular : pluralForm}`;
7470
+ }
7471
+
7472
+ // src/export/engine.ts
7473
+ function addSlice(manifest, taskId, slice) {
7474
+ const entry = manifest.tasks[taskId];
7475
+ if (entry && !entry.slices.includes(slice)) entry.slices.push(slice);
7476
+ }
7477
+ async function runExport(client, plan, opts) {
7478
+ const manifest = loadManifest(opts.root);
7479
+ manifest.workspace = plan.workspace;
7480
+ const sliceName = plan.slice.name;
7481
+ const checkpointEvery = opts.checkpointEvery ?? 25;
7482
+ const queue = [];
7483
+ const queued = /* @__PURE__ */ new Set();
7484
+ const enqueue = (id) => {
7485
+ if (!queued.has(id)) {
7486
+ queued.add(id);
7487
+ queue.push(id);
7488
+ }
7489
+ };
7490
+ for (const t of plan.tasks) enqueue(t.id);
7491
+ const summary = {
7492
+ fetched: 0,
7493
+ skipped: 0,
7494
+ failed: [],
7495
+ attachmentsDownloaded: 0,
7496
+ attachmentsFailed: 0
7497
+ };
7498
+ const touched = /* @__PURE__ */ new Set();
7499
+ let sinceCheckpoint = 0;
7500
+ while (queue.length > 0) {
7501
+ const batch = queue.splice(0, opts.concurrency);
7502
+ const toFetch = [];
7503
+ for (const id of batch) {
7504
+ touched.add(id);
7505
+ if (!opts.refresh && manifest.tasks[id]) {
7506
+ summary.skipped++;
7507
+ addSlice(manifest, id, sliceName);
7508
+ try {
7509
+ const cached = await readBundleData(opts.root, id);
7510
+ for (const s of cached.subtaskIds) enqueue(s);
7511
+ if (opts.downloadAttachments && cached.attachments.length > 0) {
7512
+ const bf = await backfillAttachments(opts.root, id, opts.download);
7513
+ summary.attachmentsDownloaded += bf.downloaded;
7514
+ summary.attachmentsFailed += bf.failed.length;
7515
+ }
7516
+ } catch {
7517
+ delete manifest.tasks[id];
7518
+ toFetch.push(id);
7519
+ summary.skipped--;
7520
+ }
7521
+ continue;
7522
+ }
7523
+ toFetch.push(id);
7524
+ }
7525
+ const outcomes = await runInBatches(toFetch, opts.concurrency, async (id) => {
7526
+ const bundle = await fetchTaskBundle(client, id);
7527
+ const written = await writeBundleData(opts.root, bundle, {
7528
+ downloadAttachments: opts.downloadAttachments,
7529
+ download: opts.download
7530
+ });
7531
+ return { bundle, written };
7532
+ });
7533
+ for (const o of outcomes) {
7534
+ if (!o.ok) {
7535
+ summary.failed.push({ id: o.item, error: o.error.message });
7536
+ continue;
7537
+ }
7538
+ const { bundle, written } = o.result;
7539
+ summary.fetched++;
7540
+ summary.attachmentsDownloaded += written.attachmentsDownloaded;
7541
+ summary.attachmentsFailed += written.attachmentsFailed.length;
7542
+ const existing = manifest.tasks[bundle.task.id];
7543
+ manifest.tasks[bundle.task.id] = {
7544
+ fetchedAt: bundle.fetchedAt,
7545
+ slices: existing?.slices.includes(sliceName) ? existing.slices : [...existing?.slices ?? [], sliceName],
7546
+ contentHash: written.contentHash
7547
+ };
7548
+ for (const s of bundle.subtaskIds) enqueue(s);
7549
+ sinceCheckpoint++;
7550
+ }
7551
+ const total = touched.size + queue.length;
7552
+ const done = summary.fetched + summary.skipped + summary.failed.length;
7553
+ opts.log(
7554
+ `[${sliceName}] ${done}/${plural(total, "task")} (${summary.fetched} fetched, ${summary.skipped} cached, ${summary.failed.length} failed)`
7555
+ );
7556
+ if (sinceCheckpoint >= checkpointEvery) {
7557
+ saveManifest(opts.root, manifest);
7558
+ sinceCheckpoint = 0;
7559
+ }
7560
+ }
7561
+ const known = new Set(Object.keys(manifest.tasks));
7562
+ const hasTask = (id) => known.has(id);
7563
+ const spaceNames = opts.spaceNames;
7564
+ const spaceName = spaceNames ? (id) => spaceNames[id] : void 0;
7565
+ const renderOutcomes = await runInBatches([...known], opts.concurrency * 2, async (id) => {
7566
+ const bundle = await readBundleData(opts.root, id);
7567
+ await renderBundleMarkdown(opts.root, bundle, hasTask, spaceName);
7568
+ });
7569
+ for (const o of renderOutcomes) {
7570
+ if (!o.ok) opts.log(`warning: could not render ${o.item}: ${o.error.message}`);
7571
+ }
7572
+ manifest.slices[sliceName] = {
7573
+ kind: plan.slice.kind,
7574
+ scope: plan.slice.scope,
7575
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
7576
+ taskCount: [...touched].filter((id) => manifest.tasks[id]).length
7577
+ };
7578
+ saveManifest(opts.root, manifest);
7579
+ return summary;
7580
+ }
7581
+
7582
+ // src/export/docs.ts
7583
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
7584
+ import { join as join4 } from "path";
7585
+ var PARENT_TYPES = {
7586
+ 7: "workspace",
7587
+ 4: "space",
7588
+ 6: "folder",
7589
+ 5: "list",
7590
+ 1: "task"
7591
+ };
7592
+ function docDirName(doc) {
7593
+ const s = doc.name ? slug(doc.name) : "";
7594
+ return s && s !== "unnamed" ? `${s}-${doc.id}` : doc.id;
7595
+ }
7596
+ function pageFileName(page) {
7597
+ const s = page.name ? slug(page.name) : "";
7598
+ return s && s !== "unnamed" ? `${s}-${page.id}.md` : `${page.id}.md`;
7599
+ }
7600
+ function pageDirName(page) {
7601
+ return pageFileName(page).replace(/\.md$/, "");
7602
+ }
7603
+ function yamlString(v) {
7604
+ return JSON.stringify(v);
7605
+ }
7606
+ function renderPage(page, docId) {
7607
+ const header = [
7608
+ "---",
7609
+ `id: ${page.id}`,
7610
+ `doc: ${docId}`,
7611
+ `title: ${page.name ? yamlString(page.name).slice(1, -1) : ""}`,
7612
+ ...page.parent_page_id ? [`parent: ${page.parent_page_id}`] : [],
7613
+ ...page.archived ? ["archived: true"] : [],
7614
+ ...page.date_created ? [`created: ${new Date(page.date_created).toISOString()}`] : [],
7615
+ ...page.date_updated ? [`updated: ${new Date(page.date_updated).toISOString()}`] : [],
7616
+ "---"
7617
+ ];
7618
+ return header.join("\n") + "\n\n" + (page.content ?? "") + "\n";
7619
+ }
7620
+ function writePages(dir, docId, pages, prefix = "", depth = 0) {
7621
+ const out = [];
7622
+ for (const page of pages) {
7623
+ const file = pageFileName(page);
7624
+ const relPath = prefix ? `${prefix}/${file}` : file;
7625
+ mkdirSync3(join4(dir, prefix), { recursive: true });
7626
+ writeFileSync2(join4(dir, relPath), renderPage(page, docId));
7627
+ out.push({ page, relPath, depth });
7628
+ if (page.pages?.length) {
7629
+ const childPrefix = prefix ? `${prefix}/${pageDirName(page)}` : pageDirName(page);
7630
+ out.push(...writePages(dir, docId, page.pages, childPrefix, depth + 1));
7631
+ }
7632
+ }
7633
+ return out;
7634
+ }
7635
+ function docTitle(doc) {
7636
+ return doc.name || `(unnamed ${doc.id})`;
7637
+ }
7638
+ function renderDocReadme(doc, written, location) {
7639
+ const lines = [
7640
+ `# ${docTitle(doc)}`,
7641
+ "",
7642
+ `Doc id: ${doc.id} \xB7 Location: ${location} \xB7 ${plural(written.length, "page")}`,
7643
+ ...doc.date_updated ? [`Last updated: ${new Date(Number(doc.date_updated)).toISOString()}`] : [],
7644
+ "",
7645
+ "## Pages",
7646
+ ""
7647
+ ];
7648
+ for (const w of written) {
7649
+ const name = w.page.name || `(untitled ${w.page.id})`;
7650
+ const flag = w.page.archived ? " (archived)" : "";
7651
+ lines.push(`${" ".repeat(w.depth)}- [${name}](${w.relPath})${flag}`);
7652
+ }
7653
+ lines.push("");
7654
+ return lines.join("\n");
7655
+ }
7656
+ function renderDocsIndex(entries) {
7657
+ const lines = [
7658
+ "# Docs",
7659
+ "",
7660
+ plural(entries.length, "doc"),
7661
+ "",
7662
+ "| Doc | Location | Pages |",
7663
+ "| --- | --- | --- |"
7664
+ ];
7665
+ for (const e of entries.sort((a, b) => docTitle(a.doc).localeCompare(docTitle(b.doc)))) {
7666
+ lines.push(`| [${docTitle(e.doc)}](${e.dir}/README.md) | ${e.location} | ${e.pages} |`);
7667
+ }
7668
+ lines.push("");
7669
+ return lines.join("\n");
7670
+ }
7671
+ function countPages(pages) {
7672
+ return pages.reduce((n, p) => n + 1 + countPages(p.pages ?? []), 0);
7673
+ }
7674
+ async function exportDocs(client, teamId, opts) {
7675
+ const manifest = loadManifest(opts.root);
7676
+ const docsRoot = join4(opts.root, "docs");
7677
+ mkdirSync3(docsRoot, { recursive: true });
7678
+ const [docs, spaces, teams] = await Promise.all([
7679
+ client.getAllDocs(teamId),
7680
+ client.getSpaces(teamId),
7681
+ client.getTeams()
7682
+ ]);
7683
+ const spaceNames = new Map(spaces.map((s) => [s.id, s.name]));
7684
+ const teamName = teams.find((t) => t.id === teamId)?.name ?? teamId;
7685
+ const locationOf = (doc) => {
7686
+ const t = doc.parent ? PARENT_TYPES[doc.parent.type] ?? `type ${doc.parent.type}` : "unknown";
7687
+ const id = doc.parent?.id ?? "";
7688
+ if (t === "space") return `space ${spaceNames.get(id) ?? id}`;
7689
+ if (t === "workspace") return `workspace ${teamName}`;
7690
+ return `${t} ${id}`;
7691
+ };
7692
+ const summary = { docs: 0, pages: 0, skipped: 0, failed: [] };
7693
+ const entries = [];
7694
+ opts.log(`Plan [docs]: ${plural(docs.length, "doc")} in workspace "${teamName}"`);
7695
+ for (const doc of docs) {
7696
+ const dir = docDirName(doc);
7697
+ const location = locationOf(doc);
7698
+ const cached = manifest.docs[doc.id];
7699
+ if (cached && !opts.refresh) {
7700
+ summary.skipped++;
7701
+ entries.push({ doc, dir, pages: cached.pageCount, location });
7702
+ continue;
7703
+ }
7704
+ try {
7705
+ const pages = await client.getDocPages(teamId, doc.id);
7706
+ const dirPath = join4(docsRoot, dir);
7707
+ mkdirSync3(dirPath, { recursive: true });
7708
+ writeFileSync2(join4(dirPath, "doc.json"), JSON.stringify({ doc, pages }, null, 2) + "\n");
7709
+ const written = writePages(dirPath, doc.id, pages);
7710
+ writeFileSync2(join4(dirPath, "README.md"), renderDocReadme(doc, written, location));
7711
+ const pageCount = countPages(pages);
7712
+ manifest.docs[doc.id] = {
7713
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
7714
+ name: doc.name,
7715
+ pageCount
7716
+ };
7717
+ summary.docs++;
7718
+ summary.pages += pageCount;
7719
+ entries.push({ doc, dir, pages: pageCount, location });
7720
+ opts.log(
7721
+ `[docs] ${summary.docs + summary.skipped + summary.failed.length}/${docs.length} ${docTitle(doc)} (${plural(pageCount, "page")})`
7722
+ );
7723
+ } catch (err) {
7724
+ summary.failed.push({ id: doc.id, error: err.message });
7725
+ }
7726
+ }
7727
+ writeFileSync2(join4(docsRoot, "README.md"), renderDocsIndex(entries));
7728
+ manifest.slices["docs"] = {
7729
+ kind: "docs",
7730
+ scope: teamId,
7731
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
7732
+ taskCount: 0
7733
+ };
7734
+ if (!manifest.workspace) manifest.workspace = { id: teamId, name: teamName };
7735
+ saveManifest(opts.root, manifest);
7736
+ return summary;
7737
+ }
7738
+
7739
+ // src/export/index-team.ts
7740
+ function link(task) {
7741
+ return `[${task.name.replace(/\|/g, "\\|")}](../../tasks/${task.id}/task.md)`;
7742
+ }
7743
+ function assignees(task) {
7744
+ return task.assignees.map((a) => a.username).join(", ");
7745
+ }
7746
+ function typeOf(task, opts) {
7747
+ const id = task.custom_item_id ?? 0;
7748
+ if (id === 0) return "task";
7749
+ if (opts.initiativeItemId !== void 0 && id === opts.initiativeItemId) return "initiative";
7750
+ return opts.typeNames?.[id] ?? `type ${id}`;
7751
+ }
7752
+ function taskTable(tasks, opts) {
7753
+ const lines = ["| Task | Status | Assignees | Type |", "| --- | --- | --- | --- |"];
7754
+ for (const t of tasks) {
7755
+ lines.push(`| ${link(t)} | ${t.status.status} | ${assignees(t)} | ${typeOf(t, opts)} |`);
7756
+ }
7757
+ return lines;
7758
+ }
7759
+ function renderTeamIndex(hierarchy, tasks, opts) {
7760
+ const topLevel = tasks.filter((t) => !t.parent);
7761
+ const byList = /* @__PURE__ */ new Map();
7762
+ for (const t of topLevel) byList.set(t.list.id, [...byList.get(t.list.id) ?? [], t]);
7763
+ const listCount = hierarchy.lists.length + hierarchy.folders.reduce((n, f) => n + f.lists.length, 0);
7764
+ const lines = [
7765
+ `# ${hierarchy.space.name} (space)`,
7766
+ "",
7767
+ `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${plural(listCount, "list")} \xB7 ${plural(hierarchy.folders.length, "folder")} \xB7 ${plural(tasks.length, "task")}`,
7768
+ ""
7769
+ ];
7770
+ const renderList = (list, level) => {
7771
+ const group = byList.get(list.id) ?? [];
7772
+ const h = "#".repeat(level);
7773
+ lines.push(`${h} ${list.name} \u2014 ${plural(group.length, "task")}`, "");
7774
+ const related = opts.relatedSlices?.filter((s) => s.listId === list.id) ?? [];
7775
+ for (const r of related) {
7776
+ lines.push(`\u2192 see also [${r.name}](../${r.name}/README.md)`, "");
7777
+ }
7778
+ if (group.length > 0) lines.push(...taskTable(group, opts), "");
7779
+ };
7780
+ for (const list of hierarchy.lists) renderList(list, 2);
7781
+ for (const folder of hierarchy.folders) {
7782
+ lines.push(`## Folder: ${folder.name}`, "");
7783
+ for (const list of folder.lists) renderList(list, 3);
7784
+ }
7785
+ return lines.join("\n");
7786
+ }
7787
+ function checkbox2(task) {
7788
+ return isDoneStatus(task.status.status) ? "[x]" : "[ ]";
7789
+ }
7790
+ function renderRoadmapIndex(list, tasks, opts) {
7791
+ const children = /* @__PURE__ */ new Map();
7792
+ for (const t of tasks) {
7793
+ if (t.parent) children.set(t.parent, [...children.get(t.parent) ?? [], t]);
7794
+ }
7795
+ const isInitiative = (t) => opts.initiativeItemId !== void 0 && t.custom_item_id === opts.initiativeItemId;
7796
+ const initiatives = tasks.filter((t) => !t.parent && isInitiative(t));
7797
+ const ungrouped = tasks.filter((t) => !t.parent && !isInitiative(t));
7798
+ const lines = [
7799
+ `# ${list.name}`,
7800
+ "",
7801
+ `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${plural(initiatives.length, "initiative")} \xB7 ${plural(tasks.length, "task")}`,
7802
+ ""
7803
+ ];
7804
+ const renderTree = (parentId, depth) => {
7805
+ for (const c of children.get(parentId) ?? []) {
7806
+ lines.push(`${" ".repeat(depth)}- ${checkbox2(c)} ${link(c)}`);
7807
+ renderTree(c.id, depth + 1);
7808
+ }
7809
+ };
7810
+ if (initiatives.length > 0) {
7811
+ lines.push("## Initiatives", "");
7812
+ for (const init of initiatives) {
7813
+ lines.push(`### ${link(init)} \u2014 ${init.status.status}`);
7814
+ const meta = [];
7815
+ if (init.assignees.length > 0) meta.push(`Owner: ${assignees(init)}`);
7816
+ if (init.tags?.length) meta.push(`Tags: ${init.tags.map((t) => t.name).join(", ")}`);
7817
+ if (init.start_date || init.due_date) {
7818
+ const from = init.start_date ? formatDateISO(init.start_date) : "?";
7819
+ const to = init.due_date ? formatDateISO(init.due_date) : "?";
7820
+ meta.push(`${from} \u2192 ${to}`);
7821
+ }
7822
+ if (meta.length > 0) lines.push(meta.join(" \xB7 "));
7823
+ lines.push("");
7824
+ if (children.has(init.id)) {
7825
+ renderTree(init.id, 0);
7826
+ lines.push("");
7827
+ }
7828
+ }
7829
+ }
7830
+ lines.push(`## Ungrouped tasks (${ungrouped.length})`, "");
7831
+ if (ungrouped.length > 0) {
7832
+ lines.push("| Task | Status | Assignees |", "| --- | --- | --- |");
7833
+ for (const t of ungrouped) lines.push(`| ${link(t)} | ${t.status.status} | ${assignees(t)} |`);
7834
+ lines.push("");
7835
+ }
7836
+ return lines.join("\n");
7837
+ }
7838
+
7839
+ // src/export/index-user.ts
7840
+ function link2(task) {
7841
+ return `[${task.name.replace(/\|/g, "\\|")}](../../tasks/${task.id}/task.md)`;
7842
+ }
7843
+ function cap(s) {
7844
+ return s.charAt(0).toUpperCase() + s.slice(1);
7845
+ }
7846
+ function closedAt(task) {
7847
+ return task.date_closed ?? task.date_done ?? void 0;
7848
+ }
7849
+ function monthOf(ms) {
7850
+ return formatDateISO(ms).slice(0, 7);
7851
+ }
7852
+ function renderUserIndex(user, tasks, opts) {
7853
+ const lines = [
7854
+ `# ${user.username} \u2014 tasks`,
7855
+ "",
7856
+ `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${plural(tasks.length, "task")} assigned \xB7 user id ${user.id}`,
7857
+ ""
7858
+ ];
7859
+ const open = tasks.filter((t) => !isDoneStatus(t.status.status));
7860
+ const done = tasks.filter((t) => isDoneStatus(t.status.status));
7861
+ const byStatus = /* @__PURE__ */ new Map();
7862
+ for (const t of open) {
7863
+ const key = t.status.status.toLowerCase();
7864
+ byStatus.set(key, [...byStatus.get(key) ?? [], t]);
7865
+ }
7866
+ const order = (s) => /progress|review|active/.test(s) ? 0 : /to ?do|open|backlog/.test(s) ? 2 : 1;
7867
+ for (const [status, group] of [...byStatus.entries()].sort(
7868
+ (a, b) => order(a[0]) - order(b[0]) || a[0].localeCompare(b[0])
7869
+ )) {
7870
+ lines.push(
7871
+ `## ${cap(status)} (${group.length})`,
7872
+ "",
7873
+ "| Task | List | Due |",
7874
+ "| --- | --- | --- |"
7875
+ );
7876
+ for (const t of group) {
7877
+ lines.push(`| ${link2(t)} | ${t.list.name} | ${t.due_date ? formatDateISO(t.due_date) : ""} |`);
7878
+ }
7879
+ lines.push("");
7880
+ }
7881
+ if (done.length > 0) {
7882
+ lines.push(`## Done (${done.length})`, "");
7883
+ const byMonth = /* @__PURE__ */ new Map();
7884
+ for (const t of done) {
7885
+ const when = closedAt(t);
7886
+ const key = when ? monthOf(when) : "unknown";
7887
+ byMonth.set(key, [...byMonth.get(key) ?? [], t]);
7888
+ }
7889
+ for (const [month, group] of [...byMonth.entries()].sort((a, b) => b[0].localeCompare(a[0]))) {
7890
+ lines.push(`### ${month}`, "", "| Task | List | Closed |", "| --- | --- | --- |");
7891
+ for (const t of group) {
7892
+ const when = closedAt(t);
7893
+ lines.push(`| ${link2(t)} | ${t.list.name} | ${when ? formatDateISO(when) : ""} |`);
7894
+ }
7895
+ lines.push("");
7896
+ }
7897
+ }
7898
+ const where = /* @__PURE__ */ new Map();
7899
+ for (const t of tasks) {
7900
+ const space = t.space?.id ? opts.spaceNames[t.space.id] ?? t.space.id : "Unknown space";
7901
+ const key = `${space} / ${t.list.name}`;
7902
+ where.set(key, (where.get(key) ?? 0) + 1);
7903
+ }
7904
+ lines.push("## Where these live", "");
7905
+ for (const [key, n] of [...where.entries()].sort((a, b) => b[1] - a[1])) {
7906
+ lines.push(`- ${key}: ${n}`);
7907
+ }
7908
+ lines.push("");
7909
+ return lines.join("\n");
7910
+ }
7911
+
7912
+ // src/export/root-readme.ts
7913
+ import { writeFileSync as writeFileSync3 } from "fs";
7914
+ import { join as join5 } from "path";
7915
+ function renderRootReadme(manifest) {
7916
+ const ws = manifest.workspace;
7917
+ const taskCount = Object.keys(manifest.tasks).length;
7918
+ const docCount = Object.keys(manifest.docs).length;
7919
+ const lines = [
7920
+ `# ClickUp export${ws ? `: ${ws.name}` : ""}`,
7921
+ "",
7922
+ `${plural(taskCount, "task")} \xB7 ${plural(docCount, "doc")} \xB7 generated by \`cup export\``,
7923
+ "",
7924
+ "## Slices",
7925
+ "",
7926
+ "Each slice is an index into the shared task store. Start from whichever matches what you are looking for.",
7927
+ "",
7928
+ "| Slice | Kind | Scope | Tasks | Exported |",
7929
+ "| --- | --- | --- | --- | --- |"
7930
+ ];
7931
+ const slices = Object.entries(manifest.slices).filter(([, s]) => s.kind !== "docs").sort((a, b) => a[0].localeCompare(b[0]));
7932
+ for (const [name, s] of slices) {
7933
+ lines.push(
7934
+ `| [${name}](slices/${name}/README.md) | ${s.kind} | ${s.scope} | ${s.taskCount} | ${s.exportedAt.slice(0, 10)} |`
7935
+ );
7936
+ }
7937
+ if (docCount > 0) lines.push("", "Docs: [docs/](docs/README.md)");
7938
+ lines.push(
7939
+ "",
7940
+ "## Layout",
7941
+ "",
7942
+ "- `tasks/<id>/` \u2014 one directory per task: `task.json` (lossless API payload), `task.md` (rendered), `comments.json` / `comments.md`, `attachments/`",
7943
+ "- `slices/<name>/README.md` \u2014 navigable index for one export scope",
7944
+ "- `manifest.json` \u2014 what has been exported and when; drives incremental re-runs",
7945
+ "",
7946
+ "Task ids match ClickUp URLs: `https://app.clickup.com/t/<id>` \u2192 `tasks/<id>/`.",
7947
+ ""
7948
+ );
7949
+ return lines.join("\n");
7950
+ }
7951
+ function writeRootReadme(root, manifest) {
7952
+ writeFileSync3(join5(root, "README.md"), renderRootReadme(manifest));
7953
+ }
7954
+
7955
+ // src/util/rate-limit.ts
7956
+ function createRateLimiter(requestsPerMinute) {
7957
+ if (!(requestsPerMinute > 0)) {
7958
+ throw new Error(`requestsPerMinute must be a positive number, got ${requestsPerMinute}`);
7959
+ }
7960
+ const capacity = Math.max(5, Math.floor(requestsPerMinute / 10));
7961
+ const refillPerMs = requestsPerMinute / 6e4;
7962
+ let tokens = capacity;
7963
+ let lastRefill = Date.now();
7964
+ const queue = [];
7965
+ let timer;
7966
+ function refill() {
7967
+ const now = Date.now();
7968
+ tokens = Math.min(capacity, tokens + (now - lastRefill) * refillPerMs);
7969
+ lastRefill = now;
7970
+ }
7971
+ function drain() {
7972
+ timer = void 0;
7973
+ refill();
7974
+ while (queue.length > 0 && tokens >= 1) {
7975
+ tokens -= 1;
7976
+ queue.shift()();
7977
+ }
7978
+ if (queue.length > 0) {
7979
+ const waitMs = Math.ceil((1 - tokens) / refillPerMs);
7980
+ timer = setTimeout(drain, waitMs);
7981
+ }
7982
+ }
7983
+ return {
7984
+ acquire() {
7985
+ return new Promise((resolve3) => {
7986
+ queue.push(resolve3);
7987
+ if (!timer) drain();
7988
+ });
7989
+ },
7990
+ penalize() {
7991
+ refill();
7992
+ tokens = 0;
7993
+ }
7994
+ };
7995
+ }
7996
+
7997
+ // src/commands/export.ts
7998
+ var CONCURRENCY = 4;
7999
+ function requireTeam(config) {
8000
+ if (!config.teamId) {
8001
+ throw new Error("Export requires a teamId in your config. Run `cup init` to set one.");
8002
+ }
8003
+ return config.teamId;
8004
+ }
8005
+ function makeClient(config, rpm) {
8006
+ return new ClickUpClient({ ...config, rateLimiter: createRateLimiter(rpm) });
8007
+ }
8008
+ function emptySummary(plan, opts) {
8009
+ return {
8010
+ slice: plan.slice.name,
8011
+ planned: plan.tasks.length,
8012
+ out: resolve(opts.out),
8013
+ dryRun: opts.dryRun,
8014
+ fetched: 0,
8015
+ skipped: 0,
8016
+ failed: [],
8017
+ attachmentsDownloaded: 0,
8018
+ attachmentsFailed: 0
8019
+ };
8020
+ }
8021
+ async function fetchTypeNames(client, teamId) {
8022
+ try {
8023
+ const types = await client.getCustomTaskTypes(teamId);
8024
+ return Object.fromEntries(types.map((t) => [t.id, t.name]));
8025
+ } catch {
8026
+ return {};
8027
+ }
8028
+ }
8029
+ function describePlan(plan) {
8030
+ const initiatives = plan.tasks.filter((t) => t.initiative).length;
8031
+ return `Plan [${plan.slice.name}]: ${plural(plan.tasks.length, "task")} (${plural(initiatives, "initiative")}) in workspace "${plan.workspace.name}"`;
8032
+ }
8033
+ async function execute(client, teamId, plan, opts, writeIndex) {
8034
+ opts.log(describePlan(plan));
8035
+ const summary = emptySummary(plan, opts);
8036
+ if (opts.dryRun) return summary;
8037
+ const root = resolve(opts.out);
8038
+ const spaces = await client.getSpaces(teamId);
8039
+ const spaceNames = Object.fromEntries(spaces.map((s) => [s.id, s.name]));
8040
+ const run2 = await runExport(client, plan, {
8041
+ root,
8042
+ refresh: opts.refresh,
8043
+ downloadAttachments: opts.attachments,
8044
+ concurrency: CONCURRENCY,
8045
+ log: opts.log,
8046
+ spaceNames
8047
+ });
8048
+ await writeIndex(root, spaceNames);
8049
+ writeRootReadme(root, loadManifest(root));
8050
+ return { ...summary, ...run2 };
8051
+ }
8052
+ async function exportUser(config, userRef, opts) {
8053
+ const teamId = requireTeam(config);
8054
+ const client = makeClient(config, opts.rpm);
8055
+ const user = await resolveUserRef(client, teamId, userRef);
8056
+ const plan = await discoverUserTasks(client, teamId, userRef);
8057
+ return execute(client, teamId, plan, opts, async (root, spaceNames) => {
8058
+ const tasks = Object.values(plan.tasksById ?? {});
8059
+ const dir = join6(root, "slices", plan.slice.name);
8060
+ mkdirSync4(dir, { recursive: true });
8061
+ writeFileSync4(
8062
+ join6(dir, "README.md"),
8063
+ renderUserIndex(user, tasks, { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), spaceNames })
8064
+ );
8065
+ writeFileSync4(
8066
+ join6(dir, "tasks.json"),
8067
+ JSON.stringify({ user, taskIds: plan.tasks.map((t) => t.id) }, null, 2) + "\n"
8068
+ );
8069
+ });
8070
+ }
8071
+ function formatExportSummary(s) {
8072
+ if (s.dryRun) return `Dry run: ${plural(s.planned, "task")} would be exported to ${s.out}`;
8073
+ const lines = [
8074
+ `Exported slice "${s.slice}" to ${s.out}`,
8075
+ ` tasks: ${s.fetched} fetched, ${s.skipped} already present, ${s.failed.length} failed`,
8076
+ ` attachments: ${s.attachmentsDownloaded} downloaded, ${s.attachmentsFailed} failed`
8077
+ ];
8078
+ for (const f of s.failed.slice(0, 10)) lines.push(` failed ${f.id}: ${f.error}`);
8079
+ if (s.failed.length > 10) lines.push(` ... and ${s.failed.length - 10} more`);
8080
+ return lines.join("\n");
8081
+ }
8082
+ function writeSliceFiles(root, sliceName, readme, meta) {
8083
+ const dir = join6(root, "slices", sliceName);
8084
+ mkdirSync4(dir, { recursive: true });
8085
+ writeFileSync4(join6(dir, "README.md"), readme);
8086
+ writeFileSync4(join6(dir, "tasks.json"), JSON.stringify(meta, null, 2) + "\n");
8087
+ }
8088
+ async function exportTeam(config, spaceRef, opts) {
8089
+ const teamId = requireTeam(config);
8090
+ const client = makeClient(config, opts.rpm);
8091
+ const plan = await discoverTeamTasks(client, teamId, spaceRef);
8092
+ return execute(client, teamId, plan, opts, async (root) => {
8093
+ const manifest = loadManifest(root);
8094
+ const relatedSlices = Object.entries(manifest.slices).filter(([, s]) => s.kind === "roadmap" || s.kind === "initiatives").map(([name, s]) => ({ name, listId: s.scope }));
8095
+ const tasks = Object.values(plan.tasksById ?? {});
8096
+ const typeNames = await fetchTypeNames(client, teamId);
8097
+ writeSliceFiles(
8098
+ root,
8099
+ plan.slice.name,
8100
+ renderTeamIndex(plan.hierarchy, tasks, {
8101
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8102
+ relatedSlices,
8103
+ initiativeItemId: opts.initiativeItemId,
8104
+ typeNames
8105
+ }),
8106
+ { hierarchy: plan.hierarchy, taskIds: plan.tasks.map((t) => t.id) }
8107
+ );
8108
+ });
8109
+ }
8110
+ async function exportRoadmap(config, listId, opts) {
8111
+ const teamId = requireTeam(config);
8112
+ const client = makeClient(config, opts.rpm);
8113
+ const plan = await discoverListTasks(client, teamId, listId, { kind: "roadmap" });
8114
+ return execute(client, teamId, plan, opts, async (root) => {
8115
+ const tasks = Object.values(plan.tasksById ?? {});
8116
+ writeSliceFiles(
8117
+ root,
8118
+ plan.slice.name,
8119
+ renderRoadmapIndex(plan.list, tasks, {
8120
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8121
+ initiativeItemId: opts.initiativeItemId
8122
+ }),
8123
+ {
8124
+ list: plan.list,
8125
+ initiativeItemId: opts.initiativeItemId ?? null,
8126
+ taskIds: plan.tasks.map((t) => t.id)
8127
+ }
8128
+ );
8129
+ });
8130
+ }
8131
+ async function exportInitiatives(config, listId, opts) {
8132
+ const teamId = requireTeam(config);
8133
+ if (opts.initiativeItemId === void 0) {
8134
+ throw new Error(
8135
+ "initiatives export needs --item-id <n>: the custom_item_id your workspace uses for initiatives (see `cup task-types`)"
8136
+ );
8137
+ }
8138
+ const client = makeClient(config, opts.rpm);
8139
+ const plan = await discoverListTasks(client, teamId, listId, {
8140
+ kind: "initiatives",
8141
+ initiativeItemId: opts.initiativeItemId
8142
+ });
8143
+ return execute(client, teamId, plan, opts, async (root) => {
8144
+ const { readBundleData: readBundleData2 } = await import("./writer-UZHLSQMW.js");
8145
+ const manifest = loadManifest(root);
8146
+ const wanted = /* @__PURE__ */ new Set();
8147
+ const queue = plan.tasks.map((t) => t.id);
8148
+ while (queue.length > 0) {
8149
+ const id = queue.pop();
8150
+ if (wanted.has(id) || !manifest.tasks[id]) continue;
8151
+ wanted.add(id);
8152
+ const b = await readBundleData2(root, id);
8153
+ queue.push(...b.subtaskIds);
8154
+ }
8155
+ const tasks = await Promise.all(
8156
+ [...wanted].map((id) => readBundleData2(root, id).then((b) => b.task))
8157
+ );
8158
+ writeSliceFiles(
8159
+ root,
8160
+ plan.slice.name,
8161
+ renderRoadmapIndex(plan.list, tasks, {
8162
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8163
+ initiativeItemId: opts.initiativeItemId
8164
+ }),
8165
+ {
8166
+ list: plan.list,
8167
+ initiativeItemId: opts.initiativeItemId,
8168
+ taskIds: plan.tasks.map((t) => t.id)
8169
+ }
8170
+ );
8171
+ });
8172
+ }
8173
+ async function exportDocs2(config, opts) {
8174
+ const teamId = requireTeam(config);
8175
+ const client = makeClient(config, opts.rpm);
8176
+ const root = resolve(opts.out);
8177
+ if (opts.dryRun) {
8178
+ const docs = await client.getAllDocs(teamId);
8179
+ opts.log(`Plan [docs]: ${plural(docs.length, "doc")}`);
8180
+ return {
8181
+ slice: "docs",
8182
+ out: root,
8183
+ dryRun: true,
8184
+ docs: docs.length,
8185
+ pages: 0,
8186
+ skipped: 0,
8187
+ failed: []
8188
+ };
8189
+ }
8190
+ const summary = await exportDocs(client, teamId, {
8191
+ root,
8192
+ refresh: opts.refresh,
8193
+ log: opts.log
8194
+ });
8195
+ writeRootReadme(root, loadManifest(root));
8196
+ return { slice: "docs", out: root, dryRun: false, ...summary };
8197
+ }
8198
+ function formatDocsSummary(s) {
8199
+ if (s.dryRun) return `Dry run: ${plural(s.docs, "doc")} would be exported to ${s.out}`;
8200
+ const lines = [
8201
+ `Exported docs to ${s.out}/docs`,
8202
+ ` docs: ${s.docs} fetched (${s.pages} pages), ${s.skipped} already present, ${s.failed.length} failed`
8203
+ ];
8204
+ for (const f of s.failed.slice(0, 10)) lines.push(` failed ${f.id}: ${f.error}`);
8205
+ return lines.join("\n");
8206
+ }
8207
+ var REQUESTS_PER_TASK = 3;
8208
+ async function exportAll(config, opts) {
8209
+ const teamId = requireTeam(config);
8210
+ const client = makeClient(config, opts.rpm);
8211
+ const root = resolve(opts.out);
8212
+ const [spaces, docs, teams] = await Promise.all([
8213
+ client.getSpaces(teamId),
8214
+ client.getAllDocs(teamId),
8215
+ client.getTeams()
8216
+ ]);
8217
+ const workspaceName = teams.find((t) => t.id === teamId)?.name ?? teamId;
8218
+ const plans = [];
8219
+ for (const space of spaces) plans.push(await discoverTeamTasks(client, teamId, space.id));
8220
+ const manifest = loadManifest(root);
8221
+ const allTaskIds = new Set(plans.flatMap((p) => p.tasks.map((t) => t.id)));
8222
+ const alreadyExported = [...allTaskIds].filter((id) => manifest.tasks[id]).length;
8223
+ const toFetch = allTaskIds.size - alreadyExported;
8224
+ const listCount = plans.reduce(
8225
+ (n, p) => n + (p.hierarchy?.lists.length ?? 0) + (p.hierarchy?.folders.reduce((m, f) => m + f.lists.length, 0) ?? 0),
8226
+ 0
8227
+ );
8228
+ const estRequests = toFetch * REQUESTS_PER_TASK + docs.length;
8229
+ const estMinutes = Math.ceil(estRequests / opts.rpm);
8230
+ opts.log(`Workspace export plan for "${workspaceName}":`);
8231
+ opts.log(
8232
+ ` ${plural(spaces.length, "space")}, ${plural(listCount, "list")}, ${plural(allTaskIds.size, "task")}, ${plural(docs.length, "doc")}`
8233
+ );
8234
+ opts.log(` Already exported: ${plural(alreadyExported, "task")} (will be skipped)`);
8235
+ opts.log(` Estimated requests: ~${estRequests}`);
8236
+ opts.log(
8237
+ ` Estimated time at ${opts.rpm} req/min: ~${estMinutes < 60 ? `${estMinutes}m` : `${Math.floor(estMinutes / 60)}h ${estMinutes % 60}m`}`
8238
+ );
8239
+ if (opts.attachments) opts.log(" Attachments: downloaded (size unknown until fetched)");
8240
+ const empty = {
8241
+ slice: "all",
8242
+ out: root,
8243
+ dryRun: opts.dryRun,
8244
+ spaces: plans.map((p) => p.slice.name),
8245
+ planned: allTaskIds.size,
8246
+ fetched: 0,
8247
+ skipped: 0,
8248
+ failed: [],
8249
+ attachmentsDownloaded: 0,
8250
+ attachmentsFailed: 0,
8251
+ docs: { docs: docs.length, pages: 0, skipped: 0, failed: [] }
8252
+ };
8253
+ if (opts.dryRun) return empty;
8254
+ if (!opts.yes) {
8255
+ if (!isTTY()) {
8256
+ throw new Error(
8257
+ "This is a long-running operation. Re-run with --yes to confirm in non-interactive mode."
8258
+ );
8259
+ }
8260
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
8261
+ const ok = await confirm3({
8262
+ message: `Export the whole workspace to ${root}?`,
8263
+ default: false
8264
+ });
8265
+ if (!ok) throw new Error("Cancelled");
8266
+ }
8267
+ const spaceNames = Object.fromEntries(spaces.map((s) => [s.id, s.name]));
8268
+ const typeNames = await fetchTypeNames(client, teamId);
8269
+ const summary = { ...empty };
8270
+ for (const plan of plans) {
8271
+ const run2 = await runExport(client, plan, {
8272
+ root,
8273
+ refresh: opts.refresh,
8274
+ downloadAttachments: opts.attachments,
8275
+ concurrency: CONCURRENCY,
8276
+ log: opts.log,
8277
+ spaceNames
8278
+ });
8279
+ summary.fetched += run2.fetched;
8280
+ summary.skipped += run2.skipped;
8281
+ summary.failed.push(...run2.failed);
8282
+ summary.attachmentsDownloaded += run2.attachmentsDownloaded;
8283
+ summary.attachmentsFailed += run2.attachmentsFailed;
8284
+ const tasks = Object.values(plan.tasksById ?? {});
8285
+ writeSliceFiles(
8286
+ root,
8287
+ plan.slice.name,
8288
+ renderTeamIndex(plan.hierarchy, tasks, {
8289
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8290
+ initiativeItemId: opts.initiativeItemId,
8291
+ typeNames
8292
+ }),
8293
+ { hierarchy: plan.hierarchy, taskIds: plan.tasks.map((t) => t.id) }
8294
+ );
8295
+ }
8296
+ summary.docs = await exportDocs(client, teamId, { root, refresh: opts.refresh, log: opts.log });
8297
+ writeRootReadme(root, loadManifest(root));
8298
+ return summary;
8299
+ }
8300
+ function formatAllSummary(s) {
8301
+ if (s.dryRun)
8302
+ return `Dry run: ${plural(s.planned, "task")} across ${plural(s.spaces.length, "space")} would be exported to ${s.out}`;
8303
+ const lines = [
8304
+ `Exported workspace to ${s.out}`,
8305
+ ` spaces: ${s.spaces.length} (${s.spaces.join(", ")})`,
8306
+ ` tasks: ${s.fetched} fetched, ${s.skipped} already present, ${s.failed.length} failed`,
8307
+ ` attachments: ${s.attachmentsDownloaded} downloaded, ${s.attachmentsFailed} failed`,
8308
+ ` docs: ${s.docs.docs} fetched (${s.docs.pages} pages), ${s.docs.skipped} already present, ${s.docs.failed.length} failed`
8309
+ ];
8310
+ for (const f of s.failed.slice(0, 10)) lines.push(` failed ${f.id}: ${f.error}`);
8311
+ if (s.failed.length > 10) lines.push(` ... and ${s.failed.length - 10} more`);
8312
+ return lines.join("\n");
8313
+ }
8314
+
7184
8315
  // src/commands/docs.ts
7185
8316
  var DOC_COLUMNS = [
7186
8317
  { key: "id", label: "ID", maxWidth: 15 },
@@ -7606,28 +8737,6 @@ function formatFieldsMarkdown(fields) {
7606
8737
  }).join("\n");
7607
8738
  }
7608
8739
 
7609
- // src/util/batch.ts
7610
- async function runInBatches(items, concurrency, fn) {
7611
- if (!Number.isInteger(concurrency) || concurrency < 1) {
7612
- throw new Error(`concurrency must be a positive integer, got ${concurrency}`);
7613
- }
7614
- const results = [];
7615
- for (let i = 0; i < items.length; i += concurrency) {
7616
- const batch = items.slice(i, i + concurrency);
7617
- const settled = await Promise.allSettled(batch.map(fn));
7618
- settled.forEach((res, idx) => {
7619
- const item = batch[idx];
7620
- if (res.status === "fulfilled") {
7621
- results.push({ item, ok: true, result: res.value });
7622
- } else {
7623
- const error = res.reason instanceof Error ? res.reason : new Error(String(res.reason), { cause: res.reason });
7624
- results.push({ item, ok: false, error });
7625
- }
7626
- });
7627
- }
7628
- return results;
7629
- }
7630
-
7631
8740
  // src/commands/field-create.ts
7632
8741
  var FIELD_CREATE_CONCURRENCY = 5;
7633
8742
  var VALID_FIELD_TYPES = [
@@ -8744,10 +9853,10 @@ function resolveRequiredMessage(opts) {
8744
9853
  }
8745
9854
  async function resolveMentions(client, teamId, mentions) {
8746
9855
  if (mentions.length === 0) return [];
8747
- const resolve2 = createCachedMemberResolver(client, teamId);
9856
+ const resolve3 = createCachedMemberResolver(client, teamId);
8748
9857
  const ids = [];
8749
9858
  for (const mention of mentions) {
8750
- ids.push(await resolve2(mention));
9859
+ ids.push(await resolve3(mention));
8751
9860
  }
8752
9861
  return ids;
8753
9862
  }
@@ -9040,6 +10149,120 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
9040
10149
  }
9041
10150
  )
9042
10151
  );
10152
+ const exportCmd = program.command("export").description("Export tasks and docs to a local archive (lossless JSON + rendered markdown)");
10153
+ function withExportOptions(cmd) {
10154
+ return cmd.option(
10155
+ "--out <dir>",
10156
+ "Archive directory (slices compose into the same dir)",
10157
+ "./clickup-export"
10158
+ ).option("--refresh", "Re-fetch tasks already present in the archive").option("--no-attachments", "Skip downloading attachment binaries (metadata still written)").option("--dry-run", "Discover and print the plan without fetching or writing").option("--rpm <n>", "Request throttle per minute (ClickUp Business limit is 100)", "90").option("--yes", "Skip confirmation prompts (required for non-interactive `export all`)").option(
10159
+ "--item-id <n>",
10160
+ "custom_item_id that marks an initiative in your workspace (see `cup task-types`)"
10161
+ ).option("--json", "Force JSON output even in terminal");
10162
+ }
10163
+ function toExportOptions(opts) {
10164
+ const rpm = Number(opts.rpm);
10165
+ if (!Number.isFinite(rpm) || rpm <= 0) throw new Error("--rpm must be a positive number");
10166
+ let initiativeItemId;
10167
+ if (opts.itemId !== void 0) {
10168
+ initiativeItemId = Number(opts.itemId);
10169
+ if (!Number.isInteger(initiativeItemId) || initiativeItemId <= 0) {
10170
+ throw new Error("--item-id must be a positive integer");
10171
+ }
10172
+ }
10173
+ return {
10174
+ out: opts.out,
10175
+ refresh: opts.refresh ?? false,
10176
+ attachments: opts.attachments,
10177
+ dryRun: opts.dryRun ?? false,
10178
+ rpm,
10179
+ log: (line) => process.stderr.write(line + "\n"),
10180
+ ...initiativeItemId !== void 0 ? { initiativeItemId } : {},
10181
+ yes: opts.yes ?? false
10182
+ };
10183
+ }
10184
+ function printExportSummary(summary, json) {
10185
+ if (shouldOutputJson(json)) {
10186
+ console.log(JSON.stringify(summary, null, 2));
10187
+ } else {
10188
+ console.log(formatExportSummary(summary));
10189
+ }
10190
+ if (summary.failed.length > 0) process.exitCode = 1;
10191
+ }
10192
+ withExportOptions(
10193
+ exportCmd.command("user <userRef>").description(
10194
+ "Export every task assigned to a user (me, id, email, or username), incl. closed and archived"
10195
+ )
10196
+ ).action(
10197
+ wrapAction(async (userRef, opts) => {
10198
+ const config = loadConfig(getProfileName());
10199
+ const summary = await exportUser(config, userRef, toExportOptions(opts));
10200
+ printExportSummary(summary, opts.json ?? false);
10201
+ })
10202
+ );
10203
+ withExportOptions(
10204
+ exportCmd.command("team <spaceRef>").description("Export every list in a space (by id or name), incl. archived lists and tasks")
10205
+ ).action(
10206
+ wrapAction(async (spaceRef, opts) => {
10207
+ const config = loadConfig(getProfileName());
10208
+ const summary = await exportTeam(config, spaceRef, toExportOptions(opts));
10209
+ printExportSummary(summary, opts.json ?? false);
10210
+ })
10211
+ );
10212
+ withExportOptions(
10213
+ exportCmd.command("roadmap <listId>").description(
10214
+ "Export a list with initiatives grouped and their subtask trees (use --item-id)"
10215
+ )
10216
+ ).action(
10217
+ wrapAction(async (listId, opts) => {
10218
+ const config = loadConfig(getProfileName());
10219
+ const summary = await exportRoadmap(config, listId, toExportOptions(opts));
10220
+ printExportSummary(summary, opts.json ?? false);
10221
+ })
10222
+ );
10223
+ withExportOptions(
10224
+ exportCmd.command("docs").description(
10225
+ "Export every workspace doc as markdown page trees (ClickUp has no bulk doc export)"
10226
+ )
10227
+ ).action(
10228
+ wrapAction(async (opts) => {
10229
+ const config = loadConfig(getProfileName());
10230
+ const summary = await exportDocs2(config, toExportOptions(opts));
10231
+ if (shouldOutputJson(opts.json ?? false)) {
10232
+ console.log(JSON.stringify(summary, null, 2));
10233
+ } else {
10234
+ console.log(formatDocsSummary(summary));
10235
+ }
10236
+ if (summary.failed.length > 0) process.exitCode = 1;
10237
+ })
10238
+ );
10239
+ withExportOptions(
10240
+ exportCmd.command("all").description(
10241
+ "Export the whole workspace: every space as a team slice, plus all docs. Plans first and asks for confirmation"
10242
+ )
10243
+ ).action(
10244
+ wrapAction(async (opts) => {
10245
+ const config = loadConfig(getProfileName());
10246
+ const summary = await exportAll(config, toExportOptions(opts));
10247
+ if (shouldOutputJson(opts.json ?? false)) {
10248
+ console.log(JSON.stringify(summary, null, 2));
10249
+ } else {
10250
+ console.log(formatAllSummary(summary));
10251
+ }
10252
+ if (summary.failed.length > 0 || summary.docs.failed.length > 0) process.exitCode = 1;
10253
+ })
10254
+ );
10255
+ withExportOptions(
10256
+ exportCmd.command("initiatives <listId>").description(
10257
+ "Export only initiative-typed tasks in a list plus their subtask trees (requires --item-id)"
10258
+ )
10259
+ ).action(
10260
+ wrapAction(async (listId, opts) => {
10261
+ const config = loadConfig(getProfileName());
10262
+ const summary = await exportInitiatives(config, listId, toExportOptions(opts));
10263
+ printExportSummary(summary, opts.json ?? false);
10264
+ })
10265
+ );
9043
10266
  program.command("sprint").description("List my tasks in the current active sprint (auto-detected)").option("--status <status>", "Filter by status").option("--space <nameOrId>", "Narrow sprint search to a specific space (partial name or ID)").option("--folder <folderId>", "Sprint folder ID (overrides config and auto-detection)").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
9044
10267
  wrapAction(
9045
10268
  async (opts) => {
@@ -11216,7 +12439,7 @@ process.on("SIGINT", () => {
11216
12439
  });
11217
12440
  function checkDirectExecution() {
11218
12441
  try {
11219
- return process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync2(resolve(process.argv[1]));
12442
+ return process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync2(resolve2(process.argv[1]));
11220
12443
  } catch {
11221
12444
  return false;
11222
12445
  }