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,71 @@
1
+ /**
2
+ * Operações de leitura de issues sobre o {@link HttpClient} do M1.
3
+ *
4
+ * Escopo (issue #9): apenas `getIssue` e `listIssues`. Sem retry/backoff
5
+ * (issue #10) e SEM normalização (issue #11) — o payload é devolvido bruto,
6
+ * tipado de forma rasa. A normalização para o modelo {@link Issue} do contrato
7
+ * acontece em outra camada.
8
+ */
9
+ import type { HttpClient, QueryParams } from './http.js';
10
+ /**
11
+ * Payload bruto de uma issue, exatamente como a API do Redmine devolve.
12
+ * Tipo raso e intencionalmente permissivo: só garante `id`; os demais campos
13
+ * (subject, journals, attachments, custom_fields, ...) ficam sob a assinatura
14
+ * de índice e serão interpretados na normalização (issue #11).
15
+ */
16
+ export interface RedmineIssuePayload {
17
+ id: number;
18
+ [key: string]: unknown;
19
+ }
20
+ /** Opções de {@link listIssues}: filtros de query + controle de paginação. */
21
+ export interface ListIssuesOptions {
22
+ /**
23
+ * Filtros repassados ao endpoint `/issues.json` (ex.: `{ project_id: 5,
24
+ * status_id: 'open' }`). São enviados junto de `offset`/`limit` em cada página.
25
+ */
26
+ filters?: QueryParams;
27
+ /**
28
+ * Itens por página (parâmetro `limit`). Default e teto: {@link REDMINE_MAX_LIMIT}.
29
+ * Valores acima do teto são reduzidos silenciosamente para 100.
30
+ */
31
+ pageSize?: number;
32
+ /**
33
+ * Máximo de itens a acumular no total. Ausente = esgota toda a coleção
34
+ * (respeitando `total_count`). Também reduz o `limit` da última página para
35
+ * não buscar além do necessário.
36
+ */
37
+ maxItems?: number;
38
+ }
39
+ /**
40
+ * Busca o detalhe completo de uma issue com todas as coleções aninhadas.
41
+ *
42
+ * Usa `include=journals,attachments,relations,children` e devolve o payload
43
+ * BRUTO (sem normalizar). A resposta do Redmine tem o formato `{ issue: {...} }`;
44
+ * o objeto interno é retornado tipado como {@link RedmineIssuePayload}.
45
+ *
46
+ * @param http - Client HTTP autenticado (ver {@link HttpClient}).
47
+ * @param id - Identificador da issue.
48
+ * @returns O payload bruto da issue.
49
+ * @throws {Error} Se a resposta não contiver a chave `issue`.
50
+ * @throws {RedmineHttpError} Propagado pelo client em respostas ≥ 400.
51
+ * @example
52
+ * const issue = await getIssue(http, 100);
53
+ */
54
+ export declare function getIssue(http: HttpClient, id: number): Promise<RedmineIssuePayload>;
55
+ /**
56
+ * Lista issues paginando `/issues.json` via `offset`/`limit` até esgotar a
57
+ * coleção (respeitando `total_count`) ou atingir `maxItems`.
58
+ *
59
+ * Cada página respeita o teto de {@link REDMINE_MAX_LIMIT} do Redmine. Quando
60
+ * `maxItems` está definido, o `limit` da última página é reduzido para não
61
+ * buscar itens além do necessário. Sem retry (issue #10).
62
+ *
63
+ * @param http - Client HTTP autenticado (ver {@link HttpClient}).
64
+ * @param options - Filtros e controle de paginação (ver {@link ListIssuesOptions}).
65
+ * @returns Array acumulado com os payloads brutos das issues.
66
+ * @throws {Error} Se alguma página não trouxer o array `issues`.
67
+ * @throws {RedmineHttpError} Propagado pelo client em respostas ≥ 400.
68
+ * @example
69
+ * const all = await listIssues(http, { filters: { project_id: 5 }, maxItems: 500 });
70
+ */
71
+ export declare function listIssues(http: HttpClient, options?: ListIssuesOptions): Promise<RedmineIssuePayload[]>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Operações de leitura de issues sobre o {@link HttpClient} do M1.
3
+ *
4
+ * Escopo (issue #9): apenas `getIssue` e `listIssues`. Sem retry/backoff
5
+ * (issue #10) e SEM normalização (issue #11) — o payload é devolvido bruto,
6
+ * tipado de forma rasa. A normalização para o modelo {@link Issue} do contrato
7
+ * acontece em outra camada.
8
+ */
9
+ /** Include padrão do detalhe de issue (ADR-005): histórico e coleções aninhadas. */
10
+ const ISSUE_INCLUDE = 'journals,attachments,relations,children';
11
+ /** Teto de itens por página imposto pelo Redmine ao parâmetro `limit`. */
12
+ const REDMINE_MAX_LIMIT = 100;
13
+ /**
14
+ * Converte um valor desconhecido em `Record<string, unknown>` para acesso seguro.
15
+ *
16
+ * @param value - Valor a inspecionar.
17
+ * @returns O objeto tipado, ou `undefined` se não for um objeto simples.
18
+ */
19
+ function asRecord(value) {
20
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
21
+ return value;
22
+ }
23
+ return undefined;
24
+ }
25
+ /**
26
+ * Busca o detalhe completo de uma issue com todas as coleções aninhadas.
27
+ *
28
+ * Usa `include=journals,attachments,relations,children` e devolve o payload
29
+ * BRUTO (sem normalizar). A resposta do Redmine tem o formato `{ issue: {...} }`;
30
+ * o objeto interno é retornado tipado como {@link RedmineIssuePayload}.
31
+ *
32
+ * @param http - Client HTTP autenticado (ver {@link HttpClient}).
33
+ * @param id - Identificador da issue.
34
+ * @returns O payload bruto da issue.
35
+ * @throws {Error} Se a resposta não contiver a chave `issue`.
36
+ * @throws {RedmineHttpError} Propagado pelo client em respostas ≥ 400.
37
+ * @example
38
+ * const issue = await getIssue(http, 100);
39
+ */
40
+ export async function getIssue(http, id) {
41
+ const raw = await http.get(`/issues/${id}.json`, { include: ISSUE_INCLUDE });
42
+ const body = asRecord(raw);
43
+ const issue = body === undefined ? undefined : asRecord(body.issue);
44
+ if (issue === undefined) {
45
+ throw new Error(`Resposta de /issues/${id}.json não contém a chave "issue".`);
46
+ }
47
+ return issue;
48
+ }
49
+ /** Extrai `issues[]` e `total_count` de uma página de `/issues.json`. */
50
+ function parsePage(raw) {
51
+ const body = asRecord(raw);
52
+ const issues = body?.issues;
53
+ if (!Array.isArray(issues)) {
54
+ throw new Error('Resposta de /issues.json não contém o array "issues".');
55
+ }
56
+ // total_count pode faltar em respostas inesperadas: assume o tamanho da página.
57
+ const totalCount = typeof body?.total_count === 'number' ? body.total_count : issues.length;
58
+ return { issues: issues, totalCount };
59
+ }
60
+ /**
61
+ * Lista issues paginando `/issues.json` via `offset`/`limit` até esgotar a
62
+ * coleção (respeitando `total_count`) ou atingir `maxItems`.
63
+ *
64
+ * Cada página respeita o teto de {@link REDMINE_MAX_LIMIT} do Redmine. Quando
65
+ * `maxItems` está definido, o `limit` da última página é reduzido para não
66
+ * buscar itens além do necessário. Sem retry (issue #10).
67
+ *
68
+ * @param http - Client HTTP autenticado (ver {@link HttpClient}).
69
+ * @param options - Filtros e controle de paginação (ver {@link ListIssuesOptions}).
70
+ * @returns Array acumulado com os payloads brutos das issues.
71
+ * @throws {Error} Se alguma página não trouxer o array `issues`.
72
+ * @throws {RedmineHttpError} Propagado pelo client em respostas ≥ 400.
73
+ * @example
74
+ * const all = await listIssues(http, { filters: { project_id: 5 }, maxItems: 500 });
75
+ */
76
+ export async function listIssues(http, options = {}) {
77
+ const { filters = {}, maxItems } = options;
78
+ const pageSize = Math.min(options.pageSize ?? REDMINE_MAX_LIMIT, REDMINE_MAX_LIMIT);
79
+ const acc = [];
80
+ let offset = 0;
81
+ for (;;) {
82
+ // Reduz o limit da última página quando maxItems se aproxima do total.
83
+ const remaining = maxItems === undefined ? pageSize : Math.min(pageSize, maxItems - acc.length);
84
+ if (remaining <= 0) {
85
+ break;
86
+ }
87
+ const params = { ...filters, offset, limit: remaining };
88
+ const { issues, totalCount } = parsePage(await http.get('/issues.json', params));
89
+ acc.push(...issues);
90
+ offset += issues.length;
91
+ // Para em página vazia (guarda contra loop infinito) ou ao esgotar a coleção.
92
+ if (issues.length === 0 || offset >= totalCount) {
93
+ break;
94
+ }
95
+ if (maxItems !== undefined && acc.length >= maxItems) {
96
+ break;
97
+ }
98
+ }
99
+ return acc;
100
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Busca full-text de issues via `/search.json` (M1-13).
3
+ *
4
+ * Camada baixa sobre o {@link HttpClient} do M1: dispara `GET /search.json` com o
5
+ * termo `q` restrito a issues (`issues=1`) e devolve os acertos de forma rasa
6
+ * (id/título/tipo/url), SEM normalizar. É best-effort por natureza — um Redmine
7
+ * sem o módulo de busca responde 404; o orquestrador ({@link fetchIssueSearch})
8
+ * captura a falha e degrada para os filtros estruturados.
9
+ *
10
+ * Nunca recebe URL/host: o {@link HttpClient} já carrega a instância/credencial.
11
+ */
12
+ import type { HttpClient } from './http.js';
13
+ /** Opções da busca full-text sobre `/search.json`. */
14
+ export interface SearchIssuesOptions {
15
+ /** Termo de busca (`q`). Deve ser não vazio (validado na superfície). */
16
+ query: string;
17
+ /** Deslocamento de paginação (`offset`). Default: `0`. */
18
+ offset?: number;
19
+ /** Itens por página (`limit`). Default e teto: {@link SEARCH_MAX_LIMIT}. */
20
+ limit?: number;
21
+ }
22
+ /** Acerto raso de `/search.json` já filtrado para o tipo `issue`. */
23
+ export interface RedmineSearchHit {
24
+ /** Id da issue correspondente ao acerto. */
25
+ id: number;
26
+ /** Título do acerto como a API devolve (ex.: `Bug #12: assunto`). */
27
+ title: string;
28
+ /** Tipo do acerto (`issue`, `issue-closed`, ...). */
29
+ type: string;
30
+ /** URL do acerto na instância Redmine. */
31
+ url: string;
32
+ }
33
+ /** Página de resultados de `/search.json`: acertos + total reportado. */
34
+ export interface SearchIssuesPage {
35
+ /** Acertos do tipo issue, na ordem devolvida pela API. */
36
+ hits: RedmineSearchHit[];
37
+ /** `total_count` reportado (ou o tamanho da página se ausente). */
38
+ totalCount: number;
39
+ }
40
+ /** Teto de itens por página imposto ao parâmetro `limit` de `/search.json`. */
41
+ export declare const SEARCH_MAX_LIMIT = 100;
42
+ /**
43
+ * Busca issues por texto livre em `/search.json`, restrito a issues.
44
+ *
45
+ * Devolve apenas os acertos do tipo issue (ids reutilizáveis como filtro
46
+ * `issue_id` em `/issues.json`). Não retenta e não normaliza — propaga os erros
47
+ * tipados do {@link HttpClient} (ex.: 404 quando a busca está indisponível) para
48
+ * o chamador decidir a degradação.
49
+ *
50
+ * @param http - Client HTTP autenticado (ver {@link HttpClient}).
51
+ * @param options - Termo e paginação (ver {@link SearchIssuesOptions}).
52
+ * @returns Página com os acertos de issue e o `total_count` reportado.
53
+ * @throws {Error} Se a resposta não contiver o array `results`.
54
+ * @throws {RedmineHttpError} Propagado pelo client em respostas ≥ 400.
55
+ * @example
56
+ * const page = await searchIssues(http, { query: 'timeout', limit: 25 });
57
+ */
58
+ export declare function searchIssues(http: HttpClient, options: SearchIssuesOptions): Promise<SearchIssuesPage>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Busca full-text de issues via `/search.json` (M1-13).
3
+ *
4
+ * Camada baixa sobre o {@link HttpClient} do M1: dispara `GET /search.json` com o
5
+ * termo `q` restrito a issues (`issues=1`) e devolve os acertos de forma rasa
6
+ * (id/título/tipo/url), SEM normalizar. É best-effort por natureza — um Redmine
7
+ * sem o módulo de busca responde 404; o orquestrador ({@link fetchIssueSearch})
8
+ * captura a falha e degrada para os filtros estruturados.
9
+ *
10
+ * Nunca recebe URL/host: o {@link HttpClient} já carrega a instância/credencial.
11
+ */
12
+ /** Teto de itens por página imposto ao parâmetro `limit` de `/search.json`. */
13
+ export const SEARCH_MAX_LIMIT = 100;
14
+ /**
15
+ * Converte um valor desconhecido em `Record<string, unknown>` para acesso seguro.
16
+ *
17
+ * @param value - Valor a inspecionar.
18
+ * @returns O objeto tipado, ou `undefined` se não for um objeto simples.
19
+ */
20
+ function asRecord(value) {
21
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
22
+ return value;
23
+ }
24
+ return undefined;
25
+ }
26
+ /** Extrai um acerto do array `results`, aceitando apenas os do tipo `issue*`. */
27
+ function parseHit(entry) {
28
+ const rec = asRecord(entry);
29
+ if (rec === undefined)
30
+ return undefined;
31
+ const { id, type } = rec;
32
+ // Reason: o /search.json mistura tipos (issue, wiki-page, ...); só issues nos
33
+ // interessam. Redmine varia entre `issue` e `issue-closed` — casamos o prefixo.
34
+ if (typeof id !== 'number' || typeof type !== 'string' || !type.startsWith('issue')) {
35
+ return undefined;
36
+ }
37
+ return {
38
+ id,
39
+ type,
40
+ title: typeof rec.title === 'string' ? rec.title : '',
41
+ url: typeof rec.url === 'string' ? rec.url : '',
42
+ };
43
+ }
44
+ /**
45
+ * Busca issues por texto livre em `/search.json`, restrito a issues.
46
+ *
47
+ * Devolve apenas os acertos do tipo issue (ids reutilizáveis como filtro
48
+ * `issue_id` em `/issues.json`). Não retenta e não normaliza — propaga os erros
49
+ * tipados do {@link HttpClient} (ex.: 404 quando a busca está indisponível) para
50
+ * o chamador decidir a degradação.
51
+ *
52
+ * @param http - Client HTTP autenticado (ver {@link HttpClient}).
53
+ * @param options - Termo e paginação (ver {@link SearchIssuesOptions}).
54
+ * @returns Página com os acertos de issue e o `total_count` reportado.
55
+ * @throws {Error} Se a resposta não contiver o array `results`.
56
+ * @throws {RedmineHttpError} Propagado pelo client em respostas ≥ 400.
57
+ * @example
58
+ * const page = await searchIssues(http, { query: 'timeout', limit: 25 });
59
+ */
60
+ export async function searchIssues(http, options) {
61
+ const limit = Math.min(options.limit ?? SEARCH_MAX_LIMIT, SEARCH_MAX_LIMIT);
62
+ const params = {
63
+ q: options.query,
64
+ issues: 1,
65
+ limit,
66
+ offset: options.offset ?? 0,
67
+ };
68
+ const body = asRecord(await http.get('/search.json', params));
69
+ const results = body?.results;
70
+ if (!Array.isArray(results)) {
71
+ throw new Error('Resposta de /search.json não contém o array "results".');
72
+ }
73
+ const hits = [];
74
+ for (const entry of results) {
75
+ const hit = parseHit(entry);
76
+ if (hit !== undefined)
77
+ hits.push(hit);
78
+ }
79
+ const totalCount = typeof body?.total_count === 'number' ? body.total_count : hits.length;
80
+ return { hits, totalCount };
81
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Credential store da api_key por instância (M1-08).
3
+ *
4
+ * Persiste a api_key em um arquivo JSON `{ "<instance>": { "apiKey": "..." } }`
5
+ * com permissão `0600` dentro de um diretório `0700` — uma entrada por URL base
6
+ * normalizada, suportando múltiplas instâncias Redmine. O caminho padrão vem de
7
+ * `env-paths` (diretório de config do usuário).
8
+ *
9
+ * A resolução segue a cascata do ADR-003: keychain → arquivo → variável de
10
+ * ambiente `REDMINE_API_KEY` (o env é sempre aceito, para CI/MCP headless). A
11
+ * interface {@link CredentialStore} (get/set/delete) é comum a todas as
12
+ * implementações; a {@link MigratingCredentialCascade} as consulta em ordem e,
13
+ * quando o keychain está disponível, migra a chave do arquivo para ele.
14
+ *
15
+ * A api_key nunca é logada e nunca aparece em mensagens de erro — os erros de
16
+ * permissão carregam apenas caminhos e o comando `chmod` a ser executado. A
17
+ * verificação de permissão POSIX fica atrás de `process.platform !== 'win32'`
18
+ * porque o Windows não tem o mesmo modelo de bits `0600` (o hardening real do
19
+ * Windows é o M5-08).
20
+ */
21
+ import type { Logger } from '../client/index.js';
22
+ import { KeyringCredentialStore, type KeyringModuleLoader } from './keyring.js';
23
+ /**
24
+ * Contrato de armazenamento de api_key por instância. A implementação de
25
+ * arquivo (M1) e a de keychain (M2) satisfazem esta interface, o que permite à
26
+ * cascata tratá-las de forma uniforme.
27
+ */
28
+ export interface CredentialStore {
29
+ /**
30
+ * Recupera a api_key da instância, ou `undefined` se não houver.
31
+ * @param instance - URL base da instância (será normalizada).
32
+ */
33
+ get(instance: string): Promise<string | undefined>;
34
+ /**
35
+ * Persiste (ou substitui) a api_key da instância.
36
+ * @param instance - URL base da instância (será normalizada).
37
+ * @param apiKey - api_key a armazenar.
38
+ */
39
+ set(instance: string, apiKey: string): Promise<void>;
40
+ /**
41
+ * Remove a api_key da instância; no-op se não existir.
42
+ * @param instance - URL base da instância (será normalizada).
43
+ */
44
+ delete(instance: string): Promise<void>;
45
+ }
46
+ /** Erro do credential store — sempre construído sem expor a api_key. */
47
+ export declare class CredentialStoreError extends Error {
48
+ constructor(message: string);
49
+ }
50
+ /**
51
+ * Normaliza a URL base de uma instância para uma chave estável.
52
+ *
53
+ * Aplica minúsculas (protocolo e host), remove a porta padrão (443 p/ https, 80
54
+ * p/ http), remove a barra final e preserva o subpath. Mantém a mesma ideia do
55
+ * futuro `instance_hash`: formas equivalentes colapsam na mesma chave.
56
+ *
57
+ * @param instance - URL base da instância (ex.: `https://Redmine.Example/`).
58
+ * @returns A URL normalizada (ex.: `https://redmine.example`).
59
+ * @throws {TypeError} Se `instance` não for uma URL válida.
60
+ * @example
61
+ * normalizeInstanceUrl('HTTPS://Redmine.Example:443/'); // 'https://redmine.example'
62
+ */
63
+ export declare function normalizeInstanceUrl(instance: string): string;
64
+ /** Caminho padrão do arquivo de credenciais (diretório de config do usuário). */
65
+ export declare function defaultCredentialsPath(): string;
66
+ /** Opções do {@link FileCredentialStore}. */
67
+ export interface FileCredentialStoreOptions {
68
+ /** Caminho do arquivo; default via `env-paths`. Útil para testes. */
69
+ filePath?: string;
70
+ }
71
+ /**
72
+ * Implementação de {@link CredentialStore} baseada em arquivo `0600` num
73
+ * diretório `0700`. Verifica as permissões ao ler (boot) e as reaplica ao
74
+ * escrever, independentemente do umask.
75
+ */
76
+ export declare class FileCredentialStore implements CredentialStore {
77
+ private readonly filePath;
78
+ /** @param options - Ver {@link FileCredentialStoreOptions}. */
79
+ constructor(options?: FileCredentialStoreOptions);
80
+ /** @inheritdoc */
81
+ get(instance: string): Promise<string | undefined>;
82
+ /** @inheritdoc */
83
+ set(instance: string, apiKey: string): Promise<void>;
84
+ /** @inheritdoc */
85
+ delete(instance: string): Promise<void>;
86
+ /** Lê e valida o arquivo; `undefined` se ainda não existir. */
87
+ private read;
88
+ /** Escreve o arquivo garantindo dir `0700` e arquivo `0600`. */
89
+ private write;
90
+ /** Falha com erro acionável se as permissões do arquivo estiverem erradas. */
91
+ private assertSecureMode;
92
+ }
93
+ /** Opções do {@link EnvCredentialStore}. */
94
+ export interface EnvCredentialStoreOptions {
95
+ /** Nome da variável de ambiente; default `REDMINE_API_KEY`. */
96
+ varName?: string;
97
+ /** Ambiente a consultar; default `process.env` (injetável em testes). */
98
+ env?: NodeJS.ProcessEnv;
99
+ }
100
+ /**
101
+ * Implementação de {@link CredentialStore} somente-leitura sobre uma variável
102
+ * de ambiente. É o fallback headless (CI/MCP) da cascata: `get` devolve a chave
103
+ * do env para qualquer instância; `set`/`delete` lançam.
104
+ */
105
+ export declare class EnvCredentialStore implements CredentialStore {
106
+ private readonly varName;
107
+ private readonly env;
108
+ /** @param options - Ver {@link EnvCredentialStoreOptions}. */
109
+ constructor(options?: EnvCredentialStoreOptions);
110
+ /** @inheritdoc */
111
+ get(): Promise<string | undefined>;
112
+ /** Somente leitura: sempre lança. */
113
+ set(): Promise<void>;
114
+ /** Somente leitura: sempre lança. */
115
+ delete(): Promise<void>;
116
+ }
117
+ /**
118
+ * Cascata de {@link CredentialStore}: `get` consulta os stores em ordem e
119
+ * devolve o primeiro valor encontrado; escritas vão para o primeiro store (o
120
+ * gravável). A ordem M1 é arquivo → env.
121
+ */
122
+ export declare class CascadingCredentialStore implements CredentialStore {
123
+ private readonly stores;
124
+ /**
125
+ * @param stores - Stores em ordem de prioridade (o primeiro é o gravável).
126
+ * @throws {CredentialStoreError} Se a lista estiver vazia.
127
+ */
128
+ constructor(stores: readonly CredentialStore[]);
129
+ /** @inheritdoc */
130
+ get(instance: string): Promise<string | undefined>;
131
+ /** @inheritdoc */
132
+ set(instance: string, apiKey: string): Promise<void>;
133
+ /** @inheritdoc */
134
+ delete(instance: string): Promise<void>;
135
+ /** O primeiro store da cascata — destino das escritas. */
136
+ private writableStore;
137
+ }
138
+ /**
139
+ * Cascata de credenciais M2 com migração para o keychain (ADR-003).
140
+ *
141
+ * A resolução (`get`) segue a ordem keychain → arquivo `0600` → env; o primeiro
142
+ * valor encontrado vence. Quando a chave vem do ARQUIVO e o keychain está
143
+ * DISPONÍVEL, a chave é migrada — gravada no keychain e só então removida do
144
+ * arquivo (a remoção é condicionada à confirmação da gravação, evitando perda da
145
+ * credencial se o keychain degradar) — com um único aviso informativo por
146
+ * cascata. As escritas (`set`) preferem o keychain quando disponível, com
147
+ * fallback para o arquivo; o env é somente leitura. A api_key nunca é logada.
148
+ */
149
+ export declare class MigratingCredentialCascade implements CredentialStore {
150
+ private readonly keyring;
151
+ private readonly file;
152
+ private readonly env;
153
+ private readonly logger;
154
+ /** Garante que o aviso de migração seja emitido uma única vez por cascata. */
155
+ private migrationWarned;
156
+ /**
157
+ * @param keyring - Store do keychain nativo (fonte prioritária e destino da migração).
158
+ * @param file - Store de arquivo `0600` (fallback e origem da migração).
159
+ * @param env - Store de ambiente somente leitura (fallback headless).
160
+ * @param logger - Logger do aviso único de migração; default no-op.
161
+ */
162
+ constructor(keyring: KeyringCredentialStore, file: CredentialStore, env: CredentialStore, logger?: Logger);
163
+ /** @inheritdoc */
164
+ get(instance: string): Promise<string | undefined>;
165
+ /** @inheritdoc */
166
+ set(instance: string, apiKey: string): Promise<void>;
167
+ /** @inheritdoc */
168
+ delete(instance: string): Promise<void>;
169
+ /**
170
+ * Identifica QUAL fonte resolveria a credencial da instância (keychain →
171
+ * arquivo → env), sem expor a api_key e SEM disparar a migração
172
+ * arquivo→keychain feita por {@link get} — diagnóstico somente-leitura
173
+ * usado pela tela `doctor` da TUI (#35), que só precisa mostrar o método em
174
+ * uso, nunca o segredo.
175
+ *
176
+ * @param instance - URL base da instância (será normalizada pelos stores).
177
+ * @returns A fonte em uso, ou `'none'` se nenhuma tiver a credencial.
178
+ */
179
+ describeSource(instance: string): Promise<CredentialSourceKind>;
180
+ /**
181
+ * Migra a chave do arquivo para o keychain quando este está disponível. A
182
+ * remoção do arquivo só ocorre após confirmar a gravação no keychain, evitando
183
+ * perda da credencial se a escrita degradar. Emite um único aviso, sem expor a
184
+ * api_key.
185
+ *
186
+ * @param instance - URL base da instância (será normalizada pelos stores).
187
+ * @param apiKey - Chave lida do arquivo a ser migrada.
188
+ */
189
+ private migrateFromFile;
190
+ }
191
+ /**
192
+ * Fonte que resolveria a credencial de uma instância na cascata M2, sem
193
+ * expor a api_key em si (consumido pela tela `doctor` da TUI, #35).
194
+ * `'none'` quando nenhuma das três fontes tem a credencial.
195
+ */
196
+ export type CredentialSourceKind = 'keyring' | 'file' | 'env' | 'none';
197
+ /** Opções de {@link createCredentialCascade} / {@link resolveApiKey}. */
198
+ export interface CredentialCascadeOptions {
199
+ /** Caminho do arquivo de credenciais; default via `env-paths`. */
200
+ filePath?: string;
201
+ /** Nome da variável de ambiente; default `REDMINE_API_KEY`. */
202
+ envVarName?: string;
203
+ /** Ambiente a consultar; default `process.env`. */
204
+ env?: NodeJS.ProcessEnv;
205
+ /** Logger para o aviso único de migração; default no-op. */
206
+ logger?: Logger;
207
+ /** Service do keychain; default `redmine-context`. */
208
+ keyringService?: string;
209
+ /** Carregador do módulo nativo do keychain; injetável em testes. */
210
+ keyringLoader?: KeyringModuleLoader;
211
+ /**
212
+ * Permite a credencial de AMBIENTE (`REDMINE_API_KEY`) como fallback. Default
213
+ * `true` (retrocompatível). SEGURANÇA (#187): a env-key é INSTANCE-AGNÓSTICA
214
+ * (o `EnvCredentialStore` a devolve para QUALQUER URL). Quando a URL da
215
+ * instância vem de uma fonte MUTÁVEL/menos confiável (ex.: `settings.json`
216
+ * persistido, origem `config`), passe `false` — assim uma URL adulterada não
217
+ * pode fazer a chave real ser enviada a um host arbitrário; só credencial
218
+ * PINADA à instância (keychain/arquivo) é aceita (fail-closed).
219
+ */
220
+ allowEnvFallback?: boolean;
221
+ }
222
+ export declare function createCredentialCascade(options?: CredentialCascadeOptions): MigratingCredentialCascade;
223
+ /**
224
+ * Resolve a api_key de uma instância pela cascata M2 (keychain → arquivo → env),
225
+ * migrando do arquivo para o keychain quando este estiver disponível.
226
+ *
227
+ * @param instance - URL base da instância (será normalizada).
228
+ * @param options - Ver {@link CredentialCascadeOptions}.
229
+ * @returns A api_key resolvida, ou `undefined` se nenhuma fonte a tiver.
230
+ * @example
231
+ * const apiKey = await resolveApiKey('https://redmine.example');
232
+ */
233
+ export declare function resolveApiKey(instance: string, options?: CredentialCascadeOptions): Promise<string | undefined>;
234
+ /**
235
+ * Identifica a fonte que resolveria a credencial de uma instância na cascata
236
+ * M2 (keychain → arquivo → env), SEM expor a api_key e sem migrar do arquivo
237
+ * para o keychain — ver {@link MigratingCredentialCascade.describeSource}.
238
+ * Usada pela tela `doctor` da TUI (#35) para relatar o método de credencial
239
+ * em uso.
240
+ *
241
+ * @param instance - URL base da instância (será normalizada).
242
+ * @param options - Ver {@link CredentialCascadeOptions}.
243
+ * @returns A fonte em uso ('keyring' | 'file' | 'env'), ou 'none'.
244
+ * @example
245
+ * const source = await describeCredentialSource('https://redmine.example');
246
+ */
247
+ export declare function describeCredentialSource(instance: string, options?: CredentialCascadeOptions): Promise<CredentialSourceKind>;