@happyvertical/smrt-template-sveltekit 0.30.0

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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * SvelteKit server hooks.
3
+ *
4
+ * Pre-wired with:
5
+ * 1. `enableTenancy()` — registers the @happyvertical/smrt-tenancy
6
+ * interceptor with `GlobalInterceptors` at priority 100, so any class
7
+ * decorated with `@TenantScoped` is auto-filtered by the tenant in
8
+ * AsyncLocalStorage. Idempotent — safe to call on every module load.
9
+ * 2. A subdomain-based tenant resolver (`tenancyHandle`) — if the URL has
10
+ * a leading subdomain (e.g. `acme.demo.local`), the request runs inside
11
+ * `withTenant({ tenantId: 'acme' })`.
12
+ * 3. `createSessionHandler({ enterTenantContext: true })` from
13
+ * @happyvertical/smrt-users/sveltekit — populates
14
+ * `event.locals.{user, permissions, tenantId, sessionId}` and pushes
15
+ * the session's tenant into the AsyncLocalStorage context.
16
+ * 4. `reconcileTenantLocals` — final reconciliation step. The session
17
+ * handler initializes `event.locals.tenantId = null` before reading
18
+ * the session, so for unauthenticated public requests it clobbers the
19
+ * subdomain value set by step 2. This handle restores the subdomain
20
+ * tenant on `event.locals.tenantId` when no session tenant set it,
21
+ * so layout/page/action code reads a consistent value regardless of
22
+ * auth state.
23
+ *
24
+ * To swap the resolution strategy, edit `src/lib/server/tenancy.ts`.
25
+ */
26
+
27
+ import { sequence } from '@sveltejs/kit/hooks';
28
+ import type { Handle, RequestEvent } from '@sveltejs/kit';
29
+
30
+ import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';
31
+ import {
32
+ createSvelteKitHandle,
33
+ enableTenancy,
34
+ getCurrentTenant,
35
+ } from '@happyvertical/smrt-tenancy';
36
+
37
+ import { resolveTenant } from '$lib/server/tenancy';
38
+ import { getSmrtConfig } from '$lib/server/smrt';
39
+
40
+ // Register the tenancy interceptor globally. Idempotent — `enableTenancy()`
41
+ // guards against duplicate registration internally.
42
+ enableTenancy();
43
+
44
+ // Both upstream factories (`createSvelteKitHandle` from smrt-tenancy and
45
+ // `createSessionHandler` from smrt-users/sveltekit) declare a structural
46
+ // event type with `locals: Record<string, unknown>` to avoid taking a
47
+ // hard dependency on `@sveltejs/kit`. SvelteKit's own `Handle` expects
48
+ // `event.locals: App.Locals` — a strict named interface without an
49
+ // index signature — so the two shapes aren't directly assignable under
50
+ // strict typechecking. The casts below adapt the upstream return types
51
+ // to SvelteKit's `Handle` once. Runtime behaviour is unchanged; the
52
+ // types still flow through `Handle` to the `sequence(...)` composition.
53
+
54
+ /**
55
+ * Subdomain → tenantId handle. Runs BEFORE the session handler so generated
56
+ * routes have a tenant in context even for unauthenticated public requests
57
+ * (e.g. tenant-scoped catalog endpoints). If a session also carries a
58
+ * tenant, the session handler's inner `withTenant()` will override.
59
+ */
60
+ const tenancyHandle = createSvelteKitHandle({
61
+ resolveTenantId: async (event) => {
62
+ // The createSvelteKitHandle adapter passes a structural event; our
63
+ // resolver accepts that same shape.
64
+ const result = await resolveTenant(event as RequestEvent);
65
+ return result.tenantId;
66
+ },
67
+ }) as unknown as Handle;
68
+
69
+ /**
70
+ * Session handle from @happyvertical/smrt-users. `enterTenantContext: true`
71
+ * makes it call `enterTenantContext()` after loading the session, so
72
+ * downstream code sees the *session's* tenant id when one is present.
73
+ */
74
+ const sessionHandle = createSessionHandler({
75
+ ...getSmrtConfig('Session'),
76
+ enterTenantContext: true,
77
+ }) as unknown as Handle;
78
+
79
+ /**
80
+ * Final reconciliation: read the active AsyncLocalStorage tenant (already
81
+ * set correctly by tenancyHandle + sessionHandle, with session winning
82
+ * over subdomain when both are present) and align BOTH `event.locals.tenantId`
83
+ * AND `event.locals.tenantContext` to it.
84
+ *
85
+ * Two failure modes this guards against:
86
+ *
87
+ * 1. **Public requests with a subdomain.** `createSessionHandler` resets
88
+ * `event.locals.tenantId` to `null` before checking for a session
89
+ * cookie. With no session, the session handler returns without
90
+ * repopulating it, so the subdomain-resolved tenant goes missing
91
+ * from layouts/actions even though the ALS context (set by
92
+ * `tenancyHandle`) still carries it.
93
+ * 2. **Authenticated requests where the session's tenant differs from
94
+ * the subdomain.** The session handler calls `withTenant(sessionCtx, ...)`
95
+ * — switching the ALS context to the session tenant — and updates
96
+ * `event.locals.tenantId` to match. But it does NOT touch
97
+ * `event.locals.tenantContext`, which is still the subdomain context
98
+ * previously written by `tenancyHandle`. Layouts/actions reading
99
+ * `tenantContext` would then see stale permissions/superAdminBypass
100
+ * that don't match the tenant their queries are scoped to.
101
+ *
102
+ * Reading the live ALS context here and writing it back to BOTH
103
+ * `locals.tenantId` and `locals.tenantContext` keeps the three vantage
104
+ * points (locals.tenantId, locals.tenantContext, ALS-scoped queries) in
105
+ * sync regardless of auth state.
106
+ *
107
+ * **ALS-scope dependency:** this handle reads `getCurrentTenant()` from
108
+ * AsyncLocalStorage, which only works because both upstream handles
109
+ * call `resolve(event)` *inside* their own ALS scope:
110
+ * - `createSvelteKitHandle` (smrt-tenancy) wraps in `withTenant(ctx, () => resolve(event))`.
111
+ * - `createSessionHandler` (smrt-users) wraps in `withSessionPermissionContext`,
112
+ * which internally calls `withTenant(sessionCtx, fn)` when the session
113
+ * has a tenantId.
114
+ * If a future refactor moved either handle's `resolve()` call outside
115
+ * its ALS scope, `getCurrentTenant()` here would return `undefined` and
116
+ * the reconciliation would silently no-op. We deliberately do NOT fall
117
+ * back to `event.locals.tenantId` here — that would re-introduce the
118
+ * stale-tenantContext bug from the subdomain handler. If the upstream
119
+ * scoping ever changes, this handle should be updated to read from a
120
+ * dedicated cross-handler carrier (e.g. `event.locals._tenantSource`)
121
+ * rather than guessing.
122
+ */
123
+ const reconcileTenantLocals: Handle = async ({ event, resolve }) => {
124
+ const activeContext = getCurrentTenant();
125
+ if (activeContext) {
126
+ event.locals.tenantId = activeContext.tenantId;
127
+ event.locals.tenantContext = activeContext;
128
+ }
129
+ return resolve(event);
130
+ };
131
+
132
+ export const handle: Handle = sequence(
133
+ tenancyHandle,
134
+ sessionHandle,
135
+ reconcileTenantLocals,
136
+ );
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Example SMRT Object
3
+ *
4
+ * This is a sample SMRT object to demonstrate the pattern.
5
+ * Rename or replace this with your own objects.
6
+ */
7
+
8
+ import { SmrtObject, smrt } from '@happyvertical/smrt-core';
9
+
10
+ @smrt({
11
+ api: {
12
+ include: ['list', 'get', 'create', 'update', 'delete', 'summarize'],
13
+ },
14
+ cli: { include: ['list', 'get', 'summarize'] },
15
+ })
16
+ export class Item extends SmrtObject {
17
+ /** Item title */
18
+ title: string = '';
19
+
20
+ /** Item description */
21
+ description: string = '';
22
+
23
+ /** Status of the item */
24
+ status: string = 'draft';
25
+
26
+ /** When the item was published */
27
+ publishedAt: Date | null = null;
28
+
29
+ /**
30
+ * AI-powered summarization
31
+ */
32
+ async summarize(): Promise<string> {
33
+ return await this.do('Create a brief summary of this item');
34
+ }
35
+
36
+ /**
37
+ * Mark item as published
38
+ */
39
+ async publish(): Promise<void> {
40
+ this.status = 'published';
41
+ this.publishedAt = new Date();
42
+ await this.save();
43
+ }
44
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SMRT Objects Index
3
+ *
4
+ * Export all SMRT objects from this file.
5
+ * They will be automatically registered and available for CLI/API/MCP.
6
+ */
7
+
8
+ export { Item } from './Item.js';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Centralized SMRT Configuration
3
+ *
4
+ * This file provides configuration for all SMRT objects in your project.
5
+ * Import this file in your routes to get properly configured collections.
6
+ */
7
+
8
+ import { ObjectRegistry, type SmrtClassOptions } from '@happyvertical/smrt-core';
9
+
10
+ // Import all SMRT objects to register them
11
+ import '../objects/index.js';
12
+
13
+ declare global {
14
+ // eslint-disable-next-line no-var
15
+ var __smrtGetRequestScopedDatabase:
16
+ | (() => SmrtClassOptions['db'] | undefined)
17
+ | undefined;
18
+ }
19
+
20
+ /**
21
+ * Per-object configuration overrides
22
+ *
23
+ * Use this to configure specific objects differently from defaults.
24
+ * Example:
25
+ * AuditLog: { db: { url: process.env.AUDIT_DB_URL!, type: 'postgres' } }
26
+ */
27
+ const objectOverrides: Record<string, Partial<SmrtClassOptions>> = {
28
+ // Add object-specific overrides here
29
+ };
30
+
31
+ /**
32
+ * Get default configuration for SMRT objects
33
+ */
34
+ function getDefaultConfig(): SmrtClassOptions {
35
+ return {
36
+ db: {
37
+ url: process.env.DATABASE_URL || './data/app.db',
38
+ type: (process.env.DATABASE_TYPE as 'sqlite' | 'postgres') || 'sqlite',
39
+ },
40
+ ai: process.env.OPENAI_API_KEY
41
+ ? {
42
+ type: 'openai',
43
+ apiKey: process.env.OPENAI_API_KEY,
44
+ }
45
+ : undefined,
46
+ };
47
+ }
48
+
49
+ function getRequestScopedDatabase(): SmrtClassOptions['db'] | undefined {
50
+ const getter = globalThis.__smrtGetRequestScopedDatabase;
51
+ return typeof getter === 'function' ? getter() : undefined;
52
+ }
53
+
54
+ /**
55
+ * Get configuration for a specific SMRT class
56
+ */
57
+ export function getSmrtConfig(className: string): SmrtClassOptions {
58
+ const defaults = getDefaultConfig();
59
+ const override = objectOverrides[className];
60
+ return override ? { ...defaults, ...override } : defaults;
61
+ }
62
+
63
+ /**
64
+ * Get a collection instance for a SMRT class
65
+ *
66
+ * Usage in routes:
67
+ * const products = await getCollection<Product>('Product');
68
+ * const items = await products.list();
69
+ */
70
+ export async function getCollection<T>(className: string) {
71
+ const config = getSmrtConfig(className);
72
+ const objectOverride = objectOverrides[className];
73
+ const requestScopedDb = objectOverride?.db
74
+ ? undefined
75
+ : getRequestScopedDatabase();
76
+
77
+ return await ObjectRegistry.getCollection<T>(className, {
78
+ ...config,
79
+ db: requestScopedDb ?? config.db,
80
+ });
81
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Tenant resolution for SvelteKit requests.
3
+ *
4
+ * This file is intentionally pluggable — consumers can swap the strategy
5
+ * function without forking. The shipped default is subdomain-based:
6
+ *
7
+ * acme.demo.local → tenantId='acme'
8
+ * www.demo.local → tenantId=null
9
+ * demo.local → tenantId=null
10
+ * localhost / 127.0.0.1 → tenantId=null
11
+ *
12
+ * The resolver's signature is intentionally minimal — it accepts a
13
+ * SvelteKit-like `RequestEvent` (typed structurally so this module has zero
14
+ * `@sveltejs/kit` runtime dependency) and returns a synchronous result.
15
+ *
16
+ * @example Swap to path-prefix resolution
17
+ * ```ts
18
+ * // src/lib/server/tenancy.ts (your project)
19
+ * import { createTenantResolver, pathPrefixStrategy } from './tenancy';
20
+ *
21
+ * export const resolveTenant = createTenantResolver(pathPrefixStrategy);
22
+ * ```
23
+ *
24
+ * @example Swap to header-based resolution
25
+ * ```ts
26
+ * import { createTenantResolver, headerStrategy } from './tenancy';
27
+ *
28
+ * export const resolveTenant = createTenantResolver(
29
+ * headerStrategy({ headerName: 'x-tenant-id' }),
30
+ * );
31
+ * ```
32
+ *
33
+ * @example Compose your own
34
+ * ```ts
35
+ * import { createTenantResolver, subdomainStrategy } from './tenancy';
36
+ *
37
+ * export const resolveTenant = createTenantResolver((event) => {
38
+ * // Try subdomain first, then fall back to a header
39
+ * const fromSubdomain = subdomainStrategy(event);
40
+ * if (fromSubdomain.tenantId) return fromSubdomain;
41
+ * return { tenantId: event.request.headers.get('x-tenant-id') };
42
+ * });
43
+ * ```
44
+ */
45
+
46
+ /**
47
+ * Structural SvelteKit RequestEvent — only the bits we need for tenant
48
+ * resolution. Keeping this minimal means the module has no @sveltejs/kit
49
+ * runtime import, which keeps tests + non-SvelteKit consumers happy.
50
+ */
51
+ export interface TenantResolverEvent {
52
+ url: URL;
53
+ request: { headers: Headers };
54
+ params?: Record<string, string | undefined>;
55
+ }
56
+
57
+ /**
58
+ * Resolution result. `null` means "no tenant for this request".
59
+ */
60
+ export interface TenantResolution {
61
+ tenantId: string | null;
62
+ }
63
+
64
+ /**
65
+ * A resolver strategy is just a function from event → resolution. Sync or
66
+ * async — the dispatcher awaits the return value.
67
+ */
68
+ export type TenantResolverStrategy = (
69
+ event: TenantResolverEvent,
70
+ ) => TenantResolution | Promise<TenantResolution>;
71
+
72
+ /**
73
+ * Hostnames that should never produce a tenant id, regardless of how many
74
+ * dots they contain. Treats `localhost`, `127.0.0.1`, and `::1` as
75
+ * tenant-less so local dev "just works" until you set up a wildcard DNS
76
+ * entry like `*.demo.local → 127.0.0.1`.
77
+ */
78
+ const ROOT_LIKE_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
79
+
80
+ /**
81
+ * Subdomain labels that are *never* tenant slugs even if they appear in the
82
+ * leading position. Consumers can pass their own list to
83
+ * {@link subdomainStrategyWith} if they need to extend this.
84
+ */
85
+ const DEFAULT_RESERVED_SUBDOMAINS = new Set(['www', 'api', 'app']);
86
+
87
+ /**
88
+ * Strategy: extract the tenant id from the leading subdomain.
89
+ *
90
+ * Behavior:
91
+ * - `acme.demo.local` → `{ tenantId: 'acme' }`
92
+ * - `acme.beta.demo.local` → `{ tenantId: 'acme' }` (only leading label)
93
+ * - `www.demo.local` → `{ tenantId: null }` (reserved)
94
+ * - `demo.local` → `{ tenantId: null }` (no leading subdomain)
95
+ * - `localhost` → `{ tenantId: null }`
96
+ * - `127.0.0.1` or any IPv4/IPv6 → `{ tenantId: null }`
97
+ */
98
+ export const subdomainStrategy: TenantResolverStrategy = (event) => {
99
+ return subdomainStrategyWith()(event);
100
+ };
101
+
102
+ /**
103
+ * Customizable subdomain strategy. Use this if you need to extend the
104
+ * reserved-subdomain list (e.g. to also reject `admin.demo.local`) or
105
+ * to deploy on a multi-label public suffix like `example.co.uk` where
106
+ * a naive label count would treat the apex itself as a tenant.
107
+ *
108
+ * The supplied `reservedSubdomains` are **merged with** the built-in
109
+ * defaults (`www`, `api`, `app`) — `www`, `api`, and `app` always stay
110
+ * reserved regardless of what you pass. Pass `[]` or omit the option
111
+ * if you only need the defaults; the merge is additive-only. Replacing
112
+ * the default list outright is not currently supported via this
113
+ * strategy — fork the function or wire your own `TenantResolverStrategy`
114
+ * if you need that.
115
+ *
116
+ * `baseDomain` (strongly recommended for production) anchors the apex.
117
+ * When set, the strategy strips the matching suffix and treats whatever
118
+ * leading labels remain as the tenant. `example.co.uk` with
119
+ * `baseDomain: 'example.co.uk'` → no tenant; `acme.example.co.uk` →
120
+ * `'acme'`. Without `baseDomain` the strategy falls back to a label-
121
+ * count heuristic that works for `.com`/`.net`/`.dev`-style single-
122
+ * label TLDs but mis-identifies multi-label public suffixes (`.co.uk`,
123
+ * `.com.au`, `.gov.uk`, …) as tenant subdomains.
124
+ */
125
+ export function subdomainStrategyWith(opts?: {
126
+ reservedSubdomains?: Iterable<string>;
127
+ /**
128
+ * Apex domain to anchor tenant detection against. Highly recommended
129
+ * for production deployments on multi-label public suffixes
130
+ * (`example.co.uk`, `example.com.au`, …). When set, a request for
131
+ * exactly that host returns `{ tenantId: null }`, and a request for
132
+ * `<tenant>.<baseDomain>` returns `{ tenantId: '<tenant>' }`. Subdomain
133
+ * resolution stops being label-count-dependent.
134
+ */
135
+ baseDomain?: string;
136
+ }): TenantResolverStrategy {
137
+ const reserved = new Set<string>(DEFAULT_RESERVED_SUBDOMAINS);
138
+ if (opts?.reservedSubdomains) {
139
+ for (const s of opts.reservedSubdomains) {
140
+ reserved.add(s.toLowerCase());
141
+ }
142
+ }
143
+ const baseDomain = opts?.baseDomain?.toLowerCase().replace(/^\.+|\.+$/g, '');
144
+
145
+ return (event) => {
146
+ const hostname = event.url.hostname.toLowerCase();
147
+
148
+ if (ROOT_LIKE_HOSTS.has(hostname)) {
149
+ return { tenantId: null };
150
+ }
151
+
152
+ // Reject bare IPv4 (e.g. `192.168.1.10`) — never a tenant subdomain.
153
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
154
+ return { tenantId: null };
155
+ }
156
+
157
+ // Preferred path: caller supplied a baseDomain. Strip it and read
158
+ // whatever's left as the tenant. This works correctly for both
159
+ // simple TLDs (`example.com`) and multi-label public suffixes
160
+ // (`example.co.uk`).
161
+ if (baseDomain) {
162
+ if (hostname === baseDomain) {
163
+ return { tenantId: null };
164
+ }
165
+ const suffix = `.${baseDomain}`;
166
+ if (hostname.endsWith(suffix)) {
167
+ const leading = hostname.slice(0, -suffix.length);
168
+ // Take only the first label of whatever's left so
169
+ // `acme.beta.example.co.uk` still resolves to `acme`.
170
+ const candidate = leading.split('.')[0];
171
+ if (!candidate || reserved.has(candidate)) {
172
+ return { tenantId: null };
173
+ }
174
+ return { tenantId: candidate };
175
+ }
176
+ // Host doesn't match the configured apex — out of scope for this
177
+ // strategy, treat as tenant-less rather than guess.
178
+ return { tenantId: null };
179
+ }
180
+
181
+ // Fallback path: no baseDomain configured. Use a label-count
182
+ // heuristic that works for single-label TLDs. **This is incorrect
183
+ // for multi-label public suffixes** (`example.co.uk` has three
184
+ // labels and would be misread as `example` being a tenant on the
185
+ // `co.uk` root). Pass `baseDomain` to opt out of the heuristic on
186
+ // any production deployment that uses such a TLD.
187
+ const labels = hostname.split('.');
188
+
189
+ // Need at least 3 labels for a tenant subdomain: <tenant>.<root>.<tld>.
190
+ // `demo.local` (2 labels) has no subdomain.
191
+ if (labels.length < 3) {
192
+ return { tenantId: null };
193
+ }
194
+
195
+ const candidate = labels[0];
196
+ if (!candidate || reserved.has(candidate)) {
197
+ return { tenantId: null };
198
+ }
199
+
200
+ return { tenantId: candidate };
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Strategy: extract the tenant id from a path prefix like `/t/<slug>/...`.
206
+ * The prefix is configurable; default is `/t/`.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * // matches /t/acme/dashboard → tenantId='acme'
211
+ * createTenantResolver(pathPrefixStrategy());
212
+ *
213
+ * // matches /tenant/acme/... → tenantId='acme'
214
+ * createTenantResolver(pathPrefixStrategy({ prefix: '/tenant/' }));
215
+ * ```
216
+ */
217
+ export function pathPrefixStrategy(opts?: {
218
+ prefix?: string;
219
+ }): TenantResolverStrategy {
220
+ const prefix = opts?.prefix ?? '/t/';
221
+ return (event) => {
222
+ const path = event.url.pathname;
223
+ if (!path.startsWith(prefix)) {
224
+ return { tenantId: null };
225
+ }
226
+ const rest = path.slice(prefix.length);
227
+ const slug = rest.split('/')[0];
228
+ return { tenantId: slug && slug.length > 0 ? slug : null };
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Strategy: read the tenant id from a request header.
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * createTenantResolver(headerStrategy({ headerName: 'x-tenant-id' }));
238
+ * ```
239
+ */
240
+ export function headerStrategy(opts?: {
241
+ headerName?: string;
242
+ }): TenantResolverStrategy {
243
+ const headerName = opts?.headerName ?? 'x-tenant-id';
244
+ return (event) => {
245
+ const value = event.request.headers.get(headerName);
246
+ return { tenantId: value && value.length > 0 ? value : null };
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Wrap any strategy into a resolver. This is a thin factory — its purpose
252
+ * is to give consumers a single, stable entry point (`resolveTenant`) they
253
+ * can swap by changing only the argument.
254
+ */
255
+ export function createTenantResolver(
256
+ strategy: TenantResolverStrategy,
257
+ ): (event: TenantResolverEvent) => Promise<TenantResolution> {
258
+ return async (event) => {
259
+ return await strategy(event);
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Default resolver used by `hooks.server.ts`. Swap the argument to
265
+ * `createTenantResolver()` (or replace this export) to change strategies.
266
+ */
267
+ export const resolveTenant = createTenantResolver(subdomainStrategy);
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ let items: any[] = $state([]);
3
+ let loading = $state(true);
4
+ let error: string | null = $state(null);
5
+
6
+ $effect(() => {
7
+ fetchItems();
8
+ });
9
+
10
+ async function fetchItems() {
11
+ try {
12
+ const response = await fetch('/api/items');
13
+ const data = await response.json();
14
+ items = data.items || [];
15
+ } catch (e) {
16
+ error = e instanceof Error ? e.message : 'Failed to fetch items';
17
+ } finally {
18
+ loading = false;
19
+ }
20
+ }
21
+ </script>
22
+
23
+ <svelte:head>
24
+ <title>SMRT SvelteKit App</title>
25
+ </svelte:head>
26
+
27
+ <main>
28
+ <h1>Welcome to SMRT + SvelteKit</h1>
29
+
30
+ <section>
31
+ <h2>Items</h2>
32
+
33
+ {#if loading}
34
+ <p>Loading...</p>
35
+ {:else if error}
36
+ <p class="error">{error}</p>
37
+ {:else if items.length === 0}
38
+ <p>No items yet. Create your first item!</p>
39
+ {:else}
40
+ <ul>
41
+ {#each items as item}
42
+ <li>
43
+ <strong>{item.title}</strong>
44
+ <span class="status">{item.status}</span>
45
+ </li>
46
+ {/each}
47
+ </ul>
48
+ {/if}
49
+ </section>
50
+
51
+ <section>
52
+ <h2>Getting Started</h2>
53
+ <ul>
54
+ <li>Edit <code>src/lib/objects/Item.ts</code> to customize your SMRT object</li>
55
+ <li>Run <code>smrt objects</code> to see registered objects</li>
56
+ <li>Run <code>smrt generate-routes</code> to regenerate API routes</li>
57
+ <li>API routes are auto-generated in <code>src/routes/api/</code></li>
58
+ </ul>
59
+ </section>
60
+ </main>
61
+
62
+ <style>
63
+ main {
64
+ max-width: 800px;
65
+ margin: 0 auto;
66
+ padding: 2rem;
67
+ font-family: system-ui, -apple-system, sans-serif;
68
+ }
69
+
70
+ h1 {
71
+ color: #667eea;
72
+ }
73
+
74
+ section {
75
+ margin: 2rem 0;
76
+ padding: 1rem;
77
+ background: #f5f5f5;
78
+ border-radius: var(--smrt-radius-md, 8px);
79
+ }
80
+
81
+ .error {
82
+ color: #c33;
83
+ }
84
+
85
+ .status {
86
+ font-size: 0.8em;
87
+ background: #e0e0e0;
88
+ padding: 2px 8px;
89
+ border-radius: var(--smrt-radius-sm, 4px);
90
+ margin-left: 8px;
91
+ }
92
+
93
+ code {
94
+ background: #e0e0e0;
95
+ padding: 2px 6px;
96
+ border-radius: var(--smrt-radius-sm, 4px);
97
+ font-size: 0.9em;
98
+ }
99
+
100
+ ul {
101
+ list-style: disc;
102
+ padding-left: 1.5rem;
103
+ }
104
+
105
+ li {
106
+ margin: 0.5rem 0;
107
+ }
108
+ </style>
@@ -0,0 +1,16 @@
1
+ import adapter from '@sveltejs/adapter-auto';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ preprocess: vitePreprocess(),
7
+
8
+ kit: {
9
+ adapter: adapter(),
10
+ alias: {
11
+ $lib: 'src/lib',
12
+ },
13
+ },
14
+ };
15
+
16
+ export default config;
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "moduleResolution": "bundler",
13
+ "experimentalDecorators": true,
14
+ "emitDecoratorMetadata": true
15
+ }
16
+ }