@zenifra/cli 0.1.0 → 0.2.1

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/bin/zenifra.mjs CHANGED
@@ -8,9 +8,79 @@ import process from 'node:process';
8
8
  import readline from 'node:readline/promises';
9
9
 
10
10
  const DEFAULT_API_BASE_URL = 'https://api.zenifra.com/v1';
11
+ const DOCS_BASE_URL = 'https://docs.zenifra.com/pt';
12
+ const DOCS_RUNTIME_URL = `${DOCS_BASE_URL}/docs/runtime`;
13
+ const DOCS_CREATE_HTTP_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/http/how-to-create-a-http-project-at-zenifra-via-console`;
14
+ const DOCS_CREATE_POSTGRESQL_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-postgresql-project-at-zenifra-via-console`;
15
+ const DOCS_CREATE_MARIADB_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-mariadb-project-at-zenifra-via-console`;
16
+ const DOCS_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/configuration`;
17
+ const DOCS_DATABASE_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/database/configuration/configuration`;
18
+ const DOCS_PAYMENTS_URL = `${DOCS_BASE_URL}/docs/payments/payments`;
11
19
  const SESSION_DIR = process.env.ZENIFRA_CONFIG_DIR || join(homedir(), '.config', 'zenifra-cli');
12
20
  const SESSION_FILE = join(SESSION_DIR, 'session.json');
21
+ const PROFILES_FILE = join(SESSION_DIR, 'profiles.json');
22
+ const PROFILE_STORE_VERSION = 1;
23
+ const DEFAULT_PROFILE_NAME = 'default';
13
24
  const FINAL_BUILD_STATUSES = new Set(['success', 'failed']);
25
+ const ALLOWED_PLAN_VALUES = new Set([
26
+ 'free',
27
+ 'static',
28
+ 'basic',
29
+ 'premium',
30
+ 'premium_plus',
31
+ 'business',
32
+ 'deep_learning_basic',
33
+ 'deep_learning_premium',
34
+ 'db-free',
35
+ 'db-starter',
36
+ 'db-basic',
37
+ 'db-premium',
38
+ 'db-enterprise',
39
+ ]);
40
+ const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
41
+ const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb']);
42
+ const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
43
+ const GITHUB_RUNTIME_VERSIONS = {
44
+ nodejs: ['24', '22', '20'],
45
+ python: ['3.13', '3.12', '3.11'],
46
+ };
47
+ const ALLOWED_POSTGRESQL_VERSIONS = ['15', '16', '17', '18'];
48
+ const ALLOWED_MARIADB_VERSIONS = ['10', '11'];
49
+ const DEFAULT_HTTP_NETWORK_ACCESS = {
50
+ ingress_white_list: [{ cidr: '0.0.0.0/0', description: 'Allow all' }],
51
+ ingress_black_list: [],
52
+ };
53
+ const WIZARD_HTTP_PLAN_OPTIONS = [
54
+ 'free',
55
+ 'static',
56
+ 'basic',
57
+ 'premium',
58
+ 'premium_plus',
59
+ 'business',
60
+ 'deep_learning_basic',
61
+ 'deep_learning_premium',
62
+ ];
63
+ const WIZARD_DATABASE_PLAN_OPTIONS = [
64
+ 'db-basic',
65
+ 'db-premium',
66
+ 'db-starter',
67
+ 'db-enterprise',
68
+ 'db-free',
69
+ ];
70
+ const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
71
+ const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb'];
72
+ let promptInterface;
73
+ let pipedInputPromise;
74
+ let pipedInputIndex = 0;
75
+ const ANSI = {
76
+ reset: '\u001b[0m',
77
+ bold: '\u001b[1m',
78
+ dim: '\u001b[2m',
79
+ cyan: '\u001b[36m',
80
+ green: '\u001b[32m',
81
+ yellow: '\u001b[33m',
82
+ red: '\u001b[31m',
83
+ };
14
84
 
15
85
  class CliError extends Error {
16
86
  constructor(message, exitCode = 1) {
@@ -24,13 +94,21 @@ function usage() {
24
94
 
25
95
  Usage:
26
96
  zenifra help <command>
27
- zenifra auth login [--api-base <url>] [--code <code>]
28
- zenifra auth api-key --key <znf_key> [--api-base <url>]
29
- zenifra auth logout
97
+ zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]
98
+ zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]
99
+ zenifra auth logout [--profile <name>]
100
+ zenifra profile list [--json]
101
+ zenifra profile show [<name>] [--json]
102
+ zenifra profile add [--name <name>] [--description <text>] [--api-base <url>] [--mode <api-key|login>] [--key <znf_key>] [--json]
103
+ zenifra profile edit <name> [--description <text>] [--api-base <url>] [--json]
104
+ zenifra profile use <name> [--json]
105
+ zenifra profile remove <name> [--json]
30
106
  zenifra orgs [--json]
31
107
  zenifra org set [--org <id>]
108
+ zenifra plans [--type <all|http|database|storage>] [--json]
109
+ zenifra create project
110
+ zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
32
111
  zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]
33
- zenifra projects create --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
34
112
  zenifra project info --project <id> [--json]
35
113
  zenifra project url --project <id> [--json]
36
114
  zenifra project logs --project <id> [--instance <id>] [--json]
@@ -50,7 +128,7 @@ Usage:
50
128
 
51
129
  Environment:
52
130
  ZENIFRA_API_URL Override API base URL.
53
- ZENIFRA_CONFIG_DIR Override local session directory.
131
+ ZENIFRA_CONFIG_DIR Override local profile directory.
54
132
  ZENIFRA_API_KEY Use a global organization API key for automation.
55
133
 
56
134
  Run "zenifra help <command>" or "zenifra <command> --help" for command-specific usage, examples, and output.
@@ -58,33 +136,113 @@ Run "zenifra help <command>" or "zenifra <command> --help" for command-specific
58
136
  }
59
137
 
60
138
  const HELP_SPECS = [
139
+ {
140
+ command: 'auth',
141
+ usage: 'zenifra auth\n zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]\n zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]\n zenifra auth logout [--profile <name>]',
142
+ description: 'Gerencia a autenticacao dos perfis locais da CLI.',
143
+ examples: ['zenifra auth', 'zenifra auth login --profile staging', 'zenifra auth api-key --profile prod --key znf_0123456789abcdef01234567_abcd...'],
144
+ output: 'Zenifra CLI - auth',
145
+ notes: ['Use "zenifra help auth login", "zenifra help auth api-key" ou "zenifra help auth logout" para detalhes de cada subcomando.'],
146
+ },
61
147
  {
62
148
  command: 'auth login',
63
- usage: 'zenifra auth login [--api-base <url>] [--code <code>]',
64
- description: 'Autentica um usuario Zenifra e salva a sessao local.',
149
+ usage: 'zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]',
150
+ description: 'Autentica um usuario Zenifra no perfil ativo ou em um perfil especifico.',
65
151
  flags: [
152
+ '--profile <name> Atualiza ou cria o perfil informado e o torna ativo.',
66
153
  '--api-base <url> Usa uma API diferente da producao.',
67
154
  '--code <code> Informa o codigo de desafio sem prompt interativo.',
68
155
  ],
69
- examples: ['zenifra auth login', 'zenifra auth login --code 123456'],
156
+ examples: ['zenifra auth login', 'zenifra auth login --profile staging --code 123456'],
70
157
  output: 'Login realizado com sucesso.',
71
158
  notes: ['Exige email, senha e, quando habilitado, codigo de desafio.'],
72
159
  },
73
160
  {
74
161
  command: 'auth api-key',
75
- usage: 'zenifra auth api-key --key <znf_key> [--api-base <url>]',
76
- description: 'Salva uma API key organizacional para comandos de automacao.',
77
- flags: ['--key <znf_key> API key organizacional.', '--api-base <url> Usa uma API diferente da producao.'],
78
- examples: ['zenifra auth api-key --key znf_0123456789abcdef01234567_abcd...'],
162
+ usage: 'zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]',
163
+ description: 'Salva uma API key organizacional no perfil ativo ou em um perfil especifico.',
164
+ flags: ['--key <znf_key> API key organizacional.', '--profile <name> Atualiza ou cria o perfil informado e o torna ativo.', '--api-base <url> Usa uma API diferente da producao.'],
165
+ examples: ['zenifra auth api-key --key znf_0123456789abcdef01234567_abcd...', 'zenifra auth api-key --profile prod --key znf_0123456789abcdef01234567_abcd...'],
79
166
  output: 'API key salva com sucesso. Ela sera usada em comandos de automacao.',
80
167
  notes: ['API keys ja carregam a organizacao e nao exigem "zenifra org set".'],
81
168
  },
82
169
  {
83
170
  command: 'auth logout',
84
- usage: 'zenifra auth logout',
85
- description: 'Remove a sessao local do CLI.',
86
- examples: ['zenifra auth logout'],
87
- output: 'Sessao removida.',
171
+ usage: 'zenifra auth logout [--profile <name>]',
172
+ description: 'Remove a autenticacao do perfil ativo ou do perfil informado.',
173
+ flags: ['--profile <name> Limpa outro perfil sem trocar o perfil ativo.'],
174
+ examples: ['zenifra auth logout', 'zenifra auth logout --profile staging'],
175
+ output: 'Autenticacao removida do perfil active.',
176
+ },
177
+ {
178
+ command: 'profile',
179
+ usage: 'zenifra profile\n zenifra profile list [--json]\n zenifra profile show [<name>] [--json]\n zenifra profile add [--name <name>] [--description <text>] [--api-base <url>] [--mode <api-key|login>] [--key <znf_key>] [--json]\n zenifra profile edit <name> [--description <text>] [--api-base <url>] [--json]\n zenifra profile use <name> [--json]\n zenifra profile remove <name> [--json]',
180
+ description: 'Gerencia perfis de ambiente locais, incluindo API base, descricao e credenciais.',
181
+ examples: ['zenifra profile', 'zenifra profile list', 'zenifra profile add --name staging --description Homologacao --api-base https://api-stg.zenifra.com/v1 --mode api-key --key znf_0123...'],
182
+ output: 'Zenifra CLI - profile',
183
+ notes: ['Use "zenifra help profile <subcomando>" para detalhes de list, show, add, edit, use e remove.'],
184
+ },
185
+ {
186
+ command: 'profile list',
187
+ usage: 'zenifra profile list [--json]',
188
+ description: 'Lista os perfis locais configurados e indica o perfil ativo.',
189
+ flags: ['--json Imprime a resposta em JSON.'],
190
+ examples: ['zenifra profile list', 'zenifra profile list --json'],
191
+ output: 'Nome Tipo API base Ativo Descricao\nprod api_key https://api.zenifra.com/v1 yes Producao\nstaging access_token https://api-stg.zenifra.com/v1 no Homologacao',
192
+ jsonOutput: '[{"name":"prod","auth_mode":"api_key","api_base_url":"https://api.zenifra.com/v1","active":true}]',
193
+ },
194
+ {
195
+ command: 'profile show',
196
+ usage: 'zenifra profile show [<name>] [--json]',
197
+ description: 'Mostra os detalhes do perfil ativo ou de um perfil especifico.',
198
+ flags: ['--json Imprime a resposta em JSON.'],
199
+ examples: ['zenifra profile show', 'zenifra profile show staging --json'],
200
+ output: 'Nome: prod\nDescricao: Producao\nTipo: api_key\nAPI base: https://api.zenifra.com/v1\nAPI key: znf_0123...FGHI',
201
+ jsonOutput: '{"name":"prod","description":"Producao","auth_mode":"api_key","api_base_url":"https://api.zenifra.com/v1","active":true}',
202
+ },
203
+ {
204
+ command: 'profile add',
205
+ usage: 'zenifra profile add [--name <name>] [--description <text>] [--api-base <url>] [--mode <api-key|login>] [--key <znf_key>] [--json]',
206
+ description: 'Cria um novo perfil e opcionalmente autentica via API key ou login de usuario.',
207
+ flags: ['--name <name> Nome do perfil.', '--description <text> Descricao opcional.', '--api-base <url> API base do perfil.', '--mode <mode> api-key ou login.', '--key <znf_key> API key para modo api-key.', '--json Imprime a resposta em JSON.'],
208
+ examples: ['zenifra profile add --name staging --description Homologacao --api-base https://api-stg.zenifra.com/v1 --mode api-key --key znf_0123...', 'zenifra profile add'],
209
+ output: 'Perfil staging salvo e definido como ativo.',
210
+ jsonOutput: '{"name":"staging","auth_mode":"api_key","active":true}',
211
+ },
212
+ {
213
+ command: 'profile edit',
214
+ usage: 'zenifra profile edit <name> [--description <text>] [--api-base <url>] [--json]',
215
+ description: 'Atualiza descricao e API base de um perfil existente.',
216
+ flags: ['--description <text> Nova descricao.', '--api-base <url> Nova API base.', '--json Imprime a resposta em JSON.'],
217
+ examples: ['zenifra profile edit staging --description Homologacao interna', 'zenifra profile edit prod --api-base https://api.zenifra.com/v1 --json'],
218
+ output: 'Perfil staging atualizado.',
219
+ jsonOutput: '{"name":"staging","description":"Homologacao interna"}',
220
+ },
221
+ {
222
+ command: 'profile use',
223
+ usage: 'zenifra profile use <name> [--json]',
224
+ description: 'Define o perfil ativo para os proximos comandos.',
225
+ flags: ['--json Imprime a resposta em JSON.'],
226
+ examples: ['zenifra profile use staging', 'zenifra profile use prod --json'],
227
+ output: 'Perfil ativo: staging',
228
+ jsonOutput: '{"active_profile":"staging"}',
229
+ },
230
+ {
231
+ command: 'profile remove',
232
+ usage: 'zenifra profile remove <name> [--json]',
233
+ description: 'Remove um perfil local que nao esteja ativo.',
234
+ flags: ['--json Imprime a resposta em JSON.'],
235
+ examples: ['zenifra profile remove staging'],
236
+ output: 'Perfil staging removido.',
237
+ jsonOutput: '{"removed":"staging"}',
238
+ },
239
+ {
240
+ command: 'org',
241
+ usage: 'zenifra org\n zenifra org set [--org <id>]',
242
+ description: 'Agrupa comandos relacionados a organizacao ativa da sessao de usuario.',
243
+ examples: ['zenifra org', 'zenifra org set', 'zenifra org set --org 507f1f77bcf86cd799439011'],
244
+ output: 'Zenifra CLI - org',
245
+ notes: ['Use "zenifra help org set" para detalhes do seletor de organizacao.'],
88
246
  },
89
247
  {
90
248
  command: 'orgs',
@@ -103,6 +261,15 @@ const HELP_SPECS = [
103
261
  examples: ['zenifra org set', 'zenifra org set --org 507f1f77bcf86cd799439011'],
104
262
  output: 'Organizacao ativa: 507f1f77bcf86cd799439011',
105
263
  },
264
+ {
265
+ command: 'plans',
266
+ usage: 'zenifra plans [--type <all|http|database|storage>] [--json]',
267
+ description: 'Lista os catalogos publicos de preco de planos HTTP, banco e armazenamento.',
268
+ flags: ['--type <type> Filtra o catalogo: all, http, database ou storage.', '--json Imprime a resposta em JSON.'],
269
+ examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type storage --json'],
270
+ output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
271
+ jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
272
+ },
106
273
  {
107
274
  command: 'projects',
108
275
  usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]',
@@ -113,13 +280,22 @@ const HELP_SPECS = [
113
280
  jsonOutput: '[{"id":"507f1f77bcf86cd799439012","name":"api-web","status":"running","type_project":"http"}]',
114
281
  },
115
282
  {
116
- command: 'projects create',
117
- usage: 'zenifra projects create --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]',
118
- description: 'Cria um projeto usando um payload de configuracao JSON.',
283
+ command: 'create project',
284
+ usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]',
285
+ description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
119
286
  flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Modo de pagamento.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--json Imprime a resposta em JSON.'],
120
- examples: ['zenifra projects create --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
121
- output: 'Projeto criado: 507f1f77bcf86cd799439012\nDominio: api-web.client.zenifra.com',
287
+ examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
288
+ output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
122
289
  jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
290
+ notes: ['Sem flags de criacao, a CLI abre um wizard guiado com docs, exemplos e indicacao de campos obrigatorios.'],
291
+ },
292
+ {
293
+ command: 'project',
294
+ usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
295
+ description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
296
+ examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
297
+ output: 'Zenifra CLI - project',
298
+ notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, envs e instances.'],
123
299
  },
124
300
  {
125
301
  command: 'project info',
@@ -319,6 +495,18 @@ function commandHelp(positionals) {
319
495
  return formatHelp(spec);
320
496
  }
321
497
 
498
+ function isRemovedProjectsCreate(positionals) {
499
+ return positionals[0] === 'projects' && positionals[1] === 'create';
500
+ }
501
+
502
+ function removedProjectsCreateMessage() {
503
+ return 'Comando removido. Use "zenifra create project". Veja "zenifra help create project" ou "zenifra create project --help".';
504
+ }
505
+
506
+ function isNamespaceCommand(command) {
507
+ return ['auth', 'profile', 'project', 'org'].includes(command);
508
+ }
509
+
322
510
  function parseArgs(argv) {
323
511
  const positional = [];
324
512
  const flags = {};
@@ -351,11 +539,71 @@ function parseArgs(argv) {
351
539
  return { positional, flags };
352
540
  }
353
541
 
354
- async function readSession() {
355
- if (!existsSync(SESSION_FILE)) {
356
- return {};
542
+ function emptyProfileStore() {
543
+ return {
544
+ version: PROFILE_STORE_VERSION,
545
+ activeProfile: null,
546
+ profiles: {},
547
+ };
548
+ }
549
+
550
+ function normalizeApiBaseUrl(value) {
551
+ return String(value || DEFAULT_API_BASE_URL).replace(/\/$/, '');
552
+ }
553
+
554
+ function normalizeProfileName(value) {
555
+ const normalized = String(value || '').trim().toLowerCase();
556
+ if (!normalized) {
557
+ throw new CliError('Nome de perfil obrigatorio.');
558
+ }
559
+ if (!/^[a-z0-9._-]+$/.test(normalized)) {
560
+ throw new CliError('Nome de perfil invalido. Use apenas letras, numeros, ponto, underscore e hifen.');
357
561
  }
562
+ return normalized;
563
+ }
564
+
565
+ function isRecord(value) {
566
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
567
+ }
358
568
 
569
+ function normalizeProfileRecord(name, profile = {}) {
570
+ return {
571
+ name,
572
+ description: profile.description ? String(profile.description) : '',
573
+ authMode: profile.authMode === 'access_token' ? 'access_token' : 'api_key',
574
+ apiBaseUrl: normalizeApiBaseUrl(profile.apiBaseUrl),
575
+ apiKey: profile.apiKey ? String(profile.apiKey) : undefined,
576
+ accessToken: profile.accessToken ? String(profile.accessToken) : undefined,
577
+ selectedOrganizationId: profile.selectedOrganizationId ? String(profile.selectedOrganizationId) : undefined,
578
+ updatedAt: profile.updatedAt ? String(profile.updatedAt) : undefined,
579
+ };
580
+ }
581
+
582
+ function sanitizeProfileStore(store) {
583
+ const next = emptyProfileStore();
584
+ if (!isRecord(store)) return next;
585
+ next.version = PROFILE_STORE_VERSION;
586
+ const rawProfiles = isRecord(store.profiles) ? store.profiles : {};
587
+ for (const [rawName, rawProfile] of Object.entries(rawProfiles)) {
588
+ const name = normalizeProfileName(rawName);
589
+ next.profiles[name] = normalizeProfileRecord(name, rawProfile);
590
+ }
591
+ if (store.activeProfile && next.profiles[normalizeProfileName(store.activeProfile)]) {
592
+ next.activeProfile = normalizeProfileName(store.activeProfile);
593
+ }
594
+ return next;
595
+ }
596
+
597
+ async function writeProfileStore(store) {
598
+ const normalized = sanitizeProfileStore(store);
599
+ await mkdir(dirname(PROFILES_FILE), { recursive: true });
600
+ await writeFile(PROFILES_FILE, `${JSON.stringify(normalized, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
601
+ await chmod(PROFILES_FILE, 0o600).catch(() => undefined);
602
+ return normalized;
603
+ }
604
+
605
+ async function readLegacySession() {
606
+ if (!existsSync(SESSION_FILE)) return null;
359
607
  try {
360
608
  return JSON.parse(await readFile(SESSION_FILE, 'utf8'));
361
609
  } catch {
@@ -363,29 +611,147 @@ async function readSession() {
363
611
  }
364
612
  }
365
613
 
366
- async function writeSession(session) {
367
- await mkdir(dirname(SESSION_FILE), { recursive: true });
368
- await writeFile(SESSION_FILE, `${JSON.stringify(session, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
369
- await chmod(SESSION_FILE, 0o600).catch(() => undefined);
614
+ async function migrateLegacySession() {
615
+ const legacy = await readLegacySession();
616
+ if (!legacy) {
617
+ return emptyProfileStore();
618
+ }
619
+ const store = emptyProfileStore();
620
+ const migratedProfile = normalizeProfileRecord(DEFAULT_PROFILE_NAME, {
621
+ description: 'Perfil migrado automaticamente do session.json legado.',
622
+ authMode: legacy.accessToken ? 'access_token' : 'api_key',
623
+ apiBaseUrl: legacy.apiBaseUrl || DEFAULT_API_BASE_URL,
624
+ apiKey: legacy.apiKey,
625
+ accessToken: legacy.accessToken,
626
+ selectedOrganizationId: legacy.selectedOrganizationId,
627
+ updatedAt: legacy.updatedAt || new Date().toISOString(),
628
+ });
629
+ store.profiles[DEFAULT_PROFILE_NAME] = migratedProfile;
630
+ store.activeProfile = DEFAULT_PROFILE_NAME;
631
+ return writeProfileStore(store);
632
+ }
633
+
634
+ async function readProfileStore() {
635
+ if (existsSync(PROFILES_FILE)) {
636
+ try {
637
+ return sanitizeProfileStore(JSON.parse(await readFile(PROFILES_FILE, 'utf8')));
638
+ } catch {
639
+ throw new CliError(`Store de perfis invalido em ${PROFILES_FILE}. Corrija o arquivo ou remova-o para recriar.`);
640
+ }
641
+ }
642
+ return migrateLegacySession();
643
+ }
644
+
645
+ function buildSession(store) {
646
+ const activeName = store.activeProfile && store.profiles[store.activeProfile] ? store.activeProfile : null;
647
+ const activeProfile = activeName ? store.profiles[activeName] : {};
648
+ return {
649
+ ...activeProfile,
650
+ __store: store,
651
+ __profileName: activeName,
652
+ };
653
+ }
654
+
655
+ async function readSession() {
656
+ return buildSession(await readProfileStore());
657
+ }
658
+
659
+ function getStore(session) {
660
+ return session.__store || emptyProfileStore();
661
+ }
662
+
663
+ function getProfileName(session) {
664
+ return session.__profileName || null;
665
+ }
666
+
667
+ function getProfileRecord(session, explicitName) {
668
+ const store = getStore(session);
669
+ const name = explicitName ? normalizeProfileName(explicitName) : getProfileName(session);
670
+ if (!name) return null;
671
+ return store.profiles[name] ? { name, profile: store.profiles[name], store } : null;
672
+ }
673
+
674
+ async function persistSession(session) {
675
+ const store = getStore(session);
676
+ const profileName = getProfileName(session);
677
+ if (!profileName) {
678
+ return writeProfileStore(store);
679
+ }
680
+ store.profiles[profileName] = normalizeProfileRecord(profileName, session);
681
+ if (!store.activeProfile || store.activeProfile === profileName) {
682
+ store.activeProfile = profileName;
683
+ }
684
+ const normalized = await writeProfileStore(store);
685
+ session.__store = normalized;
686
+ session.__profileName = profileName;
687
+ return normalized;
688
+ }
689
+
690
+ function buildProfileSession(store, profileName) {
691
+ const name = normalizeProfileName(profileName);
692
+ const profile = store.profiles[name] || normalizeProfileRecord(name, { apiBaseUrl: DEFAULT_API_BASE_URL });
693
+ return {
694
+ ...profile,
695
+ __store: store,
696
+ __profileName: name,
697
+ };
698
+ }
699
+
700
+ function ensureProfile(session, requestedName, { activate = false } = {}) {
701
+ const store = getStore(session);
702
+ const profileName = requestedName ? normalizeProfileName(requestedName) : getProfileName(session) || DEFAULT_PROFILE_NAME;
703
+ if (!store.profiles[profileName]) {
704
+ store.profiles[profileName] = normalizeProfileRecord(profileName, { apiBaseUrl: DEFAULT_API_BASE_URL });
705
+ }
706
+ if (activate || !store.activeProfile) {
707
+ store.activeProfile = profileName;
708
+ }
709
+ session.__store = store;
710
+ if (activate) {
711
+ session.__profileName = profileName;
712
+ }
713
+ return buildProfileSession(store, profileName);
714
+ }
715
+
716
+ function requireExistingProfile(session, requestedName) {
717
+ const profileName = normalizeProfileName(requestedName || getProfileName(session));
718
+ const store = getStore(session);
719
+ if (!store.profiles[profileName]) {
720
+ throw new CliError(`Perfil nao encontrado: ${profileName}`);
721
+ }
722
+ return buildProfileSession(store, profileName);
723
+ }
724
+
725
+ function activateProfileSession(session, profileName) {
726
+ const store = getStore(session);
727
+ const name = normalizeProfileName(profileName);
728
+ if (!store.profiles[name]) {
729
+ throw new CliError(`Perfil nao encontrado: ${name}`);
730
+ }
731
+ const next = buildProfileSession({ ...store, activeProfile: name }, name);
732
+ Object.assign(session, next);
733
+ session.__store.activeProfile = name;
734
+ return session;
735
+ }
736
+
737
+ function profileApiBaseUrl(profile, flags) {
738
+ return normalizeApiBaseUrl(flags.apiBase || process.env.ZENIFRA_API_URL || profile?.apiBaseUrl || DEFAULT_API_BASE_URL);
370
739
  }
371
740
 
372
741
  function apiBaseUrl(session, flags) {
373
- return String(flags.apiBase || process.env.ZENIFRA_API_URL || session.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, '');
742
+ return profileApiBaseUrl(session, flags);
374
743
  }
375
744
 
376
745
  function resolveCredential(session) {
377
746
  if (process.env.ZENIFRA_API_KEY) {
378
747
  return { token: process.env.ZENIFRA_API_KEY, type: 'api_key', source: 'env' };
379
748
  }
380
-
381
749
  if (session.apiKey) {
382
- return { token: session.apiKey, type: 'api_key', source: 'session' };
750
+ return { token: session.apiKey, type: 'api_key', source: 'profile' };
383
751
  }
384
-
385
752
  if (session.accessToken) {
386
- return { token: session.accessToken, type: 'user', source: 'session' };
753
+ return { token: session.accessToken, type: 'user', source: 'profile' };
387
754
  }
388
-
389
755
  return null;
390
756
  }
391
757
 
@@ -394,25 +760,363 @@ function hasApiKeyCredential(session) {
394
760
  }
395
761
 
396
762
  function requireUserSession(session, action) {
397
- if (session.accessToken) {
398
- return;
399
- }
400
-
763
+ if (session.accessToken) return;
401
764
  if (hasApiKeyCredential(session)) {
402
765
  throw new CliError(`${action} exige login de usuario. A API key global ja define a organizacao para comandos de automacao.`);
403
766
  }
404
-
405
767
  throw new CliError('Voce precisa fazer login primeiro: zenifra auth login');
406
768
  }
407
769
 
408
770
  async function prompt(question, defaultValue) {
409
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
771
+ const separator = question.trimEnd().endsWith('>') ? ' ' : ': ';
772
+ if (!process.stdin.isTTY) {
773
+ if (!pipedInputPromise) {
774
+ pipedInputPromise = (async () => {
775
+ const chunks = [];
776
+ for await (const chunk of process.stdin) chunks.push(chunk);
777
+ return Buffer.concat(chunks).toString('utf8').replace(/\r\n/g, '\n').split('\n');
778
+ })();
779
+ }
780
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
781
+ process.stdout.write(`${question}${suffix}${separator}`);
782
+ const lines = await pipedInputPromise;
783
+ if (pipedInputIndex >= lines.length) {
784
+ throw new CliError('Entrada interativa incompleta para o wizard.');
785
+ }
786
+ const answer = lines[pipedInputIndex] ?? '';
787
+ pipedInputIndex += 1;
788
+ process.stdout.write('\n');
789
+ return answer.trim() || defaultValue || '';
790
+ }
791
+
792
+ if (!promptInterface) {
793
+ promptInterface = readline.createInterface({ input: process.stdin, output: process.stdout });
794
+ }
410
795
  const suffix = defaultValue ? ` (${defaultValue})` : '';
411
- const answer = await rl.question(`${question}${suffix}: `);
412
- rl.close();
796
+ const answer = await promptInterface.question(`${question}${suffix}${separator}`);
413
797
  return answer.trim() || defaultValue || '';
414
798
  }
415
799
 
800
+ function shouldUseAnsi() {
801
+ return Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
802
+ }
803
+
804
+ function tone(value, color) {
805
+ return shouldUseAnsi() ? `${ANSI[color]}${value}${ANSI.reset}` : value;
806
+ }
807
+
808
+ function toneDim(value) {
809
+ return shouldUseAnsi() ? `${ANSI.dim}${value}${ANSI.reset}` : value;
810
+ }
811
+
812
+ function estimateWizardTotal(state = {}) {
813
+ const typeProject = state.typeProject;
814
+ if (!typeProject) return 18;
815
+
816
+ let total = 6;
817
+ if (typeProject === 'http') {
818
+ total += 3;
819
+
820
+ if (state.plan !== 'free') {
821
+ total += 2;
822
+ if (state.httpStoragePersistent) total += 1;
823
+ }
824
+
825
+ total += 1;
826
+ if (state.envs?.enabled) total += state.envs.count * 3;
827
+
828
+ if (state.httpSource === 'github') {
829
+ total += 9;
830
+ } else if (state.httpSource === 'oci') {
831
+ total += 2;
832
+ if (state.httpImagePublic === false) {
833
+ total += 1;
834
+ if (state.httpImageAuthType === 'username_password') total += 2;
835
+ if (state.httpImageAuthType === 'aws') total += 4;
836
+ }
837
+ } else {
838
+ total += 1;
839
+ }
840
+
841
+ if (state.planAllowsBlockIp) {
842
+ total += 2;
843
+ if (state.whitelist?.enabled) total += state.whitelist.count * 3;
844
+ if (state.blacklist?.enabled) total += state.blacklist.count * 3;
845
+ }
846
+
847
+ if (state.planAllowsSubdomain) total += 1;
848
+ return total;
849
+ }
850
+
851
+ if (typeProject === 'postgresql') {
852
+ total += 2;
853
+ if (state.plan !== 'db-free') total += 1;
854
+ return total;
855
+ }
856
+
857
+ if (typeProject === 'mariadb') {
858
+ total += 2;
859
+ return total;
860
+ }
861
+
862
+ return 18;
863
+ }
864
+
865
+ function createWizardUi(state = {}) {
866
+ return { step: 0, total: estimateWizardTotal(state), state };
867
+ }
868
+
869
+ function refreshWizardTotal(ui) {
870
+ ui.total = Math.max(ui.step, estimateWizardTotal(ui.state));
871
+ return ui.total;
872
+ }
873
+
874
+ function nextWizardStep(ui) {
875
+ refreshWizardTotal(ui);
876
+ ui.step += 1;
877
+ if (ui.step > ui.total) ui.total = ui.step;
878
+ return ui.step;
879
+ }
880
+
881
+ function isHelpRequest(value) {
882
+ return ['?', 'help', 'ajuda'].includes(normalizeFreeText(value));
883
+ }
884
+
885
+ function formatInlineOptions(options, maxVisible = 4) {
886
+ const values = options.map((option, index) => `${index + 1}=${option.value}`);
887
+ if (values.length <= maxVisible) return values.join(' ');
888
+ return `${values.slice(0, maxVisible - 1).join(' ')} ... ${values.at(-1)}`;
889
+ }
890
+
891
+ function printWizardFieldHelp({ label, required = false, examples = [], docs, allowedValues = [], options = [] }) {
892
+ process.stdout.write(`\n${tone(`Ajuda: ${label}`, 'cyan')}\n`);
893
+ process.stdout.write(` obrigatorio: ${required ? 'sim' : 'nao'}\n`);
894
+ if (allowedValues.length) process.stdout.write(` valores: ${allowedValues.join(', ')}\n`);
895
+ if (options.length) process.stdout.write(` opcoes: ${options.map((option, index) => `${index + 1}=${option.value}`).join(' | ')}\n`);
896
+ if (examples.length) process.stdout.write(` exemplo: ${examples.join(' | ')}\n`);
897
+ if (docs) process.stdout.write(` docs: ${docs}\n`);
898
+ }
899
+
900
+ function buildWizardPromptLine({ ui, label, required = false, examples = [], options = [], kind = 'text', fixedValue = null }) {
901
+ const step = nextWizardStep(ui);
902
+ const stepText = toneDim(`[${step}/${ui.total}]`);
903
+ const requiredText = required ? tone('*', 'yellow') : '';
904
+ let suffix = '';
905
+
906
+ if (kind === 'boolean') suffix = toneDim('[s/n]');
907
+ else if (kind === 'select') suffix = toneDim(`[${formatInlineOptions(options)}]`);
908
+ else if (fixedValue !== null) suffix = toneDim(`[fixo=${fixedValue}]`);
909
+ else if (examples.length) suffix = toneDim(`(ex: ${examples[0]})`);
910
+
911
+ return `${stepText} ${tone(label, 'bold')}${requiredText}${suffix ? ` ${suffix}` : ''} >`;
912
+ }
913
+
914
+ function isAffirmative(value) {
915
+ return ['s', 'sim', 'y', 'yes'].includes(normalizeFreeText(value));
916
+ }
917
+
918
+ function isNegative(value) {
919
+ return ['n', 'nao', 'não', 'no'].includes(normalizeFreeText(value));
920
+ }
921
+
922
+ function parsePositiveInteger(value) {
923
+ const number = Number(String(value).trim());
924
+ if (!Number.isInteger(number) || number <= 0) return null;
925
+ return number;
926
+ }
927
+
928
+ function parseOptionalNullableCommand(value) {
929
+ const normalized = normalizeFreeText(value);
930
+ if (!normalized) return null;
931
+ if (normalized === 'none' || normalized === 'null') return null;
932
+ return String(value).trim();
933
+ }
934
+
935
+ function hasWizardCreateInput(flags) {
936
+ return !flags.name && !flags.plan && !flags.paymentMode && !flags.config && !flags.description;
937
+ }
938
+
939
+ async function promptWizardText({ label, required = false, examples = [], docs, allowedValues = [], normalize, validate, emptyValue = '' }) {
940
+ while (true) {
941
+ const answer = await prompt(buildWizardPromptLine({
942
+ ui: promptWizardText.ui,
943
+ label,
944
+ required,
945
+ examples,
946
+ kind: 'text',
947
+ }));
948
+
949
+ if (isHelpRequest(answer)) {
950
+ printWizardFieldHelp({ label, required, examples, docs, allowedValues });
951
+ promptWizardText.ui.step -= 1;
952
+ continue;
953
+ }
954
+
955
+ if (!answer) {
956
+ if (!required) return emptyValue;
957
+ process.stdout.write(`${tone('valor obrigatorio.', 'red')}\n`);
958
+ promptWizardText.ui.step -= 1;
959
+ continue;
960
+ }
961
+
962
+ const normalized = normalize ? normalize(answer) : String(answer).trim();
963
+ const error = validate ? validate(normalized, answer) : null;
964
+ if (error) {
965
+ process.stdout.write(`${tone(`valor invalido: ${error}`, 'red')}\n`);
966
+ promptWizardText.ui.step -= 1;
967
+ continue;
968
+ }
969
+ return normalized;
970
+ }
971
+ }
972
+
973
+ async function promptWizardSecret({ label, required = false, examples = [], docs, allowedValues = [] }) {
974
+ while (true) {
975
+ const promptLine = buildWizardPromptLine({
976
+ ui: promptWizardSecret.ui,
977
+ label,
978
+ required,
979
+ examples,
980
+ kind: 'text',
981
+ });
982
+ const answer = String(await promptHidden(promptLine)).trim();
983
+ if (isHelpRequest(answer)) {
984
+ printWizardFieldHelp({ label, required, examples, docs, allowedValues });
985
+ promptWizardSecret.ui.step -= 1;
986
+ continue;
987
+ }
988
+ if (!answer) {
989
+ if (!required) return '';
990
+ process.stdout.write(`${tone('valor obrigatorio.', 'red')}\n`);
991
+ promptWizardSecret.ui.step -= 1;
992
+ continue;
993
+ }
994
+ return answer;
995
+ }
996
+ }
997
+
998
+ async function promptWizardBoolean({ label, docs, examples = ['sim', 'nao'] }) {
999
+ while (true) {
1000
+ const answer = await prompt(buildWizardPromptLine({
1001
+ ui: promptWizardBoolean.ui,
1002
+ label,
1003
+ required: true,
1004
+ examples,
1005
+ kind: 'boolean',
1006
+ }));
1007
+ if (isHelpRequest(answer)) {
1008
+ printWizardFieldHelp({ label, required: true, examples, docs });
1009
+ promptWizardBoolean.ui.step -= 1;
1010
+ continue;
1011
+ }
1012
+ if (isAffirmative(answer)) return true;
1013
+ if (isNegative(answer)) return false;
1014
+ process.stdout.write(`${tone('valor invalido: responda com "s" ou "n".', 'red')}\n`);
1015
+ promptWizardBoolean.ui.step -= 1;
1016
+ }
1017
+ }
1018
+
1019
+ async function promptWizardSelect({ label, options, docs, examples = [] }) {
1020
+ while (true) {
1021
+ const allowedValues = options.map((option) => option.value);
1022
+ const answer = await prompt(buildWizardPromptLine({
1023
+ ui: promptWizardSelect.ui,
1024
+ label,
1025
+ required: true,
1026
+ examples,
1027
+ kind: 'select',
1028
+ options,
1029
+ }));
1030
+ if (isHelpRequest(answer)) {
1031
+ printWizardFieldHelp({ label, required: true, examples, docs, allowedValues, options });
1032
+ promptWizardSelect.ui.step -= 1;
1033
+ continue;
1034
+ }
1035
+ const numericIndex = Number(answer);
1036
+ if (Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= options.length) {
1037
+ return options[numericIndex - 1].value;
1038
+ }
1039
+ const normalized = normalizeFreeText(answer);
1040
+ const found = options.find((option) => normalizeFreeText(option.value) === normalized);
1041
+ if (found) return found.value;
1042
+ process.stdout.write(`${tone(`valor invalido: escolha um item entre ${options.map((_, index) => index + 1).join(', ')} ou um dos valores aceitos.`, 'red')}\n`);
1043
+ promptWizardSelect.ui.step -= 1;
1044
+ }
1045
+ }
1046
+
1047
+ async function promptWizardNumber({ label, docs, examples = [], min = 1, max = Number.MAX_SAFE_INTEGER, fixedValue = null }) {
1048
+ while (true) {
1049
+ const allowedValues = fixedValue === null ? [] : [String(fixedValue)];
1050
+ const answer = await prompt(buildWizardPromptLine({
1051
+ ui: promptWizardNumber.ui,
1052
+ label,
1053
+ required: true,
1054
+ examples,
1055
+ kind: 'number',
1056
+ fixedValue,
1057
+ }));
1058
+ if (isHelpRequest(answer)) {
1059
+ printWizardFieldHelp({ label, required: true, examples, docs, allowedValues });
1060
+ promptWizardNumber.ui.step -= 1;
1061
+ continue;
1062
+ }
1063
+ const value = parsePositiveInteger(answer);
1064
+ if (value === null) {
1065
+ process.stdout.write(`${tone('valor invalido: informe um inteiro positivo.', 'red')}\n`);
1066
+ promptWizardNumber.ui.step -= 1;
1067
+ continue;
1068
+ }
1069
+ if (value < min || value > max || (fixedValue !== null && value !== fixedValue)) {
1070
+ process.stdout.write(`${tone(`valor invalido: informe um inteiro entre ${min} e ${max}${fixedValue !== null ? ` e igual a ${fixedValue}` : ''}.`, 'red')}\n`);
1071
+ promptWizardNumber.ui.step -= 1;
1072
+ continue;
1073
+ }
1074
+ return value;
1075
+ }
1076
+ }
1077
+
1078
+ async function promptWizardPairs({ introLabel, itemLabels, docs, examples = [], stateKey = '' }) {
1079
+ const items = [];
1080
+ const ui = promptWizardBoolean.ui;
1081
+ const wantsAny = await promptWizardBoolean({ label: introLabel, docs, examples: ['sim', 'nao'] });
1082
+ if (stateKey) ui.state[stateKey] = { enabled: wantsAny, count: wantsAny ? 1 : 0 };
1083
+ if (!wantsAny) return items;
1084
+
1085
+ while (true) {
1086
+ const item = {};
1087
+ for (const field of itemLabels) {
1088
+ item[field.key] = await promptWizardText(field);
1089
+ }
1090
+ items.push(item);
1091
+ const addMore = await promptWizardBoolean({ label: 'Deseja adicionar mais um item?', docs, examples: ['sim', 'nao'] });
1092
+ if (!addMore) break;
1093
+ if (stateKey) ui.state[stateKey].count += 1;
1094
+ }
1095
+
1096
+ return items;
1097
+ }
1098
+
1099
+ function closePromptResources() {
1100
+ if (promptInterface) {
1101
+ promptInterface.close();
1102
+ promptInterface = undefined;
1103
+ }
1104
+ if (process.stdin.isTTY) {
1105
+ process.stdin.removeAllListeners('data');
1106
+ process.stdin.removeAllListeners('keypress');
1107
+ process.stdin.pause();
1108
+ if (typeof process.stdin.setRawMode === 'function') {
1109
+ try {
1110
+ process.stdin.setRawMode(false);
1111
+ } catch {
1112
+ // ignore cleanup failures when stdin is already detached
1113
+ }
1114
+ }
1115
+ }
1116
+ pipedInputPromise = undefined;
1117
+ pipedInputIndex = 0;
1118
+ }
1119
+
416
1120
  async function promptHidden(question) {
417
1121
  if (!process.stdin.isTTY) {
418
1122
  return prompt(question);
@@ -448,7 +1152,8 @@ async function promptHidden(question) {
448
1152
  value += char;
449
1153
  };
450
1154
 
451
- process.stdout.write(`${question}: `);
1155
+ const separator = question.trimEnd().endsWith('>') ? ' ' : ': ';
1156
+ process.stdout.write(`${question}${separator}`);
452
1157
  stdin.setRawMode(true);
453
1158
  stdin.resume();
454
1159
  stdin.on('data', onData);
@@ -574,6 +1279,13 @@ function envsForOutput(envs, { showValues = false } = {}) {
574
1279
  }));
575
1280
  }
576
1281
 
1282
+ function formatPublicUrl(value) {
1283
+ const raw = String(value || '').trim();
1284
+ if (!raw) return '';
1285
+ if (/^https?:\/\//i.test(raw)) return raw;
1286
+ return `https://${raw}`;
1287
+ }
1288
+
577
1289
  function printEnvs(envs, flags) {
578
1290
  const output = envsForOutput(envs, { showValues: Boolean(flags.showValues) });
579
1291
  if (flags.json) return printJson(output);
@@ -634,7 +1346,7 @@ async function resolveOrgId(session, flags, { interactive = true } = {}) {
634
1346
  }
635
1347
  if (organizations.length === 1) {
636
1348
  session.selectedOrganizationId = idOf(organizations[0]);
637
- await writeSession(session);
1349
+ await persistSession(session);
638
1350
  return session.selectedOrganizationId;
639
1351
  }
640
1352
 
@@ -650,15 +1362,16 @@ async function resolveOrgId(session, flags, { interactive = true } = {}) {
650
1362
  }
651
1363
 
652
1364
  session.selectedOrganizationId = idOf(selected);
653
- await writeSession(session);
1365
+ await persistSession(session);
654
1366
  return session.selectedOrganizationId;
655
1367
  }
656
1368
 
657
1369
  async function handleLogin(session, flags) {
1370
+ const targetSession = ensureProfile(session, flags.profile, { activate: Boolean(flags.profile) || !getProfileName(session) });
658
1371
  const email = String(flags.email || await prompt('Email'));
659
1372
  const password = String(flags.password || await promptHidden('Senha'));
660
1373
 
661
- const loginPayload = await request(session, flags, 'PATCH', '/authentication', {
1374
+ const loginPayload = await request(targetSession, flags, 'PATCH', '/authentication', {
662
1375
  tokenRequired: false,
663
1376
  body: { email, password },
664
1377
  });
@@ -685,7 +1398,7 @@ async function handleLogin(session, flags) {
685
1398
  : `Codigo de verificacao${destination}`;
686
1399
  const code = String(flags.code || flags.totp || await prompt(label));
687
1400
 
688
- payload = await request(session, flags, 'POST', '/authentication/challenge/verify', {
1401
+ payload = await request(targetSession, flags, 'POST', '/authentication/challenge/verify', {
689
1402
  tokenRequired: false,
690
1403
  body: {
691
1404
  challenge_token: challengeToken,
@@ -700,15 +1413,18 @@ async function handleLogin(session, flags) {
700
1413
  }
701
1414
 
702
1415
  const nextSession = {
703
- ...session,
1416
+ ...targetSession,
704
1417
  accessToken,
705
1418
  apiKey: undefined,
706
- apiBaseUrl: apiBaseUrl(session, flags),
707
- selectedOrganizationId: session.selectedOrganizationId,
1419
+ authMode: 'access_token',
1420
+ apiBaseUrl: apiBaseUrl(targetSession, flags),
1421
+ selectedOrganizationId: targetSession.selectedOrganizationId,
708
1422
  updatedAt: new Date().toISOString(),
709
1423
  };
710
1424
 
711
- await writeSession(nextSession);
1425
+ nextSession.__store.activeProfile = nextSession.__profileName;
1426
+ await persistSession(nextSession);
1427
+ Object.assign(session, buildSession(nextSession.__store));
712
1428
  process.stdout.write('Login realizado com sucesso.\n');
713
1429
  }
714
1430
 
@@ -719,22 +1435,178 @@ async function handleApiKeyLogin(session, flags) {
719
1435
  throw new CliError('API key invalida. As API keys globais da Zenifra comecam com "znf_".');
720
1436
  }
721
1437
 
1438
+ const targetSession = ensureProfile(session, flags.profile, { activate: Boolean(flags.profile) || !getProfileName(session) });
722
1439
  const nextSession = {
723
- ...session,
1440
+ ...targetSession,
724
1441
  accessToken: undefined,
725
1442
  selectedOrganizationId: undefined,
726
1443
  apiKey,
727
- apiBaseUrl: apiBaseUrl(session, flags),
1444
+ authMode: 'api_key',
1445
+ apiBaseUrl: apiBaseUrl(targetSession, flags),
728
1446
  updatedAt: new Date().toISOString(),
729
1447
  };
730
1448
 
731
- await writeSession(nextSession);
1449
+ nextSession.__store.activeProfile = nextSession.__profileName;
1450
+ await persistSession(nextSession);
1451
+ Object.assign(session, buildSession(nextSession.__store));
732
1452
  process.stdout.write('API key salva com sucesso. Ela sera usada em comandos de automacao.\n');
733
1453
  }
734
1454
 
735
- async function handleLogout() {
736
- await rm(SESSION_FILE, { force: true });
737
- process.stdout.write('Sessao removida.\n');
1455
+ async function handleLogout(session, flags) {
1456
+ const targetName = flags.profile || getProfileName(session);
1457
+ if (!targetName) {
1458
+ throw new CliError('Nenhum perfil ativo configurado. Crie um perfil com "zenifra profile add" ou autentique com "zenifra auth login".');
1459
+ }
1460
+ const target = requireExistingProfile(session, targetName);
1461
+ target.accessToken = undefined;
1462
+ target.apiKey = undefined;
1463
+ target.selectedOrganizationId = undefined;
1464
+ target.authMode = 'api_key';
1465
+ target.updatedAt = new Date().toISOString();
1466
+ await persistSession(target);
1467
+ if (target.__profileName === getProfileName(session)) {
1468
+ Object.assign(session, buildSession(target.__store));
1469
+ }
1470
+ process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}.\n`);
1471
+ }
1472
+
1473
+ function maskApiKey(value) {
1474
+ if (!value) return '-';
1475
+ if (value.length <= 12) return '********';
1476
+ return `${value.slice(0, 8)}...${value.slice(-4)}`;
1477
+ }
1478
+
1479
+ function profileOutput(profile, activeName) {
1480
+ return {
1481
+ name: profile.name,
1482
+ description: profile.description || '',
1483
+ auth_mode: profile.accessToken ? 'access_token' : profile.apiKey ? 'api_key' : profile.authMode || 'api_key',
1484
+ api_base_url: profile.apiBaseUrl || DEFAULT_API_BASE_URL,
1485
+ selected_organization_id: profile.selectedOrganizationId,
1486
+ has_api_key: Boolean(profile.apiKey),
1487
+ has_access_token: Boolean(profile.accessToken),
1488
+ api_key_masked: maskApiKey(profile.apiKey),
1489
+ active: profile.name === activeName,
1490
+ updated_at: profile.updatedAt,
1491
+ };
1492
+ }
1493
+
1494
+ async function handleProfileList(session, flags) {
1495
+ const store = getStore(session);
1496
+ const profiles = Object.values(store.profiles)
1497
+ .sort((left, right) => left.name.localeCompare(right.name))
1498
+ .map((profile) => profileOutput(profile, store.activeProfile));
1499
+
1500
+ if (flags.json) return printJson(profiles);
1501
+ printTable(profiles, [
1502
+ { label: 'Nome', value: (profile) => profile.name },
1503
+ { label: 'Tipo', value: (profile) => profile.auth_mode },
1504
+ { label: 'API base', value: (profile) => profile.api_base_url },
1505
+ { label: 'Ativo', value: (profile) => (profile.active ? 'yes' : 'no') },
1506
+ { label: 'Descricao', value: (profile) => profile.description || '-' },
1507
+ ]);
1508
+ }
1509
+
1510
+ async function handleProfileShow(session, flags, positional = []) {
1511
+ const targetName = positional[2] || flags.name || getProfileName(session);
1512
+ if (!targetName) {
1513
+ throw new CliError('Nenhum perfil ativo configurado.');
1514
+ }
1515
+ const target = requireExistingProfile(session, targetName);
1516
+ const output = profileOutput(target, target.__store.activeProfile);
1517
+
1518
+ if (flags.json) return printJson(output);
1519
+ process.stdout.write(`Nome: ${output.name}\n`);
1520
+ process.stdout.write(`Descricao: ${output.description || '-'}\n`);
1521
+ process.stdout.write(`Tipo: ${output.auth_mode}\n`);
1522
+ process.stdout.write(`API base: ${output.api_base_url}\n`);
1523
+ process.stdout.write(`Ativo: ${output.active ? 'sim' : 'nao'}\n`);
1524
+ process.stdout.write(`API key: ${output.api_key_masked}\n`);
1525
+ process.stdout.write(`Token de login: ${output.has_access_token ? 'salvo' : '-'}\n`);
1526
+ process.stdout.write(`Organizacao ativa: ${output.selected_organization_id || '-'}\n`);
1527
+ }
1528
+
1529
+ async function handleProfileUse(session, flags, positional = []) {
1530
+ const targetName = positional[2] || flags.name;
1531
+ if (!targetName) throw new CliError('Informe o nome do perfil: zenifra profile use <name>.');
1532
+ activateProfileSession(session, targetName);
1533
+ await writeProfileStore(getStore(session));
1534
+ if (flags.json) return printJson({ active_profile: getProfileName(session) });
1535
+ process.stdout.write(`Perfil ativo: ${getProfileName(session)}\n`);
1536
+ }
1537
+
1538
+ async function promptProfileAddInput(flags) {
1539
+ const name = normalizeProfileName(flags.name || await prompt('Nome do perfil'));
1540
+ const description = String(flags.description ?? await prompt('Descricao do perfil'));
1541
+ const apiBase = normalizeApiBaseUrl(flags.apiBase || await prompt('API base', DEFAULT_API_BASE_URL));
1542
+ const mode = normalizeFreeText(String(flags.mode || await prompt('Modo do perfil', 'api-key')));
1543
+ if (!['api-key', 'login'].includes(mode)) {
1544
+ throw new CliError('Modo de perfil invalido. Use "api-key" ou "login".');
1545
+ }
1546
+ return { name, description, apiBase, mode };
1547
+ }
1548
+
1549
+ async function handleProfileAdd(session, flags) {
1550
+ const { name, description, apiBase, mode } = await promptProfileAddInput(flags);
1551
+ const targetSession = ensureProfile(session, name, { activate: true });
1552
+ targetSession.description = description;
1553
+ targetSession.apiBaseUrl = apiBase;
1554
+ if (mode === 'api-key') {
1555
+ const apiKey = String(flags.key || await promptHidden('API key Zenifra'));
1556
+ if (!apiKey.startsWith('znf_')) {
1557
+ throw new CliError('API key invalida. As API keys globais da Zenifra comecam com "znf_".');
1558
+ }
1559
+ targetSession.apiKey = apiKey;
1560
+ targetSession.accessToken = undefined;
1561
+ targetSession.selectedOrganizationId = undefined;
1562
+ targetSession.authMode = 'api_key';
1563
+ targetSession.updatedAt = new Date().toISOString();
1564
+ await persistSession(targetSession);
1565
+ Object.assign(session, buildSession(targetSession.__store));
1566
+ } else {
1567
+ targetSession.description = description;
1568
+ targetSession.apiBaseUrl = apiBase;
1569
+ targetSession.updatedAt = new Date().toISOString();
1570
+ await persistSession(targetSession);
1571
+ Object.assign(session, buildSession(targetSession.__store));
1572
+ await handleLogin(session, { ...flags, profile: name, apiBase });
1573
+ }
1574
+ const output = profileOutput(requireExistingProfile(session, name), getStore(session).activeProfile);
1575
+ if (flags.json) return printJson(output);
1576
+ process.stdout.write(`Perfil ${name} salvo e definido como ativo.\n`);
1577
+ }
1578
+
1579
+ async function handleProfileEdit(session, flags, positional = []) {
1580
+ const targetName = positional[2] || flags.name;
1581
+ if (!targetName) throw new CliError('Informe o nome do perfil: zenifra profile edit <name>.');
1582
+ const target = requireExistingProfile(session, targetName);
1583
+ target.description = String(flags.description ?? await prompt('Descricao do perfil', target.description || ''));
1584
+ target.apiBaseUrl = normalizeApiBaseUrl(flags.apiBase || await prompt('API base', target.apiBaseUrl || DEFAULT_API_BASE_URL));
1585
+ target.updatedAt = new Date().toISOString();
1586
+ await persistSession(target);
1587
+ if (target.__profileName === getProfileName(session)) {
1588
+ Object.assign(session, buildSession(target.__store));
1589
+ }
1590
+ const output = profileOutput(target, target.__store.activeProfile);
1591
+ if (flags.json) return printJson(output);
1592
+ process.stdout.write(`Perfil ${target.__profileName} atualizado.\n`);
1593
+ }
1594
+
1595
+ async function handleProfileRemove(session, flags, positional = []) {
1596
+ const targetName = positional[2] || flags.name;
1597
+ if (!targetName) throw new CliError('Informe o nome do perfil: zenifra profile remove <name>.');
1598
+ const normalizedName = normalizeProfileName(targetName);
1599
+ const store = getStore(session);
1600
+ if (!store.profiles[normalizedName]) {
1601
+ throw new CliError(`Perfil nao encontrado: ${normalizedName}`);
1602
+ }
1603
+ if (store.activeProfile === normalizedName) {
1604
+ throw new CliError('Nao e permitido remover o perfil ativo. Use "zenifra profile use <name>" para trocar antes.');
1605
+ }
1606
+ delete store.profiles[normalizedName];
1607
+ session.__store = await writeProfileStore(store);
1608
+ if (flags.json) return printJson({ removed: normalizedName });
1609
+ process.stdout.write(`Perfil ${normalizedName} removido.\n`);
738
1610
  }
739
1611
 
740
1612
  async function handleOrgs(session, flags) {
@@ -753,7 +1625,7 @@ async function handleOrgSet(session, flags) {
753
1625
 
754
1626
  if (flags.org) {
755
1627
  session.selectedOrganizationId = String(flags.org);
756
- await writeSession(session);
1628
+ await persistSession(session);
757
1629
  process.stdout.write(`Organizacao ativa: ${session.selectedOrganizationId}\n`);
758
1630
  return;
759
1631
  }
@@ -793,29 +1665,830 @@ async function parseConfig(input) {
793
1665
  }
794
1666
  }
795
1667
 
1668
+ function normalizeFreeText(value) {
1669
+ return String(value || '').trim().toLowerCase().replace(/\s+/g, ' ');
1670
+ }
1671
+
1672
+ function normalizePlan(value) {
1673
+ const raw = normalizeFreeText(value);
1674
+ if (!raw) return '';
1675
+
1676
+ const normalized = raw
1677
+ .replace(/[^a-z0-9]+/g, '_')
1678
+ .replace(/^_+|_+$/g, '');
1679
+
1680
+ const aliases = new Map([
1681
+ ['premium_plus', 'premium_plus'],
1682
+ ['premiumplus', 'premium_plus'],
1683
+ ['deep_learning_basic', 'deep_learning_basic'],
1684
+ ['deeplearningbasic', 'deep_learning_basic'],
1685
+ ['deep_learning_premium', 'deep_learning_premium'],
1686
+ ['deeplearningpremium', 'deep_learning_premium'],
1687
+ ['db_free', 'db-free'],
1688
+ ['dbfree', 'db-free'],
1689
+ ['db_starter', 'db-starter'],
1690
+ ['dbstarter', 'db-starter'],
1691
+ ['db_basic', 'db-basic'],
1692
+ ['dbbasic', 'db-basic'],
1693
+ ['db_premium', 'db-premium'],
1694
+ ['dbpremium', 'db-premium'],
1695
+ ['db_enterprise', 'db-enterprise'],
1696
+ ['dbenterprise', 'db-enterprise'],
1697
+ ]);
1698
+
1699
+ if (aliases.has(normalized)) return aliases.get(normalized);
1700
+ if (ALLOWED_PLAN_VALUES.has(normalized)) return normalized;
1701
+ return normalized;
1702
+ }
1703
+
1704
+ function normalizePaymentMode(value) {
1705
+ const raw = normalizeFreeText(value);
1706
+ const aliases = new Map([
1707
+ ['hourly', 'hourly'],
1708
+ ['por hora', 'hourly'],
1709
+ ['hora', 'hourly'],
1710
+ ['monthly', 'monthly'],
1711
+ ['month', 'monthly'],
1712
+ ['mensal', 'monthly'],
1713
+ ['por mes', 'monthly'],
1714
+ ['por mês', 'monthly'],
1715
+ ['yearly', 'yearly'],
1716
+ ['annual', 'yearly'],
1717
+ ['anual', 'yearly'],
1718
+ ['por ano', 'yearly'],
1719
+ ]);
1720
+ return aliases.get(raw) || raw;
1721
+ }
1722
+
1723
+ function normalizeTypeProject(value) {
1724
+ const raw = normalizeFreeText(value);
1725
+ const aliases = new Map([
1726
+ ['http', 'http'],
1727
+ ['api', 'http'],
1728
+ ['site', 'http'],
1729
+ ['postgres', 'postgresql'],
1730
+ ['postgresql', 'postgresql'],
1731
+ ['mariadb', 'mariadb'],
1732
+ ['maria db', 'mariadb'],
1733
+ ]);
1734
+ return aliases.get(raw) || raw;
1735
+ }
1736
+
1737
+ function normalizeRuntime(value) {
1738
+ const raw = normalizeFreeText(value);
1739
+ const aliases = new Map([
1740
+ ['node', 'nodejs'],
1741
+ ['nodejs', 'nodejs'],
1742
+ ['node.js', 'nodejs'],
1743
+ ['python', 'python'],
1744
+ ]);
1745
+ return aliases.get(raw) || raw;
1746
+ }
1747
+
1748
+ function wizardPlanOptionsForTypeProject(typeProject) {
1749
+ if (typeProject === 'http') return WIZARD_HTTP_PLAN_OPTIONS;
1750
+ if (typeProject === 'postgresql') return WIZARD_DATABASE_PLAN_OPTIONS;
1751
+ if (typeProject === 'mariadb') return WIZARD_DATABASE_PLAN_OPTIONS.filter((value) => value !== 'db-free');
1752
+ return WIZARD_HTTP_PLAN_OPTIONS;
1753
+ }
1754
+
1755
+ function sortWizardOptions(values, preferredOrder) {
1756
+ const order = new Map(preferredOrder.map((value, index) => [value, index]));
1757
+ return [...values].sort((left, right) => (order.get(left) ?? Number.MAX_SAFE_INTEGER) - (order.get(right) ?? Number.MAX_SAFE_INTEGER));
1758
+ }
1759
+
1760
+ function getPlanFeatures(planCatalog, planId) {
1761
+ return Array.isArray(planCatalog)
1762
+ ? planCatalog.find((plan) => plan?.plan === planId)?.features || []
1763
+ : [];
1764
+ }
1765
+
1766
+ function planHasFeature(planCatalog, planId, snippet) {
1767
+ return getPlanFeatures(planCatalog, planId).some((feature) => String(feature).includes(snippet));
1768
+ }
1769
+
1770
+ function isSubdomainCustomizable(planCatalog, planId) {
1771
+ return planHasFeature(planCatalog, planId, 'Sub-domínio HTTP personalizado');
1772
+ }
1773
+
1774
+ function isIpAccessEnabled(planCatalog, planId) {
1775
+ return planHasFeature(planCatalog, planId, 'Bloqueio de IPs');
1776
+ }
1777
+
1778
+ function getMaxReplicasFromFeatures(features) {
1779
+ const replicaFeature = features.find((feature) => /réplicas?/i.test(String(feature)));
1780
+ if (!replicaFeature) return 5;
1781
+ const match = String(replicaFeature).match(/até\s+(\d+)/i) || String(replicaFeature).match(/(\d+)\s+réplicas?/i);
1782
+ return match ? Number(match[1]) : 5;
1783
+ }
1784
+
1785
+ function requireWizardCatalogArray(value, label) {
1786
+ if (!Array.isArray(value)) {
1787
+ throw new CliError(`Falha ao carregar ${label} para o wizard. Tente novamente mais tarde.`);
1788
+ }
1789
+ return value;
1790
+ }
1791
+
1792
+ function requireWizardCatalogObject(value, label) {
1793
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1794
+ throw new CliError(`Falha ao carregar ${label} para o wizard. Tente novamente mais tarde.`);
1795
+ }
1796
+ return value;
1797
+ }
1798
+
1799
+ async function fetchWizardCatalogs(session, flags) {
1800
+ const httpPlans = requireWizardCatalogArray(
1801
+ unwrapData(await request(session, flags, 'GET', '/project/plans')),
1802
+ 'os planos HTTP',
1803
+ );
1804
+ const availableInstances = requireWizardCatalogObject(
1805
+ unwrapData(await request(session, flags, 'GET', '/projects/available')),
1806
+ 'as instancias disponiveis',
1807
+ );
1808
+ const databasePlans = requireWizardCatalogArray(
1809
+ unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
1810
+ 'os planos de banco',
1811
+ );
1812
+
1813
+ return { httpPlans, availableInstances, databasePlans };
1814
+ }
1815
+
1816
+ function normalizePlansCatalogType(value) {
1817
+ const normalized = normalizeFreeText(value || 'all');
1818
+ if (!normalized || normalized === 'all') return 'all';
1819
+ if (normalized === 'http') return 'http';
1820
+ if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
1821
+ if (normalized === 'storage') return 'storage';
1822
+ return null;
1823
+ }
1824
+
1825
+ function formatBrl(value) {
1826
+ const number = Number(value ?? 0);
1827
+ return new Intl.NumberFormat('pt-BR', {
1828
+ style: 'currency',
1829
+ currency: 'BRL',
1830
+ }).format(Number.isFinite(number) ? number : 0);
1831
+ }
1832
+
1833
+ function humanizeStorageName(value) {
1834
+ const raw = String(value || '').trim();
1835
+ if (!raw) return { storage: '-', type: '-' };
1836
+
1837
+ const normalized = normalizeFreeText(raw)
1838
+ .replaceAll('ê', 'e')
1839
+ .replaceAll('_', ' ');
1840
+ const gbMatch = normalized.match(/(\d+)\s*gb/);
1841
+ const size = gbMatch ? `${gbMatch[1]} GB` : raw;
1842
+
1843
+ if (normalized.includes('persistente')) {
1844
+ return { storage: size, type: 'Persistente' };
1845
+ }
1846
+ if (normalized.includes('efemero')) {
1847
+ return { storage: size, type: 'Efemero' };
1848
+ }
1849
+
1850
+ return { storage: raw, type: '-' };
1851
+ }
1852
+
1853
+ async function fetchPlansCatalogs(session, flags, type) {
1854
+ if (type === 'http') {
1855
+ return {
1856
+ http: requireWizardCatalogArray(
1857
+ unwrapData(await request(session, flags, 'GET', '/project/plans', { tokenRequired: false })),
1858
+ 'os planos HTTP',
1859
+ ),
1860
+ database: [],
1861
+ storage: [],
1862
+ };
1863
+ }
1864
+
1865
+ if (type === 'database') {
1866
+ return {
1867
+ http: [],
1868
+ database: requireWizardCatalogArray(
1869
+ unwrapData(await request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false })),
1870
+ 'os planos de banco',
1871
+ ),
1872
+ storage: [],
1873
+ };
1874
+ }
1875
+
1876
+ if (type === 'storage') {
1877
+ return {
1878
+ http: [],
1879
+ database: [],
1880
+ storage: requireWizardCatalogArray(
1881
+ unwrapData(await request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false })),
1882
+ 'os planos de storage',
1883
+ ),
1884
+ };
1885
+ }
1886
+
1887
+ const [httpPayload, databasePayload, storagePayload] = await Promise.all([
1888
+ request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
1889
+ request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
1890
+ request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
1891
+ ]);
1892
+
1893
+ return {
1894
+ http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
1895
+ database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
1896
+ storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
1897
+ };
1898
+ }
1899
+
1900
+ function printPlansCatalogs(payload, type) {
1901
+ if (type === 'all' || type === 'http') {
1902
+ process.stdout.write('HTTP\n');
1903
+ printTable(asArray(payload.http), [
1904
+ { label: 'Plano', value: (plan) => plan.plan || '-' },
1905
+ { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
1906
+ { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
1907
+ { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
1908
+ { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
1909
+ ]);
1910
+ }
1911
+
1912
+ if (type === 'all' || type === 'database') {
1913
+ if (type === 'all') process.stdout.write('\n');
1914
+ process.stdout.write('PostgreSQL / MariaDB\n');
1915
+ printTable(asArray(payload.database), [
1916
+ { label: 'Plano', value: (plan) => plan.plan || '-' },
1917
+ { label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
1918
+ { label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
1919
+ { label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
1920
+ { label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
1921
+ ]);
1922
+ }
1923
+
1924
+ if (type === 'all' || type === 'storage') {
1925
+ if (type === 'all') process.stdout.write('\n');
1926
+ process.stdout.write('Armazenamento\n');
1927
+ printTable(asArray(payload.storage), [
1928
+ { label: 'Storage', value: (storage) => humanizeStorageName(storage.storage).storage },
1929
+ { label: 'Tipo', value: (storage) => humanizeStorageName(storage.storage).type },
1930
+ { label: 'Hora', value: (storage) => formatBrl(storage.prices?.hourly) },
1931
+ { label: 'Mes', value: (storage) => formatBrl(storage.prices?.monthly) },
1932
+ { label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
1933
+ ]);
1934
+ }
1935
+ }
1936
+
1937
+ async function handlePlans(session, flags) {
1938
+ const type = normalizePlansCatalogType(flags.type);
1939
+ if (!type) {
1940
+ throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
1941
+ }
1942
+
1943
+ const payload = await fetchPlansCatalogs(session, flags, type);
1944
+ if (flags.json) return printJson(payload);
1945
+ printPlansCatalogs(payload, type);
1946
+ }
1947
+
1948
+ function formatAllowedValues(values) {
1949
+ return [...values].join(', ');
1950
+ }
1951
+
1952
+ function validateCreateInput({ plan, paymentMode, config }) {
1953
+ const nextPlan = normalizePlan(plan);
1954
+ if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
1955
+ throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
1956
+ }
1957
+
1958
+ const nextPaymentMode = normalizePaymentMode(paymentMode);
1959
+ if (!nextPaymentMode || !ALLOWED_PAYMENT_MODE_VALUES.has(nextPaymentMode)) {
1960
+ throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
1961
+ }
1962
+
1963
+ const nextTypeProject = normalizeTypeProject(config?.type_project);
1964
+ if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
1965
+ throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
1966
+ }
1967
+
1968
+ const nextConfig = { ...config, type_project: nextTypeProject };
1969
+ if (nextTypeProject === 'http' && nextConfig.github) {
1970
+ const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
1971
+ if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
1972
+ throw new CliError(`Runtime invalido: "${nextConfig.github.runtime || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_RUNTIME_VALUES)}. Docs: ${DOCS_RUNTIME_URL}`);
1973
+ }
1974
+ nextConfig.github = { ...nextConfig.github, runtime: nextRuntime };
1975
+ }
1976
+
1977
+ return {
1978
+ plan: nextPlan,
1979
+ paymentMode: nextPaymentMode,
1980
+ config: nextConfig,
1981
+ };
1982
+ }
1983
+
1984
+ function maskWizardSummary(value, key = '') {
1985
+ if (Array.isArray(value)) return value.map((item) => maskWizardSummary(item, key));
1986
+ if (value && typeof value === 'object') {
1987
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
1988
+ entryKey,
1989
+ /token|secret|password|access_key/i.test(entryKey) ? '********' : maskWizardSummary(entryValue, entryKey),
1990
+ ]));
1991
+ }
1992
+ if (typeof value === 'string' && /token|secret|password|access_key/i.test(key)) return '********';
1993
+ return value;
1994
+ }
1995
+
1996
+ function printWizardSummary(payload) {
1997
+ const masked = maskWizardSummary(payload);
1998
+ const lines = [
1999
+ '',
2000
+ tone('Resumo da criacao', 'green'),
2001
+ ` projeto: ${masked.name} | plano: ${masked.plan} | pagamento: ${masked.payment_mode}`,
2002
+ ` tipo: ${masked.config.type_project}`,
2003
+ ];
2004
+
2005
+ if (masked.description) lines.push(` descricao: ${masked.description}`);
2006
+
2007
+ if (masked.config.type_project === 'http') {
2008
+ lines.push(` http: porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
2009
+ if (masked.config.github) {
2010
+ lines.push(` github: ${masked.config.github.repository_owner}/${masked.config.github.repository_name}@${masked.config.github.branch} | runtime ${masked.config.github.runtime}@${masked.config.github.version}`);
2011
+ } else if (masked.config.image) {
2012
+ lines.push(` imagem: ${masked.config.image.url} | publica: ${masked.config.image.is_public ? 'sim' : 'nao'}`);
2013
+ }
2014
+ }
2015
+
2016
+ if (masked.config.type_project === 'postgresql' || masked.config.type_project === 'mariadb') {
2017
+ lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
2018
+ }
2019
+
2020
+ process.stdout.write(`${lines.join('\n')}\n`);
2021
+ }
2022
+
2023
+ async function buildHttpGithubConfig() {
2024
+ const repository_owner = await promptWizardText({
2025
+ label: 'Repository owner',
2026
+ required: true,
2027
+ examples: ['zenifra'],
2028
+ docs: DOCS_RUNTIME_URL,
2029
+ });
2030
+
2031
+ const repository_name = await promptWizardText({
2032
+ label: 'Repository name',
2033
+ required: true,
2034
+ examples: ['zenifra-cli'],
2035
+ docs: DOCS_RUNTIME_URL,
2036
+ });
2037
+
2038
+ const branch = await promptWizardText({
2039
+ label: 'Branch',
2040
+ required: true,
2041
+ examples: ['main'],
2042
+ docs: DOCS_RUNTIME_URL,
2043
+ });
2044
+
2045
+ const runtime = await promptWizardText({
2046
+ label: 'Runtime',
2047
+ required: true,
2048
+ examples: ['nodejs', 'python'],
2049
+ docs: DOCS_RUNTIME_URL,
2050
+ allowedValues: [...ALLOWED_RUNTIME_VALUES],
2051
+ normalize: normalizeRuntime,
2052
+ validate: (value) => (ALLOWED_RUNTIME_VALUES.has(value) ? null : 'use nodejs ou python'),
2053
+ });
2054
+
2055
+ const version = await promptWizardSelect({
2056
+ label: 'Versao do runtime',
2057
+ docs: DOCS_RUNTIME_URL,
2058
+ examples: GITHUB_RUNTIME_VERSIONS[runtime],
2059
+ options: GITHUB_RUNTIME_VERSIONS[runtime].map((value) => ({ value })),
2060
+ });
2061
+
2062
+ const auto_deploy = await promptWizardBoolean({
2063
+ label: 'Auto deploy',
2064
+ docs: DOCS_RUNTIME_URL,
2065
+ examples: ['sim', 'nao'],
2066
+ });
2067
+
2068
+ const start_command = await promptWizardText({
2069
+ label: 'Start command',
2070
+ required: true,
2071
+ examples: runtime === 'nodejs' ? ['npm start'] : ['python main.py'],
2072
+ docs: DOCS_RUNTIME_URL,
2073
+ });
2074
+
2075
+ const preBuildCommand = await promptWizardText({
2076
+ label: 'Pre build command',
2077
+ examples: ['npm run prisma:generate', 'poetry install'],
2078
+ docs: DOCS_RUNTIME_URL,
2079
+ emptyValue: null,
2080
+ });
2081
+
2082
+ const buildCommandRaw = await promptWizardText({
2083
+ label: 'Build command',
2084
+ examples: ['npm run build', 'none'],
2085
+ docs: DOCS_RUNTIME_URL,
2086
+ emptyValue: null,
2087
+ normalize: (value) => parseOptionalNullableCommand(value),
2088
+ });
2089
+
2090
+ return {
2091
+ repository_owner,
2092
+ repository_name,
2093
+ branch,
2094
+ runtime,
2095
+ version,
2096
+ auto_deploy,
2097
+ start_command,
2098
+ pre_build_command: preBuildCommand,
2099
+ build_command: buildCommandRaw,
2100
+ };
2101
+ }
2102
+
2103
+ async function buildHttpImageConfig() {
2104
+ const isPublic = await promptWizardBoolean({
2105
+ label: 'Imagem publica',
2106
+ docs: DOCS_CREATE_HTTP_URL,
2107
+ examples: ['sim', 'nao'],
2108
+ });
2109
+ promptWizardBoolean.ui.state.httpImagePublic = isPublic;
2110
+
2111
+ const image = {
2112
+ url: await promptWizardText({
2113
+ label: 'Image URL',
2114
+ required: true,
2115
+ examples: ['ghcr.io/zenifra/app:1.0.0', 'registry.example.com/team/api:2026-05-30'],
2116
+ docs: DOCS_CREATE_HTTP_URL,
2117
+ }),
2118
+ is_public: isPublic,
2119
+ };
2120
+
2121
+ if (isPublic) return image;
2122
+
2123
+ const authType = await promptWizardSelect({
2124
+ label: 'Tipo de autenticacao da imagem',
2125
+ docs: DOCS_CREATE_HTTP_URL,
2126
+ examples: ['username_password', 'aws'],
2127
+ options: [
2128
+ { value: 'username_password', description: 'usuario e token do registry' },
2129
+ { value: 'aws', description: 'credenciais ECR/registry AWS' },
2130
+ ],
2131
+ });
2132
+ promptWizardSelect.ui.state.httpImageAuthType = authType;
2133
+
2134
+ if (authType === 'username_password') {
2135
+ image.authentication = {
2136
+ auth_type: 'username_password',
2137
+ username: await promptWizardText({
2138
+ label: 'Username do registry',
2139
+ required: true,
2140
+ examples: ['docker-user'],
2141
+ docs: DOCS_CREATE_HTTP_URL,
2142
+ }),
2143
+ token: await promptWizardSecret({
2144
+ label: 'Token do registry',
2145
+ required: true,
2146
+ examples: ['ghp_xxx'],
2147
+ docs: DOCS_CREATE_HTTP_URL,
2148
+ }),
2149
+ };
2150
+ return image;
2151
+ }
2152
+
2153
+ image.authentication = {
2154
+ auth_type: 'aws',
2155
+ aws: {
2156
+ access_key_id: await promptWizardSecret({
2157
+ label: 'AWS access key id',
2158
+ required: true,
2159
+ examples: ['AKIA...'],
2160
+ docs: DOCS_CREATE_HTTP_URL,
2161
+ }),
2162
+ secret_access_key: await promptWizardSecret({
2163
+ label: 'AWS secret access key',
2164
+ required: true,
2165
+ examples: ['secret'],
2166
+ docs: DOCS_CREATE_HTTP_URL,
2167
+ }),
2168
+ region: await promptWizardText({
2169
+ label: 'AWS region',
2170
+ required: true,
2171
+ examples: ['us-east-1'],
2172
+ docs: DOCS_CREATE_HTTP_URL,
2173
+ }),
2174
+ account_id: await promptWizardText({
2175
+ label: 'AWS account id',
2176
+ required: true,
2177
+ examples: ['123456789012'],
2178
+ docs: DOCS_CREATE_HTTP_URL,
2179
+ }),
2180
+ },
2181
+ };
2182
+ return image;
2183
+ }
2184
+
2185
+ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
2186
+ const source = await promptWizardSelect({
2187
+ label: 'Origem do deploy HTTP',
2188
+ docs: DOCS_CREATE_HTTP_URL,
2189
+ examples: ['github', 'oci'],
2190
+ options: [
2191
+ { value: 'github', description: 'build a partir de repositorio GitHub' },
2192
+ { value: 'oci', description: 'imagem OCI pronta' },
2193
+ ],
2194
+ });
2195
+ promptWizardSelect.ui.state.httpSource = source;
2196
+
2197
+ const port = await promptWizardNumber({
2198
+ label: 'Porta da aplicacao',
2199
+ docs: DOCS_CREATE_HTTP_URL,
2200
+ examples: ['3000', '8080'],
2201
+ min: 1,
2202
+ max: 65665,
2203
+ });
2204
+
2205
+ const instances = await promptWizardNumber({
2206
+ label: 'Quantidade de instancias',
2207
+ docs: DOCS_CREATE_HTTP_URL,
2208
+ examples: ['1', '2', '3'],
2209
+ min: 1,
2210
+ max: Math.max(1, Number(availableInstances?.[plan] || 1)),
2211
+ });
2212
+
2213
+ let storage;
2214
+ if (plan === 'free') {
2215
+ storage = { persistent: false, capacity: 1 };
2216
+ promptWizardBoolean.ui.state.httpStoragePersistent = false;
2217
+ } else {
2218
+ const persistent = await promptWizardBoolean({
2219
+ label: 'Storage persistente',
2220
+ docs: DOCS_CONFIGURATION_URL,
2221
+ examples: ['sim', 'nao'],
2222
+ });
2223
+ promptWizardBoolean.ui.state.httpStoragePersistent = persistent;
2224
+
2225
+ storage = {
2226
+ persistent,
2227
+ ...(persistent ? {
2228
+ dir_path_to_persist: await promptWizardText({
2229
+ label: 'Diretorio persistente',
2230
+ required: true,
2231
+ examples: ['/data'],
2232
+ docs: DOCS_CONFIGURATION_URL,
2233
+ }),
2234
+ } : {}),
2235
+ capacity: await promptWizardNumber({
2236
+ label: 'Capacidade de storage (Gi)',
2237
+ docs: DOCS_CONFIGURATION_URL,
2238
+ examples: ['1', '5', '10'],
2239
+ min: 1,
2240
+ max: 250,
2241
+ }),
2242
+ };
2243
+ }
2244
+
2245
+ const envs = await promptWizardPairs({
2246
+ introLabel: 'Deseja adicionar variaveis de ambiente?',
2247
+ docs: DOCS_CONFIGURATION_URL,
2248
+ examples: ['sim', 'nao'],
2249
+ stateKey: 'envs',
2250
+ itemLabels: [
2251
+ { key: 'name', label: 'Nome da env', required: true, examples: ['NODE_ENV'], docs: DOCS_CONFIGURATION_URL },
2252
+ { key: 'value', label: 'Valor da env', required: true, examples: ['production'], docs: DOCS_CONFIGURATION_URL },
2253
+ ],
2254
+ });
2255
+
2256
+ const allowBlockIp = isIpAccessEnabled(httpPlans, plan);
2257
+ const customWhitelist = allowBlockIp
2258
+ ? await promptWizardPairs({
2259
+ introLabel: 'Deseja configurar whitelist personalizada de entrada?',
2260
+ docs: DOCS_CONFIGURATION_URL,
2261
+ examples: ['sim', 'nao'],
2262
+ stateKey: 'whitelist',
2263
+ itemLabels: [
2264
+ { key: 'cidr', label: 'CIDR liberado', required: true, examples: ['10.0.0.0/24'], docs: DOCS_CONFIGURATION_URL },
2265
+ { key: 'description', label: 'Descricao da whitelist', required: true, examples: ['office'], docs: DOCS_CONFIGURATION_URL },
2266
+ ],
2267
+ })
2268
+ : [];
2269
+
2270
+ const ingress_black_list = allowBlockIp
2271
+ ? await promptWizardPairs({
2272
+ introLabel: 'Deseja adicionar blacklist de entrada?',
2273
+ docs: DOCS_CONFIGURATION_URL,
2274
+ examples: ['sim', 'nao'],
2275
+ stateKey: 'blacklist',
2276
+ itemLabels: [
2277
+ { key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['192.168.0.0/24'], docs: DOCS_CONFIGURATION_URL },
2278
+ { key: 'description', label: 'Descricao da blacklist', required: true, examples: ['bloqueio temporario'], docs: DOCS_CONFIGURATION_URL },
2279
+ ],
2280
+ })
2281
+ : [];
2282
+
2283
+ const subdomain = isSubdomainCustomizable(httpPlans, plan)
2284
+ ? await promptWizardText({
2285
+ label: 'Subdomain personalizado',
2286
+ examples: ['minha-api'],
2287
+ docs: DOCS_CONFIGURATION_URL,
2288
+ })
2289
+ : '';
2290
+
2291
+ return {
2292
+ type_project: 'http',
2293
+ ...(source === 'github' ? { github: await buildHttpGithubConfig() } : { image: await buildHttpImageConfig() }),
2294
+ port,
2295
+ instances,
2296
+ storage,
2297
+ envs,
2298
+ ...(subdomain ? { subdomain } : {}),
2299
+ network_access: {
2300
+ ingress_white_list: customWhitelist.length ? customWhitelist : DEFAULT_HTTP_NETWORK_ACCESS.ingress_white_list,
2301
+ ingress_black_list,
2302
+ },
2303
+ };
2304
+ }
2305
+
2306
+ async function buildPostgresqlConfig({ plan, databasePlans }) {
2307
+ const maxReplicas = Math.max(1, getMaxReplicasFromFeatures(getPlanFeatures(databasePlans, plan)));
2308
+ return {
2309
+ type_project: 'postgresql',
2310
+ version: await promptWizardSelect({
2311
+ label: 'Versao do PostgreSQL',
2312
+ docs: DOCS_CREATE_POSTGRESQL_URL,
2313
+ examples: ALLOWED_POSTGRESQL_VERSIONS,
2314
+ options: ALLOWED_POSTGRESQL_VERSIONS.map((value) => ({ value })),
2315
+ }),
2316
+ instances: await promptWizardSelect({
2317
+ label: 'Instancias do PostgreSQL',
2318
+ docs: DOCS_CREATE_POSTGRESQL_URL,
2319
+ examples: ['1', '2', String(maxReplicas)],
2320
+ options: Array.from({ length: maxReplicas }, (_, index) => String(index + 1)).map((value) => ({ value })),
2321
+ }).then((value) => Number(value)),
2322
+ storage: {
2323
+ persistent: true,
2324
+ capacity: plan === 'db-free'
2325
+ ? 1
2326
+ : await promptWizardNumber({
2327
+ label: 'Capacidade de storage do banco (Gi)',
2328
+ docs: DOCS_DATABASE_CONFIGURATION_URL,
2329
+ examples: ['1', '10', '20'],
2330
+ min: 1,
2331
+ max: 250,
2332
+ }),
2333
+ },
2334
+ envs: [],
2335
+ network_access: DEFAULT_HTTP_NETWORK_ACCESS,
2336
+ };
2337
+ }
2338
+
2339
+ async function buildMariadbConfig() {
2340
+ return {
2341
+ type_project: 'mariadb',
2342
+ version: await promptWizardSelect({
2343
+ label: 'Versao do MariaDB',
2344
+ docs: DOCS_CREATE_MARIADB_URL,
2345
+ examples: ALLOWED_MARIADB_VERSIONS,
2346
+ options: ALLOWED_MARIADB_VERSIONS.map((value) => ({ value })),
2347
+ }),
2348
+ instances: 3,
2349
+ storage: {
2350
+ persistent: true,
2351
+ capacity: await promptWizardNumber({
2352
+ label: 'Capacidade de storage do banco (Gi)',
2353
+ docs: DOCS_DATABASE_CONFIGURATION_URL,
2354
+ examples: ['1', '10', '20'],
2355
+ min: 1,
2356
+ max: 250,
2357
+ }),
2358
+ },
2359
+ envs: [],
2360
+ network_access: DEFAULT_HTTP_NETWORK_ACCESS,
2361
+ };
2362
+ }
2363
+
2364
+ async function runProjectCreateWizard(session, flags) {
2365
+ const catalogs = await fetchWizardCatalogs(session, flags);
2366
+ const ui = createWizardUi();
2367
+ promptWizardText.ui = ui;
2368
+ promptWizardSecret.ui = ui;
2369
+ promptWizardBoolean.ui = ui;
2370
+ promptWizardSelect.ui = ui;
2371
+ promptWizardNumber.ui = ui;
2372
+
2373
+ process.stdout.write(`${tone('Wizard interativo de criacao de projeto Zenifra', 'green')}\n`);
2374
+ process.stdout.write(`${toneDim('Digite ? para ajuda detalhada em qualquer campo.')}\n`);
2375
+
2376
+ const name = await promptWizardText({
2377
+ label: 'Nome do projeto',
2378
+ required: true,
2379
+ examples: ['api-web', 'db-app-01'],
2380
+ docs: DOCS_CONFIGURATION_URL,
2381
+ });
2382
+ const description = await promptWizardText({
2383
+ label: 'Descricao',
2384
+ examples: ['API publica da empresa'],
2385
+ docs: DOCS_CONFIGURATION_URL,
2386
+ });
2387
+ const typeProject = await promptWizardSelect({
2388
+ label: 'Tipo do projeto',
2389
+ docs: DOCS_CONFIGURATION_URL,
2390
+ examples: WIZARD_TYPE_PROJECT_OPTIONS,
2391
+ options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
2392
+ });
2393
+ ui.state.typeProject = typeProject;
2394
+ const planOptions = typeProject === 'http'
2395
+ ? sortWizardOptions(catalogs.httpPlans
2396
+ .map((plan) => plan?.plan)
2397
+ .filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
2398
+ : sortWizardOptions(catalogs.databasePlans
2399
+ .map((plan) => plan?.plan)
2400
+ .filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
2401
+ const plan = await promptWizardSelect({
2402
+ label: 'Plano',
2403
+ docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
2404
+ examples: [planOptions[0], planOptions.at(-1)],
2405
+ options: planOptions.map((value) => ({ value })),
2406
+ });
2407
+ ui.state.plan = plan;
2408
+ if (typeProject === 'http') {
2409
+ ui.state.planAllowsBlockIp = isIpAccessEnabled(catalogs.httpPlans, plan);
2410
+ ui.state.planAllowsSubdomain = isSubdomainCustomizable(catalogs.httpPlans, plan);
2411
+ }
2412
+ const paymentMode = await promptWizardSelect({
2413
+ label: 'Modo de pagamento',
2414
+ docs: DOCS_PAYMENTS_URL,
2415
+ examples: WIZARD_PAYMENT_MODE_OPTIONS,
2416
+ options: WIZARD_PAYMENT_MODE_OPTIONS.map((value) => ({ value })),
2417
+ });
2418
+
2419
+ let config;
2420
+ if (typeProject === 'http') {
2421
+ config = await buildHttpConfig({
2422
+ plan,
2423
+ httpPlans: catalogs.httpPlans,
2424
+ availableInstances: catalogs.availableInstances,
2425
+ });
2426
+ } else if (typeProject === 'postgresql') {
2427
+ config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
2428
+ } else {
2429
+ config = await buildMariadbConfig();
2430
+ }
2431
+
2432
+ const payload = {
2433
+ name,
2434
+ ...(description ? { description } : {}),
2435
+ plan,
2436
+ payment_mode: paymentMode,
2437
+ config,
2438
+ };
2439
+
2440
+ printWizardSummary(payload);
2441
+ const confirmed = await promptWizardBoolean({
2442
+ label: 'Confirmar criacao do projeto',
2443
+ docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
2444
+ examples: ['sim', 'nao'],
2445
+ });
2446
+ if (!confirmed) return null;
2447
+ return payload;
2448
+ }
2449
+
796
2450
  async function handleProjectCreate(session, flags) {
797
2451
  const orgId = await resolveOrgId(session, flags);
798
- const name = flags.name || await prompt('Nome do projeto');
799
- const plan = flags.plan || await prompt('Plano', 'free');
800
- const paymentMode = flags.paymentMode || await prompt('Modo de pagamento', 'hourly');
801
- const config = await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
2452
+ const wizardPayload = hasWizardCreateInput(flags) ? await runProjectCreateWizard(session, flags) : null;
2453
+
2454
+ if (wizardPayload === null && hasWizardCreateInput(flags)) {
2455
+ process.stdout.write('Operacao cancelada pelo usuario.\n');
2456
+ return;
2457
+ }
2458
+
2459
+ const name = wizardPayload?.name || flags.name || await prompt('Nome do projeto');
2460
+ const description = wizardPayload?.description || flags.description;
2461
+ const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
2462
+ const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
2463
+ const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
2464
+ const validated = validateCreateInput({ plan, paymentMode, config });
802
2465
 
803
2466
  const payload = await request(session, flags, 'POST', '/project', {
804
2467
  orgId,
805
2468
  body: {
806
2469
  name,
807
- ...(flags.description ? { description: String(flags.description) } : {}),
808
- plan,
809
- payment_mode: paymentMode,
810
- config,
2470
+ ...(description ? { description: String(description) } : {}),
2471
+ plan: validated.plan,
2472
+ payment_mode: validated.paymentMode,
2473
+ config: validated.config,
811
2474
  },
812
2475
  });
813
2476
 
814
2477
  if (flags.json) return printJson(payload);
2478
+ if (wizardPayload) {
2479
+ closePromptResources();
2480
+ }
815
2481
  const project = unwrapData(payload);
816
- process.stdout.write(`Projeto criado: ${projectIdOf(project) || 'id indisponivel'}\n`);
817
- if (project.domain) process.stdout.write(`Dominio: ${project.domain}\n`);
818
- if (project.api_key) process.stdout.write(`API key: ${project.api_key}\n`);
2482
+ const rows = [
2483
+ { field: 'Projeto', value: projectIdOf(project) || 'id indisponivel' },
2484
+ ...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
2485
+ ...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
2486
+ ];
2487
+ process.stdout.write('\n');
2488
+ printTable(rows, [
2489
+ { label: 'Campo', value: (row) => row.field },
2490
+ { label: 'Valor', value: (row) => row.value },
2491
+ ]);
819
2492
  }
820
2493
 
821
2494
  async function getProject(session, flags, projectId, orgId) {
@@ -1083,49 +2756,68 @@ async function main() {
1083
2756
  const { positional, flags } = parseArgs(process.argv.slice(2));
1084
2757
  const [command, subcommand] = positional;
1085
2758
  const session = await readSession();
2759
+ try {
2760
+ if (isRemovedProjectsCreate(positional)) {
2761
+ throw new CliError(removedProjectsCreateMessage());
2762
+ }
1086
2763
 
1087
- if (command === 'help') {
1088
- process.stdout.write(commandHelp(positional.slice(1)));
1089
- return;
1090
- }
2764
+ if (command === 'help') {
2765
+ process.stdout.write(commandHelp(positional.slice(1)));
2766
+ return;
2767
+ }
1091
2768
 
1092
- if (!command) {
1093
- process.stdout.write(usage());
1094
- return;
1095
- }
2769
+ if (!command) {
2770
+ process.stdout.write(usage());
2771
+ return;
2772
+ }
1096
2773
 
1097
- if (flags.help) {
1098
- process.stdout.write(commandHelp(positional));
1099
- return;
1100
- }
2774
+ if (flags.help) {
2775
+ process.stdout.write(commandHelp(positional));
2776
+ return;
2777
+ }
1101
2778
 
1102
- if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
1103
- if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
1104
- if (command === 'auth' && subcommand === 'logout') return handleLogout();
1105
- if (command === 'login') return handleLogin(session, flags);
1106
- if (command === 'logout') return handleLogout();
1107
- if (command === 'orgs') return handleOrgs(session, flags);
1108
- if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
1109
- if (command === 'projects' && subcommand === 'create') return handleProjectCreate(session, flags);
1110
- if (command === 'projects') return handleProjects(session, flags);
1111
- if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
1112
- if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
1113
- if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
1114
- if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
1115
- if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
1116
- if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
1117
- if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
1118
- if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
1119
- if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
1120
- if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
1121
- if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
1122
- if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
1123
- if (command === 'builds') return handleDeployments(session, flags);
1124
- if (command === 'deployments') return handleDeployments(session, flags);
1125
- if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
1126
- if (command === 'deploy') return handleDeploy(session, flags);
1127
-
1128
- throw new CliError(`Comando desconhecido: ${positional.join(' ')}`);
2779
+ if (isNamespaceCommand(command) && !subcommand) {
2780
+ process.stdout.write(commandHelp([command]));
2781
+ return;
2782
+ }
2783
+
2784
+ if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
2785
+ if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
2786
+ if (command === 'auth' && subcommand === 'logout') return handleLogout(session, flags);
2787
+ if (command === 'profile' && subcommand === 'list') return handleProfileList(session, flags);
2788
+ if (command === 'profile' && subcommand === 'show') return handleProfileShow(session, flags, positional);
2789
+ if (command === 'profile' && subcommand === 'add') return handleProfileAdd(session, flags);
2790
+ if (command === 'profile' && subcommand === 'edit') return handleProfileEdit(session, flags, positional);
2791
+ if (command === 'profile' && subcommand === 'use') return handleProfileUse(session, flags, positional);
2792
+ if (command === 'profile' && subcommand === 'remove') return handleProfileRemove(session, flags, positional);
2793
+ if (command === 'login') return handleLogin(session, flags);
2794
+ if (command === 'logout') return handleLogout(session, flags);
2795
+ if (command === 'plans') return handlePlans(session, flags);
2796
+ if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
2797
+ if (command === 'orgs') return handleOrgs(session, flags);
2798
+ if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
2799
+ if (command === 'projects') return handleProjects(session, flags);
2800
+ if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
2801
+ if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
2802
+ if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
2803
+ if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
2804
+ if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
2805
+ if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
2806
+ if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
2807
+ if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
2808
+ if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
2809
+ if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
2810
+ if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
2811
+ if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
2812
+ if (command === 'builds') return handleDeployments(session, flags);
2813
+ if (command === 'deployments') return handleDeployments(session, flags);
2814
+ if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
2815
+ if (command === 'deploy') return handleDeploy(session, flags);
2816
+
2817
+ throw new CliError(`Comando desconhecido: ${positional.join(' ')}`);
2818
+ } finally {
2819
+ closePromptResources();
2820
+ }
1129
2821
  }
1130
2822
 
1131
2823
  main().catch((error) => {