@studiometa/forge-cli 0.4.8 → 0.5.1

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/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { C as isOutputFormat, _ as setToken, a as runCommand, c as OutputFormatter, d as createConfigStore$1, f as deleteToken, g as setOrganizationSlug, h as getToken$1, 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 { STORAGE_PROVIDER_TYPES } from "@studiometa/forge-api";
4
+ import { PHP_CONFIG_TYPES, RESTARTABLE_SERVICES, activateCertificate, createAuditLogger, createBackupConfig, createCommand, createDatabaseUser, createMonitor, createNginxTemplate, createRedirectRule, createScheduledJob, createSecurityRule, createServer, createSshKey, createStorageProvider, deleteBackupConfig, deleteDatabaseUser, deleteMonitor, deleteNginxTemplate, deleteRedirectRule, deleteScheduledJob, deleteSecurityRule, deleteSshKey, deleteStorageProvider, 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, getStorageProvider, getUser, listBackupConfigs, listCommands, listDaemons, listDatabaseUsers, listDatabases, listDeployments, listFirewallRules, listMonitors, listNginxTemplates, listPhpVersions, listRecipes, listRedirectRules, listScheduledJobs, listSecurityRules, listServers, listServices, listSites, listSshKeys, listStorageProviders, matchByName, rebootServer, restartDaemon, restartService, runRecipe, updateBackupConfig, updateDaemon, updateDatabaseUser, updateDeploymentScript, updateEnv, updateNginxConfig, updateNginxTemplate, updatePhpConfig, updatePhpMaxExecutionTime, updatePhpMaxUploadSize, updateRecipe, updateSecurityRule, updateSite } from "@studiometa/forge-core";
4
5
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
7
  import { join } from "node:path";
@@ -122,7 +123,87 @@ async function resolveSiteId(value, serverId, execCtx) {
122
123
  return String(partial[0].id);
123
124
  }
124
125
  //#endregion
126
+ //#region src/commands/backups/options.ts
127
+ /** Backup frequencies accepted by Forge. */
128
+ var BACKUP_FREQUENCIES = [
129
+ "hourly",
130
+ "daily",
131
+ "weekly",
132
+ "custom"
133
+ ];
134
+ /**
135
+ * Read a list option: repeated and/or comma separated (`--databases 1,2 --databases app`).
136
+ */
137
+ function listValues(value) {
138
+ return [value].flat().filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
139
+ }
140
+ /**
141
+ * Whether a value is a numeric ID.
142
+ */
143
+ function isId(value) {
144
+ return /^\d+$/.test(value);
145
+ }
146
+ /**
147
+ * Check the --frequency value. Throws a ValidationError for an unknown frequency.
148
+ */
149
+ function checkFrequency(frequency) {
150
+ if (!BACKUP_FREQUENCIES.includes(frequency)) throw new ValidationError(`Invalid frequency "${frequency}": use ${BACKUP_FREQUENCIES.join(", ")}`, "frequency");
151
+ }
152
+ /**
153
+ * Parse the --retention value: an integer from 1 to 8760.
154
+ */
155
+ function parseRetention(value) {
156
+ const retention = Number(value);
157
+ if (!Number.isInteger(retention) || retention < 1 || retention > 8760) throw new ValidationError(`Invalid retention "${String(value)}": use a number of backups from 1 to 8760`, "retention");
158
+ return retention;
159
+ }
160
+ /**
161
+ * Resolve a storage provider ID or exact name to its numeric ID.
162
+ */
163
+ async function resolveStorageProviderId(value, execCtx) {
164
+ if (isId(value)) return Number(value);
165
+ const { data } = await listStorageProviders({}, execCtx);
166
+ const matches = data.filter((p) => p.name === value);
167
+ if (matches.length === 0) throw new ValidationError(`No storage provider named "${value}"`, "storage-provider", ["List the storage providers with: forge storage-providers list"]);
168
+ if (matches.length > 1) throw new ValidationError(`${matches.length} storage providers are named "${value}": use the ID (${matches.map((p) => p.id).join(", ")})`, "storage-provider");
169
+ return matches[0].id;
170
+ }
171
+ /**
172
+ * Resolve database IDs or exact names to numeric IDs. Names are looked up on the server.
173
+ */
174
+ async function resolveDatabaseIds(values, server_id, execCtx) {
175
+ if (values.every(isId)) return values.map(Number);
176
+ const { data } = await listDatabases({ server_id }, execCtx);
177
+ return values.map((value) => {
178
+ if (isId(value)) return Number(value);
179
+ const database = data.find((db) => db.name === value);
180
+ if (!database) throw new ValidationError(`No database named "${value}" on the server`, "databases", [`List the databases with: forge databases list --server ${server_id}`]);
181
+ return database.id;
182
+ });
183
+ }
184
+ /**
185
+ * Read the optional backup configuration fields that map one to one to the API.
186
+ */
187
+ function optionalBackupFields(options) {
188
+ const fields = {};
189
+ for (const [option, field] of Object.entries({
190
+ name: "name",
191
+ bucket: "bucket",
192
+ directory: "directory",
193
+ day: "day",
194
+ time: "time",
195
+ cron: "cron",
196
+ "notification-email": "notification_email"
197
+ })) {
198
+ const value = options[option];
199
+ if (typeof value === "string") fields[field] = value;
200
+ }
201
+ return fields;
202
+ }
203
+ //#endregion
125
204
  //#region src/commands/backups/handlers.ts
205
+ /** Retention when --retention is not given: Forge requires a value. */
206
+ var DEFAULT_RETENTION = 5;
126
207
  async function backupsList(ctx) {
127
208
  const server = String(ctx.options.server ?? "");
128
209
  if (!server) exitWithValidationError("server_id", "forge backups list --server <server_id>", ctx.formatter);
@@ -153,7 +234,10 @@ async function backupsGet(args, ctx) {
153
234
  ctx.formatter.outputOne(result.data, [
154
235
  "id",
155
236
  "name",
237
+ "storage_provider_id",
156
238
  "provider",
239
+ "bucket",
240
+ "directory",
157
241
  "schedule",
158
242
  "displayable_schedule",
159
243
  "retention",
@@ -162,58 +246,65 @@ async function backupsGet(args, ctx) {
162
246
  ]);
163
247
  }, ctx.formatter);
164
248
  }
249
+ var CREATE_USAGE$2 = "forge backups create --server <server> --storage-provider <id|name> --frequency <hourly|daily|weekly|custom> --databases <ids|names> [--retention <n>]";
165
250
  async function backupsCreate(ctx) {
166
251
  const server = String(ctx.options.server ?? "");
167
- const storageProviderId = Number(ctx.options.provider ?? 0);
168
- const frequency = String(ctx.options.frequency ?? "");
169
- const databaseIdsRaw = ctx.options.databases;
170
- const database_ids = Array.isArray(databaseIdsRaw) ? databaseIdsRaw.map(Number) : databaseIdsRaw ? [Number(databaseIdsRaw)] : [];
171
- if (!server) exitWithValidationError("server_id", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
172
- if (!storageProviderId) exitWithValidationError("provider", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
173
- if (!frequency) exitWithValidationError("frequency", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
252
+ const storageProvider = ctx.options["storage-provider"];
253
+ const frequency = ctx.options.frequency;
254
+ const databases = listValues(ctx.options.databases);
255
+ if (!server) exitWithValidationError("server_id", CREATE_USAGE$2, ctx.formatter);
256
+ if (typeof storageProvider !== "string" || !storageProvider) exitWithValidationError("storage-provider", CREATE_USAGE$2, ctx.formatter);
257
+ if (typeof frequency !== "string" || !frequency) exitWithValidationError("frequency", CREATE_USAGE$2, ctx.formatter);
258
+ if (databases.length === 0) exitWithValidationError("databases", CREATE_USAGE$2, ctx.formatter);
174
259
  await runCommand(async () => {
260
+ checkFrequency(frequency);
261
+ const retention = ctx.options.retention === void 0 ? DEFAULT_RETENTION : parseRetention(ctx.options.retention);
175
262
  const token = ctx.getToken();
176
263
  const execCtx = ctx.createExecutorContext(token);
264
+ const server_id = await resolveServerId(server, execCtx);
265
+ const storage_provider_id = await resolveStorageProviderId(storageProvider, execCtx);
266
+ const database_ids = await resolveDatabaseIds(databases, server_id, execCtx);
177
267
  await createBackupConfig({
178
- server_id: await resolveServerId(server, execCtx),
179
- storage_provider_id: storageProviderId,
268
+ server_id,
269
+ storage_provider_id,
180
270
  frequency,
181
- retention: 5,
182
- database_ids
271
+ retention,
272
+ database_ids,
273
+ ...ctx.options["include-new-databases"] === true ? { include_new_databases: true } : {},
274
+ ...optionalBackupFields(ctx.options)
183
275
  }, execCtx);
184
276
  ctx.formatter.success("Backup configuration created.");
185
277
  }, ctx.formatter);
186
278
  }
279
+ var UPDATE_USAGE = "forge backups update <backup_id> --server <server> --storage-provider <id|name> --frequency <hourly|daily|weekly|custom> --retention <n> --databases <ids|names>";
187
280
  async function backupsUpdate(args, ctx) {
188
281
  const [id] = args;
189
282
  const server = String(ctx.options.server ?? "");
190
- const storageProviderId = Number(ctx.options.provider ?? 0);
191
- const frequency = String(ctx.options.frequency ?? "");
192
- const retention = Number(ctx.options.retention ?? 0);
193
- const databaseIdsRaw = ctx.options.databases;
194
- const database_ids = Array.isArray(databaseIdsRaw) ? databaseIdsRaw.map(Number) : databaseIdsRaw ? [Number(databaseIdsRaw)] : [];
195
- if (!id) exitWithValidationError("backup_id", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
196
- if (!server) exitWithValidationError("server_id", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
197
- if (!storageProviderId) exitWithValidationError("provider", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
198
- if (!frequency) exitWithValidationError("frequency", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
199
- if (!retention) exitWithValidationError("retention", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
283
+ const storageProvider = ctx.options["storage-provider"];
284
+ const frequency = ctx.options.frequency;
285
+ const databases = listValues(ctx.options.databases);
286
+ if (!id) exitWithValidationError("backup_id", UPDATE_USAGE, ctx.formatter);
287
+ if (!server) exitWithValidationError("server_id", UPDATE_USAGE, ctx.formatter);
288
+ if (typeof storageProvider !== "string" || !storageProvider) exitWithValidationError("storage-provider", UPDATE_USAGE, ctx.formatter);
289
+ if (typeof frequency !== "string" || !frequency) exitWithValidationError("frequency", UPDATE_USAGE, ctx.formatter);
290
+ if (ctx.options.retention === void 0) exitWithValidationError("retention", UPDATE_USAGE, ctx.formatter);
291
+ if (databases.length === 0) exitWithValidationError("databases", UPDATE_USAGE, ctx.formatter);
200
292
  await runCommand(async () => {
293
+ checkFrequency(frequency);
294
+ const retention = parseRetention(ctx.options.retention);
201
295
  const token = ctx.getToken();
202
296
  const execCtx = ctx.createExecutorContext(token);
297
+ const server_id = await resolveServerId(server, execCtx);
298
+ const storage_provider_id = await resolveStorageProviderId(storageProvider, execCtx);
299
+ const database_ids = await resolveDatabaseIds(databases, server_id, execCtx);
203
300
  await updateBackupConfig({
204
- server_id: await resolveServerId(server, execCtx),
301
+ server_id,
205
302
  id,
206
- storage_provider_id: storageProviderId,
303
+ storage_provider_id,
207
304
  frequency,
208
305
  retention,
209
306
  database_ids,
210
- ...ctx.options.name != null ? { name: String(ctx.options.name) } : {},
211
- ...ctx.options.bucket != null ? { bucket: String(ctx.options.bucket) } : {},
212
- ...ctx.options.directory != null ? { directory: String(ctx.options.directory) } : {},
213
- ...ctx.options.day != null ? { day: String(ctx.options.day) } : {},
214
- ...ctx.options.time != null ? { time: String(ctx.options.time) } : {},
215
- ...ctx.options.cron != null ? { cron: String(ctx.options.cron) } : {},
216
- ...ctx.options["notification-email"] != null ? { notification_email: String(ctx.options["notification-email"]) } : {}
307
+ ...optionalBackupFields(ctx.options)
217
308
  }, execCtx);
218
309
  ctx.formatter.success(`Backup configuration ${id} updated.`);
219
310
  }, ctx.formatter);
@@ -271,37 +362,50 @@ ${colors.bold("USAGE:")}
271
362
  forge backups get <backup_id> --server <server_id> [options]
272
363
  `);
273
364
  else if (subcommand === "create") console.log(`
274
- ${colors.bold("forge backups create")} - Create a new backup configuration
365
+ ${colors.bold("forge backups create")} - Create a database backup configuration
275
366
 
276
367
  ${colors.bold("USAGE:")}
277
- forge backups create --server <server_id> --provider <provider> --frequency <frequency> [options]
368
+ forge backups create --server <server> --storage-provider <id|name> --frequency <frequency> --databases <ids|names> [options]
278
369
 
279
370
  ${colors.bold("OPTIONS:")}
280
- --server <id> Server ID (required)
281
- --provider <name> Backup provider (required)
282
- --frequency <freq> Backup frequency (required)
283
- --databases <ids> Comma-separated database IDs to back up (optional)
284
- -f, --format <fmt> Output format: json, human, table
371
+ --server <id|name> Server ID or name (required)
372
+ --storage-provider <id|name> Storage provider ID or exact name (required, see forge storage-providers list)
373
+ --frequency <freq> hourly, daily, weekly or custom (required)
374
+ --databases <ids|names> Database IDs or exact names, comma separated or repeated (required)
375
+ --retention <n> Number of backups to keep, 1 to 8760 (default: 5)
376
+ --time <HH:MM> Daily and weekly: time on the hour or half hour, e.g. 02:00
377
+ --day <0-6> Weekly: day of week, 0 is Sunday
378
+ --cron <expr> Custom: cron expression
379
+ --bucket <bucket> Bucket (default: the storage provider bucket)
380
+ --directory <dir> Directory in the bucket
381
+ --name <name> Configuration name
382
+ --notification-email <email> Email notified when a backup fails
383
+ --include-new-databases Also back up databases created later
384
+ -f, --format <fmt> Output format: json, human, table
385
+
386
+ ${colors.bold("EXAMPLE:")}
387
+ forge backups create --server web-1 --storage-provider Backups --frequency daily \\
388
+ --time 02:00 --retention 7 --directory web-1 --databases app,wordpress
285
389
  `);
286
390
  else if (subcommand === "update") console.log(`
287
- ${colors.bold("forge backups update")} - Update a backup configuration
391
+ ${colors.bold("forge backups update")} - Update a database backup configuration
288
392
 
289
393
  ${colors.bold("USAGE:")}
290
- forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention> [options]
394
+ forge backups update <backup_id> --server <server> --storage-provider <id|name> --frequency <frequency> --retention <n> --databases <ids|names> [options]
291
395
 
292
396
  ${colors.bold("OPTIONS:")}
293
- --server <id> Server ID (required)
294
- --provider <id> Storage provider ID (required)
295
- --frequency <freq> Backup frequency (required)
296
- --retention <n> Retention count (required)
297
- --databases <ids> Comma-separated database IDs (optional)
298
- --name <name> Backup name (optional)
299
- --bucket <bucket> Bucket name (optional)
300
- --directory <dir> Directory (optional)
301
- --day <day> Day of week (optional)
302
- --time <time> Time (optional)
303
- --cron <cron> Cron expression (optional)
304
- --notification-email <email> Notification email (optional)
397
+ --server <id|name> Server ID or name (required)
398
+ --storage-provider <id|name> Storage provider ID or exact name (required)
399
+ --frequency <freq> hourly, daily, weekly or custom (required)
400
+ --retention <n> Number of backups to keep, 1 to 8760 (required)
401
+ --databases <ids|names> Database IDs or exact names, comma separated or repeated (required)
402
+ --time <HH:MM> Daily and weekly: time on the hour or half hour
403
+ --day <0-6> Weekly: day of week, 0 is Sunday
404
+ --cron <expr> Custom: cron expression
405
+ --bucket <bucket> Bucket
406
+ --directory <dir> Directory in the bucket
407
+ --name <name> Configuration name
408
+ --notification-email <email> Email notified when a backup fails
305
409
  -f, --format <fmt> Output format: json, human, table
306
410
  `);
307
411
  else if (subcommand === "delete") console.log(`
@@ -345,11 +449,11 @@ _forge_completions() {
345
449
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
346
450
 
347
451
  # Main commands
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"
452
+ commands="config servers s sites deployments d databases db database-users daemons env nginx certificates certs firewall-rules fw ssh ssh-keys backups storage-providers commands scheduled-jobs services php user monitors nginx-templates security-rules redirect-rules recipes completion help"
349
453
 
350
454
  # Subcommands for each command
351
455
  local config_cmds="set get delete"
352
- local servers_cmds="list ls get reboot"
456
+ local servers_cmds="list ls get create reboot"
353
457
  local sites_cmds="list ls get"
354
458
  local deployments_cmds="list ls deploy logs log script update-script"
355
459
  local databases_cmds="list ls get"
@@ -360,7 +464,8 @@ _forge_completions() {
360
464
  local certificates_cmds="get activate"
361
465
  local firewall_rules_cmds="list ls get"
362
466
  local ssh_keys_cmds="list ls get sync"
363
- local backups_cmds="list ls get create delete"
467
+ local backups_cmds="list ls get create update delete"
468
+ local storage_providers_cmds="list ls get create delete"
364
469
  local commands_cmds="list ls get create"
365
470
  local scheduled_jobs_cmds="list ls get create delete"
366
471
  local services_cmds="list ls restart"
@@ -374,7 +479,7 @@ _forge_completions() {
374
479
  local completion_cmds="bash zsh fish"
375
480
 
376
481
  # Global options
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"
482
+ options="--token --server --site -f --format --no-color -h --help -v --version --name --command --content --from --to --provider --frequency --cron --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 --ubuntu-version --php-version --database --private-ip --ssh-port --tag --storage-provider --databases --retention --time --day --bucket --notification-email --include-new-databases --endpoint --access-key --secret-key-file --assume-role"
378
483
 
379
484
  # Format options
380
485
  local formats="json human table"
@@ -427,6 +532,9 @@ _forge_completions() {
427
532
  backups)
428
533
  COMPREPLY=( $(compgen -W "\${backups_cmds}" -- "\${cur}") )
429
534
  ;;
535
+ storage-providers)
536
+ COMPREPLY=( $(compgen -W "\${storage_providers_cmds}" -- "\${cur}") )
537
+ ;;
430
538
  commands)
431
539
  COMPREPLY=( $(compgen -W "\${commands_cmds}" -- "\${cur}") )
432
540
  ;;
@@ -475,7 +583,7 @@ _forge_completions() {
475
583
  # Complete file paths
476
584
  COMPREPLY=( $(compgen -f -- "\${cur}") )
477
585
  ;;
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)
586
+ --token|--server|--site|--version|--name|--command|--content|--from|--to|--provider|--frequency|--cron|--type|--operator|--threshold|--minutes|--key|--port|--ip-address|--user|--script|--servers|--domain|--password|--credential-id|--region|--size|--project-type|--directory|--ubuntu-version|--php-version|--database|--private-ip|--ssh-port|--tag|--storage-provider|--databases|--retention|--time|--day|--bucket|--notification-email|--endpoint|--access-key)
479
587
  # No completion for values requiring arguments
480
588
  COMPREPLY=()
481
589
  ;;
@@ -523,7 +631,8 @@ _forge() {
523
631
  'fw:Manage firewall rules (alias)'
524
632
  'ssh:Open an SSH session to a server'
525
633
  'ssh-keys:Manage SSH keys'
526
- 'backups:Manage backup configurations'
634
+ 'backups:Manage database backup configurations'
635
+ 'storage-providers:Manage the storage providers of database backups'
527
636
  'commands:Manage site commands'
528
637
  'scheduled-jobs:Manage scheduled jobs'
529
638
  'services:List and restart server services'
@@ -556,6 +665,7 @@ _forge() {
556
665
  'list:List all servers'
557
666
  'ls:List all servers (alias)'
558
667
  'get:Get server details'
668
+ 'create:Create a server'
559
669
  'reboot:Reboot a server'
560
670
  )
561
671
  _describe 'servers command' servers_cmds
@@ -664,10 +774,22 @@ _forge() {
664
774
  'ls:List backup configurations (alias)'
665
775
  'get:Get backup configuration details'
666
776
  'create:Create a backup configuration'
777
+ 'update:Update a backup configuration'
667
778
  'delete:Delete a backup configuration'
668
779
  )
669
780
  _describe 'backups command' backups_cmds
670
781
  ;;
782
+ storage-providers)
783
+ local -a storage_providers_cmds
784
+ storage_providers_cmds=(
785
+ 'list:List storage providers'
786
+ 'ls:List storage providers (alias)'
787
+ 'get:Get storage provider details'
788
+ 'create:Create a storage provider'
789
+ 'delete:Delete a storage provider'
790
+ )
791
+ _describe 'storage-providers command' storage_providers_cmds
792
+ ;;
671
793
  commands)
672
794
  local -a commands_cmds
673
795
  commands_cmds=(
@@ -805,6 +927,7 @@ _forge() {
805
927
  '--to[To]:to:' \\
806
928
  '--provider[Provider]:provider:' \\
807
929
  '--frequency[Frequency]:frequency:' \\
930
+ '--cron[Cron expression]:cron:' \\
808
931
  '--type[Type]:type:' \\
809
932
  '--operator[Operator]:operator:' \\
810
933
  '--threshold[Threshold]:threshold:' \\
@@ -822,12 +945,31 @@ _forge() {
822
945
  '--size[Size]:size:' \\
823
946
  '--project-type[Project type]:project type:' \\
824
947
  '--directory[Directory]:directory:' \\
948
+ '--ubuntu-version[Ubuntu version]:ubuntu version:' \\
949
+ '--php-version[PHP version]:php version:' \\
950
+ '--database[Database type]:database type:' \\
951
+ '--private-ip[Private IP address]:private ip address:' \\
952
+ '--ssh-port[SSH port]:ssh port:' \\
953
+ '--tag[Tag]:tag:' \\
825
954
  '--file[File path (- for stdin)]:file:_files' \\
826
955
  '--script-file[Deployment script file]:file:_files' \\
827
956
  '--all-servers[Target every server]' \\
828
957
  '--prune[Remove SSH keys that are not in the file]' \\
829
958
  '--dry-run[Print the plan and change nothing]' \\
830
- '--check[Compare with Forge, do not write]'
959
+ '--check[Compare with Forge, do not write]' \\
960
+ '--storage-provider[Storage provider ID or name]:storage provider:' \\
961
+ '--databases[Database IDs or names]:databases:' \\
962
+ '--retention[Number of backups to keep]:retention:' \\
963
+ '--time[Backup time (HH:MM)]:time:' \\
964
+ '--day[Backup day of week (0-6)]:day:' \\
965
+ '--cron[Cron expression]:cron:' \\
966
+ '--bucket[Bucket]:bucket:' \\
967
+ '--notification-email[Notification email]:email:' \\
968
+ '--include-new-databases[Also back up new databases]' \\
969
+ '--endpoint[S3 endpoint URL]:endpoint:' \\
970
+ '--access-key[Access key ID]:access key:' \\
971
+ '--secret-key-file[Secret key file (- for stdin)]:file:_files' \\
972
+ '--assume-role[Use the EC2 instance role]'
831
973
  ;;
832
974
  esac
833
975
  }
@@ -855,7 +997,8 @@ complete -c forge -f -n "__fish_use_subcommand" -a "firewall-rules" -d "Manage f
855
997
  complete -c forge -f -n "__fish_use_subcommand" -a "fw" -d "Manage firewall rules (alias)"
856
998
  complete -c forge -f -n "__fish_use_subcommand" -a "ssh" -d "Open an SSH session to a server"
857
999
  complete -c forge -f -n "__fish_use_subcommand" -a "ssh-keys" -d "Manage SSH keys"
858
- complete -c forge -f -n "__fish_use_subcommand" -a "backups" -d "Manage backup configurations"
1000
+ complete -c forge -f -n "__fish_use_subcommand" -a "backups" -d "Manage database backup configurations"
1001
+ complete -c forge -f -n "__fish_use_subcommand" -a "storage-providers" -d "Manage the storage providers of database backups"
859
1002
  complete -c forge -f -n "__fish_use_subcommand" -a "commands" -d "Manage site commands"
860
1003
  complete -c forge -f -n "__fish_use_subcommand" -a "scheduled-jobs" -d "Manage scheduled jobs"
861
1004
  complete -c forge -f -n "__fish_use_subcommand" -a "services" -d "List and restart server services"
@@ -878,6 +1021,7 @@ complete -c forge -f -n "__fish_seen_subcommand_from config" -a "delete" -d "Del
878
1021
  complete -c forge -f -n "__fish_seen_subcommand_from servers s" -a "list" -d "List all servers"
879
1022
  complete -c forge -f -n "__fish_seen_subcommand_from servers s" -a "ls" -d "List all servers (alias)"
880
1023
  complete -c forge -f -n "__fish_seen_subcommand_from servers s" -a "get" -d "Get server details"
1024
+ complete -c forge -f -n "__fish_seen_subcommand_from servers s" -a "create" -d "Create a server"
881
1025
  complete -c forge -f -n "__fish_seen_subcommand_from servers s" -a "reboot" -d "Reboot a server"
882
1026
 
883
1027
  # sites subcommands
@@ -943,8 +1087,16 @@ complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "list" -d "List
943
1087
  complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "ls" -d "List backup configurations (alias)"
944
1088
  complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "get" -d "Get backup configuration details"
945
1089
  complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "create" -d "Create a backup configuration"
1090
+ complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "update" -d "Update a backup configuration"
946
1091
  complete -c forge -f -n "__fish_seen_subcommand_from backups" -a "delete" -d "Delete a backup configuration"
947
1092
 
1093
+ # storage-providers subcommands
1094
+ complete -c forge -f -n "__fish_seen_subcommand_from storage-providers" -a "list" -d "List storage providers"
1095
+ complete -c forge -f -n "__fish_seen_subcommand_from storage-providers" -a "ls" -d "List storage providers (alias)"
1096
+ complete -c forge -f -n "__fish_seen_subcommand_from storage-providers" -a "get" -d "Get storage provider details"
1097
+ complete -c forge -f -n "__fish_seen_subcommand_from storage-providers" -a "create" -d "Create a storage provider"
1098
+ complete -c forge -f -n "__fish_seen_subcommand_from storage-providers" -a "delete" -d "Delete a storage provider"
1099
+
948
1100
  # commands subcommands
949
1101
  complete -c forge -f -n "__fish_seen_subcommand_from commands" -a "list" -d "List commands"
950
1102
  complete -c forge -f -n "__fish_seen_subcommand_from commands" -a "ls" -d "List commands (alias)"
@@ -1034,6 +1186,7 @@ complete -c forge -l from -d "From" -r
1034
1186
  complete -c forge -l to -d "To" -r
1035
1187
  complete -c forge -l provider -d "Provider" -r
1036
1188
  complete -c forge -l frequency -d "Frequency" -r
1189
+ complete -c forge -l cron -d "Cron expression" -r
1037
1190
  complete -c forge -l type -d "Type" -r
1038
1191
  complete -c forge -l operator -d "Operator" -r
1039
1192
  complete -c forge -l threshold -d "Threshold" -r
@@ -1055,8 +1208,27 @@ complete -c forge -l file -d "File path (- for stdin)" -r -F
1055
1208
  complete -c forge -l script-file -d "Deployment script file" -r -F
1056
1209
  complete -c forge -l all-servers -d "Target every server"
1057
1210
  complete -c forge -l prune -d "Remove SSH keys that are not in the file"
1211
+ complete -c forge -l ubuntu-version -d "Ubuntu version" -r
1212
+ complete -c forge -l php-version -d "PHP version" -r
1213
+ complete -c forge -l database -d "Database type" -r
1214
+ complete -c forge -l private-ip -d "Private IP address" -r
1215
+ complete -c forge -l ssh-port -d "SSH port" -r
1216
+ complete -c forge -l tag -d "Tag" -r
1058
1217
  complete -c forge -l dry-run -d "Print the plan and change nothing"
1059
1218
  complete -c forge -l check -d "Compare with Forge, do not write"
1219
+ complete -c forge -l storage-provider -d "Storage provider ID or name" -r
1220
+ complete -c forge -l databases -d "Database IDs or names" -r
1221
+ complete -c forge -l retention -d "Number of backups to keep" -r
1222
+ complete -c forge -l time -d "Backup time (HH:MM)" -r
1223
+ complete -c forge -l day -d "Backup day of week (0-6)" -r
1224
+ complete -c forge -l cron -d "Cron expression" -r
1225
+ complete -c forge -l bucket -d "Bucket" -r
1226
+ complete -c forge -l notification-email -d "Notification email" -r
1227
+ complete -c forge -l include-new-databases -d "Also back up new databases"
1228
+ complete -c forge -l endpoint -d "S3 endpoint URL" -r
1229
+ complete -c forge -l access-key -d "Access key ID" -r
1230
+ complete -c forge -l secret-key-file -d "Secret key file (- for stdin)" -r -F
1231
+ complete -c forge -l assume-role -d "Use the EC2 instance role"
1060
1232
  `;
1061
1233
  /**
1062
1234
  * Get the appropriate completion file path for the shell
@@ -1434,7 +1606,7 @@ function handleConfigCommand(subcommand, args, options) {
1434
1606
  const rawFormat = options.format ?? options.f ?? "human";
1435
1607
  const format = isOutputFormat(rawFormat) ? rawFormat : "human";
1436
1608
  const formatter = new OutputFormatter(format, options["no-color"] === true);
1437
- const store = createConfigStore();
1609
+ const store = createConfigStore$1();
1438
1610
  switch (subcommand) {
1439
1611
  case "set": {
1440
1612
  const [key, value] = args;
@@ -1461,7 +1633,7 @@ function handleConfigCommand(subcommand, args, options) {
1461
1633
  break;
1462
1634
  }
1463
1635
  case "get": {
1464
- const token = getToken(store);
1636
+ const token = getToken$1(store);
1465
1637
  const orgSlug = getOrganizationSlug(store);
1466
1638
  if (format === "json") formatter.output({
1467
1639
  apiToken: token ? maskToken(token) : null,
@@ -3832,7 +4004,16 @@ ${colors.bold("USAGE:")}
3832
4004
  ${colors.bold("OPTIONS:")}
3833
4005
  --server <id> Server ID (required)
3834
4006
  --command <cmd> Command to run (required)
4007
+ --site <id> Site ID, to create the job on a site
4008
+ --user <user> User that runs the command (default: forge)
4009
+ --frequency <freq> minutely, hourly, nightly, weekly, monthly, reboot
4010
+ or custom (default: custom)
4011
+ --cron <expr> Cron expression, used when --frequency is custom
3835
4012
  -f, --format <fmt> Output format: json, human, table
4013
+
4014
+ ${colors.bold("EXAMPLES:")}
4015
+ forge scheduled-jobs create --server web-1 --command "php artisan schedule:run" --frequency minutely
4016
+ forge scheduled-jobs create --server web-1 --command "php artisan backup:run" --cron "0 3 * * *"
3836
4017
  `);
3837
4018
  else if (subcommand === "delete") console.log(`
3838
4019
  ${colors.bold("forge scheduled-jobs delete")} - Delete a scheduled job
@@ -4068,10 +4249,6 @@ Run ${colors.cyan("forge security-rules <subcommand> --help")} for details.
4068
4249
  //#endregion
4069
4250
  //#region src/commands/servers/handlers.ts
4070
4251
  /**
4071
- * CLI handlers for servers commands.
4072
- * Delegates all HTTP logic to forge-core executors.
4073
- */
4074
- /**
4075
4252
  * List all servers.
4076
4253
  */
4077
4254
  async function serversList(ctx) {
@@ -4102,6 +4279,57 @@ async function serversGet(args, ctx) {
4102
4279
  ctx.formatter.output(result.data);
4103
4280
  }, ctx.formatter);
4104
4281
  }
4282
+ var CREATE_USAGE$1 = "forge servers create --name <name> --ip-address <ip> [options]";
4283
+ /**
4284
+ * Create a server. Defaults to a Custom VPS app server on Ubuntu 26.04.
4285
+ */
4286
+ async function serversCreate(ctx) {
4287
+ const name = String(ctx.options.name ?? "");
4288
+ const provider = String(ctx.options.provider ?? "custom");
4289
+ const ip_address = String(ctx.options["ip-address"] ?? "");
4290
+ if (!name) exitWithValidationError("name", CREATE_USAGE$1, ctx.formatter);
4291
+ if (provider === "custom" && !ip_address) exitWithValidationError("ip-address", CREATE_USAGE$1, ctx.formatter);
4292
+ if (provider !== "custom") {
4293
+ for (const option of [
4294
+ "credential-id",
4295
+ "region",
4296
+ "size"
4297
+ ]) if (!ctx.options[option]) exitWithValidationError(option, CREATE_USAGE$1, ctx.formatter);
4298
+ }
4299
+ const options = {
4300
+ name,
4301
+ provider,
4302
+ type: String(ctx.options.type ?? "app"),
4303
+ ubuntu_version: String(ctx.options["ubuntu-version"] ?? "26.04"),
4304
+ php_version: String(ctx.options["php-version"] ?? "php84")
4305
+ };
4306
+ if (ctx.options.database) options.database_type = String(ctx.options.database);
4307
+ if (ctx.options["credential-id"]) options.credential_id = String(ctx.options["credential-id"]);
4308
+ const tags = ctx.options.tag;
4309
+ if (tags) options.tags = (Array.isArray(tags) ? tags : [tags]).map(String);
4310
+ if (provider === "custom") {
4311
+ options.custom = {
4312
+ ip_address,
4313
+ ssh_port: String(ctx.options["ssh-port"] ?? "22")
4314
+ };
4315
+ if (ctx.options["private-ip"]) options.custom.private_ip_address = String(ctx.options["private-ip"]);
4316
+ } else options[provider] = {
4317
+ region_id: String(ctx.options.region),
4318
+ size_id: String(ctx.options.size)
4319
+ };
4320
+ await runCommand(async () => {
4321
+ const token = ctx.getToken();
4322
+ const execCtx = ctx.createExecutorContext(token);
4323
+ const result = await createServer(options, execCtx);
4324
+ const format = String(ctx.options.format ?? ctx.options.f ?? "human");
4325
+ if (format === "table" || format === "json") {
4326
+ ctx.formatter.output(result.data);
4327
+ return;
4328
+ }
4329
+ ctx.formatter.success(`Server ${result.data.name} created (ID ${result.data.id}).`);
4330
+ if (provider === "custom") ctx.formatter.info(`Run the provisioning command from the Forge server page as root on ${ip_address}.`);
4331
+ }, ctx.formatter);
4332
+ }
4105
4333
  /**
4106
4334
  * Reboot a server by ID or name.
4107
4335
  */
@@ -4124,9 +4352,10 @@ var handleServersCommand = createCommandRouter({
4124
4352
  list: serversList,
4125
4353
  ls: serversList,
4126
4354
  get: [serversGet, "args"],
4355
+ create: serversCreate,
4127
4356
  reboot: [serversReboot, "args"]
4128
4357
  },
4129
- writeSubcommands: ["reboot"]
4358
+ writeSubcommands: ["create", "reboot"]
4130
4359
  });
4131
4360
  //#endregion
4132
4361
  //#region src/commands/servers/help.ts
@@ -4162,6 +4391,35 @@ ${colors.bold("OPTIONS:")}
4162
4391
  ${colors.bold("EXAMPLES:")}
4163
4392
  forge servers get 123
4164
4393
  forge servers get 123 --format json
4394
+ `);
4395
+ else if (subcommand === "create") console.log(`
4396
+ ${colors.bold("forge servers create")} - Create a server
4397
+
4398
+ ${colors.bold("USAGE:")}
4399
+ forge servers create --name <name> --ip-address <ip> [options]
4400
+
4401
+ ${colors.bold("OPTIONS:")}
4402
+ --name <name> Server name (required, 30 characters max)
4403
+ --provider <provider> Server provider (default: custom)
4404
+ --ip-address <ip> Public IP address (required for custom)
4405
+ --private-ip <ip> Private IP address (custom only)
4406
+ --ssh-port <port> SSH port (custom only, default: 22)
4407
+ --region <id> Region ID (cloud providers)
4408
+ --size <id> Size ID (cloud providers)
4409
+ --credential-id <id> Server provider credential ID (cloud providers)
4410
+ --type <type> Server type: app, web, loadbalancer, database... (default: app)
4411
+ --ubuntu-version <ver> Ubuntu version (default: 26.04)
4412
+ --php-version <ver> PHP version (default: php84)
4413
+ --database <type> Database type, e.g. mariadb1011
4414
+ --tag <tag> Tag, repeatable
4415
+ -f, --format <fmt> Output format: json, human, table
4416
+
4417
+ A Custom VPS must run a fresh Ubuntu with root SSH access. After the
4418
+ command, run the provisioning command from the Forge server page as root.
4419
+
4420
+ ${colors.bold("EXAMPLES:")}
4421
+ forge servers create --name web-1 --ip-address 203.0.113.10 --php-version php84 --database mariadb1011
4422
+ forge servers create --name worker-1 --provider hetzner --credential-id 42 --region fsn1 --size cpx31
4165
4423
  `);
4166
4424
  else if (subcommand === "reboot") console.log(`
4167
4425
  ${colors.bold("forge servers reboot")} - Reboot a server
@@ -4187,6 +4445,7 @@ ${colors.bold("ALIASES:")}
4187
4445
  ${colors.bold("SUBCOMMANDS:")}
4188
4446
  list, ls List all servers
4189
4447
  get <id> Get server details
4448
+ create Create a server (requires --name, and --ip-address for custom)
4190
4449
  reboot <id> Reboot a server
4191
4450
 
4192
4451
  ${colors.bold("OPTIONS:")}
@@ -5007,6 +5266,183 @@ Run ${colors.cyan("forge ssh-keys <subcommand> --help")} for details.
5007
5266
  `);
5008
5267
  }
5009
5268
  //#endregion
5269
+ //#region src/commands/storage-providers/handlers.ts
5270
+ var FIELDS = [
5271
+ "id",
5272
+ "name",
5273
+ "provider",
5274
+ "provider_name",
5275
+ "endpoint",
5276
+ "region",
5277
+ "bucket",
5278
+ "directory",
5279
+ "in_use"
5280
+ ];
5281
+ var CREATE_USAGE = "forge storage-providers create --name <name> --provider <type> [--endpoint <url>] [--region <region>] [--bucket <bucket>] [--access-key <id>] [--secret-key-file <path|->]";
5282
+ async function storageProvidersList(ctx) {
5283
+ const provider = typeof ctx.options.provider === "string" ? ctx.options.provider : void 0;
5284
+ await runCommand(async () => {
5285
+ const token = ctx.getToken();
5286
+ const execCtx = ctx.createExecutorContext(token);
5287
+ const result = await listStorageProviders(provider ? { provider } : {}, execCtx);
5288
+ ctx.formatter.outputList(result.data, [
5289
+ "id",
5290
+ "name",
5291
+ "provider",
5292
+ "bucket",
5293
+ "in_use"
5294
+ ], "No storage providers found.", (p) => `${String(p.id).padEnd(8)} ${p.name.padEnd(24)} ${p.provider.padEnd(10)} ${p.bucket ?? "—"}${p.in_use ? " (in use)" : ""}`);
5295
+ }, ctx.formatter);
5296
+ }
5297
+ async function storageProvidersGet(args, ctx) {
5298
+ const [id] = args;
5299
+ if (!id) exitWithValidationError("id", "forge storage-providers get <id>", ctx.formatter);
5300
+ await runCommand(async () => {
5301
+ const token = ctx.getToken();
5302
+ const execCtx = ctx.createExecutorContext(token);
5303
+ const result = await getStorageProvider({ id }, execCtx);
5304
+ ctx.formatter.outputOne(result.data, FIELDS);
5305
+ }, ctx.formatter);
5306
+ }
5307
+ /**
5308
+ * Read an optional string option. A flag given without a value is a validation error.
5309
+ */
5310
+ function stringOption(ctx, name) {
5311
+ const value = ctx.options[name];
5312
+ if (value === void 0) return void 0;
5313
+ if (typeof value !== "string" || !value) exitWithError(new ValidationError(`--${name} requires a value`, name, [`Usage: ${CREATE_USAGE}`]), ctx.formatter);
5314
+ return value;
5315
+ }
5316
+ async function storageProvidersCreate(ctx) {
5317
+ const name = ctx.options.name;
5318
+ const provider = ctx.options.provider;
5319
+ if (typeof name !== "string" || !name) exitWithValidationError("name", CREATE_USAGE, ctx.formatter);
5320
+ if (typeof provider !== "string" || !provider) exitWithValidationError("provider", CREATE_USAGE, ctx.formatter);
5321
+ if (!STORAGE_PROVIDER_TYPES.includes(provider)) exitWithError(new ValidationError(`Invalid provider "${provider}": use ${STORAGE_PROVIDER_TYPES.join(", ")}`, "provider"), ctx.formatter);
5322
+ const endpoint = stringOption(ctx, "endpoint");
5323
+ const region = stringOption(ctx, "region");
5324
+ const bucket = stringOption(ctx, "bucket");
5325
+ const directory = stringOption(ctx, "directory");
5326
+ const accessKey = stringOption(ctx, "access-key");
5327
+ const secretKey = ctx.options["secret-key-file"] === void 0 ? void 0 : readContentFile(ctx.options["secret-key-file"], "secret-key-file", {
5328
+ label: "secret key",
5329
+ usage: CREATE_USAGE,
5330
+ formatter: ctx.formatter
5331
+ }).replace(/\r?\n$/, "");
5332
+ await runCommand(async () => {
5333
+ const token = ctx.getToken();
5334
+ const execCtx = ctx.createExecutorContext(token);
5335
+ const result = await createStorageProvider({
5336
+ name,
5337
+ provider,
5338
+ ...endpoint !== void 0 ? { endpoint } : {},
5339
+ ...region !== void 0 ? { region } : {},
5340
+ ...bucket !== void 0 ? { bucket } : {},
5341
+ ...directory !== void 0 ? { directory } : {},
5342
+ ...accessKey !== void 0 ? { access_key: accessKey } : {},
5343
+ ...secretKey !== void 0 ? { secret_key: secretKey } : {},
5344
+ ...ctx.options["assume-role"] === true ? { assume_role: true } : {}
5345
+ }, execCtx);
5346
+ ctx.formatter.outputOne(result.data, FIELDS);
5347
+ }, ctx.formatter);
5348
+ }
5349
+ async function storageProvidersDelete(args, ctx) {
5350
+ const [id] = args;
5351
+ if (!id) exitWithValidationError("id", "forge storage-providers delete <id>", ctx.formatter);
5352
+ await runCommand(async () => {
5353
+ const token = ctx.getToken();
5354
+ const execCtx = ctx.createExecutorContext(token);
5355
+ await deleteStorageProvider({ id }, execCtx);
5356
+ ctx.formatter.success(`Storage provider ${id} deleted.`);
5357
+ }, ctx.formatter);
5358
+ }
5359
+ //#endregion
5360
+ //#region src/commands/storage-providers/command.ts
5361
+ var handleStorageProvidersCommand = createCommandRouter({
5362
+ resource: "storage-providers",
5363
+ handlers: {
5364
+ list: storageProvidersList,
5365
+ ls: storageProvidersList,
5366
+ get: [storageProvidersGet, "args"],
5367
+ create: storageProvidersCreate,
5368
+ delete: [storageProvidersDelete, "args"]
5369
+ },
5370
+ writeSubcommands: ["create", "delete"]
5371
+ });
5372
+ //#endregion
5373
+ //#region src/commands/storage-providers/help.ts
5374
+ function showStorageProvidersHelp(subcommand) {
5375
+ if (subcommand === "list" || subcommand === "ls") console.log(`
5376
+ ${colors.bold("forge storage-providers list")} - List the organization's storage providers
5377
+
5378
+ ${colors.bold("USAGE:")}
5379
+ forge storage-providers list [options]
5380
+
5381
+ ${colors.bold("OPTIONS:")}
5382
+ --provider <type> Only list one provider type (s3, spaces, hetzner, ovh, scaleway, custom)
5383
+ -f, --format <fmt> Output format: json, human, table
5384
+ `);
5385
+ else if (subcommand === "get") console.log(`
5386
+ ${colors.bold("forge storage-providers get")} - Get storage provider details
5387
+
5388
+ ${colors.bold("USAGE:")}
5389
+ forge storage-providers get <id> [options]
5390
+
5391
+ Forge never returns the access key or the secret key.
5392
+ `);
5393
+ else if (subcommand === "create") console.log(`
5394
+ ${colors.bold("forge storage-providers create")} - Create a storage provider for database backups
5395
+
5396
+ ${colors.bold("USAGE:")}
5397
+ forge storage-providers create --name <name> --provider <type> [options]
5398
+
5399
+ ${colors.bold("OPTIONS:")}
5400
+ --name <name> Provider name (required)
5401
+ --provider <type> s3, spaces, hetzner, ovh, scaleway or custom (S3 compatible) (required)
5402
+ --endpoint <url> S3 endpoint URL (custom providers)
5403
+ --region <region> Bucket region
5404
+ --bucket <bucket> Default bucket
5405
+ --directory <dir> Default directory in the bucket
5406
+ --access-key <id> Access key ID
5407
+ --secret-key-file <path> Read the secret key from a file, or from stdin with -
5408
+ --assume-role Use the EC2 instance role instead of keys (s3)
5409
+ -f, --format <fmt> Output format: json, human, table
5410
+
5411
+ The secret key is not accepted as an option value, so it does not show in the
5412
+ shell history or the process list. One trailing newline is removed.
5413
+
5414
+ ${colors.bold("EXAMPLE:")}
5415
+ printf '%s' "$S3_SECRET_KEY" | forge storage-providers create --name Backups \\
5416
+ --provider custom --endpoint https://s3.example.com --region eu-west-1 \\
5417
+ --bucket my-bucket --access-key "$S3_ACCESS_KEY" --secret-key-file - --format json
5418
+ `);
5419
+ else if (subcommand === "delete") console.log(`
5420
+ ${colors.bold("forge storage-providers delete")} - Delete a storage provider
5421
+
5422
+ ${colors.bold("USAGE:")}
5423
+ forge storage-providers delete <id> [options]
5424
+
5425
+ Forge refuses to delete a provider that a backup configuration uses.
5426
+ `);
5427
+ else console.log(`
5428
+ ${colors.bold("forge storage-providers")} - Manage the storage providers of database backups
5429
+
5430
+ ${colors.bold("USAGE:")}
5431
+ forge storage-providers <subcommand> [options]
5432
+
5433
+ ${colors.bold("SUBCOMMANDS:")}
5434
+ list, ls List storage providers
5435
+ get <id> Get storage provider details
5436
+ create Create a storage provider
5437
+ delete <id> Delete a storage provider
5438
+
5439
+ ${colors.bold("OPTIONS:")}
5440
+ -f, --format <fmt> Output format: json, human, table
5441
+
5442
+ Run ${colors.cyan("forge storage-providers <subcommand> --help")} for details.
5443
+ `);
5444
+ }
5445
+ //#endregion
5010
5446
  //#region src/commands/user/handlers.ts
5011
5447
  async function userGet(ctx) {
5012
5448
  await runCommand(async () => {
@@ -5060,7 +5496,11 @@ function isOptionValue(arg) {
5060
5496
  /**
5061
5497
  * Options that accept several values. Every other option keeps the last value.
5062
5498
  */
5063
- var REPEATABLE = /* @__PURE__ */ new Set(["server"]);
5499
+ var REPEATABLE = /* @__PURE__ */ new Set([
5500
+ "server",
5501
+ "tag",
5502
+ "databases"
5503
+ ]);
5064
5504
  /**
5065
5505
  * Set an option value. A repeatable option given more than once collects its string
5066
5506
  * values into an array (`--server 1 --server 2` → `["1", "2"]`).
@@ -5114,7 +5554,7 @@ function isVersionRequest({ command, options }) {
5114
5554
  }
5115
5555
  //#endregion
5116
5556
  //#region src/cli.ts
5117
- var VERSION = "0.4.8";
5557
+ var VERSION = "0.5.1";
5118
5558
  function showHelp() {
5119
5559
  console.log(`
5120
5560
  ${colors.bold("forge")} v${VERSION}
@@ -5131,6 +5571,7 @@ ${colors.bold("COMMANDS:")}
5131
5571
  servers, s Manage servers
5132
5572
  list, ls List all servers
5133
5573
  get <id> Get server details
5574
+ create Create a server (requires --name, and --ip-address for custom)
5134
5575
  reboot <id> Reboot a server
5135
5576
 
5136
5577
  sites Manage sites
@@ -5184,12 +5625,19 @@ ${colors.bold("COMMANDS:")}
5184
5625
  get <id> Get SSH key details (requires --server)
5185
5626
  sync Make server SSH keys match a file (requires --file, --server|--all-servers)
5186
5627
 
5187
- backups Manage backup configurations
5628
+ backups Manage database backup configurations
5188
5629
  list, ls List backup configurations (requires --server)
5189
5630
  get <id> Get backup configuration details (requires --server)
5190
- create Create a backup configuration (requires --server --provider --frequency)
5631
+ create Create a backup configuration (requires --server --storage-provider --frequency --databases)
5632
+ update <id> Update a backup configuration (requires --server --storage-provider --frequency --retention --databases)
5191
5633
  delete <id> Delete a backup configuration (requires --server)
5192
5634
 
5635
+ storage-providers Manage the storage providers of database backups
5636
+ list, ls List storage providers
5637
+ get <id> Get storage provider details
5638
+ create Create a storage provider (requires --name --provider)
5639
+ delete <id> Delete a storage provider
5640
+
5193
5641
  commands Manage site commands
5194
5642
  list, ls List commands (requires --server --site)
5195
5643
  get <id> Get command details (requires --server --site)
@@ -5422,6 +5870,13 @@ async function main() {
5422
5870
  }
5423
5871
  await handleBackupsCommand(subcommand ?? "list", positional, options);
5424
5872
  break;
5873
+ case "storage-providers":
5874
+ if (wantsHelp) {
5875
+ showStorageProvidersHelp(subcommand);
5876
+ process.exit(0);
5877
+ }
5878
+ await handleStorageProvidersCommand(subcommand ?? "list", positional, options);
5879
+ break;
5425
5880
  case "commands":
5426
5881
  if (wantsHelp) {
5427
5882
  showCommandsHelp(subcommand);