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.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- package/package.json +93 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatação humanizada de tamanho de arquivo (#32) — usada pela seção de
|
|
3
|
+
* anexos do detalhe da issue para exibir `Attachment.filesize` (bytes,
|
|
4
|
+
* inteiro bruto do Redmine) em um texto curto (B/KB/MB/GB), sem depender de
|
|
5
|
+
* biblioteca externa para uma conta tão simples.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Formata `bytes` como texto humanizado (ex.: `999B`, `1.5KB`, `2MB`).
|
|
9
|
+
*
|
|
10
|
+
* Escala para a maior unidade em que o valor fica `>= 1` (mas `< 1024`),
|
|
11
|
+
* arredondando para 1 casa decimal — sem casa decimal supérflua quando o
|
|
12
|
+
* valor é inteiro (`1KB`, não `1.0KB`).
|
|
13
|
+
*
|
|
14
|
+
* @param bytes - Tamanho em bytes (`Attachment.filesize`). Valores negativos
|
|
15
|
+
* nunca deveriam ocorrer (o core normaliza para `>= 0`), mas são tratados
|
|
16
|
+
* defensivamente como `0` em vez de lançar — consistente com o resto da
|
|
17
|
+
* normalização (ADR-005: nunca crashar por payload inesperado).
|
|
18
|
+
* @returns O texto humanizado.
|
|
19
|
+
* @example
|
|
20
|
+
* humanizeFileSize(999) // "999B"
|
|
21
|
+
* humanizeFileSize(1536) // "1.5KB"
|
|
22
|
+
* humanizeFileSize(2 * 1024 * 1024) // "2MB"
|
|
23
|
+
*/
|
|
24
|
+
export declare function humanizeFileSize(bytes: number): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatação humanizada de tamanho de arquivo (#32) — usada pela seção de
|
|
3
|
+
* anexos do detalhe da issue para exibir `Attachment.filesize` (bytes,
|
|
4
|
+
* inteiro bruto do Redmine) em um texto curto (B/KB/MB/GB), sem depender de
|
|
5
|
+
* biblioteca externa para uma conta tão simples.
|
|
6
|
+
*/
|
|
7
|
+
/** Unidades usadas na escalada progressiva (índice 0 = bytes, sem sufixo de escala). */
|
|
8
|
+
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
9
|
+
/**
|
|
10
|
+
* Formata `bytes` como texto humanizado (ex.: `999B`, `1.5KB`, `2MB`).
|
|
11
|
+
*
|
|
12
|
+
* Escala para a maior unidade em que o valor fica `>= 1` (mas `< 1024`),
|
|
13
|
+
* arredondando para 1 casa decimal — sem casa decimal supérflua quando o
|
|
14
|
+
* valor é inteiro (`1KB`, não `1.0KB`).
|
|
15
|
+
*
|
|
16
|
+
* @param bytes - Tamanho em bytes (`Attachment.filesize`). Valores negativos
|
|
17
|
+
* nunca deveriam ocorrer (o core normaliza para `>= 0`), mas são tratados
|
|
18
|
+
* defensivamente como `0` em vez de lançar — consistente com o resto da
|
|
19
|
+
* normalização (ADR-005: nunca crashar por payload inesperado).
|
|
20
|
+
* @returns O texto humanizado.
|
|
21
|
+
* @example
|
|
22
|
+
* humanizeFileSize(999) // "999B"
|
|
23
|
+
* humanizeFileSize(1536) // "1.5KB"
|
|
24
|
+
* humanizeFileSize(2 * 1024 * 1024) // "2MB"
|
|
25
|
+
*/
|
|
26
|
+
export function humanizeFileSize(bytes) {
|
|
27
|
+
const safeBytes = bytes > 0 ? bytes : 0;
|
|
28
|
+
let value = safeBytes;
|
|
29
|
+
let unitIndex = 0;
|
|
30
|
+
while (value >= 1024 && unitIndex < UNITS.length - 1) {
|
|
31
|
+
value /= 1024;
|
|
32
|
+
unitIndex += 1;
|
|
33
|
+
}
|
|
34
|
+
let rounded = Math.round(value * 10) / 10;
|
|
35
|
+
// O arredondamento pode "estourar" a unidade (1048575 bytes → 1024KB):
|
|
36
|
+
// nesse caso, escala mais um degrau para exibir 1MB, não 1024KB.
|
|
37
|
+
if (rounded >= 1024 && unitIndex < UNITS.length - 1) {
|
|
38
|
+
rounded = Math.round((rounded / 1024) * 10) / 10;
|
|
39
|
+
unitIndex += 1;
|
|
40
|
+
}
|
|
41
|
+
const formatted = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
42
|
+
return `${formatted}${UNITS[unitIndex]}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Espelha a heurística de `is-unicode-supported@2` (a mesma que `figures`
|
|
3
|
+
* consome) como função pura e injetável.
|
|
4
|
+
*
|
|
5
|
+
* Reason: `figures` decide o fallback UMA vez, no import, a partir do ambiente
|
|
6
|
+
* real — para orçarmos os glyphs "common" pelo MESMO critério (e testá-lo com
|
|
7
|
+
* ambientes simulados) precisamos da decisão exposta como função. Manter a
|
|
8
|
+
* lógica idêntica à da lib garante que os dois caminhos concordem.
|
|
9
|
+
*
|
|
10
|
+
* @param env - Ambiente do processo (ou um subconjunto controlado em testes).
|
|
11
|
+
* @param platform - Plataforma (`process.platform`); injetável para testes.
|
|
12
|
+
* @returns `true` quando o terminal deve renderizar glyphs Unicode.
|
|
13
|
+
* @example
|
|
14
|
+
* isUnicodeSupported({ WT_SESSION: '1' }, 'win32'); // true (Windows Terminal)
|
|
15
|
+
* isUnicodeSupported({}, 'win32'); // false (cmd.exe/PowerShell legado)
|
|
16
|
+
*/
|
|
17
|
+
export declare function isUnicodeSupported(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
18
|
+
/** Conjunto de glyphs "common" que a TUI usa fora de `figures`. */
|
|
19
|
+
export interface Glyphs {
|
|
20
|
+
/** Marcador de corte do truncamento (`../truncate.ts`). */
|
|
21
|
+
readonly ellipsis: string;
|
|
22
|
+
/** Separador entre campos de uma linha (ex.: `tamanho · tipo`). */
|
|
23
|
+
readonly middleDot: string;
|
|
24
|
+
/** Placeholder para campos ausentes (assignee, datas, old/new value). */
|
|
25
|
+
readonly emptyPlaceholder: string;
|
|
26
|
+
/** Seta "para cima" das dicas de navegação. */
|
|
27
|
+
readonly arrowUp: string;
|
|
28
|
+
/** Seta "para baixo" das dicas de navegação. */
|
|
29
|
+
readonly arrowDown: string;
|
|
30
|
+
/** Caractere da máscara de senha/api_key (`components/text-input.tsx`). */
|
|
31
|
+
readonly maskBullet: string;
|
|
32
|
+
/** Frames do spinner (`components/spinner.tsx`). */
|
|
33
|
+
readonly spinnerFrames: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/** Glyphs Unicode — terminais modernos (Windows Terminal, iTerm, etc.). */
|
|
36
|
+
export declare const UNICODE_GLYPHS: Glyphs;
|
|
37
|
+
/** Fallback ASCII puro — terminal legado do Windows (sem mojibake). */
|
|
38
|
+
export declare const ASCII_GLYPHS: Glyphs;
|
|
39
|
+
/**
|
|
40
|
+
* Escolhe o conjunto de glyphs conforme o suporte a Unicode.
|
|
41
|
+
*
|
|
42
|
+
* @param unicode - `true` para glyphs Unicode, `false` para o fallback ASCII.
|
|
43
|
+
* @returns O conjunto de glyphs correspondente.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveGlyphs(unicode: boolean): Glyphs;
|
|
46
|
+
/** Glyphs resolvidos para o ambiente atual — importado pelas telas/utilitários. */
|
|
47
|
+
export declare const glyphs: Glyphs;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glyphs NÃO cobertos por `figures` — hardening do terminal LEGADO do Windows
|
|
3
|
+
* (M5-09, #84): cmd.exe / PowerShell antigo, sem suporte a Unicode.
|
|
4
|
+
*
|
|
5
|
+
* `figures` (ver `./symbols.ts`) só degrada para ASCII os seus símbolos
|
|
6
|
+
* ESPECIAIS (tick/cross/pointer/info/warning). Os glyphs da categoria "common"
|
|
7
|
+
* dele — reticências `…`, setas `↑`/`↓`, `bullet`, etc. — permanecem Unicode
|
|
8
|
+
* MESMO no conjunto `fallbackSymbols` (confira `node_modules/figures/index.js`:
|
|
9
|
+
* `common` é espalhado tanto em `mainSymbols` quanto em `fallbackSymbols`).
|
|
10
|
+
*
|
|
11
|
+
* A TUI usa alguns desses glyphs "common" hardcoded fora do sistema de
|
|
12
|
+
* `figures` (reticências de truncamento, setas de navegação, o separador `·`,
|
|
13
|
+
* a máscara de senha `•` e os frames braille do spinner). Em um terminal
|
|
14
|
+
* legado do Windows eles apareceriam como mojibake. Este módulo centraliza
|
|
15
|
+
* ESSES glyphs com um fallback ASCII, decidido pelo MESMO sinal que `figures`
|
|
16
|
+
* usa por baixo (`is-unicode-supported`), replicado aqui como função pura e
|
|
17
|
+
* injetável (`env`/`platform`) — seguindo o padrão de `../cli/tty.ts`
|
|
18
|
+
* (`shouldRenderTui`), que também mantém a decisão de degradação testável sem
|
|
19
|
+
* ler `process` direto.
|
|
20
|
+
*/
|
|
21
|
+
import process from 'node:process';
|
|
22
|
+
/**
|
|
23
|
+
* Espelha a heurística de `is-unicode-supported@2` (a mesma que `figures`
|
|
24
|
+
* consome) como função pura e injetável.
|
|
25
|
+
*
|
|
26
|
+
* Reason: `figures` decide o fallback UMA vez, no import, a partir do ambiente
|
|
27
|
+
* real — para orçarmos os glyphs "common" pelo MESMO critério (e testá-lo com
|
|
28
|
+
* ambientes simulados) precisamos da decisão exposta como função. Manter a
|
|
29
|
+
* lógica idêntica à da lib garante que os dois caminhos concordem.
|
|
30
|
+
*
|
|
31
|
+
* @param env - Ambiente do processo (ou um subconjunto controlado em testes).
|
|
32
|
+
* @param platform - Plataforma (`process.platform`); injetável para testes.
|
|
33
|
+
* @returns `true` quando o terminal deve renderizar glyphs Unicode.
|
|
34
|
+
* @example
|
|
35
|
+
* isUnicodeSupported({ WT_SESSION: '1' }, 'win32'); // true (Windows Terminal)
|
|
36
|
+
* isUnicodeSupported({}, 'win32'); // false (cmd.exe/PowerShell legado)
|
|
37
|
+
*/
|
|
38
|
+
export function isUnicodeSupported(env = process.env, platform = process.platform) {
|
|
39
|
+
const { TERM, TERM_PROGRAM } = env;
|
|
40
|
+
if (platform !== 'win32') {
|
|
41
|
+
return TERM !== 'linux'; // console do kernel Linux não renderiza Unicode
|
|
42
|
+
}
|
|
43
|
+
return (Boolean(env.WT_SESSION) || // Windows Terminal
|
|
44
|
+
Boolean(env.TERMINUS_SUBLIME) || // Terminus (<0.2.27)
|
|
45
|
+
env.ConEmuTask === '{cmd::Cmder}' || // ConEmu e cmder
|
|
46
|
+
TERM_PROGRAM === 'Terminus-Sublime' ||
|
|
47
|
+
TERM_PROGRAM === 'vscode' ||
|
|
48
|
+
TERM === 'xterm-256color' ||
|
|
49
|
+
TERM === 'alacritty' ||
|
|
50
|
+
TERM === 'rxvt-unicode' ||
|
|
51
|
+
TERM === 'rxvt-unicode-256color' ||
|
|
52
|
+
env.TERMINAL_EMULATOR === 'JetBrains-JediTerm');
|
|
53
|
+
}
|
|
54
|
+
/** Glyphs Unicode — terminais modernos (Windows Terminal, iTerm, etc.). */
|
|
55
|
+
export const UNICODE_GLYPHS = {
|
|
56
|
+
ellipsis: '…',
|
|
57
|
+
middleDot: '·',
|
|
58
|
+
emptyPlaceholder: '—',
|
|
59
|
+
arrowUp: '↑',
|
|
60
|
+
arrowDown: '↓',
|
|
61
|
+
maskBullet: '•',
|
|
62
|
+
spinnerFrames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
63
|
+
};
|
|
64
|
+
/** Fallback ASCII puro — terminal legado do Windows (sem mojibake). */
|
|
65
|
+
export const ASCII_GLYPHS = {
|
|
66
|
+
ellipsis: '...',
|
|
67
|
+
middleDot: '|',
|
|
68
|
+
emptyPlaceholder: '-',
|
|
69
|
+
arrowUp: '^',
|
|
70
|
+
arrowDown: 'v',
|
|
71
|
+
maskBullet: '*',
|
|
72
|
+
spinnerFrames: ['|', '/', '-', '\\'],
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Escolhe o conjunto de glyphs conforme o suporte a Unicode.
|
|
76
|
+
*
|
|
77
|
+
* @param unicode - `true` para glyphs Unicode, `false` para o fallback ASCII.
|
|
78
|
+
* @returns O conjunto de glyphs correspondente.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveGlyphs(unicode) {
|
|
81
|
+
return unicode ? UNICODE_GLYPHS : ASCII_GLYPHS;
|
|
82
|
+
}
|
|
83
|
+
/** Glyphs resolvidos para o ambiente atual — importado pelas telas/utilitários. */
|
|
84
|
+
export const glyphs = resolveGlyphs(isUnicodeSupported());
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erro de rejeição de `guard()` quando o re-login em andamento é ABANDONADO
|
|
3
|
+
* (Esc na tela de onboarding, ver `../app.tsx`) em vez de concluído — fix do
|
|
4
|
+
* review do PR #119.
|
|
5
|
+
*
|
|
6
|
+
* @remarks Telas de dados que chamam `guard()` devem tratar esta rejeição
|
|
7
|
+
* como uma operação CANCELADA pelo usuário (volte a um estado neutro —
|
|
8
|
+
* ex.: `status: 'idle'` — igual a antes da chamada), e não como uma falha
|
|
9
|
+
* inesperada (nada de mensagem de erro em vermelho ou crash): o usuário
|
|
10
|
+
* escolheu conscientemente não reautenticar.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ReAuthAbortedError extends Error {
|
|
13
|
+
constructor(message?: string);
|
|
14
|
+
}
|
|
15
|
+
/** Valor retornado por {@link useAuthGuard}. */
|
|
16
|
+
export interface UseAuthGuardResult {
|
|
17
|
+
/**
|
|
18
|
+
* Envolve uma operação autenticada do core.
|
|
19
|
+
*
|
|
20
|
+
* Devolve o mesmo resultado (ou rejeição) da operação em condições normais.
|
|
21
|
+
* Em caso de `RedmineAuthError`, dispara o fluxo de re-auth descrito no
|
|
22
|
+
* módulo e só resolve depois que o usuário loga de novo e a operação é
|
|
23
|
+
* reexecutada — chamadoras que precisam de um `AbortController`/cleanup
|
|
24
|
+
* (ex.: `useEffect`) continuam funcionando normalmente: a promise
|
|
25
|
+
* devolvida é só ignorada se o componente desmontar antes dela resolver.
|
|
26
|
+
*
|
|
27
|
+
* Se o re-login for abandonado (Esc, ver `../app.tsx`), a `Promise`
|
|
28
|
+
* rejeita com {@link ReAuthAbortedError} — trate como cancelamento, não
|
|
29
|
+
* como erro (ver o remark da classe).
|
|
30
|
+
*
|
|
31
|
+
* @param operation - A chamada ao core a proteger (ex.: `fetchIssueBundle`).
|
|
32
|
+
*/
|
|
33
|
+
guard<T>(operation: () => Promise<T>): Promise<T>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hook público de re-autenticação — ver o JSDoc do módulo para o contrato
|
|
37
|
+
* completo.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useAuthGuard(): UseAuthGuardResult;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarda central de re-autenticação (M2-13, issue #36).
|
|
3
|
+
*
|
|
4
|
+
* QUALQUER tela de dados (#29+) que chame o core autenticado deve envolver
|
|
5
|
+
* essa chamada com `guard()`. Se a operação rejeitar com `RedmineAuthError`
|
|
6
|
+
* (401 — a api_key expirou ou foi revogada no servidor), o guard:
|
|
7
|
+
*
|
|
8
|
+
* 1. guarda a tela ATUAL (`useNavigation().current`) como origem e a própria
|
|
9
|
+
* chamada como callback de retry;
|
|
10
|
+
* 2. empurra `onboarding-login` em modo re-auth
|
|
11
|
+
* (`OnboardingContext.beginReAuth`, `../screens/onboarding/onboarding-context.tsx`),
|
|
12
|
+
* reaproveitando o fluxo de onboarding já existente para o usuário logar
|
|
13
|
+
* de novo — sem repetir `onboarding-url`/`onboarding-mode`, já que a
|
|
14
|
+
* instância já é conhecida;
|
|
15
|
+
* 3. quando o re-login resolve com sucesso, `validating.tsx`/`api-key.tsx`
|
|
16
|
+
* (`../screens/onboarding/`) chamam `OnboardingContext.resolveReAuth()`,
|
|
17
|
+
* que dispara o `retry` guardado aqui — SÓ NESSE MOMENTO a `Promise`
|
|
18
|
+
* devolvida por `guard()` resolve, com o resultado da nova tentativa — e a
|
|
19
|
+
* navegação volta à tela de origem (`useNavigation().popTo`, chamado pelas
|
|
20
|
+
* mesmas telas antes de `resolveReAuth()`).
|
|
21
|
+
*
|
|
22
|
+
* Se o re-login falhar (senha errada, rede fora do ar), a tela de login
|
|
23
|
+
* segue seu fluxo normal (mensagem de erro, tentar de novo) — o `reAuth`
|
|
24
|
+
* guardado no contexto permanece intacto (a origem não se perde) até uma
|
|
25
|
+
* tentativa ter sucesso.
|
|
26
|
+
*
|
|
27
|
+
* A credencial antiga é sobrescrita normalmente pela cascata de persistência
|
|
28
|
+
* (`createCredentialCascade().set()`, já chamada por
|
|
29
|
+
* `./use-onboarding-callbacks.ts` em todo login bem-sucedido, re-auth ou não)
|
|
30
|
+
* — nenhum tratamento especial é necessário aqui.
|
|
31
|
+
*
|
|
32
|
+
* Fix do review do PR #119 — dois problemas cobertos aqui:
|
|
33
|
+
*
|
|
34
|
+
* 1. Abandono do re-login (Esc): antes, a `Promise` de `guard()` ficava
|
|
35
|
+
* pendurada para sempre se o usuário apertasse Esc na tela de login em
|
|
36
|
+
* vez de completar o re-auth. Agora o atalho global de Esc
|
|
37
|
+
* (`../../app.tsx`), ao detectar que está numa tela `onboarding-*` com um
|
|
38
|
+
* `reAuth` ativo, chama `OnboardingContext.abortReAuth()` — que rejeita
|
|
39
|
+
* a `Promise` com {@link ReAuthAbortedError}. Chamadoras de `guard()`
|
|
40
|
+
* (telas de dados) devem tratar essa rejeição como uma operação
|
|
41
|
+
* CANCELADA (estado neutro — "usuário desistiu de logar de novo"), não
|
|
42
|
+
* como uma falha a ser exibida como erro/crash.
|
|
43
|
+
* 2. 401 concorrentes: antes, um segundo `guard()` falhando com 401 enquanto
|
|
44
|
+
* o primeiro já tinha iniciado o re-auth SOBRESCREVIA o `reAuth` no
|
|
45
|
+
* contexto — o primeiro retry/reject ficava órfão. Agora
|
|
46
|
+
* `OnboardingContext.beginReAuth()` acumula uma lista de pendências; uma
|
|
47
|
+
* única passagem de login bem-sucedida (`resolveReAuth()`) resolve TODOS
|
|
48
|
+
* os `guard()` pendentes.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // Tela de dados futura (#29+, ex.: `screens/home.tsx`):
|
|
52
|
+
* import { useAuthGuard } from '../hooks/use-auth-guard.js';
|
|
53
|
+
*
|
|
54
|
+
* function HomeScreen() {
|
|
55
|
+
* const { guard } = useAuthGuard();
|
|
56
|
+
* const [bundle, setBundle] = useState<IssueBundleResult>();
|
|
57
|
+
*
|
|
58
|
+
* useEffect(() => {
|
|
59
|
+
* let cancelled = false;
|
|
60
|
+
* guard(() => fetchIssueBundleViaCore(issueId)).then((result) => {
|
|
61
|
+
* if (!cancelled) setBundle(result);
|
|
62
|
+
* });
|
|
63
|
+
* return () => {
|
|
64
|
+
* cancelled = true;
|
|
65
|
+
* };
|
|
66
|
+
* }, [issueId]);
|
|
67
|
+
* // ...
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
import { useCallback, useRef } from 'react';
|
|
71
|
+
import { RedmineAuthError } from '../../../index.js';
|
|
72
|
+
import { useNavigation } from '../navigation.js';
|
|
73
|
+
import { useOnboarding } from '../screens/onboarding/onboarding-context.js';
|
|
74
|
+
/**
|
|
75
|
+
* Erro de rejeição de `guard()` quando o re-login em andamento é ABANDONADO
|
|
76
|
+
* (Esc na tela de onboarding, ver `../app.tsx`) em vez de concluído — fix do
|
|
77
|
+
* review do PR #119.
|
|
78
|
+
*
|
|
79
|
+
* @remarks Telas de dados que chamam `guard()` devem tratar esta rejeição
|
|
80
|
+
* como uma operação CANCELADA pelo usuário (volte a um estado neutro —
|
|
81
|
+
* ex.: `status: 'idle'` — igual a antes da chamada), e não como uma falha
|
|
82
|
+
* inesperada (nada de mensagem de erro em vermelho ou crash): o usuário
|
|
83
|
+
* escolheu conscientemente não reautenticar.
|
|
84
|
+
*/
|
|
85
|
+
export class ReAuthAbortedError extends Error {
|
|
86
|
+
constructor(message = 'Re-autenticação cancelada pelo usuário (Esc).') {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = 'ReAuthAbortedError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Hook público de re-autenticação — ver o JSDoc do módulo para o contrato
|
|
93
|
+
* completo.
|
|
94
|
+
*/
|
|
95
|
+
export function useAuthGuard() {
|
|
96
|
+
const { current, push } = useNavigation();
|
|
97
|
+
const { beginReAuth } = useOnboarding();
|
|
98
|
+
// Refs (não deps do useCallback): `guard` precisa ficar com identidade
|
|
99
|
+
// ESTÁVEL (padrão do repo) mesmo com `current`/`push`/`beginReAuth` mudando
|
|
100
|
+
// entre renders — o valor lido é sempre o mais recente via ref.
|
|
101
|
+
const currentRef = useRef(current);
|
|
102
|
+
currentRef.current = current;
|
|
103
|
+
const pushRef = useRef(push);
|
|
104
|
+
pushRef.current = push;
|
|
105
|
+
const beginReAuthRef = useRef(beginReAuth);
|
|
106
|
+
beginReAuthRef.current = beginReAuth;
|
|
107
|
+
const guard = useCallback(function guardOperation(operation,
|
|
108
|
+
// A origem é capturada UMA VEZ, na primeira falha — e reaproveitada em
|
|
109
|
+
// retries subsequentes (`falha do re-login não perde a origem`, DoD
|
|
110
|
+
// #36), mesmo que a navegação já tenha mudado quando o retry acontece.
|
|
111
|
+
origin = currentRef.current) {
|
|
112
|
+
return operation().catch((cause) => {
|
|
113
|
+
if (!(cause instanceof RedmineAuthError)) {
|
|
114
|
+
throw cause;
|
|
115
|
+
}
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
// Fix do review #119: `beginReAuth` devolve `true` só na PRIMEIRA
|
|
118
|
+
// vez que este ciclo de re-auth é ativado — 401 concorrentes
|
|
119
|
+
// (segunda chamada enquanto a primeira já empilhou o login) só
|
|
120
|
+
// adicionam a pendência à lista existente, sem empilhar
|
|
121
|
+
// `onboarding-login` de novo nem perder o retry/reject anterior.
|
|
122
|
+
const activated = beginReAuthRef.current(origin, {
|
|
123
|
+
retry: () => {
|
|
124
|
+
guardOperation(operation, origin).then(resolve, reject);
|
|
125
|
+
},
|
|
126
|
+
reject,
|
|
127
|
+
});
|
|
128
|
+
if (activated) {
|
|
129
|
+
pushRef.current('onboarding-login');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}, []);
|
|
134
|
+
return { guard };
|
|
135
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describeCredentialSource, type CredentialSourceKind } from '../../../index.js';
|
|
2
|
+
/** Resultado do teste leve de conectividade. */
|
|
3
|
+
export type ConnectivityStatus = 'checking' | 'ok' | 'error' | 'skipped';
|
|
4
|
+
/** Snapshot completo do painel de diagnóstico exibido por `doctor.tsx`. */
|
|
5
|
+
export interface DoctorStatus {
|
|
6
|
+
/** Versão do runtime Node (`process.version`, ou injetada em teste). */
|
|
7
|
+
nodeVersion: string;
|
|
8
|
+
/** Nome do produto (via core). */
|
|
9
|
+
toolName: string;
|
|
10
|
+
/** Versão do produto (via core). */
|
|
11
|
+
toolVersion: string;
|
|
12
|
+
/** Instância configurada (`REDMINE_URL`), ou `undefined` se ausente. */
|
|
13
|
+
instanceUrl: string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Fonte da credencial em uso na cascata — NUNCA a api_key em si.
|
|
16
|
+
* `'checking'` enquanto a consulta assíncrona ainda não resolveu.
|
|
17
|
+
*/
|
|
18
|
+
credentialMethod: CredentialSourceKind | 'checking';
|
|
19
|
+
/** Resultado do teste leve de conectividade (ver {@link ConnectivityStatus}). */
|
|
20
|
+
connectivity: ConnectivityStatus;
|
|
21
|
+
/** Detalhe do erro de conectividade, se houver — sempre seguro (sem segredos). */
|
|
22
|
+
connectivityDetail: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Resultado devolvido por um {@link ConnectivityChecker}. */
|
|
25
|
+
export interface ConnectivityCheckResult {
|
|
26
|
+
/** `true` se a instância respondeu com sucesso dentro do timeout. */
|
|
27
|
+
ok: boolean;
|
|
28
|
+
/** Detalhe legível da falha (status HTTP ou motivo da exceção). Ausente quando `ok`. */
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Executa a checagem de conectividade — injetável para testes (o default de
|
|
33
|
+
* produção usa `fetch` global com timeout via `AbortController`).
|
|
34
|
+
*/
|
|
35
|
+
export type ConnectivityChecker = (instanceUrl: string, timeoutMs: number) => Promise<ConnectivityCheckResult>;
|
|
36
|
+
/** Dependências injetáveis do hook — todas opcionais, com defaults de produção. */
|
|
37
|
+
export interface UseDoctorStatusOptions {
|
|
38
|
+
/** Ambiente consultado para `REDMINE_URL`; default `process.env`. */
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
/** Versão do Node exibida; default `process.version`. */
|
|
41
|
+
nodeVersion?: string;
|
|
42
|
+
/** Resolve a fonte da credencial; default `describeCredentialSource` do core. */
|
|
43
|
+
describeCredentialSource?: typeof describeCredentialSource;
|
|
44
|
+
/** Executa a checagem de conectividade; default via `fetch` global. */
|
|
45
|
+
checkConnectivity?: ConnectivityChecker;
|
|
46
|
+
/** Timeout (ms) da checagem de conectividade; default {@link DEFAULT_CONNECTIVITY_TIMEOUT_MS}. */
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/** Timeout default do teste de conectividade — curto o bastante para não travar a tela. */
|
|
50
|
+
export declare const DEFAULT_CONNECTIVITY_TIMEOUT_MS = 3000;
|
|
51
|
+
/**
|
|
52
|
+
* Reúne o status do doctor: node/tool/instância (síncronos, disponíveis já no
|
|
53
|
+
* primeiro render) + credencial e conectividade (assíncronos, resolvidos via
|
|
54
|
+
* `useEffect` quando há uma instância configurada).
|
|
55
|
+
*
|
|
56
|
+
* @param options - Dependências injetáveis. Ver {@link UseDoctorStatusOptions}.
|
|
57
|
+
* @returns O {@link DoctorStatus} atual — `credentialMethod`/`connectivity`
|
|
58
|
+
* começam em `'checking'`/`'skipped'` e são atualizados quando a checagem
|
|
59
|
+
* assíncrona resolve (ou `'skipped'` direto, se não há instância configurada).
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const status = useDoctorStatus(); // deps de produção (process.env, fetch real)
|
|
63
|
+
*/
|
|
64
|
+
export declare function useDoctorStatus(options?: UseDoctorStatusOptions): DoctorStatus;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lógica de status do painel `doctor` (M2-12, #35): reúne versão do Node, da
|
|
3
|
+
* ferramenta, instância configurada, método de credencial em uso e status de
|
|
4
|
+
* conectividade. Extraída para um hook porque a credencial e a conectividade
|
|
5
|
+
* são assíncronas (cascata de credenciais, rede) e precisam ser testáveis com
|
|
6
|
+
* dependências injetadas — nenhum teste deste arquivo toca keychain ou rede
|
|
7
|
+
* reais.
|
|
8
|
+
*
|
|
9
|
+
* Fronteira do core (ADR-005): a única coisa importada daqui é
|
|
10
|
+
* `describeCredentialSource` + `TOOL_NAME`/`TOOL_VERSION` via `../../../index.js`
|
|
11
|
+
* — a checagem de conectividade usa `fetch` global diretamente (mesmo padrão
|
|
12
|
+
* de baixo nível de `client/http.ts`/`config/login.ts`), porque o doctor testa
|
|
13
|
+
* apenas ALCANÇABILIDADE (GET leve, sem autenticação) — não uma chamada de
|
|
14
|
+
* API do core, então não há nada do core a reexportar para isso.
|
|
15
|
+
*
|
|
16
|
+
* Sem vazamento de segredos: a api_key nunca entra neste módulo — a fonte da
|
|
17
|
+
* credencial é relatada pelo NOME (`'keyring' | 'file' | 'env' | 'none'`), e a
|
|
18
|
+
* checagem de conectividade é uma requisição não autenticada.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useState } from 'react';
|
|
21
|
+
import { describeCredentialSource, TOOL_NAME, TOOL_VERSION } from '../../../index.js';
|
|
22
|
+
/** Timeout default do teste de conectividade — curto o bastante para não travar a tela. */
|
|
23
|
+
export const DEFAULT_CONNECTIVITY_TIMEOUT_MS = 3000;
|
|
24
|
+
/**
|
|
25
|
+
* Checagem default de conectividade: GET leve (sem autenticação) à instância,
|
|
26
|
+
* com timeout via `AbortController`. Não autenticado de propósito — o doctor
|
|
27
|
+
* testa alcançabilidade de rede, não valida a credencial (isso já é reportado
|
|
28
|
+
* separadamente por `credentialMethod`), então a api_key nunca participa
|
|
29
|
+
* desta requisição.
|
|
30
|
+
*/
|
|
31
|
+
const defaultCheckConnectivity = async (instanceUrl, timeoutMs) => {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(instanceUrl, { method: 'GET', signal: controller.signal });
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
return { ok: false, detail: `HTTP ${response.status}` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true };
|
|
40
|
+
}
|
|
41
|
+
catch (cause) {
|
|
42
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
43
|
+
return { ok: false, detail: reason };
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** Lê `REDMINE_URL` do ambiente, tratando string vazia como ausente. */
|
|
50
|
+
function instanceFromEnv(env) {
|
|
51
|
+
const value = env.REDMINE_URL;
|
|
52
|
+
return value !== undefined && value.length > 0 ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reúne o status do doctor: node/tool/instância (síncronos, disponíveis já no
|
|
56
|
+
* primeiro render) + credencial e conectividade (assíncronos, resolvidos via
|
|
57
|
+
* `useEffect` quando há uma instância configurada).
|
|
58
|
+
*
|
|
59
|
+
* @param options - Dependências injetáveis. Ver {@link UseDoctorStatusOptions}.
|
|
60
|
+
* @returns O {@link DoctorStatus} atual — `credentialMethod`/`connectivity`
|
|
61
|
+
* começam em `'checking'`/`'skipped'` e são atualizados quando a checagem
|
|
62
|
+
* assíncrona resolve (ou `'skipped'` direto, se não há instância configurada).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const status = useDoctorStatus(); // deps de produção (process.env, fetch real)
|
|
66
|
+
*/
|
|
67
|
+
export function useDoctorStatus(options = {}) {
|
|
68
|
+
const env = options.env ?? process.env;
|
|
69
|
+
const nodeVersion = options.nodeVersion ?? process.version;
|
|
70
|
+
const resolveSource = options.describeCredentialSource ?? describeCredentialSource;
|
|
71
|
+
const checkConnectivity = options.checkConnectivity ?? defaultCheckConnectivity;
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CONNECTIVITY_TIMEOUT_MS;
|
|
73
|
+
const instanceUrl = instanceFromEnv(env);
|
|
74
|
+
const [credentialMethod, setCredentialMethod] = useState(instanceUrl === undefined ? 'none' : 'checking');
|
|
75
|
+
const [connectivity, setConnectivity] = useState(instanceUrl === undefined ? 'skipped' : 'checking');
|
|
76
|
+
const [connectivityDetail, setConnectivityDetail] = useState(undefined);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (instanceUrl === undefined) {
|
|
79
|
+
setCredentialMethod('none');
|
|
80
|
+
setConnectivity('skipped');
|
|
81
|
+
setConnectivityDetail(undefined);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
setCredentialMethod('checking');
|
|
86
|
+
setConnectivity('checking');
|
|
87
|
+
setConnectivityDetail(undefined);
|
|
88
|
+
resolveSource(instanceUrl, { env }).then((source) => {
|
|
89
|
+
if (!cancelled)
|
|
90
|
+
setCredentialMethod(source);
|
|
91
|
+
}, () => {
|
|
92
|
+
if (!cancelled)
|
|
93
|
+
setCredentialMethod('none');
|
|
94
|
+
});
|
|
95
|
+
checkConnectivity(instanceUrl, timeoutMs).then((result) => {
|
|
96
|
+
if (cancelled)
|
|
97
|
+
return;
|
|
98
|
+
setConnectivity(result.ok ? 'ok' : 'error');
|
|
99
|
+
setConnectivityDetail(result.detail);
|
|
100
|
+
}, (cause) => {
|
|
101
|
+
if (cancelled)
|
|
102
|
+
return;
|
|
103
|
+
setConnectivity('error');
|
|
104
|
+
setConnectivityDetail(cause instanceof Error ? cause.message : String(cause));
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
};
|
|
109
|
+
// Reason: `env`/`resolveSource`/`checkConnectivity` são estáveis entre
|
|
110
|
+
// renders quando as deps de produção são usadas (defaults do módulo, ou
|
|
111
|
+
// `process.env`) — o efeito só precisa refazer a consulta quando a
|
|
112
|
+
// instância configurada muda.
|
|
113
|
+
}, [instanceUrl]);
|
|
114
|
+
return {
|
|
115
|
+
nodeVersion,
|
|
116
|
+
toolName: TOOL_NAME,
|
|
117
|
+
toolVersion: TOOL_VERSION,
|
|
118
|
+
instanceUrl,
|
|
119
|
+
credentialMethod,
|
|
120
|
+
connectivity,
|
|
121
|
+
connectivityDetail,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consome o interceptor ativo, se houver — chamado pelo roteador (`../app.tsx`)
|
|
3
|
+
* ANTES de aplicar o `pop()` padrão do Esc global.
|
|
4
|
+
*
|
|
5
|
+
* @returns `true` se um interceptor tratou o Esc (o chamador não deve fazer
|
|
6
|
+
* `pop()` nesse caso); `false` se nenhum interceptor está registrado (o
|
|
7
|
+
* chamador segue com o comportamento padrão).
|
|
8
|
+
*/
|
|
9
|
+
export declare function consumeEscapeInterceptor(): boolean;
|
|
10
|
+
/** Somente para testes: garante um estado limpo entre casos de teste. */
|
|
11
|
+
export declare function resetEscapeInterceptor(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Registra `onEscape` como o interceptor ativo enquanto `active` for `true`;
|
|
14
|
+
* desregistra automaticamente quando `active` vira `false` ou o componente
|
|
15
|
+
* desmonta (limpeza do `useEffect`).
|
|
16
|
+
*
|
|
17
|
+
* @param active - Só registra o interceptor enquanto `true` (ex.: campo de
|
|
18
|
+
* busca da home aberto).
|
|
19
|
+
* @param onEscape - Chamado quando `Esc` é pressionado e este interceptor
|
|
20
|
+
* está ativo. Identidade pode mudar entre renders (lido via `ref`).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* useEscapeInterceptor(isSearching, closeSearch);
|
|
24
|
+
*/
|
|
25
|
+
export declare function useEscapeInterceptor(active: boolean, onEscape: () => void): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interceptação pontual do Esc global (M2-07, issue #30).
|
|
3
|
+
*
|
|
4
|
+
* O roteador (`../app.tsx`) trata `Esc` globalmente como `pop()` da pilha de
|
|
5
|
+
* navegação (volta para a tela anterior). A busca inline da home
|
|
6
|
+
* (`../screens/home.tsx`) precisa que `Esc`, enquanto o campo de busca está
|
|
7
|
+
* ativo, feche a busca e restaure a lista original — em vez de desempilhar a
|
|
8
|
+
* home e voltar para a tela de onde ela veio. Sem esse desvio, o usuário
|
|
9
|
+
* sairia da home sem querer só por tentar limpar o texto da busca.
|
|
10
|
+
*
|
|
11
|
+
* Implementado como uma única referência mutável em nível de módulo, não um
|
|
12
|
+
* Context/Provider: a TUI só sustenta UMA tela ativa por vez (arquitetura de
|
|
13
|
+
* pilha, `../navigation.tsx`), então nunca há mais de um interceptor
|
|
14
|
+
* concorrente de fato — um Provider/Context inteiro seria complexidade sem
|
|
15
|
+
* benefício para esse caso. O mecanismo é genérico o bastante para qualquer
|
|
16
|
+
* tela futura que precise do mesmo desvio pontual do Esc global.
|
|
17
|
+
*/
|
|
18
|
+
import { useEffect, useRef } from 'react';
|
|
19
|
+
/** Interceptor ativo no momento, ou `undefined` se nenhuma tela registrou um. */
|
|
20
|
+
let activeInterceptor;
|
|
21
|
+
/**
|
|
22
|
+
* Consome o interceptor ativo, se houver — chamado pelo roteador (`../app.tsx`)
|
|
23
|
+
* ANTES de aplicar o `pop()` padrão do Esc global.
|
|
24
|
+
*
|
|
25
|
+
* @returns `true` se um interceptor tratou o Esc (o chamador não deve fazer
|
|
26
|
+
* `pop()` nesse caso); `false` se nenhum interceptor está registrado (o
|
|
27
|
+
* chamador segue com o comportamento padrão).
|
|
28
|
+
*/
|
|
29
|
+
export function consumeEscapeInterceptor() {
|
|
30
|
+
if (activeInterceptor === undefined) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
activeInterceptor();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/** Somente para testes: garante um estado limpo entre casos de teste. */
|
|
37
|
+
export function resetEscapeInterceptor() {
|
|
38
|
+
activeInterceptor = undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Registra `onEscape` como o interceptor ativo enquanto `active` for `true`;
|
|
42
|
+
* desregistra automaticamente quando `active` vira `false` ou o componente
|
|
43
|
+
* desmonta (limpeza do `useEffect`).
|
|
44
|
+
*
|
|
45
|
+
* @param active - Só registra o interceptor enquanto `true` (ex.: campo de
|
|
46
|
+
* busca da home aberto).
|
|
47
|
+
* @param onEscape - Chamado quando `Esc` é pressionado e este interceptor
|
|
48
|
+
* está ativo. Identidade pode mudar entre renders (lido via `ref`).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* useEscapeInterceptor(isSearching, closeSearch);
|
|
52
|
+
*/
|
|
53
|
+
export function useEscapeInterceptor(active, onEscape) {
|
|
54
|
+
const onEscapeRef = useRef(onEscape);
|
|
55
|
+
onEscapeRef.current = onEscape;
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!active) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
activeInterceptor = () => onEscapeRef.current();
|
|
61
|
+
return () => {
|
|
62
|
+
activeInterceptor = undefined;
|
|
63
|
+
};
|
|
64
|
+
}, [active]);
|
|
65
|
+
}
|