@iann29/synapse 1.8.8 → 1.8.10

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.
@@ -240,6 +240,34 @@ Examples:
240
240
  process.exitCode = 1;
241
241
  return;
242
242
  }
243
+
244
+ // v1.8.10: post-execute verification. Even after every step
245
+ // reports ✓ the actual end-state can be wrong — most commonly on
246
+ // Windows where the DNS Client cache holds a negative response
247
+ // even after the hosts file is updated. Re-scan and verify the
248
+ // domain ACTUALLY resolves to 127.0.0.1 (when we wrote hosts).
249
+ // If it doesn't, surface a yellow warning with a concrete
250
+ // remediation instead of pretending the setup is healthy.
251
+ const wroteHosts = results.some(
252
+ (r) => r.id === "hosts" && r.kind === "ok",
253
+ );
254
+ const verifyOk = after.resolution.resolvesToLoopback;
255
+ if (wroteHosts && !verifyOk && !skipHosts) {
256
+ summary.verifyResolution = false;
257
+ summary.verifyHint =
258
+ detection.platform.id === "windows"
259
+ ? "Windows DNS Client cache is still serving stale NXDOMAIN for this domain. Run `ipconfig /flushdns` from an Administrator PowerShell, then retry `npm run dev:https`. If it still fails, restart your machine."
260
+ : "Hosts file was written but the OS resolver still doesn't return 127.0.0.1. Try restarting nscd / systemd-resolved if you use one, or open a new shell.";
261
+ if (!ctx.out.json) {
262
+ ctx.out.stdout.write("\n");
263
+ ctx.out.warn(
264
+ `${domain} doesn't resolve to 127.0.0.1 yet — ${summary.verifyHint}`,
265
+ );
266
+ }
267
+ } else if (wroteHosts && verifyOk) {
268
+ summary.verifyResolution = true;
269
+ }
270
+
243
271
  ctx.out.stdout.write(
244
272
  `${colors.green("✓")} ${colors.bold(`HTTPS ready for ${domain}`)}\n`,
245
273
  );
@@ -634,6 +634,238 @@ function makeDeploymentCheck(target) {
634
634
  };
635
635
  }
636
636
 
637
+ // ============================================================
638
+ // v1.8.9: Local HTTPS dev category. Checks the `synapse https setup`
639
+ // surface — mkcert presence, CA trust, cert file presence + expiry,
640
+ // hostname resolution.
641
+ //
642
+ // These checks are CONTEXT-GATED: they only emit a meaningful result
643
+ // when the operator is in a project that uses dev:https OR has
644
+ // generated certs under ~/.config/dev-certs/. When neither is true,
645
+ // every check returns `silentlySkip: true` in its data, and the
646
+ // renderer hides the whole category — operators in a "no HTTPS
647
+ // here" project don't see a section of dim "skipped" lines.
648
+ //
649
+ // We require the https detect helpers; importing them here is a
650
+ // pure dependency (no side effects at require time).
651
+ // ============================================================
652
+
653
+ const httpsDetect = require("../https/detect");
654
+
655
+ // Read the cwd's package.json dev:https script (if any) and extract
656
+ // the --hostname / --experimental-https-cert / --experimental-https-key
657
+ // values. Returns null when there's no script, malformed JSON, or
658
+ // the script isn't recognisable.
659
+ function parseDevHttpsScript(cwd) {
660
+ const info = httpsDetect.detectPackageJson(cwd);
661
+ if (!info.present || !info.existingDevHttps) return null;
662
+ const cmd = info.existingDevHttps;
663
+ const hostMatch = cmd.match(/--hostname[\s=]+(\S+)/);
664
+ const certMatch = cmd.match(/--experimental-https-cert[\s=]+(\S+)/);
665
+ const keyMatch = cmd.match(/--experimental-https-key[\s=]+(\S+)/);
666
+ if (!hostMatch || !certMatch || !keyMatch) return null;
667
+ return {
668
+ domain: hostMatch[1],
669
+ cert: certMatch[1],
670
+ key: keyMatch[1],
671
+ raw: cmd,
672
+ };
673
+ }
674
+
675
+ // "Does this operator's machine HAVE any local-HTTPS context?"
676
+ // True when either:
677
+ // - cwd has a dev:https script, OR
678
+ // - ~/.config/dev-certs/ has at least one cert pair (any shape)
679
+ //
680
+ // Used as the gate for every check in this category — when it's
681
+ // false, the checks silently skip and the category is hidden.
682
+ function hasLocalHttpsContext(cwd) {
683
+ const dev = parseDevHttpsScript(cwd);
684
+ if (dev) return true;
685
+ // Check the cert store.
686
+ try {
687
+ const fs = require("node:fs");
688
+ const root = httpsDetect.certsRoot();
689
+ if (!fs.existsSync(root)) return false;
690
+ const entries = fs.readdirSync(root, { withFileTypes: true });
691
+ // Either a subdir (canonical) or a .pem pair (flat) means context.
692
+ if (entries.some((e) => e.isDirectory())) return true;
693
+ const flat = httpsDetect.detectFlatCertsInStore();
694
+ return flat.length > 0;
695
+ } catch {
696
+ return false;
697
+ }
698
+ }
699
+
700
+ const checkHttpsMkcert = {
701
+ id: "https-mkcert",
702
+ category: "local-https-dev",
703
+ title: "mkcert installed",
704
+ autoFix: "never",
705
+ dependsOn: [],
706
+ run: safeRun(async (ctx) => {
707
+ if (!hasLocalHttpsContext(ctx.cwd)) {
708
+ return { status: "skipped", summary: "no local HTTPS context", data: { silentlySkip: true } };
709
+ }
710
+ const m = httpsDetect.detectMkcert();
711
+ if (m.present) {
712
+ return { status: "ok", summary: m.version || "(version unknown)", data: { version: m.version, caroot: m.caroot } };
713
+ }
714
+ return {
715
+ status: "issue",
716
+ summary: "mkcert not found in PATH",
717
+ remediation: "Install mkcert (apt/pacman/dnf/brew/choco/scoop/winget), then run `synapse https setup <domain>`.",
718
+ data: { present: false },
719
+ };
720
+ }),
721
+ };
722
+
723
+ const checkHttpsCaTrusted = {
724
+ id: "https-ca-trusted",
725
+ category: "local-https-dev",
726
+ title: "local CA trusted (rootCA.pem present)",
727
+ autoFix: "never",
728
+ dependsOn: ["https-mkcert"],
729
+ run: safeRun(async (ctx) => {
730
+ if (!hasLocalHttpsContext(ctx.cwd)) {
731
+ return { status: "skipped", summary: "no local HTTPS context", data: { silentlySkip: true } };
732
+ }
733
+ const m = httpsDetect.detectMkcert();
734
+ if (!m.present) {
735
+ return { status: "skipped", summary: "mkcert missing", data: {} };
736
+ }
737
+ if (httpsDetect.detectCaTrusted(m)) {
738
+ return { status: "ok", summary: `rootCA in ${m.caroot}`, data: { caroot: m.caroot } };
739
+ }
740
+ return {
741
+ status: "issue",
742
+ summary: "rootCA.pem not found in mkcert's CAROOT",
743
+ remediation: "Run `mkcert -install` once (or just `synapse https setup <domain>`).",
744
+ data: { caroot: m.caroot },
745
+ };
746
+ }),
747
+ };
748
+
749
+ const checkHttpsCertFiles = {
750
+ id: "https-cert-files",
751
+ category: "local-https-dev",
752
+ title: "dev:https cert + key files exist",
753
+ autoFix: "never",
754
+ dependsOn: ["https-mkcert"],
755
+ run: safeRun(async (ctx) => {
756
+ const dev = parseDevHttpsScript(ctx.cwd);
757
+ if (!dev) {
758
+ return { status: "skipped", summary: "no dev:https script in cwd", data: { silentlySkip: true } };
759
+ }
760
+ const fs = require("node:fs");
761
+ const certExists = fs.existsSync(dev.cert);
762
+ const keyExists = fs.existsSync(dev.key);
763
+ if (certExists && keyExists) {
764
+ return {
765
+ status: "ok",
766
+ summary: `${dev.domain} ✓ both files present`,
767
+ data: { domain: dev.domain, cert: dev.cert, key: dev.key },
768
+ };
769
+ }
770
+ const missing = [];
771
+ if (!certExists) missing.push("cert");
772
+ if (!keyExists) missing.push("key");
773
+ return {
774
+ status: "issue",
775
+ summary: `${dev.domain}: missing ${missing.join(" + ")}`,
776
+ remediation: `Run \`synapse https setup ${dev.domain}\` to regenerate.`,
777
+ data: { domain: dev.domain, cert: dev.cert, key: dev.key, certExists, keyExists },
778
+ };
779
+ }),
780
+ };
781
+
782
+ const checkHttpsDomainResolves = {
783
+ id: "https-domain-resolves",
784
+ category: "local-https-dev",
785
+ title: "dev:https domain resolves to 127.0.0.1",
786
+ autoFix: "never",
787
+ dependsOn: [],
788
+ run: safeRun(async (ctx) => {
789
+ const dev = parseDevHttpsScript(ctx.cwd);
790
+ if (!dev) {
791
+ return { status: "skipped", summary: "no dev:https script in cwd", data: { silentlySkip: true } };
792
+ }
793
+ const resolution = await httpsDetect.detectDomainResolution(dev.domain);
794
+ if (resolution.resolvesToLoopback) {
795
+ return {
796
+ status: "ok",
797
+ summary: `${dev.domain} → 127.0.0.1 (via ${resolution.source})`,
798
+ data: { domain: dev.domain, source: resolution.source },
799
+ };
800
+ }
801
+ return {
802
+ status: "issue",
803
+ summary: `${dev.domain} doesn't resolve to 127.0.0.1 (got: ${resolution.got.join(", ") || "nothing"})`,
804
+ remediation: `Run \`synapse https setup ${dev.domain}\` to add the hosts entry, OR add a DNS A record pointing the domain at 127.0.0.1.`,
805
+ data: { domain: dev.domain, got: resolution.got },
806
+ };
807
+ }),
808
+ };
809
+
810
+ const checkHttpsCertExpiry = {
811
+ id: "https-cert-expiry",
812
+ category: "local-https-dev",
813
+ title: "cert not expiring soon (>30 days)",
814
+ autoFix: "never",
815
+ dependsOn: ["https-cert-files"],
816
+ run: safeRun(async (ctx) => {
817
+ const dev = parseDevHttpsScript(ctx.cwd);
818
+ if (!dev) {
819
+ return { status: "skipped", summary: "no dev:https script in cwd", data: { silentlySkip: true } };
820
+ }
821
+ const fs = require("node:fs");
822
+ if (!fs.existsSync(dev.cert)) {
823
+ return { status: "skipped", summary: "cert file missing", data: {} };
824
+ }
825
+ if (!httpsDetect.commandExists("openssl")) {
826
+ return { status: "skipped", summary: "openssl not available — can't check expiry", data: {} };
827
+ }
828
+ const { execFileSync } = require("node:child_process");
829
+ let expiry;
830
+ try {
831
+ const out = execFileSync(
832
+ "openssl",
833
+ ["x509", "-in", dev.cert, "-noout", "-enddate"],
834
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
835
+ );
836
+ const m = out.match(/notAfter=(.+)/);
837
+ if (m) expiry = new Date(m[1]);
838
+ } catch (err) {
839
+ return { status: "skipped", summary: `openssl read failed: ${err.message}`, data: {} };
840
+ }
841
+ if (!expiry || Number.isNaN(expiry.getTime())) {
842
+ return { status: "skipped", summary: "could not parse cert expiry", data: {} };
843
+ }
844
+ const daysLeft = Math.floor((expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
845
+ if (daysLeft < 0) {
846
+ return {
847
+ status: "issue",
848
+ summary: `${dev.domain}: cert EXPIRED ${-daysLeft} days ago (${expiry.toISOString().slice(0, 10)})`,
849
+ remediation: `Regenerate: \`synapse https setup ${dev.domain} --force\`.`,
850
+ data: { domain: dev.domain, expiry: expiry.toISOString(), daysLeft },
851
+ };
852
+ }
853
+ if (daysLeft < 30) {
854
+ return {
855
+ status: "warn",
856
+ summary: `${dev.domain}: cert expires in ${daysLeft} days (${expiry.toISOString().slice(0, 10)})`,
857
+ remediation: `Regenerate soon: \`synapse https setup ${dev.domain} --force\`.`,
858
+ data: { domain: dev.domain, expiry: expiry.toISOString(), daysLeft },
859
+ };
860
+ }
861
+ return {
862
+ status: "ok",
863
+ summary: `${dev.domain}: ${daysLeft} days left (expires ${expiry.toISOString().slice(0, 10)})`,
864
+ data: { domain: dev.domain, expiry: expiry.toISOString(), daysLeft },
865
+ };
866
+ }),
867
+ };
868
+
637
869
  const ALL_CHECKS = [
638
870
  // Tier A
639
871
  checkNodeVersion,
@@ -653,6 +885,22 @@ const ALL_CHECKS = [
653
885
  // Tier C (per deployment)
654
886
  makeDeploymentCheck("dev"),
655
887
  makeDeploymentCheck("prod"),
888
+
889
+ // Tier D — local HTTPS dev (v1.8.9). Context-gated: if the cwd
890
+ // doesn't have a dev:https script AND no certs are configured, the
891
+ // whole category is hidden from the report.
892
+ checkHttpsMkcert,
893
+ checkHttpsCaTrusted,
894
+ checkHttpsCertFiles,
895
+ checkHttpsDomainResolves,
896
+ checkHttpsCertExpiry,
656
897
  ];
657
898
 
658
- module.exports = { ALL_CHECKS, isBrowserReachable, cmpVer };
899
+ module.exports = {
900
+ ALL_CHECKS,
901
+ isBrowserReachable,
902
+ cmpVer,
903
+ // exports for tests
904
+ parseDevHttpsScript,
905
+ hasLocalHttpsContext,
906
+ };
@@ -21,10 +21,19 @@ const CATEGORY_TITLE = {
21
21
  project: "Project",
22
22
  backend: "Backend",
23
23
  deployments: "Deployments",
24
+ "local-https-dev": "Local HTTPS dev",
24
25
  upstream: "Upstream",
25
26
  workspace: "Workspace",
26
27
  };
27
- const CATEGORY_ORDER = ["local-env", "project", "backend", "deployments", "upstream", "workspace"];
28
+ const CATEGORY_ORDER = [
29
+ "local-env",
30
+ "project",
31
+ "backend",
32
+ "deployments",
33
+ "local-https-dev",
34
+ "upstream",
35
+ "workspace",
36
+ ];
28
37
 
29
38
  function renderHeader(report, write) {
30
39
  const parts = ["Synapse doctor"];
@@ -74,8 +83,16 @@ function renderReport(report, { stdout, verbose = false } = {}) {
74
83
  for (const cat of CATEGORY_ORDER) {
75
84
  const rows = byCategory.get(cat);
76
85
  if (!rows || rows.length === 0) continue;
86
+ // v1.8.9: filter out individual results marked `silentlySkip`
87
+ // (checks that decided their context is missing — e.g. `dev:https`
88
+ // checks in a project without that script). Then hide the
89
+ // category if NOTHING is left.
90
+ const visibleRows = rows.filter(
91
+ (r) => !(r.status === "skipped" && r.data && r.data.silentlySkip),
92
+ );
93
+ if (visibleRows.length === 0) continue;
77
94
  write(` ${colors.bold(CATEGORY_TITLE[cat] ?? cat)}\n`);
78
- for (const r of rows) renderCheck(r, { verbose }, write);
95
+ for (const r of visibleRows) renderCheck(r, { verbose }, write);
79
96
  write("\n");
80
97
  }
81
98
 
@@ -39,13 +39,22 @@ async function runChecks(checks, ctx) {
39
39
  return r.status === "issue" || r.status === "skipped";
40
40
  });
41
41
  if (failedDeps.length > 0) {
42
+ // v1.8.9: propagate `silentlySkip` from any silent-skipped
43
+ // prereq so the cascade-skipped child stays hidden too.
44
+ // Without this, https-cert-expiry would re-surface as a
45
+ // visible "·" every time its parent (https-cert-files) was
46
+ // silently skipped for "no dev:https script in cwd".
47
+ const silentParent = failedDeps.some((d) => {
48
+ const parent = resultsById.get(d);
49
+ return parent && parent.data && parent.data.silentlySkip;
50
+ });
42
51
  resultsById.set(id, {
43
52
  id,
44
53
  category: check.category,
45
54
  title: check.title,
46
55
  status: "skipped",
47
56
  summary: `skipped (prereq failed: ${failedDeps.join(", ")})`,
48
- data: { skippedBecause: failedDeps },
57
+ data: { skippedBecause: failedDeps, silentlySkip: silentParent },
49
58
  durationMs: 0,
50
59
  });
51
60
  pending.delete(id);
@@ -178,6 +178,26 @@ function readHosts(hostsPath) {
178
178
  }
179
179
  }
180
180
 
181
+ // Best-effort DNS cache flush. On Windows the DNS Client (Dnscache)
182
+ // service caches lookups separately from the hosts file — editing
183
+ // /etc/hosts on Windows does NOT invalidate the cache, so `next dev`
184
+ // and Node's dns.lookup() can both return ENOTFOUND despite the
185
+ // entry being written correctly. This bit Matheus's Windows machine
186
+ // in real-world testing (v1.8.10 bug report).
187
+ //
188
+ // `ipconfig /flushdns` is safe to call without admin elevation on
189
+ // every Windows version since at least Windows 7, but we never let a
190
+ // failure here block the wider flow — it's a hint, not a guarantee.
191
+ function flushDnsCacheIfWindows({ execImpl = execFileSync, platform = process.platform } = {}) {
192
+ if (platform !== "win32") return { ran: false, reason: "non-windows platform" };
193
+ try {
194
+ execImpl("ipconfig", ["/flushdns"], { stdio: "ignore", timeout: 5000 });
195
+ return { ran: true };
196
+ } catch (err) {
197
+ return { ran: false, reason: err.message };
198
+ }
199
+ }
200
+
181
201
  // Writes content to the hosts file. Three strategies are tried in
182
202
  // order based on the platform + the `elevation` knob:
183
203
  //
@@ -191,6 +211,13 @@ function readHosts(hostsPath) {
191
211
  // - "never": only try direct write; error if it fails
192
212
  // - "always": skip the direct write attempt
193
213
  //
214
+ // On Windows we additionally:
215
+ // - Normalise line endings to CRLF (the Windows hosts file
216
+ // traditionally uses CRLF; some Windows components are tolerant
217
+ // of LF but defensive normalisation costs nothing)
218
+ // - Run `ipconfig /flushdns` after a successful write so the
219
+ // DNS Client cache picks up the new entry immediately
220
+ //
194
221
  // `writeImpl` is injected for tests.
195
222
  function writeHosts(hostsPath, content, {
196
223
  elevation = "auto",
@@ -198,11 +225,15 @@ function writeHosts(hostsPath, content, {
198
225
  execImpl = execFileSync,
199
226
  platform = process.platform,
200
227
  } = {}) {
228
+ const onDiskContent =
229
+ platform === "win32" ? content.replace(/\r?\n/g, "\r\n") : content;
230
+
201
231
  // Try direct write first (fast path).
232
+ let result;
202
233
  if (elevation !== "always") {
203
234
  try {
204
- writeImpl(hostsPath, content);
205
- return { method: "direct", elevated: false };
235
+ writeImpl(hostsPath, onDiskContent);
236
+ result = { method: "direct", elevated: false };
206
237
  } catch (err) {
207
238
  if (elevation === "never") {
208
239
  throw new HostsError(
@@ -214,10 +245,25 @@ function writeHosts(hostsPath, content, {
214
245
  }
215
246
  }
216
247
 
248
+ if (!result) {
249
+ if (platform === "win32") {
250
+ result = writeHostsViaRunAs(hostsPath, onDiskContent, { execImpl });
251
+ } else {
252
+ result = writeHostsViaSudo(hostsPath, onDiskContent, { execImpl });
253
+ }
254
+ }
255
+
256
+ // Post-write DNS cache flush (best-effort, Windows only). The
257
+ // elevated runas path ALREADY flushes inside its PowerShell script
258
+ // — see writeHostsViaRunAs. Calling here covers the direct-write
259
+ // case (operator already in an elevated shell). Doubling up is
260
+ // harmless: a second flush is a no-op.
217
261
  if (platform === "win32") {
218
- return writeHostsViaRunAs(hostsPath, content, { execImpl });
262
+ const flush = flushDnsCacheIfWindows({ execImpl, platform });
263
+ result.dnsFlushed = flush.ran;
264
+ if (!flush.ran) result.dnsFlushReason = flush.reason;
219
265
  }
220
- return writeHostsViaSudo(hostsPath, content, { execImpl });
266
+ return result;
221
267
  }
222
268
 
223
269
  // Linux/macOS elevated write. We pipe the new content into `sudo
@@ -269,12 +315,18 @@ function writeHostsViaRunAs(hostsPath, content, { execImpl = execFileSync } = {}
269
315
  const tmpFile = path.join(os.tmpdir(), `synapse-hosts-${Date.now()}.txt`);
270
316
  fs.writeFileSync(tmpFile, content);
271
317
  const backupPath = `${hostsPath}.synapse-bak.${Date.now()}`;
272
- // PowerShell script (single-line, double-quote-friendly):
273
- // Copy-Item <hosts> <backup>; Move-Item -Force <tmp> <hosts>
318
+ // PowerShell script — runs ELEVATED via Start-Process RunAs below.
319
+ // 1. Copy current hosts to backup
320
+ // 2. Move new content over hosts
321
+ // 3. ipconfig /flushdns (v1.8.10): WITHOUT this the Windows DNS
322
+ // Client cache keeps returning NXDOMAIN for the freshly-added
323
+ // entry, breaking `next dev --hostname dev.foo.com` with
324
+ // ENOTFOUND despite the hosts file being correct.
274
325
  const script = [
275
326
  `try {`,
276
327
  ` Copy-Item -LiteralPath '${hostsPath}' -Destination '${backupPath}' -ErrorAction Stop;`,
277
328
  ` Move-Item -LiteralPath '${tmpFile}' -Destination '${hostsPath}' -Force -ErrorAction Stop;`,
329
+ ` & ipconfig /flushdns | Out-Null;`,
278
330
  ` exit 0`,
279
331
  `} catch {`,
280
332
  ` Write-Error $_.Exception.Message;`,
@@ -345,6 +397,7 @@ module.exports = {
345
397
  MANAGED_BLOCK_START,
346
398
  MANAGED_BLOCK_END,
347
399
  hostsPathForOS,
400
+ flushDnsCacheIfWindows,
348
401
  planAddEntry,
349
402
  planRemoveEntry,
350
403
  readHosts,
@@ -182,6 +182,16 @@ function plan(detection, {
182
182
  }
183
183
 
184
184
  // ---- Step: hosts file ----------------------------------------------
185
+ // Decision tree:
186
+ // 1. --skip-hosts → skip
187
+ // 2. Resolves via public DNS → skip (any machine works)
188
+ // 3. Hosts has entry + resolution works → skip (idempotent)
189
+ // 4. Hosts has entry + resolution still fails → exec ("DNS cache
190
+ // stale" fix — re-writing triggers ipconfig /flushdns on Windows).
191
+ // This is the v1.8.10 fix for the bug Matheus hit: hosts file
192
+ // had the entry but Windows DNS Client kept returning ENOTFOUND.
193
+ // 5. No entry → exec (write + flush)
194
+ const hostsHasEntry = detection.hosts.matches.some((m) => m.address === "127.0.0.1");
185
195
  if (skipHosts) {
186
196
  steps.push({
187
197
  id: "hosts",
@@ -202,13 +212,26 @@ function plan(detection, {
202
212
  skipReason:
203
213
  "no hosts edit needed — DNS A record points at loopback (any machine on any OS resolves correctly)",
204
214
  });
205
- } else if (detection.hosts.matches.some((m) => m.address === "127.0.0.1")) {
215
+ } else if (hostsHasEntry && detection.resolution.resolvesToLoopback) {
206
216
  steps.push({
207
217
  id: "hosts",
208
218
  title: "Hosts file entry",
209
219
  kind: "skip",
210
- reason: `${detection.hosts.path} already maps ${detection.domain} to 127.0.0.1`,
211
- skipReason: "entry present",
220
+ reason: `${detection.hosts.path} already maps ${detection.domain} to 127.0.0.1 and resolution works`,
221
+ skipReason: "entry present + DNS cache fresh",
222
+ });
223
+ } else if (hostsHasEntry && !detection.resolution.resolvesToLoopback) {
224
+ steps.push({
225
+ id: "hosts",
226
+ title: `Refresh DNS cache for ${detection.domain}`,
227
+ kind: "exec",
228
+ reason:
229
+ detection.platform.id === "windows"
230
+ ? `${detection.hosts.path} has the entry but Windows DNS cache is stale — re-writing to trigger \`ipconfig /flushdns\``
231
+ : `${detection.hosts.path} has the entry but resolution still fails — re-writing to force a refresh`,
232
+ async run() {
233
+ return hostsMod.addEntry(detection.hosts.path, detection.domain);
234
+ },
212
235
  });
213
236
  } else {
214
237
  const needsElevation = !detection.hosts.writable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iann29/synapse",
3
- "version": "1.8.8",
3
+ "version": "1.8.10",
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": {