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