@mesh-tech/mesh-cli 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/assets/docs-preview.css +1599 -0
  2. package/dist/bin/mesh.auth-preflight-JFKL4Y4X.js +48 -0
  3. package/dist/bin/mesh.auth-preflight-JFKL4Y4X.js.map +6 -0
  4. package/dist/bin/mesh.capture-history-A4IAVVIR.js +38 -0
  5. package/dist/bin/mesh.capture-history-A4IAVVIR.js.map +6 -0
  6. package/dist/bin/mesh.chunk-5A5OFSYR.js +187 -0
  7. package/dist/bin/mesh.chunk-5A5OFSYR.js.map +6 -0
  8. package/dist/bin/mesh.chunk-7DMAFUBQ.js +7715 -0
  9. package/dist/bin/mesh.chunk-7DMAFUBQ.js.map +6 -0
  10. package/dist/bin/mesh.chunk-A4DGMGHF.js +303 -0
  11. package/dist/bin/mesh.chunk-A4DGMGHF.js.map +6 -0
  12. package/dist/bin/mesh.chunk-CQ2IC4OP.js +217 -0
  13. package/dist/bin/mesh.chunk-CQ2IC4OP.js.map +6 -0
  14. package/dist/bin/mesh.chunk-F4654EPX.js +383 -0
  15. package/dist/bin/mesh.chunk-F4654EPX.js.map +6 -0
  16. package/dist/bin/mesh.chunk-IQOBTL42.js +60 -0
  17. package/dist/bin/mesh.chunk-IQOBTL42.js.map +6 -0
  18. package/dist/bin/mesh.chunk-J6DVWKRW.js +647 -0
  19. package/dist/bin/mesh.chunk-J6DVWKRW.js.map +6 -0
  20. package/dist/bin/mesh.chunk-JB7FWYSP.js +1751 -0
  21. package/dist/bin/mesh.chunk-JB7FWYSP.js.map +6 -0
  22. package/dist/bin/mesh.chunk-MCBMHWXP.js +837 -0
  23. package/dist/bin/mesh.chunk-MCBMHWXP.js.map +6 -0
  24. package/dist/bin/mesh.chunk-OW5EMVG5.js +507 -0
  25. package/dist/bin/mesh.chunk-OW5EMVG5.js.map +6 -0
  26. package/dist/bin/mesh.chunk-R5VVTXG4.js +10092 -0
  27. package/dist/bin/mesh.chunk-R5VVTXG4.js.map +6 -0
  28. package/dist/bin/mesh.chunk-VKV6Z3UI.js +1146 -0
  29. package/dist/bin/mesh.chunk-VKV6Z3UI.js.map +6 -0
  30. package/dist/bin/mesh.chunk-WFFDF346.js +59 -0
  31. package/dist/bin/mesh.chunk-WFFDF346.js.map +6 -0
  32. package/dist/bin/mesh.chunk-WOJOTUFZ.js +835 -0
  33. package/dist/bin/mesh.chunk-WOJOTUFZ.js.map +6 -0
  34. package/dist/bin/mesh.cli-reference-H3TOPIET.js +158 -0
  35. package/dist/bin/mesh.cli-reference-H3TOPIET.js.map +6 -0
  36. package/dist/bin/mesh.convert-KK6ZP256.js +274 -0
  37. package/dist/bin/mesh.convert-KK6ZP256.js.map +6 -0
  38. package/dist/bin/mesh.create-app-A5F45PWZ.js +39 -0
  39. package/dist/bin/mesh.create-app-A5F45PWZ.js.map +6 -0
  40. package/dist/bin/mesh.dev-token-server-GYDYPXYH.js +61 -0
  41. package/dist/bin/mesh.dev-token-server-GYDYPXYH.js.map +6 -0
  42. package/dist/bin/mesh.js +20 -26418
  43. package/dist/bin/mesh.js.map +3 -3
  44. package/dist/bin/mesh.login-JNHPVMCT.js +91 -0
  45. package/dist/bin/mesh.login-JNHPVMCT.js.map +6 -0
  46. package/dist/bin/mesh.platform-setup-EB64EFRN.js +37 -0
  47. package/dist/bin/mesh.platform-setup-EB64EFRN.js.map +6 -0
  48. package/dist/bin/mesh.preview-3IP34TDO.js +2410 -0
  49. package/dist/bin/mesh.preview-3IP34TDO.js.map +6 -0
  50. package/dist/bin/mesh.program-DWOKHNMQ.js +23 -0
  51. package/dist/bin/mesh.program-DWOKHNMQ.js.map +6 -0
  52. package/dist/bin/mesh.registry-OXF57OQW.js +47 -0
  53. package/dist/bin/mesh.registry-OXF57OQW.js.map +6 -0
  54. package/dist/bin/mesh.registry-identity-2HSGQGJC.js +42 -0
  55. package/dist/bin/mesh.registry-identity-2HSGQGJC.js.map +6 -0
  56. package/dist/bin/mesh.skills-MNDSMBIE.js +23 -0
  57. package/dist/bin/mesh.skills-MNDSMBIE.js.map +6 -0
  58. package/dist/bin/mesh.src-5D5DFSLX.js +1302 -0
  59. package/dist/bin/mesh.src-5D5DFSLX.js.map +6 -0
  60. package/dist/bin/mesh.src-G7TJBLNJ.js +40 -0
  61. package/dist/bin/mesh.src-G7TJBLNJ.js.map +6 -0
  62. package/dist/bin/mesh.stack-EFTFAOCV.js +64 -0
  63. package/dist/bin/mesh.stack-EFTFAOCV.js.map +6 -0
  64. package/dist/bin/mesh.start-ARIIY6CV.js +454 -0
  65. package/dist/bin/mesh.start-ARIIY6CV.js.map +6 -0
  66. package/dist/bin/mesh.wizard-ROAQAGSF.js +500 -0
  67. package/dist/bin/mesh.wizard-ROAQAGSF.js.map +6 -0
  68. package/dist/build-info.json +2 -2
  69. package/dist/src/commands/app-check.d.ts +2 -1
  70. package/dist/src/commands/app-check.js +32 -0
  71. package/dist/src/commands/dev.d.ts +6 -0
  72. package/dist/src/commands/dev.js +152 -3
  73. package/dist/src/commands/docs.js +33 -0
  74. package/dist/src/commands/local/act-as-local.d.ts +5 -0
  75. package/dist/src/commands/local/act-as-local.js +41 -0
  76. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  77. package/dist/src/commands/local/auth-provision.js +1 -0
  78. package/dist/src/commands/local/index.js +4 -0
  79. package/dist/src/commands/local/local-api-key.d.ts +44 -0
  80. package/dist/src/commands/local/local-api-key.js +115 -0
  81. package/dist/src/commands/local/local-spicedb.d.ts +95 -0
  82. package/dist/src/commands/local/local-spicedb.js +304 -0
  83. package/dist/src/commands/local/publish-docs.d.ts +81 -0
  84. package/dist/src/commands/local/publish-docs.js +328 -0
  85. package/dist/src/commands/local/seed-zitadel.js +2 -0
  86. package/dist/src/commands/temporal.js +191 -61
  87. package/dist/src/commands/temporal.js.map +3 -3
  88. package/dist/src/docs/convert.d.ts +35 -0
  89. package/dist/src/docs/convert.js +276 -0
  90. package/dist/src/docs/preview-source.d.ts +13 -0
  91. package/dist/src/docs/preview-source.js +58 -0
  92. package/dist/src/docs/preview.d.ts +32 -0
  93. package/dist/src/docs/preview.js +279 -0
  94. package/fragments/agents/AGENTS.md.hbs +27 -27
  95. package/fragments/base/README.md.hbs +5 -2
  96. package/fragments/base/index.ts.hbs +17 -30
  97. package/fragments/base/package.json.hbs +4 -1
  98. package/fragments/service/api/openapi.json.hbs +1 -1
  99. package/fragments/service/docs/docs.yaml.hbs +10 -0
  100. package/fragments/service/docs/pages/getting-started.md.hbs +6 -9
  101. package/package.json +19 -3
  102. package/skills/mesh/SKILL.md +2 -2
  103. package/stack/docker-compose.hub.yml +6 -0
@@ -0,0 +1,1751 @@
1
+ import {
2
+ INTENT_RANGE,
3
+ resolveTargetRoot,
4
+ seedAppSkill,
5
+ syncSkills
6
+ } from "./mesh.chunk-F4654EPX.js";
7
+ import {
8
+ ICONS,
9
+ aggregateStatus,
10
+ findMeshJson,
11
+ jsonReport,
12
+ renderHuman,
13
+ runChecks
14
+ } from "./mesh.chunk-R5VVTXG4.js";
15
+ import {
16
+ findPackageRoot
17
+ } from "./mesh.chunk-MCBMHWXP.js";
18
+ import {
19
+ logError,
20
+ logInfo,
21
+ logSuccess,
22
+ logWarn
23
+ } from "./mesh.chunk-WFFDF346.js";
24
+ import {
25
+ MeshCliError,
26
+ emitJsonPayload
27
+ } from "./mesh.chunk-IQOBTL42.js";
28
+ import {
29
+ registryPreflight
30
+ } from "./mesh.chunk-5A5OFSYR.js";
31
+
32
+ // packages/mesh-cli/src/commands/create-app.ts
33
+ import * as fs3 from "fs";
34
+ import * as os from "os";
35
+ import * as path3 from "path";
36
+ import { fileURLToPath } from "url";
37
+ import Handlebars from "handlebars";
38
+ import { parse as parseYaml2 } from "yaml";
39
+
40
+ // packages/mesh-cli/src/commands/app-check.ts
41
+ import * as fs2 from "node:fs";
42
+ import * as path2 from "node:path";
43
+
44
+ // packages/mesh-cli/src/commands/app-check-resolve.ts
45
+ import * as fs from "node:fs";
46
+ import * as path from "node:path";
47
+ import { builtinModules } from "node:module";
48
+ import { parse as parseYaml } from "yaml";
49
+ var CONDITIONS = /* @__PURE__ */ new Set(["source", "node", "import", "require", "module", "module-sync", "default"]);
50
+ var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".mjs", ".cjs", "/index.ts", "/index.js", "/index.mjs"];
51
+ var SCOPE = "@mesh-tech/";
52
+ var FILE_BUDGET = 1500;
53
+ var BUILTINS = new Set(builtinModules);
54
+ function stripComments(source) {
55
+ return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`\\])\/\/[^\n]*/g, "$1");
56
+ }
57
+ function moduleSpecifiers(source) {
58
+ const text = stripComments(source);
59
+ const out = [];
60
+ for (const m of text.matchAll(/(?<![.\w$])(?:import|export)\b([^;]*?)\bfrom\s*["']([^"']+)["']/g)) {
61
+ if (/^\s*type\s/.test(m[1] ?? "")) continue;
62
+ out.push(m[2] ?? "");
63
+ }
64
+ for (const m of text.matchAll(/(?<![.\w$])import\s*["']([^"']+)["']/g)) out.push(m[1] ?? "");
65
+ for (const m of text.matchAll(/(?<![.\w$])import\s*\(\s*["']([^"']+)["']\s*\)/g)) out.push(m[1] ?? "");
66
+ for (const m of text.matchAll(/(?<![.\w$])require\s*\(\s*["']([^"']+)["']\s*\)/g)) out.push(m[1] ?? "");
67
+ return [...new Set(out)].filter(Boolean);
68
+ }
69
+ function splitSpecifier(specifier) {
70
+ if (!specifier || specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#")) return null;
71
+ if (specifier.includes(":")) return null;
72
+ if (BUILTINS.has(specifier)) return null;
73
+ const parts = specifier.split("/");
74
+ const depth = specifier.startsWith("@") ? 2 : 1;
75
+ if (parts.length < depth || parts.some((p) => p === "")) return null;
76
+ const pkg = parts.slice(0, depth).join("/");
77
+ const rest = parts.slice(depth);
78
+ return { pkg, sub: rest.length > 0 ? `./${rest.join("/")}` : "." };
79
+ }
80
+ function findPackageDir(fromDir, pkg) {
81
+ let dir = path.resolve(fromDir);
82
+ for (; ; ) {
83
+ const candidate = path.join(dir, "node_modules", ...pkg.split("/"));
84
+ if (fs.existsSync(path.join(candidate, "package.json"))) {
85
+ try {
86
+ return fs.realpathSync(candidate);
87
+ } catch {
88
+ return candidate;
89
+ }
90
+ }
91
+ const parent = path.dirname(dir);
92
+ if (parent === dir) return null;
93
+ dir = parent;
94
+ }
95
+ }
96
+ function conditionTargets(node, acc = []) {
97
+ if (typeof node === "string") {
98
+ acc.push(node);
99
+ return acc;
100
+ }
101
+ if (Array.isArray(node)) {
102
+ for (const entry of node) conditionTargets(entry, acc);
103
+ return acc;
104
+ }
105
+ if (!node || typeof node !== "object") return acc;
106
+ for (const [key, value] of Object.entries(node)) {
107
+ if (!CONDITIONS.has(key)) continue;
108
+ conditionTargets(value, acc);
109
+ }
110
+ return acc;
111
+ }
112
+ function exportTargets(exportsField, sub) {
113
+ if (typeof exportsField === "string") return sub === "." ? [exportsField] : [];
114
+ if (Array.isArray(exportsField)) return exportsField.flatMap((entry) => exportTargets(entry, sub));
115
+ if (!exportsField || typeof exportsField !== "object") return [];
116
+ const map = exportsField;
117
+ const keys = Object.keys(map);
118
+ if (keys.length > 0 && !keys.some((k) => k.startsWith("."))) return sub === "." ? conditionTargets(map) : [];
119
+ if (sub in map) return conditionTargets(map[sub]);
120
+ let best = [];
121
+ let bestPrefix = -1;
122
+ for (const key of keys) {
123
+ const star = key.indexOf("*");
124
+ if (star === -1) continue;
125
+ const prefix = key.slice(0, star);
126
+ const suffix = key.slice(star + 1);
127
+ if (sub.length < prefix.length + suffix.length) continue;
128
+ if (!sub.startsWith(prefix) || !sub.endsWith(suffix)) continue;
129
+ if (prefix.length <= bestPrefix) continue;
130
+ const segment = sub.slice(prefix.length, sub.length - suffix.length);
131
+ bestPrefix = prefix.length;
132
+ best = conditionTargets(map[key]).map((target) => target.replaceAll("*", segment));
133
+ }
134
+ return best;
135
+ }
136
+ function isDirectory(p) {
137
+ try {
138
+ return fs.statSync(p).isDirectory();
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function firstFile(base) {
144
+ for (const ext of EXTENSIONS) {
145
+ const candidate = base + ext;
146
+ try {
147
+ if (fs.statSync(candidate).isFile()) return candidate;
148
+ } catch {
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+ function resolveSubpath(pkgDir, sub) {
154
+ let manifest = {};
155
+ try {
156
+ manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
157
+ } catch {
158
+ return null;
159
+ }
160
+ if (manifest.exports !== void 0) {
161
+ for (const target of exportTargets(manifest.exports, sub)) {
162
+ const file = firstFile(path.resolve(pkgDir, target));
163
+ if (file) return file;
164
+ }
165
+ return null;
166
+ }
167
+ if (sub === ".") return firstFile(path.resolve(pkgDir, manifest.main ?? "index.js"));
168
+ return firstFile(path.resolve(pkgDir, sub));
169
+ }
170
+ function programEntry(appDir) {
171
+ const app = path.resolve(appDir);
172
+ const index = path.join(app, "index.ts");
173
+ if (fs.existsSync(index)) return index;
174
+ let main;
175
+ try {
176
+ main = parseYaml(fs.readFileSync(path.join(app, "Pulumi.yaml"), "utf8"))?.main;
177
+ } catch {
178
+ return null;
179
+ }
180
+ if (typeof main !== "string" || !main) return null;
181
+ const target = path.resolve(app, main);
182
+ return isDirectory(target) ? firstFile(path.join(target, "index")) : firstFile(target);
183
+ }
184
+ function dependenciesInstalled(dir, stopAt) {
185
+ const root = stopAt === void 0 ? null : path.resolve(stopAt);
186
+ let at = path.resolve(dir);
187
+ for (; ; ) {
188
+ if (fs.existsSync(path.join(at, "node_modules"))) return true;
189
+ if (at === root) return false;
190
+ const parent = path.dirname(at);
191
+ if (parent === at) return false;
192
+ at = parent;
193
+ }
194
+ }
195
+ function suggestedRange(appDir, parentDir) {
196
+ try {
197
+ const manifest = JSON.parse(fs.readFileSync(path.join(appDir, "package.json"), "utf8"));
198
+ for (const section of [manifest.dependencies, manifest.devDependencies]) {
199
+ for (const [name, range] of Object.entries(section ?? {})) {
200
+ if (name.startsWith(SCOPE) && range) return range;
201
+ }
202
+ }
203
+ } catch {
204
+ }
205
+ if (!parentDir) return null;
206
+ try {
207
+ const version = JSON.parse(fs.readFileSync(path.join(parentDir, "package.json"), "utf8")).version;
208
+ return version ? `^${version}` : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function displayPath(app, visit) {
214
+ const within = path.relative(visit.pkg === null ? app : visit.pkgDir, visit.file).split(path.sep).join("/");
215
+ return visit.pkg === null ? within : `${visit.pkg}/${within}`;
216
+ }
217
+ function resolveProgramImports(appDir, options = {}) {
218
+ const app = path.resolve(appDir);
219
+ const label = options.label ?? path.basename(app);
220
+ const entryFile = options.entry ? firstFile(path.resolve(app, options.entry)) : programEntry(app);
221
+ if (!dependenciesInstalled(app, options.root)) {
222
+ return { findings: [], unchecked: "dependencies are not installed (run the install, then re-run)" };
223
+ }
224
+ if (!entryFile) {
225
+ return { findings: [], unchecked: "no Pulumi program found (index.ts, or the file Pulumi.yaml's `main` names)" };
226
+ }
227
+ const findings = [];
228
+ const reported = /* @__PURE__ */ new Set();
229
+ const seen = /* @__PURE__ */ new Set([entryFile]);
230
+ const queue = [{ file: entryFile, pkgDir: app, pkg: null, via: [] }];
231
+ let budget = FILE_BUDGET;
232
+ while (queue.length > 0 && budget-- > 0) {
233
+ const here = queue.shift();
234
+ let source;
235
+ try {
236
+ source = fs.readFileSync(here.file, "utf8");
237
+ } catch {
238
+ continue;
239
+ }
240
+ const fromDir = path.dirname(here.file);
241
+ for (const specifier of moduleSpecifiers(source)) {
242
+ if (specifier.startsWith(".")) {
243
+ if (here.pkg === null) continue;
244
+ const next = firstFile(path.resolve(fromDir, specifier));
245
+ if (next && !seen.has(next) && next.startsWith(here.pkgDir)) {
246
+ seen.add(next);
247
+ queue.push({ ...here, file: next });
248
+ }
249
+ continue;
250
+ }
251
+ const split = splitSpecifier(specifier);
252
+ if (!split) continue;
253
+ const inScope = split.pkg.startsWith(SCOPE);
254
+ if (here.pkg !== null && !inScope) continue;
255
+ const pkgDir = findPackageDir(fromDir, split.pkg);
256
+ const target = pkgDir ? resolveSubpath(pkgDir, split.sub) : null;
257
+ if (!pkgDir || !target) {
258
+ if (reported.has(specifier)) continue;
259
+ reported.add(specifier);
260
+ const range = inScope ? suggestedRange(app, here.pkg === null ? null : here.pkgDir) : null;
261
+ const line = `"${split.pkg}": "${range ?? (inScope ? "<the version your other @mesh-tech packages use>" : "<the version you need>")}"`;
262
+ const fix = pkgDir ? `${split.pkg} is installed but ships no ${split.sub} \u2014 the installed version predates the subpath; raise ${line} in ${label}/package.json` : `add ${line} to ${label}/package.json dependencies`;
263
+ findings.push({ pkg: split.pkg, specifier, file: here.file, importer: displayPath(app, here), via: here.via, fix });
264
+ continue;
265
+ }
266
+ if (!inScope || seen.has(target)) continue;
267
+ seen.add(target);
268
+ queue.push({ file: target, pkgDir, pkg: split.pkg, via: [...here.via, specifier] });
269
+ }
270
+ }
271
+ findings.sort((a, b) => a.pkg.localeCompare(b.pkg) || a.specifier.localeCompare(b.specifier));
272
+ const unchecked = queue.length > 0 ? `the import graph exceeded ${FILE_BUDGET} files` : null;
273
+ return { findings, unchecked };
274
+ }
275
+
276
+ // packages/mesh-cli/src/commands/app-check.ts
277
+ var APP_CHECK_RULES = {
278
+ UNRESOLVABLE_IMPORT: {
279
+ gate: "0.6",
280
+ title: "The Pulumi program's imports resolve",
281
+ severity: "block",
282
+ // The package is missing from the app's OWN manifest, which is why the
283
+ // remediation names that file: it is an optional peer of a @mesh-tech
284
+ // package, so the installer had nothing to complain about.
285
+ remediation: "declare the package in apps/<app>/package.json dependencies, at the range the app's other @mesh-tech/* packages use, and re-install \u2014 an optional peer that nothing names installs silently and fails at `mesh deploy`",
286
+ scope: "app"
287
+ },
288
+ UI_IN_API: {
289
+ gate: "0.1",
290
+ title: "UI is its own service",
291
+ severity: "block",
292
+ remediation: 'move the pages to apps/<app>/ui and declare new mesh.apps.Service("ui", { env, src: "./ui", \u2026 })',
293
+ scope: "app"
294
+ },
295
+ NO_UI_SERVICE: {
296
+ gate: "0.1",
297
+ title: "UI service declared",
298
+ severity: "block",
299
+ remediation: 'add apps/<app>/ui + new mesh.apps.Service("ui", \u2026) in index.ts',
300
+ scope: "app"
301
+ },
302
+ NO_REGISTER: {
303
+ gate: "0.2",
304
+ title: "App registers with the Hub",
305
+ severity: "block",
306
+ // The registration's prose is read by the CLIENT's operators, so the
307
+ // remediation names the split rather than one field: `description` is one
308
+ // sentence for them, `operatorNote` is where the engineering material goes
309
+ // (a tenant-audience Hub drops that key from the response entirely).
310
+ remediation: "end index.ts with `export const app = env.register({ displayName, description })` \u2014 one sentence a non-technical operator can act on; put the caveats in `operatorNote`",
311
+ scope: "app"
312
+ },
313
+ CUSTOM_SESSION_AUTH: {
314
+ gate: "0.3",
315
+ title: "Sign-in through the platform",
316
+ severity: "block",
317
+ remediation: 'delete the OIDC + cookie code and protect the UI service with auth: { provider: "mesh" }',
318
+ scope: "app"
319
+ },
320
+ NON_PLATFORM_LOGGER: {
321
+ gate: "0.4",
322
+ title: "Logs through @mesh-tech/logger",
323
+ severity: "block",
324
+ remediation: 'import { createLogger } from "@mesh-tech/logger" and log through it (requestLogger for HTTP)',
325
+ scope: "app"
326
+ },
327
+ LOCAL_AUTH_LIB: {
328
+ gate: "0.5",
329
+ title: "No tenant-local auth library",
330
+ severity: "advisory",
331
+ remediation: "check whether @mesh-tech/authn already owns this; if the platform lacks it, file a platform item",
332
+ scope: "repo"
333
+ },
334
+ NO_AUTHZ_POINTER: {
335
+ gate: "H",
336
+ title: "Access surfaced through the Hub",
337
+ severity: "block",
338
+ remediation: 'end index.ts with new mesh.auth.AppAuthzPointer("authz", { env, mode, opsHubMetadata: compileOpsHubMetadata(schemaDef) }) (role-gating) or { env, mode, opsHubMetadataRef: schema.opsHubMetadataRef } (policy-engine)',
339
+ scope: "app"
340
+ },
341
+ METADATA_UNPUBLISHED: {
342
+ gate: "H",
343
+ title: "Ops-hub metadata published",
344
+ severity: "block",
345
+ remediation: "export compileOpsHubMetadata(schemaDef) with mesh.exports.Export (role-gating) or let SpiceDBSchema publish it (policy-engine)",
346
+ scope: "app"
347
+ },
348
+ IAC_GRANTS: {
349
+ gate: "H",
350
+ title: "No role grants in IaC",
351
+ severity: "block",
352
+ remediation: "remove the per-user grant map; grant roles from the Hub (App \u2192 Access \u2192 Users)",
353
+ scope: "app"
354
+ },
355
+ POINTER_PROJECT_OVERRIDE: {
356
+ gate: "H",
357
+ title: "Pointer publishes the env's project",
358
+ severity: "advisory",
359
+ remediation: "drop `zitadel:` from AppAuthzPointer and pass `zitadelAppProjectId: identity.projectId` to AppEnvironment \u2014 the override is the Hub's",
360
+ scope: "app"
361
+ },
362
+ PASSWORD_STORE: {
363
+ gate: "H",
364
+ title: "No app-side credentials",
365
+ severity: "block",
366
+ remediation: "delete the password code; the Hub creates sign-in users with a one-time temporary password",
367
+ scope: "app"
368
+ },
369
+ EMAIL_ALLOWLIST: {
370
+ gate: "H",
371
+ title: "No email allowlist for roles",
372
+ severity: "block",
373
+ remediation: "declare a role on the ZitadelAppIdentity and let the Hub grant it",
374
+ scope: "app"
375
+ },
376
+ USERS_TABLE: {
377
+ gate: "H",
378
+ title: "No hand-rolled users/roles table",
379
+ severity: "block",
380
+ remediation: "drop the table; people and roles live in Zitadel and are administered from the Hub",
381
+ scope: "app"
382
+ },
383
+ DOCS_SITE_PUBLIC: {
384
+ gate: "0.3",
385
+ title: "Docs site published through the Hub docs door",
386
+ // Advisory because the public shape is the compatibility path, not a
387
+ // fault: a site on it keeps deploying, and its tenant CI should read the
388
+ // route without going red.
389
+ severity: "advisory",
390
+ remediation: "publish the site through the Hub docs door \u2014 declare the API as a mesh.apps.AppApiSurface whose docs block builds this same site (docs: { spec, pages }), declare its readers' role on the ZitadelAppIdentity and grant it from the Hub's Access tab; then drop the hostname (docs.ingress on an API surface, where docs.auth, docs.basePath and docs.hubDoor do nothing either; the ingress with its auth and authentication on a mesh.apps.ApiDocs). The public shape keeps deploying until you do",
391
+ scope: "app"
392
+ },
393
+ DOCS_SITE_DEPRECATED: {
394
+ gate: "0.3",
395
+ title: "API docs published as a native docs source",
396
+ // Advisory for the deprecation window: the site keeps deploying unchanged
397
+ // (with a preview warning) until the minor that removes it with the door,
398
+ // so tenant CI reads the route without going red.
399
+ severity: "advisory",
400
+ remediation: 'run the `mesh docs convert` command the finding names (filled from the docs block) \u2014 it writes a starter docs.yaml beside the guides, moves each page\'s leading # heading into its front-matter title with --fix-pages, and prints every problem the deploy would refuse plus the exact edit; then declare docs: { spec, config: "<dir>/docs.yaml" } and drop pages, landingPage, playground, audience, site, split, zudoku and port. The Hub renders the source itself \u2014 no docs site, container or door \u2014 and `mesh docs preview <dir>` shows it locally. The site keeps deploying, with a preview warning, for one more minor release and is removed with the Hub docs door in the next',
401
+ scope: "app"
402
+ },
403
+ DOCS_AUDIENCE_IMPLICIT: {
404
+ gate: "H",
405
+ title: "Docs audience stated, not derived",
406
+ severity: "block",
407
+ remediation: 'declare the surface as mesh.apps.AppApiSurface or VendorApiSurface (or pass kind: "app" | "vendor"), or state docs: { audience: { roles: [\u2026] } }',
408
+ scope: "app"
409
+ },
410
+ DOCS_AUDIENCE_UNGRANTABLE: {
411
+ gate: "H",
412
+ title: "Docs audience grantable from the Hub",
413
+ severity: "block",
414
+ remediation: `declare every audience role on the ZitadelAppIdentity (roles: ["docs"]) so the Hub can grant it from the app's Access tab \u2014 the audience is published on the surface's registry entry as the reader set for the Hub to admit by, and a role nobody can be granted admits nobody`,
415
+ scope: "app"
416
+ },
417
+ EVENTS_ROLES_UNDECLARED: {
418
+ gate: "H",
419
+ title: "Events roles grantable from the Hub",
420
+ severity: "block",
421
+ remediation: "spread mesh.apps.EVENTS_PROJECT_ROLES into the ZitadelAppIdentity's roles (roles: [...schemaDef.coarseRoles, ...mesh.apps.EVENTS_PROJECT_ROLES]) so events:subscriber and events:admin are project roles the Hub's Access tab can grant \u2014 the primitive cannot add them, because the identity builds its roles before the events service exists and a role added afterwards is absent from the coarseRoles the Hub reads",
422
+ scope: "app"
423
+ },
424
+ DOCS_VENDOR_AUDIENCE_PARTNER: {
425
+ gate: "H",
426
+ title: "Vendor reference never partner-readable",
427
+ // The same declaration fails `pulumi up` (the primitive refuses it); the
428
+ // check says so where the program is written, not at the deploy.
429
+ severity: "block",
430
+ remediation: "drop docs.audience from the vendor surface \u2014 a vendor surface's site is the raw core API reference, read inside the Hub by holders of the vendor-docs add-on only; audience: { roles: [] } would declare it readable by every docs-role user of the tenant, which the Hub never honours and the deploy refuses",
431
+ scope: "app"
432
+ }
433
+ };
434
+ var APP_CHECK_CODES = Object.keys(APP_CHECK_RULES);
435
+ var SRC_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".mjs", ".jsx"]);
436
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".generated", "test", "tests", "__tests__", "fixtures", "scripts"]);
437
+ var NON_SERVICE_DIRS = /* @__PURE__ */ new Set([
438
+ "docs",
439
+ "scripts",
440
+ "lib",
441
+ "libs",
442
+ "migrations",
443
+ "test",
444
+ "tests",
445
+ "node_modules",
446
+ "infra",
447
+ "mocks",
448
+ "skills",
449
+ "prisma"
450
+ ]);
451
+ function rel(root, abs) {
452
+ return path2.relative(root, abs).split(path2.sep).join("/");
453
+ }
454
+ function isDir(p) {
455
+ try {
456
+ return fs2.statSync(p).isDirectory();
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+ function readText(p) {
462
+ try {
463
+ return fs2.readFileSync(p, "utf8");
464
+ } catch {
465
+ return "";
466
+ }
467
+ }
468
+ function srcFiles(dir) {
469
+ const out = [];
470
+ const walk = (d) => {
471
+ let entries;
472
+ try {
473
+ entries = fs2.readdirSync(d, { withFileTypes: true });
474
+ } catch {
475
+ return;
476
+ }
477
+ for (const e of entries) {
478
+ const p = path2.join(d, e.name);
479
+ if (e.isDirectory()) {
480
+ if (SKIP_DIRS.has(e.name) || e.name.startsWith("scenario")) continue;
481
+ walk(p);
482
+ continue;
483
+ }
484
+ if (!e.isFile()) continue;
485
+ if (!SRC_EXT.has(path2.extname(e.name))) continue;
486
+ if (/\.(test|spec)\./.test(e.name) || /\.config\./.test(e.name) || e.name === "build.mjs") continue;
487
+ out.push(p);
488
+ }
489
+ };
490
+ walk(dir);
491
+ return out.sort();
492
+ }
493
+ function isTestHarness(name) {
494
+ return name.startsWith("test-") || name.endsWith("-test");
495
+ }
496
+ function isUiDir(name) {
497
+ return ["ui", "web", "frontend", "client", "portal-ui", "app"].includes(name) || name.endsWith("-ui");
498
+ }
499
+ function serviceDirs(app) {
500
+ let entries;
501
+ try {
502
+ entries = fs2.readdirSync(app, { withFileTypes: true });
503
+ } catch {
504
+ return [];
505
+ }
506
+ return entries.filter((e) => e.isDirectory() && !NON_SERVICE_DIRS.has(e.name) && !e.name.startsWith(".") && !isTestHarness(e.name)).map((e) => path2.join(app, e.name)).sort();
507
+ }
508
+ function authzSurface(app) {
509
+ const hits = [];
510
+ const walk = (d) => {
511
+ let entries;
512
+ try {
513
+ entries = fs2.readdirSync(d, { withFileTypes: true });
514
+ } catch {
515
+ return;
516
+ }
517
+ for (const e of entries) {
518
+ const p = path2.join(d, e.name);
519
+ if (e.isDirectory()) {
520
+ if (["node_modules", "dist", "build"].includes(e.name)) continue;
521
+ walk(p);
522
+ continue;
523
+ }
524
+ if (!e.isFile()) continue;
525
+ if (!/\.(ts|tsx|yaml|sql)$/.test(e.name) || /\.(test|spec)\.tsx?$/.test(e.name) || e.name.endsWith(".d.ts")) continue;
526
+ const lines = readText(p).split("\n");
527
+ lines.forEach((text, i) => {
528
+ if (/^\s*(\/\/|\*|\/\*|#|--)/.test(text)) return;
529
+ hits.push({ file: p, line: i + 1, text });
530
+ });
531
+ }
532
+ };
533
+ walk(app);
534
+ return hits;
535
+ }
536
+ var RE_HTML = /from ["']hono\/(html|jsx)["']|react-dom\/server|\bc\.html\(|<!doctype html|<!DOCTYPE html/;
537
+ var RE_OIDC = /openid-client|code_verifier|authorization_code|buildAuthorizeUrl|end_session/;
538
+ var RE_COOKIE = /setCookie\(|hono\/cookie|Set-Cookie/;
539
+ var RE_LOGGER = /from ["'](pino|hono\/logger|winston|bunyan)["']|require\(["'](pino|winston|bunyan)["']\)|^[^/*]*\bconsole\.(log|info|warn|error)\(/;
540
+ var RE_EXPOSURE = /ZitadelAppIdentity|createOAuthMiddleware|createAuthorizationMiddleware|zitadelJwtScheme|createAuthzMiddleware|bearerAuth|type: *"oidc"|^\s+auth: *\{|_ROLES *= *\{|coarseRoles|ZITADEL_(AUDIENCE|CLIENT_ID)|OIDC_CLIENT_ID|zitadel\.(ApplicationOidc|ApplicationApi|Project|UserGrant)\(|mesh\.apps\.EventsService\(/;
541
+ var RE_POINTER = /AppAuthzPointer/;
542
+ var RE_METADATA = /compileOpsHubMetadata|ops-hub-metadata|SpiceDBSchema/;
543
+ var RE_IAC_GRANT = /userId: *"[0-9]{6,}"/;
544
+ var RE_POINTER_OVERRIDE = /\bzitadel: *\{\s*projectId\b/;
545
+ var RE_PASSWORD = /(^|[^a-z])(bcrypt|argon2)|password_hash|passwordHash|hashPassword|temporaryPassword.*(send|mail)|(send|mail).*temporaryPassword/;
546
+ var RE_ALLOWLIST = /ADMIN_EMAILS|ALLOWED_EMAILS|allowedEmails|adminEmails/;
547
+ var RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|permissions)"?/i;
548
+ var RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
549
+ var RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
550
+ var RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g;
551
+ var RE_EVENTS_NEW = /new\s+mesh\.apps\.(EventsService)\s*\(/g;
552
+ var EVENTS_PROJECT_ROLES = ["events:subscriber", "events:admin"];
553
+ var RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g;
554
+ var DEFAULT_PARTNER_DOCS_ROLES = ["docs"];
555
+ function stringList(value) {
556
+ if (!value) return void 0;
557
+ const text = value.trim();
558
+ if (!text.startsWith("[") || !text.endsWith("]")) return void 0;
559
+ const out = [];
560
+ for (const entry of splitTopLevel(text.slice(1, -1))) {
561
+ const item = stripLeadingComments(entry).trim();
562
+ if (!item) continue;
563
+ const m = /^(["'])((?:(?!\1).)*)\1$/.exec(stripTrailingComments(item));
564
+ if (!m) return void 0;
565
+ out.push(m[2]);
566
+ }
567
+ return out;
568
+ }
569
+ var DOCS_SITE_FIELDS = ["pages", "playground", "audience", "basePath", "ingress", "auth"];
570
+ var DOCS_SITE_RETIRED_FIELDS = ["auth", "ingress", "basePath", "hubDoor"];
571
+ function balancedSpan(text, open) {
572
+ const closers = { "(": ")", "{": "}", "[": "]" };
573
+ const stack = [];
574
+ let quote;
575
+ for (let i = open; i < text.length; i++) {
576
+ const ch = text[i];
577
+ if (quote) {
578
+ if (ch === "\\") i++;
579
+ else if (ch === quote) quote = void 0;
580
+ continue;
581
+ }
582
+ if (ch === '"' || ch === "'" || ch === "`") {
583
+ quote = ch;
584
+ continue;
585
+ }
586
+ if (ch === "/" && text[i + 1] === "/") {
587
+ const eol = text.indexOf("\n", i);
588
+ if (eol === -1) return "";
589
+ i = eol;
590
+ continue;
591
+ }
592
+ if (ch === "/" && text[i + 1] === "*") {
593
+ const end = text.indexOf("*/", i + 2);
594
+ if (end === -1) return "";
595
+ i = end + 1;
596
+ continue;
597
+ }
598
+ if (closers[ch]) stack.push(closers[ch]);
599
+ else if (ch === ")" || ch === "}" || ch === "]") {
600
+ if (stack.pop() !== ch) return "";
601
+ if (stack.length === 0) return text.slice(open, i + 1);
602
+ }
603
+ }
604
+ return "";
605
+ }
606
+ function splitTopLevel(inner) {
607
+ const parts = [];
608
+ let depth = 0;
609
+ let quote;
610
+ let start = 0;
611
+ for (let i = 0; i < inner.length; i++) {
612
+ const ch = inner[i];
613
+ if (quote) {
614
+ if (ch === "\\") i++;
615
+ else if (ch === quote) quote = void 0;
616
+ continue;
617
+ }
618
+ if (ch === '"' || ch === "'" || ch === "`") {
619
+ quote = ch;
620
+ continue;
621
+ }
622
+ if (ch === "/" && inner[i + 1] === "/") {
623
+ const eol = inner.indexOf("\n", i);
624
+ if (eol === -1) break;
625
+ i = eol;
626
+ continue;
627
+ }
628
+ if (ch === "/" && inner[i + 1] === "*") {
629
+ const end = inner.indexOf("*/", i + 2);
630
+ if (end === -1) break;
631
+ i = end + 1;
632
+ continue;
633
+ }
634
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
635
+ else if (ch === ")" || ch === "}" || ch === "]") depth--;
636
+ else if (ch === "," && depth === 0) {
637
+ parts.push(inner.slice(start, i));
638
+ start = i + 1;
639
+ }
640
+ }
641
+ const last = inner.slice(start);
642
+ if (last.trim()) parts.push(last);
643
+ return parts;
644
+ }
645
+ var RE_PROPERTY = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][\w$]*))\s*(?::\s*([\s\S]*))?$/;
646
+ function stripLeadingComments(entry) {
647
+ let text = entry;
648
+ for (; ; ) {
649
+ const next = text.replace(/^\s*(?:\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)/, "");
650
+ if (next === text) return text;
651
+ text = next;
652
+ }
653
+ }
654
+ function stripTrailingComments(value) {
655
+ let out = "";
656
+ let depth = 0;
657
+ let quote;
658
+ for (let i = 0; i < value.length; i++) {
659
+ const ch = value[i];
660
+ if (quote) {
661
+ out += ch;
662
+ if (ch === "\\") {
663
+ out += value[i + 1] ?? "";
664
+ i++;
665
+ } else if (ch === quote) quote = void 0;
666
+ continue;
667
+ }
668
+ if (ch === '"' || ch === "'" || ch === "`") {
669
+ quote = ch;
670
+ out += ch;
671
+ continue;
672
+ }
673
+ if (depth === 0 && ch === "/" && value[i + 1] === "/") {
674
+ const eol = value.indexOf("\n", i);
675
+ if (eol === -1) break;
676
+ i = eol;
677
+ out += "\n";
678
+ continue;
679
+ }
680
+ if (depth === 0 && ch === "/" && value[i + 1] === "*") {
681
+ const end = value.indexOf("*/", i + 2);
682
+ if (end === -1) break;
683
+ i = end + 1;
684
+ continue;
685
+ }
686
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
687
+ else if (ch === ")" || ch === "}" || ch === "]") depth--;
688
+ out += ch;
689
+ }
690
+ return out.trim();
691
+ }
692
+ function topLevelProperties(objectText) {
693
+ const props = /* @__PURE__ */ new Map();
694
+ if (!objectText.startsWith("{") || !objectText.endsWith("}")) return props;
695
+ for (const entry of splitTopLevel(objectText.slice(1, -1))) {
696
+ const text = stripLeadingComments(entry).trim();
697
+ if (!text || text.startsWith("...")) continue;
698
+ const m = RE_PROPERTY.exec(text);
699
+ if (!m) continue;
700
+ const key = m[1] ?? m[2] ?? m[3];
701
+ props.set(key, stripTrailingComments(m[4] ?? key));
702
+ }
703
+ return props;
704
+ }
705
+ function objectLiteralOf(value, program) {
706
+ if (!value) return void 0;
707
+ if (value.startsWith("{")) return balancedSpan(value, 0) || void 0;
708
+ const id = /^[A-Za-z_$][\w$]*$/.exec(value)?.[0];
709
+ if (!id) return void 0;
710
+ const decl = new RegExp(`\\bconst\\s+${id}\\s*(?::[^=]+)?=\\s*\\{`).exec(program);
711
+ return decl ? balancedSpan(program, decl.index + decl[0].length - 1) || void 0 : void 0;
712
+ }
713
+ function optionsObject(body, program) {
714
+ for (const arg of splitTopLevel(body.slice(1, -1))) {
715
+ const trimmed = stripTrailingComments(stripLeadingComments(arg).trim());
716
+ if (trimmed.startsWith("{")) return balancedSpan(trimmed, 0) || void 0;
717
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
718
+ const resolved = objectLiteralOf(trimmed, program);
719
+ if (resolved) return resolved;
720
+ }
721
+ }
722
+ return void 0;
723
+ }
724
+ function surfaceDeclarations(text, pattern = RE_SURFACE_NEW) {
725
+ const out = [];
726
+ for (const m of text.matchAll(pattern)) {
727
+ const body = balancedSpan(text, m.index + m[0].length - 1);
728
+ if (!body) continue;
729
+ out.push({ ctor: m[1] ?? "ApiDocs", body, line: text.slice(0, m.index).split("\n").length });
730
+ }
731
+ return out;
732
+ }
733
+ function isWritten(value) {
734
+ return value !== void 0 && value !== "undefined";
735
+ }
736
+ function hasIngress(value) {
737
+ return isWritten(value) && !/^(null|false)$/.test(value);
738
+ }
739
+ function stringLiteral(value) {
740
+ const m = /^(["'`])((?:(?!\1)[^\\$]|\\.)*)\1$/.exec(stripTrailingComments(value?.trim() ?? ""));
741
+ return m ? m[2] : void 0;
742
+ }
743
+ function docsConvertCommand(docs, program) {
744
+ const words = ["mesh docs convert --spec", stringLiteral(docs.get("spec")) ?? "<spec>"];
745
+ const pagesValue = docs.get("pages");
746
+ if (isWritten(pagesValue)) {
747
+ const list = pagesValue.trim().startsWith("[") ? splitTopLevel(pagesValue.trim().slice(1, -1)) : [pagesValue];
748
+ const first = list.map((e) => stripLeadingComments(e).trim()).find((e) => e !== "") ?? "";
749
+ const dir = first.startsWith("{") ? topLevelProperties(objectLiteralOf(first, program) ?? first).get("dir") : first;
750
+ words.push("--pages", stringLiteral(dir) ?? "<pages>");
751
+ }
752
+ const landing = docs.get("landingPage");
753
+ if (isWritten(landing) && landing.trim() !== "false") words.push("--landing", stringLiteral(landing) ?? "<landing>");
754
+ return words.join(" ");
755
+ }
756
+ function docsSiteFindings(text) {
757
+ const findings = [];
758
+ for (const decl of surfaceDeclarations(text)) {
759
+ const options = optionsObject(decl.body, text);
760
+ if (!options) continue;
761
+ const args = topLevelProperties(options);
762
+ const docsText = objectLiteralOf(args.get("docs"), text);
763
+ if (!docsText) continue;
764
+ const docs = topLevelProperties(docsText);
765
+ if (isWritten(docs.get("config"))) continue;
766
+ if (docs.get("site") === "false") continue;
767
+ if (docs.get("site") !== "true" && !DOCS_SITE_FIELDS.some((f) => docs.has(f))) continue;
768
+ findings.push({
769
+ code: "DOCS_SITE_DEPRECATED",
770
+ line: decl.line,
771
+ why: `mesh.apps.${decl.ctor} builds a docs site from its docs block \u2014 deprecated for a native docs source the Hub renders itself (docs.config, a docs.yaml); convert with \`${docsConvertCommand(docs, text)}\``
772
+ });
773
+ const retired = DOCS_SITE_RETIRED_FIELDS.filter((f) => isWritten(docs.get(f)));
774
+ if (retired.length > 0) {
775
+ const named = retired.map((f) => `docs.${f}`).join(", ");
776
+ findings.push({
777
+ code: "DOCS_SITE_PUBLIC",
778
+ line: decl.line,
779
+ why: `mesh.apps.${decl.ctor} declares a docs site with ${named} \u2014 deprecated: the Hub docs door is the path this site is published on, and it is already built for it (no hostname, no sign-in and no door switch of its own), so the primitive ignores ${retired.length > 1 ? "them" : "it"}`
780
+ });
781
+ }
782
+ const kindStated = decl.ctor === "AppApiSurface" || decl.ctor === "VendorApiSurface" || args.has("kind");
783
+ if (!kindStated && !docs.has("audience")) {
784
+ findings.push({
785
+ code: "DOCS_AUDIENCE_IMPLICIT",
786
+ line: decl.line,
787
+ why: `mesh.apps.${decl.ctor} declares a docs site but neither its kind nor its audience \u2014 who the Hub admits through the door would fall out of the name heuristic, and a vendor surface must never default to partner-readable that way`
788
+ });
789
+ }
790
+ if (statedKind(decl, args) === "vendor" && docs.has("audience")) {
791
+ const audience = objectLiteralOf(docs.get("audience"), text);
792
+ const roles = audience ? stringList(topLevelProperties(audience).get("roles")) : void 0;
793
+ if (roles !== void 0 && roles.length === 0) {
794
+ findings.push({
795
+ code: "DOCS_VENDOR_AUDIENCE_PARTNER",
796
+ line: decl.line,
797
+ why: `mesh.apps.${decl.ctor} declares a vendor surface's docs site with audience: { roles: [] } \u2014 partner-readable by every docs-role user of the tenant; a vendor's spec is the raw core API, its site is read by holders of the Hub's vendor-docs add-on only, and the deploy refuses this declaration`
798
+ });
799
+ }
800
+ }
801
+ const httpText = objectLiteralOf(topLevelProperties(objectLiteralOf(args.get("surfaces"), text) ?? "{}").get("http"), text);
802
+ for (const f of docsAudienceUngrantable(text, decl, args, docs, httpText)) findings.push(f);
803
+ }
804
+ for (const decl of surfaceDeclarations(text, RE_APIDOCS_NEW)) {
805
+ const options = optionsObject(decl.body, text);
806
+ if (!options) continue;
807
+ const args = topLevelProperties(options);
808
+ if (!hasIngress(args.get("ingress"))) continue;
809
+ findings.push({
810
+ code: "DOCS_SITE_PUBLIC",
811
+ line: decl.line,
812
+ why: `mesh.apps.ApiDocs is published on an ingress \u2014 deprecated: the Hub docs door is the path a docs site is published on, and it opens for the API surface that declares the site. The hostname still deploys, with a preview warning, as the compatibility path`
813
+ });
814
+ }
815
+ return findings;
816
+ }
817
+ function statedKind(decl, args) {
818
+ if (decl.ctor === "AppApiSurface") return "app";
819
+ if (decl.ctor === "VendorApiSurface") return "vendor";
820
+ return /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1];
821
+ }
822
+ function programIdentities(text) {
823
+ const identities = [];
824
+ for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
825
+ const options = optionsObject(d.body, text);
826
+ if (!options) continue;
827
+ const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find(
828
+ (m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1
829
+ )?.[1];
830
+ identities.push({ binding, options: topLevelProperties(options) });
831
+ }
832
+ return identities;
833
+ }
834
+ function declaredProjectRoles(identity) {
835
+ const rolesText = identity.options.get("roles");
836
+ const coarseText = identity.options.get("coarseRoles");
837
+ if (rolesText !== void 0) return stringList(rolesText);
838
+ if (coarseText !== void 0) return stringList(coarseText);
839
+ return [];
840
+ }
841
+ function eventsRoleFindings(text) {
842
+ const findings = [];
843
+ const identities = programIdentities(text);
844
+ for (const decl of surfaceDeclarations(text, RE_EVENTS_NEW)) {
845
+ const options = optionsObject(decl.body, text);
846
+ if (!options) continue;
847
+ const args = topLevelProperties(options);
848
+ if (args.get("enabled") === "false") continue;
849
+ if (identities.length === 0) {
850
+ continue;
851
+ }
852
+ const identityText = objectLiteralOf(args.get("identity"), text);
853
+ const projectExpr = identityText ? topLevelProperties(identityText).get("projectId") : void 0;
854
+ const projectRoot = projectExpr ? /^([A-Za-z_$][\w$]*)\.projectId\b/.exec(projectExpr.trim())?.[1] : void 0;
855
+ const identity = (projectRoot ? identities.find((id) => id.binding === projectRoot) : void 0) ?? (identities.length === 1 ? identities[0] : void 0);
856
+ if (!identity) continue;
857
+ const declared = declaredProjectRoles(identity);
858
+ if (!declared) continue;
859
+ const missing = EVENTS_PROJECT_ROLES.filter((r) => !declared.includes(r));
860
+ if (missing.length === 0) continue;
861
+ findings.push({
862
+ code: "EVENTS_ROLES_UNDECLARED",
863
+ line: decl.line,
864
+ why: `mesh.apps.EventsService is declared but the app's ZitadelAppIdentity does not declare ${missing.map((r) => `"${r}"`).join(" or ")} \u2014 the events service authorizes bearer callers by these project roles, and a role the identity never declares cannot be granted from the Hub's Access tab, so no subscriber can be admitted`
865
+ });
866
+ }
867
+ return findings;
868
+ }
869
+ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
870
+ const findings = [];
871
+ let roles;
872
+ if (docs.has("audience")) {
873
+ const audience = objectLiteralOf(docs.get("audience"), text);
874
+ roles = audience ? stringList(topLevelProperties(audience).get("roles")) : void 0;
875
+ } else if (statedKind(decl, args) === "app") {
876
+ roles = DEFAULT_PARTNER_DOCS_ROLES;
877
+ }
878
+ if (!roles || roles.length === 0) return findings;
879
+ const identities = programIdentities(text);
880
+ if (identities.length === 0) return findings;
881
+ const docsAuth = objectLiteralOf(docs.get("auth"), text);
882
+ const clientExpr = (docsAuth ? topLevelProperties(docsAuth).get("clientId") : void 0) ?? (httpText ? topLevelProperties(objectLiteralOf(topLevelProperties(httpText).get("auth"), text) ?? "{}").get("clientId") : void 0);
883
+ const clientRoot = clientExpr ? /^([A-Za-z_$][\w$]*)\.(?:clientIds|applications)\./.exec(clientExpr.trim())?.[1] : void 0;
884
+ const identity = (clientRoot ? identities.find((id) => id.binding === clientRoot) : void 0) ?? (identities.length === 1 ? identities[0] : void 0);
885
+ if (!identity) return findings;
886
+ const declared = declaredProjectRoles(identity);
887
+ if (declared) {
888
+ for (const role of roles) {
889
+ if (declared.includes(role)) continue;
890
+ findings.push({
891
+ code: "DOCS_AUDIENCE_UNGRANTABLE",
892
+ line: decl.line,
893
+ why: `the docs audience gates on role "${role}", which the app's ZitadelAppIdentity does not declare in roles \u2014 the Hub cannot grant it, so no one can read the site`
894
+ });
895
+ }
896
+ }
897
+ return findings;
898
+ }
899
+ function listApps(root) {
900
+ const apps = [];
901
+ const appsDir = path2.join(root, "apps");
902
+ if (isDir(appsDir)) {
903
+ for (const e of fs2.readdirSync(appsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
904
+ if (!e.isDirectory()) continue;
905
+ const d = path2.join(appsDir, e.name);
906
+ if (fs2.existsSync(path2.join(d, "index.ts")) || fs2.existsSync(path2.join(d, "Pulumi.yaml"))) apps.push(rel(root, d));
907
+ }
908
+ }
909
+ if (fs2.existsSync(path2.join(root, "index.ts")) && fs2.existsSync(path2.join(root, "Pulumi.yaml"))) apps.push(".");
910
+ return apps;
911
+ }
912
+ function probeResolution(root, appRel) {
913
+ return resolveProgramImports(path2.resolve(root, appRel), { root, label: appRel === "." ? "this app" : appRel });
914
+ }
915
+ function scanApp(root, appRel, resolution = probeResolution(root, appRel)) {
916
+ const app = path2.resolve(root, appRel);
917
+ const index = path2.join(app, "index.ts");
918
+ const findings = [];
919
+ const hit = (code, p, why) => {
920
+ findings.push({ code, path: p, why });
921
+ };
922
+ const hasProgram = fs2.existsSync(index) || fs2.existsSync(path2.join(app, "Pulumi.yaml"));
923
+ if (!hasProgram) return findings;
924
+ const entry = programEntry(app);
925
+ for (const bad of resolution.findings) {
926
+ const chain = [...bad.via, bad.specifier].join(" \u2192 ");
927
+ const at = rel(root, entry ?? index);
928
+ hit("UNRESOLVABLE_IMPORT", at, `${chain} does not resolve from this app (imported by ${bad.importer}) \u2014 ${bad.fix}`);
929
+ }
930
+ if (fs2.existsSync(index)) {
931
+ const indexRel = rel(root, index);
932
+ let servesHtml = false;
933
+ let hasUiService = false;
934
+ for (const svc of serviceDirs(app)) {
935
+ const svcName = path2.basename(svc);
936
+ if (isUiDir(svcName)) {
937
+ hasUiService = true;
938
+ continue;
939
+ }
940
+ for (const f of srcFiles(svc)) {
941
+ let why = "";
942
+ if (/\.(tsx|jsx)$/.test(f)) why = `JSX view file inside the ${svcName} service`;
943
+ else if (RE_HTML.test(readText(f))) why = `renders HTML inside the ${svcName} service`;
944
+ if (why) {
945
+ servesHtml = true;
946
+ hit("UI_IN_API", rel(root, f), `${why} \u2014 a browser UI is its own mesh.apps.Service (apps/<app>/ui)`);
947
+ }
948
+ }
949
+ }
950
+ if (servesHtml && !hasUiService) {
951
+ hit("NO_UI_SERVICE", indexRel, 'the app serves HTML but declares no ui/ service \u2014 add apps/<app>/ui + new mesh.apps.Service("ui", \u2026)');
952
+ }
953
+ if (!/env\.register\(|\.annotate\(/.test(readText(index))) {
954
+ hit("NO_REGISTER", indexRel, "never calls env.register({\u2026}) \u2014 the Hub cannot list this app");
955
+ }
956
+ for (const f of docsSiteFindings(readText(index))) hit(f.code, `${indexRel}:${f.line}`, f.why);
957
+ for (const f of eventsRoleFindings(readText(index))) hit(f.code, `${indexRel}:${f.line}`, f.why);
958
+ for (const svc of serviceDirs(app)) {
959
+ for (const f of srcFiles(svc)) {
960
+ const text = readText(f);
961
+ if (RE_OIDC.test(text) && RE_COOKIE.test(text)) {
962
+ hit("CUSTOM_SESSION_AUTH", rel(root, f), 'app-written OIDC + cookie session \u2014 protect the UI service with auth: { provider: "mesh" } instead');
963
+ }
964
+ }
965
+ }
966
+ for (const svc of serviceDirs(app)) {
967
+ for (const f of srcFiles(svc)) {
968
+ const lines = readText(f).split("\n");
969
+ const i = lines.findIndex((l) => RE_LOGGER.test(l) && !l.includes("mesh-tech/logger"));
970
+ if (i === -1) continue;
971
+ hit("NON_PLATFORM_LOGGER", `${rel(root, f)}:${i + 1}`, `logs outside @mesh-tech/logger (${lines[i]?.trim() ?? ""})`);
972
+ }
973
+ }
974
+ }
975
+ const surface = authzSurface(app);
976
+ const first = (re) => surface.find((h) => re.test(h.text));
977
+ const exposure = first(RE_EXPOSURE);
978
+ const pointer = first(RE_POINTER);
979
+ const metadata = first(RE_METADATA);
980
+ const appLabel = appRel === "." ? "(root)" : path2.basename(appRel);
981
+ if (exposure && !pointer) {
982
+ const at = surface.find((h) => RE_EXPOSURE.test(h.text) && /(^|\/)index\.ts$/.test(h.file)) ?? exposure;
983
+ hit(
984
+ "NO_AUTHZ_POINTER",
985
+ rel(root, at.file),
986
+ `${appLabel} authenticates callers or declares roles but publishes no mesh.auth.AppAuthzPointer \u2014 the Hub's Access tab is blank for this app`
987
+ );
988
+ } else if (exposure && pointer && !metadata) {
989
+ hit(
990
+ "METADATA_UNPUBLISHED",
991
+ rel(root, pointer.file),
992
+ "AppAuthzPointer present but no compiled ops-hub metadata (opsHubMetadata: compileOpsHubMetadata(schemaDef) on the pointer, or a SpiceDBSchema) \u2014 the Hub's create-key and role catalog 409 METADATA_UNPUBLISHED"
993
+ );
994
+ }
995
+ const anti = (code, re, why) => {
996
+ const files = [...new Set(surface.filter((h) => re.test(h.text)).map((h) => h.file))].sort().slice(0, 3);
997
+ for (const f of files) hit(code, rel(root, f), why);
998
+ };
999
+ anti("IAC_GRANTS", RE_IAC_GRANT, "per-user role grant baked into IaC \u2014 grants belong in the Hub (Access \u2192 Users), not in a config redeploy");
1000
+ anti(
1001
+ "POINTER_PROJECT_OVERRIDE",
1002
+ RE_POINTER_OVERRIDE,
1003
+ "AppAuthzPointer is given a `zitadel:` override \u2014 an app binds its project with AppEnvironment's zitadelAppProjectId so every consumer of env.zitadel agrees; the override is reserved for the Hub"
1004
+ );
1005
+ anti(
1006
+ "PASSWORD_STORE",
1007
+ RE_PASSWORD,
1008
+ "password storage or hashing \u2014 Zitadel owns credentials; the Hub creates sign-in users with a one-time temporary password"
1009
+ );
1010
+ anti("EMAIL_ALLOWLIST", RE_ALLOWLIST, "email allowlist standing in for roles \u2014 declare a role and let the Hub grant it");
1011
+ anti("USERS_TABLE", RE_USERS_TABLE, "hand-rolled users/roles table \u2014 people and roles live in Zitadel and are administered from the Hub");
1012
+ return findings;
1013
+ }
1014
+ function isPlatformPackage(dir) {
1015
+ try {
1016
+ const pkg = JSON.parse(readText(path2.join(dir, "package.json")));
1017
+ return typeof pkg.name === "string" && pkg.name.startsWith("@mesh-tech/");
1018
+ } catch {
1019
+ return false;
1020
+ }
1021
+ }
1022
+ function scanRepo(root) {
1023
+ const findings = [];
1024
+ for (const parent of ["libs", "packages"]) {
1025
+ const dir = path2.join(root, parent);
1026
+ if (!isDir(dir)) continue;
1027
+ for (const e of fs2.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1028
+ if (!e.isDirectory() || !e.name.includes("auth")) continue;
1029
+ const lib = path2.join(dir, e.name);
1030
+ if (isPlatformPackage(lib)) continue;
1031
+ const verifies = srcFiles(lib).filter((f) => f.endsWith(".ts")).some((f) => /introspect|jwtVerify|createRemoteJWKSet|from ["']jose["']/.test(readText(f)));
1032
+ if (verifies) {
1033
+ findings.push({
1034
+ code: "LOCAL_AUTH_LIB",
1035
+ path: rel(root, lib),
1036
+ why: "tenant-local token verification \u2014 check whether @mesh-tech/authn already owns this"
1037
+ });
1038
+ }
1039
+ }
1040
+ }
1041
+ return findings;
1042
+ }
1043
+ function resultFor(code, findings, unchecked = null) {
1044
+ const rule = APP_CHECK_RULES[code];
1045
+ const mine = findings.filter((f) => f.code === code);
1046
+ if (code === "UNRESOLVABLE_IMPORT" && unchecked) {
1047
+ return { status: "ok", summary: `gate ${rule.gate}: not checked \u2014 ${unchecked}` };
1048
+ }
1049
+ if (mine.length === 0) return { status: "ok", summary: `gate ${rule.gate}: no ${code} finding` };
1050
+ return {
1051
+ status: rule.severity === "block" ? "error" : "warn",
1052
+ summary: `gate ${rule.gate}: ${code} \xD7 ${mine.length}`,
1053
+ detail: mine.map((f) => `${f.path} \u2014 ${f.why}`).join("\n "),
1054
+ remediation: rule.remediation
1055
+ };
1056
+ }
1057
+ function appContractChecks(scope) {
1058
+ return APP_CHECK_CODES.filter((code) => APP_CHECK_RULES[code].scope === scope).map((code) => ({
1059
+ id: code,
1060
+ title: `${APP_CHECK_RULES[code].title} (${APP_CHECK_RULES[code].gate})`,
1061
+ phases: ["ondemand", "preflight"],
1062
+ run: async (ctx) => resultFor(code, ctx.findings, ctx.unchecked)
1063
+ }));
1064
+ }
1065
+ async function runAppCheck(root, apps) {
1066
+ if (!isDir(root)) throw new MeshCliError(`no such checkout: ${root}`);
1067
+ const targets = apps && apps.length > 0 ? apps.map((a) => a.replace(/\/+$/, "")) : listApps(root);
1068
+ for (const a of targets) {
1069
+ if (!isDir(path2.resolve(root, a))) throw new MeshCliError(`no such app dir: ${a}`);
1070
+ }
1071
+ const scopes = [];
1072
+ for (const app of targets) {
1073
+ const resolution = probeResolution(root, app);
1074
+ const ctx = { root, app, findings: scanApp(root, app, resolution), unchecked: resolution.unchecked };
1075
+ const results = await runChecks(ctx, "ondemand", appContractChecks("app"));
1076
+ scopes.push({ scope: app, ...jsonReport(results) });
1077
+ }
1078
+ const repoResults = await runChecks({ root, app: null, findings: scanRepo(root), unchecked: null }, "ondemand", appContractChecks("repo"));
1079
+ scopes.push({ scope: "(repo)", ...jsonReport(repoResults) });
1080
+ return { root, status: aggregateStatus(scopes.map((s) => ({ status: s.status, summary: "" }))), scopes };
1081
+ }
1082
+ function renderReport(report, opts) {
1083
+ const out = [];
1084
+ for (const scope of report.scopes) {
1085
+ const shown = opts.verbose ? scope.checks : scope.checks.filter((c) => c.status !== "ok");
1086
+ if (shown.length === 0 && !opts.verbose) continue;
1087
+ out.push(`${ICONS[scope.status]} ${scope.scope}`);
1088
+ out.push(
1089
+ renderHuman(
1090
+ shown.map((c) => ({
1091
+ check: { id: c.id, title: `${c.id} \u2014 ${APP_CHECK_RULES[c.id]?.title ?? c.id}` },
1092
+ result: { status: c.status, summary: c.summary, remediation: c.remediation ?? void 0, detail: c.detail ?? void 0 }
1093
+ }))
1094
+ ).split("\n").map((l) => ` ${l}`).join("\n")
1095
+ );
1096
+ }
1097
+ out.push(`${ICONS[report.status]} app contract: ${report.status}`);
1098
+ return out.filter(Boolean).join("\n");
1099
+ }
1100
+ function registerAppCommands(program) {
1101
+ const app = program.command("app").description("Inspect a tenant app against the Mesh app contract");
1102
+ app.command("check").description(
1103
+ "Check tenant apps against the Mesh app contract (@mesh-tech/app-kit skills/apps/references/app-contract.md): a UI is its own service, the app registers with the Hub, sign-in goes through the platform proxy, logs go through @mesh-tech/logger, people/roles/keys are surfaced through the Hub, and the Pulumi program's imports resolve from the app directory. Exit 1 on any blocking finding \u2014 the same gate mesh create-app, tenant CI and the platform reviewer run. The import gate reads the installed dependency tree, so run it after the install; with no node_modules it reports itself not checked rather than met."
1104
+ ).argument("[apps...]", "repo-relative app dirs to check (default: every apps/* with a Pulumi program)").option("--root <dir>", "checkout to scan (default: the enclosing git root)").option("--json", "machine-readable output (one report per app plus the repo-scoped checks)", false).option("--verbose", "also print the gates that passed", false).action(async (apps, opts) => {
1105
+ const root = path2.resolve(opts.root ?? resolveTargetRoot());
1106
+ const report = await runAppCheck(root, apps);
1107
+ if (opts.json) {
1108
+ emitJsonPayload({ ok: report.status !== "error", ...report });
1109
+ } else {
1110
+ if (report.scopes.length === 1) logInfo(`no apps/* with a Pulumi program under ${root}`);
1111
+ console.log(renderReport(report, { verbose: opts.verbose }));
1112
+ }
1113
+ if (report.status === "error") {
1114
+ if (!opts.json) logError("the app contract is not met \u2014 every \u2717 above names the gate, the file and the replacement");
1115
+ process.exitCode = 1;
1116
+ }
1117
+ });
1118
+ }
1119
+
1120
+ // packages/mesh-cli/src/utils/scaffold-versions.ts
1121
+ import { execFileSync } from "child_process";
1122
+ var SCAFFOLD_PACKAGE_LINES = {
1123
+ "@mesh-tech/app-kit": "^1.45.0",
1124
+ "@mesh-tech/esm-bundle": "^0.1.1",
1125
+ // The events service versions with the primitive that deploys it, not with
1126
+ // the lockstep group, so it carries its own floor.
1127
+ "@mesh-tech/events-service": "^0.1.0",
1128
+ // The operator UI EventsService deploys when it is installed; its own line.
1129
+ "@mesh-tech/events-ui": "^0.1.0",
1130
+ // Likewise authz: it is not in the `fixed` group in .changeset/config.json,
1131
+ // so the lockstep range resolves to a major line it has never published.
1132
+ "@mesh-tech/authz": "^1.19.0"
1133
+ };
1134
+ var SCAFFOLD_TOOLCHAIN = {
1135
+ "@types/node": "^24.2.1",
1136
+ tsx: "^4.20.4",
1137
+ typescript: "^5.9.2"
1138
+ };
1139
+ function isPrerelease(range) {
1140
+ return /[-+]/.test(range.replace(/^\^/, ""));
1141
+ }
1142
+ function toCaretRange(version) {
1143
+ const trimmed = version.trim();
1144
+ return /^\d+\.\d+\.\d+(?:[-+].*)?$/.test(trimmed) ? `^${trimmed}` : null;
1145
+ }
1146
+ function resolvePublishedRange(pkg, cwd) {
1147
+ try {
1148
+ const out = execFileSync("npm", ["view", pkg, "version"], {
1149
+ cwd,
1150
+ encoding: "utf-8",
1151
+ timeout: 15e3,
1152
+ stdio: ["ignore", "pipe", "ignore"]
1153
+ });
1154
+ const range = toCaretRange(out);
1155
+ if (range && !isPrerelease(range)) return { range, resolved: true };
1156
+ } catch {
1157
+ }
1158
+ return { range: SCAFFOLD_PACKAGE_LINES[pkg], resolved: false };
1159
+ }
1160
+
1161
+ // packages/mesh-cli/src/commands/create-app.ts
1162
+ var __filename = fileURLToPath(import.meta.url);
1163
+ var __dirname = path3.dirname(__filename);
1164
+ var packageRoot = findPackageRoot(__dirname);
1165
+ var TEMPLATES = {
1166
+ workflow: "Hono API + Temporal worker + hello workflow (Pulumi/app-kit)",
1167
+ "api-auth": "Hono API with platform auth: authn (Zitadel JWT) + authz (SpiceDB)",
1168
+ "api-role-gating": "Hono API with role-gating authz: authn (Zitadel JWT) + coarse-role checks on Zitadel project roles, in-process (no SpiceDB)",
1169
+ "external-service": "Hono API calling a third-party vendor via platform-managed credentials (ExternalService + mock for `mesh dev --externals`)"
1170
+ };
1171
+ var REMOVED_TEMPLATES = {
1172
+ "temporal-api-worker": "renamed to 'workflow'",
1173
+ "api-web-db": "removed (SST-based) \u2014 use --primitives service,database"
1174
+ };
1175
+ var PRIMITIVES = {
1176
+ service: "HTTP Service (API with Hono)",
1177
+ database: "Database (PostgreSQL via Prisma)",
1178
+ temporal: "Temporal (workflow orchestration)",
1179
+ bucket: "S3 Bucket (file storage)",
1180
+ events: "Events and webhooks (publish, subscriptions, signed delivery)"
1181
+ };
1182
+ var HUB_ACCOUNTS = {
1183
+ mesh: "159923586610"
1184
+ };
1185
+ var PLATFORM_MONOREPO_NAME = "mesh-platform";
1186
+ function isInsidePlatformMonorepo(dir) {
1187
+ let cur = path3.resolve(dir);
1188
+ while (cur !== path3.dirname(cur)) {
1189
+ if (fs3.existsSync(path3.join(cur, "pnpm-workspace.yaml"))) {
1190
+ try {
1191
+ const pkg = JSON.parse(fs3.readFileSync(path3.join(cur, "package.json"), "utf-8"));
1192
+ if (pkg?.name === PLATFORM_MONOREPO_NAME) return true;
1193
+ } catch {
1194
+ }
1195
+ }
1196
+ cur = path3.dirname(cur);
1197
+ }
1198
+ return false;
1199
+ }
1200
+ function resolveDeployerRoleArn(tenant, platformName, env) {
1201
+ const account = HUB_ACCOUNTS[platformName] ?? "ACCOUNT_ID";
1202
+ return `arn:aws:iam::${account}:role/${tenant}-${env}-apps-deployer`;
1203
+ }
1204
+ var VALID_PRIMITIVES = Object.keys(PRIMITIVES);
1205
+ function shouldBootstrapAppsRepo(cwd) {
1206
+ return fs3.existsSync(path3.join(cwd, ".git")) && !isInsidePlatformMonorepo(cwd) && !fs3.existsSync(path3.join(cwd, "package.json")) && !fs3.existsSync(path3.join(cwd, "tenants")) && !fs3.existsSync(path3.join(cwd, "apps"));
1207
+ }
1208
+ function bootstrapAppsRepo(cwd, tenant) {
1209
+ const templateDir = path3.join(packageRoot, "templates", "apps-repo");
1210
+ const registryAccount = HUB_ACCOUNTS.mesh;
1211
+ const context = {
1212
+ repoName: path3.basename(cwd),
1213
+ tenant,
1214
+ tenantTitle: tenant.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
1215
+ registryUrl: `https://mesh-platform-${registryAccount}.d.codeartifact.us-east-2.amazonaws.com/npm/mesh-packages/`,
1216
+ intentRange: INTENT_RANGE
1217
+ };
1218
+ const staging = fs3.mkdtempSync(path3.join(os.tmpdir(), "mesh-apps-repo-"));
1219
+ try {
1220
+ copyTemplate(templateDir, staging, context);
1221
+ for (const [from, to] of [
1222
+ ["gitignore", ".gitignore"],
1223
+ ["npmrc", ".npmrc"]
1224
+ ]) {
1225
+ const p = path3.join(staging, from);
1226
+ if (fs3.existsSync(p)) fs3.renameSync(p, path3.join(staging, to));
1227
+ }
1228
+ const created = [];
1229
+ for (const entry of fs3.readdirSync(staging)) {
1230
+ const dest = path3.join(cwd, entry);
1231
+ if (fs3.existsSync(dest)) continue;
1232
+ fs3.cpSync(path3.join(staging, entry), dest, { recursive: true });
1233
+ created.push(entry);
1234
+ }
1235
+ fs3.mkdirSync(path3.join(cwd, "apps"), { recursive: true });
1236
+ created.push("apps/");
1237
+ return created;
1238
+ } finally {
1239
+ fs3.rmSync(staging, { recursive: true, force: true });
1240
+ }
1241
+ }
1242
+ function ensureWorkspaceGlobs(root, required) {
1243
+ const file = path3.join(root, "pnpm-workspace.yaml");
1244
+ if (!fs3.existsSync(file)) return [];
1245
+ const text = fs3.readFileSync(file, "utf-8");
1246
+ let declared;
1247
+ try {
1248
+ declared = parseYaml2(text)?.packages ?? [];
1249
+ } catch {
1250
+ return [];
1251
+ }
1252
+ const missing = required.filter((glob) => !declared.includes(glob));
1253
+ if (missing.length === 0) return [];
1254
+ const lines = text.split("\n");
1255
+ const start = lines.findIndex((line) => /^packages:\s*$/.test(line));
1256
+ if (start === -1) return [];
1257
+ let end = start;
1258
+ for (let i = start + 1; i < lines.length; i++) {
1259
+ const line = lines[i] ?? "";
1260
+ const isEntry = /^\s+-\s/.test(line);
1261
+ if (isEntry || /^\s*#/.test(line) || line.trim() === "") {
1262
+ if (isEntry) end = i;
1263
+ continue;
1264
+ }
1265
+ break;
1266
+ }
1267
+ const indent = (lines[end] ?? "").match(/^(\s*)-/)?.[1] ?? " ";
1268
+ lines.splice(end + 1, 0, ...missing.map((glob) => `${indent}- ${glob}`));
1269
+ fs3.writeFileSync(file, lines.join("\n"));
1270
+ return missing;
1271
+ }
1272
+ function workspaceGlobForApp(root, appDir) {
1273
+ const rel2 = path3.relative(root, appDir).split(path3.sep).join("/");
1274
+ if (rel2 === "" || rel2.split("/")[0] === "..") return null;
1275
+ return `${path3.posix.dirname(rel2)}/*/*`;
1276
+ }
1277
+ function maybeBootstrapAppsRepo(cwd, tenant, test) {
1278
+ if (test || !shouldBootstrapAppsRepo(cwd)) return;
1279
+ const created = bootstrapAppsRepo(cwd, tenant);
1280
+ logInfo(`Standalone apps repo detected \u2014 bootstrapped workspace files: ${created.join(", ")}`);
1281
+ }
1282
+ Handlebars.registerHelper("titleCase", (str) => {
1283
+ return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1284
+ });
1285
+ function isInteractive() {
1286
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1287
+ }
1288
+ async function promptForOptions(options) {
1289
+ const { default: input } = await import("@inquirer/input");
1290
+ const { default: checkbox } = await import("@inquirer/checkbox");
1291
+ const tenant = options.tenant ?? await input({
1292
+ message: "Tenant name",
1293
+ default: findMeshJson(process.cwd())?.data.tenant,
1294
+ validate: (v) => v.length > 0 ? true : "Tenant name is required"
1295
+ });
1296
+ const name = options.name ?? await input({
1297
+ message: "App name",
1298
+ validate: (v) => v.length > 0 ? true : "App name is required"
1299
+ });
1300
+ let primitives;
1301
+ if (options.primitives) {
1302
+ primitives = parsePrimitives(options.primitives);
1303
+ } else {
1304
+ primitives = await checkbox({
1305
+ message: "Select primitives",
1306
+ choices: VALID_PRIMITIVES.map((p) => ({
1307
+ name: PRIMITIVES[p],
1308
+ value: p,
1309
+ checked: p === "service"
1310
+ // service checked by default
1311
+ }))
1312
+ });
1313
+ if (primitives.length === 0) {
1314
+ primitives = ["service"];
1315
+ }
1316
+ }
1317
+ if (primitives.includes("database") && !primitives.includes("service")) {
1318
+ primitives.push("service");
1319
+ logInfo("Added 'service' \u2014 database files live in the api/ directory.");
1320
+ }
1321
+ if (primitives.includes("bucket") && !primitives.includes("service")) {
1322
+ primitives.push("service");
1323
+ logInfo("Added 'service' \u2014 bucket requires an API service for S3 helpers.");
1324
+ }
1325
+ if (primitives.includes("events") && !primitives.includes("service")) {
1326
+ primitives.push("service");
1327
+ logInfo("Added 'service' \u2014 the events publisher lives in the api/ directory.");
1328
+ }
1329
+ return { tenant, name, primitives };
1330
+ }
1331
+ function copyTemplate(srcDir, destDir, context) {
1332
+ const entries = fs3.readdirSync(srcDir, { withFileTypes: true });
1333
+ for (const entry of entries) {
1334
+ const srcPath = path3.join(srcDir, entry.name);
1335
+ let destName = entry.name;
1336
+ if (destName.endsWith(".hbs")) {
1337
+ destName = destName.slice(0, -4);
1338
+ }
1339
+ if (destName.includes("{{")) {
1340
+ destName = Handlebars.compile(destName)(context);
1341
+ }
1342
+ const destPath = path3.join(destDir, destName);
1343
+ if (entry.isDirectory()) {
1344
+ fs3.mkdirSync(destPath, { recursive: true });
1345
+ copyTemplate(srcPath, destPath, context);
1346
+ } else if (entry.name.endsWith(".hbs")) {
1347
+ const templateContent = fs3.readFileSync(srcPath, "utf-8");
1348
+ const template = Handlebars.compile(templateContent);
1349
+ const output = template(context);
1350
+ fs3.writeFileSync(destPath, output);
1351
+ } else {
1352
+ fs3.copyFileSync(srcPath, destPath);
1353
+ }
1354
+ }
1355
+ }
1356
+ function parsePrimitives(input) {
1357
+ const selected = input.split(",").map((s) => s.trim().toLowerCase());
1358
+ const invalid = selected.filter(
1359
+ (s) => !VALID_PRIMITIVES.includes(s)
1360
+ );
1361
+ if (invalid.length > 0) {
1362
+ logError(`Unknown primitives: ${invalid.join(", ")}`);
1363
+ logInfo(`Available primitives: ${VALID_PRIMITIVES.join(", ")}`);
1364
+ process.exit(1);
1365
+ }
1366
+ return selected;
1367
+ }
1368
+ function describeNoAppsHome(cwd, tenant, baseDir, test) {
1369
+ if (test || isInsidePlatformMonorepo(cwd)) {
1370
+ return {
1371
+ message: `Tenant directory not found: ${baseDir}/${tenant}/`,
1372
+ hint: "Create the tenant directory first, or run from within it."
1373
+ };
1374
+ }
1375
+ return {
1376
+ message: `This directory is not a Mesh apps repo \u2014 no apps/ here${fs3.existsSync(path3.join(cwd, ".git")) ? "" : " and it is not a git repo"}.`,
1377
+ hint: `Run: mesh init (turns this folder into your ${tenant}-mesh-apps repo), or cd into that repo and re-run mesh create-app.`
1378
+ };
1379
+ }
1380
+ function resolveAppDir(cwd, tenant, name, test) {
1381
+ const baseDir = test ? "tests/tenants" : "tenants";
1382
+ const possiblePaths = test ? [path3.join(cwd, baseDir, tenant, "apps")] : [
1383
+ path3.join(cwd, baseDir, tenant, "apps"),
1384
+ path3.join(cwd, "..", tenant, "apps"),
1385
+ path3.join(cwd, "apps")
1386
+ ];
1387
+ let appsDir = possiblePaths.find((p) => fs3.existsSync(p));
1388
+ if (!appsDir) {
1389
+ appsDir = path3.join(cwd, baseDir, tenant, "apps");
1390
+ if (!fs3.existsSync(path3.join(cwd, baseDir, tenant))) {
1391
+ const { message, hint } = describeNoAppsHome(cwd, tenant, baseDir, test);
1392
+ logError(message);
1393
+ logInfo(hint);
1394
+ process.exit(1);
1395
+ }
1396
+ if (!fs3.existsSync(appsDir)) {
1397
+ logInfo(`Creating apps directory: ${appsDir}`);
1398
+ fs3.mkdirSync(appsDir, { recursive: true });
1399
+ }
1400
+ }
1401
+ return path3.join(appsDir, name);
1402
+ }
1403
+ function generateComposableApp(appDir, context) {
1404
+ const fragmentsDir = path3.join(packageRoot, "fragments");
1405
+ const baseDir = path3.join(fragmentsDir, "base");
1406
+ if (!fs3.existsSync(baseDir)) {
1407
+ logError(`Fragments directory not found: ${baseDir}`);
1408
+ logInfo("Make sure the mesh-cli package is installed correctly.");
1409
+ process.exit(1);
1410
+ }
1411
+ copyTemplate(baseDir, appDir, context);
1412
+ if (context.service) {
1413
+ const serviceDir = path3.join(fragmentsDir, "service");
1414
+ if (!fs3.existsSync(serviceDir)) {
1415
+ throw new Error(`Fragment directory not found: ${serviceDir}`);
1416
+ }
1417
+ copyTemplate(serviceDir, appDir, context);
1418
+ }
1419
+ if (context.database) {
1420
+ const dbDir = path3.join(fragmentsDir, "database");
1421
+ if (!fs3.existsSync(dbDir)) {
1422
+ throw new Error(`Fragment directory not found: ${dbDir}`);
1423
+ }
1424
+ copyTemplate(dbDir, appDir, context);
1425
+ }
1426
+ if (context.temporal) {
1427
+ const temporalDir = path3.join(fragmentsDir, "temporal");
1428
+ if (!fs3.existsSync(temporalDir)) {
1429
+ throw new Error(`Fragment directory not found: ${temporalDir}`);
1430
+ }
1431
+ copyTemplate(temporalDir, appDir, context);
1432
+ }
1433
+ if (context.bucket) {
1434
+ const bucketDir = path3.join(fragmentsDir, "bucket");
1435
+ if (!fs3.existsSync(bucketDir)) {
1436
+ throw new Error(`Fragment directory not found: ${bucketDir}`);
1437
+ }
1438
+ copyTemplate(bucketDir, appDir, context);
1439
+ }
1440
+ if (context.events) {
1441
+ const eventsDir = path3.join(fragmentsDir, "events");
1442
+ if (!fs3.existsSync(eventsDir)) {
1443
+ throw new Error(`Fragment directory not found: ${eventsDir}`);
1444
+ }
1445
+ copyTemplate(eventsDir, appDir, context);
1446
+ }
1447
+ const agentsDir = path3.join(fragmentsDir, "agents");
1448
+ if (fs3.existsSync(agentsDir)) {
1449
+ copyTemplate(agentsDir, appDir, context);
1450
+ }
1451
+ }
1452
+ function registerCreateAppCommand(program) {
1453
+ program.command("create-app").description("Scaffold a new tenant application").option("--tenant <tenant>", "Tenant name (e.g., acme)").option("--name <name>", "Application name (e.g., billing)").option(
1454
+ "--template <template>",
1455
+ `Legacy template: ${Object.keys(TEMPLATES).join(", ")}`
1456
+ ).option(
1457
+ "--primitives <primitives>",
1458
+ `Comma-separated primitives: ${VALID_PRIMITIVES.join(", ")} (default: service)`
1459
+ ).option("--test", "Create in tests/tenants/ directory", false).option(
1460
+ "--skip-registry-check",
1461
+ "Scaffold without checking that this machine can install @mesh-tech packages (offline use; run `mesh registry login` before `pnpm install`)",
1462
+ false
1463
+ ).action(async (options) => {
1464
+ await ensureRegistryAccess(process.cwd(), options);
1465
+ if (options.template) {
1466
+ if (!options.tenant || !options.name) {
1467
+ logError("--tenant and --name are required with --template");
1468
+ process.exit(1);
1469
+ }
1470
+ await runLegacyTemplate(
1471
+ options.tenant,
1472
+ options.name,
1473
+ options.template,
1474
+ options.test
1475
+ );
1476
+ return;
1477
+ }
1478
+ let tenant;
1479
+ let name;
1480
+ let primitives;
1481
+ const needsPrompt = !options.tenant || !options.name || !options.primitives;
1482
+ if (needsPrompt && isInteractive()) {
1483
+ const prompted = await promptForOptions(options);
1484
+ tenant = prompted.tenant;
1485
+ name = prompted.name;
1486
+ primitives = prompted.primitives;
1487
+ } else if (needsPrompt) {
1488
+ options.tenant ??= findMeshJson(process.cwd())?.data.tenant;
1489
+ if (!options.tenant) {
1490
+ logError("--tenant is required (no TTY for interactive mode)");
1491
+ process.exit(1);
1492
+ }
1493
+ if (!options.name) {
1494
+ logError("--name is required (no TTY for interactive mode)");
1495
+ process.exit(1);
1496
+ }
1497
+ tenant = options.tenant;
1498
+ name = options.name;
1499
+ primitives = options.primitives ? parsePrimitives(options.primitives) : ["service"];
1500
+ } else {
1501
+ tenant = options.tenant;
1502
+ name = options.name;
1503
+ primitives = parsePrimitives(options.primitives);
1504
+ }
1505
+ if (primitives.includes("database") && !primitives.includes("service")) {
1506
+ primitives.push("service");
1507
+ logInfo("Added 'service' \u2014 database files live in the api/ directory.");
1508
+ }
1509
+ if (primitives.includes("bucket") && !primitives.includes("service")) {
1510
+ primitives.push("service");
1511
+ logInfo("Added 'service' \u2014 bucket requires an API service for S3 helpers.");
1512
+ }
1513
+ if (primitives.includes("events") && !primitives.includes("service")) {
1514
+ primitives.push("service");
1515
+ logInfo("Added 'service' \u2014 the events publisher lives in the api/ directory.");
1516
+ }
1517
+ await runComposable(tenant, name, primitives, options.test);
1518
+ });
1519
+ }
1520
+ var NO_REGISTRY_ACCESS_MESSAGE = "No registry access on this machine \u2014 @mesh-tech packages cannot be installed.\n Run: mesh init\n (sets up your tenant and signs you in to the package registry)\n Scaffolding offline anyway? add --skip-registry-check";
1521
+ async function ensureRegistryAccess(cwd, options, preflight = registryPreflight) {
1522
+ if (isInsidePlatformMonorepo(cwd)) return;
1523
+ if (options.skipRegistryCheck) {
1524
+ logWarn("Skipping the registry check \u2014 `pnpm install` will need `mesh registry login` first.");
1525
+ return;
1526
+ }
1527
+ const pf = await preflight();
1528
+ if (pf.state === "unreachable") {
1529
+ logWarn(`Could not reach the package registry to verify access (${pf.detail ?? "unknown error"}) \u2014 continuing.`);
1530
+ return;
1531
+ }
1532
+ if (pf.state !== "valid") {
1533
+ throw new MeshCliError(NO_REGISTRY_ACCESS_MESSAGE);
1534
+ }
1535
+ logSuccess(`Registry access OK (${pf.email ?? "token valid"})`);
1536
+ }
1537
+ async function runLegacyTemplate(tenant, name, template, test) {
1538
+ if (!Object.keys(TEMPLATES).includes(template)) {
1539
+ const removed = REMOVED_TEMPLATES[template];
1540
+ logError(`Unknown template: ${template}${removed ? ` (${removed})` : ""}`);
1541
+ logInfo(`Available templates:`);
1542
+ for (const [tplName, desc] of Object.entries(TEMPLATES)) {
1543
+ console.log(` ${tplName.padEnd(20)} - ${desc}`);
1544
+ }
1545
+ process.exit(1);
1546
+ }
1547
+ logInfo(`Creating app '${name}' for tenant '${tenant}'...`);
1548
+ logInfo(`Template: ${template} (${TEMPLATES[template]})`);
1549
+ if (test) {
1550
+ logInfo("Creating in tests/ directory");
1551
+ }
1552
+ maybeBootstrapAppsRepo(process.cwd(), tenant, test);
1553
+ const appDir = resolveAppDir(process.cwd(), tenant, name, test);
1554
+ if (fs3.existsSync(appDir)) {
1555
+ logError(`App directory already exists: ${appDir}`);
1556
+ process.exit(1);
1557
+ }
1558
+ const templateDir = path3.join(packageRoot, "templates", template);
1559
+ if (!fs3.existsSync(templateDir)) {
1560
+ logError(`Template not found: ${templateDir}`);
1561
+ process.exit(1);
1562
+ }
1563
+ fs3.mkdirSync(appDir, { recursive: true });
1564
+ try {
1565
+ copyTemplate(templateDir, appDir, { name, tenant });
1566
+ logSuccess(`Created app at ${appDir}`);
1567
+ } catch (error) {
1568
+ logError(`Failed to create app: ${error}`);
1569
+ fs3.rmSync(appDir, { recursive: true, force: true });
1570
+ process.exit(1);
1571
+ }
1572
+ autoSyncSkills(appDir, name);
1573
+ await checkScaffoldContract(appDir);
1574
+ printLegacyNextSteps(appDir, template);
1575
+ }
1576
+ function autoSyncSkills(appDir, appName) {
1577
+ const root = resolveTargetRoot(appDir);
1578
+ try {
1579
+ syncSkills(root);
1580
+ } catch (err) {
1581
+ logWarn(`Agent-skill sync skipped: ${err instanceof Error ? err.message : err} \u2014 run: mesh skills sync`);
1582
+ }
1583
+ try {
1584
+ const seeded = seedAppSkill(root, appName, path3.relative(root, appDir));
1585
+ if (seeded) logSuccess(`seeded: ${seeded} (yours to grow \u2014 not managed by mesh skills sync)`);
1586
+ } catch (err) {
1587
+ logWarn(`App skill stub skipped: ${err instanceof Error ? err.message : err}`);
1588
+ }
1589
+ }
1590
+ async function checkScaffoldContract(appDir) {
1591
+ const root = resolveTargetRoot(appDir);
1592
+ const report = await runAppCheck(root, [path3.relative(root, appDir) || "."]);
1593
+ if (report.status === "ok") {
1594
+ logSuccess("app contract: met (mesh app check)");
1595
+ return;
1596
+ }
1597
+ console.log(renderReport(report, { verbose: false }));
1598
+ if (report.status === "warn") {
1599
+ logSuccess("app contract: met \u2014 the \u26A0 lines above are advisories, not blocking findings (mesh app check)");
1600
+ return;
1601
+ }
1602
+ if (report.status === "error") {
1603
+ logError("the scaffold does not meet the Mesh app contract \u2014 this is a template defect; file a platform item and cite the \u2717 lines above");
1604
+ process.exitCode = 1;
1605
+ }
1606
+ }
1607
+ async function runComposable(tenant, name, primitives, test) {
1608
+ logInfo(`Creating app '${name}' for tenant '${tenant}'...`);
1609
+ logInfo(`Primitives: ${primitives.join(", ")}`);
1610
+ if (test) {
1611
+ logInfo("Creating in tests/ directory");
1612
+ }
1613
+ maybeBootstrapAppsRepo(process.cwd(), tenant, test);
1614
+ const appDir = resolveAppDir(process.cwd(), tenant, name, test);
1615
+ if (fs3.existsSync(appDir)) {
1616
+ logError(`App directory already exists: ${appDir}`);
1617
+ process.exit(1);
1618
+ }
1619
+ const region = "us-east-2";
1620
+ const platformName = "mesh";
1621
+ const platformEnv = "dev";
1622
+ const workspaceDeps = isInsidePlatformMonorepo(appDir);
1623
+ const repoRoot = resolveTargetRoot(process.cwd());
1624
+ const mesh = workspaceDeps ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/app-kit", repoRoot);
1625
+ const esmBundle = workspaceDeps ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/esm-bundle", repoRoot);
1626
+ const eventsService = workspaceDeps || !primitives.includes("events") ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/events-service", repoRoot);
1627
+ const eventsUi = workspaceDeps || !primitives.includes("events") ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/events-ui", repoRoot);
1628
+ const authz = workspaceDeps || !primitives.includes("events") ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/authz", repoRoot);
1629
+ if (!workspaceDeps) {
1630
+ logInfo(
1631
+ mesh.resolved ? `@mesh-tech/* pinned to ${mesh.range} (current published line)` : `Registry lookup unavailable \u2014 pinning @mesh-tech/* to ${mesh.range}; run \`pnpm up @mesh-tech/*\` once you have registry auth`
1632
+ );
1633
+ }
1634
+ const context = {
1635
+ name,
1636
+ tenant,
1637
+ service: primitives.includes("service"),
1638
+ database: primitives.includes("database"),
1639
+ temporal: primitives.includes("temporal"),
1640
+ bucket: primitives.includes("bucket"),
1641
+ events: primitives.includes("events"),
1642
+ region,
1643
+ workspaceDeps,
1644
+ meshRange: mesh.range,
1645
+ esmBundleRange: esmBundle.range,
1646
+ eventsServiceRange: eventsService.range,
1647
+ eventsUiRange: eventsUi.range,
1648
+ authzRange: authz.range,
1649
+ typesNodeRange: SCAFFOLD_TOOLCHAIN["@types/node"],
1650
+ tsxRange: SCAFFOLD_TOOLCHAIN.tsx,
1651
+ typescriptRange: SCAFFOLD_TOOLCHAIN.typescript,
1652
+ deployerRoleArn: resolveDeployerRoleArn(tenant, platformName, platformEnv)
1653
+ };
1654
+ fs3.mkdirSync(appDir, { recursive: true });
1655
+ try {
1656
+ generateComposableApp(appDir, context);
1657
+ logSuccess(`Created app at ${appDir}`);
1658
+ } catch (error) {
1659
+ logError(`Failed to create app: ${error}`);
1660
+ fs3.rmSync(appDir, { recursive: true, force: true });
1661
+ process.exit(1);
1662
+ }
1663
+ const nested = fs3.readdirSync(appDir, { withFileTypes: true }).some((entry) => entry.isDirectory() && fs3.existsSync(path3.join(appDir, entry.name, "package.json")));
1664
+ const glob = workspaceGlobForApp(repoRoot, appDir);
1665
+ if (nested && glob) {
1666
+ const added = ensureWorkspaceGlobs(repoRoot, [glob]);
1667
+ if (added.length > 0) {
1668
+ logInfo(`pnpm-workspace.yaml: added ${added.join(", ")} so the app's api/worker packages install`);
1669
+ }
1670
+ }
1671
+ autoSyncSkills(appDir, name);
1672
+ await checkScaffoldContract(appDir);
1673
+ printComposableNextSteps(appDir, context);
1674
+ }
1675
+ function printLegacyNextSteps(appDir, _template) {
1676
+ console.log("");
1677
+ logSuccess("App created successfully!");
1678
+ console.log("");
1679
+ console.log("Next steps:");
1680
+ console.log(` cd ${appDir}`);
1681
+ console.log(" pnpm install");
1682
+ console.log(" pnpm mesh dev");
1683
+ console.log("");
1684
+ console.log("To deploy:");
1685
+ console.log(" pnpm mesh deploy up");
1686
+ console.log("");
1687
+ }
1688
+ function printComposableNextSteps(appDir, context) {
1689
+ console.log("");
1690
+ logSuccess("App created successfully!");
1691
+ console.log("");
1692
+ console.log("Generated:");
1693
+ console.log(" index.ts Pulumi app definition");
1694
+ console.log(" AGENTS.md AI agent instructions");
1695
+ console.log(" CLAUDE.md Bridges AGENTS.md into Claude Code (@AGENTS.md)");
1696
+ if (context.service) {
1697
+ console.log(" api/ HTTP service (Hono)");
1698
+ }
1699
+ if (context.temporal) {
1700
+ console.log(" worker/ Temporal worker");
1701
+ }
1702
+ if (context.database) {
1703
+ console.log(" prisma/ Database schema");
1704
+ }
1705
+ if (context.bucket) {
1706
+ console.log(" api/src/storage.ts S3 helpers");
1707
+ }
1708
+ console.log("");
1709
+ for (const line of composableNextSteps(appDir, findMeshJson(process.cwd())?.data.platform ?? null)) {
1710
+ console.log(line);
1711
+ }
1712
+ }
1713
+ function composableNextSteps(appDir, platform) {
1714
+ const common = [
1715
+ "Next steps:",
1716
+ ` cd ${appDir}`,
1717
+ " pnpm install",
1718
+ " mesh skills sync # re-run once deps are installed: picks up package skills + Intent",
1719
+ " pnpm install # again only if skills sync reports it added @tanstack/intent"
1720
+ ];
1721
+ const local = [
1722
+ " mesh start # the local Mesh platform (once; Docker \u2014 from anywhere)",
1723
+ " mesh dev # run the app against it"
1724
+ ];
1725
+ const deploy = [
1726
+ " mesh stack init # personal dev stack (deploy: false)",
1727
+ " mesh deploy up --yes # deploy via the stack's deployer role"
1728
+ ];
1729
+ if (platform === null || platform === "local") {
1730
+ return [...common, ...local, "", "When you deploy to a Mesh platform:", ...deploy];
1731
+ }
1732
+ return [...common, ...deploy, " mesh dev # run locally against the platform"];
1733
+ }
1734
+
1735
+ export {
1736
+ registerAppCommands,
1737
+ PLATFORM_MONOREPO_NAME,
1738
+ isInsidePlatformMonorepo,
1739
+ shouldBootstrapAppsRepo,
1740
+ bootstrapAppsRepo,
1741
+ ensureWorkspaceGlobs,
1742
+ workspaceGlobForApp,
1743
+ copyTemplate,
1744
+ describeNoAppsHome,
1745
+ resolveAppDir,
1746
+ generateComposableApp,
1747
+ registerCreateAppCommand,
1748
+ NO_REGISTRY_ACCESS_MESSAGE,
1749
+ ensureRegistryAccess,
1750
+ composableNextSteps
1751
+ };