@topogram/cli 0.3.41 → 0.3.43
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 +1 -1
- package/src/cli.js +661 -3
- package/src/generator-policy.d.ts +12 -0
- package/src/generator-policy.js +344 -0
- package/src/new-project.js +10 -0
- package/src/project-config.js +5 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -44,6 +44,17 @@ import {
|
|
|
44
44
|
loadPackageGeneratorManifest,
|
|
45
45
|
packageGeneratorInstallCommand
|
|
46
46
|
} from "./generator/registry.js";
|
|
47
|
+
import {
|
|
48
|
+
defaultGeneratorPolicy,
|
|
49
|
+
GENERATOR_POLICY_FILE,
|
|
50
|
+
generatorPackageAllowed,
|
|
51
|
+
generatorPolicyDiagnosticsForBindings,
|
|
52
|
+
loadGeneratorPolicy,
|
|
53
|
+
packageBackedGeneratorBindings,
|
|
54
|
+
packageScopeFromName,
|
|
55
|
+
parseGeneratorPolicyPin,
|
|
56
|
+
writeGeneratorPolicy
|
|
57
|
+
} from "./generator-policy.js";
|
|
47
58
|
import {
|
|
48
59
|
buildAuthHintsQueryPayload,
|
|
49
60
|
buildAuthReviewPacketPayload,
|
|
@@ -207,6 +218,11 @@ function printUsage(options = {}) {
|
|
|
207
218
|
console.log(" or: topogram generator list [--json]");
|
|
208
219
|
console.log(" or: topogram generator show <id-or-package> [--json]");
|
|
209
220
|
console.log(" or: topogram generator check <path-or-package> [--json]");
|
|
221
|
+
console.log(" or: topogram generator policy init [path] [--json]");
|
|
222
|
+
console.log(" or: topogram generator policy status [path] [--json]");
|
|
223
|
+
console.log(" or: topogram generator policy check [path] [--json]");
|
|
224
|
+
console.log(" or: topogram generator policy explain [path] [--json]");
|
|
225
|
+
console.log(" or: topogram generator policy pin [package@version] [path] [--json]");
|
|
210
226
|
console.log(" or: topogram new <path> [--template hello-web|todo|./local-template|@scope/template]");
|
|
211
227
|
console.log(" or: topogram new --list-templates [--json] [--catalog <path-or-source>]");
|
|
212
228
|
console.log("");
|
|
@@ -228,6 +244,7 @@ function printUsage(options = {}) {
|
|
|
228
244
|
console.log(" topogram generator list");
|
|
229
245
|
console.log(" topogram generator show @topogram/generator-react-web");
|
|
230
246
|
console.log(" topogram generator check ./generator-package");
|
|
247
|
+
console.log(" topogram generator policy check");
|
|
231
248
|
console.log(" topogram generate");
|
|
232
249
|
console.log(" topogram import ./existing-app --out ./imported-topogram");
|
|
233
250
|
console.log(" topogram import diff ./imported-topogram");
|
|
@@ -569,6 +586,11 @@ function printGeneratorHelp() {
|
|
|
569
586
|
console.log("Usage: topogram generator list [--json]");
|
|
570
587
|
console.log(" or: topogram generator show <id-or-package> [--json]");
|
|
571
588
|
console.log(" or: topogram generator check <path-or-package> [--json]");
|
|
589
|
+
console.log(" or: topogram generator policy init [path] [--json]");
|
|
590
|
+
console.log(" or: topogram generator policy status [path] [--json]");
|
|
591
|
+
console.log(" or: topogram generator policy check [path] [--json]");
|
|
592
|
+
console.log(" or: topogram generator policy explain [path] [--json]");
|
|
593
|
+
console.log(" or: topogram generator policy pin [package@version] [path] [--json]");
|
|
572
594
|
console.log("");
|
|
573
595
|
console.log("Inspects generator manifests and checks generator pack conformance.");
|
|
574
596
|
console.log("");
|
|
@@ -577,6 +599,7 @@ function printGeneratorHelp() {
|
|
|
577
599
|
console.log(" - show accepts an installed package name or a bundled fallback generator id.");
|
|
578
600
|
console.log(" - check validates a local generator package path or an already installed package.");
|
|
579
601
|
console.log(" - Topogram does not install generator packages during show or check.");
|
|
602
|
+
console.log(` - package-backed project generators are governed by ${GENERATOR_POLICY_FILE}; bundled topogram/* generators are allowed.`);
|
|
580
603
|
console.log("");
|
|
581
604
|
console.log("Examples:");
|
|
582
605
|
console.log(" topogram generator list");
|
|
@@ -585,6 +608,10 @@ function printGeneratorHelp() {
|
|
|
585
608
|
console.log(" topogram generator show @scope/topogram-generator-web --json");
|
|
586
609
|
console.log(" topogram generator check ./generator-package");
|
|
587
610
|
console.log(" topogram generator check @scope/topogram-generator-web --json");
|
|
611
|
+
console.log(" topogram generator policy init");
|
|
612
|
+
console.log(" topogram generator policy status --json");
|
|
613
|
+
console.log(" topogram generator policy check --json");
|
|
614
|
+
console.log(" topogram generator policy pin @topogram/generator-react-web@1");
|
|
588
615
|
}
|
|
589
616
|
|
|
590
617
|
function printTemplateHelp() {
|
|
@@ -789,7 +816,7 @@ function printImportHelp() {
|
|
|
789
816
|
function printCheckHelp() {
|
|
790
817
|
console.log("Usage: topogram check [path] [--json]");
|
|
791
818
|
console.log("");
|
|
792
|
-
console.log("Validates Topogram files, project configuration, topology, generator compatibility, output ownership, and template policy.");
|
|
819
|
+
console.log("Validates Topogram files, project configuration, topology, generator compatibility, generator policy, output ownership, and template policy.");
|
|
793
820
|
console.log("");
|
|
794
821
|
console.log("Defaults: path is ./topogram.");
|
|
795
822
|
console.log("");
|
|
@@ -1524,6 +1551,556 @@ function printGeneratorShow(payload) {
|
|
|
1524
1551
|
console.log(stableStringify(payload.exampleTopologyBinding));
|
|
1525
1552
|
}
|
|
1526
1553
|
|
|
1554
|
+
/**
|
|
1555
|
+
* @param {string} name
|
|
1556
|
+
* @param {boolean} ok
|
|
1557
|
+
* @param {string} actual
|
|
1558
|
+
* @param {string} expected
|
|
1559
|
+
* @param {string} message
|
|
1560
|
+
* @param {string|null} fix
|
|
1561
|
+
* @returns {{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }}
|
|
1562
|
+
*/
|
|
1563
|
+
function generatorPolicyRule(name, ok, actual, expected, message, fix = null) {
|
|
1564
|
+
return { name, ok, actual, expected, message, fix };
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* @param {string} name
|
|
1569
|
+
* @returns {string}
|
|
1570
|
+
*/
|
|
1571
|
+
function generatorPolicyRuleLabel(name) {
|
|
1572
|
+
return ({
|
|
1573
|
+
"policy-file": "Policy file",
|
|
1574
|
+
"allowed-package": "Allowed package",
|
|
1575
|
+
"pinned-version": "Pinned version"
|
|
1576
|
+
})[name] || name;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* @param {import("./generator-policy.js").GeneratorPolicyInfo} policyInfo
|
|
1581
|
+
* @returns {import("./generator-policy.js").GeneratorPolicy}
|
|
1582
|
+
*/
|
|
1583
|
+
function effectiveGeneratorPolicy(policyInfo) {
|
|
1584
|
+
return policyInfo.policy || {
|
|
1585
|
+
version: "0.1",
|
|
1586
|
+
allowedPackageScopes: ["@topogram"],
|
|
1587
|
+
allowedPackages: [],
|
|
1588
|
+
pinnedVersions: {}
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* @param {string} filePath
|
|
1594
|
+
* @returns {any|null}
|
|
1595
|
+
*/
|
|
1596
|
+
function readJsonIfPresent(filePath) {
|
|
1597
|
+
if (!fs.existsSync(filePath)) {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
try {
|
|
1601
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1602
|
+
} catch {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* @param {Record<string, any>|null} projectPackage
|
|
1609
|
+
* @param {string} packageName
|
|
1610
|
+
* @returns {{ field: string|null, spec: string|null }}
|
|
1611
|
+
*/
|
|
1612
|
+
function dependencySpecForPackage(projectPackage, packageName) {
|
|
1613
|
+
const dependencyFields = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
1614
|
+
for (const field of dependencyFields) {
|
|
1615
|
+
const dependencies = projectPackage?.[field];
|
|
1616
|
+
if (dependencies && typeof dependencies === "object" && typeof dependencies[packageName] === "string") {
|
|
1617
|
+
return {
|
|
1618
|
+
field,
|
|
1619
|
+
spec: dependencies[packageName]
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
field: null,
|
|
1625
|
+
spec: null
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* @param {Record<string, any>|null} lockfile
|
|
1631
|
+
* @param {string} packageName
|
|
1632
|
+
* @returns {{ version: string|null, resolved: string|null, integrity: string|null, entryPath: string|null }}
|
|
1633
|
+
*/
|
|
1634
|
+
function lockfileInfoForPackage(lockfile, packageName) {
|
|
1635
|
+
const packageEntryPath = `node_modules/${packageName}`;
|
|
1636
|
+
const packageEntry = lockfile?.packages?.[packageEntryPath];
|
|
1637
|
+
if (packageEntry && typeof packageEntry === "object") {
|
|
1638
|
+
return {
|
|
1639
|
+
version: typeof packageEntry.version === "string" ? packageEntry.version : null,
|
|
1640
|
+
resolved: typeof packageEntry.resolved === "string" ? packageEntry.resolved : null,
|
|
1641
|
+
integrity: typeof packageEntry.integrity === "string" ? packageEntry.integrity : null,
|
|
1642
|
+
entryPath: packageEntryPath
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
const dependencyEntry = lockfile?.dependencies?.[packageName];
|
|
1646
|
+
if (dependencyEntry && typeof dependencyEntry === "object") {
|
|
1647
|
+
return {
|
|
1648
|
+
version: typeof dependencyEntry.version === "string" ? dependencyEntry.version : null,
|
|
1649
|
+
resolved: typeof dependencyEntry.resolved === "string" ? dependencyEntry.resolved : null,
|
|
1650
|
+
integrity: typeof dependencyEntry.integrity === "string" ? dependencyEntry.integrity : null,
|
|
1651
|
+
entryPath: packageName
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
version: null,
|
|
1656
|
+
resolved: null,
|
|
1657
|
+
integrity: null,
|
|
1658
|
+
entryPath: null
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* @param {string} projectRoot
|
|
1664
|
+
* @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 }}
|
|
1666
|
+
*/
|
|
1667
|
+
function packageInfoForGenerator(projectRoot, packageName) {
|
|
1668
|
+
const projectPackage = readJsonIfPresent(path.join(projectRoot, "package.json"));
|
|
1669
|
+
const dependency = dependencySpecForPackage(projectPackage, packageName);
|
|
1670
|
+
const lockfile = readJsonIfPresent(path.join(projectRoot, "package-lock.json"));
|
|
1671
|
+
const lockfileInfo = lockfileInfoForPackage(lockfile, packageName);
|
|
1672
|
+
const installedPackageJsonPath = path.join(projectRoot, "node_modules", ...packageName.split("/"), "package.json");
|
|
1673
|
+
const installedPackage = readJsonIfPresent(installedPackageJsonPath);
|
|
1674
|
+
return {
|
|
1675
|
+
dependencyField: dependency.field,
|
|
1676
|
+
dependencySpec: dependency.spec,
|
|
1677
|
+
installedVersion: typeof installedPackage?.version === "string" ? installedPackage.version : null,
|
|
1678
|
+
installedPackageJsonPath: installedPackage ? installedPackageJsonPath : null,
|
|
1679
|
+
lockfileVersion: lockfileInfo.version,
|
|
1680
|
+
lockfileResolved: lockfileInfo.resolved,
|
|
1681
|
+
lockfileIntegrity: lockfileInfo.integrity,
|
|
1682
|
+
lockfileEntryPath: lockfileInfo.entryPath
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* @param {string} projectRoot
|
|
1688
|
+
* @param {import("./generator-policy.js").GeneratorPolicy} policy
|
|
1689
|
+
* @param {ReturnType<typeof packageBackedGeneratorBindings>[number]} binding
|
|
1690
|
+
* @returns {ReturnType<typeof packageBackedGeneratorBindings>[number] & { allowed: boolean, packageInfo: ReturnType<typeof packageInfoForGenerator>, pin: { key: string|null, version: string|null, matches: boolean|null } }}
|
|
1691
|
+
*/
|
|
1692
|
+
function generatorPolicyBindingStatus(projectRoot, policy, binding) {
|
|
1693
|
+
const packagePin = policy.pinnedVersions[binding.packageName] || null;
|
|
1694
|
+
const generatorPin = policy.pinnedVersions[binding.generatorId] || null;
|
|
1695
|
+
const pinnedVersion = packagePin || generatorPin;
|
|
1696
|
+
return {
|
|
1697
|
+
...binding,
|
|
1698
|
+
allowed: generatorPackageAllowed(policy, binding.packageName),
|
|
1699
|
+
packageInfo: packageInfoForGenerator(projectRoot, binding.packageName),
|
|
1700
|
+
pin: {
|
|
1701
|
+
key: packagePin ? binding.packageName : generatorPin ? binding.generatorId : null,
|
|
1702
|
+
version: pinnedVersion,
|
|
1703
|
+
matches: pinnedVersion ? pinnedVersion === binding.version : null
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* @param {any[]} diagnostics
|
|
1710
|
+
* @param {Array<ReturnType<typeof generatorPolicyBindingStatus>>} bindings
|
|
1711
|
+
* @returns {any[]}
|
|
1712
|
+
*/
|
|
1713
|
+
function annotateGeneratorPolicyDiagnostics(diagnostics, bindings) {
|
|
1714
|
+
return diagnostics.map((diagnostic) => {
|
|
1715
|
+
const binding = bindings.find((item) => (
|
|
1716
|
+
item.packageName === diagnostic.packageName &&
|
|
1717
|
+
(!diagnostic.componentId || item.componentId === diagnostic.componentId)
|
|
1718
|
+
));
|
|
1719
|
+
if (!binding) {
|
|
1720
|
+
return diagnostic;
|
|
1721
|
+
}
|
|
1722
|
+
return {
|
|
1723
|
+
...diagnostic,
|
|
1724
|
+
packageVersion: binding.packageInfo.installedVersion || binding.packageInfo.lockfileVersion || null,
|
|
1725
|
+
packageDependencyField: binding.packageInfo.dependencyField,
|
|
1726
|
+
packageDependencySpec: binding.packageInfo.dependencySpec,
|
|
1727
|
+
packageLockVersion: binding.packageInfo.lockfileVersion
|
|
1728
|
+
};
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* @param {string} projectPath
|
|
1734
|
+
* @returns {{ ok: boolean, path: string, exists: boolean, policy: any, defaulted: boolean, bindings: Array<ReturnType<typeof generatorPolicyBindingStatus>>, diagnostics: any[], errors: string[] }}
|
|
1735
|
+
*/
|
|
1736
|
+
function buildGeneratorPolicyCheckPayload(projectPath) {
|
|
1737
|
+
const projectConfigInfo = loadProjectConfig(projectPath);
|
|
1738
|
+
if (!projectConfigInfo) {
|
|
1739
|
+
const diagnostic = {
|
|
1740
|
+
code: "generator_policy_project_missing",
|
|
1741
|
+
severity: "error",
|
|
1742
|
+
message: "Cannot check generator policy without topogram.project.json.",
|
|
1743
|
+
path: path.resolve(projectPath),
|
|
1744
|
+
suggestedFix: "Run this command in a Topogram project.",
|
|
1745
|
+
step: "generator-policy"
|
|
1746
|
+
};
|
|
1747
|
+
return {
|
|
1748
|
+
ok: false,
|
|
1749
|
+
path: path.join(path.resolve(projectPath), GENERATOR_POLICY_FILE),
|
|
1750
|
+
exists: false,
|
|
1751
|
+
policy: null,
|
|
1752
|
+
defaulted: false,
|
|
1753
|
+
bindings: [],
|
|
1754
|
+
diagnostics: [diagnostic],
|
|
1755
|
+
errors: [diagnostic.message]
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
const policyInfo = loadGeneratorPolicy(projectConfigInfo.configDir);
|
|
1759
|
+
const rawBindings = packageBackedGeneratorBindings(projectConfigInfo.config);
|
|
1760
|
+
const policy = policyInfo.policy || effectiveGeneratorPolicy(policyInfo);
|
|
1761
|
+
const bindings = rawBindings.map((binding) => generatorPolicyBindingStatus(projectConfigInfo.configDir, policy, binding));
|
|
1762
|
+
const diagnostics = [];
|
|
1763
|
+
if (!policyInfo.exists) {
|
|
1764
|
+
diagnostics.push({
|
|
1765
|
+
code: "generator_policy_missing",
|
|
1766
|
+
severity: "warning",
|
|
1767
|
+
message: `No ${GENERATOR_POLICY_FILE} found. Default generator policy allows @topogram/* package-backed generators and blocks other package scopes.`,
|
|
1768
|
+
path: policyInfo.path,
|
|
1769
|
+
suggestedFix: "Run `topogram generator policy init` to write an explicit project generator policy after review.",
|
|
1770
|
+
step: "generator-policy"
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
diagnostics.push(...generatorPolicyDiagnosticsForBindings(policyInfo, rawBindings, "generator-policy"));
|
|
1774
|
+
const annotatedDiagnostics = annotateGeneratorPolicyDiagnostics(diagnostics, bindings);
|
|
1775
|
+
const errors = annotatedDiagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
|
|
1776
|
+
return {
|
|
1777
|
+
ok: errors.length === 0,
|
|
1778
|
+
path: policyInfo.path,
|
|
1779
|
+
exists: policyInfo.exists,
|
|
1780
|
+
policy,
|
|
1781
|
+
defaulted: !policyInfo.exists,
|
|
1782
|
+
bindings,
|
|
1783
|
+
diagnostics: annotatedDiagnostics,
|
|
1784
|
+
errors
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* @param {string} projectPath
|
|
1790
|
+
* @returns {ReturnType<typeof buildGeneratorPolicyCheckPayload> & { rules: Array<{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }> }}
|
|
1791
|
+
*/
|
|
1792
|
+
function buildGeneratorPolicyExplainPayload(projectPath) {
|
|
1793
|
+
const check = buildGeneratorPolicyCheckPayload(projectPath);
|
|
1794
|
+
const policy = check.policy || effectiveGeneratorPolicy({ path: check.path, exists: false, policy: null, diagnostics: [] });
|
|
1795
|
+
const rules = [];
|
|
1796
|
+
rules.push(generatorPolicyRule(
|
|
1797
|
+
"policy-file",
|
|
1798
|
+
check.exists,
|
|
1799
|
+
check.exists ? "present" : "missing",
|
|
1800
|
+
"present",
|
|
1801
|
+
check.exists
|
|
1802
|
+
? "Project has a generator policy file."
|
|
1803
|
+
: "Project is using the default generator policy.",
|
|
1804
|
+
check.exists ? null : "Run `topogram generator policy init` after review."
|
|
1805
|
+
));
|
|
1806
|
+
for (const binding of check.bindings) {
|
|
1807
|
+
const scope = packageScopeFromName(binding.packageName);
|
|
1808
|
+
rules.push(generatorPolicyRule(
|
|
1809
|
+
"allowed-package",
|
|
1810
|
+
generatorPackageAllowed(policy, binding.packageName),
|
|
1811
|
+
`${binding.packageName}${scope ? ` (${scope})` : ""}`,
|
|
1812
|
+
[
|
|
1813
|
+
`scopes=${policy.allowedPackageScopes.join(", ") || "(none)"}`,
|
|
1814
|
+
`packages=${policy.allowedPackages.join(", ") || "(none)"}`
|
|
1815
|
+
].join("; "),
|
|
1816
|
+
`Component '${binding.componentId}' package-backed generator must be from an allowed package or scope.`,
|
|
1817
|
+
`Run \`topogram generator policy pin ${binding.packageName}@${binding.version}\` after reviewing the generator package.`
|
|
1818
|
+
));
|
|
1819
|
+
const pinnedVersion = policy.pinnedVersions[binding.packageName] || policy.pinnedVersions[binding.generatorId] || null;
|
|
1820
|
+
rules.push(generatorPolicyRule(
|
|
1821
|
+
"pinned-version",
|
|
1822
|
+
!pinnedVersion || pinnedVersion === binding.version,
|
|
1823
|
+
binding.version,
|
|
1824
|
+
pinnedVersion || "(unpinned)",
|
|
1825
|
+
`Component '${binding.componentId}' generator version must match its policy pin when one exists.`,
|
|
1826
|
+
`Run \`topogram generator policy pin ${binding.packageName}@${binding.version}\` after review.`
|
|
1827
|
+
));
|
|
1828
|
+
}
|
|
1829
|
+
return {
|
|
1830
|
+
...check,
|
|
1831
|
+
rules
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* @param {string} projectPath
|
|
1837
|
+
* @returns {ReturnType<typeof buildGeneratorPolicyExplainPayload> & { summary: { packageBackedGenerators: number, allowed: number, denied: number, pinned: number, unpinned: number, pinMismatches: number } }}
|
|
1838
|
+
*/
|
|
1839
|
+
function buildGeneratorPolicyStatusPayload(projectPath) {
|
|
1840
|
+
const explain = buildGeneratorPolicyExplainPayload(projectPath);
|
|
1841
|
+
return {
|
|
1842
|
+
...explain,
|
|
1843
|
+
summary: {
|
|
1844
|
+
packageBackedGenerators: explain.bindings.length,
|
|
1845
|
+
allowed: explain.bindings.filter((binding) => binding.allowed).length,
|
|
1846
|
+
denied: explain.bindings.filter((binding) => !binding.allowed).length,
|
|
1847
|
+
pinned: explain.bindings.filter((binding) => Boolean(binding.pin.version)).length,
|
|
1848
|
+
unpinned: explain.bindings.filter((binding) => !binding.pin.version).length,
|
|
1849
|
+
pinMismatches: explain.bindings.filter((binding) => binding.pin.matches === false).length
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* @param {ReturnType<typeof buildGeneratorPolicyCheckPayload>} payload
|
|
1856
|
+
* @returns {void}
|
|
1857
|
+
*/
|
|
1858
|
+
function printGeneratorPolicyCheckPayload(payload) {
|
|
1859
|
+
console.log(payload.ok ? "Generator policy check passed" : "Generator policy check failed");
|
|
1860
|
+
console.log(`Policy: ${payload.path}`);
|
|
1861
|
+
console.log(`Exists: ${payload.exists ? "yes" : "no"}`);
|
|
1862
|
+
console.log(`Defaulted: ${payload.defaulted ? "yes" : "no"}`);
|
|
1863
|
+
console.log(`Package-backed generators: ${payload.bindings.length}`);
|
|
1864
|
+
for (const binding of payload.bindings) {
|
|
1865
|
+
console.log(`- ${binding.componentId}: ${binding.generatorId}@${binding.version} via ${binding.packageName}`);
|
|
1866
|
+
console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
|
|
1867
|
+
if (binding.packageInfo.dependencySpec) {
|
|
1868
|
+
console.log(` dependency: ${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}`);
|
|
1869
|
+
}
|
|
1870
|
+
if (binding.packageInfo.lockfileVersion) {
|
|
1871
|
+
console.log(` lockfile: ${binding.packageInfo.lockfileVersion}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1875
|
+
console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
|
1876
|
+
if (diagnostic.path) {
|
|
1877
|
+
console.log(` path: ${diagnostic.path}`);
|
|
1878
|
+
}
|
|
1879
|
+
if (diagnostic.suggestedFix) {
|
|
1880
|
+
console.log(` fix: ${diagnostic.suggestedFix}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* @param {ReturnType<typeof buildGeneratorPolicyStatusPayload>} payload
|
|
1887
|
+
* @returns {void}
|
|
1888
|
+
*/
|
|
1889
|
+
function printGeneratorPolicyStatusPayload(payload) {
|
|
1890
|
+
console.log(payload.ok ? "Generator policy status: allowed" : "Generator policy status: denied");
|
|
1891
|
+
console.log(`Policy file: ${payload.path}`);
|
|
1892
|
+
console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
|
|
1893
|
+
console.log(`Default policy active: ${payload.defaulted ? "yes" : "no"}`);
|
|
1894
|
+
console.log(`Package-backed generators: ${payload.summary.packageBackedGenerators}`);
|
|
1895
|
+
console.log(`Allowed packages: ${payload.summary.allowed}`);
|
|
1896
|
+
console.log(`Denied packages: ${payload.summary.denied}`);
|
|
1897
|
+
console.log(`Pinned generators: ${payload.summary.pinned}`);
|
|
1898
|
+
console.log(`Unpinned generators: ${payload.summary.unpinned}`);
|
|
1899
|
+
console.log(`Pin mismatches: ${payload.summary.pinMismatches}`);
|
|
1900
|
+
if (payload.bindings.length > 0) {
|
|
1901
|
+
console.log("");
|
|
1902
|
+
console.log("Generator packages:");
|
|
1903
|
+
}
|
|
1904
|
+
for (const binding of payload.bindings) {
|
|
1905
|
+
console.log(`- ${binding.componentId}: ${binding.generatorId}@${binding.version} via ${binding.packageName}`);
|
|
1906
|
+
console.log(` allowed: ${binding.allowed ? "yes" : "no"}`);
|
|
1907
|
+
console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
|
|
1908
|
+
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)"}`);
|
|
1910
|
+
console.log(` policy pin: ${binding.pin.version ? `${binding.pin.key}@${binding.pin.version}` : "(none)"}`);
|
|
1911
|
+
}
|
|
1912
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1913
|
+
const label = diagnostic.severity === "warning" ? "Warning" : "Error";
|
|
1914
|
+
console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
|
|
1915
|
+
if (diagnostic.packageVersion) {
|
|
1916
|
+
console.log(` package version: ${diagnostic.packageVersion}`);
|
|
1917
|
+
}
|
|
1918
|
+
if (diagnostic.packageDependencySpec) {
|
|
1919
|
+
console.log(` dependency: ${diagnostic.packageDependencyField} ${diagnostic.packageDependencySpec}`);
|
|
1920
|
+
}
|
|
1921
|
+
if (diagnostic.suggestedFix) {
|
|
1922
|
+
console.log(` fix: ${diagnostic.suggestedFix}`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* @param {ReturnType<typeof buildGeneratorPolicyExplainPayload>} payload
|
|
1929
|
+
* @returns {void}
|
|
1930
|
+
*/
|
|
1931
|
+
function printGeneratorPolicyExplainPayload(payload) {
|
|
1932
|
+
console.log(payload.ok ? "Generator policy: allowed" : "Generator policy: denied");
|
|
1933
|
+
console.log(payload.ok
|
|
1934
|
+
? "Decision: package-backed generators are allowed by this project's generator policy."
|
|
1935
|
+
: "Decision: one or more package-backed generators are blocked by this project's generator policy.");
|
|
1936
|
+
console.log(`Policy file: ${payload.path}`);
|
|
1937
|
+
console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
|
|
1938
|
+
console.log(`Default policy active: ${payload.defaulted ? "yes" : "no"}`);
|
|
1939
|
+
if (payload.bindings.length > 0) {
|
|
1940
|
+
console.log("");
|
|
1941
|
+
console.log("Package-backed generators:");
|
|
1942
|
+
for (const binding of payload.bindings) {
|
|
1943
|
+
console.log(`- ${binding.componentId}: ${binding.generatorId}@${binding.version} via ${binding.packageName}`);
|
|
1944
|
+
console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
|
|
1945
|
+
if (binding.packageInfo.dependencySpec) {
|
|
1946
|
+
console.log(` dependency: ${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}`);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (payload.rules.length > 0) {
|
|
1951
|
+
console.log("");
|
|
1952
|
+
console.log("Policy checks:");
|
|
1953
|
+
}
|
|
1954
|
+
for (const rule of payload.rules) {
|
|
1955
|
+
console.log(`${rule.ok ? "PASS" : "FAIL"} ${generatorPolicyRuleLabel(rule.name)}: ${rule.message}`);
|
|
1956
|
+
console.log(` actual: ${rule.actual}`);
|
|
1957
|
+
console.log(` expected: ${rule.expected}`);
|
|
1958
|
+
if (!rule.ok && rule.fix) {
|
|
1959
|
+
console.log(` fix: ${rule.fix}`);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
for (const diagnostic of payload.diagnostics) {
|
|
1963
|
+
const label = diagnostic.severity === "warning" ? "Warning" : "Error";
|
|
1964
|
+
console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
|
|
1965
|
+
if (diagnostic.suggestedFix) {
|
|
1966
|
+
console.log(` fix: ${diagnostic.suggestedFix}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* @param {string} projectPath
|
|
1973
|
+
* @param {string|null|undefined} spec
|
|
1974
|
+
* @returns {{ ok: boolean, path: string, policy: any, pinned: Array<{ packageName: string, version: string }>, diagnostics: any[], errors: string[] }}
|
|
1975
|
+
*/
|
|
1976
|
+
function buildGeneratorPolicyPinPayload(projectPath, spec) {
|
|
1977
|
+
const projectConfigInfo = loadProjectConfig(projectPath);
|
|
1978
|
+
if (!projectConfigInfo) {
|
|
1979
|
+
const diagnostic = {
|
|
1980
|
+
code: "generator_policy_project_missing",
|
|
1981
|
+
severity: "error",
|
|
1982
|
+
message: "Cannot pin generator policy without topogram.project.json.",
|
|
1983
|
+
path: path.resolve(projectPath),
|
|
1984
|
+
suggestedFix: "Run this command in a Topogram project.",
|
|
1985
|
+
step: "generator-policy"
|
|
1986
|
+
};
|
|
1987
|
+
return {
|
|
1988
|
+
ok: false,
|
|
1989
|
+
path: path.join(path.resolve(projectPath), GENERATOR_POLICY_FILE),
|
|
1990
|
+
policy: null,
|
|
1991
|
+
pinned: [],
|
|
1992
|
+
diagnostics: [diagnostic],
|
|
1993
|
+
errors: [diagnostic.message]
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const policyInfo = loadGeneratorPolicy(projectConfigInfo.configDir);
|
|
1997
|
+
if (policyInfo.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
1998
|
+
const errors = policyInfo.diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
|
|
1999
|
+
return {
|
|
2000
|
+
ok: false,
|
|
2001
|
+
path: policyInfo.path,
|
|
2002
|
+
policy: policyInfo.policy,
|
|
2003
|
+
pinned: [],
|
|
2004
|
+
diagnostics: policyInfo.diagnostics,
|
|
2005
|
+
errors
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
let pins = [];
|
|
2009
|
+
try {
|
|
2010
|
+
pins = spec
|
|
2011
|
+
? [parseGeneratorPolicyPin(spec)]
|
|
2012
|
+
: packageBackedGeneratorBindings(projectConfigInfo.config).map((binding) => ({
|
|
2013
|
+
packageName: binding.packageName,
|
|
2014
|
+
version: binding.version
|
|
2015
|
+
}));
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
const diagnostic = {
|
|
2018
|
+
code: "generator_policy_pin_invalid",
|
|
2019
|
+
severity: "error",
|
|
2020
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2021
|
+
path: policyInfo.path,
|
|
2022
|
+
suggestedFix: "Pass a pin such as @topogram/generator-react-web@1.",
|
|
2023
|
+
step: "generator-policy"
|
|
2024
|
+
};
|
|
2025
|
+
return {
|
|
2026
|
+
ok: false,
|
|
2027
|
+
path: policyInfo.path,
|
|
2028
|
+
policy: policyInfo.policy,
|
|
2029
|
+
pinned: [],
|
|
2030
|
+
diagnostics: [diagnostic],
|
|
2031
|
+
errors: [diagnostic.message]
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
if (pins.length === 0) {
|
|
2035
|
+
const diagnostic = {
|
|
2036
|
+
code: "generator_policy_pin_no_generators",
|
|
2037
|
+
severity: "error",
|
|
2038
|
+
message: "No package-backed topology generator bindings are available to pin.",
|
|
2039
|
+
path: projectConfigInfo.configPath,
|
|
2040
|
+
suggestedFix: "Pass an explicit pin such as @topogram/generator-react-web@1, or use bundled generators.",
|
|
2041
|
+
step: "generator-policy"
|
|
2042
|
+
};
|
|
2043
|
+
return {
|
|
2044
|
+
ok: false,
|
|
2045
|
+
path: policyInfo.path,
|
|
2046
|
+
policy: policyInfo.policy,
|
|
2047
|
+
pinned: [],
|
|
2048
|
+
diagnostics: [diagnostic],
|
|
2049
|
+
errors: [diagnostic.message]
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
const policy = policyInfo.policy || defaultGeneratorPolicy();
|
|
2053
|
+
const allowedPackages = [...policy.allowedPackages];
|
|
2054
|
+
const allowedPackageScopes = [...policy.allowedPackageScopes];
|
|
2055
|
+
const pinnedVersions = { ...policy.pinnedVersions };
|
|
2056
|
+
for (const pin of pins) {
|
|
2057
|
+
if (!allowedPackages.includes(pin.packageName)) {
|
|
2058
|
+
allowedPackages.push(pin.packageName);
|
|
2059
|
+
}
|
|
2060
|
+
const scope = packageScopeFromName(pin.packageName);
|
|
2061
|
+
if (scope && !allowedPackageScopes.includes(scope)) {
|
|
2062
|
+
allowedPackageScopes.push(scope);
|
|
2063
|
+
}
|
|
2064
|
+
pinnedVersions[pin.packageName] = pin.version;
|
|
2065
|
+
}
|
|
2066
|
+
const nextPolicy = {
|
|
2067
|
+
...policy,
|
|
2068
|
+
allowedPackageScopes,
|
|
2069
|
+
allowedPackages,
|
|
2070
|
+
pinnedVersions
|
|
2071
|
+
};
|
|
2072
|
+
writeGeneratorPolicy(projectConfigInfo.configDir, nextPolicy);
|
|
2073
|
+
return {
|
|
2074
|
+
ok: true,
|
|
2075
|
+
path: path.join(projectConfigInfo.configDir, GENERATOR_POLICY_FILE),
|
|
2076
|
+
policy: nextPolicy,
|
|
2077
|
+
pinned: pins,
|
|
2078
|
+
diagnostics: [],
|
|
2079
|
+
errors: []
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/**
|
|
2084
|
+
* @param {{ ok: boolean, path: string, pinned: Array<{ packageName: string, version: string }>, diagnostics: any[] }} payload
|
|
2085
|
+
* @returns {void}
|
|
2086
|
+
*/
|
|
2087
|
+
function printGeneratorPolicyPinPayload(payload) {
|
|
2088
|
+
console.log(payload.ok ? "Generator policy pin updated" : "Generator policy pin failed");
|
|
2089
|
+
console.log(`Policy: ${payload.path}`);
|
|
2090
|
+
for (const pin of payload.pinned) {
|
|
2091
|
+
console.log(`Pinned: ${pin.packageName}@${pin.version}`);
|
|
2092
|
+
}
|
|
2093
|
+
for (const diagnostic of payload.diagnostics) {
|
|
2094
|
+
console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
|
|
2095
|
+
if (diagnostic.path) {
|
|
2096
|
+
console.log(` path: ${diagnostic.path}`);
|
|
2097
|
+
}
|
|
2098
|
+
if (diagnostic.suggestedFix) {
|
|
2099
|
+
console.log(` fix: ${diagnostic.suggestedFix}`);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1527
2104
|
function combineProjectValidationResults(...results) {
|
|
1528
2105
|
const errors = [];
|
|
1529
2106
|
for (const result of results) {
|
|
@@ -3340,6 +3917,7 @@ function printNewProjectResult(result, cwd) {
|
|
|
3340
3917
|
}
|
|
3341
3918
|
console.log(`Executable implementation: ${template.includesExecutableImplementation ? "yes" : "no"}`);
|
|
3342
3919
|
console.log("Policy: topogram.template-policy.json");
|
|
3920
|
+
console.log(`Generator policy: ${GENERATOR_POLICY_FILE}`);
|
|
3343
3921
|
console.log("Template files: .topogram-template-files.json");
|
|
3344
3922
|
if (template.includesExecutableImplementation) {
|
|
3345
3923
|
console.log("Trust: .topogram-template-trust.json");
|
|
@@ -3355,6 +3933,8 @@ function printNewProjectResult(result, cwd) {
|
|
|
3355
3933
|
console.log(" npm run source:status");
|
|
3356
3934
|
console.log(" npm run template:explain");
|
|
3357
3935
|
console.log(" npm run check");
|
|
3936
|
+
console.log(" npm run generator:policy:status");
|
|
3937
|
+
console.log(" npm run generator:policy:check");
|
|
3358
3938
|
if (template.includesExecutableImplementation) {
|
|
3359
3939
|
console.log(" npm run template:policy:explain");
|
|
3360
3940
|
console.log(" npm run trust:status");
|
|
@@ -6856,6 +7436,16 @@ if (args[0] === "version" || args[0] === "--version") {
|
|
|
6856
7436
|
commandArgs = { generatorShow: true, inputPath: args[2] };
|
|
6857
7437
|
} else if (args[0] === "generator" && args[1] === "check") {
|
|
6858
7438
|
commandArgs = { generatorCheck: true, inputPath: args[2] };
|
|
7439
|
+
} else if (args[0] === "generator" && args[1] === "policy" && args[2] === "init") {
|
|
7440
|
+
commandArgs = { generatorPolicyInit: true, inputPath: commandPath(3) };
|
|
7441
|
+
} else if (args[0] === "generator" && args[1] === "policy" && args[2] === "status") {
|
|
7442
|
+
commandArgs = { generatorPolicyStatus: true, inputPath: commandPath(3) };
|
|
7443
|
+
} else if (args[0] === "generator" && args[1] === "policy" && args[2] === "check") {
|
|
7444
|
+
commandArgs = { generatorPolicyCheck: true, inputPath: commandPath(3) };
|
|
7445
|
+
} else if (args[0] === "generator" && args[1] === "policy" && args[2] === "explain") {
|
|
7446
|
+
commandArgs = { generatorPolicyExplain: true, inputPath: commandPath(3) };
|
|
7447
|
+
} else if (args[0] === "generator" && args[1] === "policy" && args[2] === "pin") {
|
|
7448
|
+
commandArgs = { generatorPolicyPin: true, generatorPolicyPinSpec: args[3] && !args[3].startsWith("-") ? args[3] : null, inputPath: commandPath(4) };
|
|
6859
7449
|
} else if (args[0] === "generator") {
|
|
6860
7450
|
printGeneratorHelp();
|
|
6861
7451
|
process.exit(args[1] ? 1 : 0);
|
|
@@ -7052,6 +7642,11 @@ const shouldComponentBehavior = Boolean(commandArgs?.componentBehavior);
|
|
|
7052
7642
|
const shouldGeneratorList = Boolean(commandArgs?.generatorList);
|
|
7053
7643
|
const shouldGeneratorShow = Boolean(commandArgs?.generatorShow);
|
|
7054
7644
|
const shouldGeneratorCheck = Boolean(commandArgs?.generatorCheck);
|
|
7645
|
+
const shouldGeneratorPolicyInit = Boolean(commandArgs?.generatorPolicyInit);
|
|
7646
|
+
const shouldGeneratorPolicyStatus = Boolean(commandArgs?.generatorPolicyStatus);
|
|
7647
|
+
const shouldGeneratorPolicyCheck = Boolean(commandArgs?.generatorPolicyCheck);
|
|
7648
|
+
const shouldGeneratorPolicyExplain = Boolean(commandArgs?.generatorPolicyExplain);
|
|
7649
|
+
const shouldGeneratorPolicyPin = Boolean(commandArgs?.generatorPolicyPin);
|
|
7055
7650
|
const shouldTrustTemplate = Boolean(commandArgs?.trustTemplate);
|
|
7056
7651
|
const shouldTrustStatus = Boolean(commandArgs?.trustStatus);
|
|
7057
7652
|
const shouldTrustDiff = Boolean(commandArgs?.trustDiff);
|
|
@@ -7185,7 +7780,7 @@ const outIndex = args.indexOf("--out");
|
|
|
7185
7780
|
const outPath = outIndex >= 0 ? args[outIndex + 1] : null;
|
|
7186
7781
|
const effectiveOutDir = outDir || outPath || commandArgs?.defaultOutDir || null;
|
|
7187
7782
|
|
|
7188
|
-
if ((shouldCheck || shouldComponentCheck || shouldComponentBehavior || shouldGeneratorCheck || shouldValidate || shouldTrustTemplate || shouldTrustStatus || shouldTrustDiff || shouldSourceStatus || shouldTemplateExplain || shouldTemplateStatus || shouldTemplateDetach || shouldTemplatePolicyInit || shouldTemplatePolicyCheck || shouldTemplatePolicyExplain || shouldTemplatePolicyPin || shouldTemplateCheck || shouldTemplateUpdate || generateTarget === "app-bundle") && !inputPath) {
|
|
7783
|
+
if ((shouldCheck || shouldComponentCheck || shouldComponentBehavior || shouldGeneratorCheck || shouldGeneratorPolicyInit || shouldGeneratorPolicyStatus || shouldGeneratorPolicyCheck || shouldGeneratorPolicyExplain || shouldGeneratorPolicyPin || shouldValidate || shouldTrustTemplate || shouldTrustStatus || shouldTrustDiff || shouldSourceStatus || shouldTemplateExplain || shouldTemplateStatus || shouldTemplateDetach || shouldTemplatePolicyInit || shouldTemplatePolicyCheck || shouldTemplatePolicyExplain || shouldTemplatePolicyPin || shouldTemplateCheck || shouldTemplateUpdate || generateTarget === "app-bundle") && !inputPath) {
|
|
7189
7784
|
console.error("Missing required <path>.");
|
|
7190
7785
|
printUsage();
|
|
7191
7786
|
process.exit(1);
|
|
@@ -7239,7 +7834,7 @@ if (shouldQueryShow && !commandArgs?.queryShowName) {
|
|
|
7239
7834
|
process.exit(1);
|
|
7240
7835
|
}
|
|
7241
7836
|
|
|
7242
|
-
if ((shouldCheck || shouldComponentCheck || shouldComponentBehavior || shouldValidate || shouldTrustTemplate || shouldTrustStatus || shouldTrustDiff || shouldTemplateExplain || shouldTemplateStatus || shouldTemplatePolicyInit || shouldTemplatePolicyCheck || shouldTemplatePolicyExplain || shouldTemplatePolicyPin || shouldTemplateUpdate || generateTarget === "app-bundle") && inputPath) {
|
|
7837
|
+
if ((shouldCheck || shouldComponentCheck || shouldComponentBehavior || shouldValidate || shouldGeneratorPolicyInit || shouldGeneratorPolicyStatus || shouldGeneratorPolicyCheck || shouldGeneratorPolicyExplain || shouldGeneratorPolicyPin || shouldTrustTemplate || shouldTrustStatus || shouldTrustDiff || shouldTemplateExplain || shouldTemplateStatus || shouldTemplatePolicyInit || shouldTemplatePolicyCheck || shouldTemplatePolicyExplain || shouldTemplatePolicyPin || shouldTemplateUpdate || generateTarget === "app-bundle") && inputPath) {
|
|
7243
7838
|
inputPath = normalizeTopogramPath(inputPath);
|
|
7244
7839
|
}
|
|
7245
7840
|
|
|
@@ -7366,6 +7961,69 @@ try {
|
|
|
7366
7961
|
process.exit(payload.ok ? 0 : 1);
|
|
7367
7962
|
}
|
|
7368
7963
|
|
|
7964
|
+
if (shouldGeneratorPolicyInit) {
|
|
7965
|
+
const projectConfigInfo = loadProjectConfig(inputPath);
|
|
7966
|
+
if (!projectConfigInfo) {
|
|
7967
|
+
throw new Error("Cannot initialize generator policy without topogram.project.json.");
|
|
7968
|
+
}
|
|
7969
|
+
const policy = writeGeneratorPolicy(projectConfigInfo.configDir, defaultGeneratorPolicy());
|
|
7970
|
+
const payload = {
|
|
7971
|
+
ok: true,
|
|
7972
|
+
path: path.join(projectConfigInfo.configDir, GENERATOR_POLICY_FILE),
|
|
7973
|
+
policy,
|
|
7974
|
+
diagnostics: [],
|
|
7975
|
+
errors: []
|
|
7976
|
+
};
|
|
7977
|
+
if (emitJson) {
|
|
7978
|
+
console.log(stableStringify(payload));
|
|
7979
|
+
} else {
|
|
7980
|
+
console.log(`Wrote generator policy: ${payload.path}`);
|
|
7981
|
+
console.log(`Allowed package scopes: ${policy.allowedPackageScopes.join(", ") || "(none)"}`);
|
|
7982
|
+
console.log(`Allowed packages: ${policy.allowedPackages.join(", ") || "(none)"}`);
|
|
7983
|
+
}
|
|
7984
|
+
process.exit(0);
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7987
|
+
if (shouldGeneratorPolicyStatus) {
|
|
7988
|
+
const payload = buildGeneratorPolicyStatusPayload(inputPath);
|
|
7989
|
+
if (emitJson) {
|
|
7990
|
+
console.log(stableStringify(payload));
|
|
7991
|
+
} else {
|
|
7992
|
+
printGeneratorPolicyStatusPayload(payload);
|
|
7993
|
+
}
|
|
7994
|
+
process.exit(payload.ok ? 0 : 1);
|
|
7995
|
+
}
|
|
7996
|
+
|
|
7997
|
+
if (shouldGeneratorPolicyCheck) {
|
|
7998
|
+
const payload = buildGeneratorPolicyCheckPayload(inputPath);
|
|
7999
|
+
if (emitJson) {
|
|
8000
|
+
console.log(stableStringify(payload));
|
|
8001
|
+
} else {
|
|
8002
|
+
printGeneratorPolicyCheckPayload(payload);
|
|
8003
|
+
}
|
|
8004
|
+
process.exit(payload.ok ? 0 : 1);
|
|
8005
|
+
}
|
|
8006
|
+
|
|
8007
|
+
if (shouldGeneratorPolicyExplain) {
|
|
8008
|
+
const payload = buildGeneratorPolicyExplainPayload(inputPath);
|
|
8009
|
+
if (emitJson) {
|
|
8010
|
+
console.log(stableStringify(payload));
|
|
8011
|
+
} else {
|
|
8012
|
+
printGeneratorPolicyExplainPayload(payload);
|
|
8013
|
+
}
|
|
8014
|
+
process.exit(payload.ok ? 0 : 1);
|
|
8015
|
+
}
|
|
8016
|
+
|
|
8017
|
+
if (shouldGeneratorPolicyPin) {
|
|
8018
|
+
const payload = buildGeneratorPolicyPinPayload(inputPath, commandArgs?.generatorPolicyPinSpec);
|
|
8019
|
+
if (emitJson) {
|
|
8020
|
+
console.log(stableStringify(payload));
|
|
8021
|
+
} else {
|
|
8022
|
+
printGeneratorPolicyPinPayload(payload);
|
|
8023
|
+
}
|
|
8024
|
+
process.exit(payload.ok ? 0 : 1);
|
|
8025
|
+
}
|
|
8026
|
+
|
|
7369
8027
|
if (shouldCatalogList) {
|
|
7370
8028
|
const payload = buildCatalogListPayload(catalogSource || inputPath || null);
|
|
7371
8029
|
if (emitJson) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const GENERATOR_POLICY_FILE: string;
|
|
2
|
+
export function defaultGeneratorPolicy(): any;
|
|
3
|
+
export function validateGeneratorPolicy(value: unknown, policyPath: string): any;
|
|
4
|
+
export function packageScopeFromName(packageName: string): string | null;
|
|
5
|
+
export function generatorPackageAllowed(policy: any, packageName: string): boolean;
|
|
6
|
+
export function packageBackedGeneratorBindings(projectConfig: any): any[];
|
|
7
|
+
export function loadGeneratorPolicy(projectRoot: string): any;
|
|
8
|
+
export function writeGeneratorPolicy(projectRoot: string, policy: any): any;
|
|
9
|
+
export function generatorPolicyDiagnosticsForBindings(policyInfo: any, bindings: any[], step?: string): any[];
|
|
10
|
+
export function generatorPolicyDiagnosticsForProject(projectConfig: any, projectRoot: string, step?: string): any[];
|
|
11
|
+
export function validateProjectGeneratorPolicy(projectConfig: any, options?: { configDir?: string | null; rootDir?: string | null }): { ok: boolean; errors: Array<{ message: string; loc: null }> };
|
|
12
|
+
export function parseGeneratorPolicyPin(spec: string): { packageName: string; version: string };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { stableStringify } from "./format.js";
|
|
7
|
+
|
|
8
|
+
export const GENERATOR_POLICY_FILE = "topogram.generator-policy.json";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} GeneratorPolicy
|
|
12
|
+
* @property {string} version
|
|
13
|
+
* @property {string[]} allowedPackageScopes
|
|
14
|
+
* @property {string[]} allowedPackages
|
|
15
|
+
* @property {Record<string, string>} pinnedVersions
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} GeneratorPolicyInfo
|
|
20
|
+
* @property {string} path
|
|
21
|
+
* @property {GeneratorPolicy|null} policy
|
|
22
|
+
* @property {boolean} exists
|
|
23
|
+
* @property {GeneratorPolicyDiagnostic[]} diagnostics
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} GeneratorPolicyDiagnostic
|
|
28
|
+
* @property {string} code
|
|
29
|
+
* @property {"error"|"warning"} severity
|
|
30
|
+
* @property {string} message
|
|
31
|
+
* @property {string|null} path
|
|
32
|
+
* @property {string|null} suggestedFix
|
|
33
|
+
* @property {string|null} step
|
|
34
|
+
* @property {string|null} [componentId]
|
|
35
|
+
* @property {string|null} [generatorId]
|
|
36
|
+
* @property {string|null} [packageName]
|
|
37
|
+
* @property {string|null} [version]
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} PackageGeneratorBinding
|
|
42
|
+
* @property {string} componentId
|
|
43
|
+
* @property {string} componentType
|
|
44
|
+
* @property {string} projection
|
|
45
|
+
* @property {string} generatorId
|
|
46
|
+
* @property {string} version
|
|
47
|
+
* @property {string} packageName
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {Record<string, any>} input
|
|
52
|
+
* @returns {GeneratorPolicyDiagnostic}
|
|
53
|
+
*/
|
|
54
|
+
function generatorPolicyDiagnostic(input) {
|
|
55
|
+
return {
|
|
56
|
+
code: String(input.code || "generator_policy_failed"),
|
|
57
|
+
severity: input.severity === "warning" ? "warning" : "error",
|
|
58
|
+
message: String(input.message || "Generator policy check failed."),
|
|
59
|
+
path: typeof input.path === "string" ? input.path : null,
|
|
60
|
+
suggestedFix: typeof input.suggestedFix === "string" ? input.suggestedFix : null,
|
|
61
|
+
step: typeof input.step === "string" ? input.step : null,
|
|
62
|
+
componentId: typeof input.componentId === "string" ? input.componentId : null,
|
|
63
|
+
generatorId: typeof input.generatorId === "string" ? input.generatorId : null,
|
|
64
|
+
packageName: typeof input.packageName === "string" ? input.packageName : null,
|
|
65
|
+
version: typeof input.version === "string" ? input.version : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @param {string} fieldName
|
|
72
|
+
* @param {string} policyPath
|
|
73
|
+
* @returns {string[]}
|
|
74
|
+
*/
|
|
75
|
+
function optionalStringArray(value, fieldName, policyPath) {
|
|
76
|
+
if (value == null) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(value)) {
|
|
80
|
+
throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
|
|
81
|
+
}
|
|
82
|
+
return value.map((item) => {
|
|
83
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
84
|
+
throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
|
|
85
|
+
}
|
|
86
|
+
return item;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {unknown} value
|
|
92
|
+
* @param {string} policyPath
|
|
93
|
+
* @returns {Record<string, string>}
|
|
94
|
+
*/
|
|
95
|
+
function optionalStringRecord(value, policyPath) {
|
|
96
|
+
if (value == null) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
100
|
+
throw new Error(`${policyPath} pinnedVersions must be an object of package-or-generator ids to versions.`);
|
|
101
|
+
}
|
|
102
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
103
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
104
|
+
throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
|
|
105
|
+
}
|
|
106
|
+
return [key, item];
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @returns {GeneratorPolicy}
|
|
112
|
+
*/
|
|
113
|
+
export function defaultGeneratorPolicy() {
|
|
114
|
+
return {
|
|
115
|
+
version: "0.1",
|
|
116
|
+
allowedPackageScopes: ["@topogram"],
|
|
117
|
+
allowedPackages: [],
|
|
118
|
+
pinnedVersions: {}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {unknown} value
|
|
124
|
+
* @param {string} policyPath
|
|
125
|
+
* @returns {GeneratorPolicy}
|
|
126
|
+
*/
|
|
127
|
+
export function validateGeneratorPolicy(value, policyPath) {
|
|
128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
129
|
+
throw new Error(`${GENERATOR_POLICY_FILE} must contain a JSON object.`);
|
|
130
|
+
}
|
|
131
|
+
const raw = /** @type {Record<string, unknown>} */ (value);
|
|
132
|
+
const defaults = defaultGeneratorPolicy();
|
|
133
|
+
return {
|
|
134
|
+
version: typeof raw.version === "string" && raw.version ? raw.version : defaults.version,
|
|
135
|
+
allowedPackageScopes: raw.allowedPackageScopes == null
|
|
136
|
+
? defaults.allowedPackageScopes
|
|
137
|
+
: optionalStringArray(raw.allowedPackageScopes, "allowedPackageScopes", policyPath),
|
|
138
|
+
allowedPackages: optionalStringArray(raw.allowedPackages, "allowedPackages", policyPath),
|
|
139
|
+
pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} packageName
|
|
145
|
+
* @returns {string|null}
|
|
146
|
+
*/
|
|
147
|
+
export function packageScopeFromName(packageName) {
|
|
148
|
+
return packageName.startsWith("@") ? packageName.split("/")[0] || null : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} allowed
|
|
153
|
+
* @param {string|null} scope
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function packageScopeMatches(allowed, scope) {
|
|
157
|
+
return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {GeneratorPolicy} policy
|
|
162
|
+
* @param {string} packageName
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
export function generatorPackageAllowed(policy, packageName) {
|
|
166
|
+
if (policy.allowedPackages.includes(packageName)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const scope = packageScopeFromName(packageName);
|
|
170
|
+
return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {Record<string, any>} projectConfig
|
|
175
|
+
* @returns {PackageGeneratorBinding[]}
|
|
176
|
+
*/
|
|
177
|
+
export function packageBackedGeneratorBindings(projectConfig) {
|
|
178
|
+
const components = Array.isArray(projectConfig?.topology?.components)
|
|
179
|
+
? projectConfig.topology.components
|
|
180
|
+
: [];
|
|
181
|
+
return components
|
|
182
|
+
.filter((component) => typeof component?.generator?.package === "string" && component.generator.package.length > 0)
|
|
183
|
+
.map((component) => ({
|
|
184
|
+
componentId: String(component.id || "unknown"),
|
|
185
|
+
componentType: String(component.type || "unknown"),
|
|
186
|
+
projection: String(component.projection || "unknown"),
|
|
187
|
+
generatorId: String(component.generator.id || "unknown"),
|
|
188
|
+
version: String(component.generator.version || "unknown"),
|
|
189
|
+
packageName: String(component.generator.package)
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} projectRoot
|
|
195
|
+
* @returns {GeneratorPolicyInfo}
|
|
196
|
+
*/
|
|
197
|
+
export function loadGeneratorPolicy(projectRoot) {
|
|
198
|
+
const policyPath = path.join(projectRoot, GENERATOR_POLICY_FILE);
|
|
199
|
+
if (!fs.existsSync(policyPath)) {
|
|
200
|
+
return {
|
|
201
|
+
path: policyPath,
|
|
202
|
+
policy: null,
|
|
203
|
+
exists: false,
|
|
204
|
+
diagnostics: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return {
|
|
209
|
+
path: policyPath,
|
|
210
|
+
policy: validateGeneratorPolicy(JSON.parse(fs.readFileSync(policyPath, "utf8")), policyPath),
|
|
211
|
+
exists: true,
|
|
212
|
+
diagnostics: []
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
path: policyPath,
|
|
217
|
+
policy: null,
|
|
218
|
+
exists: true,
|
|
219
|
+
diagnostics: [generatorPolicyDiagnostic({
|
|
220
|
+
code: "generator_policy_invalid",
|
|
221
|
+
message: error instanceof Error ? error.message : String(error),
|
|
222
|
+
path: policyPath,
|
|
223
|
+
suggestedFix: `Fix ${GENERATOR_POLICY_FILE} or regenerate it with \`topogram generator policy init\`.`,
|
|
224
|
+
step: "generator-policy"
|
|
225
|
+
})]
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {string} projectRoot
|
|
232
|
+
* @param {GeneratorPolicy} policy
|
|
233
|
+
* @returns {GeneratorPolicy}
|
|
234
|
+
*/
|
|
235
|
+
export function writeGeneratorPolicy(projectRoot, policy) {
|
|
236
|
+
fs.writeFileSync(path.join(projectRoot, GENERATOR_POLICY_FILE), `${stableStringify(policy)}\n`, "utf8");
|
|
237
|
+
return policy;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {GeneratorPolicyInfo} policyInfo
|
|
242
|
+
* @returns {GeneratorPolicy}
|
|
243
|
+
*/
|
|
244
|
+
function effectivePolicy(policyInfo) {
|
|
245
|
+
return policyInfo.policy || defaultGeneratorPolicy();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {GeneratorPolicyInfo} policyInfo
|
|
250
|
+
* @param {PackageGeneratorBinding[]} bindings
|
|
251
|
+
* @param {string} step
|
|
252
|
+
* @returns {GeneratorPolicyDiagnostic[]}
|
|
253
|
+
*/
|
|
254
|
+
export function generatorPolicyDiagnosticsForBindings(policyInfo, bindings, step = "generator-policy") {
|
|
255
|
+
if (policyInfo.diagnostics.length > 0) {
|
|
256
|
+
return policyInfo.diagnostics;
|
|
257
|
+
}
|
|
258
|
+
const policy = effectivePolicy(policyInfo);
|
|
259
|
+
/** @type {GeneratorPolicyDiagnostic[]} */
|
|
260
|
+
const diagnostics = [];
|
|
261
|
+
for (const binding of bindings) {
|
|
262
|
+
if (!generatorPackageAllowed(policy, binding.packageName)) {
|
|
263
|
+
const scope = packageScopeFromName(binding.packageName);
|
|
264
|
+
const allowedScopes = policy.allowedPackageScopes.join(", ") || "(none)";
|
|
265
|
+
const allowedPackages = policy.allowedPackages.join(", ") || "(none)";
|
|
266
|
+
diagnostics.push(generatorPolicyDiagnostic({
|
|
267
|
+
code: "generator_package_denied",
|
|
268
|
+
message: `Component '${binding.componentId}' generator package '${binding.packageName}' is not allowed by ${GENERATOR_POLICY_FILE}.`,
|
|
269
|
+
path: policyInfo.path,
|
|
270
|
+
suggestedFix: `Review '${binding.packageName}', then run \`topogram generator policy pin ${binding.packageName}@${binding.version}\` or add '${scope || binding.packageName}' to ${GENERATOR_POLICY_FILE}.`,
|
|
271
|
+
step,
|
|
272
|
+
componentId: binding.componentId,
|
|
273
|
+
generatorId: binding.generatorId,
|
|
274
|
+
packageName: binding.packageName,
|
|
275
|
+
version: binding.version
|
|
276
|
+
}));
|
|
277
|
+
diagnostics[diagnostics.length - 1].message += ` Allowed scopes: ${allowedScopes}; allowed packages: ${allowedPackages}.`;
|
|
278
|
+
}
|
|
279
|
+
const pinnedVersion = policy.pinnedVersions[binding.packageName] || policy.pinnedVersions[binding.generatorId] || null;
|
|
280
|
+
if (pinnedVersion && pinnedVersion !== binding.version) {
|
|
281
|
+
diagnostics.push(generatorPolicyDiagnostic({
|
|
282
|
+
code: "generator_version_mismatch",
|
|
283
|
+
message: `Component '${binding.componentId}' generator '${binding.generatorId}' uses version '${binding.version}', but ${GENERATOR_POLICY_FILE} pins '${binding.packageName}' to '${pinnedVersion}'.`,
|
|
284
|
+
path: policyInfo.path,
|
|
285
|
+
suggestedFix: `Use generator version '${pinnedVersion}', or run \`topogram generator policy pin ${binding.packageName}@${binding.version}\` after review.`,
|
|
286
|
+
step,
|
|
287
|
+
componentId: binding.componentId,
|
|
288
|
+
generatorId: binding.generatorId,
|
|
289
|
+
packageName: binding.packageName,
|
|
290
|
+
version: binding.version
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return diagnostics;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @param {Record<string, any>} projectConfig
|
|
299
|
+
* @param {string} projectRoot
|
|
300
|
+
* @param {string} [step]
|
|
301
|
+
* @returns {GeneratorPolicyDiagnostic[]}
|
|
302
|
+
*/
|
|
303
|
+
export function generatorPolicyDiagnosticsForProject(projectConfig, projectRoot, step = "generator-policy") {
|
|
304
|
+
const bindings = packageBackedGeneratorBindings(projectConfig);
|
|
305
|
+
const policyInfo = loadGeneratorPolicy(projectRoot);
|
|
306
|
+
return generatorPolicyDiagnosticsForBindings(policyInfo, bindings, step);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {Record<string, any>} projectConfig
|
|
311
|
+
* @param {{ configDir?: string|null, rootDir?: string|null }} [options]
|
|
312
|
+
* @returns {{ ok: boolean, errors: Array<{ message: string, loc: null }> }}
|
|
313
|
+
*/
|
|
314
|
+
export function validateProjectGeneratorPolicy(projectConfig, options = {}) {
|
|
315
|
+
const projectRoot = options.configDir || options.rootDir || process.cwd();
|
|
316
|
+
const diagnostics = generatorPolicyDiagnosticsForProject(projectConfig, projectRoot, "project-config");
|
|
317
|
+
const errors = diagnostics
|
|
318
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
319
|
+
.map((diagnostic) => ({
|
|
320
|
+
message: diagnostic.suggestedFix
|
|
321
|
+
? `${diagnostic.message} Suggested fix: ${diagnostic.suggestedFix}`
|
|
322
|
+
: diagnostic.message,
|
|
323
|
+
loc: null
|
|
324
|
+
}));
|
|
325
|
+
return {
|
|
326
|
+
ok: errors.length === 0,
|
|
327
|
+
errors
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {string} spec
|
|
333
|
+
* @returns {{ packageName: string, version: string }}
|
|
334
|
+
*/
|
|
335
|
+
export function parseGeneratorPolicyPin(spec) {
|
|
336
|
+
const separator = spec.lastIndexOf("@");
|
|
337
|
+
if (separator <= 0 || separator === spec.length - 1) {
|
|
338
|
+
throw new Error("Generator policy pin requires a package name and generator version, for example @topogram/generator-react-web@1.");
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
packageName: spec.slice(0, separator),
|
|
342
|
+
version: spec.slice(separator + 1)
|
|
343
|
+
};
|
|
344
|
+
}
|
package/src/new-project.js
CHANGED
|
@@ -6,6 +6,7 @@ import crypto from "node:crypto";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
|
|
9
|
+
import { defaultGeneratorPolicy, writeGeneratorPolicy } from "./generator-policy.js";
|
|
9
10
|
import { writeTemplateTrustRecord } from "./template-trust.js";
|
|
10
11
|
|
|
11
12
|
const CLI_PACKAGE_NAME = "@topogram/cli";
|
|
@@ -1974,6 +1975,9 @@ function writeProjectPackage(projectRoot, engineRoot, template) {
|
|
|
1974
1975
|
"template:detach:dry-run": "topogram template detach --dry-run",
|
|
1975
1976
|
"template:policy:check": "topogram template policy check",
|
|
1976
1977
|
"template:policy:explain": "topogram template policy explain",
|
|
1978
|
+
"generator:policy:status": "topogram generator policy status",
|
|
1979
|
+
"generator:policy:check": "topogram generator policy check",
|
|
1980
|
+
"generator:policy:explain": "topogram generator policy explain",
|
|
1977
1981
|
"template:update:status": "topogram template update --status",
|
|
1978
1982
|
"template:update:recommend": "topogram template update --recommend",
|
|
1979
1983
|
"template:update:plan": "topogram template update --plan",
|
|
@@ -2049,6 +2053,9 @@ Useful inspection:
|
|
|
2049
2053
|
npm run template:detach:dry-run
|
|
2050
2054
|
npm run template:policy:check
|
|
2051
2055
|
npm run template:policy:explain
|
|
2056
|
+
npm run generator:policy:status
|
|
2057
|
+
npm run generator:policy:check
|
|
2058
|
+
npm run generator:policy:explain
|
|
2052
2059
|
npm run template:update:status
|
|
2053
2060
|
npm run template:update:recommend
|
|
2054
2061
|
npm run template:update:plan
|
|
@@ -2079,6 +2086,8 @@ function writeProjectReadme(projectRoot, projectConfig) {
|
|
|
2079
2086
|
"npm run template:explain",
|
|
2080
2087
|
"npm run check",
|
|
2081
2088
|
"npm run template:policy:check",
|
|
2089
|
+
"npm run generator:policy:status",
|
|
2090
|
+
"npm run generator:policy:check",
|
|
2082
2091
|
...(template.includesExecutableImplementation ? [
|
|
2083
2092
|
"npm run template:policy:explain",
|
|
2084
2093
|
"npm run trust:status"
|
|
@@ -2154,6 +2163,7 @@ export function createNewProject({
|
|
|
2154
2163
|
writeProjectReadme(projectRoot, projectConfig);
|
|
2155
2164
|
writeTemplateFilesManifest(projectRoot, projectConfig);
|
|
2156
2165
|
writeTemplatePolicy(projectRoot, defaultTemplatePolicyForTemplate(template));
|
|
2166
|
+
writeGeneratorPolicy(projectRoot, defaultGeneratorPolicy());
|
|
2157
2167
|
|
|
2158
2168
|
const warnings = [];
|
|
2159
2169
|
if (template.manifest.includesExecutableImplementation) {
|
package/src/project-config.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveGeneratorManifestForBinding,
|
|
9
9
|
validateGeneratorManifest
|
|
10
10
|
} from "./generator/registry.js";
|
|
11
|
+
import { validateProjectGeneratorPolicy } from "./generator-policy.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @typedef {Object} GeneratorBinding
|
|
@@ -434,6 +435,10 @@ export function validateProjectConfig(config, graph = null, options = {}) {
|
|
|
434
435
|
for (const component of config.topology.components) {
|
|
435
436
|
validateComponentShape(errors, component, seenIds);
|
|
436
437
|
}
|
|
438
|
+
const generatorPolicy = validateProjectGeneratorPolicy(config, options);
|
|
439
|
+
for (const error of generatorPolicy.errors) {
|
|
440
|
+
pushError(errors, error.message, error.loc);
|
|
441
|
+
}
|
|
437
442
|
if (graph) {
|
|
438
443
|
const projections = projectionById(graph);
|
|
439
444
|
for (const component of config.topology.components) {
|