@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.
Files changed (48) hide show
  1. package/README.md +428 -8
  2. package/build/bin/{short-api.cjs → short-api.js} +13 -16
  3. package/build/bin/short-create.js +76 -0
  4. package/build/bin/short-custom-field.js +50 -0
  5. package/build/bin/short-custom-fields.js +29 -0
  6. package/build/bin/{short-doc.cjs → short-doc.js} +34 -36
  7. package/build/bin/{short-docs.cjs → short-docs.js} +23 -15
  8. package/build/bin/short-epic.js +186 -0
  9. package/build/bin/short-epics.js +36 -0
  10. package/build/bin/short-find.js +6 -0
  11. package/build/bin/short-install.js +87 -0
  12. package/build/bin/{short-iteration.cjs → short-iteration.js} +41 -45
  13. package/build/bin/{short-iterations.cjs → short-iterations.js} +15 -19
  14. package/build/bin/short-label.js +130 -0
  15. package/build/bin/short-labels.js +27 -0
  16. package/build/bin/short-members.js +31 -0
  17. package/build/bin/short-objective.js +151 -0
  18. package/build/bin/short-objectives.js +63 -0
  19. package/build/bin/short-projects.js +31 -0
  20. package/build/bin/short-search.js +45 -0
  21. package/build/bin/short-story.js +458 -0
  22. package/build/bin/short-team.js +78 -0
  23. package/build/bin/short-teams.js +28 -0
  24. package/build/bin/short-workflows.js +29 -0
  25. package/build/bin/short-workspace.js +63 -0
  26. package/build/bin/short.js +8 -0
  27. package/build/lib/client.js +9 -0
  28. package/build/lib/{configure.cjs → configure.js} +18 -27
  29. package/build/lib/spinner.js +12 -0
  30. package/build/lib/{stories.cjs → stories.js} +116 -78
  31. package/build/package.js +5 -0
  32. package/package.json +44 -44
  33. package/build/_virtual/rolldown_runtime.cjs +0 -29
  34. package/build/bin/short-create.cjs +0 -58
  35. package/build/bin/short-epic.cjs +0 -74
  36. package/build/bin/short-epics.cjs +0 -36
  37. package/build/bin/short-find.cjs +0 -7
  38. package/build/bin/short-install.cjs +0 -42
  39. package/build/bin/short-members.cjs +0 -34
  40. package/build/bin/short-projects.cjs +0 -34
  41. package/build/bin/short-search.cjs +0 -49
  42. package/build/bin/short-story.cjs +0 -213
  43. package/build/bin/short-workflows.cjs +0 -32
  44. package/build/bin/short-workspace.cjs +0 -64
  45. package/build/bin/short.cjs +0 -10
  46. package/build/lib/client.cjs +0 -11
  47. package/build/lib/spinner.cjs +0 -17
  48. 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
- const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
3
- const require_lib_spinner = require('../lib/spinner.cjs');
4
- const require_lib_client = require('../lib/client.cjs');
5
- let commander = require("commander");
6
- let os = require("os");
7
- os = require_rolldown_runtime.__toESM(os);
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 = require_lib_spinner.default();
9
+ const spin = spinner();
14
10
  const log = console.log;
15
- const program = new commander.Command().usage("[command] [options]").description("view, create, or update a doc");
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
- if (![
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
- ].includes(args[0]) && !args[0].startsWith("-")) {
33
- console.error(`Error: Unknown command or invalid doc ID: ${args[0]}`);
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 require_lib_client.default.getDoc(id, params).then((r) => r.data);
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 require_lib_client.default.createDoc(docData);
76
- doc = await require_lib_client.default.getDoc(result.data.id).then((r) => r.data);
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.default.green("Doc created successfully!"));
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 require_lib_client.default.updateDoc(id, docData).then((r) => r.data);
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.default.green("Doc updated successfully!"));
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 require_lib_client.default.deleteDoc(id, {});
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.default.green(`Doc ${id} deleted successfully.`));
128
+ log(chalk.green(`Doc ${id} deleted successfully.`));
131
129
  }
132
130
  function printDoc(doc, includeHtml = false) {
133
- log(chalk.default.blue.bold(doc.title || "(Untitled)"));
134
- log(chalk.default.bold("ID:") + ` ${doc.id}`);
135
- log(chalk.default.bold("URL:") + ` ${doc.app_url}`);
136
- log(chalk.default.bold("Created:") + ` ${doc.created_at}`);
137
- if (doc.created_at !== doc.updated_at) log(chalk.default.bold("Updated:") + ` ${doc.updated_at}`);
138
- if (doc.archived) log(chalk.default.bold("Archived:") + ` ${doc.archived}`);
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.default.bold("Content (Markdown):"));
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.default.bold("Content (HTML):"));
144
+ log(chalk.bold("Content (HTML):"));
147
145
  log(doc.content_html);
148
146
  }
149
147
  }
150
148
  function openURL(url) {
151
- (0, child_process.exec)(`${os.default.platform() === "darwin" ? "open" : "xdg-open"} '${url}'`);
149
+ exec(`${os.platform() === "darwin" ? "open" : "xdg-open"} '${url}'`);
152
150
  }
153
-
154
- //#endregion
151
+ //#endregion
152
+ export {};
@@ -1,32 +1,35 @@
1
1
  #!/usr/bin/env node
2
- const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
3
- const require_lib_spinner = require('../lib/spinner.cjs');
4
- const require_lib_client = require('../lib/client.cjs');
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 = require_lib_spinner.default("Loading docs... %s ");
6
+ const spin = spinner("Loading docs... %s ");
9
7
  const log = console.log;
10
- const opts = new commander.Command().description(`List and search Shortcut Docs. By default, lists all docs you have access to.
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 require_lib_client.default.searchDocuments(searchParams)).data.data;
22
- } else {
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 require_lib_client.default.listDocs()).data;
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) => printDoc(doc));
44
+ docs.forEach((doc) => {
45
+ printDoc(doc);
46
+ });
42
47
  };
43
48
  const printDoc = (doc) => {
44
- if (opts.idonly) return log(doc.id);
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
- //#endregion
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 {};
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./short-search.js";
3
+ //#region src/bin/short-find.ts
4
+ main();
5
+ //#endregion
6
+ export {};