clivly 0.9.1 → 0.9.3

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 (86) hide show
  1. package/README.md +79 -13
  2. package/core-drizzle.d.cts +1 -1
  3. package/core-drizzle.d.mts +1 -1
  4. package/core-entity-config.cjs +2 -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 +2 -1
  13. package/core-mappings-config.d.cts +2 -2
  14. package/core-mappings-config.d.mts +2 -2
  15. package/core-mappings-config.mjs +2 -2
  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 +3 -3
  21. package/core.d.cts +8 -7
  22. package/core.d.mts +8 -7
  23. package/core.mjs +3 -3
  24. package/{drizzle-DooDUGqO.d.mts → drizzle-CiK8MMEZ.d.mts} +1 -1
  25. package/{drizzle-jz0lrAgl.d.cts → drizzle-DEtRrqtc.d.cts} +1 -1
  26. package/drizzle.d.cts +2 -2
  27. package/drizzle.d.mts +2 -2
  28. package/{entity-config-fKQhimtD.d.mts → entity-config-B2RA53nw.d.cts} +25 -1
  29. package/{entity-config-fKQhimtD.d.cts → entity-config-B2RA53nw.d.mts} +25 -1
  30. package/{entity-config-nuXge6rY.cjs → entity-config-DtBsl2MQ.cjs} +36 -12
  31. package/{entity-config-D08zAeFY.mjs → entity-config-kUHri9yA.mjs} +31 -13
  32. package/index.cjs +452 -38
  33. package/index.d.cts +3 -1
  34. package/index.d.mts +3 -1
  35. package/index.mjs +452 -39
  36. package/integration-state-BK-riZnm.d.cts +130 -0
  37. package/integration-state-BK-riZnm.d.mts +130 -0
  38. package/{internal-C-YBW2DB.d.cts → internal-CDY8g9Ic.d.mts} +1 -1
  39. package/{internal-D4HDi2GC.cjs → internal-CwnEG6Wl.cjs} +1 -1
  40. package/{internal-BLCK81kB.d.mts → internal-Da_dlcQ_.d.cts} +1 -1
  41. package/{internal-BGVUG312.mjs → internal-Df-PtOH_.mjs} +1 -1
  42. package/internal-drizzle.d.cts +1 -1
  43. package/internal-drizzle.d.mts +1 -1
  44. package/internal-entity-config.cjs +2 -1
  45. package/internal-entity-config.d.cts +2 -2
  46. package/internal-entity-config.d.mts +2 -2
  47. package/internal-entity-config.mjs +2 -2
  48. package/internal-entity-heuristics.cjs +1 -1
  49. package/internal-entity-heuristics.d.cts +1 -1
  50. package/internal-entity-heuristics.d.mts +1 -1
  51. package/internal-entity-heuristics.mjs +1 -1
  52. package/internal-mapping-form.d.cts +1 -1
  53. package/internal-mapping-form.d.mts +1 -1
  54. package/internal-mappings-config.cjs +2 -1
  55. package/internal-mappings-config.d.cts +2 -2
  56. package/internal-mappings-config.d.mts +2 -2
  57. package/internal-mappings-config.mjs +2 -2
  58. package/internal-sync-engine.d.cts +1 -1
  59. package/internal-sync-engine.d.mts +1 -1
  60. package/internal-view-compiler.d.cts +1 -1
  61. package/internal-view-compiler.d.mts +1 -1
  62. package/internal.cjs +3 -3
  63. package/internal.d.cts +8 -7
  64. package/internal.d.mts +8 -7
  65. package/internal.mjs +3 -3
  66. package/{mapping-form-Du36VZr1.d.mts → mapping-form-DH77BXLv.d.mts} +1 -1
  67. package/{mapping-form-DhY0iFmN.d.cts → mapping-form-J0aU6O7l.d.cts} +1 -1
  68. package/{mappings-config-C8ynKmDK.cjs → mappings-config-B0RqFLxu.cjs} +30 -1
  69. package/{mappings-config-DNaYGyKN.mjs → mappings-config-B1b1HVBg.mjs} +25 -2
  70. package/{mappings-config-B1qNM8VL.d.cts → mappings-config-D9dsonoi.d.mts} +40 -2
  71. package/{mappings-config-BChZsany.d.mts → mappings-config-RQLGx8ub.d.cts} +40 -2
  72. package/{namespace-probe-BkUs8raG.d.mts → namespace-probe-BmdGnYMn.d.mts} +72 -3
  73. package/{namespace-probe-aRFmpU85.d.cts → namespace-probe-EYyxWZnf.d.cts} +72 -3
  74. package/package.json +1 -1
  75. package/{sdk-Bj-ZDf4u.cjs → sdk-COLUHhJq.cjs} +755 -44
  76. package/{sdk-BvRD2jAp.mjs → sdk-CZNXBsgA.mjs} +755 -44
  77. package/sdk.cjs +4 -2
  78. package/sdk.d.cts +3 -3
  79. package/sdk.d.mts +3 -3
  80. package/sdk.mjs +3 -3
  81. package/{sync-engine-DHQ7rXZe.d.mts → sync-engine-BG2O1an2.d.cts} +2 -2
  82. package/{sync-engine-DiPjH53O.d.cts → sync-engine-DgQbrJON.d.mts} +2 -2
  83. package/{view-compiler-CcUwzZut.d.mts → view-compiler-Bo7pqXW3.d.mts} +1 -1
  84. package/{view-compiler-DqAdVxLq.d.cts → view-compiler-CmEdAMwj.d.cts} +1 -1
  85. package/vite.d.cts +1 -1
  86. 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-nuXge6rY.cjs");
3
+ const require_entity_config = require("./entity-config-DtBsl2MQ.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-Bj-ZDf4u.cjs");
6
+ const require_sdk = require("./sdk-COLUHhJq.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");
@@ -478,7 +478,7 @@ async function probeModule(opts) {
478
478
  }
479
479
  //#endregion
480
480
  //#region src/workspace-graph.ts
481
- const WINDOWS_SEP$2 = /\\/g;
481
+ const WINDOWS_SEP$3 = /\\/g;
482
482
  const MODULE_EXT$1 = /\.(ts|tsx|js|jsx|mjs|cjs|cts|mts)$/;
483
483
  const BRACE_GROUP = /\{([^{}]*)\}/;
484
484
  const MAX_GLOB_DEPTH = 8;
@@ -709,13 +709,13 @@ function exportsAllowSubpath(packageDir, subpath) {
709
709
  function toPackageSpecifier(pkg, absPath) {
710
710
  const target = (0, node_path.resolve)(absPath);
711
711
  if (resolvePackageEntry(pkg.dir) === target) return pkg.name;
712
- const subpath = (0, node_path.relative)(pkg.dir, target).replace(WINDOWS_SEP$2, "/").replace(MODULE_EXT$1, "");
712
+ const subpath = (0, node_path.relative)(pkg.dir, target).replace(WINDOWS_SEP$3, "/").replace(MODULE_EXT$1, "");
713
713
  if (subpath.length === 0) return pkg.name;
714
714
  return exportsAllowSubpath(pkg.dir, subpath) ? `${pkg.name}/${subpath}` : null;
715
715
  }
716
716
  //#endregion
717
717
  //#region src/discover-schema.ts
718
- const WINDOWS_SEP$1 = /\\/g;
718
+ const WINDOWS_SEP$2 = /\\/g;
719
719
  const MODULE_EXT = /\.(ts|js|mjs|cts|mts)$/;
720
720
  /**
721
721
  * Turn an absolute module path into the specifier the generated config imports
@@ -740,7 +740,7 @@ function toImportPath(schemaPath, cwd, graph) {
740
740
  const specifier = toPackageSpecifier(owner, schemaPath);
741
741
  if (specifier) return specifier;
742
742
  }
743
- const rel = (0, node_path.relative)(cwd, schemaPath).replace(WINDOWS_SEP$1, "/").replace(MODULE_EXT, "");
743
+ const rel = (0, node_path.relative)(cwd, schemaPath).replace(WINDOWS_SEP$2, "/").replace(MODULE_EXT, "");
744
744
  return rel.startsWith(".") ? rel : `./${rel}`;
745
745
  }
746
746
  function isDirectory(path) {
@@ -1002,7 +1002,7 @@ Usage:
1002
1002
  clivly init [options] Scaffold Clivly (config, routes, env file) for your app
1003
1003
  clivly add widget [options] Scaffold the chat-session route
1004
1004
  clivly status [options] Check your setup, step by step
1005
- clivly push-schema [options] Report your schema to Clivly without deploying
1005
+ clivly push-schema [options] Push schema, custom objects and mappings to Clivly
1006
1006
  clivly dev [options] Tunnel localhost so remote sync can reach you
1007
1007
  clivly login [options] Pair this machine and write credentials to your env file
1008
1008
  clivly --version Print the installed clivly version
@@ -1058,7 +1058,9 @@ Options:
1058
1058
  Usage:
1059
1059
  clivly status [--verbose] [--config <path>] [--env-file <path>]
1060
1060
 
1061
- Runs the setup ladder: credential, reachability, config load, edge-safety,
1061
+ Prints two sections. "Workspace" is how far setup has got on Clivly's side,
1062
+ computed by the server so the terminal and the dashboard agree. "This project"
1063
+ runs the local ladder: credential, reachability, config load, edge-safety,
1062
1064
  setup checks, sync trigger. Read-only — writes nothing.
1063
1065
 
1064
1066
  Options:
@@ -1066,13 +1068,23 @@ Options:
1066
1068
  --config <path> Path to clivly.config.ts
1067
1069
  --env-file <path> Extra env file to load
1068
1070
  `,
1069
- "push-schema": `clivly push-schema — report your schema without deploying
1071
+ "push-schema": `clivly push-schema — push everything your config declares
1070
1072
 
1071
1073
  Usage:
1072
1074
  clivly push-schema [--config <path>]
1073
1075
 
1074
- Sends your discovered tables to Clivly so the dashboard can offer them for
1075
- mapping. Reads your config; writes nothing locally.
1076
+ Pushes three things to Clivly, without deploying your app:
1077
+ 1. Your table and column NAMES, so the dashboard can offer them for mapping.
1078
+ Never row data — discovery is names only.
1079
+ 2. The custom object types you declared with \`objectType\`.
1080
+ 3. The mappings that make those objects actually receive rows.
1081
+
1082
+ Reads your config; writes nothing locally. Safe to re-run — it reports what it
1083
+ created, updated, or left alone.
1084
+
1085
+ Exits 1 if Clivly refuses your config, most often because an entity would
1086
+ overwrite a mapping edited in the dashboard. That makes it usable in CI as a
1087
+ schema gate.
1076
1088
  `,
1077
1089
  dev: `clivly dev — tunnel localhost so remote sync can reach you
1078
1090
 
@@ -1154,6 +1166,46 @@ async function installPackages(options) {
1154
1166
  }
1155
1167
  }
1156
1168
  //#endregion
1169
+ //#region src/install-needed.ts
1170
+ /** Exact `name@version` only. Anything else is left for the package manager. */
1171
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
1172
+ /**
1173
+ * Split `name@version`, tolerating scoped names where the leading `@` is part
1174
+ * of the name rather than a separator. Returns a null version for a bare name.
1175
+ */
1176
+ function parseSpec(spec) {
1177
+ const at = spec.lastIndexOf("@");
1178
+ if (at <= 0) return {
1179
+ name: spec,
1180
+ version: null
1181
+ };
1182
+ return {
1183
+ name: spec.slice(0, at),
1184
+ version: spec.slice(at + 1)
1185
+ };
1186
+ }
1187
+ /**
1188
+ * Which of `packages` still need installing.
1189
+ *
1190
+ * Deliberately conservative — a spec is only dropped when it is provably
1191
+ * satisfied. Tarball paths, `file:` specs and semver ranges are always kept:
1192
+ * the first two name no version to compare, and resolving the third needs a
1193
+ * semver implementation the CLI does not ship. Erring toward installing costs
1194
+ * a redundant install; erring the other way silently skips one the developer
1195
+ * asked for.
1196
+ */
1197
+ function packagesNeedingInstall(packages, installedVersion) {
1198
+ return packages.filter((spec) => {
1199
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.includes(":") || spec.includes("/") && !spec.startsWith("@")) return true;
1200
+ const { name, version } = parseSpec(spec);
1201
+ const present = installedVersion(name);
1202
+ if (present === null) return true;
1203
+ if (version === null) return false;
1204
+ if (!EXACT_VERSION.test(version)) return true;
1205
+ return present !== version;
1206
+ });
1207
+ }
1208
+ //#endregion
1157
1209
  //#region src/is-entry.ts
1158
1210
  function isEntrypoint(argv1, moduleUrl, realpath = node_fs.realpathSync) {
1159
1211
  if (!argv1) return false;
@@ -1615,6 +1667,106 @@ function rankContactCandidates(tables) {
1615
1667
  }));
1616
1668
  }
1617
1669
  //#endregion
1670
+ //#region src/config-import.ts
1671
+ const WINDOWS_SEP$1 = /\\/g;
1672
+ function configImport(generatedFilePath) {
1673
+ const rel = (0, node_path.relative)((0, node_path.dirname)(generatedFilePath), "clivly.config").replace(WINDOWS_SEP$1, "/");
1674
+ return rel.startsWith(".") ? rel : `./${rel}`;
1675
+ }
1676
+ //#endregion
1677
+ //#region src/boot-hook.ts
1678
+ const BOOT_HOOK_PATHS = {
1679
+ nextjs: "instrumentation.ts",
1680
+ nuxt: "server/plugins/clivly.ts",
1681
+ sveltekit: "src/hooks.server.ts",
1682
+ "tanstack-start": null,
1683
+ remix: null,
1684
+ unknown: null
1685
+ };
1686
+ function bootHookPath(framework, layout) {
1687
+ const path = BOOT_HOOK_PATHS[framework];
1688
+ if (path === null) return null;
1689
+ if (framework === "nextjs" && layout === "src/app") return `src/${path}`;
1690
+ return path;
1691
+ }
1692
+ const FAILURE_NOTE = "Clivly never blocks your app from starting: a failed check-in is logged and retried on the next tick.";
1693
+ function nextjsHook(importPath) {
1694
+ return `// Clivly boot hook. Next.js calls register() once when the server starts.
1695
+ // ${FAILURE_NOTE}
1696
+ export async function register() {
1697
+ // The keep-alive loop is a setInterval, which does not survive on the edge
1698
+ // runtime — it would stop ticking after the first response and the dashboard
1699
+ // would show an app that checked in once and then went stale forever.
1700
+ if (process.env.NEXT_RUNTIME !== "nodejs") {
1701
+ return;
1702
+ }
1703
+ try {
1704
+ const { default: clivly } = await import("${importPath}");
1705
+ await clivly.start();
1706
+ } catch (error) {
1707
+ console.warn("[clivly] could not check in:", error);
1708
+ }
1709
+ }
1710
+ `;
1711
+ }
1712
+ function nuxtHook(importPath) {
1713
+ return `import clivly from "${importPath}";
1714
+
1715
+ // Clivly boot hook. Nitro runs this once when the server starts.
1716
+ // ${FAILURE_NOTE}
1717
+ export default defineNitroPlugin(() => {
1718
+ clivly.start().catch((error: unknown) => {
1719
+ console.warn("[clivly] could not check in:", error);
1720
+ });
1721
+ });
1722
+ `;
1723
+ }
1724
+ function sveltekitHook(importPath) {
1725
+ return `import clivly from "${importPath}";
1726
+
1727
+ // Clivly boot hook. This module is evaluated once when the server starts.
1728
+ // ${FAILURE_NOTE}
1729
+ clivly.start().catch((error: unknown) => {
1730
+ console.warn("[clivly] could not check in:", error);
1731
+ });
1732
+ `;
1733
+ }
1734
+ const RENDERERS = {
1735
+ nextjs: nextjsHook,
1736
+ nuxt: nuxtHook,
1737
+ sveltekit: sveltekitHook
1738
+ };
1739
+ function renderBootHook(framework, layout) {
1740
+ const path = bootHookPath(framework, layout);
1741
+ const render = RENDERERS[framework];
1742
+ if (!(path && render)) throw new Error(`No standalone boot hook for framework: ${framework}`);
1743
+ return render(configImport(path));
1744
+ }
1745
+ const NEXT_INSTRUMENTATION_STABLE_MAJOR = 15;
1746
+ const LEADING_MAJOR = /(\d+)/;
1747
+ /** Does this Next.js version range need the experimental flag to run our hook? */
1748
+ function nextNeedsInstrumentationFlag(versionRange) {
1749
+ if (!versionRange) return false;
1750
+ const major = LEADING_MAJOR.exec(versionRange);
1751
+ if (!major?.[1]) return false;
1752
+ return Number(major[1]) < NEXT_INSTRUMENTATION_STABLE_MAJOR;
1753
+ }
1754
+ const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentation.ts only when it is switched on. Add this to next.config, or Clivly will never be told your app started:
1755
+ experimental: { instrumentationHook: true }`;
1756
+ const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
1757
+ import clivly from "./clivly.config";
1758
+ clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
1759
+ /** The boot hook as a scaffold entry, or null when this framework is patched. */
1760
+ function planBootHook(framework, layout) {
1761
+ const path = bootHookPath(framework, layout);
1762
+ if (!path) return null;
1763
+ return {
1764
+ path,
1765
+ description: "Clivly boot hook — starts check-in when your app runs",
1766
+ owned: true
1767
+ };
1768
+ }
1769
+ //#endregion
1618
1770
  //#region src/render-config.ts
1619
1771
  const CLIVLY_ENV_KEYS = [
1620
1772
  {
@@ -1925,10 +2077,6 @@ function routePath(framework, kind, layout = "app") {
1925
2077
  }
1926
2078
  const WINDOWS_SEP = /\\/g;
1927
2079
  const LEADING_DOT_SLASH = /^\.\//;
1928
- function configImport(routeFilePath) {
1929
- const rel = (0, node_path.relative)((0, node_path.dirname)(routeFilePath), "clivly.config").replace(WINDOWS_SEP, "/");
1930
- return rel.startsWith(".") ? rel : `./${rel}`;
1931
- }
1932
2080
  function hostImportPath(routeFilePath, appRelative, paths) {
1933
2081
  const fromRoot = appRelative.replace(LEADING_DOT_SLASH, "");
1934
2082
  const alias = aliasFor({
@@ -2016,6 +2164,7 @@ const COMMON_FILES = [{
2016
2164
  function planScaffold(framework, layout = "app") {
2017
2165
  const paths = ROUTE_PATHS[framework];
2018
2166
  if (!paths) return [];
2167
+ const boot = planBootHook(framework, layout);
2019
2168
  return [
2020
2169
  {
2021
2170
  path: applyLayout(framework, paths.tick, layout),
@@ -2027,6 +2176,7 @@ function planScaffold(framework, layout = "app") {
2027
2176
  description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
2028
2177
  owned: true
2029
2178
  },
2179
+ ...boot ? [boot] : [],
2030
2180
  ...COMMON_FILES
2031
2181
  ];
2032
2182
  }
@@ -2040,6 +2190,45 @@ function planWidgetScaffold(framework, layout = "app") {
2040
2190
  }];
2041
2191
  }
2042
2192
  //#endregion
2193
+ //#region src/vite-boot-patch.ts
2194
+ /** Config filenames to look for, in precedence order. */
2195
+ const VITE_CONFIG_CANDIDATES = [
2196
+ "vite.config.ts",
2197
+ "vite.config.mts",
2198
+ "vite.config.js",
2199
+ "vite.config.mjs"
2200
+ ];
2201
+ const PLUGIN_IMPORT = "import { clivlyVite } from \"clivly/vite\";";
2202
+ const CONFIG_IMPORT = "import clivly from \"./clivly.config\";";
2203
+ const PLUGIN_CALL = "clivlyVite(clivly)";
2204
+ const PLUGINS_ARRAY = /(^|[\s{,])plugins\s*:\s*\[/g;
2205
+ /** The last import line's end offset, so new imports land after the existing ones. */
2206
+ const IMPORT_LINE = /^\s*import\s.+?from\s+["'].+?["'];?\s*$/gm;
2207
+ function insertImports(source) {
2208
+ const last = [...source.matchAll(IMPORT_LINE)].at(-1);
2209
+ const block = `${PLUGIN_IMPORT}\n${CONFIG_IMPORT}`;
2210
+ if (!last) return `${block}\n\n${source}`;
2211
+ const at = (last.index ?? 0) + last[0].length;
2212
+ return `${source.slice(0, at)}\n${block}${source.slice(at)}`;
2213
+ }
2214
+ /**
2215
+ * Add the Clivly Vite plugin to an existing config.
2216
+ *
2217
+ * Returns the patched source, or `null` when the config is already patched or
2218
+ * is not a shape we can edit safely.
2219
+ */
2220
+ function patchViteConfig(source) {
2221
+ if (source.includes(PLUGIN_CALL)) return null;
2222
+ const matches = [...source.matchAll(PLUGINS_ARRAY)];
2223
+ if (matches.length !== 1) return null;
2224
+ const match = matches[0];
2225
+ if (!match) return null;
2226
+ const openBracketEnd = (match.index ?? 0) + match[0].length;
2227
+ const rest = source.slice(openBracketEnd);
2228
+ const separator = rest.startsWith("\n") ? "" : " ";
2229
+ return insertImports(`${source.slice(0, openBracketEnd)}${PLUGIN_CALL},${separator}${rest}`);
2230
+ }
2231
+ //#endregion
2043
2232
  //#region src/plan-mutations.ts
2044
2233
  function fileAction(existing, next, force) {
2045
2234
  if (existing === null) return "create";
@@ -2051,8 +2240,10 @@ function hasKey(content, key) {
2051
2240
  }
2052
2241
  function planFiles(args) {
2053
2242
  const mutations = [];
2243
+ const bootPath = bootHookPath(args.framework, args.layout);
2054
2244
  const render = (path) => {
2055
2245
  if (path === "clivly.config.ts") return renderInitConfig(args.plan);
2246
+ if (bootPath !== null && path === bootPath) return renderBootHook(args.framework, args.layout);
2056
2247
  const kind = path.includes("verify") ? "verify" : "tick";
2057
2248
  return renderRouteFile(args.framework, kind, args.authWiring, args.layout, parseTsconfig(args.readFile((0, node_path.join)(args.cwd, "tsconfig.json")) ?? "")?.paths);
2058
2249
  };
@@ -2080,10 +2271,67 @@ function planEnv(args) {
2080
2271
  action: envAction(addedKeys.length, existing.length)
2081
2272
  };
2082
2273
  }
2274
+ function declaredNextVersion(args) {
2275
+ const raw = args.readFile((0, node_path.join)(args.cwd, "package.json"));
2276
+ if (!raw) return;
2277
+ let pkg = {};
2278
+ try {
2279
+ pkg = JSON.parse(raw);
2280
+ } catch {
2281
+ pkg = {};
2282
+ }
2283
+ return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2284
+ }
2285
+ function planBootPatch(args) {
2286
+ if (bootHookPath(args.framework, args.layout) !== null) return {
2287
+ file: null,
2288
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2289
+ };
2290
+ if (args.framework === "remix") return {
2291
+ file: null,
2292
+ manual: REMIX_BOOT_STEP
2293
+ };
2294
+ if (args.framework !== "tanstack-start") return {
2295
+ file: null,
2296
+ manual: null
2297
+ };
2298
+ for (const candidate of VITE_CONFIG_CANDIDATES) {
2299
+ const existing = args.readFile((0, node_path.join)(args.cwd, candidate));
2300
+ if (existing === null) continue;
2301
+ const patched = patchViteConfig(existing);
2302
+ if (patched === null) return existing.includes("clivlyVite(clivly)") ? {
2303
+ file: {
2304
+ path: candidate,
2305
+ contents: existing,
2306
+ action: "unchanged"
2307
+ },
2308
+ manual: null
2309
+ } : {
2310
+ file: null,
2311
+ manual: `Add the Clivly plugin to ${candidate} so your app checks in when it runs:\n import { clivlyVite } from "clivly/vite";\n import clivly from "./clivly.config";\n // then, inside defineConfig: plugins: [clivlyVite(clivly), …]`
2312
+ };
2313
+ return {
2314
+ file: {
2315
+ path: candidate,
2316
+ contents: patched,
2317
+ action: "patch"
2318
+ },
2319
+ manual: null
2320
+ };
2321
+ }
2322
+ return {
2323
+ file: null,
2324
+ manual: null
2325
+ };
2326
+ }
2083
2327
  function planMutations(args) {
2328
+ const files = planFiles(args);
2329
+ const boot = planBootPatch(args);
2330
+ if (boot.file) files.push(boot.file);
2084
2331
  return {
2085
- files: planFiles(args),
2086
- env: planEnv(args)
2332
+ files,
2333
+ env: planEnv(args),
2334
+ manualSteps: boot.manual ? [boot.manual] : []
2087
2335
  };
2088
2336
  }
2089
2337
  function planWidgetMutations(args) {
@@ -2097,6 +2345,49 @@ function planWidgetMutations(args) {
2097
2345
  });
2098
2346
  }
2099
2347
  //#endregion
2348
+ //#region src/quiet-host-output.ts
2349
+ /**
2350
+ * Run `fn` with the process's stdout/stderr captured. Always restores the
2351
+ * original writers, including when `fn` throws — a leaked patch would silence
2352
+ * the whole CLI for the rest of the run.
2353
+ */
2354
+ async function captureHostOutput(fn) {
2355
+ const originals = {
2356
+ stdout: process.stdout.write,
2357
+ stderr: process.stderr.write
2358
+ };
2359
+ let output = "";
2360
+ const capture = (chunk, encoding, callback) => {
2361
+ output += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
2362
+ const done = typeof encoding === "function" ? encoding : callback;
2363
+ if (typeof done === "function") done();
2364
+ return true;
2365
+ };
2366
+ process.stdout.write = capture;
2367
+ process.stderr.write = capture;
2368
+ try {
2369
+ return {
2370
+ result: await fn(),
2371
+ output
2372
+ };
2373
+ } catch (error) {
2374
+ if (output.trim().length > 0 && error instanceof Error) error.message = `${error.message}\n${output.trim()}`;
2375
+ throw error;
2376
+ } finally {
2377
+ process.stdout.write = originals.stdout;
2378
+ process.stderr.write = originals.stderr;
2379
+ }
2380
+ }
2381
+ /**
2382
+ * Run `fn` quietly, discarding whatever the host printed on success. On failure
2383
+ * the captured output is attached to the error's message, so the host's own
2384
+ * explanation is not lost.
2385
+ */
2386
+ async function quietly(fn) {
2387
+ const { result } = await captureHostOutput(fn);
2388
+ return result;
2389
+ }
2390
+ //#endregion
2100
2391
  //#region src/probe-auth.ts
2101
2392
  const AUTH_MODULE_PATHS = [
2102
2393
  "auth",
@@ -2119,15 +2410,32 @@ function defaultLoader(cwd) {
2119
2410
  return (specifier) => jiti$1.import(specifier);
2120
2411
  }
2121
2412
  function probeAuthInstance(cwd, load) {
2122
- return probeModule({
2413
+ return quietly(() => probeModule({
2123
2414
  candidates: authInstanceCandidates(cwd),
2124
2415
  cwd,
2125
2416
  load: load ?? defaultLoader(cwd),
2126
2417
  match: isBetterAuthInstance
2127
- });
2418
+ }));
2128
2419
  }
2129
2420
  //#endregion
2130
2421
  //#region src/push-schema.ts
2422
+ /**
2423
+ * "1 mapping" / "3 mappings" — a count nobody has to decode. The plural is
2424
+ * explicit because appending "s" turns "entity" into "entitys".
2425
+ */
2426
+ function countOf(n, singular, plural = `${singular}s`) {
2427
+ return `${n} ${n === 1 ? singular : plural}`;
2428
+ }
2429
+ function reportSchema(report, out) {
2430
+ const createdObjects = report.objectTypes.filter((o) => o.created);
2431
+ if (createdObjects.length > 0) out(` ✅ Created ${countOf(createdObjects.length, "custom object")}: ${createdObjects.map((o) => o.slug).join(", ")}.\n`);
2432
+ else if (report.objectTypes.length > 0) out(` ✅ ${countOf(report.objectTypes.length, "custom object")} already present.\n`);
2433
+ const created = report.mappings.filter((m) => m.created);
2434
+ const updated = report.mappings.filter((m) => m.updated);
2435
+ if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
2436
+ if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
2437
+ if (report.mappings.length > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(report.mappings.length, "mapping")} already match your config.\n`);
2438
+ }
2131
2439
  async function runPushSchema(options) {
2132
2440
  const out = options.out ?? ((line) => process.stdout.write(line));
2133
2441
  const path = resolveConfigPath(options.cwd, options.configPath);
@@ -2155,6 +2463,29 @@ async function runPushSchema(options) {
2155
2463
  }
2156
2464
  out(" ✅ Schema reported to Clivly.\n");
2157
2465
  out(" Integration liveness and trigger URL were refreshed too.\n");
2466
+ if (sdk.ensureSchema) {
2467
+ let report;
2468
+ try {
2469
+ report = await sdk.ensureSchema();
2470
+ } catch (error) {
2471
+ out(` ❌ Clivly refused your schema — ${error.message}\n`);
2472
+ return 1;
2473
+ }
2474
+ if (report === null) {
2475
+ out(" ⚠️ Could not declare the objects and mappings in your config.\n");
2476
+ out(" Run `clivly status`, then `clivly push-schema` again.\n");
2477
+ } else reportSchema(report, out);
2478
+ out(" Open Integrations → Mapping to review them.\n");
2479
+ return 0;
2480
+ }
2481
+ const ensured = await sdk.ensureCustomObjects?.();
2482
+ if (ensured === null) {
2483
+ out(" ⚠️ Could not create the custom objects your config declares.\n");
2484
+ out(" Run `clivly status`, then `clivly push-schema` again.\n");
2485
+ } else if (ensured && ensured.length > 0) {
2486
+ const created = ensured.filter((o) => o.created);
2487
+ out(created.length > 0 ? ` ✅ Created ${created.length} custom object(s): ${created.map((o) => o.slug).join(", ")}.\n` : ` ✅ ${ensured.length} custom object(s) already present.\n`);
2488
+ }
2158
2489
  out(" Open Integrations → Mapping to map your tables.\n");
2159
2490
  return 0;
2160
2491
  }
@@ -2542,6 +2873,17 @@ function defaultReadConfigSource(path) {
2542
2873
  return null;
2543
2874
  }
2544
2875
  }
2876
+ /**
2877
+ * Ping Clivly at most once per command, memoised on the context.
2878
+ *
2879
+ * `clivly status` asks two questions of one response — "did this call reach
2880
+ * Clivly" and "how far has this workspace got" — and they must come from the
2881
+ * same answer, not two calls that could disagree.
2882
+ */
2883
+ function pingOnce(ctx) {
2884
+ ctx.pingResult ??= ctx.connect(ctx.apiKey, ctx.apiUrl);
2885
+ return ctx.pingResult;
2886
+ }
2545
2887
  const REASON_HINT = {
2546
2888
  ok: "",
2547
2889
  invalid_api_key: "The key was rejected (401). Check CLIVLY_SECRET_KEY (or legacy CLIVLY_API_KEY).",
@@ -2553,7 +2895,7 @@ const REASON_HINT = {
2553
2895
  network_error: "Could not reach Clivly. Check your network or --api-url."
2554
2896
  };
2555
2897
  const apiKeyStage = {
2556
- id: "credential",
2898
+ id: "api-key",
2557
2899
  label: "API key present",
2558
2900
  run(ctx) {
2559
2901
  if (ctx.apiKey) return Promise.resolve({
@@ -2568,11 +2910,11 @@ const apiKeyStage = {
2568
2910
  }
2569
2911
  };
2570
2912
  const reachabilityStage = {
2571
- id: "application",
2913
+ id: "reachable",
2572
2914
  label: "Clivly reachable",
2573
2915
  needs: [apiKeyStage.id],
2574
2916
  async run(ctx) {
2575
- const result = await ctx.connect(ctx.apiKey, ctx.apiUrl);
2917
+ const result = await pingOnce(ctx);
2576
2918
  if (result.ok) {
2577
2919
  const name = result.org?.name ?? "(unnamed org)";
2578
2920
  const id = result.org?.id ? ` (${result.org.id})` : "";
@@ -2706,19 +3048,57 @@ function defaultLoadSdk(path) {
2706
3048
  return loadConfigSdk(path, "doctor");
2707
3049
  }
2708
3050
  //#endregion
3051
+ //#region src/workspace-section.ts
3052
+ const MARK$1 = {
3053
+ active: "⏳",
3054
+ blocked: "🔒",
3055
+ done: "✅",
3056
+ failed: "❌",
3057
+ skipped: "⏭️ "
3058
+ };
3059
+ /**
3060
+ * The workspace half of `clivly status`, rendered from what the server sent.
3061
+ *
3062
+ * This module decides nothing about a stage's state — it prints the verdict the
3063
+ * server computed. A CLI that computed its own would be a second ladder, which
3064
+ * is the drift this whole change removes.
3065
+ *
3066
+ * "Unavailable" and "nothing is set up" are deliberately different outcomes. An
3067
+ * old server, a rejected key and a dead network all mean we could not ask; none
3068
+ * of them means the workspace is empty.
3069
+ */
3070
+ function workspaceSection(result) {
3071
+ if (!result) return {
3072
+ kind: "unavailable",
3073
+ reason: "Could not reach Clivly."
3074
+ };
3075
+ if (!result.ok) return {
3076
+ kind: "unavailable",
3077
+ reason: result.status === null ? "Could not reach Clivly (no response)." : `Clivly rejected the request — ${result.reason} (HTTP ${result.status}).`
3078
+ };
3079
+ if (!result.workspace) return {
3080
+ kind: "unavailable",
3081
+ reason: "This Clivly server does not report setup progress. Upgrade the server to see it here."
3082
+ };
3083
+ return {
3084
+ kind: "ladder",
3085
+ lines: result.workspace.steps.map((step) => ` ${MARK$1[step.state]} ${step.label}${step.evidence ? ` ${step.evidence}` : ""}`)
3086
+ };
3087
+ }
3088
+ //#endregion
2709
3089
  //#region src/status.ts
2710
3090
  /** Stage ids each legacy command maps onto. */
2711
3091
  const ALIAS_STAGE_IDS = {
2712
3092
  status: STATUS_STAGES.map((stage) => stage.id),
2713
- ping: ["credential", "application"],
3093
+ ping: ["api-key", "reachable"],
2714
3094
  doctor: [
2715
3095
  "config",
2716
3096
  "runtime",
2717
3097
  "setup-checks"
2718
3098
  ],
2719
3099
  connect: [
2720
- "credential",
2721
- "application",
3100
+ "api-key",
3101
+ "reachable",
2722
3102
  "config",
2723
3103
  "trigger-url"
2724
3104
  ]
@@ -2735,8 +3115,19 @@ const defaultConnect = (apiKey, apiUrl) => require_sdk.createClivlySDK({
2735
3115
  apiUrl,
2736
3116
  retry: { maxAttempts: 1 }
2737
3117
  }).ping();
3118
+ /**
3119
+ * Exported for tests only — the rule it encodes is the point.
3120
+ *
3121
+ * A skipped check was never evaluated (nothing to check yet), which is not the
3122
+ * same as a warning: a warning is a working setup carrying real risk. Marking
3123
+ * "not applicable" with ⚠️ put a warning on every fresh install for something
3124
+ * the developer had not done wrong, which is exactly how people are trained to
3125
+ * stop reading warnings — the same reasoning `status-stages.ts` gives for
3126
+ * refusing to turn a stage red on a setup that works.
3127
+ */
2738
3128
  function subMark(sub) {
2739
- if (sub.skipped || sub.warn) return "⚠️ ";
3129
+ if (sub.skipped) return "⏭️ ";
3130
+ if (sub.warn) return "⚠️ ";
2740
3131
  return sub.ok ? "✅" : "❌";
2741
3132
  }
2742
3133
  function renderStage(result, verbose, out) {
@@ -2763,6 +3154,13 @@ async function runStatus(options) {
2763
3154
  };
2764
3155
  out("\nClivly status\n");
2765
3156
  const results = await runLadder(stages, ctx);
3157
+ if (selected.has("reachable")) {
3158
+ const section = workspaceSection(ctx.pingResult ? await ctx.pingResult : null);
3159
+ out("\nWorkspace\n");
3160
+ if (section.kind === "ladder") for (const line of section.lines) out(`${line}\n`);
3161
+ else out(` ⚠️ ${section.reason}\n`);
3162
+ }
3163
+ out("\nThis project\n");
2766
3164
  for (const result of results) renderStage(result, options.verbose ?? false, out);
2767
3165
  const code = ladderExitCode(results);
2768
3166
  if (code !== 0 && results.some((result) => result.state === "pending")) out("\nSetup is incomplete — finish the steps above and re-run.\n");
@@ -2792,6 +3190,7 @@ const KNOWN_COMMANDS = [
2792
3190
  "init",
2793
3191
  "status",
2794
3192
  "push-schema",
3193
+ "add",
2795
3194
  "dev",
2796
3195
  "login",
2797
3196
  "help",
@@ -2899,6 +3298,14 @@ function runAdd(argv, cwd, deps = defaultAddDeps) {
2899
3298
  }
2900
3299
  return 0;
2901
3300
  }
3301
+ function readInstalledVersion(cwd, name) {
3302
+ try {
3303
+ const manifest = (0, node_path.join)(cwd, "node_modules", ...name.split("/"), "package.json");
3304
+ return JSON.parse((0, node_fs.readFileSync)(manifest, "utf8")).version ?? null;
3305
+ } catch {
3306
+ return null;
3307
+ }
3308
+ }
2902
3309
  function readOwnVersion() {
2903
3310
  const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
2904
3311
  for (const rel of ["./package.json", "../package.json"]) try {
@@ -2996,6 +3403,7 @@ function resolveInitDeps(options) {
2996
3403
  install: options.installPackages ?? installPackages,
2997
3404
  probeAuth: options.probeAuthInstance ?? probeAuthInstance,
2998
3405
  readEnv: options.readEnv ?? defaultReadEnv,
3406
+ readInstalledVersion: options.readInstalledVersion ?? readInstalledVersion,
2999
3407
  readOwnVersion: options.readOwnVersion ?? readOwnVersion,
3000
3408
  writeEnv: options.writeEnv ?? defaultWriteEnv,
3001
3409
  isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
@@ -3095,12 +3503,12 @@ function applyMutations(options, deps, mutations) {
3095
3503
  deps.writeConfig((0, node_path.join)(options.cwd, file.path), file.contents);
3096
3504
  deps.out(` ✓ ${file.path}\n`);
3097
3505
  }
3098
- if (mutations.env.action === "skip") {
3099
- deps.out(" • .env already has every CLIVLY_* key — skipped\n");
3100
- return;
3506
+ if (mutations.env.action === "skip") deps.out(" • .env already has every CLIVLY_* key — skipped\n");
3507
+ else {
3508
+ deps.writeEnv(options.cwd, mutations.env.contents);
3509
+ deps.out(" ✓ .env placeholders\n");
3101
3510
  }
3102
- deps.writeEnv(options.cwd, mutations.env.contents);
3103
- deps.out(" ✓ .env placeholders\n");
3511
+ for (const step of mutations.manualSteps) deps.out(`\n ! ${step}\n`);
3104
3512
  }
3105
3513
  const DIFF_CONTEXT_LIMIT = 40;
3106
3514
  function renderDiff(existing, next) {
@@ -3120,6 +3528,7 @@ function previewMutations(deps, mutations, options) {
3120
3528
  conflict: " (differs — --diff to see, --force to overwrite)",
3121
3529
  create: ` (${lines} lines)`,
3122
3530
  overwrite: ` (${lines} lines)`,
3531
+ patch: " (your file, plus the Clivly plugin)",
3123
3532
  unchanged: " (already up to date)"
3124
3533
  }[file.action];
3125
3534
  out(` ${file.action.padEnd(9)} ${file.path}${suffix}\n`);
@@ -3177,13 +3586,17 @@ async function scaffold(options, deps, plan) {
3177
3586
  });
3178
3587
  return 0;
3179
3588
  }
3180
- const result = await deps.install({
3181
- cwd: options.cwd,
3182
- pm,
3183
- packages
3184
- });
3185
- if (result.ok) out(`Installed clivly (${pm})\n`);
3186
- else out(`Could not install automatically — run:\n ${result.command}\n`);
3589
+ const missing = packagesNeedingInstall(packages, (name) => deps.readInstalledVersion(options.cwd, name));
3590
+ if (missing.length === 0) out(`Already installed (${pm}) — nothing to fetch\n`);
3591
+ else {
3592
+ const result = await deps.install({
3593
+ cwd: options.cwd,
3594
+ pm,
3595
+ packages: missing
3596
+ });
3597
+ if (result.ok) out(`Installed clivly (${pm})\n`);
3598
+ else out(`Could not install automatically — run:\n ${result.command}\n`);
3599
+ }
3187
3600
  applyMutations(options, deps, mutations);
3188
3601
  let loginRan = false;
3189
3602
  if (credentialsMissing(mutations.env.contents)) {
@@ -3545,6 +3958,7 @@ if (isEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)
3545
3958
  process.exitCode = code;
3546
3959
  });
3547
3960
  //#endregion
3961
+ exports.KNOWN_COMMANDS = KNOWN_COMMANDS;
3548
3962
  exports.credentialsMissing = credentialsMissing;
3549
3963
  exports.main = main;
3550
3964
  exports.nearestCommand = nearestCommand;