@zenifra/cli 0.3.4 → 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/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',
@@ -50,6 +51,7 @@ const KNOWN_FLAG_NAMES = new Set([
50
51
  'from',
51
52
  'help',
52
53
  'image',
54
+ 'includePrereleases',
53
55
  'idempotencyKey',
54
56
  'instance',
55
57
  'interval',
@@ -70,9 +72,11 @@ const KNOWN_FLAG_NAMES = new Set([
70
72
  'plan',
71
73
  'profile',
72
74
  'project',
75
+ 'repository',
73
76
  'revoke',
74
77
  'showValues',
75
78
  'status',
79
+ 'tagPattern',
76
80
  'timeout',
77
81
  'to',
78
82
  'totp',
@@ -114,6 +118,30 @@ const ALLOWED_PLAN_VALUES = new Set([
114
118
  const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
115
119
  const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse']);
116
120
  const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
121
+ const ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS = new Set(['tag', 'release']);
122
+ const ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS = new Set(['enabled', 'event', 'tag_pattern', 'include_prereleases']);
123
+ const GITHUB_TAG_PATTERN_BLOCKED_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\[\]{}\\]/u;
124
+ const GITHUB_PUBLIC_CONFIG_FIELDS = [
125
+ 'repository_owner',
126
+ 'repository_name',
127
+ 'branch',
128
+ 'runtime',
129
+ 'version',
130
+ 'auto_deploy',
131
+ 'start_command',
132
+ 'pre_build_command',
133
+ 'build_command',
134
+ ];
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
+ ];
117
145
  const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
118
146
  const GITHUB_RUNTIME_VERSIONS = {
119
147
  nodejs: ['24', '22', '20'],
@@ -160,9 +188,10 @@ const ANSI = {
160
188
  };
161
189
 
162
190
  class CliError extends Error {
163
- constructor(message, exitCode = 1) {
191
+ constructor(message, exitCode = 1, httpStatus) {
164
192
  super(message);
165
193
  this.exitCode = exitCode;
194
+ if (httpStatus !== undefined) this.httpStatus = httpStatus;
166
195
  }
167
196
  }
168
197
 
@@ -184,6 +213,11 @@ Usage:
184
213
  zenifra orgs [--json]
185
214
  zenifra org set [--org <id>]
186
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]
187
221
  zenifra create project
188
222
  zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
189
223
  zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
@@ -205,6 +239,11 @@ Usage:
205
239
  zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--json]
206
240
  zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
207
241
  zenifra project image set --project <id> --image <image> [--json]
242
+ zenifra project github --project <id> [--json]
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]
208
247
  zenifra project exposure set --project <id> --exposure <public|private> [--json]
209
248
  zenifra project envs --project <id> [--json] [--show-values]
210
249
  zenifra project env add --project <id> --name <name> --value <value> [--json]
@@ -381,6 +420,68 @@ const HELP_SPECS = [
381
420
  output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
382
421
  jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
383
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
+ },
384
485
  {
385
486
  command: 'valkey',
386
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]',
@@ -451,11 +552,95 @@ const HELP_SPECS = [
451
552
  },
452
553
  {
453
554
  command: 'project',
454
- 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]',
455
556
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
456
- 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'],
457
558
  output: 'Zenifra CLI - project',
458
- notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, 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
+ ],
610
+ },
611
+ {
612
+ command: 'project github',
613
+ usage: 'zenifra project github --project <id> [--json]',
614
+ description: 'Mostra a configuracao GitHub publica e o modo atual de deploy do projeto.',
615
+ flags: ['--project <id> ID do projeto.', '--json Imprime a configuracao publica em JSON.'],
616
+ examples: ['zenifra project github --project proj_1', 'zenifra project github --project proj_1 --json'],
617
+ output: 'Repositorio: example-team/sample-app\nBranch: main\nRuntime: nodejs@24\nModo de deploy: release\nPadrao de tags: v*\nIncluir prereleases: nao',
618
+ jsonOutput: '{"repository_owner":"example-team","repository_name":"sample-app","branch":"main","runtime":"nodejs","version":"24","auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":false}}',
619
+ notes: ['A resposta mostra apenas campos publicos da configuracao do projeto.'],
620
+ },
621
+ {
622
+ command: 'project github deploy-settings set',
623
+ usage: 'zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
624
+ description: 'Define um unico modo de deploy para o projeto com origem GitHub e confirma a configuracao salva.',
625
+ flags: [
626
+ '--project <id> ID do projeto.',
627
+ '--mode <manual|branch|tag|release> Modo de deploy exclusivo.',
628
+ '--tag-pattern <pattern> Padrao obrigatorio para tag e release. Aceita * e ?.',
629
+ '--include-prereleases <true|false> Inclui prereleases somente no modo release. Padrao: false.',
630
+ '--json Imprime a configuracao confirmada em JSON.',
631
+ ],
632
+ examples: [
633
+ 'zenifra project github deploy-settings set --project proj_1 --mode manual',
634
+ 'zenifra project github deploy-settings set --project proj_1 --mode branch',
635
+ 'zenifra project github deploy-settings set --project proj_1 --mode tag --tag-pattern "v*"',
636
+ 'zenifra project github deploy-settings set --project proj_1 --mode release --tag-pattern "v*" --include-prereleases true',
637
+ ],
638
+ output: 'Configuracao de deploy GitHub confirmada: release (v*).',
639
+ jsonOutput: '{"repository_owner":"example-team","repository_name":"sample-app","branch":"main","runtime":"nodejs","version":"24","auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":true}}',
640
+ notes: [
641
+ 'Tag e release exigem --tag-pattern explicito; prereleases so podem ser habilitadas no modo release.',
642
+ 'A configuracao salva e lida novamente antes de a CLI informar sucesso. O comando deploy manual continua disponivel.',
643
+ ],
459
644
  },
460
645
  {
461
646
  command: 'project info',
@@ -717,7 +902,7 @@ const HELP_SPECS = [
717
902
  {
718
903
  command: 'builds',
719
904
  usage: 'zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
720
- description: 'Lista builds GitHub de um projeto.',
905
+ description: 'Lista builds de um projeto com origem Git.',
721
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.'],
722
907
  examples: ['zenifra builds --project 507f1f77bcf86cd799439012 --status success'],
723
908
  output: 'Build Status Branch Commit\nbuild_123 success main abc123',
@@ -726,7 +911,7 @@ const HELP_SPECS = [
726
911
  {
727
912
  command: 'builds logs',
728
913
  usage: 'zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]',
729
- 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.',
730
915
  flags: [
731
916
  '--project <id> ID do projeto.',
732
917
  '--build <id> ID do build.',
@@ -744,7 +929,7 @@ const HELP_SPECS = [
744
929
  {
745
930
  command: 'deployments',
746
931
  usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
747
- description: 'Alias para listar builds/deployments GitHub de um projeto.',
932
+ description: 'Alias para listar builds e deployments Git de um projeto.',
748
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.'],
749
934
  examples: ['zenifra deployments --project 507f1f77bcf86cd799439012'],
750
935
  output: 'Build Status Branch Commit\nbuild_123 success main abc123',
@@ -753,7 +938,7 @@ const HELP_SPECS = [
753
938
  {
754
939
  command: 'deploy',
755
940
  usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
756
- 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.',
757
942
  flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
758
943
  examples: [
759
944
  'zenifra deploy --project 507f1f77bcf86cd799439012 --branch main',
@@ -847,7 +1032,7 @@ function removedProjectsCreateMessage() {
847
1032
  }
848
1033
 
849
1034
  function isNamespaceCommand(command) {
850
- return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
1035
+ return ['auth', 'profile', 'project', 'org', 'valkey', 'git'].includes(command);
851
1036
  }
852
1037
 
853
1038
  function parseArgs(argv) {
@@ -861,7 +1046,9 @@ function parseArgs(argv) {
861
1046
  continue;
862
1047
  }
863
1048
 
864
- const [rawKey, inlineValue] = arg.slice(2).split('=', 2);
1049
+ const argumentValueSeparator = arg.indexOf('=');
1050
+ const rawKey = argumentValueSeparator === -1 ? arg.slice(2) : arg.slice(2, argumentValueSeparator);
1051
+ const inlineValue = argumentValueSeparator === -1 ? undefined : arg.slice(argumentValueSeparator + 1);
865
1052
  const key = rawKey.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
866
1053
 
867
1054
  if (inlineValue !== undefined) {
@@ -1218,7 +1405,9 @@ function estimateWizardTotal(state = {}) {
1218
1405
  total += 1;
1219
1406
 
1220
1407
  if (state.httpSource === 'github') {
1221
- total += 9;
1408
+ total += state.httpGithubDeployMode === 'release'
1409
+ ? 11
1410
+ : state.httpGithubDeployMode === 'tag' ? 10 : 9;
1222
1411
  } else if (state.httpSource === 'oci') {
1223
1412
  total += 2;
1224
1413
  if (state.httpImagePublic === false) {
@@ -1635,30 +1824,393 @@ async function request(session, flags, method, path, {
1635
1824
 
1636
1825
  if (!response.ok) {
1637
1826
  const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
1827
+ const httpError = (errorMessage) => new CliError(errorMessage, 1, response.status);
1638
1828
  if (response.status === 401) {
1639
1829
  if (credential?.type === 'api_key') {
1640
- 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.`);
1641
1831
  }
1642
- throw new CliError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
1832
+ throw httpError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
1643
1833
  }
1644
1834
  if (response.status === 403) {
1645
- throw new CliError(message);
1835
+ throw httpError(message);
1646
1836
  }
1647
1837
  if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
1648
- 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).`);
1649
1839
  }
1650
1840
  if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
1651
- throw new CliError('Este projeto nao possui acesso a metricas.');
1841
+ throw httpError('Este projeto nao possui acesso a metricas.');
1652
1842
  }
1653
1843
  if (String(message).includes('missing x-organization-id')) {
1654
- 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>.');
1655
1845
  }
1656
- throw new CliError(message);
1846
+ throw httpError(message);
1657
1847
  }
1658
1848
 
1659
1849
  return payload;
1660
1850
  }
1661
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
+
1662
2214
  function unwrapData(payload) {
1663
2215
  return payload?.data ?? payload;
1664
2216
  }
@@ -2943,6 +3495,57 @@ function validateValkeyConfig(config, plan, catalog) {
2943
3495
  };
2944
3496
  }
2945
3497
 
3498
+ function isValidGithubTagPattern(value) {
3499
+ if (typeof value !== 'string') return false;
3500
+ const length = Array.from(value).length;
3501
+ return length >= 1 && length <= 255 && value.trim().length > 0
3502
+ && !GITHUB_TAG_PATTERN_BLOCKED_CHARACTERS.test(value);
3503
+ }
3504
+
3505
+ function validateGithubVersionDeployConfig(github) {
3506
+ if (!isRecord(github)) {
3507
+ throw new CliError('config.github deve ser um objeto.');
3508
+ }
3509
+ if (github.auto_deploy !== undefined && typeof github.auto_deploy !== 'boolean') {
3510
+ throw new CliError('config.github.auto_deploy deve ser true ou false.');
3511
+ }
3512
+
3513
+ const versionDeploy = github.version_deploy;
3514
+ if (versionDeploy === undefined) return;
3515
+ if (!isRecord(versionDeploy)) {
3516
+ throw new CliError('config.github.version_deploy deve ser um objeto.');
3517
+ }
3518
+
3519
+ const unsupportedField = Object.keys(versionDeploy).find((field) => !ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS.has(field));
3520
+ if (unsupportedField) {
3521
+ throw new CliError(`Campo invalido: config.github.version_deploy.${unsupportedField}.`);
3522
+ }
3523
+ if (versionDeploy.enabled !== undefined && typeof versionDeploy.enabled !== 'boolean') {
3524
+ throw new CliError('config.github.version_deploy.enabled deve ser true ou false.');
3525
+ }
3526
+ if (versionDeploy.event !== undefined && !ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(versionDeploy.event)) {
3527
+ throw new CliError('config.github.version_deploy.event deve ser tag ou release.');
3528
+ }
3529
+ if (versionDeploy.tag_pattern !== undefined && !isValidGithubTagPattern(versionDeploy.tag_pattern)) {
3530
+ throw new CliError('Tag pattern invalido: use de 1 a 255 caracteres e evite caracteres de controle, [ ] { } e barra invertida.');
3531
+ }
3532
+ if (versionDeploy.include_prereleases !== undefined && typeof versionDeploy.include_prereleases !== 'boolean') {
3533
+ throw new CliError('config.github.version_deploy.include_prereleases deve ser true ou false.');
3534
+ }
3535
+
3536
+ const enabled = versionDeploy.enabled === true;
3537
+ const event = versionDeploy.event || 'tag';
3538
+ if (enabled && versionDeploy.tag_pattern === undefined) {
3539
+ throw new CliError('config.github.version_deploy.tag_pattern e obrigatorio quando o version_deploy esta ativo.');
3540
+ }
3541
+ if (enabled && github.auto_deploy === true) {
3542
+ throw new CliError('github.auto_deploy e github.version_deploy nao podem ficar ativos ao mesmo tempo.');
3543
+ }
3544
+ if (versionDeploy.include_prereleases === true && event !== 'release') {
3545
+ throw new CliError('github.version_deploy.include_prereleases so pode ser true no modo release.');
3546
+ }
3547
+ }
3548
+
2946
3549
  function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2947
3550
  const nextTypeProject = normalizeTypeProject(config?.type_project);
2948
3551
  const nextPlan = normalizePlan(plan);
@@ -2959,6 +3562,17 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2959
3562
  throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
2960
3563
  }
2961
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
+
2962
3576
  const nextConfig = { ...config, type_project: nextTypeProject };
2963
3577
  if (nextTypeProject === 'valkey') {
2964
3578
  return {
@@ -2969,6 +3583,7 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2969
3583
  }
2970
3584
 
2971
3585
  if (nextTypeProject === 'http' && nextConfig.github) {
3586
+ validateGithubVersionDeployConfig(nextConfig.github);
2972
3587
  const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
2973
3588
  if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
2974
3589
  throw new CliError(`Runtime invalido: "${nextConfig.github.runtime || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_RUNTIME_VALUES)}. Docs: ${DOCS_RUNTIME_URL}`);
@@ -3080,11 +3695,45 @@ async function buildHttpGithubConfig() {
3080
3695
  options: GITHUB_RUNTIME_VERSIONS[runtime].map((value) => ({ value })),
3081
3696
  });
3082
3697
 
3083
- const auto_deploy = await promptWizardBoolean({
3084
- label: 'Auto deploy',
3698
+ const deployMode = await promptWizardSelect({
3699
+ label: 'Modo de deploy GitHub',
3085
3700
  docs: DOCS_RUNTIME_URL,
3086
- examples: ['sim', 'nao'],
3701
+ examples: ['manual', 'branch', 'tag', 'release'],
3702
+ options: [
3703
+ { value: 'manual', description: 'Deploy iniciado manualmente.' },
3704
+ { value: 'branch', description: 'Deploy quando a branch configurada for atualizada.' },
3705
+ { value: 'tag', description: 'Deploy quando uma tag corresponder ao padrao.' },
3706
+ { value: 'release', description: 'Deploy quando uma release corresponder ao padrao.' },
3707
+ ],
3087
3708
  });
3709
+ promptWizardSelect.ui.state.httpGithubDeployMode = deployMode;
3710
+
3711
+ let versionDeploy;
3712
+ if (deployMode === 'tag' || deployMode === 'release') {
3713
+ const tagPattern = await promptWizardText({
3714
+ label: 'Padrao de tags',
3715
+ required: true,
3716
+ examples: ['v*', 'release/?*'],
3717
+ docs: DOCS_RUNTIME_URL,
3718
+ validate: (value) => isValidGithubTagPattern(value)
3719
+ ? null
3720
+ : 'use de 1 a 255 caracteres; somente * e ? sao aceitos como curingas',
3721
+ });
3722
+ const includePrereleases = deployMode === 'release'
3723
+ ? await promptWizardBoolean({
3724
+ label: 'Incluir prereleases',
3725
+ docs: DOCS_RUNTIME_URL,
3726
+ examples: ['nao', 'sim'],
3727
+ })
3728
+ : false;
3729
+
3730
+ versionDeploy = {
3731
+ enabled: true,
3732
+ event: deployMode,
3733
+ tag_pattern: tagPattern,
3734
+ include_prereleases: includePrereleases,
3735
+ };
3736
+ }
3088
3737
 
3089
3738
  const start_command = await promptWizardText({
3090
3739
  label: 'Start command',
@@ -3114,7 +3763,8 @@ async function buildHttpGithubConfig() {
3114
3763
  branch,
3115
3764
  runtime,
3116
3765
  version,
3117
- auto_deploy,
3766
+ auto_deploy: deployMode === 'branch',
3767
+ ...(versionDeploy ? { version_deploy: versionDeploy } : {}),
3118
3768
  start_command,
3119
3769
  pre_build_command: preBuildCommand,
3120
3770
  build_command: buildCommandRaw,
@@ -3621,6 +4271,12 @@ async function handleProjectCreate(session, flags) {
3621
4271
  : undefined;
3622
4272
  const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
3623
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
+
3624
4280
  const payload = await request(session, flags, 'POST', '/project', {
3625
4281
  orgId,
3626
4282
  headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
@@ -3679,6 +4335,160 @@ async function handleProjectInfo(session, flags) {
3679
4335
  printProject({ ...project, id: projectId });
3680
4336
  }
3681
4337
 
4338
+ function githubConfigFromPayload(payload) {
4339
+ const github = isRecord(payload?.github)
4340
+ ? payload.github
4341
+ : isRecord(payload?.config?.github) ? payload.config.github : payload;
4342
+ if (!isRecord(github) || !github.repository_owner || !github.repository_name) {
4343
+ throw new CliError('O projeto nao possui uma configuracao GitHub disponivel.');
4344
+ }
4345
+ return github;
4346
+ }
4347
+
4348
+ function publicGithubConfiguration(github) {
4349
+ const output = Object.fromEntries(GITHUB_PUBLIC_CONFIG_FIELDS
4350
+ .filter((field) => github[field] !== undefined)
4351
+ .map((field) => [field, github[field]]));
4352
+ if (isRecord(github.version_deploy)) {
4353
+ output.version_deploy = Object.fromEntries(GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS
4354
+ .filter((field) => github.version_deploy[field] !== undefined)
4355
+ .map((field) => [field, github.version_deploy[field]]));
4356
+ }
4357
+ return output;
4358
+ }
4359
+
4360
+ function githubDeploymentMode(github) {
4361
+ const versionDeployEnabled = github.version_deploy?.enabled === true;
4362
+ if (github.auto_deploy === true && versionDeployEnabled) {
4363
+ throw new CliError('A configuracao GitHub do projeto possui modos de deploy conflitantes.');
4364
+ }
4365
+ if (github.auto_deploy === true) return 'branch';
4366
+ if (!versionDeployEnabled) return 'manual';
4367
+ if (!ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(github.version_deploy.event)) {
4368
+ throw new CliError('A configuracao GitHub do projeto possui um modo de deploy invalido.');
4369
+ }
4370
+ return github.version_deploy.event;
4371
+ }
4372
+
4373
+ function printGithubConfiguration(github) {
4374
+ const mode = githubDeploymentMode(github);
4375
+ const rows = [
4376
+ { field: 'Repositorio', value: `${github.repository_owner}/${github.repository_name}` },
4377
+ { field: 'Branch', value: github.branch || '-' },
4378
+ { field: 'Runtime', value: github.runtime ? `${github.runtime}@${github.version || '-'}` : '-' },
4379
+ { field: 'Modo de deploy', value: mode },
4380
+ ];
4381
+ if (mode === 'tag' || mode === 'release') {
4382
+ rows.push(
4383
+ { field: 'Padrao de tags', value: github.version_deploy.tag_pattern || '-' },
4384
+ { field: 'Incluir prereleases', value: github.version_deploy.include_prereleases === true ? 'sim' : 'nao' },
4385
+ );
4386
+ }
4387
+ printTable(rows, [
4388
+ { label: 'Campo', value: (row) => row.field },
4389
+ { label: 'Valor', value: (row) => row.value },
4390
+ ]);
4391
+ }
4392
+
4393
+ function parseGithubDeploySettings(flags) {
4394
+ const mode = String(flags.mode || '').toLowerCase();
4395
+ if (!['manual', 'branch', 'tag', 'release'].includes(mode)) {
4396
+ throw new CliError('Informe --mode manual, branch, tag ou release.');
4397
+ }
4398
+
4399
+ const usesVersionDeploy = mode === 'tag' || mode === 'release';
4400
+ const hasTagPattern = flags.tagPattern !== undefined;
4401
+ const hasPrereleaseOption = flags.includePrereleases !== undefined;
4402
+ if (hasTagPattern && typeof flags.tagPattern !== 'string') {
4403
+ throw new CliError('--tag-pattern requires a value.');
4404
+ }
4405
+ if (hasPrereleaseOption && typeof flags.includePrereleases !== 'string') {
4406
+ throw new CliError('--include-prereleases requires a value of true or false.');
4407
+ }
4408
+ if (usesVersionDeploy && !hasTagPattern) {
4409
+ throw new CliError('--tag-pattern e obrigatorio para os modos tag e release.');
4410
+ }
4411
+ if (!usesVersionDeploy && hasTagPattern) {
4412
+ throw new CliError('--tag-pattern so pode ser usado nos modos tag e release.');
4413
+ }
4414
+ if (hasPrereleaseOption && mode !== 'release') {
4415
+ throw new CliError('--include-prereleases so pode ser usado com --mode release.');
4416
+ }
4417
+
4418
+ let includePrereleases = false;
4419
+ if (hasPrereleaseOption) {
4420
+ const value = String(flags.includePrereleases).toLowerCase();
4421
+ if (value !== 'true' && value !== 'false') {
4422
+ throw new CliError('--include-prereleases deve ser true ou false.');
4423
+ }
4424
+ includePrereleases = value === 'true';
4425
+ }
4426
+
4427
+ const tagPattern = usesVersionDeploy ? String(flags.tagPattern) : undefined;
4428
+ if (usesVersionDeploy && !isValidGithubTagPattern(tagPattern)) {
4429
+ throw new CliError('Tag pattern invalido: use de 1 a 255 caracteres e evite caracteres de controle, [ ] { } e barra invertida.');
4430
+ }
4431
+
4432
+ return {
4433
+ mode,
4434
+ includePrereleases,
4435
+ body: {
4436
+ auto_deploy: mode === 'branch',
4437
+ version_deploy: usesVersionDeploy
4438
+ ? { enabled: true, event: mode, tag_pattern: tagPattern, include_prereleases: includePrereleases }
4439
+ : { enabled: false },
4440
+ },
4441
+ };
4442
+ }
4443
+
4444
+ function githubDeploySettingsMatch(github, desired) {
4445
+ let currentMode;
4446
+ try {
4447
+ currentMode = githubDeploymentMode(github);
4448
+ } catch {
4449
+ return false;
4450
+ }
4451
+ if (currentMode !== desired.mode) return false;
4452
+ if (desired.mode !== 'tag' && desired.mode !== 'release') return true;
4453
+
4454
+ const versionDeploy = github.version_deploy || {};
4455
+ return versionDeploy.enabled === true
4456
+ && versionDeploy.event === desired.mode
4457
+ && versionDeploy.tag_pattern === desired.body.version_deploy.tag_pattern
4458
+ && (versionDeploy.include_prereleases === true) === desired.includePrereleases;
4459
+ }
4460
+
4461
+ async function handleProjectGithub(session, flags) {
4462
+ const projectId = requireProjectId(flags, 'project github');
4463
+ if (!projectId) return;
4464
+ const orgId = await resolveOrgId(session, flags);
4465
+ const github = githubConfigFromPayload(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github`, { orgId })));
4466
+ const publicConfig = publicGithubConfiguration(github);
4467
+
4468
+ if (flags.json) return printJson(publicConfig);
4469
+ printGithubConfiguration(github);
4470
+ }
4471
+
4472
+ async function handleProjectGithubDeploySettingsSet(session, flags) {
4473
+ const projectId = requireProjectId(flags, 'project github deploy-settings set');
4474
+ if (!projectId) return;
4475
+ const desired = parseGithubDeploySettings(flags);
4476
+ const orgId = await resolveOrgId(session, flags);
4477
+
4478
+ await request(session, flags, 'PATCH', `/project/${projectId}/github/deploy-settings`, {
4479
+ orgId,
4480
+ body: desired.body,
4481
+ });
4482
+ const github = githubConfigFromPayload(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github`, { orgId })));
4483
+ if (!githubDeploySettingsMatch(github, desired)) {
4484
+ throw new CliError('A configuracao solicitada nao foi confirmada pela API. Consulte "zenifra project github".');
4485
+ }
4486
+
4487
+ const publicConfig = publicGithubConfiguration(github);
4488
+ if (flags.json) return printJson(publicConfig);
4489
+ process.stdout.write(`Configuracao de deploy GitHub confirmada: ${desired.mode}${desired.mode === 'tag' || desired.mode === 'release' ? ` (${desired.body.version_deploy.tag_pattern})` : ''}.\n`);
4490
+ }
4491
+
3682
4492
  async function handleProjectLifecycleMutation(session, flags, action) {
3683
4493
  const commandKey = `project ${action}`;
3684
4494
  const projectId = requireProjectId(flags, commandKey);
@@ -4444,8 +5254,9 @@ async function handleDeployments(session, flags) {
4444
5254
  return
4445
5255
  }
4446
5256
  const orgId = await resolveOrgId(session, flags);
5257
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
4447
5258
  const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
4448
- 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 }));
4449
5260
  const builds = asArray(data);
4450
5261
 
4451
5262
  if (flags.json) return printJson(data);
@@ -4466,11 +5277,12 @@ async function handleDeploy(session, flags) {
4466
5277
  return;
4467
5278
  }
4468
5279
  const orgId = await resolveOrgId(session, flags);
5280
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
4469
5281
  const body = {};
4470
5282
  if (flags.branch) body.branch = String(flags.branch);
4471
5283
  if (flags.commitSha) body.commit_sha = String(flags.commitSha);
4472
5284
 
4473
- const payload = await request(session, flags, 'POST', `/project/${projectId}/github/deploy`, {
5285
+ const payload = await request(session, flags, 'POST', `${projectApiPath}/deploy`, {
4474
5286
  orgId,
4475
5287
  body,
4476
5288
  });
@@ -4480,9 +5292,9 @@ async function handleDeploy(session, flags) {
4480
5292
  process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
4481
5293
  }
4482
5294
 
4483
- 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 } = {}) {
4484
5296
  const query = buildQuery({ cursor, limit }, ['cursor', 'limit'])
4485
- 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 }))
4486
5298
  }
4487
5299
 
4488
5300
  function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
@@ -4533,7 +5345,7 @@ function printBuildLogEventsJson(logs) {
4533
5345
  }
4534
5346
  }
4535
5347
 
4536
- async function followBuildLogs(session, flags, projectId, buildId, orgId, {
5348
+ async function followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4537
5349
  initialCursor = 0,
4538
5350
  limit = 200,
4539
5351
  intervalSeconds = 5,
@@ -4546,7 +5358,7 @@ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
4546
5358
  const startedAt = Date.now()
4547
5359
 
4548
5360
  while (true) {
4549
- const result = await getBuildLogs(session, flags, projectId, buildId, orgId, {
5361
+ const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4550
5362
  cursor,
4551
5363
  limit: pollLimit,
4552
5364
  })
@@ -4587,20 +5399,27 @@ async function handleBuildLogs(session, flags) {
4587
5399
  return
4588
5400
  }
4589
5401
 
4590
- const orgId = await resolveOrgId(session, flags);
4591
5402
  const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
4592
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);
4593
5412
 
4594
5413
  if (flags.follow) {
4595
- return followBuildLogs(session, flags, projectId, buildId, orgId, {
5414
+ return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4596
5415
  initialCursor: cursor,
4597
5416
  limit,
4598
- intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
4599
- timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
5417
+ intervalSeconds,
5418
+ timeoutSeconds,
4600
5419
  })
4601
5420
  }
4602
5421
 
4603
- const result = await getBuildLogs(session, flags, projectId, buildId, orgId, { cursor, limit })
5422
+ const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor, limit })
4604
5423
  if (flags.json) return printJson(result)
4605
5424
  printBuildLogs(result?.logs)
4606
5425
  }
@@ -4613,12 +5432,15 @@ async function handleDeployWatch(session, flags) {
4613
5432
  return;
4614
5433
  }
4615
5434
 
5435
+ const intervalSeconds = parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 });
5436
+ const timeoutSeconds = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 });
4616
5437
  const orgId = await resolveOrgId(session, flags);
4617
- return followBuildLogs(session, flags, projectId, buildId, orgId, {
5438
+ const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
5439
+ return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
4618
5440
  initialCursor: 0,
4619
5441
  limit: 200,
4620
- intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
4621
- timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
5442
+ intervalSeconds,
5443
+ timeoutSeconds,
4622
5444
  })
4623
5445
  }
4624
5446
 
@@ -4664,6 +5486,15 @@ async function main() {
4664
5486
  if (command === 'login') return await handleLogin(session, flags);
4665
5487
  if (command === 'logout') return handleLogout(session, flags);
4666
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);
4667
5498
  if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
4668
5499
  if (command === 'orgs') return handleOrgs(session, flags);
4669
5500
  if (command === 'whoami') return handleWhoami(session, flags);
@@ -4685,6 +5516,21 @@ async function main() {
4685
5516
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'set') return handleProjectHealthcheckSet(session, flags);
4686
5517
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
4687
5518
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'failures') return handleProjectHealthcheckFailures(session, flags);
5519
+ if (command === 'project' && subcommand === 'github' && positional[2] === 'deploy-settings' && positional[3] === 'set') return handleProjectGithubDeploySettingsSet(session, flags);
5520
+ if (command === 'project' && subcommand === 'github' && positional[2] === 'deploy-settings') {
5521
+ process.stdout.write(commandHelp(['project', 'github', 'deploy-settings', 'set']));
5522
+ return;
5523
+ }
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
+ }
4688
5534
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4689
5535
  if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
4690
5536
  if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');