@studiometa/forge-cli 0.4.5 → 0.4.7
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 +68 -6
- package/dist/cli.js +1406 -51
- package/dist/cli.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/daemons/command.d.ts.map +1 -1
- package/dist/commands/daemons/handlers.d.ts +1 -0
- package/dist/commands/daemons/handlers.d.ts.map +1 -1
- package/dist/commands/daemons/help.d.ts.map +1 -1
- package/dist/commands/deployments/command.d.ts.map +1 -1
- package/dist/commands/deployments/handlers.d.ts +3 -0
- package/dist/commands/deployments/handlers.d.ts.map +1 -1
- package/dist/commands/deployments/help.d.ts.map +1 -1
- package/dist/commands/env/env-diff.d.ts +29 -0
- package/dist/commands/env/env-diff.d.ts.map +1 -0
- package/dist/commands/env/handlers.d.ts.map +1 -1
- package/dist/commands/env/help.d.ts.map +1 -1
- package/dist/commands/nginx/handlers.d.ts.map +1 -1
- package/dist/commands/nginx/help.d.ts.map +1 -1
- package/dist/commands/php/command.d.ts +2 -0
- package/dist/commands/php/command.d.ts.map +1 -0
- package/dist/commands/php/handlers.d.ts +42 -0
- package/dist/commands/php/handlers.d.ts.map +1 -0
- package/dist/commands/php/help.d.ts +2 -0
- package/dist/commands/php/help.d.ts.map +1 -0
- package/dist/commands/php/index.d.ts +3 -0
- package/dist/commands/php/index.d.ts.map +1 -0
- package/dist/commands/services/command.d.ts +2 -0
- package/dist/commands/services/command.d.ts.map +1 -0
- package/dist/commands/services/handlers.d.ts +10 -0
- package/dist/commands/services/handlers.d.ts.map +1 -0
- package/dist/commands/services/help.d.ts +2 -0
- package/dist/commands/services/help.d.ts.map +1 -0
- package/dist/commands/services/index.d.ts +3 -0
- package/dist/commands/services/index.d.ts.map +1 -0
- package/dist/commands/ssh-keys/command.d.ts.map +1 -1
- package/dist/commands/ssh-keys/help.d.ts.map +1 -1
- package/dist/commands/ssh-keys/sync-plan.d.ts +56 -0
- package/dist/commands/ssh-keys/sync-plan.d.ts.map +1 -0
- package/dist/commands/ssh-keys/sync.d.ts +6 -0
- package/dist/commands/ssh-keys/sync.d.ts.map +1 -0
- package/dist/{error-handler-peS_8nl4.js → error-handler-Adi84RcF.js} +21 -5
- package/dist/error-handler-Adi84RcF.js.map +1 -0
- package/dist/error-handler.d.ts +5 -0
- package/dist/error-handler.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/output.d.ts +5 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/utils/args.d.ts +6 -0
- package/dist/utils/args.d.ts.map +1 -1
- package/dist/utils/content-source.d.ts +24 -0
- package/dist/utils/content-source.d.ts.map +1 -0
- package/dist/utils/drift.d.ts +33 -0
- package/dist/utils/drift.d.ts.map +1 -0
- package/dist/utils/resolve.d.ts +8 -0
- package/dist/utils/resolve.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/error-handler-peS_8nl4.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { activateCertificate, createAuditLogger, createBackupConfig, createCommand, createDatabaseUser, createMonitor, createNginxTemplate, createRedirectRule, createScheduledJob, createSecurityRule, deleteBackupConfig, deleteDatabaseUser, deleteMonitor, deleteNginxTemplate, deleteRedirectRule, deleteScheduledJob, deleteSecurityRule, deploySiteAndWait, getBackupConfig, getCertificate, getCommand, getDaemon, getDatabase, getDatabaseUser, getEnv, getFirewallRule, getMonitor, getNginxConfig, getNginxTemplate, getRecipe, getRedirectRule, getScheduledJob, getSecurityRule, getServer, getSite, getSshKey, getUser, listBackupConfigs, listCommands, listDaemons, listDatabaseUsers, listDatabases, listDeployments, listFirewallRules, listMonitors, listNginxTemplates, listRecipes, listRedirectRules, listScheduledJobs, listSecurityRules, listServers, listSites, listSshKeys, matchByName, rebootServer, restartDaemon, runRecipe, updateBackupConfig, updateDaemon, updateDatabaseUser, updateEnv, updateNginxConfig, updateNginxTemplate, updateRecipe, updateSecurityRule, updateSite } from "@studiometa/forge-core";
|
|
4
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { C as isOutputFormat, _ as setToken, a as runCommand, c as OutputFormatter, d as createConfigStore, f as deleteToken, g as setOrganizationSlug, h as getToken, l as colors, m as getOrganizationSlug, n as exitWithError, o as createContext, r as exitWithValidationError, t as ExitCode, u as setColorEnabled, v as ApiError, x as ValidationError } from "./error-handler-Adi84RcF.js";
|
|
3
|
+
import { PHP_CONFIG_TYPES, RESTARTABLE_SERVICES, activateCertificate, createAuditLogger, createBackupConfig, createCommand, createDatabaseUser, createMonitor, createNginxTemplate, createRedirectRule, createScheduledJob, createSecurityRule, createSshKey, deleteBackupConfig, deleteDatabaseUser, deleteMonitor, deleteNginxTemplate, deleteRedirectRule, deleteScheduledJob, deleteSecurityRule, deleteSshKey, deploySiteAndWait, disablePhpOpcache, enablePhpOpcache, getBackupConfig, getCertificate, getCommand, getDaemon, getDaemonLog, getDatabase, getDatabaseUser, getDeploymentLog, getDeploymentScript, getEnv, getFirewallRule, getMonitor, getNginxConfig, getNginxTemplate, getPhpConfig, getPhpMaxExecutionTime, getPhpMaxUploadSize, getPhpOpcache, getRecipe, getRedirectRule, getScheduledJob, getSecurityRule, getServer, getSite, getSshKey, getUser, listBackupConfigs, listCommands, listDaemons, listDatabaseUsers, listDatabases, listDeployments, listFirewallRules, listMonitors, listNginxTemplates, listPhpVersions, listRecipes, listRedirectRules, listScheduledJobs, listSecurityRules, listServers, listServices, listSites, listSshKeys, matchByName, rebootServer, restartDaemon, restartService, runRecipe, updateBackupConfig, updateDaemon, updateDatabaseUser, updateDeploymentScript, updateEnv, updateNginxConfig, updateNginxTemplate, updatePhpConfig, updatePhpMaxExecutionTime, updatePhpMaxUploadSize, updateRecipe, updateSecurityRule, updateSite } from "@studiometa/forge-core";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { diff } from "node:util";
|
|
7
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
8
10
|
//#region src/audit.ts
|
|
9
11
|
/**
|
|
10
12
|
* Shared audit logger for CLI write operations.
|
|
@@ -76,7 +78,14 @@ function createCommandRouter(config) {
|
|
|
76
78
|
*/
|
|
77
79
|
async function resolveServerId(value, execCtx) {
|
|
78
80
|
if (/^\d+$/.test(value)) return value;
|
|
79
|
-
|
|
81
|
+
return matchServerId(value, (await listServers({}, execCtx)).data);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a --server value to a numeric server ID string, against a list of servers
|
|
85
|
+
* that is already loaded. Same rules as {@link resolveServerId}, with no API call.
|
|
86
|
+
*/
|
|
87
|
+
function matchServerId(value, servers) {
|
|
88
|
+
if (/^\d+$/.test(value)) return value;
|
|
80
89
|
const { exact, partial } = matchByName(servers, value, (s) => s.name);
|
|
81
90
|
if (exact.length === 1) return String(exact[0].id);
|
|
82
91
|
if (partial.length === 0) {
|
|
@@ -336,24 +345,26 @@ _forge_completions() {
|
|
|
336
345
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
337
346
|
|
|
338
347
|
# Main commands
|
|
339
|
-
commands="config servers s sites deployments d databases db database-users daemons env nginx certificates certs firewall-rules fw ssh ssh-keys backups commands scheduled-jobs user monitors nginx-templates security-rules redirect-rules recipes completion help"
|
|
348
|
+
commands="config servers s sites deployments d databases db database-users daemons env nginx certificates certs firewall-rules fw ssh ssh-keys backups commands scheduled-jobs services php user monitors nginx-templates security-rules redirect-rules recipes completion help"
|
|
340
349
|
|
|
341
350
|
# Subcommands for each command
|
|
342
351
|
local config_cmds="set get delete"
|
|
343
352
|
local servers_cmds="list ls get reboot"
|
|
344
353
|
local sites_cmds="list ls get"
|
|
345
|
-
local deployments_cmds="list ls deploy"
|
|
354
|
+
local deployments_cmds="list ls deploy logs log script update-script"
|
|
346
355
|
local databases_cmds="list ls get"
|
|
347
356
|
local database_users_cmds="list ls get create delete"
|
|
348
|
-
local daemons_cmds="list ls get restart"
|
|
357
|
+
local daemons_cmds="list ls get restart logs log"
|
|
349
358
|
local env_cmds="get update"
|
|
350
359
|
local nginx_cmds="get update"
|
|
351
360
|
local certificates_cmds="get activate"
|
|
352
361
|
local firewall_rules_cmds="list ls get"
|
|
353
|
-
local ssh_keys_cmds="list ls get"
|
|
362
|
+
local ssh_keys_cmds="list ls get sync"
|
|
354
363
|
local backups_cmds="list ls get create delete"
|
|
355
364
|
local commands_cmds="list ls get create"
|
|
356
365
|
local scheduled_jobs_cmds="list ls get create delete"
|
|
366
|
+
local services_cmds="list ls restart"
|
|
367
|
+
local php_cmds="list ls config update-config max-upload-size set-max-upload-size max-execution-time set-max-execution-time opcache enable-opcache disable-opcache"
|
|
357
368
|
local user_cmds="get"
|
|
358
369
|
local monitors_cmds="list ls get create delete"
|
|
359
370
|
local nginx_templates_cmds="list ls get create update delete"
|
|
@@ -363,7 +374,7 @@ _forge_completions() {
|
|
|
363
374
|
local completion_cmds="bash zsh fish"
|
|
364
375
|
|
|
365
376
|
# Global options
|
|
366
|
-
options="--token --server --site -f --format --no-color -h --help -v --version --name --command --content --from --to --provider --frequency --type --operator --threshold --minutes --key --port --ip-address --user --private --dry-run --script --servers --domain --password --credential-id --region --size --project-type --directory"
|
|
377
|
+
options="--token --server --site -f --format --no-color -h --help -v --version --name --command --content --from --to --provider --frequency --type --operator --threshold --minutes --key --port --ip-address --user --private --dry-run --script --servers --domain --password --credential-id --region --size --project-type --directory --file --script-file --check --all-servers --prune"
|
|
367
378
|
|
|
368
379
|
# Format options
|
|
369
380
|
local formats="json human table"
|
|
@@ -422,6 +433,12 @@ _forge_completions() {
|
|
|
422
433
|
scheduled-jobs)
|
|
423
434
|
COMPREPLY=( $(compgen -W "\${scheduled_jobs_cmds}" -- "\${cur}") )
|
|
424
435
|
;;
|
|
436
|
+
services)
|
|
437
|
+
COMPREPLY=( $(compgen -W "\${services_cmds}" -- "\${cur}") )
|
|
438
|
+
;;
|
|
439
|
+
php)
|
|
440
|
+
COMPREPLY=( $(compgen -W "\${php_cmds}" -- "\${cur}") )
|
|
441
|
+
;;
|
|
425
442
|
user)
|
|
426
443
|
COMPREPLY=( $(compgen -W "\${user_cmds}" -- "\${cur}") )
|
|
427
444
|
;;
|
|
@@ -454,7 +471,11 @@ _forge_completions() {
|
|
|
454
471
|
--format|-f)
|
|
455
472
|
COMPREPLY=( $(compgen -W "\${formats}" -- "\${cur}") )
|
|
456
473
|
;;
|
|
457
|
-
--
|
|
474
|
+
--file|--script-file)
|
|
475
|
+
# Complete file paths
|
|
476
|
+
COMPREPLY=( $(compgen -f -- "\${cur}") )
|
|
477
|
+
;;
|
|
478
|
+
--token|--server|--site|--version|--name|--command|--content|--from|--to|--provider|--frequency|--type|--operator|--threshold|--minutes|--key|--port|--ip-address|--user|--script|--servers|--domain|--password|--credential-id|--region|--size|--project-type|--directory)
|
|
458
479
|
# No completion for values requiring arguments
|
|
459
480
|
COMPREPLY=()
|
|
460
481
|
;;
|
|
@@ -505,6 +526,8 @@ _forge() {
|
|
|
505
526
|
'backups:Manage backup configurations'
|
|
506
527
|
'commands:Manage site commands'
|
|
507
528
|
'scheduled-jobs:Manage scheduled jobs'
|
|
529
|
+
'services:List and restart server services'
|
|
530
|
+
'php:Manage PHP versions, configuration, limits and OPcache'
|
|
508
531
|
'user:Display authenticated user profile'
|
|
509
532
|
'monitors:Manage server monitors'
|
|
510
533
|
'nginx-templates:Manage Nginx templates'
|
|
@@ -552,6 +575,10 @@ _forge() {
|
|
|
552
575
|
'list:List deployments'
|
|
553
576
|
'ls:List deployments (alias)'
|
|
554
577
|
'deploy:Trigger a deployment'
|
|
578
|
+
'logs:Show deployment output'
|
|
579
|
+
'log:Show deployment output (alias)'
|
|
580
|
+
'script:Show the deployment script'
|
|
581
|
+
'update-script:Update the deployment script'
|
|
555
582
|
)
|
|
556
583
|
_describe 'deployments command' deployments_cmds
|
|
557
584
|
;;
|
|
@@ -582,6 +609,8 @@ _forge() {
|
|
|
582
609
|
'ls:List daemons (alias)'
|
|
583
610
|
'get:Get daemon details'
|
|
584
611
|
'restart:Restart a daemon'
|
|
612
|
+
'logs:Show daemon log output'
|
|
613
|
+
'log:Show daemon log output (alias)'
|
|
585
614
|
)
|
|
586
615
|
_describe 'daemons command' daemons_cmds
|
|
587
616
|
;;
|
|
@@ -624,6 +653,7 @@ _forge() {
|
|
|
624
653
|
'list:List SSH keys'
|
|
625
654
|
'ls:List SSH keys (alias)'
|
|
626
655
|
'get:Get SSH key details'
|
|
656
|
+
'sync:Make server SSH keys match a file'
|
|
627
657
|
)
|
|
628
658
|
_describe 'ssh-keys command' ssh_keys_cmds
|
|
629
659
|
;;
|
|
@@ -659,6 +689,32 @@ _forge() {
|
|
|
659
689
|
)
|
|
660
690
|
_describe 'scheduled-jobs command' scheduled_jobs_cmds
|
|
661
691
|
;;
|
|
692
|
+
services)
|
|
693
|
+
local -a services_cmds
|
|
694
|
+
services_cmds=(
|
|
695
|
+
'list:List services on a server'
|
|
696
|
+
'ls:List services on a server (alias)'
|
|
697
|
+
'restart:Restart a service'
|
|
698
|
+
)
|
|
699
|
+
_describe 'services command' services_cmds
|
|
700
|
+
;;
|
|
701
|
+
php)
|
|
702
|
+
local -a php_cmds
|
|
703
|
+
php_cmds=(
|
|
704
|
+
'list:List installed PHP versions'
|
|
705
|
+
'ls:List installed PHP versions (alias)'
|
|
706
|
+
'config:Show a PHP configuration file'
|
|
707
|
+
'update-config:Replace a PHP configuration file'
|
|
708
|
+
'max-upload-size:Show the max upload size'
|
|
709
|
+
'set-max-upload-size:Set the max upload size'
|
|
710
|
+
'max-execution-time:Show the max execution time'
|
|
711
|
+
'set-max-execution-time:Set the max execution time'
|
|
712
|
+
'opcache:Show the OPcache status'
|
|
713
|
+
'enable-opcache:Enable OPcache'
|
|
714
|
+
'disable-opcache:Disable OPcache'
|
|
715
|
+
)
|
|
716
|
+
_describe 'php command' php_cmds
|
|
717
|
+
;;
|
|
662
718
|
user)
|
|
663
719
|
local -a user_cmds
|
|
664
720
|
user_cmds=(
|
|
@@ -740,7 +796,8 @@ _forge() {
|
|
|
740
796
|
'(-f --format)'{-f,--format}'[Output format]:format:(json human table)' \\
|
|
741
797
|
'--no-color[Disable colored output]' \\
|
|
742
798
|
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
743
|
-
'
|
|
799
|
+
'-v[Show version]' \\
|
|
800
|
+
'--version[PHP version (php, services)]:version:' \\
|
|
744
801
|
'--name[Name]:name:' \\
|
|
745
802
|
'--command[Command]:command:' \\
|
|
746
803
|
'--content[Content]:content:' \\
|
|
@@ -764,7 +821,13 @@ _forge() {
|
|
|
764
821
|
'--region[Region]:region:' \\
|
|
765
822
|
'--size[Size]:size:' \\
|
|
766
823
|
'--project-type[Project type]:project type:' \\
|
|
767
|
-
'--directory[Directory]:directory:'
|
|
824
|
+
'--directory[Directory]:directory:' \\
|
|
825
|
+
'--file[File path (- for stdin)]:file:_files' \\
|
|
826
|
+
'--script-file[Deployment script file]:file:_files' \\
|
|
827
|
+
'--all-servers[Target every server]' \\
|
|
828
|
+
'--prune[Remove SSH keys that are not in the file]' \\
|
|
829
|
+
'--dry-run[Print the plan and change nothing]' \\
|
|
830
|
+
'--check[Compare with Forge, do not write]'
|
|
768
831
|
;;
|
|
769
832
|
esac
|
|
770
833
|
}
|
|
@@ -795,6 +858,8 @@ complete -c forge -f -n "__fish_use_subcommand" -a "ssh-keys" -d "Manage SSH key
|
|
|
795
858
|
complete -c forge -f -n "__fish_use_subcommand" -a "backups" -d "Manage backup configurations"
|
|
796
859
|
complete -c forge -f -n "__fish_use_subcommand" -a "commands" -d "Manage site commands"
|
|
797
860
|
complete -c forge -f -n "__fish_use_subcommand" -a "scheduled-jobs" -d "Manage scheduled jobs"
|
|
861
|
+
complete -c forge -f -n "__fish_use_subcommand" -a "services" -d "List and restart server services"
|
|
862
|
+
complete -c forge -f -n "__fish_use_subcommand" -a "php" -d "Manage PHP versions, configuration, limits and OPcache"
|
|
798
863
|
complete -c forge -f -n "__fish_use_subcommand" -a "user" -d "Display authenticated user profile"
|
|
799
864
|
complete -c forge -f -n "__fish_use_subcommand" -a "monitors" -d "Manage server monitors"
|
|
800
865
|
complete -c forge -f -n "__fish_use_subcommand" -a "nginx-templates" -d "Manage Nginx templates"
|
|
@@ -824,6 +889,10 @@ complete -c forge -f -n "__fish_seen_subcommand_from sites" -a "get" -d "Get sit
|
|
|
824
889
|
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "list" -d "List deployments"
|
|
825
890
|
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "ls" -d "List deployments (alias)"
|
|
826
891
|
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "deploy" -d "Trigger a deployment"
|
|
892
|
+
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "logs" -d "Show deployment output"
|
|
893
|
+
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "log" -d "Show deployment output (alias)"
|
|
894
|
+
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "script" -d "Show the deployment script"
|
|
895
|
+
complete -c forge -f -n "__fish_seen_subcommand_from deployments d" -a "update-script" -d "Update the deployment script"
|
|
827
896
|
|
|
828
897
|
# databases subcommands
|
|
829
898
|
complete -c forge -f -n "__fish_seen_subcommand_from databases db" -a "list" -d "List databases"
|
|
@@ -842,6 +911,8 @@ complete -c forge -f -n "__fish_seen_subcommand_from daemons" -a "list" -d "List
|
|
|
842
911
|
complete -c forge -f -n "__fish_seen_subcommand_from daemons" -a "ls" -d "List daemons (alias)"
|
|
843
912
|
complete -c forge -f -n "__fish_seen_subcommand_from daemons" -a "get" -d "Get daemon details"
|
|
844
913
|
complete -c forge -f -n "__fish_seen_subcommand_from daemons" -a "restart" -d "Restart a daemon"
|
|
914
|
+
complete -c forge -f -n "__fish_seen_subcommand_from daemons" -a "logs" -d "Show daemon log output"
|
|
915
|
+
complete -c forge -f -n "__fish_seen_subcommand_from daemons" -a "log" -d "Show daemon log output (alias)"
|
|
845
916
|
|
|
846
917
|
# env subcommands
|
|
847
918
|
complete -c forge -f -n "__fish_seen_subcommand_from env" -a "get" -d "Get env variables"
|
|
@@ -865,6 +936,7 @@ complete -c forge -f -n "__fish_seen_subcommand_from firewall-rules fw" -a "get"
|
|
|
865
936
|
complete -c forge -f -n "__fish_seen_subcommand_from ssh-keys" -a "list" -d "List SSH keys"
|
|
866
937
|
complete -c forge -f -n "__fish_seen_subcommand_from ssh-keys" -a "ls" -d "List SSH keys (alias)"
|
|
867
938
|
complete -c forge -f -n "__fish_seen_subcommand_from ssh-keys" -a "get" -d "Get SSH key details"
|
|
939
|
+
complete -c forge -f -n "__fish_seen_subcommand_from ssh-keys" -a "sync" -d "Make server SSH keys match a file"
|
|
868
940
|
|
|
869
941
|
# backups subcommands
|
|
870
942
|
complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "list" -d "List backup configurations"
|
|
@@ -886,6 +958,24 @@ complete -c forge -f -n "__fish_seen_subcommand_from scheduled-jobs" -a "get" -d
|
|
|
886
958
|
complete -c forge -f -n "__fish_seen_subcommand_from scheduled-jobs" -a "create" -d "Create a scheduled job"
|
|
887
959
|
complete -c forge -f -n "__fish_seen_subcommand_from scheduled-jobs" -a "delete" -d "Delete a scheduled job"
|
|
888
960
|
|
|
961
|
+
# services subcommands
|
|
962
|
+
complete -c forge -f -n "__fish_seen_subcommand_from services" -a "list" -d "List services on a server"
|
|
963
|
+
complete -c forge -f -n "__fish_seen_subcommand_from services" -a "ls" -d "List services on a server (alias)"
|
|
964
|
+
complete -c forge -f -n "__fish_seen_subcommand_from services" -a "restart" -d "Restart a service"
|
|
965
|
+
|
|
966
|
+
# php subcommands
|
|
967
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "list" -d "List installed PHP versions"
|
|
968
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "ls" -d "List installed PHP versions (alias)"
|
|
969
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "config" -d "Show a PHP configuration file"
|
|
970
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "update-config" -d "Replace a PHP configuration file"
|
|
971
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "max-upload-size" -d "Show the max upload size"
|
|
972
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "set-max-upload-size" -d "Set the max upload size"
|
|
973
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "max-execution-time" -d "Show the max execution time"
|
|
974
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "set-max-execution-time" -d "Set the max execution time"
|
|
975
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "opcache" -d "Show the OPcache status"
|
|
976
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "enable-opcache" -d "Enable OPcache"
|
|
977
|
+
complete -c forge -f -n "__fish_seen_subcommand_from php" -a "disable-opcache" -d "Disable OPcache"
|
|
978
|
+
|
|
889
979
|
# user subcommands
|
|
890
980
|
complete -c forge -f -n "__fish_seen_subcommand_from user" -a "get" -d "Get the authenticated user profile"
|
|
891
981
|
|
|
@@ -961,6 +1051,12 @@ complete -c forge -l region -d "Region" -r
|
|
|
961
1051
|
complete -c forge -l size -d "Size" -r
|
|
962
1052
|
complete -c forge -l project-type -d "Project type" -r
|
|
963
1053
|
complete -c forge -l directory -d "Directory" -r
|
|
1054
|
+
complete -c forge -l file -d "File path (- for stdin)" -r -F
|
|
1055
|
+
complete -c forge -l script-file -d "Deployment script file" -r -F
|
|
1056
|
+
complete -c forge -l all-servers -d "Target every server"
|
|
1057
|
+
complete -c forge -l prune -d "Remove SSH keys that are not in the file"
|
|
1058
|
+
complete -c forge -l dry-run -d "Print the plan and change nothing"
|
|
1059
|
+
complete -c forge -l check -d "Compare with Forge, do not write"
|
|
964
1060
|
`;
|
|
965
1061
|
/**
|
|
966
1062
|
* Get the appropriate completion file path for the shell
|
|
@@ -1432,6 +1528,22 @@ async function daemonsGet(args, ctx) {
|
|
|
1432
1528
|
]);
|
|
1433
1529
|
}, ctx.formatter);
|
|
1434
1530
|
}
|
|
1531
|
+
async function daemonsLogs(args, ctx) {
|
|
1532
|
+
const [id] = args;
|
|
1533
|
+
const server = String(ctx.options.server ?? "");
|
|
1534
|
+
if (!id) exitWithValidationError("daemon_id", "forge daemons logs <daemon_id> --server <server_id>", ctx.formatter);
|
|
1535
|
+
if (!server) exitWithValidationError("server_id", "forge daemons logs <daemon_id> --server <server_id>", ctx.formatter);
|
|
1536
|
+
await runCommand(async () => {
|
|
1537
|
+
const token = ctx.getToken();
|
|
1538
|
+
const execCtx = ctx.createExecutorContext(token);
|
|
1539
|
+
const result = await getDaemonLog({
|
|
1540
|
+
server_id: await resolveServerId(server, execCtx),
|
|
1541
|
+
id
|
|
1542
|
+
}, execCtx);
|
|
1543
|
+
if (result.data) ctx.formatter.outputText(result.data);
|
|
1544
|
+
else ctx.formatter.info("No log output available.");
|
|
1545
|
+
}, ctx.formatter);
|
|
1546
|
+
}
|
|
1435
1547
|
async function daemonsUpdate(args, ctx) {
|
|
1436
1548
|
const [id] = args;
|
|
1437
1549
|
const server = String(ctx.options.server ?? "");
|
|
@@ -1483,6 +1595,8 @@ var handleDaemonsCommand = createCommandRouter({
|
|
|
1483
1595
|
list: daemonsList,
|
|
1484
1596
|
ls: daemonsList,
|
|
1485
1597
|
get: [daemonsGet, "args"],
|
|
1598
|
+
logs: [daemonsLogs, "args"],
|
|
1599
|
+
log: [daemonsLogs, "args"],
|
|
1486
1600
|
restart: [daemonsRestart, "args"],
|
|
1487
1601
|
update: [daemonsUpdate, "args"]
|
|
1488
1602
|
},
|
|
@@ -1502,6 +1616,12 @@ ${colors.bold("forge daemons get")} - Get daemon details
|
|
|
1502
1616
|
|
|
1503
1617
|
${colors.bold("USAGE:")}
|
|
1504
1618
|
forge daemons get <daemon_id> --server <server_id>
|
|
1619
|
+
`);
|
|
1620
|
+
else if (subcommand === "logs" || subcommand === "log") console.log(`
|
|
1621
|
+
${colors.bold("forge daemons logs")} - Show the log output of a daemon
|
|
1622
|
+
|
|
1623
|
+
${colors.bold("USAGE:")}
|
|
1624
|
+
forge daemons logs <daemon_id> --server <server_id>
|
|
1505
1625
|
`);
|
|
1506
1626
|
else if (subcommand === "update") console.log(`
|
|
1507
1627
|
${colors.bold("forge daemons update")} - Update a daemon
|
|
@@ -1524,6 +1644,7 @@ ${colors.bold("USAGE:")}
|
|
|
1524
1644
|
${colors.bold("SUBCOMMANDS:")}
|
|
1525
1645
|
list, ls List daemons
|
|
1526
1646
|
get <id> Get daemon details
|
|
1647
|
+
logs <id> Show the log output of a daemon
|
|
1527
1648
|
update <id> Update a daemon
|
|
1528
1649
|
restart <id> Restart a daemon
|
|
1529
1650
|
|
|
@@ -1810,6 +1931,133 @@ Run ${colors.cyan("forge database-users <subcommand> --help")} for details.
|
|
|
1810
1931
|
`);
|
|
1811
1932
|
}
|
|
1812
1933
|
//#endregion
|
|
1934
|
+
//#region src/utils/content-source.ts
|
|
1935
|
+
/**
|
|
1936
|
+
* Read the content of an update command from a file, stdin or an inline option.
|
|
1937
|
+
*/
|
|
1938
|
+
/**
|
|
1939
|
+
* Exit when the content is empty, so Forge never gets a blank file.
|
|
1940
|
+
*/
|
|
1941
|
+
function assertNotEmpty(content, field, source) {
|
|
1942
|
+
if (!content.trim()) exitWithError(new ValidationError(`${source.label.charAt(0).toUpperCase() + source.label.slice(1)} is empty`, field), source.formatter);
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Read content from a file path, or from stdin when the path is "-".
|
|
1946
|
+
* Exits with a validation error when the path is missing, the file cannot be read or it is empty.
|
|
1947
|
+
*/
|
|
1948
|
+
function readContentFile(path, field, source) {
|
|
1949
|
+
if (typeof path !== "string" || !path) exitWithError(new ValidationError(`--${field} requires a path (use - for stdin)`, field, [`Usage: ${source.usage}`]), source.formatter);
|
|
1950
|
+
let content;
|
|
1951
|
+
try {
|
|
1952
|
+
content = readFileSync(path === "-" ? 0 : path, "utf8");
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1955
|
+
exitWithError(new ValidationError(`Cannot read ${source.label} from ${path}: ${reason}`, field), source.formatter);
|
|
1956
|
+
}
|
|
1957
|
+
assertNotEmpty(content, field, source);
|
|
1958
|
+
return content;
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Read content from exactly one of `--file <path|->` or `--content <string>`.
|
|
1962
|
+
* Exits with a validation error when both or none are given, or when the content is empty.
|
|
1963
|
+
*/
|
|
1964
|
+
function readContentOption(options, source) {
|
|
1965
|
+
const { file, content } = options;
|
|
1966
|
+
if (file !== void 0 && content !== void 0) exitWithError(new ValidationError("Use either --file or --content, not both", "file", [`Usage: ${source.usage}`]), source.formatter);
|
|
1967
|
+
if (file !== void 0) return readContentFile(file, "file", source);
|
|
1968
|
+
if (content === void 0) exitWithError(new ValidationError(`Missing ${source.label}: use --file or --content`, "file", [`Usage: ${source.usage}`]), source.formatter);
|
|
1969
|
+
const inline = typeof content === "string" ? content : "";
|
|
1970
|
+
assertNotEmpty(inline, "content", source);
|
|
1971
|
+
return inline;
|
|
1972
|
+
}
|
|
1973
|
+
//#endregion
|
|
1974
|
+
//#region src/utils/drift.ts
|
|
1975
|
+
/**
|
|
1976
|
+
* Compare local content with the content on Forge for `--check`.
|
|
1977
|
+
*/
|
|
1978
|
+
/**
|
|
1979
|
+
* Normalize CRLF to LF and drop a single trailing newline,
|
|
1980
|
+
* so editor and platform differences do not count as drift.
|
|
1981
|
+
*/
|
|
1982
|
+
function normalizeContent(text) {
|
|
1983
|
+
const lf = text.replaceAll("\r\n", "\n");
|
|
1984
|
+
return lf.endsWith("\n") ? lf.slice(0, -1) : lf;
|
|
1985
|
+
}
|
|
1986
|
+
function toLines(normalized) {
|
|
1987
|
+
return normalized === "" ? [] : normalized.split("\n");
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Build a unified-style line diff from `remote` to `local`, with `context`
|
|
1991
|
+
* unchanged lines around each change. Returns an empty string when both
|
|
1992
|
+
* are equal after normalization.
|
|
1993
|
+
*/
|
|
1994
|
+
function unifiedDiff(remote, local, context = 3) {
|
|
1995
|
+
const remoteText = normalizeContent(remote);
|
|
1996
|
+
const localText = normalizeContent(local);
|
|
1997
|
+
if (remoteText === localText) return "";
|
|
1998
|
+
const entries = diff(toLines(remoteText), toLines(localText));
|
|
1999
|
+
let remoteCount = 0;
|
|
2000
|
+
let localCount = 0;
|
|
2001
|
+
const lines = entries.map(([op, text]) => {
|
|
2002
|
+
const line = {
|
|
2003
|
+
op: op === 0 ? " " : op === 1 ? "-" : "+",
|
|
2004
|
+
text,
|
|
2005
|
+
remoteBefore: remoteCount,
|
|
2006
|
+
localBefore: localCount
|
|
2007
|
+
};
|
|
2008
|
+
if (op !== -1) remoteCount++;
|
|
2009
|
+
if (op !== 1) localCount++;
|
|
2010
|
+
return line;
|
|
2011
|
+
});
|
|
2012
|
+
const changes = lines.flatMap((line, index) => line.op === " " ? [] : [index]);
|
|
2013
|
+
const groups = [];
|
|
2014
|
+
for (const index of changes) {
|
|
2015
|
+
const group = groups.at(-1);
|
|
2016
|
+
if (group && index - group[group.length - 1] - 1 <= 2 * context) group.push(index);
|
|
2017
|
+
else groups.push([index]);
|
|
2018
|
+
}
|
|
2019
|
+
const output = ["--- remote", "+++ local"];
|
|
2020
|
+
for (const group of groups) {
|
|
2021
|
+
const start = Math.max(0, group[0] - context);
|
|
2022
|
+
const end = Math.min(lines.length, group[group.length - 1] + context + 1);
|
|
2023
|
+
const hunk = lines.slice(start, end);
|
|
2024
|
+
const remoteLength = hunk.filter((line) => line.op !== "+").length;
|
|
2025
|
+
const localLength = hunk.filter((line) => line.op !== "-").length;
|
|
2026
|
+
const remoteStart = hunk[0].remoteBefore + (remoteLength > 0 ? 1 : 0);
|
|
2027
|
+
const localStart = hunk[0].localBefore + (localLength > 0 ? 1 : 0);
|
|
2028
|
+
output.push(`@@ -${remoteStart},${remoteLength} +${localStart},${localLength} @@`);
|
|
2029
|
+
for (const line of hunk) output.push(`${line.op}${line.text}`);
|
|
2030
|
+
}
|
|
2031
|
+
return output.join("\n");
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Print the result of a `--check` and set exit code 1 when the local content differs from Forge.
|
|
2035
|
+
* It sets `process.exitCode` and does not call `process.exit`, so a long diff
|
|
2036
|
+
* written to a pipe is not cut. JSON output is `{ inSync: true }` or `{ inSync: false, diff }`.
|
|
2037
|
+
*/
|
|
2038
|
+
function reportDrift(label, drift, formatter) {
|
|
2039
|
+
const name = label.charAt(0).toUpperCase() + label.slice(1);
|
|
2040
|
+
if (formatter.isJson()) formatter.output(drift ? {
|
|
2041
|
+
inSync: false,
|
|
2042
|
+
diff: drift.json
|
|
2043
|
+
} : { inSync: true });
|
|
2044
|
+
else if (drift) {
|
|
2045
|
+
console.log(drift.text);
|
|
2046
|
+
formatter.error(`${name} differs from Forge.`);
|
|
2047
|
+
} else formatter.success(`${name} is in sync with Forge.`);
|
|
2048
|
+
if (drift) process.exitCode = ExitCode.GENERAL_ERROR;
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Check `local` against `remote` as text and report a line diff.
|
|
2052
|
+
*/
|
|
2053
|
+
function reportTextDrift(label, remote, local, formatter) {
|
|
2054
|
+
const text = unifiedDiff(remote, local);
|
|
2055
|
+
reportDrift(label, text ? {
|
|
2056
|
+
text,
|
|
2057
|
+
json: text
|
|
2058
|
+
} : null, formatter);
|
|
2059
|
+
}
|
|
2060
|
+
//#endregion
|
|
1813
2061
|
//#region src/commands/deployments/handlers.ts
|
|
1814
2062
|
async function deploymentsList(ctx) {
|
|
1815
2063
|
const server = String(ctx.options.server ?? "");
|
|
@@ -1832,17 +2080,113 @@ async function deploymentsList(ctx) {
|
|
|
1832
2080
|
], "No deployments found.", (d) => `${String(d.id).padEnd(8)} ${d.status.padEnd(12)} ${(d.commit.hash ?? "—").padEnd(10)} ${d.started_at}`);
|
|
1833
2081
|
}, ctx.formatter);
|
|
1834
2082
|
}
|
|
2083
|
+
async function deploymentsLogs(args, ctx) {
|
|
2084
|
+
const [deploymentId] = args;
|
|
2085
|
+
const server = String(ctx.options.server ?? "");
|
|
2086
|
+
const site = String(ctx.options.site ?? "");
|
|
2087
|
+
if (!server) exitWithValidationError("server_id", "forge deployments logs [deployment_id] --server <server_id> --site <site_id>", ctx.formatter);
|
|
2088
|
+
if (!site) exitWithValidationError("site_id", "forge deployments logs [deployment_id] --server <server_id> --site <site_id>", ctx.formatter);
|
|
2089
|
+
await runCommand(async () => {
|
|
2090
|
+
const token = ctx.getToken();
|
|
2091
|
+
const execCtx = ctx.createExecutorContext(token);
|
|
2092
|
+
const server_id = await resolveServerId(server, execCtx);
|
|
2093
|
+
const site_id = await resolveSiteId(site, server_id, execCtx);
|
|
2094
|
+
let deployment_id = deploymentId;
|
|
2095
|
+
if (!deployment_id) {
|
|
2096
|
+
const deployments = await listDeployments({
|
|
2097
|
+
server_id,
|
|
2098
|
+
site_id
|
|
2099
|
+
}, execCtx);
|
|
2100
|
+
if (deployments.data.length === 0) {
|
|
2101
|
+
ctx.formatter.info("No deployments found.");
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
deployment_id = String(deployments.data[0].id);
|
|
2105
|
+
}
|
|
2106
|
+
const result = await getDeploymentLog({
|
|
2107
|
+
server_id,
|
|
2108
|
+
site_id,
|
|
2109
|
+
deployment_id
|
|
2110
|
+
}, execCtx);
|
|
2111
|
+
if (result.data) ctx.formatter.outputText(result.data);
|
|
2112
|
+
else ctx.formatter.info("No log output available.");
|
|
2113
|
+
}, ctx.formatter);
|
|
2114
|
+
}
|
|
2115
|
+
async function deploymentsScript(ctx) {
|
|
2116
|
+
const server = String(ctx.options.server ?? "");
|
|
2117
|
+
const site = String(ctx.options.site ?? "");
|
|
2118
|
+
if (!server) exitWithValidationError("server_id", "forge deployments script --server <server_id> --site <site_id>", ctx.formatter);
|
|
2119
|
+
if (!site) exitWithValidationError("site_id", "forge deployments script --server <server_id> --site <site_id>", ctx.formatter);
|
|
2120
|
+
await runCommand(async () => {
|
|
2121
|
+
const token = ctx.getToken();
|
|
2122
|
+
const execCtx = ctx.createExecutorContext(token);
|
|
2123
|
+
const server_id = await resolveServerId(server, execCtx);
|
|
2124
|
+
const result = await getDeploymentScript({
|
|
2125
|
+
server_id,
|
|
2126
|
+
site_id: await resolveSiteId(site, server_id, execCtx)
|
|
2127
|
+
}, execCtx);
|
|
2128
|
+
if (ctx.formatter.isJson()) console.log(JSON.stringify({ content: result.data }));
|
|
2129
|
+
else process.stdout.write(result.data.endsWith("\n") ? result.data : `${result.data}\n`);
|
|
2130
|
+
}, ctx.formatter);
|
|
2131
|
+
}
|
|
2132
|
+
async function deploymentsUpdateScript(ctx) {
|
|
2133
|
+
const usage = "forge deployments update-script --server <server_id> --site <site_id> (--file <path|-> | --content <script>) [--check]";
|
|
2134
|
+
const server = String(ctx.options.server ?? "");
|
|
2135
|
+
const site = String(ctx.options.site ?? "");
|
|
2136
|
+
const check = Boolean(ctx.options.check);
|
|
2137
|
+
if (!server) exitWithValidationError("server_id", usage, ctx.formatter);
|
|
2138
|
+
if (!site) exitWithValidationError("site_id", usage, ctx.formatter);
|
|
2139
|
+
const script = readContentOption(ctx.options, {
|
|
2140
|
+
label: "deployment script",
|
|
2141
|
+
usage,
|
|
2142
|
+
formatter: ctx.formatter
|
|
2143
|
+
});
|
|
2144
|
+
await runCommand(async () => {
|
|
2145
|
+
const token = ctx.getToken();
|
|
2146
|
+
const execCtx = ctx.createExecutorContext(token);
|
|
2147
|
+
const server_id = await resolveServerId(server, execCtx);
|
|
2148
|
+
const site_id = await resolveSiteId(site, server_id, execCtx);
|
|
2149
|
+
if (check) {
|
|
2150
|
+
reportTextDrift("deployment script", (await getDeploymentScript({
|
|
2151
|
+
server_id,
|
|
2152
|
+
site_id
|
|
2153
|
+
}, execCtx)).data, script, ctx.formatter);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
await updateDeploymentScript({
|
|
2157
|
+
server_id,
|
|
2158
|
+
site_id,
|
|
2159
|
+
content: script
|
|
2160
|
+
}, execCtx);
|
|
2161
|
+
ctx.formatter.success("Deployment script updated.");
|
|
2162
|
+
}, ctx.formatter);
|
|
2163
|
+
}
|
|
1835
2164
|
async function deploymentsDeploy(ctx) {
|
|
2165
|
+
const usage = "forge deployments deploy --server <server_id> --site <site_id> [--script-file <path>]";
|
|
1836
2166
|
const server = String(ctx.options.server ?? "");
|
|
1837
2167
|
const site = String(ctx.options.site ?? "");
|
|
1838
2168
|
const streamLogs = Boolean(ctx.options.stream);
|
|
1839
|
-
|
|
1840
|
-
if (!
|
|
2169
|
+
const scriptFile = ctx.options["script-file"];
|
|
2170
|
+
if (!server) exitWithValidationError("server_id", usage, ctx.formatter);
|
|
2171
|
+
if (!site) exitWithValidationError("site_id", usage, ctx.formatter);
|
|
2172
|
+
const script = scriptFile === void 0 ? void 0 : readContentFile(scriptFile, "script-file", {
|
|
2173
|
+
label: "deployment script",
|
|
2174
|
+
usage,
|
|
2175
|
+
formatter: ctx.formatter
|
|
2176
|
+
});
|
|
1841
2177
|
await runCommand(async () => {
|
|
1842
2178
|
const token = ctx.getToken();
|
|
1843
2179
|
const execCtx = ctx.createExecutorContext(token);
|
|
1844
2180
|
const server_id = await resolveServerId(server, execCtx);
|
|
1845
2181
|
const site_id = await resolveSiteId(site, server_id, execCtx);
|
|
2182
|
+
if (script !== void 0) {
|
|
2183
|
+
await updateDeploymentScript({
|
|
2184
|
+
server_id,
|
|
2185
|
+
site_id,
|
|
2186
|
+
content: script
|
|
2187
|
+
}, execCtx);
|
|
2188
|
+
process.stderr.write("Deployment script updated.\n");
|
|
2189
|
+
}
|
|
1846
2190
|
const result = await deploySiteAndWait({
|
|
1847
2191
|
server_id,
|
|
1848
2192
|
site_id,
|
|
@@ -1868,9 +2212,13 @@ var handleDeploymentsCommand = createCommandRouter({
|
|
|
1868
2212
|
handlers: {
|
|
1869
2213
|
list: deploymentsList,
|
|
1870
2214
|
ls: deploymentsList,
|
|
1871
|
-
deploy: deploymentsDeploy
|
|
2215
|
+
deploy: deploymentsDeploy,
|
|
2216
|
+
logs: [deploymentsLogs, "args"],
|
|
2217
|
+
log: [deploymentsLogs, "args"],
|
|
2218
|
+
script: deploymentsScript,
|
|
2219
|
+
"update-script": deploymentsUpdateScript
|
|
1872
2220
|
},
|
|
1873
|
-
writeSubcommands: ["deploy"]
|
|
2221
|
+
writeSubcommands: ["deploy", "update-script"]
|
|
1874
2222
|
});
|
|
1875
2223
|
//#endregion
|
|
1876
2224
|
//#region src/commands/deployments/help.ts
|
|
@@ -1886,17 +2234,68 @@ ${colors.bold("OPTIONS:")}
|
|
|
1886
2234
|
--site <id> Site ID (required)
|
|
1887
2235
|
-f, --format <fmt> Output format: json, human, table
|
|
1888
2236
|
`);
|
|
1889
|
-
else if (subcommand === "
|
|
1890
|
-
${colors.bold("forge deployments
|
|
2237
|
+
else if (subcommand === "logs" || subcommand === "log") console.log(`
|
|
2238
|
+
${colors.bold("forge deployments logs")} - Show output of a deployment (latest if no id)
|
|
1891
2239
|
|
|
1892
2240
|
${colors.bold("USAGE:")}
|
|
1893
|
-
forge deployments
|
|
2241
|
+
forge deployments logs [deployment_id] --server <server_id> --site <site_id>
|
|
1894
2242
|
|
|
1895
2243
|
${colors.bold("OPTIONS:")}
|
|
1896
2244
|
--server <id> Server ID (required)
|
|
1897
2245
|
--site <id> Site ID (required)
|
|
1898
|
-
--stream Stream deployment logs in real-time (default: show progress)
|
|
1899
2246
|
-f, --format <fmt> Output format: json, human, table
|
|
2247
|
+
`);
|
|
2248
|
+
else if (subcommand === "script") console.log(`
|
|
2249
|
+
${colors.bold("forge deployments script")} - Show the deployment script of a site
|
|
2250
|
+
|
|
2251
|
+
${colors.bold("USAGE:")}
|
|
2252
|
+
forge deployments script --server <server_id> --site <site_id>
|
|
2253
|
+
|
|
2254
|
+
${colors.bold("OPTIONS:")}
|
|
2255
|
+
--server <id> Server ID (required)
|
|
2256
|
+
--site <id> Site ID (required)
|
|
2257
|
+
-f, --format <fmt> Output format: json ({ content }), human (raw script)
|
|
2258
|
+
|
|
2259
|
+
${colors.bold("EXAMPLES:")}
|
|
2260
|
+
forge deployments script --server 123 --site 456 > deploy.sh
|
|
2261
|
+
`);
|
|
2262
|
+
else if (subcommand === "update-script") console.log(`
|
|
2263
|
+
${colors.bold("forge deployments update-script")} - Update the deployment script of a site
|
|
2264
|
+
|
|
2265
|
+
${colors.bold("USAGE:")}
|
|
2266
|
+
forge deployments update-script --server <server_id> --site <site_id> --file <path>
|
|
2267
|
+
forge deployments update-script --server <server_id> --site <site_id> --content <script>
|
|
2268
|
+
forge deployments update-script --server <server_id> --site <site_id> --file <path> --check
|
|
2269
|
+
|
|
2270
|
+
${colors.bold("OPTIONS:")}
|
|
2271
|
+
--server <id> Server ID (required)
|
|
2272
|
+
--site <id> Site ID (required)
|
|
2273
|
+
--file <path> Read the script from a file (use - for stdin)
|
|
2274
|
+
--content <str> Script content
|
|
2275
|
+
--check Compare with the content on Forge and do not write.
|
|
2276
|
+
Exit 0 when in sync, 1 with a line diff (- remote, + local).
|
|
2277
|
+
-f, --format <fmt> Output format: json ({ inSync, diff? } with --check), human
|
|
2278
|
+
|
|
2279
|
+
Give exactly one of --file or --content. An empty script is rejected.
|
|
2280
|
+
|
|
2281
|
+
${colors.bold("EXAMPLES:")}
|
|
2282
|
+
forge deployments update-script --server 123 --site 456 --file deploy.sh
|
|
2283
|
+
cat deploy.sh | forge deployments update-script --server 123 --site 456 --file -
|
|
2284
|
+
forge deployments update-script --server 123 --site 456 --file deploy.sh --check
|
|
2285
|
+
`);
|
|
2286
|
+
else if (subcommand === "deploy") console.log(`
|
|
2287
|
+
${colors.bold("forge deployments deploy")} - Trigger a deployment
|
|
2288
|
+
|
|
2289
|
+
${colors.bold("USAGE:")}
|
|
2290
|
+
forge deployments deploy --server <server_id> --site <site_id> [options]
|
|
2291
|
+
|
|
2292
|
+
${colors.bold("OPTIONS:")}
|
|
2293
|
+
--server <id> Server ID (required)
|
|
2294
|
+
--site <id> Site ID (required)
|
|
2295
|
+
--script-file <path> Upload this deployment script before deploying (use - for stdin).
|
|
2296
|
+
If the upload fails, the deployment does not start.
|
|
2297
|
+
--stream Stream deployment logs in real-time (default: show progress)
|
|
2298
|
+
-f, --format <fmt> Output format: json, human, table
|
|
1900
2299
|
`);
|
|
1901
2300
|
else console.log(`
|
|
1902
2301
|
${colors.bold("forge deployments")} - Manage deployments
|
|
@@ -1910,16 +2309,96 @@ ${colors.bold("ALIASES:")}
|
|
|
1910
2309
|
${colors.bold("SUBCOMMANDS:")}
|
|
1911
2310
|
list, ls List deployments for a site
|
|
1912
2311
|
deploy Trigger a deployment
|
|
2312
|
+
logs Show output of a deployment (latest if no id)
|
|
2313
|
+
script Show the deployment script
|
|
2314
|
+
update-script Update the deployment script from a file, stdin or a string
|
|
1913
2315
|
|
|
1914
2316
|
${colors.bold("OPTIONS:")}
|
|
1915
|
-
--server <id>
|
|
1916
|
-
--site <id>
|
|
1917
|
-
|
|
2317
|
+
--server <id> Server ID (required)
|
|
2318
|
+
--site <id> Site ID (required)
|
|
2319
|
+
--file <path> Script file, - for stdin (for update-script)
|
|
2320
|
+
--content <str> Script content (for update-script)
|
|
2321
|
+
--check Compare with Forge, do not write (for update-script)
|
|
2322
|
+
--script-file <path> Upload this script before deploying (for deploy)
|
|
2323
|
+
-f, --format <fmt> Output format: json, human, table
|
|
1918
2324
|
|
|
1919
2325
|
Run ${colors.cyan("forge deployments <subcommand> --help")} for details.
|
|
1920
2326
|
`);
|
|
1921
2327
|
}
|
|
1922
2328
|
//#endregion
|
|
2329
|
+
//#region src/commands/env/env-diff.ts
|
|
2330
|
+
var NAME = /^[A-Za-z_][A-Za-z0-9_.]*$/;
|
|
2331
|
+
/**
|
|
2332
|
+
* Find the closing quote of a quoted value. `\` escapes a character in double quotes only.
|
|
2333
|
+
* Returns -1 when the value is not closed.
|
|
2334
|
+
*/
|
|
2335
|
+
function closingQuote(value, quote) {
|
|
2336
|
+
for (let index = 1; index < value.length; index++) if (quote === "\"" && value[index] === "\\") index++;
|
|
2337
|
+
else if (value[index] === quote) return index;
|
|
2338
|
+
return -1;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Parse a .env file into a map of name to value, with the dotenv rules used by Laravel:
|
|
2342
|
+
* an optional `export ` prefix, quoted values that can span lines, and ` #` inline comments
|
|
2343
|
+
* after unquoted values. Blank lines, comments and lines without a valid name are ignored,
|
|
2344
|
+
* so a part of a value can never show as a name.
|
|
2345
|
+
*/
|
|
2346
|
+
function parseEnv(content) {
|
|
2347
|
+
const variables = /* @__PURE__ */ new Map();
|
|
2348
|
+
const lines = content.split(/\r?\n/);
|
|
2349
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2350
|
+
const line = lines[index].trim();
|
|
2351
|
+
const separator = line.indexOf("=");
|
|
2352
|
+
const name = line.slice(0, Math.max(separator, 0)).trim().replace(/^export\s+/, "");
|
|
2353
|
+
if (!NAME.test(name)) continue;
|
|
2354
|
+
let value = line.slice(separator + 1).trim();
|
|
2355
|
+
const quote = value[0];
|
|
2356
|
+
if (quote === "\"" || quote === "'") {
|
|
2357
|
+
let end = closingQuote(value, quote);
|
|
2358
|
+
while (end === -1 && index + 1 < lines.length) {
|
|
2359
|
+
index++;
|
|
2360
|
+
value += `\n${lines[index]}`;
|
|
2361
|
+
end = closingQuote(value, quote);
|
|
2362
|
+
}
|
|
2363
|
+
value = value.slice(1, end === -1 ? void 0 : end);
|
|
2364
|
+
} else value = value.replace(/\s+#.*$/, "");
|
|
2365
|
+
variables.set(name, value);
|
|
2366
|
+
}
|
|
2367
|
+
return variables;
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Compare the variables of two .env files. Returns only variable names.
|
|
2371
|
+
*/
|
|
2372
|
+
function diffEnv(remote, local) {
|
|
2373
|
+
const remoteVars = parseEnv(remote);
|
|
2374
|
+
const localVars = parseEnv(local);
|
|
2375
|
+
const result = {
|
|
2376
|
+
added: [],
|
|
2377
|
+
removed: [],
|
|
2378
|
+
changed: []
|
|
2379
|
+
};
|
|
2380
|
+
for (const [name, value] of localVars) if (!remoteVars.has(name)) result.added.push(name);
|
|
2381
|
+
else if (remoteVars.get(name) !== value) result.changed.push(name);
|
|
2382
|
+
for (const name of remoteVars.keys()) if (!localVars.has(name)) result.removed.push(name);
|
|
2383
|
+
return result;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Turn an env diff into a drift report, or null when the variables are the same.
|
|
2387
|
+
* The report holds variable names only, never values, because CI logs are often shared.
|
|
2388
|
+
*/
|
|
2389
|
+
function envDrift(remote, local) {
|
|
2390
|
+
const result = diffEnv(remote, local);
|
|
2391
|
+
const lines = [
|
|
2392
|
+
...result.added.map((name) => `+ ${name} (added)`),
|
|
2393
|
+
...result.removed.map((name) => `- ${name} (removed)`),
|
|
2394
|
+
...result.changed.map((name) => `~ ${name} (changed)`)
|
|
2395
|
+
];
|
|
2396
|
+
return lines.length > 0 ? {
|
|
2397
|
+
text: lines.join("\n"),
|
|
2398
|
+
json: result
|
|
2399
|
+
} : null;
|
|
2400
|
+
}
|
|
2401
|
+
//#endregion
|
|
1923
2402
|
//#region src/commands/env/handlers.ts
|
|
1924
2403
|
async function envGet(ctx) {
|
|
1925
2404
|
const server = String(ctx.options.server ?? "");
|
|
@@ -1939,19 +2418,32 @@ async function envGet(ctx) {
|
|
|
1939
2418
|
}, ctx.formatter);
|
|
1940
2419
|
}
|
|
1941
2420
|
async function envUpdate(ctx) {
|
|
2421
|
+
const usage = "forge env update --server <server_id> --site <site_id> (--file <path|-> | --content <content>) [--check]";
|
|
1942
2422
|
const server = String(ctx.options.server ?? "");
|
|
1943
2423
|
const site = String(ctx.options.site ?? "");
|
|
1944
|
-
const
|
|
1945
|
-
if (!server) exitWithValidationError("server_id",
|
|
1946
|
-
if (!site) exitWithValidationError("site_id",
|
|
1947
|
-
|
|
2424
|
+
const check = Boolean(ctx.options.check);
|
|
2425
|
+
if (!server) exitWithValidationError("server_id", usage, ctx.formatter);
|
|
2426
|
+
if (!site) exitWithValidationError("site_id", usage, ctx.formatter);
|
|
2427
|
+
const content = readContentOption(ctx.options, {
|
|
2428
|
+
label: "environment file",
|
|
2429
|
+
usage,
|
|
2430
|
+
formatter: ctx.formatter
|
|
2431
|
+
});
|
|
1948
2432
|
await runCommand(async () => {
|
|
1949
2433
|
const token = ctx.getToken();
|
|
1950
2434
|
const execCtx = ctx.createExecutorContext(token);
|
|
1951
2435
|
const server_id = await resolveServerId(server, execCtx);
|
|
2436
|
+
const site_id = await resolveSiteId(site, server_id, execCtx);
|
|
2437
|
+
if (check) {
|
|
2438
|
+
reportDrift("environment file", envDrift((await getEnv({
|
|
2439
|
+
server_id,
|
|
2440
|
+
site_id
|
|
2441
|
+
}, execCtx)).data, content), ctx.formatter);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
1952
2444
|
await updateEnv({
|
|
1953
2445
|
server_id,
|
|
1954
|
-
site_id
|
|
2446
|
+
site_id,
|
|
1955
2447
|
content
|
|
1956
2448
|
}, execCtx);
|
|
1957
2449
|
ctx.formatter.success("Environment variables updated.");
|
|
@@ -1980,7 +2472,26 @@ ${colors.bold("USAGE:")}
|
|
|
1980
2472
|
${colors.bold("forge env update")} - Update environment variables
|
|
1981
2473
|
|
|
1982
2474
|
${colors.bold("USAGE:")}
|
|
2475
|
+
forge env update --server <server_id> --site <site_id> --file <path>
|
|
2476
|
+
forge env update --server <server_id> --site <site_id> --file <path> --check
|
|
1983
2477
|
forge env update --server <server_id> --site <site_id> --content <env_content>
|
|
2478
|
+
|
|
2479
|
+
${colors.bold("OPTIONS:")}
|
|
2480
|
+
--server <id> Server ID (required)
|
|
2481
|
+
--site <id> Site ID (required)
|
|
2482
|
+
--file <path> Read the content from a file (use - for stdin)
|
|
2483
|
+
--content <str> New env content
|
|
2484
|
+
--check Compare with the .env on Forge and do not write.
|
|
2485
|
+
Exit 0 when in sync, 1 when different. Prints only variable
|
|
2486
|
+
names (added, removed, changed), never values.
|
|
2487
|
+
-f, --format <fmt> Output format: json ({ inSync, diff? } with --check), human
|
|
2488
|
+
|
|
2489
|
+
Give exactly one of --file or --content. Empty content is rejected.
|
|
2490
|
+
|
|
2491
|
+
${colors.bold("EXAMPLES:")}
|
|
2492
|
+
forge env update --server 123 --site 456 --file .env
|
|
2493
|
+
cat .env | forge env update --server 123 --site 456 --file -
|
|
2494
|
+
forge env update --server 123 --site 456 --file .env --check
|
|
1984
2495
|
`);
|
|
1985
2496
|
else console.log(`
|
|
1986
2497
|
${colors.bold("forge env")} - Manage environment variables
|
|
@@ -1995,7 +2506,9 @@ ${colors.bold("SUBCOMMANDS:")}
|
|
|
1995
2506
|
${colors.bold("OPTIONS:")}
|
|
1996
2507
|
--server <id> Server ID (required)
|
|
1997
2508
|
--site <id> Site ID (required)
|
|
2509
|
+
--file <path> Content file, - for stdin (for update)
|
|
1998
2510
|
--content <str> New env content (for update)
|
|
2511
|
+
--check Compare with Forge, do not write (for update)
|
|
1999
2512
|
-f, --format <fmt> Output format: json, human
|
|
2000
2513
|
|
|
2001
2514
|
Run ${colors.cyan("forge env <subcommand> --help")} for details.
|
|
@@ -2266,19 +2779,32 @@ async function nginxGet(ctx) {
|
|
|
2266
2779
|
}, ctx.formatter);
|
|
2267
2780
|
}
|
|
2268
2781
|
async function nginxUpdate(ctx) {
|
|
2782
|
+
const usage = "forge nginx update --server <server_id> --site <site_id> (--file <path|-> | --content <content>) [--check]";
|
|
2269
2783
|
const server = String(ctx.options.server ?? "");
|
|
2270
2784
|
const site = String(ctx.options.site ?? "");
|
|
2271
|
-
const
|
|
2272
|
-
if (!server) exitWithValidationError("server_id",
|
|
2273
|
-
if (!site) exitWithValidationError("site_id",
|
|
2274
|
-
|
|
2785
|
+
const check = Boolean(ctx.options.check);
|
|
2786
|
+
if (!server) exitWithValidationError("server_id", usage, ctx.formatter);
|
|
2787
|
+
if (!site) exitWithValidationError("site_id", usage, ctx.formatter);
|
|
2788
|
+
const content = readContentOption(ctx.options, {
|
|
2789
|
+
label: "nginx configuration",
|
|
2790
|
+
usage,
|
|
2791
|
+
formatter: ctx.formatter
|
|
2792
|
+
});
|
|
2275
2793
|
await runCommand(async () => {
|
|
2276
2794
|
const token = ctx.getToken();
|
|
2277
2795
|
const execCtx = ctx.createExecutorContext(token);
|
|
2278
2796
|
const server_id = await resolveServerId(server, execCtx);
|
|
2797
|
+
const site_id = await resolveSiteId(site, server_id, execCtx);
|
|
2798
|
+
if (check) {
|
|
2799
|
+
reportTextDrift("nginx configuration", (await getNginxConfig({
|
|
2800
|
+
server_id,
|
|
2801
|
+
site_id
|
|
2802
|
+
}, execCtx)).data, content, ctx.formatter);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2279
2805
|
await updateNginxConfig({
|
|
2280
2806
|
server_id,
|
|
2281
|
-
site_id
|
|
2807
|
+
site_id,
|
|
2282
2808
|
content
|
|
2283
2809
|
}, execCtx);
|
|
2284
2810
|
ctx.formatter.success("Nginx configuration updated.");
|
|
@@ -2307,7 +2833,25 @@ ${colors.bold("USAGE:")}
|
|
|
2307
2833
|
${colors.bold("forge nginx update")} - Update nginx configuration
|
|
2308
2834
|
|
|
2309
2835
|
${colors.bold("USAGE:")}
|
|
2836
|
+
forge nginx update --server <server_id> --site <site_id> --file <path>
|
|
2837
|
+
forge nginx update --server <server_id> --site <site_id> --file <path> --check
|
|
2310
2838
|
forge nginx update --server <server_id> --site <site_id> --content <nginx_config>
|
|
2839
|
+
|
|
2840
|
+
${colors.bold("OPTIONS:")}
|
|
2841
|
+
--server <id> Server ID (required)
|
|
2842
|
+
--site <id> Site ID (required)
|
|
2843
|
+
--file <path> Read the content from a file (use - for stdin)
|
|
2844
|
+
--content <str> New nginx config
|
|
2845
|
+
--check Compare with the content on Forge and do not write.
|
|
2846
|
+
Exit 0 when in sync, 1 with a line diff (- remote, + local).
|
|
2847
|
+
-f, --format <fmt> Output format: json ({ inSync, diff? } with --check), human
|
|
2848
|
+
|
|
2849
|
+
Give exactly one of --file or --content. Empty content is rejected.
|
|
2850
|
+
|
|
2851
|
+
${colors.bold("EXAMPLES:")}
|
|
2852
|
+
forge nginx update --server 123 --site 456 --file nginx.conf
|
|
2853
|
+
cat nginx.conf | forge nginx update --server 123 --site 456 --file -
|
|
2854
|
+
forge nginx update --server 123 --site 456 --file nginx.conf --check
|
|
2311
2855
|
`);
|
|
2312
2856
|
else console.log(`
|
|
2313
2857
|
${colors.bold("forge nginx")} - Manage nginx configuration
|
|
@@ -2322,7 +2866,9 @@ ${colors.bold("SUBCOMMANDS:")}
|
|
|
2322
2866
|
${colors.bold("OPTIONS:")}
|
|
2323
2867
|
--server <id> Server ID (required)
|
|
2324
2868
|
--site <id> Site ID (required)
|
|
2869
|
+
--file <path> Content file, - for stdin (for update)
|
|
2325
2870
|
--content <str> New nginx config (for update)
|
|
2871
|
+
--check Compare with Forge, do not write (for update)
|
|
2326
2872
|
|
|
2327
2873
|
Run ${colors.cyan("forge nginx <subcommand> --help")} for details.
|
|
2328
2874
|
`);
|
|
@@ -2512,6 +3058,328 @@ Run ${colors.cyan("forge nginx-templates <subcommand> --help")} for details.
|
|
|
2512
3058
|
`);
|
|
2513
3059
|
}
|
|
2514
3060
|
//#endregion
|
|
3061
|
+
//#region src/commands/php/handlers.ts
|
|
3062
|
+
var CONFIG_USAGE = "forge php config --server <server_id> --version <version> --type <cli|fpm|pool> [--user <user>]";
|
|
3063
|
+
var UPDATE_CONFIG_USAGE = "forge php update-config --server <server_id> --version <version> --type <cli|fpm|pool> (--file <path|-> | --content <content>) [--user <user>] [--check]";
|
|
3064
|
+
/**
|
|
3065
|
+
* Return the --server option, or exit with a validation error when it is missing.
|
|
3066
|
+
*/
|
|
3067
|
+
function requireServer(ctx, usage) {
|
|
3068
|
+
const server = String(ctx.options.server ?? "");
|
|
3069
|
+
if (!server) exitWithValidationError("server_id", usage, ctx.formatter);
|
|
3070
|
+
return server;
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Resolve the server and run `fn` with its ID, with the CLI error handling.
|
|
3074
|
+
*/
|
|
3075
|
+
async function withServer(ctx, server, fn) {
|
|
3076
|
+
await runCommand(async () => {
|
|
3077
|
+
const execCtx = ctx.createExecutorContext(ctx.getToken());
|
|
3078
|
+
await fn(await resolveServerId(server, execCtx), execCtx);
|
|
3079
|
+
}, ctx.formatter);
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Read --version, --type and --user for the config commands.
|
|
3083
|
+
* Exits with a validation error when --version is missing, --type is not cli, fpm or pool,
|
|
3084
|
+
* or --user is given for a type other than pool.
|
|
3085
|
+
*/
|
|
3086
|
+
function requireConfigTarget(ctx, usage) {
|
|
3087
|
+
const { version, type, user } = ctx.options;
|
|
3088
|
+
if (typeof version !== "string" || !version) exitWithValidationError("version", usage, ctx.formatter);
|
|
3089
|
+
if (typeof type !== "string" || !PHP_CONFIG_TYPES.includes(type)) exitWithError(new ValidationError(typeof type === "string" ? `Invalid --type "${type}"` : "type is required", "type", [`Valid types: ${PHP_CONFIG_TYPES.join(", ")}`, `Usage: ${usage}`]), ctx.formatter);
|
|
3090
|
+
if (typeof user === "string" && user && type !== "pool") exitWithError(new ValidationError(`--user applies to --type pool only, not "${type}"`, "user", [`Usage: ${usage}`]), ctx.formatter);
|
|
3091
|
+
return {
|
|
3092
|
+
version,
|
|
3093
|
+
type,
|
|
3094
|
+
user: typeof user === "string" ? user : void 0
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Parse a whole number argument, or exit with a validation error.
|
|
3099
|
+
*/
|
|
3100
|
+
function requireWholeNumber(value, field, usage, ctx) {
|
|
3101
|
+
if (value === void 0 || !/^\d+$/.test(value)) exitWithError(new ValidationError(value === void 0 ? `${field} is required` : `${field} must be a whole number: "${value}"`, field, [`Usage: ${usage}`]), ctx.formatter);
|
|
3102
|
+
return Number(value);
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* List the PHP versions installed on a server.
|
|
3106
|
+
*/
|
|
3107
|
+
async function phpList(ctx) {
|
|
3108
|
+
await withServer(ctx, requireServer(ctx, "forge php list --server <server_id>"), async (server_id, execCtx) => {
|
|
3109
|
+
const result = await listPhpVersions({ server_id }, execCtx);
|
|
3110
|
+
ctx.formatter.outputList(result.data, [
|
|
3111
|
+
"id",
|
|
3112
|
+
"version",
|
|
3113
|
+
"binary_name",
|
|
3114
|
+
"status"
|
|
3115
|
+
], "No PHP versions installed.", (v) => `${String(v.id).padEnd(8)} ${v.version.padEnd(6)} ${v.binary_name.padEnd(10)} ${v.status}`);
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Print a PHP configuration file (cli, fpm or pool).
|
|
3120
|
+
*/
|
|
3121
|
+
async function phpConfig(ctx) {
|
|
3122
|
+
const server = requireServer(ctx, CONFIG_USAGE);
|
|
3123
|
+
const target = requireConfigTarget(ctx, CONFIG_USAGE);
|
|
3124
|
+
await withServer(ctx, server, async (server_id, execCtx) => {
|
|
3125
|
+
const result = await getPhpConfig({
|
|
3126
|
+
server_id,
|
|
3127
|
+
...target
|
|
3128
|
+
}, execCtx);
|
|
3129
|
+
if (ctx.formatter.isJson()) console.log(JSON.stringify({ content: result.data }));
|
|
3130
|
+
else console.log(result.data);
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Replace a PHP configuration file, or compare it with Forge when --check is set.
|
|
3135
|
+
*/
|
|
3136
|
+
async function phpUpdateConfig(ctx) {
|
|
3137
|
+
const server = requireServer(ctx, UPDATE_CONFIG_USAGE);
|
|
3138
|
+
const target = requireConfigTarget(ctx, UPDATE_CONFIG_USAGE);
|
|
3139
|
+
const label = `PHP ${target.type} configuration`;
|
|
3140
|
+
const content = readContentOption(ctx.options, {
|
|
3141
|
+
label,
|
|
3142
|
+
usage: UPDATE_CONFIG_USAGE,
|
|
3143
|
+
formatter: ctx.formatter
|
|
3144
|
+
});
|
|
3145
|
+
await withServer(ctx, server, async (server_id, execCtx) => {
|
|
3146
|
+
if (ctx.options.check) {
|
|
3147
|
+
const remote = await getPhpConfig({
|
|
3148
|
+
server_id,
|
|
3149
|
+
...target
|
|
3150
|
+
}, execCtx);
|
|
3151
|
+
reportTextDrift(label, remote.data, content, ctx.formatter);
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
await updatePhpConfig({
|
|
3155
|
+
server_id,
|
|
3156
|
+
...target,
|
|
3157
|
+
content
|
|
3158
|
+
}, execCtx);
|
|
3159
|
+
ctx.formatter.success(`${label} updated.`);
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Print the PHP max upload size, in megabytes.
|
|
3164
|
+
*/
|
|
3165
|
+
async function phpMaxUploadSize(ctx) {
|
|
3166
|
+
await withServer(ctx, requireServer(ctx, "forge php max-upload-size --server <server_id>"), async (server_id, execCtx) => {
|
|
3167
|
+
const { data } = await getPhpMaxUploadSize({ server_id }, execCtx);
|
|
3168
|
+
if (ctx.formatter.isJson()) ctx.formatter.output({ max_upload_size: data });
|
|
3169
|
+
else console.log(`${data} MB`);
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Set the PHP max upload size, in megabytes.
|
|
3174
|
+
*/
|
|
3175
|
+
async function phpSetMaxUploadSize(args, ctx) {
|
|
3176
|
+
const usage = "forge php set-max-upload-size <megabytes> --server <server_id>";
|
|
3177
|
+
const size = requireWholeNumber(args[0], "megabytes", usage, ctx);
|
|
3178
|
+
await withServer(ctx, requireServer(ctx, usage), async (server_id, execCtx) => {
|
|
3179
|
+
await updatePhpMaxUploadSize({
|
|
3180
|
+
server_id,
|
|
3181
|
+
size
|
|
3182
|
+
}, execCtx);
|
|
3183
|
+
ctx.formatter.success(`PHP max upload size set to ${size} MB.`);
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* Print the PHP max execution time, in seconds.
|
|
3188
|
+
*/
|
|
3189
|
+
async function phpMaxExecutionTime(ctx) {
|
|
3190
|
+
await withServer(ctx, requireServer(ctx, "forge php max-execution-time --server <server_id>"), async (server_id, execCtx) => {
|
|
3191
|
+
const { data } = await getPhpMaxExecutionTime({ server_id }, execCtx);
|
|
3192
|
+
if (ctx.formatter.isJson()) ctx.formatter.output({ max_execution_time: data });
|
|
3193
|
+
else console.log(`${data} seconds`);
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Set the PHP max execution time, in seconds.
|
|
3198
|
+
*/
|
|
3199
|
+
async function phpSetMaxExecutionTime(args, ctx) {
|
|
3200
|
+
const usage = "forge php set-max-execution-time <seconds> --server <server_id>";
|
|
3201
|
+
const seconds = requireWholeNumber(args[0], "seconds", usage, ctx);
|
|
3202
|
+
await withServer(ctx, requireServer(ctx, usage), async (server_id, execCtx) => {
|
|
3203
|
+
await updatePhpMaxExecutionTime({
|
|
3204
|
+
server_id,
|
|
3205
|
+
seconds
|
|
3206
|
+
}, execCtx);
|
|
3207
|
+
ctx.formatter.success(`PHP max execution time set to ${seconds} seconds.`);
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Print whether OPcache is enabled.
|
|
3212
|
+
*/
|
|
3213
|
+
async function phpOpcache(ctx) {
|
|
3214
|
+
await withServer(ctx, requireServer(ctx, "forge php opcache --server <server_id>"), async (server_id, execCtx) => {
|
|
3215
|
+
const { data } = await getPhpOpcache({ server_id }, execCtx);
|
|
3216
|
+
if (ctx.formatter.isJson()) ctx.formatter.output({ opcache_enabled: data });
|
|
3217
|
+
else console.log(`OPcache is ${data ? "enabled" : "disabled"}.`);
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Enable OPcache.
|
|
3222
|
+
*/
|
|
3223
|
+
async function phpEnableOpcache(ctx) {
|
|
3224
|
+
await withServer(ctx, requireServer(ctx, "forge php enable-opcache --server <server_id>"), async (server_id, execCtx) => {
|
|
3225
|
+
await enablePhpOpcache({ server_id }, execCtx);
|
|
3226
|
+
ctx.formatter.success("OPcache enabled.");
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Disable OPcache.
|
|
3231
|
+
*/
|
|
3232
|
+
async function phpDisableOpcache(ctx) {
|
|
3233
|
+
await withServer(ctx, requireServer(ctx, "forge php disable-opcache --server <server_id>"), async (server_id, execCtx) => {
|
|
3234
|
+
await disablePhpOpcache({ server_id }, execCtx);
|
|
3235
|
+
ctx.formatter.success("OPcache disabled.");
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
//#endregion
|
|
3239
|
+
//#region src/commands/php/command.ts
|
|
3240
|
+
var handlePhpCommand = createCommandRouter({
|
|
3241
|
+
resource: "php",
|
|
3242
|
+
handlers: {
|
|
3243
|
+
list: phpList,
|
|
3244
|
+
ls: phpList,
|
|
3245
|
+
config: phpConfig,
|
|
3246
|
+
"update-config": phpUpdateConfig,
|
|
3247
|
+
"max-upload-size": phpMaxUploadSize,
|
|
3248
|
+
"set-max-upload-size": [phpSetMaxUploadSize, "args"],
|
|
3249
|
+
"max-execution-time": phpMaxExecutionTime,
|
|
3250
|
+
"set-max-execution-time": [phpSetMaxExecutionTime, "args"],
|
|
3251
|
+
opcache: phpOpcache,
|
|
3252
|
+
"enable-opcache": phpEnableOpcache,
|
|
3253
|
+
"disable-opcache": phpDisableOpcache
|
|
3254
|
+
},
|
|
3255
|
+
writeSubcommands: [
|
|
3256
|
+
"update-config",
|
|
3257
|
+
"set-max-upload-size",
|
|
3258
|
+
"set-max-execution-time",
|
|
3259
|
+
"enable-opcache",
|
|
3260
|
+
"disable-opcache"
|
|
3261
|
+
]
|
|
3262
|
+
});
|
|
3263
|
+
//#endregion
|
|
3264
|
+
//#region src/commands/php/help.ts
|
|
3265
|
+
function showPhpHelp(subcommand) {
|
|
3266
|
+
if (subcommand === "list" || subcommand === "ls") console.log(`
|
|
3267
|
+
${colors.bold("forge php list")} - List the PHP versions installed on a server
|
|
3268
|
+
|
|
3269
|
+
${colors.bold("USAGE:")}
|
|
3270
|
+
forge php list --server <server_id>
|
|
3271
|
+
|
|
3272
|
+
${colors.bold("OPTIONS:")}
|
|
3273
|
+
--server <id> Server ID or name (required)
|
|
3274
|
+
-f, --format <fmt> Output format: json, human, table
|
|
3275
|
+
|
|
3276
|
+
${colors.bold("EXAMPLES:")}
|
|
3277
|
+
forge php list --server 123
|
|
3278
|
+
`);
|
|
3279
|
+
else if (subcommand === "config") console.log(`
|
|
3280
|
+
${colors.bold("forge php config")} - Print a PHP configuration file
|
|
3281
|
+
|
|
3282
|
+
${colors.bold("USAGE:")}
|
|
3283
|
+
forge php config --server <server_id> --version <version> --type <cli|fpm|pool>
|
|
3284
|
+
|
|
3285
|
+
${colors.bold("OPTIONS:")}
|
|
3286
|
+
--server <id> Server ID or name (required)
|
|
3287
|
+
--version <ver> PHP version ID, or a version such as 8.3, php83 or php8.3 (required)
|
|
3288
|
+
--type <type> cli (CLI php.ini), fpm (FPM php.ini) or pool (FPM pool) (required)
|
|
3289
|
+
--user <name> FPM pool user (pool only)
|
|
3290
|
+
-f, --format <fmt> Output format: json ({ content }), human
|
|
3291
|
+
|
|
3292
|
+
${colors.bold("EXAMPLES:")}
|
|
3293
|
+
forge php config --server 123 --version 8.3 --type fpm
|
|
3294
|
+
forge php config --server 123 --version 8.3 --type pool > www.conf
|
|
3295
|
+
`);
|
|
3296
|
+
else if (subcommand === "update-config") console.log(`
|
|
3297
|
+
${colors.bold("forge php update-config")} - Replace a PHP configuration file
|
|
3298
|
+
|
|
3299
|
+
${colors.bold("USAGE:")}
|
|
3300
|
+
forge php update-config --server <server_id> --version <version> --type <cli|fpm|pool> --file <path>
|
|
3301
|
+
forge php update-config --server <server_id> --version <version> --type <cli|fpm|pool> --file <path> --check
|
|
3302
|
+
forge php update-config --server <server_id> --version <version> --type <cli|fpm|pool> --content <config>
|
|
3303
|
+
|
|
3304
|
+
${colors.bold("OPTIONS:")}
|
|
3305
|
+
--server <id> Server ID or name (required)
|
|
3306
|
+
--version <ver> PHP version ID, or a version such as 8.3, php83 or php8.3 (required)
|
|
3307
|
+
--type <type> cli (CLI php.ini), fpm (FPM php.ini) or pool (FPM pool) (required)
|
|
3308
|
+
--user <name> FPM pool user (pool only)
|
|
3309
|
+
--file <path> Read the content from a file (use - for stdin)
|
|
3310
|
+
--content <str> New configuration file content
|
|
3311
|
+
--check Compare with the content on Forge and do not write.
|
|
3312
|
+
Exit 0 when in sync, 1 with a line diff (- remote, + local).
|
|
3313
|
+
-f, --format <fmt> Output format: json ({ inSync, diff? } with --check), human
|
|
3314
|
+
|
|
3315
|
+
Give exactly one of --file or --content. Empty content is rejected.
|
|
3316
|
+
The content replaces the whole file.
|
|
3317
|
+
|
|
3318
|
+
${colors.bold("EXAMPLES:")}
|
|
3319
|
+
forge php update-config --server 123 --version 8.3 --type fpm --file php.ini
|
|
3320
|
+
cat php.ini | forge php update-config --server 123 --version 8.3 --type cli --file -
|
|
3321
|
+
forge php update-config --server 123 --version 8.3 --type fpm --file php.ini --check
|
|
3322
|
+
`);
|
|
3323
|
+
else if (subcommand === "max-upload-size" || subcommand === "set-max-upload-size" || subcommand === "max-execution-time" || subcommand === "set-max-execution-time") console.log(`
|
|
3324
|
+
${colors.bold("forge php max-upload-size / max-execution-time")} - Get or set PHP limits
|
|
3325
|
+
|
|
3326
|
+
${colors.bold("USAGE:")}
|
|
3327
|
+
forge php max-upload-size --server <server_id>
|
|
3328
|
+
forge php set-max-upload-size <megabytes> --server <server_id>
|
|
3329
|
+
forge php max-execution-time --server <server_id>
|
|
3330
|
+
forge php set-max-execution-time <seconds> --server <server_id>
|
|
3331
|
+
|
|
3332
|
+
${colors.bold("OPTIONS:")}
|
|
3333
|
+
--server <id> Server ID or name (required)
|
|
3334
|
+
-f, --format <fmt> Output format: json, human
|
|
3335
|
+
|
|
3336
|
+
${colors.bold("EXAMPLES:")}
|
|
3337
|
+
forge php set-max-upload-size 128 --server 123
|
|
3338
|
+
forge php set-max-execution-time 120 --server 123
|
|
3339
|
+
`);
|
|
3340
|
+
else if (subcommand === "opcache" || subcommand === "enable-opcache" || subcommand === "disable-opcache") console.log(`
|
|
3341
|
+
${colors.bold("forge php opcache")} - Get, enable or disable OPcache
|
|
3342
|
+
|
|
3343
|
+
${colors.bold("USAGE:")}
|
|
3344
|
+
forge php opcache --server <server_id>
|
|
3345
|
+
forge php enable-opcache --server <server_id>
|
|
3346
|
+
forge php disable-opcache --server <server_id>
|
|
3347
|
+
|
|
3348
|
+
${colors.bold("OPTIONS:")}
|
|
3349
|
+
--server <id> Server ID or name (required)
|
|
3350
|
+
-f, --format <fmt> Output format: json, human
|
|
3351
|
+
`);
|
|
3352
|
+
else console.log(`
|
|
3353
|
+
${colors.bold("forge php")} - Manage PHP on a server
|
|
3354
|
+
|
|
3355
|
+
${colors.bold("USAGE:")}
|
|
3356
|
+
forge php <subcommand> [options]
|
|
3357
|
+
|
|
3358
|
+
${colors.bold("SUBCOMMANDS:")}
|
|
3359
|
+
list, ls List installed PHP versions
|
|
3360
|
+
config Print a configuration file (--version --type)
|
|
3361
|
+
update-config Replace a configuration file (--version --type --file|--content) [--check]
|
|
3362
|
+
max-upload-size Print the max upload size (MB)
|
|
3363
|
+
set-max-upload-size <mb> Set the max upload size (MB)
|
|
3364
|
+
max-execution-time Print the max execution time (seconds)
|
|
3365
|
+
set-max-execution-time <sec> Set the max execution time (seconds)
|
|
3366
|
+
opcache Print the OPcache status
|
|
3367
|
+
enable-opcache Enable OPcache
|
|
3368
|
+
disable-opcache Disable OPcache
|
|
3369
|
+
|
|
3370
|
+
${colors.bold("OPTIONS:")}
|
|
3371
|
+
--server <id> Server ID or name (required)
|
|
3372
|
+
--version <ver> PHP version ID or version (8.3, php83) (for config commands)
|
|
3373
|
+
--type <type> cli, fpm or pool (for config commands)
|
|
3374
|
+
--user <name> FPM pool user (for --type pool)
|
|
3375
|
+
--file <path> Content file, - for stdin (for update-config)
|
|
3376
|
+
--content <str> New content (for update-config)
|
|
3377
|
+
--check Compare with Forge, do not write (for update-config)
|
|
3378
|
+
|
|
3379
|
+
Run ${colors.cyan("forge php <subcommand> --help")} for details.
|
|
3380
|
+
`);
|
|
3381
|
+
}
|
|
3382
|
+
//#endregion
|
|
2515
3383
|
//#region src/commands/recipes/handlers.ts
|
|
2516
3384
|
async function recipesList(ctx) {
|
|
2517
3385
|
await runCommand(async () => {
|
|
@@ -3321,6 +4189,122 @@ Run ${colors.cyan("forge servers <subcommand> --help")} for subcommand details.
|
|
|
3321
4189
|
`);
|
|
3322
4190
|
}
|
|
3323
4191
|
//#endregion
|
|
4192
|
+
//#region src/commands/services/handlers.ts
|
|
4193
|
+
/**
|
|
4194
|
+
* List services on a server with a derived availability flag.
|
|
4195
|
+
*/
|
|
4196
|
+
async function servicesList(args, ctx) {
|
|
4197
|
+
const [server] = args;
|
|
4198
|
+
if (!server) exitWithValidationError("server", "forge services list <server>", ctx.formatter);
|
|
4199
|
+
await runCommand(async () => {
|
|
4200
|
+
const token = ctx.getToken();
|
|
4201
|
+
const execCtx = ctx.createExecutorContext(token);
|
|
4202
|
+
const result = await listServices({ server_id: await resolveServerId(server, execCtx) }, execCtx);
|
|
4203
|
+
ctx.formatter.outputList(result.data, [
|
|
4204
|
+
"service",
|
|
4205
|
+
"available",
|
|
4206
|
+
"detail"
|
|
4207
|
+
], "No services.", (row) => {
|
|
4208
|
+
const status = row.available ? "yes" : "no";
|
|
4209
|
+
const detail = row.detail ? ` (${row.detail})` : "";
|
|
4210
|
+
return `${row.service.padEnd(12)}${status}${detail}`;
|
|
4211
|
+
});
|
|
4212
|
+
}, ctx.formatter);
|
|
4213
|
+
}
|
|
4214
|
+
/**
|
|
4215
|
+
* Restart a service on a server.
|
|
4216
|
+
*/
|
|
4217
|
+
async function servicesRestart(args, ctx) {
|
|
4218
|
+
const [server, service] = args;
|
|
4219
|
+
if (!server) exitWithValidationError("server", "forge services restart <server> <service>", ctx.formatter);
|
|
4220
|
+
if (!service) exitWithValidationError("service", "forge services restart <server> <service>", ctx.formatter);
|
|
4221
|
+
const version = typeof ctx.options.version === "string" && ctx.options.version ? ctx.options.version : void 0;
|
|
4222
|
+
await runCommand(async () => {
|
|
4223
|
+
if (!RESTARTABLE_SERVICES.includes(service)) throw new ValidationError(`Invalid service "${service}"`, "service", [`Valid services: ${RESTARTABLE_SERVICES.join(", ")}`]);
|
|
4224
|
+
const token = ctx.getToken();
|
|
4225
|
+
const execCtx = ctx.createExecutorContext(token);
|
|
4226
|
+
const server_id = await resolveServerId(server, execCtx);
|
|
4227
|
+
let resolvedVersion = version;
|
|
4228
|
+
if (service === "php" && !resolvedVersion) {
|
|
4229
|
+
const { data: srv } = await getServer({ server_id }, execCtx);
|
|
4230
|
+
if (!srv.php_version) throw new ValidationError("The php service requires a version", "version", ["Pass --version <php_version> (e.g. --version php83)"]);
|
|
4231
|
+
resolvedVersion = srv.php_version;
|
|
4232
|
+
}
|
|
4233
|
+
await restartService({
|
|
4234
|
+
server_id,
|
|
4235
|
+
service,
|
|
4236
|
+
version: resolvedVersion
|
|
4237
|
+
}, execCtx);
|
|
4238
|
+
ctx.formatter.success(`${service} restart initiated on server ${server_id}.`);
|
|
4239
|
+
}, ctx.formatter);
|
|
4240
|
+
}
|
|
4241
|
+
//#endregion
|
|
4242
|
+
//#region src/commands/services/command.ts
|
|
4243
|
+
var handleServicesCommand = createCommandRouter({
|
|
4244
|
+
resource: "services",
|
|
4245
|
+
handlers: {
|
|
4246
|
+
list: [servicesList, "args"],
|
|
4247
|
+
ls: [servicesList, "args"],
|
|
4248
|
+
restart: [servicesRestart, "args"]
|
|
4249
|
+
},
|
|
4250
|
+
writeSubcommands: ["restart"]
|
|
4251
|
+
});
|
|
4252
|
+
//#endregion
|
|
4253
|
+
//#region src/commands/services/help.ts
|
|
4254
|
+
function showServicesHelp(subcommand) {
|
|
4255
|
+
if (subcommand === "list" || subcommand === "ls") console.log(`
|
|
4256
|
+
${colors.bold("forge services list")} - List services on a server
|
|
4257
|
+
|
|
4258
|
+
${colors.bold("USAGE:")}
|
|
4259
|
+
forge services list <server> [options]
|
|
4260
|
+
|
|
4261
|
+
${colors.bold("ARGUMENTS:")}
|
|
4262
|
+
<server> Server ID or name (required)
|
|
4263
|
+
|
|
4264
|
+
${colors.bold("NOTES:")}
|
|
4265
|
+
Availability is a heuristic derived from the server object — the API has
|
|
4266
|
+
no services listing endpoint.
|
|
4267
|
+
|
|
4268
|
+
${colors.bold("EXAMPLES:")}
|
|
4269
|
+
forge services list 123
|
|
4270
|
+
forge services list my-server --format json
|
|
4271
|
+
`);
|
|
4272
|
+
else if (subcommand === "restart") console.log(`
|
|
4273
|
+
${colors.bold("forge services restart")} - Restart a service on a server
|
|
4274
|
+
|
|
4275
|
+
${colors.bold("USAGE:")}
|
|
4276
|
+
forge services restart <server> <service> [options]
|
|
4277
|
+
|
|
4278
|
+
${colors.bold("ARGUMENTS:")}
|
|
4279
|
+
<server> Server ID or name (required)
|
|
4280
|
+
<service> One of: nginx, php, mysql, postgres, redis, supervisor
|
|
4281
|
+
|
|
4282
|
+
${colors.bold("OPTIONS:")}
|
|
4283
|
+
--version <ver> PHP version (e.g. php83) — required for the php service
|
|
4284
|
+
(defaults to the server's php_version when omitted)
|
|
4285
|
+
|
|
4286
|
+
${colors.bold("EXAMPLES:")}
|
|
4287
|
+
forge services restart 123 nginx
|
|
4288
|
+
forge services restart 123 php --version php83
|
|
4289
|
+
`);
|
|
4290
|
+
else console.log(`
|
|
4291
|
+
${colors.bold("forge services")} - List and restart server services
|
|
4292
|
+
|
|
4293
|
+
${colors.bold("USAGE:")}
|
|
4294
|
+
forge services <subcommand> [options]
|
|
4295
|
+
|
|
4296
|
+
${colors.bold("SUBCOMMANDS:")}
|
|
4297
|
+
list, ls List services on a server
|
|
4298
|
+
restart <service> Restart a service (nginx, php, mysql, postgres, redis, supervisor)
|
|
4299
|
+
|
|
4300
|
+
${colors.bold("OPTIONS:")}
|
|
4301
|
+
--version <ver> PHP version (for the php service)
|
|
4302
|
+
-f, --format <fmt> Output format: json, human, table
|
|
4303
|
+
|
|
4304
|
+
Run ${colors.cyan("forge services <subcommand> --help")} for details.
|
|
4305
|
+
`);
|
|
4306
|
+
}
|
|
4307
|
+
//#endregion
|
|
3324
4308
|
//#region src/commands/sites/handlers.ts
|
|
3325
4309
|
async function sitesList(ctx) {
|
|
3326
4310
|
const server = String(ctx.options.server ?? "");
|
|
@@ -3661,14 +4645,285 @@ async function sshKeysGet(args, ctx) {
|
|
|
3661
4645
|
}, ctx.formatter);
|
|
3662
4646
|
}
|
|
3663
4647
|
//#endregion
|
|
4648
|
+
//#region src/commands/ssh-keys/sync-plan.ts
|
|
4649
|
+
/**
|
|
4650
|
+
* Pure logic for `forge ssh-keys sync`: parse an authorized_keys file, give each key
|
|
4651
|
+
* a stable managed name, and compare the file with the keys on a server.
|
|
4652
|
+
*
|
|
4653
|
+
* The Forge API does not return public keys, so keys are matched by name. The name
|
|
4654
|
+
* ends with a fingerprint of the key, so a changed key gets a new name.
|
|
4655
|
+
*/
|
|
4656
|
+
/** The Forge API documents no name limit: keep names short enough to read in a list. */
|
|
4657
|
+
var MAX_SLUG_LENGTH = 40;
|
|
4658
|
+
var KEY_TYPE = /^(ssh-|ecdsa-sha2-|sk-)[a-z0-9@.-]+$/;
|
|
4659
|
+
var BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
4660
|
+
/**
|
|
4661
|
+
* Check the base64 key data of a public key, as OpenSSH does: it must be canonical
|
|
4662
|
+
* base64, and the decoded key must start with its own type as a length-prefixed string.
|
|
4663
|
+
*/
|
|
4664
|
+
function isValidKeyBlob(type, blob) {
|
|
4665
|
+
if (!BASE64.test(blob) || blob.length % 4 !== 0) return false;
|
|
4666
|
+
const data = Buffer.from(blob, "base64");
|
|
4667
|
+
if (data.toString("base64") !== blob || data.length < 4) return false;
|
|
4668
|
+
const typeLength = data.readUInt32BE(0);
|
|
4669
|
+
return 4 + typeLength <= data.length && data.toString("latin1", 4, 4 + typeLength) === type;
|
|
4670
|
+
}
|
|
4671
|
+
/**
|
|
4672
|
+
* Convert a key comment to a name prefix: lowercase, `[a-z0-9-]` only, "key" when empty.
|
|
4673
|
+
*/
|
|
4674
|
+
function slugify(comment) {
|
|
4675
|
+
return comment.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").slice(0, MAX_SLUG_LENGTH).replaceAll(/^-+|-+$/g, "") || "key";
|
|
4676
|
+
}
|
|
4677
|
+
/**
|
|
4678
|
+
* Managed name of a key: "<slug(comment)>-<first 8 hex chars of the SHA-256 of the key blob>".
|
|
4679
|
+
* The blob is the decoded base64 field, as in an OpenSSH fingerprint.
|
|
4680
|
+
*/
|
|
4681
|
+
function managedKeyName(comment, blob) {
|
|
4682
|
+
const hash = createHash("sha256").update(Buffer.from(blob, "base64")).digest("hex");
|
|
4683
|
+
return `${slugify(comment)}-${hash.slice(0, 8)}`;
|
|
4684
|
+
}
|
|
4685
|
+
/**
|
|
4686
|
+
* Parse an authorized_keys file: one "<type> <base64> [comment]" per line.
|
|
4687
|
+
* Blank lines and "#" comments are ignored.
|
|
4688
|
+
*
|
|
4689
|
+
* @throws ValidationError for a malformed or duplicate key, with its line number.
|
|
4690
|
+
*/
|
|
4691
|
+
function parseAuthorizedKeys(content) {
|
|
4692
|
+
const keys = [];
|
|
4693
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4694
|
+
for (const [index, raw] of content.split(/\r?\n/).entries()) {
|
|
4695
|
+
const line = index + 1;
|
|
4696
|
+
const text = raw.trim();
|
|
4697
|
+
if (!text || text.startsWith("#")) continue;
|
|
4698
|
+
const [type, blob = "", ...rest] = text.split(/\s+/);
|
|
4699
|
+
const problem = !KEY_TYPE.test(type) ? "unknown key type" : !isValidKeyBlob(type, blob) ? "missing or invalid base64 key data" : void 0;
|
|
4700
|
+
if (problem) throw new ValidationError(`Line ${line}: ${problem}, expected "<type> <base64> [comment]"`, "file", ["Use one public key per line, as in ~/.ssh/authorized_keys (no options prefix)"]);
|
|
4701
|
+
const first = seen.get(blob);
|
|
4702
|
+
if (first !== void 0) throw new ValidationError(`Line ${line}: duplicate of the key on line ${first}`, "file");
|
|
4703
|
+
seen.set(blob, line);
|
|
4704
|
+
const comment = rest.join(" ");
|
|
4705
|
+
keys.push({
|
|
4706
|
+
name: managedKeyName(comment, blob),
|
|
4707
|
+
key: [
|
|
4708
|
+
type,
|
|
4709
|
+
blob,
|
|
4710
|
+
comment
|
|
4711
|
+
].filter(Boolean).join(" "),
|
|
4712
|
+
line
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
4715
|
+
return keys;
|
|
4716
|
+
}
|
|
4717
|
+
/**
|
|
4718
|
+
* Compare the file keys with the keys on a server, for one user.
|
|
4719
|
+
* Keys of other users are ignored.
|
|
4720
|
+
*/
|
|
4721
|
+
function planSync(fileKeys, serverKeys, user) {
|
|
4722
|
+
const userKeys = serverKeys.filter((k) => k.user === user);
|
|
4723
|
+
const serverNames = new Set(userKeys.map((k) => k.name));
|
|
4724
|
+
const fileNames = new Set(fileKeys.map((k) => k.name));
|
|
4725
|
+
return {
|
|
4726
|
+
add: fileKeys.filter((k) => !serverNames.has(k.name)),
|
|
4727
|
+
remove: userKeys.filter((k) => !fileNames.has(k.name)),
|
|
4728
|
+
unchanged: fileKeys.filter((k) => serverNames.has(k.name))
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
//#endregion
|
|
4732
|
+
//#region src/commands/ssh-keys/sync.ts
|
|
4733
|
+
var USAGE = "forge ssh-keys sync --file <path|-> (--server <id>... | --all-servers) [--user <name>] [--prune] [--dry-run]";
|
|
4734
|
+
function errorMessage(error) {
|
|
4735
|
+
return error instanceof Error ? error.message : String(error);
|
|
4736
|
+
}
|
|
4737
|
+
/**
|
|
4738
|
+
* Read the --server values: repeated and/or comma separated.
|
|
4739
|
+
*/
|
|
4740
|
+
function serverValues(value) {
|
|
4741
|
+
return [value].flat().filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* Read and parse the --file option. Exits on a missing, unreadable, malformed or empty file.
|
|
4745
|
+
*/
|
|
4746
|
+
function readFileKeys(ctx) {
|
|
4747
|
+
const { file } = ctx.options;
|
|
4748
|
+
const content = readContentFile(file, "file", {
|
|
4749
|
+
label: "SSH keys file",
|
|
4750
|
+
usage: USAGE,
|
|
4751
|
+
formatter: ctx.formatter
|
|
4752
|
+
});
|
|
4753
|
+
let keys;
|
|
4754
|
+
try {
|
|
4755
|
+
keys = parseAuthorizedKeys(content);
|
|
4756
|
+
} catch (error) {
|
|
4757
|
+
exitWithError(error, ctx.formatter);
|
|
4758
|
+
}
|
|
4759
|
+
if (keys.length === 0) exitWithError(new ValidationError(`No SSH keys found in ${String(file)}`, "file"), ctx.formatter);
|
|
4760
|
+
return keys;
|
|
4761
|
+
}
|
|
4762
|
+
/**
|
|
4763
|
+
* Resolve the target servers to ids and names, without duplicates.
|
|
4764
|
+
* Names are matched against one list of servers: one API call for all targets.
|
|
4765
|
+
*/
|
|
4766
|
+
async function resolveTargets(values, allServers, execCtx) {
|
|
4767
|
+
const servers = (await listServers({}, execCtx)).data;
|
|
4768
|
+
if (allServers) return servers.map((s) => ({
|
|
4769
|
+
id: s.id,
|
|
4770
|
+
name: s.name
|
|
4771
|
+
}));
|
|
4772
|
+
const targets = /* @__PURE__ */ new Map();
|
|
4773
|
+
for (const value of values) {
|
|
4774
|
+
const id = Number(matchServerId(value, servers));
|
|
4775
|
+
targets.set(id, servers.find((s) => s.id === id)?.name ?? String(id));
|
|
4776
|
+
}
|
|
4777
|
+
return [...targets].map(([id, name]) => ({
|
|
4778
|
+
id,
|
|
4779
|
+
name
|
|
4780
|
+
}));
|
|
4781
|
+
}
|
|
4782
|
+
function printPlan(syncs, user, prune) {
|
|
4783
|
+
for (const sync of syncs) {
|
|
4784
|
+
console.log(colors.bold(`${sync.name} (${sync.id})`));
|
|
4785
|
+
for (const error of sync.errors) console.log(colors.red(` ✗ ${error}`));
|
|
4786
|
+
if (!sync.plan) continue;
|
|
4787
|
+
const { add, remove, unchanged } = sync.plan;
|
|
4788
|
+
for (const key of add) console.log(colors.green(` + ${key.name}`));
|
|
4789
|
+
if (prune) for (const key of remove) console.log(colors.red(` - ${key.name}`));
|
|
4790
|
+
for (const key of unchanged) console.log(colors.dim(` = ${key.name}`));
|
|
4791
|
+
if (!prune && remove.length > 0) console.log(colors.yellow(` ${remove.length} key(s) of user "${user}" not in the file are kept (use --prune to remove them)`));
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
function printResult(syncs) {
|
|
4795
|
+
for (const sync of syncs) {
|
|
4796
|
+
const summary = `${sync.name} (${sync.id}): ${sync.added.length} added, ${sync.removed.length} removed, ${sync.unchanged.length} unchanged`;
|
|
4797
|
+
if (sync.errors.length === 0) {
|
|
4798
|
+
console.log(colors.green(`✓ ${summary}`));
|
|
4799
|
+
continue;
|
|
4800
|
+
}
|
|
4801
|
+
console.log(colors.red(`✗ ${summary}, ${sync.errors.length} failed`));
|
|
4802
|
+
for (const error of sync.errors) console.log(colors.red(` ${error}`));
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
/**
|
|
4806
|
+
* Apply a plan to one server. Each failed operation is recorded and the next one runs.
|
|
4807
|
+
* When an add fails, the removals are skipped: the user must not lose access to the server.
|
|
4808
|
+
*/
|
|
4809
|
+
async function applyPlan(sync, plan, user, prune, execCtx) {
|
|
4810
|
+
const server_id = String(sync.id);
|
|
4811
|
+
let addFailed = false;
|
|
4812
|
+
for (const key of plan.add) try {
|
|
4813
|
+
await createSshKey({
|
|
4814
|
+
server_id,
|
|
4815
|
+
name: key.name,
|
|
4816
|
+
key: key.key,
|
|
4817
|
+
user
|
|
4818
|
+
}, execCtx);
|
|
4819
|
+
sync.added.push(key.name);
|
|
4820
|
+
} catch (error) {
|
|
4821
|
+
addFailed = true;
|
|
4822
|
+
sync.errors.push(`add ${key.name}: ${errorMessage(error)}`);
|
|
4823
|
+
}
|
|
4824
|
+
if (!prune) return;
|
|
4825
|
+
for (const key of plan.remove) {
|
|
4826
|
+
if (addFailed) {
|
|
4827
|
+
sync.errors.push(`remove ${key.name}: skipped because an add failed`);
|
|
4828
|
+
continue;
|
|
4829
|
+
}
|
|
4830
|
+
try {
|
|
4831
|
+
await deleteSshKey({
|
|
4832
|
+
server_id,
|
|
4833
|
+
id: String(key.id)
|
|
4834
|
+
}, execCtx);
|
|
4835
|
+
sync.removed.push(key.name);
|
|
4836
|
+
} catch (error) {
|
|
4837
|
+
sync.errors.push(`remove ${key.name}: ${errorMessage(error)}`);
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
/**
|
|
4842
|
+
* Make the SSH keys of a user on one or more servers match an authorized_keys file.
|
|
4843
|
+
*/
|
|
4844
|
+
async function sshKeysSync(ctx) {
|
|
4845
|
+
const { options, formatter } = ctx;
|
|
4846
|
+
const servers = serverValues(options.server);
|
|
4847
|
+
const allServers = options["all-servers"] === true;
|
|
4848
|
+
const userOption = options.user;
|
|
4849
|
+
const prune = options.prune === true;
|
|
4850
|
+
const dryRun = options["dry-run"] === true;
|
|
4851
|
+
if (options.file === void 0) exitWithValidationError("file", USAGE, formatter);
|
|
4852
|
+
if (userOption !== void 0 && (typeof userOption !== "string" || userOption === "")) exitWithError(new ValidationError("--user requires a name", "user", [`Usage: ${USAGE}`]), formatter);
|
|
4853
|
+
const user = typeof userOption === "string" ? userOption : "forge";
|
|
4854
|
+
if (allServers && options.server !== void 0) exitWithError(new ValidationError("Use either --server or --all-servers, not both", "server", [`Usage: ${USAGE}`]), formatter);
|
|
4855
|
+
if (!allServers && servers.length === 0) exitWithError(new ValidationError("A target is required: --server <id> or --all-servers", "server", [`Usage: ${USAGE}`]), formatter);
|
|
4856
|
+
const fileKeys = readFileKeys(ctx);
|
|
4857
|
+
const failed = await runCommand(async () => {
|
|
4858
|
+
const execCtx = ctx.createExecutorContext(ctx.getToken());
|
|
4859
|
+
const targets = await resolveTargets(servers, allServers, execCtx);
|
|
4860
|
+
const syncs = [];
|
|
4861
|
+
for (const { id, name } of targets) {
|
|
4862
|
+
const sync = {
|
|
4863
|
+
id,
|
|
4864
|
+
name,
|
|
4865
|
+
added: [],
|
|
4866
|
+
removed: [],
|
|
4867
|
+
unchanged: [],
|
|
4868
|
+
errors: []
|
|
4869
|
+
};
|
|
4870
|
+
try {
|
|
4871
|
+
const keys = await listSshKeys({
|
|
4872
|
+
server_id: String(id),
|
|
4873
|
+
user
|
|
4874
|
+
}, execCtx);
|
|
4875
|
+
sync.plan = planSync(fileKeys, keys.data, user);
|
|
4876
|
+
sync.unchanged = sync.plan.unchanged.map((k) => k.name);
|
|
4877
|
+
} catch (error) {
|
|
4878
|
+
sync.errors.push(`list keys: ${errorMessage(error)}`);
|
|
4879
|
+
}
|
|
4880
|
+
syncs.push(sync);
|
|
4881
|
+
}
|
|
4882
|
+
const json = formatter.isJson();
|
|
4883
|
+
if (!json) printPlan(syncs, user, prune);
|
|
4884
|
+
if (dryRun) {
|
|
4885
|
+
for (const sync of syncs) {
|
|
4886
|
+
if (!sync.plan) continue;
|
|
4887
|
+
sync.added = sync.plan.add.map((k) => k.name);
|
|
4888
|
+
if (prune) sync.removed = sync.plan.remove.map((k) => k.name);
|
|
4889
|
+
}
|
|
4890
|
+
if (!json) console.log(colors.cyan("\nDry run: no changes made."));
|
|
4891
|
+
} else {
|
|
4892
|
+
for (const sync of syncs) if (sync.plan) await applyPlan(sync, sync.plan, user, prune, execCtx);
|
|
4893
|
+
if (!json) {
|
|
4894
|
+
console.log("");
|
|
4895
|
+
printResult(syncs);
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
if (json) console.log(JSON.stringify({
|
|
4899
|
+
dry_run: dryRun,
|
|
4900
|
+
servers: syncs.map(({ id, name, added, removed, unchanged, errors }) => ({
|
|
4901
|
+
id,
|
|
4902
|
+
name,
|
|
4903
|
+
added,
|
|
4904
|
+
removed,
|
|
4905
|
+
unchanged,
|
|
4906
|
+
errors
|
|
4907
|
+
}))
|
|
4908
|
+
}, null, 2));
|
|
4909
|
+
return syncs.reduce((count, sync) => count + sync.errors.length, 0);
|
|
4910
|
+
}, formatter);
|
|
4911
|
+
if (failed) {
|
|
4912
|
+
if (!formatter.isJson()) formatter.error(`${failed} SSH key operation(s) failed`);
|
|
4913
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
//#endregion
|
|
3664
4917
|
//#region src/commands/ssh-keys/command.ts
|
|
3665
4918
|
var handleSshKeysCommand = createCommandRouter({
|
|
3666
4919
|
resource: "ssh-keys",
|
|
3667
4920
|
handlers: {
|
|
3668
4921
|
list: sshKeysList,
|
|
3669
4922
|
ls: sshKeysList,
|
|
3670
|
-
get: [sshKeysGet, "args"]
|
|
3671
|
-
|
|
4923
|
+
get: [sshKeysGet, "args"],
|
|
4924
|
+
sync: sshKeysSync
|
|
4925
|
+
},
|
|
4926
|
+
writeSubcommands: ["sync"]
|
|
3672
4927
|
});
|
|
3673
4928
|
//#endregion
|
|
3674
4929
|
//#region src/commands/ssh-keys/help.ts
|
|
@@ -3684,6 +4939,41 @@ ${colors.bold("forge ssh-keys get")} - Get SSH key details
|
|
|
3684
4939
|
|
|
3685
4940
|
${colors.bold("USAGE:")}
|
|
3686
4941
|
forge ssh-keys get <key_id> --server <server_id>
|
|
4942
|
+
`);
|
|
4943
|
+
else if (subcommand === "sync") console.log(`
|
|
4944
|
+
${colors.bold("forge ssh-keys sync")} - Make the SSH keys of servers match a file
|
|
4945
|
+
|
|
4946
|
+
${colors.bold("USAGE:")}
|
|
4947
|
+
forge ssh-keys sync --file <path|-> --server <id> [--server <id>...] [options]
|
|
4948
|
+
forge ssh-keys sync --file <path|-> --all-servers [options]
|
|
4949
|
+
|
|
4950
|
+
${colors.bold("OPTIONS:")}
|
|
4951
|
+
--file <path> authorized_keys file: one "<type> <base64> [comment]" per line,
|
|
4952
|
+
blank lines and # comments ignored (use - for stdin)
|
|
4953
|
+
--server <id> Target server ID or name (repeatable or comma separated)
|
|
4954
|
+
--all-servers Target every server in the organization
|
|
4955
|
+
--user <name> Server user of the keys (default: forge)
|
|
4956
|
+
--prune Also remove the keys of the user that are not in the file
|
|
4957
|
+
--dry-run Print the plan and change nothing
|
|
4958
|
+
-f, --format <fmt> Output format: json, human
|
|
4959
|
+
|
|
4960
|
+
${colors.bold("HOW IT WORKS:")}
|
|
4961
|
+
Each key gets the name "<comment slug>-<first 8 hex chars of SHA-256 of the key>".
|
|
4962
|
+
The Forge API does not return public keys, so keys are matched by name: a changed
|
|
4963
|
+
key gets a new name. The command lists the plan per server (+ add, - remove,
|
|
4964
|
+
= unchanged), applies it, then prints the result. Without --prune, keys are only
|
|
4965
|
+
added. Servers run one after the other; when an operation fails, the others still
|
|
4966
|
+
run and the command exits non-zero. When an add fails on a server, the removals on
|
|
4967
|
+
that server are skipped, so nobody loses access.
|
|
4968
|
+
|
|
4969
|
+
${colors.bold("JSON OUTPUT:")}
|
|
4970
|
+
{ "dry_run": bool, "servers": [{ id, name, added, removed, unchanged, errors }] }
|
|
4971
|
+
With --dry-run, "added" and "removed" hold the planned changes, not applied ones.
|
|
4972
|
+
|
|
4973
|
+
${colors.bold("EXAMPLES:")}
|
|
4974
|
+
forge ssh-keys sync --file .forge/authorized_keys --server 123 --dry-run
|
|
4975
|
+
forge ssh-keys sync --file .forge/authorized_keys --all-servers --prune
|
|
4976
|
+
cat keys.pub | forge ssh-keys sync --file - --server web-1,web-2 --user deploy
|
|
3687
4977
|
`);
|
|
3688
4978
|
else console.log(`
|
|
3689
4979
|
${colors.bold("forge ssh-keys")} - Manage SSH keys
|
|
@@ -3694,9 +4984,11 @@ ${colors.bold("USAGE:")}
|
|
|
3694
4984
|
${colors.bold("SUBCOMMANDS:")}
|
|
3695
4985
|
list, ls List SSH keys
|
|
3696
4986
|
get <id> Get SSH key details
|
|
4987
|
+
sync Make the SSH keys of servers match a file
|
|
3697
4988
|
|
|
3698
4989
|
${colors.bold("OPTIONS:")}
|
|
3699
|
-
--server <id> Server ID (required)
|
|
4990
|
+
--server <id> Server ID or name (required, except for sync --all-servers)
|
|
4991
|
+
--file <path> authorized_keys file, - for stdin (for sync)
|
|
3700
4992
|
-f, --format <fmt> Output format: json, human, table
|
|
3701
4993
|
|
|
3702
4994
|
Run ${colors.cyan("forge ssh-keys <subcommand> --help")} for details.
|
|
@@ -3746,6 +5038,26 @@ Run ${colors.cyan("forge user <subcommand> --help")} for details.
|
|
|
3746
5038
|
}
|
|
3747
5039
|
//#endregion
|
|
3748
5040
|
//#region src/utils/args.ts
|
|
5041
|
+
/**
|
|
5042
|
+
* Whether an argument can be the value of the option before it.
|
|
5043
|
+
* A lone "-" is a value (it means stdin), other "-" prefixed arguments are options.
|
|
5044
|
+
*/
|
|
5045
|
+
function isOptionValue(arg) {
|
|
5046
|
+
return arg === "-" || !arg.startsWith("-");
|
|
5047
|
+
}
|
|
5048
|
+
/**
|
|
5049
|
+
* Options that accept several values. Every other option keeps the last value.
|
|
5050
|
+
*/
|
|
5051
|
+
var REPEATABLE = /* @__PURE__ */ new Set(["server"]);
|
|
5052
|
+
/**
|
|
5053
|
+
* Set an option value. A repeatable option given more than once collects its string
|
|
5054
|
+
* values into an array (`--server 1 --server 2` → `["1", "2"]`).
|
|
5055
|
+
*/
|
|
5056
|
+
function setOption(options, key, value) {
|
|
5057
|
+
const previous = options[key];
|
|
5058
|
+
if (REPEATABLE.has(key) && typeof value === "string" && previous !== void 0 && typeof previous !== "boolean") options[key] = [previous, value].flat();
|
|
5059
|
+
else options[key] = value;
|
|
5060
|
+
}
|
|
3749
5061
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
3750
5062
|
const options = {};
|
|
3751
5063
|
const positional = [];
|
|
@@ -3754,24 +5066,22 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
3754
5066
|
const arg = argv[i];
|
|
3755
5067
|
if (arg.startsWith("--")) {
|
|
3756
5068
|
const equalIndex = arg.indexOf("=");
|
|
3757
|
-
if (equalIndex > -1)
|
|
3758
|
-
|
|
3759
|
-
options[key] = arg.slice(equalIndex + 1);
|
|
3760
|
-
} else {
|
|
5069
|
+
if (equalIndex > -1) setOption(options, arg.slice(2, equalIndex), arg.slice(equalIndex + 1));
|
|
5070
|
+
else {
|
|
3761
5071
|
const key = arg.slice(2);
|
|
3762
5072
|
const nextArg = argv[i + 1];
|
|
3763
|
-
if (nextArg !== void 0 &&
|
|
3764
|
-
options
|
|
5073
|
+
if (nextArg !== void 0 && isOptionValue(nextArg)) {
|
|
5074
|
+
setOption(options, key, nextArg);
|
|
3765
5075
|
i++;
|
|
3766
|
-
} else options
|
|
5076
|
+
} else setOption(options, key, true);
|
|
3767
5077
|
}
|
|
3768
5078
|
} else if (arg.startsWith("-") && arg.length > 1) if (arg.length === 2) {
|
|
3769
5079
|
const key = arg[1];
|
|
3770
5080
|
const nextArg = argv[i + 1];
|
|
3771
|
-
if (nextArg !== void 0 &&
|
|
3772
|
-
options
|
|
5081
|
+
if (nextArg !== void 0 && isOptionValue(nextArg)) {
|
|
5082
|
+
setOption(options, key, nextArg);
|
|
3773
5083
|
i++;
|
|
3774
|
-
} else options
|
|
5084
|
+
} else setOption(options, key, true);
|
|
3775
5085
|
} else for (let j = 1; j < arg.length; j++) options[arg[j]] = true;
|
|
3776
5086
|
else if (command.length < 2) command.push(arg);
|
|
3777
5087
|
else positional.push(arg);
|
|
@@ -3782,9 +5092,17 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
3782
5092
|
positional
|
|
3783
5093
|
};
|
|
3784
5094
|
}
|
|
5095
|
+
/**
|
|
5096
|
+
* Whether the arguments ask for the CLI version: a bare `--version` or `-v`
|
|
5097
|
+
* with no command. Commands such as `php config` and `services restart` take
|
|
5098
|
+
* `--version <value>`, so a bare `--version` after a command is not a version request.
|
|
5099
|
+
*/
|
|
5100
|
+
function isVersionRequest({ command, options }) {
|
|
5101
|
+
return command.length === 0 && (options.version === true || options.v === true);
|
|
5102
|
+
}
|
|
3785
5103
|
//#endregion
|
|
3786
5104
|
//#region src/cli.ts
|
|
3787
|
-
var VERSION = "0.4.
|
|
5105
|
+
var VERSION = "0.4.7";
|
|
3788
5106
|
function showHelp() {
|
|
3789
5107
|
console.log(`
|
|
3790
5108
|
${colors.bold("forge")} v${VERSION}
|
|
@@ -3810,6 +5128,9 @@ ${colors.bold("COMMANDS:")}
|
|
|
3810
5128
|
deployments, d Manage deployments
|
|
3811
5129
|
list, ls List deployments (requires --server --site)
|
|
3812
5130
|
deploy Trigger a deployment (requires --server --site)
|
|
5131
|
+
logs [id] Show deployment output (requires --server --site)
|
|
5132
|
+
script Show the deployment script (requires --server --site)
|
|
5133
|
+
update-script Update the deployment script (requires --server --site --file|--content) [--check]
|
|
3813
5134
|
|
|
3814
5135
|
databases, db Manage databases
|
|
3815
5136
|
list, ls List databases (requires --server)
|
|
@@ -3828,11 +5149,11 @@ ${colors.bold("COMMANDS:")}
|
|
|
3828
5149
|
|
|
3829
5150
|
env Manage environment variables
|
|
3830
5151
|
get Get env variables (requires --server --site)
|
|
3831
|
-
update Update env variables (requires --server --site --content)
|
|
5152
|
+
update Update env variables (requires --server --site --file|--content) [--check]
|
|
3832
5153
|
|
|
3833
5154
|
nginx Manage nginx configuration
|
|
3834
5155
|
get Get nginx config (requires --server --site)
|
|
3835
|
-
update Update nginx config (requires --server --site --content)
|
|
5156
|
+
update Update nginx config (requires --server --site --file|--content) [--check]
|
|
3836
5157
|
|
|
3837
5158
|
certificates, certs Manage SSL certificates (per-domain in v2)
|
|
3838
5159
|
get Get certificate for a domain (requires --server --site --domain)
|
|
@@ -3849,6 +5170,7 @@ ${colors.bold("COMMANDS:")}
|
|
|
3849
5170
|
ssh-keys Manage SSH keys
|
|
3850
5171
|
list, ls List SSH keys (requires --server)
|
|
3851
5172
|
get <id> Get SSH key details (requires --server)
|
|
5173
|
+
sync Make server SSH keys match a file (requires --file, --server|--all-servers)
|
|
3852
5174
|
|
|
3853
5175
|
backups Manage backup configurations
|
|
3854
5176
|
list, ls List backup configurations (requires --server)
|
|
@@ -3867,6 +5189,24 @@ ${colors.bold("COMMANDS:")}
|
|
|
3867
5189
|
create Create a scheduled job (requires --server --command)
|
|
3868
5190
|
delete <id> Delete a scheduled job (requires --server)
|
|
3869
5191
|
|
|
5192
|
+
services List and restart server services
|
|
5193
|
+
list, ls List services on a server (forge services list <server>)
|
|
5194
|
+
restart <service> Restart a service (forge services restart <server> <service>)
|
|
5195
|
+
|
|
5196
|
+
php Manage PHP versions, configuration, limits and OPcache
|
|
5197
|
+
list, ls List installed PHP versions (requires --server)
|
|
5198
|
+
config Show a configuration file (requires --server --version --type)
|
|
5199
|
+
update-config Replace a configuration file (requires --server --version --type --file|--content) [--check]
|
|
5200
|
+
max-upload-size Show the max upload size (requires --server)
|
|
5201
|
+
set-max-upload-size <mb>
|
|
5202
|
+
Set the max upload size (requires --server)
|
|
5203
|
+
max-execution-time Show the max execution time (requires --server)
|
|
5204
|
+
set-max-execution-time <seconds>
|
|
5205
|
+
Set the max execution time (requires --server)
|
|
5206
|
+
opcache Show the OPcache status (requires --server)
|
|
5207
|
+
enable-opcache Enable OPcache (requires --server)
|
|
5208
|
+
disable-opcache Disable OPcache (requires --server)
|
|
5209
|
+
|
|
3870
5210
|
user Display authenticated user profile
|
|
3871
5211
|
get Get the authenticated user profile
|
|
3872
5212
|
|
|
@@ -3948,14 +5288,15 @@ ${colors.bold("ENVIRONMENT VARIABLES:")}
|
|
|
3948
5288
|
`);
|
|
3949
5289
|
}
|
|
3950
5290
|
async function main() {
|
|
3951
|
-
const
|
|
5291
|
+
const args = parseArgs();
|
|
5292
|
+
const { command, options, positional } = args;
|
|
3952
5293
|
const [mainCommand, subcommand] = command;
|
|
3953
5294
|
const wantsHelp = options.help !== void 0 || options.h !== void 0;
|
|
3954
5295
|
if (wantsHelp && !mainCommand) {
|
|
3955
5296
|
showHelp();
|
|
3956
5297
|
process.exit(0);
|
|
3957
5298
|
}
|
|
3958
|
-
if (
|
|
5299
|
+
if (isVersionRequest(args)) {
|
|
3959
5300
|
console.log(VERSION);
|
|
3960
5301
|
process.exit(0);
|
|
3961
5302
|
}
|
|
@@ -4083,6 +5424,20 @@ async function main() {
|
|
|
4083
5424
|
}
|
|
4084
5425
|
await handleScheduledJobsCommand(subcommand ?? "list", positional, options);
|
|
4085
5426
|
break;
|
|
5427
|
+
case "services":
|
|
5428
|
+
if (wantsHelp) {
|
|
5429
|
+
showServicesHelp(subcommand);
|
|
5430
|
+
process.exit(0);
|
|
5431
|
+
}
|
|
5432
|
+
await handleServicesCommand(subcommand ?? "list", positional, options);
|
|
5433
|
+
break;
|
|
5434
|
+
case "php":
|
|
5435
|
+
if (wantsHelp) {
|
|
5436
|
+
showPhpHelp(subcommand);
|
|
5437
|
+
process.exit(0);
|
|
5438
|
+
}
|
|
5439
|
+
await handlePhpCommand(subcommand ?? "list", positional, options);
|
|
5440
|
+
break;
|
|
4086
5441
|
case "user":
|
|
4087
5442
|
if (wantsHelp) {
|
|
4088
5443
|
showUserHelp(subcommand);
|