@cyclonedx/cdxgen 12.2.0 → 12.3.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.
- package/README.md +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +30 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -35
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- package/types/bin/licenses.d.ts.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { PackageURL } from "packageurl-js";
|
|
2
|
+
|
|
3
|
+
import { isSpdxJsonLd } from "./bomUtils.js";
|
|
4
|
+
|
|
5
|
+
const toArray = (value) => {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (value !== undefined && value !== null) {
|
|
10
|
+
return [value];
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const toCycloneDxLikeComponent = (spdxElement) => {
|
|
16
|
+
const purl = spdxElement?.software_packageUrl;
|
|
17
|
+
let group = "";
|
|
18
|
+
let name = spdxElement?.name || spdxElement?.spdxId || "unnamed-component";
|
|
19
|
+
let version = spdxElement?.software_packageVersion || "";
|
|
20
|
+
if (purl) {
|
|
21
|
+
try {
|
|
22
|
+
const purlObj = PackageURL.fromString(purl);
|
|
23
|
+
group = purlObj.namespace || "";
|
|
24
|
+
name = purlObj.name || name;
|
|
25
|
+
version = purlObj.version || version;
|
|
26
|
+
} catch (_err) {
|
|
27
|
+
// Keep SPDX element values when purl parsing fails.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
type: spdxElement?.type === "software_File" ? "file" : "library",
|
|
32
|
+
group,
|
|
33
|
+
name,
|
|
34
|
+
version,
|
|
35
|
+
purl,
|
|
36
|
+
"bom-ref": purl || spdxElement?.spdxId || name,
|
|
37
|
+
description: spdxElement?.description,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const toCycloneDxLikeBom = (bomJson) => {
|
|
42
|
+
if (!isSpdxJsonLd(bomJson)) {
|
|
43
|
+
return bomJson;
|
|
44
|
+
}
|
|
45
|
+
const graphElements = toArray(bomJson?.["@graph"]);
|
|
46
|
+
const packageElements = graphElements.filter((element) =>
|
|
47
|
+
["software_Package", "software_File"].includes(element?.type),
|
|
48
|
+
);
|
|
49
|
+
const components = packageElements.map(toCycloneDxLikeComponent);
|
|
50
|
+
const spdxIdToRef = new Map();
|
|
51
|
+
for (let index = 0; index < packageElements.length; index++) {
|
|
52
|
+
const spdxId = packageElements[index]?.spdxId;
|
|
53
|
+
if (spdxId) {
|
|
54
|
+
spdxIdToRef.set(spdxId, components[index]["bom-ref"]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const dependencyMap = new Map();
|
|
58
|
+
for (const component of components) {
|
|
59
|
+
dependencyMap.set(component["bom-ref"], new Set());
|
|
60
|
+
}
|
|
61
|
+
for (const element of graphElements) {
|
|
62
|
+
if (
|
|
63
|
+
element?.type !== "Relationship" ||
|
|
64
|
+
element?.relationshipType !== "dependsOn"
|
|
65
|
+
) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!element?.from || typeof element.from !== "string") {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const fromRef = spdxIdToRef.get(element.from) || element.from;
|
|
72
|
+
const toRefs = toArray(element?.to).map(
|
|
73
|
+
(toRef) => spdxIdToRef.get(toRef) || toRef,
|
|
74
|
+
);
|
|
75
|
+
if (!dependencyMap.has(fromRef)) {
|
|
76
|
+
dependencyMap.set(fromRef, new Set());
|
|
77
|
+
}
|
|
78
|
+
const dependsOnSet = dependencyMap.get(fromRef);
|
|
79
|
+
for (const toRef of toRefs) {
|
|
80
|
+
if (toRef) {
|
|
81
|
+
dependsOnSet.add(toRef);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const dependencies = [];
|
|
86
|
+
for (const [ref, dependsOnSet] of dependencyMap.entries()) {
|
|
87
|
+
dependencies.push({
|
|
88
|
+
ref,
|
|
89
|
+
dependsOn: Array.from(dependsOnSet).sort(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...bomJson,
|
|
94
|
+
components,
|
|
95
|
+
dependencies,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { toCycloneDxLikeBom } from "./spdxUtils.js";
|
|
4
|
+
|
|
5
|
+
const sampleSpdx = {
|
|
6
|
+
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
|
7
|
+
"@graph": [
|
|
8
|
+
{ type: "CreationInfo", spdxId: "urn:demo#CreationInfo-main" },
|
|
9
|
+
{ type: "SpdxDocument", spdxId: "urn:demo#SPDXRef-DOCUMENT" },
|
|
10
|
+
{
|
|
11
|
+
type: "software_Package",
|
|
12
|
+
spdxId: "urn:demo#SPDXRef-app",
|
|
13
|
+
name: "app",
|
|
14
|
+
software_packageUrl: "pkg:npm/@acme/app@1.2.3",
|
|
15
|
+
software_packageVersion: "1.2.3",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: "software_Package",
|
|
19
|
+
spdxId: "urn:demo#SPDXRef-lib",
|
|
20
|
+
name: "lib",
|
|
21
|
+
software_packageUrl: "pkg:npm/lodash@4.17.21",
|
|
22
|
+
software_packageVersion: "4.17.21",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "Relationship",
|
|
26
|
+
spdxId: "urn:demo#Relationship-1",
|
|
27
|
+
relationshipType: "dependsOn",
|
|
28
|
+
from: "urn:demo#SPDXRef-app",
|
|
29
|
+
to: ["urn:demo#SPDXRef-lib"],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("spdxUtils", () => {
|
|
35
|
+
it("returns non-SPDX BOMs unchanged", () => {
|
|
36
|
+
const cyclonedxBom = { bomFormat: "CycloneDX", components: [] };
|
|
37
|
+
assert.strictEqual(toCycloneDxLikeBom(cyclonedxBom), cyclonedxBom);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("converts SPDX package and relationship graph into CycloneDX-like components/dependencies", () => {
|
|
41
|
+
const converted = toCycloneDxLikeBom(sampleSpdx);
|
|
42
|
+
assert.strictEqual(Array.isArray(converted.components), true);
|
|
43
|
+
assert.strictEqual(converted.components.length, 2);
|
|
44
|
+
assert.strictEqual(converted.components[0].name, "app");
|
|
45
|
+
assert.strictEqual(converted.components[0].version, "1.2.3");
|
|
46
|
+
assert.strictEqual(Array.isArray(converted.dependencies), true);
|
|
47
|
+
const appDependency = converted.dependencies.find(
|
|
48
|
+
(dep) => dep.ref === "pkg:npm/@acme/app@1.2.3",
|
|
49
|
+
);
|
|
50
|
+
assert.ok(appDependency);
|
|
51
|
+
assert.deepStrictEqual(appDependency.dependsOn, ["pkg:npm/lodash@4.17.21"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("ignores invalid SPDX relationships where 'from' is not a string", () => {
|
|
55
|
+
const malformedSpdx = structuredClone(sampleSpdx);
|
|
56
|
+
malformedSpdx["@graph"].push({
|
|
57
|
+
type: "Relationship",
|
|
58
|
+
spdxId: "urn:demo#Relationship-2",
|
|
59
|
+
relationshipType: "dependsOn",
|
|
60
|
+
from: ["urn:demo#SPDXRef-app"],
|
|
61
|
+
to: ["urn:demo#SPDXRef-lib"],
|
|
62
|
+
});
|
|
63
|
+
const converted = toCycloneDxLikeBom(malformedSpdx);
|
|
64
|
+
const appDependency = converted.dependencies.find(
|
|
65
|
+
(dep) => dep.ref === "pkg:npm/@acme/app@1.2.3",
|
|
66
|
+
);
|
|
67
|
+
assert.ok(appDependency);
|
|
68
|
+
assert.deepStrictEqual(appDependency.dependsOn, ["pkg:npm/lodash@4.17.21"]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { TABLE_BORDER_STYLE } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
const ANSI_PATTERN = "\\u001B\\[[0-?]*[ -/]*[@-~]";
|
|
6
|
+
const ANSI_REGEX = new RegExp(ANSI_PATTERN, "g");
|
|
7
|
+
const COMBINING_MARK_REGEX = /\p{Mark}/u;
|
|
8
|
+
const BORDER_STYLES = {
|
|
9
|
+
ascii: {
|
|
10
|
+
bottomJoin: "+",
|
|
11
|
+
bottomLeft: "+",
|
|
12
|
+
bottomRight: "+",
|
|
13
|
+
horizontal: "-",
|
|
14
|
+
midJoin: "+",
|
|
15
|
+
midLeft: "+",
|
|
16
|
+
midRight: "+",
|
|
17
|
+
topJoin: "+",
|
|
18
|
+
topLeft: "+",
|
|
19
|
+
topRight: "+",
|
|
20
|
+
vertical: "|",
|
|
21
|
+
},
|
|
22
|
+
unicode: {
|
|
23
|
+
bottomJoin: "┴",
|
|
24
|
+
bottomLeft: "└",
|
|
25
|
+
bottomRight: "┘",
|
|
26
|
+
horizontal: "─",
|
|
27
|
+
midJoin: "┼",
|
|
28
|
+
midLeft: "├",
|
|
29
|
+
midRight: "┤",
|
|
30
|
+
topJoin: "┬",
|
|
31
|
+
topLeft: "┌",
|
|
32
|
+
topRight: "┐",
|
|
33
|
+
vertical: "│",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const stripAnsi = (input) => `${input ?? ""}`.replace(ANSI_REGEX, "");
|
|
38
|
+
|
|
39
|
+
const isFullWidthCodePoint = (codePoint) => {
|
|
40
|
+
return (
|
|
41
|
+
codePoint >= 0x1100 &&
|
|
42
|
+
(codePoint <= 0x115f ||
|
|
43
|
+
codePoint === 0x2329 ||
|
|
44
|
+
codePoint === 0x232a ||
|
|
45
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
46
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
47
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
48
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
49
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
50
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
51
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
52
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
53
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff))
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const stringWidth = (input) => {
|
|
58
|
+
const clean = stripAnsi(input);
|
|
59
|
+
let width = 0;
|
|
60
|
+
for (const char of clean) {
|
|
61
|
+
if (char === "\n" || char === "\r") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (COMBINING_MARK_REGEX.test(char)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const codePoint = char.codePointAt(0);
|
|
68
|
+
width += isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
69
|
+
}
|
|
70
|
+
return width;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const alignText = (text, width, alignment = "left") => {
|
|
74
|
+
const visibleWidth = stringWidth(text);
|
|
75
|
+
if (visibleWidth >= width) {
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
const totalPad = width - visibleWidth;
|
|
79
|
+
if (alignment === "right") {
|
|
80
|
+
return `${" ".repeat(totalPad)}${text}`;
|
|
81
|
+
}
|
|
82
|
+
if (alignment === "center") {
|
|
83
|
+
const left = Math.floor(totalPad / 2);
|
|
84
|
+
const right = totalPad - left;
|
|
85
|
+
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
|
|
86
|
+
}
|
|
87
|
+
return `${text}${" ".repeat(totalPad)}`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const splitAnsiTokens = (line) => {
|
|
91
|
+
const tokens = [];
|
|
92
|
+
const ansiRegex = new RegExp(ANSI_PATTERN, "g");
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
for (const match of line.matchAll(ansiRegex)) {
|
|
95
|
+
const index = match.index ?? 0;
|
|
96
|
+
if (index > cursor) {
|
|
97
|
+
tokens.push({ isAnsi: false, value: line.slice(cursor, index) });
|
|
98
|
+
}
|
|
99
|
+
tokens.push({ isAnsi: true, value: match[0] });
|
|
100
|
+
cursor = index + match[0].length;
|
|
101
|
+
}
|
|
102
|
+
if (cursor < line.length) {
|
|
103
|
+
tokens.push({ isAnsi: false, value: line.slice(cursor) });
|
|
104
|
+
}
|
|
105
|
+
return tokens;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const wrapLineByChars = (line, width) => {
|
|
109
|
+
if (line === "") {
|
|
110
|
+
return [""];
|
|
111
|
+
}
|
|
112
|
+
if (width <= 0 || stringWidth(line) <= width) {
|
|
113
|
+
return [line];
|
|
114
|
+
}
|
|
115
|
+
const wrapped = [];
|
|
116
|
+
let chunk = "";
|
|
117
|
+
let chunkWidth = 0;
|
|
118
|
+
for (const token of splitAnsiTokens(line)) {
|
|
119
|
+
if (token.isAnsi) {
|
|
120
|
+
// Keep full ANSI sequences attached to the current chunk.
|
|
121
|
+
chunk += token.value;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const char of token.value) {
|
|
125
|
+
const charWidth = stringWidth(char);
|
|
126
|
+
if (chunkWidth + charWidth > width && chunk) {
|
|
127
|
+
wrapped.push(chunk);
|
|
128
|
+
chunk = "";
|
|
129
|
+
chunkWidth = 0;
|
|
130
|
+
}
|
|
131
|
+
chunk += char;
|
|
132
|
+
chunkWidth += charWidth;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (chunk) {
|
|
136
|
+
wrapped.push(chunk);
|
|
137
|
+
}
|
|
138
|
+
return wrapped.length ? wrapped : [""];
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const wrapLineByWords = (line, width) => {
|
|
142
|
+
if (!line) {
|
|
143
|
+
return [""];
|
|
144
|
+
}
|
|
145
|
+
if (width <= 0 || stringWidth(line) <= width) {
|
|
146
|
+
return [line];
|
|
147
|
+
}
|
|
148
|
+
const words = line.split(/\s+/).filter(Boolean);
|
|
149
|
+
if (!words.length) {
|
|
150
|
+
return wrapLineByChars(line, width);
|
|
151
|
+
}
|
|
152
|
+
const wrapped = [];
|
|
153
|
+
let current = "";
|
|
154
|
+
for (const word of words) {
|
|
155
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
156
|
+
if (stringWidth(candidate) <= width) {
|
|
157
|
+
current = candidate;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (current) {
|
|
161
|
+
wrapped.push(current);
|
|
162
|
+
}
|
|
163
|
+
if (stringWidth(word) > width) {
|
|
164
|
+
wrapped.push(...wrapLineByChars(word, width));
|
|
165
|
+
current = "";
|
|
166
|
+
} else {
|
|
167
|
+
current = word;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (current) {
|
|
171
|
+
wrapped.push(current);
|
|
172
|
+
}
|
|
173
|
+
return wrapped.length ? wrapped : [""];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const wrapCellText = (text, width, wrapWord) => {
|
|
177
|
+
const normalized = `${text ?? ""}`;
|
|
178
|
+
const lines = normalized.split(/\r?\n/);
|
|
179
|
+
const wrapped = [];
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
if (wrapWord) {
|
|
182
|
+
wrapped.push(...wrapLineByChars(line, width));
|
|
183
|
+
} else {
|
|
184
|
+
wrapped.push(...wrapLineByWords(line, width));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return wrapped.length ? wrapped : [""];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const getColumnCount = (rows, config = {}) => {
|
|
191
|
+
let maxCols = 0;
|
|
192
|
+
for (const row of rows) {
|
|
193
|
+
if (Array.isArray(row)) {
|
|
194
|
+
maxCols = Math.max(maxCols, row.length);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(config.columns)) {
|
|
198
|
+
maxCols = Math.max(maxCols, config.columns.length);
|
|
199
|
+
}
|
|
200
|
+
if (config.columnCount) {
|
|
201
|
+
maxCols = Math.max(maxCols, config.columnCount);
|
|
202
|
+
}
|
|
203
|
+
return maxCols;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const inferColumnWidth = (rows, columnIndex) => {
|
|
207
|
+
let maxWidth = 3;
|
|
208
|
+
for (const row of rows) {
|
|
209
|
+
const cell = row?.[columnIndex];
|
|
210
|
+
if (cell === undefined || cell === null) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const lines = `${cell}`.split(/\r?\n/);
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
maxWidth = Math.max(maxWidth, stringWidth(line));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return Math.min(maxWidth, 120);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const buildColumns = (rows, config = {}) => {
|
|
222
|
+
const columnDefault = config.columnDefault || {};
|
|
223
|
+
const columns = Array.isArray(config.columns) ? config.columns : [];
|
|
224
|
+
const count = getColumnCount(rows, config);
|
|
225
|
+
const built = [];
|
|
226
|
+
for (let i = 0; i < count; i++) {
|
|
227
|
+
const explicit = columns[i] || {};
|
|
228
|
+
const inferredWidth = inferColumnWidth(rows, i);
|
|
229
|
+
built.push({
|
|
230
|
+
alignment: explicit.alignment || columnDefault.alignment || "left",
|
|
231
|
+
width: Math.max(
|
|
232
|
+
1,
|
|
233
|
+
explicit.width || columnDefault.width || inferredWidth,
|
|
234
|
+
),
|
|
235
|
+
wrapWord: explicit.wrapWord ?? columnDefault.wrapWord ?? false,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return built;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const resolveBorderStyle = (config = {}) => {
|
|
242
|
+
const configBorderStyle = `${config.borderStyle || ""}`.toLowerCase();
|
|
243
|
+
if (configBorderStyle === "ascii" || configBorderStyle === "unicode") {
|
|
244
|
+
return configBorderStyle;
|
|
245
|
+
}
|
|
246
|
+
if (TABLE_BORDER_STYLE === "ascii" || TABLE_BORDER_STYLE === "unicode") {
|
|
247
|
+
return TABLE_BORDER_STYLE;
|
|
248
|
+
}
|
|
249
|
+
const inCI = `${process.env.CI || ""}`.toLowerCase() === "true";
|
|
250
|
+
return process.stdout?.isTTY && !inCI ? "unicode" : "ascii";
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const resolveBorderChars = (config = {}) => {
|
|
254
|
+
return BORDER_STYLES[resolveBorderStyle(config)] || BORDER_STYLES.ascii;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const drawBorder = (columns, borderChars, position = "mid") => {
|
|
258
|
+
const left =
|
|
259
|
+
position === "top"
|
|
260
|
+
? borderChars.topLeft
|
|
261
|
+
: position === "bottom"
|
|
262
|
+
? borderChars.bottomLeft
|
|
263
|
+
: borderChars.midLeft;
|
|
264
|
+
const join =
|
|
265
|
+
position === "top"
|
|
266
|
+
? borderChars.topJoin
|
|
267
|
+
: position === "bottom"
|
|
268
|
+
? borderChars.bottomJoin
|
|
269
|
+
: borderChars.midJoin;
|
|
270
|
+
const right =
|
|
271
|
+
position === "top"
|
|
272
|
+
? borderChars.topRight
|
|
273
|
+
: position === "bottom"
|
|
274
|
+
? borderChars.bottomRight
|
|
275
|
+
: borderChars.midRight;
|
|
276
|
+
return `${left}${columns.map((c) => borderChars.horizontal.repeat(c.width + 2)).join(join)}${right}`;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const renderRow = (row, columns, borderChars) => {
|
|
280
|
+
const wrappedColumns = columns.map((column, index) => {
|
|
281
|
+
return wrapCellText(row?.[index] ?? "", column.width, column.wrapWord);
|
|
282
|
+
});
|
|
283
|
+
let maxHeight = 1;
|
|
284
|
+
for (const lines of wrappedColumns) {
|
|
285
|
+
maxHeight = Math.max(maxHeight, lines.length);
|
|
286
|
+
}
|
|
287
|
+
const rendered = [];
|
|
288
|
+
for (let lineIndex = 0; lineIndex < maxHeight; lineIndex++) {
|
|
289
|
+
const columnSeparator = ` ${borderChars.vertical} `;
|
|
290
|
+
const line = columns
|
|
291
|
+
.map((column, columnIndex) => {
|
|
292
|
+
const raw = wrappedColumns[columnIndex][lineIndex] ?? "";
|
|
293
|
+
return alignText(raw, column.width, column.alignment);
|
|
294
|
+
})
|
|
295
|
+
.join(columnSeparator);
|
|
296
|
+
rendered.push(`${borderChars.vertical} ${line} ${borderChars.vertical}`);
|
|
297
|
+
}
|
|
298
|
+
return rendered;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const renderHeader = (header, columns, borderChars) => {
|
|
302
|
+
if (!header?.content) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
const contentAlignment = header.alignment || "left";
|
|
306
|
+
const totalWidth =
|
|
307
|
+
columns.reduce((sum, c) => sum + c.width, 0) + (columns.length - 1) * 3;
|
|
308
|
+
const headerLines = `${header.content}`.split(/\r?\n/);
|
|
309
|
+
const rendered = [];
|
|
310
|
+
for (const line of headerLines) {
|
|
311
|
+
const wrapped = wrapLineByChars(line, totalWidth);
|
|
312
|
+
for (const wrappedLine of wrapped) {
|
|
313
|
+
rendered.push(
|
|
314
|
+
`${borderChars.vertical} ${alignText(wrappedLine, totalWidth, contentAlignment)} ${borderChars.vertical}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return rendered;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const formatTable = (rows, config = {}) => {
|
|
322
|
+
if (!rows?.length) {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
const columns = buildColumns(rows, config);
|
|
326
|
+
const borderChars = resolveBorderChars(config);
|
|
327
|
+
const topBorder = drawBorder(columns, borderChars, "top");
|
|
328
|
+
const middleBorder = drawBorder(columns, borderChars, "mid");
|
|
329
|
+
const bottomBorder = drawBorder(columns, borderChars, "bottom");
|
|
330
|
+
const output = [topBorder];
|
|
331
|
+
const headerLines = renderHeader(config.header, columns, borderChars);
|
|
332
|
+
if (headerLines.length) {
|
|
333
|
+
output.push(...headerLines);
|
|
334
|
+
output.push(middleBorder);
|
|
335
|
+
}
|
|
336
|
+
for (let i = 0; i < rows.length; i++) {
|
|
337
|
+
output.push(...renderRow(rows[i], columns, borderChars));
|
|
338
|
+
output.push(i < rows.length - 1 ? middleBorder : bottomBorder);
|
|
339
|
+
}
|
|
340
|
+
return output.join("\n");
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export function table(rows, config = {}) {
|
|
344
|
+
return formatTable(rows, config);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function createStream(config = {}) {
|
|
348
|
+
let columns;
|
|
349
|
+
let middleBorder;
|
|
350
|
+
let bottomBorder;
|
|
351
|
+
let hasRows = false;
|
|
352
|
+
let closed = false;
|
|
353
|
+
const borderChars = resolveBorderChars(config);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
write(row) {
|
|
357
|
+
if (closed) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!columns) {
|
|
361
|
+
const seedRows = Array.isArray(row) ? [row] : [[row]];
|
|
362
|
+
columns = buildColumns(seedRows, config);
|
|
363
|
+
const topBorder = drawBorder(columns, borderChars, "top");
|
|
364
|
+
middleBorder = drawBorder(columns, borderChars, "mid");
|
|
365
|
+
bottomBorder = drawBorder(columns, borderChars, "bottom");
|
|
366
|
+
process.stdout.write(`${topBorder}\n`);
|
|
367
|
+
}
|
|
368
|
+
if (hasRows) {
|
|
369
|
+
process.stdout.write(`${middleBorder}\n`);
|
|
370
|
+
}
|
|
371
|
+
const safeRow = Array.isArray(row) ? row : [row];
|
|
372
|
+
const rendered = renderRow(safeRow, columns, borderChars);
|
|
373
|
+
process.stdout.write(`${rendered.join("\n")}\n`);
|
|
374
|
+
hasRows = true;
|
|
375
|
+
},
|
|
376
|
+
end() {
|
|
377
|
+
if (!columns || closed) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
process.stdout.write(`${bottomBorder}\n`);
|
|
381
|
+
closed = true;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|