clivly 0.10.3 → 0.10.5

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 (82) hide show
  1. package/README.md +48 -1
  2. package/core-drizzle.d.cts +1 -1
  3. package/core-drizzle.d.mts +1 -1
  4. package/core-entity-config.cjs +4 -1
  5. package/core-entity-config.d.cts +2 -2
  6. package/core-entity-config.d.mts +2 -2
  7. package/core-entity-config.mjs +2 -2
  8. package/core-entity-heuristics.d.cts +1 -1
  9. package/core-entity-heuristics.d.mts +1 -1
  10. package/core-mapping-form.d.cts +1 -1
  11. package/core-mapping-form.d.mts +1 -1
  12. package/core-mappings-config.cjs +1 -1
  13. package/core-mappings-config.d.cts +1 -1
  14. package/core-mappings-config.d.mts +1 -1
  15. package/core-mappings-config.mjs +1 -1
  16. package/core-sync-engine.d.cts +1 -1
  17. package/core-sync-engine.d.mts +1 -1
  18. package/core-view-compiler.d.cts +1 -1
  19. package/core-view-compiler.d.mts +1 -1
  20. package/core.cjs +5 -2
  21. package/core.d.cts +8 -8
  22. package/core.d.mts +8 -8
  23. package/core.mjs +3 -3
  24. package/{drizzle-DTOuGXEn.d.cts → drizzle-SD8WCvYj.d.cts} +1 -1
  25. package/{drizzle-SGapoly2.d.mts → drizzle-axugRJ0u.d.mts} +1 -1
  26. package/drizzle.d.cts +3 -3
  27. package/drizzle.d.mts +3 -3
  28. package/{entity-config-DgAe0_YD.cjs → entity-config-C2MfIDL2.cjs} +49 -0
  29. package/{entity-config-C4d3K9CP.mjs → entity-config-bC3DX9f1.mjs} +32 -1
  30. package/{entity-config-DNQAMbMS.d.mts → entity-config-w8oMBWf8.d.cts} +28 -1
  31. package/{entity-config-DNQAMbMS.d.cts → entity-config-w8oMBWf8.d.mts} +28 -1
  32. package/index.cjs +192 -8
  33. package/index.mjs +192 -8
  34. package/{integration-state-D8SLu228.d.cts → integration-state-BA4c0RbD.d.cts} +15 -0
  35. package/{integration-state-D8SLu228.d.mts → integration-state-BA4c0RbD.d.mts} +15 -0
  36. package/{internal-BAOG5eE4.d.mts → internal-CHvZMvxC.d.cts} +1 -1
  37. package/{internal-BHJ0X3fC.d.cts → internal-WximMGmH.d.mts} +1 -1
  38. package/internal-drizzle.d.cts +1 -1
  39. package/internal-drizzle.d.mts +1 -1
  40. package/internal-entity-config.cjs +4 -1
  41. package/internal-entity-config.d.cts +2 -2
  42. package/internal-entity-config.d.mts +2 -2
  43. package/internal-entity-config.mjs +2 -2
  44. package/internal-entity-heuristics.cjs +1 -1
  45. package/internal-entity-heuristics.d.cts +1 -1
  46. package/internal-entity-heuristics.d.mts +1 -1
  47. package/internal-entity-heuristics.mjs +1 -1
  48. package/internal-mapping-form.d.cts +1 -1
  49. package/internal-mapping-form.d.mts +1 -1
  50. package/internal-mappings-config.cjs +1 -1
  51. package/internal-mappings-config.d.cts +1 -1
  52. package/internal-mappings-config.d.mts +1 -1
  53. package/internal-mappings-config.mjs +1 -1
  54. package/internal-sync-engine.d.cts +1 -1
  55. package/internal-sync-engine.d.mts +1 -1
  56. package/internal-view-compiler.d.cts +1 -1
  57. package/internal-view-compiler.d.mts +1 -1
  58. package/internal.cjs +5 -2
  59. package/internal.d.cts +8 -8
  60. package/internal.d.mts +8 -8
  61. package/internal.mjs +3 -3
  62. package/{mapping-form-Cc7Ewv8c.d.mts → mapping-form-BsTxv8w3.d.cts} +1 -1
  63. package/{mapping-form-DwdbYAfR.d.cts → mapping-form-OTFwr1sN.d.mts} +1 -1
  64. package/{mappings-config-MoXBeI8p.mjs → mappings-config-CRSL9dzw.mjs} +1 -1
  65. package/{mappings-config-Be9D43Np.d.mts → mappings-config-DbznSjTX.d.cts} +1 -1
  66. package/{mappings-config-Dx9ldgdf.cjs → mappings-config-Dpv08-x4.cjs} +1 -1
  67. package/{mappings-config-DUt4-9iO.d.cts → mappings-config-SXu_zlmp.d.mts} +1 -1
  68. package/{namespace-probe-D7GpEwLn.d.cts → namespace-probe-lj_0bJrc.d.cts} +2 -2
  69. package/{namespace-probe-DLve5TPy.d.mts → namespace-probe-ouHKZas2.d.mts} +2 -2
  70. package/package.json +1 -1
  71. package/{sdk-D6gX0TXS.mjs → sdk-BTzaM8bF.mjs} +2 -2
  72. package/{sdk-B1vX4kUJ.cjs → sdk-DbvqXNSh.cjs} +2 -2
  73. package/sdk.cjs +2 -2
  74. package/sdk.d.cts +2 -2
  75. package/sdk.d.mts +2 -2
  76. package/sdk.mjs +2 -2
  77. package/{sync-engine-Df0kHAoJ.d.cts → sync-engine-CUKWXUv7.d.mts} +2 -2
  78. package/{sync-engine-ugFacJ6G.d.mts → sync-engine-D2YlIHZB.d.cts} +2 -2
  79. package/{view-compiler-D3zp9S3K.d.cts → view-compiler-BO4CKamn.d.cts} +1 -1
  80. package/{view-compiler-TpzDTu7z.d.mts → view-compiler-iEwUOuba.d.mts} +1 -1
  81. package/vite.d.cts +1 -1
  82. package/vite.d.mts +1 -1
package/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_entity_config = require("./entity-config-DgAe0_YD.cjs");
3
+ const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
4
4
  require("./mapping-score-CaB3F4Uu.cjs");
5
5
  const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-B1vX4kUJ.cjs");
6
+ const require_sdk = require("./sdk-DbvqXNSh.cjs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
@@ -12,6 +12,7 @@ let node_path = require("node:path");
12
12
  let node_readline_promises = require("node:readline/promises");
13
13
  let jiti = require("jiti");
14
14
  let node_url = require("node:url");
15
+ let node_net = require("node:net");
15
16
  //#region ../core/src/env-file.ts
16
17
  const ENV_FILES = {
17
18
  next: ".env.local",
@@ -333,6 +334,28 @@ const TRIGGER_URL_PLACEHOLDER = `https://<yourDomain.com>${CLIVLY_TICK_PATH}`;
333
334
  //#region src/dev-tunnel.ts
334
335
  const CLOUDFLARE_QUICK_TUNNEL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
335
336
  const DEFAULT_PORT = "3000";
337
+ /**
338
+ * The port each framework's dev server actually listens on out of the box.
339
+ *
340
+ * `clivly dev` used to tunnel to 3000 for everyone. Vite-based frameworks start
341
+ * on 5173, so the tunnel pointed at whatever else happened to hold 3000 — in one
342
+ * smoke run, a *different app entirely*, which answered, which made the tunnel
343
+ * look healthy while syncing the wrong project. Every symptom pointed away from
344
+ * the cause.
345
+ *
346
+ * These are defaults, not detections: a project that sets its own port must
347
+ * still pass `--port`. Guessing right for the untouched case is the win.
348
+ */
349
+ const FRAMEWORK_DEFAULT_PORTS = {
350
+ remix: "5173",
351
+ sveltekit: "5173",
352
+ "tanstack-start": "5173",
353
+ nextjs: "3000",
354
+ nuxt: "3000"
355
+ };
356
+ function defaultPortFor(framework) {
357
+ return (framework && FRAMEWORK_DEFAULT_PORTS[framework]) ?? DEFAULT_PORT;
358
+ }
336
359
  const DEFAULT_ROUTE_PATH = CLIVLY_TICK_PATH;
337
360
  function parseTunnelUrl(chunk) {
338
361
  const match = chunk.match(CLOUDFLARE_QUICK_TUNNEL_REGEX);
@@ -342,11 +365,50 @@ function buildTriggerUrl(origin, path) {
342
365
  return `${origin.endsWith("/") ? origin.slice(0, -1) : origin}${path.startsWith("/") ? path : `/${path}`}`;
343
366
  }
344
367
  function resolveDevTarget(flags) {
368
+ const port = flags.port ?? defaultPortFor(flags.framework);
345
369
  return {
346
- target: flags.url ?? `http://localhost:${flags.port ?? DEFAULT_PORT}`,
347
- triggerPath: flags.routePath ?? DEFAULT_ROUTE_PATH
370
+ target: flags.url ?? `http://localhost:${port}`,
371
+ triggerPath: flags.routePath ?? DEFAULT_ROUTE_PATH,
372
+ port
348
373
  };
349
374
  }
375
+ const VITE_FRAMEWORKS = /* @__PURE__ */ new Set([
376
+ "nuxt",
377
+ "remix",
378
+ "sveltekit",
379
+ "tanstack-start"
380
+ ]);
381
+ function isViteFramework(framework) {
382
+ return framework ? VITE_FRAMEWORKS.has(framework) : false;
383
+ }
384
+ /**
385
+ * The one config line that makes a Vite dev server answer the tunnel.
386
+ *
387
+ * Without it every sync fails with a bare "The host app returned 403" and the
388
+ * explanation — Vite's own, naming this exact setting — is buried in a
389
+ * diagnostics column. This affects four of the five supported frameworks, so
390
+ * `clivly dev` says it up front rather than letting each developer rediscover
391
+ * it. Printed unconditionally for Vite apps: reading the app's vite config well
392
+ * enough to know whether it is already set means parsing arbitrary TypeScript,
393
+ * and a redundant hint is far cheaper than a silent 403.
394
+ */
395
+ function viteAllowedHostsHint() {
396
+ return [
397
+ "Vite blocks unknown hosts, so the tunnel needs one line in your Vite config:",
398
+ " server: { allowedHosts: [\".trycloudflare.com\"] }",
399
+ "Without it, sync fails with 403 before it reaches your app."
400
+ ].join("\n");
401
+ }
402
+ /**
403
+ * Warns that nothing is listening on the port about to be tunnelled.
404
+ *
405
+ * A quick tunnel to a dead port still resolves and still answers, so the failure
406
+ * surfaces later as an HTTP error attributed to the host app. Naming it here —
407
+ * while the developer is still looking at `clivly dev` output — costs one line.
408
+ */
409
+ function deadPortMessage(port, framework) {
410
+ return [`Warning: nothing is listening on port ${port} yet, but the tunnel points there.${framework ? ` ${FRAMEWORK_LABELS[framework]} usually serves on ${defaultPortFor(framework)}.` : ""}`, "If your dev server uses a different port, restart with: clivly dev --port <port>"].join("\n");
411
+ }
350
412
  function detectDevCommand(scripts, explicit) {
351
413
  if (explicit && explicit.length > 0) return explicit;
352
414
  if (scripts.dev) return [
@@ -389,6 +451,76 @@ function cloudflaredMissingMessage() {
389
451
  ].join("\n");
390
452
  }
391
453
  //#endregion
454
+ //#region src/tunnel-identity.ts
455
+ /**
456
+ * Whether the published tunnel actually reaches *this* app's Clivly route.
457
+ *
458
+ * `isLocalPortListening` answers a different question — "is anything accepting
459
+ * connections on this port?" — and that is exactly the gap this closes. On a
460
+ * machine running more than one project (which is what `clivly dev` is for),
461
+ * a framework that finds its default port taken silently moves to the next one:
462
+ *
463
+ * ⚠ Port 3000 is in use by process 92495, using available port 3001 instead
464
+ *
465
+ * The tunnel is still pointed at 3000, where a *different application* is
466
+ * listening. The port probe sees a healthy socket and says nothing, and the CLI
467
+ * goes on to promise "Dashboard-triggered sync now points at this local
468
+ * session" — which is false. Signed triggers for this workspace are delivered
469
+ * to somebody else's app, and the resulting failure is reported against the
470
+ * innocent one. Liveness is not identity.
471
+ *
472
+ * The check: POST to the trigger path with no signature. A Clivly-mounted route
473
+ * rejects that with **401** — it read the request, found no valid signature and
474
+ * refused. Anything else on that port answers some other way (a framework's
475
+ * 500 error page, a 404, an unrelated 200). We are not testing authentication
476
+ * here; the 401 *is* the fingerprint, and it is one no unrelated service
477
+ * produces at this exact path by accident.
478
+ *
479
+ * Deliberately tolerant: a network hiccup returns `unknown`, never `mismatch`.
480
+ * Warning someone away from a tunnel that was fine is worse than staying quiet,
481
+ * for the same reason the port probe defaults to assuming the port is alive.
482
+ */
483
+ /** How long to wait before giving up and returning `unknown`. */
484
+ const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
485
+ /**
486
+ * Probe the published tunnel URL and report who is on the other end.
487
+ *
488
+ * `fetchImpl` is injectable so tests do not need a network or a live tunnel.
489
+ */
490
+ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
491
+ const controller = new AbortController();
492
+ const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
493
+ try {
494
+ return (await fetchImpl(triggerUrl, {
495
+ method: "POST",
496
+ headers: { "content-type": "application/json" },
497
+ body: "{}",
498
+ signal: controller.signal
499
+ })).status === 401 ? "clivly" : "mismatch";
500
+ } catch {
501
+ return "unknown";
502
+ } finally {
503
+ clearTimeout(timer);
504
+ }
505
+ }
506
+ /**
507
+ * What to tell someone whose tunnel reaches the wrong process.
508
+ *
509
+ * Names the likely cause rather than only the symptom: the overwhelmingly
510
+ * common reason is the port fallback above, and the remedy is to say which
511
+ * port the app really bound.
512
+ */
513
+ function tunnelMismatchMessage(port) {
514
+ return [
515
+ `Warning: the tunnel reaches something on port ${port}, but it is not this app's Clivly route.`,
516
+ ` The usual cause is a port fallback. If your dev server printed "Port ${port} is in use,`,
517
+ ` using available port …", it moved and the tunnel stayed on ${port} — where another`,
518
+ " project is still listening.",
519
+ " Re-run with the port it actually bound: npx clivly dev --port <port>",
520
+ " Until then, dashboard-triggered syncs will not reach this app."
521
+ ].join("\n");
522
+ }
523
+ //#endregion
392
524
  //#region src/dev.ts
393
525
  const UNREACHABLE_RESULT = {
394
526
  ok: false,
@@ -404,10 +536,12 @@ function explicitCommand(argv) {
404
536
  }
405
537
  async function runDev(options) {
406
538
  const { deps } = options;
407
- const { target, triggerPath } = resolveDevTarget({
539
+ const framework = deps.detectFramework?.(options.cwd);
540
+ const { target, triggerPath, port } = resolveDevTarget({
408
541
  url: flagValue$1(options.argv, "--url"),
409
542
  port: flagValue$1(options.argv, "--port"),
410
- routePath: flagValue$1(options.argv, "--route-path")
543
+ routePath: flagValue$1(options.argv, "--route-path"),
544
+ framework
411
545
  });
412
546
  const command = detectDevCommand(deps.readScripts(options.cwd), explicitCommand(options.argv));
413
547
  if (!deps.hasCloudflared()) {
@@ -424,6 +558,7 @@ async function runDev(options) {
424
558
  deps.out("CLIVLY_SECRET_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
425
559
  return 1;
426
560
  }
561
+ if (isViteFramework(framework)) deps.out(viteAllowedHostsHint());
427
562
  const app = command ? deps.spawnApp(command) : null;
428
563
  const tunnel = deps.spawnTunnel(target);
429
564
  let shuttingDown = false;
@@ -431,8 +566,13 @@ async function runDev(options) {
431
566
  if (shuttingDown) return;
432
567
  const triggerUrl = buildTriggerUrl(origin, triggerPath);
433
568
  deps.out(`Tunnel: ${triggerUrl}`);
569
+ if (deps.isPortListening && !flagValue$1(options.argv, "--url")) {
570
+ if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
571
+ }
572
+ const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
573
+ if (identity === "mismatch") deps.out(tunnelMismatchMessage(port));
434
574
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
435
- if (result.ok) deps.out("Dashboard-triggered sync now points at this local session.");
575
+ if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
436
576
  else deps.out(overrideFailureMessage(result.kind, result.status));
437
577
  });
438
578
  const stopWatching = deps.watchConfig?.(options.cwd, () => {
@@ -1809,6 +1949,10 @@ const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentati
1809
1949
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
1810
1950
  import clivly from "./clivly.config";
1811
1951
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
1952
+ const MISSING_VITE_CONFIG_STEP = `Could not find a vite config, so nothing starts Clivly when your app boots. Add the plugin to your Vite config:
1953
+ import { clivlyVite } from "clivly/vite";
1954
+ import clivly from "./clivly.config";
1955
+ // then, inside defineConfig: plugins: [clivlyVite(clivly), …]`;
1812
1956
  /** The boot hook as a scaffold entry, or null when this framework is patched. */
1813
1957
  function planBootHook(framework, layout) {
1814
1958
  const path = bootHookPath(framework, layout);
@@ -2402,7 +2546,7 @@ function planBootPatch(args) {
2402
2546
  }
2403
2547
  return {
2404
2548
  file: null,
2405
- manual: null
2549
+ manual: MISSING_VITE_CONFIG_STEP
2406
2550
  };
2407
2551
  }
2408
2552
  function planMutations(args) {
@@ -2426,6 +2570,43 @@ function planWidgetMutations(args) {
2426
2570
  });
2427
2571
  }
2428
2572
  //#endregion
2573
+ //#region src/port-probe.ts
2574
+ const PORT_PROBE_TIMEOUT_MS = 1500;
2575
+ /** One connect attempt against a single address family. */
2576
+ function connectsOn(port, host) {
2577
+ return new Promise((resolve) => {
2578
+ const socket = new node_net.Socket();
2579
+ let settled = false;
2580
+ const finish = (listening) => {
2581
+ if (settled) return;
2582
+ settled = true;
2583
+ socket.destroy();
2584
+ resolve(listening);
2585
+ };
2586
+ socket.setTimeout(PORT_PROBE_TIMEOUT_MS);
2587
+ socket.once("connect", () => finish(true));
2588
+ socket.once("timeout", () => finish(false));
2589
+ socket.once("error", () => finish(false));
2590
+ socket.connect(Number(port), host);
2591
+ });
2592
+ }
2593
+ /**
2594
+ * Whether anything is accepting TCP connections on a local port.
2595
+ *
2596
+ * Deliberately a bare socket connect rather than an HTTP request: the dev
2597
+ * server may legitimately answer 404/403/500 on `/`, and any of those still
2598
+ * proves something is listening. Only a refused or timed-out connection means
2599
+ * the tunnel is pointed at nothing.
2600
+ *
2601
+ * Both address families are tried, because Vite binds `::1` ONLY by default —
2602
+ * so an IPv4-only probe reports "nothing is listening" while the dev server is
2603
+ * running perfectly. A false warning here is worse than none: it sends the
2604
+ * developer to fix a port that was already correct.
2605
+ */
2606
+ function isLocalPortListening(port) {
2607
+ return Promise.all([connectsOn(port, "127.0.0.1"), connectsOn(port, "::1")]).then((results) => results.some(Boolean));
2608
+ }
2609
+ //#endregion
2429
2610
  //#region src/quiet-host-output.ts
2430
2611
  /**
2431
2612
  * Run `fn` with the process's stdout/stderr captured. Always restores the
@@ -4005,6 +4186,9 @@ function defaultDevDeps(options) {
4005
4186
  spawnTunnel: spawnCloudflaredTunnel,
4006
4187
  spawnApp: spawnAppProcess,
4007
4188
  readScripts: readPackageScripts,
4189
+ detectFramework: (cwd) => detectFramework(readDeps(cwd)),
4190
+ isPortListening: isLocalPortListening,
4191
+ probeTunnel: (triggerUrl) => probeTunnelIdentity(triggerUrl),
4008
4192
  register: (triggerUrl) => registerTunnelOverride({
4009
4193
  apiKey: options.apiKey ?? "",
4010
4194
  apiUrl: options.apiUrl,
package/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { t as CANONICAL_FIELDS } from "./entity-config-C4d3K9CP.mjs";
2
+ import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.mjs";
3
3
  import "./mapping-score-C68uAqGB.mjs";
4
4
  import { hasDefiningColumn, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
- import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-D6gX0TXS.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
@@ -11,6 +11,7 @@ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "no
11
11
  import { createInterface } from "node:readline/promises";
12
12
  import { createJiti } from "jiti";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import { Socket } from "node:net";
14
15
  //#region ../core/src/env-file.ts
15
16
  const ENV_FILES = {
16
17
  next: ".env.local",
@@ -332,6 +333,28 @@ const TRIGGER_URL_PLACEHOLDER = `https://<yourDomain.com>${CLIVLY_TICK_PATH}`;
332
333
  //#region src/dev-tunnel.ts
333
334
  const CLOUDFLARE_QUICK_TUNNEL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
334
335
  const DEFAULT_PORT = "3000";
336
+ /**
337
+ * The port each framework's dev server actually listens on out of the box.
338
+ *
339
+ * `clivly dev` used to tunnel to 3000 for everyone. Vite-based frameworks start
340
+ * on 5173, so the tunnel pointed at whatever else happened to hold 3000 — in one
341
+ * smoke run, a *different app entirely*, which answered, which made the tunnel
342
+ * look healthy while syncing the wrong project. Every symptom pointed away from
343
+ * the cause.
344
+ *
345
+ * These are defaults, not detections: a project that sets its own port must
346
+ * still pass `--port`. Guessing right for the untouched case is the win.
347
+ */
348
+ const FRAMEWORK_DEFAULT_PORTS = {
349
+ remix: "5173",
350
+ sveltekit: "5173",
351
+ "tanstack-start": "5173",
352
+ nextjs: "3000",
353
+ nuxt: "3000"
354
+ };
355
+ function defaultPortFor(framework) {
356
+ return (framework && FRAMEWORK_DEFAULT_PORTS[framework]) ?? DEFAULT_PORT;
357
+ }
335
358
  const DEFAULT_ROUTE_PATH = CLIVLY_TICK_PATH;
336
359
  function parseTunnelUrl(chunk) {
337
360
  const match = chunk.match(CLOUDFLARE_QUICK_TUNNEL_REGEX);
@@ -341,11 +364,50 @@ function buildTriggerUrl(origin, path) {
341
364
  return `${origin.endsWith("/") ? origin.slice(0, -1) : origin}${path.startsWith("/") ? path : `/${path}`}`;
342
365
  }
343
366
  function resolveDevTarget(flags) {
367
+ const port = flags.port ?? defaultPortFor(flags.framework);
344
368
  return {
345
- target: flags.url ?? `http://localhost:${flags.port ?? DEFAULT_PORT}`,
346
- triggerPath: flags.routePath ?? DEFAULT_ROUTE_PATH
369
+ target: flags.url ?? `http://localhost:${port}`,
370
+ triggerPath: flags.routePath ?? DEFAULT_ROUTE_PATH,
371
+ port
347
372
  };
348
373
  }
374
+ const VITE_FRAMEWORKS = /* @__PURE__ */ new Set([
375
+ "nuxt",
376
+ "remix",
377
+ "sveltekit",
378
+ "tanstack-start"
379
+ ]);
380
+ function isViteFramework(framework) {
381
+ return framework ? VITE_FRAMEWORKS.has(framework) : false;
382
+ }
383
+ /**
384
+ * The one config line that makes a Vite dev server answer the tunnel.
385
+ *
386
+ * Without it every sync fails with a bare "The host app returned 403" and the
387
+ * explanation — Vite's own, naming this exact setting — is buried in a
388
+ * diagnostics column. This affects four of the five supported frameworks, so
389
+ * `clivly dev` says it up front rather than letting each developer rediscover
390
+ * it. Printed unconditionally for Vite apps: reading the app's vite config well
391
+ * enough to know whether it is already set means parsing arbitrary TypeScript,
392
+ * and a redundant hint is far cheaper than a silent 403.
393
+ */
394
+ function viteAllowedHostsHint() {
395
+ return [
396
+ "Vite blocks unknown hosts, so the tunnel needs one line in your Vite config:",
397
+ " server: { allowedHosts: [\".trycloudflare.com\"] }",
398
+ "Without it, sync fails with 403 before it reaches your app."
399
+ ].join("\n");
400
+ }
401
+ /**
402
+ * Warns that nothing is listening on the port about to be tunnelled.
403
+ *
404
+ * A quick tunnel to a dead port still resolves and still answers, so the failure
405
+ * surfaces later as an HTTP error attributed to the host app. Naming it here —
406
+ * while the developer is still looking at `clivly dev` output — costs one line.
407
+ */
408
+ function deadPortMessage(port, framework) {
409
+ return [`Warning: nothing is listening on port ${port} yet, but the tunnel points there.${framework ? ` ${FRAMEWORK_LABELS[framework]} usually serves on ${defaultPortFor(framework)}.` : ""}`, "If your dev server uses a different port, restart with: clivly dev --port <port>"].join("\n");
410
+ }
349
411
  function detectDevCommand(scripts, explicit) {
350
412
  if (explicit && explicit.length > 0) return explicit;
351
413
  if (scripts.dev) return [
@@ -388,6 +450,76 @@ function cloudflaredMissingMessage() {
388
450
  ].join("\n");
389
451
  }
390
452
  //#endregion
453
+ //#region src/tunnel-identity.ts
454
+ /**
455
+ * Whether the published tunnel actually reaches *this* app's Clivly route.
456
+ *
457
+ * `isLocalPortListening` answers a different question — "is anything accepting
458
+ * connections on this port?" — and that is exactly the gap this closes. On a
459
+ * machine running more than one project (which is what `clivly dev` is for),
460
+ * a framework that finds its default port taken silently moves to the next one:
461
+ *
462
+ * ⚠ Port 3000 is in use by process 92495, using available port 3001 instead
463
+ *
464
+ * The tunnel is still pointed at 3000, where a *different application* is
465
+ * listening. The port probe sees a healthy socket and says nothing, and the CLI
466
+ * goes on to promise "Dashboard-triggered sync now points at this local
467
+ * session" — which is false. Signed triggers for this workspace are delivered
468
+ * to somebody else's app, and the resulting failure is reported against the
469
+ * innocent one. Liveness is not identity.
470
+ *
471
+ * The check: POST to the trigger path with no signature. A Clivly-mounted route
472
+ * rejects that with **401** — it read the request, found no valid signature and
473
+ * refused. Anything else on that port answers some other way (a framework's
474
+ * 500 error page, a 404, an unrelated 200). We are not testing authentication
475
+ * here; the 401 *is* the fingerprint, and it is one no unrelated service
476
+ * produces at this exact path by accident.
477
+ *
478
+ * Deliberately tolerant: a network hiccup returns `unknown`, never `mismatch`.
479
+ * Warning someone away from a tunnel that was fine is worse than staying quiet,
480
+ * for the same reason the port probe defaults to assuming the port is alive.
481
+ */
482
+ /** How long to wait before giving up and returning `unknown`. */
483
+ const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
484
+ /**
485
+ * Probe the published tunnel URL and report who is on the other end.
486
+ *
487
+ * `fetchImpl` is injectable so tests do not need a network or a live tunnel.
488
+ */
489
+ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
490
+ const controller = new AbortController();
491
+ const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
492
+ try {
493
+ return (await fetchImpl(triggerUrl, {
494
+ method: "POST",
495
+ headers: { "content-type": "application/json" },
496
+ body: "{}",
497
+ signal: controller.signal
498
+ })).status === 401 ? "clivly" : "mismatch";
499
+ } catch {
500
+ return "unknown";
501
+ } finally {
502
+ clearTimeout(timer);
503
+ }
504
+ }
505
+ /**
506
+ * What to tell someone whose tunnel reaches the wrong process.
507
+ *
508
+ * Names the likely cause rather than only the symptom: the overwhelmingly
509
+ * common reason is the port fallback above, and the remedy is to say which
510
+ * port the app really bound.
511
+ */
512
+ function tunnelMismatchMessage(port) {
513
+ return [
514
+ `Warning: the tunnel reaches something on port ${port}, but it is not this app's Clivly route.`,
515
+ ` The usual cause is a port fallback. If your dev server printed "Port ${port} is in use,`,
516
+ ` using available port …", it moved and the tunnel stayed on ${port} — where another`,
517
+ " project is still listening.",
518
+ " Re-run with the port it actually bound: npx clivly dev --port <port>",
519
+ " Until then, dashboard-triggered syncs will not reach this app."
520
+ ].join("\n");
521
+ }
522
+ //#endregion
391
523
  //#region src/dev.ts
392
524
  const UNREACHABLE_RESULT = {
393
525
  ok: false,
@@ -403,10 +535,12 @@ function explicitCommand(argv) {
403
535
  }
404
536
  async function runDev(options) {
405
537
  const { deps } = options;
406
- const { target, triggerPath } = resolveDevTarget({
538
+ const framework = deps.detectFramework?.(options.cwd);
539
+ const { target, triggerPath, port } = resolveDevTarget({
407
540
  url: flagValue$1(options.argv, "--url"),
408
541
  port: flagValue$1(options.argv, "--port"),
409
- routePath: flagValue$1(options.argv, "--route-path")
542
+ routePath: flagValue$1(options.argv, "--route-path"),
543
+ framework
410
544
  });
411
545
  const command = detectDevCommand(deps.readScripts(options.cwd), explicitCommand(options.argv));
412
546
  if (!deps.hasCloudflared()) {
@@ -423,6 +557,7 @@ async function runDev(options) {
423
557
  deps.out("CLIVLY_SECRET_KEY is not set — cannot register the tunnel URL. Set it and re-run.");
424
558
  return 1;
425
559
  }
560
+ if (isViteFramework(framework)) deps.out(viteAllowedHostsHint());
426
561
  const app = command ? deps.spawnApp(command) : null;
427
562
  const tunnel = deps.spawnTunnel(target);
428
563
  let shuttingDown = false;
@@ -430,8 +565,13 @@ async function runDev(options) {
430
565
  if (shuttingDown) return;
431
566
  const triggerUrl = buildTriggerUrl(origin, triggerPath);
432
567
  deps.out(`Tunnel: ${triggerUrl}`);
568
+ if (deps.isPortListening && !flagValue$1(options.argv, "--url")) {
569
+ if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
570
+ }
571
+ const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
572
+ if (identity === "mismatch") deps.out(tunnelMismatchMessage(port));
433
573
  const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
434
- if (result.ok) deps.out("Dashboard-triggered sync now points at this local session.");
574
+ if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
435
575
  else deps.out(overrideFailureMessage(result.kind, result.status));
436
576
  });
437
577
  const stopWatching = deps.watchConfig?.(options.cwd, () => {
@@ -1808,6 +1948,10 @@ const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentati
1808
1948
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
1809
1949
  import clivly from "./clivly.config";
1810
1950
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
1951
+ const MISSING_VITE_CONFIG_STEP = `Could not find a vite config, so nothing starts Clivly when your app boots. Add the plugin to your Vite config:
1952
+ import { clivlyVite } from "clivly/vite";
1953
+ import clivly from "./clivly.config";
1954
+ // then, inside defineConfig: plugins: [clivlyVite(clivly), …]`;
1811
1955
  /** The boot hook as a scaffold entry, or null when this framework is patched. */
1812
1956
  function planBootHook(framework, layout) {
1813
1957
  const path = bootHookPath(framework, layout);
@@ -2401,7 +2545,7 @@ function planBootPatch(args) {
2401
2545
  }
2402
2546
  return {
2403
2547
  file: null,
2404
- manual: null
2548
+ manual: MISSING_VITE_CONFIG_STEP
2405
2549
  };
2406
2550
  }
2407
2551
  function planMutations(args) {
@@ -2425,6 +2569,43 @@ function planWidgetMutations(args) {
2425
2569
  });
2426
2570
  }
2427
2571
  //#endregion
2572
+ //#region src/port-probe.ts
2573
+ const PORT_PROBE_TIMEOUT_MS = 1500;
2574
+ /** One connect attempt against a single address family. */
2575
+ function connectsOn(port, host) {
2576
+ return new Promise((resolve) => {
2577
+ const socket = new Socket();
2578
+ let settled = false;
2579
+ const finish = (listening) => {
2580
+ if (settled) return;
2581
+ settled = true;
2582
+ socket.destroy();
2583
+ resolve(listening);
2584
+ };
2585
+ socket.setTimeout(PORT_PROBE_TIMEOUT_MS);
2586
+ socket.once("connect", () => finish(true));
2587
+ socket.once("timeout", () => finish(false));
2588
+ socket.once("error", () => finish(false));
2589
+ socket.connect(Number(port), host);
2590
+ });
2591
+ }
2592
+ /**
2593
+ * Whether anything is accepting TCP connections on a local port.
2594
+ *
2595
+ * Deliberately a bare socket connect rather than an HTTP request: the dev
2596
+ * server may legitimately answer 404/403/500 on `/`, and any of those still
2597
+ * proves something is listening. Only a refused or timed-out connection means
2598
+ * the tunnel is pointed at nothing.
2599
+ *
2600
+ * Both address families are tried, because Vite binds `::1` ONLY by default —
2601
+ * so an IPv4-only probe reports "nothing is listening" while the dev server is
2602
+ * running perfectly. A false warning here is worse than none: it sends the
2603
+ * developer to fix a port that was already correct.
2604
+ */
2605
+ function isLocalPortListening(port) {
2606
+ return Promise.all([connectsOn(port, "127.0.0.1"), connectsOn(port, "::1")]).then((results) => results.some(Boolean));
2607
+ }
2608
+ //#endregion
2428
2609
  //#region src/quiet-host-output.ts
2429
2610
  /**
2430
2611
  * Run `fn` with the process's stdout/stderr captured. Always restores the
@@ -4004,6 +4185,9 @@ function defaultDevDeps(options) {
4004
4185
  spawnTunnel: spawnCloudflaredTunnel,
4005
4186
  spawnApp: spawnAppProcess,
4006
4187
  readScripts: readPackageScripts,
4188
+ detectFramework: (cwd) => detectFramework(readDeps(cwd)),
4189
+ isPortListening: isLocalPortListening,
4190
+ probeTunnel: (triggerUrl) => probeTunnelIdentity(triggerUrl),
4007
4191
  register: (triggerUrl) => registerTunnelOverride({
4008
4192
  apiKey: options.apiKey ?? "",
4009
4193
  apiUrl: options.apiUrl,
@@ -14,6 +14,14 @@ interface RunFact {
14
14
  at: number;
15
15
  message?: string;
16
16
  ok: boolean;
17
+ /**
18
+ * The HTTP status the host answered with, when there was one.
19
+ *
20
+ * Absent for a run that failed before any response existed, and for callers
21
+ * that do not track it — so every read must treat `undefined` as "unknown",
22
+ * never as "fine".
23
+ */
24
+ status?: number;
17
25
  }
18
26
  /**
19
27
  * Where the reporting app is running, as the app itself understands it.
@@ -44,6 +52,13 @@ interface IntegrationFacts {
44
52
  heartbeatIntervalMs?: number | null;
45
53
  lastHeartbeatAt: number | null;
46
54
  lastRun: RunFact | null;
55
+ /**
56
+ * Whether any entity is mapped, expressed as a number for historical
57
+ * reasons — every producer sets it to 0 or 1, never a real total. Read it as
58
+ * a flag (`> 0`) and never render it as a quantity: doing so reported "1
59
+ * entity mapped" to a workspace with three. The true count lives on the
60
+ * mappings list, which the What syncs tab reads directly.
61
+ */
47
62
  mappedEntityCount: number;
48
63
  now: number;
49
64
  /**
@@ -14,6 +14,14 @@ interface RunFact {
14
14
  at: number;
15
15
  message?: string;
16
16
  ok: boolean;
17
+ /**
18
+ * The HTTP status the host answered with, when there was one.
19
+ *
20
+ * Absent for a run that failed before any response existed, and for callers
21
+ * that do not track it — so every read must treat `undefined` as "unknown",
22
+ * never as "fine".
23
+ */
24
+ status?: number;
17
25
  }
18
26
  /**
19
27
  * Where the reporting app is running, as the app itself understands it.
@@ -44,6 +52,13 @@ interface IntegrationFacts {
44
52
  heartbeatIntervalMs?: number | null;
45
53
  lastHeartbeatAt: number | null;
46
54
  lastRun: RunFact | null;
55
+ /**
56
+ * Whether any entity is mapped, expressed as a number for historical
57
+ * reasons — every producer sets it to 0 or 1, never a real total. Read it as
58
+ * a flag (`> 0`) and never render it as a quantity: doing so reported "1
59
+ * entity mapped" to a workspace with three. The true count lives on the
60
+ * mappings list, which the What syncs tab reads directly.
61
+ */
47
62
  mappedEntityCount: number;
48
63
  now: number;
49
64
  /**
@@ -1,4 +1,4 @@
1
- import { F as ProjectionMapping, P as ProjectionJoin, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-DNQAMbMS.mjs";
1
+ import { L as ProjectionJoin, R as ProjectionMapping, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.cjs";
2
2
  //#region ../core/src/projection-cardinality.d.ts
3
3
  declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
4
4
  /** Every join in the projection that can multiply rows. */
@@ -1,4 +1,4 @@
1
- import { F as ProjectionMapping, P as ProjectionJoin, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-DNQAMbMS.cjs";
1
+ import { L as ProjectionJoin, R as ProjectionMapping, h as FilterExpr, l as ConfigValidationError, m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.mjs";
2
2
  //#region ../core/src/projection-cardinality.d.ts
3
3
  declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
4
4
  /** Every join in the projection that can multiply rows. */
@@ -1,2 +1,2 @@
1
- import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-DTOuGXEn.cjs";
1
+ import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-SD8WCvYj.cjs";
2
2
  export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };
@@ -1,2 +1,2 @@
1
- import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-SGapoly2.mjs";
1
+ import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-axugRJ0u.mjs";
2
2
  export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };