supbuddy 3.3.2 → 3.3.3

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/dist/bin.js CHANGED
@@ -19812,7 +19812,7 @@ function cliBuildKind(env3 = process.env) {
19812
19812
  return raw === "host" || raw === "npm" ? raw : "dev";
19813
19813
  }
19814
19814
  function cliVersion(env3 = process.env) {
19815
- return "3.3.2".trim() || "0.0.0-dev";
19815
+ return "3.3.3".trim() || "0.0.0-dev";
19816
19816
  }
19817
19817
  function isDevBuild(env3) {
19818
19818
  return cliBuildKind(env3) === "dev";
@@ -41733,15 +41733,24 @@ loopback IP \u2014 target the app's **canonical** dev port (Next.js \`:3000\`, V
41733
41733
  share \`127.0.0.1\` and need unique ports.
41734
41734
  `;
41735
41735
  }
41736
+ function supabaseServicePort(project, type2, managedKey, stock) {
41737
+ const svc = (project.services ?? []).find((s) => s.type === type2);
41738
+ const snapshot = svc?.port ?? svc?.ports?.[0];
41739
+ if (typeof snapshot === "number") return snapshot;
41740
+ const managed = project.supabaseManaged?.ports?.[managedKey];
41741
+ return typeof managed === "number" ? managed : stock;
41742
+ }
41736
41743
  function renderServicesMd(ctx) {
41737
41744
  const placeholder = ctx.write_secrets ? "<set; populated by Supbuddy>" : "<see Supbuddy \u2192 Configure, or use MCP tools>";
41745
+ const apiPort = supabaseServicePort(ctx.project, "supabase-kong", "api.port", 54321);
41746
+ const studioPort = supabaseServicePort(ctx.project, "supabase-studio", "studio.port", 54323);
41738
41747
  return HEADER(".supbuddy/services.md") + `# Services
41739
41748
 
41740
41749
  ## Supabase
41741
41750
 
41742
41751
  - **Status:** ${ctx.supabase_running ? "running" : "stopped"}
41743
- - **API URL:** ${ctx.supabase_running ? "http://127.0.0.1:54321" : "<not running>"}
41744
- - **Studio:** ${ctx.supabase_running ? "http://127.0.0.1:54323" : "<not running>"}
41752
+ - **API URL:** ${ctx.supabase_running ? `http://127.0.0.1:${apiPort}` : "<not running>"}
41753
+ - **Studio:** ${ctx.supabase_running ? `http://127.0.0.1:${studioPort}` : "<not running>"}
41745
41754
  - **Anon key:** ${placeholder}
41746
41755
  - **Service role key:** ${placeholder}
41747
41756
 
@@ -42311,10 +42320,17 @@ function buildContext(projectId, generatedAt) {
42311
42320
  project,
42312
42321
  mappings,
42313
42322
  proxyState: s.proxyState,
42314
- // Supabase status isn't yet exposed on the store keep the honest "stopped"
42315
- // placeholder. Compose status IS live: the scanner writes per-service state
42316
- // into `project.services`, so reflect any running compose service here.
42317
- supabase_running: false,
42323
+ // Both statuses come off the SAME per-service snapshot the scanner writes
42324
+ // into `project.services`. Supabase used to be hardcoded `false` here so
42325
+ // every `.supbuddy/services.md` in every registered repo said "Supabase:
42326
+ // stopped" whatever the truth, and `--force` re-rendered the same lie. An
42327
+ // agent reading that file on a project with the stack up concluded the DB
42328
+ // was down and acted on it. `startsWith('supabase')` (not an exact type)
42329
+ // because the snapshot carries one entry per container — supabase-kong,
42330
+ // -db, -studio, … — and matches get_supabase_status / the CLI / the TUI.
42331
+ supabase_running: (project.services ?? []).some(
42332
+ (sv) => typeof sv.type === "string" && sv.type.startsWith("supabase") && sv.status === "running"
42333
+ ),
42318
42334
  compose_running: (project.services ?? []).some((sv) => sv.type === "compose" && sv.status === "running"),
42319
42335
  mcp_endpoint: `http://127.0.0.1:${s.mcpServerState.bound_port ?? s.settings.mcp.port}/mcp`,
42320
42336
  mcp_running: !!s.mcpServerState.running,
@@ -48467,7 +48467,7 @@ headers = { Authorization = "Bearer ${token}" }
48467
48467
  `;
48468
48468
  let updated;
48469
48469
  if (existing.includes("[mcp_servers.supbuddy]")) {
48470
- updated = existing.replace(/\[mcp_servers\.supbuddy\][\s\S]*?(?=\n\[|$)/, block.trim());
48470
+ updated = existing.replace(/\[mcp_servers\.supbuddy\][\s\S]*?(?=\n\[|$)/, () => block.trim());
48471
48471
  } else {
48472
48472
  updated = (existing.endsWith("\n") ? existing : existing + "\n") + block;
48473
48473
  }
@@ -48749,7 +48749,7 @@ const systemTools = {
48749
48749
  return { ok: true, granted: missing, scopes: nextScopes };
48750
48750
  }
48751
48751
  };
48752
- function requireProjectPath(projectId) {
48752
+ function requireProjectPath$1(projectId) {
48753
48753
  const project = useStore.getState().getProject(projectId);
48754
48754
  if (!project?.path) {
48755
48755
  throw new McpError("invalid_args", `Project ${projectId} not found or has no path`);
@@ -48759,7 +48759,7 @@ function requireProjectPath(projectId) {
48759
48759
  const addonTools = {
48760
48760
  // Create a starter supbuddy.addons.yml (create-if-absent; never clobbers).
48761
48761
  scaffold_addons: async (a) => {
48762
- const projectPath = requireProjectPath(a.project_id);
48762
+ const projectPath = requireProjectPath$1(a.project_id);
48763
48763
  const { scaffoldAddonsFile: scaffoldAddonsFile2 } = await __vitePreload(async () => {
48764
48764
  const { scaffoldAddonsFile: scaffoldAddonsFile3 } = await Promise.resolve().then(() => addonManager);
48765
48765
  return { scaffoldAddonsFile: scaffoldAddonsFile3 };
@@ -48769,7 +48769,7 @@ const addonTools = {
48769
48769
  },
48770
48770
  // Run the declared one-shot add-on seed step (idempotent unless force=true).
48771
48771
  seed_addons: async (a) => {
48772
- const projectPath = requireProjectPath(a.project_id);
48772
+ const projectPath = requireProjectPath$1(a.project_id);
48773
48773
  const { runSeedIfNeeded: runSeedIfNeeded2 } = await __vitePreload(async () => {
48774
48774
  const { runSeedIfNeeded: runSeedIfNeeded3 } = await Promise.resolve().then(() => addonManager);
48775
48775
  return { runSeedIfNeeded: runSeedIfNeeded3 };
@@ -48964,14 +48964,26 @@ const supabaseExtrasTools = {
48964
48964
  // Mirror supabase:analytics:set (worker/index.ts:2506)
48965
48965
  // Does NOT trigger auto-restart (the IPC handler's autoRestart flag).
48966
48966
  // Callers that want a restart should follow up with restart_supabase.
48967
+ // Enabling analytics can CREATE the `[analytics]` section, and a created section
48968
+ // carries the CLI's stock port — so a thin project's block is re-imposed over it
48969
+ // and reported here through the same shaping the other config-replacing tools use.
48970
+ // `thinBlock` is not spread into the result: it carries the whole config.toml.
48967
48971
  set_supabase_analytics: async (args) => {
48968
48972
  requireProject$1(args.project_id);
48969
48973
  const { setAnalyticsEnabled: setAnalyticsEnabled2 } = await __vitePreload(async () => {
48970
48974
  const { setAnalyticsEnabled: setAnalyticsEnabled3 } = await Promise.resolve().then(() => supabaseHealth);
48971
48975
  return { setAnalyticsEnabled: setAnalyticsEnabled3 };
48972
48976
  }, false ? __VITE_PRELOAD__ : void 0);
48973
- const result = await setAnalyticsEnabled2(args.project_id, args.enabled);
48974
- return { ok: true, project_id: args.project_id, ...result };
48977
+ const { thinBlock, thinBlockError, ...result } = await setAnalyticsEnabled2(
48978
+ args.project_id,
48979
+ args.enabled
48980
+ );
48981
+ return {
48982
+ ok: true,
48983
+ project_id: args.project_id,
48984
+ ...result,
48985
+ ...shapeThinBlock("set_supabase_analytics", thinBlock ?? null, thinBlockError ?? null)
48986
+ };
48975
48987
  },
48976
48988
  // Mirror bundle:validate (worker/index.ts:3145)
48977
48989
  validate_bundle: async (args) => {
@@ -49472,11 +49484,11 @@ async function ensureHostedSnapshot(projectPath) {
49472
49484
  function rewriteSiteUrl(raw, newValue) {
49473
49485
  const re = /^(\s*site_url\s*=\s*)"[^"]*"/m;
49474
49486
  if (re.test(raw)) {
49475
- return raw.replace(re, `$1"${newValue}"`);
49487
+ return raw.replace(re, (_m, lead) => `${lead}"${newValue}"`);
49476
49488
  }
49477
49489
  const authRe = /^\[auth\]\s*$/m;
49478
49490
  if (authRe.test(raw)) {
49479
- return raw.replace(authRe, `[auth]
49491
+ return raw.replace(authRe, () => `[auth]
49480
49492
  site_url = "${newValue}"`);
49481
49493
  }
49482
49494
  const sep = raw.endsWith("\n") ? "" : "\n";
@@ -49486,20 +49498,22 @@ site_url = "${newValue}"
49486
49498
  `;
49487
49499
  }
49488
49500
  function rewriteStudioApiUrl(raw, newValue) {
49489
- const sectionRe = /^\[studio\][\s\S]*?(?=^\[|\Z)/m;
49501
+ const sectionRe = /^\[studio\][\s\S]*?(?=^\[|$(?![\s\S]))/m;
49490
49502
  const sectionMatch = raw.match(sectionRe);
49491
49503
  if (sectionMatch) {
49492
49504
  const section = sectionMatch[0];
49505
+ const start = sectionMatch.index ?? 0;
49506
+ const end = start + section.length;
49493
49507
  const re = /^(\s*api_url\s*=\s*)"[^"]*"/m;
49494
49508
  if (re.test(section)) {
49495
- const updatedSection = section.replace(re, `$1"${newValue}"`);
49496
- return raw.replace(section, updatedSection);
49509
+ const updatedSection = section.replace(re, (_m, lead) => `${lead}"${newValue}"`);
49510
+ return raw.slice(0, start) + updatedSection + raw.slice(end);
49497
49511
  }
49498
49512
  const headerRe = /^\[studio\]\s*$/m;
49499
49513
  if (headerRe.test(section)) {
49500
- const updatedSection = section.replace(headerRe, `[studio]
49514
+ const updatedSection = section.replace(headerRe, () => `[studio]
49501
49515
  api_url = "${newValue}"`);
49502
- return raw.replace(section, updatedSection);
49516
+ return raw.slice(0, start) + updatedSection + raw.slice(end);
49503
49517
  }
49504
49518
  }
49505
49519
  const sep = raw.endsWith("\n") ? "" : "\n";
@@ -49512,16 +49526,16 @@ function rewriteAdditionalRedirectUrls(raw, values) {
49512
49526
  const body = values.map((v) => `"${v}"`).join(", ");
49513
49527
  const re = /^(\s*additional_redirect_urls\s*=\s*)\[[^\]]*\]/m;
49514
49528
  if (re.test(raw)) {
49515
- return raw.replace(re, `$1[${body}]`);
49529
+ return raw.replace(re, (_m, lead) => `${lead}[${body}]`);
49516
49530
  }
49517
49531
  const authWithSite = /^(\[auth\][\s\S]*?^(\s*site_url\s*=\s*"[^"]*")\s*$)/m;
49518
49532
  if (authWithSite.test(raw)) {
49519
- return raw.replace(authWithSite, `$1
49533
+ return raw.replace(authWithSite, (_m, kept) => `${kept}
49520
49534
  additional_redirect_urls = [${body}]`);
49521
49535
  }
49522
49536
  const authRe = /^\[auth\]\s*$/m;
49523
49537
  if (authRe.test(raw)) {
49524
- return raw.replace(authRe, `[auth]
49538
+ return raw.replace(authRe, () => `[auth]
49525
49539
  additional_redirect_urls = [${body}]`);
49526
49540
  }
49527
49541
  const sep = raw.endsWith("\n") ? "" : "\n";
@@ -50814,15 +50828,24 @@ loopback IP — target the app's **canonical** dev port (Next.js \`:3000\`, Vite
50814
50828
  share \`127.0.0.1\` and need unique ports.
50815
50829
  `;
50816
50830
  }
50831
+ function supabaseServicePort(project, type2, managedKey, stock) {
50832
+ const svc = (project.services ?? []).find((s) => s.type === type2);
50833
+ const snapshot2 = svc?.port ?? svc?.ports?.[0];
50834
+ if (typeof snapshot2 === "number") return snapshot2;
50835
+ const managed = project.supabaseManaged?.ports?.[managedKey];
50836
+ return typeof managed === "number" ? managed : stock;
50837
+ }
50817
50838
  function renderServicesMd(ctx) {
50818
50839
  const placeholder = ctx.write_secrets ? "<set; populated by Supbuddy>" : "<see Supbuddy → Configure, or use MCP tools>";
50840
+ const apiPort = supabaseServicePort(ctx.project, "supabase-kong", "api.port", 54321);
50841
+ const studioPort = supabaseServicePort(ctx.project, "supabase-studio", "studio.port", 54323);
50819
50842
  return HEADER(".supbuddy/services.md") + `# Services
50820
50843
 
50821
50844
  ## Supabase
50822
50845
 
50823
50846
  - **Status:** ${ctx.supabase_running ? "running" : "stopped"}
50824
- - **API URL:** ${ctx.supabase_running ? "http://127.0.0.1:54321" : "<not running>"}
50825
- - **Studio:** ${ctx.supabase_running ? "http://127.0.0.1:54323" : "<not running>"}
50847
+ - **API URL:** ${ctx.supabase_running ? `http://127.0.0.1:${apiPort}` : "<not running>"}
50848
+ - **Studio:** ${ctx.supabase_running ? `http://127.0.0.1:${studioPort}` : "<not running>"}
50826
50849
  - **Anon key:** ${placeholder}
50827
50850
  - **Service role key:** ${placeholder}
50828
50851
 
@@ -51397,10 +51420,17 @@ function buildContext(projectId, generatedAt) {
51397
51420
  project,
51398
51421
  mappings,
51399
51422
  proxyState: s.proxyState,
51400
- // Supabase status isn't yet exposed on the store keep the honest "stopped"
51401
- // placeholder. Compose status IS live: the scanner writes per-service state
51402
- // into `project.services`, so reflect any running compose service here.
51403
- supabase_running: false,
51423
+ // Both statuses come off the SAME per-service snapshot the scanner writes
51424
+ // into `project.services`. Supabase used to be hardcoded `false` here so
51425
+ // every `.supbuddy/services.md` in every registered repo said "Supabase:
51426
+ // stopped" whatever the truth, and `--force` re-rendered the same lie. An
51427
+ // agent reading that file on a project with the stack up concluded the DB
51428
+ // was down and acted on it. `startsWith('supabase')` (not an exact type)
51429
+ // because the snapshot carries one entry per container — supabase-kong,
51430
+ // -db, -studio, … — and matches get_supabase_status / the CLI / the TUI.
51431
+ supabase_running: (project.services ?? []).some(
51432
+ (sv) => typeof sv.type === "string" && sv.type.startsWith("supabase") && sv.status === "running"
51433
+ ),
51404
51434
  compose_running: (project.services ?? []).some((sv) => sv.type === "compose" && sv.status === "running"),
51405
51435
  mcp_endpoint: `http://127.0.0.1:${s.mcpServerState.bound_port ?? s.settings.mcp.port}/mcp`,
51406
51436
  mcp_running: !!s.mcpServerState.running,
@@ -51949,7 +51979,22 @@ function parseEnvText(text) {
51949
51979
  }
51950
51980
  return out;
51951
51981
  }
51982
+ function requireProjectPath(project) {
51983
+ const p = project.path;
51984
+ if (typeof p !== "string" || p.trim() === "") {
51985
+ const label = project.name ? `"${project.name}" (${project.id})` : project.id;
51986
+ throw new Error(
51987
+ `Project ${label} has no recorded directory, so its env cannot be read. Refusing rather than returning an empty preview, which would read as "this project has no secrets". Re-add the project so its directory is recorded, then preview again.`
51988
+ );
51989
+ }
51990
+ return p;
51991
+ }
51952
51992
  async function previewProjectEnv(projectPath, files = [".env", ".env.local", ".env.development"], inBoxEquivalent) {
51993
+ if (typeof projectPath !== "string" || projectPath.trim() === "") {
51994
+ throw new Error(
51995
+ `previewProjectEnv needs the project's directory, got ${JSON.stringify(projectPath)}. Callers holding a Project should go through requireProjectPath(), which names the project.`
51996
+ );
51997
+ }
51953
51998
  const out = [];
51954
51999
  const seen2 = /* @__PURE__ */ new Set();
51955
52000
  for (const file of files) {
@@ -51988,7 +52033,13 @@ const cloudTools = {
51988
52033
  preview_cloud_env: async (a) => {
51989
52034
  const p = useStore.getState().getProject(a.project_id);
51990
52035
  if (!p) throw new McpError("plan_not_found", `Project ${a.project_id} not found`);
51991
- const entries = await previewProjectEnv(p.path, a.files);
52036
+ let projectPath;
52037
+ try {
52038
+ projectPath = requireProjectPath(p);
52039
+ } catch (e) {
52040
+ throw new McpError("invalid_args", e.message);
52041
+ }
52042
+ const entries = await previewProjectEnv(projectPath, a.files);
51992
52043
  const counts = entries.reduce((acc, e) => {
51993
52044
  acc[e.verdict] = (acc[e.verdict] ?? 0) + 1;
51994
52045
  return acc;
@@ -53126,6 +53177,7 @@ function filenameToImage(file) {
53126
53177
  return file.replace(/\.tar$/, "").replace(/_/g, "/");
53127
53178
  }
53128
53179
  const execFileAsync$2 = util$1.promisify(child_process.execFile);
53180
+ const STOCK_ANALYTICS_PORT = SUPABASE_PORT_KEYS.find((k) => k.key === "analytics.port")?.stock ?? 54327;
53129
53181
  function assertNotUnprovisionedVm(project, op) {
53130
53182
  if (project?.isolation === "vm") {
53131
53183
  throw new Error(
@@ -53179,6 +53231,7 @@ enabled = ${value}`);
53179
53231
  return raw + `${sep}
53180
53232
  [analytics]
53181
53233
  enabled = ${value}
53234
+ port = ${STOCK_ANALYTICS_PORT}
53182
53235
  `;
53183
53236
  }
53184
53237
  async function setAnalyticsEnabled(projectId, value) {
@@ -53188,9 +53241,17 @@ async function setAnalyticsEnabled(projectId, value) {
53188
53241
  const configPath = path.join(project.path, "supabase", "config.toml");
53189
53242
  const raw = await fs.readFile(configPath, "utf-8");
53190
53243
  const next = rewriteAnalyticsEnabled(raw, value);
53191
- if (next === raw) return { configPath, bytes: Buffer.byteLength(raw) };
53244
+ if (next === raw) {
53245
+ return { configPath, bytes: Buffer.byteLength(raw), thinBlock: null, thinBlockError: null };
53246
+ }
53192
53247
  await fs.writeFile(configPath, next, "utf-8");
53193
- return { configPath, bytes: Buffer.byteLength(next) };
53248
+ const { reapplyThinBlockAfterConfigReplace: reapplyThinBlockAfterConfigReplace2 } = await __vitePreload(async () => {
53249
+ const { reapplyThinBlockAfterConfigReplace: reapplyThinBlockAfterConfigReplace3 } = await Promise.resolve().then(() => supabaseManager);
53250
+ return { reapplyThinBlockAfterConfigReplace: reapplyThinBlockAfterConfigReplace3 };
53251
+ }, false ? __VITE_PRELOAD__ : void 0);
53252
+ const thin = await reapplyThinBlockAfterConfigReplace2(project.path, "analytics toggle");
53253
+ const bytes = await fs.stat(configPath).then((s) => s.size).catch(() => Buffer.byteLength(next));
53254
+ return { configPath, bytes, thinBlock: thin.block, thinBlockError: thin.error };
53194
53255
  }
53195
53256
  function recordSupabaseFailure(projectId, operation, error) {
53196
53257
  const store2 = useStore.getState();
@@ -56417,10 +56478,10 @@ process.on("message", async (message) => {
56417
56478
  case "supabase:analytics:set": {
56418
56479
  try {
56419
56480
  const { projectId, enabled, autoRestart } = message.data;
56420
- const result = await setAnalyticsEnabled(projectId, !!enabled);
56481
+ const { configPath, bytes } = await setAnalyticsEnabled(projectId, !!enabled);
56421
56482
  process.send({
56422
56483
  type: "supabase:analytics:set:response",
56423
- data: { ...result, restartScheduled: autoRestart !== false }
56484
+ data: { configPath, bytes, restartScheduled: autoRestart !== false }
56424
56485
  });
56425
56486
  if (autoRestart !== false) {
56426
56487
  restartSupabaseForProject(projectId, io).catch((err) => {
@@ -57112,7 +57173,7 @@ process.on("message", async (message) => {
57112
57173
  if (typeof projectId !== "string" || !projectId) throw new Error("cloud:preview-env requires a projectId");
57113
57174
  const project = useStore.getState().getProject(projectId);
57114
57175
  if (!project) throw new Error(`Project ${projectId} not found`);
57115
- const entries = await previewProjectEnv(project.path);
57176
+ const entries = await previewProjectEnv(requireProjectPath(project));
57116
57177
  process.send({
57117
57178
  type: "cloud:preview-env:response",
57118
57179
  _rid: message._rid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supbuddy",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "Run multiple Supabase projects at once on custom local domains with HTTPS. A headless CLI and daemon (proxy, DNS, Supabase/Compose lifecycle, MCP) for macOS and Linux.",
5
5
  "keywords": [
6
6
  "supabase",