@neetru/cli 2.8.0 → 2.9.3

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 (44) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/commands/admin/sdk-credential.d.ts +19 -0
  3. package/dist/commands/admin/sdk-credential.js +169 -0
  4. package/dist/commands/admin/sdk-credential.js.map +1 -0
  5. package/dist/commands/bug.d.ts +87 -0
  6. package/dist/commands/bug.js +419 -0
  7. package/dist/commands/bug.js.map +1 -0
  8. package/dist/commands/customers.d.ts +17 -0
  9. package/dist/commands/customers.js +160 -0
  10. package/dist/commands/customers.js.map +1 -0
  11. package/dist/commands/db.d.ts +4 -0
  12. package/dist/commands/db.js +380 -64
  13. package/dist/commands/db.js.map +1 -1
  14. package/dist/commands/docs.d.ts +4 -0
  15. package/dist/commands/docs.js +99 -7
  16. package/dist/commands/docs.js.map +1 -1
  17. package/dist/commands/doctor.js +4 -1
  18. package/dist/commands/doctor.js.map +1 -1
  19. package/dist/commands/marketplace.d.ts +36 -0
  20. package/dist/commands/marketplace.js +584 -0
  21. package/dist/commands/marketplace.js.map +1 -0
  22. package/dist/commands/new.js +196 -37
  23. package/dist/commands/new.js.map +1 -1
  24. package/dist/commands/open.d.ts +8 -0
  25. package/dist/commands/open.js +61 -13
  26. package/dist/commands/open.js.map +1 -1
  27. package/dist/commands/products.d.ts +23 -0
  28. package/dist/commands/products.js +39 -1
  29. package/dist/commands/products.js.map +1 -1
  30. package/dist/commands/tenants.js +24 -2
  31. package/dist/commands/tenants.js.map +1 -1
  32. package/dist/commands/ui.d.ts +1 -1
  33. package/dist/commands/ui.js +172 -2
  34. package/dist/commands/ui.js.map +1 -1
  35. package/dist/commands/workspaces.d.ts +10 -1
  36. package/dist/commands/workspaces.js +145 -23
  37. package/dist/commands/workspaces.js.map +1 -1
  38. package/dist/index.js +327 -4
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/config-schema.d.ts +10 -10
  41. package/dist/lib/pickers.d.ts +25 -0
  42. package/dist/lib/pickers.js +75 -1
  43. package/dist/lib/pickers.js.map +1 -1
  44. package/package.json +5 -2
@@ -46,6 +46,14 @@ const ENGINES = [
46
46
  'vm-mysql-single',
47
47
  'vm-mysql-cluster',
48
48
  ];
49
+ /**
50
+ * Engines NoSQL — schemaless por natureza.
51
+ * Nesses engines o `neetru db apply` NÃO roda drizzle-kit nem Postgres efêmero:
52
+ * registra apenas um snapshot de fingerprint no Core (Fase 1 — sem JSON Schema export).
53
+ *
54
+ * Fase 2 (export de JSON Schema estruturado) é backlog — ticket pdv-agiliza #11.
55
+ */
56
+ const NOSQL_ENGINES = ['nosql-vm', 'firestore-instance'];
49
57
  /** Labels amigáveis por engine pra exibição no prompt. */
50
58
  const ENGINE_LABELS = {
51
59
  'firestore-instance': 'Firestore (instância isolada)',
@@ -147,7 +155,8 @@ export async function runDbList(opts = {}) {
147
155
  { header: 'Produto', value: (r) => r.productId, maxWidth: 20 },
148
156
  { header: 'Label', value: (r) => r.label ?? '—', maxWidth: 24 },
149
157
  { header: 'Engine', value: (r) => r.engine, maxWidth: 22 },
150
- { header: 'Env', value: (r) => r.env, maxWidth: 12 },
158
+ // gestovendas #11 fix: Core retorna `environment`; `env` é alias legado.
159
+ { header: 'Env', value: (r) => r.environment ?? r.env ?? '—', maxWidth: 12 },
151
160
  { header: 'Status', value: (r) => statusColor(r.status), maxWidth: 16 },
152
161
  { header: 'Criado', value: (r) => fmtTimestamp(r.createdAt), maxWidth: 18 },
153
162
  ], resp.databases);
@@ -180,7 +189,7 @@ export async function runDbStatus(dbId, opts = {}) {
180
189
  log.dim(` ID: ${db.id}`);
181
190
  log.dim(` Produto: ${db.productId}`);
182
191
  log.dim(` Engine: ${db.engine}`);
183
- log.dim(` Env: ${db.env}`);
192
+ log.dim(` Env: ${db.environment ?? db.env ?? '—'}`);
184
193
  log.dim(` Status: ${statusColor(db.status)}`);
185
194
  if (db.region)
186
195
  log.dim(` Região: ${db.region}`);
@@ -198,8 +207,49 @@ export async function runDbStatus(dbId, opts = {}) {
198
207
  log.dim(` Criado: ${fmtTimestamp(db.createdAt)}`);
199
208
  log.dim(` Atualizado: ${fmtTimestamp(db.updatedAt)}`);
200
209
  }
201
- /** Conteúdo do `db/schema.ts` gerado durante o init. */
202
- const SCHEMA_STARTER = (productId, engine) => `/**
210
+ // pdv #7 fix: template diferenciado por família de engine.
211
+ /** Template para engines NoSQL (firestore-instance). */
212
+ const SCHEMA_STARTER_NOSQL = (productId, engine) => `/**
213
+ * Schema do banco "${productId}" — ${ENGINE_LABELS[engine] ?? engine}.
214
+ *
215
+ * Gerado por \`neetru db init\`.
216
+ * Edite este arquivo com as collections do seu produto.
217
+ * Rode \`neetru db apply\` pra enviar a definição ao Core.
218
+ *
219
+ * Para bancos NoSQL (Firestore), o schema define a estrutura esperada
220
+ * dos documentos. O Core usa essa definição para validação e auditoria —
221
+ * o banco em si é schemaless (primeira escrita cria a estrutura).
222
+ *
223
+ * Documentação: https://core.neetru.com/docs/dev/db/nosql-schema
224
+ */
225
+
226
+ // Exemplo de collection com tipagem TypeScript pura:
227
+
228
+ export interface Produto {
229
+ id: string;
230
+ nome: string;
231
+ preco: number;
232
+ ativo: boolean;
233
+ criadoEm: Date;
234
+ }
235
+
236
+ export interface Pedido {
237
+ id: string;
238
+ produtoId: string;
239
+ quantidade: number;
240
+ total: number;
241
+ status: 'pendente' | 'pago' | 'cancelado';
242
+ criadoEm: Date;
243
+ }
244
+
245
+ // Exporte as collections que o SDK vai usar:
246
+ // import { createNeetruClient } from '@neetru/sdk';
247
+ // const neetru = createNeetruClient({ ... });
248
+ // const produtos = neetru.db.collection<Produto>('produtos');
249
+ // const pedidos = neetru.db.collection<Pedido>('pedidos');
250
+ `;
251
+ /** Template padrão para engines SQL (PostgreSQL/MySQL). */
252
+ const SCHEMA_STARTER_SQL = (productId, engine) => `/**
203
253
  * Schema do banco "${productId}" — ${ENGINE_LABELS[engine] ?? engine}.
204
254
  *
205
255
  * Gerado por \`neetru db init\`.
@@ -218,24 +268,46 @@ const SCHEMA_STARTER = (productId, engine) => `/**
218
268
  // Remova o comentário acima e adapte ao seu schema.
219
269
  export {};
220
270
  `;
271
+ /** Conteúdo do `db/schema.ts` gerado durante o init. */
272
+ const SCHEMA_STARTER = (productId, engine) => {
273
+ // Engines NoSQL recebem template diferente.
274
+ if (engine === 'firestore-instance') {
275
+ return SCHEMA_STARTER_NOSQL(productId, engine);
276
+ }
277
+ return SCHEMA_STARTER_SQL(productId, engine);
278
+ };
279
+ // gestovendas #9: helper para saída JSON uniforme em db init / db apply.
280
+ function jsonError(message, error = 'error') {
281
+ console.log(JSON.stringify({ ok: false, error, message }));
282
+ }
221
283
  export async function runDbInit(opts = {}) {
222
- log.banner();
223
- log.heading('neetru db init');
284
+ if (!opts.json) {
285
+ log.banner();
286
+ log.heading('neetru db init');
287
+ }
224
288
  // ── D-3: opções legadas removidas (Sprint 10 — schema.manifest.json) ────
225
289
  // `--out` e `--force` eram usados para gerar schema.manifest.json.
226
290
  // O branch legado foi cortado. Passar essas flags agora é um erro.
227
291
  if (opts.out !== undefined || opts.force !== undefined) {
228
- log.error('As opções --out e --force foram removidas (D-3).\n' +
229
- 'O `neetru db init` agora faz UMA coisa: registra o banco no Core + ' +
230
- 'grava .neetru/db.json + scaffolda db/schema.ts.\n' +
231
- 'Para inicializar o banco, use:\n' +
232
- ' neetru db init --name <nome> --engine <engine> --env dev-local');
292
+ const msg = 'As opções --out e --force foram removidas (D-3). Use: neetru db init --name <nome> --engine <engine> --env dev-local';
293
+ if (opts.json) {
294
+ jsonError(msg, 'invalid_option');
295
+ }
296
+ else {
297
+ log.error(msg);
298
+ }
233
299
  process.exit(1);
234
300
  return;
235
301
  }
236
302
  const cfg = await loadProductConfig();
237
303
  if (!cfg) {
238
- log.error('neetru.config.json não encontrado. Rode `neetru init` primeiro.');
304
+ const msg = 'neetru.config.json não encontrado. Rode `neetru init` primeiro.';
305
+ if (opts.json) {
306
+ jsonError(msg, 'config_not_found');
307
+ }
308
+ else {
309
+ log.error(msg);
310
+ }
239
311
  process.exit(1);
240
312
  return;
241
313
  }
@@ -269,13 +341,34 @@ export async function runDbInit(opts = {}) {
269
341
  dbName = dbJson.databases.length === 0 ? 'principal' : `db${dbJson.databases.length + 1}`;
270
342
  }
271
343
  if (dbJson.databases.some((d) => d.name === dbName)) {
272
- log.error(`Banco "${dbName}" já registrado. Use outro nome ou edite .neetru/db.json.`);
344
+ const msg = `Banco "${dbName}" já registrado. Use outro nome ou edite .neetru/db.json.`;
345
+ if (opts.json) {
346
+ jsonError(msg, 'duplicate_name');
347
+ }
348
+ else {
349
+ log.error(msg);
350
+ }
273
351
  process.exit(2);
274
352
  return;
275
353
  }
276
354
  // ── engine ───────────────────────────────────────────────────────────────
355
+ // pdv #4 fix: quando --engine é passado mas não reconhecido, abortar com
356
+ // mensagem clara em vez de ignorar silenciosamente e usar o default.
277
357
  let engine;
278
- if (opts.engine && ENGINES.includes(opts.engine)) {
358
+ if (opts.engine) {
359
+ if (!ENGINES.includes(opts.engine)) {
360
+ const msg = `Engine inválido: "${opts.engine}". Engines disponíveis: ${ENGINES.join(', ')}\n` +
361
+ `Nota: o campo "engine" no SDK (rest | firestore | nosql-vm) é o TRANSPORTE da conexão, ` +
362
+ `diferente do engine de ARMAZENAMENTO passado aqui para o plano de controle.`;
363
+ if (opts.json) {
364
+ jsonError(msg, 'invalid_engine');
365
+ }
366
+ else {
367
+ log.error(msg);
368
+ }
369
+ process.exit(1);
370
+ return;
371
+ }
279
372
  engine = opts.engine;
280
373
  }
281
374
  else if (opts.yes) {
@@ -306,16 +399,12 @@ export async function runDbInit(opts = {}) {
306
399
  process.exit(1);
307
400
  return;
308
401
  }
309
- // ── grava .neetru/db.json (stub inicial ids preenchidos depois do register)
310
- const stubEntry = { name: dbName, productId, engine, ids: {} };
311
- const withStub = {
312
- ...dbJson,
313
- databases: [...dbJson.databases, stubEntry],
314
- };
315
- writeDbJson(dbJsonPath, withStub);
316
- log.success(`Banco "${dbName}" registrado localmente em ${chalk.bold(DB_JSON_REL)}`);
317
- // ── BL-6 fix: registra o banco no Core e persiste os IDs por ambiente ────
318
- const registerSpinner = ora({ text: 'Registrando banco no Core…', color: 'blue' }).start();
402
+ // ── HIGH-3: registra o banco no Core ANTES de gravar o stub local.
403
+ // Gravar localmente antes e deixar o registro falhar resultava em entrada
404
+ // órfã em .neetru/db.json — re-rodar db init adicionava banco duplicado.
405
+ const registerSpinner = opts.json
406
+ ? null
407
+ : ora({ text: 'Registrando banco no Core…', color: 'blue' }).start();
319
408
  let registerResp;
320
409
  try {
321
410
  registerResp = await apiRequest('/api/cli/v1/db/register', {
@@ -327,12 +416,20 @@ export async function runDbInit(opts = {}) {
327
416
  provisioning: { type: engine.startsWith('vm-') ? 'vm' : 'managed' },
328
417
  },
329
418
  });
330
- registerSpinner.succeed('Banco registrado no Core.');
419
+ registerSpinner?.succeed('Banco registrado no Core.');
331
420
  }
332
421
  catch (err) {
333
- registerSpinner.fail('Falha ao registrar banco no Core.');
422
+ registerSpinner?.fail('Falha ao registrar banco no Core.');
423
+ if (opts.json) {
424
+ jsonError(err.message ?? String(err), 'register_failed');
425
+ process.exit(4);
426
+ }
334
427
  handleApiError(err);
428
+ return; // handleApiError nunca retorna mas TS não sabe disso
335
429
  }
430
+ // ── Grava .neetru/db.json somente após sucesso do Core ───────────────────
431
+ if (!opts.json)
432
+ log.success(`Banco "${dbName}" registrado localmente em ${chalk.bold(DB_JSON_REL)}`);
336
433
  // Persiste os IDs retornados pelo Core em `.neetru/db.json`.
337
434
  const ids = {
338
435
  'dev-local': registerResp.ids.devLocal,
@@ -342,32 +439,121 @@ export async function runDbInit(opts = {}) {
342
439
  const finalEntry = { name: dbName, productId, engine, ids };
343
440
  const withIds = {
344
441
  version: 2,
345
- databases: [...withStub.databases.slice(0, -1), finalEntry],
442
+ databases: [...dbJson.databases, finalEntry],
346
443
  };
347
444
  writeDbJson(dbJsonPath, withIds);
348
- log.success(`IDs de ambiente gravados em ${chalk.bold(DB_JSON_REL)}`);
445
+ if (!opts.json)
446
+ log.success(`IDs de ambiente gravados em ${chalk.bold(DB_JSON_REL)}`);
349
447
  // ── scaffolda db/schema.ts se não existir ─────────────────────────────────
350
448
  const schemaPath = path.resolve(cwd, DEFAULT_SCHEMA_PATH);
449
+ let schemaCreated = false;
351
450
  if (!fsSync.existsSync(schemaPath)) {
352
451
  await fs.mkdir(path.dirname(schemaPath), { recursive: true });
353
452
  await fs.writeFile(schemaPath, SCHEMA_STARTER(productId, engine), 'utf8');
354
- log.success(`Schema gerado em ${chalk.bold(DEFAULT_SCHEMA_PATH)}`);
453
+ schemaCreated = true;
454
+ if (!opts.json)
455
+ log.success(`Schema gerado em ${chalk.bold(DEFAULT_SCHEMA_PATH)}`);
355
456
  }
356
457
  else {
357
- log.dim(` Schema existente em ${DEFAULT_SCHEMA_PATH} — não sobrescrito.`);
458
+ if (!opts.json)
459
+ log.dim(` Schema existente em ${DEFAULT_SCHEMA_PATH} — não sobrescrito.`);
460
+ }
461
+ // gestovendas #9: saída JSON uniforme.
462
+ if (opts.json) {
463
+ console.log(JSON.stringify({
464
+ ok: true,
465
+ name: dbName,
466
+ productId,
467
+ engine,
468
+ env,
469
+ ids: {
470
+ devLocal: registerResp.ids.devLocal,
471
+ staging: registerResp.ids.staging,
472
+ production: registerResp.ids.production,
473
+ },
474
+ schemaPath: DEFAULT_SCHEMA_PATH,
475
+ schemaCreated,
476
+ }));
477
+ return;
358
478
  }
359
479
  log.dim('');
360
480
  log.dim('Próximos passos:');
361
- log.dim(` 1. Edite ${DEFAULT_SCHEMA_PATH} com as tabelas do seu produto.`);
362
- log.dim(` 2. Execute \`neetru db apply\` pra enviar a migração ao Core.`);
363
- log.dim(` 3. O Core irá provisionar o banco ${chalk.bold(engine)} em ${chalk.bold(env)}.`);
481
+ log.dim(` 1. Edite ${DEFAULT_SCHEMA_PATH} com as estruturas do seu produto.`);
482
+ if (engine === 'firestore-instance') {
483
+ // pdv-agiliza #11 (Bug 11 Fase 1): branch NoSQL agora implementada no db apply.
484
+ log.dim(` 2. Para Firestore, o banco é schemaless — não há migração SQL.`);
485
+ log.dim(` O \`neetru db apply\` registra o fingerprint do schema no Core.`);
486
+ log.dim(` Use o SDK para criar collections diretamente:`);
487
+ log.dim(` neetru.db.collection<Produto>('produtos')`);
488
+ log.dim(` 3. O Core irá provisionar a instância ${chalk.bold(engine)} em ${chalk.bold(env)}.`);
489
+ }
490
+ else {
491
+ log.dim(` 2. Execute \`neetru db apply\` pra enviar a migração ao Core.`);
492
+ log.dim(` 3. O Core irá provisionar o banco ${chalk.bold(engine)} em ${chalk.bold(env)}.`);
493
+ }
494
+ }
495
+ /**
496
+ * Pipeline NoSQL — sem drizzle-kit, sem Postgres efêmero.
497
+ *
498
+ * Engines NoSQL (`nosql-vm`, `firestore-instance`) são schemaless — não há SQL
499
+ * pra gerar nem rehearsear. O apply registra apenas o fingerprint do schema.ts
500
+ * no Core, que persiste em `product_db_schema_snapshots/{id}`.
501
+ *
502
+ * Fase 1 (implementada): snapshot de fingerprint sem export de JSON Schema.
503
+ * Fase 2 (backlog pdv-agiliza #11): export de JSON Schema estruturado + validação de coleções.
504
+ */
505
+ async function runNoSqlApply(input) {
506
+ const { dbId, productId, engine, schemaPath, env, dryRun } = input;
507
+ // Fase 1: calcular fingerprint do schema.ts
508
+ const { schemaFingerprint } = await import('../lib/db-local/index.js');
509
+ const { default: fsDefault } = await import('node:fs');
510
+ const schemaContent = fsDefault.readFileSync(schemaPath, 'utf8');
511
+ const fingerprint = schemaFingerprint(schemaContent);
512
+ if (dryRun) {
513
+ return {
514
+ ok: true,
515
+ status: 'dry_run',
516
+ engine,
517
+ snapshotId: null,
518
+ fingerprint,
519
+ dryRun: true,
520
+ };
521
+ }
522
+ const resp = await apiRequest('/api/cli/v1/db/apply', {
523
+ method: 'POST',
524
+ body: {
525
+ dbId,
526
+ productId,
527
+ engine,
528
+ fingerprint,
529
+ env,
530
+ // Fase 1 — JSON Schema export ainda não implementado (Fase 2 / pdv #11).
531
+ schemaDefinition: null,
532
+ },
533
+ });
534
+ return {
535
+ ok: true,
536
+ status: resp.status ?? 'applied',
537
+ engine,
538
+ snapshotId: resp.snapshotId ?? null,
539
+ fingerprint,
540
+ dryRun: false,
541
+ };
364
542
  }
365
543
  export async function runDbApply(opts = {}) {
366
- log.banner();
367
- log.heading('neetru db apply');
544
+ if (!opts.json) {
545
+ log.banner();
546
+ log.heading('neetru db apply');
547
+ }
368
548
  const cfg = await loadProductConfig();
369
549
  if (!cfg) {
370
- log.error('neetru.config.json não encontrado. Rode `neetru init` primeiro.');
550
+ const msg = 'neetru.config.json não encontrado. Rode `neetru init` primeiro.';
551
+ if (opts.json) {
552
+ jsonError(msg, 'config_not_found');
553
+ }
554
+ else {
555
+ log.error(msg);
556
+ }
371
557
  process.exit(1);
372
558
  return;
373
559
  }
@@ -375,7 +561,13 @@ export async function runDbApply(opts = {}) {
375
561
  const dbJsonPath = path.resolve(cwd, DB_JSON_REL);
376
562
  const dbJson = readDbJson(dbJsonPath);
377
563
  if (dbJson.databases.length === 0) {
378
- log.error('Nenhum banco registrado em .neetru/db.json. Rode `neetru db init` primeiro.');
564
+ const msg = 'Nenhum banco registrado em .neetru/db.json. Rode `neetru db init` primeiro.';
565
+ if (opts.json) {
566
+ jsonError(msg, 'no_databases');
567
+ }
568
+ else {
569
+ log.error(msg);
570
+ }
379
571
  process.exit(1);
380
572
  return;
381
573
  }
@@ -384,14 +576,39 @@ export async function runDbApply(opts = {}) {
384
576
  if (opts.dbName) {
385
577
  const found = dbJson.databases.find((d) => d.name === opts.dbName);
386
578
  if (!found) {
387
- log.error(`Banco "${opts.dbName}" não encontrado em .neetru/db.json.`);
388
- log.dim(` Bancos disponíveis: ${dbJson.databases.map((d) => d.name).join(', ')}`);
579
+ const msg = `Banco "${opts.dbName}" não encontrado em .neetru/db.json. Disponíveis: ${dbJson.databases.map((d) => d.name).join(', ')}`;
580
+ if (opts.json) {
581
+ jsonError(msg, 'db_not_found');
582
+ }
583
+ else {
584
+ log.error(msg);
585
+ }
389
586
  process.exit(1);
390
587
  return;
391
588
  }
392
589
  dbEntry = found;
393
590
  }
394
- else if (dbJson.databases.length > 1 && !opts.yes) {
591
+ else if (dbJson.databases.length > 1 && (opts.json || opts.yes)) {
592
+ // HIGH-1: modo não-interativo (--json ou --yes) com múltiplos bancos e sem
593
+ // --db explícito é ambíguo — aplicar no primeiro silenciosamente pode causar
594
+ // migração no banco errado em CI. Emite erro estruturado ao invés de assumir.
595
+ const msg = 'Múltiplos bancos em .neetru/db.json. Use --db <nome>.';
596
+ if (opts.json) {
597
+ console.log(JSON.stringify({
598
+ ok: false,
599
+ error: 'db_required',
600
+ message: msg,
601
+ available: dbJson.databases.map((d) => d.name),
602
+ }));
603
+ }
604
+ else {
605
+ log.error(msg);
606
+ log.dim(` Bancos disponíveis: ${dbJson.databases.map((d) => d.name).join(', ')}`);
607
+ }
608
+ process.exit(1);
609
+ return;
610
+ }
611
+ else if (dbJson.databases.length > 1 && !opts.yes && !opts.json) {
395
612
  const { chosen } = await inquirer.prompt([
396
613
  {
397
614
  type: 'list',
@@ -417,7 +634,13 @@ export async function runDbApply(opts = {}) {
417
634
  env = normalizeEnv(opts.env ?? 'dev-local');
418
635
  }
419
636
  catch {
420
- log.error(`Ambiente inválido: "${opts.env}". Use dev-local, staging ou production.`);
637
+ const msg = `Ambiente inválido: "${opts.env}". Use dev-local, staging ou production.`;
638
+ if (opts.json) {
639
+ jsonError(msg, 'invalid_env');
640
+ }
641
+ else {
642
+ log.error(msg);
643
+ }
421
644
  process.exit(1);
422
645
  return;
423
646
  }
@@ -425,7 +648,13 @@ export async function runDbApply(opts = {}) {
425
648
  const schemaFile = opts.schemaPath ?? DEFAULT_SCHEMA_PATH;
426
649
  const schemaAbs = path.resolve(cwd, schemaFile);
427
650
  if (!fsSync.existsSync(schemaAbs)) {
428
- log.error(`Schema não encontrado em ${schemaFile}. Rode \`neetru db init\` pra criar.`);
651
+ const msg = `Schema não encontrado em ${schemaFile}. Rode \`neetru db init\` pra criar.`;
652
+ if (opts.json) {
653
+ jsonError(msg, 'schema_not_found');
654
+ }
655
+ else {
656
+ log.error(msg);
657
+ }
429
658
  process.exit(1);
430
659
  return;
431
660
  }
@@ -434,18 +663,82 @@ export async function runDbApply(opts = {}) {
434
663
  const canonicalEnv = env;
435
664
  const dbId = dbEntry.ids[canonicalEnv];
436
665
  if (!dbId) {
437
- log.error(`Banco "${dbEntry.name}" não tem ID registrado para o ambiente "${env}". ` +
438
- `Execute \`neetru db init\` novamente ou verifique .neetru/db.json.`);
666
+ const msg = `Banco "${dbEntry.name}" não tem ID registrado para o ambiente "${env}". ` +
667
+ `Execute \`neetru db init\` novamente ou verifique .neetru/db.json.`;
668
+ if (opts.json) {
669
+ jsonError(msg, 'missing_db_id');
670
+ }
671
+ else {
672
+ log.error(msg);
673
+ }
439
674
  process.exit(1);
440
675
  return;
441
676
  }
442
- log.dim(` Banco: ${dbEntry.name} (${dbEntry.engine})`);
443
- log.dim(` Schema: ${schemaFile}`);
444
- log.dim(` Env: ${env}`);
445
- log.dim(` DB ID: ${dbId}`);
446
- if (opts.dryRun)
447
- log.warn(' Modo --dry-run: nenhuma alteração será enviada ao Core.');
448
- // Executa o pipeline.
677
+ if (!opts.json) {
678
+ log.dim(` Banco: ${dbEntry.name} (${dbEntry.engine})`);
679
+ log.dim(` Schema: ${schemaFile}`);
680
+ log.dim(` Env: ${env}`);
681
+ log.dim(` DB ID: ${dbId}`);
682
+ if (opts.dryRun)
683
+ log.warn(' Modo --dry-run: nenhuma alteração será enviada ao Core.');
684
+ }
685
+ // ── Branch NoSQL (pdv-agiliza #11 — Bug 11 Fase 1) ─────────────────────────
686
+ // Engines schemaless não passam por drizzle-kit nem Postgres efêmero.
687
+ // O Core registra o snapshot de fingerprint em `product_db_schema_snapshots/{id}`.
688
+ if (NOSQL_ENGINES.includes(dbEntry.engine)) {
689
+ const nosqlSpinner = opts.json
690
+ ? null
691
+ : ora({ text: 'Aplicando schema NoSQL (sem rehearse — engine schemaless)…', color: 'blue' }).start();
692
+ try {
693
+ const nosqlResult = await runNoSqlApply({
694
+ dbId,
695
+ productId: dbEntry.productId,
696
+ engine: dbEntry.engine,
697
+ schemaPath: schemaAbs,
698
+ env: canonicalEnv,
699
+ dryRun: opts.dryRun,
700
+ });
701
+ nosqlSpinner?.stop();
702
+ // H-7 compat: persiste fingerprint pós-apply NoSQL também.
703
+ if (!opts.dryRun && nosqlResult.fingerprint) {
704
+ const dbJsonAfter = readDbJson(dbJsonPath);
705
+ const updatedDatabases = dbJsonAfter.databases.map((d) => d.name === dbEntry.name
706
+ ? { ...d, lastAppliedFingerprint: nosqlResult.fingerprint }
707
+ : d);
708
+ writeDbJson(dbJsonPath, { ...dbJsonAfter, databases: updatedDatabases });
709
+ if (!opts.json)
710
+ log.dim(` Fingerprint gravado em ${chalk.bold(DB_JSON_REL)}`);
711
+ }
712
+ if (opts.json) {
713
+ console.log(JSON.stringify({
714
+ ok: true,
715
+ status: nosqlResult.status,
716
+ engine: nosqlResult.engine,
717
+ snapshotId: nosqlResult.snapshotId,
718
+ fingerprint: nosqlResult.fingerprint,
719
+ dryRun: nosqlResult.dryRun,
720
+ }));
721
+ return;
722
+ }
723
+ if (nosqlResult.status === 'dry_run') {
724
+ log.warn(' Dry-run concluído — nenhum snapshot gravado no Core.');
725
+ }
726
+ else {
727
+ log.success(`Schema NoSQL registrado no Core${nosqlResult.snapshotId ? ` — snapshot ${chalk.bold(nosqlResult.snapshotId)}` : ''}.`);
728
+ }
729
+ return;
730
+ }
731
+ catch (err) {
732
+ nosqlSpinner?.fail('Apply NoSQL falhou.');
733
+ if (opts.json) {
734
+ jsonError(err.message ?? String(err), 'nosql_apply_failed');
735
+ process.exit(5);
736
+ }
737
+ handleApiError(err);
738
+ return;
739
+ }
740
+ }
741
+ // Executa o pipeline SQL.
449
742
  const phaseLabels = {
450
743
  fingerprint: 'Calculando fingerprint…',
451
744
  generate: 'Gerando SQL (drizzle-kit)…',
@@ -453,12 +746,13 @@ export async function runDbApply(opts = {}) {
453
746
  classify: 'Classificando migração…',
454
747
  push: 'Enviando ao Core…',
455
748
  };
456
- let spinner = ora({ text: 'Iniciando pipeline…', color: 'blue' }).start();
749
+ let spinner = opts.json ? null : ora({ text: 'Iniciando pipeline…', color: 'blue' }).start();
457
750
  let result;
458
751
  try {
459
752
  const deps = buildPipelineDeps({
460
753
  onPhase: (phase) => {
461
- spinner.text = phaseLabels[phase] ?? phase;
754
+ if (spinner)
755
+ spinner.text = phaseLabels[phase] ?? phase;
462
756
  },
463
757
  ...(opts.dryRun
464
758
  ? {
@@ -474,33 +768,55 @@ export async function runDbApply(opts = {}) {
474
768
  env: canonicalEnv,
475
769
  dbId,
476
770
  }, deps);
477
- spinner.stop();
771
+ spinner?.stop();
478
772
  }
479
773
  catch (err) {
480
- spinner.fail('Pipeline falhou.');
774
+ spinner?.fail('Pipeline falhou.');
481
775
  if (isPipelineError(err)) {
482
776
  const pe = err;
483
- log.error(`Fase "${pe.phase}": ${pe.message}`);
777
+ if (opts.json) {
778
+ jsonError(`Fase "${pe.phase}": ${pe.message}`, 'pipeline_failed');
779
+ }
780
+ else {
781
+ log.error(`Fase "${pe.phase}": ${pe.message}`);
782
+ }
484
783
  process.exit(5);
485
784
  return;
486
785
  }
487
- log.error(err.message ?? String(err));
786
+ if (opts.json) {
787
+ jsonError(err.message ?? String(err), 'pipeline_failed');
788
+ }
789
+ else {
790
+ log.error(err.message ?? String(err));
791
+ }
488
792
  process.exit(5);
489
793
  return;
490
794
  }
491
- if (result.status === 'no_changes') {
492
- log.success('Schema sem alterações — nenhuma migração necessária.');
493
- return;
494
- }
495
- // H-7 fix: persiste o fingerprint após push bem-sucedido (não dry-run) para
496
- // que o gate de deploy (`checkSchemaGate` em deploy.ts) consiga detectar drift.
795
+ // H-7 fix: persiste o fingerprint após push bem-sucedido (não dry-run).
497
796
  if (!opts.dryRun && result.fingerprint) {
498
797
  const dbJsonAfter = readDbJson(dbJsonPath);
499
798
  const updatedDatabases = dbJsonAfter.databases.map((d) => d.name === dbEntry.name
500
799
  ? { ...d, lastAppliedFingerprint: result.fingerprint }
501
800
  : d);
502
801
  writeDbJson(dbJsonPath, { ...dbJsonAfter, databases: updatedDatabases });
503
- log.dim(` Fingerprint gravado em ${chalk.bold(DB_JSON_REL)}`);
802
+ if (!opts.json)
803
+ log.dim(` Fingerprint gravado em ${chalk.bold(DB_JSON_REL)}`);
804
+ }
805
+ // gestovendas #9: saída JSON uniforme no caminho de sucesso.
806
+ if (opts.json) {
807
+ console.log(JSON.stringify({
808
+ ok: true,
809
+ status: result.status,
810
+ migrationId: result.migrationId ?? null,
811
+ severity: result.severity ?? null,
812
+ fingerprint: result.fingerprint ?? null,
813
+ dryRun: !!opts.dryRun,
814
+ }));
815
+ return;
816
+ }
817
+ if (result.status === 'no_changes') {
818
+ log.success('Schema sem alterações — nenhuma migração necessária.');
819
+ return;
504
820
  }
505
821
  log.dim('');
506
822
  log.dim(` Severidade: ${severityColor(result.severity)}`);