@shortcut-cli/shortcut-cli 3.8.1 → 5.0.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/README.md +549 -10
- package/build/bin/short-api.js +21 -22
- package/build/bin/short-create.js +58 -38
- package/build/bin/short-custom-field.js +50 -0
- package/build/bin/short-custom-fields.js +29 -0
- package/build/bin/short-doc.js +38 -41
- package/build/bin/short-docs.js +34 -27
- package/build/bin/short-epic.js +147 -36
- package/build/bin/short-epics.js +23 -24
- package/build/bin/short-find.js +4 -5
- package/build/bin/short-install.js +73 -29
- package/build/bin/short-iteration.js +180 -0
- package/build/bin/short-iterations.js +62 -0
- package/build/bin/short-label.js +130 -0
- package/build/bin/short-labels.js +27 -0
- package/build/bin/short-members.js +17 -21
- package/build/bin/short-objective.js +151 -0
- package/build/bin/short-objectives.js +63 -0
- package/build/bin/short-projects.js +17 -21
- package/build/bin/short-search.js +23 -32
- package/build/bin/short-story.js +350 -104
- package/build/bin/short-team.js +78 -0
- package/build/bin/short-teams.js +28 -0
- package/build/bin/short-workflows.js +12 -16
- package/build/bin/short-workspace.js +27 -28
- package/build/bin/short.js +5 -8
- package/build/lib/client.js +7 -9
- package/build/lib/configure.js +20 -29
- package/build/lib/spinner.js +3 -8
- package/build/lib/stories.js +171 -129
- package/build/package.js +3 -16
- package/package.json +67 -67
- package/build/_virtual/rolldown_runtime.js +0 -29
package/build/bin/short-epic.js
CHANGED
|
@@ -1,75 +1,186 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let child_process = require("child_process");
|
|
10
|
-
|
|
2
|
+
import { loadConfig } from "../lib/configure.js";
|
|
3
|
+
import client from "../lib/client.js";
|
|
4
|
+
import spinner from "../lib/spinner.js";
|
|
5
|
+
import stories_default from "../lib/stories.js";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import { exec } from "child_process";
|
|
11
9
|
//#region src/bin/short-epic.ts
|
|
12
|
-
const config =
|
|
13
|
-
const spin =
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const spin = spinner();
|
|
14
12
|
const log = console.log;
|
|
15
|
-
const program =
|
|
16
|
-
program.command("create").description("create a new epic").option("-n, --name [text]", "Set name of epic, required", "").option("-d, --description [text]", "Set description of epic", "").option("-s, --state [name]", "Set state of epic (to do, in progress, done)", "").option("--deadline [date]", "Set deadline for epic (YYYY-MM-DD)", "").option("--planned-start [date]", "Set planned start date (YYYY-MM-DD)", "").option("-o, --owners [id|name]", "Set owners of epic, comma-separated", "").option("-T, --team [id|name]", "Set team of epic", "").option("-l, --label [id|name]", "Set labels of epic, comma-separated", "").option("-M, --milestone [id]", "Set milestone of epic (deprecated, use objectives)", "").option("-I, --idonly", "Print only ID of epic result").option("-O, --open", "Open epic in browser").action(createEpic);
|
|
13
|
+
const program = new Command().usage("[command] [options]").description("create, view, or update epics");
|
|
14
|
+
program.command("create").description("create a new epic").option("-n, --name [text]", "Set name of epic, required", "").option("-d, --description [text]", "Set description of epic", "").option("-s, --state [name]", "Set state of epic (to do, in progress, done)", "").option("--deadline [date]", "Set deadline for epic (YYYY-MM-DD)", "").option("--planned-start [date]", "Set planned start date (YYYY-MM-DD)", "").option("-o, --owners [id|name]", "Set owners of epic, comma-separated", "").option("-T, --team [id|name]", "Set team of epic", "").option("-l, --label [id|name]", "Set labels of epic, comma-separated", "").option("-M, --milestone [id]", "Set milestone of epic (deprecated, use objectives)", "").option("--objectives [id|name]", "Set objectives of epic, comma-separated", "").option("-I, --idonly", "Print only ID of epic result").option("-O, --open", "Open epic in browser").action(createEpic);
|
|
15
|
+
program.command("view <id>").description("view an epic by id").option("-O, --open", "Open epic in browser").action(viewEpic);
|
|
16
|
+
program.command("update <id>").description("update an existing epic").option("-n, --name [text]", "Set name of epic", "").option("-d, --description [text]", "Set description of epic", "").option("-s, --state [name]", "Set state of epic (to do, in progress, done)", "").option("--deadline [date]", "Set deadline for epic (YYYY-MM-DD)", "").option("--planned-start [date]", "Set planned start date (YYYY-MM-DD)", "").option("-o, --owners [id|name]", "Set owners of epic, comma-separated", "").option("-T, --team [id|name]", "Set team of epic", "").option("-l, --label [id|name]", "Set labels of epic, comma-separated", "").option("-M, --milestone [id]", "Set milestone of epic (deprecated, use objectives)", "").option("--objectives [id|name]", "Set objectives of epic, comma-separated", "").option("-a, --archived", "Archive epic").option("-O, --open", "Open epic in browser").action(updateEpic);
|
|
17
|
+
program.command("stories <id>").description("list stories in an epic").option("-d, --detailed", "Show more details for each story").option("-f, --format [template]", "Format each story output by template", "").action(listEpicStories);
|
|
18
|
+
program.command("comments <id>").description("list comments on an epic").option("-d, --detailed", "Show nested replies for each comment").action(listEpicComments);
|
|
17
19
|
program.parse(process.argv);
|
|
18
20
|
async function createEpic(options) {
|
|
19
|
-
const entities = await
|
|
21
|
+
const entities = await stories_default.fetchEntities();
|
|
20
22
|
if (!options.idonly) spin.start();
|
|
21
|
-
const epicData = { name: options.name };
|
|
23
|
+
const epicData = { name: options.name ?? "" };
|
|
22
24
|
if (options.description) epicData.description = options.description;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
todo: "to do",
|
|
26
|
-
"to do": "to do",
|
|
27
|
-
inprogress: "in progress",
|
|
28
|
-
"in progress": "in progress",
|
|
29
|
-
done: "done"
|
|
30
|
-
};
|
|
31
|
-
const mappedState = stateMap[options.state.toLowerCase().replace(/[^a-z]/g, "")] || stateMap[options.state.toLowerCase()];
|
|
32
|
-
if (mappedState) epicData.state = mappedState;
|
|
33
|
-
}
|
|
25
|
+
const state = normalizeEpicState(options.state);
|
|
26
|
+
if (state) epicData.state = state;
|
|
34
27
|
if (options.deadline) epicData.deadline = new Date(options.deadline).toISOString();
|
|
35
28
|
if (options.plannedStart) epicData.planned_start_date = new Date(options.plannedStart).toISOString();
|
|
36
|
-
if (options.owners) epicData.owner_ids =
|
|
29
|
+
if (options.owners) epicData.owner_ids = stories_default.findOwnerIds(entities, options.owners);
|
|
37
30
|
if (options.team) {
|
|
38
|
-
const group =
|
|
39
|
-
if (group
|
|
31
|
+
const group = stories_default.findGroup(entities, options.team);
|
|
32
|
+
if (group?.id) epicData.group_ids = [group.id];
|
|
40
33
|
}
|
|
41
|
-
if (options.label) epicData.labels =
|
|
34
|
+
if (options.label) epicData.labels = stories_default.findLabelNames(entities, options.label);
|
|
42
35
|
if (options.milestone) epicData.milestone_id = parseInt(options.milestone, 10);
|
|
36
|
+
if (options.objectives) epicData.objective_ids = stories_default.findObjectiveIds(entities, options.objectives);
|
|
43
37
|
let epic;
|
|
44
38
|
if (!epicData.name) {
|
|
45
39
|
if (!options.idonly) spin.stop(true);
|
|
46
40
|
log("Must provide --name");
|
|
47
41
|
process.exit(1);
|
|
48
42
|
} else try {
|
|
49
|
-
epic = await
|
|
43
|
+
epic = await client.createEpic(epicData).then((r) => r.data);
|
|
50
44
|
} catch (e) {
|
|
51
45
|
if (!options.idonly) spin.stop(true);
|
|
52
|
-
log("Error creating epic:", e.message
|
|
46
|
+
log("Error creating epic:", e.message ?? String(e));
|
|
53
47
|
process.exit(1);
|
|
54
48
|
}
|
|
55
49
|
if (!options.idonly) spin.stop(true);
|
|
56
50
|
if (epic) if (options.idonly) log(epic.id);
|
|
57
51
|
else {
|
|
58
52
|
printEpic(epic);
|
|
59
|
-
if (options.open) (
|
|
53
|
+
if (options.open) openURL(`https://app.shortcut.com/${config.urlSlug}/epic/${epic.id}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function viewEpic(id, options) {
|
|
57
|
+
spin.start();
|
|
58
|
+
try {
|
|
59
|
+
const epic = await client.getEpic(parseInt(id, 10)).then((r) => r.data);
|
|
60
|
+
spin.stop(true);
|
|
61
|
+
printEpic(epic);
|
|
62
|
+
if (options.open) openURL(epic.app_url);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
spin.stop(true);
|
|
65
|
+
const error = e;
|
|
66
|
+
if (error.response?.status === 404) log(`Epic #${id} not found`);
|
|
67
|
+
else log("Error fetching epic:", error.message ?? String(e));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function updateEpic(id, options) {
|
|
72
|
+
const entities = await stories_default.fetchEntities();
|
|
73
|
+
const updateData = {};
|
|
74
|
+
if (options.name) updateData.name = options.name;
|
|
75
|
+
if (options.description) updateData.description = options.description;
|
|
76
|
+
const state = normalizeEpicState(options.state);
|
|
77
|
+
if (state) updateData.state = state;
|
|
78
|
+
if (options.deadline) updateData.deadline = new Date(options.deadline).toISOString();
|
|
79
|
+
if (options.plannedStart) updateData.planned_start_date = new Date(options.plannedStart).toISOString();
|
|
80
|
+
if (options.owners) updateData.owner_ids = stories_default.findOwnerIds(entities, options.owners);
|
|
81
|
+
if (options.team) {
|
|
82
|
+
const group = stories_default.findGroup(entities, options.team);
|
|
83
|
+
if (group?.id) updateData.group_ids = [group.id];
|
|
84
|
+
}
|
|
85
|
+
if (options.label) updateData.labels = stories_default.findLabelNames(entities, options.label);
|
|
86
|
+
if (options.milestone) updateData.milestone_id = parseInt(options.milestone, 10);
|
|
87
|
+
if (options.objectives) updateData.objective_ids = stories_default.findObjectiveIds(entities, options.objectives);
|
|
88
|
+
if (options.archived) updateData.archived = true;
|
|
89
|
+
if (Object.keys(updateData).length === 0) {
|
|
90
|
+
log("No updates provided. Use --name, --description, --state, --deadline, --planned-start, --owners, --team, --label, --milestone, --objectives, or --archived");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
spin.start();
|
|
94
|
+
try {
|
|
95
|
+
const epic = await client.updateEpic(parseInt(id, 10), updateData).then((r) => r.data);
|
|
96
|
+
spin.stop(true);
|
|
97
|
+
printEpic(epic);
|
|
98
|
+
if (options.open) openURL(epic.app_url);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
spin.stop(true);
|
|
101
|
+
const error = e;
|
|
102
|
+
if (error.response?.status === 404) log(`Epic #${id} not found`);
|
|
103
|
+
else log("Error updating epic:", error.message ?? String(e));
|
|
104
|
+
process.exit(1);
|
|
60
105
|
}
|
|
61
106
|
}
|
|
107
|
+
async function listEpicStories(id, options) {
|
|
108
|
+
spin.start();
|
|
109
|
+
try {
|
|
110
|
+
const entities = await stories_default.fetchEntities();
|
|
111
|
+
const stories = await client.listEpicStories(parseInt(id, 10), { includes_description: !!options.detailed }).then((r) => r.data);
|
|
112
|
+
spin.stop(true);
|
|
113
|
+
if (stories.length === 0) {
|
|
114
|
+
log(`No stories found in epic #${id}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
stories.map((story) => stories_default.hydrateStory(entities, story)).forEach(options.detailed ? (story) => stories_default.printDetailedStory(story, entities) : stories_default.printFormattedStory({ format: options.format }));
|
|
118
|
+
} catch (e) {
|
|
119
|
+
spin.stop(true);
|
|
120
|
+
const error = e;
|
|
121
|
+
if (error.response?.status === 404) log(`Epic #${id} not found`);
|
|
122
|
+
else log("Error fetching epic stories:", error.message ?? String(e));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function listEpicComments(id, options) {
|
|
127
|
+
spin.start();
|
|
128
|
+
try {
|
|
129
|
+
const entities = await stories_default.fetchEntities();
|
|
130
|
+
const comments = await client.listEpicComments(parseInt(id, 10)).then((r) => r.data);
|
|
131
|
+
spin.stop(true);
|
|
132
|
+
if (comments.length === 0) {
|
|
133
|
+
log(`No comments found on epic #${id}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
comments.forEach((comment) => printEpicComment(comment, entities.membersById, options.detailed));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
spin.stop(true);
|
|
139
|
+
const error = e;
|
|
140
|
+
if (error.response?.status === 404) log(`Epic #${id} not found`);
|
|
141
|
+
else log("Error fetching epic comments:", error.message ?? String(e));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function normalizeEpicState(state) {
|
|
146
|
+
if (!state) return void 0;
|
|
147
|
+
const stateMap = {
|
|
148
|
+
todo: "to do",
|
|
149
|
+
"to do": "to do",
|
|
150
|
+
inprogress: "in progress",
|
|
151
|
+
"in progress": "in progress",
|
|
152
|
+
done: "done"
|
|
153
|
+
};
|
|
154
|
+
return stateMap[state.toLowerCase().replace(/[^a-z]/g, "")] || stateMap[state.toLowerCase()];
|
|
155
|
+
}
|
|
62
156
|
function printEpic(epic) {
|
|
63
157
|
log(`#${epic.id} ${epic.name}`);
|
|
64
158
|
if (epic.description) log(`Description:\t${epic.description}`);
|
|
65
159
|
log(`State:\t\t${epic.state}`);
|
|
160
|
+
log(`Archived:\t${epic.archived ? "yes" : "no"}`);
|
|
66
161
|
if (epic.milestone_id) log(`Milestone:\t${epic.milestone_id}`);
|
|
162
|
+
if (epic.objective_ids && epic.objective_ids.length > 0) log(`Objectives:\t${epic.objective_ids.join(", ")}`);
|
|
67
163
|
if (epic.deadline) log(`Deadline:\t${epic.deadline}`);
|
|
68
164
|
if (epic.planned_start_date) log(`Planned Start:\t${epic.planned_start_date}`);
|
|
69
165
|
if (epic.owner_ids && epic.owner_ids.length > 0) log(`Owners:\t\t${epic.owner_ids.join(", ")}`);
|
|
70
166
|
if (epic.group_ids && epic.group_ids.length > 0) log(`Teams:\t\t${epic.group_ids.join(", ")}`);
|
|
71
|
-
if (epic.labels && epic.labels.length > 0) log(`Labels:\t\t${epic.labels.map((
|
|
167
|
+
if (epic.labels && epic.labels.length > 0) log(`Labels:\t\t${epic.labels.map((label) => label.name).join(", ")}`);
|
|
72
168
|
log(`URL:\t\t${epic.app_url}`);
|
|
73
169
|
}
|
|
74
|
-
|
|
75
|
-
|
|
170
|
+
function printEpicComment(comment, membersById, detailed, depth = 0) {
|
|
171
|
+
const indent = " ".repeat(depth);
|
|
172
|
+
const author = membersById?.get(comment.author_id)?.profile;
|
|
173
|
+
const authorName = author ? `${author.name} (${author.mention_name})` : comment.author_id;
|
|
174
|
+
log(`${indent}#${comment.id} ${authorName}`);
|
|
175
|
+
log(`${indent}Created: ${comment.created_at}`);
|
|
176
|
+
if (comment.updated_at !== comment.created_at) log(`${indent}Updated: ${comment.updated_at}`);
|
|
177
|
+
log(`${indent}${comment.deleted ? "[deleted]" : comment.text || "_"}`);
|
|
178
|
+
log(`${indent}URL: ${comment.app_url}`);
|
|
179
|
+
log();
|
|
180
|
+
if (detailed) comment.comments.forEach((reply) => printEpicComment(reply, membersById, detailed, depth + 1));
|
|
181
|
+
}
|
|
182
|
+
function openURL(url) {
|
|
183
|
+
exec(`${os.platform() === "darwin" ? "open" : "xdg-open"} '${url}'`);
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export {};
|
package/build/bin/short-epics.js
CHANGED
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let chalk = require("chalk");
|
|
8
|
-
chalk = require_rolldown_runtime.__toESM(chalk);
|
|
9
|
-
|
|
2
|
+
import client from "../lib/client.js";
|
|
3
|
+
import spinner from "../lib/spinner.js";
|
|
4
|
+
import stories_default from "../lib/stories.js";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import chalk from "chalk";
|
|
10
7
|
//#region src/bin/short-epics.ts
|
|
11
8
|
const log = console.log;
|
|
12
|
-
const spin =
|
|
13
|
-
const
|
|
9
|
+
const spin = spinner();
|
|
10
|
+
const opts = new Command().description("Display epics available for stories").option("-a, --archived", "List only epics including archived", "").option("-c, --completed", "List only epics that have been completed", "").option("-d, --detailed", "List more details for each epic", "").option("-f, --format [template]", "Format each epic output by template", "").option("-M, --milestone [ID]", "List epics in milestone matching id", "").option("--objectives [id|name]", "List epics linked to objective id/name, comma-separated", "").option("-t, --title [query]", "List epics with name/title containing query", "").option("-s, --started", "List epics that have been started", "").parse(process.argv).opts();
|
|
14
11
|
const main = async () => {
|
|
15
12
|
spin.start();
|
|
16
|
-
const epics = await
|
|
13
|
+
const [epics, entities] = await Promise.all([client.listEpics().then((r) => r.data), stories_default.fetchEntities()]);
|
|
17
14
|
spin.stop(true);
|
|
18
|
-
const textMatch = new RegExp(
|
|
15
|
+
const textMatch = new RegExp(opts.title ?? "", "i");
|
|
16
|
+
const objectiveIds = opts.objectives ? stories_default.findObjectiveIds(entities, opts.objectives) : [];
|
|
19
17
|
epics.filter((epic) => {
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const matchesObjectives = objectiveIds.length === 0 || objectiveIds.some((objectiveId) => epic.objective_ids.includes(objectiveId));
|
|
19
|
+
return !!`${epic.name} ${epic.name}`.match(textMatch) && !!(opts.milestone ? String(epic.milestone_id) === opts.milestone : true) && matchesObjectives;
|
|
20
|
+
}).forEach((epic) => printItem(epic, entities.objectivesById));
|
|
22
21
|
};
|
|
23
|
-
const printItem = (epic) => {
|
|
24
|
-
if (epic.archived && !
|
|
25
|
-
if (!epic.started &&
|
|
26
|
-
if (!epic.completed &&
|
|
27
|
-
let defaultFormat = `#%id %t\nMilestone:\t%m\nState:\t\t%s\nDeadline:\t%dl\n`;
|
|
22
|
+
const printItem = (epic, objectivesById) => {
|
|
23
|
+
if (epic.archived && !opts.archived) return;
|
|
24
|
+
if (!epic.started && opts.started) return;
|
|
25
|
+
if (!epic.completed && opts.completed) return;
|
|
26
|
+
let defaultFormat = `#%id %t\nMilestone:\t%m\nObjectives:\t%obj\nState:\t\t%s\nArchived:\t%ar\nDeadline:\t%dl\n`;
|
|
28
27
|
defaultFormat += `Points:\t\t%p\nPoints Started: %ps\nPoints Done:\t%pd\nCompletion:\t%c\n`;
|
|
29
|
-
if (epic.archived) defaultFormat += `Archived:\t%ar\n`;
|
|
30
28
|
if (epic.started) defaultFormat += `Started:\t%st\n`;
|
|
31
29
|
if (epic.completed) defaultFormat += `Completed:\t%co\n`;
|
|
32
|
-
if (
|
|
33
|
-
|
|
30
|
+
if (opts.detailed) defaultFormat += `Description:\t%d\n`;
|
|
31
|
+
const objectiveNames = epic.objective_ids?.map((objectiveId) => objectivesById?.get(objectiveId)?.name || String(objectiveId)).join(", ") || "_";
|
|
32
|
+
log((opts.format || defaultFormat).replace(/%id/, chalk.bold(`${epic.id}`)).replace(/%t/, chalk.blue(`${epic.name}`)).replace(/%m/, `${epic.milestone_id || "_"}`).replace(/%obj/, objectiveNames).replace(/%s/, `${epic.state}`).replace(/%dl/, `${epic.deadline || "_"}`).replace(/%d/, `${epic.description}`).replace(/%p/, `${epic.stats.num_points}`).replace(/%ps/, `${epic.stats.num_points_started}`).replace(/%pd/, `${epic.stats.num_points_done}`).replace(/%ar/, `${epic.archived}`).replace(/%c/, `${Math.round(epic.stats.num_points_done / (epic.stats.num_points || 1) * 100)}%`).replace(/%st/, `${epic.started_at}`).replace(/%co/, `${epic.completed_at}`));
|
|
34
33
|
};
|
|
35
34
|
main();
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
//#endregion
|
|
36
|
+
export {};
|
package/build/bin/short-find.js
CHANGED
|
@@ -1,43 +1,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let prompt = require("prompt");
|
|
9
|
-
prompt = require_rolldown_runtime.__toESM(prompt);
|
|
10
|
-
|
|
2
|
+
import { loadCachedConfig, updateConfig } from "../lib/configure.js";
|
|
3
|
+
import { version } from "../package.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { ShortcutClient } from "@shortcut/client";
|
|
6
|
+
import { createInterface } from "readline/promises";
|
|
7
|
+
import { Writable } from "stream";
|
|
11
8
|
//#region src/bin/short-install.ts
|
|
12
|
-
const extant =
|
|
9
|
+
const extant = loadCachedConfig();
|
|
13
10
|
const log = console.log;
|
|
14
|
-
const
|
|
11
|
+
const TOKEN_PROMPT = "Enter your Shortcut API token. You can get one at https://app.shortcut.com/settings/account/api-tokens\nAPI token: ";
|
|
12
|
+
const opts = new Command().version(version).description("Install access token and other settings for the Shortcut API").option("-f, --force", "Force install/reinstall").option("-r, --refresh", "Refresh the configuration with details from Shortcut.").option("-t, --token <token>", "Shortcut API token to save without prompting").parse(process.argv).opts();
|
|
15
13
|
const enrichConfigWithMemberDetails = async (config) => {
|
|
16
14
|
log("Fetching user/member details from Shortcut...");
|
|
17
|
-
const
|
|
15
|
+
const clientConfig = {};
|
|
16
|
+
if (process.env.SHORTCUT_API_BASE_URL) clientConfig.baseURL = process.env.SHORTCUT_API_BASE_URL;
|
|
17
|
+
const member = await new ShortcutClient(config.token ?? "", clientConfig).getCurrentMemberInfo().then((r) => r.data);
|
|
18
18
|
return {
|
|
19
|
+
...config,
|
|
19
20
|
mentionName: member.mention_name,
|
|
20
|
-
urlSlug: member.workspace2.url_slug
|
|
21
|
-
...config
|
|
21
|
+
urlSlug: member.workspace2.url_slug
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
|
+
var MaskedWritable = class extends Writable {
|
|
25
|
+
muted = false;
|
|
26
|
+
_write(chunk, encoding, callback) {
|
|
27
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString();
|
|
28
|
+
if (!this.muted) {
|
|
29
|
+
process.stdout.write(text);
|
|
30
|
+
callback();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (text.includes("\n")) process.stdout.write("\n");
|
|
34
|
+
callback();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const promptForToken = async () => {
|
|
38
|
+
const output = new MaskedWritable();
|
|
39
|
+
const rl = createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output,
|
|
42
|
+
terminal: true
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
process.stdout.write(TOKEN_PROMPT);
|
|
46
|
+
output.muted = true;
|
|
47
|
+
const token = (await rl.question("")).trim();
|
|
48
|
+
output.muted = false;
|
|
49
|
+
process.stdout.write("\n");
|
|
50
|
+
return token;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
output.muted = false;
|
|
53
|
+
process.stdout.write("\n");
|
|
54
|
+
if (error instanceof Error && "code" in error && error.code === "ABORT_ERR") process.exit(130);
|
|
55
|
+
throw error;
|
|
56
|
+
} finally {
|
|
57
|
+
rl.close();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const resolveToken = async () => {
|
|
61
|
+
if (opts.token) return opts.token.trim();
|
|
62
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
63
|
+
console.error("No API token provided. Pass --token when running non-interactively.");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
return promptForToken();
|
|
67
|
+
};
|
|
24
68
|
const main = async () => {
|
|
25
|
-
if (
|
|
26
|
-
else if (!extant.token ||
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
log("Saving config...");
|
|
36
|
-
if (require_lib_configure.updateConfig(config)) log("Saved config");
|
|
37
|
-
else log("Error saving config");
|
|
69
|
+
if (opts.refresh) updateConfig(await enrichConfigWithMemberDetails(extant));
|
|
70
|
+
else if (!extant.token || opts.force) {
|
|
71
|
+
const token = await resolveToken();
|
|
72
|
+
if (!token) {
|
|
73
|
+
console.error("No API token provided.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const config = await enrichConfigWithMemberDetails({
|
|
77
|
+
...extant,
|
|
78
|
+
token
|
|
38
79
|
});
|
|
80
|
+
log("Saving config...");
|
|
81
|
+
if (updateConfig(config)) log("Saved config");
|
|
82
|
+
else log("Error saving config");
|
|
39
83
|
} else if (extant.token) log("A configuration/token is already saved. To override, re-run with --force");
|
|
40
84
|
};
|
|
41
85
|
main();
|
|
42
|
-
|
|
43
|
-
|
|
86
|
+
//#endregion
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "../lib/configure.js";
|
|
3
|
+
import client from "../lib/client.js";
|
|
4
|
+
import spinner from "../lib/spinner.js";
|
|
5
|
+
import stories_default from "../lib/stories.js";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { exec } from "child_process";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
//#region src/bin/short-iteration.ts
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const spin = spinner();
|
|
12
|
+
const log = console.log;
|
|
13
|
+
const program = new Command().usage("[command] [options]").description("view, create, update, or delete iterations");
|
|
14
|
+
program.command("view <id>").description("view an iteration by id").option("-O, --open", "Open iteration in browser").action(viewIteration);
|
|
15
|
+
program.command("create").description("create a new iteration").option("-n, --name [text]", "Set name of iteration, required", "").option("-d, --description [text]", "Set description of iteration", "").option("--start-date [date]", "Set start date (YYYY-MM-DD), required", "").option("--end-date [date]", "Set end date (YYYY-MM-DD), required", "").option("-T, --team [id|name]", "Set team/group of iteration", "").option("-I, --idonly", "Print only ID of iteration result").option("-O, --open", "Open iteration in browser").action(createIteration);
|
|
16
|
+
program.command("update <id>").description("update an existing iteration").option("-n, --name [text]", "Set name of iteration", "").option("-d, --description [text]", "Set description of iteration", "").option("--start-date [date]", "Set start date (YYYY-MM-DD)", "").option("--end-date [date]", "Set end date (YYYY-MM-DD)", "").option("-T, --team [id|name]", "Set team/group of iteration", "").option("-O, --open", "Open iteration in browser").action(updateIteration);
|
|
17
|
+
program.command("delete <id>").description("delete an iteration").action(deleteIteration);
|
|
18
|
+
program.command("stories <id>").description("list stories in an iteration").option("-f, --format [template]", "Format each story output by template", "").action(listIterationStories);
|
|
19
|
+
program.parse(process.argv);
|
|
20
|
+
async function viewIteration(id, options) {
|
|
21
|
+
spin.start();
|
|
22
|
+
try {
|
|
23
|
+
const iteration = await client.getIteration(parseInt(id, 10)).then((r) => r.data);
|
|
24
|
+
spin.stop(true);
|
|
25
|
+
printIteration(iteration);
|
|
26
|
+
if (options.open) exec(`open https://app.shortcut.com/${config.urlSlug}/iteration/${iteration.id}`);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
spin.stop(true);
|
|
29
|
+
const error = e;
|
|
30
|
+
if (error.response?.status === 404) log(`Iteration #${id} not found`);
|
|
31
|
+
else log("Error fetching iteration:", error.message ?? String(e));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function createIteration(options) {
|
|
36
|
+
const entities = await stories_default.fetchEntities();
|
|
37
|
+
if (!options.idonly) spin.start();
|
|
38
|
+
const iterationData = {
|
|
39
|
+
name: options.name || "",
|
|
40
|
+
start_date: options.startDate || "",
|
|
41
|
+
end_date: options.endDate || ""
|
|
42
|
+
};
|
|
43
|
+
if (options.description) iterationData.description = options.description;
|
|
44
|
+
if (options.team) {
|
|
45
|
+
const group = stories_default.findGroup(entities, options.team);
|
|
46
|
+
if (group?.id) iterationData.group_ids = [group.id];
|
|
47
|
+
}
|
|
48
|
+
if (!iterationData.name) {
|
|
49
|
+
if (!options.idonly) spin.stop(true);
|
|
50
|
+
log("Must provide --name");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (!iterationData.start_date) {
|
|
54
|
+
if (!options.idonly) spin.stop(true);
|
|
55
|
+
log("Must provide --start-date");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (!iterationData.end_date) {
|
|
59
|
+
if (!options.idonly) spin.stop(true);
|
|
60
|
+
log("Must provide --end-date");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
let iteration;
|
|
64
|
+
try {
|
|
65
|
+
iteration = await client.createIteration(iterationData).then((r) => r.data);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (!options.idonly) spin.stop(true);
|
|
68
|
+
log("Error creating iteration:", e.message ?? String(e));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!options.idonly) spin.stop(true);
|
|
72
|
+
if (iteration) if (options.idonly) log(iteration.id);
|
|
73
|
+
else {
|
|
74
|
+
printIteration(iteration);
|
|
75
|
+
if (options.open) exec(`open https://app.shortcut.com/${config.urlSlug}/iteration/${iteration.id}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function updateIteration(id, options) {
|
|
79
|
+
const entities = await stories_default.fetchEntities();
|
|
80
|
+
spin.start();
|
|
81
|
+
const updateData = {};
|
|
82
|
+
if (options.name) updateData.name = options.name;
|
|
83
|
+
if (options.description) updateData.description = options.description;
|
|
84
|
+
if (options.startDate) updateData.start_date = options.startDate;
|
|
85
|
+
if (options.endDate) updateData.end_date = options.endDate;
|
|
86
|
+
if (options.team) {
|
|
87
|
+
const group = stories_default.findGroup(entities, options.team);
|
|
88
|
+
if (group?.id) updateData.group_ids = [group.id];
|
|
89
|
+
}
|
|
90
|
+
if (Object.keys(updateData).length === 0) {
|
|
91
|
+
spin.stop(true);
|
|
92
|
+
log("No updates provided. Use --name, --description, --start-date, --end-date, or --team");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
let iteration;
|
|
96
|
+
try {
|
|
97
|
+
iteration = await client.updateIteration(parseInt(id, 10), updateData).then((r) => r.data);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
spin.stop(true);
|
|
100
|
+
const error = e;
|
|
101
|
+
if (error.response?.status === 404) log(`Iteration #${id} not found`);
|
|
102
|
+
else log("Error updating iteration:", error.message ?? String(e));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
spin.stop(true);
|
|
106
|
+
if (iteration) {
|
|
107
|
+
printIteration(iteration);
|
|
108
|
+
if (options.open) exec(`open https://app.shortcut.com/${config.urlSlug}/iteration/${iteration.id}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function deleteIteration(id) {
|
|
112
|
+
spin.start();
|
|
113
|
+
try {
|
|
114
|
+
await client.deleteIteration(parseInt(id, 10));
|
|
115
|
+
spin.stop(true);
|
|
116
|
+
log(`Iteration #${id} deleted successfully`);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
spin.stop(true);
|
|
119
|
+
const error = e;
|
|
120
|
+
if (error.response?.status === 404) log(`Iteration #${id} not found`);
|
|
121
|
+
else log("Error deleting iteration:", error.message ?? String(e));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function listIterationStories(id, options) {
|
|
126
|
+
spin.start();
|
|
127
|
+
try {
|
|
128
|
+
const [stories, entities] = await Promise.all([client.listIterationStories(parseInt(id, 10)).then((r) => r.data), stories_default.fetchEntities()]);
|
|
129
|
+
spin.stop(true);
|
|
130
|
+
if (stories.length === 0) {
|
|
131
|
+
log(`No stories found in iteration #${id}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
log(chalk.bold(`Stories in iteration #${id}:`));
|
|
135
|
+
log();
|
|
136
|
+
stories.forEach((story) => {
|
|
137
|
+
const hydrated = stories_default.hydrateStory(entities, story);
|
|
138
|
+
if (options.format) stories_default.printFormattedStory({ format: options.format })(hydrated);
|
|
139
|
+
else {
|
|
140
|
+
const state = hydrated.state?.name ?? "Unknown";
|
|
141
|
+
const owners = hydrated.owners?.map((o) => o?.profile.mention_name).filter(Boolean).join(", ");
|
|
142
|
+
log(`${chalk.bold("#" + story.id)} ${chalk.blue(story.name)}`);
|
|
143
|
+
log(` Type: ${story.story_type} | State: ${state} | Owners: ${owners || "_"}`);
|
|
144
|
+
log(` Points: ${story.estimate ?? "_"}`);
|
|
145
|
+
log();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
} catch (e) {
|
|
149
|
+
spin.stop(true);
|
|
150
|
+
const error = e;
|
|
151
|
+
if (error.response?.status === 404) log(`Iteration #${id} not found`);
|
|
152
|
+
else log("Error fetching stories:", error.message ?? String(e));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function printIteration(iteration) {
|
|
157
|
+
const stats = iteration.stats;
|
|
158
|
+
const totalStories = stats.num_stories_done + stats.num_stories_started + stats.num_stories_unstarted + stats.num_stories_backlog;
|
|
159
|
+
const completionPct = stats.num_points > 0 ? Math.round(stats.num_points_done / stats.num_points * 100) : 0;
|
|
160
|
+
log(chalk.blue.bold(`#${iteration.id}`) + chalk.blue(` ${iteration.name}`));
|
|
161
|
+
if (iteration.description) log(chalk.bold("Description:") + ` ${iteration.description}`);
|
|
162
|
+
log(chalk.bold("Status:") + ` ${formatStatus(iteration.status)}`);
|
|
163
|
+
log(chalk.bold("Start Date:") + ` ${iteration.start_date}`);
|
|
164
|
+
log(chalk.bold("End Date:") + ` ${iteration.end_date}`);
|
|
165
|
+
if (iteration.group_ids && iteration.group_ids.length > 0) log(chalk.bold("Teams:") + ` ${iteration.group_ids.join(", ")}`);
|
|
166
|
+
log(chalk.bold("Stories:") + ` ${totalStories} (${stats.num_stories_done} done)`);
|
|
167
|
+
log(chalk.bold("Points:") + ` ${stats.num_points} (${stats.num_points_done} done)`);
|
|
168
|
+
log(chalk.bold("Completion:") + ` ${completionPct}%`);
|
|
169
|
+
log(chalk.bold("URL:") + ` ${iteration.app_url}`);
|
|
170
|
+
log();
|
|
171
|
+
}
|
|
172
|
+
function formatStatus(status) {
|
|
173
|
+
switch (status) {
|
|
174
|
+
case "started": return chalk.green(status);
|
|
175
|
+
case "done": return chalk.gray(status);
|
|
176
|
+
default: return chalk.yellow(status);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
export {};
|