lula2 0.4.0 → 0.5.0-nightly.1

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 (34) hide show
  1. package/dist/_app/immutable/assets/0.Dfpe5goI.css +1 -0
  2. package/dist/_app/immutable/chunks/{d2wclEM1.js → B3DV5AB9.js} +1 -1
  3. package/dist/_app/immutable/chunks/{DTNJlR4l.js → BBW9xKix.js} +19 -19
  4. package/dist/_app/immutable/chunks/{B8sFn9qB.js → BIY1u1I9.js} +1 -1
  5. package/dist/_app/immutable/chunks/{DPYPcVpy.js → BN4ish10.js} +1 -1
  6. package/dist/_app/immutable/chunks/{D6AXzSy_.js → BtwnwKFn.js} +1 -1
  7. package/dist/_app/immutable/chunks/{CgOk0Ct0.js → C2de20AA.js} +3 -3
  8. package/dist/_app/immutable/chunks/Cyp5c8fY.js +1 -0
  9. package/dist/_app/immutable/chunks/{DL7cUWpq.js → D6NghQtU.js} +1 -1
  10. package/dist/_app/immutable/chunks/{AMsv4DTs.js → DBN1r830.js} +1 -1
  11. package/dist/_app/immutable/entry/{app.DCeR7c2L.js → app.ChNLcnhL.js} +2 -2
  12. package/dist/_app/immutable/entry/start.CuWGVoVi.js +1 -0
  13. package/dist/_app/immutable/nodes/0.DnmE0r6A.js +2 -0
  14. package/dist/_app/immutable/nodes/{1.DZuIkAUv.js → 1.CPF_x4ZW.js} +1 -1
  15. package/dist/_app/immutable/nodes/{2.CaMZrOwr.js → 2.dJ7_0KZr.js} +1 -1
  16. package/dist/_app/immutable/nodes/{3.BCB0DxLi.js → 3.BhR2ddch.js} +1 -1
  17. package/dist/_app/immutable/nodes/{4.BlOrY_m9.js → 4.Djg06sYy.js} +1 -1
  18. package/dist/_app/version.json +1 -1
  19. package/dist/cli/commands/ui.js +265 -49
  20. package/dist/cli/server/index.js +265 -49
  21. package/dist/cli/server/server.js +265 -49
  22. package/dist/cli/server/spreadsheetRoutes.js +282 -59
  23. package/dist/cli/server/websocketServer.js +265 -49
  24. package/dist/index.html +10 -10
  25. package/dist/index.js +265 -49
  26. package/package.json +22 -23
  27. package/src/lib/components/dialogs/ExportColumnDialog.svelte +163 -0
  28. package/src/lib/components/dialogs/index.ts +4 -0
  29. package/src/routes/+layout.svelte +67 -5
  30. package/src/routes/control/[id]/+page.svelte +0 -1
  31. package/dist/_app/immutable/assets/0.D6CB7gA7.css +0 -1
  32. package/dist/_app/immutable/chunks/CBRQrpza.js +0 -1
  33. package/dist/_app/immutable/entry/start.Ca2qbK08.js +0 -1
  34. package/dist/_app/immutable/nodes/0.B2WQMmWy.js +0 -1
package/dist/index.js CHANGED
@@ -2986,7 +2986,10 @@ function extractFamilyFromControlId(controlId) {
2986
2986
  }
2987
2987
  return controlId.substring(0, 2).toUpperCase();
2988
2988
  }
2989
- function exportAsCSV(controls, metadata, res) {
2989
+ function exportAsCSV(controls, metadata, mappingsColumn, res) {
2990
+ return exportAsCSVWithMapping(controls, metadata, { mappings: mappingsColumn }, res);
2991
+ }
2992
+ function exportAsCSVWithMapping(controls, metadata, columnMappings, res) {
2990
2993
  const fieldSchema = metadata?.fieldSchema?.fields || {};
2991
2994
  const controlIdField = metadata?.control_id_field || "id";
2992
2995
  const allFields = /* @__PURE__ */ new Set();
@@ -2994,49 +2997,91 @@ function exportAsCSV(controls, metadata, res) {
2994
2997
  Object.keys(control).forEach((key) => allFields.add(key));
2995
2998
  });
2996
2999
  const fieldMapping = [];
3000
+ const usedDisplayNames = /* @__PURE__ */ new Set();
2997
3001
  if (allFields.has(controlIdField)) {
2998
3002
  const idSchema = fieldSchema[controlIdField];
2999
- fieldMapping.push({
3000
- fieldName: controlIdField,
3001
- displayName: idSchema?.original_name || "Control ID"
3002
- });
3003
+ const displayName = idSchema?.original_name || "Control ID";
3004
+ let isMappingColumn = false;
3005
+ if (columnMappings["mappings"]) {
3006
+ const targetDisplayName = columnMappings["mappings"];
3007
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
3008
+ isMappingColumn = true;
3009
+ }
3010
+ }
3011
+ fieldMapping.push({ fieldName: controlIdField, displayName, isMappingColumn });
3012
+ usedDisplayNames.add(displayName.toLowerCase());
3003
3013
  allFields.delete(controlIdField);
3004
3014
  } else if (allFields.has("id")) {
3005
- fieldMapping.push({
3006
- fieldName: "id",
3007
- displayName: "Control ID"
3008
- });
3015
+ let isMappingColumn = false;
3016
+ const displayName = "Control ID";
3017
+ if (columnMappings["mappings"]) {
3018
+ const targetDisplayName = columnMappings["mappings"];
3019
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
3020
+ isMappingColumn = true;
3021
+ }
3022
+ }
3023
+ fieldMapping.push({ fieldName: "id", displayName, isMappingColumn });
3024
+ usedDisplayNames.add("control id");
3009
3025
  allFields.delete("id");
3010
3026
  }
3011
3027
  if (allFields.has("family")) {
3012
3028
  const familySchema = fieldSchema["family"];
3013
- fieldMapping.push({
3014
- fieldName: "family",
3015
- displayName: familySchema?.original_name || "Family"
3016
- });
3029
+ const displayName = familySchema?.original_name || "Family";
3030
+ fieldMapping.push({ fieldName: "family", displayName });
3031
+ usedDisplayNames.add(displayName.toLowerCase());
3017
3032
  allFields.delete("family");
3018
3033
  }
3019
3034
  Array.from(allFields).filter((field) => field !== "mappings" && field !== "mappings_count").sort().forEach((field) => {
3020
3035
  const schema = fieldSchema[field];
3021
- const displayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3022
- fieldMapping.push({ fieldName: field, displayName });
3036
+ const defaultDisplayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3037
+ let isMappingColumn = false;
3038
+ let finalDisplayName = defaultDisplayName;
3039
+ if (columnMappings["mappings"]) {
3040
+ const targetDisplayName = columnMappings["mappings"];
3041
+ if (defaultDisplayName.toLowerCase() === targetDisplayName.toLowerCase()) {
3042
+ isMappingColumn = true;
3043
+ finalDisplayName = targetDisplayName;
3044
+ }
3045
+ }
3046
+ if (!usedDisplayNames.has(finalDisplayName.toLowerCase())) {
3047
+ fieldMapping.push({
3048
+ fieldName: field,
3049
+ displayName: finalDisplayName,
3050
+ isMappingColumn
3051
+ });
3052
+ usedDisplayNames.add(finalDisplayName.toLowerCase());
3053
+ }
3023
3054
  });
3024
- if (allFields.has("mappings_count")) {
3025
- fieldMapping.push({ fieldName: "mappings_count", displayName: "Mappings Count" });
3026
- }
3027
- if (allFields.has("mappings")) {
3055
+ if (allFields.has("mappings") && (!columnMappings["mappings"] || columnMappings["mappings"] === "Mappings")) {
3028
3056
  fieldMapping.push({ fieldName: "mappings", displayName: "Mappings" });
3029
3057
  }
3030
3058
  const csvRows = [];
3031
3059
  csvRows.push(fieldMapping.map((field) => `"${field.displayName}"`).join(","));
3032
3060
  controls.forEach((control) => {
3033
- const row = fieldMapping.map(({ fieldName }) => {
3034
- const value = control[fieldName];
3061
+ const row = fieldMapping.map(({ fieldName, isMappingColumn }) => {
3062
+ let value;
3063
+ if (isMappingColumn) {
3064
+ const mappingsValue = control["mappings"];
3065
+ if (Array.isArray(mappingsValue) && mappingsValue.length > 0) {
3066
+ const mappingsStr = mappingsValue.map((m) => m.description || m.justification || "").filter((desc) => desc && desc.trim() !== "").join("\n");
3067
+ if (mappingsStr.trim() !== "") {
3068
+ value = mappingsStr;
3069
+ } else {
3070
+ value = control[fieldName];
3071
+ }
3072
+ } else {
3073
+ value = control[fieldName];
3074
+ }
3075
+ } else {
3076
+ value = control[fieldName];
3077
+ }
3035
3078
  if (value === void 0 || value === null) return '""';
3036
3079
  if (fieldName === "mappings" && Array.isArray(value)) {
3037
- const mappingsStr = value.map(
3038
- (m) => `${m.status}: ${m.description.substring(0, 50)}${m.description.length > 50 ? "..." : ""}`
3039
- ).join("; ");
3080
+ const mappingsStr = value.map((m) => {
3081
+ const justification = m.description || m.justification || "";
3082
+ const status = m.status || "Unknown";
3083
+ return justification.trim() !== "" ? justification : `[${status}]`;
3084
+ }).join("\n");
3040
3085
  return `"${mappingsStr.replace(/"/g, '""')}"`;
3041
3086
  }
3042
3087
  if (Array.isArray(value)) return `"${value.join("; ").replace(/"/g, '""')}"`;
@@ -3051,32 +3096,101 @@ function exportAsCSV(controls, metadata, res) {
3051
3096
  res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
3052
3097
  res.send(csvContent);
3053
3098
  }
3054
- async function exportAsExcel(controls, metadata, res) {
3099
+ async function exportAsExcel(controls, metadata, mappingsColumn, res) {
3100
+ return await exportAsExcelWithMapping(controls, metadata, { mappings: mappingsColumn }, res);
3101
+ }
3102
+ async function exportAsExcelWithMapping(controls, metadata, columnMappings, res) {
3055
3103
  const fieldSchema = metadata?.fieldSchema?.fields || {};
3056
3104
  const controlIdField = metadata?.control_id_field || "id";
3057
- const worksheetData = controls.map((control) => {
3058
- const exportControl = {};
3059
- if (control[controlIdField]) {
3060
- const idSchema = fieldSchema[controlIdField];
3061
- const idDisplayName = idSchema?.original_name || "Control ID";
3062
- exportControl[idDisplayName] = control[controlIdField];
3063
- } else if (control.id) {
3064
- exportControl["Control ID"] = control.id;
3105
+ const allFields = /* @__PURE__ */ new Set();
3106
+ controls.forEach((control) => {
3107
+ Object.keys(control).forEach((key) => allFields.add(key));
3108
+ });
3109
+ const fieldMapping = [];
3110
+ const usedDisplayNames = /* @__PURE__ */ new Set();
3111
+ if (allFields.has(controlIdField)) {
3112
+ const idSchema = fieldSchema[controlIdField];
3113
+ const displayName = idSchema?.original_name || "Control ID";
3114
+ let isMappingColumn = false;
3115
+ if (columnMappings["mappings"]) {
3116
+ const targetDisplayName = columnMappings["mappings"];
3117
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
3118
+ isMappingColumn = true;
3119
+ }
3065
3120
  }
3066
- if (control.family) {
3067
- const familySchema = fieldSchema["family"];
3068
- const familyDisplayName = familySchema?.original_name || "Family";
3069
- exportControl[familyDisplayName] = control.family;
3121
+ fieldMapping.push({ fieldName: controlIdField, displayName, isMappingColumn });
3122
+ usedDisplayNames.add(displayName.toLowerCase());
3123
+ allFields.delete(controlIdField);
3124
+ } else if (allFields.has("id")) {
3125
+ let isMappingColumn = false;
3126
+ const displayName = "Control ID";
3127
+ if (columnMappings["mappings"]) {
3128
+ const targetDisplayName = columnMappings["mappings"];
3129
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
3130
+ isMappingColumn = true;
3131
+ }
3132
+ }
3133
+ fieldMapping.push({ fieldName: "id", displayName, isMappingColumn });
3134
+ usedDisplayNames.add("control id");
3135
+ allFields.delete("id");
3136
+ }
3137
+ if (allFields.has("family")) {
3138
+ const familySchema = fieldSchema["family"];
3139
+ const displayName = familySchema?.original_name || "Family";
3140
+ fieldMapping.push({ fieldName: "family", displayName });
3141
+ usedDisplayNames.add(displayName.toLowerCase());
3142
+ allFields.delete("family");
3143
+ }
3144
+ Array.from(allFields).filter((field) => field !== "mappings" && field !== "mappings_count").sort().forEach((field) => {
3145
+ const schema = fieldSchema[field];
3146
+ const defaultDisplayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3147
+ let isMappingColumn = false;
3148
+ let finalDisplayName = defaultDisplayName;
3149
+ if (columnMappings["mappings"]) {
3150
+ const targetDisplayName = columnMappings["mappings"];
3151
+ if (defaultDisplayName.toLowerCase() === targetDisplayName.toLowerCase()) {
3152
+ isMappingColumn = true;
3153
+ finalDisplayName = targetDisplayName;
3154
+ }
3155
+ }
3156
+ if (!usedDisplayNames.has(finalDisplayName.toLowerCase())) {
3157
+ fieldMapping.push({
3158
+ fieldName: field,
3159
+ displayName: finalDisplayName,
3160
+ isMappingColumn
3161
+ });
3162
+ usedDisplayNames.add(finalDisplayName.toLowerCase());
3070
3163
  }
3071
- Object.keys(control).forEach((key) => {
3072
- if (key === controlIdField || key === "id" || key === "family") return;
3073
- const schema = fieldSchema[key];
3074
- const displayName = schema?.original_name || (key === "mappings_count" ? "Mappings Count" : key === "mappings" ? "Mappings" : key.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()));
3075
- const value = control[key];
3076
- if (key === "mappings" && Array.isArray(value)) {
3077
- exportControl[displayName] = value.map(
3078
- (m) => `${m.status}: ${m.description.substring(0, 100)}${m.description.length > 100 ? "..." : ""}`
3079
- ).join("\n");
3164
+ });
3165
+ if (allFields.has("mappings") && (!columnMappings["mappings"] || columnMappings["mappings"] === "Mappings")) {
3166
+ fieldMapping.push({ fieldName: "mappings", displayName: "Mappings" });
3167
+ }
3168
+ const worksheetData = controls.map((control) => {
3169
+ const exportControl = {};
3170
+ fieldMapping.forEach(({ fieldName, displayName, isMappingColumn }) => {
3171
+ let value;
3172
+ if (isMappingColumn) {
3173
+ const mappingsValue = control["mappings"];
3174
+ if (Array.isArray(mappingsValue) && mappingsValue.length > 0) {
3175
+ const mappingsStr = mappingsValue.map((m) => m.description || m.justification || "").filter((desc) => desc && desc.trim() !== "").join("\n");
3176
+ if (mappingsStr.trim() !== "") {
3177
+ value = mappingsStr;
3178
+ } else {
3179
+ value = control[fieldName];
3180
+ }
3181
+ } else {
3182
+ value = control[fieldName];
3183
+ }
3184
+ } else {
3185
+ value = control[fieldName];
3186
+ }
3187
+ if (fieldName === "mappings" && Array.isArray(value)) {
3188
+ const mappingsStr = value.map((m) => {
3189
+ const justification = m.description || m.justification || "";
3190
+ const status = m.status || "Unknown";
3191
+ return justification.trim() !== "" ? justification : `[${status}]`;
3192
+ }).join("\n");
3193
+ exportControl[displayName] = mappingsStr;
3080
3194
  } else if (Array.isArray(value)) {
3081
3195
  exportControl[displayName] = value.join("; ");
3082
3196
  } else if (typeof value === "object" && value !== null) {
@@ -3490,6 +3604,7 @@ var init_spreadsheetRoutes = __esm({
3490
3604
  router.get("/export-controls", async (req, res) => {
3491
3605
  try {
3492
3606
  const format = req.query.format || "csv";
3607
+ const mappingsColumn = req.query.mappingsColumn || "Mappings";
3493
3608
  const state = getServerState();
3494
3609
  const fileStore = state.fileStore;
3495
3610
  if (!fileStore) {
@@ -3499,7 +3614,8 @@ var init_spreadsheetRoutes = __esm({
3499
3614
  const mappings = await fileStore.loadMappings();
3500
3615
  let metadata = {};
3501
3616
  try {
3502
- const metadataPath2 = join4(state.CONTROL_SET_DIR, "lula.yaml");
3617
+ const controlSetPath = getCurrentControlSetPath();
3618
+ const metadataPath2 = join4(controlSetPath, "lula.yaml");
3503
3619
  if (existsSync3(metadataPath2)) {
3504
3620
  const metadataContent = readFileSync3(metadataPath2, "utf8");
3505
3621
  metadata = yaml4.load(metadataContent);
@@ -3527,10 +3643,10 @@ var init_spreadsheetRoutes = __esm({
3527
3643
  debug(`Exporting ${controlsWithMappings.length} controls as ${format}`);
3528
3644
  switch (format.toLowerCase()) {
3529
3645
  case "csv":
3530
- return exportAsCSV(controlsWithMappings, metadata, res);
3646
+ return exportAsCSV(controlsWithMappings, metadata, mappingsColumn, res);
3531
3647
  case "excel":
3532
3648
  case "xlsx":
3533
- return await exportAsExcel(controlsWithMappings, metadata, res);
3649
+ return await exportAsExcel(controlsWithMappings, metadata, mappingsColumn, res);
3534
3650
  case "json":
3535
3651
  return exportAsJSON(controlsWithMappings, metadata, res);
3536
3652
  default:
@@ -3541,6 +3657,106 @@ var init_spreadsheetRoutes = __esm({
3541
3657
  res.status(500).json({ error: error.message });
3542
3658
  }
3543
3659
  });
3660
+ router.get("/export-column-headers", async (req, res) => {
3661
+ try {
3662
+ let metadata = {};
3663
+ try {
3664
+ const controlSetPath = getCurrentControlSetPath();
3665
+ const metadataPath2 = join4(controlSetPath, "lula.yaml");
3666
+ if (existsSync3(metadataPath2)) {
3667
+ const metadataContent = readFileSync3(metadataPath2, "utf8");
3668
+ metadata = yaml4.load(metadataContent);
3669
+ } else {
3670
+ return res.status(404).json({ error: `No lula.yaml file found in control set path: ${controlSetPath}` });
3671
+ }
3672
+ } catch {
3673
+ return res.status(500).json({ error: "Failed to read lula.yaml file" });
3674
+ }
3675
+ const fieldSchema = metadata?.fieldSchema?.fields || {};
3676
+ const controlIdField = metadata?.control_id_field || "id";
3677
+ const columnHeaders = [];
3678
+ if (fieldSchema[controlIdField]) {
3679
+ const idSchema = fieldSchema[controlIdField];
3680
+ const displayName = idSchema?.original_name || "Control ID";
3681
+ columnHeaders.push({
3682
+ value: displayName,
3683
+ label: displayName
3684
+ });
3685
+ } else {
3686
+ columnHeaders.push({ value: "Control ID", label: "Control ID" });
3687
+ }
3688
+ if (fieldSchema["family"]) {
3689
+ const familySchema = fieldSchema["family"];
3690
+ const displayName = familySchema?.original_name || "Family";
3691
+ columnHeaders.push({
3692
+ value: displayName,
3693
+ label: displayName
3694
+ });
3695
+ }
3696
+ Object.entries(fieldSchema).forEach(([fieldName, schema]) => {
3697
+ if (fieldName === controlIdField || fieldName === "family" || fieldName === "mappings" || fieldName === "mappings_count") {
3698
+ return;
3699
+ }
3700
+ const displayName = schema?.original_name || fieldName.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3701
+ columnHeaders.push({
3702
+ value: displayName,
3703
+ label: displayName
3704
+ });
3705
+ });
3706
+ columnHeaders.push({ value: "Mappings", label: "Mappings (Default)" });
3707
+ res.json({
3708
+ columnHeaders,
3709
+ defaultColumn: "Mappings"
3710
+ });
3711
+ } catch (error) {
3712
+ res.status(500).json({ error: error.message });
3713
+ }
3714
+ });
3715
+ router.post("/export-csv", async (req, res) => {
3716
+ try {
3717
+ const { format = "csv", _columns = [], columnMappings = {} } = req.body;
3718
+ const state = getServerState();
3719
+ const fileStore = state.fileStore;
3720
+ if (!fileStore) {
3721
+ return res.status(500).json({ error: "No control set loaded" });
3722
+ }
3723
+ const controls = await fileStore.loadAllControls();
3724
+ const mappings = await fileStore.loadMappings();
3725
+ let metadata = {};
3726
+ try {
3727
+ const controlSetPath = getCurrentControlSetPath();
3728
+ const metadataPath2 = join4(controlSetPath, "lula.yaml");
3729
+ if (existsSync3(metadataPath2)) {
3730
+ const metadataContent = readFileSync3(metadataPath2, "utf8");
3731
+ metadata = yaml4.load(metadataContent);
3732
+ }
3733
+ } catch (err) {
3734
+ debug("Could not load metadata:", err);
3735
+ }
3736
+ if (!controls || controls.length === 0) {
3737
+ return res.status(404).json({ error: "No controls found" });
3738
+ }
3739
+ const controlIdField = metadata?.control_id_field || "id";
3740
+ const controlsWithMappings = controls.map((control) => {
3741
+ const controlId = control[controlIdField] || control.id;
3742
+ const controlMappings = mappings.filter((m) => m.control_id === controlId);
3743
+ return {
3744
+ ...control,
3745
+ mappings_count: controlMappings.length,
3746
+ mappings: controlMappings.map((m) => ({
3747
+ uuid: m.uuid,
3748
+ status: m.status,
3749
+ description: m.justification || ""
3750
+ }))
3751
+ };
3752
+ });
3753
+ debug(`Exporting ${controlsWithMappings.length} controls as ${format} with column mappings`);
3754
+ return exportAsCSVWithMapping(controlsWithMappings, metadata, columnMappings, res);
3755
+ } catch (error) {
3756
+ console.error("Export error:", error);
3757
+ res.status(500).json({ error: error.message });
3758
+ }
3759
+ });
3544
3760
  router.post("/parse-excel", upload.single("file"), async (req, res) => {
3545
3761
  try {
3546
3762
  if (!req.file) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lula2",
3
- "version": "0.4.0",
3
+ "version": "0.5.0-nightly.1",
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",
@@ -72,8 +53,8 @@
72
53
  "yaml": "^2.8.1"
73
54
  },
74
55
  "devDependencies": {
75
- "@commitlint/cli": "^19.8.1",
76
- "@commitlint/config-conventional": "^19.8.1",
56
+ "@commitlint/cli": "^20.0.0",
57
+ "@commitlint/config-conventional": "^20.0.0",
77
58
  "@eslint/compat": "^1.3.2",
78
59
  "@eslint/eslintrc": "^3.3.1",
79
60
  "@eslint/js": "^9.35.0",
@@ -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
+ }
@@ -0,0 +1,163 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import { createEventDispatcher } from 'svelte';
6
+ import { Close } from 'carbon-icons-svelte';
7
+
8
+ interface ColumnHeader {
9
+ value: string;
10
+ label: string;
11
+ }
12
+
13
+ interface ExportColumnDialogProps {
14
+ isOpen: boolean;
15
+ format: 'csv' | 'excel';
16
+ columnHeaders: ColumnHeader[];
17
+ defaultColumn: string;
18
+ }
19
+
20
+ let {
21
+ isOpen = $bindable(),
22
+ format,
23
+ columnHeaders = [],
24
+ defaultColumn = 'Mappings'
25
+ }: ExportColumnDialogProps = $props();
26
+
27
+ const dispatch = createEventDispatcher<{
28
+ export: { format: string; mappingsColumn: string };
29
+ cancel: void;
30
+ }>();
31
+
32
+ let selectedColumn = $state(defaultColumn);
33
+
34
+ // Reset selected column and focus when dialog opens
35
+ $effect(() => {
36
+ if (isOpen) {
37
+ selectedColumn = defaultColumn;
38
+ }
39
+ });
40
+
41
+ function focusSelect(node: HTMLSelectElement) {
42
+ if (isOpen) {
43
+ setTimeout(() => node.focus(), 0);
44
+ }
45
+ return {
46
+ update(newIsOpen: boolean) {
47
+ if (newIsOpen) {
48
+ setTimeout(() => node.focus(), 0);
49
+ }
50
+ }
51
+ };
52
+ }
53
+
54
+ function handleExport() {
55
+ dispatch('export', { format, mappingsColumn: selectedColumn });
56
+ isOpen = false;
57
+ }
58
+
59
+ function handleCancel() {
60
+ dispatch('cancel');
61
+ isOpen = false;
62
+ }
63
+
64
+ function handleKeydown(event: KeyboardEvent) {
65
+ if (event.key === 'Escape') {
66
+ handleCancel();
67
+ } else if (event.key === 'Enter') {
68
+ handleExport();
69
+ }
70
+ }
71
+
72
+ function handleBackdropClick(event: MouseEvent) {
73
+ if (event.target === event.currentTarget) {
74
+ handleCancel();
75
+ }
76
+ }
77
+ </script>
78
+
79
+ <svelte:window onkeydown={handleKeydown} />
80
+
81
+ {#if isOpen}
82
+ <!-- Modal Backdrop -->
83
+ <div
84
+ class="fixed inset-0 bg-gray-700 dark:bg-gray-900 bg-opacity-50 dark:bg-opacity-75 overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4"
85
+ role="dialog"
86
+ aria-modal="true"
87
+ tabindex="-1"
88
+ onclick={handleBackdropClick}
89
+ onkeydown={(e) => e.key === 'Escape' && handleCancel()}
90
+ >
91
+ <!-- Modal Content -->
92
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
93
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
94
+ <div
95
+ class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md mx-auto"
96
+ role="document"
97
+ onclick={(e) => e.stopPropagation()}
98
+ >
99
+ <!-- Header -->
100
+ <div class="flex items-center justify-between mb-4">
101
+ <h2 class="text-xl font-semibold text-gray-900 dark:text-white">
102
+ Export as {format.toUpperCase()}
103
+ </h2>
104
+ <button
105
+ onclick={handleCancel}
106
+ class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
107
+ aria-label="Close dialog"
108
+ >
109
+ <Close class="w-6 h-6" />
110
+ </button>
111
+ </div>
112
+
113
+ <!-- Content -->
114
+ <div class="mb-6">
115
+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
116
+ Choose which column should contain the mappings data:
117
+ </p>
118
+
119
+ <div class="space-y-2">
120
+ <label
121
+ for="column-select"
122
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
123
+ >
124
+ Mappings Column
125
+ </label>
126
+ <select
127
+ id="column-select"
128
+ use:focusSelect={isOpen}
129
+ bind:value={selectedColumn}
130
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
131
+ >
132
+ {#each columnHeaders as header}
133
+ <option value={header.value}>{header.label}</option>
134
+ {/each}
135
+ </select>
136
+ </div>
137
+
138
+ <div class="mt-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md">
139
+ <p class="text-xs text-blue-600 dark:text-blue-400">
140
+ <strong>Note:</strong> Mappings data will be formatted as "status: description..." in the
141
+ selected column.
142
+ </p>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Footer -->
147
+ <div class="flex justify-end space-x-3">
148
+ <button
149
+ onclick={handleCancel}
150
+ class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
151
+ >
152
+ Cancel
153
+ </button>
154
+ <button
155
+ onclick={handleExport}
156
+ class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-lg transition-colors"
157
+ >
158
+ Export {format.toUpperCase()}
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ {/if}
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ export { default as ExportColumnDialog } from './ExportColumnDialog.svelte';