@studiometa/forge-cli 0.3.0 → 0.4.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.
Files changed (55) hide show
  1. package/README.md +18 -15
  2. package/dist/cli.js +421 -152
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/backups/command.d.ts.map +1 -1
  5. package/dist/commands/backups/handlers.d.ts +1 -0
  6. package/dist/commands/backups/handlers.d.ts.map +1 -1
  7. package/dist/commands/backups/help.d.ts.map +1 -1
  8. package/dist/commands/certificates/command.d.ts.map +1 -1
  9. package/dist/commands/certificates/handlers.d.ts +0 -1
  10. package/dist/commands/certificates/handlers.d.ts.map +1 -1
  11. package/dist/commands/certificates/help.d.ts.map +1 -1
  12. package/dist/commands/commands/handlers.d.ts.map +1 -1
  13. package/dist/commands/completion.d.ts.map +1 -1
  14. package/dist/commands/config.d.ts.map +1 -1
  15. package/dist/commands/daemons/command.d.ts.map +1 -1
  16. package/dist/commands/daemons/handlers.d.ts +1 -0
  17. package/dist/commands/daemons/handlers.d.ts.map +1 -1
  18. package/dist/commands/daemons/help.d.ts.map +1 -1
  19. package/dist/commands/database-users/command.d.ts.map +1 -1
  20. package/dist/commands/database-users/handlers.d.ts +1 -0
  21. package/dist/commands/database-users/handlers.d.ts.map +1 -1
  22. package/dist/commands/database-users/help.d.ts.map +1 -1
  23. package/dist/commands/deployments/handlers.d.ts.map +1 -1
  24. package/dist/commands/deployments/help.d.ts.map +1 -1
  25. package/dist/commands/monitors/handlers.d.ts.map +1 -1
  26. package/dist/commands/recipes/command.d.ts.map +1 -1
  27. package/dist/commands/recipes/handlers.d.ts +1 -0
  28. package/dist/commands/recipes/handlers.d.ts.map +1 -1
  29. package/dist/commands/recipes/help.d.ts.map +1 -1
  30. package/dist/commands/redirect-rules/handlers.d.ts.map +1 -1
  31. package/dist/commands/scheduled-jobs/handlers.d.ts.map +1 -1
  32. package/dist/commands/security-rules/command.d.ts.map +1 -1
  33. package/dist/commands/security-rules/handlers.d.ts +1 -0
  34. package/dist/commands/security-rules/handlers.d.ts.map +1 -1
  35. package/dist/commands/security-rules/help.d.ts.map +1 -1
  36. package/dist/commands/sites/command.d.ts.map +1 -1
  37. package/dist/commands/sites/handlers.d.ts +1 -0
  38. package/dist/commands/sites/handlers.d.ts.map +1 -1
  39. package/dist/config.d.ts +1 -1
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/context.d.ts.map +1 -1
  42. package/dist/{error-handler-Byuujxh1.js → error-handler-ByMitiOr.js} +21 -10
  43. package/dist/error-handler-ByMitiOr.js.map +1 -0
  44. package/dist/error-handler.d.ts.map +1 -1
  45. package/dist/errors.d.ts.map +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/output.d.ts.map +1 -1
  48. package/dist/types.d.ts +1 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/utils/args.d.ts +1 -0
  51. package/dist/utils/args.d.ts.map +1 -1
  52. package/dist/utils/command-router.d.ts.map +1 -1
  53. package/dist/utils/resolve.d.ts.map +1 -1
  54. package/package.json +3 -3
  55. package/dist/error-handler-Byuujxh1.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as createContext, c as colors, d as deleteToken, i as runCommand, l as setColorEnabled, m as setToken, n as exitWithValidationError, p as getToken, s as OutputFormatter, u as createConfigStore, v as ValidationError } from "./error-handler-Byuujxh1.js";
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, listCertificates, listCommands, listDaemons, listDatabaseUsers, listDatabases, listDeployments, listFirewallRules, listMonitors, listNginxTemplates, listRecipes, listRedirectRules, listScheduledJobs, listSecurityRules, listServers, listSites, listSshKeys, matchByName, rebootServer, restartDaemon, runRecipe, updateEnv, updateNginxConfig, updateNginxTemplate } from "@studiometa/forge-core";
2
+ import { S as isOutputFormat, a as createContext, b as ValidationError, c as colors, d as deleteToken, g as setToken, h as setOrganizationSlug, i as runCommand, l as setColorEnabled, m as getToken, n as exitWithValidationError, p as getOrganizationSlug, s as OutputFormatter, u as createConfigStore } from "./error-handler-ByMitiOr.js";
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
4
  import { mkdirSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { join } from "node:path";
@@ -18,7 +18,8 @@ function getCliAuditLogger() {
18
18
  */
19
19
  function createCommandRouter(config) {
20
20
  return async function(subcommand, args, options) {
21
- const formatter = new OutputFormatter(options.format ?? options.f ?? "human", options["no-color"] === true);
21
+ const rawFormat = options.format ?? options.f ?? "human";
22
+ const formatter = new OutputFormatter(isOutputFormat(rawFormat) ? rawFormat : "human", options["no-color"] === true);
22
23
  const ctx = createContext(options);
23
24
  const handler = config.handlers[subcommand];
24
25
  if (!handler) {
@@ -114,10 +115,10 @@ async function backupsList(ctx) {
114
115
  const result = await listBackupConfigs({ server_id: await resolveServerId(server, execCtx) }, execCtx);
115
116
  ctx.formatter.outputList(result.data, [
116
117
  "id",
117
- "provider_name",
118
- "frequency",
118
+ "name",
119
+ "schedule",
119
120
  "status"
120
- ], "No backup configurations found.", (b) => `${String(b.id).padEnd(8)} ${b.provider_name.padEnd(20)} ${b.frequency.padEnd(12)} ${b.status}`);
121
+ ], "No backup configurations found.", (b) => `${String(b.id).padEnd(8)} ${b.name.padEnd(20)} ${b.schedule.padEnd(12)} ${b.status}`);
121
122
  }, ctx.formatter);
122
123
  }
123
124
  async function backupsGet(args, ctx) {
@@ -134,42 +135,70 @@ async function backupsGet(args, ctx) {
134
135
  }, execCtx);
135
136
  ctx.formatter.outputOne(result.data, [
136
137
  "id",
138
+ "name",
137
139
  "provider",
138
- "provider_name",
139
- "frequency",
140
+ "schedule",
141
+ "displayable_schedule",
140
142
  "retention",
141
143
  "status",
142
- "last_backup_time"
144
+ "next_run_time"
143
145
  ]);
144
146
  }, ctx.formatter);
145
147
  }
146
148
  async function backupsCreate(ctx) {
147
149
  const server = String(ctx.options.server ?? "");
148
- const provider = String(ctx.options.provider ?? "");
150
+ const storageProviderId = Number(ctx.options.provider ?? 0);
149
151
  const frequency = String(ctx.options.frequency ?? "");
150
- const databasesRaw = ctx.options.databases;
151
- const databases = Array.isArray(databasesRaw) ? databasesRaw.map(Number) : databasesRaw ? [Number(databasesRaw)] : [];
152
+ const databaseIdsRaw = ctx.options.databases;
153
+ const database_ids = Array.isArray(databaseIdsRaw) ? databaseIdsRaw.map(Number) : databaseIdsRaw ? [Number(databaseIdsRaw)] : [];
152
154
  if (!server) exitWithValidationError("server_id", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
153
- if (!provider) exitWithValidationError("provider", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
155
+ if (!storageProviderId) exitWithValidationError("provider", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
154
156
  if (!frequency) exitWithValidationError("frequency", "forge backups create --server <server_id> --provider <provider> --frequency <frequency>", ctx.formatter);
155
157
  await runCommand(async () => {
156
158
  const token = ctx.getToken();
157
159
  const execCtx = ctx.createExecutorContext(token);
158
- const result = await createBackupConfig({
160
+ await createBackupConfig({
159
161
  server_id: await resolveServerId(server, execCtx),
160
- provider,
162
+ storage_provider_id: storageProviderId,
161
163
  frequency,
162
- credentials: {},
163
- databases
164
+ retention: 5,
165
+ database_ids
164
166
  }, execCtx);
165
- ctx.formatter.outputOne(result.data, [
166
- "id",
167
- "provider",
168
- "provider_name",
169
- "frequency",
170
- "retention",
171
- "status"
172
- ]);
167
+ ctx.formatter.success("Backup configuration created.");
168
+ }, ctx.formatter);
169
+ }
170
+ async function backupsUpdate(args, ctx) {
171
+ const [id] = args;
172
+ const server = String(ctx.options.server ?? "");
173
+ const storageProviderId = Number(ctx.options.provider ?? 0);
174
+ const frequency = String(ctx.options.frequency ?? "");
175
+ const retention = Number(ctx.options.retention ?? 0);
176
+ const databaseIdsRaw = ctx.options.databases;
177
+ const database_ids = Array.isArray(databaseIdsRaw) ? databaseIdsRaw.map(Number) : databaseIdsRaw ? [Number(databaseIdsRaw)] : [];
178
+ if (!id) exitWithValidationError("backup_id", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
179
+ if (!server) exitWithValidationError("server_id", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
180
+ if (!storageProviderId) exitWithValidationError("provider", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
181
+ if (!frequency) exitWithValidationError("frequency", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
182
+ if (!retention) exitWithValidationError("retention", "forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention>", ctx.formatter);
183
+ await runCommand(async () => {
184
+ const token = ctx.getToken();
185
+ const execCtx = ctx.createExecutorContext(token);
186
+ await updateBackupConfig({
187
+ server_id: await resolveServerId(server, execCtx),
188
+ id,
189
+ storage_provider_id: storageProviderId,
190
+ frequency,
191
+ retention,
192
+ database_ids,
193
+ ...ctx.options.name != null ? { name: String(ctx.options.name) } : {},
194
+ ...ctx.options.bucket != null ? { bucket: String(ctx.options.bucket) } : {},
195
+ ...ctx.options.directory != null ? { directory: String(ctx.options.directory) } : {},
196
+ ...ctx.options.day != null ? { day: String(ctx.options.day) } : {},
197
+ ...ctx.options.time != null ? { time: String(ctx.options.time) } : {},
198
+ ...ctx.options.cron != null ? { cron: String(ctx.options.cron) } : {},
199
+ ...ctx.options["notification-email"] != null ? { notification_email: String(ctx.options["notification-email"]) } : {}
200
+ }, execCtx);
201
+ ctx.formatter.success(`Backup configuration ${id} updated.`);
173
202
  }, ctx.formatter);
174
203
  }
175
204
  async function backupsDelete(args, ctx) {
@@ -194,9 +223,14 @@ const handleBackupsCommand = createCommandRouter({
194
223
  ls: backupsList,
195
224
  get: [backupsGet, "args"],
196
225
  create: backupsCreate,
226
+ update: [backupsUpdate, "args"],
197
227
  delete: [backupsDelete, "args"]
198
228
  },
199
- writeSubcommands: ["create", "delete"]
229
+ writeSubcommands: [
230
+ "create",
231
+ "update",
232
+ "delete"
233
+ ]
200
234
  });
201
235
  function showBackupsHelp(subcommand) {
202
236
  if (subcommand === "list" || subcommand === "ls") console.log(`
@@ -227,6 +261,27 @@ ${colors.bold("OPTIONS:")}
227
261
  --frequency <freq> Backup frequency (required)
228
262
  --databases <ids> Comma-separated database IDs to back up (optional)
229
263
  -f, --format <fmt> Output format: json, human, table
264
+ `);
265
+ else if (subcommand === "update") console.log(`
266
+ ${colors.bold("forge backups update")} - Update a backup configuration
267
+
268
+ ${colors.bold("USAGE:")}
269
+ forge backups update <backup_id> --server <server_id> --provider <provider> --frequency <frequency> --retention <retention> [options]
270
+
271
+ ${colors.bold("OPTIONS:")}
272
+ --server <id> Server ID (required)
273
+ --provider <id> Storage provider ID (required)
274
+ --frequency <freq> Backup frequency (required)
275
+ --retention <n> Retention count (required)
276
+ --databases <ids> Comma-separated database IDs (optional)
277
+ --name <name> Backup name (optional)
278
+ --bucket <bucket> Bucket name (optional)
279
+ --directory <dir> Directory (optional)
280
+ --day <day> Day of week (optional)
281
+ --time <time> Time (optional)
282
+ --cron <cron> Cron expression (optional)
283
+ --notification-email <email> Notification email (optional)
284
+ -f, --format <fmt> Output format: json, human, table
230
285
  `);
231
286
  else if (subcommand === "delete") console.log(`
232
287
  ${colors.bold("forge backups delete")} - Delete a backup configuration
@@ -244,6 +299,7 @@ ${colors.bold("SUBCOMMANDS:")}
244
299
  list, ls List backup configurations
245
300
  get <id> Get backup configuration details
246
301
  create Create a new backup configuration
302
+ update <id> Update a backup configuration
247
303
  delete <id> Delete a backup configuration
248
304
 
249
305
  ${colors.bold("OPTIONS:")}
@@ -278,7 +334,7 @@ _forge_completions() {
278
334
  local daemons_cmds="list ls get restart"
279
335
  local env_cmds="get update"
280
336
  local nginx_cmds="get update"
281
- local certificates_cmds="list ls get activate"
337
+ local certificates_cmds="get activate"
282
338
  local firewall_rules_cmds="list ls get"
283
339
  local ssh_keys_cmds="list ls get"
284
340
  local backups_cmds="list ls get create delete"
@@ -533,9 +589,7 @@ _forge() {
533
589
  certificates|certs)
534
590
  local -a certificates_cmds
535
591
  certificates_cmds=(
536
- 'list:List certificates'
537
- 'ls:List certificates (alias)'
538
- 'get:Get certificate details'
592
+ 'get:Get certificate for a domain'
539
593
  'activate:Activate a certificate'
540
594
  )
541
595
  _describe 'certificates command' certificates_cmds
@@ -782,8 +836,7 @@ complete -c forge -f -n "__fish_seen_subcommand_from nginx" -a "get" -d "Get ngi
782
836
  complete -c forge -f -n "__fish_seen_subcommand_from nginx" -a "update" -d "Update nginx config"
783
837
 
784
838
  # certificates subcommands
785
- complete -c forge -f -n "__fish_seen_subcommand_from certificates certs" -a "list" -d "List certificates"
786
- complete -c forge -f -n "__fish_seen_subcommand_from certificates certs" -a "ls" -d "List certificates (alias)"
839
+ complete -c forge -f -n "__fish_seen_subcommand_from certificates certs" -a "get" -d "Get certificate for a domain"
787
840
  complete -c forge -f -n "__fish_seen_subcommand_from certificates certs" -a "get" -d "Get certificate details"
788
841
  complete -c forge -f -n "__fish_seen_subcommand_from certificates certs" -a "activate" -d "Activate a certificate"
789
842
 
@@ -980,7 +1033,7 @@ function handleCompletionCommand(shell, options = {}) {
980
1033
  writeFileSync(installPath, script, "utf8");
981
1034
  console.log(`${colors.green("✓")} Installed ${shell} completion to ${colors.cyan(installPath)}`);
982
1035
  console.log();
983
- console.log(`${colors.bold("Next steps:")}`);
1036
+ console.log(colors.bold("Next steps:"));
984
1037
  switch (shellLower) {
985
1038
  case "bash":
986
1039
  console.log(` 1. Restart your shell: ${colors.cyan("exec bash")}`);
@@ -1000,40 +1053,22 @@ function handleCompletionCommand(shell, options = {}) {
1000
1053
  process.exit(1);
1001
1054
  }
1002
1055
  }
1003
- function requireServerAndSiteRaw$3(ctx, usage) {
1056
+ function requireServerSiteDomainRaw(ctx, usage) {
1004
1057
  const server = String(ctx.options.server ?? "");
1005
1058
  const site = String(ctx.options.site ?? "");
1059
+ const domain = String(ctx.options.domain ?? "");
1006
1060
  if (!server) exitWithValidationError("server_id", usage, ctx.formatter);
1007
1061
  if (!site) exitWithValidationError("site_id", usage, ctx.formatter);
1062
+ if (!domain) exitWithValidationError("domain_id", usage, ctx.formatter);
1008
1063
  return {
1009
1064
  server,
1010
- site
1065
+ site,
1066
+ domain
1011
1067
  };
1012
1068
  }
1013
- async function certificatesList(ctx) {
1014
- const { server, site } = requireServerAndSiteRaw$3(ctx, "forge certificates list --server <server_id> --site <site_id>");
1015
- await runCommand(async () => {
1016
- const token = ctx.getToken();
1017
- const execCtx = ctx.createExecutorContext(token);
1018
- const server_id = await resolveServerId(server, execCtx);
1019
- const result = await listCertificates({
1020
- server_id,
1021
- site_id: await resolveSiteId(site, server_id, execCtx)
1022
- }, execCtx);
1023
- ctx.formatter.outputList(result.data, [
1024
- "id",
1025
- "domain",
1026
- "type",
1027
- "status",
1028
- "active"
1029
- ], "No certificates found.", (c) => `${String(c.id).padEnd(8)} ${c.domain.padEnd(40)} ${c.type.padEnd(10)} ${c.status.padEnd(12)} ${c.active ? "active" : "—"}`);
1030
- }, ctx.formatter);
1031
- }
1032
1069
  async function certificatesGet(args, ctx) {
1033
- const [id] = args;
1034
- const usage = "forge certificates get <cert_id> --server <server_id> --site <site_id>";
1035
- if (!id) exitWithValidationError("certificate_id", usage, ctx.formatter);
1036
- const { server, site } = requireServerAndSiteRaw$3(ctx, usage);
1070
+ const { server, site, domain } = requireServerSiteDomainRaw(ctx, "forge certificates get --server <server_id> --site <site_id> --domain <domain_id>");
1071
+ const domainId = args[0] || domain;
1037
1072
  await runCommand(async () => {
1038
1073
  const token = ctx.getToken();
1039
1074
  const execCtx = ctx.createExecutorContext(token);
@@ -1041,25 +1076,22 @@ async function certificatesGet(args, ctx) {
1041
1076
  const result = await getCertificate({
1042
1077
  server_id,
1043
1078
  site_id: await resolveSiteId(site, server_id, execCtx),
1044
- id
1079
+ domain_id: domainId
1045
1080
  }, execCtx);
1046
1081
  ctx.formatter.outputOne(result.data, [
1047
1082
  "id",
1048
- "domain",
1049
1083
  "type",
1084
+ "verification_method",
1085
+ "key_type",
1050
1086
  "status",
1051
1087
  "request_status",
1052
- "active",
1053
- "existing",
1054
1088
  "created_at"
1055
1089
  ]);
1056
1090
  }, ctx.formatter);
1057
1091
  }
1058
1092
  async function certificatesActivate(args, ctx) {
1059
- const [id] = args;
1060
- const usage = "forge certificates activate <cert_id> --server <server_id> --site <site_id>";
1061
- if (!id) exitWithValidationError("certificate_id", usage, ctx.formatter);
1062
- const { server, site } = requireServerAndSiteRaw$3(ctx, usage);
1093
+ const { server, site, domain } = requireServerSiteDomainRaw(ctx, "forge certificates activate --server <server_id> --site <site_id> --domain <domain_id>");
1094
+ const domainId = args[0] || domain;
1063
1095
  await runCommand(async () => {
1064
1096
  const token = ctx.getToken();
1065
1097
  const execCtx = ctx.createExecutorContext(token);
@@ -1067,39 +1099,31 @@ async function certificatesActivate(args, ctx) {
1067
1099
  await activateCertificate({
1068
1100
  server_id,
1069
1101
  site_id: await resolveSiteId(site, server_id, execCtx),
1070
- id
1102
+ domain_id: domainId
1071
1103
  }, execCtx);
1072
- ctx.formatter.success(`Certificate ${id} activated.`);
1104
+ ctx.formatter.success(`Certificate for domain ${domainId} activated.`);
1073
1105
  }, ctx.formatter);
1074
1106
  }
1075
1107
  const handleCertificatesCommand = createCommandRouter({
1076
1108
  resource: "certificates",
1077
1109
  handlers: {
1078
- list: certificatesList,
1079
- ls: certificatesList,
1080
1110
  get: [certificatesGet, "args"],
1081
1111
  activate: [certificatesActivate, "args"]
1082
1112
  },
1083
1113
  writeSubcommands: ["activate"]
1084
1114
  });
1085
1115
  function showCertificatesHelp(subcommand) {
1086
- if (subcommand === "list" || subcommand === "ls") console.log(`
1087
- ${colors.bold("forge certificates list")} - List certificates for a site
1088
-
1089
- ${colors.bold("USAGE:")}
1090
- forge certificates list --server <server_id> --site <site_id> [options]
1091
- `);
1092
- else if (subcommand === "get") console.log(`
1093
- ${colors.bold("forge certificates get")} - Get certificate details
1116
+ if (subcommand === "get") console.log(`
1117
+ ${colors.bold("forge certificates get")} - Get certificate for a domain
1094
1118
 
1095
1119
  ${colors.bold("USAGE:")}
1096
- forge certificates get <cert_id> --server <server_id> --site <site_id>
1120
+ forge certificates get --server <server_id> --site <site_id> --domain <domain_id>
1097
1121
  `);
1098
1122
  else if (subcommand === "activate") console.log(`
1099
1123
  ${colors.bold("forge certificates activate")} - Activate a certificate
1100
1124
 
1101
1125
  ${colors.bold("USAGE:")}
1102
- forge certificates activate <cert_id> --server <server_id> --site <site_id>
1126
+ forge certificates activate --server <server_id> --site <site_id> --domain <domain_id>
1103
1127
  `);
1104
1128
  else console.log(`
1105
1129
  ${colors.bold("forge certificates")} - Manage SSL certificates
@@ -1111,15 +1135,18 @@ ${colors.bold("ALIASES:")}
1111
1135
  forge certs
1112
1136
 
1113
1137
  ${colors.bold("SUBCOMMANDS:")}
1114
- list, ls List certificates
1115
- get <id> Get certificate details
1116
- activate <id> Activate a certificate
1138
+ get Get certificate for a domain
1139
+ activate Activate a certificate
1117
1140
 
1118
1141
  ${colors.bold("OPTIONS:")}
1119
1142
  --server <id> Server ID (required)
1120
1143
  --site <id> Site ID (required)
1144
+ --domain <id> Domain record ID (required)
1121
1145
  -f, --format <fmt> Output format: json, human, table
1122
1146
 
1147
+ ${colors.bold("NOTE:")}
1148
+ In Forge v2, certificates are per-domain. Use domains to manage certificates.
1149
+
1123
1150
  Run ${colors.cyan("forge certificates <subcommand> --help")} for details.
1124
1151
  `);
1125
1152
  }
@@ -1169,7 +1196,8 @@ async function commandsGet(args, ctx) {
1169
1196
  "id",
1170
1197
  "command",
1171
1198
  "status",
1172
- "user_name",
1199
+ "user_id",
1200
+ "duration",
1173
1201
  "created_at"
1174
1202
  ]);
1175
1203
  }, ctx.formatter);
@@ -1183,17 +1211,12 @@ async function commandsCreate(ctx) {
1183
1211
  const token = ctx.getToken();
1184
1212
  const execCtx = ctx.createExecutorContext(token);
1185
1213
  const server_id = await resolveServerId(server, execCtx);
1186
- const result = await createCommand({
1214
+ await createCommand({
1187
1215
  server_id,
1188
1216
  site_id: await resolveSiteId(site, server_id, execCtx),
1189
1217
  command
1190
1218
  }, execCtx);
1191
- ctx.formatter.outputOne(result.data, [
1192
- "id",
1193
- "command",
1194
- "status",
1195
- "created_at"
1196
- ]);
1219
+ ctx.formatter.success("Command queued for execution.");
1197
1220
  }, ctx.formatter);
1198
1221
  }
1199
1222
  const handleCommandsCommand = createCommandRouter({
@@ -1263,56 +1286,82 @@ function showConfigHelp() {
1263
1286
  ${colors.bold("forge config")} - Manage CLI configuration
1264
1287
 
1265
1288
  ${colors.bold("USAGE:")}
1266
- forge config <subcommand> [options]
1289
+ forge config <subcommand> [key] [value]
1267
1290
 
1268
1291
  ${colors.bold("SUBCOMMANDS:")}
1269
- set <token> Save API token to config file
1270
- get Show current API token (masked)
1271
- delete Delete stored API token
1292
+ set <key> <value> Save a config value
1293
+ get Show current configuration (masked)
1294
+ delete Delete all stored configuration
1295
+
1296
+ ${colors.bold("CONFIG KEYS:")}
1297
+ apiToken Forge API token (scoped, from Forge dashboard)
1298
+ organizationSlug Default organization slug (e.g. 'studio-meta')
1272
1299
 
1273
1300
  ${colors.bold("EXAMPLES:")}
1274
- forge config set YOUR_FORGE_TOKEN
1301
+ forge config set apiToken YOUR_FORGE_TOKEN
1302
+ forge config set organizationSlug studio-meta
1275
1303
  forge config get
1276
1304
  forge config delete
1277
1305
  `);
1278
1306
  }
1279
1307
  function handleConfigCommand(subcommand, args, options) {
1280
- const format = String(options.format ?? options.f ?? "human");
1308
+ const rawFormat = options.format ?? options.f ?? "human";
1309
+ const format = isOutputFormat(rawFormat) ? rawFormat : "human";
1281
1310
  const formatter = new OutputFormatter(format, options["no-color"] === true);
1282
1311
  const store = createConfigStore();
1283
1312
  switch (subcommand) {
1284
1313
  case "set": {
1285
- const [token] = args;
1286
- if (!token) {
1287
- formatter.error("Token is required. Usage: forge config set <token>");
1314
+ const [key, value] = args;
1315
+ if (!key || !value) {
1316
+ formatter.error("Usage: forge config set <key> <value>");
1317
+ formatter.info("Keys: apiToken, organizationSlug");
1288
1318
  process.exit(1);
1289
1319
  return;
1290
1320
  }
1291
- setToken(token, store);
1292
- formatter.success("API token saved.");
1321
+ switch (key) {
1322
+ case "apiToken":
1323
+ setToken(value, store);
1324
+ formatter.success("API token saved.");
1325
+ break;
1326
+ case "organizationSlug":
1327
+ setOrganizationSlug(value, store);
1328
+ formatter.success(`Organization slug set to "${value}".`);
1329
+ break;
1330
+ default:
1331
+ formatter.error(`Unknown config key: ${key}`);
1332
+ formatter.info("Valid keys: apiToken, organizationSlug");
1333
+ process.exit(1);
1334
+ }
1293
1335
  break;
1294
1336
  }
1295
1337
  case "get": {
1296
1338
  const token = getToken(store);
1297
- if (!token) {
1298
- formatter.warning("No API token configured.");
1299
- formatter.info("Run: forge config set <token>");
1300
- } else {
1301
- const masked = token.length > 8 ? `${token.slice(0, 4)}${"*".repeat(token.length - 8)}${token.slice(-4)}` : "****";
1302
- if (format === "json") formatter.output({ apiToken: masked });
1303
- else console.log(`apiToken: ${masked}`);
1339
+ const orgSlug = getOrganizationSlug(store);
1340
+ if (format === "json") formatter.output({
1341
+ apiToken: token ? maskToken(token) : null,
1342
+ organizationSlug: orgSlug ?? null
1343
+ });
1344
+ else {
1345
+ if (token) console.log(`apiToken: ${maskToken(token)}`);
1346
+ else formatter.warning("No API token configured.");
1347
+ if (orgSlug) console.log(`organizationSlug: ${orgSlug}`);
1348
+ else formatter.warning("No organization slug configured.");
1349
+ if (!token || !orgSlug) formatter.info("Run: forge config set <key> <value>");
1304
1350
  }
1305
1351
  break;
1306
1352
  }
1307
1353
  case "delete":
1308
1354
  deleteToken(store);
1309
- formatter.success("API token deleted.");
1355
+ formatter.success("Configuration deleted.");
1310
1356
  break;
1311
1357
  default:
1312
1358
  showConfigHelp();
1313
1359
  break;
1314
1360
  }
1315
1361
  }
1362
+ function maskToken(token) {
1363
+ return token.length > 8 ? `${token.slice(0, 4)}${"*".repeat(token.length - 8)}${token.slice(-4)}` : "****";
1364
+ }
1316
1365
  async function daemonsList(ctx) {
1317
1366
  const server = String(ctx.options.server ?? "");
1318
1367
  if (!server) exitWithValidationError("server_id", "forge daemons list --server <server_id>", ctx.formatter);
@@ -1351,6 +1400,34 @@ async function daemonsGet(args, ctx) {
1351
1400
  ]);
1352
1401
  }, ctx.formatter);
1353
1402
  }
1403
+ async function daemonsUpdate(args, ctx) {
1404
+ const [id] = args;
1405
+ const server = String(ctx.options.server ?? "");
1406
+ const name = String(ctx.options.name ?? "");
1407
+ if (!id) exitWithValidationError("daemon_id", "forge daemons update <daemon_id> --server <server_id> --name <name>", ctx.formatter);
1408
+ if (!server) exitWithValidationError("server_id", "forge daemons update <daemon_id> --server <server_id> --name <name>", ctx.formatter);
1409
+ if (!name) exitWithValidationError("name", "forge daemons update <daemon_id> --server <server_id> --name <name>", ctx.formatter);
1410
+ await runCommand(async () => {
1411
+ const token = ctx.getToken();
1412
+ const execCtx = ctx.createExecutorContext(token);
1413
+ const options = {
1414
+ server_id: await resolveServerId(server, execCtx),
1415
+ id,
1416
+ name
1417
+ };
1418
+ if (ctx.options.config) options.config = String(ctx.options.config);
1419
+ const result = await updateDaemon(options, execCtx);
1420
+ ctx.formatter.outputOne(result.data, [
1421
+ "id",
1422
+ "command",
1423
+ "user",
1424
+ "directory",
1425
+ "processes",
1426
+ "status",
1427
+ "created_at"
1428
+ ]);
1429
+ }, ctx.formatter);
1430
+ }
1354
1431
  async function daemonsRestart(args, ctx) {
1355
1432
  const [id] = args;
1356
1433
  const server = String(ctx.options.server ?? "");
@@ -1372,9 +1449,10 @@ const handleDaemonsCommand = createCommandRouter({
1372
1449
  list: daemonsList,
1373
1450
  ls: daemonsList,
1374
1451
  get: [daemonsGet, "args"],
1375
- restart: [daemonsRestart, "args"]
1452
+ restart: [daemonsRestart, "args"],
1453
+ update: [daemonsUpdate, "args"]
1376
1454
  },
1377
- writeSubcommands: ["restart"]
1455
+ writeSubcommands: ["restart", "update"]
1378
1456
  });
1379
1457
  function showDaemonsHelp(subcommand) {
1380
1458
  if (subcommand === "list" || subcommand === "ls") console.log(`
@@ -1388,6 +1466,12 @@ ${colors.bold("forge daemons get")} - Get daemon details
1388
1466
 
1389
1467
  ${colors.bold("USAGE:")}
1390
1468
  forge daemons get <daemon_id> --server <server_id>
1469
+ `);
1470
+ else if (subcommand === "update") console.log(`
1471
+ ${colors.bold("forge daemons update")} - Update a daemon
1472
+
1473
+ ${colors.bold("USAGE:")}
1474
+ forge daemons update <daemon_id> --server <server_id> --name <name> [--config <config>]
1391
1475
  `);
1392
1476
  else if (subcommand === "restart") console.log(`
1393
1477
  ${colors.bold("forge daemons restart")} - Restart a daemon
@@ -1404,6 +1488,7 @@ ${colors.bold("USAGE:")}
1404
1488
  ${colors.bold("SUBCOMMANDS:")}
1405
1489
  list, ls List daemons
1406
1490
  get <id> Get daemon details
1491
+ update <id> Update a daemon
1407
1492
  restart <id> Restart a daemon
1408
1493
 
1409
1494
  ${colors.bold("OPTIONS:")}
@@ -1522,7 +1607,6 @@ async function databaseUsersGet(args, ctx) {
1522
1607
  "id",
1523
1608
  "name",
1524
1609
  "status",
1525
- "databases",
1526
1610
  "created_at"
1527
1611
  ]);
1528
1612
  }, ctx.formatter);
@@ -1549,7 +1633,31 @@ async function databaseUsersCreate(ctx) {
1549
1633
  "id",
1550
1634
  "name",
1551
1635
  "status",
1552
- "databases",
1636
+ "created_at"
1637
+ ]);
1638
+ }, ctx.formatter);
1639
+ }
1640
+ async function databaseUsersUpdate(args, ctx) {
1641
+ const [id] = args;
1642
+ const server = String(ctx.options.server ?? "");
1643
+ const password = ctx.options.password != null ? String(ctx.options.password) : void 0;
1644
+ const databaseIdsRaw = ctx.options["database-ids"] ?? ctx.options.database_ids;
1645
+ const database_ids = Array.isArray(databaseIdsRaw) ? databaseIdsRaw.map(Number) : databaseIdsRaw ? [Number(databaseIdsRaw)] : void 0;
1646
+ if (!id) exitWithValidationError("user_id", "forge database-users update <user_id> --server <server_id> [--password <password>] [--database-ids <ids>]", ctx.formatter);
1647
+ if (!server) exitWithValidationError("server_id", "forge database-users update <user_id> --server <server_id> [--password <password>] [--database-ids <ids>]", ctx.formatter);
1648
+ await runCommand(async () => {
1649
+ const token = ctx.getToken();
1650
+ const execCtx = ctx.createExecutorContext(token);
1651
+ const result = await updateDatabaseUser({
1652
+ server_id: await resolveServerId(server, execCtx),
1653
+ id,
1654
+ ...password !== void 0 ? { password } : {},
1655
+ ...database_ids ? { database_ids } : {}
1656
+ }, execCtx);
1657
+ ctx.formatter.outputOne(result.data, [
1658
+ "id",
1659
+ "name",
1660
+ "status",
1553
1661
  "created_at"
1554
1662
  ]);
1555
1663
  }, ctx.formatter);
@@ -1576,9 +1684,14 @@ const handleDatabaseUsersCommand = createCommandRouter({
1576
1684
  ls: databaseUsersList,
1577
1685
  get: [databaseUsersGet, "args"],
1578
1686
  create: databaseUsersCreate,
1687
+ update: [databaseUsersUpdate, "args"],
1579
1688
  delete: [databaseUsersDelete, "args"]
1580
1689
  },
1581
- writeSubcommands: ["create", "delete"]
1690
+ writeSubcommands: [
1691
+ "create",
1692
+ "update",
1693
+ "delete"
1694
+ ]
1582
1695
  });
1583
1696
  function showDatabaseUsersHelp(subcommand) {
1584
1697
  if (subcommand === "list" || subcommand === "ls") console.log(`
@@ -1609,6 +1722,18 @@ ${colors.bold("OPTIONS:")}
1609
1722
  --password <pass> User password (required)
1610
1723
  --databases <ids> Comma-separated database IDs to grant access to (optional)
1611
1724
  -f, --format <fmt> Output format: json, human, table
1725
+ `);
1726
+ else if (subcommand === "update") console.log(`
1727
+ ${colors.bold("forge database-users update")} - Update a database user
1728
+
1729
+ ${colors.bold("USAGE:")}
1730
+ forge database-users update <user_id> --server <server_id> [options]
1731
+
1732
+ ${colors.bold("OPTIONS:")}
1733
+ --server <id> Server ID (required)
1734
+ --password <pass> New password (optional)
1735
+ --database-ids <ids> Comma-separated database IDs (optional)
1736
+ -f, --format <fmt> Output format: json, human, table
1612
1737
  `);
1613
1738
  else if (subcommand === "delete") console.log(`
1614
1739
  ${colors.bold("forge database-users delete")} - Delete a database user
@@ -1626,6 +1751,7 @@ ${colors.bold("SUBCOMMANDS:")}
1626
1751
  list, ls List database users
1627
1752
  get <id> Get database user details
1628
1753
  create Create a new database user
1754
+ update <id> Update a database user
1629
1755
  delete <id> Delete a database user
1630
1756
 
1631
1757
  ${colors.bold("OPTIONS:")}
@@ -1651,14 +1777,15 @@ async function deploymentsList(ctx) {
1651
1777
  ctx.formatter.outputList(result.data, [
1652
1778
  "id",
1653
1779
  "status",
1654
- "commit_hash",
1780
+ "commit",
1655
1781
  "started_at"
1656
- ], "No deployments found.", (d) => `${String(d.id).padEnd(8)} ${d.status.padEnd(12)} ${(d.commit_hash ?? "—").padEnd(10)} ${d.started_at}`);
1782
+ ], "No deployments found.", (d) => `${String(d.id).padEnd(8)} ${d.status.padEnd(12)} ${(d.commit.hash ?? "—").padEnd(10)} ${d.started_at}`);
1657
1783
  }, ctx.formatter);
1658
1784
  }
1659
1785
  async function deploymentsDeploy(ctx) {
1660
1786
  const server = String(ctx.options.server ?? "");
1661
1787
  const site = String(ctx.options.site ?? "");
1788
+ const streamLogs = Boolean(ctx.options.stream);
1662
1789
  if (!server) exitWithValidationError("server_id", "forge deployments deploy --server <server_id> --site <site_id>", ctx.formatter);
1663
1790
  if (!site) exitWithValidationError("site_id", "forge deployments deploy --server <server_id> --site <site_id>", ctx.formatter);
1664
1791
  await runCommand(async () => {
@@ -1669,15 +1796,18 @@ async function deploymentsDeploy(ctx) {
1669
1796
  const result = await deploySiteAndWait({
1670
1797
  server_id,
1671
1798
  site_id,
1672
- onProgress: ({ status, elapsed_ms }) => {
1799
+ onProgress: streamLogs ? void 0 : ({ status, elapsed_ms }) => {
1673
1800
  process.stderr.write(`\rDeploying… ${status} (${(elapsed_ms / 1e3).toFixed(1)}s)`);
1674
- }
1801
+ },
1802
+ onLog: streamLogs ? (chunk) => {
1803
+ process.stdout.write(chunk);
1804
+ } : void 0
1675
1805
  }, execCtx);
1676
- process.stderr.write("\n");
1806
+ if (!streamLogs) process.stderr.write("\n");
1677
1807
  const elapsedSec = (result.data.elapsed_ms / 1e3).toFixed(1);
1678
1808
  if (result.data.status === "success") ctx.formatter.success(`Deployment succeeded for site ${site_id} (${elapsedSec}s).`);
1679
- else ctx.formatter.success(`Deployment failed for site ${site_id} (${elapsedSec}s).`);
1680
- if (result.data.log) ctx.formatter.output(result.data.log);
1809
+ else ctx.formatter.error(`Deployment failed for site ${site_id} (${elapsedSec}s).`);
1810
+ if (!streamLogs && result.data.log) ctx.formatter.output(result.data.log);
1681
1811
  }, ctx.formatter);
1682
1812
  }
1683
1813
  const handleDeploymentsCommand = createCommandRouter({
@@ -1705,7 +1835,13 @@ ${colors.bold("OPTIONS:")}
1705
1835
  ${colors.bold("forge deployments deploy")} - Trigger a deployment
1706
1836
 
1707
1837
  ${colors.bold("USAGE:")}
1708
- forge deployments deploy --server <server_id> --site <site_id>
1838
+ forge deployments deploy --server <server_id> --site <site_id> [options]
1839
+
1840
+ ${colors.bold("OPTIONS:")}
1841
+ --server <id> Server ID (required)
1842
+ --site <id> Site ID (required)
1843
+ --stream Stream deployment logs in real-time (default: show progress)
1844
+ -f, --format <fmt> Output format: json, human, table
1709
1845
  `);
1710
1846
  else console.log(`
1711
1847
  ${colors.bold("forge deployments")} - Manage deployments
@@ -1929,6 +2065,7 @@ async function monitorsCreate(ctx) {
1929
2065
  const type = String(ctx.options.type ?? "");
1930
2066
  const operator = String(ctx.options.operator ?? "");
1931
2067
  const threshold = Number(ctx.options.threshold ?? 0);
2068
+ const notify = String(ctx.options.notify ?? "");
1932
2069
  const minutes = Number(ctx.options.minutes ?? 0);
1933
2070
  if (!server) exitWithValidationError("server_id", "forge monitors create --server <server_id> --type <type> --operator <operator> --threshold <threshold> --minutes <minutes>", ctx.formatter);
1934
2071
  if (!type) exitWithValidationError("type", "forge monitors create --server <server_id> --type <type> --operator <operator> --threshold <threshold> --minutes <minutes>", ctx.formatter);
@@ -1941,6 +2078,7 @@ async function monitorsCreate(ctx) {
1941
2078
  type,
1942
2079
  operator,
1943
2080
  threshold,
2081
+ notify,
1944
2082
  minutes
1945
2083
  }, execCtx);
1946
2084
  ctx.formatter.outputOne(result.data, [
@@ -2313,6 +2451,30 @@ async function recipesGet(args, ctx) {
2313
2451
  ]);
2314
2452
  }, ctx.formatter);
2315
2453
  }
2454
+ async function recipesUpdate(args, ctx) {
2455
+ const [id] = args;
2456
+ if (!id) exitWithValidationError("id", "forge recipes update <recipe_id> [--name ...] [--user ...] [--script ...]", ctx.formatter);
2457
+ const name = ctx.options.name;
2458
+ const user = ctx.options.user;
2459
+ const script = ctx.options.script;
2460
+ await runCommand(async () => {
2461
+ const token = ctx.getToken();
2462
+ const execCtx = ctx.createExecutorContext(token);
2463
+ const result = await updateRecipe({
2464
+ id,
2465
+ name,
2466
+ user,
2467
+ script
2468
+ }, execCtx);
2469
+ ctx.formatter.outputOne(result.data, [
2470
+ "id",
2471
+ "name",
2472
+ "user",
2473
+ "created_at",
2474
+ "script"
2475
+ ]);
2476
+ }, ctx.formatter);
2477
+ }
2316
2478
  async function recipesRun(args, ctx) {
2317
2479
  const [id] = args;
2318
2480
  const serversRaw = String(ctx.options.servers ?? "");
@@ -2335,9 +2497,10 @@ const handleRecipesCommand = createCommandRouter({
2335
2497
  list: recipesList,
2336
2498
  ls: recipesList,
2337
2499
  get: [recipesGet, "args"],
2500
+ update: [recipesUpdate, "args"],
2338
2501
  run: [recipesRun, "args"]
2339
2502
  },
2340
- writeSubcommands: ["run"]
2503
+ writeSubcommands: ["update", "run"]
2341
2504
  });
2342
2505
  function showRecipesHelp(subcommand) {
2343
2506
  if (subcommand === "list" || subcommand === "ls") console.log(`
@@ -2351,6 +2514,17 @@ ${colors.bold("forge recipes get")} - Get recipe details
2351
2514
 
2352
2515
  ${colors.bold("USAGE:")}
2353
2516
  forge recipes get <recipe_id>
2517
+ `);
2518
+ else if (subcommand === "update") console.log(`
2519
+ ${colors.bold("forge recipes update")} - Update a recipe
2520
+
2521
+ ${colors.bold("USAGE:")}
2522
+ forge recipes update <recipe_id> [options]
2523
+
2524
+ ${colors.bold("OPTIONS:")}
2525
+ --name <name> New recipe name
2526
+ --user <user> New recipe user
2527
+ --script <script> New recipe script
2354
2528
  `);
2355
2529
  else if (subcommand === "run") console.log(`
2356
2530
  ${colors.bold("forge recipes run")} - Run a recipe on servers
@@ -2370,6 +2544,7 @@ ${colors.bold("USAGE:")}
2370
2544
  ${colors.bold("SUBCOMMANDS:")}
2371
2545
  list, ls List all recipes
2372
2546
  get <id> Get recipe details
2547
+ update <id> Update a recipe
2373
2548
  run <id> Run a recipe on servers
2374
2549
 
2375
2550
  ${colors.bold("OPTIONS:")}
@@ -2434,25 +2609,21 @@ async function redirectRulesCreate(ctx) {
2434
2609
  const { server, site } = requireServerAndSiteRaw$1(ctx, usage);
2435
2610
  const from = String(ctx.options.from ?? "");
2436
2611
  const to = String(ctx.options.to ?? "");
2612
+ const type = String(ctx.options.type ?? "redirect");
2437
2613
  if (!from) exitWithValidationError("from", usage, ctx.formatter);
2438
2614
  if (!to) exitWithValidationError("to", usage, ctx.formatter);
2439
2615
  await runCommand(async () => {
2440
2616
  const token = ctx.getToken();
2441
2617
  const execCtx = ctx.createExecutorContext(token);
2442
2618
  const server_id = await resolveServerId(server, execCtx);
2443
- const result = await createRedirectRule({
2619
+ await createRedirectRule({
2444
2620
  server_id,
2445
2621
  site_id: await resolveSiteId(site, server_id, execCtx),
2446
2622
  from,
2447
- to
2623
+ to,
2624
+ type
2448
2625
  }, execCtx);
2449
- ctx.formatter.outputOne(result.data, [
2450
- "id",
2451
- "from",
2452
- "to",
2453
- "type",
2454
- "created_at"
2455
- ]);
2626
+ ctx.formatter.success("Redirect rule created.");
2456
2627
  }, ctx.formatter);
2457
2628
  }
2458
2629
  async function redirectRulesDelete(args, ctx) {
@@ -2542,11 +2713,16 @@ Run ${colors.cyan("forge redirect-rules <subcommand> --help")} for details.
2542
2713
  }
2543
2714
  async function scheduledJobsList(ctx) {
2544
2715
  const server = String(ctx.options.server ?? "");
2716
+ const site = ctx.options.site ? String(ctx.options.site) : void 0;
2545
2717
  if (!server) exitWithValidationError("server_id", "forge scheduled-jobs list --server <server_id>", ctx.formatter);
2546
2718
  await runCommand(async () => {
2547
2719
  const token = ctx.getToken();
2548
2720
  const execCtx = ctx.createExecutorContext(token);
2549
- const result = await listScheduledJobs({ server_id: await resolveServerId(server, execCtx) }, execCtx);
2721
+ const server_id = await resolveServerId(server, execCtx);
2722
+ const result = await listScheduledJobs({
2723
+ server_id,
2724
+ site_id: site ? await resolveSiteId(site, server_id, execCtx) : void 0
2725
+ }, execCtx);
2550
2726
  ctx.formatter.outputList(result.data, [
2551
2727
  "id",
2552
2728
  "command",
@@ -2558,14 +2734,17 @@ async function scheduledJobsList(ctx) {
2558
2734
  async function scheduledJobsGet(args, ctx) {
2559
2735
  const [id] = args;
2560
2736
  const server = String(ctx.options.server ?? "");
2737
+ const site = ctx.options.site ? String(ctx.options.site) : void 0;
2561
2738
  if (!id) exitWithValidationError("job_id", "forge scheduled-jobs get <job_id> --server <server_id>", ctx.formatter);
2562
2739
  if (!server) exitWithValidationError("server_id", "forge scheduled-jobs get <job_id> --server <server_id>", ctx.formatter);
2563
2740
  await runCommand(async () => {
2564
2741
  const token = ctx.getToken();
2565
2742
  const execCtx = ctx.createExecutorContext(token);
2743
+ const server_id = await resolveServerId(server, execCtx);
2566
2744
  const result = await getScheduledJob({
2567
- server_id: await resolveServerId(server, execCtx),
2568
- id
2745
+ server_id,
2746
+ id,
2747
+ site_id: site ? await resolveSiteId(site, server_id, execCtx) : void 0
2569
2748
  }, execCtx);
2570
2749
  ctx.formatter.outputOne(result.data, [
2571
2750
  "id",
@@ -2580,15 +2759,24 @@ async function scheduledJobsGet(args, ctx) {
2580
2759
  }
2581
2760
  async function scheduledJobsCreate(ctx) {
2582
2761
  const server = String(ctx.options.server ?? "");
2762
+ const site = ctx.options.site ? String(ctx.options.site) : void 0;
2583
2763
  const command = String(ctx.options.command ?? "");
2584
- if (!server) exitWithValidationError("server_id", "forge scheduled-jobs create --server <server_id> --command <command>", ctx.formatter);
2585
- if (!command) exitWithValidationError("command", "forge scheduled-jobs create --server <server_id> --command <command>", ctx.formatter);
2764
+ const user = String(ctx.options.user ?? "forge");
2765
+ const frequency = String(ctx.options.frequency ?? "custom");
2766
+ const cron = ctx.options.cron ? String(ctx.options.cron) : void 0;
2767
+ if (!server) exitWithValidationError("server_id", "forge scheduled-jobs create --server <server_id> --command <command> --user <user> --frequency <frequency>", ctx.formatter);
2768
+ if (!command) exitWithValidationError("command", "forge scheduled-jobs create --server <server_id> --command <command> --user <user> --frequency <frequency>", ctx.formatter);
2586
2769
  await runCommand(async () => {
2587
2770
  const token = ctx.getToken();
2588
2771
  const execCtx = ctx.createExecutorContext(token);
2772
+ const server_id = await resolveServerId(server, execCtx);
2589
2773
  const result = await createScheduledJob({
2590
- server_id: await resolveServerId(server, execCtx),
2591
- command
2774
+ server_id,
2775
+ site_id: site ? await resolveSiteId(site, server_id, execCtx) : void 0,
2776
+ command,
2777
+ user,
2778
+ frequency,
2779
+ cron
2592
2780
  }, execCtx);
2593
2781
  ctx.formatter.outputOne(result.data, [
2594
2782
  "id",
@@ -2604,14 +2792,17 @@ async function scheduledJobsCreate(ctx) {
2604
2792
  async function scheduledJobsDelete(args, ctx) {
2605
2793
  const [id] = args;
2606
2794
  const server = String(ctx.options.server ?? "");
2795
+ const site = ctx.options.site ? String(ctx.options.site) : void 0;
2607
2796
  if (!id) exitWithValidationError("job_id", "forge scheduled-jobs delete <job_id> --server <server_id>", ctx.formatter);
2608
2797
  if (!server) exitWithValidationError("server_id", "forge scheduled-jobs delete <job_id> --server <server_id>", ctx.formatter);
2609
2798
  await runCommand(async () => {
2610
2799
  const token = ctx.getToken();
2611
2800
  const execCtx = ctx.createExecutorContext(token);
2801
+ const server_id = await resolveServerId(server, execCtx);
2612
2802
  await deleteScheduledJob({
2613
- server_id: await resolveServerId(server, execCtx),
2614
- id
2803
+ server_id,
2804
+ id,
2805
+ site_id: site ? await resolveSiteId(site, server_id, execCtx) : void 0
2615
2806
  }, execCtx);
2616
2807
  ctx.formatter.success(`Scheduled job ${id} deleted.`);
2617
2808
  }, ctx.formatter);
@@ -2725,7 +2916,6 @@ async function securityRulesGet(args, ctx) {
2725
2916
  "id",
2726
2917
  "name",
2727
2918
  "path",
2728
- "credentials",
2729
2919
  "created_at"
2730
2920
  ]);
2731
2921
  }, ctx.formatter);
@@ -2753,6 +2943,32 @@ async function securityRulesCreate(ctx) {
2753
2943
  ]);
2754
2944
  }, ctx.formatter);
2755
2945
  }
2946
+ async function securityRulesUpdate(args, ctx) {
2947
+ const [id] = args;
2948
+ const usage = "forge security-rules update <rule_id> --server <server_id> --site <site_id> --name <name>";
2949
+ if (!id) exitWithValidationError("rule_id", usage, ctx.formatter);
2950
+ const { server, site } = requireServerAndSiteRaw(ctx, usage);
2951
+ const name = String(ctx.options.name ?? "");
2952
+ if (!name) exitWithValidationError("name", usage, ctx.formatter);
2953
+ await runCommand(async () => {
2954
+ const token = ctx.getToken();
2955
+ const execCtx = ctx.createExecutorContext(token);
2956
+ const server_id = await resolveServerId(server, execCtx);
2957
+ const result = await updateSecurityRule({
2958
+ server_id,
2959
+ site_id: await resolveSiteId(site, server_id, execCtx),
2960
+ id,
2961
+ name,
2962
+ credentials: []
2963
+ }, execCtx);
2964
+ ctx.formatter.outputOne(result.data, [
2965
+ "id",
2966
+ "name",
2967
+ "path",
2968
+ "created_at"
2969
+ ]);
2970
+ }, ctx.formatter);
2971
+ }
2756
2972
  async function securityRulesDelete(args, ctx) {
2757
2973
  const [id] = args;
2758
2974
  const usage = "forge security-rules delete <rule_id> --server <server_id> --site <site_id>";
@@ -2777,9 +2993,14 @@ const handleSecurityRulesCommand = createCommandRouter({
2777
2993
  ls: securityRulesList,
2778
2994
  get: [securityRulesGet, "args"],
2779
2995
  create: securityRulesCreate,
2996
+ update: [securityRulesUpdate, "args"],
2780
2997
  delete: [securityRulesDelete, "args"]
2781
2998
  },
2782
- writeSubcommands: ["create", "delete"]
2999
+ writeSubcommands: [
3000
+ "create",
3001
+ "update",
3002
+ "delete"
3003
+ ]
2783
3004
  });
2784
3005
  function showSecurityRulesHelp(subcommand) {
2785
3006
  if (subcommand === "list" || subcommand === "ls") console.log(`
@@ -2805,6 +3026,18 @@ ${colors.bold("forge security-rules create")} - Create a new security rule
2805
3026
  ${colors.bold("USAGE:")}
2806
3027
  forge security-rules create --server <server_id> --site <site_id> --name <name> [options]
2807
3028
 
3029
+ ${colors.bold("OPTIONS:")}
3030
+ --server <id> Server ID (required)
3031
+ --site <id> Site ID (required)
3032
+ --name <name> Security rule name (required)
3033
+ -f, --format <fmt> Output format: json, human, table
3034
+ `);
3035
+ else if (subcommand === "update") console.log(`
3036
+ ${colors.bold("forge security-rules update")} - Update a security rule
3037
+
3038
+ ${colors.bold("USAGE:")}
3039
+ forge security-rules update <rule_id> --server <server_id> --site <site_id> --name <name> [options]
3040
+
2808
3041
  ${colors.bold("OPTIONS:")}
2809
3042
  --server <id> Server ID (required)
2810
3043
  --site <id> Site ID (required)
@@ -2827,6 +3060,7 @@ ${colors.bold("SUBCOMMANDS:")}
2827
3060
  list, ls List security rules
2828
3061
  get <id> Get security rule details
2829
3062
  create Create a new security rule
3063
+ update <id> Update a security rule
2830
3064
  delete <id> Delete a security rule
2831
3065
 
2832
3066
  ${colors.bold("OPTIONS:")}
@@ -2855,7 +3089,7 @@ async function serversList(ctx) {
2855
3089
  ctx.formatter.info("No servers found.");
2856
3090
  return;
2857
3091
  }
2858
- for (const server of result.data) console.log(`${String(server.id).padEnd(8)} ${server.name.padEnd(30)} ${server.ip_address.padEnd(16)} ${server.is_ready ? "ready" : "provisioning"}`);
3092
+ for (const server of result.data) console.log(`${String(server.id).padEnd(8)} ${server.name.padEnd(30)} ${(server.ip_address ?? "—").padEnd(16)} ${server.is_ready ? "ready" : "provisioning"}`);
2859
3093
  }
2860
3094
  }, ctx.formatter);
2861
3095
  }
@@ -2998,12 +3232,43 @@ async function sitesGet(args, ctx) {
2998
3232
  "name",
2999
3233
  "status",
3000
3234
  "php_version",
3001
- "project_type",
3235
+ "app_type",
3236
+ "repository",
3237
+ "quick_deploy",
3238
+ "deployment_status",
3239
+ "https",
3240
+ "created_at"
3241
+ ]);
3242
+ }, ctx.formatter);
3243
+ }
3244
+ async function sitesUpdate(args, ctx) {
3245
+ const [site_id] = args;
3246
+ const server = String(ctx.options.server ?? "");
3247
+ if (!site_id) exitWithValidationError("site_id", "forge sites update <site_id> --server <server_id>", ctx.formatter);
3248
+ if (!server) exitWithValidationError("server_id", "forge sites update <site_id> --server <server_id>", ctx.formatter);
3249
+ await runCommand(async () => {
3250
+ const token = ctx.getToken();
3251
+ const execCtx = ctx.createExecutorContext(token);
3252
+ const result = await updateSite({
3253
+ server_id: await resolveServerId(server, execCtx),
3254
+ site_id,
3255
+ root_path: ctx.options.root_path,
3256
+ directory: ctx.options.directory,
3257
+ type: ctx.options.type,
3258
+ php_version: ctx.options.php_version,
3259
+ push_to_deploy: ctx.options.push_to_deploy !== void 0 ? ctx.options.push_to_deploy === "true" || ctx.options.push_to_deploy === true : void 0,
3260
+ repository_branch: ctx.options.repository_branch
3261
+ }, execCtx);
3262
+ ctx.formatter.outputOne(result.data, [
3263
+ "id",
3264
+ "name",
3265
+ "status",
3266
+ "php_version",
3267
+ "app_type",
3002
3268
  "repository",
3003
- "repository_branch",
3004
3269
  "quick_deploy",
3005
3270
  "deployment_status",
3006
- "is_secured",
3271
+ "https",
3007
3272
  "created_at"
3008
3273
  ]);
3009
3274
  }, ctx.formatter);
@@ -3013,7 +3278,8 @@ const handleSitesCommand = createCommandRouter({
3013
3278
  handlers: {
3014
3279
  list: sitesList,
3015
3280
  ls: sitesList,
3016
- get: [sitesGet, "args"]
3281
+ get: [sitesGet, "args"],
3282
+ update: [sitesUpdate, "args"]
3017
3283
  }
3018
3284
  });
3019
3285
  function showSitesHelp(subcommand) {
@@ -3210,7 +3476,7 @@ function parseArgs(argv = process.argv.slice(2)) {
3210
3476
  positional
3211
3477
  };
3212
3478
  }
3213
- var VERSION = "0.3.0";
3479
+ var VERSION = "0.4.1";
3214
3480
  function showHelp() {
3215
3481
  console.log(`
3216
3482
  ${colors.bold("forge")} v${VERSION}
@@ -3260,8 +3526,8 @@ ${colors.bold("COMMANDS:")}
3260
3526
  get Get nginx config (requires --server --site)
3261
3527
  update Update nginx config (requires --server --site --content)
3262
3528
 
3263
- certificates, certs Manage SSL certificates
3264
- list, ls List certificates (requires --server --site)
3529
+ certificates, certs Manage SSL certificates (per-domain in v2)
3530
+ get Get certificate for a domain (requires --server --site --domain)
3265
3531
  get <id> Get certificate details (requires --server --site)
3266
3532
  activate <id> Activate a certificate (requires --server --site)
3267
3533
 
@@ -3330,8 +3596,10 @@ ${colors.bold("COMMANDS:")}
3330
3596
 
3331
3597
  ${colors.bold("OPTIONS:")}
3332
3598
  --token <token> Forge API token (overrides config and env)
3599
+ --org <slug> Organization slug (overrides config and FORGE_ORG)
3333
3600
  --server <id> Server ID (required for server-scoped commands)
3334
3601
  --site <id> Site ID (required for site-scoped commands)
3602
+ --domain <id> Domain record ID (for certificate commands)
3335
3603
  -f, --format <fmt> Output format: json, human, table (default: human)
3336
3604
  --no-color Disable colored output
3337
3605
  -h, --help Show help
@@ -3363,6 +3631,7 @@ ${colors.bold("CREDENTIAL PRIORITY:")}
3363
3631
 
3364
3632
  ${colors.bold("ENVIRONMENT VARIABLES:")}
3365
3633
  FORGE_API_TOKEN API token
3634
+ FORGE_ORG Organization slug (e.g. studio-meta)
3366
3635
  NO_COLOR Disable colors
3367
3636
  XDG_CONFIG_HOME Config directory (respects XDG spec)
3368
3637
  `);