@zenifra/cli 0.4.0 → 0.5.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 CHANGED
@@ -54,9 +54,15 @@ zenifra plans
54
54
  zenifra plans --type http
55
55
  zenifra plans --type valkey
56
56
  zenifra plans --type storage --json
57
+ zenifra git providers
58
+ zenifra git runtimes
59
+ zenifra git connections
60
+ zenifra git repositories resolve --connection <connection-id> --path equipe/aplicacao
61
+ zenifra git branches --connection <connection-id> --repository <repository-id>
57
62
  zenifra create project
58
63
  zenifra create project --name <name> --plan free --payment-mode hourly --config @examples/http-project.json
59
64
  zenifra create project --name <name> --plan basic --payment-mode hourly --config @examples/http-github-project.json
65
+ zenifra create project --name <name> --plan basic --payment-mode hourly --config @examples/http-git-project.json
60
66
  zenifra create project --name <name> --plan premium --payment-mode hourly --config @examples/http-autoscaling-project.json
61
67
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
62
68
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
@@ -82,6 +88,9 @@ zenifra project network --project <project-id> --view summary
82
88
  zenifra project image set --project <project-id> --image ghcr.io/zenifra/app:tag
83
89
  zenifra project github --project <project-id>
84
90
  zenifra project github deploy-settings set --project <project-id> --mode branch
91
+ zenifra project source --project <project-id>
92
+ zenifra project source branches --project <project-id>
93
+ zenifra project source deploy-settings set --project <project-id> --mode branch
85
94
  zenifra project github deploy-settings set --project <project-id> --mode tag --tag-pattern "v*"
86
95
  zenifra project github deploy-settings set --project <project-id> --mode release --tag-pattern "v*" --include-prereleases true
87
96
  zenifra project envs --project <project-id>
@@ -138,9 +147,9 @@ zenifra project metrics capabilities --project <project-id> --json
138
147
 
139
148
  ---
140
149
 
141
- ## Builds GitHub
150
+ ## Builds e deployments Git
142
151
 
143
- Use `zenifra builds` para listar o historico de builds e `zenifra builds logs` para ler os logs do pipeline GitHub de um build especifico.
152
+ Use `zenifra builds` para listar o historico de builds e `zenifra builds logs` para ler os logs de um build de projeto com origem Git.
144
153
 
145
154
  ```bash
146
155
  zenifra builds --project <project-id>
@@ -153,10 +162,37 @@ zenifra deploy watch --project <project-id> --build <build-id>
153
162
  Fluxos:
154
163
 
155
164
  - `zenifra project logs`: logs da aplicacao em execucao
156
- - `zenifra builds logs`: logs do build GitHub
157
- - `zenifra deploy`: dispara o build/deploy GitHub e retorna o `build_id`
165
+ - `zenifra builds logs`: logs do build Git
166
+ - `zenifra deploy`: dispara o build/deploy Git e retorna o `build_id`
158
167
  - `zenifra deploy watch`: usa esse `build_id` para acompanhar o build em tempo real e imprimir os logs incrementais ate o fim
159
168
 
169
+ Para criar um projeto HTTP com uma origem Git Forgejo, primeiro uma pessoa proprietaria da organizacao cria a conexao no Console. A CLI lista e usa conexoes existentes; ela nao solicita nem exibe credenciais do provedor.
170
+
171
+ Use os catalogos para conferir provedores, capacidades e runtimes, depois resolva o caminho explicito do repositorio. O ID opaco retornado pertence a essa conexao e deve ser usado com ela ao listar branches:
172
+
173
+ ```bash
174
+ zenifra git providers --json
175
+ zenifra git runtimes
176
+ zenifra git connections --json
177
+ zenifra git repositories resolve --connection <connection-id> --path equipe/aplicacao --json
178
+ zenifra git branches --connection <connection-id> --repository <repository-id>
179
+ ```
180
+
181
+ Use os IDs confirmados em `config.source` e `config.build`, como no arquivo `examples/http-git-project.json`, para criar o projeto. Esse exemplo contem valores ilustrativos e nao credenciais:
182
+
183
+ ```bash
184
+ zenifra create project --name api-web --plan basic --payment-mode hourly --config @examples/http-git-project.json
185
+ zenifra project source --project <project-id> --json
186
+ zenifra project source branches --project <project-id>
187
+ zenifra project source deploy-settings set --project <project-id> --mode branch
188
+ zenifra deploy --project <project-id> --branch main
189
+ zenifra deploy watch --project <project-id> --build <build-id>
190
+ ```
191
+
192
+ O deploy manual retorna um ID de build para acompanhar com `zenifra deploy watch`. Para ativar deploy por tag ou release, use `zenifra project source deploy-settings set --project <project-id> --mode tag --tag-pattern "v*"` ou o modo `release`; prereleases podem ser habilitadas somente no modo `release`. O comando preserva a origem e as configuracoes de build e confirma a alteracao com uma leitura posterior. A API nao oferece precondicao de revisao para essa atualizacao; evite alterar a origem ao mesmo tempo pelo Console ou por outro cliente. Projetos com configuracao GitHub legada continuam usando `zenifra project github` e `zenifra project github deploy-settings set`.
193
+
194
+ Em APIs antigas que ainda nao anunciam as rotas Git neutras, a CLI usa as rotas anteriores somente quando consegue confirmar a origem GitHub legada do projeto. Essa verificacao pode exigir permissao de leitura do projeto. Se a API negar essa leitura, a CLI encerra o comando sem tentar a rota antiga; projetos Forgejo ou com outra origem generica nunca sao tratados como GitHub.
195
+
160
196
  Alguns builds antigos podem disponibilizar somente um resumo terminal. Nesse caso, a saida legivel avisa que eventos detalhados nao estavam disponiveis; `--json` preserva a resposta recebida da API.
161
197
 
162
198
  ### Modos de deploy GitHub
@@ -309,6 +345,7 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
309
345
  - `examples/http-github-project.json`: projeto HTTP com build via GitHub
310
346
  - `examples/http-github-tag-project.json`: projeto HTTP com deploy por tag e padrao exato
311
347
  - `examples/http-github-release-project.json`: projeto HTTP com deploy por release e padrao `v*`
348
+ - `examples/http-git-project.json`: projeto HTTP com origem Git por conexao configurada no Console/API
312
349
  - `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
313
350
  - `examples/postgresql-project.json`: projeto PostgreSQL
314
351
  - `examples/mariadb-project.json`: projeto MariaDB
@@ -348,6 +385,7 @@ Valores aceitos:
348
385
  - `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
349
386
  - `config.github.auto_deploy`: use `true` para o modo `branch`; mantenha `false` ao habilitar `version_deploy`
350
387
  - `config.github.version_deploy`: use `enabled: true`, `event: "tag"` ou `"release"` e um `tag_pattern` explicito; `include_prereleases` e opcional e padrao `false`
388
+ - `config.source` e `config.build` (quando houver origem Git por conexao): use os IDs retornados pela configuracao segura no Console/API; nao informe credenciais Git nesses campos
351
389
  - `config.autoscaling` (somente HTTP pago): `enabled: true`, `max_instances` maior ou igual a `config.instances` e alvos opcionais de CPU/memoria entre 1 e 100
352
390
 
353
391
  Observacoes do wizard:
package/bin/zenifra.mjs CHANGED
@@ -37,6 +37,7 @@ const KNOWN_FLAG_NAMES = new Set([
37
37
  'challengeToken',
38
38
  'code',
39
39
  'connectionFile',
40
+ 'connection',
40
41
  'commitSha',
41
42
  'config',
42
43
  'count',
@@ -71,6 +72,7 @@ const KNOWN_FLAG_NAMES = new Set([
71
72
  'plan',
72
73
  'profile',
73
74
  'project',
75
+ 'repository',
74
76
  'revoke',
75
77
  'showValues',
76
78
  'status',
@@ -131,6 +133,15 @@ const GITHUB_PUBLIC_CONFIG_FIELDS = [
131
133
  'build_command',
132
134
  ];
133
135
  const GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS = ['enabled', 'event', 'tag_pattern', 'include_prereleases'];
136
+ const GIT_BUILD_SETTINGS_FIELDS = [
137
+ 'runtime',
138
+ 'version',
139
+ 'start_command',
140
+ 'pre_build_command',
141
+ 'build_command',
142
+ 'dockerfile_path',
143
+ 'context_path',
144
+ ];
134
145
  const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
135
146
  const GITHUB_RUNTIME_VERSIONS = {
136
147
  nodejs: ['24', '22', '20'],
@@ -177,9 +188,10 @@ const ANSI = {
177
188
  };
178
189
 
179
190
  class CliError extends Error {
180
- constructor(message, exitCode = 1) {
191
+ constructor(message, exitCode = 1, httpStatus) {
181
192
  super(message);
182
193
  this.exitCode = exitCode;
194
+ if (httpStatus !== undefined) this.httpStatus = httpStatus;
183
195
  }
184
196
  }
185
197
 
@@ -201,6 +213,11 @@ Usage:
201
213
  zenifra orgs [--json]
202
214
  zenifra org set [--org <id>]
203
215
  zenifra plans [--type <all|http|database|storage|valkey>] [--json]
216
+ zenifra git providers [--json]
217
+ zenifra git runtimes [--json]
218
+ zenifra git connections [--json]
219
+ zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]
220
+ zenifra git branches --connection <id> --repository <opaque-id> [--json]
204
221
  zenifra create project
205
222
  zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
206
223
  zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
@@ -224,6 +241,9 @@ Usage:
224
241
  zenifra project image set --project <id> --image <image> [--json]
225
242
  zenifra project github --project <id> [--json]
226
243
  zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
244
+ zenifra project source --project <id> [--json]
245
+ zenifra project source branches --project <id> [--json]
246
+ zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
227
247
  zenifra project exposure set --project <id> --exposure <public|private> [--json]
228
248
  zenifra project envs --project <id> [--json] [--show-values]
229
249
  zenifra project env add --project <id> --name <name> --value <value> [--json]
@@ -400,6 +420,68 @@ const HELP_SPECS = [
400
420
  output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
401
421
  jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
402
422
  },
423
+ {
424
+ command: 'git',
425
+ 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]',
426
+ description: 'Consulta provedores, runtimes, conexoes e repositorios Git disponiveis na organizacao.',
427
+ examples: ['zenifra git providers', 'zenifra git connections', 'zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao'],
428
+ output: 'Zenifra CLI - git',
429
+ notes: ['A conexao Forgejo e criada no Console por uma pessoa proprietaria da organizacao. A CLI lista e usa conexoes existentes.'],
430
+ },
431
+ {
432
+ command: 'git providers',
433
+ usage: 'zenifra git providers [--json]',
434
+ description: 'Lista provedores Git e as capacidades disponiveis para a organizacao selecionada.',
435
+ flags: ['--json Imprime a resposta publica da API em JSON.'],
436
+ examples: ['zenifra git providers', 'zenifra git providers --json'],
437
+ output: 'Provedor Disponivel Descoberta Deploy por branch Deploy por versao\nforgejo sim nao sim sim',
438
+ jsonOutput: '{"status":"success","data":{"api_version":1,"providers":[{"id":"forgejo","available":true,"capabilities":{"repositoryDiscovery":false,"pushDeploy":true,"nativePreviews":false,"versionDeploy":true}}]}}',
439
+ },
440
+ {
441
+ command: 'git runtimes',
442
+ usage: 'zenifra git runtimes [--json]',
443
+ description: 'Lista runtimes e versoes disponiveis para builds de repositorios Git.',
444
+ flags: ['--json Imprime a resposta publica da API em JSON.'],
445
+ examples: ['zenifra git runtimes', 'zenifra git runtimes --json'],
446
+ output: 'Runtime Versao padrao Versoes\nnodejs 24 24, 22, 20',
447
+ 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"}]}]}',
448
+ },
449
+ {
450
+ command: 'git connections',
451
+ usage: 'zenifra git connections [--json]',
452
+ description: 'Lista as conexoes Git disponiveis para a organizacao selecionada.',
453
+ flags: ['--json Imprime as conexoes publicas em JSON.'],
454
+ examples: ['zenifra git connections', 'zenifra git connections --json'],
455
+ output: 'ID Provedor Nome Status\nconnection-1 forgejo Forgejo team active',
456
+ 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}}]}',
457
+ notes: ['A conexao precisa ter sido criada por uma pessoa proprietaria no Console. As credenciais nao aparecem na resposta.'],
458
+ },
459
+ {
460
+ command: 'git repositories',
461
+ usage: 'zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]',
462
+ description: 'Agrupa comandos para resolver repositorios por caminho explicito.',
463
+ examples: ['zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao'],
464
+ output: 'Zenifra CLI - git repositories',
465
+ },
466
+ {
467
+ command: 'git repositories resolve',
468
+ usage: 'zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]',
469
+ description: 'Valida e resolve o caminho explicito de um repositorio na conexao selecionada.',
470
+ flags: ['--connection <id> ID da conexao Git da organizacao.', '--path <owner/repo> Caminho explicito do repositorio.', '--json Imprime o resultado em JSON.'],
471
+ examples: ['zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao --json'],
472
+ output: 'ID Caminho Branch padrao Privado\nrepo_opaque_1 equipe/aplicacao main sim',
473
+ jsonOutput: '{"status":"success","data":{"id":"repository-id","path":"equipe/aplicacao","default_branch":"main","private":true}}',
474
+ notes: ['Use o ID opaco retornado somente com a mesma conexao.'],
475
+ },
476
+ {
477
+ command: 'git branches',
478
+ usage: 'zenifra git branches --connection <id> --repository <opaque-id> [--json]',
479
+ description: 'Lista branches de um repositorio resolvido pela conexao informada.',
480
+ flags: ['--connection <id> ID da conexao Git da organizacao.', '--repository <id> ID opaco retornado por git repositories resolve.', '--json Imprime a resposta em JSON.'],
481
+ examples: ['zenifra git branches --connection conn-1 --repository repository-id'],
482
+ output: 'Branch Commit\nmain abc123',
483
+ jsonOutput: '{"status":"success","data":[{"name":"main","commit_sha":"<commit-sha>"}]}',
484
+ },
403
485
  {
404
486
  command: 'valkey',
405
487
  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]',
@@ -470,11 +552,61 @@ const HELP_SPECS = [
470
552
  },
471
553
  {
472
554
  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]',
555
+ 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 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
556
  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'],
557
+ 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
558
  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.'],
559
+ notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, github, source, exposure, autoscaling, billing, envs e instances.'],
560
+ },
561
+ {
562
+ command: 'project source',
563
+ 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]',
564
+ description: 'Consulta a origem Git generica de um projeto, lista branches ou altera seu modo de deploy.',
565
+ examples: ['zenifra project source --project proj_1', 'zenifra project source branches --project proj_1'],
566
+ output: 'Zenifra CLI - project source',
567
+ notes: ['Use project github para ler ou alterar configuracoes GitHub legadas.'],
568
+ },
569
+ {
570
+ command: 'project source branches',
571
+ usage: 'zenifra project source branches --project <id> [--json]',
572
+ description: 'Lista branches disponiveis para a origem Git generica atual do projeto.',
573
+ flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
574
+ examples: ['zenifra project source branches --project proj_1'],
575
+ output: 'Branch Commit\nmain abc123',
576
+ jsonOutput: '{"status":"success","data":[{"name":"main","commit_sha":"<commit-sha>"}]}',
577
+ },
578
+ {
579
+ command: 'project source deploy-settings',
580
+ usage: 'zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
581
+ description: 'Agrupa os modos de deploy da origem Git generica.',
582
+ examples: ['zenifra project source deploy-settings set --project proj_1 --mode branch'],
583
+ output: 'Zenifra CLI - project source deploy-settings',
584
+ },
585
+ {
586
+ command: 'project source deploy-settings set',
587
+ usage: 'zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
588
+ description: 'Define e confirma o modo de deploy da origem Git Forgejo do projeto.',
589
+ flags: [
590
+ '--project <id> ID do projeto.',
591
+ '--mode <manual|branch|tag|release> Modo de deploy exclusivo.',
592
+ '--tag-pattern <pattern> Obrigatorio para tag e release; aceita * e ?.',
593
+ '--include-prereleases <true|false> Inclui prereleases somente no modo release. Padrao: false.',
594
+ '--json Imprime a configuracao confirmada em JSON.',
595
+ ],
596
+ examples: [
597
+ 'zenifra project source deploy-settings set --project proj_1 --mode manual',
598
+ 'zenifra project source deploy-settings set --project proj_1 --mode branch',
599
+ 'zenifra project source deploy-settings set --project proj_1 --mode tag --tag-pattern "v*"',
600
+ 'zenifra project source deploy-settings set --project proj_1 --mode release --tag-pattern "v*" --include-prereleases true',
601
+ ],
602
+ output: 'Configuracao de deploy da origem Git confirmada: release (v*).',
603
+ 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}}',
604
+ notes: [
605
+ 'Tag e release exigem --tag-pattern; prereleases so podem ser habilitadas para release.',
606
+ 'O comando preserva identidade, branch e configuracao de build da origem e confirma o resultado com uma nova leitura.',
607
+ 'A API substitui source e build juntos e nao oferece precondicao de revisao. Evite alterar a origem ao mesmo tempo por outro cliente.',
608
+ 'Para projetos com origem GitHub legada, use "zenifra project github deploy-settings set".',
609
+ ],
478
610
  },
479
611
  {
480
612
  command: 'project github',
@@ -770,7 +902,7 @@ const HELP_SPECS = [
770
902
  {
771
903
  command: 'builds',
772
904
  usage: 'zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
773
- description: 'Lista builds GitHub de um projeto.',
905
+ description: 'Lista builds de um projeto com origem Git.',
774
906
  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
907
  examples: ['zenifra builds --project 507f1f77bcf86cd799439012 --status success'],
776
908
  output: 'Build Status Branch Commit\nbuild_123 success main abc123',
@@ -779,7 +911,7 @@ const HELP_SPECS = [
779
911
  {
780
912
  command: 'builds logs',
781
913
  usage: 'zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]',
782
- description: 'Lê os logs de um build GitHub existente e opcionalmente acompanha novos chunks em tempo real.',
914
+ description: 'Le os logs de um build Git existente e opcionalmente acompanha novos eventos em tempo real.',
783
915
  flags: [
784
916
  '--project <id> ID do projeto.',
785
917
  '--build <id> ID do build.',
@@ -797,7 +929,7 @@ const HELP_SPECS = [
797
929
  {
798
930
  command: 'deployments',
799
931
  usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
800
- description: 'Alias para listar builds/deployments GitHub de um projeto.',
932
+ description: 'Alias para listar builds e deployments Git de um projeto.',
801
933
  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
934
  examples: ['zenifra deployments --project 507f1f77bcf86cd799439012'],
803
935
  output: 'Build Status Branch Commit\nbuild_123 success main abc123',
@@ -806,7 +938,7 @@ const HELP_SPECS = [
806
938
  {
807
939
  command: 'deploy',
808
940
  usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
809
- description: 'Dispara um build/deploy GitHub para o projeto e retorna o build_id para acompanhamento posterior.',
941
+ description: 'Dispara um build/deploy Git para o projeto e retorna o build_id para acompanhamento posterior.',
810
942
  flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
811
943
  examples: [
812
944
  'zenifra deploy --project 507f1f77bcf86cd799439012 --branch main',
@@ -900,7 +1032,7 @@ function removedProjectsCreateMessage() {
900
1032
  }
901
1033
 
902
1034
  function isNamespaceCommand(command) {
903
- return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
1035
+ return ['auth', 'profile', 'project', 'org', 'valkey', 'git'].includes(command);
904
1036
  }
905
1037
 
906
1038
  function parseArgs(argv) {
@@ -1692,30 +1824,393 @@ async function request(session, flags, method, path, {
1692
1824
 
1693
1825
  if (!response.ok) {
1694
1826
  const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
1827
+ const httpError = (errorMessage) => new CliError(errorMessage, 1, response.status);
1695
1828
  if (response.status === 401) {
1696
1829
  if (credential?.type === 'api_key') {
1697
- throw new CliError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
1830
+ throw httpError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
1698
1831
  }
1699
- throw new CliError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
1832
+ throw httpError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
1700
1833
  }
1701
1834
  if (response.status === 403) {
1702
- throw new CliError(message);
1835
+ throw httpError(message);
1703
1836
  }
1704
1837
  if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
1705
- throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
1838
+ throw httpError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
1706
1839
  }
1707
1840
  if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
1708
- throw new CliError('Este projeto nao possui acesso a metricas.');
1841
+ throw httpError('Este projeto nao possui acesso a metricas.');
1709
1842
  }
1710
1843
  if (String(message).includes('missing x-organization-id')) {
1711
- throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
1844
+ throw httpError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
1712
1845
  }
1713
- throw new CliError(message);
1846
+ throw httpError(message);
1714
1847
  }
1715
1848
 
1716
1849
  return payload;
1717
1850
  }
1718
1851
 
1852
+ function hasValidGitProvidersApiV1(payload) {
1853
+ if (payload?.status !== 'success' || !isRecord(payload.data) || payload.data.api_version !== 1 || !Array.isArray(payload.data.providers)) {
1854
+ return false;
1855
+ }
1856
+
1857
+ return payload.data.providers.every((provider) => (
1858
+ isRecord(provider)
1859
+ && typeof provider.id === 'string'
1860
+ && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(provider.id)
1861
+ && typeof provider.available === 'boolean'
1862
+ && isRecord(provider.capabilities)
1863
+ && typeof provider.capabilities.repositoryDiscovery === 'boolean'
1864
+ && typeof provider.capabilities.pushDeploy === 'boolean'
1865
+ && typeof provider.capabilities.nativePreviews === 'boolean'
1866
+ ));
1867
+ }
1868
+
1869
+ async function supportsNeutralGitRoutes(session, flags, orgId) {
1870
+ let payload;
1871
+ try {
1872
+ payload = await request(session, flags, 'GET', '/git/providers', { orgId });
1873
+ } catch (error) {
1874
+ if ([404, 405, 501].includes(error?.httpStatus)) return false;
1875
+ throw error;
1876
+ }
1877
+
1878
+ if (!hasValidGitProvidersApiV1(payload)) {
1879
+ throw new CliError('A API retornou uma resposta invalida para as capacidades de origem Git.');
1880
+ }
1881
+ return true;
1882
+ }
1883
+
1884
+ function hasLegacyGithubSource(project) {
1885
+ if (!isRecord(project)) return false;
1886
+ // Public Git source projections must never be rewritten as legacy GitHub input.
1887
+ // preview.source is separate preview metadata and does not identify the primary project source.
1888
+ const genericSourceFields = ['source', 'build', 'git_source', 'git_build'];
1889
+ if (genericSourceFields.some((field) => project[field] !== undefined || project.config?.[field] !== undefined)) {
1890
+ return false;
1891
+ }
1892
+
1893
+ const github = project.github || project.config?.github;
1894
+ return isRecord(github)
1895
+ && typeof github.repository_owner === 'string'
1896
+ && github.repository_owner.trim().length > 0
1897
+ && typeof github.repository_name === 'string'
1898
+ && github.repository_name.trim().length > 0;
1899
+ }
1900
+
1901
+ async function resolveGitProjectApiPath(session, flags, projectId, orgId) {
1902
+ if (await supportsNeutralGitRoutes(session, flags, orgId)) {
1903
+ return `/project/${projectId}`;
1904
+ }
1905
+
1906
+ const project = await getProject(session, flags, projectId, orgId);
1907
+ if (!hasLegacyGithubSource(project)) {
1908
+ throw new CliError('Esta API antiga so permite builds e deploys de projetos com uma origem GitHub legada compativel.');
1909
+ }
1910
+ return `/project/${projectId}/github`;
1911
+ }
1912
+
1913
+ function requiredGitOpaqueId(flags, name, commandKey) {
1914
+ const value = flags[name];
1915
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value)) {
1916
+ printCommandHelpAndFail(commandKey);
1917
+ return null;
1918
+ }
1919
+ return value;
1920
+ }
1921
+
1922
+ function requiredGitRepositoryPath(flags, commandKey) {
1923
+ const value = flags.path;
1924
+ const segments = typeof value === 'string' ? value.split('/') : [];
1925
+ if (typeof value !== 'string'
1926
+ || value.length < 3
1927
+ || value.length > 1024
1928
+ || segments.length < 2
1929
+ || segments.some((segment) => segment.length === 0)
1930
+ || /[\u0000-\u001f\u007f-\u009f\\?#%]/u.test(value)) {
1931
+ printCommandHelpAndFail(commandKey);
1932
+ return null;
1933
+ }
1934
+ return value;
1935
+ }
1936
+
1937
+ function yesNo(value) {
1938
+ if (value === true) return 'sim';
1939
+ if (value === false) return 'nao';
1940
+ return 'indisponivel';
1941
+ }
1942
+
1943
+ async function handleGitProviders(session, flags) {
1944
+ const orgId = await resolveOrgId(session, flags);
1945
+ const payload = await request(session, flags, 'GET', '/git/providers', { orgId });
1946
+ if (!hasValidGitProvidersApiV1(payload)) {
1947
+ throw new CliError('A API retornou uma resposta invalida para o catalogo de provedores Git.');
1948
+ }
1949
+ if (flags.json) return printJson(payload);
1950
+
1951
+ printTable(payload.data.providers, [
1952
+ { label: 'Provedor', value: (provider) => provider.id },
1953
+ { label: 'Disponivel', value: (provider) => yesNo(provider.available) },
1954
+ { label: 'Descoberta', value: (provider) => yesNo(provider.capabilities.repositoryDiscovery) },
1955
+ { label: 'Deploy por branch', value: (provider) => yesNo(provider.capabilities.pushDeploy) },
1956
+ { label: 'Deploy por versao', value: (provider) => yesNo(provider.capabilities.versionDeploy) },
1957
+ ]);
1958
+ }
1959
+
1960
+ async function handleGitRuntimes(session, flags) {
1961
+ const orgId = await resolveOrgId(session, flags);
1962
+ const payload = await request(session, flags, 'GET', '/git/runtime-catalog', { orgId });
1963
+ const runtimes = unwrapData(payload);
1964
+ if (!Array.isArray(runtimes)) {
1965
+ throw new CliError('A API retornou uma resposta invalida para o catalogo de runtimes Git.');
1966
+ }
1967
+ if (flags.json) return printJson(payload);
1968
+
1969
+ printTable(runtimes, [
1970
+ { label: 'Runtime', value: (runtime) => runtime.label || runtime.id || '-' },
1971
+ { label: 'Versao padrao', value: (runtime) => runtime.default_version || '-' },
1972
+ { label: 'Versoes', value: (runtime) => Array.isArray(runtime.versions) ? runtime.versions.map((version) => version.id).join(', ') : '-' },
1973
+ ]);
1974
+ }
1975
+
1976
+ async function handleGitConnections(session, flags) {
1977
+ const orgId = await resolveOrgId(session, flags);
1978
+ const payload = await request(session, flags, 'GET', '/git/connections', { orgId });
1979
+ const connections = unwrapData(payload);
1980
+ if (!Array.isArray(connections)) {
1981
+ throw new CliError('A API retornou uma resposta invalida para as conexoes Git.');
1982
+ }
1983
+ if (flags.json) return printJson(payload);
1984
+
1985
+ printTable(connections, [
1986
+ { label: 'ID', value: (connection) => connection.id || '-' },
1987
+ { label: 'Provedor', value: (connection) => connection.provider_id || '-' },
1988
+ { label: 'Nome', value: (connection) => connection.display_name || '-' },
1989
+ { label: 'Status', value: (connection) => connection.status || '-' },
1990
+ ]);
1991
+ }
1992
+
1993
+ async function handleGitRepositoryResolve(session, flags) {
1994
+ const connectionId = requiredGitOpaqueId(flags, 'connection', 'git repositories resolve');
1995
+ const repositoryPath = requiredGitRepositoryPath(flags, 'git repositories resolve');
1996
+ if (!connectionId || !repositoryPath) return;
1997
+
1998
+ const orgId = await resolveOrgId(session, flags);
1999
+ const endpoint = '/git/connections/' + encodeURIComponent(connectionId) + '/repositories/resolve';
2000
+ const payload = await request(session, flags, 'POST', endpoint, {
2001
+ orgId,
2002
+ body: { path: repositoryPath },
2003
+ });
2004
+ if (flags.json) return printJson(payload);
2005
+
2006
+ const repository = unwrapData(payload);
2007
+ printTable([repository], [
2008
+ { label: 'ID', value: (entry) => entry.id || '-' },
2009
+ { label: 'Caminho', value: (entry) => entry.path || '-' },
2010
+ { label: 'Branch padrao', value: (entry) => entry.default_branch || '-' },
2011
+ { label: 'Privado', value: (entry) => yesNo(entry.private) },
2012
+ ]);
2013
+ }
2014
+
2015
+ async function handleGitBranches(session, flags) {
2016
+ const connectionId = requiredGitOpaqueId(flags, 'connection', 'git branches');
2017
+ const repositoryId = requiredGitOpaqueId(flags, 'repository', 'git branches');
2018
+ if (!connectionId || !repositoryId) return;
2019
+
2020
+ const orgId = await resolveOrgId(session, flags);
2021
+ const endpoint = '/git/connections/' + encodeURIComponent(connectionId)
2022
+ + '/repositories/' + encodeURIComponent(repositoryId) + '/branches';
2023
+ const payload = await request(session, flags, 'GET', endpoint, { orgId });
2024
+ const branches = unwrapData(payload);
2025
+ if (!Array.isArray(branches)) {
2026
+ throw new CliError('A API retornou uma resposta invalida para as branches Git.');
2027
+ }
2028
+ if (flags.json) return printJson(payload);
2029
+
2030
+ printTable(branches, [
2031
+ { label: 'Branch', value: (branch) => branch.name || '-' },
2032
+ { label: 'Commit', value: (branch) => branch.commit_sha || '-' },
2033
+ ]);
2034
+ }
2035
+
2036
+ function printGitProjectSource(data) {
2037
+ if (!isRecord(data?.source)) {
2038
+ process.stdout.write('Este projeto nao possui uma origem Git generica configurada.\n');
2039
+ return;
2040
+ }
2041
+
2042
+ const source = data.source;
2043
+ const build = isRecord(data.build) ? data.build : {};
2044
+ const mode = source.provider_id === 'github' ? null : gitSourceDeploymentMode(source);
2045
+ const rows = [
2046
+ { field: 'Provedor', value: source.provider_id || '-' },
2047
+ { field: 'Repositorio', value: source.repository_path || source.repository_id || '-' },
2048
+ { field: 'Branch', value: source.branch || '-' },
2049
+ { field: 'Runtime', value: build.runtime ? build.runtime + '@' + (build.version || '-') : '-' },
2050
+ { field: 'Modo de deploy', value: mode || (source.provider_id === 'github' ? 'consulte project github' : 'indisponivel') },
2051
+ ];
2052
+ if (mode === 'tag' || mode === 'release') {
2053
+ rows.push(
2054
+ { field: 'Padrao de tags', value: source.version_deploy?.tag_pattern || '-' },
2055
+ { field: 'Incluir prereleases', value: source.version_deploy?.include_prereleases === true ? 'sim' : 'nao' },
2056
+ );
2057
+ }
2058
+ printTable(rows, [
2059
+ { label: 'Campo', value: (row) => row.field },
2060
+ { label: 'Valor', value: (row) => row.value },
2061
+ ]);
2062
+ }
2063
+
2064
+ async function handleProjectSource(session, flags) {
2065
+ const projectId = requireProjectId(flags, 'project source');
2066
+ if (!projectId) return;
2067
+ const orgId = await resolveOrgId(session, flags);
2068
+ const payload = await request(session, flags, 'GET', '/project/' + encodeURIComponent(projectId) + '/source', { orgId });
2069
+ const source = unwrapData(payload);
2070
+ if (flags.json) return printJson(payload);
2071
+ printGitProjectSource(source);
2072
+ }
2073
+
2074
+ async function handleProjectSourceBranches(session, flags) {
2075
+ const projectId = requireProjectId(flags, 'project source branches');
2076
+ if (!projectId) return;
2077
+ const orgId = await resolveOrgId(session, flags);
2078
+ const endpoint = '/project/' + encodeURIComponent(projectId) + '/source/branches';
2079
+ const payload = await request(session, flags, 'GET', endpoint, { orgId });
2080
+ const branches = unwrapData(payload);
2081
+ if (!Array.isArray(branches)) {
2082
+ throw new CliError('A API retornou uma resposta invalida para as branches da origem Git.');
2083
+ }
2084
+ if (flags.json) return printJson(payload);
2085
+ printTable(branches, [
2086
+ { label: 'Branch', value: (branch) => branch.name || '-' },
2087
+ { label: 'Commit', value: (branch) => branch.commit_sha || '-' },
2088
+ ]);
2089
+ }
2090
+
2091
+ function projectGitSourceInput(source) {
2092
+ if (!isRecord(source)
2093
+ || typeof source.connection_id !== 'string'
2094
+ || typeof source.repository_id !== 'string'
2095
+ || typeof source.branch !== 'string'
2096
+ || typeof source.auto_deploy !== 'boolean') {
2097
+ throw new CliError('A API retornou uma origem Git incompleta; nenhuma alteracao foi enviada.');
2098
+ }
2099
+
2100
+ const input = {
2101
+ connection_id: source.connection_id,
2102
+ repository_id: source.repository_id,
2103
+ branch: source.branch,
2104
+ auto_deploy: source.auto_deploy,
2105
+ };
2106
+ if (source.version_deploy === undefined || source.version_deploy === null) return input;
2107
+ if (!isRecord(source.version_deploy)) {
2108
+ throw new CliError('A API retornou configuracoes de deploy Git invalidas; nenhuma alteracao foi enviada.');
2109
+ }
2110
+ const versionDeploy = {};
2111
+ for (const field of GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS) {
2112
+ if (Object.hasOwn(source.version_deploy, field)) versionDeploy[field] = source.version_deploy[field];
2113
+ }
2114
+ input.version_deploy = versionDeploy;
2115
+ return input;
2116
+ }
2117
+
2118
+ function projectGitBuildSettingsInput(build) {
2119
+ if (!isRecord(build)) {
2120
+ throw new CliError('A API retornou configuracoes de build incompletas; nenhuma alteracao foi enviada.');
2121
+ }
2122
+ const input = {};
2123
+ for (const field of GIT_BUILD_SETTINGS_FIELDS) {
2124
+ if (Object.hasOwn(build, field)) input[field] = build[field];
2125
+ }
2126
+ return input;
2127
+ }
2128
+
2129
+ function gitSourceDeploymentMode(source) {
2130
+ const versionDeployEnabled = source?.version_deploy?.enabled === true;
2131
+ if (source?.auto_deploy === true && versionDeployEnabled) return null;
2132
+ if (source?.auto_deploy === true) return 'branch';
2133
+ if (!versionDeployEnabled) return 'manual';
2134
+ if (!ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(source.version_deploy.event)) return null;
2135
+ return source.version_deploy.event;
2136
+ }
2137
+
2138
+ function projectGitSourceMatches(data, expectedSource, expectedBuild, desired) {
2139
+ if (!isRecord(data) || !isRecord(data.source) || !isRecord(data.build)) return false;
2140
+ const source = data.source;
2141
+ if (source.provider_id !== 'forgejo'
2142
+ || source.connection_id !== expectedSource.connection_id
2143
+ || source.repository_id !== expectedSource.repository_id
2144
+ || source.branch !== expectedSource.branch
2145
+ || gitSourceDeploymentMode(source) !== desired.mode) return false;
2146
+
2147
+ if (desired.mode === 'tag' || desired.mode === 'release') {
2148
+ if (source.version_deploy?.tag_pattern !== desired.body.version_deploy.tag_pattern
2149
+ || (source.version_deploy?.include_prereleases === true) !== desired.includePrereleases) return false;
2150
+ }
2151
+
2152
+ return Object.entries(expectedBuild).every(([field, value]) => data.build[field] === value);
2153
+ }
2154
+
2155
+ async function handleProjectSourceDeploySettingsSet(session, flags) {
2156
+ const desired = parseGithubDeploySettings(flags);
2157
+ const projectId = requireProjectId(flags, 'project source deploy-settings set');
2158
+ if (!projectId) return;
2159
+ const orgId = await resolveOrgId(session, flags);
2160
+ const endpoint = '/project/' + encodeURIComponent(projectId) + '/source';
2161
+ let current;
2162
+ try {
2163
+ current = unwrapData(await request(session, flags, 'GET', endpoint, { orgId }));
2164
+ } catch (error) {
2165
+ if (error?.httpStatus === 404) {
2166
+ 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);
2167
+ }
2168
+ throw error;
2169
+ }
2170
+
2171
+ if (!isRecord(current?.source) || !isRecord(current?.build)) {
2172
+ throw new CliError('Este projeto nao tem uma origem Git generica configurada. Configure-a antes de alterar o modo de deploy.');
2173
+ }
2174
+ if (current.source.provider_id === 'github') {
2175
+ throw new CliError('Esta origem GitHub nao pode ser alterada por este comando. Consulte e altere com "zenifra project github deploy-settings set".');
2176
+ }
2177
+ if (current.source.provider_id !== 'forgejo') {
2178
+ throw new CliError('O provedor da origem nao e suportado por este comando; nenhuma alteracao foi enviada.');
2179
+ }
2180
+ if (desired.mode === 'branch' && current.capabilities?.pushDeploy !== true) {
2181
+ throw new CliError('A origem Git nao oferece deploy automatico por branch.');
2182
+ }
2183
+ if ((desired.mode === 'tag' || desired.mode === 'release') && current.capabilities?.versionDeploy !== true) {
2184
+ throw new CliError('A origem Git nao oferece deploy automatico por tag ou release.');
2185
+ }
2186
+
2187
+ const source = projectGitSourceInput(current.source);
2188
+ const build = projectGitBuildSettingsInput(current.build);
2189
+ source.auto_deploy = desired.body.auto_deploy;
2190
+ delete source.version_deploy;
2191
+ if (desired.mode === 'tag' || desired.mode === 'release') {
2192
+ source.version_deploy = desired.body.version_deploy;
2193
+ }
2194
+ const expectedSource = {
2195
+ connection_id: source.connection_id,
2196
+ repository_id: source.repository_id,
2197
+ branch: source.branch,
2198
+ };
2199
+
2200
+ await request(session, flags, 'PUT', endpoint, {
2201
+ orgId,
2202
+ body: { source, build },
2203
+ });
2204
+ const confirmed = unwrapData(await request(session, flags, 'GET', endpoint, { orgId }));
2205
+ if (!projectGitSourceMatches(confirmed, expectedSource, build, desired)) {
2206
+ throw new CliError('A configuracao solicitada nao foi confirmada pela API. Consulte "zenifra project source".');
2207
+ }
2208
+
2209
+ if (flags.json) return printJson({ status: 'success', data: confirmed });
2210
+ process.stdout.write('Configuracao de deploy da origem Git confirmada: ' + desired.mode
2211
+ + ((desired.mode === 'tag' || desired.mode === 'release') ? ' (' + desired.body.version_deploy.tag_pattern + ')' : '') + '.\n');
2212
+ }
2213
+
1719
2214
  function unwrapData(payload) {
1720
2215
  return payload?.data ?? payload;
1721
2216
  }
@@ -3067,6 +3562,17 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
3067
3562
  throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
3068
3563
  }
3069
3564
 
3565
+ const hasGenericGitConfig = config?.source !== undefined || config?.build !== undefined;
3566
+ if (hasGenericGitConfig && (!isRecord(config.source) || !isRecord(config.build))) {
3567
+ throw new CliError('config.source e config.build devem ser informados juntos para projetos com origem Git.');
3568
+ }
3569
+ if (hasGenericGitConfig && nextTypeProject !== 'http') {
3570
+ throw new CliError('config.source e config.build so podem ser usados em projetos HTTP.');
3571
+ }
3572
+ if (hasGenericGitConfig && config.github !== undefined) {
3573
+ throw new CliError('Use config.github ou config.source/config.build, sem misturar os formatos de origem Git.');
3574
+ }
3575
+
3070
3576
  const nextConfig = { ...config, type_project: nextTypeProject };
3071
3577
  if (nextTypeProject === 'valkey') {
3072
3578
  return {
@@ -3765,6 +4271,12 @@ async function handleProjectCreate(session, flags) {
3765
4271
  : undefined;
3766
4272
  const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
3767
4273
 
4274
+ if (validated.config.source !== undefined || validated.config.build !== undefined) {
4275
+ if (!await supportsNeutralGitRoutes(session, flags, orgId)) {
4276
+ 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.');
4277
+ }
4278
+ }
4279
+
3768
4280
  const payload = await request(session, flags, 'POST', '/project', {
3769
4281
  orgId,
3770
4282
  headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
@@ -4742,8 +5254,9 @@ async function handleDeployments(session, flags) {
4742
5254
  return
4743
5255
  }
4744
5256
  const orgId = await resolveOrgId(session, flags);
5257
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
4745
5258
  const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
4746
- const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds${query}`, { orgId }));
5259
+ const data = unwrapData(await request(session, flags, 'GET', `${projectApiPath}/builds${query}`, { orgId }));
4747
5260
  const builds = asArray(data);
4748
5261
 
4749
5262
  if (flags.json) return printJson(data);
@@ -4764,11 +5277,12 @@ async function handleDeploy(session, flags) {
4764
5277
  return;
4765
5278
  }
4766
5279
  const orgId = await resolveOrgId(session, flags);
5280
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
4767
5281
  const body = {};
4768
5282
  if (flags.branch) body.branch = String(flags.branch);
4769
5283
  if (flags.commitSha) body.commit_sha = String(flags.commitSha);
4770
5284
 
4771
- const payload = await request(session, flags, 'POST', `/project/${projectId}/github/deploy`, {
5285
+ const payload = await request(session, flags, 'POST', `${projectApiPath}/deploy`, {
4772
5286
  orgId,
4773
5287
  body,
4774
5288
  });
@@ -4778,9 +5292,9 @@ async function handleDeploy(session, flags) {
4778
5292
  process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
4779
5293
  }
4780
5294
 
4781
- async function getBuildLogs(session, flags, projectId, buildId, orgId, { cursor = 0, limit = 200 } = {}) {
5295
+ async function getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor = 0, limit = 200 } = {}) {
4782
5296
  const query = buildQuery({ cursor, limit }, ['cursor', 'limit'])
4783
- return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}/logs${query}`, { orgId }))
5297
+ return unwrapData(await request(session, flags, 'GET', `${projectApiPath}/builds/${buildId}/logs${query}`, { orgId }))
4784
5298
  }
4785
5299
 
4786
5300
  function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
@@ -4831,7 +5345,7 @@ function printBuildLogEventsJson(logs) {
4831
5345
  }
4832
5346
  }
4833
5347
 
4834
- async function followBuildLogs(session, flags, projectId, buildId, orgId, {
5348
+ async function followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4835
5349
  initialCursor = 0,
4836
5350
  limit = 200,
4837
5351
  intervalSeconds = 5,
@@ -4844,7 +5358,7 @@ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
4844
5358
  const startedAt = Date.now()
4845
5359
 
4846
5360
  while (true) {
4847
- const result = await getBuildLogs(session, flags, projectId, buildId, orgId, {
5361
+ const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4848
5362
  cursor,
4849
5363
  limit: pollLimit,
4850
5364
  })
@@ -4885,20 +5399,27 @@ async function handleBuildLogs(session, flags) {
4885
5399
  return
4886
5400
  }
4887
5401
 
4888
- const orgId = await resolveOrgId(session, flags);
4889
5402
  const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
4890
5403
  const limit = parseIntegerOption(flags.limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
5404
+ const intervalSeconds = flags.follow
5405
+ ? parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 })
5406
+ : undefined;
5407
+ const timeoutSeconds = flags.follow
5408
+ ? parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 })
5409
+ : undefined;
5410
+ const orgId = await resolveOrgId(session, flags);
5411
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
4891
5412
 
4892
5413
  if (flags.follow) {
4893
- return followBuildLogs(session, flags, projectId, buildId, orgId, {
5414
+ return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4894
5415
  initialCursor: cursor,
4895
5416
  limit,
4896
- intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
4897
- timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
5417
+ intervalSeconds,
5418
+ timeoutSeconds,
4898
5419
  })
4899
5420
  }
4900
5421
 
4901
- const result = await getBuildLogs(session, flags, projectId, buildId, orgId, { cursor, limit })
5422
+ const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor, limit })
4902
5423
  if (flags.json) return printJson(result)
4903
5424
  printBuildLogs(result?.logs)
4904
5425
  }
@@ -4911,12 +5432,15 @@ async function handleDeployWatch(session, flags) {
4911
5432
  return;
4912
5433
  }
4913
5434
 
5435
+ const intervalSeconds = parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 });
5436
+ const timeoutSeconds = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 });
4914
5437
  const orgId = await resolveOrgId(session, flags);
4915
- return followBuildLogs(session, flags, projectId, buildId, orgId, {
5438
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
5439
+ return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4916
5440
  initialCursor: 0,
4917
5441
  limit: 200,
4918
- intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
4919
- timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
5442
+ intervalSeconds,
5443
+ timeoutSeconds,
4920
5444
  })
4921
5445
  }
4922
5446
 
@@ -4962,6 +5486,15 @@ async function main() {
4962
5486
  if (command === 'login') return await handleLogin(session, flags);
4963
5487
  if (command === 'logout') return handleLogout(session, flags);
4964
5488
  if (command === 'plans') return handlePlans(session, flags);
5489
+ if (command === 'git' && subcommand === 'providers') return handleGitProviders(session, flags);
5490
+ if (command === 'git' && subcommand === 'runtimes') return handleGitRuntimes(session, flags);
5491
+ if (command === 'git' && subcommand === 'connections') return handleGitConnections(session, flags);
5492
+ if (command === 'git' && subcommand === 'repositories' && positional.length === 2) {
5493
+ process.stdout.write(commandHelp(['git', 'repositories']));
5494
+ return;
5495
+ }
5496
+ if (command === 'git' && subcommand === 'repositories' && positional[2] === 'resolve') return handleGitRepositoryResolve(session, flags);
5497
+ if (command === 'git' && subcommand === 'branches') return handleGitBranches(session, flags);
4965
5498
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
4966
5499
  if (command === 'orgs') return handleOrgs(session, flags);
4967
5500
  if (command === 'whoami') return handleWhoami(session, flags);
@@ -4989,6 +5522,15 @@ async function main() {
4989
5522
  return;
4990
5523
  }
4991
5524
  if (command === 'project' && subcommand === 'github' && positional.length === 2) return handleProjectGithub(session, flags);
5525
+ if (command === 'project' && subcommand === 'source' && positional.length === 2) return handleProjectSource(session, flags);
5526
+ if (command === 'project' && subcommand === 'source' && positional[2] === 'branches') return handleProjectSourceBranches(session, flags);
5527
+ if (command === 'project' && subcommand === 'source' && positional[2] === 'deploy-settings' && positional[3] === 'set') {
5528
+ return handleProjectSourceDeploySettingsSet(session, flags);
5529
+ }
5530
+ if (command === 'project' && subcommand === 'source' && positional[2] === 'deploy-settings') {
5531
+ process.stdout.write(commandHelp(['project', 'source', 'deploy-settings']));
5532
+ return;
5533
+ }
4992
5534
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4993
5535
  if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
4994
5536
  if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
@@ -0,0 +1,38 @@
1
+ {
2
+ "type_project": "http",
3
+ "exposure": "public",
4
+ "source": {
5
+ "connection_id": "connection-id-from-console",
6
+ "repository_id": "repository-id-from-connection",
7
+ "branch": "main",
8
+ "auto_deploy": true
9
+ },
10
+ "build": {
11
+ "runtime": "nodejs",
12
+ "version": "24",
13
+ "start_command": "npm start",
14
+ "pre_build_command": null,
15
+ "build_command": "npm run build"
16
+ },
17
+ "port": 3000,
18
+ "instances": 1,
19
+ "storage": {
20
+ "persistent": false,
21
+ "capacity": 1
22
+ },
23
+ "envs": [
24
+ {
25
+ "name": "NODE_ENV",
26
+ "value": "production"
27
+ }
28
+ ],
29
+ "network_access": {
30
+ "ingress_white_list": [
31
+ {
32
+ "cidr": "0.0.0.0/0",
33
+ "description": "Allow all"
34
+ }
35
+ ],
36
+ "ingress_black_list": []
37
+ }
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",