@shortcut-cli/shortcut-cli 3.8.1 → 5.0.0

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