livelocale 0.9.0 → 0.10.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.
@@ -313,11 +313,12 @@ var init_transform = __esm({
313
313
  });
314
314
 
315
315
  // src/codemod/index.ts
316
- var fs10 = __toESM(require("fs"), 1);
317
- var path15 = __toESM(require("path"), 1);
316
+ var fs12 = __toESM(require("fs"), 1);
317
+ var path18 = __toESM(require("path"), 1);
318
318
  var net = __toESM(require("net"), 1);
319
319
 
320
320
  // src/server/module-dir.ts
321
+ var import_fs = require("fs");
321
322
  var path = __toESM(require("path"), 1);
322
323
  var import_url = require("url");
323
324
  var import_meta = {};
@@ -327,6 +328,15 @@ function moduleDir() {
327
328
  }
328
329
  return path.dirname((0, import_url.fileURLToPath)(import_meta.url));
329
330
  }
331
+ function readOwnVersion() {
332
+ const file = path.resolve(moduleDir(), "../../package.json");
333
+ const pkg = JSON.parse((0, import_fs.readFileSync)(file, "utf-8"));
334
+ const version = pkg?.version;
335
+ if (typeof version !== "string" || version === "") {
336
+ throw new Error(`No version in ${file}.`);
337
+ }
338
+ return version;
339
+ }
330
340
 
331
341
  // src/codemod/studio-server.ts
332
342
  var fs = __toESM(require("fs"), 1);
@@ -751,7 +761,7 @@ function isShareManifest(value) {
751
761
  const layout = own2(value, "layout");
752
762
  const locales = own2(value, "locales");
753
763
  const sourceLocale = own2(value, "sourceLocale");
754
- return own2(value, "format") === SHARE_MANIFEST_FORMAT && (repo === void 0 || typeof repo === "string" && isValidRepoSlug(repo)) && isNullOr(own2(value, "defaultBranch"), isValidBranchName) && isNullOr(own2(value, "branch"), isValidBranchName) && isNullOr(own2(value, "base"), (base) => BASE_SHA_RE2.test(base)) && typeof own2(value, "localesPath") === "string" && (layout === "flat" || layout === "namespaced") && isArrayOf(locales, isLocaleCode) && typeof sourceLocale === "string" && locales.includes(sourceLocale) && isNullOr(own2(value, "basePath"), (path16) => path16.startsWith("/") && path16.endsWith("/")) && isArrayOf(own2(value, "codeKeys"), isString);
764
+ 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"), (path19) => path19.startsWith("/") && path19.endsWith("/")) && isArrayOf(own2(value, "codeKeys"), isString);
755
765
  }
756
766
 
757
767
  // src/codemod/doctor.ts
@@ -1600,7 +1610,7 @@ function validateMessageSyntax(message) {
1600
1610
  }
1601
1611
 
1602
1612
  // src/server/key-scanner.ts
1603
- var import_fs = require("fs");
1613
+ var import_fs2 = require("fs");
1604
1614
  var path4 = __toESM(require("path"), 1);
1605
1615
 
1606
1616
  // src/core/env.ts
@@ -1885,7 +1895,7 @@ var KeyScanner = class {
1885
1895
  const gitignorePath = path4.join(projectRoot, ".gitignore");
1886
1896
  let stat;
1887
1897
  try {
1888
- stat = await import_fs.promises.stat(gitignorePath);
1898
+ stat = await import_fs2.promises.stat(gitignorePath);
1889
1899
  } catch {
1890
1900
  this.gitignoreCache = null;
1891
1901
  return NO_IGNORES;
@@ -1895,7 +1905,7 @@ var KeyScanner = class {
1895
1905
  return hit.value;
1896
1906
  }
1897
1907
  try {
1898
- const content = await import_fs.promises.readFile(gitignorePath, "utf-8");
1908
+ const content = await import_fs2.promises.readFile(gitignorePath, "utf-8");
1899
1909
  const matcher = parseGitignore(content);
1900
1910
  this.gitignoreCache = { mtimeMs: stat.mtimeMs, size: stat.size, value: matcher };
1901
1911
  return matcher;
@@ -1920,7 +1930,7 @@ var KeyScanner = class {
1920
1930
  const scanDir = async (dir) => {
1921
1931
  let items;
1922
1932
  try {
1923
- items = await import_fs.promises.readdir(dir, { withFileTypes: true });
1933
+ items = await import_fs2.promises.readdir(dir, { withFileTypes: true });
1924
1934
  } catch {
1925
1935
  return;
1926
1936
  }
@@ -1938,7 +1948,7 @@ var KeyScanner = class {
1938
1948
  if (gitignore.ignores(toRel(fullPath), false)) continue;
1939
1949
  let stat;
1940
1950
  try {
1941
- stat = await import_fs.promises.stat(fullPath);
1951
+ stat = await import_fs2.promises.stat(fullPath);
1942
1952
  } catch {
1943
1953
  continue;
1944
1954
  }
@@ -1950,7 +1960,7 @@ var KeyScanner = class {
1950
1960
  continue;
1951
1961
  }
1952
1962
  try {
1953
- const content = await import_fs.promises.readFile(fullPath, "utf-8");
1963
+ const content = await import_fs2.promises.readFile(fullPath, "utf-8");
1954
1964
  const keys = extractKeys(content);
1955
1965
  this.fileCache.set(fullPath, { mtimeMs: stat.mtimeMs, size: stat.size, value: keys });
1956
1966
  for (const k of keys) keysSet.add(k);
@@ -1977,18 +1987,18 @@ var KeyScanner = class {
1977
1987
  const localePath = path4.resolve(resolvedBasePath, locale);
1978
1988
  let isDir = false;
1979
1989
  try {
1980
- const stats = await import_fs.promises.stat(localePath);
1990
+ const stats = await import_fs2.promises.stat(localePath);
1981
1991
  isDir = stats.isDirectory();
1982
1992
  } catch {
1983
1993
  }
1984
1994
  let data = {};
1985
1995
  if (isDir) {
1986
- const files = await import_fs.promises.readdir(localePath);
1996
+ const files = await import_fs2.promises.readdir(localePath);
1987
1997
  for (const file of files) {
1988
1998
  if (file.endsWith(".json")) {
1989
1999
  const ns = path4.basename(file, ".json");
1990
2000
  try {
1991
- const content = await import_fs.promises.readFile(path4.join(localePath, file), "utf-8");
2001
+ const content = await import_fs2.promises.readFile(path4.join(localePath, file), "utf-8");
1992
2002
  data[ns] = JSON.parse(content);
1993
2003
  } catch {
1994
2004
  }
@@ -2001,7 +2011,7 @@ var KeyScanner = class {
2001
2011
  }
2002
2012
  const filePath = path4.resolve(resolvedBasePath, `${locale}.json`);
2003
2013
  if (filePath.startsWith(resolvedBasePath)) {
2004
- const content = await import_fs.promises.readFile(filePath, "utf-8");
2014
+ const content = await import_fs2.promises.readFile(filePath, "utf-8");
2005
2015
  data = JSON.parse(content);
2006
2016
  }
2007
2017
  } catch {
@@ -2255,23 +2265,23 @@ function formatCheckReport(report, limit = 25) {
2255
2265
 
2256
2266
  // src/server/pack.ts
2257
2267
  var import_child_process2 = require("child_process");
2258
- var import_fs4 = require("fs");
2268
+ var import_fs5 = require("fs");
2259
2269
  var path8 = __toESM(require("path"), 1);
2260
2270
 
2261
2271
  // src/server/build-info.ts
2262
2272
  var import_child_process = require("child_process");
2263
- var import_fs2 = require("fs");
2273
+ var import_fs3 = require("fs");
2264
2274
  var path6 = __toESM(require("path"), 1);
2265
- var runGit = (args, cwd) => new Promise((resolve14) => {
2275
+ var runGit = (args, cwd) => new Promise((resolve16) => {
2266
2276
  try {
2267
2277
  (0, import_child_process.execFile)(
2268
2278
  "git",
2269
2279
  args,
2270
2280
  { cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
2271
- (error, stdout) => resolve14(error ? null : String(stdout).trim())
2281
+ (error, stdout) => resolve16(error ? null : String(stdout).trim())
2272
2282
  );
2273
2283
  } catch {
2274
- resolve14(null);
2284
+ resolve16(null);
2275
2285
  }
2276
2286
  });
2277
2287
  var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
@@ -2290,7 +2300,7 @@ var asSlug = (value) => isValidRepoSlug(value) ? value : null;
2290
2300
  function readEvent(eventPath) {
2291
2301
  if (!eventPath) return {};
2292
2302
  try {
2293
- const parsed = JSON.parse((0, import_fs2.readFileSync)(eventPath, "utf-8"));
2303
+ const parsed = JSON.parse((0, import_fs3.readFileSync)(eventPath, "utf-8"));
2294
2304
  return isRecord(parsed) ? parsed : {};
2295
2305
  } catch {
2296
2306
  return {};
@@ -2389,7 +2399,7 @@ function parseGitHubSlug(remote) {
2389
2399
  }
2390
2400
 
2391
2401
  // src/server/build-target.ts
2392
- var import_fs3 = require("fs");
2402
+ var import_fs4 = require("fs");
2393
2403
  var path7 = __toESM(require("path"), 1);
2394
2404
  var NEXT_CONFIG_FILES = [
2395
2405
  "next.config.js",
@@ -2411,7 +2421,7 @@ var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
2411
2421
  var EXPORTS_POST = /export\s+(?:const|let|var|async\s+function|function)\s+POST\b|export\s*\{[^}]*\bPOST\b[^}]*\}/;
2412
2422
  function readText2(file) {
2413
2423
  try {
2414
- return (0, import_fs3.readFileSync)(file, "utf-8");
2424
+ return (0, import_fs4.readFileSync)(file, "utf-8");
2415
2425
  } catch {
2416
2426
  return null;
2417
2427
  }
@@ -2432,7 +2442,7 @@ function hasDependency(pkg, name) {
2432
2442
  function firstExisting2(projectDir, names) {
2433
2443
  for (const name of names) {
2434
2444
  const file = path7.join(projectDir, name);
2435
- if ((0, import_fs3.existsSync)(file)) return file;
2445
+ if ((0, import_fs4.existsSync)(file)) return file;
2436
2446
  }
2437
2447
  return null;
2438
2448
  }
@@ -2563,8 +2573,8 @@ var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the
2563
2573
  function isPlainObject2(value) {
2564
2574
  return typeof value === "object" && value !== null && !Array.isArray(value);
2565
2575
  }
2566
- function toPosix(relative8) {
2567
- return relative8.split(path8.sep).join("/") || ".";
2576
+ function toPosix(relative9) {
2577
+ return relative9.split(path8.sep).join("/") || ".";
2568
2578
  }
2569
2579
  function isInside(parent, child) {
2570
2580
  const rel = path8.relative(parent, child);
@@ -2582,7 +2592,7 @@ async function readJsonObject(file, projectDir) {
2582
2592
  const shown = toPosix(path8.relative(projectDir, file));
2583
2593
  let parsed;
2584
2594
  try {
2585
- parsed = JSON.parse(await import_fs4.promises.readFile(file, "utf-8"));
2595
+ parsed = JSON.parse(await import_fs5.promises.readFile(file, "utf-8"));
2586
2596
  } catch (error) {
2587
2597
  throw new PackError(
2588
2598
  "INVALID_LOCALE_FILE",
@@ -2604,8 +2614,8 @@ async function collectShareData(options) {
2604
2614
  const warnings = [];
2605
2615
  let entries;
2606
2616
  try {
2607
- if (!(await import_fs4.promises.stat(localesDir)).isDirectory()) throw new Error("not a directory");
2608
- entries = await import_fs4.promises.readdir(localesDir, { withFileTypes: true });
2617
+ if (!(await import_fs5.promises.stat(localesDir)).isDirectory()) throw new Error("not a directory");
2618
+ entries = await import_fs5.promises.readdir(localesDir, { withFileTypes: true });
2609
2619
  } catch {
2610
2620
  throw new PackError(
2611
2621
  "LOCALES_NOT_FOUND",
@@ -2643,7 +2653,7 @@ async function collectShareData(options) {
2643
2653
  if (dirLocales.has(locale)) {
2644
2654
  dictionary = {};
2645
2655
  const folder = path8.join(localesDir, locale);
2646
- const files = (await import_fs4.promises.readdir(folder, { withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".json")).map((file) => file.name).sort();
2656
+ const files = (await import_fs5.promises.readdir(folder, { withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".json")).map((file) => file.name).sort();
2647
2657
  for (const file of files) {
2648
2658
  const namespace = file.slice(0, -".json".length);
2649
2659
  defineOwn(dictionary, namespace, await readJsonObject(path8.join(folder, file), projectDir));
@@ -2684,14 +2694,14 @@ function defaultStudioCandidates() {
2684
2694
  }
2685
2695
  function isDirectory(target) {
2686
2696
  try {
2687
- return (0, import_fs4.statSync)(target).isDirectory();
2697
+ return (0, import_fs5.statSync)(target).isDirectory();
2688
2698
  } catch {
2689
2699
  return false;
2690
2700
  }
2691
2701
  }
2692
2702
  function resolveStudioDir(candidates = defaultStudioCandidates()) {
2693
2703
  for (const dir of candidates) {
2694
- if ((0, import_fs4.existsSync)(path8.join(dir, "index.html")) && isDirectory(path8.join(dir, "assets"))) {
2704
+ if ((0, import_fs5.existsSync)(path8.join(dir, "index.html")) && isDirectory(path8.join(dir, "assets"))) {
2695
2705
  return dir;
2696
2706
  }
2697
2707
  }
@@ -2699,13 +2709,13 @@ function resolveStudioDir(candidates = defaultStudioCandidates()) {
2699
2709
  }
2700
2710
  async function ownsShareDir(shareDir) {
2701
2711
  try {
2702
- await import_fs4.promises.lstat(shareDir);
2712
+ await import_fs5.promises.lstat(shareDir);
2703
2713
  } catch {
2704
2714
  return true;
2705
2715
  }
2706
2716
  try {
2707
2717
  const manifest = JSON.parse(
2708
- await import_fs4.promises.readFile(path8.join(shareDir, SHARE_MANIFEST_FILE), "utf-8")
2718
+ await import_fs5.promises.readFile(path8.join(shareDir, SHARE_MANIFEST_FILE), "utf-8")
2709
2719
  );
2710
2720
  return isPlainObject2(manifest) && typeof manifest["format"] === "number";
2711
2721
  } catch {
@@ -2715,7 +2725,7 @@ async function ownsShareDir(shareDir) {
2715
2725
  async function findHtmlFiles(outDir, shareDir) {
2716
2726
  const found = [];
2717
2727
  const walk = async (dir) => {
2718
- for (const entry of await import_fs4.promises.readdir(dir, { withFileTypes: true })) {
2728
+ for (const entry of await import_fs5.promises.readdir(dir, { withFileTypes: true })) {
2719
2729
  if (entry.isSymbolicLink()) continue;
2720
2730
  const full = path8.join(dir, entry.name);
2721
2731
  if (entry.isDirectory()) {
@@ -2762,30 +2772,30 @@ async function packOutput(options) {
2762
2772
  `${toPosix(path8.relative(projectDir, outDir))}/${SHARE_DIR}/ already exists and was not written by LiveLocale. Move it away and run again.`
2763
2773
  );
2764
2774
  }
2765
- await import_fs4.promises.rm(shareDir, { recursive: true, force: true });
2766
- await import_fs4.promises.cp(studioDir, shareDir, { recursive: true });
2767
- await import_fs4.promises.writeFile(
2775
+ await import_fs5.promises.rm(shareDir, { recursive: true, force: true });
2776
+ await import_fs5.promises.cp(studioDir, shareDir, { recursive: true });
2777
+ await import_fs5.promises.writeFile(
2768
2778
  path8.join(shareDir, SHARE_MANIFEST_FILE),
2769
2779
  JSON.stringify(data.manifest, null, 2) + "\n",
2770
2780
  "utf-8"
2771
2781
  );
2772
2782
  const localesDir = path8.join(shareDir, SHARE_LOCALES_DIR);
2773
- await import_fs4.promises.mkdir(localesDir, { recursive: true });
2783
+ await import_fs5.promises.mkdir(localesDir, { recursive: true });
2774
2784
  for (const locale of data.manifest.locales) {
2775
- await import_fs4.promises.writeFile(
2785
+ await import_fs5.promises.writeFile(
2776
2786
  path8.join(localesDir, `${locale}.json`),
2777
2787
  JSON.stringify(data.dictionaries[locale]),
2778
2788
  "utf-8"
2779
2789
  );
2780
2790
  }
2781
- await import_fs4.promises.writeFile(path8.join(shareDir, SHARE_FLAG_FILE), FLAG_SCRIPT, "utf-8");
2782
- const inject = injectHtml === "auto" ? target.kind !== "next" && !(0, import_fs4.existsSync)(path8.join(outDir, "_next")) : injectHtml;
2791
+ await import_fs5.promises.writeFile(path8.join(shareDir, SHARE_FLAG_FILE), FLAG_SCRIPT, "utf-8");
2792
+ const inject = injectHtml === "auto" ? target.kind !== "next" && !(0, import_fs5.existsSync)(path8.join(outDir, "_next")) : injectHtml;
2783
2793
  const htmlInjected = inject ? await findHtmlFiles(outDir, shareDir) : [];
2784
2794
  for (const rel of htmlInjected) {
2785
2795
  const file = path8.join(outDir, ...rel.split("/"));
2786
- const html = await import_fs4.promises.readFile(file, "utf-8");
2796
+ const html = await import_fs5.promises.readFile(file, "utf-8");
2787
2797
  const injected = injectFlagScript(html, flagSrcFor(base, rel));
2788
- if (injected !== html) await import_fs4.promises.writeFile(file, injected, "utf-8");
2798
+ if (injected !== html) await import_fs5.promises.writeFile(file, injected, "utf-8");
2789
2799
  }
2790
2800
  const warnings = [...data.warnings];
2791
2801
  if (base.kind === "relative") warnings.push(RELATIVE_BASE_WARNING);
@@ -2863,7 +2873,7 @@ function quoteWindowsArg(arg) {
2863
2873
  }
2864
2874
  return out + "\\".repeat(backslashes * 2) + '"';
2865
2875
  }
2866
- var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reject2) => {
2876
+ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve16, reject2) => {
2867
2877
  const child = process.platform === "win32" ? (0, import_child_process2.spawn)([command, ...args].map(quoteWindowsArg).join(" "), {
2868
2878
  cwd,
2869
2879
  env,
@@ -2871,10 +2881,10 @@ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reje
2871
2881
  shell: true
2872
2882
  }) : (0, import_child_process2.spawn)(command, args, { cwd, env, stdio: "inherit" });
2873
2883
  child.on("error", (error) => {
2874
- if (error.code === "ENOENT") resolve14(127);
2884
+ if (error.code === "ENOENT") resolve16(127);
2875
2885
  else reject2(error);
2876
2886
  });
2877
- child.on("close", (code) => resolve14(code ?? 1));
2887
+ child.on("close", (code) => resolve16(code ?? 1));
2878
2888
  });
2879
2889
  var BUILD_USAGE = "Usage: livelocale build [--preview] -- <your build command>";
2880
2890
  var NEXT_PREVIEW_WARNING = "Next.js pages have no watermarks without --preview; the Studio will open but nothing is editable.";
@@ -2883,7 +2893,7 @@ var FRESH_SLACK_MS = 2e3;
2883
2893
  function gitignoresShareDir(projectDir) {
2884
2894
  let content;
2885
2895
  try {
2886
- content = (0, import_fs4.readFileSync)(path8.join(projectDir, ".gitignore"), "utf-8");
2896
+ content = (0, import_fs5.readFileSync)(path8.join(projectDir, ".gitignore"), "utf-8");
2887
2897
  } catch {
2888
2898
  return false;
2889
2899
  }
@@ -2892,7 +2902,7 @@ function gitignoresShareDir(projectDir) {
2892
2902
  function freshOutput(projectDir, candidates, startMs) {
2893
2903
  for (const candidate of candidates) {
2894
2904
  try {
2895
- const stat = (0, import_fs4.statSync)(path8.join(projectDir, candidate, "index.html"));
2905
+ const stat = (0, import_fs5.statSync)(path8.join(projectDir, candidate, "index.html"));
2896
2906
  if (stat.isFile() && stat.mtimeMs >= startMs - FRESH_SLACK_MS) return candidate;
2897
2907
  } catch {
2898
2908
  }
@@ -2922,7 +2932,7 @@ async function runBuildCommand(options) {
2922
2932
  `Next.js static export (output: 'export') cannot build ${blocking}: it exports POST, and export mode only allows GET route handlers. The share link does not need this route. Delete ${blocking} (or move it out of this build) and run again.`
2923
2933
  );
2924
2934
  }
2925
- await import_fs4.promises.mkdir(path8.join(projectDir, "public"), { recursive: true });
2935
+ await import_fs5.promises.mkdir(path8.join(projectDir, "public"), { recursive: true });
2926
2936
  const pack2 = await packOutput({
2927
2937
  ...packOptions,
2928
2938
  projectDir,
@@ -2951,122 +2961,12 @@ async function runBuildCommand(options) {
2951
2961
  }
2952
2962
 
2953
2963
  // src/codemod/apply/cli.ts
2954
- var path14 = __toESM(require("path"), 1);
2964
+ var path16 = __toESM(require("path"), 1);
2955
2965
 
2956
- // src/codemod/apply/link-input.ts
2957
- var import_child_process3 = require("child_process");
2966
+ // src/codemod/apply/github-event.ts
2958
2967
  var fs6 = __toESM(require("fs"), 1);
2959
- var path9 = __toESM(require("path"), 1);
2960
- var MAX_APPLY_INPUT_BYTES = 1048576;
2961
- var TOO_LARGE_MESSAGE2 = "The input is larger than 1 MB.";
2962
- var NO_INPUT_MESSAGE = 'Pass the change link, a file that holds it, "-" to read stdin, or --clipboard. (cmd.exe cuts arguments at 8,191 characters; use --clipboard or a file for long links.)';
2963
- var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
2964
- var ApplyInputError = class extends Error {
2965
- constructor(message) {
2966
- super(message);
2967
- this.name = "ApplyInputError";
2968
- }
2969
- };
2970
- function checkSize(text) {
2971
- if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
2972
- throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2973
- }
2974
- return text;
2975
- }
2976
- async function readStream(stream) {
2977
- const chunks = [];
2978
- let size = 0;
2979
- for await (const chunk of stream) {
2980
- const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
2981
- size += bytes.length;
2982
- if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2983
- chunks.push(bytes);
2984
- }
2985
- return Buffer.concat(chunks).toString("utf8");
2986
- }
2987
- function existingFile(arg, cwd) {
2988
- try {
2989
- const resolved = path9.resolve(cwd, arg);
2990
- return fs6.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
2991
- } catch {
2992
- return null;
2993
- }
2994
- }
2995
- async function readApplyInput(arg, options) {
2996
- if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
2997
- if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
2998
- if (arg === "-") return readStream(options.stdin ?? process.stdin);
2999
- const file = existingFile(arg, options.cwd);
3000
- if (file === null) return checkSize(arg);
3001
- if (fs6.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
3002
- return checkSize(fs6.readFileSync(file, "utf8"));
3003
- }
3004
- function clipboardCommands(platform) {
3005
- if (platform === "win32") {
3006
- return [
3007
- ["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
3008
- ];
3009
- }
3010
- if (platform === "darwin") return [["pbpaste", []]];
3011
- return [
3012
- ["wl-paste", ["--no-newline"]],
3013
- ["xclip", ["-selection", "clipboard", "-o"]],
3014
- ["xsel", ["--clipboard", "--output"]]
3015
- ];
3016
- }
3017
- var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
3018
- function run(execFileImpl, [command, args]) {
3019
- return new Promise((resolve14) => {
3020
- try {
3021
- execFileImpl(
3022
- command,
3023
- args,
3024
- { encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
3025
- (error, stdout) => {
3026
- const code = error?.code;
3027
- if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve14(TOO_LARGE);
3028
- else resolve14(error ? null : String(stdout));
3029
- }
3030
- );
3031
- } catch {
3032
- resolve14(null);
3033
- }
3034
- });
3035
- }
3036
- async function readClipboard(execFileImpl = import_child_process3.execFile, platform = process.platform) {
3037
- for (const command of clipboardCommands(platform)) {
3038
- const text = await run(execFileImpl, command);
3039
- if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
3040
- if (text !== null) return checkSize(text);
3041
- }
3042
- throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
3043
- }
3044
-
3045
- // src/codemod/apply/local.ts
3046
- var import_child_process5 = require("child_process");
3047
- var fs9 = __toESM(require("fs"), 1);
3048
2968
  var os = __toESM(require("os"), 1);
3049
- var path13 = __toESM(require("path"), 1);
3050
-
3051
- // src/core/change-link-node.ts
3052
- var import_zlib = require("zlib");
3053
- var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
3054
- var TOO_LARGE_MESSAGE3 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
3055
- var nodeInflate = async (bytes, maxOutputLength) => {
3056
- let result;
3057
- try {
3058
- result = (0, import_zlib.inflateSync)(bytes, { maxOutputLength, info: true });
3059
- } catch (err) {
3060
- if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
3061
- throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE3);
3062
- }
3063
- throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3064
- }
3065
- if (result.engine.bytesWritten !== bytes.length) {
3066
- throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3067
- }
3068
- return result.buffer;
3069
- };
2969
+ var path10 = __toESM(require("path"), 1);
3070
2970
 
3071
2971
  // src/client/watermark.ts
3072
2972
  var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
@@ -3134,6 +3034,11 @@ function detectJsonIndent(content) {
3134
3034
  const ws = match[1];
3135
3035
  return ws.includes(" ") ? " " : ws;
3136
3036
  }
3037
+ function stringifyLike(data, content) {
3038
+ const indent = content === null ? " " : detectJsonIndent(content);
3039
+ const trailing = content === null || content.endsWith("\n") ? "\n" : "";
3040
+ return JSON.stringify(data, null, indent) + trailing;
3041
+ }
3137
3042
 
3138
3043
  // src/core/save-rules.ts
3139
3044
  var reject = (status, code, error) => ({
@@ -3339,109 +3244,1060 @@ function planApply(input) {
3339
3244
  return { edits, counts };
3340
3245
  }
3341
3246
 
3342
- // src/server/file-mutator.ts
3343
- var import_fs5 = require("fs");
3344
- var path11 = __toESM(require("path"), 1);
3345
-
3346
- // src/core/safe-path.ts
3347
- var path10 = __toESM(require("path"), 1);
3348
- function resolveSafeLocalePath(basePath, locale) {
3349
- if (!isSafeLocaleSegment(locale)) {
3350
- throw new Error("Path traversal attempt");
3247
+ // src/core/change-link-node.ts
3248
+ var import_zlib = require("zlib");
3249
+ var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
3250
+ var TOO_LARGE_MESSAGE2 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
3251
+ var nodeInflate = async (bytes, maxOutputLength) => {
3252
+ let result;
3253
+ try {
3254
+ result = (0, import_zlib.inflateSync)(bytes, { maxOutputLength, info: true });
3255
+ } catch (err) {
3256
+ if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
3257
+ throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE2);
3258
+ }
3259
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3351
3260
  }
3352
- const resolvedBasePath = path10.resolve(basePath);
3353
- const filePath = path10.resolve(resolvedBasePath, `${locale}.json`);
3354
- if (!filePath.startsWith(resolvedBasePath)) {
3355
- throw new Error("Path traversal attempt");
3261
+ if (result.engine.bytesWritten !== bytes.length) {
3262
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3356
3263
  }
3357
- return filePath;
3358
- }
3264
+ return result.buffer;
3265
+ };
3359
3266
 
3360
- // src/server/file-mutator.ts
3361
- var FileMutex = class {
3362
- queue = Promise.resolve();
3363
- async acquire(fn) {
3364
- const next = this.queue.then(fn);
3365
- this.queue = next.then(
3366
- () => {
3367
- },
3368
- () => {
3369
- }
3370
- );
3371
- return next;
3267
+ // src/server/github-rest.ts
3268
+ var GitHubApiError = class extends Error {
3269
+ constructor(message, status) {
3270
+ super(message);
3271
+ this.status = status;
3272
+ this.name = "GitHubApiError";
3372
3273
  }
3274
+ status;
3373
3275
  };
3374
- var FileMutator = class {
3375
- fileLocks = /* @__PURE__ */ new Map();
3376
- requireEnabled;
3377
- quiet;
3378
- scanner;
3379
- constructor(options = {}) {
3380
- this.requireEnabled = options.requireEnabled ?? true;
3381
- this.quiet = options.quiet ?? false;
3382
- this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
3276
+ var CHUNK_BYTES = 32768;
3277
+ function encodeBase64Utf8(text) {
3278
+ const bytes = new TextEncoder().encode(text);
3279
+ let binary = "";
3280
+ for (let i = 0; i < bytes.length; i += CHUNK_BYTES) {
3281
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_BYTES));
3383
3282
  }
3384
- assertEnabled() {
3385
- if (!this.requireEnabled) return;
3386
- const reason = getDisabledReason();
3387
- if (reason !== null) {
3388
- throw new Error(
3389
- `[livelocale] FileMutator can only be executed in development mode (${reason}).`
3283
+ return btoa(binary);
3284
+ }
3285
+ function decodeBase64Utf8(base64) {
3286
+ const binary = atob(base64.replace(/\s/g, ""));
3287
+ const bytes = new Uint8Array(binary.length);
3288
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3289
+ return new TextDecoder().decode(bytes);
3290
+ }
3291
+ function isPullRequestCreationForbidden(err) {
3292
+ return err instanceof GitHubApiError && err.status === 403 && /not permitted to create or approve pull requests/i.test(err.message);
3293
+ }
3294
+ function encodeRefPath(ref) {
3295
+ return ref.split("/").map(encodeURIComponent).join("/");
3296
+ }
3297
+ function createGitHubRest(config) {
3298
+ const repo = config.repo;
3299
+ const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
3300
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
3301
+ const owner = repo.split("/")[0] ?? "";
3302
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
3303
+ throw new Error(`[livelocale] github.repo must be "owner/name", got "${repo}".`);
3304
+ }
3305
+ if (typeof fetchImpl !== "function") {
3306
+ throw new Error("[livelocale] The GitHub gateway needs a global fetch (Node 18+).");
3307
+ }
3308
+ const headers = (hasBody) => ({
3309
+ Authorization: `Bearer ${config.token}`,
3310
+ Accept: "application/vnd.github+json",
3311
+ "X-GitHub-Api-Version": "2022-11-28",
3312
+ ...hasBody ? { "Content-Type": "application/json" } : {}
3313
+ });
3314
+ async function api(method, path19, options = {}) {
3315
+ const url = new URL(`${apiBase}/repos/${repo}${path19}`);
3316
+ for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
3317
+ const response = await fetchImpl(url.toString(), {
3318
+ method,
3319
+ headers: headers(options.body !== void 0),
3320
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
3321
+ });
3322
+ let data = null;
3323
+ try {
3324
+ data = await response.json();
3325
+ } catch {
3326
+ }
3327
+ if (response.status === 404 && options.allow404) return { status: 404, data: null };
3328
+ if (!response.ok) {
3329
+ const detail = data?.message || "request failed";
3330
+ const hint = /not accessible by/i.test(detail) ? ' The token lacks a permission: give it "Contents" and "Pull requests" with "Read and write" on this repository.' : "";
3331
+ throw new GitHubApiError(
3332
+ `GitHub API ${response.status} on ${method} ${path19}: ${detail}.${hint}`,
3333
+ response.status
3390
3334
  );
3391
3335
  }
3336
+ return { status: response.status, data };
3392
3337
  }
3393
- /**
3394
- * Helper to serialize execution per file path
3395
- */
3396
- async withLock(filePath, fn) {
3397
- let lock = this.fileLocks.get(filePath);
3398
- if (!lock) {
3399
- lock = new FileMutex();
3400
- this.fileLocks.set(filePath, lock);
3338
+ async function probeStatus(method, path19, body) {
3339
+ const response = await fetchImpl(`${apiBase}/repos/${repo}${path19}`, {
3340
+ method,
3341
+ headers: headers(body !== void 0),
3342
+ body: body !== void 0 ? JSON.stringify(body) : void 0
3343
+ });
3344
+ try {
3345
+ await response.arrayBuffer();
3346
+ } catch {
3401
3347
  }
3402
- return lock.acquire(fn);
3348
+ return response.status;
3403
3349
  }
3404
- /**
3405
- * Updates a locale JSON file on disk atomically and safely.
3406
- * Enforces development mode checks, path traversal mitigation, key depth restrictions,
3407
- * structural JSON validation, and concurrency serialization.
3408
- */
3409
- async updateLocaleKey(basePath, locale, keyPath, newValue) {
3410
- this.assertEnabled();
3411
- if (!isSafeLocaleSegment(locale)) {
3350
+ async function refSha(branch) {
3351
+ const res = await api(
3352
+ "GET",
3353
+ `/git/ref/heads/${encodeRefPath(branch)}`,
3354
+ { allow404: true }
3355
+ );
3356
+ return res.data?.object?.sha ?? null;
3357
+ }
3358
+ async function findOpenPullRequest(head, base) {
3359
+ const res = await api("GET", "/pulls", {
3360
+ query: { head: `${owner}:${head}`, base, state: "open" }
3361
+ });
3362
+ const pull = res.data?.[0];
3363
+ return pull ? { number: pull.number, url: pull.html_url } : null;
3364
+ }
3365
+ async function openOrReusePullRequest(input) {
3366
+ const existing = await findOpenPullRequest(input.head, input.base);
3367
+ if (existing) {
3368
+ if (input.updateBodyIfReused) {
3369
+ await api("PATCH", `/pulls/${existing.number}`, { body: { body: input.body } });
3370
+ }
3371
+ return { ...existing, reused: true };
3372
+ }
3373
+ const res = await api("POST", "/pulls", {
3374
+ body: { title: input.title, head: input.head, base: input.base, body: input.body }
3375
+ });
3376
+ return { number: res.data.number, url: res.data.html_url, reused: false };
3377
+ }
3378
+ return { repo, api, probeStatus, refSha, findOpenPullRequest, openOrReusePullRequest };
3379
+ }
3380
+
3381
+ // src/codemod/apply/github-commit.ts
3382
+ var path9 = __toESM(require("path"), 1);
3383
+ var BRANCH_PREFIX = "livelocale/";
3384
+ function normalizeRepoDir(p) {
3385
+ const posix2 = p.replace(/\\/g, "/");
3386
+ if (posix2.startsWith("/") || /^[A-Za-z]:/.test(posix2)) {
3387
+ throw new Error(`[livelocale] The locales path must be relative to the repository: "${p}".`);
3388
+ }
3389
+ const segments = posix2.split("/").filter((s) => s !== "" && s !== ".");
3390
+ if (segments.includes("..")) {
3391
+ throw new Error(`[livelocale] The locales path must stay inside the repository: "${p}".`);
3392
+ }
3393
+ return segments.join("/");
3394
+ }
3395
+ function joinRepoPath(dir, rest) {
3396
+ return dir === "" ? rest : `${dir}/${rest}`;
3397
+ }
3398
+ function localeFileFor(localesDir, layout, locale, key) {
3399
+ const dir = normalizeRepoDir(localesDir);
3400
+ if (layout === "flat") return { file: joinRepoPath(dir, `${locale}.json`), innerKey: key };
3401
+ const dot = key.indexOf(".");
3402
+ if (dot <= 0 || dot === key.length - 1) {
3403
+ throw new Error(`[livelocale] "${key}" names no key inside a namespace file.`);
3404
+ }
3405
+ return {
3406
+ file: joinRepoPath(dir, `${locale}/${key.slice(0, dot)}.json`),
3407
+ innerKey: key.slice(dot + 1)
3408
+ };
3409
+ }
3410
+ function isPlainObject4(value) {
3411
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3412
+ }
3413
+ function applyEditsToText(text, edits) {
3414
+ const data = JSON.parse(text);
3415
+ if (!isPlainObject4(data)) throw new Error("[livelocale] A locale file is not a JSON object.");
3416
+ for (const edit of edits) setNestedProperty(data, edit.innerKey, edit.value);
3417
+ return stringifyLike(data, text);
3418
+ }
3419
+ async function listDir(rest, dir, ref) {
3420
+ const res = await rest.api("GET", `/contents/${encodeRefPath(dir)}`, {
3421
+ query: { ref },
3422
+ allow404: true
3423
+ });
3424
+ return Array.isArray(res.data) ? res.data : null;
3425
+ }
3426
+ async function readBlob(rest, sha) {
3427
+ const res = await rest.api(
3428
+ "GET",
3429
+ `/git/blobs/${encodeURIComponent(sha)}`
3430
+ );
3431
+ return decodeBase64Utf8(res.data?.content ?? "");
3432
+ }
3433
+ function parseLocaleFile(text, file) {
3434
+ let data;
3435
+ try {
3436
+ data = JSON.parse(text);
3437
+ } catch {
3438
+ throw new Error(`[livelocale] ${file} is not valid JSON.`);
3439
+ }
3440
+ if (!isPlainObject4(data)) throw new Error(`[livelocale] ${file} is not a JSON object.`);
3441
+ return data;
3442
+ }
3443
+ async function readRemoteLocales(rest, commitSha, localesDir, locales) {
3444
+ const dir = normalizeRepoDir(localesDir);
3445
+ const entries = await listDir(rest, dir, commitSha);
3446
+ if (entries === null) {
3447
+ throw new Error(`[livelocale] No locales folder at ${dir || "."} on the target.`);
3448
+ }
3449
+ const targets = /* @__PURE__ */ new Map();
3450
+ const files = /* @__PURE__ */ new Map();
3451
+ for (const locale of locales) {
3452
+ if (targets.has(locale)) continue;
3453
+ const flat = entries.find((e) => e.type === "file" && e.name === `${locale}.json`);
3454
+ const folder = entries.find((e) => e.type === "dir" && e.name === locale);
3455
+ if (!isValidLocaleFormat(locale) || !flat && !folder) {
3456
+ targets.set(locale, null);
3457
+ continue;
3458
+ }
3459
+ if (flat) {
3460
+ const file = joinRepoPath(dir, flat.name);
3461
+ const text = await readBlob(rest, flat.sha);
3462
+ files.set(file, text);
3463
+ targets.set(locale, { layout: "flat", dict: parseLocaleFile(text, file) });
3464
+ continue;
3465
+ }
3466
+ const dict = {};
3467
+ for (const entry of await listDir(rest, joinRepoPath(dir, locale), commitSha) ?? []) {
3468
+ if (entry.type !== "file" || !entry.name.endsWith(".json")) continue;
3469
+ const file = joinRepoPath(dir, `${locale}/${entry.name}`);
3470
+ const text = await readBlob(rest, entry.sha);
3471
+ files.set(file, text);
3472
+ dict[entry.name.slice(0, -".json".length)] = parseLocaleFile(text, file);
3473
+ }
3474
+ targets.set(locale, { layout: "namespaced", dict });
3475
+ }
3476
+ return { sha: commitSha, targets, files };
3477
+ }
3478
+ function buildFileChanges(localesDir, plan, remote, baseFiles) {
3479
+ const dir = normalizeRepoDir(localesDir);
3480
+ const prefix = dir === "" ? "" : `${dir}/`;
3481
+ const byFile = /* @__PURE__ */ new Map();
3482
+ for (const edit of plan.edits) {
3483
+ if (edit.status !== "apply") continue;
3484
+ const layout = remote.targets.get(edit.locale)?.layout ?? "flat";
3485
+ const { file, innerKey } = localeFileFor(dir, layout, edit.locale, edit.key);
3486
+ const normalized = path9.posix.normalize(file);
3487
+ if (normalized !== file || normalized.startsWith("../") || normalized.startsWith("/") || !normalized.startsWith(prefix)) {
3412
3488
  throw new Error(
3413
- `[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
3489
+ `[livelocale] Refusing to write ${file}: it is outside the locales folder ${dir || "."}.`
3414
3490
  );
3415
3491
  }
3416
- const resolvedBasePath = path11.resolve(basePath);
3417
- const localeDir = path11.resolve(resolvedBasePath, locale);
3418
- let filePath = "";
3419
- let mutationKeyPath = keyPath;
3420
- let isNamespaceDir = false;
3421
- try {
3422
- const stats = await import_fs5.promises.stat(localeDir);
3423
- if (stats.isDirectory()) {
3424
- isNamespaceDir = true;
3425
- }
3426
- } catch {
3492
+ const list = byFile.get(file) ?? [];
3493
+ list.push({ innerKey, value: edit.to });
3494
+ byFile.set(file, list);
3495
+ }
3496
+ const changes = /* @__PURE__ */ new Map();
3497
+ for (const [file, edits] of byFile) {
3498
+ const text = baseFiles?.get(file) ?? remote.files.get(file);
3499
+ if (text === void 0) {
3500
+ throw new Error(`[livelocale] ${file} was not read from the target.`);
3427
3501
  }
3428
- if (isNamespaceDir) {
3429
- const firstDot = keyPath.indexOf(".");
3430
- if (firstDot !== -1) {
3431
- const namespace = keyPath.substring(0, firstDot);
3502
+ const next = applyEditsToText(text, edits);
3503
+ if (next !== text) changes.set(file, next);
3504
+ }
3505
+ return changes;
3506
+ }
3507
+ async function prepareBranch(rest, baseName, base, targetSha, runId) {
3508
+ const head = await rest.refSha(baseName);
3509
+ if (head === null) return { branch: baseName, parentSha: targetSha, mode: "new" };
3510
+ if (await rest.findOpenPullRequest(baseName, base)) {
3511
+ return { branch: baseName, parentSha: head, mode: "append" };
3512
+ }
3513
+ return { branch: `${baseName}-${runId}`, parentSha: targetSha, mode: "new" };
3514
+ }
3515
+ async function commitFiles(rest, input) {
3516
+ const { branch, parentSha, mode } = input.branchPlan;
3517
+ if (!branch.startsWith(BRANCH_PREFIX)) {
3518
+ throw new Error(`[livelocale] Refusing to write "${branch}": only ${BRANCH_PREFIX}* branches.`);
3519
+ }
3520
+ if (input.files.size === 0) return { sha: null, unchanged: true };
3521
+ const parent = await rest.api(
3522
+ "GET",
3523
+ `/git/commits/${encodeURIComponent(parentSha)}`
3524
+ );
3525
+ const baseTree = parent.data.tree.sha;
3526
+ const entries = [];
3527
+ for (const [file, text] of input.files) {
3528
+ const blob = await rest.api("POST", "/git/blobs", {
3529
+ body: { content: encodeBase64Utf8(text), encoding: "base64" }
3530
+ });
3531
+ entries.push({ path: file, mode: "100644", type: "blob", sha: blob.data.sha });
3532
+ }
3533
+ const tree = await rest.api("POST", "/git/trees", {
3534
+ body: { base_tree: baseTree, tree: entries }
3535
+ });
3536
+ const treeSha = tree.data.sha;
3537
+ if (treeSha === baseTree) return { sha: null, unchanged: true };
3538
+ const commit = await rest.api("POST", "/git/commits", {
3539
+ body: { message: input.message, tree: treeSha, parents: [parentSha], author: input.author }
3540
+ });
3541
+ const sha = commit.data.sha;
3542
+ if (mode === "new") {
3543
+ await rest.api("POST", "/git/refs", { body: { ref: `refs/heads/${branch}`, sha } });
3544
+ } else {
3545
+ await rest.api("PATCH", `/git/refs/heads/${encodeRefPath(branch)}`, {
3546
+ body: { sha, force: false }
3547
+ });
3548
+ }
3549
+ return { sha };
3550
+ }
3551
+ function noreplyAuthor(actor) {
3552
+ return { name: actor.login, email: `${actor.id}+${actor.login}@users.noreply.github.com` };
3553
+ }
3554
+
3555
+ // src/codemod/apply/github-trigger.ts
3556
+ var APPLY_COMMAND_RE = /^\/livelocale\s+apply(\s+--force)?\s*$/;
3557
+ var COMMAND_PREFIX = "/livelocale";
3558
+ var ISSUE_BRANCH_RE = /^livelocale\/issue-(\d+)(?:-\d+)?$/;
3559
+ var TRUSTED_ASSOCIATIONS = /* @__PURE__ */ new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
3560
+ var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["admin", "write"]);
3561
+ var TRUSTED_ROLES = /* @__PURE__ */ new Set(["admin", "maintain", "write"]);
3562
+ var ignore = (reason) => ({ kind: "ignore", reason });
3563
+ function actorOf(user) {
3564
+ return { login: String(user?.login ?? ""), id: Number(user?.id ?? 0) };
3565
+ }
3566
+ function textOf(value) {
3567
+ return typeof value === "string" ? value : "";
3568
+ }
3569
+ function holdsChangeToken(text) {
3570
+ try {
3571
+ findChangeToken(text);
3572
+ return true;
3573
+ } catch {
3574
+ return false;
3575
+ }
3576
+ }
3577
+ function parseTrigger(eventName, event) {
3578
+ const repo = textOf(event?.repository?.full_name);
3579
+ const defaultBranch = textOf(event?.repository?.default_branch);
3580
+ switch (eventName) {
3581
+ case "issues": {
3582
+ if (event?.action !== "opened") return ignore("issue not opened");
3583
+ const issue = event.issue;
3584
+ const body = textOf(issue?.body);
3585
+ if (!body.includes(CHANGE_BLOCK_OPEN)) return ignore("no change block");
3586
+ return {
3587
+ kind: "apply",
3588
+ via: "issue",
3589
+ issueNumber: Number(issue.number),
3590
+ actor: actorOf(issue.user),
3591
+ association: issue.author_association ?? null,
3592
+ text: body,
3593
+ force: false,
3594
+ repo,
3595
+ defaultBranch
3596
+ };
3597
+ }
3598
+ case "issue_comment": {
3599
+ if (event?.action !== "created") return ignore("comment not created");
3600
+ const issue = event.issue;
3601
+ const comment = event.comment;
3602
+ if (issue?.pull_request) return ignore("comment on a pull request");
3603
+ if (comment?.user?.type === "Bot") return ignore("comment by a bot");
3604
+ const body = textOf(comment?.body);
3605
+ const firstLine = body.split(/\r\n|\r|\n/)[0] ?? "";
3606
+ if (!firstLine.startsWith(COMMAND_PREFIX)) return ignore("not a livelocale command");
3607
+ const command = APPLY_COMMAND_RE.exec(firstLine);
3608
+ if (!command) return ignore("unknown command");
3609
+ return {
3610
+ kind: "apply",
3611
+ via: "comment",
3612
+ issueNumber: Number(issue.number),
3613
+ // The gate checks the commenter, never the issue author.
3614
+ actor: actorOf(comment.user),
3615
+ association: comment.author_association ?? null,
3616
+ text: holdsChangeToken(body) ? body : textOf(issue?.body),
3617
+ force: command[1] !== void 0,
3618
+ repo,
3619
+ defaultBranch
3620
+ };
3621
+ }
3622
+ case "workflow_dispatch":
3623
+ return {
3624
+ kind: "apply",
3625
+ via: "dispatch",
3626
+ issueNumber: null,
3627
+ actor: actorOf(event?.sender),
3628
+ association: null,
3629
+ text: textOf(event?.inputs?.link),
3630
+ force: false,
3631
+ repo,
3632
+ defaultBranch
3633
+ };
3634
+ case "pull_request": {
3635
+ if (event?.action !== "closed") return ignore("pull request not closed");
3636
+ const pull = event.pull_request;
3637
+ const branch = ISSUE_BRANCH_RE.exec(textOf(pull?.head?.ref));
3638
+ if (!branch) return ignore("not a livelocale issue branch");
3639
+ if (textOf(pull?.head?.repo?.full_name) !== repo || repo === "") {
3640
+ return ignore("pull request from another repository");
3641
+ }
3642
+ return {
3643
+ kind: "close-issue",
3644
+ issueNumber: Number(branch[1]),
3645
+ prUrl: textOf(pull.html_url),
3646
+ merged: pull.merged === true,
3647
+ repo
3648
+ };
3649
+ }
3650
+ default:
3651
+ return ignore(`unsupported event ${eventName}`);
3652
+ }
3653
+ }
3654
+ async function isTrustedActor(rest, login, association) {
3655
+ if (association !== null && TRUSTED_ASSOCIATIONS.has(association)) return true;
3656
+ try {
3657
+ const res = await rest.api(
3658
+ "GET",
3659
+ `/collaborators/${encodeURIComponent(login)}/permission`
3660
+ );
3661
+ const permission = res.data?.permission ?? "";
3662
+ const role = res.data?.role_name ?? "";
3663
+ return TRUSTED_PERMISSIONS.has(permission) || TRUSTED_ROLES.has(role);
3664
+ } catch (err) {
3665
+ if (err instanceof GitHubApiError && (err.status === 403 || err.status === 404)) return false;
3666
+ throw err;
3667
+ }
3668
+ }
3669
+ function chooseTargetBranch(requested, allowList, defaultBranch) {
3670
+ if (!requested) return { branch: defaultBranch, reason: "default" };
3671
+ const allowed = allowList.length > 0 ? allowList : [defaultBranch];
3672
+ if (allowed.includes(requested)) return { branch: requested, reason: "link" };
3673
+ return { branch: defaultBranch, reason: "not-allowed" };
3674
+ }
3675
+ function branchBaseName(t, runId) {
3676
+ return t.issueNumber !== null ? `livelocale/issue-${t.issueNumber}` : `livelocale/run-${runId}`;
3677
+ }
3678
+
3679
+ // src/codemod/apply/report.ts
3680
+ var DEFAULT_SPAN_LENGTH = 120;
3681
+ var DEFAULT_TABLE_ROWS = 100;
3682
+ var MAX_BODY_CHARS = 6e4;
3683
+ var MAX_NOTE_CHARS2 = 2e3;
3684
+ var MAX_NAME_CHARS = 64;
3685
+ var MAX_CHECK_LINES = 50;
3686
+ var MAX_CHECK_SUMMARY_CHARS = 1e4;
3687
+ var BODY_MARGIN = 200;
3688
+ var STATUS_LABELS = {
3689
+ apply: "applied",
3690
+ already: "already done",
3691
+ conflict: "conflict",
3692
+ "needs-force": "needs --force",
3693
+ invalid: "rejected"
3694
+ };
3695
+ var BIDI_CONTROL_RE = /[‪-‮⁦-⁩]/g;
3696
+ var NEWLINE_RE = /\r\n|\r|\n/g;
3697
+ var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
3698
+ var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
3699
+ function cut(text, max) {
3700
+ if (text.length <= max) return text;
3701
+ let out = text.slice(0, max);
3702
+ const last = out.charCodeAt(out.length - 1);
3703
+ if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
3704
+ return out;
3705
+ }
3706
+ function cutLines(text, max) {
3707
+ if (text.length <= max) return text;
3708
+ const kept = [];
3709
+ let length = 0;
3710
+ for (const line of text.split("\n")) {
3711
+ const added = (kept.length > 0 ? 1 : 0) + line.length;
3712
+ if (length + added + 2 > max) break;
3713
+ kept.push(line);
3714
+ length += added;
3715
+ }
3716
+ return [...kept, "\u2026"].join("\n");
3717
+ }
3718
+ function longestBacktickRun(text) {
3719
+ let longest = 0;
3720
+ for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
3721
+ return longest;
3722
+ }
3723
+ function codePointLabel(ch) {
3724
+ return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
3725
+ }
3726
+ function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
3727
+ const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
3728
+ const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
3729
+ if (content === "") return "` `";
3730
+ const fence = "`".repeat(longestBacktickRun(content) + 1);
3731
+ const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
3732
+ return `${fence}${pad}${content}${pad}${fence}`;
3733
+ }
3734
+ function sanitizeName(name) {
3735
+ if (name === void 0) return void 0;
3736
+ const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
3737
+ const result = cut(cleaned, MAX_NAME_CHARS).trim();
3738
+ return result === "" ? void 0 : result;
3739
+ }
3740
+ var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
3741
+ function summarizeCheck(report, edited) {
3742
+ const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
3743
+ const mine = report.issues.filter(
3744
+ (issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
3745
+ );
3746
+ if (mine.length === 0) return NO_CHECK_PROBLEMS;
3747
+ const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
3748
+ for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
3749
+ lines.push(
3750
+ `- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
3751
+ );
3752
+ }
3753
+ if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
3754
+ const others = report.issues.length - mine.length;
3755
+ if (others > 0) {
3756
+ lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
3757
+ }
3758
+ return lines.join("\n");
3759
+ }
3760
+ function checksCell(edit) {
3761
+ const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
3762
+ if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
3763
+ parts.push(codeSpan(edit.message));
3764
+ }
3765
+ return parts.join("; ");
3766
+ }
3767
+ function renderRow(edit) {
3768
+ const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
3769
+ const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
3770
+ const cells = [
3771
+ STATUS_LABELS[edit.status],
3772
+ codeSpan(edit.locale),
3773
+ codeSpan(edit.key),
3774
+ before,
3775
+ after,
3776
+ checksCell(edit)
3777
+ ];
3778
+ return `| ${cells.join(" | ")} |`;
3779
+ }
3780
+ function renderTable(plan, limit, maxChars) {
3781
+ const lines = [
3782
+ "| Status | Locale | Key | Before | After | Checks |",
3783
+ "| --- | --- | --- | --- | --- | --- |"
3784
+ ];
3785
+ const moreLine = (n) => `
3786
+
3787
+ \u2026and ${n} more`;
3788
+ let length = lines.join("\n").length + moreLine(plan.edits.length).length;
3789
+ let shown = 0;
3790
+ for (const edit of plan.edits) {
3791
+ if (shown >= limit) break;
3792
+ const row = renderRow(edit);
3793
+ if (length + row.length + 1 > maxChars) break;
3794
+ lines.push(row);
3795
+ length += row.length + 1;
3796
+ shown += 1;
3797
+ }
3798
+ const rest = plan.edits.length - shown;
3799
+ return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
3800
+ }
3801
+ function fencedNote(note) {
3802
+ const text = cut(note, MAX_NOTE_CHARS2);
3803
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
3804
+ return `${fence}text
3805
+ ${text}
3806
+ ${fence}`;
3807
+ }
3808
+ function closingLine(ctx) {
3809
+ if (ctx.issueNumber === void 0) return void 0;
3810
+ if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
3811
+ return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
3812
+ }
3813
+ function targetNote(target) {
3814
+ if (target?.reason !== "not-allowed" || target.requested === void 0) return void 0;
3815
+ return `The link was made from ${codeSpan(target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(target.branch)}.`;
3816
+ }
3817
+ function creditLines(ctx, withTarget) {
3818
+ const lines = [];
3819
+ if (ctx.by?.trim()) lines.push(`Translated by ${codeSpan(ctx.by)}`);
3820
+ const target = withTarget ? targetNote(ctx.target) : void 0;
3821
+ if (target) lines.push(target);
3822
+ if (ctx.note?.trim()) lines.push(fencedNote(ctx.note));
3823
+ return lines;
3824
+ }
3825
+ function renderPullRequestBody(ctx) {
3826
+ const head = [
3827
+ "Applies translations sent with a LiveLocale change link.",
3828
+ ...creditLines(ctx, true)
3829
+ ];
3830
+ const tail = [];
3831
+ if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
3832
+ if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
3833
+ const closing = closingLine(ctx);
3834
+ if (closing) tail.push(closing);
3835
+ const fixed = [...head, ...tail].join("\n\n").length;
3836
+ const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
3837
+ return [...head, table, ...tail].join("\n\n");
3838
+ }
3839
+ function renderCommitMessage(input) {
3840
+ let subject = "i18n: apply LiveLocale changes";
3841
+ if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
3842
+ else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
3843
+ const applied = input.plan.edits.filter((edit) => edit.status === "apply");
3844
+ const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
3845
+ const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
3846
+ const name = sanitizeName(input.by);
3847
+ if (name) parts.push(`Translated-by: ${name}`);
3848
+ return parts.join("\n\n");
3849
+ }
3850
+ function renderTerminalSummary(plan) {
3851
+ const c = plan.counts;
3852
+ const lines = [
3853
+ `Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
3854
+ ];
3855
+ for (const edit of plan.edits) {
3856
+ lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
3857
+ }
3858
+ return lines.join("\n");
3859
+ }
3860
+ var MAX_ERROR_MESSAGE_CHARS = 2e3;
3861
+ function needsForceHint(plan) {
3862
+ const n = plan?.counts["needs-force"] ?? 0;
3863
+ if (n === 0) return void 0;
3864
+ return `${n} change(s) were saved with "Save anyway". A maintainer can apply them with \`/livelocale apply --force\`.`;
3865
+ }
3866
+ function withTable(head, plan, tail) {
3867
+ if (!plan) return [...head, ...tail].join("\n\n");
3868
+ const fixed = [...head, ...tail].join("\n\n").length;
3869
+ const table = renderTable(plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
3870
+ return [...head, table, ...tail].join("\n\n");
3871
+ }
3872
+ function checkTail(checkSummary) {
3873
+ return checkSummary ? [cutLines(checkSummary, MAX_CHECK_SUMMARY_CHARS)] : [];
3874
+ }
3875
+ function renderIssueComment(ctx) {
3876
+ const { outcome } = ctx;
3877
+ switch (outcome.kind) {
3878
+ case "waiting-for-maintainer":
3879
+ return "Waiting for a maintainer: comment `/livelocale apply`";
3880
+ case "wrong-repo":
3881
+ return `This change link was made for ${codeSpan(outcome.linkRepo)}, not ${codeSpan(outcome.repo)}. Nothing was applied.`;
3882
+ case "error": {
3883
+ const message = cut(outcome.message, MAX_ERROR_MESSAGE_CHARS).replace(/\.$/, "");
3884
+ const runLog = ctx.runUrl ? ` Run log: ${ctx.runUrl}` : "";
3885
+ return `LiveLocale could not apply this link: ${message}.${runLog}`;
3886
+ }
3887
+ case "nothing-to-apply": {
3888
+ const hint = needsForceHint(ctx.plan);
3889
+ return withTable(
3890
+ ["Nothing to apply.", ...creditLines(ctx, false)],
3891
+ ctx.plan,
3892
+ hint ? [hint] : []
3893
+ );
3894
+ }
3895
+ case "pr": {
3896
+ const headline = `${outcome.reused ? "Updated pull request" : "Pull request"}: ${outcome.url}`;
3897
+ return withTable(
3898
+ [headline, ...creditLines(ctx, true)],
3899
+ ctx.plan,
3900
+ checkTail(ctx.checkSummary)
3901
+ );
3902
+ }
3903
+ case "compare": {
3904
+ const headline = [
3905
+ `This repository does not let GitHub Actions open pull requests, so the branch ${codeSpan(outcome.branch)} was pushed instead. Create the pull request with one click: ${outcome.url}`,
3906
+ '(Or allow it once: Settings \u2192 Actions \u2192 General \u2192 "Allow GitHub Actions to create and approve pull requests".)'
3907
+ ];
3908
+ return withTable(
3909
+ [...headline, ...creditLines(ctx, true)],
3910
+ ctx.plan,
3911
+ checkTail(ctx.checkSummary)
3912
+ );
3913
+ }
3914
+ }
3915
+ }
3916
+
3917
+ // src/codemod/apply/github-event.ts
3918
+ var PR_TITLE = "i18n: LiveLocale changes";
3919
+ var NOT_FOUND_MESSAGE = "No LiveLocale change link (lc1.\u2026) found.";
3920
+ var ExpectedFailure = class extends Error {
3921
+ };
3922
+ function errorMessage(err) {
3923
+ return err instanceof Error ? err.message : String(err);
3924
+ }
3925
+ function required(env, name) {
3926
+ const value = env[name];
3927
+ if (!value) throw new Error(`[livelocale] ${name} is not set; run this inside GitHub Actions.`);
3928
+ return value;
3929
+ }
3930
+ async function closeIssue(rest, trigger, log) {
3931
+ const n = trigger.issueNumber;
3932
+ const issue = await rest.api("GET", `/issues/${n}`, {
3933
+ allow404: true
3934
+ });
3935
+ const body = typeof issue.data?.body === "string" ? issue.data.body : "";
3936
+ if (issue.data?.state !== "open" || !body.includes(CHANGE_BLOCK_OPEN)) {
3937
+ log(`Issue #${n} is not an open LiveLocale issue; nothing to close.`);
3938
+ return "ignored";
3939
+ }
3940
+ await rest.api("PATCH", `/issues/${n}`, {
3941
+ body: { state: "closed", state_reason: trigger.merged ? "completed" : "not_planned" }
3942
+ });
3943
+ await rest.api("POST", `/issues/${n}/comments`, {
3944
+ body: { body: `Closed because ${trigger.prUrl} was ${trigger.merged ? "merged" : "closed"}.` }
3945
+ });
3946
+ log(`Closed issue #${n}.`);
3947
+ return "closed-issue";
3948
+ }
3949
+ async function checkChanges(options, workspace, remote, changes, plan) {
3950
+ const dir = normalizeRepoDir(options.localesPath);
3951
+ const prefix = dir === "" ? "" : `${dir}/`;
3952
+ const tmp = fs6.mkdtempSync(path10.join(os.tmpdir(), "livelocale-check-"));
3953
+ try {
3954
+ for (const [file, text] of new Map([...remote.files, ...changes])) {
3955
+ const target = path10.join(tmp, ...file.slice(prefix.length).split("/"));
3956
+ fs6.mkdirSync(path10.dirname(target), { recursive: true });
3957
+ fs6.writeFileSync(target, text, "utf-8");
3958
+ }
3959
+ const report = await runCheck({
3960
+ projectDir: path10.join(workspace, options.dir ?? "."),
3961
+ localesPath: tmp,
3962
+ sourceLocale: options.sourceLocale,
3963
+ unused: false
3964
+ });
3965
+ const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
3966
+ return summarizeCheck(report, edited);
3967
+ } catch (err) {
3968
+ return `Check could not run: ${errorMessage(err)}`;
3969
+ } finally {
3970
+ fs6.rmSync(tmp, { recursive: true, force: true });
3971
+ }
3972
+ }
3973
+ async function runGitHubEventApply(options) {
3974
+ const { env } = options;
3975
+ const log = options.log ?? ((line) => console.log(line));
3976
+ const token = required(env, "GITHUB_TOKEN");
3977
+ const eventPath = required(env, "GITHUB_EVENT_PATH");
3978
+ const event = JSON.parse(fs6.readFileSync(eventPath, "utf-8"));
3979
+ const trigger = parseTrigger(env.GITHUB_EVENT_NAME ?? "", event);
3980
+ if (trigger.kind === "ignore") {
3981
+ log(`Nothing to do: ${trigger.reason}.`);
3982
+ return "ignored";
3983
+ }
3984
+ const repo = env.GITHUB_REPOSITORY || trigger.repo;
3985
+ const rest = createGitHubRest({
3986
+ repo,
3987
+ token,
3988
+ apiBase: env.GITHUB_API_URL || void 0,
3989
+ fetchImpl: options.fetchImpl
3990
+ });
3991
+ if (trigger.kind === "close-issue") return closeIssue(rest, trigger, log);
3992
+ const server = (env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, "");
3993
+ const runId = env.GITHUB_RUN_ID ?? "0";
3994
+ const runUrl = `${server}/${repo}/actions/runs/${runId}`;
3995
+ const issueNumber = trigger.issueNumber ?? void 0;
3996
+ const report = async (outcome, ctx = {}) => {
3997
+ const text = renderIssueComment({ ...ctx, runUrl, outcome });
3998
+ if (issueNumber !== void 0) {
3999
+ await rest.api("POST", `/issues/${issueNumber}/comments`, { body: { body: text } });
4000
+ } else if (env.GITHUB_STEP_SUMMARY) {
4001
+ fs6.appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}
4002
+ `, "utf-8");
4003
+ }
4004
+ log(`Result: ${outcome.kind}`);
4005
+ return outcome.kind;
4006
+ };
4007
+ if (trigger.via !== "dispatch") {
4008
+ if (!await isTrustedActor(rest, trigger.actor.login, trigger.association)) {
4009
+ return report({ kind: "waiting-for-maintainer" });
4010
+ }
4011
+ }
4012
+ try {
4013
+ return await applyLink(options, rest, trigger, { repo, runId, runUrl, server, log, report });
4014
+ } catch (err) {
4015
+ if (err instanceof ExpectedFailure) return report({ kind: "error", message: err.message });
4016
+ await report({ kind: "error", message: errorMessage(err) });
4017
+ throw err;
4018
+ }
4019
+ }
4020
+ async function applyLink(options, rest, trigger, run3) {
4021
+ const { repo, runId, runUrl, log, report } = run3;
4022
+ const issueNumber = trigger.issueNumber ?? void 0;
4023
+ let set;
4024
+ try {
4025
+ set = await decodeChangeLink(trigger.text, nodeInflate);
4026
+ } catch (err) {
4027
+ if (!(err instanceof ChangeLinkError)) throw err;
4028
+ throw new ExpectedFailure(err.code === "NOT_FOUND" ? NOT_FOUND_MESSAGE : err.message);
4029
+ }
4030
+ if (set.repo !== void 0 && set.repo.toLowerCase() !== repo.toLowerCase()) {
4031
+ return report({ kind: "wrong-repo", linkRepo: set.repo, repo });
4032
+ }
4033
+ const defaultBranch = trigger.defaultBranch;
4034
+ const chosen = chooseTargetBranch(set.branch, options.branches ?? [defaultBranch], defaultBranch);
4035
+ const target = {
4036
+ branch: chosen.branch,
4037
+ isDefault: chosen.branch === defaultBranch,
4038
+ ...set.branch ? { requested: set.branch } : {},
4039
+ reason: chosen.reason
4040
+ };
4041
+ const targetSha = await rest.refSha(target.branch);
4042
+ if (targetSha === null) {
4043
+ throw new Error(`[livelocale] The target branch ${target.branch} does not exist.`);
4044
+ }
4045
+ const locales = [.../* @__PURE__ */ new Set([...set.edits.map((e) => e.locale), options.sourceLocale])];
4046
+ const remote = await readRemoteLocales(rest, targetSha, options.localesPath, locales);
4047
+ if (!remote.targets.get(options.sourceLocale)) {
4048
+ throw new ExpectedFailure(
4049
+ `Source locale \`${options.sourceLocale}\` has no locale file on \`${target.branch}\`.`
4050
+ );
4051
+ }
4052
+ const plan = planApply({
4053
+ edits: set.edits,
4054
+ sourceLocale: options.sourceLocale,
4055
+ targets: remote.targets,
4056
+ allowForce: trigger.force
4057
+ });
4058
+ log(renderTerminalSummary(plan));
4059
+ const credits = { plan, by: set.by, note: set.note, target };
4060
+ if (plan.counts.apply === 0) return report({ kind: "nothing-to-apply" }, credits);
4061
+ const branchPlan = await prepareBranch(
4062
+ rest,
4063
+ branchBaseName(trigger, runId),
4064
+ target.branch,
4065
+ targetSha,
4066
+ runId
4067
+ );
4068
+ const base = branchPlan.mode === "append" ? await readRemoteLocales(rest, branchPlan.parentSha, options.localesPath, locales) : remote;
4069
+ const changes = buildFileChanges(options.localesPath, plan, remote, base.files);
4070
+ const commit = await commitFiles(rest, {
4071
+ branchPlan,
4072
+ files: changes,
4073
+ message: renderCommitMessage({ plan, by: set.by, issueNumber, runId }),
4074
+ author: noreplyAuthor(trigger.actor)
4075
+ });
4076
+ log(
4077
+ commit.sha === null ? `${branchPlan.branch} already holds these changes.` : `Committed to ${branchPlan.branch}.`
4078
+ );
4079
+ const workspace = options.env.GITHUB_WORKSPACE || process.cwd();
4080
+ const checkSummary = await checkChanges(options, workspace, base, changes, plan);
4081
+ const title = issueNumber !== void 0 ? `${PR_TITLE} (#${issueNumber})` : `${PR_TITLE} (run ${runId})`;
4082
+ const body = renderPullRequestBody({ ...credits, issueNumber, checkSummary, runUrl });
4083
+ let outcome;
4084
+ try {
4085
+ const pull = await rest.openOrReusePullRequest({
4086
+ head: branchPlan.branch,
4087
+ base: target.branch,
4088
+ title,
4089
+ body,
4090
+ updateBodyIfReused: true
4091
+ });
4092
+ outcome = { kind: "pr", url: pull.url, reused: pull.reused };
4093
+ } catch (err) {
4094
+ if (!isPullRequestCreationForbidden(err)) throw err;
4095
+ const closing = closingLine({ target, issueNumber });
4096
+ const url = `${run3.server}/${repo}/compare/${encodeRefPath(target.branch)}...${encodeRefPath(branchPlan.branch)}?quick_pull=1&title=${encodeURIComponent(title)}` + (closing ? `&body=${encodeURIComponent(closing)}` : "");
4097
+ outcome = { kind: "compare", url, branch: branchPlan.branch };
4098
+ }
4099
+ log(`Pull request: ${outcome.url}`);
4100
+ return report(outcome, { ...credits, checkSummary });
4101
+ }
4102
+
4103
+ // src/codemod/apply/link-input.ts
4104
+ var import_child_process3 = require("child_process");
4105
+ var fs7 = __toESM(require("fs"), 1);
4106
+ var path11 = __toESM(require("path"), 1);
4107
+ var MAX_APPLY_INPUT_BYTES = 1048576;
4108
+ var TOO_LARGE_MESSAGE3 = "The input is larger than 1 MB.";
4109
+ 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.)';
4110
+ var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
4111
+ var ApplyInputError = class extends Error {
4112
+ constructor(message) {
4113
+ super(message);
4114
+ this.name = "ApplyInputError";
4115
+ }
4116
+ };
4117
+ function checkSize(text) {
4118
+ if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
4119
+ throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4120
+ }
4121
+ return text;
4122
+ }
4123
+ async function readStream(stream) {
4124
+ const chunks = [];
4125
+ let size = 0;
4126
+ for await (const chunk of stream) {
4127
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
4128
+ size += bytes.length;
4129
+ if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4130
+ chunks.push(bytes);
4131
+ }
4132
+ return Buffer.concat(chunks).toString("utf8");
4133
+ }
4134
+ function existingFile(arg, cwd) {
4135
+ try {
4136
+ const resolved = path11.resolve(cwd, arg);
4137
+ return fs7.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
4138
+ } catch {
4139
+ return null;
4140
+ }
4141
+ }
4142
+ async function readApplyInput(arg, options) {
4143
+ if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
4144
+ if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
4145
+ if (arg === "-") return readStream(options.stdin ?? process.stdin);
4146
+ const file = existingFile(arg, options.cwd);
4147
+ if (file === null) return checkSize(arg);
4148
+ if (fs7.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4149
+ return checkSize(fs7.readFileSync(file, "utf8"));
4150
+ }
4151
+ function clipboardCommands(platform) {
4152
+ if (platform === "win32") {
4153
+ return [
4154
+ ["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
4155
+ ];
4156
+ }
4157
+ if (platform === "darwin") return [["pbpaste", []]];
4158
+ return [
4159
+ ["wl-paste", ["--no-newline"]],
4160
+ ["xclip", ["-selection", "clipboard", "-o"]],
4161
+ ["xsel", ["--clipboard", "--output"]]
4162
+ ];
4163
+ }
4164
+ var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
4165
+ function run(execFileImpl, [command, args]) {
4166
+ return new Promise((resolve16) => {
4167
+ try {
4168
+ execFileImpl(
4169
+ command,
4170
+ args,
4171
+ { encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
4172
+ (error, stdout) => {
4173
+ const code = error?.code;
4174
+ if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve16(TOO_LARGE);
4175
+ else resolve16(error ? null : String(stdout));
4176
+ }
4177
+ );
4178
+ } catch {
4179
+ resolve16(null);
4180
+ }
4181
+ });
4182
+ }
4183
+ async function readClipboard(execFileImpl = import_child_process3.execFile, platform = process.platform) {
4184
+ for (const command of clipboardCommands(platform)) {
4185
+ const text = await run(execFileImpl, command);
4186
+ if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4187
+ if (text !== null) return checkSize(text);
4188
+ }
4189
+ throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
4190
+ }
4191
+
4192
+ // src/codemod/apply/local.ts
4193
+ var import_child_process5 = require("child_process");
4194
+ var fs10 = __toESM(require("fs"), 1);
4195
+ var os2 = __toESM(require("os"), 1);
4196
+ var path15 = __toESM(require("path"), 1);
4197
+
4198
+ // src/server/file-mutator.ts
4199
+ var import_fs6 = require("fs");
4200
+ var path13 = __toESM(require("path"), 1);
4201
+
4202
+ // src/core/safe-path.ts
4203
+ var path12 = __toESM(require("path"), 1);
4204
+ function resolveSafeLocalePath(basePath, locale) {
4205
+ if (!isSafeLocaleSegment(locale)) {
4206
+ throw new Error("Path traversal attempt");
4207
+ }
4208
+ const resolvedBasePath = path12.resolve(basePath);
4209
+ const filePath = path12.resolve(resolvedBasePath, `${locale}.json`);
4210
+ if (!filePath.startsWith(resolvedBasePath)) {
4211
+ throw new Error("Path traversal attempt");
4212
+ }
4213
+ return filePath;
4214
+ }
4215
+
4216
+ // src/server/file-mutator.ts
4217
+ var FileMutex = class {
4218
+ queue = Promise.resolve();
4219
+ async acquire(fn) {
4220
+ const next = this.queue.then(fn);
4221
+ this.queue = next.then(
4222
+ () => {
4223
+ },
4224
+ () => {
4225
+ }
4226
+ );
4227
+ return next;
4228
+ }
4229
+ };
4230
+ var FileMutator = class {
4231
+ fileLocks = /* @__PURE__ */ new Map();
4232
+ requireEnabled;
4233
+ quiet;
4234
+ scanner;
4235
+ constructor(options = {}) {
4236
+ this.requireEnabled = options.requireEnabled ?? true;
4237
+ this.quiet = options.quiet ?? false;
4238
+ this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
4239
+ }
4240
+ assertEnabled() {
4241
+ if (!this.requireEnabled) return;
4242
+ const reason = getDisabledReason();
4243
+ if (reason !== null) {
4244
+ throw new Error(
4245
+ `[livelocale] FileMutator can only be executed in development mode (${reason}).`
4246
+ );
4247
+ }
4248
+ }
4249
+ /**
4250
+ * Helper to serialize execution per file path
4251
+ */
4252
+ async withLock(filePath, fn) {
4253
+ let lock = this.fileLocks.get(filePath);
4254
+ if (!lock) {
4255
+ lock = new FileMutex();
4256
+ this.fileLocks.set(filePath, lock);
4257
+ }
4258
+ return lock.acquire(fn);
4259
+ }
4260
+ /**
4261
+ * Updates a locale JSON file on disk atomically and safely.
4262
+ * Enforces development mode checks, path traversal mitigation, key depth restrictions,
4263
+ * structural JSON validation, and concurrency serialization.
4264
+ */
4265
+ async updateLocaleKey(basePath, locale, keyPath, newValue) {
4266
+ this.assertEnabled();
4267
+ if (!isSafeLocaleSegment(locale)) {
4268
+ throw new Error(
4269
+ `[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
4270
+ );
4271
+ }
4272
+ const resolvedBasePath = path13.resolve(basePath);
4273
+ const localeDir = path13.resolve(resolvedBasePath, locale);
4274
+ let filePath = "";
4275
+ let mutationKeyPath = keyPath;
4276
+ let isNamespaceDir = false;
4277
+ try {
4278
+ const stats = await import_fs6.promises.stat(localeDir);
4279
+ if (stats.isDirectory()) {
4280
+ isNamespaceDir = true;
4281
+ }
4282
+ } catch {
4283
+ }
4284
+ if (isNamespaceDir) {
4285
+ const firstDot = keyPath.indexOf(".");
4286
+ if (firstDot !== -1) {
4287
+ const namespace = keyPath.substring(0, firstDot);
3432
4288
  if (DANGEROUS_KEY_SEGMENTS.has(namespace)) {
3433
4289
  throw new Error(
3434
4290
  `[livelocale] Forbidden namespace segment in path: "${namespace}". Segment names like __proto__, constructor, and prototype are not allowed.`
3435
4291
  );
3436
4292
  }
3437
4293
  mutationKeyPath = keyPath.substring(firstDot + 1);
3438
- filePath = path11.resolve(localeDir, `${namespace}.json`);
4294
+ filePath = path13.resolve(localeDir, `${namespace}.json`);
3439
4295
  } else {
3440
- filePath = path11.resolve(localeDir, `${keyPath}.json`);
4296
+ filePath = path13.resolve(localeDir, `${keyPath}.json`);
3441
4297
  mutationKeyPath = "";
3442
4298
  }
3443
4299
  } else {
3444
- filePath = path11.resolve(resolvedBasePath, `${locale}.json`);
4300
+ filePath = path13.resolve(resolvedBasePath, `${locale}.json`);
3445
4301
  }
3446
4302
  if (!filePath.startsWith(resolvedBasePath)) {
3447
4303
  throw new Error(
@@ -3453,7 +4309,7 @@ var FileMutator = class {
3453
4309
  let indent = " ";
3454
4310
  let trailingNewline = "\n";
3455
4311
  try {
3456
- const fileContent = await import_fs5.promises.readFile(filePath, "utf-8");
4312
+ const fileContent = await import_fs6.promises.readFile(filePath, "utf-8");
3457
4313
  indent = detectJsonIndent(fileContent);
3458
4314
  trailingNewline = fileContent.endsWith("\n") ? "\n" : "";
3459
4315
  try {
@@ -3493,17 +4349,17 @@ var FileMutator = class {
3493
4349
  }
3494
4350
  const tempPath = `${filePath}.tmp`;
3495
4351
  try {
3496
- await import_fs5.promises.writeFile(tempPath, stringified, "utf-8");
4352
+ await import_fs6.promises.writeFile(tempPath, stringified, "utf-8");
3497
4353
  } catch (err) {
3498
4354
  throw new Error(
3499
4355
  `[livelocale] Failed to write temp file at ${tempPath}. Operation: write. Original error: ${err.message}`
3500
4356
  );
3501
4357
  }
3502
4358
  try {
3503
- await import_fs5.promises.rename(tempPath, filePath);
4359
+ await import_fs6.promises.rename(tempPath, filePath);
3504
4360
  } catch (err) {
3505
4361
  try {
3506
- await import_fs5.promises.unlink(tempPath);
4362
+ await import_fs6.promises.unlink(tempPath);
3507
4363
  } catch {
3508
4364
  }
3509
4365
  throw new Error(
@@ -3521,19 +4377,19 @@ var FileMutator = class {
3521
4377
  */
3522
4378
  async readLocale(basePath, locale) {
3523
4379
  this.assertEnabled();
3524
- const localePath = path11.resolve(basePath, locale);
4380
+ const localePath = path13.resolve(basePath, locale);
3525
4381
  let isDir = false;
3526
4382
  try {
3527
- isDir = (await import_fs5.promises.stat(localePath)).isDirectory();
4383
+ isDir = (await import_fs6.promises.stat(localePath)).isDirectory();
3528
4384
  } catch {
3529
4385
  }
3530
4386
  if (isDir) {
3531
4387
  const merged = {};
3532
- for (const file of await import_fs5.promises.readdir(localePath)) {
4388
+ for (const file of await import_fs6.promises.readdir(localePath)) {
3533
4389
  if (!file.endsWith(".json")) continue;
3534
4390
  try {
3535
- merged[path11.basename(file, ".json")] = JSON.parse(
3536
- await import_fs5.promises.readFile(path11.join(localePath, file), "utf-8")
4391
+ merged[path13.basename(file, ".json")] = JSON.parse(
4392
+ await import_fs6.promises.readFile(path13.join(localePath, file), "utf-8")
3537
4393
  );
3538
4394
  } catch {
3539
4395
  }
@@ -3541,15 +4397,15 @@ var FileMutator = class {
3541
4397
  return merged;
3542
4398
  }
3543
4399
  const filePath = resolveSafeLocalePath(basePath, locale);
3544
- return JSON.parse(await import_fs5.promises.readFile(filePath, "utf-8"));
4400
+ return JSON.parse(await import_fs6.promises.readFile(filePath, "utf-8"));
3545
4401
  }
3546
4402
  /**
3547
4403
  * Lists all locale codes dynamically scanned from the locales directory.
3548
4404
  */
3549
4405
  async listLocales(basePath) {
3550
4406
  this.assertEnabled();
3551
- const resolvedBasePath = path11.resolve(basePath);
3552
- const items = await import_fs5.promises.readdir(resolvedBasePath, { withFileTypes: true });
4407
+ const resolvedBasePath = path13.resolve(basePath);
4408
+ const items = await import_fs6.promises.readdir(resolvedBasePath, { withFileTypes: true });
3553
4409
  const localesSet = /* @__PURE__ */ new Set();
3554
4410
  for (const item of items) {
3555
4411
  if (item.isDirectory()) {
@@ -3557,7 +4413,7 @@ var FileMutator = class {
3557
4413
  localesSet.add(item.name);
3558
4414
  }
3559
4415
  } else if (item.isFile() && item.name.endsWith(".json")) {
3560
- const name = path11.basename(item.name, ".json");
4416
+ const name = path13.basename(item.name, ".json");
3561
4417
  if (LOCALE_RE.test(name)) {
3562
4418
  localesSet.add(name);
3563
4419
  }
@@ -3578,8 +4434,8 @@ var FileMutator = class {
3578
4434
  if (!LOCALE_RE.test(locale)) {
3579
4435
  throw new Error(`[livelocale] Invalid locale format: "${locale}"`);
3580
4436
  }
3581
- const resolvedBasePath = path11.resolve(basePath);
3582
- const localePath = path11.resolve(resolvedBasePath, locale);
4437
+ const resolvedBasePath = path13.resolve(basePath);
4438
+ const localePath = path13.resolve(resolvedBasePath, locale);
3583
4439
  if (!localePath.startsWith(resolvedBasePath)) {
3584
4440
  throw new Error(
3585
4441
  `[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
@@ -3587,19 +4443,19 @@ var FileMutator = class {
3587
4443
  }
3588
4444
  let exists = false;
3589
4445
  try {
3590
- await import_fs5.promises.stat(localePath);
4446
+ await import_fs6.promises.stat(localePath);
3591
4447
  exists = true;
3592
4448
  } catch {
3593
4449
  }
3594
4450
  try {
3595
- await import_fs5.promises.stat(`${localePath}.json`);
4451
+ await import_fs6.promises.stat(`${localePath}.json`);
3596
4452
  exists = true;
3597
4453
  } catch {
3598
4454
  }
3599
4455
  if (exists) {
3600
4456
  throw new Error(`[livelocale] Locale "${locale}" already exists.`);
3601
4457
  }
3602
- const items = await import_fs5.promises.readdir(resolvedBasePath, { withFileTypes: true });
4458
+ const items = await import_fs6.promises.readdir(resolvedBasePath, { withFileTypes: true });
3603
4459
  let referenceDir = null;
3604
4460
  for (const item of items) {
3605
4461
  if (item.isDirectory() && LOCALE_RE.test(item.name)) {
@@ -3608,16 +4464,16 @@ var FileMutator = class {
3608
4464
  }
3609
4465
  }
3610
4466
  if (referenceDir) {
3611
- await import_fs5.promises.mkdir(localePath, { recursive: true });
3612
- const refPath = path11.resolve(resolvedBasePath, referenceDir);
3613
- const files = await import_fs5.promises.readdir(refPath);
4467
+ await import_fs6.promises.mkdir(localePath, { recursive: true });
4468
+ const refPath = path13.resolve(resolvedBasePath, referenceDir);
4469
+ const files = await import_fs6.promises.readdir(refPath);
3614
4470
  for (const file of files) {
3615
4471
  if (file.endsWith(".json")) {
3616
- await import_fs5.promises.writeFile(path11.join(localePath, file), "{}", "utf-8");
4472
+ await import_fs6.promises.writeFile(path13.join(localePath, file), "{}", "utf-8");
3617
4473
  }
3618
4474
  }
3619
4475
  } else {
3620
- await import_fs5.promises.writeFile(`${localePath}.json`, "{}", "utf-8");
4476
+ await import_fs6.promises.writeFile(`${localePath}.json`, "{}", "utf-8");
3621
4477
  }
3622
4478
  console.log(`[livelocale] Added locale '${locale}'`);
3623
4479
  }
@@ -3632,16 +4488,16 @@ var FileMutator = class {
3632
4488
  if (!LOCALE_RE.test(oldLocale) || !LOCALE_RE.test(newLocale)) {
3633
4489
  throw new Error(`[livelocale] Invalid locale format`);
3634
4490
  }
3635
- const resolvedBasePath = path11.resolve(basePath);
3636
- const oldPath = path11.resolve(resolvedBasePath, oldLocale);
3637
- const newPath = path11.resolve(resolvedBasePath, newLocale);
4491
+ const resolvedBasePath = path13.resolve(basePath);
4492
+ const oldPath = path13.resolve(resolvedBasePath, oldLocale);
4493
+ const newPath = path13.resolve(resolvedBasePath, newLocale);
3638
4494
  if (!oldPath.startsWith(resolvedBasePath) || !newPath.startsWith(resolvedBasePath)) {
3639
4495
  throw new Error("[livelocale] Path traversal attempt");
3640
4496
  }
3641
4497
  let isDir = false;
3642
4498
  let exists = false;
3643
4499
  try {
3644
- const stats = await import_fs5.promises.stat(oldPath);
4500
+ const stats = await import_fs6.promises.stat(oldPath);
3645
4501
  isDir = stats.isDirectory();
3646
4502
  exists = true;
3647
4503
  } catch {
@@ -3649,7 +4505,7 @@ var FileMutator = class {
3649
4505
  let isFile = false;
3650
4506
  if (!exists) {
3651
4507
  try {
3652
- await import_fs5.promises.stat(`${oldPath}.json`);
4508
+ await import_fs6.promises.stat(`${oldPath}.json`);
3653
4509
  isFile = true;
3654
4510
  exists = true;
3655
4511
  } catch {
@@ -3660,12 +4516,12 @@ var FileMutator = class {
3660
4516
  }
3661
4517
  let newExists = false;
3662
4518
  try {
3663
- await import_fs5.promises.stat(newPath);
4519
+ await import_fs6.promises.stat(newPath);
3664
4520
  newExists = true;
3665
4521
  } catch {
3666
4522
  }
3667
4523
  try {
3668
- await import_fs5.promises.stat(`${newPath}.json`);
4524
+ await import_fs6.promises.stat(`${newPath}.json`);
3669
4525
  newExists = true;
3670
4526
  } catch {
3671
4527
  }
@@ -3673,9 +4529,9 @@ var FileMutator = class {
3673
4529
  throw new Error(`[livelocale] Target locale "${newLocale}" already exists.`);
3674
4530
  }
3675
4531
  if (isDir) {
3676
- await import_fs5.promises.rename(oldPath, newPath);
4532
+ await import_fs6.promises.rename(oldPath, newPath);
3677
4533
  } else if (isFile) {
3678
- await import_fs5.promises.rename(`${oldPath}.json`, `${newPath}.json`);
4534
+ await import_fs6.promises.rename(`${oldPath}.json`, `${newPath}.json`);
3679
4535
  }
3680
4536
  console.log(`[livelocale] Renamed locale '${oldLocale}' to '${newLocale}'`);
3681
4537
  }
@@ -3690,15 +4546,15 @@ var FileMutator = class {
3690
4546
  if (!LOCALE_RE.test(locale)) {
3691
4547
  throw new Error(`[livelocale] Invalid locale format`);
3692
4548
  }
3693
- const resolvedBasePath = path11.resolve(basePath);
3694
- const localePath = path11.resolve(resolvedBasePath, locale);
4549
+ const resolvedBasePath = path13.resolve(basePath);
4550
+ const localePath = path13.resolve(resolvedBasePath, locale);
3695
4551
  if (!localePath.startsWith(resolvedBasePath)) {
3696
4552
  throw new Error("[livelocale] Path traversal attempt");
3697
4553
  }
3698
4554
  let isDir = false;
3699
4555
  let exists = false;
3700
4556
  try {
3701
- const stats = await import_fs5.promises.stat(localePath);
4557
+ const stats = await import_fs6.promises.stat(localePath);
3702
4558
  isDir = stats.isDirectory();
3703
4559
  exists = true;
3704
4560
  } catch {
@@ -3706,7 +4562,7 @@ var FileMutator = class {
3706
4562
  let isFile = false;
3707
4563
  if (!exists) {
3708
4564
  try {
3709
- await import_fs5.promises.stat(`${localePath}.json`);
4565
+ await import_fs6.promises.stat(`${localePath}.json`);
3710
4566
  isFile = true;
3711
4567
  exists = true;
3712
4568
  } catch {
@@ -3716,9 +4572,9 @@ var FileMutator = class {
3716
4572
  throw new Error(`[livelocale] Locale "${locale}" does not exist.`);
3717
4573
  }
3718
4574
  if (isDir) {
3719
- await import_fs5.promises.rm(localePath, { recursive: true, force: true });
4575
+ await import_fs6.promises.rm(localePath, { recursive: true, force: true });
3720
4576
  } else if (isFile) {
3721
- await import_fs5.promises.unlink(`${localePath}.json`);
4577
+ await import_fs6.promises.unlink(`${localePath}.json`);
3722
4578
  }
3723
4579
  console.log(`[livelocale] Deleted locale '${locale}'`);
3724
4580
  }
@@ -3746,8 +4602,8 @@ var FileMutator = class {
3746
4602
  // src/server/git-client.ts
3747
4603
  var import_child_process4 = require("child_process");
3748
4604
  var import_util = require("util");
3749
- var fs8 = __toESM(require("fs"), 1);
3750
- var path12 = __toESM(require("path"), 1);
4605
+ var fs9 = __toESM(require("fs"), 1);
4606
+ var path14 = __toESM(require("path"), 1);
3751
4607
  var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3752
4608
  var MAX_DIFF_BYTES = 2e5;
3753
4609
  function mapStatus(xy) {
@@ -3778,15 +4634,15 @@ var GitClient = class {
3778
4634
  }
3779
4635
  /** Resolves the repository root and the locales path relative to it (forward slashes). */
3780
4636
  async locate(localesPath) {
3781
- const absolute = path12.resolve(localesPath);
3782
- const cwd = fs8.existsSync(absolute) ? absolute : path12.dirname(absolute);
3783
- if (!fs8.existsSync(cwd)) return null;
4637
+ const absolute = path14.resolve(localesPath);
4638
+ const cwd = fs9.existsSync(absolute) ? absolute : path14.dirname(absolute);
4639
+ if (!fs9.existsSync(cwd)) return null;
3784
4640
  try {
3785
4641
  const root = (await this.git(cwd, ["rev-parse", "--show-toplevel"])).trim();
3786
4642
  if (!root) return null;
3787
- const rel = path12.relative(root, absolute).split(path12.sep).join("/");
4643
+ const rel = path14.relative(root, absolute).split(path14.sep).join("/");
3788
4644
  if (rel.startsWith("..")) return null;
3789
- return { root: path12.resolve(root), rel: rel || ".", absolute };
4645
+ return { root: path14.resolve(root), rel: rel || ".", absolute };
3790
4646
  } catch {
3791
4647
  return null;
3792
4648
  }
@@ -3818,301 +4674,127 @@ var GitClient = class {
3818
4674
  if (file.startsWith('"') && file.endsWith('"')) file = file.slice(1, -1);
3819
4675
  changes.push({ path: file, status: mapStatus(xy) });
3820
4676
  }
3821
- } catch {
3822
- }
3823
- return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
3824
- }
3825
- async diff(localesPath) {
3826
- const loc = await this.locate(localesPath);
3827
- if (!loc) return "";
3828
- let out = "";
3829
- try {
3830
- out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
3831
- } catch {
3832
- }
3833
- try {
3834
- const untracked = await this.git(loc.root, [
3835
- "ls-files",
3836
- "--others",
3837
- "--exclude-standard",
3838
- "--",
3839
- loc.rel
3840
- ]);
3841
- for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
3842
- const full = path12.join(loc.root, file);
3843
- let content = "";
3844
- try {
3845
- content = fs8.readFileSync(full, "utf-8");
3846
- } catch {
3847
- continue;
3848
- }
3849
- const lines = content.split("\n");
3850
- if (lines[lines.length - 1] === "") lines.pop();
3851
- out += `diff --git a/${file} b/${file}
3852
- new file mode 100644
3853
- --- /dev/null
3854
- +++ b/${file}
3855
- @@ -0,0 +1,${lines.length} @@
3856
- ` + lines.map((l) => `+${l}`).join("\n") + "\n";
3857
- }
3858
- } catch {
3859
- }
3860
- if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
3861
- out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
3862
- }
3863
- return out;
3864
- }
3865
- async commit(localesPath, message) {
3866
- const loc = await this.locate(localesPath);
3867
- if (!loc) throw new Error("Not a git repository.");
3868
- const status = await this.status(localesPath);
3869
- if (status.changes.length === 0) {
3870
- throw new Error(`No changes to commit under ${loc.rel}.`);
3871
- }
3872
- await this.git(loc.root, ["add", "-A", "--", loc.rel]);
3873
- await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
3874
- const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
3875
- return { hash };
3876
- }
3877
- async discard(localesPath) {
3878
- const loc = await this.locate(localesPath);
3879
- if (!loc) throw new Error("Not a git repository.");
3880
- try {
3881
- await this.git(loc.root, ["checkout", "--", loc.rel]);
3882
- } catch {
3883
- }
3884
- await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
3885
- }
3886
- };
3887
-
3888
- // src/codemod/apply/report.ts
3889
- var DEFAULT_SPAN_LENGTH = 120;
3890
- var DEFAULT_TABLE_ROWS = 100;
3891
- var MAX_BODY_CHARS = 6e4;
3892
- var MAX_NOTE_CHARS2 = 2e3;
3893
- var MAX_NAME_CHARS = 64;
3894
- var MAX_CHECK_LINES = 50;
3895
- var MAX_CHECK_SUMMARY_CHARS = 1e4;
3896
- var BODY_MARGIN = 200;
3897
- var STATUS_LABELS = {
3898
- apply: "applied",
3899
- already: "already done",
3900
- conflict: "conflict",
3901
- "needs-force": "needs --force",
3902
- invalid: "rejected"
3903
- };
3904
- var BIDI_CONTROL_RE = /[‪-‮⁦-⁩]/g;
3905
- var NEWLINE_RE = /\r\n|\r|\n/g;
3906
- var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
3907
- var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
3908
- function cut(text, max) {
3909
- if (text.length <= max) return text;
3910
- let out = text.slice(0, max);
3911
- const last = out.charCodeAt(out.length - 1);
3912
- if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
3913
- return out;
3914
- }
3915
- function cutLines(text, max) {
3916
- if (text.length <= max) return text;
3917
- const kept = [];
3918
- let length = 0;
3919
- for (const line of text.split("\n")) {
3920
- const added = (kept.length > 0 ? 1 : 0) + line.length;
3921
- if (length + added + 2 > max) break;
3922
- kept.push(line);
3923
- length += added;
3924
- }
3925
- return [...kept, "\u2026"].join("\n");
3926
- }
3927
- function longestBacktickRun(text) {
3928
- let longest = 0;
3929
- for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
3930
- return longest;
3931
- }
3932
- function codePointLabel(ch) {
3933
- return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
3934
- }
3935
- function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
3936
- const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
3937
- const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
3938
- if (content === "") return "` `";
3939
- const fence = "`".repeat(longestBacktickRun(content) + 1);
3940
- const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
3941
- return `${fence}${pad}${content}${pad}${fence}`;
3942
- }
3943
- function sanitizeName(name) {
3944
- if (name === void 0) return void 0;
3945
- const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
3946
- const result = cut(cleaned, MAX_NAME_CHARS).trim();
3947
- return result === "" ? void 0 : result;
3948
- }
3949
- var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
3950
- function summarizeCheck(report, edited) {
3951
- const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
3952
- const mine = report.issues.filter(
3953
- (issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
3954
- );
3955
- if (mine.length === 0) return NO_CHECK_PROBLEMS;
3956
- const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
3957
- for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
3958
- lines.push(
3959
- `- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
3960
- );
3961
- }
3962
- if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
3963
- const others = report.issues.length - mine.length;
3964
- if (others > 0) {
3965
- lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
3966
- }
3967
- return lines.join("\n");
3968
- }
3969
- function checksCell(edit) {
3970
- const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
3971
- if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
3972
- parts.push(codeSpan(edit.message));
4677
+ } catch {
4678
+ }
4679
+ return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
3973
4680
  }
3974
- return parts.join("; ");
3975
- }
3976
- function renderRow(edit) {
3977
- const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
3978
- const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
3979
- const cells = [
3980
- STATUS_LABELS[edit.status],
3981
- codeSpan(edit.locale),
3982
- codeSpan(edit.key),
3983
- before,
3984
- after,
3985
- checksCell(edit)
3986
- ];
3987
- return `| ${cells.join(" | ")} |`;
3988
- }
3989
- function renderTable(plan, limit, maxChars) {
3990
- const lines = [
3991
- "| Status | Locale | Key | Before | After | Checks |",
3992
- "| --- | --- | --- | --- | --- | --- |"
3993
- ];
3994
- const moreLine = (n) => `
3995
-
3996
- \u2026and ${n} more`;
3997
- let length = lines.join("\n").length + moreLine(plan.edits.length).length;
3998
- let shown = 0;
3999
- for (const edit of plan.edits) {
4000
- if (shown >= limit) break;
4001
- const row = renderRow(edit);
4002
- if (length + row.length + 1 > maxChars) break;
4003
- lines.push(row);
4004
- length += row.length + 1;
4005
- shown += 1;
4681
+ async diff(localesPath) {
4682
+ const loc = await this.locate(localesPath);
4683
+ if (!loc) return "";
4684
+ let out = "";
4685
+ try {
4686
+ out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
4687
+ } catch {
4688
+ }
4689
+ try {
4690
+ const untracked = await this.git(loc.root, [
4691
+ "ls-files",
4692
+ "--others",
4693
+ "--exclude-standard",
4694
+ "--",
4695
+ loc.rel
4696
+ ]);
4697
+ for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
4698
+ const full = path14.join(loc.root, file);
4699
+ let content = "";
4700
+ try {
4701
+ content = fs9.readFileSync(full, "utf-8");
4702
+ } catch {
4703
+ continue;
4704
+ }
4705
+ const lines = content.split("\n");
4706
+ if (lines[lines.length - 1] === "") lines.pop();
4707
+ out += `diff --git a/${file} b/${file}
4708
+ new file mode 100644
4709
+ --- /dev/null
4710
+ +++ b/${file}
4711
+ @@ -0,0 +1,${lines.length} @@
4712
+ ` + lines.map((l) => `+${l}`).join("\n") + "\n";
4713
+ }
4714
+ } catch {
4715
+ }
4716
+ if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
4717
+ out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
4718
+ }
4719
+ return out;
4006
4720
  }
4007
- const rest = plan.edits.length - shown;
4008
- return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
4009
- }
4010
- function fencedNote(note) {
4011
- const text = cut(note, MAX_NOTE_CHARS2);
4012
- const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
4013
- return `${fence}text
4014
- ${text}
4015
- ${fence}`;
4016
- }
4017
- function closingLine(ctx) {
4018
- if (ctx.issueNumber === void 0) return void 0;
4019
- if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
4020
- return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
4021
- }
4022
- function renderPullRequestBody(ctx) {
4023
- const head = ["Applies translations sent with a LiveLocale change link."];
4024
- if (ctx.by?.trim()) head.push(`Translated by ${codeSpan(ctx.by)}`);
4025
- if (ctx.target.reason === "not-allowed" && ctx.target.requested !== void 0) {
4026
- head.push(
4027
- `The link was made from ${codeSpan(ctx.target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(ctx.target.branch)}.`
4028
- );
4721
+ async commit(localesPath, message) {
4722
+ const loc = await this.locate(localesPath);
4723
+ if (!loc) throw new Error("Not a git repository.");
4724
+ const status = await this.status(localesPath);
4725
+ if (status.changes.length === 0) {
4726
+ throw new Error(`No changes to commit under ${loc.rel}.`);
4727
+ }
4728
+ await this.git(loc.root, ["add", "-A", "--", loc.rel]);
4729
+ await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
4730
+ const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
4731
+ return { hash };
4029
4732
  }
4030
- if (ctx.note?.trim()) head.push(fencedNote(ctx.note));
4031
- const tail = [];
4032
- if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
4033
- if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
4034
- const closing = closingLine(ctx);
4035
- if (closing) tail.push(closing);
4036
- const fixed = [...head, ...tail].join("\n\n").length;
4037
- const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
4038
- return [...head, table, ...tail].join("\n\n");
4039
- }
4040
- function renderCommitMessage(input) {
4041
- let subject = "i18n: apply LiveLocale changes";
4042
- if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
4043
- else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
4044
- const applied = input.plan.edits.filter((edit) => edit.status === "apply");
4045
- const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
4046
- const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
4047
- const name = sanitizeName(input.by);
4048
- if (name) parts.push(`Translated-by: ${name}`);
4049
- return parts.join("\n\n");
4050
- }
4051
- function renderTerminalSummary(plan) {
4052
- const c = plan.counts;
4053
- const lines = [
4054
- `Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
4055
- ];
4056
- for (const edit of plan.edits) {
4057
- lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
4733
+ async discard(localesPath) {
4734
+ const loc = await this.locate(localesPath);
4735
+ if (!loc) throw new Error("Not a git repository.");
4736
+ try {
4737
+ await this.git(loc.root, ["checkout", "--", loc.rel]);
4738
+ } catch {
4739
+ }
4740
+ await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
4058
4741
  }
4059
- return lines.join("\n");
4060
- }
4742
+ };
4061
4743
 
4062
4744
  // src/codemod/apply/local.ts
4063
- function isPlainObject4(value) {
4745
+ function isPlainObject5(value) {
4064
4746
  return typeof value === "object" && value !== null && !Array.isArray(value);
4065
4747
  }
4066
4748
  async function readTarget(mutator, localesDir, locale) {
4067
4749
  if (!isValidLocaleFormat(locale)) return null;
4068
- const dir = path13.join(localesDir, locale);
4750
+ const dir = path15.join(localesDir, locale);
4069
4751
  let layout;
4070
- if (fs9.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
4752
+ if (fs10.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
4071
4753
  layout = "namespaced";
4072
- } else if (fs9.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
4754
+ } else if (fs10.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
4073
4755
  layout = "flat";
4074
4756
  } else {
4075
4757
  return null;
4076
4758
  }
4077
4759
  const dict = await mutator.readLocale(localesDir, locale);
4078
- if (!isPlainObject4(dict)) {
4760
+ if (!isPlainObject5(dict)) {
4079
4761
  throw new Error(`The "${locale}" locale file is not a JSON object.`);
4080
4762
  }
4081
4763
  return { layout, dict };
4082
4764
  }
4083
4765
  function fileFor(localesPath, layout, locale, key) {
4084
- const rel = layout === "flat" ? path13.join(localesPath, `${locale}.json`) : path13.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
4085
- return path13.normalize(rel).split(path13.sep).join("/");
4766
+ const rel = layout === "flat" ? path15.join(localesPath, `${locale}.json`) : path15.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
4767
+ return path15.normalize(rel).split(path15.sep).join("/");
4086
4768
  }
4087
- var PR_TITLE = "i18n: LiveLocale changes";
4769
+ var PR_TITLE2 = "i18n: LiveLocale changes";
4088
4770
  var GITHUB_REMOTE_RE = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/;
4089
4771
  var defaultGit = (args, cwd, opts) => {
4090
4772
  const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
4091
4773
  if (opts?.inherit) {
4092
- return new Promise((resolve14, reject2) => {
4774
+ return new Promise((resolve16, reject2) => {
4093
4775
  const child = (0, import_child_process5.spawn)("git", args, { cwd, env, stdio: "inherit", windowsHide: true });
4094
4776
  child.on("error", reject2);
4095
4777
  child.on(
4096
4778
  "close",
4097
- (code) => code === 0 ? resolve14("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
4779
+ (code) => code === 0 ? resolve16("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
4098
4780
  );
4099
4781
  });
4100
4782
  }
4101
- return new Promise((resolve14, reject2) => {
4783
+ return new Promise((resolve16, reject2) => {
4102
4784
  (0, import_child_process5.execFile)(
4103
4785
  "git",
4104
4786
  args,
4105
4787
  { cwd, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 },
4106
- (error, stdout) => error ? reject2(error) : resolve14(String(stdout))
4788
+ (error, stdout) => error ? reject2(error) : resolve16(String(stdout))
4107
4789
  );
4108
4790
  });
4109
4791
  };
4110
- var defaultGh = (args, cwd) => new Promise((resolve14, reject2) => {
4792
+ var defaultGh = (args, cwd) => new Promise((resolve16, reject2) => {
4111
4793
  (0, import_child_process5.execFile)(
4112
4794
  "gh",
4113
4795
  args,
4114
4796
  { cwd, windowsHide: true },
4115
- (error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve14(String(stdout))
4797
+ (error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve16(String(stdout))
4116
4798
  );
4117
4799
  });
4118
4800
  function utcStamp(date) {
@@ -4144,7 +4826,7 @@ async function pickBranchName(git, root, stamp) {
4144
4826
  for (let n = 2; await taken(name); n++) name = `${base}-${n}`;
4145
4827
  return name;
4146
4828
  }
4147
- function errorMessage(err) {
4829
+ function errorMessage2(err) {
4148
4830
  return err instanceof Error ? err.message : String(err);
4149
4831
  }
4150
4832
  async function writeEdits(mutator, plan, targets, localesDir, localesPath) {
@@ -4164,12 +4846,12 @@ function exitCodeFor(plan) {
4164
4846
  async function preparePullRequest(git, projectDir, localesDir) {
4165
4847
  const top = (await tryRun(() => git(["rev-parse", "--show-toplevel"], projectDir)))?.trim();
4166
4848
  if (!top) throw new ApplyInputError("apply --pr needs a git repository.");
4167
- const root = path13.resolve(top);
4849
+ const root = path15.resolve(top);
4168
4850
  const original = (await tryRun(() => git(["symbolic-ref", "--quiet", "--short", "HEAD"], root)))?.trim();
4169
4851
  if (!original) throw new ApplyInputError("Check out a branch first.");
4170
4852
  const status = await new GitClient().status(localesDir);
4171
4853
  if (status.changes.length > 0) {
4172
- const rel = path13.relative(root, localesDir).split(path13.sep).join("/") || ".";
4854
+ const rel = path15.relative(root, localesDir).split(path15.sep).join("/") || ".";
4173
4855
  throw new ApplyInputError(
4174
4856
  `Commit or stash the changes under ${rel} first; apply --pr commits everything in that folder.`
4175
4857
  );
@@ -4177,10 +4859,10 @@ async function preparePullRequest(git, projectDir, localesDir) {
4177
4859
  return { root, original };
4178
4860
  }
4179
4861
  async function createPullRequest(log, gh, root, base, head, body) {
4180
- const tmpDir = fs9.mkdtempSync(path13.join(os.tmpdir(), "livelocale-pr-"));
4181
- const bodyFile = path13.join(tmpDir, "body.md");
4862
+ const tmpDir = fs10.mkdtempSync(path15.join(os2.tmpdir(), "livelocale-pr-"));
4863
+ const bodyFile = path15.join(tmpDir, "body.md");
4182
4864
  try {
4183
- fs9.writeFileSync(bodyFile, body, "utf-8");
4865
+ fs10.writeFileSync(bodyFile, body, "utf-8");
4184
4866
  const out = await gh(
4185
4867
  [
4186
4868
  "pr",
@@ -4190,7 +4872,7 @@ async function createPullRequest(log, gh, root, base, head, body) {
4190
4872
  "--head",
4191
4873
  head,
4192
4874
  "--title",
4193
- PR_TITLE,
4875
+ PR_TITLE2,
4194
4876
  "--body-file",
4195
4877
  bodyFile
4196
4878
  ],
@@ -4201,20 +4883,20 @@ async function createPullRequest(log, gh, root, base, head, body) {
4201
4883
  } catch (err) {
4202
4884
  if (err?.code !== "ENOENT") {
4203
4885
  const stderr = err?.stderr;
4204
- const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage(err);
4886
+ const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage2(err);
4205
4887
  log(`gh could not open the pull request: ${reason}`);
4206
4888
  }
4207
4889
  return void 0;
4208
4890
  } finally {
4209
- fs9.rmSync(tmpDir, { recursive: true, force: true });
4891
+ fs10.rmSync(tmpDir, { recursive: true, force: true });
4210
4892
  }
4211
4893
  }
4212
4894
  async function runLocalApply(options) {
4213
4895
  const log = options.log ?? ((line) => console.log(line));
4214
4896
  const set = await decodeChangeLink(options.input, nodeInflate);
4215
4897
  const localesPath = options.localesPath ?? "locales";
4216
- const localesDir = path13.resolve(options.projectDir, localesPath);
4217
- if (!fs9.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
4898
+ const localesDir = path15.resolve(options.projectDir, localesPath);
4899
+ if (!fs10.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
4218
4900
  throw new ApplyInputError(`No locales folder at ${localesPath}.`);
4219
4901
  }
4220
4902
  const pr = options.pr === true && options.dryRun !== true;
@@ -4270,7 +4952,7 @@ async function runLocalApply(options) {
4270
4952
  const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
4271
4953
  checkSummary = summarizeCheck(report, edited);
4272
4954
  } catch (err) {
4273
- checkSummary = `Check could not run: ${errorMessage(err)}`;
4955
+ checkSummary = `Check could not run: ${errorMessage2(err)}`;
4274
4956
  }
4275
4957
  await new GitClient().commit(localesDir, renderCommitMessage({ plan, by: set.by }));
4276
4958
  await git(["push", "-u", "origin", branch], root, { inherit: true });
@@ -4314,7 +4996,8 @@ var FLAG_OPTIONS = {
4314
4996
  "--clipboard": "clipboard",
4315
4997
  "--pr": "pr",
4316
4998
  "--force": "force",
4317
- "--dry-run": "dryRun"
4999
+ "--dry-run": "dryRun",
5000
+ "--github-event": "githubEvent"
4318
5001
  };
4319
5002
  function parseApplyArgs(argv) {
4320
5003
  const args = {
@@ -4325,11 +5008,20 @@ function parseApplyArgs(argv) {
4325
5008
  dryRun: false,
4326
5009
  dir: ".",
4327
5010
  locales: void 0,
4328
- source: void 0
5011
+ source: void 0,
5012
+ githubEvent: false,
5013
+ branches: void 0
4329
5014
  };
4330
5015
  for (let i = 0; i < argv.length; i++) {
4331
5016
  const arg = argv[i];
4332
- if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
5017
+ if (arg === "--branches") {
5018
+ const value = argv[i + 1];
5019
+ if (value === void 0 || value.startsWith("--")) {
5020
+ throw new ApplyInputError(`${arg} needs a value.`);
5021
+ }
5022
+ args.branches = value.split(",").map((branch) => branch.trim()).filter(Boolean);
5023
+ i++;
5024
+ } else if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
4333
5025
  const value = argv[i + 1];
4334
5026
  if (value === void 0 || value.startsWith("--")) {
4335
5027
  throw new ApplyInputError(`${arg} needs a value.`);
@@ -4349,17 +5041,43 @@ function parseApplyArgs(argv) {
4349
5041
  if (args.clipboard && args.input !== void 0) {
4350
5042
  throw new ApplyInputError("Pass the link or --clipboard, not both.");
4351
5043
  }
5044
+ if (args.githubEvent) {
5045
+ if (args.force) {
5046
+ throw new ApplyInputError(
5047
+ "--force is not allowed with --github-event; a maintainer comments `/livelocale apply --force`."
5048
+ );
5049
+ }
5050
+ if (args.input !== void 0) {
5051
+ throw new ApplyInputError(
5052
+ "--github-event reads the link from the event file; do not pass one."
5053
+ );
5054
+ }
5055
+ if (args.locales === void 0 || args.source === void 0) {
5056
+ throw new ApplyInputError("--github-event needs --locales and --source.");
5057
+ }
5058
+ }
4352
5059
  return args;
4353
5060
  }
4354
5061
  async function runApplyCommand(argv) {
4355
5062
  try {
4356
5063
  const args = parseApplyArgs(argv);
5064
+ if (args.githubEvent) {
5065
+ const result2 = await runGitHubEventApply({
5066
+ env: process.env,
5067
+ localesPath: args.locales,
5068
+ sourceLocale: args.source,
5069
+ branches: args.branches,
5070
+ dir: args.dir
5071
+ });
5072
+ process.exitCode = result2 === "error" || result2 === "wrong-repo" ? 1 : 0;
5073
+ return;
5074
+ }
4357
5075
  const input = await readApplyInput(args.input, {
4358
5076
  cwd: process.cwd(),
4359
5077
  clipboard: args.clipboard
4360
5078
  });
4361
5079
  const result = await runLocalApply({
4362
- projectDir: path14.resolve(args.dir),
5080
+ projectDir: path16.resolve(args.dir),
4363
5081
  localesPath: args.locales,
4364
5082
  sourceLocale: args.source,
4365
5083
  input,
@@ -4374,6 +5092,327 @@ async function runApplyCommand(argv) {
4374
5092
  }
4375
5093
  }
4376
5094
 
5095
+ // src/codemod/init-share.ts
5096
+ var fs11 = __toESM(require("fs"), 1);
5097
+ var path17 = __toESM(require("path"), 1);
5098
+ var CHECKOUT_ACTION = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2";
5099
+ var WORKFLOW_PATH = ".github/workflows/livelocale.yml";
5100
+ var LOCALES_CANDIDATES = [
5101
+ "locales",
5102
+ "src/locales",
5103
+ "public/locales",
5104
+ "src/i18n/locales",
5105
+ "messages"
5106
+ ];
5107
+ var SAFE_ARG = /^[A-Za-z0-9_./-]+$/;
5108
+ var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
5109
+ var MAX_BRANCHES = 10;
5110
+ var SHARE_GITIGNORE = "public/livelocale/";
5111
+ function readText3(file) {
5112
+ try {
5113
+ return fs11.readFileSync(file, "utf-8");
5114
+ } catch {
5115
+ return null;
5116
+ }
5117
+ }
5118
+ function parsePackageJson(text) {
5119
+ if (text === null) return null;
5120
+ try {
5121
+ const parsed = JSON.parse(text);
5122
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5123
+ } catch {
5124
+ return null;
5125
+ }
5126
+ }
5127
+ function hasDependency2(pkg, name) {
5128
+ return Boolean(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]);
5129
+ }
5130
+ function detectShareStack(projectDir) {
5131
+ const target = detectBuildTarget(projectDir);
5132
+ if (target.kind === "next") {
5133
+ return target.staticExport ? { stack: "next-export", evidence: "Next.js with output: 'export'" } : { stack: "next-server", evidence: "Next.js without output: 'export'" };
5134
+ }
5135
+ if (target.kind === "vite") return { stack: "vite", evidence: "Vite" };
5136
+ if (target.kind === "cra") return { stack: "cra", evidence: "react-scripts" };
5137
+ const pkg = parsePackageJson(readText3(path17.join(projectDir, "package.json")));
5138
+ for (const name of ["webpack", "@rsbuild/core"]) {
5139
+ if (hasDependency2(pkg, name)) return { stack: "webpack", evidence: `${name} dependency` };
5140
+ }
5141
+ const config = fs11.readdirSync(projectDir).find((name) => /^webpack\.config\.[A-Za-z]+$/.test(name));
5142
+ if (config) return { stack: "webpack", evidence: config };
5143
+ return { stack: "unknown", evidence: "no Vite, Next.js, CRA, webpack or Rsbuild setup found" };
5144
+ }
5145
+ function checkSafePath(name, value) {
5146
+ const segments = value.split("/");
5147
+ if (!SAFE_ARG.test(value) || value.startsWith("/") || segments.includes("..")) {
5148
+ throw new Error(`\`${name}\` may only contain letters, digits, ".", "_", "-" and "/".`);
5149
+ }
5150
+ }
5151
+ function validateShareArgs(o) {
5152
+ checkSafePath("--locales", o.localesPath);
5153
+ if (o.dir !== void 0) checkSafePath("--dir", o.dir);
5154
+ if (!LOCALE_RE.test(o.sourceLocale)) {
5155
+ throw new Error("`--source` must be a locale code such as en or pt-BR.");
5156
+ }
5157
+ if (o.branches.length === 0) throw new Error("`--branches` needs at least one branch.");
5158
+ if (o.branches.length > MAX_BRANCHES) {
5159
+ throw new Error(`\`--branches\` takes at most ${MAX_BRANCHES} branches.`);
5160
+ }
5161
+ for (const branch of o.branches) checkSafePath("--branches", branch);
5162
+ }
5163
+ function renderWorkflow(o) {
5164
+ validateShareArgs(o);
5165
+ if (!VERSION_RE.test(o.version)) throw new Error(`Not a livelocale version: ${o.version}`);
5166
+ const dirArg = o.dir === void 0 ? "" : ` --dir ${o.dir}`;
5167
+ return [
5168
+ "# Written by `npx livelocale init --share`. Turns LiveLocale change links into pull requests.",
5169
+ "# livelocale reads the link from the event file; it never reaches a shell command.",
5170
+ "name: LiveLocale",
5171
+ "on:",
5172
+ " issues: { types: [opened] }",
5173
+ " issue_comment: { types: [created] }",
5174
+ " pull_request: { types: [closed] }",
5175
+ " workflow_dispatch: { inputs: { link: { description: 'LiveLocale change link', required: true } } }",
5176
+ "permissions: { contents: write, pull-requests: write, issues: write }",
5177
+ "concurrency: { group: 'livelocale-${{ github.event.issue.number || github.run_id }}', queue: max }",
5178
+ "jobs:",
5179
+ " apply:",
5180
+ " if: >-",
5181
+ " github.event_name == 'workflow_dispatch' ||",
5182
+ " (github.event_name == 'issues' && contains(github.event.issue.body, '```livelocale')) ||",
5183
+ " (github.event_name == 'issue_comment' && !github.event.issue.pull_request &&",
5184
+ " startsWith(github.event.comment.body, '/livelocale')) ||",
5185
+ " (github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'livelocale/issue-') &&",
5186
+ " github.event.pull_request.base.ref != github.event.repository.default_branch)",
5187
+ " runs-on: ubuntu-latest",
5188
+ " steps:",
5189
+ ` - uses: ${CHECKOUT_ACTION}`,
5190
+ " with: { persist-credentials: false }",
5191
+ ` - run: npx --yes livelocale@${o.version} apply --github-event --locales ${o.localesPath} --source ${o.sourceLocale} --branches ${o.branches.join(",")}${dirArg}`,
5192
+ " env: { GITHUB_TOKEN: '${{ github.token }}' }",
5193
+ ""
5194
+ ].join("\n");
5195
+ }
5196
+ function toPosix2(p) {
5197
+ return p.split(path17.sep).join("/");
5198
+ }
5199
+ function normalizeArgPath(p) {
5200
+ return p.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
5201
+ }
5202
+ function realDir(dir) {
5203
+ try {
5204
+ return fs11.realpathSync(dir);
5205
+ } catch {
5206
+ return path17.resolve(dir);
5207
+ }
5208
+ }
5209
+ function holdsLocaleJson(dir) {
5210
+ let entries;
5211
+ try {
5212
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
5213
+ } catch {
5214
+ return false;
5215
+ }
5216
+ return entries.some((entry) => {
5217
+ if (entry.isFile())
5218
+ return entry.name.endsWith(".json") && LOCALE_RE.test(entry.name.slice(0, -5));
5219
+ if (!entry.isDirectory() || !LOCALE_RE.test(entry.name)) return false;
5220
+ try {
5221
+ return fs11.readdirSync(path17.join(dir, entry.name)).some((name) => name.endsWith(".json"));
5222
+ } catch {
5223
+ return false;
5224
+ }
5225
+ });
5226
+ }
5227
+ function findLocalesPath(projectDir) {
5228
+ const found = LOCALES_CANDIDATES.find((rel) => holdsLocaleJson(path17.join(projectDir, rel)));
5229
+ if (!found) throw new Error("Pass --locales <path>.");
5230
+ return found;
5231
+ }
5232
+ async function defaultBranchOf(git, cwd) {
5233
+ const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
5234
+ if (ref?.startsWith("origin/") && ref.length > "origin/".length) {
5235
+ return ref.slice("origin/".length);
5236
+ }
5237
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
5238
+ if (head && head !== "HEAD") return head;
5239
+ return "main";
5240
+ }
5241
+ function planWrite(projectDir, rel, content) {
5242
+ const current = readText3(path17.join(projectDir, rel));
5243
+ if (current === null) return { path: rel, action: "create", content };
5244
+ return current === content ? { path: rel, action: "unchanged" } : { path: rel, action: "update", content };
5245
+ }
5246
+ function planViteConfig(c) {
5247
+ const addNote = `Add liveLocaleVite({ localesPath: '${c.localesPath}', share: true }) to the plugins array in vite.config.`;
5248
+ const config = VITE_CONFIG_FILES.find((name) => fs11.existsSync(path17.join(c.projectDir, name)));
5249
+ if (!config) {
5250
+ c.notes.push(addNote);
5251
+ return [];
5252
+ }
5253
+ const source = readText3(path17.join(c.projectDir, config)) ?? "";
5254
+ if (!source.includes("liveLocaleVite(")) {
5255
+ c.notes.push(addNote);
5256
+ return [{ path: config, action: "skipped", reason: "no liveLocaleVite(...) call" }];
5257
+ }
5258
+ if (/\bshare\s*:/.test(source)) return [{ path: config, action: "unchanged" }];
5259
+ const call = /liveLocaleVite\(\{\s*/;
5260
+ if (!call.test(source)) {
5261
+ c.notes.push(addNote);
5262
+ return [{ path: config, action: "skipped", reason: "liveLocaleVite(...) has no { options }" }];
5263
+ }
5264
+ const source2 = c.sourceLocale === "en" ? "" : `sourceLocale: '${c.sourceLocale}', `;
5265
+ const next = source.replace(call, () => `liveLocaleVite({ share: true, ${source2}`);
5266
+ return [{ path: config, action: "update", content: next }];
5267
+ }
5268
+ function planPackageScripts(c) {
5269
+ const text = readText3(path17.join(c.projectDir, "package.json"));
5270
+ const pkg = parsePackageJson(text);
5271
+ const args = `--locales ${c.localesPath} --source ${c.sourceLocale}`;
5272
+ const isNext = c.stack === "next-export" || c.stack === "next-server";
5273
+ const outDir = c.stack === "cra" ? "build" : "dist";
5274
+ const packCommand = `livelocale pack ${outDir} ${args}`;
5275
+ if (!pkg) {
5276
+ c.notes.push(
5277
+ isNext ? `Add "build:preview": "livelocale build --preview ${args} -- next build" to package.json.` : `Run "${packCommand}" after your build.`
5278
+ );
5279
+ return [{ path: "package.json", action: "skipped", reason: "no readable package.json" }];
5280
+ }
5281
+ const scripts = { ...pkg.scripts ?? {} };
5282
+ if (isNext) {
5283
+ const wanted = `livelocale build --preview ${args} -- next build`;
5284
+ const current = scripts["build:preview"];
5285
+ if (current === wanted || typeof current === "string" && current.includes("livelocale build")) {
5286
+ return [{ path: "package.json", action: "unchanged" }];
5287
+ }
5288
+ if (current !== void 0) {
5289
+ c.notes.push(`Set "build:preview" to "${wanted}" in package.json.`);
5290
+ return [
5291
+ {
5292
+ path: "package.json",
5293
+ action: "skipped",
5294
+ reason: 'scripts["build:preview"] already exists'
5295
+ }
5296
+ ];
5297
+ }
5298
+ scripts["build:preview"] = wanted;
5299
+ } else {
5300
+ const build = scripts.build;
5301
+ if (typeof build !== "string" || build.trim() === "") {
5302
+ c.notes.push(`Run "${packCommand}" after your build.`);
5303
+ return [{ path: "package.json", action: "skipped", reason: "no scripts.build" }];
5304
+ }
5305
+ if (build.includes("livelocale pack")) return [{ path: "package.json", action: "unchanged" }];
5306
+ scripts.build = `${build} && ${packCommand}`;
5307
+ }
5308
+ return [
5309
+ {
5310
+ path: "package.json",
5311
+ action: "update",
5312
+ content: stringifyLike({ ...pkg, scripts }, text)
5313
+ }
5314
+ ];
5315
+ }
5316
+ function planGitignore(projectDir) {
5317
+ const current = readText3(path17.join(projectDir, ".gitignore"));
5318
+ if (current === null) {
5319
+ return { path: ".gitignore", action: "create", content: `${SHARE_GITIGNORE}
5320
+ ` };
5321
+ }
5322
+ if (parseGitignore(current).ignores("public/livelocale", true)) {
5323
+ return { path: ".gitignore", action: "unchanged" };
5324
+ }
5325
+ const sep9 = current === "" || current.endsWith("\n") ? "" : "\n";
5326
+ return { path: ".gitignore", action: "update", content: `${current}${sep9}${SHARE_GITIGNORE}
5327
+ ` };
5328
+ }
5329
+ function planBuild(c) {
5330
+ switch (c.stack) {
5331
+ case "vite":
5332
+ return planViteConfig(c);
5333
+ case "cra":
5334
+ case "webpack":
5335
+ return planPackageScripts(c);
5336
+ case "next-export":
5337
+ case "next-server": {
5338
+ const files = [...planPackageScripts(c), planGitignore(c.projectDir)];
5339
+ if (c.stack === "next-export") {
5340
+ const route = findExportBlockingRoute(c.projectDir);
5341
+ if (route) {
5342
+ c.notes.push(
5343
+ `Delete ${route}: static export rejects POST routes, and livelocale build stops while it exists.`
5344
+ );
5345
+ }
5346
+ } else {
5347
+ c.notes.push(
5348
+ 'Use "npm run build:preview" as the build command of your preview deployments.'
5349
+ );
5350
+ }
5351
+ return files;
5352
+ }
5353
+ case "unknown":
5354
+ c.notes.push(
5355
+ `No supported build setup found. In custom CI, run LIVELOCALE=preview <your build> (only for SSR/SSG), then npx livelocale pack <outDir> --locales ${c.localesPath} --source ${c.sourceLocale}.`
5356
+ );
5357
+ return [];
5358
+ }
5359
+ }
5360
+ async function planInitShare(o) {
5361
+ const projectDir = realDir(o.projectDir);
5362
+ const git = o.git ?? runGit;
5363
+ const sourceLocale = o.sourceLocale ?? "en";
5364
+ const localesPath = o.localesPath !== void 0 ? normalizeArgPath(o.localesPath) : findLocalesPath(projectDir);
5365
+ validateShareArgs({ localesPath, sourceLocale, branches: o.branches ?? ["main"] });
5366
+ const { stack } = detectShareStack(projectDir);
5367
+ const notes = [];
5368
+ const files = planBuild({ projectDir, stack, localesPath, sourceLocale, notes });
5369
+ const remote = await git(["remote", "get-url", "origin"], projectDir);
5370
+ if (!remote || parseGitHubSlug(remote) === null) {
5371
+ notes.push('One-click pull requests need GitHub. Use: npx livelocale apply "<link>" --pr');
5372
+ return { stack, files, notes };
5373
+ }
5374
+ const top = await git(["rev-parse", "--show-toplevel"], projectDir);
5375
+ const repoRoot = top ? realDir(top) : projectDir;
5376
+ const appRel = toPosix2(path17.relative(repoRoot, projectDir));
5377
+ const dir = appRel === "" ? void 0 : appRel;
5378
+ const repoLocales = dir === void 0 ? localesPath : `${dir}/${localesPath}`;
5379
+ const defaultBranch = await defaultBranchOf(git, projectDir);
5380
+ const branches = o.branches ?? [defaultBranch];
5381
+ const workflow = renderWorkflow({
5382
+ version: o.version,
5383
+ localesPath: repoLocales,
5384
+ sourceLocale,
5385
+ branches,
5386
+ dir
5387
+ });
5388
+ const workflowRel = toPosix2(path17.relative(projectDir, path17.join(repoRoot, WORKFLOW_PATH)));
5389
+ const planned = planWrite(projectDir, workflowRel, workflow);
5390
+ files.push(
5391
+ planned.action === "update" && !o.overwrite ? {
5392
+ path: workflowRel,
5393
+ action: "skipped",
5394
+ reason: "it exists with different content; pass --overwrite to replace it"
5395
+ } : planned
5396
+ );
5397
+ for (const branch of branches) {
5398
+ if (branch === defaultBranch) continue;
5399
+ notes.push(
5400
+ `Also merge ${WORKFLOW_PATH} into \`${branch}\`; pull requests into it run workflows from that branch, so LiveLocale cannot close their issues otherwise.`
5401
+ );
5402
+ }
5403
+ return { stack, files, notes };
5404
+ }
5405
+ function writePlannedFiles(projectDir, files) {
5406
+ for (const file of files) {
5407
+ if (file.action !== "create" && file.action !== "update" || file.content === void 0) {
5408
+ continue;
5409
+ }
5410
+ const target = path17.resolve(projectDir, file.path);
5411
+ fs11.mkdirSync(path17.dirname(target), { recursive: true });
5412
+ fs11.writeFileSync(target, file.content, "utf-8");
5413
+ }
5414
+ }
5415
+
4377
5416
  // src/codemod/index.ts
4378
5417
  var DEFAULT_STUDIO_PORT = 3010;
4379
5418
  var DEFAULT_APP_ORIGIN = "http://localhost:3000";
@@ -4401,6 +5440,11 @@ Options for migrate:
4401
5440
 
4402
5441
  Options for init:
4403
5442
  --dir <path> Target project directory (default: current directory)
5443
+ --share Set up the share link build and the GitHub workflow instead of the API route
5444
+ --locales <path> (--share) Locales directory relative to the project (default: detected)
5445
+ --source <locale> (--share) Source locale (default: en)
5446
+ --branches <a,b> (--share) Branches a change link may target (default: the default branch)
5447
+ --overwrite (--share) Replace an existing .github/workflows/livelocale.yml
4404
5448
 
4405
5449
  Options for studio:
4406
5450
  --port <number> Port to run the studio server on (default: ${DEFAULT_STUDIO_PORT})
@@ -4446,6 +5490,8 @@ Options for apply:
4446
5490
  --source <locale> Source locale (default: en, else the first locale)
4447
5491
  --force Apply edits the translator saved with "Save anyway"
4448
5492
  --dry-run Show what would change without writing files
5493
+ --github-event Run inside the generated GitHub workflow; reads the link from the event file
5494
+ --branches <a,b> Branches a link may target (default: the repository's default branch)
4449
5495
 
4450
5496
  Global Options:
4451
5497
  -h, --help Show help information
@@ -4474,11 +5520,11 @@ function normalizeAppArg(value) {
4474
5520
  }
4475
5521
  }
4476
5522
  function isPortAnswering(port) {
4477
- return new Promise((resolve14) => {
5523
+ return new Promise((resolve16) => {
4478
5524
  const socket = net.connect({ host: "localhost", port });
4479
5525
  const done = (answering) => {
4480
5526
  socket.destroy();
4481
- resolve14(answering);
5527
+ resolve16(answering);
4482
5528
  };
4483
5529
  socket.setTimeout(1e3, () => done(false));
4484
5530
  socket.once("connect", () => done(true));
@@ -4488,8 +5534,8 @@ function isPortAnswering(port) {
4488
5534
  async function runStudio(port, appOrigin) {
4489
5535
  console.log(`
4490
5536
  \u{1F680} Starting LiveLocale Studio...`);
4491
- const studioDir = path15.resolve(moduleDir(), "../studio");
4492
- if (!fs10.existsSync(path15.join(studioDir, "index.html"))) {
5537
+ const studioDir = path18.resolve(moduleDir(), "../studio");
5538
+ if (!fs12.existsSync(path18.join(studioDir, "index.html"))) {
4493
5539
  console.error(`Error: Studio assets not found at "${studioDir}".`);
4494
5540
  console.error(`Did you build the project? Run "npm run build" first.`);
4495
5541
  process.exit(1);
@@ -4521,48 +5567,48 @@ async function runStudio(port, appOrigin) {
4521
5567
  function runInit(projectDir) {
4522
5568
  console.log(`
4523
5569
  \u{1F680} Initializing LiveLocale API handler...`);
4524
- const absoluteDir = path15.resolve(projectDir);
4525
- if (!fs10.existsSync(absoluteDir)) {
5570
+ const absoluteDir = path18.resolve(projectDir);
5571
+ if (!fs12.existsSync(absoluteDir)) {
4526
5572
  console.error(`Error: Directory "${projectDir}" does not exist.`);
4527
5573
  process.exit(1);
4528
5574
  }
4529
- const isTypeScript = fs10.existsSync(path15.join(absoluteDir, "tsconfig.json"));
5575
+ const isTypeScript = fs12.existsSync(path18.join(absoluteDir, "tsconfig.json"));
4530
5576
  const fileExt = isTypeScript ? "ts" : "js";
4531
5577
  console.log(`\u{1F4C1} Target: ${absoluteDir}`);
4532
5578
  console.log(`\u{1F527} Language detected: ${isTypeScript ? "TypeScript" : "JavaScript"}`);
4533
5579
  let hasNextIntl = false;
4534
5580
  let hasReactI18next = false;
4535
- const targetPkgPath = path15.join(absoluteDir, "package.json");
4536
- if (fs10.existsSync(targetPkgPath)) {
5581
+ const targetPkgPath = path18.join(absoluteDir, "package.json");
5582
+ if (fs12.existsSync(targetPkgPath)) {
4537
5583
  try {
4538
- const pkg = JSON.parse(fs10.readFileSync(targetPkgPath, "utf8"));
5584
+ const pkg = JSON.parse(fs12.readFileSync(targetPkgPath, "utf8"));
4539
5585
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4540
5586
  hasNextIntl = Boolean(allDeps["next-intl"]);
4541
5587
  hasReactI18next = Boolean(allDeps["react-i18next"]);
4542
5588
  } catch {
4543
5589
  }
4544
5590
  }
4545
- const appDirPath = path15.join(absoluteDir, "app");
4546
- const pagesDirPath = path15.join(absoluteDir, "pages");
4547
- const srcAppDirPath = path15.join(absoluteDir, "src", "app");
4548
- const srcPagesDirPath = path15.join(absoluteDir, "src", "pages");
5591
+ const appDirPath = path18.join(absoluteDir, "app");
5592
+ const pagesDirPath = path18.join(absoluteDir, "pages");
5593
+ const srcAppDirPath = path18.join(absoluteDir, "src", "app");
5594
+ const srcPagesDirPath = path18.join(absoluteDir, "src", "pages");
4549
5595
  let targetPath = "";
4550
5596
  let routerType = "";
4551
5597
  let content = "";
4552
- if (fs10.existsSync(appDirPath)) {
4553
- targetPath = path15.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
5598
+ if (fs12.existsSync(appDirPath)) {
5599
+ targetPath = path18.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
4554
5600
  routerType = "Next.js App Router (root)";
4555
- } else if (fs10.existsSync(srcAppDirPath)) {
4556
- targetPath = path15.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
5601
+ } else if (fs12.existsSync(srcAppDirPath)) {
5602
+ targetPath = path18.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
4557
5603
  routerType = "Next.js App Router (src/)";
4558
- } else if (fs10.existsSync(pagesDirPath)) {
4559
- targetPath = path15.join(pagesDirPath, "api", `livelocale.${fileExt}`);
5604
+ } else if (fs12.existsSync(pagesDirPath)) {
5605
+ targetPath = path18.join(pagesDirPath, "api", `livelocale.${fileExt}`);
4560
5606
  routerType = "Next.js Pages Router (root)";
4561
- } else if (fs10.existsSync(srcPagesDirPath)) {
4562
- targetPath = path15.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
5607
+ } else if (fs12.existsSync(srcPagesDirPath)) {
5608
+ targetPath = path18.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
4563
5609
  routerType = "Next.js Pages Router (src/)";
4564
5610
  } else {
4565
- targetPath = path15.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
5611
+ targetPath = path18.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
4566
5612
  routerType = "Default App Router (app/ not found)";
4567
5613
  }
4568
5614
  console.log(`\u{1F4CD} Project structure: ${routerType}`);
@@ -4590,14 +5636,14 @@ const handler = createLiveLocaleHandler({
4590
5636
  export default handler;
4591
5637
  `;
4592
5638
  }
4593
- const targetDir = path15.dirname(targetPath);
4594
- if (!fs10.existsSync(targetDir)) {
4595
- fs10.mkdirSync(targetDir, { recursive: true });
5639
+ const targetDir = path18.dirname(targetPath);
5640
+ if (!fs12.existsSync(targetDir)) {
5641
+ fs12.mkdirSync(targetDir, { recursive: true });
4596
5642
  }
4597
5643
  try {
4598
- fs10.writeFileSync(targetPath, content, "utf8");
5644
+ fs12.writeFileSync(targetPath, content, "utf8");
4599
5645
  console.log(`\u2705 Created API Route Handler successfully at:`);
4600
- console.log(` ${path15.relative(absoluteDir, targetPath)}`);
5646
+ console.log(` ${path18.relative(absoluteDir, targetPath)}`);
4601
5647
  if (hasNextIntl) {
4602
5648
  console.log(`
4603
5649
  \u{1F4A1} Detected 'next-intl' in your project!`);
@@ -4639,8 +5685,8 @@ async function runMigrate(args) {
4639
5685
  }
4640
5686
  }
4641
5687
  }
4642
- const absoluteDir = path15.resolve(dir);
4643
- if (!fs10.existsSync(absoluteDir)) {
5688
+ const absoluteDir = path18.resolve(dir);
5689
+ if (!fs12.existsSync(absoluteDir)) {
4644
5690
  console.error(`Error: Directory "${dir}" does not exist.`);
4645
5691
  process.exit(1);
4646
5692
  }
@@ -4664,28 +5710,28 @@ async function runMigrate(args) {
4664
5710
  let scannedCount = 0;
4665
5711
  let migratedCount = 0;
4666
5712
  async function walk(currentDir) {
4667
- const files = fs10.readdirSync(currentDir, { withFileTypes: true });
5713
+ const files = fs12.readdirSync(currentDir, { withFileTypes: true });
4668
5714
  for (const file of files) {
4669
- const fullPath = path15.join(currentDir, file.name);
5715
+ const fullPath = path18.join(currentDir, file.name);
4670
5716
  if (file.isDirectory()) {
4671
5717
  if (excludes.includes(file.name)) {
4672
5718
  continue;
4673
5719
  }
4674
5720
  await walk(fullPath);
4675
5721
  } else if (file.isFile()) {
4676
- const ext = path15.extname(file.name);
5722
+ const ext = path18.extname(file.name);
4677
5723
  if ([".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
4678
5724
  scannedCount++;
4679
5725
  try {
4680
- const content = fs10.readFileSync(fullPath, "utf8");
5726
+ const content = fs12.readFileSync(fullPath, "utf8");
4681
5727
  const result = await transformReactI18next2(content, fullPath);
4682
5728
  if (result.modified) {
4683
5729
  migratedCount++;
4684
- const relativePath = path15.relative(absoluteDir, fullPath);
5730
+ const relativePath = path18.relative(absoluteDir, fullPath);
4685
5731
  if (dryRun) {
4686
5732
  console.log(`[DRY RUN] Would migrate: ${relativePath}`);
4687
5733
  } else {
4688
- fs10.writeFileSync(fullPath, result.code, "utf8");
5734
+ fs12.writeFileSync(fullPath, result.code, "utf8");
4689
5735
  console.log(`\u2705 Migrated: ${relativePath}`);
4690
5736
  }
4691
5737
  }
@@ -4734,8 +5780,8 @@ async function runDoctorCommand(args) {
4734
5780
  dir = arg;
4735
5781
  }
4736
5782
  }
4737
- const projectDir = path15.resolve(dir);
4738
- if (!fs10.existsSync(projectDir)) {
5783
+ const projectDir = path18.resolve(dir);
5784
+ if (!fs12.existsSync(projectDir)) {
4739
5785
  console.error(`Error: Directory "${dir}" does not exist.`);
4740
5786
  process.exit(1);
4741
5787
  }
@@ -4796,8 +5842,8 @@ async function runCheckCommand(args) {
4796
5842
  dir = arg;
4797
5843
  }
4798
5844
  }
4799
- const projectDir = path15.resolve(dir);
4800
- if (!fs10.existsSync(projectDir)) {
5845
+ const projectDir = path18.resolve(dir);
5846
+ if (!fs12.existsSync(projectDir)) {
4801
5847
  console.error(`Error: Directory "${dir}" does not exist.`);
4802
5848
  process.exit(1);
4803
5849
  }
@@ -4838,7 +5884,7 @@ async function runCheckCommand(args) {
4838
5884
  var PACK_USAGE = "Usage: livelocale pack <outDir> [options]";
4839
5885
  function printPackSummary(result, projectDir, warnings) {
4840
5886
  for (const warning of warnings) console.log(`\u26A0\uFE0F ${warning}`);
4841
- const outRel = path15.relative(projectDir, path15.dirname(result.shareDir)).split(path15.sep).join("/") || ".";
5887
+ const outRel = path18.relative(projectDir, path18.dirname(result.shareDir)).split(path18.sep).join("/") || ".";
4842
5888
  console.log(
4843
5889
  `\u2705 LiveLocale share folder written to ${outRel}/livelocale/ (${result.manifest.locales.length} locales, ${result.manifest.codeKeys.length} code keys, ${result.htmlInjected.length} HTML files linked).`
4844
5890
  );
@@ -4850,7 +5896,7 @@ async function runPackCommand(args) {
4850
5896
  if (options.positional.length !== 1 || options.out !== void 0 || options.preview || options.command.length > 0) {
4851
5897
  throw new PackError("USAGE", PACK_USAGE);
4852
5898
  }
4853
- const projectDir = path15.resolve(options.dir);
5899
+ const projectDir = path18.resolve(options.dir);
4854
5900
  const result = await packOutput({
4855
5901
  projectDir,
4856
5902
  outDir: options.positional[0],
@@ -4873,7 +5919,7 @@ async function runBuildCli(args) {
4873
5919
  if (options.positional.length > 0 || !options.inject) {
4874
5920
  throw new PackError("USAGE", BUILD_USAGE);
4875
5921
  }
4876
- const projectDir = path15.resolve(options.dir);
5922
+ const projectDir = path18.resolve(options.dir);
4877
5923
  const result = await runBuildCommand({
4878
5924
  projectDir,
4879
5925
  command: options.command,
@@ -4896,6 +5942,55 @@ async function runBuildCli(args) {
4896
5942
  process.exitCode = 1;
4897
5943
  }
4898
5944
  }
5945
+ async function runInitShareCommand(args) {
5946
+ let dir = ".";
5947
+ let localesPath;
5948
+ let sourceLocale;
5949
+ let branches;
5950
+ let overwrite = false;
5951
+ for (let i = 1; i < args.length; i++) {
5952
+ const arg = args[i];
5953
+ if (arg === "--dir") {
5954
+ dir = args[++i] || ".";
5955
+ } else if (arg === "--locales") {
5956
+ localesPath = args[++i];
5957
+ } else if (arg === "--source") {
5958
+ sourceLocale = args[++i];
5959
+ } else if (arg === "--branches") {
5960
+ branches = (args[++i] ?? "").split(",").map((b) => b.trim()).filter(Boolean);
5961
+ } else if (arg === "--overwrite") {
5962
+ overwrite = true;
5963
+ }
5964
+ }
5965
+ const projectDir = path18.resolve(dir);
5966
+ if (!fs12.existsSync(projectDir)) {
5967
+ console.error(`Error: Directory "${dir}" does not exist.`);
5968
+ process.exit(1);
5969
+ }
5970
+ try {
5971
+ const plan = await planInitShare({
5972
+ projectDir,
5973
+ localesPath,
5974
+ sourceLocale,
5975
+ branches,
5976
+ version: readOwnVersion(),
5977
+ overwrite
5978
+ });
5979
+ writePlannedFiles(projectDir, plan.files);
5980
+ console.log(`
5981
+ \u{1F680} LiveLocale share link setup (${plan.stack})`);
5982
+ for (const file of plan.files) {
5983
+ console.log(
5984
+ ` ${file.action.padEnd(9)} ${file.path}${file.reason ? ` (${file.reason})` : ""}`
5985
+ );
5986
+ }
5987
+ for (const note of plan.notes) console.log(`\u{1F4A1} ${note}`);
5988
+ console.log("Commit and push: the workflow must be on the default branch.");
5989
+ } catch (err) {
5990
+ console.error(`\u274C ${err.message}`);
5991
+ process.exitCode = 1;
5992
+ }
5993
+ }
4899
5994
  async function run2() {
4900
5995
  const args = process.argv.slice(2);
4901
5996
  if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
@@ -4928,6 +6023,10 @@ async function run2() {
4928
6023
  await runApplyCommand(args.slice(1));
4929
6024
  return;
4930
6025
  }
6026
+ if (args[0] === "init" && args.includes("--share")) {
6027
+ await runInitShareCommand(args);
6028
+ return;
6029
+ }
4931
6030
  if (args[0] === "init") {
4932
6031
  let dir = ".";
4933
6032
  for (let i = 1; i < args.length; i++) {