@devchangjun/ctm 0.1.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 (3) hide show
  1. package/README.md +219 -0
  2. package/dist/index.js +1191 -0
  3. package/package.json +43 -0
package/dist/index.js ADDED
@@ -0,0 +1,1191 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import chalk from "chalk";
8
+ import { input, password, select } from "@inquirer/prompts";
9
+ import ora from "ora";
10
+
11
+ // src/lib/config.ts
12
+ import Conf from "conf";
13
+ var conf = new Conf({
14
+ projectName: "ctm",
15
+ schema: {
16
+ jiraBaseUrl: { type: "string", default: "" },
17
+ jiraEmail: { type: "string", default: "" },
18
+ jiraApiToken: { type: "string", default: "" },
19
+ jiraAccountId: { type: "string", default: "" },
20
+ jiraProjectKey: { type: "string", default: "" },
21
+ baseBranch: { type: "string", default: "main" },
22
+ branchPrefix: { type: "string", default: "" }
23
+ }
24
+ });
25
+ function getConfig() {
26
+ return {
27
+ jiraBaseUrl: conf.get("jiraBaseUrl"),
28
+ jiraEmail: conf.get("jiraEmail"),
29
+ jiraApiToken: conf.get("jiraApiToken"),
30
+ jiraAccountId: conf.get("jiraAccountId"),
31
+ jiraProjectKey: conf.get("jiraProjectKey"),
32
+ baseBranch: conf.get("baseBranch"),
33
+ branchPrefix: conf.get("branchPrefix")
34
+ };
35
+ }
36
+ function setConfig(config) {
37
+ for (const [key, value] of Object.entries(config)) {
38
+ conf.set(key, value);
39
+ }
40
+ }
41
+ function getConfigPath() {
42
+ return conf.path;
43
+ }
44
+ function ensureConfig() {
45
+ const config = getConfig();
46
+ const missing = [];
47
+ if (!config.jiraBaseUrl) missing.push("jiraBaseUrl");
48
+ if (!config.jiraEmail) missing.push("jiraEmail");
49
+ if (!config.jiraApiToken) missing.push("jiraApiToken");
50
+ if (!config.jiraAccountId) missing.push("jiraAccountId");
51
+ if (!config.jiraProjectKey) missing.push("jiraProjectKey");
52
+ if (missing.length > 0) {
53
+ throw new Error(
54
+ `CTM not configured (missing: ${missing.join(", ")}). Run \`ctm init\` first.`
55
+ );
56
+ }
57
+ return config;
58
+ }
59
+ function resolveIssueKey(input3, projectKey) {
60
+ if (/^\d+$/.test(input3)) return `${projectKey}-${input3}`;
61
+ return input3.toUpperCase();
62
+ }
63
+ function extractIssueKeyFromBranch(branch) {
64
+ const match = branch.match(/([A-Z]+-\d+)/);
65
+ return match ? match[1] : null;
66
+ }
67
+ var BRANCH_PREFIXES = ["feat", "fix", "refactor", "qa", "chore"];
68
+ function buildBranchName(prefix, issueKey) {
69
+ if (prefix) return `${prefix}/${issueKey}`;
70
+ return issueKey;
71
+ }
72
+ function smartBranchPrefix(issueType, defaultPrefix) {
73
+ const type = issueType.toLowerCase();
74
+ if (type.includes("bug")) return "fix";
75
+ if (type.includes("story") || type.includes("feature") || type.includes("improvement"))
76
+ return "feat";
77
+ if (type.includes("refactor")) return "refactor";
78
+ if (type.includes("qa") || type.includes("test")) return "qa";
79
+ if (type.includes("task") || type.includes("epic") || type.includes("chore")) return "chore";
80
+ const match = BRANCH_PREFIXES.find((p) => p === defaultPrefix);
81
+ return match ?? "feat";
82
+ }
83
+
84
+ // src/lib/jira.ts
85
+ var _config = null;
86
+ function initJira(config) {
87
+ _config = config;
88
+ }
89
+ function cfg() {
90
+ if (!_config) throw new Error("Jira client not initialized. Call initJira() first.");
91
+ return _config;
92
+ }
93
+ function auth(email, token) {
94
+ return `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
95
+ }
96
+ async function req(path, method = "GET", body) {
97
+ const { baseUrl, email, apiToken } = cfg();
98
+ const res = await fetch(`${baseUrl}${path}`, {
99
+ method,
100
+ headers: {
101
+ Authorization: auth(email, apiToken),
102
+ Accept: "application/json",
103
+ "Content-Type": "application/json"
104
+ },
105
+ body: body !== void 0 ? JSON.stringify(body) : void 0
106
+ });
107
+ if (!res.ok) {
108
+ const text = await res.text().catch(() => "");
109
+ throw new Error(`Jira ${res.status} ${res.statusText}: ${text.slice(0, 300)}`);
110
+ }
111
+ if (res.status === 204) return void 0;
112
+ return res.json();
113
+ }
114
+ async function validateCredentials(baseUrl, email, token) {
115
+ try {
116
+ const res = await fetch(`${baseUrl}/rest/api/3/myself`, {
117
+ headers: { Authorization: auth(email, token) }
118
+ });
119
+ return res.ok;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+ async function getMyself() {
125
+ return req("/rest/api/3/myself");
126
+ }
127
+ async function getProjects() {
128
+ const data = await req(
129
+ "/rest/api/3/project/search?maxResults=100&orderBy=name"
130
+ );
131
+ return data.values.map((p) => ({ key: p.key, name: p.name }));
132
+ }
133
+ function parseNode(node) {
134
+ if (node.type === "text") return node.text ?? "";
135
+ if (node.content) return node.content.map(parseNode).join("");
136
+ return "";
137
+ }
138
+ function parseDescription(desc) {
139
+ if (!desc) return "";
140
+ if (typeof desc === "string") return desc;
141
+ try {
142
+ const node = desc;
143
+ if (node.content) {
144
+ return node.content.map((child) => parseNode(child)).join("\n").trim();
145
+ }
146
+ } catch {
147
+ }
148
+ return "";
149
+ }
150
+ function toCtmIssue(raw, baseUrl) {
151
+ return {
152
+ id: raw.id,
153
+ key: raw.key,
154
+ title: raw.fields.summary,
155
+ description: parseDescription(raw.fields.description) || void 0,
156
+ priority: raw.fields.priority ?? { id: "", name: "None" },
157
+ status: raw.fields.status,
158
+ url: `${baseUrl}/browse/${raw.key}`,
159
+ issuetype: raw.fields.issuetype ?? { id: "", name: "Task" },
160
+ labels: raw.fields.labels ?? [],
161
+ assignee: raw.fields.assignee
162
+ };
163
+ }
164
+ var ISSUE_FIELDS = [
165
+ "summary",
166
+ "description",
167
+ "priority",
168
+ "status",
169
+ "assignee",
170
+ "labels",
171
+ "issuetype"
172
+ ];
173
+ async function getMyIssues(accountId, opts = {}) {
174
+ const parts = [`assignee = "${accountId}"`];
175
+ if (opts.projectKey) parts.push(`project = "${opts.projectKey}"`);
176
+ if (opts.statusFilter) {
177
+ parts.push(`status = "${opts.statusFilter}"`);
178
+ } else {
179
+ parts.push(`statusCategory != Done`);
180
+ }
181
+ const jql = `${parts.join(" AND ")} ORDER BY updated DESC`;
182
+ const data = await req("/rest/api/3/search/jql", "POST", {
183
+ jql,
184
+ fields: ISSUE_FIELDS,
185
+ maxResults: 50
186
+ });
187
+ const { baseUrl } = cfg();
188
+ return data.issues.map((i) => toCtmIssue(i, baseUrl));
189
+ }
190
+ async function getIssue(key) {
191
+ const data = await req(
192
+ `/rest/api/3/issue/${key}?fields=${ISSUE_FIELDS.join(",")}`
193
+ );
194
+ const { baseUrl } = cfg();
195
+ return toCtmIssue(data, baseUrl);
196
+ }
197
+ async function updateIssueStatus(issueKey, targetStatus) {
198
+ const data = await req(`/rest/api/3/issue/${issueKey}/transitions`);
199
+ const transition = data.transitions.find((t) => {
200
+ const name = (t.to?.name ?? t.name).toLowerCase();
201
+ return name === targetStatus.toLowerCase();
202
+ });
203
+ if (!transition) {
204
+ const available = data.transitions.map((t) => t.to?.name ?? t.name).join(", ");
205
+ throw new Error(`No transition to "${targetStatus}". Available: ${available}`);
206
+ }
207
+ await req(`/rest/api/3/issue/${issueKey}/transitions`, "POST", {
208
+ transition: { id: transition.id }
209
+ });
210
+ }
211
+ async function addComment(issueKey, text) {
212
+ await req(`/rest/api/3/issue/${issueKey}/comment`, "POST", {
213
+ body: {
214
+ type: "doc",
215
+ version: 1,
216
+ content: [
217
+ {
218
+ type: "paragraph",
219
+ content: [{ type: "text", text }]
220
+ }
221
+ ]
222
+ }
223
+ });
224
+ }
225
+
226
+ // src/commands/init.ts
227
+ async function initCommand() {
228
+ const existing = getConfig();
229
+ console.log(chalk.bold("\n\u{1F3AB} CTM \u2014 Colo Ticket Manager\n"));
230
+ console.log(chalk.dim("Configure your Jira connection. Press Enter to keep existing values.\n"));
231
+ const jiraBaseUrl = await input({
232
+ message: "Jira base URL",
233
+ default: existing.jiraBaseUrl || void 0,
234
+ validate: (v) => v.startsWith("https://") ? true : "Must start with https://"
235
+ });
236
+ const jiraEmail = await input({
237
+ message: "Jira email",
238
+ default: existing.jiraEmail || void 0,
239
+ validate: (v) => v.includes("@") ? true : "Enter a valid email"
240
+ });
241
+ const jiraApiToken = await password({
242
+ message: "Jira API token (hidden)",
243
+ mask: "\u2022",
244
+ validate: (v) => v.length > 0 ? true : "Required"
245
+ });
246
+ const spinner = ora("Validating credentials\u2026").start();
247
+ const valid = await validateCredentials(jiraBaseUrl, jiraEmail, jiraApiToken);
248
+ if (!valid) {
249
+ spinner.fail(chalk.red("Invalid credentials. Check your email and API token."));
250
+ console.log(
251
+ chalk.dim(
252
+ " Get a token: https://id.atlassian.com/manage-profile/security/api-tokens"
253
+ )
254
+ );
255
+ process.exit(1);
256
+ }
257
+ initJira({ baseUrl: jiraBaseUrl, email: jiraEmail, apiToken: jiraApiToken });
258
+ const me = await getMyself();
259
+ spinner.succeed(
260
+ `Authenticated as ${chalk.green(me.displayName)} ${chalk.dim(`(${me.emailAddress})`)}`
261
+ );
262
+ const projectSpinner = ora("Loading projects\u2026").start();
263
+ const projects = await getProjects();
264
+ projectSpinner.stop();
265
+ if (projects.length === 0) {
266
+ console.log(chalk.yellow("No projects found. Check your Jira permissions."));
267
+ process.exit(1);
268
+ }
269
+ const jiraProjectKey = await select({
270
+ message: "Default Jira project",
271
+ choices: projects.map((p) => ({
272
+ value: p.key,
273
+ name: `${chalk.cyan(p.key.padEnd(10))} ${p.name}`
274
+ })),
275
+ default: existing.jiraProjectKey || projects[0]?.key,
276
+ pageSize: 12
277
+ });
278
+ const baseBranch = await input({
279
+ message: "Base branch",
280
+ default: existing.baseBranch || "main"
281
+ });
282
+ const branchPrefix = await input({
283
+ message: "Default branch prefix (e.g. feat, fix \u2014 leave empty for none)",
284
+ default: existing.branchPrefix || ""
285
+ });
286
+ setConfig({
287
+ jiraBaseUrl,
288
+ jiraEmail,
289
+ jiraApiToken,
290
+ jiraAccountId: me.accountId,
291
+ jiraProjectKey,
292
+ baseBranch,
293
+ branchPrefix
294
+ });
295
+ console.log();
296
+ console.log(chalk.green("\u2713 CTM configured successfully!"));
297
+ console.log(` ${chalk.dim("Config:")} ${chalk.cyan(getConfigPath())}`);
298
+ console.log();
299
+ console.log(chalk.dim(" Next: ctm ls \u2014 view your Jira issues"));
300
+ console.log(chalk.dim(" ctm start 123 \u2014 create a branch for issue 123"));
301
+ console.log();
302
+ }
303
+
304
+ // src/commands/issues.ts
305
+ import chalk3 from "chalk";
306
+ import ora2 from "ora";
307
+
308
+ // src/lib/ui.ts
309
+ import chalk2 from "chalk";
310
+ var PRIORITY_COLOR = {
311
+ highest: chalk2.red,
312
+ high: chalk2.yellow,
313
+ medium: chalk2.blue,
314
+ low: chalk2.gray,
315
+ lowest: chalk2.gray
316
+ };
317
+ function priorityBadge(name) {
318
+ const color = PRIORITY_COLOR[name.toLowerCase()] ?? chalk2.white;
319
+ return color("\u25CF");
320
+ }
321
+ function priorityLabel(name) {
322
+ const color = PRIORITY_COLOR[name.toLowerCase()] ?? chalk2.white;
323
+ return color(name);
324
+ }
325
+ function printIssue(issue) {
326
+ console.log();
327
+ console.log(chalk2.bold("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
328
+ console.log(` ${chalk2.cyan("Key:")} ${chalk2.bold(issue.key)}`);
329
+ console.log(` ${chalk2.cyan("Title:")} ${issue.title}`);
330
+ console.log(` ${chalk2.cyan("Type:")} ${issue.issuetype.name}`);
331
+ console.log(` ${chalk2.cyan("Priority:")} ${priorityLabel(issue.priority.name)}`);
332
+ console.log(` ${chalk2.cyan("Status:")} ${issue.status.name}`);
333
+ if (issue.assignee) {
334
+ console.log(` ${chalk2.cyan("Assignee:")} ${issue.assignee.displayName}`);
335
+ }
336
+ if (issue.labels.length > 0) {
337
+ console.log(` ${chalk2.cyan("Labels:")} ${issue.labels.join(", ")}`);
338
+ }
339
+ console.log(` ${chalk2.cyan("URL:")} ${chalk2.underline(chalk2.dim(issue.url))}`);
340
+ if (issue.description) {
341
+ const preview = issue.description.slice(0, 200);
342
+ const suffix = issue.description.length > 200 ? " \u2026" : "";
343
+ console.log(` ${chalk2.cyan("Desc:")}`);
344
+ for (const line of preview.split("\n").slice(0, 4)) {
345
+ console.log(` ${chalk2.dim(line)}`);
346
+ }
347
+ if (suffix) console.log(chalk2.dim(" \u2026"));
348
+ }
349
+ console.log(chalk2.bold("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
350
+ console.log();
351
+ }
352
+ function printSuccess(msg) {
353
+ console.log(`${chalk2.green("\u2713")} ${msg}`);
354
+ }
355
+ function printWarn(msg) {
356
+ console.log(`${chalk2.yellow("\u26A0")} ${msg}`);
357
+ }
358
+ function printError(msg) {
359
+ console.error(`${chalk2.red("\u2717")} ${msg}`);
360
+ }
361
+
362
+ // src/commands/issues.ts
363
+ var STATUS_PRIORITY = ["In Progress", "To Do", "Open", "Backlog", "Selected for Development"];
364
+ function groupByStatus(issues) {
365
+ const map = /* @__PURE__ */ new Map();
366
+ for (const issue of issues) {
367
+ const s = issue.status.name;
368
+ if (!map.has(s)) map.set(s, []);
369
+ map.get(s).push(issue);
370
+ }
371
+ const sorted = /* @__PURE__ */ new Map();
372
+ for (const s of STATUS_PRIORITY) {
373
+ if (map.has(s)) sorted.set(s, map.get(s));
374
+ }
375
+ for (const [k, v] of map) {
376
+ if (!sorted.has(k)) sorted.set(k, v);
377
+ }
378
+ return sorted;
379
+ }
380
+ async function issuesCommand(options) {
381
+ const config = ensureConfig();
382
+ initJira({
383
+ baseUrl: config.jiraBaseUrl,
384
+ email: config.jiraEmail,
385
+ apiToken: config.jiraApiToken
386
+ });
387
+ const spinner = ora2("Fetching issues\u2026").start();
388
+ const issues = await getMyIssues(config.jiraAccountId, {
389
+ projectKey: options.all ? void 0 : options.project ?? config.jiraProjectKey,
390
+ statusFilter: options.status
391
+ });
392
+ spinner.stop();
393
+ if (issues.length === 0) {
394
+ console.log(chalk3.dim("\nNo open issues assigned to you.\n"));
395
+ return;
396
+ }
397
+ const grouped = groupByStatus(issues);
398
+ console.log();
399
+ for (const [status, group] of grouped) {
400
+ console.log(chalk3.bold(status));
401
+ for (const issue of group) {
402
+ const badge = priorityBadge(issue.priority.name);
403
+ const key = chalk3.cyan(issue.key.padEnd(12));
404
+ const title = issue.title.length > 55 ? issue.title.slice(0, 54) + "\u2026" : issue.title.padEnd(55);
405
+ const type = chalk3.dim(`[${issue.issuetype.name}]`);
406
+ console.log(` ${badge} ${key} ${title} ${type}`);
407
+ }
408
+ console.log();
409
+ }
410
+ console.log(
411
+ chalk3.dim(`${issues.length} issue(s) \xB7 ctm start [key] to begin working`)
412
+ );
413
+ console.log();
414
+ }
415
+
416
+ // src/commands/start.ts
417
+ import chalk4 from "chalk";
418
+ import ora3 from "ora";
419
+ import { select as select2, input as input2, confirm } from "@inquirer/prompts";
420
+
421
+ // src/lib/git.ts
422
+ import simpleGit from "simple-git";
423
+ import { execa } from "execa";
424
+ import { basename, dirname, join } from "path";
425
+ function getGit(cwd = process.cwd()) {
426
+ return simpleGit(cwd);
427
+ }
428
+ async function getCurrentBranch() {
429
+ const result = await getGit().revparse(["--abbrev-ref", "HEAD"]);
430
+ return result.trim();
431
+ }
432
+ async function getRepoRoot() {
433
+ const result = await getGit().revparse(["--show-toplevel"]);
434
+ return result.trim();
435
+ }
436
+ async function hasUncommittedChanges() {
437
+ const status = await getGit().status();
438
+ return !status.isClean();
439
+ }
440
+ async function stashChanges(message) {
441
+ await getGit().stash(["push", "-m", message]);
442
+ }
443
+ async function createAndCheckout(branchName, baseBranch) {
444
+ const git = getGit();
445
+ await git.fetch(["--prune"]);
446
+ const local = await git.branchLocal();
447
+ if (local.all.includes(branchName)) {
448
+ await git.checkout(branchName);
449
+ return;
450
+ }
451
+ await git.checkoutBranch(branchName, `origin/${baseBranch}`);
452
+ }
453
+ async function checkoutBranch(branchName) {
454
+ await getGit().checkout(branchName);
455
+ }
456
+ async function pushBranch(branchName) {
457
+ await getGit().push(["-u", "origin", branchName]);
458
+ }
459
+ async function getLocalBranches() {
460
+ const result = await getGit().branchLocal();
461
+ return result.all;
462
+ }
463
+ async function deleteLocalBranch(branchName) {
464
+ await getGit().deleteLocalBranch(branchName, true);
465
+ }
466
+ async function deleteRemoteBranch(branchName) {
467
+ await getGit().push(["origin", "--delete", branchName]);
468
+ }
469
+ async function getDiffStat(baseBranch) {
470
+ try {
471
+ const stat = await getGit().diffSummary([`origin/${baseBranch}..HEAD`]);
472
+ return { files: stat.files.length, insertions: stat.insertions, deletions: stat.deletions };
473
+ } catch {
474
+ return { files: 0, insertions: 0, deletions: 0 };
475
+ }
476
+ }
477
+ async function hasNewCommits(baseBranch) {
478
+ try {
479
+ const log = await getGit().log([`origin/${baseBranch}..HEAD`]);
480
+ return log.total > 0;
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
485
+ async function createPR(title, body) {
486
+ const result = await execa("gh", ["pr", "create", "--title", title, "--body", body]);
487
+ const lines = result.stdout.trim().split("\n");
488
+ return lines[lines.length - 1];
489
+ }
490
+ function getWorktreePath(repoRoot, branchName) {
491
+ const repoName = basename(repoRoot);
492
+ const safeBranch = branchName.replace(/\//g, "-");
493
+ return join(dirname(repoRoot), `${repoName}--${safeBranch}`);
494
+ }
495
+ async function addWorktree(worktreePath, branchName, baseBranch) {
496
+ const git = getGit();
497
+ await git.fetch(["--prune"]);
498
+ const local = await git.branchLocal();
499
+ if (local.all.includes(branchName)) {
500
+ await git.raw(["worktree", "add", worktreePath, branchName]);
501
+ } else {
502
+ await git.raw(["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`]);
503
+ }
504
+ }
505
+ async function listWorktrees() {
506
+ const git = getGit();
507
+ const raw = await git.raw(["worktree", "list", "--porcelain"]);
508
+ const entries = [];
509
+ let current = {};
510
+ for (const line of raw.split("\n")) {
511
+ if (line.startsWith("worktree ")) {
512
+ if (current.path) entries.push(current);
513
+ current = { path: line.slice(9), isMain: entries.length === 0 };
514
+ } else if (line.startsWith("HEAD ")) {
515
+ current.head = line.slice(5);
516
+ } else if (line.startsWith("branch ")) {
517
+ current.branch = line.slice(7);
518
+ } else if (line === "detached") {
519
+ current.branch = "detached";
520
+ }
521
+ }
522
+ if (current.path) entries.push(current);
523
+ return entries;
524
+ }
525
+ async function removeWorktree(worktreePath, force = false) {
526
+ const git = getGit();
527
+ const args = ["worktree", "remove", ...force ? ["--force"] : [], worktreePath];
528
+ await git.raw(args);
529
+ await git.raw(["worktree", "prune"]);
530
+ }
531
+ async function findWorktreeForBranch(branchName) {
532
+ const trees = await listWorktrees();
533
+ return trees.find((t) => t.branch === `refs/heads/${branchName}`);
534
+ }
535
+
536
+ // src/commands/start.ts
537
+ async function pickIssueInteractively(config) {
538
+ const spinner = ora3("Loading your issues\u2026").start();
539
+ const issues = await getMyIssues(config.jiraAccountId, {
540
+ projectKey: config.jiraProjectKey
541
+ });
542
+ spinner.stop();
543
+ if (issues.length === 0) {
544
+ throw new Error("No open issues assigned to you in this project.");
545
+ }
546
+ const key = await select2({
547
+ message: "Select an issue to start:",
548
+ choices: issues.map((i) => ({
549
+ value: i.key,
550
+ name: `${chalk4.cyan(i.key.padEnd(12))} ${(i.title.length > 54 ? i.title.slice(0, 53) + "\u2026" : i.title).padEnd(55)} ${chalk4.dim(i.priority.name)}`
551
+ })),
552
+ pageSize: 12
553
+ });
554
+ return issues.find((i) => i.key === key);
555
+ }
556
+ async function startCommand(keyArg, options) {
557
+ const config = ensureConfig();
558
+ initJira({
559
+ baseUrl: config.jiraBaseUrl,
560
+ email: config.jiraEmail,
561
+ apiToken: config.jiraApiToken
562
+ });
563
+ let issue;
564
+ if (!keyArg) {
565
+ issue = await pickIssueInteractively(config);
566
+ } else {
567
+ const key = resolveIssueKey(keyArg, config.jiraProjectKey);
568
+ const spinner = ora3(`Fetching ${key}\u2026`).start();
569
+ issue = await getIssue(key);
570
+ spinner.succeed(`Found: ${chalk4.cyan(issue.key)} \u2014 ${issue.title}`);
571
+ }
572
+ printIssue(issue);
573
+ const defaultPrefix = smartBranchPrefix(issue.issuetype.name, config.branchPrefix);
574
+ const prefix = await select2({
575
+ message: "Branch type",
576
+ choices: BRANCH_PREFIXES.map((p) => ({
577
+ value: p,
578
+ name: p
579
+ })),
580
+ default: defaultPrefix
581
+ });
582
+ const suggestedBranch = buildBranchName(prefix, issue.key);
583
+ const branchName = await input2({
584
+ message: "Branch name (edit if needed)",
585
+ default: suggestedBranch,
586
+ validate: (v) => {
587
+ if (!v) return "Required";
588
+ if (/\s/.test(v)) return "No spaces allowed";
589
+ return true;
590
+ }
591
+ });
592
+ if (options.worktree) {
593
+ await runWorktreeMode(branchName, issue.key, config.baseBranch);
594
+ } else {
595
+ await runBranchMode(branchName, config.baseBranch);
596
+ }
597
+ try {
598
+ await updateIssueStatus(issue.key, "In Progress");
599
+ printSuccess(`Jira status \u2192 ${chalk4.green("In Progress")}`);
600
+ } catch (err) {
601
+ printWarn(`Could not update Jira status: ${String(err)}`);
602
+ }
603
+ console.log();
604
+ console.log(` Ticket: ${chalk4.dim(issue.url)}`);
605
+ console.log();
606
+ }
607
+ async function runBranchMode(branchName, baseBranch) {
608
+ const currentBranch = await getCurrentBranch();
609
+ if (await hasUncommittedChanges()) {
610
+ printWarn(`Uncommitted changes on "${currentBranch}".`);
611
+ const shouldStash = await confirm({
612
+ message: "Stash them before switching?",
613
+ default: true
614
+ });
615
+ if (shouldStash) {
616
+ await stashChanges(`ctm: stash before ${branchName}`);
617
+ printSuccess("Changes stashed.");
618
+ }
619
+ }
620
+ const spinner = ora3(`Creating branch ${chalk4.cyan(branchName)}\u2026`).start();
621
+ try {
622
+ await createAndCheckout(branchName, baseBranch);
623
+ spinner.succeed(`Switched to ${chalk4.cyan(branchName)}`);
624
+ } catch (err) {
625
+ spinner.fail(`Failed to create branch: ${String(err)}`);
626
+ process.exit(1);
627
+ }
628
+ console.log();
629
+ console.log(`${chalk4.green("\u2713")} Ready! Branch: ${chalk4.cyan(branchName)}`);
630
+ }
631
+ async function runWorktreeMode(branchName, issueKey, baseBranch) {
632
+ const repoRoot = await getRepoRoot();
633
+ const worktreePath = getWorktreePath(repoRoot, branchName);
634
+ const existing = await findWorktreeForBranch(branchName);
635
+ if (existing) {
636
+ printWarn(`Worktree already exists for "${branchName}"`);
637
+ console.log(` ${chalk4.cyan(existing.path)}`);
638
+ console.log();
639
+ console.log(` ${chalk4.dim("cd")} ${chalk4.cyan(existing.path)}`);
640
+ console.log();
641
+ process.exit(0);
642
+ }
643
+ const spinner = ora3(
644
+ `Creating worktree ${chalk4.cyan(branchName)} at ${chalk4.dim(worktreePath)}\u2026`
645
+ ).start();
646
+ try {
647
+ await addWorktree(worktreePath, branchName, baseBranch);
648
+ spinner.succeed(`Worktree created for ${chalk4.cyan(branchName)}`);
649
+ } catch (err) {
650
+ spinner.fail(`Failed to create worktree: ${String(err)}`);
651
+ process.exit(1);
652
+ }
653
+ console.log();
654
+ console.log(`${chalk4.green("\u2713")} Ready!`);
655
+ console.log(` ${chalk4.bold("Worktree:")} ${chalk4.cyan(worktreePath)}`);
656
+ console.log(` ${chalk4.bold("Branch:")} ${chalk4.cyan(branchName)}`);
657
+ console.log();
658
+ console.log(` ${chalk4.dim("Move into the worktree:")}`);
659
+ console.log(` ${chalk4.cyan(`cd ${worktreePath}`)}`);
660
+ void issueKey;
661
+ }
662
+
663
+ // src/commands/done.ts
664
+ import chalk5 from "chalk";
665
+ import ora4 from "ora";
666
+ import { confirm as confirm2 } from "@inquirer/prompts";
667
+ import { readFile } from "fs/promises";
668
+ import { join as join2 } from "path";
669
+ async function doneCommand(keyArg) {
670
+ const config = ensureConfig();
671
+ initJira({
672
+ baseUrl: config.jiraBaseUrl,
673
+ email: config.jiraEmail,
674
+ apiToken: config.jiraApiToken
675
+ });
676
+ const currentBranch = await getCurrentBranch();
677
+ let issueKey;
678
+ if (keyArg) {
679
+ issueKey = resolveIssueKey(keyArg, config.jiraProjectKey);
680
+ } else {
681
+ const extracted = extractIssueKeyFromBranch(currentBranch);
682
+ if (!extracted) {
683
+ printError(
684
+ `Cannot detect issue key from branch "${currentBranch}". Pass it explicitly: ctm done CTM-123`
685
+ );
686
+ process.exit(1);
687
+ }
688
+ issueKey = extracted;
689
+ }
690
+ const issueSpinner = ora4(`Fetching ${issueKey}\u2026`).start();
691
+ const issue = await getIssue(issueKey);
692
+ issueSpinner.stop();
693
+ printIssue(issue);
694
+ if (await hasUncommittedChanges()) {
695
+ printError("You have uncommitted changes. Commit or stash them first.");
696
+ process.exit(1);
697
+ }
698
+ if (!await hasNewCommits(config.baseBranch)) {
699
+ printWarn(`No new commits on "${currentBranch}" vs origin/${config.baseBranch}.`);
700
+ const proceed = await confirm2({ message: "Create PR anyway?", default: false });
701
+ if (!proceed) process.exit(0);
702
+ }
703
+ const go = await confirm2({
704
+ message: `Push "${currentBranch}" and create PR for "${issueKey}"?`,
705
+ default: true
706
+ });
707
+ if (!go) process.exit(0);
708
+ const pushSpinner = ora4(`Pushing ${chalk5.cyan(currentBranch)}\u2026`).start();
709
+ try {
710
+ await pushBranch(currentBranch);
711
+ pushSpinner.succeed(`Pushed ${chalk5.cyan(currentBranch)}`);
712
+ } catch (err) {
713
+ pushSpinner.fail(`Push failed: ${String(err)}`);
714
+ process.exit(1);
715
+ }
716
+ const prTitle = `${issue.key}: ${issue.title}`;
717
+ let prBody;
718
+ try {
719
+ const repoRoot = await getRepoRoot();
720
+ prBody = await readFile(join2(repoRoot, ".github", "pull_request_template.md"), "utf-8");
721
+ } catch {
722
+ prBody = `Jira: [${issue.key}](${issue.url})`;
723
+ }
724
+ const prSpinner = ora4("Creating PR\u2026").start();
725
+ let prUrl;
726
+ try {
727
+ prUrl = await createPR(prTitle, prBody);
728
+ prSpinner.succeed(`PR created: ${chalk5.cyan(prUrl)}`);
729
+ } catch (err) {
730
+ prSpinner.fail(`Failed to create PR: ${String(err)}`);
731
+ console.log(chalk5.dim(" Ensure `gh` is installed and authenticated: gh auth login"));
732
+ process.exit(1);
733
+ }
734
+ try {
735
+ await addComment(issue.key, `PR: ${prUrl}`);
736
+ printSuccess("PR URL added to Jira issue.");
737
+ } catch {
738
+ }
739
+ console.log();
740
+ console.log(`${chalk5.green("\u2713")} Done!`);
741
+ console.log(` PR: ${chalk5.cyan(prUrl)}`);
742
+ console.log(` Ticket: ${chalk5.dim(issue.url)}`);
743
+ console.log();
744
+ }
745
+
746
+ // src/commands/status.ts
747
+ import chalk6 from "chalk";
748
+ import ora5 from "ora";
749
+ async function statusCommand() {
750
+ const config = ensureConfig();
751
+ const currentBranch = await getCurrentBranch();
752
+ const issueKey = extractIssueKeyFromBranch(currentBranch);
753
+ console.log();
754
+ console.log(`${chalk6.cyan("Branch:")} ${chalk6.white(currentBranch)}`);
755
+ if (!issueKey) {
756
+ console.log(chalk6.dim(" No Jira ticket detected in branch name."));
757
+ console.log();
758
+ return;
759
+ }
760
+ initJira({
761
+ baseUrl: config.jiraBaseUrl,
762
+ email: config.jiraEmail,
763
+ apiToken: config.jiraApiToken
764
+ });
765
+ const spinner = ora5(`Fetching ${issueKey}\u2026`).start();
766
+ let issue;
767
+ try {
768
+ issue = await getIssue(issueKey);
769
+ spinner.stop();
770
+ } catch (err) {
771
+ spinner.fail(`Could not fetch issue: ${String(err)}`);
772
+ return;
773
+ }
774
+ const diff = await getDiffStat(config.baseBranch);
775
+ console.log(`${chalk6.cyan("Ticket:")} ${chalk6.bold(issue.key)} \u2014 ${issue.title}`);
776
+ console.log(`${chalk6.cyan("Status:")} ${issue.status.name}`);
777
+ console.log(`${chalk6.cyan("Priority:")} ${priorityLabel(issue.priority.name)}`);
778
+ console.log(`${chalk6.cyan("URL:")} ${chalk6.underline(chalk6.dim(issue.url))}`);
779
+ console.log();
780
+ if (diff.files > 0) {
781
+ console.log(
782
+ `${chalk6.cyan("Changes:")} ${diff.files} file(s) ${chalk6.green(`+${diff.insertions}`)} ${chalk6.red(`-${diff.deletions}`)}`
783
+ );
784
+ } else {
785
+ console.log(chalk6.dim("No changes vs base branch yet."));
786
+ }
787
+ console.log();
788
+ }
789
+
790
+ // src/commands/clean.ts
791
+ import chalk7 from "chalk";
792
+ import { confirm as confirm3 } from "@inquirer/prompts";
793
+ import ora6 from "ora";
794
+ async function cleanCommand(keyArg) {
795
+ const config = ensureConfig();
796
+ const currentBranch = await getCurrentBranch();
797
+ let branchToClean;
798
+ if (keyArg) {
799
+ const key = resolveIssueKey(keyArg, config.jiraProjectKey);
800
+ const branches = await getLocalBranches();
801
+ const match = branches.find((b) => b.includes(key));
802
+ if (!match) {
803
+ printError(`No local branch found matching issue key "${key}".`);
804
+ console.log(chalk7.dim(` Local branches: ${branches.slice(0, 5).join(", ")}`));
805
+ process.exit(1);
806
+ }
807
+ branchToClean = match;
808
+ } else {
809
+ const key = extractIssueKeyFromBranch(currentBranch);
810
+ if (!key) {
811
+ printError(
812
+ `Cannot detect issue key from branch "${currentBranch}". Pass it explicitly: ctm clean CTM-123`
813
+ );
814
+ process.exit(1);
815
+ }
816
+ branchToClean = currentBranch;
817
+ }
818
+ console.log();
819
+ console.log(`Branch to delete: ${chalk7.cyan(branchToClean)}`);
820
+ console.log();
821
+ const confirmed = await confirm3({
822
+ message: `Delete branch "${branchToClean}"?`,
823
+ default: false
824
+ });
825
+ if (!confirmed) process.exit(0);
826
+ if (branchToClean === currentBranch) {
827
+ const switchSpinner = ora6(`Switching to ${chalk7.cyan(config.baseBranch)}\u2026`).start();
828
+ try {
829
+ await checkoutBranch(config.baseBranch);
830
+ switchSpinner.succeed(`Switched to ${chalk7.cyan(config.baseBranch)}`);
831
+ } catch (err) {
832
+ switchSpinner.fail(`Cannot switch to base branch: ${String(err)}`);
833
+ process.exit(1);
834
+ }
835
+ }
836
+ const worktree = await findWorktreeForBranch(branchToClean);
837
+ if (worktree) {
838
+ printWarn(`Worktree found: ${chalk7.cyan(worktree.path)}`);
839
+ const removeWt = await confirm3({
840
+ message: `Remove worktree directory as well?`,
841
+ default: true
842
+ });
843
+ if (removeWt) {
844
+ const wtSpinner = ora6("Removing worktree\u2026").start();
845
+ try {
846
+ await removeWorktree(worktree.path);
847
+ wtSpinner.succeed(`Worktree removed: ${chalk7.cyan(worktree.path)}`);
848
+ } catch (err) {
849
+ wtSpinner.fail(`Could not remove worktree: ${String(err)}`);
850
+ printWarn("Continuing with branch deletion anyway.");
851
+ }
852
+ }
853
+ }
854
+ const localSpinner = ora6("Deleting local branch\u2026").start();
855
+ try {
856
+ await deleteLocalBranch(branchToClean);
857
+ localSpinner.succeed(`Deleted local branch ${chalk7.cyan(branchToClean)}`);
858
+ } catch (err) {
859
+ localSpinner.fail(`Failed: ${String(err)}`);
860
+ }
861
+ const deleteRemote = await confirm3({
862
+ message: `Also delete remote branch origin/${branchToClean}?`,
863
+ default: false
864
+ });
865
+ if (deleteRemote) {
866
+ const remoteSpinner = ora6("Deleting remote branch\u2026").start();
867
+ try {
868
+ await deleteRemoteBranch(branchToClean);
869
+ remoteSpinner.succeed("Remote branch deleted.");
870
+ } catch (err) {
871
+ remoteSpinner.fail(`Failed: ${String(err)}`);
872
+ }
873
+ }
874
+ console.log();
875
+ printSuccess("Cleanup complete.");
876
+ console.log();
877
+ }
878
+
879
+ // src/commands/help.ts
880
+ import chalk8 from "chalk";
881
+ var COMMANDS = [
882
+ {
883
+ name: "init",
884
+ usage: "ctm init",
885
+ description: "Jira \uC5F0\uACB0 \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4. \uCD5C\uCD08 1\uD68C \uC2E4\uD589\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
886
+ examples: [
887
+ { cmd: "ctm init", comment: "\uB300\uD654\uD615 \uC124\uC815 \uC2DC\uC791" }
888
+ ]
889
+ },
890
+ {
891
+ name: "issues",
892
+ aliases: ["ls"],
893
+ usage: "ctm ls [options]",
894
+ description: "\uB0B4 Jira \uC774\uC288 \uBAA9\uB85D\uC744 \uC0C1\uD0DC\uBCC4\uB85C \uCD9C\uB825\uD569\uB2C8\uB2E4.",
895
+ options: [
896
+ { flag: "-a, --all", desc: "\uC804\uCCB4 \uD504\uB85C\uC81D\uD2B8\uC758 \uC774\uC288 \uC870\uD68C" },
897
+ { flag: "-s, --status <status>", desc: "\uD2B9\uC815 \uC0C1\uD0DC\uB85C \uD544\uD130 (\uC608: 'In Progress')" },
898
+ { flag: "-p, --project <key>", desc: "\uD2B9\uC815 \uD504\uB85C\uC81D\uD2B8 \uD0A4\uB85C \uD544\uD130" }
899
+ ],
900
+ examples: [
901
+ { cmd: "ctm ls", comment: "\uAE30\uBCF8 \uD504\uB85C\uC81D\uD2B8 \uC774\uC288 \uBAA9\uB85D" },
902
+ { cmd: "ctm ls --all", comment: "\uC804\uCCB4 \uD504\uB85C\uC81D\uD2B8" },
903
+ { cmd: "ctm ls --status 'In Progress'", comment: "\uC9C4\uD589 \uC911\uC778 \uC774\uC288\uB9CC" },
904
+ { cmd: "ctm ls --project CGKR", comment: "\uD2B9\uC815 \uD504\uB85C\uC81D\uD2B8" }
905
+ ]
906
+ },
907
+ {
908
+ name: "start",
909
+ usage: "ctm start [key] [-w]",
910
+ description: "Jira \uC774\uC288\uC5D0 \uB300\uD55C \uBE0C\uB7F0\uCE58\uB97C \uC0DD\uC131\uD558\uACE0 \uCCB4\uD06C\uC544\uC6C3\uD569\uB2C8\uB2E4.\n Jira \uC0C1\uD0DC\uB97C 'In Progress'\uB85C \uBCC0\uACBD\uD569\uB2C8\uB2E4.",
911
+ options: [
912
+ { flag: "-w, --worktree", desc: "\uBCC4\uB3C4 \uB514\uB809\uD1A0\uB9AC(worktree)\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4 (\uBE0C\uB7F0\uCE58 \uC804\uD658 \uC5C6\uC774 \uBCD1\uB82C \uC791\uC5C5 \uAC00\uB2A5)" }
913
+ ],
914
+ examples: [
915
+ { cmd: "ctm start", comment: "\uC774\uC288 \uBAA9\uB85D\uC5D0\uC11C \uC778\uD130\uB799\uD2F0\uBE0C \uC120\uD0DD" },
916
+ { cmd: "ctm start CGKR-1423", comment: "\uD2B9\uC815 \uC774\uC288 \uD0A4\uB85C \uBC14\uB85C \uC2DC\uC791" },
917
+ { cmd: "ctm start 1423 --worktree", comment: "worktree \uBAA8\uB4DC\uB85C \uBC1C\uB3D9" }
918
+ ]
919
+ },
920
+ {
921
+ name: "done",
922
+ usage: "ctm done [key]",
923
+ description: "\uD604\uC7AC \uBE0C\uB79C\uCE58\uB97C push\uD558\uACE0 GitHub PR\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4.\n .github/pull_request_template.md \uAC00 \uC788\uC73C\uBA74 \uC790\uB3D9\uC73C\uB85C PR body\uC5D0 \uC801\uC6A9\uB429\uB2C8\uB2E4.\n gh CLI\uAC00 \uC124\uCE58\xB7\uC778\uC99D\uB418\uC5B4 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.",
924
+ examples: [
925
+ { cmd: "ctm done", comment: "\uD604\uC7AC \uBE0C\uB79C\uCE58 \uAE30\uC900 \uC790\uB3D9 \uAC10\uC9C0" },
926
+ { cmd: "ctm done CGKR-1423", comment: "\uC774\uC288 \uD0A4 \uBA85\uC2DC" }
927
+ ]
928
+ },
929
+ {
930
+ name: "status",
931
+ aliases: ["st"],
932
+ usage: "ctm st",
933
+ description: "\uD604\uC7AC \uBE0C\uB79C\uCE58\uC640 \uC5F0\uACB0\uB41C Jira \uC774\uC288 \uC815\uBCF4, \uBCC0\uACBD \uD604\uD669\uC744 \uCD9C\uB825\uD569\uB2C8\uB2E4.",
934
+ examples: [
935
+ { cmd: "ctm st", comment: "\uD604\uC7AC \uC0C1\uD0DC \uD655\uC778" }
936
+ ]
937
+ },
938
+ {
939
+ name: "clean",
940
+ usage: "ctm clean [key]",
941
+ description: "Jira \uC774\uC288\uC5D0 \uC5F0\uACB0\uB41C \uB85C\uCEC8 \uBE0C\uB7F0\uCE58\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4.\n \uC6D0\uACA9 \uBE0C\uB7F0\uCE58\uB3C4 \uD568\uAED8 \uC0AD\uC81C\uD560\uC9C0 \uBB3B\uC5B4\uBD05\uB2C8\uB2E4.\n worktree\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uC815\uB9AC\uD560\uC9C0 \uBB3B\uC5B4\uBF45\uB2C8\uB2E4.",
942
+ examples: [
943
+ { cmd: "ctm clean", comment: "\uD604\uC7AC \uBE0C\uB7F0\uCE58 \uC0AD\uC81C" },
944
+ { cmd: "ctm clean CGKR-1423", comment: "\uD2B9\uC815 \uC774\uC288 \uBE0C\uB7F0\uCE58 \uC0AD\uC81C" },
945
+ { cmd: "ctm clean 1423", comment: "\uC22B\uC790\uB9CC \uC785\uB825 \uAC00\uB2A5" }
946
+ ]
947
+ },
948
+ {
949
+ name: "worktree",
950
+ aliases: ["wt"],
951
+ usage: "ctm wt [rm <key>]",
952
+ description: "worktree \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uAC70\uB098 \uC81C\uAC70\uD569\uB2C8\uB2E4.\n ctm start --worktree\uB85C \uC0DD\uC131\uB41C worktree\uB97C \uAD00\uB9AC\uD569\uB2C8\uB2E4.",
953
+ options: [
954
+ { flag: "-b, --branch", desc: "worktree \uC81C\uAC70 \uC2DC \uBE0C\uB7F0\uCE58\uB3C4 \uD568\uAED8 \uC0AD\uC81C" },
955
+ { flag: "-f, --force", desc: "\uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC5B4\uB3C4 \uAC15\uC81C \uC81C\uAC70" }
956
+ ],
957
+ examples: [
958
+ { cmd: "ctm wt", comment: "worktree \uBAA9\uB85D" },
959
+ { cmd: "ctm wt rm CGKR-1423", comment: "worktree \uC81C\uAC70 (\uBE0C\uB7F0\uCE58 \uC720\uC9C0)" },
960
+ { cmd: "ctm wt rm 1423 --branch", comment: "worktree + \uBE0C\uB7F0\uCE58 \uD568\uAED8 \uC0AD\uC81C" }
961
+ ]
962
+ }
963
+ ];
964
+ var DIM_RULE = chalk8.dim("\u2500".repeat(56));
965
+ function renderHeader() {
966
+ console.log();
967
+ console.log(` ${chalk8.bold.cyan("ctm")} ${chalk8.dim("\u2014 Colo Ticket Manager")}`);
968
+ console.log(` ${chalk8.dim("Jira \uD2F0\uCF13 \uAE30\uBC18 Git \uBE0C\uB79C\uCE58 \uAD00\uB9AC CLI")}`);
969
+ console.log();
970
+ }
971
+ function renderUsageLine() {
972
+ console.log(` ${chalk8.bold("\uC0AC\uC6A9\uBC95")} ${chalk8.cyan("ctm")} ${chalk8.dim("<command>")} ${chalk8.dim("[options]")}`);
973
+ console.log();
974
+ }
975
+ function renderCommandRow(cmd) {
976
+ const aliases = cmd.aliases?.length ? chalk8.dim(` (${cmd.aliases.join(", ")})`) : "";
977
+ const name = chalk8.cyan(cmd.name.padEnd(10)) + aliases;
978
+ const shortDesc = cmd.description.split("\n")[0];
979
+ console.log(` ${name} ${shortDesc}`);
980
+ }
981
+ function renderCommandDetail(cmd) {
982
+ const aliases = cmd.aliases?.length ? " " + chalk8.dim(`alias: ${cmd.aliases.join(", ")}`) : "";
983
+ console.log();
984
+ console.log(DIM_RULE);
985
+ console.log(` ${chalk8.bold.cyan(cmd.name)}${aliases}`);
986
+ console.log();
987
+ console.log(` ${chalk8.bold("\uC0AC\uC6A9\uBC95")} ${chalk8.cyan(cmd.usage)}`);
988
+ console.log();
989
+ for (const line of cmd.description.split("\n")) {
990
+ console.log(` ${line}`);
991
+ }
992
+ console.log();
993
+ if (cmd.options?.length) {
994
+ console.log(` ${chalk8.bold("\uC635\uC158")}`);
995
+ for (const opt of cmd.options) {
996
+ console.log(` ${chalk8.green(opt.flag.padEnd(26))} ${opt.desc}`);
997
+ }
998
+ console.log();
999
+ }
1000
+ console.log(` ${chalk8.bold("\uC608\uC2DC")}`);
1001
+ for (const ex of cmd.examples) {
1002
+ console.log(` ${chalk8.cyan(ex.cmd.padEnd(38))} ${chalk8.dim(ex.comment)}`);
1003
+ }
1004
+ }
1005
+ function renderWorkflow() {
1006
+ console.log();
1007
+ console.log(` ${chalk8.bold("\uC77C\uBC18\uC801\uC778 \uC6CC\uD06C\uD50C\uB85C\uC6B0")}`);
1008
+ console.log();
1009
+ const steps = [
1010
+ ["ctm init", "\uCD5C\uCD08 1\uD68C \u2014 Jira \uC5F0\uACB0 \uC124\uC815"],
1011
+ ["ctm ls", "\uB0B4 \uC774\uC288 \uBAA9\uB85D \uD655\uC778"],
1012
+ ["ctm start CGKR-123", "\uBE0C\uB79C\uCE58 \uC0DD\uC131 + Jira 'In Progress'"],
1013
+ ["ctm st", "\uD604\uC7AC \uC0C1\uD0DC \uD655\uC778"],
1014
+ ["ctm done", "push + GitHub PR \uC0DD\uC131"],
1015
+ ["ctm clean", "\uBA38\uC9C0 \uD6C4 \uBE0C\uB79C\uCE58 \uC815\uB9AC"]
1016
+ ];
1017
+ for (const [cmd, comment] of steps) {
1018
+ console.log(` ${chalk8.cyan(cmd.padEnd(24))} ${chalk8.dim(comment)}`);
1019
+ }
1020
+ console.log();
1021
+ }
1022
+ function helpCommand(commandName) {
1023
+ if (commandName) {
1024
+ const target = COMMANDS.find(
1025
+ (c) => c.name === commandName || c.aliases?.includes(commandName)
1026
+ );
1027
+ if (!target) {
1028
+ console.error(
1029
+ `${chalk8.red("\u2717")} \uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${chalk8.bold(commandName)}
1030
+ `
1031
+ );
1032
+ console.log(` \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBA85\uB839\uC5B4: ${COMMANDS.map((c) => chalk8.cyan(c.name)).join(", ")}`);
1033
+ console.log();
1034
+ process.exit(1);
1035
+ }
1036
+ renderHeader();
1037
+ renderCommandDetail(target);
1038
+ console.log();
1039
+ console.log(DIM_RULE);
1040
+ console.log();
1041
+ return;
1042
+ }
1043
+ renderHeader();
1044
+ renderUsageLine();
1045
+ console.log(` ${chalk8.bold("\uBA85\uB839\uC5B4")}`);
1046
+ console.log();
1047
+ for (const cmd of COMMANDS) {
1048
+ renderCommandRow(cmd);
1049
+ }
1050
+ renderWorkflow();
1051
+ console.log(
1052
+ ` ${chalk8.dim(`\uC790\uC138\uD55C \uB3C4\uC6C0\uB9D0: ${chalk8.cyan("ctm help <command>")}`)}`
1053
+ );
1054
+ console.log();
1055
+ }
1056
+
1057
+ // src/commands/worktree.ts
1058
+ import chalk9 from "chalk";
1059
+ import { confirm as confirm4 } from "@inquirer/prompts";
1060
+ import ora7 from "ora";
1061
+ async function worktreeListCommand() {
1062
+ let trees;
1063
+ try {
1064
+ trees = await listWorktrees();
1065
+ } catch (err) {
1066
+ printError(`Failed to list worktrees: ${String(err)}`);
1067
+ process.exit(1);
1068
+ }
1069
+ console.log();
1070
+ if (trees.length === 0) {
1071
+ console.log(chalk9.dim(" No worktrees found.\n"));
1072
+ return;
1073
+ }
1074
+ console.log(
1075
+ ` ${"PATH".padEnd(52)} ${"BRANCH".padEnd(30)} HEAD`
1076
+ );
1077
+ console.log(chalk9.dim(` ${"\u2500".repeat(52)} ${"\u2500".repeat(30)} ${"\u2500".repeat(7)}`));
1078
+ for (const t of trees) {
1079
+ const pathLabel = t.isMain ? `${t.path} ${chalk9.dim("(main)")}` : t.path;
1080
+ const branchDisplay = t.branch.replace("refs/heads/", "");
1081
+ const shortHead = t.head.slice(0, 7);
1082
+ const pathCol = chalk9.cyan(pathLabel.length > 50 ? "\u2026" + pathLabel.slice(-49) : pathLabel);
1083
+ const branchCol = t.isMain ? chalk9.dim(branchDisplay) : chalk9.white(branchDisplay);
1084
+ console.log(` ${pathCol.padEnd(52)} ${branchCol.padEnd(30)} ${chalk9.dim(shortHead)}`);
1085
+ }
1086
+ console.log();
1087
+ console.log(chalk9.dim(` ${trees.length} worktree(s) \xB7 ctm wt rm [key] to remove`));
1088
+ console.log();
1089
+ }
1090
+ async function worktreeRemoveCommand(keyArg, options) {
1091
+ const config = ensureConfig();
1092
+ let branchName;
1093
+ if (keyArg) {
1094
+ const issueKey = resolveIssueKey(keyArg, config.jiraProjectKey);
1095
+ const branches = await getLocalBranches();
1096
+ const match = branches.find((b) => b.includes(issueKey));
1097
+ if (!match) {
1098
+ printError(`No local branch found matching issue key "${issueKey}".`);
1099
+ process.exit(1);
1100
+ }
1101
+ branchName = match;
1102
+ } else {
1103
+ printError("Please specify an issue key: ctm wt rm <key>");
1104
+ console.log(chalk9.dim(" Example: ctm wt rm CGKR-1423 or ctm wt rm 1423"));
1105
+ process.exit(1);
1106
+ }
1107
+ const tree = await findWorktreeForBranch(branchName);
1108
+ if (!tree) {
1109
+ const issueKey = extractIssueKeyFromBranch(branchName) ?? branchName;
1110
+ const all = await listWorktrees();
1111
+ const fuzzy = all.find(
1112
+ (t) => !t.isMain && t.branch.includes(issueKey)
1113
+ );
1114
+ if (!fuzzy) {
1115
+ printWarn(`No worktree found for branch "${branchName}".`);
1116
+ console.log(chalk9.dim(" Use ctm wt to see existing worktrees."));
1117
+ process.exit(0);
1118
+ }
1119
+ return doRemove(fuzzy, branchName, options);
1120
+ }
1121
+ return doRemove(tree, branchName, options);
1122
+ }
1123
+ async function doRemove(tree, branchName, options) {
1124
+ const branchDisplay = tree.branch.replace("refs/heads/", "");
1125
+ console.log();
1126
+ console.log(` ${chalk9.bold("Worktree:")} ${chalk9.cyan(tree.path)}`);
1127
+ console.log(` ${chalk9.bold("Branch:")} ${chalk9.cyan(branchDisplay)}`);
1128
+ console.log();
1129
+ const confirmed = await confirm4({
1130
+ message: `Remove worktree at "${tree.path}"?`,
1131
+ default: false
1132
+ });
1133
+ if (!confirmed) process.exit(0);
1134
+ const spinner = ora7("Removing worktree\u2026").start();
1135
+ try {
1136
+ await removeWorktree(tree.path, options.force);
1137
+ spinner.succeed(`Worktree removed: ${chalk9.cyan(tree.path)}`);
1138
+ } catch (err) {
1139
+ spinner.fail(`Failed to remove worktree: ${String(err)}`);
1140
+ console.log(chalk9.dim(" Try: ctm wt rm <key> --force"));
1141
+ process.exit(1);
1142
+ }
1143
+ const deleteBranch = options.branch ?? await confirm4({
1144
+ message: `Also delete branch "${branchDisplay}"?`,
1145
+ default: false
1146
+ });
1147
+ if (deleteBranch) {
1148
+ try {
1149
+ await deleteLocalBranch(branchName);
1150
+ printSuccess(`Local branch "${branchDisplay}" deleted.`);
1151
+ } catch (err) {
1152
+ printWarn(`Could not delete local branch: ${String(err)}`);
1153
+ }
1154
+ const deleteRemote = await confirm4({
1155
+ message: `Also delete remote branch origin/${branchDisplay}?`,
1156
+ default: false
1157
+ });
1158
+ if (deleteRemote) {
1159
+ try {
1160
+ await deleteRemoteBranch(branchName);
1161
+ printSuccess("Remote branch deleted.");
1162
+ } catch (err) {
1163
+ printWarn(`Could not delete remote branch: ${String(err)}`);
1164
+ }
1165
+ }
1166
+ }
1167
+ console.log();
1168
+ printSuccess("Done.");
1169
+ console.log();
1170
+ }
1171
+
1172
+ // src/index.ts
1173
+ var program = new Command();
1174
+ program.name("ctm").description("Colo Ticket Manager \u2014 Jira-driven branch management").version("0.1.0");
1175
+ program.command("init").description("Configure CTM with Jira credentials and project settings").action(initCommand);
1176
+ program.command("issues").alias("ls").description("List Jira issues assigned to you").option("-a, --all", "Show issues from all projects").option("-s, --status <status>", "Filter by status").option("-p, --project <project>", "Filter by project key").action(issuesCommand);
1177
+ program.command("start [key]").description("Create and checkout a branch for a Jira issue (e.g. CTM-123 or just 123)").option("-w, --worktree", "Create a linked worktree instead of switching branches").action(startCommand);
1178
+ program.command("done [key]").description("Push branch, create PR, and mark the Jira issue as Done").action(doneCommand);
1179
+ program.command("status").alias("st").description("Show current branch status and linked Jira issue").action(statusCommand);
1180
+ program.command("clean [key]").description("Delete local (and optionally remote) branch for a Jira issue").action(cleanCommand);
1181
+ program.command("help [command]").description("\uBA85\uB839\uC5B4 \uB3C4\uC6C0\uB9D0 \uCD9C\uB825").action(helpCommand);
1182
+ var wt = program.command("worktree").alias("wt").description("Manage git worktrees created by ctm start --worktree");
1183
+ wt.command("list", { isDefault: true }).description("List all worktrees for the current repo").action(worktreeListCommand);
1184
+ wt.command("rm [key]").description("Remove a worktree (optionally also delete the branch)").option("-b, --branch", "Also delete the git branch").option("-f, --force", "Force remove even with untracked changes").action(worktreeRemoveCommand);
1185
+ program.parseAsync().catch((err) => {
1186
+ if (err instanceof Error && err.name === "ExitPromptError") {
1187
+ process.exit(0);
1188
+ }
1189
+ console.error(err instanceof Error ? err.message : String(err));
1190
+ process.exit(1);
1191
+ });