@supacloud/cli 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +301 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -238838,7 +238838,8 @@ var ACTION_POLICY = {
238838
238838
  },
238839
238839
  ai: { local: ["show_skill", "install_skill"] },
238840
238840
  app: { local: ["generate", "compile", "check", "graph", "explain"] },
238841
- db: { local: ["lint", "explain"], read: ["module_check"] }
238841
+ db: { local: ["lint", "explain"], read: ["module_check"] },
238842
+ dev: { read: ["status"], write: ["sync", "watch", "migrate"] }
238842
238843
  };
238843
238844
  function declaredMode(moduleName, action) {
238844
238845
  const policy = ACTION_POLICY[moduleName];
@@ -238857,6 +238858,8 @@ function executionMode(moduleName, action, args) {
238857
238858
  return "read";
238858
238859
  if (moduleName === "supabase" && action === "push" && args.dry_run === true)
238859
238860
  return "read";
238861
+ if (moduleName === "dev" && action === "migrate" && args.apply !== true)
238862
+ return "read";
238860
238863
  return declaredMode(moduleName, action);
238861
238864
  }
238862
238865
  function authorizeExecution(moduleName, args, authorization) {
@@ -250808,10 +250811,282 @@ function registerDeployTools(server2, http, options = {}) {
250808
250811
  }
250809
250812
  });
250810
250813
  }
250814
+
250815
+ // src/shared/tools/remote-dev-tools.ts
250816
+ import { spawn as spawn4 } from "node:child_process";
250817
+ import { existsSync as existsSync11 } from "node:fs";
250818
+ import { readFile as readFile3 } from "node:fs/promises";
250819
+ import { readdir } from "node:fs/promises";
250820
+ import { createHash as createHash5 } from "node:crypto";
250821
+ import { join as join10, resolve as resolve11 } from "node:path";
250822
+ var SAFE_TOKEN = /^[A-Za-z0-9._:@/+,-]+$/;
250823
+ var SAFE_REMOTE_ROOT = /^\/(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+\/?$/;
250824
+ var remoteDevToolSchema = {
250825
+ action: withDescription(stringEnum(["sync", "watch", "status", "migrate"]), "Remote development action"),
250826
+ target: optional(stringEnum(["db", "functions", "frontend", "project"]), "Sync target (default: project)"),
250827
+ project_dir: optional(Type.String(), "Local project directory (default: current directory)"),
250828
+ remote_root: optional(Type.String(), "Remote development root"),
250829
+ remote_host: optional(Type.String(), "Remote test server host"),
250830
+ remote_user: optional(Type.String(), "SSH user"),
250831
+ remote_port: optional(Type.Number(), "SSH port"),
250832
+ remote_key: optional(Type.String(), "SSH private key path"),
250833
+ function: optional(Type.String(), "Function slug"),
250834
+ delete: optional(Type.Boolean(), "Delete remote files absent locally"),
250835
+ reload: optional(Type.Boolean(), "Reload the affected target after sync (default: true)"),
250836
+ interval_ms: optional(Type.Number(), "Watch debounce interval in milliseconds (default: 300)"),
250837
+ json: optional(Type.Boolean(), "Emit machine-readable JSON"),
250838
+ apply: optional(Type.Boolean(), "Apply generated migrations to the selected test database"),
250839
+ drizzle_config: optional(Type.String(), "Drizzle config path"),
250840
+ migrations_dir: optional(Type.String(), "Migration directory"),
250841
+ drizzle_bin: optional(Type.String(), "drizzle-kit executable")
250842
+ };
250843
+ function runProcess(executable, args, cwd) {
250844
+ return new Promise((resolveResult, reject) => {
250845
+ const child = spawn4(executable, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
250846
+ let stdout = "";
250847
+ let stderr = "";
250848
+ child.stdout?.setEncoding("utf8");
250849
+ child.stderr?.setEncoding("utf8");
250850
+ child.stdout?.on("data", (chunk) => {
250851
+ stdout += String(chunk);
250852
+ });
250853
+ child.stderr?.on("data", (chunk) => {
250854
+ stderr += String(chunk);
250855
+ });
250856
+ child.once("error", reject);
250857
+ child.once("close", (exitCode) => resolveResult({ exitCode: exitCode ?? 1, stdout, stderr }));
250858
+ });
250859
+ }
250860
+ function resolveDrizzleCommand(root, configured) {
250861
+ if (configured?.trim())
250862
+ return configured.trim();
250863
+ const local = join10(root, "node_modules", ".bin", "drizzle-kit");
250864
+ return existsSync11(local) ? local : "drizzle-kit";
250865
+ }
250866
+ function toolFailed(value) {
250867
+ return value?.isError === true || value?.content?.some((chunk) => typeof chunk?.text === "string" && chunk.text.trimStart().startsWith("❌"));
250868
+ }
250869
+ async function migrateDatabase(args, options) {
250870
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
250871
+ const projectConfig = await readProjectConfig(root);
250872
+ const config = projectConfig.dev || {};
250873
+ const database = config.database || {};
250874
+ const execute = options.execute || ((command, commandArgs, cwd) => runProcess(command, commandArgs, cwd));
250875
+ const drizzleConfig = resolve11(root, String(args.drizzle_config || database.drizzleConfig || "drizzle.config.ts"));
250876
+ const migrationsDir = String(args.migrations_dir || database.migrationsDir || "supabase/migrations");
250877
+ if (!existsSync11(drizzleConfig))
250878
+ throw new Error(`Drizzle config not found: ${drizzleConfig}`);
250879
+ const generated = await execute(resolveDrizzleCommand(root, typeof args.drizzle_bin === "string" ? args.drizzle_bin : database.drizzleBin), ["generate", "--config", drizzleConfig], root);
250880
+ if (generated.exitCode !== 0)
250881
+ throw new Error(`Drizzle migration generation failed: ${generated.stderr.trim() || `exit ${generated.exitCode}`}`);
250882
+ if (!options.runDatabase)
250883
+ throw new Error("dev migrate requires Management API context");
250884
+ const dryRun = await options.runDatabase({ action: "push_migrations", dir: migrationsDir, dry_run: true, strict: database.strict !== false });
250885
+ if (toolFailed(dryRun))
250886
+ throw new Error("SupaCloud migration dry-run failed");
250887
+ if (args.apply !== true)
250888
+ return { ok: true, mode: "dev", action: "migrate", generated: true, applied: false, migrations_dir: migrationsDir, dry_run: dryRun };
250889
+ const applied = await options.runDatabase({ action: "push_migrations", dir: migrationsDir, strict: database.strict !== false });
250890
+ if (toolFailed(applied))
250891
+ throw new Error("SupaCloud migration apply failed");
250892
+ return { ok: true, mode: "dev", action: "migrate", generated: true, applied: true, migrations_dir: migrationsDir, result: applied };
250893
+ }
250894
+ async function readProjectConfig(root) {
250895
+ const file = join10(root, "supacloud.json");
250896
+ if (!existsSync11(file))
250897
+ return {};
250898
+ try {
250899
+ const parsed = JSON.parse(await readFile3(file, "utf8"));
250900
+ return parsed && typeof parsed === "object" ? parsed : {};
250901
+ } catch (error) {
250902
+ throw new Error(`Invalid supacloud.json: ${error instanceof Error ? error.message : String(error)}`);
250903
+ }
250904
+ }
250905
+ async function readDevConfig(root) {
250906
+ const config = await readProjectConfig(root);
250907
+ return config.dev || {};
250908
+ }
250909
+ async function sourceFingerprint(root) {
250910
+ const files = [];
250911
+ const visit = async (directory) => {
250912
+ const entries = await readdir(directory, { withFileTypes: true });
250913
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
250914
+ const path = join10(directory, entry.name);
250915
+ if (entry.isDirectory() && ![".git", "node_modules", "dist", "generated", ".supacloud"].includes(entry.name))
250916
+ await visit(path);
250917
+ else if (entry.isFile())
250918
+ files.push(path);
250919
+ }
250920
+ };
250921
+ await visit(root);
250922
+ const hash2 = createHash5("sha256");
250923
+ for (const file of files) {
250924
+ hash2.update(file.slice(root.length).replace(/\\/g, "/"));
250925
+ hash2.update(await readFile3(file));
250926
+ }
250927
+ return hash2.digest("hex");
250928
+ }
250929
+ function safeToken(value, label) {
250930
+ if (!value || !SAFE_TOKEN.test(value))
250931
+ throw new Error(`Invalid ${label}`);
250932
+ return value;
250933
+ }
250934
+ function remoteRoot(value) {
250935
+ const normalized = value.trim().replace(/\\/g, "/");
250936
+ if (!SAFE_REMOTE_ROOT.test(normalized) || normalized.includes(".."))
250937
+ throw new Error("Invalid remote_root");
250938
+ return normalized.replace(/\/$/, "");
250939
+ }
250940
+ function targetDirectory(root, target, functionSlug, config = {}) {
250941
+ if (target === "db")
250942
+ return join10(root, "supabase", "migrations");
250943
+ if (target === "functions") {
250944
+ const targets = config.targets && typeof config.targets === "object" ? config.targets : {};
250945
+ const match = Object.values(targets).find((entry) => entry?.type === "edge_function" && (!functionSlug || String(entry.slug || "") === functionSlug));
250946
+ const base = match?.root ? resolve11(root, String(match.root)) : join10(root, "supabase", "functions");
250947
+ return match?.root ? base : functionSlug ? join10(base, safeToken(functionSlug, "function")) : base;
250948
+ }
250949
+ if (target === "frontend") {
250950
+ const targets = config.targets && typeof config.targets === "object" ? config.targets : {};
250951
+ const match = Object.values(targets).find((entry) => entry?.type === "frontend");
250952
+ return match?.root ? resolve11(root, String(match.root)) : join10(root, "apps", "web");
250953
+ }
250954
+ return root;
250955
+ }
250956
+ function remoteTargetRoot(root, target, functionSlug) {
250957
+ if (target === "db")
250958
+ return `${root}/database/migrations`;
250959
+ if (target === "functions")
250960
+ return `${root}/functions/${functionSlug ? safeToken(functionSlug, "function") : ""}`.replace(/\/$/, "");
250961
+ if (target === "frontend")
250962
+ return `${root}/frontend`;
250963
+ return `${root}/project`;
250964
+ }
250965
+ function connectionArgs(options, config, args) {
250966
+ const host = String(args.remote_host || config.host || options.host || "").trim();
250967
+ const user = String(args.remote_user || config.user || options.sshUser || "").trim();
250968
+ const port = Number(args.remote_port || config.port || options.sshPort || 22);
250969
+ const key = String(args.remote_key || config.key || options.sshKey || "").trim();
250970
+ if (!host)
250971
+ throw new Error("Remote dev requires SUPACLOUD_HOST or --remote_host");
250972
+ safeToken(host, "remote_host");
250973
+ safeToken(user, "remote_user");
250974
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
250975
+ throw new Error("Invalid remote_port");
250976
+ if (!key || key.includes("\x00") || key.includes(`
250977
+ `))
250978
+ throw new Error("Invalid remote_key");
250979
+ return { host, user, port, key };
250980
+ }
250981
+ function targetConfigPath(target) {
250982
+ return target === "project" ? "project" : target;
250983
+ }
250984
+ function reloadCommand(config, target, projectRef2, functionSlug) {
250985
+ const command = config.reloadCommand?.trim() || "supacloud-dev-agent reload";
250986
+ if (!/^[A-Za-z0-9._/-]+(?: [A-Za-z0-9._:/=-]+)*$/.test(command))
250987
+ throw new Error("Invalid reloadCommand in supacloud.json");
250988
+ return [...command.split(" "), "--project-ref", safeToken(projectRef2 || "test", "project_ref"), "--target", targetConfigPath(target), ...functionSlug ? ["--function", safeToken(functionSlug, "function")] : []];
250989
+ }
250990
+ function sshArgs(connection, remoteCommand) {
250991
+ return ["-p", String(connection.port), "-i", resolve11(connection.key), "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes", `${connection.user}@${connection.host}`, ...remoteCommand];
250992
+ }
250993
+ function rsyncArgs(connection, source, destination, excludes, deleteRemote) {
250994
+ const args = ["-az", "--checksum", "--partial", "--protect-args", "-e", `ssh -p ${connection.port} -i ${resolve11(connection.key)} -o BatchMode=yes -o StrictHostKeyChecking=yes`];
250995
+ if (deleteRemote)
250996
+ args.push("--delete-delay");
250997
+ for (const exclude of excludes) {
250998
+ if (!/^\/?[A-Za-z0-9._*/-]+$/.test(exclude))
250999
+ throw new Error("Invalid dev exclude pattern");
251000
+ args.push("--exclude", exclude);
251001
+ }
251002
+ args.push(`${source.replace(/\/$/, "")}/`, `${connection.user}@${connection.host}:${destination.replace(/\/$/, "")}/`);
251003
+ return args;
251004
+ }
251005
+ async function syncOnce(args, options) {
251006
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
251007
+ const projectConfig = await readProjectConfig(root);
251008
+ const config = projectConfig.dev || {};
251009
+ const target = String(args.target || "project");
251010
+ const source = targetDirectory(root, target, typeof args.function === "string" ? args.function : undefined, projectConfig);
251011
+ if (!existsSync11(source))
251012
+ throw new Error(`Dev source directory not found: ${source}`);
251013
+ let compiled = false;
251014
+ if (config.compile === true && target !== "db") {
251015
+ const compileRoot = resolve11(root, config.compileRoot || ".");
251016
+ const compileOutDir = resolve11(root, config.compileOutDir || "generated");
251017
+ const compilation = await compileProject({
251018
+ rootDir: compileRoot,
251019
+ outDir: compileOutDir,
251020
+ strict: config.compileStrict !== false
251021
+ });
251022
+ const errors = compilation.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
251023
+ if (errors.length > 0) {
251024
+ throw new Error(`DI compile failed: ${errors.map((diagnostic) => `${diagnostic.code} ${diagnostic.message}`).join("; ")}`);
251025
+ }
251026
+ compiled = true;
251027
+ }
251028
+ const remoteBase = remoteRoot(String(args.remote_root || config.remoteRoot || `/var/lib/supacloud/dev/${options.projectRef || "project"}`));
251029
+ const slug = typeof args.function === "string" ? args.function : undefined;
251030
+ const destination = remoteTargetRoot(remoteBase, target, slug);
251031
+ const connection = connectionArgs(options, config, args);
251032
+ const execute = options.execute || ((command, commandArgs, cwd) => runProcess(command, commandArgs, cwd));
251033
+ const mkdir3 = await execute("ssh", sshArgs(connection, ["mkdir", "-p", destination]), root);
251034
+ if (mkdir3.exitCode !== 0)
251035
+ throw new Error(`Remote dev prepare failed: ${mkdir3.stderr.trim() || `exit ${mkdir3.exitCode}`}`);
251036
+ const excludes = Array.isArray(config.excludes) ? config.excludes : ["node_modules", ".git", ".env*", "dist", ".supacloud"];
251037
+ const sync = await execute("rsync", rsyncArgs(connection, source, destination, excludes, args.delete === true), root);
251038
+ if (sync.exitCode !== 0)
251039
+ throw new Error(`Remote dev sync failed: ${sync.stderr.trim() || `exit ${sync.exitCode}`}`);
251040
+ const shouldReload = args.reload !== false;
251041
+ let reload = null;
251042
+ if (shouldReload) {
251043
+ reload = await execute("ssh", sshArgs(connection, reloadCommand(config, target, options.projectRef, slug)), root);
251044
+ if (reload.exitCode !== 0)
251045
+ throw new Error(`Remote dev reload failed: ${reload.stderr.trim() || `exit ${reload.exitCode}`}`);
251046
+ }
251047
+ return { ok: true, mode: "dev", action: "sync", environment: options.environment || null, target, source, destination, host: connection.host, compiled, reloaded: shouldReload };
251048
+ }
251049
+ function registerRemoteDevTools(server2, options = {}) {
251050
+ server2.tool("dev", "Remote test-server development sync. It never targets production and never syncs secrets.", remoteDevToolSchema, async (args) => {
251051
+ if (["production", "prod"].includes((options.environment || "").toLowerCase()))
251052
+ throw new Error("Remote dev mode is forbidden for production environments");
251053
+ if (args.action === "status") {
251054
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
251055
+ const config = await readDevConfig(root);
251056
+ const connection = connectionArgs(options, config, args);
251057
+ const execute = options.execute || ((command, commandArgs, cwd) => runProcess(command, commandArgs, cwd));
251058
+ const status = await execute("ssh", sshArgs(connection, ["supacloud-dev-agent", "status", "--project-ref", safeToken(options.projectRef || "test", "project_ref")]), root);
251059
+ return { content: [{ type: "text", text: JSON.stringify({ ok: status.exitCode === 0, mode: "dev", action: "status", host: connection.host, output: status.stdout.trim(), error: status.stderr.trim() }, null, 2) }], isError: status.exitCode !== 0 };
251060
+ }
251061
+ if (args.action === "sync") {
251062
+ return { content: [{ type: "text", text: JSON.stringify(await syncOnce(args, options), null, 2) }] };
251063
+ }
251064
+ if (args.action === "migrate") {
251065
+ return { content: [{ type: "text", text: JSON.stringify(await migrateDatabase(args, options), null, 2) }] };
251066
+ }
251067
+ if (args.action === "watch") {
251068
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
251069
+ const interval = Math.max(100, Math.min(1e4, Number(args.interval_ms || 300)));
251070
+ let fingerprint = "";
251071
+ let lastSync = null;
251072
+ for (;; ) {
251073
+ const nextFingerprint = await sourceFingerprint(root);
251074
+ if (nextFingerprint !== fingerprint) {
251075
+ lastSync = await syncOnce(args, options);
251076
+ fingerprint = nextFingerprint;
251077
+ process.stdout.write(`${JSON.stringify({ ...lastSync, watching: true })}
251078
+ `);
251079
+ }
251080
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, interval));
251081
+ }
251082
+ }
251083
+ throw new Error("Unsupported remote dev action");
251084
+ });
251085
+ }
250811
251086
  // package.json
250812
251087
  var package_default = {
250813
251088
  name: "@supacloud/cli",
250814
- version: "0.41.0",
251089
+ version: "0.42.0",
250815
251090
  description: "Project-scoped CLI for SupaCloud users",
250816
251091
  type: "module",
250817
251092
  main: "./dist/index.js",
@@ -251062,6 +251337,11 @@ EXAMPLES
251062
251337
  ${preferredCommand} deploy
251063
251338
  ${preferredCommand} deploy --target web
251064
251339
  ${preferredCommand} deploy --target api
251340
+ ${preferredCommand} dev sync --env test --target functions --function api
251341
+ ${preferredCommand} dev status --env test
251342
+ ${preferredCommand} dev watch --env test --target project
251343
+ ${preferredCommand} dev sync --env test --target db
251344
+ ${preferredCommand} dev migrate --env test
251065
251345
  ${preferredCommand} project get
251066
251346
  ${preferredCommand} project logs --log_type database
251067
251347
  ${preferredCommand} project task_stats
@@ -251228,6 +251508,15 @@ function createCliTools(context, confirmProduction) {
251228
251508
  Object.assign(tools, captureTools((server2) => registerDatabaseTools(server2, undefined, {
251229
251509
  localOnly: true
251230
251510
  })));
251511
+ Object.assign(tools, captureTools((server2) => registerRemoteDevTools(server2, {
251512
+ cwd: process.cwd(),
251513
+ host: process.env.SUPACLOUD_DEV_HOST || context.host,
251514
+ sshUser: context.sshUser,
251515
+ sshPort: context.sshPort,
251516
+ sshKey: context.sshKey,
251517
+ projectRef: context.projectRef || undefined,
251518
+ environment: context.environment
251519
+ })));
251231
251520
  tools.setup_help = {
251232
251521
  schema: {},
251233
251522
  callback: async () => ({
@@ -251280,6 +251569,16 @@ function createCliTools(context, confirmProduction) {
251280
251569
  }));
251281
251570
  pushMigrations = databaseTools.database?.callback;
251282
251571
  assign(databaseTools);
251572
+ assign(captureTools((server2) => registerRemoteDevTools(server2, {
251573
+ cwd: process.cwd(),
251574
+ host: process.env.SUPACLOUD_DEV_HOST || context.host,
251575
+ sshUser: context.sshUser,
251576
+ sshPort: context.sshPort,
251577
+ sshKey: context.sshKey,
251578
+ projectRef: context.projectRef || undefined,
251579
+ environment: context.environment,
251580
+ runDatabase: databaseTools.database?.callback
251581
+ })));
251283
251582
  assign(captureTools((server2) => registerAuthTools(server2, http)));
251284
251583
  assign(captureTools((server2) => registerOAuthClientTools(server2, http)));
251285
251584
  assign(captureTools((server2) => registerStorageTools(server2, http)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",