@supacloud/cli 0.90.0 → 0.90.1

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 CHANGED
@@ -633,54 +633,25 @@ query parameters and accepts only its authoritative JSON boolean `false`
633
633
  read-back. The fixture binding comes from the validated disable receipt before
634
634
  reporting success. Management endpoint projection is checked before and after.
635
635
 
636
- ### Release scope rebinding and inspection
636
+ ### Internal release scope controls
637
637
 
638
- Release scopes bind deployment manifests (functions, migrations, web assets) to
639
- an exact 40-character base commit SHA and one concrete deployment environment.
640
- Old scope files without `targetEnvironmentId` are rejected and must be
641
- regenerated. Test and production scopes may share the same source commit, but
642
- they are separate environment-bound release identities.
638
+ Scope files and manual release-controller operations are internal compatibility
639
+ surfaces. They are not part of the normal customer deployment path. Use the
640
+ project command instead:
643
641
 
644
- ```bash
645
- # Rebind a single environment-bound scope to latest git main (or origin/main)
646
- supacloud release scope_rebind --file supacloud/fa/release-scopes/20260907-intake-p1-production.json --environment_id production
647
-
648
- # Rebind multiple production files together
649
- supacloud release scope_rebind --files "20260907-intake-p1-production.json,20260907-intake-p1-production-hotfix.json" --environment_id production
650
-
651
- # Rebind staging separately; a staging scope can never be rebound as production
652
- supacloud release scope_rebind --task 20260907-intake-p1 --environment_id staging
653
-
654
- # Rebind all scopes matching a task ID or filename prefix in standard directories
655
- supacloud release scope_rebind --task 20260907-intake-p1 --environment_id production
656
-
657
- # Specify an explicit base commit SHA or git ref
658
- supacloud release scope_rebind --task 20260907-intake-p1 --base_commit origin/main --environment_id production
659
-
660
- # Preview changes without modifying files
661
- supacloud release scope_rebind --task 20260907-intake-p1 --environment_id production --dry_run
662
-
663
- # Inspect scope status, SHA-256 hash, and alignment with git HEAD / origin/main
664
- supacloud release scope_inspect --file 20260907-intake-p1-production.json --environment_id production
665
-
666
- # Scaffold a new canonical release scope
667
- supacloud release scope_create \
668
- --file supacloud/fa/release-scopes/20260907-intake-p1.json \
669
- --task FA-INTAKE-P1 \
670
- --ref abc123 \
671
- --environment_id production \
672
- --functions "fa-api,fa-worker" \
673
- --migrations "20260907120000_init"
642
+ ```sh
643
+ supacloud --env test deploy
644
+ supacloud --env production deploy --confirm-production PROJECT_REF
674
645
  ```
675
646
 
676
- Scope actions run locally without requiring Management API credentials. Files
677
- are formatted canonically with 2 spaces and sorted keys, and the authoritative
678
- `scope_sha256` checksum is recalculated automatically. `scope_create` resolves
679
- `origin/main` by default, so a new test or production scope is generated from
680
- the current main baseline instead of reusing an older scope identity.
681
- These commands are for release maintenance and audit workflows; customers do
682
- not need to create or rebind a scope before running the one-command deployment
683
- flow above.
647
+ FA deployment policy and the boundary between platform-owned operations and
648
+ FA-owned business features are documented in
649
+ [`docs/fa-deployment-capability-boundary.md`](../../docs/fa-deployment-capability-boundary.md).
650
+
651
+ Scope creation, rebinding, inspection and hash calculation are intentionally
652
+ undocumented here. They remain available only for internal compatibility,
653
+ audit and historical receipt migration. Customers do not need a scope file
654
+ before running the one-command deployment flow above.
684
655
 
685
656
  The legacy `.env` fallback is unclassified and therefore does not enable the
686
657
  production confirmation gate. Production automation must select a `prod` or
package/dist/index.js CHANGED
@@ -8713,6 +8713,8 @@ function decodedSchema(inputSchema, outputSchema, decode, options) {
8713
8713
  return result;
8714
8714
  }
8715
8715
  function schemaEnumValues(schema) {
8716
+ if (!schema)
8717
+ return [];
8716
8718
  const view = schema;
8717
8719
  if (Array.isArray(view.enum)) {
8718
8720
  return view.enum.filter((value) => typeof value === "string" || typeof value === "number").map((value) => String(value));
@@ -8818,7 +8820,7 @@ async function runCli(cliTools, args, options = {}) {
8818
8820
  `);
8819
8821
  const getSchemaShape = (schema) => schemaProperties(schema);
8820
8822
  const getEnumOptions = (field) => schemaEnumValues(field);
8821
- const getDescription = (field) => schemaDescription(field);
8823
+ const getDescription = (field) => field ? schemaDescription(field) : "";
8822
8824
  const formatToolHelp = (toolName, tool) => {
8823
8825
  const shape = getSchemaShape(tool.schema);
8824
8826
  const actionField = shape.action;
@@ -28024,7 +28026,8 @@ function registerReleaseTools(server, http, options = {}) {
28024
28026
  }
28025
28027
 
28026
28028
  // src/shared/tools/deploy-tools.ts
28027
- import { access, lstat as lstat5, mkdtemp as mkdtemp2, opendir, rm as rm3 } from "node:fs/promises";
28029
+ import { access, lstat as lstat5, mkdtemp as mkdtemp2, opendir, readFile as readFile6, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
28030
+ import { spawn as spawn2 } from "node:child_process";
28028
28031
  import { createHash as createHash9 } from "node:crypto";
28029
28032
  import { tmpdir as tmpdir3 } from "node:os";
28030
28033
  import { basename as basename8, dirname as dirname11, join as join17, relative as relative10, resolve as resolve22, sep as sep5 } from "node:path";
@@ -28074,7 +28077,7 @@ function frontendDeployment(candidate) {
28074
28077
  }
28075
28078
  async function readJson(path) {
28076
28079
  try {
28077
- return await Bun.file(path).json();
28080
+ return JSON.parse(await readFile6(path, "utf8"));
28078
28081
  } catch (error) {
28079
28082
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
28080
28083
  return null;
@@ -28320,9 +28323,9 @@ function resolveInsideProject(projectDirectory, candidate, allowRoot = false) {
28320
28323
  return resolved;
28321
28324
  }
28322
28325
  function createBuildEnvironment(names, environmentVariables) {
28323
- const selectedNames = [...new Set(names || [])].sort();
28324
- const selectedNameSet = new Set(selectedNames);
28325
28326
  const values = environmentVariables ?? process.env;
28327
+ const selectedNames = [...new Set(names ?? Object.keys(values).filter(isPublicBuildEnv))].sort();
28328
+ const selectedNameSet = new Set(selectedNames);
28326
28329
  const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !isProtectedBuildEnv(name) && (!isPublicBuildEnv(name) || selectedNameSet.has(name)) && (!SENSITIVE_BUILD_ENV.test(name) || isPublicBuildEnv(name))));
28327
28330
  const missing = selectedNames.filter((name) => !values[name]?.trim());
28328
28331
  for (const name of selectedNames) {
@@ -28338,15 +28341,21 @@ function assertBuildEnvironment(build, skipBuild) {
28338
28341
  }
28339
28342
  }
28340
28343
  async function runBuild(command, cwd, environment, json) {
28341
- const child = Bun.spawn(["sh", "-c", command], {
28342
- cwd,
28343
- env: environment,
28344
- stdio: ["inherit", json ? 2 : "inherit", "inherit"]
28344
+ await new Promise((resolve, reject) => {
28345
+ const child = spawn2("sh", ["-c", command], {
28346
+ cwd,
28347
+ env: environment,
28348
+ stdio: ["inherit", json ? 2 : "inherit", "inherit"]
28349
+ });
28350
+ child.once("error", reject);
28351
+ child.once("close", (exitCode) => {
28352
+ if (exitCode === 0) {
28353
+ resolve();
28354
+ } else {
28355
+ reject(new Error(`Build command failed (exit ${exitCode ?? "unknown"}): ${command}`));
28356
+ }
28357
+ });
28345
28358
  });
28346
- const exitCode = await child.exited;
28347
- if (exitCode !== 0) {
28348
- throw new Error(`Build command failed (exit ${exitCode ?? "unknown"}): ${command}`);
28349
- }
28350
28359
  }
28351
28360
  async function collectArchiveFiles(root) {
28352
28361
  const rootStat = await lstat5(root).catch(() => null);
@@ -28379,7 +28388,7 @@ async function collectArchiveFiles(root) {
28379
28388
  const files = new Map;
28380
28389
  const digests = [];
28381
28390
  for (const path of paths) {
28382
- const data = new Uint8Array(await Bun.file(path).arrayBuffer());
28391
+ const data = new Uint8Array(await readFile6(path));
28383
28392
  bytes += data.byteLength;
28384
28393
  if (bytes > MAX_SOURCE_BYTES)
28385
28394
  throw new Error(`Frontend output exceeds ${MAX_SOURCE_BYTES} bytes`);
@@ -28403,7 +28412,7 @@ async function createFrontendArchive(outputDirectory) {
28403
28412
  const archivePath = join17(temporaryDirectory, `${basename8(outputDirectory)}.tar.zst`);
28404
28413
  try {
28405
28414
  const archiveBytes = await createFrontendTarZstd(collected.files);
28406
- await Bun.write(archivePath, archiveBytes);
28415
+ await writeFile6(archivePath, archiveBytes);
28407
28416
  const sha256 = createHash9("sha256").update(archiveBytes).digest("hex");
28408
28417
  return {
28409
28418
  archivePath,
@@ -28792,7 +28801,7 @@ function registerDeployTools(server, http, options = {}) {
28792
28801
 
28793
28802
  // src/shared/tools/remote-dev-tools.ts
28794
28803
  import { existsSync as existsSync14 } from "node:fs";
28795
- import { readFile as readFile6 } from "node:fs/promises";
28804
+ import { readFile as readFile7 } from "node:fs/promises";
28796
28805
  import { readdir as readdir7 } from "node:fs/promises";
28797
28806
  import { createHash as createHash10 } from "node:crypto";
28798
28807
  import { join as join18, resolve as resolve23 } from "node:path";
@@ -28870,7 +28879,7 @@ async function readProjectConfig(root) {
28870
28879
  if (!existsSync14(file))
28871
28880
  return {};
28872
28881
  try {
28873
- const parsed = JSON.parse(await readFile6(file, "utf8"));
28882
+ const parsed = JSON.parse(await readFile7(file, "utf8"));
28874
28883
  return parsed && typeof parsed === "object" ? parsed : {};
28875
28884
  } catch (error) {
28876
28885
  throw new Error(`Invalid supacloud.json: ${error instanceof Error ? error.message : String(error)}`);
@@ -28896,7 +28905,7 @@ async function sourceFingerprint(root) {
28896
28905
  const hash = createHash10("sha256");
28897
28906
  for (const file of files) {
28898
28907
  hash.update(file.slice(root.length).replace(/\\/g, "/"));
28899
- hash.update(await readFile6(file));
28908
+ hash.update(await readFile7(file));
28900
28909
  }
28901
28910
  return hash.digest("hex");
28902
28911
  }
@@ -29062,7 +29071,7 @@ function registerRemoteDevTools(server, options = {}) {
29062
29071
 
29063
29072
  // src/shared/tools/developer-experience-tools.ts
29064
29073
  import { existsSync as existsSync15 } from "node:fs";
29065
- import { mkdir as mkdir7, readdir as readdir8, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
29074
+ import { mkdir as mkdir7, readdir as readdir8, readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
29066
29075
  import { dirname as dirname12, join as join19, resolve as resolve24 } from "node:path";
29067
29076
  var initToolSchema = {
29068
29077
  root: optional(String2(), "Project directory to initialize (default: current directory)"),
@@ -29117,7 +29126,7 @@ async function readPackageManifest(root) {
29117
29126
  if (!existsSync15(path))
29118
29127
  return {};
29119
29128
  try {
29120
- const parsed = JSON.parse(await readFile7(path, "utf8"));
29129
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
29121
29130
  const value = record5(parsed);
29122
29131
  if (!value)
29123
29132
  throw new Error("package.json must contain an object");
@@ -29135,7 +29144,7 @@ async function readPackageManifest(root) {
29135
29144
  }
29136
29145
  async function writeNewFile(path, content) {
29137
29146
  try {
29138
- await writeFile6(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
29147
+ await writeFile7(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
29139
29148
  return true;
29140
29149
  } catch (error) {
29141
29150
  if (error instanceof Error && "code" in error && error.code === "EEXIST")
@@ -29145,7 +29154,7 @@ async function writeNewFile(path, content) {
29145
29154
  }
29146
29155
  async function ensureGitignore(root) {
29147
29156
  const path = join19(root, ".gitignore");
29148
- const existing = existsSync15(path) ? await readFile7(path, "utf8") : "";
29157
+ const existing = existsSync15(path) ? await readFile8(path, "utf8") : "";
29149
29158
  const required = [".env.supacloud.*", "!.env.supacloud.*.example"];
29150
29159
  const missing = required.filter((entry) => !existing.split(/\r?\n/).includes(entry));
29151
29160
  if (missing.length === 0)
@@ -29153,7 +29162,7 @@ async function ensureGitignore(root) {
29153
29162
  const prefix = existing.length > 0 && !existing.endsWith(`
29154
29163
  `) ? `${existing}
29155
29164
  ` : existing;
29156
- await writeFile6(path, `${prefix}${missing.join(`
29165
+ await writeFile7(path, `${prefix}${missing.join(`
29157
29166
  `)}
29158
29167
  `, "utf8");
29159
29168
  return true;
@@ -29176,7 +29185,7 @@ async function initializeProject(request, options) {
29176
29185
  };
29177
29186
  const created = [];
29178
29187
  try {
29179
- await writeFile6(configPath, `${JSON.stringify(config, null, 2)}
29188
+ await writeFile7(configPath, `${JSON.stringify(config, null, 2)}
29180
29189
  `, {
29181
29190
  encoding: "utf8",
29182
29191
  flag: "wx",
@@ -29372,7 +29381,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
29372
29381
  // package.json
29373
29382
  var package_default = {
29374
29383
  name: "@supacloud/cli",
29375
- version: "0.90.0",
29384
+ version: "0.90.1",
29376
29385
  description: "Project-scoped CLI for SupaCloud users",
29377
29386
  type: "module",
29378
29387
  main: "./dist/index.js",
@@ -29665,8 +29674,6 @@ EXAMPLES
29665
29674
  ${preferredCommand} release postgrest_restart --ref abc123
29666
29675
  ${preferredCommand} release release_canary_fixture_stage_replay --ref abc123 --subject <uuid> --request_id <uuid>
29667
29676
  ${preferredCommand} release release_canary_fixture_disable_replay --ref abc123 --fixture_id <uuid> --disable_request_id <uuid> --issuer <issuer-url> --subject <uuid>
29668
- ${preferredCommand} release scope_rebind --file supacloud/fa/release-scopes/20260907-intake-p1-production.json --environment_id production
29669
- ${preferredCommand} release scope_inspect --file supacloud/fa/release-scopes/20260907-intake-p1-production.json --environment_id production
29670
29677
  ${preferredCommand} queue stats --queue emails
29671
29678
  ${preferredCommand} queue dlq --queue emails --limit 20
29672
29679
  ${preferredCommand} frontend list --ref abc123
@@ -8,6 +8,6 @@ export declare function stringEnum<const Values extends readonly [string, ...str
8
8
  export declare function withDescription<T extends TSchema>(schema: T, description: string): T;
9
9
  export declare function optional<T extends TSchema>(schema: T, description?: string): "~optional" extends keyof T ? T : Type.TOptional<T>;
10
10
  export declare function decodedSchema<TInput extends TSchema, TOutput extends TSchema>(inputSchema: TInput, outputSchema: TOutput, decode: (input: StaticDecode<TInput>) => unknown, options?: TSchemaOptions): Type.TCodec<TInput, import("typebox").StaticType<[], "Decode", {}, {}, TOutput>>;
11
- export declare function schemaEnumValues(schema: TSchema): string[];
11
+ export declare function schemaEnumValues(schema: TSchema | undefined): string[];
12
12
  export declare function schemaDescription(schema: TSchema): string;
13
13
  export declare function schemaProperties(schema: TSchema | ToolSchema): ToolSchema;
@@ -97,6 +97,8 @@ function decodedSchema(inputSchema, outputSchema, decode, options) {
97
97
  return result;
98
98
  }
99
99
  function schemaEnumValues(schema) {
100
+ if (!schema)
101
+ return [];
100
102
  const view = schema;
101
103
  if (Array.isArray(view.enum)) {
102
104
  return view.enum.filter((value) => typeof value === "string" || typeof value === "number").map((value) => String(value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.90.0",
3
+ "version": "0.90.1",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",