@studiometa/forge-mcp 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +18 -0
- package/dist/auth.js.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/formatters.d.ts +187 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/handlers/backups.d.ts +2 -0
- package/dist/handlers/backups.d.ts.map +1 -0
- package/dist/handlers/certificates.d.ts +2 -0
- package/dist/handlers/certificates.d.ts.map +1 -0
- package/dist/handlers/commands.d.ts +2 -0
- package/dist/handlers/commands.d.ts.map +1 -0
- package/dist/handlers/daemons.d.ts +2 -0
- package/dist/handlers/daemons.d.ts.map +1 -0
- package/dist/handlers/database-users.d.ts +2 -0
- package/dist/handlers/database-users.d.ts.map +1 -0
- package/dist/handlers/databases.d.ts +2 -0
- package/dist/handlers/databases.d.ts.map +1 -0
- package/dist/handlers/deployments.d.ts +9 -0
- package/dist/handlers/deployments.d.ts.map +1 -0
- package/dist/handlers/env.d.ts +2 -0
- package/dist/handlers/env.d.ts.map +1 -0
- package/dist/handlers/factory.d.ts +71 -0
- package/dist/handlers/factory.d.ts.map +1 -0
- package/dist/handlers/firewall-rules.d.ts +2 -0
- package/dist/handlers/firewall-rules.d.ts.map +1 -0
- package/dist/handlers/help.d.ts +16 -0
- package/dist/handlers/help.d.ts.map +1 -0
- package/dist/handlers/index.d.ts +20 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/monitors.d.ts +2 -0
- package/dist/handlers/monitors.d.ts.map +1 -0
- package/dist/handlers/nginx-config.d.ts +2 -0
- package/dist/handlers/nginx-config.d.ts.map +1 -0
- package/dist/handlers/nginx-templates.d.ts +2 -0
- package/dist/handlers/nginx-templates.d.ts.map +1 -0
- package/dist/handlers/recipes.d.ts +2 -0
- package/dist/handlers/recipes.d.ts.map +1 -0
- package/dist/handlers/redirect-rules.d.ts +2 -0
- package/dist/handlers/redirect-rules.d.ts.map +1 -0
- package/dist/handlers/scheduled-jobs.d.ts +2 -0
- package/dist/handlers/scheduled-jobs.d.ts.map +1 -0
- package/dist/handlers/schema.d.ts +16 -0
- package/dist/handlers/schema.d.ts.map +1 -0
- package/dist/handlers/security-rules.d.ts +2 -0
- package/dist/handlers/security-rules.d.ts.map +1 -0
- package/dist/handlers/servers.d.ts +2 -0
- package/dist/handlers/servers.d.ts.map +1 -0
- package/dist/handlers/sites.d.ts +2 -0
- package/dist/handlers/sites.d.ts.map +1 -0
- package/dist/handlers/ssh-keys.d.ts +2 -0
- package/dist/handlers/ssh-keys.d.ts.map +1 -0
- package/dist/handlers/types.d.ts +38 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/user.d.ts +2 -0
- package/dist/handlers/user.d.ts.map +1 -0
- package/dist/handlers/utils.d.ts +29 -0
- package/dist/handlers/utils.d.ts.map +1 -0
- package/dist/hints.d.ts +60 -0
- package/dist/hints.d.ts.map +1 -0
- package/dist/http-CfjqK_e4.js +277 -0
- package/dist/http-CfjqK_e4.js.map +1 -0
- package/dist/http.d.ts +55 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +3 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/instructions.d.ts +11 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +76 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.d.ts +64 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/src-BdwavqrN.js +189 -0
- package/dist/src-BdwavqrN.js.map +1 -0
- package/dist/stdio.d.ts +36 -0
- package/dist/stdio.d.ts.map +1 -0
- package/dist/tools.d.ts +47 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/version-DaD5zvGh.js +3470 -0
- package/dist/version-DaD5zvGh.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +53 -1
- package/skills/SKILL.md +219 -0
|
@@ -0,0 +1,3470 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { HttpClient, isForgeApiError } from "@studiometa/forge-api";
|
|
5
|
+
import { RESOURCES, activateCertificate, createAuditLogger, createBackupConfig, createCertificate, createCommand, createDaemon, createDatabase, createDatabaseUser, createFirewallRule, createMonitor, createNginxTemplate, createRecipe, createRedirectRule, createScheduledJob, createSecurityRule, createServer, createSite, createSshKey, deleteBackupConfig, deleteCertificate, deleteDaemon, deleteDatabase, deleteDatabaseUser, deleteFirewallRule, deleteMonitor, deleteNginxTemplate, deleteRecipe, deleteRedirectRule, deleteScheduledJob, deleteSecurityRule, deleteServer, deleteSite, deleteSshKey, deploySiteAndWait, getBackupConfig, getCertificate, getCommand, getDaemon, getDatabase, getDatabaseUser, getDeploymentOutput, getDeploymentScript, 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, rebootServer, restartDaemon, runRecipe, updateDeploymentScript, updateEnv, updateNginxConfig, updateNginxTemplate } from "@studiometa/forge-core";
|
|
6
|
+
/**
|
|
7
|
+
* MCP Server Instructions
|
|
8
|
+
*
|
|
9
|
+
* These instructions are sent to MCP clients during initialization
|
|
10
|
+
* and used as context/hints for the LLM. Ensures the AI agent
|
|
11
|
+
* knows how to properly use the Forge MCP server.
|
|
12
|
+
*
|
|
13
|
+
* The content is derived from skills/SKILL.md (without YAML frontmatter).
|
|
14
|
+
*/
|
|
15
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
/**
|
|
17
|
+
* Load instructions from SKILL.md file.
|
|
18
|
+
* Removes YAML frontmatter (content between --- markers).
|
|
19
|
+
*/
|
|
20
|
+
function loadInstructions() {
|
|
21
|
+
try {
|
|
22
|
+
return readFileSync(join(__dirname, "..", "skills", "SKILL.md"), "utf-8").replace(/^---\n[\s\S]*?\n---\n+/, "").trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "Laravel Forge MCP Server - Use the forge tool with resource and action parameters. Call action=\"help\" for documentation.";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const INSTRUCTIONS = loadInstructions();
|
|
28
|
+
/**
|
|
29
|
+
* Format a list of servers.
|
|
30
|
+
*/
|
|
31
|
+
function formatServerList(servers) {
|
|
32
|
+
if (servers.length === 0) return "No servers found.";
|
|
33
|
+
const lines = servers.map((s) => `• ${s.name} (ID: ${s.id}) — ${s.provider} ${s.region} — ${s.ip_address} — ${s.is_ready ? "ready" : "provisioning"}`);
|
|
34
|
+
return `${servers.length} server(s):\n${lines.join("\n")}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format a single server.
|
|
38
|
+
*/
|
|
39
|
+
function formatServer(server) {
|
|
40
|
+
return [
|
|
41
|
+
`Server: ${server.name} (ID: ${server.id})`,
|
|
42
|
+
`Provider: ${server.provider} (${server.region})`,
|
|
43
|
+
`IP: ${server.ip_address}`,
|
|
44
|
+
`PHP: ${server.php_version}`,
|
|
45
|
+
`Ubuntu: ${server.ubuntu_version}`,
|
|
46
|
+
`Status: ${server.is_ready ? "ready" : "provisioning"}`,
|
|
47
|
+
`Created: ${server.created_at}`
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Format a list of sites.
|
|
52
|
+
*/
|
|
53
|
+
function formatSiteList(sites, serverId) {
|
|
54
|
+
if (sites.length === 0) return serverId ? `No sites on server ${serverId}.` : "No sites found.";
|
|
55
|
+
const lines = sites.map((s) => `• ${s.name} (ID: ${s.id}) — ${s.project_type} — ${s.status}`);
|
|
56
|
+
return `${serverId ? `${sites.length} site(s) on server ${serverId}:` : `${sites.length} site(s):`}\n${lines.join("\n")}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Format a single site.
|
|
60
|
+
*/
|
|
61
|
+
function formatSite(site) {
|
|
62
|
+
return [
|
|
63
|
+
`Site: ${site.name} (ID: ${site.id})`,
|
|
64
|
+
`Type: ${site.project_type}`,
|
|
65
|
+
`Directory: ${site.directory}`,
|
|
66
|
+
`Repository: ${site.repository ?? "none"}`,
|
|
67
|
+
`Branch: ${site.repository_branch ?? "none"}`,
|
|
68
|
+
`Status: ${site.status}`,
|
|
69
|
+
`Deploy status: ${site.deployment_status ?? "none"}`,
|
|
70
|
+
`Quick deploy: ${site.quick_deploy ? "enabled" : "disabled"}`,
|
|
71
|
+
`PHP: ${site.php_version}`,
|
|
72
|
+
`Created: ${site.created_at}`
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format a list of databases.
|
|
77
|
+
*/
|
|
78
|
+
function formatDatabaseList(databases) {
|
|
79
|
+
if (databases.length === 0) return "No databases found.";
|
|
80
|
+
const lines = databases.map((d) => `• ${d.name} (ID: ${d.id}) — ${d.status}`);
|
|
81
|
+
return `${databases.length} database(s):\n${lines.join("\n")}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Format a single database.
|
|
85
|
+
*/
|
|
86
|
+
function formatDatabase(db) {
|
|
87
|
+
return `Database: ${db.name} (ID: ${db.id})\nStatus: ${db.status}\nCreated: ${db.created_at}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Format a list of database users.
|
|
91
|
+
*/
|
|
92
|
+
function formatDatabaseUserList(users) {
|
|
93
|
+
if (users.length === 0) return "No database users found.";
|
|
94
|
+
const lines = users.map((u) => `• ${u.name} (ID: ${u.id}) — ${u.status}`);
|
|
95
|
+
return `${users.length} database user(s):\n${lines.join("\n")}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Format a single database user.
|
|
99
|
+
*/
|
|
100
|
+
function formatDatabaseUser(user) {
|
|
101
|
+
return [
|
|
102
|
+
`Database User: ${user.name} (ID: ${user.id})`,
|
|
103
|
+
`Status: ${user.status}`,
|
|
104
|
+
`Databases: ${user.databases.length > 0 ? user.databases.join(", ") : "none"}`,
|
|
105
|
+
`Created: ${user.created_at}`
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format a list of deployments.
|
|
110
|
+
*/
|
|
111
|
+
function formatDeploymentList(deployments) {
|
|
112
|
+
if (deployments.length === 0) return "No deployments found.";
|
|
113
|
+
const lines = deployments.map((d) => `• #${d.id} — ${d.status} — ${d.commit_hash?.slice(0, 7) ?? "no commit"} — ${d.started_at}`);
|
|
114
|
+
return `${deployments.length} deployment(s):\n${lines.join("\n")}`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Format a deployment action result.
|
|
118
|
+
*
|
|
119
|
+
* When a `DeployResult` is provided the output includes status, elapsed time,
|
|
120
|
+
* and the deployment log. When called with just IDs (legacy) it falls back to
|
|
121
|
+
* the simple confirmation message so existing tests keep passing.
|
|
122
|
+
*/
|
|
123
|
+
function formatDeployAction(siteId, serverId, result) {
|
|
124
|
+
if (!result) return `Deployment triggered for site ${siteId} on server ${serverId}.`;
|
|
125
|
+
const elapsedSec = (result.elapsed_ms / 1e3).toFixed(1);
|
|
126
|
+
const lines = [`Deployment ${result.status === "success" ? "✓ succeeded" : "✗ failed"} for site ${siteId} on server ${serverId} (${elapsedSec}s).`];
|
|
127
|
+
if (result.log) lines.push("", "Deployment log:", result.log);
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Format deployment script content.
|
|
132
|
+
*/
|
|
133
|
+
function formatDeploymentScript(script) {
|
|
134
|
+
return `Deployment script:\n${script}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Format deployment output.
|
|
138
|
+
*/
|
|
139
|
+
function formatDeploymentOutput(deploymentId, output) {
|
|
140
|
+
return `Deployment ${deploymentId} output:\n${output}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format a deployment script update confirmation.
|
|
144
|
+
*/
|
|
145
|
+
function formatDeploymentScriptUpdated(siteId, serverId) {
|
|
146
|
+
return `Deployment script updated for site ${siteId} on server ${serverId}.`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Format a list of certificates.
|
|
150
|
+
*/
|
|
151
|
+
function formatCertificateList(certificates) {
|
|
152
|
+
if (certificates.length === 0) return "No certificates found.";
|
|
153
|
+
const lines = certificates.map((c) => `• ${c.domain} (ID: ${c.id}) — ${c.type} — ${c.active ? "active" : "inactive"} — ${c.status}`);
|
|
154
|
+
return `${certificates.length} certificate(s):\n${lines.join("\n")}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Format a single certificate.
|
|
158
|
+
*/
|
|
159
|
+
function formatCertificate(cert) {
|
|
160
|
+
return `Certificate: ${cert.domain} (ID: ${cert.id})\nType: ${cert.type}\nStatus: ${cert.status}\nActive: ${cert.active}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Format a list of daemons.
|
|
164
|
+
*/
|
|
165
|
+
function formatDaemonList(daemons) {
|
|
166
|
+
if (daemons.length === 0) return "No daemons found.";
|
|
167
|
+
const lines = daemons.map((d) => `• ${d.command} (ID: ${d.id}) — user: ${d.user} — ${d.status}`);
|
|
168
|
+
return `${daemons.length} daemon(s):\n${lines.join("\n")}`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Format a single daemon.
|
|
172
|
+
*/
|
|
173
|
+
function formatDaemon(daemon) {
|
|
174
|
+
return `Daemon: ${daemon.command} (ID: ${daemon.id})\nUser: ${daemon.user}\nProcesses: ${daemon.processes}\nStatus: ${daemon.status}`;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Format a list of firewall rules.
|
|
178
|
+
*/
|
|
179
|
+
function formatFirewallRuleList(rules) {
|
|
180
|
+
if (rules.length === 0) return "No firewall rules found.";
|
|
181
|
+
const lines = rules.map((r) => `• ${r.name} (ID: ${r.id}) — port: ${r.port} — ${r.ip_address} — ${r.status}`);
|
|
182
|
+
return `${rules.length} firewall rule(s):\n${lines.join("\n")}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Format a single firewall rule.
|
|
186
|
+
*/
|
|
187
|
+
function formatFirewallRule(rule) {
|
|
188
|
+
return `Firewall Rule: ${rule.name} (ID: ${rule.id})\nPort: ${rule.port}\nType: ${rule.type}\nIP: ${rule.ip_address}\nStatus: ${rule.status}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Format a list of monitors.
|
|
192
|
+
*/
|
|
193
|
+
function formatMonitorList(monitors) {
|
|
194
|
+
if (monitors.length === 0) return "No monitors found.";
|
|
195
|
+
const lines = monitors.map((m) => `• ${m.type} ${m.operator} ${m.threshold} (ID: ${m.id}) — ${m.state}`);
|
|
196
|
+
return `${monitors.length} monitor(s):\n${lines.join("\n")}`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Format a single monitor.
|
|
200
|
+
*/
|
|
201
|
+
function formatMonitor(monitor) {
|
|
202
|
+
return `Monitor: ${monitor.type} ${monitor.operator} ${monitor.threshold} (ID: ${monitor.id})\nState: ${monitor.state}\nMinutes: ${monitor.minutes}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Format a list of SSH keys.
|
|
206
|
+
*/
|
|
207
|
+
function formatSshKeyList(keys) {
|
|
208
|
+
if (keys.length === 0) return "No SSH keys found.";
|
|
209
|
+
const lines = keys.map((k) => `• ${k.name} (ID: ${k.id}) — ${k.status}`);
|
|
210
|
+
return `${keys.length} SSH key(s):\n${lines.join("\n")}`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Format a single SSH key.
|
|
214
|
+
*/
|
|
215
|
+
function formatSshKey(key) {
|
|
216
|
+
return `SSH Key: ${key.name} (ID: ${key.id})\nStatus: ${key.status}\nCreated: ${key.created_at}`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Format a list of scheduled jobs.
|
|
220
|
+
*/
|
|
221
|
+
function formatScheduledJobList(jobs) {
|
|
222
|
+
if (jobs.length === 0) return "No scheduled jobs found.";
|
|
223
|
+
const lines = jobs.map((j) => `• ${j.command} (ID: ${j.id}) — ${j.frequency} — ${j.status} — user: ${j.user}`);
|
|
224
|
+
return `${jobs.length} scheduled job(s):\n${lines.join("\n")}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Format a single scheduled job.
|
|
228
|
+
*/
|
|
229
|
+
function formatScheduledJob(job) {
|
|
230
|
+
return [
|
|
231
|
+
`Job: ${job.command} (ID: ${job.id})`,
|
|
232
|
+
`User: ${job.user}`,
|
|
233
|
+
`Frequency: ${job.frequency}`,
|
|
234
|
+
`Cron: ${job.cron}`,
|
|
235
|
+
`Status: ${job.status}`,
|
|
236
|
+
`Created: ${job.created_at}`
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Format a list of security rules.
|
|
241
|
+
*/
|
|
242
|
+
function formatSecurityRuleList(rules) {
|
|
243
|
+
if (rules.length === 0) return "No security rules found.";
|
|
244
|
+
const lines = rules.map((r) => `• ${r.name} (ID: ${r.id}) — path: ${r.path ?? "/"}`);
|
|
245
|
+
return `${rules.length} security rule(s):\n${lines.join("\n")}`;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Format a single security rule.
|
|
249
|
+
*/
|
|
250
|
+
function formatSecurityRule(rule) {
|
|
251
|
+
return `Security Rule: ${rule.name} (ID: ${rule.id})\nPath: ${rule.path ?? "/"}`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Format a list of redirect rules.
|
|
255
|
+
*/
|
|
256
|
+
function formatRedirectRuleList(rules) {
|
|
257
|
+
if (rules.length === 0) return "No redirect rules found.";
|
|
258
|
+
const lines = rules.map((r) => `• ${r.from} → ${r.to} (ID: ${r.id}) — ${r.type}`);
|
|
259
|
+
return `${rules.length} redirect rule(s):\n${lines.join("\n")}`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Format a single redirect rule.
|
|
263
|
+
*/
|
|
264
|
+
function formatRedirectRule(rule) {
|
|
265
|
+
return `Redirect Rule: ${rule.from} → ${rule.to} (ID: ${rule.id})\nType: ${rule.type}`;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Format nginx configuration content.
|
|
269
|
+
*/
|
|
270
|
+
function formatNginxConfig(content) {
|
|
271
|
+
return `Nginx configuration:\n${content}`;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Format a list of nginx templates.
|
|
275
|
+
*/
|
|
276
|
+
function formatNginxTemplateList(templates) {
|
|
277
|
+
if (templates.length === 0) return "No nginx templates found.";
|
|
278
|
+
const lines = templates.map((t) => `• ${t.name} (ID: ${t.id})`);
|
|
279
|
+
return `${templates.length} nginx template(s):\n${lines.join("\n")}`;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Format a single nginx template.
|
|
283
|
+
*/
|
|
284
|
+
function formatNginxTemplate(template) {
|
|
285
|
+
return `Nginx Template: ${template.name} (ID: ${template.id})\n\n${template.content}`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Format a list of backup configurations.
|
|
289
|
+
*/
|
|
290
|
+
function formatBackupConfigList(backups) {
|
|
291
|
+
if (backups.length === 0) return "No backup configurations found.";
|
|
292
|
+
const lines = backups.map((b) => `• ${b.provider_name} (ID: ${b.id}) — ${b.frequency} — ${b.status} — last: ${b.last_backup_time ?? "never"}`);
|
|
293
|
+
return `${backups.length} backup config(s):\n${lines.join("\n")}`;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Format a single backup configuration.
|
|
297
|
+
*/
|
|
298
|
+
function formatBackupConfig(backup) {
|
|
299
|
+
return [
|
|
300
|
+
`Backup Config: ${backup.provider_name} (ID: ${backup.id})`,
|
|
301
|
+
`Frequency: ${backup.frequency}`,
|
|
302
|
+
`Status: ${backup.status}`,
|
|
303
|
+
`Retention: ${backup.retention} backups`,
|
|
304
|
+
`Databases: ${backup.databases.map((d) => d.name).join(", ") || "none"}`,
|
|
305
|
+
`Last backup: ${backup.last_backup_time ?? "never"}`
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Format a list of recipes.
|
|
310
|
+
*/
|
|
311
|
+
function formatRecipeList(recipes) {
|
|
312
|
+
if (recipes.length === 0) return "No recipes found.";
|
|
313
|
+
const lines = recipes.map((r) => `• ${r.name} (ID: ${r.id}) — user: ${r.user}`);
|
|
314
|
+
return `${recipes.length} recipe(s):\n${lines.join("\n")}`;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Format a single recipe.
|
|
318
|
+
*/
|
|
319
|
+
function formatRecipe(recipe) {
|
|
320
|
+
return `Recipe: ${recipe.name} (ID: ${recipe.id})\nUser: ${recipe.user}\nScript:\n${recipe.script}`;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Format a list of commands.
|
|
324
|
+
*/
|
|
325
|
+
function formatCommandList(commands) {
|
|
326
|
+
if (commands.length === 0) return "No commands found.";
|
|
327
|
+
const lines = commands.map((c) => `• #${c.id} — ${c.status} — ${c.user_name} — ${c.command.slice(0, 60)}`);
|
|
328
|
+
return `${commands.length} command(s):\n${lines.join("\n")}`;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Format a single command.
|
|
332
|
+
*/
|
|
333
|
+
function formatCommand(command) {
|
|
334
|
+
return [
|
|
335
|
+
`Command #${command.id}`,
|
|
336
|
+
`Command: ${command.command}`,
|
|
337
|
+
`Status: ${command.status}`,
|
|
338
|
+
`User: ${command.user_name}`,
|
|
339
|
+
`Created: ${command.created_at}`
|
|
340
|
+
].join("\n");
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Format environment variables content.
|
|
344
|
+
*/
|
|
345
|
+
function formatEnv(content) {
|
|
346
|
+
return `Environment variables:\n${content}`;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Format the authenticated user.
|
|
350
|
+
*/
|
|
351
|
+
function formatUser(user) {
|
|
352
|
+
return [
|
|
353
|
+
`User: ${user.name} (ID: ${user.id})`,
|
|
354
|
+
`Email: ${user.email}`,
|
|
355
|
+
`GitHub: ${user.connected_to_github ? "connected" : "not connected"}`,
|
|
356
|
+
`GitLab: ${user.connected_to_gitlab ? "connected" : "not connected"}`,
|
|
357
|
+
`2FA: ${user.two_factor_enabled ? "enabled" : "disabled"}`
|
|
358
|
+
].join("\n");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Custom error classes for MCP server
|
|
362
|
+
*
|
|
363
|
+
* These provide structured error handling with LLM-friendly messages
|
|
364
|
+
* that include guidance on how to resolve issues.
|
|
365
|
+
*/
|
|
366
|
+
/**
|
|
367
|
+
* Error thrown when user input validation fails.
|
|
368
|
+
* These errors should be returned to the user directly.
|
|
369
|
+
*
|
|
370
|
+
* Includes optional hints for how to resolve the issue.
|
|
371
|
+
*/
|
|
372
|
+
var UserInputError = class extends Error {
|
|
373
|
+
hints;
|
|
374
|
+
constructor(message, hints) {
|
|
375
|
+
super(message);
|
|
376
|
+
this.name = "UserInputError";
|
|
377
|
+
this.hints = hints;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Format error message with hints for LLM consumption
|
|
381
|
+
*/
|
|
382
|
+
toFormattedMessage() {
|
|
383
|
+
let msg = `**Input Error:** ${this.message}`;
|
|
384
|
+
if (this.hints && this.hints.length > 0) msg += "\n\n**Hints:**\n" + this.hints.map((h) => `- ${h}`).join("\n");
|
|
385
|
+
return msg;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Check if an error is a UserInputError
|
|
390
|
+
*/
|
|
391
|
+
function isUserInputError(error) {
|
|
392
|
+
return error instanceof UserInputError;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Create a successful result with both human-readable text and structured content.
|
|
396
|
+
*
|
|
397
|
+
* - When `data` is a string, `structuredContent` wraps it as `{ result: data }`.
|
|
398
|
+
* - When `data` is an object/array, `structuredContent` is `{ result: data }` and
|
|
399
|
+
* the text representation is the JSON-serialized form.
|
|
400
|
+
*/
|
|
401
|
+
function jsonResult(data) {
|
|
402
|
+
return {
|
|
403
|
+
content: [{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
|
|
406
|
+
}],
|
|
407
|
+
structuredContent: {
|
|
408
|
+
success: true,
|
|
409
|
+
result: data
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Validate an ID-like value (must be alphanumeric/dashes only).
|
|
415
|
+
* Prevents path traversal via `../` in URL segments.
|
|
416
|
+
*
|
|
417
|
+
* @returns true if the value is safe, false otherwise.
|
|
418
|
+
*/
|
|
419
|
+
function sanitizeId(value) {
|
|
420
|
+
return /^[\w-]+$/.test(value);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Create an error result with structured error content.
|
|
424
|
+
*/
|
|
425
|
+
function errorResult(message) {
|
|
426
|
+
return {
|
|
427
|
+
content: [{
|
|
428
|
+
type: "text",
|
|
429
|
+
text: `Error: ${message}`
|
|
430
|
+
}],
|
|
431
|
+
structuredContent: {
|
|
432
|
+
success: false,
|
|
433
|
+
error: message
|
|
434
|
+
},
|
|
435
|
+
isError: true
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Create a resource handler from configuration.
|
|
440
|
+
*
|
|
441
|
+
* Returns a function that routes actions to the correct executor,
|
|
442
|
+
* validates required fields, and formats results.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```typescript
|
|
446
|
+
* export const handleDatabases = createResourceHandler({
|
|
447
|
+
* resource: 'databases',
|
|
448
|
+
* actions: ['list', 'get', 'create', 'delete'],
|
|
449
|
+
* requiredFields: {
|
|
450
|
+
* list: ['server_id'],
|
|
451
|
+
* get: ['server_id', 'id'],
|
|
452
|
+
* create: ['server_id', 'name'],
|
|
453
|
+
* delete: ['server_id', 'id'],
|
|
454
|
+
* },
|
|
455
|
+
* executors: {
|
|
456
|
+
* list: listDatabases,
|
|
457
|
+
* get: getDatabase,
|
|
458
|
+
* create: createDatabase,
|
|
459
|
+
* delete: deleteDatabase,
|
|
460
|
+
* },
|
|
461
|
+
* formatResult: (action, data) => {
|
|
462
|
+
* if (action === 'list') return formatDatabaseList(data);
|
|
463
|
+
* if (action === 'get') return formatDatabase(data);
|
|
464
|
+
* return 'Done.';
|
|
465
|
+
* },
|
|
466
|
+
* });
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
function createResourceHandler(config) {
|
|
470
|
+
const { resource, actions, requiredFields = {}, executors, hints, mapOptions, formatResult } = config;
|
|
471
|
+
return async (action, args, ctx) => {
|
|
472
|
+
if (!actions.includes(action)) return errorResult(`Unknown action "${action}" for ${resource}. Valid actions: ${actions.join(", ")}.`);
|
|
473
|
+
const required = requiredFields[action] ?? [];
|
|
474
|
+
for (const field of required) if (!args[field]) return errorResult(`Missing required field: ${field}`);
|
|
475
|
+
for (const field of [
|
|
476
|
+
"id",
|
|
477
|
+
"server_id",
|
|
478
|
+
"site_id"
|
|
479
|
+
]) {
|
|
480
|
+
const value = args[field];
|
|
481
|
+
if (value !== void 0 && !sanitizeId(String(value))) return errorResult(`Invalid ${field}: "${value}". IDs must be alphanumeric.`);
|
|
482
|
+
}
|
|
483
|
+
const executor = executors[action];
|
|
484
|
+
if (!executor) return errorResult(`Action "${action}" is not yet implemented for ${resource}.`);
|
|
485
|
+
let options;
|
|
486
|
+
if (mapOptions) options = mapOptions(action, args);
|
|
487
|
+
else {
|
|
488
|
+
const { resource: _r, action: _a, compact: _c, ...rest } = args;
|
|
489
|
+
options = rest;
|
|
490
|
+
}
|
|
491
|
+
const result = await executor(options, ctx.executorContext);
|
|
492
|
+
if (result.data === void 0) return jsonResult(formatResult ? formatResult(action, void 0, args) : "Done.");
|
|
493
|
+
if (ctx.compact && formatResult) return jsonResult(formatResult(action, result.data, args));
|
|
494
|
+
if (action === "get" && ctx.includeHints && hints) {
|
|
495
|
+
/* v8 ignore start */
|
|
496
|
+
const id = args.id ?? args.server_id ?? "";
|
|
497
|
+
return jsonResult({
|
|
498
|
+
...result.data,
|
|
499
|
+
_hints: hints(result.data, String(id))
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return jsonResult(result.data);
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const handleBackups = createResourceHandler({
|
|
506
|
+
resource: "backups",
|
|
507
|
+
actions: [
|
|
508
|
+
"list",
|
|
509
|
+
"get",
|
|
510
|
+
"create",
|
|
511
|
+
"delete"
|
|
512
|
+
],
|
|
513
|
+
requiredFields: {
|
|
514
|
+
list: ["server_id"],
|
|
515
|
+
get: ["server_id", "id"],
|
|
516
|
+
create: [
|
|
517
|
+
"server_id",
|
|
518
|
+
"provider",
|
|
519
|
+
"credentials",
|
|
520
|
+
"frequency",
|
|
521
|
+
"databases"
|
|
522
|
+
],
|
|
523
|
+
delete: ["server_id", "id"]
|
|
524
|
+
},
|
|
525
|
+
executors: {
|
|
526
|
+
list: listBackupConfigs,
|
|
527
|
+
get: getBackupConfig,
|
|
528
|
+
create: createBackupConfig,
|
|
529
|
+
delete: deleteBackupConfig
|
|
530
|
+
},
|
|
531
|
+
formatResult: (action, data, args) => {
|
|
532
|
+
switch (action) {
|
|
533
|
+
case "list": return formatBackupConfigList(data);
|
|
534
|
+
case "get": return formatBackupConfig(data);
|
|
535
|
+
case "create": return formatBackupConfig(data);
|
|
536
|
+
case "delete": return `Backup config ${args.id} deleted.`;
|
|
537
|
+
default: return "Done.";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
/**
|
|
542
|
+
* Hints after getting a server.
|
|
543
|
+
*/
|
|
544
|
+
function getServerHints(serverId) {
|
|
545
|
+
return {
|
|
546
|
+
related_resources: [
|
|
547
|
+
{
|
|
548
|
+
resource: "sites",
|
|
549
|
+
description: "List sites on this server",
|
|
550
|
+
example: {
|
|
551
|
+
resource: "sites",
|
|
552
|
+
action: "list",
|
|
553
|
+
server_id: serverId
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
resource: "databases",
|
|
558
|
+
description: "List databases",
|
|
559
|
+
example: {
|
|
560
|
+
resource: "databases",
|
|
561
|
+
action: "list",
|
|
562
|
+
server_id: serverId
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
resource: "daemons",
|
|
567
|
+
description: "List background processes",
|
|
568
|
+
example: {
|
|
569
|
+
resource: "daemons",
|
|
570
|
+
action: "list",
|
|
571
|
+
server_id: serverId
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
resource: "firewall-rules",
|
|
576
|
+
description: "List firewall rules",
|
|
577
|
+
example: {
|
|
578
|
+
resource: "firewall-rules",
|
|
579
|
+
action: "list",
|
|
580
|
+
server_id: serverId
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
resource: "ssh-keys",
|
|
585
|
+
description: "List SSH keys on this server",
|
|
586
|
+
example: {
|
|
587
|
+
resource: "ssh-keys",
|
|
588
|
+
action: "list",
|
|
589
|
+
server_id: serverId
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
common_actions: [{
|
|
594
|
+
action: "Reboot server",
|
|
595
|
+
example: {
|
|
596
|
+
resource: "servers",
|
|
597
|
+
action: "reboot",
|
|
598
|
+
id: serverId
|
|
599
|
+
}
|
|
600
|
+
}]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Hints after getting a site.
|
|
605
|
+
*/
|
|
606
|
+
function getSiteHints(serverId, siteId) {
|
|
607
|
+
return {
|
|
608
|
+
related_resources: [
|
|
609
|
+
{
|
|
610
|
+
resource: "deployments",
|
|
611
|
+
description: "List deployments for this site",
|
|
612
|
+
example: {
|
|
613
|
+
resource: "deployments",
|
|
614
|
+
action: "list",
|
|
615
|
+
server_id: serverId,
|
|
616
|
+
site_id: siteId
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
resource: "env",
|
|
621
|
+
description: "Get environment variables",
|
|
622
|
+
example: {
|
|
623
|
+
resource: "env",
|
|
624
|
+
action: "get",
|
|
625
|
+
server_id: serverId,
|
|
626
|
+
site_id: siteId
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
resource: "certificates",
|
|
631
|
+
description: "List SSL certificates",
|
|
632
|
+
example: {
|
|
633
|
+
resource: "certificates",
|
|
634
|
+
action: "list",
|
|
635
|
+
server_id: serverId,
|
|
636
|
+
site_id: siteId
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
resource: "nginx",
|
|
641
|
+
description: "Get nginx configuration",
|
|
642
|
+
example: {
|
|
643
|
+
resource: "nginx",
|
|
644
|
+
action: "get",
|
|
645
|
+
server_id: serverId,
|
|
646
|
+
site_id: siteId
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
common_actions: [{
|
|
651
|
+
action: "Deploy this site",
|
|
652
|
+
example: {
|
|
653
|
+
resource: "deployments",
|
|
654
|
+
action: "deploy",
|
|
655
|
+
server_id: serverId,
|
|
656
|
+
site_id: siteId
|
|
657
|
+
}
|
|
658
|
+
}]
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Hints after getting a database.
|
|
663
|
+
*/
|
|
664
|
+
function getDatabaseHints(serverId, databaseId) {
|
|
665
|
+
return {
|
|
666
|
+
related_resources: [{
|
|
667
|
+
resource: "databases",
|
|
668
|
+
description: "List all databases on this server",
|
|
669
|
+
example: {
|
|
670
|
+
resource: "databases",
|
|
671
|
+
action: "list",
|
|
672
|
+
server_id: serverId
|
|
673
|
+
}
|
|
674
|
+
}],
|
|
675
|
+
common_actions: [{
|
|
676
|
+
action: "Delete this database",
|
|
677
|
+
example: {
|
|
678
|
+
resource: "databases",
|
|
679
|
+
action: "delete",
|
|
680
|
+
server_id: serverId,
|
|
681
|
+
id: databaseId
|
|
682
|
+
}
|
|
683
|
+
}]
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Hints after getting a database user.
|
|
688
|
+
*/
|
|
689
|
+
function getDatabaseUserHints(serverId, userId) {
|
|
690
|
+
return {
|
|
691
|
+
related_resources: [{
|
|
692
|
+
resource: "database-users",
|
|
693
|
+
description: "List all database users on this server",
|
|
694
|
+
example: {
|
|
695
|
+
resource: "database-users",
|
|
696
|
+
action: "list",
|
|
697
|
+
server_id: serverId
|
|
698
|
+
}
|
|
699
|
+
}, {
|
|
700
|
+
resource: "databases",
|
|
701
|
+
description: "List databases on this server",
|
|
702
|
+
example: {
|
|
703
|
+
resource: "databases",
|
|
704
|
+
action: "list",
|
|
705
|
+
server_id: serverId
|
|
706
|
+
}
|
|
707
|
+
}],
|
|
708
|
+
common_actions: [{
|
|
709
|
+
action: "Delete this database user",
|
|
710
|
+
example: {
|
|
711
|
+
resource: "database-users",
|
|
712
|
+
action: "delete",
|
|
713
|
+
server_id: serverId,
|
|
714
|
+
id: userId
|
|
715
|
+
}
|
|
716
|
+
}]
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Hints after getting a daemon.
|
|
721
|
+
*/
|
|
722
|
+
function getDaemonHints(serverId, daemonId) {
|
|
723
|
+
return {
|
|
724
|
+
related_resources: [{
|
|
725
|
+
resource: "daemons",
|
|
726
|
+
description: "List all daemons on this server",
|
|
727
|
+
example: {
|
|
728
|
+
resource: "daemons",
|
|
729
|
+
action: "list",
|
|
730
|
+
server_id: serverId
|
|
731
|
+
}
|
|
732
|
+
}],
|
|
733
|
+
common_actions: [{
|
|
734
|
+
action: "Restart this daemon",
|
|
735
|
+
example: {
|
|
736
|
+
resource: "daemons",
|
|
737
|
+
action: "restart",
|
|
738
|
+
server_id: serverId,
|
|
739
|
+
id: daemonId
|
|
740
|
+
}
|
|
741
|
+
}, {
|
|
742
|
+
action: "Delete this daemon",
|
|
743
|
+
example: {
|
|
744
|
+
resource: "daemons",
|
|
745
|
+
action: "delete",
|
|
746
|
+
server_id: serverId,
|
|
747
|
+
id: daemonId
|
|
748
|
+
}
|
|
749
|
+
}]
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Hints after getting a certificate.
|
|
754
|
+
*/
|
|
755
|
+
function getCertificateHints(serverId, siteId, certificateId) {
|
|
756
|
+
return {
|
|
757
|
+
related_resources: [{
|
|
758
|
+
resource: "certificates",
|
|
759
|
+
description: "List all certificates for this site",
|
|
760
|
+
example: {
|
|
761
|
+
resource: "certificates",
|
|
762
|
+
action: "list",
|
|
763
|
+
server_id: serverId,
|
|
764
|
+
site_id: siteId
|
|
765
|
+
}
|
|
766
|
+
}],
|
|
767
|
+
common_actions: [{
|
|
768
|
+
action: "Activate this certificate",
|
|
769
|
+
example: {
|
|
770
|
+
resource: "certificates",
|
|
771
|
+
action: "activate",
|
|
772
|
+
server_id: serverId,
|
|
773
|
+
site_id: siteId,
|
|
774
|
+
id: certificateId
|
|
775
|
+
}
|
|
776
|
+
}, {
|
|
777
|
+
action: "Delete this certificate",
|
|
778
|
+
example: {
|
|
779
|
+
resource: "certificates",
|
|
780
|
+
action: "delete",
|
|
781
|
+
server_id: serverId,
|
|
782
|
+
site_id: siteId,
|
|
783
|
+
id: certificateId
|
|
784
|
+
}
|
|
785
|
+
}]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Hints after getting a firewall rule.
|
|
790
|
+
*/
|
|
791
|
+
function getFirewallRuleHints(serverId, ruleId) {
|
|
792
|
+
return {
|
|
793
|
+
related_resources: [{
|
|
794
|
+
resource: "firewall-rules",
|
|
795
|
+
description: "List all firewall rules on this server",
|
|
796
|
+
example: {
|
|
797
|
+
resource: "firewall-rules",
|
|
798
|
+
action: "list",
|
|
799
|
+
server_id: serverId
|
|
800
|
+
}
|
|
801
|
+
}],
|
|
802
|
+
common_actions: [{
|
|
803
|
+
action: "Delete this firewall rule",
|
|
804
|
+
example: {
|
|
805
|
+
resource: "firewall-rules",
|
|
806
|
+
action: "delete",
|
|
807
|
+
server_id: serverId,
|
|
808
|
+
id: ruleId
|
|
809
|
+
}
|
|
810
|
+
}]
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Hints after getting an SSH key.
|
|
815
|
+
*/
|
|
816
|
+
function getSshKeyHints(serverId, keyId) {
|
|
817
|
+
return {
|
|
818
|
+
related_resources: [{
|
|
819
|
+
resource: "ssh-keys",
|
|
820
|
+
description: "List all SSH keys on this server",
|
|
821
|
+
example: {
|
|
822
|
+
resource: "ssh-keys",
|
|
823
|
+
action: "list",
|
|
824
|
+
server_id: serverId
|
|
825
|
+
}
|
|
826
|
+
}],
|
|
827
|
+
common_actions: [{
|
|
828
|
+
action: "Delete this SSH key",
|
|
829
|
+
example: {
|
|
830
|
+
resource: "ssh-keys",
|
|
831
|
+
action: "delete",
|
|
832
|
+
server_id: serverId,
|
|
833
|
+
id: keyId
|
|
834
|
+
}
|
|
835
|
+
}]
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Hints after getting a recipe.
|
|
840
|
+
*/
|
|
841
|
+
function getRecipeHints(recipeId) {
|
|
842
|
+
return {
|
|
843
|
+
related_resources: [{
|
|
844
|
+
resource: "recipes",
|
|
845
|
+
description: "List all recipes",
|
|
846
|
+
example: {
|
|
847
|
+
resource: "recipes",
|
|
848
|
+
action: "list"
|
|
849
|
+
}
|
|
850
|
+
}],
|
|
851
|
+
common_actions: [{
|
|
852
|
+
action: "Run this recipe on servers",
|
|
853
|
+
example: {
|
|
854
|
+
resource: "recipes",
|
|
855
|
+
action: "run",
|
|
856
|
+
id: recipeId,
|
|
857
|
+
servers: []
|
|
858
|
+
}
|
|
859
|
+
}, {
|
|
860
|
+
action: "Delete this recipe",
|
|
861
|
+
example: {
|
|
862
|
+
resource: "recipes",
|
|
863
|
+
action: "delete",
|
|
864
|
+
id: recipeId
|
|
865
|
+
}
|
|
866
|
+
}]
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Hints after getting an nginx template.
|
|
871
|
+
*/
|
|
872
|
+
function getNginxTemplateHints(serverId, templateId) {
|
|
873
|
+
return {
|
|
874
|
+
related_resources: [{
|
|
875
|
+
resource: "nginx-templates",
|
|
876
|
+
description: "List all nginx templates on this server",
|
|
877
|
+
example: {
|
|
878
|
+
resource: "nginx-templates",
|
|
879
|
+
action: "list",
|
|
880
|
+
server_id: serverId
|
|
881
|
+
}
|
|
882
|
+
}],
|
|
883
|
+
common_actions: [{
|
|
884
|
+
action: "Update this nginx template",
|
|
885
|
+
example: {
|
|
886
|
+
resource: "nginx-templates",
|
|
887
|
+
action: "update",
|
|
888
|
+
server_id: serverId,
|
|
889
|
+
id: templateId,
|
|
890
|
+
content: "<nginx config content>"
|
|
891
|
+
}
|
|
892
|
+
}, {
|
|
893
|
+
action: "Delete this nginx template",
|
|
894
|
+
example: {
|
|
895
|
+
resource: "nginx-templates",
|
|
896
|
+
action: "delete",
|
|
897
|
+
server_id: serverId,
|
|
898
|
+
id: templateId
|
|
899
|
+
}
|
|
900
|
+
}]
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const handleCertificates = createResourceHandler({
|
|
904
|
+
resource: "certificates",
|
|
905
|
+
actions: [
|
|
906
|
+
"list",
|
|
907
|
+
"get",
|
|
908
|
+
"create",
|
|
909
|
+
"delete",
|
|
910
|
+
"activate"
|
|
911
|
+
],
|
|
912
|
+
requiredFields: {
|
|
913
|
+
list: ["server_id", "site_id"],
|
|
914
|
+
get: [
|
|
915
|
+
"server_id",
|
|
916
|
+
"site_id",
|
|
917
|
+
"id"
|
|
918
|
+
],
|
|
919
|
+
create: [
|
|
920
|
+
"server_id",
|
|
921
|
+
"site_id",
|
|
922
|
+
"domain"
|
|
923
|
+
],
|
|
924
|
+
delete: [
|
|
925
|
+
"server_id",
|
|
926
|
+
"site_id",
|
|
927
|
+
"id"
|
|
928
|
+
],
|
|
929
|
+
activate: [
|
|
930
|
+
"server_id",
|
|
931
|
+
"site_id",
|
|
932
|
+
"id"
|
|
933
|
+
]
|
|
934
|
+
},
|
|
935
|
+
executors: {
|
|
936
|
+
list: listCertificates,
|
|
937
|
+
get: getCertificate,
|
|
938
|
+
create: createCertificate,
|
|
939
|
+
delete: deleteCertificate,
|
|
940
|
+
activate: activateCertificate
|
|
941
|
+
},
|
|
942
|
+
hints: (data, id) => {
|
|
943
|
+
const cert = data;
|
|
944
|
+
return getCertificateHints(String(cert.server_id), String(cert.site_id), id);
|
|
945
|
+
},
|
|
946
|
+
formatResult: (action, data, args) => {
|
|
947
|
+
switch (action) {
|
|
948
|
+
case "list": return formatCertificateList(data);
|
|
949
|
+
case "get": return formatCertificate(data);
|
|
950
|
+
case "create": return formatCertificate(data);
|
|
951
|
+
case "delete": return `Certificate ${args.id} deleted.`;
|
|
952
|
+
case "activate": return `Certificate ${args.id} activated.`;
|
|
953
|
+
default: return "Done.";
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
const handleCommands = createResourceHandler({
|
|
958
|
+
resource: "commands",
|
|
959
|
+
actions: [
|
|
960
|
+
"list",
|
|
961
|
+
"get",
|
|
962
|
+
"create"
|
|
963
|
+
],
|
|
964
|
+
requiredFields: {
|
|
965
|
+
list: ["server_id", "site_id"],
|
|
966
|
+
get: [
|
|
967
|
+
"server_id",
|
|
968
|
+
"site_id",
|
|
969
|
+
"id"
|
|
970
|
+
],
|
|
971
|
+
create: [
|
|
972
|
+
"server_id",
|
|
973
|
+
"site_id",
|
|
974
|
+
"command"
|
|
975
|
+
]
|
|
976
|
+
},
|
|
977
|
+
executors: {
|
|
978
|
+
list: listCommands,
|
|
979
|
+
get: getCommand,
|
|
980
|
+
create: createCommand
|
|
981
|
+
},
|
|
982
|
+
formatResult: (action, data) => {
|
|
983
|
+
switch (action) {
|
|
984
|
+
case "list": return formatCommandList(data);
|
|
985
|
+
case "get": return formatCommand(data);
|
|
986
|
+
case "create": return formatCommand(data);
|
|
987
|
+
default: return "Done.";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
const handleDaemons = createResourceHandler({
|
|
992
|
+
resource: "daemons",
|
|
993
|
+
actions: [
|
|
994
|
+
"list",
|
|
995
|
+
"get",
|
|
996
|
+
"create",
|
|
997
|
+
"delete",
|
|
998
|
+
"restart"
|
|
999
|
+
],
|
|
1000
|
+
requiredFields: {
|
|
1001
|
+
list: ["server_id"],
|
|
1002
|
+
get: ["server_id", "id"],
|
|
1003
|
+
create: ["server_id", "command"],
|
|
1004
|
+
delete: ["server_id", "id"],
|
|
1005
|
+
restart: ["server_id", "id"]
|
|
1006
|
+
},
|
|
1007
|
+
executors: {
|
|
1008
|
+
list: listDaemons,
|
|
1009
|
+
get: getDaemon,
|
|
1010
|
+
create: createDaemon,
|
|
1011
|
+
delete: deleteDaemon,
|
|
1012
|
+
restart: restartDaemon
|
|
1013
|
+
},
|
|
1014
|
+
hints: (data, id) => {
|
|
1015
|
+
const daemon = data;
|
|
1016
|
+
return getDaemonHints(String(daemon.server_id), id);
|
|
1017
|
+
},
|
|
1018
|
+
formatResult: (action, data, args) => {
|
|
1019
|
+
switch (action) {
|
|
1020
|
+
case "list": return formatDaemonList(data);
|
|
1021
|
+
case "get": return formatDaemon(data);
|
|
1022
|
+
case "create": return formatDaemon(data);
|
|
1023
|
+
case "delete": return `Daemon ${args.id} deleted.`;
|
|
1024
|
+
case "restart": return `Daemon ${args.id} restarted.`;
|
|
1025
|
+
default: return "Done.";
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
const handleDatabases = createResourceHandler({
|
|
1030
|
+
resource: "databases",
|
|
1031
|
+
actions: [
|
|
1032
|
+
"list",
|
|
1033
|
+
"get",
|
|
1034
|
+
"create",
|
|
1035
|
+
"delete"
|
|
1036
|
+
],
|
|
1037
|
+
requiredFields: {
|
|
1038
|
+
list: ["server_id"],
|
|
1039
|
+
get: ["server_id", "id"],
|
|
1040
|
+
create: ["server_id", "name"],
|
|
1041
|
+
delete: ["server_id", "id"]
|
|
1042
|
+
},
|
|
1043
|
+
executors: {
|
|
1044
|
+
list: listDatabases,
|
|
1045
|
+
get: getDatabase,
|
|
1046
|
+
create: createDatabase,
|
|
1047
|
+
delete: deleteDatabase
|
|
1048
|
+
},
|
|
1049
|
+
hints: (data, id) => {
|
|
1050
|
+
const db = data;
|
|
1051
|
+
return getDatabaseHints(String(db.server_id), id);
|
|
1052
|
+
},
|
|
1053
|
+
formatResult: (action, data, args) => {
|
|
1054
|
+
switch (action) {
|
|
1055
|
+
case "list": return formatDatabaseList(data);
|
|
1056
|
+
case "get": return formatDatabase(data);
|
|
1057
|
+
case "create": return formatDatabase(data);
|
|
1058
|
+
case "delete": return `Database ${args.id} deleted.`;
|
|
1059
|
+
default: return "Done.";
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
const handleDatabaseUsers = createResourceHandler({
|
|
1064
|
+
resource: "database-users",
|
|
1065
|
+
actions: [
|
|
1066
|
+
"list",
|
|
1067
|
+
"get",
|
|
1068
|
+
"create",
|
|
1069
|
+
"delete"
|
|
1070
|
+
],
|
|
1071
|
+
requiredFields: {
|
|
1072
|
+
list: ["server_id"],
|
|
1073
|
+
get: ["server_id", "id"],
|
|
1074
|
+
create: [
|
|
1075
|
+
"server_id",
|
|
1076
|
+
"name",
|
|
1077
|
+
"password"
|
|
1078
|
+
],
|
|
1079
|
+
delete: ["server_id", "id"]
|
|
1080
|
+
},
|
|
1081
|
+
executors: {
|
|
1082
|
+
list: listDatabaseUsers,
|
|
1083
|
+
get: getDatabaseUser,
|
|
1084
|
+
create: createDatabaseUser,
|
|
1085
|
+
delete: deleteDatabaseUser
|
|
1086
|
+
},
|
|
1087
|
+
hints: (data, id) => {
|
|
1088
|
+
const user = data;
|
|
1089
|
+
return getDatabaseUserHints(String(user.server_id), id);
|
|
1090
|
+
},
|
|
1091
|
+
formatResult: (action, data, args) => {
|
|
1092
|
+
switch (action) {
|
|
1093
|
+
case "list": return formatDatabaseUserList(data);
|
|
1094
|
+
case "get": return formatDatabaseUser(data);
|
|
1095
|
+
case "create": return formatDatabaseUser(data);
|
|
1096
|
+
case "delete": return `Database user ${args.id} deleted.`;
|
|
1097
|
+
default: return "Done.";
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
/**
|
|
1102
|
+
* Handle deployment resource actions.
|
|
1103
|
+
*
|
|
1104
|
+
* Not using the factory because the `get` action has special logic
|
|
1105
|
+
* (with id → output, without id → script).
|
|
1106
|
+
*/
|
|
1107
|
+
async function handleDeployments(action, args, ctx) {
|
|
1108
|
+
if (!args.server_id) return errorResult("Missing required field: server_id");
|
|
1109
|
+
if (!args.site_id) return errorResult("Missing required field: site_id");
|
|
1110
|
+
if (!sanitizeId(args.server_id)) return errorResult(`Invalid server_id: "${args.server_id}". IDs must be alphanumeric.`);
|
|
1111
|
+
if (!sanitizeId(args.site_id)) return errorResult(`Invalid site_id: "${args.site_id}". IDs must be alphanumeric.`);
|
|
1112
|
+
if (args.id && !sanitizeId(args.id)) return errorResult(`Invalid id: "${args.id}". IDs must be alphanumeric.`);
|
|
1113
|
+
const opts = {
|
|
1114
|
+
server_id: args.server_id,
|
|
1115
|
+
site_id: args.site_id
|
|
1116
|
+
};
|
|
1117
|
+
switch (action) {
|
|
1118
|
+
case "list": {
|
|
1119
|
+
const result = await listDeployments(opts, ctx.executorContext);
|
|
1120
|
+
if (ctx.compact) return jsonResult(formatDeploymentList(result.data));
|
|
1121
|
+
return jsonResult(result.data);
|
|
1122
|
+
}
|
|
1123
|
+
case "deploy": {
|
|
1124
|
+
const deployResult = await deploySiteAndWait(opts, ctx.executorContext);
|
|
1125
|
+
return jsonResult(formatDeployAction(args.site_id, args.server_id, deployResult.data));
|
|
1126
|
+
}
|
|
1127
|
+
case "get":
|
|
1128
|
+
if (args.id) {
|
|
1129
|
+
const result = await getDeploymentOutput({
|
|
1130
|
+
...opts,
|
|
1131
|
+
deployment_id: args.id
|
|
1132
|
+
}, ctx.executorContext);
|
|
1133
|
+
return jsonResult(formatDeploymentOutput(args.id, result.data));
|
|
1134
|
+
}
|
|
1135
|
+
return jsonResult(formatDeploymentScript((await getDeploymentScript(opts, ctx.executorContext)).data));
|
|
1136
|
+
case "update":
|
|
1137
|
+
if (!args.content) return errorResult("Missing required field: content");
|
|
1138
|
+
await updateDeploymentScript({
|
|
1139
|
+
...opts,
|
|
1140
|
+
content: args.content
|
|
1141
|
+
}, ctx.executorContext);
|
|
1142
|
+
return jsonResult(formatDeploymentScriptUpdated(args.site_id, args.server_id));
|
|
1143
|
+
default: return errorResult(`Unknown action "${action}" for deployments. Valid actions: list, deploy, get, update.`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const handleEnv = createResourceHandler({
|
|
1147
|
+
resource: "env",
|
|
1148
|
+
actions: ["get", "update"],
|
|
1149
|
+
requiredFields: {
|
|
1150
|
+
get: ["server_id", "site_id"],
|
|
1151
|
+
update: [
|
|
1152
|
+
"server_id",
|
|
1153
|
+
"site_id",
|
|
1154
|
+
"content"
|
|
1155
|
+
]
|
|
1156
|
+
},
|
|
1157
|
+
executors: {
|
|
1158
|
+
get: getEnv,
|
|
1159
|
+
update: updateEnv
|
|
1160
|
+
},
|
|
1161
|
+
mapOptions: (_action, args) => ({
|
|
1162
|
+
server_id: args.server_id,
|
|
1163
|
+
site_id: args.site_id,
|
|
1164
|
+
content: args.content
|
|
1165
|
+
}),
|
|
1166
|
+
formatResult: (action, data) => {
|
|
1167
|
+
switch (action) {
|
|
1168
|
+
case "get": return formatEnv(data);
|
|
1169
|
+
case "update": return "Environment variables updated.";
|
|
1170
|
+
default: return "Done.";
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
const handleFirewallRules = createResourceHandler({
|
|
1175
|
+
resource: "firewall-rules",
|
|
1176
|
+
actions: [
|
|
1177
|
+
"list",
|
|
1178
|
+
"get",
|
|
1179
|
+
"create",
|
|
1180
|
+
"delete"
|
|
1181
|
+
],
|
|
1182
|
+
requiredFields: {
|
|
1183
|
+
list: ["server_id"],
|
|
1184
|
+
get: ["server_id", "id"],
|
|
1185
|
+
create: [
|
|
1186
|
+
"server_id",
|
|
1187
|
+
"name",
|
|
1188
|
+
"port"
|
|
1189
|
+
],
|
|
1190
|
+
delete: ["server_id", "id"]
|
|
1191
|
+
},
|
|
1192
|
+
executors: {
|
|
1193
|
+
list: listFirewallRules,
|
|
1194
|
+
get: getFirewallRule,
|
|
1195
|
+
create: createFirewallRule,
|
|
1196
|
+
delete: deleteFirewallRule
|
|
1197
|
+
},
|
|
1198
|
+
hints: (data, id) => {
|
|
1199
|
+
const rule = data;
|
|
1200
|
+
return getFirewallRuleHints(String(rule.server_id), id);
|
|
1201
|
+
},
|
|
1202
|
+
formatResult: (action, data, args) => {
|
|
1203
|
+
switch (action) {
|
|
1204
|
+
case "list": return formatFirewallRuleList(data);
|
|
1205
|
+
case "get": return formatFirewallRule(data);
|
|
1206
|
+
case "create": return formatFirewallRule(data);
|
|
1207
|
+
case "delete": return `Firewall rule ${args.id} deleted.`;
|
|
1208
|
+
default: return "Done.";
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
var RESOURCE_HELP = {
|
|
1213
|
+
servers: {
|
|
1214
|
+
description: "Manage Laravel Forge servers — provisioned cloud instances (DigitalOcean, AWS, Hetzner, etc.)",
|
|
1215
|
+
scope: "global (no parent ID needed)",
|
|
1216
|
+
actions: {
|
|
1217
|
+
list: "List all servers in your Forge account",
|
|
1218
|
+
get: "Get a single server by ID with full details",
|
|
1219
|
+
create: "Provision a new server (requires provider, type, region, name)",
|
|
1220
|
+
delete: "Delete a server by ID (irreversible)",
|
|
1221
|
+
reboot: "Reboot a server by ID"
|
|
1222
|
+
},
|
|
1223
|
+
fields: {
|
|
1224
|
+
id: "Server ID",
|
|
1225
|
+
name: "Server name (hostname)",
|
|
1226
|
+
provider: "Cloud provider (hetzner, ocean2, aws, linode, vultr, custom)",
|
|
1227
|
+
region: "Provider region code",
|
|
1228
|
+
ip_address: "Public IP address",
|
|
1229
|
+
is_ready: "Whether the server has finished provisioning",
|
|
1230
|
+
php_version: "Default PHP version (php84, php83, etc.)"
|
|
1231
|
+
},
|
|
1232
|
+
examples: [
|
|
1233
|
+
{
|
|
1234
|
+
description: "List all servers",
|
|
1235
|
+
params: {
|
|
1236
|
+
resource: "servers",
|
|
1237
|
+
action: "list"
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
description: "Get server details",
|
|
1242
|
+
params: {
|
|
1243
|
+
resource: "servers",
|
|
1244
|
+
action: "get",
|
|
1245
|
+
id: "123"
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
description: "Reboot a server",
|
|
1250
|
+
params: {
|
|
1251
|
+
resource: "servers",
|
|
1252
|
+
action: "reboot",
|
|
1253
|
+
id: "123"
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
]
|
|
1257
|
+
},
|
|
1258
|
+
sites: {
|
|
1259
|
+
description: "Manage sites on a Forge server — web applications, PHP projects, static sites",
|
|
1260
|
+
scope: "server (requires server_id)",
|
|
1261
|
+
actions: {
|
|
1262
|
+
list: "List all sites on a server",
|
|
1263
|
+
get: "Get a single site by ID",
|
|
1264
|
+
create: "Create a new site (requires domain, project_type)",
|
|
1265
|
+
delete: "Delete a site by ID"
|
|
1266
|
+
},
|
|
1267
|
+
fields: {
|
|
1268
|
+
id: "Site ID",
|
|
1269
|
+
server_id: "Parent server ID",
|
|
1270
|
+
name: "Domain name (e.g. example.com)",
|
|
1271
|
+
project_type: "Project type (php, html, symfony, symfony_dev, symfony_four, laravel)",
|
|
1272
|
+
directory: "Web root directory (e.g. /public)",
|
|
1273
|
+
repository: "Git repository URL (if connected)",
|
|
1274
|
+
deployment_status: "Last deployment status (null, deploying, deployed, failed)"
|
|
1275
|
+
},
|
|
1276
|
+
examples: [
|
|
1277
|
+
{
|
|
1278
|
+
description: "List sites on a server",
|
|
1279
|
+
params: {
|
|
1280
|
+
resource: "sites",
|
|
1281
|
+
action: "list",
|
|
1282
|
+
server_id: "123"
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
description: "Get site details",
|
|
1287
|
+
params: {
|
|
1288
|
+
resource: "sites",
|
|
1289
|
+
action: "get",
|
|
1290
|
+
server_id: "123",
|
|
1291
|
+
id: "456"
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
description: "Create a Laravel site",
|
|
1296
|
+
params: {
|
|
1297
|
+
resource: "sites",
|
|
1298
|
+
action: "create",
|
|
1299
|
+
server_id: "123",
|
|
1300
|
+
domain: "app.example.com",
|
|
1301
|
+
project_type: "php",
|
|
1302
|
+
directory: "/public"
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
]
|
|
1306
|
+
},
|
|
1307
|
+
deployments: {
|
|
1308
|
+
description: "Manage site deployments — trigger, monitor, and configure deploy scripts",
|
|
1309
|
+
scope: "site (requires server_id + site_id)",
|
|
1310
|
+
actions: {
|
|
1311
|
+
list: "List recent deployments for a site",
|
|
1312
|
+
deploy: "Trigger a new deployment",
|
|
1313
|
+
get: "Get deployment output (requires deployment_id in 'id' field)",
|
|
1314
|
+
update: "Update the deployment script (provide 'content' field)"
|
|
1315
|
+
},
|
|
1316
|
+
fields: {
|
|
1317
|
+
id: "Deployment ID",
|
|
1318
|
+
server_id: "Parent server ID",
|
|
1319
|
+
site_id: "Parent site ID",
|
|
1320
|
+
status: "Deployment status (null, deploying, deployed, failed)",
|
|
1321
|
+
displayable_type: "Type of deployment trigger",
|
|
1322
|
+
ended_at: "Completion timestamp"
|
|
1323
|
+
},
|
|
1324
|
+
examples: [
|
|
1325
|
+
{
|
|
1326
|
+
description: "Deploy a site",
|
|
1327
|
+
params: {
|
|
1328
|
+
resource: "deployments",
|
|
1329
|
+
action: "deploy",
|
|
1330
|
+
server_id: "123",
|
|
1331
|
+
site_id: "456"
|
|
1332
|
+
}
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
description: "List deployments",
|
|
1336
|
+
params: {
|
|
1337
|
+
resource: "deployments",
|
|
1338
|
+
action: "list",
|
|
1339
|
+
server_id: "123",
|
|
1340
|
+
site_id: "456"
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
description: "Get deployment output",
|
|
1345
|
+
params: {
|
|
1346
|
+
resource: "deployments",
|
|
1347
|
+
action: "get",
|
|
1348
|
+
server_id: "123",
|
|
1349
|
+
site_id: "456",
|
|
1350
|
+
id: "789"
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
description: "Update deploy script",
|
|
1355
|
+
params: {
|
|
1356
|
+
resource: "deployments",
|
|
1357
|
+
action: "update",
|
|
1358
|
+
server_id: "123",
|
|
1359
|
+
site_id: "456",
|
|
1360
|
+
content: "cd /home/forge/app.example.com\ngit pull\ncomposer install\nphp artisan migrate --force"
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
},
|
|
1365
|
+
env: {
|
|
1366
|
+
description: "Manage environment variables (.env file) for a site",
|
|
1367
|
+
scope: "site (requires server_id + site_id)",
|
|
1368
|
+
actions: {
|
|
1369
|
+
get: "Get the current .env file content",
|
|
1370
|
+
update: "Replace the entire .env file (provide 'content' field)"
|
|
1371
|
+
},
|
|
1372
|
+
examples: [{
|
|
1373
|
+
description: "Get env variables",
|
|
1374
|
+
params: {
|
|
1375
|
+
resource: "env",
|
|
1376
|
+
action: "get",
|
|
1377
|
+
server_id: "123",
|
|
1378
|
+
site_id: "456"
|
|
1379
|
+
}
|
|
1380
|
+
}, {
|
|
1381
|
+
description: "Update env",
|
|
1382
|
+
params: {
|
|
1383
|
+
resource: "env",
|
|
1384
|
+
action: "update",
|
|
1385
|
+
server_id: "123",
|
|
1386
|
+
site_id: "456",
|
|
1387
|
+
content: "APP_ENV=production\nAPP_DEBUG=false\nDB_HOST=127.0.0.1"
|
|
1388
|
+
}
|
|
1389
|
+
}]
|
|
1390
|
+
},
|
|
1391
|
+
nginx: {
|
|
1392
|
+
description: "Manage Nginx configuration for a site",
|
|
1393
|
+
scope: "site (requires server_id + site_id)",
|
|
1394
|
+
actions: {
|
|
1395
|
+
get: "Get the current Nginx config",
|
|
1396
|
+
update: "Replace the Nginx config (provide 'content' field)"
|
|
1397
|
+
},
|
|
1398
|
+
examples: [{
|
|
1399
|
+
description: "Get nginx config",
|
|
1400
|
+
params: {
|
|
1401
|
+
resource: "nginx",
|
|
1402
|
+
action: "get",
|
|
1403
|
+
server_id: "123",
|
|
1404
|
+
site_id: "456"
|
|
1405
|
+
}
|
|
1406
|
+
}]
|
|
1407
|
+
},
|
|
1408
|
+
certificates: {
|
|
1409
|
+
description: "Manage SSL/TLS certificates for a site — Let's Encrypt, custom, or cloned",
|
|
1410
|
+
scope: "site (requires server_id + site_id)",
|
|
1411
|
+
actions: {
|
|
1412
|
+
list: "List certificates for a site",
|
|
1413
|
+
get: "Get certificate details",
|
|
1414
|
+
create: "Create/request a new certificate",
|
|
1415
|
+
delete: "Delete a certificate",
|
|
1416
|
+
activate: "Activate a certificate (make it the active cert for the site)"
|
|
1417
|
+
},
|
|
1418
|
+
examples: [
|
|
1419
|
+
{
|
|
1420
|
+
description: "List certificates",
|
|
1421
|
+
params: {
|
|
1422
|
+
resource: "certificates",
|
|
1423
|
+
action: "list",
|
|
1424
|
+
server_id: "123",
|
|
1425
|
+
site_id: "456"
|
|
1426
|
+
}
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
description: "Create Let's Encrypt cert",
|
|
1430
|
+
params: {
|
|
1431
|
+
resource: "certificates",
|
|
1432
|
+
action: "create",
|
|
1433
|
+
server_id: "123",
|
|
1434
|
+
site_id: "456",
|
|
1435
|
+
domain: "example.com",
|
|
1436
|
+
type: "new"
|
|
1437
|
+
}
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
description: "Activate a certificate",
|
|
1441
|
+
params: {
|
|
1442
|
+
resource: "certificates",
|
|
1443
|
+
action: "activate",
|
|
1444
|
+
server_id: "123",
|
|
1445
|
+
site_id: "456",
|
|
1446
|
+
id: "789"
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
]
|
|
1450
|
+
},
|
|
1451
|
+
databases: {
|
|
1452
|
+
description: "Manage MySQL/PostgreSQL databases on a server",
|
|
1453
|
+
scope: "server (requires server_id)",
|
|
1454
|
+
actions: {
|
|
1455
|
+
list: "List databases on a server",
|
|
1456
|
+
get: "Get database details",
|
|
1457
|
+
create: "Create a new database (optionally with user)",
|
|
1458
|
+
delete: "Delete a database"
|
|
1459
|
+
},
|
|
1460
|
+
fields: {
|
|
1461
|
+
name: "Database name",
|
|
1462
|
+
user: "(create only) Create a database user",
|
|
1463
|
+
password: "(create only) User password"
|
|
1464
|
+
},
|
|
1465
|
+
examples: [{
|
|
1466
|
+
description: "List databases",
|
|
1467
|
+
params: {
|
|
1468
|
+
resource: "databases",
|
|
1469
|
+
action: "list",
|
|
1470
|
+
server_id: "123"
|
|
1471
|
+
}
|
|
1472
|
+
}, {
|
|
1473
|
+
description: "Create database with user",
|
|
1474
|
+
params: {
|
|
1475
|
+
resource: "databases",
|
|
1476
|
+
action: "create",
|
|
1477
|
+
server_id: "123",
|
|
1478
|
+
name: "myapp",
|
|
1479
|
+
user: "admin",
|
|
1480
|
+
password: "secret123"
|
|
1481
|
+
}
|
|
1482
|
+
}]
|
|
1483
|
+
},
|
|
1484
|
+
"database-users": {
|
|
1485
|
+
description: "Manage database users on a server",
|
|
1486
|
+
scope: "server (requires server_id)",
|
|
1487
|
+
actions: {
|
|
1488
|
+
list: "List database users on a server",
|
|
1489
|
+
get: "Get database user details",
|
|
1490
|
+
create: "Create a new database user",
|
|
1491
|
+
delete: "Delete a database user"
|
|
1492
|
+
},
|
|
1493
|
+
fields: {
|
|
1494
|
+
name: "Database user name",
|
|
1495
|
+
password: "User password",
|
|
1496
|
+
databases: "(create only) Array of database IDs to grant access to"
|
|
1497
|
+
},
|
|
1498
|
+
examples: [{
|
|
1499
|
+
description: "List database users",
|
|
1500
|
+
params: {
|
|
1501
|
+
resource: "database-users",
|
|
1502
|
+
action: "list",
|
|
1503
|
+
server_id: "123"
|
|
1504
|
+
}
|
|
1505
|
+
}, {
|
|
1506
|
+
description: "Create a database user",
|
|
1507
|
+
params: {
|
|
1508
|
+
resource: "database-users",
|
|
1509
|
+
action: "create",
|
|
1510
|
+
server_id: "123",
|
|
1511
|
+
name: "forge",
|
|
1512
|
+
password: "secret123",
|
|
1513
|
+
databases: [1, 2]
|
|
1514
|
+
}
|
|
1515
|
+
}]
|
|
1516
|
+
},
|
|
1517
|
+
daemons: {
|
|
1518
|
+
description: "Manage background processes (daemons) — queue workers, websocket servers, etc.",
|
|
1519
|
+
scope: "server (requires server_id)",
|
|
1520
|
+
actions: {
|
|
1521
|
+
list: "List daemons on a server",
|
|
1522
|
+
get: "Get daemon details",
|
|
1523
|
+
create: "Create a new daemon",
|
|
1524
|
+
delete: "Delete a daemon",
|
|
1525
|
+
restart: "Restart a daemon"
|
|
1526
|
+
},
|
|
1527
|
+
fields: {
|
|
1528
|
+
command: "Shell command to run",
|
|
1529
|
+
user: "Execution user (default: forge)",
|
|
1530
|
+
directory: "Working directory",
|
|
1531
|
+
processes: "Number of processes (default: 1)"
|
|
1532
|
+
},
|
|
1533
|
+
examples: [
|
|
1534
|
+
{
|
|
1535
|
+
description: "List daemons",
|
|
1536
|
+
params: {
|
|
1537
|
+
resource: "daemons",
|
|
1538
|
+
action: "list",
|
|
1539
|
+
server_id: "123"
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
description: "Create queue worker",
|
|
1544
|
+
params: {
|
|
1545
|
+
resource: "daemons",
|
|
1546
|
+
action: "create",
|
|
1547
|
+
server_id: "123",
|
|
1548
|
+
command: "php artisan queue:work --tries=3",
|
|
1549
|
+
user: "forge"
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
description: "Restart a daemon",
|
|
1554
|
+
params: {
|
|
1555
|
+
resource: "daemons",
|
|
1556
|
+
action: "restart",
|
|
1557
|
+
server_id: "123",
|
|
1558
|
+
id: "456"
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
]
|
|
1562
|
+
},
|
|
1563
|
+
"firewall-rules": {
|
|
1564
|
+
description: "Manage UFW firewall rules on a server",
|
|
1565
|
+
scope: "server (requires server_id)",
|
|
1566
|
+
actions: {
|
|
1567
|
+
list: "List firewall rules",
|
|
1568
|
+
get: "Get rule details",
|
|
1569
|
+
create: "Create a new firewall rule",
|
|
1570
|
+
delete: "Delete a firewall rule"
|
|
1571
|
+
},
|
|
1572
|
+
fields: {
|
|
1573
|
+
name: "Rule name/description",
|
|
1574
|
+
port: "Port number or range (e.g. 80, 8000-9000)",
|
|
1575
|
+
type: "allow or deny (default: allow)",
|
|
1576
|
+
ip_address: "Restrict to specific IP (optional)"
|
|
1577
|
+
},
|
|
1578
|
+
examples: [{
|
|
1579
|
+
description: "List firewall rules",
|
|
1580
|
+
params: {
|
|
1581
|
+
resource: "firewall-rules",
|
|
1582
|
+
action: "list",
|
|
1583
|
+
server_id: "123"
|
|
1584
|
+
}
|
|
1585
|
+
}, {
|
|
1586
|
+
description: "Open port 3000",
|
|
1587
|
+
params: {
|
|
1588
|
+
resource: "firewall-rules",
|
|
1589
|
+
action: "create",
|
|
1590
|
+
server_id: "123",
|
|
1591
|
+
name: "Allow Node.js",
|
|
1592
|
+
port: 3e3
|
|
1593
|
+
}
|
|
1594
|
+
}]
|
|
1595
|
+
},
|
|
1596
|
+
"ssh-keys": {
|
|
1597
|
+
description: "Manage SSH keys on a server",
|
|
1598
|
+
scope: "server (requires server_id)",
|
|
1599
|
+
actions: {
|
|
1600
|
+
list: "List SSH keys",
|
|
1601
|
+
get: "Get key details",
|
|
1602
|
+
create: "Add an SSH key to the server",
|
|
1603
|
+
delete: "Remove an SSH key"
|
|
1604
|
+
},
|
|
1605
|
+
fields: {
|
|
1606
|
+
name: "Key label/description",
|
|
1607
|
+
key: "Public key content (ssh-rsa ...)",
|
|
1608
|
+
username: "User to add the key to (optional)"
|
|
1609
|
+
},
|
|
1610
|
+
examples: [{
|
|
1611
|
+
description: "List SSH keys",
|
|
1612
|
+
params: {
|
|
1613
|
+
resource: "ssh-keys",
|
|
1614
|
+
action: "list",
|
|
1615
|
+
server_id: "123"
|
|
1616
|
+
}
|
|
1617
|
+
}, {
|
|
1618
|
+
description: "Add SSH key",
|
|
1619
|
+
params: {
|
|
1620
|
+
resource: "ssh-keys",
|
|
1621
|
+
action: "create",
|
|
1622
|
+
server_id: "123",
|
|
1623
|
+
name: "Deploy Key",
|
|
1624
|
+
key: "ssh-rsa AAAA..."
|
|
1625
|
+
}
|
|
1626
|
+
}]
|
|
1627
|
+
},
|
|
1628
|
+
"security-rules": {
|
|
1629
|
+
description: "Manage HTTP Basic Auth security rules for a site",
|
|
1630
|
+
scope: "site (requires server_id + site_id)",
|
|
1631
|
+
actions: {
|
|
1632
|
+
list: "List security rules",
|
|
1633
|
+
get: "Get rule details",
|
|
1634
|
+
create: "Create a security rule with credentials",
|
|
1635
|
+
delete: "Delete a security rule"
|
|
1636
|
+
},
|
|
1637
|
+
examples: [{
|
|
1638
|
+
description: "List security rules",
|
|
1639
|
+
params: {
|
|
1640
|
+
resource: "security-rules",
|
|
1641
|
+
action: "list",
|
|
1642
|
+
server_id: "123",
|
|
1643
|
+
site_id: "456"
|
|
1644
|
+
}
|
|
1645
|
+
}, {
|
|
1646
|
+
description: "Create basic auth",
|
|
1647
|
+
params: {
|
|
1648
|
+
resource: "security-rules",
|
|
1649
|
+
action: "create",
|
|
1650
|
+
server_id: "123",
|
|
1651
|
+
site_id: "456",
|
|
1652
|
+
name: "Staging Auth",
|
|
1653
|
+
credentials: [{
|
|
1654
|
+
username: "admin",
|
|
1655
|
+
password: "secret"
|
|
1656
|
+
}]
|
|
1657
|
+
}
|
|
1658
|
+
}]
|
|
1659
|
+
},
|
|
1660
|
+
"redirect-rules": {
|
|
1661
|
+
description: "Manage URL redirect rules for a site",
|
|
1662
|
+
scope: "site (requires server_id + site_id)",
|
|
1663
|
+
actions: {
|
|
1664
|
+
list: "List redirect rules",
|
|
1665
|
+
get: "Get rule details",
|
|
1666
|
+
create: "Create a redirect rule",
|
|
1667
|
+
delete: "Delete a redirect rule"
|
|
1668
|
+
},
|
|
1669
|
+
fields: {
|
|
1670
|
+
from: "Source path (regex supported)",
|
|
1671
|
+
to: "Destination URL",
|
|
1672
|
+
type: "redirect (302, default) or permanent (301)"
|
|
1673
|
+
},
|
|
1674
|
+
examples: [{
|
|
1675
|
+
description: "List redirects",
|
|
1676
|
+
params: {
|
|
1677
|
+
resource: "redirect-rules",
|
|
1678
|
+
action: "list",
|
|
1679
|
+
server_id: "123",
|
|
1680
|
+
site_id: "456"
|
|
1681
|
+
}
|
|
1682
|
+
}, {
|
|
1683
|
+
description: "Create redirect",
|
|
1684
|
+
params: {
|
|
1685
|
+
resource: "redirect-rules",
|
|
1686
|
+
action: "create",
|
|
1687
|
+
server_id: "123",
|
|
1688
|
+
site_id: "456",
|
|
1689
|
+
from: "/old-page",
|
|
1690
|
+
to: "/new-page",
|
|
1691
|
+
type: "permanent"
|
|
1692
|
+
}
|
|
1693
|
+
}]
|
|
1694
|
+
},
|
|
1695
|
+
monitors: {
|
|
1696
|
+
description: "Manage server monitoring alerts — CPU, disk, memory thresholds",
|
|
1697
|
+
scope: "server (requires server_id)",
|
|
1698
|
+
actions: {
|
|
1699
|
+
list: "List monitors",
|
|
1700
|
+
get: "Get monitor details",
|
|
1701
|
+
create: "Create a monitor",
|
|
1702
|
+
delete: "Delete a monitor"
|
|
1703
|
+
},
|
|
1704
|
+
fields: {
|
|
1705
|
+
type: "Monitor type (disk, cpu, memory)",
|
|
1706
|
+
operator: "Comparison operator (gte, lte)",
|
|
1707
|
+
threshold: "Threshold value (e.g. 80 for 80%)",
|
|
1708
|
+
minutes: "Check interval in minutes"
|
|
1709
|
+
},
|
|
1710
|
+
examples: [{
|
|
1711
|
+
description: "List monitors",
|
|
1712
|
+
params: {
|
|
1713
|
+
resource: "monitors",
|
|
1714
|
+
action: "list",
|
|
1715
|
+
server_id: "123"
|
|
1716
|
+
}
|
|
1717
|
+
}, {
|
|
1718
|
+
description: "Alert on high disk usage",
|
|
1719
|
+
params: {
|
|
1720
|
+
resource: "monitors",
|
|
1721
|
+
action: "create",
|
|
1722
|
+
server_id: "123",
|
|
1723
|
+
type: "disk",
|
|
1724
|
+
operator: "gte",
|
|
1725
|
+
threshold: 80,
|
|
1726
|
+
minutes: 5
|
|
1727
|
+
}
|
|
1728
|
+
}]
|
|
1729
|
+
},
|
|
1730
|
+
"nginx-templates": {
|
|
1731
|
+
description: "Manage reusable Nginx configuration templates on a server",
|
|
1732
|
+
scope: "server (requires server_id)",
|
|
1733
|
+
actions: {
|
|
1734
|
+
list: "List nginx templates",
|
|
1735
|
+
get: "Get template with content",
|
|
1736
|
+
create: "Create a new template",
|
|
1737
|
+
update: "Update template name or content",
|
|
1738
|
+
delete: "Delete a template"
|
|
1739
|
+
},
|
|
1740
|
+
examples: [{
|
|
1741
|
+
description: "List templates",
|
|
1742
|
+
params: {
|
|
1743
|
+
resource: "nginx-templates",
|
|
1744
|
+
action: "list",
|
|
1745
|
+
server_id: "123"
|
|
1746
|
+
}
|
|
1747
|
+
}, {
|
|
1748
|
+
description: "Get template content",
|
|
1749
|
+
params: {
|
|
1750
|
+
resource: "nginx-templates",
|
|
1751
|
+
action: "get",
|
|
1752
|
+
server_id: "123",
|
|
1753
|
+
id: "456"
|
|
1754
|
+
}
|
|
1755
|
+
}]
|
|
1756
|
+
},
|
|
1757
|
+
backups: {
|
|
1758
|
+
description: "Manage backup configurations — automated database backups to S3, Spaces, or other providers",
|
|
1759
|
+
scope: "server (requires server_id)",
|
|
1760
|
+
actions: {
|
|
1761
|
+
list: "List backup configurations on a server",
|
|
1762
|
+
get: "Get backup config details (databases, schedule, retention)",
|
|
1763
|
+
create: "Create a new backup configuration",
|
|
1764
|
+
delete: "Delete a backup configuration"
|
|
1765
|
+
},
|
|
1766
|
+
fields: {
|
|
1767
|
+
provider: "Backup provider (s3, spaces, custom)",
|
|
1768
|
+
credentials: "Provider credentials (keys, bucket, region)",
|
|
1769
|
+
frequency: "Backup frequency (daily, weekly, custom)",
|
|
1770
|
+
databases: "Array of database IDs to back up",
|
|
1771
|
+
retention: "Number of backups to retain"
|
|
1772
|
+
},
|
|
1773
|
+
examples: [{
|
|
1774
|
+
description: "List backup configs",
|
|
1775
|
+
params: {
|
|
1776
|
+
resource: "backups",
|
|
1777
|
+
action: "list",
|
|
1778
|
+
server_id: "123"
|
|
1779
|
+
}
|
|
1780
|
+
}, {
|
|
1781
|
+
description: "Get backup config details",
|
|
1782
|
+
params: {
|
|
1783
|
+
resource: "backups",
|
|
1784
|
+
action: "get",
|
|
1785
|
+
server_id: "123",
|
|
1786
|
+
id: "456"
|
|
1787
|
+
}
|
|
1788
|
+
}]
|
|
1789
|
+
},
|
|
1790
|
+
commands: {
|
|
1791
|
+
description: "Execute and list site commands — run artisan commands or shell scripts on a site",
|
|
1792
|
+
scope: "site (requires server_id + site_id)",
|
|
1793
|
+
actions: {
|
|
1794
|
+
list: "List commands executed on a site",
|
|
1795
|
+
get: "Get command details and status",
|
|
1796
|
+
create: "Execute a new command on the site"
|
|
1797
|
+
},
|
|
1798
|
+
fields: { command: "Shell command to execute" },
|
|
1799
|
+
examples: [{
|
|
1800
|
+
description: "List commands",
|
|
1801
|
+
params: {
|
|
1802
|
+
resource: "commands",
|
|
1803
|
+
action: "list",
|
|
1804
|
+
server_id: "123",
|
|
1805
|
+
site_id: "456"
|
|
1806
|
+
}
|
|
1807
|
+
}, {
|
|
1808
|
+
description: "Run a command",
|
|
1809
|
+
params: {
|
|
1810
|
+
resource: "commands",
|
|
1811
|
+
action: "create",
|
|
1812
|
+
server_id: "123",
|
|
1813
|
+
site_id: "456",
|
|
1814
|
+
command: "php artisan migrate --force"
|
|
1815
|
+
}
|
|
1816
|
+
}]
|
|
1817
|
+
},
|
|
1818
|
+
"scheduled-jobs": {
|
|
1819
|
+
description: "Manage cron jobs (scheduled tasks) on a server",
|
|
1820
|
+
scope: "server (requires server_id)",
|
|
1821
|
+
actions: {
|
|
1822
|
+
list: "List scheduled jobs on a server",
|
|
1823
|
+
get: "Get job details (command, frequency, cron expression)",
|
|
1824
|
+
create: "Create a new scheduled job",
|
|
1825
|
+
delete: "Delete a scheduled job"
|
|
1826
|
+
},
|
|
1827
|
+
fields: {
|
|
1828
|
+
command: "Shell command to schedule",
|
|
1829
|
+
user: "Execution user (default: forge)",
|
|
1830
|
+
frequency: "Frequency preset (minutely, hourly, nightly, weekly, monthly, custom)",
|
|
1831
|
+
minute: "(custom frequency) Minute field",
|
|
1832
|
+
hour: "(custom frequency) Hour field",
|
|
1833
|
+
day: "(custom frequency) Day of month field",
|
|
1834
|
+
month: "(custom frequency) Month field",
|
|
1835
|
+
weekday: "(custom frequency) Day of week field"
|
|
1836
|
+
},
|
|
1837
|
+
examples: [{
|
|
1838
|
+
description: "List scheduled jobs",
|
|
1839
|
+
params: {
|
|
1840
|
+
resource: "scheduled-jobs",
|
|
1841
|
+
action: "list",
|
|
1842
|
+
server_id: "123"
|
|
1843
|
+
}
|
|
1844
|
+
}, {
|
|
1845
|
+
description: "Create minutely scheduler",
|
|
1846
|
+
params: {
|
|
1847
|
+
resource: "scheduled-jobs",
|
|
1848
|
+
action: "create",
|
|
1849
|
+
server_id: "123",
|
|
1850
|
+
command: "php /home/forge/app.com/artisan schedule:run",
|
|
1851
|
+
frequency: "minutely",
|
|
1852
|
+
user: "forge"
|
|
1853
|
+
}
|
|
1854
|
+
}]
|
|
1855
|
+
},
|
|
1856
|
+
user: {
|
|
1857
|
+
description: "Get the currently authenticated Forge user profile",
|
|
1858
|
+
scope: "global (no parent ID needed)",
|
|
1859
|
+
actions: { get: "Get the authenticated user's profile (name, email, connected services)" },
|
|
1860
|
+
examples: [{
|
|
1861
|
+
description: "Get user profile",
|
|
1862
|
+
params: {
|
|
1863
|
+
resource: "user",
|
|
1864
|
+
action: "get"
|
|
1865
|
+
}
|
|
1866
|
+
}]
|
|
1867
|
+
},
|
|
1868
|
+
recipes: {
|
|
1869
|
+
description: "Manage and run server recipes — reusable bash scripts executed on one or more servers",
|
|
1870
|
+
scope: "global (no parent ID needed)",
|
|
1871
|
+
actions: {
|
|
1872
|
+
list: "List all recipes",
|
|
1873
|
+
get: "Get recipe details and script content",
|
|
1874
|
+
create: "Create a new recipe",
|
|
1875
|
+
delete: "Delete a recipe",
|
|
1876
|
+
run: "Run a recipe on specified servers (provide 'servers' as array of server IDs)"
|
|
1877
|
+
},
|
|
1878
|
+
fields: {
|
|
1879
|
+
name: "Recipe name",
|
|
1880
|
+
script: "Bash script content",
|
|
1881
|
+
user: "Execution user (default: root)",
|
|
1882
|
+
servers: "(run only) Array of server IDs to run on"
|
|
1883
|
+
},
|
|
1884
|
+
examples: [
|
|
1885
|
+
{
|
|
1886
|
+
description: "List recipes",
|
|
1887
|
+
params: {
|
|
1888
|
+
resource: "recipes",
|
|
1889
|
+
action: "list"
|
|
1890
|
+
}
|
|
1891
|
+
},
|
|
1892
|
+
{
|
|
1893
|
+
description: "Create a recipe",
|
|
1894
|
+
params: {
|
|
1895
|
+
resource: "recipes",
|
|
1896
|
+
action: "create",
|
|
1897
|
+
name: "Clear caches",
|
|
1898
|
+
script: "php artisan cache:clear\nphp artisan view:clear"
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
description: "Run recipe on servers",
|
|
1903
|
+
params: {
|
|
1904
|
+
resource: "recipes",
|
|
1905
|
+
action: "run",
|
|
1906
|
+
id: "123",
|
|
1907
|
+
servers: [
|
|
1908
|
+
1,
|
|
1909
|
+
2,
|
|
1910
|
+
3
|
|
1911
|
+
]
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
]
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
/**
|
|
1918
|
+
* Handle help action — returns documentation for a specific resource.
|
|
1919
|
+
*/
|
|
1920
|
+
function handleHelp(resource) {
|
|
1921
|
+
const help = RESOURCE_HELP[resource];
|
|
1922
|
+
if (!help) return handleHelpOverview();
|
|
1923
|
+
return jsonResult({
|
|
1924
|
+
resource,
|
|
1925
|
+
...help
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Get help for all resources (overview).
|
|
1930
|
+
*/
|
|
1931
|
+
function handleHelpOverview() {
|
|
1932
|
+
return jsonResult({
|
|
1933
|
+
message: "Use action=\"help\" with a specific resource for detailed documentation",
|
|
1934
|
+
resources: Object.entries(RESOURCE_HELP).map(([resource, help]) => ({
|
|
1935
|
+
resource,
|
|
1936
|
+
description: help.description,
|
|
1937
|
+
scope: help.scope,
|
|
1938
|
+
actions: Object.keys(help.actions)
|
|
1939
|
+
})),
|
|
1940
|
+
_tip: "Always call { action: 'help', resource: '<name>' } before your first interaction with any resource to learn required fields and examples."
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
const handleMonitors = createResourceHandler({
|
|
1944
|
+
resource: "monitors",
|
|
1945
|
+
actions: [
|
|
1946
|
+
"list",
|
|
1947
|
+
"get",
|
|
1948
|
+
"create",
|
|
1949
|
+
"delete"
|
|
1950
|
+
],
|
|
1951
|
+
requiredFields: {
|
|
1952
|
+
list: ["server_id"],
|
|
1953
|
+
get: ["server_id", "id"],
|
|
1954
|
+
create: [
|
|
1955
|
+
"server_id",
|
|
1956
|
+
"type",
|
|
1957
|
+
"operator",
|
|
1958
|
+
"threshold",
|
|
1959
|
+
"minutes"
|
|
1960
|
+
],
|
|
1961
|
+
delete: ["server_id", "id"]
|
|
1962
|
+
},
|
|
1963
|
+
executors: {
|
|
1964
|
+
list: listMonitors,
|
|
1965
|
+
get: getMonitor,
|
|
1966
|
+
create: createMonitor,
|
|
1967
|
+
delete: deleteMonitor
|
|
1968
|
+
},
|
|
1969
|
+
formatResult: (action, data, args) => {
|
|
1970
|
+
switch (action) {
|
|
1971
|
+
case "list": return formatMonitorList(data);
|
|
1972
|
+
case "get": return formatMonitor(data);
|
|
1973
|
+
case "create": return formatMonitor(data);
|
|
1974
|
+
case "delete": return `Monitor ${args.id} deleted.`;
|
|
1975
|
+
default: return "Done.";
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
const handleNginxConfig = createResourceHandler({
|
|
1980
|
+
resource: "nginx",
|
|
1981
|
+
actions: ["get", "update"],
|
|
1982
|
+
requiredFields: {
|
|
1983
|
+
get: ["server_id", "site_id"],
|
|
1984
|
+
update: [
|
|
1985
|
+
"server_id",
|
|
1986
|
+
"site_id",
|
|
1987
|
+
"content"
|
|
1988
|
+
]
|
|
1989
|
+
},
|
|
1990
|
+
executors: {
|
|
1991
|
+
get: getNginxConfig,
|
|
1992
|
+
update: updateNginxConfig
|
|
1993
|
+
},
|
|
1994
|
+
mapOptions: (_action, args) => ({
|
|
1995
|
+
server_id: args.server_id,
|
|
1996
|
+
site_id: args.site_id,
|
|
1997
|
+
content: args.content
|
|
1998
|
+
}),
|
|
1999
|
+
formatResult: (action, data) => {
|
|
2000
|
+
switch (action) {
|
|
2001
|
+
case "get": return formatNginxConfig(data);
|
|
2002
|
+
case "update": return "Nginx configuration updated.";
|
|
2003
|
+
default: return "Done.";
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
const handleNginxTemplates = createResourceHandler({
|
|
2008
|
+
resource: "nginx-templates",
|
|
2009
|
+
actions: [
|
|
2010
|
+
"list",
|
|
2011
|
+
"get",
|
|
2012
|
+
"create",
|
|
2013
|
+
"update",
|
|
2014
|
+
"delete"
|
|
2015
|
+
],
|
|
2016
|
+
requiredFields: {
|
|
2017
|
+
list: ["server_id"],
|
|
2018
|
+
get: ["server_id", "id"],
|
|
2019
|
+
create: [
|
|
2020
|
+
"server_id",
|
|
2021
|
+
"name",
|
|
2022
|
+
"content"
|
|
2023
|
+
],
|
|
2024
|
+
update: ["server_id", "id"],
|
|
2025
|
+
delete: ["server_id", "id"]
|
|
2026
|
+
},
|
|
2027
|
+
executors: {
|
|
2028
|
+
list: listNginxTemplates,
|
|
2029
|
+
get: getNginxTemplate,
|
|
2030
|
+
create: createNginxTemplate,
|
|
2031
|
+
update: updateNginxTemplate,
|
|
2032
|
+
delete: deleteNginxTemplate
|
|
2033
|
+
},
|
|
2034
|
+
hints: (data, id) => {
|
|
2035
|
+
const template = data;
|
|
2036
|
+
return getNginxTemplateHints(String(template.server_id), id);
|
|
2037
|
+
},
|
|
2038
|
+
formatResult: (action, data, args) => {
|
|
2039
|
+
switch (action) {
|
|
2040
|
+
case "list": return formatNginxTemplateList(data);
|
|
2041
|
+
case "get": return formatNginxTemplate(data);
|
|
2042
|
+
case "create": return formatNginxTemplate(data);
|
|
2043
|
+
case "update": return formatNginxTemplate(data);
|
|
2044
|
+
case "delete": return `Nginx template ${args.id} deleted.`;
|
|
2045
|
+
default: return "Done.";
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
const handleRecipes = createResourceHandler({
|
|
2050
|
+
resource: "recipes",
|
|
2051
|
+
actions: [
|
|
2052
|
+
"list",
|
|
2053
|
+
"get",
|
|
2054
|
+
"create",
|
|
2055
|
+
"delete",
|
|
2056
|
+
"run"
|
|
2057
|
+
],
|
|
2058
|
+
requiredFields: {
|
|
2059
|
+
get: ["id"],
|
|
2060
|
+
create: ["name", "script"],
|
|
2061
|
+
delete: ["id"],
|
|
2062
|
+
run: ["id", "servers"]
|
|
2063
|
+
},
|
|
2064
|
+
executors: {
|
|
2065
|
+
list: listRecipes,
|
|
2066
|
+
get: getRecipe,
|
|
2067
|
+
create: createRecipe,
|
|
2068
|
+
delete: deleteRecipe,
|
|
2069
|
+
run: runRecipe
|
|
2070
|
+
},
|
|
2071
|
+
hints: (_data, id) => getRecipeHints(id),
|
|
2072
|
+
formatResult: (action, data, args) => {
|
|
2073
|
+
switch (action) {
|
|
2074
|
+
case "list": return formatRecipeList(data);
|
|
2075
|
+
case "get": return formatRecipe(data);
|
|
2076
|
+
case "create": return formatRecipe(data);
|
|
2077
|
+
case "delete": return `Recipe ${args.id} deleted.`;
|
|
2078
|
+
case "run": {
|
|
2079
|
+
const servers = args.servers;
|
|
2080
|
+
const count = Array.isArray(servers) ? servers.length : 1;
|
|
2081
|
+
return `Recipe ${args.id} run on ${count} server(s).`;
|
|
2082
|
+
}
|
|
2083
|
+
default: return "Done.";
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
const handleRedirectRules = createResourceHandler({
|
|
2088
|
+
resource: "redirect-rules",
|
|
2089
|
+
actions: [
|
|
2090
|
+
"list",
|
|
2091
|
+
"get",
|
|
2092
|
+
"create",
|
|
2093
|
+
"delete"
|
|
2094
|
+
],
|
|
2095
|
+
requiredFields: {
|
|
2096
|
+
list: ["server_id", "site_id"],
|
|
2097
|
+
get: [
|
|
2098
|
+
"server_id",
|
|
2099
|
+
"site_id",
|
|
2100
|
+
"id"
|
|
2101
|
+
],
|
|
2102
|
+
create: [
|
|
2103
|
+
"server_id",
|
|
2104
|
+
"site_id",
|
|
2105
|
+
"from",
|
|
2106
|
+
"to"
|
|
2107
|
+
],
|
|
2108
|
+
delete: [
|
|
2109
|
+
"server_id",
|
|
2110
|
+
"site_id",
|
|
2111
|
+
"id"
|
|
2112
|
+
]
|
|
2113
|
+
},
|
|
2114
|
+
executors: {
|
|
2115
|
+
list: listRedirectRules,
|
|
2116
|
+
get: getRedirectRule,
|
|
2117
|
+
create: createRedirectRule,
|
|
2118
|
+
delete: deleteRedirectRule
|
|
2119
|
+
},
|
|
2120
|
+
formatResult: (action, data, args) => {
|
|
2121
|
+
switch (action) {
|
|
2122
|
+
case "list": return formatRedirectRuleList(data);
|
|
2123
|
+
case "get": return formatRedirectRule(data);
|
|
2124
|
+
case "create": return formatRedirectRule(data);
|
|
2125
|
+
case "delete": return `Redirect rule ${args.id} deleted.`;
|
|
2126
|
+
default: return "Done.";
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
const handleScheduledJobs = createResourceHandler({
|
|
2131
|
+
resource: "scheduled-jobs",
|
|
2132
|
+
actions: [
|
|
2133
|
+
"list",
|
|
2134
|
+
"get",
|
|
2135
|
+
"create",
|
|
2136
|
+
"delete"
|
|
2137
|
+
],
|
|
2138
|
+
requiredFields: {
|
|
2139
|
+
list: ["server_id"],
|
|
2140
|
+
get: ["server_id", "id"],
|
|
2141
|
+
create: ["server_id", "command"],
|
|
2142
|
+
delete: ["server_id", "id"]
|
|
2143
|
+
},
|
|
2144
|
+
executors: {
|
|
2145
|
+
list: listScheduledJobs,
|
|
2146
|
+
get: getScheduledJob,
|
|
2147
|
+
create: createScheduledJob,
|
|
2148
|
+
delete: deleteScheduledJob
|
|
2149
|
+
},
|
|
2150
|
+
formatResult: (action, data, args) => {
|
|
2151
|
+
switch (action) {
|
|
2152
|
+
case "list": return formatScheduledJobList(data);
|
|
2153
|
+
case "get": return formatScheduledJob(data);
|
|
2154
|
+
case "create": return formatScheduledJob(data);
|
|
2155
|
+
case "delete": return `Scheduled job ${args.id} deleted.`;
|
|
2156
|
+
default: return "Done.";
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
var RESOURCE_SCHEMAS = {
|
|
2161
|
+
servers: {
|
|
2162
|
+
actions: [
|
|
2163
|
+
"list",
|
|
2164
|
+
"get",
|
|
2165
|
+
"create",
|
|
2166
|
+
"delete",
|
|
2167
|
+
"reboot"
|
|
2168
|
+
],
|
|
2169
|
+
scope: "global",
|
|
2170
|
+
required: {
|
|
2171
|
+
get: ["id"],
|
|
2172
|
+
create: [
|
|
2173
|
+
"provider",
|
|
2174
|
+
"type",
|
|
2175
|
+
"region",
|
|
2176
|
+
"name"
|
|
2177
|
+
],
|
|
2178
|
+
delete: ["id"],
|
|
2179
|
+
reboot: ["id"]
|
|
2180
|
+
},
|
|
2181
|
+
create: {
|
|
2182
|
+
provider: {
|
|
2183
|
+
required: true,
|
|
2184
|
+
type: "string — hetzner, ocean2, aws, etc."
|
|
2185
|
+
},
|
|
2186
|
+
type: {
|
|
2187
|
+
required: true,
|
|
2188
|
+
type: "string — app, web, worker, etc."
|
|
2189
|
+
},
|
|
2190
|
+
region: {
|
|
2191
|
+
required: true,
|
|
2192
|
+
type: "string — provider-specific region code"
|
|
2193
|
+
},
|
|
2194
|
+
name: {
|
|
2195
|
+
required: true,
|
|
2196
|
+
type: "string"
|
|
2197
|
+
},
|
|
2198
|
+
credential_id: {
|
|
2199
|
+
required: false,
|
|
2200
|
+
type: "string — provider credential ID"
|
|
2201
|
+
},
|
|
2202
|
+
php_version: {
|
|
2203
|
+
required: false,
|
|
2204
|
+
type: "string — php84, php83, etc."
|
|
2205
|
+
},
|
|
2206
|
+
database: {
|
|
2207
|
+
required: false,
|
|
2208
|
+
type: "string — database name to create"
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
},
|
|
2212
|
+
sites: {
|
|
2213
|
+
actions: [
|
|
2214
|
+
"list",
|
|
2215
|
+
"get",
|
|
2216
|
+
"create",
|
|
2217
|
+
"delete"
|
|
2218
|
+
],
|
|
2219
|
+
scope: "server",
|
|
2220
|
+
required: {
|
|
2221
|
+
list: ["server_id"],
|
|
2222
|
+
get: ["server_id", "id"],
|
|
2223
|
+
create: [
|
|
2224
|
+
"server_id",
|
|
2225
|
+
"domain",
|
|
2226
|
+
"project_type"
|
|
2227
|
+
],
|
|
2228
|
+
delete: ["server_id", "id"]
|
|
2229
|
+
},
|
|
2230
|
+
create: {
|
|
2231
|
+
domain: {
|
|
2232
|
+
required: true,
|
|
2233
|
+
type: "string — e.g. example.com"
|
|
2234
|
+
},
|
|
2235
|
+
project_type: {
|
|
2236
|
+
required: true,
|
|
2237
|
+
type: "string — php, html, symfony, etc."
|
|
2238
|
+
},
|
|
2239
|
+
directory: {
|
|
2240
|
+
required: false,
|
|
2241
|
+
type: "string — web root (/public)"
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
deployments: {
|
|
2246
|
+
actions: [
|
|
2247
|
+
"list",
|
|
2248
|
+
"deploy",
|
|
2249
|
+
"get",
|
|
2250
|
+
"update"
|
|
2251
|
+
],
|
|
2252
|
+
scope: "site",
|
|
2253
|
+
required: {
|
|
2254
|
+
list: ["server_id", "site_id"],
|
|
2255
|
+
deploy: ["server_id", "site_id"],
|
|
2256
|
+
get: [
|
|
2257
|
+
"server_id",
|
|
2258
|
+
"site_id",
|
|
2259
|
+
"id"
|
|
2260
|
+
],
|
|
2261
|
+
update: [
|
|
2262
|
+
"server_id",
|
|
2263
|
+
"site_id",
|
|
2264
|
+
"content"
|
|
2265
|
+
]
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
env: {
|
|
2269
|
+
actions: ["get", "update"],
|
|
2270
|
+
scope: "site",
|
|
2271
|
+
required: {
|
|
2272
|
+
get: ["server_id", "site_id"],
|
|
2273
|
+
update: [
|
|
2274
|
+
"server_id",
|
|
2275
|
+
"site_id",
|
|
2276
|
+
"content"
|
|
2277
|
+
]
|
|
2278
|
+
}
|
|
2279
|
+
},
|
|
2280
|
+
nginx: {
|
|
2281
|
+
actions: ["get", "update"],
|
|
2282
|
+
scope: "site",
|
|
2283
|
+
required: {
|
|
2284
|
+
get: ["server_id", "site_id"],
|
|
2285
|
+
update: [
|
|
2286
|
+
"server_id",
|
|
2287
|
+
"site_id",
|
|
2288
|
+
"content"
|
|
2289
|
+
]
|
|
2290
|
+
}
|
|
2291
|
+
},
|
|
2292
|
+
certificates: {
|
|
2293
|
+
actions: [
|
|
2294
|
+
"list",
|
|
2295
|
+
"get",
|
|
2296
|
+
"create",
|
|
2297
|
+
"delete",
|
|
2298
|
+
"activate"
|
|
2299
|
+
],
|
|
2300
|
+
scope: "site",
|
|
2301
|
+
required: {
|
|
2302
|
+
list: ["server_id", "site_id"],
|
|
2303
|
+
get: [
|
|
2304
|
+
"server_id",
|
|
2305
|
+
"site_id",
|
|
2306
|
+
"id"
|
|
2307
|
+
],
|
|
2308
|
+
create: [
|
|
2309
|
+
"server_id",
|
|
2310
|
+
"site_id",
|
|
2311
|
+
"domain"
|
|
2312
|
+
],
|
|
2313
|
+
delete: [
|
|
2314
|
+
"server_id",
|
|
2315
|
+
"site_id",
|
|
2316
|
+
"id"
|
|
2317
|
+
],
|
|
2318
|
+
activate: [
|
|
2319
|
+
"server_id",
|
|
2320
|
+
"site_id",
|
|
2321
|
+
"id"
|
|
2322
|
+
]
|
|
2323
|
+
},
|
|
2324
|
+
create: {
|
|
2325
|
+
type: {
|
|
2326
|
+
required: false,
|
|
2327
|
+
type: "string — new, existing, clone (default: new)"
|
|
2328
|
+
},
|
|
2329
|
+
domain: {
|
|
2330
|
+
required: true,
|
|
2331
|
+
type: "string"
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
databases: {
|
|
2336
|
+
actions: [
|
|
2337
|
+
"list",
|
|
2338
|
+
"get",
|
|
2339
|
+
"create",
|
|
2340
|
+
"delete"
|
|
2341
|
+
],
|
|
2342
|
+
scope: "server",
|
|
2343
|
+
required: {
|
|
2344
|
+
list: ["server_id"],
|
|
2345
|
+
get: ["server_id", "id"],
|
|
2346
|
+
create: ["server_id", "name"],
|
|
2347
|
+
delete: ["server_id", "id"]
|
|
2348
|
+
},
|
|
2349
|
+
create: {
|
|
2350
|
+
name: {
|
|
2351
|
+
required: true,
|
|
2352
|
+
type: "string — database name"
|
|
2353
|
+
},
|
|
2354
|
+
user: {
|
|
2355
|
+
required: false,
|
|
2356
|
+
type: "string — database user to create"
|
|
2357
|
+
},
|
|
2358
|
+
password: {
|
|
2359
|
+
required: false,
|
|
2360
|
+
type: "string — user password"
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
},
|
|
2364
|
+
"database-users": {
|
|
2365
|
+
actions: [
|
|
2366
|
+
"list",
|
|
2367
|
+
"get",
|
|
2368
|
+
"create",
|
|
2369
|
+
"delete"
|
|
2370
|
+
],
|
|
2371
|
+
scope: "server",
|
|
2372
|
+
required: {
|
|
2373
|
+
list: ["server_id"],
|
|
2374
|
+
get: ["server_id", "id"],
|
|
2375
|
+
create: [
|
|
2376
|
+
"server_id",
|
|
2377
|
+
"name",
|
|
2378
|
+
"password"
|
|
2379
|
+
],
|
|
2380
|
+
delete: ["server_id", "id"]
|
|
2381
|
+
},
|
|
2382
|
+
create: {
|
|
2383
|
+
name: {
|
|
2384
|
+
required: true,
|
|
2385
|
+
type: "string — database user name"
|
|
2386
|
+
},
|
|
2387
|
+
password: {
|
|
2388
|
+
required: true,
|
|
2389
|
+
type: "string — user password"
|
|
2390
|
+
},
|
|
2391
|
+
databases: {
|
|
2392
|
+
required: false,
|
|
2393
|
+
type: "array — database IDs to grant access to"
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
},
|
|
2397
|
+
daemons: {
|
|
2398
|
+
actions: [
|
|
2399
|
+
"list",
|
|
2400
|
+
"get",
|
|
2401
|
+
"create",
|
|
2402
|
+
"delete",
|
|
2403
|
+
"restart"
|
|
2404
|
+
],
|
|
2405
|
+
scope: "server",
|
|
2406
|
+
required: {
|
|
2407
|
+
list: ["server_id"],
|
|
2408
|
+
get: ["server_id", "id"],
|
|
2409
|
+
create: ["server_id", "command"],
|
|
2410
|
+
delete: ["server_id", "id"],
|
|
2411
|
+
restart: ["server_id", "id"]
|
|
2412
|
+
},
|
|
2413
|
+
create: {
|
|
2414
|
+
command: {
|
|
2415
|
+
required: true,
|
|
2416
|
+
type: "string — e.g. php artisan queue:work"
|
|
2417
|
+
},
|
|
2418
|
+
user: {
|
|
2419
|
+
required: false,
|
|
2420
|
+
type: "string — default: forge"
|
|
2421
|
+
},
|
|
2422
|
+
directory: {
|
|
2423
|
+
required: false,
|
|
2424
|
+
type: "string — working directory"
|
|
2425
|
+
},
|
|
2426
|
+
processes: {
|
|
2427
|
+
required: false,
|
|
2428
|
+
type: "number — default: 1"
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
},
|
|
2432
|
+
"firewall-rules": {
|
|
2433
|
+
actions: [
|
|
2434
|
+
"list",
|
|
2435
|
+
"get",
|
|
2436
|
+
"create",
|
|
2437
|
+
"delete"
|
|
2438
|
+
],
|
|
2439
|
+
scope: "server",
|
|
2440
|
+
required: {
|
|
2441
|
+
list: ["server_id"],
|
|
2442
|
+
get: ["server_id", "id"],
|
|
2443
|
+
create: [
|
|
2444
|
+
"server_id",
|
|
2445
|
+
"name",
|
|
2446
|
+
"port"
|
|
2447
|
+
],
|
|
2448
|
+
delete: ["server_id", "id"]
|
|
2449
|
+
},
|
|
2450
|
+
create: {
|
|
2451
|
+
name: {
|
|
2452
|
+
required: true,
|
|
2453
|
+
type: "string"
|
|
2454
|
+
},
|
|
2455
|
+
port: {
|
|
2456
|
+
required: true,
|
|
2457
|
+
type: "number|string — e.g. 80, 443, 8000-9000"
|
|
2458
|
+
},
|
|
2459
|
+
type: {
|
|
2460
|
+
required: false,
|
|
2461
|
+
type: "string — allow (default) or deny"
|
|
2462
|
+
},
|
|
2463
|
+
ip_address: {
|
|
2464
|
+
required: false,
|
|
2465
|
+
type: "string — restrict to IP"
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
"ssh-keys": {
|
|
2470
|
+
actions: [
|
|
2471
|
+
"list",
|
|
2472
|
+
"get",
|
|
2473
|
+
"create",
|
|
2474
|
+
"delete"
|
|
2475
|
+
],
|
|
2476
|
+
scope: "server",
|
|
2477
|
+
required: {
|
|
2478
|
+
list: ["server_id"],
|
|
2479
|
+
get: ["server_id", "id"],
|
|
2480
|
+
create: [
|
|
2481
|
+
"server_id",
|
|
2482
|
+
"name",
|
|
2483
|
+
"key"
|
|
2484
|
+
],
|
|
2485
|
+
delete: ["server_id", "id"]
|
|
2486
|
+
},
|
|
2487
|
+
create: {
|
|
2488
|
+
name: {
|
|
2489
|
+
required: true,
|
|
2490
|
+
type: "string — key label"
|
|
2491
|
+
},
|
|
2492
|
+
key: {
|
|
2493
|
+
required: true,
|
|
2494
|
+
type: "string — public key content"
|
|
2495
|
+
},
|
|
2496
|
+
username: {
|
|
2497
|
+
required: false,
|
|
2498
|
+
type: "string — user to add key to"
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
},
|
|
2502
|
+
"security-rules": {
|
|
2503
|
+
actions: [
|
|
2504
|
+
"list",
|
|
2505
|
+
"get",
|
|
2506
|
+
"create",
|
|
2507
|
+
"delete"
|
|
2508
|
+
],
|
|
2509
|
+
scope: "site",
|
|
2510
|
+
required: {
|
|
2511
|
+
list: ["server_id", "site_id"],
|
|
2512
|
+
get: [
|
|
2513
|
+
"server_id",
|
|
2514
|
+
"site_id",
|
|
2515
|
+
"id"
|
|
2516
|
+
],
|
|
2517
|
+
create: [
|
|
2518
|
+
"server_id",
|
|
2519
|
+
"site_id",
|
|
2520
|
+
"name",
|
|
2521
|
+
"credentials"
|
|
2522
|
+
],
|
|
2523
|
+
delete: [
|
|
2524
|
+
"server_id",
|
|
2525
|
+
"site_id",
|
|
2526
|
+
"id"
|
|
2527
|
+
]
|
|
2528
|
+
},
|
|
2529
|
+
create: {
|
|
2530
|
+
name: {
|
|
2531
|
+
required: true,
|
|
2532
|
+
type: "string — rule name"
|
|
2533
|
+
},
|
|
2534
|
+
path: {
|
|
2535
|
+
required: false,
|
|
2536
|
+
type: "string — protected path (default: /)"
|
|
2537
|
+
},
|
|
2538
|
+
credentials: {
|
|
2539
|
+
required: true,
|
|
2540
|
+
type: "array — [{username, password}]"
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
"redirect-rules": {
|
|
2545
|
+
actions: [
|
|
2546
|
+
"list",
|
|
2547
|
+
"get",
|
|
2548
|
+
"create",
|
|
2549
|
+
"delete"
|
|
2550
|
+
],
|
|
2551
|
+
scope: "site",
|
|
2552
|
+
required: {
|
|
2553
|
+
list: ["server_id", "site_id"],
|
|
2554
|
+
get: [
|
|
2555
|
+
"server_id",
|
|
2556
|
+
"site_id",
|
|
2557
|
+
"id"
|
|
2558
|
+
],
|
|
2559
|
+
create: [
|
|
2560
|
+
"server_id",
|
|
2561
|
+
"site_id",
|
|
2562
|
+
"from",
|
|
2563
|
+
"to"
|
|
2564
|
+
],
|
|
2565
|
+
delete: [
|
|
2566
|
+
"server_id",
|
|
2567
|
+
"site_id",
|
|
2568
|
+
"id"
|
|
2569
|
+
]
|
|
2570
|
+
},
|
|
2571
|
+
create: {
|
|
2572
|
+
from: {
|
|
2573
|
+
required: true,
|
|
2574
|
+
type: "string — source path"
|
|
2575
|
+
},
|
|
2576
|
+
to: {
|
|
2577
|
+
required: true,
|
|
2578
|
+
type: "string — destination URL"
|
|
2579
|
+
},
|
|
2580
|
+
type: {
|
|
2581
|
+
required: false,
|
|
2582
|
+
type: "string — redirect (302) or permanent (301)"
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
monitors: {
|
|
2587
|
+
actions: [
|
|
2588
|
+
"list",
|
|
2589
|
+
"get",
|
|
2590
|
+
"create",
|
|
2591
|
+
"delete"
|
|
2592
|
+
],
|
|
2593
|
+
scope: "server",
|
|
2594
|
+
required: {
|
|
2595
|
+
list: ["server_id"],
|
|
2596
|
+
get: ["server_id", "id"],
|
|
2597
|
+
create: [
|
|
2598
|
+
"server_id",
|
|
2599
|
+
"type",
|
|
2600
|
+
"operator",
|
|
2601
|
+
"threshold",
|
|
2602
|
+
"minutes"
|
|
2603
|
+
],
|
|
2604
|
+
delete: ["server_id", "id"]
|
|
2605
|
+
},
|
|
2606
|
+
create: {
|
|
2607
|
+
type: {
|
|
2608
|
+
required: true,
|
|
2609
|
+
type: "string — disk, cpu, memory, etc."
|
|
2610
|
+
},
|
|
2611
|
+
operator: {
|
|
2612
|
+
required: true,
|
|
2613
|
+
type: "string — gte, lte"
|
|
2614
|
+
},
|
|
2615
|
+
threshold: {
|
|
2616
|
+
required: true,
|
|
2617
|
+
type: "number — e.g. 80"
|
|
2618
|
+
},
|
|
2619
|
+
minutes: {
|
|
2620
|
+
required: true,
|
|
2621
|
+
type: "number — check interval in minutes"
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
},
|
|
2625
|
+
"nginx-templates": {
|
|
2626
|
+
actions: [
|
|
2627
|
+
"list",
|
|
2628
|
+
"get",
|
|
2629
|
+
"create",
|
|
2630
|
+
"update",
|
|
2631
|
+
"delete"
|
|
2632
|
+
],
|
|
2633
|
+
scope: "server",
|
|
2634
|
+
required: {
|
|
2635
|
+
list: ["server_id"],
|
|
2636
|
+
get: ["server_id", "id"],
|
|
2637
|
+
create: [
|
|
2638
|
+
"server_id",
|
|
2639
|
+
"name",
|
|
2640
|
+
"content"
|
|
2641
|
+
],
|
|
2642
|
+
update: ["server_id", "id"],
|
|
2643
|
+
delete: ["server_id", "id"]
|
|
2644
|
+
},
|
|
2645
|
+
create: {
|
|
2646
|
+
name: {
|
|
2647
|
+
required: true,
|
|
2648
|
+
type: "string — template name"
|
|
2649
|
+
},
|
|
2650
|
+
content: {
|
|
2651
|
+
required: true,
|
|
2652
|
+
type: "string — nginx configuration template"
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
backups: {
|
|
2657
|
+
actions: [
|
|
2658
|
+
"list",
|
|
2659
|
+
"get",
|
|
2660
|
+
"create",
|
|
2661
|
+
"delete"
|
|
2662
|
+
],
|
|
2663
|
+
scope: "server",
|
|
2664
|
+
required: {
|
|
2665
|
+
list: ["server_id"],
|
|
2666
|
+
get: ["server_id", "id"],
|
|
2667
|
+
create: [
|
|
2668
|
+
"server_id",
|
|
2669
|
+
"provider",
|
|
2670
|
+
"credentials",
|
|
2671
|
+
"frequency",
|
|
2672
|
+
"databases"
|
|
2673
|
+
],
|
|
2674
|
+
delete: ["server_id", "id"]
|
|
2675
|
+
},
|
|
2676
|
+
create: {
|
|
2677
|
+
provider: {
|
|
2678
|
+
required: true,
|
|
2679
|
+
type: "string — s3, spaces, custom"
|
|
2680
|
+
},
|
|
2681
|
+
credentials: {
|
|
2682
|
+
required: true,
|
|
2683
|
+
type: "object — provider credentials (key, secret, etc.)"
|
|
2684
|
+
},
|
|
2685
|
+
frequency: {
|
|
2686
|
+
required: true,
|
|
2687
|
+
type: "string — daily, weekly, custom"
|
|
2688
|
+
},
|
|
2689
|
+
databases: {
|
|
2690
|
+
required: true,
|
|
2691
|
+
type: "array — database IDs to back up"
|
|
2692
|
+
},
|
|
2693
|
+
directory: {
|
|
2694
|
+
required: false,
|
|
2695
|
+
type: "string — backup directory"
|
|
2696
|
+
},
|
|
2697
|
+
email: {
|
|
2698
|
+
required: false,
|
|
2699
|
+
type: "string — notification email"
|
|
2700
|
+
},
|
|
2701
|
+
retention: {
|
|
2702
|
+
required: false,
|
|
2703
|
+
type: "number — backups to retain (default: 7)"
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
},
|
|
2707
|
+
commands: {
|
|
2708
|
+
actions: [
|
|
2709
|
+
"list",
|
|
2710
|
+
"get",
|
|
2711
|
+
"create"
|
|
2712
|
+
],
|
|
2713
|
+
scope: "site",
|
|
2714
|
+
required: {
|
|
2715
|
+
list: ["server_id", "site_id"],
|
|
2716
|
+
get: [
|
|
2717
|
+
"server_id",
|
|
2718
|
+
"site_id",
|
|
2719
|
+
"id"
|
|
2720
|
+
],
|
|
2721
|
+
create: [
|
|
2722
|
+
"server_id",
|
|
2723
|
+
"site_id",
|
|
2724
|
+
"command"
|
|
2725
|
+
]
|
|
2726
|
+
},
|
|
2727
|
+
create: { command: {
|
|
2728
|
+
required: true,
|
|
2729
|
+
type: "string — shell command to execute"
|
|
2730
|
+
} }
|
|
2731
|
+
},
|
|
2732
|
+
"scheduled-jobs": {
|
|
2733
|
+
actions: [
|
|
2734
|
+
"list",
|
|
2735
|
+
"get",
|
|
2736
|
+
"create",
|
|
2737
|
+
"delete"
|
|
2738
|
+
],
|
|
2739
|
+
scope: "server",
|
|
2740
|
+
required: {
|
|
2741
|
+
list: ["server_id"],
|
|
2742
|
+
get: ["server_id", "id"],
|
|
2743
|
+
create: ["server_id", "command"],
|
|
2744
|
+
delete: ["server_id", "id"]
|
|
2745
|
+
},
|
|
2746
|
+
create: {
|
|
2747
|
+
command: {
|
|
2748
|
+
required: true,
|
|
2749
|
+
type: "string — command to schedule"
|
|
2750
|
+
},
|
|
2751
|
+
user: {
|
|
2752
|
+
required: false,
|
|
2753
|
+
type: "string — execution user (default: forge)"
|
|
2754
|
+
},
|
|
2755
|
+
frequency: {
|
|
2756
|
+
required: false,
|
|
2757
|
+
type: "string — minutely, hourly, nightly, weekly, monthly, custom"
|
|
2758
|
+
},
|
|
2759
|
+
minute: {
|
|
2760
|
+
required: false,
|
|
2761
|
+
type: "string — cron minute field (custom frequency)"
|
|
2762
|
+
},
|
|
2763
|
+
hour: {
|
|
2764
|
+
required: false,
|
|
2765
|
+
type: "string — cron hour field"
|
|
2766
|
+
},
|
|
2767
|
+
day: {
|
|
2768
|
+
required: false,
|
|
2769
|
+
type: "string — cron day field"
|
|
2770
|
+
},
|
|
2771
|
+
month: {
|
|
2772
|
+
required: false,
|
|
2773
|
+
type: "string — cron month field"
|
|
2774
|
+
},
|
|
2775
|
+
weekday: {
|
|
2776
|
+
required: false,
|
|
2777
|
+
type: "string — cron weekday field"
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
},
|
|
2781
|
+
user: {
|
|
2782
|
+
actions: ["get"],
|
|
2783
|
+
scope: "global",
|
|
2784
|
+
required: {}
|
|
2785
|
+
},
|
|
2786
|
+
recipes: {
|
|
2787
|
+
actions: [
|
|
2788
|
+
"list",
|
|
2789
|
+
"get",
|
|
2790
|
+
"create",
|
|
2791
|
+
"delete",
|
|
2792
|
+
"run"
|
|
2793
|
+
],
|
|
2794
|
+
scope: "global",
|
|
2795
|
+
required: {
|
|
2796
|
+
get: ["id"],
|
|
2797
|
+
create: ["name", "script"],
|
|
2798
|
+
delete: ["id"],
|
|
2799
|
+
run: ["id", "servers"]
|
|
2800
|
+
},
|
|
2801
|
+
create: {
|
|
2802
|
+
name: {
|
|
2803
|
+
required: true,
|
|
2804
|
+
type: "string — recipe name"
|
|
2805
|
+
},
|
|
2806
|
+
script: {
|
|
2807
|
+
required: true,
|
|
2808
|
+
type: "string — bash script content"
|
|
2809
|
+
},
|
|
2810
|
+
user: {
|
|
2811
|
+
required: false,
|
|
2812
|
+
type: "string — execution user (default: root)"
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
/**
|
|
2818
|
+
* Handle schema action — returns compact spec for a specific resource.
|
|
2819
|
+
*/
|
|
2820
|
+
function handleSchema(resource) {
|
|
2821
|
+
const schema = RESOURCE_SCHEMAS[resource];
|
|
2822
|
+
if (!schema) return jsonResult({
|
|
2823
|
+
error: `Unknown resource: ${resource}`,
|
|
2824
|
+
available_resources: Object.keys(RESOURCE_SCHEMAS)
|
|
2825
|
+
});
|
|
2826
|
+
return jsonResult({
|
|
2827
|
+
resource,
|
|
2828
|
+
...schema
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Get schema overview for all resources.
|
|
2833
|
+
*/
|
|
2834
|
+
function handleSchemaOverview() {
|
|
2835
|
+
return jsonResult({
|
|
2836
|
+
_tip: "Use action=\"schema\" with a specific resource for full required/create spec",
|
|
2837
|
+
resources: Object.entries(RESOURCE_SCHEMAS).map(([resource, schema]) => ({
|
|
2838
|
+
resource,
|
|
2839
|
+
actions: schema.actions,
|
|
2840
|
+
scope: schema.scope
|
|
2841
|
+
}))
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
const handleSecurityRules = createResourceHandler({
|
|
2845
|
+
resource: "security-rules",
|
|
2846
|
+
actions: [
|
|
2847
|
+
"list",
|
|
2848
|
+
"get",
|
|
2849
|
+
"create",
|
|
2850
|
+
"delete"
|
|
2851
|
+
],
|
|
2852
|
+
requiredFields: {
|
|
2853
|
+
list: ["server_id", "site_id"],
|
|
2854
|
+
get: [
|
|
2855
|
+
"server_id",
|
|
2856
|
+
"site_id",
|
|
2857
|
+
"id"
|
|
2858
|
+
],
|
|
2859
|
+
create: [
|
|
2860
|
+
"server_id",
|
|
2861
|
+
"site_id",
|
|
2862
|
+
"name",
|
|
2863
|
+
"credentials"
|
|
2864
|
+
],
|
|
2865
|
+
delete: [
|
|
2866
|
+
"server_id",
|
|
2867
|
+
"site_id",
|
|
2868
|
+
"id"
|
|
2869
|
+
]
|
|
2870
|
+
},
|
|
2871
|
+
executors: {
|
|
2872
|
+
list: listSecurityRules,
|
|
2873
|
+
get: getSecurityRule,
|
|
2874
|
+
create: createSecurityRule,
|
|
2875
|
+
delete: deleteSecurityRule
|
|
2876
|
+
},
|
|
2877
|
+
formatResult: (action, data, args) => {
|
|
2878
|
+
switch (action) {
|
|
2879
|
+
case "list": return formatSecurityRuleList(data);
|
|
2880
|
+
case "get": return formatSecurityRule(data);
|
|
2881
|
+
case "create": return formatSecurityRule(data);
|
|
2882
|
+
case "delete": return `Security rule ${args.id} deleted.`;
|
|
2883
|
+
default: return "Done.";
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
const handleServers = createResourceHandler({
|
|
2888
|
+
resource: "servers",
|
|
2889
|
+
actions: [
|
|
2890
|
+
"list",
|
|
2891
|
+
"get",
|
|
2892
|
+
"create",
|
|
2893
|
+
"delete",
|
|
2894
|
+
"reboot"
|
|
2895
|
+
],
|
|
2896
|
+
requiredFields: {
|
|
2897
|
+
get: ["id"],
|
|
2898
|
+
create: [
|
|
2899
|
+
"provider",
|
|
2900
|
+
"name",
|
|
2901
|
+
"type",
|
|
2902
|
+
"region"
|
|
2903
|
+
],
|
|
2904
|
+
delete: ["id"],
|
|
2905
|
+
reboot: ["id"]
|
|
2906
|
+
},
|
|
2907
|
+
executors: {
|
|
2908
|
+
list: listServers,
|
|
2909
|
+
get: getServer,
|
|
2910
|
+
create: createServer,
|
|
2911
|
+
delete: deleteServer,
|
|
2912
|
+
reboot: rebootServer
|
|
2913
|
+
},
|
|
2914
|
+
hints: (_data, id) => getServerHints(id),
|
|
2915
|
+
mapOptions: (action, args) => {
|
|
2916
|
+
switch (action) {
|
|
2917
|
+
case "get":
|
|
2918
|
+
case "delete":
|
|
2919
|
+
case "reboot": return { server_id: args.id };
|
|
2920
|
+
case "create": return {
|
|
2921
|
+
provider: args.provider,
|
|
2922
|
+
credential_id: Number(args.credential_id) || 0,
|
|
2923
|
+
name: args.name,
|
|
2924
|
+
type: args.type ?? "app",
|
|
2925
|
+
size: args.size ?? "",
|
|
2926
|
+
region: args.region
|
|
2927
|
+
};
|
|
2928
|
+
default: return {};
|
|
2929
|
+
}
|
|
2930
|
+
},
|
|
2931
|
+
formatResult: (action, data, args) => {
|
|
2932
|
+
switch (action) {
|
|
2933
|
+
case "list": return formatServerList(data);
|
|
2934
|
+
case "get": return formatServer(data);
|
|
2935
|
+
case "create": return formatServer(data);
|
|
2936
|
+
case "delete": return `Server ${args.id} deleted.`;
|
|
2937
|
+
case "reboot": return `Server ${args.id} reboot initiated.`;
|
|
2938
|
+
default: return "Done.";
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
});
|
|
2942
|
+
const handleSites = createResourceHandler({
|
|
2943
|
+
resource: "sites",
|
|
2944
|
+
actions: [
|
|
2945
|
+
"list",
|
|
2946
|
+
"get",
|
|
2947
|
+
"create",
|
|
2948
|
+
"delete"
|
|
2949
|
+
],
|
|
2950
|
+
requiredFields: {
|
|
2951
|
+
list: ["server_id"],
|
|
2952
|
+
get: ["server_id", "id"],
|
|
2953
|
+
create: ["server_id", "domain"],
|
|
2954
|
+
delete: ["server_id", "id"]
|
|
2955
|
+
},
|
|
2956
|
+
executors: {
|
|
2957
|
+
list: listSites,
|
|
2958
|
+
get: getSite,
|
|
2959
|
+
create: createSite,
|
|
2960
|
+
delete: deleteSite
|
|
2961
|
+
},
|
|
2962
|
+
hints: (data, id) => {
|
|
2963
|
+
const site = data;
|
|
2964
|
+
return getSiteHints(String(site.server_id), id);
|
|
2965
|
+
},
|
|
2966
|
+
mapOptions: (action, args) => {
|
|
2967
|
+
switch (action) {
|
|
2968
|
+
case "list": return { server_id: args.server_id };
|
|
2969
|
+
case "get": return {
|
|
2970
|
+
server_id: args.server_id,
|
|
2971
|
+
site_id: args.id
|
|
2972
|
+
};
|
|
2973
|
+
case "create": return {
|
|
2974
|
+
server_id: args.server_id,
|
|
2975
|
+
domain: args.domain,
|
|
2976
|
+
project_type: args.project_type ?? "php",
|
|
2977
|
+
directory: args.directory
|
|
2978
|
+
};
|
|
2979
|
+
case "delete": return {
|
|
2980
|
+
server_id: args.server_id,
|
|
2981
|
+
site_id: args.id
|
|
2982
|
+
};
|
|
2983
|
+
default: return {};
|
|
2984
|
+
}
|
|
2985
|
+
},
|
|
2986
|
+
formatResult: (action, data, args) => {
|
|
2987
|
+
switch (action) {
|
|
2988
|
+
case "list": return formatSiteList(data, args.server_id);
|
|
2989
|
+
case "get": return formatSite(data);
|
|
2990
|
+
case "create": return formatSite(data);
|
|
2991
|
+
case "delete": return `Site ${args.id} deleted from server ${args.server_id}.`;
|
|
2992
|
+
default: return "Done.";
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
const handleSshKeys = createResourceHandler({
|
|
2997
|
+
resource: "ssh-keys",
|
|
2998
|
+
actions: [
|
|
2999
|
+
"list",
|
|
3000
|
+
"get",
|
|
3001
|
+
"create",
|
|
3002
|
+
"delete"
|
|
3003
|
+
],
|
|
3004
|
+
requiredFields: {
|
|
3005
|
+
list: ["server_id"],
|
|
3006
|
+
get: ["server_id", "id"],
|
|
3007
|
+
create: [
|
|
3008
|
+
"server_id",
|
|
3009
|
+
"name",
|
|
3010
|
+
"key"
|
|
3011
|
+
],
|
|
3012
|
+
delete: ["server_id", "id"]
|
|
3013
|
+
},
|
|
3014
|
+
executors: {
|
|
3015
|
+
list: listSshKeys,
|
|
3016
|
+
get: getSshKey,
|
|
3017
|
+
create: createSshKey,
|
|
3018
|
+
delete: deleteSshKey
|
|
3019
|
+
},
|
|
3020
|
+
hints: (data, id) => {
|
|
3021
|
+
const key = data;
|
|
3022
|
+
return getSshKeyHints(String(key.server_id), id);
|
|
3023
|
+
},
|
|
3024
|
+
formatResult: (action, data, args) => {
|
|
3025
|
+
switch (action) {
|
|
3026
|
+
case "list": return formatSshKeyList(data);
|
|
3027
|
+
case "get": return formatSshKey(data);
|
|
3028
|
+
case "create": return formatSshKey(data);
|
|
3029
|
+
case "delete": return `SSH key ${args.id} deleted.`;
|
|
3030
|
+
default: return "Done.";
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
const handleUser = createResourceHandler({
|
|
3035
|
+
resource: "user",
|
|
3036
|
+
actions: ["get"],
|
|
3037
|
+
executors: { get: getUser },
|
|
3038
|
+
formatResult: (_action, data) => {
|
|
3039
|
+
return formatUser(data);
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
/**
|
|
3043
|
+
* Read-only actions — safe operations that don't modify server state.
|
|
3044
|
+
*/
|
|
3045
|
+
const READ_ACTIONS = [
|
|
3046
|
+
"list",
|
|
3047
|
+
"get",
|
|
3048
|
+
"help",
|
|
3049
|
+
"schema"
|
|
3050
|
+
];
|
|
3051
|
+
/**
|
|
3052
|
+
* Write actions — operations that modify server state.
|
|
3053
|
+
* These are separated into the `forge_write` tool for safety.
|
|
3054
|
+
*/
|
|
3055
|
+
const WRITE_ACTIONS = [
|
|
3056
|
+
"create",
|
|
3057
|
+
"update",
|
|
3058
|
+
"delete",
|
|
3059
|
+
"deploy",
|
|
3060
|
+
"reboot",
|
|
3061
|
+
"restart",
|
|
3062
|
+
"activate",
|
|
3063
|
+
"run"
|
|
3064
|
+
];
|
|
3065
|
+
/**
|
|
3066
|
+
* Check if an action is a write action.
|
|
3067
|
+
*/
|
|
3068
|
+
function isWriteAction(action) {
|
|
3069
|
+
return WRITE_ACTIONS.includes(action);
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Check if an action is a read action.
|
|
3073
|
+
*/
|
|
3074
|
+
function isReadAction(action) {
|
|
3075
|
+
return READ_ACTIONS.includes(action);
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Output schema shared by all forge tools.
|
|
3079
|
+
*
|
|
3080
|
+
* All tools return a consistent envelope:
|
|
3081
|
+
* - success: true/false
|
|
3082
|
+
* - result: the resource data (shape varies by resource and action)
|
|
3083
|
+
* - error: error message (only when success is false)
|
|
3084
|
+
*
|
|
3085
|
+
* The `result` field contains resource-specific data:
|
|
3086
|
+
* - list actions → array of resource objects
|
|
3087
|
+
* - get actions → single resource object (or string for env/nginx/scripts)
|
|
3088
|
+
* - help/schema → documentation text or schema object
|
|
3089
|
+
* - write actions → confirmation message or updated resource
|
|
3090
|
+
*/
|
|
3091
|
+
var OUTPUT_SCHEMA = {
|
|
3092
|
+
type: "object",
|
|
3093
|
+
properties: {
|
|
3094
|
+
success: {
|
|
3095
|
+
type: "boolean",
|
|
3096
|
+
description: "Whether the operation succeeded"
|
|
3097
|
+
},
|
|
3098
|
+
result: { description: "Operation result — shape varies by resource and action (array for list, object for get, string for text content)" },
|
|
3099
|
+
error: {
|
|
3100
|
+
type: "string",
|
|
3101
|
+
description: "Error message (only present on failure)"
|
|
3102
|
+
}
|
|
3103
|
+
},
|
|
3104
|
+
required: ["success"]
|
|
3105
|
+
};
|
|
3106
|
+
/**
|
|
3107
|
+
* Shared input schema properties used by both forge and forge_write tools.
|
|
3108
|
+
*/
|
|
3109
|
+
var SHARED_INPUT_PROPERTIES = {
|
|
3110
|
+
resource: {
|
|
3111
|
+
type: "string",
|
|
3112
|
+
enum: [...RESOURCES],
|
|
3113
|
+
description: "Forge resource to operate on"
|
|
3114
|
+
},
|
|
3115
|
+
id: {
|
|
3116
|
+
type: "string",
|
|
3117
|
+
description: "Resource ID (for get, delete, update actions)"
|
|
3118
|
+
},
|
|
3119
|
+
server_id: {
|
|
3120
|
+
type: "string",
|
|
3121
|
+
description: "Server ID (required for most resources)"
|
|
3122
|
+
},
|
|
3123
|
+
site_id: {
|
|
3124
|
+
type: "string",
|
|
3125
|
+
description: "Site ID (required for site-level resources: deployments, env, certificates, etc.)"
|
|
3126
|
+
},
|
|
3127
|
+
compact: {
|
|
3128
|
+
type: "boolean",
|
|
3129
|
+
description: "Compact output (default: true for list, false for get)"
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
/**
|
|
3133
|
+
* Core tools available in both stdio and HTTP transports.
|
|
3134
|
+
*
|
|
3135
|
+
* Split into two tools for safety:
|
|
3136
|
+
* - `forge` — read-only operations (auto-approvable by MCP clients)
|
|
3137
|
+
* - `forge_write` — write operations (always requires confirmation)
|
|
3138
|
+
*/
|
|
3139
|
+
const TOOLS = [{
|
|
3140
|
+
name: "forge",
|
|
3141
|
+
title: "Laravel Forge",
|
|
3142
|
+
description: [
|
|
3143
|
+
"Laravel Forge API — read operations.",
|
|
3144
|
+
`Resources: ${RESOURCES.join(", ")}.`,
|
|
3145
|
+
`Actions: ${[...READ_ACTIONS].join(", ")}.`,
|
|
3146
|
+
"Discovery: action=help with any resource for filters and examples.",
|
|
3147
|
+
"Server operations require id. Site operations require server_id.",
|
|
3148
|
+
"Deployment operations require server_id and site_id."
|
|
3149
|
+
].join("\n"),
|
|
3150
|
+
annotations: {
|
|
3151
|
+
title: "Laravel Forge",
|
|
3152
|
+
readOnlyHint: true,
|
|
3153
|
+
destructiveHint: false,
|
|
3154
|
+
idempotentHint: true,
|
|
3155
|
+
openWorldHint: true
|
|
3156
|
+
},
|
|
3157
|
+
inputSchema: {
|
|
3158
|
+
type: "object",
|
|
3159
|
+
properties: {
|
|
3160
|
+
...SHARED_INPUT_PROPERTIES,
|
|
3161
|
+
action: {
|
|
3162
|
+
type: "string",
|
|
3163
|
+
enum: [...READ_ACTIONS],
|
|
3164
|
+
description: "Read action to perform. Use \"help\" for resource documentation."
|
|
3165
|
+
}
|
|
3166
|
+
},
|
|
3167
|
+
required: ["resource", "action"]
|
|
3168
|
+
},
|
|
3169
|
+
outputSchema: OUTPUT_SCHEMA
|
|
3170
|
+
}, {
|
|
3171
|
+
name: "forge_write",
|
|
3172
|
+
title: "Laravel Forge (Write)",
|
|
3173
|
+
description: [
|
|
3174
|
+
"Laravel Forge API — write operations (create, update, delete, deploy, reboot, etc.).",
|
|
3175
|
+
`Resources: ${RESOURCES.join(", ")}.`,
|
|
3176
|
+
`Actions: ${[...WRITE_ACTIONS].join(", ")}.`,
|
|
3177
|
+
"Server operations require id. Site operations require server_id.",
|
|
3178
|
+
"Deployment operations require server_id and site_id.",
|
|
3179
|
+
"Use forge tool with action=help for resource documentation."
|
|
3180
|
+
].join("\n"),
|
|
3181
|
+
annotations: {
|
|
3182
|
+
title: "Laravel Forge (Write)",
|
|
3183
|
+
readOnlyHint: false,
|
|
3184
|
+
destructiveHint: true,
|
|
3185
|
+
idempotentHint: false,
|
|
3186
|
+
openWorldHint: true
|
|
3187
|
+
},
|
|
3188
|
+
inputSchema: {
|
|
3189
|
+
type: "object",
|
|
3190
|
+
properties: {
|
|
3191
|
+
...SHARED_INPUT_PROPERTIES,
|
|
3192
|
+
action: {
|
|
3193
|
+
type: "string",
|
|
3194
|
+
enum: [...WRITE_ACTIONS],
|
|
3195
|
+
description: "Write action to perform"
|
|
3196
|
+
},
|
|
3197
|
+
name: {
|
|
3198
|
+
type: "string",
|
|
3199
|
+
description: "Resource name (servers, databases, daemons, etc.)"
|
|
3200
|
+
},
|
|
3201
|
+
provider: {
|
|
3202
|
+
type: "string",
|
|
3203
|
+
description: "Server provider (e.g. ocean2, linode, aws)"
|
|
3204
|
+
},
|
|
3205
|
+
region: {
|
|
3206
|
+
type: "string",
|
|
3207
|
+
description: "Server region (e.g. nyc3, us-east-1)"
|
|
3208
|
+
},
|
|
3209
|
+
size: {
|
|
3210
|
+
type: "string",
|
|
3211
|
+
description: "Server size (e.g. s-1vcpu-1gb)"
|
|
3212
|
+
},
|
|
3213
|
+
credential_id: {
|
|
3214
|
+
type: "string",
|
|
3215
|
+
description: "Provider credential ID for server creation"
|
|
3216
|
+
},
|
|
3217
|
+
type: {
|
|
3218
|
+
type: "string",
|
|
3219
|
+
description: "Resource type (e.g. app, web, worker for servers; mysql, postgres for databases; disk_usage, used_memory for monitors)"
|
|
3220
|
+
},
|
|
3221
|
+
domain: {
|
|
3222
|
+
type: "string",
|
|
3223
|
+
description: "Site domain name (e.g. example.com)"
|
|
3224
|
+
},
|
|
3225
|
+
project_type: {
|
|
3226
|
+
type: "string",
|
|
3227
|
+
description: "Site project type (e.g. php, html, symfony, laravel)"
|
|
3228
|
+
},
|
|
3229
|
+
directory: {
|
|
3230
|
+
type: "string",
|
|
3231
|
+
description: "Web directory relative to site root (e.g. /public)"
|
|
3232
|
+
},
|
|
3233
|
+
content: {
|
|
3234
|
+
type: "string",
|
|
3235
|
+
description: "Content body for env variables, nginx config, or deployment script updates"
|
|
3236
|
+
},
|
|
3237
|
+
command: {
|
|
3238
|
+
type: "string",
|
|
3239
|
+
description: "Shell command to execute (daemons: background process command; recipes: bash script inline; commands: site command)"
|
|
3240
|
+
},
|
|
3241
|
+
user: {
|
|
3242
|
+
type: "string",
|
|
3243
|
+
description: "Unix user to run as (daemons, scheduled jobs; e.g. forge, root)"
|
|
3244
|
+
},
|
|
3245
|
+
port: {
|
|
3246
|
+
type: ["string", "number"],
|
|
3247
|
+
description: "Port number or range (firewall rules, e.g. 80 or 8000-9000)"
|
|
3248
|
+
},
|
|
3249
|
+
ip_address: {
|
|
3250
|
+
type: "string",
|
|
3251
|
+
description: "IP address to allow/block (firewall rules, e.g. 192.168.1.1)"
|
|
3252
|
+
},
|
|
3253
|
+
key: {
|
|
3254
|
+
type: "string",
|
|
3255
|
+
description: "Public SSH key content (ssh-rsa ... or ssh-ed25519 ...)"
|
|
3256
|
+
},
|
|
3257
|
+
from: {
|
|
3258
|
+
type: "string",
|
|
3259
|
+
description: "Source path for redirect rules (e.g. /old-page)"
|
|
3260
|
+
},
|
|
3261
|
+
to: {
|
|
3262
|
+
type: "string",
|
|
3263
|
+
description: "Destination URL for redirect rules (e.g. /new-page)"
|
|
3264
|
+
},
|
|
3265
|
+
credentials: {
|
|
3266
|
+
type: "array",
|
|
3267
|
+
description: "HTTP basic auth credentials for security rules [{username, password}]",
|
|
3268
|
+
items: { type: "object" }
|
|
3269
|
+
},
|
|
3270
|
+
operator: {
|
|
3271
|
+
type: "string",
|
|
3272
|
+
description: "Comparison operator for monitors (e.g. gte, lte)"
|
|
3273
|
+
},
|
|
3274
|
+
threshold: {
|
|
3275
|
+
type: "number",
|
|
3276
|
+
description: "Threshold value that triggers the monitor alert"
|
|
3277
|
+
},
|
|
3278
|
+
minutes: {
|
|
3279
|
+
type: "number",
|
|
3280
|
+
description: "Check interval in minutes for monitors (e.g. 5)"
|
|
3281
|
+
},
|
|
3282
|
+
frequency: {
|
|
3283
|
+
type: "string",
|
|
3284
|
+
description: "Cron frequency for scheduled jobs (e.g. minutely, hourly, nightly, custom)"
|
|
3285
|
+
},
|
|
3286
|
+
script: {
|
|
3287
|
+
type: "string",
|
|
3288
|
+
description: "Bash script content for recipes"
|
|
3289
|
+
},
|
|
3290
|
+
servers: {
|
|
3291
|
+
type: "array",
|
|
3292
|
+
description: "Server IDs to run a recipe on (e.g. [123, 456])",
|
|
3293
|
+
items: { type: "number" }
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
required: ["resource", "action"]
|
|
3297
|
+
},
|
|
3298
|
+
outputSchema: OUTPUT_SCHEMA
|
|
3299
|
+
}];
|
|
3300
|
+
/**
|
|
3301
|
+
* Get the list of core tools, optionally filtered.
|
|
3302
|
+
*
|
|
3303
|
+
* In read-only mode, forge_write is excluded entirely — it won't appear
|
|
3304
|
+
* in the tool listing and cannot be called.
|
|
3305
|
+
*/
|
|
3306
|
+
function getTools(options) {
|
|
3307
|
+
if (options?.readOnly) return TOOLS.filter((t) => t.name !== "forge_write");
|
|
3308
|
+
return [...TOOLS];
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Additional tools only available in stdio mode.
|
|
3312
|
+
*/
|
|
3313
|
+
const STDIO_ONLY_TOOLS = [{
|
|
3314
|
+
name: "forge_configure",
|
|
3315
|
+
title: "Configure Forge",
|
|
3316
|
+
description: "Configure Laravel Forge API token. The token is stored locally in the XDG config directory.",
|
|
3317
|
+
annotations: {
|
|
3318
|
+
title: "Configure Forge",
|
|
3319
|
+
readOnlyHint: false,
|
|
3320
|
+
destructiveHint: false,
|
|
3321
|
+
idempotentHint: true,
|
|
3322
|
+
openWorldHint: false
|
|
3323
|
+
},
|
|
3324
|
+
inputSchema: {
|
|
3325
|
+
type: "object",
|
|
3326
|
+
properties: { apiToken: {
|
|
3327
|
+
type: "string",
|
|
3328
|
+
description: "Your Laravel Forge API token"
|
|
3329
|
+
} },
|
|
3330
|
+
required: ["apiToken"]
|
|
3331
|
+
},
|
|
3332
|
+
outputSchema: {
|
|
3333
|
+
type: "object",
|
|
3334
|
+
properties: {
|
|
3335
|
+
success: { type: "boolean" },
|
|
3336
|
+
message: {
|
|
3337
|
+
type: "string",
|
|
3338
|
+
description: "Confirmation message"
|
|
3339
|
+
},
|
|
3340
|
+
apiToken: {
|
|
3341
|
+
type: "string",
|
|
3342
|
+
description: "Masked API token (last 4 chars)"
|
|
3343
|
+
}
|
|
3344
|
+
},
|
|
3345
|
+
required: ["success"]
|
|
3346
|
+
}
|
|
3347
|
+
}, {
|
|
3348
|
+
name: "forge_get_config",
|
|
3349
|
+
title: "Get Forge Config",
|
|
3350
|
+
description: "Get current Forge configuration (shows masked token and config status).",
|
|
3351
|
+
annotations: {
|
|
3352
|
+
title: "Get Forge Config",
|
|
3353
|
+
readOnlyHint: true,
|
|
3354
|
+
destructiveHint: false,
|
|
3355
|
+
idempotentHint: true,
|
|
3356
|
+
openWorldHint: false
|
|
3357
|
+
},
|
|
3358
|
+
inputSchema: {
|
|
3359
|
+
type: "object",
|
|
3360
|
+
properties: {}
|
|
3361
|
+
},
|
|
3362
|
+
outputSchema: {
|
|
3363
|
+
type: "object",
|
|
3364
|
+
properties: {
|
|
3365
|
+
apiToken: {
|
|
3366
|
+
type: "string",
|
|
3367
|
+
description: "Masked API token or 'not configured'"
|
|
3368
|
+
},
|
|
3369
|
+
configured: {
|
|
3370
|
+
type: "boolean",
|
|
3371
|
+
description: "Whether a token is configured"
|
|
3372
|
+
}
|
|
3373
|
+
},
|
|
3374
|
+
required: ["configured"]
|
|
3375
|
+
}
|
|
3376
|
+
}];
|
|
3377
|
+
/** Valid resources derived from core constants */
|
|
3378
|
+
var VALID_RESOURCES = [...RESOURCES];
|
|
3379
|
+
var _auditLogger = null;
|
|
3380
|
+
function getAuditLogger() {
|
|
3381
|
+
if (!_auditLogger) _auditLogger = createAuditLogger("mcp");
|
|
3382
|
+
return _auditLogger;
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Route to the appropriate resource handler.
|
|
3386
|
+
*/
|
|
3387
|
+
function routeToHandler(resource, action, args, ctx) {
|
|
3388
|
+
switch (resource) {
|
|
3389
|
+
case "servers": return handleServers(action, args, ctx);
|
|
3390
|
+
case "sites": return handleSites(action, args, ctx);
|
|
3391
|
+
case "deployments": return handleDeployments(action, args, ctx);
|
|
3392
|
+
case "env": return handleEnv(action, args, ctx);
|
|
3393
|
+
case "nginx": return handleNginxConfig(action, args, ctx);
|
|
3394
|
+
case "certificates": return handleCertificates(action, args, ctx);
|
|
3395
|
+
case "databases": return handleDatabases(action, args, ctx);
|
|
3396
|
+
case "database-users": return handleDatabaseUsers(action, args, ctx);
|
|
3397
|
+
case "daemons": return handleDaemons(action, args, ctx);
|
|
3398
|
+
case "firewall-rules": return handleFirewallRules(action, args, ctx);
|
|
3399
|
+
case "ssh-keys": return handleSshKeys(action, args, ctx);
|
|
3400
|
+
case "security-rules": return handleSecurityRules(action, args, ctx);
|
|
3401
|
+
case "redirect-rules": return handleRedirectRules(action, args, ctx);
|
|
3402
|
+
case "monitors": return handleMonitors(action, args, ctx);
|
|
3403
|
+
case "nginx-templates": return handleNginxTemplates(action, args, ctx);
|
|
3404
|
+
case "recipes": return handleRecipes(action, args, ctx);
|
|
3405
|
+
case "backups": return handleBackups(action, args, ctx);
|
|
3406
|
+
case "commands": return handleCommands(action, args, ctx);
|
|
3407
|
+
case "scheduled-jobs": return handleScheduledJobs(action, args, ctx);
|
|
3408
|
+
case "user": return handleUser(action, args, ctx);
|
|
3409
|
+
default: return Promise.resolve(errorResult(`Unknown resource "${resource}". Valid resources: ${VALID_RESOURCES.join(", ")}. Use action="help" for documentation.`));
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
/**
|
|
3413
|
+
* Execute a tool call with provided credentials.
|
|
3414
|
+
*
|
|
3415
|
+
* This is the main entry point shared between stdio and HTTP transports.
|
|
3416
|
+
* Validates that the action matches the tool (read vs write).
|
|
3417
|
+
*/
|
|
3418
|
+
async function executeToolWithCredentials(name, args, credentials) {
|
|
3419
|
+
const { resource, action, compact, ...rest } = args;
|
|
3420
|
+
if (!resource || !action) return errorResult("Missing required fields: \"resource\" and \"action\".");
|
|
3421
|
+
if (name === "forge" && isWriteAction(action)) return errorResult(`Action "${action}" is a write operation. Use the "forge_write" tool instead.`);
|
|
3422
|
+
if (name === "forge_write" && isReadAction(action)) return errorResult(`Action "${action}" is a read operation. Use the "forge" tool instead.`);
|
|
3423
|
+
if (action === "help")
|
|
3424
|
+
/* v8 ignore start */
|
|
3425
|
+
return resource ? handleHelp(resource) : handleHelpOverview();
|
|
3426
|
+
if (action === "schema")
|
|
3427
|
+
/* v8 ignore start */
|
|
3428
|
+
return resource ? handleSchema(resource) : handleSchemaOverview();
|
|
3429
|
+
if (!VALID_RESOURCES.includes(resource)) return errorResult(`Unknown resource "${resource}". Valid resources: ${VALID_RESOURCES.join(", ")}. Use action="help" for documentation.`);
|
|
3430
|
+
const handlerContext = {
|
|
3431
|
+
executorContext: { client: new HttpClient({ token: credentials.apiToken }) },
|
|
3432
|
+
compact: compact ?? action !== "get",
|
|
3433
|
+
includeHints: action === "get"
|
|
3434
|
+
};
|
|
3435
|
+
try {
|
|
3436
|
+
const result = await routeToHandler(resource, action, {
|
|
3437
|
+
resource,
|
|
3438
|
+
action,
|
|
3439
|
+
...rest
|
|
3440
|
+
}, handlerContext);
|
|
3441
|
+
if (name === "forge_write") getAuditLogger().log({
|
|
3442
|
+
source: "mcp",
|
|
3443
|
+
resource,
|
|
3444
|
+
action,
|
|
3445
|
+
args: rest,
|
|
3446
|
+
status: result.isError ? "error" : "success"
|
|
3447
|
+
});
|
|
3448
|
+
return result;
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
let errorMessage;
|
|
3451
|
+
if (isUserInputError(error)) errorMessage = error.toFormattedMessage();
|
|
3452
|
+
else if (isForgeApiError(error)) errorMessage = `Forge API error (${error.status}): ${error.message}`;
|
|
3453
|
+
else
|
|
3454
|
+
/* v8 ignore start */
|
|
3455
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3456
|
+
if (name === "forge_write") getAuditLogger().log({
|
|
3457
|
+
source: "mcp",
|
|
3458
|
+
resource,
|
|
3459
|
+
action,
|
|
3460
|
+
args: rest,
|
|
3461
|
+
status: "error",
|
|
3462
|
+
error: errorMessage
|
|
3463
|
+
});
|
|
3464
|
+
return errorResult(errorMessage);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
const VERSION = "0.2.0";
|
|
3468
|
+
export { INSTRUCTIONS as a, getTools as i, executeToolWithCredentials as n, STDIO_ONLY_TOOLS as r, VERSION as t };
|
|
3469
|
+
|
|
3470
|
+
//# sourceMappingURL=version-DaD5zvGh.js.map
|