@happyvertical/smrt-cli 0.53.2 → 0.53.4

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 (65) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +40 -0
  3. package/agents/app-commands.md +93 -0
  4. package/bin/smrt-web.mjs +19 -0
  5. package/bin/smrt.js +18 -12
  6. package/dist/{ApiKey-BZmbOTnL-nvcrCRDQ.js → ApiKey-BZmbOTnL-DPPF6Swn.js} +2 -2
  7. package/dist/ApiKeyCollection-Bfb_nw81-B1-2YKj8.js +2 -0
  8. package/dist/AuditLogCollection-B1YonCAP-DP4PCCw-.js +2 -0
  9. package/dist/{NostrIdentity-n70E0vck-BYlxfKXV.js → NostrIdentity-n70E0vck-D60BSg6H.js} +3 -3
  10. package/dist/NostrIdentityCollection-Ca74dkaL-C-v57o9N.js +2 -0
  11. package/dist/{OidcIdentity-Cb1bxreM-DDXZ2ULx.js → OidcIdentity-Cb1bxreM-B2eV2ahq.js} +2 -2
  12. package/dist/OidcIdentityCollection-TV8VdK69-BodvOtPJ.js +2 -0
  13. package/dist/ProfileAssetCollection-B9hacUJa-B7PLe0ld.js +2 -0
  14. package/dist/{ProfileCollection-DuJT7Mum-CEPfbh20.js → ProfileCollection-DuJT7Mum-B3Uhqphs.js} +26 -26
  15. package/dist/ProfileLinkCollection-4uNURz32-CLY1cBnn.js +2 -0
  16. package/dist/ProfileMetadataCollection-DQhbfEfa-Bzv-E4ep.js +2 -0
  17. package/dist/{ProfileMetafieldCollection-CoBuM_5G-DiMAQiDg.js → ProfileMetafieldCollection-CoBuM_5G-D-VqQuY1.js} +1 -1
  18. package/dist/{ProfileRelationshipCollection-DUVgaIqf-CUh6lQdy.js → ProfileRelationshipCollection-DUVgaIqf-gEkDBmkh.js} +1 -1
  19. package/dist/{ProfileRelationshipTermCollection-DtA9OM23-Sn18A9Vz.js → ProfileRelationshipTermCollection-DtA9OM23-DI_awdol.js} +1 -1
  20. package/dist/{ProfileRelationshipType-BRiRka6t-C0Sh1CTk.js → ProfileRelationshipType-BRiRka6t-Ci-qvFhm.js} +1 -1
  21. package/dist/{ProfileRelationshipTypeCollection-BdXwd7Kf-DEnQtZuK.js → ProfileRelationshipTypeCollection-BdXwd7Kf-CaQMLH9f.js} +1 -1
  22. package/dist/app/cli.d.ts +44 -0
  23. package/dist/app/cli.d.ts.map +1 -0
  24. package/dist/app/errors.d.ts +22 -0
  25. package/dist/app/errors.d.ts.map +1 -0
  26. package/dist/app/identity.d.ts +24 -0
  27. package/dist/app/identity.d.ts.map +1 -0
  28. package/dist/app/index.d.ts +19 -0
  29. package/dist/app/index.d.ts.map +1 -0
  30. package/dist/app/launchers.d.ts +40 -0
  31. package/dist/app/launchers.d.ts.map +1 -0
  32. package/dist/app/operations.d.ts +28 -0
  33. package/dist/app/operations.d.ts.map +1 -0
  34. package/dist/app/portability-assets.d.ts +151 -0
  35. package/dist/app/portability-assets.d.ts.map +1 -0
  36. package/dist/app/portability.d.ts +117 -0
  37. package/dist/app/portability.d.ts.map +1 -0
  38. package/dist/app/process-record.d.ts +54 -0
  39. package/dist/app/process-record.d.ts.map +1 -0
  40. package/dist/app/runtime.d.ts +83 -0
  41. package/dist/app/runtime.d.ts.map +1 -0
  42. package/dist/app.d.ts +1 -0
  43. package/dist/app.js +4 -0
  44. package/dist/cli-BRFsobey.js +1275 -0
  45. package/dist/cli-generator.d.ts.map +1 -1
  46. package/dist/commands/utilities.d.ts.map +1 -1
  47. package/dist/{commands-D6UUd6cJ.js → commands-dFz1e9A6.js} +17 -288
  48. package/dist/{dist-BwlARYH5.js → dist-Berwe80z.js} +4 -4
  49. package/dist/{dist-vvYmwpFv.js → dist-C2BqYVu7.js} +27 -27
  50. package/dist/{dist-BCAkEXaz.js → dist-D4BNILLN.js} +3 -3
  51. package/dist/{dist-BGsb09DN.js → dist-EJ63xUEp.js} +2 -2
  52. package/dist/index.js +14 -9
  53. package/dist/mcp-apps-packaging-CixRnxz_.js +286 -0
  54. package/dist/{oidcProvisioningPrimitives-D9OYu5-F-D5PxeL32.js → oidcProvisioningPrimitives-D9OYu5-F-B-kYsBTM.js} +4 -4
  55. package/dist/portability-DmQWSSXA.js +859 -0
  56. package/dist/resolveIdentity-B-PK6RwU-BmAU1Kse.js +2 -0
  57. package/package.json +14 -9
  58. package/dist/ApiKeyCollection-Bfb_nw81-CZVWaFPV.js +0 -2
  59. package/dist/AuditLogCollection-B1YonCAP-CnMHEquA.js +0 -2
  60. package/dist/NostrIdentityCollection-Ca74dkaL-C1Q9BNQc.js +0 -2
  61. package/dist/OidcIdentityCollection-TV8VdK69-RZ2ocmoN.js +0 -2
  62. package/dist/ProfileAssetCollection-B9hacUJa-C1qMe0_q.js +0 -2
  63. package/dist/ProfileLinkCollection-4uNURz32-GWzU0-kb.js +0 -2
  64. package/dist/ProfileMetadataCollection-DQhbfEfa-Dog-9nKM.js +0 -2
  65. package/dist/resolveIdentity-B-PK6RwU-CkUCBn4e.js +0 -2
@@ -0,0 +1,1275 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { acquireWriterLease, createProviderReadinessProbe, findInstalledPackage, prepareApplicationStateRoot, readActiveWriterLease, resolveApplicationId, resolveApplicationStateRoot, runtimeConfigurationFingerprint, withOperationLock } from "@happyvertical/smrt-app-runtime";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { platform } from "node:os";
8
+ import { randomBytes } from "node:crypto";
9
+ //#region src/app/errors.ts
10
+ /**
11
+ * Shared error helpers for the `smrt app` command group.
12
+ */
13
+ /** Read a Node `code` from an unknown thrown value. */
14
+ function errorCode(error) {
15
+ return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
16
+ }
17
+ /** Error carrying a process exit code from a failed child command. */
18
+ var AppCommandError = class extends Error {
19
+ exitCode;
20
+ constructor(message, exitCode = 1, options) {
21
+ super(message);
22
+ this.name = "AppCommandError";
23
+ this.exitCode = exitCode;
24
+ if (options && "cause" in options) Object.defineProperty(this, "cause", {
25
+ value: options.cause,
26
+ configurable: true
27
+ });
28
+ }
29
+ };
30
+ /** Minimum length before an environment value is treated as redactable. */
31
+ var MIN_REDACTED_VALUE_LENGTH = 8;
32
+ /** Environment names whose values must never reach operator output. */
33
+ var SECRET_ENVIRONMENT_NAME = /(?:^|_)(?:DATABASE_URL|URL|DSN|TOKEN|SECRET|PASSWORD|PASSWD|KEY|CREDENTIALS?|AUTH)(?:_|$)/i;
34
+ /**
35
+ * Remove secret material from an operator-facing message.
36
+ *
37
+ * Errors raised by drivers, config loaders, and providers routinely echo a
38
+ * connection string, credential, or bootstrap token. Redaction covers:
39
+ * literal values of secret-named environment variables, URL userinfo,
40
+ * `token=`/`password=`/`secret=` query values, and bearer tokens.
41
+ */
42
+ function redactSecrets(message, environment = process.env) {
43
+ let redacted = message;
44
+ const values = Object.entries(environment).filter(([name, value]) => typeof value === "string" && value.length >= MIN_REDACTED_VALUE_LENGTH && SECRET_ENVIRONMENT_NAME.test(name)).map(([, value]) => value).sort((left, right) => right.length - left.length);
45
+ for (const value of values) redacted = redacted.replaceAll(value, "[redacted]");
46
+ return redacted.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:'"]*(?::[^\s/@'"]*)?@/gi, "$1[redacted]@").replace(/([?&;](?:token|password|secret|key|access_token)=)[^&\s"'<>]+/gi, "$1[redacted]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g, "Bearer [redacted]");
47
+ }
48
+ //#endregion
49
+ //#region src/app/identity.ts
50
+ /**
51
+ * Application identity, private state root, and operator-artifact path
52
+ * custody for the `smrt app` command group.
53
+ *
54
+ * Identity, configuration fingerprint, and the private state root are
55
+ * `@happyvertical/smrt-app-runtime`'s single implementation (re-exported
56
+ * here), so the running web process and these commands can never derive
57
+ * different identities or lock domains. Operator-artifact path custody is
58
+ * CLI-only and stays here.
59
+ */
60
+ function isInside(parent, child) {
61
+ const relative = child.slice(parent.length);
62
+ return child === parent || child.startsWith(parent) && (relative.startsWith("/") || relative.startsWith("\\"));
63
+ }
64
+ /**
65
+ * Resolve an operator-selected artifact through its nearest existing ancestor
66
+ * and reject paths that could place application data in or over the checkout.
67
+ */
68
+ function assertExternalArtifactPath(options) {
69
+ const canonicalSource = realpathSync(resolve(options.sourceRoot));
70
+ const missingSegments = [];
71
+ let existingAncestor = resolve(options.path);
72
+ while (!existsSync(existingAncestor)) {
73
+ const parent = dirname(existingAncestor);
74
+ if (parent === existingAncestor) break;
75
+ missingSegments.unshift(basename(existingAncestor));
76
+ existingAncestor = parent;
77
+ }
78
+ const canonicalPath = resolve(realpathSync(existingAncestor), ...missingSegments);
79
+ if (isInside(canonicalSource, canonicalPath) || isInside(canonicalPath, canonicalSource)) throw new Error(`${options.label || "Artifact"} must remain outside the source tree.`);
80
+ return canonicalPath;
81
+ }
82
+ //#endregion
83
+ //#region src/app/runtime.ts
84
+ /**
85
+ * Shared per-invocation context for the `smrt app` command group: source
86
+ * root, application identity, I/O, child-process runners, and the
87
+ * profile-aware runtime environment.
88
+ */
89
+ /** Locate this package's root (`…/smrt-cli`) from the running module. */
90
+ function cliPackageRoot() {
91
+ let directory = dirname(fileURLToPath(import.meta.url));
92
+ for (let depth = 0; depth < 5; depth += 1) {
93
+ const candidate = join(directory, "package.json");
94
+ if (existsSync(candidate)) try {
95
+ if (JSON.parse(readFileSync(candidate, "utf8")).name === "@happyvertical/smrt-cli") return directory;
96
+ } catch {}
97
+ const parent = dirname(directory);
98
+ if (parent === directory) break;
99
+ directory = parent;
100
+ }
101
+ throw new Error("The smrt CLI package root could not be located.");
102
+ }
103
+ /** Absolute path of the `smrt` executable shipped with this package. */
104
+ function cliExecutablePath() {
105
+ return join(cliPackageRoot(), "bin", "smrt.js");
106
+ }
107
+ /** Absolute path of the web launcher `start` spawns. */
108
+ function webLauncherPath() {
109
+ return join(cliPackageRoot(), "bin", "smrt-web.mjs");
110
+ }
111
+ /** Load `<sourceRoot>/.env` without overriding the shell; a missing file is fine. */
112
+ function loadSourceEnvironment(sourceRoot) {
113
+ try {
114
+ process.loadEnvFile(join(sourceRoot, ".env"));
115
+ } catch (error) {
116
+ if (errorCode(error) !== "ENOENT") throw error;
117
+ }
118
+ }
119
+ function runChild(sourceRoot, binary, args, options = {}) {
120
+ const result = spawnSync(binary, args, {
121
+ cwd: sourceRoot,
122
+ env: options.env || process.env,
123
+ encoding: "utf8",
124
+ shell: options.shell || false,
125
+ stdio: options.capture ? [
126
+ "ignore",
127
+ "pipe",
128
+ "pipe"
129
+ ] : "inherit"
130
+ });
131
+ if (!options.allowFailure && (result.error || result.status !== 0)) throw new AppCommandError(`${options.label || binary} failed with exit code ${result.status ?? 1}.`, result.status ?? 1, { cause: result.error });
132
+ return result;
133
+ }
134
+ /** Default package-manager runner: honours a pnpm `npm_execpath`. */
135
+ function createPackageManagerRunner(sourceRoot) {
136
+ return (args, options = {}) => {
137
+ const npmExecPath = process.env.npm_execpath;
138
+ const pnpmExecPath = npmExecPath && basename(npmExecPath).toLowerCase().startsWith("pnpm") ? npmExecPath : null;
139
+ return pnpmExecPath ? runChild(sourceRoot, process.execPath, [pnpmExecPath, ...args], options) : runChild(sourceRoot, platform() === "win32" ? "pnpm.cmd" : "pnpm", args, {
140
+ ...options,
141
+ shell: platform() === "win32"
142
+ });
143
+ };
144
+ }
145
+ /** Default self-runner: this package's own `smrt` executable. */
146
+ function createSmrtRunner(sourceRoot) {
147
+ return (args, options = {}) => runChild(sourceRoot, process.execPath, [cliExecutablePath(), ...args], options);
148
+ }
149
+ /** Default browser opener (`SMRT_OPEN_STUB` writes the URL to a file instead). */
150
+ function createBrowserOpener(sourceRoot) {
151
+ return (url) => {
152
+ if (process.env.SMRT_OPEN_STUB) {
153
+ writeFileSync(resolve(process.env.SMRT_OPEN_STUB), `${url}\n`);
154
+ return;
155
+ }
156
+ const [binary, args] = platform() === "darwin" ? ["open", [url]] : platform() === "win32" ? ["cmd", [
157
+ "/c",
158
+ "start",
159
+ "",
160
+ url
161
+ ]] : ["xdg-open", [url]];
162
+ runChild(sourceRoot, binary, args);
163
+ };
164
+ }
165
+ /** Default runtime resolver: reload `smrt.config` without the cache. */
166
+ async function resolveConfiguredRuntime(sourceRoot) {
167
+ const { loadConfig, resolveConfiguredApplicationRuntime } = await import("@happyvertical/smrt-config");
168
+ await loadConfig({
169
+ cache: false,
170
+ searchFrom: sourceRoot
171
+ });
172
+ return resolveConfiguredApplicationRuntime();
173
+ }
174
+ /** Build the default dependency set for `sourceRoot`. */
175
+ async function defaultDependencies(sourceRoot) {
176
+ return {
177
+ runtime: await import("@happyvertical/smrt-app-runtime"),
178
+ resolveRuntime: resolveConfiguredRuntime,
179
+ runPackageManager: createPackageManagerRunner(sourceRoot),
180
+ runSmrt: createSmrtRunner(sourceRoot),
181
+ openBrowser: createBrowserOpener(sourceRoot),
182
+ fetch: globalThis.fetch.bind(globalThis),
183
+ sleep: (milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)),
184
+ signal: (pid, signal) => {
185
+ process.kill(pid, signal);
186
+ }
187
+ };
188
+ }
189
+ /** Resolve the application id from `<sourceRoot>/package.json` and `SMRT_APP_ID`. */
190
+ function resolveContextApplicationId(sourceRoot) {
191
+ return resolveApplicationId({
192
+ sourceRoot,
193
+ packageName: JSON.parse(readFileSync(join(sourceRoot, "package.json"), "utf8")).name,
194
+ explicitId: process.env.SMRT_APP_ID
195
+ });
196
+ }
197
+ /** Create or verify the private state root for this invocation. */
198
+ function preparedStateRoot(context) {
199
+ return prepareApplicationStateRoot({
200
+ appId: context.appId,
201
+ dataDirectory: process.env.SMRT_DATA_DIR,
202
+ sourceRoot: context.sourceRoot
203
+ });
204
+ }
205
+ /** Compute the environment a web/migration child of this app runs with. */
206
+ function runtimeEnvironment(context, runtime) {
207
+ const env = {
208
+ ...process.env,
209
+ SMRT_APP_ID: context.appId,
210
+ SMRT_RUNTIME_PROFILE: runtime.profile,
211
+ HOST: runtime.profile === "local" ? "127.0.0.1" : process.env.HOST || "0.0.0.0",
212
+ PORT: process.env.PORT || "5173"
213
+ };
214
+ if (runtime.profile === "local") {
215
+ const paths = context.deps.runtime.resolveLocalRuntimePaths({
216
+ appId: context.appId,
217
+ dataDirectory: process.env.SMRT_DATA_DIR,
218
+ sourceRoot: context.sourceRoot
219
+ });
220
+ env.DATABASE_TYPE = "sqlite";
221
+ env.DATABASE_URL = paths.database;
222
+ env.SMRT_ASSETS_DIR = paths.assets;
223
+ return {
224
+ env,
225
+ paths,
226
+ assetRoot: paths.assets
227
+ };
228
+ }
229
+ if (!process.env.DATABASE_URL) throw new Error(`${runtime.profile} requires DATABASE_URL; copy the matching env example and configure providers.`);
230
+ env.DATABASE_TYPE = "postgres";
231
+ let assetRoot = null;
232
+ if (runtime.providers.assets.provider === "local-files") {
233
+ if (!process.env.SMRT_ASSETS_DIR || !isAbsolute(process.env.SMRT_ASSETS_DIR)) throw new Error("Filesystem-backed deployed profiles require an absolute SMRT_ASSETS_DIR outside the source tree.");
234
+ assetRoot = assertExternalArtifactPath({
235
+ sourceRoot: context.sourceRoot,
236
+ path: process.env.SMRT_ASSETS_DIR,
237
+ label: "Asset storage root"
238
+ });
239
+ env.SMRT_ASSETS_DIR = assetRoot;
240
+ }
241
+ return {
242
+ env,
243
+ paths: null,
244
+ assetRoot
245
+ };
246
+ }
247
+ //#endregion
248
+ //#region src/app/launchers.ts
249
+ /**
250
+ * Process wrappers: explicit schema preparation (`migrate`), deployed job
251
+ * workers (`worker`), and the Vite launcher (`dev`, `build`, `vite`).
252
+ *
253
+ * Ported from the template's `scripts/smrt-prepare-migration.mjs`,
254
+ * `scripts/smrt-worker.mjs`, `scripts/smrt-vite.mjs`, and
255
+ * `scripts/smrt-mcp-apps.mjs`.
256
+ */
257
+ /**
258
+ * Prepare the schema explicitly: under the operation lock, refuse while a
259
+ * local writer is live, establish local storage custody without creating
260
+ * schema or bootstrap records, then run `smrt db:migrate`.
261
+ */
262
+ async function prepareMigration(context) {
263
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
264
+ const stateRoot = preparedStateRoot(context);
265
+ await withOperationLock(stateRoot, "db:migrate", async () => {
266
+ const env = {
267
+ ...process.env,
268
+ SMRT_APP_ID: context.appId
269
+ };
270
+ if (runtime.profile === "local") {
271
+ if (readActiveWriterLease(stateRoot)) throw new Error("Stop the local application before preparing its schema.");
272
+ const paths = context.deps.runtime.resolveLocalRuntimePaths({
273
+ appId: context.appId,
274
+ dataDirectory: process.env.SMRT_DATA_DIR,
275
+ sourceRoot: context.sourceRoot
276
+ });
277
+ await context.deps.runtime.prepareLocalDatabaseStorage({
278
+ appId: context.appId,
279
+ dataDirectory: process.env.SMRT_DATA_DIR,
280
+ sourceRoot: context.sourceRoot
281
+ });
282
+ env.DATABASE_TYPE = "sqlite";
283
+ env.DATABASE_URL = paths.database;
284
+ env.SMRT_ASSETS_DIR = paths.assets;
285
+ }
286
+ const result = context.deps.runSmrt(["db:migrate"], {
287
+ env,
288
+ allowFailure: true
289
+ });
290
+ if (result.error || result.status !== 0) throw new AppCommandError("s-m-r-t database migration failed.", result.status ?? 1, { cause: result.error });
291
+ });
292
+ return 0;
293
+ }
294
+ var WORKER_USAGE = "Usage: smrt app worker [task|schedule]";
295
+ /** Parse `smrt app worker [kind]`, refusing anything but `task`/`schedule`. */
296
+ function parseWorkerKind(args) {
297
+ if (args.length > 1) throw new AppCommandError(WORKER_USAGE, 2);
298
+ const kind = args[0] ?? "task";
299
+ if (kind !== "task" && kind !== "schedule") throw new AppCommandError(WORKER_USAGE, 2);
300
+ return kind;
301
+ }
302
+ /**
303
+ * Start a deployed job worker. Resolves once the runner is started; the
304
+ * process stays alive until SIGINT/SIGTERM closes the runtime.
305
+ */
306
+ async function runWorker(context, args) {
307
+ const kind = parseWorkerKind(args);
308
+ const configured = await context.deps.resolveRuntime(context.sourceRoot);
309
+ if (configured.profile === "local") throw new Error("Local jobs run inline or embedded; a separate worker requires self-hosted or cloud.");
310
+ if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required.");
311
+ const registration = join(context.sourceRoot, ".smrt", "runtime", "register.js");
312
+ if (!existsSync(registration)) throw new Error("Missing .smrt/runtime/register.js: run pnpm build before starting a worker.");
313
+ await import(pathToFileURL(registration).href);
314
+ const probe = (component) => createProviderReadinessProbe(component, {
315
+ profile: configured.profile,
316
+ provider: configured.providers[component].provider
317
+ }, { sourceRoot: context.sourceRoot });
318
+ const runtime = await context.deps.runtime.initializeDeployedApplicationRuntime({
319
+ profile: configured.profile,
320
+ providers: {
321
+ database: configured.providers.database,
322
+ authentication: configured.providers.authentication,
323
+ tenancy: configured.providers.tenancy,
324
+ assets: configured.providers.assets,
325
+ secrets: configured.providers.secrets,
326
+ jobs: configured.providers.jobs,
327
+ network: configured.providers.network
328
+ },
329
+ database: {
330
+ engine: "postgres",
331
+ connect: async () => {
332
+ const { getDatabase } = await import("@happyvertical/sql");
333
+ return await getDatabase({
334
+ type: "postgres",
335
+ url: process.env.DATABASE_URL
336
+ });
337
+ },
338
+ close: async (db) => {
339
+ await db.close?.();
340
+ }
341
+ },
342
+ authentication: {
343
+ provider: configured.providers.authentication.provider,
344
+ readiness: probe("authentication")
345
+ },
346
+ assets: {
347
+ provider: configured.providers.assets.provider,
348
+ readiness: probe("assets")
349
+ },
350
+ secrets: {
351
+ provider: configured.providers.secrets.provider,
352
+ readiness: probe("secrets")
353
+ }
354
+ });
355
+ await (kind === "schedule" ? await runtime.createScheduleWorker() : await runtime.createTaskWorker({ concurrency: Number.parseInt(process.env.SMRT_WORKER_CONCURRENCY || "4", 10) })).start();
356
+ context.io.stdout(`${JSON.stringify({
357
+ schemaVersion: 1,
358
+ status: "ready",
359
+ kind,
360
+ secretValuesIncluded: false
361
+ })}\n`);
362
+ let stopping = false;
363
+ const stop = async () => {
364
+ if (stopping) return;
365
+ stopping = true;
366
+ await runtime.close();
367
+ };
368
+ process.once("SIGINT", () => void stop());
369
+ process.once("SIGTERM", () => void stop());
370
+ return 0;
371
+ }
372
+ /** Failure of the Vite launcher; rendered as `s-m-r-t Vite launcher: …`. */
373
+ var ViteLauncherError = class extends Error {};
374
+ /** Resolve the application's installed Vite CLI entry, refusing unsafe paths. */
375
+ function resolveViteEntry(sourceRoot) {
376
+ const installed = findInstalledPackage(sourceRoot, "vite");
377
+ if (!installed) throw new ViteLauncherError("Could not resolve the installed Vite package. Verify the generated app dependencies and Vite compatibility.");
378
+ const packagePath = join(realpathSync(installed), "package.json");
379
+ const packageRoot = dirname(packagePath);
380
+ let metadata;
381
+ try {
382
+ metadata = JSON.parse(readFileSync(packagePath, "utf8"));
383
+ } catch {
384
+ throw new ViteLauncherError("The installed Vite package metadata is invalid; reinstall dependencies.");
385
+ }
386
+ const declaredBin = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.vite;
387
+ if (typeof declaredBin !== "string" || declaredBin === "") throw new ViteLauncherError("The installed Vite package does not declare its CLI.");
388
+ const entry = resolve(packageRoot, declaredBin);
389
+ const relativeEntry = relative(packageRoot, entry);
390
+ if (relativeEntry === "" || relativeEntry.startsWith("..") || isAbsolute(relativeEntry)) throw new ViteLauncherError("The installed Vite package declares an unsafe CLI path.");
391
+ if (!existsSync(entry)) throw new ViteLauncherError("The installed Vite package CLI is missing; reinstall dependencies.");
392
+ return entry;
393
+ }
394
+ /**
395
+ * Run the application's Vite CLI in this process with `args` verbatim. The
396
+ * caller has already loaded `.env` without overriding the shell.
397
+ */
398
+ async function launchVite(context, args) {
399
+ let entry;
400
+ try {
401
+ entry = resolveViteEntry(context.sourceRoot);
402
+ } catch (error) {
403
+ if (error instanceof ViteLauncherError) {
404
+ context.io.stderr(`s-m-r-t Vite launcher: ${error.message}\n`);
405
+ return 1;
406
+ }
407
+ throw error;
408
+ }
409
+ process.argv = [
410
+ process.execPath,
411
+ entry,
412
+ ...args
413
+ ];
414
+ await import(pathToFileURL(entry).href);
415
+ return 0;
416
+ }
417
+ /**
418
+ * Validate opt-in portable MCP Apps metadata (`./mcp-apps`) when present.
419
+ * Returns the exit code; findings go to stderr.
420
+ */
421
+ async function validateMcpAppsIfPresent(context) {
422
+ const root = join(context.sourceRoot, "mcp-apps");
423
+ if (!existsSync(root)) return 0;
424
+ const { validateMcpAppsPackage } = await import("./mcp-apps-packaging-CixRnxz_.js");
425
+ const result = validateMcpAppsPackage(root);
426
+ for (const finding of result.findings) context.io.stderr(`${finding.code}: ${finding.message}\n`);
427
+ if (!result.valid) {
428
+ context.io.stderr(`Error: MCP Apps package validation failed with ${result.findings.length} finding(s)\n`);
429
+ return 1;
430
+ }
431
+ context.io.stdout("MCP Apps package at ./mcp-apps is valid.\n");
432
+ return 0;
433
+ }
434
+ //#endregion
435
+ //#region src/app/process-record.ts
436
+ /**
437
+ * Managed web-process record (`<state>/app.pid`).
438
+ *
439
+ * Ported from the template's `scripts/smrt-process.mjs`. A recorded pid is
440
+ * only trusted when the live process's command line proves it is the web
441
+ * launcher with this record's instance nonce, so a recycled pid is never
442
+ * signalled.
443
+ */
444
+ /** File name of the web launcher; part of the process-identity proof. */
445
+ var WEB_LAUNCHER_NAME = "smrt-web.mjs";
446
+ function processCommand(pid) {
447
+ if (platform() === "win32") {
448
+ const result = spawnSync("powershell.exe", [
449
+ "-NoProfile",
450
+ "-NonInteractive",
451
+ "-Command",
452
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`
453
+ ], {
454
+ encoding: "utf8",
455
+ windowsHide: true
456
+ });
457
+ return result.status === 0 ? result.stdout.trim() : null;
458
+ }
459
+ const result = spawnSync("ps", [
460
+ "-p",
461
+ String(pid),
462
+ "-o",
463
+ "command="
464
+ ], { encoding: "utf8" });
465
+ return result.status === 0 ? result.stdout.trim() : null;
466
+ }
467
+ /** Write the private (mode 0600) process record. */
468
+ function writeProcessRecord(path, record) {
469
+ writeFileSync(path, `${JSON.stringify(record)}\n`, { mode: 384 });
470
+ }
471
+ /**
472
+ * Send SIGTERM. Returns `false` when the process had already exited; any
473
+ * other failure (e.g. EPERM) is rethrown.
474
+ */
475
+ function sendTerminationSignal(pid, killProcess = process.kill) {
476
+ try {
477
+ killProcess(pid, "SIGTERM");
478
+ return true;
479
+ } catch (error) {
480
+ if (errorCode(error) === "ESRCH") return false;
481
+ throw error;
482
+ }
483
+ }
484
+ /** Split a process command line into argv-like tokens, dropping quotes. */
485
+ function commandTokens(command) {
486
+ return command.trim().split(/\s+/).map((token) => token.replace(/^"|"$/g, "")).filter(Boolean);
487
+ }
488
+ /**
489
+ * True when `command` is exactly the web launcher started with this record's
490
+ * nonce: the final argument is `--smrt-instance=<instance>` and the argument
491
+ * before it is a path whose basename is `smrt-web.mjs` (the CLI's
492
+ * `bin/smrt-web.mjs`, or the template's `scripts/smrt-web.mjs`). Substring
493
+ * matches such as `evil-smrt-web.mjs` or a nonce with a suffix are rejected.
494
+ */
495
+ function matchesApplicationProcess(record, command) {
496
+ if (typeof command !== "string") return false;
497
+ const tokens = commandTokens(command);
498
+ if (tokens.length < 2) return false;
499
+ const script = tokens[tokens.length - 2];
500
+ return tokens[tokens.length - 1] === `--smrt-instance=${record.instance}` && script.split(/[\\/]/).pop() === "smrt-web.mjs";
501
+ }
502
+ function isLive(pid) {
503
+ try {
504
+ process.kill(pid, 0);
505
+ return true;
506
+ } catch (error) {
507
+ const code = errorCode(error);
508
+ return code === "EPERM" || code === "EACCES";
509
+ }
510
+ }
511
+ /**
512
+ * Re-check `record` against the live process table. Only `gone` and
513
+ * `mismatched` justify dropping the record; `unverifiable` (the process
514
+ * query failed or returned nothing) must keep it and must not signal.
515
+ */
516
+ function checkOwnedProcess(record) {
517
+ if (!isLive(record.pid)) return "gone";
518
+ const command = processCommand(record.pid);
519
+ if (command === null || command === "") return isLive(record.pid) ? "unverifiable" : "gone";
520
+ return matchesApplicationProcess(record, command) ? "owned" : "mismatched";
521
+ }
522
+ /** True only when {@link checkOwnedProcess} proves the record still owned. */
523
+ function verifyOwnedProcess(record) {
524
+ return checkOwnedProcess(record) === "owned";
525
+ }
526
+ /**
527
+ * Return the recorded web process when it is live and provably ours;
528
+ * otherwise remove the stale record and return `null`.
529
+ */
530
+ function readOwnedProcess(path) {
531
+ let record;
532
+ try {
533
+ record = JSON.parse(readFileSync(path, "utf8"));
534
+ if (!Number.isSafeInteger(record.pid) || record.pid < 1 || typeof record.instance !== "string" || !/^[a-f0-9]{32}$/.test(record.instance)) throw new Error("Invalid process record.");
535
+ } catch {
536
+ rmSync(path, { force: true });
537
+ return null;
538
+ }
539
+ try {
540
+ process.kill(record.pid, 0);
541
+ } catch (error) {
542
+ const code = errorCode(error);
543
+ if (code !== "EPERM" && code !== "EACCES") {
544
+ rmSync(path, { force: true });
545
+ return null;
546
+ }
547
+ }
548
+ const command = processCommand(record.pid);
549
+ if (command === null || command === "") throw new Error(`Application process ${record.pid} is live but its identity cannot be verified.`);
550
+ if (!matchesApplicationProcess(record, command)) {
551
+ rmSync(path, { force: true });
552
+ return null;
553
+ }
554
+ return record;
555
+ }
556
+ //#endregion
557
+ //#region src/app/operations.ts
558
+ /**
559
+ * Local/deployed application operations: install, setup, recover, start,
560
+ * stop, doctor, open, backup, export, import.
561
+ *
562
+ * Ported from the template's `scripts/smrt-app.mjs`. Output JSON, exit
563
+ * codes, state files, and environment variables are unchanged so the
564
+ * template's `pnpm app:*` scripts can become `smrt app <operation>`.
565
+ */
566
+ /** Operations implemented by this module. */
567
+ var APP_OPERATIONS = [
568
+ "install",
569
+ "setup",
570
+ "recover",
571
+ "start",
572
+ "stop",
573
+ "doctor",
574
+ "open",
575
+ "backup",
576
+ "export",
577
+ "import"
578
+ ];
579
+ function printJson(context, value, pretty = false) {
580
+ context.io.stdout(`${pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)}\n`);
581
+ }
582
+ function ensurePrivateDirectory(path) {
583
+ mkdirSync(path, {
584
+ recursive: true,
585
+ mode: 448
586
+ });
587
+ }
588
+ function nearestExistingAncestor(path) {
589
+ let candidate = path;
590
+ while (!existsSync(candidate)) {
591
+ const parent = dirname(candidate);
592
+ if (parent === candidate) return candidate;
593
+ candidate = parent;
594
+ }
595
+ return candidate;
596
+ }
597
+ function pidPath(context) {
598
+ return join(preparedStateRoot(context), "app.pid");
599
+ }
600
+ function onboardingPath(context) {
601
+ return join(preparedStateRoot(context), "onboarding.json");
602
+ }
603
+ function onboardingLaunchPath(context) {
604
+ return join(preparedStateRoot(context), "onboarding-launch.html");
605
+ }
606
+ function removeOnboardingHandoff(context) {
607
+ rmSync(onboardingPath(context), { force: true });
608
+ rmSync(onboardingLaunchPath(context), { force: true });
609
+ }
610
+ function writePrivateAtomic(destination, contents) {
611
+ const temporary = `${destination}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
612
+ try {
613
+ writeFileSync(temporary, contents, {
614
+ flag: "wx",
615
+ mode: 384
616
+ });
617
+ renameSync(temporary, destination);
618
+ } finally {
619
+ rmSync(temporary, { force: true });
620
+ }
621
+ }
622
+ function saveOnboardingLaunch(context, url) {
623
+ const escapedUrl = url.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
624
+ writePrivateAtomic(onboardingLaunchPath(context), `<!doctype html><meta http-equiv="refresh" content="0;url=${escapedUrl}">\n`);
625
+ }
626
+ function saveOnboardingUrl(context, url) {
627
+ ensurePrivateDirectory(preparedStateRoot(context));
628
+ saveOnboardingLaunch(context, url);
629
+ writePrivateAtomic(onboardingPath(context), `${JSON.stringify({
630
+ schemaVersion: 1,
631
+ url
632
+ })}\n`);
633
+ }
634
+ var InvalidOnboardingHandoff = class extends Error {};
635
+ /** Read the retained loopback onboarding URL; tampered handoffs are removed. */
636
+ function readOnboardingUrl(context) {
637
+ try {
638
+ const value = JSON.parse(readFileSync(onboardingPath(context), "utf8"));
639
+ if (value?.schemaVersion !== 1 || typeof value.url !== "string") throw new InvalidOnboardingHandoff("Invalid onboarding handoff.");
640
+ const url = new URL(value.url);
641
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" && url.hostname !== "localhost" || url.pathname !== "/setup") throw new InvalidOnboardingHandoff("Invalid onboarding handoff.");
642
+ return url.toString();
643
+ } catch (error) {
644
+ if (errorCode(error) === "ENOENT" || error instanceof SyntaxError || error instanceof InvalidOnboardingHandoff || errorCode(error) === "ERR_INVALID_URL") {
645
+ removeOnboardingHandoff(context);
646
+ return null;
647
+ }
648
+ throw error;
649
+ }
650
+ }
651
+ /** Throw unless the configured profile is `local`. */
652
+ async function assertLocalOperation(context, operation) {
653
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
654
+ if (runtime.profile !== "local") throw new Error(`${operation} is local-profile only; run the production Node build or container for deployed profiles.`);
655
+ return runtime;
656
+ }
657
+ async function initializeLocal(context, runtime, env, options = {}) {
658
+ if (runtime.profile !== "local") return null;
659
+ return context.deps.runtime.initializeLocalApplicationRuntime({
660
+ appId: context.appId,
661
+ dataDirectory: process.env.SMRT_DATA_DIR,
662
+ sourceRoot: context.sourceRoot,
663
+ bindHost: env.HOST || "127.0.0.1",
664
+ providers: {
665
+ database: runtime.providers.database,
666
+ authentication: runtime.providers.authentication,
667
+ tenancy: runtime.providers.tenancy,
668
+ assets: runtime.providers.assets,
669
+ secrets: runtime.providers.secrets,
670
+ jobs: runtime.providers.jobs,
671
+ network: runtime.providers.network
672
+ },
673
+ prepareDatabase: options.prepareDatabase ? async () => {
674
+ context.deps.runSmrt(["db:migrate"], {
675
+ env,
676
+ label: "smrt db:migrate"
677
+ });
678
+ } : void 0,
679
+ backgroundJobs: process.env.SMRT_BACKGROUND_JOBS === "true"
680
+ });
681
+ }
682
+ async function setup(context, operationLock) {
683
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
684
+ const operatorLease = runtime.profile === "local" ? acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance }) : null;
685
+ try {
686
+ const { env } = runtimeEnvironment(context, runtime);
687
+ context.deps.runPackageManager(["build"], {
688
+ env,
689
+ label: "pnpm build"
690
+ });
691
+ let initialized = null;
692
+ if (runtime.profile === "local") initialized = await initializeLocal(context, runtime, env, { prepareDatabase: true });
693
+ else context.deps.runSmrt(["db:migrate"], {
694
+ env,
695
+ label: "smrt db:migrate"
696
+ });
697
+ const port = env.PORT || "5173";
698
+ const onboardingUrl = initialized?.bootstrap ? `http://127.0.0.1:${port}/setup?token=${encodeURIComponent(initialized.bootstrap.token)}` : null;
699
+ const diagnostics = await initialized?.runtime.diagnostics();
700
+ if (onboardingUrl) saveOnboardingUrl(context, onboardingUrl);
701
+ if (diagnostics?.bootstrap.status === "claimed") removeOnboardingHandoff(context);
702
+ await initialized?.runtime.db.close?.();
703
+ const retainedOnboardingUrl = onboardingUrl || readOnboardingUrl(context);
704
+ const onboardingAvailable = runtime.profile === "local" && retainedOnboardingUrl !== null;
705
+ const report = {
706
+ schemaVersion: 1,
707
+ status: "ready",
708
+ profile: runtime.profile,
709
+ onboardingAvailable,
710
+ onboardingRecovery: onboardingAvailable ? "pnpm app:open" : null,
711
+ secretValuesIncluded: false
712
+ };
713
+ printJson(context, report, true);
714
+ return {
715
+ ...report,
716
+ onboardingUrl: retainedOnboardingUrl
717
+ };
718
+ } finally {
719
+ operatorLease?.release();
720
+ }
721
+ }
722
+ async function recoverOnboarding(context, operationLock) {
723
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
724
+ const { env } = runtimeEnvironment(context, runtime);
725
+ if (runtime.profile !== "local") throw new Error("Owner onboarding recovery is local-only.");
726
+ const operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance });
727
+ try {
728
+ const initialized = await initializeLocal(context, runtime, env);
729
+ if (!initialized) throw new Error("Local runtime initialization failed.");
730
+ const invitation = await initialized.runtime.rotateBootstrapInvitation();
731
+ saveOnboardingUrl(context, `http://127.0.0.1:${env.PORT || "5173"}/setup?token=${encodeURIComponent(invitation.token)}`);
732
+ await initialized.runtime.db.close?.();
733
+ printJson(context, {
734
+ schemaVersion: 1,
735
+ status: "ready",
736
+ onboardingAvailable: true,
737
+ recovery: "Run pnpm app:start, then pnpm app:open.",
738
+ secretValuesIncluded: false
739
+ });
740
+ } finally {
741
+ operatorLease.release();
742
+ }
743
+ }
744
+ function isAlive(pid) {
745
+ process.kill(pid, 0);
746
+ return true;
747
+ }
748
+ async function waitForReady(context, url, pid, instance, configuration) {
749
+ const healthUrl = new URL("/api/_runtime/health", url);
750
+ for (let attempt = 0; attempt < 40; attempt += 1) {
751
+ try {
752
+ const response = await context.deps.fetch(healthUrl, { redirect: "manual" });
753
+ if (response.status === 200) {
754
+ const health = await response.json();
755
+ if (health?.status === "ready" && health.application === context.appId && health.instance === instance && health.configuration === configuration) {
756
+ isAlive(pid);
757
+ return;
758
+ }
759
+ }
760
+ isAlive(pid);
761
+ } catch {
762
+ try {
763
+ isAlive(pid);
764
+ } catch {
765
+ throw new Error("The application process exited before becoming ready.");
766
+ }
767
+ }
768
+ await context.deps.sleep(250);
769
+ }
770
+ throw new Error(`The application did not become ready at ${url}.`);
771
+ }
772
+ async function start(context, operationLock) {
773
+ const runtime = await assertLocalOperation(context, "app:start");
774
+ const { env } = runtimeEnvironment(context, runtime);
775
+ const url = `http://127.0.0.1:${env.PORT || "5173"}/`;
776
+ env.ORIGIN ||= url;
777
+ const configuration = runtimeConfigurationFingerprint(runtime, env);
778
+ const existing = readOwnedProcess(pidPath(context));
779
+ if (existing) {
780
+ await waitForReady(context, url, existing.pid, existing.instance, configuration);
781
+ printJson(context, {
782
+ schemaVersion: 1,
783
+ status: "running",
784
+ pid: existing.pid
785
+ });
786
+ return existing.pid;
787
+ }
788
+ ensurePrivateDirectory(preparedStateRoot(context));
789
+ const instance = randomBytes(16).toString("hex");
790
+ const child = spawn(process.execPath, [webLauncherPath(), `--smrt-instance=${instance}`], {
791
+ cwd: context.sourceRoot,
792
+ env: {
793
+ ...env,
794
+ HOST: runtime.profile === "local" ? "127.0.0.1" : env.HOST || "0.0.0.0",
795
+ PORT: env.PORT || "5173",
796
+ SMRT_PROCESS_INSTANCE: instance,
797
+ SMRT_OPERATION_INSTANCE: operationLock?.instance
798
+ },
799
+ detached: true,
800
+ stdio: "ignore"
801
+ });
802
+ child.unref();
803
+ const pid = child.pid;
804
+ if (pid === void 0) throw new Error("The application process could not be started.");
805
+ writeProcessRecord(pidPath(context), {
806
+ pid,
807
+ instance
808
+ });
809
+ try {
810
+ await waitForReady(context, url, pid, instance, configuration);
811
+ } catch (error) {
812
+ if (!await terminateFailedStart(context, child)) throw new Error(`${error instanceof Error ? error.message : "The application did not become ready."} Application process ${pid} did not exit after SIGTERM and SIGKILL and remains recorded; run pnpm app:stop.`);
813
+ rmSync(pidPath(context), { force: true });
814
+ throw error;
815
+ }
816
+ printJson(context, {
817
+ schemaVersion: 1,
818
+ status: "started",
819
+ pid
820
+ });
821
+ return pid;
822
+ }
823
+ /**
824
+ * Terminate a launcher that failed to prove readiness: SIGTERM, wait, then
825
+ * SIGKILL, wait. Resolves `true` only once the child has provably exited.
826
+ * Uses the child handle we own, so a reaped pid is never signalled.
827
+ */
828
+ async function terminateFailedStart(context, child) {
829
+ const exited = () => child.exitCode !== null || child.signalCode !== null;
830
+ for (const [signal, attempts] of [["SIGTERM", 50], ["SIGKILL", 20]]) {
831
+ if (exited()) return true;
832
+ try {
833
+ context.deps.signal(child.pid, signal);
834
+ } catch (error) {
835
+ if (errorCode(error) === "ESRCH") return true;
836
+ throw error;
837
+ }
838
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
839
+ await context.deps.sleep(100);
840
+ if (exited()) return true;
841
+ }
842
+ }
843
+ return exited();
844
+ }
845
+ async function stop(context) {
846
+ const record = readOwnedProcess(pidPath(context));
847
+ const pid = record?.pid || null;
848
+ if (!record || !pid) {
849
+ rmSync(pidPath(context), { force: true });
850
+ printJson(context, {
851
+ schemaVersion: 1,
852
+ status: "stopped"
853
+ });
854
+ return;
855
+ }
856
+ const state = checkOwnedProcess(record);
857
+ if (state === "unverifiable") throw new Error(`Application process ${pid} is live but its identity could not be re-verified; app.pid was kept and no signal was sent. Retry pnpm app:stop.`);
858
+ if (state !== "owned" || !sendTerminationSignal(pid)) {
859
+ rmSync(pidPath(context), { force: true });
860
+ printJson(context, {
861
+ schemaVersion: 1,
862
+ status: "stopped",
863
+ pid
864
+ });
865
+ return;
866
+ }
867
+ for (let attempt = 0; attempt < 40; attempt += 1) {
868
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
869
+ try {
870
+ process.kill(pid, 0);
871
+ } catch {
872
+ break;
873
+ }
874
+ if (attempt === 39) throw new Error(`Application process ${pid} did not stop cleanly.`);
875
+ }
876
+ rmSync(pidPath(context), { force: true });
877
+ printJson(context, {
878
+ schemaVersion: 1,
879
+ status: "stopped",
880
+ pid
881
+ });
882
+ }
883
+ async function open(context) {
884
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
885
+ const url = `http://${runtime.profile === "local" ? "127.0.0.1" : process.env.HOST || "127.0.0.1"}:${process.env.PORT || "5173"}/`;
886
+ const onboardingUrl = runtime.profile === "local" ? readOnboardingUrl(context) : null;
887
+ if (onboardingUrl) saveOnboardingLaunch(context, onboardingUrl);
888
+ const destination = onboardingUrl ? pathToFileURL(onboardingLaunchPath(context)).toString() : url;
889
+ context.deps.openBrowser(destination);
890
+ printJson(context, {
891
+ schemaVersion: 1,
892
+ status: "opened",
893
+ url
894
+ });
895
+ }
896
+ async function doctor(context) {
897
+ const findings = [];
898
+ let runtime = null;
899
+ let paths = null;
900
+ let statePath = null;
901
+ let canInspectMigrations = false;
902
+ try {
903
+ runtime = await context.deps.resolveRuntime(context.sourceRoot);
904
+ } catch {
905
+ findings.push({
906
+ code: "invalid-runtime-profile",
907
+ severity: "error",
908
+ message: "The canonical runtime profile is invalid.",
909
+ recovery: "Select local, self-hosted, or cloud in smrt.config.ts."
910
+ });
911
+ }
912
+ if (Number(process.versions.node.split(".")[0]) < 26) findings.push({
913
+ code: "unsupported-node",
914
+ severity: "error",
915
+ message: "Node.js 26 or newer is required.",
916
+ recovery: "Install the Node.js version declared in package.json engines."
917
+ });
918
+ if (runtime) {
919
+ if (runtime.profile !== "local") for (const [component, provider] of [
920
+ ["authentication", runtime.providers.authentication.provider],
921
+ ["assets", runtime.providers.assets.provider],
922
+ ["secrets", runtime.providers.secrets.provider]
923
+ ]) try {
924
+ await createProviderReadinessProbe(component, {
925
+ profile: runtime.profile,
926
+ provider
927
+ }, { sourceRoot: context.sourceRoot })();
928
+ } catch {
929
+ findings.push({
930
+ code: "provider-not-configured",
931
+ component,
932
+ severity: "error",
933
+ message: `The ${component} provider is not ready.`,
934
+ recovery: `Configure and verify the installed ${component} provider readiness module.`
935
+ });
936
+ }
937
+ try {
938
+ ({paths} = runtimeEnvironment(context, runtime));
939
+ statePath = resolveApplicationStateRoot({
940
+ appId: context.appId,
941
+ dataDirectory: process.env.SMRT_DATA_DIR,
942
+ sourceRoot: context.sourceRoot
943
+ });
944
+ accessSync(nearestExistingAncestor(statePath), constants.W_OK);
945
+ if (runtime.profile === "local") paths = await context.deps.runtime.validateLocalDatabaseStorage({
946
+ appId: context.appId,
947
+ dataDirectory: process.env.SMRT_DATA_DIR,
948
+ sourceRoot: context.sourceRoot
949
+ });
950
+ canInspectMigrations = true;
951
+ const host = process.env.HOST || (runtime.profile === "local" ? "127.0.0.1" : "0.0.0.0");
952
+ if (runtime.profile === "local" && host !== "127.0.0.1" && host !== "::1") findings.push({
953
+ code: "unsafe-local-bind",
954
+ severity: "error",
955
+ message: "Local owner bootstrap may only bind to a loopback address.",
956
+ recovery: "Unset HOST or set HOST=127.0.0.1."
957
+ });
958
+ if (paths?.root) accessSync(nearestExistingAncestor(paths.root), constants.W_OK);
959
+ } catch (error) {
960
+ findings.push({
961
+ code: "runtime-path-unavailable",
962
+ severity: "error",
963
+ message: "A required runtime path or provider configuration is unavailable.",
964
+ recovery: error instanceof Error ? error.message : "Configure a writable runtime data path."
965
+ });
966
+ }
967
+ if (canInspectMigrations) try {
968
+ const status = context.deps.runSmrt(["db:status", "--json"], {
969
+ env: runtimeEnvironment(context, runtime).env,
970
+ capture: true,
971
+ allowFailure: true,
972
+ label: "smrt db:status --json"
973
+ });
974
+ if (status.status !== 0) findings.push({
975
+ code: "migration-status-failed",
976
+ severity: "error",
977
+ message: "Database migration status could not be verified.",
978
+ recovery: "Run pnpm app:setup and inspect the private migration logs."
979
+ });
980
+ else {
981
+ const migrationStatus = JSON.parse(status.stdout || "{}");
982
+ if ((migrationStatus.drift?.length || 0) > 0 || (migrationStatus.migrations?.failed?.actionRequired || 0) > 0 || migrationStatus.schemaContract?.ok === false || migrationStatus.preconditions?.some((item) => item.status === "error")) findings.push({
983
+ code: "migration-required",
984
+ severity: "error",
985
+ message: "Database migrations are pending or failed.",
986
+ recovery: "Run pnpm app:setup, then rerun pnpm app:doctor."
987
+ });
988
+ }
989
+ } catch {
990
+ findings.push({
991
+ code: "migration-status-failed",
992
+ severity: "error",
993
+ message: "Database migration status could not be parsed.",
994
+ recovery: "Run pnpm app:setup and inspect the private migration logs."
995
+ });
996
+ }
997
+ }
998
+ const report = {
999
+ schemaVersion: 1,
1000
+ status: findings.some((finding) => finding.severity === "error") ? "error" : "ready",
1001
+ profile: runtime?.profile || null,
1002
+ capabilities: runtime?.capabilities || null,
1003
+ paths: paths || statePath ? {
1004
+ root: paths?.root || null,
1005
+ database: paths?.database || null,
1006
+ assets: paths?.assets || null,
1007
+ state: statePath
1008
+ } : null,
1009
+ findings,
1010
+ secretValuesIncluded: false
1011
+ };
1012
+ printJson(context, report, true);
1013
+ return report.status === "error" ? 1 : 0;
1014
+ }
1015
+ async function backup(context, args, operationLock) {
1016
+ if ((await context.deps.resolveRuntime(context.sourceRoot)).profile !== "local") throw new Error("This scaffold delegates deployed backups to the selected operator or managed provider.");
1017
+ const explicitDestination = args[0] ? assertExternalArtifactPath({
1018
+ sourceRoot: context.sourceRoot,
1019
+ path: resolve(args[0]),
1020
+ label: "Backup destination"
1021
+ }) : null;
1022
+ const paths = await context.deps.runtime.validateLocalDatabaseStorage({
1023
+ appId: context.appId,
1024
+ dataDirectory: process.env.SMRT_DATA_DIR,
1025
+ sourceRoot: context.sourceRoot
1026
+ });
1027
+ const operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance });
1028
+ try {
1029
+ const destination = assertExternalArtifactPath({
1030
+ sourceRoot: context.sourceRoot,
1031
+ path: explicitDestination || join(dirname(paths.root), "backups", `${context.appId}-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}`),
1032
+ label: "Backup destination"
1033
+ });
1034
+ ensurePrivateDirectory(dirname(destination));
1035
+ try {
1036
+ mkdirSync(destination, { mode: 448 });
1037
+ } catch (error) {
1038
+ if (errorCode(error) === "EEXIST") throw new Error(`Backup destination already exists: ${destination}`);
1039
+ throw error;
1040
+ }
1041
+ try {
1042
+ cpSync(paths.root, destination, {
1043
+ recursive: true,
1044
+ force: false,
1045
+ errorOnExist: true
1046
+ });
1047
+ } catch (error) {
1048
+ rmSync(destination, {
1049
+ recursive: true,
1050
+ force: true
1051
+ });
1052
+ throw error;
1053
+ }
1054
+ printJson(context, {
1055
+ schemaVersion: 1,
1056
+ status: "backed-up",
1057
+ destination
1058
+ });
1059
+ } finally {
1060
+ operatorLease.release();
1061
+ }
1062
+ }
1063
+ /**
1064
+ * The application's own adapter at `scripts/smrt-portability.mjs` keeps
1065
+ * working as the documented extension point; otherwise the built-in adapter
1066
+ * is used.
1067
+ */
1068
+ async function loadPortabilityAdapter(context) {
1069
+ const override = join(context.sourceRoot, "scripts", "smrt-portability.mjs");
1070
+ if (existsSync(override)) return await import(pathToFileURL(override).href);
1071
+ return import("./portability-DmQWSSXA.js").then((n) => n.s);
1072
+ }
1073
+ async function portability(context, operation, args, operationLock) {
1074
+ const adapter = await loadPortabilityAdapter(context);
1075
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
1076
+ const environment = runtimeEnvironment(context, runtime);
1077
+ const requestedPath = args[0] ? resolve(args[0]) : void 0;
1078
+ const artifactPath = requestedPath ? assertExternalArtifactPath({
1079
+ sourceRoot: context.sourceRoot,
1080
+ path: requestedPath,
1081
+ label: operation === "export" ? "Export destination" : "Import source"
1082
+ }) : requestedPath;
1083
+ if (runtime.profile === "local") environment.paths = await context.deps.runtime.validateLocalDatabaseStorage({
1084
+ appId: context.appId,
1085
+ dataDirectory: process.env.SMRT_DATA_DIR,
1086
+ sourceRoot: context.sourceRoot
1087
+ });
1088
+ const portabilityContext = {
1089
+ appId: context.appId,
1090
+ sourceRoot: context.sourceRoot,
1091
+ stateRoot: preparedStateRoot(context),
1092
+ runtime,
1093
+ env: environment.env,
1094
+ paths: environment.paths,
1095
+ assetRoot: environment.assetRoot
1096
+ };
1097
+ let operatorLease = null;
1098
+ if (operation === "import") {
1099
+ if (runtime.profile === "local") operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance });
1100
+ if (runtime.profile !== "local" && process.env.SMRT_MAINTENANCE_MODE !== "true") throw new Error("Stop deployed web/workers and set SMRT_MAINTENANCE_MODE=true before importing.");
1101
+ }
1102
+ try {
1103
+ const result = await adapter[operation === "export" ? "exportApplication" : "importApplication"]({
1104
+ ...portabilityContext,
1105
+ path: artifactPath
1106
+ });
1107
+ printJson(context, {
1108
+ schemaVersion: 1,
1109
+ status: `${operation}ed`,
1110
+ ...result
1111
+ }, true);
1112
+ } finally {
1113
+ operatorLease?.release();
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Run one application operation. Resolves to the process exit code; throws
1118
+ * on failure (the caller renders the error envelope).
1119
+ */
1120
+ async function runApplicationOperation(context, operation, args) {
1121
+ switch (operation) {
1122
+ case "install":
1123
+ await assertLocalOperation(context, "app:install");
1124
+ await withOperationLock(preparedStateRoot(context), "install", async (lock) => {
1125
+ const report = await setup(context, lock);
1126
+ await start(context, lock);
1127
+ const baseUrl = `http://127.0.0.1:${process.env.PORT || "5173"}/`;
1128
+ if (report.onboardingUrl) saveOnboardingLaunch(context, report.onboardingUrl);
1129
+ context.deps.openBrowser(report.onboardingUrl ? pathToFileURL(onboardingLaunchPath(context)).toString() : baseUrl);
1130
+ });
1131
+ return 0;
1132
+ case "setup":
1133
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => setup(context, lock));
1134
+ return 0;
1135
+ case "recover":
1136
+ await assertLocalOperation(context, "app:recover");
1137
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => recoverOnboarding(context, lock));
1138
+ return 0;
1139
+ case "start":
1140
+ await assertLocalOperation(context, "app:start");
1141
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => start(context, lock));
1142
+ return 0;
1143
+ case "doctor": return doctor(context);
1144
+ case "open":
1145
+ await open(context);
1146
+ return 0;
1147
+ case "stop":
1148
+ await assertLocalOperation(context, "app:stop");
1149
+ await withOperationLock(preparedStateRoot(context), operation, () => stop(context));
1150
+ return 0;
1151
+ case "backup":
1152
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => backup(context, args, lock));
1153
+ return 0;
1154
+ case "export":
1155
+ case "import":
1156
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => portability(context, operation, args, lock));
1157
+ return 0;
1158
+ }
1159
+ }
1160
+ //#endregion
1161
+ //#region src/app/cli.ts
1162
+ /**
1163
+ * `smrt app <operation>` dispatcher.
1164
+ *
1165
+ * Every operation writes machine-readable JSON to stdout and, on failure,
1166
+ * one secret-free JSON envelope to stderr with exit code 1 — the contract
1167
+ * the template's `scripts/smrt-app.mjs` established.
1168
+ */
1169
+ var cli_exports = /* @__PURE__ */ __exportAll({
1170
+ APP_COMMANDS: () => APP_COMMANDS,
1171
+ APP_RECOVERY: () => APP_RECOVERY,
1172
+ errorEnvelope: () => errorEnvelope,
1173
+ renderAppHelp: () => renderAppHelp,
1174
+ runAppCommand: () => runAppCommand
1175
+ });
1176
+ /** Recovery line carried by every error envelope. */
1177
+ var APP_RECOVERY = "Run pnpm app:doctor and follow its recovery instructions.";
1178
+ /** Every `smrt app` command with a one-line description (for help output). */
1179
+ var APP_COMMANDS = Object.freeze({
1180
+ install: "Local: set up, start, and open owner onboarding in one step",
1181
+ setup: "Build, apply migrations explicitly, and prepare owner onboarding",
1182
+ recover: "Local: issue a fresh single-use owner onboarding invitation",
1183
+ start: "Local: start the production build on loopback and prove readiness",
1184
+ stop: "Local: stop the managed application process",
1185
+ doctor: "Report runtime, storage, provider, and migration findings (default)",
1186
+ open: "Open the application or pending owner onboarding in a browser",
1187
+ "backup [destination]": "Local: copy the private data root to a new directory",
1188
+ "export [path]": "Write a logical, asset-aware export bundle",
1189
+ "import <path>": "Import a logical export bundle into an empty application",
1190
+ migrate: "Prepare storage custody and run smrt db:migrate under the lock",
1191
+ "worker [task|schedule]": "Deployed: run the task or schedule worker",
1192
+ "dev [...vite args]": "Run the application's Vite dev server",
1193
+ "build [...vite args]": "Validate ./mcp-apps when present, then run the Vite build",
1194
+ "vite [...args]": "Run the application's installed Vite CLI with args"
1195
+ });
1196
+ /** Build the stderr envelope for `error`, redacting secret material. */
1197
+ function errorEnvelope(error) {
1198
+ const runtimeCode = error instanceof Error && error.name === "LocalRuntimeError" && typeof error.code === "string" ? error.code : void 0;
1199
+ return {
1200
+ schemaVersion: 1,
1201
+ status: "error",
1202
+ code: "operation-failed",
1203
+ ...runtimeCode ? { runtimeCode } : {},
1204
+ message: redactSecrets(error instanceof Error ? error.message : "Application operation failed."),
1205
+ recovery: APP_RECOVERY,
1206
+ secretValuesIncluded: false
1207
+ };
1208
+ }
1209
+ /** Render `smrt app` usage. */
1210
+ function renderAppHelp() {
1211
+ const width = Math.max(...Object.keys(APP_COMMANDS).map((key) => key.length));
1212
+ return `Usage: smrt app <operation> [args]\n\nOperations:\n${Object.entries(APP_COMMANDS).map(([name, description]) => ` ${name.padEnd(width)} ${description}`).join("\n")}\n`;
1213
+ }
1214
+ var ENV_FILE_OPERATIONS = /* @__PURE__ */ new Set([
1215
+ ...APP_OPERATIONS,
1216
+ "migrate",
1217
+ "dev",
1218
+ "build",
1219
+ "vite"
1220
+ ]);
1221
+ /**
1222
+ * Run `smrt app <argv…>` and resolve to the process exit code.
1223
+ *
1224
+ * `argv` is everything after `app`. A leading `--` before operation
1225
+ * arguments is ignored, matching `pnpm app:import -- <path>`.
1226
+ */
1227
+ async function runAppCommand(argv, options = {}) {
1228
+ const io = {
1229
+ stdout: options.io?.stdout ?? ((text) => void process.stdout.write(text)),
1230
+ stderr: options.io?.stderr ?? ((text) => void process.stderr.write(text))
1231
+ };
1232
+ const operation = argv[0] || "doctor";
1233
+ const rawArgs = argv.slice(1);
1234
+ const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
1235
+ if (operation === "help" || operation === "--help" || operation === "-h") {
1236
+ io.stdout(renderAppHelp());
1237
+ return 0;
1238
+ }
1239
+ try {
1240
+ const sourceRoot = resolve(options.cwd ?? process.cwd());
1241
+ if (ENV_FILE_OPERATIONS.has(operation)) loadSourceEnvironment(sourceRoot);
1242
+ if (operation === "dev" || operation === "build" || operation === "vite") {
1243
+ const context = {
1244
+ sourceRoot,
1245
+ appId: "",
1246
+ io,
1247
+ deps: {}
1248
+ };
1249
+ if (operation === "build") {
1250
+ const status = await validateMcpAppsIfPresent(context);
1251
+ if (status !== 0) return status;
1252
+ }
1253
+ return launchVite(context, operation === "vite" ? args : [operation, ...args]);
1254
+ }
1255
+ const deps = {
1256
+ ...await defaultDependencies(sourceRoot),
1257
+ ...options.dependencies
1258
+ };
1259
+ const context = {
1260
+ sourceRoot,
1261
+ appId: resolveContextApplicationId(sourceRoot),
1262
+ io,
1263
+ deps
1264
+ };
1265
+ if (operation === "migrate") return await prepareMigration(context);
1266
+ if (operation === "worker") return await runWorker(context, args);
1267
+ if (!APP_OPERATIONS.includes(operation)) throw new Error(`Unknown app operation: ${operation}`);
1268
+ return await runApplicationOperation(context, operation, args);
1269
+ } catch (error) {
1270
+ io.stderr(`${JSON.stringify(errorEnvelope(error))}\n`);
1271
+ return 1;
1272
+ }
1273
+ }
1274
+ //#endregion
1275
+ export { AppCommandError as A, runtimeEnvironment as C, resolveApplicationId as D, prepareApplicationStateRoot as E, redactSecrets as M, resolveApplicationStateRoot as O, cliExecutablePath as S, assertExternalArtifactPath as T, launchVite as _, renderAppHelp as a, runWorker as b, runApplicationOperation as c, matchesApplicationProcess as d, readOwnedProcess as f, ViteLauncherError as g, writeProcessRecord as h, errorEnvelope as i, errorCode as j, runtimeConfigurationFingerprint as k, WEB_LAUNCHER_NAME as l, verifyOwnedProcess as m, APP_RECOVERY as n, runAppCommand as o, sendTerminationSignal as p, cli_exports as r, APP_OPERATIONS as s, APP_COMMANDS as t, checkOwnedProcess as u, prepareMigration as v, webLauncherPath as w, validateMcpAppsIfPresent as x, resolveViteEntry as y };