@supacloud/admin 0.8.2 → 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 +176 -6
  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
@@ -28678,6 +28678,150 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
28678
28678
  }
28679
28679
 
28680
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
+ }
28681
28825
  var formatTasks = (data) => {
28682
28826
  if (!Array.isArray(data))
28683
28827
  return JSON.stringify(data, null, 2);
@@ -28721,7 +28865,7 @@ function resolveRef(refFromArgs, defaultRef) {
28721
28865
  return ref;
28722
28866
  }
28723
28867
  function registerAdminProjectCliTools(server, http) {
28724
- 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", {
28725
28869
  action: withDescription(stringEnum([
28726
28870
  "list",
28727
28871
  "create",
@@ -28735,9 +28879,11 @@ function registerAdminProjectCliTools(server, http) {
28735
28879
  "api_keys",
28736
28880
  "health",
28737
28881
  "logs",
28738
- "tasks"
28882
+ "tasks",
28883
+ "services",
28884
+ "service_control"
28739
28885
  ]), "Action to perform"),
28740
- 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')"),
28741
28887
  name: optional(Type.String(), "[create] Project name"),
28742
28888
  region: optional(Type.String(), "[create] Region (default: local)"),
28743
28889
  organization_id: optional(Type.String(), "[create] Organization ID"),
@@ -28746,7 +28892,9 @@ function registerAdminProjectCliTools(server, http) {
28746
28892
  auth_domain: optional(Type.String(), "[create] Explicit Auth/OIDC domain"),
28747
28893
  studio_domain: optional(Type.String(), "[create] Explicit Studio domain"),
28748
28894
  settings: optional(Type.Record(Type.String(), Type.Unknown()), "[update_settings] Config fields to update"),
28749
- 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")
28750
28898
  }, async ({
28751
28899
  action,
28752
28900
  ref,
@@ -28758,7 +28906,9 @@ function registerAdminProjectCliTools(server, http) {
28758
28906
  auth_domain,
28759
28907
  studio_domain,
28760
28908
  settings,
28761
- log_type
28909
+ log_type,
28910
+ service,
28911
+ service_action
28762
28912
  }) => {
28763
28913
  let text;
28764
28914
  switch (action) {
@@ -28834,6 +28984,24 @@ function registerAdminProjectCliTools(server, http) {
28834
28984
  text = res.ok ? formatTasks(res.data) : `❌ Failed (${res.status})`;
28835
28985
  break;
28836
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
+ }
28837
29005
  default:
28838
29006
  text = `❌ Unknown action: ${action}`;
28839
29007
  }
@@ -29172,7 +29340,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
29172
29340
  // package.json
29173
29341
  var package_default = {
29174
29342
  name: "@supacloud/admin",
29175
- version: "0.8.2",
29343
+ version: "0.9.0",
29176
29344
  description: "Platform administration CLI for SupaCloud operators",
29177
29345
  type: "module",
29178
29346
  main: "./dist/index.js",
@@ -29286,6 +29454,8 @@ EXAMPLES
29286
29454
  supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
29287
29455
  supacloud-admin project create --name my-app --domain example.com
29288
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
29289
29459
  supacloud-admin platform metrics
29290
29460
  supacloud-admin gateway routes --ref abc123
29291
29461
  supacloud-admin gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",