@happyvertical/smrt-core 0.53.3 → 0.53.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 (68) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +47 -0
  3. package/agents/build-knowledge.md +17 -0
  4. package/agents/generators.md +24 -1
  5. package/dist/class.d.ts.map +1 -1
  6. package/dist/class.js +29 -5
  7. package/dist/class.js.map +1 -1
  8. package/dist/generated-collection-access.d.ts +43 -0
  9. package/dist/generated-collection-access.d.ts.map +1 -0
  10. package/dist/generated-collection-access.js +71 -0
  11. package/dist/generated-collection-access.js.map +1 -0
  12. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  13. package/dist/generators/mcp-runtime-template.js +8 -5
  14. package/dist/generators/mcp-runtime-template.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -2
  18. package/dist/manifest/static-manifest.js +1 -1
  19. package/dist/manifest/static-manifest.js.map +1 -1
  20. package/dist/manifest/store.js +1 -1
  21. package/dist/manifest.json +1 -1
  22. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  23. package/dist/scanner/manifest-generator.js +3 -1
  24. package/dist/scanner/manifest-generator.js.map +1 -1
  25. package/dist/smrt-knowledge.json +10 -9
  26. package/dist/utils/qualified-names.d.ts +15 -0
  27. package/dist/utils/qualified-names.d.ts.map +1 -1
  28. package/dist/utils/qualified-names.js +31 -1
  29. package/dist/utils/qualified-names.js.map +1 -1
  30. package/dist/vite-plugin/changes-route.d.ts +4 -3
  31. package/dist/vite-plugin/changes-route.d.ts.map +1 -1
  32. package/dist/vite-plugin/changes-route.js +8 -7
  33. package/dist/vite-plugin/changes-route.js.map +1 -1
  34. package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
  35. package/dist/vite-plugin/dev-plane-route.js +4 -4
  36. package/dist/vite-plugin/dev-plane-route.js.map +1 -1
  37. package/dist/vite-plugin/events-route.d.ts.map +1 -1
  38. package/dist/vite-plugin/events-route.js +4 -4
  39. package/dist/vite-plugin/events-route.js.map +1 -1
  40. package/dist/vite-plugin/index.d.ts.map +1 -1
  41. package/dist/vite-plugin/index.js +21 -7
  42. package/dist/vite-plugin/index.js.map +1 -1
  43. package/dist/vite-plugin/resources-route.d.ts.map +1 -1
  44. package/dist/vite-plugin/resources-route.js +9 -4
  45. package/dist/vite-plugin/resources-route.js.map +1 -1
  46. package/dist/vite-plugin/sveltekit-config-import.d.ts +22 -0
  47. package/dist/vite-plugin/sveltekit-config-import.d.ts.map +1 -1
  48. package/dist/vite-plugin/sveltekit-config-import.js +44 -2
  49. package/dist/vite-plugin/sveltekit-config-import.js.map +1 -1
  50. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  51. package/dist/vite-plugin/sveltekit-generator.js +59 -9
  52. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  53. package/dist/vite-plugin/sveltekit-register-injection.d.ts +26 -0
  54. package/dist/vite-plugin/sveltekit-register-injection.d.ts.map +1 -0
  55. package/dist/vite-plugin/sveltekit-register-injection.js +37 -0
  56. package/dist/vite-plugin/sveltekit-register-injection.js.map +1 -0
  57. package/dist/vite-plugin/sync-apply-route.d.ts +1 -1
  58. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  59. package/dist/vite-plugin/sync-apply-route.js +4 -4
  60. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  61. package/dist/vite-preset/index.d.ts +65 -0
  62. package/dist/vite-preset/index.d.ts.map +1 -0
  63. package/dist/vite-preset/index.js +105 -0
  64. package/dist/vite-preset/index.js.map +1 -0
  65. package/dist/vite.d.ts +5 -0
  66. package/dist/vite.d.ts.map +1 -0
  67. package/dist/vite.js +2 -0
  68. package/package.json +15 -10
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Register generated objects before the application's server config module
3
+ * runs (#3416).
4
+ *
5
+ * The config module (`src/lib/server/smrt.ts` by default) creates the app
6
+ * runtime and is the first SMRT module the server imports (`hooks.server.ts`,
7
+ * page loads, and every generated route's access prelude import it). The
8
+ * plugin generates `smrt-register.ts` beside it on every dev/build run, so it
9
+ * prepends that import to the config module instead of each application
10
+ * hand-writing it behind a "not generated yet" guard. Registration stays the
11
+ * generated module's deterministic `consumer.packages` + local-object order.
12
+ */
13
+ /** File name of the generated registration module beside the config module. */
14
+ export declare const SVELTEKIT_REGISTRATION_FILE_NAME = "smrt-register.ts";
15
+ export interface SvelteKitRegisterInjectionOptions {
16
+ projectRoot: string;
17
+ configPath?: string;
18
+ configFileName?: string;
19
+ }
20
+ /**
21
+ * Return `code` with the generated registration import prepended when `id` is
22
+ * the configured config module and the registration module exists; otherwise
23
+ * `null`. The import shares the first line, so later line numbers are kept.
24
+ */
25
+ export declare function injectSvelteKitRegistration(code: string, id: string, options: SvelteKitRegisterInjectionOptions): string | null;
26
+ //# sourceMappingURL=sveltekit-register-injection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sveltekit-register-injection.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/sveltekit-register-injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,+EAA+E;AAC/E,eAAO,MAAM,gCAAgC,qBAAqB,CAAC;AAInE,MAAM,WAAW,iCAAiC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,iCAAiC,GACzC,MAAM,GAAG,IAAI,CAWf"}
@@ -0,0 +1,37 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ //#region src/vite-plugin/sveltekit-register-injection.ts
4
+ /**
5
+ * Register generated objects before the application's server config module
6
+ * runs (#3416).
7
+ *
8
+ * The config module (`src/lib/server/smrt.ts` by default) creates the app
9
+ * runtime and is the first SMRT module the server imports (`hooks.server.ts`,
10
+ * page loads, and every generated route's access prelude import it). The
11
+ * plugin generates `smrt-register.ts` beside it on every dev/build run, so it
12
+ * prepends that import to the config module instead of each application
13
+ * hand-writing it behind a "not generated yet" guard. Registration stays the
14
+ * generated module's deterministic `consumer.packages` + local-object order.
15
+ */
16
+ /** File name of the generated registration module beside the config module. */
17
+ var SVELTEKIT_REGISTRATION_FILE_NAME = "smrt-register.ts";
18
+ var REGISTRATION_IMPORT = "import './smrt-register.js';";
19
+ function normalizeModulePath(id) {
20
+ return resolve(id.replace(/[?#].*$/, "")).replace(/\\/g, "/");
21
+ }
22
+ /**
23
+ * Return `code` with the generated registration import prepended when `id` is
24
+ * the configured config module and the registration module exists; otherwise
25
+ * `null`. The import shares the first line, so later line numbers are kept.
26
+ */
27
+ function injectSvelteKitRegistration(code, id, options) {
28
+ const configDir = resolve(options.projectRoot, options.configPath || "src/lib/server");
29
+ const configFile = join(configDir, options.configFileName || "smrt.ts");
30
+ if (normalizeModulePath(id) !== normalizeModulePath(configFile)) return null;
31
+ if (!existsSync(join(configDir, "smrt-register.ts"))) return null;
32
+ return `${REGISTRATION_IMPORT}${code}`;
33
+ }
34
+ //#endregion
35
+ export { SVELTEKIT_REGISTRATION_FILE_NAME, injectSvelteKitRegistration };
36
+
37
+ //# sourceMappingURL=sveltekit-register-injection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sveltekit-register-injection.js","names":[],"sources":["../../src/vite-plugin/sveltekit-register-injection.ts"],"sourcesContent":["/**\n * Register generated objects before the application's server config module\n * runs (#3416).\n *\n * The config module (`src/lib/server/smrt.ts` by default) creates the app\n * runtime and is the first SMRT module the server imports (`hooks.server.ts`,\n * page loads, and every generated route's access prelude import it). The\n * plugin generates `smrt-register.ts` beside it on every dev/build run, so it\n * prepends that import to the config module instead of each application\n * hand-writing it behind a \"not generated yet\" guard. Registration stays the\n * generated module's deterministic `consumer.packages` + local-object order.\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\n\n/** File name of the generated registration module beside the config module. */\nexport const SVELTEKIT_REGISTRATION_FILE_NAME = 'smrt-register.ts';\n\nconst REGISTRATION_IMPORT = \"import './smrt-register.js';\";\n\nexport interface SvelteKitRegisterInjectionOptions {\n projectRoot: string;\n configPath?: string;\n configFileName?: string;\n}\n\nfunction normalizeModulePath(id: string): string {\n return resolve(id.replace(/[?#].*$/, '')).replace(/\\\\/g, '/');\n}\n\n/**\n * Return `code` with the generated registration import prepended when `id` is\n * the configured config module and the registration module exists; otherwise\n * `null`. The import shares the first line, so later line numbers are kept.\n */\nexport function injectSvelteKitRegistration(\n code: string,\n id: string,\n options: SvelteKitRegisterInjectionOptions,\n): string | null {\n const configDir = resolve(\n options.projectRoot,\n options.configPath || 'src/lib/server',\n );\n const configFile = join(configDir, options.configFileName || 'smrt.ts');\n if (normalizeModulePath(id) !== normalizeModulePath(configFile)) return null;\n if (!existsSync(join(configDir, SVELTEKIT_REGISTRATION_FILE_NAME))) {\n return null;\n }\n return `${REGISTRATION_IMPORT}${code}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAa,mCAAmC;AAEhD,IAAM,sBAAsB;AAQ5B,SAAS,oBAAoB,IAAoB;CAC/C,OAAO,QAAQ,GAAG,QAAQ,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,OAAO,GAAG;AAC9D;;;;;;AAOA,SAAgB,4BACd,MACA,IACA,SACe;CACf,MAAM,YAAY,QAChB,QAAQ,aACR,QAAQ,cAAc,gBACxB;CACA,MAAM,aAAa,KAAK,WAAW,QAAQ,kBAAkB,SAAS;CACtE,IAAI,oBAAoB,EAAE,MAAM,oBAAoB,UAAU,GAAG,OAAO;CACxE,IAAI,CAAC,WAAW,KAAK,WAAA,kBAA2C,CAAC,GAC/D,OAAO;CAET,OAAO,GAAG,sBAAsB;AAClC"}
@@ -52,7 +52,7 @@ export declare function collectSyncApplyTargets(manifest: SmartObjectManifest):
52
52
  * Render the generated `+server.ts` content for the sync-apply route.
53
53
  * Exported for tests.
54
54
  */
55
- export declare function generateSyncApplyRouteTemplate(targets: SyncTargetSpec[], configImport: string): string;
55
+ export declare function generateSyncApplyRouteTemplate(targets: SyncTargetSpec[], collectionAccessImports: string): string;
56
56
  /**
57
57
  * Generate `{routesDir}/sync/apply/+server.ts` for the manifest's syncable
58
58
  * models. Returns true when a route was written. Skips generation (with a
@@ -1 +1 @@
1
- {"version":3,"file":"sync-apply-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/sync-apply-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,QAAA,MAAM,gBAAgB,yCAA0C,CAAC;AAEjE,KAAK,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,wDAAwD;AACxD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,cAAc,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,YAAY,EAAE,OAAO,CAAC;CACvB;AA6ED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,mBAAmB,GAC5B,cAAc,EAAE,CAiClB;AAiBD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,cAAc,EAAE,EACzB,YAAY,EAAE,MAAM,GACnB,MAAM,CAkJR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAiCT"}
1
+ {"version":3,"file":"sync-apply-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/sync-apply-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAEV,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,QAAA,MAAM,gBAAgB,yCAA0C,CAAC;AAEjE,KAAK,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,wDAAwD;AACxD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,cAAc,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,YAAY,EAAE,OAAO,CAAC;CACvB;AA6ED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,mBAAmB,GAC5B,cAAc,EAAE,CAiClB;AAiBD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,cAAc,EAAE,EACzB,uBAAuB,EAAE,MAAM,GAC9B,MAAM,CAkJR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAmCT"}
@@ -1,6 +1,6 @@
1
1
  import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
2
2
  import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
3
- import { resolveSvelteKitConfigImport } from "./sveltekit-config-import.js";
3
+ import { generateCollectionAccessImports } from "./sveltekit-config-import.js";
4
4
  import { isCollectionManifestClass } from "./web-collections.js";
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { join } from "node:path";
@@ -117,7 +117,7 @@ function serializeTargets(targets) {
117
117
  * Render the generated `+server.ts` content for the sync-apply route.
118
118
  * Exported for tests.
119
119
  */
120
- function generateSyncApplyRouteTemplate(targets, configImport) {
120
+ function generateSyncApplyRouteTemplate(targets, collectionAccessImports) {
121
121
  const anyTenantScoped = targets.some((target) => target.tenantScoped);
122
122
  return `${AUTO_GENERATED_ROUTE_HEADER}
123
123
  // DO NOT EDIT - changes will be overwritten
@@ -133,8 +133,8 @@ import {
133
133
  type SyncApplyTarget,
134
134
  } from '@happyvertical/smrt-core';
135
135
  import { json } from '@sveltejs/kit';
136
- import { getCollection } from '${configImport}';
137
136
  import type { RequestHandler } from './$types';
137
+ ${collectionAccessImports}
138
138
  ${anyTenantScoped ? `
139
139
  import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
140
140
 
@@ -277,7 +277,7 @@ function generateSyncApplyRoute(projectRoot, manifest, options) {
277
277
  }
278
278
  }
279
279
  if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
280
- writeFileSync(routePath, generateSyncApplyRouteTemplate(targets, resolveSvelteKitConfigImport(projectRoot, routeDir, options)), "utf-8");
280
+ writeFileSync(routePath, generateSyncApplyRouteTemplate(targets, generateCollectionAccessImports(projectRoot, routeDir, options, ["getCollection"])), "utf-8");
281
281
  console.log(`[smrt] Generated: ${routePath}`);
282
282
  return true;
283
283
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sync-apply-route.js","names":[],"sources":["../../src/vite-plugin/sync-apply-route.ts"],"sourcesContent":["/**\n * SvelteKit generator for the sync-apply batch write route (#1759).\n *\n * Emits ONE route — `{routesDir}/sync/apply/+server.ts` — implementing the\n * shared web/mobile write contract for every syncable model in the manifest\n * (models whose API config exposes at least one of create/update/delete). The\n * generated file is deliberately thin: it maps `object` segments to\n * collections plus per-model policy data, and delegates all batch processing\n * to `processSyncApplyBatch` from `@happyvertical/smrt-core` — the same\n * engine the runtime REST generator uses, so the two transports cannot drift.\n *\n * This lives in its own module (rather than sveltekit-generator.ts) so the\n * shared generator file only needs a one-line registration hook; sibling\n * work (#1757, #1758) touches the same generator files in parallel.\n *\n * Contract documentation: docs/content/architecture/sync-apply-contract.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport type { ApiConfig } from '../registry/types.js';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport { resolveSvelteKitConfigImport } from './sveltekit-config-import.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\nimport { isCollectionManifestClass } from './web-collections.js';\n\nconst MUTATING_ACTIONS = ['create', 'update', 'delete'] as const;\n\ntype MutatingAction = (typeof MUTATING_ACTIONS)[number];\n\n/** Per-model data embedded into the generated route. */\ninterface SyncTargetSpec {\n segment: string;\n /**\n * The manifest registry key, passed VERBATIM to `getCollection()` exactly\n * as the generated CRUD routes do. May be package-qualified\n * (`@happyvertical/smrt-ledgers:Account`) — collapsing it to a simple class\n * name would resolve ambiguously when two loaded packages declare the same\n * simple name.\n */\n registryKey: string;\n /**\n * `<collection>` prefix of the `<collection>.<action>` operation permission\n * the mirrored generated write routes require (#2977) — the same\n * `decoratorConfig.collection ?? collection` resolution as\n * sveltekit-generator.ts `resolvePermissionCollection`.\n */\n permissionCollection: string;\n ops: MutatingAction[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n tenantScoped: boolean;\n}\n\nfunction getApiConfigObject(apiConfig: unknown): ApiConfig | null {\n if (!apiConfig || typeof apiConfig !== 'object') {\n return null;\n }\n return apiConfig as ApiConfig;\n}\n\n/**\n * Mirror of sveltekit-generator.ts `resolveStandardCrudActions`, narrowed to\n * the mutating subset the sync contract can carry. Kept local so this module\n * needs no exports added to the (parallel-edited) shared generator file.\n */\nfunction resolveMutatingActions(apiConfig: unknown): MutatingAction[] {\n if (apiConfig === false) return [];\n if (\n apiConfig === true ||\n apiConfig === undefined ||\n typeof apiConfig !== 'object'\n ) {\n return [...MUTATING_ACTIONS];\n }\n\n const config = apiConfig as { include?: string[]; exclude?: string[] };\n let actions: MutatingAction[] = Array.isArray(config.include)\n ? MUTATING_ACTIONS.filter((action) => config.include?.includes(action))\n : [...MUTATING_ACTIONS];\n if (Array.isArray(config.exclude)) {\n const excluded = config.exclude;\n actions = actions.filter((action) => !excluded.includes(action));\n }\n return actions;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiPublicAccess` (fail-closed). */\nfunction getApiPublicAccess(apiConfig: unknown): boolean | 'read' {\n const config = getApiConfigObject(apiConfig);\n const value = config?.public;\n if (value === true || value === 'read') {\n return value;\n }\n return false;\n}\n\n/** Mirror of sveltekit-generator.ts `collectReadonlyFieldNames` (#1540, 2b). */\nfunction collectReadonlyFieldNames(objectDef: SmartObjectDefinition): string[] {\n const fields = objectDef.fields || {};\n const names: string[] = [];\n for (const [name, def] of Object.entries(fields)) {\n const meta = (def as { _meta?: Record<string, unknown> })._meta;\n if (\n (def as { readonly?: boolean }).readonly === true ||\n meta?.readonly === true\n ) {\n names.push(name);\n }\n }\n return names;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiWritableAllowlist`. */\nfunction getApiWritableAllowlist(apiConfig: unknown): string[] | null {\n const config = getApiConfigObject(apiConfig);\n return Array.isArray(config?.writable) ? config.writable : null;\n}\n\n/** Mirror of sveltekit-generator.ts `resolvePermissionCollection` (#2977). */\nfunction resolvePermissionCollection(objectDef: SmartObjectDefinition): string {\n const configured = (\n objectDef.decoratorConfig as { collection?: unknown } | undefined\n )?.collection;\n return typeof configured === 'string' && configured.length > 0\n ? configured\n : objectDef.collection;\n}\n\n/**\n * Collect the syncable targets from a manifest: non-collection objects whose\n * API config is enabled and exposes at least one mutating action.\n */\nexport function collectSyncApplyTargets(\n manifest: SmartObjectManifest,\n): SyncTargetSpec[] {\n const targets: SyncTargetSpec[] = [];\n\n for (const [className, objectDef] of Object.entries(manifest.objects)) {\n if (isCollectionManifestClass(manifest, objectDef)) continue;\n // The framework's own abstract base classes are scaffolding, not\n // resources — never make them a sync-apply target, regardless of\n // config (#2642).\n if (isFrameworkBaseClass(objectDef.className, objectDef.packageName)) {\n continue;\n }\n\n const apiConfig = objectDef.decoratorConfig?.api;\n if (apiConfig === false) continue;\n\n const ops = resolveMutatingActions(apiConfig);\n if (ops.length === 0) continue;\n\n if (!objectDef.collection) continue;\n\n targets.push({\n segment: objectDef.collection,\n registryKey: className,\n permissionCollection: resolvePermissionCollection(objectDef),\n ops,\n readonlyFields: collectReadonlyFieldNames(objectDef),\n writableAllowlist: getApiWritableAllowlist(apiConfig),\n publicAccess: getApiPublicAccess(apiConfig),\n tenantScoped: !!objectDef.decoratorConfig?.tenantScoped,\n });\n }\n\n return targets.sort((a, b) => a.segment.localeCompare(b.segment));\n}\n\nfunction serializeTargets(targets: SyncTargetSpec[]): string {\n const entries = targets.map((target) => {\n const fields = [\n `registryKey: ${JSON.stringify(target.registryKey)}`,\n `permissionCollection: ${JSON.stringify(target.permissionCollection)}`,\n `ops: ${JSON.stringify(target.ops)}`,\n `readonlyFields: ${JSON.stringify(target.readonlyFields)}`,\n `writableAllowlist: ${JSON.stringify(target.writableAllowlist)}`,\n `publicAccess: ${JSON.stringify(target.publicAccess)}`,\n ];\n return ` ${JSON.stringify(target.segment)}: {\\n ${fields.join(',\\n ')},\\n },`;\n });\n return `{\\n${entries.join('\\n')}\\n}`;\n}\n\n/**\n * Render the generated `+server.ts` content for the sync-apply route.\n * Exported for tests.\n */\nexport function generateSyncApplyRouteTemplate(\n targets: SyncTargetSpec[],\n configImport: string,\n): string {\n const anyTenantScoped = targets.some((target) => target.tenantScoped);\n\n const tenantHelper = anyTenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// Idempotent sync-apply batch write contract (#1759) — the shared write path\n// for the web offline outbox (#1762) and the KMP mobile write queue (#1739).\n// Contract: docs/content/architecture/sync-apply-contract.md\n\nimport {\n applySyncWritablePolicy,\n processSyncApplyBatch,\n type SyncApplyOp,\n type SyncApplyTarget,\n} from '@happyvertical/smrt-core';\nimport { json } from '@sveltejs/kit';\nimport { getCollection } from '${configImport}';\nimport type { RequestHandler } from './$types';\n${tenantHelper}\ninterface SyncTargetConfig {\n /** Manifest registry key (may be package-qualified), as CRUD routes use. */\n registryKey: string;\n /** \\`<collection>\\` prefix of the required \\`<collection>.<op>\\` permission. */\n permissionCollection: string;\n ops: SyncApplyOp[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n}\n\n// One entry per syncable model, keyed by the collection route segment its\n// generated CRUD routes use.\nconst SYNC_TARGETS: Record<string, SyncTargetConfig> = ${serializeTargets(targets)};\n\n// Fail-closed authorization (#1540): sync mutations require an authenticated\n// principal on \\`locals\\` unless the target model is \\`api: { public: true }\\`.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction readRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction hasGrantedPermission(value: unknown, permission: string): boolean {\n if (Array.isArray(value)) return value.includes(permission);\n if (value instanceof Set) return value.has(permission);\n return false;\n}\n\n// Fail-closed operation permission (#2977, #3011): each applied op requires\n// the same \\`<collection>.<op>\\` permission the mirrored generated write route\n// enforces, read from the principal's session permission snapshot. A missing\n// snapshot denies; only an explicit tenancy super-admin bypass skips it.\nfunction hasOperationPermission(\n locals: unknown,\n permissionCollection: string,\n op: SyncApplyOp,\n): boolean {\n const l = readRecord(locals);\n const tenantContext = readRecord(l.tenantContext);\n if (tenantContext.superAdminBypass === true) return true;\n const permission = \\`\\${permissionCollection}.\\${op}\\`;\n return (\n hasGrantedPermission(l.permissions, permission) ||\n hasGrantedPermission(l.permissionSet, permission) ||\n hasGrantedPermission(l.smrtPermissions, permission) ||\n hasGrantedPermission(tenantContext.permissions, permission)\n );\n}\n\nexport const POST: RequestHandler = async ({ locals, request }) => {\n const authenticated = hasAuthenticatedPrincipal(locals);\n${anyTenantScoped ? ' establishTenantContext(locals);\\n' : ''}\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return json(\n { error: { code: 'invalid_batch', message: 'Invalid JSON body' } },\n { status: 400 },\n );\n }\n\n const outcome = await processSyncApplyBatch(body, {\n resolveTarget: async (segment: string): Promise<SyncApplyTarget | null> => {\n const target = SYNC_TARGETS[segment];\n if (!target) return null;\n // The verbatim manifest key, exactly as the generated CRUD routes pass\n // it — package-qualified keys stay qualified so same-simple-name\n // classes across packages resolve unambiguously.\n const collection = await getCollection(target.registryKey);\n return {\n objectName: target.registryKey,\n collection,\n isOpAllowed: (op: SyncApplyOp) => target.ops.includes(op),\n authorize: (op: SyncApplyOp) => {\n // \\`public: true\\` keeps its unauthenticated write semantics, as on\n // the generated CRUD routes.\n if (target.publicAccess === true) return 'ok' as const;\n if (!authenticated) return 'auth_required' as const;\n return hasOperationPermission(locals, target.permissionCollection, op)\n ? ('ok' as const)\n : ('forbidden' as const);\n },\n prepare: (payload: Record<string, unknown>) =>\n applySyncWritablePolicy(payload, {\n readonlyFields: target.readonlyFields,\n writableAllowlist: target.writableAllowlist,\n }),\n };\n },\n });\n\n return json(outcome.body, { status: outcome.status });\n};\n`;\n}\n\n/**\n * Generate `{routesDir}/sync/apply/+server.ts` for the manifest's syncable\n * models. Returns true when a route was written. Skips generation (with a\n * warning) if a non-generated file already occupies the path, and writes\n * nothing when the manifest has no syncable models.\n */\nexport function generateSyncApplyRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n const targets = collectSyncApplyTargets(manifest);\n if (targets.length === 0) {\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, 'sync', 'apply');\n const routePath = join(routeDir, '+server.ts');\n\n if (existsSync(routePath)) {\n const existing = readFileSync(routePath, 'utf-8');\n if (!existing.startsWith(AUTO_GENERATED_ROUTE_HEADER)) {\n console.warn(\n `[smrt] Skipping sync-apply route - ${routePath} exists and is not generated`,\n );\n return false;\n }\n }\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n\n writeFileSync(\n routePath,\n generateSyncApplyRouteTemplate(\n targets,\n resolveSvelteKitConfigImport(projectRoot, routeDir, options),\n ),\n 'utf-8',\n );\n console.log(`[smrt] Generated: ${routePath}`);\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync-apply-route.js","names":[],"sources":["../../src/vite-plugin/sync-apply-route.ts"],"sourcesContent":["/**\n * SvelteKit generator for the sync-apply batch write route (#1759).\n *\n * Emits ONE route — `{routesDir}/sync/apply/+server.ts` — implementing the\n * shared web/mobile write contract for every syncable model in the manifest\n * (models whose API config exposes at least one of create/update/delete). The\n * generated file is deliberately thin: it maps `object` segments to\n * collections plus per-model policy data, and delegates all batch processing\n * to `processSyncApplyBatch` from `@happyvertical/smrt-core` — the same\n * engine the runtime REST generator uses, so the two transports cannot drift.\n *\n * This lives in its own module (rather than sveltekit-generator.ts) so the\n * shared generator file only needs a one-line registration hook; sibling\n * work (#1757, #1758) touches the same generator files in parallel.\n *\n * Contract documentation: docs/content/architecture/sync-apply-contract.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport type { ApiConfig } from '../registry/types.js';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport { generateCollectionAccessImports } from './sveltekit-config-import.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\nimport { isCollectionManifestClass } from './web-collections.js';\n\nconst MUTATING_ACTIONS = ['create', 'update', 'delete'] as const;\n\ntype MutatingAction = (typeof MUTATING_ACTIONS)[number];\n\n/** Per-model data embedded into the generated route. */\ninterface SyncTargetSpec {\n segment: string;\n /**\n * The manifest registry key, passed VERBATIM to `getCollection()` exactly\n * as the generated CRUD routes do. May be package-qualified\n * (`@happyvertical/smrt-ledgers:Account`) — collapsing it to a simple class\n * name would resolve ambiguously when two loaded packages declare the same\n * simple name.\n */\n registryKey: string;\n /**\n * `<collection>` prefix of the `<collection>.<action>` operation permission\n * the mirrored generated write routes require (#2977) — the same\n * `decoratorConfig.collection ?? collection` resolution as\n * sveltekit-generator.ts `resolvePermissionCollection`.\n */\n permissionCollection: string;\n ops: MutatingAction[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n tenantScoped: boolean;\n}\n\nfunction getApiConfigObject(apiConfig: unknown): ApiConfig | null {\n if (!apiConfig || typeof apiConfig !== 'object') {\n return null;\n }\n return apiConfig as ApiConfig;\n}\n\n/**\n * Mirror of sveltekit-generator.ts `resolveStandardCrudActions`, narrowed to\n * the mutating subset the sync contract can carry. Kept local so this module\n * needs no exports added to the (parallel-edited) shared generator file.\n */\nfunction resolveMutatingActions(apiConfig: unknown): MutatingAction[] {\n if (apiConfig === false) return [];\n if (\n apiConfig === true ||\n apiConfig === undefined ||\n typeof apiConfig !== 'object'\n ) {\n return [...MUTATING_ACTIONS];\n }\n\n const config = apiConfig as { include?: string[]; exclude?: string[] };\n let actions: MutatingAction[] = Array.isArray(config.include)\n ? MUTATING_ACTIONS.filter((action) => config.include?.includes(action))\n : [...MUTATING_ACTIONS];\n if (Array.isArray(config.exclude)) {\n const excluded = config.exclude;\n actions = actions.filter((action) => !excluded.includes(action));\n }\n return actions;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiPublicAccess` (fail-closed). */\nfunction getApiPublicAccess(apiConfig: unknown): boolean | 'read' {\n const config = getApiConfigObject(apiConfig);\n const value = config?.public;\n if (value === true || value === 'read') {\n return value;\n }\n return false;\n}\n\n/** Mirror of sveltekit-generator.ts `collectReadonlyFieldNames` (#1540, 2b). */\nfunction collectReadonlyFieldNames(objectDef: SmartObjectDefinition): string[] {\n const fields = objectDef.fields || {};\n const names: string[] = [];\n for (const [name, def] of Object.entries(fields)) {\n const meta = (def as { _meta?: Record<string, unknown> })._meta;\n if (\n (def as { readonly?: boolean }).readonly === true ||\n meta?.readonly === true\n ) {\n names.push(name);\n }\n }\n return names;\n}\n\n/** Mirror of sveltekit-generator.ts `getApiWritableAllowlist`. */\nfunction getApiWritableAllowlist(apiConfig: unknown): string[] | null {\n const config = getApiConfigObject(apiConfig);\n return Array.isArray(config?.writable) ? config.writable : null;\n}\n\n/** Mirror of sveltekit-generator.ts `resolvePermissionCollection` (#2977). */\nfunction resolvePermissionCollection(objectDef: SmartObjectDefinition): string {\n const configured = (\n objectDef.decoratorConfig as { collection?: unknown } | undefined\n )?.collection;\n return typeof configured === 'string' && configured.length > 0\n ? configured\n : objectDef.collection;\n}\n\n/**\n * Collect the syncable targets from a manifest: non-collection objects whose\n * API config is enabled and exposes at least one mutating action.\n */\nexport function collectSyncApplyTargets(\n manifest: SmartObjectManifest,\n): SyncTargetSpec[] {\n const targets: SyncTargetSpec[] = [];\n\n for (const [className, objectDef] of Object.entries(manifest.objects)) {\n if (isCollectionManifestClass(manifest, objectDef)) continue;\n // The framework's own abstract base classes are scaffolding, not\n // resources — never make them a sync-apply target, regardless of\n // config (#2642).\n if (isFrameworkBaseClass(objectDef.className, objectDef.packageName)) {\n continue;\n }\n\n const apiConfig = objectDef.decoratorConfig?.api;\n if (apiConfig === false) continue;\n\n const ops = resolveMutatingActions(apiConfig);\n if (ops.length === 0) continue;\n\n if (!objectDef.collection) continue;\n\n targets.push({\n segment: objectDef.collection,\n registryKey: className,\n permissionCollection: resolvePermissionCollection(objectDef),\n ops,\n readonlyFields: collectReadonlyFieldNames(objectDef),\n writableAllowlist: getApiWritableAllowlist(apiConfig),\n publicAccess: getApiPublicAccess(apiConfig),\n tenantScoped: !!objectDef.decoratorConfig?.tenantScoped,\n });\n }\n\n return targets.sort((a, b) => a.segment.localeCompare(b.segment));\n}\n\nfunction serializeTargets(targets: SyncTargetSpec[]): string {\n const entries = targets.map((target) => {\n const fields = [\n `registryKey: ${JSON.stringify(target.registryKey)}`,\n `permissionCollection: ${JSON.stringify(target.permissionCollection)}`,\n `ops: ${JSON.stringify(target.ops)}`,\n `readonlyFields: ${JSON.stringify(target.readonlyFields)}`,\n `writableAllowlist: ${JSON.stringify(target.writableAllowlist)}`,\n `publicAccess: ${JSON.stringify(target.publicAccess)}`,\n ];\n return ` ${JSON.stringify(target.segment)}: {\\n ${fields.join(',\\n ')},\\n },`;\n });\n return `{\\n${entries.join('\\n')}\\n}`;\n}\n\n/**\n * Render the generated `+server.ts` content for the sync-apply route.\n * Exported for tests.\n */\nexport function generateSyncApplyRouteTemplate(\n targets: SyncTargetSpec[],\n collectionAccessImports: string,\n): string {\n const anyTenantScoped = targets.some((target) => target.tenantScoped);\n\n const tenantHelper = anyTenantScoped\n ? `\nimport { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';\n\nfunction establishTenantContext(locals: unknown): void {\n if (hasTenantContext()) return;\n if (!locals || typeof locals !== 'object') return;\n const l = locals as Record<string, unknown>;\n const user = l.user as Record<string, unknown> | undefined;\n const session = l.session as Record<string, unknown> | undefined;\n const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;\n if (typeof tenantId === 'string' && tenantId) {\n enterTenantContext({ tenantId });\n }\n}\n`\n : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// Idempotent sync-apply batch write contract (#1759) — the shared write path\n// for the web offline outbox (#1762) and the KMP mobile write queue (#1739).\n// Contract: docs/content/architecture/sync-apply-contract.md\n\nimport {\n applySyncWritablePolicy,\n processSyncApplyBatch,\n type SyncApplyOp,\n type SyncApplyTarget,\n} from '@happyvertical/smrt-core';\nimport { json } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';\n${collectionAccessImports}\n${tenantHelper}\ninterface SyncTargetConfig {\n /** Manifest registry key (may be package-qualified), as CRUD routes use. */\n registryKey: string;\n /** \\`<collection>\\` prefix of the required \\`<collection>.<op>\\` permission. */\n permissionCollection: string;\n ops: SyncApplyOp[];\n readonlyFields: string[];\n writableAllowlist: string[] | null;\n publicAccess: boolean | 'read';\n}\n\n// One entry per syncable model, keyed by the collection route segment its\n// generated CRUD routes use.\nconst SYNC_TARGETS: Record<string, SyncTargetConfig> = ${serializeTargets(targets)};\n\n// Fail-closed authorization (#1540): sync mutations require an authenticated\n// principal on \\`locals\\` unless the target model is \\`api: { public: true }\\`.\nfunction hasAuthenticatedPrincipal(locals: unknown): boolean {\n if (!locals || typeof locals !== 'object') return false;\n const l = locals as Record<string, unknown>;\n const isResolvedPrincipal = (v: unknown) =>\n typeof v === 'object' && v !== null;\n return (\n isResolvedPrincipal(l.user) ||\n isResolvedPrincipal(l.session) ||\n l.smrtAuth === true\n );\n}\n\nfunction readRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction hasGrantedPermission(value: unknown, permission: string): boolean {\n if (Array.isArray(value)) return value.includes(permission);\n if (value instanceof Set) return value.has(permission);\n return false;\n}\n\n// Fail-closed operation permission (#2977, #3011): each applied op requires\n// the same \\`<collection>.<op>\\` permission the mirrored generated write route\n// enforces, read from the principal's session permission snapshot. A missing\n// snapshot denies; only an explicit tenancy super-admin bypass skips it.\nfunction hasOperationPermission(\n locals: unknown,\n permissionCollection: string,\n op: SyncApplyOp,\n): boolean {\n const l = readRecord(locals);\n const tenantContext = readRecord(l.tenantContext);\n if (tenantContext.superAdminBypass === true) return true;\n const permission = \\`\\${permissionCollection}.\\${op}\\`;\n return (\n hasGrantedPermission(l.permissions, permission) ||\n hasGrantedPermission(l.permissionSet, permission) ||\n hasGrantedPermission(l.smrtPermissions, permission) ||\n hasGrantedPermission(tenantContext.permissions, permission)\n );\n}\n\nexport const POST: RequestHandler = async ({ locals, request }) => {\n const authenticated = hasAuthenticatedPrincipal(locals);\n${anyTenantScoped ? ' establishTenantContext(locals);\\n' : ''}\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n return json(\n { error: { code: 'invalid_batch', message: 'Invalid JSON body' } },\n { status: 400 },\n );\n }\n\n const outcome = await processSyncApplyBatch(body, {\n resolveTarget: async (segment: string): Promise<SyncApplyTarget | null> => {\n const target = SYNC_TARGETS[segment];\n if (!target) return null;\n // The verbatim manifest key, exactly as the generated CRUD routes pass\n // it — package-qualified keys stay qualified so same-simple-name\n // classes across packages resolve unambiguously.\n const collection = await getCollection(target.registryKey);\n return {\n objectName: target.registryKey,\n collection,\n isOpAllowed: (op: SyncApplyOp) => target.ops.includes(op),\n authorize: (op: SyncApplyOp) => {\n // \\`public: true\\` keeps its unauthenticated write semantics, as on\n // the generated CRUD routes.\n if (target.publicAccess === true) return 'ok' as const;\n if (!authenticated) return 'auth_required' as const;\n return hasOperationPermission(locals, target.permissionCollection, op)\n ? ('ok' as const)\n : ('forbidden' as const);\n },\n prepare: (payload: Record<string, unknown>) =>\n applySyncWritablePolicy(payload, {\n readonlyFields: target.readonlyFields,\n writableAllowlist: target.writableAllowlist,\n }),\n };\n },\n });\n\n return json(outcome.body, { status: outcome.status });\n};\n`;\n}\n\n/**\n * Generate `{routesDir}/sync/apply/+server.ts` for the manifest's syncable\n * models. Returns true when a route was written. Skips generation (with a\n * warning) if a non-generated file already occupies the path, and writes\n * nothing when the manifest has no syncable models.\n */\nexport function generateSyncApplyRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n const targets = collectSyncApplyTargets(manifest);\n if (targets.length === 0) {\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, 'sync', 'apply');\n const routePath = join(routeDir, '+server.ts');\n\n if (existsSync(routePath)) {\n const existing = readFileSync(routePath, 'utf-8');\n if (!existing.startsWith(AUTO_GENERATED_ROUTE_HEADER)) {\n console.warn(\n `[smrt] Skipping sync-apply route - ${routePath} exists and is not generated`,\n );\n return false;\n }\n }\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n\n writeFileSync(\n routePath,\n generateSyncApplyRouteTemplate(\n targets,\n generateCollectionAccessImports(projectRoot, routeDir, options, [\n 'getCollection',\n ]),\n ),\n 'utf-8',\n );\n console.log(`[smrt] Generated: ${routePath}`);\n return true;\n}\n"],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Single-plugin Vite preset for SMRT applications.
3
+ *
4
+ * `plugins: [sveltekit(), smrt()]` replaces the hand-copied glue every app
5
+ * carried: the Oxc legacy-decorator block, `smrtConsumer({...})` with an
6
+ * explicit package list, and `smrtPlugin({...})` with the SvelteKit path
7
+ * options. `smrtConsumer` and `smrtPlugin` stay public and unchanged.
8
+ */
9
+ import type { Plugin } from 'vite';
10
+ export interface SmrtPresetOptions {
11
+ /**
12
+ * SMRT packages whose object manifests this app consumes. Takes precedence
13
+ * over `consumer.packages` in `smrt.config.ts`. An empty array declares that
14
+ * the app consumes none. Tooling/UI packages must not be listed.
15
+ */
16
+ packages?: readonly string[];
17
+ /**
18
+ * Project root. Omit to follow the root Vite/SvelteKit resolve (the launch
19
+ * cwd); `smrt.config.ts` is searched from this value or `process.cwd()`.
20
+ */
21
+ projectRoot?: string;
22
+ /** Directory of the app's own `@smrt()` objects. Default `src/lib/objects`. */
23
+ objectsDir?: string;
24
+ /** Generated type declarations. Default `src/lib/types/smrt-generated`. */
25
+ typesDir?: string;
26
+ /** Generated API routes. Default `src/routes/api`. */
27
+ routesDir?: string;
28
+ /** Directory holding the generated runtime config module. Default `src/lib/server`. */
29
+ configPath?: string;
30
+ /** File name of that module. Default `smrt.ts`. */
31
+ configFileName?: string;
32
+ /** Source globs scanned for objects. Default `<objectsDir>/**\/*.ts`. */
33
+ include?: string[];
34
+ /** Source globs excluded from the scan. Default test and spec files. */
35
+ exclude?: string[];
36
+ /**
37
+ * Apply the Oxc legacy-decorator config. Default `true`. Values the app sets
38
+ * under `oxc.decorator` itself are never overwritten.
39
+ */
40
+ decorators?: boolean;
41
+ }
42
+ export declare const SMRT_PRESET_DEFAULTS: {
43
+ readonly objectsDir: "src/lib/objects";
44
+ readonly typesDir: "src/lib/types/smrt-generated";
45
+ readonly routesDir: "src/routes/api";
46
+ readonly configPath: "src/lib/server";
47
+ readonly configFileName: "smrt.ts";
48
+ readonly exclude: readonly ["**/*.test.ts", "**/*.spec.ts"];
49
+ };
50
+ /**
51
+ * Packages SMRT's SvelteKit entries share with the app by identity. SvelteKit
52
+ * recognizes `redirect()`/`fail()` results by class, so a server entry such as
53
+ * `@happyvertical/smrt-app-runtime/sveltekit` must import the app's own copy.
54
+ * A registry install already resolves that peer from the app; a linked or
55
+ * workspace package resolves it from its own directory instead, and its
56
+ * redirects then surface as 500s. Deduping makes both layouts identical.
57
+ */
58
+ export declare const SMRT_SHARED_RUNTIME_PACKAGES: readonly ["@sveltejs/kit"];
59
+ /**
60
+ * SMRT Vite preset: decorators, dependency manifests, and app object
61
+ * generation in one entry. Returns a promise because the package list is read
62
+ * from `smrt.config.ts`; Vite accepts promised plugins.
63
+ */
64
+ export declare function smrt(options?: SmrtPresetOptions): Promise<Plugin[]>;
65
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-preset/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,MAAM,CAAC;AAI/C,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,oBAAoB;;;;;;;CAOvB,CAAC;AAmEX;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,4BAA6B,CAAC;AAWvE;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAiD7E"}
@@ -0,0 +1,105 @@
1
+ import { smrtPlugin } from "../vite-plugin/index.js";
2
+ import { smrtConsumer } from "../consumer-plugin/index.js";
3
+ //#region src/vite-preset/index.ts
4
+ var SMRT_PRESET_DEFAULTS = {
5
+ objectsDir: "src/lib/objects",
6
+ typesDir: "src/lib/types/smrt-generated",
7
+ routesDir: "src/routes/api",
8
+ configPath: "src/lib/server",
9
+ configFileName: "smrt.ts",
10
+ exclude: ["**/*.test.ts", "**/*.spec.ts"]
11
+ };
12
+ function validatePackages(value, source) {
13
+ if (!Array.isArray(value) || value.some((p) => typeof p !== "string" || p.trim() === "")) throw new Error(`[smrt] ${source} must be an array of non-empty package names, got ${JSON.stringify(value)}.`);
14
+ return [...new Set(value.map((p) => p.trim()))];
15
+ }
16
+ /** Resolve the declared consumer package list: option first, then smrt.config. */
17
+ async function resolvePackages(options, searchFrom) {
18
+ if (options.packages !== void 0) return validatePackages(options.packages, "smrt({ packages })");
19
+ let declared;
20
+ try {
21
+ const { loadConfig } = await import("@happyvertical/smrt-config");
22
+ declared = (await loadConfig({
23
+ cache: false,
24
+ searchFrom
25
+ })).consumer?.packages;
26
+ } catch (error) {
27
+ throw new Error(`[smrt] Could not read smrt.config to resolve consumer packages: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ if (declared === void 0) throw new Error("[smrt] No consumer package list declared. Set `consumer: { packages: [...] }` in smrt.config.ts (use [] when the app consumes no SMRT packages) or pass `smrt({ packages })`. The list is explicit so runtime registration is deterministic; it is never inferred from package.json.");
30
+ return validatePackages(declared, "smrt.config `consumer.packages`");
31
+ }
32
+ function decoratorPlugin() {
33
+ return {
34
+ name: "smrt:decorators",
35
+ config(user) {
36
+ const decorator = user.oxc?.decorator;
37
+ const fill = {};
38
+ if (decorator?.legacy === void 0) fill.legacy = true;
39
+ if (decorator?.emitDecoratorMetadata === void 0) fill.emitDecoratorMetadata = true;
40
+ return Object.keys(fill).length ? { oxc: { decorator: fill } } : void 0;
41
+ }
42
+ };
43
+ }
44
+ /**
45
+ * Packages SMRT's SvelteKit entries share with the app by identity. SvelteKit
46
+ * recognizes `redirect()`/`fail()` results by class, so a server entry such as
47
+ * `@happyvertical/smrt-app-runtime/sveltekit` must import the app's own copy.
48
+ * A registry install already resolves that peer from the app; a linked or
49
+ * workspace package resolves it from its own directory instead, and its
50
+ * redirects then surface as 500s. Deduping makes both layouts identical.
51
+ */
52
+ var SMRT_SHARED_RUNTIME_PACKAGES = ["@sveltejs/kit"];
53
+ function sharedRuntimePlugin() {
54
+ return {
55
+ name: "smrt:shared-runtime",
56
+ config() {
57
+ return { resolve: { dedupe: [...SMRT_SHARED_RUNTIME_PACKAGES] } };
58
+ }
59
+ };
60
+ }
61
+ /**
62
+ * SMRT Vite preset: decorators, dependency manifests, and app object
63
+ * generation in one entry. Returns a promise because the package list is read
64
+ * from `smrt.config.ts`; Vite accepts promised plugins.
65
+ */
66
+ async function smrt(options = {}) {
67
+ const D = SMRT_PRESET_DEFAULTS;
68
+ const objectsDir = options.objectsDir ?? D.objectsDir;
69
+ const typesDir = options.typesDir ?? D.typesDir;
70
+ const routesDir = options.routesDir ?? D.routesDir;
71
+ const configPath = options.configPath ?? D.configPath;
72
+ const configFileName = options.configFileName ?? D.configFileName;
73
+ const rootOption = options.projectRoot ? { projectRoot: options.projectRoot } : {};
74
+ const packages = await resolvePackages(options, options.projectRoot ?? process.cwd());
75
+ const plugins = [];
76
+ if (options.decorators !== false) plugins.push(decoratorPlugin());
77
+ plugins.push(sharedRuntimePlugin());
78
+ if (packages.length > 0) plugins.push(smrtConsumer({
79
+ ...rootOption,
80
+ packages,
81
+ generateTypes: true,
82
+ typesDir,
83
+ svelteKit: true
84
+ }));
85
+ const pluginOptions = {
86
+ ...rootOption,
87
+ include: options.include ?? [`${objectsDir}/**/*.ts`],
88
+ exclude: options.exclude ?? [...D.exclude],
89
+ generateTypes: true,
90
+ typeDeclarationsPath: typesDir,
91
+ svelteKit: {
92
+ enabled: true,
93
+ routesDir,
94
+ objectsDir,
95
+ configPath,
96
+ configFileName
97
+ }
98
+ };
99
+ plugins.push(smrtPlugin(pluginOptions));
100
+ return plugins;
101
+ }
102
+ //#endregion
103
+ export { SMRT_PRESET_DEFAULTS, SMRT_SHARED_RUNTIME_PACKAGES, smrt };
104
+
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/vite-preset/index.ts"],"sourcesContent":["/**\n * Single-plugin Vite preset for SMRT applications.\n *\n * `plugins: [sveltekit(), smrt()]` replaces the hand-copied glue every app\n * carried: the Oxc legacy-decorator block, `smrtConsumer({...})` with an\n * explicit package list, and `smrtPlugin({...})` with the SvelteKit path\n * options. `smrtConsumer` and `smrtPlugin` stay public and unchanged.\n */\nimport type { Plugin, UserConfig } from 'vite';\nimport { smrtConsumer } from '../consumer-plugin/index.js';\nimport { type SmrtPluginOptions, smrtPlugin } from '../vite-plugin/index.js';\n\nexport interface SmrtPresetOptions {\n /**\n * SMRT packages whose object manifests this app consumes. Takes precedence\n * over `consumer.packages` in `smrt.config.ts`. An empty array declares that\n * the app consumes none. Tooling/UI packages must not be listed.\n */\n packages?: readonly string[];\n /**\n * Project root. Omit to follow the root Vite/SvelteKit resolve (the launch\n * cwd); `smrt.config.ts` is searched from this value or `process.cwd()`.\n */\n projectRoot?: string;\n /** Directory of the app's own `@smrt()` objects. Default `src/lib/objects`. */\n objectsDir?: string;\n /** Generated type declarations. Default `src/lib/types/smrt-generated`. */\n typesDir?: string;\n /** Generated API routes. Default `src/routes/api`. */\n routesDir?: string;\n /** Directory holding the generated runtime config module. Default `src/lib/server`. */\n configPath?: string;\n /** File name of that module. Default `smrt.ts`. */\n configFileName?: string;\n /** Source globs scanned for objects. Default `<objectsDir>/**\\/*.ts`. */\n include?: string[];\n /** Source globs excluded from the scan. Default test and spec files. */\n exclude?: string[];\n /**\n * Apply the Oxc legacy-decorator config. Default `true`. Values the app sets\n * under `oxc.decorator` itself are never overwritten.\n */\n decorators?: boolean;\n}\n\nexport const SMRT_PRESET_DEFAULTS = {\n objectsDir: 'src/lib/objects',\n typesDir: 'src/lib/types/smrt-generated',\n routesDir: 'src/routes/api',\n configPath: 'src/lib/server',\n configFileName: 'smrt.ts',\n exclude: ['**/*.test.ts', '**/*.spec.ts'],\n} as const;\n\nfunction validatePackages(value: unknown, source: string): string[] {\n if (\n !Array.isArray(value) ||\n value.some((p) => typeof p !== 'string' || p.trim() === '')\n ) {\n throw new Error(\n `[smrt] ${source} must be an array of non-empty package names, got ${JSON.stringify(value)}.`,\n );\n }\n return [...new Set(value.map((p: string) => p.trim()))];\n}\n\n/** Resolve the declared consumer package list: option first, then smrt.config. */\nasync function resolvePackages(\n options: SmrtPresetOptions,\n searchFrom: string,\n): Promise<string[]> {\n if (options.packages !== undefined) {\n return validatePackages(options.packages, 'smrt({ packages })');\n }\n\n let declared: unknown;\n try {\n const { loadConfig } = await import('@happyvertical/smrt-config');\n const config = await loadConfig({ cache: false, searchFrom });\n declared = config.consumer?.packages;\n } catch (error) {\n throw new Error(\n `[smrt] Could not read smrt.config to resolve consumer packages: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n if (declared === undefined) {\n throw new Error(\n '[smrt] No consumer package list declared. Set `consumer: { packages: [...] }` in smrt.config.ts ' +\n '(use [] when the app consumes no SMRT packages) or pass `smrt({ packages })`. ' +\n 'The list is explicit so runtime registration is deterministic; it is never inferred from package.json.',\n );\n }\n return validatePackages(declared, 'smrt.config `consumer.packages`');\n}\n\nfunction decoratorPlugin(): Plugin {\n return {\n name: 'smrt:decorators',\n // Vite merges a hook's result over the user config, so fill only the\n // values the app has not set itself.\n config(user: UserConfig) {\n const decorator = (\n user as { oxc?: { decorator?: Record<string, unknown> } }\n ).oxc?.decorator;\n const fill: Record<string, boolean> = {};\n if (decorator?.legacy === undefined) fill.legacy = true;\n if (decorator?.emitDecoratorMetadata === undefined) {\n fill.emitDecoratorMetadata = true;\n }\n return Object.keys(fill).length\n ? ({ oxc: { decorator: fill } } as UserConfig)\n : undefined;\n },\n };\n}\n\n/**\n * Packages SMRT's SvelteKit entries share with the app by identity. SvelteKit\n * recognizes `redirect()`/`fail()` results by class, so a server entry such as\n * `@happyvertical/smrt-app-runtime/sveltekit` must import the app's own copy.\n * A registry install already resolves that peer from the app; a linked or\n * workspace package resolves it from its own directory instead, and its\n * redirects then surface as 500s. Deduping makes both layouts identical.\n */\nexport const SMRT_SHARED_RUNTIME_PACKAGES = ['@sveltejs/kit'] as const;\n\nfunction sharedRuntimePlugin(): Plugin {\n return {\n name: 'smrt:shared-runtime',\n config() {\n return { resolve: { dedupe: [...SMRT_SHARED_RUNTIME_PACKAGES] } };\n },\n };\n}\n\n/**\n * SMRT Vite preset: decorators, dependency manifests, and app object\n * generation in one entry. Returns a promise because the package list is read\n * from `smrt.config.ts`; Vite accepts promised plugins.\n */\nexport async function smrt(options: SmrtPresetOptions = {}): Promise<Plugin[]> {\n const D = SMRT_PRESET_DEFAULTS;\n const objectsDir = options.objectsDir ?? D.objectsDir;\n const typesDir = options.typesDir ?? D.typesDir;\n const routesDir = options.routesDir ?? D.routesDir;\n const configPath = options.configPath ?? D.configPath;\n const configFileName = options.configFileName ?? D.configFileName;\n const rootOption = options.projectRoot\n ? { projectRoot: options.projectRoot }\n : {};\n\n const packages = await resolvePackages(\n options,\n options.projectRoot ?? process.cwd(),\n );\n\n const plugins: Plugin[] = [];\n if (options.decorators !== false) plugins.push(decoratorPlugin());\n plugins.push(sharedRuntimePlugin());\n // smrtConsumer must precede smrtPlugin, as in the template's hand-written form.\n // With no declared packages there is nothing to consume, and an empty list\n // would otherwise trigger dependency discovery.\n if (packages.length > 0) {\n plugins.push(\n smrtConsumer({\n ...rootOption,\n packages,\n generateTypes: true,\n typesDir,\n svelteKit: true,\n }),\n );\n }\n const pluginOptions: SmrtPluginOptions = {\n ...rootOption,\n include: options.include ?? [`${objectsDir}/**/*.ts`],\n exclude: options.exclude ?? [...D.exclude],\n generateTypes: true,\n typeDeclarationsPath: typesDir,\n svelteKit: {\n enabled: true,\n routesDir,\n objectsDir,\n configPath,\n configFileName,\n },\n };\n plugins.push(smrtPlugin(pluginOptions));\n return plugins;\n}\n"],"mappings":";;;AA6CA,IAAa,uBAAuB;CAClC,YAAY;CACZ,UAAU;CACV,WAAW;CACX,YAAY;CACZ,gBAAgB;CAChB,SAAS,CAAC,gBAAgB,cAAc;AAC1C;AAEA,SAAS,iBAAiB,OAAgB,QAA0B;CAClE,IACE,CAAC,MAAM,QAAQ,KAAK,KACpB,MAAM,MAAM,MAAM,OAAO,MAAM,YAAY,EAAE,KAAK,MAAM,EAAE,GAE1D,MAAM,IAAI,MACR,UAAU,OAAO,oDAAoD,KAAK,UAAU,KAAK,EAAE,EAC7F;CAEF,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,KAAK,MAAc,EAAE,KAAK,CAAC,CAAC,CAAC;AACxD;;AAGA,eAAe,gBACb,SACA,YACmB;CACnB,IAAI,QAAQ,aAAa,KAAA,GACvB,OAAO,iBAAiB,QAAQ,UAAU,oBAAoB;CAGhE,IAAI;CACJ,IAAI;EACF,MAAM,EAAE,eAAe,MAAM,OAAO;EAEpC,YAAW,MADU,WAAW;GAAE,OAAO;GAAO;EAAW,CAAC,EAAA,CAC1C,UAAU;CAC9B,SAAS,OAAO;EACd,MAAM,IAAI,MACR,mEACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEzD;CACF;CAEA,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MACR,sRAGF;CAEF,OAAO,iBAAiB,UAAU,iCAAiC;AACrE;AAEA,SAAS,kBAA0B;CACjC,OAAO;EACL,MAAM;EAGN,OAAO,MAAkB;GACvB,MAAM,YACJ,KACA,KAAK;GACP,MAAM,OAAgC,CAAC;GACvC,IAAI,WAAW,WAAW,KAAA,GAAW,KAAK,SAAS;GACnD,IAAI,WAAW,0BAA0B,KAAA,GACvC,KAAK,wBAAwB;GAE/B,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,SACpB,EAAE,KAAK,EAAE,WAAW,KAAK,EAAE,IAC5B,KAAA;EACN;CACF;AACF;;;;;;;;;AAUA,IAAa,+BAA+B,CAAC,eAAe;AAE5D,SAAS,sBAA8B;CACrC,OAAO;EACL,MAAM;EACN,SAAS;GACP,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,4BAA4B,EAAE,EAAE;EAClE;CACF;AACF;;;;;;AAOA,eAAsB,KAAK,UAA6B,CAAC,GAAsB;CAC7E,MAAM,IAAI;CACV,MAAM,aAAa,QAAQ,cAAc,EAAE;CAC3C,MAAM,WAAW,QAAQ,YAAY,EAAE;CACvC,MAAM,YAAY,QAAQ,aAAa,EAAE;CACzC,MAAM,aAAa,QAAQ,cAAc,EAAE;CAC3C,MAAM,iBAAiB,QAAQ,kBAAkB,EAAE;CACnD,MAAM,aAAa,QAAQ,cACvB,EAAE,aAAa,QAAQ,YAAY,IACnC,CAAC;CAEL,MAAM,WAAW,MAAM,gBACrB,SACA,QAAQ,eAAe,QAAQ,IAAI,CACrC;CAEA,MAAM,UAAoB,CAAC;CAC3B,IAAI,QAAQ,eAAe,OAAO,QAAQ,KAAK,gBAAgB,CAAC;CAChE,QAAQ,KAAK,oBAAoB,CAAC;CAIlC,IAAI,SAAS,SAAS,GACpB,QAAQ,KACN,aAAa;EACX,GAAG;EACH;EACA,eAAe;EACf;EACA,WAAW;CACb,CAAC,CACH;CAEF,MAAM,gBAAmC;EACvC,GAAG;EACH,SAAS,QAAQ,WAAW,CAAC,GAAG,WAAW,SAAS;EACpD,SAAS,QAAQ,WAAW,CAAC,GAAG,EAAE,OAAO;EACzC,eAAe;EACf,sBAAsB;EACtB,WAAW;GACT,SAAS;GACT;GACA;GACA;GACA;EACF;CACF;CACA,QAAQ,KAAK,WAAW,aAAa,CAAC;CACtC,OAAO;AACT"}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `@happyvertical/smrt-core/vite` — the one-call Vite preset.
3
+ */
4
+ export * from './vite-preset/index.js';
5
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,wBAAwB,CAAC"}
package/dist/vite.js ADDED
@@ -0,0 +1,2 @@
1
+ import { SMRT_PRESET_DEFAULTS, SMRT_SHARED_RUNTIME_PACKAGES, smrt } from "./vite-preset/index.js";
2
+ export { SMRT_PRESET_DEFAULTS, SMRT_SHARED_RUNTIME_PACKAGES, smrt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-core",
3
- "version": "0.53.3",
3
+ "version": "0.53.5",
4
4
  "description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
5
5
  "author": "HappyVertical",
6
6
  "type": "module",
@@ -88,6 +88,11 @@
88
88
  "default": "./dist/knowledge.js"
89
89
  },
90
90
  "./smrt-knowledge.json": "./dist/smrt-knowledge.json",
91
+ "./vite": {
92
+ "types": "./dist/vite.d.ts",
93
+ "import": "./dist/vite.js",
94
+ "default": "./dist/vite.js"
95
+ },
91
96
  "./vite-plugin": {
92
97
  "types": "./dist/vite-plugin.d.ts",
93
98
  "import": "./dist/vite-plugin.js",
@@ -150,15 +155,15 @@
150
155
  }
151
156
  },
152
157
  "dependencies": {
153
- "@happyvertical/ai": "^0.98.0",
154
- "@happyvertical/files": "^0.98.0",
155
- "@happyvertical/json": "^0.98.0",
156
- "@happyvertical/logger": "^0.98.0",
157
- "@happyvertical/smrt-config": "0.53.3",
158
- "@happyvertical/smrt-scanner": "0.53.3",
159
- "@happyvertical/smrt-types": "0.53.3",
160
- "@happyvertical/sql": "^0.98.0",
161
- "@happyvertical/utils": "^0.98.0",
158
+ "@happyvertical/ai": "^0.100.3",
159
+ "@happyvertical/files": "^0.100.3",
160
+ "@happyvertical/json": "^0.100.3",
161
+ "@happyvertical/logger": "^0.100.3",
162
+ "@happyvertical/smrt-config": "0.53.5",
163
+ "@happyvertical/smrt-scanner": "0.53.5",
164
+ "@happyvertical/smrt-types": "0.53.5",
165
+ "@happyvertical/sql": "^0.100.3",
166
+ "@happyvertical/utils": "^0.100.3",
162
167
  "@oxc-project/runtime": "^0.150.0",
163
168
  "cheerio": "^1.2.0",
164
169
  "fast-glob": "3.3.3",