cyber-asana 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,998 @@
1
+ #!/usr/bin/env node
2
+ import { $ as interpolateTemplate, At as getGoal, B as createTag, C as getDependencies, Ct as getPortfolio, D as getTasksByGid, Dt as createGoal, E as getTask, F as scanTodos, Ft as getAttachment, G as listTagsForTask, H as deleteTag, I as searchTasks, It as listAttachments, J as updateTag, K as listTasksForTag, L as updateTask, M as removeDependents, Mt as updateGoal, N as removeFollowersFromTask, O as listSubtasks, P as removeTaskFromProject, Q as getTaskTemplateData, Rt as listItems, S as deleteTask, St as deletePortfolio, T as getMyTasks, Tt as updatePortfolio, U as getTag, Vt as setTokenOverride, W as listTags, X as createStory, _ as addFollowersToTask, _t as searchProjects, at as getSection, b as createTask, bt as createPortfolio, c as getUser, dt as deleteProject, et as listStories, f as getTeam, ft as exportProject, g as addDependents, gt as renderProjectMarkdown, h as addDependencies, ht as listProjects, i as listWorkspaces, it as deleteSection, j as removeDependencies, jt as listGoals, k as listTasks, kt as deleteGoal, l as listUsers, lt as createProject, mt as getProjectTaskCounts, nt as createSection, ot as listSections, p as listTeams, pt as getProject, q as removeTagFromTask, r as getWorkspace, s as getMe, st as updateSection, t as createRuntimeContext, v as addTaskToProject, vt as updateProject, w as getDependents, wt as listPortfolios, y as createSubtask, z as addTagToTask, zt as nextPageOffset } from "./composition-Ds2nQa_V.js";
3
+ import { a as buildProjectUpdateFields, i as buildProjectCreateFields, n as buildTaskUpdateFields, t as buildTaskCreateFields } from "./write-options-CRFsXUQl.js";
4
+ import { writeFile } from "node:fs/promises";
5
+ import { Command, InvalidArgumentError, Option } from "commander";
6
+
7
+ //#region src/cli-options.ts
8
+ function parseLimit(value) {
9
+ const limit = Number(value);
10
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new InvalidArgumentError("limit must be an integer from 1 to 100");
11
+ return limit;
12
+ }
13
+ function parseMaxPages(value) {
14
+ const maxPages = Number(value);
15
+ if (!Number.isInteger(maxPages) || maxPages < 1) throw new InvalidArgumentError("max-pages must be an integer greater than 0");
16
+ return maxPages;
17
+ }
18
+ function addPaginationOptions(cmd, opts) {
19
+ if (opts?.limit === false) return cmd.option("--offset <token>", "Offset token returned by a previous paginated response").option("--opt-fields <fields>", "Comma-separated optional Asana fields to include");
20
+ return cmd.option("--limit <number>", "Results per page, from 1 to 100 (default: 100)", parseLimit).option("--offset <token>", "Offset token returned by a previous paginated response").option("--opt-fields <fields>", "Comma-separated optional Asana fields to include").option("--all", "Fetch all pages up to --max-pages").option("--max-pages <number>", "Maximum pages to fetch with --all (default: 10)", parseMaxPages);
21
+ }
22
+ function paginationOptionsFromCli(opts) {
23
+ if (opts.all && opts.offset) throw new InvalidArgumentError("--all cannot be used with --offset");
24
+ return {
25
+ limit: opts.limit,
26
+ offset: opts.offset,
27
+ optFields: opts.optFields,
28
+ fetchAll: opts.all,
29
+ maxPages: opts.maxPages
30
+ };
31
+ }
32
+ function itemsForOutput(result) {
33
+ return listItems(result);
34
+ }
35
+ function printNextPageHint(result) {
36
+ const offset = nextPageOffset(result);
37
+ if (offset) console.log(`\nNext offset: ${offset}`);
38
+ }
39
+ function addGidOption(cmd, baseName, description, opts) {
40
+ const normalizedOption = new Option(`--${baseName}-gid <gid>`, description);
41
+ if (opts?.env) normalizedOption.env(opts.env);
42
+ cmd.addOption(normalizedOption);
43
+ cmd.addOption(new Option(`--${baseName} <gid>`, `${description} (legacy alias)`));
44
+ return cmd;
45
+ }
46
+ function normalizedGid(opts, baseName) {
47
+ const normalized = opts[`${baseName}Gid`];
48
+ if (typeof normalized === "string" && normalized.length > 0) return normalized;
49
+ const legacy = opts[baseName];
50
+ if (typeof legacy === "string" && legacy.length > 0) return legacy;
51
+ }
52
+ function requiredGid(opts, baseName, label) {
53
+ const gid = normalizedGid(opts, baseName);
54
+ if (!gid) throw new InvalidArgumentError(`${label} is required`);
55
+ return gid;
56
+ }
57
+
58
+ //#endregion
59
+ //#region src/output.ts
60
+ function printJson(data) {
61
+ console.log(JSON.stringify(data, null, 2));
62
+ }
63
+ function printFields(fields) {
64
+ const entries = Object.entries(fields).filter(([, v]) => v != null);
65
+ const width = Math.max(...entries.map(([k]) => k.length));
66
+ for (const [key, val] of entries) console.log(`${key.padEnd(width)} ${val}`);
67
+ }
68
+ function printTable(items, cols) {
69
+ if (items.length === 0) {
70
+ console.log("(none)");
71
+ return;
72
+ }
73
+ const widths = cols.map((c) => Math.max(c.label.length, ...items.map((i) => c.get(i).length)));
74
+ console.log(cols.map((c, i) => c.label.toUpperCase().padEnd(widths[i])).join(" "));
75
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
76
+ for (const item of items) console.log(cols.map((c, i) => c.get(item).padEnd(widths[i])).join(" "));
77
+ }
78
+ function output(data, readable) {
79
+ if (process.argv.includes("--json")) printJson(data);
80
+ else readable();
81
+ }
82
+
83
+ //#endregion
84
+ //#region src/attachments/cli.ts
85
+ function attachmentCommand() {
86
+ const cmd = new Command("attachment").description("Manage Asana attachments");
87
+ addPaginationOptions(addGidOption(cmd.command("list").description("List attachments for a task"), "task", "Task GID")).action(async (opts) => {
88
+ const data = await listAttachments(requiredGid(opts, "task", "Task GID"), paginationOptionsFromCli(opts));
89
+ output(data, () => {
90
+ printTable(itemsForOutput(data), [{
91
+ label: "Name",
92
+ get: (a) => a.name
93
+ }, {
94
+ label: "ID",
95
+ get: (a) => a.gid
96
+ }]);
97
+ printNextPageHint(data);
98
+ });
99
+ });
100
+ cmd.command("get <gid>").description("Get an attachment by GID").action(async (gid) => {
101
+ const data = await getAttachment(gid);
102
+ output(data, () => printFields({
103
+ Name: data.name,
104
+ ID: data.gid,
105
+ URL: data.download_url ?? null
106
+ }));
107
+ });
108
+ return cmd;
109
+ }
110
+
111
+ //#endregion
112
+ //#region src/goals/cli.ts
113
+ function fmtGoal(g) {
114
+ printFields({
115
+ Name: g.name,
116
+ ID: g.gid,
117
+ URL: g.permalink_url ?? null,
118
+ Due: g.due_on ?? null,
119
+ Status: g.status ?? null
120
+ });
121
+ }
122
+ function goalCommand() {
123
+ const cmd = new Command("goal").description("Manage Asana goals");
124
+ addPaginationOptions(addGidOption(cmd.command("list").description("List goals in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" })).action(async (opts) => {
125
+ const data = await listGoals(requiredGid(opts, "workspace", "Workspace GID"), paginationOptionsFromCli(opts));
126
+ output(data, () => {
127
+ printTable(itemsForOutput(data), [
128
+ {
129
+ label: "Name",
130
+ get: (g) => g.name
131
+ },
132
+ {
133
+ label: "ID",
134
+ get: (g) => g.gid
135
+ },
136
+ {
137
+ label: "Due",
138
+ get: (g) => g.due_on ?? ""
139
+ }
140
+ ]);
141
+ printNextPageHint(data);
142
+ });
143
+ });
144
+ cmd.command("get <gid>").description("Get a goal by GID").action(async (gid) => {
145
+ const data = await getGoal(gid);
146
+ output(data, () => fmtGoal(data));
147
+ });
148
+ addGidOption(cmd.command("create <name>").description("Create a goal"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }).option("--notes <text>", "Goal notes").option("--due-on <date>", "Due date (YYYY-MM-DD)").action(async (name, opts) => {
149
+ const data = await createGoal(requiredGid(opts, "workspace", "Workspace GID"), name, {
150
+ notes: opts.notes,
151
+ due_on: opts.dueOn
152
+ });
153
+ output(data, () => fmtGoal(data));
154
+ });
155
+ cmd.command("update <gid>").description("Update a goal").option("--name <name>", "New name").option("--notes <text>", "New notes").option("--due-on <date>", "Due date (YYYY-MM-DD)").action(async (gid, opts) => {
156
+ const data = await updateGoal(gid, {
157
+ name: opts.name,
158
+ notes: opts.notes,
159
+ due_on: opts.dueOn
160
+ });
161
+ output(data, () => fmtGoal(data));
162
+ });
163
+ cmd.command("delete <gid>").description("Delete a goal").action(async (gid) => {
164
+ await deleteGoal(gid);
165
+ console.log(`Deleted goal ${gid}`);
166
+ });
167
+ return cmd;
168
+ }
169
+
170
+ //#endregion
171
+ //#region src/portfolios/cli.ts
172
+ function fmtPortfolio(p) {
173
+ printFields({
174
+ Name: p.name,
175
+ ID: p.gid,
176
+ URL: p.permalink_url ?? null
177
+ });
178
+ }
179
+ function portfolioCommand() {
180
+ const cmd = new Command("portfolio").description("Manage Asana portfolios");
181
+ addPaginationOptions(addGidOption(cmd.command("list").description("List portfolios in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" })).action(async (opts) => {
182
+ const data = await listPortfolios(requiredGid(opts, "workspace", "Workspace GID"), paginationOptionsFromCli(opts));
183
+ output(data, () => {
184
+ printTable(itemsForOutput(data), [{
185
+ label: "Name",
186
+ get: (p) => p.name
187
+ }, {
188
+ label: "ID",
189
+ get: (p) => p.gid
190
+ }]);
191
+ printNextPageHint(data);
192
+ });
193
+ });
194
+ cmd.command("get <gid>").description("Get a portfolio by GID").action(async (gid) => {
195
+ const data = await getPortfolio(gid);
196
+ output(data, () => fmtPortfolio(data));
197
+ });
198
+ addGidOption(cmd.command("create <name>").description("Create a portfolio"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }).action(async (name, opts) => {
199
+ const data = await createPortfolio(requiredGid(opts, "workspace", "Workspace GID"), name);
200
+ output(data, () => fmtPortfolio(data));
201
+ });
202
+ cmd.command("update <gid>").description("Update a portfolio").option("--name <name>", "New name").action(async (gid, opts) => {
203
+ const data = await updatePortfolio(gid, opts);
204
+ output(data, () => fmtPortfolio(data));
205
+ });
206
+ cmd.command("delete <gid>").description("Delete a portfolio").action(async (gid) => {
207
+ await deletePortfolio(gid);
208
+ console.log(`Deleted portfolio ${gid}`);
209
+ });
210
+ return cmd;
211
+ }
212
+
213
+ //#endregion
214
+ //#region src/projects/cli.ts
215
+ function fmtProject(p) {
216
+ printFields({
217
+ Name: p.name,
218
+ ID: p.gid,
219
+ URL: p.permalink_url,
220
+ Color: p.color || null,
221
+ Notes: p.notes || null
222
+ });
223
+ }
224
+ function fmtProjectList(projects) {
225
+ printTable(projects, [{
226
+ label: "Name",
227
+ get: (p) => p.name
228
+ }, {
229
+ label: "ID",
230
+ get: (p) => p.gid
231
+ }]);
232
+ }
233
+ function fmtProjectCounts(projectGid, counts, usingDefaultFields) {
234
+ if (usingDefaultFields) {
235
+ printFields({
236
+ "Project ID": projectGid,
237
+ "Total Tasks": String(counts.num_tasks ?? 0),
238
+ "Incomplete Tasks": String(counts.num_incomplete_tasks ?? 0),
239
+ "Completed Tasks": String(counts.num_completed_tasks ?? 0)
240
+ });
241
+ return;
242
+ }
243
+ printFields(Object.fromEntries(Object.entries(counts).map(([key, value]) => [key, value == null ? null : String(value)])));
244
+ }
245
+ function projectCommand() {
246
+ const cmd = new Command("project").description("Manage Asana projects");
247
+ addPaginationOptions(addGidOption(cmd.command("list").description("List projects in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" })).action(async (opts) => {
248
+ const data = await listProjects(requiredGid(opts, "workspace", "Workspace GID"), paginationOptionsFromCli(opts));
249
+ output(data, () => {
250
+ fmtProjectList(itemsForOutput(data));
251
+ printNextPageHint(data);
252
+ });
253
+ });
254
+ cmd.command("get <gid>").description("Get a project by GID").action(async (gid) => {
255
+ const data = await getProject(gid);
256
+ output(data, () => fmtProject(data));
257
+ });
258
+ cmd.command("counts <gid>").description("Get task counts for a project").option("--opt-fields <fields>", "Comma-separated project count fields to include").action(async (gid, opts) => {
259
+ const data = await getProjectTaskCounts(gid, opts.optFields ? { optFields: opts.optFields } : void 0);
260
+ output(data, () => fmtProjectCounts(gid, data, !opts.optFields));
261
+ });
262
+ addGidOption(cmd.command("search [text]").description("Search projects in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }).option("--completed", "Only completed projects").option("--no-completed", "Only incomplete projects").option("--team <gid[,gid...]>", "Team GIDs (any match)").option("--owner <gid[,gid...]>", "Owner user identifiers (any match)").option("--member <gid[,gid...]>", "Member user identifiers (any match)").option("--member-not <gid[,gid...]>", "Member user identifiers to exclude").option("--portfolio <gid[,gid...]>", "Portfolio GIDs (any match)").option("--completed-on <date>", "Exact completion date (YYYY-MM-DD)").option("--completed-on-before <date>", "Completion date before (YYYY-MM-DD)").option("--completed-on-after <date>", "Completion date after (YYYY-MM-DD)").option("--completed-at-before <datetime>", "Completion datetime before (ISO 8601)").option("--completed-at-after <datetime>", "Completion datetime after (ISO 8601)").option("--created-on <date>", "Exact creation date (YYYY-MM-DD)").option("--created-on-before <date>", "Creation date before (YYYY-MM-DD)").option("--created-on-after <date>", "Creation date after (YYYY-MM-DD)").option("--created-at-before <datetime>", "Creation datetime before (ISO 8601)").option("--created-at-after <datetime>", "Creation datetime after (ISO 8601)").option("--due-on <date>", "Exact due date (YYYY-MM-DD)").option("--due-on-before <date>", "Due date before (YYYY-MM-DD)").option("--due-on-after <date>", "Due date after (YYYY-MM-DD)").option("--due-at-before <datetime>", "Due datetime before (ISO 8601)").option("--due-at-after <datetime>", "Due datetime after (ISO 8601)").option("--start-on <date>", "Exact start date (YYYY-MM-DD)").option("--start-on-before <date>", "Start date before (YYYY-MM-DD)").option("--start-on-after <date>", "Start date after (YYYY-MM-DD)").option("--sort-by <field>", "Sort field: due_date, created_at, completed_at, modified_at").option("--sort-asc", "Sort ascending (default: descending)").option("--opt-fields <fields>", "Comma-separated optional Asana fields to include").action(async (text, opts) => {
263
+ const data = await searchProjects(requiredGid(opts, "workspace", "Workspace GID"), {
264
+ text,
265
+ completed: opts.completed,
266
+ teamsAny: opts.team,
267
+ ownerAny: opts.owner,
268
+ membersAny: opts.member,
269
+ membersNot: opts.memberNot,
270
+ portfoliosAny: opts.portfolio,
271
+ completedOn: opts.completedOn,
272
+ completedOnBefore: opts.completedOnBefore,
273
+ completedOnAfter: opts.completedOnAfter,
274
+ completedAtBefore: opts.completedAtBefore,
275
+ completedAtAfter: opts.completedAtAfter,
276
+ createdOn: opts.createdOn,
277
+ createdOnBefore: opts.createdOnBefore,
278
+ createdOnAfter: opts.createdOnAfter,
279
+ createdAtBefore: opts.createdAtBefore,
280
+ createdAtAfter: opts.createdAtAfter,
281
+ dueOn: opts.dueOn,
282
+ dueOnBefore: opts.dueOnBefore,
283
+ dueOnAfter: opts.dueOnAfter,
284
+ dueAtBefore: opts.dueAtBefore,
285
+ dueAtAfter: opts.dueAtAfter,
286
+ startOn: opts.startOn,
287
+ startOnBefore: opts.startOnBefore,
288
+ startOnAfter: opts.startOnAfter,
289
+ sortBy: opts.sortBy,
290
+ sortAscending: opts.sortAsc,
291
+ optFields: opts.optFields
292
+ });
293
+ output(data, () => fmtProjectList(data));
294
+ });
295
+ addGidOption(cmd.command("create <name>").description("Create a new project"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }).option("--notes <text>", "Project notes").option("--html-notes <html>", "Project notes as HTML").option("--color <color>", "Project color").option("--privacy-setting <value>", "Project privacy setting").option("--default-view <value>", "Project default view").option("--due-on <date>", "Due date (YYYY-MM-DD)").option("--start-on <date>", "Start date (YYYY-MM-DD)").action(async (name, opts) => {
296
+ const data = await createProject(requiredGid(opts, "workspace", "Workspace GID"), name, buildProjectCreateFields({
297
+ notes: opts.notes,
298
+ htmlNotes: opts.htmlNotes,
299
+ color: opts.color,
300
+ privacySetting: opts.privacySetting,
301
+ defaultView: opts.defaultView,
302
+ dueOn: opts.dueOn,
303
+ startOn: opts.startOn
304
+ }));
305
+ output(data, () => fmtProject(data));
306
+ });
307
+ cmd.command("update <gid>").description("Update a project").option("--name <name>", "New name").option("--notes <text>", "New notes").option("--html-notes <html>", "New notes as HTML").option("--color <color>", "New color").option("--privacy-setting <value>", "New project privacy setting").option("--default-view <value>", "New default view").option("--due-on <date>", "Due date (YYYY-MM-DD)").option("--start-on <date>", "Start date (YYYY-MM-DD)").option("--clear-due-on", "Clear the due date").option("--clear-start-on", "Clear the start date").action(async (gid, opts) => {
308
+ const data = await updateProject(gid, {
309
+ name: opts.name,
310
+ ...buildProjectUpdateFields({
311
+ notes: opts.notes,
312
+ htmlNotes: opts.htmlNotes,
313
+ color: opts.color,
314
+ privacySetting: opts.privacySetting,
315
+ defaultView: opts.defaultView,
316
+ dueOn: opts.dueOn,
317
+ startOn: opts.startOn,
318
+ clearDueOn: opts.clearDueOn,
319
+ clearStartOn: opts.clearStartOn
320
+ })
321
+ });
322
+ output(data, () => fmtProject(data));
323
+ });
324
+ cmd.command("delete <gid>").description("Delete a project").action(async (gid) => {
325
+ await deleteProject(gid);
326
+ console.log(`Deleted project ${gid}`);
327
+ });
328
+ cmd.command("export <gid>").description("Export a project with all sections and tasks").option("--output <file>", "Write output to a file instead of stdout").action(async (gid, opts) => {
329
+ const data = await exportProject(gid);
330
+ if (process.argv.includes("--json")) {
331
+ const json = JSON.stringify(data, null, 2);
332
+ if (opts.output) await writeFile(opts.output, json, "utf-8");
333
+ else console.log(json);
334
+ } else {
335
+ const md = renderProjectMarkdown(data);
336
+ if (opts.output) {
337
+ await writeFile(opts.output, md, "utf-8");
338
+ console.log(`Wrote ${opts.output}`);
339
+ } else console.log(md);
340
+ }
341
+ });
342
+ return cmd;
343
+ }
344
+
345
+ //#endregion
346
+ //#region src/sections/cli.ts
347
+ function fmtSection(s) {
348
+ printFields({
349
+ Name: s.name,
350
+ ID: s.gid
351
+ });
352
+ }
353
+ function sectionCommand() {
354
+ const cmd = new Command("section").description("Manage Asana sections");
355
+ addPaginationOptions(addGidOption(cmd.command("list").description("List sections in a project"), "project", "Project GID")).action(async (opts) => {
356
+ const data = await listSections(requiredGid(opts, "project", "Project GID"), paginationOptionsFromCli(opts));
357
+ output(data, () => {
358
+ printTable(itemsForOutput(data), [{
359
+ label: "Name",
360
+ get: (s) => s.name
361
+ }, {
362
+ label: "ID",
363
+ get: (s) => s.gid
364
+ }]);
365
+ printNextPageHint(data);
366
+ });
367
+ });
368
+ cmd.command("get <gid>").description("Get a section by GID").action(async (gid) => {
369
+ const data = await getSection(gid);
370
+ output(data, () => fmtSection(data));
371
+ });
372
+ addGidOption(cmd.command("create <name>").description("Create a section in a project"), "project", "Project GID").action(async (name, opts) => {
373
+ const data = await createSection(requiredGid(opts, "project", "Project GID"), name);
374
+ output(data, () => fmtSection(data));
375
+ });
376
+ cmd.command("update <gid>").description("Update a section").requiredOption("--name <name>", "New name").action(async (gid, opts) => {
377
+ const data = await updateSection(gid, opts.name);
378
+ output(data, () => fmtSection(data));
379
+ });
380
+ cmd.command("delete <gid>").description("Delete a section").action(async (gid) => {
381
+ await deleteSection(gid);
382
+ console.log(`Deleted section ${gid}`);
383
+ });
384
+ return cmd;
385
+ }
386
+
387
+ //#endregion
388
+ //#region src/stories/cli.ts
389
+ function fmtStory(s) {
390
+ printFields({
391
+ ID: s.gid,
392
+ Type: s.type ?? null,
393
+ By: s.created_by?.name ?? null,
394
+ At: s.created_at ?? null,
395
+ Text: s.text ?? null
396
+ });
397
+ }
398
+ function resolveStoryApi(api) {
399
+ if (typeof api === "function") return api();
400
+ return api ?? {
401
+ listStories,
402
+ createStory,
403
+ getTaskTemplateData
404
+ };
405
+ }
406
+ function storyCommand(name = "story", api) {
407
+ const cmd = new Command(name).description("Manage Asana stories (comments)");
408
+ addPaginationOptions(addGidOption(cmd.command("list").description("List stories for a task"), "task", "Task GID")).action(async (opts) => {
409
+ const data = await resolveStoryApi(api).listStories(requiredGid(opts, "task", "Task GID"), paginationOptionsFromCli(opts));
410
+ output(data, () => {
411
+ printTable(itemsForOutput(data), [
412
+ {
413
+ label: "ID",
414
+ get: (s) => s.gid
415
+ },
416
+ {
417
+ label: "Type",
418
+ get: (s) => s.type ?? ""
419
+ },
420
+ {
421
+ label: "By",
422
+ get: (s) => s.created_by?.name ?? ""
423
+ },
424
+ {
425
+ label: "Text",
426
+ get: (s) => (s.text ?? "").slice(0, 60)
427
+ }
428
+ ]);
429
+ printNextPageHint(data);
430
+ });
431
+ });
432
+ addGidOption(cmd.command("create [text]").description("Add a comment to a task").option("--html-text <html>", "Comment rich text as Asana HTML").option("--template", "Treat text as a template; interpolates {task.name}, {task.assignee}, {task.due_on}, {task.notes}"), "task", "Task GID").action(async (text, opts) => {
433
+ const taskGid = requiredGid(opts, "task", "Task GID");
434
+ const task = opts.template ? await resolveStoryApi(api).getTaskTemplateData(taskGid) : void 0;
435
+ const data = await resolveStoryApi(api).createStory(taskGid, {
436
+ ...text !== void 0 && { text: task ? interpolateTemplate(text, task) : text },
437
+ ...opts.htmlText !== void 0 && { html_text: task ? interpolateTemplate(opts.htmlText, task) : opts.htmlText }
438
+ });
439
+ output(data, () => fmtStory(data));
440
+ });
441
+ return cmd;
442
+ }
443
+
444
+ //#endregion
445
+ //#region src/tags/cli.ts
446
+ function fmtTag(t) {
447
+ printFields({
448
+ Name: t.name,
449
+ ID: t.gid,
450
+ Color: t.color ?? null
451
+ });
452
+ }
453
+ function fmtTaskList$1(tasks) {
454
+ printTable(tasks, [
455
+ {
456
+ label: "Name",
457
+ get: (t) => t.name
458
+ },
459
+ {
460
+ label: "ID",
461
+ get: (t) => t.gid
462
+ },
463
+ {
464
+ label: "Done",
465
+ get: (t) => t.completed ? "yes" : "no"
466
+ },
467
+ {
468
+ label: "Due",
469
+ get: (t) => t.due_on ?? ""
470
+ }
471
+ ]);
472
+ }
473
+ function resolveTagApi(api) {
474
+ if (typeof api === "function") return api();
475
+ return api ?? {
476
+ listTags,
477
+ getTag,
478
+ createTag,
479
+ updateTag,
480
+ deleteTag,
481
+ listTagsForTask,
482
+ listTasksForTag,
483
+ addTagToTask,
484
+ removeTagFromTask
485
+ };
486
+ }
487
+ function tagCommand(api) {
488
+ const cmd = new Command("tag").description("Manage Asana tags");
489
+ addPaginationOptions(addGidOption(cmd.command("list").description("List tags in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" })).action(async (opts) => {
490
+ const data = await resolveTagApi(api).listTags(requiredGid(opts, "workspace", "Workspace GID"), paginationOptionsFromCli(opts));
491
+ output(data, () => {
492
+ printTable(itemsForOutput(data), [
493
+ {
494
+ label: "Name",
495
+ get: (t) => t.name
496
+ },
497
+ {
498
+ label: "ID",
499
+ get: (t) => t.gid
500
+ },
501
+ {
502
+ label: "Color",
503
+ get: (t) => t.color ?? ""
504
+ }
505
+ ]);
506
+ printNextPageHint(data);
507
+ });
508
+ });
509
+ cmd.command("get <gid>").description("Get a tag by GID").action(async (gid) => {
510
+ const data = await resolveTagApi(api).getTag(gid);
511
+ output(data, () => fmtTag(data));
512
+ });
513
+ const createCmd = addGidOption(cmd.command("create <name>").description("Create a tag"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" });
514
+ createCmd.option("--color <color>", "Tag color").option("--notes <text>", "Tag notes");
515
+ createCmd.action(async (name, opts) => {
516
+ const data = await resolveTagApi(api).createTag(requiredGid(opts, "workspace", "Workspace GID"), name, {
517
+ ...opts.color !== void 0 && { color: opts.color },
518
+ ...opts.notes !== void 0 && { notes: opts.notes }
519
+ });
520
+ output(data, () => fmtTag(data));
521
+ });
522
+ cmd.command("update <gid>").description("Update a tag").option("--name <name>", "New tag name").option("--color <color>", "New tag color").option("--notes <text>", "New tag notes").action(async (gid, opts) => {
523
+ const data = await resolveTagApi(api).updateTag(gid, {
524
+ ...opts.name !== void 0 && { name: opts.name },
525
+ ...opts.color !== void 0 && { color: opts.color },
526
+ ...opts.notes !== void 0 && { notes: opts.notes }
527
+ });
528
+ output(data, () => fmtTag(data));
529
+ });
530
+ cmd.command("delete <gid>").description("Delete a tag").action(async (gid) => {
531
+ await resolveTagApi(api).deleteTag(gid);
532
+ console.log(`Deleted tag ${gid}`);
533
+ });
534
+ const taskCmd = cmd.command("task").description("Manage task tag relationships");
535
+ addPaginationOptions(taskCmd.command("list <task-gid>").description("List tags for a task")).action(async (taskGid, opts) => {
536
+ const data = await resolveTagApi(api).listTagsForTask(taskGid, paginationOptionsFromCli(opts));
537
+ output(data, () => {
538
+ printTable(itemsForOutput(data), [
539
+ {
540
+ label: "Name",
541
+ get: (t) => t.name
542
+ },
543
+ {
544
+ label: "ID",
545
+ get: (t) => t.gid
546
+ },
547
+ {
548
+ label: "Color",
549
+ get: (t) => t.color ?? ""
550
+ }
551
+ ]);
552
+ printNextPageHint(data);
553
+ });
554
+ });
555
+ taskCmd.command("add <task-gid> <tag-gid>").description("Add a tag to a task").action(async (taskGid, tagGid) => {
556
+ output(await resolveTagApi(api).addTagToTask(taskGid, tagGid), () => printFields({
557
+ Task: taskGid,
558
+ Tag: tagGid,
559
+ Status: "added"
560
+ }));
561
+ });
562
+ taskCmd.command("remove <task-gid> <tag-gid>").description("Remove a tag from a task").action(async (taskGid, tagGid) => {
563
+ output(await resolveTagApi(api).removeTagFromTask(taskGid, tagGid), () => printFields({
564
+ Task: taskGid,
565
+ Tag: tagGid,
566
+ Status: "removed"
567
+ }));
568
+ });
569
+ addPaginationOptions(cmd.command("tasks <tag-gid>").description("List tasks for a tag")).action(async (tagGid, opts) => {
570
+ const data = await resolveTagApi(api).listTasksForTag(tagGid, paginationOptionsFromCli(opts));
571
+ output(data, () => {
572
+ fmtTaskList$1(itemsForOutput(data));
573
+ printNextPageHint(data);
574
+ });
575
+ });
576
+ return cmd;
577
+ }
578
+
579
+ //#endregion
580
+ //#region src/tasks/cli.ts
581
+ function fmtTask(t) {
582
+ printFields({
583
+ Name: t.name,
584
+ ID: t.gid,
585
+ URL: t.permalink_url,
586
+ Assignee: t.assignee?.name ?? null,
587
+ Due: t.due_on ?? null,
588
+ Done: t.completed != null ? String(t.completed) : null,
589
+ Notes: t.notes || null
590
+ });
591
+ }
592
+ function fmtTaskList(tasks) {
593
+ printTable(tasks, [
594
+ {
595
+ label: "Name",
596
+ get: (t) => t.name
597
+ },
598
+ {
599
+ label: "ID",
600
+ get: (t) => t.gid
601
+ },
602
+ {
603
+ label: "Done",
604
+ get: (t) => t.completed ? "yes" : "no"
605
+ },
606
+ {
607
+ label: "Due",
608
+ get: (t) => t.due_on ?? ""
609
+ }
610
+ ]);
611
+ }
612
+ function collectOption(value, previous = []) {
613
+ return [...previous, value];
614
+ }
615
+ function taskCommand() {
616
+ const cmd = new Command("task").description("Manage Asana tasks");
617
+ addPaginationOptions(addGidOption(cmd.command("list").description("List tasks in a project"), "project", "Project GID").option("--completed-since <date>", "Only include tasks completed on or after this date (ISO 8601 or \"now\")").option("--incomplete", "Only show incomplete tasks (shorthand for --completed-since now)")).action(async (opts) => {
618
+ const data = await listTasks(requiredGid(opts, "project", "Project GID"), {
619
+ completedSince: opts.incomplete ? "now" : opts.completedSince,
620
+ ...paginationOptionsFromCli(opts)
621
+ });
622
+ output(data, () => {
623
+ fmtTaskList(itemsForOutput(data));
624
+ printNextPageHint(data);
625
+ });
626
+ });
627
+ addPaginationOptions(addGidOption(cmd.command("my-tasks").description("Manage My Tasks for the authenticated user").command("list").description("List My Tasks"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }).option("--completed-since <date>", "Only include tasks completed on or after this date (ISO 8601 or \"now\")").option("--incomplete", "Only show incomplete tasks (shorthand for --completed-since now)")).action(async (opts) => {
628
+ const data = await getMyTasks(requiredGid(opts, "workspace", "Workspace GID"), {
629
+ completedSince: opts.incomplete ? "now" : opts.completedSince,
630
+ ...paginationOptionsFromCli(opts)
631
+ });
632
+ output(data, () => {
633
+ fmtTaskList(itemsForOutput(data));
634
+ printNextPageHint(data);
635
+ });
636
+ });
637
+ cmd.command("get <gid>").description("Get a task by GID").action(async (gid) => {
638
+ const data = await getTask(gid);
639
+ output(data, () => fmtTask(data));
640
+ });
641
+ cmd.command("get-many <gids...>").description("Get multiple tasks by GID").option("--opt-fields <fields>", "Comma-separated optional Asana fields to include").action(async (gids, opts) => {
642
+ const data = await getTasksByGid(gids, opts.optFields ? { optFields: opts.optFields } : void 0);
643
+ output(data, () => {
644
+ if (data.length === 0) {
645
+ console.log("(none)");
646
+ return;
647
+ }
648
+ for (const item of data) if (item.ok) fmtTask(item.task);
649
+ else {
650
+ console.log(`Task ${item.gid}`);
651
+ console.log(`Status ${String(item.status)}`);
652
+ console.log(`Errors ${item.errors.map((error) => error && typeof error === "object" && "message" in error ? String(error.message) : JSON.stringify(error)).join("; ")}`);
653
+ }
654
+ });
655
+ });
656
+ addGidOption(addGidOption(addGidOption(addGidOption(cmd.command("create <name>").description("Create a new task"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }), "project", "Project GID"), "parent", "Parent task GID"), "assignee", "Assignee user GID").option("--notes <text>", "Task notes").option("--html-notes <html>", "Task notes as HTML").option("--due-on <date>", "Due date (YYYY-MM-DD)").option("--resource-subtype <subtype>", "Task resource subtype (e.g. default_task, milestone)").option("--follower <gid[,gid...]>", "Follower user GIDs").option("--custom-fields-json <json>", "Custom field values as a JSON object").option("--custom-field <gid=value>", "Custom field value override", collectOption, []).action(async (name, opts) => {
657
+ const data = await createTask(requiredGid(opts, "workspace", "Workspace GID"), name, buildTaskCreateFields({
658
+ notes: opts.notes,
659
+ htmlNotes: opts.htmlNotes,
660
+ assignee: normalizedGid(opts, "assignee"),
661
+ projectInput: opts.projectGid ?? opts.project,
662
+ followerInput: opts.follower,
663
+ dueOn: opts.dueOn,
664
+ parent: normalizedGid(opts, "parent"),
665
+ resourceSubtype: opts.resourceSubtype,
666
+ customFieldsJson: opts.customFieldsJson,
667
+ customFieldEntries: opts.customField
668
+ }));
669
+ output(data, () => fmtTask(data));
670
+ });
671
+ addGidOption(addGidOption(cmd.command("update <gid>").description("Update a task").option("--name <name>", "New name").option("--notes <text>", "New notes").option("--html-notes <html>", "New notes as HTML").option("--completed", "Mark as completed").option("--due-on <date>", "Due date (YYYY-MM-DD)").option("--clear-due-on", "Clear the due date").option("--clear-parent", "Remove the parent task relationship").option("--resource-subtype <subtype>", "Task resource subtype (e.g. default_task, milestone)").option("--custom-fields-json <json>", "Custom field values as a JSON object").option("--custom-field <gid=value>", "Custom field value override", collectOption, []), "parent", "Parent task GID"), "assignee", "Assignee user GID").action(async (gid, opts) => {
672
+ const data = await updateTask(gid, buildTaskUpdateFields({
673
+ name: opts.name,
674
+ notes: opts.notes,
675
+ htmlNotes: opts.htmlNotes,
676
+ completed: opts.completed,
677
+ dueOn: opts.dueOn,
678
+ clearDueOn: opts.clearDueOn,
679
+ assignee: normalizedGid(opts, "assignee"),
680
+ parent: normalizedGid(opts, "parent"),
681
+ clearParent: opts.clearParent,
682
+ resourceSubtype: opts.resourceSubtype,
683
+ customFieldsJson: opts.customFieldsJson,
684
+ customFieldEntries: opts.customField
685
+ }));
686
+ output(data, () => fmtTask(data));
687
+ });
688
+ cmd.command("delete <gid>").description("Delete a task").action(async (gid) => {
689
+ await deleteTask(gid);
690
+ console.log(`Deleted task ${gid}`);
691
+ });
692
+ const subtaskCmd = cmd.command("subtask").description("Manage subtasks");
693
+ addPaginationOptions(subtaskCmd.command("list <task-gid>").description("List subtasks of a task").option("--incomplete", "Only show incomplete subtasks").option("--assignee-email", "Include assignee email (opt_fields: assignee,assignee.email)").option("--follower-emails", "Include follower emails (opt_fields: followers,followers.email)").option("--num-subtasks", "Include subtask count (opt_fields: num_subtasks)").option("--custom-fields", "Include custom fields (opt_fields: custom_fields)")).action(async (taskGid, opts) => {
694
+ const extraFields = [
695
+ opts.assigneeEmail && "assignee,assignee.email",
696
+ opts.followerEmails && "followers,followers.email",
697
+ opts.numSubtasks && "num_subtasks",
698
+ opts.customFields && "custom_fields"
699
+ ].filter(Boolean).join(",");
700
+ const pagination = paginationOptionsFromCli(opts);
701
+ if (extraFields) pagination.optFields = [pagination.optFields, extraFields].filter(Boolean).join(",");
702
+ const data = await listSubtasks(taskGid, {
703
+ completedSince: opts.incomplete ? "now" : void 0,
704
+ ...pagination
705
+ });
706
+ output(data, () => {
707
+ fmtTaskList(itemsForOutput(data));
708
+ printNextPageHint(data);
709
+ });
710
+ });
711
+ addGidOption(subtaskCmd.command("create <task-gid> <name>").description("Create a subtask under a task").option("--notes <text>", "Task notes").option("--due-on <date>", "Due date (YYYY-MM-DD)"), "assignee", "Assignee user GID").action(async (taskGid, name, opts) => {
712
+ const data = await createSubtask(taskGid, name, {
713
+ notes: opts.notes,
714
+ assignee: normalizedGid(opts, "assignee"),
715
+ dueOn: opts.dueOn
716
+ });
717
+ output(data, () => fmtTask(data));
718
+ });
719
+ addGidOption(cmd.command("search [text]").description("Search tasks in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }).option("--completed", "Only completed tasks").option("--no-completed", "Only incomplete tasks").option("--subtask", "Only subtasks").option("--no-subtask", "Exclude subtasks").option("--has-attachment", "Only tasks with attachments").option("--is-blocking", "Only tasks blocking others").option("--is-blocked", "Only tasks blocked by others").option("--assignee <gid[,gid...]>", "Assignee GIDs (any match)").option("--assignee-not <gid[,gid...]>", "Assignee GIDs to exclude").option("--project <gid[,gid...]>", "Project GIDs (any match)").option("--project-not <gid[,gid...]>", "Project GIDs to exclude").option("--project-all <gid[,gid...]>", "Project GIDs (all must match)").option("--section <gid[,gid...]>", "Section GIDs (any match)").option("--section-not <gid[,gid...]>", "Section GIDs to exclude").option("--section-all <gid[,gid...]>", "Section GIDs (all must match)").option("--tag <gid[,gid...]>", "Tag GIDs (any match)").option("--tag-not <gid[,gid...]>", "Tag GIDs to exclude").option("--tag-all <gid[,gid...]>", "Tag GIDs (all must match)").option("--team <gid[,gid...]>", "Team GIDs (any match)").option("--portfolio <gid[,gid...]>", "Portfolio GIDs (any match)").option("--follower <gid[,gid...]>", "Follower user GIDs (any match)").option("--follower-not <gid[,gid...]>", "Follower user GIDs to exclude").option("--created-by <gid[,gid...]>", "Created-by user GIDs (any match)").option("--created-by-not <gid[,gid...]>", "Created-by user GIDs to exclude").option("--assigned-by <gid[,gid...]>", "Assigned-by user GIDs (any match)").option("--assigned-by-not <gid[,gid...]>", "Assigned-by user GIDs to exclude").option("--liked-by-not <gid[,gid...]>", "User GIDs who did not like the task").option("--commented-on-by-not <gid[,gid...]>", "User GIDs who did not comment on the task").option("--due-on <date>", "Exact due date (YYYY-MM-DD)").option("--due-on-before <date>", "Due date before (YYYY-MM-DD)").option("--due-on-after <date>", "Due date after (YYYY-MM-DD)").option("--due-at-before <datetime>", "Due datetime before (ISO 8601)").option("--due-at-after <datetime>", "Due datetime after (ISO 8601)").option("--start-on <date>", "Exact start date (YYYY-MM-DD)").option("--start-on-before <date>", "Start date before (YYYY-MM-DD)").option("--start-on-after <date>", "Start date after (YYYY-MM-DD)").option("--created-on <date>", "Exact creation date (YYYY-MM-DD)").option("--created-on-before <date>", "Creation date before (YYYY-MM-DD)").option("--created-on-after <date>", "Creation date after (YYYY-MM-DD)").option("--created-at-before <datetime>", "Creation datetime before (ISO 8601)").option("--created-at-after <datetime>", "Creation datetime after (ISO 8601)").option("--completed-on <date>", "Exact completion date (YYYY-MM-DD)").option("--completed-on-before <date>", "Completion date before (YYYY-MM-DD)").option("--completed-on-after <date>", "Completion date after (YYYY-MM-DD)").option("--completed-at-before <datetime>", "Completion datetime before (ISO 8601)").option("--completed-at-after <datetime>", "Completion datetime after (ISO 8601)").option("--modified-on <date>", "Exact modification date (YYYY-MM-DD)").option("--modified-on-before <date>", "Modification date before (YYYY-MM-DD)").option("--modified-on-after <date>", "Modification date after (YYYY-MM-DD)").option("--modified-at-before <datetime>", "Modification datetime before (ISO 8601)").option("--modified-at-after <datetime>", "Modification datetime after (ISO 8601)").option("--subtype <subtype>", "Resource subtype filter (e.g. milestone)").option("--sort-by <field>", "Sort field: due_date, created_at, completed_at, likes, modified_at").option("--sort-asc", "Sort ascending (default: descending)").option("--opt-fields <fields>", "Comma-separated optional Asana fields to include").action(async (text, opts) => {
720
+ const searchOpts = {
721
+ text,
722
+ completed: opts.completed,
723
+ isSubtask: opts.subtask,
724
+ hasAttachment: opts.hasAttachment,
725
+ isBlocking: opts.isBlocking,
726
+ isBlocked: opts.isBlocked,
727
+ assigneeAny: opts.assignee,
728
+ assigneeNot: opts.assigneeNot,
729
+ projectsAny: opts.project,
730
+ projectsNot: opts.projectNot,
731
+ projectsAll: opts.projectAll,
732
+ sectionsAny: opts.section,
733
+ sectionsNot: opts.sectionNot,
734
+ sectionsAll: opts.sectionAll,
735
+ tagsAny: opts.tag,
736
+ tagsNot: opts.tagNot,
737
+ tagsAll: opts.tagAll,
738
+ teamsAny: opts.team,
739
+ portfoliosAny: opts.portfolio,
740
+ followersAny: opts.follower,
741
+ followersNot: opts.followerNot,
742
+ createdByAny: opts.createdBy,
743
+ createdByNot: opts.createdByNot,
744
+ assignedByAny: opts.assignedBy,
745
+ assignedByNot: opts.assignedByNot,
746
+ likedByNot: opts.likedByNot,
747
+ commentedOnByNot: opts.commentedOnByNot,
748
+ dueOn: opts.dueOn,
749
+ dueOnBefore: opts.dueOnBefore,
750
+ dueOnAfter: opts.dueOnAfter,
751
+ dueAtBefore: opts.dueAtBefore,
752
+ dueAtAfter: opts.dueAtAfter,
753
+ startOn: opts.startOn,
754
+ startOnBefore: opts.startOnBefore,
755
+ startOnAfter: opts.startOnAfter,
756
+ createdOn: opts.createdOn,
757
+ createdOnBefore: opts.createdOnBefore,
758
+ createdOnAfter: opts.createdOnAfter,
759
+ createdAtBefore: opts.createdAtBefore,
760
+ createdAtAfter: opts.createdAtAfter,
761
+ completedOn: opts.completedOn,
762
+ completedOnBefore: opts.completedOnBefore,
763
+ completedOnAfter: opts.completedOnAfter,
764
+ completedAtBefore: opts.completedAtBefore,
765
+ completedAtAfter: opts.completedAtAfter,
766
+ modifiedOn: opts.modifiedOn,
767
+ modifiedOnBefore: opts.modifiedOnBefore,
768
+ modifiedOnAfter: opts.modifiedOnAfter,
769
+ modifiedAtBefore: opts.modifiedAtBefore,
770
+ modifiedAtAfter: opts.modifiedAtAfter,
771
+ resourceSubtype: opts.subtype,
772
+ sortBy: opts.sortBy,
773
+ sortAscending: opts.sortAsc,
774
+ optFields: opts.optFields
775
+ };
776
+ const data = await searchTasks(requiredGid(opts, "workspace", "Workspace GID"), searchOpts);
777
+ output(data, () => fmtTaskList(data));
778
+ });
779
+ const projectCmd = cmd.command("project").description("Manage project membership for a task");
780
+ addGidOption(projectCmd.command("add <task-gid> <project-gid>").description("Add a task to a project").option("--insert-after <gid>", "Insert after this task GID").option("--insert-before <gid>", "Insert before this task GID"), "section", "Section GID").action(async (taskGid, projectGid, opts) => {
781
+ if (opts.insertAfter && opts.insertBefore) throw new Error("--insert-after and --insert-before are mutually exclusive");
782
+ await addTaskToProject(taskGid, projectGid, {
783
+ sectionGid: normalizedGid(opts, "section"),
784
+ insertAfter: opts.insertAfter,
785
+ insertBefore: opts.insertBefore
786
+ });
787
+ console.log(`Added task ${taskGid} to project ${projectGid}`);
788
+ });
789
+ projectCmd.command("remove <task-gid> <project-gid>").description("Remove a task from a project").action(async (taskGid, projectGid) => {
790
+ await removeTaskFromProject(taskGid, projectGid);
791
+ console.log(`Removed task ${taskGid} from project ${projectGid}`);
792
+ });
793
+ const followerCmd = cmd.command("follower").description("Manage followers for a task");
794
+ followerCmd.command("add <task-gid> <follower-gids...>").description("Add followers to a task").action(async (taskGid, followerGids) => {
795
+ await addFollowersToTask(taskGid, followerGids);
796
+ console.log(`Added ${followerGids.length} follower(s) to task ${taskGid}`);
797
+ });
798
+ followerCmd.command("remove <task-gid> <follower-gids...>").description("Remove followers from a task").action(async (taskGid, followerGids) => {
799
+ await removeFollowersFromTask(taskGid, followerGids);
800
+ console.log(`Removed ${followerGids.length} follower(s) from task ${taskGid}`);
801
+ });
802
+ const dependencyCmd = cmd.command("dependency").description("Manage task dependencies (tasks this task depends on)");
803
+ dependencyCmd.command("list <task-gid>").description("List dependencies of a task").option("--opt-fields <fields>", "Comma-separated Asana fields to include").action(async (taskGid, opts) => {
804
+ const data = await getDependencies(taskGid, { optFields: opts.optFields });
805
+ output(data, () => fmtTaskList(data));
806
+ });
807
+ dependencyCmd.command("add <task-gid> <dep-gids...>").description("Add dependencies to a task (space-separated GIDs)").action(async (taskGid, depGids) => {
808
+ await addDependencies(taskGid, depGids);
809
+ console.log(`Added ${depGids.length} dependency(s) to task ${taskGid}`);
810
+ });
811
+ dependencyCmd.command("remove <task-gid> <dep-gids...>").description("Remove dependencies from a task (space-separated GIDs)").action(async (taskGid, depGids) => {
812
+ await removeDependencies(taskGid, depGids);
813
+ console.log(`Removed ${depGids.length} dependency(s) from task ${taskGid}`);
814
+ });
815
+ const dependentCmd = cmd.command("dependent").description("Manage task dependents (tasks that depend on this task)");
816
+ dependentCmd.command("list <task-gid>").description("List dependents of a task").option("--opt-fields <fields>", "Comma-separated Asana fields to include").action(async (taskGid, opts) => {
817
+ const data = await getDependents(taskGid, { optFields: opts.optFields });
818
+ output(data, () => fmtTaskList(data));
819
+ });
820
+ dependentCmd.command("add <task-gid> <dep-gids...>").description("Add dependents to a task (space-separated GIDs)").action(async (taskGid, depGids) => {
821
+ await addDependents(taskGid, depGids);
822
+ console.log(`Added ${depGids.length} dependent(s) to task ${taskGid}`);
823
+ });
824
+ dependentCmd.command("remove <task-gid> <dep-gids...>").description("Remove dependents from a task (space-separated GIDs)").action(async (taskGid, depGids) => {
825
+ await removeDependents(taskGid, depGids);
826
+ console.log(`Removed ${depGids.length} dependent(s) from task ${taskGid}`);
827
+ });
828
+ cmd.command("scan-todos [dir]").description("Scan source files for TODO/FIXME/HACK comments and return structured results").option("--ext <extensions>", "Comma-separated file extensions to scan (e.g. .ts,.py)", ".ts,.tsx,.js,.jsx,.mjs,.py,.go,.rs,.java,.rb").option("--exclude <dirs>", "Comma-separated directory names to skip", "node_modules,dist,.git,build,coverage,__pycache__").action(async (dir, opts) => {
829
+ const data = await scanTodos(dir ?? process.cwd(), {
830
+ extensions: opts.ext.split(",").map((e) => e.trim()),
831
+ exclude: opts.exclude.split(",").map((e) => e.trim())
832
+ });
833
+ output(data, () => {
834
+ if (data.length === 0) {
835
+ console.log("(none)");
836
+ return;
837
+ }
838
+ printTable(data, [
839
+ {
840
+ label: "File",
841
+ get: (t) => t.file
842
+ },
843
+ {
844
+ label: "Line",
845
+ get: (t) => String(t.line)
846
+ },
847
+ {
848
+ label: "Pattern",
849
+ get: (t) => t.pattern
850
+ },
851
+ {
852
+ label: "Text",
853
+ get: (t) => t.text
854
+ }
855
+ ]);
856
+ });
857
+ });
858
+ return cmd;
859
+ }
860
+
861
+ //#endregion
862
+ //#region src/teams/cli.ts
863
+ function teamCommand() {
864
+ const cmd = new Command("team").description("Manage Asana teams");
865
+ addPaginationOptions(addGidOption(cmd.command("list").description("List teams in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" })).action(async (opts) => {
866
+ const data = await listTeams(requiredGid(opts, "workspace", "Workspace GID"), paginationOptionsFromCli(opts));
867
+ output(data, () => {
868
+ printTable(itemsForOutput(data), [{
869
+ label: "Name",
870
+ get: (t) => t.name
871
+ }, {
872
+ label: "ID",
873
+ get: (t) => t.gid
874
+ }]);
875
+ printNextPageHint(data);
876
+ });
877
+ });
878
+ cmd.command("get <gid>").description("Get a team by GID").action(async (gid) => {
879
+ const data = await getTeam(gid);
880
+ output(data, () => printFields({
881
+ Name: data.name,
882
+ ID: data.gid
883
+ }));
884
+ });
885
+ return cmd;
886
+ }
887
+
888
+ //#endregion
889
+ //#region src/users/cli.ts
890
+ function fmtUser(u) {
891
+ printFields({
892
+ Name: u.name,
893
+ ID: u.gid,
894
+ Email: u.email ?? null
895
+ });
896
+ }
897
+ function fmtUserList(users) {
898
+ printTable(users, [
899
+ {
900
+ label: "Name",
901
+ get: (u) => u.name
902
+ },
903
+ {
904
+ label: "ID",
905
+ get: (u) => u.gid
906
+ },
907
+ {
908
+ label: "Email",
909
+ get: (u) => u.email ?? ""
910
+ }
911
+ ]);
912
+ }
913
+ function userCommand() {
914
+ const cmd = new Command("user").description("Manage Asana users");
915
+ addPaginationOptions(addGidOption(cmd.command("list").description("List users in a workspace"), "workspace", "Workspace GID", { env: "ASANA_WORKSPACE" }), { limit: false }).action(async (opts) => {
916
+ const data = await listUsers(requiredGid(opts, "workspace", "Workspace GID"), paginationOptionsFromCli(opts));
917
+ output(data, () => {
918
+ fmtUserList(itemsForOutput(data));
919
+ printNextPageHint(data);
920
+ });
921
+ });
922
+ cmd.command("get <gid>").description("Get a user by GID").action(async (gid) => {
923
+ const data = await getUser(gid);
924
+ output(data, () => fmtUser(data));
925
+ });
926
+ cmd.command("me").description("Get the authenticated user").action(async () => {
927
+ const data = await getMe();
928
+ output(data, () => fmtUser(data));
929
+ });
930
+ return cmd;
931
+ }
932
+
933
+ //#endregion
934
+ //#region src/workspaces/cli.ts
935
+ function workspaceCommand() {
936
+ const cmd = new Command("workspace").description("Manage Asana workspaces");
937
+ addPaginationOptions(cmd.command("list").description("List all workspaces")).action(async (opts) => {
938
+ const data = await listWorkspaces(paginationOptionsFromCli(opts));
939
+ output(data, () => {
940
+ printTable(itemsForOutput(data), [{
941
+ label: "Name",
942
+ get: (w) => w.name
943
+ }, {
944
+ label: "ID",
945
+ get: (w) => w.gid
946
+ }]);
947
+ printNextPageHint(data);
948
+ });
949
+ });
950
+ cmd.command("get <gid>").description("Get a workspace by GID").action(async (gid) => {
951
+ const data = await getWorkspace(gid);
952
+ output(data, () => printFields({
953
+ Name: data.name,
954
+ ID: data.gid
955
+ }));
956
+ });
957
+ return cmd;
958
+ }
959
+
960
+ //#endregion
961
+ //#region src/cli.ts
962
+ const program = new Command();
963
+ let runtimeContext;
964
+ function getRuntimeContext() {
965
+ runtimeContext ??= createRuntimeContext();
966
+ return runtimeContext;
967
+ }
968
+ program.name("cyber-asana").description("Asana CLI for AI agents").version("0.0.0").option("--token <token>", "Asana PAT — overrides ASANA_TOKEN env var").option("--json", "Output raw JSON instead of formatted text").addHelpText("after", "\nAuthentication: set ASANA_TOKEN env var or pass --token <pat>.").hook("preAction", () => {
969
+ const { token } = program.opts();
970
+ if (token) setTokenOverride(token);
971
+ });
972
+ program.addCommand(workspaceCommand());
973
+ program.addCommand(projectCommand());
974
+ program.addCommand(taskCommand());
975
+ program.addCommand(sectionCommand());
976
+ program.addCommand(userCommand());
977
+ program.addCommand(teamCommand());
978
+ program.addCommand(portfolioCommand());
979
+ program.addCommand(goalCommand());
980
+ program.addCommand(tagCommand(() => getRuntimeContext().tags));
981
+ program.addCommand(attachmentCommand());
982
+ program.addCommand(storyCommand("story", () => getRuntimeContext().stories));
983
+ program.addCommand(storyCommand("comment", () => getRuntimeContext().stories));
984
+ program.parseAsync(process.argv).catch((err) => {
985
+ if (err && typeof err === "object" && "response" in err) {
986
+ const msgs = err.response?.body?.errors?.map((e) => e.message);
987
+ if (msgs?.length) {
988
+ console.error(`Asana API error: ${msgs.join("; ")}`);
989
+ process.exit(1);
990
+ }
991
+ }
992
+ console.error(err instanceof Error ? err.message : String(err));
993
+ process.exit(1);
994
+ });
995
+
996
+ //#endregion
997
+ export { };
998
+ //# sourceMappingURL=cli.js.map