@neetru/cli 2.7.2 → 2.7.4

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.
@@ -9,11 +9,13 @@ import { resolveSessionEmail } from '../lib/auth.js';
9
9
  import { checkForUpdate } from '../lib/version-check.js';
10
10
  import { CLI_VERSION } from '../version.js';
11
11
  import { log } from '../utils/logger.js';
12
- export const MENU_OUTLINE_COLOR = '#001F3F';
12
+ export const MENU_OUTLINE_COLOR = '#334C65';
13
+ export const MENU_FOOTER_TEXT_COLOR = '#CBD5E1';
13
14
  const MENU_COLUMNS = 2;
14
15
  const MENU_GAP = 2;
15
16
  const MENU_MIN_WIDTH = 58;
16
17
  const MENU_MAX_WIDTH = 112;
18
+ const DOCS_RECENT_LIMIT = 5;
17
19
  /**
18
20
  * Erro lançado no lugar de `process.exit()` enquanto o menu interativo está
19
21
  * ativo. Os comandos do CLI chamam `process.exit()` em erro (ex.: "Nenhum
@@ -40,9 +42,9 @@ export const MAIN_MENU = [
40
42
  description: 'Projeto no diretório atual, login, versão e config',
41
43
  },
42
44
  {
43
- label: 'Criar produto + workspace',
45
+ label: 'Começar um projeto',
44
46
  value: 'quickstart',
45
- description: 'Fluxo guiado: novo produto, workspace e scaffold local',
47
+ description: 'Scaffold local (init), criar produto (new) e sobre o SDK',
46
48
  },
47
49
  {
48
50
  label: 'Produtos',
@@ -80,7 +82,12 @@ export const MAIN_MENU = [
80
82
  description: 'Tickets de suporte — listar, responder, mudar status',
81
83
  },
82
84
  {
83
- label: 'Status e diagnóstico',
85
+ label: 'Docs live',
86
+ value: 'docs',
87
+ description: 'Registry publicado no Core - listar e ler markdown',
88
+ },
89
+ {
90
+ label: 'Status e diagnostico',
84
91
  value: 'health',
85
92
  description: 'Saúde das superfícies, doctor e whoami',
86
93
  },
@@ -124,6 +131,39 @@ function padMenuText(text, width) {
124
131
  function menuBorder(text) {
125
132
  return chalk.hex(MENU_OUTLINE_COLOR)(text);
126
133
  }
134
+ function menuFooterText(text) {
135
+ return chalk.hex(MENU_FOOTER_TEXT_COLOR)(text);
136
+ }
137
+ function renderProgressBar(progress, width = 10) {
138
+ const safeProgress = Math.max(0, Math.min(100, Math.round(progress)));
139
+ const filled = Math.round((safeProgress / 100) * width);
140
+ return `${menuBorder('[')}${chalk.hex('#1E90FF')('#'.repeat(filled))}${chalk.dim('-'.repeat(width - filled))}${menuBorder(']')} ${safeProgress}%`;
141
+ }
142
+ function renderTaskFooter(tasks, width) {
143
+ if (!tasks || tasks.length === 0) {
144
+ return menuFooterText(padMenuText('tasks sem tarefas ativas', width));
145
+ }
146
+ const task = tasks[0];
147
+ const remaining = tasks.length > 1 ? ` +${tasks.length - 1}` : '';
148
+ return menuFooterText(padMenuText(`task ${task.label}${remaining} `, Math.max(0, width - 18))) + renderProgressBar(task.progress);
149
+ }
150
+ function renderFooterColumns(left, right, width) {
151
+ const gap = ' ';
152
+ const leftWidth = Math.max(20, Math.floor((width - gap.length) * 0.45));
153
+ const rightWidth = Math.max(10, width - leftWidth - gap.length);
154
+ return `${menuFooterText(padMenuText(left, leftWidth))}${gap}${menuFooterText(padMenuText(right, rightWidth))}`;
155
+ }
156
+ function renderMenuFooter(footer, width) {
157
+ const session = footer.sessionEmail
158
+ ? `logado como ${footer.sessionEmail}`
159
+ : 'sem login - neetru login';
160
+ const taskWidth = Math.max(18, Math.floor(width * 0.52));
161
+ return [
162
+ ` ${menuBorder('─'.repeat(width))}`,
163
+ ` ${renderFooterColumns('Neetru CLI', `Developer Kit neetru.com v${CLI_VERSION}`, width)}`,
164
+ ` ${menuFooterText(padMenuText(session, Math.max(20, width - taskWidth - 2)))} ${renderTaskFooter(footer.tasks, taskWidth)}`,
165
+ ];
166
+ }
127
167
  function renderMenuCell(item, selected, width) {
128
168
  if (!item) {
129
169
  const empty = ' '.repeat(width);
@@ -166,7 +206,7 @@ export function moveMenuSelection(currentIndex, direction, totalItems, columns =
166
206
  }
167
207
  return current;
168
208
  }
169
- export function renderMainMenu(items, selectedIndex, terminalColumns = process.stdout.columns) {
209
+ export function renderMainMenu(items, selectedIndex, terminalColumns = process.stdout.columns, footer = {}) {
170
210
  const targetWidth = clampMenuWidth(terminalColumns);
171
211
  const columnWidth = Math.max(24, Math.floor((targetWidth - 2 - MENU_GAP) / MENU_COLUMNS));
172
212
  const innerWidth = columnWidth * MENU_COLUMNS + MENU_GAP;
@@ -182,8 +222,39 @@ export function renderMainMenu(items, selectedIndex, terminalColumns = process.s
182
222
  });
183
223
  lines.push(` ${menuBorder('│')}${cells.map((cell) => cell[0]).join(' '.repeat(MENU_GAP))}${menuBorder('│')}`);
184
224
  lines.push(` ${menuBorder('│')}${cells.map((cell) => cell[1]).join(' '.repeat(MENU_GAP))}${menuBorder('│')}`);
225
+ if (row < rows - 1) {
226
+ lines.push(` ${menuBorder('│')}${' '.repeat(innerWidth)}${menuBorder('│')}`);
227
+ }
185
228
  }
186
229
  lines.push(` ${menuBorder(`└${'─'.repeat(innerWidth)}┘`)}`);
230
+ lines.push(...renderMenuFooter(footer, innerWidth + 2));
231
+ return lines.join('\n');
232
+ }
233
+ /**
234
+ * Renderiza um submenu em coluna única, alinhado à esquerda, com a mesma
235
+ * moldura azul-marinho e o marcador `> <` do menu principal. A largura
236
+ * acompanha o item mais longo (clampada entre 18 e 48).
237
+ */
238
+ export function renderSubMenu(title, items, selectedIndex) {
239
+ const longest = items.reduce((max, item) => Math.max(max, item.label.length), 0);
240
+ const labelWidth = Math.max(18, Math.min(48, longest));
241
+ const innerWidth = labelWidth + 6;
242
+ const lines = [
243
+ ` ${chalk.bold.hex(MENU_OUTLINE_COLOR)(title)}`,
244
+ ` ${chalk.dim('↑/↓ navega · Enter abre · Esc volta')}`,
245
+ ` ${menuBorder(`┌${'─'.repeat(innerWidth)}┐`)}`,
246
+ ];
247
+ items.forEach((item, index) => {
248
+ const padded = padMenuText(item.label, labelWidth);
249
+ if (index === selectedIndex) {
250
+ const label = chalk.bgHex('#D7E9FF').hex(MENU_OUTLINE_COLOR).bold(padded);
251
+ lines.push(` ${menuBorder('│')} ${menuBorder('>')} ${label} ${menuBorder('<')} ${menuBorder('│')}`);
252
+ }
253
+ else {
254
+ lines.push(` ${menuBorder('│')} ${chalk.bold(padded)} ${menuBorder('│')}`);
255
+ }
256
+ });
257
+ lines.push(` ${menuBorder(`└${'─'.repeat(innerWidth)}┘`)}`);
187
258
  return lines.join('\n');
188
259
  }
189
260
  function decodeMenuKey(data) {
@@ -207,9 +278,50 @@ function readMenuKey() {
207
278
  process.stdin.once('data', (data) => resolve(decodeMenuKey(data)));
208
279
  });
209
280
  }
210
- async function promptMainMenu(items) {
281
+ function formatDocDate(value) {
282
+ if (!value)
283
+ return undefined;
284
+ const date = new Date(value);
285
+ if (Number.isNaN(date.getTime()))
286
+ return undefined;
287
+ return date.toISOString().slice(0, 10);
288
+ }
289
+ function docPublishedTime(doc) {
290
+ const time = Date.parse(doc.publishedAt ?? '');
291
+ return Number.isNaN(time) ? 0 : time;
292
+ }
293
+ async function loadDocsBannerInfo() {
294
+ const token = await resolveToken();
295
+ if (!token)
296
+ return { message: 'login para consultar registry' };
297
+ const ctrl = new AbortController();
298
+ const timer = setTimeout(() => ctrl.abort(), 1500);
299
+ try {
300
+ const result = await apiRequest('/api/cli/v1/docs', {
301
+ token,
302
+ signal: ctrl.signal,
303
+ });
304
+ const recent = [...(result.docs ?? [])]
305
+ .sort((a, b) => docPublishedTime(b) - docPublishedTime(a))
306
+ .slice(0, DOCS_RECENT_LIMIT)
307
+ .map((doc) => ({
308
+ title: doc.title || doc.slug,
309
+ slug: doc.slug,
310
+ updated: formatDocDate(doc.publishedAt),
311
+ }));
312
+ return { count: result.count ?? result.docs?.length ?? 0, recent };
313
+ }
314
+ catch {
315
+ return { message: 'registry indisponivel agora' };
316
+ }
317
+ finally {
318
+ clearTimeout(timer);
319
+ }
320
+ }
321
+ async function promptMainMenu(items, footer = {}) {
211
322
  let selectedIndex = 0;
212
323
  let renderedLines = 0;
324
+ let result = null;
213
325
  const wasRaw = process.stdin.isRaw === true;
214
326
  const clearRenderedMenu = () => {
215
327
  if (renderedLines > 0) {
@@ -223,15 +335,19 @@ async function promptMainMenu(items) {
223
335
  process.stdout.write('\x1B[?25l');
224
336
  try {
225
337
  for (;;) {
226
- const frame = renderMainMenu(items, selectedIndex);
338
+ const frame = renderMainMenu(items, selectedIndex, process.stdout.columns, footer);
227
339
  clearRenderedMenu();
228
340
  process.stdout.write(`${frame}\n`);
229
341
  renderedLines = frame.split('\n').length;
230
342
  const key = await readMenuKey();
231
- if (key === 'abort')
232
- return null;
233
- if (key === 'enter')
234
- return { action: items[selectedIndex].value };
343
+ if (key === 'abort') {
344
+ result = null;
345
+ break;
346
+ }
347
+ if (key === 'enter') {
348
+ result = { action: items[selectedIndex].value };
349
+ break;
350
+ }
235
351
  if (key)
236
352
  selectedIndex = moveMenuSelection(selectedIndex, key, items.length);
237
353
  }
@@ -239,10 +355,58 @@ async function promptMainMenu(items) {
239
355
  finally {
240
356
  clearRenderedMenu();
241
357
  process.stdout.write('\x1B[?25h');
242
- if (typeof process.stdin.setRawMode === 'function')
243
- process.stdin.setRawMode(wasRaw);
244
- if (!wasRaw)
245
- process.stdin.pause();
358
+ // restaura/pausa o stdin ao DEIXAR o menu (Ctrl+C ou "Sair"). Quando
359
+ // uma ação/submenu roda a seguir, o stdin fica "quente" (raw + resumed)
360
+ // pro inquirer do submenu assumir o stream sem comer a 1ª seta — o
361
+ // pause()/toggle de raw mode aqui é o que engolia a primeira tecla.
362
+ const leavingMenu = result === null || result.action === 'exit';
363
+ if (leavingMenu) {
364
+ if (typeof process.stdin.setRawMode === 'function') {
365
+ process.stdin.setRawMode(wasRaw);
366
+ }
367
+ if (!wasRaw)
368
+ process.stdin.pause();
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+ /**
374
+ * Submenu interativo em coluna única — espelha o `promptMainMenu`, mas com
375
+ * uma lista vertical estreita. Retorna o `value` do item escolhido; Esc ou
376
+ * Ctrl+C retornam `'back'`. Deixa o stdin "quente" na saída (a ação a seguir,
377
+ * ou o menu principal, assumem o stream sem comer a 1ª tecla).
378
+ */
379
+ async function promptSubMenu(title, items) {
380
+ let selectedIndex = 0;
381
+ let renderedLines = 0;
382
+ const clearRendered = () => {
383
+ if (renderedLines > 0) {
384
+ process.stdout.write(`\x1B[${renderedLines}A\x1B[J`);
385
+ renderedLines = 0;
386
+ }
387
+ };
388
+ process.stdin.resume();
389
+ if (typeof process.stdin.setRawMode === 'function')
390
+ process.stdin.setRawMode(true);
391
+ process.stdout.write('\x1B[?25l');
392
+ try {
393
+ for (;;) {
394
+ const frame = renderSubMenu(title, items, selectedIndex);
395
+ clearRendered();
396
+ process.stdout.write(`${frame}\n`);
397
+ renderedLines = frame.split('\n').length;
398
+ const key = await readMenuKey();
399
+ if (key === 'abort')
400
+ return 'back';
401
+ if (key === 'enter')
402
+ return items[selectedIndex].value;
403
+ if (key)
404
+ selectedIndex = moveMenuSelection(selectedIndex, key, items.length, 1);
405
+ }
406
+ }
407
+ finally {
408
+ clearRendered();
409
+ process.stdout.write('\x1B[?25h');
246
410
  }
247
411
  }
248
412
  function canPrompt() {
@@ -253,7 +417,7 @@ async function pause() {
253
417
  {
254
418
  type: 'input',
255
419
  name: 'continue',
256
- message: 'Enter para voltar ao menu',
420
+ message: 'Enter para continuar',
257
421
  },
258
422
  ]);
259
423
  }
@@ -267,13 +431,24 @@ async function safeRun(fn, opts = {}) {
267
431
  // `CommandExitError` = um comando chamou `process.exit()` e já imprimiu a
268
432
  // própria mensagem. Qualquer outro erro: mostramos a mensagem. Nos dois
269
433
  // casos pausamos (a função não chegou ao seu próprio `pause()`) e voltamos
270
- // ao menu — o CLI NUNCA encerra por causa de um comando.
434
+ // ao fluxo interativo — o CLI NUNCA encerra por causa de um comando.
271
435
  if (!(error instanceof CommandExitError)) {
272
436
  log.error(error instanceof Error ? error.message : String(error));
273
437
  }
274
438
  await pause();
275
439
  }
276
440
  }
441
+ async function runSubmenuAction(fn) {
442
+ try {
443
+ await fn();
444
+ }
445
+ catch (error) {
446
+ if (!(error instanceof CommandExitError)) {
447
+ log.error(error instanceof Error ? error.message : String(error));
448
+ }
449
+ }
450
+ await pause();
451
+ }
277
452
  async function readLocalProject(cwd) {
278
453
  for (const file of ['neetru.config.json', '.neetru.json']) {
279
454
  const full = path.join(cwd, file);
@@ -355,286 +530,340 @@ async function quickstart() {
355
530
  skipOpen: answers.skipOpen,
356
531
  });
357
532
  }
358
- async function productsMenu() {
359
- const answer = await inquirer.prompt([
360
- {
361
- type: 'list',
362
- name: 'action',
363
- message: 'Produtos',
364
- choices: [
365
- { name: 'Listar produtos', value: 'list' },
366
- { name: 'Criar produto', value: 'create' },
367
- { name: 'Publicar no catalogo', value: 'publish' },
368
- { name: 'Remover do catalogo', value: 'unpublish' },
369
- { name: 'Voltar', value: 'back' },
370
- ],
371
- },
533
+ /** Tela de info sobre o SDK Neetru (@neetru/sdk). */
534
+ function showSdkInfo() {
535
+ log.heading('SDK Neetru — @neetru/sdk');
536
+ console.log(` ${chalk.dim('O que é')} biblioteca que o SEU produto importa pra falar com o Core`);
537
+ console.log(` ${chalk.dim('Instala')} ${chalk.cyan('npm install @neetru/sdk')} (dependência do projeto)`);
538
+ console.log(` ${chalk.dim('No scaffold')} o ${chalk.cyan('neetru init')} já adiciona o @neetru/sdk + exemplos`);
539
+ console.log();
540
+ console.log(` ${chalk.dim('Uso no código:')}`);
541
+ console.log(chalk.cyan(" import { createNeetruClient } from '@neetru/sdk';"));
542
+ console.log(chalk.cyan(" const neetru = createNeetruClient({ apiKey, env: 'prod' });"));
543
+ console.log(chalk.cyan(' await neetru.auth.signIn();'));
544
+ console.log();
545
+ console.log(` ${chalk.dim('React:')} ${chalk.cyan('@neetru/sdk/react')} — <EntitlementGate>, <CheckoutLink>`);
546
+ }
547
+ /**
548
+ * Sub-menu "Começar um projeto": scaffold local (`neetru init`), a macro
549
+ * de criação completa (`neetru new`) e a info do SDK.
550
+ */
551
+ async function projectMenu() {
552
+ const action = await promptSubMenu('Começar um projeto', [
553
+ { label: 'Scaffold local — neetru init', value: 'init' },
554
+ { label: 'Criar produto + workspace — neetru new', value: 'new' },
555
+ { label: 'Sobre o SDK Neetru', value: 'sdk' },
556
+ { label: 'Voltar', value: 'back' },
372
557
  ]);
373
- if (answer.action === 'back')
558
+ if (action === 'back')
374
559
  return;
375
- const products = await import('./products.js');
376
- if (answer.action === 'list')
377
- await products.runProductsList({});
378
- if (answer.action === 'create')
379
- await products.runProductsCreate({});
380
- if (answer.action === 'publish')
381
- await products.runProductsPublish(undefined, {});
382
- if (answer.action === 'unpublish')
383
- await products.runProductsUnpublish(undefined, {});
560
+ if (action === 'init') {
561
+ const { name } = await inquirer.prompt([
562
+ {
563
+ type: 'input',
564
+ name: 'name',
565
+ message: 'Nome do projeto:',
566
+ validate: (v) => v.trim().length > 0 || 'Obrigatório.',
567
+ },
568
+ ]);
569
+ const { runInit } = await import('./init.js');
570
+ await runInit({ name, type: 'nextjs' });
571
+ }
572
+ if (action === 'new')
573
+ await quickstart();
574
+ if (action === 'sdk')
575
+ showSdkInfo();
384
576
  await pause();
385
577
  }
578
+ async function productsMenu() {
579
+ for (;;) {
580
+ const action = await promptSubMenu('Produtos', [
581
+ { label: 'Listar produtos', value: 'list' },
582
+ { label: 'Criar produto', value: 'create' },
583
+ { label: 'Publicar no catálogo', value: 'publish' },
584
+ { label: 'Remover do catálogo', value: 'unpublish' },
585
+ { label: 'Voltar', value: 'back' },
586
+ ]);
587
+ if (action === 'back')
588
+ return;
589
+ await runSubmenuAction(async () => {
590
+ const products = await import('./products.js');
591
+ if (action === 'list')
592
+ await products.runProductsList({});
593
+ if (action === 'create')
594
+ await products.runProductsCreate({});
595
+ if (action === 'publish')
596
+ await products.runProductsPublish(undefined, {});
597
+ if (action === 'unpublish')
598
+ await products.runProductsUnpublish(undefined, {});
599
+ });
600
+ }
601
+ }
386
602
  async function workspacesMenu() {
387
- const answer = await inquirer.prompt([
388
- {
389
- type: 'list',
390
- name: 'action',
391
- message: 'Workspaces',
392
- choices: [
393
- { name: 'Listar workspaces', value: 'list' },
394
- { name: 'Criar workspace', value: 'create' },
395
- { name: 'Ver detalhes', value: 'get' },
396
- { name: 'Abrir no painel', value: 'open' },
397
- { name: 'Promover bundle', value: 'advance' },
398
- { name: 'Voltar', value: 'back' },
399
- ],
400
- },
401
- ]);
402
- if (answer.action === 'back')
403
- return;
404
- const workspaces = await import('./workspaces.js');
405
- if (answer.action === 'list')
406
- await workspaces.runWorkspacesList({});
407
- if (answer.action === 'create')
408
- await workspaces.runWorkspacesCreate({});
409
- if (answer.action === 'get')
410
- await workspaces.runWorkspacesGet(undefined, {});
411
- if (answer.action === 'open')
412
- await workspaces.runWorkspacesOpen(undefined);
413
- if (answer.action === 'advance')
414
- await workspaces.runWorkspacesAdvance(undefined, {});
415
- await pause();
603
+ for (;;) {
604
+ const action = await promptSubMenu('Workspaces', [
605
+ { label: 'Listar workspaces', value: 'list' },
606
+ { label: 'Criar workspace', value: 'create' },
607
+ { label: 'Ver detalhes', value: 'get' },
608
+ { label: 'Abrir no painel', value: 'open' },
609
+ { label: 'Promover bundle', value: 'advance' },
610
+ { label: 'Voltar', value: 'back' },
611
+ ]);
612
+ if (action === 'back')
613
+ return;
614
+ await runSubmenuAction(async () => {
615
+ const workspaces = await import('./workspaces.js');
616
+ if (action === 'list')
617
+ await workspaces.runWorkspacesList({});
618
+ if (action === 'create')
619
+ await workspaces.runWorkspacesCreate({});
620
+ if (action === 'get')
621
+ await workspaces.runWorkspacesGet(undefined, {});
622
+ if (action === 'open')
623
+ await workspaces.runWorkspacesOpen(undefined);
624
+ if (action === 'advance')
625
+ await workspaces.runWorkspacesAdvance(undefined, {});
626
+ });
627
+ }
416
628
  }
417
629
  async function serversMenu() {
418
- const answer = await inquirer.prompt([
419
- {
420
- type: 'list',
421
- name: 'action',
422
- message: 'Servers',
423
- choices: [
424
- { name: 'Listar servers', value: 'list' },
425
- { name: 'Provisionar VM', value: 'provision' },
426
- { name: 'Despachar comando', value: 'dispatch' },
427
- { name: 'Desativar server', value: 'deactivate' },
428
- { name: 'Voltar', value: 'back' },
429
- ],
430
- },
431
- ]);
432
- if (answer.action === 'back')
433
- return;
434
- const servers = await import('./servers.js');
435
- if (answer.action === 'list')
436
- await servers.runServersList({ capacity: true });
437
- if (answer.action === 'provision')
438
- await servers.runServersProvision({});
439
- if (answer.action === 'dispatch')
440
- await servers.runServersDispatch(undefined, undefined, {});
441
- if (answer.action === 'deactivate')
442
- await servers.runServersDeactivate(undefined, {});
443
- await pause();
630
+ for (;;) {
631
+ const action = await promptSubMenu('Servers', [
632
+ { label: 'Listar servers', value: 'list' },
633
+ { label: 'Provisionar VM', value: 'provision' },
634
+ { label: 'Despachar comando', value: 'dispatch' },
635
+ { label: 'Desativar server', value: 'deactivate' },
636
+ { label: 'Voltar', value: 'back' },
637
+ ]);
638
+ if (action === 'back')
639
+ return;
640
+ await runSubmenuAction(async () => {
641
+ const servers = await import('./servers.js');
642
+ if (action === 'list')
643
+ await servers.runServersList({ capacity: true });
644
+ if (action === 'provision')
645
+ await servers.runServersProvision({});
646
+ if (action === 'dispatch')
647
+ await servers.runServersDispatch(undefined, undefined, {});
648
+ if (action === 'deactivate')
649
+ await servers.runServersDeactivate(undefined, {});
650
+ });
651
+ }
444
652
  }
445
653
  async function databasesMenu() {
446
- const answer = await inquirer.prompt([
447
- {
448
- type: 'list',
449
- name: 'action',
450
- message: 'Bancos de produto',
451
- choices: [
452
- { name: 'Listar bancos', value: 'list' },
453
- { name: 'Criar banco', value: 'create' },
454
- { name: 'Detalhar banco', value: 'get' },
455
- { name: 'Listar engines', value: 'engines' },
456
- { name: 'Retry provisionamento', value: 'retry' },
457
- { name: 'Rotacionar credenciais', value: 'rotate' },
458
- { name: 'Arquivar banco', value: 'delete' },
459
- { name: 'Voltar', value: 'back' },
460
- ],
461
- },
462
- ]);
463
- if (answer.action === 'back')
464
- return;
465
- const db = await import('./products-db.js');
466
- if (answer.action === 'list')
467
- await db.runDbList({});
468
- if (answer.action === 'create')
469
- await db.runDbCreate({ region: 'us-central1' });
470
- if (answer.action === 'get')
471
- await db.runDbGet(undefined, {});
472
- if (answer.action === 'engines')
473
- await db.runDbEngines({});
474
- if (answer.action === 'retry')
475
- await db.runDbRetry(undefined, {});
476
- if (answer.action === 'rotate')
477
- await db.runDbRotate(undefined, {});
478
- if (answer.action === 'delete')
479
- await db.runDbDelete(undefined, {});
480
- await pause();
654
+ for (;;) {
655
+ const action = await promptSubMenu('Bancos de produto', [
656
+ { label: 'Listar bancos', value: 'list' },
657
+ { label: 'Criar banco', value: 'create' },
658
+ { label: 'Detalhar banco', value: 'get' },
659
+ { label: 'Listar engines', value: 'engines' },
660
+ { label: 'Retry provisionamento', value: 'retry' },
661
+ { label: 'Rotacionar credenciais', value: 'rotate' },
662
+ { label: 'Arquivar banco', value: 'delete' },
663
+ { label: 'Voltar', value: 'back' },
664
+ ]);
665
+ if (action === 'back')
666
+ return;
667
+ await runSubmenuAction(async () => {
668
+ const db = await import('./products-db.js');
669
+ if (action === 'list')
670
+ await db.runDbList({});
671
+ if (action === 'create')
672
+ await db.runDbCreate({ region: 'us-central1' });
673
+ if (action === 'get')
674
+ await db.runDbGet(undefined, {});
675
+ if (action === 'engines')
676
+ await db.runDbEngines({});
677
+ if (action === 'retry')
678
+ await db.runDbRetry(undefined, {});
679
+ if (action === 'rotate')
680
+ await db.runDbRotate(undefined, {});
681
+ if (action === 'delete')
682
+ await db.runDbDelete(undefined, {});
683
+ });
684
+ }
481
685
  }
482
686
  async function tenantsMenu() {
483
- const answer = await inquirer.prompt([
484
- {
485
- type: 'list',
486
- name: 'action',
487
- message: 'Tenants',
488
- choices: [
489
- { name: 'Listar tenants', value: 'list' },
490
- { name: 'Detalhar tenant', value: 'get' },
491
- { name: 'Criar tenant', value: 'create' },
492
- { name: 'Atualizar tenant', value: 'update' },
493
- { name: 'Suspender tenant', value: 'suspend' },
494
- { name: 'Reativar tenant', value: 'reactivate' },
495
- { name: 'Voltar', value: 'back' },
496
- ],
497
- },
498
- ]);
499
- if (answer.action === 'back')
500
- return;
501
- const tenants = await import('./tenants.js');
502
- if (answer.action === 'list')
503
- await tenants.runTenantsList({});
504
- if (answer.action === 'get')
505
- await tenants.runTenantsGet(undefined, {});
506
- if (answer.action === 'create')
507
- await tenants.runTenantsCreate({});
508
- if (answer.action === 'update')
509
- await tenants.runTenantsUpdate(undefined, {});
510
- if (answer.action === 'suspend')
511
- await tenants.runTenantsSuspend(undefined, {});
512
- if (answer.action === 'reactivate')
513
- await tenants.runTenantsReactivate(undefined, {});
514
- await pause();
515
- }
516
- async function supportMenu() {
517
- const answer = await inquirer.prompt([
518
- {
519
- type: 'list',
520
- name: 'action',
521
- message: 'Suporte',
522
- choices: [
523
- { name: 'Listar tickets', value: 'list' },
524
- { name: 'Detalhar ticket', value: 'describe' },
525
- { name: 'Responder ticket', value: 'reply' },
526
- { name: 'Mudar status', value: 'status' },
527
- { name: 'Voltar', value: 'back' },
528
- ],
529
- },
530
- ]);
531
- if (answer.action === 'back')
532
- return;
533
- const support = await import('./support.js');
534
- if (answer.action === 'list')
535
- await support.runSupportTicketsList({});
536
- if (answer.action === 'describe') {
537
- const { id } = await inquirer.prompt([
538
- { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
687
+ for (;;) {
688
+ const action = await promptSubMenu('Tenants', [
689
+ { label: 'Listar tenants', value: 'list' },
690
+ { label: 'Detalhar tenant', value: 'get' },
691
+ { label: 'Criar tenant', value: 'create' },
692
+ { label: 'Atualizar tenant', value: 'update' },
693
+ { label: 'Suspender tenant', value: 'suspend' },
694
+ { label: 'Reativar tenant', value: 'reactivate' },
695
+ { label: 'Voltar', value: 'back' },
539
696
  ]);
540
- await support.runSupportTicketsDescribe(id, {});
697
+ if (action === 'back')
698
+ return;
699
+ await runSubmenuAction(async () => {
700
+ const tenants = await import('./tenants.js');
701
+ if (action === 'list')
702
+ await tenants.runTenantsList({});
703
+ if (action === 'get')
704
+ await tenants.runTenantsGet(undefined, {});
705
+ if (action === 'create')
706
+ await tenants.runTenantsCreate({});
707
+ if (action === 'update')
708
+ await tenants.runTenantsUpdate(undefined, {});
709
+ if (action === 'suspend')
710
+ await tenants.runTenantsSuspend(undefined, {});
711
+ if (action === 'reactivate')
712
+ await tenants.runTenantsReactivate(undefined, {});
713
+ });
541
714
  }
542
- if (answer.action === 'reply') {
543
- const { id, message } = await inquirer.prompt([
544
- { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
545
- { type: 'editor', name: 'message', message: 'Mensagem:' },
715
+ }
716
+ async function supportMenu() {
717
+ for (;;) {
718
+ const action = await promptSubMenu('Suporte', [
719
+ { label: 'Listar tickets', value: 'list' },
720
+ { label: 'Detalhar ticket', value: 'describe' },
721
+ { label: 'Responder ticket', value: 'reply' },
722
+ { label: 'Mudar status', value: 'status' },
723
+ { label: 'Voltar', value: 'back' },
546
724
  ]);
547
- await support.runSupportTicketsReply(id, { message });
725
+ if (action === 'back')
726
+ return;
727
+ await runSubmenuAction(async () => {
728
+ const support = await import('./support.js');
729
+ if (action === 'list')
730
+ await support.runSupportTicketsList({});
731
+ if (action === 'describe') {
732
+ const { id } = await inquirer.prompt([
733
+ { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
734
+ ]);
735
+ await support.runSupportTicketsDescribe(id, {});
736
+ }
737
+ if (action === 'reply') {
738
+ const { id, message } = await inquirer.prompt([
739
+ { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
740
+ { type: 'editor', name: 'message', message: 'Mensagem:' },
741
+ ]);
742
+ await support.runSupportTicketsReply(id, { message });
743
+ }
744
+ if (action === 'status') {
745
+ const { id, to } = await inquirer.prompt([
746
+ { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
747
+ {
748
+ type: 'list',
749
+ name: 'to',
750
+ message: 'Novo status:',
751
+ choices: ['open', 'in_progress', 'resolved', 'closed'],
752
+ },
753
+ ]);
754
+ await support.runSupportTicketsStatus(id, { to });
755
+ }
756
+ });
548
757
  }
549
- if (answer.action === 'status') {
550
- const { id, to } = await inquirer.prompt([
551
- { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
552
- {
553
- type: 'list',
554
- name: 'to',
555
- message: 'Novo status:',
556
- choices: ['open', 'in_progress', 'resolved', 'closed'],
557
- },
558
- ]);
559
- await support.runSupportTicketsStatus(id, { to });
758
+ }
759
+ export function buildDocsMenuChoices(docsInfo) {
760
+ const recent = docsInfo?.recent?.slice(0, DOCS_RECENT_LIMIT) ?? [];
761
+ const recentChoices = recent.map((doc) => ({
762
+ name: `Abrir ${chalk.bold(doc.title)}${doc.updated ? chalk.dim(` ${doc.updated}`) : ''}`,
763
+ value: `recent:${doc.slug}`,
764
+ short: doc.title,
765
+ }));
766
+ return [
767
+ ...recentChoices,
768
+ { name: 'Listar todos os docs publicados', value: 'list', short: 'Listar docs' },
769
+ { name: 'Buscar por slug', value: 'get', short: 'Buscar por slug' },
770
+ { name: 'Ler metadata por slug', value: 'meta', short: 'Ler metadata' },
771
+ { name: 'Voltar', value: 'back', short: 'Voltar' },
772
+ ];
773
+ }
774
+ async function docsMenu(docsInfo) {
775
+ for (;;) {
776
+ const items = buildDocsMenuChoices(docsInfo).map((choice) => ({
777
+ label: choice.short,
778
+ value: choice.value,
779
+ }));
780
+ const action = (await promptSubMenu('Docs live', items));
781
+ if (action === 'back')
782
+ return;
783
+ await runSubmenuAction(async () => {
784
+ const docs = await import('./docs.js');
785
+ if (action.startsWith('recent:')) {
786
+ await docs.runDocsGet(action.slice('recent:'.length), { metaOnly: false });
787
+ }
788
+ if (action === 'list') {
789
+ await docs.runDocsList({});
790
+ }
791
+ if (action === 'get' || action === 'meta') {
792
+ const { slug } = await inquirer.prompt([
793
+ { type: 'input', name: 'slug', message: 'Slug do doc:', validate: Boolean },
794
+ ]);
795
+ await docs.runDocsGet(slug, { metaOnly: action === 'meta' });
796
+ }
797
+ });
560
798
  }
561
- await pause();
562
799
  }
563
800
  async function healthMenu() {
564
- const answer = await inquirer.prompt([
565
- {
566
- type: 'list',
567
- name: 'action',
568
- message: 'Status e diagnostico',
569
- choices: [
570
- { name: 'Status das superficies', value: 'status' },
571
- { name: 'Doctor', value: 'doctor' },
572
- { name: 'Whoami', value: 'whoami' },
573
- { name: 'Voltar', value: 'back' },
574
- ],
575
- },
576
- ]);
577
- if (answer.action === 'back')
578
- return;
579
- if (answer.action === 'status') {
580
- const { runSurfaceStatus } = await import('./surface-status.js');
581
- await runSurfaceStatus({});
582
- }
583
- if (answer.action === 'doctor') {
584
- const doctor = await import('./doctor.js');
585
- const checks = [
586
- await doctor.checkToken(),
587
- await doctor.checkCoreHealth(),
588
- await doctor.checkConfigSchema(process.cwd()),
589
- await doctor.checkNeetruEnv(process.cwd()),
590
- await doctor.checkCliVersion(),
591
- ];
592
- log.heading('Doctor');
593
- for (const check of checks) {
594
- const marker = check.ok ? chalk.green('*') : check.warning ? chalk.yellow('!') : chalk.red('x');
595
- console.log(` ${marker} ${check.name}${check.detail ? chalk.dim(` - ${check.detail}`) : ''}`);
596
- }
597
- }
598
- if (answer.action === 'whoami') {
599
- const { runWhoami } = await import('./whoami.js');
600
- await runWhoami({});
801
+ for (;;) {
802
+ const action = await promptSubMenu('Status e diagnóstico', [
803
+ { label: 'Status das superfícies', value: 'status' },
804
+ { label: 'Doctor', value: 'doctor' },
805
+ { label: 'Whoami', value: 'whoami' },
806
+ { label: 'Voltar', value: 'back' },
807
+ ]);
808
+ if (action === 'back')
809
+ return;
810
+ await runSubmenuAction(async () => {
811
+ if (action === 'status') {
812
+ const { runSurfaceStatus } = await import('./surface-status.js');
813
+ await runSurfaceStatus({});
814
+ }
815
+ if (action === 'doctor') {
816
+ const doctor = await import('./doctor.js');
817
+ const checks = [
818
+ await doctor.checkToken(),
819
+ await doctor.checkCoreHealth(),
820
+ await doctor.checkConfigSchema(process.cwd()),
821
+ await doctor.checkNeetruEnv(process.cwd()),
822
+ await doctor.checkCliVersion(),
823
+ ];
824
+ log.heading('Doctor');
825
+ for (const check of checks) {
826
+ const marker = check.ok ? chalk.green('*') : check.warning ? chalk.yellow('!') : chalk.red('x');
827
+ console.log(` ${marker} ${check.name}${check.detail ? chalk.dim(` - ${check.detail}`) : ''}`);
828
+ }
829
+ }
830
+ if (action === 'whoami') {
831
+ const { runWhoami } = await import('./whoami.js');
832
+ await runWhoami({});
833
+ }
834
+ });
601
835
  }
602
- await pause();
603
836
  }
604
837
  async function authMenu() {
605
- const answer = await inquirer.prompt([
606
- {
607
- type: 'list',
608
- name: 'action',
609
- message: 'Login e configuracao',
610
- choices: [
611
- { name: 'Login', value: 'login' },
612
- { name: 'Logout', value: 'logout' },
613
- { name: 'Ver config', value: 'config' },
614
- { name: 'Caminho da config', value: 'path' },
615
- { name: 'Voltar', value: 'back' },
616
- ],
617
- },
618
- ]);
619
- if (answer.action === 'back')
620
- return;
621
- if (answer.action === 'login') {
622
- const { runLogin } = await import('./login.js');
623
- await runLogin({});
624
- }
625
- if (answer.action === 'logout') {
626
- const { runLogout } = await import('./logout.js');
627
- await runLogout();
628
- }
629
- if (answer.action === 'config') {
630
- const { configGet } = await import('./config.js');
631
- configGet();
632
- }
633
- if (answer.action === 'path') {
634
- const { configPath } = await import('./config.js');
635
- configPath();
838
+ for (;;) {
839
+ const action = await promptSubMenu('Login e configuração', [
840
+ { label: 'Login', value: 'login' },
841
+ { label: 'Logout', value: 'logout' },
842
+ { label: 'Ver config', value: 'config' },
843
+ { label: 'Caminho da config', value: 'path' },
844
+ { label: 'Voltar', value: 'back' },
845
+ ]);
846
+ if (action === 'back')
847
+ return;
848
+ await runSubmenuAction(async () => {
849
+ if (action === 'login') {
850
+ const { runLogin } = await import('./login.js');
851
+ await runLogin({});
852
+ }
853
+ if (action === 'logout') {
854
+ const { runLogout } = await import('./logout.js');
855
+ await runLogout();
856
+ }
857
+ if (action === 'config') {
858
+ const { configGet } = await import('./config.js');
859
+ configGet();
860
+ }
861
+ if (action === 'path') {
862
+ const { configPath } = await import('./config.js');
863
+ configPath();
864
+ }
865
+ });
636
866
  }
637
- await pause();
638
867
  }
639
868
  export async function runTerminalUi() {
640
869
  if (!canPrompt()) {
@@ -650,16 +879,17 @@ export async function runTerminalUi() {
650
879
  throw new CommandExitError(typeof code === 'number' ? code : 0);
651
880
  });
652
881
  try {
653
- const [email, version] = await Promise.all([
882
+ const [email, version, docsInfo] = await Promise.all([
654
883
  resolveSessionEmail(),
655
884
  checkForUpdate(),
885
+ loadDocsBannerInfo(),
656
886
  ]);
657
- log.banner(email, version);
887
+ log.banner(email, version, docsInfo);
658
888
  let running = true;
659
889
  while (running) {
660
890
  let answer;
661
891
  try {
662
- answer = await promptMainMenu(MAIN_MENU);
892
+ answer = await promptMainMenu(MAIN_MENU, { sessionEmail: email });
663
893
  }
664
894
  catch {
665
895
  // Ctrl+C / fechamento do prompt = o usuário encerrando por vontade
@@ -678,7 +908,7 @@ export async function runTerminalUi() {
678
908
  if (answer.action === 'overview')
679
909
  await safeRun(showOverview);
680
910
  if (answer.action === 'quickstart')
681
- await safeRun(quickstart, { pauseAfter: false });
911
+ await safeRun(projectMenu, { pauseAfter: false });
682
912
  if (answer.action === 'products')
683
913
  await safeRun(productsMenu, { pauseAfter: false });
684
914
  if (answer.action === 'workspaces')
@@ -695,6 +925,8 @@ export async function runTerminalUi() {
695
925
  await safeRun(tenantsMenu, { pauseAfter: false });
696
926
  if (answer.action === 'support')
697
927
  await safeRun(supportMenu, { pauseAfter: false });
928
+ if (answer.action === 'docs')
929
+ await safeRun(() => docsMenu(docsInfo), { pauseAfter: false });
698
930
  if (answer.action === 'health')
699
931
  await safeRun(healthMenu, { pauseAfter: false });
700
932
  if (answer.action === 'auth')