@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.
- package/CHANGELOG.md +20 -0
- package/dist/commands/ui.d.ts +22 -1
- package/dist/commands/ui.js +581 -270
- package/dist/commands/ui.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config-schema.d.ts +2 -2
- package/dist/lib/version-check.d.ts +20 -0
- package/dist/lib/version-check.js +55 -0
- package/dist/lib/version-check.js.map +1 -0
- package/dist/utils/logger.d.ts +19 -8
- package/dist/utils/logger.js +69 -10
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/logo.d.ts +2 -0
- package/dist/utils/logo.js +33 -1
- package/dist/utils/logo.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/ui.js
CHANGED
|
@@ -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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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: '
|
|
409
|
-
message: '
|
|
410
|
-
choices:
|
|
696
|
+
name: 'action',
|
|
697
|
+
message: 'Docs live',
|
|
698
|
+
choices: buildDocsMenuChoices(docsInfo),
|
|
411
699
|
},
|
|
412
700
|
]);
|
|
413
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
508
|
-
|
|
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
|
|
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')
|