@shortcut-cli/shortcut-cli 4.0.0 → 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 +428 -8
- package/build/bin/{short-api.cjs → short-api.js} +13 -16
- package/build/bin/short-create.js +76 -0
- package/build/bin/short-custom-field.js +50 -0
- package/build/bin/short-custom-fields.js +29 -0
- package/build/bin/{short-doc.cjs → short-doc.js} +34 -36
- package/build/bin/{short-docs.cjs → short-docs.js} +23 -15
- package/build/bin/short-epic.js +186 -0
- package/build/bin/short-epics.js +36 -0
- package/build/bin/short-find.js +6 -0
- package/build/bin/short-install.js +87 -0
- package/build/bin/{short-iteration.cjs → short-iteration.js} +41 -45
- package/build/bin/{short-iterations.cjs → short-iterations.js} +15 -19
- package/build/bin/short-label.js +130 -0
- package/build/bin/short-labels.js +27 -0
- package/build/bin/short-members.js +31 -0
- package/build/bin/short-objective.js +151 -0
- package/build/bin/short-objectives.js +63 -0
- package/build/bin/short-projects.js +31 -0
- package/build/bin/short-search.js +45 -0
- package/build/bin/short-story.js +458 -0
- package/build/bin/short-team.js +78 -0
- package/build/bin/short-teams.js +28 -0
- package/build/bin/short-workflows.js +29 -0
- package/build/bin/short-workspace.js +63 -0
- package/build/bin/short.js +8 -0
- package/build/lib/client.js +9 -0
- package/build/lib/{configure.cjs → configure.js} +18 -27
- package/build/lib/spinner.js +12 -0
- package/build/lib/{stories.cjs → stories.js} +116 -78
- package/build/package.js +5 -0
- package/package.json +44 -44
- package/build/_virtual/rolldown_runtime.cjs +0 -29
- package/build/bin/short-create.cjs +0 -58
- package/build/bin/short-epic.cjs +0 -74
- package/build/bin/short-epics.cjs +0 -36
- package/build/bin/short-find.cjs +0 -7
- package/build/bin/short-install.cjs +0 -42
- package/build/bin/short-members.cjs +0 -34
- package/build/bin/short-projects.cjs +0 -34
- package/build/bin/short-search.cjs +0 -49
- package/build/bin/short-story.cjs +0 -213
- package/build/bin/short-workflows.cjs +0 -32
- package/build/bin/short-workspace.cjs +0 -64
- package/build/bin/short.cjs +0 -10
- package/build/lib/client.cjs +0 -11
- package/build/lib/spinner.cjs +0 -17
- package/build/package.cjs +0 -18
|
@@ -0,0 +1,76 @@
|
|
|
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, Option } from "commander";
|
|
7
|
+
import { exec } from "child_process";
|
|
8
|
+
//#region src/bin/short-create.ts
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
const spin = spinner();
|
|
11
|
+
const log = console.log;
|
|
12
|
+
const opts = new Command().usage("[options]").description("create a story with provided details").option("-d, --description [text]", "Set description of story", "").option("-e, --estimate [number]", "Set estimate of story").option("--epic [id|name]", "Set epic of story").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, --iteration [id|name]", "Set iteration of story").option("-I, --idonly", "Print only ID of story result").option("-l, --label [id|name]", "Stories with label id/name, by regex", "").option("-o, --owners [id|name]", "Set owners of story, comma-separated", "").option("-O, --open", "Open story in browser").option("-p, --project [id|name]", "Set project of story, required if --state is not set", "").option("-T, --team [id|name]", "Set team of story", "").option("-t, --title [text]", "Set title of story, required", "").option("-s, --state [id|name]", "Set workflow state of story, required if --project is not set", "").addOption(new Option("-y, --type <name>", "Set type of story").choices([
|
|
13
|
+
"feature",
|
|
14
|
+
"bug",
|
|
15
|
+
"chore"
|
|
16
|
+
]).default("feature")).parse(process.argv).opts();
|
|
17
|
+
const main = async () => {
|
|
18
|
+
const entities = await stories_default.fetchEntities();
|
|
19
|
+
if (!opts.idonly) spin.start();
|
|
20
|
+
const project = opts.project ? stories_default.findProject(entities, opts.project) : void 0;
|
|
21
|
+
const group = opts.team ? stories_default.findGroup(entities, opts.team) : void 0;
|
|
22
|
+
const state = opts.state ? stories_default.findState(entities, opts.state) : void 0;
|
|
23
|
+
const epic = opts.epic ? stories_default.findEpic(entities, opts.epic) : void 0;
|
|
24
|
+
const iteration = opts.iteration ? stories_default.findIteration(entities, opts.iteration) : void 0;
|
|
25
|
+
const update = {
|
|
26
|
+
name: opts.title ?? "",
|
|
27
|
+
story_type: opts.type,
|
|
28
|
+
description: `${opts.description}`
|
|
29
|
+
};
|
|
30
|
+
if (project) update.project_id = project.id;
|
|
31
|
+
if (group) update.group_id = group.id;
|
|
32
|
+
if (state) update.workflow_state_id = state.id;
|
|
33
|
+
if (epic) update.epic_id = epic.id;
|
|
34
|
+
if (iteration) update.iteration_id = iteration.id;
|
|
35
|
+
if (opts.estimate) update.estimate = parseInt(opts.estimate, 10);
|
|
36
|
+
if (opts.owners) update.owner_ids = stories_default.findOwnerIds(entities, opts.owners);
|
|
37
|
+
if (opts.label) update.labels = stories_default.findLabelNames(entities, opts.label);
|
|
38
|
+
let story;
|
|
39
|
+
if (!update.name) {
|
|
40
|
+
if (!opts.idonly) spin.stop(true);
|
|
41
|
+
log("Must provide --title");
|
|
42
|
+
} else if (opts.project && !project) {
|
|
43
|
+
if (!opts.idonly) spin.stop(true);
|
|
44
|
+
log(`Project ${opts.project} not found`);
|
|
45
|
+
} else if (opts.state && !state) {
|
|
46
|
+
if (!opts.idonly) spin.stop(true);
|
|
47
|
+
log(`State ${opts.state} not found`);
|
|
48
|
+
} else if (opts.team && !group) {
|
|
49
|
+
if (!opts.idonly) spin.stop(true);
|
|
50
|
+
log(`Team ${opts.team} not found`);
|
|
51
|
+
} else if (opts.epic && !epic) {
|
|
52
|
+
if (!opts.idonly) spin.stop(true);
|
|
53
|
+
log(`Epic ${opts.epic} not found`);
|
|
54
|
+
} else if (opts.iteration && !iteration) {
|
|
55
|
+
if (!opts.idonly) spin.stop(true);
|
|
56
|
+
log(`Iteration ${opts.iteration} not found`);
|
|
57
|
+
} else if (!update.project_id && !update.workflow_state_id) {
|
|
58
|
+
if (!opts.idonly) spin.stop(true);
|
|
59
|
+
log("Must provide --project or --state");
|
|
60
|
+
} else try {
|
|
61
|
+
story = await client.createStory(update).then((r) => r.data);
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
log("Error creating story");
|
|
64
|
+
}
|
|
65
|
+
if (!opts.idonly) spin.stop(true);
|
|
66
|
+
if (story) {
|
|
67
|
+
const hydrateStory = stories_default.hydrateStory(entities, story);
|
|
68
|
+
stories_default.printDetailedStory(hydrateStory);
|
|
69
|
+
if (opts.gitBranch) stories_default.checkoutStoryBranch(hydrateStory);
|
|
70
|
+
else if (opts.gitBranchShort) stories_default.checkoutStoryBranch(hydrateStory, `${config.mentionName}/sc-${story.id}/`);
|
|
71
|
+
if (opts.open) exec("open " + stories_default.storyURL(story));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
main();
|
|
75
|
+
//#endregion
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import client from "../lib/client.js";
|
|
3
|
+
import spinner from "../lib/spinner.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
//#region src/bin/short-custom-field.ts
|
|
7
|
+
const spin = spinner();
|
|
8
|
+
const log = console.log;
|
|
9
|
+
const program = new Command().argument("<id>").usage("<id> [options]").description("view a custom field by id").parse(process.argv);
|
|
10
|
+
const id = program.args[0];
|
|
11
|
+
if (!id) {
|
|
12
|
+
program.outputHelp();
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
async function main() {
|
|
16
|
+
if (!id) process.exit(1);
|
|
17
|
+
spin.start();
|
|
18
|
+
try {
|
|
19
|
+
const field = await client.getCustomField(id).then((r) => r.data);
|
|
20
|
+
spin.stop(true);
|
|
21
|
+
printField(field);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
spin.stop(true);
|
|
24
|
+
const error = e;
|
|
25
|
+
if (error.response?.status === 404) log(`Custom field ${id} not found`);
|
|
26
|
+
else log(`Error fetching custom field: ${error.message ?? String(e)}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function printField(field) {
|
|
31
|
+
log(chalk.bold(field.id) + chalk.blue(` ${field.name}`));
|
|
32
|
+
log(chalk.bold("Type: ") + ` ${field.field_type}`);
|
|
33
|
+
log(chalk.bold("Enabled: ") + ` ${field.enabled}`);
|
|
34
|
+
log(chalk.bold("Story Types: ") + ` ${(field.story_types || []).join(", ") || "_"}`);
|
|
35
|
+
log(chalk.bold("Position: ") + ` ${field.position}`);
|
|
36
|
+
log(chalk.bold("Fixed: ") + ` ${field.fixed_position || false}`);
|
|
37
|
+
if (field.canonical_name) log(chalk.bold("Canonical: ") + ` ${field.canonical_name}`);
|
|
38
|
+
if (field.description) log(chalk.bold("Description: ") + ` ${field.description}`);
|
|
39
|
+
if (field.values?.length) {
|
|
40
|
+
log(chalk.bold("Values:"));
|
|
41
|
+
field.values.forEach((value) => {
|
|
42
|
+
log(` - ${value.value} (${value.id}) enabled=${value.enabled} position=${value.position}${value.color_key ? ` color=${value.color_key}` : ""}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
log(chalk.bold("Created: ") + ` ${field.created_at}`);
|
|
46
|
+
log(chalk.bold("Updated: ") + ` ${field.updated_at}`);
|
|
47
|
+
}
|
|
48
|
+
main();
|
|
49
|
+
//#endregion
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import client from "../lib/client.js";
|
|
3
|
+
import spinner from "../lib/spinner.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
//#region src/bin/short-custom-fields.ts
|
|
7
|
+
const spin = spinner();
|
|
8
|
+
const log = console.log;
|
|
9
|
+
const opts = new Command().description("Display custom fields available for stories").option("-d, --disabled", "List custom fields including disabled", "").option("-s, --search [query]", "List custom fields with name containing query", "").parse(process.argv).opts();
|
|
10
|
+
async function main() {
|
|
11
|
+
spin.start();
|
|
12
|
+
const fields = await client.listCustomFields().then((r) => r.data);
|
|
13
|
+
spin.stop(true);
|
|
14
|
+
const searchMatch = new RegExp(opts.search ?? "", "i");
|
|
15
|
+
fields.filter((field) => !!`${field.id} ${field.name} ${field.description || ""}`.match(searchMatch)).forEach(printFieldSummary);
|
|
16
|
+
}
|
|
17
|
+
function printFieldSummary(field) {
|
|
18
|
+
if (!field.enabled && !opts.disabled) return;
|
|
19
|
+
log(chalk.bold(field.id) + chalk.blue(` ${field.name}`));
|
|
20
|
+
log(chalk.bold("Type: ") + ` ${field.field_type}`);
|
|
21
|
+
log(chalk.bold("Enabled: ") + ` ${field.enabled}`);
|
|
22
|
+
log(chalk.bold("Story Types: ") + ` ${(field.story_types || []).join(", ") || "_"}`);
|
|
23
|
+
log(chalk.bold("Values: ") + ` ${field.values?.length || 0}`);
|
|
24
|
+
if (field.description) log(chalk.bold("Description: ") + ` ${field.description}`);
|
|
25
|
+
log();
|
|
26
|
+
}
|
|
27
|
+
main();
|
|
28
|
+
//#endregion
|
|
29
|
+
export {};
|
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let child_process = require("child_process");
|
|
9
|
-
let chalk = require("chalk");
|
|
10
|
-
chalk = require_rolldown_runtime.__toESM(chalk);
|
|
11
|
-
|
|
2
|
+
import client from "../lib/client.js";
|
|
3
|
+
import spinner from "../lib/spinner.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { exec } from "child_process";
|
|
7
|
+
import chalk from "chalk";
|
|
12
8
|
//#region src/bin/short-doc.ts
|
|
13
|
-
const spin =
|
|
9
|
+
const spin = spinner();
|
|
14
10
|
const log = console.log;
|
|
15
|
-
const program = new
|
|
11
|
+
const program = new Command().usage("[command] [options]").description("view, create, or update a doc");
|
|
16
12
|
program.command("view <id>").description("view a doc by ID").option("--html", "Include HTML content in output").option("-O, --open", "Open doc in browser").option("-q, --quiet", "Print only doc content, no metadata").action(viewDoc);
|
|
17
13
|
program.command("create").description("create a new doc").option("-t, --title <text>", "Set title of doc (required)").option("-c, --content <text>", "Set content of doc (required)").option("--markdown", "Treat content as markdown (default is HTML)").option("-I, --idonly", "Print only ID of doc result").option("-O, --open", "Open doc in browser").action(createDoc);
|
|
18
14
|
program.command("update <id>").description("update an existing doc").option("-t, --title <text>", "Update title of doc").option("-c, --content <text>", "Update content of doc").option("--markdown", "Treat content as markdown (default is HTML)").option("-O, --open", "Open doc in browser").action(updateDoc);
|
|
19
15
|
program.command("delete <id>").description("delete a doc").option("--confirm", "Confirm deletion without prompting").action(deleteDoc);
|
|
20
16
|
const args = process.argv.slice(2);
|
|
21
|
-
if (args.length > 0 && isUUID(args[0])) process.argv.splice(2, 0, "view");
|
|
17
|
+
if (args.length > 0 && args[0] && isUUID(args[0])) process.argv.splice(2, 0, "view");
|
|
22
18
|
program.parse(process.argv);
|
|
23
19
|
if (args.length === 0) {
|
|
24
20
|
program.outputHelp();
|
|
25
21
|
process.exit(1);
|
|
26
|
-
} else if (args.length > 0 && !isUUID(args[0])) {
|
|
27
|
-
|
|
22
|
+
} else if (args.length > 0 && args[0] && !isUUID(args[0])) {
|
|
23
|
+
const validCommands = [
|
|
28
24
|
"view",
|
|
29
25
|
"create",
|
|
30
26
|
"update",
|
|
31
27
|
"delete"
|
|
32
|
-
]
|
|
33
|
-
|
|
28
|
+
];
|
|
29
|
+
const firstArg = args[0];
|
|
30
|
+
if (!validCommands.includes(firstArg) && !firstArg.startsWith("-")) {
|
|
31
|
+
console.error(`Error: Unknown command or invalid doc ID: ${firstArg}`);
|
|
34
32
|
console.error("Run \"short doc --help\" for usage information.");
|
|
35
33
|
process.exit(1);
|
|
36
34
|
}
|
|
@@ -44,7 +42,7 @@ async function viewDoc(id, options) {
|
|
|
44
42
|
try {
|
|
45
43
|
const params = {};
|
|
46
44
|
if (options.html) params.content_format = "html";
|
|
47
|
-
doc = await
|
|
45
|
+
doc = await client.getDoc(id, Object.keys(params).length > 0 ? params : void 0).then((r) => r.data);
|
|
48
46
|
} catch (e) {
|
|
49
47
|
if (!options.quiet) spin.stop(true);
|
|
50
48
|
log("Error fetching doc:", e.message ?? String(e));
|
|
@@ -72,8 +70,8 @@ async function createDoc(options) {
|
|
|
72
70
|
};
|
|
73
71
|
let doc;
|
|
74
72
|
try {
|
|
75
|
-
const result = await
|
|
76
|
-
doc = await
|
|
73
|
+
const result = await client.createDoc(docData);
|
|
74
|
+
doc = await client.getDoc(result.data.id).then((r) => r.data);
|
|
77
75
|
} catch (e) {
|
|
78
76
|
if (!options.idonly) spin.stop(true);
|
|
79
77
|
log("Error creating doc:", e.message ?? String(e));
|
|
@@ -82,7 +80,7 @@ async function createDoc(options) {
|
|
|
82
80
|
if (!options.idonly) spin.stop(true);
|
|
83
81
|
if (options.idonly) log(doc.id);
|
|
84
82
|
else {
|
|
85
|
-
log(chalk.
|
|
83
|
+
log(chalk.green("Doc created successfully!"));
|
|
86
84
|
printDoc(doc);
|
|
87
85
|
}
|
|
88
86
|
if (options.open) openURL(doc.app_url);
|
|
@@ -101,14 +99,14 @@ async function updateDoc(id, options) {
|
|
|
101
99
|
}
|
|
102
100
|
let doc;
|
|
103
101
|
try {
|
|
104
|
-
doc = await
|
|
102
|
+
doc = await client.updateDoc(id, docData).then((r) => r.data);
|
|
105
103
|
} catch (e) {
|
|
106
104
|
spin.stop(true);
|
|
107
105
|
log("Error updating doc:", e.message ?? String(e));
|
|
108
106
|
process.exit(1);
|
|
109
107
|
}
|
|
110
108
|
spin.stop(true);
|
|
111
|
-
log(chalk.
|
|
109
|
+
log(chalk.green("Doc updated successfully!"));
|
|
112
110
|
printDoc(doc);
|
|
113
111
|
if (options.open) openURL(doc.app_url);
|
|
114
112
|
}
|
|
@@ -120,35 +118,35 @@ async function deleteDoc(id, options) {
|
|
|
120
118
|
}
|
|
121
119
|
spin.start();
|
|
122
120
|
try {
|
|
123
|
-
await
|
|
121
|
+
await client.deleteDoc(id, {});
|
|
124
122
|
} catch (e) {
|
|
125
123
|
spin.stop(true);
|
|
126
124
|
log("Error deleting doc:", e.message ?? String(e));
|
|
127
125
|
process.exit(1);
|
|
128
126
|
}
|
|
129
127
|
spin.stop(true);
|
|
130
|
-
log(chalk.
|
|
128
|
+
log(chalk.green(`Doc ${id} deleted successfully.`));
|
|
131
129
|
}
|
|
132
130
|
function printDoc(doc, includeHtml = false) {
|
|
133
|
-
log(chalk.
|
|
134
|
-
log(chalk.
|
|
135
|
-
log(chalk.
|
|
136
|
-
log(chalk.
|
|
137
|
-
if (doc.created_at !== doc.updated_at) log(chalk.
|
|
138
|
-
if (doc.archived) log(chalk.
|
|
131
|
+
log(chalk.blue.bold(doc.title || "(Untitled)"));
|
|
132
|
+
log(chalk.bold("ID:") + ` ${doc.id}`);
|
|
133
|
+
log(chalk.bold("URL:") + ` ${doc.app_url}`);
|
|
134
|
+
log(chalk.bold("Created:") + ` ${doc.created_at}`);
|
|
135
|
+
if (doc.created_at !== doc.updated_at) log(chalk.bold("Updated:") + ` ${doc.updated_at}`);
|
|
136
|
+
if (doc.archived) log(chalk.bold("Archived:") + ` ${doc.archived}`);
|
|
139
137
|
log();
|
|
140
138
|
if (doc.content_markdown) {
|
|
141
|
-
log(chalk.
|
|
139
|
+
log(chalk.bold("Content (Markdown):"));
|
|
142
140
|
log(doc.content_markdown);
|
|
143
141
|
}
|
|
144
142
|
if (includeHtml && doc.content_html) {
|
|
145
143
|
log();
|
|
146
|
-
log(chalk.
|
|
144
|
+
log(chalk.bold("Content (HTML):"));
|
|
147
145
|
log(doc.content_html);
|
|
148
146
|
}
|
|
149
147
|
}
|
|
150
148
|
function openURL(url) {
|
|
151
|
-
|
|
149
|
+
exec(`${os.platform() === "darwin" ? "open" : "xdg-open"} '${url}'`);
|
|
152
150
|
}
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
//#endregion
|
|
152
|
+
export {};
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
let commander = require("commander");
|
|
6
|
-
|
|
2
|
+
import client from "../lib/client.js";
|
|
3
|
+
import spinner from "../lib/spinner.js";
|
|
4
|
+
import { Command } from "commander";
|
|
7
5
|
//#region src/bin/short-docs.ts
|
|
8
|
-
const spin =
|
|
6
|
+
const spin = spinner("Loading docs... %s ");
|
|
9
7
|
const log = console.log;
|
|
10
|
-
const opts = new
|
|
8
|
+
const opts = new Command().description(`List and search Shortcut Docs. By default, lists all docs you have access to.
|
|
11
9
|
Use --title to search docs by title.`).usage("[options]").option("-a, --archived", "Search for archived docs (requires --title)").option("-m, --mine", "Search for docs created by me (requires --title)").option("-f, --following", "Search for docs I am following (requires --title)").option("-t, --title [text]", "Search docs by title (required for search filters)").option("-q, --quiet", "Print only doc output, no loading dialog").option("-I, --idonly", "Print only IDs of doc results").parse(process.argv).opts();
|
|
12
10
|
const main = async () => {
|
|
13
11
|
if (!opts.quiet) spin.start();
|
|
14
12
|
let docs = [];
|
|
15
13
|
try {
|
|
16
|
-
if (opts.title) {
|
|
14
|
+
if (opts.title) if (!opts.archived && !opts.mine && !opts.following) {
|
|
15
|
+
const result = await client.listDocs();
|
|
16
|
+
const title = opts.title.toLowerCase();
|
|
17
|
+
docs = result.data.filter((doc) => (doc.title || "").toLowerCase().includes(title));
|
|
18
|
+
} else {
|
|
17
19
|
const searchParams = { title: opts.title };
|
|
18
20
|
if (opts.archived !== void 0) searchParams.archived = !!opts.archived;
|
|
19
21
|
if (opts.mine) searchParams.created_by_me = true;
|
|
20
22
|
if (opts.following) searchParams.followed_by_me = true;
|
|
21
|
-
docs = (await
|
|
22
|
-
}
|
|
23
|
+
docs = (await client.searchDocuments(searchParams)).data.data;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
23
26
|
if (opts.archived || opts.mine || opts.following) {
|
|
24
27
|
if (!opts.quiet) spin.stop(true);
|
|
25
28
|
log("Note: --archived, --mine, and --following require --title for searching.");
|
|
26
29
|
log("Listing all docs instead...");
|
|
27
30
|
if (!opts.quiet) spin.start();
|
|
28
31
|
}
|
|
29
|
-
docs = (await
|
|
32
|
+
docs = (await client.listDocs()).data;
|
|
30
33
|
}
|
|
31
34
|
} catch (e) {
|
|
32
35
|
if (!opts.quiet) spin.stop(true);
|
|
@@ -38,13 +41,18 @@ const main = async () => {
|
|
|
38
41
|
log("No docs found.");
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
docs.forEach((doc) =>
|
|
44
|
+
docs.forEach((doc) => {
|
|
45
|
+
printDoc(doc);
|
|
46
|
+
});
|
|
42
47
|
};
|
|
43
48
|
const printDoc = (doc) => {
|
|
44
|
-
if (opts.idonly)
|
|
49
|
+
if (opts.idonly) {
|
|
50
|
+
log(doc.id);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
45
53
|
log(`${doc.id} ${doc.title || "(Untitled)"}`);
|
|
46
54
|
log(`\tURL: ${doc.app_url}`);
|
|
47
55
|
};
|
|
48
56
|
main();
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
//#endregion
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
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 os from "os";
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
//#region src/bin/short-epic.ts
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const spin = spinner();
|
|
12
|
+
const log = console.log;
|
|
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);
|
|
19
|
+
program.parse(process.argv);
|
|
20
|
+
async function createEpic(options) {
|
|
21
|
+
const entities = await stories_default.fetchEntities();
|
|
22
|
+
if (!options.idonly) spin.start();
|
|
23
|
+
const epicData = { name: options.name ?? "" };
|
|
24
|
+
if (options.description) epicData.description = options.description;
|
|
25
|
+
const state = normalizeEpicState(options.state);
|
|
26
|
+
if (state) epicData.state = state;
|
|
27
|
+
if (options.deadline) epicData.deadline = new Date(options.deadline).toISOString();
|
|
28
|
+
if (options.plannedStart) epicData.planned_start_date = new Date(options.plannedStart).toISOString();
|
|
29
|
+
if (options.owners) epicData.owner_ids = stories_default.findOwnerIds(entities, options.owners);
|
|
30
|
+
if (options.team) {
|
|
31
|
+
const group = stories_default.findGroup(entities, options.team);
|
|
32
|
+
if (group?.id) epicData.group_ids = [group.id];
|
|
33
|
+
}
|
|
34
|
+
if (options.label) epicData.labels = stories_default.findLabelNames(entities, options.label);
|
|
35
|
+
if (options.milestone) epicData.milestone_id = parseInt(options.milestone, 10);
|
|
36
|
+
if (options.objectives) epicData.objective_ids = stories_default.findObjectiveIds(entities, options.objectives);
|
|
37
|
+
let epic;
|
|
38
|
+
if (!epicData.name) {
|
|
39
|
+
if (!options.idonly) spin.stop(true);
|
|
40
|
+
log("Must provide --name");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
} else try {
|
|
43
|
+
epic = await client.createEpic(epicData).then((r) => r.data);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (!options.idonly) spin.stop(true);
|
|
46
|
+
log("Error creating epic:", e.message ?? String(e));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!options.idonly) spin.stop(true);
|
|
50
|
+
if (epic) if (options.idonly) log(epic.id);
|
|
51
|
+
else {
|
|
52
|
+
printEpic(epic);
|
|
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);
|
|
105
|
+
}
|
|
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
|
+
}
|
|
156
|
+
function printEpic(epic) {
|
|
157
|
+
log(`#${epic.id} ${epic.name}`);
|
|
158
|
+
if (epic.description) log(`Description:\t${epic.description}`);
|
|
159
|
+
log(`State:\t\t${epic.state}`);
|
|
160
|
+
log(`Archived:\t${epic.archived ? "yes" : "no"}`);
|
|
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(", ")}`);
|
|
163
|
+
if (epic.deadline) log(`Deadline:\t${epic.deadline}`);
|
|
164
|
+
if (epic.planned_start_date) log(`Planned Start:\t${epic.planned_start_date}`);
|
|
165
|
+
if (epic.owner_ids && epic.owner_ids.length > 0) log(`Owners:\t\t${epic.owner_ids.join(", ")}`);
|
|
166
|
+
if (epic.group_ids && epic.group_ids.length > 0) log(`Teams:\t\t${epic.group_ids.join(", ")}`);
|
|
167
|
+
if (epic.labels && epic.labels.length > 0) log(`Labels:\t\t${epic.labels.map((label) => label.name).join(", ")}`);
|
|
168
|
+
log(`URL:\t\t${epic.app_url}`);
|
|
169
|
+
}
|
|
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 {};
|
|
@@ -0,0 +1,36 @@
|
|
|
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-epics.ts
|
|
8
|
+
const log = console.log;
|
|
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();
|
|
11
|
+
const main = async () => {
|
|
12
|
+
spin.start();
|
|
13
|
+
const [epics, entities] = await Promise.all([client.listEpics().then((r) => r.data), stories_default.fetchEntities()]);
|
|
14
|
+
spin.stop(true);
|
|
15
|
+
const textMatch = new RegExp(opts.title ?? "", "i");
|
|
16
|
+
const objectiveIds = opts.objectives ? stories_default.findObjectiveIds(entities, opts.objectives) : [];
|
|
17
|
+
epics.filter((epic) => {
|
|
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));
|
|
21
|
+
};
|
|
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`;
|
|
27
|
+
defaultFormat += `Points:\t\t%p\nPoints Started: %ps\nPoints Done:\t%pd\nCompletion:\t%c\n`;
|
|
28
|
+
if (epic.started) defaultFormat += `Started:\t%st\n`;
|
|
29
|
+
if (epic.completed) defaultFormat += `Completed:\t%co\n`;
|
|
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}`));
|
|
33
|
+
};
|
|
34
|
+
main();
|
|
35
|
+
//#endregion
|
|
36
|
+
export {};
|