lula2 0.7.5 → 0.8.4

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/BHkKokgA.js +1 -0
  4. package/dist/_app/immutable/chunks/{DsuS1uUo.js → BNu54jRO.js} +1 -1
  5. package/dist/_app/immutable/chunks/BzIr_GrC.js +79 -0
  6. package/dist/_app/immutable/chunks/CC6oS456.js +1 -0
  7. package/dist/_app/immutable/chunks/DHuA7MQr.js +1 -0
  8. package/dist/_app/immutable/chunks/{BIdjJ0zz.js → DJTXGU6C.js} +1 -1
  9. package/dist/_app/immutable/chunks/DSxRA67V.js +2 -0
  10. package/dist/_app/immutable/chunks/DpCtGpHu.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.CpHUD0XU.js} +2 -2
  15. package/dist/_app/immutable/entry/start.BavDkynd.js +1 -0
  16. package/dist/_app/immutable/nodes/{0.CGKh5y4X.js → 0.D5TULpJI.js} +2 -2
  17. package/dist/_app/immutable/nodes/1.BBgWG9H0.js +1 -0
  18. package/dist/_app/immutable/nodes/{2.Hrl6uq-b.js → 2.c2WlghKX.js} +1 -1
  19. package/dist/_app/immutable/nodes/3.hNTFAKFs.js +1 -0
  20. package/dist/_app/immutable/nodes/{4.DAVWsDkK.js → 4.C7MOPYAO.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 +2 -2
  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
@@ -92,561 +92,13 @@ var init_controlHelpers = __esm({
92
92
  }
93
93
  });
94
94
 
95
- // cli/server/infrastructure/fileStore.ts
96
- var fileStore_exports = {};
97
- __export(fileStore_exports, {
98
- FileStore: () => FileStore
99
- });
100
- import {
101
- existsSync as existsSync2,
102
- promises as fs,
103
- mkdirSync,
104
- readdirSync,
105
- readFileSync as readFileSync2,
106
- statSync,
107
- unlinkSync,
108
- writeFileSync
109
- } from "fs";
110
- import * as yaml2 from "js-yaml";
111
- import { join as join2 } from "path";
112
- import { createHash } from "crypto";
113
- var FileStore;
114
- var init_fileStore = __esm({
115
- "cli/server/infrastructure/fileStore.ts"() {
116
- "use strict";
117
- init_controlHelpers();
118
- FileStore = class {
119
- baseDir;
120
- controlsDir;
121
- mappingsDir;
122
- // Simple cache - just control ID to filename mapping
123
- controlMetadataCache = /* @__PURE__ */ new Map();
124
- constructor(options) {
125
- this.baseDir = options.baseDir;
126
- this.controlsDir = join2(this.baseDir, "controls");
127
- this.mappingsDir = join2(this.baseDir, "mappings");
128
- if (existsSync2(this.controlsDir)) {
129
- this.refreshControlsCache();
130
- }
131
- }
132
- /**
133
- * Get simple filename from control ID
134
- */
135
- getControlFilename(controlId) {
136
- const sanitized = controlId.replace(/^([A-Z]+)-(.*)/, (match, prefix, suffix) => {
137
- return `${prefix}-${suffix.replace(/[^\w]/g, "_")}`;
138
- });
139
- return `${sanitized}.yaml`;
140
- }
141
- /**
142
- * Get family name from control ID
143
- */
144
- getControlFamily(controlId) {
145
- return controlId.split("-")[0];
146
- }
147
- /**
148
- * Ensure required directories exist
149
- */
150
- ensureDirectories() {
151
- if (!this.baseDir || this.baseDir === "." || this.baseDir === process.cwd()) {
152
- return;
153
- }
154
- const lulaConfigPath2 = join2(this.baseDir, "lula.yaml");
155
- if (!existsSync2(lulaConfigPath2)) {
156
- return;
157
- }
158
- if (!existsSync2(this.controlsDir)) {
159
- mkdirSync(this.controlsDir, { recursive: true });
160
- }
161
- if (!existsSync2(this.mappingsDir)) {
162
- mkdirSync(this.mappingsDir, { recursive: true });
163
- }
164
- }
165
- /**
166
- * Get control metadata by ID
167
- */
168
- getControlMetadata(controlId) {
169
- return this.controlMetadataCache.get(controlId);
170
- }
171
- /**
172
- * Load a control by ID
173
- */
174
- async loadControl(controlId) {
175
- const sanitizedId = controlId.replace(/[^\w\-]/g, "_");
176
- const possibleFlatPaths = [
177
- join2(this.controlsDir, `${controlId}.yaml`),
178
- join2(this.controlsDir, `${sanitizedId}.yaml`)
179
- ];
180
- for (const flatFilePath of possibleFlatPaths) {
181
- if (existsSync2(flatFilePath)) {
182
- try {
183
- const content = readFileSync2(flatFilePath, "utf8");
184
- const parsed = yaml2.load(content);
185
- if (!parsed.id) {
186
- try {
187
- parsed.id = getControlId(parsed, this.baseDir);
188
- } catch {
189
- parsed.id = controlId;
190
- }
191
- }
192
- return parsed;
193
- } catch (error) {
194
- console.error(`Failed to load control ${controlId} from flat structure:`, error);
195
- throw new Error(
196
- `Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
197
- );
198
- }
199
- }
200
- }
201
- const family = this.getControlFamily(controlId);
202
- const familyDir = join2(this.controlsDir, family);
203
- const possibleFamilyPaths = [
204
- join2(familyDir, `${controlId}.yaml`),
205
- join2(familyDir, `${sanitizedId}.yaml`)
206
- ];
207
- for (const filePath of possibleFamilyPaths) {
208
- if (existsSync2(filePath)) {
209
- try {
210
- const content = readFileSync2(filePath, "utf8");
211
- const parsed = yaml2.load(content);
212
- if (!parsed.id) {
213
- try {
214
- parsed.id = getControlId(parsed, this.baseDir);
215
- } catch {
216
- parsed.id = controlId;
217
- }
218
- }
219
- return parsed;
220
- } catch (error) {
221
- console.error(`Failed to load control ${controlId}:`, error);
222
- throw new Error(
223
- `Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
224
- );
225
- }
226
- }
227
- }
228
- return null;
229
- }
230
- /**
231
- * Save a control
232
- */
233
- async saveControl(control) {
234
- this.ensureDirectories();
235
- const controlId = getControlId(control, this.baseDir);
236
- const family = this.getControlFamily(controlId);
237
- const filename = this.getControlFilename(controlId);
238
- const familyDir = join2(this.controlsDir, family);
239
- const filePath = join2(familyDir, filename);
240
- if (!existsSync2(familyDir)) {
241
- mkdirSync(familyDir, { recursive: true });
242
- }
243
- try {
244
- let yamlContent;
245
- if (existsSync2(filePath)) {
246
- const existingContent = readFileSync2(filePath, "utf8");
247
- const existingControl = yaml2.load(existingContent);
248
- const fieldsToUpdate = {};
249
- for (const key in control) {
250
- if (key === "timeline" || key === "unifiedHistory" || key === "_metadata" || key === "id") {
251
- continue;
252
- }
253
- if (JSON.stringify(control[key]) !== JSON.stringify(existingControl[key])) {
254
- fieldsToUpdate[key] = control[key];
255
- }
256
- }
257
- if (Object.keys(fieldsToUpdate).length > 0) {
258
- const updatedControl = { ...existingControl, ...fieldsToUpdate };
259
- yamlContent = yaml2.dump(updatedControl, {
260
- indent: 2,
261
- lineWidth: 80,
262
- noRefs: true,
263
- sortKeys: false
264
- });
265
- } else {
266
- yamlContent = existingContent;
267
- }
268
- } else {
269
- const controlToSave = { ...control };
270
- delete controlToSave.timeline;
271
- delete controlToSave.unifiedHistory;
272
- delete controlToSave._metadata;
273
- delete controlToSave.id;
274
- yamlContent = yaml2.dump(controlToSave, {
275
- indent: 2,
276
- lineWidth: 80,
277
- noRefs: true,
278
- sortKeys: false
279
- });
280
- }
281
- writeFileSync(filePath, yamlContent, "utf8");
282
- this.controlMetadataCache.set(controlId, {
283
- controlId,
284
- filename,
285
- family
286
- });
287
- } catch (error) {
288
- throw new Error(
289
- `Failed to save control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
290
- );
291
- }
292
- }
293
- /**
294
- * Delete a control
295
- */
296
- async deleteControl(controlId) {
297
- const family = this.getControlFamily(controlId);
298
- const filename = this.getControlFilename(controlId);
299
- const familyDir = join2(this.controlsDir, family);
300
- const filePath = join2(familyDir, filename);
301
- if (existsSync2(filePath)) {
302
- unlinkSync(filePath);
303
- this.controlMetadataCache.delete(controlId);
304
- }
305
- }
306
- /**
307
- * Load all controls
308
- */
309
- async loadAllControls() {
310
- if (!existsSync2(this.controlsDir)) {
311
- return [];
312
- }
313
- let controlOrder = null;
314
- try {
315
- const lulaConfigPath2 = join2(this.baseDir, "lula.yaml");
316
- if (existsSync2(lulaConfigPath2)) {
317
- const content = readFileSync2(lulaConfigPath2, "utf8");
318
- const metadata = yaml2.load(content);
319
- controlOrder = metadata?.controlOrder || null;
320
- }
321
- } catch (error) {
322
- console.error(`Failed to load lula.yaml for controlOrder (path: ${lulaConfigPath}):`, error);
323
- }
324
- const entries = readdirSync(this.controlsDir);
325
- const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
326
- if (yamlFiles.length > 0) {
327
- const promises = yamlFiles.map(async (file) => {
328
- try {
329
- const filePath = join2(this.controlsDir, file);
330
- const content = await fs.readFile(filePath, "utf8");
331
- const parsed = yaml2.load(content);
332
- if (!parsed.id) {
333
- parsed.id = getControlId(parsed, this.baseDir);
334
- }
335
- return parsed;
336
- } catch (error) {
337
- console.error(`Failed to load control from file ${file}:`, error);
338
- return null;
339
- }
340
- });
341
- const results2 = await Promise.all(promises);
342
- const controls2 = results2.filter((c) => c !== null);
343
- if (controlOrder && controlOrder.length > 0) {
344
- return this.sortControlsByOrder(controls2, controlOrder);
345
- }
346
- return controls2;
347
- }
348
- const families = entries.filter((name) => {
349
- const familyPath = join2(this.controlsDir, name);
350
- return statSync(familyPath).isDirectory();
351
- });
352
- const allPromises = [];
353
- for (const family of families) {
354
- const familyPath = join2(this.controlsDir, family);
355
- const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
356
- const familyPromises = files.map(async (file) => {
357
- try {
358
- const controlId = file.replace(".yaml", "");
359
- const control = await this.loadControl(controlId);
360
- return control;
361
- } catch (error) {
362
- console.error(`Failed to load control from file ${file}:`, error);
363
- return null;
364
- }
365
- });
366
- allPromises.push(...familyPromises);
367
- }
368
- const results = await Promise.all(allPromises);
369
- const controls = results.filter((c) => c !== null);
370
- if (controlOrder && controlOrder.length > 0) {
371
- return this.sortControlsByOrder(controls, controlOrder);
372
- }
373
- return controls;
374
- }
375
- /**
376
- * Sort controls based on the provided order array
377
- */
378
- sortControlsByOrder(controls, controlOrder) {
379
- const orderMap = /* @__PURE__ */ new Map();
380
- controlOrder.forEach((controlId, index) => {
381
- orderMap.set(controlId, index);
382
- });
383
- return controls.sort((a, b) => {
384
- const aIndex = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
385
- const bIndex = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
386
- return aIndex - bIndex;
387
- });
388
- }
389
- /**
390
- * Load mappings from mappings directory
391
- */
392
- async loadMappings() {
393
- const mappings = [];
394
- if (!existsSync2(this.mappingsDir)) {
395
- return mappings;
396
- }
397
- const families = readdirSync(this.mappingsDir).filter((name) => {
398
- const familyPath = join2(this.mappingsDir, name);
399
- return statSync(familyPath).isDirectory();
400
- });
401
- for (const family of families) {
402
- const familyPath = join2(this.mappingsDir, family);
403
- const files = readdirSync(familyPath).filter((file) => file.endsWith("-mappings.yaml"));
404
- for (const file of files) {
405
- const mappingFile = join2(familyPath, file);
406
- try {
407
- const content = readFileSync2(mappingFile, "utf8");
408
- const parsed = yaml2.load(content);
409
- if (Array.isArray(parsed)) {
410
- parsed.forEach((mapping) => {
411
- mapping.hash = createHash("sha256").update(JSON.stringify(mapping)).digest("hex");
412
- return mapping;
413
- });
414
- mappings.push(...parsed);
415
- }
416
- } catch (error) {
417
- console.error(`Failed to load mappings from ${family}/${file}:`, error);
418
- }
419
- }
420
- }
421
- return mappings;
422
- }
423
- /**
424
- * Save a single mapping
425
- */
426
- async saveMapping(mapping) {
427
- this.ensureDirectories();
428
- const controlId = mapping.control_id;
429
- const family = this.getControlFamily(controlId);
430
- const familyDir = join2(this.mappingsDir, family);
431
- const mappingFile = join2(
432
- familyDir,
433
- `${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
434
- );
435
- if (!existsSync2(familyDir)) {
436
- mkdirSync(familyDir, { recursive: true });
437
- }
438
- let existingMappings = [];
439
- if (existsSync2(mappingFile)) {
440
- try {
441
- const content = readFileSync2(mappingFile, "utf8");
442
- existingMappings = yaml2.load(content) || [];
443
- } catch (error) {
444
- console.error(`Failed to parse existing mappings file: ${mappingFile}`, error);
445
- existingMappings = [];
446
- }
447
- }
448
- const cleanMapping = { ...mapping };
449
- delete cleanMapping.hash;
450
- existingMappings.push(cleanMapping);
451
- try {
452
- const yamlContent = yaml2.dump(existingMappings, {
453
- indent: 2,
454
- lineWidth: -1,
455
- noRefs: true
456
- });
457
- writeFileSync(mappingFile, yamlContent, "utf8");
458
- } catch (error) {
459
- throw new Error(
460
- `Failed to save mapping for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
461
- );
462
- }
463
- }
464
- /**
465
- * Delete a single mapping
466
- */
467
- async deleteMapping(compositeKey) {
468
- const mappingFiles = this.getAllMappingFiles();
469
- for (const file of mappingFiles) {
470
- try {
471
- const content = readFileSync2(file, "utf8");
472
- let mappings = yaml2.load(content) || [];
473
- const originalLength = mappings.length;
474
- mappings = mappings.filter((m) => {
475
- const hash = createHash("sha256").update(JSON.stringify(m)).digest("hex");
476
- return `${m.control_id}:${hash}` !== compositeKey;
477
- });
478
- if (mappings.length < originalLength) {
479
- if (mappings.length === 0) {
480
- unlinkSync(file);
481
- } else {
482
- const yamlContent = yaml2.dump(mappings, {
483
- indent: 2,
484
- lineWidth: -1,
485
- noRefs: true
486
- });
487
- writeFileSync(file, yamlContent, "utf8");
488
- }
489
- return;
490
- }
491
- } catch (error) {
492
- console.error(`Error processing mapping file ${file}:`, error);
493
- }
494
- }
495
- }
496
- /**
497
- * Get all mapping files
498
- */
499
- getAllMappingFiles() {
500
- const files = [];
501
- if (!existsSync2(this.mappingsDir)) {
502
- return files;
503
- }
504
- const flatFiles = readdirSync(this.mappingsDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(this.mappingsDir, file));
505
- files.push(...flatFiles);
506
- const entries = readdirSync(this.mappingsDir, { withFileTypes: true });
507
- for (const entry of entries) {
508
- if (entry.isDirectory()) {
509
- const familyDir = join2(this.mappingsDir, entry.name);
510
- const familyFiles = readdirSync(familyDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(familyDir, file));
511
- files.push(...familyFiles);
512
- }
513
- }
514
- return files;
515
- }
516
- /**
517
- * Save mappings to per-control files
518
- */
519
- async saveMappings(mappings) {
520
- this.ensureDirectories();
521
- const mappingsByControl = /* @__PURE__ */ new Map();
522
- for (const mapping of mappings) {
523
- const controlId = mapping.control_id;
524
- if (!mappingsByControl.has(controlId)) {
525
- mappingsByControl.set(controlId, []);
526
- }
527
- mappingsByControl.get(controlId).push(mapping);
528
- }
529
- for (const [controlId, controlMappings] of mappingsByControl) {
530
- const family = this.getControlFamily(controlId);
531
- const familyDir = join2(this.mappingsDir, family);
532
- const mappingFile = join2(
533
- familyDir,
534
- `${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
535
- );
536
- if (!existsSync2(familyDir)) {
537
- mkdirSync(familyDir, { recursive: true });
538
- }
539
- const cleanMappings = controlMappings.map((m) => {
540
- const clean = { ...m };
541
- delete clean.hash;
542
- return clean;
543
- });
544
- try {
545
- const yamlContent = yaml2.dump(cleanMappings, {
546
- indent: 2,
547
- lineWidth: -1,
548
- noRefs: true
549
- });
550
- writeFileSync(mappingFile, yamlContent, "utf8");
551
- } catch (error) {
552
- throw new Error(
553
- `Failed to save mappings for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
554
- );
555
- }
556
- }
557
- }
558
- /**
559
- * Refresh controls cache
560
- */
561
- refreshControlsCache() {
562
- this.controlMetadataCache.clear();
563
- if (!existsSync2(this.controlsDir)) {
564
- return;
565
- }
566
- const entries = readdirSync(this.controlsDir);
567
- const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
568
- if (yamlFiles.length > 0) {
569
- for (const filename of yamlFiles) {
570
- const controlId = filename.replace(".yaml", "");
571
- const family = this.getControlFamily(controlId);
572
- this.controlMetadataCache.set(controlId, {
573
- controlId,
574
- filename,
575
- family
576
- });
577
- }
578
- return;
579
- }
580
- const families = entries.filter((name) => {
581
- const familyPath = join2(this.controlsDir, name);
582
- return statSync(familyPath).isDirectory();
583
- });
584
- for (const family of families) {
585
- const familyPath = join2(this.controlsDir, family);
586
- const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
587
- for (const filename of files) {
588
- try {
589
- const filePath = join2(familyPath, filename);
590
- const content = readFileSync2(filePath, "utf8");
591
- const parsed = yaml2.load(content);
592
- const controlId = getControlId(parsed, this.baseDir);
593
- this.controlMetadataCache.set(controlId, {
594
- controlId,
595
- filename,
596
- family
597
- });
598
- } catch (error) {
599
- console.error(`Failed to read control metadata from ${family}/${filename}:`, error);
600
- const controlId = filename.replace(".yaml", "").replace(/_/g, "/");
601
- this.controlMetadataCache.set(controlId, {
602
- controlId,
603
- filename,
604
- family
605
- });
606
- }
607
- }
608
- }
609
- }
610
- /**
611
- * Get file store statistics
612
- */
613
- getStats() {
614
- const controlCount = this.controlMetadataCache.size;
615
- let mappingCount = 0;
616
- if (existsSync2(this.mappingsDir)) {
617
- const families = readdirSync(this.mappingsDir).filter((name) => {
618
- const familyPath = join2(this.mappingsDir, name);
619
- return statSync(familyPath).isDirectory();
620
- });
621
- mappingCount = families.length;
622
- }
623
- const familyCount = new Set(
624
- Array.from(this.controlMetadataCache.values()).map((meta) => meta.family)
625
- ).size;
626
- return {
627
- controlCount,
628
- mappingCount,
629
- familyCount
630
- };
631
- }
632
- /**
633
- * Clear all caches
634
- */
635
- clearCache() {
636
- this.controlMetadataCache.clear();
637
- this.refreshControlsCache();
638
- }
639
- };
640
- }
641
- });
642
-
643
95
  // cli/server/infrastructure/yamlDiff.ts
644
- import * as yaml3 from "js-yaml";
96
+ import * as yaml2 from "js-yaml";
645
97
  function createYamlDiff(oldYaml, newYaml, isArrayFile = false) {
646
98
  try {
647
99
  const emptyDefault = isArrayFile ? "[]" : "{}";
648
- const oldData = yaml3.load(oldYaml || emptyDefault);
649
- const newData = yaml3.load(newYaml || emptyDefault);
100
+ const oldData = yaml2.load(oldYaml || emptyDefault);
101
+ const newData = yaml2.load(newYaml || emptyDefault);
650
102
  const changes = compareValues(oldData, newData, "");
651
103
  return {
652
104
  hasChanges: changes.length > 0,
@@ -834,616 +286,1198 @@ function compareMappingArrays(oldArr, newArr, basePath) {
834
286
  description: `Modified mapping`
835
287
  });
836
288
  }
837
- }
838
- }
839
- if (changes.length === 0 && oldArr.length !== newArr.length) {
840
- changes.push({
841
- type: "modified",
842
- path: basePath || "mappings",
843
- oldValue: oldArr,
844
- newValue: newArr,
845
- description: `Mappings changed from ${oldArr.length} to ${newArr.length} items`
846
- });
847
- }
848
- return changes;
849
- }
850
- function deepEqual(a, b) {
851
- if (a === b) return true;
852
- if (a === null || b === null) return false;
853
- if (typeof a !== typeof b) return false;
854
- if (Array.isArray(a) && Array.isArray(b)) {
855
- if (a.length !== b.length) return false;
856
- for (let i = 0; i < a.length; i++) {
857
- if (!deepEqual(a[i], b[i])) return false;
858
- }
859
- return true;
860
- }
861
- if (typeof a === "object" && typeof b === "object") {
862
- const aObj = a;
863
- const bObj = b;
864
- const aKeys = Object.keys(aObj);
865
- const bKeys = Object.keys(bObj);
866
- if (aKeys.length !== bKeys.length) return false;
867
- for (const key of aKeys) {
868
- if (!bKeys.includes(key)) return false;
869
- if (!deepEqual(aObj[key], bObj[key])) return false;
870
- }
871
- return true;
872
- }
873
- return false;
874
- }
875
- function generateSummary(changes) {
876
- if (changes.length === 0) {
877
- return "No changes detected";
878
- }
879
- const added = changes.filter((c) => c.type === "added").length;
880
- const removed = changes.filter((c) => c.type === "removed").length;
881
- const modified = changes.filter((c) => c.type === "modified").length;
882
- const parts = [];
883
- if (added > 0) parts.push(`${added} added`);
884
- if (removed > 0) parts.push(`${removed} removed`);
885
- if (modified > 0) parts.push(`${modified} modified`);
886
- return parts.join(", ");
887
- }
888
- var init_yamlDiff = __esm({
889
- "cli/server/infrastructure/yamlDiff.ts"() {
890
- "use strict";
891
- }
892
- });
893
-
894
- // cli/server/infrastructure/gitHistory.ts
895
- import * as fs2 from "fs";
896
- import * as git from "isomorphic-git";
897
- import { relative } from "path";
898
- import { execSync } from "child_process";
899
- var GitHistoryUtil;
900
- var init_gitHistory = __esm({
901
- "cli/server/infrastructure/gitHistory.ts"() {
902
- "use strict";
903
- init_yamlDiff();
904
- GitHistoryUtil = class {
905
- baseDir;
906
- execSync;
907
- constructor(baseDir, execSyncFn) {
908
- this.baseDir = baseDir;
909
- this.execSync = execSyncFn || execSync;
289
+ }
290
+ }
291
+ if (changes.length === 0 && oldArr.length !== newArr.length) {
292
+ changes.push({
293
+ type: "modified",
294
+ path: basePath || "mappings",
295
+ oldValue: oldArr,
296
+ newValue: newArr,
297
+ description: `Mappings changed from ${oldArr.length} to ${newArr.length} items`
298
+ });
299
+ }
300
+ return changes;
301
+ }
302
+ function deepEqual(a, b) {
303
+ if (a === b) return true;
304
+ if (a === null || b === null) return false;
305
+ if (typeof a !== typeof b) return false;
306
+ if (Array.isArray(a) && Array.isArray(b)) {
307
+ if (a.length !== b.length) return false;
308
+ for (let i = 0; i < a.length; i++) {
309
+ if (!deepEqual(a[i], b[i])) return false;
310
+ }
311
+ return true;
312
+ }
313
+ if (typeof a === "object" && typeof b === "object") {
314
+ const aObj = a;
315
+ const bObj = b;
316
+ const aKeys = Object.keys(aObj);
317
+ const bKeys = Object.keys(bObj);
318
+ if (aKeys.length !== bKeys.length) return false;
319
+ for (const key of aKeys) {
320
+ if (!bKeys.includes(key)) return false;
321
+ if (!deepEqual(aObj[key], bObj[key])) return false;
322
+ }
323
+ return true;
324
+ }
325
+ return false;
326
+ }
327
+ function generateSummary(changes) {
328
+ if (changes.length === 0) {
329
+ return "No changes detected";
330
+ }
331
+ const added = changes.filter((c) => c.type === "added").length;
332
+ const removed = changes.filter((c) => c.type === "removed").length;
333
+ const modified = changes.filter((c) => c.type === "modified").length;
334
+ const parts = [];
335
+ if (added > 0) parts.push(`${added} added`);
336
+ if (removed > 0) parts.push(`${removed} removed`);
337
+ if (modified > 0) parts.push(`${modified} modified`);
338
+ return parts.join(", ");
339
+ }
340
+ var init_yamlDiff = __esm({
341
+ "cli/server/infrastructure/yamlDiff.ts"() {
342
+ "use strict";
343
+ }
344
+ });
345
+
346
+ // cli/server/infrastructure/gitHistory.ts
347
+ import * as fs from "fs";
348
+ import * as git from "isomorphic-git";
349
+ import { relative } from "path";
350
+ import { execSync } from "child_process";
351
+ var GitHistoryUtil;
352
+ var init_gitHistory = __esm({
353
+ "cli/server/infrastructure/gitHistory.ts"() {
354
+ "use strict";
355
+ init_yamlDiff();
356
+ GitHistoryUtil = class {
357
+ baseDir;
358
+ execSync;
359
+ constructor(baseDir, execSyncFn) {
360
+ this.baseDir = baseDir;
361
+ this.execSync = execSyncFn || execSync;
362
+ }
363
+ /**
364
+ * Execute a git command using native git binary with credentials support
365
+ */
366
+ async executeGitCommand(command, cwd) {
367
+ try {
368
+ const workingDir = cwd || await git.findRoot({ fs, filepath: process.cwd() });
369
+ const output = this.execSync(command, {
370
+ cwd: workingDir,
371
+ encoding: "utf8",
372
+ stdio: ["pipe", "pipe", "pipe"]
373
+ });
374
+ return { success: true, output: output.toString() };
375
+ } catch (error) {
376
+ return {
377
+ success: false,
378
+ output: "",
379
+ error: error.stderr?.toString() || error.message
380
+ };
381
+ }
382
+ }
383
+ /**
384
+ * Check if the directory is a git repository
385
+ */
386
+ async isGitRepository() {
387
+ try {
388
+ const gitDir = await git.findRoot({ fs, filepath: process.cwd() });
389
+ return !!gitDir;
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+ /**
395
+ * Get git history for a specific file
396
+ */
397
+ async getFileHistory(filePath, limit = 50) {
398
+ const isGitRepo = await this.isGitRepository();
399
+ if (!isGitRepo) {
400
+ return {
401
+ filePath,
402
+ commits: [],
403
+ totalCommits: 0,
404
+ firstCommit: null,
405
+ lastCommit: null
406
+ };
407
+ }
408
+ try {
409
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
410
+ const relativePath = relative(gitRoot, filePath);
411
+ const commits = await git.log({
412
+ fs,
413
+ dir: gitRoot,
414
+ filepath: relativePath,
415
+ depth: limit
416
+ });
417
+ if (!commits || commits.length === 0) {
418
+ return {
419
+ filePath,
420
+ commits: [],
421
+ totalCommits: 0,
422
+ firstCommit: null,
423
+ lastCommit: null
424
+ };
425
+ }
426
+ const gitCommits = await this.convertIsomorphicCommits(commits, relativePath, gitRoot);
427
+ return {
428
+ filePath,
429
+ commits: gitCommits,
430
+ totalCommits: gitCommits.length,
431
+ firstCommit: gitCommits[gitCommits.length - 1] || null,
432
+ lastCommit: gitCommits[0] || null
433
+ };
434
+ } catch (error) {
435
+ const err = error;
436
+ if (err?.code === "NotFoundError" || err?.message?.includes("Could not find file")) {
437
+ return {
438
+ filePath,
439
+ commits: [],
440
+ totalCommits: 0,
441
+ firstCommit: null,
442
+ lastCommit: null
443
+ };
444
+ }
445
+ console.error(`Unexpected error getting git history for ${filePath}:`, error);
446
+ return {
447
+ filePath,
448
+ commits: [],
449
+ totalCommits: 0,
450
+ firstCommit: null,
451
+ lastCommit: null
452
+ };
453
+ }
454
+ }
455
+ /**
456
+ * Get the total number of commits for a file
457
+ */
458
+ async getFileCommitCount(filePath) {
459
+ const isGitRepo = await this.isGitRepository();
460
+ if (!isGitRepo) {
461
+ return 0;
462
+ }
463
+ try {
464
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
465
+ const relativePath = relative(gitRoot, filePath);
466
+ const commits = await git.log({
467
+ fs,
468
+ dir: gitRoot,
469
+ filepath: relativePath
470
+ });
471
+ return commits.length;
472
+ } catch {
473
+ return 0;
474
+ }
475
+ }
476
+ /**
477
+ * Get the latest commit info for a file
478
+ */
479
+ async getLatestCommit(filePath) {
480
+ const history = await this.getFileHistory(filePath, 1);
481
+ return history.lastCommit;
482
+ }
483
+ /**
484
+ * Get file content at a specific commit (public method)
485
+ */
486
+ async getFileContentAtCommit(filePath, commitHash) {
487
+ const isGitRepo = await this.isGitRepository();
488
+ if (!isGitRepo) {
489
+ return null;
490
+ }
491
+ try {
492
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
493
+ const relativePath = relative(gitRoot, filePath);
494
+ return await this.getFileAtCommit(commitHash, relativePath, gitRoot);
495
+ } catch (error) {
496
+ console.error(`Error getting file content at commit ${commitHash}:`, error);
497
+ return null;
498
+ }
499
+ }
500
+ /**
501
+ * Convert isomorphic-git commits to our GitCommit format
502
+ */
503
+ async convertIsomorphicCommits(commits, relativePath, gitRoot) {
504
+ const gitCommits = [];
505
+ for (let i = 0; i < commits.length; i++) {
506
+ const commit = commits[i];
507
+ const includeDiff = i < 5;
508
+ let changes = { insertions: 0, deletions: 0, files: 1 };
509
+ let diff;
510
+ let yamlDiff;
511
+ if (includeDiff) {
512
+ try {
513
+ const diffResult = await this.getCommitDiff(commit.oid, relativePath, gitRoot);
514
+ changes = diffResult.changes;
515
+ diff = diffResult.diff;
516
+ yamlDiff = diffResult.yamlDiff;
517
+ } catch {
518
+ }
519
+ }
520
+ gitCommits.push({
521
+ hash: commit.oid,
522
+ shortHash: commit.oid.substring(0, 7),
523
+ author: commit.commit.author.name,
524
+ authorEmail: commit.commit.author.email,
525
+ date: new Date(commit.commit.author.timestamp * 1e3).toISOString(),
526
+ message: commit.commit.message,
527
+ changes,
528
+ ...diff && { diff },
529
+ ...yamlDiff && { yamlDiff }
530
+ });
531
+ }
532
+ return gitCommits;
533
+ }
534
+ /**
535
+ * Get diff for a specific commit and file
536
+ */
537
+ async getCommitDiff(commitOid, relativePath, gitRoot) {
538
+ try {
539
+ const commit = await git.readCommit({ fs, dir: gitRoot, oid: commitOid });
540
+ const parentOid = commit.commit.parent.length > 0 ? commit.commit.parent[0] : null;
541
+ if (!parentOid) {
542
+ const currentContent2 = await this.getFileAtCommit(commitOid, relativePath, gitRoot);
543
+ if (currentContent2) {
544
+ const lines = currentContent2.split("\n");
545
+ const isMappingFile2 = relativePath.includes("-mappings.yaml");
546
+ const yamlDiff2 = createYamlDiff("", currentContent2, isMappingFile2);
547
+ return {
548
+ changes: { insertions: lines.length, deletions: 0, files: 1 },
549
+ diff: `--- /dev/null
550
+ +++ b/${relativePath}
551
+ @@ -0,0 +1,${lines.length} @@
552
+ ` + lines.map((line) => "+" + line).join("\n"),
553
+ yamlDiff: yamlDiff2
554
+ };
555
+ }
556
+ return { changes: { insertions: 0, deletions: 0, files: 1 } };
557
+ }
558
+ const currentContent = await this.getFileAtCommit(commitOid, relativePath, gitRoot);
559
+ const parentContent = await this.getFileAtCommit(parentOid, relativePath, gitRoot);
560
+ if (!currentContent && !parentContent) {
561
+ return { changes: { insertions: 0, deletions: 0, files: 1 } };
562
+ }
563
+ const currentLines = currentContent ? currentContent.split("\n") : [];
564
+ const parentLines = parentContent ? parentContent.split("\n") : [];
565
+ const diff = await this.createSimpleDiff(parentLines, currentLines, relativePath);
566
+ const { insertions, deletions } = this.countChanges(parentLines, currentLines);
567
+ const isMappingFile = relativePath.includes("-mappings.yaml");
568
+ const yamlDiff = createYamlDiff(parentContent || "", currentContent || "", isMappingFile);
569
+ return {
570
+ changes: { insertions, deletions, files: 1 },
571
+ diff,
572
+ yamlDiff
573
+ };
574
+ } catch {
575
+ return { changes: { insertions: 0, deletions: 0, files: 1 } };
576
+ }
577
+ }
578
+ /**
579
+ * Get file content at a specific commit
580
+ */
581
+ async getFileAtCommit(commitOid, filepath, gitRoot) {
582
+ try {
583
+ const { blob } = await git.readBlob({
584
+ fs,
585
+ dir: gitRoot,
586
+ oid: commitOid,
587
+ filepath
588
+ });
589
+ return new TextDecoder().decode(blob);
590
+ } catch (_error) {
591
+ return null;
592
+ }
910
593
  }
911
594
  /**
912
- * Execute a git command using native git binary with credentials support
595
+ * Create a simple unified diff between two file versions
913
596
  */
914
- async executeGitCommand(command, cwd) {
597
+ async createSimpleDiff(oldLines, newLines, filepath) {
598
+ const diffLines = [];
599
+ diffLines.push(`--- a/${filepath}`);
600
+ diffLines.push(`+++ b/${filepath}`);
601
+ const oldCount = oldLines.length;
602
+ const newCount = newLines.length;
603
+ diffLines.push(`@@ -1,${oldCount} +1,${newCount} @@`);
604
+ let i = 0, j = 0;
605
+ while (i < oldLines.length || j < newLines.length) {
606
+ if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
607
+ diffLines.push(` ${oldLines[i]}`);
608
+ i++;
609
+ j++;
610
+ } else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
611
+ diffLines.push(`-${oldLines[i]}`);
612
+ i++;
613
+ } else if (j < newLines.length) {
614
+ diffLines.push(`+${newLines[j]}`);
615
+ j++;
616
+ }
617
+ }
618
+ return diffLines.join("\n");
619
+ }
620
+ /**
621
+ * Count insertions and deletions between two file versions
622
+ */
623
+ countChanges(oldLines, newLines) {
624
+ let insertions = 0;
625
+ let deletions = 0;
626
+ const maxLines = Math.max(oldLines.length, newLines.length);
627
+ for (let i = 0; i < maxLines; i++) {
628
+ const oldLine = i < oldLines.length ? oldLines[i] : null;
629
+ const newLine = i < newLines.length ? newLines[i] : null;
630
+ if (oldLine === null) {
631
+ insertions++;
632
+ } else if (newLine === null) {
633
+ deletions++;
634
+ } else if (oldLine !== newLine) {
635
+ insertions++;
636
+ deletions++;
637
+ }
638
+ }
639
+ return { insertions, deletions };
640
+ }
641
+ /**
642
+ * Get git stats for the entire repository
643
+ */
644
+ async getRepositoryStats() {
645
+ const isGitRepo = await this.isGitRepository();
646
+ if (!isGitRepo) {
647
+ return {
648
+ totalCommits: 0,
649
+ contributors: 0,
650
+ lastCommitDate: null,
651
+ firstCommitDate: null
652
+ };
653
+ }
915
654
  try {
916
- const workingDir = cwd || await git.findRoot({ fs: fs2, filepath: process.cwd() });
917
- const output = this.execSync(command, {
918
- cwd: workingDir,
919
- encoding: "utf8",
920
- stdio: ["pipe", "pipe", "pipe"]
655
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
656
+ const commits = await git.log({ fs, dir: gitRoot });
657
+ const contributorEmails = /* @__PURE__ */ new Set();
658
+ commits.forEach((commit) => {
659
+ contributorEmails.add(commit.commit.author.email);
921
660
  });
922
- return { success: true, output: output.toString() };
661
+ const firstCommit = commits[commits.length - 1];
662
+ const lastCommit = commits[0];
663
+ return {
664
+ totalCommits: commits.length,
665
+ contributors: contributorEmails.size,
666
+ lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
667
+ firstCommitDate: firstCommit ? new Date(firstCommit.commit.author.timestamp * 1e3).toISOString() : null
668
+ };
923
669
  } catch (error) {
670
+ console.error("Error getting repository stats:", error);
924
671
  return {
925
- success: false,
926
- output: "",
927
- error: error.stderr?.toString() || error.message
672
+ totalCommits: 0,
673
+ contributors: 0,
674
+ lastCommitDate: null,
675
+ firstCommitDate: null
676
+ };
677
+ }
678
+ }
679
+ /**
680
+ * Get current branch name
681
+ */
682
+ async getCurrentBranch() {
683
+ try {
684
+ const isGitRepo = await this.isGitRepository();
685
+ if (!isGitRepo) {
686
+ return null;
687
+ }
688
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
689
+ const branch = await git.currentBranch({ fs, dir: gitRoot });
690
+ return branch || null;
691
+ } catch (error) {
692
+ console.error("Error getting current branch:", error);
693
+ return null;
694
+ }
695
+ }
696
+ /**
697
+ * Get git status information
698
+ */
699
+ async getGitStatus() {
700
+ try {
701
+ const isGitRepo = await this.isGitRepository();
702
+ if (!isGitRepo) {
703
+ return {
704
+ isGitRepository: false,
705
+ currentBranch: null,
706
+ branchInfo: null,
707
+ canPull: false,
708
+ canPush: false
709
+ };
710
+ }
711
+ const currentBranch2 = await this.getCurrentBranch();
712
+ if (!currentBranch2) {
713
+ return {
714
+ isGitRepository: true,
715
+ currentBranch: null,
716
+ branchInfo: null,
717
+ canPull: false,
718
+ canPush: false
719
+ };
720
+ }
721
+ const branchInfo = await this.getBranchInfo(currentBranch2);
722
+ return {
723
+ isGitRepository: true,
724
+ currentBranch: currentBranch2,
725
+ branchInfo,
726
+ canPull: branchInfo?.isBehind || false,
727
+ canPush: branchInfo?.isAhead || false
728
+ };
729
+ } catch (error) {
730
+ console.error("Error getting git status:", error);
731
+ return {
732
+ isGitRepository: false,
733
+ currentBranch: null,
734
+ branchInfo: null,
735
+ canPull: false,
736
+ canPush: false
928
737
  };
929
738
  }
930
739
  }
931
740
  /**
932
- * Check if the directory is a git repository
741
+ * Get branch comparison information
933
742
  */
934
- async isGitRepository() {
743
+ async getBranchInfo(branchName) {
935
744
  try {
936
- const gitDir = await git.findRoot({ fs: fs2, filepath: process.cwd() });
937
- return !!gitDir;
938
- } catch {
939
- return false;
745
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
746
+ const localCommits = await git.log({ fs, dir: gitRoot, ref: branchName });
747
+ try {
748
+ const remotes = await git.listRemotes({ fs, dir: gitRoot });
749
+ for (const remote of remotes) {
750
+ try {
751
+ const fetchResult = await this.executeGitCommand(`git fetch ${remote.remote}`, gitRoot);
752
+ if (!fetchResult.success) {
753
+ console.warn(`Could not fetch from remote ${remote.remote}:`, fetchResult.error);
754
+ }
755
+ } catch (fetchError) {
756
+ console.warn(`Could not fetch from remote ${remote.remote}:`, fetchError);
757
+ }
758
+ }
759
+ let remoteCommits = [];
760
+ let foundRemote = false;
761
+ for (const remote of remotes) {
762
+ const remoteBranchRef = `${remote.remote}/${branchName}`;
763
+ try {
764
+ remoteCommits = await git.log({ fs, dir: gitRoot, ref: remoteBranchRef });
765
+ foundRemote = true;
766
+ break;
767
+ } catch (error) {
768
+ console.warn(`Could not get commits for ${remoteBranchRef}: ${error}`);
769
+ }
770
+ }
771
+ if (!foundRemote || remoteCommits.length === 0) {
772
+ const lastCommit2 = localCommits[0];
773
+ return {
774
+ currentBranch: branchName,
775
+ isAhead: false,
776
+ isBehind: false,
777
+ aheadCount: 0,
778
+ behindCount: 0,
779
+ lastCommitDate: lastCommit2 ? new Date(lastCommit2.commit.author.timestamp * 1e3).toISOString() : null,
780
+ lastCommitMessage: lastCommit2?.commit.message || null,
781
+ hasUnpushedChanges: false
782
+ };
783
+ }
784
+ const localHashes = new Set(localCommits.map((c) => c.oid));
785
+ const remoteHashes = new Set(remoteCommits.map((c) => c.oid));
786
+ const aheadCount = localCommits.filter((c) => !remoteHashes.has(c.oid)).length;
787
+ const behindCount = remoteCommits.filter((c) => !localHashes.has(c.oid)).length;
788
+ const lastCommit = localCommits[0];
789
+ return {
790
+ currentBranch: branchName,
791
+ isAhead: aheadCount > 0,
792
+ isBehind: behindCount > 0,
793
+ aheadCount,
794
+ behindCount,
795
+ lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
796
+ lastCommitMessage: lastCommit?.commit.message || null,
797
+ hasUnpushedChanges: aheadCount > 0
798
+ };
799
+ } catch {
800
+ const lastCommit = localCommits[0];
801
+ return {
802
+ currentBranch: branchName,
803
+ isAhead: false,
804
+ isBehind: false,
805
+ aheadCount: 0,
806
+ behindCount: 0,
807
+ lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
808
+ lastCommitMessage: lastCommit?.commit.message || null,
809
+ hasUnpushedChanges: false
810
+ };
811
+ }
812
+ } catch (error) {
813
+ console.error("Error getting branch info:", error);
814
+ return null;
940
815
  }
941
816
  }
942
817
  /**
943
- * Get git history for a specific file
818
+ * Fetch updates from remote repositories using native git command
944
819
  */
945
- async getFileHistory(filePath, limit = 50) {
946
- const isGitRepo = await this.isGitRepository();
947
- if (!isGitRepo) {
820
+ async fetchFromRemotes() {
821
+ try {
822
+ const isGitRepo = await this.isGitRepository();
823
+ if (!isGitRepo) {
824
+ return { success: false, message: "Not a git repository", details: [] };
825
+ }
826
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
827
+ const remotes = await git.listRemotes({ fs, dir: gitRoot });
828
+ if (remotes.length === 0) {
829
+ return { success: true, message: "No remotes configured", details: [] };
830
+ }
831
+ const details = [];
832
+ let hasErrors = false;
833
+ for (const remote of remotes) {
834
+ try {
835
+ const fetchResult = await this.executeGitCommand(`git fetch ${remote.remote}`, gitRoot);
836
+ if (fetchResult.success) {
837
+ details.push(`Fetched from ${remote.remote}`);
838
+ } else {
839
+ details.push(`Failed to fetch from ${remote.remote}: ${fetchResult.error}`);
840
+ hasErrors = true;
841
+ }
842
+ } catch (error) {
843
+ details.push(`Error fetching from ${remote.remote}: ${error}`);
844
+ hasErrors = true;
845
+ }
846
+ }
948
847
  return {
949
- filePath,
950
- commits: [],
951
- totalCommits: 0,
952
- firstCommit: null,
953
- lastCommit: null
848
+ success: !hasErrors,
849
+ message: hasErrors ? "Fetch completed with some errors" : "Successfully fetched from all remotes",
850
+ details
851
+ };
852
+ } catch (error) {
853
+ console.error("Error fetching from remotes:", error);
854
+ return {
855
+ success: false,
856
+ message: error instanceof Error ? error.message : "Unknown error occurred",
857
+ details: []
954
858
  };
955
859
  }
860
+ }
861
+ /**
862
+ * Pull changes from remote using native git command
863
+ */
864
+ async pullChanges() {
956
865
  try {
957
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
958
- const relativePath = relative(gitRoot, filePath);
959
- const commits = await git.log({
960
- fs: fs2,
961
- dir: gitRoot,
962
- filepath: relativePath,
963
- depth: limit
964
- });
965
- if (!commits || commits.length === 0) {
866
+ const isGitRepo = await this.isGitRepository();
867
+ if (!isGitRepo) {
868
+ return { success: false, message: "Not a git repository" };
869
+ }
870
+ const currentBranch2 = await this.getCurrentBranch();
871
+ if (!currentBranch2) {
872
+ return { success: false, message: "No current branch found" };
873
+ }
874
+ const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
875
+ const remotes = await git.listRemotes({ fs, dir: gitRoot });
876
+ if (remotes.length === 0) {
877
+ return { success: false, message: "No remotes configured" };
878
+ }
879
+ const targetRemote = remotes[0].remote;
880
+ const pullCommand = `git pull ${targetRemote} ${currentBranch2}`;
881
+ const pullResult = await this.executeGitCommand(pullCommand, gitRoot);
882
+ if (!pullResult.success) {
966
883
  return {
967
- filePath,
968
- commits: [],
969
- totalCommits: 0,
970
- firstCommit: null,
971
- lastCommit: null
884
+ success: false,
885
+ message: `Failed to pull changes: ${pullResult.error}`
972
886
  };
973
887
  }
974
- const gitCommits = await this.convertIsomorphicCommits(commits, relativePath, gitRoot);
975
888
  return {
976
- filePath,
977
- commits: gitCommits,
978
- totalCommits: gitCommits.length,
979
- firstCommit: gitCommits[gitCommits.length - 1] || null,
980
- lastCommit: gitCommits[0] || null
889
+ success: true,
890
+ message: pullResult.output || "Successfully pulled changes"
981
891
  };
982
892
  } catch (error) {
983
- const err = error;
984
- if (err?.code === "NotFoundError" || err?.message?.includes("Could not find file")) {
985
- return {
986
- filePath,
987
- commits: [],
988
- totalCommits: 0,
989
- firstCommit: null,
990
- lastCommit: null
991
- };
992
- }
993
- console.error(`Unexpected error getting git history for ${filePath}:`, error);
893
+ console.error("Error pulling changes:", error);
994
894
  return {
995
- filePath,
996
- commits: [],
997
- totalCommits: 0,
998
- firstCommit: null,
999
- lastCommit: null
895
+ success: false,
896
+ message: error instanceof Error ? error.message : "Unknown error occurred"
1000
897
  };
1001
898
  }
1002
899
  }
900
+ };
901
+ }
902
+ });
903
+
904
+ // cli/server/infrastructure/fileStore.ts
905
+ var fileStore_exports = {};
906
+ __export(fileStore_exports, {
907
+ FileStore: () => FileStore
908
+ });
909
+ import { createHash } from "crypto";
910
+ import {
911
+ existsSync as existsSync2,
912
+ promises as fs2,
913
+ mkdirSync,
914
+ readdirSync,
915
+ readFileSync as readFileSync2,
916
+ statSync,
917
+ unlinkSync,
918
+ writeFileSync
919
+ } from "fs";
920
+ import * as yaml3 from "js-yaml";
921
+ import { join as join2 } from "path";
922
+ var FileStore;
923
+ var init_fileStore = __esm({
924
+ "cli/server/infrastructure/fileStore.ts"() {
925
+ "use strict";
926
+ init_controlHelpers();
927
+ FileStore = class {
928
+ baseDir;
929
+ controlsDir;
930
+ mappingsDir;
931
+ // Simple cache - just control ID to filename mapping
932
+ controlMetadataCache = /* @__PURE__ */ new Map();
933
+ constructor(options) {
934
+ this.baseDir = options.baseDir;
935
+ this.controlsDir = join2(this.baseDir, "controls");
936
+ this.mappingsDir = join2(this.baseDir, "mappings");
937
+ if (existsSync2(this.controlsDir)) {
938
+ this.refreshControlsCache();
939
+ }
940
+ }
1003
941
  /**
1004
- * Get the total number of commits for a file
942
+ * Update a single mapping in place, preserving file order
1005
943
  */
1006
- async getFileCommitCount(filePath) {
1007
- const isGitRepo = await this.isGitRepository();
1008
- if (!isGitRepo) {
1009
- return 0;
1010
- }
1011
- try {
1012
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1013
- const relativePath = relative(gitRoot, filePath);
1014
- const commits = await git.log({
1015
- fs: fs2,
1016
- dir: gitRoot,
1017
- filepath: relativePath
1018
- });
1019
- return commits.length;
1020
- } catch {
1021
- return 0;
944
+ async updateMapping(oldCompositeKey, updatedMapping) {
945
+ const mappingFiles = this.getAllMappingFiles();
946
+ for (const file of mappingFiles) {
947
+ try {
948
+ const content = readFileSync2(file, "utf8");
949
+ let mappings = yaml3.load(content) || [];
950
+ let changed = false;
951
+ mappings = mappings.map((m) => {
952
+ const hash = createHash("sha256").update(JSON.stringify(m)).digest("hex");
953
+ if (`${m.control_id}:${hash}` === oldCompositeKey) {
954
+ const clean = { ...updatedMapping };
955
+ delete clean.hash;
956
+ changed = true;
957
+ return clean;
958
+ }
959
+ return m;
960
+ });
961
+ if (changed) {
962
+ const yamlContent = yaml3.dump(mappings, {
963
+ indent: 2,
964
+ lineWidth: -1,
965
+ noRefs: true
966
+ });
967
+ writeFileSync(file, yamlContent, "utf8");
968
+ return;
969
+ }
970
+ } catch (error) {
971
+ console.error(`Error processing mapping file ${file}:`, error);
972
+ }
1022
973
  }
1023
974
  }
1024
975
  /**
1025
- * Get the latest commit info for a file
976
+ * Get simple filename from control ID
977
+ */
978
+ getControlFilename(controlId) {
979
+ const sanitized = controlId.replace(/^([A-Z]+)-(.*)/, (match, prefix, suffix) => {
980
+ return `${prefix}-${suffix.replace(/[^\w]/g, "_")}`;
981
+ });
982
+ return `${sanitized}.yaml`;
983
+ }
984
+ /**
985
+ * Get family name from control ID
1026
986
  */
1027
- async getLatestCommit(filePath) {
1028
- const history = await this.getFileHistory(filePath, 1);
1029
- return history.lastCommit;
987
+ getControlFamily(controlId) {
988
+ return controlId.split("-")[0];
1030
989
  }
1031
990
  /**
1032
- * Get file content at a specific commit (public method)
991
+ * Ensure required directories exist
1033
992
  */
1034
- async getFileContentAtCommit(filePath, commitHash) {
1035
- const isGitRepo = await this.isGitRepository();
1036
- if (!isGitRepo) {
1037
- return null;
993
+ ensureDirectories() {
994
+ if (!this.baseDir || this.baseDir === "." || this.baseDir === process.cwd()) {
995
+ return;
1038
996
  }
1039
- try {
1040
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1041
- const relativePath = relative(gitRoot, filePath);
1042
- return await this.getFileAtCommit(commitHash, relativePath, gitRoot);
1043
- } catch (error) {
1044
- console.error(`Error getting file content at commit ${commitHash}:`, error);
1045
- return null;
997
+ const lulaConfigPath = join2(this.baseDir, "lula.yaml");
998
+ if (!existsSync2(lulaConfigPath)) {
999
+ return;
1000
+ }
1001
+ if (!existsSync2(this.controlsDir)) {
1002
+ mkdirSync(this.controlsDir, { recursive: true });
1003
+ }
1004
+ if (!existsSync2(this.mappingsDir)) {
1005
+ mkdirSync(this.mappingsDir, { recursive: true });
1046
1006
  }
1047
1007
  }
1048
1008
  /**
1049
- * Convert isomorphic-git commits to our GitCommit format
1009
+ * Get control metadata by ID
1050
1010
  */
1051
- async convertIsomorphicCommits(commits, relativePath, gitRoot) {
1052
- const gitCommits = [];
1053
- for (let i = 0; i < commits.length; i++) {
1054
- const commit = commits[i];
1055
- const includeDiff = i < 5;
1056
- let changes = { insertions: 0, deletions: 0, files: 1 };
1057
- let diff;
1058
- let yamlDiff;
1059
- if (includeDiff) {
1011
+ getControlMetadata(controlId) {
1012
+ return this.controlMetadataCache.get(controlId);
1013
+ }
1014
+ /**
1015
+ * Load a control by ID
1016
+ */
1017
+ async loadControl(controlId) {
1018
+ const sanitizedId = controlId.replace(/[^\w\-]/g, "_");
1019
+ const possibleFlatPaths = [
1020
+ join2(this.controlsDir, `${controlId}.yaml`),
1021
+ join2(this.controlsDir, `${sanitizedId}.yaml`)
1022
+ ];
1023
+ for (const flatFilePath of possibleFlatPaths) {
1024
+ if (existsSync2(flatFilePath)) {
1060
1025
  try {
1061
- const diffResult = await this.getCommitDiff(commit.oid, relativePath, gitRoot);
1062
- changes = diffResult.changes;
1063
- diff = diffResult.diff;
1064
- yamlDiff = diffResult.yamlDiff;
1065
- } catch {
1026
+ const content = readFileSync2(flatFilePath, "utf8");
1027
+ const parsed = yaml3.load(content);
1028
+ if (!parsed.id) {
1029
+ try {
1030
+ parsed.id = getControlId(parsed, this.baseDir);
1031
+ } catch {
1032
+ parsed.id = controlId;
1033
+ }
1034
+ }
1035
+ return parsed;
1036
+ } catch (error) {
1037
+ console.error(`Failed to load control ${controlId} from flat structure:`, error);
1038
+ throw new Error(
1039
+ `Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
1040
+ );
1066
1041
  }
1067
1042
  }
1068
- gitCommits.push({
1069
- hash: commit.oid,
1070
- shortHash: commit.oid.substring(0, 7),
1071
- author: commit.commit.author.name,
1072
- authorEmail: commit.commit.author.email,
1073
- date: new Date(commit.commit.author.timestamp * 1e3).toISOString(),
1074
- message: commit.commit.message,
1075
- changes,
1076
- ...diff && { diff },
1077
- ...yamlDiff && { yamlDiff }
1078
- });
1079
1043
  }
1080
- return gitCommits;
1044
+ const family = this.getControlFamily(controlId);
1045
+ const familyDir = join2(this.controlsDir, family);
1046
+ const possibleFamilyPaths = [
1047
+ join2(familyDir, `${controlId}.yaml`),
1048
+ join2(familyDir, `${sanitizedId}.yaml`)
1049
+ ];
1050
+ for (const filePath of possibleFamilyPaths) {
1051
+ if (existsSync2(filePath)) {
1052
+ try {
1053
+ const content = readFileSync2(filePath, "utf8");
1054
+ const parsed = yaml3.load(content);
1055
+ if (!parsed.id) {
1056
+ try {
1057
+ parsed.id = getControlId(parsed, this.baseDir);
1058
+ } catch {
1059
+ parsed.id = controlId;
1060
+ }
1061
+ }
1062
+ return parsed;
1063
+ } catch (error) {
1064
+ console.error(`Failed to load control ${controlId}:`, error);
1065
+ throw new Error(
1066
+ `Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
1067
+ );
1068
+ }
1069
+ }
1070
+ }
1071
+ return null;
1081
1072
  }
1082
1073
  /**
1083
- * Get diff for a specific commit and file
1074
+ * Save a control
1084
1075
  */
1085
- async getCommitDiff(commitOid, relativePath, gitRoot) {
1076
+ async saveControl(control) {
1077
+ this.ensureDirectories();
1078
+ const controlId = getControlId(control, this.baseDir);
1079
+ const family = this.getControlFamily(controlId);
1080
+ const filename = this.getControlFilename(controlId);
1081
+ const familyDir = join2(this.controlsDir, family);
1082
+ const filePath = join2(familyDir, filename);
1083
+ if (!existsSync2(familyDir)) {
1084
+ mkdirSync(familyDir, { recursive: true });
1085
+ }
1086
1086
  try {
1087
- const commit = await git.readCommit({ fs: fs2, dir: gitRoot, oid: commitOid });
1088
- const parentOid = commit.commit.parent.length > 0 ? commit.commit.parent[0] : null;
1089
- if (!parentOid) {
1090
- const currentContent2 = await this.getFileAtCommit(commitOid, relativePath, gitRoot);
1091
- if (currentContent2) {
1092
- const lines = currentContent2.split("\n");
1093
- const isMappingFile2 = relativePath.includes("-mappings.yaml");
1094
- const yamlDiff2 = createYamlDiff("", currentContent2, isMappingFile2);
1095
- return {
1096
- changes: { insertions: lines.length, deletions: 0, files: 1 },
1097
- diff: `--- /dev/null
1098
- +++ b/${relativePath}
1099
- @@ -0,0 +1,${lines.length} @@
1100
- ` + lines.map((line) => "+" + line).join("\n"),
1101
- yamlDiff: yamlDiff2
1102
- };
1087
+ let yamlContent;
1088
+ if (existsSync2(filePath)) {
1089
+ const existingContent = readFileSync2(filePath, "utf8");
1090
+ const existingControl = yaml3.load(existingContent);
1091
+ const fieldsToUpdate = {};
1092
+ for (const key in control) {
1093
+ if (key === "timeline" || key === "unifiedHistory" || key === "_metadata" || key === "id") {
1094
+ continue;
1095
+ }
1096
+ if (JSON.stringify(control[key]) !== JSON.stringify(existingControl[key])) {
1097
+ fieldsToUpdate[key] = control[key];
1098
+ }
1103
1099
  }
1104
- return { changes: { insertions: 0, deletions: 0, files: 1 } };
1105
- }
1106
- const currentContent = await this.getFileAtCommit(commitOid, relativePath, gitRoot);
1107
- const parentContent = await this.getFileAtCommit(parentOid, relativePath, gitRoot);
1108
- if (!currentContent && !parentContent) {
1109
- return { changes: { insertions: 0, deletions: 0, files: 1 } };
1100
+ if (Object.keys(fieldsToUpdate).length > 0) {
1101
+ const updatedControl = { ...existingControl, ...fieldsToUpdate };
1102
+ yamlContent = yaml3.dump(updatedControl, {
1103
+ indent: 2,
1104
+ lineWidth: 80,
1105
+ noRefs: true,
1106
+ sortKeys: false
1107
+ });
1108
+ } else {
1109
+ yamlContent = existingContent;
1110
+ }
1111
+ } else {
1112
+ const controlToSave = { ...control };
1113
+ delete controlToSave.timeline;
1114
+ delete controlToSave.unifiedHistory;
1115
+ delete controlToSave._metadata;
1116
+ delete controlToSave.id;
1117
+ yamlContent = yaml3.dump(controlToSave, {
1118
+ indent: 2,
1119
+ lineWidth: 80,
1120
+ noRefs: true,
1121
+ sortKeys: false
1122
+ });
1110
1123
  }
1111
- const currentLines = currentContent ? currentContent.split("\n") : [];
1112
- const parentLines = parentContent ? parentContent.split("\n") : [];
1113
- const diff = await this.createSimpleDiff(parentLines, currentLines, relativePath);
1114
- const { insertions, deletions } = this.countChanges(parentLines, currentLines);
1115
- const isMappingFile = relativePath.includes("-mappings.yaml");
1116
- const yamlDiff = createYamlDiff(parentContent || "", currentContent || "", isMappingFile);
1117
- return {
1118
- changes: { insertions, deletions, files: 1 },
1119
- diff,
1120
- yamlDiff
1121
- };
1122
- } catch {
1123
- return { changes: { insertions: 0, deletions: 0, files: 1 } };
1124
+ writeFileSync(filePath, yamlContent, "utf8");
1125
+ this.controlMetadataCache.set(controlId, {
1126
+ controlId,
1127
+ filename,
1128
+ family
1129
+ });
1130
+ } catch (error) {
1131
+ throw new Error(
1132
+ `Failed to save control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
1133
+ );
1124
1134
  }
1125
1135
  }
1126
1136
  /**
1127
- * Get file content at a specific commit
1137
+ * Delete a control
1128
1138
  */
1129
- async getFileAtCommit(commitOid, filepath, gitRoot) {
1139
+ async deleteControl(controlId) {
1140
+ const family = this.getControlFamily(controlId);
1141
+ const filename = this.getControlFilename(controlId);
1142
+ const familyDir = join2(this.controlsDir, family);
1143
+ const filePath = join2(familyDir, filename);
1144
+ if (existsSync2(filePath)) {
1145
+ unlinkSync(filePath);
1146
+ this.controlMetadataCache.delete(controlId);
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Load all controls
1151
+ */
1152
+ async loadAllControls() {
1153
+ if (!existsSync2(this.controlsDir)) {
1154
+ return [];
1155
+ }
1156
+ let controlOrder = null;
1157
+ const lulaConfigPath = join2(this.baseDir, "lula.yaml");
1130
1158
  try {
1131
- const { blob } = await git.readBlob({
1132
- fs: fs2,
1133
- dir: gitRoot,
1134
- oid: commitOid,
1135
- filepath
1159
+ if (existsSync2(lulaConfigPath)) {
1160
+ const content = readFileSync2(lulaConfigPath, "utf8");
1161
+ const metadata = yaml3.load(content);
1162
+ controlOrder = metadata?.controlOrder || null;
1163
+ }
1164
+ } catch (error) {
1165
+ console.error(`Failed to load lula.yaml for controlOrder (path: ${lulaConfigPath}):`, error);
1166
+ }
1167
+ const entries = readdirSync(this.controlsDir);
1168
+ const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
1169
+ if (yamlFiles.length > 0) {
1170
+ const promises = yamlFiles.map(async (file) => {
1171
+ try {
1172
+ const filePath = join2(this.controlsDir, file);
1173
+ const content = await fs2.readFile(filePath, "utf8");
1174
+ const parsed = yaml3.load(content);
1175
+ if (!parsed.id) {
1176
+ parsed.id = getControlId(parsed, this.baseDir);
1177
+ }
1178
+ return parsed;
1179
+ } catch (error) {
1180
+ console.error(`Failed to load control from file ${file}:`, error);
1181
+ return null;
1182
+ }
1183
+ });
1184
+ const results2 = await Promise.all(promises);
1185
+ const controls2 = results2.filter((c) => c !== null);
1186
+ if (controlOrder && controlOrder.length > 0) {
1187
+ return this.sortControlsByOrder(controls2, controlOrder);
1188
+ }
1189
+ return controls2;
1190
+ }
1191
+ const families = entries.filter((name) => {
1192
+ const familyPath = join2(this.controlsDir, name);
1193
+ return statSync(familyPath).isDirectory();
1194
+ });
1195
+ const allPromises = [];
1196
+ for (const family of families) {
1197
+ const familyPath = join2(this.controlsDir, family);
1198
+ const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
1199
+ const familyPromises = files.map(async (file) => {
1200
+ try {
1201
+ const controlId = file.replace(".yaml", "");
1202
+ const control = await this.loadControl(controlId);
1203
+ return control;
1204
+ } catch (error) {
1205
+ console.error(`Failed to load control from file ${file}:`, error);
1206
+ return null;
1207
+ }
1136
1208
  });
1137
- return new TextDecoder().decode(blob);
1138
- } catch (_error) {
1139
- return null;
1209
+ allPromises.push(...familyPromises);
1210
+ }
1211
+ const results = await Promise.all(allPromises);
1212
+ const controls = results.filter((c) => c !== null);
1213
+ if (controlOrder && controlOrder.length > 0) {
1214
+ return this.sortControlsByOrder(controls, controlOrder);
1140
1215
  }
1216
+ return controls;
1141
1217
  }
1142
1218
  /**
1143
- * Create a simple unified diff between two file versions
1219
+ * Sort controls based on the provided order array
1144
1220
  */
1145
- async createSimpleDiff(oldLines, newLines, filepath) {
1146
- const diffLines = [];
1147
- diffLines.push(`--- a/${filepath}`);
1148
- diffLines.push(`+++ b/${filepath}`);
1149
- const oldCount = oldLines.length;
1150
- const newCount = newLines.length;
1151
- diffLines.push(`@@ -1,${oldCount} +1,${newCount} @@`);
1152
- let i = 0, j = 0;
1153
- while (i < oldLines.length || j < newLines.length) {
1154
- if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
1155
- diffLines.push(` ${oldLines[i]}`);
1156
- i++;
1157
- j++;
1158
- } else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
1159
- diffLines.push(`-${oldLines[i]}`);
1160
- i++;
1161
- } else if (j < newLines.length) {
1162
- diffLines.push(`+${newLines[j]}`);
1163
- j++;
1164
- }
1165
- }
1166
- return diffLines.join("\n");
1221
+ sortControlsByOrder(controls, controlOrder) {
1222
+ const orderMap = /* @__PURE__ */ new Map();
1223
+ controlOrder.forEach((controlId, index) => {
1224
+ orderMap.set(controlId, index);
1225
+ });
1226
+ return controls.sort((a, b) => {
1227
+ const aIndex = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
1228
+ const bIndex = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
1229
+ return aIndex - bIndex;
1230
+ });
1167
1231
  }
1168
1232
  /**
1169
- * Count insertions and deletions between two file versions
1233
+ * Load mappings from mappings directory
1170
1234
  */
1171
- countChanges(oldLines, newLines) {
1172
- let insertions = 0;
1173
- let deletions = 0;
1174
- const maxLines = Math.max(oldLines.length, newLines.length);
1175
- for (let i = 0; i < maxLines; i++) {
1176
- const oldLine = i < oldLines.length ? oldLines[i] : null;
1177
- const newLine = i < newLines.length ? newLines[i] : null;
1178
- if (oldLine === null) {
1179
- insertions++;
1180
- } else if (newLine === null) {
1181
- deletions++;
1182
- } else if (oldLine !== newLine) {
1183
- insertions++;
1184
- deletions++;
1235
+ async loadMappings() {
1236
+ const mappings = [];
1237
+ if (!existsSync2(this.mappingsDir)) {
1238
+ return mappings;
1239
+ }
1240
+ const families = readdirSync(this.mappingsDir).filter((name) => {
1241
+ const familyPath = join2(this.mappingsDir, name);
1242
+ return statSync(familyPath).isDirectory();
1243
+ });
1244
+ for (const family of families) {
1245
+ const familyPath = join2(this.mappingsDir, family);
1246
+ const files = readdirSync(familyPath).filter((file) => file.endsWith("-mappings.yaml"));
1247
+ for (const file of files) {
1248
+ const mappingFile = join2(familyPath, file);
1249
+ try {
1250
+ const content = readFileSync2(mappingFile, "utf8");
1251
+ const parsed = yaml3.load(content);
1252
+ if (Array.isArray(parsed)) {
1253
+ parsed.forEach((mapping) => {
1254
+ mapping.hash = createHash("sha256").update(JSON.stringify(mapping)).digest("hex");
1255
+ return mapping;
1256
+ });
1257
+ mappings.push(...parsed);
1258
+ }
1259
+ } catch (error) {
1260
+ console.error(`Failed to load mappings from ${family}/${file}:`, error);
1261
+ }
1185
1262
  }
1186
1263
  }
1187
- return { insertions, deletions };
1264
+ return mappings;
1188
1265
  }
1189
1266
  /**
1190
- * Get git stats for the entire repository
1267
+ * Save a single mapping
1191
1268
  */
1192
- async getRepositoryStats() {
1193
- const isGitRepo = await this.isGitRepository();
1194
- if (!isGitRepo) {
1195
- return {
1196
- totalCommits: 0,
1197
- contributors: 0,
1198
- lastCommitDate: null,
1199
- firstCommitDate: null
1200
- };
1269
+ async saveMapping(mapping) {
1270
+ this.ensureDirectories();
1271
+ const controlId = mapping.control_id;
1272
+ const family = this.getControlFamily(controlId);
1273
+ const familyDir = join2(this.mappingsDir, family);
1274
+ const mappingFile = join2(
1275
+ familyDir,
1276
+ `${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
1277
+ );
1278
+ if (!existsSync2(familyDir)) {
1279
+ mkdirSync(familyDir, { recursive: true });
1280
+ }
1281
+ let existingMappings = [];
1282
+ if (existsSync2(mappingFile)) {
1283
+ try {
1284
+ const content = readFileSync2(mappingFile, "utf8");
1285
+ existingMappings = yaml3.load(content) || [];
1286
+ } catch (error) {
1287
+ console.error(`Failed to parse existing mappings file: ${mappingFile}`, error);
1288
+ existingMappings = [];
1289
+ }
1201
1290
  }
1291
+ const cleanMapping = { ...mapping };
1292
+ delete cleanMapping.hash;
1293
+ existingMappings.push(cleanMapping);
1202
1294
  try {
1203
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1204
- const commits = await git.log({ fs: fs2, dir: gitRoot });
1205
- const contributorEmails = /* @__PURE__ */ new Set();
1206
- commits.forEach((commit) => {
1207
- contributorEmails.add(commit.commit.author.email);
1295
+ const yamlContent = yaml3.dump(existingMappings, {
1296
+ indent: 2,
1297
+ lineWidth: -1,
1298
+ noRefs: true
1208
1299
  });
1209
- const firstCommit = commits[commits.length - 1];
1210
- const lastCommit = commits[0];
1211
- return {
1212
- totalCommits: commits.length,
1213
- contributors: contributorEmails.size,
1214
- lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
1215
- firstCommitDate: firstCommit ? new Date(firstCommit.commit.author.timestamp * 1e3).toISOString() : null
1216
- };
1300
+ writeFileSync(mappingFile, yamlContent, "utf8");
1217
1301
  } catch (error) {
1218
- console.error("Error getting repository stats:", error);
1219
- return {
1220
- totalCommits: 0,
1221
- contributors: 0,
1222
- lastCommitDate: null,
1223
- firstCommitDate: null
1224
- };
1302
+ throw new Error(
1303
+ `Failed to save mapping for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
1304
+ );
1225
1305
  }
1226
1306
  }
1227
1307
  /**
1228
- * Get current branch name
1308
+ * Delete a single mapping
1229
1309
  */
1230
- async getCurrentBranch() {
1231
- try {
1232
- const isGitRepo = await this.isGitRepository();
1233
- if (!isGitRepo) {
1234
- return null;
1310
+ async deleteMapping(compositeKey) {
1311
+ const mappingFiles = this.getAllMappingFiles();
1312
+ for (const file of mappingFiles) {
1313
+ try {
1314
+ const content = readFileSync2(file, "utf8");
1315
+ let mappings = yaml3.load(content) || [];
1316
+ const originalLength = mappings.length;
1317
+ mappings = mappings.filter((m) => {
1318
+ const hash = createHash("sha256").update(JSON.stringify(m)).digest("hex");
1319
+ return `${m.control_id}:${hash}` !== compositeKey;
1320
+ });
1321
+ if (mappings.length < originalLength) {
1322
+ if (mappings.length === 0) {
1323
+ unlinkSync(file);
1324
+ } else {
1325
+ const yamlContent = yaml3.dump(mappings, {
1326
+ indent: 2,
1327
+ lineWidth: -1,
1328
+ noRefs: true
1329
+ });
1330
+ writeFileSync(file, yamlContent, "utf8");
1331
+ }
1332
+ return;
1333
+ }
1334
+ } catch (error) {
1335
+ console.error(`Error processing mapping file ${file}:`, error);
1235
1336
  }
1236
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1237
- const branch = await git.currentBranch({ fs: fs2, dir: gitRoot });
1238
- return branch || null;
1239
- } catch (error) {
1240
- console.error("Error getting current branch:", error);
1241
- return null;
1242
1337
  }
1243
1338
  }
1244
1339
  /**
1245
- * Get git status information
1340
+ * Get all mapping files
1246
1341
  */
1247
- async getGitStatus() {
1248
- try {
1249
- const isGitRepo = await this.isGitRepository();
1250
- if (!isGitRepo) {
1251
- return {
1252
- isGitRepository: false,
1253
- currentBranch: null,
1254
- branchInfo: null,
1255
- canPull: false,
1256
- canPush: false
1257
- };
1258
- }
1259
- const currentBranch2 = await this.getCurrentBranch();
1260
- if (!currentBranch2) {
1261
- return {
1262
- isGitRepository: true,
1263
- currentBranch: null,
1264
- branchInfo: null,
1265
- canPull: false,
1266
- canPush: false
1267
- };
1342
+ getAllMappingFiles() {
1343
+ const files = [];
1344
+ if (!existsSync2(this.mappingsDir)) {
1345
+ return files;
1346
+ }
1347
+ const flatFiles = readdirSync(this.mappingsDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(this.mappingsDir, file));
1348
+ files.push(...flatFiles);
1349
+ const entries = readdirSync(this.mappingsDir, { withFileTypes: true });
1350
+ for (const entry of entries) {
1351
+ if (entry.isDirectory()) {
1352
+ const familyDir = join2(this.mappingsDir, entry.name);
1353
+ const familyFiles = readdirSync(familyDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(familyDir, file));
1354
+ files.push(...familyFiles);
1268
1355
  }
1269
- const branchInfo = await this.getBranchInfo(currentBranch2);
1270
- return {
1271
- isGitRepository: true,
1272
- currentBranch: currentBranch2,
1273
- branchInfo,
1274
- canPull: branchInfo?.isBehind || false,
1275
- canPush: branchInfo?.isAhead || false
1276
- };
1277
- } catch (error) {
1278
- console.error("Error getting git status:", error);
1279
- return {
1280
- isGitRepository: false,
1281
- currentBranch: null,
1282
- branchInfo: null,
1283
- canPull: false,
1284
- canPush: false
1285
- };
1286
1356
  }
1357
+ return files;
1287
1358
  }
1288
1359
  /**
1289
- * Get branch comparison information
1360
+ * Save mappings to per-control files
1290
1361
  */
1291
- async getBranchInfo(branchName) {
1292
- try {
1293
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1294
- const localCommits = await git.log({ fs: fs2, dir: gitRoot, ref: branchName });
1362
+ async saveMappings(mappings) {
1363
+ this.ensureDirectories();
1364
+ const mappingsByControl = /* @__PURE__ */ new Map();
1365
+ for (const mapping of mappings) {
1366
+ const controlId = mapping.control_id;
1367
+ if (!mappingsByControl.has(controlId)) {
1368
+ mappingsByControl.set(controlId, []);
1369
+ }
1370
+ mappingsByControl.get(controlId).push(mapping);
1371
+ }
1372
+ for (const [controlId, controlMappings] of mappingsByControl) {
1373
+ const family = this.getControlFamily(controlId);
1374
+ const familyDir = join2(this.mappingsDir, family);
1375
+ const mappingFile = join2(
1376
+ familyDir,
1377
+ `${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
1378
+ );
1379
+ if (!existsSync2(familyDir)) {
1380
+ mkdirSync(familyDir, { recursive: true });
1381
+ }
1382
+ const cleanMappings = controlMappings.map((m) => {
1383
+ const clean = { ...m };
1384
+ delete clean.hash;
1385
+ return clean;
1386
+ });
1295
1387
  try {
1296
- const remotes = await git.listRemotes({ fs: fs2, dir: gitRoot });
1297
- for (const remote of remotes) {
1298
- try {
1299
- const fetchResult = await this.executeGitCommand(`git fetch ${remote.remote}`, gitRoot);
1300
- if (!fetchResult.success) {
1301
- console.warn(`Could not fetch from remote ${remote.remote}:`, fetchResult.error);
1302
- }
1303
- } catch (fetchError) {
1304
- console.warn(`Could not fetch from remote ${remote.remote}:`, fetchError);
1305
- }
1306
- }
1307
- let remoteCommits = [];
1308
- let foundRemote = false;
1309
- for (const remote of remotes) {
1310
- const remoteBranchRef = `${remote.remote}/${branchName}`;
1311
- try {
1312
- remoteCommits = await git.log({ fs: fs2, dir: gitRoot, ref: remoteBranchRef });
1313
- foundRemote = true;
1314
- break;
1315
- } catch (error) {
1316
- console.warn(`Could not get commits for ${remoteBranchRef}: ${error}`);
1317
- }
1318
- }
1319
- if (!foundRemote || remoteCommits.length === 0) {
1320
- const lastCommit2 = localCommits[0];
1321
- return {
1322
- currentBranch: branchName,
1323
- isAhead: false,
1324
- isBehind: false,
1325
- aheadCount: 0,
1326
- behindCount: 0,
1327
- lastCommitDate: lastCommit2 ? new Date(lastCommit2.commit.author.timestamp * 1e3).toISOString() : null,
1328
- lastCommitMessage: lastCommit2?.commit.message || null,
1329
- hasUnpushedChanges: false
1330
- };
1331
- }
1332
- const localHashes = new Set(localCommits.map((c) => c.oid));
1333
- const remoteHashes = new Set(remoteCommits.map((c) => c.oid));
1334
- const aheadCount = localCommits.filter((c) => !remoteHashes.has(c.oid)).length;
1335
- const behindCount = remoteCommits.filter((c) => !localHashes.has(c.oid)).length;
1336
- const lastCommit = localCommits[0];
1337
- return {
1338
- currentBranch: branchName,
1339
- isAhead: aheadCount > 0,
1340
- isBehind: behindCount > 0,
1341
- aheadCount,
1342
- behindCount,
1343
- lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
1344
- lastCommitMessage: lastCommit?.commit.message || null,
1345
- hasUnpushedChanges: aheadCount > 0
1346
- };
1347
- } catch {
1348
- const lastCommit = localCommits[0];
1349
- return {
1350
- currentBranch: branchName,
1351
- isAhead: false,
1352
- isBehind: false,
1353
- aheadCount: 0,
1354
- behindCount: 0,
1355
- lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
1356
- lastCommitMessage: lastCommit?.commit.message || null,
1357
- hasUnpushedChanges: false
1358
- };
1388
+ const yamlContent = yaml3.dump(cleanMappings, {
1389
+ indent: 2,
1390
+ lineWidth: -1,
1391
+ noRefs: true
1392
+ });
1393
+ writeFileSync(mappingFile, yamlContent, "utf8");
1394
+ } catch (error) {
1395
+ throw new Error(
1396
+ `Failed to save mappings for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
1397
+ );
1359
1398
  }
1360
- } catch (error) {
1361
- console.error("Error getting branch info:", error);
1362
- return null;
1363
1399
  }
1364
1400
  }
1365
1401
  /**
1366
- * Fetch updates from remote repositories using native git command
1402
+ * Refresh controls cache
1367
1403
  */
1368
- async fetchFromRemotes() {
1369
- try {
1370
- const isGitRepo = await this.isGitRepository();
1371
- if (!isGitRepo) {
1372
- return { success: false, message: "Not a git repository", details: [] };
1373
- }
1374
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1375
- const remotes = await git.listRemotes({ fs: fs2, dir: gitRoot });
1376
- if (remotes.length === 0) {
1377
- return { success: true, message: "No remotes configured", details: [] };
1404
+ refreshControlsCache() {
1405
+ this.controlMetadataCache.clear();
1406
+ if (!existsSync2(this.controlsDir)) {
1407
+ return;
1408
+ }
1409
+ const entries = readdirSync(this.controlsDir);
1410
+ const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
1411
+ if (yamlFiles.length > 0) {
1412
+ for (const filename of yamlFiles) {
1413
+ const controlId = filename.replace(".yaml", "");
1414
+ const family = this.getControlFamily(controlId);
1415
+ this.controlMetadataCache.set(controlId, {
1416
+ controlId,
1417
+ filename,
1418
+ family
1419
+ });
1378
1420
  }
1379
- const details = [];
1380
- let hasErrors = false;
1381
- for (const remote of remotes) {
1421
+ return;
1422
+ }
1423
+ const families = entries.filter((name) => {
1424
+ const familyPath = join2(this.controlsDir, name);
1425
+ return statSync(familyPath).isDirectory();
1426
+ });
1427
+ for (const family of families) {
1428
+ const familyPath = join2(this.controlsDir, family);
1429
+ const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
1430
+ for (const filename of files) {
1382
1431
  try {
1383
- const fetchResult = await this.executeGitCommand(`git fetch ${remote.remote}`, gitRoot);
1384
- if (fetchResult.success) {
1385
- details.push(`Fetched from ${remote.remote}`);
1386
- } else {
1387
- details.push(`Failed to fetch from ${remote.remote}: ${fetchResult.error}`);
1388
- hasErrors = true;
1389
- }
1432
+ const filePath = join2(familyPath, filename);
1433
+ const content = readFileSync2(filePath, "utf8");
1434
+ const parsed = yaml3.load(content);
1435
+ const controlId = getControlId(parsed, this.baseDir);
1436
+ this.controlMetadataCache.set(controlId, {
1437
+ controlId,
1438
+ filename,
1439
+ family
1440
+ });
1390
1441
  } catch (error) {
1391
- details.push(`Error fetching from ${remote.remote}: ${error}`);
1392
- hasErrors = true;
1442
+ console.error(`Failed to read control metadata from ${family}/${filename}:`, error);
1443
+ const controlId = filename.replace(".yaml", "").replace(/_/g, "/");
1444
+ this.controlMetadataCache.set(controlId, {
1445
+ controlId,
1446
+ filename,
1447
+ family
1448
+ });
1393
1449
  }
1394
1450
  }
1395
- return {
1396
- success: !hasErrors,
1397
- message: hasErrors ? "Fetch completed with some errors" : "Successfully fetched from all remotes",
1398
- details
1399
- };
1400
- } catch (error) {
1401
- console.error("Error fetching from remotes:", error);
1402
- return {
1403
- success: false,
1404
- message: error instanceof Error ? error.message : "Unknown error occurred",
1405
- details: []
1406
- };
1407
1451
  }
1408
1452
  }
1409
1453
  /**
1410
- * Pull changes from remote using native git command
1454
+ * Get file store statistics
1411
1455
  */
1412
- async pullChanges() {
1413
- try {
1414
- const isGitRepo = await this.isGitRepository();
1415
- if (!isGitRepo) {
1416
- return { success: false, message: "Not a git repository" };
1417
- }
1418
- const currentBranch2 = await this.getCurrentBranch();
1419
- if (!currentBranch2) {
1420
- return { success: false, message: "No current branch found" };
1421
- }
1422
- const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
1423
- const remotes = await git.listRemotes({ fs: fs2, dir: gitRoot });
1424
- if (remotes.length === 0) {
1425
- return { success: false, message: "No remotes configured" };
1426
- }
1427
- const targetRemote = remotes[0].remote;
1428
- const pullCommand = `git pull ${targetRemote} ${currentBranch2}`;
1429
- const pullResult = await this.executeGitCommand(pullCommand, gitRoot);
1430
- if (!pullResult.success) {
1431
- return {
1432
- success: false,
1433
- message: `Failed to pull changes: ${pullResult.error}`
1434
- };
1435
- }
1436
- return {
1437
- success: true,
1438
- message: pullResult.output || "Successfully pulled changes"
1439
- };
1440
- } catch (error) {
1441
- console.error("Error pulling changes:", error);
1442
- return {
1443
- success: false,
1444
- message: error instanceof Error ? error.message : "Unknown error occurred"
1445
- };
1456
+ getStats() {
1457
+ const controlCount = this.controlMetadataCache.size;
1458
+ let mappingCount = 0;
1459
+ if (existsSync2(this.mappingsDir)) {
1460
+ const families = readdirSync(this.mappingsDir).filter((name) => {
1461
+ const familyPath = join2(this.mappingsDir, name);
1462
+ return statSync(familyPath).isDirectory();
1463
+ });
1464
+ mappingCount = families.length;
1446
1465
  }
1466
+ const familyCount = new Set(
1467
+ Array.from(this.controlMetadataCache.values()).map((meta) => meta.family)
1468
+ ).size;
1469
+ return {
1470
+ controlCount,
1471
+ mappingCount,
1472
+ familyCount
1473
+ };
1474
+ }
1475
+ /**
1476
+ * Clear all caches
1477
+ */
1478
+ clearCache() {
1479
+ this.controlMetadataCache.clear();
1480
+ this.refreshControlsCache();
1447
1481
  }
1448
1482
  };
1449
1483
  }
@@ -2653,12 +2687,12 @@ var init_spreadsheetRoutes = __esm({
2653
2687
  import { readFileSync as readFileSync4 } from "fs";
2654
2688
  init_debug();
2655
2689
  init_controlHelpers();
2656
- init_serverState();
2657
2690
  init_gitHistory();
2691
+ init_serverState();
2658
2692
  import * as yaml5 from "js-yaml";
2693
+ import crypto2 from "node:crypto";
2659
2694
  import { join as join5 } from "path";
2660
2695
  import { WebSocket, WebSocketServer } from "ws";
2661
- import crypto2 from "node:crypto";
2662
2696
  var WebSocketManager = class {
2663
2697
  wss = null;
2664
2698
  clients = /* @__PURE__ */ new Set();
@@ -2699,6 +2733,62 @@ var WebSocketManager = class {
2699
2733
  }
2700
2734
  break;
2701
2735
  }
2736
+ case "update-mapping": {
2737
+ const state = getServerState();
2738
+ if (payload && payload.old_composite_key && payload.mapping) {
2739
+ const oldCompositeKey = payload.old_composite_key;
2740
+ const existing = state.mappingsCache.get(oldCompositeKey);
2741
+ if (!existing) {
2742
+ console.error("Mapping not found for update:", oldCompositeKey);
2743
+ break;
2744
+ }
2745
+ const incoming = payload.mapping;
2746
+ const updated = {
2747
+ ...existing,
2748
+ ...incoming,
2749
+ control_id: incoming.control_id || existing.control_id,
2750
+ uuid: incoming.uuid || existing.uuid
2751
+ };
2752
+ if (!updated.hash || updated.hash === "") {
2753
+ updated.hash = crypto2.createHash("sha256").update(JSON.stringify({ ...updated, hash: void 0 })).digest("hex");
2754
+ }
2755
+ const oldHash = existing.hash;
2756
+ const oldControlId = existing.control_id;
2757
+ const oldFamily = oldControlId.split("-")[0];
2758
+ const newHash = updated.hash;
2759
+ const newControlId = updated.control_id;
2760
+ const newFamily = newControlId.split("-")[0];
2761
+ const newCompositeKey = `${newControlId}:${newHash}`;
2762
+ await state.fileStore.updateMapping(oldCompositeKey, updated);
2763
+ const entries = Array.from(state.mappingsCache.entries());
2764
+ const oldIndex = entries.findIndex(([key]) => key === oldCompositeKey);
2765
+ if (oldIndex === -1) {
2766
+ state.mappingsCache.delete(oldCompositeKey);
2767
+ state.mappingsCache.set(newCompositeKey, updated);
2768
+ } else {
2769
+ entries[oldIndex] = [newCompositeKey, updated];
2770
+ state.mappingsCache = new Map(entries);
2771
+ }
2772
+ state.mappingsByFamily.get(oldFamily)?.delete(oldHash);
2773
+ state.mappingsByControl.get(oldControlId)?.delete(oldHash);
2774
+ if (!state.mappingsByFamily.has(newFamily)) {
2775
+ state.mappingsByFamily.set(newFamily, /* @__PURE__ */ new Set());
2776
+ }
2777
+ state.mappingsByFamily.get(newFamily).add(newHash);
2778
+ if (!state.mappingsByControl.has(newControlId)) {
2779
+ state.mappingsByControl.set(newControlId, /* @__PURE__ */ new Set());
2780
+ }
2781
+ state.mappingsByControl.get(newControlId).add(newHash);
2782
+ ws.send(
2783
+ JSON.stringify({
2784
+ type: "mapping-updated",
2785
+ payload: { uuid: updated.uuid, success: true }
2786
+ })
2787
+ );
2788
+ this.broadcastState();
2789
+ }
2790
+ break;
2791
+ }
2702
2792
  case "refresh-controls": {
2703
2793
  const state = getServerState();
2704
2794
  state.controlsCache.clear();