@mesh-tech/mesh-cli 0.35.2 → 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 (107) 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 -26414
  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/create-app.d.ts +1 -0
  72. package/dist/src/commands/create-app.js +4 -0
  73. package/dist/src/commands/dev.d.ts +6 -0
  74. package/dist/src/commands/dev.js +152 -3
  75. package/dist/src/commands/docs.js +33 -0
  76. package/dist/src/commands/local/act-as-local.d.ts +5 -0
  77. package/dist/src/commands/local/act-as-local.js +41 -0
  78. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  79. package/dist/src/commands/local/auth-provision.js +1 -0
  80. package/dist/src/commands/local/index.js +4 -0
  81. package/dist/src/commands/local/local-api-key.d.ts +44 -0
  82. package/dist/src/commands/local/local-api-key.js +115 -0
  83. package/dist/src/commands/local/local-spicedb.d.ts +95 -0
  84. package/dist/src/commands/local/local-spicedb.js +304 -0
  85. package/dist/src/commands/local/publish-docs.d.ts +81 -0
  86. package/dist/src/commands/local/publish-docs.js +328 -0
  87. package/dist/src/commands/local/seed-zitadel.js +2 -0
  88. package/dist/src/commands/temporal.js +191 -61
  89. package/dist/src/commands/temporal.js.map +3 -3
  90. package/dist/src/docs/convert.d.ts +35 -0
  91. package/dist/src/docs/convert.js +276 -0
  92. package/dist/src/docs/preview-source.d.ts +13 -0
  93. package/dist/src/docs/preview-source.js +58 -0
  94. package/dist/src/docs/preview.d.ts +32 -0
  95. package/dist/src/docs/preview.js +279 -0
  96. package/dist/src/utils/scaffold-versions.d.ts +1 -0
  97. package/dist/src/utils/scaffold-versions.js +1 -0
  98. package/fragments/agents/AGENTS.md.hbs +27 -27
  99. package/fragments/base/README.md.hbs +5 -2
  100. package/fragments/base/index.ts.hbs +22 -33
  101. package/fragments/base/package.json.hbs +5 -1
  102. package/fragments/service/api/openapi.json.hbs +1 -1
  103. package/fragments/service/docs/docs.yaml.hbs +10 -0
  104. package/fragments/service/docs/pages/getting-started.md.hbs +6 -9
  105. package/package.json +19 -3
  106. package/skills/mesh/SKILL.md +2 -2
  107. package/stack/docker-compose.hub.yml +6 -0
@@ -515,6 +515,9 @@ async function runComposable(tenant, name, primitives, test) {
515
515
  const eventsService = workspaceDeps || !primitives.includes("events")
516
516
  ? { range: "", resolved: true }
517
517
  : resolvePublishedRange("@mesh-tech/events-service", repoRoot);
518
+ const eventsUi = workspaceDeps || !primitives.includes("events")
519
+ ? { range: "", resolved: true }
520
+ : resolvePublishedRange("@mesh-tech/events-ui", repoRoot);
518
521
  const authz = workspaceDeps || !primitives.includes("events")
519
522
  ? { range: "", resolved: true }
520
523
  : resolvePublishedRange("@mesh-tech/authz", repoRoot);
@@ -536,6 +539,7 @@ async function runComposable(tenant, name, primitives, test) {
536
539
  meshRange: mesh.range,
537
540
  esmBundleRange: esmBundle.range,
538
541
  eventsServiceRange: eventsService.range,
542
+ eventsUiRange: eventsUi.range,
539
543
  authzRange: authz.range,
540
544
  typesNodeRange: SCAFFOLD_TOOLCHAIN["@types/node"],
541
545
  tsxRange: SCAFFOLD_TOOLCHAIN.tsx,
@@ -41,6 +41,10 @@ export interface SessionState {
41
41
  runner?: 'tmux' | 'docker';
42
42
  composeExternals?: ComposeExternalRef[];
43
43
  externalProbeFiles?: string[];
44
+ localDocs?: {
45
+ tenant: string;
46
+ app: string;
47
+ };
44
48
  workflowFingerprint?: string;
45
49
  }
46
50
  export declare function workflowChanged(prior: string | undefined, current: string): boolean;
@@ -53,6 +57,8 @@ export declare function buildChildAwsEnv(awsEnv: Record<string, string>, method:
53
57
  sessionScratchDir: string;
54
58
  meshBin: string;
55
59
  }): Record<string, string>;
60
+ export declare function readDotEnvFile(file: string): Record<string, string>;
61
+ export declare function rebaseServiceSrc(src: string, monorepoRoot: string | null): string;
56
62
  export declare function isLinkedDependencyDir(serviceDir: string, appRoot: string): boolean;
57
63
  export declare function monorepoRelativeSrc(src: string): string | null;
58
64
  export declare function tunnelClientAddress(tunnel: {
@@ -19,7 +19,11 @@ import { fingerprintWorkflowSource } from '../utils/workflow-fingerprint.js';
19
19
  import { buildLaunchCommand, envFileName, waitForPort, writeEnvFile } from './dev-launch.js';
20
20
  import { buildLocalDevOutput, detectLocalTenant, ensureLocalPlatformRunning, hasStackBacking, localAppNamespace } from './local/dev-local.js';
21
21
  import { ensureAppTenantAuth, ensureSignInApp, registerLocalApp } from './local/auth-provision.js';
22
- import { localAwsEnv, ensureTemporalNamespace } from './local/seed.js';
22
+ import { localAwsEnv, ensureTemporalNamespace, LOCAL_AWS_CONFIG, LOCAL_ENV } from './local/seed.js';
23
+ import { publishLocalDocs, readLocalPublishDeclaration, unpublishLocalDocs } from './local/publish-docs.js';
24
+ import { actAsEnv, publishLocalHubActAsKey } from './local/act-as-local.js';
25
+ import { ensureLocalApiKey, ensureLocalStaffKey, readLocalKeyDeclaration } from './local/local-api-key.js';
26
+ import { api as zitadelApi, readHubAuth, readSeederPat } from './local/seed-zitadel.js';
23
27
  import { composeExternalUp, composeExternalsDown, externalMode, localProbesRemove, parseExternalsSelection, readLocalMocks, seedLocalMock, } from './local/mocks.js';
24
28
  import { logShipperPath } from './local/dev-local.js';
25
29
  import { writeAppServiceProbes } from './local/stack.js';
@@ -565,9 +569,49 @@ export function buildChildAwsEnv(awsEnv, method, profile, opts) {
565
569
  }
566
570
  return result;
567
571
  }
568
- function rebaseServiceSrc(src, monorepoRoot) {
572
+ export function readDotEnvFile(file) {
573
+ let text;
574
+ try {
575
+ text = fs.readFileSync(file, 'utf-8');
576
+ }
577
+ catch {
578
+ return {};
579
+ }
580
+ const out = {};
581
+ for (const raw of text.split(/\r?\n/)) {
582
+ const line = raw.trim();
583
+ if (!line || line.startsWith('#'))
584
+ continue;
585
+ const eq = line.indexOf('=');
586
+ if (eq <= 0)
587
+ continue;
588
+ const key = line.slice(0, eq).replace(/^export\s+/, '').trim();
589
+ let value = line.slice(eq + 1).trim();
590
+ if (value[0] === '"' || value[0] === "'") {
591
+ const close = value.indexOf(value[0], 1);
592
+ value = close > 0 ? value.slice(1, close) : value.slice(1);
593
+ }
594
+ else {
595
+ const hash = value.search(/\s#/);
596
+ if (hash >= 0)
597
+ value = value.slice(0, hash).trimEnd();
598
+ }
599
+ out[key] = value;
600
+ }
601
+ return out;
602
+ }
603
+ async function unpublishSessionDocs(localDocs) {
604
+ if (!localDocs)
605
+ return;
606
+ const removed = await unpublishLocalDocs(localDocs.tenant, localDocs.app, LOCAL_AWS_CONFIG);
607
+ if (removed.length > 0)
608
+ logInfo(`Took ${localDocs.app}'s docs off the local Hub (${removed.join(', ')}).`);
609
+ }
610
+ export function rebaseServiceSrc(src, monorepoRoot) {
569
611
  if (!monorepoRoot)
570
612
  return src;
613
+ if (src === '.' || src === './')
614
+ return src;
571
615
  const rel = monorepoRelativeSrc(src);
572
616
  if (rel === null)
573
617
  return src;
@@ -1623,6 +1667,7 @@ export function registerDevCommand(program) {
1623
1667
  dockerDevDown(sessionName);
1624
1668
  composeExternalsDown(loadSessionState(sessionName)?.composeExternals);
1625
1669
  localProbesRemove(loadSessionState(sessionName)?.externalProbeFiles);
1670
+ await unpublishSessionDocs(loadSessionState(sessionName)?.localDocs);
1626
1671
  removeSessionState(sessionName);
1627
1672
  logSuccess(`Killed docker dev session: ${sessionName}`);
1628
1673
  }
@@ -1634,6 +1679,7 @@ export function registerDevCommand(program) {
1634
1679
  if (options.kill) {
1635
1680
  composeExternalsDown(loadSessionState(sessionName)?.composeExternals);
1636
1681
  localProbesRemove(loadSessionState(sessionName)?.externalProbeFiles);
1682
+ await unpublishSessionDocs(loadSessionState(sessionName)?.localDocs);
1637
1683
  try {
1638
1684
  const devOutput = loadSessionState(sessionName)?.devOutput;
1639
1685
  if (devOutput) {
@@ -1672,6 +1718,7 @@ export function registerDevCommand(program) {
1672
1718
  if (options.kill) {
1673
1719
  composeExternalsDown(loadSessionState(sessionName)?.composeExternals);
1674
1720
  localProbesRemove(loadSessionState(sessionName)?.externalProbeFiles);
1721
+ await unpublishSessionDocs(loadSessionState(sessionName)?.localDocs);
1675
1722
  if (loadSessionState(sessionName)?.runner === 'docker') {
1676
1723
  dockerDevDown(sessionName);
1677
1724
  removeSessionState(sessionName);
@@ -1774,7 +1821,9 @@ export function registerDevCommand(program) {
1774
1821
  return;
1775
1822
  }
1776
1823
  let signInServices = [];
1824
+ let provisioned;
1777
1825
  let appVersion;
1826
+ let appDisplayName;
1778
1827
  try {
1779
1828
  const tenant = localTenant;
1780
1829
  const app = rawDevOutput.app ?? projectName;
@@ -1793,10 +1842,13 @@ export function registerDevCommand(program) {
1793
1842
  }
1794
1843
  if (typeof appPkg?.version === 'string')
1795
1844
  appVersion = appPkg.version;
1845
+ if (typeof appPkg?.mesh?.displayName === 'string' && appPkg.mesh.displayName) {
1846
+ appDisplayName = appPkg.mesh.displayName;
1847
+ }
1796
1848
  }
1797
1849
  catch {
1798
1850
  }
1799
- await ensureAppTenantAuth({ tenant, app, services: authServices, roles: authRoles });
1851
+ provisioned = await ensureAppTenantAuth({ tenant, app, services: authServices, roles: authRoles });
1800
1852
  await ensureTemporalNamespace(localAppNamespace(tenant, app));
1801
1853
  }
1802
1854
  catch (err) {
@@ -1828,6 +1880,7 @@ export function registerDevCommand(program) {
1828
1880
  tenant: localTenant,
1829
1881
  app: devOutput.app ?? projectName,
1830
1882
  version: appVersion,
1883
+ ...(appDisplayName ? { displayName: appDisplayName } : {}),
1831
1884
  services: Object.keys(devOutput.services),
1832
1885
  ports: Object.fromEntries(Object.entries(devOutput.services).map(([name, svc]) => [name, svc.port])),
1833
1886
  kinds: Object.fromEntries(Object.entries(devOutput.services).flatMap(([name, svc]) => svc.kind ? [[name, svc.kind]] : [])),
@@ -1839,6 +1892,100 @@ export function registerDevCommand(program) {
1839
1892
  catch (err) {
1840
1893
  logWarn(`Local registry registration skipped: ${err instanceof Error ? err.message : err}`);
1841
1894
  }
1895
+ let localDocs;
1896
+ try {
1897
+ const appPkg = JSON.parse(fs.readFileSync(path.join(appRoot, 'package.json'), 'utf-8'));
1898
+ const declaration = readLocalPublishDeclaration(appPkg, appRoot);
1899
+ if (declaration.docs || declaration.authzMetadata || declaration.problems.length > 0) {
1900
+ const app = devOutput.app ?? projectName;
1901
+ const serviceNames = Object.keys(devOutput.services).filter((n) => !n.startsWith('mock-'));
1902
+ const httpService = declaration.docs?.service ?? serviceNames[0];
1903
+ const service = httpService ? devOutput.services[httpService] : undefined;
1904
+ if (!httpService || !service?.port) {
1905
+ logWarn(`mesh.docs names service '${httpService ?? ''}', which this app does not run — docs not published.`);
1906
+ }
1907
+ else {
1908
+ const serviceDir = path.resolve(appRoot, service.src);
1909
+ const env = service.env ?? (service.env = {});
1910
+ const serviceEnv = {
1911
+ ...readDotEnvFile(path.join(serviceDir, '.env')),
1912
+ ...Object.fromEntries(Object.entries(process.env).filter((e) => typeof e[1] === 'string')),
1913
+ ...Object.fromEntries(Object.entries(env).map(([k, v]) => [k, v.value])),
1914
+ };
1915
+ const published = await publishLocalDocs({
1916
+ appRoot,
1917
+ tenant: localTenant,
1918
+ app,
1919
+ httpPort: service.port,
1920
+ serviceEnv,
1921
+ ...(provisioned
1922
+ ? { zitadel: { orgId: provisioned.orgId, projectId: provisioned.projectId, issuer: 'http://localhost:8080' } }
1923
+ : {}),
1924
+ awsConfig: LOCAL_AWS_CONFIG,
1925
+ declaration,
1926
+ });
1927
+ for (const [key, value] of Object.entries(published.pins))
1928
+ env[key] = { value };
1929
+ for (const [key, value] of Object.entries(published.authzEnv))
1930
+ env[key] = { value };
1931
+ if (published.sandbox)
1932
+ env.INTEGRATION_SANDBOX = { value: published.sandbox };
1933
+ if (published.actAs) {
1934
+ const publication = await publishLocalHubActAsKey(LOCAL_AWS_CONFIG);
1935
+ if (!publication) {
1936
+ logWarn('The local Hub has no machine key yet — try-it cannot reach this app. Run: mesh start');
1937
+ }
1938
+ else {
1939
+ for (const [key, value] of Object.entries(actAsEnv(publication, localTenant, LOCAL_ENV, app))) {
1940
+ env[key] = { value };
1941
+ }
1942
+ const relays = declaration.docs?.integrationTryIt ?? [];
1943
+ if (relays.length > 0)
1944
+ env.INTEGRATION_TRY_IT_INTEGRATIONS = { value: relays.join(",") };
1945
+ }
1946
+ }
1947
+ if (published.docsPublished) {
1948
+ localDocs = { tenant: localTenant, app };
1949
+ logInfo(`Published ${app}'s docs to the local Hub (sandbox: ${published.sandbox}` +
1950
+ `${published.actAs ? ', try-it on' : ', try-it off'}` +
1951
+ `${Object.keys(published.pins).length ? `; pinned ${Object.entries(published.pins).map(([k, v]) => `${k}=${v}`).join(' ')}` : ''}).`);
1952
+ }
1953
+ if (published.authzPublished) {
1954
+ logInfo(`Published ${app}'s API-key metadata to the local Hub` +
1955
+ `${Object.keys(published.authzEnv).length ? ' — its schema is on the local SpiceDB, so key bundles take effect in the app' : ''}.`);
1956
+ const keyDecl = readLocalKeyDeclaration(appPkg);
1957
+ const hubProjectId = readHubAuth()?.projectId;
1958
+ if (keyDecl && hubProjectId) {
1959
+ try {
1960
+ const staffKey = await ensureLocalStaffKey({
1961
+ pat: readSeederPat(),
1962
+ api: zitadelApi,
1963
+ hubProjectId,
1964
+ awsConfig: LOCAL_AWS_CONFIG,
1965
+ });
1966
+ const key = await ensureLocalApiKey({ tenant: localTenant, app, declaration: keyDecl, staffKey });
1967
+ logInfo(key.kind === 'minted'
1968
+ ? `Minted local API key '${key.name}' for ${app} (${keyDecl.bundles.join(', ') || 'no bundles'}) — try-it runs as it.`
1969
+ : `Local API key '${key.name}' for ${app} is active — try-it runs as it.`);
1970
+ }
1971
+ catch (err) {
1972
+ logWarn(`No local API key for ${app}: ${err instanceof Error ? err.message : err} — mint one on the app's Access tab.`);
1973
+ }
1974
+ }
1975
+ else if (keyDecl) {
1976
+ logWarn(`No local API key for ${app}: the local Hub is not seeded yet (run: mesh start).`);
1977
+ }
1978
+ }
1979
+ for (const note of published.notes)
1980
+ logWarn(`docs: ${note}`);
1981
+ for (const problem of published.problems)
1982
+ logWarn(`docs source: ${problem}`);
1983
+ }
1984
+ }
1985
+ }
1986
+ catch (err) {
1987
+ logWarn(`Local docs publishing stopped: ${err instanceof Error ? err.message : err}`);
1988
+ }
1842
1989
  const composeExternals = [];
1843
1990
  const externalProbeFiles = [];
1844
1991
  for (const [name, decl] of Object.entries(selectedMocks)) {
@@ -1913,6 +2060,7 @@ export function registerDevCommand(program) {
1913
2060
  runner: 'docker',
1914
2061
  composeExternals,
1915
2062
  externalProbeFiles,
2063
+ ...(localDocs ? { localDocs } : {}),
1916
2064
  });
1917
2065
  console.log('');
1918
2066
  logSuccess(`Dev session started (docker): ${sessionName}`);
@@ -1936,6 +2084,7 @@ export function registerDevCommand(program) {
1936
2084
  runner: 'tmux',
1937
2085
  composeExternals,
1938
2086
  externalProbeFiles,
2087
+ ...(localDocs ? { localDocs } : {}),
1939
2088
  });
1940
2089
  return;
1941
2090
  }
@@ -122,6 +122,7 @@ async function buildDocs(configPathArg) {
122
122
  }
123
123
  logSuccess(`Docs build complete (${generatedSpecs.size} OpenAPI file${generatedSpecs.size === 1 ? "" : "s"})`);
124
124
  }
125
+ const DOCS_PREVIEW_PORT = "4410";
125
126
  const CLI_REFERENCE_PATH = "docs/portal/generated/cli-reference.md";
126
127
  async function renderCliReference() {
127
128
  const [{ createProgram }, { extractCliReference, renderCliReferenceMarkdown }] = await Promise.all([
@@ -240,6 +241,38 @@ export function registerDocsCommand(program) {
240
241
  const { runDocsServeStatic } = await import("../docs/start.js");
241
242
  await runDocsServeStatic(opts);
242
243
  })());
244
+ docs
245
+ .command("preview")
246
+ .description("Render one app's docs source locally — its docs.yaml, Markdown guides and OpenAPI reference, as the Hub's Docs tab shows them — with no Hub and no platform. Every problem the deploy would refuse is printed and shown on the page; try-it runs in the Hub only")
247
+ .argument("<dir>", "the directory that holds docs.yaml, or the docs.yaml file itself")
248
+ .option("-p, --port <port>", "port to serve on, on localhost (0 picks a free one; a port in use fails)", DOCS_PREVIEW_PORT)
249
+ .option("-o, --out <file>", "write the assembled docs-source bundle (JSON) to this file and exit — the file HUB_LOCAL_DOCS_SOURCES points a local Hub at")
250
+ .option("-s, --spec <file>", "the OpenAPI spec to document, overriding docs.yaml's spec — pass the one the deploy reads (the ApiSurface's docs.spec, or its doc package's openapi.json); default: docs.yaml's spec, else openapi.json beside it")
251
+ .option("--check", "print the problems and exit 1 when there are any, without serving (CI / pre-deploy)", false)
252
+ .action((dir, opts) => guarded(async () => {
253
+ const { runDocsPreview } = await import("../docs/preview.js");
254
+ const ok = await runDocsPreview({ target: dir, port: opts.port, out: opts.out, spec: opts.spec, check: opts.check });
255
+ if (!ok)
256
+ process.exitCode = 1;
257
+ })());
258
+ docs
259
+ .command("convert")
260
+ .description("Write a starter docs.yaml — a native docs source the Hub renders itself — from the inputs of an ApiSurface's site-shaped docs block (spec, pages, landingPage), then assemble it as the deploy does and print every problem left and the ApiSurface edit to make. Exits 1 while problems remain; replaces nothing without --force")
261
+ .requiredOption("-s, --spec <file>", "the OpenAPI spec the surface documents (the block's docs.spec)")
262
+ .option("--pages <dir>", "the block's docs.pages directory of Markdown guides")
263
+ .option("--landing <page>", "the block's docs.landingPage — a file, or a page id under --pages (default: the index page under --pages, when there is one)")
264
+ .option("--title <title>", "the source's title (default: the spec's info.title)")
265
+ .option("-o, --out <dir>", "where docs.yaml is written (default: the pages directory's parent, else the spec's directory)")
266
+ .option("--fix-pages", "rewrite pages for the native rules: a leading # heading becomes the front-matter title, and a landing file outside --pages is copied to <pages>/index.md; .mdx pages are reported, never converted", false)
267
+ .option("--force", "replace an existing docs.yaml, and a page the landing copy would land on", false)
268
+ .option("--root <dir>", "the Pulumi program's directory, which the printed ApiSurface edit's paths are relative to (default: the working directory)")
269
+ .option("--prefix <prefix>", "the block's pages prefix ({ dir, prefix }): the old site served pages under /<prefix>/; the conversion lists the page ids that lose it")
270
+ .action((opts) => guarded(async () => {
271
+ const { runDocsConvert } = await import("../docs/convert.js");
272
+ const ok = await runDocsConvert(opts);
273
+ if (!ok)
274
+ process.exitCode = 1;
275
+ })());
243
276
  docs
244
277
  .command("list")
245
278
  .description("List the published docs versions (each is an @mesh-tech/* baseline)")
@@ -0,0 +1,5 @@
1
+ import { type HubActAsExports } from "@mesh-tech/api-registry";
2
+ export declare const LOCAL_OPS_HUB_SECRET_ID = "mesh/local/dev/zitadel/ops-hub";
3
+ export declare function hubActAsPublication(secretString: string): HubActAsExports;
4
+ export declare function publishLocalHubActAsKey(awsConfig: object): Promise<HubActAsExports | undefined>;
5
+ export declare function actAsEnv(publication: HubActAsExports, tenant: string, env: string, surface: string): Record<string, string>;
@@ -0,0 +1,41 @@
1
+ import { createPublicKey } from "node:crypto";
2
+ import { actAsAudience, hubActAsPath, parseHubActAsExports } from "@mesh-tech/api-registry";
3
+ import { LOCAL_ENV, LOCAL_TENANT } from "./seed.js";
4
+ export const LOCAL_OPS_HUB_SECRET_ID = "mesh/local/dev/zitadel/ops-hub";
5
+ export function hubActAsPublication(secretString) {
6
+ const parsed = JSON.parse(secretString);
7
+ if (typeof parsed.keyId !== "string" || typeof parsed.key !== "string" || typeof parsed.userId !== "string") {
8
+ throw new Error("the Hub's machine key has no keyId/key/userId");
9
+ }
10
+ const jwk = createPublicKey(parsed.key).export({ format: "jwk" });
11
+ if (jwk.kty !== "RSA" || !jwk.n || !jwk.e)
12
+ throw new Error("the Hub's machine key is not an RSA key");
13
+ return parseHubActAsExports({
14
+ issuer: parsed.userId,
15
+ keys: [{ kid: parsed.keyId, kty: "RSA", n: jwk.n, e: jwk.e, alg: "RS256", use: "sig" }],
16
+ });
17
+ }
18
+ export async function publishLocalHubActAsKey(awsConfig) {
19
+ const { SecretsManagerClient, GetSecretValueCommand } = await import("@aws-sdk/client-secrets-manager");
20
+ const { SSMClient, PutParameterCommand } = await import("@aws-sdk/client-ssm");
21
+ const secret = await new SecretsManagerClient(awsConfig)
22
+ .send(new GetSecretValueCommand({ SecretId: LOCAL_OPS_HUB_SECRET_ID }))
23
+ .catch(() => null);
24
+ if (!secret?.SecretString)
25
+ return undefined;
26
+ const publication = hubActAsPublication(secret.SecretString);
27
+ await new SSMClient(awsConfig).send(new PutParameterCommand({
28
+ Name: hubActAsPath(LOCAL_TENANT, LOCAL_ENV),
29
+ Type: "String",
30
+ Overwrite: true,
31
+ Value: JSON.stringify(publication),
32
+ Description: "Hub act-as key — the public half of the local Hub's machine key",
33
+ }));
34
+ return publication;
35
+ }
36
+ export function actAsEnv(publication, tenant, env, surface) {
37
+ return {
38
+ INTEGRATION_ACT_AS_KEYS: JSON.stringify(publication),
39
+ INTEGRATION_ACT_AS_AUDIENCE: actAsAudience(tenant, env, surface),
40
+ };
41
+ }
@@ -14,6 +14,7 @@ export declare function registerLocalApp(args: AppTenantAuthArgs & {
14
14
  ports?: Record<string, number>;
15
15
  links?: string[];
16
16
  version?: string;
17
+ displayName?: string;
17
18
  kinds?: Record<string, string>;
18
19
  }): Promise<string>;
19
20
  export declare function reconcileRegistryFromZitadel(): Promise<{
@@ -138,6 +138,7 @@ export async function registerLocalApp(args) {
138
138
  services: args.services,
139
139
  ports: args.ports ?? {},
140
140
  ...(args.version ? { version: args.version } : {}),
141
+ ...(args.displayName ? { displayName: args.displayName } : {}),
141
142
  deployedAt: new Date().toISOString(),
142
143
  }, "Local run metadata (mesh dev --local)");
143
144
  logSuccess(`Registered app in the local registry → ${base}`);
@@ -11,6 +11,7 @@ import { LOCAL_AWS_CONFIG, localAwsEnv, seedLocalPlatform, verifyFabric } from "
11
11
  import { LOGIN_CONTEXT, readHubAuth, seedMockIdps, seedZitadel, TEST_USERS } from "./seed-zitadel.js";
12
12
  import { idpEndpoints, mockIdpComposeEnv, MOCK_OIDC_PORT, MOCK_SAML_PORT } from "./mock-idps.js";
13
13
  import { publishHubAuthzCatalog } from "./seed-hub-catalog.js";
14
+ import { removeAllAliasContainers } from "./publish-docs.js";
14
15
  import { describePortConflicts, findPortConflicts, hostPortsOf } from "./helpers.js";
15
16
  const WAIT_TIMEOUT_MS = 180_000;
16
17
  const WAIT_POLL_MS = 3_000;
@@ -313,6 +314,9 @@ export function registerLocalCommands(program) {
313
314
  if (foreign && !opts.force) {
314
315
  throw new MeshCliError(`The running local platform was started from a different checkout (${foreign}) — another project may be using it.`, { remediation: { command: "mesh stop --force # stop it anyway" } });
315
316
  }
317
+ const aliases = removeAllAliasContainers();
318
+ if (aliases.length > 0)
319
+ logInfo(`Removed ${aliases.length} local docs alias container(s).`);
316
320
  const args = ["down"];
317
321
  if (opts.destroy)
318
322
  args.push("-v");
@@ -0,0 +1,44 @@
1
+ export declare const LOCAL_CLI_SECRET_ID = "mesh/local/dev/zitadel/local-cli";
2
+ export interface LocalKeyDeclaration {
3
+ name: string;
4
+ bundles: string[];
5
+ rateLimit: number;
6
+ }
7
+ export declare function readLocalKeyDeclaration(pkg: unknown): LocalKeyDeclaration | undefined;
8
+ export declare function jwtBearerAssertion(key: {
9
+ keyId: string;
10
+ key: string;
11
+ userId: string;
12
+ }, audience: string, nowSeconds?: number): string;
13
+ type ZitadelApi = (pat: string, method: string, apiPath: string, body?: unknown) => Promise<any>;
14
+ export declare function ensureLocalStaffKey(input: {
15
+ pat: string;
16
+ api: ZitadelApi;
17
+ hubProjectId: string;
18
+ awsConfig: object;
19
+ }): Promise<{
20
+ keyId: string;
21
+ key: string;
22
+ userId: string;
23
+ }>;
24
+ export type LocalKeyOutcome = {
25
+ kind: "kept";
26
+ name: string;
27
+ id: string;
28
+ } | {
29
+ kind: "minted";
30
+ name: string;
31
+ id: string;
32
+ clientId: string;
33
+ };
34
+ export declare function ensureLocalApiKey(input: {
35
+ tenant: string;
36
+ app: string;
37
+ declaration: LocalKeyDeclaration;
38
+ staffKey: {
39
+ keyId: string;
40
+ key: string;
41
+ userId: string;
42
+ };
43
+ }): Promise<LocalKeyOutcome>;
44
+ export {};
@@ -0,0 +1,115 @@
1
+ import { createSign, randomUUID } from "node:crypto";
2
+ import { LOCAL_ENV } from "./seed.js";
3
+ export const LOCAL_CLI_SECRET_ID = "mesh/local/dev/zitadel/local-cli";
4
+ const ISSUER = "http://localhost:8080";
5
+ const HUB_API = "http://localhost:4568";
6
+ export function readLocalKeyDeclaration(pkg) {
7
+ const authz = pkg?.mesh?.authz;
8
+ if (typeof authz?.metadata !== "string")
9
+ return undefined;
10
+ const key = (authz.localKey ?? {});
11
+ return {
12
+ name: typeof key.name === "string" && key.name ? key.name : "local-try-it",
13
+ bundles: Array.isArray(key.bundles) ? key.bundles.filter((b) => typeof b === "string") : [],
14
+ rateLimit: typeof key.rateLimit === "number" && key.rateLimit > 0 ? key.rateLimit : 600,
15
+ };
16
+ }
17
+ export function jwtBearerAssertion(key, audience, nowSeconds = Math.floor(Date.now() / 1000)) {
18
+ const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
19
+ const header = b64({ alg: "RS256", kid: key.keyId });
20
+ const payload = b64({ iss: key.userId, sub: key.userId, aud: audience, iat: nowSeconds, exp: nowSeconds + 300 });
21
+ const signature = createSign("RSA-SHA256").update(`${header}.${payload}`).sign(key.key).toString("base64url");
22
+ return `${header}.${payload}.${signature}`;
23
+ }
24
+ export async function ensureLocalStaffKey(input) {
25
+ const { SecretsManagerClient, GetSecretValueCommand, CreateSecretCommand, PutSecretValueCommand } = await import("@aws-sdk/client-secrets-manager");
26
+ const sm = new SecretsManagerClient(input.awsConfig);
27
+ const ensureGrant = async (userId) => {
28
+ try {
29
+ await input.api(input.pat, "POST", `/management/v1/users/${userId}/grants`, {
30
+ projectId: input.hubProjectId,
31
+ roleKeys: ["ops"],
32
+ });
33
+ }
34
+ catch (err) {
35
+ if (err.status !== 409)
36
+ throw err;
37
+ }
38
+ };
39
+ const existing = await sm.send(new GetSecretValueCommand({ SecretId: LOCAL_CLI_SECRET_ID })).catch(() => null);
40
+ if (existing?.SecretString) {
41
+ const kept = JSON.parse(existing.SecretString);
42
+ await ensureGrant(kept.userId);
43
+ return kept;
44
+ }
45
+ const userName = "mesh-local-cli";
46
+ const found = await input.api(input.pat, "POST", "/management/v1/users/_search", {
47
+ queries: [{ userNameQuery: { userName, method: "TEXT_QUERY_METHOD_EQUALS" } }],
48
+ });
49
+ let userId = found?.result?.[0]?.id;
50
+ if (!userId) {
51
+ const created = await input.api(input.pat, "POST", "/management/v1/users/machine", {
52
+ userName,
53
+ name: "mesh CLI (local)",
54
+ description: "Local staff caller the mesh CLI uses against the local Hub (mesh dev --local)",
55
+ accessTokenType: "ACCESS_TOKEN_TYPE_JWT",
56
+ });
57
+ userId = created?.userId;
58
+ }
59
+ if (!userId)
60
+ throw new Error("the local Zitadel did not create the CLI's staff caller");
61
+ await ensureGrant(userId);
62
+ const created = await input.api(input.pat, "POST", `/management/v1/users/${userId}/keys`, { type: "KEY_TYPE_JSON" });
63
+ if (!created?.keyDetails)
64
+ throw new Error("the local Zitadel returned no key for the CLI's staff caller");
65
+ const raw = JSON.parse(Buffer.from(created.keyDetails, "base64").toString("utf8"));
66
+ const key = { keyId: raw.keyId, key: raw.key, userId: raw.userId };
67
+ await sm
68
+ .send(new CreateSecretCommand({ Name: LOCAL_CLI_SECRET_ID, SecretString: JSON.stringify(key) }))
69
+ .catch(() => sm.send(new PutSecretValueCommand({ SecretId: LOCAL_CLI_SECRET_ID, SecretString: JSON.stringify(key) })));
70
+ return key;
71
+ }
72
+ async function staffToken(key) {
73
+ const res = await fetch(`${ISSUER}/oauth/v2/token`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
76
+ body: new URLSearchParams({
77
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
78
+ assertion: jwtBearerAssertion(key, ISSUER),
79
+ scope: "openid profile",
80
+ }),
81
+ });
82
+ const body = (await res.json().catch(() => ({})));
83
+ if (!res.ok || !body.access_token)
84
+ throw new Error(`the local Zitadel refused a token: ${body.error_description ?? res.status}`);
85
+ return body.access_token;
86
+ }
87
+ export async function ensureLocalApiKey(input) {
88
+ const token = await staffToken(input.staffKey);
89
+ const base = `${HUB_API}/tenants/${encodeURIComponent(input.tenant)}/environments/${LOCAL_ENV}/api-keys`;
90
+ const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
91
+ const listed = await fetch(`${base}?app=${encodeURIComponent(input.app)}`, { headers });
92
+ if (!listed.ok)
93
+ throw new Error(`the local Hub refused the key list (${listed.status}: ${await listed.text()})`);
94
+ const keys = (await listed.json());
95
+ const kept = keys.find((k) => k.name === input.declaration.name && k.status === "active");
96
+ if (kept)
97
+ return { kind: "kept", name: kept.name, id: kept.id };
98
+ const res = await fetch(base, {
99
+ method: "POST",
100
+ headers,
101
+ body: JSON.stringify({
102
+ app: input.app,
103
+ name: input.declaration.name,
104
+ rateLimit: input.declaration.rateLimit,
105
+ idempotencyKey: `mesh-local-${randomUUID()}`,
106
+ ...(input.declaration.bundles.length
107
+ ? { assignments: input.declaration.bundles.map((bundle) => ({ bundle, reason: "mesh dev --local try-it key" })) }
108
+ : {}),
109
+ }),
110
+ });
111
+ const body = (await res.json().catch(() => ({})));
112
+ if (!res.ok || !body.id)
113
+ throw new Error(`the local Hub did not mint the key (${res.status} ${body.code ?? ""}: ${body.error ?? ""})`);
114
+ return { kind: "minted", name: input.declaration.name, id: body.id, clientId: body.credentials?.clientId ?? "" };
115
+ }