@happyvertical/smrt-template-sveltekit 0.43.10 → 0.44.1

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 (41) hide show
  1. package/AGENTS.md +31 -6
  2. package/README.md +26 -10
  3. package/index.js +53 -2
  4. package/package.json +19 -5
  5. package/template/.dockerignore +12 -0
  6. package/template/.env.cloud.example +9 -0
  7. package/template/.env.example +15 -3
  8. package/template/.env.self-hosted.example +11 -0
  9. package/template/AGENTS.md +32 -0
  10. package/template/Dockerfile +23 -0
  11. package/template/INSTALL_PROMPT.md +11 -0
  12. package/template/README.md +74 -32
  13. package/template/compose.yaml +56 -0
  14. package/template/gitignore.template +45 -0
  15. package/template/package.json +30 -15
  16. package/template/scripts/smrt-app.mjs +884 -0
  17. package/template/scripts/smrt-operation-lock.mjs +102 -0
  18. package/template/scripts/smrt-portability-assets.mjs +707 -0
  19. package/template/scripts/smrt-portability.mjs +470 -0
  20. package/template/scripts/smrt-prepare-migration.mjs +93 -0
  21. package/template/scripts/smrt-process.mjs +82 -0
  22. package/template/scripts/smrt-provider-readiness.mjs +33 -0
  23. package/template/scripts/smrt-runtime-identity.mjs +238 -0
  24. package/template/scripts/smrt-vite.mjs +45 -0
  25. package/template/scripts/smrt-web.mjs +10 -0
  26. package/template/scripts/smrt-worker.mjs +86 -0
  27. package/template/scripts/smrt-writer-lease.mjs +199 -0
  28. package/template/smrt.config.ts +34 -2
  29. package/template/src/hooks.server.ts +4 -1
  30. package/template/src/lib/RuntimeDiagnosticsWebMcp.svelte +11 -0
  31. package/template/src/lib/runtime-diagnostics-webmcp.ts +108 -0
  32. package/template/src/lib/server/application-runtime.ts +262 -0
  33. package/template/src/lib/server/runtime-diagnostics.ts +114 -0
  34. package/template/src/lib/server/smrt.ts +4 -4
  35. package/template/src/routes/+layout.svelte +2 -0
  36. package/template/src/routes/api/_runtime/diagnostics/+server.ts +5 -0
  37. package/template/src/routes/api/_runtime/health/+server.ts +30 -0
  38. package/template/src/routes/setup/+page.server.ts +96 -0
  39. package/template/src/routes/setup/+page.svelte +31 -0
  40. package/template/svelte.config.js +1 -1
  41. package/template/tsconfig.json +1 -0
@@ -0,0 +1,238 @@
1
+ import {
2
+ closeSync,
3
+ constants,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ } from 'node:fs';
11
+ import { basename, dirname, join, parse, resolve } from 'node:path';
12
+ import { homedir, platform } from 'node:os';
13
+ import {
14
+ encodeApplicationId,
15
+ resolveLocalRuntimePaths,
16
+ validateApplicationId,
17
+ } from '@happyvertical/smrt-app-runtime';
18
+
19
+ /**
20
+ * Resolve one stable identity for CLI, development, and app operations.
21
+ * @param {{sourceRoot?: string, packageName?: string, explicitId?: string}} [options]
22
+ */
23
+ export function resolveApplicationId(options = {}) {
24
+ const sourceRoot = options.sourceRoot || process.cwd();
25
+ let packageName = options.packageName;
26
+ if (!packageName) {
27
+ packageName = JSON.parse(
28
+ readFileSync(join(sourceRoot, 'package.json'), 'utf8'),
29
+ ).name;
30
+ }
31
+ if (typeof packageName !== 'string' || packageName.trim() === '') {
32
+ throw new Error('package.json must declare a non-empty package name.');
33
+ }
34
+ return options.explicitId
35
+ ? validateApplicationId(options.explicitId)
36
+ : encodeApplicationId(packageName);
37
+ }
38
+
39
+ /**
40
+ * State is derived from the canonical application/data identity. It is not an
41
+ * independent override because every process and operator command must share
42
+ * one lock domain for a given database root.
43
+ * @param {{appId: string, dataDirectory?: string, sourceRoot?: string, platformName?: string, homeDirectory?: string, environment?: Record<string, string | undefined>}} options
44
+ */
45
+ export function resolveApplicationStateRoot(options) {
46
+ const paths = resolveLocalRuntimePaths({
47
+ appId: options.appId,
48
+ dataDirectory: options.dataDirectory,
49
+ sourceRoot: options.sourceRoot,
50
+ });
51
+ const platformName = options.platformName || platform();
52
+ const homeDirectory = options.homeDirectory || homedir();
53
+ const environment = options.environment || process.env;
54
+ const stateBase =
55
+ platformName === 'darwin'
56
+ ? join(homeDirectory, 'Library', 'Application Support')
57
+ : platformName === 'win32'
58
+ ? environment.LOCALAPPDATA || homeDirectory
59
+ : environment.XDG_STATE_HOME || join(homeDirectory, '.local', 'state');
60
+ const dataIdentity = createHash('sha256')
61
+ .update(resolve(paths.root))
62
+ .digest('hex')
63
+ .slice(0, 12);
64
+ return resolve(stateBase, `.${options.appId}-${dataIdentity}-state`);
65
+ }
66
+
67
+ /** @param {string | undefined} value */
68
+ function databaseTargetIdentity(value) {
69
+ if (!value) return null;
70
+ try {
71
+ const target = new URL(value);
72
+ const sslMode = target.searchParams.get('sslmode');
73
+ target.username = '';
74
+ target.password = '';
75
+ target.search = '';
76
+ target.hash = '';
77
+ if (sslMode) target.searchParams.set('sslmode', sslMode);
78
+ return target.toString();
79
+ } catch {
80
+ return resolve(value);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Secret-safe identity used to reject stale managed processes after profile,
86
+ * provider, database-target, or listener configuration changes.
87
+ * @param {{profile: string, providers: object}} runtime
88
+ * @param {Record<string, string | undefined>} [environment]
89
+ */
90
+ export function runtimeConfigurationFingerprint(
91
+ runtime,
92
+ environment = process.env,
93
+ ) {
94
+ return createHash('sha256')
95
+ .update(
96
+ JSON.stringify({
97
+ profile: runtime.profile,
98
+ providers: runtime.providers,
99
+ databaseTarget: databaseTargetIdentity(environment.DATABASE_URL),
100
+ host: environment.HOST || null,
101
+ port: environment.PORT || null,
102
+ backgroundJobs: environment.SMRT_BACKGROUND_JOBS === 'true',
103
+ readinessModules: {
104
+ authentication: environment.SMRT_AUTH_READINESS_MODULE || null,
105
+ assets: environment.SMRT_ASSETS_READINESS_MODULE || null,
106
+ secrets: environment.SMRT_SECRETS_READINESS_MODULE || null,
107
+ },
108
+ }),
109
+ )
110
+ .digest('hex');
111
+ }
112
+
113
+ /** @param {string} parent @param {string} child */
114
+ function isInside(parent, child) {
115
+ const relative = child.slice(parent.length);
116
+ return (
117
+ child === parent ||
118
+ (child.startsWith(parent) && (relative.startsWith('/') || relative.startsWith('\\')))
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Resolve an operator-selected artifact through its nearest existing ancestor
124
+ * and reject paths that could place application data in or over the checkout.
125
+ * @param {{sourceRoot: string, path: string, label?: string}} options
126
+ */
127
+ export function assertExternalArtifactPath(options) {
128
+ const canonicalSource = realpathSync(resolve(options.sourceRoot));
129
+ const missingSegments = [];
130
+ let existingAncestor = resolve(options.path);
131
+ while (!existsSync(existingAncestor)) {
132
+ const parent = dirname(existingAncestor);
133
+ if (parent === existingAncestor) break;
134
+ missingSegments.unshift(basename(existingAncestor));
135
+ existingAncestor = parent;
136
+ }
137
+ const canonicalPath = resolve(
138
+ realpathSync(existingAncestor),
139
+ ...missingSegments,
140
+ );
141
+ if (
142
+ isInside(canonicalSource, canonicalPath) ||
143
+ isInside(canonicalPath, canonicalSource)
144
+ ) {
145
+ throw new Error(
146
+ `${options.label || 'Artifact'} must remain outside the source tree.`,
147
+ );
148
+ }
149
+ return canonicalPath;
150
+ }
151
+
152
+ /** @param {unknown} error */
153
+ function errorCode(error) {
154
+ return error && typeof error === 'object' && 'code' in error
155
+ ? error.code
156
+ : undefined;
157
+ }
158
+
159
+ /**
160
+ * Create or verify the private, app-bound state/lock directory.
161
+ * @param {{appId: string, dataDirectory?: string, sourceRoot?: string, platformName?: string, homeDirectory?: string, environment?: Record<string, string | undefined>}} options
162
+ */
163
+ export function prepareApplicationStateRoot(options) {
164
+ const sourceRoot = realpathSync(resolve(options.sourceRoot || process.cwd()));
165
+ const stateRoot = resolveApplicationStateRoot(options);
166
+ if (isInside(sourceRoot, stateRoot) || isInside(stateRoot, sourceRoot)) {
167
+ throw new Error('Application state must remain outside the source tree.');
168
+ }
169
+ const currentUid = process.getuid?.();
170
+ let component = parse(stateRoot).root;
171
+ for (const part of stateRoot.slice(component.length).split(/[\\/]+/).filter(Boolean)) {
172
+ component = join(component, part);
173
+ try {
174
+ const details = lstatSync(component);
175
+ if (details.isSymbolicLink() || !details.isDirectory()) {
176
+ throw new Error(`Application state path component is unsafe: ${component}`);
177
+ }
178
+ const sharedStickyRoot = details.uid === 0 && (details.mode & 0o1000) !== 0;
179
+ if (
180
+ currentUid !== undefined &&
181
+ ((details.uid !== currentUid && details.uid !== 0) ||
182
+ ((details.mode & 0o022) !== 0 && !sharedStickyRoot))
183
+ ) {
184
+ throw new Error(`Application state path lacks trusted custody: ${component}`);
185
+ }
186
+ } catch (error) {
187
+ if (errorCode(error) !== 'ENOENT') throw error;
188
+ try {
189
+ mkdirSync(component, { mode: 0o700 });
190
+ } catch (mkdirError) {
191
+ if (errorCode(mkdirError) !== 'EEXIST') throw mkdirError;
192
+ }
193
+ const created = lstatSync(component);
194
+ if (
195
+ created.isSymbolicLink() ||
196
+ !created.isDirectory() ||
197
+ (currentUid !== undefined && created.uid !== currentUid) ||
198
+ (created.mode & 0o777) !== 0o700
199
+ ) {
200
+ throw new Error(`Application state path component is unsafe: ${component}`);
201
+ }
202
+ }
203
+ }
204
+ const rootDetails = lstatSync(stateRoot);
205
+ if (
206
+ rootDetails.isSymbolicLink() ||
207
+ !rootDetails.isDirectory() ||
208
+ (currentUid !== undefined && rootDetails.uid !== currentUid) ||
209
+ (rootDetails.mode & 0o777) !== 0o700
210
+ ) {
211
+ throw new Error('Application state root must be current-user-owned mode 0700.');
212
+ }
213
+ const markerPath = join(stateRoot, `.smrt-state-${validateApplicationId(options.appId)}`);
214
+ let descriptor;
215
+ try {
216
+ descriptor = openSync(
217
+ markerPath,
218
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
219
+ 0o600,
220
+ );
221
+ } catch (error) {
222
+ if (errorCode(error) !== 'EEXIST') throw error;
223
+ const marker = lstatSync(markerPath);
224
+ if (
225
+ marker.isSymbolicLink() ||
226
+ !marker.isFile() ||
227
+ marker.size !== 0 ||
228
+ (currentUid !== undefined && marker.uid !== currentUid) ||
229
+ (marker.mode & 0o777) !== 0o600
230
+ ) {
231
+ throw new Error('Application state marker is unsafe.');
232
+ }
233
+ } finally {
234
+ if (descriptor !== undefined) closeSync(descriptor);
235
+ }
236
+ return stateRoot;
237
+ }
238
+ import { createHash } from 'node:crypto';
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync, writeSync } from 'node:fs';
2
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+
5
+ function fail(message) {
6
+ writeSync(2, `s-m-r-t Vite launcher: ${message}\n`);
7
+ process.exit(1);
8
+ }
9
+
10
+ let packagePath;
11
+ try {
12
+ packagePath = fileURLToPath(import.meta.resolve('vite/package.json'));
13
+ } catch (error) {
14
+ const detail = error instanceof Error ? `: ${error.message}` : '';
15
+ fail(
16
+ `Could not resolve the installed Vite package${detail}. Verify the generated app dependencies and Vite compatibility.`,
17
+ );
18
+ }
19
+ const packageRoot = dirname(packagePath);
20
+ let metadata;
21
+ try {
22
+ metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
23
+ } catch {
24
+ fail('The installed Vite package metadata is invalid; reinstall dependencies.');
25
+ }
26
+ const declaredBin =
27
+ typeof metadata.bin === 'string' ? metadata.bin : metadata.bin?.vite;
28
+ if (typeof declaredBin !== 'string' || declaredBin === '') {
29
+ fail('The installed Vite package does not declare its CLI.');
30
+ }
31
+ const entry = resolve(packageRoot, declaredBin);
32
+ const relativeEntry = relative(packageRoot, entry);
33
+ if (
34
+ relativeEntry === '' ||
35
+ relativeEntry.startsWith('..') ||
36
+ isAbsolute(relativeEntry)
37
+ ) {
38
+ fail('The installed Vite package declares an unsafe CLI path.');
39
+ }
40
+ if (!existsSync(entry)) {
41
+ fail('The installed Vite package CLI is missing; reinstall dependencies.');
42
+ }
43
+
44
+ process.argv = [process.execPath, entry, ...process.argv.slice(2)];
45
+ await import(pathToFileURL(entry).href);
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ const instance = process.argv
4
+ .find((value) => value.startsWith('--smrt-instance='))
5
+ ?.slice('--smrt-instance='.length);
6
+ if (!instance || !/^[a-f0-9]{32}$/.test(instance)) {
7
+ throw new Error('A valid application process identity is required.');
8
+ }
9
+
10
+ await import('../build/index.js');
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { initializeDeployedApplicationRuntime } from '@happyvertical/smrt-app-runtime';
4
+ import {
5
+ loadConfig,
6
+ resolveConfiguredApplicationRuntime,
7
+ } from '@happyvertical/smrt-config';
8
+ import { getDatabase } from '@happyvertical/sql';
9
+ import { createProviderReadinessProbe } from './smrt-provider-readiness.mjs';
10
+
11
+ await loadConfig({ cache: false });
12
+ const configured = resolveConfiguredApplicationRuntime();
13
+ if (configured.profile === 'local') {
14
+ throw new Error('Local jobs run inline or embedded; a separate worker requires self-hosted or cloud.');
15
+ }
16
+ if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is required.');
17
+
18
+ const runtime = await initializeDeployedApplicationRuntime({
19
+ profile: configured.profile,
20
+ providers: {
21
+ database: configured.providers.database,
22
+ authentication: configured.providers.authentication,
23
+ tenancy: configured.providers.tenancy,
24
+ assets: configured.providers.assets,
25
+ secrets: configured.providers.secrets,
26
+ jobs: configured.providers.jobs,
27
+ network: configured.providers.network,
28
+ },
29
+ database: {
30
+ engine: 'postgres',
31
+ connect: () =>
32
+ getDatabase({ type: 'postgres', url: process.env.DATABASE_URL }),
33
+ close: async (db) => db.close?.(),
34
+ },
35
+ authentication: {
36
+ provider: configured.providers.authentication.provider,
37
+ readiness: createProviderReadinessProbe('authentication', {
38
+ profile: configured.profile,
39
+ provider: configured.providers.authentication.provider,
40
+ }),
41
+ },
42
+ assets: {
43
+ provider: configured.providers.assets.provider,
44
+ readiness: createProviderReadinessProbe('assets', {
45
+ profile: configured.profile,
46
+ provider: configured.providers.assets.provider,
47
+ }),
48
+ },
49
+ secrets: {
50
+ provider: configured.providers.secrets.provider,
51
+ readiness: createProviderReadinessProbe('secrets', {
52
+ profile: configured.profile,
53
+ provider: configured.providers.secrets.provider,
54
+ }),
55
+ },
56
+ });
57
+
58
+ const kind = process.argv[2] || 'task';
59
+ const runner =
60
+ kind === 'schedule'
61
+ ? await runtime.createScheduleWorker()
62
+ : await runtime.createTaskWorker({
63
+ concurrency: Number.parseInt(
64
+ process.env.SMRT_WORKER_CONCURRENCY || '4',
65
+ 10,
66
+ ),
67
+ });
68
+
69
+ await runner.start();
70
+ console.log(
71
+ JSON.stringify({
72
+ schemaVersion: 1,
73
+ status: 'ready',
74
+ kind,
75
+ secretValuesIncluded: false,
76
+ }),
77
+ );
78
+
79
+ let stopping = false;
80
+ async function stop() {
81
+ if (stopping) return;
82
+ stopping = true;
83
+ await runtime.close();
84
+ }
85
+ process.once('SIGINT', () => void stop());
86
+ process.once('SIGTERM', () => void stop());
@@ -0,0 +1,199 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import {
3
+ closeSync,
4
+ fstatSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ /** @param {number} pid */
15
+ function processExists(pid) {
16
+ try {
17
+ process.kill(pid, 0);
18
+ return true;
19
+ } catch (error) {
20
+ const code =
21
+ error && typeof error === 'object' && 'code' in error
22
+ ? error.code
23
+ : undefined;
24
+ return code === 'EPERM' || code === 'EACCES';
25
+ }
26
+ }
27
+
28
+ /** @param {{ schemaVersion: number, pid: number, instance: string }} record */
29
+ function validateRecord(record) {
30
+ if (
31
+ record.schemaVersion !== 1 ||
32
+ !Number.isSafeInteger(record.pid) ||
33
+ record.pid < 1 ||
34
+ typeof record.instance !== 'string' ||
35
+ !/^[a-f0-9]{32}$/.test(record.instance)
36
+ ) {
37
+ throw new Error('The application writer lease is malformed.');
38
+ }
39
+ return record;
40
+ }
41
+
42
+ /** @param {string | number} pathOrDescriptor */
43
+ function readRecord(pathOrDescriptor) {
44
+ return validateRecord(
45
+ JSON.parse(readFileSync(pathOrDescriptor, 'utf8')),
46
+ );
47
+ }
48
+
49
+ /** @param {string} stateRoot */
50
+ export function readActiveWriterLease(stateRoot) {
51
+ const path = join(stateRoot, 'writer.lease');
52
+ for (let attempt = 0; attempt < 4; attempt += 1) {
53
+ let descriptor;
54
+ try {
55
+ descriptor = openSync(path, 'r');
56
+ const identity = fstatSync(descriptor);
57
+ const record = readRecord(descriptor);
58
+ closeSync(descriptor);
59
+ descriptor = undefined;
60
+ if (processExists(record.pid)) return record;
61
+ const current = lstatSync(path);
62
+ if (current.dev !== identity.dev || current.ino !== identity.ino) {
63
+ continue;
64
+ }
65
+ rmSync(path);
66
+ return null;
67
+ } catch (error) {
68
+ if (descriptor !== undefined) closeSync(descriptor);
69
+ if (
70
+ error &&
71
+ typeof error === 'object' &&
72
+ 'code' in error &&
73
+ error.code === 'ENOENT'
74
+ ) {
75
+ return null;
76
+ }
77
+ throw new Error(
78
+ 'The application writer lease cannot be verified; inspect the private state directory.',
79
+ );
80
+ }
81
+ }
82
+ throw new Error(
83
+ 'The application writer lease changed repeatedly and cannot be verified.',
84
+ );
85
+ }
86
+
87
+ /**
88
+ * @param {string} stateRoot
89
+ * @param {{ operationInstance?: string }} [options]
90
+ */
91
+ export function acquireWriterLease(stateRoot, options = {}) {
92
+ mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
93
+ const path = join(stateRoot, 'writer.lease');
94
+ const active = readActiveWriterLease(stateRoot);
95
+ if (active?.pid === process.pid) return { release() {} };
96
+ if (active) {
97
+ throw new Error(
98
+ `Another application writer is active (process ${active.pid}).`,
99
+ );
100
+ }
101
+ const instance = randomBytes(16).toString('hex');
102
+ let descriptor;
103
+ for (let attempt = 0; attempt < 2; attempt += 1) {
104
+ try {
105
+ descriptor = openSync(path, 'wx', 0o600);
106
+ writeFileSync(
107
+ descriptor,
108
+ `${JSON.stringify({ schemaVersion: 1, pid: process.pid, instance })}\n`,
109
+ );
110
+ break;
111
+ } catch (error) {
112
+ if (descriptor !== undefined) {
113
+ closeSync(descriptor);
114
+ descriptor = undefined;
115
+ }
116
+ if (
117
+ error &&
118
+ typeof error === 'object' &&
119
+ 'code' in error &&
120
+ error.code === 'EEXIST'
121
+ ) {
122
+ const owner = readActiveWriterLease(stateRoot);
123
+ if (!owner && attempt === 0) continue;
124
+ throw new Error(
125
+ `Another application writer is active${owner ? ` (process ${owner.pid})` : ''}.`,
126
+ );
127
+ }
128
+ rmSync(path, { force: true });
129
+ throw error;
130
+ }
131
+ }
132
+ if (descriptor === undefined) {
133
+ throw new Error('The application writer lease could not be acquired.');
134
+ }
135
+ closeSync(descriptor);
136
+ let released = false;
137
+ const release = () => {
138
+ if (released) return;
139
+ released = true;
140
+ try {
141
+ const current = readRecord(path);
142
+ if (current.pid === process.pid && current.instance === instance) {
143
+ rmSync(path, { force: true });
144
+ }
145
+ } catch {
146
+ // A missing or externally repaired lease is not ours to remove.
147
+ }
148
+ };
149
+ let operationDescriptor;
150
+ try {
151
+ const operationPath = join(stateRoot, 'operation.lock');
152
+ operationDescriptor = openSync(operationPath, 'r');
153
+ const operationIdentity = fstatSync(operationDescriptor);
154
+ const operation = JSON.parse(readFileSync(operationDescriptor, 'utf8'));
155
+ closeSync(operationDescriptor);
156
+ operationDescriptor = undefined;
157
+ if (
158
+ !Number.isSafeInteger(operation.pid) ||
159
+ operation.pid < 1 ||
160
+ typeof operation.instance !== 'string'
161
+ ) {
162
+ throw new Error('The application operation lock cannot be verified.');
163
+ }
164
+ if (!processExists(operation.pid)) {
165
+ const currentIdentity = lstatSync(operationPath);
166
+ if (
167
+ currentIdentity.dev !== operationIdentity.dev ||
168
+ currentIdentity.ino !== operationIdentity.ino
169
+ ) {
170
+ throw new Error('The application operation lock changed unexpectedly.');
171
+ }
172
+ rmSync(operationPath);
173
+ } else if (operation.instance !== options.operationInstance) {
174
+ release();
175
+ throw new Error(
176
+ 'An application operation is active; wait for it to finish before starting a writer.',
177
+ );
178
+ }
179
+ } catch (error) {
180
+ if (operationDescriptor !== undefined) closeSync(operationDescriptor);
181
+ if (
182
+ !(
183
+ error &&
184
+ typeof error === 'object' &&
185
+ 'code' in error &&
186
+ error.code === 'ENOENT'
187
+ )
188
+ ) {
189
+ if (error instanceof SyntaxError) {
190
+ release();
191
+ throw new Error('The application operation lock cannot be verified.');
192
+ }
193
+ release();
194
+ throw error;
195
+ }
196
+ }
197
+ process.once('exit', release);
198
+ return { release };
199
+ }
@@ -5,7 +5,33 @@
5
5
  * See: https://github.com/happyvertical/smrt
6
6
  */
7
7
 
8
+ import { resolveLocalRuntimePaths } from '@happyvertical/smrt-app-runtime';
9
+ import { resolveApplicationId } from './scripts/smrt-runtime-identity.mjs';
10
+
11
+ const profile = (process.env.SMRT_RUNTIME_PROFILE || 'local') as
12
+ | 'local'
13
+ | 'self-hosted'
14
+ | 'cloud';
15
+ const appId = resolveApplicationId({
16
+ sourceRoot: process.cwd(),
17
+ explicitId: process.env.SMRT_APP_ID,
18
+ });
19
+ const localDatabase =
20
+ profile === 'local'
21
+ ? resolveLocalRuntimePaths({
22
+ appId,
23
+ dataDirectory: process.env.SMRT_DATA_DIR,
24
+ sourceRoot: process.cwd(),
25
+ }).database
26
+ : undefined;
27
+
8
28
  export default {
29
+ // This is the single infrastructure selector for the application. Domain
30
+ // objects and generated REST/CLI/MCP/WebMCP surfaces do not branch on it.
31
+ runtime: {
32
+ profile,
33
+ },
34
+
9
35
  // Agent/developer knowledge artifacts are generated by smrtPlugin().
10
36
  // HTTP API routes stay off unless explicitly enabled for a dev/admin-only workflow.
11
37
  knowledge: {
@@ -24,8 +50,14 @@ export default {
24
50
  // CLI configuration
25
51
  cli: {
26
52
  database: {
27
- type: 'sqlite',
28
- url: process.env.DATABASE_URL || './app.db',
53
+ type:
54
+ profile === 'local' ? 'sqlite' : 'postgres',
55
+ // `smrt-app` sets DATABASE_URL to the profile-owned data path before
56
+ // migrations. Deployed profiles require an operator-owned URL.
57
+ url:
58
+ profile === 'local'
59
+ ? process.env.DATABASE_URL || localDatabase
60
+ : process.env.DATABASE_URL,
29
61
  },
30
62
  verbose: false,
31
63
  },
@@ -14,14 +14,17 @@
14
14
 
15
15
  import { getCurrentTenant, enableTenancy } from '@happyvertical/smrt-tenancy';
16
16
  import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';
17
- import type { Handle } from '@sveltejs/kit';
17
+ import type { Handle, ServerInit } from '@sveltejs/kit';
18
18
  import { sequence } from '@sveltejs/kit/hooks';
19
19
 
20
20
  import { getSmrtConfig } from '$lib/server/smrt';
21
+ import { ensureApplicationRuntimeReady } from '$lib/server/application-runtime';
21
22
  import { resolveTenant } from '$lib/server/tenancy';
22
23
 
23
24
  enableTenancy();
24
25
 
26
+ export const init: ServerInit = ensureApplicationRuntimeReady;
27
+
25
28
  const tenantSelectionHandle: Handle = async ({ event, resolve }) => {
26
29
  const selection = await resolveTenant(event);
27
30
  event.locals.selectedTenantId = selection.tenantId;
@@ -0,0 +1,11 @@
1
+ <script lang="ts">
2
+ import { registerRuntimeDiagnosticsWebMcp } from './runtime-diagnostics-webmcp.js';
3
+
4
+ let { enabled = true }: { enabled?: boolean } = $props();
5
+
6
+ $effect(() => {
7
+ if (!enabled) return;
8
+ const owner = registerRuntimeDiagnosticsWebMcp();
9
+ return () => owner?.dispose();
10
+ });
11
+ </script>