clivly 0.9.1 → 0.9.2

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 (84) hide show
  1. package/README.md +19 -7
  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 +6 -6
  22. package/core.d.mts +6 -6
  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 +371 -27
  33. package/index.d.cts +1 -0
  34. package/index.d.mts +1 -0
  35. package/index.mjs +371 -27
  36. package/{internal-C-YBW2DB.d.cts → internal-CDY8g9Ic.d.mts} +1 -1
  37. package/{internal-D4HDi2GC.cjs → internal-CwnEG6Wl.cjs} +1 -1
  38. package/{internal-BLCK81kB.d.mts → internal-Da_dlcQ_.d.cts} +1 -1
  39. package/{internal-BGVUG312.mjs → internal-Df-PtOH_.mjs} +1 -1
  40. package/internal-drizzle.d.cts +1 -1
  41. package/internal-drizzle.d.mts +1 -1
  42. package/internal-entity-config.cjs +2 -1
  43. package/internal-entity-config.d.cts +2 -2
  44. package/internal-entity-config.d.mts +2 -2
  45. package/internal-entity-config.mjs +2 -2
  46. package/internal-entity-heuristics.cjs +1 -1
  47. package/internal-entity-heuristics.d.cts +1 -1
  48. package/internal-entity-heuristics.d.mts +1 -1
  49. package/internal-entity-heuristics.mjs +1 -1
  50. package/internal-mapping-form.d.cts +1 -1
  51. package/internal-mapping-form.d.mts +1 -1
  52. package/internal-mappings-config.cjs +2 -1
  53. package/internal-mappings-config.d.cts +2 -2
  54. package/internal-mappings-config.d.mts +2 -2
  55. package/internal-mappings-config.mjs +2 -2
  56. package/internal-sync-engine.d.cts +1 -1
  57. package/internal-sync-engine.d.mts +1 -1
  58. package/internal-view-compiler.d.cts +1 -1
  59. package/internal-view-compiler.d.mts +1 -1
  60. package/internal.cjs +3 -3
  61. package/internal.d.cts +6 -6
  62. package/internal.d.mts +6 -6
  63. package/internal.mjs +3 -3
  64. package/{mapping-form-Du36VZr1.d.mts → mapping-form-DH77BXLv.d.mts} +1 -1
  65. package/{mapping-form-DhY0iFmN.d.cts → mapping-form-J0aU6O7l.d.cts} +1 -1
  66. package/{mappings-config-C8ynKmDK.cjs → mappings-config-B0RqFLxu.cjs} +30 -1
  67. package/{mappings-config-DNaYGyKN.mjs → mappings-config-B1b1HVBg.mjs} +25 -2
  68. package/{mappings-config-B1qNM8VL.d.cts → mappings-config-D9dsonoi.d.mts} +40 -2
  69. package/{mappings-config-BChZsany.d.mts → mappings-config-RQLGx8ub.d.cts} +40 -2
  70. package/{namespace-probe-BkUs8raG.d.mts → namespace-probe-C6aHjedO.d.cts} +65 -3
  71. package/{namespace-probe-aRFmpU85.d.cts → namespace-probe-Dj-0Q3Hb.d.mts} +65 -3
  72. package/package.json +1 -1
  73. package/{sdk-Bj-ZDf4u.cjs → sdk-D4Wp_uxb.cjs} +754 -44
  74. package/{sdk-BvRD2jAp.mjs → sdk-DXV8M_Io.mjs} +754 -44
  75. package/sdk.cjs +4 -2
  76. package/sdk.d.cts +3 -3
  77. package/sdk.d.mts +3 -3
  78. package/sdk.mjs +3 -3
  79. package/{sync-engine-DHQ7rXZe.d.mts → sync-engine-BG2O1an2.d.cts} +2 -2
  80. package/{sync-engine-DiPjH53O.d.cts → sync-engine-DgQbrJON.d.mts} +2 -2
  81. package/{view-compiler-CcUwzZut.d.mts → view-compiler-Bo7pqXW3.d.mts} +1 -1
  82. package/{view-compiler-DqAdVxLq.d.cts → view-compiler-CmEdAMwj.d.cts} +1 -1
  83. package/vite.d.cts +1 -1
  84. 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-D4Wp_uxb.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) {
@@ -1154,6 +1154,46 @@ async function installPackages(options) {
1154
1154
  }
1155
1155
  }
1156
1156
  //#endregion
1157
+ //#region src/install-needed.ts
1158
+ /** Exact `name@version` only. Anything else is left for the package manager. */
1159
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
1160
+ /**
1161
+ * Split `name@version`, tolerating scoped names where the leading `@` is part
1162
+ * of the name rather than a separator. Returns a null version for a bare name.
1163
+ */
1164
+ function parseSpec(spec) {
1165
+ const at = spec.lastIndexOf("@");
1166
+ if (at <= 0) return {
1167
+ name: spec,
1168
+ version: null
1169
+ };
1170
+ return {
1171
+ name: spec.slice(0, at),
1172
+ version: spec.slice(at + 1)
1173
+ };
1174
+ }
1175
+ /**
1176
+ * Which of `packages` still need installing.
1177
+ *
1178
+ * Deliberately conservative — a spec is only dropped when it is provably
1179
+ * satisfied. Tarball paths, `file:` specs and semver ranges are always kept:
1180
+ * the first two name no version to compare, and resolving the third needs a
1181
+ * semver implementation the CLI does not ship. Erring toward installing costs
1182
+ * a redundant install; erring the other way silently skips one the developer
1183
+ * asked for.
1184
+ */
1185
+ function packagesNeedingInstall(packages, installedVersion) {
1186
+ return packages.filter((spec) => {
1187
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.includes(":") || spec.includes("/") && !spec.startsWith("@")) return true;
1188
+ const { name, version } = parseSpec(spec);
1189
+ const present = installedVersion(name);
1190
+ if (present === null) return true;
1191
+ if (version === null) return false;
1192
+ if (!EXACT_VERSION.test(version)) return true;
1193
+ return present !== version;
1194
+ });
1195
+ }
1196
+ //#endregion
1157
1197
  //#region src/is-entry.ts
1158
1198
  function isEntrypoint(argv1, moduleUrl, realpath = node_fs.realpathSync) {
1159
1199
  if (!argv1) return false;
@@ -1615,6 +1655,106 @@ function rankContactCandidates(tables) {
1615
1655
  }));
1616
1656
  }
1617
1657
  //#endregion
1658
+ //#region src/config-import.ts
1659
+ const WINDOWS_SEP$1 = /\\/g;
1660
+ function configImport(generatedFilePath) {
1661
+ const rel = (0, node_path.relative)((0, node_path.dirname)(generatedFilePath), "clivly.config").replace(WINDOWS_SEP$1, "/");
1662
+ return rel.startsWith(".") ? rel : `./${rel}`;
1663
+ }
1664
+ //#endregion
1665
+ //#region src/boot-hook.ts
1666
+ const BOOT_HOOK_PATHS = {
1667
+ nextjs: "instrumentation.ts",
1668
+ nuxt: "server/plugins/clivly.ts",
1669
+ sveltekit: "src/hooks.server.ts",
1670
+ "tanstack-start": null,
1671
+ remix: null,
1672
+ unknown: null
1673
+ };
1674
+ function bootHookPath(framework, layout) {
1675
+ const path = BOOT_HOOK_PATHS[framework];
1676
+ if (path === null) return null;
1677
+ if (framework === "nextjs" && layout === "src/app") return `src/${path}`;
1678
+ return path;
1679
+ }
1680
+ const FAILURE_NOTE = "Clivly never blocks your app from starting: a failed check-in is logged and retried on the next tick.";
1681
+ function nextjsHook(importPath) {
1682
+ return `// Clivly boot hook. Next.js calls register() once when the server starts.
1683
+ // ${FAILURE_NOTE}
1684
+ export async function register() {
1685
+ // The keep-alive loop is a setInterval, which does not survive on the edge
1686
+ // runtime — it would stop ticking after the first response and the dashboard
1687
+ // would show an app that checked in once and then went stale forever.
1688
+ if (process.env.NEXT_RUNTIME !== "nodejs") {
1689
+ return;
1690
+ }
1691
+ try {
1692
+ const { default: clivly } = await import("${importPath}");
1693
+ await clivly.start();
1694
+ } catch (error) {
1695
+ console.warn("[clivly] could not check in:", error);
1696
+ }
1697
+ }
1698
+ `;
1699
+ }
1700
+ function nuxtHook(importPath) {
1701
+ return `import clivly from "${importPath}";
1702
+
1703
+ // Clivly boot hook. Nitro runs this once when the server starts.
1704
+ // ${FAILURE_NOTE}
1705
+ export default defineNitroPlugin(() => {
1706
+ clivly.start().catch((error: unknown) => {
1707
+ console.warn("[clivly] could not check in:", error);
1708
+ });
1709
+ });
1710
+ `;
1711
+ }
1712
+ function sveltekitHook(importPath) {
1713
+ return `import clivly from "${importPath}";
1714
+
1715
+ // Clivly boot hook. This module is evaluated once when the server starts.
1716
+ // ${FAILURE_NOTE}
1717
+ clivly.start().catch((error: unknown) => {
1718
+ console.warn("[clivly] could not check in:", error);
1719
+ });
1720
+ `;
1721
+ }
1722
+ const RENDERERS = {
1723
+ nextjs: nextjsHook,
1724
+ nuxt: nuxtHook,
1725
+ sveltekit: sveltekitHook
1726
+ };
1727
+ function renderBootHook(framework, layout) {
1728
+ const path = bootHookPath(framework, layout);
1729
+ const render = RENDERERS[framework];
1730
+ if (!(path && render)) throw new Error(`No standalone boot hook for framework: ${framework}`);
1731
+ return render(configImport(path));
1732
+ }
1733
+ const NEXT_INSTRUMENTATION_STABLE_MAJOR = 15;
1734
+ const LEADING_MAJOR = /(\d+)/;
1735
+ /** Does this Next.js version range need the experimental flag to run our hook? */
1736
+ function nextNeedsInstrumentationFlag(versionRange) {
1737
+ if (!versionRange) return false;
1738
+ const major = LEADING_MAJOR.exec(versionRange);
1739
+ if (!major?.[1]) return false;
1740
+ return Number(major[1]) < NEXT_INSTRUMENTATION_STABLE_MAJOR;
1741
+ }
1742
+ 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:
1743
+ experimental: { instrumentationHook: true }`;
1744
+ const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
1745
+ import clivly from "./clivly.config";
1746
+ clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
1747
+ /** The boot hook as a scaffold entry, or null when this framework is patched. */
1748
+ function planBootHook(framework, layout) {
1749
+ const path = bootHookPath(framework, layout);
1750
+ if (!path) return null;
1751
+ return {
1752
+ path,
1753
+ description: "Clivly boot hook — starts check-in when your app runs",
1754
+ owned: true
1755
+ };
1756
+ }
1757
+ //#endregion
1618
1758
  //#region src/render-config.ts
1619
1759
  const CLIVLY_ENV_KEYS = [
1620
1760
  {
@@ -1925,10 +2065,6 @@ function routePath(framework, kind, layout = "app") {
1925
2065
  }
1926
2066
  const WINDOWS_SEP = /\\/g;
1927
2067
  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
2068
  function hostImportPath(routeFilePath, appRelative, paths) {
1933
2069
  const fromRoot = appRelative.replace(LEADING_DOT_SLASH, "");
1934
2070
  const alias = aliasFor({
@@ -2016,6 +2152,7 @@ const COMMON_FILES = [{
2016
2152
  function planScaffold(framework, layout = "app") {
2017
2153
  const paths = ROUTE_PATHS[framework];
2018
2154
  if (!paths) return [];
2155
+ const boot = planBootHook(framework, layout);
2019
2156
  return [
2020
2157
  {
2021
2158
  path: applyLayout(framework, paths.tick, layout),
@@ -2027,6 +2164,7 @@ function planScaffold(framework, layout = "app") {
2027
2164
  description: "Clivly auth-verify route (createAuthVerifyHandler) — finish resolveUser",
2028
2165
  owned: true
2029
2166
  },
2167
+ ...boot ? [boot] : [],
2030
2168
  ...COMMON_FILES
2031
2169
  ];
2032
2170
  }
@@ -2040,6 +2178,45 @@ function planWidgetScaffold(framework, layout = "app") {
2040
2178
  }];
2041
2179
  }
2042
2180
  //#endregion
2181
+ //#region src/vite-boot-patch.ts
2182
+ /** Config filenames to look for, in precedence order. */
2183
+ const VITE_CONFIG_CANDIDATES = [
2184
+ "vite.config.ts",
2185
+ "vite.config.mts",
2186
+ "vite.config.js",
2187
+ "vite.config.mjs"
2188
+ ];
2189
+ const PLUGIN_IMPORT = "import { clivlyVite } from \"clivly/vite\";";
2190
+ const CONFIG_IMPORT = "import clivly from \"./clivly.config\";";
2191
+ const PLUGIN_CALL = "clivlyVite(clivly)";
2192
+ const PLUGINS_ARRAY = /(^|[\s{,])plugins\s*:\s*\[/g;
2193
+ /** The last import line's end offset, so new imports land after the existing ones. */
2194
+ const IMPORT_LINE = /^\s*import\s.+?from\s+["'].+?["'];?\s*$/gm;
2195
+ function insertImports(source) {
2196
+ const last = [...source.matchAll(IMPORT_LINE)].at(-1);
2197
+ const block = `${PLUGIN_IMPORT}\n${CONFIG_IMPORT}`;
2198
+ if (!last) return `${block}\n\n${source}`;
2199
+ const at = (last.index ?? 0) + last[0].length;
2200
+ return `${source.slice(0, at)}\n${block}${source.slice(at)}`;
2201
+ }
2202
+ /**
2203
+ * Add the Clivly Vite plugin to an existing config.
2204
+ *
2205
+ * Returns the patched source, or `null` when the config is already patched or
2206
+ * is not a shape we can edit safely.
2207
+ */
2208
+ function patchViteConfig(source) {
2209
+ if (source.includes(PLUGIN_CALL)) return null;
2210
+ const matches = [...source.matchAll(PLUGINS_ARRAY)];
2211
+ if (matches.length !== 1) return null;
2212
+ const match = matches[0];
2213
+ if (!match) return null;
2214
+ const openBracketEnd = (match.index ?? 0) + match[0].length;
2215
+ const rest = source.slice(openBracketEnd);
2216
+ const separator = rest.startsWith("\n") ? "" : " ";
2217
+ return insertImports(`${source.slice(0, openBracketEnd)}${PLUGIN_CALL},${separator}${rest}`);
2218
+ }
2219
+ //#endregion
2043
2220
  //#region src/plan-mutations.ts
2044
2221
  function fileAction(existing, next, force) {
2045
2222
  if (existing === null) return "create";
@@ -2051,8 +2228,10 @@ function hasKey(content, key) {
2051
2228
  }
2052
2229
  function planFiles(args) {
2053
2230
  const mutations = [];
2231
+ const bootPath = bootHookPath(args.framework, args.layout);
2054
2232
  const render = (path) => {
2055
2233
  if (path === "clivly.config.ts") return renderInitConfig(args.plan);
2234
+ if (bootPath !== null && path === bootPath) return renderBootHook(args.framework, args.layout);
2056
2235
  const kind = path.includes("verify") ? "verify" : "tick";
2057
2236
  return renderRouteFile(args.framework, kind, args.authWiring, args.layout, parseTsconfig(args.readFile((0, node_path.join)(args.cwd, "tsconfig.json")) ?? "")?.paths);
2058
2237
  };
@@ -2080,10 +2259,67 @@ function planEnv(args) {
2080
2259
  action: envAction(addedKeys.length, existing.length)
2081
2260
  };
2082
2261
  }
2262
+ function declaredNextVersion(args) {
2263
+ const raw = args.readFile((0, node_path.join)(args.cwd, "package.json"));
2264
+ if (!raw) return;
2265
+ let pkg = {};
2266
+ try {
2267
+ pkg = JSON.parse(raw);
2268
+ } catch {
2269
+ pkg = {};
2270
+ }
2271
+ return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2272
+ }
2273
+ function planBootPatch(args) {
2274
+ if (bootHookPath(args.framework, args.layout) !== null) return {
2275
+ file: null,
2276
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2277
+ };
2278
+ if (args.framework === "remix") return {
2279
+ file: null,
2280
+ manual: REMIX_BOOT_STEP
2281
+ };
2282
+ if (args.framework !== "tanstack-start") return {
2283
+ file: null,
2284
+ manual: null
2285
+ };
2286
+ for (const candidate of VITE_CONFIG_CANDIDATES) {
2287
+ const existing = args.readFile((0, node_path.join)(args.cwd, candidate));
2288
+ if (existing === null) continue;
2289
+ const patched = patchViteConfig(existing);
2290
+ if (patched === null) return existing.includes("clivlyVite(clivly)") ? {
2291
+ file: {
2292
+ path: candidate,
2293
+ contents: existing,
2294
+ action: "unchanged"
2295
+ },
2296
+ manual: null
2297
+ } : {
2298
+ file: null,
2299
+ 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), …]`
2300
+ };
2301
+ return {
2302
+ file: {
2303
+ path: candidate,
2304
+ contents: patched,
2305
+ action: "patch"
2306
+ },
2307
+ manual: null
2308
+ };
2309
+ }
2310
+ return {
2311
+ file: null,
2312
+ manual: null
2313
+ };
2314
+ }
2083
2315
  function planMutations(args) {
2316
+ const files = planFiles(args);
2317
+ const boot = planBootPatch(args);
2318
+ if (boot.file) files.push(boot.file);
2084
2319
  return {
2085
- files: planFiles(args),
2086
- env: planEnv(args)
2320
+ files,
2321
+ env: planEnv(args),
2322
+ manualSteps: boot.manual ? [boot.manual] : []
2087
2323
  };
2088
2324
  }
2089
2325
  function planWidgetMutations(args) {
@@ -2097,6 +2333,49 @@ function planWidgetMutations(args) {
2097
2333
  });
2098
2334
  }
2099
2335
  //#endregion
2336
+ //#region src/quiet-host-output.ts
2337
+ /**
2338
+ * Run `fn` with the process's stdout/stderr captured. Always restores the
2339
+ * original writers, including when `fn` throws — a leaked patch would silence
2340
+ * the whole CLI for the rest of the run.
2341
+ */
2342
+ async function captureHostOutput(fn) {
2343
+ const originals = {
2344
+ stdout: process.stdout.write,
2345
+ stderr: process.stderr.write
2346
+ };
2347
+ let output = "";
2348
+ const capture = (chunk, encoding, callback) => {
2349
+ output += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
2350
+ const done = typeof encoding === "function" ? encoding : callback;
2351
+ if (typeof done === "function") done();
2352
+ return true;
2353
+ };
2354
+ process.stdout.write = capture;
2355
+ process.stderr.write = capture;
2356
+ try {
2357
+ return {
2358
+ result: await fn(),
2359
+ output
2360
+ };
2361
+ } catch (error) {
2362
+ if (output.trim().length > 0 && error instanceof Error) error.message = `${error.message}\n${output.trim()}`;
2363
+ throw error;
2364
+ } finally {
2365
+ process.stdout.write = originals.stdout;
2366
+ process.stderr.write = originals.stderr;
2367
+ }
2368
+ }
2369
+ /**
2370
+ * Run `fn` quietly, discarding whatever the host printed on success. On failure
2371
+ * the captured output is attached to the error's message, so the host's own
2372
+ * explanation is not lost.
2373
+ */
2374
+ async function quietly(fn) {
2375
+ const { result } = await captureHostOutput(fn);
2376
+ return result;
2377
+ }
2378
+ //#endregion
2100
2379
  //#region src/probe-auth.ts
2101
2380
  const AUTH_MODULE_PATHS = [
2102
2381
  "auth",
@@ -2119,15 +2398,32 @@ function defaultLoader(cwd) {
2119
2398
  return (specifier) => jiti$1.import(specifier);
2120
2399
  }
2121
2400
  function probeAuthInstance(cwd, load) {
2122
- return probeModule({
2401
+ return quietly(() => probeModule({
2123
2402
  candidates: authInstanceCandidates(cwd),
2124
2403
  cwd,
2125
2404
  load: load ?? defaultLoader(cwd),
2126
2405
  match: isBetterAuthInstance
2127
- });
2406
+ }));
2128
2407
  }
2129
2408
  //#endregion
2130
2409
  //#region src/push-schema.ts
2410
+ /**
2411
+ * "1 mapping" / "3 mappings" — a count nobody has to decode. The plural is
2412
+ * explicit because appending "s" turns "entity" into "entitys".
2413
+ */
2414
+ function countOf(n, singular, plural = `${singular}s`) {
2415
+ return `${n} ${n === 1 ? singular : plural}`;
2416
+ }
2417
+ function reportSchema(report, out) {
2418
+ const createdObjects = report.objectTypes.filter((o) => o.created);
2419
+ if (createdObjects.length > 0) out(` ✅ Created ${countOf(createdObjects.length, "custom object")}: ${createdObjects.map((o) => o.slug).join(", ")}.\n`);
2420
+ else if (report.objectTypes.length > 0) out(` ✅ ${countOf(report.objectTypes.length, "custom object")} already present.\n`);
2421
+ const created = report.mappings.filter((m) => m.created);
2422
+ const updated = report.mappings.filter((m) => m.updated);
2423
+ if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
2424
+ if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
2425
+ if (report.mappings.length > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(report.mappings.length, "mapping")} already match your config.\n`);
2426
+ }
2131
2427
  async function runPushSchema(options) {
2132
2428
  const out = options.out ?? ((line) => process.stdout.write(line));
2133
2429
  const path = resolveConfigPath(options.cwd, options.configPath);
@@ -2155,6 +2451,29 @@ async function runPushSchema(options) {
2155
2451
  }
2156
2452
  out(" ✅ Schema reported to Clivly.\n");
2157
2453
  out(" Integration liveness and trigger URL were refreshed too.\n");
2454
+ if (sdk.ensureSchema) {
2455
+ let report;
2456
+ try {
2457
+ report = await sdk.ensureSchema();
2458
+ } catch (error) {
2459
+ out(` ❌ Clivly refused your schema — ${error.message}\n`);
2460
+ return 1;
2461
+ }
2462
+ if (report === null) {
2463
+ out(" ⚠️ Could not declare the objects and mappings in your config.\n");
2464
+ out(" Run `clivly status`, then `clivly push-schema` again.\n");
2465
+ } else reportSchema(report, out);
2466
+ out(" Open Integrations → Mapping to review them.\n");
2467
+ return 0;
2468
+ }
2469
+ const ensured = await sdk.ensureCustomObjects?.();
2470
+ if (ensured === null) {
2471
+ out(" ⚠️ Could not create the custom objects your config declares.\n");
2472
+ out(" Run `clivly status`, then `clivly push-schema` again.\n");
2473
+ } else if (ensured && ensured.length > 0) {
2474
+ const created = ensured.filter((o) => o.created);
2475
+ 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`);
2476
+ }
2158
2477
  out(" Open Integrations → Mapping to map your tables.\n");
2159
2478
  return 0;
2160
2479
  }
@@ -2735,8 +3054,19 @@ const defaultConnect = (apiKey, apiUrl) => require_sdk.createClivlySDK({
2735
3054
  apiUrl,
2736
3055
  retry: { maxAttempts: 1 }
2737
3056
  }).ping();
3057
+ /**
3058
+ * Exported for tests only — the rule it encodes is the point.
3059
+ *
3060
+ * A skipped check was never evaluated (nothing to check yet), which is not the
3061
+ * same as a warning: a warning is a working setup carrying real risk. Marking
3062
+ * "not applicable" with ⚠️ put a warning on every fresh install for something
3063
+ * the developer had not done wrong, which is exactly how people are trained to
3064
+ * stop reading warnings — the same reasoning `status-stages.ts` gives for
3065
+ * refusing to turn a stage red on a setup that works.
3066
+ */
2738
3067
  function subMark(sub) {
2739
- if (sub.skipped || sub.warn) return "⚠️ ";
3068
+ if (sub.skipped) return "⏭️ ";
3069
+ if (sub.warn) return "⚠️ ";
2740
3070
  return sub.ok ? "✅" : "❌";
2741
3071
  }
2742
3072
  function renderStage(result, verbose, out) {
@@ -2899,6 +3229,14 @@ function runAdd(argv, cwd, deps = defaultAddDeps) {
2899
3229
  }
2900
3230
  return 0;
2901
3231
  }
3232
+ function readInstalledVersion(cwd, name) {
3233
+ try {
3234
+ const manifest = (0, node_path.join)(cwd, "node_modules", ...name.split("/"), "package.json");
3235
+ return JSON.parse((0, node_fs.readFileSync)(manifest, "utf8")).version ?? null;
3236
+ } catch {
3237
+ return null;
3238
+ }
3239
+ }
2902
3240
  function readOwnVersion() {
2903
3241
  const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
2904
3242
  for (const rel of ["./package.json", "../package.json"]) try {
@@ -2996,6 +3334,7 @@ function resolveInitDeps(options) {
2996
3334
  install: options.installPackages ?? installPackages,
2997
3335
  probeAuth: options.probeAuthInstance ?? probeAuthInstance,
2998
3336
  readEnv: options.readEnv ?? defaultReadEnv,
3337
+ readInstalledVersion: options.readInstalledVersion ?? readInstalledVersion,
2999
3338
  readOwnVersion: options.readOwnVersion ?? readOwnVersion,
3000
3339
  writeEnv: options.writeEnv ?? defaultWriteEnv,
3001
3340
  isInteractive: options.yes ? () => false : options.isInteractive ?? defaultIsInteractive,
@@ -3095,12 +3434,12 @@ function applyMutations(options, deps, mutations) {
3095
3434
  deps.writeConfig((0, node_path.join)(options.cwd, file.path), file.contents);
3096
3435
  deps.out(` ✓ ${file.path}\n`);
3097
3436
  }
3098
- if (mutations.env.action === "skip") {
3099
- deps.out(" • .env already has every CLIVLY_* key — skipped\n");
3100
- return;
3437
+ if (mutations.env.action === "skip") deps.out(" • .env already has every CLIVLY_* key — skipped\n");
3438
+ else {
3439
+ deps.writeEnv(options.cwd, mutations.env.contents);
3440
+ deps.out(" ✓ .env placeholders\n");
3101
3441
  }
3102
- deps.writeEnv(options.cwd, mutations.env.contents);
3103
- deps.out(" ✓ .env placeholders\n");
3442
+ for (const step of mutations.manualSteps) deps.out(`\n ! ${step}\n`);
3104
3443
  }
3105
3444
  const DIFF_CONTEXT_LIMIT = 40;
3106
3445
  function renderDiff(existing, next) {
@@ -3120,6 +3459,7 @@ function previewMutations(deps, mutations, options) {
3120
3459
  conflict: " (differs — --diff to see, --force to overwrite)",
3121
3460
  create: ` (${lines} lines)`,
3122
3461
  overwrite: ` (${lines} lines)`,
3462
+ patch: " (your file, plus the Clivly plugin)",
3123
3463
  unchanged: " (already up to date)"
3124
3464
  }[file.action];
3125
3465
  out(` ${file.action.padEnd(9)} ${file.path}${suffix}\n`);
@@ -3177,13 +3517,17 @@ async function scaffold(options, deps, plan) {
3177
3517
  });
3178
3518
  return 0;
3179
3519
  }
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`);
3520
+ const missing = packagesNeedingInstall(packages, (name) => deps.readInstalledVersion(options.cwd, name));
3521
+ if (missing.length === 0) out(`Already installed (${pm}) — nothing to fetch\n`);
3522
+ else {
3523
+ const result = await deps.install({
3524
+ cwd: options.cwd,
3525
+ pm,
3526
+ packages: missing
3527
+ });
3528
+ if (result.ok) out(`Installed clivly (${pm})\n`);
3529
+ else out(`Could not install automatically — run:\n ${result.command}\n`);
3530
+ }
3187
3531
  applyMutations(options, deps, mutations);
3188
3532
  let loginRan = false;
3189
3533
  if (credentialsMissing(mutations.env.contents)) {
package/index.d.cts CHANGED
@@ -98,6 +98,7 @@ interface RunInitOptions {
98
98
  readDeps?: (cwd: string) => Record<string, string>;
99
99
  readEnv?: (cwd: string) => string;
100
100
  readFile?: (path: string) => string | null;
101
+ readInstalledVersion?: (cwd: string, name: string) => string | null;
101
102
  readOwnVersion?: () => string | undefined;
102
103
  resolveAppLayout?: (cwd: string, fileExists: (path: string) => boolean) => AppLayout;
103
104
  resolveSchemaPath?: (cwd: string) => string | null;
package/index.d.mts CHANGED
@@ -98,6 +98,7 @@ interface RunInitOptions {
98
98
  readDeps?: (cwd: string) => Record<string, string>;
99
99
  readEnv?: (cwd: string) => string;
100
100
  readFile?: (path: string) => string | null;
101
+ readInstalledVersion?: (cwd: string, name: string) => string | null;
101
102
  readOwnVersion?: () => string | undefined;
102
103
  resolveAppLayout?: (cwd: string, fileExists: (path: string) => boolean) => AppLayout;
103
104
  resolveSchemaPath?: (cwd: string) => string | null;