redmine-context 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Handlers dos comandos do CLI (M1-11): `issue` e `login`.
3
+ *
4
+ * Consomem EXCLUSIVAMENTE a superfície pública do core (`../../index.js`,
5
+ * fronteira do ADR-005) — a orquestração `fetchIssueBundle`, o `loginWithPassword`
6
+ * e a cascata de credenciais. Cada handler devolve o exit code do processo; o
7
+ * mapeamento status→código vive em {@link exitCodeForError} e é documentado no
8
+ * `--help`.
9
+ */
10
+ import { mkdir, writeFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import * as core from '../../index.js';
13
+ /** Exit codes do CLI (documentados no `--help`). */
14
+ export const EXIT = {
15
+ /** Erro genérico ou uso inválido. */
16
+ GENERIC: 1,
17
+ /** Falha de autenticação ou credencial ausente. */
18
+ AUTH: 2,
19
+ /** Erro de rede ou HTTP. */
20
+ NETWORK: 3,
21
+ /** Issue inexistente. */
22
+ NOT_FOUND: 4,
23
+ };
24
+ /** Extrai uma mensagem legível de um erro desconhecido. */
25
+ function messageOf(error) {
26
+ return error instanceof Error ? error.message : String(error);
27
+ }
28
+ /**
29
+ * Mapeia um erro para o exit code do CLI.
30
+ *
31
+ * Ordem importa: os erros específicos (404, 401) estendem `RedmineHttpError` e
32
+ * precisam ser testados antes do genérico HTTP. Erros de rede (sem status) são
33
+ * detectados pela mensagem do client e também caem em {@link EXIT.NETWORK}.
34
+ *
35
+ * @param error - Erro capturado durante a operação.
36
+ * @returns O exit code correspondente.
37
+ */
38
+ export function exitCodeForError(error) {
39
+ if (error instanceof core.RedmineNotFoundError)
40
+ return EXIT.NOT_FOUND;
41
+ if (error instanceof core.RedmineAuthError)
42
+ return EXIT.AUTH;
43
+ if (error instanceof core.RedmineLoginError)
44
+ return EXIT.AUTH;
45
+ if (error instanceof core.RedmineHttpError)
46
+ return EXIT.NETWORK;
47
+ if (error instanceof Error && /falha de rede|network|fetch failed|econn|enotfound|etimedout/i.test(error.message)) {
48
+ return EXIT.NETWORK;
49
+ }
50
+ return EXIT.GENERIC;
51
+ }
52
+ /** Lê uma flag com valor string, ou `undefined` se ausente/booleana. */
53
+ function stringFlag(parsed, name) {
54
+ const value = parsed.flags.get(name);
55
+ return typeof value === 'string' ? value : undefined;
56
+ }
57
+ /**
58
+ * Comando `issue <id>`: resolve credencial pela cascata, empacota e emite o
59
+ * bundle (stdout ou `--out <dir>`), com progresso em stderr.
60
+ *
61
+ * @param parsed - Argumentos parseados (posicional `<id>` + flags).
62
+ * @param deps - Dependências injetáveis (I/O, env).
63
+ * @returns Exit code do processo.
64
+ */
65
+ export async function runIssue(parsed, deps) {
66
+ const idRaw = parsed.positionals[1];
67
+ const issueId = Number(idRaw);
68
+ if (idRaw === undefined || !Number.isInteger(issueId) || issueId <= 0) {
69
+ deps.stderr(`Id de issue inválido: ${idRaw ?? '(ausente)'}. Uso: redmine-context issue <id>\n`);
70
+ return EXIT.GENERIC;
71
+ }
72
+ // Instância: --url → REDMINE_URL → URL persistida no login (#187).
73
+ const persistedUrl = deps.settings ? await deps.settings.getInstanceUrl() : undefined;
74
+ const resolved = core.resolveInstanceUrl({
75
+ flagUrl: stringFlag(parsed, 'url'),
76
+ envUrl: deps.env.REDMINE_URL,
77
+ persistedUrl,
78
+ });
79
+ if (resolved === undefined) {
80
+ deps.stderr('Instância não configurada. Use --url <url>, defina REDMINE_URL, ou rode `redmine-context login`.\n');
81
+ return EXIT.GENERIC;
82
+ }
83
+ const baseUrl = resolved.url;
84
+ const insecure = parsed.flags.get('insecure') === true;
85
+ let apiKey;
86
+ try {
87
+ apiKey = await core.resolveApiKey(baseUrl, {
88
+ env: deps.env,
89
+ // SEGURANÇA (#187): se a URL veio do settings.json persistido (origem
90
+ // `config`, fonte mutável), NÃO usar a REDMINE_API_KEY instance-agnóstica —
91
+ // só credencial pinada à instância (keychain/arquivo). URL de --url/env é
92
+ // confiável (mesma invocação), então mantém o fallback de ambiente.
93
+ allowEnvFallback: resolved.origin !== 'config',
94
+ logger: { warn: (message) => deps.stderr(`${message}\n`) },
95
+ });
96
+ }
97
+ catch (error) {
98
+ deps.stderr(`${messageOf(error)}\n`);
99
+ return exitCodeForError(error);
100
+ }
101
+ if (apiKey === undefined) {
102
+ deps.stderr(`Nenhuma credencial encontrada para ${baseUrl}.\nRode: redmine-context login\n`);
103
+ return EXIT.AUTH;
104
+ }
105
+ const format = parsed.flags.get('json') === true ? 'json' : 'md';
106
+ const outDir = stringFlag(parsed, 'out');
107
+ // --extract liga a extração de texto dos anexos (OCR) no bundle (M3-13).
108
+ const extractAttachments = parsed.flags.get('extract') === true;
109
+ try {
110
+ let content;
111
+ for await (const event of core.fetchIssueBundle({
112
+ baseUrl,
113
+ apiKey,
114
+ issueId,
115
+ format,
116
+ toolVersion: core.TOOL_VERSION,
117
+ insecure,
118
+ extractAttachments,
119
+ })) {
120
+ if (event.kind === 'progress') {
121
+ deps.stderr(`... ${event.message}\n`);
122
+ }
123
+ else {
124
+ content = event.value.content;
125
+ }
126
+ }
127
+ if (content === undefined) {
128
+ deps.stderr('Operação não produziu um bundle.\n');
129
+ return EXIT.GENERIC;
130
+ }
131
+ if (outDir !== undefined) {
132
+ const ext = format === 'json' ? 'json' : 'md';
133
+ const filePath = join(outDir, `${issueId}.${ext}`);
134
+ await mkdir(outDir, { recursive: true });
135
+ await writeFile(filePath, content, 'utf8');
136
+ deps.stderr(`Bundle gravado em ${filePath}\n`);
137
+ }
138
+ else {
139
+ deps.stdout(content);
140
+ }
141
+ return 0;
142
+ }
143
+ catch (error) {
144
+ deps.stderr(`${messageOf(error)}\n`);
145
+ return exitCodeForError(error);
146
+ }
147
+ }
148
+ /**
149
+ * Comando `doctor`: diagnostica os binários de mídia (hoje o `tesseract`) e
150
+ * imprime um relatório em TEXTO PURO no stdout. Degrada naturalmente em
151
+ * `NO_COLOR`/não-TTY (não emite cor/ANSI). Exit 0 se todos presentes, 1 se
152
+ * faltar algum — o exit code deixa o resultado programável em scripts.
153
+ *
154
+ * @param _parsed - Argumentos parseados (o comando não usa flags hoje).
155
+ * @param deps - Dependências injetáveis (I/O).
156
+ * @returns Exit code do processo (0 = tudo ok, 1 = binário faltando).
157
+ */
158
+ export async function runDoctor(_parsed, deps) {
159
+ const diagnoses = await core.diagnoseBinaries();
160
+ deps.stdout('Binários de mídia:\n');
161
+ let allPresent = true;
162
+ for (const binary of diagnoses) {
163
+ if (binary.found) {
164
+ const version = binary.version !== undefined ? ` v${binary.version}` : '';
165
+ const location = binary.path !== undefined ? ` (${binary.path})` : '';
166
+ deps.stdout(` [ok] ${binary.name}${version}${location}\n`);
167
+ }
168
+ else {
169
+ allPresent = false;
170
+ deps.stdout(` [faltando] ${binary.name} — instale com: ${binary.installHint}\n`);
171
+ }
172
+ }
173
+ return allPresent ? 0 : EXIT.GENERIC;
174
+ }
175
+ /**
176
+ * Descobre a api_key interativamente: login por senha e, no fallback de 2FA
177
+ * (ou senha inválida), orienta colar a api_key.
178
+ *
179
+ * @param baseUrl - URL da instância Redmine.
180
+ * @param insecure - Permite `http://` sem TLS.
181
+ * @param deps - Dependências injetáveis (prompts, I/O).
182
+ * @returns A api_key resolvida.
183
+ * @throws {RedmineLoginError | RedmineAuthError | Error} Se o login falhar sem
184
+ * fallback utilizável.
185
+ */
186
+ async function resolveKeyInteractively(baseUrl, insecure, deps) {
187
+ const username = (await deps.prompt('Usuário: ')).trim();
188
+ const password = await deps.promptPassword('Senha: ');
189
+ try {
190
+ const result = await core.loginWithPassword({ baseUrl, username, password, insecure });
191
+ return result.apiKey;
192
+ }
193
+ catch (error) {
194
+ if (error instanceof core.RedmineAuthError) {
195
+ deps.stderr(`${messageOf(error)}\n`);
196
+ deps.stderr('Cole sua api_key para concluir o login (conta com 2FA ou senha inválida):\n');
197
+ const pasted = (await deps.promptPassword('api_key: ')).trim();
198
+ if (pasted.length === 0) {
199
+ throw error;
200
+ }
201
+ return pasted;
202
+ }
203
+ throw error;
204
+ }
205
+ }
206
+ /**
207
+ * Comando `login`: autentica (senha ou `--api-key`) e salva a api_key na
208
+ * cascata para a instância informada.
209
+ *
210
+ * @param parsed - Argumentos parseados (flags `--url`, `--api-key`, `--insecure`).
211
+ * @param deps - Dependências injetáveis (prompts, I/O, env).
212
+ * @returns Exit code do processo.
213
+ */
214
+ export async function runLogin(parsed, deps) {
215
+ const insecure = parsed.flags.get('insecure') === true;
216
+ // Mesma precedência do resto (--url → REDMINE_URL → persistida), tratando
217
+ // string vazia como ausente via o resolvedor único (#187).
218
+ const persistedUrl = deps.settings ? await deps.settings.getInstanceUrl() : undefined;
219
+ let baseUrl = core.resolveInstanceUrl({
220
+ flagUrl: stringFlag(parsed, 'url'),
221
+ envUrl: deps.env.REDMINE_URL,
222
+ persistedUrl,
223
+ })?.url;
224
+ if (baseUrl === undefined || baseUrl.length === 0) {
225
+ baseUrl = (await deps.prompt('URL do Redmine: ')).trim();
226
+ }
227
+ if (baseUrl.length === 0) {
228
+ deps.stderr('URL do Redmine é obrigatória.\n');
229
+ return EXIT.GENERIC;
230
+ }
231
+ const directKey = stringFlag(parsed, 'api-key');
232
+ try {
233
+ const apiKey = directKey !== undefined && directKey.length > 0
234
+ ? directKey
235
+ : await resolveKeyInteractively(baseUrl, insecure, deps);
236
+ if (apiKey.length === 0) {
237
+ deps.stderr('api_key vazia; login abortado.\n');
238
+ return EXIT.AUTH;
239
+ }
240
+ // Persiste preferindo o keychain do SO (fallback arquivo se indisponível).
241
+ await core
242
+ .createCredentialCascade({
243
+ env: deps.env,
244
+ logger: { warn: (message) => deps.stderr(`${message}\n`) },
245
+ })
246
+ .set(baseUrl, apiKey);
247
+ // Persiste a instância (#187) para que TUI/CLI/MCP não dependam de REDMINE_URL.
248
+ // Falha ao salvar a instância NÃO invalida o login (a credencial já foi salva).
249
+ try {
250
+ await deps.settings?.setInstanceUrl(baseUrl);
251
+ }
252
+ catch (error) {
253
+ deps.stderr(`aviso: não foi possível salvar a instância padrão: ${messageOf(error)}\n`);
254
+ }
255
+ deps.stderr(`Credencial salva para ${baseUrl}.\n`);
256
+ return 0;
257
+ }
258
+ catch (error) {
259
+ deps.stderr(`${messageOf(error)}\n`);
260
+ return exitCodeForError(error);
261
+ }
262
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point do CLI `redmine-context` (M1-11).
4
+ *
5
+ * Superfície fina (ADR-005): faz parse manual de `argv` — sem framework de CLI,
6
+ * são apenas dois comandos —, despacha para os handlers em `./commands.js` e
7
+ * devolve o exit code. Progresso vai para stderr; o bundle vai para stdout.
8
+ *
9
+ * O core é consumido apenas pela superfície pública (`../../index.js`); nenhum
10
+ * módulo interno é importado aqui (regra eslint `no-restricted-imports`).
11
+ */
12
+ import type { ParsedArgs, RunDeps } from './types.js';
13
+ /**
14
+ * Parse manual de `argv` (após o nome do comando).
15
+ *
16
+ * Reconhece `--flag valor` para as flags de {@link VALUE_FLAGS} e `--flag`/`-h`
17
+ * como booleanas; o restante são posicionais.
18
+ *
19
+ * @param argv - Argumentos após `node main.js`.
20
+ * @returns Posicionais e flags. Ver {@link ParsedArgs}.
21
+ */
22
+ export declare function parseArgs(argv: string[]): ParsedArgs;
23
+ /**
24
+ * Ponto de entrada testável: faz parse, despacha o comando e devolve o exit code.
25
+ *
26
+ * @param argv - Argumentos após `node main.js` (ex.: `process.argv.slice(2)`).
27
+ * @param overrides - Dependências injetáveis (I/O, prompts, env) para testes.
28
+ * @returns O exit code do processo (0 = sucesso).
29
+ * @example
30
+ * const code = await run(['issue', '42', '--url', 'https://redmine.example']);
31
+ */
32
+ export declare function run(argv: string[], overrides?: Partial<RunDeps>): Promise<number>;
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point do CLI `redmine-context` (M1-11).
4
+ *
5
+ * Superfície fina (ADR-005): faz parse manual de `argv` — sem framework de CLI,
6
+ * são apenas dois comandos —, despacha para os handlers em `./commands.js` e
7
+ * devolve o exit code. Progresso vai para stderr; o bundle vai para stdout.
8
+ *
9
+ * O core é consumido apenas pela superfície pública (`../../index.js`); nenhum
10
+ * módulo interno é importado aqui (regra eslint `no-restricted-imports`).
11
+ */
12
+ import { realpathSync } from 'node:fs';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { runDoctor, runIssue, runLogin } from './commands.js';
15
+ import { createPromptSession } from './prompts.js';
16
+ import { shouldRenderTui } from './tty.js';
17
+ import { TOOL_VERSION, defaultSettingsStore } from '../../index.js';
18
+ import { runStdioServer } from '../mcp/server.js';
19
+ import { runTui } from '../tui/index.js';
20
+ /** Flags que consomem o próximo token como valor (as demais são booleanas). */
21
+ const VALUE_FLAGS = new Set(['out', 'url', 'api-key']);
22
+ /** Texto do `--help` — limpo, sem referências a ferramentas de IA. */
23
+ const HELP = `redmine-context — contexto completo de issues do Redmine para LLMs
24
+
25
+ Uso:
26
+ redmine-context issue <id> [opções]
27
+ redmine-context login [opções]
28
+ redmine-context doctor
29
+ redmine-context mcp
30
+ redmine-context --version (imprime a versão e sai)
31
+ redmine-context --help (imprime esta ajuda e sai)
32
+
33
+ Comando issue:
34
+ Busca a issue, normaliza e imprime o bundle em stdout (Markdown por padrão).
35
+ O progresso da operação é enviado para stderr.
36
+
37
+ --json Emite o bundle JSON canônico em vez de Markdown.
38
+ --extract Extrai o texto (OCR) dos anexos de imagem e o embute no
39
+ bundle (adiciona latência de download+OCR; requer tesseract).
40
+ --out <dir> Grava o bundle em <dir>/<id>.md|.json em vez de stdout.
41
+ --url <url> URL da instância Redmine (ou defina REDMINE_URL).
42
+ --insecure Permite http:// sem TLS (não recomendado).
43
+
44
+ Comando login:
45
+ Autentica por usuário/senha e salva a api_key para a instância.
46
+ Em contas com 2FA, cole a api_key quando solicitado (ou use --api-key).
47
+
48
+ --url <url> URL da instância Redmine.
49
+ --api-key <key> Salva a api_key diretamente, sem senha.
50
+ --insecure Permite http:// sem TLS (não recomendado).
51
+
52
+ Comando doctor:
53
+ Diagnostica os binários de mídia (ex.: tesseract p/ OCR) e imprime um
54
+ relatório em texto puro. Mostra caminho e versão quando presentes, ou a
55
+ instrução de instalação do seu SO quando ausentes. Exit 0 se tudo ok, 1 se
56
+ faltar algum.
57
+
58
+ Comando mcp:
59
+ Sobe um servidor MCP (stdio) expondo as tools read-only get_issue_context,
60
+ search_issues e get_attachment_text. A instância vem de REDMINE_URL +
61
+ credencial da cascata (REDMINE_API_KEY); nenhuma tool aceita URL/host
62
+ arbitrário. Logs vão para stderr.
63
+
64
+ Credenciais:
65
+ A api_key é resolvida na ordem: keychain do sistema -> arquivo de
66
+ credenciais -> REDMINE_API_KEY (env). O login salva no keychain quando
67
+ disponível e migra credenciais antigas do arquivo automaticamente.
68
+ Sem credencial, rode: redmine-context login
69
+
70
+ Exit codes:
71
+ 0 sucesso
72
+ 1 erro genérico ou uso inválido
73
+ 2 falha de autenticação ou credencial ausente
74
+ 3 erro de rede ou HTTP
75
+ 4 issue inexistente
76
+ `;
77
+ /**
78
+ * Parse manual de `argv` (após o nome do comando).
79
+ *
80
+ * Reconhece `--flag valor` para as flags de {@link VALUE_FLAGS} e `--flag`/`-h`
81
+ * como booleanas; o restante são posicionais.
82
+ *
83
+ * @param argv - Argumentos após `node main.js`.
84
+ * @returns Posicionais e flags. Ver {@link ParsedArgs}.
85
+ */
86
+ export function parseArgs(argv) {
87
+ const positionals = [];
88
+ const flags = new Map();
89
+ for (let i = 0; i < argv.length; i++) {
90
+ const token = argv[i];
91
+ if (token === undefined)
92
+ continue;
93
+ if (token === '-h') {
94
+ flags.set('help', true);
95
+ continue;
96
+ }
97
+ if (token === '-v') {
98
+ flags.set('version', true);
99
+ continue;
100
+ }
101
+ if (token.startsWith('--')) {
102
+ const name = token.slice(2);
103
+ if (VALUE_FLAGS.has(name)) {
104
+ const value = argv[i + 1];
105
+ flags.set(name, value ?? '');
106
+ i++;
107
+ }
108
+ else {
109
+ flags.set(name, true);
110
+ }
111
+ continue;
112
+ }
113
+ positionals.push(token);
114
+ }
115
+ return { positionals, flags };
116
+ }
117
+ /**
118
+ * Constrói as dependências default apontando para o processo real.
119
+ *
120
+ * Os prompts compartilham UMA sessão readline (bug #106): interfaces
121
+ * sucessivas sobre o mesmo stdin perdem entrada e pausam o stream. A sessão é
122
+ * lazy (comandos não interativos nunca tocam o stdin) e é fechada pelo
123
+ * chamador via `closePrompts`.
124
+ */
125
+ function defaultDeps() {
126
+ const session = createPromptSession();
127
+ return {
128
+ stdout: (text) => void process.stdout.write(text),
129
+ stderr: (text) => void process.stderr.write(text),
130
+ env: process.env,
131
+ prompt: (q) => session.prompt(q),
132
+ promptPassword: (q) => session.promptPassword(q),
133
+ isTTY: process.stdout.isTTY === true,
134
+ settings: defaultSettingsStore(),
135
+ closePrompts: () => session.close(),
136
+ };
137
+ }
138
+ /**
139
+ * Ponto de entrada testável: faz parse, despacha o comando e devolve o exit code.
140
+ *
141
+ * @param argv - Argumentos após `node main.js` (ex.: `process.argv.slice(2)`).
142
+ * @param overrides - Dependências injetáveis (I/O, prompts, env) para testes.
143
+ * @returns O exit code do processo (0 = sucesso).
144
+ * @example
145
+ * const code = await run(['issue', '42', '--url', 'https://redmine.example']);
146
+ */
147
+ export async function run(argv, overrides = {}) {
148
+ const defaults = defaultDeps();
149
+ const deps = { ...defaults, ...overrides };
150
+ try {
151
+ return await dispatch(argv, deps);
152
+ }
153
+ finally {
154
+ // Sem isso, a interface readline mantém o processo vivo após o login.
155
+ defaults.closePrompts();
156
+ }
157
+ }
158
+ /** Despacha o comando parseado — extraído para o run() poder fechar a sessão. */
159
+ async function dispatch(argv, deps) {
160
+ const parsed = parseArgs(argv);
161
+ const command = parsed.positionals[0];
162
+ if (parsed.flags.get('version') === true || command === 'version') {
163
+ deps.stdout(`${TOOL_VERSION}\n`);
164
+ return 0;
165
+ }
166
+ if (parsed.flags.get('help') === true || command === 'help') {
167
+ deps.stdout(HELP);
168
+ return 0;
169
+ }
170
+ if (command === undefined) {
171
+ // Sem positionals: abre a TUI (M2-01) somente quando shouldRenderTui()
172
+ // aprova (M2-03) — NO_COLOR, CI=true ou stdout não-TTY sempre degradam
173
+ // para o caminho de texto puro da CLI (help em stderr, exit 1).
174
+ if (shouldRenderTui(deps.env, deps.isTTY === true)) {
175
+ return runTui();
176
+ }
177
+ deps.stderr(HELP);
178
+ return 1;
179
+ }
180
+ if (command === 'issue') {
181
+ return runIssue(parsed, deps);
182
+ }
183
+ if (command === 'login') {
184
+ return runLogin(parsed, deps);
185
+ }
186
+ if (command === 'doctor') {
187
+ return runDoctor(parsed, deps);
188
+ }
189
+ if (command === 'mcp') {
190
+ // Wire fino: o server bloqueia até o transporte stdio fechar (então exit 0).
191
+ await runStdioServer({ env: deps.env });
192
+ return 0;
193
+ }
194
+ deps.stderr(`Comando desconhecido: ${command}\n\n${HELP}`);
195
+ return 1;
196
+ }
197
+ /* c8 ignore start -- wire de execução; coberto pelo teste E2E real (#20). */
198
+ // O npm expõe o `bin` como SYMLINK (node_modules/.bin/redmine-context) — é assim
199
+ // que `npx` e `npm i -g` invocam. `import.meta.url` é o realpath do arquivo, mas
200
+ // `process.argv[1]` é o caminho do symlink; sem resolver o realpath, o guard não
201
+ // dispararia via npx e o CLI/MCP sairia vazio (#76). `realpathSync` alinha os dois.
202
+ const invokedPath = process.argv[1];
203
+ const invokedRealPath = invokedPath !== undefined ? realpathSync(invokedPath) : undefined;
204
+ if (invokedRealPath !== undefined && import.meta.url === pathToFileURL(invokedRealPath).href) {
205
+ run(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
206
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
207
+ process.exit(1);
208
+ });
209
+ }
210
+ /* c8 ignore stop */
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Prompts interativos do CLI (M1-11) — leitura de linha e senha mascarada.
3
+ *
4
+ * Implementação mínima sobre o `readline` nativo (sem dependência de terceiros):
5
+ * `prompt` lê uma linha com echo normal; `promptPassword` desliga o echo dos
6
+ * caracteres digitados (sem eco — a senha nunca aparece no terminal nem em logs).
7
+ * Os streams de entrada/saída são injetáveis para permitir testes determinísticos.
8
+ */
9
+ /** Streams de I/O do prompt; default: `process.stdin` / `process.stdout`. */
10
+ export interface PromptIo {
11
+ /** Stream de entrada de onde a resposta é lida. */
12
+ input: NodeJS.ReadableStream;
13
+ /** Stream de saída onde a pergunta (e o echo) é escrita. */
14
+ output: NodeJS.WritableStream;
15
+ }
16
+ /**
17
+ * Sessão de prompts compartilhando UMA única `readline.Interface`.
18
+ *
19
+ * Abrir e fechar interfaces sucessivas sobre o mesmo stdin perde entrada
20
+ * bufferizada e pausa o stream (bug #106: o login travava entre o prompt de
21
+ * usuário e o de senha). A sessão cria a interface de forma lazy no primeiro
22
+ * prompt e a mantém viva até `close()`.
23
+ */
24
+ export interface PromptSession {
25
+ /** Lê uma linha com echo normal. */
26
+ prompt(question: string): Promise<string>;
27
+ /** Lê uma linha sensível SEM ecoar os caracteres. */
28
+ promptPassword(question: string): Promise<string>;
29
+ /** Encerra a interface (idempotente). Obrigatório ao fim do fluxo. */
30
+ close(): void;
31
+ }
32
+ /**
33
+ * Cria uma {@link PromptSession} sobre os streams informados.
34
+ *
35
+ * @param io - Streams de I/O (ver {@link PromptIo}). Default: stdin/stdout.
36
+ * @returns Sessão com `prompt`/`promptPassword`/`close`.
37
+ * @example
38
+ * const session = createPromptSession();
39
+ * const user = await session.prompt('Usuário: ');
40
+ * const pass = await session.promptPassword('Senha: ');
41
+ * session.close();
42
+ */
43
+ export declare function createPromptSession(io?: PromptIo): PromptSession;
44
+ /**
45
+ * Lê uma linha do usuário com echo normal.
46
+ *
47
+ * @param question - Texto exibido antes da entrada (ex.: `'Usuário: '`).
48
+ * @param io - Streams de I/O (ver {@link PromptIo}). Default: stdin/stdout.
49
+ * @returns A linha digitada (sem o `\n` final).
50
+ * @example
51
+ * const user = await prompt('Usuário: ');
52
+ */
53
+ export declare function prompt(question: string, io?: PromptIo): Promise<string>;
54
+ /**
55
+ * Lê uma linha sensível (senha, api_key) SEM ecoar os caracteres digitados.
56
+ *
57
+ * A pergunta é escrita normalmente; a partir daí o writer interno do readline é
58
+ * silenciado, de modo que nenhum caractere da entrada aparece no terminal.
59
+ *
60
+ * @param question - Texto exibido antes da entrada (ex.: `'Senha: '`).
61
+ * @param io - Streams de I/O (ver {@link PromptIo}). Default: stdin/stdout.
62
+ * @returns O valor digitado (sem o `\n` final), nunca ecoado.
63
+ * @example
64
+ * const password = await promptPassword('Senha: ');
65
+ */
66
+ export declare function promptPassword(question: string, io?: PromptIo): Promise<string>;