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