@happyvertical/smrt-cli 0.53.3 → 0.53.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +64 -0
  3. package/agents/app-commands.md +118 -0
  4. package/bin/smrt-web.mjs +19 -0
  5. package/bin/smrt.js +18 -12
  6. package/dist/{ApiKey-BZmbOTnL-DchyhIPA.js → ApiKey-BZmbOTnL-BbZupUZ3.js} +2 -2
  7. package/dist/ApiKeyCollection-Bfb_nw81-DYAZwx0O.js +87 -0
  8. package/dist/AuditLogCollection-B1YonCAP-Bkj2Ezon.js +193 -0
  9. package/dist/{NostrIdentity-n70E0vck-CLGPdJ1W.js → NostrIdentity-n70E0vck-Di3_eNCq.js} +4 -4
  10. package/dist/NostrIdentityCollection-Ca74dkaL-DvjOg9Xy.js +127 -0
  11. package/dist/{OidcIdentity-Cb1bxreM-DPBTZddF.js → OidcIdentity-Cb1bxreM-LXmC7uAf.js} +2 -2
  12. package/dist/OidcIdentityCollection-TV8VdK69-Cv1ZTeuk.js +86 -0
  13. package/dist/ProfileAssetCollection-B9hacUJa-CPuaWLk4.js +101 -0
  14. package/dist/{ProfileCollection-DuJT7Mum-BUYQQ9xZ.js → ProfileCollection-DuJT7Mum-nXbHrPpX.js} +31 -31
  15. package/dist/ProfileLinkCollection-4uNURz32-CbKZYmYm.js +233 -0
  16. package/dist/ProfileMetadataCollection-DQhbfEfa-_1rQCLTR.js +108 -0
  17. package/dist/ProfileMetafieldCollection-CoBuM_5G-CPjW3l5r.js +145 -0
  18. package/dist/ProfileRelationshipCollection-DUVgaIqf-RCDqYz6O.js +161 -0
  19. package/dist/ProfileRelationshipTermCollection-DtA9OM23-BoGhQBSQ.js +107 -0
  20. package/dist/{ProfileRelationshipType-BRiRka6t-xzRfZRFG.js → ProfileRelationshipType-BRiRka6t-Dezjv5jP.js} +1 -1
  21. package/dist/ProfileRelationshipTypeCollection-BdXwd7Kf-HoTmV1q-.js +57 -0
  22. package/dist/app/cli.d.ts +51 -0
  23. package/dist/app/cli.d.ts.map +1 -0
  24. package/dist/app/errors.d.ts +55 -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 +20 -0
  29. package/dist/app/index.d.ts.map +1 -0
  30. package/dist/app/launchers.d.ts +51 -0
  31. package/dist/app/launchers.d.ts.map +1 -0
  32. package/dist/app/operations.d.ts +30 -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 +98 -0
  41. package/dist/app/runtime.d.ts.map +1 -0
  42. package/dist/app/tokens.d.ts +21 -0
  43. package/dist/app/tokens.d.ts.map +1 -0
  44. package/dist/app.d.ts +1 -0
  45. package/dist/app.js +4 -0
  46. package/dist/cli-OZsH5cdL.js +1602 -0
  47. package/dist/cli-generator.d.ts.map +1 -1
  48. package/dist/commands/init.d.ts.map +1 -1
  49. package/dist/commands/utilities.d.ts.map +1 -1
  50. package/dist/{commands-B8N38deG.js → commands-UXK2F_0j.js} +49 -336
  51. package/dist/database-environment-D_Gk1Brf.js +20 -0
  52. package/dist/database-environment.d.ts +43 -0
  53. package/dist/database-environment.d.ts.map +1 -0
  54. package/dist/dist-BVjJWjT-.js +20 -0
  55. package/dist/{dist-B62O-xH6.js → dist-C87Ib9Vx.js} +2 -2
  56. package/dist/{dist-CL9KGm1j.js → dist-DLw_E34G.js} +1654 -1748
  57. package/dist/{dist-BBQyIRsO.js → dist-GIaWaUho.js} +4 -4
  58. package/dist/{dist-C8BOgli4.js → dist-RSE5hCY4.js} +3 -3
  59. package/dist/index.js +19 -9
  60. package/dist/mcp-apps-packaging-CixRnxz_.js +286 -0
  61. package/dist/{oidcProvisioningPrimitives-D9OYu5-F-l8afZs4F.js → oidcProvisioningPrimitives-D9OYu5-F-T9KkkDCd.js} +5 -5
  62. package/dist/portability-CES1YC2m.js +859 -0
  63. package/dist/resolveIdentity-B-PK6RwU-C9ogf8YG.js +269 -0
  64. package/package.json +19 -14
  65. package/dist/ApiKeyCollection-Bfb_nw81-DR5XF_SZ.js +0 -2
  66. package/dist/AuditLogCollection-B1YonCAP-PbzALncL.js +0 -2
  67. package/dist/NostrIdentityCollection-Ca74dkaL-C2r-uDlC.js +0 -2
  68. package/dist/OidcIdentityCollection-TV8VdK69-2fZROnih.js +0 -2
  69. package/dist/ProfileAssetCollection-B9hacUJa-D33sifgF.js +0 -2
  70. package/dist/ProfileLinkCollection-4uNURz32-CHDTinax.js +0 -2
  71. package/dist/ProfileMetadataCollection-DQhbfEfa-BQTogfVX.js +0 -2
  72. package/dist/ProfileMetafieldCollection-CoBuM_5G-BXtoIP02.js +0 -2
  73. package/dist/ProfileRelationshipCollection-DUVgaIqf-CNMliUF3.js +0 -2
  74. package/dist/ProfileRelationshipTermCollection-DtA9OM23-DPOMtMr7.js +0 -2
  75. package/dist/ProfileRelationshipTypeCollection-BdXwd7Kf-Co3PdH01.js +0 -2
  76. package/dist/resolveIdentity-B-PK6RwU-B306Ch1I.js +0 -2
@@ -0,0 +1,1602 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { accessSync, chmodSync, closeSync, constants, cpSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { acquireWriterLease, createProviderReadinessProbe, findInstalledPackage, openLocalMcpTokenStore, 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
+ /**
31
+ * A `smrt app start` whose web process never proved readiness. Carries the
32
+ * already-redacted, bounded tail of that process's output and the private
33
+ * log it came from, for the error envelope.
34
+ */
35
+ var ApplicationStartError = class extends Error {
36
+ output;
37
+ logFile;
38
+ constructor(message, output, logFile) {
39
+ super(message);
40
+ this.name = "ApplicationStartError";
41
+ this.output = output;
42
+ this.logFile = logFile;
43
+ }
44
+ };
45
+ /** Replacement marker for every redacted value. */
46
+ var REDACTED = "[redacted]";
47
+ /** Minimum length before an environment value is treated as redactable. */
48
+ var MIN_REDACTED_VALUE_LENGTH = 8;
49
+ /** Environment names whose values must never reach operator output. */
50
+ var SECRET_ENVIRONMENT_NAME = /(?:^|_)(?:DATABASE_URL|URL|DSN|TOKEN|SECRET|PASSWORD|PASSWD|KEY|CREDENTIALS?|AUTH)(?:_|$)/i;
51
+ /** URL userinfo (`scheme://user:pass@`); group 2 is the credential span. */
52
+ var URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@:'"]*(?::[^\s/@'"]*)?)@/dgi;
53
+ /** Query/parameter credential (`?token=…`); group 2 is the value span. */
54
+ var QUERY_CREDENTIAL = /([?&;](?:token|password|secret|key|access_token)=)([^&\s"'<>]+)/dgi;
55
+ /** Bearer token; group 2 is the token span (8+ characters unless strict). */
56
+ var BEARER = /\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})/dg;
57
+ var BEARER_ANY_LENGTH = /\b(Bearer\s+)([A-Za-z0-9._~+/=-]+)/dg;
58
+ function structuralSpans(text, strict) {
59
+ const spans = [];
60
+ for (const pattern of [
61
+ URL_USERINFO,
62
+ QUERY_CREDENTIAL,
63
+ strict ? BEARER_ANY_LENGTH : BEARER
64
+ ]) for (const match of text.matchAll(pattern)) {
65
+ const span = match.indices?.[2];
66
+ if (span && (span[1] > span[0] || pattern === URL_USERINFO)) spans.push([span[0], span[1]]);
67
+ }
68
+ return spans;
69
+ }
70
+ /** Every existing `[redacted]` marker in `text`: opaque to later passes. */
71
+ function markerSpans(text) {
72
+ return literalSpans(text, [REDACTED], []);
73
+ }
74
+ function literalSpans(text, values, opaque) {
75
+ const spans = [];
76
+ for (const value of values) for (let index = text.indexOf(value); index !== -1; index = text.indexOf(value, index + 1)) {
77
+ const end = index + value.length;
78
+ if (opaque.some(([start, stop]) => index >= start && end <= stop)) continue;
79
+ spans.push([index, end]);
80
+ }
81
+ return spans;
82
+ }
83
+ /** Replace the union of `spans` (overlapping or touching spans merge). */
84
+ function maskSpans(text, spans) {
85
+ if (spans.length === 0) return text;
86
+ spans.sort((left, right) => left[0] - right[0] || right[1] - left[1]);
87
+ let result = "";
88
+ let cursor = 0;
89
+ let [start, end] = spans[0];
90
+ const flush = () => {
91
+ result += text.slice(cursor, start) + REDACTED;
92
+ cursor = end;
93
+ };
94
+ for (const [nextStart, nextEnd] of spans.slice(1)) {
95
+ if (nextStart <= end) {
96
+ end = Math.max(end, nextEnd);
97
+ continue;
98
+ }
99
+ flush();
100
+ [start, end] = [nextStart, nextEnd];
101
+ }
102
+ flush();
103
+ return result + text.slice(cursor);
104
+ }
105
+ /**
106
+ * Remove secret material from an operator-facing message.
107
+ *
108
+ * Errors raised by drivers, config loaders, and providers routinely echo a
109
+ * connection string, credential, or bootstrap token. Redaction covers:
110
+ * literal values of secret-named environment variables, URL userinfo,
111
+ * `token=`/`password=`/`secret=` query values, and bearer tokens.
112
+ *
113
+ * Every span is located on the raw text and the union is masked at once, so
114
+ * a literal value can never split a structural match (an env value that is
115
+ * a Bearer-token prefix, or a URL's scheme and host, used to leave the rest
116
+ * of the token or the URL password visible). A final structural pass over
117
+ * the masked text keeps everything the earlier sequential order masked.
118
+ */
119
+ function redactSecrets(message, environment = process.env, options = {}) {
120
+ const strict = options.strict === true;
121
+ const minimumLength = strict ? 1 : MIN_REDACTED_VALUE_LENGTH;
122
+ const secretValues = Object.entries(environment).filter(([name, value]) => typeof value === "string" && value.length >= minimumLength && SECRET_ENVIRONMENT_NAME.test(name)).map(([, value]) => value);
123
+ const markers = markerSpans(message);
124
+ const masked = maskSpans(message, [
125
+ ...markers,
126
+ ...structuralSpans(message, strict),
127
+ ...literalSpans(message, secretValues, markers)
128
+ ]);
129
+ return maskSpans(masked, [...markerSpans(masked), ...structuralSpans(masked, strict)]);
130
+ }
131
+ /**
132
+ * The last `maxBytes` of `text`: a leading partial line (and any split
133
+ * multi-byte character) is dropped when the cut lands mid-text. Redact
134
+ * before cutting; cutting never exposes anything redaction masked.
135
+ */
136
+ function boundedTail(text, maxBytes) {
137
+ const bytes = Buffer.from(text, "utf8");
138
+ if (bytes.length <= maxBytes) return text;
139
+ const tail = bytes.subarray(bytes.length - maxBytes).toString("utf8");
140
+ let cut = tail.slice(tail.indexOf("\n") + 1);
141
+ while (Buffer.byteLength(cut) > maxBytes) cut = cut.slice(1);
142
+ return cut;
143
+ }
144
+ //#endregion
145
+ //#region src/app/identity.ts
146
+ /**
147
+ * Application identity, private state root, and operator-artifact path
148
+ * custody for the `smrt app` command group.
149
+ *
150
+ * Identity, configuration fingerprint, and the private state root are
151
+ * `@happyvertical/smrt-app-runtime`'s single implementation (re-exported
152
+ * here), so the running web process and these commands can never derive
153
+ * different identities or lock domains. Operator-artifact path custody is
154
+ * CLI-only and stays here.
155
+ */
156
+ function isInside(parent, child) {
157
+ const relative = child.slice(parent.length);
158
+ return child === parent || child.startsWith(parent) && (relative.startsWith("/") || relative.startsWith("\\"));
159
+ }
160
+ /**
161
+ * Resolve an operator-selected artifact through its nearest existing ancestor
162
+ * and reject paths that could place application data in or over the checkout.
163
+ */
164
+ function assertExternalArtifactPath(options) {
165
+ const canonicalSource = realpathSync(resolve(options.sourceRoot));
166
+ const missingSegments = [];
167
+ let existingAncestor = resolve(options.path);
168
+ while (!existsSync(existingAncestor)) {
169
+ const parent = dirname(existingAncestor);
170
+ if (parent === existingAncestor) break;
171
+ missingSegments.unshift(basename(existingAncestor));
172
+ existingAncestor = parent;
173
+ }
174
+ const canonicalPath = resolve(realpathSync(existingAncestor), ...missingSegments);
175
+ if (isInside(canonicalSource, canonicalPath) || isInside(canonicalPath, canonicalSource)) throw new Error(`${options.label || "Artifact"} must remain outside the source tree.`);
176
+ return canonicalPath;
177
+ }
178
+ //#endregion
179
+ //#region src/app/runtime.ts
180
+ /**
181
+ * Shared per-invocation context for the `smrt app` command group: source
182
+ * root, application identity, I/O, child-process runners, and the
183
+ * profile-aware runtime environment.
184
+ */
185
+ /** Locate this package's root (`…/smrt-cli`) from the running module. */
186
+ function cliPackageRoot() {
187
+ let directory = dirname(fileURLToPath(import.meta.url));
188
+ for (let depth = 0; depth < 5; depth += 1) {
189
+ const candidate = join(directory, "package.json");
190
+ if (existsSync(candidate)) try {
191
+ if (JSON.parse(readFileSync(candidate, "utf8")).name === "@happyvertical/smrt-cli") return directory;
192
+ } catch {}
193
+ const parent = dirname(directory);
194
+ if (parent === directory) break;
195
+ directory = parent;
196
+ }
197
+ throw new Error("The smrt CLI package root could not be located.");
198
+ }
199
+ /** Absolute path of the `smrt` executable shipped with this package. */
200
+ function cliExecutablePath() {
201
+ return join(cliPackageRoot(), "bin", "smrt.js");
202
+ }
203
+ /** Absolute path of the web launcher `start` spawns. */
204
+ function webLauncherPath() {
205
+ return join(cliPackageRoot(), "bin", "smrt-web.mjs");
206
+ }
207
+ /** Load `<sourceRoot>/.env` without overriding the shell; a missing file is fine. */
208
+ function loadSourceEnvironment(sourceRoot) {
209
+ try {
210
+ process.loadEnvFile(join(sourceRoot, ".env"));
211
+ } catch (error) {
212
+ if (errorCode(error) !== "ENOENT") throw error;
213
+ }
214
+ }
215
+ function runChild(sourceRoot, binary, args, options = {}) {
216
+ const result = spawnSync(binary, args, {
217
+ cwd: sourceRoot,
218
+ env: options.env || process.env,
219
+ encoding: "utf8",
220
+ shell: options.shell || false,
221
+ stdio: options.capture ? [
222
+ "ignore",
223
+ "pipe",
224
+ "pipe"
225
+ ] : "inherit"
226
+ });
227
+ 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 });
228
+ return result;
229
+ }
230
+ /** Default package-manager runner: honours a pnpm `npm_execpath`. */
231
+ function createPackageManagerRunner(sourceRoot) {
232
+ return (args, options = {}) => {
233
+ const npmExecPath = process.env.npm_execpath;
234
+ const pnpmExecPath = npmExecPath && basename(npmExecPath).toLowerCase().startsWith("pnpm") ? npmExecPath : null;
235
+ return pnpmExecPath ? runChild(sourceRoot, process.execPath, [pnpmExecPath, ...args], options) : runChild(sourceRoot, platform() === "win32" ? "pnpm.cmd" : "pnpm", args, {
236
+ ...options,
237
+ shell: platform() === "win32"
238
+ });
239
+ };
240
+ }
241
+ /** Default self-runner: this package's own `smrt` executable. */
242
+ function createSmrtRunner(sourceRoot) {
243
+ return (args, options = {}) => runChild(sourceRoot, process.execPath, [cliExecutablePath(), ...args], options);
244
+ }
245
+ /** Default browser opener (`SMRT_OPEN_STUB` writes the URL to a file instead). */
246
+ function createBrowserOpener(sourceRoot) {
247
+ return (url) => {
248
+ if (process.env.SMRT_OPEN_STUB) {
249
+ writeFileSync(resolve(process.env.SMRT_OPEN_STUB), `${url}\n`);
250
+ return;
251
+ }
252
+ const [binary, args] = platform() === "darwin" ? ["open", [url]] : platform() === "win32" ? ["cmd", [
253
+ "/c",
254
+ "start",
255
+ "",
256
+ url
257
+ ]] : ["xdg-open", [url]];
258
+ runChild(sourceRoot, binary, args);
259
+ };
260
+ }
261
+ /**
262
+ * Default runtime resolver: reload `smrt.config` without the cache.
263
+ *
264
+ * Profile selection is `resolveEffectiveApplicationRuntime()` from
265
+ * smrt-config — the rule the web process applies in app-runtime's SvelteKit
266
+ * entry — so the operator and the server it manages never disagree: no
267
+ * `runtime` block (or no config file) is the `local` profile, and a present
268
+ * value that is not a valid block (`null`, `false`, `0`, `''`, ...) fails
269
+ * closed.
270
+ */
271
+ async function resolveConfiguredRuntime(sourceRoot) {
272
+ const { loadConfig, resolveEffectiveApplicationRuntime } = await import("@happyvertical/smrt-config");
273
+ return resolveEffectiveApplicationRuntime(await loadConfig({
274
+ cache: false,
275
+ searchFrom: sourceRoot
276
+ }));
277
+ }
278
+ /** Build the default dependency set for `sourceRoot`. */
279
+ async function defaultDependencies(sourceRoot) {
280
+ return {
281
+ runtime: await import("@happyvertical/smrt-app-runtime"),
282
+ resolveRuntime: resolveConfiguredRuntime,
283
+ runPackageManager: createPackageManagerRunner(sourceRoot),
284
+ runSmrt: createSmrtRunner(sourceRoot),
285
+ openBrowser: createBrowserOpener(sourceRoot),
286
+ fetch: globalThis.fetch.bind(globalThis),
287
+ sleep: (milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)),
288
+ signal: (pid, signal) => {
289
+ process.kill(pid, signal);
290
+ }
291
+ };
292
+ }
293
+ /** Resolve the application id from `<sourceRoot>/package.json` and `SMRT_APP_ID`. */
294
+ function resolveContextApplicationId(sourceRoot) {
295
+ return resolveApplicationId({
296
+ sourceRoot,
297
+ packageName: JSON.parse(readFileSync(join(sourceRoot, "package.json"), "utf8")).name,
298
+ explicitId: process.env.SMRT_APP_ID
299
+ });
300
+ }
301
+ /** Create or verify the private state root for this invocation. */
302
+ function preparedStateRoot(context) {
303
+ return prepareApplicationStateRoot({
304
+ appId: context.appId,
305
+ dataDirectory: process.env.SMRT_DATA_DIR,
306
+ sourceRoot: context.sourceRoot
307
+ });
308
+ }
309
+ /** Compute the environment a web/migration child of this app runs with. */
310
+ function runtimeEnvironment(context, runtime) {
311
+ const env = {
312
+ ...process.env,
313
+ SMRT_APP_ID: context.appId,
314
+ SMRT_RUNTIME_PROFILE: runtime.profile,
315
+ HOST: runtime.profile === "local" ? "127.0.0.1" : process.env.HOST || "0.0.0.0",
316
+ PORT: process.env.PORT || "5173"
317
+ };
318
+ if (runtime.profile === "local") {
319
+ const paths = context.deps.runtime.resolveLocalRuntimePaths({
320
+ appId: context.appId,
321
+ dataDirectory: process.env.SMRT_DATA_DIR,
322
+ sourceRoot: context.sourceRoot
323
+ });
324
+ env.DATABASE_TYPE = "sqlite";
325
+ env.DATABASE_URL = paths.database;
326
+ env.SMRT_ASSETS_DIR = paths.assets;
327
+ return {
328
+ env,
329
+ paths,
330
+ assetRoot: paths.assets
331
+ };
332
+ }
333
+ if (!process.env.DATABASE_URL) throw new Error(`${runtime.profile} requires DATABASE_URL; copy the matching env example and configure providers.`);
334
+ env.DATABASE_TYPE = "postgres";
335
+ let assetRoot = null;
336
+ if (runtime.providers.assets.provider === "local-files") {
337
+ 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.");
338
+ assetRoot = assertExternalArtifactPath({
339
+ sourceRoot: context.sourceRoot,
340
+ path: process.env.SMRT_ASSETS_DIR,
341
+ label: "Asset storage root"
342
+ });
343
+ env.SMRT_ASSETS_DIR = assetRoot;
344
+ }
345
+ return {
346
+ env,
347
+ paths: null,
348
+ assetRoot
349
+ };
350
+ }
351
+ //#endregion
352
+ //#region src/app/launchers.ts
353
+ /**
354
+ * Process wrappers: explicit schema preparation (`migrate`), deployed job
355
+ * workers (`worker`), and the Vite launcher (`dev`, `build`, `vite`).
356
+ *
357
+ * Ported from the template's `scripts/smrt-prepare-migration.mjs`,
358
+ * `scripts/smrt-worker.mjs`, `scripts/smrt-vite.mjs`, and
359
+ * `scripts/smrt-mcp-apps.mjs`.
360
+ */
361
+ /**
362
+ * Prepare the schema explicitly: under the operation lock, refuse while a
363
+ * local writer is live, establish local storage custody without creating
364
+ * schema or bootstrap records, then run `smrt db:migrate`.
365
+ */
366
+ async function prepareMigration(context) {
367
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
368
+ const stateRoot = preparedStateRoot(context);
369
+ await withOperationLock(stateRoot, "db:migrate", async () => {
370
+ const env = {
371
+ ...process.env,
372
+ SMRT_APP_ID: context.appId
373
+ };
374
+ if (runtime.profile === "local") {
375
+ if (readActiveWriterLease(stateRoot)) throw new Error("Stop the local application before preparing its schema.");
376
+ const paths = context.deps.runtime.resolveLocalRuntimePaths({
377
+ appId: context.appId,
378
+ dataDirectory: process.env.SMRT_DATA_DIR,
379
+ sourceRoot: context.sourceRoot
380
+ });
381
+ await context.deps.runtime.prepareLocalDatabaseStorage({
382
+ appId: context.appId,
383
+ dataDirectory: process.env.SMRT_DATA_DIR,
384
+ sourceRoot: context.sourceRoot
385
+ });
386
+ env.DATABASE_TYPE = "sqlite";
387
+ env.DATABASE_URL = paths.database;
388
+ env.SMRT_ASSETS_DIR = paths.assets;
389
+ }
390
+ const result = context.deps.runSmrt(["db:migrate"], {
391
+ env,
392
+ allowFailure: true
393
+ });
394
+ if (result.error || result.status !== 0) throw new AppCommandError("s-m-r-t database migration failed.", result.status ?? 1, { cause: result.error });
395
+ });
396
+ return 0;
397
+ }
398
+ var WORKER_USAGE = "Usage: smrt app worker [task|schedule]";
399
+ /** Parse `smrt app worker [kind]`, refusing anything but `task`/`schedule`. */
400
+ function parseWorkerKind(args) {
401
+ if (args.length > 1) throw new AppCommandError(WORKER_USAGE, 2);
402
+ const kind = args[0] ?? "task";
403
+ if (kind !== "task" && kind !== "schedule") throw new AppCommandError(WORKER_USAGE, 2);
404
+ return kind;
405
+ }
406
+ /**
407
+ * Start a deployed job worker. Resolves once the runner is started; the
408
+ * process stays alive until SIGINT/SIGTERM closes the runtime.
409
+ */
410
+ async function runWorker(context, args) {
411
+ const kind = parseWorkerKind(args);
412
+ const configured = await context.deps.resolveRuntime(context.sourceRoot);
413
+ if (configured.profile === "local") throw new Error("Local jobs run inline or embedded; a separate worker requires self-hosted or cloud.");
414
+ if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required.");
415
+ const registration = join(context.sourceRoot, ".smrt", "runtime", "register.js");
416
+ if (!existsSync(registration)) throw new Error("Missing .smrt/runtime/register.js: run pnpm build before starting a worker.");
417
+ await import(pathToFileURL(registration).href);
418
+ const probe = (component) => createProviderReadinessProbe(component, {
419
+ profile: configured.profile,
420
+ provider: configured.providers[component].provider
421
+ }, { sourceRoot: context.sourceRoot });
422
+ const runtime = await context.deps.runtime.initializeDeployedApplicationRuntime({
423
+ profile: configured.profile,
424
+ providers: {
425
+ database: configured.providers.database,
426
+ authentication: configured.providers.authentication,
427
+ tenancy: configured.providers.tenancy,
428
+ assets: configured.providers.assets,
429
+ secrets: configured.providers.secrets,
430
+ jobs: configured.providers.jobs,
431
+ network: configured.providers.network
432
+ },
433
+ database: {
434
+ engine: "postgres",
435
+ connect: async () => {
436
+ const { getDatabase } = await import("@happyvertical/sql");
437
+ return await getDatabase({
438
+ type: "postgres",
439
+ url: process.env.DATABASE_URL
440
+ });
441
+ },
442
+ close: async (db) => {
443
+ await db.close?.();
444
+ }
445
+ },
446
+ authentication: {
447
+ provider: configured.providers.authentication.provider,
448
+ readiness: probe("authentication")
449
+ },
450
+ assets: {
451
+ provider: configured.providers.assets.provider,
452
+ readiness: probe("assets")
453
+ },
454
+ secrets: {
455
+ provider: configured.providers.secrets.provider,
456
+ readiness: probe("secrets")
457
+ }
458
+ });
459
+ await (kind === "schedule" ? await runtime.createScheduleWorker() : await runtime.createTaskWorker({ concurrency: Number.parseInt(process.env.SMRT_WORKER_CONCURRENCY || "4", 10) })).start();
460
+ context.io.stdout(`${JSON.stringify({
461
+ schemaVersion: 1,
462
+ status: "ready",
463
+ kind,
464
+ secretValuesIncluded: false
465
+ })}\n`);
466
+ let stopping = false;
467
+ const stop = async () => {
468
+ if (stopping) return;
469
+ stopping = true;
470
+ await runtime.close();
471
+ };
472
+ process.once("SIGINT", () => void stop());
473
+ process.once("SIGTERM", () => void stop());
474
+ return 0;
475
+ }
476
+ /** Failure of the Vite launcher; rendered as `s-m-r-t Vite launcher: …`. */
477
+ var ViteLauncherError = class extends Error {};
478
+ /** Resolve the application's installed Vite CLI entry, refusing unsafe paths. */
479
+ function resolveViteEntry(sourceRoot) {
480
+ const installed = findInstalledPackage(sourceRoot, "vite");
481
+ if (!installed) throw new ViteLauncherError("Could not resolve the installed Vite package. Verify the generated app dependencies and Vite compatibility.");
482
+ const packagePath = join(realpathSync(installed), "package.json");
483
+ const packageRoot = dirname(packagePath);
484
+ let metadata;
485
+ try {
486
+ metadata = JSON.parse(readFileSync(packagePath, "utf8"));
487
+ } catch {
488
+ throw new ViteLauncherError("The installed Vite package metadata is invalid; reinstall dependencies.");
489
+ }
490
+ const declaredBin = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.vite;
491
+ if (typeof declaredBin !== "string" || declaredBin === "") throw new ViteLauncherError("The installed Vite package does not declare its CLI.");
492
+ const entry = resolve(packageRoot, declaredBin);
493
+ const relativeEntry = relative(packageRoot, entry);
494
+ if (relativeEntry === "" || relativeEntry.startsWith("..") || isAbsolute(relativeEntry)) throw new ViteLauncherError("The installed Vite package declares an unsafe CLI path.");
495
+ if (!existsSync(entry)) throw new ViteLauncherError("The installed Vite package CLI is missing; reinstall dependencies.");
496
+ return entry;
497
+ }
498
+ /** Loopback host every `smrt app` URL (onboarding, `open`, `start`) names. */
499
+ var LOOPBACK_HOST = "127.0.0.1";
500
+ /**
501
+ * Arguments for `smrt app dev`: the caller's Vite arguments, plus
502
+ * `--host 127.0.0.1` unless they name `--host`, and `--port $PORT` when
503
+ * `PORT` is set and they name no `--port`. Vite's own default host is
504
+ * `localhost`, which Node resolves to `[::1]` first, so the dev server would
505
+ * not answer the `http://127.0.0.1:<PORT>` URL onboarding and `app:open` hand
506
+ * out. An explicit `--host`/`--port` (or `smrt app vite dev …`) wins.
507
+ */
508
+ function devServerArguments(args, env = process.env) {
509
+ const names = (flag) => args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
510
+ const result = [...args];
511
+ if (!names("--host")) result.push("--host", LOOPBACK_HOST);
512
+ if (!names("--port") && env.PORT) result.push("--port", env.PORT);
513
+ return result;
514
+ }
515
+ /**
516
+ * Run the application's Vite CLI in this process with `args` verbatim. The
517
+ * caller has already loaded `.env` without overriding the shell.
518
+ */
519
+ async function launchVite(context, args) {
520
+ let entry;
521
+ try {
522
+ entry = resolveViteEntry(context.sourceRoot);
523
+ } catch (error) {
524
+ if (error instanceof ViteLauncherError) {
525
+ context.io.stderr(`s-m-r-t Vite launcher: ${error.message}\n`);
526
+ return 1;
527
+ }
528
+ throw error;
529
+ }
530
+ process.argv = [
531
+ process.execPath,
532
+ entry,
533
+ ...args
534
+ ];
535
+ await import(pathToFileURL(entry).href);
536
+ return 0;
537
+ }
538
+ /**
539
+ * Validate opt-in portable MCP Apps metadata (`./mcp-apps`) when present.
540
+ * Returns the exit code; findings go to stderr.
541
+ */
542
+ async function validateMcpAppsIfPresent(context) {
543
+ const root = join(context.sourceRoot, "mcp-apps");
544
+ if (!existsSync(root)) return 0;
545
+ const { validateMcpAppsPackage } = await import("./mcp-apps-packaging-CixRnxz_.js");
546
+ const result = validateMcpAppsPackage(root);
547
+ for (const finding of result.findings) context.io.stderr(`${finding.code}: ${finding.message}\n`);
548
+ if (!result.valid) {
549
+ context.io.stderr(`Error: MCP Apps package validation failed with ${result.findings.length} finding(s)\n`);
550
+ return 1;
551
+ }
552
+ context.io.stdout("MCP Apps package at ./mcp-apps is valid.\n");
553
+ return 0;
554
+ }
555
+ //#endregion
556
+ //#region src/app/process-record.ts
557
+ /**
558
+ * Managed web-process record (`<state>/app.pid`).
559
+ *
560
+ * Ported from the template's `scripts/smrt-process.mjs`. A recorded pid is
561
+ * only trusted when the live process's command line proves it is the web
562
+ * launcher with this record's instance nonce, so a recycled pid is never
563
+ * signalled.
564
+ */
565
+ /** File name of the web launcher; part of the process-identity proof. */
566
+ var WEB_LAUNCHER_NAME = "smrt-web.mjs";
567
+ function processCommand(pid) {
568
+ if (platform() === "win32") {
569
+ const result = spawnSync("powershell.exe", [
570
+ "-NoProfile",
571
+ "-NonInteractive",
572
+ "-Command",
573
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`
574
+ ], {
575
+ encoding: "utf8",
576
+ windowsHide: true
577
+ });
578
+ return result.status === 0 ? result.stdout.trim() : null;
579
+ }
580
+ const result = spawnSync("ps", [
581
+ "-p",
582
+ String(pid),
583
+ "-o",
584
+ "command="
585
+ ], { encoding: "utf8" });
586
+ return result.status === 0 ? result.stdout.trim() : null;
587
+ }
588
+ /** Write the private (mode 0600) process record. */
589
+ function writeProcessRecord(path, record) {
590
+ writeFileSync(path, `${JSON.stringify(record)}\n`, { mode: 384 });
591
+ }
592
+ /**
593
+ * Send SIGTERM. Returns `false` when the process had already exited; any
594
+ * other failure (e.g. EPERM) is rethrown.
595
+ */
596
+ function sendTerminationSignal(pid, killProcess = process.kill) {
597
+ try {
598
+ killProcess(pid, "SIGTERM");
599
+ return true;
600
+ } catch (error) {
601
+ if (errorCode(error) === "ESRCH") return false;
602
+ throw error;
603
+ }
604
+ }
605
+ /** Split a process command line into argv-like tokens, dropping quotes. */
606
+ function commandTokens(command) {
607
+ return command.trim().split(/\s+/).map((token) => token.replace(/^"|"$/g, "")).filter(Boolean);
608
+ }
609
+ /**
610
+ * True when `command` is exactly the web launcher started with this record's
611
+ * nonce: the final argument is `--smrt-instance=<instance>` and the argument
612
+ * before it is a path whose basename is `smrt-web.mjs` (the CLI's
613
+ * `bin/smrt-web.mjs`, or the template's `scripts/smrt-web.mjs`). Substring
614
+ * matches such as `evil-smrt-web.mjs` or a nonce with a suffix are rejected.
615
+ */
616
+ function matchesApplicationProcess(record, command) {
617
+ if (typeof command !== "string") return false;
618
+ const tokens = commandTokens(command);
619
+ if (tokens.length < 2) return false;
620
+ const script = tokens[tokens.length - 2];
621
+ return tokens[tokens.length - 1] === `--smrt-instance=${record.instance}` && script.split(/[\\/]/).pop() === "smrt-web.mjs";
622
+ }
623
+ function isLive(pid) {
624
+ try {
625
+ process.kill(pid, 0);
626
+ return true;
627
+ } catch (error) {
628
+ const code = errorCode(error);
629
+ return code === "EPERM" || code === "EACCES";
630
+ }
631
+ }
632
+ /**
633
+ * Re-check `record` against the live process table. Only `gone` and
634
+ * `mismatched` justify dropping the record; `unverifiable` (the process
635
+ * query failed or returned nothing) must keep it and must not signal.
636
+ */
637
+ function checkOwnedProcess(record) {
638
+ if (!isLive(record.pid)) return "gone";
639
+ const command = processCommand(record.pid);
640
+ if (command === null || command === "") return isLive(record.pid) ? "unverifiable" : "gone";
641
+ return matchesApplicationProcess(record, command) ? "owned" : "mismatched";
642
+ }
643
+ /** True only when {@link checkOwnedProcess} proves the record still owned. */
644
+ function verifyOwnedProcess(record) {
645
+ return checkOwnedProcess(record) === "owned";
646
+ }
647
+ /**
648
+ * Return the recorded web process when it is live and provably ours;
649
+ * otherwise remove the stale record and return `null`.
650
+ */
651
+ function readOwnedProcess(path) {
652
+ let record;
653
+ try {
654
+ record = JSON.parse(readFileSync(path, "utf8"));
655
+ 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.");
656
+ } catch {
657
+ rmSync(path, { force: true });
658
+ return null;
659
+ }
660
+ try {
661
+ process.kill(record.pid, 0);
662
+ } catch (error) {
663
+ const code = errorCode(error);
664
+ if (code !== "EPERM" && code !== "EACCES") {
665
+ rmSync(path, { force: true });
666
+ return null;
667
+ }
668
+ }
669
+ const command = processCommand(record.pid);
670
+ if (command === null || command === "") throw new Error(`Application process ${record.pid} is live but its identity cannot be verified.`);
671
+ if (!matchesApplicationProcess(record, command)) {
672
+ rmSync(path, { force: true });
673
+ return null;
674
+ }
675
+ return record;
676
+ }
677
+ //#endregion
678
+ //#region src/app/operations.ts
679
+ /**
680
+ * Local/deployed application operations: install, setup, recover, start,
681
+ * stop, doctor, open, backup, export, import.
682
+ *
683
+ * Ported from the template's `scripts/smrt-app.mjs`. Output JSON, exit
684
+ * codes, state files, and environment variables are unchanged so the
685
+ * template's `pnpm app:*` scripts can become `smrt app <operation>`.
686
+ */
687
+ /** Operations implemented by this module. */
688
+ var APP_OPERATIONS = [
689
+ "install",
690
+ "setup",
691
+ "recover",
692
+ "start",
693
+ "stop",
694
+ "doctor",
695
+ "open",
696
+ "backup",
697
+ "export",
698
+ "import"
699
+ ];
700
+ function printJson$1(context, value, pretty = false) {
701
+ context.io.stdout(`${pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)}\n`);
702
+ }
703
+ /** Print the one-time URL to the operator's terminal only, never to logs. */
704
+ function printOnboardingUrl(context, url) {
705
+ if (!url || !context.io.operatorTerminal) return;
706
+ context.io.operatorTerminal(`Open this one-time owner setup link on this device (it works once and expires):\n ${url}\n`);
707
+ }
708
+ function ensurePrivateDirectory(path) {
709
+ mkdirSync(path, {
710
+ recursive: true,
711
+ mode: 448
712
+ });
713
+ }
714
+ function nearestExistingAncestor(path) {
715
+ let candidate = path;
716
+ while (!existsSync(candidate)) {
717
+ const parent = dirname(candidate);
718
+ if (parent === candidate) return candidate;
719
+ candidate = parent;
720
+ }
721
+ return candidate;
722
+ }
723
+ function pidPath(context) {
724
+ return join(preparedStateRoot(context), "app.pid");
725
+ }
726
+ function onboardingPath(context) {
727
+ return join(preparedStateRoot(context), "onboarding.json");
728
+ }
729
+ function onboardingLaunchPath(context) {
730
+ return join(preparedStateRoot(context), "onboarding-launch.html");
731
+ }
732
+ function removeOnboardingHandoff(context) {
733
+ rmSync(onboardingPath(context), { force: true });
734
+ rmSync(onboardingLaunchPath(context), { force: true });
735
+ }
736
+ function writePrivateAtomic(destination, contents) {
737
+ const temporary = `${destination}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
738
+ try {
739
+ writeFileSync(temporary, contents, {
740
+ flag: "wx",
741
+ mode: 384
742
+ });
743
+ renameSync(temporary, destination);
744
+ } finally {
745
+ rmSync(temporary, { force: true });
746
+ }
747
+ }
748
+ function saveOnboardingLaunch(context, url) {
749
+ const escapedUrl = url.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
750
+ writePrivateAtomic(onboardingLaunchPath(context), `<!doctype html><meta http-equiv="refresh" content="0;url=${escapedUrl}">\n`);
751
+ }
752
+ function saveOnboardingUrl(context, url) {
753
+ ensurePrivateDirectory(preparedStateRoot(context));
754
+ saveOnboardingLaunch(context, url);
755
+ writePrivateAtomic(onboardingPath(context), `${JSON.stringify({
756
+ schemaVersion: 1,
757
+ url
758
+ })}\n`);
759
+ }
760
+ var InvalidOnboardingHandoff = class extends Error {};
761
+ /** Read the retained loopback onboarding URL; tampered handoffs are removed. */
762
+ function readOnboardingUrl(context) {
763
+ try {
764
+ const value = JSON.parse(readFileSync(onboardingPath(context), "utf8"));
765
+ if (value?.schemaVersion !== 1 || typeof value.url !== "string") throw new InvalidOnboardingHandoff("Invalid onboarding handoff.");
766
+ const url = new URL(value.url);
767
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" && url.hostname !== "localhost" || url.pathname !== "/setup") throw new InvalidOnboardingHandoff("Invalid onboarding handoff.");
768
+ return url.toString();
769
+ } catch (error) {
770
+ if (errorCode(error) === "ENOENT" || error instanceof SyntaxError || error instanceof InvalidOnboardingHandoff || errorCode(error) === "ERR_INVALID_URL") {
771
+ removeOnboardingHandoff(context);
772
+ return null;
773
+ }
774
+ throw error;
775
+ }
776
+ }
777
+ /** Throw unless the configured profile is `local`. */
778
+ async function assertLocalOperation(context, operation) {
779
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
780
+ if (runtime.profile !== "local") throw new Error(`${operation} is local-profile only; run the production Node build or container for deployed profiles.`);
781
+ return runtime;
782
+ }
783
+ async function initializeLocal(context, runtime, env, options = {}) {
784
+ if (runtime.profile !== "local") return null;
785
+ return context.deps.runtime.initializeLocalApplicationRuntime({
786
+ appId: context.appId,
787
+ dataDirectory: process.env.SMRT_DATA_DIR,
788
+ sourceRoot: context.sourceRoot,
789
+ bindHost: env.HOST || "127.0.0.1",
790
+ providers: {
791
+ database: runtime.providers.database,
792
+ authentication: runtime.providers.authentication,
793
+ tenancy: runtime.providers.tenancy,
794
+ assets: runtime.providers.assets,
795
+ secrets: runtime.providers.secrets,
796
+ jobs: runtime.providers.jobs,
797
+ network: runtime.providers.network
798
+ },
799
+ prepareDatabase: options.prepareDatabase ? async () => {
800
+ context.deps.runSmrt(["db:migrate"], {
801
+ env,
802
+ label: "smrt db:migrate"
803
+ });
804
+ } : void 0,
805
+ backgroundJobs: process.env.SMRT_BACKGROUND_JOBS === "true"
806
+ });
807
+ }
808
+ async function setup(context, operationLock) {
809
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
810
+ const operatorLease = runtime.profile === "local" ? acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance }) : null;
811
+ try {
812
+ const { env } = runtimeEnvironment(context, runtime);
813
+ context.deps.runPackageManager(["build"], {
814
+ env,
815
+ label: "pnpm build"
816
+ });
817
+ let initialized = null;
818
+ if (runtime.profile === "local") initialized = await initializeLocal(context, runtime, env, { prepareDatabase: true });
819
+ else context.deps.runSmrt(["db:migrate"], {
820
+ env,
821
+ label: "smrt db:migrate"
822
+ });
823
+ const port = env.PORT || "5173";
824
+ const onboardingUrl = initialized?.bootstrap ? `http://127.0.0.1:${port}/setup?token=${encodeURIComponent(initialized.bootstrap.token)}` : null;
825
+ const diagnostics = await initialized?.runtime.diagnostics();
826
+ if (onboardingUrl) saveOnboardingUrl(context, onboardingUrl);
827
+ if (diagnostics?.bootstrap.status === "claimed") removeOnboardingHandoff(context);
828
+ await initialized?.runtime.db.close?.();
829
+ const retainedOnboardingUrl = onboardingUrl || readOnboardingUrl(context);
830
+ const onboardingAvailable = runtime.profile === "local" && retainedOnboardingUrl !== null;
831
+ const report = {
832
+ schemaVersion: 1,
833
+ status: "ready",
834
+ profile: runtime.profile,
835
+ onboardingAvailable,
836
+ onboardingRecovery: onboardingAvailable ? "pnpm app:open" : null,
837
+ secretValuesIncluded: false
838
+ };
839
+ printJson$1(context, report, true);
840
+ return {
841
+ ...report,
842
+ onboardingUrl: retainedOnboardingUrl
843
+ };
844
+ } finally {
845
+ operatorLease?.release();
846
+ }
847
+ }
848
+ async function recoverOnboarding(context, operationLock) {
849
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
850
+ const { env } = runtimeEnvironment(context, runtime);
851
+ if (runtime.profile !== "local") throw new Error("Owner onboarding recovery is local-only.");
852
+ const operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance });
853
+ try {
854
+ const initialized = await initializeLocal(context, runtime, env);
855
+ if (!initialized) throw new Error("Local runtime initialization failed.");
856
+ const invitation = await initialized.runtime.rotateBootstrapInvitation();
857
+ const url = `http://127.0.0.1:${env.PORT || "5173"}/setup?token=${encodeURIComponent(invitation.token)}`;
858
+ saveOnboardingUrl(context, url);
859
+ await initialized.runtime.db.close?.();
860
+ printJson$1(context, {
861
+ schemaVersion: 1,
862
+ status: "ready",
863
+ onboardingAvailable: true,
864
+ recovery: "Run pnpm app:start, then pnpm app:open.",
865
+ secretValuesIncluded: false
866
+ });
867
+ printOnboardingUrl(context, url);
868
+ } finally {
869
+ operatorLease.release();
870
+ }
871
+ }
872
+ function isAlive(pid) {
873
+ process.kill(pid, 0);
874
+ return true;
875
+ }
876
+ async function waitForReady(context, url, pid, instance, configuration) {
877
+ const healthUrl = new URL("/api/_runtime/health", url);
878
+ for (let attempt = 0; attempt < 40; attempt += 1) {
879
+ try {
880
+ const response = await context.deps.fetch(healthUrl, { redirect: "manual" });
881
+ if (response.status === 200) {
882
+ const health = await response.json();
883
+ if (health?.status === "ready" && health.application === context.appId && health.instance === instance && health.configuration === configuration) {
884
+ isAlive(pid);
885
+ return;
886
+ }
887
+ }
888
+ isAlive(pid);
889
+ } catch {
890
+ try {
891
+ isAlive(pid);
892
+ } catch {
893
+ throw new Error("The application process exited before becoming ready.");
894
+ }
895
+ }
896
+ await context.deps.sleep(250);
897
+ }
898
+ throw new Error(`The application did not become ready at ${url}.`);
899
+ }
900
+ async function start(context, operationLock) {
901
+ const runtime = await assertLocalOperation(context, "app:start");
902
+ const { env } = runtimeEnvironment(context, runtime);
903
+ const url = `http://127.0.0.1:${env.PORT || "5173"}/`;
904
+ env.ORIGIN ||= url;
905
+ const configuration = runtimeConfigurationFingerprint(runtime, env);
906
+ const existing = readOwnedProcess(pidPath(context));
907
+ if (existing) {
908
+ await waitForReady(context, url, existing.pid, existing.instance, configuration);
909
+ printJson$1(context, {
910
+ schemaVersion: 1,
911
+ status: "running",
912
+ pid: existing.pid
913
+ });
914
+ return existing.pid;
915
+ }
916
+ ensurePrivateDirectory(preparedStateRoot(context));
917
+ const instance = randomBytes(16).toString("hex");
918
+ const childEnv = {
919
+ ...env,
920
+ HOST: runtime.profile === "local" ? "127.0.0.1" : env.HOST || "0.0.0.0",
921
+ PORT: env.PORT || "5173",
922
+ SMRT_PROCESS_INSTANCE: instance,
923
+ SMRT_OPERATION_INSTANCE: operationLock?.instance
924
+ };
925
+ const logFile = startLogPath(context);
926
+ const logFd = openStartLog(logFile);
927
+ let child;
928
+ try {
929
+ child = spawn(process.execPath, [webLauncherPath(), `--smrt-instance=${instance}`], {
930
+ cwd: context.sourceRoot,
931
+ env: childEnv,
932
+ detached: true,
933
+ stdio: [
934
+ "ignore",
935
+ logFd,
936
+ logFd
937
+ ]
938
+ });
939
+ } finally {
940
+ closeSync(logFd);
941
+ }
942
+ child.unref();
943
+ const pid = child.pid;
944
+ if (pid === void 0) throw new Error("The application process could not be started.");
945
+ writeProcessRecord(pidPath(context), {
946
+ pid,
947
+ instance
948
+ });
949
+ try {
950
+ await waitForReady(context, url, pid, instance, configuration);
951
+ } catch (error) {
952
+ 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.`);
953
+ rmSync(pidPath(context), { force: true });
954
+ throw new ApplicationStartError(error instanceof Error ? error.message : "The application did not become ready.", redactedLogTail(logFile, childEnv), logFile);
955
+ }
956
+ printJson$1(context, {
957
+ schemaVersion: 1,
958
+ status: "started",
959
+ pid
960
+ });
961
+ return pid;
962
+ }
963
+ /** Upper bound on the web-process output a failed `start` reports. */
964
+ var START_OUTPUT_TAIL_BYTES = 8 * 1024;
965
+ /** Window read before redaction, so a cut never splits a reported secret. */
966
+ var START_OUTPUT_WINDOW_BYTES = 64 * 1024;
967
+ function startLogPath(context) {
968
+ return join(preparedStateRoot(context), "app.log");
969
+ }
970
+ /**
971
+ * Create a fresh 0600 `app.log` for this start. The previous run's log is
972
+ * removed first and the new one is created exclusively, so a planted link or
973
+ * file is never written through.
974
+ */
975
+ function openStartLog(path) {
976
+ rmSync(path, { force: true });
977
+ return openSync(path, "wx", 384);
978
+ }
979
+ /**
980
+ * The last {@link START_OUTPUT_TAIL_BYTES} of `path`, strictly redacted
981
+ * against the child's environment. A bounded window is redacted before it is cut, and a
982
+ * window that starts mid-file drops its leading partial record (all of it
983
+ * when the window holds no newline), so no fragment of a secret whose
984
+ * prefix lies before the window can survive the cut.
985
+ */
986
+ function redactedLogTail(path, env) {
987
+ let text;
988
+ try {
989
+ const fd = openSync(path, "r");
990
+ try {
991
+ const size = fstatSync(fd).size;
992
+ const length = Math.min(size, START_OUTPUT_WINDOW_BYTES);
993
+ const buffer = Buffer.alloc(length);
994
+ readSync(fd, buffer, 0, length, size - length);
995
+ text = buffer.toString("utf8");
996
+ if (length < size) {
997
+ const newline = text.indexOf("\n");
998
+ text = newline === -1 ? "" : text.slice(newline + 1);
999
+ }
1000
+ } finally {
1001
+ closeSync(fd);
1002
+ }
1003
+ } catch {
1004
+ return "";
1005
+ }
1006
+ return boundedTail(redactSecrets(text, env, { strict: true }), START_OUTPUT_TAIL_BYTES);
1007
+ }
1008
+ /**
1009
+ * Terminate a launcher that failed to prove readiness: SIGTERM, wait, then
1010
+ * SIGKILL, wait. Resolves `true` only once the child has provably exited.
1011
+ * Uses the child handle we own, so a reaped pid is never signalled.
1012
+ */
1013
+ async function terminateFailedStart(context, child) {
1014
+ const exited = () => child.exitCode !== null || child.signalCode !== null;
1015
+ for (const [signal, attempts] of [["SIGTERM", 50], ["SIGKILL", 20]]) {
1016
+ if (exited()) return true;
1017
+ try {
1018
+ context.deps.signal(child.pid, signal);
1019
+ } catch (error) {
1020
+ if (errorCode(error) === "ESRCH") return true;
1021
+ throw error;
1022
+ }
1023
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1024
+ await context.deps.sleep(100);
1025
+ if (exited()) return true;
1026
+ }
1027
+ }
1028
+ return exited();
1029
+ }
1030
+ async function stop(context) {
1031
+ const record = readOwnedProcess(pidPath(context));
1032
+ const pid = record?.pid || null;
1033
+ if (!record || !pid) {
1034
+ rmSync(pidPath(context), { force: true });
1035
+ printJson$1(context, {
1036
+ schemaVersion: 1,
1037
+ status: "stopped"
1038
+ });
1039
+ return;
1040
+ }
1041
+ const state = checkOwnedProcess(record);
1042
+ 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.`);
1043
+ if (state !== "owned" || !sendTerminationSignal(pid)) {
1044
+ rmSync(pidPath(context), { force: true });
1045
+ printJson$1(context, {
1046
+ schemaVersion: 1,
1047
+ status: "stopped",
1048
+ pid
1049
+ });
1050
+ return;
1051
+ }
1052
+ for (let attempt = 0; attempt < 40; attempt += 1) {
1053
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
1054
+ try {
1055
+ process.kill(pid, 0);
1056
+ } catch {
1057
+ break;
1058
+ }
1059
+ if (attempt === 39) throw new Error(`Application process ${pid} did not stop cleanly.`);
1060
+ }
1061
+ rmSync(pidPath(context), { force: true });
1062
+ printJson$1(context, {
1063
+ schemaVersion: 1,
1064
+ status: "stopped",
1065
+ pid
1066
+ });
1067
+ }
1068
+ async function open(context) {
1069
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
1070
+ const url = `http://${runtime.profile === "local" ? "127.0.0.1" : process.env.HOST || "127.0.0.1"}:${process.env.PORT || "5173"}/`;
1071
+ const onboardingUrl = runtime.profile === "local" ? readOnboardingUrl(context) : null;
1072
+ if (onboardingUrl) saveOnboardingLaunch(context, onboardingUrl);
1073
+ const destination = onboardingUrl ? pathToFileURL(onboardingLaunchPath(context)).toString() : url;
1074
+ context.deps.openBrowser(destination);
1075
+ printJson$1(context, {
1076
+ schemaVersion: 1,
1077
+ status: "opened",
1078
+ url
1079
+ });
1080
+ }
1081
+ async function doctor(context) {
1082
+ const findings = [];
1083
+ let runtime = null;
1084
+ let paths = null;
1085
+ let statePath = null;
1086
+ let canInspectMigrations = false;
1087
+ try {
1088
+ runtime = await context.deps.resolveRuntime(context.sourceRoot);
1089
+ } catch {
1090
+ findings.push({
1091
+ code: "invalid-runtime-profile",
1092
+ severity: "error",
1093
+ message: "The canonical runtime profile is invalid.",
1094
+ recovery: "Select local, self-hosted, or cloud in smrt.config.ts."
1095
+ });
1096
+ }
1097
+ if (Number(process.versions.node.split(".")[0]) < 26) findings.push({
1098
+ code: "unsupported-node",
1099
+ severity: "error",
1100
+ message: "Node.js 26 or newer is required.",
1101
+ recovery: "Install the Node.js version declared in package.json engines."
1102
+ });
1103
+ if (runtime) {
1104
+ if (runtime.profile !== "local") for (const [component, provider] of [
1105
+ ["authentication", runtime.providers.authentication.provider],
1106
+ ["assets", runtime.providers.assets.provider],
1107
+ ["secrets", runtime.providers.secrets.provider]
1108
+ ]) try {
1109
+ await createProviderReadinessProbe(component, {
1110
+ profile: runtime.profile,
1111
+ provider
1112
+ }, { sourceRoot: context.sourceRoot })();
1113
+ } catch {
1114
+ findings.push({
1115
+ code: "provider-not-configured",
1116
+ component,
1117
+ severity: "error",
1118
+ message: `The ${component} provider is not ready.`,
1119
+ recovery: `Configure and verify the installed ${component} provider readiness module.`
1120
+ });
1121
+ }
1122
+ try {
1123
+ ({paths} = runtimeEnvironment(context, runtime));
1124
+ statePath = resolveApplicationStateRoot({
1125
+ appId: context.appId,
1126
+ dataDirectory: process.env.SMRT_DATA_DIR,
1127
+ sourceRoot: context.sourceRoot
1128
+ });
1129
+ accessSync(nearestExistingAncestor(statePath), constants.W_OK);
1130
+ if (runtime.profile === "local") paths = await context.deps.runtime.validateLocalDatabaseStorage({
1131
+ appId: context.appId,
1132
+ dataDirectory: process.env.SMRT_DATA_DIR,
1133
+ sourceRoot: context.sourceRoot
1134
+ });
1135
+ canInspectMigrations = true;
1136
+ const host = process.env.HOST || (runtime.profile === "local" ? "127.0.0.1" : "0.0.0.0");
1137
+ if (runtime.profile === "local" && host !== "127.0.0.1" && host !== "::1") findings.push({
1138
+ code: "unsafe-local-bind",
1139
+ severity: "error",
1140
+ message: "Local owner bootstrap may only bind to a loopback address.",
1141
+ recovery: "Unset HOST or set HOST=127.0.0.1."
1142
+ });
1143
+ if (paths?.root) accessSync(nearestExistingAncestor(paths.root), constants.W_OK);
1144
+ } catch (error) {
1145
+ findings.push({
1146
+ code: "runtime-path-unavailable",
1147
+ severity: "error",
1148
+ message: "A required runtime path or provider configuration is unavailable.",
1149
+ recovery: error instanceof Error ? error.message : "Configure a writable runtime data path."
1150
+ });
1151
+ }
1152
+ if (canInspectMigrations) try {
1153
+ const status = context.deps.runSmrt(["db:status", "--json"], {
1154
+ env: runtimeEnvironment(context, runtime).env,
1155
+ capture: true,
1156
+ allowFailure: true,
1157
+ label: "smrt db:status --json"
1158
+ });
1159
+ if (status.status !== 0) findings.push({
1160
+ code: "migration-status-failed",
1161
+ severity: "error",
1162
+ message: "Database migration status could not be verified.",
1163
+ recovery: "Run pnpm app:setup and inspect the private migration logs."
1164
+ });
1165
+ else {
1166
+ const migrationStatus = JSON.parse(status.stdout || "{}");
1167
+ 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({
1168
+ code: "migration-required",
1169
+ severity: "error",
1170
+ message: "Database migrations are pending or failed.",
1171
+ recovery: "Run pnpm app:setup, then rerun pnpm app:doctor."
1172
+ });
1173
+ }
1174
+ } catch {
1175
+ findings.push({
1176
+ code: "migration-status-failed",
1177
+ severity: "error",
1178
+ message: "Database migration status could not be parsed.",
1179
+ recovery: "Run pnpm app:setup and inspect the private migration logs."
1180
+ });
1181
+ }
1182
+ }
1183
+ const report = {
1184
+ schemaVersion: 1,
1185
+ status: findings.some((finding) => finding.severity === "error") ? "error" : "ready",
1186
+ profile: runtime?.profile || null,
1187
+ capabilities: runtime?.capabilities || null,
1188
+ paths: paths || statePath ? {
1189
+ root: paths?.root || null,
1190
+ database: paths?.database || null,
1191
+ assets: paths?.assets || null,
1192
+ state: statePath
1193
+ } : null,
1194
+ findings,
1195
+ secretValuesIncluded: false
1196
+ };
1197
+ printJson$1(context, report, true);
1198
+ return report.status === "error" ? 1 : 0;
1199
+ }
1200
+ async function backup(context, args) {
1201
+ if ((await context.deps.resolveRuntime(context.sourceRoot)).profile !== "local") throw new Error("This scaffold delegates deployed backups to the selected operator or managed provider.");
1202
+ const explicitDestination = args[0] ? assertExternalArtifactPath({
1203
+ sourceRoot: context.sourceRoot,
1204
+ path: resolve(args[0]),
1205
+ label: "Backup destination"
1206
+ }) : null;
1207
+ const paths = await context.deps.runtime.validateLocalDatabaseStorage({
1208
+ appId: context.appId,
1209
+ dataDirectory: process.env.SMRT_DATA_DIR,
1210
+ sourceRoot: context.sourceRoot
1211
+ });
1212
+ await withOperationLock(preparedStateRoot(context), "backup", async (lock) => {
1213
+ const operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: lock.instance });
1214
+ try {
1215
+ const destination = assertExternalArtifactPath({
1216
+ sourceRoot: context.sourceRoot,
1217
+ path: explicitDestination || join(dirname(paths.root), "backups", `${context.appId}-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}`),
1218
+ label: "Backup destination"
1219
+ });
1220
+ const storageRelative = relative(paths.root, destination);
1221
+ if (storageRelative === "" || !isAbsolute(storageRelative) && storageRelative !== ".." && !storageRelative.startsWith(`..${sep}`)) throw new Error("Backup destination must remain outside local storage.");
1222
+ ensurePrivateDirectory(dirname(destination));
1223
+ try {
1224
+ mkdirSync(destination, { mode: 448 });
1225
+ } catch (error) {
1226
+ if (error.code !== "EEXIST") throw error;
1227
+ throw new Error(`Backup destination already exists: ${destination}`);
1228
+ }
1229
+ try {
1230
+ for (const entry of readdirSync(paths.root)) cpSync(join(paths.root, entry), join(destination, entry), {
1231
+ recursive: true,
1232
+ force: false,
1233
+ errorOnExist: true
1234
+ });
1235
+ chmodSync(destination, 448);
1236
+ } catch (error) {
1237
+ throw new Error(`Backup copy failed; destination preserved at ${destination}`, { cause: error });
1238
+ }
1239
+ printJson$1(context, {
1240
+ schemaVersion: 1,
1241
+ status: "backed-up",
1242
+ destination
1243
+ });
1244
+ } finally {
1245
+ operatorLease.release();
1246
+ }
1247
+ });
1248
+ }
1249
+ /**
1250
+ * The application's own adapter at `scripts/smrt-portability.mjs` keeps
1251
+ * working as the documented extension point; otherwise the built-in adapter
1252
+ * is used.
1253
+ */
1254
+ async function loadPortabilityAdapter(context) {
1255
+ const override = join(context.sourceRoot, "scripts", "smrt-portability.mjs");
1256
+ if (existsSync(override)) return await import(pathToFileURL(override).href);
1257
+ return import("./portability-CES1YC2m.js").then((n) => n.s);
1258
+ }
1259
+ async function portability(context, operation, args, operationLock) {
1260
+ const adapter = await loadPortabilityAdapter(context);
1261
+ const runtime = await context.deps.resolveRuntime(context.sourceRoot);
1262
+ const environment = runtimeEnvironment(context, runtime);
1263
+ const requestedPath = args[0] ? resolve(args[0]) : void 0;
1264
+ const artifactPath = requestedPath ? assertExternalArtifactPath({
1265
+ sourceRoot: context.sourceRoot,
1266
+ path: requestedPath,
1267
+ label: operation === "export" ? "Export destination" : "Import source"
1268
+ }) : requestedPath;
1269
+ if (runtime.profile === "local") environment.paths = await context.deps.runtime.validateLocalDatabaseStorage({
1270
+ appId: context.appId,
1271
+ dataDirectory: process.env.SMRT_DATA_DIR,
1272
+ sourceRoot: context.sourceRoot
1273
+ });
1274
+ const portabilityContext = {
1275
+ appId: context.appId,
1276
+ sourceRoot: context.sourceRoot,
1277
+ stateRoot: preparedStateRoot(context),
1278
+ runtime,
1279
+ env: environment.env,
1280
+ paths: environment.paths,
1281
+ assetRoot: environment.assetRoot
1282
+ };
1283
+ let operatorLease = null;
1284
+ if (operation === "import") {
1285
+ if (runtime.profile === "local") operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: operationLock?.instance });
1286
+ 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.");
1287
+ }
1288
+ try {
1289
+ const result = await adapter[operation === "export" ? "exportApplication" : "importApplication"]({
1290
+ ...portabilityContext,
1291
+ path: artifactPath
1292
+ });
1293
+ printJson$1(context, {
1294
+ schemaVersion: 1,
1295
+ status: `${operation}ed`,
1296
+ ...result
1297
+ }, true);
1298
+ } finally {
1299
+ operatorLease?.release();
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Run one application operation. Resolves to the process exit code; throws
1304
+ * on failure (the caller renders the error envelope).
1305
+ */
1306
+ async function runApplicationOperation(context, operation, args) {
1307
+ switch (operation) {
1308
+ case "install":
1309
+ await assertLocalOperation(context, "app:install");
1310
+ await withOperationLock(preparedStateRoot(context), "install", async (lock) => {
1311
+ const report = await setup(context, lock);
1312
+ await start(context, lock);
1313
+ const baseUrl = `http://127.0.0.1:${process.env.PORT || "5173"}/`;
1314
+ if (report.onboardingUrl) saveOnboardingLaunch(context, report.onboardingUrl);
1315
+ context.deps.openBrowser(report.onboardingUrl ? pathToFileURL(onboardingLaunchPath(context)).toString() : baseUrl);
1316
+ });
1317
+ return 0;
1318
+ case "setup":
1319
+ await withOperationLock(preparedStateRoot(context), operation, async (lock) => {
1320
+ printOnboardingUrl(context, (await setup(context, lock)).onboardingUrl);
1321
+ });
1322
+ return 0;
1323
+ case "recover":
1324
+ await assertLocalOperation(context, "app:recover");
1325
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => recoverOnboarding(context, lock));
1326
+ return 0;
1327
+ case "start":
1328
+ await assertLocalOperation(context, "app:start");
1329
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => start(context, lock));
1330
+ return 0;
1331
+ case "doctor": return doctor(context);
1332
+ case "open":
1333
+ await open(context);
1334
+ return 0;
1335
+ case "stop":
1336
+ await assertLocalOperation(context, "app:stop");
1337
+ await withOperationLock(preparedStateRoot(context), operation, () => stop(context));
1338
+ return 0;
1339
+ case "backup":
1340
+ await backup(context, args);
1341
+ return 0;
1342
+ case "export":
1343
+ case "import":
1344
+ await withOperationLock(preparedStateRoot(context), operation, (lock) => portability(context, operation, args, lock));
1345
+ return 0;
1346
+ }
1347
+ }
1348
+ //#endregion
1349
+ //#region src/app/tokens.ts
1350
+ /**
1351
+ * `smrt app token` — owner-minted bearer tokens for local MCP clients.
1352
+ *
1353
+ * ```
1354
+ * smrt app token --scopes notes.read [--expires 30d] [--label "Claude Desktop"]
1355
+ * smrt app token list
1356
+ * smrt app token revoke <id>
1357
+ * ```
1358
+ *
1359
+ * Local profile only. The token is printed once, in the `issued` JSON
1360
+ * document on stdout, and is otherwise never written anywhere: the runtime
1361
+ * stores only its HMAC. `list` and `revoke` output carries no token values.
1362
+ */
1363
+ /** Usage text for `smrt app token`. */
1364
+ var TOKEN_USAGE = "Usage: smrt app token [create] --scopes <scope[,scope...]> [--expires <n>s|m|h|d] [--label <text>] | token list | token revoke <id>";
1365
+ var UNIT_SECONDS = {
1366
+ s: 1,
1367
+ m: 60,
1368
+ h: 3600,
1369
+ d: 1440 * 60
1370
+ };
1371
+ /** Parse `30d`, `12h`, `90m`, `3600s` or bare seconds. */
1372
+ function parseTokenLifetime(value) {
1373
+ const match = /^([1-9][0-9]{0,8})([smhd]?)$/u.exec(value.trim());
1374
+ if (!match) throw new Error(`Invalid --expires value. ${TOKEN_USAGE}`);
1375
+ return Number(match[1]) * UNIT_SECONDS[match[2] || "s"];
1376
+ }
1377
+ function parseCreateArgs(args) {
1378
+ const scopes = [];
1379
+ let expiresInSeconds;
1380
+ let label;
1381
+ for (let index = 0; index < args.length; index += 1) {
1382
+ const arg = args[index];
1383
+ const equals = arg.indexOf("=");
1384
+ const name = equals > 0 ? arg.slice(0, equals) : arg;
1385
+ if (![
1386
+ "--scopes",
1387
+ "--scope",
1388
+ "--expires",
1389
+ "--label"
1390
+ ].includes(name)) throw new Error(`Unknown token option: ${name}. ${TOKEN_USAGE}`);
1391
+ const value = equals > 0 ? arg.slice(equals + 1) : args[++index];
1392
+ if (value === void 0 || equals < 0 && value.startsWith("--")) throw new Error(`Token option ${name} requires a value. ${TOKEN_USAGE}`);
1393
+ if (name === "--scopes" || name === "--scope") scopes.push(...value.split(/[\s,]+/u).filter(Boolean));
1394
+ else if (name === "--expires") {
1395
+ if (expiresInSeconds !== void 0) throw new Error("Token option --expires was provided more than once.");
1396
+ expiresInSeconds = parseTokenLifetime(value);
1397
+ } else {
1398
+ if (label !== void 0) throw new Error("Token option --label was provided more than once.");
1399
+ label = value;
1400
+ }
1401
+ }
1402
+ if (scopes.length === 0) throw new Error(`At least one scope is required. ${TOKEN_USAGE}`);
1403
+ return {
1404
+ scopes,
1405
+ expiresInSeconds,
1406
+ label
1407
+ };
1408
+ }
1409
+ function printJson(context, value) {
1410
+ context.io.stdout(`${JSON.stringify(value, null, 2)}\n`);
1411
+ }
1412
+ /** Run `smrt app token <args…>` and resolve to the exit code. */
1413
+ async function runTokenOperation(context, args) {
1414
+ const [first, ...rest] = args;
1415
+ if (first === "help" || first === "--help" || first === "-h") {
1416
+ context.io.stdout(`${TOKEN_USAGE}\n`);
1417
+ return 0;
1418
+ }
1419
+ const subcommand = first === void 0 || first.startsWith("--") ? "create" : first;
1420
+ const subArgs = subcommand === first ? rest : args;
1421
+ if (![
1422
+ "create",
1423
+ "list",
1424
+ "revoke"
1425
+ ].includes(subcommand)) throw new Error(`Unknown token operation: ${subcommand}. ${TOKEN_USAGE}`);
1426
+ const create = subcommand === "create" ? parseCreateArgs(subArgs) : null;
1427
+ if (subcommand === "list" && subArgs.length > 0) throw new Error(`token list takes no arguments. ${TOKEN_USAGE}`);
1428
+ if (subcommand === "revoke" && subArgs.length !== 1) throw new Error(`token revoke takes exactly one token id. ${TOKEN_USAGE}`);
1429
+ await assertLocalOperation(context, "app:token");
1430
+ const open = context.deps.runtime.openLocalMcpTokenStore ?? openLocalMcpTokenStore;
1431
+ const withStore = async (fn) => {
1432
+ const store = await open({
1433
+ appId: context.appId,
1434
+ dataDirectory: process.env.SMRT_DATA_DIR,
1435
+ sourceRoot: context.sourceRoot
1436
+ });
1437
+ try {
1438
+ return await fn(store);
1439
+ } finally {
1440
+ await store.close();
1441
+ }
1442
+ };
1443
+ if (subcommand === "list") {
1444
+ printJson(context, {
1445
+ schemaVersion: 1,
1446
+ status: "ok",
1447
+ tokens: await withStore((store) => store.list()),
1448
+ secretValuesIncluded: false
1449
+ });
1450
+ return 0;
1451
+ }
1452
+ return withOperationLock(preparedStateRoot(context), `token-${subcommand}`, async () => {
1453
+ if (create) {
1454
+ const issued = await withStore((store) => store.issue(create));
1455
+ printJson(context, {
1456
+ schemaVersion: 1,
1457
+ status: "issued",
1458
+ id: issued.id,
1459
+ token: issued.token,
1460
+ scopes: issued.scopes,
1461
+ label: issued.label,
1462
+ createdAt: issued.createdAt,
1463
+ expiresAt: issued.expiresAt,
1464
+ notice: "This token is shown once and only its hash is stored. Put it in your MCP client configuration now; revoke it with smrt app token revoke <id>.",
1465
+ secretValuesIncluded: true
1466
+ });
1467
+ return 0;
1468
+ }
1469
+ const id = subArgs[0];
1470
+ printJson(context, {
1471
+ schemaVersion: 1,
1472
+ status: await withStore((store) => store.revoke(id)),
1473
+ id,
1474
+ secretValuesIncluded: false
1475
+ });
1476
+ return 0;
1477
+ });
1478
+ }
1479
+ //#endregion
1480
+ //#region src/app/cli.ts
1481
+ /**
1482
+ * `smrt app <operation>` dispatcher.
1483
+ *
1484
+ * Every operation writes machine-readable JSON to stdout and, on failure,
1485
+ * one secret-free JSON envelope to stderr with exit code 1 — the contract
1486
+ * the template's `scripts/smrt-app.mjs` established.
1487
+ */
1488
+ var cli_exports = /* @__PURE__ */ __exportAll({
1489
+ APP_COMMANDS: () => APP_COMMANDS,
1490
+ APP_RECOVERY: () => APP_RECOVERY,
1491
+ errorEnvelope: () => errorEnvelope,
1492
+ renderAppHelp: () => renderAppHelp,
1493
+ runAppCommand: () => runAppCommand
1494
+ });
1495
+ /** Recovery line carried by every error envelope. */
1496
+ var APP_RECOVERY = "Run pnpm app:doctor and follow its recovery instructions.";
1497
+ /** Every `smrt app` command with a one-line description (for help output). */
1498
+ var APP_COMMANDS = Object.freeze({
1499
+ install: "Local: set up, start, and open owner onboarding in one step",
1500
+ setup: "Build, apply migrations explicitly, and prepare owner onboarding",
1501
+ recover: "Local: issue a fresh single-use owner onboarding invitation",
1502
+ start: "Local: start the production build on loopback and prove readiness",
1503
+ stop: "Local: stop the managed application process",
1504
+ doctor: "Report runtime, storage, provider, and migration findings (default)",
1505
+ open: "Open the application or pending owner onboarding in a browser",
1506
+ "backup [destination]": "Local: copy the private data root to a new directory",
1507
+ "export [path]": "Write a logical, asset-aware export bundle",
1508
+ "import <path>": "Import a logical export bundle into an empty application",
1509
+ migrate: "Prepare storage custody and run smrt db:migrate under the lock",
1510
+ "token [list|revoke <id>] [--scopes ...]": "Local: issue, list, or revoke a scoped bearer token for a local MCP client",
1511
+ "worker [task|schedule]": "Deployed: run the task or schedule worker",
1512
+ "dev [...vite args]": "Run the application's Vite dev server",
1513
+ "build [...vite args]": "Validate ./mcp-apps when present, then run the Vite build",
1514
+ "vite [...args]": "Run the application's installed Vite CLI with args"
1515
+ });
1516
+ /** Build the stderr envelope for `error`, redacting secret material. */
1517
+ function errorEnvelope(error) {
1518
+ const runtimeCode = error instanceof Error && error.name === "LocalRuntimeError" && typeof error.code === "string" ? error.code : void 0;
1519
+ return {
1520
+ schemaVersion: 1,
1521
+ status: "error",
1522
+ code: "operation-failed",
1523
+ ...runtimeCode ? { runtimeCode } : {},
1524
+ message: redactSecrets(error instanceof Error ? error.message : "Application operation failed."),
1525
+ ...error instanceof ApplicationStartError ? {
1526
+ output: boundedTail(redactSecrets(error.output, process.env, { strict: true }), START_OUTPUT_TAIL_BYTES),
1527
+ logFile: error.logFile
1528
+ } : {},
1529
+ recovery: APP_RECOVERY,
1530
+ secretValuesIncluded: false
1531
+ };
1532
+ }
1533
+ /** Render `smrt app` usage. */
1534
+ function renderAppHelp() {
1535
+ const width = Math.max(...Object.keys(APP_COMMANDS).map((key) => key.length));
1536
+ return `Usage: smrt app <operation> [args]\n\nOperations:\n${Object.entries(APP_COMMANDS).map(([name, description]) => ` ${name.padEnd(width)} ${description}`).join("\n")}\n`;
1537
+ }
1538
+ var ENV_FILE_OPERATIONS = /* @__PURE__ */ new Set([
1539
+ ...APP_OPERATIONS,
1540
+ "migrate",
1541
+ "token",
1542
+ "dev",
1543
+ "build",
1544
+ "vite"
1545
+ ]);
1546
+ /**
1547
+ * Run `smrt app <argv…>` and resolve to the process exit code.
1548
+ *
1549
+ * `argv` is everything after `app`. A leading `--` before operation
1550
+ * arguments is ignored, matching `pnpm app:import -- <path>`.
1551
+ */
1552
+ async function runAppCommand(argv, options = {}) {
1553
+ const io = {
1554
+ stdout: options.io?.stdout ?? ((text) => void process.stdout.write(text)),
1555
+ stderr: options.io?.stderr ?? ((text) => void process.stderr.write(text)),
1556
+ operatorTerminal: options.io ? options.io.operatorTerminal : process.stderr.isTTY && process.stdout.isTTY ? (text) => void process.stderr.write(text) : void 0
1557
+ };
1558
+ const operation = argv[0] || "doctor";
1559
+ const rawArgs = argv.slice(1);
1560
+ const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
1561
+ if (operation === "help" || operation === "--help" || operation === "-h") {
1562
+ io.stdout(renderAppHelp());
1563
+ return 0;
1564
+ }
1565
+ try {
1566
+ const sourceRoot = resolve(options.cwd ?? process.cwd());
1567
+ if (ENV_FILE_OPERATIONS.has(operation)) loadSourceEnvironment(sourceRoot);
1568
+ if (operation === "dev" || operation === "build" || operation === "vite") {
1569
+ const context = {
1570
+ sourceRoot,
1571
+ appId: "",
1572
+ io,
1573
+ deps: {}
1574
+ };
1575
+ if (operation === "build") {
1576
+ const status = await validateMcpAppsIfPresent(context);
1577
+ if (status !== 0) return status;
1578
+ }
1579
+ return launchVite(context, operation === "vite" ? args : operation === "dev" ? ["dev", ...devServerArguments(args)] : [operation, ...args]);
1580
+ }
1581
+ const deps = {
1582
+ ...await defaultDependencies(sourceRoot),
1583
+ ...options.dependencies
1584
+ };
1585
+ const context = {
1586
+ sourceRoot,
1587
+ appId: resolveContextApplicationId(sourceRoot),
1588
+ io,
1589
+ deps
1590
+ };
1591
+ if (operation === "migrate") return await prepareMigration(context);
1592
+ if (operation === "worker") return await runWorker(context, args);
1593
+ if (operation === "token") return await runTokenOperation(context, args);
1594
+ if (!APP_OPERATIONS.includes(operation)) throw new Error(`Unknown app operation: ${operation}`);
1595
+ return await runApplicationOperation(context, operation, args);
1596
+ } catch (error) {
1597
+ io.stderr(`${JSON.stringify(errorEnvelope(error))}\n`);
1598
+ return 1;
1599
+ }
1600
+ }
1601
+ //#endregion
1602
+ export { assertExternalArtifactPath as A, prepareMigration as C, cliExecutablePath as D, validateMcpAppsIfPresent as E, AppCommandError as F, ApplicationStartError as I, errorCode as L, resolveApplicationId as M, resolveApplicationStateRoot as N, runtimeEnvironment as O, runtimeConfigurationFingerprint as P, redactSecrets as R, launchVite as S, runWorker as T, sendTerminationSignal as _, renderAppHelp as a, ViteLauncherError as b, parseTokenLifetime as c, START_OUTPUT_TAIL_BYTES as d, runApplicationOperation as f, readOwnedProcess as g, matchesApplicationProcess as h, errorEnvelope as i, prepareApplicationStateRoot as j, webLauncherPath as k, runTokenOperation as l, checkOwnedProcess as m, APP_RECOVERY as n, runAppCommand as o, WEB_LAUNCHER_NAME as p, cli_exports as r, TOKEN_USAGE as s, APP_COMMANDS as t, APP_OPERATIONS as u, verifyOwnedProcess as v, resolveViteEntry as w, devServerArguments as x, writeProcessRecord as y };