@supacloud/cli 0.32.0 → 0.34.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 +55 -0
- package/dist/index.js +820 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,32 @@ loopback development origins, with the default `:80` likewise omitted. Use
|
|
|
106
106
|
`SUPACLOUD_PROJECT_REF` when it cannot be inferred from a managed
|
|
107
107
|
`<ref>.api.*` application hostname.
|
|
108
108
|
|
|
109
|
+
### Immutable frontend releases
|
|
110
|
+
|
|
111
|
+
The `frontend` command keeps the existing deployment, Git, and legacy ZIP
|
|
112
|
+
actions and also exposes the immutable prebuilt release workflow:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
supacloud-cli frontend list_releases --ref abc123 --id web
|
|
116
|
+
supacloud-cli frontend get_release --ref abc123 --id web --release_id <sha256>
|
|
117
|
+
supacloud-cli frontend upload_release --ref abc123 --id web --zip_path ./dist.zip
|
|
118
|
+
supacloud-cli frontend activate_release --ref abc123 --id web \
|
|
119
|
+
--release_id <sha256> \
|
|
120
|
+
--expected_active_release_id absent \
|
|
121
|
+
--expected_activation_id absent \
|
|
122
|
+
--mutation_id <retry-stable-uuid-v4>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`upload_release` hashes and streams an existing regular ZIP file without
|
|
126
|
+
buffering the full archive. The Management API binds the upload to that SHA-256,
|
|
127
|
+
and the CLI reads the immutable release back before reporting success.
|
|
128
|
+
`activate_release` uses both the observed active release and activation IDs as
|
|
129
|
+
optimistic concurrency tokens, then verifies the authoritative active release.
|
|
130
|
+
Use the values returned by `list_releases`; `absent` is valid only when no
|
|
131
|
+
release has been activated. Production uploads and activations require the
|
|
132
|
+
normal exact `--confirm-production <ref>` value, and
|
|
133
|
+
`SUPACLOUD_READ_ONLY=true` blocks both mutations.
|
|
134
|
+
|
|
109
135
|
### Verified release controls
|
|
110
136
|
|
|
111
137
|
`release` is an official CLI entry point for verified Management API controls
|
|
@@ -440,6 +466,35 @@ Use `--data_mode full_clone` only for an explicitly approved non-sensitive or
|
|
|
440
466
|
masked debugging dataset. Whole-database replacement is an administrator-only
|
|
441
467
|
break-glass API mode and is intentionally not exposed by this project CLI.
|
|
442
468
|
|
|
469
|
+
## SupaCloud Lite CLI adapter
|
|
470
|
+
|
|
471
|
+
Lite can be used from its standalone `supacloud-lite` CLI and from the main
|
|
472
|
+
`supacloud-cli` through the local-only `lite` module. The adapter never calls
|
|
473
|
+
the Management API, never invokes the official Supabase CLI, and never treats a
|
|
474
|
+
PGlite data directory as a Postgres DSN.
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
supacloud-cli lite migrate --project_dir .
|
|
478
|
+
supacloud-cli lite status --project_dir .
|
|
479
|
+
supacloud-cli lite db_diff --project_dir . --file add_accounts
|
|
480
|
+
supacloud-cli lite db_pull --project_dir . --file remote_schema
|
|
481
|
+
supacloud-cli lite gen_types --project_dir . --output src/database.types.ts
|
|
482
|
+
supacloud-cli lite snapshot_create --project_dir . --output backups/lite.tar.gz
|
|
483
|
+
supacloud-cli lite doctor --project_dir . --json
|
|
484
|
+
supacloud-cli lite start --project_dir . --port 54321
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The adapter resolves the executable in this order:
|
|
488
|
+
|
|
489
|
+
1. `SUPACLOUD_LITE_CLI_BIN`
|
|
490
|
+
2. `<workdir>/node_modules/@supacloud/lite/dist/launcher.cjs`
|
|
491
|
+
3. `supacloud-lite` on `PATH`
|
|
492
|
+
|
|
493
|
+
Install `@supacloud/lite` or provide an explicit binary before using the
|
|
494
|
+
adapter. Lite actions are local-only, so Management API context and project
|
|
495
|
+
refs are not required. The `supabase` module remains the official CLI adapter;
|
|
496
|
+
use it for upstream Supabase CLI actions and Management-backed remote pushes.
|
|
497
|
+
|
|
443
498
|
## Official Supabase CLI adapter
|
|
444
499
|
|
|
445
500
|
The `supabase` command group is a thin, allowlisted adapter around the official
|
package/dist/index.js
CHANGED
|
@@ -6471,6 +6471,24 @@ var ACTION_POLICY = {
|
|
|
6471
6471
|
local: ["version", "migration_new", "db_diff", "db_reset", "db_pull", "db_dump", "migration_list", "gen_types"],
|
|
6472
6472
|
write: ["push"]
|
|
6473
6473
|
},
|
|
6474
|
+
lite: {
|
|
6475
|
+
local: [
|
|
6476
|
+
"version",
|
|
6477
|
+
"start",
|
|
6478
|
+
"migrate",
|
|
6479
|
+
"status",
|
|
6480
|
+
"keys",
|
|
6481
|
+
"gen_types",
|
|
6482
|
+
"db_reset",
|
|
6483
|
+
"db_diff",
|
|
6484
|
+
"db_pull",
|
|
6485
|
+
"snapshot_create",
|
|
6486
|
+
"snapshot_restore",
|
|
6487
|
+
"upgrade",
|
|
6488
|
+
"inspect",
|
|
6489
|
+
"doctor"
|
|
6490
|
+
]
|
|
6491
|
+
},
|
|
6474
6492
|
auth: {
|
|
6475
6493
|
read: ["list_users", "get_user", "list_providers", "get_provider", "supported_providers", "get_settings", "get_config", "get_oauth_server"],
|
|
6476
6494
|
write: ["generate_link", "configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config", "migrate_oauth_server"]
|
|
@@ -6496,8 +6514,8 @@ var ACTION_POLICY = {
|
|
|
6496
6514
|
},
|
|
6497
6515
|
secrets: { read: ["list"], write: ["upsert", "delete"] },
|
|
6498
6516
|
frontend: {
|
|
6499
|
-
read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
|
|
6500
|
-
write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env"]
|
|
6517
|
+
read: ["list", "get", "build_logs", "list_frameworks", "list_records", "list_releases", "get_release"],
|
|
6518
|
+
write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env", "upload_release", "activate_release"]
|
|
6501
6519
|
},
|
|
6502
6520
|
task_events: { read: ["inspect_webhook"], write: ["register_webhook", "unregister_webhook"] },
|
|
6503
6521
|
diagnostics: { read: ["list_checks", "get_run"], write: ["run_checks", "repair"] },
|
|
@@ -6857,6 +6875,7 @@ class HttpTransport {
|
|
|
6857
6875
|
async post(path, body, options) {
|
|
6858
6876
|
const timeoutMs = validatedPostTimeout(options);
|
|
6859
6877
|
const maxJsonBytes = validatedJsonResponseLimit(options?.maxJsonBytes);
|
|
6878
|
+
const responseTimeoutMs = validatedResponseTimeout(options?.responseTimeoutMs);
|
|
6860
6879
|
try {
|
|
6861
6880
|
if (maxJsonBytes === undefined) {
|
|
6862
6881
|
return await this.mutationWithResponseReader("POST", path, serializedRequestBody(body), responseJsonOrNull, timeoutMs);
|
|
@@ -6866,7 +6885,7 @@ class HttpTransport {
|
|
|
6866
6885
|
headers: this.headers(),
|
|
6867
6886
|
body: serializedRequestBody(body)
|
|
6868
6887
|
}, timeoutMs);
|
|
6869
|
-
const data = await boundedResponseJson(response, maxJsonBytes);
|
|
6888
|
+
const data = await boundedResponseJson(response, maxJsonBytes, responseTimeoutMs);
|
|
6870
6889
|
return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
|
|
6871
6890
|
} catch (error) {
|
|
6872
6891
|
return transportFailure(error);
|
|
@@ -6876,6 +6895,39 @@ class HttpTransport {
|
|
|
6876
6895
|
const timeoutMs = validatedPostTimeout(options);
|
|
6877
6896
|
return this.mutationWithResponseReader("POST", path, serializedRequestBody(body), releaseMutationResponseJson, timeoutMs);
|
|
6878
6897
|
}
|
|
6898
|
+
async postBinary(path, body, options) {
|
|
6899
|
+
if (options.contentType !== "application/zip") {
|
|
6900
|
+
throw new Error("Binary HTTP content type is invalid");
|
|
6901
|
+
}
|
|
6902
|
+
if (!Number.isSafeInteger(options.contentLength) || options.contentLength < 1 || options.contentLength !== body.byteLength) {
|
|
6903
|
+
throw new RangeError("Binary HTTP body length is invalid");
|
|
6904
|
+
}
|
|
6905
|
+
if (!/^[0-9a-f]{64}$/u.test(options.contentSha256)) {
|
|
6906
|
+
throw new Error("Binary HTTP body SHA-256 is invalid");
|
|
6907
|
+
}
|
|
6908
|
+
const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
|
|
6909
|
+
const timeoutMs = validatedPostTimeout(options);
|
|
6910
|
+
const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
|
|
6911
|
+
try {
|
|
6912
|
+
const request = {
|
|
6913
|
+
method: "POST",
|
|
6914
|
+
headers: {
|
|
6915
|
+
Authorization: `Bearer ${this.token}`,
|
|
6916
|
+
"Content-Type": options.contentType,
|
|
6917
|
+
"Content-Length": String(options.contentLength),
|
|
6918
|
+
"x-supacloud-content-sha256": options.contentSha256,
|
|
6919
|
+
...this.apiKey ? { apikey: this.apiKey } : {}
|
|
6920
|
+
},
|
|
6921
|
+
body: body.stream,
|
|
6922
|
+
duplex: "half"
|
|
6923
|
+
};
|
|
6924
|
+
const response = await fetchWithRetry(`${this.baseUrl}${path}`, request, timeoutMs);
|
|
6925
|
+
const data = await boundedResponseJson(response, maxJsonBytes, responseTimeoutMs);
|
|
6926
|
+
return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
|
|
6927
|
+
} catch (error) {
|
|
6928
|
+
return transportFailure(error);
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6879
6931
|
async patchReleaseMutation(path, body) {
|
|
6880
6932
|
return this.mutationWithResponseReader("PATCH", path, serializedRequestBody(body), releaseMutationResponseJson);
|
|
6881
6933
|
}
|
|
@@ -11016,9 +11068,418 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
11016
11068
|
// src/shared/tools/frontend-tools.ts
|
|
11017
11069
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
11018
11070
|
import { basename as basename3 } from "node:path";
|
|
11071
|
+
|
|
11072
|
+
// src/shared/tools/frontend-release-control.ts
|
|
11073
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
11074
|
+
import { constants as fsConstants2 } from "node:fs";
|
|
11075
|
+
import { open } from "node:fs/promises";
|
|
11076
|
+
import { resolve as resolve3 } from "node:path";
|
|
11077
|
+
var PROJECT_REF_PATTERN3 = /^[A-Za-z0-9_-]{1,20}$/u;
|
|
11078
|
+
var DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
11079
|
+
var RELEASE_ID_PATTERN = /^[0-9a-f]{64}$/u;
|
|
11080
|
+
var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
11081
|
+
var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
11082
|
+
var ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
|
|
11083
|
+
var ARCHIVE_CHUNK_BYTES = 64 * 1024;
|
|
11084
|
+
var RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
11085
|
+
var UPLOAD_REQUEST_TIMEOUT_MS = 10 * 60000;
|
|
11086
|
+
var MUTATION_RESPONSE_TIMEOUT_MS = 5000;
|
|
11087
|
+
var RELEASE_LIST_LIMIT_MAX = 100;
|
|
11088
|
+
function toolResponse(payload) {
|
|
11089
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
11090
|
+
}
|
|
11091
|
+
function releaseFailure(operation, code, status) {
|
|
11092
|
+
return {
|
|
11093
|
+
isError: true,
|
|
11094
|
+
content: [{
|
|
11095
|
+
type: "text",
|
|
11096
|
+
text: JSON.stringify({ ok: false, operation, error: { code, http_status: status } })
|
|
11097
|
+
}]
|
|
11098
|
+
};
|
|
11099
|
+
}
|
|
11100
|
+
function exactKeys(candidate, keys) {
|
|
11101
|
+
const actual = Object.keys(candidate).sort();
|
|
11102
|
+
const expected = [...keys].sort();
|
|
11103
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
11104
|
+
}
|
|
11105
|
+
function releaseRecord(candidate) {
|
|
11106
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11107
|
+
return null;
|
|
11108
|
+
const record = candidate;
|
|
11109
|
+
const keys = [
|
|
11110
|
+
"schema",
|
|
11111
|
+
"project_ref",
|
|
11112
|
+
"deployment_id",
|
|
11113
|
+
"release_id",
|
|
11114
|
+
"sha256",
|
|
11115
|
+
"tree_sha256",
|
|
11116
|
+
"size_bytes",
|
|
11117
|
+
"file_count",
|
|
11118
|
+
"created_at",
|
|
11119
|
+
"kind"
|
|
11120
|
+
];
|
|
11121
|
+
if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || typeof record.release_id !== "string" || !RELEASE_ID_PATTERN.test(record.release_id) || record.sha256 !== record.release_id || typeof record.tree_sha256 !== "string" || !RELEASE_ID_PATTERN.test(record.tree_sha256) || !Number.isSafeInteger(record.size_bytes) || Number(record.size_bytes) < 1 || !Number.isSafeInteger(record.file_count) || Number(record.file_count) < 1 || !canonicalTimestamp(record.created_at) || record.kind !== "prebuilt_static")
|
|
11122
|
+
return null;
|
|
11123
|
+
return {
|
|
11124
|
+
project_ref: record.project_ref,
|
|
11125
|
+
deployment_id: record.deployment_id,
|
|
11126
|
+
release_id: record.release_id,
|
|
11127
|
+
sha256: record.release_id,
|
|
11128
|
+
tree_sha256: record.tree_sha256,
|
|
11129
|
+
size_bytes: Number(record.size_bytes),
|
|
11130
|
+
file_count: Number(record.file_count),
|
|
11131
|
+
created_at: record.created_at,
|
|
11132
|
+
kind: "prebuilt_static"
|
|
11133
|
+
};
|
|
11134
|
+
}
|
|
11135
|
+
function releaseEnvelope(candidate, expected) {
|
|
11136
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11137
|
+
return null;
|
|
11138
|
+
const envelope = candidate;
|
|
11139
|
+
const release = exactKeys(envelope, ["project_ref", "deployment_id", "release"]) ? releaseRecord(envelope.release) : null;
|
|
11140
|
+
if (!release || envelope.project_ref !== expected.projectRef || envelope.deployment_id !== expected.deploymentId || release.project_ref !== expected.projectRef || release.deployment_id !== expected.deploymentId || release.release_id !== expected.releaseId)
|
|
11141
|
+
return null;
|
|
11142
|
+
return release;
|
|
11143
|
+
}
|
|
11144
|
+
function canonicalTimestamp(candidate) {
|
|
11145
|
+
if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
|
|
11146
|
+
return false;
|
|
11147
|
+
const milliseconds = Date.parse(candidate);
|
|
11148
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
11149
|
+
}
|
|
11150
|
+
function nullableIdentity(candidate, pattern) {
|
|
11151
|
+
if (candidate === null)
|
|
11152
|
+
return null;
|
|
11153
|
+
return typeof candidate === "string" && pattern.test(candidate) ? candidate : undefined;
|
|
11154
|
+
}
|
|
11155
|
+
function releaseInventory(candidate, expected) {
|
|
11156
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11157
|
+
return null;
|
|
11158
|
+
const record = candidate;
|
|
11159
|
+
const keys = [
|
|
11160
|
+
"project_ref",
|
|
11161
|
+
"deployment_id",
|
|
11162
|
+
"active_release_id",
|
|
11163
|
+
"active_activation_id",
|
|
11164
|
+
"releases",
|
|
11165
|
+
"next_cursor"
|
|
11166
|
+
];
|
|
11167
|
+
if (!exactKeys(record, keys) || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || !Array.isArray(record.releases))
|
|
11168
|
+
return null;
|
|
11169
|
+
const activeReleaseId = nullableIdentity(record.active_release_id, RELEASE_ID_PATTERN);
|
|
11170
|
+
const activeActivationId = nullableIdentity(record.active_activation_id, MUTATION_ID_PATTERN);
|
|
11171
|
+
const nextCursor = nullableIdentity(record.next_cursor, RELEASE_ID_PATTERN);
|
|
11172
|
+
const releases = record.releases.map(releaseRecord);
|
|
11173
|
+
if (activeReleaseId === undefined || activeActivationId === undefined || nextCursor === undefined || activeReleaseId === null !== (activeActivationId === null) || releases.some((release) => release === null))
|
|
11174
|
+
return null;
|
|
11175
|
+
const verified = releases;
|
|
11176
|
+
if (new Set(verified.map((release) => release.release_id)).size !== verified.length || verified.some((release) => release.project_ref !== record.project_ref || release.deployment_id !== record.deployment_id) || expected && (record.project_ref !== expected.projectRef || record.deployment_id !== expected.deploymentId))
|
|
11177
|
+
return null;
|
|
11178
|
+
return {
|
|
11179
|
+
project_ref: record.project_ref,
|
|
11180
|
+
deployment_id: record.deployment_id,
|
|
11181
|
+
active_release_id: activeReleaseId,
|
|
11182
|
+
active_activation_id: activeActivationId,
|
|
11183
|
+
releases: verified,
|
|
11184
|
+
next_cursor: nextCursor
|
|
11185
|
+
};
|
|
11186
|
+
}
|
|
11187
|
+
function releaseEndpoint(projectRef2, deploymentId) {
|
|
11188
|
+
if (!PROJECT_REF_PATTERN3.test(projectRef2))
|
|
11189
|
+
throw new Error("'ref' is invalid for frontend releases");
|
|
11190
|
+
if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
|
|
11191
|
+
throw new Error("'id' is invalid for frontend releases");
|
|
11192
|
+
return `/v1/projects/${encodeURIComponent(projectRef2)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
|
|
11193
|
+
}
|
|
11194
|
+
function releasePath(projectRef2, deploymentId, releaseId) {
|
|
11195
|
+
if (!RELEASE_ID_PATTERN.test(releaseId))
|
|
11196
|
+
throw new Error("'release_id' must be a SHA-256 digest");
|
|
11197
|
+
return `${releaseEndpoint(projectRef2, deploymentId)}/${releaseId}`;
|
|
11198
|
+
}
|
|
11199
|
+
function sameArchiveIdentity(left, right) {
|
|
11200
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
11201
|
+
}
|
|
11202
|
+
async function archiveSha256(handle, sizeBytes) {
|
|
11203
|
+
const hash2 = createHash3("sha256");
|
|
11204
|
+
const chunk = new Uint8Array(Math.min(sizeBytes, ARCHIVE_CHUNK_BYTES));
|
|
11205
|
+
for (let offset = 0;offset < sizeBytes; ) {
|
|
11206
|
+
const requested = Math.min(chunk.byteLength, sizeBytes - offset);
|
|
11207
|
+
const { bytesRead } = await handle.read(chunk, 0, requested, offset);
|
|
11208
|
+
if (bytesRead < 1)
|
|
11209
|
+
throw new Error("Frontend release archive changed while it was hashed");
|
|
11210
|
+
hash2.update(chunk.subarray(0, bytesRead));
|
|
11211
|
+
offset += bytesRead;
|
|
11212
|
+
}
|
|
11213
|
+
return hash2.digest("hex");
|
|
11214
|
+
}
|
|
11215
|
+
async function verifiedArchive(path) {
|
|
11216
|
+
const archivePath = resolve3(path);
|
|
11217
|
+
const handle = await open(archivePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
11218
|
+
try {
|
|
11219
|
+
const before = await handle.stat({ bigint: true });
|
|
11220
|
+
const sizeBytes = Number(before.size);
|
|
11221
|
+
if (!before.isFile() || sizeBytes < 1 || sizeBytes > ARCHIVE_MAX_BYTES) {
|
|
11222
|
+
throw new Error(`Frontend release archive must be a 1-${ARCHIVE_MAX_BYTES} byte regular file`);
|
|
11223
|
+
}
|
|
11224
|
+
const sha256 = await archiveSha256(handle, sizeBytes);
|
|
11225
|
+
const after = await handle.stat({ bigint: true });
|
|
11226
|
+
if (!sameArchiveIdentity(before, after)) {
|
|
11227
|
+
throw new Error("Frontend release archive identity changed while it was hashed");
|
|
11228
|
+
}
|
|
11229
|
+
return { handle, sizeBytes, sha256 };
|
|
11230
|
+
} catch (error) {
|
|
11231
|
+
await handle.close();
|
|
11232
|
+
throw error;
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
function archiveStream(archive) {
|
|
11236
|
+
let offset = 0;
|
|
11237
|
+
return new ReadableStream({
|
|
11238
|
+
async pull(controller) {
|
|
11239
|
+
if (offset === archive.sizeBytes) {
|
|
11240
|
+
controller.close();
|
|
11241
|
+
return;
|
|
11242
|
+
}
|
|
11243
|
+
const length = Math.min(ARCHIVE_CHUNK_BYTES, archive.sizeBytes - offset);
|
|
11244
|
+
const chunk = new Uint8Array(length);
|
|
11245
|
+
const { bytesRead } = await archive.handle.read(chunk, 0, length, offset);
|
|
11246
|
+
if (bytesRead < 1)
|
|
11247
|
+
throw new Error("Frontend release archive changed while it was uploaded");
|
|
11248
|
+
offset += bytesRead;
|
|
11249
|
+
controller.enqueue(bytesRead === length ? chunk : chunk.subarray(0, bytesRead));
|
|
11250
|
+
}
|
|
11251
|
+
});
|
|
11252
|
+
}
|
|
11253
|
+
function releaseReadFailure(operation, response) {
|
|
11254
|
+
return releaseFailure(operation, response.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", response.status);
|
|
11255
|
+
}
|
|
11256
|
+
async function listFrontendReleases(http, projectRef2, deploymentId, cursor, limit = 50) {
|
|
11257
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > RELEASE_LIST_LIMIT_MAX) {
|
|
11258
|
+
throw new Error("'limit' must be 1-100");
|
|
11259
|
+
}
|
|
11260
|
+
if (cursor !== undefined && !RELEASE_ID_PATTERN.test(cursor))
|
|
11261
|
+
throw new Error("'cursor' is invalid");
|
|
11262
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
11263
|
+
if (cursor)
|
|
11264
|
+
query.set("cursor", cursor);
|
|
11265
|
+
const response = await http.get(`${releaseEndpoint(projectRef2, deploymentId)}?${query}`, {
|
|
11266
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11267
|
+
});
|
|
11268
|
+
const inventory = response.ok ? releaseInventory(response.data, { projectRef: projectRef2, deploymentId }) : null;
|
|
11269
|
+
if (!inventory || inventory.releases.length > limit) {
|
|
11270
|
+
return releaseReadFailure("frontend.list_releases", response);
|
|
11271
|
+
}
|
|
11272
|
+
return toolResponse(inventory);
|
|
11273
|
+
}
|
|
11274
|
+
async function getFrontendRelease(http, projectRef2, deploymentId, releaseId) {
|
|
11275
|
+
const response = await http.get(releasePath(projectRef2, deploymentId, releaseId), {
|
|
11276
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11277
|
+
});
|
|
11278
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId }) : null;
|
|
11279
|
+
if (!release) {
|
|
11280
|
+
return releaseReadFailure("frontend.get_release", response);
|
|
11281
|
+
}
|
|
11282
|
+
return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release });
|
|
11283
|
+
}
|
|
11284
|
+
async function uploadFrontendRelease(http, projectRef2, deploymentId, archivePath) {
|
|
11285
|
+
const endpoint = releaseEndpoint(projectRef2, deploymentId);
|
|
11286
|
+
const archive = await verifiedArchive(archivePath);
|
|
11287
|
+
let response;
|
|
11288
|
+
try {
|
|
11289
|
+
response = await http.postBinary(endpoint, {
|
|
11290
|
+
stream: archiveStream(archive),
|
|
11291
|
+
byteLength: archive.sizeBytes
|
|
11292
|
+
}, {
|
|
11293
|
+
contentType: "application/zip",
|
|
11294
|
+
contentLength: archive.sizeBytes,
|
|
11295
|
+
contentSha256: archive.sha256,
|
|
11296
|
+
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
11297
|
+
timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
|
|
11298
|
+
responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
|
|
11299
|
+
});
|
|
11300
|
+
} finally {
|
|
11301
|
+
await archive.handle.close();
|
|
11302
|
+
}
|
|
11303
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId: archive.sha256 }) : null;
|
|
11304
|
+
if (!release) {
|
|
11305
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
11306
|
+
return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
|
|
11307
|
+
}
|
|
11308
|
+
return uploadReadback(http, projectRef2, deploymentId, archive.sha256, response.status);
|
|
11309
|
+
}
|
|
11310
|
+
const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
11311
|
+
const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef: projectRef2, deploymentId, releaseId: release.release_id }) : null;
|
|
11312
|
+
if (!verified || verified.tree_sha256 !== release.tree_sha256) {
|
|
11313
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
|
|
11314
|
+
}
|
|
11315
|
+
return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release: verified });
|
|
11316
|
+
}
|
|
11317
|
+
async function uploadReadback(http, projectRef2, deploymentId, releaseId, uploadStatus) {
|
|
11318
|
+
const response = await http.get(releasePath(projectRef2, deploymentId, releaseId), {
|
|
11319
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11320
|
+
});
|
|
11321
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId }) : null;
|
|
11322
|
+
if (!release) {
|
|
11323
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
|
|
11324
|
+
}
|
|
11325
|
+
return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release });
|
|
11326
|
+
}
|
|
11327
|
+
function stableJson(candidate) {
|
|
11328
|
+
if (candidate === null || typeof candidate !== "object")
|
|
11329
|
+
return JSON.stringify(candidate);
|
|
11330
|
+
if (Array.isArray(candidate))
|
|
11331
|
+
return `[${candidate.map(stableJson).join(",")}]`;
|
|
11332
|
+
const record = candidate;
|
|
11333
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
11334
|
+
}
|
|
11335
|
+
function activationFingerprint(identity) {
|
|
11336
|
+
return createHash3("sha256").update(stableJson({
|
|
11337
|
+
project_ref: identity.projectRef,
|
|
11338
|
+
deployment_id: identity.deploymentId,
|
|
11339
|
+
release_id: identity.releaseId,
|
|
11340
|
+
expected_active_release_id: identity.expectedActiveReleaseId,
|
|
11341
|
+
activation_id: identity.mutationId,
|
|
11342
|
+
expected_activation_id: identity.expectedActivationId
|
|
11343
|
+
})).digest("hex");
|
|
11344
|
+
}
|
|
11345
|
+
function activationResourceKey(deploymentId) {
|
|
11346
|
+
return `v1/frontend_release/${Buffer.from(deploymentId, "utf8").toString("base64url")}`;
|
|
11347
|
+
}
|
|
11348
|
+
function publicMutationReceipt(candidate, expected) {
|
|
11349
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11350
|
+
return null;
|
|
11351
|
+
const envelope = candidate;
|
|
11352
|
+
const mutation = envelope.mutation;
|
|
11353
|
+
const expectedKeys = [
|
|
11354
|
+
"project_ref",
|
|
11355
|
+
"mutation_id",
|
|
11356
|
+
"operation",
|
|
11357
|
+
"resource_key",
|
|
11358
|
+
"request_fingerprint",
|
|
11359
|
+
"principal",
|
|
11360
|
+
"status",
|
|
11361
|
+
"checkpoint",
|
|
11362
|
+
"receipt",
|
|
11363
|
+
"response_status",
|
|
11364
|
+
"failure_code",
|
|
11365
|
+
"lease",
|
|
11366
|
+
"completed_at",
|
|
11367
|
+
"created_at",
|
|
11368
|
+
"updated_at"
|
|
11369
|
+
];
|
|
11370
|
+
if (!mutation || !exactKeys(envelope, ["project_ref", "mutation"]) || !exactKeys(mutation, expectedKeys) || envelope.project_ref !== expected.projectRef || mutation.project_ref !== expected.projectRef || mutation.mutation_id !== expected.mutationId || mutation.resource_key !== expected.resourceKey || mutation.request_fingerprint !== expected.requestFingerprint || typeof mutation.operation !== "string" || typeof mutation.status !== "string" || !(mutation.response_status === null || Number.isSafeInteger(mutation.response_status)) || !(mutation.failure_code === null || typeof mutation.failure_code === "string"))
|
|
11371
|
+
return null;
|
|
11372
|
+
return {
|
|
11373
|
+
operation: mutation.operation,
|
|
11374
|
+
status: mutation.status,
|
|
11375
|
+
responseStatus: mutation.response_status,
|
|
11376
|
+
failureCode: mutation.failure_code
|
|
11377
|
+
};
|
|
11378
|
+
}
|
|
11379
|
+
function activationReceipt(candidate, input) {
|
|
11380
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11381
|
+
return null;
|
|
11382
|
+
const record = candidate;
|
|
11383
|
+
const release = releaseRecord(record.release);
|
|
11384
|
+
const mutation = record.mutation;
|
|
11385
|
+
if (!release || !mutation || !exactKeys(record, [
|
|
11386
|
+
"project_ref",
|
|
11387
|
+
"deployment_id",
|
|
11388
|
+
"active_release_id",
|
|
11389
|
+
"activation_id",
|
|
11390
|
+
"release",
|
|
11391
|
+
"mutation"
|
|
11392
|
+
]) || !exactKeys(mutation, ["mutation_id", "status", "replayed"]) || record.project_ref !== input.projectRef || record.deployment_id !== input.deploymentId || record.active_release_id !== input.releaseId || record.activation_id !== input.mutationId || release.project_ref !== input.projectRef || release.deployment_id !== input.deploymentId || release.release_id !== input.releaseId || mutation.mutation_id !== input.mutationId || mutation.status !== "succeeded" || typeof mutation.replayed !== "boolean")
|
|
11393
|
+
return null;
|
|
11394
|
+
return release;
|
|
11395
|
+
}
|
|
11396
|
+
async function activeReleaseReadback(http, identity) {
|
|
11397
|
+
const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
|
|
11398
|
+
const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
|
|
11399
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11400
|
+
});
|
|
11401
|
+
const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
|
|
11402
|
+
projectRef: identity.projectRef,
|
|
11403
|
+
deploymentId: identity.deploymentId
|
|
11404
|
+
}) : null;
|
|
11405
|
+
if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
|
|
11406
|
+
return { release: null, status: inventoryRead.status };
|
|
11407
|
+
}
|
|
11408
|
+
const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
11409
|
+
const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
|
|
11410
|
+
projectRef: identity.projectRef,
|
|
11411
|
+
deploymentId: identity.deploymentId,
|
|
11412
|
+
releaseId: identity.releaseId
|
|
11413
|
+
}) : null;
|
|
11414
|
+
return { release, status: releaseRead.status };
|
|
11415
|
+
}
|
|
11416
|
+
async function activateFrontendRelease(http, input) {
|
|
11417
|
+
const mutationId = input.mutationId;
|
|
11418
|
+
if (!MUTATION_ID_PATTERN.test(mutationId))
|
|
11419
|
+
throw new Error("'mutation_id' must be a UUIDv4");
|
|
11420
|
+
if (input.expectedActiveReleaseId !== "absent" && !RELEASE_ID_PATTERN.test(input.expectedActiveReleaseId)) {
|
|
11421
|
+
throw new Error("'expected_active_release_id' is invalid");
|
|
11422
|
+
}
|
|
11423
|
+
if (input.expectedActivationId !== "absent" && !MUTATION_ID_PATTERN.test(input.expectedActivationId)) {
|
|
11424
|
+
throw new Error("'expected_activation_id' is invalid");
|
|
11425
|
+
}
|
|
11426
|
+
const endpoint = `${releasePath(input.projectRef, input.deploymentId, input.releaseId)}/activate`;
|
|
11427
|
+
const response = await http.post(endpoint, {
|
|
11428
|
+
expected_active_release_id: input.expectedActiveReleaseId,
|
|
11429
|
+
expected_activation_id: input.expectedActivationId,
|
|
11430
|
+
mutation_id: mutationId
|
|
11431
|
+
}, {
|
|
11432
|
+
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
11433
|
+
responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
|
|
11434
|
+
});
|
|
11435
|
+
const release = activationReceipt(response.data, { ...input, mutationId });
|
|
11436
|
+
if (!response.ok || !release) {
|
|
11437
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
11438
|
+
return releaseFailure("frontend.activate_release", "HTTP_ERROR", response.status);
|
|
11439
|
+
}
|
|
11440
|
+
return activationReadback(http, input, mutationId, response.status);
|
|
11441
|
+
}
|
|
11442
|
+
const readback = await activeReleaseReadback(http, { ...input, mutationId });
|
|
11443
|
+
if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
|
|
11444
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
|
|
11445
|
+
}
|
|
11446
|
+
return toolResponse({
|
|
11447
|
+
project_ref: input.projectRef,
|
|
11448
|
+
deployment_id: input.deploymentId,
|
|
11449
|
+
active_release_id: input.releaseId,
|
|
11450
|
+
activation_id: mutationId,
|
|
11451
|
+
release: readback.release
|
|
11452
|
+
});
|
|
11453
|
+
}
|
|
11454
|
+
async function activationReadback(http, input, mutationId, activationStatus) {
|
|
11455
|
+
const mutationRead = await http.get(`/v1/projects/${encodeURIComponent(input.projectRef)}/mutations/${mutationId}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
11456
|
+
const mutationEnvelope = mutationRead.data;
|
|
11457
|
+
const mutation = mutationRead.ok ? publicMutationReceipt(mutationRead.data, {
|
|
11458
|
+
projectRef: input.projectRef,
|
|
11459
|
+
mutationId,
|
|
11460
|
+
resourceKey: activationResourceKey(input.deploymentId),
|
|
11461
|
+
requestFingerprint: activationFingerprint({ ...input, mutationId })
|
|
11462
|
+
}) : null;
|
|
11463
|
+
if (!mutation || mutationEnvelope?.project_ref !== input.projectRef || mutation.operation !== "frontend.release.activate" || mutation.status !== "succeeded" || mutation.responseStatus !== 200 || mutation.failureCode !== null) {
|
|
11464
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
|
|
11465
|
+
}
|
|
11466
|
+
const readback = await activeReleaseReadback(http, { ...input, mutationId });
|
|
11467
|
+
if (!readback.release) {
|
|
11468
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
|
|
11469
|
+
}
|
|
11470
|
+
return toolResponse({
|
|
11471
|
+
project_ref: input.projectRef,
|
|
11472
|
+
deployment_id: input.deploymentId,
|
|
11473
|
+
active_release_id: input.releaseId,
|
|
11474
|
+
activation_id: mutationId,
|
|
11475
|
+
release: readback.release
|
|
11476
|
+
});
|
|
11477
|
+
}
|
|
11478
|
+
|
|
11479
|
+
// src/shared/tools/frontend-tools.ts
|
|
11019
11480
|
function registerFrontendTools(server, http) {
|
|
11020
|
-
server.tool("frontend", `Frontend hosting
|
|
11021
|
-
Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records`, {
|
|
11481
|
+
server.tool("frontend", `Frontend hosting and immutable prebuilt releases. Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
|
|
11482
|
+
Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records, list_releases, get_release, upload_release, activate_release`, {
|
|
11022
11483
|
action: withDescription(stringEnum([
|
|
11023
11484
|
"list",
|
|
11024
11485
|
"get",
|
|
@@ -11033,7 +11494,11 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11033
11494
|
"remove_domain",
|
|
11034
11495
|
"set_env",
|
|
11035
11496
|
"list_frameworks",
|
|
11036
|
-
"list_records"
|
|
11497
|
+
"list_records",
|
|
11498
|
+
"list_releases",
|
|
11499
|
+
"get_release",
|
|
11500
|
+
"upload_release",
|
|
11501
|
+
"activate_release"
|
|
11037
11502
|
]), "Action"),
|
|
11038
11503
|
ref: optional(Type.String(), "Project ref"),
|
|
11039
11504
|
id: optional(Type.String(), "Deployment ID"),
|
|
@@ -11048,9 +11513,37 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11048
11513
|
env_vars: optional(Type.Record(Type.String(), Type.String()), "[create/update/set_env] Environment variables"),
|
|
11049
11514
|
git_url: optional(Type.String(), "[deploy_git] Git repository URL"),
|
|
11050
11515
|
branch: optional(Type.String(), "[deploy_git] Branch (default: main)"),
|
|
11051
|
-
zip_path: optional(Type.String(), "[deploy_upload] Local
|
|
11516
|
+
zip_path: optional(Type.String(), "[deploy_upload/upload_release] Local ZIP file path"),
|
|
11517
|
+
release_id: optional(Type.String(), "[get_release/activate_release] SHA-256 release ID"),
|
|
11518
|
+
expected_active_release_id: optional(Type.String(), "[activate_release] Current release SHA-256 or absent"),
|
|
11519
|
+
expected_activation_id: optional(Type.String(), "[activate_release] Current activation UUIDv4 or absent"),
|
|
11520
|
+
mutation_id: optional(Type.String(), "[activate_release] Required retry-stable UUIDv4"),
|
|
11521
|
+
cursor: optional(Type.String(), "[list_releases] Last release SHA-256 cursor"),
|
|
11522
|
+
limit: optional(Type.Number(), "[list_releases] Page size, 1-100 (default 50)")
|
|
11052
11523
|
}, async (args) => {
|
|
11053
|
-
const {
|
|
11524
|
+
const {
|
|
11525
|
+
action,
|
|
11526
|
+
ref,
|
|
11527
|
+
id,
|
|
11528
|
+
name,
|
|
11529
|
+
framework,
|
|
11530
|
+
domain,
|
|
11531
|
+
build_command,
|
|
11532
|
+
output_dir,
|
|
11533
|
+
install_command,
|
|
11534
|
+
node_version,
|
|
11535
|
+
health_check_path,
|
|
11536
|
+
env_vars,
|
|
11537
|
+
git_url,
|
|
11538
|
+
branch,
|
|
11539
|
+
zip_path,
|
|
11540
|
+
release_id,
|
|
11541
|
+
expected_active_release_id,
|
|
11542
|
+
expected_activation_id,
|
|
11543
|
+
mutation_id,
|
|
11544
|
+
cursor,
|
|
11545
|
+
limit
|
|
11546
|
+
} = args;
|
|
11054
11547
|
const need = (f, v) => {
|
|
11055
11548
|
if (!v)
|
|
11056
11549
|
throw new Error(`'${f}' required for '${action}'`);
|
|
@@ -11157,6 +11650,35 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11157
11650
|
need("id", id);
|
|
11158
11651
|
text = ok(await http.get(`/v1/projects/${ref}/frontend/deployments/${id}/records`));
|
|
11159
11652
|
break;
|
|
11653
|
+
case "list_releases":
|
|
11654
|
+
need("ref", ref);
|
|
11655
|
+
need("id", id);
|
|
11656
|
+
return listFrontendReleases(http, ref, id, cursor, limit);
|
|
11657
|
+
case "get_release":
|
|
11658
|
+
need("ref", ref);
|
|
11659
|
+
need("id", id);
|
|
11660
|
+
need("release_id", release_id);
|
|
11661
|
+
return getFrontendRelease(http, ref, id, release_id);
|
|
11662
|
+
case "upload_release":
|
|
11663
|
+
need("ref", ref);
|
|
11664
|
+
need("id", id);
|
|
11665
|
+
need("zip_path", zip_path);
|
|
11666
|
+
return uploadFrontendRelease(http, ref, id, zip_path);
|
|
11667
|
+
case "activate_release":
|
|
11668
|
+
need("ref", ref);
|
|
11669
|
+
need("id", id);
|
|
11670
|
+
need("release_id", release_id);
|
|
11671
|
+
need("expected_active_release_id", expected_active_release_id);
|
|
11672
|
+
need("expected_activation_id", expected_activation_id);
|
|
11673
|
+
need("mutation_id", mutation_id);
|
|
11674
|
+
return activateFrontendRelease(http, {
|
|
11675
|
+
projectRef: ref,
|
|
11676
|
+
deploymentId: id,
|
|
11677
|
+
releaseId: release_id,
|
|
11678
|
+
expectedActiveReleaseId: expected_active_release_id,
|
|
11679
|
+
expectedActivationId: expected_activation_id,
|
|
11680
|
+
mutationId: mutation_id
|
|
11681
|
+
});
|
|
11160
11682
|
default:
|
|
11161
11683
|
text = `❌ Unknown action`;
|
|
11162
11684
|
}
|
|
@@ -11166,7 +11688,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11166
11688
|
|
|
11167
11689
|
// src/shared/tools/project-read-projection.ts
|
|
11168
11690
|
var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
|
|
11169
|
-
var
|
|
11691
|
+
var PROJECT_REF_PATTERN4 = /^[a-z0-9-]{1,20}$/;
|
|
11170
11692
|
var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
11171
11693
|
var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
11172
11694
|
var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
@@ -11230,7 +11752,7 @@ function matchingText(candidate, maxLength, pattern) {
|
|
|
11230
11752
|
const candidateText = boundedText2(candidate, maxLength);
|
|
11231
11753
|
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
11232
11754
|
}
|
|
11233
|
-
function
|
|
11755
|
+
function canonicalTimestamp2(candidate) {
|
|
11234
11756
|
const timestamp = boundedText2(candidate, 64);
|
|
11235
11757
|
if (!timestamp)
|
|
11236
11758
|
return null;
|
|
@@ -11240,12 +11762,12 @@ function canonicalTimestamp(candidate) {
|
|
|
11240
11762
|
function projectedSummary(project) {
|
|
11241
11763
|
const summary = {
|
|
11242
11764
|
id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
11243
|
-
ref: matchingText(project.ref, 20,
|
|
11765
|
+
ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN4),
|
|
11244
11766
|
organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
11245
11767
|
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
11246
11768
|
name: boundedText2(project.name, 100),
|
|
11247
11769
|
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
11248
|
-
created_at:
|
|
11770
|
+
created_at: canonicalTimestamp2(project.created_at),
|
|
11249
11771
|
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
11250
11772
|
};
|
|
11251
11773
|
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
@@ -11364,7 +11886,7 @@ function projectGetRead(response, expectedRef) {
|
|
|
11364
11886
|
// src/shared/tools/project-endpoint-read.ts
|
|
11365
11887
|
var PROJECT_ENDPOINT_RESPONSE_MAX_BYTES = 256 * 1024;
|
|
11366
11888
|
var PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
11367
|
-
var
|
|
11889
|
+
var PROJECT_REF_PATTERN5 = /^[a-z0-9-]{1,20}$/;
|
|
11368
11890
|
var PROJECT_ENDPOINTS_SCHEMA = "supacloud.project-endpoints.v1";
|
|
11369
11891
|
var PROJECT_ENDPOINT_SOURCES = new Set([
|
|
11370
11892
|
"explicit_api_domain",
|
|
@@ -11434,7 +11956,7 @@ function projectEndpoint2(candidate) {
|
|
|
11434
11956
|
}
|
|
11435
11957
|
function projectEndpointProjection(candidate) {
|
|
11436
11958
|
const projection = plainRecord2(candidate);
|
|
11437
|
-
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !
|
|
11959
|
+
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN5.test(projection.project_ref))
|
|
11438
11960
|
return null;
|
|
11439
11961
|
const endpoints = plainRecord2(projection.endpoints);
|
|
11440
11962
|
if (!endpoints || !hasOnlyKeys2(endpoints, ENDPOINTS_KEYS))
|
|
@@ -12513,7 +13035,7 @@ function registerBranchTools(server, http, options = {}) {
|
|
|
12513
13035
|
// src/shared/tools/supabase-cli-tools.ts
|
|
12514
13036
|
import { spawn } from "node:child_process";
|
|
12515
13037
|
import { chmodSync, existsSync as existsSync5, mkdirSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12516
|
-
import { dirname, isAbsolute, join as join3, resolve as
|
|
13038
|
+
import { dirname, isAbsolute, join as join3, resolve as resolve4 } from "node:path";
|
|
12517
13039
|
var SENSITIVE_ENV_KEY = /(?:^|_)(?:PASSWORD|PASS|SECRET|TOKEN|KEY|CREDENTIALS?|AUTHORIZATION|AUTH|SESSION|COOKIE|BEARER|DB_URI|DB_URL|DSN|DATABASE_URL|DATABASE_URI|CONNECTION_STRING|CONNECTION_URI)(?:_|$)/i;
|
|
12518
13040
|
var VALID_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
12519
13041
|
var VALID_MIGRATION_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,100}$/;
|
|
@@ -12557,12 +13079,12 @@ function schemaArguments(schema) {
|
|
|
12557
13079
|
function workdirArguments(workdir) {
|
|
12558
13080
|
if (!workdir)
|
|
12559
13081
|
throw new Error("A workdir is required");
|
|
12560
|
-
return ["--workdir",
|
|
13082
|
+
return ["--workdir", resolve4(workdir)];
|
|
12561
13083
|
}
|
|
12562
13084
|
function resolveOutputPath(workdir, file, label) {
|
|
12563
13085
|
if (!file || /[\r\n\0]/.test(file))
|
|
12564
13086
|
throw new Error(`${label} file is required`);
|
|
12565
|
-
return isAbsolute(file) ?
|
|
13087
|
+
return isAbsolute(file) ? resolve4(file) : resolve4(workdir, file);
|
|
12566
13088
|
}
|
|
12567
13089
|
function databaseTargetArguments(databaseUrl) {
|
|
12568
13090
|
return databaseUrl ? ["--db-url", requirePostgresUrl(databaseUrl)] : ["--local"];
|
|
@@ -12647,7 +13169,7 @@ function actionArguments(request, workdir) {
|
|
|
12647
13169
|
function buildOfficialSupabaseArgs(request) {
|
|
12648
13170
|
if (request.action === "version")
|
|
12649
13171
|
return ["--version"];
|
|
12650
|
-
const workdir = request.workdir ?
|
|
13172
|
+
const workdir = request.workdir ? resolve4(request.workdir) : undefined;
|
|
12651
13173
|
if (!workdir)
|
|
12652
13174
|
throw new Error("A workdir is required");
|
|
12653
13175
|
return [...actionArguments(request, workdir), ...workdirArguments(workdir)];
|
|
@@ -12693,13 +13215,13 @@ function resolveOfficialSupabaseCommand(workdir, environment = process.env) {
|
|
|
12693
13215
|
}
|
|
12694
13216
|
return ["npx", "--yes", `supabase@${version}`];
|
|
12695
13217
|
}
|
|
12696
|
-
const localPackageEntry = join3(
|
|
13218
|
+
const localPackageEntry = join3(resolve4(workdir), "node_modules", "supabase", "dist", "supabase.js");
|
|
12697
13219
|
if (existsSync5(localPackageEntry))
|
|
12698
13220
|
return [process.execPath, localPackageEntry];
|
|
12699
13221
|
return ["supabase"];
|
|
12700
13222
|
}
|
|
12701
13223
|
function resolveExistingWorkdir(workdirInput, fallback) {
|
|
12702
|
-
const workdir =
|
|
13224
|
+
const workdir = resolve4(workdirInput || fallback);
|
|
12703
13225
|
if (!existsSync5(workdir) || !statSync3(workdir).isDirectory()) {
|
|
12704
13226
|
throw new Error(`Supabase workdir not found: ${workdir}`);
|
|
12705
13227
|
}
|
|
@@ -12813,7 +13335,7 @@ async function executeMigrationPush(request, runtime) {
|
|
|
12813
13335
|
if (!projectRef2)
|
|
12814
13336
|
return missingProjectRefResult();
|
|
12815
13337
|
const workdir = resolveExistingWorkdir(request.workdir, runtime.fallbackWorkdir);
|
|
12816
|
-
const migrationDirectory =
|
|
13338
|
+
const migrationDirectory = resolve4(workdir, request.dir || "supabase/migrations");
|
|
12817
13339
|
const migrationResponse = await pushMigrations({
|
|
12818
13340
|
action: "push_migrations",
|
|
12819
13341
|
ref: projectRef2,
|
|
@@ -12894,10 +13416,236 @@ function registerSupabaseCliTools(server, options = {}) {
|
|
|
12894
13416
|
}, (request) => executeSupabaseAction(request, runtime));
|
|
12895
13417
|
}
|
|
12896
13418
|
|
|
13419
|
+
// src/shared/tools/lite-cli-tools.ts
|
|
13420
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
13421
|
+
import { existsSync as existsSync6, statSync as statSync4 } from "node:fs";
|
|
13422
|
+
import { join as join4, resolve as resolve5 } from "node:path";
|
|
13423
|
+
function requireWorkdir(workdir, fallback) {
|
|
13424
|
+
const resolved = resolve5(workdir || fallback);
|
|
13425
|
+
if (!existsSync6(resolved) || !statSync4(resolved).isDirectory()) {
|
|
13426
|
+
throw new Error(`Lite workdir not found: ${resolved}`);
|
|
13427
|
+
}
|
|
13428
|
+
return resolved;
|
|
13429
|
+
}
|
|
13430
|
+
function optionalFlag(args, flag, value) {
|
|
13431
|
+
if (value !== undefined)
|
|
13432
|
+
args.push(flag, String(value));
|
|
13433
|
+
}
|
|
13434
|
+
function booleanFlag(args, flag, value) {
|
|
13435
|
+
if (value === true)
|
|
13436
|
+
args.push(flag);
|
|
13437
|
+
}
|
|
13438
|
+
function buildLiteArgs(request) {
|
|
13439
|
+
const args = [];
|
|
13440
|
+
if (request.action === "version")
|
|
13441
|
+
return ["--version"];
|
|
13442
|
+
switch (request.action) {
|
|
13443
|
+
case "start":
|
|
13444
|
+
case "migrate":
|
|
13445
|
+
case "status":
|
|
13446
|
+
case "keys":
|
|
13447
|
+
case "upgrade":
|
|
13448
|
+
case "inspect":
|
|
13449
|
+
case "doctor":
|
|
13450
|
+
args.push(request.action);
|
|
13451
|
+
break;
|
|
13452
|
+
case "gen_types":
|
|
13453
|
+
args.push("gen", "types");
|
|
13454
|
+
break;
|
|
13455
|
+
case "db_reset":
|
|
13456
|
+
args.push("db", "reset");
|
|
13457
|
+
break;
|
|
13458
|
+
case "db_diff":
|
|
13459
|
+
args.push("db", "diff");
|
|
13460
|
+
break;
|
|
13461
|
+
case "db_pull":
|
|
13462
|
+
args.push("db", "pull");
|
|
13463
|
+
if (request.file)
|
|
13464
|
+
args.push(request.file);
|
|
13465
|
+
break;
|
|
13466
|
+
case "snapshot_create":
|
|
13467
|
+
args.push("snapshot", "create");
|
|
13468
|
+
break;
|
|
13469
|
+
case "snapshot_restore":
|
|
13470
|
+
if (!request.snapshot_file)
|
|
13471
|
+
throw new Error("snapshot_restore requires --snapshot_file");
|
|
13472
|
+
args.push("snapshot", "restore", request.snapshot_file);
|
|
13473
|
+
break;
|
|
13474
|
+
default:
|
|
13475
|
+
throw new Error(`Unsupported Lite CLI action: ${String(request.action)}`);
|
|
13476
|
+
}
|
|
13477
|
+
optionalFlag(args, "--project-dir", request.project_dir);
|
|
13478
|
+
optionalFlag(args, "--state-dir", request.state_dir);
|
|
13479
|
+
optionalFlag(args, "--data-dir", request.data_dir);
|
|
13480
|
+
optionalFlag(args, "--storage-dir", request.storage_dir);
|
|
13481
|
+
optionalFlag(args, "--storage-backend", request.storage_backend);
|
|
13482
|
+
optionalFlag(args, "--s3-prefix", request.s3_prefix);
|
|
13483
|
+
optionalFlag(args, "--engine", request.engine);
|
|
13484
|
+
optionalFlag(args, "--host", request.host);
|
|
13485
|
+
optionalFlag(args, "--port", request.port);
|
|
13486
|
+
optionalFlag(args, "--api-url", request.api_url);
|
|
13487
|
+
optionalFlag(args, "--site-url", request.site_url);
|
|
13488
|
+
optionalFlag(args, "--replication-profile", request.replication_profile);
|
|
13489
|
+
optionalFlag(args, "--replication-host", request.replication_host);
|
|
13490
|
+
optionalFlag(args, "--replication-port", request.replication_port);
|
|
13491
|
+
optionalFlag(args, "--replication-allow-cidrs", request.replication_allow_cidrs);
|
|
13492
|
+
optionalFlag(args, "--powersync-tables", request.powersync_tables);
|
|
13493
|
+
optionalFlag(args, "--replication-tls-cert", request.replication_tls_cert);
|
|
13494
|
+
optionalFlag(args, "--replication-tls-key", request.replication_tls_key);
|
|
13495
|
+
optionalFlag(args, "--output", request.output);
|
|
13496
|
+
optionalFlag(args, "--file", request.action === "db_diff" ? request.file : undefined);
|
|
13497
|
+
booleanFlag(args, "--service-role", request.service_role);
|
|
13498
|
+
booleanFlag(args, "--force", request.force);
|
|
13499
|
+
booleanFlag(args, "--memory", request.memory);
|
|
13500
|
+
booleanFlag(args, "--json", request.json);
|
|
13501
|
+
return args;
|
|
13502
|
+
}
|
|
13503
|
+
function resolveLiteCommand(workdir, environment = process.env) {
|
|
13504
|
+
const explicitBinary = environment.SUPACLOUD_LITE_CLI_BIN?.trim();
|
|
13505
|
+
if (explicitBinary) {
|
|
13506
|
+
if (explicitBinary.includes("\x00"))
|
|
13507
|
+
throw new Error("Invalid SUPACLOUD_LITE_CLI_BIN");
|
|
13508
|
+
return [explicitBinary];
|
|
13509
|
+
}
|
|
13510
|
+
const localPackageEntry = join4(resolve5(workdir), "node_modules", "@supacloud", "lite", "dist", "launcher.cjs");
|
|
13511
|
+
if (existsSync6(localPackageEntry))
|
|
13512
|
+
return [process.execPath, localPackageEntry];
|
|
13513
|
+
return ["supacloud-lite"];
|
|
13514
|
+
}
|
|
13515
|
+
function spawnLiteCommand(command, workdir, environment, inheritOutput) {
|
|
13516
|
+
const [executable, ...commandArguments] = command;
|
|
13517
|
+
return new Promise((resolveExecution, rejectExecution) => {
|
|
13518
|
+
const child = spawn2(executable, commandArguments, {
|
|
13519
|
+
cwd: workdir,
|
|
13520
|
+
env: { ...environment, NO_COLOR: "1" },
|
|
13521
|
+
shell: false,
|
|
13522
|
+
stdio: inheritOutput ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
|
|
13523
|
+
windowsHide: true
|
|
13524
|
+
});
|
|
13525
|
+
const forwardSignal = (signal) => child.kill(signal);
|
|
13526
|
+
process.once("SIGINT", forwardSignal);
|
|
13527
|
+
process.once("SIGTERM", forwardSignal);
|
|
13528
|
+
const cleanup = () => {
|
|
13529
|
+
process.off("SIGINT", forwardSignal);
|
|
13530
|
+
process.off("SIGTERM", forwardSignal);
|
|
13531
|
+
};
|
|
13532
|
+
if (inheritOutput) {
|
|
13533
|
+
child.once("error", (error) => {
|
|
13534
|
+
cleanup();
|
|
13535
|
+
rejectExecution(error);
|
|
13536
|
+
});
|
|
13537
|
+
child.once("close", (exitCode) => {
|
|
13538
|
+
cleanup();
|
|
13539
|
+
resolveExecution({ exitCode: exitCode ?? 1, stdout: "", stderr: "" });
|
|
13540
|
+
});
|
|
13541
|
+
return;
|
|
13542
|
+
}
|
|
13543
|
+
if (!child.stdout || !child.stderr) {
|
|
13544
|
+
cleanup();
|
|
13545
|
+
rejectExecution(new Error("Lite CLI child process did not expose piped output"));
|
|
13546
|
+
return;
|
|
13547
|
+
}
|
|
13548
|
+
let standardOutput = "";
|
|
13549
|
+
let standardError = "";
|
|
13550
|
+
child.stdout.setEncoding("utf8");
|
|
13551
|
+
child.stderr.setEncoding("utf8");
|
|
13552
|
+
child.stdout.on("data", (chunk) => {
|
|
13553
|
+
standardOutput += chunk;
|
|
13554
|
+
});
|
|
13555
|
+
child.stderr.on("data", (chunk) => {
|
|
13556
|
+
standardError += chunk;
|
|
13557
|
+
});
|
|
13558
|
+
child.once("error", (error) => {
|
|
13559
|
+
cleanup();
|
|
13560
|
+
rejectExecution(error);
|
|
13561
|
+
});
|
|
13562
|
+
child.once("close", (exitCode) => {
|
|
13563
|
+
cleanup();
|
|
13564
|
+
resolveExecution({ exitCode: exitCode ?? 1, stdout: standardOutput, stderr: standardError });
|
|
13565
|
+
});
|
|
13566
|
+
});
|
|
13567
|
+
}
|
|
13568
|
+
async function executeLiteCli(request, environment, fallbackWorkdir) {
|
|
13569
|
+
const workdir = requireWorkdir(request.workdir, fallbackWorkdir);
|
|
13570
|
+
const command = [...resolveLiteCommand(workdir, environment), ...buildLiteArgs({ ...request, workdir })];
|
|
13571
|
+
try {
|
|
13572
|
+
return await spawnLiteCommand(command, workdir, environment, request.action === "start");
|
|
13573
|
+
} catch (error) {
|
|
13574
|
+
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
13575
|
+
throw new Error([
|
|
13576
|
+
"SupaCloud Lite CLI could not be started.",
|
|
13577
|
+
"Install @supacloud/lite, put supacloud-lite on PATH, or set SUPACLOUD_LITE_CLI_BIN.",
|
|
13578
|
+
failureMessage
|
|
13579
|
+
].join(" "));
|
|
13580
|
+
}
|
|
13581
|
+
}
|
|
13582
|
+
function formatExecutionText2(action, execution) {
|
|
13583
|
+
const combinedOutput = [execution.stdout.trim(), execution.stderr.trim()].filter(Boolean).join(`
|
|
13584
|
+
`);
|
|
13585
|
+
const heading = execution.exitCode === 0 ? `✅ SupaCloud Lite ${action} completed` : `❌ SupaCloud Lite ${action} failed (exit ${execution.exitCode})`;
|
|
13586
|
+
return combinedOutput ? `${heading}
|
|
13587
|
+
${combinedOutput}` : heading;
|
|
13588
|
+
}
|
|
13589
|
+
function registerLiteCliTools(server, options = {}) {
|
|
13590
|
+
const environment = options.environment || process.env;
|
|
13591
|
+
const fallbackWorkdir = options.currentWorkingDirectory || process.cwd();
|
|
13592
|
+
const execute = options.executeLiteCli || ((request) => executeLiteCli(request, environment, fallbackWorkdir));
|
|
13593
|
+
server.tool("lite", "Controlled adapter for the local SupaCloud Lite CLI. Lite actions are local-only and never use the Management API or official Supabase CLI.", {
|
|
13594
|
+
action: withDescription(stringEnum([
|
|
13595
|
+
"version",
|
|
13596
|
+
"start",
|
|
13597
|
+
"migrate",
|
|
13598
|
+
"status",
|
|
13599
|
+
"keys",
|
|
13600
|
+
"gen_types",
|
|
13601
|
+
"db_reset",
|
|
13602
|
+
"db_diff",
|
|
13603
|
+
"db_pull",
|
|
13604
|
+
"snapshot_create",
|
|
13605
|
+
"snapshot_restore",
|
|
13606
|
+
"upgrade",
|
|
13607
|
+
"inspect",
|
|
13608
|
+
"doctor"
|
|
13609
|
+
]), "Lite CLI action"),
|
|
13610
|
+
workdir: optional(Type.String(), "[*] Process working directory (default: current directory)"),
|
|
13611
|
+
project_dir: optional(Type.String(), "[*] Project containing supabase/"),
|
|
13612
|
+
state_dir: optional(Type.String(), "[*] Lite state root"),
|
|
13613
|
+
data_dir: optional(Type.String(), "[*] PGlite/native data directory"),
|
|
13614
|
+
storage_dir: optional(Type.String(), "[*] Object storage directory"),
|
|
13615
|
+
storage_backend: optional(stringEnum(["fs", "memory", "s3"]), "[*] Storage backend"),
|
|
13616
|
+
s3_prefix: optional(Type.String(), "[*] S3 object key prefix"),
|
|
13617
|
+
engine: optional(stringEnum(["pglite", "native"]), "[*] Database engine"),
|
|
13618
|
+
host: optional(Type.String(), "[start] Listen host"),
|
|
13619
|
+
port: optional(Type.Number(), "[start] Listen port"),
|
|
13620
|
+
api_url: optional(Type.String(), "[start] Public API URL"),
|
|
13621
|
+
site_url: optional(Type.String(), "[start] Auth site URL"),
|
|
13622
|
+
replication_profile: optional(stringEnum(["powersync"]), "[start/doctor] Replication profile"),
|
|
13623
|
+
replication_host: optional(Type.String(), "[start] Replication listener host"),
|
|
13624
|
+
replication_port: optional(Type.Number(), "[start] Replication listener port"),
|
|
13625
|
+
replication_allow_cidrs: optional(Type.String(), "[start] Replication client CIDRs"),
|
|
13626
|
+
powersync_tables: optional(Type.String(), "[start] PowerSync publication tables"),
|
|
13627
|
+
replication_tls_cert: optional(Type.String(), "[start] Replication TLS certificate"),
|
|
13628
|
+
replication_tls_key: optional(Type.String(), "[start] Replication TLS private key"),
|
|
13629
|
+
output: optional(Type.String(), "[gen_types/snapshot_create/upgrade] Output path"),
|
|
13630
|
+
file: optional(Type.String(), "[db_diff/db_pull] Migration suffix or name"),
|
|
13631
|
+
snapshot_file: optional(Type.String(), "[snapshot_restore] Snapshot archive"),
|
|
13632
|
+
service_role: optional(Type.Boolean(), "[keys] Also print the service_role key"),
|
|
13633
|
+
force: optional(Type.Boolean(), "[snapshot_restore] Replace non-empty restore targets"),
|
|
13634
|
+
memory: optional(Type.Boolean(), "[*] Use an in-memory PGlite database"),
|
|
13635
|
+
json: optional(Type.Boolean(), "[doctor] Emit machine-readable output")
|
|
13636
|
+
}, async (request) => {
|
|
13637
|
+
const execution = await execute(request);
|
|
13638
|
+
return {
|
|
13639
|
+
isError: execution.exitCode !== 0,
|
|
13640
|
+
content: [{ type: "text", text: formatExecutionText2(request.action, execution) }]
|
|
13641
|
+
};
|
|
13642
|
+
});
|
|
13643
|
+
}
|
|
13644
|
+
|
|
12897
13645
|
// src/shared/tools/ai-tools.ts
|
|
12898
13646
|
import {
|
|
12899
13647
|
cpSync,
|
|
12900
|
-
existsSync as
|
|
13648
|
+
existsSync as existsSync7,
|
|
12901
13649
|
lstatSync as lstatSync2,
|
|
12902
13650
|
mkdirSync as mkdirSync2,
|
|
12903
13651
|
mkdtempSync as mkdtempSync2,
|
|
@@ -12907,13 +13655,13 @@ import {
|
|
|
12907
13655
|
rmSync as rmSync2
|
|
12908
13656
|
} from "node:fs";
|
|
12909
13657
|
import { homedir as homedir2 } from "node:os";
|
|
12910
|
-
import { dirname as dirname2, join as
|
|
13658
|
+
import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
|
|
12911
13659
|
import { fileURLToPath } from "node:url";
|
|
12912
13660
|
var SKILL_NAME = "supacloud-cli";
|
|
12913
13661
|
function regularFiles(rootDirectory, currentDirectory = rootDirectory) {
|
|
12914
13662
|
const files = [];
|
|
12915
13663
|
for (const directoryEntry of readdirSync3(currentDirectory, { withFileTypes: true })) {
|
|
12916
|
-
const entryPath =
|
|
13664
|
+
const entryPath = join5(currentDirectory, directoryEntry.name);
|
|
12917
13665
|
if (directoryEntry.isSymbolicLink())
|
|
12918
13666
|
throw new Error(`Skill directories cannot contain symlinks: ${entryPath}`);
|
|
12919
13667
|
if (directoryEntry.isDirectory())
|
|
@@ -12924,13 +13672,13 @@ function regularFiles(rootDirectory, currentDirectory = rootDirectory) {
|
|
|
12924
13672
|
return files.sort();
|
|
12925
13673
|
}
|
|
12926
13674
|
function directoriesMatch(sourceDirectory, destinationDirectory) {
|
|
12927
|
-
if (!
|
|
13675
|
+
if (!existsSync7(destinationDirectory) || !lstatSync2(destinationDirectory).isDirectory())
|
|
12928
13676
|
return false;
|
|
12929
13677
|
const sourceFiles = regularFiles(sourceDirectory);
|
|
12930
13678
|
const destinationFiles = regularFiles(destinationDirectory);
|
|
12931
13679
|
if (sourceFiles.join("\x00") !== destinationFiles.join("\x00"))
|
|
12932
13680
|
return false;
|
|
12933
|
-
return sourceFiles.every((file) => readFileSync5(
|
|
13681
|
+
return sourceFiles.every((file) => readFileSync5(join5(sourceDirectory, file)).equals(readFileSync5(join5(destinationDirectory, file))));
|
|
12934
13682
|
}
|
|
12935
13683
|
function backupTimestamp(now) {
|
|
12936
13684
|
return now.toISOString().replace(/[-:.]/g, "");
|
|
@@ -12939,7 +13687,7 @@ function availableBackupDirectory(destinationDirectory, now) {
|
|
|
12939
13687
|
const baseDirectory = `${destinationDirectory}.backup-${backupTimestamp(now)}`;
|
|
12940
13688
|
let candidate = baseDirectory;
|
|
12941
13689
|
let suffix = 2;
|
|
12942
|
-
while (
|
|
13690
|
+
while (existsSync7(candidate)) {
|
|
12943
13691
|
candidate = `${baseDirectory}-${suffix}`;
|
|
12944
13692
|
suffix += 1;
|
|
12945
13693
|
}
|
|
@@ -12947,8 +13695,8 @@ function availableBackupDirectory(destinationDirectory, now) {
|
|
|
12947
13695
|
}
|
|
12948
13696
|
function stagedSkill(sourceDirectory, targetRoot) {
|
|
12949
13697
|
mkdirSync2(targetRoot, { recursive: true });
|
|
12950
|
-
const stagingRoot = mkdtempSync2(
|
|
12951
|
-
const stagingSkill =
|
|
13698
|
+
const stagingRoot = mkdtempSync2(join5(targetRoot, ".supacloud-cli-install-"));
|
|
13699
|
+
const stagingSkill = join5(stagingRoot, SKILL_NAME);
|
|
12952
13700
|
try {
|
|
12953
13701
|
cpSync(sourceDirectory, stagingSkill, { recursive: true, errorOnExist: true });
|
|
12954
13702
|
} catch (error) {
|
|
@@ -12980,13 +13728,13 @@ function replaceSkill(sourceDirectory, targetRoot, destinationDirectory, backupD
|
|
|
12980
13728
|
}
|
|
12981
13729
|
}
|
|
12982
13730
|
function skillSummary(request, action, files, backupDirectory) {
|
|
12983
|
-
const sourceDirectory =
|
|
12984
|
-
const targetRoot =
|
|
13731
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13732
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
12985
13733
|
return {
|
|
12986
13734
|
name: SKILL_NAME,
|
|
12987
13735
|
sourceDirectory,
|
|
12988
13736
|
targetRoot,
|
|
12989
|
-
destinationDirectory:
|
|
13737
|
+
destinationDirectory: join5(targetRoot, SKILL_NAME),
|
|
12990
13738
|
action,
|
|
12991
13739
|
mode: request.mode,
|
|
12992
13740
|
changed: action !== "none",
|
|
@@ -12995,14 +13743,14 @@ function skillSummary(request, action, files, backupDirectory) {
|
|
|
12995
13743
|
};
|
|
12996
13744
|
}
|
|
12997
13745
|
function installSkill(request) {
|
|
12998
|
-
const sourceDirectory =
|
|
12999
|
-
const targetRoot =
|
|
13000
|
-
const destinationDirectory =
|
|
13001
|
-
if (!
|
|
13746
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13747
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13748
|
+
const destinationDirectory = join5(targetRoot, SKILL_NAME);
|
|
13749
|
+
if (!existsSync7(join5(sourceDirectory, "SKILL.md"))) {
|
|
13002
13750
|
throw new Error(`Bundled SupaCloud CLI skill not found: ${sourceDirectory}`);
|
|
13003
13751
|
}
|
|
13004
13752
|
const files = regularFiles(sourceDirectory);
|
|
13005
|
-
if (!
|
|
13753
|
+
if (!existsSync7(destinationDirectory))
|
|
13006
13754
|
return installNewSkill(request, files);
|
|
13007
13755
|
if (directoriesMatch(sourceDirectory, destinationDirectory)) {
|
|
13008
13756
|
return skillSummary(request, "none", files, null);
|
|
@@ -13013,17 +13761,17 @@ function installSkill(request) {
|
|
|
13013
13761
|
return installReplacementSkill(request, files);
|
|
13014
13762
|
}
|
|
13015
13763
|
function installNewSkill(request, files) {
|
|
13016
|
-
const sourceDirectory =
|
|
13017
|
-
const targetRoot =
|
|
13764
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13765
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13018
13766
|
if (request.mode === "write") {
|
|
13019
|
-
createSkill(sourceDirectory, targetRoot,
|
|
13767
|
+
createSkill(sourceDirectory, targetRoot, join5(targetRoot, SKILL_NAME));
|
|
13020
13768
|
}
|
|
13021
13769
|
return skillSummary(request, "create", files, null);
|
|
13022
13770
|
}
|
|
13023
13771
|
function installReplacementSkill(request, files) {
|
|
13024
|
-
const sourceDirectory =
|
|
13025
|
-
const targetRoot =
|
|
13026
|
-
const destinationDirectory =
|
|
13772
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13773
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13774
|
+
const destinationDirectory = join5(targetRoot, SKILL_NAME);
|
|
13027
13775
|
const backupDirectory = availableBackupDirectory(destinationDirectory, request.now);
|
|
13028
13776
|
if (request.mode === "write") {
|
|
13029
13777
|
replaceSkill(sourceDirectory, targetRoot, destinationDirectory, backupDirectory);
|
|
@@ -13032,15 +13780,15 @@ function installReplacementSkill(request, files) {
|
|
|
13032
13780
|
}
|
|
13033
13781
|
function resolveDefaultCodexSkillRoot(environment = process.env, homeDirectory = homedir2()) {
|
|
13034
13782
|
const codexHome = environment.CODEX_HOME?.trim();
|
|
13035
|
-
return
|
|
13783
|
+
return join5(resolve6(codexHome || join5(homeDirectory, ".codex")), "skills");
|
|
13036
13784
|
}
|
|
13037
13785
|
function resolveBundledSkillDirectory(moduleUrl = import.meta.url) {
|
|
13038
13786
|
const moduleDirectory = dirname2(fileURLToPath(moduleUrl));
|
|
13039
13787
|
const candidates = [
|
|
13040
|
-
|
|
13041
|
-
|
|
13788
|
+
resolve6(moduleDirectory, "../../../skills", SKILL_NAME),
|
|
13789
|
+
resolve6(moduleDirectory, "../skills", SKILL_NAME)
|
|
13042
13790
|
];
|
|
13043
|
-
const skillDirectory = candidates.find((candidate) =>
|
|
13791
|
+
const skillDirectory = candidates.find((candidate) => existsSync7(join5(candidate, "SKILL.md")));
|
|
13044
13792
|
if (!skillDirectory)
|
|
13045
13793
|
throw new Error("Bundled SupaCloud CLI skill is missing from this installation");
|
|
13046
13794
|
return skillDirectory;
|
|
@@ -13062,7 +13810,7 @@ function registerAiTools(server) {
|
|
|
13062
13810
|
name: SKILL_NAME,
|
|
13063
13811
|
sourceDirectory,
|
|
13064
13812
|
defaultTargetRoot,
|
|
13065
|
-
defaultDestination:
|
|
13813
|
+
defaultDestination: join5(defaultTargetRoot, SKILL_NAME)
|
|
13066
13814
|
});
|
|
13067
13815
|
}
|
|
13068
13816
|
return textResponse(installSkill({
|
|
@@ -13077,8 +13825,8 @@ function registerAiTools(server) {
|
|
|
13077
13825
|
|
|
13078
13826
|
// src/shared/tools/scheduled-function-tools.ts
|
|
13079
13827
|
import { randomUUID } from "node:crypto";
|
|
13080
|
-
import { readFileSync as readFileSync6, statSync as
|
|
13081
|
-
import { resolve as
|
|
13828
|
+
import { readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
|
|
13829
|
+
import { resolve as resolve7 } from "node:path";
|
|
13082
13830
|
import { isDeepStrictEqual } from "node:util";
|
|
13083
13831
|
var HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
|
|
13084
13832
|
var ENVIRONMENT_NAME_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
@@ -13162,8 +13910,8 @@ function validScheduledFunctionCron(expression) {
|
|
|
13162
13910
|
function readScheduleBodyFile(bodyPathInput) {
|
|
13163
13911
|
if (!bodyPathInput.trim())
|
|
13164
13912
|
throw new Error("'body_file' must be a path");
|
|
13165
|
-
const bodyPath =
|
|
13166
|
-
const bodyStat =
|
|
13913
|
+
const bodyPath = resolve7(bodyPathInput);
|
|
13914
|
+
const bodyStat = statSync5(bodyPath);
|
|
13167
13915
|
if (!bodyStat.isFile() || bodyStat.size > MAX_BODY_FILE_BYTES) {
|
|
13168
13916
|
throw new Error("Scheduled Function body file must be a regular file no larger than 1 MiB");
|
|
13169
13917
|
}
|
|
@@ -13508,14 +14256,14 @@ var SCHEDULE_TOOL_SCHEMA = {
|
|
|
13508
14256
|
};
|
|
13509
14257
|
|
|
13510
14258
|
// src/shared/mutation-protocol.ts
|
|
13511
|
-
var
|
|
14259
|
+
var MUTATION_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
13512
14260
|
var FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
|
|
13513
14261
|
var OPERATION_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
13514
14262
|
var RESOURCE_KEY_PATTERN = /^v1\/(?:[a-z0-9][a-z0-9._-]{0,63})\/([A-Za-z0-9_-]{2,171})$/;
|
|
13515
14263
|
var RESOURCE_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
13516
14264
|
var FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
13517
14265
|
var LEASE_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,254}$/;
|
|
13518
|
-
var
|
|
14266
|
+
var TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
13519
14267
|
var MAX_STATUS_RESPONSE_BYTES = 196608;
|
|
13520
14268
|
var MAX_RESOURCE_ID_BYTES = 128;
|
|
13521
14269
|
var FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -13548,7 +14296,7 @@ var MUTATION_KEYS = [
|
|
|
13548
14296
|
var PRINCIPAL_KEYS = ["type", "id"];
|
|
13549
14297
|
var LEASE_KEYS = ["owner", "expires_at", "fencing_epoch"];
|
|
13550
14298
|
function isMutationId(candidate) {
|
|
13551
|
-
return typeof candidate === "string" &&
|
|
14299
|
+
return typeof candidate === "string" && MUTATION_ID_PATTERN2.test(candidate);
|
|
13552
14300
|
}
|
|
13553
14301
|
function objectRecord4(candidate) {
|
|
13554
14302
|
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
@@ -13563,14 +14311,14 @@ function emptyProjection(candidate) {
|
|
|
13563
14311
|
const record = objectRecord4(candidate);
|
|
13564
14312
|
return record && Object.keys(record).length === 0 ? record : null;
|
|
13565
14313
|
}
|
|
13566
|
-
function
|
|
13567
|
-
if (typeof candidate !== "string" || !
|
|
14314
|
+
function canonicalTimestamp3(candidate) {
|
|
14315
|
+
if (typeof candidate !== "string" || !TIMESTAMP_PATTERN2.test(candidate))
|
|
13568
14316
|
return false;
|
|
13569
14317
|
const milliseconds = Date.parse(candidate);
|
|
13570
14318
|
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
13571
14319
|
}
|
|
13572
14320
|
function nullableTimestamp(candidate) {
|
|
13573
|
-
return candidate === null ||
|
|
14321
|
+
return candidate === null || canonicalTimestamp3(candidate);
|
|
13574
14322
|
}
|
|
13575
14323
|
function safePrincipal(candidate) {
|
|
13576
14324
|
const principal = exactRecord(candidate, PRINCIPAL_KEYS);
|
|
@@ -13653,7 +14401,7 @@ function validMutationTerminalFields(mutation) {
|
|
|
13653
14401
|
return false;
|
|
13654
14402
|
if (mutation.failure_code !== null && (typeof mutation.failure_code !== "string" || !FAILURE_CODE_PATTERN.test(mutation.failure_code)))
|
|
13655
14403
|
return false;
|
|
13656
|
-
return nullableTimestamp(mutation.completed_at) &&
|
|
14404
|
+
return nullableTimestamp(mutation.completed_at) && canonicalTimestamp3(mutation.created_at) && canonicalTimestamp3(mutation.updated_at);
|
|
13657
14405
|
}
|
|
13658
14406
|
function safeMutationStatus(candidate) {
|
|
13659
14407
|
const mutation = exactRecord(candidate, MUTATION_KEYS);
|
|
@@ -13761,7 +14509,7 @@ var RELEASE_CANARY_CLAIM_MAX_LENGTH = 2048;
|
|
|
13761
14509
|
function isRecord3(value) {
|
|
13762
14510
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13763
14511
|
}
|
|
13764
|
-
function
|
|
14512
|
+
function canonicalTimestamp4(value) {
|
|
13765
14513
|
if (typeof value !== "string")
|
|
13766
14514
|
return false;
|
|
13767
14515
|
const parsed = new Date(value);
|
|
@@ -13774,7 +14522,7 @@ function backupBelongsToProject(backupId, projectRef2) {
|
|
|
13774
14522
|
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef2}_`);
|
|
13775
14523
|
}
|
|
13776
14524
|
function verifiedBackup(value, projectRef2) {
|
|
13777
|
-
if (!isRecord3(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !
|
|
14525
|
+
if (!isRecord3(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp4(value.created_at) || !canonicalTimestamp4(value.completed_at) || new Date(value.completed_at).valueOf() < new Date(value.created_at).valueOf() || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sha256 !== "string" || !SHA256.test(value.sha256))
|
|
13778
14526
|
return null;
|
|
13779
14527
|
return {
|
|
13780
14528
|
backup_id: value.backup_id,
|
|
@@ -14121,7 +14869,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
14121
14869
|
// package.json
|
|
14122
14870
|
var package_default = {
|
|
14123
14871
|
name: "@supacloud/cli",
|
|
14124
|
-
version: "0.
|
|
14872
|
+
version: "0.34.0",
|
|
14125
14873
|
description: "Project-scoped CLI for SupaCloud users",
|
|
14126
14874
|
type: "module",
|
|
14127
14875
|
main: "./dist/index.js",
|
|
@@ -14386,6 +15134,9 @@ EXAMPLES
|
|
|
14386
15134
|
${preferredCommand} supabase db_diff --schema public --name add_accounts
|
|
14387
15135
|
${preferredCommand} supabase push --ref abc123 --dir supabase/migrations --dry_run
|
|
14388
15136
|
${preferredCommand} supabase db_dump --db_url "postgresql://..." --file backups/schema.sql
|
|
15137
|
+
${preferredCommand} lite migrate --project_dir .
|
|
15138
|
+
${preferredCommand} lite start --project_dir . --port 54321
|
|
15139
|
+
${preferredCommand} lite doctor --project_dir . --json
|
|
14389
15140
|
${preferredCommand} branch create --name feature-auth --data_mode schema_only
|
|
14390
15141
|
${preferredCommand} branch promotion_plan --branch_ref preview123
|
|
14391
15142
|
${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
|
|
@@ -14438,6 +15189,7 @@ function createCliTools(context, confirmProduction) {
|
|
|
14438
15189
|
projectRef: context.projectRef || undefined,
|
|
14439
15190
|
readOnly: context.readOnly
|
|
14440
15191
|
})));
|
|
15192
|
+
Object.assign(tools, captureTools((server) => registerLiteCliTools(server)));
|
|
14441
15193
|
Object.assign(tools, captureTools((server) => registerAiTools(server)));
|
|
14442
15194
|
const registerContextAwareHelp = () => {
|
|
14443
15195
|
tools.project = {
|
|
@@ -14494,6 +15246,11 @@ function createCliTools(context, confirmProduction) {
|
|
|
14494
15246
|
if (branchHelpTool) {
|
|
14495
15247
|
tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
|
|
14496
15248
|
}
|
|
15249
|
+
const frontendContextCallback = tools.frontend.callback;
|
|
15250
|
+
const frontendHelpTool = captureTools((server) => registerFrontendTools(server, {})).frontend;
|
|
15251
|
+
if (frontendHelpTool) {
|
|
15252
|
+
tools.frontend = { schema: frontendHelpTool.schema, callback: frontendContextCallback };
|
|
15253
|
+
}
|
|
14497
15254
|
};
|
|
14498
15255
|
if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
|
|
14499
15256
|
registerContextAwareHelp();
|
|
@@ -14599,7 +15356,7 @@ async function main() {
|
|
|
14599
15356
|
return;
|
|
14600
15357
|
}
|
|
14601
15358
|
const cliTools = createCliTools(context, globalOptions.confirmProduction);
|
|
14602
|
-
if (args.length === 1 && !["ai", "supabase"].includes(args[0]) && cliTools[args[0]]) {
|
|
15359
|
+
if (args.length === 1 && !["ai", "supabase", "lite"].includes(args[0]) && cliTools[args[0]]) {
|
|
14603
15360
|
const result = await cliTools[args[0]].callback({});
|
|
14604
15361
|
if (result?.content && Array.isArray(result.content)) {
|
|
14605
15362
|
for (const chunk of result.content) {
|