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.d.ts +1 -0
- package/dist/cli.js +998 -0
- package/dist/cli.js.map +1 -0
- package/dist/composition-Ds2nQa_V.js +1188 -0
- package/dist/composition-Ds2nQa_V.js.map +1 -0
- package/dist/index.d.ts +658 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +915 -0
- package/dist/mcp.js.map +1 -0
- package/dist/write-options-CRFsXUQl.js +118 -0
- package/dist/write-options-CRFsXUQl.js.map +1 -0
- package/license +21 -0
- package/package.json +70 -0
- package/readme.md +372 -0
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
|