@happyvertical/smrt-cli 0.53.4 → 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 (64) hide show
  1. package/AGENTS.md +2 -0
  2. package/README.md +24 -0
  3. package/agents/app-commands.md +28 -3
  4. package/dist/{ApiKey-BZmbOTnL-DPPF6Swn.js → ApiKey-BZmbOTnL-BbZupUZ3.js} +2 -2
  5. package/dist/ApiKeyCollection-Bfb_nw81-DYAZwx0O.js +87 -0
  6. package/dist/AuditLogCollection-B1YonCAP-Bkj2Ezon.js +193 -0
  7. package/dist/{NostrIdentity-n70E0vck-D60BSg6H.js → NostrIdentity-n70E0vck-Di3_eNCq.js} +4 -4
  8. package/dist/NostrIdentityCollection-Ca74dkaL-DvjOg9Xy.js +127 -0
  9. package/dist/{OidcIdentity-Cb1bxreM-B2eV2ahq.js → OidcIdentity-Cb1bxreM-LXmC7uAf.js} +2 -2
  10. package/dist/OidcIdentityCollection-TV8VdK69-Cv1ZTeuk.js +86 -0
  11. package/dist/ProfileAssetCollection-B9hacUJa-CPuaWLk4.js +101 -0
  12. package/dist/{ProfileCollection-DuJT7Mum-B3Uhqphs.js → ProfileCollection-DuJT7Mum-nXbHrPpX.js} +31 -31
  13. package/dist/ProfileLinkCollection-4uNURz32-CbKZYmYm.js +233 -0
  14. package/dist/ProfileMetadataCollection-DQhbfEfa-_1rQCLTR.js +108 -0
  15. package/dist/ProfileMetafieldCollection-CoBuM_5G-CPjW3l5r.js +145 -0
  16. package/dist/ProfileRelationshipCollection-DUVgaIqf-RCDqYz6O.js +161 -0
  17. package/dist/ProfileRelationshipTermCollection-DtA9OM23-BoGhQBSQ.js +107 -0
  18. package/dist/{ProfileRelationshipType-BRiRka6t-Ci-qvFhm.js → ProfileRelationshipType-BRiRka6t-Dezjv5jP.js} +1 -1
  19. package/dist/ProfileRelationshipTypeCollection-BdXwd7Kf-HoTmV1q-.js +57 -0
  20. package/dist/app/cli.d.ts +7 -0
  21. package/dist/app/cli.d.ts.map +1 -1
  22. package/dist/app/errors.d.ts +34 -1
  23. package/dist/app/errors.d.ts.map +1 -1
  24. package/dist/app/index.d.ts +4 -3
  25. package/dist/app/index.d.ts.map +1 -1
  26. package/dist/app/launchers.d.ts +11 -0
  27. package/dist/app/launchers.d.ts.map +1 -1
  28. package/dist/app/operations.d.ts +2 -0
  29. package/dist/app/operations.d.ts.map +1 -1
  30. package/dist/app/runtime.d.ts +17 -2
  31. package/dist/app/runtime.d.ts.map +1 -1
  32. package/dist/app/tokens.d.ts +21 -0
  33. package/dist/app/tokens.d.ts.map +1 -0
  34. package/dist/app.js +3 -3
  35. package/dist/{cli-BRFsobey.js → cli-OZsH5cdL.js} +403 -76
  36. package/dist/cli-generator.d.ts.map +1 -1
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/utilities.d.ts.map +1 -1
  39. package/dist/{commands-dFz1e9A6.js → commands-UXK2F_0j.js} +35 -51
  40. package/dist/database-environment-D_Gk1Brf.js +20 -0
  41. package/dist/database-environment.d.ts +43 -0
  42. package/dist/database-environment.d.ts.map +1 -0
  43. package/dist/dist-BVjJWjT-.js +20 -0
  44. package/dist/{dist-EJ63xUEp.js → dist-C87Ib9Vx.js} +2 -2
  45. package/dist/{dist-C2BqYVu7.js → dist-DLw_E34G.js} +1654 -1748
  46. package/dist/{dist-Berwe80z.js → dist-GIaWaUho.js} +4 -4
  47. package/dist/{dist-D4BNILLN.js → dist-RSE5hCY4.js} +3 -3
  48. package/dist/index.js +15 -10
  49. package/dist/{oidcProvisioningPrimitives-D9OYu5-F-B-kYsBTM.js → oidcProvisioningPrimitives-D9OYu5-F-T9KkkDCd.js} +5 -5
  50. package/dist/{portability-DmQWSSXA.js → portability-CES1YC2m.js} +1 -1
  51. package/dist/resolveIdentity-B-PK6RwU-C9ogf8YG.js +269 -0
  52. package/package.json +15 -15
  53. package/dist/ApiKeyCollection-Bfb_nw81-B1-2YKj8.js +0 -2
  54. package/dist/AuditLogCollection-B1YonCAP-DP4PCCw-.js +0 -2
  55. package/dist/NostrIdentityCollection-Ca74dkaL-C-v57o9N.js +0 -2
  56. package/dist/OidcIdentityCollection-TV8VdK69-BodvOtPJ.js +0 -2
  57. package/dist/ProfileAssetCollection-B9hacUJa-B7PLe0ld.js +0 -2
  58. package/dist/ProfileLinkCollection-4uNURz32-CLY1cBnn.js +0 -2
  59. package/dist/ProfileMetadataCollection-DQhbfEfa-Bzv-E4ep.js +0 -2
  60. package/dist/ProfileMetafieldCollection-CoBuM_5G-D-VqQuY1.js +0 -2
  61. package/dist/ProfileRelationshipCollection-DUVgaIqf-gEkDBmkh.js +0 -2
  62. package/dist/ProfileRelationshipTermCollection-DtA9OM23-DI_awdol.js +0 -2
  63. package/dist/ProfileRelationshipTypeCollection-BdXwd7Kf-CaQMLH9f.js +0 -2
  64. package/dist/resolveIdentity-B-PK6RwU-BmAU1Kse.js +0 -2
@@ -1,8 +1,8 @@
1
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";
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
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { acquireWriterLease, createProviderReadinessProbe, findInstalledPackage, prepareApplicationStateRoot, readActiveWriterLease, resolveApplicationId, resolveApplicationStateRoot, runtimeConfigurationFingerprint, withOperationLock } from "@happyvertical/smrt-app-runtime";
5
+ import { acquireWriterLease, createProviderReadinessProbe, findInstalledPackage, openLocalMcpTokenStore, prepareApplicationStateRoot, readActiveWriterLease, resolveApplicationId, resolveApplicationStateRoot, runtimeConfigurationFingerprint, withOperationLock } from "@happyvertical/smrt-app-runtime";
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { platform } from "node:os";
8
8
  import { randomBytes } from "node:crypto";
@@ -27,10 +27,81 @@ var AppCommandError = class extends Error {
27
27
  });
28
28
  }
29
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]";
30
47
  /** Minimum length before an environment value is treated as redactable. */
31
48
  var MIN_REDACTED_VALUE_LENGTH = 8;
32
49
  /** Environment names whose values must never reach operator output. */
33
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
+ }
34
105
  /**
35
106
  * Remove secret material from an operator-facing message.
36
107
  *
@@ -38,12 +109,37 @@ var SECRET_ENVIRONMENT_NAME = /(?:^|_)(?:DATABASE_URL|URL|DSN|TOKEN|SECRET|PASSW
38
109
  * connection string, credential, or bootstrap token. Redaction covers:
39
110
  * literal values of secret-named environment variables, URL userinfo,
40
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.
41
118
  */
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]");
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;
47
143
  }
48
144
  //#endregion
49
145
  //#region src/app/identity.ts
@@ -162,14 +258,22 @@ function createBrowserOpener(sourceRoot) {
162
258
  runChild(sourceRoot, binary, args);
163
259
  };
164
260
  }
165
- /** Default runtime resolver: reload `smrt.config` without the cache. */
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
+ */
166
271
  async function resolveConfiguredRuntime(sourceRoot) {
167
- const { loadConfig, resolveConfiguredApplicationRuntime } = await import("@happyvertical/smrt-config");
168
- await loadConfig({
272
+ const { loadConfig, resolveEffectiveApplicationRuntime } = await import("@happyvertical/smrt-config");
273
+ return resolveEffectiveApplicationRuntime(await loadConfig({
169
274
  cache: false,
170
275
  searchFrom: sourceRoot
171
- });
172
- return resolveConfiguredApplicationRuntime();
276
+ }));
173
277
  }
174
278
  /** Build the default dependency set for `sourceRoot`. */
175
279
  async function defaultDependencies(sourceRoot) {
@@ -391,6 +495,23 @@ function resolveViteEntry(sourceRoot) {
391
495
  if (!existsSync(entry)) throw new ViteLauncherError("The installed Vite package CLI is missing; reinstall dependencies.");
392
496
  return entry;
393
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
+ }
394
515
  /**
395
516
  * Run the application's Vite CLI in this process with `args` verbatim. The
396
517
  * caller has already loaded `.env` without overriding the shell.
@@ -576,9 +697,14 @@ var APP_OPERATIONS = [
576
697
  "export",
577
698
  "import"
578
699
  ];
579
- function printJson(context, value, pretty = false) {
700
+ function printJson$1(context, value, pretty = false) {
580
701
  context.io.stdout(`${pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)}\n`);
581
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
+ }
582
708
  function ensurePrivateDirectory(path) {
583
709
  mkdirSync(path, {
584
710
  recursive: true,
@@ -710,7 +836,7 @@ async function setup(context, operationLock) {
710
836
  onboardingRecovery: onboardingAvailable ? "pnpm app:open" : null,
711
837
  secretValuesIncluded: false
712
838
  };
713
- printJson(context, report, true);
839
+ printJson$1(context, report, true);
714
840
  return {
715
841
  ...report,
716
842
  onboardingUrl: retainedOnboardingUrl
@@ -728,15 +854,17 @@ async function recoverOnboarding(context, operationLock) {
728
854
  const initialized = await initializeLocal(context, runtime, env);
729
855
  if (!initialized) throw new Error("Local runtime initialization failed.");
730
856
  const invitation = await initialized.runtime.rotateBootstrapInvitation();
731
- saveOnboardingUrl(context, `http://127.0.0.1:${env.PORT || "5173"}/setup?token=${encodeURIComponent(invitation.token)}`);
857
+ const url = `http://127.0.0.1:${env.PORT || "5173"}/setup?token=${encodeURIComponent(invitation.token)}`;
858
+ saveOnboardingUrl(context, url);
732
859
  await initialized.runtime.db.close?.();
733
- printJson(context, {
860
+ printJson$1(context, {
734
861
  schemaVersion: 1,
735
862
  status: "ready",
736
863
  onboardingAvailable: true,
737
864
  recovery: "Run pnpm app:start, then pnpm app:open.",
738
865
  secretValuesIncluded: false
739
866
  });
867
+ printOnboardingUrl(context, url);
740
868
  } finally {
741
869
  operatorLease.release();
742
870
  }
@@ -778,7 +906,7 @@ async function start(context, operationLock) {
778
906
  const existing = readOwnedProcess(pidPath(context));
779
907
  if (existing) {
780
908
  await waitForReady(context, url, existing.pid, existing.instance, configuration);
781
- printJson(context, {
909
+ printJson$1(context, {
782
910
  schemaVersion: 1,
783
911
  status: "running",
784
912
  pid: existing.pid
@@ -787,18 +915,30 @@ async function start(context, operationLock) {
787
915
  }
788
916
  ensurePrivateDirectory(preparedStateRoot(context));
789
917
  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
- });
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
+ }
802
942
  child.unref();
803
943
  const pid = child.pid;
804
944
  if (pid === void 0) throw new Error("The application process could not be started.");
@@ -811,15 +951,60 @@ async function start(context, operationLock) {
811
951
  } catch (error) {
812
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.`);
813
953
  rmSync(pidPath(context), { force: true });
814
- throw error;
954
+ throw new ApplicationStartError(error instanceof Error ? error.message : "The application did not become ready.", redactedLogTail(logFile, childEnv), logFile);
815
955
  }
816
- printJson(context, {
956
+ printJson$1(context, {
817
957
  schemaVersion: 1,
818
958
  status: "started",
819
959
  pid
820
960
  });
821
961
  return pid;
822
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
+ }
823
1008
  /**
824
1009
  * Terminate a launcher that failed to prove readiness: SIGTERM, wait, then
825
1010
  * SIGKILL, wait. Resolves `true` only once the child has provably exited.
@@ -847,7 +1032,7 @@ async function stop(context) {
847
1032
  const pid = record?.pid || null;
848
1033
  if (!record || !pid) {
849
1034
  rmSync(pidPath(context), { force: true });
850
- printJson(context, {
1035
+ printJson$1(context, {
851
1036
  schemaVersion: 1,
852
1037
  status: "stopped"
853
1038
  });
@@ -857,7 +1042,7 @@ async function stop(context) {
857
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.`);
858
1043
  if (state !== "owned" || !sendTerminationSignal(pid)) {
859
1044
  rmSync(pidPath(context), { force: true });
860
- printJson(context, {
1045
+ printJson$1(context, {
861
1046
  schemaVersion: 1,
862
1047
  status: "stopped",
863
1048
  pid
@@ -874,7 +1059,7 @@ async function stop(context) {
874
1059
  if (attempt === 39) throw new Error(`Application process ${pid} did not stop cleanly.`);
875
1060
  }
876
1061
  rmSync(pidPath(context), { force: true });
877
- printJson(context, {
1062
+ printJson$1(context, {
878
1063
  schemaVersion: 1,
879
1064
  status: "stopped",
880
1065
  pid
@@ -887,7 +1072,7 @@ async function open(context) {
887
1072
  if (onboardingUrl) saveOnboardingLaunch(context, onboardingUrl);
888
1073
  const destination = onboardingUrl ? pathToFileURL(onboardingLaunchPath(context)).toString() : url;
889
1074
  context.deps.openBrowser(destination);
890
- printJson(context, {
1075
+ printJson$1(context, {
891
1076
  schemaVersion: 1,
892
1077
  status: "opened",
893
1078
  url
@@ -1009,10 +1194,10 @@ async function doctor(context) {
1009
1194
  findings,
1010
1195
  secretValuesIncluded: false
1011
1196
  };
1012
- printJson(context, report, true);
1197
+ printJson$1(context, report, true);
1013
1198
  return report.status === "error" ? 1 : 0;
1014
1199
  }
1015
- async function backup(context, args, operationLock) {
1200
+ async function backup(context, args) {
1016
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.");
1017
1202
  const explicitDestination = args[0] ? assertExternalArtifactPath({
1018
1203
  sourceRoot: context.sourceRoot,
@@ -1024,41 +1209,42 @@ async function backup(context, args, operationLock) {
1024
1209
  dataDirectory: process.env.SMRT_DATA_DIR,
1025
1210
  sourceRoot: context.sourceRoot
1026
1211
  });
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
- }
1212
+ await withOperationLock(preparedStateRoot(context), "backup", async (lock) => {
1213
+ const operatorLease = acquireWriterLease(preparedStateRoot(context), { operationInstance: lock.instance });
1041
1214
  try {
1042
- cpSync(paths.root, destination, {
1043
- recursive: true,
1044
- force: false,
1045
- errorOnExist: true
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"
1046
1219
  });
1047
- } catch (error) {
1048
- rmSync(destination, {
1049
- recursive: true,
1050
- force: true
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
1051
1243
  });
1052
- throw error;
1244
+ } finally {
1245
+ operatorLease.release();
1053
1246
  }
1054
- printJson(context, {
1055
- schemaVersion: 1,
1056
- status: "backed-up",
1057
- destination
1058
- });
1059
- } finally {
1060
- operatorLease.release();
1061
- }
1247
+ });
1062
1248
  }
1063
1249
  /**
1064
1250
  * The application's own adapter at `scripts/smrt-portability.mjs` keeps
@@ -1068,7 +1254,7 @@ async function backup(context, args, operationLock) {
1068
1254
  async function loadPortabilityAdapter(context) {
1069
1255
  const override = join(context.sourceRoot, "scripts", "smrt-portability.mjs");
1070
1256
  if (existsSync(override)) return await import(pathToFileURL(override).href);
1071
- return import("./portability-DmQWSSXA.js").then((n) => n.s);
1257
+ return import("./portability-CES1YC2m.js").then((n) => n.s);
1072
1258
  }
1073
1259
  async function portability(context, operation, args, operationLock) {
1074
1260
  const adapter = await loadPortabilityAdapter(context);
@@ -1104,7 +1290,7 @@ async function portability(context, operation, args, operationLock) {
1104
1290
  ...portabilityContext,
1105
1291
  path: artifactPath
1106
1292
  });
1107
- printJson(context, {
1293
+ printJson$1(context, {
1108
1294
  schemaVersion: 1,
1109
1295
  status: `${operation}ed`,
1110
1296
  ...result
@@ -1130,7 +1316,9 @@ async function runApplicationOperation(context, operation, args) {
1130
1316
  });
1131
1317
  return 0;
1132
1318
  case "setup":
1133
- await withOperationLock(preparedStateRoot(context), operation, (lock) => setup(context, lock));
1319
+ await withOperationLock(preparedStateRoot(context), operation, async (lock) => {
1320
+ printOnboardingUrl(context, (await setup(context, lock)).onboardingUrl);
1321
+ });
1134
1322
  return 0;
1135
1323
  case "recover":
1136
1324
  await assertLocalOperation(context, "app:recover");
@@ -1149,7 +1337,7 @@ async function runApplicationOperation(context, operation, args) {
1149
1337
  await withOperationLock(preparedStateRoot(context), operation, () => stop(context));
1150
1338
  return 0;
1151
1339
  case "backup":
1152
- await withOperationLock(preparedStateRoot(context), operation, (lock) => backup(context, args, lock));
1340
+ await backup(context, args);
1153
1341
  return 0;
1154
1342
  case "export":
1155
1343
  case "import":
@@ -1158,6 +1346,137 @@ async function runApplicationOperation(context, operation, args) {
1158
1346
  }
1159
1347
  }
1160
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
1161
1480
  //#region src/app/cli.ts
1162
1481
  /**
1163
1482
  * `smrt app <operation>` dispatcher.
@@ -1188,6 +1507,7 @@ var APP_COMMANDS = Object.freeze({
1188
1507
  "export [path]": "Write a logical, asset-aware export bundle",
1189
1508
  "import <path>": "Import a logical export bundle into an empty application",
1190
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",
1191
1511
  "worker [task|schedule]": "Deployed: run the task or schedule worker",
1192
1512
  "dev [...vite args]": "Run the application's Vite dev server",
1193
1513
  "build [...vite args]": "Validate ./mcp-apps when present, then run the Vite build",
@@ -1202,6 +1522,10 @@ function errorEnvelope(error) {
1202
1522
  code: "operation-failed",
1203
1523
  ...runtimeCode ? { runtimeCode } : {},
1204
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
+ } : {},
1205
1529
  recovery: APP_RECOVERY,
1206
1530
  secretValuesIncluded: false
1207
1531
  };
@@ -1214,6 +1538,7 @@ function renderAppHelp() {
1214
1538
  var ENV_FILE_OPERATIONS = /* @__PURE__ */ new Set([
1215
1539
  ...APP_OPERATIONS,
1216
1540
  "migrate",
1541
+ "token",
1217
1542
  "dev",
1218
1543
  "build",
1219
1544
  "vite"
@@ -1227,7 +1552,8 @@ var ENV_FILE_OPERATIONS = /* @__PURE__ */ new Set([
1227
1552
  async function runAppCommand(argv, options = {}) {
1228
1553
  const io = {
1229
1554
  stdout: options.io?.stdout ?? ((text) => void process.stdout.write(text)),
1230
- stderr: options.io?.stderr ?? ((text) => void process.stderr.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
1231
1557
  };
1232
1558
  const operation = argv[0] || "doctor";
1233
1559
  const rawArgs = argv.slice(1);
@@ -1250,7 +1576,7 @@ async function runAppCommand(argv, options = {}) {
1250
1576
  const status = await validateMcpAppsIfPresent(context);
1251
1577
  if (status !== 0) return status;
1252
1578
  }
1253
- return launchVite(context, operation === "vite" ? args : [operation, ...args]);
1579
+ return launchVite(context, operation === "vite" ? args : operation === "dev" ? ["dev", ...devServerArguments(args)] : [operation, ...args]);
1254
1580
  }
1255
1581
  const deps = {
1256
1582
  ...await defaultDependencies(sourceRoot),
@@ -1264,6 +1590,7 @@ async function runAppCommand(argv, options = {}) {
1264
1590
  };
1265
1591
  if (operation === "migrate") return await prepareMigration(context);
1266
1592
  if (operation === "worker") return await runWorker(context, args);
1593
+ if (operation === "token") return await runTokenOperation(context, args);
1267
1594
  if (!APP_OPERATIONS.includes(operation)) throw new Error(`Unknown app operation: ${operation}`);
1268
1595
  return await runApplicationOperation(context, operation, args);
1269
1596
  } catch (error) {
@@ -1272,4 +1599,4 @@ async function runAppCommand(argv, options = {}) {
1272
1599
  }
1273
1600
  }
1274
1601
  //#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 };
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 };