@happyvertical/smrt-core 0.37.8 → 0.37.10
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.
- package/AGENTS.md +10 -0
- package/dist/change-feed.d.ts +261 -0
- package/dist/change-feed.d.ts.map +1 -0
- package/dist/change-feed.js +482 -0
- package/dist/change-feed.js.map +1 -0
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +4 -2
- package/dist/class.js.map +1 -1
- package/dist/collection.js +2 -2
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +24 -19
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/dispatch/bus.js +2 -2
- package/dist/dispatch/index.js +1 -1
- package/dist/generated-client-runtime.d.ts +26 -0
- package/dist/generated-client-runtime.d.ts.map +1 -0
- package/dist/generated-client-runtime.js +88 -0
- package/dist/generated-client-runtime.js.map +1 -0
- package/dist/generators/changes-route.d.ts +52 -0
- package/dist/generators/changes-route.d.ts.map +1 -0
- package/dist/generators/changes-route.js +111 -0
- package/dist/generators/changes-route.js.map +1 -0
- package/dist/generators/rest.d.ts +28 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +53 -15
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/manifest/static-manifest.js +2 -2
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +502 -2
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/object.js +2 -2
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +66 -7
- package/dist/prebuild/index.js.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/system/schema.d.ts +24 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +45 -3
- package/dist/system/schema.js.map +1 -1
- package/dist/testing/database.js +1 -1
- package/dist/vite-plugin/changes-route.d.ts +10 -0
- package/dist/vite-plugin/changes-route.d.ts.map +1 -0
- package/dist/vite-plugin/changes-route.js +174 -0
- package/dist/vite-plugin/changes-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +7 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +148 -21
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/route-header.d.ts +10 -0
- package/dist/vite-plugin/route-header.d.ts.map +1 -0
- package/dist/vite-plugin/route-header.js +14 -0
- package/dist/vite-plugin/route-header.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +7 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +5 -3
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +2 -2
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +73 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -0
- package/dist/vite-plugin/web-collections.js +174 -0
- package/dist/vite-plugin/web-collections.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/vite-plugin/changes-route.ts
|
|
5
|
+
/**
|
|
6
|
+
* SvelteKit `_changes` route generation for the change feed (issue #1758).
|
|
7
|
+
*
|
|
8
|
+
* Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped
|
|
9
|
+
* GET endpoint over the `_smrt_changes` log, part of the client/mobile sync
|
|
10
|
+
* contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`
|
|
11
|
+
* only carries a one-line registration.
|
|
12
|
+
*
|
|
13
|
+
* Design notes:
|
|
14
|
+
* - **Fail-closed auth** (#1540 posture): the handler requires an
|
|
15
|
+
* authenticated principal on `locals`. The feed spans every table, so
|
|
16
|
+
* per-model `api: { public }` opt-outs deliberately do not apply.
|
|
17
|
+
* - **Tenant scoping**: when the project has tenant-scoped objects, the
|
|
18
|
+
* route establishes tenant context from `locals` exactly like generated
|
|
19
|
+
* collection routes, then reads through
|
|
20
|
+
* `getTenantScopedChangesSince()` — a tenant only ever sees its own
|
|
21
|
+
* changes plus global rows.
|
|
22
|
+
* - **Database resolution**: the route anchors on the project's first
|
|
23
|
+
* generated collection (alphabetical) via the consumer's existing
|
|
24
|
+
* `getCollection()` helper, inheriting its configuration, request-scoped
|
|
25
|
+
* database support and system-table bootstrap. Multi-database projects
|
|
26
|
+
* (per-object `db` overrides) see the anchor collection's feed.
|
|
27
|
+
* - Cleanup rides the existing generated-route sweep: the emitted file
|
|
28
|
+
* starts with {@link AUTO_GENERATED_ROUTE_HEADER}.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private
|
|
32
|
+
* there): collection classes share route paths with their item class and
|
|
33
|
+
* never anchor routes themselves.
|
|
34
|
+
*/
|
|
35
|
+
function isCollectionDefinition(objectDef) {
|
|
36
|
+
return objectDef.extends === "SmrtCollection" || !!objectDef.extendsTypeArg;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pick the class the route resolves its database through: the first
|
|
40
|
+
* non-collection object by sorted manifest key, for determinism across
|
|
41
|
+
* builds.
|
|
42
|
+
*
|
|
43
|
+
* Returns the manifest **registry key** verbatim (which may be
|
|
44
|
+
* package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted
|
|
45
|
+
* route passes it straight to `getCollection()`, exactly as the generated CRUD
|
|
46
|
+
* routes do. Collapsing it to a simple class name would resolve ambiguously
|
|
47
|
+
* when two loaded packages declare the same simple name (mirrors the #1778
|
|
48
|
+
* verbatim-key fix and the sync-apply route's `registryKey`).
|
|
49
|
+
*/
|
|
50
|
+
function resolveAnchorClassName(manifest) {
|
|
51
|
+
const sortedNames = Object.keys(manifest.objects).sort();
|
|
52
|
+
for (const name of sortedNames) {
|
|
53
|
+
const def = manifest.objects[name];
|
|
54
|
+
if (def && !isCollectionDefinition(def)) return name;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function manifestHasTenantScopedObject(manifest) {
|
|
59
|
+
return Object.values(manifest.objects).some((def) => !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate the `_changes/+server.ts` route. Returns true when a route was
|
|
63
|
+
* written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;
|
|
64
|
+
* skipped (with a log line) when the manifest has no objects to anchor the
|
|
65
|
+
* database on.
|
|
66
|
+
*/
|
|
67
|
+
function generateChangesRoute(projectRoot, manifest, options) {
|
|
68
|
+
if (options.changesRoute?.enabled === false) return false;
|
|
69
|
+
const anchorClassName = resolveAnchorClassName(manifest);
|
|
70
|
+
if (!anchorClassName) {
|
|
71
|
+
console.log("[smrt] Skipping _changes route - no SMRT objects to anchor the database on");
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const routeDir = join(projectRoot, options.routesDir, "_changes");
|
|
75
|
+
const content = generateChangesRouteTemplate(anchorClassName, manifestHasTenantScopedObject(manifest));
|
|
76
|
+
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
77
|
+
const filePath = join(routeDir, "+server.ts");
|
|
78
|
+
writeFileSync(filePath, content, "utf-8");
|
|
79
|
+
console.log(`[smrt] Generated: ${filePath}`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
function generateChangesRouteTemplate(anchorClassName, tenantScoped) {
|
|
83
|
+
return `${AUTO_GENERATED_ROUTE_HEADER}
|
|
84
|
+
// DO NOT EDIT - changes will be overwritten
|
|
85
|
+
//
|
|
86
|
+
// GET /_changes — cursor read over the _smrt_changes change feed (#1758).
|
|
87
|
+
// Part of the client/mobile sync contract: poll with the returned cursor to
|
|
88
|
+
// observe every committed framework save/delete (deletes are tombstones)
|
|
89
|
+
// exactly once. Query params: since (cursor, default 0), tables
|
|
90
|
+
// (comma-separated), limit. A response with resyncRequired: true (still
|
|
91
|
+
// HTTP 200 — protocol state, not an error) means the cursor cannot be
|
|
92
|
+
// served incrementally (pruned or foreign) and the client must re-fetch
|
|
93
|
+
// in full before resuming polling.
|
|
94
|
+
|
|
95
|
+
import { error, json } from '@sveltejs/kit';
|
|
96
|
+
import { getTenantScopedChangesSince } from '@happyvertical/smrt-core';
|
|
97
|
+
import { getCollection } from '$lib/server/smrt';
|
|
98
|
+
import type { RequestHandler } from './$types';
|
|
99
|
+
|
|
100
|
+
// Fail-closed authorization (#1540): the change feed spans every table, so
|
|
101
|
+
// it is never public — an authenticated principal on \`locals\` is required.
|
|
102
|
+
function hasAuthenticatedPrincipal(locals: unknown): boolean {
|
|
103
|
+
if (!locals || typeof locals !== 'object') return false;
|
|
104
|
+
const l = locals as Record<string, unknown>;
|
|
105
|
+
const isResolvedPrincipal = (v: unknown) =>
|
|
106
|
+
typeof v === 'object' && v !== null;
|
|
107
|
+
return (
|
|
108
|
+
isResolvedPrincipal(l.user) ||
|
|
109
|
+
isResolvedPrincipal(l.session) ||
|
|
110
|
+
l.smrtAuth === true
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function requireRouteAuth(locals: unknown): void {
|
|
115
|
+
if (!hasAuthenticatedPrincipal(locals)) {
|
|
116
|
+
throw error(401, 'Authentication required');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
${tenantScoped ? `
|
|
120
|
+
import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenancy';
|
|
121
|
+
|
|
122
|
+
function establishTenantContext(locals: unknown): void {
|
|
123
|
+
if (hasTenantContext()) return;
|
|
124
|
+
if (!locals || typeof locals !== 'object') return;
|
|
125
|
+
const l = locals as Record<string, unknown>;
|
|
126
|
+
const user = l.user as Record<string, unknown> | undefined;
|
|
127
|
+
const session = l.session as Record<string, unknown> | undefined;
|
|
128
|
+
const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;
|
|
129
|
+
if (typeof tenantId === 'string' && tenantId) {
|
|
130
|
+
enterTenantContext({ tenantId });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
` : ""}
|
|
134
|
+
export const GET: RequestHandler = async ({ locals, url }) => {
|
|
135
|
+
requireRouteAuth(locals);${tenantScoped ? "\n establishTenantContext(locals);" : ""}
|
|
136
|
+
|
|
137
|
+
const since = Number(url.searchParams.get('since') ?? '0');
|
|
138
|
+
if (!Number.isFinite(since) || since < 0) {
|
|
139
|
+
throw error(400, "'since' must be a non-negative number");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let limit: number | undefined;
|
|
143
|
+
const limitParam = url.searchParams.get('limit');
|
|
144
|
+
if (limitParam !== null) {
|
|
145
|
+
limit = Number(limitParam);
|
|
146
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
147
|
+
throw error(400, "'limit' must be a positive number");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const tablesParam = url.searchParams.get('tables');
|
|
152
|
+
const tables = tablesParam
|
|
153
|
+
? tablesParam
|
|
154
|
+
.split(',')
|
|
155
|
+
.map((table) => table.trim())
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
: undefined;
|
|
158
|
+
|
|
159
|
+
// The feed lives in the project's database; anchor on the
|
|
160
|
+
// ${anchorClassName} collection to reuse its configured connection.
|
|
161
|
+
const collection = await getCollection('${anchorClassName}');
|
|
162
|
+
const page = await getTenantScopedChangesSince(collection.db, {
|
|
163
|
+
since,
|
|
164
|
+
tables,
|
|
165
|
+
limit,
|
|
166
|
+
});
|
|
167
|
+
return json(page);
|
|
168
|
+
};
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { generateChangesRoute };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=changes-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes-route.js","names":[],"sources":["../../src/vite-plugin/changes-route.ts"],"sourcesContent":["/**\n * SvelteKit `_changes` route generation for the change feed (issue #1758).\n *\n * Emits `{routesDir}/_changes/+server.ts` — an auth-guarded, tenant-scoped\n * GET endpoint over the `_smrt_changes` log, part of the client/mobile sync\n * contract (PRD #1755). Kept in its own module so `sveltekit-generator.ts`\n * only carries a one-line registration.\n *\n * Design notes:\n * - **Fail-closed auth** (#1540 posture): the handler requires an\n * authenticated principal on `locals`. The feed spans every table, so\n * per-model `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scoping**: when the project has tenant-scoped objects, the\n * route establishes tenant context from `locals` exactly like generated\n * collection routes, then reads through\n * `getTenantScopedChangesSince()` — a tenant only ever sees its own\n * changes plus global rows.\n * - **Database resolution**: the route anchors on the project's first\n * generated collection (alphabetical) via the consumer's existing\n * `getCollection()` helper, inheriting its configuration, request-scoped\n * database support and system-table bootstrap. Multi-database projects\n * (per-object `db` overrides) see the anchor collection's feed.\n * - Cleanup rides the existing generated-route sweep: the emitted file\n * starts with {@link AUTO_GENERATED_ROUTE_HEADER}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/**\n * Mirrors `isCollectionClass` in `sveltekit-generator.ts` (module-private\n * there): collection classes share route paths with their item class and\n * never anchor routes themselves.\n */\nfunction isCollectionDefinition(objectDef: SmartObjectDefinition): boolean {\n return objectDef.extends === 'SmrtCollection' || !!objectDef.extendsTypeArg;\n}\n\n/**\n * Pick the class the route resolves its database through: the first\n * non-collection object by sorted manifest key, for determinism across\n * builds.\n *\n * Returns the manifest **registry key** verbatim (which may be\n * package-qualified, e.g. `@happyvertical/smrt-ledgers:Account`) — the emitted\n * route passes it straight to `getCollection()`, exactly as the generated CRUD\n * routes do. Collapsing it to a simple class name would resolve ambiguously\n * when two loaded packages declare the same simple name (mirrors the #1778\n * verbatim-key fix and the sync-apply route's `registryKey`).\n */\nfunction resolveAnchorClassName(manifest: SmartObjectManifest): string | null {\n const sortedNames = Object.keys(manifest.objects).sort();\n for (const name of sortedNames) {\n const def = manifest.objects[name];\n if (def && !isCollectionDefinition(def)) {\n return name;\n }\n }\n return null;\n}\n\nfunction manifestHasTenantScopedObject(manifest: SmartObjectManifest): boolean {\n return Object.values(manifest.objects).some(\n (def) =>\n !isCollectionDefinition(def) && !!def.decoratorConfig?.tenantScoped,\n );\n}\n\n/**\n * Generate the `_changes/+server.ts` route. Returns true when a route was\n * written. Disabled with `sveltekit: { changesRoute: { enabled: false } }`;\n * skipped (with a log line) when the manifest has no objects to anchor the\n * database on.\n */\nexport function generateChangesRoute(\n projectRoot: string,\n manifest: SmartObjectManifest,\n options: SvelteKitOptions,\n): boolean {\n if (options.changesRoute?.enabled === false) {\n return false;\n }\n\n const anchorClassName = resolveAnchorClassName(manifest);\n if (!anchorClassName) {\n console.log(\n '[smrt] Skipping _changes route - no SMRT objects to anchor the database on',\n );\n return false;\n }\n\n const routeDir = join(projectRoot, options.routesDir, '_changes');\n const content = generateChangesRouteTemplate(\n anchorClassName,\n manifestHasTenantScopedObject(manifest),\n );\n\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(filePath, content, 'utf-8');\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateChangesRouteTemplate(\n anchorClassName: string,\n tenantScoped: boolean,\n): string {\n const tenantHelper = tenantScoped\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 const tenantCall = tenantScoped ? '\\n establishTenantContext(locals);' : '';\n\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// GET /_changes — cursor read over the _smrt_changes change feed (#1758).\n// Part of the client/mobile sync contract: poll with the returned cursor to\n// observe every committed framework save/delete (deletes are tombstones)\n// exactly once. Query params: since (cursor, default 0), tables\n// (comma-separated), limit. A response with resyncRequired: true (still\n// HTTP 200 — protocol state, not an error) means the cursor cannot be\n// served incrementally (pruned or foreign) and the client must re-fetch\n// in full before resuming polling.\n\nimport { error, json } from '@sveltejs/kit';\nimport { getTenantScopedChangesSince } from '@happyvertical/smrt-core';\nimport { getCollection } from '$lib/server/smrt';\nimport type { RequestHandler } from './$types';\n\n// Fail-closed authorization (#1540): the change feed spans every table, so\n// it is never public — an authenticated principal on \\`locals\\` is required.\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 requireRouteAuth(locals: unknown): void {\n if (!hasAuthenticatedPrincipal(locals)) {\n throw error(401, 'Authentication required');\n }\n}\n${tenantHelper}\nexport const GET: RequestHandler = async ({ locals, url }) => {\n requireRouteAuth(locals);${tenantCall}\n\n const since = Number(url.searchParams.get('since') ?? '0');\n if (!Number.isFinite(since) || since < 0) {\n throw error(400, \"'since' must be a non-negative number\");\n }\n\n let limit: number | undefined;\n const limitParam = url.searchParams.get('limit');\n if (limitParam !== null) {\n limit = Number(limitParam);\n if (!Number.isFinite(limit) || limit < 1) {\n throw error(400, \"'limit' must be a positive number\");\n }\n }\n\n const tablesParam = url.searchParams.get('tables');\n const tables = tablesParam\n ? tablesParam\n .split(',')\n .map((table) => table.trim())\n .filter(Boolean)\n : undefined;\n\n // The feed lives in the project's database; anchor on the\n // ${anchorClassName} collection to reuse its configured connection.\n const collection = await getCollection('${anchorClassName}');\n const page = await getTenantScopedChangesSince(collection.db, {\n since,\n tables,\n limit,\n });\n return json(page);\n};\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,uBAAuB,WAA2C;CACzE,OAAO,UAAU,YAAY,oBAAoB,CAAC,CAAC,UAAU;AAC/D;;;;;;;;;;;;;AAcA,SAAS,uBAAuB,UAA8C;CAC5E,MAAM,cAAc,OAAO,KAAK,SAAS,OAAO,CAAC,CAAC,KAAK;CACvD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,MAAM,SAAS,QAAQ;EAC7B,IAAI,OAAO,CAAC,uBAAuB,GAAG,GACpC,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAS,8BAA8B,UAAwC;CAC7E,OAAO,OAAO,OAAO,SAAS,OAAO,CAAC,CAAC,MACpC,QACC,CAAC,uBAAuB,GAAG,KAAK,CAAC,CAAC,IAAI,iBAAiB,YAC3D;AACF;;;;;;;AAQA,SAAgB,qBACd,aACA,UACA,SACS;CACT,IAAI,QAAQ,cAAc,YAAY,OACpC,OAAO;CAGT,MAAM,kBAAkB,uBAAuB,QAAQ;CACvD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IACN,4EACF;EACA,OAAO;CACT;CAEA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,UAAU;CAChE,MAAM,UAAU,6BACd,iBACA,8BAA8B,QAAQ,CACxC;CAEA,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cAAc,UAAU,SAAS,OAAO;CACxC,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,6BACP,iBACA,cACQ;CAoBR,OAAO,GAAG,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAnBjB,eACjB;;;;;;;;;;;;;;IAeA,GAuCS;;6BAtCM,eAAe,wCAAwC,GAwCpC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBjC,gBAAgB;4CACqB,gBAAgB;;;;;;;;;AAS5D"}
|
|
@@ -46,6 +46,13 @@ export interface SmrtPluginOptions {
|
|
|
46
46
|
* the next major. An explicit `api.routes[name].path` always wins.
|
|
47
47
|
*/
|
|
48
48
|
kebabRoutes?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Change-feed `_changes` route generation (#1758). Enabled by default
|
|
51
|
+
* (the route is auth-guarded fail-closed); `{ enabled: false }` skips it.
|
|
52
|
+
*/
|
|
53
|
+
changesRoute?: {
|
|
54
|
+
enabled?: boolean;
|
|
55
|
+
};
|
|
49
56
|
};
|
|
50
57
|
/** Domain-scoped agent/developer knowledge artifact generation. */
|
|
51
58
|
knowledge?: DomainKnowledgeConfig | false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAIlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAkB5D,YAAY,EACV,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAalC,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE;QACV,yDAAyD;QACzD,OAAO,EAAE,OAAO,CAAC;QACjB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mDAAmD;QACnD,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;WAGG;QACH,YAAY,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;KACtC,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AA+BD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAaR;AAED,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAm5BlE"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { buildDomainKnowledgeManifest } from "../knowledge.js";
|
|
2
2
|
import { importWorkspaceModule } from "../utils/import-workspace-module.js";
|
|
3
3
|
import { discoverSmrtPackages } from "../manifest/discover-smrt-packages.js";
|
|
4
|
+
import { CLIENT_FETCH_RUNTIME } from "../generated-client-runtime.js";
|
|
4
5
|
import { importBuildAwareModule } from "./import-build-aware.js";
|
|
5
6
|
import { findCliApiCoherenceViolations, generateSvelteKitRoutes, methodNameToKebab, resolveApiActionSet, validateCliIncludeAgainstApi } from "./sveltekit-generator.js";
|
|
7
|
+
import { buildWebFieldDefinitions, buildWebRelationships, selectWebCollectionEntries } from "./web-collections.js";
|
|
6
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
9
|
import { dirname, join } from "node:path";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,7 +21,8 @@ var VIRTUAL_MODULES = {
|
|
|
19
21
|
"@happyvertical/smrt-virt-manifest": "smrt:manifest",
|
|
20
22
|
"@happyvertical/smrt-virt-schema": "smrt:schema",
|
|
21
23
|
"@happyvertical/smrt-virt-ui": "smrt:ui",
|
|
22
|
-
"@happyvertical/smrt-virt-cli": "smrt:cli"
|
|
24
|
+
"@happyvertical/smrt-virt-cli": "smrt:cli",
|
|
25
|
+
"@happyvertical/smrt-virt-web": "smrt:web"
|
|
23
26
|
};
|
|
24
27
|
async function importScanner() {
|
|
25
28
|
return importWorkspaceModule({
|
|
@@ -256,6 +259,7 @@ function smrtPlugin(options = {}) {
|
|
|
256
259
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
257
260
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
258
261
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
262
|
+
changesRoute: svelteKit.changesRoute,
|
|
259
263
|
knowledge: await resolveKnowledgeConfig(resolvedConfig.root, manifest)
|
|
260
264
|
});
|
|
261
265
|
},
|
|
@@ -319,6 +323,7 @@ function smrtPlugin(options = {}) {
|
|
|
319
323
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
320
324
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
321
325
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
326
|
+
changesRoute: svelteKit.changesRoute,
|
|
322
327
|
knowledge: await resolveKnowledgeConfig(server.config.root, manifest)
|
|
323
328
|
});
|
|
324
329
|
Object.values(VIRTUAL_MODULES).forEach((id) => {
|
|
@@ -346,6 +351,7 @@ function smrtPlugin(options = {}) {
|
|
|
346
351
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
347
352
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
348
353
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
354
|
+
changesRoute: svelteKit.changesRoute,
|
|
349
355
|
knowledge: await resolveKnowledgeConfig(server.config.root, manifest)
|
|
350
356
|
});
|
|
351
357
|
}
|
|
@@ -369,6 +375,7 @@ function smrtPlugin(options = {}) {
|
|
|
369
375
|
configPath: svelteKit.configPath || "src/lib/server",
|
|
370
376
|
configFileName: svelteKit.configFileName || "smrt.ts",
|
|
371
377
|
kebabRoutes: svelteKit.kebabRoutes ?? false,
|
|
378
|
+
changesRoute: svelteKit.changesRoute,
|
|
372
379
|
knowledge: await resolveKnowledgeConfig(server.config.root, manifest)
|
|
373
380
|
});
|
|
374
381
|
}
|
|
@@ -396,6 +403,7 @@ function smrtPlugin(options = {}) {
|
|
|
396
403
|
case "smrt:ui": return await loadDefaultUI();
|
|
397
404
|
case "smrt:index-html": return await loadDefaultHTML();
|
|
398
405
|
case "smrt:cli": return await generateCLIModule(manifest);
|
|
406
|
+
case "smrt:web": return generateWebModule(manifest);
|
|
399
407
|
default: return null;
|
|
400
408
|
}
|
|
401
409
|
},
|
|
@@ -600,6 +608,8 @@ function generateClientModule(manifest, options = {}) {
|
|
|
600
608
|
// Auto-generated API client from SMRT objects
|
|
601
609
|
// This file is generated automatically - do not edit
|
|
602
610
|
|
|
611
|
+
${CLIENT_FETCH_RUNTIME}
|
|
612
|
+
|
|
603
613
|
export function createClient(basePath = '/api/v1') {
|
|
604
614
|
return {${uniqueApiClientEntries(Object.entries(manifest.objects)).map(({ obj, clientKey }) => {
|
|
605
615
|
const { collection, methods = {} } = obj;
|
|
@@ -616,45 +626,45 @@ export function createClient(basePath = '/api/v1') {
|
|
|
616
626
|
return options.kebabRoutes ? methodNameToKebab(methodName) : methodName;
|
|
617
627
|
};
|
|
618
628
|
const customMethodImpls = customMethods.map(([methodName, _method]) => {
|
|
619
|
-
return ` ${methodName}: (id, options) =>
|
|
629
|
+
return ` ${methodName}: (id, options) => __smrtFetchJson(basePath + '/${collection}/' + id + '/${segmentFor(methodName)}', {
|
|
620
630
|
method: 'POST',
|
|
621
631
|
headers: { 'Content-Type': 'application/json' },
|
|
622
632
|
body: JSON.stringify(options || {})
|
|
623
|
-
})
|
|
633
|
+
})`;
|
|
624
634
|
}).join(",\n");
|
|
625
635
|
return `
|
|
626
636
|
${clientKey}: {
|
|
627
|
-
list: (params) =>
|
|
637
|
+
list: (params) => __smrtFetchJson(basePath + '/${collection}', {
|
|
628
638
|
method: 'GET',
|
|
629
639
|
headers: { 'Content-Type': 'application/json' }
|
|
630
|
-
})
|
|
640
|
+
}),
|
|
631
641
|
|
|
632
|
-
get: (id) =>
|
|
642
|
+
get: (id) => __smrtFetchJson(basePath + '/${collection}/' + id, {
|
|
633
643
|
method: 'GET',
|
|
634
644
|
headers: { 'Content-Type': 'application/json' }
|
|
635
|
-
})
|
|
645
|
+
}),
|
|
636
646
|
|
|
637
|
-
create: (data) =>
|
|
647
|
+
create: (data) => __smrtFetchJson(basePath + '/${collection}', {
|
|
638
648
|
method: 'POST',
|
|
639
649
|
headers: { 'Content-Type': 'application/json' },
|
|
640
650
|
body: JSON.stringify(data)
|
|
641
|
-
})
|
|
651
|
+
}),
|
|
642
652
|
|
|
643
|
-
update: (id, data) =>
|
|
653
|
+
update: (id, data) => __smrtFetchJson(basePath + '/${collection}/' + id, {
|
|
644
654
|
method: 'PUT',
|
|
645
655
|
headers: { 'Content-Type': 'application/json' },
|
|
646
656
|
body: JSON.stringify(data)
|
|
647
|
-
})
|
|
657
|
+
}),
|
|
648
658
|
|
|
649
|
-
delete: (id) =>
|
|
659
|
+
delete: (id) => __smrtFetchOk(basePath + '/${collection}/' + id, {
|
|
650
660
|
method: 'DELETE',
|
|
651
661
|
headers: { 'Content-Type': 'application/json' }
|
|
652
|
-
})
|
|
662
|
+
}),
|
|
653
663
|
|
|
654
|
-
search: (query) =>
|
|
664
|
+
search: (query) => __smrtFetchJson(basePath + '/${collection}/search?q=' + encodeURIComponent(query), {
|
|
655
665
|
method: 'GET',
|
|
656
666
|
headers: { 'Content-Type': 'application/json' }
|
|
657
|
-
})
|
|
667
|
+
})${customMethods.length > 0 ? `,\n${customMethodImpls}` : ""}
|
|
658
668
|
}`;
|
|
659
669
|
}).join(",")}
|
|
660
670
|
};
|
|
@@ -664,6 +674,50 @@ export { createClient as default };
|
|
|
664
674
|
`;
|
|
665
675
|
}
|
|
666
676
|
/**
|
|
677
|
+
* Generate the virtual web-collection definition module
|
|
678
|
+
* (`@happyvertical/smrt-virt-web`, #1761).
|
|
679
|
+
*
|
|
680
|
+
* Emits typed per-collection metadata — REST collection name, endpoint path,
|
|
681
|
+
* id field, exposed CRUD actions, and persisted field definitions — for every
|
|
682
|
+
* API-exposed model in the manifest. This is the codegen contract consumed by
|
|
683
|
+
* `@happyvertical/smrt-web` to construct client collections over the generated
|
|
684
|
+
* REST surface. Deliberately data-only: no fetch code is emitted here, so the
|
|
685
|
+
* runtime wrapper owns all HTTP/error semantics in one place. Selection and
|
|
686
|
+
* field rules live in {@link selectWebCollectionEntries} /
|
|
687
|
+
* {@link buildWebFieldDefinitions} so this value emission and the matching
|
|
688
|
+
* d.ts type emission cannot drift.
|
|
689
|
+
*/
|
|
690
|
+
function generateWebModule(manifest) {
|
|
691
|
+
const definitions = {};
|
|
692
|
+
for (const { collection, obj, actions } of selectWebCollectionEntries(manifest)) definitions[collection] = {
|
|
693
|
+
name: collection,
|
|
694
|
+
className: obj.className,
|
|
695
|
+
endpoint: `/${collection}`,
|
|
696
|
+
idField: "id",
|
|
697
|
+
actions,
|
|
698
|
+
fields: buildWebFieldDefinitions(obj),
|
|
699
|
+
relationships: buildWebRelationships(obj, manifest)
|
|
700
|
+
};
|
|
701
|
+
return `
|
|
702
|
+
// Auto-generated web collection definitions from SMRT objects (#1761)
|
|
703
|
+
// This file is generated automatically - do not edit
|
|
704
|
+
|
|
705
|
+
export const collectionDefinitions = ${JSON.stringify(definitions, null, 2)};
|
|
706
|
+
|
|
707
|
+
export function getCollectionDefinition(name) {
|
|
708
|
+
const definition = collectionDefinitions[name];
|
|
709
|
+
if (!definition) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
\`[smrt] Unknown web collection definition: \${name}. Known: \${Object.keys(collectionDefinitions).join(', ')}\`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return definition;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export { collectionDefinitions as default };
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
667
721
|
* Generate virtual MCP module
|
|
668
722
|
*/
|
|
669
723
|
async function generateMCPModule(manifest) {
|
|
@@ -835,8 +889,8 @@ async function generateTypeDeclarationFile(manifest, projectRoot, typeDeclaratio
|
|
|
835
889
|
${Object.entries(obj.fields).map(([fieldName, field]) => {
|
|
836
890
|
return ` ${fieldName}${field.required === false ? "?" : ""}: ${mapTypeScriptType(field.type)};`;
|
|
837
891
|
}).join("\n")}
|
|
838
|
-
|
|
839
|
-
|
|
892
|
+
created_at?: string;
|
|
893
|
+
updated_at?: string;
|
|
840
894
|
}`;
|
|
841
895
|
}).join("\n\n");
|
|
842
896
|
const apiClientInterface = uniqueApiClientEntries(Object.entries(manifest.objects)).map(({ obj, clientKey }) => {
|
|
@@ -851,6 +905,7 @@ ${Object.entries(obj.fields).map(([fieldName, field]) => {
|
|
|
851
905
|
if (customMethods.length > 0) return ` ${clientKey}: CrudOperations<${interfaceName}> & {\n${customMethodSignatures}\n };`;
|
|
852
906
|
else return ` ${clientKey}: CrudOperations<${interfaceName}>;`;
|
|
853
907
|
}).join("\n");
|
|
908
|
+
const webCollectionInterface = selectWebCollectionEntries(manifest).map(({ collection, obj }) => ` ${collection}: SmrtWebCollectionDefinition<import('@happyvertical/smrt-virt-types').${obj.className}Data>;`).join("\n");
|
|
854
909
|
Object.entries(manifest.objects).flatMap(([_name, obj]) => Object.entries(obj.methods).map(([methodName, method]) => ({
|
|
855
910
|
name: `${methodName}_${obj.collection}`,
|
|
856
911
|
description: `${method.name} operation on ${obj.collection}`,
|
|
@@ -922,15 +977,26 @@ declare module '@happyvertical/smrt-virt-routes' {
|
|
|
922
977
|
export default setupRoutes;
|
|
923
978
|
}
|
|
924
979
|
|
|
925
|
-
// Client module - Auto-generated API client
|
|
980
|
+
// Client module - Auto-generated API client
|
|
981
|
+
// Wire-shape policy (#1797): the server returns BARE JSON — a bare array for
|
|
982
|
+
// list/search, a bare object for get/create/update — with snake_case field
|
|
983
|
+
// names (created_at, updated_at). These declarations match that shape (no
|
|
984
|
+
// envelope, no camelCase). Fetchers reject on non-2xx with a SmrtClientError
|
|
985
|
+
// (#1796). This is byte-identical to the prebuild declaration path.
|
|
926
986
|
declare module '@happyvertical/smrt-virt-client' {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
data?: T;
|
|
987
|
+
/** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */
|
|
988
|
+
export interface ApiError {
|
|
930
989
|
error?: string;
|
|
931
990
|
message?: string;
|
|
932
991
|
}
|
|
933
992
|
|
|
993
|
+
/** Typed error thrown by every fetcher on a non-2xx response (#1796). */
|
|
994
|
+
export interface SmrtClientError extends Error {
|
|
995
|
+
name: 'SmrtClientError';
|
|
996
|
+
status: number;
|
|
997
|
+
body?: ApiError | string;
|
|
998
|
+
}
|
|
999
|
+
|
|
934
1000
|
export interface CrudOperations<T = any> {
|
|
935
1001
|
list(params?: Record<string, any>): Promise<T[]>;
|
|
936
1002
|
get(id: string): Promise<T>;
|
|
@@ -977,6 +1043,67 @@ ${objectInterfaces}
|
|
|
977
1043
|
export default types;
|
|
978
1044
|
}
|
|
979
1045
|
|
|
1046
|
+
// Web module - Typed collection definitions for the browser client data
|
|
1047
|
+
// runtime (@happyvertical/smrt-web, #1761)
|
|
1048
|
+
declare module '@happyvertical/smrt-virt-web' {
|
|
1049
|
+
export type SmrtWebFieldType =
|
|
1050
|
+
| 'text'
|
|
1051
|
+
| 'decimal'
|
|
1052
|
+
| 'boolean'
|
|
1053
|
+
| 'integer'
|
|
1054
|
+
| 'datetime'
|
|
1055
|
+
| 'json'
|
|
1056
|
+
| 'foreignKey'
|
|
1057
|
+
| 'crossPackageRef'
|
|
1058
|
+
| 'meta';
|
|
1059
|
+
|
|
1060
|
+
export interface SmrtWebFieldDefinition {
|
|
1061
|
+
type: SmrtWebFieldType;
|
|
1062
|
+
required?: boolean;
|
|
1063
|
+
default?: unknown;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export type SmrtWebRelationshipKind =
|
|
1067
|
+
| 'foreignKey'
|
|
1068
|
+
| 'crossPackageRef'
|
|
1069
|
+
| 'oneToMany'
|
|
1070
|
+
| 'manyToMany';
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* A manifest-derived edge to a sibling REST collection. Mutating this
|
|
1074
|
+
* collection invalidates the caches of the collections named by these edges
|
|
1075
|
+
* (#1761 relationship-derived invalidation).
|
|
1076
|
+
*/
|
|
1077
|
+
export interface SmrtWebRelationship {
|
|
1078
|
+
field: string;
|
|
1079
|
+
kind: SmrtWebRelationshipKind;
|
|
1080
|
+
relatedCollection: string;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {
|
|
1084
|
+
name: string;
|
|
1085
|
+
className: string;
|
|
1086
|
+
endpoint: string;
|
|
1087
|
+
idField: string;
|
|
1088
|
+
actions: string[];
|
|
1089
|
+
fields: Record<string, SmrtWebFieldDefinition>;
|
|
1090
|
+
/** Manifest-derived relationship edges to sibling REST collections. */
|
|
1091
|
+
relationships: SmrtWebRelationship[];
|
|
1092
|
+
/** Phantom row-type carrier for inference — never present at runtime. */
|
|
1093
|
+
_row?: TData;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
export interface SmrtWebCollectionDefinitions {
|
|
1097
|
+
${webCollectionInterface}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export const collectionDefinitions: SmrtWebCollectionDefinitions;
|
|
1101
|
+
export function getCollectionDefinition<
|
|
1102
|
+
K extends keyof SmrtWebCollectionDefinitions,
|
|
1103
|
+
>(name: K): SmrtWebCollectionDefinitions[K];
|
|
1104
|
+
export default collectionDefinitions;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
980
1107
|
// CLI module - Auto-generated command-line interface
|
|
981
1108
|
declare module '@happyvertical/smrt-virt-cli' {
|
|
982
1109
|
export interface CLIConfig {
|