tdecollab 0.2.3 → 0.3.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.
@@ -0,0 +1,2754 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ImageDownloader
4
+ } from "../chunk-JBDK5WP3.js";
5
+ import {
6
+ ConfluenceContentApi,
7
+ ConfluenceSearchApi,
8
+ ConfluenceSpaceApi,
9
+ GitlabMergeRequestApi,
10
+ GitlabPipelineApi,
11
+ JiraIssueApi,
12
+ JiraSearchApi,
13
+ MarkdownToStorageConverter,
14
+ StorageToMarkdownConverter,
15
+ createConfluenceClient,
16
+ createGitlabClient,
17
+ createJiraClient,
18
+ loadConfluenceConfig,
19
+ loadGitlabConfig,
20
+ loadJiraConfig
21
+ } from "../chunk-6AFNYE7N.js";
22
+ import "../chunk-IFYMZLQI.js";
23
+
24
+ // tools/tui/index.tsx
25
+ import { render } from "ink";
26
+
27
+ // tools/tui/App.tsx
28
+ import { Box as Box16, useApp, useStdout as useStdout2 } from "ink";
29
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState8 } from "react";
30
+
31
+ // tools/tui/command-def.ts
32
+ var COMMANDS = {
33
+ "confluence:page:get": {
34
+ key: "confluence:page:get",
35
+ label: "confluence page get",
36
+ description: "Confluence \uD398\uC774\uC9C0\uB97C \uC870\uD68C\uD558\uACE0 Markdown\uC73C\uB85C \uBCC0\uD658\uD558\uC5EC \uCD9C\uB825\uD558\uAC70\uB098 \uD30C\uC77C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. \uC774\uBBF8\uC9C0\uB97C \uD568\uAED8 \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
37
+ synopsis: "tdecollab confluence page get <pageId> [options]",
38
+ svc: "confluence",
39
+ fields: [
40
+ { key: "pageId", label: "pageId", type: "text", required: true, hint: "\uD398\uC774\uC9C0 ID (\uC22B\uC790)", prefix: "#" },
41
+ { key: "output", label: "--output", type: "text", hint: "\uC800\uC7A5\uD560 Markdown \uD30C\uC77C \uACBD\uB85C", prefix: "\u{1F4C4}", pathType: "file" },
42
+ { key: "downloadImages", label: "-d --download-images", type: "bool", defaultValue: false },
43
+ { key: "imageDir", label: "--image-dir", type: "text", defaultValue: "images", hint: "\uC774\uBBF8\uC9C0 \uB514\uB809\uD1A0\uB9AC \uC774\uB984 (output \uD30C\uC77C \uC704\uCE58 \uD558\uC704\uC5D0 \uC0DD\uC131\uB428)", prefix: "\u{1F4C1}" },
44
+ { key: "quiet", label: "-q --quiet", type: "bool", defaultValue: false },
45
+ { key: "raw", label: "-r --raw", type: "bool", defaultValue: false }
46
+ ]
47
+ },
48
+ "confluence:page:create": {
49
+ key: "confluence:page:create",
50
+ label: "confluence page create",
51
+ description: "Markdown \uD30C\uC77C \uB610\uB294 \uD14D\uC2A4\uD2B8\uB85C Confluence \uD398\uC774\uC9C0\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.",
52
+ synopsis: "tdecollab confluence page create -s <space> -t <title> [options]",
53
+ svc: "confluence",
54
+ fields: [
55
+ { key: "space", label: "--space", type: "text", required: true, hint: "Confluence \uC2A4\uD398\uC774\uC2A4 \uD0A4 (\uC608: TDE)", prefix: "\u{1F511}" },
56
+ { key: "title", label: "--title", type: "text", required: true, hint: "\uD398\uC774\uC9C0 \uC81C\uBAA9", prefix: "\u270E" },
57
+ { key: "file", label: "--file", type: "text", hint: "Markdown \uD30C\uC77C \uACBD\uB85C", prefix: "\u{1F4C4}", pathType: "file" },
58
+ { key: "parent", label: "--parent", type: "text", hint: "\uBD80\uBAA8 \uD398\uC774\uC9C0 ID", prefix: "#" }
59
+ ]
60
+ },
61
+ "confluence:page:update": {
62
+ key: "confluence:page:update",
63
+ label: "confluence page update",
64
+ description: "\uAE30\uC874 Confluence \uD398\uC774\uC9C0\uB97C \uC5C5\uB370\uC774\uD2B8\uD569\uB2C8\uB2E4.",
65
+ synopsis: "tdecollab confluence page update <pageId> [options]",
66
+ svc: "confluence",
67
+ fields: [
68
+ { key: "pageId", label: "pageId", type: "text", required: true, hint: "\uC5C5\uB370\uC774\uD2B8\uD560 \uD398\uC774\uC9C0 ID", prefix: "#" },
69
+ { key: "title", label: "--title", type: "text", hint: "\uC0C8 \uC81C\uBAA9 (\uC0DD\uB7B5 \uC2DC \uAE30\uC874 \uC81C\uBAA9 \uC720\uC9C0)" },
70
+ { key: "file", label: "--file", type: "text", hint: "Markdown \uD30C\uC77C \uACBD\uB85C", prefix: "\u{1F4C4}", pathType: "file" }
71
+ ]
72
+ },
73
+ "confluence:space:list": {
74
+ key: "confluence:space:list",
75
+ label: "confluence space list",
76
+ description: "Confluence \uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
77
+ synopsis: "tdecollab confluence space list [options]",
78
+ svc: "confluence",
79
+ fields: [
80
+ { key: "limit", label: "--limit", type: "text", defaultValue: "50", hint: "\uCD5C\uB300 \uC870\uD68C \uC218" }
81
+ ]
82
+ },
83
+ "confluence:search": {
84
+ key: "confluence:search",
85
+ label: "confluence search",
86
+ description: "CQL(Confluence Query Language)\uB85C \uD398\uC774\uC9C0\uB97C \uAC80\uC0C9\uD569\uB2C8\uB2E4.",
87
+ synopsis: "tdecollab confluence search <cql> [options]",
88
+ svc: "confluence",
89
+ fields: [
90
+ { key: "cql", label: "cql", type: "text", required: true, hint: 'CQL \uAC80\uC0C9 \uCFFC\uB9AC (\uC608: title ~ "\uAC00\uC774\uB4DC" AND space = TDE)', prefix: "cql\u203A" },
91
+ { key: "limit", label: "--limit", type: "text", defaultValue: "10", hint: "\uCD5C\uB300 \uACB0\uACFC \uC218" }
92
+ ]
93
+ },
94
+ "jira:issue:get": {
95
+ key: "jira:issue:get",
96
+ label: "jira issue get",
97
+ description: "JIRA \uC774\uC288\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
98
+ synopsis: "tdecollab jira issue get <issueId>",
99
+ svc: "jira",
100
+ fields: [
101
+ { key: "issueId", label: "issueId", type: "text", required: true, hint: "\uC774\uC288 \uD0A4 (\uC608: PROJ-1234)", prefix: "\u{1F3AB}" }
102
+ ]
103
+ },
104
+ "jira:issue:create": {
105
+ key: "jira:issue:create",
106
+ label: "jira issue create",
107
+ description: "JIRA \uC774\uC288\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.",
108
+ synopsis: "tdecollab jira issue create -p <project> -s <summary> [options]",
109
+ svc: "jira",
110
+ fields: [
111
+ { key: "project", label: "--project (-p)", type: "text", required: true, hint: "\uD504\uB85C\uC81D\uD2B8 \uD0A4 (\uC608: PROJ)" },
112
+ { key: "summary", label: "--summary (-s)", type: "text", required: true, hint: "\uC774\uC288 \uC81C\uBAA9" },
113
+ { key: "type", label: "--type (-t)", type: "select", defaultValue: "Task", options: ["Task", "Bug", "Story", "Epic", "Sub-task"] },
114
+ { key: "assignee", label: "--assignee (-a)", type: "text", hint: "\uB2F4\uB2F9\uC790 \uC0AC\uC6A9\uC790\uBA85" },
115
+ { key: "labels", label: "--labels (-l)", type: "text", hint: "\uB808\uC774\uBE14 (\uC27C\uD45C \uAD6C\uBD84)" }
116
+ ]
117
+ },
118
+ "jira:issue:transition": {
119
+ key: "jira:issue:transition",
120
+ label: "jira issue transition",
121
+ description: "JIRA \uC774\uC288 \uC0C1\uD0DC\uB97C \uC804\uD658\uD569\uB2C8\uB2E4.",
122
+ synopsis: "tdecollab jira issue transition <issueId> -t <transitionId>",
123
+ svc: "jira",
124
+ fields: [
125
+ { key: "issueId", label: "issueId", type: "text", required: true, hint: "\uC774\uC288 \uD0A4", prefix: "\u{1F3AB}" },
126
+ { key: "transitionId", label: "--transition (-t)", type: "text", required: true, hint: "Transition ID" }
127
+ ]
128
+ },
129
+ "jira:search": {
130
+ key: "jira:search",
131
+ label: "jira search",
132
+ description: "JQL\uB85C JIRA \uC774\uC288\uB97C \uAC80\uC0C9\uD569\uB2C8\uB2E4.",
133
+ synopsis: "tdecollab jira search <jql> [options]",
134
+ svc: "jira",
135
+ fields: [
136
+ { key: "jql", label: "jql", type: "text", required: true, hint: "JQL \uCFFC\uB9AC (\uC608: assignee = currentUser())", prefix: "jql\u203A" },
137
+ { key: "limit", label: "--limit", type: "text", defaultValue: "20", hint: "\uCD5C\uB300 \uACB0\uACFC \uC218" }
138
+ ]
139
+ },
140
+ "gitlab:mr:list": {
141
+ key: "gitlab:mr:list",
142
+ label: "gitlab mr list",
143
+ description: "GitLab \uD504\uB85C\uC81D\uD2B8\uC758 Merge Request \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
144
+ synopsis: "tdecollab gitlab mr list <projectId> [options]",
145
+ svc: "gitlab",
146
+ fields: [
147
+ { key: "projectId", label: "projectId", type: "text", required: true, hint: "\uD504\uB85C\uC81D\uD2B8 ID (\uC22B\uC790)", prefix: "#" },
148
+ { key: "state", label: "--state (-s)", type: "select", defaultValue: "opened", options: ["opened", "closed", "merged", "all"] }
149
+ ]
150
+ },
151
+ "gitlab:mr:get": {
152
+ key: "gitlab:mr:get",
153
+ label: "gitlab mr get",
154
+ description: "GitLab Merge Request \uC0C1\uC138 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
155
+ synopsis: "tdecollab gitlab mr get <projectId> <mrId>",
156
+ svc: "gitlab",
157
+ fields: [
158
+ { key: "projectId", label: "projectId", type: "text", required: true, hint: "\uD504\uB85C\uC81D\uD2B8 ID", prefix: "#" },
159
+ { key: "mrId", label: "mrId", type: "text", required: true, hint: "MR \uBC88\uD638", prefix: "!" }
160
+ ]
161
+ },
162
+ "gitlab:mr:create": {
163
+ key: "gitlab:mr:create",
164
+ label: "gitlab mr create",
165
+ description: "GitLab Merge Request\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4.",
166
+ synopsis: "tdecollab gitlab mr create <projectId> --source <branch> [options]",
167
+ svc: "gitlab",
168
+ fields: [
169
+ { key: "projectId", label: "projectId", type: "text", required: true, hint: "\uD504\uB85C\uC81D\uD2B8 ID", prefix: "#" },
170
+ { key: "sourceBranch", label: "--source", type: "text", required: true, hint: "\uC18C\uC2A4 \uBE0C\uB79C\uCE58\uBA85" },
171
+ { key: "targetBranch", label: "--target", type: "text", defaultValue: "main", hint: "\uD0C0\uAC9F \uBE0C\uB79C\uCE58\uBA85" },
172
+ { key: "title", label: "--title", type: "text", hint: "MR \uC81C\uBAA9 (\uAE30\uBCF8: \uBE0C\uB79C\uCE58\uBA85)" }
173
+ ]
174
+ },
175
+ "gitlab:pipeline:get": {
176
+ key: "gitlab:pipeline:get",
177
+ label: "gitlab pipeline get",
178
+ description: "GitLab \uD30C\uC774\uD504\uB77C\uC778 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
179
+ synopsis: "tdecollab gitlab pipeline get <projectId> <pipelineId> [options]",
180
+ svc: "gitlab",
181
+ fields: [
182
+ { key: "projectId", label: "projectId", type: "text", required: true, hint: "\uD504\uB85C\uC81D\uD2B8 ID", prefix: "#" },
183
+ { key: "pipelineId", label: "pipelineId", type: "text", required: true, hint: "\uD30C\uC774\uD504\uB77C\uC778 ID", prefix: "#" },
184
+ { key: "jobs", label: "--jobs", type: "bool", defaultValue: false }
185
+ ]
186
+ }
187
+ };
188
+ function applyValueTransform(commandKey, values) {
189
+ if (commandKey === "confluence:page:get") {
190
+ const result = { ...values };
191
+ const imageDir = String(result["imageDir"] ?? "").trim();
192
+ const output = String(result["output"] ?? "").trim();
193
+ if (imageDir && output) {
194
+ if (!imageDir.includes("/")) {
195
+ const lastSlash = output.lastIndexOf("/");
196
+ const outputDir = lastSlash >= 0 ? output.substring(0, lastSlash) : ".";
197
+ result["imageDir"] = `${outputDir}/${imageDir}`.replace(/\/+/g, "/");
198
+ }
199
+ }
200
+ return result;
201
+ }
202
+ return values;
203
+ }
204
+ function buildPreview(def, values) {
205
+ const transformed = applyValueTransform(def.key, values);
206
+ const parts = ["tdecollab", ...def.key.split(":")];
207
+ for (const field of def.fields) {
208
+ const val = transformed[field.key] ?? field.defaultValue;
209
+ if (!val && val !== false) continue;
210
+ if (field.type === "bool") {
211
+ if (val === true) parts.push(`-${field.key.charAt(0)}`);
212
+ } else if (field.type === "text") {
213
+ const isPositional = !field.label.startsWith("-");
214
+ if (isPositional) {
215
+ if (val) parts.push(String(val));
216
+ } else {
217
+ const flag = field.label.split(" ")[0];
218
+ if (val) parts.push(flag, String(val));
219
+ }
220
+ }
221
+ }
222
+ return parts.join(" ");
223
+ }
224
+
225
+ // tools/tui/menu-def.ts
226
+ var MENU = [
227
+ {
228
+ key: "confluence",
229
+ label: "Confluence",
230
+ icon: "\u25C6",
231
+ badge: "9",
232
+ badgeColor: "#7DD3FC",
233
+ children: [
234
+ {
235
+ key: "confluence/page",
236
+ label: "page",
237
+ icon: "\u25B8",
238
+ children: [
239
+ { key: "confluence/page/get", label: "get", icon: "\xB7", commandKey: "confluence:page:get" },
240
+ { key: "confluence/page/create", label: "create", icon: "\xB7", commandKey: "confluence:page:create" },
241
+ { key: "confluence/page/update", label: "update", icon: "\xB7", commandKey: "confluence:page:update" }
242
+ ]
243
+ },
244
+ {
245
+ key: "confluence/space",
246
+ label: "space",
247
+ icon: "\u25B8",
248
+ children: [
249
+ { key: "confluence/space/list", label: "list", icon: "\xB7", commandKey: "confluence:space:list" }
250
+ ]
251
+ },
252
+ {
253
+ key: "confluence/search",
254
+ label: "search",
255
+ icon: "\u25B8",
256
+ commandKey: "confluence:search"
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ key: "jira",
262
+ label: "JIRA",
263
+ icon: "\u25C6",
264
+ badge: "7",
265
+ badgeColor: "#86EFAC",
266
+ children: [
267
+ {
268
+ key: "jira/issue",
269
+ label: "issue",
270
+ icon: "\u25B8",
271
+ children: [
272
+ { key: "jira/issue/get", label: "get", icon: "\xB7", commandKey: "jira:issue:get" },
273
+ { key: "jira/issue/create", label: "create", icon: "\xB7", commandKey: "jira:issue:create" },
274
+ { key: "jira/issue/transition", label: "transition", icon: "\xB7", commandKey: "jira:issue:transition" }
275
+ ]
276
+ },
277
+ {
278
+ key: "jira/search",
279
+ label: "search",
280
+ icon: "\u25B8",
281
+ commandKey: "jira:search"
282
+ }
283
+ ]
284
+ },
285
+ {
286
+ key: "gitlab",
287
+ label: "GitLab",
288
+ icon: "\u25C6",
289
+ badge: "7",
290
+ badgeColor: "#FBBF24",
291
+ children: [
292
+ {
293
+ key: "gitlab/mr",
294
+ label: "mr",
295
+ icon: "\u25B8",
296
+ children: [
297
+ { key: "gitlab/mr/list", label: "list", icon: "\xB7", commandKey: "gitlab:mr:list" },
298
+ { key: "gitlab/mr/get", label: "get", icon: "\xB7", commandKey: "gitlab:mr:get" },
299
+ { key: "gitlab/mr/create", label: "create", icon: "\xB7", commandKey: "gitlab:mr:create" }
300
+ ]
301
+ },
302
+ {
303
+ key: "gitlab/pipeline",
304
+ label: "pipeline",
305
+ icon: "\u25B8",
306
+ children: [
307
+ { key: "gitlab/pipeline/get", label: "get", icon: "\xB7", commandKey: "gitlab:pipeline:get" }
308
+ ]
309
+ }
310
+ ]
311
+ }
312
+ ];
313
+ var META_ITEMS = [
314
+ { key: "history", label: "History", icon: "\u23F1" },
315
+ { key: "settings", label: "Settings", icon: "\u2699", dim: true }
316
+ ];
317
+ function flattenMenu(items, expanded, depth = 0, parentKey) {
318
+ const result = [];
319
+ for (const item of items) {
320
+ result.push({ item, depth, parentKey });
321
+ if (item.children && expanded.includes(item.key)) {
322
+ result.push(...flattenMenu(item.children, expanded, depth + 1, item.key));
323
+ }
324
+ }
325
+ return result;
326
+ }
327
+ var DEFAULT_EXPANDED = ["confluence", "confluence/page"];
328
+ function pathFromCommandKey(commandKey) {
329
+ return commandKey.split(":");
330
+ }
331
+ function getParentMenuKeys(commandKey) {
332
+ const menuKey = commandKey.replace(/:/g, "/");
333
+ const parts = menuKey.split("/");
334
+ const parents = [];
335
+ for (let i = 1; i < parts.length; i++) {
336
+ parents.push(parts.slice(0, i).join("/"));
337
+ }
338
+ return parents;
339
+ }
340
+
341
+ // tools/tui/screens/ErrorScreen.tsx
342
+ import { Box as Box7, Text as Text6, useInput } from "ink";
343
+
344
+ // tools/tui/components/AppShell.tsx
345
+ import { Box } from "ink";
346
+
347
+ // tools/tui/theme.ts
348
+ var T = {
349
+ bg: "#08090C",
350
+ termBg: "#0E1014",
351
+ panelBg: "#13161C",
352
+ panelHi: "#181C24",
353
+ border: "#2A2F3A",
354
+ borderDim: "#1E222B",
355
+ borderHi: "#3D4452",
356
+ fg: "#E6E8EC",
357
+ fgDim: "#9CA3AF",
358
+ fgMute: "#6B7280",
359
+ fgFaint: "#4B5563",
360
+ pink: "#FF7BAC",
361
+ cyan: "#7DD3FC",
362
+ mint: "#86EFAC",
363
+ amber: "#FBBF24",
364
+ violet: "#C4B5FD",
365
+ red: "#F87171",
366
+ blue: "#60A5FA"
367
+ };
368
+ var SVC_COLOR = {
369
+ confluence: T.cyan,
370
+ jira: T.mint,
371
+ gitlab: T.amber,
372
+ cf: T.cyan,
373
+ jr: T.mint,
374
+ gl: T.amber
375
+ };
376
+ var DEFAULT_ACCENT = T.pink;
377
+
378
+ // tools/tui/components/AppShell.tsx
379
+ import { jsx, jsxs } from "react/jsx-runtime";
380
+ function AppShell({
381
+ header,
382
+ menu,
383
+ body,
384
+ log,
385
+ footer,
386
+ menuWidth = 32
387
+ }) {
388
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, height: "100%", width: "100%", children: [
389
+ /* @__PURE__ */ jsx(
390
+ Box,
391
+ {
392
+ borderStyle: "single",
393
+ borderColor: T.borderDim,
394
+ paddingX: 1,
395
+ flexShrink: 0,
396
+ children: header
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsxs(Box, { flexGrow: 1, gap: 1, minHeight: 0, children: [
400
+ /* @__PURE__ */ jsx(Box, { width: menuWidth, flexShrink: 0, children: menu }),
401
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, minWidth: 0, children: body })
402
+ ] }),
403
+ /* @__PURE__ */ jsx(Box, { height: 10, flexShrink: 0, children: log }),
404
+ footer && /* @__PURE__ */ jsx(
405
+ Box,
406
+ {
407
+ borderStyle: "single",
408
+ borderColor: T.borderDim,
409
+ flexShrink: 0,
410
+ children: footer
411
+ }
412
+ )
413
+ ] });
414
+ }
415
+
416
+ // tools/tui/components/HeaderBar.tsx
417
+ import { Box as Box2, Text } from "ink";
418
+ import React from "react";
419
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
420
+ function HeaderBar({ crumbs = [], status, accent = T.pink }) {
421
+ return /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, gap: 1, children: [
422
+ /* @__PURE__ */ jsx2(Text, { color: accent, bold: true, children: "tdecollab" }),
423
+ /* @__PURE__ */ jsx2(Text, { color: T.fgFaint, children: "v0.2.3" }),
424
+ /* @__PURE__ */ jsx2(Text, { color: T.fgFaint, children: "\u2502" }),
425
+ crumbs.map((c, i) => /* @__PURE__ */ jsxs2(React.Fragment, { children: [
426
+ i > 0 && /* @__PURE__ */ jsx2(Text, { color: T.fgFaint, children: "\u203A" }),
427
+ /* @__PURE__ */ jsx2(Text, { color: i === crumbs.length - 1 ? T.fg : T.fgDim, children: c })
428
+ ] }, i)),
429
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
430
+ status && /* @__PURE__ */ jsx2(Box2, { children: status })
431
+ ] });
432
+ }
433
+
434
+ // tools/tui/components/Keymap.tsx
435
+ import { Box as Box3, Text as Text2 } from "ink";
436
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
437
+ function Keymap({ keys, accent = T.pink, hidden = false }) {
438
+ if (hidden) return null;
439
+ return /* @__PURE__ */ jsx3(Box3, { paddingX: 1, gap: 2, flexWrap: "wrap", children: keys.map((k, i) => /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
440
+ /* @__PURE__ */ jsxs3(Text2, { backgroundColor: T.panelHi, color: accent, children: [
441
+ " ",
442
+ k.key,
443
+ " "
444
+ ] }),
445
+ /* @__PURE__ */ jsx3(Text2, { color: T.fgDim, children: k.label })
446
+ ] }, i)) });
447
+ }
448
+
449
+ // tools/tui/components/LogPane.tsx
450
+ import { Box as Box4, Text as Text3 } from "ink";
451
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
452
+ var LEVEL_COLOR = {
453
+ info: T.fgDim,
454
+ ok: T.mint,
455
+ warn: T.amber,
456
+ err: T.red,
457
+ run: T.cyan,
458
+ dim: T.fgMute
459
+ };
460
+ var LEVEL_LABEL = {
461
+ info: "INFO",
462
+ ok: " OK ",
463
+ warn: "WARN",
464
+ err: " ERR",
465
+ run: "RUN ",
466
+ dim: "... "
467
+ };
468
+ function LogPane({ lines, accent = T.pink, title = "Log", maxLines = 8 }) {
469
+ const visible = lines.slice(-maxLines);
470
+ return /* @__PURE__ */ jsxs4(
471
+ Box4,
472
+ {
473
+ borderStyle: "round",
474
+ borderColor: T.borderDim,
475
+ flexDirection: "column",
476
+ flexGrow: 1,
477
+ children: [
478
+ /* @__PURE__ */ jsxs4(
479
+ Box4,
480
+ {
481
+ borderStyle: "single",
482
+ borderBottom: true,
483
+ borderTop: false,
484
+ borderLeft: false,
485
+ borderRight: false,
486
+ borderColor: T.borderDim,
487
+ paddingX: 1,
488
+ children: [
489
+ /* @__PURE__ */ jsx4(Text3, { color: T.fgFaint, bold: true, children: title.toUpperCase() }),
490
+ /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
491
+ /* @__PURE__ */ jsxs4(Text3, { color: T.fgFaint, children: [
492
+ lines.length,
493
+ " lines"
494
+ ] })
495
+ ]
496
+ }
497
+ ),
498
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: visible.map((l, i) => /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
499
+ /* @__PURE__ */ jsx4(Text3, { color: T.fgFaint, children: l.ts }),
500
+ /* @__PURE__ */ jsx4(Text3, { color: LEVEL_COLOR[l.level] ?? T.fgDim, bold: true, children: LEVEL_LABEL[l.level] ?? l.level.toUpperCase() }),
501
+ /* @__PURE__ */ jsx4(
502
+ Text3,
503
+ {
504
+ color: l.level === "err" ? T.red : l.level === "warn" ? T.amber : T.fg,
505
+ children: l.text
506
+ }
507
+ )
508
+ ] }, i)) })
509
+ ]
510
+ }
511
+ );
512
+ }
513
+
514
+ // tools/tui/components/MenuTree.tsx
515
+ import { Box as Box5, Text as Text4 } from "ink";
516
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
517
+ function MenuTree({
518
+ items,
519
+ cursor,
520
+ accent = T.pink,
521
+ maxVisible = 30,
522
+ scrollOffset = 0
523
+ }) {
524
+ const visible = items.slice(scrollOffset, scrollOffset + maxVisible);
525
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: visible.map((flat, visIdx) => {
526
+ const idx = visIdx + scrollOffset;
527
+ const { item, depth } = flat;
528
+ const isActive = idx === cursor;
529
+ const indent = depth * 2;
530
+ const isSeparator = item.label.startsWith("\u2500");
531
+ if (isSeparator) {
532
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text4, { color: T.fgFaint, children: item.label }) }, item.key);
533
+ }
534
+ return /* @__PURE__ */ jsxs5(Box5, { gap: 0, children: [
535
+ /* @__PURE__ */ jsx5(Text4, { children: " ".repeat(indent) }),
536
+ /* @__PURE__ */ jsx5(Text4, { color: isActive ? accent : "transparent", children: isActive ? "\u25B6" : " " }),
537
+ item.icon && /* @__PURE__ */ jsxs5(Text4, { color: isActive ? accent : item.dim ? T.fgFaint : T.fgDim, children: [
538
+ item.icon,
539
+ " "
540
+ ] }),
541
+ /* @__PURE__ */ jsx5(
542
+ Text4,
543
+ {
544
+ color: isActive ? accent : item.dim ? T.fgMute : T.fg,
545
+ bold: isActive,
546
+ children: item.label
547
+ }
548
+ ),
549
+ item.badge && /* @__PURE__ */ jsxs5(Text4, { color: item.badgeColor ?? T.fgDim, children: [
550
+ " ",
551
+ item.badge
552
+ ] })
553
+ ] }, item.key);
554
+ }) });
555
+ }
556
+
557
+ // tools/tui/components/Panel.tsx
558
+ import { Box as Box6, Text as Text5 } from "ink";
559
+ import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
560
+ function Panel({
561
+ title,
562
+ badge,
563
+ accent = T.pink,
564
+ focused = false,
565
+ children,
566
+ headerRight,
567
+ width,
568
+ flexGrow
569
+ }) {
570
+ const borderColor = focused ? accent : T.border;
571
+ return /* @__PURE__ */ jsxs6(
572
+ Box6,
573
+ {
574
+ borderStyle: "round",
575
+ borderColor,
576
+ flexDirection: "column",
577
+ width,
578
+ flexGrow: flexGrow ?? (width ? 0 : 1),
579
+ children: [
580
+ (title || badge) && /* @__PURE__ */ jsxs6(
581
+ Box6,
582
+ {
583
+ borderStyle: "single",
584
+ borderBottom: true,
585
+ borderTop: false,
586
+ borderLeft: false,
587
+ borderRight: false,
588
+ borderColor: T.borderDim,
589
+ paddingX: 1,
590
+ paddingY: 0,
591
+ children: [
592
+ badge && /* @__PURE__ */ jsxs6(
593
+ Text5,
594
+ {
595
+ backgroundColor: focused ? accent : T.panelHi,
596
+ color: focused ? T.bg : T.fgDim,
597
+ bold: true,
598
+ children: [
599
+ " ",
600
+ badge,
601
+ " "
602
+ ]
603
+ }
604
+ ),
605
+ title && /* @__PURE__ */ jsxs6(Text5, { color: focused ? accent : T.fgDim, children: [
606
+ badge ? " " : "",
607
+ title
608
+ ] }),
609
+ headerRight && /* @__PURE__ */ jsxs6(Fragment, { children: [
610
+ /* @__PURE__ */ jsx6(Text5, { children: " " }),
611
+ headerRight
612
+ ] })
613
+ ]
614
+ }
615
+ ),
616
+ /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", flexGrow: 1, paddingX: 1, children })
617
+ ]
618
+ }
619
+ );
620
+ }
621
+
622
+ // tools/tui/screens/ErrorScreen.tsx
623
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
624
+ function ErrorScreen({ state, onBack, onRetry, accent = DEFAULT_ACCENT }) {
625
+ const crumbs = pathFromCommandKey(state.commandKey);
626
+ const errors = Object.entries(state.formErrors);
627
+ useInput((input, key) => {
628
+ if (key.escape) onBack();
629
+ else if (input === "r" || input === "R") onRetry();
630
+ });
631
+ const menuItems = [
632
+ ...flattenMenu(MENU, state.expandedKeys),
633
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
634
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
635
+ ];
636
+ const menuCursor = menuItems.findIndex((f) => f.item.commandKey === state.commandKey);
637
+ return /* @__PURE__ */ jsx7(
638
+ AppShell,
639
+ {
640
+ header: /* @__PURE__ */ jsx7(
641
+ HeaderBar,
642
+ {
643
+ accent,
644
+ crumbs,
645
+ status: /* @__PURE__ */ jsxs7(Box7, { gap: 1, children: [
646
+ /* @__PURE__ */ jsx7(Text6, { color: T.red, children: "\u25CF" }),
647
+ /* @__PURE__ */ jsxs7(Text6, { color: T.red, children: [
648
+ errors.length,
649
+ " errors"
650
+ ] })
651
+ ] })
652
+ }
653
+ ),
654
+ menu: /* @__PURE__ */ jsx7(Panel, { title: "Commands", badge: "MENU", accent, children: /* @__PURE__ */ jsx7(MenuTree, { items: menuItems, cursor: menuCursor >= 0 ? menuCursor : 0, accent }) }),
655
+ body: /* @__PURE__ */ jsx7(Panel, { title: crumbs.join(" "), badge: "ERROR", accent: T.red, focused: true, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, paddingY: 1, children: [
656
+ /* @__PURE__ */ jsxs7(
657
+ Box7,
658
+ {
659
+ borderStyle: "single",
660
+ borderColor: T.red,
661
+ paddingX: 1,
662
+ flexDirection: "column",
663
+ gap: 0,
664
+ children: [
665
+ /* @__PURE__ */ jsx7(Text6, { color: T.red, bold: true, children: "\u2715 \uC720\uD6A8\uC131 \uAC80\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (E_VALIDATION)" }),
666
+ /* @__PURE__ */ jsxs7(Text6, { color: T.fgDim, children: [
667
+ errors.length,
668
+ "\uAC1C \uC624\uB958\uB97C \uC218\uC815\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."
669
+ ] })
670
+ ]
671
+ }
672
+ ),
673
+ errors.map(([key, msg]) => /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", gap: 0, children: /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
674
+ /* @__PURE__ */ jsx7(Text6, { color: T.red, bold: true, children: key.padEnd(20) }),
675
+ /* @__PURE__ */ jsx7(Text6, { color: T.red, children: msg })
676
+ ] }) }, key)),
677
+ state.logs.filter((l) => l.level === "err" || l.level === "warn").map((l, i) => /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
678
+ /* @__PURE__ */ jsx7(Text6, { color: l.level === "err" ? T.red : T.amber, children: l.level === "err" ? "\u2715" : "\u26A0" }),
679
+ /* @__PURE__ */ jsx7(Text6, { color: l.level === "err" ? T.red : T.amber, children: l.text })
680
+ ] }, i)),
681
+ /* @__PURE__ */ jsxs7(Box7, { gap: 3, marginTop: 1, children: [
682
+ /* @__PURE__ */ jsx7(Box7, { gap: 1, children: /* @__PURE__ */ jsx7(Text6, { backgroundColor: T.panelHi, color: T.fg, children: " \u21B5 Back to form " }) }),
683
+ /* @__PURE__ */ jsx7(Box7, { gap: 1, children: /* @__PURE__ */ jsx7(Text6, { backgroundColor: T.panelHi, color: T.amber, children: " R Retry " }) }),
684
+ /* @__PURE__ */ jsx7(Box7, { gap: 1, children: /* @__PURE__ */ jsx7(Text6, { backgroundColor: T.panelHi, color: T.fgDim, children: " Esc Menu " }) })
685
+ ] })
686
+ ] }) }),
687
+ log: /* @__PURE__ */ jsx7(LogPane, { lines: state.logs, accent, title: "Diagnostics" }),
688
+ footer: /* @__PURE__ */ jsx7(Keymap, { accent, keys: [
689
+ { key: "\u21B5", label: "back to form" },
690
+ { key: "R", label: "retry" },
691
+ { key: "Esc", label: "menu" }
692
+ ] })
693
+ }
694
+ );
695
+ }
696
+
697
+ // tools/tui/screens/FormScreen.tsx
698
+ import { Box as Box10, Text as Text9, useInput as useInput3 } from "ink";
699
+ import { useState as useState2 } from "react";
700
+
701
+ // tools/tui/components/FilePicker.tsx
702
+ import { Box as Box8, Text as Text7, useInput as useInput2 } from "ink";
703
+ import fs from "fs";
704
+ import path from "path";
705
+ import os from "os";
706
+ import { useMemo, useState } from "react";
707
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
708
+ function resolveStartDir(initialPath) {
709
+ if (!initialPath) return process.cwd();
710
+ const expanded = initialPath.replace(/^~/, os.homedir());
711
+ const abs = path.isAbsolute(expanded) ? expanded : path.resolve(process.cwd(), expanded);
712
+ try {
713
+ const stat = fs.statSync(abs);
714
+ return stat.isDirectory() ? abs : path.dirname(abs);
715
+ } catch {
716
+ let dir = path.dirname(abs);
717
+ while (dir && dir !== "/" && !fs.existsSync(dir)) {
718
+ dir = path.dirname(dir);
719
+ }
720
+ return dir || process.cwd();
721
+ }
722
+ }
723
+ function FilePicker({
724
+ initialPath,
725
+ pathType,
726
+ onSelect,
727
+ onCancel,
728
+ accent = DEFAULT_ACCENT,
729
+ maxVisible = 14
730
+ }) {
731
+ const [currentDir, setCurrentDir] = useState(() => resolveStartDir(initialPath));
732
+ const [cursor, setCursor] = useState(0);
733
+ const [scroll, setScroll] = useState(0);
734
+ const entries = useMemo(() => {
735
+ try {
736
+ const items = fs.readdirSync(currentDir, { withFileTypes: true }).filter((d) => !d.name.startsWith("."));
737
+ const dirs = items.filter((i) => i.isDirectory()).map((i) => ({ name: i.name, isDir: true }));
738
+ const files = pathType === "file" ? items.filter((i) => i.isFile()).map((i) => ({ name: i.name, isDir: false })) : [];
739
+ return [
740
+ { name: ".", isDir: true, isCurrent: true },
741
+ { name: "..", isDir: true },
742
+ ...dirs.sort((a, b) => a.name.localeCompare(b.name)),
743
+ ...files.sort((a, b) => a.name.localeCompare(b.name))
744
+ ];
745
+ } catch {
746
+ return [
747
+ { name: ".", isDir: true, isCurrent: true },
748
+ { name: "..", isDir: true }
749
+ ];
750
+ }
751
+ }, [currentDir, pathType]);
752
+ function move(delta) {
753
+ const next = Math.max(0, Math.min(entries.length - 1, cursor + delta));
754
+ setCursor(next);
755
+ if (next < scroll) setScroll(next);
756
+ else if (next >= scroll + maxVisible) setScroll(next - maxVisible + 1);
757
+ }
758
+ function selectEntry(entry) {
759
+ if (entry.isCurrent) {
760
+ onSelect(toRelativeIfPossible(currentDir) + path.sep);
761
+ return;
762
+ }
763
+ const fullPath = entry.name === ".." ? path.dirname(currentDir) : path.join(currentDir, entry.name);
764
+ if (entry.isDir) {
765
+ setCurrentDir(fullPath);
766
+ setCursor(0);
767
+ setScroll(0);
768
+ } else {
769
+ onSelect(toRelativeIfPossible(fullPath));
770
+ }
771
+ }
772
+ function selectCurrentDir() {
773
+ onSelect(toRelativeIfPossible(currentDir) + path.sep);
774
+ }
775
+ useInput2((input, key) => {
776
+ if (key.escape) {
777
+ onCancel();
778
+ } else if (key.upArrow) {
779
+ move(-1);
780
+ } else if (key.downArrow) {
781
+ move(1);
782
+ } else if (key.pageUp) {
783
+ move(-maxVisible);
784
+ } else if (key.pageDown) {
785
+ move(maxVisible);
786
+ } else if (key.return) {
787
+ const entry = entries[cursor];
788
+ if (entry) selectEntry(entry);
789
+ } else if (input === " " || key.tab) {
790
+ if (pathType === "dir") {
791
+ selectCurrentDir();
792
+ } else {
793
+ const entry = entries[cursor];
794
+ if (entry && !entry.isDir) selectEntry(entry);
795
+ }
796
+ }
797
+ });
798
+ const visible = entries.slice(scroll, scroll + maxVisible);
799
+ const displayPath = currentDir.replace(os.homedir(), "~");
800
+ return /* @__PURE__ */ jsxs8(
801
+ Box8,
802
+ {
803
+ borderStyle: "round",
804
+ borderColor: accent,
805
+ flexDirection: "column",
806
+ paddingX: 1,
807
+ children: [
808
+ /* @__PURE__ */ jsxs8(Box8, { children: [
809
+ /* @__PURE__ */ jsx8(Text7, { color: accent, bold: true, children: pathType === "file" ? "\u{1F4C4} \uD30C\uC77C \uC120\uD0DD" : "\u{1F4C1} \uB514\uB809\uD1A0\uB9AC \uC120\uD0DD" }),
810
+ /* @__PURE__ */ jsxs8(Text7, { color: T.fgFaint, children: [
811
+ " ",
812
+ displayPath
813
+ ] })
814
+ ] }),
815
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
816
+ visible.map((entry, vi) => {
817
+ const idx = vi + scroll;
818
+ const isActive = idx === cursor;
819
+ return /* @__PURE__ */ jsxs8(Box8, { children: [
820
+ /* @__PURE__ */ jsx8(Text7, { color: isActive ? accent : "transparent", children: isActive ? "\u25B6 " : " " }),
821
+ /* @__PURE__ */ jsxs8(
822
+ Text7,
823
+ {
824
+ color: isActive ? accent : entry.isCurrent ? T.amber : entry.isDir ? T.cyan : T.fg,
825
+ bold: isActive,
826
+ children: [
827
+ entry.isCurrent ? "\u2713 " : entry.isDir ? "\u{1F4C1} " : "\u{1F4C4} ",
828
+ entry.name,
829
+ entry.isCurrent ? " (\uD604\uC7AC \uB514\uB809\uD1A0\uB9AC \uC120\uD0DD)" : entry.isDir && entry.name !== ".." ? "/" : ""
830
+ ]
831
+ }
832
+ )
833
+ ] }, `${entry.name}-${idx}`);
834
+ }),
835
+ entries.length > maxVisible && /* @__PURE__ */ jsxs8(Text7, { color: T.fgFaint, children: [
836
+ scroll + 1,
837
+ "\u2013",
838
+ Math.min(scroll + maxVisible, entries.length),
839
+ " / ",
840
+ entries.length
841
+ ] })
842
+ ] }),
843
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text7, { color: T.fgFaint, children: '\u2191\u2193 navigate \xB7 \u21B5 on "." select current \xB7 \u21B5 on dir = open \xB7 \u21B5 on file = select \xB7 Esc cancel' }) })
844
+ ]
845
+ }
846
+ );
847
+ }
848
+ function toRelativeIfPossible(absPath) {
849
+ const cwd = process.cwd();
850
+ const rel = path.relative(cwd, absPath);
851
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
852
+ return "./" + rel;
853
+ }
854
+ return absPath;
855
+ }
856
+
857
+ // tools/tui/components/FormField.tsx
858
+ import { Box as Box9, Text as Text8 } from "ink";
859
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
860
+ function FormField({ field, value, focused, error, accent = T.pink }) {
861
+ const labelText = field.label.padEnd(28);
862
+ if (field.type === "bool") {
863
+ const checked = value === true;
864
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 0, children: [
865
+ /* @__PURE__ */ jsxs9(Box9, { gap: 1, children: [
866
+ /* @__PURE__ */ jsx9(Text8, { color: focused ? accent : T.fgDim, bold: focused, children: labelText }),
867
+ /* @__PURE__ */ jsx9(Text8, { color: checked ? accent : T.fgFaint, bold: focused, children: checked ? "\u25A3" : "\u25A2" }),
868
+ /* @__PURE__ */ jsx9(Text8, { color: focused ? T.fg : T.fgDim, children: checked ? "enabled" : "disabled" })
869
+ ] }),
870
+ focused && /* @__PURE__ */ jsx9(Box9, { marginLeft: 30, children: /* @__PURE__ */ jsx9(Text8, { color: T.fgFaint, children: "Space\uB85C \uD1A0\uAE00" }) })
871
+ ] });
872
+ }
873
+ if (field.type === "select") {
874
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 0, children: [
875
+ /* @__PURE__ */ jsxs9(Box9, { gap: 1, children: [
876
+ /* @__PURE__ */ jsx9(Text8, { color: focused ? accent : T.fgDim, bold: focused, children: labelText }),
877
+ /* @__PURE__ */ jsxs9(
878
+ Box9,
879
+ {
880
+ borderStyle: "single",
881
+ borderColor: focused ? accent : T.border,
882
+ paddingX: 1,
883
+ children: [
884
+ /* @__PURE__ */ jsx9(Text8, { color: T.fg, children: String(value) }),
885
+ /* @__PURE__ */ jsx9(Text8, { color: T.fgFaint, children: " \u25BC" })
886
+ ]
887
+ }
888
+ )
889
+ ] }),
890
+ focused && /* @__PURE__ */ jsx9(Box9, { marginLeft: 30, children: /* @__PURE__ */ jsxs9(Text8, { color: T.fgFaint, children: [
891
+ "Space\uB85C \uB2E4\uC74C \uC635\uC158 (",
892
+ field.options?.join(" \xB7 "),
893
+ ")"
894
+ ] }) })
895
+ ] });
896
+ }
897
+ const borderColor = error ? T.red : focused ? accent : T.border;
898
+ const valStr = String(value ?? "");
899
+ const isPath = !!field.pathType;
900
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 0, children: [
901
+ /* @__PURE__ */ jsxs9(Box9, { gap: 1, children: [
902
+ /* @__PURE__ */ jsx9(Text8, { color: focused ? accent : T.fgDim, bold: focused, children: labelText }),
903
+ field.required && /* @__PURE__ */ jsx9(Text8, { color: T.amber, children: "*" }),
904
+ /* @__PURE__ */ jsxs9(
905
+ Box9,
906
+ {
907
+ borderStyle: "single",
908
+ borderColor,
909
+ paddingX: 1,
910
+ flexGrow: 1,
911
+ children: [
912
+ field.prefix && /* @__PURE__ */ jsxs9(Text8, { color: T.fgFaint, children: [
913
+ field.prefix,
914
+ " "
915
+ ] }),
916
+ valStr ? /* @__PURE__ */ jsx9(Text8, { color: T.fg, children: valStr }) : /* @__PURE__ */ jsx9(Text8, { color: T.fgFaint, children: field.hint ?? "" }),
917
+ focused && /* @__PURE__ */ jsx9(Text8, { backgroundColor: accent, color: T.bg, children: " " })
918
+ ]
919
+ }
920
+ )
921
+ ] }),
922
+ focused && /* @__PURE__ */ jsx9(Box9, { marginLeft: 30, children: error ? /* @__PURE__ */ jsx9(Text8, { color: T.red, children: error }) : isPath ? /* @__PURE__ */ jsxs9(Text8, { color: T.fgFaint, children: [
923
+ /* @__PURE__ */ jsx9(Text8, { color: T.amber, children: "\u21B5" }),
924
+ " ",
925
+ field.pathType === "file" ? "\uD30C\uC77C" : "\uB514\uB809\uD1A0\uB9AC",
926
+ " \uC120\uD0DD\uCC3D \uC5F4\uAE30",
927
+ field.hint ? ` \xB7 ${field.hint}` : ""
928
+ ] }) : field.hint ? /* @__PURE__ */ jsx9(Text8, { color: T.fgFaint, children: field.hint }) : null })
929
+ ] });
930
+ }
931
+
932
+ // tools/tui/url-parser.ts
933
+ function parseConfluenceUrl(input) {
934
+ const url = input.trim();
935
+ if (!url) return {};
936
+ const result = {};
937
+ const spacesMatch = url.match(/\/spaces\/([^/]+)\/pages\/(\d+)/);
938
+ if (spacesMatch) {
939
+ result.space = decodeURIComponent(spacesMatch[1]);
940
+ result.pageId = spacesMatch[2];
941
+ return result;
942
+ }
943
+ const pageIdQuery = url.match(/[?&]pageId=(\d+)/);
944
+ if (pageIdQuery) {
945
+ result.pageId = pageIdQuery[1];
946
+ }
947
+ const spaceQuery = url.match(/[?&]spaceKey=([^&]+)/);
948
+ if (spaceQuery) {
949
+ result.space = decodeURIComponent(spaceQuery[1]);
950
+ }
951
+ if (!result.space) {
952
+ const displayMatch = url.match(/\/display\/([^/?#]+)/);
953
+ if (displayMatch) result.space = decodeURIComponent(displayMatch[1]);
954
+ }
955
+ return result;
956
+ }
957
+ function applyUrlFill(commandKey, parsed, current) {
958
+ const result = { ...current };
959
+ if (commandKey === "confluence:page:get" || commandKey === "confluence:page:update") {
960
+ if (parsed.space) result["space"] = parsed.space;
961
+ if (parsed.pageId) result["pageId"] = parsed.pageId;
962
+ } else if (commandKey === "confluence:page:create") {
963
+ if (parsed.space) result["space"] = parsed.space;
964
+ if (parsed.pageId) result["parent"] = parsed.pageId;
965
+ }
966
+ return result;
967
+ }
968
+ function supportsUrlFill(commandKey) {
969
+ return [
970
+ "confluence:page:get",
971
+ "confluence:page:update",
972
+ "confluence:page:create"
973
+ ].includes(commandKey);
974
+ }
975
+ function getUrlFillTargets(commandKey) {
976
+ if (commandKey === "confluence:page:create") return ["space", "parent"];
977
+ if (commandKey === "confluence:page:get" || commandKey === "confluence:page:update") return ["space", "pageId"];
978
+ return [];
979
+ }
980
+
981
+ // tools/tui/screens/FormScreen.tsx
982
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
983
+ var URL_FILL_COLOR = T.blue;
984
+ function FormScreen({ state, onRun, onBack, onSavePreset, accent = DEFAULT_ACCENT }) {
985
+ const def = COMMANDS[state.commandKey];
986
+ const [values, setValues] = useState2(state.formValues);
987
+ const [errors, setErrors] = useState2(state.formErrors);
988
+ const [focusIdx, setFocusIdx] = useState2(state.focusedField);
989
+ const [pickerOpen, setPickerOpen] = useState2(false);
990
+ const [urlValue, setUrlValue] = useState2("");
991
+ if (!def) return /* @__PURE__ */ jsxs10(Text9, { color: T.red, children: [
992
+ "\uCEE4\uB9E8\uB4DC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ",
993
+ state.commandKey
994
+ ] });
995
+ const fields = def.fields;
996
+ const hasUrlFill = supportsUrlFill(state.commandKey);
997
+ const fillTargets = hasUrlFill ? getUrlFillTargets(state.commandKey) : [];
998
+ const onUrlField = hasUrlFill && focusIdx === -1;
999
+ const currentField = onUrlField ? void 0 : fields[focusIdx];
1000
+ const preview = buildPreview(def, values);
1001
+ function updateUrl(next) {
1002
+ setUrlValue(next);
1003
+ const parsed = parseConfluenceUrl(next);
1004
+ if (parsed.space || parsed.pageId) {
1005
+ setValues((v) => applyUrlFill(state.commandKey, parsed, v));
1006
+ setErrors({});
1007
+ }
1008
+ }
1009
+ function nextField() {
1010
+ setFocusIdx((i) => {
1011
+ const min = hasUrlFill ? -1 : 0;
1012
+ const next = i + 1;
1013
+ return next > fields.length - 1 ? min : next;
1014
+ });
1015
+ }
1016
+ function prevField() {
1017
+ setFocusIdx((i) => {
1018
+ const min = hasUrlFill ? -1 : 0;
1019
+ const prev = i - 1;
1020
+ return prev < min ? fields.length - 1 : prev;
1021
+ });
1022
+ }
1023
+ function validate() {
1024
+ const errs = {};
1025
+ for (const f of fields) {
1026
+ if (f.required && !values[f.key]) {
1027
+ errs[f.key] = `\uD544\uC218 \uC635\uC158\uC785\uB2C8\uB2E4. ${f.hint ?? ""}`;
1028
+ }
1029
+ }
1030
+ return errs;
1031
+ }
1032
+ function tryRun() {
1033
+ const errs = validate();
1034
+ if (Object.keys(errs).length > 0) {
1035
+ setErrors(errs);
1036
+ const firstErrIdx = fields.findIndex((f) => errs[f.key]);
1037
+ if (firstErrIdx >= 0) setFocusIdx(firstErrIdx);
1038
+ } else {
1039
+ setErrors({});
1040
+ onRun(values);
1041
+ }
1042
+ }
1043
+ useInput3((input, key) => {
1044
+ if (pickerOpen) return;
1045
+ if (key.ctrl && input === "r") {
1046
+ tryRun();
1047
+ return;
1048
+ }
1049
+ if (key.ctrl && input === "s") {
1050
+ onSavePreset(values);
1051
+ return;
1052
+ }
1053
+ if (key.escape) {
1054
+ onBack();
1055
+ return;
1056
+ }
1057
+ if (key.tab && key.shift) {
1058
+ prevField();
1059
+ return;
1060
+ }
1061
+ if (key.tab) {
1062
+ nextField();
1063
+ return;
1064
+ }
1065
+ if (key.upArrow) {
1066
+ prevField();
1067
+ return;
1068
+ }
1069
+ if (key.downArrow) {
1070
+ nextField();
1071
+ return;
1072
+ }
1073
+ if (onUrlField) {
1074
+ if (key.backspace || key.delete) {
1075
+ updateUrl(urlValue.slice(0, -1));
1076
+ return;
1077
+ }
1078
+ if (key.return) return;
1079
+ if (input && !key.ctrl && !key.meta) {
1080
+ updateUrl(urlValue + input);
1081
+ }
1082
+ return;
1083
+ }
1084
+ if (!currentField) return;
1085
+ if (currentField.type === "bool") {
1086
+ if (input === " " || key.return) {
1087
+ setValues((v) => ({ ...v, [currentField.key]: !v[currentField.key] }));
1088
+ setErrors((e) => {
1089
+ const n = { ...e };
1090
+ delete n[currentField.key];
1091
+ return n;
1092
+ });
1093
+ }
1094
+ return;
1095
+ }
1096
+ if (currentField.type === "select") {
1097
+ if (input === " " || key.return) {
1098
+ const opts = currentField.options ?? [];
1099
+ const cur = String(values[currentField.key] ?? opts[0]);
1100
+ const nextIdx = (opts.indexOf(cur) + 1) % opts.length;
1101
+ setValues((v) => ({ ...v, [currentField.key]: opts[nextIdx] }));
1102
+ }
1103
+ return;
1104
+ }
1105
+ if (key.return) {
1106
+ if (currentField.pathType) {
1107
+ setPickerOpen(true);
1108
+ return;
1109
+ }
1110
+ return;
1111
+ }
1112
+ if (key.backspace || key.delete) {
1113
+ setValues((v) => ({
1114
+ ...v,
1115
+ [currentField.key]: String(v[currentField.key] ?? "").slice(0, -1)
1116
+ }));
1117
+ setErrors((e) => {
1118
+ const n = { ...e };
1119
+ delete n[currentField.key];
1120
+ return n;
1121
+ });
1122
+ return;
1123
+ }
1124
+ if (input && !key.ctrl && !key.meta) {
1125
+ setValues((v) => ({
1126
+ ...v,
1127
+ [currentField.key]: String(v[currentField.key] ?? "") + input
1128
+ }));
1129
+ setErrors((e) => {
1130
+ const n = { ...e };
1131
+ delete n[currentField.key];
1132
+ return n;
1133
+ });
1134
+ }
1135
+ });
1136
+ const menuItems = [
1137
+ ...flattenMenu(MENU, state.expandedKeys),
1138
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
1139
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
1140
+ ];
1141
+ const menuCursor = menuItems.findIndex((f) => f.item.commandKey === state.commandKey);
1142
+ const crumbs = pathFromCommandKey(state.commandKey);
1143
+ const filledCount = fields.filter((f) => values[f.key] !== "" && values[f.key] !== void 0 && values[f.key] !== false).length;
1144
+ return /* @__PURE__ */ jsx10(
1145
+ AppShell,
1146
+ {
1147
+ header: /* @__PURE__ */ jsx10(
1148
+ HeaderBar,
1149
+ {
1150
+ accent,
1151
+ crumbs,
1152
+ status: /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
1153
+ /* @__PURE__ */ jsx10(Text9, { color: Object.keys(errors).length > 0 ? T.red : T.mint, children: "\u25CF" }),
1154
+ /* @__PURE__ */ jsx10(Text9, { color: T.fgDim, children: Object.keys(errors).length > 0 ? `${Object.keys(errors).length} errors` : `${filledCount} / ${fields.length} fields` })
1155
+ ] })
1156
+ }
1157
+ ),
1158
+ menu: /* @__PURE__ */ jsx10(Panel, { title: "Commands", badge: "MENU", accent, children: /* @__PURE__ */ jsx10(MenuTree, { items: menuItems, cursor: menuCursor >= 0 ? menuCursor : 0, accent }) }),
1159
+ body: pickerOpen && currentField?.pathType ? /* @__PURE__ */ jsx10(Panel, { title: `${currentField.label} \u2014 \uACBD\uB85C \uC120\uD0DD`, badge: "PICKER", accent, focused: true, children: /* @__PURE__ */ jsx10(
1160
+ FilePicker,
1161
+ {
1162
+ initialPath: String(values[currentField.key] ?? ""),
1163
+ pathType: currentField.pathType,
1164
+ accent,
1165
+ onSelect: (selected) => {
1166
+ setValues((v) => ({ ...v, [currentField.key]: selected }));
1167
+ setErrors((e) => {
1168
+ const n = { ...e };
1169
+ delete n[currentField.key];
1170
+ return n;
1171
+ });
1172
+ setPickerOpen(false);
1173
+ },
1174
+ onCancel: () => setPickerOpen(false)
1175
+ }
1176
+ ) }) : /* @__PURE__ */ jsx10(
1177
+ Panel,
1178
+ {
1179
+ title: def.label,
1180
+ badge: "FORM",
1181
+ accent,
1182
+ focused: true,
1183
+ headerRight: /* @__PURE__ */ jsx10(Text9, { color: T.amber, children: "* required" }),
1184
+ children: /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingY: 1, children: [
1185
+ hasUrlFill && /* @__PURE__ */ jsxs10(
1186
+ Box10,
1187
+ {
1188
+ flexDirection: "column",
1189
+ borderStyle: "round",
1190
+ borderColor: onUrlField ? URL_FILL_COLOR : T.borderDim,
1191
+ paddingX: 1,
1192
+ marginBottom: 1,
1193
+ children: [
1194
+ /* @__PURE__ */ jsxs10(Box10, { children: [
1195
+ /* @__PURE__ */ jsx10(Text9, { color: URL_FILL_COLOR, bold: true, children: "\u{1F517} \uBE60\uB978 \uC785\uB825" }),
1196
+ /* @__PURE__ */ jsxs10(Text9, { color: T.fgFaint, children: [
1197
+ " ",
1198
+ "\u2014 Confluence \uD398\uC774\uC9C0 URL\uC744 \uBD99\uC5EC\uB123\uC73C\uBA74",
1199
+ " "
1200
+ ] }),
1201
+ /* @__PURE__ */ jsx10(Text9, { color: URL_FILL_COLOR, bold: true, children: fillTargets.join(", ") }),
1202
+ /* @__PURE__ */ jsxs10(Text9, { color: T.fgFaint, children: [
1203
+ " ",
1204
+ "\uD544\uB4DC\uAC00 \uC790\uB3D9 \uC785\uB825\uB429\uB2C8\uB2E4"
1205
+ ] })
1206
+ ] }),
1207
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
1208
+ urlValue ? /* @__PURE__ */ jsx10(Text9, { color: T.fg, children: urlValue }) : /* @__PURE__ */ jsx10(Text9, { color: T.fgFaint, children: "https://confluence.tde.sktelecom.com/spaces/TDE/pages/123456/..." }),
1209
+ onUrlField && /* @__PURE__ */ jsx10(Text9, { backgroundColor: URL_FILL_COLOR, color: T.bg, children: " " })
1210
+ ] })
1211
+ ]
1212
+ }
1213
+ ),
1214
+ fields.map((f, i) => /* @__PURE__ */ jsx10(
1215
+ FormField,
1216
+ {
1217
+ field: f,
1218
+ value: values[f.key] ?? "",
1219
+ focused: i === focusIdx,
1220
+ error: errors[f.key],
1221
+ accent
1222
+ },
1223
+ f.key
1224
+ )),
1225
+ /* @__PURE__ */ jsxs10(
1226
+ Box10,
1227
+ {
1228
+ borderStyle: "single",
1229
+ borderColor: T.borderDim,
1230
+ paddingX: 1,
1231
+ marginTop: 1,
1232
+ flexDirection: "column",
1233
+ children: [
1234
+ /* @__PURE__ */ jsx10(Text9, { color: T.fgFaint, bold: true, children: "PREVIEW" }),
1235
+ /* @__PURE__ */ jsxs10(Text9, { children: [
1236
+ /* @__PURE__ */ jsx10(Text9, { color: T.fgFaint, children: "$ " }),
1237
+ /* @__PURE__ */ jsx10(Text9, { color: accent, children: "tdecollab " }),
1238
+ /* @__PURE__ */ jsxs10(Text9, { color: T.cyan, children: [
1239
+ def.key.split(":").join(" "),
1240
+ " "
1241
+ ] }),
1242
+ /* @__PURE__ */ jsx10(Text9, { color: T.fg, children: preview.replace(/^tdecollab\s+\S+(?:\s+\S+){0,2}\s*/, "") })
1243
+ ] })
1244
+ ]
1245
+ }
1246
+ ),
1247
+ /* @__PURE__ */ jsxs10(Box10, { gap: 3, marginTop: 1, children: [
1248
+ /* @__PURE__ */ jsx10(Text9, { backgroundColor: accent, color: T.bg, bold: true, children: " Ctrl+R Run " }),
1249
+ /* @__PURE__ */ jsx10(Text9, { backgroundColor: T.panelHi, color: T.fg, children: " Ctrl+S Save preset " }),
1250
+ /* @__PURE__ */ jsx10(Text9, { backgroundColor: T.panelHi, color: T.fgDim, children: " Esc Back " })
1251
+ ] })
1252
+ ] })
1253
+ }
1254
+ ),
1255
+ log: /* @__PURE__ */ jsx10(LogPane, { lines: state.logs, accent }),
1256
+ footer: pickerOpen ? /* @__PURE__ */ jsx10(Keymap, { accent, keys: [
1257
+ { key: "\u2191\u2193", label: "navigate" },
1258
+ { key: "\u21B5", label: "open / select" },
1259
+ { key: "Tab/Spc", label: "select dir" },
1260
+ { key: "Esc", label: "cancel" }
1261
+ ] }) : /* @__PURE__ */ jsx10(Keymap, { accent, keys: [
1262
+ { key: "Tab", label: "next field" },
1263
+ { key: "\u21E7Tab", label: "prev" },
1264
+ { key: "A-Z", label: "\uBC14\uB85C \uC785\uB825" },
1265
+ { key: "\u21B5", label: currentField?.pathType ? "\uACBD\uB85C \uC120\uD0DD\uCC3D" : currentField?.type === "bool" ? "toggle" : "\u2014" },
1266
+ { key: "Ctrl+R", label: "Run" },
1267
+ { key: "Ctrl+S", label: "Save" },
1268
+ { key: "Esc", label: "Back" }
1269
+ ] })
1270
+ }
1271
+ );
1272
+ }
1273
+
1274
+ // tools/tui/screens/HistoryScreen.tsx
1275
+ import { Box as Box11, Text as Text10, useInput as useInput4 } from "ink";
1276
+ import { useState as useState3 } from "react";
1277
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1278
+ var SVC_LABEL = { cf: "CF", jr: "JR", gl: "GL" };
1279
+ var STATE_COLOR = { ok: T.mint, warn: T.amber, err: T.red };
1280
+ function HistoryScreen({ state, onBack, onReplay, accent = DEFAULT_ACCENT }) {
1281
+ const [selected, setSelected] = useState3(state.historySelected);
1282
+ const history = state.history;
1283
+ const okCount = history.filter((h) => h.state === "ok").length;
1284
+ const warnCount = history.filter((h) => h.state === "warn").length;
1285
+ const errCount = history.filter((h) => h.state === "err").length;
1286
+ useInput4((input, key) => {
1287
+ if (key.escape) onBack();
1288
+ else if (key.upArrow) setSelected((s) => Math.max(0, s - 1));
1289
+ else if (key.downArrow) setSelected((s) => Math.min(history.length - 1, s + 1));
1290
+ else if (key.return) {
1291
+ if (history[selected]) onReplay(history[selected]);
1292
+ } else if (input === "x" || input === "X") {
1293
+ }
1294
+ });
1295
+ const menuItems = [
1296
+ ...flattenMenu(MENU, state.expandedKeys),
1297
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
1298
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
1299
+ ];
1300
+ const menuCursor = menuItems.findIndex((f) => f.item.key === "history");
1301
+ const selectedEntry = history[selected];
1302
+ return /* @__PURE__ */ jsx11(
1303
+ AppShell,
1304
+ {
1305
+ header: /* @__PURE__ */ jsx11(
1306
+ HeaderBar,
1307
+ {
1308
+ accent,
1309
+ crumbs: ["history"],
1310
+ status: /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
1311
+ /* @__PURE__ */ jsx11(Text10, { color: T.fg, children: history.length }),
1312
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgDim, children: "commands \xB7" }),
1313
+ /* @__PURE__ */ jsxs11(Text10, { color: T.mint, children: [
1314
+ okCount,
1315
+ " ok"
1316
+ ] }),
1317
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgDim, children: "/" }),
1318
+ /* @__PURE__ */ jsxs11(Text10, { color: T.amber, children: [
1319
+ warnCount,
1320
+ " warn"
1321
+ ] }),
1322
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgDim, children: "/" }),
1323
+ /* @__PURE__ */ jsxs11(Text10, { color: T.red, children: [
1324
+ errCount,
1325
+ " err"
1326
+ ] })
1327
+ ] })
1328
+ }
1329
+ ),
1330
+ menu: /* @__PURE__ */ jsx11(Panel, { title: "Commands", badge: "MENU", accent, children: /* @__PURE__ */ jsx11(MenuTree, { items: menuItems, cursor: menuCursor >= 0 ? menuCursor : 0, accent }) }),
1331
+ body: /* @__PURE__ */ jsx11(Panel, { title: "History", badge: "LOG", accent, focused: true, children: /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
1332
+ /* @__PURE__ */ jsxs11(
1333
+ Box11,
1334
+ {
1335
+ borderStyle: "single",
1336
+ borderBottom: true,
1337
+ borderTop: false,
1338
+ borderLeft: false,
1339
+ borderRight: false,
1340
+ borderColor: T.borderDim,
1341
+ children: [
1342
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, bold: true, children: "TIME ".padEnd(7) }),
1343
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, bold: true, children: "SVC ".padEnd(5) }),
1344
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, bold: true, children: "COMMAND".padEnd(55) }),
1345
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, bold: true, children: "TOOK".padEnd(9) }),
1346
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, bold: true, children: "RESULT" })
1347
+ ]
1348
+ }
1349
+ ),
1350
+ history.length === 0 ? /* @__PURE__ */ jsx11(Box11, { marginTop: 2, children: /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, children: "\uD788\uC2A4\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA85\uB839\uC744 \uC2E4\uD589\uD558\uBA74 \uC5EC\uAE30\uC5D0 \uAE30\uB85D\uB429\uB2C8\uB2E4." }) }) : history.map((h, i) => /* @__PURE__ */ jsxs11(
1351
+ Box11,
1352
+ {
1353
+ borderStyle: "single",
1354
+ borderBottom: true,
1355
+ borderTop: false,
1356
+ borderLeft: false,
1357
+ borderRight: false,
1358
+ borderColor: T.borderDim,
1359
+ children: [
1360
+ /* @__PURE__ */ jsx11(Text10, { color: i === selected ? accent : T.fgDim, children: h.when.padEnd(7) }),
1361
+ /* @__PURE__ */ jsx11(
1362
+ Text10,
1363
+ {
1364
+ backgroundColor: `${SVC_COLOR[h.svc] ?? T.fgDim}22`,
1365
+ color: SVC_COLOR[h.svc] ?? T.fgDim,
1366
+ bold: true,
1367
+ children: ` ${SVC_LABEL[h.svc] ?? h.svc} `
1368
+ }
1369
+ ),
1370
+ /* @__PURE__ */ jsx11(Text10, { children: " " }),
1371
+ /* @__PURE__ */ jsx11(Text10, { color: i === selected ? accent : T.fg, children: `tdecollab ${h.cmd}`.slice(0, 54).padEnd(55) }),
1372
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgDim, children: h.dur.padEnd(9) }),
1373
+ /* @__PURE__ */ jsxs11(Text10, { color: STATE_COLOR[h.state] ?? T.fg, children: [
1374
+ "\u25CF ",
1375
+ h.result
1376
+ ] })
1377
+ ]
1378
+ },
1379
+ i
1380
+ )),
1381
+ selectedEntry && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, flexDirection: "column", gap: 0, children: [
1382
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgFaint, children: "\u2500\u2500\u2500 \uC120\uD0DD\uB41C \uD56D\uBAA9 \u2500\u2500\u2500" }),
1383
+ /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
1384
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgDim, children: "\uBA85\uB839:" }),
1385
+ /* @__PURE__ */ jsxs11(Text10, { color: accent, children: [
1386
+ "tdecollab ",
1387
+ selectedEntry.cmd
1388
+ ] })
1389
+ ] }),
1390
+ /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
1391
+ /* @__PURE__ */ jsx11(Text10, { color: T.fgDim, children: "\uACB0\uACFC:" }),
1392
+ /* @__PURE__ */ jsx11(Text10, { color: STATE_COLOR[selectedEntry.state], children: selectedEntry.result }),
1393
+ /* @__PURE__ */ jsxs11(Text10, { color: T.fgDim, children: [
1394
+ "\xB7 ",
1395
+ selectedEntry.dur
1396
+ ] })
1397
+ ] })
1398
+ ] })
1399
+ ] }) }),
1400
+ log: /* @__PURE__ */ jsx11(LogPane, { lines: state.logs, accent }),
1401
+ footer: /* @__PURE__ */ jsx11(Keymap, { accent, keys: [
1402
+ { key: "\u2191\u2193", label: "navigate" },
1403
+ { key: "\u21B5", label: "replay" },
1404
+ { key: "x", label: "clear" },
1405
+ { key: "Esc", label: "back" }
1406
+ ] })
1407
+ }
1408
+ );
1409
+ }
1410
+
1411
+ // tools/tui/screens/ListScreen.tsx
1412
+ import { Box as Box12, Text as Text11, useInput as useInput5 } from "ink";
1413
+ import { useState as useState4 } from "react";
1414
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1415
+ function ListScreen({ state, onBack, onDrillIn, accent = DEFAULT_ACCENT }) {
1416
+ const [selected, setSelected] = useState4(state.resultListSelected);
1417
+ const crumbs = pathFromCommandKey(state.commandKey);
1418
+ const rows = state.resultList;
1419
+ const cols = state.resultListCols;
1420
+ useInput5((_input, key) => {
1421
+ if (key.escape) onBack();
1422
+ else if (key.upArrow) setSelected((s) => Math.max(0, s - 1));
1423
+ else if (key.downArrow) setSelected((s) => Math.min(rows.length - 1, s + 1));
1424
+ else if (key.return) {
1425
+ if (rows[selected] && onDrillIn) onDrillIn(rows[selected]);
1426
+ }
1427
+ });
1428
+ const menuItems = [
1429
+ ...flattenMenu(MENU, state.expandedKeys),
1430
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
1431
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
1432
+ ];
1433
+ const menuCursor = menuItems.findIndex((f) => f.item.commandKey === state.commandKey);
1434
+ const colWidth = cols.length > 0 ? Math.floor(60 / cols.length) : 20;
1435
+ return /* @__PURE__ */ jsx12(
1436
+ AppShell,
1437
+ {
1438
+ header: /* @__PURE__ */ jsx12(
1439
+ HeaderBar,
1440
+ {
1441
+ accent,
1442
+ crumbs,
1443
+ status: /* @__PURE__ */ jsxs12(Box12, { gap: 1, children: [
1444
+ /* @__PURE__ */ jsx12(Text11, { color: T.mint, children: "\u25CF" }),
1445
+ /* @__PURE__ */ jsxs12(Text11, { color: T.fgDim, children: [
1446
+ rows.length,
1447
+ " results"
1448
+ ] })
1449
+ ] })
1450
+ }
1451
+ ),
1452
+ menu: /* @__PURE__ */ jsx12(Panel, { title: "Commands", badge: "MENU", accent, children: /* @__PURE__ */ jsx12(MenuTree, { items: menuItems, cursor: menuCursor >= 0 ? menuCursor : 0, accent }) }),
1453
+ body: /* @__PURE__ */ jsx12(Panel, { title: crumbs.join(" "), badge: "LIST", accent, focused: true, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
1454
+ /* @__PURE__ */ jsx12(Box12, { borderStyle: "single", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, borderColor: T.borderDim, children: cols.map((c) => /* @__PURE__ */ jsx12(Text11, { color: T.fgFaint, bold: true, children: c.toUpperCase().padEnd(colWidth) }, c)) }),
1455
+ rows.map((row, i) => /* @__PURE__ */ jsx12(
1456
+ Box12,
1457
+ {
1458
+ borderStyle: "single",
1459
+ borderBottom: true,
1460
+ borderTop: false,
1461
+ borderLeft: false,
1462
+ borderRight: false,
1463
+ borderColor: T.borderDim,
1464
+ children: cols.map((c) => /* @__PURE__ */ jsx12(
1465
+ Text11,
1466
+ {
1467
+ color: i === selected ? accent : T.fg,
1468
+ bold: i === selected,
1469
+ children: String(row[c] ?? "").slice(0, colWidth - 1).padEnd(colWidth)
1470
+ },
1471
+ c
1472
+ ))
1473
+ },
1474
+ i
1475
+ )),
1476
+ /* @__PURE__ */ jsxs12(Box12, { marginTop: 1, children: [
1477
+ /* @__PURE__ */ jsxs12(Text11, { color: T.fgFaint, children: [
1478
+ "showing 1\u2013",
1479
+ rows.length,
1480
+ " \xB7 row",
1481
+ " "
1482
+ ] }),
1483
+ /* @__PURE__ */ jsx12(Text11, { color: accent, children: selected + 1 }),
1484
+ /* @__PURE__ */ jsx12(Text11, { color: T.fgFaint, children: " selected \xB7 \u21B5 to drill in" })
1485
+ ] })
1486
+ ] }) }),
1487
+ log: /* @__PURE__ */ jsx12(LogPane, { lines: state.logs, accent }),
1488
+ footer: /* @__PURE__ */ jsx12(Keymap, { accent, keys: [
1489
+ { key: "\u2191\u2193", label: "navigate" },
1490
+ { key: "\u21B5", label: "view" },
1491
+ { key: "Esc", label: "back" }
1492
+ ] })
1493
+ }
1494
+ );
1495
+ }
1496
+
1497
+ // tools/tui/screens/MenuScreen.tsx
1498
+ import { Box as Box13, Text as Text12, useInput as useInput6 } from "ink";
1499
+ import { useCallback, useEffect, useState as useState5 } from "react";
1500
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1501
+ function MenuScreen({
1502
+ state,
1503
+ onSelectCommand,
1504
+ onToggleExpanded,
1505
+ onOpenHistory,
1506
+ onQuit,
1507
+ accent = DEFAULT_ACCENT
1508
+ }) {
1509
+ const expanded = state.expandedKeys;
1510
+ const allItems = [
1511
+ ...flattenMenu(MENU, expanded),
1512
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
1513
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
1514
+ ];
1515
+ const initialCursor = (() => {
1516
+ if (!state.activePath) return 0;
1517
+ const idx = allItems.findIndex(
1518
+ (f) => f.item.commandKey === state.activePath || f.item.key === state.activePath
1519
+ );
1520
+ return idx >= 0 ? idx : 0;
1521
+ })();
1522
+ const [cursor, setCursor] = useState5(initialCursor);
1523
+ const [scrollOffset, setScrollOffset] = useState5(0);
1524
+ const currentFlat = allItems[cursor];
1525
+ const currentItem = currentFlat?.item;
1526
+ const commandKey = currentItem?.commandKey;
1527
+ const commandDef = commandKey ? COMMANDS[commandKey] : null;
1528
+ const adjustScroll = useCallback((newCursor) => {
1529
+ const maxVisible = 24;
1530
+ if (newCursor < scrollOffset) setScrollOffset(newCursor);
1531
+ else if (newCursor >= scrollOffset + maxVisible) setScrollOffset(newCursor - maxVisible + 1);
1532
+ }, [scrollOffset]);
1533
+ useInput6((input, key) => {
1534
+ if (key.upArrow) {
1535
+ const next = Math.max(0, cursor - 1);
1536
+ setCursor(next);
1537
+ adjustScroll(next);
1538
+ } else if (key.downArrow) {
1539
+ const next = Math.min(allItems.length - 1, cursor + 1);
1540
+ setCursor(next);
1541
+ adjustScroll(next);
1542
+ } else if (key.rightArrow || key.return) {
1543
+ if (currentItem?.children) {
1544
+ onToggleExpanded(currentItem.key);
1545
+ } else if (currentItem?.commandKey) {
1546
+ onSelectCommand(currentItem.commandKey);
1547
+ } else if (currentItem?.key === "history") {
1548
+ onOpenHistory();
1549
+ }
1550
+ } else if (key.leftArrow) {
1551
+ if (currentFlat?.parentKey && expanded.includes(currentFlat.parentKey)) {
1552
+ onToggleExpanded(currentFlat.parentKey);
1553
+ }
1554
+ } else if (input === "h") {
1555
+ onOpenHistory();
1556
+ } else if (input === "q" || key.ctrl && input === "c") {
1557
+ onQuit();
1558
+ }
1559
+ });
1560
+ useEffect(() => {
1561
+ if (initialCursor > 0) adjustScroll(initialCursor);
1562
+ }, []);
1563
+ const crumbs = commandKey ? pathFromCommandKey(commandKey) : currentItem?.key === "history" ? ["history"] : ["menu"];
1564
+ return /* @__PURE__ */ jsx13(
1565
+ AppShell,
1566
+ {
1567
+ header: /* @__PURE__ */ jsx13(
1568
+ HeaderBar,
1569
+ {
1570
+ accent,
1571
+ crumbs,
1572
+ status: /* @__PURE__ */ jsxs13(Box13, { gap: 1, children: [
1573
+ /* @__PURE__ */ jsx13(Text12, { color: T.mint, children: "\u25CF" }),
1574
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, children: "ready" })
1575
+ ] })
1576
+ }
1577
+ ),
1578
+ menu: /* @__PURE__ */ jsx13(Panel, { title: "Commands", badge: "MENU", accent, focused: true, children: /* @__PURE__ */ jsx13(MenuTree, { items: allItems, cursor, accent, scrollOffset }) }),
1579
+ body: /* @__PURE__ */ jsx13(
1580
+ Panel,
1581
+ {
1582
+ title: commandDef?.label ?? currentItem?.label ?? "tdecollab",
1583
+ badge: "DETAILS",
1584
+ accent,
1585
+ children: commandDef ? /* @__PURE__ */ jsx13(CommandDetails, { def: commandDef, accent }) : /* @__PURE__ */ jsx13(WelcomeView, { accent })
1586
+ }
1587
+ ),
1588
+ log: /* @__PURE__ */ jsx13(LogPane, { lines: state.logs, accent }),
1589
+ footer: /* @__PURE__ */ jsx13(Keymap, { accent, keys: [
1590
+ { key: "\u2191\u2193", label: "navigate" },
1591
+ { key: "\u2192/\u21B5", label: "select / expand" },
1592
+ { key: "\u2190", label: "collapse" },
1593
+ { key: "h", label: "history" },
1594
+ { key: "q", label: "quit" }
1595
+ ] })
1596
+ }
1597
+ );
1598
+ }
1599
+ function WelcomeView({ accent }) {
1600
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, paddingY: 1, children: [
1601
+ /* @__PURE__ */ jsx13(Text12, { color: accent, bold: true, children: "tdecollab TUI" }),
1602
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, children: "\uC88C\uCE21 \uBA54\uB274\uC5D0\uC11C \uBA85\uB839\uC744 \uC120\uD0DD\uD558\uC138\uC694." }),
1603
+ /* @__PURE__ */ jsxs13(Box13, { marginTop: 1, flexDirection: "column", gap: 0, children: [
1604
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgFaint, children: " Confluence \u25C6 \uD398\uC774\uC9C0/\uC2A4\uD398\uC774\uC2A4/\uAC80\uC0C9" }),
1605
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgFaint, children: " JIRA \u25C6 \uC774\uC288/\uAC80\uC0C9" }),
1606
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgFaint, children: " GitLab \u25C6 MR/\uD30C\uC774\uD504\uB77C\uC778" })
1607
+ ] }),
1608
+ /* @__PURE__ */ jsxs13(Box13, { marginTop: 1, children: [
1609
+ /* @__PURE__ */ jsx13(Text12, { color: T.amber, children: "\u21B5" }),
1610
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, children: " \uB97C \uB20C\uB7EC \uBA85\uB839 \uD3FC\uC73C\uB85C \uC9C4\uC785\uD569\uB2C8\uB2E4." })
1611
+ ] })
1612
+ ] });
1613
+ }
1614
+ function CommandDetails({ def, accent }) {
1615
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, paddingY: 1, children: [
1616
+ /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 0, children: [
1617
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, bold: true, children: "DESCRIPTION" }),
1618
+ /* @__PURE__ */ jsx13(Text12, { color: T.fg, children: def.description })
1619
+ ] }),
1620
+ /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 0, children: [
1621
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, bold: true, children: "SYNOPSIS" }),
1622
+ /* @__PURE__ */ jsx13(Box13, { borderStyle: "single", borderColor: T.borderDim, paddingX: 1, marginTop: 0, children: /* @__PURE__ */ jsxs13(Text12, { color: T.cyan, children: [
1623
+ "$ ",
1624
+ def.synopsis
1625
+ ] }) })
1626
+ ] }),
1627
+ /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 0, children: [
1628
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, bold: true, children: "OPTIONS" }),
1629
+ def.fields.map((f) => /* @__PURE__ */ jsxs13(Box13, { gap: 2, children: [
1630
+ /* @__PURE__ */ jsx13(Text12, { color: accent, children: f.label.padEnd(28) }),
1631
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, children: f.hint ?? (f.type === "bool" ? "flag" : f.type) }),
1632
+ f.required && /* @__PURE__ */ jsx13(Text12, { color: T.amber, children: " *required" })
1633
+ ] }, f.key))
1634
+ ] }),
1635
+ /* @__PURE__ */ jsxs13(
1636
+ Box13,
1637
+ {
1638
+ borderStyle: "single",
1639
+ borderColor: T.borderDim,
1640
+ paddingX: 1,
1641
+ marginTop: 1,
1642
+ children: [
1643
+ /* @__PURE__ */ jsx13(Text12, { color: T.amber, children: "\u21B5" }),
1644
+ /* @__PURE__ */ jsx13(Text12, { color: T.fgDim, children: " Enter\uB97C \uB20C\uB7EC \uC785\uB825 \uD3FC\uC73C\uB85C \uC9C4\uC785\uD569\uB2C8\uB2E4." })
1645
+ ]
1646
+ }
1647
+ )
1648
+ ] });
1649
+ }
1650
+
1651
+ // tools/tui/screens/RunningScreen.tsx
1652
+ import { Box as Box14, Text as Text14 } from "ink";
1653
+
1654
+ // tools/tui/components/Spinner.tsx
1655
+ import { Text as Text13 } from "ink";
1656
+ import { useEffect as useEffect2, useState as useState6 } from "react";
1657
+ import { jsx as jsx14 } from "react/jsx-runtime";
1658
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1659
+ function Spinner({ color = T.cyan }) {
1660
+ const [frame, setFrame] = useState6(0);
1661
+ useEffect2(() => {
1662
+ const id = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80);
1663
+ return () => clearInterval(id);
1664
+ }, []);
1665
+ return /* @__PURE__ */ jsx14(Text13, { color, children: FRAMES[frame] });
1666
+ }
1667
+
1668
+ // tools/tui/screens/RunningScreen.tsx
1669
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1670
+ function RunningScreen({ state, onCancel, accent = DEFAULT_ACCENT }) {
1671
+ const crumbs = pathFromCommandKey(state.commandKey);
1672
+ const doneCount = state.steps.filter((s) => s.state === "done").length;
1673
+ const total = state.steps.length;
1674
+ const menuItems = [
1675
+ ...flattenMenu(MENU, state.expandedKeys),
1676
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
1677
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
1678
+ ];
1679
+ const menuCursor = menuItems.findIndex((f) => f.item.commandKey === state.commandKey);
1680
+ const progressPct = total > 0 ? Math.round(doneCount / total * 100) : 0;
1681
+ const barWidth = 40;
1682
+ const filled = Math.round(progressPct / 100 * barWidth);
1683
+ return /* @__PURE__ */ jsx15(
1684
+ AppShell,
1685
+ {
1686
+ header: /* @__PURE__ */ jsx15(
1687
+ HeaderBar,
1688
+ {
1689
+ accent,
1690
+ crumbs,
1691
+ status: /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
1692
+ /* @__PURE__ */ jsx15(Spinner, { color: T.cyan }),
1693
+ /* @__PURE__ */ jsx15(Text14, { color: T.cyan, bold: true, children: "RUNNING" })
1694
+ ] })
1695
+ }
1696
+ ),
1697
+ menu: /* @__PURE__ */ jsx15(Panel, { title: "Commands", badge: "MENU", accent, children: /* @__PURE__ */ jsx15(MenuTree, { items: menuItems, cursor: menuCursor >= 0 ? menuCursor : 0, accent }) }),
1698
+ body: /* @__PURE__ */ jsx15(Panel, { title: "Pipeline", badge: "EXEC", accent: T.cyan, focused: true, children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", gap: 1, paddingY: 1, children: [
1699
+ /* @__PURE__ */ jsx15(Text14, { color: T.fgDim, bold: true, children: "STEPS" }),
1700
+ state.steps.map((step) => /* @__PURE__ */ jsxs14(Box14, { gap: 2, children: [
1701
+ step.state === "done" && /* @__PURE__ */ jsx15(Text14, { color: T.mint, children: "\u2713" }),
1702
+ step.state === "running" && /* @__PURE__ */ jsx15(Spinner, { color: T.cyan }),
1703
+ step.state === "pending" && /* @__PURE__ */ jsx15(Text14, { color: T.fgFaint, children: "\u25CB" }),
1704
+ step.state === "err" && /* @__PURE__ */ jsx15(Text14, { color: T.red, children: "\u2715" }),
1705
+ /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
1706
+ /* @__PURE__ */ jsx15(
1707
+ Text14,
1708
+ {
1709
+ color: step.state === "pending" ? T.fgMute : step.state === "running" ? T.fg : step.state === "err" ? T.red : T.fg,
1710
+ bold: step.state === "running",
1711
+ children: step.title
1712
+ }
1713
+ ),
1714
+ /* @__PURE__ */ jsx15(Text14, { color: T.fgFaint, children: step.detail })
1715
+ ] }),
1716
+ step.state === "running" && /* @__PURE__ */ jsx15(Text14, { backgroundColor: T.cyan, color: T.bg, bold: true, children: " IN PROGRESS " }),
1717
+ step.state === "done" && /* @__PURE__ */ jsx15(Text14, { color: T.fgFaint, children: "done" })
1718
+ ] }, step.id)),
1719
+ /* @__PURE__ */ jsxs14(
1720
+ Box14,
1721
+ {
1722
+ borderStyle: "single",
1723
+ borderColor: T.borderDim,
1724
+ paddingX: 1,
1725
+ flexDirection: "column",
1726
+ marginTop: 1,
1727
+ children: [
1728
+ /* @__PURE__ */ jsxs14(Box14, { gap: 2, children: [
1729
+ /* @__PURE__ */ jsx15(Text14, { color: T.fgDim, children: "Overall progress" }),
1730
+ /* @__PURE__ */ jsxs14(Text14, { color: T.cyan, children: [
1731
+ doneCount,
1732
+ " / ",
1733
+ total,
1734
+ " \xB7 ",
1735
+ progressPct,
1736
+ "%"
1737
+ ] })
1738
+ ] }),
1739
+ /* @__PURE__ */ jsxs14(Box14, { children: [
1740
+ /* @__PURE__ */ jsx15(Text14, { color: T.mint, children: "\u2588".repeat(filled) }),
1741
+ /* @__PURE__ */ jsx15(Text14, { color: T.borderDim, children: "\u2591".repeat(barWidth - filled) })
1742
+ ] })
1743
+ ]
1744
+ }
1745
+ )
1746
+ ] }) }),
1747
+ log: /* @__PURE__ */ jsx15(LogPane, { lines: state.logs, accent, title: "Stream", maxLines: 6 }),
1748
+ footer: /* @__PURE__ */ jsx15(Keymap, { accent, keys: [
1749
+ { key: "Ctrl-C", label: "cancel" }
1750
+ ] })
1751
+ }
1752
+ );
1753
+ }
1754
+
1755
+ // tools/tui/screens/TextView.tsx
1756
+ import { Box as Box15, Text as Text15, useInput as useInput7, useStdout } from "ink";
1757
+ import { useState as useState7 } from "react";
1758
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1759
+ function TextView({ state, onBack, accent = DEFAULT_ACCENT }) {
1760
+ const { stdout } = useStdout();
1761
+ const termHeight = stdout?.rows ?? 40;
1762
+ const visibleLines = Math.max(8, termHeight - 18);
1763
+ const lines = state.resultText.split("\n");
1764
+ const [scrollTop, setScrollTop] = useState7(0);
1765
+ const crumbs = pathFromCommandKey(state.commandKey);
1766
+ const maxScroll = Math.max(0, lines.length - visibleLines);
1767
+ const halfPage = Math.max(1, Math.floor(visibleLines / 2));
1768
+ useInput7((input, key) => {
1769
+ if (key.escape || input === "q") onBack();
1770
+ else if (key.downArrow || input === "j") setScrollTop((s) => Math.min(maxScroll, s + 1));
1771
+ else if (key.upArrow || input === "k") setScrollTop((s) => Math.max(0, s - 1));
1772
+ else if (input === "g") setScrollTop(0);
1773
+ else if (input === "G") setScrollTop(maxScroll);
1774
+ else if (key.ctrl && input === "f" || key.pageDown) {
1775
+ setScrollTop((s) => Math.min(maxScroll, s + visibleLines));
1776
+ } else if (key.ctrl && input === "b" || key.pageUp) {
1777
+ setScrollTop((s) => Math.max(0, s - visibleLines));
1778
+ } else if (key.ctrl && input === "d") {
1779
+ setScrollTop((s) => Math.min(maxScroll, s + halfPage));
1780
+ } else if (key.ctrl && input === "u") {
1781
+ setScrollTop((s) => Math.max(0, s - halfPage));
1782
+ }
1783
+ });
1784
+ const visible = lines.slice(scrollTop, scrollTop + visibleLines);
1785
+ const menuItems = [
1786
+ ...flattenMenu(MENU, state.expandedKeys),
1787
+ { item: { key: "_sep", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", dim: true }, depth: 0 },
1788
+ ...META_ITEMS.map((m) => ({ item: m, depth: 0 }))
1789
+ ];
1790
+ const menuCursor = menuItems.findIndex((f) => f.item.commandKey === state.commandKey);
1791
+ return /* @__PURE__ */ jsx16(
1792
+ AppShell,
1793
+ {
1794
+ header: /* @__PURE__ */ jsx16(
1795
+ HeaderBar,
1796
+ {
1797
+ accent,
1798
+ crumbs: [...crumbs, "result"],
1799
+ status: /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
1800
+ /* @__PURE__ */ jsx16(Text15, { color: T.cyan, children: "VIEW" }),
1801
+ /* @__PURE__ */ jsxs15(Text15, { color: T.fgDim, children: [
1802
+ "\xB7 ",
1803
+ lines.length,
1804
+ " lines"
1805
+ ] })
1806
+ ] })
1807
+ }
1808
+ ),
1809
+ menu: /* @__PURE__ */ jsx16(Panel, { title: "Commands", badge: "MENU", accent, children: /* @__PURE__ */ jsx16(MenuTree, { items: menuItems, cursor: menuCursor >= 0 ? menuCursor : 0, accent }) }),
1810
+ body: /* @__PURE__ */ jsx16(Panel, { title: "Result", badge: "MARKDOWN", accent, focused: true, children: /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
1811
+ visible.map((line, i) => {
1812
+ const lineNo = scrollTop + i + 1;
1813
+ const isH1 = line.startsWith("# ");
1814
+ const isH2 = line.startsWith("## ");
1815
+ const isH3 = line.startsWith("### ");
1816
+ const isCode = line.startsWith("```") || line.startsWith(" ");
1817
+ const isListItem = /^[*\-+] /.test(line) || /^\d+\. /.test(line);
1818
+ return /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
1819
+ /* @__PURE__ */ jsx16(Text15, { color: T.fgFaint, children: String(lineNo).padStart(4) }),
1820
+ /* @__PURE__ */ jsx16(
1821
+ Text15,
1822
+ {
1823
+ color: isH1 ? accent : isH2 ? accent : isH3 ? T.cyan : isCode ? T.mint : isListItem ? T.fg : T.fg,
1824
+ bold: isH1 || isH2,
1825
+ children: line
1826
+ }
1827
+ )
1828
+ ] }, lineNo);
1829
+ }),
1830
+ /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { color: T.fgFaint, children: [
1831
+ scrollTop + 1,
1832
+ "\u2013",
1833
+ Math.min(scrollTop + visibleLines, lines.length),
1834
+ " / ",
1835
+ lines.length,
1836
+ " lines"
1837
+ ] }) })
1838
+ ] }) }),
1839
+ log: /* @__PURE__ */ jsx16(LogPane, { lines: state.logs, accent }),
1840
+ footer: /* @__PURE__ */ jsx16(Keymap, { accent, keys: [
1841
+ { key: "j/k", label: "line" },
1842
+ { key: "Ctrl+D/U", label: "half page" },
1843
+ { key: "Ctrl+F/B", label: "full page" },
1844
+ { key: "g/G", label: "top/end" },
1845
+ { key: "q/Esc", label: "back" }
1846
+ ] })
1847
+ }
1848
+ );
1849
+ }
1850
+
1851
+ // tools/tui/state.ts
1852
+ function makeLog(level, text) {
1853
+ const now = /* @__PURE__ */ new Date();
1854
+ const ts = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
1855
+ return { ts, level, text };
1856
+ }
1857
+
1858
+ // tools/tui/tuiconfig.ts
1859
+ import { cosmiconfig } from "cosmiconfig";
1860
+ import fs2 from "fs";
1861
+ import path2 from "path";
1862
+ import os2 from "os";
1863
+ var explorer = cosmiconfig("tdecollab", {
1864
+ searchPlaces: [
1865
+ ".tdecollab.json",
1866
+ ".tdecollab.yaml",
1867
+ ".tdecollab.yml",
1868
+ "tdecollab.config.json",
1869
+ path2.join(os2.homedir(), ".tdecollab.json")
1870
+ ]
1871
+ });
1872
+ var _config = null;
1873
+ var _configPath = null;
1874
+ async function loadTuiConfig() {
1875
+ if (_config) return _config;
1876
+ try {
1877
+ const result = await explorer.search();
1878
+ if (result) {
1879
+ _config = result.config;
1880
+ _configPath = result.filepath;
1881
+ } else {
1882
+ _config = {};
1883
+ }
1884
+ } catch {
1885
+ _config = {};
1886
+ }
1887
+ return _config;
1888
+ }
1889
+ function getDefaultConfigPath() {
1890
+ return path2.join(process.cwd(), ".tdecollab.json");
1891
+ }
1892
+ function saveTuiConfig(updates) {
1893
+ const configPath = _configPath ?? getDefaultConfigPath();
1894
+ let existing = {};
1895
+ if (fs2.existsSync(configPath)) {
1896
+ try {
1897
+ existing = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
1898
+ } catch {
1899
+ existing = {};
1900
+ }
1901
+ }
1902
+ const merged = {
1903
+ ...existing,
1904
+ ...updates,
1905
+ presets: { ...existing.presets ?? {}, ...updates.presets ?? {} },
1906
+ lastUsed: { ...existing.lastUsed ?? {}, ...updates.lastUsed ?? {} }
1907
+ };
1908
+ fs2.writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
1909
+ _config = merged;
1910
+ }
1911
+ function saveLastUsed(commandKey, values) {
1912
+ saveTuiConfig({ lastUsed: { [commandKey]: values } });
1913
+ }
1914
+
1915
+ // tools/tui/history.ts
1916
+ import fs3 from "fs";
1917
+ import path3 from "path";
1918
+ import os3 from "os";
1919
+ var HISTORY_FILE = path3.join(os3.homedir(), ".tdecollab_history.json");
1920
+ var MAX_HISTORY = 100;
1921
+ function loadHistory() {
1922
+ try {
1923
+ if (fs3.existsSync(HISTORY_FILE)) {
1924
+ return JSON.parse(fs3.readFileSync(HISTORY_FILE, "utf-8"));
1925
+ }
1926
+ } catch {
1927
+ }
1928
+ return [];
1929
+ }
1930
+ function appendHistory(entry) {
1931
+ const history = loadHistory();
1932
+ history.unshift(entry);
1933
+ if (history.length > MAX_HISTORY) history.splice(MAX_HISTORY);
1934
+ try {
1935
+ fs3.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), "utf-8");
1936
+ } catch {
1937
+ }
1938
+ }
1939
+
1940
+ // tools/tui/executor/confluence.ts
1941
+ import fs4 from "fs";
1942
+ import path4 from "path";
1943
+ function makeStep(id, title, detail = "") {
1944
+ return { id, title, detail, state: "pending" };
1945
+ }
1946
+ async function executePageGet(values, onSteps, onLog) {
1947
+ const logs = [];
1948
+ const addLog = (l) => {
1949
+ logs.push(l);
1950
+ onLog(l);
1951
+ };
1952
+ const steps = [
1953
+ makeStep("validate", "Validate arguments"),
1954
+ makeStep("fetch", "Fetch page from Confluence"),
1955
+ makeStep("convert", "Convert to Markdown"),
1956
+ makeStep("images", "Download images", values["downloadImages"] ? "enabled" : "skipped"),
1957
+ makeStep("output", "Write output")
1958
+ ];
1959
+ onSteps([...steps]);
1960
+ steps[0] = { ...steps[0], state: "running", detail: `pageId=${values["pageId"]}` };
1961
+ onSteps([...steps]);
1962
+ if (!values["pageId"]) throw new Error("pageId is required");
1963
+ steps[0] = { ...steps[0], state: "done" };
1964
+ onSteps([...steps]);
1965
+ const config = loadConfluenceConfig();
1966
+ const client = createConfluenceClient(config);
1967
+ const api = new ConfluenceContentApi(client);
1968
+ steps[1] = { ...steps[1], state: "running", detail: `GET /rest/api/content/${values["pageId"]}` };
1969
+ onSteps([...steps]);
1970
+ addLog(makeLog("run", `GET /rest/api/content/${values["pageId"]}?expand=body.storage,version`));
1971
+ const page = await api.getPage(String(values["pageId"]));
1972
+ addLog(makeLog("ok", `200 OK \xB7 "${page.title}" \xB7 ${page.space?.key}`));
1973
+ steps[1] = { ...steps[1], state: "done", detail: `"${page.title}"` };
1974
+ onSteps([...steps]);
1975
+ steps[2] = { ...steps[2], state: "running" };
1976
+ onSteps([...steps]);
1977
+ let imageUrlMap;
1978
+ if (values["downloadImages"] && page.body?.storage?.value) {
1979
+ steps[3] = { ...steps[3], state: "running", detail: "downloading..." };
1980
+ onSteps([...steps]);
1981
+ let baseDir = process.cwd();
1982
+ if (values["output"]) baseDir = path4.dirname(path4.resolve(process.cwd(), String(values["output"])));
1983
+ const imgDir = path4.resolve(baseDir, String(values["imageDir"] || "./images"));
1984
+ const downloader = new ImageDownloader(api, { outputDir: imgDir, pageId: page.id, baseUrl: config.baseUrl });
1985
+ imageUrlMap = await downloader.downloadAllImages(page.body.storage.value);
1986
+ for (const [key, abs] of imageUrlMap.entries()) {
1987
+ imageUrlMap.set(key, path4.relative(baseDir, abs).split(path4.sep).join("/"));
1988
+ }
1989
+ addLog(makeLog("ok", `\uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC (${imageUrlMap.size}\uAC1C) \u2192 ${imgDir}`));
1990
+ steps[3] = { ...steps[3], state: "done", detail: `${imageUrlMap.size} files` };
1991
+ onSteps([...steps]);
1992
+ } else {
1993
+ steps[3] = { ...steps[3], state: "done", detail: "skipped" };
1994
+ onSteps([...steps]);
1995
+ }
1996
+ const converter = new StorageToMarkdownConverter();
1997
+ const markdown = page.body?.storage?.value ? converter.convert(page.body.storage.value, imageUrlMap) : "(No content)";
1998
+ addLog(makeLog("ok", `Markdown \uBCC0\uD658 \uC644\uB8CC (${markdown.split("\n").length} lines)`));
1999
+ steps[2] = { ...steps[2], state: "done", detail: `${markdown.split("\n").length} lines` };
2000
+ onSteps([...steps]);
2001
+ steps[4] = { ...steps[4], state: "running" };
2002
+ onSteps([...steps]);
2003
+ if (values["output"]) {
2004
+ const outputPath = path4.resolve(process.cwd(), String(values["output"]));
2005
+ fs4.mkdirSync(path4.dirname(outputPath), { recursive: true });
2006
+ fs4.writeFileSync(outputPath, markdown, "utf-8");
2007
+ addLog(makeLog("ok", `\uD30C\uC77C \uC800\uC7A5: ${outputPath}`));
2008
+ steps[4] = { ...steps[4], state: "done", detail: outputPath };
2009
+ } else {
2010
+ steps[4] = { ...steps[4], state: "done", detail: "console output" };
2011
+ }
2012
+ onSteps([...steps]);
2013
+ return { type: "text", content: markdown, logs };
2014
+ }
2015
+ async function executePageCreate(values, onSteps, onLog) {
2016
+ const logs = [];
2017
+ const addLog = (l) => {
2018
+ logs.push(l);
2019
+ onLog(l);
2020
+ };
2021
+ const steps = [
2022
+ makeStep("validate", "Validate arguments"),
2023
+ makeStep("read", "Read Markdown file"),
2024
+ makeStep("convert", "Convert Markdown \u2192 Storage"),
2025
+ makeStep("create", "Create Confluence page")
2026
+ ];
2027
+ onSteps([...steps]);
2028
+ steps[0] = { ...steps[0], state: "running" };
2029
+ onSteps([...steps]);
2030
+ if (!values["space"]) throw new Error("--space is required");
2031
+ if (!values["title"]) throw new Error("--title is required");
2032
+ steps[0] = { ...steps[0], state: "done" };
2033
+ onSteps([...steps]);
2034
+ const config = loadConfluenceConfig();
2035
+ const client = createConfluenceClient(config);
2036
+ const api = new ConfluenceContentApi(client);
2037
+ let content = "";
2038
+ if (values["file"]) {
2039
+ steps[1] = { ...steps[1], state: "running", detail: String(values["file"]) };
2040
+ onSteps([...steps]);
2041
+ const filePath = path4.resolve(process.cwd(), String(values["file"]));
2042
+ if (!fs4.existsSync(filePath)) throw new Error(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`);
2043
+ content = fs4.readFileSync(filePath, "utf-8");
2044
+ addLog(makeLog("ok", `\uD30C\uC77C \uC77D\uAE30 \uC644\uB8CC: ${filePath} (${content.split("\n").length} lines)`));
2045
+ steps[1] = { ...steps[1], state: "done", detail: `${content.split("\n").length} lines` };
2046
+ onSteps([...steps]);
2047
+ } else {
2048
+ steps[1] = { ...steps[1], state: "done", detail: "no file (empty page)" };
2049
+ onSteps([...steps]);
2050
+ }
2051
+ steps[2] = { ...steps[2], state: "running" };
2052
+ onSteps([...steps]);
2053
+ const converter = new MarkdownToStorageConverter();
2054
+ const storageXml = converter.convert(content);
2055
+ addLog(makeLog("ok", "Markdown \u2192 Storage XML \uBCC0\uD658 \uC644\uB8CC"));
2056
+ steps[2] = { ...steps[2], state: "done" };
2057
+ onSteps([...steps]);
2058
+ steps[3] = { ...steps[3], state: "running", detail: `POST /rest/api/content \xB7 space=${values["space"]}` };
2059
+ onSteps([...steps]);
2060
+ addLog(makeLog("run", `POST /rest/api/content space=${values["space"]} title="${values["title"]}"`));
2061
+ const page = await api.createPage({
2062
+ title: String(values["title"]),
2063
+ spaceKey: String(values["space"]),
2064
+ body: storageXml,
2065
+ parentId: values["parent"] ? String(values["parent"]) : void 0
2066
+ });
2067
+ addLog(makeLog("ok", `\uD398\uC774\uC9C0 \uC0DD\uC131 \uC644\uB8CC: "${page.title}" (ID: ${page.id})`));
2068
+ steps[3] = { ...steps[3], state: "done", detail: `ID: ${page.id}` };
2069
+ onSteps([...steps]);
2070
+ const resultText = [
2071
+ `# \uD398\uC774\uC9C0 \uC0DD\uC131 \uC644\uB8CC`,
2072
+ ``,
2073
+ `- **\uC81C\uBAA9**: ${page.title}`,
2074
+ `- **ID**: ${page.id}`,
2075
+ `- **\uC2A4\uD398\uC774\uC2A4**: ${page.space?.key}`,
2076
+ `- **URL**: ${config.baseUrl}${page._links?.webui ?? ""}`
2077
+ ].join("\n");
2078
+ return { type: "text", content: resultText, logs };
2079
+ }
2080
+ async function executeSpaceList(_values, onSteps, onLog) {
2081
+ const logs = [];
2082
+ const addLog = (l) => {
2083
+ logs.push(l);
2084
+ onLog(l);
2085
+ };
2086
+ const steps = [makeStep("fetch", "Fetch space list")];
2087
+ onSteps([...steps]);
2088
+ steps[0] = { ...steps[0], state: "running", detail: "GET /rest/api/space" };
2089
+ onSteps([...steps]);
2090
+ addLog(makeLog("run", "GET /rest/api/space?limit=50"));
2091
+ const config = loadConfluenceConfig();
2092
+ const client = createConfluenceClient(config);
2093
+ const api = new ConfluenceSpaceApi(client);
2094
+ const spaces = await api.getSpaces();
2095
+ addLog(makeLog("ok", `${spaces.length}\uAC1C \uC2A4\uD398\uC774\uC2A4 \uC870\uD68C \uC644\uB8CC`));
2096
+ steps[0] = { ...steps[0], state: "done", detail: `${spaces.length} spaces` };
2097
+ onSteps([...steps]);
2098
+ const list = spaces.map((s) => ({
2099
+ key: s.key ?? "",
2100
+ name: s.name ?? "",
2101
+ type: s.type ?? "",
2102
+ id: String(s.id ?? "")
2103
+ }));
2104
+ return { type: "list", list, cols: ["key", "name", "type", "id"], logs };
2105
+ }
2106
+ async function executeSearch(values, onSteps, onLog) {
2107
+ const logs = [];
2108
+ const addLog = (l) => {
2109
+ logs.push(l);
2110
+ onLog(l);
2111
+ };
2112
+ const steps = [makeStep("search", "Search Confluence pages")];
2113
+ onSteps([...steps]);
2114
+ if (!values["cql"]) throw new Error("cql is required");
2115
+ steps[0] = { ...steps[0], state: "running", detail: String(values["cql"]) };
2116
+ onSteps([...steps]);
2117
+ addLog(makeLog("run", `GET /rest/api/content/search?cql=${encodeURIComponent(String(values["cql"]))}`));
2118
+ const config = loadConfluenceConfig();
2119
+ const client = createConfluenceClient(config);
2120
+ const api = new ConfluenceSearchApi(client);
2121
+ const searchResult = await api.searchByCql(String(values["cql"]), 0, Number(values["limit"] ?? 10));
2122
+ const results = searchResult.results ?? [];
2123
+ addLog(makeLog("ok", `${results.length}\uAC1C \uACB0\uACFC`));
2124
+ steps[0] = { ...steps[0], state: "done", detail: `${results.length} results` };
2125
+ onSteps([...steps]);
2126
+ const list = results.map((r) => ({
2127
+ id: String(r.id ?? ""),
2128
+ title: r.title ?? "",
2129
+ space: r.space?.key ?? "",
2130
+ type: r.type ?? ""
2131
+ }));
2132
+ return { type: "list", list, cols: ["id", "title", "space", "type"], logs };
2133
+ }
2134
+ async function executePageUpdate(values, onSteps, onLog) {
2135
+ const logs = [];
2136
+ const addLog = (l) => {
2137
+ logs.push(l);
2138
+ onLog(l);
2139
+ };
2140
+ const steps = [
2141
+ makeStep("validate", "Validate arguments"),
2142
+ makeStep("fetch", "Fetch current page"),
2143
+ makeStep("convert", "Convert Markdown"),
2144
+ makeStep("update", "Update page")
2145
+ ];
2146
+ onSteps([...steps]);
2147
+ steps[0] = { ...steps[0], state: "running" };
2148
+ onSteps([...steps]);
2149
+ if (!values["pageId"]) throw new Error("pageId is required");
2150
+ steps[0] = { ...steps[0], state: "done" };
2151
+ onSteps([...steps]);
2152
+ const config = loadConfluenceConfig();
2153
+ const client = createConfluenceClient(config);
2154
+ const api = new ConfluenceContentApi(client);
2155
+ steps[1] = { ...steps[1], state: "running", detail: `GET /rest/api/content/${values["pageId"]}` };
2156
+ onSteps([...steps]);
2157
+ addLog(makeLog("run", `GET /rest/api/content/${values["pageId"]}`));
2158
+ const current = await api.getPage(String(values["pageId"]));
2159
+ addLog(makeLog("ok", `\uD604\uC7AC \uD398\uC774\uC9C0: "${current.title}" v${current.version?.number}`));
2160
+ steps[1] = { ...steps[1], state: "done", detail: `v${current.version?.number}` };
2161
+ onSteps([...steps]);
2162
+ steps[2] = { ...steps[2], state: "running" };
2163
+ onSteps([...steps]);
2164
+ let storageXml = current.body?.storage?.value ?? "";
2165
+ if (values["file"]) {
2166
+ const filePath = path4.resolve(process.cwd(), String(values["file"]));
2167
+ const markdown = fs4.readFileSync(filePath, "utf-8");
2168
+ const converter = new MarkdownToStorageConverter();
2169
+ storageXml = converter.convert(markdown);
2170
+ addLog(makeLog("ok", `Markdown \uBCC0\uD658 \uC644\uB8CC: ${filePath}`));
2171
+ }
2172
+ steps[2] = { ...steps[2], state: "done" };
2173
+ onSteps([...steps]);
2174
+ steps[3] = { ...steps[3], state: "running", detail: `PUT /rest/api/content/${values["pageId"]}` };
2175
+ onSteps([...steps]);
2176
+ addLog(makeLog("run", `PUT /rest/api/content/${values["pageId"]}`));
2177
+ const updated = await api.updatePage({
2178
+ id: String(values["pageId"]),
2179
+ title: values["title"] ? String(values["title"]) : current.title,
2180
+ body: storageXml,
2181
+ version: (current.version?.number ?? 1) + 1
2182
+ });
2183
+ addLog(makeLog("ok", `\uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC: "${updated.title}" v${updated.version?.number}`));
2184
+ steps[3] = { ...steps[3], state: "done", detail: `v${updated.version?.number}` };
2185
+ onSteps([...steps]);
2186
+ return {
2187
+ type: "text",
2188
+ content: `# \uD398\uC774\uC9C0 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC
2189
+
2190
+ - **\uC81C\uBAA9**: ${updated.title}
2191
+ - **ID**: ${updated.id}
2192
+ - **\uBC84\uC804**: v${updated.version?.number}`,
2193
+ logs
2194
+ };
2195
+ }
2196
+
2197
+ // tools/tui/executor/jira.ts
2198
+ function makeStep2(id, title, detail = "") {
2199
+ return { id, title, detail, state: "pending" };
2200
+ }
2201
+ async function executeIssueGet(values, onSteps, onLog) {
2202
+ const logs = [];
2203
+ const addLog = (l) => {
2204
+ logs.push(l);
2205
+ onLog(l);
2206
+ };
2207
+ const steps = [makeStep2("fetch", "Fetch JIRA issue")];
2208
+ onSteps([...steps]);
2209
+ if (!values["issueId"]) throw new Error("issueId is required");
2210
+ steps[0] = { ...steps[0], state: "running", detail: String(values["issueId"]) };
2211
+ onSteps([...steps]);
2212
+ addLog(makeLog("run", `GET /rest/api/2/issue/${values["issueId"]}`));
2213
+ const config = loadJiraConfig();
2214
+ const client = createJiraClient(config);
2215
+ const api = new JiraIssueApi(client);
2216
+ const issue = await api.getIssue(String(values["issueId"]));
2217
+ addLog(makeLog("ok", `200 OK \xB7 "${issue.fields?.summary}"`));
2218
+ steps[0] = { ...steps[0], state: "done" };
2219
+ onSteps([...steps]);
2220
+ const lines = [
2221
+ `# ${issue.key}: ${issue.fields?.summary ?? ""}`,
2222
+ "",
2223
+ `- **\uC0C1\uD0DC**: ${issue.fields?.status?.name ?? ""}`,
2224
+ `- **\uB2F4\uB2F9\uC790**: ${issue.fields?.assignee?.displayName ?? "\uBBF8\uC9C0\uC815"}`,
2225
+ `- **\uC6B0\uC120\uC21C\uC704**: ${issue.fields?.priority?.name ?? ""}`,
2226
+ `- **\uC774\uC288 \uC720\uD615**: ${issue.fields?.issuetype?.name ?? ""}`,
2227
+ "",
2228
+ `## \uC124\uBA85`,
2229
+ issue.fields?.description ?? "(\uC5C6\uC74C)"
2230
+ ];
2231
+ return { type: "text", content: lines.join("\n"), logs };
2232
+ }
2233
+ async function executeIssueCreate(values, onSteps, onLog) {
2234
+ const logs = [];
2235
+ const addLog = (l) => {
2236
+ logs.push(l);
2237
+ onLog(l);
2238
+ };
2239
+ const steps = [
2240
+ makeStep2("validate", "Validate arguments"),
2241
+ makeStep2("create", "Create JIRA issue")
2242
+ ];
2243
+ onSteps([...steps]);
2244
+ steps[0] = { ...steps[0], state: "running" };
2245
+ onSteps([...steps]);
2246
+ if (!values["project"]) throw new Error("--project is required");
2247
+ if (!values["summary"]) throw new Error("--summary is required");
2248
+ steps[0] = { ...steps[0], state: "done" };
2249
+ onSteps([...steps]);
2250
+ const config = loadJiraConfig();
2251
+ const client = createJiraClient(config);
2252
+ const api = new JiraIssueApi(client);
2253
+ steps[1] = { ...steps[1], state: "running", detail: `POST /rest/api/2/issue` };
2254
+ onSteps([...steps]);
2255
+ addLog(makeLog("run", `POST /rest/api/2/issue project=${values["project"]}`));
2256
+ const issue = await api.createIssue({
2257
+ projectKey: String(values["project"]),
2258
+ summary: String(values["summary"]),
2259
+ issueType: String(values["type"] ?? "Task"),
2260
+ ...values["assignee"] ? { assignee: String(values["assignee"]) } : {},
2261
+ ...values["labels"] ? { labels: String(values["labels"]).split(",").map((l) => l.trim()) } : {}
2262
+ });
2263
+ addLog(makeLog("ok", `\uC774\uC288 \uC0DD\uC131 \uC644\uB8CC: ${issue.key}`));
2264
+ steps[1] = { ...steps[1], state: "done", detail: issue.key };
2265
+ onSteps([...steps]);
2266
+ return {
2267
+ type: "text",
2268
+ content: `# \uC774\uC288 \uC0DD\uC131 \uC644\uB8CC
2269
+
2270
+ - **\uD0A4**: ${issue.key}
2271
+ - **\uC81C\uBAA9**: ${values["summary"]}`,
2272
+ logs
2273
+ };
2274
+ }
2275
+ async function executeIssueTransition(values, onSteps, onLog) {
2276
+ const logs = [];
2277
+ const addLog = (l) => {
2278
+ logs.push(l);
2279
+ onLog(l);
2280
+ };
2281
+ const steps = [makeStep2("transition", "Transition JIRA issue")];
2282
+ onSteps([...steps]);
2283
+ if (!values["issueId"] || !values["transitionId"]) throw new Error("issueId and transitionId are required");
2284
+ steps[0] = { ...steps[0], state: "running", detail: `${values["issueId"]} \u2192 ${values["transitionId"]}` };
2285
+ onSteps([...steps]);
2286
+ addLog(makeLog("run", `POST /rest/api/2/issue/${values["issueId"]}/transitions`));
2287
+ const config = loadJiraConfig();
2288
+ const client = createJiraClient(config);
2289
+ await client.post(`/rest/api/2/issue/${values["issueId"]}/transitions`, {
2290
+ transition: { id: String(values["transitionId"]) }
2291
+ });
2292
+ addLog(makeLog("ok", `\uC804\uD658 \uC644\uB8CC: ${values["issueId"]}`));
2293
+ steps[0] = { ...steps[0], state: "done" };
2294
+ onSteps([...steps]);
2295
+ return { type: "text", content: `# \uC774\uC288 \uC804\uD658 \uC644\uB8CC
2296
+
2297
+ - **\uC774\uC288**: ${values["issueId"]}
2298
+ - **Transition ID**: ${values["transitionId"]}`, logs };
2299
+ }
2300
+ async function executeJiraSearch(values, onSteps, onLog) {
2301
+ const logs = [];
2302
+ const addLog = (l) => {
2303
+ logs.push(l);
2304
+ onLog(l);
2305
+ };
2306
+ const steps = [makeStep2("search", "Search JIRA issues")];
2307
+ onSteps([...steps]);
2308
+ if (!values["jql"]) throw new Error("jql is required");
2309
+ steps[0] = { ...steps[0], state: "running", detail: String(values["jql"]) };
2310
+ onSteps([...steps]);
2311
+ addLog(makeLog("run", `GET /rest/api/2/search?jql=${encodeURIComponent(String(values["jql"]))}`));
2312
+ const config = loadJiraConfig();
2313
+ const client = createJiraClient(config);
2314
+ const api = new JiraSearchApi(client);
2315
+ const result = await api.searchByJql(String(values["jql"]), 0, Number(values["limit"] ?? 20));
2316
+ addLog(makeLog("ok", `${result.issues?.length ?? 0}\uAC1C \uC774\uC288 \uC870\uD68C \uC644\uB8CC`));
2317
+ steps[0] = { ...steps[0], state: "done", detail: `${result.issues?.length ?? 0} issues` };
2318
+ onSteps([...steps]);
2319
+ const list = (result.issues ?? []).map((i) => ({
2320
+ key: i.key ?? "",
2321
+ summary: (i.fields?.summary ?? "").slice(0, 50),
2322
+ status: i.fields?.status?.name ?? "",
2323
+ assignee: i.fields?.assignee?.displayName ?? ""
2324
+ }));
2325
+ return { type: "list", list, cols: ["key", "summary", "status", "assignee"], logs };
2326
+ }
2327
+
2328
+ // tools/tui/executor/gitlab.ts
2329
+ function makeStep3(id, title, detail = "") {
2330
+ return { id, title, detail, state: "pending" };
2331
+ }
2332
+ async function executeMrList(values, onSteps, onLog) {
2333
+ const logs = [];
2334
+ const addLog = (l) => {
2335
+ logs.push(l);
2336
+ onLog(l);
2337
+ };
2338
+ const steps = [makeStep3("fetch", "Fetch MR list")];
2339
+ onSteps([...steps]);
2340
+ if (!values["projectId"]) throw new Error("projectId is required");
2341
+ steps[0] = { ...steps[0], state: "running", detail: `project=${values["projectId"]}` };
2342
+ onSteps([...steps]);
2343
+ addLog(makeLog("run", `GET /api/v4/projects/${values["projectId"]}/merge_requests?state=${values["state"] ?? "opened"}`));
2344
+ const config = loadGitlabConfig();
2345
+ const client = createGitlabClient(config);
2346
+ const api = new GitlabMergeRequestApi(client);
2347
+ const mrs = await api.getMergeRequests(Number(values["projectId"]), {
2348
+ state: String(values["state"] ?? "opened")
2349
+ });
2350
+ addLog(makeLog("ok", `${mrs.length}\uAC1C MR \uC870\uD68C \uC644\uB8CC`));
2351
+ steps[0] = { ...steps[0], state: "done", detail: `${mrs.length} MRs` };
2352
+ onSteps([...steps]);
2353
+ const list = mrs.map((mr) => ({
2354
+ iid: String(mr.iid ?? ""),
2355
+ title: (mr.title ?? "").slice(0, 50),
2356
+ state: mr.state ?? "",
2357
+ author: mr.author?.name ?? "",
2358
+ target: mr.target_branch ?? ""
2359
+ }));
2360
+ return { type: "list", list, cols: ["iid", "title", "state", "author", "target"], logs };
2361
+ }
2362
+ async function executeMrGet(values, onSteps, onLog) {
2363
+ const logs = [];
2364
+ const addLog = (l) => {
2365
+ logs.push(l);
2366
+ onLog(l);
2367
+ };
2368
+ const steps = [makeStep3("fetch", "Fetch MR detail")];
2369
+ onSteps([...steps]);
2370
+ if (!values["projectId"] || !values["mrId"]) throw new Error("projectId and mrId are required");
2371
+ steps[0] = { ...steps[0], state: "running" };
2372
+ onSteps([...steps]);
2373
+ addLog(makeLog("run", `GET /api/v4/projects/${values["projectId"]}/merge_requests/${values["mrId"]}`));
2374
+ const config = loadGitlabConfig();
2375
+ const client = createGitlabClient(config);
2376
+ const api = new GitlabMergeRequestApi(client);
2377
+ const mr = await api.getMergeRequest(Number(values["projectId"]), Number(values["mrId"]));
2378
+ addLog(makeLog("ok", `200 OK \xB7 !${mr.iid} "${mr.title}"`));
2379
+ steps[0] = { ...steps[0], state: "done" };
2380
+ onSteps([...steps]);
2381
+ const lines = [
2382
+ `# MR !${mr.iid}: ${mr.title}`,
2383
+ "",
2384
+ `- **\uC0C1\uD0DC**: ${mr.state}`,
2385
+ `- **\uC791\uC131\uC790**: ${mr.author?.name ?? ""}`,
2386
+ `- **\uBE0C\uB79C\uCE58**: \`${mr.source_branch}\` \u2192 \`${mr.target_branch}\``,
2387
+ `- **\uC0DD\uC131\uC77C**: ${mr.created_at ?? ""}`,
2388
+ "",
2389
+ `## \uC124\uBA85`,
2390
+ mr.description ?? "(\uC5C6\uC74C)"
2391
+ ];
2392
+ return { type: "text", content: lines.join("\n"), logs };
2393
+ }
2394
+ async function executeMrCreate(values, onSteps, onLog) {
2395
+ const logs = [];
2396
+ const addLog = (l) => {
2397
+ logs.push(l);
2398
+ onLog(l);
2399
+ };
2400
+ const steps = [
2401
+ makeStep3("validate", "Validate arguments"),
2402
+ makeStep3("create", "Create MR")
2403
+ ];
2404
+ onSteps([...steps]);
2405
+ steps[0] = { ...steps[0], state: "running" };
2406
+ onSteps([...steps]);
2407
+ if (!values["projectId"] || !values["sourceBranch"]) throw new Error("projectId and sourceBranch are required");
2408
+ steps[0] = { ...steps[0], state: "done" };
2409
+ onSteps([...steps]);
2410
+ const config = loadGitlabConfig();
2411
+ const client = createGitlabClient(config);
2412
+ const api = new GitlabMergeRequestApi(client);
2413
+ steps[1] = { ...steps[1], state: "running", detail: `POST /api/v4/projects/${values["projectId"]}/merge_requests` };
2414
+ onSteps([...steps]);
2415
+ addLog(makeLog("run", `POST /api/v4/projects/${values["projectId"]}/merge_requests`));
2416
+ const mr = await api.createMergeRequest(Number(values["projectId"]), {
2417
+ source_branch: String(values["sourceBranch"]),
2418
+ target_branch: String(values["targetBranch"] ?? "main"),
2419
+ title: values["title"] ? String(values["title"]) : `Merge ${values["sourceBranch"]} \u2192 ${values["targetBranch"] ?? "main"}`
2420
+ });
2421
+ addLog(makeLog("ok", `MR \uC0DD\uC131 \uC644\uB8CC: !${mr.iid}`));
2422
+ steps[1] = { ...steps[1], state: "done", detail: `!${mr.iid}` };
2423
+ onSteps([...steps]);
2424
+ return {
2425
+ type: "text",
2426
+ content: `# MR \uC0DD\uC131 \uC644\uB8CC
2427
+
2428
+ - **\uBC88\uD638**: !${mr.iid}
2429
+ - **\uC81C\uBAA9**: ${mr.title}
2430
+ - **\uBE0C\uB79C\uCE58**: \`${mr.source_branch}\` \u2192 \`${mr.target_branch}\``,
2431
+ logs
2432
+ };
2433
+ }
2434
+ async function executePipelineGet(values, onSteps, onLog) {
2435
+ const logs = [];
2436
+ const addLog = (l) => {
2437
+ logs.push(l);
2438
+ onLog(l);
2439
+ };
2440
+ const steps = [makeStep3("fetch", "Fetch pipeline")];
2441
+ onSteps([...steps]);
2442
+ if (!values["projectId"] || !values["pipelineId"]) throw new Error("projectId and pipelineId are required");
2443
+ steps[0] = { ...steps[0], state: "running" };
2444
+ onSteps([...steps]);
2445
+ addLog(makeLog("run", `GET /api/v4/projects/${values["projectId"]}/pipelines/${values["pipelineId"]}`));
2446
+ const config = loadGitlabConfig();
2447
+ const client = createGitlabClient(config);
2448
+ const api = new GitlabPipelineApi(client);
2449
+ const pipeline = await api.getPipeline(Number(values["projectId"]), Number(values["pipelineId"]));
2450
+ addLog(makeLog("ok", `200 OK \xB7 pipeline #${pipeline.id} \xB7 ${pipeline.status}`));
2451
+ let jobsText = "";
2452
+ if (values["jobs"]) {
2453
+ const jobs = await api.getPipelineJobs(Number(values["projectId"]), Number(values["pipelineId"]));
2454
+ addLog(makeLog("ok", `${jobs.length}\uAC1C job \uC870\uD68C \uC644\uB8CC`));
2455
+ jobsText = "\n\n## Jobs\n\n" + jobs.map((j) => `- **${j.name}**: ${j.status}`).join("\n");
2456
+ }
2457
+ steps[0] = { ...steps[0], state: "done", detail: pipeline.status };
2458
+ onSteps([...steps]);
2459
+ const lines = [
2460
+ `# Pipeline #${pipeline.id}`,
2461
+ "",
2462
+ `- **\uC0C1\uD0DC**: ${pipeline.status}`,
2463
+ `- **\uBE0C\uB79C\uCE58**: ${pipeline.ref ?? ""}`,
2464
+ `- **SHA**: ${(pipeline.sha ?? "").slice(0, 8)}`,
2465
+ `- **\uC0DD\uC131\uC77C**: ${pipeline.created_at ?? ""}`,
2466
+ jobsText
2467
+ ];
2468
+ return { type: "text", content: lines.join("\n"), logs };
2469
+ }
2470
+
2471
+ // tools/tui/App.tsx
2472
+ import { jsx as jsx17 } from "react/jsx-runtime";
2473
+ var EXECUTOR_MAP = {
2474
+ "confluence:page:get": executePageGet,
2475
+ "confluence:page:create": executePageCreate,
2476
+ "confluence:page:update": executePageUpdate,
2477
+ "confluence:space:list": executeSpaceList,
2478
+ "confluence:search": executeSearch,
2479
+ "jira:issue:get": executeIssueGet,
2480
+ "jira:issue:create": executeIssueCreate,
2481
+ "jira:issue:transition": executeIssueTransition,
2482
+ "jira:search": executeJiraSearch,
2483
+ "gitlab:mr:list": executeMrList,
2484
+ "gitlab:mr:get": executeMrGet,
2485
+ "gitlab:mr:create": executeMrCreate,
2486
+ "gitlab:pipeline:get": executePipelineGet
2487
+ };
2488
+ var INITIAL_STATE = {
2489
+ screen: "menu",
2490
+ activePath: "confluence:page:get",
2491
+ expandedKeys: DEFAULT_EXPANDED,
2492
+ commandKey: "",
2493
+ formValues: {},
2494
+ formErrors: {},
2495
+ focusedField: 0,
2496
+ steps: [],
2497
+ progress: 0,
2498
+ resultText: "",
2499
+ resultList: [],
2500
+ resultListCols: [],
2501
+ resultListSelected: 0,
2502
+ logs: [makeLog("info", "session \uC2DC\uC791 \xB7 tdecollab TUI v0.2.3")],
2503
+ history: loadHistory(),
2504
+ historySelected: 0
2505
+ };
2506
+ function App({ config }) {
2507
+ const { exit } = useApp();
2508
+ const { stdout } = useStdout2();
2509
+ const accent = config.accent ?? DEFAULT_ACCENT;
2510
+ const [state, setState] = useState8(INITIAL_STATE);
2511
+ const stateRef = useRef(state);
2512
+ stateRef.current = state;
2513
+ const [size, setSize] = useState8({
2514
+ cols: stdout?.columns ?? 120,
2515
+ rows: stdout?.rows ?? 40
2516
+ });
2517
+ useEffect3(() => {
2518
+ if (!stdout) return;
2519
+ const onResize = () => setSize({ cols: stdout.columns, rows: stdout.rows });
2520
+ stdout.on("resize", onResize);
2521
+ return () => {
2522
+ stdout.off("resize", onResize);
2523
+ };
2524
+ }, [stdout]);
2525
+ const addLog = useCallback2((log) => {
2526
+ setState((s) => ({ ...s, logs: [...s.logs, log] }));
2527
+ }, []);
2528
+ const handleSelectCommand = useCallback2((commandKey) => {
2529
+ const def = COMMANDS[commandKey];
2530
+ if (!def) return;
2531
+ const lastUsed = config.lastUsed?.[commandKey] ?? {};
2532
+ const initValues = {};
2533
+ for (const f of def.fields) {
2534
+ initValues[f.key] = f.key in lastUsed ? lastUsed[f.key] : f.defaultValue ?? "";
2535
+ }
2536
+ const parents = getParentMenuKeys(commandKey);
2537
+ setState((s) => ({
2538
+ ...s,
2539
+ screen: "form",
2540
+ commandKey,
2541
+ formValues: initValues,
2542
+ formErrors: {},
2543
+ focusedField: 0,
2544
+ activePath: commandKey,
2545
+ expandedKeys: Array.from(/* @__PURE__ */ new Set([...s.expandedKeys, ...parents]))
2546
+ }));
2547
+ addLog(makeLog("dim", `\uD3FC \uC5F4\uAE30: ${commandKey}`));
2548
+ }, [config.lastUsed, addLog]);
2549
+ const handleToggleExpanded = useCallback2((key) => {
2550
+ setState((s) => ({
2551
+ ...s,
2552
+ expandedKeys: s.expandedKeys.includes(key) ? s.expandedKeys.filter((k) => k !== key) : [...s.expandedKeys, key]
2553
+ }));
2554
+ }, []);
2555
+ const handleRun = useCallback2(async (values) => {
2556
+ const commandKey = stateRef.current.commandKey;
2557
+ const executor = EXECUTOR_MAP[commandKey];
2558
+ if (!executor) {
2559
+ addLog(makeLog("err", `\uC2E4\uD589\uAE30\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${commandKey}`));
2560
+ return;
2561
+ }
2562
+ const startTime = Date.now();
2563
+ setState((s) => ({
2564
+ ...s,
2565
+ screen: "running",
2566
+ formValues: values,
2567
+ steps: [],
2568
+ progress: 0
2569
+ }));
2570
+ addLog(makeLog("run", `tdecollab ${commandKey.split(":").join(" ")}`));
2571
+ try {
2572
+ const result = await executor(
2573
+ values,
2574
+ (steps) => setState((s) => ({ ...s, steps })),
2575
+ (log) => setState((s) => ({ ...s, logs: [...s.logs, log] }))
2576
+ );
2577
+ const dur = `${((Date.now() - startTime) / 1e3).toFixed(1)}s`;
2578
+ const def = COMMANDS[commandKey];
2579
+ const svc = def?.svc === "confluence" ? "cf" : def?.svc === "jira" ? "jr" : "gl";
2580
+ const histEntry = {
2581
+ when: (/* @__PURE__ */ new Date()).toTimeString().slice(0, 5),
2582
+ svc,
2583
+ cmd: commandKey.split(":").join(" "),
2584
+ state: "ok",
2585
+ dur,
2586
+ result: result.type === "list" ? `${result.list?.length ?? 0} items` : "done"
2587
+ };
2588
+ appendHistory(histEntry);
2589
+ saveLastUsed(commandKey, values);
2590
+ if (result.type === "list") {
2591
+ setState((s) => ({
2592
+ ...s,
2593
+ screen: "result-list",
2594
+ resultList: result.list ?? [],
2595
+ resultListCols: result.cols ?? [],
2596
+ resultListSelected: 0,
2597
+ logs: [...s.logs, makeLog("ok", `\uC644\uB8CC (${dur})`)],
2598
+ history: [histEntry, ...s.history]
2599
+ }));
2600
+ } else {
2601
+ setState((s) => ({
2602
+ ...s,
2603
+ screen: "result-text",
2604
+ resultText: result.content ?? "",
2605
+ logs: [...s.logs, makeLog("ok", `\uC644\uB8CC (${dur})`)],
2606
+ history: [histEntry, ...s.history]
2607
+ }));
2608
+ }
2609
+ } catch (err) {
2610
+ const msg = err instanceof Error ? err.message : String(err);
2611
+ const dur = `${((Date.now() - startTime) / 1e3).toFixed(1)}s`;
2612
+ const def = COMMANDS[commandKey];
2613
+ const svc = def?.svc === "confluence" ? "cf" : def?.svc === "jira" ? "jr" : "gl";
2614
+ const histEntry = {
2615
+ when: (/* @__PURE__ */ new Date()).toTimeString().slice(0, 5),
2616
+ svc,
2617
+ cmd: commandKey.split(":").join(" "),
2618
+ state: "err",
2619
+ dur,
2620
+ result: msg.slice(0, 40)
2621
+ };
2622
+ appendHistory(histEntry);
2623
+ setState((s) => ({
2624
+ ...s,
2625
+ screen: "error",
2626
+ formErrors: { error: msg },
2627
+ logs: [...s.logs, makeLog("err", msg)],
2628
+ history: [histEntry, ...s.history]
2629
+ }));
2630
+ }
2631
+ }, [addLog]);
2632
+ const handleBack = useCallback2(() => {
2633
+ setState((s) => ({ ...s, screen: "menu" }));
2634
+ }, []);
2635
+ const handleOpenHistory = useCallback2(() => {
2636
+ setState((s) => ({ ...s, screen: "history", history: loadHistory() }));
2637
+ }, []);
2638
+ const handleReplay = useCallback2((entry) => {
2639
+ const commandKey = entry.cmd.replace(/\s+/g, ":");
2640
+ handleSelectCommand(commandKey);
2641
+ }, [handleSelectCommand]);
2642
+ const handleSavePreset = useCallback2((values) => {
2643
+ const commandKey = stateRef.current.commandKey;
2644
+ saveLastUsed(commandKey, values);
2645
+ addLog(makeLog("ok", `\uD504\uB9AC\uC14B \uC800\uC7A5: ${commandKey}`));
2646
+ }, [addLog]);
2647
+ const handleQuit = useCallback2(() => exit(), [exit]);
2648
+ let screen;
2649
+ switch (state.screen) {
2650
+ case "menu":
2651
+ screen = /* @__PURE__ */ jsx17(
2652
+ MenuScreen,
2653
+ {
2654
+ state,
2655
+ onSelectCommand: handleSelectCommand,
2656
+ onToggleExpanded: handleToggleExpanded,
2657
+ onOpenHistory: handleOpenHistory,
2658
+ onQuit: handleQuit,
2659
+ accent
2660
+ }
2661
+ );
2662
+ break;
2663
+ case "form":
2664
+ screen = /* @__PURE__ */ jsx17(
2665
+ FormScreen,
2666
+ {
2667
+ state,
2668
+ onRun: handleRun,
2669
+ onBack: handleBack,
2670
+ onSavePreset: handleSavePreset,
2671
+ accent
2672
+ }
2673
+ );
2674
+ break;
2675
+ case "running":
2676
+ screen = /* @__PURE__ */ jsx17(RunningScreen, { state, onCancel: handleBack, accent });
2677
+ break;
2678
+ case "result-list":
2679
+ screen = /* @__PURE__ */ jsx17(ListScreen, { state, onBack: handleBack, accent });
2680
+ break;
2681
+ case "result-text":
2682
+ screen = /* @__PURE__ */ jsx17(TextView, { state, onBack: handleBack, accent });
2683
+ break;
2684
+ case "error":
2685
+ screen = /* @__PURE__ */ jsx17(
2686
+ ErrorScreen,
2687
+ {
2688
+ state,
2689
+ onBack: () => setState((s) => ({ ...s, screen: "form" })),
2690
+ onRetry: () => handleRun(state.formValues),
2691
+ accent
2692
+ }
2693
+ );
2694
+ break;
2695
+ case "history":
2696
+ screen = /* @__PURE__ */ jsx17(HistoryScreen, { state, onBack: handleBack, onReplay: handleReplay, accent });
2697
+ break;
2698
+ default:
2699
+ screen = /* @__PURE__ */ jsx17(
2700
+ MenuScreen,
2701
+ {
2702
+ state,
2703
+ onSelectCommand: handleSelectCommand,
2704
+ onToggleExpanded: handleToggleExpanded,
2705
+ onOpenHistory: handleOpenHistory,
2706
+ onQuit: handleQuit,
2707
+ accent
2708
+ }
2709
+ );
2710
+ }
2711
+ return /* @__PURE__ */ jsx17(Box16, { width: size.cols, height: size.rows, flexDirection: "column", children: screen });
2712
+ }
2713
+
2714
+ // tools/tui/index.tsx
2715
+ import { jsx as jsx18 } from "react/jsx-runtime";
2716
+ var ENTER_ALT_SCREEN = "\x1B[?1049h\x1B[H";
2717
+ var LEAVE_ALT_SCREEN = "\x1B[?1049l";
2718
+ var HIDE_CURSOR = "\x1B[?25l";
2719
+ var SHOW_CURSOR = "\x1B[?25h";
2720
+ var cleanedUp = false;
2721
+ function cleanup() {
2722
+ if (cleanedUp) return;
2723
+ cleanedUp = true;
2724
+ process.stdout.write(SHOW_CURSOR + LEAVE_ALT_SCREEN);
2725
+ }
2726
+ async function main() {
2727
+ const config = await loadTuiConfig();
2728
+ process.stdout.write(ENTER_ALT_SCREEN + HIDE_CURSOR);
2729
+ process.on("exit", cleanup);
2730
+ process.on("SIGINT", () => {
2731
+ cleanup();
2732
+ process.exit(130);
2733
+ });
2734
+ process.on("SIGTERM", () => {
2735
+ cleanup();
2736
+ process.exit(143);
2737
+ });
2738
+ process.on("uncaughtException", (err) => {
2739
+ cleanup();
2740
+ console.error(err);
2741
+ process.exit(1);
2742
+ });
2743
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx18(App, { config }), {
2744
+ exitOnCtrlC: true
2745
+ });
2746
+ await waitUntilExit();
2747
+ cleanup();
2748
+ }
2749
+ main().catch((err) => {
2750
+ cleanup();
2751
+ console.error("TUI \uC2DC\uC791 \uC2E4\uD328:", err);
2752
+ process.exit(1);
2753
+ });
2754
+ //# sourceMappingURL=index.js.map