@supacloud/admin 0.8.1 → 0.9.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 +19 -0
- package/dist/index.js +186 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ npx @supacloud/admin ssh versions
|
|
|
31
31
|
npx @supacloud/admin ssh diagnose
|
|
32
32
|
npx @supacloud/admin project create --name my-app --domain example.com
|
|
33
33
|
npx @supacloud/admin project list
|
|
34
|
+
npx @supacloud/admin project services --ref abc123
|
|
35
|
+
npx @supacloud/admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
`ssh versions` emits JSON with `schema_version: 1` and fixed
|
|
@@ -106,6 +108,23 @@ Project commands owned by this CLI:
|
|
|
106
108
|
- `project restore`
|
|
107
109
|
- `project restart`
|
|
108
110
|
- `project update_settings`
|
|
111
|
+
- `project services` — read-only project service inventory
|
|
112
|
+
- `project service_control` — constrained project service lifecycle control
|
|
113
|
+
|
|
114
|
+
Service control accepts canonical service names only. `postgrest` supports
|
|
115
|
+
`start`, `stop`, `restart`, `pause`, `resume`, and `status`; `gotrue`, `storage`,
|
|
116
|
+
`postgresql`, `realtime`, and `gateway` support `start`, `stop`, and `restart`.
|
|
117
|
+
The command calls only the Management API's existing
|
|
118
|
+
`/v1/projects/{ref}/services` routes. A non-2xx response, a `success: false`
|
|
119
|
+
receipt, or a response that does not match the requested service and action
|
|
120
|
+
exits non-zero. Successful inventory and control responses are emitted as JSON
|
|
121
|
+
with `project_ref` for strict read-back.
|
|
122
|
+
|
|
123
|
+
The Management API remains authoritative for SupAuth ownership. Controlling
|
|
124
|
+
GoTrue on a shared-auth project fails with `AUTH_RUNTIME_MANAGED_BY_OWNER`;
|
|
125
|
+
the CLI does not redirect the operation to the owner project. Supply
|
|
126
|
+
`SUPACLOUD_API_TOKEN` through the environment only; service-control commands do
|
|
127
|
+
not accept credential flags.
|
|
109
128
|
|
|
110
129
|
Gateway / Caddy commands (config is injected via the Caddy JSON Admin API; requires admin privileges):
|
|
111
130
|
|
package/dist/index.js
CHANGED
|
@@ -25302,6 +25302,11 @@ async function removePartialUpload(sftp, partialPath, uploadError) {
|
|
|
25302
25302
|
}
|
|
25303
25303
|
|
|
25304
25304
|
// src/shared/cli.ts
|
|
25305
|
+
function cliToolResultIsError(toolResult) {
|
|
25306
|
+
if (toolResult.isError === true)
|
|
25307
|
+
return true;
|
|
25308
|
+
return toolResult.content?.some((chunk) => chunk.type === "text" && chunk.text?.trimStart().startsWith("❌") === true) ?? false;
|
|
25309
|
+
}
|
|
25305
25310
|
function coerceCliValue(value) {
|
|
25306
25311
|
if (value === "true")
|
|
25307
25312
|
return true;
|
|
@@ -25430,7 +25435,7 @@ async function runCli(cliTools, args, options = {}) {
|
|
|
25430
25435
|
} else {
|
|
25431
25436
|
console.log(JSON.stringify(result, null, 2));
|
|
25432
25437
|
}
|
|
25433
|
-
process.exit(result
|
|
25438
|
+
process.exit(cliToolResultIsError(result) ? 1 : 0);
|
|
25434
25439
|
} catch (error) {
|
|
25435
25440
|
const message = formatCliError(error);
|
|
25436
25441
|
console.error(`❌ Error: ${message}`);
|
|
@@ -28673,6 +28678,150 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
|
|
|
28673
28678
|
}
|
|
28674
28679
|
|
|
28675
28680
|
// src/shared/tools/project-cli-tools.ts
|
|
28681
|
+
var PROJECT_SERVICE_NAMES = [
|
|
28682
|
+
"postgrest",
|
|
28683
|
+
"gotrue",
|
|
28684
|
+
"storage",
|
|
28685
|
+
"postgresql",
|
|
28686
|
+
"realtime",
|
|
28687
|
+
"gateway"
|
|
28688
|
+
];
|
|
28689
|
+
var PROJECT_SERVICE_CONTROL_ACTIONS = [
|
|
28690
|
+
"start",
|
|
28691
|
+
"stop",
|
|
28692
|
+
"restart",
|
|
28693
|
+
"pause",
|
|
28694
|
+
"resume",
|
|
28695
|
+
"status"
|
|
28696
|
+
];
|
|
28697
|
+
var STUDIO_PROJECT_SERVICE_NAMES = [
|
|
28698
|
+
"db",
|
|
28699
|
+
"rest",
|
|
28700
|
+
"auth",
|
|
28701
|
+
"realtime",
|
|
28702
|
+
"storage"
|
|
28703
|
+
];
|
|
28704
|
+
var STUDIO_PROJECT_SERVICE_STATUSES = [
|
|
28705
|
+
"ACTIVE_HEALTHY",
|
|
28706
|
+
"COMING_UP",
|
|
28707
|
+
"UNHEALTHY"
|
|
28708
|
+
];
|
|
28709
|
+
var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
|
|
28710
|
+
var AUTH_SERVICE_HOST_SUFFIX = "-auth";
|
|
28711
|
+
var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
|
|
28712
|
+
var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
|
|
28713
|
+
var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
|
|
28714
|
+
var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
|
|
28715
|
+
postgrest: ["start", "stop", "restart", "pause", "resume", "status"],
|
|
28716
|
+
gotrue: ["start", "stop", "restart"],
|
|
28717
|
+
storage: ["start", "stop", "restart"],
|
|
28718
|
+
postgresql: ["start", "stop", "restart"],
|
|
28719
|
+
realtime: ["start", "stop", "restart"],
|
|
28720
|
+
gateway: ["start", "stop", "restart"]
|
|
28721
|
+
};
|
|
28722
|
+
function projectToolResponse(text) {
|
|
28723
|
+
return { content: [{ type: "text", text }] };
|
|
28724
|
+
}
|
|
28725
|
+
function failedProjectServiceResponse(message) {
|
|
28726
|
+
return {
|
|
28727
|
+
content: [{ type: "text", text: `❌ ${message}` }],
|
|
28728
|
+
isError: true
|
|
28729
|
+
};
|
|
28730
|
+
}
|
|
28731
|
+
function failedProjectServiceHttpResponse(response) {
|
|
28732
|
+
if (response.status === 409 && isRecordPayload(response.data) && response.data.code === AUTH_RUNTIME_MANAGED_BY_OWNER && typeof response.data.authority_project_ref === "string" && SAFE_AUTHORITY_PROJECT_REF.test(response.data.authority_project_ref)) {
|
|
28733
|
+
const ownerBoundary = {
|
|
28734
|
+
status: response.status,
|
|
28735
|
+
code: AUTH_RUNTIME_MANAGED_BY_OWNER,
|
|
28736
|
+
authority_project_ref: response.data.authority_project_ref
|
|
28737
|
+
};
|
|
28738
|
+
return failedProjectServiceResponse(JSON.stringify(ownerBoundary));
|
|
28739
|
+
}
|
|
28740
|
+
return failedProjectServiceResponse(`Failed (${response.status})`);
|
|
28741
|
+
}
|
|
28742
|
+
function isRecordPayload(payload) {
|
|
28743
|
+
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
28744
|
+
}
|
|
28745
|
+
function isStudioProjectServiceName(name) {
|
|
28746
|
+
return typeof name === "string" && STUDIO_PROJECT_SERVICE_NAMES.some((allowedName) => name === allowedName);
|
|
28747
|
+
}
|
|
28748
|
+
function hasValidStudioServiceHealth(serviceId, status, healthy) {
|
|
28749
|
+
if (status === "INACTIVE")
|
|
28750
|
+
return serviceId === "auth" && healthy === false;
|
|
28751
|
+
const knownStatus = STUDIO_PROJECT_SERVICE_STATUSES.some((allowedStatus) => status === allowedStatus);
|
|
28752
|
+
return knownStatus && healthy === (status === "ACTIVE_HEALTHY");
|
|
28753
|
+
}
|
|
28754
|
+
function hasValidStudioServiceHost(serviceId, projectRef, hostIds) {
|
|
28755
|
+
if (!Array.isArray(hostIds) || hostIds.length !== 1 || typeof hostIds[0] !== "string")
|
|
28756
|
+
return false;
|
|
28757
|
+
if (serviceId === "auth") {
|
|
28758
|
+
if (!hostIds[0].endsWith(AUTH_SERVICE_HOST_SUFFIX))
|
|
28759
|
+
return false;
|
|
28760
|
+
return SAFE_AUTHORITY_PROJECT_REF.test(hostIds[0].slice(0, -AUTH_SERVICE_HOST_SUFFIX.length));
|
|
28761
|
+
}
|
|
28762
|
+
return hostIds[0] === `${projectRef}-${serviceId}`;
|
|
28763
|
+
}
|
|
28764
|
+
function isProjectServiceStatus(payload, projectRef) {
|
|
28765
|
+
if (!isRecordPayload(payload))
|
|
28766
|
+
return false;
|
|
28767
|
+
if (!isStudioProjectServiceName(payload.id) || payload.name !== payload.id)
|
|
28768
|
+
return false;
|
|
28769
|
+
return hasValidStudioServiceHealth(payload.id, payload.status, payload.healthy) && hasValidStudioServiceHost(payload.id, projectRef, payload.service_host_ids);
|
|
28770
|
+
}
|
|
28771
|
+
function projectServiceStatusOutput(status) {
|
|
28772
|
+
return {
|
|
28773
|
+
id: status.id,
|
|
28774
|
+
name: status.name,
|
|
28775
|
+
status: status.status,
|
|
28776
|
+
healthy: status.healthy,
|
|
28777
|
+
service_host_ids: [status.service_host_ids[0]]
|
|
28778
|
+
};
|
|
28779
|
+
}
|
|
28780
|
+
function projectServicesResponse(projectRef, response) {
|
|
28781
|
+
if (!response.ok)
|
|
28782
|
+
return failedProjectServiceHttpResponse(response);
|
|
28783
|
+
if (!SAFE_PROJECT_REF2.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
|
|
28784
|
+
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
28785
|
+
}
|
|
28786
|
+
if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
|
|
28787
|
+
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
28788
|
+
}
|
|
28789
|
+
const serviceIds = new Set(response.data.map((service) => service.id));
|
|
28790
|
+
if (serviceIds.size !== STUDIO_PROJECT_SERVICE_NAMES.length) {
|
|
28791
|
+
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
28792
|
+
}
|
|
28793
|
+
const services = response.data.map(projectServiceStatusOutput);
|
|
28794
|
+
return projectToolResponse(JSON.stringify({ project_ref: projectRef, services }, null, 2));
|
|
28795
|
+
}
|
|
28796
|
+
function supportsProjectServiceAction(service, action) {
|
|
28797
|
+
return SUPPORTED_PROJECT_SERVICE_ACTIONS[service].includes(action);
|
|
28798
|
+
}
|
|
28799
|
+
function projectServiceReceiptError(receipt, requestedService, requestedAction) {
|
|
28800
|
+
if (!isRecordPayload(receipt))
|
|
28801
|
+
return "Project service control response is invalid";
|
|
28802
|
+
if (receipt.service !== requestedService || receipt.action !== requestedAction) {
|
|
28803
|
+
return "Project service control response does not match the request";
|
|
28804
|
+
}
|
|
28805
|
+
if (receipt.success === false)
|
|
28806
|
+
return "Project service control failed";
|
|
28807
|
+
if (receipt.success !== true || typeof receipt.message !== "string" || receipt.message.length > MAX_SERVICE_CONTROL_MESSAGE_LENGTH) {
|
|
28808
|
+
return "Project service control response is invalid";
|
|
28809
|
+
}
|
|
28810
|
+
return null;
|
|
28811
|
+
}
|
|
28812
|
+
function projectServiceControlResponse(projectRef, requestedService, requestedAction, response) {
|
|
28813
|
+
if (!response.ok)
|
|
28814
|
+
return failedProjectServiceHttpResponse(response);
|
|
28815
|
+
const receiptError = projectServiceReceiptError(response.data, requestedService, requestedAction);
|
|
28816
|
+
if (receiptError)
|
|
28817
|
+
return failedProjectServiceResponse(receiptError);
|
|
28818
|
+
return projectToolResponse(JSON.stringify({
|
|
28819
|
+
project_ref: projectRef,
|
|
28820
|
+
service: requestedService,
|
|
28821
|
+
action: requestedAction,
|
|
28822
|
+
success: true
|
|
28823
|
+
}, null, 2));
|
|
28824
|
+
}
|
|
28676
28825
|
var formatTasks = (data) => {
|
|
28677
28826
|
if (!Array.isArray(data))
|
|
28678
28827
|
return JSON.stringify(data, null, 2);
|
|
@@ -28716,7 +28865,7 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
28716
28865
|
return ref;
|
|
28717
28866
|
}
|
|
28718
28867
|
function registerAdminProjectCliTools(server, http) {
|
|
28719
|
-
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks", {
|
|
28868
|
+
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, service_control", {
|
|
28720
28869
|
action: withDescription(stringEnum([
|
|
28721
28870
|
"list",
|
|
28722
28871
|
"create",
|
|
@@ -28730,9 +28879,11 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28730
28879
|
"api_keys",
|
|
28731
28880
|
"health",
|
|
28732
28881
|
"logs",
|
|
28733
|
-
"tasks"
|
|
28882
|
+
"tasks",
|
|
28883
|
+
"services",
|
|
28884
|
+
"service_control"
|
|
28734
28885
|
]), "Action to perform"),
|
|
28735
|
-
ref: optional(Type.String(), "Project ref (required for most actions except 'list' and 'create')"),
|
|
28886
|
+
ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
|
|
28736
28887
|
name: optional(Type.String(), "[create] Project name"),
|
|
28737
28888
|
region: optional(Type.String(), "[create] Region (default: local)"),
|
|
28738
28889
|
organization_id: optional(Type.String(), "[create] Organization ID"),
|
|
@@ -28741,7 +28892,9 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28741
28892
|
auth_domain: optional(Type.String(), "[create] Explicit Auth/OIDC domain"),
|
|
28742
28893
|
studio_domain: optional(Type.String(), "[create] Explicit Studio domain"),
|
|
28743
28894
|
settings: optional(Type.Record(Type.String(), Type.Unknown()), "[update_settings] Config fields to update"),
|
|
28744
|
-
log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service")
|
|
28895
|
+
log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service"),
|
|
28896
|
+
service: optional(stringEnum(PROJECT_SERVICE_NAMES), "[service_control] Canonical service name"),
|
|
28897
|
+
service_action: optional(stringEnum(PROJECT_SERVICE_CONTROL_ACTIONS), "[service_control] Supported action for the selected service")
|
|
28745
28898
|
}, async ({
|
|
28746
28899
|
action,
|
|
28747
28900
|
ref,
|
|
@@ -28753,7 +28906,9 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28753
28906
|
auth_domain,
|
|
28754
28907
|
studio_domain,
|
|
28755
28908
|
settings,
|
|
28756
|
-
log_type
|
|
28909
|
+
log_type,
|
|
28910
|
+
service,
|
|
28911
|
+
service_action
|
|
28757
28912
|
}) => {
|
|
28758
28913
|
let text;
|
|
28759
28914
|
switch (action) {
|
|
@@ -28829,6 +28984,24 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28829
28984
|
text = res.ok ? formatTasks(res.data) : `❌ Failed (${res.status})`;
|
|
28830
28985
|
break;
|
|
28831
28986
|
}
|
|
28987
|
+
case "services": {
|
|
28988
|
+
const resolvedRef = resolveRef(ref);
|
|
28989
|
+
return projectServicesResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/services`));
|
|
28990
|
+
}
|
|
28991
|
+
case "service_control": {
|
|
28992
|
+
const resolvedRef = resolveRef(ref);
|
|
28993
|
+
if (!service)
|
|
28994
|
+
throw new Error("'service' is required for service_control");
|
|
28995
|
+
if (!service_action)
|
|
28996
|
+
throw new Error("'service_action' is required for service_control");
|
|
28997
|
+
if (!supportsProjectServiceAction(service, service_action)) {
|
|
28998
|
+
throw new Error(`'${service_action}' is not supported for service '${service}'`);
|
|
28999
|
+
}
|
|
29000
|
+
const encodedRef = encodeURIComponent(resolvedRef);
|
|
29001
|
+
const encodedService = encodeURIComponent(service);
|
|
29002
|
+
const encodedAction = encodeURIComponent(service_action);
|
|
29003
|
+
return projectServiceControlResponse(resolvedRef, service, service_action, await http.post(`/v1/projects/${encodedRef}/services/${encodedService}/${encodedAction}`));
|
|
29004
|
+
}
|
|
28832
29005
|
default:
|
|
28833
29006
|
text = `❌ Unknown action: ${action}`;
|
|
28834
29007
|
}
|
|
@@ -29167,7 +29340,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
29167
29340
|
// package.json
|
|
29168
29341
|
var package_default = {
|
|
29169
29342
|
name: "@supacloud/admin",
|
|
29170
|
-
version: "0.
|
|
29343
|
+
version: "0.9.0",
|
|
29171
29344
|
description: "Platform administration CLI for SupaCloud operators",
|
|
29172
29345
|
type: "module",
|
|
29173
29346
|
main: "./dist/index.js",
|
|
@@ -29281,6 +29454,8 @@ EXAMPLES
|
|
|
29281
29454
|
supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
|
|
29282
29455
|
supacloud-admin project create --name my-app --domain example.com
|
|
29283
29456
|
supacloud-admin project list
|
|
29457
|
+
supacloud-admin project services --ref abc123
|
|
29458
|
+
supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
29284
29459
|
supacloud-admin platform metrics
|
|
29285
29460
|
supacloud-admin gateway routes --ref abc123
|
|
29286
29461
|
supacloud-admin gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
|
|
@@ -29460,11 +29635,11 @@ async function main() {
|
|
|
29460
29635
|
console.log(chunk.text);
|
|
29461
29636
|
}
|
|
29462
29637
|
}
|
|
29463
|
-
|
|
29464
|
-
|
|
29465
|
-
return;
|
|
29638
|
+
} else {
|
|
29639
|
+
console.log(JSON.stringify(result, null, 2));
|
|
29466
29640
|
}
|
|
29467
|
-
|
|
29641
|
+
if (cliToolResultIsError(result))
|
|
29642
|
+
process.exitCode = 1;
|
|
29468
29643
|
return;
|
|
29469
29644
|
}
|
|
29470
29645
|
await runCli(cliTools, args, { commandName: "supacloud-admin" });
|