@iann29/synapse 1.8.7 → 1.8.9

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.
@@ -0,0 +1,412 @@
1
+ // The planner turns a Detection object (see detect.js) into a list
2
+ // of Steps the executor can run. Steps are declarative records the
3
+ // preview UI can also print without executing — enabling --dry-run
4
+ // and -v shows for free.
5
+ //
6
+ // Each Step is shaped:
7
+ // {
8
+ // id: string, // stable; logs key off this
9
+ // title: string, // human-readable one-liner
10
+ // kind: "exec" | "skip" | "blocker" | "warn",
11
+ // reason: string, // why we chose this path
12
+ // run?: (ctx) => Promise<{ changed, ... }>
13
+ // skipReason?: string, // why we're NOT running
14
+ // blocker?: string, // why the whole plan is blocked
15
+ // }
16
+ //
17
+ // `kind: blocker` halts the plan before execution (e.g. mkcert
18
+ // missing — we can't proceed). The executor refuses to run if any
19
+ // blocker is present.
20
+
21
+ const fs = require("node:fs");
22
+ const path = require("node:path");
23
+ const detectMod = require("./detect");
24
+ const mkcertMod = require("./mkcert");
25
+ const hostsMod = require("./hosts");
26
+ const nextjsMod = require("./nextjs");
27
+
28
+ // Maps a detected platform + pkg manager combo to the canonical
29
+ // install instructions for mkcert. We don't auto-install (mutating
30
+ // pkg managers from a CLI feels wrong); we just print the command.
31
+ function mkcertInstallHint(detection) {
32
+ const { id } = detection.platform;
33
+ const managers = detection.packageManagers;
34
+ if (id === "linux" || id === "wsl") {
35
+ if (managers.includes("pacman")) return "sudo pacman -S mkcert nss";
36
+ if (managers.includes("apt-get")) return "sudo apt-get install mkcert libnss3-tools";
37
+ if (managers.includes("dnf")) return "sudo dnf install mkcert nss-tools";
38
+ if (managers.includes("yum")) return "sudo yum install mkcert nss-tools";
39
+ if (managers.includes("zypper")) return "sudo zypper install mkcert mozilla-nss-tools";
40
+ if (managers.includes("apk")) return "sudo apk add mkcert nss-tools";
41
+ return "Install mkcert from https://github.com/FiloSottile/mkcert/releases";
42
+ }
43
+ if (id === "macos") {
44
+ if (managers.includes("brew")) return "brew install mkcert nss";
45
+ return "Install Homebrew (https://brew.sh) then run: brew install mkcert nss";
46
+ }
47
+ if (id === "windows") {
48
+ if (managers.includes("scoop")) return "scoop bucket add extras && scoop install mkcert";
49
+ if (managers.includes("choco")) return "choco install mkcert";
50
+ if (managers.includes("winget")) return "winget install FiloSottile.mkcert";
51
+ return "Install mkcert: https://github.com/FiloSottile/mkcert/releases (download the .exe to a PATH directory)";
52
+ }
53
+ return "Install mkcert from https://github.com/FiloSottile/mkcert/releases";
54
+ }
55
+
56
+ // Computes the canonical dev:https script for a domain + cert path
57
+ // pair. Always uses POSIX-style forward slashes — Next.js accepts
58
+ // them on Windows too, and they make the script identical across
59
+ // platforms (so a project shared between Linux + Windows partners
60
+ // reads the same).
61
+ function devHttpsCommandFor(detection, domain) {
62
+ const { certPath, keyPath } = detection.existingCerts || detectMod.certFilesFor(domain);
63
+ // Convert to posix path style; on Linux/macOS this is a no-op.
64
+ const toPosix = (p) => p.replace(/\\/g, "/");
65
+ return detectMod.buildDevHttpsScript(domain, toPosix(certPath), toPosix(keyPath));
66
+ }
67
+
68
+ // The main entry. Given a detection + options, returns an array of
69
+ // Steps. Caller decides whether to print, prompt, then execute.
70
+ function plan(detection, {
71
+ force = false,
72
+ skipHosts = false,
73
+ skipScript = false,
74
+ } = {}) {
75
+ const steps = [];
76
+
77
+ // ---- Pre-validate (blockers go first) ------------------------------
78
+ if (!detection.validation.ok) {
79
+ steps.push({
80
+ id: "validate",
81
+ title: "Validate domain",
82
+ kind: "blocker",
83
+ reason: detection.validation.reason,
84
+ blocker: detection.validation.reason,
85
+ });
86
+ return steps;
87
+ }
88
+ if (!detection.mkcert.present) {
89
+ const hint = mkcertInstallHint(detection);
90
+ steps.push({
91
+ id: "mkcert-missing",
92
+ title: "mkcert installed",
93
+ kind: "blocker",
94
+ reason: "mkcert not found in PATH",
95
+ blocker: `mkcert is required but not installed. Run:\n ${hint}\nThen re-run \`synapse https setup\`.`,
96
+ });
97
+ return steps;
98
+ }
99
+
100
+ // ---- Step: mkcert -install (CA trust) ------------------------------
101
+ if (!detection.caTrusted) {
102
+ steps.push({
103
+ id: "ca-install",
104
+ title: "Install local CA into system + browser trust stores (mkcert -install)",
105
+ kind: "exec",
106
+ reason: "rootCA.pem not present in mkcert's CAROOT — first-time setup",
107
+ async run() {
108
+ return mkcertMod.installCA();
109
+ },
110
+ });
111
+ } else {
112
+ steps.push({
113
+ id: "ca-install",
114
+ title: "Install local CA",
115
+ kind: "skip",
116
+ reason: "CA already trusted (rootCA.pem present)",
117
+ skipReason: "idempotent",
118
+ });
119
+ }
120
+ // Firefox/NSS reminder — not a blocker, just a warning.
121
+ if (!detection.hasCertutil && (detection.platform.id === "linux" || detection.platform.id === "wsl")) {
122
+ steps.push({
123
+ id: "nss-warn",
124
+ title: "Firefox trust (NSS)",
125
+ kind: "warn",
126
+ reason:
127
+ "certutil not found — Firefox uses a separate NSS DB. Without it, Chrome/Edge will trust the cert but Firefox won't.",
128
+ skipReason:
129
+ "Install NSS tools to fix Firefox trust on this machine (apt: libnss3-tools, pacman: nss, dnf: nss-tools), then re-run `mkcert -install`.",
130
+ });
131
+ }
132
+
133
+ // ---- Step: ensure cert dir exists ----------------------------------
134
+ const { dir: certDir, certPath, keyPath } = detection.existingCerts || detectMod.certFilesFor(detection.domain);
135
+ if (!fs.existsSync(certDir)) {
136
+ steps.push({
137
+ id: "cert-dir",
138
+ title: `Create ${certDir}`,
139
+ kind: "exec",
140
+ reason: "cert directory missing",
141
+ async run() {
142
+ fs.mkdirSync(certDir, { recursive: true, mode: 0o700 });
143
+ return { changed: true };
144
+ },
145
+ });
146
+ } else {
147
+ steps.push({
148
+ id: "cert-dir",
149
+ title: `Cert directory ${certDir}`,
150
+ kind: "skip",
151
+ reason: "directory already exists",
152
+ skipReason: "idempotent",
153
+ });
154
+ }
155
+
156
+ // ---- Step: generate certificate ------------------------------------
157
+ const certPresent =
158
+ detection.existingCerts && detection.existingCerts.present && !force;
159
+ if (certPresent) {
160
+ steps.push({
161
+ id: "cert-generate",
162
+ title: `Generate cert pair for ${detection.domain}`,
163
+ kind: "skip",
164
+ reason: "cert + key already exist in canonical location",
165
+ skipReason:
166
+ "use `synapse https setup <domain> --force` to regenerate (invalidates the current cert).",
167
+ });
168
+ } else {
169
+ steps.push({
170
+ id: "cert-generate",
171
+ title: `Generate cert pair for ${detection.domain} (mkcert)`,
172
+ kind: "exec",
173
+ reason: force ? "--force: regenerating" : "no cert present in canonical location",
174
+ async run() {
175
+ return mkcertMod.generateCert({
176
+ domain: detection.domain,
177
+ certPath,
178
+ keyPath,
179
+ });
180
+ },
181
+ });
182
+ }
183
+
184
+ // ---- Step: hosts file ----------------------------------------------
185
+ if (skipHosts) {
186
+ steps.push({
187
+ id: "hosts",
188
+ title: "Hosts file entry",
189
+ kind: "skip",
190
+ reason: "--skip-hosts requested",
191
+ skipReason: "operator opted out",
192
+ });
193
+ } else if (
194
+ detection.resolution.resolvesToLoopback &&
195
+ detection.resolution.source === "dns"
196
+ ) {
197
+ steps.push({
198
+ id: "hosts",
199
+ title: "Hosts file entry",
200
+ kind: "skip",
201
+ reason: `${detection.domain} already resolves to 127.0.0.1 via public DNS`,
202
+ skipReason:
203
+ "no hosts edit needed — DNS A record points at loopback (any machine on any OS resolves correctly)",
204
+ });
205
+ } else if (detection.hosts.matches.some((m) => m.address === "127.0.0.1")) {
206
+ steps.push({
207
+ id: "hosts",
208
+ title: "Hosts file entry",
209
+ kind: "skip",
210
+ reason: `${detection.hosts.path} already maps ${detection.domain} to 127.0.0.1`,
211
+ skipReason: "entry present",
212
+ });
213
+ } else {
214
+ const needsElevation = !detection.hosts.writable;
215
+ steps.push({
216
+ id: "hosts",
217
+ title: `Add "127.0.0.1 ${detection.domain}" to ${detection.hosts.path}`,
218
+ kind: "exec",
219
+ reason: needsElevation
220
+ ? `${detection.hosts.path} requires elevation (sudo/Administrator)`
221
+ : "writable directly",
222
+ async run() {
223
+ return hostsMod.addEntry(detection.hosts.path, detection.domain);
224
+ },
225
+ });
226
+ }
227
+
228
+ // ---- Step: WSL Windows hosts companion -----------------------------
229
+ if (
230
+ detection.platform.id === "wsl" &&
231
+ detection.wslWindowsHosts &&
232
+ detection.wslWindowsHosts.exists &&
233
+ !detection.wslWindowsHosts.matches.some((m) => m.address === "127.0.0.1") &&
234
+ !detection.resolution.resolvesToLoopback
235
+ ) {
236
+ steps.push({
237
+ id: "hosts-wsl",
238
+ title: `Add Windows-side hosts entry (${detection.wslWindowsHosts.path})`,
239
+ kind: "warn",
240
+ reason:
241
+ "WSL Linux hosts file does NOT affect Windows browsers. Edit the Windows hosts file from a Windows Administrator shell:",
242
+ skipReason: `Open PowerShell as Administrator and append: 127.0.0.1\t${detection.domain} to ${detection.wslWindowsHosts.path}`,
243
+ });
244
+ }
245
+
246
+ // ---- Step: package.json script -------------------------------------
247
+ if (skipScript) {
248
+ steps.push({
249
+ id: "script",
250
+ title: "package.json dev:https",
251
+ kind: "skip",
252
+ reason: "--skip-script requested",
253
+ skipReason: "operator opted out",
254
+ });
255
+ } else if (!detection.pkg.present) {
256
+ steps.push({
257
+ id: "script",
258
+ title: "package.json dev:https",
259
+ kind: "skip",
260
+ reason: "no package.json in cwd",
261
+ skipReason: `cd into your Next.js project, then re-run, OR add manually:\n "dev:https": "${devHttpsCommandFor(detection, detection.domain)}"`,
262
+ });
263
+ } else if (!detection.pkg.hasNext) {
264
+ steps.push({
265
+ id: "script",
266
+ title: "package.json dev:https",
267
+ kind: "warn",
268
+ reason: "package.json present but `next` not in dependencies",
269
+ skipReason:
270
+ "not a Next.js project — skipping the dev:https script. The cert is still ready in ~/.config/dev-certs.",
271
+ });
272
+ } else {
273
+ const command = devHttpsCommandFor(detection, detection.domain);
274
+ const existing = detection.pkg.existingDevHttps;
275
+ if (existing && detectMod.devHttpsScriptsEqual(existing, command)) {
276
+ steps.push({
277
+ id: "script",
278
+ title: "package.json dev:https",
279
+ kind: "skip",
280
+ reason: "script already matches the canonical command",
281
+ skipReason: "idempotent",
282
+ });
283
+ } else {
284
+ steps.push({
285
+ id: "script",
286
+ title: existing
287
+ ? "Update package.json dev:https script"
288
+ : "Add package.json dev:https script",
289
+ kind: "exec",
290
+ reason: existing
291
+ ? `existing script differs from canonical — will replace.\n before: ${existing}\n after: ${command}`
292
+ : "no dev:https script in package.json",
293
+ async run() {
294
+ return nextjsMod.setDevHttpsScript(detection.pkg.path, command);
295
+ },
296
+ });
297
+ }
298
+ }
299
+
300
+ // ---- Step: legacy cert migration hint ------------------------------
301
+ if (detection.legacyCerts && detection.legacyCerts.length > 0) {
302
+ const names = detection.legacyCerts.map((c) => c.domain).join(", ");
303
+ steps.push({
304
+ id: "legacy-warn",
305
+ title: "Legacy certs detected in project root",
306
+ kind: "warn",
307
+ reason: `Found ${detection.legacyCerts.length} legacy cert pair(s) in cwd: ${names}`,
308
+ skipReason:
309
+ "Run `synapse https migrate` to move them under ~/.config/dev-certs/ and update package.json paths.",
310
+ });
311
+ }
312
+
313
+ return steps;
314
+ }
315
+
316
+ // Used by the remove command. Generates an undo plan symmetric to
317
+ // the setup plan: removes cert files, hosts entry, dev:https script.
318
+ function planRemove(detection, { keepCerts = false, keepScript = false, keepHosts = false } = {}) {
319
+ const steps = [];
320
+ if (!detection.validation.ok) {
321
+ steps.push({
322
+ id: "validate",
323
+ title: "Validate domain",
324
+ kind: "blocker",
325
+ reason: detection.validation.reason,
326
+ blocker: detection.validation.reason,
327
+ });
328
+ return steps;
329
+ }
330
+ const { dir: certDir, certPath, keyPath } = detection.existingCerts || detectMod.certFilesFor(detection.domain);
331
+
332
+ if (!keepCerts) {
333
+ if (detection.existingCerts && detection.existingCerts.present) {
334
+ steps.push({
335
+ id: "cert-remove",
336
+ title: `Delete ${certDir}`,
337
+ kind: "exec",
338
+ reason: "cert present in canonical location",
339
+ async run() {
340
+ fs.rmSync(certDir, { recursive: true, force: true });
341
+ return { changed: true };
342
+ },
343
+ });
344
+ } else {
345
+ steps.push({
346
+ id: "cert-remove",
347
+ title: "Delete cert directory",
348
+ kind: "skip",
349
+ reason: "no cert present in canonical location",
350
+ skipReason: "nothing to delete",
351
+ });
352
+ }
353
+ }
354
+
355
+ if (!keepHosts) {
356
+ if (detection.hosts.matches.length > 0) {
357
+ steps.push({
358
+ id: "hosts-remove",
359
+ title: `Remove ${detection.domain} from ${detection.hosts.path}`,
360
+ kind: "exec",
361
+ reason: "managed hosts entry present",
362
+ async run() {
363
+ return hostsMod.removeEntry(detection.hosts.path, detection.domain);
364
+ },
365
+ });
366
+ } else {
367
+ steps.push({
368
+ id: "hosts-remove",
369
+ title: "Remove hosts entry",
370
+ kind: "skip",
371
+ reason: "no entry found",
372
+ skipReason: "nothing to remove",
373
+ });
374
+ }
375
+ }
376
+
377
+ if (!keepScript) {
378
+ if (detection.pkg.existingDevHttps) {
379
+ steps.push({
380
+ id: "script-remove",
381
+ title: "Remove dev:https script from package.json",
382
+ kind: "exec",
383
+ reason: "script present",
384
+ async run() {
385
+ return nextjsMod.removeDevHttpsScript(detection.pkg.path);
386
+ },
387
+ });
388
+ } else {
389
+ steps.push({
390
+ id: "script-remove",
391
+ title: "Remove dev:https script",
392
+ kind: "skip",
393
+ reason: "no script present",
394
+ skipReason: "nothing to remove",
395
+ });
396
+ }
397
+ }
398
+ return steps;
399
+ }
400
+
401
+ // Tells the caller whether the plan is executable (no blockers).
402
+ function planIsExecutable(steps) {
403
+ return steps.every((s) => s.kind !== "blocker");
404
+ }
405
+
406
+ module.exports = {
407
+ plan,
408
+ planRemove,
409
+ planIsExecutable,
410
+ mkcertInstallHint,
411
+ devHttpsCommandFor,
412
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iann29/synapse",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
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": {