@neetru/cli 2.7.4 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +220 -208
  2. package/README.md +137 -137
  3. package/dist/cli-kit/format.d.ts +49 -0
  4. package/dist/cli-kit/format.js +88 -0
  5. package/dist/cli-kit/format.js.map +1 -0
  6. package/dist/cli-kit/glyphs.d.ts +22 -0
  7. package/dist/cli-kit/glyphs.js +22 -0
  8. package/dist/cli-kit/glyphs.js.map +1 -0
  9. package/dist/cli-kit/index.d.ts +13 -0
  10. package/dist/cli-kit/index.js +12 -0
  11. package/dist/cli-kit/index.js.map +1 -0
  12. package/dist/cli-kit/palette.d.ts +10 -0
  13. package/dist/cli-kit/palette.js +36 -0
  14. package/dist/cli-kit/palette.js.map +1 -0
  15. package/dist/commands/ai.js +8 -8
  16. package/dist/commands/autocomplete.js +34 -34
  17. package/dist/commands/db.d.ts +87 -7
  18. package/dist/commands/db.js +697 -126
  19. package/dist/commands/db.js.map +1 -1
  20. package/dist/commands/deploy.d.ts +5 -0
  21. package/dist/commands/deploy.js +68 -0
  22. package/dist/commands/deploy.js.map +1 -1
  23. package/dist/commands/dev.d.ts +68 -0
  24. package/dist/commands/dev.js +345 -0
  25. package/dist/commands/dev.js.map +1 -0
  26. package/dist/commands/init.js +121 -121
  27. package/dist/commands/new.d.ts +6 -0
  28. package/dist/commands/new.js +31 -10
  29. package/dist/commands/new.js.map +1 -1
  30. package/dist/commands/products-db.d.ts +1 -1
  31. package/dist/commands/products-db.js +17 -4
  32. package/dist/commands/products-db.js.map +1 -1
  33. package/dist/commands/upgrade.js +5 -2
  34. package/dist/commands/upgrade.js.map +1 -1
  35. package/dist/index.js +258 -42
  36. package/dist/index.js.map +1 -1
  37. package/dist/lib/ai/context.js +90 -90
  38. package/dist/lib/db-local/db-json.d.ts +63 -0
  39. package/dist/lib/db-local/db-json.js +189 -0
  40. package/dist/lib/db-local/db-json.js.map +1 -0
  41. package/dist/lib/db-local/env.d.ts +26 -0
  42. package/dist/lib/db-local/env.js +64 -0
  43. package/dist/lib/db-local/env.js.map +1 -0
  44. package/dist/lib/db-local/fingerprint.d.ts +8 -0
  45. package/dist/lib/db-local/fingerprint.js +28 -0
  46. package/dist/lib/db-local/fingerprint.js.map +1 -0
  47. package/dist/lib/db-local/index.d.ts +15 -0
  48. package/dist/lib/db-local/index.js +14 -0
  49. package/dist/lib/db-local/index.js.map +1 -0
  50. package/dist/lib/db-pipeline/build-deps.d.ts +14 -0
  51. package/dist/lib/db-pipeline/build-deps.js +158 -0
  52. package/dist/lib/db-pipeline/build-deps.js.map +1 -0
  53. package/dist/lib/db-pipeline/errors.d.ts +29 -0
  54. package/dist/lib/db-pipeline/errors.js +29 -0
  55. package/dist/lib/db-pipeline/errors.js.map +1 -0
  56. package/dist/lib/db-pipeline/index.d.ts +26 -0
  57. package/dist/lib/db-pipeline/index.js +25 -0
  58. package/dist/lib/db-pipeline/index.js.map +1 -0
  59. package/dist/lib/db-pipeline/pipeline.d.ts +13 -0
  60. package/dist/lib/db-pipeline/pipeline.js +119 -0
  61. package/dist/lib/db-pipeline/pipeline.js.map +1 -0
  62. package/dist/lib/db-pipeline/rehearse.d.ts +99 -0
  63. package/dist/lib/db-pipeline/rehearse.js +219 -0
  64. package/dist/lib/db-pipeline/rehearse.js.map +1 -0
  65. package/dist/lib/db-pipeline/types.d.ts +112 -0
  66. package/dist/lib/db-pipeline/types.js +20 -0
  67. package/dist/lib/db-pipeline/types.js.map +1 -0
  68. package/package.json +63 -62
  69. package/templates/auth/callback.ts +22 -22
  70. package/templates/auth/sign-in.tsx +41 -41
  71. package/templates/billing/checkout.ts +22 -22
  72. package/templates/billing/page.tsx +43 -43
  73. package/templates/support/ticket-form.tsx +68 -68
  74. package/templates/usage/track.ts +30 -30
  75. package/templates/users/profile.tsx +43 -43
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@neetru/db-pipeline` — o motor client-side do `neetru db apply`.
3
+ *
4
+ * O `neetru db apply` publica uma mudança de schema de um produto. Este
5
+ * pacote roda, CLIENT-SIDE, as fases 1-5:
6
+ * 1. fingerprint — hash determinístico do schema.
7
+ * 2. generate — `drizzle-kit generate` produz o `.sql`.
8
+ * 3. rehearse — ensaia o `.sql` num Postgres efêmero real.
9
+ * 4. classify — `@neetru/db-classifier` marca aditiva/destrutiva.
10
+ * 5. push — adquire um lock + envia o artefato validado pro Core.
11
+ *
12
+ * Ele NÃO decide política — produz um `.sql` validado e classificado e o
13
+ * envia. A decisão de aplicar é do Core.
14
+ *
15
+ * Superfície pública:
16
+ * - `runApplyPipeline` — o orquestrador deps-injetado (unit-testável).
17
+ * - `buildPipelineDeps` — a fiação de produção (drizzle-kit / classifier / HTTP).
18
+ * - `PipelineError` / `isPipelineError` — o erro tipado do pipeline.
19
+ * - os tipos do contrato.
20
+ */
21
+ export { runApplyPipeline } from './pipeline.js';
22
+ export { buildPipelineDeps } from './build-deps.js';
23
+ export { PipelineError, isPipelineError } from './errors.js';
24
+ export { buildRehearseInContainer, buildDockerExecFn, RehearseDockerUnavailableError, isRehearseDockerUnavailableError, } from './rehearse.js';
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/db-pipeline/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,8BAA8B,EAC9B,gCAAgC,GACjC,MAAM,eAAe,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { PipelineDeps, PipelineInput, PipelineResult } from './types.js';
2
+ /**
3
+ * Roda o pipeline `neetru db apply` (fases 1-5), emitindo `onPhase` antes de
4
+ * cada fase.
5
+ *
6
+ * Retorna `{ migrationId, status, severity }`. Quando o `.sql` gerado é vazio
7
+ * (o schema não mudou), retorna cedo com `status: 'no_changes'` e SEM chamar
8
+ * `pushToCore` — não há nada pra empurrar.
9
+ *
10
+ * Lança `PipelineError` quando a ensaiada (`rehearse`) falha. Propaga
11
+ * qualquer outro erro das deps (ex.: `generateSql` falhar) sem engolir.
12
+ */
13
+ export declare function runApplyPipeline(input: PipelineInput, deps: PipelineDeps): Promise<PipelineResult>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `@neetru/db-pipeline` — o orquestrador do `neetru db apply`.
3
+ *
4
+ * `runApplyPipeline` encadeia as 5 fases CLIENT-SIDE do `neetru db apply`:
5
+ *
6
+ * 1. fingerprint — `readSchemaFile` → `schemaFingerprint` (de `@neetru/db-local`).
7
+ * 2. generate — `generateSql(schemaPath)` produz o `.sql`.
8
+ * `.sql` vazio (sem mudança) → retorna cedo (`no_changes`).
9
+ * 3. rehearse — `rehearseInContainer(sql)` ensaia num Postgres efêmero.
10
+ * `!ok` → lança `PipelineError` (migração quebrada, NÃO empurra).
11
+ * 4. classify — `classify(sql)` marca aditiva/destrutiva.
12
+ * 5. push — `pushToCore({ sql, fingerprint, classification, env })`.
13
+ *
14
+ * REGRA DURA — fail-closed: uma falha em `generate` ou `rehearse` PARA o
15
+ * pipeline. Nada é empurrado pro Core. O pipeline nunca engole um erro —
16
+ * tudo é re-lançado (encadeado num `PipelineError` quando faz sentido).
17
+ *
18
+ * Deps-injetado: zero Docker, zero Postgres, zero rede aqui dentro. A fiação
19
+ * real vive em `buildPipelineDeps()` (`./build-deps.js`).
20
+ */
21
+ import { schemaFingerprint } from '../db-local/index.js';
22
+ import { PipelineError } from './errors.js';
23
+ /** Dispara o callback de progresso sem deixar um erro do callback derrubar o pipeline. */
24
+ function emitPhase(deps, phase) {
25
+ if (typeof deps.onPhase !== 'function')
26
+ return;
27
+ try {
28
+ deps.onPhase(phase);
29
+ }
30
+ catch {
31
+ // O callback de progresso é cosmético — um erro nele não pode quebrar
32
+ // uma migração. Ignora de forma deliberada.
33
+ }
34
+ }
35
+ /**
36
+ * Roda o pipeline `neetru db apply` (fases 1-5), emitindo `onPhase` antes de
37
+ * cada fase.
38
+ *
39
+ * Retorna `{ migrationId, status, severity }`. Quando o `.sql` gerado é vazio
40
+ * (o schema não mudou), retorna cedo com `status: 'no_changes'` e SEM chamar
41
+ * `pushToCore` — não há nada pra empurrar.
42
+ *
43
+ * Lança `PipelineError` quando a ensaiada (`rehearse`) falha. Propaga
44
+ * qualquer outro erro das deps (ex.: `generateSql` falhar) sem engolir.
45
+ */
46
+ export async function runApplyPipeline(input, deps) {
47
+ const { schemaPath, env, dbId } = input;
48
+ // ── Fase 1 — fingerprint ────────────────────────────────────────────────
49
+ emitPhase(deps, 'fingerprint');
50
+ let fingerprint;
51
+ try {
52
+ const schemaContent = deps.readSchemaFile(schemaPath);
53
+ fingerprint = schemaFingerprint(schemaContent);
54
+ }
55
+ catch (err) {
56
+ throw new PipelineError('fingerprint', `Falha ao ler/fingerprint o schema em "${schemaPath}": ${err instanceof Error ? err.message : String(err)}`, { cause: err });
57
+ }
58
+ // ── Fase 2 — generate ───────────────────────────────────────────────────
59
+ emitPhase(deps, 'generate');
60
+ let sql;
61
+ try {
62
+ sql = await deps.generateSql(schemaPath);
63
+ }
64
+ catch (err) {
65
+ // Fail-closed: sem `.sql` não há o que ensaiar nem empurrar.
66
+ throw new PipelineError('generate', `Falha ao gerar o .sql (drizzle-kit generate) para "${schemaPath}": ${err instanceof Error ? err.message : String(err)}`, { cause: err });
67
+ }
68
+ // `.sql` vazio = o schema não mudou. Não há migração a criar — retorna cedo.
69
+ // `pushToCore` NÃO é chamado.
70
+ if (typeof sql !== 'string' || sql.trim().length === 0) {
71
+ return {
72
+ migrationId: '(nenhuma)',
73
+ status: 'no_changes',
74
+ severity: 'aditiva',
75
+ };
76
+ }
77
+ // ── Fase 3 — rehearse ───────────────────────────────────────────────────
78
+ emitPhase(deps, 'rehearse');
79
+ let rehearsal;
80
+ try {
81
+ rehearsal = await deps.rehearseInContainer(sql);
82
+ }
83
+ catch (err) {
84
+ throw new PipelineError('rehearse', `A ensaiada do .sql num Postgres efêmero não pôde rodar: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
85
+ }
86
+ if (!rehearsal || rehearsal.ok !== true) {
87
+ // O `.sql` falhou na própria ensaiada — empurrar isso pro Core seria
88
+ // empurrar uma migração quebrada. Fail-closed: para aqui.
89
+ throw new PipelineError('rehearse', `A migração falhou na ensaiada (Postgres efêmero) e NÃO será enviada` +
90
+ `${rehearsal?.error ? `: ${rehearsal.error}` : '.'}`);
91
+ }
92
+ // ── Fase 4 — classify ───────────────────────────────────────────────────
93
+ emitPhase(deps, 'classify');
94
+ let classification;
95
+ try {
96
+ classification = deps.classify(sql);
97
+ }
98
+ catch (err) {
99
+ throw new PipelineError('classify', `Falha ao classificar a migração (aditiva/destrutiva): ${err instanceof Error ? err.message : String(err)}`, { cause: err });
100
+ }
101
+ // ── Fase 5 — push ───────────────────────────────────────────────────────
102
+ emitPhase(deps, 'push');
103
+ let pushed;
104
+ try {
105
+ pushed = await deps.pushToCore({ dbId, sql, fingerprint, classification, env });
106
+ }
107
+ catch (err) {
108
+ throw new PipelineError('push', `Falha ao adquirir o lock / enviar o artefato pro Core: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
109
+ }
110
+ return {
111
+ migrationId: pushed.migrationId,
112
+ status: pushed.status,
113
+ severity: classification.overallSeverity,
114
+ // H-7 fix: devolve o fingerprint pro caller para que ele persista em
115
+ // `.neetru/db.json` como `lastAppliedFingerprint`.
116
+ fingerprint,
117
+ };
118
+ }
119
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../../src/lib/db-pipeline/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `@neetru/db-pipeline` — ensaiada real num Postgres efêmero via Docker CLI.
3
+ *
4
+ * Esta é a implementação REAL de `rehearseInContainer`. Ela:
5
+ * 1. Verifica se o Docker está disponível (`docker version`).
6
+ * 2. Sobe um container Postgres efêmero (`postgres:16-alpine`).
7
+ * 3. Aguarda o Postgres estar pronto para aceitar conexões (`pg_isready`).
8
+ * 4. Aplica o `.sql` candidato via `docker exec psql`.
9
+ * 5. Derruba o container no `finally` (mesmo em caso de erro — sem leaks).
10
+ * 6. Devolve `{ ok: true }` ou `{ ok: false, error: <mensagem do psql> }`.
11
+ *
12
+ * CAMADA DOCKER INJETÁVEL (`DockerExecFn`) — zero Docker real nos testes.
13
+ * `buildRehearseInContainer(execDocker)` retorna a função com a assinatura
14
+ * exata de `PipelineDeps['rehearseInContainer']`.
15
+ *
16
+ * Fail-closed:
17
+ * - Docker ausente → lança `RehearseDockerUnavailableError`.
18
+ * Nunca retorna `{ ok: true }` sem fazer o trabalho.
19
+ * - SQL inválido → `{ ok: false, error: <mensagem do psql> }` (não lança).
20
+ *
21
+ * Override explícito via `skipRehearse`:
22
+ * - Passa `skipRehearse: true` em `RehearseOptions` para contornar o container.
23
+ * - Este override DEVE SER LOGADO pelo caller (`--skip-rehearse` na CLI).
24
+ * - Não é silencioso — se o caller silenciar o log, a responsabilidade é dele.
25
+ */
26
+ import type { RehearsalResult } from './types.js';
27
+ /**
28
+ * Resultado de um exec de docker.
29
+ *
30
+ * Intencionalmente isomorfo ao `DockerExecResult` de `dev.ts` — mas definido
31
+ * localmente para que o módulo de rehearse não acople ao módulo de dev.
32
+ */
33
+ export interface DockerExecResult {
34
+ stdout: string;
35
+ exit: number;
36
+ }
37
+ /**
38
+ * Função injetável que executa `docker <args>`.
39
+ *
40
+ * Produção: `buildDockerExecFn()` retorna a fiação real via `child_process.spawn`.
41
+ * Testes: mock passado diretamente — sem Docker, sem CI, sem side effects.
42
+ *
43
+ * Contrato: NÃO deve lançar em exit != 0. O caller decide o que fazer com o
44
+ * código de saída. Só lança em falha estrutural (ex.: `spawn` não encontrou o
45
+ * executável).
46
+ */
47
+ export type DockerExecFn = (args: string[]) => Promise<DockerExecResult>;
48
+ /** Opções para `buildRehearseInContainer`. */
49
+ export interface RehearseOptions {
50
+ /**
51
+ * Imagem Docker a usar para o container efêmero.
52
+ * Default: `postgres:16-alpine`.
53
+ */
54
+ image?: string;
55
+ /**
56
+ * Timeout em ms para aguardar o Postgres ficar pronto (`pg_isready`).
57
+ * Default: 30000 (30s).
58
+ */
59
+ readyTimeoutMs?: number;
60
+ /**
61
+ * Override operacional explícito: pula o container e devolve `{ ok: true }`.
62
+ *
63
+ * DEVE SER LOGADO pelo caller antes de invocar. Uso: ambientes CI sem Docker,
64
+ * flag `--skip-rehearse` na CLI. Não é silencioso por contrato.
65
+ */
66
+ skipRehearse?: boolean;
67
+ }
68
+ /**
69
+ * Lançado quando o Docker CLI não está disponível no PATH ou o daemon não
70
+ * responde. Distingue "ambiente sem Docker" de "SQL quebrado".
71
+ */
72
+ export declare class RehearseDockerUnavailableError extends Error {
73
+ readonly isRehearseDockerUnavailableError: true;
74
+ constructor(message: string, options?: {
75
+ cause?: unknown;
76
+ });
77
+ }
78
+ /** Type guard — `true` sse `value` é um `RehearseDockerUnavailableError`. */
79
+ export declare function isRehearseDockerUnavailableError(value: unknown): value is RehearseDockerUnavailableError;
80
+ /**
81
+ * Constrói a função `rehearseInContainer` com a camada Docker injetada.
82
+ *
83
+ * Produção:
84
+ * ```ts
85
+ * const rehearse = buildRehearseInContainer(buildDockerExecFn());
86
+ * // deps: { rehearseInContainer: rehearse, ... }
87
+ * ```
88
+ *
89
+ * Testes: injeta um mock de `execDocker` que controla cada resposta.
90
+ */
91
+ export declare function buildRehearseInContainer(execDocker: DockerExecFn, opts?: RehearseOptions): (sql: string) => Promise<RehearsalResult>;
92
+ /**
93
+ * Constrói a função `execDocker` de produção via `child_process.spawn`.
94
+ *
95
+ * Separada do `buildRehearseInContainer` para que:
96
+ * (a) o módulo seja importável sem side effects em testes;
97
+ * (b) o `spawn` seja injetável em camada superior quando necessário.
98
+ */
99
+ export declare function buildDockerExecFn(): DockerExecFn;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * `@neetru/db-pipeline` — ensaiada real num Postgres efêmero via Docker CLI.
3
+ *
4
+ * Esta é a implementação REAL de `rehearseInContainer`. Ela:
5
+ * 1. Verifica se o Docker está disponível (`docker version`).
6
+ * 2. Sobe um container Postgres efêmero (`postgres:16-alpine`).
7
+ * 3. Aguarda o Postgres estar pronto para aceitar conexões (`pg_isready`).
8
+ * 4. Aplica o `.sql` candidato via `docker exec psql`.
9
+ * 5. Derruba o container no `finally` (mesmo em caso de erro — sem leaks).
10
+ * 6. Devolve `{ ok: true }` ou `{ ok: false, error: <mensagem do psql> }`.
11
+ *
12
+ * CAMADA DOCKER INJETÁVEL (`DockerExecFn`) — zero Docker real nos testes.
13
+ * `buildRehearseInContainer(execDocker)` retorna a função com a assinatura
14
+ * exata de `PipelineDeps['rehearseInContainer']`.
15
+ *
16
+ * Fail-closed:
17
+ * - Docker ausente → lança `RehearseDockerUnavailableError`.
18
+ * Nunca retorna `{ ok: true }` sem fazer o trabalho.
19
+ * - SQL inválido → `{ ok: false, error: <mensagem do psql> }` (não lança).
20
+ *
21
+ * Override explícito via `skipRehearse`:
22
+ * - Passa `skipRehearse: true` em `RehearseOptions` para contornar o container.
23
+ * - Este override DEVE SER LOGADO pelo caller (`--skip-rehearse` na CLI).
24
+ * - Não é silencioso — se o caller silenciar o log, a responsabilidade é dele.
25
+ */
26
+ import { randomBytes } from 'node:crypto';
27
+ import { spawn } from 'node:child_process';
28
+ // ---------------------------------------------------------------------------
29
+ // Erro tipado: Docker indisponível
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Lançado quando o Docker CLI não está disponível no PATH ou o daemon não
33
+ * responde. Distingue "ambiente sem Docker" de "SQL quebrado".
34
+ */
35
+ export class RehearseDockerUnavailableError extends Error {
36
+ isRehearseDockerUnavailableError = true;
37
+ constructor(message, options) {
38
+ super(message, options);
39
+ this.name = 'RehearseDockerUnavailableError';
40
+ if (typeof Error.captureStackTrace === 'function') {
41
+ Error.captureStackTrace(this, RehearseDockerUnavailableError);
42
+ }
43
+ }
44
+ }
45
+ /** Type guard — `true` sse `value` é um `RehearseDockerUnavailableError`. */
46
+ export function isRehearseDockerUnavailableError(value) {
47
+ return (value instanceof RehearseDockerUnavailableError ||
48
+ (typeof value === 'object' &&
49
+ value !== null &&
50
+ value
51
+ .isRehearseDockerUnavailableError === true));
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Constantes internas
55
+ // ---------------------------------------------------------------------------
56
+ /** Imagem Postgres padrão — leve e suficiente para ensaiar DML/DDL. */
57
+ const DEFAULT_IMAGE = 'postgres:16-alpine';
58
+ /** Tempo máximo de espera para o Postgres aceitar conexões. */
59
+ const DEFAULT_READY_TIMEOUT_MS = 30_000;
60
+ /** Intervalo entre polls de `pg_isready`. */
61
+ const READY_POLL_INTERVAL_MS = 500;
62
+ /**
63
+ * Credenciais usadas APENAS no container efêmero.
64
+ * Não há dado real — este banco existe por menos de 60s e é descartado.
65
+ */
66
+ const PG_ENV = {
67
+ user: 'neetru_rehearse',
68
+ password: 'neetru_rehearse_dev',
69
+ db: 'neetru_rehearse',
70
+ };
71
+ // ---------------------------------------------------------------------------
72
+ // Helpers internos
73
+ // ---------------------------------------------------------------------------
74
+ /** Gera um nome de container único por ensaiada (sem colisões em CI paralelo). */
75
+ function ephemeralContainerName() {
76
+ return `neetru-rehearse-${randomBytes(6).toString('hex')}`;
77
+ }
78
+ /**
79
+ * Aguarda o Postgres aceitar conexões.
80
+ * Retorna `true` se ficou pronto dentro do timeout, `false` caso contrário.
81
+ */
82
+ async function waitForPostgresReady(execDocker, containerName, timeoutMs) {
83
+ const deadline = Date.now() + timeoutMs;
84
+ while (Date.now() < deadline) {
85
+ const result = await execDocker([
86
+ 'exec', containerName,
87
+ 'pg_isready',
88
+ '--username', PG_ENV.user,
89
+ '--dbname', PG_ENV.db,
90
+ '--quiet',
91
+ ]);
92
+ if (result.exit === 0)
93
+ return true;
94
+ // Aguarda antes do próximo poll.
95
+ await new Promise((res) => setTimeout(res, READY_POLL_INTERVAL_MS));
96
+ }
97
+ return false;
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // buildRehearseInContainer — fábrica principal
101
+ // ---------------------------------------------------------------------------
102
+ /**
103
+ * Constrói a função `rehearseInContainer` com a camada Docker injetada.
104
+ *
105
+ * Produção:
106
+ * ```ts
107
+ * const rehearse = buildRehearseInContainer(buildDockerExecFn());
108
+ * // deps: { rehearseInContainer: rehearse, ... }
109
+ * ```
110
+ *
111
+ * Testes: injeta um mock de `execDocker` que controla cada resposta.
112
+ */
113
+ export function buildRehearseInContainer(execDocker, opts = {}) {
114
+ const image = opts.image ?? DEFAULT_IMAGE;
115
+ const readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
116
+ const skipRehearse = opts.skipRehearse ?? false;
117
+ return async function rehearseInContainer(sql) {
118
+ // ── Override explícito (--skip-rehearse) ─────────────────────────────
119
+ // O caller já logou este bypass antes de invocar — não é silencioso.
120
+ if (skipRehearse) {
121
+ return { ok: true };
122
+ }
123
+ // ── 1. Verificar disponibilidade do Docker ────────────────────────────
124
+ const versionResult = await execDocker(['version', '--format', '{{.Server.Version}}']);
125
+ if (versionResult.exit !== 0) {
126
+ throw new RehearseDockerUnavailableError('Docker CLI não está disponível ou o daemon não está rodando. ' +
127
+ 'Instale o Docker e certifique-se de que o daemon está ativo, ou use ' +
128
+ '`--skip-rehearse` como override explícito (isso deve ser logado). ' +
129
+ `Detalhe: ${versionResult.stdout.trim() || '(sem saída de docker version)'}`);
130
+ }
131
+ // ── 2. Subir container efêmero ────────────────────────────────────────
132
+ const containerName = ephemeralContainerName();
133
+ const runResult = await execDocker([
134
+ 'run', '--detach', '--rm',
135
+ '--name', containerName,
136
+ '-e', `POSTGRES_USER=${PG_ENV.user}`,
137
+ '-e', `POSTGRES_PASSWORD=${PG_ENV.password}`,
138
+ '-e', `POSTGRES_DB=${PG_ENV.db}`,
139
+ image,
140
+ ]);
141
+ if (runResult.exit !== 0) {
142
+ throw new RehearseDockerUnavailableError(`Não foi possível iniciar o container efêmero "${containerName}" ` +
143
+ `(imagem: ${image}). Saída: ${runResult.stdout.trim() || '(sem saída)'}`);
144
+ }
145
+ // ── 3. Aguardar pronto + aplicar SQL + derrubar ───────────────────────
146
+ try {
147
+ const ready = await waitForPostgresReady(execDocker, containerName, readyTimeoutMs);
148
+ if (!ready) {
149
+ return {
150
+ ok: false,
151
+ error: `Timeout (${readyTimeoutMs}ms) aguardando o Postgres ficar pronto ` +
152
+ `no container "${containerName}". ` +
153
+ `Verifique se a imagem "${image}" está disponível localmente.`,
154
+ };
155
+ }
156
+ // Aplica o SQL candidato. `--single-transaction` garante rollback total
157
+ // em caso de erro parcial — o container fica limpo para o teardown.
158
+ const applyResult = await execDocker([
159
+ 'exec', '--interactive', containerName,
160
+ 'psql',
161
+ '--username', PG_ENV.user,
162
+ '--dbname', PG_ENV.db,
163
+ '--no-psqlrc',
164
+ '--single-transaction',
165
+ '--command', sql,
166
+ ]);
167
+ if (applyResult.exit !== 0) {
168
+ // SQL inválido — retorna o erro (não lança). O pipeline trata como
169
+ // `PipelineError('rehearse')`.
170
+ return {
171
+ ok: false,
172
+ error: applyResult.stdout.trim() || '(psql não retornou mensagem de erro)',
173
+ };
174
+ }
175
+ return { ok: true };
176
+ }
177
+ finally {
178
+ // Derruba o container sempre — o `--rm` no run ajuda, mas o `stop`
179
+ // explícito garante para daemons que ainda não processaram o --rm.
180
+ await execDocker(['stop', containerName]).catch(() => {
181
+ // Ignora erros de teardown: o container pode já ter sido removido
182
+ // automaticamente pelo `--rm`. O erro da aplicação do SQL já foi
183
+ // capturado (ou retornado) acima.
184
+ });
185
+ }
186
+ };
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // buildDockerExecFn — fiação de produção
190
+ // ---------------------------------------------------------------------------
191
+ /**
192
+ * Constrói a função `execDocker` de produção via `child_process.spawn`.
193
+ *
194
+ * Separada do `buildRehearseInContainer` para que:
195
+ * (a) o módulo seja importável sem side effects em testes;
196
+ * (b) o `spawn` seja injetável em camada superior quando necessário.
197
+ */
198
+ export function buildDockerExecFn() {
199
+ return function execDocker(args) {
200
+ return new Promise((resolve) => {
201
+ const child = spawn('docker', args, {
202
+ shell: process.platform === 'win32',
203
+ });
204
+ let stdout = '';
205
+ let stderr = '';
206
+ child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
207
+ child.stderr?.on('data', (chunk) => { stderr += String(chunk); });
208
+ child.on('error', (_err) => {
209
+ // Docker não encontrado no PATH: resolve com exit=1 + mensagem.
210
+ resolve({ stdout: stderr || stdout, exit: 1 });
211
+ });
212
+ child.on('exit', (code) => {
213
+ // Combina stdout + stderr: psql escreve erros em stderr.
214
+ resolve({ stdout: stdout + stderr, exit: code ?? 1 });
215
+ });
216
+ });
217
+ };
218
+ }
219
+ //# sourceMappingURL=rehearse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rehearse.js","sourceRoot":"","sources":["../../../src/lib/db-pipeline/rehearse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `@neetru/db-pipeline` — contratos do orquestrador do `neetru db apply`.
3
+ *
4
+ * O pipeline roda CLIENT-SIDE as fases 1-5 do `neetru db apply`:
5
+ * 1. `fingerprint` — hash determinístico do schema.
6
+ * 2. `generate` — `drizzle-kit generate` produz o `.sql`.
7
+ * 3. `rehearse` — ensaia o `.sql` num Postgres efêmero real.
8
+ * 4. `classify` — `@neetru/db-classifier` marca aditiva/destrutiva.
9
+ * 5. `push` — adquire lock + envia o artefato validado pro Core.
10
+ *
11
+ * O pipeline NÃO decide política — ele produz um `.sql` validado e
12
+ * classificado e o envia. A decisão de aplicar (ou exigir confirmação numa
13
+ * migração destrutiva) é do Core.
14
+ *
15
+ * Tudo aqui é deps-injetado: o orquestrador é unit-testável sem Docker, sem
16
+ * Postgres e sem rede. A fiação real (drizzle-kit / container / HTTP) vive
17
+ * no `buildPipelineDeps()`.
18
+ */
19
+ /** As 5 fases do pipeline, na ordem de execução. */
20
+ export type PipelinePhase = 'fingerprint' | 'generate' | 'rehearse' | 'classify' | 'push';
21
+ /** Severidade global da migração — espelha o `@neetru/db-classifier`. */
22
+ export type MigrationSeverity = 'aditiva' | 'destrutiva';
23
+ /** Os 3 ambientes canônicos do `neetru db apply`. */
24
+ export type ApplyEnv = 'dev-local' | 'staging' | 'production';
25
+ /**
26
+ * Relatório de classificação — subconjunto estrutural do `ClassificationReport`
27
+ * do `@neetru/db-classifier`. Tipado localmente pra que o orquestrador e seus
28
+ * testes não dependam do shape interno do classifier.
29
+ */
30
+ export interface ClassificationResult {
31
+ /** `destrutiva` se QUALQUER statement for destrutivo; senão `aditiva`. */
32
+ overallSeverity: MigrationSeverity;
33
+ /** `true` sse `overallSeverity === 'destrutiva'` — dispara a pausa sagrada. */
34
+ requiresConfirmation: boolean;
35
+ /** Classificação por statement, na ordem do arquivo. */
36
+ statements: unknown[];
37
+ }
38
+ /** Resultado de uma ensaiada num Postgres efêmero. */
39
+ export interface RehearsalResult {
40
+ /** `true` sse o `.sql` rodou inteiro sem erro no container. */
41
+ ok: boolean;
42
+ /** Mensagem do erro de SQL quando `ok` é `false`. */
43
+ error?: string;
44
+ }
45
+ /** O artefato validado que o pipeline empurra pro Core. */
46
+ export interface ApplyArtifact {
47
+ /** ID do banco no Core (resolvido de `.neetru/db.json` → `ids[env]`). */
48
+ dbId: string;
49
+ /** O `.sql` gerado pelo `drizzle-kit generate`. */
50
+ sql: string;
51
+ /** Fingerprint determinístico do schema que originou o `.sql`. */
52
+ fingerprint: string;
53
+ /** Relatório de classificação aditiva/destrutiva. */
54
+ classification: unknown;
55
+ /** Ambiente alvo da migração. */
56
+ env: string;
57
+ }
58
+ /** Resposta do Core ao receber o artefato. */
59
+ export interface PushResult {
60
+ /** ID da migração registrada no Core. */
61
+ migrationId: string;
62
+ /** Status da migração no Core (`queued`, `applied`, …). */
63
+ status: string;
64
+ }
65
+ /**
66
+ * Dependências injetadas no orquestrador. Cada função isola um efeito
67
+ * colateral (I/O, processo, container, rede) pra que o core do pipeline seja
68
+ * testável de forma determinística.
69
+ */
70
+ export interface PipelineDeps {
71
+ /** Lê o arquivo de schema (ex.: `db/schema.ts`) e devolve o conteúdo. */
72
+ readSchemaFile: (path: string) => string;
73
+ /** Roda `drizzle-kit generate` e devolve o `.sql` gerado. */
74
+ generateSql: (schemaPath: string) => Promise<string>;
75
+ /** Ensaia o `.sql` num Postgres efêmero — roda o script inteiro. */
76
+ rehearseInContainer: (sql: string) => Promise<RehearsalResult>;
77
+ /** Classifica o `.sql` em aditiva/destrutiva (função pura, sem I/O). */
78
+ classify: (sql: string) => ClassificationResult;
79
+ /** Adquire o lock e empurra o artefato validado pro Core. */
80
+ pushToCore: (artifact: ApplyArtifact) => Promise<PushResult>;
81
+ /** Callback de progresso — chamado ANTES de cada fase começar. */
82
+ onPhase?: (phase: PipelinePhase) => void;
83
+ }
84
+ /** Entrada do `runApplyPipeline`. */
85
+ export interface PipelineInput {
86
+ /** Caminho do arquivo de schema do produto. */
87
+ schemaPath: string;
88
+ /** Ambiente alvo da migração. */
89
+ env: ApplyEnv;
90
+ /**
91
+ * ID do banco no Core — resolvido de `.neetru/db.json → ids[env]` pelo
92
+ * comando `runDbApply` antes de chamar o pipeline. Repassado diretamente
93
+ * ao artefato que vai pro Core (`POST /api/cli/v1/db/apply`).
94
+ */
95
+ dbId: string;
96
+ }
97
+ /** Resultado final do pipeline. */
98
+ export interface PipelineResult {
99
+ /** ID da migração no Core. `'(nenhuma)'` quando não houve mudança. */
100
+ migrationId: string;
101
+ /** Status reportado: status do Core, ou `'no_changes'` quando o `.sql` é vazio. */
102
+ status: string;
103
+ /** Severidade global da migração. `'aditiva'` quando não houve mudança. */
104
+ severity: MigrationSeverity;
105
+ /**
106
+ * Fingerprint SHA-256 do schema que originou esta migração.
107
+ * Gravado de volta em `.neetru/db.json` pelo `runDbApply` (H-7 fix) para
108
+ * que o gate de deploy possa detectar drift entre schema local e aplicado.
109
+ * `undefined` quando `status === 'no_changes'` (não houve migração).
110
+ */
111
+ fingerprint?: string;
112
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@neetru/db-pipeline` — contratos do orquestrador do `neetru db apply`.
3
+ *
4
+ * O pipeline roda CLIENT-SIDE as fases 1-5 do `neetru db apply`:
5
+ * 1. `fingerprint` — hash determinístico do schema.
6
+ * 2. `generate` — `drizzle-kit generate` produz o `.sql`.
7
+ * 3. `rehearse` — ensaia o `.sql` num Postgres efêmero real.
8
+ * 4. `classify` — `@neetru/db-classifier` marca aditiva/destrutiva.
9
+ * 5. `push` — adquire lock + envia o artefato validado pro Core.
10
+ *
11
+ * O pipeline NÃO decide política — ele produz um `.sql` validado e
12
+ * classificado e o envia. A decisão de aplicar (ou exigir confirmação numa
13
+ * migração destrutiva) é do Core.
14
+ *
15
+ * Tudo aqui é deps-injetado: o orquestrador é unit-testável sem Docker, sem
16
+ * Postgres e sem rede. A fiação real (drizzle-kit / container / HTTP) vive
17
+ * no `buildPipelineDeps()`.
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/db-pipeline/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}