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.
@@ -292,11 +292,12 @@ var init_transform = __esm({
292
292
  });
293
293
 
294
294
  // src/codemod/index.ts
295
- import * as fs10 from "fs";
296
- import * as path15 from "path";
295
+ import * as fs12 from "fs";
296
+ import * as path18 from "path";
297
297
  import * as net from "net";
298
298
 
299
299
  // src/server/module-dir.ts
300
+ import { readFileSync } from "fs";
300
301
  import * as path from "path";
301
302
  import { fileURLToPath } from "url";
302
303
  function moduleDir() {
@@ -305,6 +306,15 @@ function moduleDir() {
305
306
  }
306
307
  return path.dirname(fileURLToPath(import.meta.url));
307
308
  }
309
+ function readOwnVersion() {
310
+ const file = path.resolve(moduleDir(), "../../package.json");
311
+ const pkg = JSON.parse(readFileSync(file, "utf-8"));
312
+ const version = pkg?.version;
313
+ if (typeof version !== "string" || version === "") {
314
+ throw new Error(`No version in ${file}.`);
315
+ }
316
+ return version;
317
+ }
308
318
 
309
319
  // src/codemod/studio-server.ts
310
320
  import * as fs from "fs";
@@ -729,7 +739,7 @@ function isShareManifest(value) {
729
739
  const layout = own2(value, "layout");
730
740
  const locales = own2(value, "locales");
731
741
  const sourceLocale = own2(value, "sourceLocale");
732
- return own2(value, "format") === SHARE_MANIFEST_FORMAT && (repo === void 0 || typeof repo === "string" && isValidRepoSlug(repo)) && isNullOr(own2(value, "defaultBranch"), isValidBranchName) && isNullOr(own2(value, "branch"), isValidBranchName) && isNullOr(own2(value, "base"), (base) => BASE_SHA_RE2.test(base)) && typeof own2(value, "localesPath") === "string" && (layout === "flat" || layout === "namespaced") && isArrayOf(locales, isLocaleCode) && typeof sourceLocale === "string" && locales.includes(sourceLocale) && isNullOr(own2(value, "basePath"), (path16) => path16.startsWith("/") && path16.endsWith("/")) && isArrayOf(own2(value, "codeKeys"), isString);
742
+ 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);
733
743
  }
734
744
 
735
745
  // src/codemod/doctor.ts
@@ -2233,23 +2243,23 @@ function formatCheckReport(report, limit = 25) {
2233
2243
 
2234
2244
  // src/server/pack.ts
2235
2245
  import { spawn } from "child_process";
2236
- import { existsSync as existsSync5, readFileSync as readFileSync5, statSync as statSync4, promises as fs5 } from "fs";
2246
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4, promises as fs5 } from "fs";
2237
2247
  import * as path8 from "path";
2238
2248
 
2239
2249
  // src/server/build-info.ts
2240
2250
  import { execFile } from "child_process";
2241
- import { readFileSync as readFileSync3 } from "fs";
2251
+ import { readFileSync as readFileSync4 } from "fs";
2242
2252
  import * as path6 from "path";
2243
- var runGit = (args, cwd) => new Promise((resolve14) => {
2253
+ var runGit = (args, cwd) => new Promise((resolve16) => {
2244
2254
  try {
2245
2255
  execFile(
2246
2256
  "git",
2247
2257
  args,
2248
2258
  { cwd, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
2249
- (error, stdout) => resolve14(error ? null : String(stdout).trim())
2259
+ (error, stdout) => resolve16(error ? null : String(stdout).trim())
2250
2260
  );
2251
2261
  } catch {
2252
- resolve14(null);
2262
+ resolve16(null);
2253
2263
  }
2254
2264
  });
2255
2265
  var SHA_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
@@ -2268,7 +2278,7 @@ var asSlug = (value) => isValidRepoSlug(value) ? value : null;
2268
2278
  function readEvent(eventPath) {
2269
2279
  if (!eventPath) return {};
2270
2280
  try {
2271
- const parsed = JSON.parse(readFileSync3(eventPath, "utf-8"));
2281
+ const parsed = JSON.parse(readFileSync4(eventPath, "utf-8"));
2272
2282
  return isRecord(parsed) ? parsed : {};
2273
2283
  } catch {
2274
2284
  return {};
@@ -2367,7 +2377,7 @@ function parseGitHubSlug(remote) {
2367
2377
  }
2368
2378
 
2369
2379
  // src/server/build-target.ts
2370
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
2380
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2371
2381
  import * as path7 from "path";
2372
2382
  var NEXT_CONFIG_FILES = [
2373
2383
  "next.config.js",
@@ -2389,7 +2399,7 @@ var VITE_BASE = /\bbase\s*:\s*['"`](\/[^'"`]*|\.\/?|)['"`]/;
2389
2399
  var EXPORTS_POST = /export\s+(?:const|let|var|async\s+function|function)\s+POST\b|export\s*\{[^}]*\bPOST\b[^}]*\}/;
2390
2400
  function readText2(file) {
2391
2401
  try {
2392
- return readFileSync4(file, "utf-8");
2402
+ return readFileSync5(file, "utf-8");
2393
2403
  } catch {
2394
2404
  return null;
2395
2405
  }
@@ -2541,8 +2551,8 @@ var NO_REPO_WARNING = "exposeRepo is on but no GitHub repository was found; the
2541
2551
  function isPlainObject2(value) {
2542
2552
  return typeof value === "object" && value !== null && !Array.isArray(value);
2543
2553
  }
2544
- function toPosix(relative8) {
2545
- return relative8.split(path8.sep).join("/") || ".";
2554
+ function toPosix(relative9) {
2555
+ return relative9.split(path8.sep).join("/") || ".";
2546
2556
  }
2547
2557
  function isInside(parent, child) {
2548
2558
  const rel = path8.relative(parent, child);
@@ -2841,7 +2851,7 @@ function quoteWindowsArg(arg) {
2841
2851
  }
2842
2852
  return out + "\\".repeat(backslashes * 2) + '"';
2843
2853
  }
2844
- var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reject2) => {
2854
+ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve16, reject2) => {
2845
2855
  const child = process.platform === "win32" ? spawn([command, ...args].map(quoteWindowsArg).join(" "), {
2846
2856
  cwd,
2847
2857
  env,
@@ -2849,10 +2859,10 @@ var spawnInherit = (command, args, { cwd, env }) => new Promise((resolve14, reje
2849
2859
  shell: true
2850
2860
  }) : spawn(command, args, { cwd, env, stdio: "inherit" });
2851
2861
  child.on("error", (error) => {
2852
- if (error.code === "ENOENT") resolve14(127);
2862
+ if (error.code === "ENOENT") resolve16(127);
2853
2863
  else reject2(error);
2854
2864
  });
2855
- child.on("close", (code) => resolve14(code ?? 1));
2865
+ child.on("close", (code) => resolve16(code ?? 1));
2856
2866
  });
2857
2867
  var BUILD_USAGE = "Usage: livelocale build [--preview] -- <your build command>";
2858
2868
  var NEXT_PREVIEW_WARNING = "Next.js pages have no watermarks without --preview; the Studio will open but nothing is editable.";
@@ -2861,7 +2871,7 @@ var FRESH_SLACK_MS = 2e3;
2861
2871
  function gitignoresShareDir(projectDir) {
2862
2872
  let content;
2863
2873
  try {
2864
- content = readFileSync5(path8.join(projectDir, ".gitignore"), "utf-8");
2874
+ content = readFileSync6(path8.join(projectDir, ".gitignore"), "utf-8");
2865
2875
  } catch {
2866
2876
  return false;
2867
2877
  }
@@ -2929,122 +2939,12 @@ async function runBuildCommand(options) {
2929
2939
  }
2930
2940
 
2931
2941
  // src/codemod/apply/cli.ts
2932
- import * as path14 from "path";
2942
+ import * as path16 from "path";
2933
2943
 
2934
- // src/codemod/apply/link-input.ts
2935
- import { execFile as execFile2 } from "child_process";
2944
+ // src/codemod/apply/github-event.ts
2936
2945
  import * as fs6 from "fs";
2937
- import * as path9 from "path";
2938
- var MAX_APPLY_INPUT_BYTES = 1048576;
2939
- var TOO_LARGE_MESSAGE2 = "The input is larger than 1 MB.";
2940
- var NO_INPUT_MESSAGE = 'Pass the change link, a file that holds it, "-" to read stdin, or --clipboard. (cmd.exe cuts arguments at 8,191 characters; use --clipboard or a file for long links.)';
2941
- var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
2942
- var ApplyInputError = class extends Error {
2943
- constructor(message) {
2944
- super(message);
2945
- this.name = "ApplyInputError";
2946
- }
2947
- };
2948
- function checkSize(text) {
2949
- if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
2950
- throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2951
- }
2952
- return text;
2953
- }
2954
- async function readStream(stream) {
2955
- const chunks = [];
2956
- let size = 0;
2957
- for await (const chunk of stream) {
2958
- const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
2959
- size += bytes.length;
2960
- if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2961
- chunks.push(bytes);
2962
- }
2963
- return Buffer.concat(chunks).toString("utf8");
2964
- }
2965
- function existingFile(arg, cwd) {
2966
- try {
2967
- const resolved = path9.resolve(cwd, arg);
2968
- return fs6.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
2969
- } catch {
2970
- return null;
2971
- }
2972
- }
2973
- async function readApplyInput(arg, options) {
2974
- if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
2975
- if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
2976
- if (arg === "-") return readStream(options.stdin ?? process.stdin);
2977
- const file = existingFile(arg, options.cwd);
2978
- if (file === null) return checkSize(arg);
2979
- if (fs6.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
2980
- return checkSize(fs6.readFileSync(file, "utf8"));
2981
- }
2982
- function clipboardCommands(platform) {
2983
- if (platform === "win32") {
2984
- return [
2985
- ["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
2986
- ];
2987
- }
2988
- if (platform === "darwin") return [["pbpaste", []]];
2989
- return [
2990
- ["wl-paste", ["--no-newline"]],
2991
- ["xclip", ["-selection", "clipboard", "-o"]],
2992
- ["xsel", ["--clipboard", "--output"]]
2993
- ];
2994
- }
2995
- var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
2996
- function run(execFileImpl, [command, args]) {
2997
- return new Promise((resolve14) => {
2998
- try {
2999
- execFileImpl(
3000
- command,
3001
- args,
3002
- { encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
3003
- (error, stdout) => {
3004
- const code = error?.code;
3005
- if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve14(TOO_LARGE);
3006
- else resolve14(error ? null : String(stdout));
3007
- }
3008
- );
3009
- } catch {
3010
- resolve14(null);
3011
- }
3012
- });
3013
- }
3014
- async function readClipboard(execFileImpl = execFile2, platform = process.platform) {
3015
- for (const command of clipboardCommands(platform)) {
3016
- const text = await run(execFileImpl, command);
3017
- if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE2);
3018
- if (text !== null) return checkSize(text);
3019
- }
3020
- throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
3021
- }
3022
-
3023
- // src/codemod/apply/local.ts
3024
- import { execFile as execFile4, spawn as spawn2 } from "child_process";
3025
- import * as fs9 from "fs";
3026
2946
  import * as os from "os";
3027
- import * as path13 from "path";
3028
-
3029
- // src/core/change-link-node.ts
3030
- import { inflateSync } from "zlib";
3031
- var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
3032
- var TOO_LARGE_MESSAGE3 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
3033
- var nodeInflate = async (bytes, maxOutputLength) => {
3034
- let result;
3035
- try {
3036
- result = inflateSync(bytes, { maxOutputLength, info: true });
3037
- } catch (err) {
3038
- if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
3039
- throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE3);
3040
- }
3041
- throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3042
- }
3043
- if (result.engine.bytesWritten !== bytes.length) {
3044
- throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3045
- }
3046
- return result.buffer;
3047
- };
2947
+ import * as path10 from "path";
3048
2948
 
3049
2949
  // src/client/watermark.ts
3050
2950
  var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
@@ -3112,6 +3012,11 @@ function detectJsonIndent(content) {
3112
3012
  const ws = match[1];
3113
3013
  return ws.includes(" ") ? " " : ws;
3114
3014
  }
3015
+ function stringifyLike(data, content) {
3016
+ const indent = content === null ? " " : detectJsonIndent(content);
3017
+ const trailing = content === null || content.endsWith("\n") ? "\n" : "";
3018
+ return JSON.stringify(data, null, indent) + trailing;
3019
+ }
3115
3020
 
3116
3021
  // src/core/save-rules.ts
3117
3022
  var reject = (status, code, error) => ({
@@ -3317,113 +3222,1064 @@ function planApply(input) {
3317
3222
  return { edits, counts };
3318
3223
  }
3319
3224
 
3320
- // src/server/file-mutator.ts
3321
- import { promises as fs7 } from "fs";
3322
- import * as path11 from "path";
3323
-
3324
- // src/core/safe-path.ts
3325
- import * as path10 from "path";
3326
- function resolveSafeLocalePath(basePath, locale) {
3327
- if (!isSafeLocaleSegment(locale)) {
3328
- throw new Error("Path traversal attempt");
3225
+ // src/core/change-link-node.ts
3226
+ import { inflateSync } from "zlib";
3227
+ var CORRUPT_MESSAGE2 = "This change link is damaged or incomplete. Ask the sender to send the file instead.";
3228
+ var TOO_LARGE_MESSAGE2 = `A change link holds at most ${MAX_INFLATED_BYTES / 1024} KB of text. Send the changes in smaller links.`;
3229
+ var nodeInflate = async (bytes, maxOutputLength) => {
3230
+ let result;
3231
+ try {
3232
+ result = inflateSync(bytes, { maxOutputLength, info: true });
3233
+ } catch (err) {
3234
+ if (err instanceof Error && "code" in err && err.code === "ERR_BUFFER_TOO_LARGE") {
3235
+ throw new ChangeLinkError("TOO_LARGE", TOO_LARGE_MESSAGE2);
3236
+ }
3237
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3329
3238
  }
3330
- const resolvedBasePath = path10.resolve(basePath);
3331
- const filePath = path10.resolve(resolvedBasePath, `${locale}.json`);
3332
- if (!filePath.startsWith(resolvedBasePath)) {
3333
- throw new Error("Path traversal attempt");
3239
+ if (result.engine.bytesWritten !== bytes.length) {
3240
+ throw new ChangeLinkError("CORRUPT", CORRUPT_MESSAGE2);
3334
3241
  }
3335
- return filePath;
3336
- }
3242
+ return result.buffer;
3243
+ };
3337
3244
 
3338
- // src/server/file-mutator.ts
3339
- var FileMutex = class {
3340
- queue = Promise.resolve();
3341
- async acquire(fn) {
3342
- const next = this.queue.then(fn);
3343
- this.queue = next.then(
3344
- () => {
3345
- },
3346
- () => {
3347
- }
3348
- );
3349
- return next;
3245
+ // src/server/github-rest.ts
3246
+ var GitHubApiError = class extends Error {
3247
+ constructor(message, status) {
3248
+ super(message);
3249
+ this.status = status;
3250
+ this.name = "GitHubApiError";
3350
3251
  }
3252
+ status;
3351
3253
  };
3352
- var FileMutator = class {
3353
- fileLocks = /* @__PURE__ */ new Map();
3354
- requireEnabled;
3355
- quiet;
3356
- scanner;
3357
- constructor(options = {}) {
3358
- this.requireEnabled = options.requireEnabled ?? true;
3359
- this.quiet = options.quiet ?? false;
3360
- this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
3361
- }
3362
- assertEnabled() {
3363
- if (!this.requireEnabled) return;
3364
- const reason = getDisabledReason();
3365
- if (reason !== null) {
3366
- throw new Error(
3367
- `[livelocale] FileMutator can only be executed in development mode (${reason}).`
3368
- );
3369
- }
3254
+ var CHUNK_BYTES = 32768;
3255
+ function encodeBase64Utf8(text) {
3256
+ const bytes = new TextEncoder().encode(text);
3257
+ let binary = "";
3258
+ for (let i = 0; i < bytes.length; i += CHUNK_BYTES) {
3259
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_BYTES));
3370
3260
  }
3371
- /**
3372
- * Helper to serialize execution per file path
3373
- */
3374
- async withLock(filePath, fn) {
3375
- let lock = this.fileLocks.get(filePath);
3376
- if (!lock) {
3377
- lock = new FileMutex();
3378
- this.fileLocks.set(filePath, lock);
3261
+ return btoa(binary);
3262
+ }
3263
+ function decodeBase64Utf8(base64) {
3264
+ const binary = atob(base64.replace(/\s/g, ""));
3265
+ const bytes = new Uint8Array(binary.length);
3266
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3267
+ return new TextDecoder().decode(bytes);
3268
+ }
3269
+ function isPullRequestCreationForbidden(err) {
3270
+ return err instanceof GitHubApiError && err.status === 403 && /not permitted to create or approve pull requests/i.test(err.message);
3271
+ }
3272
+ function encodeRefPath(ref) {
3273
+ return ref.split("/").map(encodeURIComponent).join("/");
3274
+ }
3275
+ function createGitHubRest(config) {
3276
+ const repo = config.repo;
3277
+ const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
3278
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
3279
+ const owner = repo.split("/")[0] ?? "";
3280
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
3281
+ throw new Error(`[livelocale] github.repo must be "owner/name", got "${repo}".`);
3282
+ }
3283
+ if (typeof fetchImpl !== "function") {
3284
+ throw new Error("[livelocale] The GitHub gateway needs a global fetch (Node 18+).");
3285
+ }
3286
+ const headers = (hasBody) => ({
3287
+ Authorization: `Bearer ${config.token}`,
3288
+ Accept: "application/vnd.github+json",
3289
+ "X-GitHub-Api-Version": "2022-11-28",
3290
+ ...hasBody ? { "Content-Type": "application/json" } : {}
3291
+ });
3292
+ async function api(method, path19, options = {}) {
3293
+ const url = new URL(`${apiBase}/repos/${repo}${path19}`);
3294
+ for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
3295
+ const response = await fetchImpl(url.toString(), {
3296
+ method,
3297
+ headers: headers(options.body !== void 0),
3298
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
3299
+ });
3300
+ let data = null;
3301
+ try {
3302
+ data = await response.json();
3303
+ } catch {
3379
3304
  }
3380
- return lock.acquire(fn);
3381
- }
3382
- /**
3383
- * Updates a locale JSON file on disk atomically and safely.
3384
- * Enforces development mode checks, path traversal mitigation, key depth restrictions,
3385
- * structural JSON validation, and concurrency serialization.
3386
- */
3387
- async updateLocaleKey(basePath, locale, keyPath, newValue) {
3388
- this.assertEnabled();
3389
- if (!isSafeLocaleSegment(locale)) {
3390
- throw new Error(
3391
- `[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
3305
+ if (response.status === 404 && options.allow404) return { status: 404, data: null };
3306
+ if (!response.ok) {
3307
+ const detail = data?.message || "request failed";
3308
+ 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.' : "";
3309
+ throw new GitHubApiError(
3310
+ `GitHub API ${response.status} on ${method} ${path19}: ${detail}.${hint}`,
3311
+ response.status
3392
3312
  );
3393
3313
  }
3394
- const resolvedBasePath = path11.resolve(basePath);
3395
- const localeDir = path11.resolve(resolvedBasePath, locale);
3396
- let filePath = "";
3397
- let mutationKeyPath = keyPath;
3398
- let isNamespaceDir = false;
3314
+ return { status: response.status, data };
3315
+ }
3316
+ async function probeStatus(method, path19, body) {
3317
+ const response = await fetchImpl(`${apiBase}/repos/${repo}${path19}`, {
3318
+ method,
3319
+ headers: headers(body !== void 0),
3320
+ body: body !== void 0 ? JSON.stringify(body) : void 0
3321
+ });
3399
3322
  try {
3400
- const stats = await fs7.stat(localeDir);
3401
- if (stats.isDirectory()) {
3402
- isNamespaceDir = true;
3403
- }
3323
+ await response.arrayBuffer();
3404
3324
  } catch {
3405
3325
  }
3406
- if (isNamespaceDir) {
3407
- const firstDot = keyPath.indexOf(".");
3408
- if (firstDot !== -1) {
3409
- const namespace = keyPath.substring(0, firstDot);
3410
- if (DANGEROUS_KEY_SEGMENTS.has(namespace)) {
3411
- throw new Error(
3412
- `[livelocale] Forbidden namespace segment in path: "${namespace}". Segment names like __proto__, constructor, and prototype are not allowed.`
3413
- );
3414
- }
3415
- mutationKeyPath = keyPath.substring(firstDot + 1);
3416
- filePath = path11.resolve(localeDir, `${namespace}.json`);
3417
- } else {
3418
- filePath = path11.resolve(localeDir, `${keyPath}.json`);
3419
- mutationKeyPath = "";
3326
+ return response.status;
3327
+ }
3328
+ async function refSha(branch) {
3329
+ const res = await api(
3330
+ "GET",
3331
+ `/git/ref/heads/${encodeRefPath(branch)}`,
3332
+ { allow404: true }
3333
+ );
3334
+ return res.data?.object?.sha ?? null;
3335
+ }
3336
+ async function findOpenPullRequest(head, base) {
3337
+ const res = await api("GET", "/pulls", {
3338
+ query: { head: `${owner}:${head}`, base, state: "open" }
3339
+ });
3340
+ const pull = res.data?.[0];
3341
+ return pull ? { number: pull.number, url: pull.html_url } : null;
3342
+ }
3343
+ async function openOrReusePullRequest(input) {
3344
+ const existing = await findOpenPullRequest(input.head, input.base);
3345
+ if (existing) {
3346
+ if (input.updateBodyIfReused) {
3347
+ await api("PATCH", `/pulls/${existing.number}`, { body: { body: input.body } });
3420
3348
  }
3421
- } else {
3422
- filePath = path11.resolve(resolvedBasePath, `${locale}.json`);
3349
+ return { ...existing, reused: true };
3423
3350
  }
3424
- if (!filePath.startsWith(resolvedBasePath)) {
3351
+ const res = await api("POST", "/pulls", {
3352
+ body: { title: input.title, head: input.head, base: input.base, body: input.body }
3353
+ });
3354
+ return { number: res.data.number, url: res.data.html_url, reused: false };
3355
+ }
3356
+ return { repo, api, probeStatus, refSha, findOpenPullRequest, openOrReusePullRequest };
3357
+ }
3358
+
3359
+ // src/codemod/apply/github-commit.ts
3360
+ import * as path9 from "path";
3361
+ var BRANCH_PREFIX = "livelocale/";
3362
+ function normalizeRepoDir(p) {
3363
+ const posix2 = p.replace(/\\/g, "/");
3364
+ if (posix2.startsWith("/") || /^[A-Za-z]:/.test(posix2)) {
3365
+ throw new Error(`[livelocale] The locales path must be relative to the repository: "${p}".`);
3366
+ }
3367
+ const segments = posix2.split("/").filter((s) => s !== "" && s !== ".");
3368
+ if (segments.includes("..")) {
3369
+ throw new Error(`[livelocale] The locales path must stay inside the repository: "${p}".`);
3370
+ }
3371
+ return segments.join("/");
3372
+ }
3373
+ function joinRepoPath(dir, rest) {
3374
+ return dir === "" ? rest : `${dir}/${rest}`;
3375
+ }
3376
+ function localeFileFor(localesDir, layout, locale, key) {
3377
+ const dir = normalizeRepoDir(localesDir);
3378
+ if (layout === "flat") return { file: joinRepoPath(dir, `${locale}.json`), innerKey: key };
3379
+ const dot = key.indexOf(".");
3380
+ if (dot <= 0 || dot === key.length - 1) {
3381
+ throw new Error(`[livelocale] "${key}" names no key inside a namespace file.`);
3382
+ }
3383
+ return {
3384
+ file: joinRepoPath(dir, `${locale}/${key.slice(0, dot)}.json`),
3385
+ innerKey: key.slice(dot + 1)
3386
+ };
3387
+ }
3388
+ function isPlainObject4(value) {
3389
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3390
+ }
3391
+ function applyEditsToText(text, edits) {
3392
+ const data = JSON.parse(text);
3393
+ if (!isPlainObject4(data)) throw new Error("[livelocale] A locale file is not a JSON object.");
3394
+ for (const edit of edits) setNestedProperty(data, edit.innerKey, edit.value);
3395
+ return stringifyLike(data, text);
3396
+ }
3397
+ async function listDir(rest, dir, ref) {
3398
+ const res = await rest.api("GET", `/contents/${encodeRefPath(dir)}`, {
3399
+ query: { ref },
3400
+ allow404: true
3401
+ });
3402
+ return Array.isArray(res.data) ? res.data : null;
3403
+ }
3404
+ async function readBlob(rest, sha) {
3405
+ const res = await rest.api(
3406
+ "GET",
3407
+ `/git/blobs/${encodeURIComponent(sha)}`
3408
+ );
3409
+ return decodeBase64Utf8(res.data?.content ?? "");
3410
+ }
3411
+ function parseLocaleFile(text, file) {
3412
+ let data;
3413
+ try {
3414
+ data = JSON.parse(text);
3415
+ } catch {
3416
+ throw new Error(`[livelocale] ${file} is not valid JSON.`);
3417
+ }
3418
+ if (!isPlainObject4(data)) throw new Error(`[livelocale] ${file} is not a JSON object.`);
3419
+ return data;
3420
+ }
3421
+ async function readRemoteLocales(rest, commitSha, localesDir, locales) {
3422
+ const dir = normalizeRepoDir(localesDir);
3423
+ const entries = await listDir(rest, dir, commitSha);
3424
+ if (entries === null) {
3425
+ throw new Error(`[livelocale] No locales folder at ${dir || "."} on the target.`);
3426
+ }
3427
+ const targets = /* @__PURE__ */ new Map();
3428
+ const files = /* @__PURE__ */ new Map();
3429
+ for (const locale of locales) {
3430
+ if (targets.has(locale)) continue;
3431
+ const flat = entries.find((e) => e.type === "file" && e.name === `${locale}.json`);
3432
+ const folder = entries.find((e) => e.type === "dir" && e.name === locale);
3433
+ if (!isValidLocaleFormat(locale) || !flat && !folder) {
3434
+ targets.set(locale, null);
3435
+ continue;
3436
+ }
3437
+ if (flat) {
3438
+ const file = joinRepoPath(dir, flat.name);
3439
+ const text = await readBlob(rest, flat.sha);
3440
+ files.set(file, text);
3441
+ targets.set(locale, { layout: "flat", dict: parseLocaleFile(text, file) });
3442
+ continue;
3443
+ }
3444
+ const dict = {};
3445
+ for (const entry of await listDir(rest, joinRepoPath(dir, locale), commitSha) ?? []) {
3446
+ if (entry.type !== "file" || !entry.name.endsWith(".json")) continue;
3447
+ const file = joinRepoPath(dir, `${locale}/${entry.name}`);
3448
+ const text = await readBlob(rest, entry.sha);
3449
+ files.set(file, text);
3450
+ dict[entry.name.slice(0, -".json".length)] = parseLocaleFile(text, file);
3451
+ }
3452
+ targets.set(locale, { layout: "namespaced", dict });
3453
+ }
3454
+ return { sha: commitSha, targets, files };
3455
+ }
3456
+ function buildFileChanges(localesDir, plan, remote, baseFiles) {
3457
+ const dir = normalizeRepoDir(localesDir);
3458
+ const prefix = dir === "" ? "" : `${dir}/`;
3459
+ const byFile = /* @__PURE__ */ new Map();
3460
+ for (const edit of plan.edits) {
3461
+ if (edit.status !== "apply") continue;
3462
+ const layout = remote.targets.get(edit.locale)?.layout ?? "flat";
3463
+ const { file, innerKey } = localeFileFor(dir, layout, edit.locale, edit.key);
3464
+ const normalized = path9.posix.normalize(file);
3465
+ if (normalized !== file || normalized.startsWith("../") || normalized.startsWith("/") || !normalized.startsWith(prefix)) {
3425
3466
  throw new Error(
3426
- `[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
3467
+ `[livelocale] Refusing to write ${file}: it is outside the locales folder ${dir || "."}.`
3468
+ );
3469
+ }
3470
+ const list = byFile.get(file) ?? [];
3471
+ list.push({ innerKey, value: edit.to });
3472
+ byFile.set(file, list);
3473
+ }
3474
+ const changes = /* @__PURE__ */ new Map();
3475
+ for (const [file, edits] of byFile) {
3476
+ const text = baseFiles?.get(file) ?? remote.files.get(file);
3477
+ if (text === void 0) {
3478
+ throw new Error(`[livelocale] ${file} was not read from the target.`);
3479
+ }
3480
+ const next = applyEditsToText(text, edits);
3481
+ if (next !== text) changes.set(file, next);
3482
+ }
3483
+ return changes;
3484
+ }
3485
+ async function prepareBranch(rest, baseName, base, targetSha, runId) {
3486
+ const head = await rest.refSha(baseName);
3487
+ if (head === null) return { branch: baseName, parentSha: targetSha, mode: "new" };
3488
+ if (await rest.findOpenPullRequest(baseName, base)) {
3489
+ return { branch: baseName, parentSha: head, mode: "append" };
3490
+ }
3491
+ return { branch: `${baseName}-${runId}`, parentSha: targetSha, mode: "new" };
3492
+ }
3493
+ async function commitFiles(rest, input) {
3494
+ const { branch, parentSha, mode } = input.branchPlan;
3495
+ if (!branch.startsWith(BRANCH_PREFIX)) {
3496
+ throw new Error(`[livelocale] Refusing to write "${branch}": only ${BRANCH_PREFIX}* branches.`);
3497
+ }
3498
+ if (input.files.size === 0) return { sha: null, unchanged: true };
3499
+ const parent = await rest.api(
3500
+ "GET",
3501
+ `/git/commits/${encodeURIComponent(parentSha)}`
3502
+ );
3503
+ const baseTree = parent.data.tree.sha;
3504
+ const entries = [];
3505
+ for (const [file, text] of input.files) {
3506
+ const blob = await rest.api("POST", "/git/blobs", {
3507
+ body: { content: encodeBase64Utf8(text), encoding: "base64" }
3508
+ });
3509
+ entries.push({ path: file, mode: "100644", type: "blob", sha: blob.data.sha });
3510
+ }
3511
+ const tree = await rest.api("POST", "/git/trees", {
3512
+ body: { base_tree: baseTree, tree: entries }
3513
+ });
3514
+ const treeSha = tree.data.sha;
3515
+ if (treeSha === baseTree) return { sha: null, unchanged: true };
3516
+ const commit = await rest.api("POST", "/git/commits", {
3517
+ body: { message: input.message, tree: treeSha, parents: [parentSha], author: input.author }
3518
+ });
3519
+ const sha = commit.data.sha;
3520
+ if (mode === "new") {
3521
+ await rest.api("POST", "/git/refs", { body: { ref: `refs/heads/${branch}`, sha } });
3522
+ } else {
3523
+ await rest.api("PATCH", `/git/refs/heads/${encodeRefPath(branch)}`, {
3524
+ body: { sha, force: false }
3525
+ });
3526
+ }
3527
+ return { sha };
3528
+ }
3529
+ function noreplyAuthor(actor) {
3530
+ return { name: actor.login, email: `${actor.id}+${actor.login}@users.noreply.github.com` };
3531
+ }
3532
+
3533
+ // src/codemod/apply/github-trigger.ts
3534
+ var APPLY_COMMAND_RE = /^\/livelocale\s+apply(\s+--force)?\s*$/;
3535
+ var COMMAND_PREFIX = "/livelocale";
3536
+ var ISSUE_BRANCH_RE = /^livelocale\/issue-(\d+)(?:-\d+)?$/;
3537
+ var TRUSTED_ASSOCIATIONS = /* @__PURE__ */ new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
3538
+ var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["admin", "write"]);
3539
+ var TRUSTED_ROLES = /* @__PURE__ */ new Set(["admin", "maintain", "write"]);
3540
+ var ignore = (reason) => ({ kind: "ignore", reason });
3541
+ function actorOf(user) {
3542
+ return { login: String(user?.login ?? ""), id: Number(user?.id ?? 0) };
3543
+ }
3544
+ function textOf(value) {
3545
+ return typeof value === "string" ? value : "";
3546
+ }
3547
+ function holdsChangeToken(text) {
3548
+ try {
3549
+ findChangeToken(text);
3550
+ return true;
3551
+ } catch {
3552
+ return false;
3553
+ }
3554
+ }
3555
+ function parseTrigger(eventName, event) {
3556
+ const repo = textOf(event?.repository?.full_name);
3557
+ const defaultBranch = textOf(event?.repository?.default_branch);
3558
+ switch (eventName) {
3559
+ case "issues": {
3560
+ if (event?.action !== "opened") return ignore("issue not opened");
3561
+ const issue = event.issue;
3562
+ const body = textOf(issue?.body);
3563
+ if (!body.includes(CHANGE_BLOCK_OPEN)) return ignore("no change block");
3564
+ return {
3565
+ kind: "apply",
3566
+ via: "issue",
3567
+ issueNumber: Number(issue.number),
3568
+ actor: actorOf(issue.user),
3569
+ association: issue.author_association ?? null,
3570
+ text: body,
3571
+ force: false,
3572
+ repo,
3573
+ defaultBranch
3574
+ };
3575
+ }
3576
+ case "issue_comment": {
3577
+ if (event?.action !== "created") return ignore("comment not created");
3578
+ const issue = event.issue;
3579
+ const comment = event.comment;
3580
+ if (issue?.pull_request) return ignore("comment on a pull request");
3581
+ if (comment?.user?.type === "Bot") return ignore("comment by a bot");
3582
+ const body = textOf(comment?.body);
3583
+ const firstLine = body.split(/\r\n|\r|\n/)[0] ?? "";
3584
+ if (!firstLine.startsWith(COMMAND_PREFIX)) return ignore("not a livelocale command");
3585
+ const command = APPLY_COMMAND_RE.exec(firstLine);
3586
+ if (!command) return ignore("unknown command");
3587
+ return {
3588
+ kind: "apply",
3589
+ via: "comment",
3590
+ issueNumber: Number(issue.number),
3591
+ // The gate checks the commenter, never the issue author.
3592
+ actor: actorOf(comment.user),
3593
+ association: comment.author_association ?? null,
3594
+ text: holdsChangeToken(body) ? body : textOf(issue?.body),
3595
+ force: command[1] !== void 0,
3596
+ repo,
3597
+ defaultBranch
3598
+ };
3599
+ }
3600
+ case "workflow_dispatch":
3601
+ return {
3602
+ kind: "apply",
3603
+ via: "dispatch",
3604
+ issueNumber: null,
3605
+ actor: actorOf(event?.sender),
3606
+ association: null,
3607
+ text: textOf(event?.inputs?.link),
3608
+ force: false,
3609
+ repo,
3610
+ defaultBranch
3611
+ };
3612
+ case "pull_request": {
3613
+ if (event?.action !== "closed") return ignore("pull request not closed");
3614
+ const pull = event.pull_request;
3615
+ const branch = ISSUE_BRANCH_RE.exec(textOf(pull?.head?.ref));
3616
+ if (!branch) return ignore("not a livelocale issue branch");
3617
+ if (textOf(pull?.head?.repo?.full_name) !== repo || repo === "") {
3618
+ return ignore("pull request from another repository");
3619
+ }
3620
+ return {
3621
+ kind: "close-issue",
3622
+ issueNumber: Number(branch[1]),
3623
+ prUrl: textOf(pull.html_url),
3624
+ merged: pull.merged === true,
3625
+ repo
3626
+ };
3627
+ }
3628
+ default:
3629
+ return ignore(`unsupported event ${eventName}`);
3630
+ }
3631
+ }
3632
+ async function isTrustedActor(rest, login, association) {
3633
+ if (association !== null && TRUSTED_ASSOCIATIONS.has(association)) return true;
3634
+ try {
3635
+ const res = await rest.api(
3636
+ "GET",
3637
+ `/collaborators/${encodeURIComponent(login)}/permission`
3638
+ );
3639
+ const permission = res.data?.permission ?? "";
3640
+ const role = res.data?.role_name ?? "";
3641
+ return TRUSTED_PERMISSIONS.has(permission) || TRUSTED_ROLES.has(role);
3642
+ } catch (err) {
3643
+ if (err instanceof GitHubApiError && (err.status === 403 || err.status === 404)) return false;
3644
+ throw err;
3645
+ }
3646
+ }
3647
+ function chooseTargetBranch(requested, allowList, defaultBranch) {
3648
+ if (!requested) return { branch: defaultBranch, reason: "default" };
3649
+ const allowed = allowList.length > 0 ? allowList : [defaultBranch];
3650
+ if (allowed.includes(requested)) return { branch: requested, reason: "link" };
3651
+ return { branch: defaultBranch, reason: "not-allowed" };
3652
+ }
3653
+ function branchBaseName(t, runId) {
3654
+ return t.issueNumber !== null ? `livelocale/issue-${t.issueNumber}` : `livelocale/run-${runId}`;
3655
+ }
3656
+
3657
+ // src/codemod/apply/report.ts
3658
+ var DEFAULT_SPAN_LENGTH = 120;
3659
+ var DEFAULT_TABLE_ROWS = 100;
3660
+ var MAX_BODY_CHARS = 6e4;
3661
+ var MAX_NOTE_CHARS2 = 2e3;
3662
+ var MAX_NAME_CHARS = 64;
3663
+ var MAX_CHECK_LINES = 50;
3664
+ var MAX_CHECK_SUMMARY_CHARS = 1e4;
3665
+ var BODY_MARGIN = 200;
3666
+ var STATUS_LABELS = {
3667
+ apply: "applied",
3668
+ already: "already done",
3669
+ conflict: "conflict",
3670
+ "needs-force": "needs --force",
3671
+ invalid: "rejected"
3672
+ };
3673
+ var BIDI_CONTROL_RE = /[‪-‮⁦-⁩]/g;
3674
+ var NEWLINE_RE = /\r\n|\r|\n/g;
3675
+ var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
3676
+ var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
3677
+ function cut(text, max) {
3678
+ if (text.length <= max) return text;
3679
+ let out = text.slice(0, max);
3680
+ const last = out.charCodeAt(out.length - 1);
3681
+ if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
3682
+ return out;
3683
+ }
3684
+ function cutLines(text, max) {
3685
+ if (text.length <= max) return text;
3686
+ const kept = [];
3687
+ let length = 0;
3688
+ for (const line of text.split("\n")) {
3689
+ const added = (kept.length > 0 ? 1 : 0) + line.length;
3690
+ if (length + added + 2 > max) break;
3691
+ kept.push(line);
3692
+ length += added;
3693
+ }
3694
+ return [...kept, "\u2026"].join("\n");
3695
+ }
3696
+ function longestBacktickRun(text) {
3697
+ let longest = 0;
3698
+ for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
3699
+ return longest;
3700
+ }
3701
+ function codePointLabel(ch) {
3702
+ return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
3703
+ }
3704
+ function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
3705
+ const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
3706
+ const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
3707
+ if (content === "") return "` `";
3708
+ const fence = "`".repeat(longestBacktickRun(content) + 1);
3709
+ const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
3710
+ return `${fence}${pad}${content}${pad}${fence}`;
3711
+ }
3712
+ function sanitizeName(name) {
3713
+ if (name === void 0) return void 0;
3714
+ const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
3715
+ const result = cut(cleaned, MAX_NAME_CHARS).trim();
3716
+ return result === "" ? void 0 : result;
3717
+ }
3718
+ var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
3719
+ function summarizeCheck(report, edited) {
3720
+ const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
3721
+ const mine = report.issues.filter(
3722
+ (issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
3723
+ );
3724
+ if (mine.length === 0) return NO_CHECK_PROBLEMS;
3725
+ const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
3726
+ for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
3727
+ lines.push(
3728
+ `- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
3729
+ );
3730
+ }
3731
+ if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
3732
+ const others = report.issues.length - mine.length;
3733
+ if (others > 0) {
3734
+ lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
3735
+ }
3736
+ return lines.join("\n");
3737
+ }
3738
+ function checksCell(edit) {
3739
+ const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
3740
+ if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
3741
+ parts.push(codeSpan(edit.message));
3742
+ }
3743
+ return parts.join("; ");
3744
+ }
3745
+ function renderRow(edit) {
3746
+ const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
3747
+ const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
3748
+ const cells = [
3749
+ STATUS_LABELS[edit.status],
3750
+ codeSpan(edit.locale),
3751
+ codeSpan(edit.key),
3752
+ before,
3753
+ after,
3754
+ checksCell(edit)
3755
+ ];
3756
+ return `| ${cells.join(" | ")} |`;
3757
+ }
3758
+ function renderTable(plan, limit, maxChars) {
3759
+ const lines = [
3760
+ "| Status | Locale | Key | Before | After | Checks |",
3761
+ "| --- | --- | --- | --- | --- | --- |"
3762
+ ];
3763
+ const moreLine = (n) => `
3764
+
3765
+ \u2026and ${n} more`;
3766
+ let length = lines.join("\n").length + moreLine(plan.edits.length).length;
3767
+ let shown = 0;
3768
+ for (const edit of plan.edits) {
3769
+ if (shown >= limit) break;
3770
+ const row = renderRow(edit);
3771
+ if (length + row.length + 1 > maxChars) break;
3772
+ lines.push(row);
3773
+ length += row.length + 1;
3774
+ shown += 1;
3775
+ }
3776
+ const rest = plan.edits.length - shown;
3777
+ return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
3778
+ }
3779
+ function fencedNote(note) {
3780
+ const text = cut(note, MAX_NOTE_CHARS2);
3781
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
3782
+ return `${fence}text
3783
+ ${text}
3784
+ ${fence}`;
3785
+ }
3786
+ function closingLine(ctx) {
3787
+ if (ctx.issueNumber === void 0) return void 0;
3788
+ if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
3789
+ return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
3790
+ }
3791
+ function targetNote(target) {
3792
+ if (target?.reason !== "not-allowed" || target.requested === void 0) return void 0;
3793
+ return `The link was made from ${codeSpan(target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(target.branch)}.`;
3794
+ }
3795
+ function creditLines(ctx, withTarget) {
3796
+ const lines = [];
3797
+ if (ctx.by?.trim()) lines.push(`Translated by ${codeSpan(ctx.by)}`);
3798
+ const target = withTarget ? targetNote(ctx.target) : void 0;
3799
+ if (target) lines.push(target);
3800
+ if (ctx.note?.trim()) lines.push(fencedNote(ctx.note));
3801
+ return lines;
3802
+ }
3803
+ function renderPullRequestBody(ctx) {
3804
+ const head = [
3805
+ "Applies translations sent with a LiveLocale change link.",
3806
+ ...creditLines(ctx, true)
3807
+ ];
3808
+ const tail = [];
3809
+ if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
3810
+ if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
3811
+ const closing = closingLine(ctx);
3812
+ if (closing) tail.push(closing);
3813
+ const fixed = [...head, ...tail].join("\n\n").length;
3814
+ const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
3815
+ return [...head, table, ...tail].join("\n\n");
3816
+ }
3817
+ function renderCommitMessage(input) {
3818
+ let subject = "i18n: apply LiveLocale changes";
3819
+ if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
3820
+ else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
3821
+ const applied = input.plan.edits.filter((edit) => edit.status === "apply");
3822
+ const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
3823
+ const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
3824
+ const name = sanitizeName(input.by);
3825
+ if (name) parts.push(`Translated-by: ${name}`);
3826
+ return parts.join("\n\n");
3827
+ }
3828
+ function renderTerminalSummary(plan) {
3829
+ const c = plan.counts;
3830
+ const lines = [
3831
+ `Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
3832
+ ];
3833
+ for (const edit of plan.edits) {
3834
+ lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
3835
+ }
3836
+ return lines.join("\n");
3837
+ }
3838
+ var MAX_ERROR_MESSAGE_CHARS = 2e3;
3839
+ function needsForceHint(plan) {
3840
+ const n = plan?.counts["needs-force"] ?? 0;
3841
+ if (n === 0) return void 0;
3842
+ return `${n} change(s) were saved with "Save anyway". A maintainer can apply them with \`/livelocale apply --force\`.`;
3843
+ }
3844
+ function withTable(head, plan, tail) {
3845
+ if (!plan) return [...head, ...tail].join("\n\n");
3846
+ const fixed = [...head, ...tail].join("\n\n").length;
3847
+ const table = renderTable(plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
3848
+ return [...head, table, ...tail].join("\n\n");
3849
+ }
3850
+ function checkTail(checkSummary) {
3851
+ return checkSummary ? [cutLines(checkSummary, MAX_CHECK_SUMMARY_CHARS)] : [];
3852
+ }
3853
+ function renderIssueComment(ctx) {
3854
+ const { outcome } = ctx;
3855
+ switch (outcome.kind) {
3856
+ case "waiting-for-maintainer":
3857
+ return "Waiting for a maintainer: comment `/livelocale apply`";
3858
+ case "wrong-repo":
3859
+ return `This change link was made for ${codeSpan(outcome.linkRepo)}, not ${codeSpan(outcome.repo)}. Nothing was applied.`;
3860
+ case "error": {
3861
+ const message = cut(outcome.message, MAX_ERROR_MESSAGE_CHARS).replace(/\.$/, "");
3862
+ const runLog = ctx.runUrl ? ` Run log: ${ctx.runUrl}` : "";
3863
+ return `LiveLocale could not apply this link: ${message}.${runLog}`;
3864
+ }
3865
+ case "nothing-to-apply": {
3866
+ const hint = needsForceHint(ctx.plan);
3867
+ return withTable(
3868
+ ["Nothing to apply.", ...creditLines(ctx, false)],
3869
+ ctx.plan,
3870
+ hint ? [hint] : []
3871
+ );
3872
+ }
3873
+ case "pr": {
3874
+ const headline = `${outcome.reused ? "Updated pull request" : "Pull request"}: ${outcome.url}`;
3875
+ return withTable(
3876
+ [headline, ...creditLines(ctx, true)],
3877
+ ctx.plan,
3878
+ checkTail(ctx.checkSummary)
3879
+ );
3880
+ }
3881
+ case "compare": {
3882
+ const headline = [
3883
+ `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}`,
3884
+ '(Or allow it once: Settings \u2192 Actions \u2192 General \u2192 "Allow GitHub Actions to create and approve pull requests".)'
3885
+ ];
3886
+ return withTable(
3887
+ [...headline, ...creditLines(ctx, true)],
3888
+ ctx.plan,
3889
+ checkTail(ctx.checkSummary)
3890
+ );
3891
+ }
3892
+ }
3893
+ }
3894
+
3895
+ // src/codemod/apply/github-event.ts
3896
+ var PR_TITLE = "i18n: LiveLocale changes";
3897
+ var NOT_FOUND_MESSAGE = "No LiveLocale change link (lc1.\u2026) found.";
3898
+ var ExpectedFailure = class extends Error {
3899
+ };
3900
+ function errorMessage(err) {
3901
+ return err instanceof Error ? err.message : String(err);
3902
+ }
3903
+ function required(env, name) {
3904
+ const value = env[name];
3905
+ if (!value) throw new Error(`[livelocale] ${name} is not set; run this inside GitHub Actions.`);
3906
+ return value;
3907
+ }
3908
+ async function closeIssue(rest, trigger, log) {
3909
+ const n = trigger.issueNumber;
3910
+ const issue = await rest.api("GET", `/issues/${n}`, {
3911
+ allow404: true
3912
+ });
3913
+ const body = typeof issue.data?.body === "string" ? issue.data.body : "";
3914
+ if (issue.data?.state !== "open" || !body.includes(CHANGE_BLOCK_OPEN)) {
3915
+ log(`Issue #${n} is not an open LiveLocale issue; nothing to close.`);
3916
+ return "ignored";
3917
+ }
3918
+ await rest.api("PATCH", `/issues/${n}`, {
3919
+ body: { state: "closed", state_reason: trigger.merged ? "completed" : "not_planned" }
3920
+ });
3921
+ await rest.api("POST", `/issues/${n}/comments`, {
3922
+ body: { body: `Closed because ${trigger.prUrl} was ${trigger.merged ? "merged" : "closed"}.` }
3923
+ });
3924
+ log(`Closed issue #${n}.`);
3925
+ return "closed-issue";
3926
+ }
3927
+ async function checkChanges(options, workspace, remote, changes, plan) {
3928
+ const dir = normalizeRepoDir(options.localesPath);
3929
+ const prefix = dir === "" ? "" : `${dir}/`;
3930
+ const tmp = fs6.mkdtempSync(path10.join(os.tmpdir(), "livelocale-check-"));
3931
+ try {
3932
+ for (const [file, text] of new Map([...remote.files, ...changes])) {
3933
+ const target = path10.join(tmp, ...file.slice(prefix.length).split("/"));
3934
+ fs6.mkdirSync(path10.dirname(target), { recursive: true });
3935
+ fs6.writeFileSync(target, text, "utf-8");
3936
+ }
3937
+ const report = await runCheck({
3938
+ projectDir: path10.join(workspace, options.dir ?? "."),
3939
+ localesPath: tmp,
3940
+ sourceLocale: options.sourceLocale,
3941
+ unused: false
3942
+ });
3943
+ const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
3944
+ return summarizeCheck(report, edited);
3945
+ } catch (err) {
3946
+ return `Check could not run: ${errorMessage(err)}`;
3947
+ } finally {
3948
+ fs6.rmSync(tmp, { recursive: true, force: true });
3949
+ }
3950
+ }
3951
+ async function runGitHubEventApply(options) {
3952
+ const { env } = options;
3953
+ const log = options.log ?? ((line) => console.log(line));
3954
+ const token = required(env, "GITHUB_TOKEN");
3955
+ const eventPath = required(env, "GITHUB_EVENT_PATH");
3956
+ const event = JSON.parse(fs6.readFileSync(eventPath, "utf-8"));
3957
+ const trigger = parseTrigger(env.GITHUB_EVENT_NAME ?? "", event);
3958
+ if (trigger.kind === "ignore") {
3959
+ log(`Nothing to do: ${trigger.reason}.`);
3960
+ return "ignored";
3961
+ }
3962
+ const repo = env.GITHUB_REPOSITORY || trigger.repo;
3963
+ const rest = createGitHubRest({
3964
+ repo,
3965
+ token,
3966
+ apiBase: env.GITHUB_API_URL || void 0,
3967
+ fetchImpl: options.fetchImpl
3968
+ });
3969
+ if (trigger.kind === "close-issue") return closeIssue(rest, trigger, log);
3970
+ const server = (env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, "");
3971
+ const runId = env.GITHUB_RUN_ID ?? "0";
3972
+ const runUrl = `${server}/${repo}/actions/runs/${runId}`;
3973
+ const issueNumber = trigger.issueNumber ?? void 0;
3974
+ const report = async (outcome, ctx = {}) => {
3975
+ const text = renderIssueComment({ ...ctx, runUrl, outcome });
3976
+ if (issueNumber !== void 0) {
3977
+ await rest.api("POST", `/issues/${issueNumber}/comments`, { body: { body: text } });
3978
+ } else if (env.GITHUB_STEP_SUMMARY) {
3979
+ fs6.appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}
3980
+ `, "utf-8");
3981
+ }
3982
+ log(`Result: ${outcome.kind}`);
3983
+ return outcome.kind;
3984
+ };
3985
+ if (trigger.via !== "dispatch") {
3986
+ if (!await isTrustedActor(rest, trigger.actor.login, trigger.association)) {
3987
+ return report({ kind: "waiting-for-maintainer" });
3988
+ }
3989
+ }
3990
+ try {
3991
+ return await applyLink(options, rest, trigger, { repo, runId, runUrl, server, log, report });
3992
+ } catch (err) {
3993
+ if (err instanceof ExpectedFailure) return report({ kind: "error", message: err.message });
3994
+ await report({ kind: "error", message: errorMessage(err) });
3995
+ throw err;
3996
+ }
3997
+ }
3998
+ async function applyLink(options, rest, trigger, run3) {
3999
+ const { repo, runId, runUrl, log, report } = run3;
4000
+ const issueNumber = trigger.issueNumber ?? void 0;
4001
+ let set;
4002
+ try {
4003
+ set = await decodeChangeLink(trigger.text, nodeInflate);
4004
+ } catch (err) {
4005
+ if (!(err instanceof ChangeLinkError)) throw err;
4006
+ throw new ExpectedFailure(err.code === "NOT_FOUND" ? NOT_FOUND_MESSAGE : err.message);
4007
+ }
4008
+ if (set.repo !== void 0 && set.repo.toLowerCase() !== repo.toLowerCase()) {
4009
+ return report({ kind: "wrong-repo", linkRepo: set.repo, repo });
4010
+ }
4011
+ const defaultBranch = trigger.defaultBranch;
4012
+ const chosen = chooseTargetBranch(set.branch, options.branches ?? [defaultBranch], defaultBranch);
4013
+ const target = {
4014
+ branch: chosen.branch,
4015
+ isDefault: chosen.branch === defaultBranch,
4016
+ ...set.branch ? { requested: set.branch } : {},
4017
+ reason: chosen.reason
4018
+ };
4019
+ const targetSha = await rest.refSha(target.branch);
4020
+ if (targetSha === null) {
4021
+ throw new Error(`[livelocale] The target branch ${target.branch} does not exist.`);
4022
+ }
4023
+ const locales = [.../* @__PURE__ */ new Set([...set.edits.map((e) => e.locale), options.sourceLocale])];
4024
+ const remote = await readRemoteLocales(rest, targetSha, options.localesPath, locales);
4025
+ if (!remote.targets.get(options.sourceLocale)) {
4026
+ throw new ExpectedFailure(
4027
+ `Source locale \`${options.sourceLocale}\` has no locale file on \`${target.branch}\`.`
4028
+ );
4029
+ }
4030
+ const plan = planApply({
4031
+ edits: set.edits,
4032
+ sourceLocale: options.sourceLocale,
4033
+ targets: remote.targets,
4034
+ allowForce: trigger.force
4035
+ });
4036
+ log(renderTerminalSummary(plan));
4037
+ const credits = { plan, by: set.by, note: set.note, target };
4038
+ if (plan.counts.apply === 0) return report({ kind: "nothing-to-apply" }, credits);
4039
+ const branchPlan = await prepareBranch(
4040
+ rest,
4041
+ branchBaseName(trigger, runId),
4042
+ target.branch,
4043
+ targetSha,
4044
+ runId
4045
+ );
4046
+ const base = branchPlan.mode === "append" ? await readRemoteLocales(rest, branchPlan.parentSha, options.localesPath, locales) : remote;
4047
+ const changes = buildFileChanges(options.localesPath, plan, remote, base.files);
4048
+ const commit = await commitFiles(rest, {
4049
+ branchPlan,
4050
+ files: changes,
4051
+ message: renderCommitMessage({ plan, by: set.by, issueNumber, runId }),
4052
+ author: noreplyAuthor(trigger.actor)
4053
+ });
4054
+ log(
4055
+ commit.sha === null ? `${branchPlan.branch} already holds these changes.` : `Committed to ${branchPlan.branch}.`
4056
+ );
4057
+ const workspace = options.env.GITHUB_WORKSPACE || process.cwd();
4058
+ const checkSummary = await checkChanges(options, workspace, base, changes, plan);
4059
+ const title = issueNumber !== void 0 ? `${PR_TITLE} (#${issueNumber})` : `${PR_TITLE} (run ${runId})`;
4060
+ const body = renderPullRequestBody({ ...credits, issueNumber, checkSummary, runUrl });
4061
+ let outcome;
4062
+ try {
4063
+ const pull = await rest.openOrReusePullRequest({
4064
+ head: branchPlan.branch,
4065
+ base: target.branch,
4066
+ title,
4067
+ body,
4068
+ updateBodyIfReused: true
4069
+ });
4070
+ outcome = { kind: "pr", url: pull.url, reused: pull.reused };
4071
+ } catch (err) {
4072
+ if (!isPullRequestCreationForbidden(err)) throw err;
4073
+ const closing = closingLine({ target, issueNumber });
4074
+ const url = `${run3.server}/${repo}/compare/${encodeRefPath(target.branch)}...${encodeRefPath(branchPlan.branch)}?quick_pull=1&title=${encodeURIComponent(title)}` + (closing ? `&body=${encodeURIComponent(closing)}` : "");
4075
+ outcome = { kind: "compare", url, branch: branchPlan.branch };
4076
+ }
4077
+ log(`Pull request: ${outcome.url}`);
4078
+ return report(outcome, { ...credits, checkSummary });
4079
+ }
4080
+
4081
+ // src/codemod/apply/link-input.ts
4082
+ import { execFile as execFile2 } from "child_process";
4083
+ import * as fs7 from "fs";
4084
+ import * as path11 from "path";
4085
+ var MAX_APPLY_INPUT_BYTES = 1048576;
4086
+ var TOO_LARGE_MESSAGE3 = "The input is larger than 1 MB.";
4087
+ 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.)';
4088
+ var NO_CLIPBOARD_MESSAGE = "Could not read the clipboard. Save the change link to a file and pass the file instead.";
4089
+ var ApplyInputError = class extends Error {
4090
+ constructor(message) {
4091
+ super(message);
4092
+ this.name = "ApplyInputError";
4093
+ }
4094
+ };
4095
+ function checkSize(text) {
4096
+ if (Buffer.byteLength(text, "utf8") > MAX_APPLY_INPUT_BYTES) {
4097
+ throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4098
+ }
4099
+ return text;
4100
+ }
4101
+ async function readStream(stream) {
4102
+ const chunks = [];
4103
+ let size = 0;
4104
+ for await (const chunk of stream) {
4105
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
4106
+ size += bytes.length;
4107
+ if (size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4108
+ chunks.push(bytes);
4109
+ }
4110
+ return Buffer.concat(chunks).toString("utf8");
4111
+ }
4112
+ function existingFile(arg, cwd) {
4113
+ try {
4114
+ const resolved = path11.resolve(cwd, arg);
4115
+ return fs7.statSync(resolved, { throwIfNoEntry: false })?.isFile() ? resolved : null;
4116
+ } catch {
4117
+ return null;
4118
+ }
4119
+ }
4120
+ async function readApplyInput(arg, options) {
4121
+ if (options.clipboard) return checkSize(await (options.readClipboard ?? readClipboard)());
4122
+ if (arg === void 0 || arg === "") throw new ApplyInputError(NO_INPUT_MESSAGE);
4123
+ if (arg === "-") return readStream(options.stdin ?? process.stdin);
4124
+ const file = existingFile(arg, options.cwd);
4125
+ if (file === null) return checkSize(arg);
4126
+ if (fs7.statSync(file).size > MAX_APPLY_INPUT_BYTES) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4127
+ return checkSize(fs7.readFileSync(file, "utf8"));
4128
+ }
4129
+ function clipboardCommands(platform) {
4130
+ if (platform === "win32") {
4131
+ return [
4132
+ ["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard -Raw"]]
4133
+ ];
4134
+ }
4135
+ if (platform === "darwin") return [["pbpaste", []]];
4136
+ return [
4137
+ ["wl-paste", ["--no-newline"]],
4138
+ ["xclip", ["-selection", "clipboard", "-o"]],
4139
+ ["xsel", ["--clipboard", "--output"]]
4140
+ ];
4141
+ }
4142
+ var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
4143
+ function run(execFileImpl, [command, args]) {
4144
+ return new Promise((resolve16) => {
4145
+ try {
4146
+ execFileImpl(
4147
+ command,
4148
+ args,
4149
+ { encoding: "utf8", windowsHide: true, maxBuffer: MAX_APPLY_INPUT_BYTES + 1 },
4150
+ (error, stdout) => {
4151
+ const code = error?.code;
4152
+ if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") resolve16(TOO_LARGE);
4153
+ else resolve16(error ? null : String(stdout));
4154
+ }
4155
+ );
4156
+ } catch {
4157
+ resolve16(null);
4158
+ }
4159
+ });
4160
+ }
4161
+ async function readClipboard(execFileImpl = execFile2, platform = process.platform) {
4162
+ for (const command of clipboardCommands(platform)) {
4163
+ const text = await run(execFileImpl, command);
4164
+ if (text === TOO_LARGE) throw new ApplyInputError(TOO_LARGE_MESSAGE3);
4165
+ if (text !== null) return checkSize(text);
4166
+ }
4167
+ throw new ApplyInputError(NO_CLIPBOARD_MESSAGE);
4168
+ }
4169
+
4170
+ // src/codemod/apply/local.ts
4171
+ import { execFile as execFile4, spawn as spawn2 } from "child_process";
4172
+ import * as fs10 from "fs";
4173
+ import * as os2 from "os";
4174
+ import * as path15 from "path";
4175
+
4176
+ // src/server/file-mutator.ts
4177
+ import { promises as fs8 } from "fs";
4178
+ import * as path13 from "path";
4179
+
4180
+ // src/core/safe-path.ts
4181
+ import * as path12 from "path";
4182
+ function resolveSafeLocalePath(basePath, locale) {
4183
+ if (!isSafeLocaleSegment(locale)) {
4184
+ throw new Error("Path traversal attempt");
4185
+ }
4186
+ const resolvedBasePath = path12.resolve(basePath);
4187
+ const filePath = path12.resolve(resolvedBasePath, `${locale}.json`);
4188
+ if (!filePath.startsWith(resolvedBasePath)) {
4189
+ throw new Error("Path traversal attempt");
4190
+ }
4191
+ return filePath;
4192
+ }
4193
+
4194
+ // src/server/file-mutator.ts
4195
+ var FileMutex = class {
4196
+ queue = Promise.resolve();
4197
+ async acquire(fn) {
4198
+ const next = this.queue.then(fn);
4199
+ this.queue = next.then(
4200
+ () => {
4201
+ },
4202
+ () => {
4203
+ }
4204
+ );
4205
+ return next;
4206
+ }
4207
+ };
4208
+ var FileMutator = class {
4209
+ fileLocks = /* @__PURE__ */ new Map();
4210
+ requireEnabled;
4211
+ quiet;
4212
+ scanner;
4213
+ constructor(options = {}) {
4214
+ this.requireEnabled = options.requireEnabled ?? true;
4215
+ this.quiet = options.quiet ?? false;
4216
+ this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
4217
+ }
4218
+ assertEnabled() {
4219
+ if (!this.requireEnabled) return;
4220
+ const reason = getDisabledReason();
4221
+ if (reason !== null) {
4222
+ throw new Error(
4223
+ `[livelocale] FileMutator can only be executed in development mode (${reason}).`
4224
+ );
4225
+ }
4226
+ }
4227
+ /**
4228
+ * Helper to serialize execution per file path
4229
+ */
4230
+ async withLock(filePath, fn) {
4231
+ let lock = this.fileLocks.get(filePath);
4232
+ if (!lock) {
4233
+ lock = new FileMutex();
4234
+ this.fileLocks.set(filePath, lock);
4235
+ }
4236
+ return lock.acquire(fn);
4237
+ }
4238
+ /**
4239
+ * Updates a locale JSON file on disk atomically and safely.
4240
+ * Enforces development mode checks, path traversal mitigation, key depth restrictions,
4241
+ * structural JSON validation, and concurrency serialization.
4242
+ */
4243
+ async updateLocaleKey(basePath, locale, keyPath, newValue) {
4244
+ this.assertEnabled();
4245
+ if (!isSafeLocaleSegment(locale)) {
4246
+ throw new Error(
4247
+ `[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
4248
+ );
4249
+ }
4250
+ const resolvedBasePath = path13.resolve(basePath);
4251
+ const localeDir = path13.resolve(resolvedBasePath, locale);
4252
+ let filePath = "";
4253
+ let mutationKeyPath = keyPath;
4254
+ let isNamespaceDir = false;
4255
+ try {
4256
+ const stats = await fs8.stat(localeDir);
4257
+ if (stats.isDirectory()) {
4258
+ isNamespaceDir = true;
4259
+ }
4260
+ } catch {
4261
+ }
4262
+ if (isNamespaceDir) {
4263
+ const firstDot = keyPath.indexOf(".");
4264
+ if (firstDot !== -1) {
4265
+ const namespace = keyPath.substring(0, firstDot);
4266
+ if (DANGEROUS_KEY_SEGMENTS.has(namespace)) {
4267
+ throw new Error(
4268
+ `[livelocale] Forbidden namespace segment in path: "${namespace}". Segment names like __proto__, constructor, and prototype are not allowed.`
4269
+ );
4270
+ }
4271
+ mutationKeyPath = keyPath.substring(firstDot + 1);
4272
+ filePath = path13.resolve(localeDir, `${namespace}.json`);
4273
+ } else {
4274
+ filePath = path13.resolve(localeDir, `${keyPath}.json`);
4275
+ mutationKeyPath = "";
4276
+ }
4277
+ } else {
4278
+ filePath = path13.resolve(resolvedBasePath, `${locale}.json`);
4279
+ }
4280
+ if (!filePath.startsWith(resolvedBasePath)) {
4281
+ throw new Error(
4282
+ `[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
3427
4283
  );
3428
4284
  }
3429
4285
  await this.withLock(filePath, async () => {
@@ -3431,7 +4287,7 @@ var FileMutator = class {
3431
4287
  let indent = " ";
3432
4288
  let trailingNewline = "\n";
3433
4289
  try {
3434
- const fileContent = await fs7.readFile(filePath, "utf-8");
4290
+ const fileContent = await fs8.readFile(filePath, "utf-8");
3435
4291
  indent = detectJsonIndent(fileContent);
3436
4292
  trailingNewline = fileContent.endsWith("\n") ? "\n" : "";
3437
4293
  try {
@@ -3471,17 +4327,17 @@ var FileMutator = class {
3471
4327
  }
3472
4328
  const tempPath = `${filePath}.tmp`;
3473
4329
  try {
3474
- await fs7.writeFile(tempPath, stringified, "utf-8");
4330
+ await fs8.writeFile(tempPath, stringified, "utf-8");
3475
4331
  } catch (err) {
3476
4332
  throw new Error(
3477
4333
  `[livelocale] Failed to write temp file at ${tempPath}. Operation: write. Original error: ${err.message}`
3478
4334
  );
3479
4335
  }
3480
4336
  try {
3481
- await fs7.rename(tempPath, filePath);
4337
+ await fs8.rename(tempPath, filePath);
3482
4338
  } catch (err) {
3483
4339
  try {
3484
- await fs7.unlink(tempPath);
4340
+ await fs8.unlink(tempPath);
3485
4341
  } catch {
3486
4342
  }
3487
4343
  throw new Error(
@@ -3499,19 +4355,19 @@ var FileMutator = class {
3499
4355
  */
3500
4356
  async readLocale(basePath, locale) {
3501
4357
  this.assertEnabled();
3502
- const localePath = path11.resolve(basePath, locale);
4358
+ const localePath = path13.resolve(basePath, locale);
3503
4359
  let isDir = false;
3504
4360
  try {
3505
- isDir = (await fs7.stat(localePath)).isDirectory();
4361
+ isDir = (await fs8.stat(localePath)).isDirectory();
3506
4362
  } catch {
3507
4363
  }
3508
4364
  if (isDir) {
3509
4365
  const merged = {};
3510
- for (const file of await fs7.readdir(localePath)) {
4366
+ for (const file of await fs8.readdir(localePath)) {
3511
4367
  if (!file.endsWith(".json")) continue;
3512
4368
  try {
3513
- merged[path11.basename(file, ".json")] = JSON.parse(
3514
- await fs7.readFile(path11.join(localePath, file), "utf-8")
4369
+ merged[path13.basename(file, ".json")] = JSON.parse(
4370
+ await fs8.readFile(path13.join(localePath, file), "utf-8")
3515
4371
  );
3516
4372
  } catch {
3517
4373
  }
@@ -3519,15 +4375,15 @@ var FileMutator = class {
3519
4375
  return merged;
3520
4376
  }
3521
4377
  const filePath = resolveSafeLocalePath(basePath, locale);
3522
- return JSON.parse(await fs7.readFile(filePath, "utf-8"));
4378
+ return JSON.parse(await fs8.readFile(filePath, "utf-8"));
3523
4379
  }
3524
4380
  /**
3525
4381
  * Lists all locale codes dynamically scanned from the locales directory.
3526
4382
  */
3527
4383
  async listLocales(basePath) {
3528
4384
  this.assertEnabled();
3529
- const resolvedBasePath = path11.resolve(basePath);
3530
- const items = await fs7.readdir(resolvedBasePath, { withFileTypes: true });
4385
+ const resolvedBasePath = path13.resolve(basePath);
4386
+ const items = await fs8.readdir(resolvedBasePath, { withFileTypes: true });
3531
4387
  const localesSet = /* @__PURE__ */ new Set();
3532
4388
  for (const item of items) {
3533
4389
  if (item.isDirectory()) {
@@ -3535,7 +4391,7 @@ var FileMutator = class {
3535
4391
  localesSet.add(item.name);
3536
4392
  }
3537
4393
  } else if (item.isFile() && item.name.endsWith(".json")) {
3538
- const name = path11.basename(item.name, ".json");
4394
+ const name = path13.basename(item.name, ".json");
3539
4395
  if (LOCALE_RE.test(name)) {
3540
4396
  localesSet.add(name);
3541
4397
  }
@@ -3556,8 +4412,8 @@ var FileMutator = class {
3556
4412
  if (!LOCALE_RE.test(locale)) {
3557
4413
  throw new Error(`[livelocale] Invalid locale format: "${locale}"`);
3558
4414
  }
3559
- const resolvedBasePath = path11.resolve(basePath);
3560
- const localePath = path11.resolve(resolvedBasePath, locale);
4415
+ const resolvedBasePath = path13.resolve(basePath);
4416
+ const localePath = path13.resolve(resolvedBasePath, locale);
3561
4417
  if (!localePath.startsWith(resolvedBasePath)) {
3562
4418
  throw new Error(
3563
4419
  `[livelocale] Path traversal attempt: Resolved file path must start with basePath.`
@@ -3565,19 +4421,19 @@ var FileMutator = class {
3565
4421
  }
3566
4422
  let exists = false;
3567
4423
  try {
3568
- await fs7.stat(localePath);
4424
+ await fs8.stat(localePath);
3569
4425
  exists = true;
3570
4426
  } catch {
3571
4427
  }
3572
4428
  try {
3573
- await fs7.stat(`${localePath}.json`);
4429
+ await fs8.stat(`${localePath}.json`);
3574
4430
  exists = true;
3575
4431
  } catch {
3576
4432
  }
3577
4433
  if (exists) {
3578
4434
  throw new Error(`[livelocale] Locale "${locale}" already exists.`);
3579
4435
  }
3580
- const items = await fs7.readdir(resolvedBasePath, { withFileTypes: true });
4436
+ const items = await fs8.readdir(resolvedBasePath, { withFileTypes: true });
3581
4437
  let referenceDir = null;
3582
4438
  for (const item of items) {
3583
4439
  if (item.isDirectory() && LOCALE_RE.test(item.name)) {
@@ -3586,16 +4442,16 @@ var FileMutator = class {
3586
4442
  }
3587
4443
  }
3588
4444
  if (referenceDir) {
3589
- await fs7.mkdir(localePath, { recursive: true });
3590
- const refPath = path11.resolve(resolvedBasePath, referenceDir);
3591
- const files = await fs7.readdir(refPath);
4445
+ await fs8.mkdir(localePath, { recursive: true });
4446
+ const refPath = path13.resolve(resolvedBasePath, referenceDir);
4447
+ const files = await fs8.readdir(refPath);
3592
4448
  for (const file of files) {
3593
4449
  if (file.endsWith(".json")) {
3594
- await fs7.writeFile(path11.join(localePath, file), "{}", "utf-8");
4450
+ await fs8.writeFile(path13.join(localePath, file), "{}", "utf-8");
3595
4451
  }
3596
4452
  }
3597
4453
  } else {
3598
- await fs7.writeFile(`${localePath}.json`, "{}", "utf-8");
4454
+ await fs8.writeFile(`${localePath}.json`, "{}", "utf-8");
3599
4455
  }
3600
4456
  console.log(`[livelocale] Added locale '${locale}'`);
3601
4457
  }
@@ -3610,16 +4466,16 @@ var FileMutator = class {
3610
4466
  if (!LOCALE_RE.test(oldLocale) || !LOCALE_RE.test(newLocale)) {
3611
4467
  throw new Error(`[livelocale] Invalid locale format`);
3612
4468
  }
3613
- const resolvedBasePath = path11.resolve(basePath);
3614
- const oldPath = path11.resolve(resolvedBasePath, oldLocale);
3615
- const newPath = path11.resolve(resolvedBasePath, newLocale);
4469
+ const resolvedBasePath = path13.resolve(basePath);
4470
+ const oldPath = path13.resolve(resolvedBasePath, oldLocale);
4471
+ const newPath = path13.resolve(resolvedBasePath, newLocale);
3616
4472
  if (!oldPath.startsWith(resolvedBasePath) || !newPath.startsWith(resolvedBasePath)) {
3617
4473
  throw new Error("[livelocale] Path traversal attempt");
3618
4474
  }
3619
4475
  let isDir = false;
3620
4476
  let exists = false;
3621
4477
  try {
3622
- const stats = await fs7.stat(oldPath);
4478
+ const stats = await fs8.stat(oldPath);
3623
4479
  isDir = stats.isDirectory();
3624
4480
  exists = true;
3625
4481
  } catch {
@@ -3627,7 +4483,7 @@ var FileMutator = class {
3627
4483
  let isFile = false;
3628
4484
  if (!exists) {
3629
4485
  try {
3630
- await fs7.stat(`${oldPath}.json`);
4486
+ await fs8.stat(`${oldPath}.json`);
3631
4487
  isFile = true;
3632
4488
  exists = true;
3633
4489
  } catch {
@@ -3638,12 +4494,12 @@ var FileMutator = class {
3638
4494
  }
3639
4495
  let newExists = false;
3640
4496
  try {
3641
- await fs7.stat(newPath);
4497
+ await fs8.stat(newPath);
3642
4498
  newExists = true;
3643
4499
  } catch {
3644
4500
  }
3645
4501
  try {
3646
- await fs7.stat(`${newPath}.json`);
4502
+ await fs8.stat(`${newPath}.json`);
3647
4503
  newExists = true;
3648
4504
  } catch {
3649
4505
  }
@@ -3651,9 +4507,9 @@ var FileMutator = class {
3651
4507
  throw new Error(`[livelocale] Target locale "${newLocale}" already exists.`);
3652
4508
  }
3653
4509
  if (isDir) {
3654
- await fs7.rename(oldPath, newPath);
4510
+ await fs8.rename(oldPath, newPath);
3655
4511
  } else if (isFile) {
3656
- await fs7.rename(`${oldPath}.json`, `${newPath}.json`);
4512
+ await fs8.rename(`${oldPath}.json`, `${newPath}.json`);
3657
4513
  }
3658
4514
  console.log(`[livelocale] Renamed locale '${oldLocale}' to '${newLocale}'`);
3659
4515
  }
@@ -3668,15 +4524,15 @@ var FileMutator = class {
3668
4524
  if (!LOCALE_RE.test(locale)) {
3669
4525
  throw new Error(`[livelocale] Invalid locale format`);
3670
4526
  }
3671
- const resolvedBasePath = path11.resolve(basePath);
3672
- const localePath = path11.resolve(resolvedBasePath, locale);
4527
+ const resolvedBasePath = path13.resolve(basePath);
4528
+ const localePath = path13.resolve(resolvedBasePath, locale);
3673
4529
  if (!localePath.startsWith(resolvedBasePath)) {
3674
4530
  throw new Error("[livelocale] Path traversal attempt");
3675
4531
  }
3676
4532
  let isDir = false;
3677
4533
  let exists = false;
3678
4534
  try {
3679
- const stats = await fs7.stat(localePath);
4535
+ const stats = await fs8.stat(localePath);
3680
4536
  isDir = stats.isDirectory();
3681
4537
  exists = true;
3682
4538
  } catch {
@@ -3684,7 +4540,7 @@ var FileMutator = class {
3684
4540
  let isFile = false;
3685
4541
  if (!exists) {
3686
4542
  try {
3687
- await fs7.stat(`${localePath}.json`);
4543
+ await fs8.stat(`${localePath}.json`);
3688
4544
  isFile = true;
3689
4545
  exists = true;
3690
4546
  } catch {
@@ -3694,9 +4550,9 @@ var FileMutator = class {
3694
4550
  throw new Error(`[livelocale] Locale "${locale}" does not exist.`);
3695
4551
  }
3696
4552
  if (isDir) {
3697
- await fs7.rm(localePath, { recursive: true, force: true });
4553
+ await fs8.rm(localePath, { recursive: true, force: true });
3698
4554
  } else if (isFile) {
3699
- await fs7.unlink(`${localePath}.json`);
4555
+ await fs8.unlink(`${localePath}.json`);
3700
4556
  }
3701
4557
  console.log(`[livelocale] Deleted locale '${locale}'`);
3702
4558
  }
@@ -3724,8 +4580,8 @@ var FileMutator = class {
3724
4580
  // src/server/git-client.ts
3725
4581
  import { execFile as execFile3 } from "child_process";
3726
4582
  import { promisify } from "util";
3727
- import * as fs8 from "fs";
3728
- import * as path12 from "path";
4583
+ import * as fs9 from "fs";
4584
+ import * as path14 from "path";
3729
4585
  var execFileAsync = promisify(execFile3);
3730
4586
  var MAX_DIFF_BYTES = 2e5;
3731
4587
  function mapStatus(xy) {
@@ -3756,15 +4612,15 @@ var GitClient = class {
3756
4612
  }
3757
4613
  /** Resolves the repository root and the locales path relative to it (forward slashes). */
3758
4614
  async locate(localesPath) {
3759
- const absolute = path12.resolve(localesPath);
3760
- const cwd = fs8.existsSync(absolute) ? absolute : path12.dirname(absolute);
3761
- if (!fs8.existsSync(cwd)) return null;
4615
+ const absolute = path14.resolve(localesPath);
4616
+ const cwd = fs9.existsSync(absolute) ? absolute : path14.dirname(absolute);
4617
+ if (!fs9.existsSync(cwd)) return null;
3762
4618
  try {
3763
4619
  const root = (await this.git(cwd, ["rev-parse", "--show-toplevel"])).trim();
3764
4620
  if (!root) return null;
3765
- const rel = path12.relative(root, absolute).split(path12.sep).join("/");
4621
+ const rel = path14.relative(root, absolute).split(path14.sep).join("/");
3766
4622
  if (rel.startsWith("..")) return null;
3767
- return { root: path12.resolve(root), rel: rel || ".", absolute };
4623
+ return { root: path14.resolve(root), rel: rel || ".", absolute };
3768
4624
  } catch {
3769
4625
  return null;
3770
4626
  }
@@ -3796,301 +4652,127 @@ var GitClient = class {
3796
4652
  if (file.startsWith('"') && file.endsWith('"')) file = file.slice(1, -1);
3797
4653
  changes.push({ path: file, status: mapStatus(xy) });
3798
4654
  }
3799
- } catch {
3800
- }
3801
- return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
3802
- }
3803
- async diff(localesPath) {
3804
- const loc = await this.locate(localesPath);
3805
- if (!loc) return "";
3806
- let out = "";
3807
- try {
3808
- out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
3809
- } catch {
3810
- }
3811
- try {
3812
- const untracked = await this.git(loc.root, [
3813
- "ls-files",
3814
- "--others",
3815
- "--exclude-standard",
3816
- "--",
3817
- loc.rel
3818
- ]);
3819
- for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
3820
- const full = path12.join(loc.root, file);
3821
- let content = "";
3822
- try {
3823
- content = fs8.readFileSync(full, "utf-8");
3824
- } catch {
3825
- continue;
3826
- }
3827
- const lines = content.split("\n");
3828
- if (lines[lines.length - 1] === "") lines.pop();
3829
- out += `diff --git a/${file} b/${file}
3830
- new file mode 100644
3831
- --- /dev/null
3832
- +++ b/${file}
3833
- @@ -0,0 +1,${lines.length} @@
3834
- ` + lines.map((l) => `+${l}`).join("\n") + "\n";
3835
- }
3836
- } catch {
3837
- }
3838
- if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
3839
- out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
3840
- }
3841
- return out;
3842
- }
3843
- async commit(localesPath, message) {
3844
- const loc = await this.locate(localesPath);
3845
- if (!loc) throw new Error("Not a git repository.");
3846
- const status = await this.status(localesPath);
3847
- if (status.changes.length === 0) {
3848
- throw new Error(`No changes to commit under ${loc.rel}.`);
3849
- }
3850
- await this.git(loc.root, ["add", "-A", "--", loc.rel]);
3851
- await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
3852
- const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
3853
- return { hash };
3854
- }
3855
- async discard(localesPath) {
3856
- const loc = await this.locate(localesPath);
3857
- if (!loc) throw new Error("Not a git repository.");
3858
- try {
3859
- await this.git(loc.root, ["checkout", "--", loc.rel]);
3860
- } catch {
3861
- }
3862
- await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
3863
- }
3864
- };
3865
-
3866
- // src/codemod/apply/report.ts
3867
- var DEFAULT_SPAN_LENGTH = 120;
3868
- var DEFAULT_TABLE_ROWS = 100;
3869
- var MAX_BODY_CHARS = 6e4;
3870
- var MAX_NOTE_CHARS2 = 2e3;
3871
- var MAX_NAME_CHARS = 64;
3872
- var MAX_CHECK_LINES = 50;
3873
- var MAX_CHECK_SUMMARY_CHARS = 1e4;
3874
- var BODY_MARGIN = 200;
3875
- var STATUS_LABELS = {
3876
- apply: "applied",
3877
- already: "already done",
3878
- conflict: "conflict",
3879
- "needs-force": "needs --force",
3880
- invalid: "rejected"
3881
- };
3882
- var BIDI_CONTROL_RE = /[‪-‮⁦-⁩]/g;
3883
- var NEWLINE_RE = /\r\n|\r|\n/g;
3884
- var SPAN_CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
3885
- var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
3886
- function cut(text, max) {
3887
- if (text.length <= max) return text;
3888
- let out = text.slice(0, max);
3889
- const last = out.charCodeAt(out.length - 1);
3890
- if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
3891
- return out;
3892
- }
3893
- function cutLines(text, max) {
3894
- if (text.length <= max) return text;
3895
- const kept = [];
3896
- let length = 0;
3897
- for (const line of text.split("\n")) {
3898
- const added = (kept.length > 0 ? 1 : 0) + line.length;
3899
- if (length + added + 2 > max) break;
3900
- kept.push(line);
3901
- length += added;
3902
- }
3903
- return [...kept, "\u2026"].join("\n");
3904
- }
3905
- function longestBacktickRun(text) {
3906
- let longest = 0;
3907
- for (const run3 of text.match(/`+/g) ?? []) longest = Math.max(longest, run3.length);
3908
- return longest;
3909
- }
3910
- function codePointLabel(ch) {
3911
- return `\u27E8U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
3912
- }
3913
- function codeSpan(text, maxLength = DEFAULT_SPAN_LENGTH) {
3914
- const truncated = text.length > maxLength ? `${cut(text, maxLength)}\u2026` : text;
3915
- const content = truncated.replace(NEWLINE_RE, "\u23CE").replace(BIDI_CONTROL_RE, codePointLabel).replace(SPAN_CONTROL_RE, codePointLabel).replace(/\|/g, "\\|");
3916
- if (content === "") return "` `";
3917
- const fence = "`".repeat(longestBacktickRun(content) + 1);
3918
- const pad = /^[` ]|[` ]$/.test(content) ? " " : "";
3919
- return `${fence}${pad}${content}${pad}${fence}`;
3920
- }
3921
- function sanitizeName(name) {
3922
- if (name === void 0) return void 0;
3923
- const cleaned = name.replace(NEWLINE_RE, " ").replace(CONTROL_RE, "").replace(/[@#<>]/g, "").trim();
3924
- const result = cut(cleaned, MAX_NAME_CHARS).trim();
3925
- return result === "" ? void 0 : result;
3926
- }
3927
- var NO_CHECK_PROBLEMS = "\u2705 `livelocale check`: no problems in the changed keys.";
3928
- function summarizeCheck(report, edited) {
3929
- const changed = new Set(edited.map(([locale, key]) => `${locale}\0${key}`));
3930
- const mine = report.issues.filter(
3931
- (issue) => issue.locale !== void 0 && issue.key !== void 0 && changed.has(`${issue.locale}\0${issue.key}`)
3932
- );
3933
- if (mine.length === 0) return NO_CHECK_PROBLEMS;
3934
- const lines = [`\u26A0 \`livelocale check\` found ${mine.length} problem(s) in the changed keys:`, ""];
3935
- for (const issue of mine.slice(0, MAX_CHECK_LINES)) {
3936
- lines.push(
3937
- `- ${codeSpan(issue.locale ?? "")} ${codeSpan(issue.key ?? "")}: ${codeSpan(issue.message)}`
3938
- );
3939
- }
3940
- if (mine.length > MAX_CHECK_LINES) lines.push(`- \u2026and ${mine.length - MAX_CHECK_LINES} more`);
3941
- const others = report.issues.length - mine.length;
3942
- if (others > 0) {
3943
- lines.push("", `(${others} other problem(s) already in the repository are not shown.)`);
3944
- }
3945
- return lines.join("\n");
3946
- }
3947
- function checksCell(edit) {
3948
- const parts = edit.flags.map((flag) => FLAG_MESSAGES[flag]);
3949
- if ((edit.status === "invalid" || edit.status === "needs-force") && edit.message) {
3950
- parts.push(codeSpan(edit.message));
4655
+ } catch {
4656
+ }
4657
+ return { isRepo: true, branch, changes, mode: "local", pullRequestUrl: null };
3951
4658
  }
3952
- return parts.join("; ");
3953
- }
3954
- function renderRow(edit) {
3955
- const before = edit.from === null ? "\u2014" : codeSpan(edit.from);
3956
- const after = edit.status === "conflict" ? `now: ${edit.current === void 0 ? "(removed)" : codeSpan(edit.current)}` : codeSpan(edit.to);
3957
- const cells = [
3958
- STATUS_LABELS[edit.status],
3959
- codeSpan(edit.locale),
3960
- codeSpan(edit.key),
3961
- before,
3962
- after,
3963
- checksCell(edit)
3964
- ];
3965
- return `| ${cells.join(" | ")} |`;
3966
- }
3967
- function renderTable(plan, limit, maxChars) {
3968
- const lines = [
3969
- "| Status | Locale | Key | Before | After | Checks |",
3970
- "| --- | --- | --- | --- | --- | --- |"
3971
- ];
3972
- const moreLine = (n) => `
3973
-
3974
- \u2026and ${n} more`;
3975
- let length = lines.join("\n").length + moreLine(plan.edits.length).length;
3976
- let shown = 0;
3977
- for (const edit of plan.edits) {
3978
- if (shown >= limit) break;
3979
- const row = renderRow(edit);
3980
- if (length + row.length + 1 > maxChars) break;
3981
- lines.push(row);
3982
- length += row.length + 1;
3983
- shown += 1;
4659
+ async diff(localesPath) {
4660
+ const loc = await this.locate(localesPath);
4661
+ if (!loc) return "";
4662
+ let out = "";
4663
+ try {
4664
+ out = await this.git(loc.root, ["diff", "--no-color", "--", loc.rel]);
4665
+ } catch {
4666
+ }
4667
+ try {
4668
+ const untracked = await this.git(loc.root, [
4669
+ "ls-files",
4670
+ "--others",
4671
+ "--exclude-standard",
4672
+ "--",
4673
+ loc.rel
4674
+ ]);
4675
+ for (const file of untracked.split("\n").map((l) => l.trim()).filter(Boolean)) {
4676
+ const full = path14.join(loc.root, file);
4677
+ let content = "";
4678
+ try {
4679
+ content = fs9.readFileSync(full, "utf-8");
4680
+ } catch {
4681
+ continue;
4682
+ }
4683
+ const lines = content.split("\n");
4684
+ if (lines[lines.length - 1] === "") lines.pop();
4685
+ out += `diff --git a/${file} b/${file}
4686
+ new file mode 100644
4687
+ --- /dev/null
4688
+ +++ b/${file}
4689
+ @@ -0,0 +1,${lines.length} @@
4690
+ ` + lines.map((l) => `+${l}`).join("\n") + "\n";
4691
+ }
4692
+ } catch {
4693
+ }
4694
+ if (Buffer.byteLength(out, "utf-8") > MAX_DIFF_BYTES) {
4695
+ out = out.slice(0, MAX_DIFF_BYTES) + "\n\u2026 diff truncated \u2026\n";
4696
+ }
4697
+ return out;
3984
4698
  }
3985
- const rest = plan.edits.length - shown;
3986
- return lines.join("\n") + (rest > 0 ? moreLine(rest) : "");
3987
- }
3988
- function fencedNote(note) {
3989
- const text = cut(note, MAX_NOTE_CHARS2);
3990
- const fence = "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
3991
- return `${fence}text
3992
- ${text}
3993
- ${fence}`;
3994
- }
3995
- function closingLine(ctx) {
3996
- if (ctx.issueNumber === void 0) return void 0;
3997
- if (ctx.target.isDefault) return `Closes #${ctx.issueNumber}`;
3998
- return `Refs #${ctx.issueNumber}. LiveLocale closes the issue when this pull request is merged or closed.`;
3999
- }
4000
- function renderPullRequestBody(ctx) {
4001
- const head = ["Applies translations sent with a LiveLocale change link."];
4002
- if (ctx.by?.trim()) head.push(`Translated by ${codeSpan(ctx.by)}`);
4003
- if (ctx.target.reason === "not-allowed" && ctx.target.requested !== void 0) {
4004
- head.push(
4005
- `The link was made from ${codeSpan(ctx.target.requested)}, which is not in the allowed branches, so this targets ${codeSpan(ctx.target.branch)}.`
4006
- );
4699
+ async commit(localesPath, message) {
4700
+ const loc = await this.locate(localesPath);
4701
+ if (!loc) throw new Error("Not a git repository.");
4702
+ const status = await this.status(localesPath);
4703
+ if (status.changes.length === 0) {
4704
+ throw new Error(`No changes to commit under ${loc.rel}.`);
4705
+ }
4706
+ await this.git(loc.root, ["add", "-A", "--", loc.rel]);
4707
+ await this.git(loc.root, ["commit", "-q", "-m", message, "--", loc.rel]);
4708
+ const hash = (await this.git(loc.root, ["rev-parse", "--short", "HEAD"])).trim();
4709
+ return { hash };
4007
4710
  }
4008
- if (ctx.note?.trim()) head.push(fencedNote(ctx.note));
4009
- const tail = [];
4010
- if (ctx.checkSummary) tail.push(cutLines(ctx.checkSummary, MAX_CHECK_SUMMARY_CHARS));
4011
- if (ctx.runUrl) tail.push(`Run log: ${ctx.runUrl}`);
4012
- const closing = closingLine(ctx);
4013
- if (closing) tail.push(closing);
4014
- const fixed = [...head, ...tail].join("\n\n").length;
4015
- const table = renderTable(ctx.plan, DEFAULT_TABLE_ROWS, MAX_BODY_CHARS - BODY_MARGIN - fixed);
4016
- return [...head, table, ...tail].join("\n\n");
4017
- }
4018
- function renderCommitMessage(input) {
4019
- let subject = "i18n: apply LiveLocale changes";
4020
- if (input.issueNumber !== void 0) subject += ` (#${input.issueNumber})`;
4021
- else if (input.runId !== void 0) subject += ` (run ${input.runId})`;
4022
- const applied = input.plan.edits.filter((edit) => edit.status === "apply");
4023
- const locales = [...new Set(applied.map((edit) => edit.locale))].sort();
4024
- const parts = [subject, `${applied.length} translation(s) in ${locales.join(", ")}.`];
4025
- const name = sanitizeName(input.by);
4026
- if (name) parts.push(`Translated-by: ${name}`);
4027
- return parts.join("\n\n");
4028
- }
4029
- function renderTerminalSummary(plan) {
4030
- const c = plan.counts;
4031
- const lines = [
4032
- `Applied ${c.apply} \xB7 already done ${c.already} \xB7 conflicts ${c.conflict} \xB7 needs --force ${c["needs-force"]} \xB7 rejected ${c.invalid}`
4033
- ];
4034
- for (const edit of plan.edits) {
4035
- lines.push(`${STATUS_LABELS[edit.status]} ${edit.locale} ${edit.key}`);
4711
+ async discard(localesPath) {
4712
+ const loc = await this.locate(localesPath);
4713
+ if (!loc) throw new Error("Not a git repository.");
4714
+ try {
4715
+ await this.git(loc.root, ["checkout", "--", loc.rel]);
4716
+ } catch {
4717
+ }
4718
+ await this.git(loc.root, ["clean", "-fd", "--", loc.rel]);
4036
4719
  }
4037
- return lines.join("\n");
4038
- }
4720
+ };
4039
4721
 
4040
4722
  // src/codemod/apply/local.ts
4041
- function isPlainObject4(value) {
4723
+ function isPlainObject5(value) {
4042
4724
  return typeof value === "object" && value !== null && !Array.isArray(value);
4043
4725
  }
4044
4726
  async function readTarget(mutator, localesDir, locale) {
4045
4727
  if (!isValidLocaleFormat(locale)) return null;
4046
- const dir = path13.join(localesDir, locale);
4728
+ const dir = path15.join(localesDir, locale);
4047
4729
  let layout;
4048
- if (fs9.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
4730
+ if (fs10.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
4049
4731
  layout = "namespaced";
4050
- } else if (fs9.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
4732
+ } else if (fs10.statSync(`${dir}.json`, { throwIfNoEntry: false })?.isFile()) {
4051
4733
  layout = "flat";
4052
4734
  } else {
4053
4735
  return null;
4054
4736
  }
4055
4737
  const dict = await mutator.readLocale(localesDir, locale);
4056
- if (!isPlainObject4(dict)) {
4738
+ if (!isPlainObject5(dict)) {
4057
4739
  throw new Error(`The "${locale}" locale file is not a JSON object.`);
4058
4740
  }
4059
4741
  return { layout, dict };
4060
4742
  }
4061
4743
  function fileFor(localesPath, layout, locale, key) {
4062
- const rel = layout === "flat" ? path13.join(localesPath, `${locale}.json`) : path13.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
4063
- return path13.normalize(rel).split(path13.sep).join("/");
4744
+ const rel = layout === "flat" ? path15.join(localesPath, `${locale}.json`) : path15.join(localesPath, locale, `${key.slice(0, key.indexOf("."))}.json`);
4745
+ return path15.normalize(rel).split(path15.sep).join("/");
4064
4746
  }
4065
- var PR_TITLE = "i18n: LiveLocale changes";
4747
+ var PR_TITLE2 = "i18n: LiveLocale changes";
4066
4748
  var GITHUB_REMOTE_RE = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/;
4067
4749
  var defaultGit = (args, cwd, opts) => {
4068
4750
  const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
4069
4751
  if (opts?.inherit) {
4070
- return new Promise((resolve14, reject2) => {
4752
+ return new Promise((resolve16, reject2) => {
4071
4753
  const child = spawn2("git", args, { cwd, env, stdio: "inherit", windowsHide: true });
4072
4754
  child.on("error", reject2);
4073
4755
  child.on(
4074
4756
  "close",
4075
- (code) => code === 0 ? resolve14("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
4757
+ (code) => code === 0 ? resolve16("") : reject2(new Error(`git ${args[0]} failed (exit ${code}).`))
4076
4758
  );
4077
4759
  });
4078
4760
  }
4079
- return new Promise((resolve14, reject2) => {
4761
+ return new Promise((resolve16, reject2) => {
4080
4762
  execFile4(
4081
4763
  "git",
4082
4764
  args,
4083
4765
  { cwd, env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 },
4084
- (error, stdout) => error ? reject2(error) : resolve14(String(stdout))
4766
+ (error, stdout) => error ? reject2(error) : resolve16(String(stdout))
4085
4767
  );
4086
4768
  });
4087
4769
  };
4088
- var defaultGh = (args, cwd) => new Promise((resolve14, reject2) => {
4770
+ var defaultGh = (args, cwd) => new Promise((resolve16, reject2) => {
4089
4771
  execFile4(
4090
4772
  "gh",
4091
4773
  args,
4092
4774
  { cwd, windowsHide: true },
4093
- (error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve14(String(stdout))
4775
+ (error, stdout, stderr) => error ? reject2(Object.assign(error, { stderr: String(stderr) })) : resolve16(String(stdout))
4094
4776
  );
4095
4777
  });
4096
4778
  function utcStamp(date) {
@@ -4122,7 +4804,7 @@ async function pickBranchName(git, root, stamp) {
4122
4804
  for (let n = 2; await taken(name); n++) name = `${base}-${n}`;
4123
4805
  return name;
4124
4806
  }
4125
- function errorMessage(err) {
4807
+ function errorMessage2(err) {
4126
4808
  return err instanceof Error ? err.message : String(err);
4127
4809
  }
4128
4810
  async function writeEdits(mutator, plan, targets, localesDir, localesPath) {
@@ -4142,12 +4824,12 @@ function exitCodeFor(plan) {
4142
4824
  async function preparePullRequest(git, projectDir, localesDir) {
4143
4825
  const top = (await tryRun(() => git(["rev-parse", "--show-toplevel"], projectDir)))?.trim();
4144
4826
  if (!top) throw new ApplyInputError("apply --pr needs a git repository.");
4145
- const root = path13.resolve(top);
4827
+ const root = path15.resolve(top);
4146
4828
  const original = (await tryRun(() => git(["symbolic-ref", "--quiet", "--short", "HEAD"], root)))?.trim();
4147
4829
  if (!original) throw new ApplyInputError("Check out a branch first.");
4148
4830
  const status = await new GitClient().status(localesDir);
4149
4831
  if (status.changes.length > 0) {
4150
- const rel = path13.relative(root, localesDir).split(path13.sep).join("/") || ".";
4832
+ const rel = path15.relative(root, localesDir).split(path15.sep).join("/") || ".";
4151
4833
  throw new ApplyInputError(
4152
4834
  `Commit or stash the changes under ${rel} first; apply --pr commits everything in that folder.`
4153
4835
  );
@@ -4155,10 +4837,10 @@ async function preparePullRequest(git, projectDir, localesDir) {
4155
4837
  return { root, original };
4156
4838
  }
4157
4839
  async function createPullRequest(log, gh, root, base, head, body) {
4158
- const tmpDir = fs9.mkdtempSync(path13.join(os.tmpdir(), "livelocale-pr-"));
4159
- const bodyFile = path13.join(tmpDir, "body.md");
4840
+ const tmpDir = fs10.mkdtempSync(path15.join(os2.tmpdir(), "livelocale-pr-"));
4841
+ const bodyFile = path15.join(tmpDir, "body.md");
4160
4842
  try {
4161
- fs9.writeFileSync(bodyFile, body, "utf-8");
4843
+ fs10.writeFileSync(bodyFile, body, "utf-8");
4162
4844
  const out = await gh(
4163
4845
  [
4164
4846
  "pr",
@@ -4168,7 +4850,7 @@ async function createPullRequest(log, gh, root, base, head, body) {
4168
4850
  "--head",
4169
4851
  head,
4170
4852
  "--title",
4171
- PR_TITLE,
4853
+ PR_TITLE2,
4172
4854
  "--body-file",
4173
4855
  bodyFile
4174
4856
  ],
@@ -4179,20 +4861,20 @@ async function createPullRequest(log, gh, root, base, head, body) {
4179
4861
  } catch (err) {
4180
4862
  if (err?.code !== "ENOENT") {
4181
4863
  const stderr = err?.stderr;
4182
- const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage(err);
4864
+ const reason = typeof stderr === "string" && stderr.trim() ? stderr.trim() : errorMessage2(err);
4183
4865
  log(`gh could not open the pull request: ${reason}`);
4184
4866
  }
4185
4867
  return void 0;
4186
4868
  } finally {
4187
- fs9.rmSync(tmpDir, { recursive: true, force: true });
4869
+ fs10.rmSync(tmpDir, { recursive: true, force: true });
4188
4870
  }
4189
4871
  }
4190
4872
  async function runLocalApply(options) {
4191
4873
  const log = options.log ?? ((line) => console.log(line));
4192
4874
  const set = await decodeChangeLink(options.input, nodeInflate);
4193
4875
  const localesPath = options.localesPath ?? "locales";
4194
- const localesDir = path13.resolve(options.projectDir, localesPath);
4195
- if (!fs9.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
4876
+ const localesDir = path15.resolve(options.projectDir, localesPath);
4877
+ if (!fs10.statSync(localesDir, { throwIfNoEntry: false })?.isDirectory()) {
4196
4878
  throw new ApplyInputError(`No locales folder at ${localesPath}.`);
4197
4879
  }
4198
4880
  const pr = options.pr === true && options.dryRun !== true;
@@ -4248,7 +4930,7 @@ async function runLocalApply(options) {
4248
4930
  const edited = plan.edits.filter((edit) => edit.status === "apply").map((edit) => [edit.locale, edit.key]);
4249
4931
  checkSummary = summarizeCheck(report, edited);
4250
4932
  } catch (err) {
4251
- checkSummary = `Check could not run: ${errorMessage(err)}`;
4933
+ checkSummary = `Check could not run: ${errorMessage2(err)}`;
4252
4934
  }
4253
4935
  await new GitClient().commit(localesDir, renderCommitMessage({ plan, by: set.by }));
4254
4936
  await git(["push", "-u", "origin", branch], root, { inherit: true });
@@ -4292,7 +4974,8 @@ var FLAG_OPTIONS = {
4292
4974
  "--clipboard": "clipboard",
4293
4975
  "--pr": "pr",
4294
4976
  "--force": "force",
4295
- "--dry-run": "dryRun"
4977
+ "--dry-run": "dryRun",
4978
+ "--github-event": "githubEvent"
4296
4979
  };
4297
4980
  function parseApplyArgs(argv) {
4298
4981
  const args = {
@@ -4303,11 +4986,20 @@ function parseApplyArgs(argv) {
4303
4986
  dryRun: false,
4304
4987
  dir: ".",
4305
4988
  locales: void 0,
4306
- source: void 0
4989
+ source: void 0,
4990
+ githubEvent: false,
4991
+ branches: void 0
4307
4992
  };
4308
4993
  for (let i = 0; i < argv.length; i++) {
4309
4994
  const arg = argv[i];
4310
- if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
4995
+ if (arg === "--branches") {
4996
+ const value = argv[i + 1];
4997
+ if (value === void 0 || value.startsWith("--")) {
4998
+ throw new ApplyInputError(`${arg} needs a value.`);
4999
+ }
5000
+ args.branches = value.split(",").map((branch) => branch.trim()).filter(Boolean);
5001
+ i++;
5002
+ } else if (Object.prototype.hasOwnProperty.call(VALUE_OPTIONS2, arg)) {
4311
5003
  const value = argv[i + 1];
4312
5004
  if (value === void 0 || value.startsWith("--")) {
4313
5005
  throw new ApplyInputError(`${arg} needs a value.`);
@@ -4327,17 +5019,43 @@ function parseApplyArgs(argv) {
4327
5019
  if (args.clipboard && args.input !== void 0) {
4328
5020
  throw new ApplyInputError("Pass the link or --clipboard, not both.");
4329
5021
  }
5022
+ if (args.githubEvent) {
5023
+ if (args.force) {
5024
+ throw new ApplyInputError(
5025
+ "--force is not allowed with --github-event; a maintainer comments `/livelocale apply --force`."
5026
+ );
5027
+ }
5028
+ if (args.input !== void 0) {
5029
+ throw new ApplyInputError(
5030
+ "--github-event reads the link from the event file; do not pass one."
5031
+ );
5032
+ }
5033
+ if (args.locales === void 0 || args.source === void 0) {
5034
+ throw new ApplyInputError("--github-event needs --locales and --source.");
5035
+ }
5036
+ }
4330
5037
  return args;
4331
5038
  }
4332
5039
  async function runApplyCommand(argv) {
4333
5040
  try {
4334
5041
  const args = parseApplyArgs(argv);
5042
+ if (args.githubEvent) {
5043
+ const result2 = await runGitHubEventApply({
5044
+ env: process.env,
5045
+ localesPath: args.locales,
5046
+ sourceLocale: args.source,
5047
+ branches: args.branches,
5048
+ dir: args.dir
5049
+ });
5050
+ process.exitCode = result2 === "error" || result2 === "wrong-repo" ? 1 : 0;
5051
+ return;
5052
+ }
4335
5053
  const input = await readApplyInput(args.input, {
4336
5054
  cwd: process.cwd(),
4337
5055
  clipboard: args.clipboard
4338
5056
  });
4339
5057
  const result = await runLocalApply({
4340
- projectDir: path14.resolve(args.dir),
5058
+ projectDir: path16.resolve(args.dir),
4341
5059
  localesPath: args.locales,
4342
5060
  sourceLocale: args.source,
4343
5061
  input,
@@ -4352,6 +5070,327 @@ async function runApplyCommand(argv) {
4352
5070
  }
4353
5071
  }
4354
5072
 
5073
+ // src/codemod/init-share.ts
5074
+ import * as fs11 from "fs";
5075
+ import * as path17 from "path";
5076
+ var CHECKOUT_ACTION = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2";
5077
+ var WORKFLOW_PATH = ".github/workflows/livelocale.yml";
5078
+ var LOCALES_CANDIDATES = [
5079
+ "locales",
5080
+ "src/locales",
5081
+ "public/locales",
5082
+ "src/i18n/locales",
5083
+ "messages"
5084
+ ];
5085
+ var SAFE_ARG = /^[A-Za-z0-9_./-]+$/;
5086
+ var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
5087
+ var MAX_BRANCHES = 10;
5088
+ var SHARE_GITIGNORE = "public/livelocale/";
5089
+ function readText3(file) {
5090
+ try {
5091
+ return fs11.readFileSync(file, "utf-8");
5092
+ } catch {
5093
+ return null;
5094
+ }
5095
+ }
5096
+ function parsePackageJson(text) {
5097
+ if (text === null) return null;
5098
+ try {
5099
+ const parsed = JSON.parse(text);
5100
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5101
+ } catch {
5102
+ return null;
5103
+ }
5104
+ }
5105
+ function hasDependency2(pkg, name) {
5106
+ return Boolean(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]);
5107
+ }
5108
+ function detectShareStack(projectDir) {
5109
+ const target = detectBuildTarget(projectDir);
5110
+ if (target.kind === "next") {
5111
+ return target.staticExport ? { stack: "next-export", evidence: "Next.js with output: 'export'" } : { stack: "next-server", evidence: "Next.js without output: 'export'" };
5112
+ }
5113
+ if (target.kind === "vite") return { stack: "vite", evidence: "Vite" };
5114
+ if (target.kind === "cra") return { stack: "cra", evidence: "react-scripts" };
5115
+ const pkg = parsePackageJson(readText3(path17.join(projectDir, "package.json")));
5116
+ for (const name of ["webpack", "@rsbuild/core"]) {
5117
+ if (hasDependency2(pkg, name)) return { stack: "webpack", evidence: `${name} dependency` };
5118
+ }
5119
+ const config = fs11.readdirSync(projectDir).find((name) => /^webpack\.config\.[A-Za-z]+$/.test(name));
5120
+ if (config) return { stack: "webpack", evidence: config };
5121
+ return { stack: "unknown", evidence: "no Vite, Next.js, CRA, webpack or Rsbuild setup found" };
5122
+ }
5123
+ function checkSafePath(name, value) {
5124
+ const segments = value.split("/");
5125
+ if (!SAFE_ARG.test(value) || value.startsWith("/") || segments.includes("..")) {
5126
+ throw new Error(`\`${name}\` may only contain letters, digits, ".", "_", "-" and "/".`);
5127
+ }
5128
+ }
5129
+ function validateShareArgs(o) {
5130
+ checkSafePath("--locales", o.localesPath);
5131
+ if (o.dir !== void 0) checkSafePath("--dir", o.dir);
5132
+ if (!LOCALE_RE.test(o.sourceLocale)) {
5133
+ throw new Error("`--source` must be a locale code such as en or pt-BR.");
5134
+ }
5135
+ if (o.branches.length === 0) throw new Error("`--branches` needs at least one branch.");
5136
+ if (o.branches.length > MAX_BRANCHES) {
5137
+ throw new Error(`\`--branches\` takes at most ${MAX_BRANCHES} branches.`);
5138
+ }
5139
+ for (const branch of o.branches) checkSafePath("--branches", branch);
5140
+ }
5141
+ function renderWorkflow(o) {
5142
+ validateShareArgs(o);
5143
+ if (!VERSION_RE.test(o.version)) throw new Error(`Not a livelocale version: ${o.version}`);
5144
+ const dirArg = o.dir === void 0 ? "" : ` --dir ${o.dir}`;
5145
+ return [
5146
+ "# Written by `npx livelocale init --share`. Turns LiveLocale change links into pull requests.",
5147
+ "# livelocale reads the link from the event file; it never reaches a shell command.",
5148
+ "name: LiveLocale",
5149
+ "on:",
5150
+ " issues: { types: [opened] }",
5151
+ " issue_comment: { types: [created] }",
5152
+ " pull_request: { types: [closed] }",
5153
+ " workflow_dispatch: { inputs: { link: { description: 'LiveLocale change link', required: true } } }",
5154
+ "permissions: { contents: write, pull-requests: write, issues: write }",
5155
+ "concurrency: { group: 'livelocale-${{ github.event.issue.number || github.run_id }}', queue: max }",
5156
+ "jobs:",
5157
+ " apply:",
5158
+ " if: >-",
5159
+ " github.event_name == 'workflow_dispatch' ||",
5160
+ " (github.event_name == 'issues' && contains(github.event.issue.body, '```livelocale')) ||",
5161
+ " (github.event_name == 'issue_comment' && !github.event.issue.pull_request &&",
5162
+ " startsWith(github.event.comment.body, '/livelocale')) ||",
5163
+ " (github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'livelocale/issue-') &&",
5164
+ " github.event.pull_request.base.ref != github.event.repository.default_branch)",
5165
+ " runs-on: ubuntu-latest",
5166
+ " steps:",
5167
+ ` - uses: ${CHECKOUT_ACTION}`,
5168
+ " with: { persist-credentials: false }",
5169
+ ` - run: npx --yes livelocale@${o.version} apply --github-event --locales ${o.localesPath} --source ${o.sourceLocale} --branches ${o.branches.join(",")}${dirArg}`,
5170
+ " env: { GITHUB_TOKEN: '${{ github.token }}' }",
5171
+ ""
5172
+ ].join("\n");
5173
+ }
5174
+ function toPosix2(p) {
5175
+ return p.split(path17.sep).join("/");
5176
+ }
5177
+ function normalizeArgPath(p) {
5178
+ return p.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
5179
+ }
5180
+ function realDir(dir) {
5181
+ try {
5182
+ return fs11.realpathSync(dir);
5183
+ } catch {
5184
+ return path17.resolve(dir);
5185
+ }
5186
+ }
5187
+ function holdsLocaleJson(dir) {
5188
+ let entries;
5189
+ try {
5190
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
5191
+ } catch {
5192
+ return false;
5193
+ }
5194
+ return entries.some((entry) => {
5195
+ if (entry.isFile())
5196
+ return entry.name.endsWith(".json") && LOCALE_RE.test(entry.name.slice(0, -5));
5197
+ if (!entry.isDirectory() || !LOCALE_RE.test(entry.name)) return false;
5198
+ try {
5199
+ return fs11.readdirSync(path17.join(dir, entry.name)).some((name) => name.endsWith(".json"));
5200
+ } catch {
5201
+ return false;
5202
+ }
5203
+ });
5204
+ }
5205
+ function findLocalesPath(projectDir) {
5206
+ const found = LOCALES_CANDIDATES.find((rel) => holdsLocaleJson(path17.join(projectDir, rel)));
5207
+ if (!found) throw new Error("Pass --locales <path>.");
5208
+ return found;
5209
+ }
5210
+ async function defaultBranchOf(git, cwd) {
5211
+ const ref = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
5212
+ if (ref?.startsWith("origin/") && ref.length > "origin/".length) {
5213
+ return ref.slice("origin/".length);
5214
+ }
5215
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
5216
+ if (head && head !== "HEAD") return head;
5217
+ return "main";
5218
+ }
5219
+ function planWrite(projectDir, rel, content) {
5220
+ const current = readText3(path17.join(projectDir, rel));
5221
+ if (current === null) return { path: rel, action: "create", content };
5222
+ return current === content ? { path: rel, action: "unchanged" } : { path: rel, action: "update", content };
5223
+ }
5224
+ function planViteConfig(c) {
5225
+ const addNote = `Add liveLocaleVite({ localesPath: '${c.localesPath}', share: true }) to the plugins array in vite.config.`;
5226
+ const config = VITE_CONFIG_FILES.find((name) => fs11.existsSync(path17.join(c.projectDir, name)));
5227
+ if (!config) {
5228
+ c.notes.push(addNote);
5229
+ return [];
5230
+ }
5231
+ const source = readText3(path17.join(c.projectDir, config)) ?? "";
5232
+ if (!source.includes("liveLocaleVite(")) {
5233
+ c.notes.push(addNote);
5234
+ return [{ path: config, action: "skipped", reason: "no liveLocaleVite(...) call" }];
5235
+ }
5236
+ if (/\bshare\s*:/.test(source)) return [{ path: config, action: "unchanged" }];
5237
+ const call = /liveLocaleVite\(\{\s*/;
5238
+ if (!call.test(source)) {
5239
+ c.notes.push(addNote);
5240
+ return [{ path: config, action: "skipped", reason: "liveLocaleVite(...) has no { options }" }];
5241
+ }
5242
+ const source2 = c.sourceLocale === "en" ? "" : `sourceLocale: '${c.sourceLocale}', `;
5243
+ const next = source.replace(call, () => `liveLocaleVite({ share: true, ${source2}`);
5244
+ return [{ path: config, action: "update", content: next }];
5245
+ }
5246
+ function planPackageScripts(c) {
5247
+ const text = readText3(path17.join(c.projectDir, "package.json"));
5248
+ const pkg = parsePackageJson(text);
5249
+ const args = `--locales ${c.localesPath} --source ${c.sourceLocale}`;
5250
+ const isNext = c.stack === "next-export" || c.stack === "next-server";
5251
+ const outDir = c.stack === "cra" ? "build" : "dist";
5252
+ const packCommand = `livelocale pack ${outDir} ${args}`;
5253
+ if (!pkg) {
5254
+ c.notes.push(
5255
+ isNext ? `Add "build:preview": "livelocale build --preview ${args} -- next build" to package.json.` : `Run "${packCommand}" after your build.`
5256
+ );
5257
+ return [{ path: "package.json", action: "skipped", reason: "no readable package.json" }];
5258
+ }
5259
+ const scripts = { ...pkg.scripts ?? {} };
5260
+ if (isNext) {
5261
+ const wanted = `livelocale build --preview ${args} -- next build`;
5262
+ const current = scripts["build:preview"];
5263
+ if (current === wanted || typeof current === "string" && current.includes("livelocale build")) {
5264
+ return [{ path: "package.json", action: "unchanged" }];
5265
+ }
5266
+ if (current !== void 0) {
5267
+ c.notes.push(`Set "build:preview" to "${wanted}" in package.json.`);
5268
+ return [
5269
+ {
5270
+ path: "package.json",
5271
+ action: "skipped",
5272
+ reason: 'scripts["build:preview"] already exists'
5273
+ }
5274
+ ];
5275
+ }
5276
+ scripts["build:preview"] = wanted;
5277
+ } else {
5278
+ const build = scripts.build;
5279
+ if (typeof build !== "string" || build.trim() === "") {
5280
+ c.notes.push(`Run "${packCommand}" after your build.`);
5281
+ return [{ path: "package.json", action: "skipped", reason: "no scripts.build" }];
5282
+ }
5283
+ if (build.includes("livelocale pack")) return [{ path: "package.json", action: "unchanged" }];
5284
+ scripts.build = `${build} && ${packCommand}`;
5285
+ }
5286
+ return [
5287
+ {
5288
+ path: "package.json",
5289
+ action: "update",
5290
+ content: stringifyLike({ ...pkg, scripts }, text)
5291
+ }
5292
+ ];
5293
+ }
5294
+ function planGitignore(projectDir) {
5295
+ const current = readText3(path17.join(projectDir, ".gitignore"));
5296
+ if (current === null) {
5297
+ return { path: ".gitignore", action: "create", content: `${SHARE_GITIGNORE}
5298
+ ` };
5299
+ }
5300
+ if (parseGitignore(current).ignores("public/livelocale", true)) {
5301
+ return { path: ".gitignore", action: "unchanged" };
5302
+ }
5303
+ const sep9 = current === "" || current.endsWith("\n") ? "" : "\n";
5304
+ return { path: ".gitignore", action: "update", content: `${current}${sep9}${SHARE_GITIGNORE}
5305
+ ` };
5306
+ }
5307
+ function planBuild(c) {
5308
+ switch (c.stack) {
5309
+ case "vite":
5310
+ return planViteConfig(c);
5311
+ case "cra":
5312
+ case "webpack":
5313
+ return planPackageScripts(c);
5314
+ case "next-export":
5315
+ case "next-server": {
5316
+ const files = [...planPackageScripts(c), planGitignore(c.projectDir)];
5317
+ if (c.stack === "next-export") {
5318
+ const route = findExportBlockingRoute(c.projectDir);
5319
+ if (route) {
5320
+ c.notes.push(
5321
+ `Delete ${route}: static export rejects POST routes, and livelocale build stops while it exists.`
5322
+ );
5323
+ }
5324
+ } else {
5325
+ c.notes.push(
5326
+ 'Use "npm run build:preview" as the build command of your preview deployments.'
5327
+ );
5328
+ }
5329
+ return files;
5330
+ }
5331
+ case "unknown":
5332
+ c.notes.push(
5333
+ `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}.`
5334
+ );
5335
+ return [];
5336
+ }
5337
+ }
5338
+ async function planInitShare(o) {
5339
+ const projectDir = realDir(o.projectDir);
5340
+ const git = o.git ?? runGit;
5341
+ const sourceLocale = o.sourceLocale ?? "en";
5342
+ const localesPath = o.localesPath !== void 0 ? normalizeArgPath(o.localesPath) : findLocalesPath(projectDir);
5343
+ validateShareArgs({ localesPath, sourceLocale, branches: o.branches ?? ["main"] });
5344
+ const { stack } = detectShareStack(projectDir);
5345
+ const notes = [];
5346
+ const files = planBuild({ projectDir, stack, localesPath, sourceLocale, notes });
5347
+ const remote = await git(["remote", "get-url", "origin"], projectDir);
5348
+ if (!remote || parseGitHubSlug(remote) === null) {
5349
+ notes.push('One-click pull requests need GitHub. Use: npx livelocale apply "<link>" --pr');
5350
+ return { stack, files, notes };
5351
+ }
5352
+ const top = await git(["rev-parse", "--show-toplevel"], projectDir);
5353
+ const repoRoot = top ? realDir(top) : projectDir;
5354
+ const appRel = toPosix2(path17.relative(repoRoot, projectDir));
5355
+ const dir = appRel === "" ? void 0 : appRel;
5356
+ const repoLocales = dir === void 0 ? localesPath : `${dir}/${localesPath}`;
5357
+ const defaultBranch = await defaultBranchOf(git, projectDir);
5358
+ const branches = o.branches ?? [defaultBranch];
5359
+ const workflow = renderWorkflow({
5360
+ version: o.version,
5361
+ localesPath: repoLocales,
5362
+ sourceLocale,
5363
+ branches,
5364
+ dir
5365
+ });
5366
+ const workflowRel = toPosix2(path17.relative(projectDir, path17.join(repoRoot, WORKFLOW_PATH)));
5367
+ const planned = planWrite(projectDir, workflowRel, workflow);
5368
+ files.push(
5369
+ planned.action === "update" && !o.overwrite ? {
5370
+ path: workflowRel,
5371
+ action: "skipped",
5372
+ reason: "it exists with different content; pass --overwrite to replace it"
5373
+ } : planned
5374
+ );
5375
+ for (const branch of branches) {
5376
+ if (branch === defaultBranch) continue;
5377
+ notes.push(
5378
+ `Also merge ${WORKFLOW_PATH} into \`${branch}\`; pull requests into it run workflows from that branch, so LiveLocale cannot close their issues otherwise.`
5379
+ );
5380
+ }
5381
+ return { stack, files, notes };
5382
+ }
5383
+ function writePlannedFiles(projectDir, files) {
5384
+ for (const file of files) {
5385
+ if (file.action !== "create" && file.action !== "update" || file.content === void 0) {
5386
+ continue;
5387
+ }
5388
+ const target = path17.resolve(projectDir, file.path);
5389
+ fs11.mkdirSync(path17.dirname(target), { recursive: true });
5390
+ fs11.writeFileSync(target, file.content, "utf-8");
5391
+ }
5392
+ }
5393
+
4355
5394
  // src/codemod/index.ts
4356
5395
  var DEFAULT_STUDIO_PORT = 3010;
4357
5396
  var DEFAULT_APP_ORIGIN = "http://localhost:3000";
@@ -4379,6 +5418,11 @@ Options for migrate:
4379
5418
 
4380
5419
  Options for init:
4381
5420
  --dir <path> Target project directory (default: current directory)
5421
+ --share Set up the share link build and the GitHub workflow instead of the API route
5422
+ --locales <path> (--share) Locales directory relative to the project (default: detected)
5423
+ --source <locale> (--share) Source locale (default: en)
5424
+ --branches <a,b> (--share) Branches a change link may target (default: the default branch)
5425
+ --overwrite (--share) Replace an existing .github/workflows/livelocale.yml
4382
5426
 
4383
5427
  Options for studio:
4384
5428
  --port <number> Port to run the studio server on (default: ${DEFAULT_STUDIO_PORT})
@@ -4424,6 +5468,8 @@ Options for apply:
4424
5468
  --source <locale> Source locale (default: en, else the first locale)
4425
5469
  --force Apply edits the translator saved with "Save anyway"
4426
5470
  --dry-run Show what would change without writing files
5471
+ --github-event Run inside the generated GitHub workflow; reads the link from the event file
5472
+ --branches <a,b> Branches a link may target (default: the repository's default branch)
4427
5473
 
4428
5474
  Global Options:
4429
5475
  -h, --help Show help information
@@ -4452,11 +5498,11 @@ function normalizeAppArg(value) {
4452
5498
  }
4453
5499
  }
4454
5500
  function isPortAnswering(port) {
4455
- return new Promise((resolve14) => {
5501
+ return new Promise((resolve16) => {
4456
5502
  const socket = net.connect({ host: "localhost", port });
4457
5503
  const done = (answering) => {
4458
5504
  socket.destroy();
4459
- resolve14(answering);
5505
+ resolve16(answering);
4460
5506
  };
4461
5507
  socket.setTimeout(1e3, () => done(false));
4462
5508
  socket.once("connect", () => done(true));
@@ -4466,8 +5512,8 @@ function isPortAnswering(port) {
4466
5512
  async function runStudio(port, appOrigin) {
4467
5513
  console.log(`
4468
5514
  \u{1F680} Starting LiveLocale Studio...`);
4469
- const studioDir = path15.resolve(moduleDir(), "../studio");
4470
- if (!fs10.existsSync(path15.join(studioDir, "index.html"))) {
5515
+ const studioDir = path18.resolve(moduleDir(), "../studio");
5516
+ if (!fs12.existsSync(path18.join(studioDir, "index.html"))) {
4471
5517
  console.error(`Error: Studio assets not found at "${studioDir}".`);
4472
5518
  console.error(`Did you build the project? Run "npm run build" first.`);
4473
5519
  process.exit(1);
@@ -4499,48 +5545,48 @@ async function runStudio(port, appOrigin) {
4499
5545
  function runInit(projectDir) {
4500
5546
  console.log(`
4501
5547
  \u{1F680} Initializing LiveLocale API handler...`);
4502
- const absoluteDir = path15.resolve(projectDir);
4503
- if (!fs10.existsSync(absoluteDir)) {
5548
+ const absoluteDir = path18.resolve(projectDir);
5549
+ if (!fs12.existsSync(absoluteDir)) {
4504
5550
  console.error(`Error: Directory "${projectDir}" does not exist.`);
4505
5551
  process.exit(1);
4506
5552
  }
4507
- const isTypeScript = fs10.existsSync(path15.join(absoluteDir, "tsconfig.json"));
5553
+ const isTypeScript = fs12.existsSync(path18.join(absoluteDir, "tsconfig.json"));
4508
5554
  const fileExt = isTypeScript ? "ts" : "js";
4509
5555
  console.log(`\u{1F4C1} Target: ${absoluteDir}`);
4510
5556
  console.log(`\u{1F527} Language detected: ${isTypeScript ? "TypeScript" : "JavaScript"}`);
4511
5557
  let hasNextIntl = false;
4512
5558
  let hasReactI18next = false;
4513
- const targetPkgPath = path15.join(absoluteDir, "package.json");
4514
- if (fs10.existsSync(targetPkgPath)) {
5559
+ const targetPkgPath = path18.join(absoluteDir, "package.json");
5560
+ if (fs12.existsSync(targetPkgPath)) {
4515
5561
  try {
4516
- const pkg = JSON.parse(fs10.readFileSync(targetPkgPath, "utf8"));
5562
+ const pkg = JSON.parse(fs12.readFileSync(targetPkgPath, "utf8"));
4517
5563
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4518
5564
  hasNextIntl = Boolean(allDeps["next-intl"]);
4519
5565
  hasReactI18next = Boolean(allDeps["react-i18next"]);
4520
5566
  } catch {
4521
5567
  }
4522
5568
  }
4523
- const appDirPath = path15.join(absoluteDir, "app");
4524
- const pagesDirPath = path15.join(absoluteDir, "pages");
4525
- const srcAppDirPath = path15.join(absoluteDir, "src", "app");
4526
- const srcPagesDirPath = path15.join(absoluteDir, "src", "pages");
5569
+ const appDirPath = path18.join(absoluteDir, "app");
5570
+ const pagesDirPath = path18.join(absoluteDir, "pages");
5571
+ const srcAppDirPath = path18.join(absoluteDir, "src", "app");
5572
+ const srcPagesDirPath = path18.join(absoluteDir, "src", "pages");
4527
5573
  let targetPath = "";
4528
5574
  let routerType = "";
4529
5575
  let content = "";
4530
- if (fs10.existsSync(appDirPath)) {
4531
- targetPath = path15.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
5576
+ if (fs12.existsSync(appDirPath)) {
5577
+ targetPath = path18.join(appDirPath, "api", "livelocale", `route.${fileExt}`);
4532
5578
  routerType = "Next.js App Router (root)";
4533
- } else if (fs10.existsSync(srcAppDirPath)) {
4534
- targetPath = path15.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
5579
+ } else if (fs12.existsSync(srcAppDirPath)) {
5580
+ targetPath = path18.join(srcAppDirPath, "api", "livelocale", `route.${fileExt}`);
4535
5581
  routerType = "Next.js App Router (src/)";
4536
- } else if (fs10.existsSync(pagesDirPath)) {
4537
- targetPath = path15.join(pagesDirPath, "api", `livelocale.${fileExt}`);
5582
+ } else if (fs12.existsSync(pagesDirPath)) {
5583
+ targetPath = path18.join(pagesDirPath, "api", `livelocale.${fileExt}`);
4538
5584
  routerType = "Next.js Pages Router (root)";
4539
- } else if (fs10.existsSync(srcPagesDirPath)) {
4540
- targetPath = path15.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
5585
+ } else if (fs12.existsSync(srcPagesDirPath)) {
5586
+ targetPath = path18.join(srcPagesDirPath, "api", `livelocale.${fileExt}`);
4541
5587
  routerType = "Next.js Pages Router (src/)";
4542
5588
  } else {
4543
- targetPath = path15.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
5589
+ targetPath = path18.join(absoluteDir, "app", "api", "livelocale", `route.${fileExt}`);
4544
5590
  routerType = "Default App Router (app/ not found)";
4545
5591
  }
4546
5592
  console.log(`\u{1F4CD} Project structure: ${routerType}`);
@@ -4568,14 +5614,14 @@ const handler = createLiveLocaleHandler({
4568
5614
  export default handler;
4569
5615
  `;
4570
5616
  }
4571
- const targetDir = path15.dirname(targetPath);
4572
- if (!fs10.existsSync(targetDir)) {
4573
- fs10.mkdirSync(targetDir, { recursive: true });
5617
+ const targetDir = path18.dirname(targetPath);
5618
+ if (!fs12.existsSync(targetDir)) {
5619
+ fs12.mkdirSync(targetDir, { recursive: true });
4574
5620
  }
4575
5621
  try {
4576
- fs10.writeFileSync(targetPath, content, "utf8");
5622
+ fs12.writeFileSync(targetPath, content, "utf8");
4577
5623
  console.log(`\u2705 Created API Route Handler successfully at:`);
4578
- console.log(` ${path15.relative(absoluteDir, targetPath)}`);
5624
+ console.log(` ${path18.relative(absoluteDir, targetPath)}`);
4579
5625
  if (hasNextIntl) {
4580
5626
  console.log(`
4581
5627
  \u{1F4A1} Detected 'next-intl' in your project!`);
@@ -4617,8 +5663,8 @@ async function runMigrate(args) {
4617
5663
  }
4618
5664
  }
4619
5665
  }
4620
- const absoluteDir = path15.resolve(dir);
4621
- if (!fs10.existsSync(absoluteDir)) {
5666
+ const absoluteDir = path18.resolve(dir);
5667
+ if (!fs12.existsSync(absoluteDir)) {
4622
5668
  console.error(`Error: Directory "${dir}" does not exist.`);
4623
5669
  process.exit(1);
4624
5670
  }
@@ -4642,28 +5688,28 @@ async function runMigrate(args) {
4642
5688
  let scannedCount = 0;
4643
5689
  let migratedCount = 0;
4644
5690
  async function walk(currentDir) {
4645
- const files = fs10.readdirSync(currentDir, { withFileTypes: true });
5691
+ const files = fs12.readdirSync(currentDir, { withFileTypes: true });
4646
5692
  for (const file of files) {
4647
- const fullPath = path15.join(currentDir, file.name);
5693
+ const fullPath = path18.join(currentDir, file.name);
4648
5694
  if (file.isDirectory()) {
4649
5695
  if (excludes.includes(file.name)) {
4650
5696
  continue;
4651
5697
  }
4652
5698
  await walk(fullPath);
4653
5699
  } else if (file.isFile()) {
4654
- const ext = path15.extname(file.name);
5700
+ const ext = path18.extname(file.name);
4655
5701
  if ([".js", ".jsx", ".ts", ".tsx"].includes(ext)) {
4656
5702
  scannedCount++;
4657
5703
  try {
4658
- const content = fs10.readFileSync(fullPath, "utf8");
5704
+ const content = fs12.readFileSync(fullPath, "utf8");
4659
5705
  const result = await transformReactI18next2(content, fullPath);
4660
5706
  if (result.modified) {
4661
5707
  migratedCount++;
4662
- const relativePath = path15.relative(absoluteDir, fullPath);
5708
+ const relativePath = path18.relative(absoluteDir, fullPath);
4663
5709
  if (dryRun) {
4664
5710
  console.log(`[DRY RUN] Would migrate: ${relativePath}`);
4665
5711
  } else {
4666
- fs10.writeFileSync(fullPath, result.code, "utf8");
5712
+ fs12.writeFileSync(fullPath, result.code, "utf8");
4667
5713
  console.log(`\u2705 Migrated: ${relativePath}`);
4668
5714
  }
4669
5715
  }
@@ -4712,8 +5758,8 @@ async function runDoctorCommand(args) {
4712
5758
  dir = arg;
4713
5759
  }
4714
5760
  }
4715
- const projectDir = path15.resolve(dir);
4716
- if (!fs10.existsSync(projectDir)) {
5761
+ const projectDir = path18.resolve(dir);
5762
+ if (!fs12.existsSync(projectDir)) {
4717
5763
  console.error(`Error: Directory "${dir}" does not exist.`);
4718
5764
  process.exit(1);
4719
5765
  }
@@ -4774,8 +5820,8 @@ async function runCheckCommand(args) {
4774
5820
  dir = arg;
4775
5821
  }
4776
5822
  }
4777
- const projectDir = path15.resolve(dir);
4778
- if (!fs10.existsSync(projectDir)) {
5823
+ const projectDir = path18.resolve(dir);
5824
+ if (!fs12.existsSync(projectDir)) {
4779
5825
  console.error(`Error: Directory "${dir}" does not exist.`);
4780
5826
  process.exit(1);
4781
5827
  }
@@ -4816,7 +5862,7 @@ async function runCheckCommand(args) {
4816
5862
  var PACK_USAGE = "Usage: livelocale pack <outDir> [options]";
4817
5863
  function printPackSummary(result, projectDir, warnings) {
4818
5864
  for (const warning of warnings) console.log(`\u26A0\uFE0F ${warning}`);
4819
- const outRel = path15.relative(projectDir, path15.dirname(result.shareDir)).split(path15.sep).join("/") || ".";
5865
+ const outRel = path18.relative(projectDir, path18.dirname(result.shareDir)).split(path18.sep).join("/") || ".";
4820
5866
  console.log(
4821
5867
  `\u2705 LiveLocale share folder written to ${outRel}/livelocale/ (${result.manifest.locales.length} locales, ${result.manifest.codeKeys.length} code keys, ${result.htmlInjected.length} HTML files linked).`
4822
5868
  );
@@ -4828,7 +5874,7 @@ async function runPackCommand(args) {
4828
5874
  if (options.positional.length !== 1 || options.out !== void 0 || options.preview || options.command.length > 0) {
4829
5875
  throw new PackError("USAGE", PACK_USAGE);
4830
5876
  }
4831
- const projectDir = path15.resolve(options.dir);
5877
+ const projectDir = path18.resolve(options.dir);
4832
5878
  const result = await packOutput({
4833
5879
  projectDir,
4834
5880
  outDir: options.positional[0],
@@ -4851,7 +5897,7 @@ async function runBuildCli(args) {
4851
5897
  if (options.positional.length > 0 || !options.inject) {
4852
5898
  throw new PackError("USAGE", BUILD_USAGE);
4853
5899
  }
4854
- const projectDir = path15.resolve(options.dir);
5900
+ const projectDir = path18.resolve(options.dir);
4855
5901
  const result = await runBuildCommand({
4856
5902
  projectDir,
4857
5903
  command: options.command,
@@ -4874,6 +5920,55 @@ async function runBuildCli(args) {
4874
5920
  process.exitCode = 1;
4875
5921
  }
4876
5922
  }
5923
+ async function runInitShareCommand(args) {
5924
+ let dir = ".";
5925
+ let localesPath;
5926
+ let sourceLocale;
5927
+ let branches;
5928
+ let overwrite = false;
5929
+ for (let i = 1; i < args.length; i++) {
5930
+ const arg = args[i];
5931
+ if (arg === "--dir") {
5932
+ dir = args[++i] || ".";
5933
+ } else if (arg === "--locales") {
5934
+ localesPath = args[++i];
5935
+ } else if (arg === "--source") {
5936
+ sourceLocale = args[++i];
5937
+ } else if (arg === "--branches") {
5938
+ branches = (args[++i] ?? "").split(",").map((b) => b.trim()).filter(Boolean);
5939
+ } else if (arg === "--overwrite") {
5940
+ overwrite = true;
5941
+ }
5942
+ }
5943
+ const projectDir = path18.resolve(dir);
5944
+ if (!fs12.existsSync(projectDir)) {
5945
+ console.error(`Error: Directory "${dir}" does not exist.`);
5946
+ process.exit(1);
5947
+ }
5948
+ try {
5949
+ const plan = await planInitShare({
5950
+ projectDir,
5951
+ localesPath,
5952
+ sourceLocale,
5953
+ branches,
5954
+ version: readOwnVersion(),
5955
+ overwrite
5956
+ });
5957
+ writePlannedFiles(projectDir, plan.files);
5958
+ console.log(`
5959
+ \u{1F680} LiveLocale share link setup (${plan.stack})`);
5960
+ for (const file of plan.files) {
5961
+ console.log(
5962
+ ` ${file.action.padEnd(9)} ${file.path}${file.reason ? ` (${file.reason})` : ""}`
5963
+ );
5964
+ }
5965
+ for (const note of plan.notes) console.log(`\u{1F4A1} ${note}`);
5966
+ console.log("Commit and push: the workflow must be on the default branch.");
5967
+ } catch (err) {
5968
+ console.error(`\u274C ${err.message}`);
5969
+ process.exitCode = 1;
5970
+ }
5971
+ }
4877
5972
  async function run2() {
4878
5973
  const args = process.argv.slice(2);
4879
5974
  if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
@@ -4906,6 +6001,10 @@ async function run2() {
4906
6001
  await runApplyCommand(args.slice(1));
4907
6002
  return;
4908
6003
  }
6004
+ if (args[0] === "init" && args.includes("--share")) {
6005
+ await runInitShareCommand(args);
6006
+ return;
6007
+ }
4909
6008
  if (args[0] === "init") {
4910
6009
  let dir = ".";
4911
6010
  for (let i = 1; i < args.length; i++) {