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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callbacks REAIS do onboarding (#28) — fecha o wiring entre as telas de
|
|
3
|
+
* onboarding (`../screens/onboarding/`) e o core: login por senha
|
|
4
|
+
* (`loginWithPassword`), validação de api_key colada (`validateApiKey`, o
|
|
5
|
+
* fallback de 2FA do ADR-003) e persistência via a cascata de credenciais M2
|
|
6
|
+
* (`createCredentialCascade` — keychain preferido, com migração automática).
|
|
7
|
+
*
|
|
8
|
+
* Único ponto de import do core para o fluxo de onboarding (ADR-005): as
|
|
9
|
+
* telas em si (`../screens/onboarding/*.tsx`) só recebem os callbacks
|
|
10
|
+
* injetados via `<OnboardingProvider callbacks={...}>` (`../app.tsx`) — nunca
|
|
11
|
+
* importam `../../../index.js` diretamente.
|
|
12
|
+
*
|
|
13
|
+
* Mantém uma cópia local da URL (via o callback `onUrlSubmit`, atualizado a
|
|
14
|
+
* cada submissão da tela de URL) porque o `OnboardingContext` só existe
|
|
15
|
+
* DEPOIS deste hook rodar (`<OnboardingProvider callbacks={...}>` recebe os
|
|
16
|
+
* callbacks como prop) — não há como este hook consumir `useOnboarding()`
|
|
17
|
+
* sem um ciclo de dependência.
|
|
18
|
+
*/
|
|
19
|
+
import { useRef } from 'react';
|
|
20
|
+
import { createCredentialCascade, defaultSettingsStore, loginWithPassword, validateApiKey, RedmineAuthError, } from '../../../index.js';
|
|
21
|
+
/**
|
|
22
|
+
* Interpreta `REDMINE_INSECURE` (`1`/`true`, case-insensitive) — mesma
|
|
23
|
+
* convenção usada por `../screens/onboarding/url.tsx` e pelo MCP
|
|
24
|
+
* (`../../mcp/server.ts`).
|
|
25
|
+
*/
|
|
26
|
+
function isInsecureEnvSet(env) {
|
|
27
|
+
const raw = env.REDMINE_INSECURE;
|
|
28
|
+
return raw !== undefined && /^(1|true)$/i.test(raw.trim());
|
|
29
|
+
}
|
|
30
|
+
/** Extrai uma mensagem segura de uma falha desconhecida — o core já redige segredos antes de lançar. */
|
|
31
|
+
function messageOf(cause) {
|
|
32
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Persiste a instância autenticada (#187) — simétrico ao `login` da CLL. Falha ao
|
|
36
|
+
* salvar a URL NÃO invalida o onboarding (a credencial já foi salva): swallow.
|
|
37
|
+
*/
|
|
38
|
+
async function persistInstance(settings, url) {
|
|
39
|
+
try {
|
|
40
|
+
await settings.setInstanceUrl(url);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Sem persistência a TUI ainda funciona nesta sessão (REDMINE_URL/prompt).
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Monta os callbacks reais de onboarding — injetados em
|
|
48
|
+
* `<OnboardingProvider callbacks={...}>` por `../app.tsx`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* const callbacks = useOnboardingCallbacks();
|
|
52
|
+
* return <OnboardingProvider callbacks={callbacks}>...</OnboardingProvider>;
|
|
53
|
+
*/
|
|
54
|
+
export function useOnboardingCallbacks(settings = defaultSettingsStore()) {
|
|
55
|
+
// Ref (não state): a URL só é lida dentro dos callbacks de login/api_key,
|
|
56
|
+
// nunca precisa disparar um re-render por si só.
|
|
57
|
+
const urlRef = useRef('');
|
|
58
|
+
const onUrlSubmit = (url) => {
|
|
59
|
+
urlRef.current = url;
|
|
60
|
+
};
|
|
61
|
+
const onModeSelect = () => {
|
|
62
|
+
// Reason: o modo escolhido só direciona a NAVEGAÇÃO entre telas
|
|
63
|
+
// (`../screens/onboarding/mode.tsx`) — nenhuma chamada ao core depende
|
|
64
|
+
// dele, então não há estado a guardar aqui.
|
|
65
|
+
};
|
|
66
|
+
const onLoginSubmit = async (credentials) => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await loginWithPassword({
|
|
69
|
+
baseUrl: urlRef.current,
|
|
70
|
+
username: credentials.username,
|
|
71
|
+
password: credentials.password,
|
|
72
|
+
insecure: isInsecureEnvSet(process.env),
|
|
73
|
+
});
|
|
74
|
+
await createCredentialCascade().set(urlRef.current, result.apiKey);
|
|
75
|
+
await persistInstance(settings, urlRef.current);
|
|
76
|
+
return { kind: 'success', user: result.user };
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
if (cause instanceof RedmineAuthError) {
|
|
80
|
+
return { kind: 'auth-error', message: cause.message };
|
|
81
|
+
}
|
|
82
|
+
return { kind: 'network-error', message: messageOf(cause) };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const onApiKeySubmit = async (apiKey) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await validateApiKey({
|
|
88
|
+
baseUrl: urlRef.current,
|
|
89
|
+
apiKey,
|
|
90
|
+
insecure: isInsecureEnvSet(process.env),
|
|
91
|
+
});
|
|
92
|
+
await createCredentialCascade().set(urlRef.current, result.apiKey);
|
|
93
|
+
await persistInstance(settings, urlRef.current);
|
|
94
|
+
return { kind: 'success', user: result.user };
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
if (cause instanceof RedmineAuthError) {
|
|
98
|
+
return { kind: 'auth-error', message: cause.message };
|
|
99
|
+
}
|
|
100
|
+
return { kind: 'network-error', message: messageOf(cause) };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return { onUrlSubmit, onModeSelect, onLoginSubmit, onApiKeySubmit };
|
|
104
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook de largura do terminal (M2-16, #39): lê `process.stdout.columns` com
|
|
3
|
+
* fallback, escuta o evento `resize` do stdout REAL para atualizar em
|
|
4
|
+
* runtime (redimensionamento da janela do terminal), e é injetável via
|
|
5
|
+
* {@link TerminalWidthProvider} para testes.
|
|
6
|
+
*
|
|
7
|
+
* A injeção via contexto existe porque `ink-testing-library` FIXA
|
|
8
|
+
* `stdout.columns` em `100` (não expõe opção de configurar — ver
|
|
9
|
+
* `node_modules/ink-testing-library/build/index.js`), então nenhum teste
|
|
10
|
+
* usando o `render()` daquela lib consegue simular 80/60 colunas só ajustando
|
|
11
|
+
* `process.stdout.columns`. Testes de layout responsivo usam
|
|
12
|
+
* `tests/surfaces/tui/render-at-width.tsx`, que injeta a largura por este
|
|
13
|
+
* provider (e mocka o `stdout` passado ao Ink real com o mesmo valor, para
|
|
14
|
+
* que o PRÓPRIO wrap do Ink também reflita a largura simulada).
|
|
15
|
+
*/
|
|
16
|
+
import { type ReactNode } from 'react';
|
|
17
|
+
/** Largura assumida quando `process.stdout.columns` está indisponível (ex.: saída não-TTY). */
|
|
18
|
+
export declare const DEFAULT_TERMINAL_WIDTH = 80;
|
|
19
|
+
/** Altura assumida quando `process.stdout.rows` está indisponível. */
|
|
20
|
+
export declare const DEFAULT_TERMINAL_HEIGHT = 24;
|
|
21
|
+
/**
|
|
22
|
+
* Provider de largura FIXA — usado pelos testes (injeta 80/60 sem depender do
|
|
23
|
+
* `process.stdout` real). Fora de testes, a árvore da TUI (`../app.tsx`) usa o
|
|
24
|
+
* {@link TerminalSizeProvider} (uma única assinatura de `resize`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function TerminalWidthProvider({ width, children }: {
|
|
27
|
+
width: number;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* Provider de TAMANHO do terminal (largura + altura) com UMA ÚNICA assinatura do
|
|
32
|
+
* evento `resize` do stdout (#190 fix). Montado no topo da TUI (`../app.tsx`) para
|
|
33
|
+
* que `useTerminalWidth`/`useTerminalHeight` leiam do contexto em vez de cada
|
|
34
|
+
* consumidor (ex.: cada linha da lista) assinar o próprio `resize` — o que
|
|
35
|
+
* estourava o limite de 10 listeners do EventEmitter (MaxListenersExceededWarning).
|
|
36
|
+
*/
|
|
37
|
+
export declare function TerminalSizeProvider({ children }: {
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* Largura atual do terminal, em colunas.
|
|
42
|
+
*
|
|
43
|
+
* Usa o provider mais próximo se houver ({@link TerminalSizeProvider} em produção
|
|
44
|
+
* ou {@link TerminalWidthProvider} nos testes); senão (sem provider) lê o stdout
|
|
45
|
+
* real e assina o `resize` — caminho de fallback para uso isolado.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useTerminalWidth(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Altura atual do terminal, em linhas. Lê do {@link TerminalSizeProvider}; sem ele
|
|
50
|
+
* (uso isolado/testes) cai no fallback {@link DEFAULT_TERMINAL_HEIGHT}.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useTerminalHeight(): number;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Hook de largura do terminal (M2-16, #39): lê `process.stdout.columns` com
|
|
4
|
+
* fallback, escuta o evento `resize` do stdout REAL para atualizar em
|
|
5
|
+
* runtime (redimensionamento da janela do terminal), e é injetável via
|
|
6
|
+
* {@link TerminalWidthProvider} para testes.
|
|
7
|
+
*
|
|
8
|
+
* A injeção via contexto existe porque `ink-testing-library` FIXA
|
|
9
|
+
* `stdout.columns` em `100` (não expõe opção de configurar — ver
|
|
10
|
+
* `node_modules/ink-testing-library/build/index.js`), então nenhum teste
|
|
11
|
+
* usando o `render()` daquela lib consegue simular 80/60 colunas só ajustando
|
|
12
|
+
* `process.stdout.columns`. Testes de layout responsivo usam
|
|
13
|
+
* `tests/surfaces/tui/render-at-width.tsx`, que injeta a largura por este
|
|
14
|
+
* provider (e mocka o `stdout` passado ao Ink real com o mesmo valor, para
|
|
15
|
+
* que o PRÓPRIO wrap do Ink também reflita a largura simulada).
|
|
16
|
+
*/
|
|
17
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
18
|
+
/** Largura assumida quando `process.stdout.columns` está indisponível (ex.: saída não-TTY). */
|
|
19
|
+
export const DEFAULT_TERMINAL_WIDTH = 80;
|
|
20
|
+
/** Altura assumida quando `process.stdout.rows` está indisponível. */
|
|
21
|
+
export const DEFAULT_TERMINAL_HEIGHT = 24;
|
|
22
|
+
const TerminalWidthContext = createContext(undefined);
|
|
23
|
+
/** Altura atual (linhas), provida pelo {@link TerminalSizeProvider}; `undefined` fora dele. */
|
|
24
|
+
const TerminalHeightContext = createContext(undefined);
|
|
25
|
+
/** Lê a largura atual do stdout do processo, com fallback para {@link DEFAULT_TERMINAL_WIDTH}. */
|
|
26
|
+
function readProcessColumns() {
|
|
27
|
+
const columns = process.stdout.columns;
|
|
28
|
+
return typeof columns === 'number' && columns > 0 ? columns : DEFAULT_TERMINAL_WIDTH;
|
|
29
|
+
}
|
|
30
|
+
/** Lê a altura atual do stdout do processo, com fallback para {@link DEFAULT_TERMINAL_HEIGHT}. */
|
|
31
|
+
function readProcessRows() {
|
|
32
|
+
const rows = process.stdout.rows;
|
|
33
|
+
return typeof rows === 'number' && rows > 0 ? rows : DEFAULT_TERMINAL_HEIGHT;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Provider de largura FIXA — usado pelos testes (injeta 80/60 sem depender do
|
|
37
|
+
* `process.stdout` real). Fora de testes, a árvore da TUI (`../app.tsx`) usa o
|
|
38
|
+
* {@link TerminalSizeProvider} (uma única assinatura de `resize`).
|
|
39
|
+
*/
|
|
40
|
+
export function TerminalWidthProvider({ width, children }) {
|
|
41
|
+
return _jsx(TerminalWidthContext.Provider, { value: width, children: children });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Provider de TAMANHO do terminal (largura + altura) com UMA ÚNICA assinatura do
|
|
45
|
+
* evento `resize` do stdout (#190 fix). Montado no topo da TUI (`../app.tsx`) para
|
|
46
|
+
* que `useTerminalWidth`/`useTerminalHeight` leiam do contexto em vez de cada
|
|
47
|
+
* consumidor (ex.: cada linha da lista) assinar o próprio `resize` — o que
|
|
48
|
+
* estourava o limite de 10 listeners do EventEmitter (MaxListenersExceededWarning).
|
|
49
|
+
*/
|
|
50
|
+
export function TerminalSizeProvider({ children }) {
|
|
51
|
+
const [size, setSize] = useState(() => ({ width: readProcessColumns(), height: readProcessRows() }));
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const onResize = () => setSize({ width: readProcessColumns(), height: readProcessRows() });
|
|
54
|
+
process.stdout.on('resize', onResize);
|
|
55
|
+
return () => {
|
|
56
|
+
process.stdout.off('resize', onResize);
|
|
57
|
+
};
|
|
58
|
+
}, []);
|
|
59
|
+
return (_jsx(TerminalWidthContext.Provider, { value: size.width, children: _jsx(TerminalHeightContext.Provider, { value: size.height, children: children }) }));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Largura atual do terminal, em colunas.
|
|
63
|
+
*
|
|
64
|
+
* Usa o provider mais próximo se houver ({@link TerminalSizeProvider} em produção
|
|
65
|
+
* ou {@link TerminalWidthProvider} nos testes); senão (sem provider) lê o stdout
|
|
66
|
+
* real e assina o `resize` — caminho de fallback para uso isolado.
|
|
67
|
+
*/
|
|
68
|
+
export function useTerminalWidth() {
|
|
69
|
+
const injected = useContext(TerminalWidthContext);
|
|
70
|
+
const [width, setWidth] = useState(readProcessColumns);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
// Com um provider (produção via TerminalSizeProvider, ou testes), a largura é
|
|
73
|
+
// a fonte da verdade — não assina o stdout real por consumidor.
|
|
74
|
+
if (injected !== undefined)
|
|
75
|
+
return;
|
|
76
|
+
const handleResize = () => setWidth(readProcessColumns());
|
|
77
|
+
process.stdout.on('resize', handleResize);
|
|
78
|
+
return () => {
|
|
79
|
+
process.stdout.off('resize', handleResize);
|
|
80
|
+
};
|
|
81
|
+
}, [injected]);
|
|
82
|
+
return injected ?? width;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Altura atual do terminal, em linhas. Lê do {@link TerminalSizeProvider}; sem ele
|
|
86
|
+
* (uso isolado/testes) cai no fallback {@link DEFAULT_TERMINAL_HEIGHT}.
|
|
87
|
+
*/
|
|
88
|
+
export function useTerminalHeight() {
|
|
89
|
+
return useContext(TerminalHeightContext) ?? DEFAULT_TERMINAL_HEIGHT;
|
|
90
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type SettingsStore } from '../../index.js';
|
|
2
|
+
import { type InstanceInfo } from './instance.js';
|
|
3
|
+
import type { Theme } from './theme.js';
|
|
4
|
+
/** Dependências injetáveis do `runTui` (default: processo real). */
|
|
5
|
+
export interface RunTuiDeps {
|
|
6
|
+
/** Ambiente (para `REDMINE_URL`); default `process.env`. */
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
/** Store da URL persistida + paleta; default {@link defaultSettingsStore}. */
|
|
9
|
+
settings?: SettingsStore;
|
|
10
|
+
/** Stream de saída (para o alt-screen); default `process.stdout`. Injetável em testes. */
|
|
11
|
+
stdout?: NodeJS.WriteStream;
|
|
12
|
+
/** `process` para os handlers de sinal do alt-screen; default `process`. Injetável em testes. */
|
|
13
|
+
proc?: AltScreenProc;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a instância da TUI no boot (#187) e POPULA `env.REDMINE_URL` para os
|
|
17
|
+
* hooks de dados (que leem o ambiente) — `REDMINE_URL` vence; sem ela, usa a URL
|
|
18
|
+
* persistida no `login`. Extraída para teste (o `runTui` bloqueia em `waitUntilExit`).
|
|
19
|
+
*
|
|
20
|
+
* @param env - Ambiente a consultar/popular (o `runTui` passa `process.env`).
|
|
21
|
+
* @param settings - Store da URL persistida.
|
|
22
|
+
* @returns Metadados da instância + ação de limpar a persistida, para o contexto.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveTuiInstance(env: NodeJS.ProcessEnv, settings: SettingsStore): Promise<InstanceInfo>;
|
|
25
|
+
/**
|
|
26
|
+
* Sobe a TUI e aguarda o app sair.
|
|
27
|
+
*
|
|
28
|
+
* @param deps - Ver {@link RunTuiDeps} (env/settings/stdout); default: processo real.
|
|
29
|
+
* @returns Exit code `0` (sair da TUI é sempre encerramento normal).
|
|
30
|
+
*/
|
|
31
|
+
export declare function runTui(deps?: RunTuiDeps): Promise<number>;
|
|
32
|
+
/** Subconjunto do `process` que `installAltScreen` usa (injetável em testes). */
|
|
33
|
+
export interface AltScreenProc {
|
|
34
|
+
on(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
35
|
+
off(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
36
|
+
exit(code?: number): never;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Entra no alt-screen e instala a restauração à prova de saída não cooperativa
|
|
40
|
+
* (#190, B1). Restaura em: saída normal (via o `dispose` retornado, chamado no
|
|
41
|
+
* `finally`), `process.exit`/fim natural (`'exit'`) e sinais de término
|
|
42
|
+
* (SIGINT/SIGTERM/SIGHUP) — nesses, restaura SÍNCRONO e então encerra com o código
|
|
43
|
+
* convencional. A restauração é IDEMPOTENTE (só escreve a sequência de saída uma vez).
|
|
44
|
+
*
|
|
45
|
+
* @param out - Stream do terminal.
|
|
46
|
+
* @param proc - `process` (ou um fake em testes).
|
|
47
|
+
* @param theme - Paleta ativa no boot (aplica fundo/texto default via OSC).
|
|
48
|
+
* @returns `dispose()` — restaura + remove os listeners (saída normal).
|
|
49
|
+
*/
|
|
50
|
+
export declare function installAltScreen(out: NodeJS.WriteStream, proc: AltScreenProc, theme?: Theme): () => void;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Ponto de entrada da TUI (M2-01).
|
|
4
|
+
*
|
|
5
|
+
* Renderiza o roteador (`app.tsx`) via Ink e resolve quando o usuário sai
|
|
6
|
+
* (`q`, ver `app.tsx`). Consumido pelo CLI quando `redmine-context` roda sem
|
|
7
|
+
* argumentos e o stdout é um TTY (`../cli/main.ts`).
|
|
8
|
+
*
|
|
9
|
+
* `exitOnCtrlC: false` (M2-04): o app implementa seu próprio padrão de saída
|
|
10
|
+
* (duplo Ctrl+C, ver `hooks/use-exit-guard.ts`) — sem essa flag, o Ink sai do
|
|
11
|
+
* processo na primeira pressão de Ctrl+C antes do handler global rodar.
|
|
12
|
+
*
|
|
13
|
+
* Full-screen + paletas (#190): entra no buffer de tela alternativo (alt-screen)
|
|
14
|
+
* em TTY e passa a paleta persistida + o flag full-screen ao `App`.
|
|
15
|
+
*/
|
|
16
|
+
import { render } from 'ink';
|
|
17
|
+
import { defaultSettingsStore } from '../../index.js';
|
|
18
|
+
import { App } from './app.js';
|
|
19
|
+
import { InstanceProvider } from './instance.js';
|
|
20
|
+
import { DEFAULT_PALETTE_ID, resolvePalette } from './palettes.js';
|
|
21
|
+
import { applyTerminalColors, resetTerminalColors } from './terminal-colors.js';
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a instância da TUI no boot (#187) e POPULA `env.REDMINE_URL` para os
|
|
24
|
+
* hooks de dados (que leem o ambiente) — `REDMINE_URL` vence; sem ela, usa a URL
|
|
25
|
+
* persistida no `login`. Extraída para teste (o `runTui` bloqueia em `waitUntilExit`).
|
|
26
|
+
*
|
|
27
|
+
* @param env - Ambiente a consultar/popular (o `runTui` passa `process.env`).
|
|
28
|
+
* @param settings - Store da URL persistida.
|
|
29
|
+
* @returns Metadados da instância + ação de limpar a persistida, para o contexto.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveTuiInstance(env, settings) {
|
|
32
|
+
let url = nonEmpty(env.REDMINE_URL);
|
|
33
|
+
let origin = url !== undefined ? 'env' : 'none';
|
|
34
|
+
if (url === undefined) {
|
|
35
|
+
const persisted = await settings.getInstanceUrl().catch(() => undefined);
|
|
36
|
+
if (persisted !== undefined && persisted.length > 0) {
|
|
37
|
+
env.REDMINE_URL = persisted;
|
|
38
|
+
url = persisted;
|
|
39
|
+
origin = 'config';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...(url !== undefined ? { url } : {}),
|
|
44
|
+
origin,
|
|
45
|
+
clearPersisted: async () => {
|
|
46
|
+
await settings.clearInstanceUrl();
|
|
47
|
+
// Se a instância desta sessão veio da persistida, ela foi escrita em
|
|
48
|
+
// `env.REDMINE_URL` no boot — limpa também, senão os hooks de dados
|
|
49
|
+
// continuariam vendo a instância "logada" após o logout (só um restart
|
|
50
|
+
// corrigiria). Não toca `env` quando a origem foi a própria env real.
|
|
51
|
+
if (origin === 'config') {
|
|
52
|
+
delete env.REDMINE_URL;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sobe a TUI e aguarda o app sair.
|
|
59
|
+
*
|
|
60
|
+
* @param deps - Ver {@link RunTuiDeps} (env/settings/stdout); default: processo real.
|
|
61
|
+
* @returns Exit code `0` (sair da TUI é sempre encerramento normal).
|
|
62
|
+
*/
|
|
63
|
+
export async function runTui(deps = {}) {
|
|
64
|
+
const env = deps.env ?? process.env;
|
|
65
|
+
const settings = deps.settings ?? defaultSettingsStore();
|
|
66
|
+
const instance = await resolveTuiInstance(env, settings);
|
|
67
|
+
// Paleta persistida (#190) — default (Catppuccin Mocha) se nenhuma escolhida.
|
|
68
|
+
const paletteId = (await settings.getPaletteId().catch(() => undefined)) ?? DEFAULT_PALETTE_ID;
|
|
69
|
+
// Full-screen (#190): alt-screen buffer (como vim/htop) só em TTY real — a TUI
|
|
70
|
+
// ocupa a tela toda e o conteúdo anterior do terminal é restaurado ao sair.
|
|
71
|
+
// Fora de TTY o `shouldRenderTui` (M2-03) já nem chega aqui.
|
|
72
|
+
const out = deps.stdout ?? process.stdout;
|
|
73
|
+
const fullScreen = out.isTTY === true;
|
|
74
|
+
// `dispose` restaura o terminal na saída NORMAL (finally). Os handlers de sinal
|
|
75
|
+
// instalados por `installAltScreen` cobrem a saída NÃO cooperativa (SIGTERM/
|
|
76
|
+
// SIGHUP/SIGINT externos, fechar a aba) — sem eles o `finally` de uma Promise
|
|
77
|
+
// não roda e o terminal fica preso no alt-screen (achado B1 da review).
|
|
78
|
+
const bootTheme = resolvePalette(paletteId).theme;
|
|
79
|
+
const dispose = fullScreen ? installAltScreen(out, deps.proc ?? process, bootTheme) : () => undefined;
|
|
80
|
+
try {
|
|
81
|
+
const { waitUntilExit } = render(_jsx(InstanceProvider, { value: instance, children: _jsx(App, { initialPaletteId: paletteId, settings: settings }) }), { exitOnCtrlC: false });
|
|
82
|
+
await waitUntilExit();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
dispose();
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Entra no alt-screen, aplica as cores da paleta como default do terminal (OSC,
|
|
91
|
+
* ANTES do clear — a tela é limpa já com o fundo temático), limpa e esconde o cursor.
|
|
92
|
+
*/
|
|
93
|
+
function enterAltScreen(out, theme) {
|
|
94
|
+
out.write('\x1b[?1049h');
|
|
95
|
+
if (theme !== undefined)
|
|
96
|
+
applyTerminalColors(out, theme);
|
|
97
|
+
out.write('\x1b[H\x1b[2J\x1b[?25l');
|
|
98
|
+
}
|
|
99
|
+
/** Restaura cursor + cores default do terminal + buffer principal (ao sair). */
|
|
100
|
+
function leaveAltScreen(out, theme) {
|
|
101
|
+
out.write('\x1b[?25h');
|
|
102
|
+
if (theme !== undefined)
|
|
103
|
+
resetTerminalColors(out);
|
|
104
|
+
out.write('\x1b[?1049l');
|
|
105
|
+
}
|
|
106
|
+
/** Sinais que terminam o processo e para os quais restauramos o terminal. */
|
|
107
|
+
const RESTORE_SIGNALS = [
|
|
108
|
+
['SIGINT', 130],
|
|
109
|
+
['SIGTERM', 143],
|
|
110
|
+
['SIGHUP', 129],
|
|
111
|
+
];
|
|
112
|
+
/**
|
|
113
|
+
* Entra no alt-screen e instala a restauração à prova de saída não cooperativa
|
|
114
|
+
* (#190, B1). Restaura em: saída normal (via o `dispose` retornado, chamado no
|
|
115
|
+
* `finally`), `process.exit`/fim natural (`'exit'`) e sinais de término
|
|
116
|
+
* (SIGINT/SIGTERM/SIGHUP) — nesses, restaura SÍNCRONO e então encerra com o código
|
|
117
|
+
* convencional. A restauração é IDEMPOTENTE (só escreve a sequência de saída uma vez).
|
|
118
|
+
*
|
|
119
|
+
* @param out - Stream do terminal.
|
|
120
|
+
* @param proc - `process` (ou um fake em testes).
|
|
121
|
+
* @param theme - Paleta ativa no boot (aplica fundo/texto default via OSC).
|
|
122
|
+
* @returns `dispose()` — restaura + remove os listeners (saída normal).
|
|
123
|
+
*/
|
|
124
|
+
export function installAltScreen(out, proc, theme) {
|
|
125
|
+
enterAltScreen(out, theme);
|
|
126
|
+
let restored = false;
|
|
127
|
+
const restore = () => {
|
|
128
|
+
if (restored)
|
|
129
|
+
return;
|
|
130
|
+
restored = true;
|
|
131
|
+
leaveAltScreen(out, theme);
|
|
132
|
+
};
|
|
133
|
+
const onExit = () => restore();
|
|
134
|
+
const signalHandlers = RESTORE_SIGNALS.map(([signal, code]) => {
|
|
135
|
+
const handler = () => {
|
|
136
|
+
restore();
|
|
137
|
+
cleanup();
|
|
138
|
+
proc.exit(code);
|
|
139
|
+
};
|
|
140
|
+
proc.on(signal, handler);
|
|
141
|
+
return [signal, handler];
|
|
142
|
+
});
|
|
143
|
+
proc.on('exit', onExit);
|
|
144
|
+
function cleanup() {
|
|
145
|
+
proc.off('exit', onExit);
|
|
146
|
+
for (const [signal, handler] of signalHandlers)
|
|
147
|
+
proc.off(signal, handler);
|
|
148
|
+
}
|
|
149
|
+
return () => {
|
|
150
|
+
restore();
|
|
151
|
+
cleanup();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Retorna a string aparada se não-vazia, senão `undefined`. */
|
|
155
|
+
function nonEmpty(value) {
|
|
156
|
+
const trimmed = value?.trim();
|
|
157
|
+
return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined;
|
|
158
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contexto da INSTÂNCIA resolvida da TUI (#187).
|
|
3
|
+
*
|
|
4
|
+
* O `runTui` resolve a instância uma vez no boot — `REDMINE_URL` (env) tem
|
|
5
|
+
* precedência; sem ela, usa a URL persistida no `login` — e a expõe aqui para as
|
|
6
|
+
* telas que precisam saber a ORIGEM (`config`/`doctor`) e para o `logout` também
|
|
7
|
+
* limpar a URL persistida. Os hooks de dados continuam lendo `process.env`
|
|
8
|
+
* (populado no boot), então este contexto carrega só metadados + a ação de limpar.
|
|
9
|
+
*/
|
|
10
|
+
import { type ReactNode } from 'react';
|
|
11
|
+
/** Origem da instância em uso na TUI. */
|
|
12
|
+
export type InstanceOrigin = 'env' | 'config' | 'none';
|
|
13
|
+
/** Metadados da instância resolvida + ação para limpar a URL persistida. */
|
|
14
|
+
export interface InstanceInfo {
|
|
15
|
+
/** URL da instância em uso, ou `undefined` se nenhuma. */
|
|
16
|
+
readonly url?: string;
|
|
17
|
+
/** De onde veio: `env` (REDMINE_URL), `config` (persistida) ou `none`. */
|
|
18
|
+
readonly origin: InstanceOrigin;
|
|
19
|
+
/** Remove a URL persistida (usado no logout); no-op se não houver store. */
|
|
20
|
+
readonly clearPersisted: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** Provider da instância resolvida (montado pelo `runTui`). */
|
|
23
|
+
export declare function InstanceProvider({ value, children, }: {
|
|
24
|
+
value: InstanceInfo;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}): ReactNode;
|
|
27
|
+
/** Lê a instância resolvida no contexto (default seguro fora do provider). */
|
|
28
|
+
export declare function useInstance(): InstanceInfo;
|
|
29
|
+
/**
|
|
30
|
+
* `true` se a credencial de AMBIENTE (`REDMINE_API_KEY`) pode ser usada nesta
|
|
31
|
+
* sessão. SEGURANÇA (#187): quando a instância veio de fonte MUTÁVEL (`settings.json`
|
|
32
|
+
* persistido, origem `config`), retorna `false` — a env-key é instance-agnóstica e
|
|
33
|
+
* não pode ser enviada a uma URL controlável por arquivo. Espelha o `allowEnvFallback`
|
|
34
|
+
* já aplicado na CLI (`issue`) e no MCP; deve ser passado no `CredentialCascadeOptions`
|
|
35
|
+
* de TODA resolução de credencial da TUI.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useEnvFallbackAllowed(): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Contexto da INSTÂNCIA resolvida da TUI (#187).
|
|
4
|
+
*
|
|
5
|
+
* O `runTui` resolve a instância uma vez no boot — `REDMINE_URL` (env) tem
|
|
6
|
+
* precedência; sem ela, usa a URL persistida no `login` — e a expõe aqui para as
|
|
7
|
+
* telas que precisam saber a ORIGEM (`config`/`doctor`) e para o `logout` também
|
|
8
|
+
* limpar a URL persistida. Os hooks de dados continuam lendo `process.env`
|
|
9
|
+
* (populado no boot), então este contexto carrega só metadados + a ação de limpar.
|
|
10
|
+
*/
|
|
11
|
+
import { createContext, useContext } from 'react';
|
|
12
|
+
/** Default seguro: sem instância e limpeza no-op (usado fora do provider, ex.: testes). */
|
|
13
|
+
const DEFAULT = {
|
|
14
|
+
origin: 'none',
|
|
15
|
+
clearPersisted: async () => undefined,
|
|
16
|
+
};
|
|
17
|
+
const InstanceContext = createContext(DEFAULT);
|
|
18
|
+
/** Provider da instância resolvida (montado pelo `runTui`). */
|
|
19
|
+
export function InstanceProvider({ value, children, }) {
|
|
20
|
+
return _jsx(InstanceContext.Provider, { value: value, children: children });
|
|
21
|
+
}
|
|
22
|
+
/** Lê a instância resolvida no contexto (default seguro fora do provider). */
|
|
23
|
+
export function useInstance() {
|
|
24
|
+
return useContext(InstanceContext);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `true` se a credencial de AMBIENTE (`REDMINE_API_KEY`) pode ser usada nesta
|
|
28
|
+
* sessão. SEGURANÇA (#187): quando a instância veio de fonte MUTÁVEL (`settings.json`
|
|
29
|
+
* persistido, origem `config`), retorna `false` — a env-key é instance-agnóstica e
|
|
30
|
+
* não pode ser enviada a uma URL controlável por arquivo. Espelha o `allowEnvFallback`
|
|
31
|
+
* já aplicado na CLI (`issue`) e no MCP; deve ser passado no `CredentialCascadeOptions`
|
|
32
|
+
* de TODA resolução de credencial da TUI.
|
|
33
|
+
*/
|
|
34
|
+
export function useEnvFallbackAllowed() {
|
|
35
|
+
return useInstance().origin !== 'config';
|
|
36
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registro leve de jobs da sessão (#34/M2-11) — fonte de dados do painel de
|
|
3
|
+
* jobs (`./screens/jobs.tsx`, tecla `t` na home/detalhe).
|
|
4
|
+
*
|
|
5
|
+
* Mesmo padrão de `./screens/home-selection.tsx`/`./screens/loaded-issue-context.tsx`
|
|
6
|
+
* (ver o JSDoc de qualquer um dos dois para as alternativas descartadas —
|
|
7
|
+
* singleton de módulo vazaria estado entre testes; guardar isto dentro de
|
|
8
|
+
* `NavigationValue` acoplaria o roteador a um detalhe de produtores
|
|
9
|
+
* específicos): instanciado uma vez em `./app.tsx`, ACIMA da pilha de
|
|
10
|
+
* navegação, para que jobs sobrevivam ao push/pop de telas — iniciar uma
|
|
11
|
+
* exportação (`./screens/export.tsx`), navegar para outra tela e voltar ainda
|
|
12
|
+
* deve mostrar o job (em andamento ou já concluído) no painel.
|
|
13
|
+
*
|
|
14
|
+
* ## Contrato para produtores futuros (M3/M4 — extração de anexos)
|
|
15
|
+
*
|
|
16
|
+
* Qualquer operação assíncrona da sessão que o usuário deva acompanhar
|
|
17
|
+
* (exportação hoje; extração de imagem/PDF no M3, áudio/vídeo no M4) é um
|
|
18
|
+
* "produtor": chama {@link JobRegistryValue.registerJob} uma vez ao iniciar
|
|
19
|
+
* e {@link JobRegistryValue.updateJobStatus} a cada transição de estado.
|
|
20
|
+
* Regras do contrato:
|
|
21
|
+
*
|
|
22
|
+
* 1. `id` é escolhido pelo PRODUTOR, não pelo registro — deve ser único por
|
|
23
|
+
* EXECUÇÃO (ex.: `extract-${attachmentId}-${Date.now()}`), permitindo que
|
|
24
|
+
* tentativas repetidas do mesmo tipo de job apareçam como entradas
|
|
25
|
+
* distintas no painel (histórico da sessão, não um contador único por
|
|
26
|
+
* recurso). `registerJob` faz upsert por `id`: registrar de novo com um
|
|
27
|
+
* `id` já existente substitui a entrada em vez de duplicá-la.
|
|
28
|
+
* 2. Transições de `status` esperadas: `pending` (enfileirado, ainda não
|
|
29
|
+
* iniciado) → `processing` (em execução) → `done` | `failed` (estado
|
|
30
|
+
* terminal). Produtores sem fase de fila (ex.: exportação, #33 — dispara
|
|
31
|
+
* e já começa a gravar) podem registrar direto em `processing`.
|
|
32
|
+
* 3. `startedAt` (epoch ms, `Date.now()`) é OPCIONAL — quando presente, é o
|
|
33
|
+
* instante em que o job entrou em `processing` (não o momento do
|
|
34
|
+
* registro em si, que pode ser antes se o job nasceu `pending`). Hoje o
|
|
35
|
+
* painel (`./screens/jobs.tsx`) não renderiza duração a partir disso —
|
|
36
|
+
* campo já capturado para uma UI futura sem exigir novo campo no
|
|
37
|
+
* contrato.
|
|
38
|
+
* 4. `cancelable`/`onCancel`: só produtores que sabem interromper a
|
|
39
|
+
* operação de fato devem marcar `cancelable: true` E fornecer
|
|
40
|
+
* `onCancel` (chamado pelo painel ao usuário pressionar Ctrl+C com o job
|
|
41
|
+
* selecionado). Exportação (#33) NÃO é cancelável — nada a abortar no
|
|
42
|
+
* meio de um `writeFile` — o painel mostra "(não cancelável)" para ela.
|
|
43
|
+
* A infraestrutura de cancelamento REAL (abortar uma extração em
|
|
44
|
+
* andamento) é do M4; até lá, nenhum produtor real marca `cancelable`.
|
|
45
|
+
* 5. Jobs terminais (`done`/`failed`) permanecem no registro até o fim da
|
|
46
|
+
* sessão (histórico "concluídas da sessão" da AC) — nenhuma limpeza
|
|
47
|
+
* automática hoje.
|
|
48
|
+
*/
|
|
49
|
+
import { type ReactNode } from 'react';
|
|
50
|
+
/** Estados possíveis de um job (ver a regra 2 do contrato no JSDoc do módulo). */
|
|
51
|
+
export type JobStatus = 'pending' | 'processing' | 'done' | 'failed';
|
|
52
|
+
/**
|
|
53
|
+
* Um job da sessão, exibido no painel (`./screens/jobs.tsx`). Ver o JSDoc do
|
|
54
|
+
* módulo para o contrato completo consumido por produtores futuros (M3/M4).
|
|
55
|
+
*/
|
|
56
|
+
export interface Job {
|
|
57
|
+
/** Identificador único da EXECUÇÃO (não do recurso) — escolhido pelo produtor, ver regra 1. */
|
|
58
|
+
id: string;
|
|
59
|
+
/** Rótulo legível exibido no painel (ex.: `Exportar #88 (md)`). */
|
|
60
|
+
label: string;
|
|
61
|
+
/** Estado atual — ver as transições esperadas na regra 2. */
|
|
62
|
+
status: JobStatus;
|
|
63
|
+
/** Instante (epoch ms) em que o job entrou em `processing` — ver regra 3. Opcional. */
|
|
64
|
+
startedAt?: number;
|
|
65
|
+
/** `true` só quando o produtor também fornece {@link onCancel} — ver regra 4. Default `false`. */
|
|
66
|
+
cancelable?: boolean;
|
|
67
|
+
/** Chamado pelo painel ao usuário pressionar Ctrl+C com este job selecionado, quando `cancelable` é `true`. */
|
|
68
|
+
onCancel?: () => void;
|
|
69
|
+
/** Progresso incremental 0–100 (extrações M3/M4). Omitido: indeterminado. */
|
|
70
|
+
progress?: number;
|
|
71
|
+
/** Detalhe curto da fase atual ou mensagem de erro (ex.: "transcrevendo áudio"). */
|
|
72
|
+
detail?: string;
|
|
73
|
+
}
|
|
74
|
+
/** Campos atualizáveis de um job em andamento — ver {@link JobRegistryValue.updateJob}. */
|
|
75
|
+
export interface JobPatch {
|
|
76
|
+
status?: JobStatus;
|
|
77
|
+
progress?: number;
|
|
78
|
+
detail?: string;
|
|
79
|
+
label?: string;
|
|
80
|
+
}
|
|
81
|
+
/** Valor exposto pelo contexto do registro de jobs. */
|
|
82
|
+
export interface JobRegistryValue {
|
|
83
|
+
/** Jobs da sessão, na ordem de registro (mais antigo primeiro). */
|
|
84
|
+
jobs: readonly Job[];
|
|
85
|
+
/**
|
|
86
|
+
* Registra um job novo, ou substitui (upsert por `id`) um já existente —
|
|
87
|
+
* ver regra 1 do contrato.
|
|
88
|
+
*/
|
|
89
|
+
registerJob: (job: Job) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Atualiza campos do job (`status`/`progress`/`detail`/`label`). Sem efeito
|
|
92
|
+
* se `id` não existir. Transições a partir de estado TERMINAL (`done`/
|
|
93
|
+
* `failed`) são ignoradas — bug de produtor não regride o painel.
|
|
94
|
+
*/
|
|
95
|
+
updateJob: (id: string, patch: JobPatch) => void;
|
|
96
|
+
/** Atalho de compatibilidade: `updateJob(id, { status })`. */
|
|
97
|
+
updateJobStatus: (id: string, status: JobStatus) => void;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Provider do registro de jobs — instanciado uma vez em `./app.tsx`,
|
|
101
|
+
* envolvendo toda a árvore de telas (ver o JSDoc do módulo).
|
|
102
|
+
*/
|
|
103
|
+
export declare function JobRegistryProvider({ children }: {
|
|
104
|
+
children: ReactNode;
|
|
105
|
+
}): import("react").JSX.Element;
|
|
106
|
+
/**
|
|
107
|
+
* Hook do registro de jobs, consumido tanto por produtores (`./screens/export.tsx`,
|
|
108
|
+
* futuramente extração no M3/M4) quanto pelo painel (`./screens/jobs.tsx`).
|
|
109
|
+
*
|
|
110
|
+
* @throws {Error} Se chamado fora de um `<JobRegistryProvider>` — indica erro
|
|
111
|
+
* de composição (tela renderizada sem passar pelo roteador).
|
|
112
|
+
*/
|
|
113
|
+
export declare function useJobRegistry(): JobRegistryValue;
|