@utilix-tech/sdk 0.7.0 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -2,11 +2,29 @@
2
2
 
3
3
  Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
4
 
5
- ## 0.3.1
6
- - Metadata fixes: corrected repository/bug tracker links, updated tool count in the package description.
5
+ ## 0.9.0
6
+ - Added Sitemap.xml Validator.
7
+
8
+ ## 0.8.0
9
+ - Added ID3 Tag Reader and .env.example Diff Checker.
10
+
11
+ ## 0.7.0
12
+ - Added WAV Audio Info and Readability Score Calculator.
13
+
14
+ ## 0.6.0
15
+ - Added EXIF Viewer and Commit Message Generator (AI).
16
+
17
+ ## 0.5.0
18
+ - Added PDF Metadata Reader and JWK Viewer.
19
+
20
+ ## 0.4.1
21
+ - Added HAR File Viewer and NDJSON Formatter/Validator.
22
+
23
+ ## 0.4.0
24
+ - Metadata fixes: corrected repository/bug tracker links, updated the stale dependency, added missing package files.
7
25
 
8
26
  ## 0.3.0
9
- - Added the `media` module.
27
+ - Added the `media` module (Passive Voice Detector, Image Info).
10
28
 
11
29
  ## 0.2.0
12
30
  - 400+ functions across 14 modules, including the `ai_agent` module for LLM/RAG pipelines.
package/README.md CHANGED
@@ -156,12 +156,12 @@ scoreReadability("The cat sat on the mat. It was a sunny day.");
156
156
 
157
157
  ---
158
158
 
159
- ### `/data`: CSV / YAML / TOML / XML / INI / NDJSON
159
+ ### `/data`: CSV / YAML / TOML / XML / INI / NDJSON / .env diff
160
160
 
161
161
  ```ts
162
162
  import { parseCsv, csvToJson, validateYaml, tomlToJson, jsonToToml,
163
163
  formatXml, xmlToJson, jsonToXml, parseIni,
164
- validateNdjson, formatNdjson, ndjsonToJsonArray } from "@utilix-tech/sdk/data";
164
+ validateNdjson, formatNdjson, ndjsonToJsonArray, diffEnvExample } from "@utilix-tech/sdk/data";
165
165
 
166
166
  // CSV
167
167
  const rows = parseCsv("name,age\nalice,30\nbob,25");
@@ -185,6 +185,10 @@ parseIni("[db]\nhost=localhost\nport=5432");
185
185
  validateNdjson('{"id":1}\n{"id":2}'); // { valid: true, totalLines: 2, ... }
186
186
  formatNdjson('{"id":1}'); // '{\n "id": 1\n}'
187
187
  ndjsonToJsonArray('{"id":1}\n{"id":2}'); // '[\n { "id": 1 },\n { "id": 2 }\n]'
188
+
189
+ // Diff a .env file against its .env.example template
190
+ diffEnvExample("FOO=1\nBAR=2", "FOO=\nBAR=y\nBAZ=z");
191
+ // { missingInExample: [], missingInEnv: ["BAZ"], emptyValueInExample: ["FOO"], keysInBoth: 2 }
188
192
  ```
189
193
 
190
194
  ---
@@ -265,7 +269,7 @@ formatValue(1234567.89, { locale: "en-US", currency: "USD" });
265
269
  ```ts
266
270
  import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
267
271
  searchStatusCodes, buildGeoUrl, countryCodeToFlag,
268
- parseHar } from "@utilix-tech/sdk/network";
272
+ parseHar, validateSitemap } from "@utilix-tech/sdk/network";
269
273
 
270
274
  getStatusCode(404);
271
275
  // { code: 404, text: "Not Found", description: "...", category: "Client Error" }
@@ -282,6 +286,10 @@ countryCodeToFlag("US"); // "🇺🇸"
282
286
  const har = await fs.promises.readFile("network.har", "utf-8");
283
287
  parseHar(har);
284
288
  // { version: "1.2", entries: [...], summary: { totalRequests, totalSize, failedRequests, ... } }
289
+
290
+ // Validate a sitemap.xml (or sitemap index) against the sitemaps.org protocol
291
+ validateSitemap('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://example.com/</loc></url></urlset>');
292
+ // { type: "urlset", urlCount: 1, entries: [...], issues: [...], valid: true }
285
293
  ```
286
294
 
287
295
  ---
@@ -420,10 +428,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
420
428
 
421
429
  ---
422
430
 
423
- ### `/media`: Image Header Parsing, EXIF, PDF & WAV Metadata
431
+ ### `/media`: Image Header Parsing, EXIF, PDF, WAV & ID3 Metadata
424
432
 
425
433
  ```ts
426
- import { readImageInfo, readExifData, readPdfMetadata, readWavInfo } from "@utilix-tech/sdk/media";
434
+ import { readImageInfo, readExifData, readPdfMetadata, readWavInfo, readId3Tags } from "@utilix-tech/sdk/media";
427
435
 
428
436
  // Reads format, dimensions, bit depth, and alpha channel straight from
429
437
  // file bytes: no decoding, no canvas, no image library.
@@ -448,9 +456,16 @@ readPdfMetadata(new Uint8Array(pdfBytes));
448
456
  const wavBytes = await fs.promises.readFile("recording.wav");
449
457
  readWavInfo(new Uint8Array(wavBytes));
450
458
  // { audioFormat: 1, audioFormatLabel: "PCM", channels: 2, sampleRate: 44100, bitsPerSample: 16, durationSeconds: 12.4, ... }
459
+
460
+ // Reads title, artist, album, year, genre, comment, and track number from
461
+ // an MP3's ID3 tags. Prefers ID3v2.3/2.4 text frames, falls back to the
462
+ // classic 128-byte ID3v1/1.1 trailer.
463
+ const mp3Bytes = await fs.promises.readFile("track.mp3");
464
+ readId3Tags(new Uint8Array(mp3Bytes));
465
+ // { version: "ID3v2.3.0", title: "Track Name", artist: "Artist Name", genre: "Rock", ... }
451
466
  ```
452
467
 
453
- `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only.
468
+ `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded).
454
469
 
455
470
  ---
456
471
 
@@ -4,6 +4,7 @@ import { __export } from './chunk-MLKGABMK.js';
4
4
  var media_exports = {};
5
5
  __export(media_exports, {
6
6
  readExifData: () => readExifData,
7
+ readId3Tags: () => readId3Tags,
7
8
  readImageInfo: () => readImageInfo,
8
9
  readPdfMetadata: () => readPdfMetadata,
9
10
  readWavInfo: () => readWavInfo
@@ -520,5 +521,214 @@ function readWavInfo(bytes) {
520
521
  fileSize
521
522
  };
522
523
  }
524
+ var ID3V1_GENRES = [
525
+ "Blues",
526
+ "Classic Rock",
527
+ "Country",
528
+ "Dance",
529
+ "Disco",
530
+ "Funk",
531
+ "Grunge",
532
+ "Hip-Hop",
533
+ "Jazz",
534
+ "Metal",
535
+ "New Age",
536
+ "Oldies",
537
+ "Other",
538
+ "Pop",
539
+ "R&B",
540
+ "Rap",
541
+ "Reggae",
542
+ "Rock",
543
+ "Techno",
544
+ "Industrial",
545
+ "Alternative",
546
+ "Ska",
547
+ "Death Metal",
548
+ "Pranks",
549
+ "Soundtrack",
550
+ "Euro-Techno",
551
+ "Ambient",
552
+ "Trip-Hop",
553
+ "Vocal",
554
+ "Jazz+Funk",
555
+ "Fusion",
556
+ "Trance",
557
+ "Classical",
558
+ "Instrumental",
559
+ "Acid",
560
+ "House",
561
+ "Game",
562
+ "Sound Clip",
563
+ "Gospel",
564
+ "Noise",
565
+ "AlternRock",
566
+ "Bass",
567
+ "Soul",
568
+ "Punk",
569
+ "Space",
570
+ "Meditative",
571
+ "Instrumental Pop",
572
+ "Instrumental Rock",
573
+ "Ethnic",
574
+ "Gothic",
575
+ "Darkwave",
576
+ "Techno-Industrial",
577
+ "Electronic",
578
+ "Pop-Folk",
579
+ "Eurodance",
580
+ "Dream",
581
+ "Southern Rock",
582
+ "Comedy",
583
+ "Cult",
584
+ "Gangsta",
585
+ "Top 40",
586
+ "Christian Rap",
587
+ "Pop/Funk",
588
+ "Jungle",
589
+ "Native American",
590
+ "Cabaret",
591
+ "New Wave",
592
+ "Psychedelic",
593
+ "Rave",
594
+ "Showtunes",
595
+ "Trailer",
596
+ "Lo-Fi",
597
+ "Tribal",
598
+ "Acid Punk",
599
+ "Acid Jazz",
600
+ "Polka",
601
+ "Retro",
602
+ "Musical",
603
+ "Rock & Roll",
604
+ "Hard Rock"
605
+ ];
606
+ function readSynchsafeUInt32(bytes, offset) {
607
+ return (bytes[offset] & 127) << 21 | (bytes[offset + 1] & 127) << 14 | (bytes[offset + 2] & 127) << 7 | bytes[offset + 3] & 127;
608
+ }
609
+ function readUInt32BE(bytes, offset) {
610
+ return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
611
+ }
612
+ function decodeId3Text(frameBytes) {
613
+ if (frameBytes.length === 0) return "";
614
+ const encodingByte = frameBytes[0];
615
+ const content = frameBytes.subarray(1);
616
+ let out = "";
617
+ if (encodingByte === 3) {
618
+ out = Buffer.from(content).toString("utf8");
619
+ } else if (encodingByte === 1 || encodingByte === 2) {
620
+ let little = encodingByte === 1;
621
+ let start = 0;
622
+ if (encodingByte === 1 && content.length >= 2) {
623
+ if (content[0] === 255 && content[1] === 254) {
624
+ little = true;
625
+ start = 2;
626
+ } else if (content[0] === 254 && content[1] === 255) {
627
+ little = false;
628
+ start = 2;
629
+ }
630
+ }
631
+ for (let i = start; i + 1 < content.length; i += 2) {
632
+ const code = little ? content[i] | content[i + 1] << 8 : content[i] << 8 | content[i + 1];
633
+ if (code === 0) break;
634
+ out += String.fromCharCode(code);
635
+ }
636
+ } else {
637
+ for (const b of content) {
638
+ if (b === 0) break;
639
+ out += String.fromCharCode(b);
640
+ }
641
+ }
642
+ return out.replace(/\0+$/, "").trim();
643
+ }
644
+ function readId3v2(bytes) {
645
+ if (bytes.length < 10 || bytes[0] !== 73 || bytes[1] !== 68 || bytes[2] !== 51) return null;
646
+ const majorVersion = bytes[3];
647
+ const revision = bytes[4];
648
+ const tagSize = readSynchsafeUInt32(bytes, 6);
649
+ const version = `ID3v2.${majorVersion}.${revision}`;
650
+ if (majorVersion !== 3 && majorVersion !== 4) {
651
+ return { version, frames: {}, frameCount: 0 };
652
+ }
653
+ const frames = {};
654
+ let offset = 10;
655
+ const end = Math.min(10 + tagSize, bytes.length);
656
+ let frameCount = 0;
657
+ while (offset + 10 <= end) {
658
+ const frameId = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
659
+ if (frameId === "\0\0\0\0") break;
660
+ const frameSize = majorVersion === 4 ? readSynchsafeUInt32(bytes, offset + 4) : readUInt32BE(bytes, offset + 4);
661
+ const frameStart = offset + 10;
662
+ if (frameSize <= 0 || frameStart + frameSize > bytes.length) break;
663
+ const frameBytes = bytes.subarray(frameStart, frameStart + frameSize);
664
+ if (frameId[0] === "T" && frameId !== "TXXX") {
665
+ frames[frameId] = decodeId3Text(frameBytes);
666
+ } else if (frameId === "COMM" && frameBytes.length > 4) {
667
+ const rebuilt = new Uint8Array(frameBytes.length - 3);
668
+ rebuilt[0] = frameBytes[0];
669
+ rebuilt.set(frameBytes.subarray(4), 1);
670
+ const decoded = decodeId3Text(rebuilt);
671
+ const parts = decoded.split("\0");
672
+ frames["COMM"] = (parts[parts.length - 1] || decoded).trim();
673
+ }
674
+ frameCount++;
675
+ offset = frameStart + frameSize;
676
+ }
677
+ return { version, frames, frameCount };
678
+ }
679
+ function readId3v1(bytes) {
680
+ if (bytes.length < 128) return null;
681
+ const start = bytes.length - 128;
682
+ if (String.fromCharCode(bytes[start], bytes[start + 1], bytes[start + 2]) !== "TAG") return null;
683
+ const readField = (offset, length) => {
684
+ let s = "";
685
+ for (let i = 0; i < length; i++) {
686
+ const b = bytes[start + offset + i];
687
+ if (b === 0) break;
688
+ s += String.fromCharCode(b);
689
+ }
690
+ return s.trim();
691
+ };
692
+ const title = readField(3, 30);
693
+ const artist = readField(33, 30);
694
+ const album = readField(63, 30);
695
+ const year = readField(93, 4);
696
+ const isV11 = bytes[start + 125] === 0 && bytes[start + 126] !== 0;
697
+ const comment = readField(97, isV11 ? 28 : 30);
698
+ const genreIndex = bytes[start + 127];
699
+ const genre = ID3V1_GENRES[genreIndex] ?? `Unknown (${genreIndex})`;
700
+ const result = { genre };
701
+ if (title) result.title = title;
702
+ if (artist) result.artist = artist;
703
+ if (album) result.album = album;
704
+ if (year) result.year = year;
705
+ if (comment) result.comment = comment;
706
+ if (isV11) result.trackNumber = String(bytes[start + 126]);
707
+ return result;
708
+ }
709
+ function readId3Tags(bytes) {
710
+ const v2 = readId3v2(bytes);
711
+ const v1 = readId3v1(bytes);
712
+ if (v2 && Object.keys(v2.frames).length > 0) {
713
+ const f = v2.frames;
714
+ const result = { version: v2.version, frameCount: v2.frameCount };
715
+ if (f["TIT2"]) result.title = f["TIT2"];
716
+ if (f["TPE1"]) result.artist = f["TPE1"];
717
+ if (f["TALB"]) result.album = f["TALB"];
718
+ const year = f["TYER"] || f["TDRC"];
719
+ if (year) result.year = year;
720
+ if (f["TCON"]) result.genre = f["TCON"];
721
+ if (f["COMM"]) result.comment = f["COMM"];
722
+ if (f["TRCK"]) result.trackNumber = f["TRCK"];
723
+ return result;
724
+ }
725
+ if (v1) {
726
+ return { version: v1.trackNumber !== void 0 ? "ID3v1.1" : "ID3v1", ...v1 };
727
+ }
728
+ if (v2) {
729
+ return { error: `Unsupported ID3 tag version: ${v2.version} (only ID3v2.3, ID3v2.4, and ID3v1/1.1 are supported)` };
730
+ }
731
+ return { error: "No ID3v1 or ID3v2.3/2.4 tags found in this file" };
732
+ }
523
733
 
524
- export { media_exports, readExifData, readImageInfo, readPdfMetadata, readWavInfo };
734
+ export { media_exports, readExifData, readId3Tags, readImageInfo, readPdfMetadata, readWavInfo };
@@ -32,7 +32,8 @@ __export(network_exports, {
32
32
  typeNumberToLabel: () => typeNumberToLabel,
33
33
  validateDomain: () => validateDomain,
34
34
  validateHeaderName: () => validateHeaderName,
35
- validateHeaderValue: () => validateHeaderValue
35
+ validateHeaderValue: () => validateHeaderValue,
36
+ validateSitemap: () => validateSitemap
36
37
  });
37
38
  var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SOA", "PTR", "SRV", "CAA"];
38
39
  function buildDnsQueryUrl(domain, type) {
@@ -1440,5 +1441,95 @@ function parseHar(input) {
1440
1441
  summary: { totalRequests, totalSize, totalTime, avgTime, failedRequests, methods, statusCodes }
1441
1442
  };
1442
1443
  }
1444
+ var VALID_CHANGEFREQ = /* @__PURE__ */ new Set(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]);
1445
+ function sitemapLineNumber(xml, index) {
1446
+ let line = 1;
1447
+ for (let i = 0; i < index; i++) {
1448
+ if (xml[i] === "\n") line++;
1449
+ }
1450
+ return line;
1451
+ }
1452
+ function extractSitemapTag(block, tag) {
1453
+ const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
1454
+ return m ? m[1].trim() : void 0;
1455
+ }
1456
+ function isValidW3CDatetime(s) {
1457
+ return /^\d{4}(-\d{2}(-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?)?)?$/.test(s);
1458
+ }
1459
+ function isAbsoluteHttpUrl(s) {
1460
+ return /^https?:\/\//i.test(s);
1461
+ }
1462
+ function validateSitemap(xml) {
1463
+ if (!xml || !xml.trim()) return { error: "Input is empty" };
1464
+ const trimmed = xml.trim();
1465
+ const issues = [];
1466
+ if (!/^\s*<\?xml/i.test(trimmed)) {
1467
+ issues.push({ severity: "warning", line: 1, message: 'Missing XML declaration (<?xml version="1.0" encoding="UTF-8"?>)' });
1468
+ }
1469
+ const isUrlset = /<urlset[\s>]/i.test(trimmed);
1470
+ const isSitemapIndex = /<sitemapindex[\s>]/i.test(trimmed);
1471
+ if (!isUrlset && !isSitemapIndex) {
1472
+ return { error: "Not a valid sitemap: root element must be <urlset> or <sitemapindex>" };
1473
+ }
1474
+ if (isUrlset && isSitemapIndex) {
1475
+ return { error: "Ambiguous file: contains both <urlset> and <sitemapindex> root elements" };
1476
+ }
1477
+ const type = isUrlset ? "urlset" : "sitemapindex";
1478
+ if (isUrlset && !/xmlns\s*=\s*["']http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9["']/i.test(trimmed)) {
1479
+ issues.push({ severity: "warning", line: 1, message: "Missing or non-standard xmlns namespace (expected http://www.sitemaps.org/schemas/sitemap/0.9)" });
1480
+ }
1481
+ const blockTag = isUrlset ? "url" : "sitemap";
1482
+ const blockRe = new RegExp(`<${blockTag}>([\\s\\S]*?)</${blockTag}>`, "gi");
1483
+ const seenLocs = /* @__PURE__ */ new Set();
1484
+ const entries = [];
1485
+ let match;
1486
+ while ((match = blockRe.exec(trimmed)) !== null) {
1487
+ const block = match[1];
1488
+ const line = sitemapLineNumber(trimmed, match.index);
1489
+ const loc = extractSitemapTag(block, "loc");
1490
+ if (!loc) {
1491
+ issues.push({ severity: "error", line, message: `<${blockTag}> entry is missing required <loc>` });
1492
+ continue;
1493
+ }
1494
+ if (!isAbsoluteHttpUrl(loc)) {
1495
+ issues.push({ severity: "error", line, message: `<loc>${loc}</loc> must be an absolute URL starting with http:// or https://` });
1496
+ }
1497
+ if (/&(?!amp;|lt;|gt;|apos;|quot;|#\d+;)/.test(loc)) {
1498
+ issues.push({ severity: "error", line, message: `<loc>${loc}</loc> contains an unescaped '&' (use &amp; instead)` });
1499
+ }
1500
+ if (seenLocs.has(loc)) {
1501
+ issues.push({ severity: "warning", line, message: `Duplicate <loc>${loc}</loc>` });
1502
+ }
1503
+ seenLocs.add(loc);
1504
+ const lastmod = extractSitemapTag(block, "lastmod");
1505
+ if (lastmod !== void 0 && !isValidW3CDatetime(lastmod)) {
1506
+ issues.push({ severity: "warning", line, message: `<lastmod>${lastmod}</lastmod> is not a valid W3C datetime` });
1507
+ }
1508
+ if (type === "urlset") {
1509
+ const changefreq = extractSitemapTag(block, "changefreq");
1510
+ if (changefreq !== void 0 && !VALID_CHANGEFREQ.has(changefreq.toLowerCase())) {
1511
+ issues.push({ severity: "error", line, message: `<changefreq>${changefreq}</changefreq> must be one of: ${[...VALID_CHANGEFREQ].join(", ")}` });
1512
+ }
1513
+ const priority = extractSitemapTag(block, "priority");
1514
+ if (priority !== void 0) {
1515
+ const p = Number(priority);
1516
+ if (Number.isNaN(p) || p < 0 || p > 1) {
1517
+ issues.push({ severity: "error", line, message: `<priority>${priority}</priority> must be a number between 0.0 and 1.0` });
1518
+ }
1519
+ }
1520
+ entries.push({ loc, lastmod, changefreq, priority, line });
1521
+ } else {
1522
+ entries.push({ loc, lastmod, line });
1523
+ }
1524
+ }
1525
+ if (entries.length === 0) {
1526
+ issues.push({ severity: "error", line: 1, message: `No <${blockTag}> entries found` });
1527
+ }
1528
+ if (type === "urlset" && entries.length > 5e4) {
1529
+ issues.push({ severity: "error", line: 1, message: `Sitemap contains ${entries.length} URLs, exceeding the 50,000 URL limit per the sitemaps.org spec` });
1530
+ }
1531
+ const valid = !issues.some((i) => i.severity === "error");
1532
+ return { type, urlCount: entries.length, entries, issues, valid };
1533
+ }
1443
1534
 
1444
- export { DNS_RECORD_TYPES, HTTP_HEADERS, STATUS_CODES, buildDnsQueryUrl, buildGeoUrl, buildMapUrl, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseDnsResponse, parseGeoResponse, parseHar, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue };
1535
+ export { DNS_RECORD_TYPES, HTTP_HEADERS, STATUS_CODES, buildDnsQueryUrl, buildGeoUrl, buildMapUrl, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseDnsResponse, parseGeoResponse, parseHar, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateSitemap };
@@ -7,6 +7,7 @@ var data_exports = {};
7
7
  __export(data_exports, {
8
8
  csvToJson: () => csvToJson,
9
9
  detectDelimiter: () => detectDelimiter,
10
+ diffEnvExample: () => diffEnvExample,
10
11
  envToExport: () => envToExport,
11
12
  envToJson: () => envToJson,
12
13
  formatNdjson: () => formatNdjson,
@@ -1081,6 +1082,23 @@ function envToExport(input) {
1081
1082
  function validateEnvKey(key) {
1082
1083
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
1083
1084
  }
1085
+ function diffEnvExample(envInput, exampleInput) {
1086
+ const envParsed = parseEnv(envInput);
1087
+ if (envParsed.errors.length > 0) {
1088
+ return { error: `.env: ${envParsed.errors.map((e) => `Line ${e.line}: ${e.message}`).join("; ")}` };
1089
+ }
1090
+ const exampleParsed = parseEnv(exampleInput);
1091
+ if (exampleParsed.errors.length > 0) {
1092
+ return { error: `.env.example: ${exampleParsed.errors.map((e) => `Line ${e.line}: ${e.message}`).join("; ")}` };
1093
+ }
1094
+ const envKeys = new Set(envParsed.entries.map((e) => e.key));
1095
+ const exampleValues = new Map(exampleParsed.entries.map((e) => [e.key, e.value]));
1096
+ const missingInExample = [...envKeys].filter((k) => !exampleValues.has(k)).sort();
1097
+ const missingInEnv = [...exampleValues.keys()].filter((k) => !envKeys.has(k)).sort();
1098
+ const emptyValueInExample = [...exampleValues.entries()].filter(([k, v]) => envKeys.has(k) && v === "").map(([k]) => k).sort();
1099
+ const keysInBoth = [...envKeys].filter((k) => exampleValues.has(k)).length;
1100
+ return { missingInExample, missingInEnv, emptyValueInExample, keysInBoth };
1101
+ }
1084
1102
  function validateNdjson(input) {
1085
1103
  const rawLines = input.split("\n");
1086
1104
  const lines = [];
@@ -1140,4 +1158,4 @@ function jsonArrayToNdjson(input) {
1140
1158
  return parsed.map((item) => JSON.stringify(item)).join("\n");
1141
1159
  }
1142
1160
 
1143
- export { csvToJson, data_exports, detectDelimiter, envToExport, envToJson, formatNdjson, formatToml, formatXml, getSection, getSectionNames, getTomlStats, getXmlStats, getYamlStats, iniToJson, jsonArrayToNdjson, jsonToIni, jsonToToml, jsonToXml, minifyNdjson, minifyToml, minifyXml, ndjsonToJsonArray, parseCsv, parseEnv, parseIni, parseXmlToObject, stringifyIni, toCsvString, tomlToJson, validateEnvKey, validateIni, validateNdjson, validateToml, validateXml, validateYaml, xmlToJson };
1161
+ export { csvToJson, data_exports, detectDelimiter, diffEnvExample, envToExport, envToJson, formatNdjson, formatToml, formatXml, getSection, getSectionNames, getTomlStats, getXmlStats, getYamlStats, iniToJson, jsonArrayToNdjson, jsonToIni, jsonToToml, jsonToXml, minifyNdjson, minifyToml, minifyXml, ndjsonToJsonArray, parseCsv, parseEnv, parseIni, parseXmlToObject, stringifyIni, toCsvString, tomlToJson, validateEnvKey, validateIni, validateNdjson, validateToml, validateXml, validateYaml, xmlToJson };
@@ -10,6 +10,7 @@
10
10
  * - csv-viewer : detectDelimiter, parseCsv, csvToJson, toCsvString
11
11
  * - ini-parser : parseIni, stringifyIni, iniToJson, jsonToIni, getSection, getSectionNames, validateIni
12
12
  * - env-parser : parseEnv, envToJson, envToExport, validateEnvKey
13
+ * - env-diff-checker: diffEnvExample
13
14
  * - ndjson-formatter: validateNdjson, minifyNdjson, formatNdjson, ndjsonToJsonArray, jsonArrayToNdjson
14
15
  */
15
16
  interface ValidationResult {
@@ -168,6 +169,21 @@ declare function envToJson(input: string): string | {
168
169
  };
169
170
  declare function envToExport(input: string): string;
170
171
  declare function validateEnvKey(key: string): boolean;
172
+ interface EnvDiffResult {
173
+ missingInExample: string[];
174
+ missingInEnv: string[];
175
+ emptyValueInExample: string[];
176
+ keysInBoth: number;
177
+ }
178
+ interface EnvDiffError {
179
+ error: string;
180
+ }
181
+ /**
182
+ * Diff a .env file against its .env.example template. Flags keys present in
183
+ * one file but not the other, plus keys that exist in both but whose
184
+ * .env.example value is an empty placeholder (i.e. undocumented default).
185
+ */
186
+ declare function diffEnvExample(envInput: string, exampleInput: string): EnvDiffResult | EnvDiffError;
171
187
  interface NdjsonLineResult {
172
188
  line: number;
173
189
  raw: string;
@@ -196,6 +212,8 @@ declare function jsonArrayToNdjson(input: string): string | {
196
212
  };
197
213
 
198
214
  type data_CsvParseResult = CsvParseResult;
215
+ type data_EnvDiffError = EnvDiffError;
216
+ type data_EnvDiffResult = EnvDiffResult;
199
217
  type data_EnvEntry = EnvEntry;
200
218
  type data_EnvParseResult = EnvParseResult;
201
219
  type data_IniSection = IniSection;
@@ -209,6 +227,7 @@ type data_XmlFormatResult = XmlFormatResult;
209
227
  type data_XmlToJsonOptions = XmlToJsonOptions;
210
228
  declare const data_csvToJson: typeof csvToJson;
211
229
  declare const data_detectDelimiter: typeof detectDelimiter;
230
+ declare const data_diffEnvExample: typeof diffEnvExample;
212
231
  declare const data_envToExport: typeof envToExport;
213
232
  declare const data_envToJson: typeof envToJson;
214
233
  declare const data_formatNdjson: typeof formatNdjson;
@@ -243,7 +262,7 @@ declare const data_validateXml: typeof validateXml;
243
262
  declare const data_validateYaml: typeof validateYaml;
244
263
  declare const data_xmlToJson: typeof xmlToJson;
245
264
  declare namespace data {
246
- export { type data_CsvParseResult as CsvParseResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_IniSection as IniSection, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_csvToJson as csvToJson, data_detectDelimiter as detectDelimiter, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatToml as formatToml, data_formatXml as formatXml, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseIni as parseIni, data_parseXmlToObject as parseXmlToObject, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
265
+ export { type data_CsvParseResult as CsvParseResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_IniSection as IniSection, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_csvToJson as csvToJson, data_detectDelimiter as detectDelimiter, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatToml as formatToml, data_formatXml as formatXml, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseIni as parseIni, data_parseXmlToObject as parseXmlToObject, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
247
266
  }
248
267
 
249
- export { parseCsv as A, parseEnv as B, type CsvParseResult as C, parseIni as D, type EnvEntry as E, parseXmlToObject as F, stringifyIni as G, toCsvString as H, type IniSection as I, tomlToJson as J, validateEnvKey as K, validateIni as L, validateNdjson as M, type NdjsonLineResult as N, validateToml as O, type ParseOptions as P, validateXml as Q, validateYaml as R, xmlToJson as S, type TomlFormatResult as T, type ValidationResult as V, type XmlFormatResult as X, type EnvParseResult as a, type NdjsonValidateResult as b, type ParsedIni as c, data as d, type XmlToJsonOptions as e, csvToJson as f, detectDelimiter as g, envToExport as h, envToJson as i, formatNdjson as j, formatToml as k, formatXml as l, getSection as m, getSectionNames as n, getTomlStats as o, getXmlStats as p, getYamlStats as q, iniToJson as r, jsonArrayToNdjson as s, jsonToIni as t, jsonToToml as u, jsonToXml as v, minifyNdjson as w, minifyToml as x, minifyXml as y, ndjsonToJsonArray as z };
268
+ export { minifyToml as A, minifyXml as B, type CsvParseResult as C, ndjsonToJsonArray as D, type EnvDiffError as E, parseCsv as F, parseEnv as G, parseIni as H, type IniSection as I, parseXmlToObject as J, stringifyIni as K, toCsvString as L, tomlToJson as M, type NdjsonLineResult as N, validateEnvKey as O, type ParseOptions as P, validateIni as Q, validateNdjson as R, validateToml as S, type TomlFormatResult as T, validateXml as U, type ValidationResult as V, validateYaml as W, type XmlFormatResult as X, xmlToJson as Y, type EnvDiffResult as a, type EnvEntry as b, type EnvParseResult as c, data as d, type NdjsonValidateResult as e, type ParsedIni as f, type XmlToJsonOptions as g, csvToJson as h, detectDelimiter as i, diffEnvExample as j, envToExport as k, envToJson as l, formatNdjson as m, formatToml as n, formatXml as o, getSection as p, getSectionNames as q, getTomlStats as r, getXmlStats as s, getYamlStats as t, iniToJson as u, jsonArrayToNdjson as v, jsonToIni as w, jsonToToml as x, jsonToXml as y, minifyNdjson as z };
@@ -10,6 +10,7 @@
10
10
  * - csv-viewer : detectDelimiter, parseCsv, csvToJson, toCsvString
11
11
  * - ini-parser : parseIni, stringifyIni, iniToJson, jsonToIni, getSection, getSectionNames, validateIni
12
12
  * - env-parser : parseEnv, envToJson, envToExport, validateEnvKey
13
+ * - env-diff-checker: diffEnvExample
13
14
  * - ndjson-formatter: validateNdjson, minifyNdjson, formatNdjson, ndjsonToJsonArray, jsonArrayToNdjson
14
15
  */
15
16
  interface ValidationResult {
@@ -168,6 +169,21 @@ declare function envToJson(input: string): string | {
168
169
  };
169
170
  declare function envToExport(input: string): string;
170
171
  declare function validateEnvKey(key: string): boolean;
172
+ interface EnvDiffResult {
173
+ missingInExample: string[];
174
+ missingInEnv: string[];
175
+ emptyValueInExample: string[];
176
+ keysInBoth: number;
177
+ }
178
+ interface EnvDiffError {
179
+ error: string;
180
+ }
181
+ /**
182
+ * Diff a .env file against its .env.example template. Flags keys present in
183
+ * one file but not the other, plus keys that exist in both but whose
184
+ * .env.example value is an empty placeholder (i.e. undocumented default).
185
+ */
186
+ declare function diffEnvExample(envInput: string, exampleInput: string): EnvDiffResult | EnvDiffError;
171
187
  interface NdjsonLineResult {
172
188
  line: number;
173
189
  raw: string;
@@ -196,6 +212,8 @@ declare function jsonArrayToNdjson(input: string): string | {
196
212
  };
197
213
 
198
214
  type data_CsvParseResult = CsvParseResult;
215
+ type data_EnvDiffError = EnvDiffError;
216
+ type data_EnvDiffResult = EnvDiffResult;
199
217
  type data_EnvEntry = EnvEntry;
200
218
  type data_EnvParseResult = EnvParseResult;
201
219
  type data_IniSection = IniSection;
@@ -209,6 +227,7 @@ type data_XmlFormatResult = XmlFormatResult;
209
227
  type data_XmlToJsonOptions = XmlToJsonOptions;
210
228
  declare const data_csvToJson: typeof csvToJson;
211
229
  declare const data_detectDelimiter: typeof detectDelimiter;
230
+ declare const data_diffEnvExample: typeof diffEnvExample;
212
231
  declare const data_envToExport: typeof envToExport;
213
232
  declare const data_envToJson: typeof envToJson;
214
233
  declare const data_formatNdjson: typeof formatNdjson;
@@ -243,7 +262,7 @@ declare const data_validateXml: typeof validateXml;
243
262
  declare const data_validateYaml: typeof validateYaml;
244
263
  declare const data_xmlToJson: typeof xmlToJson;
245
264
  declare namespace data {
246
- export { type data_CsvParseResult as CsvParseResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_IniSection as IniSection, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_csvToJson as csvToJson, data_detectDelimiter as detectDelimiter, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatToml as formatToml, data_formatXml as formatXml, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseIni as parseIni, data_parseXmlToObject as parseXmlToObject, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
265
+ export { type data_CsvParseResult as CsvParseResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_IniSection as IniSection, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_csvToJson as csvToJson, data_detectDelimiter as detectDelimiter, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatToml as formatToml, data_formatXml as formatXml, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseIni as parseIni, data_parseXmlToObject as parseXmlToObject, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
247
266
  }
248
267
 
249
- export { parseCsv as A, parseEnv as B, type CsvParseResult as C, parseIni as D, type EnvEntry as E, parseXmlToObject as F, stringifyIni as G, toCsvString as H, type IniSection as I, tomlToJson as J, validateEnvKey as K, validateIni as L, validateNdjson as M, type NdjsonLineResult as N, validateToml as O, type ParseOptions as P, validateXml as Q, validateYaml as R, xmlToJson as S, type TomlFormatResult as T, type ValidationResult as V, type XmlFormatResult as X, type EnvParseResult as a, type NdjsonValidateResult as b, type ParsedIni as c, data as d, type XmlToJsonOptions as e, csvToJson as f, detectDelimiter as g, envToExport as h, envToJson as i, formatNdjson as j, formatToml as k, formatXml as l, getSection as m, getSectionNames as n, getTomlStats as o, getXmlStats as p, getYamlStats as q, iniToJson as r, jsonArrayToNdjson as s, jsonToIni as t, jsonToToml as u, jsonToXml as v, minifyNdjson as w, minifyToml as x, minifyXml as y, ndjsonToJsonArray as z };
268
+ export { minifyToml as A, minifyXml as B, type CsvParseResult as C, ndjsonToJsonArray as D, type EnvDiffError as E, parseCsv as F, parseEnv as G, parseIni as H, type IniSection as I, parseXmlToObject as J, stringifyIni as K, toCsvString as L, tomlToJson as M, type NdjsonLineResult as N, validateEnvKey as O, type ParseOptions as P, validateIni as Q, validateNdjson as R, validateToml as S, type TomlFormatResult as T, validateXml as U, type ValidationResult as V, validateYaml as W, type XmlFormatResult as X, xmlToJson as Y, type EnvDiffResult as a, type EnvEntry as b, type EnvParseResult as c, data as d, type NdjsonValidateResult as e, type ParsedIni as f, type XmlToJsonOptions as g, csvToJson as h, detectDelimiter as i, diffEnvExample as j, envToExport as k, envToJson as l, formatNdjson as m, formatToml as n, formatXml as o, getSection as p, getSectionNames as q, getTomlStats as r, getXmlStats as s, getYamlStats as t, iniToJson as u, jsonArrayToNdjson as v, jsonToIni as w, jsonToToml as x, jsonToXml as y, minifyNdjson as z };