@topogram/cli 0.3.43 → 0.3.45

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -595,9 +595,9 @@ function printGeneratorHelp() {
595
595
  console.log("Inspects generator manifests and checks generator pack conformance.");
596
596
  console.log("");
597
597
  console.log("Notes:");
598
- console.log(" - list shows bundled generators plus installed package-backed generators declared in package.json.");
599
- console.log(" - show accepts an installed package name or a bundled fallback generator id.");
600
- console.log(" - check validates a local generator package path or an already installed package.");
598
+ console.log(" - list shows bundled generators plus installed package-backed generators declared in package.json; it reads manifests only.");
599
+ console.log(" - show accepts an installed package name or a bundled fallback generator id; it does not load adapter code.");
600
+ console.log(" - check validates a local generator package path or an already installed package by loading the adapter and running smoke generation.");
601
601
  console.log(" - Topogram does not install generator packages during show or check.");
602
602
  console.log(` - package-backed project generators are governed by ${GENERATOR_POLICY_FILE}; bundled topogram/* generators are allowed.`);
603
603
  console.log("");
@@ -1250,6 +1250,7 @@ function printGeneratorCheck(payload) {
1250
1250
  console.log(`Projection platforms: ${payload.manifest.projectionPlatforms.join(", ")}`);
1251
1251
  console.log(`Source mode: ${payload.manifest.source}`);
1252
1252
  }
1253
+ console.log("Executes package code: yes (loads adapter and runs smoke generate)");
1253
1254
  console.log("");
1254
1255
  console.log("Checks:");
1255
1256
  for (const check of payload.checks || []) {
@@ -1285,6 +1286,8 @@ function generatorManifestSummary(manifest, metadata = {}) {
1285
1286
  stack: manifest.stack || {},
1286
1287
  capabilities: manifest.capabilities || {},
1287
1288
  source: manifest.source,
1289
+ loadsAdapter: false,
1290
+ executesPackageCode: false,
1288
1291
  ...(manifest.profile ? { profile: manifest.profile } : {}),
1289
1292
  ...(manifest.package ? { package: manifest.package } : {}),
1290
1293
  ...(installCommand ? { installCommand } : {}),
@@ -1496,6 +1499,8 @@ function printGeneratorList(payload) {
1496
1499
  const stack = Object.values(generator.stack || {}).join(" + ") || "not declared";
1497
1500
  console.log(`- ${id}${generator.version ? `@${generator.version}` : ""} (${generator.surface || "unknown"}, ${status})`);
1498
1501
  console.log(` Source: ${generator.source}`);
1502
+ console.log(" Adapter loaded: no");
1503
+ console.log(" Executes package code: no");
1499
1504
  if (generator.source === "package") {
1500
1505
  console.log(` Installed: ${generator.installed ? "yes" : "no"}`);
1501
1506
  }
@@ -1529,6 +1534,8 @@ function printGeneratorShow(payload) {
1529
1534
  console.log(`Generator: ${generator.id}@${generator.version}`);
1530
1535
  console.log(`Surface: ${generator.surface}`);
1531
1536
  console.log(`Source: ${generator.source}${generator.planned ? " (planned)" : ""}`);
1537
+ console.log("Adapter loaded: no");
1538
+ console.log("Executes package code: no");
1532
1539
  if (generator.source === "package") {
1533
1540
  console.log(`Installed: ${generator.installed ? "yes" : "no"}`);
1534
1541
  }
@@ -1631,7 +1638,7 @@ function dependencySpecForPackage(projectPackage, packageName) {
1631
1638
  * @param {string} packageName
1632
1639
  * @returns {{ version: string|null, resolved: string|null, integrity: string|null, entryPath: string|null }}
1633
1640
  */
1634
- function lockfileInfoForPackage(lockfile, packageName) {
1641
+ function npmLockfileInfoForPackage(lockfile, packageName) {
1635
1642
  const packageEntryPath = `node_modules/${packageName}`;
1636
1643
  const packageEntry = lockfile?.packages?.[packageEntryPath];
1637
1644
  if (packageEntry && typeof packageEntry === "object") {
@@ -1659,16 +1666,62 @@ function lockfileInfoForPackage(lockfile, packageName) {
1659
1666
  };
1660
1667
  }
1661
1668
 
1669
+ /**
1670
+ * @param {string} projectRoot
1671
+ * @returns {{ kind: "npm"|"pnpm"|"yarn"|"bun"|null, path: string|null, data: Record<string, any>|null, note: string|null }}
1672
+ */
1673
+ function lockfileMetadataForProject(projectRoot) {
1674
+ const npmLockfilePath = path.join(projectRoot, "package-lock.json");
1675
+ if (fs.existsSync(npmLockfilePath)) {
1676
+ return {
1677
+ kind: "npm",
1678
+ path: npmLockfilePath,
1679
+ data: readJsonIfPresent(npmLockfilePath),
1680
+ note: null
1681
+ };
1682
+ }
1683
+ const candidates = [
1684
+ { kind: "pnpm", file: "pnpm-lock.yaml" },
1685
+ { kind: "yarn", file: "yarn.lock" },
1686
+ { kind: "bun", file: "bun.lock" },
1687
+ { kind: "bun", file: "bun.lockb" }
1688
+ ];
1689
+ for (const candidate of candidates) {
1690
+ const lockfilePath = path.join(projectRoot, candidate.file);
1691
+ if (fs.existsSync(lockfilePath)) {
1692
+ return {
1693
+ kind: /** @type {"pnpm"|"yarn"|"bun"} */ (candidate.kind),
1694
+ path: lockfilePath,
1695
+ data: null,
1696
+ note: `${candidate.file} found; package versions are not inspected by this command.`
1697
+ };
1698
+ }
1699
+ }
1700
+ return {
1701
+ kind: null,
1702
+ path: null,
1703
+ data: null,
1704
+ note: null
1705
+ };
1706
+ }
1707
+
1662
1708
  /**
1663
1709
  * @param {string} projectRoot
1664
1710
  * @param {string} packageName
1665
- * @returns {{ dependencyField: string|null, dependencySpec: string|null, installedVersion: string|null, installedPackageJsonPath: string|null, lockfileVersion: string|null, lockfileResolved: string|null, lockfileIntegrity: string|null, lockfileEntryPath: string|null }}
1711
+ * @returns {{ dependencyField: string|null, dependencySpec: string|null, installedVersion: string|null, installedPackageJsonPath: string|null, lockfileKind: "npm"|"pnpm"|"yarn"|"bun"|null, lockfilePath: string|null, lockfileVersion: string|null, lockfileResolved: string|null, lockfileIntegrity: string|null, lockfileEntryPath: string|null, lockfileNote: string|null }}
1666
1712
  */
1667
1713
  function packageInfoForGenerator(projectRoot, packageName) {
1668
1714
  const projectPackage = readJsonIfPresent(path.join(projectRoot, "package.json"));
1669
1715
  const dependency = dependencySpecForPackage(projectPackage, packageName);
1670
- const lockfile = readJsonIfPresent(path.join(projectRoot, "package-lock.json"));
1671
- const lockfileInfo = lockfileInfoForPackage(lockfile, packageName);
1716
+ const lockfile = lockfileMetadataForProject(projectRoot);
1717
+ const lockfileInfo = lockfile.kind === "npm"
1718
+ ? npmLockfileInfoForPackage(lockfile.data, packageName)
1719
+ : {
1720
+ version: null,
1721
+ resolved: null,
1722
+ integrity: null,
1723
+ entryPath: null
1724
+ };
1672
1725
  const installedPackageJsonPath = path.join(projectRoot, "node_modules", ...packageName.split("/"), "package.json");
1673
1726
  const installedPackage = readJsonIfPresent(installedPackageJsonPath);
1674
1727
  return {
@@ -1676,13 +1729,31 @@ function packageInfoForGenerator(projectRoot, packageName) {
1676
1729
  dependencySpec: dependency.spec,
1677
1730
  installedVersion: typeof installedPackage?.version === "string" ? installedPackage.version : null,
1678
1731
  installedPackageJsonPath: installedPackage ? installedPackageJsonPath : null,
1732
+ lockfileKind: lockfile.kind,
1733
+ lockfilePath: lockfile.path,
1679
1734
  lockfileVersion: lockfileInfo.version,
1680
1735
  lockfileResolved: lockfileInfo.resolved,
1681
1736
  lockfileIntegrity: lockfileInfo.integrity,
1682
- lockfileEntryPath: lockfileInfo.entryPath
1737
+ lockfileEntryPath: lockfileInfo.entryPath,
1738
+ lockfileNote: lockfile.note
1683
1739
  };
1684
1740
  }
1685
1741
 
1742
+ /**
1743
+ * @param {ReturnType<typeof packageInfoForGenerator>} packageInfo
1744
+ * @returns {string}
1745
+ */
1746
+ function formatGeneratorPackageLockfile(packageInfo) {
1747
+ if (!packageInfo.lockfileKind || !packageInfo.lockfilePath) {
1748
+ return "(not found)";
1749
+ }
1750
+ const label = path.basename(packageInfo.lockfilePath);
1751
+ if (packageInfo.lockfileVersion) {
1752
+ return `${packageInfo.lockfileKind} ${packageInfo.lockfileVersion}`;
1753
+ }
1754
+ return `${label} (version not inspected)`;
1755
+ }
1756
+
1686
1757
  /**
1687
1758
  * @param {string} projectRoot
1688
1759
  * @param {import("./generator-policy.js").GeneratorPolicy} policy
@@ -1724,11 +1795,68 @@ function annotateGeneratorPolicyDiagnostics(diagnostics, bindings) {
1724
1795
  packageVersion: binding.packageInfo.installedVersion || binding.packageInfo.lockfileVersion || null,
1725
1796
  packageDependencyField: binding.packageInfo.dependencyField,
1726
1797
  packageDependencySpec: binding.packageInfo.dependencySpec,
1798
+ packageLockfileKind: binding.packageInfo.lockfileKind,
1799
+ packageLockfilePath: binding.packageInfo.lockfilePath,
1727
1800
  packageLockVersion: binding.packageInfo.lockfileVersion
1728
1801
  };
1729
1802
  });
1730
1803
  }
1731
1804
 
1805
+ /**
1806
+ * @param {Array<ReturnType<typeof generatorPolicyBindingStatus>>} bindings
1807
+ * @returns {any[]}
1808
+ */
1809
+ function generatorPolicyPackageMetadataDiagnostics(bindings) {
1810
+ const diagnostics = [];
1811
+ for (const binding of bindings) {
1812
+ if (!binding.packageInfo.dependencySpec) {
1813
+ diagnostics.push({
1814
+ code: "generator_package_dependency_missing",
1815
+ severity: "warning",
1816
+ message: `Component '${binding.componentId}' generator package '${binding.packageName}' is not declared in package.json dependencies.`,
1817
+ path: binding.packageInfo.installedPackageJsonPath,
1818
+ suggestedFix: `Declare '${binding.packageName}' in package.json devDependencies so generator adoption is visible in package review.`,
1819
+ step: "generator-policy",
1820
+ componentId: binding.componentId,
1821
+ generatorId: binding.generatorId,
1822
+ packageName: binding.packageName,
1823
+ version: binding.version,
1824
+ packageVersion: binding.packageInfo.installedVersion || binding.packageInfo.lockfileVersion || null,
1825
+ packageDependencyField: binding.packageInfo.dependencyField,
1826
+ packageDependencySpec: binding.packageInfo.dependencySpec,
1827
+ packageLockfileKind: binding.packageInfo.lockfileKind,
1828
+ packageLockfilePath: binding.packageInfo.lockfilePath,
1829
+ packageLockVersion: binding.packageInfo.lockfileVersion
1830
+ });
1831
+ }
1832
+ if (
1833
+ binding.packageInfo.installedVersion &&
1834
+ binding.packageInfo.lockfileVersion &&
1835
+ binding.packageInfo.installedVersion !== binding.packageInfo.lockfileVersion
1836
+ ) {
1837
+ diagnostics.push({
1838
+ code: "generator_package_version_drift",
1839
+ severity: "warning",
1840
+ message: `Component '${binding.componentId}' generator package '${binding.packageName}' is installed at '${binding.packageInfo.installedVersion}', but package-lock records '${binding.packageInfo.lockfileVersion}'.`,
1841
+ path: binding.packageInfo.lockfilePath,
1842
+ suggestedFix: "Run the package manager install command and review the resulting lockfile before pinning generator policy.",
1843
+ step: "generator-policy",
1844
+ componentId: binding.componentId,
1845
+ generatorId: binding.generatorId,
1846
+ packageName: binding.packageName,
1847
+ version: binding.version,
1848
+ packageVersion: binding.packageInfo.installedVersion,
1849
+ packageDependencyField: binding.packageInfo.dependencyField,
1850
+ packageDependencySpec: binding.packageInfo.dependencySpec,
1851
+ packageLockfileKind: binding.packageInfo.lockfileKind,
1852
+ packageLockfilePath: binding.packageInfo.lockfilePath,
1853
+ packageLockVersion: binding.packageInfo.lockfileVersion
1854
+ });
1855
+ }
1856
+ }
1857
+ return diagnostics;
1858
+ }
1859
+
1732
1860
  /**
1733
1861
  * @param {string} projectPath
1734
1862
  * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, defaulted: boolean, bindings: Array<ReturnType<typeof generatorPolicyBindingStatus>>, diagnostics: any[], errors: string[] }}
@@ -1771,6 +1899,7 @@ function buildGeneratorPolicyCheckPayload(projectPath) {
1771
1899
  });
1772
1900
  }
1773
1901
  diagnostics.push(...generatorPolicyDiagnosticsForBindings(policyInfo, rawBindings, "generator-policy"));
1902
+ diagnostics.push(...generatorPolicyPackageMetadataDiagnostics(bindings));
1774
1903
  const annotatedDiagnostics = annotateGeneratorPolicyDiagnostics(diagnostics, bindings);
1775
1904
  const errors = annotatedDiagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
1776
1905
  return {
@@ -1868,7 +1997,9 @@ function printGeneratorPolicyCheckPayload(payload) {
1868
1997
  console.log(` dependency: ${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}`);
1869
1998
  }
1870
1999
  if (binding.packageInfo.lockfileVersion) {
1871
- console.log(` lockfile: ${binding.packageInfo.lockfileVersion}`);
2000
+ console.log(` lockfile: ${formatGeneratorPackageLockfile(binding.packageInfo)}`);
2001
+ } else if (binding.packageInfo.lockfileKind) {
2002
+ console.log(` lockfile: ${formatGeneratorPackageLockfile(binding.packageInfo)}`);
1872
2003
  }
1873
2004
  }
1874
2005
  for (const diagnostic of payload.diagnostics) {
@@ -1906,7 +2037,7 @@ function printGeneratorPolicyStatusPayload(payload) {
1906
2037
  console.log(` allowed: ${binding.allowed ? "yes" : "no"}`);
1907
2038
  console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
1908
2039
  console.log(` dependency: ${binding.packageInfo.dependencyField && binding.packageInfo.dependencySpec ? `${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}` : "(not declared)"}`);
1909
- console.log(` lockfile: ${binding.packageInfo.lockfileVersion || "(not locked)"}`);
2040
+ console.log(` lockfile: ${formatGeneratorPackageLockfile(binding.packageInfo)}`);
1910
2041
  console.log(` policy pin: ${binding.pin.version ? `${binding.pin.key}@${binding.pin.version}` : "(none)"}`);
1911
2042
  }
1912
2043
  for (const diagnostic of payload.diagnostics) {
@@ -1918,6 +2049,9 @@ function printGeneratorPolicyStatusPayload(payload) {
1918
2049
  if (diagnostic.packageDependencySpec) {
1919
2050
  console.log(` dependency: ${diagnostic.packageDependencyField} ${diagnostic.packageDependencySpec}`);
1920
2051
  }
2052
+ if (diagnostic.packageLockfilePath) {
2053
+ console.log(` lockfile: ${path.basename(diagnostic.packageLockfilePath)}${diagnostic.packageLockVersion ? ` ${diagnostic.packageLockVersion}` : ""}`);
2054
+ }
1921
2055
  if (diagnostic.suggestedFix) {
1922
2056
  console.log(` fix: ${diagnostic.suggestedFix}`);
1923
2057
  }
@@ -2057,10 +2191,6 @@ function buildGeneratorPolicyPinPayload(projectPath, spec) {
2057
2191
  if (!allowedPackages.includes(pin.packageName)) {
2058
2192
  allowedPackages.push(pin.packageName);
2059
2193
  }
2060
- const scope = packageScopeFromName(pin.packageName);
2061
- if (scope && !allowedPackageScopes.includes(scope)) {
2062
- allowedPackageScopes.push(scope);
2063
- }
2064
2194
  pinnedVersions[pin.packageName] = pin.version;
2065
2195
  }
2066
2196
  const nextPolicy = {
@@ -10,6 +10,10 @@ import {
10
10
  resolveGeneratorManifestForBinding,
11
11
  validateGeneratorManifest
12
12
  } from "./registry.js";
13
+ import {
14
+ generatorPolicyDiagnosticsForBindings,
15
+ loadGeneratorPolicy
16
+ } from "../generator-policy.js";
13
17
  import { generateDbContractGraph } from "./surfaces/databases/contract.js";
14
18
  import { generateDbLifecyclePlan } from "./surfaces/databases/lifecycle-shared.js";
15
19
  import {
@@ -252,6 +256,26 @@ function loadPackageGeneratorAdapter(manifest, component, options = {}) {
252
256
  throw new Error(`Component '${component?.id || "unknown"}' generator '${manifest.id}@${manifest.version}' is package-backed but does not declare a package.`);
253
257
  }
254
258
  const rootDir = options.configDir || options.rootDir || process.cwd();
259
+ const diagnostics = generatorPolicyDiagnosticsForBindings(
260
+ loadGeneratorPolicy(rootDir),
261
+ [{
262
+ componentId: String(component?.id || "unknown"),
263
+ componentType: String(component?.type || manifest.surface || "unknown"),
264
+ projection: String(component?.projection?.id || component?.projection || "unknown"),
265
+ generatorId: String(component?.generator?.id || manifest.id),
266
+ version: String(component?.generator?.version || manifest.version),
267
+ packageName
268
+ }],
269
+ "generator-adapter"
270
+ );
271
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
272
+ if (errors.length > 0) {
273
+ throw new Error(errors.map((diagnostic) =>
274
+ diagnostic.suggestedFix
275
+ ? `${diagnostic.message} Suggested fix: ${diagnostic.suggestedFix}`
276
+ : diagnostic.message
277
+ ).join("\n"));
278
+ }
255
279
  let moduleValue;
256
280
  try {
257
281
  moduleValue = requireFromProject(rootDir)(packageName);
@@ -25,6 +25,7 @@ import {
25
25
  * @property {Array<{ name: string, ok: boolean, message: string }>} checks
26
26
  * @property {string[]} errors
27
27
  * @property {{ files: number, artifacts: number, diagnostics: number }|null} smoke
28
+ * @property {boolean} executesPackageCode
28
29
  */
29
30
 
30
31
  /**
@@ -238,7 +239,8 @@ export function checkGeneratorPack(sourceSpec, options = {}) {
238
239
  manifest: null,
239
240
  checks: [],
240
241
  errors: [],
241
- smoke: null
242
+ smoke: null,
243
+ executesPackageCode: true
242
244
  };
243
245
 
244
246
  /** @type {any|null} */
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { getProjection } from "../shared.js";
4
+ import { generateServerContract } from "./server-contract.js";
4
5
 
5
6
  function renderPackageJson(profile) {
6
7
  const dependencies = profile === "express"
@@ -41,10 +42,10 @@ function routePath(path) {
41
42
  return String(path || "/").replace(/:([A-Za-z0-9_]+)/g, ":$1");
42
43
  }
43
44
 
44
- function renderHonoIndex(projection) {
45
- const routes = (projection.http || []).map((route) => {
45
+ function renderHonoIndex(projection, contract) {
46
+ const routes = (contract.routes || []).map((route) => {
46
47
  const method = String(route.method || "GET").toLowerCase();
47
- return `app.${method}("${routePath(route.path)}", (c) => c.json({ ok: true, capability: "${route.capabilityId}", input: { params: c.req.param(), query: c.req.query() } }, ${route.success || 200} as any));`;
48
+ return `app.${method}("${routePath(route.path)}", (c) => c.json({ ok: true, capability: "${route.capabilityId}", input: { params: c.req.param(), query: c.req.query() } }, ${route.successStatus || 200} as any));`;
48
49
  }).join("\n");
49
50
  return `import { serve } from "@hono/node-server";
50
51
  import { Hono } from "hono";
@@ -65,10 +66,10 @@ function expressPath(path) {
65
66
  return routePath(path);
66
67
  }
67
68
 
68
- function renderExpressIndex(projection) {
69
- const routes = (projection.http || []).map((route) => {
69
+ function renderExpressIndex(projection, contract) {
70
+ const routes = (contract.routes || []).map((route) => {
70
71
  const method = String(route.method || "GET").toLowerCase();
71
- return `app.${method}("${expressPath(route.path)}", (req, res) => res.status(${route.success || 200}).json({ ok: true, capability: "${route.capabilityId}", input: { params: req.params, query: req.query } }));`;
72
+ return `app.${method}("${expressPath(route.path)}", (req, res) => res.status(${route.successStatus || 200}).json({ ok: true, capability: "${route.capabilityId}", input: { params: req.params, query: req.query } }));`;
72
73
  }).join("\n");
73
74
  return `import express from "express";
74
75
 
@@ -88,10 +89,11 @@ app.listen(port, () => {
88
89
 
89
90
  export function generateStatelessServer(graph, options = {}) {
90
91
  const projection = getProjection(graph, options.projectionId);
92
+ const contract = generateServerContract(graph, { ...options, projectionId: projection.id });
91
93
  const profile = options.profile === "express" ? "express" : "hono";
92
94
  return {
93
95
  "package.json": renderPackageJson(profile),
94
96
  "tsconfig.json": renderTsconfig(),
95
- "src/index.ts": profile === "express" ? renderExpressIndex(projection) : renderHonoIndex(projection)
97
+ "src/index.ts": profile === "express" ? renderExpressIndex(projection, contract) : renderHonoIndex(projection, contract)
96
98
  };
97
99
  }