clivly 0.9.4 → 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 (4) hide show
  1. package/README.md +23 -6
  2. package/index.cjs +120 -3
  3. package/index.mjs +120 -3
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -87,7 +87,10 @@ Detects your framework, ORM, auth adapter and app layout, then scaffolds:
87
87
  | The auth-verify route for your framework | Yes — `--force` to overwrite |
88
88
 
89
89
  Plus `CLIVLY_SECRET_KEY`, `CLIVLY_SYNC_TRIGGER_URL` and
90
- `CLIVLY_SYNC_TRIGGER_SECRET` placeholders in your env file.
90
+ `CLIVLY_SYNC_TRIGGER_SECRET` in your env file. The two secrets are written
91
+ empty, for you or `clivly login` to fill; the trigger URL gets a visible
92
+ `https://<yourDomain.com>/api/clivly/tick` template, because there the shape is
93
+ the documentation.
91
94
 
92
95
  | Flag | Purpose |
93
96
  | --- | --- |
@@ -233,7 +236,7 @@ the CLI requires pairing.
233
236
  | --- | --- | --- |
234
237
  | `CLIVLY_SECRET_KEY` | Dashboard → **Connect app**, or **Settings → API keys** | **Shown once.** Copy it immediately; you cannot retrieve it later, only regenerate. Regenerating **revokes the previous key**, so redeploy after rotating. The legacy name `CLIVLY_API_KEY` is still accepted as a fallback |
235
238
  | `CLIVLY_SYNC_TRIGGER_SECRET` | Dashboard → **Integrations → Remote sync** | Must match the value in your app's env exactly. A mismatch surfaces as a `401` on the signed trigger |
236
- | `CLIVLY_SYNC_TRIGGER_URL` | Your own deployment | The public URL of your mounted tick route, e.g. `https://app.example.com/api/clivly/tick`. Optional until you deploy — push-only setups can leave it unset |
239
+ | `CLIVLY_SYNC_TRIGGER_URL` | Your own deployment | The public URL of your mounted tick route. `init` and `login` both scaffold it as `https://<yourDomain.com>/api/clivly/tick` — the angle brackets mark it unedited, and the SDK ignores it until you replace them, so leaving it alone costs nothing. Optional until you deploy — push-only setups can leave it as the template |
237
240
  | `CLIVLY_PUBLISHABLE_KEY` | Same place as the secret key | Only needed for the chat widget. Safe to expose in client code |
238
241
 
239
242
  Write them into the env file of the package that imports `clivly`, then re-run
@@ -265,7 +268,7 @@ It answers two questions under two headings:
265
268
  scope; `clivly doctor` is local by definition and omits it.
266
269
  - **This project** — the local ladder, run here on your machine: credential →
267
270
  Clivly reachable → `clivly.config` loads → `clivly.config` is edge-safe →
268
- setup checks → sync trigger URL.
271
+ boot hook wired → setup checks → sync trigger URL.
269
272
 
270
273
  ```text
271
274
  Clivly status
@@ -278,11 +281,23 @@ Workspace
278
281
  🔒 Let Clivly start a sync
279
282
 
280
283
  This project
281
- ✅ Step 1 of 6 · API key present
282
- ✅ Step 2 of 6 · Clivly reachable — Acme
284
+ ✅ Step 1 of 7 · API key present
285
+ ✅ Step 2 of 7 · Clivly reachable — Acme
283
286
  ...
284
287
  ```
285
288
 
289
+ **Boot hook wired** is the rung to know about. It finds the boot site for your
290
+ framework and confirms something there actually starts Clivly — and on Next.js
291
+ below 15, that `experimental.instrumentationHook` is set in `next.config`,
292
+ without which `instrumentation.ts` is ignored outright and your app never checks
293
+ in. That failure is otherwise completely silent: the file is present, the config
294
+ loads, the credentials work, and the dashboard stays empty.
295
+
296
+ It is the one rung with no prerequisites, so it still reports when the config
297
+ above it fails to load. A framework the CLI does not recognise shows it as
298
+ skipped rather than passed — there is nowhere known to look, and a green step
299
+ for an unasked question is what made this worth adding.
300
+
286
301
  If the Workspace section reads
287
302
  `⚠️ This Clivly server does not report setup progress`, or names a rejected key
288
303
  or an unreachable host, that means the question could not be asked — not that
@@ -406,7 +421,7 @@ work, and are kept because output and docs have recommended them — but prefer
406
421
  | --- | --- |
407
422
  | `clivly ping` | Credential + reachability |
408
423
  | `clivly connect` | The above, plus config and trigger URL |
409
- | `clivly doctor` | Config, setup checks and edge safety |
424
+ | `clivly doctor` | Config, edge safety, boot hook and setup checks |
410
425
 
411
426
  Each prints a deprecation notice, then the steps it covers, renumbered to its own
412
427
  scope:
@@ -478,6 +493,8 @@ socket reconnect. The CLI does not enable this setting on your behalf.
478
493
  | `init` cannot find your schema | Pass `--schema <path\|package>` |
479
494
  | `init` scaffolded a `db` import that doesn't resolve | The probe cannot evaluate a db module that throws without `DATABASE_URL`. Fix the `TODO(clivly)` import path it left, or pass `--db-import` |
480
495
  | `clivly login` exits non-zero, pairing unavailable | The backend has `CLIVLY_CLI_PAIRING_ENABLED` off. Use the manual credential path above |
496
+ | App runs, no errors, dashboard stays silent | Run `clivly status` — the **Boot hook wired** step names the cause. On Next.js below 15 it is almost always the missing `experimental: { instrumentationHook: true }` in `next.config` |
497
+ | `Clivly reachable` fails with `invalid_api_key` (401) | The key is not one this org accepts. Re-run `npx clivly login` to re-pair, and check the key belongs to the org whose dashboard you are watching |
481
498
  | `clivly dev` cannot start | Install `cloudflared`, or set `CLIVLY_SYNC_TRIGGER_URL` to a tunnel you run yourself |
482
499
  | Credentials written but the app never loads them | Wrong env file for your framework — see the table under `clivly login` |
483
500
 
package/index.cjs CHANGED
@@ -974,7 +974,7 @@ function isKeySet(contents, key) {
974
974
  function appendEnvKeys(existing, entries) {
975
975
  const missing = entries.filter((e) => !hasKey$1(existing, e.key));
976
976
  if (missing.length === 0) return existing;
977
- 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");
978
978
  return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
979
979
  }
980
980
  /**
@@ -1810,7 +1810,8 @@ const CLIVLY_ENV_KEYS = [
1810
1810
  },
1811
1811
  {
1812
1812
  key: "CLIVLY_SYNC_TRIGGER_URL",
1813
- 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."
1814
1815
  },
1815
1816
  {
1816
1817
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
@@ -2741,6 +2742,103 @@ function ladderExitCode(results) {
2741
2742
  return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
2742
2743
  }
2743
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
2744
2842
  //#region src/edge-safety.ts
2745
2843
  const NODE_BUILTINS = /* @__PURE__ */ new Set([
2746
2844
  "assert",
@@ -2927,7 +3025,7 @@ function pingOnce(ctx) {
2927
3025
  }
2928
3026
  const REASON_HINT = {
2929
3027
  ok: "",
2930
- 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.",
2931
3029
  forbidden: "The key is valid but lacks access to this org (403).",
2932
3030
  rate_limited: "Rate limited (429). Wait a moment and retry.",
2933
3031
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
@@ -3083,6 +3181,24 @@ const STATUS_STAGES = [
3083
3181
  });
3084
3182
  }
3085
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
+ },
3086
3202
  sdkChecksStage,
3087
3203
  triggerUrlStage
3088
3204
  ];
@@ -3136,6 +3252,7 @@ const ALIAS_STAGE_IDS = {
3136
3252
  doctor: [
3137
3253
  "config",
3138
3254
  "runtime",
3255
+ "boot-hook",
3139
3256
  "setup-checks"
3140
3257
  ],
3141
3258
  connect: [
package/index.mjs CHANGED
@@ -973,7 +973,7 @@ function isKeySet(contents, key) {
973
973
  function appendEnvKeys(existing, entries) {
974
974
  const missing = entries.filter((e) => !hasKey$1(existing, e.key));
975
975
  if (missing.length === 0) return existing;
976
- const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=`).join("\n");
976
+ const block = missing.map((e) => `${e.comment ? `# ${e.comment}\n` : ""}${e.key}=${e.value ?? ""}`).join("\n");
977
977
  return `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${existing.length > 0 ? "\n" : ""}# Clivly\n${block}\n`;
978
978
  }
979
979
  /**
@@ -1809,7 +1809,8 @@ const CLIVLY_ENV_KEYS = [
1809
1809
  },
1810
1810
  {
1811
1811
  key: "CLIVLY_SYNC_TRIGGER_URL",
1812
- comment: "Public URL of your mounted tick route (e.g. https://app.example.com/api/clivly/tick)"
1812
+ value: TRIGGER_URL_PLACEHOLDER,
1813
+ comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1813
1814
  },
1814
1815
  {
1815
1816
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
@@ -2740,6 +2741,103 @@ function ladderExitCode(results) {
2740
2741
  return results.every((result) => result.state !== "fail" && result.state !== "pending") ? 0 : 1;
2741
2742
  }
2742
2743
  //#endregion
2744
+ //#region src/boot-hook-check.ts
2745
+ const REMIX_ENTRY_CANDIDATES = [
2746
+ "app/entry.server.tsx",
2747
+ "app/entry.server.ts",
2748
+ "app/entry.server.jsx",
2749
+ "app/entry.server.js"
2750
+ ];
2751
+ const NEXT_CONFIG_CANDIDATES = [
2752
+ "next.config.ts",
2753
+ "next.config.mjs",
2754
+ "next.config.js",
2755
+ "next.config.cjs",
2756
+ "next.config.mts"
2757
+ ];
2758
+ const CLIVLY_MENTION = /clivly/i;
2759
+ const INSTRUMENTATION_HOOK_ENABLED = /instrumentationHook\s*:\s*true/;
2760
+ /**
2761
+ * Where this framework's boot hook lives, most likely location first.
2762
+ *
2763
+ * The file-owning frameworks are asked of `boot-hook.ts` rather than restated,
2764
+ * so a path change there cannot leave this check hunting for a file `init` no
2765
+ * longer writes. The other two are patched into a file the app already owns.
2766
+ */
2767
+ function bootSiteCandidates(framework) {
2768
+ const owned = [bootHookPath(framework, "app"), bootHookPath(framework, "src/app")].filter((path) => path !== null);
2769
+ if (owned.length > 0) return [...new Set(owned)];
2770
+ if (framework === "tanstack-start") return [...VITE_CONFIG_CANDIDATES];
2771
+ if (framework === "remix") return REMIX_ENTRY_CANDIDATES;
2772
+ return [];
2773
+ }
2774
+ function declaredDeps(readFile) {
2775
+ const raw = readFile("package.json");
2776
+ if (raw === null) return null;
2777
+ try {
2778
+ const pkg = JSON.parse(raw);
2779
+ return {
2780
+ ...pkg.dependencies,
2781
+ ...pkg.devDependencies
2782
+ };
2783
+ } catch {
2784
+ return null;
2785
+ }
2786
+ }
2787
+ function findBootSite(readFile, framework) {
2788
+ for (const candidate of bootSiteCandidates(framework)) {
2789
+ const source = readFile(candidate);
2790
+ if (source !== null && CLIVLY_MENTION.test(source)) return candidate;
2791
+ }
2792
+ return null;
2793
+ }
2794
+ /**
2795
+ * On Next.js below 15 the hook file is ignored unless the flag is on, so a
2796
+ * present-and-correct instrumentation.ts is not evidence of anything. A missing
2797
+ * next.config is the same failure as one without the flag: either way nothing
2798
+ * switches the hook on.
2799
+ */
2800
+ function nextFlagVerdict(readFile, version, site) {
2801
+ if (!nextNeedsInstrumentationFlag(version)) return null;
2802
+ for (const candidate of NEXT_CONFIG_CANDIDATES) {
2803
+ const source = readFile(candidate);
2804
+ if (source !== null && INSTRUMENTATION_HOOK_ENABLED.test(source)) return null;
2805
+ }
2806
+ return {
2807
+ state: "broken",
2808
+ detail: `${site} exists, but Next.js ${version} ignores it — experimental.instrumentationHook is not enabled in next.config.`,
2809
+ hint: NEXT_INSTRUMENTATION_FLAG_STEP
2810
+ };
2811
+ }
2812
+ function checkBootHook(readFile) {
2813
+ const deps = declaredDeps(readFile);
2814
+ if (deps === null) return {
2815
+ state: "unknown",
2816
+ detail: "package.json not readable — cannot tell how this app boots."
2817
+ };
2818
+ const framework = detectFramework(deps);
2819
+ if (framework === "unknown") return {
2820
+ state: "unknown",
2821
+ detail: "No supported framework detected, so there is no known place to check."
2822
+ };
2823
+ const label = FRAMEWORK_LABELS[framework];
2824
+ const site = findBootSite(readFile, framework);
2825
+ if (site === null) return {
2826
+ state: "broken",
2827
+ detail: `No Clivly boot hook found in this ${label} app.`,
2828
+ 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]}).`
2829
+ };
2830
+ const nextVersion = deps.next;
2831
+ if (framework === "nextjs" && nextVersion) {
2832
+ const broken = nextFlagVerdict(readFile, nextVersion, site);
2833
+ if (broken) return broken;
2834
+ }
2835
+ return {
2836
+ state: "ok",
2837
+ detail: site
2838
+ };
2839
+ }
2840
+ //#endregion
2743
2841
  //#region src/edge-safety.ts
2744
2842
  const NODE_BUILTINS = /* @__PURE__ */ new Set([
2745
2843
  "assert",
@@ -2926,7 +3024,7 @@ function pingOnce(ctx) {
2926
3024
  }
2927
3025
  const REASON_HINT = {
2928
3026
  ok: "",
2929
- invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
3027
+ 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.",
2930
3028
  forbidden: "The key is valid but lacks access to this org (403).",
2931
3029
  rate_limited: "Rate limited (429). Wait a moment and retry.",
2932
3030
  server_error: "Clivly returned a server error (5xx). Retry shortly.",
@@ -3082,6 +3180,24 @@ const STATUS_STAGES = [
3082
3180
  });
3083
3181
  }
3084
3182
  },
3183
+ {
3184
+ id: "boot-hook",
3185
+ label: "Boot hook wired",
3186
+ run(ctx) {
3187
+ const read = ctx.readConfigSource ?? defaultReadConfigSource;
3188
+ const root = dirname(ctx.configPath);
3189
+ const verdict = checkBootHook((relative) => read(resolve(root, relative)));
3190
+ if (verdict.state === "ok") return Promise.resolve({
3191
+ state: "pass",
3192
+ detail: `starts Clivly from ${verdict.detail}`
3193
+ });
3194
+ return Promise.resolve({
3195
+ state: verdict.state === "unknown" ? "skip" : "fail",
3196
+ detail: verdict.detail,
3197
+ hint: verdict.hint
3198
+ });
3199
+ }
3200
+ },
3085
3201
  sdkChecksStage,
3086
3202
  triggerUrlStage
3087
3203
  ];
@@ -3135,6 +3251,7 @@ const ALIAS_STAGE_IDS = {
3135
3251
  doctor: [
3136
3252
  "config",
3137
3253
  "runtime",
3254
+ "boot-hook",
3138
3255
  "setup-checks"
3139
3256
  ],
3140
3257
  connect: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",