@zenifra/cli 0.4.0 → 0.6.0
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 +70 -9
- package/bin/zenifra.mjs +1171 -62
- package/examples/http-git-project.json +38 -0
- package/examples/job-project.json +20 -0
- package/package.json +1 -1
package/bin/zenifra.mjs
CHANGED
|
@@ -27,6 +27,9 @@ const PROFILES_FILE = join(SESSION_DIR, 'profiles.json');
|
|
|
27
27
|
const PROFILE_STORE_VERSION = 1;
|
|
28
28
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
29
29
|
const DEFAULT_HTTP_TIMEOUT_MS = 300_000;
|
|
30
|
+
const OCI_IMAGE_REFERENCE_PATTERN = /^[a-z0-9._-]+\/(?:[a-z0-9._-]+\/)*[a-z0-9._-]+(?::[a-z0-9._-]+|@sha256:[a-f0-9]{64})$/;
|
|
31
|
+
const OCI_IMAGE_REFERENCE_MIN_LENGTH = 8;
|
|
32
|
+
const OCI_IMAGE_REFERENCE_MAX_LENGTH = 256;
|
|
30
33
|
const KNOWN_FLAG_NAMES = new Set([
|
|
31
34
|
'apiBase',
|
|
32
35
|
'oauth',
|
|
@@ -37,6 +40,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
37
40
|
'challengeToken',
|
|
38
41
|
'code',
|
|
39
42
|
'connectionFile',
|
|
43
|
+
'connection',
|
|
40
44
|
'commitSha',
|
|
41
45
|
'config',
|
|
42
46
|
'count',
|
|
@@ -71,7 +75,10 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
71
75
|
'plan',
|
|
72
76
|
'profile',
|
|
73
77
|
'project',
|
|
78
|
+
'repository',
|
|
74
79
|
'revoke',
|
|
80
|
+
'run',
|
|
81
|
+
'runId',
|
|
75
82
|
'showValues',
|
|
76
83
|
'status',
|
|
77
84
|
'tagPattern',
|
|
@@ -113,8 +120,8 @@ const ALLOWED_PLAN_VALUES = new Set([
|
|
|
113
120
|
'queue-premium',
|
|
114
121
|
'queue-enterprise',
|
|
115
122
|
]);
|
|
116
|
-
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
|
|
117
|
-
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse']);
|
|
123
|
+
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly', 'per_minute']);
|
|
124
|
+
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse', 'job']);
|
|
118
125
|
const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
|
|
119
126
|
const ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS = new Set(['tag', 'release']);
|
|
120
127
|
const ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS = new Set(['enabled', 'event', 'tag_pattern', 'include_prereleases']);
|
|
@@ -131,6 +138,15 @@ const GITHUB_PUBLIC_CONFIG_FIELDS = [
|
|
|
131
138
|
'build_command',
|
|
132
139
|
];
|
|
133
140
|
const GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS = ['enabled', 'event', 'tag_pattern', 'include_prereleases'];
|
|
141
|
+
const GIT_BUILD_SETTINGS_FIELDS = [
|
|
142
|
+
'runtime',
|
|
143
|
+
'version',
|
|
144
|
+
'start_command',
|
|
145
|
+
'pre_build_command',
|
|
146
|
+
'build_command',
|
|
147
|
+
'dockerfile_path',
|
|
148
|
+
'context_path',
|
|
149
|
+
];
|
|
134
150
|
const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
|
|
135
151
|
const GITHUB_RUNTIME_VERSIONS = {
|
|
136
152
|
nodejs: ['24', '22', '20'],
|
|
@@ -160,6 +176,7 @@ const WIZARD_DATABASE_PLAN_OPTIONS = [
|
|
|
160
176
|
'db-free',
|
|
161
177
|
];
|
|
162
178
|
const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
|
|
179
|
+
const WIZARD_JOB_PLAN_OPTIONS = WIZARD_HTTP_PLAN_OPTIONS.map((plan) => `job-${plan}`);
|
|
163
180
|
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb', 'valkey'];
|
|
164
181
|
const WIZARD_VALKEY_PROFILE_OPTIONS = ['key_value', 'cache', 'queue'];
|
|
165
182
|
const WIZARD_HTTP_EXPOSURE_OPTIONS = ['public', 'private'];
|
|
@@ -177,9 +194,10 @@ const ANSI = {
|
|
|
177
194
|
};
|
|
178
195
|
|
|
179
196
|
class CliError extends Error {
|
|
180
|
-
constructor(message, exitCode = 1) {
|
|
197
|
+
constructor(message, exitCode = 1, httpStatus) {
|
|
181
198
|
super(message);
|
|
182
199
|
this.exitCode = exitCode;
|
|
200
|
+
if (httpStatus !== undefined) this.httpStatus = httpStatus;
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
|
|
@@ -200,10 +218,15 @@ Usage:
|
|
|
200
218
|
zenifra whoami [--json]
|
|
201
219
|
zenifra orgs [--json]
|
|
202
220
|
zenifra org set [--org <id>]
|
|
203
|
-
zenifra plans [--type <all|http|database|storage|valkey>] [--json]
|
|
221
|
+
zenifra plans [--type <all|http|database|storage|valkey|job>] [--json]
|
|
222
|
+
zenifra git providers [--json]
|
|
223
|
+
zenifra git runtimes [--json]
|
|
224
|
+
zenifra git connections [--json]
|
|
225
|
+
zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]
|
|
226
|
+
zenifra git branches --connection <id> --repository <opaque-id> [--json]
|
|
204
227
|
zenifra create project
|
|
205
|
-
zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
|
|
206
|
-
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
|
|
228
|
+
zenifra create project --name <name> --plan <plan> [--payment-mode <mode>] --config <json|@file> [--description <text>] [--org <id>] [--json]
|
|
229
|
+
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey|job>] [--page <n>] [--limit <n>]
|
|
207
230
|
zenifra valkey status --project <id> [--json]
|
|
208
231
|
zenifra valkey connection --project <id> [--json]
|
|
209
232
|
zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]
|
|
@@ -214,6 +237,9 @@ Usage:
|
|
|
214
237
|
zenifra project delete --project <id> --yes [--json]
|
|
215
238
|
zenifra project url --project <id> [--json]
|
|
216
239
|
zenifra project logs --project <id> [--instance <id>] [--json]
|
|
240
|
+
zenifra project runs --project <id> [--page <n>] [--limit <n>] [--json]
|
|
241
|
+
zenifra project runs cancel --project <id> --run <id> [--json]
|
|
242
|
+
zenifra project runs logs --project <id> --run <id> [--json]
|
|
217
243
|
zenifra project metrics --project <id> [--instance <id>] [--json]
|
|
218
244
|
zenifra project metrics capabilities --project <id> [--json]
|
|
219
245
|
zenifra project healthcheck get --project <id> [--json]
|
|
@@ -224,6 +250,9 @@ Usage:
|
|
|
224
250
|
zenifra project image set --project <id> --image <image> [--json]
|
|
225
251
|
zenifra project github --project <id> [--json]
|
|
226
252
|
zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
|
|
253
|
+
zenifra project source --project <id> [--json]
|
|
254
|
+
zenifra project source branches --project <id> [--json]
|
|
255
|
+
zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
|
|
227
256
|
zenifra project exposure set --project <id> --exposure <public|private> [--json]
|
|
228
257
|
zenifra project envs --project <id> [--json] [--show-values]
|
|
229
258
|
zenifra project env add --project <id> --name <name> --value <value> [--json]
|
|
@@ -393,13 +422,75 @@ const HELP_SPECS = [
|
|
|
393
422
|
},
|
|
394
423
|
{
|
|
395
424
|
command: 'plans',
|
|
396
|
-
usage: 'zenifra plans [--type <all|http|database|storage|valkey>] [--json]',
|
|
397
|
-
description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento e
|
|
398
|
-
flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
|
|
399
|
-
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json'],
|
|
425
|
+
usage: 'zenifra plans [--type <all|http|database|storage|valkey|job>] [--json]',
|
|
426
|
+
description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento, Valkey e Jobs agendados.',
|
|
427
|
+
flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, job, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
|
|
428
|
+
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json', 'zenifra plans --type job --json'],
|
|
400
429
|
output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
|
|
401
430
|
jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
|
|
402
431
|
},
|
|
432
|
+
{
|
|
433
|
+
command: 'git',
|
|
434
|
+
usage: 'zenifra git\n zenifra git providers [--json]\n zenifra git runtimes [--json]\n zenifra git connections [--json]\n zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]\n zenifra git branches --connection <id> --repository <opaque-id> [--json]',
|
|
435
|
+
description: 'Consulta provedores, runtimes, conexoes e repositorios Git disponiveis na organizacao.',
|
|
436
|
+
examples: ['zenifra git providers', 'zenifra git connections', 'zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao'],
|
|
437
|
+
output: 'Zenifra CLI - git',
|
|
438
|
+
notes: ['A conexao Forgejo e criada no Console por uma pessoa proprietaria da organizacao. A CLI lista e usa conexoes existentes.'],
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
command: 'git providers',
|
|
442
|
+
usage: 'zenifra git providers [--json]',
|
|
443
|
+
description: 'Lista provedores Git e as capacidades disponiveis para a organizacao selecionada.',
|
|
444
|
+
flags: ['--json Imprime a resposta publica da API em JSON.'],
|
|
445
|
+
examples: ['zenifra git providers', 'zenifra git providers --json'],
|
|
446
|
+
output: 'Provedor Disponivel Descoberta Deploy por branch Deploy por versao\nforgejo sim nao sim sim',
|
|
447
|
+
jsonOutput: '{"status":"success","data":{"api_version":1,"providers":[{"id":"forgejo","available":true,"capabilities":{"repositoryDiscovery":false,"pushDeploy":true,"nativePreviews":false,"versionDeploy":true}}]}}',
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
command: 'git runtimes',
|
|
451
|
+
usage: 'zenifra git runtimes [--json]',
|
|
452
|
+
description: 'Lista runtimes e versoes disponiveis para builds de repositorios Git.',
|
|
453
|
+
flags: ['--json Imprime a resposta publica da API em JSON.'],
|
|
454
|
+
examples: ['zenifra git runtimes', 'zenifra git runtimes --json'],
|
|
455
|
+
output: 'Runtime Versao padrao Versoes\nnodejs 24 24, 22, 20',
|
|
456
|
+
jsonOutput: '{"status":"success","data":[{"id":"nodejs","label":"Node.js","default_version":"24","default_port":3000,"default_start_command":"npm start","default_build_command":"npm run build","versions":[{"id":"24","label":"24"}]}]}',
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
command: 'git connections',
|
|
460
|
+
usage: 'zenifra git connections [--json]',
|
|
461
|
+
description: 'Lista as conexoes Git disponiveis para a organizacao selecionada.',
|
|
462
|
+
flags: ['--json Imprime as conexoes publicas em JSON.'],
|
|
463
|
+
examples: ['zenifra git connections', 'zenifra git connections --json'],
|
|
464
|
+
output: 'ID Provedor Nome Status\nconnection-1 forgejo Forgejo team active',
|
|
465
|
+
jsonOutput: '{"status":"success","data":[{"id":"connection-1","provider_id":"forgejo","display_name":"Forgejo team","status":"active","capabilities":{"repositoryDiscovery":false,"pushDeploy":true,"nativePreviews":false,"versionDeploy":true}}]}',
|
|
466
|
+
notes: ['A conexao precisa ter sido criada por uma pessoa proprietaria no Console. As credenciais nao aparecem na resposta.'],
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
command: 'git repositories',
|
|
470
|
+
usage: 'zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]',
|
|
471
|
+
description: 'Agrupa comandos para resolver repositorios por caminho explicito.',
|
|
472
|
+
examples: ['zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao'],
|
|
473
|
+
output: 'Zenifra CLI - git repositories',
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
command: 'git repositories resolve',
|
|
477
|
+
usage: 'zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]',
|
|
478
|
+
description: 'Valida e resolve o caminho explicito de um repositorio na conexao selecionada.',
|
|
479
|
+
flags: ['--connection <id> ID da conexao Git da organizacao.', '--path <owner/repo> Caminho explicito do repositorio.', '--json Imprime o resultado em JSON.'],
|
|
480
|
+
examples: ['zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao --json'],
|
|
481
|
+
output: 'ID Caminho Branch padrao Privado\nrepo_opaque_1 equipe/aplicacao main sim',
|
|
482
|
+
jsonOutput: '{"status":"success","data":{"id":"repository-id","path":"equipe/aplicacao","default_branch":"main","private":true}}',
|
|
483
|
+
notes: ['Use o ID opaco retornado somente com a mesma conexao.'],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
command: 'git branches',
|
|
487
|
+
usage: 'zenifra git branches --connection <id> --repository <opaque-id> [--json]',
|
|
488
|
+
description: 'Lista branches de um repositorio resolvido pela conexao informada.',
|
|
489
|
+
flags: ['--connection <id> ID da conexao Git da organizacao.', '--repository <id> ID opaco retornado por git repositories resolve.', '--json Imprime a resposta em JSON.'],
|
|
490
|
+
examples: ['zenifra git branches --connection conn-1 --repository repository-id'],
|
|
491
|
+
output: 'Branch Commit\nmain abc123',
|
|
492
|
+
jsonOutput: '{"status":"success","data":[{"name":"main","commit_sha":"<commit-sha>"}]}',
|
|
493
|
+
},
|
|
403
494
|
{
|
|
404
495
|
command: 'valkey',
|
|
405
496
|
usage: 'zenifra valkey\n zenifra valkey status --project <id> [--json]\n zenifra valkey connection --project <id> [--json]\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
@@ -451,7 +542,7 @@ const HELP_SPECS = [
|
|
|
451
542
|
},
|
|
452
543
|
{
|
|
453
544
|
command: 'projects',
|
|
454
|
-
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]',
|
|
545
|
+
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey|job>] [--page <n>] [--limit <n>]',
|
|
455
546
|
description: 'Lista projetos da organizacao ativa ou da API key.',
|
|
456
547
|
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.'],
|
|
457
548
|
examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
|
|
@@ -460,9 +551,9 @@ const HELP_SPECS = [
|
|
|
460
551
|
},
|
|
461
552
|
{
|
|
462
553
|
command: 'create project',
|
|
463
|
-
usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--idempotency-key <key>] [--org <id>] [--json]',
|
|
554
|
+
usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> [--payment-mode <mode>] --config <json|@file> [--description <text>] [--idempotency-key <key>] [--org <id>] [--json]',
|
|
464
555
|
description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
|
|
465
|
-
flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode>
|
|
556
|
+
flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Opcional para Jobs; demais tipos seguem a validacao do catalogo.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--idempotency-key <key> Chave para repetir a mesma criacao com seguranca.', '--json Imprime a resposta em JSON.'],
|
|
466
557
|
examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
|
|
467
558
|
output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.clients.zenifra.com',
|
|
468
559
|
jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
|
|
@@ -470,11 +561,61 @@ const HELP_SPECS = [
|
|
|
470
561
|
},
|
|
471
562
|
{
|
|
472
563
|
command: 'project',
|
|
473
|
-
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project stop --project <id> [--json]\n zenifra project resume --project <id> [--json]\n zenifra project delete --project <id> --yes [--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 metrics capabilities --project <id> [--json]\n zenifra project healthcheck get --project <id> [--json]\n zenifra project healthcheck set --project <id> --path /health [--json]\n zenifra project healthcheck disable --project <id> [--json]\n zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--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 exposure set --project <id> --exposure <public|private> [--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 autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
564
|
+
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project source --project <id> [--json]\n zenifra project source branches --project <id> [--json]\n zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]\n zenifra project stop --project <id> [--json]\n zenifra project resume --project <id> [--json]\n zenifra project delete --project <id> --yes [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project runs --project <id> [--page <n>] [--limit <n>] [--json]\n zenifra project runs cancel --project <id> --run <id> [--json]\n zenifra project runs logs --project <id> --run <id> [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project metrics capabilities --project <id> [--json]\n zenifra project healthcheck get --project <id> [--json]\n zenifra project healthcheck set --project <id> --path /health [--json]\n zenifra project healthcheck disable --project <id> [--json]\n zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--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 exposure set --project <id> --exposure <public|private> [--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 autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
474
565
|
description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
|
|
475
|
-
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
566
|
+
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project source --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
476
567
|
output: 'Zenifra CLI - project',
|
|
477
|
-
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, github, exposure, autoscaling, billing, envs e instances.'],
|
|
568
|
+
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, runs, cancelamento, metrics, capabilities, network, image, github, source, exposure, autoscaling, billing, envs e instances.'],
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
command: 'project source',
|
|
572
|
+
usage: 'zenifra project source --project <id> [--json]\n zenifra project source branches --project <id> [--json]\n zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
573
|
+
description: 'Consulta a origem Git generica de um projeto, lista branches ou altera seu modo de deploy.',
|
|
574
|
+
examples: ['zenifra project source --project proj_1', 'zenifra project source branches --project proj_1'],
|
|
575
|
+
output: 'Zenifra CLI - project source',
|
|
576
|
+
notes: ['Use project github para ler ou alterar configuracoes GitHub legadas.'],
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
command: 'project source branches',
|
|
580
|
+
usage: 'zenifra project source branches --project <id> [--json]',
|
|
581
|
+
description: 'Lista branches disponiveis para a origem Git generica atual do projeto.',
|
|
582
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
583
|
+
examples: ['zenifra project source branches --project proj_1'],
|
|
584
|
+
output: 'Branch Commit\nmain abc123',
|
|
585
|
+
jsonOutput: '{"status":"success","data":[{"name":"main","commit_sha":"<commit-sha>"}]}',
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
command: 'project source deploy-settings',
|
|
589
|
+
usage: 'zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
590
|
+
description: 'Agrupa os modos de deploy da origem Git generica.',
|
|
591
|
+
examples: ['zenifra project source deploy-settings set --project proj_1 --mode branch'],
|
|
592
|
+
output: 'Zenifra CLI - project source deploy-settings',
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
command: 'project source deploy-settings set',
|
|
596
|
+
usage: 'zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
597
|
+
description: 'Define e confirma o modo de deploy da origem Git Forgejo do projeto.',
|
|
598
|
+
flags: [
|
|
599
|
+
'--project <id> ID do projeto.',
|
|
600
|
+
'--mode <manual|branch|tag|release> Modo de deploy exclusivo.',
|
|
601
|
+
'--tag-pattern <pattern> Obrigatorio para tag e release; aceita * e ?.',
|
|
602
|
+
'--include-prereleases <true|false> Inclui prereleases somente no modo release. Padrao: false.',
|
|
603
|
+
'--json Imprime a configuracao confirmada em JSON.',
|
|
604
|
+
],
|
|
605
|
+
examples: [
|
|
606
|
+
'zenifra project source deploy-settings set --project proj_1 --mode manual',
|
|
607
|
+
'zenifra project source deploy-settings set --project proj_1 --mode branch',
|
|
608
|
+
'zenifra project source deploy-settings set --project proj_1 --mode tag --tag-pattern "v*"',
|
|
609
|
+
'zenifra project source deploy-settings set --project proj_1 --mode release --tag-pattern "v*" --include-prereleases true',
|
|
610
|
+
],
|
|
611
|
+
output: 'Configuracao de deploy da origem Git confirmada: release (v*).',
|
|
612
|
+
jsonOutput: '{"status":"success","data":{"source":{"connection_id":"connection-id","repository_id":"repository-id","branch":"main","auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":true},"provider_id":"forgejo","repository_path":"equipe/aplicacao"},"build":{"runtime":"nodejs","version":"24","start_command":"npm start"},"source_revision":2}}',
|
|
613
|
+
notes: [
|
|
614
|
+
'Tag e release exigem --tag-pattern; prereleases so podem ser habilitadas para release.',
|
|
615
|
+
'O comando preserva identidade, branch e configuracao de build da origem e confirma o resultado com uma nova leitura.',
|
|
616
|
+
'A API substitui source e build juntos e nao oferece precondicao de revisao. Evite alterar a origem ao mesmo tempo por outro cliente.',
|
|
617
|
+
'Para projetos com origem GitHub legada, use "zenifra project github deploy-settings set".',
|
|
618
|
+
],
|
|
478
619
|
},
|
|
479
620
|
{
|
|
480
621
|
command: 'project github',
|
|
@@ -566,6 +707,38 @@ const HELP_SPECS = [
|
|
|
566
707
|
jsonOutput: '"server started\\nGET /health 200"',
|
|
567
708
|
notes: ['Nao faz streaming continuo; retorna o snapshot disponibilizado pela API.'],
|
|
568
709
|
},
|
|
710
|
+
{
|
|
711
|
+
command: 'project runs',
|
|
712
|
+
usage: 'zenifra project runs --project <id> [--page <n>] [--limit <n>] [--json]',
|
|
713
|
+
description: 'Lista as execucoes de um Job agendado com o historico de cobranca.',
|
|
714
|
+
flags: ['--project <id> ID do projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Maximo: 50.', '--json Imprime a resposta em JSON.'],
|
|
715
|
+
examples: ['zenifra project runs --project 507f1f77bcf86cd799439012', 'zenifra project runs --project 507f1f77bcf86cd799439012 --page 2 --limit 20 --json'],
|
|
716
|
+
output: 'ID Status Agendada Duracao Minutos cobrados Valor\nrun_1 succeeded 2026-09-01T12:00:00.000Z 70 s 2 R$ 0,04',
|
|
717
|
+
jsonOutput: '{"runs":[{"id":"run_1","status":"succeeded","duration_seconds":70,"billed_minutes":2,"plan":"job-basic","currency":"brl","amount":4}],"pagination":{"page":1,"limit":20,"total":1,"total_pages":1}}',
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
command: 'project runs cancel',
|
|
721
|
+
usage: 'zenifra project runs cancel --project <id> --run <id> [--json]',
|
|
722
|
+
description: 'Cancela somente a execucao selecionada de um Job e cobra somente o tempo consumido.',
|
|
723
|
+
flags: ['--project <id> ID do projeto.', '--run <id> ID publico da execucao.', '--json Imprime a resposta em JSON.'],
|
|
724
|
+
examples: ['zenifra project runs cancel --project 507f1f77bcf86cd799439012 --run run_1'],
|
|
725
|
+
output: 'Execucao: cancelled',
|
|
726
|
+
jsonOutput: '{"run":{"id":"run_1","status":"cancelled","billed_minutes":2}}',
|
|
727
|
+
notes: [
|
|
728
|
+
'Cancela somente a execucao informada; o cron do projeto continua agendando novas execucoes. Use "zenifra project stop --project <id>" para pausar o projeto e interromper execucoes futuras.',
|
|
729
|
+
'O cancelamento aguarda ate 30 segundos pela finalizacao segura e pode forcar o encerramento depois desse periodo. Se a execucao nao puder ser cancelada com seguranca, aguarde que ela termine ou alcance o limite de tempo.',
|
|
730
|
+
],
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
command: 'project runs logs',
|
|
734
|
+
usage: 'zenifra project runs logs --project <id> --run <id> [--json]',
|
|
735
|
+
description: 'Mostra os logs de uma execucao especifica de um Job agendado.',
|
|
736
|
+
flags: ['--project <id> ID do projeto.', '--run <id> ID publico da execucao.', '--json Imprime a resposta em JSON.'],
|
|
737
|
+
examples: ['zenifra project runs logs --project 507f1f77bcf86cd799439012 --run run_1', 'zenifra project runs logs --project 507f1f77bcf86cd799439012 --run run_1 --json'],
|
|
738
|
+
output: 'started\ncompleted',
|
|
739
|
+
jsonOutput: '{"logs":"started\\ncompleted","truncated":false}',
|
|
740
|
+
notes: ['Os logs pertencem a uma execucao identificada pelo ID publico retornado em project runs.'],
|
|
741
|
+
},
|
|
569
742
|
{
|
|
570
743
|
command: 'project metrics',
|
|
571
744
|
usage: 'zenifra project metrics --project <id> [--instance <id>] [--json]',
|
|
@@ -770,7 +943,7 @@ const HELP_SPECS = [
|
|
|
770
943
|
{
|
|
771
944
|
command: 'builds',
|
|
772
945
|
usage: 'zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
773
|
-
description: 'Lista builds
|
|
946
|
+
description: 'Lista builds de um projeto com origem Git.',
|
|
774
947
|
flags: ['--project <id> ID do projeto.', '--page <n> Pagina.', '--limit <n> Itens por pagina.', '--branch <name> Filtra branch.', '--status <status> Filtra status.', '--json Imprime a resposta em JSON.'],
|
|
775
948
|
examples: ['zenifra builds --project 507f1f77bcf86cd799439012 --status success'],
|
|
776
949
|
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
@@ -779,7 +952,7 @@ const HELP_SPECS = [
|
|
|
779
952
|
{
|
|
780
953
|
command: 'builds logs',
|
|
781
954
|
usage: 'zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
782
|
-
description: '
|
|
955
|
+
description: 'Le os logs de um build Git existente e opcionalmente acompanha novos eventos em tempo real.',
|
|
783
956
|
flags: [
|
|
784
957
|
'--project <id> ID do projeto.',
|
|
785
958
|
'--build <id> ID do build.',
|
|
@@ -797,7 +970,7 @@ const HELP_SPECS = [
|
|
|
797
970
|
{
|
|
798
971
|
command: 'deployments',
|
|
799
972
|
usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
800
|
-
description: 'Alias para listar builds
|
|
973
|
+
description: 'Alias para listar builds e deployments Git de um projeto.',
|
|
801
974
|
flags: ['--project <id> ID do projeto.', '--page <n> Pagina.', '--limit <n> Itens por pagina.', '--branch <name> Filtra branch.', '--status <status> Filtra status.', '--json Imprime a resposta em JSON.'],
|
|
802
975
|
examples: ['zenifra deployments --project 507f1f77bcf86cd799439012'],
|
|
803
976
|
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
@@ -806,7 +979,7 @@ const HELP_SPECS = [
|
|
|
806
979
|
{
|
|
807
980
|
command: 'deploy',
|
|
808
981
|
usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
|
|
809
|
-
description: 'Dispara um build/deploy
|
|
982
|
+
description: 'Dispara um build/deploy Git para o projeto e retorna o build_id para acompanhamento posterior.',
|
|
810
983
|
flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
|
|
811
984
|
examples: [
|
|
812
985
|
'zenifra deploy --project 507f1f77bcf86cd799439012 --branch main',
|
|
@@ -877,7 +1050,8 @@ function commandHelp(positionals) {
|
|
|
877
1050
|
const key = helpKeyFromPositionals(positionals);
|
|
878
1051
|
if (!key) return usage();
|
|
879
1052
|
|
|
880
|
-
const spec = HELP_BY_COMMAND.get(key)
|
|
1053
|
+
const spec = HELP_BY_COMMAND.get(key)
|
|
1054
|
+
|| (key === 'project run logs' ? HELP_BY_COMMAND.get('project runs logs') : undefined);
|
|
881
1055
|
if (!spec) {
|
|
882
1056
|
throw new CliError(`Ajuda nao encontrada para: ${key}. Use "zenifra help <command>".`);
|
|
883
1057
|
}
|
|
@@ -900,7 +1074,7 @@ function removedProjectsCreateMessage() {
|
|
|
900
1074
|
}
|
|
901
1075
|
|
|
902
1076
|
function isNamespaceCommand(command) {
|
|
903
|
-
return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
|
|
1077
|
+
return ['auth', 'profile', 'project', 'org', 'valkey', 'git'].includes(command);
|
|
904
1078
|
}
|
|
905
1079
|
|
|
906
1080
|
function parseArgs(argv) {
|
|
@@ -1470,9 +1644,9 @@ async function promptWizardBoolean({ label, docs, examples = ['sim', 'nao'] }) {
|
|
|
1470
1644
|
}
|
|
1471
1645
|
}
|
|
1472
1646
|
|
|
1473
|
-
async function promptWizardSelect({ label, options, docs, examples = [] }) {
|
|
1647
|
+
async function promptWizardSelect({ label, options, docs, examples = [], extraValues = [] }) {
|
|
1474
1648
|
while (true) {
|
|
1475
|
-
const allowedValues = options.map((option) => option.value);
|
|
1649
|
+
const allowedValues = [...options.map((option) => option.value), ...extraValues];
|
|
1476
1650
|
const answer = await prompt(buildWizardPromptLine({
|
|
1477
1651
|
ui: promptWizardSelect.ui,
|
|
1478
1652
|
label,
|
|
@@ -1493,6 +1667,8 @@ async function promptWizardSelect({ label, options, docs, examples = [] }) {
|
|
|
1493
1667
|
const normalized = normalizeFreeText(answer);
|
|
1494
1668
|
const found = options.find((option) => normalizeFreeText(option.value) === normalized);
|
|
1495
1669
|
if (found) return found.value;
|
|
1670
|
+
const extraValue = extraValues.find((value) => normalizeFreeText(value) === normalized);
|
|
1671
|
+
if (extraValue) return extraValue;
|
|
1496
1672
|
process.stdout.write(`${tone(`valor invalido: escolha um item entre ${options.map((_, index) => index + 1).join(', ')} ou um dos valores aceitos.`, 'red')}\n`);
|
|
1497
1673
|
promptWizardSelect.ui.step -= 1;
|
|
1498
1674
|
}
|
|
@@ -1625,6 +1801,14 @@ function httpTimeoutMs() {
|
|
|
1625
1801
|
return value;
|
|
1626
1802
|
}
|
|
1627
1803
|
|
|
1804
|
+
function publicApiErrorMessage(value) {
|
|
1805
|
+
const message = String(value || 'Nao foi possivel concluir a operacao.');
|
|
1806
|
+
if (/kubernetes|k8s|namespace|cluster|pod|serviceaccount|deployment|internal[_ -]/i.test(message)) {
|
|
1807
|
+
return 'Nao foi possivel concluir a operacao. Tente novamente mais tarde.';
|
|
1808
|
+
}
|
|
1809
|
+
return message;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1628
1812
|
async function request(session, flags, method, path, {
|
|
1629
1813
|
body,
|
|
1630
1814
|
orgId,
|
|
@@ -1691,31 +1875,394 @@ async function request(session, flags, method, path, {
|
|
|
1691
1875
|
}
|
|
1692
1876
|
|
|
1693
1877
|
if (!response.ok) {
|
|
1694
|
-
const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}
|
|
1878
|
+
const message = publicApiErrorMessage(payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`);
|
|
1879
|
+
const httpError = (errorMessage) => new CliError(errorMessage, 1, response.status);
|
|
1695
1880
|
if (response.status === 401) {
|
|
1696
1881
|
if (credential?.type === 'api_key') {
|
|
1697
|
-
throw
|
|
1882
|
+
throw httpError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
|
|
1698
1883
|
}
|
|
1699
|
-
throw
|
|
1884
|
+
throw httpError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
|
|
1700
1885
|
}
|
|
1701
1886
|
if (response.status === 403) {
|
|
1702
|
-
throw
|
|
1887
|
+
throw httpError(message);
|
|
1703
1888
|
}
|
|
1704
1889
|
if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
|
|
1705
|
-
throw
|
|
1890
|
+
throw httpError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
|
|
1706
1891
|
}
|
|
1707
1892
|
if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
|
|
1708
|
-
throw
|
|
1893
|
+
throw httpError('Este projeto nao possui acesso a metricas.');
|
|
1709
1894
|
}
|
|
1710
1895
|
if (String(message).includes('missing x-organization-id')) {
|
|
1711
|
-
throw
|
|
1896
|
+
throw httpError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
1712
1897
|
}
|
|
1713
|
-
throw
|
|
1898
|
+
throw httpError(message);
|
|
1714
1899
|
}
|
|
1715
1900
|
|
|
1716
1901
|
return payload;
|
|
1717
1902
|
}
|
|
1718
1903
|
|
|
1904
|
+
function hasValidGitProvidersApiV1(payload) {
|
|
1905
|
+
if (payload?.status !== 'success' || !isRecord(payload.data) || payload.data.api_version !== 1 || !Array.isArray(payload.data.providers)) {
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
return payload.data.providers.every((provider) => (
|
|
1910
|
+
isRecord(provider)
|
|
1911
|
+
&& typeof provider.id === 'string'
|
|
1912
|
+
&& /^[a-z0-9][a-z0-9_-]{0,63}$/.test(provider.id)
|
|
1913
|
+
&& typeof provider.available === 'boolean'
|
|
1914
|
+
&& isRecord(provider.capabilities)
|
|
1915
|
+
&& typeof provider.capabilities.repositoryDiscovery === 'boolean'
|
|
1916
|
+
&& typeof provider.capabilities.pushDeploy === 'boolean'
|
|
1917
|
+
&& typeof provider.capabilities.nativePreviews === 'boolean'
|
|
1918
|
+
));
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
async function supportsNeutralGitRoutes(session, flags, orgId) {
|
|
1922
|
+
let payload;
|
|
1923
|
+
try {
|
|
1924
|
+
payload = await request(session, flags, 'GET', '/git/providers', { orgId });
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
if ([404, 405, 501].includes(error?.httpStatus)) return false;
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (!hasValidGitProvidersApiV1(payload)) {
|
|
1931
|
+
throw new CliError('A API retornou uma resposta invalida para as capacidades de origem Git.');
|
|
1932
|
+
}
|
|
1933
|
+
return true;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function hasLegacyGithubSource(project) {
|
|
1937
|
+
if (!isRecord(project)) return false;
|
|
1938
|
+
// Public Git source projections must never be rewritten as legacy GitHub input.
|
|
1939
|
+
// preview.source is separate preview metadata and does not identify the primary project source.
|
|
1940
|
+
const genericSourceFields = ['source', 'build', 'git_source', 'git_build'];
|
|
1941
|
+
if (genericSourceFields.some((field) => project[field] !== undefined || project.config?.[field] !== undefined)) {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const github = project.github || project.config?.github;
|
|
1946
|
+
return isRecord(github)
|
|
1947
|
+
&& typeof github.repository_owner === 'string'
|
|
1948
|
+
&& github.repository_owner.trim().length > 0
|
|
1949
|
+
&& typeof github.repository_name === 'string'
|
|
1950
|
+
&& github.repository_name.trim().length > 0;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
async function resolveGitProjectApiPath(session, flags, projectId, orgId) {
|
|
1954
|
+
if (await supportsNeutralGitRoutes(session, flags, orgId)) {
|
|
1955
|
+
return `/project/${projectId}`;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
1959
|
+
if (!hasLegacyGithubSource(project)) {
|
|
1960
|
+
throw new CliError('Esta API antiga so permite builds e deploys de projetos com uma origem GitHub legada compativel.');
|
|
1961
|
+
}
|
|
1962
|
+
return `/project/${projectId}/github`;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function requiredGitOpaqueId(flags, name, commandKey) {
|
|
1966
|
+
const value = flags[name];
|
|
1967
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value)) {
|
|
1968
|
+
printCommandHelpAndFail(commandKey);
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
return value;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function requiredGitRepositoryPath(flags, commandKey) {
|
|
1975
|
+
const value = flags.path;
|
|
1976
|
+
const segments = typeof value === 'string' ? value.split('/') : [];
|
|
1977
|
+
if (typeof value !== 'string'
|
|
1978
|
+
|| value.length < 3
|
|
1979
|
+
|| value.length > 1024
|
|
1980
|
+
|| segments.length < 2
|
|
1981
|
+
|| segments.some((segment) => segment.length === 0)
|
|
1982
|
+
|| /[\u0000-\u001f\u007f-\u009f\\?#%]/u.test(value)) {
|
|
1983
|
+
printCommandHelpAndFail(commandKey);
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
return value;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function yesNo(value) {
|
|
1990
|
+
if (value === true) return 'sim';
|
|
1991
|
+
if (value === false) return 'nao';
|
|
1992
|
+
return 'indisponivel';
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
async function handleGitProviders(session, flags) {
|
|
1996
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1997
|
+
const payload = await request(session, flags, 'GET', '/git/providers', { orgId });
|
|
1998
|
+
if (!hasValidGitProvidersApiV1(payload)) {
|
|
1999
|
+
throw new CliError('A API retornou uma resposta invalida para o catalogo de provedores Git.');
|
|
2000
|
+
}
|
|
2001
|
+
if (flags.json) return printJson(payload);
|
|
2002
|
+
|
|
2003
|
+
printTable(payload.data.providers, [
|
|
2004
|
+
{ label: 'Provedor', value: (provider) => provider.id },
|
|
2005
|
+
{ label: 'Disponivel', value: (provider) => yesNo(provider.available) },
|
|
2006
|
+
{ label: 'Descoberta', value: (provider) => yesNo(provider.capabilities.repositoryDiscovery) },
|
|
2007
|
+
{ label: 'Deploy por branch', value: (provider) => yesNo(provider.capabilities.pushDeploy) },
|
|
2008
|
+
{ label: 'Deploy por versao', value: (provider) => yesNo(provider.capabilities.versionDeploy) },
|
|
2009
|
+
]);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
async function handleGitRuntimes(session, flags) {
|
|
2013
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2014
|
+
const payload = await request(session, flags, 'GET', '/git/runtime-catalog', { orgId });
|
|
2015
|
+
const runtimes = unwrapData(payload);
|
|
2016
|
+
if (!Array.isArray(runtimes)) {
|
|
2017
|
+
throw new CliError('A API retornou uma resposta invalida para o catalogo de runtimes Git.');
|
|
2018
|
+
}
|
|
2019
|
+
if (flags.json) return printJson(payload);
|
|
2020
|
+
|
|
2021
|
+
printTable(runtimes, [
|
|
2022
|
+
{ label: 'Runtime', value: (runtime) => runtime.label || runtime.id || '-' },
|
|
2023
|
+
{ label: 'Versao padrao', value: (runtime) => runtime.default_version || '-' },
|
|
2024
|
+
{ label: 'Versoes', value: (runtime) => Array.isArray(runtime.versions) ? runtime.versions.map((version) => version.id).join(', ') : '-' },
|
|
2025
|
+
]);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
async function handleGitConnections(session, flags) {
|
|
2029
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2030
|
+
const payload = await request(session, flags, 'GET', '/git/connections', { orgId });
|
|
2031
|
+
const connections = unwrapData(payload);
|
|
2032
|
+
if (!Array.isArray(connections)) {
|
|
2033
|
+
throw new CliError('A API retornou uma resposta invalida para as conexoes Git.');
|
|
2034
|
+
}
|
|
2035
|
+
if (flags.json) return printJson(payload);
|
|
2036
|
+
|
|
2037
|
+
printTable(connections, [
|
|
2038
|
+
{ label: 'ID', value: (connection) => connection.id || '-' },
|
|
2039
|
+
{ label: 'Provedor', value: (connection) => connection.provider_id || '-' },
|
|
2040
|
+
{ label: 'Nome', value: (connection) => connection.display_name || '-' },
|
|
2041
|
+
{ label: 'Status', value: (connection) => connection.status || '-' },
|
|
2042
|
+
]);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
async function handleGitRepositoryResolve(session, flags) {
|
|
2046
|
+
const connectionId = requiredGitOpaqueId(flags, 'connection', 'git repositories resolve');
|
|
2047
|
+
const repositoryPath = requiredGitRepositoryPath(flags, 'git repositories resolve');
|
|
2048
|
+
if (!connectionId || !repositoryPath) return;
|
|
2049
|
+
|
|
2050
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2051
|
+
const endpoint = '/git/connections/' + encodeURIComponent(connectionId) + '/repositories/resolve';
|
|
2052
|
+
const payload = await request(session, flags, 'POST', endpoint, {
|
|
2053
|
+
orgId,
|
|
2054
|
+
body: { path: repositoryPath },
|
|
2055
|
+
});
|
|
2056
|
+
if (flags.json) return printJson(payload);
|
|
2057
|
+
|
|
2058
|
+
const repository = unwrapData(payload);
|
|
2059
|
+
printTable([repository], [
|
|
2060
|
+
{ label: 'ID', value: (entry) => entry.id || '-' },
|
|
2061
|
+
{ label: 'Caminho', value: (entry) => entry.path || '-' },
|
|
2062
|
+
{ label: 'Branch padrao', value: (entry) => entry.default_branch || '-' },
|
|
2063
|
+
{ label: 'Privado', value: (entry) => yesNo(entry.private) },
|
|
2064
|
+
]);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
async function handleGitBranches(session, flags) {
|
|
2068
|
+
const connectionId = requiredGitOpaqueId(flags, 'connection', 'git branches');
|
|
2069
|
+
const repositoryId = requiredGitOpaqueId(flags, 'repository', 'git branches');
|
|
2070
|
+
if (!connectionId || !repositoryId) return;
|
|
2071
|
+
|
|
2072
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2073
|
+
const endpoint = '/git/connections/' + encodeURIComponent(connectionId)
|
|
2074
|
+
+ '/repositories/' + encodeURIComponent(repositoryId) + '/branches';
|
|
2075
|
+
const payload = await request(session, flags, 'GET', endpoint, { orgId });
|
|
2076
|
+
const branches = unwrapData(payload);
|
|
2077
|
+
if (!Array.isArray(branches)) {
|
|
2078
|
+
throw new CliError('A API retornou uma resposta invalida para as branches Git.');
|
|
2079
|
+
}
|
|
2080
|
+
if (flags.json) return printJson(payload);
|
|
2081
|
+
|
|
2082
|
+
printTable(branches, [
|
|
2083
|
+
{ label: 'Branch', value: (branch) => branch.name || '-' },
|
|
2084
|
+
{ label: 'Commit', value: (branch) => branch.commit_sha || '-' },
|
|
2085
|
+
]);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function printGitProjectSource(data) {
|
|
2089
|
+
if (!isRecord(data?.source)) {
|
|
2090
|
+
process.stdout.write('Este projeto nao possui uma origem Git generica configurada.\n');
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
const source = data.source;
|
|
2095
|
+
const build = isRecord(data.build) ? data.build : {};
|
|
2096
|
+
const mode = source.provider_id === 'github' ? null : gitSourceDeploymentMode(source);
|
|
2097
|
+
const rows = [
|
|
2098
|
+
{ field: 'Provedor', value: source.provider_id || '-' },
|
|
2099
|
+
{ field: 'Repositorio', value: source.repository_path || source.repository_id || '-' },
|
|
2100
|
+
{ field: 'Branch', value: source.branch || '-' },
|
|
2101
|
+
{ field: 'Runtime', value: build.runtime ? build.runtime + '@' + (build.version || '-') : '-' },
|
|
2102
|
+
{ field: 'Modo de deploy', value: mode || (source.provider_id === 'github' ? 'consulte project github' : 'indisponivel') },
|
|
2103
|
+
];
|
|
2104
|
+
if (mode === 'tag' || mode === 'release') {
|
|
2105
|
+
rows.push(
|
|
2106
|
+
{ field: 'Padrao de tags', value: source.version_deploy?.tag_pattern || '-' },
|
|
2107
|
+
{ field: 'Incluir prereleases', value: source.version_deploy?.include_prereleases === true ? 'sim' : 'nao' },
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
printTable(rows, [
|
|
2111
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
2112
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
2113
|
+
]);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
async function handleProjectSource(session, flags) {
|
|
2117
|
+
const projectId = requireProjectId(flags, 'project source');
|
|
2118
|
+
if (!projectId) return;
|
|
2119
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2120
|
+
const payload = await request(session, flags, 'GET', '/project/' + encodeURIComponent(projectId) + '/source', { orgId });
|
|
2121
|
+
const source = unwrapData(payload);
|
|
2122
|
+
if (flags.json) return printJson(payload);
|
|
2123
|
+
printGitProjectSource(source);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
async function handleProjectSourceBranches(session, flags) {
|
|
2127
|
+
const projectId = requireProjectId(flags, 'project source branches');
|
|
2128
|
+
if (!projectId) return;
|
|
2129
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2130
|
+
const endpoint = '/project/' + encodeURIComponent(projectId) + '/source/branches';
|
|
2131
|
+
const payload = await request(session, flags, 'GET', endpoint, { orgId });
|
|
2132
|
+
const branches = unwrapData(payload);
|
|
2133
|
+
if (!Array.isArray(branches)) {
|
|
2134
|
+
throw new CliError('A API retornou uma resposta invalida para as branches da origem Git.');
|
|
2135
|
+
}
|
|
2136
|
+
if (flags.json) return printJson(payload);
|
|
2137
|
+
printTable(branches, [
|
|
2138
|
+
{ label: 'Branch', value: (branch) => branch.name || '-' },
|
|
2139
|
+
{ label: 'Commit', value: (branch) => branch.commit_sha || '-' },
|
|
2140
|
+
]);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function projectGitSourceInput(source) {
|
|
2144
|
+
if (!isRecord(source)
|
|
2145
|
+
|| typeof source.connection_id !== 'string'
|
|
2146
|
+
|| typeof source.repository_id !== 'string'
|
|
2147
|
+
|| typeof source.branch !== 'string'
|
|
2148
|
+
|| typeof source.auto_deploy !== 'boolean') {
|
|
2149
|
+
throw new CliError('A API retornou uma origem Git incompleta; nenhuma alteracao foi enviada.');
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const input = {
|
|
2153
|
+
connection_id: source.connection_id,
|
|
2154
|
+
repository_id: source.repository_id,
|
|
2155
|
+
branch: source.branch,
|
|
2156
|
+
auto_deploy: source.auto_deploy,
|
|
2157
|
+
};
|
|
2158
|
+
if (source.version_deploy === undefined || source.version_deploy === null) return input;
|
|
2159
|
+
if (!isRecord(source.version_deploy)) {
|
|
2160
|
+
throw new CliError('A API retornou configuracoes de deploy Git invalidas; nenhuma alteracao foi enviada.');
|
|
2161
|
+
}
|
|
2162
|
+
const versionDeploy = {};
|
|
2163
|
+
for (const field of GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS) {
|
|
2164
|
+
if (Object.hasOwn(source.version_deploy, field)) versionDeploy[field] = source.version_deploy[field];
|
|
2165
|
+
}
|
|
2166
|
+
input.version_deploy = versionDeploy;
|
|
2167
|
+
return input;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function projectGitBuildSettingsInput(build) {
|
|
2171
|
+
if (!isRecord(build)) {
|
|
2172
|
+
throw new CliError('A API retornou configuracoes de build incompletas; nenhuma alteracao foi enviada.');
|
|
2173
|
+
}
|
|
2174
|
+
const input = {};
|
|
2175
|
+
for (const field of GIT_BUILD_SETTINGS_FIELDS) {
|
|
2176
|
+
if (Object.hasOwn(build, field)) input[field] = build[field];
|
|
2177
|
+
}
|
|
2178
|
+
return input;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
function gitSourceDeploymentMode(source) {
|
|
2182
|
+
const versionDeployEnabled = source?.version_deploy?.enabled === true;
|
|
2183
|
+
if (source?.auto_deploy === true && versionDeployEnabled) return null;
|
|
2184
|
+
if (source?.auto_deploy === true) return 'branch';
|
|
2185
|
+
if (!versionDeployEnabled) return 'manual';
|
|
2186
|
+
if (!ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(source.version_deploy.event)) return null;
|
|
2187
|
+
return source.version_deploy.event;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
function projectGitSourceMatches(data, expectedSource, expectedBuild, desired) {
|
|
2191
|
+
if (!isRecord(data) || !isRecord(data.source) || !isRecord(data.build)) return false;
|
|
2192
|
+
const source = data.source;
|
|
2193
|
+
if (source.provider_id !== 'forgejo'
|
|
2194
|
+
|| source.connection_id !== expectedSource.connection_id
|
|
2195
|
+
|| source.repository_id !== expectedSource.repository_id
|
|
2196
|
+
|| source.branch !== expectedSource.branch
|
|
2197
|
+
|| gitSourceDeploymentMode(source) !== desired.mode) return false;
|
|
2198
|
+
|
|
2199
|
+
if (desired.mode === 'tag' || desired.mode === 'release') {
|
|
2200
|
+
if (source.version_deploy?.tag_pattern !== desired.body.version_deploy.tag_pattern
|
|
2201
|
+
|| (source.version_deploy?.include_prereleases === true) !== desired.includePrereleases) return false;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
return Object.entries(expectedBuild).every(([field, value]) => data.build[field] === value);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
async function handleProjectSourceDeploySettingsSet(session, flags) {
|
|
2208
|
+
const desired = parseGithubDeploySettings(flags);
|
|
2209
|
+
const projectId = requireProjectId(flags, 'project source deploy-settings set');
|
|
2210
|
+
if (!projectId) return;
|
|
2211
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2212
|
+
const endpoint = '/project/' + encodeURIComponent(projectId) + '/source';
|
|
2213
|
+
let current;
|
|
2214
|
+
try {
|
|
2215
|
+
current = unwrapData(await request(session, flags, 'GET', endpoint, { orgId }));
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
if (error?.httpStatus === 404) {
|
|
2218
|
+
throw new CliError('A origem Git generica nao esta disponivel para este projeto ou API. Para projetos GitHub legados, use "zenifra project github deploy-settings set".', 1, 404);
|
|
2219
|
+
}
|
|
2220
|
+
throw error;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
if (!isRecord(current?.source) || !isRecord(current?.build)) {
|
|
2224
|
+
throw new CliError('Este projeto nao tem uma origem Git generica configurada. Configure-a antes de alterar o modo de deploy.');
|
|
2225
|
+
}
|
|
2226
|
+
if (current.source.provider_id === 'github') {
|
|
2227
|
+
throw new CliError('Esta origem GitHub nao pode ser alterada por este comando. Consulte e altere com "zenifra project github deploy-settings set".');
|
|
2228
|
+
}
|
|
2229
|
+
if (current.source.provider_id !== 'forgejo') {
|
|
2230
|
+
throw new CliError('O provedor da origem nao e suportado por este comando; nenhuma alteracao foi enviada.');
|
|
2231
|
+
}
|
|
2232
|
+
if (desired.mode === 'branch' && current.capabilities?.pushDeploy !== true) {
|
|
2233
|
+
throw new CliError('A origem Git nao oferece deploy automatico por branch.');
|
|
2234
|
+
}
|
|
2235
|
+
if ((desired.mode === 'tag' || desired.mode === 'release') && current.capabilities?.versionDeploy !== true) {
|
|
2236
|
+
throw new CliError('A origem Git nao oferece deploy automatico por tag ou release.');
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
const source = projectGitSourceInput(current.source);
|
|
2240
|
+
const build = projectGitBuildSettingsInput(current.build);
|
|
2241
|
+
source.auto_deploy = desired.body.auto_deploy;
|
|
2242
|
+
delete source.version_deploy;
|
|
2243
|
+
if (desired.mode === 'tag' || desired.mode === 'release') {
|
|
2244
|
+
source.version_deploy = desired.body.version_deploy;
|
|
2245
|
+
}
|
|
2246
|
+
const expectedSource = {
|
|
2247
|
+
connection_id: source.connection_id,
|
|
2248
|
+
repository_id: source.repository_id,
|
|
2249
|
+
branch: source.branch,
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
await request(session, flags, 'PUT', endpoint, {
|
|
2253
|
+
orgId,
|
|
2254
|
+
body: { source, build },
|
|
2255
|
+
});
|
|
2256
|
+
const confirmed = unwrapData(await request(session, flags, 'GET', endpoint, { orgId }));
|
|
2257
|
+
if (!projectGitSourceMatches(confirmed, expectedSource, build, desired)) {
|
|
2258
|
+
throw new CliError('A configuracao solicitada nao foi confirmada pela API. Consulte "zenifra project source".');
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
if (flags.json) return printJson({ status: 'success', data: confirmed });
|
|
2262
|
+
process.stdout.write('Configuracao de deploy da origem Git confirmada: ' + desired.mode
|
|
2263
|
+
+ ((desired.mode === 'tag' || desired.mode === 'release') ? ' (' + desired.body.version_deploy.tag_pattern + ')' : '') + '.\n');
|
|
2264
|
+
}
|
|
2265
|
+
|
|
1719
2266
|
function unwrapData(payload) {
|
|
1720
2267
|
return payload?.data ?? payload;
|
|
1721
2268
|
}
|
|
@@ -1945,9 +2492,66 @@ function printEnvs(envs, flags) {
|
|
|
1945
2492
|
]);
|
|
1946
2493
|
}
|
|
1947
2494
|
|
|
2495
|
+
function isJobProject(project) {
|
|
2496
|
+
return project?.type_project === 'job' || project?.config?.type_project === 'job';
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const JOB_INFO_HIDDEN_KEYS = new Set(['domain', 'url', 'exposure', 'port', 'instances']);
|
|
2500
|
+
const JOB_CONFIG_HIDDEN_KEYS = new Set(['exposure', 'port', 'instances', 'network_access', 'domain', 'custom_domains', 'autoscaling', 'healthcheck']);
|
|
2501
|
+
const INTERNAL_PROJECT_KEYS = new Set([
|
|
2502
|
+
'current_instances',
|
|
2503
|
+
'runtime_name',
|
|
2504
|
+
'namespace',
|
|
2505
|
+
'k8s_job_name',
|
|
2506
|
+
]);
|
|
2507
|
+
|
|
2508
|
+
function sanitizePublicProjectValue(value) {
|
|
2509
|
+
if (Array.isArray(value)) return value.map(sanitizePublicProjectValue);
|
|
2510
|
+
if (!value || typeof value !== 'object') return value;
|
|
2511
|
+
return Object.fromEntries(Object.entries(value)
|
|
2512
|
+
.filter(([key]) => !INTERNAL_PROJECT_KEYS.has(key))
|
|
2513
|
+
.map(([key, entryValue]) => [key, sanitizePublicProjectValue(entryValue)]));
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function sanitizeJobConfigInfo(value) {
|
|
2517
|
+
if (Array.isArray(value)) return value.map(sanitizeJobConfigInfo);
|
|
2518
|
+
if (!value || typeof value !== 'object') return value;
|
|
2519
|
+
return Object.fromEntries(Object.entries(value)
|
|
2520
|
+
.filter(([key]) => !JOB_CONFIG_HIDDEN_KEYS.has(key))
|
|
2521
|
+
.map(([key, entryValue]) => [key, sanitizeJobConfigInfo(entryValue)]));
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
function sanitizePublicProject(project) {
|
|
2525
|
+
const sanitized = sanitizePublicProjectValue(project);
|
|
2526
|
+
if (!sanitized || typeof sanitized !== 'object' || Array.isArray(sanitized)) return sanitized;
|
|
2527
|
+
return Object.fromEntries(Object.entries(sanitized)
|
|
2528
|
+
.filter(([key]) => !JOB_INFO_HIDDEN_KEYS.has(key))
|
|
2529
|
+
.map(([key, value]) => [key, key === 'config' ? sanitizeJobConfigInfo(value) : value]));
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function sanitizeJobProjectResponse(payload) {
|
|
2533
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
|
2534
|
+
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
|
2535
|
+
return { ...payload, data: sanitizePublicProject(payload.data) };
|
|
2536
|
+
}
|
|
2537
|
+
return sanitizePublicProjectValue(payload);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
function publicProjectsData(data) {
|
|
2541
|
+
if (Array.isArray(data)) return data.map((project) => isJobProject(project) ? sanitizePublicProject(project) : project);
|
|
2542
|
+
if (!data || typeof data !== 'object') return data;
|
|
2543
|
+
return {
|
|
2544
|
+
...data,
|
|
2545
|
+
projects: Array.isArray(data.projects)
|
|
2546
|
+
? data.projects.map((project) => isJobProject(project) ? sanitizePublicProject(project) : project)
|
|
2547
|
+
: data.projects,
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
|
|
1948
2551
|
function printProject(project) {
|
|
1949
2552
|
const managedService = project.managed_service;
|
|
1950
2553
|
const isValkey = project.type_project === 'valkey' || managedService?.engine === 'valkey';
|
|
2554
|
+
const isJob = isJobProject(project);
|
|
1951
2555
|
const rows = [
|
|
1952
2556
|
{ label: 'ID', value: projectIdOf(project) || '-' },
|
|
1953
2557
|
{ label: 'Nome', value: project.name || '-' },
|
|
@@ -1956,7 +2560,13 @@ function printProject(project) {
|
|
|
1956
2560
|
{ label: 'Plano', value: project.plan || '-' },
|
|
1957
2561
|
];
|
|
1958
2562
|
|
|
1959
|
-
if (
|
|
2563
|
+
if (isJob) {
|
|
2564
|
+
const schedule = project.job || project.config?.job || {};
|
|
2565
|
+
rows.push(
|
|
2566
|
+
{ label: 'Agendamento', value: schedule.cron || '-' },
|
|
2567
|
+
{ label: 'Armazenamento', value: project.storage?.persistent === true || project.config?.storage?.persistent === true ? 'persistente' : 'efemero' },
|
|
2568
|
+
);
|
|
2569
|
+
} else if (isValkey) {
|
|
1960
2570
|
rows.push(
|
|
1961
2571
|
{ label: 'Perfil', value: managedService?.profile || '-' },
|
|
1962
2572
|
{ label: 'Versao', value: managedService?.version || '-' },
|
|
@@ -2423,9 +3033,10 @@ async function handleProjects(session, flags) {
|
|
|
2423
3033
|
};
|
|
2424
3034
|
const query = buildQuery(projectFlags, ['type', 'page', 'limit']);
|
|
2425
3035
|
const data = unwrapData(await request(session, flags, 'GET', `/project${query}`, { orgId }));
|
|
2426
|
-
const
|
|
3036
|
+
const publicData = publicProjectsData(data);
|
|
3037
|
+
const projects = asArray(publicData);
|
|
2427
3038
|
|
|
2428
|
-
if (flags.json) return printJson(
|
|
3039
|
+
if (flags.json) return printJson(publicData);
|
|
2429
3040
|
printTable(projects, [
|
|
2430
3041
|
{ label: 'ID', value: projectIdOf },
|
|
2431
3042
|
{ label: 'Nome', value: (project) => project.name || '-' },
|
|
@@ -2514,6 +3125,8 @@ function normalizePlan(value) {
|
|
|
2514
3125
|
]);
|
|
2515
3126
|
|
|
2516
3127
|
if (aliases.has(normalized)) return aliases.get(normalized);
|
|
3128
|
+
const normalizedJobPlan = normalized.startsWith('job_') ? `job-${normalized.slice(4)}` : '';
|
|
3129
|
+
if (WIZARD_JOB_PLAN_OPTIONS.includes(normalizedJobPlan)) return normalizedJobPlan;
|
|
2517
3130
|
if (ALLOWED_PLAN_VALUES.has(normalized)) return normalized;
|
|
2518
3131
|
return normalized;
|
|
2519
3132
|
}
|
|
@@ -2533,6 +3146,10 @@ function normalizePaymentMode(value) {
|
|
|
2533
3146
|
['annual', 'yearly'],
|
|
2534
3147
|
['anual', 'yearly'],
|
|
2535
3148
|
['por ano', 'yearly'],
|
|
3149
|
+
['per_minute', 'per_minute'],
|
|
3150
|
+
['per-minute', 'per_minute'],
|
|
3151
|
+
['por minuto', 'per_minute'],
|
|
3152
|
+
['minuto', 'per_minute'],
|
|
2536
3153
|
]);
|
|
2537
3154
|
return aliases.get(raw) || raw;
|
|
2538
3155
|
}
|
|
@@ -2669,12 +3286,21 @@ async function fetchWizardCatalogs(session, flags) {
|
|
|
2669
3286
|
return { httpPlans, availableInstances, databasePlans };
|
|
2670
3287
|
}
|
|
2671
3288
|
|
|
3289
|
+
function requireJobPlans(value) {
|
|
3290
|
+
const plans = Array.isArray(value) ? value : value?.plans;
|
|
3291
|
+
if (!Array.isArray(plans)) {
|
|
3292
|
+
throw new CliError('Falha ao carregar o catalogo de Jobs agendados. Tente novamente mais tarde.');
|
|
3293
|
+
}
|
|
3294
|
+
return plans;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
2672
3297
|
function normalizePlansCatalogType(value) {
|
|
2673
3298
|
const normalized = normalizeFreeText(value || 'all');
|
|
2674
3299
|
if (!normalized || normalized === 'all') return 'all';
|
|
2675
3300
|
if (normalized === 'http') return 'http';
|
|
2676
3301
|
if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
|
|
2677
3302
|
if (normalized === 'valkey') return 'valkey';
|
|
3303
|
+
if (normalized === 'job' || normalized === 'jobs' || normalized === 'scheduled job' || normalized === 'scheduled jobs') return 'job';
|
|
2678
3304
|
if (normalized === 'key-value' || normalized === 'key value' || normalized === 'key_value') return 'valkey-key_value';
|
|
2679
3305
|
if (normalized === 'cache') return 'valkey-cache';
|
|
2680
3306
|
if (normalized === 'queue') return 'valkey-queue';
|
|
@@ -2695,6 +3321,18 @@ function formatBrlFromCents(value) {
|
|
|
2695
3321
|
return formatBrl(Number.isFinite(number) ? number / 100 : 0);
|
|
2696
3322
|
}
|
|
2697
3323
|
|
|
3324
|
+
function formatJobBrlFromCents(value) {
|
|
3325
|
+
if (value === undefined || value === null || value === '') return '-';
|
|
3326
|
+
const number = Number(value);
|
|
3327
|
+
if (!Number.isFinite(number)) return '-';
|
|
3328
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
3329
|
+
style: 'currency',
|
|
3330
|
+
currency: 'BRL',
|
|
3331
|
+
minimumFractionDigits: 2,
|
|
3332
|
+
maximumFractionDigits: 4,
|
|
3333
|
+
}).format(number / 100);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
2698
3336
|
function humanizeStorageName(value) {
|
|
2699
3337
|
const raw = String(value || '').trim();
|
|
2700
3338
|
if (!raw) return { storage: '-', type: '-' };
|
|
@@ -2792,6 +3430,17 @@ async function fetchPlansCatalogs(session, flags, type) {
|
|
|
2792
3430
|
};
|
|
2793
3431
|
}
|
|
2794
3432
|
|
|
3433
|
+
if (type === 'job') {
|
|
3434
|
+
return {
|
|
3435
|
+
http: [],
|
|
3436
|
+
database: [],
|
|
3437
|
+
storage: [],
|
|
3438
|
+
job: requireJobPlans(
|
|
3439
|
+
unwrapData(await request(session, flags, 'GET', '/project/job/plans', { tokenRequired: false })),
|
|
3440
|
+
),
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
|
|
2795
3444
|
if (type === 'valkey' || type.startsWith('valkey-')) {
|
|
2796
3445
|
const catalog = requireValkeyCatalog(
|
|
2797
3446
|
unwrapData(await request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false })),
|
|
@@ -2804,10 +3453,11 @@ async function fetchPlansCatalogs(session, flags, type) {
|
|
|
2804
3453
|
};
|
|
2805
3454
|
}
|
|
2806
3455
|
|
|
2807
|
-
const [httpPayload, databasePayload, storagePayload, valkeyPayload] = await Promise.all([
|
|
3456
|
+
const [httpPayload, databasePayload, storagePayload, jobPayload, valkeyPayload] = await Promise.all([
|
|
2808
3457
|
request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
|
|
2809
3458
|
request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
|
|
2810
3459
|
request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
|
|
3460
|
+
request(session, flags, 'GET', '/project/job/plans', { tokenRequired: false }),
|
|
2811
3461
|
request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false }),
|
|
2812
3462
|
]);
|
|
2813
3463
|
|
|
@@ -2815,6 +3465,7 @@ async function fetchPlansCatalogs(session, flags, type) {
|
|
|
2815
3465
|
http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
|
|
2816
3466
|
database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
|
|
2817
3467
|
storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
|
|
3468
|
+
job: requireJobPlans(unwrapData(jobPayload)),
|
|
2818
3469
|
valkey: requireValkeyCatalog(unwrapData(valkeyPayload)),
|
|
2819
3470
|
};
|
|
2820
3471
|
}
|
|
@@ -2853,6 +3504,15 @@ function formatPlanCapabilities(capabilities) {
|
|
|
2853
3504
|
return available.join(', ') || '-';
|
|
2854
3505
|
}
|
|
2855
3506
|
|
|
3507
|
+
function printJobCatalog(plans) {
|
|
3508
|
+
process.stdout.write('Jobs agendados' + String.fromCharCode(10));
|
|
3509
|
+
printTable(asArray(plans), [
|
|
3510
|
+
{ label: 'Plano', value: (plan) => plan.plan || plan.id || '-' },
|
|
3511
|
+
{ label: 'Por minuto', value: (plan) => formatJobBrlFromCents(plan.price_per_minute) },
|
|
3512
|
+
{ label: 'Features', value: (plan) => asArray(plan.features).join(', ') || '-' },
|
|
3513
|
+
]);
|
|
3514
|
+
}
|
|
3515
|
+
|
|
2856
3516
|
function printPlansCatalogs(payload, type) {
|
|
2857
3517
|
if (type === 'all' || type === 'http') {
|
|
2858
3518
|
process.stdout.write('HTTP\n');
|
|
@@ -2890,6 +3550,16 @@ function printPlansCatalogs(payload, type) {
|
|
|
2890
3550
|
]);
|
|
2891
3551
|
}
|
|
2892
3552
|
|
|
3553
|
+
if (type === 'all') {
|
|
3554
|
+
process.stdout.write('\n');
|
|
3555
|
+
printJobCatalog(payload.job);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
if (type === 'job') {
|
|
3559
|
+
printJobCatalog(payload.job);
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
2893
3563
|
if (type === 'all' || type === 'valkey' || type.startsWith('valkey-')) {
|
|
2894
3564
|
if (type === 'all') process.stdout.write('\n');
|
|
2895
3565
|
printValkeyCatalog(payload.valkey, valkeyProfileFilter(type));
|
|
@@ -2899,7 +3569,7 @@ function printPlansCatalogs(payload, type) {
|
|
|
2899
3569
|
async function handlePlans(session, flags) {
|
|
2900
3570
|
const type = normalizePlansCatalogType(flags.type);
|
|
2901
3571
|
if (!type) {
|
|
2902
|
-
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey.');
|
|
3572
|
+
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey, job, key-value, cache ou queue.');
|
|
2903
3573
|
}
|
|
2904
3574
|
|
|
2905
3575
|
const payload = await fetchPlansCatalogs(session, flags, type);
|
|
@@ -2911,6 +3581,75 @@ function formatAllowedValues(values) {
|
|
|
2911
3581
|
return [...values].join(', ');
|
|
2912
3582
|
}
|
|
2913
3583
|
|
|
3584
|
+
function validateJobConfig(config) {
|
|
3585
|
+
if (!isRecord(config)) throw new CliError('config deve ser um objeto.');
|
|
3586
|
+
|
|
3587
|
+
if (!isRecord(config.job)) throw new CliError('Jobs exigem config.job.');
|
|
3588
|
+
const cron = String(config.job.cron || '').trim();
|
|
3589
|
+
if (cron.split(/\s+/).filter(Boolean).length !== 5) {
|
|
3590
|
+
throw new CliError('config.job.cron deve conter exatamente cinco campos em UTC.');
|
|
3591
|
+
}
|
|
3592
|
+
const forbiddenFields = ['instances', 'port', 'exposure', 'network_access', 'domain', 'custom_domains', 'autoscaling', 'healthcheck'];
|
|
3593
|
+
const forbiddenField = forbiddenFields.find((field) => config[field] !== undefined);
|
|
3594
|
+
if (forbiddenField) {
|
|
3595
|
+
throw new CliError(`Jobs nao aceitam config.${forbiddenField}.`);
|
|
3596
|
+
}
|
|
3597
|
+
const forbiddenJobField = ['command', 'args'].find((field) => config.job[field] !== undefined);
|
|
3598
|
+
if (forbiddenJobField) {
|
|
3599
|
+
throw new CliError(`Jobs nao aceitam config.job.${forbiddenJobField}. A imagem ja define o comando e os argumentos.`);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
if (!isRecord(config.image) || typeof config.image.url !== 'string' || !config.image.url.trim()) {
|
|
3603
|
+
throw new CliError('Jobs exigem uma imagem OCI em config.image. O fluxo GitHub ainda nao esta disponivel para Jobs.');
|
|
3604
|
+
}
|
|
3605
|
+
const imageReference = config.image.url.trim();
|
|
3606
|
+
if (imageReference.length < OCI_IMAGE_REFERENCE_MIN_LENGTH
|
|
3607
|
+
|| imageReference.length > OCI_IMAGE_REFERENCE_MAX_LENGTH
|
|
3608
|
+
|| !OCI_IMAGE_REFERENCE_PATTERN.test(imageReference)) {
|
|
3609
|
+
throw new CliError('config.image.url deve ser uma referencia OCI completa com registro e tag ou digest.');
|
|
3610
|
+
}
|
|
3611
|
+
if (config.github !== undefined) {
|
|
3612
|
+
throw new CliError('Jobs aceitam somente config.image. O fluxo GitHub ainda nao esta disponivel para Jobs.');
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
if (!Array.isArray(config.envs)) {
|
|
3616
|
+
throw new CliError('config.envs deve ser um array.');
|
|
3617
|
+
}
|
|
3618
|
+
if (config.envs.length > 100) throw new CliError('config.envs nao pode conter mais de 100 variaveis.');
|
|
3619
|
+
for (const env of config.envs) {
|
|
3620
|
+
if (!isRecord(env) || typeof env.name !== 'string' || typeof env.value !== 'string') {
|
|
3621
|
+
throw new CliError('Cada item de config.envs deve conter name e value como texto.');
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
if (!isRecord(config.storage)) throw new CliError('Jobs exigem config.storage.');
|
|
3626
|
+
if (typeof config.storage.persistent !== 'boolean') {
|
|
3627
|
+
throw new CliError('config.storage.persistent deve ser booleano.');
|
|
3628
|
+
}
|
|
3629
|
+
const capacity = Number(config.storage.capacity);
|
|
3630
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 250) {
|
|
3631
|
+
throw new CliError('config.storage.capacity deve ser um inteiro entre 1 e 250 GiB.');
|
|
3632
|
+
}
|
|
3633
|
+
if (config.storage.persistent) {
|
|
3634
|
+
if (typeof config.storage.dir_path_to_persist !== 'string' || !config.storage.dir_path_to_persist.trim()) {
|
|
3635
|
+
throw new CliError('config.storage.dir_path_to_persist e obrigatorio para storage persistente.');
|
|
3636
|
+
}
|
|
3637
|
+
} else if (config.storage.dir_path_to_persist !== undefined) {
|
|
3638
|
+
throw new CliError('config.storage.dir_path_to_persist nao e aceito para storage efemero.');
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
return { ...config, type_project: 'job', job: { ...config.job, cron } };
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
function validateJobPlanAndPayment(plan, paymentMode) {
|
|
3645
|
+
if (!WIZARD_JOB_PLAN_OPTIONS.includes(plan)) {
|
|
3646
|
+
throw new CliError(`Plano invalido para Jobs: "${plan}". Valores aceitos: ${formatAllowedValues(WIZARD_JOB_PLAN_OPTIONS)}.`);
|
|
3647
|
+
}
|
|
3648
|
+
if (paymentMode !== 'per_minute') {
|
|
3649
|
+
throw new CliError('Jobs agendados aceitam somente o modo de pagamento per_minute.');
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
|
|
2914
3653
|
function validateCreateAutoscaling(config, plan, typeProject) {
|
|
2915
3654
|
if (config?.autoscaling === undefined) return config;
|
|
2916
3655
|
if (typeProject !== 'http') {
|
|
@@ -3054,19 +3793,39 @@ function validateGithubVersionDeployConfig(github) {
|
|
|
3054
3793
|
function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
3055
3794
|
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
3056
3795
|
const nextPlan = normalizePlan(plan);
|
|
3796
|
+
const nextPaymentMode = normalizePaymentMode(paymentMode);
|
|
3797
|
+
|
|
3798
|
+
if (nextTypeProject === 'job') {
|
|
3799
|
+
validateJobPlanAndPayment(nextPlan, nextPaymentMode);
|
|
3800
|
+
return {
|
|
3801
|
+
plan: nextPlan,
|
|
3802
|
+
paymentMode: nextPaymentMode,
|
|
3803
|
+
config: validateJobConfig({ ...config, type_project: nextTypeProject }),
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3057
3807
|
if (!nextPlan || (nextTypeProject !== 'valkey' && !ALLOWED_PLAN_VALUES.has(nextPlan))) {
|
|
3058
3808
|
throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
3059
3809
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
if (!nextPaymentMode || !ALLOWED_PAYMENT_MODE_VALUES.has(nextPaymentMode)) {
|
|
3063
|
-
throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
|
|
3810
|
+
if (!nextPaymentMode || !ALLOWED_PAYMENT_MODE_VALUES.has(nextPaymentMode) || nextPaymentMode === 'per_minute') {
|
|
3811
|
+
throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(new Set(['hourly', 'monthly', 'yearly']))}. Docs: ${DOCS_PAYMENTS_URL}`);
|
|
3064
3812
|
}
|
|
3065
3813
|
|
|
3066
3814
|
if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
|
|
3067
3815
|
throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
3068
3816
|
}
|
|
3069
3817
|
|
|
3818
|
+
const hasGenericGitConfig = config?.source !== undefined || config?.build !== undefined;
|
|
3819
|
+
if (hasGenericGitConfig && (!isRecord(config.source) || !isRecord(config.build))) {
|
|
3820
|
+
throw new CliError('config.source e config.build devem ser informados juntos para projetos com origem Git.');
|
|
3821
|
+
}
|
|
3822
|
+
if (hasGenericGitConfig && nextTypeProject !== 'http') {
|
|
3823
|
+
throw new CliError('config.source e config.build so podem ser usados em projetos HTTP.');
|
|
3824
|
+
}
|
|
3825
|
+
if (hasGenericGitConfig && config.github !== undefined) {
|
|
3826
|
+
throw new CliError('Use config.github ou config.source/config.build, sem misturar os formatos de origem Git.');
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3070
3829
|
const nextConfig = { ...config, type_project: nextTypeProject };
|
|
3071
3830
|
if (nextTypeProject === 'valkey') {
|
|
3072
3831
|
return {
|
|
@@ -3118,10 +3877,13 @@ function maskWizardSummary(value, key = '') {
|
|
|
3118
3877
|
|
|
3119
3878
|
function printWizardSummary(payload) {
|
|
3120
3879
|
const masked = maskWizardSummary(payload);
|
|
3880
|
+
const projectSummary = masked.config.type_project === 'job'
|
|
3881
|
+
? ` projeto: ${masked.name} | plano: ${masked.plan}`
|
|
3882
|
+
: ` projeto: ${masked.name} | plano: ${masked.plan} | pagamento: ${masked.payment_mode}`;
|
|
3121
3883
|
const lines = [
|
|
3122
3884
|
'',
|
|
3123
3885
|
tone('Resumo da criacao', 'green'),
|
|
3124
|
-
|
|
3886
|
+
projectSummary,
|
|
3125
3887
|
` tipo: ${masked.config.type_project}`,
|
|
3126
3888
|
];
|
|
3127
3889
|
|
|
@@ -3143,6 +3905,12 @@ function printWizardSummary(payload) {
|
|
|
3143
3905
|
lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
|
|
3144
3906
|
}
|
|
3145
3907
|
|
|
3908
|
+
if (masked.config.type_project === 'job') {
|
|
3909
|
+
lines.push(` Job: cron ${masked.config.job.cron} UTC | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
3910
|
+
if (masked.config.image) lines.push(` imagem: ${masked.config.image.url} | publica: ${masked.config.image.is_public ? 'sim' : 'nao'}`);
|
|
3911
|
+
if (masked.config.github) lines.push(` github: ${masked.config.github.repository_owner}/${masked.config.github.repository_name}@${masked.config.github.branch}`);
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3146
3914
|
if (masked.config.type_project === 'valkey') {
|
|
3147
3915
|
lines.push(` Valkey: perfil ${masked.config.profile} | versao ${masked.config.version}${masked.config.storage ? ` | storage ${masked.config.storage.capacity}Gi persistente` : ' | sem storage persistente'}`);
|
|
3148
3916
|
}
|
|
@@ -3632,6 +4400,67 @@ async function buildValkeyConfig({ profile, plan, catalog }) {
|
|
|
3632
4400
|
};
|
|
3633
4401
|
}
|
|
3634
4402
|
|
|
4403
|
+
async function buildJobConfig() {
|
|
4404
|
+
const image = {
|
|
4405
|
+
url: await promptWizardText({
|
|
4406
|
+
label: 'Imagem OCI da tarefa',
|
|
4407
|
+
required: true,
|
|
4408
|
+
examples: ['ghcr.io/example/report:1.0.0', 'registry.example.com/team/job@sha256:digest'],
|
|
4409
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
4410
|
+
validate: (value) => {
|
|
4411
|
+
const reference = value.trim();
|
|
4412
|
+
return reference.length >= OCI_IMAGE_REFERENCE_MIN_LENGTH
|
|
4413
|
+
&& reference.length <= OCI_IMAGE_REFERENCE_MAX_LENGTH
|
|
4414
|
+
&& OCI_IMAGE_REFERENCE_PATTERN.test(reference)
|
|
4415
|
+
? null
|
|
4416
|
+
: 'use uma referencia OCI com registro e tag ou digest';
|
|
4417
|
+
},
|
|
4418
|
+
}),
|
|
4419
|
+
is_public: true,
|
|
4420
|
+
};
|
|
4421
|
+
const persistent = await promptWizardBoolean({
|
|
4422
|
+
label: 'Armazenamento persistente',
|
|
4423
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
4424
|
+
examples: ['sim', 'nao'],
|
|
4425
|
+
});
|
|
4426
|
+
const storage = {
|
|
4427
|
+
persistent,
|
|
4428
|
+
capacity: await promptWizardNumber({
|
|
4429
|
+
label: 'Capacidade de armazenamento (Gi)',
|
|
4430
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
4431
|
+
examples: ['1', '5', '10'],
|
|
4432
|
+
min: 1,
|
|
4433
|
+
max: 250,
|
|
4434
|
+
}),
|
|
4435
|
+
...(persistent ? {
|
|
4436
|
+
dir_path_to_persist: await promptWizardText({
|
|
4437
|
+
label: 'Diretorio persistente',
|
|
4438
|
+
required: true,
|
|
4439
|
+
examples: ['/data'],
|
|
4440
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
4441
|
+
}),
|
|
4442
|
+
} : {}),
|
|
4443
|
+
};
|
|
4444
|
+
const envs = await promptWizardPairs({
|
|
4445
|
+
introLabel: 'Deseja adicionar variaveis de ambiente?',
|
|
4446
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
4447
|
+
examples: ['sim', 'nao'],
|
|
4448
|
+
stateKey: 'jobEnvs',
|
|
4449
|
+
itemLabels: [
|
|
4450
|
+
{ key: 'name', label: 'Nome da env', required: true, examples: ['MODE'], docs: DOCS_CONFIGURATION_URL },
|
|
4451
|
+
{ key: 'value', label: 'Valor da env', required: true, examples: ['daily'], docs: DOCS_CONFIGURATION_URL },
|
|
4452
|
+
],
|
|
4453
|
+
});
|
|
4454
|
+
const cron = await promptWizardText({
|
|
4455
|
+
label: 'Cron em UTC (cinco campos)',
|
|
4456
|
+
required: true,
|
|
4457
|
+
examples: ['0 * * * *'],
|
|
4458
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
4459
|
+
validate: (value) => value.split(/\s+/).filter(Boolean).length === 5 ? null : 'use exatamente cinco campos em UTC',
|
|
4460
|
+
});
|
|
4461
|
+
return { type_project: 'job', image, envs, storage, job: { cron } };
|
|
4462
|
+
}
|
|
4463
|
+
|
|
3635
4464
|
async function runProjectCreateWizard(session, flags) {
|
|
3636
4465
|
const catalogs = await fetchWizardCatalogs(session, flags);
|
|
3637
4466
|
const ui = createWizardUi();
|
|
@@ -3660,11 +4489,15 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
3660
4489
|
docs: DOCS_CONFIGURATION_URL,
|
|
3661
4490
|
examples: WIZARD_TYPE_PROJECT_OPTIONS,
|
|
3662
4491
|
options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
|
|
4492
|
+
extraValues: ['job'],
|
|
3663
4493
|
});
|
|
3664
4494
|
ui.state.typeProject = typeProject;
|
|
3665
4495
|
const valkeyCatalog = typeProject === 'valkey'
|
|
3666
4496
|
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3667
4497
|
: undefined;
|
|
4498
|
+
const jobPlans = typeProject === 'job'
|
|
4499
|
+
? requireJobPlans(unwrapData(await request(session, flags, 'GET', '/project/job/plans')))
|
|
4500
|
+
: undefined;
|
|
3668
4501
|
const profile = typeProject === 'valkey'
|
|
3669
4502
|
? await promptWizardSelect({
|
|
3670
4503
|
label: 'Perfil Valkey',
|
|
@@ -3682,6 +4515,8 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
3682
4515
|
? valkeyCatalog.plans
|
|
3683
4516
|
.filter((planDefinition) => planDefinition.profile === profile)
|
|
3684
4517
|
.map((planDefinition) => planDefinition.id)
|
|
4518
|
+
: typeProject === 'job'
|
|
4519
|
+
? sortWizardOptions(jobPlans.map((plan) => plan?.plan || plan?.id).filter(Boolean), WIZARD_JOB_PLAN_OPTIONS)
|
|
3685
4520
|
: sortWizardOptions(catalogs.databasePlans
|
|
3686
4521
|
.map((plan) => plan?.plan)
|
|
3687
4522
|
.filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
|
|
@@ -3689,7 +4524,8 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
3689
4524
|
label: 'Plano',
|
|
3690
4525
|
docs: typeProject === 'http'
|
|
3691
4526
|
? DOCS_CREATE_HTTP_URL
|
|
3692
|
-
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL
|
|
4527
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL
|
|
4528
|
+
: typeProject === 'job' ? DOCS_CONFIGURATION_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
3693
4529
|
examples: [planOptions[0], planOptions.at(-1)],
|
|
3694
4530
|
options: planOptions.map((value) => ({ value })),
|
|
3695
4531
|
});
|
|
@@ -3699,7 +4535,7 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
3699
4535
|
ui.state.planAllowsSubdomain = isSubdomainCustomizable(catalogs.httpPlans, plan);
|
|
3700
4536
|
ui.state.planAllowsAutoscaling = isAutoscalingEnabledForPlan(catalogs.httpPlans, plan);
|
|
3701
4537
|
}
|
|
3702
|
-
const paymentMode = await promptWizardSelect({
|
|
4538
|
+
const paymentMode = typeProject === 'job' ? 'per_minute' : await promptWizardSelect({
|
|
3703
4539
|
label: 'Modo de pagamento',
|
|
3704
4540
|
docs: DOCS_PAYMENTS_URL,
|
|
3705
4541
|
examples: WIZARD_PAYMENT_MODE_OPTIONS,
|
|
@@ -3717,6 +4553,8 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
3717
4553
|
config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
|
|
3718
4554
|
} else if (typeProject === 'valkey') {
|
|
3719
4555
|
config = await buildValkeyConfig({ profile, plan, catalog: valkeyCatalog });
|
|
4556
|
+
} else if (typeProject === 'job') {
|
|
4557
|
+
config = await buildJobConfig();
|
|
3720
4558
|
} else {
|
|
3721
4559
|
config = await buildMariadbConfig();
|
|
3722
4560
|
}
|
|
@@ -3753,18 +4591,26 @@ async function handleProjectCreate(session, flags) {
|
|
|
3753
4591
|
const name = wizardPayload?.name || flags.name || await prompt('Nome do projeto');
|
|
3754
4592
|
const description = wizardPayload?.description || flags.description;
|
|
3755
4593
|
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
3756
|
-
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
3757
4594
|
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
3758
4595
|
const idempotencyKey = flags.idempotencyKey === undefined ? undefined : validateIdempotencyKey(flags.idempotencyKey);
|
|
3759
4596
|
if (Array.isArray(config?.custom_domains)) {
|
|
3760
4597
|
config.custom_domains = normalizeCustomDomains(config.custom_domains, { primaryDomain: config.domain });
|
|
3761
4598
|
}
|
|
3762
4599
|
const typeProject = normalizeTypeProject(config?.type_project);
|
|
4600
|
+
const paymentMode = typeProject === 'job'
|
|
4601
|
+
? flags.paymentMode || 'per_minute'
|
|
4602
|
+
: wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
3763
4603
|
const valkeyCatalog = typeProject === 'valkey'
|
|
3764
4604
|
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3765
4605
|
: undefined;
|
|
3766
4606
|
const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
|
|
3767
4607
|
|
|
4608
|
+
if (validated.config.source !== undefined || validated.config.build !== undefined) {
|
|
4609
|
+
if (!await supportsNeutralGitRoutes(session, flags, orgId)) {
|
|
4610
|
+
throw new CliError('Esta API nao oferece suporte a criacao de projetos com origem Git generica. Atualize a API ou use a configuracao GitHub legada compativel.');
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
|
|
3768
4614
|
const payload = await request(session, flags, 'POST', '/project', {
|
|
3769
4615
|
orgId,
|
|
3770
4616
|
headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
|
|
@@ -3777,14 +4623,16 @@ async function handleProjectCreate(session, flags) {
|
|
|
3777
4623
|
},
|
|
3778
4624
|
});
|
|
3779
4625
|
|
|
3780
|
-
if (flags.json)
|
|
4626
|
+
if (flags.json) {
|
|
4627
|
+
return printJson(validated.config.type_project === 'job' ? sanitizeJobProjectResponse(payload) : payload);
|
|
4628
|
+
}
|
|
3781
4629
|
if (wizardPayload) {
|
|
3782
4630
|
closePromptResources();
|
|
3783
4631
|
}
|
|
3784
4632
|
const project = unwrapData(payload);
|
|
3785
4633
|
const rows = [
|
|
3786
4634
|
{ field: 'Projeto', value: projectIdOf(project) || 'id indisponivel' },
|
|
3787
|
-
...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
|
|
4635
|
+
...(validated.config.type_project === 'job' ? [] : project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
|
|
3788
4636
|
...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
|
|
3789
4637
|
];
|
|
3790
4638
|
if (validated.config.type_project === 'valkey') {
|
|
@@ -3819,7 +4667,9 @@ async function handleProjectInfo(session, flags) {
|
|
|
3819
4667
|
const orgId = await resolveOrgId(session, flags);
|
|
3820
4668
|
const project = await getProject(session, flags, projectId, orgId);
|
|
3821
4669
|
|
|
3822
|
-
if (flags.json) return printJson(projectInfoForOutput(
|
|
4670
|
+
if (flags.json) return printJson(projectInfoForOutput(
|
|
4671
|
+
isJobProject(project) ? sanitizePublicProject(project) : project,
|
|
4672
|
+
));
|
|
3823
4673
|
printProject({ ...project, id: projectId });
|
|
3824
4674
|
}
|
|
3825
4675
|
|
|
@@ -4032,6 +4882,231 @@ async function handleProjectLogs(session, flags) {
|
|
|
4032
4882
|
printLogs(logs, flags);
|
|
4033
4883
|
}
|
|
4034
4884
|
|
|
4885
|
+
const PUBLIC_JOB_RUN_KEYS = [
|
|
4886
|
+
'status',
|
|
4887
|
+
'scheduled_at',
|
|
4888
|
+
'started_at',
|
|
4889
|
+
'finished_at',
|
|
4890
|
+
'duration_seconds',
|
|
4891
|
+
'billed_minutes',
|
|
4892
|
+
'plan',
|
|
4893
|
+
'currency',
|
|
4894
|
+
'amount',
|
|
4895
|
+
];
|
|
4896
|
+
|
|
4897
|
+
function sanitizeJobRun(run) {
|
|
4898
|
+
if (!run || typeof run !== 'object' || Array.isArray(run)) return {};
|
|
4899
|
+
const output = {};
|
|
4900
|
+
const id = run.id || run._id || run.run_id;
|
|
4901
|
+
if (id !== undefined) output.id = id;
|
|
4902
|
+
for (const key of PUBLIC_JOB_RUN_KEYS) {
|
|
4903
|
+
if (run[key] !== undefined) output[key] = run[key];
|
|
4904
|
+
}
|
|
4905
|
+
return output;
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4908
|
+
function jobRunsDataOf(data) {
|
|
4909
|
+
if (Array.isArray(data)) return { runs: data.map(sanitizeJobRun), pagination: null };
|
|
4910
|
+
if (!data || typeof data !== 'object') return { runs: [], pagination: null };
|
|
4911
|
+
return {
|
|
4912
|
+
runs: Array.isArray(data.runs) ? data.runs.map(sanitizeJobRun) : [],
|
|
4913
|
+
pagination: data.pagination ?? null,
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
function sanitizeJobRunResponse(data) {
|
|
4918
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && data.run !== undefined) {
|
|
4919
|
+
return { run: sanitizeJobRun(data.run) };
|
|
4920
|
+
}
|
|
4921
|
+
return sanitizeJobRun(data);
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
function formatJobRunDuration(durationSeconds) {
|
|
4925
|
+
if (durationSeconds === undefined || durationSeconds === null || !Number.isFinite(Number(durationSeconds))) return '-';
|
|
4926
|
+
return `${Number(Number(durationSeconds).toFixed(1))} s`;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
function formatJobRunAmount(run) {
|
|
4930
|
+
const amount = run.amount;
|
|
4931
|
+
if (amount === undefined || amount === null) return '-';
|
|
4932
|
+
return formatJobBrlFromCents(amount);
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
function printJobRuns(data) {
|
|
4936
|
+
const runs = asArray(data?.runs);
|
|
4937
|
+
printTable(runs, [
|
|
4938
|
+
{ label: 'ID', value: (run) => run.id || '-' },
|
|
4939
|
+
{ label: 'Status', value: (run) => run.status || '-' },
|
|
4940
|
+
{ label: 'Agendada', value: (run) => run.scheduled_at || '-' },
|
|
4941
|
+
{ label: 'Iniciada', value: (run) => run.started_at || '-' },
|
|
4942
|
+
{ label: 'Finalizada', value: (run) => run.finished_at || '-' },
|
|
4943
|
+
{ label: 'Duracao', value: (run) => formatJobRunDuration(run.duration_seconds) },
|
|
4944
|
+
{ label: 'Minutos cobrados', value: (run) => run.billed_minutes ?? '-' },
|
|
4945
|
+
{ label: 'Valor', value: formatJobRunAmount },
|
|
4946
|
+
]);
|
|
4947
|
+
printPagination(data?.pagination, 'execucao');
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
function sanitizeJobRunLogEntries(logs) {
|
|
4951
|
+
if (!Array.isArray(logs)) return logs;
|
|
4952
|
+
return logs.map((entry) => {
|
|
4953
|
+
if (typeof entry === 'string') return entry;
|
|
4954
|
+
if (!entry || typeof entry !== 'object') return String(entry ?? '');
|
|
4955
|
+
return Object.fromEntries(['sequence', 'timestamp', 'level', 'message']
|
|
4956
|
+
.filter((key) => entry[key] !== undefined)
|
|
4957
|
+
.map((key) => [key, entry[key]]));
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
function sanitizeJobRunLogs(data) {
|
|
4962
|
+
if (typeof data === 'string') return data;
|
|
4963
|
+
if (Array.isArray(data)) return sanitizeJobRunLogEntries(data);
|
|
4964
|
+
if (!data || typeof data !== 'object') return data;
|
|
4965
|
+
const output = {};
|
|
4966
|
+
for (const key of ['logs', 'next_cursor', 'truncated', 'finished', 'status']) {
|
|
4967
|
+
if (data[key] !== undefined) output[key] = key === 'logs' ? sanitizeJobRunLogEntries(data[key]) : data[key];
|
|
4968
|
+
}
|
|
4969
|
+
return output;
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
function skipTerminalString(text, index) {
|
|
4973
|
+
while (index < text.length) {
|
|
4974
|
+
const code = text.charCodeAt(index);
|
|
4975
|
+
if (code === 0x07 || code === 0x9c) return index + 1;
|
|
4976
|
+
if (code === 0x1b && text.charCodeAt(index + 1) === 0x5c) return index + 2;
|
|
4977
|
+
index += 1;
|
|
4978
|
+
}
|
|
4979
|
+
return index;
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
function skipTerminalCsi(text, index) {
|
|
4983
|
+
while (index < text.length) {
|
|
4984
|
+
const code = text.charCodeAt(index);
|
|
4985
|
+
index += 1;
|
|
4986
|
+
if (code >= 0x40 && code <= 0x7e) break;
|
|
4987
|
+
}
|
|
4988
|
+
return index;
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
function skipTerminalEscape(text, index) {
|
|
4992
|
+
const next = text.charCodeAt(index);
|
|
4993
|
+
if (next >= 0x30 && next <= 0x7e) return index + 1;
|
|
4994
|
+
while (index < text.length && text.charCodeAt(index) >= 0x20 && text.charCodeAt(index) <= 0x2f) {
|
|
4995
|
+
index += 1;
|
|
4996
|
+
}
|
|
4997
|
+
if (index < text.length && text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x7e) {
|
|
4998
|
+
return index + 1;
|
|
4999
|
+
}
|
|
5000
|
+
return index;
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
function stripTerminalControls(value) {
|
|
5004
|
+
const text = String(value ?? '');
|
|
5005
|
+
let output = '';
|
|
5006
|
+
let index = 0;
|
|
5007
|
+
|
|
5008
|
+
while (index < text.length) {
|
|
5009
|
+
const code = text.charCodeAt(index);
|
|
5010
|
+
if (code === 0x1b) {
|
|
5011
|
+
const next = text.charCodeAt(index + 1);
|
|
5012
|
+
if (next === 0x5d) {
|
|
5013
|
+
index = skipTerminalString(text, index + 2);
|
|
5014
|
+
} else if (next === 0x50 || next === 0x58 || next === 0x5e || next === 0x5f) {
|
|
5015
|
+
index = skipTerminalString(text, index + 2);
|
|
5016
|
+
} else if (next === 0x5b) {
|
|
5017
|
+
index = skipTerminalCsi(text, index + 2);
|
|
5018
|
+
} else {
|
|
5019
|
+
index = skipTerminalEscape(text, index + 1);
|
|
5020
|
+
}
|
|
5021
|
+
continue;
|
|
5022
|
+
}
|
|
5023
|
+
if (code === 0x9b) {
|
|
5024
|
+
index = skipTerminalCsi(text, index + 1);
|
|
5025
|
+
continue;
|
|
5026
|
+
}
|
|
5027
|
+
if (code === 0x9d || code === 0x90 || code === 0x98 || code === 0x9e || code === 0x9f) {
|
|
5028
|
+
index = skipTerminalString(text, index + 1);
|
|
5029
|
+
continue;
|
|
5030
|
+
}
|
|
5031
|
+
if ((code <= 0x1f && code !== 0x09 && code !== 0x0a) || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
|
|
5032
|
+
index += 1;
|
|
5033
|
+
continue;
|
|
5034
|
+
}
|
|
5035
|
+
output += text[index];
|
|
5036
|
+
index += 1;
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
return output;
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
function printHumanJobRunLogs(logs) {
|
|
5043
|
+
if (Array.isArray(logs)) {
|
|
5044
|
+
for (const log of logs) {
|
|
5045
|
+
const line = typeof log === 'string' ? log : buildLogLineOf(log);
|
|
5046
|
+
process.stdout.write(`${stripTerminalControls(line)}\n`);
|
|
5047
|
+
}
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
const value = logs || '';
|
|
5051
|
+
process.stdout.write(`${stripTerminalControls(value)}${String(value).endsWith('\n') ? '' : '\n'}`);
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
async function handleProjectRuns(session, flags) {
|
|
5055
|
+
const projectId = requireProjectId(flags, 'project runs');
|
|
5056
|
+
if (!projectId) return;
|
|
5057
|
+
validateProjectListFlags(flags, { maxLimit: 50 });
|
|
5058
|
+
const orgId = await resolveOrgId(session, flags);
|
|
5059
|
+
const query = buildQuery(flags, ['page', 'limit']);
|
|
5060
|
+
const data = jobRunsDataOf(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/job-runs${query}`, { orgId })));
|
|
5061
|
+
|
|
5062
|
+
if (flags.json) return printJson(data);
|
|
5063
|
+
printJobRuns(data);
|
|
5064
|
+
}
|
|
5065
|
+
|
|
5066
|
+
async function handleProjectRunCancel(session, flags) {
|
|
5067
|
+
const projectId = requireProjectId(flags, 'project runs cancel');
|
|
5068
|
+
if (!projectId) return;
|
|
5069
|
+
if (!flags.run && !flags.runId || flags.run === true || flags.runId === true) {
|
|
5070
|
+
printCommandHelpAndFail('project runs cancel');
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
const runId = String(flags.run || flags.runId);
|
|
5074
|
+
const orgId = await resolveOrgId(session, flags);
|
|
5075
|
+
const data = unwrapData(await request(
|
|
5076
|
+
session,
|
|
5077
|
+
flags,
|
|
5078
|
+
'POST',
|
|
5079
|
+
`/project/${projectId}/job-runs/${encodeURIComponent(runId)}/cancel`,
|
|
5080
|
+
{ orgId, body: {} },
|
|
5081
|
+
));
|
|
5082
|
+
|
|
5083
|
+
if (flags.json) return printJson(sanitizeJobRunResponse(data));
|
|
5084
|
+
const run = data?.run || data;
|
|
5085
|
+
process.stdout.write(`Execucao: ${run?.status || 'cancelled'}\n`);
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
async function handleProjectRunLogs(session, flags) {
|
|
5089
|
+
const projectId = requireProjectId(flags, 'project runs logs');
|
|
5090
|
+
if (!projectId) return;
|
|
5091
|
+
if (!flags.run && !flags.runId || flags.run === true || flags.runId === true) {
|
|
5092
|
+
printCommandHelpAndFail('project runs logs');
|
|
5093
|
+
return;
|
|
5094
|
+
}
|
|
5095
|
+
const runId = String(flags.run || flags.runId);
|
|
5096
|
+
const orgId = await resolveOrgId(session, flags);
|
|
5097
|
+
const data = sanitizeJobRunLogs(unwrapData(await request(
|
|
5098
|
+
session,
|
|
5099
|
+
flags,
|
|
5100
|
+
'GET',
|
|
5101
|
+
`/project/${projectId}/job-runs/${encodeURIComponent(runId)}/logs`,
|
|
5102
|
+
{ orgId },
|
|
5103
|
+
)));
|
|
5104
|
+
|
|
5105
|
+
if (flags.json) return printJson(data);
|
|
5106
|
+
const logs = typeof data === 'object' && data !== null && !Array.isArray(data) ? data.logs : data;
|
|
5107
|
+
printHumanJobRunLogs(logs);
|
|
5108
|
+
}
|
|
5109
|
+
|
|
4035
5110
|
async function handleProjectMetricsCapabilities(session, flags) {
|
|
4036
5111
|
const projectId = requireProjectId(flags, 'project metrics capabilities');
|
|
4037
5112
|
if (!projectId) return;
|
|
@@ -4742,8 +5817,9 @@ async function handleDeployments(session, flags) {
|
|
|
4742
5817
|
return
|
|
4743
5818
|
}
|
|
4744
5819
|
const orgId = await resolveOrgId(session, flags);
|
|
5820
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
4745
5821
|
const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
|
|
4746
|
-
const data = unwrapData(await request(session, flags, 'GET',
|
|
5822
|
+
const data = unwrapData(await request(session, flags, 'GET', `${projectApiPath}/builds${query}`, { orgId }));
|
|
4747
5823
|
const builds = asArray(data);
|
|
4748
5824
|
|
|
4749
5825
|
if (flags.json) return printJson(data);
|
|
@@ -4764,11 +5840,12 @@ async function handleDeploy(session, flags) {
|
|
|
4764
5840
|
return;
|
|
4765
5841
|
}
|
|
4766
5842
|
const orgId = await resolveOrgId(session, flags);
|
|
5843
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
4767
5844
|
const body = {};
|
|
4768
5845
|
if (flags.branch) body.branch = String(flags.branch);
|
|
4769
5846
|
if (flags.commitSha) body.commit_sha = String(flags.commitSha);
|
|
4770
5847
|
|
|
4771
|
-
const payload = await request(session, flags, 'POST',
|
|
5848
|
+
const payload = await request(session, flags, 'POST', `${projectApiPath}/deploy`, {
|
|
4772
5849
|
orgId,
|
|
4773
5850
|
body,
|
|
4774
5851
|
});
|
|
@@ -4778,9 +5855,9 @@ async function handleDeploy(session, flags) {
|
|
|
4778
5855
|
process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
|
|
4779
5856
|
}
|
|
4780
5857
|
|
|
4781
|
-
async function getBuildLogs(session, flags,
|
|
5858
|
+
async function getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor = 0, limit = 200 } = {}) {
|
|
4782
5859
|
const query = buildQuery({ cursor, limit }, ['cursor', 'limit'])
|
|
4783
|
-
return unwrapData(await request(session, flags, 'GET',
|
|
5860
|
+
return unwrapData(await request(session, flags, 'GET', `${projectApiPath}/builds/${buildId}/logs${query}`, { orgId }))
|
|
4784
5861
|
}
|
|
4785
5862
|
|
|
4786
5863
|
function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
|
|
@@ -4831,7 +5908,7 @@ function printBuildLogEventsJson(logs) {
|
|
|
4831
5908
|
}
|
|
4832
5909
|
}
|
|
4833
5910
|
|
|
4834
|
-
async function followBuildLogs(session, flags,
|
|
5911
|
+
async function followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4835
5912
|
initialCursor = 0,
|
|
4836
5913
|
limit = 200,
|
|
4837
5914
|
intervalSeconds = 5,
|
|
@@ -4844,7 +5921,7 @@ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
|
4844
5921
|
const startedAt = Date.now()
|
|
4845
5922
|
|
|
4846
5923
|
while (true) {
|
|
4847
|
-
const result = await getBuildLogs(session, flags,
|
|
5924
|
+
const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4848
5925
|
cursor,
|
|
4849
5926
|
limit: pollLimit,
|
|
4850
5927
|
})
|
|
@@ -4885,20 +5962,27 @@ async function handleBuildLogs(session, flags) {
|
|
|
4885
5962
|
return
|
|
4886
5963
|
}
|
|
4887
5964
|
|
|
4888
|
-
const orgId = await resolveOrgId(session, flags);
|
|
4889
5965
|
const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
4890
5966
|
const limit = parseIntegerOption(flags.limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
5967
|
+
const intervalSeconds = flags.follow
|
|
5968
|
+
? parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 })
|
|
5969
|
+
: undefined;
|
|
5970
|
+
const timeoutSeconds = flags.follow
|
|
5971
|
+
? parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 })
|
|
5972
|
+
: undefined;
|
|
5973
|
+
const orgId = await resolveOrgId(session, flags);
|
|
5974
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
4891
5975
|
|
|
4892
5976
|
if (flags.follow) {
|
|
4893
|
-
return followBuildLogs(session, flags,
|
|
5977
|
+
return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4894
5978
|
initialCursor: cursor,
|
|
4895
5979
|
limit,
|
|
4896
|
-
intervalSeconds
|
|
4897
|
-
timeoutSeconds
|
|
5980
|
+
intervalSeconds,
|
|
5981
|
+
timeoutSeconds,
|
|
4898
5982
|
})
|
|
4899
5983
|
}
|
|
4900
5984
|
|
|
4901
|
-
const result = await getBuildLogs(session, flags,
|
|
5985
|
+
const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor, limit })
|
|
4902
5986
|
if (flags.json) return printJson(result)
|
|
4903
5987
|
printBuildLogs(result?.logs)
|
|
4904
5988
|
}
|
|
@@ -4911,12 +5995,15 @@ async function handleDeployWatch(session, flags) {
|
|
|
4911
5995
|
return;
|
|
4912
5996
|
}
|
|
4913
5997
|
|
|
5998
|
+
const intervalSeconds = parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 });
|
|
5999
|
+
const timeoutSeconds = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 });
|
|
4914
6000
|
const orgId = await resolveOrgId(session, flags);
|
|
4915
|
-
|
|
6001
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
6002
|
+
return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4916
6003
|
initialCursor: 0,
|
|
4917
6004
|
limit: 200,
|
|
4918
|
-
intervalSeconds
|
|
4919
|
-
timeoutSeconds
|
|
6005
|
+
intervalSeconds,
|
|
6006
|
+
timeoutSeconds,
|
|
4920
6007
|
})
|
|
4921
6008
|
}
|
|
4922
6009
|
|
|
@@ -4962,6 +6049,15 @@ async function main() {
|
|
|
4962
6049
|
if (command === 'login') return await handleLogin(session, flags);
|
|
4963
6050
|
if (command === 'logout') return handleLogout(session, flags);
|
|
4964
6051
|
if (command === 'plans') return handlePlans(session, flags);
|
|
6052
|
+
if (command === 'git' && subcommand === 'providers') return handleGitProviders(session, flags);
|
|
6053
|
+
if (command === 'git' && subcommand === 'runtimes') return handleGitRuntimes(session, flags);
|
|
6054
|
+
if (command === 'git' && subcommand === 'connections') return handleGitConnections(session, flags);
|
|
6055
|
+
if (command === 'git' && subcommand === 'repositories' && positional.length === 2) {
|
|
6056
|
+
process.stdout.write(commandHelp(['git', 'repositories']));
|
|
6057
|
+
return;
|
|
6058
|
+
}
|
|
6059
|
+
if (command === 'git' && subcommand === 'repositories' && positional[2] === 'resolve') return handleGitRepositoryResolve(session, flags);
|
|
6060
|
+
if (command === 'git' && subcommand === 'branches') return handleGitBranches(session, flags);
|
|
4965
6061
|
if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
|
|
4966
6062
|
if (command === 'orgs') return handleOrgs(session, flags);
|
|
4967
6063
|
if (command === 'whoami') return handleWhoami(session, flags);
|
|
@@ -4989,11 +6085,24 @@ async function main() {
|
|
|
4989
6085
|
return;
|
|
4990
6086
|
}
|
|
4991
6087
|
if (command === 'project' && subcommand === 'github' && positional.length === 2) return handleProjectGithub(session, flags);
|
|
6088
|
+
if (command === 'project' && subcommand === 'source' && positional.length === 2) return handleProjectSource(session, flags);
|
|
6089
|
+
if (command === 'project' && subcommand === 'source' && positional[2] === 'branches') return handleProjectSourceBranches(session, flags);
|
|
6090
|
+
if (command === 'project' && subcommand === 'source' && positional[2] === 'deploy-settings' && positional[3] === 'set') {
|
|
6091
|
+
return handleProjectSourceDeploySettingsSet(session, flags);
|
|
6092
|
+
}
|
|
6093
|
+
if (command === 'project' && subcommand === 'source' && positional[2] === 'deploy-settings') {
|
|
6094
|
+
process.stdout.write(commandHelp(['project', 'source', 'deploy-settings']));
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
4992
6097
|
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
4993
6098
|
if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
|
|
4994
6099
|
if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
|
|
4995
6100
|
if (command === 'project' && subcommand === 'delete') return handleProjectDelete(session, flags);
|
|
4996
6101
|
if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
|
|
6102
|
+
if (command === 'project' && subcommand === 'runs' && positional[2] === 'cancel') return handleProjectRunCancel(session, flags);
|
|
6103
|
+
if (command === 'project' && subcommand === 'runs' && positional[2] === 'logs') return handleProjectRunLogs(session, flags);
|
|
6104
|
+
if (command === 'project' && subcommand === 'runs') return handleProjectRuns(session, flags);
|
|
6105
|
+
if (command === 'project' && subcommand === 'run' && positional[2] === 'logs') return handleProjectRunLogs(session, flags);
|
|
4997
6106
|
if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
|
|
4998
6107
|
if (command === 'project' && subcommand === 'metrics' && positional[2] === 'capabilities') return handleProjectMetricsCapabilities(session, flags);
|
|
4999
6108
|
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|