@zenifra/cli 0.1.0 → 0.2.2
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/README.md +101 -11
- package/bin/zenifra.mjs +1815 -114
- package/examples/http-github-project.json +35 -0
- package/examples/http-project.json +4 -5
- package/examples/mariadb-project.json +19 -0
- package/examples/postgresql-project.json +19 -0
- package/package.json +3 -3
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>]
|
|
32
|
-
zenifra
|
|
33
|
-
zenifra
|
|
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]
|
|
111
|
+
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]
|
|
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
|
|
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
|
|
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
|
|
77
|
-
flags: ['--key <znf_key>
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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,23 +261,41 @@ 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
|
-
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>]',
|
|
275
|
+
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
|
|
109
276
|
description: 'Lista projetos da organizacao ativa ou da API key.',
|
|
110
|
-
flags: ['--json
|
|
111
|
-
examples: ['zenifra projects --type http', 'zenifra projects --json'],
|
|
277
|
+
flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Padrao: 15.'],
|
|
278
|
+
examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
|
|
112
279
|
output: 'ID Nome Status Plano Tipo\n507f1f77bcf86cd799439012 api-web running free http',
|
|
113
|
-
jsonOutput: '[{"id":"507f1f77bcf86cd799439012","name":"api-web","status":"running","type_project":"http"}]',
|
|
280
|
+
jsonOutput: '{"projects":[{"id":"507f1f77bcf86cd799439012","name":"api-web","status":"running","type_project":"http"}],"pagination":{"page":1,"limit":15,"total":1,"pages":1}}',
|
|
114
281
|
},
|
|
115
282
|
{
|
|
116
|
-
command: '
|
|
117
|
-
usage: 'zenifra
|
|
118
|
-
description: 'Cria um projeto
|
|
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
|
|
121
|
-
output: '
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
...
|
|
1416
|
+
...targetSession,
|
|
704
1417
|
accessToken,
|
|
705
1418
|
apiKey: undefined,
|
|
706
|
-
|
|
707
|
-
|
|
1419
|
+
authMode: 'access_token',
|
|
1420
|
+
apiBaseUrl: apiBaseUrl(targetSession, flags),
|
|
1421
|
+
selectedOrganizationId: targetSession.selectedOrganizationId,
|
|
708
1422
|
updatedAt: new Date().toISOString(),
|
|
709
1423
|
};
|
|
710
1424
|
|
|
711
|
-
|
|
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
|
-
...
|
|
1440
|
+
...targetSession,
|
|
724
1441
|
accessToken: undefined,
|
|
725
1442
|
selectedOrganizationId: undefined,
|
|
726
1443
|
apiKey,
|
|
727
|
-
|
|
1444
|
+
authMode: 'api_key',
|
|
1445
|
+
apiBaseUrl: apiBaseUrl(targetSession, flags),
|
|
728
1446
|
updatedAt: new Date().toISOString(),
|
|
729
1447
|
};
|
|
730
1448
|
|
|
731
|
-
|
|
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
|
-
|
|
737
|
-
|
|
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
|
|
1628
|
+
await persistSession(session);
|
|
757
1629
|
process.stdout.write(`Organizacao ativa: ${session.selectedOrganizationId}\n`);
|
|
758
1630
|
return;
|
|
759
1631
|
}
|
|
@@ -764,7 +1636,12 @@ async function handleOrgSet(session, flags) {
|
|
|
764
1636
|
|
|
765
1637
|
async function handleProjects(session, flags) {
|
|
766
1638
|
const orgId = await resolveOrgId(session, flags);
|
|
767
|
-
const
|
|
1639
|
+
const projectFlags = {
|
|
1640
|
+
...flags,
|
|
1641
|
+
page: flags.page ?? 1,
|
|
1642
|
+
limit: flags.limit ?? 15,
|
|
1643
|
+
};
|
|
1644
|
+
const query = buildQuery(projectFlags, ['type', 'page', 'limit']);
|
|
768
1645
|
const data = unwrapData(await request(session, flags, 'GET', `/project${query}`, { orgId }));
|
|
769
1646
|
const projects = asArray(data);
|
|
770
1647
|
|
|
@@ -776,6 +1653,10 @@ async function handleProjects(session, flags) {
|
|
|
776
1653
|
{ label: 'Plano', value: (project) => project.plan || '-' },
|
|
777
1654
|
{ label: 'Tipo', value: (project) => project.type_project || project.config?.type_project || '-' },
|
|
778
1655
|
]);
|
|
1656
|
+
if (data?.pagination) {
|
|
1657
|
+
const { page = 1, pages = 1, total = projects.length } = data.pagination;
|
|
1658
|
+
process.stdout.write(`\nPagina ${page} de ${Math.max(pages, 1)} (${total} projeto(s))\n`);
|
|
1659
|
+
}
|
|
779
1660
|
}
|
|
780
1661
|
|
|
781
1662
|
async function parseConfig(input) {
|
|
@@ -793,29 +1674,830 @@ async function parseConfig(input) {
|
|
|
793
1674
|
}
|
|
794
1675
|
}
|
|
795
1676
|
|
|
1677
|
+
function normalizeFreeText(value) {
|
|
1678
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function normalizePlan(value) {
|
|
1682
|
+
const raw = normalizeFreeText(value);
|
|
1683
|
+
if (!raw) return '';
|
|
1684
|
+
|
|
1685
|
+
const normalized = raw
|
|
1686
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
1687
|
+
.replace(/^_+|_+$/g, '');
|
|
1688
|
+
|
|
1689
|
+
const aliases = new Map([
|
|
1690
|
+
['premium_plus', 'premium_plus'],
|
|
1691
|
+
['premiumplus', 'premium_plus'],
|
|
1692
|
+
['deep_learning_basic', 'deep_learning_basic'],
|
|
1693
|
+
['deeplearningbasic', 'deep_learning_basic'],
|
|
1694
|
+
['deep_learning_premium', 'deep_learning_premium'],
|
|
1695
|
+
['deeplearningpremium', 'deep_learning_premium'],
|
|
1696
|
+
['db_free', 'db-free'],
|
|
1697
|
+
['dbfree', 'db-free'],
|
|
1698
|
+
['db_starter', 'db-starter'],
|
|
1699
|
+
['dbstarter', 'db-starter'],
|
|
1700
|
+
['db_basic', 'db-basic'],
|
|
1701
|
+
['dbbasic', 'db-basic'],
|
|
1702
|
+
['db_premium', 'db-premium'],
|
|
1703
|
+
['dbpremium', 'db-premium'],
|
|
1704
|
+
['db_enterprise', 'db-enterprise'],
|
|
1705
|
+
['dbenterprise', 'db-enterprise'],
|
|
1706
|
+
]);
|
|
1707
|
+
|
|
1708
|
+
if (aliases.has(normalized)) return aliases.get(normalized);
|
|
1709
|
+
if (ALLOWED_PLAN_VALUES.has(normalized)) return normalized;
|
|
1710
|
+
return normalized;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function normalizePaymentMode(value) {
|
|
1714
|
+
const raw = normalizeFreeText(value);
|
|
1715
|
+
const aliases = new Map([
|
|
1716
|
+
['hourly', 'hourly'],
|
|
1717
|
+
['por hora', 'hourly'],
|
|
1718
|
+
['hora', 'hourly'],
|
|
1719
|
+
['monthly', 'monthly'],
|
|
1720
|
+
['month', 'monthly'],
|
|
1721
|
+
['mensal', 'monthly'],
|
|
1722
|
+
['por mes', 'monthly'],
|
|
1723
|
+
['por mês', 'monthly'],
|
|
1724
|
+
['yearly', 'yearly'],
|
|
1725
|
+
['annual', 'yearly'],
|
|
1726
|
+
['anual', 'yearly'],
|
|
1727
|
+
['por ano', 'yearly'],
|
|
1728
|
+
]);
|
|
1729
|
+
return aliases.get(raw) || raw;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function normalizeTypeProject(value) {
|
|
1733
|
+
const raw = normalizeFreeText(value);
|
|
1734
|
+
const aliases = new Map([
|
|
1735
|
+
['http', 'http'],
|
|
1736
|
+
['api', 'http'],
|
|
1737
|
+
['site', 'http'],
|
|
1738
|
+
['postgres', 'postgresql'],
|
|
1739
|
+
['postgresql', 'postgresql'],
|
|
1740
|
+
['mariadb', 'mariadb'],
|
|
1741
|
+
['maria db', 'mariadb'],
|
|
1742
|
+
]);
|
|
1743
|
+
return aliases.get(raw) || raw;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function normalizeRuntime(value) {
|
|
1747
|
+
const raw = normalizeFreeText(value);
|
|
1748
|
+
const aliases = new Map([
|
|
1749
|
+
['node', 'nodejs'],
|
|
1750
|
+
['nodejs', 'nodejs'],
|
|
1751
|
+
['node.js', 'nodejs'],
|
|
1752
|
+
['python', 'python'],
|
|
1753
|
+
]);
|
|
1754
|
+
return aliases.get(raw) || raw;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function wizardPlanOptionsForTypeProject(typeProject) {
|
|
1758
|
+
if (typeProject === 'http') return WIZARD_HTTP_PLAN_OPTIONS;
|
|
1759
|
+
if (typeProject === 'postgresql') return WIZARD_DATABASE_PLAN_OPTIONS;
|
|
1760
|
+
if (typeProject === 'mariadb') return WIZARD_DATABASE_PLAN_OPTIONS.filter((value) => value !== 'db-free');
|
|
1761
|
+
return WIZARD_HTTP_PLAN_OPTIONS;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function sortWizardOptions(values, preferredOrder) {
|
|
1765
|
+
const order = new Map(preferredOrder.map((value, index) => [value, index]));
|
|
1766
|
+
return [...values].sort((left, right) => (order.get(left) ?? Number.MAX_SAFE_INTEGER) - (order.get(right) ?? Number.MAX_SAFE_INTEGER));
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function getPlanFeatures(planCatalog, planId) {
|
|
1770
|
+
return Array.isArray(planCatalog)
|
|
1771
|
+
? planCatalog.find((plan) => plan?.plan === planId)?.features || []
|
|
1772
|
+
: [];
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function planHasFeature(planCatalog, planId, snippet) {
|
|
1776
|
+
return getPlanFeatures(planCatalog, planId).some((feature) => String(feature).includes(snippet));
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function isSubdomainCustomizable(planCatalog, planId) {
|
|
1780
|
+
return planHasFeature(planCatalog, planId, 'Sub-domínio HTTP personalizado');
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function isIpAccessEnabled(planCatalog, planId) {
|
|
1784
|
+
return planHasFeature(planCatalog, planId, 'Bloqueio de IPs');
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function getMaxReplicasFromFeatures(features) {
|
|
1788
|
+
const replicaFeature = features.find((feature) => /réplicas?/i.test(String(feature)));
|
|
1789
|
+
if (!replicaFeature) return 5;
|
|
1790
|
+
const match = String(replicaFeature).match(/até\s+(\d+)/i) || String(replicaFeature).match(/(\d+)\s+réplicas?/i);
|
|
1791
|
+
return match ? Number(match[1]) : 5;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function requireWizardCatalogArray(value, label) {
|
|
1795
|
+
if (!Array.isArray(value)) {
|
|
1796
|
+
throw new CliError(`Falha ao carregar ${label} para o wizard. Tente novamente mais tarde.`);
|
|
1797
|
+
}
|
|
1798
|
+
return value;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function requireWizardCatalogObject(value, label) {
|
|
1802
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1803
|
+
throw new CliError(`Falha ao carregar ${label} para o wizard. Tente novamente mais tarde.`);
|
|
1804
|
+
}
|
|
1805
|
+
return value;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async function fetchWizardCatalogs(session, flags) {
|
|
1809
|
+
const httpPlans = requireWizardCatalogArray(
|
|
1810
|
+
unwrapData(await request(session, flags, 'GET', '/project/plans')),
|
|
1811
|
+
'os planos HTTP',
|
|
1812
|
+
);
|
|
1813
|
+
const availableInstances = requireWizardCatalogObject(
|
|
1814
|
+
unwrapData(await request(session, flags, 'GET', '/projects/available')),
|
|
1815
|
+
'as instancias disponiveis',
|
|
1816
|
+
);
|
|
1817
|
+
const databasePlans = requireWizardCatalogArray(
|
|
1818
|
+
unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
|
|
1819
|
+
'os planos de banco',
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
return { httpPlans, availableInstances, databasePlans };
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function normalizePlansCatalogType(value) {
|
|
1826
|
+
const normalized = normalizeFreeText(value || 'all');
|
|
1827
|
+
if (!normalized || normalized === 'all') return 'all';
|
|
1828
|
+
if (normalized === 'http') return 'http';
|
|
1829
|
+
if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
|
|
1830
|
+
if (normalized === 'storage') return 'storage';
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function formatBrl(value) {
|
|
1835
|
+
const number = Number(value ?? 0);
|
|
1836
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
1837
|
+
style: 'currency',
|
|
1838
|
+
currency: 'BRL',
|
|
1839
|
+
}).format(Number.isFinite(number) ? number : 0);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function humanizeStorageName(value) {
|
|
1843
|
+
const raw = String(value || '').trim();
|
|
1844
|
+
if (!raw) return { storage: '-', type: '-' };
|
|
1845
|
+
|
|
1846
|
+
const normalized = normalizeFreeText(raw)
|
|
1847
|
+
.replaceAll('ê', 'e')
|
|
1848
|
+
.replaceAll('_', ' ');
|
|
1849
|
+
const gbMatch = normalized.match(/(\d+)\s*gb/);
|
|
1850
|
+
const size = gbMatch ? `${gbMatch[1]} GB` : raw;
|
|
1851
|
+
|
|
1852
|
+
if (normalized.includes('persistente')) {
|
|
1853
|
+
return { storage: size, type: 'Persistente' };
|
|
1854
|
+
}
|
|
1855
|
+
if (normalized.includes('efemero')) {
|
|
1856
|
+
return { storage: size, type: 'Efemero' };
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
return { storage: raw, type: '-' };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
async function fetchPlansCatalogs(session, flags, type) {
|
|
1863
|
+
if (type === 'http') {
|
|
1864
|
+
return {
|
|
1865
|
+
http: requireWizardCatalogArray(
|
|
1866
|
+
unwrapData(await request(session, flags, 'GET', '/project/plans', { tokenRequired: false })),
|
|
1867
|
+
'os planos HTTP',
|
|
1868
|
+
),
|
|
1869
|
+
database: [],
|
|
1870
|
+
storage: [],
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (type === 'database') {
|
|
1875
|
+
return {
|
|
1876
|
+
http: [],
|
|
1877
|
+
database: requireWizardCatalogArray(
|
|
1878
|
+
unwrapData(await request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false })),
|
|
1879
|
+
'os planos de banco',
|
|
1880
|
+
),
|
|
1881
|
+
storage: [],
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
if (type === 'storage') {
|
|
1886
|
+
return {
|
|
1887
|
+
http: [],
|
|
1888
|
+
database: [],
|
|
1889
|
+
storage: requireWizardCatalogArray(
|
|
1890
|
+
unwrapData(await request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false })),
|
|
1891
|
+
'os planos de storage',
|
|
1892
|
+
),
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const [httpPayload, databasePayload, storagePayload] = await Promise.all([
|
|
1897
|
+
request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
|
|
1898
|
+
request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
|
|
1899
|
+
request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
|
|
1900
|
+
]);
|
|
1901
|
+
|
|
1902
|
+
return {
|
|
1903
|
+
http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
|
|
1904
|
+
database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
|
|
1905
|
+
storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function printPlansCatalogs(payload, type) {
|
|
1910
|
+
if (type === 'all' || type === 'http') {
|
|
1911
|
+
process.stdout.write('HTTP\n');
|
|
1912
|
+
printTable(asArray(payload.http), [
|
|
1913
|
+
{ label: 'Plano', value: (plan) => plan.plan || '-' },
|
|
1914
|
+
{ label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
|
|
1915
|
+
{ label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
|
|
1916
|
+
{ label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
|
|
1917
|
+
{ label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
|
|
1918
|
+
]);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
if (type === 'all' || type === 'database') {
|
|
1922
|
+
if (type === 'all') process.stdout.write('\n');
|
|
1923
|
+
process.stdout.write('PostgreSQL / MariaDB\n');
|
|
1924
|
+
printTable(asArray(payload.database), [
|
|
1925
|
+
{ label: 'Plano', value: (plan) => plan.plan || '-' },
|
|
1926
|
+
{ label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly) },
|
|
1927
|
+
{ label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly) },
|
|
1928
|
+
{ label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly) },
|
|
1929
|
+
{ label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
|
|
1930
|
+
]);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (type === 'all' || type === 'storage') {
|
|
1934
|
+
if (type === 'all') process.stdout.write('\n');
|
|
1935
|
+
process.stdout.write('Armazenamento\n');
|
|
1936
|
+
printTable(asArray(payload.storage), [
|
|
1937
|
+
{ label: 'Storage', value: (storage) => humanizeStorageName(storage.storage).storage },
|
|
1938
|
+
{ label: 'Tipo', value: (storage) => humanizeStorageName(storage.storage).type },
|
|
1939
|
+
{ label: 'Hora', value: (storage) => formatBrl(storage.prices?.hourly) },
|
|
1940
|
+
{ label: 'Mes', value: (storage) => formatBrl(storage.prices?.monthly) },
|
|
1941
|
+
{ label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
|
|
1942
|
+
]);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
async function handlePlans(session, flags) {
|
|
1947
|
+
const type = normalizePlansCatalogType(flags.type);
|
|
1948
|
+
if (!type) {
|
|
1949
|
+
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
const payload = await fetchPlansCatalogs(session, flags, type);
|
|
1953
|
+
if (flags.json) return printJson(payload);
|
|
1954
|
+
printPlansCatalogs(payload, type);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function formatAllowedValues(values) {
|
|
1958
|
+
return [...values].join(', ');
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function validateCreateInput({ plan, paymentMode, config }) {
|
|
1962
|
+
const nextPlan = normalizePlan(plan);
|
|
1963
|
+
if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
|
|
1964
|
+
throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const nextPaymentMode = normalizePaymentMode(paymentMode);
|
|
1968
|
+
if (!nextPaymentMode || !ALLOWED_PAYMENT_MODE_VALUES.has(nextPaymentMode)) {
|
|
1969
|
+
throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
1973
|
+
if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
|
|
1974
|
+
throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
const nextConfig = { ...config, type_project: nextTypeProject };
|
|
1978
|
+
if (nextTypeProject === 'http' && nextConfig.github) {
|
|
1979
|
+
const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
|
|
1980
|
+
if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
|
|
1981
|
+
throw new CliError(`Runtime invalido: "${nextConfig.github.runtime || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_RUNTIME_VALUES)}. Docs: ${DOCS_RUNTIME_URL}`);
|
|
1982
|
+
}
|
|
1983
|
+
nextConfig.github = { ...nextConfig.github, runtime: nextRuntime };
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
return {
|
|
1987
|
+
plan: nextPlan,
|
|
1988
|
+
paymentMode: nextPaymentMode,
|
|
1989
|
+
config: nextConfig,
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function maskWizardSummary(value, key = '') {
|
|
1994
|
+
if (Array.isArray(value)) return value.map((item) => maskWizardSummary(item, key));
|
|
1995
|
+
if (value && typeof value === 'object') {
|
|
1996
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
1997
|
+
entryKey,
|
|
1998
|
+
/token|secret|password|access_key/i.test(entryKey) ? '********' : maskWizardSummary(entryValue, entryKey),
|
|
1999
|
+
]));
|
|
2000
|
+
}
|
|
2001
|
+
if (typeof value === 'string' && /token|secret|password|access_key/i.test(key)) return '********';
|
|
2002
|
+
return value;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function printWizardSummary(payload) {
|
|
2006
|
+
const masked = maskWizardSummary(payload);
|
|
2007
|
+
const lines = [
|
|
2008
|
+
'',
|
|
2009
|
+
tone('Resumo da criacao', 'green'),
|
|
2010
|
+
` projeto: ${masked.name} | plano: ${masked.plan} | pagamento: ${masked.payment_mode}`,
|
|
2011
|
+
` tipo: ${masked.config.type_project}`,
|
|
2012
|
+
];
|
|
2013
|
+
|
|
2014
|
+
if (masked.description) lines.push(` descricao: ${masked.description}`);
|
|
2015
|
+
|
|
2016
|
+
if (masked.config.type_project === 'http') {
|
|
2017
|
+
lines.push(` http: porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
2018
|
+
if (masked.config.github) {
|
|
2019
|
+
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}`);
|
|
2020
|
+
} else if (masked.config.image) {
|
|
2021
|
+
lines.push(` imagem: ${masked.config.image.url} | publica: ${masked.config.image.is_public ? 'sim' : 'nao'}`);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
if (masked.config.type_project === 'postgresql' || masked.config.type_project === 'mariadb') {
|
|
2026
|
+
lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
async function buildHttpGithubConfig() {
|
|
2033
|
+
const repository_owner = await promptWizardText({
|
|
2034
|
+
label: 'Repository owner',
|
|
2035
|
+
required: true,
|
|
2036
|
+
examples: ['zenifra'],
|
|
2037
|
+
docs: DOCS_RUNTIME_URL,
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
const repository_name = await promptWizardText({
|
|
2041
|
+
label: 'Repository name',
|
|
2042
|
+
required: true,
|
|
2043
|
+
examples: ['zenifra-cli'],
|
|
2044
|
+
docs: DOCS_RUNTIME_URL,
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
const branch = await promptWizardText({
|
|
2048
|
+
label: 'Branch',
|
|
2049
|
+
required: true,
|
|
2050
|
+
examples: ['main'],
|
|
2051
|
+
docs: DOCS_RUNTIME_URL,
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
const runtime = await promptWizardText({
|
|
2055
|
+
label: 'Runtime',
|
|
2056
|
+
required: true,
|
|
2057
|
+
examples: ['nodejs', 'python'],
|
|
2058
|
+
docs: DOCS_RUNTIME_URL,
|
|
2059
|
+
allowedValues: [...ALLOWED_RUNTIME_VALUES],
|
|
2060
|
+
normalize: normalizeRuntime,
|
|
2061
|
+
validate: (value) => (ALLOWED_RUNTIME_VALUES.has(value) ? null : 'use nodejs ou python'),
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
const version = await promptWizardSelect({
|
|
2065
|
+
label: 'Versao do runtime',
|
|
2066
|
+
docs: DOCS_RUNTIME_URL,
|
|
2067
|
+
examples: GITHUB_RUNTIME_VERSIONS[runtime],
|
|
2068
|
+
options: GITHUB_RUNTIME_VERSIONS[runtime].map((value) => ({ value })),
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
const auto_deploy = await promptWizardBoolean({
|
|
2072
|
+
label: 'Auto deploy',
|
|
2073
|
+
docs: DOCS_RUNTIME_URL,
|
|
2074
|
+
examples: ['sim', 'nao'],
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
const start_command = await promptWizardText({
|
|
2078
|
+
label: 'Start command',
|
|
2079
|
+
required: true,
|
|
2080
|
+
examples: runtime === 'nodejs' ? ['npm start'] : ['python main.py'],
|
|
2081
|
+
docs: DOCS_RUNTIME_URL,
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
const preBuildCommand = await promptWizardText({
|
|
2085
|
+
label: 'Pre build command',
|
|
2086
|
+
examples: ['npm run prisma:generate', 'poetry install'],
|
|
2087
|
+
docs: DOCS_RUNTIME_URL,
|
|
2088
|
+
emptyValue: null,
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
const buildCommandRaw = await promptWizardText({
|
|
2092
|
+
label: 'Build command',
|
|
2093
|
+
examples: ['npm run build', 'none'],
|
|
2094
|
+
docs: DOCS_RUNTIME_URL,
|
|
2095
|
+
emptyValue: null,
|
|
2096
|
+
normalize: (value) => parseOptionalNullableCommand(value),
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
return {
|
|
2100
|
+
repository_owner,
|
|
2101
|
+
repository_name,
|
|
2102
|
+
branch,
|
|
2103
|
+
runtime,
|
|
2104
|
+
version,
|
|
2105
|
+
auto_deploy,
|
|
2106
|
+
start_command,
|
|
2107
|
+
pre_build_command: preBuildCommand,
|
|
2108
|
+
build_command: buildCommandRaw,
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
async function buildHttpImageConfig() {
|
|
2113
|
+
const isPublic = await promptWizardBoolean({
|
|
2114
|
+
label: 'Imagem publica',
|
|
2115
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2116
|
+
examples: ['sim', 'nao'],
|
|
2117
|
+
});
|
|
2118
|
+
promptWizardBoolean.ui.state.httpImagePublic = isPublic;
|
|
2119
|
+
|
|
2120
|
+
const image = {
|
|
2121
|
+
url: await promptWizardText({
|
|
2122
|
+
label: 'Image URL',
|
|
2123
|
+
required: true,
|
|
2124
|
+
examples: ['ghcr.io/zenifra/app:1.0.0', 'registry.example.com/team/api:2026-05-30'],
|
|
2125
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2126
|
+
}),
|
|
2127
|
+
is_public: isPublic,
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
if (isPublic) return image;
|
|
2131
|
+
|
|
2132
|
+
const authType = await promptWizardSelect({
|
|
2133
|
+
label: 'Tipo de autenticacao da imagem',
|
|
2134
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2135
|
+
examples: ['username_password', 'aws'],
|
|
2136
|
+
options: [
|
|
2137
|
+
{ value: 'username_password', description: 'usuario e token do registry' },
|
|
2138
|
+
{ value: 'aws', description: 'credenciais ECR/registry AWS' },
|
|
2139
|
+
],
|
|
2140
|
+
});
|
|
2141
|
+
promptWizardSelect.ui.state.httpImageAuthType = authType;
|
|
2142
|
+
|
|
2143
|
+
if (authType === 'username_password') {
|
|
2144
|
+
image.authentication = {
|
|
2145
|
+
auth_type: 'username_password',
|
|
2146
|
+
username: await promptWizardText({
|
|
2147
|
+
label: 'Username do registry',
|
|
2148
|
+
required: true,
|
|
2149
|
+
examples: ['docker-user'],
|
|
2150
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2151
|
+
}),
|
|
2152
|
+
token: await promptWizardSecret({
|
|
2153
|
+
label: 'Token do registry',
|
|
2154
|
+
required: true,
|
|
2155
|
+
examples: ['ghp_xxx'],
|
|
2156
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2157
|
+
}),
|
|
2158
|
+
};
|
|
2159
|
+
return image;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
image.authentication = {
|
|
2163
|
+
auth_type: 'aws',
|
|
2164
|
+
aws: {
|
|
2165
|
+
access_key_id: await promptWizardSecret({
|
|
2166
|
+
label: 'AWS access key id',
|
|
2167
|
+
required: true,
|
|
2168
|
+
examples: ['AKIA...'],
|
|
2169
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2170
|
+
}),
|
|
2171
|
+
secret_access_key: await promptWizardSecret({
|
|
2172
|
+
label: 'AWS secret access key',
|
|
2173
|
+
required: true,
|
|
2174
|
+
examples: ['secret'],
|
|
2175
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2176
|
+
}),
|
|
2177
|
+
region: await promptWizardText({
|
|
2178
|
+
label: 'AWS region',
|
|
2179
|
+
required: true,
|
|
2180
|
+
examples: ['us-east-1'],
|
|
2181
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2182
|
+
}),
|
|
2183
|
+
account_id: await promptWizardText({
|
|
2184
|
+
label: 'AWS account id',
|
|
2185
|
+
required: true,
|
|
2186
|
+
examples: ['123456789012'],
|
|
2187
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2188
|
+
}),
|
|
2189
|
+
},
|
|
2190
|
+
};
|
|
2191
|
+
return image;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
2195
|
+
const source = await promptWizardSelect({
|
|
2196
|
+
label: 'Origem do deploy HTTP',
|
|
2197
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2198
|
+
examples: ['github', 'oci'],
|
|
2199
|
+
options: [
|
|
2200
|
+
{ value: 'github', description: 'build a partir de repositorio GitHub' },
|
|
2201
|
+
{ value: 'oci', description: 'imagem OCI pronta' },
|
|
2202
|
+
],
|
|
2203
|
+
});
|
|
2204
|
+
promptWizardSelect.ui.state.httpSource = source;
|
|
2205
|
+
|
|
2206
|
+
const port = await promptWizardNumber({
|
|
2207
|
+
label: 'Porta da aplicacao',
|
|
2208
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2209
|
+
examples: ['3000', '8080'],
|
|
2210
|
+
min: 1,
|
|
2211
|
+
max: 65665,
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
const instances = await promptWizardNumber({
|
|
2215
|
+
label: 'Quantidade de instancias',
|
|
2216
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2217
|
+
examples: ['1', '2', '3'],
|
|
2218
|
+
min: 1,
|
|
2219
|
+
max: Math.max(1, Number(availableInstances?.[plan] || 1)),
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
let storage;
|
|
2223
|
+
if (plan === 'free') {
|
|
2224
|
+
storage = { persistent: false, capacity: 1 };
|
|
2225
|
+
promptWizardBoolean.ui.state.httpStoragePersistent = false;
|
|
2226
|
+
} else {
|
|
2227
|
+
const persistent = await promptWizardBoolean({
|
|
2228
|
+
label: 'Storage persistente',
|
|
2229
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2230
|
+
examples: ['sim', 'nao'],
|
|
2231
|
+
});
|
|
2232
|
+
promptWizardBoolean.ui.state.httpStoragePersistent = persistent;
|
|
2233
|
+
|
|
2234
|
+
storage = {
|
|
2235
|
+
persistent,
|
|
2236
|
+
...(persistent ? {
|
|
2237
|
+
dir_path_to_persist: await promptWizardText({
|
|
2238
|
+
label: 'Diretorio persistente',
|
|
2239
|
+
required: true,
|
|
2240
|
+
examples: ['/data'],
|
|
2241
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2242
|
+
}),
|
|
2243
|
+
} : {}),
|
|
2244
|
+
capacity: await promptWizardNumber({
|
|
2245
|
+
label: 'Capacidade de storage (Gi)',
|
|
2246
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2247
|
+
examples: ['1', '5', '10'],
|
|
2248
|
+
min: 1,
|
|
2249
|
+
max: 250,
|
|
2250
|
+
}),
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
const envs = await promptWizardPairs({
|
|
2255
|
+
introLabel: 'Deseja adicionar variaveis de ambiente?',
|
|
2256
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2257
|
+
examples: ['sim', 'nao'],
|
|
2258
|
+
stateKey: 'envs',
|
|
2259
|
+
itemLabels: [
|
|
2260
|
+
{ key: 'name', label: 'Nome da env', required: true, examples: ['NODE_ENV'], docs: DOCS_CONFIGURATION_URL },
|
|
2261
|
+
{ key: 'value', label: 'Valor da env', required: true, examples: ['production'], docs: DOCS_CONFIGURATION_URL },
|
|
2262
|
+
],
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
const allowBlockIp = isIpAccessEnabled(httpPlans, plan);
|
|
2266
|
+
const customWhitelist = allowBlockIp
|
|
2267
|
+
? await promptWizardPairs({
|
|
2268
|
+
introLabel: 'Deseja configurar whitelist personalizada de entrada?',
|
|
2269
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2270
|
+
examples: ['sim', 'nao'],
|
|
2271
|
+
stateKey: 'whitelist',
|
|
2272
|
+
itemLabels: [
|
|
2273
|
+
{ key: 'cidr', label: 'CIDR liberado', required: true, examples: ['10.0.0.0/24'], docs: DOCS_CONFIGURATION_URL },
|
|
2274
|
+
{ key: 'description', label: 'Descricao da whitelist', required: true, examples: ['office'], docs: DOCS_CONFIGURATION_URL },
|
|
2275
|
+
],
|
|
2276
|
+
})
|
|
2277
|
+
: [];
|
|
2278
|
+
|
|
2279
|
+
const ingress_black_list = allowBlockIp
|
|
2280
|
+
? await promptWizardPairs({
|
|
2281
|
+
introLabel: 'Deseja adicionar blacklist de entrada?',
|
|
2282
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2283
|
+
examples: ['sim', 'nao'],
|
|
2284
|
+
stateKey: 'blacklist',
|
|
2285
|
+
itemLabels: [
|
|
2286
|
+
{ key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['192.168.0.0/24'], docs: DOCS_CONFIGURATION_URL },
|
|
2287
|
+
{ key: 'description', label: 'Descricao da blacklist', required: true, examples: ['bloqueio temporario'], docs: DOCS_CONFIGURATION_URL },
|
|
2288
|
+
],
|
|
2289
|
+
})
|
|
2290
|
+
: [];
|
|
2291
|
+
|
|
2292
|
+
const subdomain = isSubdomainCustomizable(httpPlans, plan)
|
|
2293
|
+
? await promptWizardText({
|
|
2294
|
+
label: 'Subdomain personalizado',
|
|
2295
|
+
examples: ['minha-api'],
|
|
2296
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2297
|
+
})
|
|
2298
|
+
: '';
|
|
2299
|
+
|
|
2300
|
+
return {
|
|
2301
|
+
type_project: 'http',
|
|
2302
|
+
...(source === 'github' ? { github: await buildHttpGithubConfig() } : { image: await buildHttpImageConfig() }),
|
|
2303
|
+
port,
|
|
2304
|
+
instances,
|
|
2305
|
+
storage,
|
|
2306
|
+
envs,
|
|
2307
|
+
...(subdomain ? { subdomain } : {}),
|
|
2308
|
+
network_access: {
|
|
2309
|
+
ingress_white_list: customWhitelist.length ? customWhitelist : DEFAULT_HTTP_NETWORK_ACCESS.ingress_white_list,
|
|
2310
|
+
ingress_black_list,
|
|
2311
|
+
},
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
async function buildPostgresqlConfig({ plan, databasePlans }) {
|
|
2316
|
+
const maxReplicas = Math.max(1, getMaxReplicasFromFeatures(getPlanFeatures(databasePlans, plan)));
|
|
2317
|
+
return {
|
|
2318
|
+
type_project: 'postgresql',
|
|
2319
|
+
version: await promptWizardSelect({
|
|
2320
|
+
label: 'Versao do PostgreSQL',
|
|
2321
|
+
docs: DOCS_CREATE_POSTGRESQL_URL,
|
|
2322
|
+
examples: ALLOWED_POSTGRESQL_VERSIONS,
|
|
2323
|
+
options: ALLOWED_POSTGRESQL_VERSIONS.map((value) => ({ value })),
|
|
2324
|
+
}),
|
|
2325
|
+
instances: await promptWizardSelect({
|
|
2326
|
+
label: 'Instancias do PostgreSQL',
|
|
2327
|
+
docs: DOCS_CREATE_POSTGRESQL_URL,
|
|
2328
|
+
examples: ['1', '2', String(maxReplicas)],
|
|
2329
|
+
options: Array.from({ length: maxReplicas }, (_, index) => String(index + 1)).map((value) => ({ value })),
|
|
2330
|
+
}).then((value) => Number(value)),
|
|
2331
|
+
storage: {
|
|
2332
|
+
persistent: true,
|
|
2333
|
+
capacity: plan === 'db-free'
|
|
2334
|
+
? 1
|
|
2335
|
+
: await promptWizardNumber({
|
|
2336
|
+
label: 'Capacidade de storage do banco (Gi)',
|
|
2337
|
+
docs: DOCS_DATABASE_CONFIGURATION_URL,
|
|
2338
|
+
examples: ['1', '10', '20'],
|
|
2339
|
+
min: 1,
|
|
2340
|
+
max: 250,
|
|
2341
|
+
}),
|
|
2342
|
+
},
|
|
2343
|
+
envs: [],
|
|
2344
|
+
network_access: DEFAULT_HTTP_NETWORK_ACCESS,
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
async function buildMariadbConfig() {
|
|
2349
|
+
return {
|
|
2350
|
+
type_project: 'mariadb',
|
|
2351
|
+
version: await promptWizardSelect({
|
|
2352
|
+
label: 'Versao do MariaDB',
|
|
2353
|
+
docs: DOCS_CREATE_MARIADB_URL,
|
|
2354
|
+
examples: ALLOWED_MARIADB_VERSIONS,
|
|
2355
|
+
options: ALLOWED_MARIADB_VERSIONS.map((value) => ({ value })),
|
|
2356
|
+
}),
|
|
2357
|
+
instances: 3,
|
|
2358
|
+
storage: {
|
|
2359
|
+
persistent: true,
|
|
2360
|
+
capacity: await promptWizardNumber({
|
|
2361
|
+
label: 'Capacidade de storage do banco (Gi)',
|
|
2362
|
+
docs: DOCS_DATABASE_CONFIGURATION_URL,
|
|
2363
|
+
examples: ['1', '10', '20'],
|
|
2364
|
+
min: 1,
|
|
2365
|
+
max: 250,
|
|
2366
|
+
}),
|
|
2367
|
+
},
|
|
2368
|
+
envs: [],
|
|
2369
|
+
network_access: DEFAULT_HTTP_NETWORK_ACCESS,
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
async function runProjectCreateWizard(session, flags) {
|
|
2374
|
+
const catalogs = await fetchWizardCatalogs(session, flags);
|
|
2375
|
+
const ui = createWizardUi();
|
|
2376
|
+
promptWizardText.ui = ui;
|
|
2377
|
+
promptWizardSecret.ui = ui;
|
|
2378
|
+
promptWizardBoolean.ui = ui;
|
|
2379
|
+
promptWizardSelect.ui = ui;
|
|
2380
|
+
promptWizardNumber.ui = ui;
|
|
2381
|
+
|
|
2382
|
+
process.stdout.write(`${tone('Wizard interativo de criacao de projeto Zenifra', 'green')}\n`);
|
|
2383
|
+
process.stdout.write(`${toneDim('Digite ? para ajuda detalhada em qualquer campo.')}\n`);
|
|
2384
|
+
|
|
2385
|
+
const name = await promptWizardText({
|
|
2386
|
+
label: 'Nome do projeto',
|
|
2387
|
+
required: true,
|
|
2388
|
+
examples: ['api-web', 'db-app-01'],
|
|
2389
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2390
|
+
});
|
|
2391
|
+
const description = await promptWizardText({
|
|
2392
|
+
label: 'Descricao',
|
|
2393
|
+
examples: ['API publica da empresa'],
|
|
2394
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2395
|
+
});
|
|
2396
|
+
const typeProject = await promptWizardSelect({
|
|
2397
|
+
label: 'Tipo do projeto',
|
|
2398
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2399
|
+
examples: WIZARD_TYPE_PROJECT_OPTIONS,
|
|
2400
|
+
options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
|
|
2401
|
+
});
|
|
2402
|
+
ui.state.typeProject = typeProject;
|
|
2403
|
+
const planOptions = typeProject === 'http'
|
|
2404
|
+
? sortWizardOptions(catalogs.httpPlans
|
|
2405
|
+
.map((plan) => plan?.plan)
|
|
2406
|
+
.filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
|
|
2407
|
+
: sortWizardOptions(catalogs.databasePlans
|
|
2408
|
+
.map((plan) => plan?.plan)
|
|
2409
|
+
.filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
|
|
2410
|
+
const plan = await promptWizardSelect({
|
|
2411
|
+
label: 'Plano',
|
|
2412
|
+
docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2413
|
+
examples: [planOptions[0], planOptions.at(-1)],
|
|
2414
|
+
options: planOptions.map((value) => ({ value })),
|
|
2415
|
+
});
|
|
2416
|
+
ui.state.plan = plan;
|
|
2417
|
+
if (typeProject === 'http') {
|
|
2418
|
+
ui.state.planAllowsBlockIp = isIpAccessEnabled(catalogs.httpPlans, plan);
|
|
2419
|
+
ui.state.planAllowsSubdomain = isSubdomainCustomizable(catalogs.httpPlans, plan);
|
|
2420
|
+
}
|
|
2421
|
+
const paymentMode = await promptWizardSelect({
|
|
2422
|
+
label: 'Modo de pagamento',
|
|
2423
|
+
docs: DOCS_PAYMENTS_URL,
|
|
2424
|
+
examples: WIZARD_PAYMENT_MODE_OPTIONS,
|
|
2425
|
+
options: WIZARD_PAYMENT_MODE_OPTIONS.map((value) => ({ value })),
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
let config;
|
|
2429
|
+
if (typeProject === 'http') {
|
|
2430
|
+
config = await buildHttpConfig({
|
|
2431
|
+
plan,
|
|
2432
|
+
httpPlans: catalogs.httpPlans,
|
|
2433
|
+
availableInstances: catalogs.availableInstances,
|
|
2434
|
+
});
|
|
2435
|
+
} else if (typeProject === 'postgresql') {
|
|
2436
|
+
config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
|
|
2437
|
+
} else {
|
|
2438
|
+
config = await buildMariadbConfig();
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
const payload = {
|
|
2442
|
+
name,
|
|
2443
|
+
...(description ? { description } : {}),
|
|
2444
|
+
plan,
|
|
2445
|
+
payment_mode: paymentMode,
|
|
2446
|
+
config,
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
printWizardSummary(payload);
|
|
2450
|
+
const confirmed = await promptWizardBoolean({
|
|
2451
|
+
label: 'Confirmar criacao do projeto',
|
|
2452
|
+
docs: typeProject === 'http' ? DOCS_CREATE_HTTP_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2453
|
+
examples: ['sim', 'nao'],
|
|
2454
|
+
});
|
|
2455
|
+
if (!confirmed) return null;
|
|
2456
|
+
return payload;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
796
2459
|
async function handleProjectCreate(session, flags) {
|
|
797
2460
|
const orgId = await resolveOrgId(session, flags);
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
2461
|
+
const wizardPayload = hasWizardCreateInput(flags) ? await runProjectCreateWizard(session, flags) : null;
|
|
2462
|
+
|
|
2463
|
+
if (wizardPayload === null && hasWizardCreateInput(flags)) {
|
|
2464
|
+
process.stdout.write('Operacao cancelada pelo usuario.\n');
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
const name = wizardPayload?.name || flags.name || await prompt('Nome do projeto');
|
|
2469
|
+
const description = wizardPayload?.description || flags.description;
|
|
2470
|
+
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
2471
|
+
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
2472
|
+
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
2473
|
+
const validated = validateCreateInput({ plan, paymentMode, config });
|
|
802
2474
|
|
|
803
2475
|
const payload = await request(session, flags, 'POST', '/project', {
|
|
804
2476
|
orgId,
|
|
805
2477
|
body: {
|
|
806
2478
|
name,
|
|
807
|
-
...(
|
|
808
|
-
plan,
|
|
809
|
-
payment_mode: paymentMode,
|
|
810
|
-
config,
|
|
2479
|
+
...(description ? { description: String(description) } : {}),
|
|
2480
|
+
plan: validated.plan,
|
|
2481
|
+
payment_mode: validated.paymentMode,
|
|
2482
|
+
config: validated.config,
|
|
811
2483
|
},
|
|
812
2484
|
});
|
|
813
2485
|
|
|
814
2486
|
if (flags.json) return printJson(payload);
|
|
2487
|
+
if (wizardPayload) {
|
|
2488
|
+
closePromptResources();
|
|
2489
|
+
}
|
|
815
2490
|
const project = unwrapData(payload);
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
2491
|
+
const rows = [
|
|
2492
|
+
{ field: 'Projeto', value: projectIdOf(project) || 'id indisponivel' },
|
|
2493
|
+
...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
|
|
2494
|
+
...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
|
|
2495
|
+
];
|
|
2496
|
+
process.stdout.write('\n');
|
|
2497
|
+
printTable(rows, [
|
|
2498
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
2499
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
2500
|
+
]);
|
|
819
2501
|
}
|
|
820
2502
|
|
|
821
2503
|
async function getProject(session, flags, projectId, orgId) {
|
|
@@ -1083,49 +2765,68 @@ async function main() {
|
|
|
1083
2765
|
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
1084
2766
|
const [command, subcommand] = positional;
|
|
1085
2767
|
const session = await readSession();
|
|
2768
|
+
try {
|
|
2769
|
+
if (isRemovedProjectsCreate(positional)) {
|
|
2770
|
+
throw new CliError(removedProjectsCreateMessage());
|
|
2771
|
+
}
|
|
1086
2772
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
2773
|
+
if (command === 'help') {
|
|
2774
|
+
process.stdout.write(commandHelp(positional.slice(1)));
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
1091
2777
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
2778
|
+
if (!command) {
|
|
2779
|
+
process.stdout.write(usage());
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
1096
2782
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
2783
|
+
if (flags.help) {
|
|
2784
|
+
process.stdout.write(commandHelp(positional));
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
1101
2787
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
2788
|
+
if (isNamespaceCommand(command) && !subcommand) {
|
|
2789
|
+
process.stdout.write(commandHelp([command]));
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
|
|
2794
|
+
if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
|
|
2795
|
+
if (command === 'auth' && subcommand === 'logout') return handleLogout(session, flags);
|
|
2796
|
+
if (command === 'profile' && subcommand === 'list') return handleProfileList(session, flags);
|
|
2797
|
+
if (command === 'profile' && subcommand === 'show') return handleProfileShow(session, flags, positional);
|
|
2798
|
+
if (command === 'profile' && subcommand === 'add') return handleProfileAdd(session, flags);
|
|
2799
|
+
if (command === 'profile' && subcommand === 'edit') return handleProfileEdit(session, flags, positional);
|
|
2800
|
+
if (command === 'profile' && subcommand === 'use') return handleProfileUse(session, flags, positional);
|
|
2801
|
+
if (command === 'profile' && subcommand === 'remove') return handleProfileRemove(session, flags, positional);
|
|
2802
|
+
if (command === 'login') return handleLogin(session, flags);
|
|
2803
|
+
if (command === 'logout') return handleLogout(session, flags);
|
|
2804
|
+
if (command === 'plans') return handlePlans(session, flags);
|
|
2805
|
+
if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
|
|
2806
|
+
if (command === 'orgs') return handleOrgs(session, flags);
|
|
2807
|
+
if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
|
|
2808
|
+
if (command === 'projects') return handleProjects(session, flags);
|
|
2809
|
+
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
2810
|
+
if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
|
|
2811
|
+
if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
|
|
2812
|
+
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|
|
2813
|
+
if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
|
|
2814
|
+
if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
|
|
2815
|
+
if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
|
|
2816
|
+
if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
|
|
2817
|
+
if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
|
|
2818
|
+
if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
|
|
2819
|
+
if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
|
|
2820
|
+
if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
|
|
2821
|
+
if (command === 'builds') return handleDeployments(session, flags);
|
|
2822
|
+
if (command === 'deployments') return handleDeployments(session, flags);
|
|
2823
|
+
if (command === 'deploy' && subcommand === 'watch') return handleDeployWatch(session, flags);
|
|
2824
|
+
if (command === 'deploy') return handleDeploy(session, flags);
|
|
2825
|
+
|
|
2826
|
+
throw new CliError(`Comando desconhecido: ${positional.join(' ')}`);
|
|
2827
|
+
} finally {
|
|
2828
|
+
closePromptResources();
|
|
2829
|
+
}
|
|
1129
2830
|
}
|
|
1130
2831
|
|
|
1131
2832
|
main().catch((error) => {
|