brew-tui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +50 -16
  2. package/build/{brewbar-installer-SFVZEIA6.js → brewbar-installer-ZEMXNDHP.js} +13 -3
  3. package/build/{brewbar-installer-SFVZEIA6.js.map → brewbar-installer-ZEMXNDHP.js.map} +1 -1
  4. package/build/brewfile-manager-3SERRYNC.js +20 -0
  5. package/build/chunk-42URLVAJ.js +299 -0
  6. package/build/chunk-42URLVAJ.js.map +1 -0
  7. package/build/chunk-4I344KQX.js +221 -0
  8. package/build/chunk-4I344KQX.js.map +1 -0
  9. package/build/chunk-KDHEUNRI.js +62 -0
  10. package/build/chunk-KDHEUNRI.js.map +1 -0
  11. package/build/{chunk-PPSKR6PA.js → chunk-KDJNCZD7.js} +239 -17
  12. package/build/chunk-KDJNCZD7.js.map +1 -0
  13. package/build/chunk-KN4GCMIE.js +48 -0
  14. package/build/chunk-KN4GCMIE.js.map +1 -0
  15. package/build/{chunk-65YZJX2E.js → chunk-KVCVIRWI.js} +6 -20
  16. package/build/chunk-KVCVIRWI.js.map +1 -0
  17. package/build/chunk-LXF72RCD.js +467 -0
  18. package/build/chunk-LXF72RCD.js.map +1 -0
  19. package/build/chunk-U2DRWB7A.js +123 -0
  20. package/build/chunk-U2DRWB7A.js.map +1 -0
  21. package/build/chunk-UWS4A4F5.js +25 -0
  22. package/build/chunk-UWS4A4F5.js.map +1 -0
  23. package/build/compliance-checker-X7P623UF.js +12 -0
  24. package/build/compliance-checker-X7P623UF.js.map +1 -0
  25. package/build/{history-logger-2PGYSPFL.js → history-logger-PBDOLKNJ.js} +3 -2
  26. package/build/history-logger-PBDOLKNJ.js.map +1 -0
  27. package/build/index.js +2019 -452
  28. package/build/index.js.map +1 -1
  29. package/build/policy-io-EECGRKNA.js +11 -0
  30. package/build/policy-io-EECGRKNA.js.map +1 -0
  31. package/build/snapshot-RAPGMAJF.js +17 -0
  32. package/build/snapshot-RAPGMAJF.js.map +1 -0
  33. package/build/sync-engine-4ERSW4EQ.js +18 -0
  34. package/build/sync-engine-4ERSW4EQ.js.map +1 -0
  35. package/package.json +11 -9
  36. package/build/chunk-65YZJX2E.js.map +0 -1
  37. package/build/chunk-PPSKR6PA.js.map +0 -1
  38. /package/build/{history-logger-2PGYSPFL.js.map → brewfile-manager-3SERRYNC.js.map} +0 -0
@@ -0,0 +1,467 @@
1
+ import {
2
+ captureSnapshot,
3
+ saveSnapshot,
4
+ streamBrew
5
+ } from "./chunk-4I344KQX.js";
6
+ import {
7
+ DATA_DIR,
8
+ ensureDataDirs
9
+ } from "./chunk-UWS4A4F5.js";
10
+ import {
11
+ logger
12
+ } from "./chunk-KDHEUNRI.js";
13
+
14
+ // src/lib/brewfile/brewfile-manager.ts
15
+ import { readFile, writeFile, rename } from "fs/promises";
16
+ import { join } from "path";
17
+
18
+ // src/lib/diff-engine/diff.ts
19
+ function compareVersions(a, b) {
20
+ const splitSegment = (seg) => {
21
+ const underIdx = seg.indexOf("_");
22
+ if (underIdx !== -1) {
23
+ const main = parseInt(seg.slice(0, underIdx), 10);
24
+ const rev = parseInt(seg.slice(underIdx + 1), 10);
25
+ return [isNaN(main) ? -1 : main, isNaN(rev) ? 0 : rev];
26
+ }
27
+ const n = parseInt(seg, 10);
28
+ return [isNaN(n) ? -1 : n, 0];
29
+ };
30
+ const aParts = a.split(".");
31
+ const bParts = b.split(".");
32
+ const len = Math.max(aParts.length, bParts.length);
33
+ for (let i = 0; i < len; i++) {
34
+ const aSeg = aParts[i] ?? "0";
35
+ const bSeg = bParts[i] ?? "0";
36
+ const [aMain, aRev] = splitSegment(aSeg);
37
+ const [bMain, bRev] = splitSegment(bSeg);
38
+ if (aMain === -1 && bMain === -1) {
39
+ const cmp = aSeg.localeCompare(bSeg);
40
+ if (cmp !== 0) return cmp;
41
+ continue;
42
+ }
43
+ const aVal = aMain === -1 ? -1 : aMain;
44
+ const bVal = bMain === -1 ? -1 : bMain;
45
+ if (aVal !== bVal) return aVal - bVal;
46
+ if (aRev !== bRev) return aRev - bRev;
47
+ }
48
+ return 0;
49
+ }
50
+ function diffPackages(base, current, type, result) {
51
+ const baseMap = new Map(base.map((p) => [p.name, p.version]));
52
+ const currentMap = new Map(current.map((p) => [p.name, p.version]));
53
+ for (const [name, version] of currentMap) {
54
+ if (!baseMap.has(name)) {
55
+ result.added.push({ name, version, type });
56
+ } else {
57
+ const baseVersion = baseMap.get(name);
58
+ if (version !== baseVersion) {
59
+ const cmp = compareVersions(version, baseVersion);
60
+ if (cmp > 0) {
61
+ result.upgraded.push({ name, from: baseVersion, to: version, type });
62
+ } else {
63
+ result.downgraded.push({ name, from: baseVersion, to: version, type });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ for (const [name, version] of baseMap) {
69
+ if (!currentMap.has(name)) {
70
+ result.removed.push({ name, version, type });
71
+ }
72
+ }
73
+ }
74
+ function diffSnapshots(base, current) {
75
+ const result = { added: [], removed: [], upgraded: [], downgraded: [] };
76
+ diffPackages(base.formulae, current.formulae, "formula", result);
77
+ diffPackages(base.casks, current.casks, "cask", result);
78
+ const baseSet = new Set(base.taps);
79
+ const currentSet = new Set(current.taps);
80
+ for (const tap of currentSet) {
81
+ if (!baseSet.has(tap)) result.added.push({ name: tap, version: "", type: "tap" });
82
+ }
83
+ for (const tap of baseSet) {
84
+ if (!currentSet.has(tap)) result.removed.push({ name: tap, version: "", type: "tap" });
85
+ }
86
+ return result;
87
+ }
88
+ function diffDesiredActual(desired, actual) {
89
+ const result = { added: [], removed: [], upgraded: [], downgraded: [] };
90
+ const strict = desired.strictMode === true;
91
+ function processPackages(desiredPkgs, actualPkgs, type) {
92
+ const actualMap = new Map(actualPkgs.map((p) => [p.name, p.version]));
93
+ const desiredNames = new Set(desiredPkgs.map((p) => p.name));
94
+ for (const pkg of desiredPkgs) {
95
+ const actualVersion = actualMap.get(pkg.name);
96
+ if (actualVersion === void 0) {
97
+ result.added.push({ name: pkg.name, version: pkg.version ?? "", type });
98
+ } else if (pkg.version !== void 0 && pkg.version !== actualVersion) {
99
+ const cmp = compareVersions(pkg.version, actualVersion);
100
+ if (cmp > 0) {
101
+ result.upgraded.push({ name: pkg.name, from: actualVersion, to: pkg.version, type });
102
+ } else {
103
+ result.downgraded.push({ name: pkg.name, from: actualVersion, to: pkg.version, type });
104
+ }
105
+ }
106
+ }
107
+ if (strict) {
108
+ for (const pkg of actualPkgs) {
109
+ if (!desiredNames.has(pkg.name)) {
110
+ result.removed.push({ name: pkg.name, version: pkg.version, type });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ processPackages(desired.formulae, actual.formulae, "formula");
116
+ processPackages(desired.casks, actual.casks, "cask");
117
+ const actualTapSet = new Set(actual.taps);
118
+ const desiredTapSet = new Set(desired.taps);
119
+ for (const tap of desiredTapSet) {
120
+ if (!actualTapSet.has(tap)) {
121
+ result.added.push({ name: tap, version: "", type: "tap" });
122
+ }
123
+ }
124
+ if (strict) {
125
+ for (const tap of actualTapSet) {
126
+ if (!desiredTapSet.has(tap)) {
127
+ result.removed.push({ name: tap, version: "", type: "tap" });
128
+ }
129
+ }
130
+ }
131
+ return result;
132
+ }
133
+
134
+ // src/lib/brewfile/yaml-serializer.ts
135
+ var MUST_QUOTE_RE = /[:'"#[\]{}?!@&*>|%]/;
136
+ var STARTS_SPECIAL_RE = /^[-\s]|^\d/;
137
+ function needsQuoting(s) {
138
+ if (s === "") return true;
139
+ if (STARTS_SPECIAL_RE.test(s)) return true;
140
+ if (MUST_QUOTE_RE.test(s)) return true;
141
+ if (s !== s.trimStart() || s !== s.trimEnd()) return true;
142
+ return false;
143
+ }
144
+ function quote(s) {
145
+ if (!needsQuoting(s)) return s;
146
+ return `'${s.replace(/'/g, "''")}'`;
147
+ }
148
+ function unquote(s) {
149
+ const trimmed = s.trim();
150
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
151
+ return trimmed.slice(1, -1).replace(/''/g, "'");
152
+ }
153
+ return trimmed;
154
+ }
155
+ function serializeBrewfile(schema) {
156
+ const lines = [];
157
+ lines.push(`version: ${schema.version}`);
158
+ lines.push("meta:");
159
+ lines.push(` name: ${quote(schema.meta.name)}`);
160
+ if (schema.meta.description !== void 0) {
161
+ lines.push(` description: ${quote(schema.meta.description)}`);
162
+ }
163
+ lines.push(` createdAt: ${quote(schema.meta.createdAt)}`);
164
+ lines.push(` updatedAt: ${quote(schema.meta.updatedAt)}`);
165
+ lines.push("formulae:");
166
+ if (schema.formulae.length === 0) {
167
+ lines.push(" []");
168
+ } else {
169
+ for (const f of schema.formulae) {
170
+ lines.push(` - name: ${quote(f.name)}`);
171
+ if (f.version !== void 0) {
172
+ lines.push(` version: ${quote(f.version)}`);
173
+ }
174
+ if (f.options !== void 0 && f.options.length > 0) {
175
+ lines.push(" options:");
176
+ for (const opt of f.options) {
177
+ lines.push(` - ${quote(opt)}`);
178
+ }
179
+ }
180
+ }
181
+ }
182
+ lines.push("casks:");
183
+ if (schema.casks.length === 0) {
184
+ lines.push(" []");
185
+ } else {
186
+ for (const c of schema.casks) {
187
+ lines.push(` - name: ${quote(c.name)}`);
188
+ if (c.version !== void 0) {
189
+ lines.push(` version: ${quote(c.version)}`);
190
+ }
191
+ }
192
+ }
193
+ lines.push("taps:");
194
+ if (schema.taps.length === 0) {
195
+ lines.push(" []");
196
+ } else {
197
+ for (const tap of schema.taps) {
198
+ lines.push(` - ${quote(tap)}`);
199
+ }
200
+ }
201
+ if (schema.strictMode !== void 0) {
202
+ lines.push(`strictMode: ${schema.strictMode}`);
203
+ }
204
+ return lines.join("\n") + "\n";
205
+ }
206
+ function parseBrewfile(yaml) {
207
+ const rawLines = yaml.split("\n");
208
+ let version;
209
+ const meta = {};
210
+ const formulae = [];
211
+ const casks = [];
212
+ const taps = [];
213
+ let strictMode;
214
+ let context = "root";
215
+ let currentFormula = null;
216
+ let currentCask = null;
217
+ for (const rawLine of rawLines) {
218
+ const line = rawLine;
219
+ const trimmed = line.trim();
220
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
221
+ const indent = line.length - line.trimStart().length;
222
+ if (trimmed.endsWith(": []")) {
223
+ const key = trimmed.slice(0, -4);
224
+ if (key === "formulae") {
225
+ context = "formulae";
226
+ continue;
227
+ }
228
+ if (key === "casks") {
229
+ context = "casks";
230
+ continue;
231
+ }
232
+ if (key === "taps") {
233
+ context = "taps";
234
+ continue;
235
+ }
236
+ }
237
+ if (indent === 0) {
238
+ if (trimmed.startsWith("version:")) {
239
+ const val = trimmed.slice("version:".length).trim();
240
+ version = parseInt(val, 10);
241
+ context = "root";
242
+ continue;
243
+ }
244
+ if (trimmed === "meta:") {
245
+ context = "meta";
246
+ continue;
247
+ }
248
+ if (trimmed === "formulae:") {
249
+ context = "formulae";
250
+ continue;
251
+ }
252
+ if (trimmed === "casks:") {
253
+ context = "casks";
254
+ continue;
255
+ }
256
+ if (trimmed === "taps:") {
257
+ context = "taps";
258
+ continue;
259
+ }
260
+ if (trimmed.startsWith("strictMode:")) {
261
+ const val = trimmed.slice("strictMode:".length).trim();
262
+ strictMode = val === "true";
263
+ context = "root";
264
+ continue;
265
+ }
266
+ context = "root";
267
+ continue;
268
+ }
269
+ if (context === "meta" && indent === 2) {
270
+ const colonIdx = trimmed.indexOf(":");
271
+ if (colonIdx === -1) continue;
272
+ const key = trimmed.slice(0, colonIdx).trim();
273
+ const val = unquote(trimmed.slice(colonIdx + 1));
274
+ if (key === "name") meta.name = val;
275
+ else if (key === "description") meta.description = val;
276
+ else if (key === "createdAt") meta.createdAt = val;
277
+ else if (key === "updatedAt") meta.updatedAt = val;
278
+ continue;
279
+ }
280
+ if (context === "formulae" || context === "formulae_item" || context === "formulae_options") {
281
+ if (indent === 2 && trimmed.startsWith("- name:")) {
282
+ const val = unquote(trimmed.slice("- name:".length));
283
+ currentFormula = { name: val };
284
+ formulae.push(currentFormula);
285
+ context = "formulae_item";
286
+ continue;
287
+ }
288
+ if (indent === 4 && context === "formulae_item") {
289
+ if (trimmed.startsWith("version:")) {
290
+ if (currentFormula) currentFormula.version = unquote(trimmed.slice("version:".length));
291
+ } else if (trimmed === "options:") {
292
+ if (currentFormula) currentFormula.options = [];
293
+ context = "formulae_options";
294
+ }
295
+ continue;
296
+ }
297
+ if (indent === 6 && context === "formulae_options") {
298
+ if (trimmed.startsWith("- ")) {
299
+ const val = unquote(trimmed.slice(2));
300
+ if (currentFormula?.options) currentFormula.options.push(val);
301
+ }
302
+ continue;
303
+ }
304
+ if (indent === 4 && context === "formulae_options") {
305
+ context = "formulae_item";
306
+ if (trimmed.startsWith("version:")) {
307
+ if (currentFormula) currentFormula.version = unquote(trimmed.slice("version:".length));
308
+ }
309
+ continue;
310
+ }
311
+ }
312
+ if (context === "casks" || context === "casks_item") {
313
+ if (indent === 2 && trimmed.startsWith("- name:")) {
314
+ const val = unquote(trimmed.slice("- name:".length));
315
+ currentCask = { name: val };
316
+ casks.push(currentCask);
317
+ context = "casks_item";
318
+ continue;
319
+ }
320
+ if (indent === 4 && context === "casks_item") {
321
+ if (trimmed.startsWith("version:")) {
322
+ if (currentCask) currentCask.version = unquote(trimmed.slice("version:".length));
323
+ }
324
+ continue;
325
+ }
326
+ }
327
+ if (context === "taps" && indent === 2 && trimmed.startsWith("- ")) {
328
+ taps.push(unquote(trimmed.slice(2)));
329
+ continue;
330
+ }
331
+ }
332
+ if (version !== 1) {
333
+ throw new Error(`Invalid Brewfile: expected version 1, got ${String(version)}`);
334
+ }
335
+ if (!meta.name) {
336
+ throw new Error("Invalid Brewfile: missing meta.name");
337
+ }
338
+ if (!meta.createdAt || !meta.updatedAt) {
339
+ throw new Error("Invalid Brewfile: missing meta.createdAt or meta.updatedAt");
340
+ }
341
+ const result = {
342
+ version: 1,
343
+ meta: {
344
+ name: meta.name,
345
+ createdAt: meta.createdAt,
346
+ updatedAt: meta.updatedAt
347
+ },
348
+ formulae,
349
+ casks,
350
+ taps
351
+ };
352
+ if (meta.description !== void 0) result.meta.description = meta.description;
353
+ if (strictMode !== void 0) result.strictMode = strictMode;
354
+ return result;
355
+ }
356
+
357
+ // src/lib/brewfile/brewfile-manager.ts
358
+ var BREWFILE_PATH = join(DATA_DIR, "brewfile.yaml");
359
+ async function loadBrewfile() {
360
+ try {
361
+ const raw = await readFile(BREWFILE_PATH, "utf-8");
362
+ return parseBrewfile(raw);
363
+ } catch (err) {
364
+ if (isNodeError(err) && err.code === "ENOENT") {
365
+ return null;
366
+ }
367
+ logger.warn("Failed to parse Brewfile", { error: String(err) });
368
+ return null;
369
+ }
370
+ }
371
+ async function saveBrewfile(schema) {
372
+ await ensureDataDirs();
373
+ const updated = {
374
+ ...schema,
375
+ meta: { ...schema.meta, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
376
+ };
377
+ const tmpPath = BREWFILE_PATH + ".tmp";
378
+ await writeFile(tmpPath, serializeBrewfile(updated), { encoding: "utf-8", mode: 384 });
379
+ await rename(tmpPath, BREWFILE_PATH);
380
+ }
381
+ async function createDefaultBrewfile(name) {
382
+ const snapshot = await captureSnapshot();
383
+ const now = (/* @__PURE__ */ new Date()).toISOString();
384
+ const schema = {
385
+ version: 1,
386
+ meta: { name, createdAt: now, updatedAt: now },
387
+ formulae: snapshot.formulae.map((f) => ({ name: f.name })),
388
+ casks: snapshot.casks.map((c) => ({ name: c.name })),
389
+ taps: [...snapshot.taps],
390
+ strictMode: false
391
+ };
392
+ return schema;
393
+ }
394
+ async function computeDrift(schema) {
395
+ const snapshot = await captureSnapshot();
396
+ const diff = diffDesiredActual(schema, snapshot);
397
+ const missingPackages = diff.added.filter((e) => e.type !== "tap").map((e) => e.name);
398
+ const extraPackages = diff.removed.filter((e) => e.type !== "tap").map((e) => e.name);
399
+ const wrongVersions = [
400
+ ...diff.upgraded.map((e) => ({ name: e.name, desired: e.to, actual: e.from })),
401
+ ...diff.downgraded.map((e) => ({ name: e.name, desired: e.to, actual: e.from }))
402
+ ];
403
+ const penalty = missingPackages.length * 10 + extraPackages.length * 2 + wrongVersions.length * 5;
404
+ const score = Math.max(0, Math.min(100, 100 - penalty));
405
+ return { diff, score, missingPackages, extraPackages, wrongVersions };
406
+ }
407
+ async function* reconcile(schema, isPro) {
408
+ if (!isPro) {
409
+ yield "Pro license required for reconcile.";
410
+ return;
411
+ }
412
+ const report = await computeDrift(schema);
413
+ if (report.missingPackages.length === 0 && report.wrongVersions.length === 0 && report.extraPackages.length === 0) {
414
+ yield "Already in sync \u2014 nothing to do.";
415
+ return;
416
+ }
417
+ for (const name of report.missingPackages) {
418
+ yield `\u2192 Installing ${name}...`;
419
+ try {
420
+ for await (const line of streamBrew(["install", name])) {
421
+ yield line;
422
+ }
423
+ } catch (err) {
424
+ yield `\u2717 Failed to install ${name}: ${err instanceof Error ? err.message : String(err)}`;
425
+ }
426
+ }
427
+ for (const { name, desired } of report.wrongVersions) {
428
+ const target = `${name}@${desired}`;
429
+ yield `\u2192 Installing ${target}...`;
430
+ try {
431
+ for await (const line of streamBrew(["install", target])) {
432
+ yield line;
433
+ }
434
+ } catch (err) {
435
+ yield `\u2717 Failed to install ${target}: ${err instanceof Error ? err.message : String(err)}`;
436
+ }
437
+ }
438
+ for (const name of report.extraPackages) {
439
+ yield `\u26A0 Extra package not in Brewfile: ${name} (remove manually if desired)`;
440
+ }
441
+ try {
442
+ const postSnapshot = await captureSnapshot();
443
+ await saveSnapshot(postSnapshot, "post-reconcile");
444
+ } catch (err) {
445
+ logger.warn("Failed to save post-reconcile snapshot", { error: String(err) });
446
+ }
447
+ logger.info("Brewfile reconcile complete", {
448
+ missing: report.missingPackages.length,
449
+ extra: report.extraPackages.length,
450
+ wrong: report.wrongVersions.length
451
+ });
452
+ yield "\u2713 Reconciliation complete.";
453
+ }
454
+ function isNodeError(err) {
455
+ return err instanceof Error && "code" in err;
456
+ }
457
+
458
+ export {
459
+ diffSnapshots,
460
+ BREWFILE_PATH,
461
+ loadBrewfile,
462
+ saveBrewfile,
463
+ createDefaultBrewfile,
464
+ computeDrift,
465
+ reconcile
466
+ };
467
+ //# sourceMappingURL=chunk-LXF72RCD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/brewfile/brewfile-manager.ts","../src/lib/diff-engine/diff.ts","../src/lib/brewfile/yaml-serializer.ts"],"sourcesContent":["import { readFile, writeFile, rename } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { DATA_DIR, ensureDataDirs } from '../data-dir.js';\nimport { captureSnapshot, saveSnapshot } from '../state-snapshot/snapshot.js';\nimport { diffDesiredActual } from '../diff-engine/diff.js';\nimport { serializeBrewfile, parseBrewfile } from './yaml-serializer.js';\nimport { streamBrew } from '../brew-cli.js';\nimport { logger } from '../../utils/logger.js';\nimport type { BrewfileSchema, DriftReport } from './types.js';\n\nexport const BREWFILE_PATH = join(DATA_DIR, 'brewfile.yaml');\n\nexport async function loadBrewfile(): Promise<BrewfileSchema | null> {\n try {\n const raw = await readFile(BREWFILE_PATH, 'utf-8');\n return parseBrewfile(raw);\n } catch (err) {\n if (isNodeError(err) && err.code === 'ENOENT') {\n return null;\n }\n logger.warn('Failed to parse Brewfile', { error: String(err) });\n return null;\n }\n}\n\nexport async function saveBrewfile(schema: BrewfileSchema): Promise<void> {\n await ensureDataDirs();\n const updated: BrewfileSchema = {\n ...schema,\n meta: { ...schema.meta, updatedAt: new Date().toISOString() },\n };\n const tmpPath = BREWFILE_PATH + '.tmp';\n await writeFile(tmpPath, serializeBrewfile(updated), { encoding: 'utf-8', mode: 0o600 });\n await rename(tmpPath, BREWFILE_PATH);\n}\n\nexport async function createDefaultBrewfile(name: string): Promise<BrewfileSchema> {\n const snapshot = await captureSnapshot();\n const now = new Date().toISOString();\n\n const schema: BrewfileSchema = {\n version: 1,\n meta: { name, createdAt: now, updatedAt: now },\n formulae: snapshot.formulae.map((f) => ({ name: f.name })),\n casks: snapshot.casks.map((c) => ({ name: c.name })),\n taps: [...snapshot.taps],\n strictMode: false,\n };\n\n return schema;\n}\n\nexport async function computeDrift(schema: BrewfileSchema): Promise<DriftReport> {\n const snapshot = await captureSnapshot();\n const diff = diffDesiredActual(schema, snapshot);\n\n // missingPackages: in desired but not in actual (added in diff means \"missing from actual\")\n const missingPackages = diff.added\n .filter((e) => e.type !== 'tap')\n .map((e) => e.name);\n\n // extraPackages: in actual but not in desired (removed in diff means \"extra in actual\")\n const extraPackages = diff.removed\n .filter((e) => e.type !== 'tap')\n .map((e) => e.name);\n\n // wrongVersions: upgraded + downgraded entries\n const wrongVersions = [\n ...diff.upgraded.map((e) => ({ name: e.name, desired: e.to, actual: e.from })),\n ...diff.downgraded.map((e) => ({ name: e.name, desired: e.to, actual: e.from })),\n ];\n\n const penalty =\n missingPackages.length * 10 +\n extraPackages.length * 2 +\n wrongVersions.length * 5;\n\n const score = Math.max(0, Math.min(100, 100 - penalty));\n\n return { diff, score, missingPackages, extraPackages, wrongVersions };\n}\n\nexport async function* reconcile(\n schema: BrewfileSchema,\n isPro: boolean,\n): AsyncGenerator<string> {\n if (!isPro) {\n yield 'Pro license required for reconcile.';\n return;\n }\n\n const report = await computeDrift(schema);\n\n if (\n report.missingPackages.length === 0 &&\n report.wrongVersions.length === 0 &&\n report.extraPackages.length === 0\n ) {\n yield 'Already in sync — nothing to do.';\n return;\n }\n\n // Install missing packages\n for (const name of report.missingPackages) {\n yield `→ Installing ${name}...`;\n try {\n for await (const line of streamBrew(['install', name])) {\n yield line;\n }\n } catch (err) {\n yield `✗ Failed to install ${name}: ${err instanceof Error ? err.message : String(err)}`;\n }\n }\n\n // Fix wrong versions\n for (const { name, desired } of report.wrongVersions) {\n const target = `${name}@${desired}`;\n yield `→ Installing ${target}...`;\n try {\n for await (const line of streamBrew(['install', target])) {\n yield line;\n }\n } catch (err) {\n yield `✗ Failed to install ${target}: ${err instanceof Error ? err.message : String(err)}`;\n }\n }\n\n // Extra packages (strict mode) — warn only, never auto-uninstall\n for (const name of report.extraPackages) {\n yield `⚠ Extra package not in Brewfile: ${name} (remove manually if desired)`;\n }\n\n // Save post-reconcile snapshot\n try {\n const postSnapshot = await captureSnapshot();\n await saveSnapshot(postSnapshot, 'post-reconcile');\n } catch (err) {\n logger.warn('Failed to save post-reconcile snapshot', { error: String(err) });\n }\n\n logger.info('Brewfile reconcile complete', {\n missing: report.missingPackages.length,\n extra: report.extraPackages.length,\n wrong: report.wrongVersions.length,\n });\n\n yield '✓ Reconciliation complete.';\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && 'code' in err;\n}\n","import type { BrewSnapshot } from '../state-snapshot/snapshot.js';\n\nexport interface BrewDiff {\n added: Array<{ name: string; version: string; type: 'formula' | 'cask' | 'tap' }>;\n removed: Array<{ name: string; version: string; type: 'formula' | 'cask' | 'tap' }>;\n upgraded: Array<{ name: string; from: string; to: string; type: 'formula' | 'cask' }>;\n downgraded: Array<{ name: string; from: string; to: string; type: 'formula' | 'cask' }>;\n}\n\n// Temporary — replaced in Phase 2 when brewfile module is implemented\ninterface BrewfileSchema {\n formulae: Array<{ name: string; version?: string }>;\n casks: Array<{ name: string; version?: string }>;\n taps: string[];\n strictMode?: boolean;\n}\n\n/** Compare two Homebrew version strings segment by segment.\n * Returns positive if a > b, negative if a < b, 0 if equal.\n *\n * Handles Homebrew-specific formats: `1.2.3_1` (revision suffix),\n * date-based (`2024.05.20`), and alpha/rc segments compared lexically\n * when they cannot be parsed as integers.\n */\nfunction compareVersions(a: string, b: string): number {\n // Split on `.` to get segments; within each segment handle `_N` revision.\n const splitSegment = (seg: string): [number, number] => {\n const underIdx = seg.indexOf('_');\n if (underIdx !== -1) {\n const main = parseInt(seg.slice(0, underIdx), 10);\n const rev = parseInt(seg.slice(underIdx + 1), 10);\n return [isNaN(main) ? -1 : main, isNaN(rev) ? 0 : rev];\n }\n const n = parseInt(seg, 10);\n return [isNaN(n) ? -1 : n, 0];\n };\n\n const aParts = a.split('.');\n const bParts = b.split('.');\n const len = Math.max(aParts.length, bParts.length);\n\n for (let i = 0; i < len; i++) {\n const aSeg = aParts[i] ?? '0';\n const bSeg = bParts[i] ?? '0';\n\n const [aMain, aRev] = splitSegment(aSeg);\n const [bMain, bRev] = splitSegment(bSeg);\n\n // Fall back to lexical comparison when segments are non-numeric on both sides\n if (aMain === -1 && bMain === -1) {\n const cmp = aSeg.localeCompare(bSeg);\n if (cmp !== 0) return cmp;\n continue;\n }\n // Treat non-numeric as lower than any numeric\n const aVal = aMain === -1 ? -1 : aMain;\n const bVal = bMain === -1 ? -1 : bMain;\n\n if (aVal !== bVal) return aVal - bVal;\n if (aRev !== bRev) return aRev - bRev;\n }\n\n return 0;\n}\n\nfunction diffPackages<T extends { name: string; version: string }>(\n base: T[],\n current: T[],\n type: 'formula' | 'cask',\n result: BrewDiff,\n): void {\n const baseMap = new Map(base.map((p) => [p.name, p.version]));\n const currentMap = new Map(current.map((p) => [p.name, p.version]));\n\n for (const [name, version] of currentMap) {\n if (!baseMap.has(name)) {\n result.added.push({ name, version, type });\n } else {\n const baseVersion = baseMap.get(name)!;\n if (version !== baseVersion) {\n const cmp = compareVersions(version, baseVersion);\n if (cmp > 0) {\n result.upgraded.push({ name, from: baseVersion, to: version, type });\n } else {\n result.downgraded.push({ name, from: baseVersion, to: version, type });\n }\n }\n }\n }\n\n for (const [name, version] of baseMap) {\n if (!currentMap.has(name)) {\n result.removed.push({ name, version, type });\n }\n }\n}\n\nexport function diffSnapshots(base: BrewSnapshot, current: BrewSnapshot): BrewDiff {\n const result: BrewDiff = { added: [], removed: [], upgraded: [], downgraded: [] };\n\n diffPackages(base.formulae, current.formulae, 'formula', result);\n diffPackages(base.casks, current.casks, 'cask', result);\n\n const baseSet = new Set(base.taps);\n const currentSet = new Set(current.taps);\n\n for (const tap of currentSet) {\n if (!baseSet.has(tap)) result.added.push({ name: tap, version: '', type: 'tap' });\n }\n for (const tap of baseSet) {\n if (!currentSet.has(tap)) result.removed.push({ name: tap, version: '', type: 'tap' });\n }\n\n return result;\n}\n\n/** Compare a desired Brewfile schema against the actual installed snapshot.\n *\n * - Packages in desired but not in actual → added\n * - Packages in actual but not in desired → removed (only when strictMode=true)\n * - Packages in both, desired.version defined and differs from actual → upgraded/downgraded\n * - Taps follow the same logic (no version concept)\n */\nexport function diffDesiredActual(desired: BrewfileSchema, actual: BrewSnapshot): BrewDiff {\n const result: BrewDiff = { added: [], removed: [], upgraded: [], downgraded: [] };\n const strict = desired.strictMode === true;\n\n function processPackages(\n desiredPkgs: Array<{ name: string; version?: string }>,\n actualPkgs: Array<{ name: string; version: string }>,\n type: 'formula' | 'cask',\n ): void {\n const actualMap = new Map(actualPkgs.map((p) => [p.name, p.version]));\n const desiredNames = new Set(desiredPkgs.map((p) => p.name));\n\n for (const pkg of desiredPkgs) {\n const actualVersion = actualMap.get(pkg.name);\n if (actualVersion === undefined) {\n // Package missing from actual — mark as added (needs to be installed)\n result.added.push({ name: pkg.name, version: pkg.version ?? '', type });\n } else if (pkg.version !== undefined && pkg.version !== actualVersion) {\n const cmp = compareVersions(pkg.version, actualVersion);\n if (cmp > 0) {\n result.upgraded.push({ name: pkg.name, from: actualVersion, to: pkg.version, type });\n } else {\n result.downgraded.push({ name: pkg.name, from: actualVersion, to: pkg.version, type });\n }\n }\n }\n\n if (strict) {\n for (const pkg of actualPkgs) {\n if (!desiredNames.has(pkg.name)) {\n // Extra package in actual not in desired — mark as removed (violation)\n result.removed.push({ name: pkg.name, version: pkg.version, type });\n }\n }\n }\n }\n\n processPackages(desired.formulae, actual.formulae, 'formula');\n processPackages(desired.casks, actual.casks, 'cask');\n\n // Taps\n const actualTapSet = new Set(actual.taps);\n const desiredTapSet = new Set(desired.taps);\n\n for (const tap of desiredTapSet) {\n if (!actualTapSet.has(tap)) {\n result.added.push({ name: tap, version: '', type: 'tap' });\n }\n }\n\n if (strict) {\n for (const tap of actualTapSet) {\n if (!desiredTapSet.has(tap)) {\n result.removed.push({ name: tap, version: '', type: 'tap' });\n }\n }\n }\n\n return result;\n}\n","import type { BrewfileSchema } from './types.js';\n\n/**\n * Minimal YAML serializer/deserializer for BrewfileSchema.\n * No external dependencies — supports only the exact format we emit.\n *\n * Quoting rules: single-quote strings that contain :, #, ', \";\n * start with -, [, {, ?, !, @, &, *, >, |, %, digits, or have\n * leading/trailing whitespace, or are empty.\n * Inside single-quoted YAML, ' is escaped as ''.\n */\n\nconst MUST_QUOTE_RE = /[:'\"#[\\]{}?!@&*>|%]/;\nconst STARTS_SPECIAL_RE = /^[-\\s]|^\\d/;\n\nfunction needsQuoting(s: string): boolean {\n if (s === '') return true;\n if (STARTS_SPECIAL_RE.test(s)) return true;\n if (MUST_QUOTE_RE.test(s)) return true;\n if (s !== s.trimStart() || s !== s.trimEnd()) return true;\n return false;\n}\n\nfunction quote(s: string): string {\n if (!needsQuoting(s)) return s;\n return `'${s.replace(/'/g, \"''\")}'`;\n}\n\nfunction unquote(s: string): string {\n const trimmed = s.trim();\n if (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\") && trimmed.length >= 2) {\n return trimmed.slice(1, -1).replace(/''/g, \"'\");\n }\n return trimmed;\n}\n\n// ── Serializer ──────────────────────────────────────────────────────────────\n\nexport function serializeBrewfile(schema: BrewfileSchema): string {\n const lines: string[] = [];\n\n lines.push(`version: ${schema.version}`);\n lines.push('meta:');\n lines.push(` name: ${quote(schema.meta.name)}`);\n if (schema.meta.description !== undefined) {\n lines.push(` description: ${quote(schema.meta.description)}`);\n }\n lines.push(` createdAt: ${quote(schema.meta.createdAt)}`);\n lines.push(` updatedAt: ${quote(schema.meta.updatedAt)}`);\n\n lines.push('formulae:');\n if (schema.formulae.length === 0) {\n lines.push(' []');\n } else {\n for (const f of schema.formulae) {\n lines.push(` - name: ${quote(f.name)}`);\n if (f.version !== undefined) {\n lines.push(` version: ${quote(f.version)}`);\n }\n if (f.options !== undefined && f.options.length > 0) {\n lines.push(' options:');\n for (const opt of f.options) {\n lines.push(` - ${quote(opt)}`);\n }\n }\n }\n }\n\n lines.push('casks:');\n if (schema.casks.length === 0) {\n lines.push(' []');\n } else {\n for (const c of schema.casks) {\n lines.push(` - name: ${quote(c.name)}`);\n if (c.version !== undefined) {\n lines.push(` version: ${quote(c.version)}`);\n }\n }\n }\n\n lines.push('taps:');\n if (schema.taps.length === 0) {\n lines.push(' []');\n } else {\n for (const tap of schema.taps) {\n lines.push(` - ${quote(tap)}`);\n }\n }\n\n if (schema.strictMode !== undefined) {\n lines.push(`strictMode: ${schema.strictMode}`);\n }\n\n return lines.join('\\n') + '\\n';\n}\n\n// ── Parser ───────────────────────────────────────────────────────────────────\n\ntype ParseContext =\n | 'root'\n | 'meta'\n | 'formulae'\n | 'formulae_item'\n | 'formulae_options'\n | 'casks'\n | 'casks_item'\n | 'taps';\n\nexport function parseBrewfile(yaml: string): BrewfileSchema {\n const rawLines = yaml.split('\\n');\n\n // Working state\n let version: number | undefined;\n const meta: Partial<{ name: string; description: string; createdAt: string; updatedAt: string }> = {};\n const formulae: BrewfileSchema['formulae'] = [];\n const casks: BrewfileSchema['casks'] = [];\n const taps: string[] = [];\n let strictMode: boolean | undefined;\n\n let context: ParseContext = 'root';\n let currentFormula: (typeof formulae)[number] | null = null;\n let currentCask: (typeof casks)[number] | null = null;\n\n for (const rawLine of rawLines) {\n // Skip blank lines and comments\n const line = rawLine;\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Detect indentation level\n const indent = line.length - line.trimStart().length;\n\n // Empty inline arrays (formulae: [], casks: [], taps: [])\n if (trimmed.endsWith(': []')) {\n const key = trimmed.slice(0, -4);\n if (key === 'formulae') { context = 'formulae'; continue; }\n if (key === 'casks') { context = 'casks'; continue; }\n if (key === 'taps') { context = 'taps'; continue; }\n }\n\n // Root-level keys (indent === 0, contains ':')\n if (indent === 0) {\n if (trimmed.startsWith('version:')) {\n const val = trimmed.slice('version:'.length).trim();\n version = parseInt(val, 10);\n context = 'root';\n continue;\n }\n if (trimmed === 'meta:') { context = 'meta'; continue; }\n if (trimmed === 'formulae:') { context = 'formulae'; continue; }\n if (trimmed === 'casks:') { context = 'casks'; continue; }\n if (trimmed === 'taps:') { context = 'taps'; continue; }\n if (trimmed.startsWith('strictMode:')) {\n const val = trimmed.slice('strictMode:'.length).trim();\n strictMode = val === 'true';\n context = 'root';\n continue;\n }\n // Unknown root key — skip\n context = 'root';\n continue;\n }\n\n // ── meta block (indent 2) ──\n if (context === 'meta' && indent === 2) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx === -1) continue;\n const key = trimmed.slice(0, colonIdx).trim();\n const val = unquote(trimmed.slice(colonIdx + 1));\n if (key === 'name') meta.name = val;\n else if (key === 'description') meta.description = val;\n else if (key === 'createdAt') meta.createdAt = val;\n else if (key === 'updatedAt') meta.updatedAt = val;\n continue;\n }\n\n // ── formulae block ──\n if (context === 'formulae' || context === 'formulae_item' || context === 'formulae_options') {\n // New formula item: \" - name: value\" (indent 2)\n if (indent === 2 && trimmed.startsWith('- name:')) {\n const val = unquote(trimmed.slice('- name:'.length));\n currentFormula = { name: val };\n formulae.push(currentFormula);\n context = 'formulae_item';\n continue;\n }\n // Properties of current formula (indent 4)\n if (indent === 4 && context === 'formulae_item') {\n if (trimmed.startsWith('version:')) {\n if (currentFormula) currentFormula.version = unquote(trimmed.slice('version:'.length));\n } else if (trimmed === 'options:') {\n if (currentFormula) currentFormula.options = [];\n context = 'formulae_options';\n }\n continue;\n }\n // Options items (indent 6): \" - value\"\n if (indent === 6 && context === 'formulae_options') {\n if (trimmed.startsWith('- ')) {\n const val = unquote(trimmed.slice(2));\n if (currentFormula?.options) currentFormula.options.push(val);\n }\n continue;\n }\n // If we see something at indent 4 after options, back to formulae_item\n if (indent === 4 && context === 'formulae_options') {\n context = 'formulae_item';\n if (trimmed.startsWith('version:')) {\n if (currentFormula) currentFormula.version = unquote(trimmed.slice('version:'.length));\n }\n continue;\n }\n }\n\n // ── casks block ──\n if (context === 'casks' || context === 'casks_item') {\n if (indent === 2 && trimmed.startsWith('- name:')) {\n const val = unquote(trimmed.slice('- name:'.length));\n currentCask = { name: val };\n casks.push(currentCask);\n context = 'casks_item';\n continue;\n }\n if (indent === 4 && context === 'casks_item') {\n if (trimmed.startsWith('version:')) {\n if (currentCask) currentCask.version = unquote(trimmed.slice('version:'.length));\n }\n continue;\n }\n }\n\n // ── taps block (indent 2): \" - value\" ──\n if (context === 'taps' && indent === 2 && trimmed.startsWith('- ')) {\n taps.push(unquote(trimmed.slice(2)));\n continue;\n }\n }\n\n // Validate\n if (version !== 1) {\n throw new Error(`Invalid Brewfile: expected version 1, got ${String(version)}`);\n }\n if (!meta.name) {\n throw new Error('Invalid Brewfile: missing meta.name');\n }\n if (!meta.createdAt || !meta.updatedAt) {\n throw new Error('Invalid Brewfile: missing meta.createdAt or meta.updatedAt');\n }\n\n const result: BrewfileSchema = {\n version: 1,\n meta: {\n name: meta.name,\n createdAt: meta.createdAt,\n updatedAt: meta.updatedAt,\n },\n formulae,\n casks,\n taps,\n };\n\n if (meta.description !== undefined) result.meta.description = meta.description;\n if (strictMode !== undefined) result.strictMode = strictMode;\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,123 @@
1
+ import {
2
+ captureSnapshot
3
+ } from "./chunk-4I344KQX.js";
4
+
5
+ // src/lib/compliance/compliance-checker.ts
6
+ import { hostname } from "os";
7
+ function versionAtLeast(installed, minimum) {
8
+ const parseSegments = (v) => v.split(".").map((seg) => parseInt(seg.replace(/[^0-9]/g, ""), 10) || 0);
9
+ const ins = parseSegments(installed);
10
+ const min = parseSegments(minimum);
11
+ const len = Math.max(ins.length, min.length);
12
+ for (let i = 0; i < len; i++) {
13
+ const a = ins[i] ?? 0;
14
+ const b = min[i] ?? 0;
15
+ if (a > b) return true;
16
+ if (a < b) return false;
17
+ }
18
+ return true;
19
+ }
20
+ async function checkCompliance(policy, isPro) {
21
+ if (!isPro) throw new Error("Pro license required");
22
+ const snapshot = await captureSnapshot();
23
+ const violations = [];
24
+ const formulaeMap = new Map(snapshot.formulae.map((f) => [f.name, f]));
25
+ const casksMap = new Map(snapshot.casks.map((c) => [c.name, c]));
26
+ const tapsSet = new Set(snapshot.taps);
27
+ for (const req of policy.required) {
28
+ const entry = req.type === "formula" ? formulaeMap.get(req.name) : casksMap.get(req.name);
29
+ if (!entry) {
30
+ violations.push({
31
+ severity: "error",
32
+ type: "missing",
33
+ packageName: req.name,
34
+ packageType: req.type,
35
+ detail: `Missing: ${req.name} (required)`,
36
+ required: req.minVersion
37
+ });
38
+ continue;
39
+ }
40
+ if (req.minVersion && !versionAtLeast(entry.version, req.minVersion)) {
41
+ violations.push({
42
+ severity: "error",
43
+ type: "wrong-version",
44
+ packageName: req.name,
45
+ packageType: req.type,
46
+ detail: `Wrong version: ${req.name} (required ${req.minVersion}, installed ${entry.version})`,
47
+ required: req.minVersion,
48
+ installed: entry.version
49
+ });
50
+ }
51
+ }
52
+ for (const forbidden of policy.forbidden) {
53
+ const present = forbidden.type === "formula" ? formulaeMap.has(forbidden.name) : casksMap.has(forbidden.name);
54
+ if (present) {
55
+ const reason = forbidden.reason ? ` \u2014 ${forbidden.reason}` : "";
56
+ violations.push({
57
+ severity: "error",
58
+ type: "forbidden",
59
+ packageName: forbidden.name,
60
+ packageType: forbidden.type,
61
+ detail: `Forbidden: ${forbidden.name}${reason}`
62
+ });
63
+ }
64
+ }
65
+ for (const tap of policy.requiredTaps) {
66
+ if (!tapsSet.has(tap)) {
67
+ violations.push({
68
+ severity: "warning",
69
+ type: "missing",
70
+ packageName: tap,
71
+ packageType: "formula",
72
+ detail: `Missing tap: ${tap} (required)`
73
+ });
74
+ }
75
+ }
76
+ if (policy.strictMode === true) {
77
+ const requiredNames = new Set(
78
+ policy.required.filter((r) => r.type === "formula").map((r) => r.name)
79
+ );
80
+ const requiredCasks = new Set(
81
+ policy.required.filter((r) => r.type === "cask").map((r) => r.name)
82
+ );
83
+ for (const f of snapshot.formulae) {
84
+ if (!requiredNames.has(f.name)) {
85
+ violations.push({
86
+ severity: "warning",
87
+ type: "extra",
88
+ packageName: f.name,
89
+ packageType: "formula",
90
+ detail: `Extra package: ${f.name}`
91
+ });
92
+ }
93
+ }
94
+ for (const c of snapshot.casks) {
95
+ if (!requiredCasks.has(c.name)) {
96
+ violations.push({
97
+ severity: "warning",
98
+ type: "extra",
99
+ packageName: c.name,
100
+ packageType: "cask",
101
+ detail: `Extra package: ${c.name}`
102
+ });
103
+ }
104
+ }
105
+ }
106
+ const errors = violations.filter((v) => v.severity === "error").length;
107
+ const warnings = violations.filter((v) => v.severity === "warning").length;
108
+ const score = Math.max(0, 100 - errors * 15 - warnings * 5);
109
+ return {
110
+ compliant: violations.length === 0,
111
+ score,
112
+ violations,
113
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
114
+ machineName: hostname(),
115
+ policyName: policy.meta.teamName
116
+ };
117
+ }
118
+
119
+ export {
120
+ versionAtLeast,
121
+ checkCompliance
122
+ };
123
+ //# sourceMappingURL=chunk-U2DRWB7A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/compliance/compliance-checker.ts"],"sourcesContent":["import { hostname } from 'node:os';\nimport type { PolicyFile, ComplianceReport, ComplianceViolation } from './types.js';\nimport { captureSnapshot } from '../state-snapshot/snapshot.js';\n\n/**\n * Compara versiones segmento a segmento ignorando sufijos alfa.\n * \"3.12.1\" >= \"3.11\" → true. \"3.10.0\" >= \"3.11\" → false.\n */\nexport function versionAtLeast(installed: string, minimum: string): boolean {\n const parseSegments = (v: string): number[] =>\n v.split('.').map((seg) => parseInt(seg.replace(/[^0-9]/g, ''), 10) || 0);\n\n const ins = parseSegments(installed);\n const min = parseSegments(minimum);\n const len = Math.max(ins.length, min.length);\n\n for (let i = 0; i < len; i++) {\n const a = ins[i] ?? 0;\n const b = min[i] ?? 0;\n if (a > b) return true;\n if (a < b) return false;\n }\n return true; // igual\n}\n\nexport async function checkCompliance(\n policy: PolicyFile,\n isPro: boolean,\n): Promise<ComplianceReport> {\n if (!isPro) throw new Error('Pro license required');\n\n const snapshot = await captureSnapshot();\n const violations: ComplianceViolation[] = [];\n\n // Conjuntos rápidos para lookup\n const formulaeMap = new Map(snapshot.formulae.map((f) => [f.name, f]));\n const casksMap = new Map(snapshot.casks.map((c) => [c.name, c]));\n const tapsSet = new Set(snapshot.taps);\n\n // Required packages\n for (const req of policy.required) {\n const entry =\n req.type === 'formula' ? formulaeMap.get(req.name) : casksMap.get(req.name);\n\n if (!entry) {\n violations.push({\n severity: 'error',\n type: 'missing',\n packageName: req.name,\n packageType: req.type,\n detail: `Missing: ${req.name} (required)`,\n required: req.minVersion,\n });\n continue;\n }\n\n if (req.minVersion && !versionAtLeast(entry.version, req.minVersion)) {\n violations.push({\n severity: 'error',\n type: 'wrong-version',\n packageName: req.name,\n packageType: req.type,\n detail: `Wrong version: ${req.name} (required ${req.minVersion}, installed ${entry.version})`,\n required: req.minVersion,\n installed: entry.version,\n });\n }\n }\n\n // Forbidden packages\n for (const forbidden of policy.forbidden) {\n const present =\n forbidden.type === 'formula'\n ? formulaeMap.has(forbidden.name)\n : casksMap.has(forbidden.name);\n\n if (present) {\n const reason = forbidden.reason ? ` — ${forbidden.reason}` : '';\n violations.push({\n severity: 'error',\n type: 'forbidden',\n packageName: forbidden.name,\n packageType: forbidden.type,\n detail: `Forbidden: ${forbidden.name}${reason}`,\n });\n }\n }\n\n // Required taps\n for (const tap of policy.requiredTaps) {\n if (!tapsSet.has(tap)) {\n violations.push({\n severity: 'warning',\n type: 'missing',\n packageName: tap,\n packageType: 'formula',\n detail: `Missing tap: ${tap} (required)`,\n });\n }\n }\n\n // Strict mode: paquetes extra\n if (policy.strictMode === true) {\n const requiredNames = new Set(\n policy.required.filter((r) => r.type === 'formula').map((r) => r.name),\n );\n const requiredCasks = new Set(\n policy.required.filter((r) => r.type === 'cask').map((r) => r.name),\n );\n\n for (const f of snapshot.formulae) {\n if (!requiredNames.has(f.name)) {\n violations.push({\n severity: 'warning',\n type: 'extra',\n packageName: f.name,\n packageType: 'formula',\n detail: `Extra package: ${f.name}`,\n });\n }\n }\n\n for (const c of snapshot.casks) {\n if (!requiredCasks.has(c.name)) {\n violations.push({\n severity: 'warning',\n type: 'extra',\n packageName: c.name,\n packageType: 'cask',\n detail: `Extra package: ${c.name}`,\n });\n }\n }\n }\n\n // Score: errores -15pts, warnings -5pts\n const errors = violations.filter((v) => v.severity === 'error').length;\n const warnings = violations.filter((v) => v.severity === 'warning').length;\n const score = Math.max(0, 100 - errors * 15 - warnings * 5);\n\n return {\n compliant: violations.length === 0,\n score,\n violations,\n checkedAt: new Date().toISOString(),\n machineName: hostname(),\n policyName: policy.meta.teamName,\n };\n}\n"],"mappings":"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","names":[]}