summrise-agent 1.2.470 → 1.2.472

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/bin/summrise.js CHANGED
@@ -206,6 +206,34 @@ function apiPost(pathname, body) {
206
206
  throw new Error("gateway bad response: " + out.slice(0, 120));
207
207
  }
208
208
  }
209
+ // ── `sh()` IS `cmd.exe /d /s /c "<string>"`, AND DOUBLE QUOTES DO NOT STOP `%` ──
210
+ //
211
+ // Quoting a value stops cmd from reading `&`, `|`, `>` and `^` as OPERATORS (that is
212
+ // the `psArgv` incident below). It does NOT stop PERCENT EXPANSION: cmd expands
213
+ // `%NAME%` inside a quoted region exactly as it does outside one, so an interpolated
214
+ // PATH that contains a `%` is still a variable reference rather than data. `%` is
215
+ // legal in an NTFS name, and the install/data dirs here are chosen by the operator,
216
+ // so this is reachable rather than theoretical.
217
+ //
218
+ // MEASURED on d1 (2026-09-25) through this exact `spawnSync(cmd, {shell:true})` path;
219
+ // each arrow is the child's own stdout:
220
+ // `echo [%CMDCMDLINE%]` -> [C:\WINDOWS\system32\cmd.exe /d /s /c "echo [%CMDCMDLINE%]"]
221
+ // `echo ["C:\%ProgramFiles%\Summrise"]` -> ["C:\C:\Program Files\Summrise"] (rewritten)
222
+ // shell `powershell ... "Write-Output '%ProgramFiles%'"` -> C:\Program Files (value lost)
223
+ // argv `powershell ... Write-Output '%ProgramFiles%'` -> %ProgramFiles% (verbatim)
224
+ //
225
+ // AND THE BATCH-FILE ESCAPE IS THE WRONG FIX: cmd's `%%` -> `%` rule belongs to batch
226
+ // FILES. On a `/d /s /c` command line that same measurement prints `[100%%]` for
227
+ // `echo [100%%]` — the doubling is NOT collapsed, so "escaping" a value with `%%`
228
+ // would put a DOUBLED sign into the path, which is worse than the defect. What
229
+ // remains is argv (no cmd in the path at all — `ps()` below hands PowerShell its
230
+ // script directly) or a value that provably cannot contain `%`. A site that keeps a
231
+ // cmd line anyway says which of those it is in a `cmd-% <kind>: <reason>` comment,
232
+ // and test/cli.test.mjs fails any interpolating site that does not.
233
+ //
234
+ // `rmdir`, `taskkill`, `sc`, `schtasks` and `reg` cannot take argv (the first is a
235
+ // cmd BUILTIN — there is no rmdir.exe), so a path handed to one of those must reach
236
+ // cmd as a `%NAME%` reference rather than as text.
209
237
  function sh(cmd, opts = {}) {
210
238
  return (0, child_process_1.spawnSync)(cmd, { shell: true, stdio: "inherit", ...opts });
211
239
  }
@@ -280,8 +308,11 @@ exports.psq = psq;
280
308
  // Best-effort + logged, never fatal. `sink` is a PS output pipe
281
309
  // (e.g. Write-Host, or the update log pipe). Single-quoted PS literals
282
310
  // only (npm audit #6) except the double quotes the .lnk Arguments path
283
- // needs — callers passing through -Command "..." must backslash-escape
284
- // them (see setup step 7); the update swap script runs from a file.
311
+ // needs — those are LITERAL characters, because they sit inside
312
+ // single-quoted PS strings, so NO caller escapes them: `setup` hands this
313
+ // script to `ps()` as argv and the update swap runs it from a file. The
314
+ // backslash-escape `setup` used to apply was deleted with that conversion
315
+ // (see step 7), which is why this line no longer says "escape them".
285
316
  // exported: unit-tested in test/cli.test.mjs.
286
317
  function deskShortcutRepairPs(scriptsQ, deskDirQ, sink) {
287
318
  return [
@@ -1055,7 +1086,9 @@ function ensureElectron() {
1055
1086
  if (!zip)
1056
1087
  return null;
1057
1088
  fs.mkdirSync(dist, { recursive: true });
1058
- sh(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${(0, exports.psq)(zip)}' -DestinationPath '${(0, exports.psq)(dist)}'"`);
1089
+ // argv via `ps()`, not a cmd string: BOTH values are paths, and cmd expands `%NAME%`
1090
+ // even inside the double quotes the old form carried (see `sh()`).
1091
+ ps(`Expand-Archive -Force -Path '${(0, exports.psq)(zip)}' -DestinationPath '${(0, exports.psq)(dist)}'`);
1059
1092
  if (!fs.existsSync(path.join(dist, "electron.exe")))
1060
1093
  return null;
1061
1094
  try {
@@ -1736,15 +1769,20 @@ const commands = {
1736
1769
  sh("reg delete HKLM\\SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\Cloudflared /f >NUL 2>&1");
1737
1770
  // 4. Stale update-busy marker (a crashed update would lock updates).
1738
1771
  const BUSY = path.join(process.env.ProgramData || "C:\\ProgramData", "SummriseAgent", "update-busy");
1739
- sh(`powershell -NoProfile -Command "Remove-Item -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(BUSY)}'"`);
1772
+ // `ps()` (argv) rather than `sh()`: BUSY is a PATH under %ProgramData%, and cmd
1773
+ // expands `%NAME%` in a value even when the value is quoted (see `sh()`).
1774
+ ps(`Remove-Item -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(BUSY)}'`);
1740
1775
  // 5. Refresh the BOXED playwright bundle: delete the old tree first so a
1741
1776
  // removed package/version never leaves stale files behind.
1742
1777
  // round-163: kill the runner/bridge node processes FIRST — a running
1743
1778
  // node.exe holds its image file locked, the Remove-Item/Expand-Archive
1744
1779
  // pair silently skipped it, and the device was left with a playwright
1745
1780
  // dir WITHOUT node.exe (bridge could never spawn again; observed d1).
1746
- sh(`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -like '*${(0, exports.psq)(DIR)}*playwright*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`);
1747
- sh(`powershell -NoProfile -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(PW_DIR)}'"`);
1781
+ // argv (`ps()`), because DIR is the install dir: an operator-chosen PATH, so cmd
1782
+ // could find a `%NAME%` in it — and this script's `|` and `{ }` are only data
1783
+ // because no shell parses the line at all.
1784
+ ps(`Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -like '*${(0, exports.psq)(DIR)}*playwright*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`);
1785
+ ps(`Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(PW_DIR)}'`);
1748
1786
  // 6. Legacy install dirs from retired installers (C:\summrise-agent /
1749
1787
  // D:\summrise-agent). If the registry now points at a DIFFERENT dir and a
1750
1788
  // legacy dir exists, it is a residue of the old channel — remove it
@@ -1753,15 +1791,45 @@ const commands = {
1753
1791
  for (const legacy of ["C:\\summrise-agent", "D:\\summrise-agent"]) {
1754
1792
  if (legacy !== DIR && fs.existsSync(legacy)) {
1755
1793
  console.log("setup: removing legacy install dir", legacy);
1794
+ // cmd-% literal: `legacy` is one of the two STRING LITERALS in the array on the
1795
+ // `for` line above, so this value cannot carry a `%` — reviewed, left on the cmd
1796
+ // line (the pin re-reads that array and checks it is literals all the way down).
1756
1797
  sh(`powershell -NoProfile -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(legacy)}'"`);
1757
1798
  }
1758
1799
  }
1759
1800
  // 7. stage-brand: heal a stale desktop shortcut (a 2026-09-01 Summrise.lnk
1760
1801
  // launches the RETIRED Tauri exe with the old embedded icon) + drop
1761
1802
  // the retired orphans. Repair-only (helper checks link existence).
1762
- // Backslash-escape the .lnk Arguments double quotes for -Command.
1763
1803
  console.log("setup: reconciling desktop shortcut (retired-exe repair)...");
1764
- sh(`powershell -NoProfile -Command "${deskShortcutRepairPs((0, exports.psq)(SCRIPTS_DIR), (0, exports.psq)(DESK_DIR), "Write-Host").join("; ").replace(/"/g, '\\"')}"`);
1804
+ // argv (`ps()`), because BOTH values are paths (SCRIPTS_DIR, DESK_DIR): cmd expands
1805
+ // `%NAME%` inside a quoted region too (see `sh()`), so a `%` in the operator-chosen
1806
+ // install dir would arrive at PowerShell as a variable REFERENCE rather than as data.
1807
+ // This was the LAST acknowledged residual marker in this file, and the pin's cap is
1808
+ // ZERO now — a site that keeps a cmd line has to prove its value cannot carry a `%`.
1809
+ //
1810
+ // A PATH WITH NO `%` OR `"` IS UNAFFECTED: the script PowerShell ends up parsing is the
1811
+ // same text, now handed over directly instead of through cmd. A `"` IN ONE OF THE PATHS
1812
+ // CHANGES SHAPE, and that is the point: cmd reads `"` as a quote TOGGLE, so under the old
1813
+ // form it moved the quoted region (and the `.replace()` below rewrote the whole script
1814
+ // around it); as argv it is data like any other character. (`"` is not a legal character
1815
+ // in an NTFS name, so that one is the CLASS being closed rather than a reachable path — a
1816
+ // `%` in a directory name is legal, and was the reachable member.)
1817
+ //
1818
+ // AND THE `.replace(/"/g, '\\"')` THAT USED TO BE ON THIS LINE IS DELETED AS DEAD, not
1819
+ // merely redundant — it protected nothing and it rewrote the PAYLOAD. `\"` is not a cmd
1820
+ // escape (cmd's is `^`), so under cmd's own rules it did not stop the quoted region
1821
+ // ending at the first `"`; and the `.replace()` ran over the WHOLE joined script, so it
1822
+ // also hit the two double quotes that live INSIDE `deskShortcutRepairPs`'s single-quoted
1823
+ // PowerShell strings — the `.lnk` Arguments value. MEASURED against the builder: exactly
1824
+ // those two characters changed, so the shortcut would have been handed
1825
+ // `-File \"<path>\"` (a literal backslash PowerShell does not strip) had the escape
1826
+ // survived the trip. WHAT LET IT SURVIVE IS REASONED, NOT MEASURED HERE (there is no
1827
+ // cmd.exe on this box): node's CreateProcess quoting, cmd's `/s /c` verbatim tail and
1828
+ // powershell.exe's own `\"` unescaping unwound each other — exactly the "works by
1829
+ // accident" the deleted marker said about this line. With no shell in the path there is
1830
+ // no re-parsing layer left to protect against: `ps()` hands PowerShell the script
1831
+ // VERBATIM, the same way the update swap's `-File` path does.
1832
+ ps(deskShortcutRepairPs((0, exports.psq)(SCRIPTS_DIR), (0, exports.psq)(DESK_DIR), "Write-Host").join("; "));
1765
1833
  // C1: write the registry single source of truth (InstallDir; DataDir
1766
1834
  // defaults to %ProgramData%\Summrise). Everything else reads it back.
1767
1835
  // `regOk` is collected here and summarised at the end of setup: best-effort, but the
@@ -1825,13 +1893,15 @@ const commands = {
1825
1893
  if (PW_ZIP) {
1826
1894
  const pwDir = PW_DIR;
1827
1895
  fs.mkdirSync(pwDir, { recursive: true });
1828
- sh(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'"`);
1896
+ // argv (`ps()`): PW_ZIP and COMPONENTS_DIR are paths, and cmd expands `%NAME%`
1897
+ // inside quotes (see `sh()`).
1898
+ ps(`Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'`);
1829
1899
  // round-163: the whole point of the bundle is node.exe — VERIFY it
1830
1900
  // landed (a silently-missing copy killed the bridge forever on d1).
1831
1901
  // One retry, then fail loudly: a half-staged bundle is worse than none.
1832
1902
  if (!fs.existsSync(path.join(pwDir, "node.exe"))) {
1833
1903
  console.log("setup: node.exe missing after expand -- retrying once");
1834
- sh(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'"`);
1904
+ ps(`Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'`);
1835
1905
  }
1836
1906
  // "node_modules verified" was in the message while only node.exe was checked —
1837
1907
  // and node_modules is where the entry point the agent actually runs lives, so a
@@ -1932,16 +2002,38 @@ const commands = {
1932
2002
  // filter. `pre` is a PATH (npm's global prefix), so it is DOUBLE-QUOTED for the
1933
2003
  // cmd layer: unquoted, a prefix like `C:\Program Files\nodejs` was split at its
1934
2004
  // space and npm installed the CLI somewhere else while reporting success.
2005
+ //
2006
+ // QUOTING IS NOT ENOUGH FOR `pre`, because cmd expands `%NAME%` inside quotes —
2007
+ // so the path does not appear in this command line at all. `%SUMMRISE_NPM_PREFIX%`
2008
+ // does, and the value travels in the environment below. MEASURED on d1, through
2009
+ // npm.cmd ITSELF (the target here is a `.cmd` BATCH shim, and a batch file re-parses
2010
+ // its own arguments, so the /c line was not the only parser to clear):
2011
+ // `npm config get prefix --prefix "%SUMMRISE_PROBE2%"` -> C:\%ProgramFiles%\Summrise
2012
+ // `npm config get prefix --prefix "C:\%ProgramFiles%\Summrise"` -> C:\C:\Program Files\Summrise
2013
+ // The reference is expanded ONCE and what it produced is not re-scanned, so a prefix
2014
+ // literally named `C:\%ProgramFiles%\Summrise` arrives as exactly that, while the raw
2015
+ // interpolation of the same value does not; the quotes still hold an `&` inside one
2016
+ // argument. The pin requires every `%NAME%` on a cmd line to be defined in the same
2017
+ // call's `env`.
2018
+ //
2019
+ // cmd-% version: `${selfVer}` is package.json's own version — a semver, read from
2020
+ // the file the tests read too, so it cannot carry a `%` (and it must be quoted for
2021
+ // the cmd layer regardless, or npm reads an empty spec).
1935
2022
  const inst = (0, child_process_1.spawnSync)("npm", [
1936
2023
  "i",
1937
2024
  "-g",
1938
2025
  `"summrise-agent@${selfVer}"`,
1939
2026
  "--prefix",
1940
- `"${pre}"`,
2027
+ `"%SUMMRISE_NPM_PREFIX%"`,
1941
2028
  "--registry=https://registry.npmjs.org/",
1942
2029
  "--no-audit",
1943
2030
  "--no-fund",
1944
- ], { encoding: "utf8", shell: true, timeout: 300000 });
2031
+ ], {
2032
+ encoding: "utf8",
2033
+ shell: true,
2034
+ timeout: 300000,
2035
+ env: { ...process.env, SUMMRISE_NPM_PREFIX: pre },
2036
+ });
1945
2037
  console.log(inst.status === 0
1946
2038
  ? `setup: ${selfVer} installed globally -- \`summrise status\` works from any shell`
1947
2039
  : `setup: WARNING -- could not install the CLI globally; run: npm i -g summrise-agent@${selfVer}`);
@@ -1967,13 +2059,16 @@ const commands = {
1967
2059
  // regardless". Worse, the `catch` below could not fire for this — `sh()` RETURNS, it does not
1968
2060
  // throw — so the only thing it ever caught was `writeFileSync`.
1969
2061
  //
1970
- // ONE CALL, and its status is READ. DOUBLE quotes in the command: a cmd-layer argument, because the
1971
- // single-quoted version this shipped with reached PowerShell with the quotes included and died on
1972
- // the device with "unsupported path format" for a path that was perfectly valid.
2062
+ // ONE CALL, and its status is READ. The path is an ARGV ELEMENT now, and that is the fix for
2063
+ // both quoting attempts that came before it: the single-quoted version this shipped with
2064
+ // reached PowerShell with the quotes INCLUDED and died on the device with "unsupported path
2065
+ // format" for a path that was perfectly valid, and the double-quoted version that replaced it
2066
+ // still handed a PATH to cmd — where `%NAME%` expands inside the quotes (see `sh()`). argv
2067
+ // needs no quotes at all: PowerShell receives the path verbatim.
1973
2068
  //
1974
2069
  // A WARNING, NOT FATAL, unlike the agent task: headless installs have no SummriseDesktop, and the
1975
2070
  // registration script itself exits quietly when electron is already alive.
1976
- const reg302 = sh(`powershell -NoProfile -ExecutionPolicy Bypass -File "${reg}"`, { stdio: "pipe" });
2071
+ const reg302 = (0, child_process_1.spawnSync)("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", reg], { stdio: "pipe" });
1977
2072
  if (reg302 && reg302.status === 0) {
1978
2073
  console.log("setup: SummriseDesktop registered (logon + a 5-minute watchdog) and started");
1979
2074
  }
@@ -2224,11 +2319,14 @@ const commands = {
2224
2319
  // The register script is WRITTEN here (a machine that never ran setup still gets a working
2225
2320
  // task, from the same builder setup uses) but NOT run: desktop-start.ps1 runs it only when
2226
2321
  // the task is genuinely missing.
2227
- // ONE STRING, NOT an args array. With `shell: true` Node concatenates the arguments and warns
2228
- // about it (DEP0190: "the arguments are not escaped, only concatenated") — a warning the
2229
- // operator saw on the device the first time this command worked. The path is double-quoted,
2230
- // which is what -File accepts at the cmd layer; nothing here needs an argument list.
2231
- const r = (0, child_process_1.spawnSync)(`powershell -NoProfile -ExecutionPolicy Bypass -File "${start}"`, { encoding: "utf8", shell: true });
2322
+ // ARGV, NO SHELL — the path is the reason. `shell: true` JOINS an args array into one cmd.exe
2323
+ // line and warns about it (DEP0190: "the arguments are not escaped, only concatenated" — the
2324
+ // warning the operator saw on the device the first time this command worked), and that line is
2325
+ // then re-parsed a second time: `%NAME%` expands even inside the double quotes the old
2326
+ // one-string form carried (see `sh()`), and this path lives under the install dir, which the
2327
+ // operator chooses. Passed as argv, the path reaches PowerShell VERBATIM as one element and
2328
+ // DEP0190 goes with the shell instead of being worked around.
2329
+ const r = (0, child_process_1.spawnSync)("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", start], { encoding: "utf8" });
2232
2330
  const out = String(r.stdout || "").trim();
2233
2331
  if (out.endsWith("already-running")) {
2234
2332
  console.log("desktop: the Summrise shell is ALREADY RUNNING (look for the window, or the tray icon)");
@@ -3123,8 +3221,15 @@ const commands = {
3123
3221
  sh("sc stop Cloudflared 2>NUL");
3124
3222
  sh("sc delete Cloudflared 2>NUL");
3125
3223
  sh("reg delete HKLM\\SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\Cloudflared /f 2>NUL");
3126
- // program dir + registry
3127
- sh(`rmdir /s /q "${DIR}"`);
3224
+ // program dir + registry.
3225
+ //
3226
+ // NOT `rmdir /s /q "<DIR>"`: `rmdir` is a cmd BUILTIN — there is no rmdir.exe to hand argv
3227
+ // to — so a PATH given to it is text that cmd re-parses, and `%NAME%` expands even inside
3228
+ // those quotes (see `sh()`). DIR is the install dir, i.e. operator-chosen. `ps()` runs the
3229
+ // same deletion (Remove-Item -Recurse -Force, the form the legacy loop below already uses
3230
+ // because it also copes with locked/read-only files) with the path as argv. The VERIFY below
3231
+ // is what decides, which is why silence on failure is acceptable here.
3232
+ ps(`Remove-Item -LiteralPath '${(0, exports.psq)(DIR)}' -Recurse -Force -ErrorAction SilentlyContinue`);
3128
3233
  // Legacy install dirs from retired installers (C:\summrise-agent /
3129
3234
  // D:\summrise-agent) — uninstall must leave NO residue anywhere.
3130
3235
  for (const legacy of ["C:\\summrise-agent", "D:\\summrise-agent"]) {
@@ -3132,6 +3237,9 @@ const commands = {
3132
3237
  console.log("uninstall: removing legacy install dir", legacy);
3133
3238
  // PowerShell Remove-Item -Recurse -Force handles locked/read-only
3134
3239
  // files better than rmdir; retry once after a short wait.
3240
+ //
3241
+ // cmd-% literal: `legacy` is one of the two STRING LITERALS in the array on the `for`
3242
+ // line above, so this value cannot carry a `%` — the pin re-reads that array.
3135
3243
  sh(`powershell -NoProfile -Command "Remove-Item -LiteralPath '${(0, exports.psq)(legacy)}' -Recurse -Force -ErrorAction SilentlyContinue; Start-Sleep -Milliseconds 500; Remove-Item -LiteralPath '${(0, exports.psq)(legacy)}' -Recurse -Force -ErrorAction SilentlyContinue"`);
3136
3244
  if (fs.existsSync(legacy)) {
3137
3245
  console.log("uninstall: WARNING -- legacy dir still present:", legacy);
@@ -3167,8 +3275,10 @@ const commands = {
3167
3275
  console.log("uninstall: program dir + registry removed");
3168
3276
  }
3169
3277
  if (purge) {
3170
- sh(`rmdir /s /q "${DATA}"`);
3171
- // A failed rmdir used to print "data dir purged" anyway.
3278
+ // Same as the program dir above: `rmdir` is a builtin, so its path would be text cmd
3279
+ // re-parses (`%NAME%` expands inside quotes), and DATA is a path the operator can remap.
3280
+ ps(`Remove-Item -LiteralPath '${(0, exports.psq)(DATA)}' -Recurse -Force -ErrorAction SilentlyContinue`);
3281
+ // A failed removal used to print "data dir purged" anyway.
3172
3282
  if (fs.existsSync(DATA)) {
3173
3283
  console.error(`uninstall: FAILED to purge the data dir -- ${DATA} is still present`);
3174
3284
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summrise-agent",
3
- "version": "1.2.470",
3
+ "version": "1.2.472",
4
4
  "description": "Summrise Agent — Windows device MCP server (terminal/SSH/serial) + panel. Self-hosted npm distribution.",
5
5
  "//build": "唯一构建路径:在本目录执行 npm run build。它是自包含的:经 npm exec 按需拉取 typescript@5,无需预装任何 node_modules(干净检出可跑;旧脚本引用 ../summrise-desktop-electron/node_modules/.bin/tsc,干净检出必 ENOENT)。必须用 npm exec --package … -- tsc 的形式:npx 会吃掉传给 tsc 的 -p/--outDir 等参数(round-435:npx 写法从未真正跑通过)。先编 electron TS(--noCheck 纯 emit,与 release.yml 一致)同步两处 src/*.js,再编 src/summrise.ts 到 bin/summrise.js 并校验 round-298 标记(弱 oracle,只证某次构建发生,新鲜性由 publish-release.sh 的 tsc 重编 + cmp 门保证)。",
6
6
  "bin": {
Binary file