livelocale 0.8.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.
Files changed (53) hide show
  1. package/README.md +230 -5
  2. package/dist/client/index.cjs +178 -70
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +36 -22
  5. package/dist/client/index.d.ts +36 -22
  6. package/dist/client/index.js +177 -70
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codemod/index.cjs +3057 -152
  9. package/dist/codemod/index.cjs.map +1 -1
  10. package/dist/codemod/index.js +3058 -153
  11. package/dist/codemod/index.js.map +1 -1
  12. package/dist/react/index.cjs +185 -77
  13. package/dist/react/index.cjs.map +1 -1
  14. package/dist/react/index.js +185 -77
  15. package/dist/react/index.js.map +1 -1
  16. package/dist/server/index.cjs +108 -66
  17. package/dist/server/index.cjs.map +1 -1
  18. package/dist/server/index.d.cts +13 -3
  19. package/dist/server/index.d.ts +13 -3
  20. package/dist/server/index.js +107 -66
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/studio/assets/index-Bvbtkry-.css +1 -0
  23. package/dist/studio/assets/index-DC_jRsox.js +13 -0
  24. package/dist/studio/assets/inter-latin-300-normal-BVlfKGgI.woff2 +0 -0
  25. package/dist/studio/assets/inter-latin-300-normal-i8F0SvXL.woff +0 -0
  26. package/dist/studio/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  27. package/dist/studio/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  28. package/dist/studio/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  29. package/dist/studio/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  30. package/dist/studio/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  31. package/dist/studio/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  32. package/dist/studio/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  33. package/dist/studio/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  34. package/dist/studio/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
  35. package/dist/studio/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
  36. package/dist/studio/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
  37. package/dist/studio/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
  38. package/dist/studio/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
  39. package/dist/studio/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
  40. package/dist/studio/assets/outfit-latin-700-normal-Cu9v6i1X.woff2 +0 -0
  41. package/dist/studio/assets/outfit-latin-700-normal-D4itBLBr.woff +0 -0
  42. package/dist/studio/index.html +3 -6
  43. package/dist/vite/index.cjs +739 -77
  44. package/dist/vite/index.cjs.map +1 -1
  45. package/dist/vite/index.d.cts +22 -2
  46. package/dist/vite/index.d.ts +22 -2
  47. package/dist/vite/index.js +738 -77
  48. package/dist/vite/index.js.map +1 -1
  49. package/dist/{wrap-CsOtHp8r.d.cts → wrap-2qVxLN9Q.d.cts} +10 -18
  50. package/dist/{wrap-CsOtHp8r.d.ts → wrap-2qVxLN9Q.d.ts} +10 -18
  51. package/package.json +4 -1
  52. package/dist/studio/assets/index-D7ifnqy5.js +0 -10
  53. package/dist/studio/assets/index-Zh0KtFUH.css +0 -2
@@ -313,14 +313,24 @@ var init_transform = __esm({
313
313
  });
314
314
 
315
315
  // src/codemod/index.ts
316
- var fs5 = __toESM(require("fs"), 1);
317
- var path5 = __toESM(require("path"), 1);
316
+ var fs10 = __toESM(require("fs"), 1);
317
+ var path15 = __toESM(require("path"), 1);
318
318
  var net = __toESM(require("net"), 1);
319
+
320
+ // src/server/module-dir.ts
321
+ var path = __toESM(require("path"), 1);
319
322
  var import_url = require("url");
323
+ var import_meta = {};
324
+ function moduleDir() {
325
+ if (typeof __dirname !== "undefined") {
326
+ return __dirname;
327
+ }
328
+ return path.dirname((0, import_url.fileURLToPath)(import_meta.url));
329
+ }
320
330
 
321
331
  // src/codemod/studio-server.ts
322
332
  var fs = __toESM(require("fs"), 1);
323
- var path = __toESM(require("path"), 1);
333
+ var path2 = __toESM(require("path"), 1);
324
334
  var http = __toESM(require("http"), 1);
325
335
  var MIME_TYPES = {
326
336
  ".html": "text/html; charset=utf-8",
@@ -349,8 +359,8 @@ function injectStudioConfig(html, config) {
349
359
  ${html}`;
350
360
  }
351
361
  function createStudioServer(options) {
352
- const studioDir = path.resolve(options.studioDir);
353
- const indexPath = path.join(studioDir, "index.html");
362
+ const studioDir = path2.resolve(options.studioDir);
363
+ const indexPath = path2.join(studioDir, "index.html");
354
364
  const config = { appOrigin: options.appOrigin };
355
365
  const sendIndex = (res) => {
356
366
  fs.readFile(indexPath, "utf-8", (err, html) => {
@@ -379,13 +389,13 @@ function createStudioServer(options) {
379
389
  const urlPath = req.url?.split("?")[0] || "/";
380
390
  let filePath;
381
391
  try {
382
- filePath = path.resolve(studioDir, `.${decodeURIComponent(urlPath)}`);
392
+ filePath = path2.resolve(studioDir, `.${decodeURIComponent(urlPath)}`);
383
393
  } catch {
384
394
  res.statusCode = 400;
385
395
  res.end("Bad Request");
386
396
  return;
387
397
  }
388
- if (filePath !== studioDir && !filePath.startsWith(studioDir + path.sep)) {
398
+ if (filePath !== studioDir && !filePath.startsWith(studioDir + path2.sep)) {
389
399
  res.statusCode = 403;
390
400
  res.end("Forbidden");
391
401
  return;
@@ -394,7 +404,7 @@ function createStudioServer(options) {
394
404
  sendIndex(res);
395
405
  return;
396
406
  }
397
- const ext = path.extname(filePath);
407
+ const ext = path2.extname(filePath);
398
408
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
399
409
  fs.readFile(filePath, (err, content) => {
400
410
  if (err) {
@@ -410,14 +420,340 @@ function createStudioServer(options) {
410
420
 
411
421
  // src/codemod/doctor.ts
412
422
  var fs2 = __toESM(require("fs"), 1);
413
- var path2 = __toESM(require("path"), 1);
423
+ var path3 = __toESM(require("path"), 1);
414
424
 
415
425
  // src/core/validate.ts
416
426
  var LOCALE_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,4})?$/;
427
+ var KEY_RE = /^[a-zA-Z0-9._-]+$/;
428
+ var MAX_LOCALE_LENGTH = 10;
429
+ var MAX_KEY_LENGTH = 200;
430
+ var MAX_VALUE_LENGTH = 1e4;
431
+ var DANGEROUS_KEY_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
432
+ function isSafeKeyPath(key) {
433
+ if (typeof key !== "string") return false;
434
+ return !key.split(".").some((segment) => DANGEROUS_KEY_SEGMENTS.has(segment));
435
+ }
436
+ function isValidLocaleFormat(locale) {
437
+ return typeof locale === "string" && locale.length >= 2 && locale.length <= MAX_LOCALE_LENGTH && LOCALE_RE.test(locale);
438
+ }
439
+ function isValidKeyFormat(key) {
440
+ return typeof key === "string" && key.length >= 1 && key.length <= MAX_KEY_LENGTH && KEY_RE.test(key);
441
+ }
442
+ function isValidValueFormat(value) {
443
+ return typeof value === "string" && value.length <= MAX_VALUE_LENGTH;
444
+ }
417
445
  function isSafeLocaleSegment(locale) {
418
446
  return !(locale.includes("..") || locale.includes("/") || locale.includes("\\") || locale.includes("%") || locale.includes("\0"));
419
447
  }
420
448
 
449
+ // src/core/flag-script.ts
450
+ var FLAG_SCRIPT_SIGNATURE = "/*! livelocale flag v1 */";
451
+ var FLAG_SCRIPT = `${FLAG_SCRIPT_SIGNATURE}
452
+ (function () {
453
+ try {
454
+ var me = document.currentScript;
455
+ if (!me || !me.src || window.parent === window || window.__LIVELOCALE_FLAG__ !== undefined)
456
+ return;
457
+ var studio = new URL('.', me.src);
458
+ var host = window.parent.location;
459
+ if (
460
+ host.origin === 'null' ||
461
+ host.origin !== window.location.origin ||
462
+ host.origin !== studio.origin
463
+ )
464
+ return;
465
+ if (host.pathname.indexOf(studio.pathname) !== 0) return;
466
+ window.__LIVELOCALE_FLAG__ = 'preview';
467
+ } catch (e) {}
468
+ })();
469
+ `;
470
+
471
+ // src/core/change-link.ts
472
+ var CHANGE_LINK_PREFIX = "lc1.";
473
+ var CHANGE_BLOCK_OPEN = "```livelocale";
474
+ var CHANGE_BLOCK_CLOSE = "```";
475
+ var ChangeLinkError = class extends Error {
476
+ code;
477
+ constructor(code, message) {
478
+ super(message);
479
+ this.name = "ChangeLinkError";
480
+ this.code = code;
481
+ }
482
+ };
483
+ var CORRUPT_MESSAGE = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
484
+ var UNSUPPORTED_VERSION_MESSAGE = "This change link was made by a newer LiveLocale. Update LiveLocale and try again.";
485
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
486
+ function fromBase64Url(text) {
487
+ if (!/^[A-Za-z0-9_-]*$/.test(text) || text.length % 4 === 1) {
488
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE);
489
+ }
490
+ const tail = text.length % 4;
491
+ if (tail !== 0) {
492
+ const unusedBits = tail === 2 ? 15 : 3;
493
+ if ((BASE64URL_ALPHABET.indexOf(text[text.length - 1]) & unusedBits) !== 0) {
494
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE);
495
+ }
496
+ }
497
+ const binary = atob(
498
+ text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(tail === 0 ? 0 : 4 - tail)
499
+ );
500
+ const bytes = new Uint8Array(binary.length);
501
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
502
+ return bytes;
503
+ }
504
+ function fencedBlockContent(text) {
505
+ const open = new RegExp(`${CHANGE_BLOCK_OPEN}(?:\\r\\n|\\r|\\n)`).exec(text);
506
+ if (!open) return null;
507
+ const lines = text.slice(open.index + open[0].length).split(/\r\n|\r|\n/);
508
+ const close = lines.indexOf(CHANGE_BLOCK_CLOSE);
509
+ return (close < 0 ? lines : lines.slice(0, close)).join("").replace(/\s+/g, "");
510
+ }
511
+ function findChangeToken(text) {
512
+ const scope = (fencedBlockContent(text) ?? text).replace(/%23/gi, "#");
513
+ const tokens = /* @__PURE__ */ new Set();
514
+ for (const match of scope.matchAll(/(?:^|[^A-Za-z0-9_-])lc1\.([A-Za-z0-9_-]+)/g)) {
515
+ tokens.add(match[1]);
516
+ }
517
+ if (tokens.size === 1) return CHANGE_LINK_PREFIX + [...tokens][0];
518
+ if (tokens.size > 1) {
519
+ throw new ChangeLinkError(
520
+ "AMBIGUOUS",
521
+ "This text holds more than one change link. Paste one link at a time."
522
+ );
523
+ }
524
+ if (/(?:^|[^A-Za-z0-9_-])lc(?:[2-9]|[1-9]\d+)\.[A-Za-z0-9_-]/.test(scope)) {
525
+ throw new ChangeLinkError("UNSUPPORTED_VERSION", UNSUPPORTED_VERSION_MESSAGE);
526
+ }
527
+ throw new ChangeLinkError(
528
+ "NOT_FOUND",
529
+ 'This is not a LiveLocale change link. A change link contains "lc1.".'
530
+ );
531
+ }
532
+ function isValidRepoSlug(slug) {
533
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(slug)) return false;
534
+ const repo = slug.slice(slug.indexOf("/") + 1);
535
+ return repo !== "." && repo !== "..";
536
+ }
537
+ var BRANCH_NAME_RE = /^(?!-)(?!.*\.\.)[^\s\x00-\x1f\x7f]{1,255}$/;
538
+ function isValidBranchName(name) {
539
+ return BRANCH_NAME_RE.test(name) && !hasLoneSurrogate(name);
540
+ }
541
+ var CHANGE_LINK_VERSION = 1;
542
+ var MAX_CHANGE_EDITS = 500;
543
+ var MAX_BY_CHARS = 100;
544
+ var MAX_NOTE_CHARS = 2e3;
545
+ var BASE_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})?$/;
546
+ var BY_FORBIDDEN_RE = /[\x00-\x1f\x7f]/;
547
+ var NOTE_FORBIDDEN_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
548
+ var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
549
+ function isPlainObject(value) {
550
+ return typeof value === "object" && value !== null && !Array.isArray(value);
551
+ }
552
+ function own(obj, key) {
553
+ return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
554
+ }
555
+ function invalid(message) {
556
+ return new ChangeLinkError("INVALID", message);
557
+ }
558
+ function hasLoneSurrogate(text) {
559
+ return typeof text === "string" && LONE_SURROGATE_RE.test(text);
560
+ }
561
+ function parseEdit(entry, n) {
562
+ const fail = (problem) => invalid(`edit ${n}: ${problem}`);
563
+ if (!Array.isArray(entry) || entry.length !== 4 && entry.length !== 5) {
564
+ throw fail("is not a [locale, key, from, to] entry.");
565
+ }
566
+ const [locale, key, from, to, force] = entry;
567
+ if (typeof locale !== "string" || !isValidLocaleFormat(locale)) {
568
+ throw fail("the locale is not a locale code.");
569
+ }
570
+ if (typeof key !== "string" || !isValidKeyFormat(key) || !isSafeKeyPath(key)) {
571
+ throw fail("the key is not a valid translation key.");
572
+ }
573
+ if (from !== null && (typeof from !== "string" || !isValidValueFormat(from))) {
574
+ throw fail(`the current text is not text of at most ${MAX_VALUE_LENGTH} characters.`);
575
+ }
576
+ if (typeof to !== "string" || !isValidValueFormat(to)) {
577
+ throw fail(`the new text is not text of at most ${MAX_VALUE_LENGTH} characters.`);
578
+ }
579
+ if (entry.length === 5 && force !== 1) throw fail("the force flag must be 1.");
580
+ return { locale, key, from, to, force: entry.length === 5 };
581
+ }
582
+ function checkKeysDistinct(edits) {
583
+ const keysByLocale = /* @__PURE__ */ new Map();
584
+ edits.forEach(({ locale, key }, i) => {
585
+ let keys = keysByLocale.get(locale);
586
+ if (!keys) keysByLocale.set(locale, keys = /* @__PURE__ */ new Set());
587
+ if (keys.has(key)) throw invalid(`edit ${i + 1}: an earlier edit changes the same key.`);
588
+ keys.add(key);
589
+ });
590
+ edits.forEach(({ locale, key }, i) => {
591
+ const keys = keysByLocale.get(locale);
592
+ for (let dot = key.indexOf("."); dot >= 0; dot = key.indexOf(".", dot + 1)) {
593
+ if (keys.has(key.slice(0, dot))) {
594
+ throw invalid(`edit ${i + 1}: another edit changes a key that contains this one.`);
595
+ }
596
+ }
597
+ });
598
+ }
599
+ function parseChangeSet(value) {
600
+ if (!isPlainObject(value)) throw invalid("This change link does not hold a change set.");
601
+ if (own(value, "v") !== CHANGE_LINK_VERSION) {
602
+ throw new ChangeLinkError("UNSUPPORTED_VERSION", UNSUPPORTED_VERSION_MESSAGE);
603
+ }
604
+ const repo = own(value, "repo");
605
+ if (repo !== void 0 && (typeof repo !== "string" || !isValidRepoSlug(repo))) {
606
+ throw invalid("The repository is not an owner/name slug.");
607
+ }
608
+ const branch = own(value, "branch");
609
+ if (typeof branch !== "string" || branch !== "" && !isValidBranchName(branch)) {
610
+ throw invalid("The branch is not a valid branch name.");
611
+ }
612
+ const base = own(value, "base");
613
+ if (typeof base !== "string" || !BASE_SHA_RE.test(base)) {
614
+ throw invalid("The base is not a full commit id.");
615
+ }
616
+ const src = own(value, "src");
617
+ if (typeof src !== "string" || !isValidLocaleFormat(src)) {
618
+ throw invalid("The source locale is not a locale code.");
619
+ }
620
+ const by = own(value, "by");
621
+ if (typeof by !== "string" || by.length > MAX_BY_CHARS || BY_FORBIDDEN_RE.test(by)) {
622
+ throw invalid(`The name is not one line of at most ${MAX_BY_CHARS} characters.`);
623
+ }
624
+ const note = own(value, "note");
625
+ if (typeof note !== "string" || note.length > MAX_NOTE_CHARS || NOTE_FORBIDDEN_RE.test(note)) {
626
+ throw invalid(`The note is not plain text of at most ${MAX_NOTE_CHARS} characters.`);
627
+ }
628
+ const entries = own(value, "e");
629
+ if (!Array.isArray(entries)) throw invalid("This change link holds no list of edits.");
630
+ if (entries.length > MAX_CHANGE_EDITS) {
631
+ throw new ChangeLinkError(
632
+ "TOO_MANY_EDITS",
633
+ `A change link holds at most ${MAX_CHANGE_EDITS} edits. Send the rest in another link.`
634
+ );
635
+ }
636
+ if (entries.length === 0) throw invalid("This change link holds no edits.");
637
+ const edits = Array.from(entries, (entry, i) => parseEdit(entry, i + 1));
638
+ checkKeysDistinct(edits);
639
+ if ([repo, branch, base, src, by, note].some(hasLoneSurrogate)) {
640
+ throw invalid("This change link holds broken text.");
641
+ }
642
+ edits.forEach(({ locale, key, from, to }, i) => {
643
+ if ([locale, key, from, to].some(hasLoneSurrogate)) {
644
+ throw invalid(`edit ${i + 1}: holds broken text.`);
645
+ }
646
+ });
647
+ return { ...repo === void 0 ? {} : { repo }, branch, base, src, by, note, edits };
648
+ }
649
+ var MAX_INFLATED_BYTES = 512 * 1024;
650
+ var MAX_TOKEN_CHARS = 1e6;
651
+ var TOO_LARGE_MESSAGE = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
652
+ var UNSUPPORTED_ENVIRONMENT_MESSAGE = "This browser cannot create or read change links. Use a current Chrome, Edge or Firefox, or Safari 16.4 or later.";
653
+ var FEED_SLICE_BYTES = 4096;
654
+ async function pipeBytes(stream, bytes, maxOutputLength) {
655
+ const input = new Uint8Array(bytes);
656
+ const writer = stream.writable.getWriter();
657
+ const reader = stream.readable.getReader();
658
+ let stopped = false;
659
+ void (async () => {
660
+ for (let at = 0; at < input.length; at += FEED_SLICE_BYTES) {
661
+ if (stopped) return;
662
+ await writer.write(input.subarray(at, at + FEED_SLICE_BYTES));
663
+ }
664
+ if (!stopped) await writer.close();
665
+ })().catch(() => {
666
+ });
667
+ const chunks = [];
668
+ let total = 0;
669
+ try {
670
+ for (; ; ) {
671
+ const { done, value } = await reader.read();
672
+ if (done) break;
673
+ total += value.length;
674
+ if (total > maxOutputLength) throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE);
675
+ chunks.push(value);
676
+ }
677
+ } catch (err) {
678
+ stopped = true;
679
+ void writer.abort(err).catch(() => {
680
+ });
681
+ await reader.cancel(err).catch(() => {
682
+ });
683
+ throw err;
684
+ }
685
+ const out = new Uint8Array(total);
686
+ let offset = 0;
687
+ for (const chunk of chunks) {
688
+ out.set(chunk, offset);
689
+ offset += chunk.length;
690
+ }
691
+ return out;
692
+ }
693
+ var streamInflate = async (bytes, maxOutputLength) => {
694
+ if (typeof globalThis.DecompressionStream !== "function") {
695
+ throw new ChangeLinkError("UNSUPPORTED_ENVIRONMENT", UNSUPPORTED_ENVIRONMENT_MESSAGE);
696
+ }
697
+ try {
698
+ return await pipeBytes(new DecompressionStream("deflate"), bytes, maxOutputLength);
699
+ } catch (err) {
700
+ if (err instanceof ChangeLinkError) throw err;
701
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE);
702
+ }
703
+ };
704
+ async function decodeChangeLink(text, inflate = streamInflate) {
705
+ const token = findChangeToken(text);
706
+ if (token.length > MAX_TOKEN_CHARS) {
707
+ throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE);
708
+ }
709
+ try {
710
+ const bytes = await inflate(
711
+ fromBase64Url(token.slice(CHANGE_LINK_PREFIX.length)),
712
+ MAX_INFLATED_BYTES
713
+ );
714
+ const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
715
+ return parseChangeSet(JSON.parse(json));
716
+ } catch (err) {
717
+ if (err instanceof ChangeLinkError) throw err;
718
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE);
719
+ }
720
+ }
721
+
722
+ // src/core/share-manifest.ts
723
+ var SHARE_DIR = "livelocale";
724
+ var SHARE_MANIFEST_FILE = "manifest.json";
725
+ var SHARE_FLAG_FILE = "flag.js";
726
+ var SHARE_LOCALES_DIR = "locales";
727
+ var SHARE_MANIFEST_FORMAT = 1;
728
+ var BASE_SHA_RE2 = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
729
+ function own2(obj, key) {
730
+ return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
731
+ }
732
+ function isArrayOf(value, test) {
733
+ if (!Array.isArray(value)) return false;
734
+ for (let i = 0; i < value.length; i++) {
735
+ if (!test(value[i])) return false;
736
+ }
737
+ return true;
738
+ }
739
+ function isString(value) {
740
+ return typeof value === "string";
741
+ }
742
+ function isLocaleCode(value) {
743
+ return typeof value === "string" && LOCALE_RE.test(value);
744
+ }
745
+ function isNullOr(value, test) {
746
+ return value === null || typeof value === "string" && test(value);
747
+ }
748
+ function isShareManifest(value) {
749
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
750
+ const repo = own2(value, "repo");
751
+ const layout = own2(value, "layout");
752
+ const locales = own2(value, "locales");
753
+ const sourceLocale = own2(value, "sourceLocale");
754
+ return own2(value, "format") === SHARE_MANIFEST_FORMAT && (repo === void 0 || typeof repo === "string" && isValidRepoSlug(repo)) && isNullOr(own2(value, "defaultBranch"), isValidBranchName) && isNullOr(own2(value, "branch"), isValidBranchName) && isNullOr(own2(value, "base"), (base) => BASE_SHA_RE2.test(base)) && typeof own2(value, "localesPath") === "string" && (layout === "flat" || layout === "namespaced") && isArrayOf(locales, isLocaleCode) && typeof sourceLocale === "string" && locales.includes(sourceLocale) && isNullOr(own2(value, "basePath"), (path16) => path16.startsWith("/") && path16.endsWith("/")) && isArrayOf(own2(value, "codeKeys"), isString);
755
+ }
756
+
421
757
  // src/codemod/doctor.ts
422
758
  var ROUTE_CANDIDATES = [
423
759
  "app/api/livelocale/route.ts",
@@ -460,10 +796,32 @@ var NEXT_CONFIG_CANDIDATES = [
460
796
  "next.config.ts",
461
797
  "next.config.cjs"
462
798
  ];
799
+ var VITE_CONFIG_CANDIDATES = [
800
+ "vite.config.ts",
801
+ "vite.config.mts",
802
+ "vite.config.js",
803
+ "vite.config.mjs",
804
+ "vite.config.cjs",
805
+ "vite.config.cts"
806
+ ];
807
+ var SERVICE_WORKER_CONFIG_CANDIDATES = [
808
+ ...VITE_CONFIG_CANDIDATES,
809
+ "webpack.config.js",
810
+ "webpack.config.cjs",
811
+ "webpack.config.mjs",
812
+ "webpack.config.ts",
813
+ "craco.config.js",
814
+ "craco.config.ts",
815
+ "vue.config.js",
816
+ "config-overrides.js",
817
+ ...NEXT_CONFIG_CANDIDATES,
818
+ "package.json"
819
+ ];
463
820
  var API_ROUTE = "/api/livelocale";
821
+ var DENYLIST_LINE = "navigateFallbackDenylist: [/^\\/livelocale\\//]";
464
822
  function firstExisting(root, candidates) {
465
823
  for (const rel of candidates) {
466
- if (fs2.existsSync(path2.join(root, rel))) return rel;
824
+ if (fs2.existsSync(path3.join(root, rel))) return rel;
467
825
  }
468
826
  return null;
469
827
  }
@@ -474,9 +832,31 @@ function readText(file) {
474
832
  return "";
475
833
  }
476
834
  }
835
+ function buildsStaticShare(root) {
836
+ const vite = firstExisting(root, VITE_CONFIG_CANDIDATES);
837
+ const viteText = vite ? readText(path3.join(root, vite)) : "";
838
+ if (/liveLocaleVite\s*\(\s*\{[^)]*\bshare\b(?!\s*:\s*false)/.test(viteText)) return true;
839
+ let scripts;
840
+ try {
841
+ scripts = JSON.parse(readText(path3.join(root, "package.json")) || "{}")?.scripts;
842
+ } catch {
843
+ return false;
844
+ }
845
+ if (typeof scripts !== "object" || scripts === null) return false;
846
+ return Object.values(scripts).some(
847
+ (cmd) => typeof cmd === "string" && /\blivelocale\s+(?:pack|build)\b/.test(cmd)
848
+ );
849
+ }
477
850
  function checkRouteHandler(root) {
478
851
  const found = firstExisting(root, ROUTE_CANDIDATES);
479
852
  if (found) return { level: "ok", title: "Mutation route handler", detail: found };
853
+ if (buildsStaticShare(root)) {
854
+ return {
855
+ level: "ok",
856
+ title: "Mutation route handler",
857
+ detail: "not needed for the share link (static build)"
858
+ };
859
+ }
480
860
  return {
481
861
  level: "fail",
482
862
  title: "Mutation route handler",
@@ -484,7 +864,7 @@ function checkRouteHandler(root) {
484
864
  };
485
865
  }
486
866
  function checkProvider(root) {
487
- const existing = LAYOUT_CANDIDATES.filter((rel) => fs2.existsSync(path2.join(root, rel)));
867
+ const existing = LAYOUT_CANDIDATES.filter((rel) => fs2.existsSync(path3.join(root, rel)));
488
868
  if (existing.length === 0) {
489
869
  return {
490
870
  level: "warn",
@@ -492,7 +872,7 @@ function checkProvider(root) {
492
872
  detail: "No root layout (app/layout.*) or pages/_app.* found, so the provider could not be verified."
493
873
  };
494
874
  }
495
- const mounted = existing.find((rel) => /LiveLocaleProvider/.test(readText(path2.join(root, rel))));
875
+ const mounted = existing.find((rel) => /LiveLocaleProvider/.test(readText(path3.join(root, rel))));
496
876
  if (mounted) return { level: "ok", title: "LiveLocaleProvider mounted", detail: mounted };
497
877
  return {
498
878
  level: "warn",
@@ -503,7 +883,7 @@ function checkProvider(root) {
503
883
  function checkMiddleware(root) {
504
884
  const found = firstExisting(root, MIDDLEWARE_CANDIDATES);
505
885
  if (!found) return { level: "ok", title: "Middleware matcher", detail: "no middleware file" };
506
- const text = readText(path2.join(root, found));
886
+ const text = readText(path3.join(root, found));
507
887
  if (text.includes("api/livelocale")) {
508
888
  return { level: "ok", title: "Middleware matcher", detail: `${found} excludes ${API_ROUTE}` };
509
889
  }
@@ -522,7 +902,7 @@ function checkFrameHeaders(root) {
522
902
  const found = firstExisting(root, NEXT_CONFIG_CANDIDATES);
523
903
  if (!found)
524
904
  return { level: "ok", title: "Frame-blocking headers in next.config", detail: "none" };
525
- const text = readText(path2.join(root, found));
905
+ const text = readText(path3.join(root, found));
526
906
  if (STUDIO_ORIGIN_RE.test(text)) {
527
907
  return {
528
908
  level: "ok",
@@ -543,8 +923,8 @@ function checkFrameHeaders(root) {
543
923
  };
544
924
  }
545
925
  function checkLocales(root, localesPath) {
546
- const dir = path2.resolve(root, localesPath ?? "locales");
547
- const rel = path2.relative(root, dir) || ".";
926
+ const dir = path3.resolve(root, localesPath ?? "locales");
927
+ const rel = path3.relative(root, dir) || ".";
548
928
  if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
549
929
  return {
550
930
  level: "fail",
@@ -555,27 +935,27 @@ function checkLocales(root, localesPath) {
555
935
  const entries = fs2.readdirSync(dir, { withFileTypes: true });
556
936
  const flat = entries.filter((e) => e.isFile() && e.name.endsWith(".json"));
557
937
  const dirs = entries.filter((e) => e.isDirectory() && LOCALE_RE.test(e.name));
558
- const invalid = [];
938
+ const invalid2 = [];
559
939
  let fileCount = 0;
560
940
  const validate = (file, label) => {
561
941
  fileCount++;
562
942
  try {
563
943
  const parsed = JSON.parse(fs2.readFileSync(file, "utf-8"));
564
944
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
565
- invalid.push(`${label} (root must be an object)`);
945
+ invalid2.push(`${label} (root must be an object)`);
566
946
  }
567
947
  } catch (err) {
568
- invalid.push(`${label} (${err.message})`);
948
+ invalid2.push(`${label} (${err.message})`);
569
949
  }
570
950
  };
571
- for (const f of flat) validate(path2.join(dir, f.name), f.name);
951
+ for (const f of flat) validate(path3.join(dir, f.name), f.name);
572
952
  for (const d of dirs) {
573
- for (const f of fs2.readdirSync(path2.join(dir, d.name))) {
574
- if (f.endsWith(".json")) validate(path2.join(dir, d.name, f), `${d.name}/${f}`);
953
+ for (const f of fs2.readdirSync(path3.join(dir, d.name))) {
954
+ if (f.endsWith(".json")) validate(path3.join(dir, d.name, f), `${d.name}/${f}`);
575
955
  }
576
956
  }
577
- if (invalid.length > 0) {
578
- return { level: "fail", title: "Locale files", detail: `Invalid JSON: ${invalid.join("; ")}` };
957
+ if (invalid2.length > 0) {
958
+ return { level: "fail", title: "Locale files", detail: `Invalid JSON: ${invalid2.join("; ")}` };
579
959
  }
580
960
  if (fileCount === 0) {
581
961
  return {
@@ -598,6 +978,22 @@ function checkLocales(root, localesPath) {
598
978
  detail: `${rel}: ${layout}, ${fileCount} file(s) parse`
599
979
  };
600
980
  }
981
+ function checkServiceWorkerConfig(root) {
982
+ const files = SERVICE_WORKER_CONFIG_CANDIDATES.filter(
983
+ (rel) => fs2.existsSync(path3.join(root, rel))
984
+ ).map((rel) => ({ rel, text: readText(path3.join(root, rel)) }));
985
+ const pwa = files.find(({ text }) => /VitePWA\s*\(|workbox-webpack-plugin/.test(text));
986
+ if (!pwa) return null;
987
+ if (files.some(({ text }) => /navigateFallbackDenylist[\s\S]{0,300}?livelocale/.test(text))) {
988
+ return null;
989
+ }
990
+ const plugin = /VitePWA\s*\(/.test(pwa.text) ? "VitePWA" : "workbox-webpack-plugin";
991
+ return {
992
+ level: "warn",
993
+ title: "Service worker config",
994
+ detail: `${pwa.rel} uses ${plugin}, and its navigation fallback would answer livelocale/index.html with your app's page. Add to its Workbox options: ${DENYLIST_LINE}`
995
+ };
996
+ }
601
997
  async function probe(fetchImpl, url, token) {
602
998
  try {
603
999
  const signal = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(15e3) : void 0;
@@ -730,8 +1126,265 @@ async function checkLive(appOrigin, fetchImpl, token) {
730
1126
  }
731
1127
  return findings;
732
1128
  }
1129
+ function shareLinkFor(appBase) {
1130
+ return `${appBase}${SHARE_DIR}/index.html`;
1131
+ }
1132
+ function normalizeAppUrl(value) {
1133
+ const url = new URL(value);
1134
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1135
+ throw new Error(`Not an http(s) URL: ${value}`);
1136
+ }
1137
+ let pathname = url.pathname.replace(/index\.html$/, "");
1138
+ if (!pathname.endsWith("/")) pathname += "/";
1139
+ return { origin: url.origin, base: `${url.origin}${pathname}` };
1140
+ }
1141
+ var STUDIO_TITLE = "LiveLocale \xB7 Studio";
1142
+ var UPLOAD_HINT = "Upload the livelocale/ folder from your build output.";
1143
+ function isAnswer(page) {
1144
+ return !("error" in page);
1145
+ }
1146
+ async function fetchPage(fetchImpl, url, origin) {
1147
+ const res = await probe(fetchImpl, url);
1148
+ if (!res.ok) return { url, error: res.error };
1149
+ let body = "";
1150
+ try {
1151
+ body = await res.res.text();
1152
+ } catch {
1153
+ }
1154
+ return {
1155
+ url,
1156
+ status: res.res.status,
1157
+ headers: res.res.headers,
1158
+ body,
1159
+ wall: loginWall(res.res, origin)
1160
+ };
1161
+ }
1162
+ function answered(title, page) {
1163
+ if (!isAnswer(page)) {
1164
+ return { level: "fail", title, detail: `${page.url} is not reachable (${page.error}).` };
1165
+ }
1166
+ if (page.wall) return { level: "fail", title, detail: page.wall };
1167
+ return page;
1168
+ }
1169
+ function parseJson(text) {
1170
+ try {
1171
+ return { ok: true, value: JSON.parse(text) };
1172
+ } catch {
1173
+ return { ok: false };
1174
+ }
1175
+ }
1176
+ function checkStudioPage(studioPage, root) {
1177
+ const title = "Share Studio page";
1178
+ const studio = answered(title, studioPage);
1179
+ if ("level" in studio) return studio;
1180
+ if (studio.status === 200 && isAnswer(root) && root.status === 200 && studio.body === root.body) {
1181
+ return {
1182
+ level: "fail",
1183
+ title,
1184
+ detail: `The host answered livelocale/index.html with your app's page (SPA fallback or service worker). ${UPLOAD_HINT}`
1185
+ };
1186
+ }
1187
+ if (studio.status === 200 && studio.body.includes(STUDIO_TITLE)) {
1188
+ return { level: "ok", title, detail: studio.url };
1189
+ }
1190
+ return {
1191
+ level: "fail",
1192
+ title,
1193
+ detail: `${studio.url} answered HTTP ${studio.status} without the LiveLocale Studio. ${UPLOAD_HINT}`
1194
+ };
1195
+ }
1196
+ function checkManifest(manifestPage) {
1197
+ const title = "Share manifest";
1198
+ const manifest = answered(title, manifestPage);
1199
+ if ("level" in manifest) return manifest;
1200
+ if (manifest.status === 404) {
1201
+ return {
1202
+ level: "fail",
1203
+ title,
1204
+ detail: `${manifest.url} answered HTTP 404; did the build run with share: true or livelocale pack?`
1205
+ };
1206
+ }
1207
+ const parsed = manifest.status === 200 ? parseJson(manifest.body) : { ok: false };
1208
+ if (parsed.ok && isShareManifest(parsed.value)) {
1209
+ const m = parsed.value;
1210
+ return {
1211
+ level: "ok",
1212
+ title,
1213
+ detail: `${m.locales.length} locale(s), source ${m.sourceLocale}, built from ${m.branch ?? "an unknown branch"} @ ${m.base?.slice(0, 7) ?? "an unknown commit"}`
1214
+ };
1215
+ }
1216
+ return {
1217
+ level: "fail",
1218
+ title,
1219
+ detail: `${manifest.url} answered HTTP ${manifest.status} without a LiveLocale share manifest (format 1). Rebuild with share: true or run livelocale pack.`
1220
+ };
1221
+ }
1222
+ var HEAD_OPEN = /<head(?=[\s>])[^>]*>/i;
1223
+ var HTML_OPEN = /<html(?=[\s>])[^>]*>/i;
1224
+ var DOCTYPE = /<!doctype\b[^>]*>/i;
1225
+ function firstHeadScript(html) {
1226
+ let start = 0;
1227
+ for (const pattern of [HEAD_OPEN, HTML_OPEN, DOCTYPE]) {
1228
+ const match = pattern.exec(html);
1229
+ if (match) {
1230
+ start = match.index + match[0].length;
1231
+ break;
1232
+ }
1233
+ }
1234
+ const rest = html.slice(start).replace(/^(?:\s|<!--[\s\S]*?-->)*/, "");
1235
+ return /^<script\b([^>]*)>/i.exec(rest)?.[1] ?? null;
1236
+ }
1237
+ function flagTagProblem(root, base, flagUrl) {
1238
+ const expected = `The first element in <head> of ${base} must be <script src="${flagUrl}"></script> without async. Run livelocale pack on the build output again.`;
1239
+ if (!isAnswer(root) || root.status !== 200) return expected;
1240
+ const attrs = firstHeadScript(root.body);
1241
+ if (attrs === null || /(?:^|\s)async(?=[\s=]|$)/i.test(attrs)) return expected;
1242
+ const src = /(?:^|\s)src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i.exec(attrs);
1243
+ const value = src ? src[1] ?? src[2] ?? src[3] ?? "" : "";
1244
+ try {
1245
+ if (src && new URL(value, base).href === flagUrl) return null;
1246
+ } catch {
1247
+ }
1248
+ return expected;
1249
+ }
1250
+ function checkFlagScript(flagPage, root, base) {
1251
+ const title = "Share flag script";
1252
+ const flag = answered(title, flagPage);
1253
+ if ("level" in flag) return flag;
1254
+ const problems = [];
1255
+ const type = flag.headers.get("content-type") || "";
1256
+ if (flag.status !== 200 || !/javascript|ecmascript/i.test(type) || !flag.body.startsWith(FLAG_SCRIPT_SIGNATURE)) {
1257
+ problems.push(
1258
+ `${flag.url} answered HTTP ${flag.status} (${type || "no content type"}) without the LiveLocale flag script. ${UPLOAD_HINT}`
1259
+ );
1260
+ }
1261
+ const tag = flagTagProblem(root, base, flag.url);
1262
+ if (tag) problems.push(tag);
1263
+ if (problems.length > 0) return { level: "fail", title, detail: problems.join(" ") };
1264
+ return { level: "ok", title, detail: `${flag.url} loads first in <head>` };
1265
+ }
1266
+ function cspSources(csp, directive) {
1267
+ for (const part of csp.split(";")) {
1268
+ const [name, ...sources] = part.trim().split(/\s+/);
1269
+ if (name && name.toLowerCase() === directive) return sources;
1270
+ }
1271
+ return null;
1272
+ }
1273
+ function allowsSelf(sources, origin) {
1274
+ return sources.some((s) => s === "'self'" || s === "*" || s.replace(/\/+$/, "") === origin);
1275
+ }
1276
+ function checkShareFrameHeaders(pages, origin) {
1277
+ const title = "Share frame headers";
1278
+ const blockers = [];
1279
+ for (const page of pages) {
1280
+ const answer = answered(title, page);
1281
+ if ("level" in answer) return answer;
1282
+ const xfo = (answer.headers.get("x-frame-options") || "").trim();
1283
+ if (/^deny$/i.test(xfo)) blockers.push(`${page.url} sends X-Frame-Options: ${xfo}`);
1284
+ const ancestors = cspSources(
1285
+ answer.headers.get("content-security-policy") || "",
1286
+ "frame-ancestors"
1287
+ );
1288
+ if (ancestors && (ancestors.includes("'none'") || !allowsSelf(ancestors, origin))) {
1289
+ blockers.push(`${page.url} sends frame-ancestors ${ancestors.join(" ") || "'none'"}`);
1290
+ }
1291
+ }
1292
+ if (blockers.length > 0) {
1293
+ return {
1294
+ level: "fail",
1295
+ title,
1296
+ detail: `${blockers.join("; ")}. The share Studio frames your app from the same origin; use X-Frame-Options: SAMEORIGIN or frame-ancestors 'self'.`
1297
+ };
1298
+ }
1299
+ return { level: "ok", title, detail: "both pages can be framed by the same origin" };
1300
+ }
1301
+ function checkShareCsp(pages, origin) {
1302
+ const title = "Share CSP";
1303
+ const blocked = [];
1304
+ for (const page of pages) {
1305
+ const answer = answered(title, page);
1306
+ if ("level" in answer) return answer;
1307
+ const csp = answer.headers.get("content-security-policy") || "";
1308
+ const scriptSrc = cspSources(csp, "script-src");
1309
+ const sources = scriptSrc ?? cspSources(csp, "default-src");
1310
+ if (sources && !allowsSelf(sources, origin)) {
1311
+ blocked.push(`${page.url} (${scriptSrc ? "script-src" : "default-src"})`);
1312
+ }
1313
+ }
1314
+ if (blocked.length > 0) {
1315
+ return {
1316
+ level: "warn",
1317
+ title,
1318
+ detail: `${blocked.join(", ")} leaves out 'self', so the browser may block livelocale/flag.js and the Studio's scripts. Add 'self' to script-src.`
1319
+ };
1320
+ }
1321
+ return { level: "ok", title, detail: "scripts from 'self' are allowed" };
1322
+ }
1323
+ function workerCandidates(root, base) {
1324
+ const urls = [];
1325
+ if (isAnswer(root)) {
1326
+ const registered = /serviceWorker\s*\.\s*register\s*\(\s*["'`]([^"'`]+)["'`]/.exec(root.body);
1327
+ if (registered) {
1328
+ try {
1329
+ urls.push(new URL(registered[1], base).href);
1330
+ } catch {
1331
+ }
1332
+ }
1333
+ }
1334
+ const names = ["sw.js", "service-worker.js"];
1335
+ for (const url of [
1336
+ ...names.map((name) => new URL(name, base).href),
1337
+ ...names.map((name) => new URL(`/${name}`, base).href)
1338
+ ]) {
1339
+ if (!urls.includes(url)) urls.push(url);
1340
+ }
1341
+ return urls;
1342
+ }
1343
+ async function checkServiceWorker(fetchImpl, root, base, origin) {
1344
+ const title = "Service worker";
1345
+ const found = [];
1346
+ for (const url of workerCandidates(root, base)) {
1347
+ const worker = await fetchPage(fetchImpl, url, origin);
1348
+ if (!isAnswer(worker)) continue;
1349
+ if (worker.wall) return { level: "fail", title, detail: worker.wall };
1350
+ if (worker.status !== 200 || /html/i.test(worker.headers.get("content-type") || "")) continue;
1351
+ found.push(url);
1352
+ if (/createHandlerBoundToURL|navigateFallback/.test(worker.body) && !worker.body.includes("livelocale")) {
1353
+ return {
1354
+ level: "warn",
1355
+ title,
1356
+ detail: `${url} answers navigations with the app shell, so livelocale/index.html would open your app. Add to your Workbox config: ${DENYLIST_LINE}`
1357
+ };
1358
+ }
1359
+ }
1360
+ return {
1361
+ level: "ok",
1362
+ title,
1363
+ detail: found.length > 0 ? `${found.join(", ")} leaves livelocale/ alone` : "none found"
1364
+ };
1365
+ }
1366
+ async function checkShare(origin, base, fetchImpl, manifest) {
1367
+ const shareUrl = (file) => `${base}${SHARE_DIR}/${file}`;
1368
+ const [root, studio, flag] = await Promise.all([
1369
+ fetchPage(fetchImpl, base, origin),
1370
+ fetchPage(fetchImpl, shareLinkFor(base), origin),
1371
+ fetchPage(fetchImpl, shareUrl(SHARE_FLAG_FILE), origin)
1372
+ ]);
1373
+ return [
1374
+ checkStudioPage(studio, root),
1375
+ checkManifest(manifest),
1376
+ checkFlagScript(flag, root, base),
1377
+ checkShareFrameHeaders([root, studio], origin),
1378
+ checkShareCsp([root, studio], origin),
1379
+ await checkServiceWorker(fetchImpl, root, base, origin),
1380
+ { level: "ok", title: "Share link", detail: shareLinkFor(base) }
1381
+ ];
1382
+ }
1383
+ function looksLikeShareBuild(manifest) {
1384
+ return isAnswer(manifest) && manifest.status === 200 && parseJson(manifest.body).ok;
1385
+ }
733
1386
  async function runDoctor(options) {
734
- const root = path2.resolve(options.projectDir);
1387
+ const root = path3.resolve(options.projectDir);
735
1388
  const findings = [
736
1389
  checkRouteHandler(root),
737
1390
  checkProvider(root),
@@ -739,7 +1392,10 @@ async function runDoctor(options) {
739
1392
  checkFrameHeaders(root),
740
1393
  checkLocales(root, options.localesPath)
741
1394
  ];
742
- if (options.appOrigin) {
1395
+ const workerConfig = checkServiceWorkerConfig(root);
1396
+ if (workerConfig) findings.push(workerConfig);
1397
+ const appOrigin = options.appOrigin ?? (options.appBase && new URL(options.appBase).origin);
1398
+ if (appOrigin) {
743
1399
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
744
1400
  if (typeof fetchImpl !== "function") {
745
1401
  findings.push({
@@ -748,8 +1404,19 @@ async function runDoctor(options) {
748
1404
  detail: "Live checks need Node 18+ (global fetch)."
749
1405
  });
750
1406
  } else {
751
- const token = options.token ?? process.env["LIVELOCALE_TOKEN"] ?? void 0;
752
- findings.push(...await checkLive(options.appOrigin, fetchImpl, token || void 0));
1407
+ const origin = appOrigin.replace(/\/+$/, "");
1408
+ const base = options.appBase ?? `${origin}/`;
1409
+ const manifest = await fetchPage(
1410
+ fetchImpl,
1411
+ `${base}${SHARE_DIR}/${SHARE_MANIFEST_FILE}`,
1412
+ origin
1413
+ );
1414
+ if (options.share || looksLikeShareBuild(manifest)) {
1415
+ findings.push(...await checkShare(origin, base, fetchImpl, manifest));
1416
+ } else {
1417
+ const token = options.token ?? process.env["LIVELOCALE_TOKEN"] ?? void 0;
1418
+ findings.push(...await checkLive(origin, fetchImpl, token || void 0));
1419
+ }
753
1420
  }
754
1421
  }
755
1422
  return findings;
@@ -769,7 +1436,7 @@ function formatFindings(findings) {
769
1436
 
770
1437
  // src/codemod/check.ts
771
1438
  var fs4 = __toESM(require("fs"), 1);
772
- var path4 = __toESM(require("path"), 1);
1439
+ var path5 = __toESM(require("path"), 1);
773
1440
 
774
1441
  // src/core/placeholders.ts
775
1442
  var PLURAL_TYPES = /* @__PURE__ */ new Set(["plural", "select", "selectordinal"]);
@@ -934,7 +1601,7 @@ function validateMessageSyntax(message) {
934
1601
 
935
1602
  // src/server/key-scanner.ts
936
1603
  var import_fs = require("fs");
937
- var path3 = __toESM(require("path"), 1);
1604
+ var path4 = __toESM(require("path"), 1);
938
1605
 
939
1606
  // src/core/env.ts
940
1607
  var OFF_VALUES = /* @__PURE__ */ new Set(["off", "0", "false"]);
@@ -1215,7 +1882,7 @@ var KeyScanner = class {
1215
1882
  }
1216
1883
  }
1217
1884
  async loadGitignore(projectRoot) {
1218
- const gitignorePath = path3.join(projectRoot, ".gitignore");
1885
+ const gitignorePath = path4.join(projectRoot, ".gitignore");
1219
1886
  let stat;
1220
1887
  try {
1221
1888
  stat = await import_fs.promises.stat(gitignorePath);
@@ -1240,16 +1907,16 @@ var KeyScanner = class {
1240
1907
  * Scans the project (the parent directory of `basePath`) for translation key references.
1241
1908
  */
1242
1909
  async scanCodebaseKeys(basePath) {
1243
- return this.scanSourceKeys(path3.dirname(path3.resolve(basePath)));
1910
+ return this.scanSourceKeys(path4.dirname(path4.resolve(basePath)));
1244
1911
  }
1245
1912
  /** Scans every source file under `projectRoot` for translation key references. */
1246
1913
  async scanSourceKeys(projectRoot) {
1247
1914
  this.assertEnabled();
1248
- projectRoot = path3.resolve(projectRoot);
1915
+ projectRoot = path4.resolve(projectRoot);
1249
1916
  const gitignore = await this.loadGitignore(projectRoot);
1250
1917
  const keysSet = /* @__PURE__ */ new Set();
1251
1918
  const seen = /* @__PURE__ */ new Set();
1252
- const toRel = (fullPath) => path3.relative(projectRoot, fullPath).split(path3.sep).join("/");
1919
+ const toRel = (fullPath) => path4.relative(projectRoot, fullPath).split(path4.sep).join("/");
1253
1920
  const scanDir = async (dir) => {
1254
1921
  let items;
1255
1922
  try {
@@ -1258,7 +1925,7 @@ var KeyScanner = class {
1258
1925
  return;
1259
1926
  }
1260
1927
  for (const item of items) {
1261
- const fullPath = path3.join(dir, item.name);
1928
+ const fullPath = path4.join(dir, item.name);
1262
1929
  if (item.isDirectory()) {
1263
1930
  if (IGNORE_DIRS.has(item.name)) continue;
1264
1931
  if (gitignore.ignores(toRel(fullPath), true)) continue;
@@ -1266,7 +1933,7 @@ var KeyScanner = class {
1266
1933
  continue;
1267
1934
  }
1268
1935
  if (!item.isFile()) continue;
1269
- if (!SOURCE_EXTENSIONS.has(path3.extname(item.name))) continue;
1936
+ if (!SOURCE_EXTENSIONS.has(path4.extname(item.name))) continue;
1270
1937
  if (item.name.endsWith(".d.ts")) continue;
1271
1938
  if (gitignore.ignores(toRel(fullPath), false)) continue;
1272
1939
  let stat;
@@ -1306,8 +1973,8 @@ var KeyScanner = class {
1306
1973
  const keysSet = /* @__PURE__ */ new Set();
1307
1974
  for (const locale of locales) {
1308
1975
  try {
1309
- const resolvedBasePath = path3.resolve(basePath);
1310
- const localePath = path3.resolve(resolvedBasePath, locale);
1976
+ const resolvedBasePath = path4.resolve(basePath);
1977
+ const localePath = path4.resolve(resolvedBasePath, locale);
1311
1978
  let isDir = false;
1312
1979
  try {
1313
1980
  const stats = await import_fs.promises.stat(localePath);
@@ -1319,9 +1986,9 @@ var KeyScanner = class {
1319
1986
  const files = await import_fs.promises.readdir(localePath);
1320
1987
  for (const file of files) {
1321
1988
  if (file.endsWith(".json")) {
1322
- const ns = path3.basename(file, ".json");
1989
+ const ns = path4.basename(file, ".json");
1323
1990
  try {
1324
- const content = await import_fs.promises.readFile(path3.join(localePath, file), "utf-8");
1991
+ const content = await import_fs.promises.readFile(path4.join(localePath, file), "utf-8");
1325
1992
  data[ns] = JSON.parse(content);
1326
1993
  } catch {
1327
1994
  }
@@ -1332,7 +1999,7 @@ var KeyScanner = class {
1332
1999
  if (!isSafeLocaleSegment(locale)) {
1333
2000
  continue;
1334
2001
  }
1335
- const filePath = path3.resolve(resolvedBasePath, `${locale}.json`);
2002
+ const filePath = path4.resolve(resolvedBasePath, `${locale}.json`);
1336
2003
  if (filePath.startsWith(resolvedBasePath)) {
1337
2004
  const content = await import_fs.promises.readFile(filePath, "utf-8");
1338
2005
  data = JSON.parse(content);
@@ -1398,7 +2065,7 @@ function listLocales(localesDir) {
1398
2065
  for (const item of fs4.readdirSync(localesDir, { withFileTypes: true })) {
1399
2066
  if (item.isDirectory() && LOCALE_RE.test(item.name)) locales.add(item.name);
1400
2067
  else if (item.isFile() && item.name.endsWith(".json")) {
1401
- const name = path4.basename(item.name, ".json");
2068
+ const name = path5.basename(item.name, ".json");
1402
2069
  if (LOCALE_RE.test(name)) locales.add(name);
1403
2070
  }
1404
2071
  }
@@ -1418,7 +2085,7 @@ function flatten(obj, prefix, data) {
1418
2085
  }
1419
2086
  function readLocale(localesDir, locale, projectDir, issues) {
1420
2087
  const data = { leaves: /* @__PURE__ */ new Map(), branches: /* @__PURE__ */ new Set() };
1421
- const rel = (p) => path4.relative(projectDir, p).split(path4.sep).join("/");
2088
+ const rel = (p) => path5.relative(projectDir, p).split(path5.sep).join("/");
1422
2089
  const parse = (file) => {
1423
2090
  try {
1424
2091
  return JSON.parse(fs4.readFileSync(file, "utf-8"));
@@ -1433,18 +2100,18 @@ function readLocale(localesDir, locale, projectDir, issues) {
1433
2100
  return void 0;
1434
2101
  }
1435
2102
  };
1436
- const dir = path4.join(localesDir, locale);
2103
+ const dir = path5.join(localesDir, locale);
1437
2104
  if (fs4.existsSync(dir) && fs4.statSync(dir).isDirectory()) {
1438
2105
  for (const file of fs4.readdirSync(dir).sort()) {
1439
2106
  if (!file.endsWith(".json")) continue;
1440
- const ns = path4.basename(file, ".json");
1441
- const parsed = parse(path4.join(dir, file));
2107
+ const ns = path5.basename(file, ".json");
2108
+ const parsed = parse(path5.join(dir, file));
1442
2109
  if (parsed === void 0) continue;
1443
2110
  data.branches.add(ns);
1444
2111
  flatten(parsed, ns, data);
1445
2112
  }
1446
2113
  } else {
1447
- flatten(parse(path4.join(localesDir, `${locale}.json`)), "", data);
2114
+ flatten(parse(path5.join(localesDir, `${locale}.json`)), "", data);
1448
2115
  }
1449
2116
  return data;
1450
2117
  }
@@ -1468,8 +2135,8 @@ function isReferenced(key, codeKeys) {
1468
2135
  return false;
1469
2136
  }
1470
2137
  async function runCheck(options) {
1471
- const projectDir = path4.resolve(options.projectDir);
1472
- const localesDir = path4.resolve(projectDir, options.localesPath ?? "locales");
2138
+ const projectDir = path5.resolve(options.projectDir);
2139
+ const localesDir = path5.resolve(projectDir, options.localesPath ?? "locales");
1473
2140
  const issues = [];
1474
2141
  const strict = options.strict ?? false;
1475
2142
  const warn = strict ? "fail" : "warn";
@@ -1586,62 +2253,2205 @@ function formatCheckReport(report, limit = 25) {
1586
2253
  return lines.join("\n");
1587
2254
  }
1588
2255
 
1589
- // src/codemod/index.ts
1590
- var import_meta = {};
1591
- var getDirname = () => {
1592
- if (typeof __dirname !== "undefined") {
1593
- return __dirname;
1594
- }
1595
- return path5.dirname((0, import_url.fileURLToPath)(import_meta.url));
1596
- };
1597
- var DEFAULT_STUDIO_PORT = 3010;
1598
- var DEFAULT_APP_ORIGIN = "http://localhost:3000";
1599
- function printHelp() {
1600
- console.log(`
1601
- LiveLocale (livelocale) CLI
1602
-
1603
- Usage:
1604
- npx livelocale <command> [options]
1605
-
1606
- Commands:
1607
- migrate Bulk migrate existing react-i18next translation usages to wrap them with wrapTranslationEngine automatically.
1608
- init Initialize the API route handler inside a Next.js project automatically.
1609
- studio Start the visual editing studio local web server.
1610
- doctor Check the project for common integration problems (route, provider, middleware, CSP, locales).
1611
- check Check translations for missing keys, placeholder drift, broken ICU syntax and unused keys (CI-friendly).
1612
-
1613
- Options for migrate:
1614
- --dir <path> Directory to scan (default: current directory)
1615
- --exclude <list> Comma-separated list of directories to exclude (default: node_modules,.next,dist,.git)
1616
- --dry-run Preview changes without writing them to disk
1617
-
1618
- Options for init:
1619
- --dir <path> Target project directory (default: current directory)
1620
-
1621
- Options for studio:
1622
- --port <number> Port to run the studio server on (default: ${DEFAULT_STUDIO_PORT})
1623
- --app <origin> Origin of the app being edited (default: ${DEFAULT_APP_ORIGIN})
1624
- Can also be changed at run time with ?app=<origin> on the Studio URL.
1625
-
1626
- Options for doctor:
1627
- --dir <path> Project directory to inspect (default: current directory)
1628
- --locales <path> Locales directory relative to the project (default: locales)
1629
- --app <origin> Also probe the running app, e.g. http://localhost:3000
1630
- --token <token> Access token for a preview deployment (default: LIVELOCALE_TOKEN)
1631
-
1632
- Options for check:
1633
- --dir <path> Project directory to scan (default: current directory)
1634
- --locales <path> Locales directory relative to the project (default: locales)
1635
- --source <locale> Locale the others are compared against (default: en, else the first locale)
1636
- --no-unused Do not report keys that code never references
1637
- --strict Treat warnings (unused keys, empty values) as failures
1638
- --json Print the full report as JSON
2256
+ // src/server/pack.ts
2257
+ var import_child_process2 = require("child_process");
2258
+ var import_fs4 = require("fs");
2259
+ var path8 = __toESM(require("path"), 1);
1639
2260
 
1640
- Global Options:
1641
- -h, --help Show help information
1642
- `);
1643
- }
1644
- function normalizeOriginArg(value, flag) {
2261
+ // src/server/build-info.ts
2262
+ var import_child_process = require("child_process");
2263
+ var import_fs2 = require("fs");
2264
+ var path6 = __toESM(require("path"), 1);
2265
+ var runGit = (args, cwd) => new Promise((resolve14) => {
2266
+ try {
2267
+ (0, import_child_process.execFile)(
2268
+ "git",
2269
+ args,
2270
+ { cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
2271
+ (error, stdout) => resolve14(error ? null : String(stdout).trim())
2272
+ );
2273
+ } catch {
2274
+ resolve14(null);
2275
+ }
2276
+ });
2277
+ var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
2278
+ async function firstValid(candidates, accept) {
2279
+ for (const candidate of candidates) {
2280
+ const value = typeof candidate === "function" ? await candidate() : candidate;
2281
+ if (typeof value !== "string" || value === "") continue;
2282
+ const accepted = accept(value);
2283
+ if (accepted !== null) return accepted;
2284
+ }
2285
+ return null;
2286
+ }
2287
+ var asBranch = (value) => isValidBranchName(value) ? value : null;
2288
+ var asSha = (value) => SHA_RE.test(value) ? value.toLowerCase() : null;
2289
+ var asSlug = (value) => isValidRepoSlug(value) ? value : null;
2290
+ function readEvent(eventPath) {
2291
+ if (!eventPath) return {};
2292
+ try {
2293
+ const parsed = JSON.parse((0, import_fs2.readFileSync)(eventPath, "utf-8"));
2294
+ return isRecord(parsed) ? parsed : {};
2295
+ } catch {
2296
+ return {};
2297
+ }
2298
+ }
2299
+ function isRecord(value) {
2300
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2301
+ }
2302
+ function pick(obj, ...keys) {
2303
+ let current = obj;
2304
+ for (const key of keys) {
2305
+ if (!isRecord(current)) return void 0;
2306
+ current = current[key];
2307
+ }
2308
+ return typeof current === "string" ? current : void 0;
2309
+ }
2310
+ async function resolveBuildInfo(options) {
2311
+ const { cwd, env = process.env, git = runGit, includeRepo = false } = options;
2312
+ const event = readEvent(env.GITHUB_EVENT_PATH);
2313
+ const isPullRequest = env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
2314
+ const netlify = env.NETLIFY === "true";
2315
+ const gitlab = env.GITLAB_CI === "true";
2316
+ const branch = await firstValid(
2317
+ [
2318
+ env.GITHUB_HEAD_REF,
2319
+ env.GITHUB_REF_TYPE === "branch" ? env.GITHUB_REF_NAME : null,
2320
+ env.VERCEL_GIT_COMMIT_REF,
2321
+ env.CF_PAGES_BRANCH,
2322
+ netlify ? env.BRANCH : null,
2323
+ gitlab ? env.CI_COMMIT_REF_NAME : null,
2324
+ async () => {
2325
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
2326
+ return head === "HEAD" ? null : head;
2327
+ }
2328
+ ],
2329
+ asBranch
2330
+ );
2331
+ const base = await firstValid(
2332
+ [
2333
+ isPullRequest ? pick(event, "pull_request", "head", "sha") : null,
2334
+ env.GITHUB_SHA,
2335
+ env.VERCEL_GIT_COMMIT_SHA,
2336
+ env.CF_PAGES_COMMIT_SHA,
2337
+ netlify ? env.COMMIT_REF : null,
2338
+ gitlab ? env.CI_COMMIT_SHA : null,
2339
+ () => git(["rev-parse", "HEAD"], cwd)
2340
+ ],
2341
+ asSha
2342
+ );
2343
+ const defaultBranch = await firstValid(
2344
+ [
2345
+ pick(event, "repository", "default_branch"),
2346
+ async () => {
2347
+ const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
2348
+ return ref?.startsWith("origin/") ? ref.slice("origin/".length) : null;
2349
+ }
2350
+ ],
2351
+ asBranch
2352
+ );
2353
+ const githubCom = env.GITHUB_SERVER_URL === void 0 || env.GITHUB_SERVER_URL === "" || env.GITHUB_SERVER_URL === "https://github.com";
2354
+ const repo = includeRepo ? await firstValid(
2355
+ [
2356
+ githubCom ? env.GITHUB_REPOSITORY : null,
2357
+ env.VERCEL_GIT_PROVIDER === "github" && env.VERCEL_GIT_REPO_OWNER && env.VERCEL_GIT_REPO_SLUG ? `${env.VERCEL_GIT_REPO_OWNER}/${env.VERCEL_GIT_REPO_SLUG}` : null,
2358
+ netlify && env.REPOSITORY_URL ? parseGitHubSlug(env.REPOSITORY_URL) : null,
2359
+ async () => {
2360
+ const remote = await git(["remote", "get-url", "origin"], cwd);
2361
+ return remote ? parseGitHubSlug(remote) : null;
2362
+ }
2363
+ ],
2364
+ asSlug
2365
+ ) : null;
2366
+ const top = await git(["rev-parse", "--show-toplevel"], cwd);
2367
+ const gitRoot = top ? path6.resolve(top) : null;
2368
+ return { repo, defaultBranch, branch, base, gitRoot };
2369
+ }
2370
+ function parseGitHubSlug(remote) {
2371
+ const text = remote.trim();
2372
+ let repoPath;
2373
+ const scp = /^[A-Za-z0-9._-]+@github\.com:(.*)$/i.exec(text);
2374
+ if (scp) {
2375
+ repoPath = scp[1] ?? "";
2376
+ } else {
2377
+ let url;
2378
+ try {
2379
+ url = new URL(text);
2380
+ } catch {
2381
+ return null;
2382
+ }
2383
+ if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
2384
+ if (url.hostname.toLowerCase() !== "github.com") return null;
2385
+ repoPath = url.pathname;
2386
+ }
2387
+ const trimmed = repoPath.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
2388
+ return isValidRepoSlug(trimmed) ? trimmed : null;
2389
+ }
2390
+
2391
+ // src/server/build-target.ts
2392
+ var import_fs3 = require("fs");
2393
+ var path7 = __toESM(require("path"), 1);
2394
+ var NEXT_CONFIG_FILES = [
2395
+ "next.config.js",
2396
+ "next.config.mjs",
2397
+ "next.config.cjs",
2398
+ "next.config.ts",
2399
+ "next.config.mts"
2400
+ ];
2401
+ var VITE_CONFIG_FILES = [
2402
+ "vite.config.js",
2403
+ "vite.config.mjs",
2404
+ "vite.config.cjs",
2405
+ "vite.config.ts",
2406
+ "vite.config.mts"
2407
+ ];
2408
+ var OUTPUT_EXPORT = /\boutput\s*:\s*['"`]export['"`]/;
2409
+ var BASE_PATH = /\bbasePath\s*:\s*['"`](\/[^'"`]*)['"`]/;
2410
+ var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
2411
+ var EXPORTS_POST = /export\s+(?:const|let|var|async\s+function|function)\s+POST\b|export\s*\{[^}]*\bPOST\b[^}]*\}/;
2412
+ function readText2(file) {
2413
+ try {
2414
+ return (0, import_fs3.readFileSync)(file, "utf-8");
2415
+ } catch {
2416
+ return null;
2417
+ }
2418
+ }
2419
+ function readPackageJson(projectDir) {
2420
+ const text = readText2(path7.join(projectDir, "package.json"));
2421
+ if (text === null) return {};
2422
+ try {
2423
+ const parsed = JSON.parse(text);
2424
+ return parsed && typeof parsed === "object" ? parsed : {};
2425
+ } catch {
2426
+ return {};
2427
+ }
2428
+ }
2429
+ function hasDependency(pkg, name) {
2430
+ return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
2431
+ }
2432
+ function firstExisting2(projectDir, names) {
2433
+ for (const name of names) {
2434
+ const file = path7.join(projectDir, name);
2435
+ if ((0, import_fs3.existsSync)(file)) return file;
2436
+ }
2437
+ return null;
2438
+ }
2439
+ function stripComments(source) {
2440
+ return source.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").filter((line) => !line.trimStart().startsWith("//")).join("\n");
2441
+ }
2442
+ function craBase(homepage) {
2443
+ if (typeof homepage !== "string") return null;
2444
+ const value = homepage.trim();
2445
+ if (value === "." || value === "./") return "./";
2446
+ if (value.startsWith("/")) return value;
2447
+ try {
2448
+ return new URL(value).pathname;
2449
+ } catch {
2450
+ return null;
2451
+ }
2452
+ }
2453
+ function detectBuildTarget(projectDir) {
2454
+ const pkg = readPackageJson(projectDir);
2455
+ const nextConfig = firstExisting2(projectDir, NEXT_CONFIG_FILES);
2456
+ if (nextConfig || hasDependency(pkg, "next")) {
2457
+ const source = stripComments((nextConfig && readText2(nextConfig)) ?? "");
2458
+ return {
2459
+ kind: "next",
2460
+ staticExport: OUTPUT_EXPORT.test(source),
2461
+ base: BASE_PATH.exec(source)?.[1] ?? null
2462
+ };
2463
+ }
2464
+ if (hasDependency(pkg, "react-scripts")) return { kind: "cra", base: craBase(pkg.homepage) };
2465
+ if (hasDependency(pkg, "vite") || firstExisting2(projectDir, VITE_CONFIG_FILES)) {
2466
+ const viteConfig = firstExisting2(projectDir, VITE_CONFIG_FILES);
2467
+ const source = stripComments((viteConfig && readText2(viteConfig)) ?? "");
2468
+ return { kind: "vite", base: VITE_BASE.exec(source)?.[1] ?? null };
2469
+ }
2470
+ return { kind: "other", base: null };
2471
+ }
2472
+ var ROUTE_DIRS = ["app", "src/app"];
2473
+ var ROUTE_EXTENSIONS = ["ts", "js", "mjs", "tsx", "jsx"];
2474
+ function findExportBlockingRoute(projectDir) {
2475
+ for (const dir of ROUTE_DIRS) {
2476
+ for (const ext of ROUTE_EXTENSIONS) {
2477
+ const rel = `${dir}/api/livelocale/route.${ext}`;
2478
+ const source = readText2(path7.join(projectDir, rel));
2479
+ if (source !== null && EXPORTS_POST.test(source)) return rel;
2480
+ }
2481
+ }
2482
+ return null;
2483
+ }
2484
+ function outputCandidates(target) {
2485
+ return target.kind === "cra" ? ["build", "dist", "out"] : ["dist", "build", "out"];
2486
+ }
2487
+
2488
+ // src/server/pack-error.ts
2489
+ var PackError = class extends Error {
2490
+ code;
2491
+ constructor(code, message) {
2492
+ super(message);
2493
+ this.name = "PackError";
2494
+ this.code = code;
2495
+ }
2496
+ };
2497
+
2498
+ // src/server/share-html.ts
2499
+ var FULL_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
2500
+ var WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
2501
+ var SEGMENT = /^(?:[A-Za-z0-9._~@+-]|%[0-9A-Fa-f]{2})+$/;
2502
+ function normalizeBase(value) {
2503
+ if (value === "" || value === "." || value === "./") return { kind: "relative" };
2504
+ if (WINDOWS_PATH.test(value)) {
2505
+ throw new PackError(
2506
+ "BAD_BASE",
2507
+ `The app base "${value}" is a Windows path. Your shell (Git Bash/MSYS) probably converted a site path such as /repo/ into it. Run the command with MSYS_NO_PATHCONV=1, or from an npm script, and pass the site path, for example --base /app/.`
2508
+ );
2509
+ }
2510
+ if (FULL_URL.test(value)) {
2511
+ throw new PackError(
2512
+ "BAD_BASE",
2513
+ `The app base "${value}" is a full URL. Pass the path your HTML is served from, for example --base /app/ (shareBase in the Vite plugin).`
2514
+ );
2515
+ }
2516
+ const notASitePath = () => new PackError(
2517
+ "BAD_BASE",
2518
+ `The app base "${value}" is not a site path. Use "/", "/app/" or "./".`
2519
+ );
2520
+ if (!value.startsWith("/")) throw notASitePath();
2521
+ const segments = value.slice(1).split("/");
2522
+ if (segments[segments.length - 1] === "") segments.pop();
2523
+ for (const segment of segments) {
2524
+ if (segment === "." || segment === ".." || !SEGMENT.test(segment)) throw notASitePath();
2525
+ }
2526
+ return { kind: "absolute", path: segments.length === 0 ? "/" : `/${segments.join("/")}/` };
2527
+ }
2528
+ function basePathOf(base) {
2529
+ return base.kind === "absolute" ? base.path : null;
2530
+ }
2531
+ function flagSrcFor(base, htmlRelPath) {
2532
+ const file = `${SHARE_DIR}/${SHARE_FLAG_FILE}`;
2533
+ if (base.kind === "absolute") return `${base.path}${file}`;
2534
+ const depth = htmlRelPath.split("/").length - 1;
2535
+ return depth === 0 ? `./${file}` : `${"../".repeat(depth)}${file}`;
2536
+ }
2537
+ function shareUrlPathFor(base) {
2538
+ const page = `${SHARE_DIR}/index.html`;
2539
+ return base.kind === "absolute" ? `${base.path}${page}` : page;
2540
+ }
2541
+ var EXISTING_FLAG_TAG = /<script\b[^>]*\bsrc="[^"]*livelocale\/flag\.js"[^>]*>\s*<\/script>/gi;
2542
+ var HEAD_OPEN2 = /<head(?=[\s>])[^>]*>/i;
2543
+ var HTML_OPEN2 = /<html(?=[\s>])[^>]*>/i;
2544
+ var DOCTYPE2 = /<!doctype\b[^>]*>/i;
2545
+ function escapeAttribute(value) {
2546
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2547
+ }
2548
+ function injectFlagScript(html, src) {
2549
+ const tag = `<script src="${escapeAttribute(src)}"></script>`;
2550
+ const cleaned = html.replace(EXISTING_FLAG_TAG, "");
2551
+ for (const pattern of [HEAD_OPEN2, HTML_OPEN2, DOCTYPE2]) {
2552
+ const match = pattern.exec(cleaned);
2553
+ if (match) {
2554
+ const end = match.index + match[0].length;
2555
+ return cleaned.slice(0, end) + tag + cleaned.slice(end);
2556
+ }
2557
+ }
2558
+ return tag + cleaned;
2559
+ }
2560
+
2561
+ // src/server/pack.ts
2562
+ var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the manifest has no repo.";
2563
+ function isPlainObject2(value) {
2564
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2565
+ }
2566
+ function toPosix(relative8) {
2567
+ return relative8.split(path8.sep).join("/") || ".";
2568
+ }
2569
+ function isInside(parent, child) {
2570
+ const rel = path8.relative(parent, child);
2571
+ return rel === "" || !rel.startsWith("..") && !path8.isAbsolute(rel);
2572
+ }
2573
+ function defineOwn(target, key, value) {
2574
+ Object.defineProperty(target, key, {
2575
+ value,
2576
+ enumerable: true,
2577
+ writable: true,
2578
+ configurable: true
2579
+ });
2580
+ }
2581
+ async function readJsonObject(file, projectDir) {
2582
+ const shown = toPosix(path8.relative(projectDir, file));
2583
+ let parsed;
2584
+ try {
2585
+ parsed = JSON.parse(await import_fs4.promises.readFile(file, "utf-8"));
2586
+ } catch (error) {
2587
+ throw new PackError(
2588
+ "INVALID_LOCALE_FILE",
2589
+ `${shown} is not a JSON object: ${error instanceof Error ? error.message : String(error)}`
2590
+ );
2591
+ }
2592
+ if (!isPlainObject2(parsed)) {
2593
+ const kind = Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed;
2594
+ throw new PackError(
2595
+ "INVALID_LOCALE_FILE",
2596
+ `${shown} is not a JSON object: the root is ${kind}`
2597
+ );
2598
+ }
2599
+ return parsed;
2600
+ }
2601
+ async function collectShareData(options) {
2602
+ const projectDir = path8.resolve(options.projectDir);
2603
+ const localesDir = path8.resolve(projectDir, options.localesPath ?? "locales");
2604
+ const warnings = [];
2605
+ let entries;
2606
+ try {
2607
+ if (!(await import_fs4.promises.stat(localesDir)).isDirectory()) throw new Error("not a directory");
2608
+ entries = await import_fs4.promises.readdir(localesDir, { withFileTypes: true });
2609
+ } catch {
2610
+ throw new PackError(
2611
+ "LOCALES_NOT_FOUND",
2612
+ `Locales directory not found: ${localesDir}. Pass --locales <path>.`
2613
+ );
2614
+ }
2615
+ const dirLocales = /* @__PURE__ */ new Set();
2616
+ const fileLocales = /* @__PURE__ */ new Set();
2617
+ for (const entry of entries) {
2618
+ if (entry.isDirectory() && LOCALE_RE.test(entry.name)) {
2619
+ dirLocales.add(entry.name);
2620
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
2621
+ const code = entry.name.slice(0, -".json".length);
2622
+ if (LOCALE_RE.test(code)) fileLocales.add(code);
2623
+ }
2624
+ }
2625
+ const locales = [.../* @__PURE__ */ new Set([...dirLocales, ...fileLocales])].sort();
2626
+ if (locales.length === 0) {
2627
+ throw new PackError(
2628
+ "NO_LOCALES",
2629
+ `No locale files in ${localesDir} (expected en.json or en/*.json).`
2630
+ );
2631
+ }
2632
+ const layout = dirLocales.size > 0 ? "namespaced" : "flat";
2633
+ const sourceLocale = options.sourceLocale ?? (locales.includes("en") ? "en" : locales[0]);
2634
+ if (!locales.includes(sourceLocale)) {
2635
+ throw new PackError(
2636
+ "SOURCE_LOCALE_NOT_FOUND",
2637
+ `Source locale "${sourceLocale}" not found in ${localesDir} (found: ${locales.join(", ")}).`
2638
+ );
2639
+ }
2640
+ const dictionaries = {};
2641
+ for (const locale of locales) {
2642
+ let dictionary;
2643
+ if (dirLocales.has(locale)) {
2644
+ dictionary = {};
2645
+ const folder = path8.join(localesDir, locale);
2646
+ const files = (await import_fs4.promises.readdir(folder, { withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".json")).map((file) => file.name).sort();
2647
+ for (const file of files) {
2648
+ const namespace = file.slice(0, -".json".length);
2649
+ defineOwn(dictionary, namespace, await readJsonObject(path8.join(folder, file), projectDir));
2650
+ }
2651
+ } else {
2652
+ dictionary = await readJsonObject(path8.join(localesDir, `${locale}.json`), projectDir);
2653
+ }
2654
+ defineOwn(dictionaries, locale, dictionary);
2655
+ }
2656
+ const info = await resolveBuildInfo({
2657
+ cwd: projectDir,
2658
+ env: options.env,
2659
+ git: options.git,
2660
+ includeRepo: options.exposeRepo === true
2661
+ });
2662
+ const root = info.gitRoot !== null && isInside(info.gitRoot, localesDir) ? info.gitRoot : projectDir;
2663
+ const codeKeys = await new KeyScanner({ requireEnabled: false }).scanSourceKeys(projectDir);
2664
+ if (options.exposeRepo === true && info.repo === null) warnings.push(NO_REPO_WARNING);
2665
+ const manifest = {
2666
+ format: SHARE_MANIFEST_FORMAT,
2667
+ ...options.exposeRepo === true && info.repo !== null ? { repo: info.repo } : {},
2668
+ defaultBranch: info.defaultBranch,
2669
+ branch: info.branch,
2670
+ base: info.base,
2671
+ localesPath: toPosix(path8.relative(root, localesDir)),
2672
+ layout,
2673
+ sourceLocale,
2674
+ locales,
2675
+ basePath: options.basePath,
2676
+ codeKeys: [...codeKeys].sort()
2677
+ };
2678
+ return { manifest, dictionaries, warnings };
2679
+ }
2680
+ var RELATIVE_BASE_WARNING = "The app base is relative (./), so flag.js is linked per HTML file. Pages served by a client-side route fallback cannot load it; build with an absolute base (--base /path/) if the app has such routes.";
2681
+ function defaultStudioCandidates() {
2682
+ const here = moduleDir();
2683
+ return [path8.resolve(here, "../studio"), path8.resolve(here, "../../dist/studio")];
2684
+ }
2685
+ function isDirectory(target) {
2686
+ try {
2687
+ return (0, import_fs4.statSync)(target).isDirectory();
2688
+ } catch {
2689
+ return false;
2690
+ }
2691
+ }
2692
+ function resolveStudioDir(candidates = defaultStudioCandidates()) {
2693
+ for (const dir of candidates) {
2694
+ if ((0, import_fs4.existsSync)(path8.join(dir, "index.html")) && isDirectory(path8.join(dir, "assets"))) {
2695
+ return dir;
2696
+ }
2697
+ }
2698
+ return null;
2699
+ }
2700
+ async function ownsShareDir(shareDir) {
2701
+ try {
2702
+ await import_fs4.promises.lstat(shareDir);
2703
+ } catch {
2704
+ return true;
2705
+ }
2706
+ try {
2707
+ const manifest = JSON.parse(
2708
+ await import_fs4.promises.readFile(path8.join(shareDir, SHARE_MANIFEST_FILE), "utf-8")
2709
+ );
2710
+ return isPlainObject2(manifest) && typeof manifest["format"] === "number";
2711
+ } catch {
2712
+ return false;
2713
+ }
2714
+ }
2715
+ async function findHtmlFiles(outDir, shareDir) {
2716
+ const found = [];
2717
+ const walk = async (dir) => {
2718
+ for (const entry of await import_fs4.promises.readdir(dir, { withFileTypes: true })) {
2719
+ if (entry.isSymbolicLink()) continue;
2720
+ const full = path8.join(dir, entry.name);
2721
+ if (entry.isDirectory()) {
2722
+ if (full !== shareDir) await walk(full);
2723
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
2724
+ found.push(toPosix(path8.relative(outDir, full)));
2725
+ }
2726
+ }
2727
+ };
2728
+ await walk(outDir);
2729
+ return found.sort();
2730
+ }
2731
+ async function packOutput(options) {
2732
+ const {
2733
+ outDir: outOption,
2734
+ base: baseOption,
2735
+ injectHtml = "auto",
2736
+ studioDir: studioOption,
2737
+ ...shareOptions
2738
+ } = options;
2739
+ const projectDir = path8.resolve(options.projectDir);
2740
+ const outDir = path8.resolve(projectDir, outOption);
2741
+ if (!isDirectory(outDir)) {
2742
+ throw new PackError(
2743
+ "OUT_DIR_NOT_FOUND",
2744
+ `Build output not found: ${outDir}. Run your build first.`
2745
+ );
2746
+ }
2747
+ const target = detectBuildTarget(projectDir);
2748
+ const base = normalizeBase(baseOption ?? target.base ?? "/");
2749
+ const candidates = studioOption !== void 0 ? [path8.resolve(projectDir, studioOption)] : defaultStudioCandidates();
2750
+ const studioDir = resolveStudioDir(candidates);
2751
+ if (studioDir === null) {
2752
+ throw new PackError(
2753
+ "STUDIO_NOT_FOUND",
2754
+ `LiveLocale Studio files are missing from the installed package (looked in ${candidates.join(", ")}). Reinstall livelocale.`
2755
+ );
2756
+ }
2757
+ const data = await collectShareData({ ...shareOptions, projectDir, basePath: basePathOf(base) });
2758
+ const shareDir = path8.join(outDir, SHARE_DIR);
2759
+ if (!await ownsShareDir(shareDir)) {
2760
+ throw new PackError(
2761
+ "SHARE_DIR_TAKEN",
2762
+ `${toPosix(path8.relative(projectDir, outDir))}/${SHARE_DIR}/ already exists and was not written by LiveLocale. Move it away and run again.`
2763
+ );
2764
+ }
2765
+ await import_fs4.promises.rm(shareDir, { recursive: true, force: true });
2766
+ await import_fs4.promises.cp(studioDir, shareDir, { recursive: true });
2767
+ await import_fs4.promises.writeFile(
2768
+ path8.join(shareDir, SHARE_MANIFEST_FILE),
2769
+ JSON.stringify(data.manifest, null, 2) + "\n",
2770
+ "utf-8"
2771
+ );
2772
+ const localesDir = path8.join(shareDir, SHARE_LOCALES_DIR);
2773
+ await import_fs4.promises.mkdir(localesDir, { recursive: true });
2774
+ for (const locale of data.manifest.locales) {
2775
+ await import_fs4.promises.writeFile(
2776
+ path8.join(localesDir, `${locale}.json`),
2777
+ JSON.stringify(data.dictionaries[locale]),
2778
+ "utf-8"
2779
+ );
2780
+ }
2781
+ await import_fs4.promises.writeFile(path8.join(shareDir, SHARE_FLAG_FILE), FLAG_SCRIPT, "utf-8");
2782
+ const inject = injectHtml === "auto" ? target.kind !== "next" && !(0, import_fs4.existsSync)(path8.join(outDir, "_next")) : injectHtml;
2783
+ const htmlInjected = inject ? await findHtmlFiles(outDir, shareDir) : [];
2784
+ for (const rel of htmlInjected) {
2785
+ const file = path8.join(outDir, ...rel.split("/"));
2786
+ const html = await import_fs4.promises.readFile(file, "utf-8");
2787
+ const injected = injectFlagScript(html, flagSrcFor(base, rel));
2788
+ if (injected !== html) await import_fs4.promises.writeFile(file, injected, "utf-8");
2789
+ }
2790
+ const warnings = [...data.warnings];
2791
+ if (base.kind === "relative") warnings.push(RELATIVE_BASE_WARNING);
2792
+ return {
2793
+ shareDir,
2794
+ manifest: data.manifest,
2795
+ htmlInjected,
2796
+ shareUrlPath: shareUrlPathFor(base),
2797
+ warnings
2798
+ };
2799
+ }
2800
+ var VALUE_OPTIONS = {
2801
+ "--dir": "dir",
2802
+ "--locales": "locales",
2803
+ "--source": "source",
2804
+ "--base": "base",
2805
+ "--out": "out"
2806
+ };
2807
+ function parseShareArgs(argv) {
2808
+ const args = {
2809
+ positional: [],
2810
+ dir: ".",
2811
+ locales: void 0,
2812
+ source: void 0,
2813
+ base: void 0,
2814
+ out: void 0,
2815
+ exposeRepo: false,
2816
+ inject: true,
2817
+ preview: false,
2818
+ command: []
2819
+ };
2820
+ for (let i = 0; i < argv.length; i++) {
2821
+ const arg = argv[i];
2822
+ if (arg === "--") {
2823
+ args.command = argv.slice(i + 1);
2824
+ break;
2825
+ }
2826
+ if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS, arg)) {
2827
+ const value = argv[i + 1];
2828
+ if (value === void 0 || value.startsWith("--")) {
2829
+ throw new PackError("USAGE", `${arg} needs a value.`);
2830
+ }
2831
+ args[VALUE_OPTIONS[arg]] = value;
2832
+ i++;
2833
+ } else if (arg === "--expose-repo") {
2834
+ args.exposeRepo = true;
2835
+ } else if (arg === "--no-inject") {
2836
+ args.inject = false;
2837
+ } else if (arg === "--preview") {
2838
+ args.preview = true;
2839
+ } else if (arg.startsWith("-") && arg !== "-") {
2840
+ throw new PackError("USAGE", `Unknown option "${arg}".`);
2841
+ } else {
2842
+ args.positional.push(arg);
2843
+ }
2844
+ }
2845
+ return args;
2846
+ }
2847
+ var WINDOWS_NEEDS_QUOTES = /[\s"&|<>^()]/;
2848
+ function quoteWindowsArg(arg) {
2849
+ if (arg !== "" && !WINDOWS_NEEDS_QUOTES.test(arg)) return arg;
2850
+ let out = '"';
2851
+ let backslashes = 0;
2852
+ for (const ch of arg) {
2853
+ if (ch === "\\") {
2854
+ backslashes++;
2855
+ continue;
2856
+ }
2857
+ if (ch === '"') {
2858
+ out += "\\".repeat(backslashes * 2 + 1) + '"';
2859
+ } else {
2860
+ out += "\\".repeat(backslashes) + ch;
2861
+ }
2862
+ backslashes = 0;
2863
+ }
2864
+ return out + "\\".repeat(backslashes * 2) + '"';
2865
+ }
2866
+ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reject2) => {
2867
+ const child = process.platform === "win32" ? (0, import_child_process2.spawn)([command, ...args].map(quoteWindowsArg).join(" "), {
2868
+ cwd,
2869
+ env,
2870
+ stdio: "inherit",
2871
+ shell: true
2872
+ }) : (0, import_child_process2.spawn)(command, args, { cwd, env, stdio: "inherit" });
2873
+ child.on("error", (error) => {
2874
+ if (error.code === "ENOENT") resolve14(127);
2875
+ else reject2(error);
2876
+ });
2877
+ child.on("close", (code) => resolve14(code ?? 1));
2878
+ });
2879
+ var BUILD_USAGE = "Usage: livelocale build [--preview] -- <your build command>";
2880
+ var NEXT_PREVIEW_WARNING = "Next.js pages have no watermarks without --preview; the Studio will open but nothing is editable.";
2881
+ var NEXT_GITIGNORE_WARNING = "Add public/livelocale/ to .gitignore; it is generated on every build.";
2882
+ var FRESH_SLACK_MS = 2e3;
2883
+ function gitignoresShareDir(projectDir) {
2884
+ let content;
2885
+ try {
2886
+ content = (0, import_fs4.readFileSync)(path8.join(projectDir, ".gitignore"), "utf-8");
2887
+ } catch {
2888
+ return false;
2889
+ }
2890
+ return parseGitignore(content).ignores(`public/${SHARE_DIR}`, true);
2891
+ }
2892
+ function freshOutput(projectDir, candidates, startMs) {
2893
+ for (const candidate of candidates) {
2894
+ try {
2895
+ const stat = (0, import_fs4.statSync)(path8.join(projectDir, candidate, "index.html"));
2896
+ if (stat.isFile() && stat.mtimeMs >= startMs - FRESH_SLACK_MS) return candidate;
2897
+ } catch {
2898
+ }
2899
+ }
2900
+ return null;
2901
+ }
2902
+ async function runBuildCommand(options) {
2903
+ const {
2904
+ command,
2905
+ preview = false,
2906
+ outDir,
2907
+ run: run3 = spawnInherit,
2908
+ now = Date.now,
2909
+ ...packOptions
2910
+ } = options;
2911
+ if (command.length === 0) throw new PackError("USAGE", BUILD_USAGE);
2912
+ const projectDir = path8.resolve(options.projectDir);
2913
+ const target = detectBuildTarget(projectDir);
2914
+ const env = options.env ?? process.env;
2915
+ const childEnv = preview ? { ...env, LIVELOCALE: "preview", NEXT_PUBLIC_LIVELOCALE: "preview" } : env;
2916
+ const runBuild = () => run3(command[0], command.slice(1), { cwd: projectDir, env: childEnv });
2917
+ if (target.kind === "next") {
2918
+ const blocking = target.staticExport ? findExportBlockingRoute(projectDir) : null;
2919
+ if (blocking !== null) {
2920
+ throw new PackError(
2921
+ "NEXT_EXPORT_POST_ROUTE",
2922
+ `Next.js static export (output: 'export') cannot build ${blocking}: it exports POST, and export mode only allows GET route handlers. The share link does not need this route. Delete ${blocking} (or move it out of this build) and run again.`
2923
+ );
2924
+ }
2925
+ await import_fs4.promises.mkdir(path8.join(projectDir, "public"), { recursive: true });
2926
+ const pack2 = await packOutput({
2927
+ ...packOptions,
2928
+ projectDir,
2929
+ outDir: "public",
2930
+ injectHtml: false
2931
+ });
2932
+ const warnings = [];
2933
+ if (!preview) warnings.push(NEXT_PREVIEW_WARNING);
2934
+ if (!gitignoresShareDir(projectDir)) warnings.push(NEXT_GITIGNORE_WARNING);
2935
+ const exitCode2 = await runBuild();
2936
+ return { exitCode: exitCode2, pack: pack2, warnings: [...warnings, ...pack2.warnings] };
2937
+ }
2938
+ const startMs = now();
2939
+ const exitCode = await runBuild();
2940
+ if (exitCode !== 0) return { exitCode, pack: null, warnings: [] };
2941
+ const candidates = outputCandidates(target);
2942
+ const output = outDir ?? freshOutput(projectDir, candidates, startMs);
2943
+ if (output === null) {
2944
+ throw new PackError(
2945
+ "OUTPUT_NOT_FOUND",
2946
+ `Could not find the build output (looked for a fresh index.html in ${candidates.map((c) => `${c}/`).join(", ")}). Pass --out <dir>.`
2947
+ );
2948
+ }
2949
+ const pack = await packOutput({ ...packOptions, projectDir, outDir: output, injectHtml: "auto" });
2950
+ return { exitCode, pack, warnings: [...pack.warnings] };
2951
+ }
2952
+
2953
+ // src/codemod/apply/cli.ts
2954
+ var path14 = __toESM(require("path"), 1);
2955
+
2956
+ // src/codemod/apply/link-input.ts
2957
+ var import_child_process3 = require("child_process");
2958
+ var fs6 = __toESM(require("fs"), 1);
2959
+ var path9 = __toESM(require("path"), 1);
2960
+ var MAX_APPLY_INPUT_BYTES = 1048576;
2961
+ var TOO_LARGE_MESSAGE2 = "The input is larger than 1 MB.";
2962
+ var NO_INPUT_MESSAGE = 'Pass the change link, a file that holds it, "-" to read stdin, or --clipboard. (cmd.exe cuts arguments at 8,191 characters; use --clipboard or a file for long links.)';
2963
+ var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
2964
+ var ApplyInputError = class extends Error {
2965
+ constructor(message) {
2966
+ super(message);
2967
+ this.name = "ApplyInputError";
2968
+ }
2969
+ };
2970
+ function checkSize(text) {
2971
+ if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
2972
+ throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2973
+ }
2974
+ return text;
2975
+ }
2976
+ async function readStream(stream) {
2977
+ const chunks = [];
2978
+ let size = 0;
2979
+ for await (const chunk of stream) {
2980
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
2981
+ size += bytes.length;
2982
+ if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2983
+ chunks.push(bytes);
2984
+ }
2985
+ return Buffer.concat(chunks).toString("utf8");
2986
+ }
2987
+ function existingFile(arg, cwd) {
2988
+ try {
2989
+ const resolved = path9.resolve(cwd, arg);
2990
+ return fs6.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
2991
+ } catch {
2992
+ return null;
2993
+ }
2994
+ }
2995
+ async function readApplyInput(arg, options) {
2996
+ if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
2997
+ if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
2998
+ if (arg === "-") return readStream(options.stdin ?? process.stdin);
2999
+ const file = existingFile(arg, options.cwd);
3000
+ if (file === null) return checkSize(arg);
3001
+ if (fs6.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
3002
+ return checkSize(fs6.readFileSync(file, "utf8"));
3003
+ }
3004
+ function clipboardCommands(platform) {
3005
+ if (platform === "win32") {
3006
+ return [
3007
+ ["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
3008
+ ];
3009
+ }
3010
+ if (platform === "darwin") return [["pbpaste", []]];
3011
+ return [
3012
+ ["wl-paste", ["--no-newline"]],
3013
+ ["xclip", ["-selection", "clipboard", "-o"]],
3014
+ ["xsel", ["--clipboard", "--output"]]
3015
+ ];
3016
+ }
3017
+ var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
3018
+ function run(execFileImpl, [command, args]) {
3019
+ return new Promise((resolve14) => {
3020
+ try {
3021
+ execFileImpl(
3022
+ command,
3023
+ args,
3024
+ { encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
3025
+ (error, stdout) => {
3026
+ const code = error?.code;
3027
+ if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve14(TOO_LARGE);
3028
+ else resolve14(error ? null : String(stdout));
3029
+ }
3030
+ );
3031
+ } catch {
3032
+ resolve14(null);
3033
+ }
3034
+ });
3035
+ }
3036
+ async function readClipboard(execFileImpl = import_child_process3.execFile, platform = process.platform) {
3037
+ for (const command of clipboardCommands(platform)) {
3038
+ const text = await run(execFileImpl, command);
3039
+ if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
3040
+ if (text !== null) return checkSize(text);
3041
+ }
3042
+ throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
3043
+ }
3044
+
3045
+ // src/codemod/apply/local.ts
3046
+ var import_child_process5 = require("child_process");
3047
+ var fs9 = __toESM(require("fs"), 1);
3048
+ var os = __toESM(require("os"), 1);
3049
+ var path13 = __toESM(require("path"), 1);
3050
+
3051
+ // src/core/change-link-node.ts
3052
+ var import_zlib = require("zlib");
3053
+ var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
3054
+ var TOO_LARGE_MESSAGE3 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
3055
+ var nodeInflate = async (bytes, maxOutputLength) => {
3056
+ let result;
3057
+ try {
3058
+ result = (0, import_zlib.inflateSync)(bytes, { maxOutputLength, info: true });
3059
+ } catch (err) {
3060
+ if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
3061
+ throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE3);
3062
+ }
3063
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3064
+ }
3065
+ if (result.engine.bytesWritten !== bytes.length) {
3066
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3067
+ }
3068
+ return result.buffer;
3069
+ };
3070
+
3071
+ // src/client/watermark.ts
3072
+ var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
3073
+ function containsWatermark(text) {
3074
+ WATERMARK_RE.lastIndex = 0;
3075
+ const found = WATERMARK_RE.test(text);
3076
+ WATERMARK_RE.lastIndex = 0;
3077
+ return found;
3078
+ }
3079
+
3080
+ // src/core/json-path.ts
3081
+ var MAX_KEY_DEPTH = 30;
3082
+ function setNestedProperty(obj, keyPath, value) {
3083
+ const keys = keyPath.split(".");
3084
+ if (keys.length > MAX_KEY_DEPTH) {
3085
+ throw new Error(
3086
+ `[livelocale] Key depth limit exceeded. Maximum depth is ${MAX_KEY_DEPTH} levels.`
3087
+ );
3088
+ }
3089
+ if (keys.some((k) => DANGEROUS_KEY_SEGMENTS.has(k))) {
3090
+ throw new Error(
3091
+ `[livelocale] Forbidden key segment in path: "${keyPath}". Segment names like __proto__, constructor, and prototype are not allowed.`
3092
+ );
3093
+ }
3094
+ let current = obj;
3095
+ for (let i = 0; i < keys.length; i++) {
3096
+ const key = keys[i];
3097
+ if (i === keys.length - 1) {
3098
+ current[key] = value;
3099
+ return;
3100
+ }
3101
+ if (!(key in current) || current[key] === null || typeof current[key] !== "object" || Array.isArray(current[key])) {
3102
+ current[key] = {};
3103
+ }
3104
+ current = current[key];
3105
+ }
3106
+ }
3107
+ function isPlainObject3(value) {
3108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3109
+ }
3110
+ function hasOwn(obj, key) {
3111
+ return Object.prototype.hasOwnProperty.call(obj, key);
3112
+ }
3113
+ function lookupKey(dict, keyPath) {
3114
+ if (!isPlainObject3(dict)) return { state: "missing" };
3115
+ const segments = keyPath.split(".");
3116
+ const leaf = segments.length - 1;
3117
+ let current = dict;
3118
+ for (let i = 0; i < leaf; i++) {
3119
+ if (hasOwn(current, segments.slice(i).join("."))) return { state: "occupied" };
3120
+ const segment = segments[i];
3121
+ if (!hasOwn(current, segment)) return { state: "missing" };
3122
+ const next = current[segment];
3123
+ if (!isPlainObject3(next)) return { state: "occupied" };
3124
+ current = next;
3125
+ }
3126
+ const key = segments[leaf];
3127
+ if (!hasOwn(current, key)) return { state: "missing" };
3128
+ const value = current[key];
3129
+ return typeof value === "string" ? { state: "string", value } : { state: "occupied" };
3130
+ }
3131
+ function detectJsonIndent(content) {
3132
+ const match = /^([ \t]+)"/m.exec(content);
3133
+ if (!match) return " ";
3134
+ const ws = match[1];
3135
+ return ws.includes(" ") ? " " : ws;
3136
+ }
3137
+
3138
+ // src/core/save-rules.ts
3139
+ var reject = (status, code, error) => ({
3140
+ ok: false,
3141
+ status,
3142
+ code,
3143
+ error
3144
+ });
3145
+ function validateWrite(input, options = {}) {
3146
+ const { locale, key, value } = input;
3147
+ if (!isValidLocaleFormat(locale)) {
3148
+ return reject(400, "INVALID_LOCALE_FORMAT", "Invalid locale string parameter.");
3149
+ }
3150
+ if (!isValidKeyFormat(key)) {
3151
+ return reject(
3152
+ 400,
3153
+ "INVALID_KEY_FORMAT",
3154
+ "Invalid key path. Must be 1-200 characters containing alphanumeric, dots, underscores, or hyphens."
3155
+ );
3156
+ }
3157
+ if (!isSafeKeyPath(key)) {
3158
+ return reject(
3159
+ 400,
3160
+ "INVALID_KEY_FORMAT",
3161
+ "Forbidden key path: segments like __proto__, constructor, or prototype are not allowed."
3162
+ );
3163
+ }
3164
+ if (!isValidValueFormat(value)) {
3165
+ return reject(
3166
+ 400,
3167
+ "INVALID_VALUE_FORMAT",
3168
+ "Invalid value. Must be a string less than 10000 characters."
3169
+ );
3170
+ }
3171
+ const accepted = {
3172
+ ok: true,
3173
+ locale,
3174
+ key,
3175
+ value
3176
+ };
3177
+ if (options.force === true) return accepted;
3178
+ const syntaxError = validateMessageSyntax(accepted.value);
3179
+ if (syntaxError) {
3180
+ return reject(422, "MESSAGE_SYNTAX", `Message syntax: ${syntaxError}.`);
3181
+ }
3182
+ const comparison = comparePlaceholders(options.reference, accepted.value);
3183
+ if (!comparison.ok) {
3184
+ const parts = [];
3185
+ if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
3186
+ if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
3187
+ return {
3188
+ ...reject(
3189
+ 422,
3190
+ "PLACEHOLDER_MISMATCH",
3191
+ `Placeholders differ from the reference: ${parts.join("; ")}.`
3192
+ ),
3193
+ details: { missing: comparison.missing, extra: comparison.extra }
3194
+ };
3195
+ }
3196
+ return accepted;
3197
+ }
3198
+ function sourceReference(key, ctx) {
3199
+ const found = lookupKey(ctx.source, key);
3200
+ return found.state === "string" ? found.value : null;
3201
+ }
3202
+ function validateEdit(edit, ctx, options) {
3203
+ const { locale, key, value } = edit;
3204
+ const input = { locale, key, value };
3205
+ const format = validateWrite(input, { force: true });
3206
+ if (!format.ok) return format;
3207
+ const dict = locale === ctx.sourceLocale ? ctx.source : ctx.target ?? {};
3208
+ if (lookupKey(dict, key).state === "occupied") {
3209
+ return reject(
3210
+ 409,
3211
+ "KEY_NOT_A_STRING",
3212
+ `Cannot save "${key}" in ${locale}: it would replace translations that are not a single text.`
3213
+ );
3214
+ }
3215
+ return validateWrite(input, {
3216
+ reference: sourceReference(key, ctx),
3217
+ force: options?.force === true
3218
+ });
3219
+ }
3220
+ var RISK_FLAG_MESSAGES = {
3221
+ markup: "Adds markup that the source text does not have.",
3222
+ "javascript-url": "Adds a javascript:, vbscript: or data: link.",
3223
+ "new-url": "Adds a web address that the source text does not have.",
3224
+ "bidi-control": "Contains invisible text-direction control characters."
3225
+ };
3226
+ var TAG_PATTERN = /<\/?([A-Za-z][\w-]*)\b[^<>]*>/g;
3227
+ var SCRIPT_LINK_PATTERNS = [/\b(?:javascript|vbscript)\s*:/i, /\bdata:[a-z]+\/[\w.+-]+/i];
3228
+ var URL_PATTERN = /\b(?:https?:\/\/|www\.)[^\s<>"')]+/gi;
3229
+ var BIDI_CONTROL_PATTERN = /[\u202A-\u202E\u2066-\u2069]/g;
3230
+ var tagNames = (text) => Array.from(text.matchAll(TAG_PATTERN), (m) => m[1].toLowerCase());
3231
+ var allMatches = (pattern, text) => text.match(pattern) ?? [];
3232
+ var hasNew = (known, found) => {
3233
+ const set = new Set(known);
3234
+ return found.some((item) => !set.has(item));
3235
+ };
3236
+ function detectRiskFlags(reference, value) {
3237
+ const ref = reference ?? "";
3238
+ const flags = [];
3239
+ if (hasNew(tagNames(ref), tagNames(value))) flags.push("markup");
3240
+ if (SCRIPT_LINK_PATTERNS.some((pattern) => pattern.test(value) && !pattern.test(ref))) {
3241
+ flags.push("javascript-url");
3242
+ }
3243
+ if (hasNew(allMatches(URL_PATTERN, ref), allMatches(URL_PATTERN, value))) flags.push("new-url");
3244
+ if (hasNew(allMatches(BIDI_CONTROL_PATTERN, ref), allMatches(BIDI_CONTROL_PATTERN, value))) {
3245
+ flags.push("bidi-control");
3246
+ }
3247
+ return flags;
3248
+ }
3249
+
3250
+ // src/core/apply-plan.ts
3251
+ var FLAG_MESSAGES = {
3252
+ ...RISK_FLAG_MESSAGES,
3253
+ forced: "Saved with \u201CSave anyway\u201D.",
3254
+ "new-key": "Adds a key the source locale does not have."
3255
+ };
3256
+ function inExistingNamespace(dict, key) {
3257
+ const dot = key.indexOf(".");
3258
+ if (dot === -1) return false;
3259
+ const namespace = key.slice(0, dot);
3260
+ if (!Object.prototype.hasOwnProperty.call(dict, namespace)) return false;
3261
+ const value = dict[namespace];
3262
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3263
+ }
3264
+ function planEdit(edit, input, source) {
3265
+ const { locale, key, from, to, force } = edit;
3266
+ const base = { locale, key, from, to, force };
3267
+ const invalid2 = (code, message) => ({
3268
+ ...base,
3269
+ status: "invalid",
3270
+ code,
3271
+ message,
3272
+ flags: []
3273
+ });
3274
+ const target = input.targets.get(locale);
3275
+ if (!target) return invalid2("UNKNOWN_LOCALE", `No "${locale}" locale file on the target.`);
3276
+ if (target.layout === "namespaced" && !inExistingNamespace(target.dict, key)) {
3277
+ return invalid2(
3278
+ "UNKNOWN_NAMESPACE",
3279
+ `"${key}" is not in an existing namespace file of ${locale}.`
3280
+ );
3281
+ }
3282
+ if (containsWatermark(to)) {
3283
+ return invalid2("CONTAINS_WATERMARK", "The new text contains LiveLocale watermark characters.");
3284
+ }
3285
+ const ctx = { sourceLocale: input.sourceLocale, source, target: target.dict };
3286
+ const reference = sourceReference(key, ctx);
3287
+ const flags = [];
3288
+ const planned = (outcome) => ({
3289
+ ...base,
3290
+ ...outcome,
3291
+ flags: [...flags, ...detectRiskFlags(reference ?? from ?? void 0, to)]
3292
+ });
3293
+ const found = lookupKey(target.dict, key);
3294
+ if (found.state === "string" && found.value === to) {
3295
+ return planned({ status: "already", current: found.value });
3296
+ }
3297
+ const verdict = validateEdit({ locale, key, value: to }, ctx, { force: false });
3298
+ if (!verdict.ok) {
3299
+ if (verdict.status !== 422 || !force) return invalid2(verdict.code, verdict.error);
3300
+ if (!input.allowForce) {
3301
+ return planned({ status: "needs-force", code: verdict.code, message: verdict.error });
3302
+ }
3303
+ flags.push("forced");
3304
+ }
3305
+ const current = found.state === "string" ? found.value : void 0;
3306
+ if (current === void 0) {
3307
+ if (from !== null) {
3308
+ return planned({
3309
+ status: "conflict",
3310
+ code: "REMOVED_ON_TARGET",
3311
+ message: "Removed on the target since the link was made."
3312
+ });
3313
+ }
3314
+ if (reference === null) flags.push("new-key");
3315
+ return planned({ status: "apply" });
3316
+ }
3317
+ if (current === from) return planned({ status: "apply" });
3318
+ return planned({
3319
+ status: "conflict",
3320
+ code: "CHANGED_ON_TARGET",
3321
+ message: "Changed on the target since the link was made.",
3322
+ current
3323
+ });
3324
+ }
3325
+ function planApply(input) {
3326
+ const source = input.targets.get(input.sourceLocale);
3327
+ if (!source) {
3328
+ throw new Error(`Source locale "${input.sourceLocale}" has no locale file on the target.`);
3329
+ }
3330
+ const edits = input.edits.map((edit) => planEdit(edit, input, source.dict));
3331
+ const counts = {
3332
+ apply: 0,
3333
+ already: 0,
3334
+ conflict: 0,
3335
+ invalid: 0,
3336
+ "needs-force": 0
3337
+ };
3338
+ for (const edit of edits) counts[edit.status] += 1;
3339
+ return { edits, counts };
3340
+ }
3341
+
3342
+ // src/server/file-mutator.ts
3343
+ var import_fs5 = require("fs");
3344
+ var path11 = __toESM(require("path"), 1);
3345
+
3346
+ // src/core/safe-path.ts
3347
+ var path10 = __toESM(require("path"), 1);
3348
+ function resolveSafeLocalePath(basePath, locale) {
3349
+ if (!isSafeLocaleSegment(locale)) {
3350
+ throw new Error("Path traversal attempt");
3351
+ }
3352
+ const resolvedBasePath = path10.resolve(basePath);
3353
+ const filePath = path10.resolve(resolvedBasePath, `${locale}.json`);
3354
+ if (!filePath.startsWith(resolvedBasePath)) {
3355
+ throw new Error("Path traversal attempt");
3356
+ }
3357
+ return filePath;
3358
+ }
3359
+
3360
+ // src/server/file-mutator.ts
3361
+ var FileMutex = class {
3362
+ queue = Promise.resolve();
3363
+ async acquire(fn) {
3364
+ const next = this.queue.then(fn);
3365
+ this.queue = next.then(
3366
+ () => {
3367
+ },
3368
+ () => {
3369
+ }
3370
+ );
3371
+ return next;
3372
+ }
3373
+ };
3374
+ var FileMutator = class {
3375
+ fileLocks = /* @__PURE__ */ new Map();
3376
+ requireEnabled;
3377
+ quiet;
3378
+ scanner;
3379
+ constructor(options = {}) {
3380
+ this.requireEnabled = options.requireEnabled ?? true;
3381
+ this.quiet = options.quiet ?? false;
3382
+ this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
3383
+ }
3384
+ assertEnabled() {
3385
+ if (!this.requireEnabled) return;
3386
+ const reason = getDisabledReason();
3387
+ if (reason !== null) {
3388
+ throw new Error(
3389
+ `[livelocale] FileMutator can only be executed in development mode (${reason}).`
3390
+ );
3391
+ }
3392
+ }
3393
+ /**
3394
+ * Helper to serialize execution per file path
3395
+ */
3396
+ async withLock(filePath, fn) {
3397
+ let lock = this.fileLocks.get(filePath);
3398
+ if (!lock) {
3399
+ lock = new FileMutex();
3400
+ this.fileLocks.set(filePath, lock);
3401
+ }
3402
+ return lock.acquire(fn);
3403
+ }
3404
+ /**
3405
+ * Updates a locale JSON file on disk atomically and safely.
3406
+ * Enforces development mode checks, path traversal mitigation, key depth restrictions,
3407
+ * structural JSON validation, and concurrency serialization.
3408
+ */
3409
+ async updateLocaleKey(basePath, locale, keyPath, newValue) {
3410
+ this.assertEnabled();
3411
+ if (!isSafeLocaleSegment(locale)) {
3412
+ throw new Error(
3413
+ `[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
3414
+ );
3415
+ }
3416
+ const resolvedBasePath = path11.resolve(basePath);
3417
+ const localeDir = path11.resolve(resolvedBasePath, locale);
3418
+ let filePath = "";
3419
+ let mutationKeyPath = keyPath;
3420
+ let isNamespaceDir = false;
3421
+ try {
3422
+ const stats = await import_fs5.promises.stat(localeDir);
3423
+ if (stats.isDirectory()) {
3424
+ isNamespaceDir = true;
3425
+ }
3426
+ } catch {
3427
+ }
3428
+ if (isNamespaceDir) {
3429
+ const firstDot = keyPath.indexOf(".");
3430
+ if (firstDot !== -1) {
3431
+ const namespace = keyPath.substring(0, firstDot);
3432
+ if (DANGEROUS_KEY_SEGMENTS.has(namespace)) {
3433
+ throw new Error(
3434
+ `[livelocale] Forbidden namespace segment in path: "${namespace}". Segment names like __proto__, constructor, and prototype are not allowed.`
3435
+ );
3436
+ }
3437
+ mutationKeyPath = keyPath.substring(firstDot + 1);
3438
+ filePath = path11.resolve(localeDir, `${namespace}.json`);
3439
+ } else {
3440
+ filePath = path11.resolve(localeDir, `${keyPath}.json`);
3441
+ mutationKeyPath = "";
3442
+ }
3443
+ } else {
3444
+ filePath = path11.resolve(resolvedBasePath, `${locale}.json`);
3445
+ }
3446
+ if (!filePath.startsWith(resolvedBasePath)) {
3447
+ throw new Error(
3448
+ `[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
3449
+ );
3450
+ }
3451
+ await this.withLock(filePath, async () => {
3452
+ let currentData = {};
3453
+ let indent = " ";
3454
+ let trailingNewline = "\n";
3455
+ try {
3456
+ const fileContent = await import_fs5.promises.readFile(filePath, "utf-8");
3457
+ indent = detectJsonIndent(fileContent);
3458
+ trailingNewline = fileContent.endsWith("\n") ? "\n" : "";
3459
+ try {
3460
+ const parsed = JSON.parse(fileContent);
3461
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3462
+ throw new Error("Root must be an object");
3463
+ }
3464
+ currentData = parsed;
3465
+ } catch (parseError) {
3466
+ throw new Error(
3467
+ `[livelocale] Invalid locale file: root must be an object. Code: INVALID_LOCALE. Original error: ${parseError.message}`
3468
+ );
3469
+ }
3470
+ } catch (err) {
3471
+ if (err.code !== "ENOENT") {
3472
+ throw new Error(
3473
+ `[livelocale] Failed to read locale file at ${filePath}. Operation: read. Original error: ${err.message}`
3474
+ );
3475
+ }
3476
+ }
3477
+ if (mutationKeyPath) {
3478
+ setNestedProperty(currentData, mutationKeyPath, newValue);
3479
+ } else {
3480
+ throw new Error(
3481
+ "[livelocale] Invalid namespace key path: must contain at least one nested key."
3482
+ );
3483
+ }
3484
+ let stringified;
3485
+ try {
3486
+ stringified = JSON.stringify(currentData, null, indent);
3487
+ JSON.parse(stringified);
3488
+ stringified += trailingNewline;
3489
+ } catch (err) {
3490
+ throw new Error(
3491
+ `[livelocale] Produced JSON is invalid or has circular references. Original error: ${err.message}`
3492
+ );
3493
+ }
3494
+ const tempPath = `${filePath}.tmp`;
3495
+ try {
3496
+ await import_fs5.promises.writeFile(tempPath, stringified, "utf-8");
3497
+ } catch (err) {
3498
+ throw new Error(
3499
+ `[livelocale] Failed to write temp file at ${tempPath}. Operation: write. Original error: ${err.message}`
3500
+ );
3501
+ }
3502
+ try {
3503
+ await import_fs5.promises.rename(tempPath, filePath);
3504
+ } catch (err) {
3505
+ try {
3506
+ await import_fs5.promises.unlink(tempPath);
3507
+ } catch {
3508
+ }
3509
+ throw new Error(
3510
+ `[livelocale] Failed to rename temp file to target at ${filePath}. Operation: rename. Original error: ${err.message}`
3511
+ );
3512
+ }
3513
+ if (!this.quiet) {
3514
+ console.log(`[livelocale] Updated key '${keyPath}' in locale '${locale}'`);
3515
+ }
3516
+ });
3517
+ }
3518
+ /**
3519
+ * Returns the full dictionary for a locale: a flat `{locale}.json`, or every
3520
+ * `*.json` inside the `{locale}/` folder merged under its namespace.
3521
+ */
3522
+ async readLocale(basePath, locale) {
3523
+ this.assertEnabled();
3524
+ const localePath = path11.resolve(basePath, locale);
3525
+ let isDir = false;
3526
+ try {
3527
+ isDir = (await import_fs5.promises.stat(localePath)).isDirectory();
3528
+ } catch {
3529
+ }
3530
+ if (isDir) {
3531
+ const merged = {};
3532
+ for (const file of await import_fs5.promises.readdir(localePath)) {
3533
+ if (!file.endsWith(".json")) continue;
3534
+ try {
3535
+ merged[path11.basename(file, ".json")] = JSON.parse(
3536
+ await import_fs5.promises.readFile(path11.join(localePath, file), "utf-8")
3537
+ );
3538
+ } catch {
3539
+ }
3540
+ }
3541
+ return merged;
3542
+ }
3543
+ const filePath = resolveSafeLocalePath(basePath, locale);
3544
+ return JSON.parse(await import_fs5.promises.readFile(filePath, "utf-8"));
3545
+ }
3546
+ /**
3547
+ * Lists all locale codes dynamically scanned from the locales directory.
3548
+ */
3549
+ async listLocales(basePath) {
3550
+ this.assertEnabled();
3551
+ const resolvedBasePath = path11.resolve(basePath);
3552
+ const items = await import_fs5.promises.readdir(resolvedBasePath, { withFileTypes: true });
3553
+ const localesSet = /* @__PURE__ */ new Set();
3554
+ for (const item of items) {
3555
+ if (item.isDirectory()) {
3556
+ if (LOCALE_RE.test(item.name)) {
3557
+ localesSet.add(item.name);
3558
+ }
3559
+ } else if (item.isFile() && item.name.endsWith(".json")) {
3560
+ const name = path11.basename(item.name, ".json");
3561
+ if (LOCALE_RE.test(name)) {
3562
+ localesSet.add(name);
3563
+ }
3564
+ }
3565
+ }
3566
+ return Array.from(localesSet).sort();
3567
+ }
3568
+ /**
3569
+ * Adds a new locale directory/file based on current repository structure.
3570
+ */
3571
+ async addLocale(basePath, locale) {
3572
+ this.assertEnabled();
3573
+ if (!isSafeLocaleSegment(locale)) {
3574
+ throw new Error(
3575
+ `[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
3576
+ );
3577
+ }
3578
+ if (!LOCALE_RE.test(locale)) {
3579
+ throw new Error(`[livelocale] Invalid locale format: "${locale}"`);
3580
+ }
3581
+ const resolvedBasePath = path11.resolve(basePath);
3582
+ const localePath = path11.resolve(resolvedBasePath, locale);
3583
+ if (!localePath.startsWith(resolvedBasePath)) {
3584
+ throw new Error(
3585
+ `[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
3586
+ );
3587
+ }
3588
+ let exists = false;
3589
+ try {
3590
+ await import_fs5.promises.stat(localePath);
3591
+ exists = true;
3592
+ } catch {
3593
+ }
3594
+ try {
3595
+ await import_fs5.promises.stat(`${localePath}.json`);
3596
+ exists = true;
3597
+ } catch {
3598
+ }
3599
+ if (exists) {
3600
+ throw new Error(`[livelocale] Locale "${locale}" already exists.`);
3601
+ }
3602
+ const items = await import_fs5.promises.readdir(resolvedBasePath, { withFileTypes: true });
3603
+ let referenceDir = null;
3604
+ for (const item of items) {
3605
+ if (item.isDirectory() && LOCALE_RE.test(item.name)) {
3606
+ referenceDir = item.name;
3607
+ break;
3608
+ }
3609
+ }
3610
+ if (referenceDir) {
3611
+ await import_fs5.promises.mkdir(localePath, { recursive: true });
3612
+ const refPath = path11.resolve(resolvedBasePath, referenceDir);
3613
+ const files = await import_fs5.promises.readdir(refPath);
3614
+ for (const file of files) {
3615
+ if (file.endsWith(".json")) {
3616
+ await import_fs5.promises.writeFile(path11.join(localePath, file), "{}", "utf-8");
3617
+ }
3618
+ }
3619
+ } else {
3620
+ await import_fs5.promises.writeFile(`${localePath}.json`, "{}", "utf-8");
3621
+ }
3622
+ console.log(`[livelocale] Added locale '${locale}'`);
3623
+ }
3624
+ /**
3625
+ * Renames an existing locale (directory or file) to a new name.
3626
+ */
3627
+ async renameLocale(basePath, oldLocale, newLocale) {
3628
+ this.assertEnabled();
3629
+ if (!isSafeLocaleSegment(oldLocale) || !isSafeLocaleSegment(newLocale)) {
3630
+ throw new Error("[livelocale] Path traversal attempt detected");
3631
+ }
3632
+ if (!LOCALE_RE.test(oldLocale) || !LOCALE_RE.test(newLocale)) {
3633
+ throw new Error(`[livelocale] Invalid locale format`);
3634
+ }
3635
+ const resolvedBasePath = path11.resolve(basePath);
3636
+ const oldPath = path11.resolve(resolvedBasePath, oldLocale);
3637
+ const newPath = path11.resolve(resolvedBasePath, newLocale);
3638
+ if (!oldPath.startsWith(resolvedBasePath) || !newPath.startsWith(resolvedBasePath)) {
3639
+ throw new Error("[livelocale] Path traversal attempt");
3640
+ }
3641
+ let isDir = false;
3642
+ let exists = false;
3643
+ try {
3644
+ const stats = await import_fs5.promises.stat(oldPath);
3645
+ isDir = stats.isDirectory();
3646
+ exists = true;
3647
+ } catch {
3648
+ }
3649
+ let isFile = false;
3650
+ if (!exists) {
3651
+ try {
3652
+ await import_fs5.promises.stat(`${oldPath}.json`);
3653
+ isFile = true;
3654
+ exists = true;
3655
+ } catch {
3656
+ }
3657
+ }
3658
+ if (!exists) {
3659
+ throw new Error(`[livelocale] Locale "${oldLocale}" does not exist.`);
3660
+ }
3661
+ let newExists = false;
3662
+ try {
3663
+ await import_fs5.promises.stat(newPath);
3664
+ newExists = true;
3665
+ } catch {
3666
+ }
3667
+ try {
3668
+ await import_fs5.promises.stat(`${newPath}.json`);
3669
+ newExists = true;
3670
+ } catch {
3671
+ }
3672
+ if (newExists) {
3673
+ throw new Error(`[livelocale] Target locale "${newLocale}" already exists.`);
3674
+ }
3675
+ if (isDir) {
3676
+ await import_fs5.promises.rename(oldPath, newPath);
3677
+ } else if (isFile) {
3678
+ await import_fs5.promises.rename(`${oldPath}.json`, `${newPath}.json`);
3679
+ }
3680
+ console.log(`[livelocale] Renamed locale '${oldLocale}' to '${newLocale}'`);
3681
+ }
3682
+ /**
3683
+ * Deletes an existing locale (directory or file).
3684
+ */
3685
+ async deleteLocale(basePath, locale) {
3686
+ this.assertEnabled();
3687
+ if (!isSafeLocaleSegment(locale)) {
3688
+ throw new Error("[livelocale] Path traversal attempt detected");
3689
+ }
3690
+ if (!LOCALE_RE.test(locale)) {
3691
+ throw new Error(`[livelocale] Invalid locale format`);
3692
+ }
3693
+ const resolvedBasePath = path11.resolve(basePath);
3694
+ const localePath = path11.resolve(resolvedBasePath, locale);
3695
+ if (!localePath.startsWith(resolvedBasePath)) {
3696
+ throw new Error("[livelocale] Path traversal attempt");
3697
+ }
3698
+ let isDir = false;
3699
+ let exists = false;
3700
+ try {
3701
+ const stats = await import_fs5.promises.stat(localePath);
3702
+ isDir = stats.isDirectory();
3703
+ exists = true;
3704
+ } catch {
3705
+ }
3706
+ let isFile = false;
3707
+ if (!exists) {
3708
+ try {
3709
+ await import_fs5.promises.stat(`${localePath}.json`);
3710
+ isFile = true;
3711
+ exists = true;
3712
+ } catch {
3713
+ }
3714
+ }
3715
+ if (!exists) {
3716
+ throw new Error(`[livelocale] Locale "${locale}" does not exist.`);
3717
+ }
3718
+ if (isDir) {
3719
+ await import_fs5.promises.rm(localePath, { recursive: true, force: true });
3720
+ } else if (isFile) {
3721
+ await import_fs5.promises.unlink(`${localePath}.json`);
3722
+ }
3723
+ console.log(`[livelocale] Deleted locale '${locale}'`);
3724
+ }
3725
+ /**
3726
+ * Scans codebase directory recursively for references to translation keys.
3727
+ * Delegates to the composed KeyScanner (SRP).
3728
+ */
3729
+ async scanCodebaseKeys(basePath) {
3730
+ return this.scanner.scanCodebaseKeys(basePath);
3731
+ }
3732
+ /**
3733
+ * Scans all locale files/folders to compile the set of all keys currently in locales.
3734
+ */
3735
+ async scanLocalesKeys(basePath) {
3736
+ return this.scanner.scanLocalesKeys(basePath, this.listLocales.bind(this));
3737
+ }
3738
+ /**
3739
+ * Returns merged lists of keys from codebase and existing locale files.
3740
+ */
3741
+ async getKeysMetadata(basePath) {
3742
+ return this.scanner.getKeysMetadata(basePath, this.listLocales.bind(this));
3743
+ }
3744
+ };
3745
+
3746
+ // src/server/git-client.ts
3747
+ var import_child_process4 = require("child_process");
3748
+ var import_util = require("util");
3749
+ var fs8 = __toESM(require("fs"), 1);
3750
+ var path12 = __toESM(require("path"), 1);
3751
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3752
+ var MAX_DIFF_BYTES = 2e5;
3753
+ function mapStatus(xy) {
3754
+ if (xy === "??") return "added";
3755
+ const code = xy.trim()[0] ?? "";
3756
+ switch (code) {
3757
+ case "M":
3758
+ return "modified";
3759
+ case "A":
3760
+ return "added";
3761
+ case "D":
3762
+ return "deleted";
3763
+ case "R":
3764
+ return "renamed";
3765
+ default:
3766
+ return "modified";
3767
+ }
3768
+ }
3769
+ var GitClient = class {
3770
+ async git(cwd, args) {
3771
+ const { stdout } = await execFileAsync("git", args, {
3772
+ cwd,
3773
+ maxBuffer: 16 * 1024 * 1024,
3774
+ windowsHide: true,
3775
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
3776
+ });
3777
+ return stdout;
3778
+ }
3779
+ /** Resolves the repository root and the locales path relative to it (forward slashes). */
3780
+ async locate(localesPath) {
3781
+ const absolute = path12.resolve(localesPath);
3782
+ const cwd = fs8.existsSync(absolute) ? absolute : path12.dirname(absolute);
3783
+ if (!fs8.existsSync(cwd)) return null;
3784
+ try {
3785
+ const root = (await this.git(cwd, ["rev-parse", "--show-toplevel"])).trim();
3786
+ if (!root) return null;
3787
+ const rel = path12.relative(root, absolute).split(path12.sep).join("/");
3788
+ if (rel.startsWith("..")) return null;
3789
+ return { root: path12.resolve(root), rel: rel || ".", absolute };
3790
+ } catch {
3791
+ return null;
3792
+ }
3793
+ }
3794
+ async status(localesPath) {
3795
+ const loc = await this.locate(localesPath);
3796
+ if (!loc) return { isRepo: false, branch: null, changes: [] };
3797
+ let branch = null;
3798
+ try {
3799
+ branch = (await this.git(loc.root, ["rev-parse", "--abbrev-ref", "HEAD"])).trim() || null;
3800
+ } catch {
3801
+ branch = null;
3802
+ }
3803
+ const changes = [];
3804
+ try {
3805
+ const out = await this.git(loc.root, [
3806
+ "status",
3807
+ "--porcelain=v1",
3808
+ "--untracked-files=all",
3809
+ "--",
3810
+ loc.rel
3811
+ ]);
3812
+ for (const line of out.split("\n")) {
3813
+ if (line.length < 4) continue;
3814
+ const xy = line.slice(0, 2);
3815
+ let file = line.slice(3).trim();
3816
+ const arrow = file.indexOf(" -> ");
3817
+ if (arrow !== -1) file = file.slice(arrow + 4);
3818
+ if (file.startsWith('"') && file.endsWith('"')) file = file.slice(1, -1);
3819
+ changes.push({ path: file, status: mapStatus(xy) });
3820
+ }
3821
+ } catch {
3822
+ }
3823
+ return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
3824
+ }
3825
+ async diff(localesPath) {
3826
+ const loc = await this.locate(localesPath);
3827
+ if (!loc) return "";
3828
+ let out = "";
3829
+ try {
3830
+ out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
3831
+ } catch {
3832
+ }
3833
+ try {
3834
+ const untracked = await this.git(loc.root, [
3835
+ "ls-files",
3836
+ "--others",
3837
+ "--exclude-standard",
3838
+ "--",
3839
+ loc.rel
3840
+ ]);
3841
+ for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
3842
+ const full = path12.join(loc.root, file);
3843
+ let content = "";
3844
+ try {
3845
+ content = fs8.readFileSync(full, "utf-8");
3846
+ } catch {
3847
+ continue;
3848
+ }
3849
+ const lines = content.split("\n");
3850
+ if (lines[lines.length - 1] === "") lines.pop();
3851
+ out += `diff --git a/${file} b/${file}
3852
+ new file mode 100644
3853
+ --- /dev/null
3854
+ +++ b/${file}
3855
+ @@ -0,0 +1,${lines.length} @@
3856
+ ` + lines.map((l) => `+${l}`).join("\n") + "\n";
3857
+ }
3858
+ } catch {
3859
+ }
3860
+ if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
3861
+ out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
3862
+ }
3863
+ return out;
3864
+ }
3865
+ async commit(localesPath, message) {
3866
+ const loc = await this.locate(localesPath);
3867
+ if (!loc) throw new Error("Not a git repository.");
3868
+ const status = await this.status(localesPath);
3869
+ if (status.changes.length === 0) {
3870
+ throw new Error(`No changes to commit under ${loc.rel}.`);
3871
+ }
3872
+ await this.git(loc.root, ["add", "-A", "--", loc.rel]);
3873
+ await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
3874
+ const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
3875
+ return { hash };
3876
+ }
3877
+ async discard(localesPath) {
3878
+ const loc = await this.locate(localesPath);
3879
+ if (!loc) throw new Error("Not a git repository.");
3880
+ try {
3881
+ await this.git(loc.root, ["checkout", "--", loc.rel]);
3882
+ } catch {
3883
+ }
3884
+ await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
3885
+ }
3886
+ };
3887
+
3888
+ // src/codemod/apply/report.ts
3889
+ var DEFAULT_SPAN_LENGTH = 120;
3890
+ var DEFAULT_TABLE_ROWS = 100;
3891
+ var MAX_BODY_CHARS = 6e4;
3892
+ var MAX_NOTE_CHARS2 = 2e3;
3893
+ var MAX_NAME_CHARS = 64;
3894
+ var MAX_CHECK_LINES = 50;
3895
+ var MAX_CHECK_SUMMARY_CHARS = 1e4;
3896
+ var BODY_MARGIN = 200;
3897
+ var STATUS_LABELS = {
3898
+ apply: "applied",
3899
+ already: "already done",
3900
+ conflict: "conflict",
3901
+ "needs-force": "needs --force",
3902
+ invalid: "rejected"
3903
+ };
3904
+ var BIDI_CONTROL_RE = /[‪-‮⁦-⁩]/g;
3905
+ var NEWLINE_RE = /\r\n|\r|\n/g;
3906
+ var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
3907
+ var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
3908
+ function cut(text, max) {
3909
+ if (text.length <= max) return text;
3910
+ let out = text.slice(0, max);
3911
+ const last = out.charCodeAt(out.length - 1);
3912
+ if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
3913
+ return out;
3914
+ }
3915
+ function cutLines(text, max) {
3916
+ if (text.length <= max) return text;
3917
+ const kept = [];
3918
+ let length = 0;
3919
+ for (const line of text.split("\n")) {
3920
+ const added = (kept.length > 0 ? 1 : 0) + line.length;
3921
+ if (length + added + 2 > max) break;
3922
+ kept.push(line);
3923
+ length += added;
3924
+ }
3925
+ return [...kept, "\u2026"].join("\n");
3926
+ }
3927
+ function longestBacktickRun(text) {
3928
+ let longest = 0;
3929
+ for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
3930
+ return longest;
3931
+ }
3932
+ function codePointLabel(ch) {
3933
+ return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
3934
+ }
3935
+ function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
3936
+ const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
3937
+ const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
3938
+ if (content === "") return "` `";
3939
+ const fence = "`".repeat(longestBacktickRun(content) + 1);
3940
+ const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
3941
+ return `${fence}${pad}${content}${pad}${fence}`;
3942
+ }
3943
+ function sanitizeName(name) {
3944
+ if (name === void 0) return void 0;
3945
+ const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
3946
+ const result = cut(cleaned, MAX_NAME_CHARS).trim();
3947
+ return result === "" ? void 0 : result;
3948
+ }
3949
+ var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
3950
+ function summarizeCheck(report, edited) {
3951
+ const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
3952
+ const mine = report.issues.filter(
3953
+ (issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
3954
+ );
3955
+ if (mine.length === 0) return NO_CHECK_PROBLEMS;
3956
+ const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
3957
+ for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
3958
+ lines.push(
3959
+ `- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
3960
+ );
3961
+ }
3962
+ if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
3963
+ const others = report.issues.length - mine.length;
3964
+ if (others > 0) {
3965
+ lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
3966
+ }
3967
+ return lines.join("\n");
3968
+ }
3969
+ function checksCell(edit) {
3970
+ const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
3971
+ if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
3972
+ parts.push(codeSpan(edit.message));
3973
+ }
3974
+ return parts.join("; ");
3975
+ }
3976
+ function renderRow(edit) {
3977
+ const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
3978
+ const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
3979
+ const cells = [
3980
+ STATUS_LABELS[edit.status],
3981
+ codeSpan(edit.locale),
3982
+ codeSpan(edit.key),
3983
+ before,
3984
+ after,
3985
+ checksCell(edit)
3986
+ ];
3987
+ return `| ${cells.join(" | ")} |`;
3988
+ }
3989
+ function renderTable(plan, limit, maxChars) {
3990
+ const lines = [
3991
+ "| Status | Locale | Key | Before | After | Checks |",
3992
+ "| --- | --- | --- | --- | --- | --- |"
3993
+ ];
3994
+ const moreLine = (n) => `
3995
+
3996
+ \u2026and ${n} more`;
3997
+ let length = lines.join("\n").length + moreLine(plan.edits.length).length;
3998
+ let shown = 0;
3999
+ for (const edit of plan.edits) {
4000
+ if (shown >= limit) break;
4001
+ const row = renderRow(edit);
4002
+ if (length + row.length + 1 > maxChars) break;
4003
+ lines.push(row);
4004
+ length += row.length + 1;
4005
+ shown += 1;
4006
+ }
4007
+ const rest = plan.edits.length - shown;
4008
+ return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
4009
+ }
4010
+ function fencedNote(note) {
4011
+ const text = cut(note, MAX_NOTE_CHARS2);
4012
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
4013
+ return `${fence}text
4014
+ ${text}
4015
+ ${fence}`;
4016
+ }
4017
+ function closingLine(ctx) {
4018
+ if (ctx.issueNumber === void 0) return void 0;
4019
+ if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
4020
+ return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
4021
+ }
4022
+ function renderPullRequestBody(ctx) {
4023
+ const head = ["Applies translations sent with a LiveLocale change link."];
4024
+ if (ctx.by?.trim()) head.push(`Translated by ${codeSpan(ctx.by)}`);
4025
+ if (ctx.target.reason === "not-allowed" && ctx.target.requested !== void 0) {
4026
+ head.push(
4027
+ `The link was made from ${codeSpan(ctx.target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(ctx.target.branch)}.`
4028
+ );
4029
+ }
4030
+ if (ctx.note?.trim()) head.push(fencedNote(ctx.note));
4031
+ const tail = [];
4032
+ if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
4033
+ if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
4034
+ const closing = closingLine(ctx);
4035
+ if (closing) tail.push(closing);
4036
+ const fixed = [...head, ...tail].join("\n\n").length;
4037
+ const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
4038
+ return [...head, table, ...tail].join("\n\n");
4039
+ }
4040
+ function renderCommitMessage(input) {
4041
+ let subject = "i18n: apply LiveLocale changes";
4042
+ if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
4043
+ else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
4044
+ const applied = input.plan.edits.filter((edit) => edit.status === "apply");
4045
+ const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
4046
+ const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
4047
+ const name = sanitizeName(input.by);
4048
+ if (name) parts.push(`Translated-by: ${name}`);
4049
+ return parts.join("\n\n");
4050
+ }
4051
+ function renderTerminalSummary(plan) {
4052
+ const c = plan.counts;
4053
+ const lines = [
4054
+ `Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
4055
+ ];
4056
+ for (const edit of plan.edits) {
4057
+ lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
4058
+ }
4059
+ return lines.join("\n");
4060
+ }
4061
+
4062
+ // src/codemod/apply/local.ts
4063
+ function isPlainObject4(value) {
4064
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4065
+ }
4066
+ async function readTarget(mutator, localesDir, locale) {
4067
+ if (!isValidLocaleFormat(locale)) return null;
4068
+ const dir = path13.join(localesDir, locale);
4069
+ let layout;
4070
+ if (fs9.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
4071
+ layout = "namespaced";
4072
+ } else if (fs9.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
4073
+ layout = "flat";
4074
+ } else {
4075
+ return null;
4076
+ }
4077
+ const dict = await mutator.readLocale(localesDir, locale);
4078
+ if (!isPlainObject4(dict)) {
4079
+ throw new Error(`The "${locale}" locale file is not a JSON object.`);
4080
+ }
4081
+ return { layout, dict };
4082
+ }
4083
+ function fileFor(localesPath, layout, locale, key) {
4084
+ const rel = layout === "flat" ? path13.join(localesPath, `${locale}.json`) : path13.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
4085
+ return path13.normalize(rel).split(path13.sep).join("/");
4086
+ }
4087
+ var PR_TITLE = "i18n: LiveLocale changes";
4088
+ var GITHUB_REMOTE_RE = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/;
4089
+ var defaultGit = (args, cwd, opts) => {
4090
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
4091
+ if (opts?.inherit) {
4092
+ return new Promise((resolve14, reject2) => {
4093
+ const child = (0, import_child_process5.spawn)("git", args, { cwd, env, stdio: "inherit", windowsHide: true });
4094
+ child.on("error", reject2);
4095
+ child.on(
4096
+ "close",
4097
+ (code) => code === 0 ? resolve14("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
4098
+ );
4099
+ });
4100
+ }
4101
+ return new Promise((resolve14, reject2) => {
4102
+ (0, import_child_process5.execFile)(
4103
+ "git",
4104
+ args,
4105
+ { cwd, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 },
4106
+ (error, stdout) => error ? reject2(error) : resolve14(String(stdout))
4107
+ );
4108
+ });
4109
+ };
4110
+ var defaultGh = (args, cwd) => new Promise((resolve14, reject2) => {
4111
+ (0, import_child_process5.execFile)(
4112
+ "gh",
4113
+ args,
4114
+ { cwd, windowsHide: true },
4115
+ (error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve14(String(stdout))
4116
+ );
4117
+ });
4118
+ function utcStamp(date) {
4119
+ const p = (n) => String(n).padStart(2, "0");
4120
+ return `${date.getUTCFullYear()}${p(date.getUTCMonth() + 1)}${p(date.getUTCDate())}-${p(date.getUTCHours())}${p(date.getUTCMinutes())}${p(date.getUTCSeconds())}`;
4121
+ }
4122
+ async function tryRun(run3) {
4123
+ try {
4124
+ return await run3();
4125
+ } catch {
4126
+ return null;
4127
+ }
4128
+ }
4129
+ async function pickBranchName(git, root, stamp) {
4130
+ const pushUrl = (await tryRun(() => git(["remote", "get-url", "--push", "origin"], root)))?.trim();
4131
+ const taken = async (name2) => {
4132
+ const local = await tryRun(
4133
+ () => git(["rev-parse", "--verify", "--quiet", `refs/heads/${name2}`], root)
4134
+ );
4135
+ if (local !== null) return true;
4136
+ if (!pushUrl) return false;
4137
+ const remote = await tryRun(
4138
+ () => git(["ls-remote", "--heads", pushUrl, `refs/heads/${name2}`], root)
4139
+ );
4140
+ return remote !== null && remote.trim() !== "";
4141
+ };
4142
+ const base = `livelocale/apply-${stamp}`;
4143
+ let name = base;
4144
+ for (let n = 2; await taken(name); n++) name = `${base}-${n}`;
4145
+ return name;
4146
+ }
4147
+ function errorMessage(err) {
4148
+ return err instanceof Error ? err.message : String(err);
4149
+ }
4150
+ async function writeEdits(mutator, plan, targets, localesDir, localesPath) {
4151
+ const written = [];
4152
+ for (const edit of plan.edits) {
4153
+ if (edit.status !== "apply") continue;
4154
+ await mutator.updateLocaleKey(localesDir, edit.locale, edit.key, edit.to);
4155
+ const file = fileFor(localesPath, targets.get(edit.locale).layout, edit.locale, edit.key);
4156
+ if (!written.includes(file)) written.push(file);
4157
+ }
4158
+ return written;
4159
+ }
4160
+ function exitCodeFor(plan) {
4161
+ const c = plan.counts;
4162
+ return c.conflict + c.invalid + c["needs-force"] > 0 ? 1 : 0;
4163
+ }
4164
+ async function preparePullRequest(git, projectDir, localesDir) {
4165
+ const top = (await tryRun(() => git(["rev-parse", "--show-toplevel"], projectDir)))?.trim();
4166
+ if (!top) throw new ApplyInputError("apply --pr needs a git repository.");
4167
+ const root = path13.resolve(top);
4168
+ const original = (await tryRun(() => git(["symbolic-ref", "--quiet", "--short", "HEAD"], root)))?.trim();
4169
+ if (!original) throw new ApplyInputError("Check out a branch first.");
4170
+ const status = await new GitClient().status(localesDir);
4171
+ if (status.changes.length > 0) {
4172
+ const rel = path13.relative(root, localesDir).split(path13.sep).join("/") || ".";
4173
+ throw new ApplyInputError(
4174
+ `Commit or stash the changes under ${rel} first; apply --pr commits everything in that folder.`
4175
+ );
4176
+ }
4177
+ return { root, original };
4178
+ }
4179
+ async function createPullRequest(log, gh, root, base, head, body) {
4180
+ const tmpDir = fs9.mkdtempSync(path13.join(os.tmpdir(), "livelocale-pr-"));
4181
+ const bodyFile = path13.join(tmpDir, "body.md");
4182
+ try {
4183
+ fs9.writeFileSync(bodyFile, body, "utf-8");
4184
+ const out = await gh(
4185
+ [
4186
+ "pr",
4187
+ "create",
4188
+ "--base",
4189
+ base,
4190
+ "--head",
4191
+ head,
4192
+ "--title",
4193
+ PR_TITLE,
4194
+ "--body-file",
4195
+ bodyFile
4196
+ ],
4197
+ root
4198
+ );
4199
+ const lines = out.split("\n").map((line) => line.trim()).filter(Boolean);
4200
+ return lines[lines.length - 1];
4201
+ } catch (err) {
4202
+ if (err?.code !== "ENOENT") {
4203
+ const stderr = err?.stderr;
4204
+ const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage(err);
4205
+ log(`gh could not open the pull request: ${reason}`);
4206
+ }
4207
+ return void 0;
4208
+ } finally {
4209
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
4210
+ }
4211
+ }
4212
+ async function runLocalApply(options) {
4213
+ const log = options.log ?? ((line) => console.log(line));
4214
+ const set = await decodeChangeLink(options.input, nodeInflate);
4215
+ const localesPath = options.localesPath ?? "locales";
4216
+ const localesDir = path13.resolve(options.projectDir, localesPath);
4217
+ if (!fs9.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
4218
+ throw new ApplyInputError(`No locales folder at ${localesPath}.`);
4219
+ }
4220
+ const pr = options.pr === true && options.dryRun !== true;
4221
+ const git = options.exec?.git ?? defaultGit;
4222
+ const repo = pr ? await preparePullRequest(git, options.projectDir, localesDir) : void 0;
4223
+ if (repo && set.branch !== "" && set.branch !== repo.original) {
4224
+ log(
4225
+ `The link was made from the branch \`${set.branch}\`; the pull request targets \`${repo.original}\`.`
4226
+ );
4227
+ }
4228
+ const mutator = new FileMutator({ requireEnabled: false, quiet: true });
4229
+ let sourceLocale = options.sourceLocale;
4230
+ if (sourceLocale === void 0) {
4231
+ const locales = await mutator.listLocales(localesDir);
4232
+ sourceLocale = locales.includes("en") ? "en" : locales[0] ?? "en";
4233
+ }
4234
+ if (set.src !== sourceLocale) {
4235
+ log(
4236
+ `The link says the source locale is \`${set.src}\`; using \`${sourceLocale}\` (pass --source to change).`
4237
+ );
4238
+ }
4239
+ const targets = /* @__PURE__ */ new Map();
4240
+ for (const locale of /* @__PURE__ */ new Set([sourceLocale, ...set.edits.map((e) => e.locale)])) {
4241
+ targets.set(locale, await readTarget(mutator, localesDir, locale));
4242
+ }
4243
+ const plan = planApply({
4244
+ edits: set.edits,
4245
+ sourceLocale,
4246
+ targets,
4247
+ allowForce: options.force === true
4248
+ });
4249
+ const exitCode = exitCodeFor(plan);
4250
+ if (!repo || plan.counts.apply === 0) {
4251
+ const written = repo || options.dryRun ? [] : await writeEdits(mutator, plan, targets, localesDir, localesPath);
4252
+ log(renderTerminalSummary(plan));
4253
+ return { plan, written, exitCode };
4254
+ }
4255
+ const { root, original } = repo;
4256
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
4257
+ const branch = await pickBranchName(git, root, utcStamp(now()));
4258
+ await git(["switch", "-c", branch], root);
4259
+ try {
4260
+ const written = await writeEdits(mutator, plan, targets, localesDir, localesPath);
4261
+ log(renderTerminalSummary(plan));
4262
+ let checkSummary;
4263
+ try {
4264
+ const report = await runCheck({
4265
+ projectDir: options.projectDir,
4266
+ localesPath: localesDir,
4267
+ sourceLocale,
4268
+ unused: false
4269
+ });
4270
+ const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
4271
+ checkSummary = summarizeCheck(report, edited);
4272
+ } catch (err) {
4273
+ checkSummary = `Check could not run: ${errorMessage(err)}`;
4274
+ }
4275
+ await new GitClient().commit(localesDir, renderCommitMessage({ plan, by: set.by }));
4276
+ await git(["push", "-u", "origin", branch], root, { inherit: true });
4277
+ const remote = (await tryRun(() => git(["remote", "get-url", "origin"], root)))?.trim();
4278
+ const slug = remote ? GITHUB_REMOTE_RE.exec(remote)?.[1] : void 0;
4279
+ let prUrl;
4280
+ if (slug) {
4281
+ const body = renderPullRequestBody({
4282
+ plan,
4283
+ by: set.by,
4284
+ note: set.note,
4285
+ checkSummary,
4286
+ target: { branch: original, isDefault: false, reason: "local" }
4287
+ });
4288
+ prUrl = await createPullRequest(
4289
+ log,
4290
+ options.exec?.gh ?? defaultGh,
4291
+ root,
4292
+ original,
4293
+ branch,
4294
+ body
4295
+ );
4296
+ }
4297
+ if (prUrl) {
4298
+ log(`Opened ${prUrl}`);
4299
+ } else {
4300
+ log(`Pushed ${branch}. Open a pull/merge request from it into ${original}.`);
4301
+ if (slug) log(`https://github.com/${slug}/compare/${original}...${branch}?expand=1`);
4302
+ }
4303
+ await git(["switch", original], root);
4304
+ return { plan, written, branch, prUrl, exitCode };
4305
+ } catch (err) {
4306
+ log(`You are on ${branch}; your changes are committed there.`);
4307
+ throw err;
4308
+ }
4309
+ }
4310
+
4311
+ // src/codemod/apply/cli.ts
4312
+ var VALUE_OPTIONS2 = { "--dir": "dir", "--locales": "locales", "--source": "source" };
4313
+ var FLAG_OPTIONS = {
4314
+ "--clipboard": "clipboard",
4315
+ "--pr": "pr",
4316
+ "--force": "force",
4317
+ "--dry-run": "dryRun"
4318
+ };
4319
+ function parseApplyArgs(argv) {
4320
+ const args = {
4321
+ input: void 0,
4322
+ clipboard: false,
4323
+ pr: false,
4324
+ force: false,
4325
+ dryRun: false,
4326
+ dir: ".",
4327
+ locales: void 0,
4328
+ source: void 0
4329
+ };
4330
+ for (let i = 0; i < argv.length; i++) {
4331
+ const arg = argv[i];
4332
+ if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
4333
+ const value = argv[i + 1];
4334
+ if (value === void 0 || value.startsWith("--")) {
4335
+ throw new ApplyInputError(`${arg} needs a value.`);
4336
+ }
4337
+ args[VALUE_OPTIONS2[arg]] = value;
4338
+ i++;
4339
+ } else if (Object.prototype.hasOwnProperty.call(FLAG_OPTIONS, arg)) {
4340
+ args[FLAG_OPTIONS[arg]] = true;
4341
+ } else if (arg.startsWith("-") && arg !== "-") {
4342
+ throw new ApplyInputError(`Unknown option "${arg}".`);
4343
+ } else if (args.input !== void 0) {
4344
+ throw new ApplyInputError('Pass one change link, file or "-".');
4345
+ } else {
4346
+ args.input = arg;
4347
+ }
4348
+ }
4349
+ if (args.clipboard && args.input !== void 0) {
4350
+ throw new ApplyInputError("Pass the link or --clipboard, not both.");
4351
+ }
4352
+ return args;
4353
+ }
4354
+ async function runApplyCommand(argv) {
4355
+ try {
4356
+ const args = parseApplyArgs(argv);
4357
+ const input = await readApplyInput(args.input, {
4358
+ cwd: process.cwd(),
4359
+ clipboard: args.clipboard
4360
+ });
4361
+ const result = await runLocalApply({
4362
+ projectDir: path14.resolve(args.dir),
4363
+ localesPath: args.locales,
4364
+ sourceLocale: args.source,
4365
+ input,
4366
+ force: args.force,
4367
+ dryRun: args.dryRun,
4368
+ pr: args.pr
4369
+ });
4370
+ process.exitCode = result.exitCode;
4371
+ } catch (err) {
4372
+ console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
4373
+ process.exitCode = 1;
4374
+ }
4375
+ }
4376
+
4377
+ // src/codemod/index.ts
4378
+ var DEFAULT_STUDIO_PORT = 3010;
4379
+ var DEFAULT_APP_ORIGIN = "http://localhost:3000";
4380
+ function printHelp() {
4381
+ console.log(`
4382
+ LiveLocale (livelocale) CLI
4383
+
4384
+ Usage:
4385
+ npx livelocale <command> [options]
4386
+
4387
+ Commands:
4388
+ migrate Bulk migrate existing react-i18next translation usages to wrap them with wrapTranslationEngine automatically.
4389
+ init Initialize the API route handler inside a Next.js project automatically.
4390
+ studio Start the visual editing studio local web server.
4391
+ doctor Check the project for common integration problems (route, provider, middleware, CSP, locales).
4392
+ check Check translations for missing keys, placeholder drift, broken ICU syntax and unused keys (CI-friendly).
4393
+ pack Write the share Studio (livelocale/) into a built site folder and link every HTML page to it.
4394
+ build Run your build command, then pack its output (Next.js: packs into public/livelocale/ first).
4395
+ apply Apply a LiveLocale change link to your locale files (--pr opens a pull request)
4396
+
4397
+ Options for migrate:
4398
+ --dir <path> Directory to scan (default: current directory)
4399
+ --exclude <list> Comma-separated list of directories to exclude (default: node_modules,.next,dist,.git)
4400
+ --dry-run Preview changes without writing them to disk
4401
+
4402
+ Options for init:
4403
+ --dir <path> Target project directory (default: current directory)
4404
+
4405
+ Options for studio:
4406
+ --port <number> Port to run the studio server on (default: ${DEFAULT_STUDIO_PORT})
4407
+ --app <origin> Origin of the app being edited (default: ${DEFAULT_APP_ORIGIN})
4408
+ Can also be changed at run time with ?app=<origin> on the Studio URL.
4409
+
4410
+ Options for doctor:
4411
+ --dir <path> Project directory to inspect (default: current directory)
4412
+ --locales <path> Locales directory relative to the project (default: locales)
4413
+ --app <url> Also probe the running app or deployed site, e.g. http://localhost:3000
4414
+ or https://user.github.io/repo/ (a path is the app base)
4415
+ --share Run the share link probes even when livelocale/manifest.json is missing
4416
+ --token <token> Access token for a preview deployment (default: LIVELOCALE_TOKEN)
4417
+
4418
+ Options for check:
4419
+ --dir <path> Project directory to scan (default: current directory)
4420
+ --locales <path> Locales directory relative to the project (default: locales)
4421
+ --source <locale> Locale the others are compared against (default: en, else the first locale)
4422
+ --no-unused Do not report keys that code never references
4423
+ --strict Treat warnings (unused keys, empty values) as failures
4424
+ --json Print the full report as JSON
4425
+
4426
+ Options for pack:
4427
+ pack <outDir> Built site folder, relative to --dir (e.g. dist, build, out)
4428
+ --dir <path> Project directory (default: current directory)
4429
+ --locales <path> Locales directory relative to the project (default: locales)
4430
+ --source <locale> Source locale (default: en, else the first locale)
4431
+ --base <path> Site path the app is served from, e.g. /repo/ (default: detected, else /)
4432
+ --expose-repo Put the GitHub owner/repo into livelocale/manifest.json
4433
+ --no-inject Do not add the flag.js script to HTML files
4434
+
4435
+ Options for build:
4436
+ build [--preview] [--out <dir>] [pack options] -- <command...>
4437
+ --preview Run the command with LIVELOCALE=preview and NEXT_PUBLIC_LIVELOCALE=preview
4438
+ --out <dir> Build output folder (default: detected from dist/, build/, out/)
4439
+
4440
+ Options for apply:
4441
+ apply <link|file|-> The change link, a file that holds it, or - to read stdin
4442
+ --clipboard Read the change link from the clipboard instead
4443
+ --pr Commit the changes on a new branch and open a pull request
4444
+ --dir <path> Project directory (default: current directory)
4445
+ --locales <path> Locales directory relative to the project (default: locales)
4446
+ --source <locale> Source locale (default: en, else the first locale)
4447
+ --force Apply edits the translator saved with "Save anyway"
4448
+ --dry-run Show what would change without writing files
4449
+
4450
+ Global Options:
4451
+ -h, --help Show help information
4452
+ `);
4453
+ }
4454
+ function normalizeOriginArg(value, flag) {
1645
4455
  try {
1646
4456
  const url = new URL(value);
1647
4457
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("bad protocol");
@@ -1653,12 +4463,22 @@ function normalizeOriginArg(value, flag) {
1653
4463
  process.exit(1);
1654
4464
  }
1655
4465
  }
4466
+ function normalizeAppArg(value) {
4467
+ try {
4468
+ return normalizeAppUrl(value);
4469
+ } catch {
4470
+ console.error(
4471
+ `Error: --app must be an http(s) URL such as http://localhost:3000 or https://user.github.io/repo/ (got "${value}").`
4472
+ );
4473
+ process.exit(1);
4474
+ }
4475
+ }
1656
4476
  function isPortAnswering(port) {
1657
- return new Promise((resolve6) => {
4477
+ return new Promise((resolve14) => {
1658
4478
  const socket = net.connect({ host: "localhost", port });
1659
4479
  const done = (answering) => {
1660
4480
  socket.destroy();
1661
- resolve6(answering);
4481
+ resolve14(answering);
1662
4482
  };
1663
4483
  socket.setTimeout(1e3, () => done(false));
1664
4484
  socket.once("connect", () => done(true));
@@ -1668,8 +4488,8 @@ function isPortAnswering(port) {
1668
4488
  async function runStudio(port, appOrigin) {
1669
4489
  console.log(`
1670
4490
  \u{1F680} Starting LiveLocale Studio...`);
1671
- const studioDir = path5.resolve(getDirname(), "../studio");
1672
- if (!fs5.existsSync(path5.join(studioDir, "index.html"))) {
4491
+ const studioDir = path15.resolve(moduleDir(), "../studio");
4492
+ if (!fs10.existsSync(path15.join(studioDir, "index.html"))) {
1673
4493
  console.error(`Error: Studio assets not found at "${studioDir}".`);
1674
4494
  console.error(`Did you build the project? Run "npm run build" first.`);
1675
4495
  process.exit(1);
@@ -1701,48 +4521,48 @@ async function runStudio(port, appOrigin) {
1701
4521
  function runInit(projectDir) {
1702
4522
  console.log(`
1703
4523
  \u{1F680} Initializing LiveLocale API handler...`);
1704
- const absoluteDir = path5.resolve(projectDir);
1705
- if (!fs5.existsSync(absoluteDir)) {
4524
+ const absoluteDir = path15.resolve(projectDir);
4525
+ if (!fs10.existsSync(absoluteDir)) {
1706
4526
  console.error(`Error: Directory "${projectDir}" does not exist.`);
1707
4527
  process.exit(1);
1708
4528
  }
1709
- const isTypeScript = fs5.existsSync(path5.join(absoluteDir, "tsconfig.json"));
4529
+ const isTypeScript = fs10.existsSync(path15.join(absoluteDir, "tsconfig.json"));
1710
4530
  const fileExt = isTypeScript ? "ts" : "js";
1711
4531
  console.log(`\u{1F4C1} Target: ${absoluteDir}`);
1712
4532
  console.log(`\u{1F527} Language detected: ${isTypeScript ? "TypeScript" : "JavaScript"}`);
1713
4533
  let hasNextIntl = false;
1714
4534
  let hasReactI18next = false;
1715
- const targetPkgPath = path5.join(absoluteDir, "package.json");
1716
- if (fs5.existsSync(targetPkgPath)) {
4535
+ const targetPkgPath = path15.join(absoluteDir, "package.json");
4536
+ if (fs10.existsSync(targetPkgPath)) {
1717
4537
  try {
1718
- const pkg = JSON.parse(fs5.readFileSync(targetPkgPath, "utf8"));
4538
+ const pkg = JSON.parse(fs10.readFileSync(targetPkgPath, "utf8"));
1719
4539
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1720
4540
  hasNextIntl = Boolean(allDeps["next-intl"]);
1721
4541
  hasReactI18next = Boolean(allDeps["react-i18next"]);
1722
4542
  } catch {
1723
4543
  }
1724
4544
  }
1725
- const appDirPath = path5.join(absoluteDir, "app");
1726
- const pagesDirPath = path5.join(absoluteDir, "pages");
1727
- const srcAppDirPath = path5.join(absoluteDir, "src", "app");
1728
- const srcPagesDirPath = path5.join(absoluteDir, "src", "pages");
4545
+ const appDirPath = path15.join(absoluteDir, "app");
4546
+ const pagesDirPath = path15.join(absoluteDir, "pages");
4547
+ const srcAppDirPath = path15.join(absoluteDir, "src", "app");
4548
+ const srcPagesDirPath = path15.join(absoluteDir, "src", "pages");
1729
4549
  let targetPath = "";
1730
4550
  let routerType = "";
1731
4551
  let content = "";
1732
- if (fs5.existsSync(appDirPath)) {
1733
- targetPath = path5.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
4552
+ if (fs10.existsSync(appDirPath)) {
4553
+ targetPath = path15.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
1734
4554
  routerType = "Next.js App Router (root)";
1735
- } else if (fs5.existsSync(srcAppDirPath)) {
1736
- targetPath = path5.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
4555
+ } else if (fs10.existsSync(srcAppDirPath)) {
4556
+ targetPath = path15.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
1737
4557
  routerType = "Next.js App Router (src/)";
1738
- } else if (fs5.existsSync(pagesDirPath)) {
1739
- targetPath = path5.join(pagesDirPath, "api", `livelocale.${fileExt}`);
4558
+ } else if (fs10.existsSync(pagesDirPath)) {
4559
+ targetPath = path15.join(pagesDirPath, "api", `livelocale.${fileExt}`);
1740
4560
  routerType = "Next.js Pages Router (root)";
1741
- } else if (fs5.existsSync(srcPagesDirPath)) {
1742
- targetPath = path5.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
4561
+ } else if (fs10.existsSync(srcPagesDirPath)) {
4562
+ targetPath = path15.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
1743
4563
  routerType = "Next.js Pages Router (src/)";
1744
4564
  } else {
1745
- targetPath = path5.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
4565
+ targetPath = path15.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
1746
4566
  routerType = "Default App Router (app/ not found)";
1747
4567
  }
1748
4568
  console.log(`\u{1F4CD} Project structure: ${routerType}`);
@@ -1770,14 +4590,14 @@ const handler = createLiveLocaleHandler({
1770
4590
  export default handler;
1771
4591
  `;
1772
4592
  }
1773
- const targetDir = path5.dirname(targetPath);
1774
- if (!fs5.existsSync(targetDir)) {
1775
- fs5.mkdirSync(targetDir, { recursive: true });
4593
+ const targetDir = path15.dirname(targetPath);
4594
+ if (!fs10.existsSync(targetDir)) {
4595
+ fs10.mkdirSync(targetDir, { recursive: true });
1776
4596
  }
1777
4597
  try {
1778
- fs5.writeFileSync(targetPath, content, "utf8");
4598
+ fs10.writeFileSync(targetPath, content, "utf8");
1779
4599
  console.log(`\u2705 Created API Route Handler successfully at:`);
1780
- console.log(` ${path5.relative(absoluteDir, targetPath)}`);
4600
+ console.log(` ${path15.relative(absoluteDir, targetPath)}`);
1781
4601
  if (hasNextIntl) {
1782
4602
  console.log(`
1783
4603
  \u{1F4A1} Detected 'next-intl' in your project!`);
@@ -1819,8 +4639,8 @@ async function runMigrate(args) {
1819
4639
  }
1820
4640
  }
1821
4641
  }
1822
- const absoluteDir = path5.resolve(dir);
1823
- if (!fs5.existsSync(absoluteDir)) {
4642
+ const absoluteDir = path15.resolve(dir);
4643
+ if (!fs10.existsSync(absoluteDir)) {
1824
4644
  console.error(`Error: Directory "${dir}" does not exist.`);
1825
4645
  process.exit(1);
1826
4646
  }
@@ -1844,28 +4664,28 @@ async function runMigrate(args) {
1844
4664
  let scannedCount = 0;
1845
4665
  let migratedCount = 0;
1846
4666
  async function walk(currentDir) {
1847
- const files = fs5.readdirSync(currentDir, { withFileTypes: true });
4667
+ const files = fs10.readdirSync(currentDir, { withFileTypes: true });
1848
4668
  for (const file of files) {
1849
- const fullPath = path5.join(currentDir, file.name);
4669
+ const fullPath = path15.join(currentDir, file.name);
1850
4670
  if (file.isDirectory()) {
1851
4671
  if (excludes.includes(file.name)) {
1852
4672
  continue;
1853
4673
  }
1854
4674
  await walk(fullPath);
1855
4675
  } else if (file.isFile()) {
1856
- const ext = path5.extname(file.name);
4676
+ const ext = path15.extname(file.name);
1857
4677
  if ([".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
1858
4678
  scannedCount++;
1859
4679
  try {
1860
- const content = fs5.readFileSync(fullPath, "utf8");
4680
+ const content = fs10.readFileSync(fullPath, "utf8");
1861
4681
  const result = await transformReactI18next2(content, fullPath);
1862
4682
  if (result.modified) {
1863
4683
  migratedCount++;
1864
- const relativePath = path5.relative(absoluteDir, fullPath);
4684
+ const relativePath = path15.relative(absoluteDir, fullPath);
1865
4685
  if (dryRun) {
1866
4686
  console.log(`[DRY RUN] Would migrate: ${relativePath}`);
1867
4687
  } else {
1868
- fs5.writeFileSync(fullPath, result.code, "utf8");
4688
+ fs10.writeFileSync(fullPath, result.code, "utf8");
1869
4689
  console.log(`\u2705 Migrated: ${relativePath}`);
1870
4690
  }
1871
4691
  }
@@ -1895,7 +4715,8 @@ async function runMigrate(args) {
1895
4715
  async function runDoctorCommand(args) {
1896
4716
  let dir = ".";
1897
4717
  let localesPath;
1898
- let appOrigin;
4718
+ let app;
4719
+ let share = false;
1899
4720
  let token;
1900
4721
  for (let i = 1; i < args.length; i++) {
1901
4722
  const arg = args[i];
@@ -1904,23 +4725,32 @@ async function runDoctorCommand(args) {
1904
4725
  } else if (arg === "--locales") {
1905
4726
  localesPath = args[++i];
1906
4727
  } else if (arg === "--app") {
1907
- appOrigin = normalizeOriginArg(args[++i] || "", "--app");
4728
+ app = normalizeAppArg(args[++i] || "");
4729
+ } else if (arg === "--share") {
4730
+ share = true;
1908
4731
  } else if (arg === "--token") {
1909
4732
  token = args[++i];
1910
4733
  } else if (arg && !arg.startsWith("--")) {
1911
4734
  dir = arg;
1912
4735
  }
1913
4736
  }
1914
- const projectDir = path5.resolve(dir);
1915
- if (!fs5.existsSync(projectDir)) {
4737
+ const projectDir = path15.resolve(dir);
4738
+ if (!fs10.existsSync(projectDir)) {
1916
4739
  console.error(`Error: Directory "${dir}" does not exist.`);
1917
4740
  process.exit(1);
1918
4741
  }
1919
4742
  console.log(`
1920
4743
  \u{1FA7A} LiveLocale doctor \u2014 ${projectDir}`);
1921
- if (appOrigin) console.log(`\u{1F517} Probing app at ${appOrigin}`);
4744
+ if (app) console.log(`\u{1F517} Probing app at ${app.base}`);
1922
4745
  console.log("--------------------------------------------------");
1923
- const findings = await runDoctor({ projectDir, localesPath, appOrigin, token });
4746
+ const findings = await runDoctor({
4747
+ projectDir,
4748
+ localesPath,
4749
+ appOrigin: app?.origin,
4750
+ appBase: app?.base,
4751
+ share,
4752
+ token
4753
+ });
1924
4754
  console.log(formatFindings(findings));
1925
4755
  console.log("--------------------------------------------------");
1926
4756
  const fails = findings.filter((f) => f.level === "fail").length;
@@ -1933,7 +4763,9 @@ async function runDoctorCommand(args) {
1933
4763
  console.log(
1934
4764
  warns > 0 ? `\u2705 No blocking problems, ${warns} warning(s) worth a look.` : `\u2705 Everything looks good.`
1935
4765
  );
1936
- if (!appOrigin) {
4766
+ const shareLink = findings.find((f) => f.title === "Share link")?.detail;
4767
+ if (shareLink) console.log(`\u{1F517} Share link: ${shareLink}`);
4768
+ if (!app) {
1937
4769
  console.log(
1938
4770
  `\u{1F4A1} Start your dev server and re-run with --app http://localhost:3000 for live checks.`
1939
4771
  );
@@ -1964,8 +4796,8 @@ async function runCheckCommand(args) {
1964
4796
  dir = arg;
1965
4797
  }
1966
4798
  }
1967
- const projectDir = path5.resolve(dir);
1968
- if (!fs5.existsSync(projectDir)) {
4799
+ const projectDir = path15.resolve(dir);
4800
+ if (!fs10.existsSync(projectDir)) {
1969
4801
  console.error(`Error: Directory "${dir}" does not exist.`);
1970
4802
  process.exit(1);
1971
4803
  }
@@ -2003,13 +4835,74 @@ async function runCheckCommand(args) {
2003
4835
  warns > 0 ? `\u2705 No problems, ${warns} warning(s) worth a look.` : `\u2705 Translations look good.`
2004
4836
  );
2005
4837
  }
2006
- async function run() {
4838
+ var PACK_USAGE = "Usage: livelocale pack <outDir> [options]";
4839
+ function printPackSummary(result, projectDir, warnings) {
4840
+ for (const warning of warnings) console.log(`\u26A0\uFE0F ${warning}`);
4841
+ const outRel = path15.relative(projectDir, path15.dirname(result.shareDir)).split(path15.sep).join("/") || ".";
4842
+ console.log(
4843
+ `\u2705 LiveLocale share folder written to ${outRel}/livelocale/ (${result.manifest.locales.length} locales, ${result.manifest.codeKeys.length} code keys, ${result.htmlInjected.length} HTML files linked).`
4844
+ );
4845
+ console.log(`\u{1F517} Open ${result.shareUrlPath} on the deployed site.`);
4846
+ }
4847
+ async function runPackCommand(args) {
4848
+ try {
4849
+ const options = parseShareArgs(args.slice(1));
4850
+ if (options.positional.length !== 1 || options.out !== void 0 || options.preview || options.command.length > 0) {
4851
+ throw new PackError("USAGE", PACK_USAGE);
4852
+ }
4853
+ const projectDir = path15.resolve(options.dir);
4854
+ const result = await packOutput({
4855
+ projectDir,
4856
+ outDir: options.positional[0],
4857
+ localesPath: options.locales,
4858
+ sourceLocale: options.source,
4859
+ base: options.base,
4860
+ exposeRepo: options.exposeRepo,
4861
+ injectHtml: options.inject ? "auto" : false
4862
+ });
4863
+ printPackSummary(result, projectDir, result.warnings);
4864
+ } catch (err) {
4865
+ if (!(err instanceof PackError)) throw err;
4866
+ console.error(`\u274C ${err.message}`);
4867
+ process.exitCode = 1;
4868
+ }
4869
+ }
4870
+ async function runBuildCli(args) {
4871
+ try {
4872
+ const options = parseShareArgs(args.slice(1));
4873
+ if (options.positional.length > 0 || !options.inject) {
4874
+ throw new PackError("USAGE", BUILD_USAGE);
4875
+ }
4876
+ const projectDir = path15.resolve(options.dir);
4877
+ const result = await runBuildCommand({
4878
+ projectDir,
4879
+ command: options.command,
4880
+ preview: options.preview,
4881
+ outDir: options.out,
4882
+ localesPath: options.locales,
4883
+ sourceLocale: options.source,
4884
+ base: options.base,
4885
+ exposeRepo: options.exposeRepo
4886
+ });
4887
+ if (result.pack !== null) {
4888
+ printPackSummary(result.pack, projectDir, result.warnings);
4889
+ } else {
4890
+ for (const warning of result.warnings) console.log(`\u26A0\uFE0F ${warning}`);
4891
+ }
4892
+ process.exitCode = result.exitCode;
4893
+ } catch (err) {
4894
+ if (!(err instanceof PackError)) throw err;
4895
+ console.error(`\u274C ${err.message}`);
4896
+ process.exitCode = 1;
4897
+ }
4898
+ }
4899
+ async function run2() {
2007
4900
  const args = process.argv.slice(2);
2008
4901
  if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
2009
4902
  printHelp();
2010
4903
  process.exit(0);
2011
4904
  }
2012
- const COMMANDS = ["migrate", "init", "studio", "doctor", "check"];
4905
+ const COMMANDS = ["migrate", "init", "studio", "doctor", "check", "pack", "build", "apply"];
2013
4906
  if (!args[0] || !COMMANDS.includes(args[0])) {
2014
4907
  console.error(`Unknown command: "${args[0]}". Expected one of: ${COMMANDS.join(", ")}.`);
2015
4908
  printHelp();
@@ -2023,6 +4916,18 @@ async function run() {
2023
4916
  await runCheckCommand(args);
2024
4917
  return;
2025
4918
  }
4919
+ if (args[0] === "pack") {
4920
+ await runPackCommand(args);
4921
+ return;
4922
+ }
4923
+ if (args[0] === "build") {
4924
+ await runBuildCli(args);
4925
+ return;
4926
+ }
4927
+ if (args[0] === "apply") {
4928
+ await runApplyCommand(args.slice(1));
4929
+ return;
4930
+ }
2026
4931
  if (args[0] === "init") {
2027
4932
  let dir = ".";
2028
4933
  for (let i = 1; i < args.length; i++) {
@@ -2053,7 +4958,7 @@ async function run() {
2053
4958
  }
2054
4959
  await runMigrate(args);
2055
4960
  }
2056
- run().catch((err) => {
4961
+ run2().catch((err) => {
2057
4962
  console.error(`\u274C ${err?.message || err}`);
2058
4963
  process.exit(1);
2059
4964
  });