@sonenta/cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ npx @sonenta/cli --help
16
16
 
17
17
  ```bash
18
18
  # 1. Authenticate (the key must carry the `mcp:*` scope — see Auth below)
19
- sonenta login --host https://api.sonenta.com --token vrb_live_<prefix>.<secret>
19
+ sonenta login --host https://api.sonenta.dev --token vrb_live_<prefix>.<secret>
20
20
 
21
21
  # 2. Bind the current directory to a project
22
22
  sonenta init --project 069fc15d-… --version main
@@ -51,9 +51,9 @@ The credentials file shape:
51
51
 
52
52
  ```json
53
53
  {
54
- "default": "https://api.sonenta.com",
54
+ "default": "https://api.sonenta.dev",
55
55
  "hosts": {
56
- "https://api.sonenta.com": { "api_key": "vrb_live_…", "user_email": "…" },
56
+ "https://api.sonenta.dev": { "api_key": "vrb_live_…", "user_email": "…" },
57
57
  "https://api.dev.sonenta.ca": { "api_key": "vrb_live_…" }
58
58
  }
59
59
  }
package/dist/index.js CHANGED
@@ -1106,6 +1106,86 @@ async function requireAuth(opts = {}) {
1106
1106
  return ctx;
1107
1107
  }
1108
1108
 
1109
+ // src/mcpserver.ts
1110
+ import { promises as fs4 } from "fs";
1111
+ import { resolve as resolve3 } from "path";
1112
+ var MCP_JSON_FILENAME = ".mcp.json";
1113
+ var MCP_SERVER_KEY = "sonenta";
1114
+ var MCP_PACKAGE = "@sonenta/mcp";
1115
+ function buildServerBlock(env, opts = {}) {
1116
+ const e = {};
1117
+ if (opts.embedKey && env.apiKey) e.SONENTA_API_KEY = env.apiKey;
1118
+ if (env.host) e.SONENTA_BASE_URL = env.host.replace(/\/+$/, "");
1119
+ if (env.projectUuid) e.SONENTA_PROJECT = env.projectUuid;
1120
+ return { command: "npx", args: ["-y", MCP_PACKAGE], env: e };
1121
+ }
1122
+ async function readMcpJson(path) {
1123
+ try {
1124
+ const raw = await fs4.readFile(path, "utf8");
1125
+ const trimmed = raw.trim();
1126
+ if (!trimmed) return { json: {}, existed: true };
1127
+ const parsed = JSON.parse(trimmed);
1128
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1129
+ throw new Error(`${MCP_JSON_FILENAME} is not a JSON object`);
1130
+ }
1131
+ return { json: parsed, existed: true };
1132
+ } catch (err) {
1133
+ if (err?.code === "ENOENT") {
1134
+ return { json: {}, existed: false };
1135
+ }
1136
+ throw err;
1137
+ }
1138
+ }
1139
+ async function wireMcpServer(env, opts = {}) {
1140
+ const baseDir = opts.baseDir ?? process.cwd();
1141
+ const path = resolve3(baseDir, MCP_JSON_FILENAME);
1142
+ const { json, existed } = await readMcpJson(path);
1143
+ const embeddedKey = Boolean(opts.embedKey && env.apiKey);
1144
+ const block = buildServerBlock(env, { embedKey: opts.embedKey });
1145
+ const servers = json.mcpServers && typeof json.mcpServers === "object" ? json.mcpServers : {};
1146
+ const prior = servers[MCP_SERVER_KEY];
1147
+ const identical = prior !== void 0 && deepEqual(prior, block);
1148
+ servers[MCP_SERVER_KEY] = block;
1149
+ json.mcpServers = servers;
1150
+ const action = !existed ? "created" : identical ? "unchanged" : "updated";
1151
+ if (action !== "unchanged") {
1152
+ await fs4.writeFile(path, JSON.stringify(json, null, 2) + "\n", "utf8");
1153
+ }
1154
+ let gitignoreUpdated = false;
1155
+ const wantGitignore = opts.gitignore ?? embeddedKey;
1156
+ if (wantGitignore) {
1157
+ gitignoreUpdated = await ensureGitignored(baseDir, MCP_JSON_FILENAME);
1158
+ }
1159
+ return { path, action, serverKey: MCP_SERVER_KEY, gitignoreUpdated, embeddedKey };
1160
+ }
1161
+ async function ensureGitignored(baseDir, entry) {
1162
+ const path = resolve3(baseDir, ".gitignore");
1163
+ let current = "";
1164
+ try {
1165
+ current = await fs4.readFile(path, "utf8");
1166
+ } catch (err) {
1167
+ if (err?.code !== "ENOENT") throw err;
1168
+ }
1169
+ const already = current.split(/\r?\n/).map((l) => l.trim()).some((l) => l === entry || l === `/${entry}`);
1170
+ if (already) return false;
1171
+ const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
1172
+ const header = current.length === 0 ? "" : "\n# Sonenta MCP server config (contains an API key)\n";
1173
+ await fs4.writeFile(path, `${current}${prefix}${header}${entry}
1174
+ `, "utf8");
1175
+ return true;
1176
+ }
1177
+ function deepEqual(a, b) {
1178
+ if (a === b) return true;
1179
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1180
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
1181
+ const ak = Object.keys(a);
1182
+ const bk = Object.keys(b);
1183
+ if (ak.length !== bk.length) return false;
1184
+ return ak.every(
1185
+ (k) => deepEqual(a[k], b[k])
1186
+ );
1187
+ }
1188
+
1109
1189
  // src/commands/agents.ts
1110
1190
  var agentsCommand = new Command("agents").description("Install bundled Claude agents (e.g. sonenta-a11y) into .claude/agents/.").addCommand(
1111
1191
  new Command("list").description("List the bundled agents available to install.").option("--dir <path>", "Project directory (default: current directory)").action(async (opts) => {
@@ -1122,20 +1202,55 @@ var agentsCommand = new Command("agents").description("Install bundled Claude ag
1122
1202
  Install with: sonenta agents add <name>`);
1123
1203
  })
1124
1204
  ).addCommand(
1125
- new Command("add").description("Write a bundled agent definition into <dir>/.claude/agents/<name>.md.").argument("<name>", "Agent name (e.g. sonenta-a11y)").option("--dir <path>", "Project directory (default: current directory)").option("--host <url>", "Override host (otherwise from config/credentials)").option("--force", "Overwrite an existing agent definition", false).action(async (name, opts) => {
1126
- await requireAuth({ hostOverride: opts.host });
1127
- const path = await writeAgent(name, { baseDir: opts.dir, force: opts.force });
1128
- console.log(`Wrote ${path}`);
1129
- console.log(
1130
- `
1131
- The ${name} agent drives the Sonenta a11y MCP tools. Make sure the Sonenta MCP server is configured (npx -y @sonenta/mcp) with an mcp:* SONENTA_API_KEY, then use the agent in Claude Code or CI.`
1132
- );
1133
- console.log(`Agent dir: ${AGENTS_DIR}/`);
1134
- })
1205
+ new Command("add").description("Write a bundled agent definition into <dir>/.claude/agents/<name>.md.").argument("<name>", "Agent name (e.g. sonenta-a11y)").option("--dir <path>", "Project directory (default: current directory)").option("--host <url>", "Override host (otherwise from config/credentials)").option("--force", "Overwrite an existing agent definition", false).option("--no-mcp", "Skip auto-wiring the @sonenta/mcp server into .mcp.json").option(
1206
+ "--embed-key",
1207
+ "Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
1208
+ false
1209
+ ).action(
1210
+ async (name, opts) => {
1211
+ const ctx = await requireAuth({ hostOverride: opts.host });
1212
+ const path = await writeAgent(name, { baseDir: opts.dir, force: opts.force });
1213
+ console.log(`Wrote ${path}`);
1214
+ if (opts.mcp) {
1215
+ const wired = await wireMcpServer(
1216
+ { apiKey: ctx.apiKey, host: ctx.host, projectUuid: ctx.projectUuid },
1217
+ { baseDir: opts.dir, embedKey: opts.embedKey }
1218
+ );
1219
+ const verb = wired.action === "created" ? "Created" : wired.action === "updated" ? "Updated" : "Verified";
1220
+ console.log(
1221
+ `${verb} ${MCP_JSON_FILENAME} \u2192 connected the "${wired.serverKey}" server (${"npx -y @sonenta/mcp"}, host ${ctx.host}${ctx.projectUuid ? `, project ${ctx.projectUuid}` : ""}).`
1222
+ );
1223
+ if (wired.embeddedKey) {
1224
+ console.log(
1225
+ `Embedded your API key in ${MCP_JSON_FILENAME}` + (wired.gitignoreUpdated ? " and added it to .gitignore" : "") + " \u2014 keep it out of git."
1226
+ );
1227
+ } else {
1228
+ console.log(
1229
+ `No secret stored in ${MCP_JSON_FILENAME} \u2014 the server reads your API key from ~/.sonenta at startup (run \`sonenta login\` if it can't). Safe to commit.`
1230
+ );
1231
+ }
1232
+ if (!ctx.projectUuid) {
1233
+ console.log(
1234
+ "Note: no project bound \u2014 run `sonenta init --project <uuid>` so the agent's tools default to one project (or pass project_uuid per call)."
1235
+ );
1236
+ }
1237
+ console.log(
1238
+ `
1239
+ \u27F3 Reload your Claude Code session (or restart the MCP client) so the "${wired.serverKey}" server connects \u2014 then ${name}'s tools are available.`
1240
+ );
1241
+ } else {
1242
+ console.log(
1243
+ `
1244
+ Skipped .mcp.json (--no-mcp). The ${name} agent needs the Sonenta MCP server (npx -y @sonenta/mcp) with an mcp:* SONENTA_API_KEY to have any tools.`
1245
+ );
1246
+ }
1247
+ console.log(`Agent dir: ${AGENTS_DIR}/`);
1248
+ }
1249
+ )
1135
1250
  );
1136
1251
 
1137
1252
  // src/commands/export.ts
1138
- import { promises as fs4 } from "fs";
1253
+ import { promises as fs5 } from "fs";
1139
1254
  import { join as join2 } from "path";
1140
1255
  import { Command as Command2 } from "commander";
1141
1256
 
@@ -1275,9 +1390,9 @@ var exportCommand = new Command2("export").description(
1275
1390
  for (const [lang, nss] of Object.entries(collected)) {
1276
1391
  for (const [ns, flat] of Object.entries(nss)) {
1277
1392
  const dir = join2(opts.out, lang);
1278
- await fs4.mkdir(dir, { recursive: true });
1393
+ await fs5.mkdir(dir, { recursive: true });
1279
1394
  const p = join2(dir, `${ns}.json`);
1280
- await fs4.writeFile(p, JSON.stringify(shape(flat), null, 2) + "\n", "utf8");
1395
+ await fs5.writeFile(p, JSON.stringify(shape(flat), null, 2) + "\n", "utf8");
1281
1396
  console.log(` ${p} ${Object.keys(flat).length} keys`);
1282
1397
  files++;
1283
1398
  }
@@ -1295,7 +1410,7 @@ var exportCommand = new Command2("export").description(
1295
1410
  );
1296
1411
 
1297
1412
  // src/commands/import.ts
1298
- import { promises as fs5 } from "fs";
1413
+ import { promises as fs6 } from "fs";
1299
1414
  import { basename, dirname as dirname3 } from "path";
1300
1415
  import { Command as Command3 } from "commander";
1301
1416
  function resolveLangNs(filePath, optLang, optNs) {
@@ -1313,7 +1428,7 @@ function resolveLangNs(filePath, optLang, optNs) {
1313
1428
  return { lang, ns };
1314
1429
  }
1315
1430
  async function readTree(filePath) {
1316
- const parsed = JSON.parse(await fs5.readFile(filePath, "utf8"));
1431
+ const parsed = JSON.parse(await fs6.readFile(filePath, "utf8"));
1317
1432
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1318
1433
  throw new Error(`${filePath} is not a JSON object`);
1319
1434
  }
@@ -1370,13 +1485,13 @@ var importCommand = new Command3("import").description(
1370
1485
 
1371
1486
  // src/commands/init.ts
1372
1487
  import { existsSync } from "fs";
1373
- import { resolve as resolve4 } from "path";
1488
+ import { resolve as resolve5 } from "path";
1374
1489
  import { Command as Command4 } from "commander";
1375
1490
 
1376
1491
  // src/repodoc.ts
1377
- import { promises as fs6 } from "fs";
1378
- import { resolve as resolve3 } from "path";
1379
- var DOC_API_HOST = "https://api.sonenta.com";
1492
+ import { promises as fs7 } from "fs";
1493
+ import { resolve as resolve4 } from "path";
1494
+ var DOC_API_HOST = "https://api.sonenta.dev";
1380
1495
  var DOC_CDN_HOST = "https://cdn.sonenta.com";
1381
1496
  var REPO_DOC_FILES = ["CLAUDE.md", "AGENTS.md"];
1382
1497
  var BLOCK_BEGIN = "<!-- SONENTA:BEGIN \u2014 managed by `sonenta init`; edits between these markers are overwritten -->";
@@ -1464,13 +1579,13 @@ function renderManagedBlock(d) {
1464
1579
  async function upsertManagedBlock(filePath, block) {
1465
1580
  let existing = null;
1466
1581
  try {
1467
- existing = await fs6.readFile(filePath, "utf8");
1582
+ existing = await fs7.readFile(filePath, "utf8");
1468
1583
  } catch {
1469
1584
  existing = null;
1470
1585
  }
1471
1586
  const normalizedBlock = block.endsWith("\n") ? block : block + "\n";
1472
1587
  if (existing === null) {
1473
- await fs6.writeFile(filePath, normalizedBlock, "utf8");
1588
+ await fs7.writeFile(filePath, normalizedBlock, "utf8");
1474
1589
  return "created";
1475
1590
  }
1476
1591
  const begin = existing.indexOf(BLOCK_BEGIN);
@@ -1479,18 +1594,18 @@ async function upsertManagedBlock(filePath, block) {
1479
1594
  const before = existing.slice(0, begin);
1480
1595
  const after = existing.slice(end + BLOCK_END.length);
1481
1596
  const blockCore = block.slice(0, block.indexOf(BLOCK_END) + BLOCK_END.length);
1482
- await fs6.writeFile(filePath, before + blockCore + after, "utf8");
1597
+ await fs7.writeFile(filePath, before + blockCore + after, "utf8");
1483
1598
  return "updated";
1484
1599
  }
1485
1600
  const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
1486
- await fs6.writeFile(filePath, existing + sep + normalizedBlock, "utf8");
1601
+ await fs7.writeFile(filePath, existing + sep + normalizedBlock, "utf8");
1487
1602
  return "inserted";
1488
1603
  }
1489
1604
  async function writeRepoDocs(dir, data) {
1490
1605
  const block = renderManagedBlock(data);
1491
1606
  const results = [];
1492
1607
  for (const file of REPO_DOC_FILES) {
1493
- const path = resolve3(dir, file);
1608
+ const path = resolve4(dir, file);
1494
1609
  const action = await upsertManagedBlock(path, block);
1495
1610
  results.push({ file, path, action });
1496
1611
  }
@@ -1498,7 +1613,7 @@ async function writeRepoDocs(dir, data) {
1498
1613
  }
1499
1614
 
1500
1615
  // src/commands/init.ts
1501
- var DEFAULT_HOST = "https://api.sonenta.com";
1616
+ var DEFAULT_HOST = "https://api.sonenta.dev";
1502
1617
  var ACTION_LABEL = {
1503
1618
  created: "Created",
1504
1619
  updated: "Updated",
@@ -1539,9 +1654,13 @@ async function gatherRepoDocData(opts) {
1539
1654
  }
1540
1655
  var initCommand = new Command4("init").description(
1541
1656
  "Scaffold sonenta.config.json AND write a managed Sonenta block into CLAUDE.md / AGENTS.md so coding agents know how this repo uses Sonenta."
1542
- ).option("--host <url>", "API base URL", DEFAULT_HOST).option("--project <uuid>", "Project UUID").option("--version <slug>", "Version slug (default: main)", "main").option("--force", "Overwrite an existing sonenta.config.json", false).option("--no-repo-doc", "Skip writing the managed block into CLAUDE.md / AGENTS.md").action(
1657
+ ).option("--host <url>", "API base URL", DEFAULT_HOST).option("--project <uuid>", "Project UUID").option("--version <slug>", "Version slug (default: main)", "main").option("--force", "Overwrite an existing sonenta.config.json", false).option("--no-repo-doc", "Skip writing the managed block into CLAUDE.md / AGENTS.md").option("--no-mcp", "Skip auto-wiring the @sonenta/mcp server into .mcp.json").option(
1658
+ "--embed-key",
1659
+ "Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
1660
+ false
1661
+ ).action(
1543
1662
  async (opts) => {
1544
- const path = resolve4(process.cwd(), CONFIG_FILENAME);
1663
+ const path = resolve5(process.cwd(), CONFIG_FILENAME);
1545
1664
  if (existsSync(path) && !opts.force) {
1546
1665
  console.error(
1547
1666
  `${CONFIG_FILENAME} already exists at ${path}. Pass --force to overwrite.`
@@ -1568,6 +1687,50 @@ var initCommand = new Command4("init").description(
1568
1687
  } else {
1569
1688
  console.log("Skipped CLAUDE.md / AGENTS.md (--no-repo-doc).");
1570
1689
  }
1690
+ if (opts.mcp) {
1691
+ let liveHost = opts.host !== DEFAULT_HOST ? opts.host : void 0;
1692
+ if (!liveHost) {
1693
+ const creds = await readCredentials().catch(() => null);
1694
+ liveHost = creds?.default ?? void 0;
1695
+ }
1696
+ let resolved = null;
1697
+ try {
1698
+ const ctx = await resolveContext({ hostOverride: liveHost });
1699
+ resolved = { apiKey: ctx.apiKey, host: ctx.host, projectUuid: ctx.projectUuid };
1700
+ } catch {
1701
+ resolved = opts.embedKey ? null : { host: liveHost ?? opts.host, projectUuid: opts.project };
1702
+ }
1703
+ if (resolved) {
1704
+ const wired = await wireMcpServer(
1705
+ {
1706
+ apiKey: resolved.apiKey,
1707
+ host: resolved.host,
1708
+ projectUuid: resolved.projectUuid ?? opts.project
1709
+ },
1710
+ { embedKey: opts.embedKey }
1711
+ );
1712
+ const verb = wired.action === "created" ? "Created" : wired.action === "updated" ? "Updated" : "Verified";
1713
+ console.log(
1714
+ `${verb} ${MCP_JSON_FILENAME} \u2192 connected the "${wired.serverKey}" server (npx -y @sonenta/mcp${resolved.host ? `, host ${resolved.host}` : ""}).`
1715
+ );
1716
+ if (wired.embeddedKey) {
1717
+ console.log(
1718
+ `Embedded your API key in ${MCP_JSON_FILENAME}` + (wired.gitignoreUpdated ? " and added it to .gitignore" : "") + " \u2014 keep it out of git."
1719
+ );
1720
+ } else {
1721
+ console.log(
1722
+ `No secret stored in ${MCP_JSON_FILENAME} \u2014 the server reads your API key from ~/.sonenta at startup (run \`sonenta login\` if it can't). Safe to commit.`
1723
+ );
1724
+ }
1725
+ console.log(
1726
+ `\u27F3 Reload your Claude Code session so the "${wired.serverKey}" server connects.`
1727
+ );
1728
+ } else {
1729
+ console.log(
1730
+ `Note: skipped ${MCP_JSON_FILENAME} wiring (--embed-key needs a login). Run \`sonenta login\` then \`sonenta agents add <name>\`.`
1731
+ );
1732
+ }
1733
+ }
1571
1734
  if (!opts.project) {
1572
1735
  console.log(
1573
1736
  "Tip: pass --project <uuid> to bind this directory to a specific project (or edit project_uuid in the file later), then re-run `sonenta init --force`."
@@ -1596,8 +1759,8 @@ async function promptLine(message) {
1596
1759
  if (!process.stdin.isTTY) return "";
1597
1760
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1598
1761
  try {
1599
- return await new Promise((resolve6) => {
1600
- rl.question(message, (answer) => resolve6(answer.trim()));
1762
+ return await new Promise((resolve7) => {
1763
+ rl.question(message, (answer) => resolve7(answer.trim()));
1601
1764
  });
1602
1765
  } finally {
1603
1766
  rl.close();
@@ -1613,7 +1776,7 @@ async function promptSecret(message) {
1613
1776
  process.stdout.write(message);
1614
1777
  process.stdin.setRawMode(true);
1615
1778
  process.stdin.resume();
1616
- return await new Promise((resolve6) => {
1779
+ return await new Promise((resolve7) => {
1617
1780
  let buffer = "";
1618
1781
  const onData = (chunk) => {
1619
1782
  for (const byte of chunk) {
@@ -1622,7 +1785,7 @@ async function promptSecret(message) {
1622
1785
  process.stdin.removeListener("data", onData);
1623
1786
  process.stdin.setRawMode(false);
1624
1787
  process.stdin.pause();
1625
- resolve6(buffer);
1788
+ resolve7(buffer);
1626
1789
  return;
1627
1790
  }
1628
1791
  if (byte === CTRL_C) {
@@ -1651,10 +1814,10 @@ async function promptSecret(message) {
1651
1814
  var TOKEN_REGEX = /^vrb_[a-z]+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
1652
1815
  var loginCommand = new Command6("login").description(
1653
1816
  "Store an API key for a host. Token resolution order: --token, SONENTA_TOKEN env, then interactive prompt (TTY only)."
1654
- ).option("--host <url>", "API base URL", "https://api.sonenta.com").option("--token <vrb_live_\u2026>", "API key token (prefix.secret form)").option("--email <email>", "User email associated with the token (optional)").option("--default", "Set this host as the default for future commands", false).action(async (opts) => {
1817
+ ).option("--host <url>", "API base URL", "https://api.sonenta.dev").option("--token <vrb_live_\u2026>", "API key token (prefix.secret form)").option("--email <email>", "User email associated with the token (optional)").option("--default", "Set this host as the default for future commands", false).action(async (opts) => {
1655
1818
  let host = opts.host;
1656
1819
  if (!host && process.stdin.isTTY) {
1657
- host = await promptLine("Host (default https://api.sonenta.com): ") || "https://api.sonenta.com";
1820
+ host = await promptLine("Host (default https://api.sonenta.dev): ") || "https://api.sonenta.dev";
1658
1821
  }
1659
1822
  let token = opts.token ?? (process.env.SONENTA_TOKEN ?? process.env.VERBUMIA_TOKEN) ?? "";
1660
1823
  if (!token && process.stdin.isTTY) {
@@ -1773,14 +1936,14 @@ var projectsCommand = new Command9("projects").description("Inspect Verbumia pro
1773
1936
  import { Command as Command10 } from "commander";
1774
1937
 
1775
1938
  // src/locales.ts
1776
- import { promises as fs7 } from "fs";
1777
- import { join as join3, resolve as resolve5 } from "path";
1939
+ import { promises as fs8 } from "fs";
1940
+ import { join as join3, resolve as resolve6 } from "path";
1778
1941
  var DEFAULT_LOCALES_DIR = "locales";
1779
1942
  async function listLocaleFiles(rootDir) {
1780
- const root = resolve5(rootDir);
1943
+ const root = resolve6(rootDir);
1781
1944
  let langDirs;
1782
1945
  try {
1783
- langDirs = await fs7.readdir(root);
1946
+ langDirs = await fs8.readdir(root);
1784
1947
  } catch {
1785
1948
  return [];
1786
1949
  }
@@ -1789,12 +1952,12 @@ async function listLocaleFiles(rootDir) {
1789
1952
  const langPath = join3(root, lang);
1790
1953
  let stat;
1791
1954
  try {
1792
- stat = await fs7.stat(langPath);
1955
+ stat = await fs8.stat(langPath);
1793
1956
  } catch {
1794
1957
  continue;
1795
1958
  }
1796
1959
  if (!stat.isDirectory()) continue;
1797
- const files = await fs7.readdir(langPath);
1960
+ const files = await fs8.readdir(langPath);
1798
1961
  for (const f of files) {
1799
1962
  if (!f.endsWith(".json")) continue;
1800
1963
  out.push({ lang, namespace: f.replace(/\.json$/, ""), path: join3(langPath, f) });
@@ -1803,7 +1966,7 @@ async function listLocaleFiles(rootDir) {
1803
1966
  return out;
1804
1967
  }
1805
1968
  async function readLocaleFile(path) {
1806
- const raw = await fs7.readFile(path, "utf8");
1969
+ const raw = await fs8.readFile(path, "utf8");
1807
1970
  const parsed = JSON.parse(raw);
1808
1971
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1809
1972
  throw new Error(`${path} is not a flat object`);
@@ -1819,12 +1982,12 @@ async function readLocaleFile(path) {
1819
1982
  }
1820
1983
  async function writeLocaleFile(rootDir, lang, namespace, values) {
1821
1984
  const dir = join3(rootDir, lang);
1822
- await fs7.mkdir(dir, { recursive: true });
1985
+ await fs8.mkdir(dir, { recursive: true });
1823
1986
  const path = join3(dir, `${namespace}.json`);
1824
1987
  const sorted = Object.fromEntries(
1825
1988
  Object.entries(values).sort(([a], [b]) => a.localeCompare(b))
1826
1989
  );
1827
- await fs7.writeFile(path, JSON.stringify(sorted, null, 2) + "\n", "utf8");
1990
+ await fs8.writeFile(path, JSON.stringify(sorted, null, 2) + "\n", "utf8");
1828
1991
  return path;
1829
1992
  }
1830
1993
  function diffFlat(local, remote) {
@@ -1963,7 +2126,7 @@ var releasesCommand = new Command12("releases").description("Manage CDN releases
1963
2126
  );
1964
2127
 
1965
2128
  // src/commands/snapshot.ts
1966
- import { promises as fs8 } from "fs";
2129
+ import { promises as fs9 } from "fs";
1967
2130
  import { Command as Command13 } from "commander";
1968
2131
  function bundleUrl(cdnBase, project, version, lang, ns) {
1969
2132
  return `${cdnBase.replace(/\/+$/, "")}/p/${project}/${version}/latest/${lang}/${ns}.json`;
@@ -2030,7 +2193,7 @@ var snapshotCommand = new Command13("snapshot").description(
2030
2193
  opts.format === "json" ? "json" : "ts"
2031
2194
  );
2032
2195
  if (opts.out) {
2033
- await fs8.writeFile(opts.out, output, "utf8");
2196
+ await fs9.writeFile(opts.out, output, "utf8");
2034
2197
  console.log(
2035
2198
  `wrote ${opts.out}: ${fetched} bundle(s)` + (missing ? `, ${missing} not published (404)` : "")
2036
2199
  );