lula2 0.6.2 → 0.6.3-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.
package/dist/index.js CHANGED
@@ -1881,6 +1881,17 @@ var init_fileStore = __esm({
1881
1881
  if (!existsSync2(this.controlsDir)) {
1882
1882
  return [];
1883
1883
  }
1884
+ let controlOrder = null;
1885
+ try {
1886
+ const lulaConfigPath = join2(this.baseDir, "lula.yaml");
1887
+ if (existsSync2(lulaConfigPath)) {
1888
+ const content = readFileSync2(lulaConfigPath, "utf8");
1889
+ const metadata = yaml2.load(content);
1890
+ controlOrder = metadata?.controlOrder || null;
1891
+ }
1892
+ } catch (error) {
1893
+ console.error("Failed to load lula.yaml for controlOrder:", error);
1894
+ }
1884
1895
  const entries = readdirSync(this.controlsDir);
1885
1896
  const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
1886
1897
  if (yamlFiles.length > 0) {
@@ -1899,7 +1910,11 @@ var init_fileStore = __esm({
1899
1910
  }
1900
1911
  });
1901
1912
  const results2 = await Promise.all(promises);
1902
- return results2.filter((c) => c !== null);
1913
+ const controls2 = results2.filter((c) => c !== null);
1914
+ if (controlOrder && controlOrder.length > 0) {
1915
+ return this.sortControlsByOrder(controls2, controlOrder);
1916
+ }
1917
+ return controls2;
1903
1918
  }
1904
1919
  const families = entries.filter((name) => {
1905
1920
  const familyPath = join2(this.controlsDir, name);
@@ -1922,7 +1937,25 @@ var init_fileStore = __esm({
1922
1937
  allPromises.push(...familyPromises);
1923
1938
  }
1924
1939
  const results = await Promise.all(allPromises);
1925
- return results.filter((c) => c !== null);
1940
+ const controls = results.filter((c) => c !== null);
1941
+ if (controlOrder && controlOrder.length > 0) {
1942
+ return this.sortControlsByOrder(controls, controlOrder);
1943
+ }
1944
+ return controls;
1945
+ }
1946
+ /**
1947
+ * Sort controls based on the provided order array
1948
+ */
1949
+ sortControlsByOrder(controls, controlOrder) {
1950
+ const orderMap = /* @__PURE__ */ new Map();
1951
+ controlOrder.forEach((controlId, index) => {
1952
+ orderMap.set(controlId, index);
1953
+ });
1954
+ return controls.sort((a, b) => {
1955
+ const aIndex = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
1956
+ const bIndex = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
1957
+ return aIndex - bIndex;
1958
+ });
1926
1959
  }
1927
1960
  /**
1928
1961
  * Load mappings from mappings directory
@@ -2949,7 +2982,7 @@ function processImportParameters(reqBody) {
2949
2982
  frontendFieldSchema
2950
2983
  };
2951
2984
  }
2952
- async function parseUploadedFile(file) {
2985
+ async function parseUploadedFile(file, sheetName) {
2953
2986
  const fileName = file.originalname || "";
2954
2987
  const isCSV = fileName.toLowerCase().endsWith(".csv");
2955
2988
  let rawData = [];
@@ -2958,10 +2991,13 @@ async function parseUploadedFile(file) {
2958
2991
  rawData = parseCSV(csvContent);
2959
2992
  } else {
2960
2993
  const workbook = XLSX.read(file.buffer, { type: "buffer" });
2961
- const worksheetName = workbook.SheetNames[0];
2994
+ const worksheetName = sheetName || workbook.SheetNames[0];
2962
2995
  if (!worksheetName) {
2963
2996
  throw new Error("No worksheet found in file");
2964
2997
  }
2998
+ if (sheetName && !workbook.SheetNames.includes(sheetName)) {
2999
+ throw new Error(`Sheet "${sheetName}" not found in workbook`);
3000
+ }
2965
3001
  const worksheet = workbook.Sheets[worksheetName];
2966
3002
  rawData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null });
2967
3003
  }
@@ -3039,6 +3075,7 @@ function processSpreadsheetData(rawData, headers, startRowIndex, params) {
3039
3075
  continue;
3040
3076
  }
3041
3077
  const family = extractFamilyFromControlId(controlId);
3078
+ control._originalRowIndex = i;
3042
3079
  control.family = family;
3043
3080
  controls.push(control);
3044
3081
  if (!families.has(family)) {
@@ -3161,6 +3198,7 @@ async function createOutputStructure(processedData, fieldSchema, params) {
3161
3198
  params.controlIdField,
3162
3199
  params.namingConvention
3163
3200
  );
3201
+ const controlOrder = controls.sort((a, b) => (a._originalRowIndex || 0) - (b._originalRowIndex || 0)).map((control) => control[controlIdFieldNameClean]);
3164
3202
  const controlSetData = {
3165
3203
  name: params.controlSetName,
3166
3204
  description: params.controlSetDescription,
@@ -3168,12 +3206,16 @@ async function createOutputStructure(processedData, fieldSchema, params) {
3168
3206
  control_id_field: controlIdFieldNameClean,
3169
3207
  controlCount: controls.length,
3170
3208
  families: uniqueFamilies,
3209
+ controlOrder,
3171
3210
  fieldSchema
3172
3211
  };
3173
3212
  writeFileSync2(join4(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
3174
3213
  const controlsDir = join4(baseDir, "controls");
3175
3214
  const mappingsDir = join4(baseDir, "mappings");
3176
- families.forEach((familyControls, family) => {
3215
+ const sortedFamilies = Array.from(families.entries()).sort(
3216
+ (a, b) => a[0].localeCompare(b[0])
3217
+ );
3218
+ sortedFamilies.forEach(([family, familyControls]) => {
3177
3219
  const familyDir = join4(controlsDir, family);
3178
3220
  const familyMappingsDir = join4(mappingsDir, family);
3179
3221
  if (!existsSync3(familyDir)) {
@@ -3182,7 +3224,10 @@ async function createOutputStructure(processedData, fieldSchema, params) {
3182
3224
  if (!existsSync3(familyMappingsDir)) {
3183
3225
  mkdirSync2(familyMappingsDir, { recursive: true });
3184
3226
  }
3185
- familyControls.forEach((control) => {
3227
+ const sortedFamilyControls = familyControls.sort(
3228
+ (a, b) => (a._originalRowIndex || 0) - (b._originalRowIndex || 0)
3229
+ );
3230
+ sortedFamilyControls.forEach((control) => {
3186
3231
  const controlId = control[controlIdFieldNameClean];
3187
3232
  if (!controlId) {
3188
3233
  console.error("Missing control ID for control:", control);
@@ -3204,7 +3249,7 @@ async function createOutputStructure(processedData, fieldSchema, params) {
3204
3249
  filteredControl.family = control.family;
3205
3250
  }
3206
3251
  Object.keys(control).forEach((fieldName) => {
3207
- if (fieldName === "family") return;
3252
+ if (fieldName === "family" || fieldName === "_originalRowIndex") return;
3208
3253
  if (params.justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
3209
3254
  justificationContents.push(control[fieldName]);
3210
3255
  }
@@ -3568,12 +3613,14 @@ function exportAsJSON(controls, metadata, res) {
3568
3613
  res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
3569
3614
  res.json(exportData);
3570
3615
  }
3571
- var router, upload, spreadsheetRoutes_default;
3616
+ var MAX_HEADER_CANDIDATES, PREVIEW_COLUMNS, router, upload, spreadsheetRoutes_default;
3572
3617
  var init_spreadsheetRoutes = __esm({
3573
3618
  "cli/server/spreadsheetRoutes.ts"() {
3574
3619
  "use strict";
3575
3620
  init_debug();
3576
3621
  init_serverState();
3622
+ MAX_HEADER_CANDIDATES = 5;
3623
+ PREVIEW_COLUMNS = 4;
3577
3624
  router = express.Router();
3578
3625
  upload = multer({
3579
3626
  storage: multer.memoryStorage(),
@@ -3586,7 +3633,8 @@ var init_spreadsheetRoutes = __esm({
3586
3633
  return res.status(400).json({ error: "No file uploaded" });
3587
3634
  }
3588
3635
  const params = processImportParameters(req.body);
3589
- const rawData = await parseUploadedFile(req.file);
3636
+ const sheetName = req.body.sheetName;
3637
+ const rawData = await parseUploadedFile(req.file, sheetName);
3590
3638
  const startRowIndex = parseInt(params.startRow) - 1;
3591
3639
  if (rawData.length <= startRowIndex) {
3592
3640
  return res.status(400).json({ error: "Start row exceeds sheet data" });
@@ -3798,9 +3846,9 @@ var init_spreadsheetRoutes = __esm({
3798
3846
  const worksheet = workbook.Sheets[worksheetName];
3799
3847
  rows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null });
3800
3848
  }
3801
- const headerCandidates = rows.slice(0, 5).map((row, index) => ({
3849
+ const headerCandidates = rows.slice(0, MAX_HEADER_CANDIDATES).map((row, index) => ({
3802
3850
  row: index + 1,
3803
- preview: row.slice(0, 4).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
3851
+ preview: row.slice(0, PREVIEW_COLUMNS).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
3804
3852
  }));
3805
3853
  res.json({
3806
3854
  sheets,
@@ -3857,6 +3905,39 @@ var init_spreadsheetRoutes = __esm({
3857
3905
  res.status(500).json({ error: "Failed to parse Excel sheet" });
3858
3906
  }
3859
3907
  });
3908
+ router.post("/parse-excel-sheet-previews", upload.single("file"), async (req, res) => {
3909
+ try {
3910
+ const { sheetName } = req.body;
3911
+ if (!req.file) {
3912
+ return res.status(400).json({ error: "No file uploaded" });
3913
+ }
3914
+ const fileName = req.file.originalname || "";
3915
+ const isCSV = fileName.toLowerCase().endsWith(".csv");
3916
+ let rows = [];
3917
+ if (isCSV) {
3918
+ const csvContent = req.file.buffer.toString("utf-8");
3919
+ rows = parseCSV(csvContent);
3920
+ } else {
3921
+ const workbook = XLSX.read(req.file.buffer, { type: "buffer" });
3922
+ if (!workbook.SheetNames.includes(sheetName)) {
3923
+ return res.status(400).json({ error: `Sheet "${sheetName}" not found` });
3924
+ }
3925
+ const worksheet = workbook.Sheets[sheetName];
3926
+ rows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null });
3927
+ }
3928
+ const headerCandidates = rows.slice(0, MAX_HEADER_CANDIDATES).map((row, index) => ({
3929
+ row: index + 1,
3930
+ preview: row.slice(0, PREVIEW_COLUMNS).filter((v) => v !== null).filter((v) => v !== void 0).join(", ") + (row.length > 4 ? ", ..." : "")
3931
+ }));
3932
+ res.json({
3933
+ rowPreviews: headerCandidates,
3934
+ totalRows: rows.length
3935
+ });
3936
+ } catch (error) {
3937
+ console.error("Error getting sheet previews:", error);
3938
+ res.status(500).json({ error: "Failed to get sheet previews" });
3939
+ }
3940
+ });
3860
3941
  spreadsheetRoutes_default = router;
3861
3942
  }
3862
3943
  });
@@ -5599,155 +5680,209 @@ function containsLulaAnnotations(text) {
5599
5680
  const lines = text.split("\n");
5600
5681
  return lines.some((line) => line.includes("@lulaStart") || line.includes("@lulaEnd"));
5601
5682
  }
5602
- function crawlCommand() {
5603
- return new Command().command("crawl").description("Detect compliance-related changes between @lulaStart and @lulaEnd in PR files").addOption(
5604
- new Option("--post-mode <mode>", "How to post findings").choices(["review", "comment"]).default("review")
5605
- ).action(async (opts) => {
5606
- let leavePost = false;
5607
- const { owner, repo, pull_number } = getPRContext();
5608
- console.log(`Analyzing PR #${pull_number} in ${owner}/${repo} for compliance changes...`);
5609
- const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
5610
- const pr = await octokit.pulls.get({ owner, repo, pull_number });
5611
- const prBranch = pr.data.head.ref;
5612
- const { data: files } = await octokit.pulls.listFiles({ owner, repo, pull_number });
5613
- let commentBody = `${LULA_SIGNATURE}
5683
+ function createInitialCommentBody(filesCount) {
5684
+ return `${LULA_SIGNATURE}
5614
5685
  ## Lula Compliance Overview
5615
5686
 
5616
5687
  Please review the changes to ensure they meet compliance standards.
5617
5688
 
5618
5689
  ### Reviewed Changes
5619
5690
 
5620
- Lula reviewed ${files.length} files changed that affect compliance.
5691
+ Lula reviewed ${filesCount} files changed that affect compliance.
5621
5692
 
5622
5693
  `;
5623
- const deletedFilesWithAnnotations = [];
5624
- for (const file of files) {
5625
- if (file.status === "removed") {
5626
- try {
5627
- const oldText = await fetchRawFileViaAPI({
5628
- octokit,
5629
- owner,
5630
- repo,
5631
- path: file.filename,
5632
- ref: "main"
5633
- });
5634
- if (containsLulaAnnotations(oldText)) {
5635
- deletedFilesWithAnnotations.push(file.filename);
5636
- }
5637
- } catch (err) {
5638
- console.error(`Error checking deleted file ${file.filename}: ${err}`);
5694
+ }
5695
+ async function analyzeDeletedFiles(context) {
5696
+ const { octokit, owner, repo, files } = context;
5697
+ const deletedFilesWithAnnotations = [];
5698
+ for (const file of files) {
5699
+ if (file.status === "removed") {
5700
+ try {
5701
+ const oldText = await fetchRawFileViaAPI({
5702
+ octokit,
5703
+ owner,
5704
+ repo,
5705
+ path: file.filename,
5706
+ ref: "main"
5707
+ });
5708
+ if (containsLulaAnnotations(oldText)) {
5709
+ deletedFilesWithAnnotations.push(file.filename);
5639
5710
  }
5711
+ } catch (err) {
5712
+ console.error(`Error checking deleted file ${file.filename}: ${err}`);
5640
5713
  }
5641
5714
  }
5642
- if (deletedFilesWithAnnotations.length > 0) {
5643
- leavePost = true;
5644
- commentBody += `
5715
+ }
5716
+ if (deletedFilesWithAnnotations.length === 0) {
5717
+ return { hasFindings: false, warningContent: "" };
5718
+ }
5719
+ let warningContent = `
5645
5720
 
5646
5721
  **Compliance Warning: Files with Lula annotations were deleted**
5647
5722
 
5648
5723
  `;
5649
- commentBody += `The following files contained compliance annotations (\`@lulaStart\`/\`@lulaEnd\`) and were deleted in this PR. This may affect compliance coverage:
5724
+ warningContent += `The following files contained compliance annotations (\`@lulaStart\`/\`@lulaEnd\`) and were deleted in this PR. This may affect compliance coverage:
5650
5725
 
5651
5726
  `;
5652
- for (const filename of deletedFilesWithAnnotations) {
5653
- commentBody += `- \`${filename}\`
5727
+ for (const filename of deletedFilesWithAnnotations) {
5728
+ warningContent += `- \`${filename}\`
5654
5729
  `;
5655
- }
5656
- commentBody += `
5730
+ }
5731
+ warningContent += `
5657
5732
  Please review whether:
5658
5733
  `;
5659
- commentBody += `- The compliance coverage provided by these files is still needed
5734
+ warningContent += `- The compliance coverage provided by these files is still needed
5660
5735
  `;
5661
- commentBody += `- Alternative compliance measures have been implemented
5736
+ warningContent += `- Alternative compliance measures have been implemented
5662
5737
  `;
5663
- commentBody += `- The deletion is intentional and compliance-approved
5738
+ warningContent += `- The deletion is intentional and compliance-approved
5664
5739
 
5665
5740
  `;
5666
- commentBody += `---
5741
+ warningContent += `---
5667
5742
 
5668
5743
  `;
5669
- }
5670
- for (const file of files) {
5671
- if (file.status === "added" || file.status === "removed") continue;
5672
- try {
5673
- const [oldText, newText] = await Promise.all([
5674
- fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: "main" }),
5675
- fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: prBranch })
5676
- ]);
5677
- const changedBlocks = getChangedBlocks(oldText, newText);
5678
- const removedBlocks = getRemovedBlocks(oldText, newText);
5679
- for (const block of changedBlocks) {
5680
- console.log(`Commenting regarding \`${file.filename}\`.`);
5681
- leavePost = true;
5682
- commentBody += `
5744
+ return { hasFindings: true, warningContent };
5745
+ }
5746
+ function generateChangedBlocksContent(filename, changedBlocks, newText) {
5747
+ let content = "";
5748
+ for (const block of changedBlocks) {
5749
+ console.log(`Commenting regarding \`${filename}\`.`);
5750
+ content += `
5683
5751
 
5684
5752
  ---
5685
5753
  | File | Lines Changed |
5686
5754
  | ---- | ------------- |
5687
5755
  `;
5688
- const newBlockText = newText.split("\n").slice(block.startLine, block.endLine).join("\n");
5689
- const blockSha256 = createHash2("sha256").update(newBlockText).digest("hex");
5690
- commentBody += `| \`${file.filename}\` | \`${block.startLine + 1}\u2013${block.endLine}\` |
5756
+ const newBlockText = newText.split("\n").slice(block.startLine, block.endLine).join("\n");
5757
+ const blockSha256 = createHash2("sha256").update(newBlockText).digest("hex");
5758
+ content += `| \`${filename}\` | \`${block.startLine + 1}\u2013${block.endLine}\` |
5691
5759
  > **uuid**-\`${block.uuid}\`
5692
5760
  **sha256** \`${blockSha256}\`
5693
5761
 
5694
5762
  `;
5695
- }
5696
- if (removedBlocks.length > 0) {
5697
- leavePost = true;
5698
- console.log(`Found removed annotations in \`${file.filename}\`.`);
5699
- commentBody += `
5763
+ }
5764
+ return content;
5765
+ }
5766
+ function generateRemovedBlocksContent(filename, removedBlocks, oldText) {
5767
+ if (removedBlocks.length === 0) {
5768
+ return "";
5769
+ }
5770
+ console.log(`Found removed annotations in \`${filename}\`.`);
5771
+ let content = `
5700
5772
 
5701
- **Compliance Warning: Lula annotations were removed from \`${file.filename}\`**
5773
+ **Compliance Warning: Lula annotations were removed from \`${filename}\`**
5702
5774
 
5703
5775
  `;
5704
- commentBody += `The following compliance annotation blocks were present in the original file but are missing in the updated version:
5776
+ content += `The following compliance annotation blocks were present in the original file but are missing in the updated version:
5705
5777
 
5706
5778
  `;
5707
- commentBody += `| File | Original Lines | UUID |
5779
+ content += `| File | Original Lines | UUID |
5708
5780
  `;
5709
- commentBody += `| ---- | -------------- | ---- |
5781
+ content += `| ---- | -------------- | ---- |
5710
5782
  `;
5711
- for (const block of removedBlocks) {
5712
- const oldBlockText = oldText.split("\n").slice(block.startLine, block.endLine).join("\n");
5713
- const blockSha256 = createHash2("sha256").update(oldBlockText).digest("hex");
5714
- commentBody += `| \`${file.filename}\` | \`${block.startLine + 1}\u2013${block.endLine}\` | \`${block.uuid}\` |
5783
+ for (const block of removedBlocks) {
5784
+ const oldBlockText = oldText.split("\n").slice(block.startLine, block.endLine).join("\n");
5785
+ const blockSha256 = createHash2("sha256").update(oldBlockText).digest("hex");
5786
+ content += `| \`${filename}\` | \`${block.startLine + 1}\u2013${block.endLine}\` | \`${block.uuid}\` |
5715
5787
  `;
5716
- commentBody += `> **sha256** \`${blockSha256}\`
5788
+ content += `> **sha256** \`${blockSha256}\`
5717
5789
 
5718
5790
  `;
5719
- }
5720
- commentBody += `Please review whether:
5791
+ }
5792
+ content += `Please review whether:
5721
5793
  `;
5722
- commentBody += `- The removal of these compliance annotations is intentional
5794
+ content += `- The removal of these compliance annotations is intentional
5723
5795
  `;
5724
- commentBody += `- Alternative compliance measures have been implemented
5796
+ content += `- Alternative compliance measures have been implemented
5725
5797
  `;
5726
- commentBody += `- The compliance coverage is still adequate
5798
+ content += `- The compliance coverage is still adequate
5727
5799
 
5728
5800
  `;
5729
- commentBody += `---
5801
+ content += `---
5730
5802
 
5731
5803
  `;
5732
- }
5733
- } catch (err) {
5734
- console.error(`Error processing ${file.filename}: ${err}`);
5735
- }
5736
- }
5737
- if (opts.postMode === "comment") {
5738
- await deleteOldIssueComments({ octokit, owner, repo, pull_number });
5739
- } else {
5740
- await dismissOldReviews({ octokit, owner, repo, pull_number });
5741
- await deleteOldReviewComments({ octokit, owner, repo, pull_number });
5804
+ return content;
5805
+ }
5806
+ async function analyzeModifiedFiles(context) {
5807
+ const { octokit, owner, repo, prBranch, files } = context;
5808
+ let changesContent = "";
5809
+ let hasFindings = false;
5810
+ for (const file of files) {
5811
+ if (file.status === "added" || file.status === "removed") continue;
5812
+ try {
5813
+ const [oldText, newText] = await Promise.all([
5814
+ fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: "main" }),
5815
+ fetchRawFileViaAPI({ octokit, owner, repo, path: file.filename, ref: prBranch })
5816
+ ]);
5817
+ const changedBlocks = getChangedBlocks(oldText, newText);
5818
+ const removedBlocks = getRemovedBlocks(oldText, newText);
5819
+ if (changedBlocks.length > 0) {
5820
+ hasFindings = true;
5821
+ changesContent += generateChangedBlocksContent(file.filename, changedBlocks, newText);
5822
+ }
5823
+ if (removedBlocks.length > 0) {
5824
+ hasFindings = true;
5825
+ changesContent += generateRemovedBlocksContent(file.filename, removedBlocks, oldText);
5826
+ }
5827
+ } catch (err) {
5828
+ console.error(`Error processing ${file.filename}: ${err}`);
5742
5829
  }
5743
- if (leavePost) {
5830
+ }
5831
+ return { hasFindings, changesContent };
5832
+ }
5833
+ async function performComplianceAnalysis(context) {
5834
+ let commentBody = createInitialCommentBody(context.files.length);
5835
+ let hasFindings = false;
5836
+ const deletedAnalysis = await analyzeDeletedFiles(context);
5837
+ if (deletedAnalysis.hasFindings) {
5838
+ hasFindings = true;
5839
+ commentBody += deletedAnalysis.warningContent;
5840
+ }
5841
+ const modifiedAnalysis = await analyzeModifiedFiles(context);
5842
+ if (modifiedAnalysis.hasFindings) {
5843
+ hasFindings = true;
5844
+ commentBody += modifiedAnalysis.changesContent;
5845
+ }
5846
+ return { hasFindings, commentBody };
5847
+ }
5848
+ async function cleanupOldPosts(context, postMode) {
5849
+ const { octokit, owner, repo, pull_number } = context;
5850
+ if (postMode === "comment") {
5851
+ await deleteOldIssueComments({ octokit, owner, repo, pull_number });
5852
+ } else {
5853
+ await dismissOldReviews({ octokit, owner, repo, pull_number });
5854
+ await deleteOldReviewComments({ octokit, owner, repo, pull_number });
5855
+ }
5856
+ }
5857
+ function crawlCommand() {
5858
+ return new Command().command("crawl").description("Detect compliance-related changes between @lulaStart and @lulaEnd in PR files").addOption(
5859
+ new Option("--post-mode <mode>", "How to post findings").choices(["review", "comment"]).default("review")
5860
+ ).action(async (opts) => {
5861
+ const { owner, repo, pull_number } = getPRContext();
5862
+ console.log(`Analyzing PR #${pull_number} in ${owner}/${repo} for compliance changes...`);
5863
+ const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
5864
+ const pr = await octokit.pulls.get({ owner, repo, pull_number });
5865
+ const prBranch = pr.data.head.ref;
5866
+ const { data: files } = await octokit.pulls.listFiles({ owner, repo, pull_number });
5867
+ const context = {
5868
+ octokit,
5869
+ owner,
5870
+ repo,
5871
+ pull_number,
5872
+ prBranch,
5873
+ files
5874
+ };
5875
+ const analysisResult = await performComplianceAnalysis(context);
5876
+ await cleanupOldPosts(context, opts.postMode);
5877
+ if (analysisResult.hasFindings) {
5878
+ const finalBody = analysisResult.commentBody + closingBody;
5744
5879
  await postFinding({
5745
5880
  octokit,
5746
5881
  postMode: opts.postMode,
5747
5882
  owner,
5748
5883
  repo,
5749
5884
  pull_number,
5750
- body: commentBody + closingBody
5885
+ body: finalBody
5751
5886
  });
5752
5887
  const header = `Posted (${opts.postMode})`;
5753
5888
  const underline = "-".repeat(header.length);
@@ -5755,7 +5890,7 @@ Please review whether:
5755
5890
  ${header}
5756
5891
  ${underline}
5757
5892
 
5758
- ${commentBody + closingBody}
5893
+ ${finalBody}
5759
5894
 
5760
5895
  `);
5761
5896
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lula2",
3
- "version": "0.6.2",
3
+ "version": "0.6.3-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: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
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": "^60.1.0",
82
+ "eslint-plugin-jsdoc": "^61.0.0",
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:xlsx-republish --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
+ }