@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,151 @@
|
|
|
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 os from "os";
|
|
6
|
+
import { exec } from "child_process";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
//#region src/bin/short-objective.ts
|
|
9
|
+
const spin = spinner();
|
|
10
|
+
const log = console.log;
|
|
11
|
+
const program = new Command().usage("[command] [options]").description("view, create, or update objectives");
|
|
12
|
+
program.command("view <id>").description("view an objective by id").option("-O, --open", "Open objective in browser").action(viewObjective);
|
|
13
|
+
program.command("create").description("create a new objective").option("-n, --name [text]", "Set name of objective, required", "").option("-d, --description [text]", "Set description of objective", "").option("-s, --state [name]", "Set state of objective (to do, in progress, done)", "").option("--started-at [date]", "Set started override (ISO date or YYYY-MM-DD)", "").option("--completed-at [date]", "Set completed override (ISO date or YYYY-MM-DD)", "").option("-I, --idonly", "Print only ID of objective result").option("-O, --open", "Open objective in browser").action(createObjective);
|
|
14
|
+
program.command("update <id>").description("update an existing objective").option("-n, --name [text]", "Set name of objective", "").option("-d, --description [text]", "Set description of objective", "").option("-s, --state [name]", "Set state of objective (to do, in progress, done)", "").option("--started-at [date]", "Set started override (ISO date or YYYY-MM-DD)", "").option("--completed-at [date]", "Set completed override (ISO date or YYYY-MM-DD)", "").option("-a, --archived", "Archive objective").option("-O, --open", "Open objective in browser").action(updateObjective);
|
|
15
|
+
program.command("epics <id>").description("list epics in an objective").action(listObjectiveEpics);
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
if (args.length > 0 && args[0] && /^\d+$/.test(args[0])) process.argv.splice(2, 0, "view");
|
|
18
|
+
program.parse(process.argv);
|
|
19
|
+
if (args.length === 0) {
|
|
20
|
+
program.outputHelp();
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
async function viewObjective(id, options) {
|
|
24
|
+
spin.start();
|
|
25
|
+
try {
|
|
26
|
+
const objective = await client.getObjective(parseInt(id, 10)).then((r) => r.data);
|
|
27
|
+
spin.stop(true);
|
|
28
|
+
printObjective(objective);
|
|
29
|
+
if (options.open) openURL(objective.app_url);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
spin.stop(true);
|
|
32
|
+
const error = e;
|
|
33
|
+
if (error.response?.status === 404) log(`Objective #${id} not found`);
|
|
34
|
+
else log("Error fetching objective:", error.message ?? String(e));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function createObjective(options) {
|
|
39
|
+
if (!options.name) {
|
|
40
|
+
log("Must provide --name");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (!options.idonly) spin.start();
|
|
44
|
+
const objectiveData = { name: options.name };
|
|
45
|
+
if (options.description) objectiveData.description = options.description;
|
|
46
|
+
const state = normalizeObjectiveState(options.state);
|
|
47
|
+
if (state) objectiveData.state = state;
|
|
48
|
+
if (options.startedAt) objectiveData.started_at_override = normalizeDate(options.startedAt);
|
|
49
|
+
if (options.completedAt) objectiveData.completed_at_override = normalizeDate(options.completedAt);
|
|
50
|
+
let objective;
|
|
51
|
+
try {
|
|
52
|
+
objective = await client.createObjective(objectiveData).then((r) => r.data);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (!options.idonly) spin.stop(true);
|
|
55
|
+
log("Error creating objective:", e.message ?? String(e));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (!options.idonly) spin.stop(true);
|
|
59
|
+
if (options.idonly) {
|
|
60
|
+
log(objective.id);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
printObjective(objective);
|
|
64
|
+
if (options.open) openURL(objective.app_url);
|
|
65
|
+
}
|
|
66
|
+
async function updateObjective(id, options) {
|
|
67
|
+
const updateData = {};
|
|
68
|
+
if (options.name) updateData.name = options.name;
|
|
69
|
+
if (options.description) updateData.description = options.description;
|
|
70
|
+
const state = normalizeObjectiveState(options.state);
|
|
71
|
+
if (state) updateData.state = state;
|
|
72
|
+
if (options.startedAt) updateData.started_at_override = normalizeDate(options.startedAt);
|
|
73
|
+
if (options.completedAt) updateData.completed_at_override = normalizeDate(options.completedAt);
|
|
74
|
+
if (options.archived) updateData.archived = true;
|
|
75
|
+
if (Object.keys(updateData).length === 0) {
|
|
76
|
+
log("No updates provided. Use --name, --description, --state, --started-at, --completed-at, or --archived");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
spin.start();
|
|
80
|
+
try {
|
|
81
|
+
const objective = await client.updateObjective(parseInt(id, 10), updateData).then((r) => r.data);
|
|
82
|
+
spin.stop(true);
|
|
83
|
+
printObjective(objective);
|
|
84
|
+
if (options.open) openURL(objective.app_url);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
spin.stop(true);
|
|
87
|
+
const error = e;
|
|
88
|
+
if (error.response?.status === 404) log(`Objective #${id} not found`);
|
|
89
|
+
else log("Error updating objective:", error.message ?? String(e));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function listObjectiveEpics(id) {
|
|
94
|
+
spin.start();
|
|
95
|
+
try {
|
|
96
|
+
const epics = await client.listObjectiveEpics(parseInt(id, 10)).then((r) => r.data);
|
|
97
|
+
spin.stop(true);
|
|
98
|
+
if (epics.length === 0) {
|
|
99
|
+
log(`No epics found in objective #${id}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
log(chalk.bold(`Epics in objective #${id}:`));
|
|
103
|
+
log();
|
|
104
|
+
epics.forEach(printEpic);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
spin.stop(true);
|
|
107
|
+
const error = e;
|
|
108
|
+
if (error.response?.status === 404) log(`Objective #${id} not found`);
|
|
109
|
+
else log("Error fetching objective epics:", error.message ?? String(e));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function normalizeObjectiveState(state) {
|
|
114
|
+
if (!state) return void 0;
|
|
115
|
+
const stateMap = {
|
|
116
|
+
todo: "to do",
|
|
117
|
+
"to do": "to do",
|
|
118
|
+
inprogress: "in progress",
|
|
119
|
+
"in progress": "in progress",
|
|
120
|
+
done: "done"
|
|
121
|
+
};
|
|
122
|
+
return stateMap[state.toLowerCase().replace(/[^a-z]/g, "")] || stateMap[state.toLowerCase()];
|
|
123
|
+
}
|
|
124
|
+
function normalizeDate(value) {
|
|
125
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return (/* @__PURE__ */ new Date(`${value}T00:00:00.000Z`)).toISOString();
|
|
126
|
+
return new Date(value).toISOString();
|
|
127
|
+
}
|
|
128
|
+
function printObjective(objective) {
|
|
129
|
+
log(`#${objective.id} ${objective.name}`);
|
|
130
|
+
log(`State:\t\t${objective.state}`);
|
|
131
|
+
log(`Started:\t${objective.started ? "yes" : "no"}`);
|
|
132
|
+
log(`Completed:\t${objective.completed ? "yes" : "no"}`);
|
|
133
|
+
log(`Archived:\t${objective.archived ? "yes" : "no"}`);
|
|
134
|
+
if (objective.description) log(`Description:\t${objective.description}`);
|
|
135
|
+
if (objective.categories.length > 0) log(`Categories:\t${objective.categories.map((category) => category.name).join(", ")}`);
|
|
136
|
+
if (objective.started_at) log(`Started At:\t${objective.started_at}`);
|
|
137
|
+
if (objective.completed_at) log(`Completed At:\t${objective.completed_at}`);
|
|
138
|
+
log(`Updated:\t${objective.updated_at}`);
|
|
139
|
+
log(`URL:\t\t${objective.app_url}`);
|
|
140
|
+
}
|
|
141
|
+
function printEpic(epic) {
|
|
142
|
+
log(`${chalk.bold("#" + epic.id)} ${chalk.blue(epic.name)}`);
|
|
143
|
+
log(` State: ${epic.state} | Started: ${epic.started ? "yes" : "no"} | Completed: ${epic.completed ? "yes" : "no"}`);
|
|
144
|
+
log(` URL: ${epic.app_url}`);
|
|
145
|
+
log();
|
|
146
|
+
}
|
|
147
|
+
function openURL(url) {
|
|
148
|
+
exec(`${os.platform() === "darwin" ? "open" : "xdg-open"} '${url}'`);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
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-objectives.ts
|
|
7
|
+
const log = console.log;
|
|
8
|
+
const spin = spinner();
|
|
9
|
+
const program = new Command().description(`List and search Shortcut objectives. By default, lists all objectives.
|
|
10
|
+
Passing search operators will use the Shortcut objective search API and page through all results.`).allowExcessArguments(true).usage("[options] [SEARCH OPERATORS]").option("-a, --archived", "List only objectives including archived", "").option("-c, --completed", "List only objectives that have been completed", "").option("-d, --detailed", "List more details for each objective", "").option("-f, --format [template]", "Format each objective output by template", "").option("-s, --started", "List objectives that have been started", "").option("-S, --state [state]", "Filter objectives by state", "").option("-t, --title [query]", "Filter objectives with name/title containing query", "");
|
|
11
|
+
program.parse(process.argv);
|
|
12
|
+
const opts = program.opts();
|
|
13
|
+
const main = async () => {
|
|
14
|
+
spin.start();
|
|
15
|
+
let objectives = [];
|
|
16
|
+
try {
|
|
17
|
+
objectives = program.args.length ? await searchObjectives(program.args) : await listObjectives();
|
|
18
|
+
} catch (e) {
|
|
19
|
+
spin.stop(true);
|
|
20
|
+
log("Error fetching objectives:", e.message ?? String(e));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
spin.stop(true);
|
|
24
|
+
const textMatch = new RegExp(opts.title ?? "", "i");
|
|
25
|
+
const stateMatch = new RegExp(opts.state ?? "", "i");
|
|
26
|
+
objectives.filter((objective) => {
|
|
27
|
+
return `${objective.name} ${objective.description ?? ""}`.match(textMatch) && objective.state.match(stateMatch);
|
|
28
|
+
}).forEach(printItem);
|
|
29
|
+
};
|
|
30
|
+
async function listObjectives() {
|
|
31
|
+
return client.listObjectives().then((r) => r.data);
|
|
32
|
+
}
|
|
33
|
+
async function searchObjectives(args) {
|
|
34
|
+
const query = args.join(" ");
|
|
35
|
+
let result = await client.searchObjectives({
|
|
36
|
+
query,
|
|
37
|
+
detail: "full"
|
|
38
|
+
});
|
|
39
|
+
let objectives = result.data.data;
|
|
40
|
+
while (result.data.next) {
|
|
41
|
+
const nextCursor = new URLSearchParams(result.data.next).get("next");
|
|
42
|
+
result = await client.searchObjectives({
|
|
43
|
+
query,
|
|
44
|
+
detail: "full",
|
|
45
|
+
next: nextCursor ?? void 0
|
|
46
|
+
});
|
|
47
|
+
objectives = objectives.concat(result.data.data);
|
|
48
|
+
}
|
|
49
|
+
return objectives;
|
|
50
|
+
}
|
|
51
|
+
const printItem = (objective) => {
|
|
52
|
+
if (objective.archived && !opts.archived) return;
|
|
53
|
+
if (!objective.started && opts.started) return;
|
|
54
|
+
if (!objective.completed && opts.completed) return;
|
|
55
|
+
let defaultFormat = `#%id %t\nState:\t\t%s\nStarted:\t%st\nCompleted:\t%co\n`;
|
|
56
|
+
if (opts.detailed) defaultFormat += `Updated:\t%u\nCategories:\t%cat\nURL:\t\t%url\nDescription:\t%d\n`;
|
|
57
|
+
const format = opts.format || defaultFormat;
|
|
58
|
+
const categories = "categories" in objective ? objective.categories.map((category) => category.name).join(", ") || "_" : "_";
|
|
59
|
+
log(format.replace(/%id/g, chalk.bold(`${objective.id}`)).replace(/%t/g, chalk.blue(`${objective.name}`)).replace(/%st/g, `${objective.started ? "yes" : "no"}`).replace(/%co/g, `${objective.completed ? "yes" : "no"}`).replace(/%s/g, `${objective.state}`).replace(/%u/g, `${objective.updated_at}`).replace(/%cat/g, categories).replace(/%url/g, `${objective.app_url}`).replace(/%d/g, `${objective.description || "_"}`));
|
|
60
|
+
};
|
|
61
|
+
main();
|
|
62
|
+
//#endregion
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
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-projects.ts
|
|
7
|
+
const spin = spinner();
|
|
8
|
+
const log = console.log;
|
|
9
|
+
const opts = new Command().description("Display projects available for stories").option("-a, --archived", "List only projects including archived", "").option("-d, --detailed", "List more details for each project", "").option("-t, --title [query]", "List projects with name/title containing query", "").parse(process.argv).opts();
|
|
10
|
+
const main = async () => {
|
|
11
|
+
spin.start();
|
|
12
|
+
const projects = await client.listProjects().then((r) => r.data);
|
|
13
|
+
spin.stop(true);
|
|
14
|
+
const textMatch = new RegExp(opts.title ?? "", "i");
|
|
15
|
+
projects.filter((o) => {
|
|
16
|
+
return !!`${o.name} ${o.name}`.match(textMatch);
|
|
17
|
+
}).map(printItem);
|
|
18
|
+
};
|
|
19
|
+
const printItem = (proj) => {
|
|
20
|
+
if (proj.archived && !opts.archived) return;
|
|
21
|
+
log(chalk.bold(`#${proj.id}`) + chalk.blue(` ${proj.name}`));
|
|
22
|
+
log(chalk.bold("Points: ") + ` ${proj.stats.num_points}`);
|
|
23
|
+
log(chalk.bold("Stories: ") + ` ${proj.stats.num_stories}`);
|
|
24
|
+
log(chalk.bold("Started: ") + ` ${proj.start_time}`);
|
|
25
|
+
if (proj.archived) log(chalk.bold("Archived: ") + ` ${proj.archived}`);
|
|
26
|
+
if (opts.detailed) log(chalk.bold("Description: ") + ` ${proj.description}`);
|
|
27
|
+
log();
|
|
28
|
+
};
|
|
29
|
+
main();
|
|
30
|
+
//#endregion
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import configure_default from "../lib/configure.js";
|
|
3
|
+
import spinner from "../lib/spinner.js";
|
|
4
|
+
import stories_default from "../lib/stories.js";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
//#region src/bin/short-search.ts
|
|
7
|
+
const spin = spinner("Finding... %s ");
|
|
8
|
+
const log = console.log;
|
|
9
|
+
const program = new Command().description(`Search through Shortcut stories. Arguments (non-flag/options) will be
|
|
10
|
+
passed to Shortcut story search API as search operators. Passing '%self%' as
|
|
11
|
+
a search operator argument will be replaced by your mention name. Note that
|
|
12
|
+
passing search operators and options (e.g. --owner foobar) will use the
|
|
13
|
+
options as extra filtering in the client.
|
|
14
|
+
|
|
15
|
+
Refer to https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators
|
|
16
|
+
for more details about search operators.`).allowExcessArguments(true).usage("[options] [SEARCH OPERATORS]").option("-a, --archived", "Include archived Stories").option("-c, --created [operator][date]", "Stories created within criteria (operator is one of <|>|=)", "").option("-q, --quiet", "Print only story output, no loading dialog", "").option("-l, --label [id|name]", "Stories with label id/name, by regex", "").option("-o, --owner [name]", "Stories with owner, by regex", "").option("-p, --project [id]", "Stories in project", "").option("-s, --state [id|name]", "Stories in workflow state id/name, by regex", "").option("--epic [id|name]", "Stories in epic id/name, by regex", "").option("-i, --iteration [id|name]", "Stories in iteration id/name, by regex", "").option("-S, --save [name]", "Save search configuration as workspace").option("-t, --text [name]", "Stories with text in name, by regex", "").option("-e, --estimate [operator][number]", "Stories estimated within criteria (operator is one of <|>|=)", "").option("-u, --updated [operator][date]", "Stories updated within criteria (operator is one of <|>|=)", "").option("-y, --type [name]", "Stories of type, by regex", "").option("-r, --sort [field]", "Sort stories by field (accessor[:asc|desc][,next])", "state.position:asc,position:asc").option("-f, --format [template]", "Format each story output by template", "");
|
|
17
|
+
const getWorkspaceOptions = (opts) => {
|
|
18
|
+
const blacklistedKeys = ["save"];
|
|
19
|
+
return Object.entries(opts).filter(([key]) => !blacklistedKeys.includes(key)).reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {});
|
|
20
|
+
};
|
|
21
|
+
const main = async () => {
|
|
22
|
+
program.parse(process.argv);
|
|
23
|
+
const opts = program.opts();
|
|
24
|
+
if (!opts.quiet) {
|
|
25
|
+
if (!program.args.length) log("Fetching all stories for search since no search operators were passed ...");
|
|
26
|
+
spin.start();
|
|
27
|
+
}
|
|
28
|
+
let stories = [];
|
|
29
|
+
try {
|
|
30
|
+
stories = await stories_default.listStories({
|
|
31
|
+
...opts,
|
|
32
|
+
args: program.args
|
|
33
|
+
});
|
|
34
|
+
} catch (e) {
|
|
35
|
+
log("Error fetching stories:", e);
|
|
36
|
+
}
|
|
37
|
+
if (!opts.quiet) spin.stop(true);
|
|
38
|
+
stories.map(stories_default.printFormattedStory(opts));
|
|
39
|
+
if (!opts.save) return;
|
|
40
|
+
const name = opts.save === true ? "default" : opts.save;
|
|
41
|
+
if (configure_default.saveWorkspace(name, getWorkspaceOptions(opts))) log("Saved query as %s workspace", name);
|
|
42
|
+
};
|
|
43
|
+
if (process.argv[1]?.includes("short-search")) main();
|
|
44
|
+
//#endregion
|
|
45
|
+
export { main, program };
|