@iann29/synapse 1.8.1 → 1.8.2

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.
@@ -195,7 +195,11 @@ show up in the menu).`,
195
195
  }),
196
196
  );
197
197
  const creds = await api.cliCredentials(dev.name);
198
- const envPath = writeProjectEnv(ctx.cwd, creds);
198
+ const envPath = writeProjectEnv(ctx.cwd, creds, {
199
+ team: { slug: team.slug, name: team.name },
200
+ project: { slug: project.slug, name: project.name },
201
+ target: "dev",
202
+ });
199
203
 
200
204
  ctx.out.result(
201
205
  {
@@ -200,6 +200,44 @@ const checkEnvLocalHasVars = {
200
200
  }),
201
201
  };
202
202
 
203
+ // v1.8.2: separate check for the Cloud-compatible NEXT_PUBLIC_* vars.
204
+ // Self-hosted auth (above) is REQUIRED. These public vars are
205
+ // convenience for code that imports CONVEX_URL the way Cloud
206
+ // tutorials do — missing them only warns, never blocks (the CLI
207
+ // still works without them).
208
+ const checkEnvLocalHasPublicVars = {
209
+ id: "env-local-has-public-convex-vars",
210
+ category: "project",
211
+ title: "NEXT_PUBLIC_CONVEX_URL + NEXT_PUBLIC_CONVEX_SITE_URL in .env.local",
212
+ autoFix: "never",
213
+ dependsOn: ["env-local-present"],
214
+ run: safeRun(async (ctx) => {
215
+ if (!ctx.projectConfig) {
216
+ return { status: "skipped", summary: "no linked project", data: {} };
217
+ }
218
+ const env = readProjectEnv(ctx.cwd);
219
+ const hasUrl = !!env.NEXT_PUBLIC_CONVEX_URL;
220
+ const hasSite = !!env.NEXT_PUBLIC_CONVEX_SITE_URL;
221
+ if (hasUrl && hasSite) {
222
+ return {
223
+ status: "ok",
224
+ summary: "both public vars present",
225
+ data: { hasUrl, hasSite },
226
+ };
227
+ }
228
+ const missing = [];
229
+ if (!hasUrl) missing.push("NEXT_PUBLIC_CONVEX_URL");
230
+ if (!hasSite) missing.push("NEXT_PUBLIC_CONVEX_SITE_URL");
231
+ return {
232
+ status: "warn",
233
+ summary: `missing: ${missing.join(", ")} (Cloud-style vars; CLI still works)`,
234
+ remediation:
235
+ "Run `synapse select` to regenerate .env.local with Cloud-compatible vars (CLI v1.8.2+).",
236
+ data: { hasUrl, hasSite, missing },
237
+ };
238
+ }),
239
+ };
240
+
203
241
  const checkGitignoreProtectsEnv = {
204
242
  id: "gitignore-protects-env",
205
243
  category: "project",
@@ -609,6 +647,7 @@ const ALL_CHECKS = [
609
647
  checkAuthTokenValid,
610
648
  checkEnvLocalPresent,
611
649
  checkEnvLocalHasVars,
650
+ checkEnvLocalHasPublicVars,
612
651
  checkProjectStillExists,
613
652
 
614
653
  // Tier C (per deployment)
package/lib/env-file.js CHANGED
@@ -1,9 +1,48 @@
1
+ // Writes / reads `.env.local` for Synapse-linked projects.
2
+ //
3
+ // As of v1.8.2 the file is drop-in compatible with Convex Cloud
4
+ // tutorials:
5
+ //
6
+ // # Convex (Synapse self-hosted — drop-in compatible with Cloud tutorials)
7
+ // NEXT_PUBLIC_CONVEX_URL="https://<name>.app.synapsepanel.com"
8
+ // NEXT_PUBLIC_CONVEX_SITE_URL="https://<name>.app.synapsepanel.com"
9
+ // CONVEX_DEPLOYMENT=dev:<name> # team: <team>, project: <project>
10
+ //
11
+ // # Self-hosted auth (Synapse cannot use Cloud account session)
12
+ // CONVEX_SELF_HOSTED_URL="https://<name>.app.synapsepanel.com"
13
+ // CONVEX_SELF_HOSTED_ADMIN_KEY="<name>|..."
14
+ //
15
+ // In Cloud, NEXT_PUBLIC_CONVEX_URL points at `.convex.cloud` and
16
+ // NEXT_PUBLIC_CONVEX_SITE_URL at `.convex.site` — two different
17
+ // origins. In self-hosted both point at the same URL; the backend
18
+ // container routes API calls and HTTP actions on the same host.
19
+ //
20
+ // CONVEX_DEPLOYMENT is kept uncommented for cosmetic familiarity —
21
+ // the synapse wrapper around `npx convex` (`runConvex` in
22
+ // lib/convex.js) deletes it from the child env when self-hosted
23
+ // vars are present, so it never accidentally triggers Cloud auth.
24
+
1
25
  const fs = require("node:fs");
2
26
  const path = require("node:path");
3
27
 
4
28
  const SELF_HOSTED_URL = "CONVEX_SELF_HOSTED_URL";
5
29
  const SELF_HOSTED_ADMIN_KEY = "CONVEX_SELF_HOSTED_ADMIN_KEY";
6
30
  const CONVEX_DEPLOYMENT = "CONVEX_DEPLOYMENT";
31
+ const NEXT_PUBLIC_CONVEX_URL = "NEXT_PUBLIC_CONVEX_URL";
32
+ const NEXT_PUBLIC_CONVEX_SITE_URL = "NEXT_PUBLIC_CONVEX_SITE_URL";
33
+
34
+ const PUBLIC_HEADER = "# Convex (Synapse self-hosted — drop-in compatible with Cloud tutorials)";
35
+ const AUTH_HEADER = "# Self-hosted auth (Synapse cannot use Cloud account session)";
36
+
37
+ // Keys whose VALUE we own. The CONVEX_DEPLOYMENT line is special-cased
38
+ // (it's a bare assignment with an inline comment, not a quoted value),
39
+ // so it's not in this map.
40
+ const MANAGED_VALUE_KEYS = [
41
+ NEXT_PUBLIC_CONVEX_URL,
42
+ NEXT_PUBLIC_CONVEX_SITE_URL,
43
+ SELF_HOSTED_URL,
44
+ SELF_HOSTED_ADMIN_KEY,
45
+ ];
7
46
 
8
47
  function quoteEnvValue(value) {
9
48
  return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
@@ -18,11 +57,24 @@ function keyFromLine(line) {
18
57
  return match ? match[1] : null;
19
58
  }
20
59
 
21
- function commentDeploymentLine(line) {
22
- if (/^\s*#/.test(line)) {
23
- return line;
24
- }
25
- return `# ${line} # disabled by synapse CLI for self-hosted Convex`;
60
+ // Detects a CONVEX_DEPLOYMENT line whether it's:
61
+ // - Bare: `CONVEX_DEPLOYMENT=dev:x`
62
+ // - Exported: `export CONVEX_DEPLOYMENT=dev:x`
63
+ // - Commented: `# CONVEX_DEPLOYMENT=... # disabled by synapse CLI...` (v1.8.1-)
64
+ // Returns true for any of these so `synapse select` can authoritatively
65
+ // replace a previously-commented form with the new uncommented line.
66
+ function isDeploymentLine(line) {
67
+ if (keyFromLine(line) === CONVEX_DEPLOYMENT) return true;
68
+ return /^\s*#\s*(?:export\s+)?CONVEX_DEPLOYMENT\s*=/.test(line);
69
+ }
70
+
71
+ // Trim any character that would break a single-line trailing comment.
72
+ // We're not parsing it back as a value (it lives in a `# ...` tail),
73
+ // but a literal newline in a team/project name would split the line and
74
+ // poison subsequent parsing. Defensive normalization only.
75
+ function sanitizeForComment(value) {
76
+ if (value === undefined || value === null) return "";
77
+ return String(value).replace(/[\r\n]+/g, " ").replace(/#/g, "·").trim();
26
78
  }
27
79
 
28
80
  function unquoteEnvValue(raw) {
@@ -63,53 +115,135 @@ function readProjectEnv(projectDir) {
63
115
  return parseEnvContent(fs.readFileSync(file, "utf8"));
64
116
  }
65
117
 
66
- function updateEnvContent(content, { convexUrl, adminKey }) {
67
- const lines = content ? content.split(/\r?\n/) : [];
68
- if (lines.length > 0 && lines[lines.length - 1] === "") {
69
- lines.pop();
118
+ // Build the authoritative CONVEX_DEPLOYMENT line. Returns null when we
119
+ // don't have enough info to write one (e.g. legacy caller with no
120
+ // deploymentName) caller should skip writing instead of writing a
121
+ // half-formed line.
122
+ function buildDeploymentLine({ deploymentName, target, teamName, projectName, teamSlug, projectSlug }) {
123
+ if (!deploymentName) return null;
124
+ const safeTarget = target === "prod" ? "prod" : "dev";
125
+ const teamLabel = sanitizeForComment(teamName || teamSlug);
126
+ const projectLabel = sanitizeForComment(projectName || projectSlug);
127
+ const parts = [];
128
+ if (teamLabel) parts.push(`team: ${teamLabel}`);
129
+ if (projectLabel) parts.push(`project: ${projectLabel}`);
130
+ const comment = parts.length > 0 ? ` # ${parts.join(", ")}` : "";
131
+ return `${CONVEX_DEPLOYMENT}=${safeTarget}:${deploymentName}${comment}`;
132
+ }
133
+
134
+ function updateEnvContent(content, opts) {
135
+ const { convexUrl, adminKey } = opts || {};
136
+ if (!convexUrl || !adminKey) {
137
+ throw new Error("updateEnvContent requires convexUrl + adminKey");
70
138
  }
139
+ const deploymentLine = buildDeploymentLine(opts || {});
140
+
141
+ const lines = content ? content.split(/\r?\n/) : [];
142
+ // Drop trailing empty lines — we'll re-add a single newline via join.
143
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
144
+
145
+ // Canonical assignments by key.
146
+ const assignments = {
147
+ [NEXT_PUBLIC_CONVEX_URL]: envAssignment(NEXT_PUBLIC_CONVEX_URL, convexUrl),
148
+ [NEXT_PUBLIC_CONVEX_SITE_URL]: envAssignment(NEXT_PUBLIC_CONVEX_SITE_URL, convexUrl),
149
+ [SELF_HOSTED_URL]: envAssignment(SELF_HOSTED_URL, convexUrl),
150
+ [SELF_HOSTED_ADMIN_KEY]: envAssignment(SELF_HOSTED_ADMIN_KEY, adminKey),
151
+ };
71
152
 
72
- const replacements = new Map([
73
- [SELF_HOSTED_URL, envAssignment(SELF_HOSTED_URL, convexUrl)],
74
- [SELF_HOSTED_ADMIN_KEY, envAssignment(SELF_HOSTED_ADMIN_KEY, adminKey)],
75
- ]);
76
153
  const seen = new Set();
77
154
  const out = [];
155
+ let seenDeployment = false;
78
156
 
79
157
  for (const line of lines) {
80
158
  const key = keyFromLine(line);
81
- if (key === CONVEX_DEPLOYMENT) {
82
- out.push(commentDeploymentLine(line));
159
+
160
+ // CONVEX_DEPLOYMENT handling (incl. legacy `# CONVEX_DEPLOYMENT=...
161
+ // # disabled by synapse CLI` form):
162
+ // - new API (caller passed deploymentName) → first match becomes
163
+ // the authoritative line; subsequent matches are dropped.
164
+ // - legacy API (no deploymentName) → leave existing lines
165
+ // untouched so callers still using the old shape don't surprise
166
+ // their users.
167
+ if (isDeploymentLine(line)) {
168
+ if (deploymentLine) {
169
+ if (!seenDeployment) {
170
+ out.push(deploymentLine);
171
+ seenDeployment = true;
172
+ }
173
+ continue;
174
+ }
175
+ out.push(line);
83
176
  continue;
84
177
  }
85
- if (replacements.has(key)) {
86
- if (!seen.has(key)) {
87
- out.push(replacements.get(key));
88
- seen.add(key);
89
- }
178
+
179
+ if (assignments[key] && !seen.has(key)) {
180
+ out.push(assignments[key]);
181
+ seen.add(key);
182
+ continue;
183
+ }
184
+ if (assignments[key] && seen.has(key)) {
185
+ // Duplicate of a key we already wrote — drop to keep file canonical.
90
186
  continue;
91
187
  }
92
188
  out.push(line);
93
189
  }
94
190
 
95
- if (out.length > 0 && out[out.length - 1] !== "") {
96
- out.push("");
191
+ // Append any unseen managed keys, grouped under section headers so a
192
+ // brand-new file gets a tidy layout. On idempotent rewrites the loop
193
+ // above will have replaced everything in place; only the first-time
194
+ // write hits this block.
195
+ const newPublicLines = [];
196
+ if (!seen.has(NEXT_PUBLIC_CONVEX_URL)) {
197
+ newPublicLines.push(assignments[NEXT_PUBLIC_CONVEX_URL]);
198
+ seen.add(NEXT_PUBLIC_CONVEX_URL);
97
199
  }
98
- for (const [key, line] of replacements.entries()) {
99
- if (!seen.has(key)) {
100
- out.push(line);
200
+ if (!seen.has(NEXT_PUBLIC_CONVEX_SITE_URL)) {
201
+ newPublicLines.push(assignments[NEXT_PUBLIC_CONVEX_SITE_URL]);
202
+ seen.add(NEXT_PUBLIC_CONVEX_SITE_URL);
203
+ }
204
+ if (deploymentLine && !seenDeployment) {
205
+ newPublicLines.push(deploymentLine);
206
+ seenDeployment = true;
207
+ }
208
+
209
+ const newAuthLines = [];
210
+ if (!seen.has(SELF_HOSTED_URL)) {
211
+ newAuthLines.push(assignments[SELF_HOSTED_URL]);
212
+ seen.add(SELF_HOSTED_URL);
213
+ }
214
+ if (!seen.has(SELF_HOSTED_ADMIN_KEY)) {
215
+ newAuthLines.push(assignments[SELF_HOSTED_ADMIN_KEY]);
216
+ seen.add(SELF_HOSTED_ADMIN_KEY);
217
+ }
218
+
219
+ if (newPublicLines.length > 0 || newAuthLines.length > 0) {
220
+ if (out.length > 0) out.push("");
221
+ if (newPublicLines.length > 0) {
222
+ out.push(PUBLIC_HEADER);
223
+ out.push(...newPublicLines);
224
+ }
225
+ if (newAuthLines.length > 0) {
226
+ if (newPublicLines.length > 0) out.push("");
227
+ out.push(AUTH_HEADER);
228
+ out.push(...newAuthLines);
101
229
  }
102
230
  }
103
231
 
104
232
  return out.join("\n") + "\n";
105
233
  }
106
234
 
107
- function writeProjectEnv(projectDir, credentials) {
235
+ function writeProjectEnv(projectDir, credentials, opts = {}) {
108
236
  const file = path.join(projectDir, ".env.local");
109
237
  const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
110
238
  const next = updateEnvContent(existing, {
111
239
  convexUrl: credentials.convexUrl,
112
240
  adminKey: credentials.adminKey,
241
+ deploymentName: credentials.deploymentName || opts.deploymentName,
242
+ target: opts.target,
243
+ teamName: opts.team?.name,
244
+ teamSlug: opts.team?.slug,
245
+ projectName: opts.project?.name,
246
+ projectSlug: opts.project?.slug,
113
247
  });
114
248
  fs.writeFileSync(file, next, { mode: 0o600 });
115
249
  try {
@@ -122,12 +256,20 @@ function writeProjectEnv(projectDir, credentials) {
122
256
 
123
257
  module.exports = {
124
258
  CONVEX_DEPLOYMENT,
259
+ NEXT_PUBLIC_CONVEX_URL,
260
+ NEXT_PUBLIC_CONVEX_SITE_URL,
125
261
  SELF_HOSTED_ADMIN_KEY,
126
262
  SELF_HOSTED_URL,
263
+ MANAGED_VALUE_KEYS,
264
+ PUBLIC_HEADER,
265
+ AUTH_HEADER,
266
+ buildDeploymentLine,
267
+ isDeploymentLine,
127
268
  keyFromLine,
128
269
  parseEnvContent,
129
270
  quoteEnvValue,
130
271
  readProjectEnv,
272
+ sanitizeForComment,
131
273
  updateEnvContent,
132
274
  writeProjectEnv,
133
275
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iann29/synapse",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "Thin CLI wrapper for using the official Convex CLI with Synapse-managed deployments.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {