clivly 0.9.3 → 0.9.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 (60) hide show
  1. package/README.md +23 -6
  2. package/core-entity-config.cjs +1 -1
  3. package/core-entity-config.mjs +1 -1
  4. package/core-mappings-config.cjs +1 -1
  5. package/core-mappings-config.mjs +1 -1
  6. package/core-sync-engine.cjs +1 -1
  7. package/core-sync-engine.mjs +1 -1
  8. package/core-view-compiler.cjs +1 -1
  9. package/core-view-compiler.mjs +1 -1
  10. package/core.cjs +11 -6
  11. package/core.d.cts +2 -2
  12. package/core.d.mts +2 -2
  13. package/core.mjs +8 -7
  14. package/drizzle.cjs +74 -9
  15. package/drizzle.d.cts +83 -7
  16. package/drizzle.d.mts +83 -7
  17. package/drizzle.mjs +74 -10
  18. package/{entity-config-DtBsl2MQ.cjs → entity-config-7sGCk_nw.cjs} +1 -1
  19. package/{entity-config-kUHri9yA.mjs → entity-config-B74SPOUZ.mjs} +1 -1
  20. package/index.cjs +180 -21
  21. package/index.mjs +180 -21
  22. package/{internal-Df-PtOH_.mjs → internal-CykXpEGk.mjs} +2 -2
  23. package/{internal-CwnEG6Wl.cjs → internal-DqduZU3l.cjs} +2 -2
  24. package/internal-entity-config.cjs +1 -1
  25. package/internal-entity-config.mjs +1 -1
  26. package/internal-entity-heuristics.cjs +1 -1
  27. package/internal-entity-heuristics.mjs +1 -1
  28. package/internal-mappings-config.cjs +1 -1
  29. package/internal-mappings-config.mjs +1 -1
  30. package/{internal-CDY8g9Ic.d.mts → internal-o2M7FGlR.d.mts} +29 -1
  31. package/internal-sync-engine.cjs +1 -1
  32. package/internal-sync-engine.mjs +1 -1
  33. package/internal-view-compiler.cjs +1 -1
  34. package/internal-view-compiler.mjs +1 -1
  35. package/{internal-Da_dlcQ_.d.cts → internal-wCXSELDN.d.cts} +29 -1
  36. package/internal.cjs +11 -6
  37. package/internal.d.cts +2 -2
  38. package/internal.d.mts +2 -2
  39. package/internal.mjs +8 -7
  40. package/{mappings-config-B0RqFLxu.cjs → mappings-config-CLyEg0sf.cjs} +1 -1
  41. package/{mappings-config-B1b1HVBg.mjs → mappings-config-DQS_s4dg.mjs} +1 -1
  42. package/{namespace-probe-EYyxWZnf.d.cts → namespace-probe-BoGI18dQ.d.cts} +8 -0
  43. package/{namespace-probe-BmdGnYMn.d.mts → namespace-probe-Cn7voAd0.d.mts} +8 -0
  44. package/package.json +1 -1
  45. package/{projection-D7xmQP4S.mjs → projection-CSGJbZlZ.mjs} +25 -4
  46. package/{projection-C5244vGa.cjs → projection-Dz6_i1-w.cjs} +25 -4
  47. package/projection-sql-BgnfiGqF.mjs +65 -0
  48. package/projection-sql-Cl1qIn8m.cjs +94 -0
  49. package/{sdk-COLUHhJq.cjs → sdk-CM1UO-I7.cjs} +58 -15
  50. package/{sdk-CZNXBsgA.mjs → sdk-DYBDR939.mjs} +53 -16
  51. package/sdk.cjs +3 -2
  52. package/sdk.d.cts +5 -2
  53. package/sdk.d.mts +5 -2
  54. package/sdk.mjs +3 -3
  55. package/{sync-engine-abE94g6X.cjs → sync-engine-C4A5jgA4.cjs} +1 -1
  56. package/{sync-engine-SuxSzbZ4.mjs → sync-engine-Dt5QQSKO.mjs} +1 -1
  57. package/{view-compiler-Blu4OXJ7.cjs → view-compiler-B5nDal14.cjs} +21 -74
  58. package/{view-compiler-CQx8TRrW.mjs → view-compiler-CSYwEWf5.mjs} +2 -55
  59. package/vite.d.cts +1 -1
  60. package/vite.d.mts +1 -1
package/drizzle.mjs CHANGED
@@ -1,6 +1,77 @@
1
+ import { t as BASE_REF } from "./projection-CSGJbZlZ.mjs";
1
2
  import { t as DRIZZLE_META } from "./drizzle-meta-D-5k3n0i.mjs";
3
+ import { a as quoteIdent, n as compileProjectionIdentity, r as compileProjectionJoins, t as compileProjectionFrom } from "./projection-sql-BgnfiGqF.mjs";
2
4
  import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-3kvhkwO7.mjs";
3
5
  import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
6
+ //#region ../sdk/src/drizzle-projection.ts
7
+ /** `<ref>.<column>` — the token `fields` carries and cloud-ingest reads. */
8
+ function refToken(ref) {
9
+ return `${ref.table}.${ref.column}`;
10
+ }
11
+ function refSql(ref) {
12
+ return `${quoteIdent(ref.table)}.${quoteIdent(ref.column)}`;
13
+ }
14
+ function parseRef(value) {
15
+ if (typeof value !== "string") return value;
16
+ const dot = value.indexOf(".");
17
+ if (dot <= 0 || dot === value.length - 1) throw new Error(`fromDrizzleProjection: cursorField "${value}" must be a ref token like "base.updated_at".`);
18
+ return {
19
+ table: value.slice(0, dot),
20
+ column: value.slice(dot + 1)
21
+ };
22
+ }
23
+ /**
24
+ * Refs address tables symbolically — `base` or a join key — never by table name,
25
+ * which is what stops a projection reaching a table it did not declare. An
26
+ * unresolvable ref would otherwise compile to SQL referencing a missing alias
27
+ * and fail at the database with a message that never mentions the config.
28
+ */
29
+ function assertKnownRef(ref, known, context) {
30
+ if (!known.has(ref.table)) throw new Error(`fromDrizzleProjection: ${context} names unknown ref "${ref.table}" — expected "${[...known].join("\", \"")}".`);
31
+ }
32
+ function normalizeRows(result) {
33
+ if (Array.isArray(result)) return result;
34
+ const rows = result.rows;
35
+ if (Array.isArray(rows)) return rows;
36
+ throw new Error("fromDrizzleProjection: db.execute() returned neither an array of rows nor { rows }.");
37
+ }
38
+ function fromDrizzleProjection(db, projection, opts) {
39
+ const client = db;
40
+ const known = /* @__PURE__ */ new Set([BASE_REF, ...projection.joins.map((j) => j.key)]);
41
+ if (projection.identity.kind === "base_pk" && projection.identity.column === void 0 && opts.basePkColumn === void 0) throw new Error(`fromDrizzleProjection: projection "${projection.entityKey}" has a base_pk identity with no column — pass { basePkColumn }.`);
42
+ const cursorRef = parseRef(opts.cursorField);
43
+ assertKnownRef(cursorRef, known, "cursorField");
44
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) assertKnownRef(binding.from, known, `field "${field}"`);
45
+ const identityExpr = compileProjectionIdentity(projection, opts.basePkColumn ?? "");
46
+ const cursorToken = refToken(cursorRef);
47
+ const cursorExpr = `CAST(${refSql(cursorRef)} AS timestamptz)`;
48
+ const cursorRead = `to_char(${cursorExpr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
49
+ const selectItems = [`${identityExpr} AS ${quoteIdent("id")}`, `${cursorRead} AS ${quoteIdent(cursorToken)}`];
50
+ const emitted = /* @__PURE__ */ new Set([cursorToken]);
51
+ for (const binding of Object.values(projection.fieldBindings)) {
52
+ const token = refToken(binding.from);
53
+ if (emitted.has(token)) continue;
54
+ emitted.add(token);
55
+ selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(token)}`);
56
+ }
57
+ const head = [
58
+ `select ${selectItems.join(", ")}`,
59
+ compileProjectionFrom(projection),
60
+ ...compileProjectionJoins(projection)
61
+ ].join(" ");
62
+ return {
63
+ entity: opts.entity,
64
+ cursorField: cursorToken,
65
+ idField: "id",
66
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
67
+ fetchPage: async ({ since, limit }) => {
68
+ const where = since ? sql` where (${sql.raw(cursorExpr)}, ${sql.raw(identityExpr)}) > (${since.time.toISOString()}::timestamptz, ${since.id})` : sql``;
69
+ const query = sql`${sql.raw(head)}${where} order by ${sql.raw(cursorExpr)} asc, ${sql.raw(identityExpr)} asc limit ${limit}`;
70
+ return normalizeRows(await client.execute(query));
71
+ }
72
+ };
73
+ }
74
+ //#endregion
4
75
  //#region ../sdk/src/drizzle.ts
5
76
  function inferIdField(table) {
6
77
  const columns = getTableColumns(table);
@@ -13,14 +84,6 @@ function inferIdField(table) {
13
84
  const kind = primaryKeys.length === 0 ? "no" : "a composite";
14
85
  throw new Error(`fromDrizzle: table "${getTableName(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
15
86
  }
16
- /**
17
- * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
18
- * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
19
- * `idField` defaults to the table's single-column primary key.
20
- *
21
- * `drizzle-orm` is an optional peer dependency — installed only when you import
22
- * `@clivly/sdk/drizzle`.
23
- */
24
87
  function fromDrizzle(db, table, opts) {
25
88
  const drizzleDb = db;
26
89
  const idField = opts.idField ?? inferIdField(table);
@@ -37,7 +100,8 @@ function fromDrizzle(db, table, opts) {
37
100
  entity: opts.entity,
38
101
  cursorField: cursorColumnName,
39
102
  idField: idColumnName,
40
- fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit)
103
+ fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit),
104
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType }
41
105
  };
42
106
  const uniqueConstraints = buildUniqueConstraints(table);
43
107
  const meta = {
@@ -74,4 +138,4 @@ function drizzleIntrospector(db) {
74
138
  };
75
139
  }
76
140
  //#endregion
77
- export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
141
+ export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
@@ -1,4 +1,4 @@
1
- const require_projection = require("./projection-C5244vGa.cjs");
1
+ const require_projection = require("./projection-Dz6_i1-w.cjs");
2
2
  //#region ../core/src/schema-meta.ts
3
3
  /**
4
4
  * Accessors over a discovered table's schema-v2 metadata.
@@ -1,4 +1,4 @@
1
- import { a as _enum, c as literal, d as strictObject, f as string, i as projectionSchema, l as number, o as array, p as union, r as identityRefs, s as boolean, t as BASE_REF, u as record } from "./projection-D7xmQP4S.mjs";
1
+ import { a as _enum, c as literal, d as strictObject, f as string, i as projectionSchema, l as number, o as array, p as union, r as identityRefs, s as boolean, t as BASE_REF, u as record } from "./projection-CSGJbZlZ.mjs";
2
2
  //#region ../core/src/schema-meta.ts
3
3
  /**
4
4
  * Accessors over a discovered table's schema-v2 metadata.
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-DtBsl2MQ.cjs");
3
+ const require_entity_config = require("./entity-config-7sGCk_nw.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-COLUHhJq.cjs");
6
+ const require_sdk = require("./sdk-CM1UO-I7.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");
@@ -292,10 +292,37 @@ function addCommand(pm, packages) {
292
292
  };
293
293
  }
294
294
  //#endregion
295
+ //#region src/tick-route.ts
296
+ /**
297
+ * The tick route's public URL path, and the template `clivly login` writes for
298
+ * it.
299
+ *
300
+ * Every framework the CLI scaffolds mounts the tick handler at the same URL —
301
+ * the *files* differ (`app/api/clivly/tick/route.ts`,
302
+ * `src/routes/api/clivly/tick.ts`, `app/routes/api.clivly.tick.ts`, …) but each
303
+ * one resolves to `/api/clivly/tick`. So this is a constant, not a per-framework
304
+ * lookup, and `clivly dev`, the generated config and the `.env` template can all
305
+ * read from one place instead of repeating the string and drifting.
306
+ */
307
+ const CLIVLY_TICK_PATH = "/api/clivly/tick";
308
+ /**
309
+ * What `clivly login` writes into `.env` for `CLIVLY_SYNC_TRIGGER_URL`.
310
+ *
311
+ * A visible template beats an empty line: the developer sees the shape they
312
+ * eventually need — a public origin plus the tick path — instead of an unhelpful
313
+ * `CLIVLY_SYNC_TRIGGER_URL=` they have to go and look up.
314
+ *
315
+ * It is safe to ship a truthy non-address here only because the SDK refuses to
316
+ * treat a bracketed value as configured (`isPlaceholderUrl` in
317
+ * `@clivly/sdk/trigger-url`). Change the brackets here and that guard stops
318
+ * matching, so the two must stay in step.
319
+ */
320
+ const TRIGGER_URL_PLACEHOLDER = `https://<yourDomain.com>${CLIVLY_TICK_PATH}`;
321
+ //#endregion
295
322
  //#region src/dev-tunnel.ts
296
323
  const CLOUDFLARE_QUICK_TUNNEL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
297
324
  const DEFAULT_PORT = "3000";
298
- const DEFAULT_ROUTE_PATH = "/api/clivly/tick";
325
+ const DEFAULT_ROUTE_PATH = CLIVLY_TICK_PATH;
299
326
  function parseTunnelUrl(chunk) {
300
327
  const match = chunk.match(CLOUDFLARE_QUICK_TUNNEL_REGEX);
301
328
  return match ? match[0] : null;
@@ -947,7 +974,7 @@ function isKeySet(contents, key) {
947
974
  function appendEnvKeys(existing, entries) {
948
975
  const missing = entries.filter((e) => !hasKey$1(existing, e.key));
949
976
  if (missing.length === 0) return existing;
950
- const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
977
+ const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=${e.value ?? ""}`).join("\n");
951
978
  return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
952
979
  }
953
980
  /**
@@ -1400,15 +1427,23 @@ async function confirmWorkspace(deps, credentials) {
1400
1427
  return await deps.confirm("Write this workspace's credentials to .env? [y/N] ");
1401
1428
  }
1402
1429
  function credentialEntries(credentials) {
1403
- return [{
1404
- key: SECRET_KEY_ENV,
1405
- value: credentials.apiKey,
1406
- comment: `Org secret key — ${CREDENTIALS_LOCATION}`
1407
- }, {
1408
- key: "CLIVLY_SYNC_TRIGGER_SECRET",
1409
- value: credentials.triggerSecret,
1410
- comment: `Signing secret — ${SYNC_SECRET_LOCATION}`
1411
- }];
1430
+ return [
1431
+ {
1432
+ key: SECRET_KEY_ENV,
1433
+ value: credentials.apiKey,
1434
+ comment: `Org secret key — ${CREDENTIALS_LOCATION}`
1435
+ },
1436
+ {
1437
+ key: "CLIVLY_SYNC_TRIGGER_SECRET",
1438
+ value: credentials.triggerSecret,
1439
+ comment: `Signing secret — ${SYNC_SECRET_LOCATION}`
1440
+ },
1441
+ {
1442
+ key: "CLIVLY_SYNC_TRIGGER_URL",
1443
+ value: TRIGGER_URL_PLACEHOLDER,
1444
+ comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1445
+ }
1446
+ ];
1412
1447
  }
1413
1448
  function writeCredentials(options, credentials) {
1414
1449
  const { deps } = options;
@@ -1775,7 +1810,8 @@ const CLIVLY_ENV_KEYS = [
1775
1810
  },
1776
1811
  {
1777
1812
  key: "CLIVLY_SYNC_TRIGGER_URL",
1778
- comment: "Public URL of your mounted tick route (e.g. https://app.example.com/api/clivly/tick)"
1813
+ value: TRIGGER_URL_PLACEHOLDER,
1814
+ comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1779
1815
  },
1780
1816
  {
1781
1817
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
@@ -1873,7 +1909,13 @@ export default createClivlySDK({
1873
1909
  ${sources.join(",\n")},
1874
1910
  },
1875
1911
  syncTrigger: {
1876
- // The public URL Clivly Cloud calls to trigger a sync — your mounted tick route.
1912
+ // Where Clivly Cloud calls to start a sync — your mounted tick route.
1913
+ //
1914
+ // \`path\` lets the SDK build its own public URL from the hosting platform's
1915
+ // environment (VERCEL_URL, CF_PAGES_URL, RAILWAY_PUBLIC_DOMAIN,
1916
+ // RENDER_EXTERNAL_URL, FLY_APP_NAME), so on those platforms you never have
1917
+ // to set the URL by hand. \`url\` overrides that when you do.
1918
+ path: "${CLIVLY_TICK_PATH}",
1877
1919
  url: process.env.CLIVLY_SYNC_TRIGGER_URL,
1878
1920
  secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
1879
1921
  },
@@ -2700,6 +2742,103 @@ function ladderExitCode(results) {
2700
2742
  return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
2701
2743
  }
2702
2744
  //#endregion
2745
+ //#region src/boot-hook-check.ts
2746
+ const REMIX_ENTRY_CANDIDATES = [
2747
+ "app/entry.server.tsx",
2748
+ "app/entry.server.ts",
2749
+ "app/entry.server.jsx",
2750
+ "app/entry.server.js"
2751
+ ];
2752
+ const NEXT_CONFIG_CANDIDATES = [
2753
+ "next.config.ts",
2754
+ "next.config.mjs",
2755
+ "next.config.js",
2756
+ "next.config.cjs",
2757
+ "next.config.mts"
2758
+ ];
2759
+ const CLIVLY_MENTION = /clivly/i;
2760
+ const INSTRUMENTATION_HOOK_ENABLED = /instrumentationHook\s*:\s*true/;
2761
+ /**
2762
+ * Where this framework's boot hook lives, most likely location first.
2763
+ *
2764
+ * The file-owning frameworks are asked of `boot-hook.ts` rather than restated,
2765
+ * so a path change there cannot leave this check hunting for a file `init` no
2766
+ * longer writes. The other two are patched into a file the app already owns.
2767
+ */
2768
+ function bootSiteCandidates(framework) {
2769
+ const owned = [bootHookPath(framework, "app"), bootHookPath(framework, "src/app")].filter((path) => path !== null);
2770
+ if (owned.length > 0) return [...new Set(owned)];
2771
+ if (framework === "tanstack-start") return [...VITE_CONFIG_CANDIDATES];
2772
+ if (framework === "remix") return REMIX_ENTRY_CANDIDATES;
2773
+ return [];
2774
+ }
2775
+ function declaredDeps(readFile) {
2776
+ const raw = readFile("package.json");
2777
+ if (raw === null) return null;
2778
+ try {
2779
+ const pkg = JSON.parse(raw);
2780
+ return {
2781
+ ...pkg.dependencies,
2782
+ ...pkg.devDependencies
2783
+ };
2784
+ } catch {
2785
+ return null;
2786
+ }
2787
+ }
2788
+ function findBootSite(readFile, framework) {
2789
+ for (const candidate of bootSiteCandidates(framework)) {
2790
+ const source = readFile(candidate);
2791
+ if (source !== null && CLIVLY_MENTION.test(source)) return candidate;
2792
+ }
2793
+ return null;
2794
+ }
2795
+ /**
2796
+ * On Next.js below 15 the hook file is ignored unless the flag is on, so a
2797
+ * present-and-correct instrumentation.ts is not evidence of anything. A missing
2798
+ * next.config is the same failure as one without the flag: either way nothing
2799
+ * switches the hook on.
2800
+ */
2801
+ function nextFlagVerdict(readFile, version, site) {
2802
+ if (!nextNeedsInstrumentationFlag(version)) return null;
2803
+ for (const candidate of NEXT_CONFIG_CANDIDATES) {
2804
+ const source = readFile(candidate);
2805
+ if (source !== null && INSTRUMENTATION_HOOK_ENABLED.test(source)) return null;
2806
+ }
2807
+ return {
2808
+ state: "broken",
2809
+ detail: `${site} exists, but Next.js ${version} ignores it — experimental.instrumentationHook is not enabled in next.config.`,
2810
+ hint: NEXT_INSTRUMENTATION_FLAG_STEP
2811
+ };
2812
+ }
2813
+ function checkBootHook(readFile) {
2814
+ const deps = declaredDeps(readFile);
2815
+ if (deps === null) return {
2816
+ state: "unknown",
2817
+ detail: "package.json not readable — cannot tell how this app boots."
2818
+ };
2819
+ const framework = detectFramework(deps);
2820
+ if (framework === "unknown") return {
2821
+ state: "unknown",
2822
+ detail: "No supported framework detected, so there is no known place to check."
2823
+ };
2824
+ const label = FRAMEWORK_LABELS[framework];
2825
+ const site = findBootSite(readFile, framework);
2826
+ if (site === null) return {
2827
+ state: "broken",
2828
+ detail: `No Clivly boot hook found in this ${label} app.`,
2829
+ hint: `Nothing starts the SDK, so your app never tells Clivly it is running — the dashboard stays silent with no error anywhere. Run \`clivly init\` to scaffold it, or call \`clivly.start()\` where your server boots (${bootSiteCandidates(framework)[0]}).`
2830
+ };
2831
+ const nextVersion = deps.next;
2832
+ if (framework === "nextjs" && nextVersion) {
2833
+ const broken = nextFlagVerdict(readFile, nextVersion, site);
2834
+ if (broken) return broken;
2835
+ }
2836
+ return {
2837
+ state: "ok",
2838
+ detail: site
2839
+ };
2840
+ }
2841
+ //#endregion
2703
2842
  //#region src/edge-safety.ts
2704
2843
  const NODE_BUILTINS = /* @__PURE__ */ new Set([
2705
2844
  "assert",
@@ -2886,7 +3025,7 @@ function pingOnce(ctx) {
2886
3025
  }
2887
3026
  const REASON_HINT = {
2888
3027
  ok: "",
2889
- invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
3028
+ invalid_api_key: "The key was rejected (401). Run `clivly login` to re-pair and rewrite the credentials, and check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY) belongs to the org whose dashboard you are watching.",
2890
3029
  forbidden: "The key is valid but lacks access to this org (403).",
2891
3030
  rate_limited: "Rate limited (429). Wait a moment and retry.",
2892
3031
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
@@ -2986,14 +3125,15 @@ const triggerUrlStage = {
2986
3125
  needs: [configStage.id],
2987
3126
  run(ctx) {
2988
3127
  const url = ctx.sdk?.syncTrigger?.url;
2989
- if (url) return Promise.resolve({
3128
+ if (url && !require_sdk.isPlaceholderUrl(url)) return Promise.resolve({
2990
3129
  state: "pass",
2991
3130
  detail: url
2992
3131
  });
3132
+ const isPlaceholder = url !== void 0;
2993
3133
  return Promise.resolve({
2994
3134
  state: "warn",
2995
- detail: "Not set — remote sync cannot call back into this app.",
2996
- hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
3135
+ detail: isPlaceholder ? `Still the placeholder (${url}) — remote sync cannot call back into this app.` : "Not set — remote sync cannot call back into this app.",
3136
+ hint: "Replace <yourDomain.com> with your deployed origin, or delete the line — on Vercel, Cloudflare Pages, Railway, Render and Fly the SDK derives the URL from the platform. For local development run `clivly dev`: it opens a tunnel and registers the URL for you, outranking whatever is in .env. Push-only setups can ignore this."
2997
3137
  });
2998
3138
  }
2999
3139
  };
@@ -3041,6 +3181,24 @@ const STATUS_STAGES = [
3041
3181
  });
3042
3182
  }
3043
3183
  },
3184
+ {
3185
+ id: "boot-hook",
3186
+ label: "Boot hook wired",
3187
+ run(ctx) {
3188
+ const read = ctx.readConfigSource ?? defaultReadConfigSource;
3189
+ const root = (0, node_path.dirname)(ctx.configPath);
3190
+ const verdict = checkBootHook((relative) => read((0, node_path.resolve)(root, relative)));
3191
+ if (verdict.state === "ok") return Promise.resolve({
3192
+ state: "pass",
3193
+ detail: `starts Clivly from ${verdict.detail}`
3194
+ });
3195
+ return Promise.resolve({
3196
+ state: verdict.state === "unknown" ? "skip" : "fail",
3197
+ detail: verdict.detail,
3198
+ hint: verdict.hint
3199
+ });
3200
+ }
3201
+ },
3044
3202
  sdkChecksStage,
3045
3203
  triggerUrlStage
3046
3204
  ];
@@ -3094,6 +3252,7 @@ const ALIAS_STAGE_IDS = {
3094
3252
  doctor: [
3095
3253
  "config",
3096
3254
  "runtime",
3255
+ "boot-hook",
3097
3256
  "setup-checks"
3098
3257
  ],
3099
3258
  connect: [
@@ -3547,8 +3706,8 @@ function previewMutations(deps, mutations, options) {
3547
3706
  function printNextSteps(out, authComplete, loginRan, envFile) {
3548
3707
  out("\nNext steps:\n");
3549
3708
  out(loginRan ? " 1. Credentials written by `clivly login`\n" : ` 1. Add CLIVLY_SECRET_KEY to ${envFile} — create one at\n ${CREDENTIALS_LOCATION}\n (the legacy CLIVLY_API_KEY is still accepted)\n`);
3550
- out(" 2. Set CLIVLY_SYNC_TRIGGER_URL to your public tick route\n");
3551
- out(" Local dev? Run `clivly dev` to open a tunnel, or skip this step until you deploy (remote sync is deploy-only locally).\n");
3709
+ out(" 2. Local dev: run `clivly dev` for dashboard-triggered sync\n");
3710
+ out(" It tunnels your app and registers the URL for you — remote sync is\n deploy-only otherwise, so nothing is broken if you skip it. Once deployed\n to Vercel/Cloudflare/Railway/Render/Fly the URL is detected automatically;\n set CLIVLY_SYNC_TRIGGER_URL by hand only for a custom domain.\n");
3552
3711
  const step = authComplete ? 3 : 4;
3553
3712
  if (!authComplete) out(" 3. Finish resolveUser in the auth/verify route\n");
3554
3713
  out(` ${step}. Run \`clivly status\` to see where you are\n`);