@neetru/cli 2.7.1 → 2.7.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.
@@ -6,8 +6,16 @@ import { apiRequest } from '../lib/api-client.js';
6
6
  import { config } from '../lib/config.js';
7
7
  import { resolveToken } from '../lib/cli-read.js';
8
8
  import { resolveSessionEmail } from '../lib/auth.js';
9
+ import { checkForUpdate } from '../lib/version-check.js';
9
10
  import { CLI_VERSION } from '../version.js';
10
11
  import { log } from '../utils/logger.js';
12
+ export const MENU_OUTLINE_COLOR = '#334C65';
13
+ export const MENU_FOOTER_TEXT_COLOR = '#CBD5E1';
14
+ const MENU_COLUMNS = 2;
15
+ const MENU_GAP = 2;
16
+ const MENU_MIN_WIDTH = 58;
17
+ const MENU_MAX_WIDTH = 112;
18
+ const DOCS_RECENT_LIMIT = 5;
11
19
  /**
12
20
  * Erro lançado no lugar de `process.exit()` enquanto o menu interativo está
13
21
  * ativo. Os comandos do CLI chamam `process.exit()` em erro (ex.: "Nenhum
@@ -74,7 +82,12 @@ export const MAIN_MENU = [
74
82
  description: 'Tickets de suporte — listar, responder, mudar status',
75
83
  },
76
84
  {
77
- 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',
78
91
  value: 'health',
79
92
  description: 'Saúde das superfícies, doctor e whoami',
80
93
  },
@@ -99,6 +112,223 @@ export function buildMenuChoices(items) {
99
112
  short: item.label,
100
113
  }));
101
114
  }
115
+ function clampMenuWidth(columns) {
116
+ const terminalWidth = columns && columns > 0 ? columns : 96;
117
+ return Math.max(MENU_MIN_WIDTH, Math.min(MENU_MAX_WIDTH, terminalWidth - 4));
118
+ }
119
+ function truncateMenuText(text, width) {
120
+ if (text.length <= width)
121
+ return text;
122
+ if (width <= 0)
123
+ return '';
124
+ if (width <= 3)
125
+ return '.'.repeat(width);
126
+ return `${text.slice(0, width - 3)}...`;
127
+ }
128
+ function padMenuText(text, width) {
129
+ return truncateMenuText(text, width).padEnd(width);
130
+ }
131
+ function menuBorder(text) {
132
+ return chalk.hex(MENU_OUTLINE_COLOR)(text);
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
+ }
167
+ function renderMenuCell(item, selected, width) {
168
+ if (!item) {
169
+ const empty = ' '.repeat(width);
170
+ return [empty, empty];
171
+ }
172
+ if (selected) {
173
+ const labelWidth = width - 4;
174
+ const label = chalk.bgHex('#D7E9FF').hex(MENU_OUTLINE_COLOR).bold(padMenuText(item.label, labelWidth));
175
+ const detail = chalk.hex(MENU_OUTLINE_COLOR)(padMenuText(item.description ?? '', width - 2));
176
+ return [
177
+ `${menuBorder('>')} ${label} ${menuBorder('<')}`,
178
+ ` ${detail}`,
179
+ ];
180
+ }
181
+ return [
182
+ ` ${chalk.bold(padMenuText(item.label, width - 2))}`,
183
+ ` ${chalk.dim(padMenuText(item.description ?? '', width - 2))}`,
184
+ ];
185
+ }
186
+ export function moveMenuSelection(currentIndex, direction, totalItems, columns = MENU_COLUMNS) {
187
+ if (totalItems <= 0)
188
+ return 0;
189
+ const current = Math.max(0, Math.min(currentIndex, totalItems - 1));
190
+ const currentRow = Math.floor(current / columns);
191
+ const currentColumn = current % columns;
192
+ if (direction === 'left' || direction === 'right') {
193
+ const nextColumn = currentColumn + (direction === 'left' ? -1 : 1);
194
+ if (nextColumn < 0 || nextColumn >= columns)
195
+ return current;
196
+ const nextIndex = currentRow * columns + nextColumn;
197
+ return nextIndex < totalItems ? nextIndex : current;
198
+ }
199
+ const rowCount = Math.ceil(totalItems / columns);
200
+ const delta = direction === 'up' ? -1 : 1;
201
+ for (let step = 1; step <= rowCount; step += 1) {
202
+ const nextRow = (currentRow + delta * step + rowCount) % rowCount;
203
+ const nextIndex = nextRow * columns + currentColumn;
204
+ if (nextIndex < totalItems)
205
+ return nextIndex;
206
+ }
207
+ return current;
208
+ }
209
+ export function renderMainMenu(items, selectedIndex, terminalColumns = process.stdout.columns, footer = {}) {
210
+ const targetWidth = clampMenuWidth(terminalColumns);
211
+ const columnWidth = Math.max(24, Math.floor((targetWidth - 2 - MENU_GAP) / MENU_COLUMNS));
212
+ const innerWidth = columnWidth * MENU_COLUMNS + MENU_GAP;
213
+ const rows = Math.ceil(items.length / MENU_COLUMNS);
214
+ const lines = [
215
+ ` ${chalk.dim('Use ↑/↓/←/→. Enter abre. Ativo: > <.')}`,
216
+ ` ${menuBorder(`┌${'─'.repeat(innerWidth)}┐`)}`,
217
+ ];
218
+ for (let row = 0; row < rows; row += 1) {
219
+ const cells = Array.from({ length: MENU_COLUMNS }, (_, column) => {
220
+ const index = row * MENU_COLUMNS + column;
221
+ return renderMenuCell(items[index], index === selectedIndex, columnWidth);
222
+ });
223
+ lines.push(` ${menuBorder('│')}${cells.map((cell) => cell[0]).join(' '.repeat(MENU_GAP))}${menuBorder('│')}`);
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
+ }
228
+ }
229
+ lines.push(` ${menuBorder(`└${'─'.repeat(innerWidth)}┘`)}`);
230
+ lines.push(...renderMenuFooter(footer, innerWidth + 2));
231
+ return lines.join('\n');
232
+ }
233
+ function decodeMenuKey(data) {
234
+ const key = data.toString('utf8');
235
+ if (key === '\r' || key === '\n')
236
+ return 'enter';
237
+ if (key === '\u0003' || key === '\x1B' || key.toLowerCase() === 'q')
238
+ return 'abort';
239
+ if (key === '\x1B[A' || key === '\x1BOA' || key.toLowerCase() === 'k' || key.toLowerCase() === 'w')
240
+ return 'up';
241
+ if (key === '\x1B[B' || key === '\x1BOB' || key.toLowerCase() === 'j' || key.toLowerCase() === 's')
242
+ return 'down';
243
+ if (key === '\x1B[D' || key === '\x1BOD' || key.toLowerCase() === 'h' || key.toLowerCase() === 'a')
244
+ return 'left';
245
+ if (key === '\x1B[C' || key === '\x1BOC' || key.toLowerCase() === 'l' || key.toLowerCase() === 'd')
246
+ return 'right';
247
+ return null;
248
+ }
249
+ function readMenuKey() {
250
+ return new Promise((resolve) => {
251
+ process.stdin.once('data', (data) => resolve(decodeMenuKey(data)));
252
+ });
253
+ }
254
+ function formatDocDate(value) {
255
+ if (!value)
256
+ return undefined;
257
+ const date = new Date(value);
258
+ if (Number.isNaN(date.getTime()))
259
+ return undefined;
260
+ return date.toISOString().slice(0, 10);
261
+ }
262
+ function docPublishedTime(doc) {
263
+ const time = Date.parse(doc.publishedAt ?? '');
264
+ return Number.isNaN(time) ? 0 : time;
265
+ }
266
+ async function loadDocsBannerInfo() {
267
+ const token = await resolveToken();
268
+ if (!token)
269
+ return { message: 'login para consultar registry' };
270
+ const ctrl = new AbortController();
271
+ const timer = setTimeout(() => ctrl.abort(), 1500);
272
+ try {
273
+ const result = await apiRequest('/api/cli/v1/docs', {
274
+ token,
275
+ signal: ctrl.signal,
276
+ });
277
+ const recent = [...(result.docs ?? [])]
278
+ .sort((a, b) => docPublishedTime(b) - docPublishedTime(a))
279
+ .slice(0, DOCS_RECENT_LIMIT)
280
+ .map((doc) => ({
281
+ title: doc.title || doc.slug,
282
+ slug: doc.slug,
283
+ updated: formatDocDate(doc.publishedAt),
284
+ }));
285
+ return { count: result.count ?? result.docs?.length ?? 0, recent };
286
+ }
287
+ catch {
288
+ return { message: 'registry indisponivel agora' };
289
+ }
290
+ finally {
291
+ clearTimeout(timer);
292
+ }
293
+ }
294
+ async function promptMainMenu(items, footer = {}) {
295
+ let selectedIndex = 0;
296
+ let renderedLines = 0;
297
+ const wasRaw = process.stdin.isRaw === true;
298
+ const clearRenderedMenu = () => {
299
+ if (renderedLines > 0) {
300
+ process.stdout.write(`\x1B[${renderedLines}A\x1B[J`);
301
+ renderedLines = 0;
302
+ }
303
+ };
304
+ process.stdin.resume();
305
+ if (typeof process.stdin.setRawMode === 'function')
306
+ process.stdin.setRawMode(true);
307
+ process.stdout.write('\x1B[?25l');
308
+ try {
309
+ for (;;) {
310
+ const frame = renderMainMenu(items, selectedIndex, process.stdout.columns, footer);
311
+ clearRenderedMenu();
312
+ process.stdout.write(`${frame}\n`);
313
+ renderedLines = frame.split('\n').length;
314
+ const key = await readMenuKey();
315
+ if (key === 'abort')
316
+ return null;
317
+ if (key === 'enter')
318
+ return { action: items[selectedIndex].value };
319
+ if (key)
320
+ selectedIndex = moveMenuSelection(selectedIndex, key, items.length);
321
+ }
322
+ }
323
+ finally {
324
+ clearRenderedMenu();
325
+ process.stdout.write('\x1B[?25h');
326
+ if (typeof process.stdin.setRawMode === 'function')
327
+ process.stdin.setRawMode(wasRaw);
328
+ if (!wasRaw)
329
+ process.stdin.pause();
330
+ }
331
+ }
102
332
  function canPrompt() {
103
333
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && process.env.CI !== 'true';
104
334
  }
@@ -107,7 +337,7 @@ async function pause() {
107
337
  {
108
338
  type: 'input',
109
339
  name: 'continue',
110
- message: 'Enter para voltar ao menu',
340
+ message: 'Enter para continuar',
111
341
  },
112
342
  ]);
113
343
  }
@@ -121,13 +351,24 @@ async function safeRun(fn, opts = {}) {
121
351
  // `CommandExitError` = um comando chamou `process.exit()` e já imprimiu a
122
352
  // própria mensagem. Qualquer outro erro: mostramos a mensagem. Nos dois
123
353
  // casos pausamos (a função não chegou ao seu próprio `pause()`) e voltamos
124
- // ao menu — o CLI NUNCA encerra por causa de um comando.
354
+ // ao fluxo interativo — o CLI NUNCA encerra por causa de um comando.
125
355
  if (!(error instanceof CommandExitError)) {
126
356
  log.error(error instanceof Error ? error.message : String(error));
127
357
  }
128
358
  await pause();
129
359
  }
130
360
  }
361
+ async function runSubmenuAction(fn) {
362
+ try {
363
+ await fn();
364
+ }
365
+ catch (error) {
366
+ if (!(error instanceof CommandExitError)) {
367
+ log.error(error instanceof Error ? error.message : String(error));
368
+ }
369
+ }
370
+ await pause();
371
+ }
131
372
  async function readLocalProject(cwd) {
132
373
  for (const file of ['neetru.config.json', '.neetru.json']) {
133
374
  const full = path.join(cwd, file);
@@ -210,285 +451,353 @@ async function quickstart() {
210
451
  });
211
452
  }
212
453
  async function productsMenu() {
213
- const answer = await inquirer.prompt([
214
- {
215
- type: 'list',
216
- name: 'action',
217
- message: 'Produtos',
218
- choices: [
219
- { name: 'Listar produtos', value: 'list' },
220
- { name: 'Criar produto', value: 'create' },
221
- { name: 'Publicar no catalogo', value: 'publish' },
222
- { name: 'Remover do catalogo', value: 'unpublish' },
223
- { name: 'Voltar', value: 'back' },
224
- ],
225
- },
226
- ]);
227
- if (answer.action === 'back')
228
- return;
229
- const products = await import('./products.js');
230
- if (answer.action === 'list')
231
- await products.runProductsList({});
232
- if (answer.action === 'create')
233
- await products.runProductsCreate({});
234
- if (answer.action === 'publish')
235
- await products.runProductsPublish(undefined, {});
236
- if (answer.action === 'unpublish')
237
- await products.runProductsUnpublish(undefined, {});
238
- await pause();
454
+ for (;;) {
455
+ const answer = await inquirer.prompt([
456
+ {
457
+ type: 'list',
458
+ name: 'action',
459
+ message: 'Produtos',
460
+ choices: [
461
+ { name: 'Listar produtos', value: 'list' },
462
+ { name: 'Criar produto', value: 'create' },
463
+ { name: 'Publicar no catalogo', value: 'publish' },
464
+ { name: 'Remover do catalogo', value: 'unpublish' },
465
+ { name: 'Voltar', value: 'back' },
466
+ ],
467
+ },
468
+ ]);
469
+ if (answer.action === 'back')
470
+ return;
471
+ await runSubmenuAction(async () => {
472
+ const products = await import('./products.js');
473
+ if (answer.action === 'list')
474
+ await products.runProductsList({});
475
+ if (answer.action === 'create')
476
+ await products.runProductsCreate({});
477
+ if (answer.action === 'publish')
478
+ await products.runProductsPublish(undefined, {});
479
+ if (answer.action === 'unpublish')
480
+ await products.runProductsUnpublish(undefined, {});
481
+ });
482
+ }
239
483
  }
240
484
  async function workspacesMenu() {
241
- const answer = await inquirer.prompt([
242
- {
243
- type: 'list',
244
- name: 'action',
245
- message: 'Workspaces',
246
- choices: [
247
- { name: 'Listar workspaces', value: 'list' },
248
- { name: 'Criar workspace', value: 'create' },
249
- { name: 'Ver detalhes', value: 'get' },
250
- { name: 'Abrir no painel', value: 'open' },
251
- { name: 'Promover bundle', value: 'advance' },
252
- { name: 'Voltar', value: 'back' },
253
- ],
254
- },
255
- ]);
256
- if (answer.action === 'back')
257
- return;
258
- const workspaces = await import('./workspaces.js');
259
- if (answer.action === 'list')
260
- await workspaces.runWorkspacesList({});
261
- if (answer.action === 'create')
262
- await workspaces.runWorkspacesCreate({});
263
- if (answer.action === 'get')
264
- await workspaces.runWorkspacesGet(undefined, {});
265
- if (answer.action === 'open')
266
- await workspaces.runWorkspacesOpen(undefined);
267
- if (answer.action === 'advance')
268
- await workspaces.runWorkspacesAdvance(undefined, {});
269
- await pause();
485
+ for (;;) {
486
+ const answer = await inquirer.prompt([
487
+ {
488
+ type: 'list',
489
+ name: 'action',
490
+ message: 'Workspaces',
491
+ choices: [
492
+ { name: 'Listar workspaces', value: 'list' },
493
+ { name: 'Criar workspace', value: 'create' },
494
+ { name: 'Ver detalhes', value: 'get' },
495
+ { name: 'Abrir no painel', value: 'open' },
496
+ { name: 'Promover bundle', value: 'advance' },
497
+ { name: 'Voltar', value: 'back' },
498
+ ],
499
+ },
500
+ ]);
501
+ if (answer.action === 'back')
502
+ return;
503
+ await runSubmenuAction(async () => {
504
+ const workspaces = await import('./workspaces.js');
505
+ if (answer.action === 'list')
506
+ await workspaces.runWorkspacesList({});
507
+ if (answer.action === 'create')
508
+ await workspaces.runWorkspacesCreate({});
509
+ if (answer.action === 'get')
510
+ await workspaces.runWorkspacesGet(undefined, {});
511
+ if (answer.action === 'open')
512
+ await workspaces.runWorkspacesOpen(undefined);
513
+ if (answer.action === 'advance')
514
+ await workspaces.runWorkspacesAdvance(undefined, {});
515
+ });
516
+ }
270
517
  }
271
518
  async function serversMenu() {
272
- const answer = await inquirer.prompt([
273
- {
274
- type: 'list',
275
- name: 'action',
276
- message: 'Servers',
277
- choices: [
278
- { name: 'Listar servers', value: 'list' },
279
- { name: 'Provisionar VM', value: 'provision' },
280
- { name: 'Despachar comando', value: 'dispatch' },
281
- { name: 'Desativar server', value: 'deactivate' },
282
- { name: 'Voltar', value: 'back' },
283
- ],
284
- },
285
- ]);
286
- if (answer.action === 'back')
287
- return;
288
- const servers = await import('./servers.js');
289
- if (answer.action === 'list')
290
- await servers.runServersList({ capacity: true });
291
- if (answer.action === 'provision')
292
- await servers.runServersProvision({});
293
- if (answer.action === 'dispatch')
294
- await servers.runServersDispatch(undefined, undefined, {});
295
- if (answer.action === 'deactivate')
296
- await servers.runServersDeactivate(undefined, {});
297
- await pause();
519
+ for (;;) {
520
+ const answer = await inquirer.prompt([
521
+ {
522
+ type: 'list',
523
+ name: 'action',
524
+ message: 'Servers',
525
+ choices: [
526
+ { name: 'Listar servers', value: 'list' },
527
+ { name: 'Provisionar VM', value: 'provision' },
528
+ { name: 'Despachar comando', value: 'dispatch' },
529
+ { name: 'Desativar server', value: 'deactivate' },
530
+ { name: 'Voltar', value: 'back' },
531
+ ],
532
+ },
533
+ ]);
534
+ if (answer.action === 'back')
535
+ return;
536
+ await runSubmenuAction(async () => {
537
+ const servers = await import('./servers.js');
538
+ if (answer.action === 'list')
539
+ await servers.runServersList({ capacity: true });
540
+ if (answer.action === 'provision')
541
+ await servers.runServersProvision({});
542
+ if (answer.action === 'dispatch')
543
+ await servers.runServersDispatch(undefined, undefined, {});
544
+ if (answer.action === 'deactivate')
545
+ await servers.runServersDeactivate(undefined, {});
546
+ });
547
+ }
298
548
  }
299
549
  async function databasesMenu() {
300
- const answer = await inquirer.prompt([
301
- {
302
- type: 'list',
303
- name: 'action',
304
- message: 'Bancos de produto',
305
- choices: [
306
- { name: 'Listar bancos', value: 'list' },
307
- { name: 'Criar banco', value: 'create' },
308
- { name: 'Detalhar banco', value: 'get' },
309
- { name: 'Listar engines', value: 'engines' },
310
- { name: 'Retry provisionamento', value: 'retry' },
311
- { name: 'Rotacionar credenciais', value: 'rotate' },
312
- { name: 'Arquivar banco', value: 'delete' },
313
- { name: 'Voltar', value: 'back' },
314
- ],
315
- },
316
- ]);
317
- if (answer.action === 'back')
318
- return;
319
- const db = await import('./products-db.js');
320
- if (answer.action === 'list')
321
- await db.runDbList({});
322
- if (answer.action === 'create')
323
- await db.runDbCreate({ region: 'us-central1' });
324
- if (answer.action === 'get')
325
- await db.runDbGet(undefined, {});
326
- if (answer.action === 'engines')
327
- await db.runDbEngines({});
328
- if (answer.action === 'retry')
329
- await db.runDbRetry(undefined, {});
330
- if (answer.action === 'rotate')
331
- await db.runDbRotate(undefined, {});
332
- if (answer.action === 'delete')
333
- await db.runDbDelete(undefined, {});
334
- await pause();
550
+ for (;;) {
551
+ const answer = await inquirer.prompt([
552
+ {
553
+ type: 'list',
554
+ name: 'action',
555
+ message: 'Bancos de produto',
556
+ choices: [
557
+ { name: 'Listar bancos', value: 'list' },
558
+ { name: 'Criar banco', value: 'create' },
559
+ { name: 'Detalhar banco', value: 'get' },
560
+ { name: 'Listar engines', value: 'engines' },
561
+ { name: 'Retry provisionamento', value: 'retry' },
562
+ { name: 'Rotacionar credenciais', value: 'rotate' },
563
+ { name: 'Arquivar banco', value: 'delete' },
564
+ { name: 'Voltar', value: 'back' },
565
+ ],
566
+ },
567
+ ]);
568
+ if (answer.action === 'back')
569
+ return;
570
+ await runSubmenuAction(async () => {
571
+ const db = await import('./products-db.js');
572
+ if (answer.action === 'list')
573
+ await db.runDbList({});
574
+ if (answer.action === 'create')
575
+ await db.runDbCreate({ region: 'us-central1' });
576
+ if (answer.action === 'get')
577
+ await db.runDbGet(undefined, {});
578
+ if (answer.action === 'engines')
579
+ await db.runDbEngines({});
580
+ if (answer.action === 'retry')
581
+ await db.runDbRetry(undefined, {});
582
+ if (answer.action === 'rotate')
583
+ await db.runDbRotate(undefined, {});
584
+ if (answer.action === 'delete')
585
+ await db.runDbDelete(undefined, {});
586
+ });
587
+ }
335
588
  }
336
589
  async function tenantsMenu() {
337
- const answer = await inquirer.prompt([
338
- {
339
- type: 'list',
340
- name: 'action',
341
- message: 'Tenants',
342
- choices: [
343
- { name: 'Listar tenants', value: 'list' },
344
- { name: 'Detalhar tenant', value: 'get' },
345
- { name: 'Criar tenant', value: 'create' },
346
- { name: 'Atualizar tenant', value: 'update' },
347
- { name: 'Suspender tenant', value: 'suspend' },
348
- { name: 'Reativar tenant', value: 'reactivate' },
349
- { name: 'Voltar', value: 'back' },
350
- ],
351
- },
352
- ]);
353
- if (answer.action === 'back')
354
- return;
355
- const tenants = await import('./tenants.js');
356
- if (answer.action === 'list')
357
- await tenants.runTenantsList({});
358
- if (answer.action === 'get')
359
- await tenants.runTenantsGet(undefined, {});
360
- if (answer.action === 'create')
361
- await tenants.runTenantsCreate({});
362
- if (answer.action === 'update')
363
- await tenants.runTenantsUpdate(undefined, {});
364
- if (answer.action === 'suspend')
365
- await tenants.runTenantsSuspend(undefined, {});
366
- if (answer.action === 'reactivate')
367
- await tenants.runTenantsReactivate(undefined, {});
368
- await pause();
369
- }
370
- async function supportMenu() {
371
- const answer = await inquirer.prompt([
372
- {
373
- type: 'list',
374
- name: 'action',
375
- message: 'Suporte',
376
- choices: [
377
- { name: 'Listar tickets', value: 'list' },
378
- { name: 'Detalhar ticket', value: 'describe' },
379
- { name: 'Responder ticket', value: 'reply' },
380
- { name: 'Mudar status', value: 'status' },
381
- { name: 'Voltar', value: 'back' },
382
- ],
383
- },
384
- ]);
385
- if (answer.action === 'back')
386
- return;
387
- const support = await import('./support.js');
388
- if (answer.action === 'list')
389
- await support.runSupportTicketsList({});
390
- if (answer.action === 'describe') {
391
- const { id } = await inquirer.prompt([
392
- { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
590
+ for (;;) {
591
+ const answer = await inquirer.prompt([
592
+ {
593
+ type: 'list',
594
+ name: 'action',
595
+ message: 'Tenants',
596
+ choices: [
597
+ { name: 'Listar tenants', value: 'list' },
598
+ { name: 'Detalhar tenant', value: 'get' },
599
+ { name: 'Criar tenant', value: 'create' },
600
+ { name: 'Atualizar tenant', value: 'update' },
601
+ { name: 'Suspender tenant', value: 'suspend' },
602
+ { name: 'Reativar tenant', value: 'reactivate' },
603
+ { name: 'Voltar', value: 'back' },
604
+ ],
605
+ },
393
606
  ]);
394
- await support.runSupportTicketsDescribe(id, {});
607
+ if (answer.action === 'back')
608
+ return;
609
+ await runSubmenuAction(async () => {
610
+ const tenants = await import('./tenants.js');
611
+ if (answer.action === 'list')
612
+ await tenants.runTenantsList({});
613
+ if (answer.action === 'get')
614
+ await tenants.runTenantsGet(undefined, {});
615
+ if (answer.action === 'create')
616
+ await tenants.runTenantsCreate({});
617
+ if (answer.action === 'update')
618
+ await tenants.runTenantsUpdate(undefined, {});
619
+ if (answer.action === 'suspend')
620
+ await tenants.runTenantsSuspend(undefined, {});
621
+ if (answer.action === 'reactivate')
622
+ await tenants.runTenantsReactivate(undefined, {});
623
+ });
395
624
  }
396
- if (answer.action === 'reply') {
397
- const { id, message } = await inquirer.prompt([
398
- { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
399
- { type: 'editor', name: 'message', message: 'Mensagem:' },
625
+ }
626
+ async function supportMenu() {
627
+ for (;;) {
628
+ const answer = await inquirer.prompt([
629
+ {
630
+ type: 'list',
631
+ name: 'action',
632
+ message: 'Suporte',
633
+ choices: [
634
+ { name: 'Listar tickets', value: 'list' },
635
+ { name: 'Detalhar ticket', value: 'describe' },
636
+ { name: 'Responder ticket', value: 'reply' },
637
+ { name: 'Mudar status', value: 'status' },
638
+ { name: 'Voltar', value: 'back' },
639
+ ],
640
+ },
400
641
  ]);
401
- await support.runSupportTicketsReply(id, { message });
642
+ if (answer.action === 'back')
643
+ return;
644
+ await runSubmenuAction(async () => {
645
+ const support = await import('./support.js');
646
+ if (answer.action === 'list')
647
+ await support.runSupportTicketsList({});
648
+ if (answer.action === 'describe') {
649
+ const { id } = await inquirer.prompt([
650
+ { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
651
+ ]);
652
+ await support.runSupportTicketsDescribe(id, {});
653
+ }
654
+ if (answer.action === 'reply') {
655
+ const { id, message } = await inquirer.prompt([
656
+ { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
657
+ { type: 'editor', name: 'message', message: 'Mensagem:' },
658
+ ]);
659
+ await support.runSupportTicketsReply(id, { message });
660
+ }
661
+ if (answer.action === 'status') {
662
+ const { id, to } = await inquirer.prompt([
663
+ { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
664
+ {
665
+ type: 'list',
666
+ name: 'to',
667
+ message: 'Novo status:',
668
+ choices: ['open', 'in_progress', 'resolved', 'closed'],
669
+ },
670
+ ]);
671
+ await support.runSupportTicketsStatus(id, { to });
672
+ }
673
+ });
402
674
  }
403
- if (answer.action === 'status') {
404
- const { id, to } = await inquirer.prompt([
405
- { type: 'input', name: 'id', message: 'Ticket ID:', validate: Boolean },
675
+ }
676
+ export function buildDocsMenuChoices(docsInfo) {
677
+ const recent = docsInfo?.recent?.slice(0, DOCS_RECENT_LIMIT) ?? [];
678
+ const recentChoices = recent.map((doc) => ({
679
+ name: `Abrir ${chalk.bold(doc.title)}${doc.updated ? chalk.dim(` ${doc.updated}`) : ''}`,
680
+ value: `recent:${doc.slug}`,
681
+ short: doc.title,
682
+ }));
683
+ return [
684
+ ...recentChoices,
685
+ { name: 'Listar todos os docs publicados', value: 'list', short: 'Listar docs' },
686
+ { name: 'Buscar por slug', value: 'get', short: 'Buscar por slug' },
687
+ { name: 'Ler metadata por slug', value: 'meta', short: 'Ler metadata' },
688
+ { name: 'Voltar', value: 'back', short: 'Voltar' },
689
+ ];
690
+ }
691
+ async function docsMenu(docsInfo) {
692
+ for (;;) {
693
+ const answer = await inquirer.prompt([
406
694
  {
407
695
  type: 'list',
408
- name: 'to',
409
- message: 'Novo status:',
410
- choices: ['open', 'in_progress', 'resolved', 'closed'],
696
+ name: 'action',
697
+ message: 'Docs live',
698
+ choices: buildDocsMenuChoices(docsInfo),
411
699
  },
412
700
  ]);
413
- await support.runSupportTicketsStatus(id, { to });
701
+ if (answer.action === 'back')
702
+ return;
703
+ await runSubmenuAction(async () => {
704
+ const docs = await import('./docs.js');
705
+ if (answer.action.startsWith('recent:')) {
706
+ await docs.runDocsGet(answer.action.slice('recent:'.length), { metaOnly: false });
707
+ }
708
+ if (answer.action === 'list') {
709
+ await docs.runDocsList({});
710
+ }
711
+ if (answer.action === 'get' || answer.action === 'meta') {
712
+ const { slug } = await inquirer.prompt([
713
+ { type: 'input', name: 'slug', message: 'Slug do doc:', validate: Boolean },
714
+ ]);
715
+ await docs.runDocsGet(slug, { metaOnly: answer.action === 'meta' });
716
+ }
717
+ });
414
718
  }
415
- await pause();
416
719
  }
417
720
  async function healthMenu() {
418
- const answer = await inquirer.prompt([
419
- {
420
- type: 'list',
421
- name: 'action',
422
- message: 'Status e diagnostico',
423
- choices: [
424
- { name: 'Status das superficies', value: 'status' },
425
- { name: 'Doctor', value: 'doctor' },
426
- { name: 'Whoami', value: 'whoami' },
427
- { name: 'Voltar', value: 'back' },
428
- ],
429
- },
430
- ]);
431
- if (answer.action === 'back')
432
- return;
433
- if (answer.action === 'status') {
434
- const { runSurfaceStatus } = await import('./surface-status.js');
435
- await runSurfaceStatus({});
436
- }
437
- if (answer.action === 'doctor') {
438
- const doctor = await import('./doctor.js');
439
- const checks = [
440
- await doctor.checkToken(),
441
- await doctor.checkCoreHealth(),
442
- await doctor.checkConfigSchema(process.cwd()),
443
- await doctor.checkNeetruEnv(process.cwd()),
444
- await doctor.checkCliVersion(),
445
- ];
446
- log.heading('Doctor');
447
- for (const check of checks) {
448
- const marker = check.ok ? chalk.green('*') : check.warning ? chalk.yellow('!') : chalk.red('x');
449
- console.log(` ${marker} ${check.name}${check.detail ? chalk.dim(` - ${check.detail}`) : ''}`);
450
- }
451
- }
452
- if (answer.action === 'whoami') {
453
- const { runWhoami } = await import('./whoami.js');
454
- await runWhoami({});
721
+ for (;;) {
722
+ const answer = await inquirer.prompt([
723
+ {
724
+ type: 'list',
725
+ name: 'action',
726
+ message: 'Status e diagnostico',
727
+ choices: [
728
+ { name: 'Status das superficies', value: 'status' },
729
+ { name: 'Doctor', value: 'doctor' },
730
+ { name: 'Whoami', value: 'whoami' },
731
+ { name: 'Voltar', value: 'back' },
732
+ ],
733
+ },
734
+ ]);
735
+ if (answer.action === 'back')
736
+ return;
737
+ await runSubmenuAction(async () => {
738
+ if (answer.action === 'status') {
739
+ const { runSurfaceStatus } = await import('./surface-status.js');
740
+ await runSurfaceStatus({});
741
+ }
742
+ if (answer.action === 'doctor') {
743
+ const doctor = await import('./doctor.js');
744
+ const checks = [
745
+ await doctor.checkToken(),
746
+ await doctor.checkCoreHealth(),
747
+ await doctor.checkConfigSchema(process.cwd()),
748
+ await doctor.checkNeetruEnv(process.cwd()),
749
+ await doctor.checkCliVersion(),
750
+ ];
751
+ log.heading('Doctor');
752
+ for (const check of checks) {
753
+ const marker = check.ok ? chalk.green('*') : check.warning ? chalk.yellow('!') : chalk.red('x');
754
+ console.log(` ${marker} ${check.name}${check.detail ? chalk.dim(` - ${check.detail}`) : ''}`);
755
+ }
756
+ }
757
+ if (answer.action === 'whoami') {
758
+ const { runWhoami } = await import('./whoami.js');
759
+ await runWhoami({});
760
+ }
761
+ });
455
762
  }
456
- await pause();
457
763
  }
458
764
  async function authMenu() {
459
- const answer = await inquirer.prompt([
460
- {
461
- type: 'list',
462
- name: 'action',
463
- message: 'Login e configuracao',
464
- choices: [
465
- { name: 'Login', value: 'login' },
466
- { name: 'Logout', value: 'logout' },
467
- { name: 'Ver config', value: 'config' },
468
- { name: 'Caminho da config', value: 'path' },
469
- { name: 'Voltar', value: 'back' },
470
- ],
471
- },
472
- ]);
473
- if (answer.action === 'back')
474
- return;
475
- if (answer.action === 'login') {
476
- const { runLogin } = await import('./login.js');
477
- await runLogin({});
478
- }
479
- if (answer.action === 'logout') {
480
- const { runLogout } = await import('./logout.js');
481
- await runLogout();
482
- }
483
- if (answer.action === 'config') {
484
- const { configGet } = await import('./config.js');
485
- configGet();
486
- }
487
- if (answer.action === 'path') {
488
- const { configPath } = await import('./config.js');
489
- configPath();
765
+ for (;;) {
766
+ const answer = await inquirer.prompt([
767
+ {
768
+ type: 'list',
769
+ name: 'action',
770
+ message: 'Login e configuracao',
771
+ choices: [
772
+ { name: 'Login', value: 'login' },
773
+ { name: 'Logout', value: 'logout' },
774
+ { name: 'Ver config', value: 'config' },
775
+ { name: 'Caminho da config', value: 'path' },
776
+ { name: 'Voltar', value: 'back' },
777
+ ],
778
+ },
779
+ ]);
780
+ if (answer.action === 'back')
781
+ return;
782
+ await runSubmenuAction(async () => {
783
+ if (answer.action === 'login') {
784
+ const { runLogin } = await import('./login.js');
785
+ await runLogin({});
786
+ }
787
+ if (answer.action === 'logout') {
788
+ const { runLogout } = await import('./logout.js');
789
+ await runLogout();
790
+ }
791
+ if (answer.action === 'config') {
792
+ const { configGet } = await import('./config.js');
793
+ configGet();
794
+ }
795
+ if (answer.action === 'path') {
796
+ const { configPath } = await import('./config.js');
797
+ configPath();
798
+ }
799
+ });
490
800
  }
491
- await pause();
492
801
  }
493
802
  export async function runTerminalUi() {
494
803
  if (!canPrompt()) {
@@ -504,21 +813,17 @@ export async function runTerminalUi() {
504
813
  throw new CommandExitError(typeof code === 'number' ? code : 0);
505
814
  });
506
815
  try {
507
- const email = await resolveSessionEmail();
508
- log.banner(email);
816
+ const [email, version, docsInfo] = await Promise.all([
817
+ resolveSessionEmail(),
818
+ checkForUpdate(),
819
+ loadDocsBannerInfo(),
820
+ ]);
821
+ log.banner(email, version, docsInfo);
509
822
  let running = true;
510
823
  while (running) {
511
824
  let answer;
512
825
  try {
513
- answer = await inquirer.prompt([
514
- {
515
- type: 'list',
516
- name: 'action',
517
- message: 'Neetru',
518
- pageSize: 24,
519
- choices: buildMenuChoices(MAIN_MENU),
520
- },
521
- ]);
826
+ answer = await promptMainMenu(MAIN_MENU, { sessionEmail: email });
522
827
  }
523
828
  catch {
524
829
  // Ctrl+C / fechamento do prompt = o usuário encerrando por vontade
@@ -526,6 +831,10 @@ export async function runTerminalUi() {
526
831
  running = false;
527
832
  continue;
528
833
  }
834
+ if (!answer) {
835
+ running = false;
836
+ continue;
837
+ }
529
838
  if (answer.action === 'exit') {
530
839
  running = false;
531
840
  continue;
@@ -550,6 +859,8 @@ export async function runTerminalUi() {
550
859
  await safeRun(tenantsMenu, { pauseAfter: false });
551
860
  if (answer.action === 'support')
552
861
  await safeRun(supportMenu, { pauseAfter: false });
862
+ if (answer.action === 'docs')
863
+ await safeRun(() => docsMenu(docsInfo), { pauseAfter: false });
553
864
  if (answer.action === 'health')
554
865
  await safeRun(healthMenu, { pauseAfter: false });
555
866
  if (answer.action === 'auth')