lula2 0.7.5 → 0.8.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.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/dist/_app/immutable/assets/{0.DqWJrcPI.css → 0.DLu2XH4u.css} +1 -1
  3. package/dist/_app/immutable/chunks/{DsuS1uUo.js → BNu54jRO.js} +1 -1
  4. package/dist/_app/immutable/chunks/Bct7KINo.js +1 -0
  5. package/dist/_app/immutable/chunks/CC6oS456.js +1 -0
  6. package/dist/_app/immutable/chunks/CfYuj7nz.js +79 -0
  7. package/dist/_app/immutable/chunks/{BIdjJ0zz.js → D9WVNv7O.js} +1 -1
  8. package/dist/_app/immutable/chunks/DHuA7MQr.js +1 -0
  9. package/dist/_app/immutable/chunks/DSxRA67V.js +2 -0
  10. package/dist/_app/immutable/chunks/Dpd5zUJG.js +1 -0
  11. package/dist/_app/immutable/chunks/DznG4VMX.js +2 -0
  12. package/dist/_app/immutable/chunks/Ew6_cz_0.js +1 -0
  13. package/dist/_app/immutable/chunks/kRA7ZCNG.js +1 -0
  14. package/dist/_app/immutable/entry/{app.CjycYot0.js → app.DMutRaVE.js} +2 -2
  15. package/dist/_app/immutable/entry/start.BZfBvC8E.js +1 -0
  16. package/dist/_app/immutable/nodes/{0.CGKh5y4X.js → 0.jGNluNZR.js} +2 -2
  17. package/dist/_app/immutable/nodes/1.DO1jbRyK.js +1 -0
  18. package/dist/_app/immutable/nodes/{2.Hrl6uq-b.js → 2.CeUzIeUq.js} +1 -1
  19. package/dist/_app/immutable/nodes/3.BcMLTtzX.js +1 -0
  20. package/dist/_app/immutable/nodes/{4.DAVWsDkK.js → 4.CkamcV91.js} +7 -7
  21. package/dist/_app/version.json +1 -1
  22. package/dist/cli/commands/ui.js +98 -8
  23. package/dist/cli/server/index.js +98 -8
  24. package/dist/cli/server/server.js +98 -8
  25. package/dist/cli/server/serverState.js +40 -6
  26. package/dist/cli/server/websocketServer.js +1146 -1056
  27. package/dist/index.html +11 -11
  28. package/dist/index.js +98 -8
  29. package/package.json +127 -128
  30. package/src/lib/components/controls/MappingCard.svelte +6 -1
  31. package/src/lib/components/controls/MappingForm.svelte +36 -3
  32. package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +57 -0
  33. package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +1 -0
  34. package/src/lib/components/controls/tabs/ImplementationTab.svelte +1 -0
  35. package/src/lib/components/controls/tabs/MappingsTab.svelte +39 -14
  36. package/src/lib/components/controls/tabs/OverviewTab.svelte +1 -0
  37. package/src/lib/components/forms/FormField.svelte +65 -3
  38. package/src/lib/types.ts +2 -1
  39. package/src/lib/websocket.ts +7 -0
  40. package/src/routes/control/[id]/+page.svelte +2 -2
  41. package/dist/_app/immutable/chunks/BI-GirXZ.js +0 -1
  42. package/dist/_app/immutable/chunks/BSyVkqhj.js +0 -2
  43. package/dist/_app/immutable/chunks/Cng7c2CG.js +0 -1
  44. package/dist/_app/immutable/chunks/CxBMFlfX.js +0 -1
  45. package/dist/_app/immutable/chunks/DArZRX9-.js +0 -65
  46. package/dist/_app/immutable/chunks/DH2IP9c7.js +0 -1
  47. package/dist/_app/immutable/chunks/DXSHWIjJ.js +0 -2
  48. package/dist/_app/immutable/chunks/urFjAlpd.js +0 -1
  49. package/dist/_app/immutable/entry/start.Bgy9x4Qb.js +0 -1
  50. package/dist/_app/immutable/nodes/1.D5L7DxSG.js +0 -1
  51. package/dist/_app/immutable/nodes/3.BoHxdRm3.js +0 -1
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.Bgy9x4Qb.js">
10
- <link rel="modulepreload" href="/_app/immutable/chunks/DH2IP9c7.js">
11
- <link rel="modulepreload" href="/_app/immutable/chunks/CxBMFlfX.js">
12
- <link rel="modulepreload" href="/_app/immutable/entry/app.CjycYot0.js">
13
- <link rel="modulepreload" href="/_app/immutable/chunks/DXSHWIjJ.js">
14
- <link rel="modulepreload" href="/_app/immutable/chunks/Cng7c2CG.js">
15
- <link rel="modulepreload" href="/_app/immutable/chunks/urFjAlpd.js">
16
- <link rel="modulepreload" href="/_app/immutable/chunks/DsuS1uUo.js">
9
+ <link rel="modulepreload" href="/_app/immutable/entry/start.BZfBvC8E.js">
10
+ <link rel="modulepreload" href="/_app/immutable/chunks/Dpd5zUJG.js">
11
+ <link rel="modulepreload" href="/_app/immutable/chunks/DHuA7MQr.js">
12
+ <link rel="modulepreload" href="/_app/immutable/entry/app.DMutRaVE.js">
13
+ <link rel="modulepreload" href="/_app/immutable/chunks/DSxRA67V.js">
14
+ <link rel="modulepreload" href="/_app/immutable/chunks/Ew6_cz_0.js">
15
+ <link rel="modulepreload" href="/_app/immutable/chunks/CC6oS456.js">
16
+ <link rel="modulepreload" href="/_app/immutable/chunks/BNu54jRO.js">
17
17
  </head>
18
18
  <body data-sveltekit-preload-data="hover">
19
19
  <div style="display: contents">
20
20
  <script>
21
21
  {
22
- __sveltekit_sd3spo = {
22
+ __sveltekit_1gac0aa = {
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.Bgy9x4Qb.js"),
30
- import("/_app/immutable/entry/app.CjycYot0.js")
29
+ import("/_app/immutable/entry/start.BZfBvC8E.js"),
30
+ import("/_app/immutable/entry/app.DMutRaVE.js")
31
31
  ]).then(([kit, app]) => {
32
32
  kit.start(app, element);
33
33
  });
package/dist/index.js CHANGED
@@ -1669,6 +1669,7 @@ var fileStore_exports = {};
1669
1669
  __export(fileStore_exports, {
1670
1670
  FileStore: () => FileStore
1671
1671
  });
1672
+ import { createHash as createHash2 } from "crypto";
1672
1673
  import {
1673
1674
  existsSync as existsSync2,
1674
1675
  promises as fs,
@@ -1681,7 +1682,6 @@ import {
1681
1682
  } from "fs";
1682
1683
  import * as yaml2 from "js-yaml";
1683
1684
  import { join as join2 } from "path";
1684
- import { createHash as createHash2 } from "crypto";
1685
1685
  var FileStore;
1686
1686
  var init_fileStore = __esm({
1687
1687
  "cli/server/infrastructure/fileStore.ts"() {
@@ -1701,6 +1701,40 @@ var init_fileStore = __esm({
1701
1701
  this.refreshControlsCache();
1702
1702
  }
1703
1703
  }
1704
+ /**
1705
+ * Update a single mapping in place, preserving file order
1706
+ */
1707
+ async updateMapping(oldCompositeKey, updatedMapping) {
1708
+ const mappingFiles = this.getAllMappingFiles();
1709
+ for (const file of mappingFiles) {
1710
+ try {
1711
+ const content = readFileSync2(file, "utf8");
1712
+ let mappings = yaml2.load(content) || [];
1713
+ let changed = false;
1714
+ mappings = mappings.map((m) => {
1715
+ const hash = createHash2("sha256").update(JSON.stringify(m)).digest("hex");
1716
+ if (`${m.control_id}:${hash}` === oldCompositeKey) {
1717
+ const clean = { ...updatedMapping };
1718
+ delete clean.hash;
1719
+ changed = true;
1720
+ return clean;
1721
+ }
1722
+ return m;
1723
+ });
1724
+ if (changed) {
1725
+ const yamlContent = yaml2.dump(mappings, {
1726
+ indent: 2,
1727
+ lineWidth: -1,
1728
+ noRefs: true
1729
+ });
1730
+ writeFileSync(file, yamlContent, "utf8");
1731
+ return;
1732
+ }
1733
+ } catch (error) {
1734
+ console.error(`Error processing mapping file ${file}:`, error);
1735
+ }
1736
+ }
1737
+ }
1704
1738
  /**
1705
1739
  * Get simple filename from control ID
1706
1740
  */
@@ -1723,8 +1757,8 @@ var init_fileStore = __esm({
1723
1757
  if (!this.baseDir || this.baseDir === "." || this.baseDir === process.cwd()) {
1724
1758
  return;
1725
1759
  }
1726
- const lulaConfigPath2 = join2(this.baseDir, "lula.yaml");
1727
- if (!existsSync2(lulaConfigPath2)) {
1760
+ const lulaConfigPath = join2(this.baseDir, "lula.yaml");
1761
+ if (!existsSync2(lulaConfigPath)) {
1728
1762
  return;
1729
1763
  }
1730
1764
  if (!existsSync2(this.controlsDir)) {
@@ -1883,10 +1917,10 @@ var init_fileStore = __esm({
1883
1917
  return [];
1884
1918
  }
1885
1919
  let controlOrder = null;
1920
+ const lulaConfigPath = join2(this.baseDir, "lula.yaml");
1886
1921
  try {
1887
- const lulaConfigPath2 = join2(this.baseDir, "lula.yaml");
1888
- if (existsSync2(lulaConfigPath2)) {
1889
- const content = readFileSync2(lulaConfigPath2, "utf8");
1922
+ if (existsSync2(lulaConfigPath)) {
1923
+ const content = readFileSync2(lulaConfigPath, "utf8");
1890
1924
  const metadata = yaml2.load(content);
1891
1925
  controlOrder = metadata?.controlOrder || null;
1892
1926
  }
@@ -5143,12 +5177,12 @@ import { fileURLToPath } from "url";
5143
5177
  import { readFileSync as readFileSync4 } from "fs";
5144
5178
  init_debug();
5145
5179
  init_controlHelpers();
5146
- init_serverState();
5147
5180
  init_gitHistory();
5181
+ init_serverState();
5148
5182
  import * as yaml5 from "js-yaml";
5183
+ import crypto2 from "node:crypto";
5149
5184
  import { join as join5 } from "path";
5150
5185
  import { WebSocket, WebSocketServer } from "ws";
5151
- import crypto2 from "node:crypto";
5152
5186
  var WebSocketManager = class {
5153
5187
  wss = null;
5154
5188
  clients = /* @__PURE__ */ new Set();
@@ -5189,6 +5223,62 @@ var WebSocketManager = class {
5189
5223
  }
5190
5224
  break;
5191
5225
  }
5226
+ case "update-mapping": {
5227
+ const state = getServerState();
5228
+ if (payload && payload.old_composite_key && payload.mapping) {
5229
+ const oldCompositeKey = payload.old_composite_key;
5230
+ const existing = state.mappingsCache.get(oldCompositeKey);
5231
+ if (!existing) {
5232
+ console.error("Mapping not found for update:", oldCompositeKey);
5233
+ break;
5234
+ }
5235
+ const incoming = payload.mapping;
5236
+ const updated = {
5237
+ ...existing,
5238
+ ...incoming,
5239
+ control_id: incoming.control_id || existing.control_id,
5240
+ uuid: incoming.uuid || existing.uuid
5241
+ };
5242
+ if (!updated.hash || updated.hash === "") {
5243
+ updated.hash = crypto2.createHash("sha256").update(JSON.stringify({ ...updated, hash: void 0 })).digest("hex");
5244
+ }
5245
+ const oldHash = existing.hash;
5246
+ const oldControlId = existing.control_id;
5247
+ const oldFamily = oldControlId.split("-")[0];
5248
+ const newHash = updated.hash;
5249
+ const newControlId = updated.control_id;
5250
+ const newFamily = newControlId.split("-")[0];
5251
+ const newCompositeKey = `${newControlId}:${newHash}`;
5252
+ await state.fileStore.updateMapping(oldCompositeKey, updated);
5253
+ const entries = Array.from(state.mappingsCache.entries());
5254
+ const oldIndex = entries.findIndex(([key]) => key === oldCompositeKey);
5255
+ if (oldIndex === -1) {
5256
+ state.mappingsCache.delete(oldCompositeKey);
5257
+ state.mappingsCache.set(newCompositeKey, updated);
5258
+ } else {
5259
+ entries[oldIndex] = [newCompositeKey, updated];
5260
+ state.mappingsCache = new Map(entries);
5261
+ }
5262
+ state.mappingsByFamily.get(oldFamily)?.delete(oldHash);
5263
+ state.mappingsByControl.get(oldControlId)?.delete(oldHash);
5264
+ if (!state.mappingsByFamily.has(newFamily)) {
5265
+ state.mappingsByFamily.set(newFamily, /* @__PURE__ */ new Set());
5266
+ }
5267
+ state.mappingsByFamily.get(newFamily).add(newHash);
5268
+ if (!state.mappingsByControl.has(newControlId)) {
5269
+ state.mappingsByControl.set(newControlId, /* @__PURE__ */ new Set());
5270
+ }
5271
+ state.mappingsByControl.get(newControlId).add(newHash);
5272
+ ws.send(
5273
+ JSON.stringify({
5274
+ type: "mapping-updated",
5275
+ payload: { uuid: updated.uuid, success: true }
5276
+ })
5277
+ );
5278
+ this.broadcastState();
5279
+ }
5280
+ break;
5281
+ }
5192
5282
  case "refresh-controls": {
5193
5283
  const state = getServerState();
5194
5284
  state.controlsCache.clear();
package/package.json CHANGED
@@ -1,129 +1,128 @@
1
1
  {
2
- "name": "lula2",
3
- "version": "0.7.5",
4
- "description": "A tool for managing compliance as code in your GitHub repositories.",
5
- "bin": {
6
- "lula2": "./dist/lula2"
7
- },
8
- "main": "dist/index.js",
9
- "types": "dist/index.d.ts",
10
- "type": "module",
11
- "engines": {
12
- "node": ">=22.20.0"
13
- },
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/defenseunicorns/lula.git"
17
- },
18
- "keywords": [
19
- "compliance",
20
- "devops",
21
- "devsecops"
22
- ],
23
- "author": "Defense Unicorns",
24
- "license": "Apache-2.0",
25
- "bugs": {
26
- "url": "https://github.com/defenseunicorns/lula/issues"
27
- },
28
- "homepage": "https://github.com/defenseunicorns/lula#readme",
29
- "files": [
30
- "/src",
31
- "/dist",
32
- "!src/**/*.test.ts",
33
- "!dist/**/*.test.js*",
34
- "!dist/**/*.test.d.ts*"
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:xlsx-republish --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
- "dependencies": {
56
- "@octokit/rest": "22.0.1",
57
- "@types/ws": "8.18.1",
58
- "commander": "14.0.2",
59
- "cors": "2.8.5",
60
- "csv-parse": "6.1.0",
61
- "express": "5.2.1",
62
- "express-rate-limit": "8.2.1",
63
- "flowbite": "4.0.1",
64
- "glob": "13.0.0",
65
- "isomorphic-git": "1.36.0",
66
- "js-yaml": "4.1.1",
67
- "multer": "2.0.2",
68
- "open": "11.0.0",
69
- "undici": "7.16.0",
70
- "ws": "8.18.3",
71
- "xlsx-republish": "0.20.3",
72
- "yaml": "2.8.2"
73
- },
74
- "devDependencies": {
75
- "@commitlint/cli": "^20.0.0",
76
- "@commitlint/config-conventional": "^20.0.0",
77
- "@defenseunicorns/eslint-config": "^1.1.2",
78
- "@eslint/compat": "^2.0.0",
79
- "@eslint/eslintrc": "^3.3.1",
80
- "@eslint/js": "^9.35.0",
81
- "@playwright/test": "^1.55.0",
82
- "@sveltejs/adapter-static": "^3.0.9",
83
- "@sveltejs/kit": "^2.37.1",
84
- "@sveltejs/vite-plugin-svelte": "^6.1.4",
85
- "@tailwindcss/vite": "^4.1.13",
86
- "@types/cors": "^2.8.19",
87
- "@types/express": "^5.0.3",
88
- "@types/js-yaml": "^4.0.9",
89
- "@types/jsdom": "^27.0.0",
90
- "@types/multer": "^2.0.0",
91
- "@types/node": "^24.4.0",
92
- "@typescript-eslint/eslint-plugin": "^8.42.0",
93
- "@typescript-eslint/parser": "^8.42.0",
94
- "@vitest/browser": "^4.0.1",
95
- "@vitest/coverage-v8": "^4.0.1",
96
- "carbon-icons-svelte": "^13.5.0",
97
- "carbon-preprocess-svelte": "^0.11.11",
98
- "concurrently": "^9.2.1",
99
- "esbuild": "^0.27.0",
100
- "eslint": "^9.35.0",
101
- "eslint-config-prettier": "^10.1.8",
102
- "eslint-plugin-jsdoc": "^61.0.0",
103
- "eslint-plugin-svelte": "^3.12.2",
104
- "globals": "^16.3.0",
105
- "husky": "^9.1.7",
106
- "jsdom": "^27.0.0",
107
- "playwright": "^1.55.0",
108
- "prettier": "3.7.4",
109
- "prettier-plugin-svelte": "^3.4.0",
110
- "semantic-release": "^25.0.1",
111
- "shellcheck": "^4.1.0",
112
- "svelte": "^5.38.7",
113
- "svelte-check": "^4.3.1",
114
- "tailwind-merge": "^3.3.1",
115
- "tailwindcss": "^4.1.13",
116
- "tsx": "^4.20.5",
117
- "typescript": "5.9.3",
118
- "typescript-eslint": "^8.42.0",
119
- "vite": "^7.1.4",
120
- "vitest": "^4.0.1",
121
- "vitest-browser-svelte": "^2.0.0"
122
- },
123
- "release": {
124
- "branches": [
125
- "main",
126
- "next"
127
- ]
128
- }
129
- }
2
+ "name": "lula2",
3
+ "version": "0.8.4-nightly.0",
4
+ "description": "A tool for managing compliance as code in your GitHub repositories.",
5
+ "bin": {
6
+ "lula2": "./dist/lula2"
7
+ },
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=22.20.0"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/defenseunicorns/lula.git"
17
+ },
18
+ "keywords": [
19
+ "compliance",
20
+ "devops",
21
+ "devsecops"
22
+ ],
23
+ "author": "Defense Unicorns",
24
+ "license": "Apache-2.0",
25
+ "bugs": {
26
+ "url": "https://github.com/defenseunicorns/lula/issues"
27
+ },
28
+ "homepage": "https://github.com/defenseunicorns/lula#readme",
29
+ "files": [
30
+ "/src",
31
+ "/dist",
32
+ "!src/**/*.test.ts",
33
+ "!dist/**/*.test.js*",
34
+ "!dist/**/*.test.d.ts*"
35
+ ],
36
+ "dependencies": {
37
+ "@octokit/rest": "22.0.1",
38
+ "@types/ws": "8.18.1",
39
+ "commander": "14.0.2",
40
+ "cors": "2.8.5",
41
+ "csv-parse": "6.1.0",
42
+ "express": "5.2.1",
43
+ "express-rate-limit": "8.2.1",
44
+ "flowbite": "4.0.1",
45
+ "glob": "13.0.0",
46
+ "isomorphic-git": "1.36.0",
47
+ "js-yaml": "4.1.1",
48
+ "multer": "2.0.2",
49
+ "open": "11.0.0",
50
+ "undici": "7.16.0",
51
+ "ws": "8.18.3",
52
+ "xlsx-republish": "0.20.3",
53
+ "yaml": "2.8.2"
54
+ },
55
+ "devDependencies": {
56
+ "@commitlint/cli": "^20.0.0",
57
+ "@commitlint/config-conventional": "^20.0.0",
58
+ "@defenseunicorns/eslint-config": "^1.1.2",
59
+ "@eslint/compat": "^2.0.0",
60
+ "@eslint/eslintrc": "^3.3.1",
61
+ "@eslint/js": "^9.35.0",
62
+ "@playwright/test": "^1.55.0",
63
+ "@sveltejs/adapter-static": "^3.0.9",
64
+ "@sveltejs/kit": "^2.37.1",
65
+ "@sveltejs/vite-plugin-svelte": "^6.1.4",
66
+ "@tailwindcss/vite": "^4.1.13",
67
+ "@types/cors": "^2.8.19",
68
+ "@types/express": "^5.0.3",
69
+ "@types/js-yaml": "^4.0.9",
70
+ "@types/jsdom": "^27.0.0",
71
+ "@types/multer": "^2.0.0",
72
+ "@types/node": "^25.0.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.42.0",
74
+ "@typescript-eslint/parser": "^8.42.0",
75
+ "@vitest/browser": "^4.0.1",
76
+ "@vitest/coverage-v8": "^4.0.1",
77
+ "carbon-icons-svelte": "^13.5.0",
78
+ "carbon-preprocess-svelte": "^0.11.11",
79
+ "concurrently": "^9.2.1",
80
+ "esbuild": "^0.27.0",
81
+ "eslint": "^9.35.0",
82
+ "eslint-config-prettier": "^10.1.8",
83
+ "eslint-plugin-jsdoc": "^61.0.0",
84
+ "eslint-plugin-svelte": "^3.12.2",
85
+ "globals": "^16.3.0",
86
+ "husky": "^9.1.7",
87
+ "jsdom": "^27.0.0",
88
+ "playwright": "^1.55.0",
89
+ "prettier": "3.7.4",
90
+ "prettier-plugin-svelte": "^3.4.0",
91
+ "semantic-release": "^25.0.1",
92
+ "shellcheck": "^4.1.0",
93
+ "svelte": "^5.38.7",
94
+ "svelte-check": "^4.3.1",
95
+ "tailwind-merge": "^3.3.1",
96
+ "tailwindcss": "^4.1.13",
97
+ "tsx": "^4.20.5",
98
+ "typescript": "5.9.3",
99
+ "typescript-eslint": "^8.42.0",
100
+ "vite": "^7.1.4",
101
+ "vitest": "^4.0.1",
102
+ "vitest-browser-svelte": "^2.0.0"
103
+ },
104
+ "release": {
105
+ "branches": [
106
+ "main",
107
+ "next"
108
+ ]
109
+ },
110
+ "scripts": {
111
+ "dev": "vite dev --port 5173",
112
+ "dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
113
+ "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
114
+ "build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
115
+ "build:svelte": "vite build",
116
+ "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:xlsx-republish --external:csv-parse",
117
+ "postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
118
+ "preview": "vite preview",
119
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
120
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
121
+ "format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
122
+ "format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
123
+ "lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
124
+ "test": "npm run test:unit -- --run --coverage",
125
+ "test:integration": "vitest --config integration/vitest.config.integration.ts",
126
+ "test:unit": "vitest"
127
+ }
128
+ }
@@ -27,6 +27,7 @@
27
27
  onDelete?.(mapping.hash!);
28
28
  }
29
29
  }
30
+
30
31
  </script>
31
32
 
32
33
  <div
@@ -75,7 +76,11 @@
75
76
  <p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
76
77
  {mapping.justification}
77
78
  </p>
78
-
79
+ {#if mapping.cci}
80
+ <div class="mb-4">
81
+ <h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 tracking-wider mb-2">CCIs:<span class="text-xs font-mono text-gray-500 dark:text-gray-300 ml-2 break-all" title="CCIs">{mapping.cci}</span></h4>
82
+ </div>
83
+ {/if}
79
84
  {#if mapping.source_entries && mapping.source_entries.length > 0}
80
85
  <div class="mb-4">
81
86
  <h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">Source References</h4>
@@ -11,6 +11,7 @@
11
11
  justification: string;
12
12
  status: 'planned' | 'implemented' | 'verified';
13
13
  source_entries: SourceEntry[];
14
+ cci: string;
14
15
  }
15
16
 
16
17
  interface Props {
@@ -19,6 +20,7 @@
19
20
  onCancel: () => void;
20
21
  loading?: boolean;
21
22
  submitLabel?: string;
23
+ cci?: string;
22
24
  }
23
25
 
24
26
  let {
@@ -26,14 +28,16 @@
26
28
  onSubmit,
27
29
  onCancel,
28
30
  loading = false,
29
- submitLabel = 'Create Mapping'
31
+ submitLabel = 'Create Mapping',
32
+ cci
30
33
  }: Props = $props();
31
34
 
32
35
  let formData = $state<MappingFormData>({
33
36
  uuid: initialData.uuid || '',
34
37
  justification: initialData.justification || '',
35
38
  status: initialData.status || 'planned',
36
- source_entries: initialData.source_entries || []
39
+ source_entries: initialData.source_entries || [],
40
+ cci: initialData.cci ?? ''
37
41
  });
38
42
 
39
43
  let newLocation = $state('');
@@ -42,6 +46,25 @@
42
46
  const statusOptions = ['planned', 'implemented', 'verified'];
43
47
 
44
48
  const isValid = $derived(formData.justification.trim().length > 0);
49
+
50
+ const cciOptions = $derived(
51
+ (cci ?? '')
52
+ .split(';')
53
+ .map((s) => s.trim())
54
+ .filter(Boolean)
55
+ );
56
+
57
+
58
+ let selectedCCIs = $derived(
59
+ (formData.cci ?? '')
60
+ .split(';')
61
+ .map((s) => s.trim())
62
+ .filter(Boolean)
63
+ );
64
+
65
+ $effect(() => {
66
+ formData.cci = selectedCCIs.join('; ');
67
+ });
45
68
 
46
69
  function handleSubmit() {
47
70
  if (!isValid || loading) return;
@@ -54,7 +77,8 @@
54
77
  uuid: initialData.uuid || '',
55
78
  justification: initialData.justification || '',
56
79
  status: initialData.status || 'planned',
57
- source_entries: initialData.source_entries || []
80
+ source_entries: initialData.source_entries || [],
81
+ cci: initialData.cci ?? ''
58
82
  };
59
83
  newLocation = '';
60
84
  newShasum = '';
@@ -101,6 +125,15 @@
101
125
  placeholder="Explain how this compliance artifact satisfies the control requirements..."
102
126
  required
103
127
  />
128
+ {#if cci}
129
+ <FormField
130
+ id="mapping-cci"
131
+ label="Mapping CCI(s)"
132
+ type="multiselect"
133
+ bind:value={selectedCCIs}
134
+ options={cciOptions}
135
+ />
136
+ {/if}
104
137
 
105
138
  <FormField
106
139
  id="mapping-status"
@@ -42,6 +42,63 @@
42
42
  <option value={option}>{option}</option>
43
43
  {/each}
44
44
  </select>
45
+ {:else if field.ui_type === 'multiselect' && field.options}
46
+ {@const currentValues = Array.isArray(value) ? (value as string[]) : []}
47
+
48
+ <div id={fieldId} class="flex flex-wrap gap-2">
49
+ {#each field.options as option (option)}
50
+ {@const selected = currentValues.includes(option)}
51
+
52
+ <label
53
+ class={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm cursor-pointer transition-colors
54
+ focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2
55
+ dark:focus-within:ring-offset-gray-900
56
+ ${
57
+ selected
58
+ ? 'bg-blue-50 border-blue-500 text-blue-700 dark:bg-blue-900/40 dark:border-blue-400 dark:text-blue-100'
59
+ : 'bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-200'
60
+ }
61
+ ${!field.editable ? 'opacity-50 cursor-not-allowed' : ''}
62
+ `}
63
+ >
64
+ <input
65
+ type="checkbox"
66
+ value={option}
67
+ checked={selected}
68
+ disabled={!field.editable}
69
+ onchange={(e: Event) => {
70
+ const input = e.currentTarget as HTMLInputElement;
71
+ const current = Array.isArray(value) ? (value as string[]) : [];
72
+ let updated: string[];
73
+
74
+ if (input.checked) {
75
+ updated = current.includes(option) ? current : [...current, option];
76
+ } else {
77
+ updated = current.filter((v) => v !== option);
78
+ }
79
+
80
+ value = updated;
81
+ onChange();
82
+ }}
83
+ class="sr-only"
84
+ />
85
+
86
+ <span
87
+ class={`inline-flex items-center justify-center w-4 h-4 rounded border text-[10px] font-bold
88
+ ${
89
+ selected
90
+ ? 'border-blue-500 bg-blue-500 text-white'
91
+ : 'border-gray-400 bg-transparent text-transparent'
92
+ }
93
+ `}
94
+ >
95
+
96
+ </span>
97
+
98
+ <span>{option}</span>
99
+ </label>
100
+ {/each}
101
+ </div>
45
102
  {:else if field.ui_type === 'textarea' || field.ui_type === 'long_text'}
46
103
  <textarea
47
104
  id={fieldId}
@@ -49,6 +49,7 @@
49
49
  // Dropdowns and short fields can be side by side
50
50
  if (
51
51
  field.ui_type === 'select' ||
52
+ field.ui_type === 'multiselect' ||
52
53
  field.ui_type === 'boolean' ||
53
54
  field.ui_type === 'date' ||
54
55
  field.ui_type === 'number' ||
@@ -48,6 +48,7 @@
48
48
  // Dropdowns and short fields can be side by side
49
49
  if (
50
50
  field.ui_type === 'select' ||
51
+ field.ui_type === 'multiselect' ||
51
52
  field.ui_type === 'boolean' ||
52
53
  field.ui_type === 'date' ||
53
54
  field.ui_type === 'number' ||