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
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ export const TOOL_NAME = 'redmine-context';
2
+ // Manter em sincronia com package.json (validado por tests/packaging/smoke-pack.test.ts).
3
+ export const TOOL_VERSION = '1.0.0';
4
+ // Superfície pública do core: contrato de tipos + padrão de progresso (ADR-005).
5
+ // As superfícies devem consumir o core somente por aqui / por ./contract.js.
6
+ export * from './contract.js';
7
+ // Orquestração get → normalize → bundle reutilizável por CLI (#17) e MCP (#18).
8
+ export { fetchIssueBundle, } from './fetch-issue-bundle.js';
9
+ // Orquestração de extração de anexos (M3-10): baixa → dispatcher → extrator →
10
+ // cache → Map<attachmentId, ExtractionResult>, embutida nos bundles pela flag
11
+ // `extractAttachments` de `fetchIssueBundle`. CLI/MCP ligam a flag na #55.
12
+ export { extractIssueAttachments, } from './extract-issue-attachments.js';
13
+ // Orquestração get → normalize → extração de UM anexo (M3-13): reutiliza o
14
+ // pipeline com cache de `extractIssueAttachments` para a tool MCP read-only
15
+ // `get_attachment_text` (#55). Devolve o ExtractionResult do anexo pedido.
16
+ export { fetchAttachmentText, fetchAttachmentTextCacheFirst, AttachmentNotFoundError, } from './fetch-attachment-text.js';
17
+ // Leitura cache-first não-bloqueante das extrações (M4-11 #70): lê o que está
18
+ // pronto e sinaliza `processing` para o resto, disparando a extração cara em
19
+ // background pela fila — a superfície MCP responde na hora (< 5s), sem bloquear.
20
+ export { extractIssueAttachmentsCacheFirst, makeQueueBackgroundExtractor, processingResult, } from './cache-first.js';
21
+ // Orquestração de busca (filtros + full-text best-effort) para a tool MCP (#19).
22
+ export { fetchIssueSearch, SEARCH_DEFAULT_LIMIT, } from './fetch-issue-search.js';
23
+ // Primitiva full-text `/search.json` (usada pela orquestração acima).
24
+ export { searchIssues } from './client/index.js';
25
+ // Client HTTP base (auth por api_key + retry) — usado por telas que precisam
26
+ // montar suas próprias chamadas ao core sem uma orquestração pronta (ex.: a
27
+ // home da TUI, #29, que lista "minhas issues" via `listIssues` abaixo).
28
+ export { createHttpClient, } from './client/index.js';
29
+ // Listagem paginada de issues (`GET /issues.json`), payload bruto sem
30
+ // normalização — usada pela home da TUI (#29) com o filtro `assigned_to_id=me`.
31
+ export { listIssues } from './client/index.js';
32
+ // Detalhe completo de uma issue (`GET /issues/{id}.json`), payload bruto sem
33
+ // normalização — usado pela tela de detalhe da TUI (#31), que normaliza com
34
+ // `normalizeIssue` abaixo para render o modelo estável `Issue` (sem passar
35
+ // pelo bundle Markdown/JSON, que é o formato de saída da CLI/MCP, não do
36
+ // modelo em memória que a TUI precisa para paginar/rolar o conteúdo).
37
+ export { getIssue } from './client/index.js';
38
+ // Normalização do payload bruto de issue no modelo estável `Issue` do
39
+ // contrato — usada por `fetchIssueBundle` (CLI/MCP) e, desde a #31, também
40
+ // diretamente pela tela de detalhe da TUI (`getIssue` + `normalizeIssue`,
41
+ // sem serializar para Markdown/JSON).
42
+ export { normalizeIssue } from './normalize/index.js';
43
+ // Empacotamento Markdown/JSON direto (M1-09/M1-10) — usado por
44
+ // `fetchIssueBundle` internamente e, desde a #33, também diretamente pela
45
+ // tela de exportação da TUI: a issue já está normalizada em memória (`./use-issue-detail.js`,
46
+ // #31), então gravar o bundle não precisa refazer a busca+normalização via
47
+ // `fetchIssueBundle` — só empacotar o que já foi carregado.
48
+ export { buildMarkdownBundle, buildJsonBundle, fenceBlock, } from './bundle/index.js';
49
+ // Erros HTTP tipados — usados pelas superfícies para mapear exit codes (ADR-005).
50
+ export { RedmineHttpError, RedmineAuthError, RedmineForbiddenError, RedmineNotFoundError, } from './client/index.js';
51
+ // Login por senha (M1-07) e cascata de credenciais (M1-08) para as superfícies.
52
+ export { loginWithPassword, validateApiKey, RedmineLoginError, } from './config/index.js';
53
+ export { createCredentialCascade, resolveApiKey, describeCredentialSource, normalizeInstanceUrl, CredentialStoreError, } from './config/index.js';
54
+ // Persistência da URL da instância (#187) — fallback além de REDMINE_URL/--url.
55
+ export { defaultSettingsStore, resolveInstanceUrl, } from './config/index.js';
56
+ // Diagnóstico de binários de mídia (M3-11 #53, M4-01 #57) — núcleo do comando
57
+ // `doctor` (CLI) e da seção "Binários de mídia" da TUI. Detecção de tesseract,
58
+ // ffmpeg e whisper.cpp + status do modelo GGUF, com hint de instalação por SO
59
+ // (ADR-002).
60
+ export { diagnoseBinaries, tesseractInstallHint, pdftotextInstallHint, ffmpegInstallHint, whisperInstallHint, } from './config/index.js';
61
+ // Localização de binários de mídia e path canônico do modelo GGUF (M4-01, #57).
62
+ // `whisperModelDir` é o ponto único de verdade do cache de modelos, consumido
63
+ // pelo `doctor` e pelo download do modelo (#58) (ADR-002).
64
+ export { findFfmpeg, detectFfmpegVersion, findWhisper, whisperModelDir, } from './extract/index.js';
65
+ // Download do modelo GGUF com SHA-256 pinado e guard headless (M4-02, #58).
66
+ // Opt-in interativo (ADR-002): recusa em MCP/headless; consome `whisperModelDir`.
67
+ export { downloadGgufModel, GgufDownloadError, GGUF_MODEL_NAME, GGUF_MODEL_URL, GGUF_MODEL_SHA256, } from './extract/index.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Normalização das coleções aninhadas de uma issue (issue #12).
3
+ *
4
+ * Cobre o que a issue #11 deixou como coleções vazias: custom_fields, relations,
5
+ * parent/children e watchers. Mesmo contrato de robustez de {@link normalizeIssue}
6
+ * (`src/normalize/issue.ts`): reutiliza os helpers defensivos de `./helpers.js` e
7
+ * NUNCA lança — itens malformados são descartados e campos ausentes degradam para
8
+ * defaults estáveis do contrato (ADR-005).
9
+ */
10
+ import type { CustomField, IssueChild, IssueRelation, RedmineRef } from '../contract.js';
11
+ /**
12
+ * Normaliza a coleção de custom fields de uma issue.
13
+ *
14
+ * @param value - Valor bruto de `issue.custom_fields`.
15
+ * @param fieldFormats - Mapa opcional `id → field_format` fornecido externamente.
16
+ * @returns Array de custom fields válidos (itens malformados descartados).
17
+ */
18
+ export declare function normalizeCustomFields(value: unknown, fieldFormats?: Map<number, string>): CustomField[];
19
+ /**
20
+ * Normaliza a coleção de relations de uma issue.
21
+ *
22
+ * @param value - Valor bruto de `issue.relations`.
23
+ * @returns Array de relations válidas (itens malformados descartados).
24
+ */
25
+ export declare function normalizeRelations(value: unknown): IssueRelation[];
26
+ /**
27
+ * Normaliza a coleção de children (nível de topo) de uma issue.
28
+ *
29
+ * @param value - Valor bruto de `issue.children`.
30
+ * @returns Array de children válidos (itens malformados descartados).
31
+ */
32
+ export declare function normalizeChildren(value: unknown): IssueChild[];
33
+ /**
34
+ * Extrai a referência de parent (`{ id }`) do payload.
35
+ *
36
+ * @param value - Valor bruto de `issue.parent`.
37
+ * @returns `{ id }` quando há `id` numérico; senão `undefined`.
38
+ */
39
+ export declare function normalizeParent(value: unknown): {
40
+ id: number;
41
+ } | undefined;
42
+ /**
43
+ * Normaliza a lista de watchers (refs `{ id, name }`).
44
+ *
45
+ * Só deve ser chamada quando a chave `watchers` está presente no payload — a
46
+ * ausência da chave (include não pedido ou 403) é degradação e o campo do
47
+ * contrato fica omitido pelo chamador.
48
+ *
49
+ * @param value - Valor bruto de `issue.watchers`.
50
+ * @returns Array de refs válidas (itens malformados descartados).
51
+ */
52
+ export declare function normalizeWatchers(value: unknown): RedmineRef[];
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Normalização das coleções aninhadas de uma issue (issue #12).
3
+ *
4
+ * Cobre o que a issue #11 deixou como coleções vazias: custom_fields, relations,
5
+ * parent/children e watchers. Mesmo contrato de robustez de {@link normalizeIssue}
6
+ * (`src/normalize/issue.ts`): reutiliza os helpers defensivos de `./helpers.js` e
7
+ * NUNCA lança — itens malformados são descartados e campos ausentes degradam para
8
+ * defaults estáveis do contrato (ADR-005).
9
+ */
10
+ import { asArray, asNumber, asRecord, asString, isDefined, normalizeRef } from './helpers.js';
11
+ /**
12
+ * Extrai o `raw_value` de um custom field preservando o bruto da API.
13
+ *
14
+ * Mantém `string`, `""` e arrays (filtrando itens não-textuais) exatamente como
15
+ * vieram; `null`, ausência e tipos inesperados colapsam para `null`.
16
+ *
17
+ * @param value - Valor bruto de `custom_field.value`.
18
+ * @returns O bruto preservado no formato do contrato.
19
+ */
20
+ function rawCustomFieldValue(value) {
21
+ if (Array.isArray(value))
22
+ return value.filter((item) => typeof item === 'string');
23
+ return asString(value) ?? null;
24
+ }
25
+ /**
26
+ * Normaliza APENAS a ausência de um custom field (`""`/`null` → `null`).
27
+ *
28
+ * Não há coerção de tipo (ADR-005): qualquer valor presente e não-vazio é
29
+ * repassado intacto — inclusive arrays.
30
+ *
31
+ * @param raw - O `raw_value` já extraído.
32
+ * @returns `null` para ausência (`""`/`null`); senão o próprio bruto.
33
+ */
34
+ function normalizedCustomFieldValue(raw) {
35
+ if (raw === null || raw === '')
36
+ return null;
37
+ return raw;
38
+ }
39
+ /**
40
+ * Normaliza um custom field para o contrato {@link CustomField}.
41
+ *
42
+ * `field_format` não vem na API de issue: só é anotado quando o chamador fornece
43
+ * o formato via `fieldFormats` (ex.: de `/custom_fields.json`).
44
+ *
45
+ * @param value - Item bruto de `issue.custom_fields[]`.
46
+ * @param fieldFormats - Mapa opcional `id → field_format` fornecido externamente.
47
+ * @returns O custom field normalizado, ou `undefined` se não tiver `id` numérico.
48
+ */
49
+ function normalizeCustomField(value, fieldFormats) {
50
+ const record = asRecord(value);
51
+ if (record === undefined)
52
+ return undefined;
53
+ const id = asNumber(record.id);
54
+ if (id === undefined)
55
+ return undefined;
56
+ const raw = rawCustomFieldValue(record.value);
57
+ const field = {
58
+ id,
59
+ name: asString(record.name) ?? '',
60
+ value: normalizedCustomFieldValue(raw),
61
+ raw_value: raw,
62
+ };
63
+ const format = fieldFormats?.get(id);
64
+ if (format !== undefined)
65
+ field.field_format = format;
66
+ return field;
67
+ }
68
+ /**
69
+ * Normaliza a coleção de custom fields de uma issue.
70
+ *
71
+ * @param value - Valor bruto de `issue.custom_fields`.
72
+ * @param fieldFormats - Mapa opcional `id → field_format` fornecido externamente.
73
+ * @returns Array de custom fields válidos (itens malformados descartados).
74
+ */
75
+ export function normalizeCustomFields(value, fieldFormats) {
76
+ return asArray(value)
77
+ .map((item) => normalizeCustomField(item, fieldFormats))
78
+ .filter(isDefined);
79
+ }
80
+ /**
81
+ * Normaliza uma relação entre issues para o contrato {@link IssueRelation}.
82
+ *
83
+ * @param value - Item bruto de `issue.relations[]`.
84
+ * @returns A relação normalizada, ou `undefined` se não tiver `id` numérico.
85
+ * `delay` ausente vira `null`; demais campos numéricos/textuais degradam.
86
+ */
87
+ function normalizeRelation(value) {
88
+ const record = asRecord(value);
89
+ if (record === undefined)
90
+ return undefined;
91
+ const id = asNumber(record.id);
92
+ if (id === undefined)
93
+ return undefined;
94
+ return {
95
+ id,
96
+ issue_id: asNumber(record.issue_id) ?? 0,
97
+ issue_to_id: asNumber(record.issue_to_id) ?? 0,
98
+ relation_type: asString(record.relation_type) ?? '',
99
+ delay: asNumber(record.delay) ?? null,
100
+ };
101
+ }
102
+ /**
103
+ * Normaliza a coleção de relations de uma issue.
104
+ *
105
+ * @param value - Valor bruto de `issue.relations`.
106
+ * @returns Array de relations válidas (itens malformados descartados).
107
+ */
108
+ export function normalizeRelations(value) {
109
+ return asArray(value).map(normalizeRelation).filter(isDefined);
110
+ }
111
+ /**
112
+ * Normaliza uma issue-filha para o contrato {@link IssueChild}.
113
+ *
114
+ * O contrato é plano (`{id, tracker?, subject?}`): eventuais `children` aninhados
115
+ * do payload são ignorados aqui — cada nível é uma issue por si.
116
+ *
117
+ * @param value - Item bruto de `issue.children[]`.
118
+ * @returns A filha normalizada, ou `undefined` se não tiver `id` numérico.
119
+ */
120
+ function normalizeChild(value) {
121
+ const record = asRecord(value);
122
+ if (record === undefined)
123
+ return undefined;
124
+ const id = asNumber(record.id);
125
+ if (id === undefined)
126
+ return undefined;
127
+ const child = { id };
128
+ const tracker = normalizeRef(record.tracker);
129
+ if (tracker !== undefined)
130
+ child.tracker = tracker;
131
+ const subject = asString(record.subject);
132
+ if (subject !== undefined)
133
+ child.subject = subject;
134
+ return child;
135
+ }
136
+ /**
137
+ * Normaliza a coleção de children (nível de topo) de uma issue.
138
+ *
139
+ * @param value - Valor bruto de `issue.children`.
140
+ * @returns Array de children válidos (itens malformados descartados).
141
+ */
142
+ export function normalizeChildren(value) {
143
+ return asArray(value).map(normalizeChild).filter(isDefined);
144
+ }
145
+ /**
146
+ * Extrai a referência de parent (`{ id }`) do payload.
147
+ *
148
+ * @param value - Valor bruto de `issue.parent`.
149
+ * @returns `{ id }` quando há `id` numérico; senão `undefined`.
150
+ */
151
+ export function normalizeParent(value) {
152
+ const record = asRecord(value);
153
+ if (record === undefined)
154
+ return undefined;
155
+ const id = asNumber(record.id);
156
+ return id === undefined ? undefined : { id };
157
+ }
158
+ /**
159
+ * Normaliza a lista de watchers (refs `{ id, name }`).
160
+ *
161
+ * Só deve ser chamada quando a chave `watchers` está presente no payload — a
162
+ * ausência da chave (include não pedido ou 403) é degradação e o campo do
163
+ * contrato fica omitido pelo chamador.
164
+ *
165
+ * @param value - Valor bruto de `issue.watchers`.
166
+ * @returns Array de refs válidas (itens malformados descartados).
167
+ */
168
+ export function normalizeWatchers(value) {
169
+ return asArray(value).map(normalizeRef).filter(isDefined);
170
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Helpers de parsing defensivo compartilhados pela normalização (issues #11/#12).
3
+ *
4
+ * Núcleo do estilo "nunca lança" de `src/normalize/*`: cada função converte um
5
+ * valor `unknown` em um tipo estável ou num `undefined`/default previsível,
6
+ * permitindo que payloads parciais ou malformados degradem campo a campo sem
7
+ * crashar. Reutilizados por {@link normalizeIssue} e pelos normalizadores de
8
+ * coleções (custom_fields, relations, children, watchers).
9
+ */
10
+ import type { RedmineRef } from '../contract.js';
11
+ /**
12
+ * Converte um valor desconhecido em `Record<string, unknown>` para acesso seguro.
13
+ *
14
+ * @param value - Valor a inspecionar.
15
+ * @returns O objeto tipado, ou `undefined` se não for um objeto simples.
16
+ */
17
+ export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
18
+ /** Devolve o array bruto, ou `[]` quando o valor não é um array. */
19
+ export declare function asArray(value: unknown): unknown[];
20
+ /** `string` quando o valor é textual, senão `undefined`. */
21
+ export declare function asString(value: unknown): string | undefined;
22
+ /** `number` quando o valor é numérico, senão `undefined`. */
23
+ export declare function asNumber(value: unknown): number | undefined;
24
+ /** Preserva `string`/`null` de valores brutos; tipos inesperados viram `null`. */
25
+ export declare function asStringOrNull(value: unknown): string | null;
26
+ /** Type guard para remover `undefined` após um `map` defensivo. */
27
+ export declare function isDefined<T>(value: T | undefined): value is T;
28
+ /**
29
+ * Normaliza uma ref nomeada do Redmine (`{ id, name }`).
30
+ *
31
+ * @param value - Valor bruto (objeto esperado).
32
+ * @returns A ref quando há `id` numérico; senão `undefined`. `name` não-string
33
+ * degrada para `""`.
34
+ */
35
+ export declare function normalizeRef(value: unknown): RedmineRef | undefined;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Helpers de parsing defensivo compartilhados pela normalização (issues #11/#12).
3
+ *
4
+ * Núcleo do estilo "nunca lança" de `src/normalize/*`: cada função converte um
5
+ * valor `unknown` em um tipo estável ou num `undefined`/default previsível,
6
+ * permitindo que payloads parciais ou malformados degradem campo a campo sem
7
+ * crashar. Reutilizados por {@link normalizeIssue} e pelos normalizadores de
8
+ * coleções (custom_fields, relations, children, watchers).
9
+ */
10
+ /**
11
+ * Converte um valor desconhecido em `Record<string, unknown>` para acesso seguro.
12
+ *
13
+ * @param value - Valor a inspecionar.
14
+ * @returns O objeto tipado, ou `undefined` se não for um objeto simples.
15
+ */
16
+ export function asRecord(value) {
17
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
18
+ return value;
19
+ }
20
+ return undefined;
21
+ }
22
+ /** Devolve o array bruto, ou `[]` quando o valor não é um array. */
23
+ export function asArray(value) {
24
+ return Array.isArray(value) ? value : [];
25
+ }
26
+ /** `string` quando o valor é textual, senão `undefined`. */
27
+ export function asString(value) {
28
+ return typeof value === 'string' ? value : undefined;
29
+ }
30
+ /** `number` quando o valor é numérico, senão `undefined`. */
31
+ export function asNumber(value) {
32
+ return typeof value === 'number' ? value : undefined;
33
+ }
34
+ /** Preserva `string`/`null` de valores brutos; tipos inesperados viram `null`. */
35
+ export function asStringOrNull(value) {
36
+ if (value === null)
37
+ return null;
38
+ return typeof value === 'string' ? value : null;
39
+ }
40
+ /** Type guard para remover `undefined` após um `map` defensivo. */
41
+ export function isDefined(value) {
42
+ return value !== undefined;
43
+ }
44
+ /**
45
+ * Normaliza uma ref nomeada do Redmine (`{ id, name }`).
46
+ *
47
+ * @param value - Valor bruto (objeto esperado).
48
+ * @returns A ref quando há `id` numérico; senão `undefined`. `name` não-string
49
+ * degrada para `""`.
50
+ */
51
+ export function normalizeRef(value) {
52
+ const record = asRecord(value);
53
+ if (record === undefined)
54
+ return undefined;
55
+ const id = asNumber(record.id);
56
+ if (id === undefined)
57
+ return undefined;
58
+ return { id, name: asString(record.name) ?? '' };
59
+ }
@@ -0,0 +1,2 @@
1
+ export declare const MODULE_NAME: "normalize";
2
+ export { normalizeIssue } from './issue.js';
@@ -0,0 +1,2 @@
1
+ export const MODULE_NAME = 'normalize';
2
+ export { normalizeIssue } from './issue.js';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Normalização do payload bruto de issue (issues #11/#12).
3
+ *
4
+ * Converte o {@link RedmineIssuePayload} devolvido por `getIssue`
5
+ * (`/issues/{id}.json?include=journals,attachments,relations,children`) no modelo
6
+ * estável {@link Issue} do contrato. Cobre o núcleo (#11) — id, subject,
7
+ * description, refs, datas, journals e attachments — e as coleções aninhadas
8
+ * (#12) delegadas a `./collections.js`: custom_fields, relations, parent/children
9
+ * e watchers.
10
+ *
11
+ * Parsing 100% defensivo (estilo `src/client/issues.ts`): payload parcial ou
12
+ * malformado NUNCA lança — cada campo degrada para um default estável.
13
+ */
14
+ import type { RedmineIssuePayload } from '../client/issues.js';
15
+ import type { Issue } from '../contract.js';
16
+ /**
17
+ * Normaliza o payload bruto de uma issue no modelo {@link Issue} do contrato.
18
+ *
19
+ * Núcleo (#11): id, subject, description, refs, datas, journals e attachments.
20
+ * Coleções (#12): custom_fields, relations, parent/children e watchers. Nunca
21
+ * lança: payload parcial/malformado degrada campo a campo para defaults estáveis.
22
+ *
23
+ * `watchers` só aparece quando a chave está presente no payload; a ausência
24
+ * (include não pedido ou 403) omite o campo como degradação. A API de issue não
25
+ * devolve `field_format` dos custom fields — o chamador pode anotá-lo via
26
+ * `fieldFormats` (ex.: a partir de `/custom_fields.json`).
27
+ *
28
+ * @param payload - Payload bruto de `getIssue`.
29
+ * @param fieldFormats - Mapa opcional `custom_field_id → field_format`.
30
+ * @returns A issue normalizada, sempre válida perante o contrato.
31
+ * @example
32
+ * const issue = normalizeIssue(await getIssue(http, 100));
33
+ */
34
+ export declare function normalizeIssue(payload: RedmineIssuePayload, fieldFormats?: Map<number, string>): Issue;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Normalização do payload bruto de issue (issues #11/#12).
3
+ *
4
+ * Converte o {@link RedmineIssuePayload} devolvido por `getIssue`
5
+ * (`/issues/{id}.json?include=journals,attachments,relations,children`) no modelo
6
+ * estável {@link Issue} do contrato. Cobre o núcleo (#11) — id, subject,
7
+ * description, refs, datas, journals e attachments — e as coleções aninhadas
8
+ * (#12) delegadas a `./collections.js`: custom_fields, relations, parent/children
9
+ * e watchers.
10
+ *
11
+ * Parsing 100% defensivo (estilo `src/client/issues.ts`): payload parcial ou
12
+ * malformado NUNCA lança — cada campo degrada para um default estável.
13
+ */
14
+ import { normalizeChildren, normalizeCustomFields, normalizeParent, normalizeRelations, normalizeWatchers, } from './collections.js';
15
+ import { asArray, asNumber, asRecord, asString, asStringOrNull, isDefined, normalizeRef, } from './helpers.js';
16
+ /** Ref placeholder para campos obrigatórios do contrato ausentes no payload. */
17
+ // Congelado: instância compartilhada entre todas as issues degradadas — mutação
18
+ // acidental corromperia o placeholder globalmente (modo strict lança).
19
+ const NULL_REF = Object.freeze({ id: 0, name: '' });
20
+ /** Ref obrigatória do contrato: cai para {@link NULL_REF} se ausente/inválida. */
21
+ function requireRef(value) {
22
+ return normalizeRef(value) ?? NULL_REF;
23
+ }
24
+ /**
25
+ * Normaliza um detalhe bruto de journal, preservando o histórico sem interpretar.
26
+ *
27
+ * @param value - Item bruto de `journal.details[]`.
28
+ * @returns O detalhe normalizado, ou `undefined` se não for um objeto.
29
+ */
30
+ function normalizeJournalDetail(value) {
31
+ const record = asRecord(value);
32
+ if (record === undefined)
33
+ return undefined;
34
+ const detail = {
35
+ property: asString(record.property) ?? '',
36
+ name: asString(record.name) ?? '',
37
+ };
38
+ if ('old_value' in record)
39
+ detail.old_value = asStringOrNull(record.old_value);
40
+ if ('new_value' in record)
41
+ detail.new_value = asStringOrNull(record.new_value);
42
+ return detail;
43
+ }
44
+ /**
45
+ * Normaliza uma entrada de journal (nota opcional + `details[]` brutos).
46
+ *
47
+ * @param value - Item bruto de `issue.journals[]`.
48
+ * @returns O journal normalizado, ou `undefined` se não tiver `id` numérico.
49
+ */
50
+ function normalizeJournal(value) {
51
+ const record = asRecord(value);
52
+ if (record === undefined)
53
+ return undefined;
54
+ const id = asNumber(record.id);
55
+ if (id === undefined)
56
+ return undefined;
57
+ const journal = {
58
+ id,
59
+ created_on: asString(record.created_on) ?? '',
60
+ details: asArray(record.details).map(normalizeJournalDetail).filter(isDefined),
61
+ };
62
+ // notes só é significativa quando não-vazia (Redmine devolve "" em journal de detalhe).
63
+ const notes = asString(record.notes);
64
+ if (notes !== undefined && notes !== '')
65
+ journal.notes = notes;
66
+ const user = normalizeRef(record.user);
67
+ if (user !== undefined)
68
+ journal.user = user;
69
+ return journal;
70
+ }
71
+ /**
72
+ * Normaliza um anexo, mantendo `content_type`/`digest` quando presentes.
73
+ *
74
+ * @param value - Item bruto de `issue.attachments[]`.
75
+ * @returns O anexo normalizado, ou `undefined` se não tiver `id` numérico.
76
+ */
77
+ function normalizeAttachment(value) {
78
+ const record = asRecord(value);
79
+ if (record === undefined)
80
+ return undefined;
81
+ const id = asNumber(record.id);
82
+ if (id === undefined)
83
+ return undefined;
84
+ const attachment = {
85
+ id,
86
+ filename: asString(record.filename) ?? '',
87
+ filesize: asNumber(record.filesize) ?? 0,
88
+ created_on: asString(record.created_on) ?? '',
89
+ content_url: asString(record.content_url) ?? '',
90
+ };
91
+ const contentType = asString(record.content_type);
92
+ if (contentType !== undefined)
93
+ attachment.content_type = contentType;
94
+ const description = asString(record.description);
95
+ if (description !== undefined && description !== '')
96
+ attachment.description = description;
97
+ const author = normalizeRef(record.author);
98
+ if (author !== undefined)
99
+ attachment.author = author;
100
+ const digest = asString(record.digest);
101
+ if (digest !== undefined)
102
+ attachment.digest = digest;
103
+ return attachment;
104
+ }
105
+ /**
106
+ * Normaliza o payload bruto de uma issue no modelo {@link Issue} do contrato.
107
+ *
108
+ * Núcleo (#11): id, subject, description, refs, datas, journals e attachments.
109
+ * Coleções (#12): custom_fields, relations, parent/children e watchers. Nunca
110
+ * lança: payload parcial/malformado degrada campo a campo para defaults estáveis.
111
+ *
112
+ * `watchers` só aparece quando a chave está presente no payload; a ausência
113
+ * (include não pedido ou 403) omite o campo como degradação. A API de issue não
114
+ * devolve `field_format` dos custom fields — o chamador pode anotá-lo via
115
+ * `fieldFormats` (ex.: a partir de `/custom_fields.json`).
116
+ *
117
+ * @param payload - Payload bruto de `getIssue`.
118
+ * @param fieldFormats - Mapa opcional `custom_field_id → field_format`.
119
+ * @returns A issue normalizada, sempre válida perante o contrato.
120
+ * @example
121
+ * const issue = normalizeIssue(await getIssue(http, 100));
122
+ */
123
+ export function normalizeIssue(payload, fieldFormats) {
124
+ const record = asRecord(payload) ?? {};
125
+ const issue = {
126
+ id: asNumber(record.id) ?? 0,
127
+ subject: asString(record.subject) ?? '',
128
+ project: requireRef(record.project),
129
+ tracker: requireRef(record.tracker),
130
+ status: requireRef(record.status),
131
+ priority: requireRef(record.priority),
132
+ author: requireRef(record.author),
133
+ created_on: asString(record.created_on) ?? '',
134
+ updated_on: asString(record.updated_on) ?? '',
135
+ journals: asArray(record.journals).map(normalizeJournal).filter(isDefined),
136
+ attachments: asArray(record.attachments).map(normalizeAttachment).filter(isDefined),
137
+ custom_fields: normalizeCustomFields(record.custom_fields, fieldFormats),
138
+ relations: normalizeRelations(record.relations),
139
+ children: normalizeChildren(record.children),
140
+ };
141
+ const description = asString(record.description);
142
+ if (description !== undefined && description !== '')
143
+ issue.description = description;
144
+ const assignedTo = normalizeRef(record.assigned_to);
145
+ if (assignedTo !== undefined)
146
+ issue.assigned_to = assignedTo;
147
+ const parent = normalizeParent(record.parent);
148
+ if (parent !== undefined)
149
+ issue.parent = parent;
150
+ // Ausência da chave `watchers` = degradação (403/include ausente): campo omitido.
151
+ if ('watchers' in record)
152
+ issue.watchers = normalizeWatchers(record.watchers);
153
+ return issue;
154
+ }
@@ -0,0 +1,61 @@
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 type { ParsedArgs, RunDeps } from './types.js';
11
+ /** Exit codes do CLI (documentados no `--help`). */
12
+ export declare const EXIT: {
13
+ /** Erro genérico ou uso inválido. */
14
+ readonly GENERIC: 1;
15
+ /** Falha de autenticação ou credencial ausente. */
16
+ readonly AUTH: 2;
17
+ /** Erro de rede ou HTTP. */
18
+ readonly NETWORK: 3;
19
+ /** Issue inexistente. */
20
+ readonly NOT_FOUND: 4;
21
+ };
22
+ /**
23
+ * Mapeia um erro para o exit code do CLI.
24
+ *
25
+ * Ordem importa: os erros específicos (404, 401) estendem `RedmineHttpError` e
26
+ * precisam ser testados antes do genérico HTTP. Erros de rede (sem status) são
27
+ * detectados pela mensagem do client e também caem em {@link EXIT.NETWORK}.
28
+ *
29
+ * @param error - Erro capturado durante a operação.
30
+ * @returns O exit code correspondente.
31
+ */
32
+ export declare function exitCodeForError(error: unknown): number;
33
+ /**
34
+ * Comando `issue <id>`: resolve credencial pela cascata, empacota e emite o
35
+ * bundle (stdout ou `--out <dir>`), com progresso em stderr.
36
+ *
37
+ * @param parsed - Argumentos parseados (posicional `<id>` + flags).
38
+ * @param deps - Dependências injetáveis (I/O, env).
39
+ * @returns Exit code do processo.
40
+ */
41
+ export declare function runIssue(parsed: ParsedArgs, deps: RunDeps): Promise<number>;
42
+ /**
43
+ * Comando `doctor`: diagnostica os binários de mídia (hoje o `tesseract`) e
44
+ * imprime um relatório em TEXTO PURO no stdout. Degrada naturalmente em
45
+ * `NO_COLOR`/não-TTY (não emite cor/ANSI). Exit 0 se todos presentes, 1 se
46
+ * faltar algum — o exit code deixa o resultado programável em scripts.
47
+ *
48
+ * @param _parsed - Argumentos parseados (o comando não usa flags hoje).
49
+ * @param deps - Dependências injetáveis (I/O).
50
+ * @returns Exit code do processo (0 = tudo ok, 1 = binário faltando).
51
+ */
52
+ export declare function runDoctor(_parsed: ParsedArgs, deps: RunDeps): Promise<number>;
53
+ /**
54
+ * Comando `login`: autentica (senha ou `--api-key`) e salva a api_key na
55
+ * cascata para a instância informada.
56
+ *
57
+ * @param parsed - Argumentos parseados (flags `--url`, `--api-key`, `--insecure`).
58
+ * @param deps - Dependências injetáveis (prompts, I/O, env).
59
+ * @returns Exit code do processo.
60
+ */
61
+ export declare function runLogin(parsed: ParsedArgs, deps: RunDeps): Promise<number>;