@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.
Files changed (3) hide show
  1. package/README.md +19 -0
  2. package/dist/index.js +186 -11
  3. 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 && typeof result === "object" && "isError" in result && result.isError === true ? 1 : 0);
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.8.1",
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
- if (result.isError === true)
29464
- process.exitCode = 1;
29465
- return;
29638
+ } else {
29639
+ console.log(JSON.stringify(result, null, 2));
29466
29640
  }
29467
- console.log(JSON.stringify(result, null, 2));
29641
+ if (cliToolResultIsError(result))
29642
+ process.exitCode = 1;
29468
29643
  return;
29469
29644
  }
29470
29645
  await runCli(cliTools, args, { commandName: "supacloud-admin" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",