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,18 @@
1
+ /** Tempo (ms) que o aviso de saída fica armado antes de expirar sozinho. */
2
+ export declare const EXIT_GUARD_WINDOW_MS = 2000;
3
+ /** Valor retornado pelo hook. */
4
+ export interface UseExitGuardResult {
5
+ /** `true` enquanto o aviso "pressione Ctrl+C de novo para sair" deve aparecer. */
6
+ armed: boolean;
7
+ }
8
+ /**
9
+ * Liga o handler global de Ctrl+C do app.
10
+ *
11
+ * @param onExit Chamado na segunda pressão dentro da janela de tempo.
12
+ * @param onCancel Chamado na primeira pressão (cancela uma operação em
13
+ * andamento, se houver). Opcional.
14
+ * @param windowMs Janela de tempo em que a segunda pressão conta como
15
+ * confirmação de saída. Parametrizável para testes; usa
16
+ * {@link EXIT_GUARD_WINDOW_MS} por padrão em produção.
17
+ */
18
+ export declare function useExitGuard(onExit: () => void, onCancel?: () => void, windowMs?: number): UseExitGuardResult;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Padrão "duplo Ctrl+C" da TUI (M2-04): a primeira pressão cancela uma
3
+ * operação em andamento (gancho `onCancel` — ainda sem consumidor no M2-04,
4
+ * mas já pronto para telas assíncronas futuras: login em validação no
5
+ * M2-05.2, job rodando no M2-11) e arma um aviso visual temporizado; uma
6
+ * segunda pressão dentro da janela de tempo encerra o app via `onExit`. Fora
7
+ * da janela, a pressão seguinte volta a contar como a "primeira".
8
+ *
9
+ * Isolado do roteador (`../app.tsx`) para poder ser testado sem depender do
10
+ * resto da árvore de telas — `app.tsx` só liga `onExit` a `useApp().exit()`.
11
+ *
12
+ * Requer que o app rode com `exitOnCtrlC: false` no `render()` do Ink
13
+ * (`../index.ts`) — do contrário, o próprio Ink intercepta Ctrl+C antes de
14
+ * `useInput()` rodar e sai do processo na primeira pressão, sem chance deste
15
+ * hook aplicar o padrão de duas pressões.
16
+ */
17
+ import { useInput } from 'ink';
18
+ import { useEffect, useRef, useState } from 'react';
19
+ /** Tempo (ms) que o aviso de saída fica armado antes de expirar sozinho. */
20
+ export const EXIT_GUARD_WINDOW_MS = 2000;
21
+ /**
22
+ * Liga o handler global de Ctrl+C do app.
23
+ *
24
+ * @param onExit Chamado na segunda pressão dentro da janela de tempo.
25
+ * @param onCancel Chamado na primeira pressão (cancela uma operação em
26
+ * andamento, se houver). Opcional.
27
+ * @param windowMs Janela de tempo em que a segunda pressão conta como
28
+ * confirmação de saída. Parametrizável para testes; usa
29
+ * {@link EXIT_GUARD_WINDOW_MS} por padrão em produção.
30
+ */
31
+ export function useExitGuard(onExit, onCancel, windowMs = EXIT_GUARD_WINDOW_MS) {
32
+ const [armed, setArmed] = useState(false);
33
+ // Reason: `armedRef` é a fonte de verdade lida de forma síncrona dentro do
34
+ // handler de input — evita depender da closure presa ao `armed` da última
35
+ // renderização (o handler de `useInput` é recriado a cada render, mas o
36
+ // `ref` sempre reflete o estado mais recente, mesmo entre pressões rápidas).
37
+ const armedRef = useRef(false);
38
+ const timeoutRef = useRef(undefined);
39
+ useEffect(() => {
40
+ return () => {
41
+ if (timeoutRef.current !== undefined) {
42
+ clearTimeout(timeoutRef.current);
43
+ }
44
+ };
45
+ }, []);
46
+ useInput((input, key) => {
47
+ if (!(key.ctrl && input === 'c')) {
48
+ return;
49
+ }
50
+ if (armedRef.current) {
51
+ onExit();
52
+ return;
53
+ }
54
+ onCancel?.();
55
+ armedRef.current = true;
56
+ setArmed(true);
57
+ if (timeoutRef.current !== undefined) {
58
+ clearTimeout(timeoutRef.current);
59
+ }
60
+ timeoutRef.current = setTimeout(() => {
61
+ armedRef.current = false;
62
+ setArmed(false);
63
+ }, windowMs);
64
+ });
65
+ return { armed };
66
+ }
@@ -0,0 +1,62 @@
1
+ import { type Issue } from '../../../index.js';
2
+ /** Formato escolhido na tela: os dois formatos do core, ou ambos de uma vez. */
3
+ export type ExportFormat = 'md' | 'json' | 'both';
4
+ /** Um arquivo gravado com sucesso. */
5
+ export interface ExportedFile {
6
+ /** Formato do arquivo (sempre um dos dois formatos reais do core, nunca `'both'`). */
7
+ format: 'md' | 'json';
8
+ /** Caminho absoluto do arquivo gravado. */
9
+ path: string;
10
+ }
11
+ /** Estado da exportação, consumido por `../screens/export.tsx`. */
12
+ export type ExportBundleState = {
13
+ status: 'idle';
14
+ } | {
15
+ status: 'exporting';
16
+ } | {
17
+ status: 'success';
18
+ files: ExportedFile[];
19
+ } | {
20
+ status: 'error';
21
+ message: string;
22
+ };
23
+ /** Dependências injetáveis do hook — hoje só o ambiente (para `REDMINE_URL`). */
24
+ export interface UseExportBundleOptions {
25
+ /** Ambiente consultado para `REDMINE_URL` (compõe o bundle); default `process.env`. */
26
+ env?: NodeJS.ProcessEnv;
27
+ }
28
+ /** Valor retornado pelo hook. */
29
+ export interface UseExportBundleResult {
30
+ /** Estado atual da exportação (ver {@link ExportBundleState}). */
31
+ state: ExportBundleState;
32
+ /** Dispara a exportação: empacota e grava `<id>.<ext>` no destino informado. */
33
+ runExport: (format: ExportFormat, destination: string) => Promise<void>;
34
+ /** Volta ao estado `idle` — usado para tentar de novo após um erro. */
35
+ reset: () => void;
36
+ }
37
+ /**
38
+ * Expande um `~` inicial para o diretório home do usuário (mesma convenção
39
+ * de shells POSIX) — `~` sozinho ou `~/resto/do/caminho`. Qualquer outro
40
+ * caminho (absoluto, relativo, ou um `~` no meio da string) é devolvido sem
41
+ * alteração.
42
+ *
43
+ * @param path - Caminho digitado pelo usuário no destino.
44
+ * @param getHomedir - Resolve o diretório home; default `node:os`'s `homedir`.
45
+ * @returns Caminho com `~` expandido, se aplicável.
46
+ * @example
47
+ * expandHome('~/exports') // => '/home/ana/exports'
48
+ * expandHome('/tmp/out') // => '/tmp/out' (inalterado)
49
+ */
50
+ export declare function expandHome(path: string, getHomedir?: () => string): string;
51
+ /**
52
+ * Empacota `issue` (`buildMarkdownBundle`/`buildJsonBundle`) e grava o(s)
53
+ * arquivo(s) resultante(s) no destino informado.
54
+ *
55
+ * @param issue - Issue já normalizada, em memória (ver o JSDoc do módulo).
56
+ * @param options - Dependências injetáveis. Ver {@link UseExportBundleOptions}.
57
+ * @returns O {@link UseExportBundleResult} atual.
58
+ * @example
59
+ * const { state, runExport } = useExportBundle(issue);
60
+ * await runExport('both', '~/exports');
61
+ */
62
+ export declare function useExportBundle(issue: Issue | undefined, options?: UseExportBundleOptions): UseExportBundleResult;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Lógica de dados da tela de exportação do bundle (#33): empacota a issue já
3
+ * carregada (`../screens/loaded-issue-context.js`, ver o JSDoc daquele módulo
4
+ * para a escolha de REUSAR a issue em memória em vez de refazer a busca) nos
5
+ * formatos Markdown/JSON/ambos e grava `<id>.md`/`<id>.json` no destino
6
+ * informado pelo usuário.
7
+ *
8
+ * Fronteira do core (ADR-005): `buildMarkdownBundle`/`buildJsonBundle` +
9
+ * `TOOL_VERSION` vêm de `../../../index.js` — nenhum módulo interno do core é
10
+ * importado direto por aqui.
11
+ *
12
+ * `node:fs/promises` (`writeFile`) é importado DIRETO no topo do módulo (sem
13
+ * injeção via opções) — mesmo padrão de `../../cli/commands.ts`, o único
14
+ * outro lugar do repo que grava bundles em disco. Os testes mockam o módulo
15
+ * inteiro com `vi.mock('node:fs/promises', ...)` (ver
16
+ * `tests/surfaces/tui/hooks/use-export-bundle.test.tsx`), não memfs.
17
+ *
18
+ * ## Escolha deliberada: sem `mkdir` automático
19
+ *
20
+ * Diferente do `--out` da CLI (que cria o diretório com `mkdir(..., {
21
+ * recursive: true })` porque o usuário passou uma flag explícita para um
22
+ * pipeline não-interativo), esta tela é interativa e a AC da #33 pede um
23
+ * estado de erro dedicado para "diretório inexistente" — se o diretório
24
+ * fosse criado silenciosamente, esse caminho de erro nunca aconteceria e o
25
+ * usuário nunca veria (nem teria chance de corrigir) um destino digitado
26
+ * errado. `writeFile` falha com `ENOENT` nesse caso; o erro é mostrado com a
27
+ * ação corretiva de criar o diretório ou escolher outro destino.
28
+ */
29
+ import { writeFile } from 'node:fs/promises';
30
+ import { homedir } from 'node:os';
31
+ import { join, resolve } from 'node:path';
32
+ import { useCallback, useState } from 'react';
33
+ import { buildJsonBundle, buildMarkdownBundle, TOOL_VERSION } from '../../../index.js';
34
+ /**
35
+ * Expande um `~` inicial para o diretório home do usuário (mesma convenção
36
+ * de shells POSIX) — `~` sozinho ou `~/resto/do/caminho`. Qualquer outro
37
+ * caminho (absoluto, relativo, ou um `~` no meio da string) é devolvido sem
38
+ * alteração.
39
+ *
40
+ * @param path - Caminho digitado pelo usuário no destino.
41
+ * @param getHomedir - Resolve o diretório home; default `node:os`'s `homedir`.
42
+ * @returns Caminho com `~` expandido, se aplicável.
43
+ * @example
44
+ * expandHome('~/exports') // => '/home/ana/exports'
45
+ * expandHome('/tmp/out') // => '/tmp/out' (inalterado)
46
+ */
47
+ export function expandHome(path, getHomedir = homedir) {
48
+ if (path === '~')
49
+ return getHomedir();
50
+ if (path.startsWith('~/'))
51
+ return join(getHomedir(), path.slice(2));
52
+ return path;
53
+ }
54
+ /** Os dois formatos reais do core que `format` produz (`'both'` vira os dois). */
55
+ function realFormats(format) {
56
+ return format === 'both' ? ['md', 'json'] : [format];
57
+ }
58
+ /**
59
+ * Empacota `issue` (`buildMarkdownBundle`/`buildJsonBundle`) e grava o(s)
60
+ * arquivo(s) resultante(s) no destino informado.
61
+ *
62
+ * @param issue - Issue já normalizada, em memória (ver o JSDoc do módulo).
63
+ * @param options - Dependências injetáveis. Ver {@link UseExportBundleOptions}.
64
+ * @returns O {@link UseExportBundleResult} atual.
65
+ * @example
66
+ * const { state, runExport } = useExportBundle(issue);
67
+ * await runExport('both', '~/exports');
68
+ */
69
+ export function useExportBundle(issue, options = {}) {
70
+ const env = options.env ?? process.env;
71
+ const [state, setState] = useState({ status: 'idle' });
72
+ const reset = useCallback(() => {
73
+ setState({ status: 'idle' });
74
+ }, []);
75
+ const runExport = useCallback(async (format, destination) => {
76
+ if (issue === undefined) {
77
+ setState({ status: 'error', message: 'Nenhuma issue carregada para exportar.' });
78
+ return;
79
+ }
80
+ setState({ status: 'exporting' });
81
+ const trimmed = destination.trim();
82
+ const dir = resolve(expandHome(trimmed.length > 0 ? trimmed : '.'));
83
+ const meta = { baseUrl: env.REDMINE_URL ?? '', toolVersion: TOOL_VERSION };
84
+ try {
85
+ const files = [];
86
+ for (const fmt of realFormats(format)) {
87
+ const content = fmt === 'json' ? buildJsonBundle(issue, meta).canonical : buildMarkdownBundle(issue, meta);
88
+ const path = join(dir, `${issue.id}.${fmt}`);
89
+ await writeFile(path, content, 'utf8');
90
+ files.push({ format: fmt, path });
91
+ }
92
+ setState({ status: 'success', files });
93
+ }
94
+ catch (cause) {
95
+ const message = cause instanceof Error ? cause.message : String(cause);
96
+ setState({ status: 'error', message });
97
+ }
98
+ }, [issue, env]);
99
+ return { state, runExport, reset };
100
+ }
@@ -0,0 +1,61 @@
1
+ import { createHttpClient, getIssue, normalizeIssue, resolveApiKey, type Issue } from '../../../index.js';
2
+ /**
3
+ * Estado da busca de detalhe de issue, consumido por
4
+ * `../screens/issue-detail.tsx`. `no-selection` cobre o caso defensivo de a
5
+ * tela ser alcançada sem uma issue selecionada (ver
6
+ * `../screens/home-selection.tsx`) — não deveria acontecer no fluxo normal
7
+ * (a home sempre define `selectedIssueId` antes de empilhar esta tela), mas
8
+ * evita uma busca com `issueId` inválido caso aconteça.
9
+ */
10
+ export type IssueDetailState = {
11
+ status: 'no-selection';
12
+ } | {
13
+ status: 'loading';
14
+ } | {
15
+ status: 'loaded';
16
+ issue: Issue;
17
+ } | {
18
+ status: 'error-network';
19
+ message: string;
20
+ } | {
21
+ status: 'error-forbidden';
22
+ message: string;
23
+ } | {
24
+ status: 'error-not-found';
25
+ message: string;
26
+ } | {
27
+ status: 'auth-aborted';
28
+ message: string;
29
+ };
30
+ /** Dependências injetáveis do hook — todas opcionais, com defaults de produção via o core. */
31
+ export interface UseIssueDetailOptions {
32
+ /** Ambiente consultado para `REDMINE_URL`; default `process.env`. */
33
+ env?: NodeJS.ProcessEnv;
34
+ /** Resolve a api_key pela cascata M2; default `resolveApiKey` do core. */
35
+ resolveApiKey?: typeof resolveApiKey;
36
+ /** Constrói o client HTTP autenticado; default `createHttpClient` do core. */
37
+ createHttpClient?: typeof createHttpClient;
38
+ /** Busca o payload bruto de `/issues/{id}.json`; default `getIssue` do core. */
39
+ getIssue?: typeof getIssue;
40
+ /** Normaliza o payload bruto no modelo `Issue`; default `normalizeIssue` do core. */
41
+ normalizeIssue?: typeof normalizeIssue;
42
+ }
43
+ /** Valor retornado pelo hook. */
44
+ export interface UseIssueDetailResult {
45
+ /** Estado atual da busca (ver {@link IssueDetailState}). */
46
+ state: IssueDetailState;
47
+ /** Reexecuta a busca do zero — usado pelo retry (tecla `r`) dos estados de erro. */
48
+ retry: () => void;
49
+ }
50
+ /**
51
+ * Busca o detalhe completo de uma issue (`getIssue` + `normalizeIssue`) via
52
+ * o core.
53
+ *
54
+ * @param issueId - Id da issue a buscar, ou `undefined` (estado defensivo
55
+ * `no-selection`, ver {@link IssueDetailState}).
56
+ * @param options - Dependências injetáveis. Ver {@link UseIssueDetailOptions}.
57
+ * @returns O {@link UseIssueDetailResult} atual.
58
+ * @example
59
+ * const { state, retry } = useIssueDetail(selectedIssueId); // deps de produção
60
+ */
61
+ export declare function useIssueDetail(issueId: number | undefined, options?: UseIssueDetailOptions): UseIssueDetailResult;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Lógica de dados da tela de detalhe de issue (#31): busca o detalhe
3
+ * completo via o core — resolve a credencial pela cascata (`resolveApiKey`),
4
+ * monta um client HTTP autenticado (`createHttpClient`), busca o payload
5
+ * bruto (`getIssue`) e normaliza para o modelo estável (`normalizeIssue`).
6
+ *
7
+ * Escolha deliberada: `getIssue` + `normalizeIssue` DIRETO, não
8
+ * `fetchIssueBundle` — o bundle serializa para Markdown/JSON (formato de
9
+ * saída da CLI/MCP); a tela de detalhe da TUI precisa do modelo `Issue` em
10
+ * memória para renderizar metadados + paginar/rolar descrição e journals
11
+ * (`../components/scroll-view.tsx`), não de um texto já serializado.
12
+ *
13
+ * Extraída para um hook pelo mesmo motivo de `./use-my-issues.ts`: a busca é
14
+ * assíncrona e precisa ser testável com dependências injetadas — nenhum
15
+ * teste deste arquivo toca keychain ou rede reais.
16
+ *
17
+ * Fronteira do core (ADR-005): a única coisa importada daqui é
18
+ * `../../../index.js` — a tela (`../screens/issue-detail.tsx`) só formata os
19
+ * estados visuais, nunca chama o core diretamente.
20
+ *
21
+ * A instância vem de `REDMINE_URL` (mesma convenção do doctor/home — ver
22
+ * `./use-my-issues.ts`), lida do `env` injetado (default `process.env`).
23
+ *
24
+ * Mandatório (M2-13, #36): a busca é envolvida por `useAuthGuard` — um 401
25
+ * dispara o re-login e retoma a busca sozinha (o estado permanece `loading`
26
+ * até lá); se o re-login for abandonado (Esc), o hook expõe o estado
27
+ * `auth-aborted` (neutro, não um erro), com `retry` disponível.
28
+ */
29
+ import { useCallback, useEffect, useState } from 'react';
30
+ import { createHttpClient, getIssue, normalizeIssue, resolveApiKey, RedmineForbiddenError, RedmineNotFoundError, } from '../../../index.js';
31
+ import { useEnvFallbackAllowed } from '../instance.js';
32
+ import { ReAuthAbortedError, useAuthGuard } from './use-auth-guard.js';
33
+ /** Lê `REDMINE_URL` do ambiente, tratando string vazia como ausente (mesma convenção do doctor/home). */
34
+ function instanceFromEnv(env) {
35
+ const value = env.REDMINE_URL;
36
+ return value !== undefined && value.length > 0 ? value : undefined;
37
+ }
38
+ /**
39
+ * Busca o detalhe completo de uma issue (`getIssue` + `normalizeIssue`) via
40
+ * o core.
41
+ *
42
+ * @param issueId - Id da issue a buscar, ou `undefined` (estado defensivo
43
+ * `no-selection`, ver {@link IssueDetailState}).
44
+ * @param options - Dependências injetáveis. Ver {@link UseIssueDetailOptions}.
45
+ * @returns O {@link UseIssueDetailResult} atual.
46
+ * @example
47
+ * const { state, retry } = useIssueDetail(selectedIssueId); // deps de produção
48
+ */
49
+ export function useIssueDetail(issueId, options = {}) {
50
+ const env = options.env ?? process.env;
51
+ const resolve = options.resolveApiKey ?? resolveApiKey;
52
+ const buildClient = options.createHttpClient ?? createHttpClient;
53
+ const fetchIssue = options.getIssue ?? getIssue;
54
+ const normalize = options.normalizeIssue ?? normalizeIssue;
55
+ // Mandatório (M2-13, #36): ver o JSDoc do módulo.
56
+ const { guard } = useAuthGuard();
57
+ // SEGURANÇA (#187): origem config (URL persistida) → sem env-key instance-agnóstica.
58
+ const allowEnvFallback = useEnvFallbackAllowed();
59
+ const [state, setState] = useState(issueId === undefined ? { status: 'no-selection' } : { status: 'loading' });
60
+ const [reloadToken, setReloadToken] = useState(0);
61
+ const retry = useCallback(() => {
62
+ setReloadToken((current) => current + 1);
63
+ }, []);
64
+ useEffect(() => {
65
+ let cancelled = false;
66
+ if (issueId === undefined) {
67
+ setState({ status: 'no-selection' });
68
+ return;
69
+ }
70
+ setState({ status: 'loading' });
71
+ const instanceUrl = instanceFromEnv(env);
72
+ if (instanceUrl === undefined) {
73
+ setState({
74
+ status: 'error-network',
75
+ message: 'Instância não configurada (defina REDMINE_URL).',
76
+ });
77
+ return;
78
+ }
79
+ const cascadeOptions = { env, allowEnvFallback };
80
+ void (async () => {
81
+ try {
82
+ const apiKey = await resolve(instanceUrl, cascadeOptions);
83
+ if (apiKey === undefined) {
84
+ if (!cancelled) {
85
+ setState({
86
+ status: 'error-network',
87
+ message: `Nenhuma credencial encontrada para ${instanceUrl}.`,
88
+ });
89
+ }
90
+ return;
91
+ }
92
+ const http = buildClient({ baseUrl: instanceUrl, apiKey });
93
+ const payload = await guard(() => fetchIssue(http, issueId));
94
+ if (cancelled)
95
+ return;
96
+ setState({ status: 'loaded', issue: normalize(payload) });
97
+ }
98
+ catch (cause) {
99
+ if (cancelled)
100
+ return;
101
+ if (cause instanceof RedmineNotFoundError) {
102
+ setState({ status: 'error-not-found', message: `Issue #${issueId} não encontrada (404).` });
103
+ return;
104
+ }
105
+ if (cause instanceof RedmineForbiddenError) {
106
+ setState({
107
+ status: 'error-forbidden',
108
+ message: 'Sem permissão para ver esta issue nesta instância (403).',
109
+ });
110
+ return;
111
+ }
112
+ if (cause instanceof ReAuthAbortedError) {
113
+ setState({
114
+ status: 'auth-aborted',
115
+ message: 'login cancelado — pressione r para tentar de novo',
116
+ });
117
+ return;
118
+ }
119
+ const message = cause instanceof Error ? cause.message : String(cause);
120
+ setState({ status: 'error-network', message });
121
+ }
122
+ })();
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ // Reason: `env`/`resolve`/`buildClient`/`fetchIssue`/`normalize`/`guard`
127
+ // são estáveis entre renders com as deps de produção — o efeito só
128
+ // precisa refazer a busca quando `issueId` muda (nova issue selecionada)
129
+ // ou `reloadToken` muda (retry).
130
+ }, [issueId, env, allowEnvFallback, resolve, buildClient, fetchIssue, normalize, guard, reloadToken]);
131
+ return { state, retry };
132
+ }
@@ -0,0 +1,71 @@
1
+ import { fetchIssueSearch, resolveApiKey } from '../../../index.js';
2
+ /** Debounce (ms) default aplicado a `query` antes de disparar a busca. */
3
+ export declare const DEFAULT_DEBOUNCE_MS = 300;
4
+ /** Filtro rápido de status (tecla `f` cicla entre os três, ver `../screens/home.tsx`). */
5
+ export type SearchStatusFilter = 'open' | 'closed' | 'all';
6
+ /**
7
+ * Estado da busca, consumido por `../screens/home.tsx`. Mesmo vocabulário de
8
+ * `MyIssuesState` (`use-my-issues.ts`) + um `idle` inicial próprio: a busca
9
+ * só começa a existir quando há algo para buscar (query não vazia OU filtro
10
+ * de status diferente de `'all'`) — sem isso, `idle` evita uma chamada de
11
+ * rede desnecessária assim que o campo de busca abre vazio.
12
+ */
13
+ export type IssueSearchState = {
14
+ status: 'idle';
15
+ } | {
16
+ status: 'loading';
17
+ } | {
18
+ status: 'loaded';
19
+ content: string;
20
+ count: number;
21
+ degraded: boolean;
22
+ warnings: string[];
23
+ } | {
24
+ status: 'error-network';
25
+ message: string;
26
+ } | {
27
+ status: 'error-forbidden';
28
+ message: string;
29
+ } | {
30
+ status: 'auth-aborted';
31
+ message: string;
32
+ };
33
+ /** Dependências injetáveis do hook — todas opcionais, com defaults de produção via o core. */
34
+ export interface UseIssueSearchOptions {
35
+ /** Ambiente consultado para `REDMINE_URL`; default `process.env`. */
36
+ env?: NodeJS.ProcessEnv;
37
+ /** Resolve a api_key pela cascata M2; default `resolveApiKey` do core. */
38
+ resolveApiKey?: typeof resolveApiKey;
39
+ /** Orquestra a busca (filtros + full-text best-effort); default `fetchIssueSearch` do core. */
40
+ fetchIssueSearch?: typeof fetchIssueSearch;
41
+ /** Debounce (ms) aplicado a `query`. Default: {@link DEFAULT_DEBOUNCE_MS}. */
42
+ debounceMs?: number;
43
+ }
44
+ /** Valor retornado pelo hook. */
45
+ export interface UseIssueSearchResult {
46
+ /** Estado atual da busca (ver {@link IssueSearchState}). */
47
+ state: IssueSearchState;
48
+ /**
49
+ * Restaura o estado para `idle` IMEDIATAMENTE, cancelando qualquer debounce
50
+ * pendente — usado pelo Esc da home (M2-07, #30) para "limpar a busca"
51
+ * sem esperar o debounce nem disparar uma nova chamada a `fetchIssueSearch`.
52
+ * A tela também deve resetar `query`/`statusFilter` para os valores neutros
53
+ * (`''`/`'all'`) ao chamar isto, para que o próximo efeito não reagende a
54
+ * mesma busca.
55
+ */
56
+ clear: () => void;
57
+ }
58
+ /**
59
+ * Busca issues (filtros + full-text best-effort) via o core, com debounce de
60
+ * digitação e o filtro rápido de status.
61
+ *
62
+ * @param query - Termo full-text bruto (não debounced) — tipicamente o
63
+ * `value` do `TextInput` da busca da home.
64
+ * @param statusFilter - Filtro rápido de status (ver {@link SearchStatusFilter}).
65
+ * @param options - Dependências injetáveis. Ver {@link UseIssueSearchOptions}.
66
+ * @returns O {@link UseIssueSearchResult} atual.
67
+ *
68
+ * @example
69
+ * const { state, clear } = useIssueSearch(query, statusFilter); // deps de produção
70
+ */
71
+ export declare function useIssueSearch(query: string, statusFilter: SearchStatusFilter, options?: UseIssueSearchOptions): UseIssueSearchResult;