lula2 0.3.2 → 0.3.4-nightly.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/dist/_app/immutable/assets/0.D6CB7gA7.css +1 -0
- package/dist/_app/immutable/chunks/BITsSGhD.js +65 -0
- package/dist/_app/immutable/chunks/{DVAnYkTd.js → BcKIo18J.js} +3 -3
- package/dist/_app/immutable/chunks/BhjtS45v.js +2 -0
- package/dist/_app/immutable/chunks/{BtuEtkd3.js → BkFSJLu9.js} +1 -1
- package/dist/_app/immutable/chunks/CHdfnWSV.js +1 -0
- package/dist/_app/immutable/chunks/{152nb-LI.js → CzIDw1HQ.js} +1 -1
- package/dist/_app/immutable/chunks/{1spjHGNy.js → DknPZycG.js} +1 -1
- package/dist/_app/immutable/chunks/{DY3-lqhI.js → DwdPeWTx.js} +1 -1
- package/dist/_app/immutable/chunks/{CNOPXlDW.js → vNis_B2V.js} +1 -1
- package/dist/_app/immutable/entry/app.rFT55pRO.js +2 -0
- package/dist/_app/immutable/entry/start.BWvTyytY.js +1 -0
- package/dist/_app/immutable/nodes/{0.Bw6pYYFU.js → 0.CMylckYT.js} +1 -1
- package/dist/_app/immutable/nodes/{1.DbCtPiQe.js → 1.DPLsosR-.js} +1 -1
- package/dist/_app/immutable/nodes/{2.CWnpuE-a.js → 2.DvLMOmaz.js} +1 -1
- package/dist/_app/immutable/nodes/{3.hZ7uQfPx.js → 3.hM9U_7ZV.js} +1 -1
- package/dist/_app/immutable/nodes/4.DNVoxjQw.js +11 -0
- package/dist/_app/version.json +1 -1
- package/dist/cli/commands/ui.js +17 -19
- package/dist/cli/server/index.js +17 -19
- package/dist/cli/server/server.js +17 -19
- package/dist/cli/server/serverState.js +3 -1
- package/dist/cli/server/spreadsheetRoutes.js +4 -8
- package/dist/cli/server/websocketServer.js +17 -19
- package/dist/index.html +10 -10
- package/dist/index.js +17 -19
- package/package.json +21 -22
- package/src/lib/components/control-sets/ControlSetSelector.svelte +1 -1
- package/src/lib/components/controls/ControlDetailsPanel.svelte +1 -1
- package/src/lib/components/controls/ControlsList.svelte +4 -48
- package/src/lib/components/controls/DynamicControlEditor.svelte +2 -2
- package/src/lib/components/controls/MappingCard.svelte +1 -1
- package/src/lib/components/controls/MappingForm.svelte +1 -1
- package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +1 -1
- package/src/lib/components/controls/renderers/FieldRenderer.svelte +2 -2
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +3 -3
- package/src/lib/components/controls/tabs/ImplementationTab.svelte +3 -3
- package/src/lib/components/controls/tabs/MappingsTab.svelte +1 -1
- package/src/lib/components/controls/tabs/OverviewTab.svelte +4 -4
- package/src/lib/components/controls/tabs/TimelineTab.svelte +3 -4
- package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +4 -4
- package/src/lib/components/forms/DynamicControlForm.svelte +10 -10
- package/src/lib/components/forms/DynamicField.svelte +4 -4
- package/src/lib/components/forms/FormField.svelte +1 -1
- package/src/lib/components/setup/ExistingControlSets.svelte +1 -2
- package/src/lib/components/setup/SpreadsheetImport.svelte +112 -115
- package/src/lib/components/ui/CustomDropdown.svelte +1 -1
- package/src/lib/components/ui/FilterBuilder.svelte +4 -4
- package/src/lib/components/ui/TabNavigation.svelte +1 -1
- package/src/lib/components/version-control/DiffViewer.svelte +1 -1
- package/src/lib/components/version-control/YamlDiffViewer.svelte +2 -2
- package/src/stores/compliance.ts +2 -69
- package/dist/_app/immutable/assets/0.CLKu6Q8_.css +0 -1
- package/dist/_app/immutable/chunks/C113Bo4B.js +0 -2
- package/dist/_app/immutable/chunks/C57TYu27.js +0 -65
- package/dist/_app/immutable/chunks/Dfk9QG8E.js +0 -1
- package/dist/_app/immutable/entry/app.D0O4A2k5.js +0 -2
- package/dist/_app/immutable/entry/start.DtHY97nW.js +0 -1
- package/dist/_app/immutable/nodes/4.j9xYp9Vt.js +0 -12
|
@@ -132,7 +132,9 @@ var init_fileStore = __esm({
|
|
|
132
132
|
* Get simple filename from control ID
|
|
133
133
|
*/
|
|
134
134
|
getControlFilename(controlId) {
|
|
135
|
-
const sanitized = controlId.replace(
|
|
135
|
+
const sanitized = controlId.replace(/^([A-Z]+)-(.*)/, (match, prefix, suffix) => {
|
|
136
|
+
return `${prefix}-${suffix.replace(/[^\w]/g, "_")}`;
|
|
137
|
+
});
|
|
136
138
|
return `${sanitized}.yaml`;
|
|
137
139
|
}
|
|
138
140
|
/**
|
|
@@ -1722,7 +1724,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
1722
1724
|
if (req.body.fieldSchema) {
|
|
1723
1725
|
try {
|
|
1724
1726
|
frontendFieldSchema = JSON.parse(req.body.fieldSchema);
|
|
1725
|
-
} catch
|
|
1727
|
+
} catch {
|
|
1726
1728
|
}
|
|
1727
1729
|
}
|
|
1728
1730
|
const fields = {};
|
|
@@ -1782,9 +1784,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
1782
1784
|
uiType = "long_text";
|
|
1783
1785
|
}
|
|
1784
1786
|
let category = frontendConfig?.category || "custom";
|
|
1785
|
-
if (
|
|
1786
|
-
category = "mappings";
|
|
1787
|
-
} else if (!frontendConfig) {
|
|
1787
|
+
if (!frontendConfig) {
|
|
1788
1788
|
if (fieldName.includes("status") || fieldName.includes("state")) {
|
|
1789
1789
|
category = "compliance";
|
|
1790
1790
|
} else if (fieldName.includes("title") || fieldName.includes("name") || fieldName.includes("description")) {
|
|
@@ -1812,7 +1812,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
1812
1812
|
// Control ID is always first
|
|
1813
1813
|
category: isControlIdField ? "core" : category,
|
|
1814
1814
|
// Control ID is always core
|
|
1815
|
-
tab: isControlIdField ? "overview" :
|
|
1815
|
+
tab: isControlIdField ? "overview" : frontendConfig?.tab || void 0
|
|
1816
1816
|
// Use frontend config or default
|
|
1817
1817
|
};
|
|
1818
1818
|
if (uiType === "select") {
|
|
@@ -1841,7 +1841,6 @@ var init_spreadsheetRoutes = __esm({
|
|
|
1841
1841
|
writeFileSync2(join4(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
|
|
1842
1842
|
const controlsDir = join4(baseDir, "controls");
|
|
1843
1843
|
const mappingsDir = join4(baseDir, "mappings");
|
|
1844
|
-
const justificationFieldNames = justificationFields;
|
|
1845
1844
|
families.forEach((familyControls, family) => {
|
|
1846
1845
|
const familyDir = join4(controlsDir, family);
|
|
1847
1846
|
const familyMappingsDir = join4(mappingsDir, family);
|
|
@@ -1876,7 +1875,6 @@ var init_spreadsheetRoutes = __esm({
|
|
|
1876
1875
|
if (fieldName === "family") return;
|
|
1877
1876
|
if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
|
|
1878
1877
|
justificationContents.push(control[fieldName]);
|
|
1879
|
-
filteredControl[fieldName] = control[fieldName];
|
|
1880
1878
|
}
|
|
1881
1879
|
const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
|
|
1882
1880
|
const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
|
|
@@ -1992,7 +1990,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
1992
1990
|
}
|
|
1993
1991
|
const headerCandidates = rows.slice(0, 5).map((row, index) => ({
|
|
1994
1992
|
row: index + 1,
|
|
1995
|
-
preview: row.slice(0, 4).filter((v) => v
|
|
1993
|
+
preview: row.slice(0, 4).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
|
|
1996
1994
|
}));
|
|
1997
1995
|
res.json({
|
|
1998
1996
|
sheets,
|
|
@@ -2239,6 +2237,7 @@ var WebSocketManager = class {
|
|
|
2239
2237
|
`${control.id}.yaml`,
|
|
2240
2238
|
`${control.id.replace(/\./g, "_")}.yaml`,
|
|
2241
2239
|
// AC-1.1 -> AC-1_1.yaml
|
|
2240
|
+
// eslint-disable-next-line no-useless-escape
|
|
2242
2241
|
`${control.id.replace(/[^\w\-]/g, "_")}.yaml`
|
|
2243
2242
|
// General sanitization
|
|
2244
2243
|
];
|
|
@@ -2460,17 +2459,15 @@ var WebSocketManager = class {
|
|
|
2460
2459
|
id: control.id,
|
|
2461
2460
|
family: control.family
|
|
2462
2461
|
};
|
|
2463
|
-
const fieldSchema = controlSetData.fieldSchema?.fields || {};
|
|
2464
|
-
for (const [fieldName
|
|
2465
|
-
if (
|
|
2462
|
+
const fieldSchema = controlSetData.fieldSchema?.fields || controlSetData.field_schema?.fields || {};
|
|
2463
|
+
for (const [fieldName] of Object.entries(fieldSchema)) {
|
|
2464
|
+
if (control[fieldName] !== void 0) {
|
|
2466
2465
|
metadata[fieldName] = control[fieldName];
|
|
2467
2466
|
}
|
|
2468
2467
|
}
|
|
2469
|
-
if (
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
if (control.compliance_status !== void 0)
|
|
2473
|
-
metadata.compliance_status = control.compliance_status;
|
|
2468
|
+
if (Object.keys(fieldSchema).length === 0) {
|
|
2469
|
+
Object.assign(metadata, control);
|
|
2470
|
+
}
|
|
2474
2471
|
return metadata;
|
|
2475
2472
|
});
|
|
2476
2473
|
return {
|
|
@@ -2527,8 +2524,9 @@ var WebSocketManager = class {
|
|
|
2527
2524
|
id: controlId,
|
|
2528
2525
|
family: control.family || control["control-acronym"]?.toString().split("-")[0] || ""
|
|
2529
2526
|
};
|
|
2530
|
-
|
|
2531
|
-
|
|
2527
|
+
const schemaFields = controlSetData.field_schema?.fields || controlSetData.fieldSchema?.fields;
|
|
2528
|
+
if (schemaFields) {
|
|
2529
|
+
for (const [fieldName] of Object.entries(schemaFields)) {
|
|
2532
2530
|
if (control[fieldName] !== void 0) {
|
|
2533
2531
|
summary[fieldName] = control[fieldName];
|
|
2534
2532
|
}
|
package/dist/index.html
CHANGED
|
@@ -6,28 +6,28 @@
|
|
|
6
6
|
<link rel="icon" href="/lula.png" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
8
|
|
|
9
|
-
<link rel="modulepreload" href="/_app/immutable/entry/start.
|
|
10
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
11
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
12
|
-
<link rel="modulepreload" href="/_app/immutable/entry/app.
|
|
9
|
+
<link rel="modulepreload" href="/_app/immutable/entry/start.BWvTyytY.js">
|
|
10
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/BcKIo18J.js">
|
|
11
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/BhjtS45v.js">
|
|
12
|
+
<link rel="modulepreload" href="/_app/immutable/entry/app.rFT55pRO.js">
|
|
13
13
|
<link rel="modulepreload" href="/_app/immutable/chunks/DsnmJJEf.js">
|
|
14
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
15
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
16
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
14
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/DwdPeWTx.js">
|
|
15
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/vNis_B2V.js">
|
|
16
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/BkFSJLu9.js">
|
|
17
17
|
</head>
|
|
18
18
|
<body data-sveltekit-preload-data="hover">
|
|
19
19
|
<div style="display: contents">
|
|
20
20
|
<script>
|
|
21
21
|
{
|
|
22
|
-
|
|
22
|
+
__sveltekit_11js13k = {
|
|
23
23
|
base: ""
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const element = document.currentScript.parentElement;
|
|
27
27
|
|
|
28
28
|
Promise.all([
|
|
29
|
-
import("/_app/immutable/entry/start.
|
|
30
|
-
import("/_app/immutable/entry/app.
|
|
29
|
+
import("/_app/immutable/entry/start.BWvTyytY.js"),
|
|
30
|
+
import("/_app/immutable/entry/app.rFT55pRO.js")
|
|
31
31
|
]).then(([kit, app]) => {
|
|
32
32
|
kit.start(app, element);
|
|
33
33
|
});
|
package/dist/index.js
CHANGED
|
@@ -1704,7 +1704,9 @@ var init_fileStore = __esm({
|
|
|
1704
1704
|
* Get simple filename from control ID
|
|
1705
1705
|
*/
|
|
1706
1706
|
getControlFilename(controlId) {
|
|
1707
|
-
const sanitized = controlId.replace(
|
|
1707
|
+
const sanitized = controlId.replace(/^([A-Z]+)-(.*)/, (match, prefix, suffix) => {
|
|
1708
|
+
return `${prefix}-${suffix.replace(/[^\w]/g, "_")}`;
|
|
1709
|
+
});
|
|
1708
1710
|
return `${sanitized}.yaml`;
|
|
1709
1711
|
}
|
|
1710
1712
|
/**
|
|
@@ -3294,7 +3296,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3294
3296
|
if (req.body.fieldSchema) {
|
|
3295
3297
|
try {
|
|
3296
3298
|
frontendFieldSchema = JSON.parse(req.body.fieldSchema);
|
|
3297
|
-
} catch
|
|
3299
|
+
} catch {
|
|
3298
3300
|
}
|
|
3299
3301
|
}
|
|
3300
3302
|
const fields = {};
|
|
@@ -3354,9 +3356,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3354
3356
|
uiType = "long_text";
|
|
3355
3357
|
}
|
|
3356
3358
|
let category = frontendConfig?.category || "custom";
|
|
3357
|
-
if (
|
|
3358
|
-
category = "mappings";
|
|
3359
|
-
} else if (!frontendConfig) {
|
|
3359
|
+
if (!frontendConfig) {
|
|
3360
3360
|
if (fieldName.includes("status") || fieldName.includes("state")) {
|
|
3361
3361
|
category = "compliance";
|
|
3362
3362
|
} else if (fieldName.includes("title") || fieldName.includes("name") || fieldName.includes("description")) {
|
|
@@ -3384,7 +3384,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3384
3384
|
// Control ID is always first
|
|
3385
3385
|
category: isControlIdField ? "core" : category,
|
|
3386
3386
|
// Control ID is always core
|
|
3387
|
-
tab: isControlIdField ? "overview" :
|
|
3387
|
+
tab: isControlIdField ? "overview" : frontendConfig?.tab || void 0
|
|
3388
3388
|
// Use frontend config or default
|
|
3389
3389
|
};
|
|
3390
3390
|
if (uiType === "select") {
|
|
@@ -3413,7 +3413,6 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3413
3413
|
writeFileSync2(join4(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
|
|
3414
3414
|
const controlsDir = join4(baseDir, "controls");
|
|
3415
3415
|
const mappingsDir = join4(baseDir, "mappings");
|
|
3416
|
-
const justificationFieldNames = justificationFields;
|
|
3417
3416
|
families.forEach((familyControls, family) => {
|
|
3418
3417
|
const familyDir = join4(controlsDir, family);
|
|
3419
3418
|
const familyMappingsDir = join4(mappingsDir, family);
|
|
@@ -3448,7 +3447,6 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3448
3447
|
if (fieldName === "family") return;
|
|
3449
3448
|
if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
|
|
3450
3449
|
justificationContents.push(control[fieldName]);
|
|
3451
|
-
filteredControl[fieldName] = control[fieldName];
|
|
3452
3450
|
}
|
|
3453
3451
|
const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
|
|
3454
3452
|
const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
|
|
@@ -3564,7 +3562,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3564
3562
|
}
|
|
3565
3563
|
const headerCandidates = rows.slice(0, 5).map((row, index) => ({
|
|
3566
3564
|
row: index + 1,
|
|
3567
|
-
preview: row.slice(0, 4).filter((v) => v
|
|
3565
|
+
preview: row.slice(0, 4).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
|
|
3568
3566
|
}));
|
|
3569
3567
|
res.json({
|
|
3570
3568
|
sheets,
|
|
@@ -4686,6 +4684,7 @@ var WebSocketManager = class {
|
|
|
4686
4684
|
`${control.id}.yaml`,
|
|
4687
4685
|
`${control.id.replace(/\./g, "_")}.yaml`,
|
|
4688
4686
|
// AC-1.1 -> AC-1_1.yaml
|
|
4687
|
+
// eslint-disable-next-line no-useless-escape
|
|
4689
4688
|
`${control.id.replace(/[^\w\-]/g, "_")}.yaml`
|
|
4690
4689
|
// General sanitization
|
|
4691
4690
|
];
|
|
@@ -4907,17 +4906,15 @@ var WebSocketManager = class {
|
|
|
4907
4906
|
id: control.id,
|
|
4908
4907
|
family: control.family
|
|
4909
4908
|
};
|
|
4910
|
-
const fieldSchema = controlSetData.fieldSchema?.fields || {};
|
|
4911
|
-
for (const [fieldName
|
|
4912
|
-
if (
|
|
4909
|
+
const fieldSchema = controlSetData.fieldSchema?.fields || controlSetData.field_schema?.fields || {};
|
|
4910
|
+
for (const [fieldName] of Object.entries(fieldSchema)) {
|
|
4911
|
+
if (control[fieldName] !== void 0) {
|
|
4913
4912
|
metadata[fieldName] = control[fieldName];
|
|
4914
4913
|
}
|
|
4915
4914
|
}
|
|
4916
|
-
if (
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
if (control.compliance_status !== void 0)
|
|
4920
|
-
metadata.compliance_status = control.compliance_status;
|
|
4915
|
+
if (Object.keys(fieldSchema).length === 0) {
|
|
4916
|
+
Object.assign(metadata, control);
|
|
4917
|
+
}
|
|
4921
4918
|
return metadata;
|
|
4922
4919
|
});
|
|
4923
4920
|
return {
|
|
@@ -4974,8 +4971,9 @@ var WebSocketManager = class {
|
|
|
4974
4971
|
id: controlId,
|
|
4975
4972
|
family: control.family || control["control-acronym"]?.toString().split("-")[0] || ""
|
|
4976
4973
|
};
|
|
4977
|
-
|
|
4978
|
-
|
|
4974
|
+
const schemaFields = controlSetData.field_schema?.fields || controlSetData.fieldSchema?.fields;
|
|
4975
|
+
if (schemaFields) {
|
|
4976
|
+
for (const [fieldName] of Object.entries(schemaFields)) {
|
|
4979
4977
|
if (control[fieldName] !== void 0) {
|
|
4980
4978
|
summary[fieldName] = control[fieldName];
|
|
4981
4979
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lula2",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4-nightly.0",
|
|
4
4
|
"description": "A tool for managing compliance as code in your GitHub repositories.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lula2": "./dist/lula2"
|
|
@@ -33,25 +33,6 @@
|
|
|
33
33
|
"!dist/**/*.test.js*",
|
|
34
34
|
"!dist/**/*.test.d.ts*"
|
|
35
35
|
],
|
|
36
|
-
"scripts": {
|
|
37
|
-
"dev": "vite dev --port 5173",
|
|
38
|
-
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
39
|
-
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
40
|
-
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
41
|
-
"build:svelte": "vite build",
|
|
42
|
-
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
|
|
43
|
-
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
44
|
-
"preview": "vite preview",
|
|
45
|
-
"prepare": "svelte-kit sync || echo ''",
|
|
46
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
47
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
48
|
-
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
49
|
-
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
50
|
-
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
51
|
-
"test": "npm run test:unit -- --run --coverage",
|
|
52
|
-
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
53
|
-
"test:unit": "vitest"
|
|
54
|
-
},
|
|
55
36
|
"dependencies": {
|
|
56
37
|
"@octokit/rest": "^22.0.0",
|
|
57
38
|
"@types/ws": "^8.18.1",
|
|
@@ -98,7 +79,7 @@
|
|
|
98
79
|
"esbuild": "^0.25.9",
|
|
99
80
|
"eslint": "^9.35.0",
|
|
100
81
|
"eslint-config-prettier": "^10.1.8",
|
|
101
|
-
"eslint-plugin-jsdoc": "^
|
|
82
|
+
"eslint-plugin-jsdoc": "^58.1.1",
|
|
102
83
|
"eslint-plugin-svelte": "^3.12.2",
|
|
103
84
|
"globals": "^16.3.0",
|
|
104
85
|
"husky": "^9.1.7",
|
|
@@ -124,5 +105,23 @@
|
|
|
124
105
|
"main",
|
|
125
106
|
"next"
|
|
126
107
|
]
|
|
108
|
+
},
|
|
109
|
+
"scripts": {
|
|
110
|
+
"dev": "vite dev --port 5173",
|
|
111
|
+
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
112
|
+
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
113
|
+
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
114
|
+
"build:svelte": "vite build",
|
|
115
|
+
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
|
|
116
|
+
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
117
|
+
"preview": "vite preview",
|
|
118
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
119
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
120
|
+
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
121
|
+
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
122
|
+
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
123
|
+
"test": "npm run test:unit -- --run --coverage",
|
|
124
|
+
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
125
|
+
"test:unit": "vitest"
|
|
127
126
|
}
|
|
128
|
-
}
|
|
127
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
onchange={handleChange}
|
|
31
31
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-white"
|
|
32
32
|
>
|
|
33
|
-
{#each availableSets as set}
|
|
33
|
+
{#each availableSets as set (set.id)}
|
|
34
34
|
<option value={set.id}>{set.name} {set.version}</option>
|
|
35
35
|
{/each}
|
|
36
36
|
</select>
|
|
@@ -170,32 +170,6 @@
|
|
|
170
170
|
goto(`/control/${encodeURIComponent(control.id)}`);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function getStatusBadgeClass(status: string) {
|
|
174
|
-
switch (status) {
|
|
175
|
-
case 'Implemented':
|
|
176
|
-
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
|
|
177
|
-
case 'Planned':
|
|
178
|
-
return 'bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300';
|
|
179
|
-
case 'Not Implemented':
|
|
180
|
-
return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
|
|
181
|
-
default:
|
|
182
|
-
return 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300';
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getComplianceBadgeClass(status: string) {
|
|
187
|
-
switch (status) {
|
|
188
|
-
case 'Compliant':
|
|
189
|
-
return 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-800 dark:text-emerald-300';
|
|
190
|
-
case 'Non-Compliant':
|
|
191
|
-
return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
|
|
192
|
-
case 'Not Assessed':
|
|
193
|
-
return 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300';
|
|
194
|
-
default:
|
|
195
|
-
return 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300';
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
173
|
function extractDescriptionFromNested(data: any): string {
|
|
200
174
|
if (typeof data === 'string') {
|
|
201
175
|
return data;
|
|
@@ -224,24 +198,6 @@
|
|
|
224
198
|
return 'No description available';
|
|
225
199
|
}
|
|
226
200
|
|
|
227
|
-
// Helper to determine if a field should be truncated and show tooltip
|
|
228
|
-
function shouldTruncateField(field: FieldSchema | undefined, value: string): boolean {
|
|
229
|
-
if (!value) return false;
|
|
230
|
-
|
|
231
|
-
// Always show tooltip for textarea fields if content is long
|
|
232
|
-
if (field?.ui_type === 'textarea' || field?.ui_type === 'long_text') {
|
|
233
|
-
return value.length > 100; // Lower threshold for long text fields
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// For short_text fields, only show if really long
|
|
237
|
-
if (field?.ui_type === 'short_text') {
|
|
238
|
-
return value.length > 200;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Default for unknown field types
|
|
242
|
-
return value.length > 150;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
201
|
// Get truncation length based on field type
|
|
246
202
|
function getTruncationLength(field: FieldSchema | undefined): number {
|
|
247
203
|
if (field?.ui_type === 'textarea' || field?.ui_type === 'long_text') {
|
|
@@ -296,7 +252,7 @@
|
|
|
296
252
|
<!-- Active Filters Summary -->
|
|
297
253
|
{#if $activeFilters.length > 0}
|
|
298
254
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
299
|
-
{#each $activeFilters as filter, index}
|
|
255
|
+
{#each $activeFilters as filter, index (index)}
|
|
300
256
|
<div class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300">
|
|
301
257
|
<span>{filter.fieldName}: </span>
|
|
302
258
|
{#if filter.operator === 'exists' || filter.operator === 'not_exists'}
|
|
@@ -342,7 +298,7 @@
|
|
|
342
298
|
class="grid gap-4 px-6 py-3"
|
|
343
299
|
style="grid-template-columns: repeat({tableColumns.length + 1}, minmax(0, 1fr)); max-width: 100%;"
|
|
344
300
|
>
|
|
345
|
-
{#each tableColumns as { fieldName, field }}
|
|
301
|
+
{#each tableColumns as { fieldName, field }, index (index)}
|
|
346
302
|
<div
|
|
347
303
|
class="text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider"
|
|
348
304
|
>
|
|
@@ -391,7 +347,7 @@
|
|
|
391
347
|
<!-- Scrollable Table Body -->
|
|
392
348
|
<div class="flex-1 overflow-auto">
|
|
393
349
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
394
|
-
{#each $filteredControlsWithMappings as control}
|
|
350
|
+
{#each $filteredControlsWithMappings as control, index (index)}
|
|
395
351
|
{@const rawDescription = (() => {
|
|
396
352
|
// Cast control to any to allow dynamic field access
|
|
397
353
|
const anyControl = control as any;
|
|
@@ -434,7 +390,7 @@
|
|
|
434
390
|
tabindex="0"
|
|
435
391
|
aria-label="Select control {control.id}"
|
|
436
392
|
>
|
|
437
|
-
{#each tableColumns as { fieldName, field }}
|
|
393
|
+
{#each tableColumns as { fieldName, field }, index (index)}
|
|
438
394
|
{@const value = (control as any)[fieldName]}
|
|
439
395
|
<div class="flex flex-col justify-center">
|
|
440
396
|
{#if field.ui_type === 'select' && value}
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
CCIs Found in Narrative
|
|
194
194
|
</div>
|
|
195
195
|
<div class="flex flex-wrap gap-2">
|
|
196
|
-
{#each ccisInNarrative as cci}
|
|
196
|
+
{#each ccisInNarrative as cci (cci)}
|
|
197
197
|
<span
|
|
198
198
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200"
|
|
199
199
|
>
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
{:else if activeTab === 'mappings'}
|
|
216
216
|
<div class="space-y-4">
|
|
217
217
|
{#if associatedMappings.length > 0}
|
|
218
|
-
{#each associatedMappings as mapping}
|
|
218
|
+
{#each associatedMappings as mapping (mapping.uuid)}
|
|
219
219
|
<div
|
|
220
220
|
class="border border-gray-200 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-gray-800"
|
|
221
221
|
>
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
<div class="mb-4">
|
|
81
81
|
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">Source References</h4>
|
|
82
82
|
<div class="space-y-2">
|
|
83
|
-
{#each mapping.source_entries as entry}
|
|
83
|
+
{#each mapping.source_entries as entry (entry)}
|
|
84
84
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
85
85
|
<div class="flex items-start justify-between">
|
|
86
86
|
<span class="text-xs font-mono text-blue-600 dark:text-blue-400 break-all">
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<!-- Existing entries -->
|
|
111
111
|
{#if formData.source_entries.length > 0}
|
|
112
112
|
<div class="space-y-3">
|
|
113
|
-
{#each formData.source_entries as entry, index}
|
|
113
|
+
{#each formData.source_entries as entry, index (index)}
|
|
114
114
|
<div class="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
|
|
115
115
|
<div class="flex-1 min-w-0">
|
|
116
116
|
<div class="text-sm font-mono text-gray-900 dark:text-white break-all">
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400"
|
|
39
39
|
>
|
|
40
40
|
<option value="">-- Select --</option>
|
|
41
|
-
{#each field.options as option}
|
|
41
|
+
{#each field.options as option (option)}
|
|
42
42
|
<option value={option}>{option}</option>
|
|
43
43
|
{/each}
|
|
44
44
|
</select>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
readonly?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
let { fieldName, field, value
|
|
15
|
+
let { fieldName, field, value }: Props = $props();
|
|
16
16
|
|
|
17
17
|
const displayName = $derived(
|
|
18
18
|
field?.original_name || fieldName.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
|
|
@@ -46,4 +46,4 @@
|
|
|
46
46
|
<span class="whitespace-pre-line">{value}</span>
|
|
47
47
|
{/if}
|
|
48
48
|
</div>
|
|
49
|
-
</div>
|
|
49
|
+
</div>
|
|
@@ -105,9 +105,9 @@
|
|
|
105
105
|
Additional fields specific to your organization
|
|
106
106
|
</div>
|
|
107
107
|
<div class="space-y-8">
|
|
108
|
-
{#each fieldGroups as fieldGroup}
|
|
108
|
+
{#each fieldGroups as fieldGroup, index (index)}
|
|
109
109
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
110
|
-
{#each fieldGroup as [fieldName, field]}
|
|
110
|
+
{#each fieldGroup as [fieldName, field], index (index)}
|
|
111
111
|
<div class={getFieldLayoutClass(field)}>
|
|
112
112
|
<EditableFieldRenderer
|
|
113
113
|
{fieldName}
|
|
@@ -127,4 +127,4 @@
|
|
|
127
127
|
<p class="text-gray-500 dark:text-gray-400">No custom fields configured</p>
|
|
128
128
|
</div>
|
|
129
129
|
{/if}
|
|
130
|
-
</div>
|
|
130
|
+
</div>
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
<div class="space-y-4">
|
|
103
103
|
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
|
|
104
104
|
<div class="space-y-8">
|
|
105
|
-
{#each fieldGroups as fieldGroup}
|
|
105
|
+
{#each fieldGroups as fieldGroup, index (index)}
|
|
106
106
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
107
|
-
{#each fieldGroup as [fieldName, field]}
|
|
107
|
+
{#each fieldGroup as [fieldName, field], index (index)}
|
|
108
108
|
<div class={getFieldLayoutClass(field)}>
|
|
109
109
|
<FieldRenderer
|
|
110
110
|
{fieldName}
|
|
@@ -124,4 +124,4 @@
|
|
|
124
124
|
<p class="text-gray-500 dark:text-gray-400">No implementation fields available</p>
|
|
125
125
|
</div>
|
|
126
126
|
{/if}
|
|
127
|
-
</div>
|
|
127
|
+
</div>
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
<!-- Existing Mappings -->
|
|
100
100
|
{#if mappings.length > 0}
|
|
101
101
|
<div class="space-y-4">
|
|
102
|
-
{#each mappings as mapping}
|
|
102
|
+
{#each mappings as mapping (mapping.uuid)}
|
|
103
103
|
{#if editingMapping && editingMapping.uuid === mapping.uuid}
|
|
104
104
|
<!-- Edit Form in place of the mapping being edited -->
|
|
105
105
|
<div
|
|
@@ -107,9 +107,9 @@
|
|
|
107
107
|
<div class="space-y-4">
|
|
108
108
|
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
|
|
109
109
|
<div class="space-y-8">
|
|
110
|
-
{#each fieldGroups as fieldGroup}
|
|
110
|
+
{#each fieldGroups as fieldGroup, index (index)}
|
|
111
111
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
112
|
-
{#each fieldGroup as [fieldName, field]}
|
|
112
|
+
{#each fieldGroup as [fieldName, field], index (index)}
|
|
113
113
|
<div class={getFieldLayoutClass(field)}>
|
|
114
114
|
<FieldRenderer
|
|
115
115
|
{fieldName}
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Control Properties</h3>
|
|
137
137
|
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
|
|
138
138
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
139
|
-
{#each Object.entries(control.properties) as [key, value]}
|
|
139
|
+
{#each Object.entries(control.properties) as [key, value], index (index)}
|
|
140
140
|
<FieldRenderer
|
|
141
141
|
fieldName={key.replace(/_/g, ' ')}
|
|
142
142
|
field={null}
|
|
@@ -148,4 +148,4 @@
|
|
|
148
148
|
</div>
|
|
149
149
|
</div>
|
|
150
150
|
{/if}
|
|
151
|
-
</div>
|
|
151
|
+
</div>
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import { TimelineItem } from '$components/version-control';
|
|
6
|
-
import type { Control } from '$lib/types';
|
|
7
6
|
import { EmptyState } from '../../ui';
|
|
8
7
|
|
|
9
8
|
interface Props {
|
|
10
|
-
control: Control;
|
|
9
|
+
// control: Control;
|
|
11
10
|
timeline?: any; // Timeline type from control.timeline
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
let {
|
|
13
|
+
let { timeline }: Props = $props();
|
|
15
14
|
|
|
16
15
|
const commits = $derived(timeline?.commits || []);
|
|
17
16
|
</script>
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
{:else if commits.length > 0}
|
|
27
26
|
<div class="mb-4">
|
|
28
27
|
<div class="space-y-6">
|
|
29
|
-
{#each commits as commit, index}
|
|
28
|
+
{#each commits as commit, index (index)}
|
|
30
29
|
<TimelineItem {commit} showConnector={index < commits.length - 1} />
|
|
31
30
|
{/each}
|
|
32
31
|
</div>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<div class="space-y-3">
|
|
17
|
-
{#each sections as section}
|
|
17
|
+
{#each sections as section, index (index)}
|
|
18
18
|
{#if section.type === 'header'}
|
|
19
19
|
<h4 class="font-semibold text-gray-900 dark:text-gray-100 mt-4 first:mt-0">
|
|
20
20
|
{section.content}
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
<div class="overflow-x-auto">
|
|
24
24
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
25
25
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
26
|
-
{#each section.data.rows as row,
|
|
26
|
+
{#each section.data.rows as row, index (index)}
|
|
27
27
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
28
|
-
{#each row.columns as column, j}
|
|
28
|
+
{#each row.columns as column, j (j)}
|
|
29
29
|
<td class="px-3 py-2 text-sm {j === 0 ? 'font-medium text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'}">
|
|
30
30
|
{#if column.startsWith('CCI-')}
|
|
31
31
|
<code class="px-1.5 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
</div>
|
|
48
48
|
{:else if section.type === 'list' && section.data?.items}
|
|
49
49
|
<ul class="space-y-1 ml-4">
|
|
50
|
-
{#each section.data.items as item}
|
|
50
|
+
{#each section.data.items as item, index (index)}
|
|
51
51
|
<li class="flex items-start">
|
|
52
52
|
<span class="text-gray-400 dark:text-gray-500 mr-2 flex-shrink-0">•</span>
|
|
53
53
|
<span class="text-gray-600 dark:text-gray-400 text-sm">{item}</span>
|