@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-story.js
CHANGED
|
@@ -1,71 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
fs = require_rolldown_runtime.__toESM(fs);
|
|
15
|
-
let os = require("os");
|
|
16
|
-
os = require_rolldown_runtime.__toESM(os);
|
|
17
|
-
let child_process = require("child_process");
|
|
18
|
-
let chalk = require("chalk");
|
|
19
|
-
chalk = require_rolldown_runtime.__toESM(chalk);
|
|
20
|
-
let https = require("https");
|
|
21
|
-
https = require_rolldown_runtime.__toESM(https);
|
|
22
|
-
|
|
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 debugging from "debug";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import https from "https";
|
|
23
14
|
//#region src/bin/short-story.ts
|
|
24
|
-
const config =
|
|
25
|
-
const spin =
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const spin = spinner();
|
|
26
17
|
const log = console.log;
|
|
27
18
|
const logError = console.error;
|
|
28
|
-
const debug
|
|
29
|
-
|
|
19
|
+
const debug = debugging("short");
|
|
20
|
+
let handledSubcommand = false;
|
|
21
|
+
if (process.argv[2] === "history") {
|
|
22
|
+
handledSubcommand = true;
|
|
23
|
+
showStoryHistory(process.argv[3]).catch((e) => {
|
|
24
|
+
logError("Error fetching story history", e);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (process.argv[2] === "comments") {
|
|
29
|
+
handledSubcommand = true;
|
|
30
|
+
showStoryComments(process.argv[3]).catch((e) => {
|
|
31
|
+
logError("Error fetching story comments", e);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (process.argv[2] === "tasks") {
|
|
36
|
+
handledSubcommand = true;
|
|
37
|
+
showStoryTasks(process.argv[3]).catch((e) => {
|
|
38
|
+
logError("Error fetching story tasks", e);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (process.argv[2] === "sub-tasks") {
|
|
43
|
+
handledSubcommand = true;
|
|
44
|
+
showStorySubTasks(process.argv[3]).catch((e) => {
|
|
45
|
+
logError("Error fetching story sub-tasks", e);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (process.argv[2] === "relation" && process.argv[3] === "add") {
|
|
50
|
+
handledSubcommand = true;
|
|
51
|
+
addStoryRelation(process.argv[4], process.argv[5], process.argv[6], process.argv[7]).catch((e) => {
|
|
52
|
+
logError("Error creating story relation", e);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const program = new Command().allowExcessArguments(true).argument("[id]").usage("[options] <id>").description("Update and/or display story details").option("-a, --archived", "Update story as archived").option("-c, --comment [text]", "Add comment to story", "").option("-d, --description [text]", "Update description of story", "").option("--deadline [date]", "Update due date of story (YYYY-MM-DD)", "").option("-D, --download", "Download all attached files", "").option("--download-dir [path]", "Directory to download files to", ".").option("-e, --estimate [number]", "Update estimate of story", "").option("--epic [id|name]", "Set epic of story").option("-i, --iteration [id|name]", "Set iteration of story").option("-f, --format [template]", "Format the story output by template", "").option("--from-git", "Fetch story parsed by ID from current git branch").option("--follower [id|name]", "Update followers of story, comma-separated", "").option("--git-branch", "Checkout git branch from story slug <mention-name>/ch<id>/<type>-<title>\n as required by the Git integration: https://bit.ly/2RKO1FF").option("--git-branch-short", "Checkout git branch from story slug <mention-name>/ch<id>/<title>").option("-I, --idonly", "Print only ID of story results", "").option("-l, --label [id|name]", "Stories with label id/name, by regex", "").option("--move-after [id]", "Move story to position below story ID").option("--move-before [id]", "Move story to position above story ID").option("--move-down [n]", "Move story position downward by n stories").option("--move-up [n]", "Move story position upward by n stories").option("-o, --owners [id|name]", "Update owners of story, comma-separated", "").option("-O, --open", "Open story in browser").option("--oe, --open-epic", "Open story's epic in browser").option("--oi, --open-iteration", "Open story's iteration in browser").option("--op, --open-project", "Open story's project in browser").option("--external-link [url]", "Add external link to story, comma-separated", "").option("-q, --quiet", "Print only story output, no loading dialog", "").option("--requester [id|name]", "Update requester of story", "").option("-s, --state [id|name]", "Update workflow state of story", "").option("-t, --title [text]", "Update title/name of story", "").option("-T, --team [id|name]", "Update team/group of story", "").option("--task [text]", "Create new task on story").option("--task-complete [text]", "Toggle completion of task on story matching text").option("-y, --type [name]", "Update type of story", "").parse(process.argv);
|
|
57
|
+
const opts = program.opts();
|
|
30
58
|
const main = async () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
if (handledSubcommand) return;
|
|
60
|
+
const entities = await stories_default.fetchEntities();
|
|
61
|
+
if (!(opts.idonly || opts.quiet)) spin.start();
|
|
62
|
+
debug("constructing story update");
|
|
34
63
|
const update = {};
|
|
35
|
-
if (
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
if (opts.archived) update.archived = true;
|
|
65
|
+
if (opts.state) update.workflow_state_id = stories_default.findState(entities, opts.state)?.id;
|
|
66
|
+
if (opts.estimate) update.estimate = parseInt(opts.estimate, 10);
|
|
67
|
+
if (opts.title) update.name = opts.title;
|
|
68
|
+
if (opts.description) update.description = `${opts.description}`;
|
|
69
|
+
if (opts.deadline) update.deadline = normalizeDate(opts.deadline);
|
|
70
|
+
if (opts.type) {
|
|
71
|
+
const storyTypes = [
|
|
43
72
|
"feature",
|
|
44
73
|
"bug",
|
|
45
74
|
"chore"
|
|
46
|
-
]
|
|
75
|
+
];
|
|
76
|
+
const typeMatch = new RegExp(opts.type, "i");
|
|
77
|
+
update.story_type = storyTypes.find((t) => t.match(typeMatch));
|
|
47
78
|
}
|
|
48
|
-
if (
|
|
49
|
-
if (
|
|
50
|
-
if (
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
if (opts.owners) update.owner_ids = stories_default.findOwnerIds(entities, opts.owners);
|
|
80
|
+
if (opts.follower) update.follower_ids = stories_default.findOwnerIds(entities, opts.follower);
|
|
81
|
+
if (opts.epic) update.epic_id = stories_default.findEpic(entities, opts.epic)?.id;
|
|
82
|
+
if (opts.iteration) update.iteration_id = stories_default.findIteration(entities, opts.iteration)?.id;
|
|
83
|
+
if (opts.label) update.labels = stories_default.findLabelNames(entities, opts.label);
|
|
84
|
+
if (opts.team) update.group_id = stories_default.findGroup(entities, opts.team)?.id;
|
|
85
|
+
if (opts.requester) update.requested_by_id = stories_default.findMember(entities, opts.requester)?.id;
|
|
86
|
+
const hasPositionUpdate = opts.moveAfter !== void 0 || opts.moveBefore !== void 0 || opts.moveDown !== void 0 || opts.moveUp !== void 0;
|
|
87
|
+
const hasUpdate = Object.keys(update).length > 0 || hasPositionUpdate || !!opts.externalLink;
|
|
88
|
+
debug("constructed story update", update);
|
|
56
89
|
const gitID = [];
|
|
57
|
-
if (
|
|
58
|
-
debug
|
|
90
|
+
if (opts.fromGit || !program.args.length) {
|
|
91
|
+
debug("fetching story ID from git");
|
|
59
92
|
let branch = "";
|
|
60
93
|
try {
|
|
61
|
-
branch =
|
|
94
|
+
branch = execSync("git branch").toString("utf-8");
|
|
62
95
|
} catch (e) {
|
|
63
|
-
debug
|
|
96
|
+
debug(e);
|
|
64
97
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
98
|
+
const storyIdMatch = branch.match(/\*.*/)?.[0].match(/\/(ch|sc-)([0-9]+)/);
|
|
99
|
+
if (storyIdMatch?.[2]) {
|
|
100
|
+
debug("parsing story ID from git branch:", branch);
|
|
101
|
+
const id = parseInt(storyIdMatch[2], 10);
|
|
102
|
+
debug("parsed story ID from git branch:", id);
|
|
69
103
|
if (id) gitID.push(id.toString());
|
|
70
104
|
} else {
|
|
71
105
|
stopSpinner();
|
|
@@ -74,13 +108,14 @@ const main = async () => {
|
|
|
74
108
|
}
|
|
75
109
|
}
|
|
76
110
|
program.args.map((a) => (a.match(/\d+/) || [])[0]).concat(gitID).map(async (_id) => {
|
|
111
|
+
if (!_id) return;
|
|
77
112
|
const id = parseInt(_id, 10);
|
|
78
113
|
let story;
|
|
79
114
|
try {
|
|
80
|
-
if (
|
|
81
|
-
debug
|
|
82
|
-
await
|
|
83
|
-
debug
|
|
115
|
+
if (opts.comment) {
|
|
116
|
+
debug("request comment create");
|
|
117
|
+
await client.createStoryComment(id, { text: opts.comment });
|
|
118
|
+
debug("response comment create");
|
|
84
119
|
}
|
|
85
120
|
} catch (e) {
|
|
86
121
|
stopSpinner();
|
|
@@ -88,10 +123,10 @@ const main = async () => {
|
|
|
88
123
|
process.exit(3);
|
|
89
124
|
}
|
|
90
125
|
try {
|
|
91
|
-
if (
|
|
92
|
-
debug
|
|
93
|
-
await
|
|
94
|
-
debug
|
|
126
|
+
if (opts.task) {
|
|
127
|
+
debug("request task create");
|
|
128
|
+
await client.createTask(id, { description: opts.task });
|
|
129
|
+
debug("response task create");
|
|
95
130
|
}
|
|
96
131
|
} catch (e) {
|
|
97
132
|
stopSpinner();
|
|
@@ -99,23 +134,27 @@ const main = async () => {
|
|
|
99
134
|
process.exit(3);
|
|
100
135
|
}
|
|
101
136
|
try {
|
|
102
|
-
debug
|
|
103
|
-
story = await
|
|
104
|
-
debug
|
|
137
|
+
debug("request story");
|
|
138
|
+
story = await client.getStory(id).then((r) => r.data);
|
|
139
|
+
debug("response story");
|
|
140
|
+
if (opts.externalLink) {
|
|
141
|
+
const links = opts.externalLink.split(",").map((link) => link.trim()).filter(Boolean);
|
|
142
|
+
update.external_links = Array.from(new Set([...story.external_links || [], ...links]));
|
|
143
|
+
}
|
|
105
144
|
} catch (e) {
|
|
106
145
|
stopSpinner();
|
|
107
146
|
logError("Error fetching story", id);
|
|
108
147
|
process.exit(4);
|
|
109
148
|
}
|
|
110
149
|
try {
|
|
111
|
-
if (
|
|
112
|
-
debug
|
|
113
|
-
const descMatch = new RegExp(
|
|
150
|
+
if (opts.taskComplete) {
|
|
151
|
+
debug("calculating task(s) to complete");
|
|
152
|
+
const descMatch = new RegExp(opts.taskComplete, "i");
|
|
114
153
|
const tasks = story.tasks.filter((t) => t.description.match(descMatch));
|
|
115
154
|
const updatedTaskIds = tasks.map((t) => t.id);
|
|
116
|
-
debug
|
|
117
|
-
await Promise.all(tasks.map((t) =>
|
|
118
|
-
debug
|
|
155
|
+
debug("request tasks complete", updatedTaskIds);
|
|
156
|
+
await Promise.all(tasks.map((t) => client.updateTask(id, t.id, { complete: !t.complete })));
|
|
157
|
+
debug("response tasks complete");
|
|
119
158
|
story.tasks = story.tasks.map((t) => {
|
|
120
159
|
if (updatedTaskIds.indexOf(t.id) > -1) t.complete = !t.complete;
|
|
121
160
|
return t;
|
|
@@ -129,22 +168,22 @@ const main = async () => {
|
|
|
129
168
|
try {
|
|
130
169
|
if (hasUpdate) {
|
|
131
170
|
if (hasPositionUpdate) {
|
|
132
|
-
debug
|
|
133
|
-
const siblings = await
|
|
171
|
+
debug("calculating move up/down");
|
|
172
|
+
const siblings = await stories_default.listStories({
|
|
134
173
|
state: story.workflow_state_id.toString(),
|
|
135
174
|
sort: "state.position:asc,position:asc"
|
|
136
175
|
});
|
|
137
176
|
const siblingIds = siblings.map((s) => s.id);
|
|
138
177
|
const storyIndex = siblingIds.indexOf(~~id);
|
|
139
|
-
if (
|
|
140
|
-
else if (
|
|
141
|
-
else if (
|
|
142
|
-
else if (
|
|
143
|
-
debug
|
|
178
|
+
if (opts.moveAfter) update.after_id = ~~opts.moveAfter;
|
|
179
|
+
else if (opts.moveBefore) update.before_id = ~~opts.moveBefore;
|
|
180
|
+
else if (opts.moveUp) update.before_id = siblingIds[Math.max(0, storyIndex - (~~opts.moveUp || 1))];
|
|
181
|
+
else if (opts.moveDown) update.after_id = siblingIds[Math.min(siblings.length - 1, storyIndex + (~~opts.moveDown || 1))];
|
|
182
|
+
debug("constructed story position update", update);
|
|
144
183
|
}
|
|
145
|
-
debug
|
|
146
|
-
const changed = await
|
|
147
|
-
debug
|
|
184
|
+
debug("request story update");
|
|
185
|
+
const changed = await client.updateStory(id, update).then((r) => r.data);
|
|
186
|
+
debug("response story update");
|
|
148
187
|
story = Object.assign({}, story, changed);
|
|
149
188
|
}
|
|
150
189
|
} catch (e) {
|
|
@@ -152,61 +191,268 @@ const main = async () => {
|
|
|
152
191
|
logError("Error updating story", id);
|
|
153
192
|
process.exit(5);
|
|
154
193
|
}
|
|
155
|
-
if (story) story =
|
|
156
|
-
if (!
|
|
194
|
+
if (story) story = stories_default.hydrateStory(entities, story);
|
|
195
|
+
if (!opts.idonly) spin.stop(true);
|
|
157
196
|
if (story) {
|
|
158
197
|
printStory(story, entities);
|
|
159
|
-
if (
|
|
160
|
-
if (
|
|
198
|
+
if (opts.open) openURL(stories_default.storyURL(story));
|
|
199
|
+
if (opts.openEpic) {
|
|
161
200
|
if (!story.epic_id) {
|
|
162
201
|
logError("This story is not part of an epic.");
|
|
163
202
|
process.exit(21);
|
|
164
203
|
}
|
|
165
|
-
openURL(
|
|
204
|
+
openURL(stories_default.buildURL("epic", story.epic_id));
|
|
166
205
|
}
|
|
167
|
-
if (
|
|
206
|
+
if (opts.openIteration) {
|
|
168
207
|
if (!story.iteration_id) {
|
|
169
208
|
logError("This story is not part of an iteration.");
|
|
170
209
|
process.exit(22);
|
|
171
210
|
}
|
|
172
|
-
openURL(
|
|
211
|
+
openURL(stories_default.buildURL("iteration", story.iteration_id));
|
|
212
|
+
}
|
|
213
|
+
if (opts.openProject) {
|
|
214
|
+
if (story.project_id !== void 0 && story.project_id !== null) openURL(stories_default.buildURL("project", story.project_id));
|
|
173
215
|
}
|
|
174
|
-
if (program.openProject) openURL(require_lib_stories.default.buildURL("project", story.project_id));
|
|
175
216
|
}
|
|
176
|
-
if (
|
|
177
|
-
if (story &&
|
|
217
|
+
if (opts.download) downloadFiles(story);
|
|
218
|
+
if (story && opts.gitBranch) {
|
|
178
219
|
if (!config.mentionName) {
|
|
179
220
|
stopSpinner();
|
|
180
|
-
|
|
221
|
+
stories_default.checkoutStoryBranch(story, `${story.story_type}-${story.id}-`);
|
|
181
222
|
logError("Error creating story branch in Shortcut format");
|
|
182
223
|
logError("Please run: \"short install --force\" to add your mention name to the config.");
|
|
183
224
|
process.exit(10);
|
|
184
225
|
}
|
|
185
|
-
|
|
186
|
-
} else if (story &&
|
|
226
|
+
stories_default.checkoutStoryBranch(story);
|
|
227
|
+
} else if (story && opts.gitBranchShort) stories_default.checkoutStoryBranch(story, `${config.mentionName}/sc-${story.id}/`);
|
|
187
228
|
});
|
|
188
229
|
stopSpinner();
|
|
189
230
|
};
|
|
231
|
+
async function showStoryHistory(idArg) {
|
|
232
|
+
const id = parseInt(idArg || "", 10);
|
|
233
|
+
if (!id) {
|
|
234
|
+
logError("Usage: short story history <id>");
|
|
235
|
+
process.exit(2);
|
|
236
|
+
}
|
|
237
|
+
spin.start();
|
|
238
|
+
try {
|
|
239
|
+
const history = await client.storyHistory(id).then((r) => r.data);
|
|
240
|
+
spin.stop(true);
|
|
241
|
+
if (history.length === 0) {
|
|
242
|
+
log(`No history found for story #${id}`);
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
history.forEach(printHistoryItem);
|
|
246
|
+
process.exit(0);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
spin.stop(true);
|
|
249
|
+
logError(`Error fetching story history ${id}`);
|
|
250
|
+
process.exit(4);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function showStoryComments(idArg) {
|
|
254
|
+
const id = parseInt(idArg || "", 10);
|
|
255
|
+
if (!id) {
|
|
256
|
+
logError("Usage: short story comments <id>");
|
|
257
|
+
process.exit(2);
|
|
258
|
+
}
|
|
259
|
+
spin.start();
|
|
260
|
+
try {
|
|
261
|
+
const entities = await stories_default.fetchEntities();
|
|
262
|
+
const story = await client.getStory(id).then((r) => r.data);
|
|
263
|
+
spin.stop(true);
|
|
264
|
+
if (!story.comments.length) {
|
|
265
|
+
log(`No comments found for story #${id}`);
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
printStoryComments(story.comments, entities);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
spin.stop(true);
|
|
272
|
+
logError(`Error fetching story comments ${id}`);
|
|
273
|
+
process.exit(4);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function showStoryTasks(idArg) {
|
|
277
|
+
const id = parseInt(idArg || "", 10);
|
|
278
|
+
if (!id) {
|
|
279
|
+
logError("Usage: short story tasks <id>");
|
|
280
|
+
process.exit(2);
|
|
281
|
+
}
|
|
282
|
+
spin.start();
|
|
283
|
+
try {
|
|
284
|
+
const entities = await stories_default.fetchEntities();
|
|
285
|
+
const story = await client.getStory(id).then((r) => r.data);
|
|
286
|
+
spin.stop(true);
|
|
287
|
+
if (!story.tasks.length) {
|
|
288
|
+
log(`No tasks found for story #${id}`);
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
printStoryTasks(story.tasks, entities);
|
|
292
|
+
process.exit(0);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
spin.stop(true);
|
|
295
|
+
logError(`Error fetching story tasks ${id}`);
|
|
296
|
+
process.exit(4);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function showStorySubTasks(idArg) {
|
|
300
|
+
const id = parseInt(idArg || "", 10);
|
|
301
|
+
if (!id) {
|
|
302
|
+
logError("Usage: short story sub-tasks <id>");
|
|
303
|
+
process.exit(2);
|
|
304
|
+
}
|
|
305
|
+
spin.start();
|
|
306
|
+
try {
|
|
307
|
+
const entities = await stories_default.fetchEntities();
|
|
308
|
+
const stories = await client.listStorySubTasks(id).then((r) => r.data);
|
|
309
|
+
spin.stop(true);
|
|
310
|
+
if (!stories.length) {
|
|
311
|
+
log(`No sub-tasks found for story #${id}`);
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
stories.map((story) => stories_default.hydrateStory(entities, story)).forEach(stories_default.printFormattedStory({}));
|
|
315
|
+
process.exit(0);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
spin.stop(true);
|
|
318
|
+
logError(`Error fetching story sub-tasks ${id}`);
|
|
319
|
+
process.exit(4);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function addStoryRelation(storyIdArg, relatedIdArg, typeFlag, typeValue) {
|
|
323
|
+
const storyId = parseInt(storyIdArg || "", 10);
|
|
324
|
+
const relatedId = parseInt(relatedIdArg || "", 10);
|
|
325
|
+
const relationType = typeFlag === "--type" ? typeValue : void 0;
|
|
326
|
+
if (!storyId || !relatedId || !relationType) {
|
|
327
|
+
logError("Usage: short story relation add <storyId> <relatedId> --type <type>");
|
|
328
|
+
process.exit(2);
|
|
329
|
+
}
|
|
330
|
+
const normalized = normalizeRelationType(storyId, relatedId, relationType);
|
|
331
|
+
if (!normalized) {
|
|
332
|
+
logError("Invalid relation type. Use one of: blocks, blocked-by, duplicates, duplicated-by, relates-to");
|
|
333
|
+
process.exit(2);
|
|
334
|
+
}
|
|
335
|
+
spin.start();
|
|
336
|
+
try {
|
|
337
|
+
const link = await client.createStoryLink(normalized).then((r) => r.data);
|
|
338
|
+
spin.stop(true);
|
|
339
|
+
log(`Added relation: story #${link.subject_id} ${link.verb} story #${link.object_id} (#${link.id})`);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
spin.stop(true);
|
|
342
|
+
logError(`Error creating story relation ${storyId} -> ${relatedId}`);
|
|
343
|
+
process.exit(4);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
190
346
|
const openURL = (url) => {
|
|
191
|
-
|
|
347
|
+
execSync(`${os.platform() === "darwin" ? "open" : "xdg-open"} '${url}'`);
|
|
192
348
|
};
|
|
193
349
|
const stopSpinner = () => {
|
|
194
|
-
if (!(
|
|
350
|
+
if (!(opts.idonly || opts.quiet)) spin.stop(true);
|
|
195
351
|
};
|
|
196
352
|
const downloadFiles = (story) => story.files.map((file) => {
|
|
197
|
-
https.
|
|
198
|
-
const filePath = path.
|
|
199
|
-
log(chalk.
|
|
200
|
-
const stream = fs.
|
|
353
|
+
https.get(stories_default.fileURL(file), (res) => {
|
|
354
|
+
const filePath = path.join(opts.downloadDir ?? ".", file.name);
|
|
355
|
+
log(chalk.bold("Downloading file to: ") + filePath);
|
|
356
|
+
const stream = fs.createWriteStream(filePath);
|
|
201
357
|
res.pipe(stream);
|
|
202
358
|
stream.on("finish", () => stream.close());
|
|
203
359
|
});
|
|
204
360
|
});
|
|
205
361
|
const printStory = (story, entities) => {
|
|
206
|
-
if (
|
|
207
|
-
if (
|
|
208
|
-
|
|
362
|
+
if (opts.idonly) return log(story.id);
|
|
363
|
+
if (opts.format) return stories_default.printFormattedStory(opts)(story);
|
|
364
|
+
stories_default.printDetailedStory(story, entities);
|
|
365
|
+
};
|
|
366
|
+
const printHistoryItem = (item) => {
|
|
367
|
+
const actor = item.actor_name || item.member_id || "Unknown";
|
|
368
|
+
log(chalk.blue.bold(`${item.changed_at}`) + ` ${actor}`);
|
|
369
|
+
item.actions.forEach((action) => {
|
|
370
|
+
log(`- ${summarizeHistoryAction(action)}`);
|
|
371
|
+
});
|
|
372
|
+
log();
|
|
373
|
+
};
|
|
374
|
+
const printStoryComments = (comments, entities) => {
|
|
375
|
+
const repliesByParent = /* @__PURE__ */ new Map();
|
|
376
|
+
const roots = [];
|
|
377
|
+
comments.forEach((comment) => {
|
|
378
|
+
if (comment.parent_id) {
|
|
379
|
+
const replies = repliesByParent.get(comment.parent_id) || [];
|
|
380
|
+
replies.push(comment);
|
|
381
|
+
repliesByParent.set(comment.parent_id, replies);
|
|
382
|
+
} else roots.push(comment);
|
|
383
|
+
});
|
|
384
|
+
roots.sort((a, b) => a.position - b.position).forEach((comment) => printStoryComment(comment, entities, repliesByParent, 0));
|
|
385
|
+
};
|
|
386
|
+
const printStoryComment = (comment, entities, repliesByParent, depth) => {
|
|
387
|
+
const indent = " ".repeat(depth);
|
|
388
|
+
const author = comment.author_id ? entities.membersById?.get(comment.author_id)?.profile : void 0;
|
|
389
|
+
const authorText = author ? `${author.name} (${author.mention_name})` : comment.author_id || "Unknown";
|
|
390
|
+
log(`${indent}${chalk.bold("#" + comment.id)} ${authorText}`);
|
|
391
|
+
log(`${indent}Created: ${comment.created_at}`);
|
|
392
|
+
if (comment.updated_at && comment.updated_at !== comment.created_at) log(`${indent}Updated: ${comment.updated_at}`);
|
|
393
|
+
if (comment.blocker) log(`${indent}Blocker: true`);
|
|
394
|
+
log(`${indent}${comment.deleted ? "[deleted]" : comment.text || "_"}`);
|
|
395
|
+
log(`${indent}URL: ${comment.app_url}`);
|
|
396
|
+
log();
|
|
397
|
+
(repliesByParent.get(comment.id) || []).sort((a, b) => a.position - b.position).forEach((reply) => printStoryComment(reply, entities, repliesByParent, depth + 1));
|
|
398
|
+
};
|
|
399
|
+
const printStoryTasks = (tasks, entities) => {
|
|
400
|
+
tasks.slice().sort((a, b) => a.position - b.position).forEach((task) => printStoryTask(task, entities));
|
|
401
|
+
};
|
|
402
|
+
const printStoryTask = (task, entities) => {
|
|
403
|
+
const status = task.complete ? "[x]" : "[ ]";
|
|
404
|
+
const owners = task.owner_ids.map((ownerId) => entities.membersById?.get(ownerId)?.profile?.mention_name || ownerId).join(", ");
|
|
405
|
+
log(`${chalk.bold("#" + task.id)} ${status} ${task.description}`);
|
|
406
|
+
if (owners) log(`Owners: ${owners}`);
|
|
407
|
+
if (task.completed_at) log(`Completed: ${task.completed_at}`);
|
|
408
|
+
if (task.updated_at) log(`Updated: ${task.updated_at}`);
|
|
409
|
+
log();
|
|
410
|
+
};
|
|
411
|
+
function normalizeRelationType(storyId, relatedId, type) {
|
|
412
|
+
const normalized = type.toLowerCase().replace(/[_\s]+/g, "-");
|
|
413
|
+
if (normalized === "blocks") return {
|
|
414
|
+
subject_id: storyId,
|
|
415
|
+
object_id: relatedId,
|
|
416
|
+
verb: "blocks"
|
|
417
|
+
};
|
|
418
|
+
if (normalized === "blocked-by") return {
|
|
419
|
+
subject_id: relatedId,
|
|
420
|
+
object_id: storyId,
|
|
421
|
+
verb: "blocks"
|
|
422
|
+
};
|
|
423
|
+
if (normalized === "duplicates") return {
|
|
424
|
+
subject_id: storyId,
|
|
425
|
+
object_id: relatedId,
|
|
426
|
+
verb: "duplicates"
|
|
427
|
+
};
|
|
428
|
+
if (normalized === "duplicated-by") return {
|
|
429
|
+
subject_id: relatedId,
|
|
430
|
+
object_id: storyId,
|
|
431
|
+
verb: "duplicates"
|
|
432
|
+
};
|
|
433
|
+
if (normalized === "relates-to" || normalized === "relates") return {
|
|
434
|
+
subject_id: storyId,
|
|
435
|
+
object_id: relatedId,
|
|
436
|
+
verb: "relates to"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const summarizeHistoryAction = (action) => {
|
|
440
|
+
const entityType = String(action.entity_type || "item");
|
|
441
|
+
const actionType = String(action.action || "changed");
|
|
442
|
+
const name = typeof action.name === "string" ? action.name : void 0;
|
|
443
|
+
const description = typeof action.description === "string" ? action.description : void 0;
|
|
444
|
+
if (actionType === "update" && action.changes && typeof action.changes === "object") {
|
|
445
|
+
const fields = Object.keys(action.changes);
|
|
446
|
+
if (fields.length > 0) return `updated ${entityType} ${name ? `"${name}" ` : ""}(fields: ${fields.join(", ")})`;
|
|
447
|
+
}
|
|
448
|
+
if (name) return `${actionType} ${entityType} "${name}"`;
|
|
449
|
+
if (description) return `${actionType} ${entityType} "${description}"`;
|
|
450
|
+
return `${actionType} ${entityType}`;
|
|
451
|
+
};
|
|
452
|
+
const normalizeDate = (value) => {
|
|
453
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return (/* @__PURE__ */ new Date(`${value}T00:00:00.000Z`)).toISOString();
|
|
454
|
+
return new Date(value).toISOString();
|
|
209
455
|
};
|
|
210
456
|
main();
|
|
211
|
-
|
|
212
|
-
|
|
457
|
+
//#endregion
|
|
458
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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";
|
|
7
|
+
//#region src/bin/short-team.ts
|
|
8
|
+
const spin = spinner();
|
|
9
|
+
const log = console.log;
|
|
10
|
+
const program = new Command().usage("[command] [options]").description("view a team or list its stories");
|
|
11
|
+
program.command("view <idOrName>").description("view a team by id or name").action(viewTeam);
|
|
12
|
+
program.command("stories <idOrName>").description("list stories for a team by id or name").option("-d, --detailed", "Show more details for each story").option("-f, --format [template]", "Format each story output by template", "").action(listTeamStories);
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
if (args.length > 0 && args[0] !== "view" && args[0] !== "stories") process.argv.splice(2, 0, "view");
|
|
15
|
+
program.parse(process.argv);
|
|
16
|
+
if (args.length === 0) {
|
|
17
|
+
program.outputHelp();
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
async function viewTeam(idOrName) {
|
|
21
|
+
spin.start();
|
|
22
|
+
try {
|
|
23
|
+
const entities = await stories_default.fetchEntities();
|
|
24
|
+
const team = stories_default.findGroup(entities, idOrName);
|
|
25
|
+
if (!team) {
|
|
26
|
+
spin.stop(true);
|
|
27
|
+
log(`Team ${idOrName} not found`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const fullTeam = await client.getGroup(team.id).then((r) => r.data);
|
|
31
|
+
spin.stop(true);
|
|
32
|
+
printTeam(fullTeam);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
spin.stop(true);
|
|
35
|
+
log(`Error fetching team: ${e.message ?? String(e)}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function listTeamStories(idOrName, options) {
|
|
40
|
+
spin.start();
|
|
41
|
+
try {
|
|
42
|
+
const entities = await stories_default.fetchEntities();
|
|
43
|
+
const team = stories_default.findGroup(entities, idOrName);
|
|
44
|
+
if (!team) {
|
|
45
|
+
spin.stop(true);
|
|
46
|
+
log(`Team ${idOrName} not found`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const stories = await client.listGroupStories(team.id).then((r) => r.data);
|
|
50
|
+
spin.stop(true);
|
|
51
|
+
if (stories.length === 0) {
|
|
52
|
+
log(`No stories found for team #${team.id} ${team.name}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
stories.map((story) => stories_default.hydrateStory(entities, story)).forEach(options.detailed ? (story) => stories_default.printDetailedStory(story, entities) : stories_default.printFormattedStory({ format: options.format }));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
spin.stop(true);
|
|
58
|
+
log(`Error fetching team stories: ${e.message ?? String(e)}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function printTeam(team) {
|
|
63
|
+
log(chalk.bold(`#${team.id}`) + chalk.blue(` ${team.name}`));
|
|
64
|
+
log(chalk.bold("Mention: ") + ` ${team.mention_name}`);
|
|
65
|
+
log(chalk.bold("Stories: ") + ` ${team.num_stories}`);
|
|
66
|
+
log(chalk.bold("Started: ") + ` ${team.num_stories_started}`);
|
|
67
|
+
log(chalk.bold("Backlog: ") + ` ${team.num_stories_backlog}`);
|
|
68
|
+
log(chalk.bold("Epics Started: ") + ` ${team.num_epics_started}`);
|
|
69
|
+
log(chalk.bold("Members: ") + ` ${team.member_ids.length}`);
|
|
70
|
+
log(chalk.bold("Workflows: ") + ` ${team.workflow_ids.length}`);
|
|
71
|
+
log(chalk.bold("Archived: ") + ` ${team.archived}`);
|
|
72
|
+
if (team.description) log(chalk.bold("Description: ") + ` ${team.description}`);
|
|
73
|
+
if (team.color) log(chalk.bold("Color: ") + ` ${team.color}`);
|
|
74
|
+
log(chalk.bold("URL: ") + ` ${team.app_url}`);
|
|
75
|
+
log();
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export {};
|