@krtclcdy/tersio 2.25.7 → 2.25.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +17 -10
  2. package/dashboard/dist/index.html +1 -1
  3. package/dist/cli/common.js +25 -3
  4. package/dist/cli/dashboard.js +7 -2
  5. package/dist/cli/doctor-fix.js +12 -7
  6. package/dist/cli/doctor.js +16 -3
  7. package/dist/cli/install.js +5 -5
  8. package/dist/cli/profile.js +28 -3
  9. package/dist/cli/rtk-wiring.js +13 -10
  10. package/dist/cli/usage.js +5 -0
  11. package/dist/extensions/caveman/index.js +3 -6
  12. package/dist/extensions/combo/index.js +1 -6
  13. package/dist/extensions/commands/index.js +7 -2
  14. package/dist/extensions/pstack/index.js +2 -1
  15. package/dist/extensions/rtk/filter.js +45 -4
  16. package/dist/extensions/rtk/session.js +4 -2
  17. package/dist/extensions/server.js +44 -29
  18. package/dist/extensions/shared/agent-stats.js +21 -13
  19. package/dist/extensions/shared/pricing.js +5 -4
  20. package/dist/extensions/shared/rtk-eligible.js +7 -11
  21. package/dist/extensions/shared/rtk-gain.js +2 -6
  22. package/dist/extensions/shared/sqlite.js +1 -6
  23. package/dist/extensions/shared/usage-ledger.js +97 -12
  24. package/dist/extensions/shared/utils.js +33 -0
  25. package/dist/extensions/updater/index.js +15 -7
  26. package/extensions/caveman/index.ts +3 -6
  27. package/extensions/combo/index.ts +1 -6
  28. package/extensions/commands/index.ts +7 -2
  29. package/extensions/pstack/index.ts +2 -1
  30. package/extensions/pstack/playbooks/autopilot-full.md +1 -1
  31. package/extensions/pstack/playbooks/babysit.md +1 -1
  32. package/extensions/pstack/playbooks/eval.md +1 -1
  33. package/extensions/pstack/playbooks/multi-phase-plan.md +2 -2
  34. package/extensions/pstack/playbooks/orchestrate.md +3 -3
  35. package/extensions/pstack/playbooks/session-pickup.md +1 -1
  36. package/extensions/pstack/playbooks/shipping.md +2 -2
  37. package/extensions/pstack/playbooks/worktree-cleanup.md +1 -1
  38. package/extensions/pstack/skills/reflect/SKILL.md +2 -2
  39. package/extensions/pstack/skills/show-me-your-work/SKILL.md +2 -2
  40. package/extensions/rtk/filter.ts +51 -4
  41. package/extensions/rtk/session.ts +4 -2
  42. package/extensions/server.ts +31 -23
  43. package/extensions/shared/agent-stats.ts +21 -12
  44. package/extensions/shared/pricing.ts +7 -5
  45. package/extensions/shared/rtk-eligible.ts +7 -11
  46. package/extensions/shared/rtk-gain.ts +2 -6
  47. package/extensions/shared/sqlite.ts +1 -6
  48. package/extensions/shared/usage-ledger.ts +102 -14
  49. package/extensions/shared/utils.ts +29 -0
  50. package/extensions/updater/index.ts +18 -7
  51. package/package.json +3 -5
  52. package/dist/bench/code-out/pfull-0/debounce.js +0 -13
  53. package/dist/bench/code-out/pfull-0/deepClone.js +0 -18
  54. package/dist/bench/code-out/pfull-0/groupBy.js +0 -15
  55. package/dist/bench/code-out/pfull-1/debounce.js +0 -13
  56. package/dist/bench/code-out/pfull-1/deepClone.js +0 -17
  57. package/dist/bench/code-out/pfull-1/groupBy.js +0 -14
  58. package/dist/bench/code-out/pfull-2/debounce.js +0 -13
  59. package/dist/bench/code-out/pfull-2/deepClone.js +0 -18
  60. package/dist/bench/code-out/pfull-2/groupBy.js +0 -15
  61. package/dist/bench/code-out/pfull-3/debounce.js +0 -12
  62. package/dist/bench/code-out/pfull-3/deepClone.js +0 -17
  63. package/dist/bench/code-out/pfull-3/groupBy.js +0 -7
  64. package/dist/bench/code-out/pfull-4/debounce.js +0 -10
  65. package/dist/bench/code-out/pfull-4/deepClone.js +0 -17
  66. package/dist/bench/code-out/pfull-4/groupBy.js +0 -14
  67. package/dist/bench/code-out/plite-0/debounce.js +0 -13
  68. package/dist/bench/code-out/plite-0/deepClone.js +0 -18
  69. package/dist/bench/code-out/plite-0/groupBy.js +0 -15
  70. package/dist/bench/code-out/plite-1/debounce.js +0 -13
  71. package/dist/bench/code-out/plite-1/deepClone.js +0 -17
  72. package/dist/bench/code-out/plite-1/groupBy.js +0 -11
  73. package/dist/bench/code-out/plite-2/debounce.js +0 -12
  74. package/dist/bench/code-out/plite-2/deepClone.js +0 -17
  75. package/dist/bench/code-out/plite-2/groupBy.js +0 -14
  76. package/dist/bench/code-out/plite-3/debounce.js +0 -12
  77. package/dist/bench/code-out/plite-3/deepClone.js +0 -17
  78. package/dist/bench/code-out/plite-3/groupBy.js +0 -14
  79. package/dist/bench/code-out/plite-4/debounce.js +0 -12
  80. package/dist/bench/code-out/plite-4/deepClone.js +0 -17
  81. package/dist/bench/code-out/plite-4/groupBy.js +0 -14
  82. package/dist/bench/code-out/pultra-0/debounce.js +0 -12
  83. package/dist/bench/code-out/pultra-0/deepClone.js +0 -20
  84. package/dist/bench/code-out/pultra-0/groupBy.js +0 -14
  85. package/dist/bench/code-out/pultra-1/debounce.js +0 -10
  86. package/dist/bench/code-out/pultra-1/deepClone.js +0 -17
  87. package/dist/bench/code-out/pultra-1/groupBy.js +0 -7
  88. package/dist/bench/code-out/pultra-2/debounce.js +0 -13
  89. package/dist/bench/code-out/pultra-2/deepClone.js +0 -18
  90. package/dist/bench/code-out/pultra-2/groupBy.js +0 -15
  91. package/dist/bench/code-out/pultra-3/debounce.js +0 -13
  92. package/dist/bench/code-out/pultra-3/deepClone.js +0 -18
  93. package/dist/bench/code-out/pultra-3/groupBy.js +0 -15
  94. package/dist/bench/code-out/pultra-4/debounce.js +0 -13
  95. package/dist/bench/code-out/pultra-4/deepClone.js +0 -18
  96. package/dist/bench/code-out/pultra-4/groupBy.js +0 -15
  97. package/dist/bench/code-out/run-0/debounce.js +0 -12
  98. package/dist/bench/code-out/run-0/deepClone.js +0 -17
  99. package/dist/bench/code-out/run-0/groupBy.js +0 -14
  100. package/dist/bench/code-out/run-1/debounce.js +0 -13
  101. package/dist/bench/code-out/run-1/deepClone.js +0 -15
  102. package/dist/bench/code-out/run-1/groupBy.js +0 -13
  103. package/dist/bench/code-out/run-2/debounce.js +0 -12
  104. package/dist/bench/code-out/run-2/deepClone.js +0 -18
  105. package/dist/bench/code-out/run-2/groupBy.js +0 -13
  106. package/dist/bench/code-out/run-3/debounce.js +0 -12
  107. package/dist/bench/code-out/run-3/deepClone.js +0 -18
  108. package/dist/bench/code-out/run-3/groupBy.js +0 -14
  109. package/dist/bench/code-out/run-4/debounce.js +0 -12
  110. package/dist/bench/code-out/run-4/deepClone.js +0 -16
  111. package/dist/bench/code-out/run-4/groupBy.js +0 -13
  112. package/dist/extensions/caveman/rule-lite.md +0 -77
  113. package/dist/extensions/caveman/rule-megacave-lite.md +0 -59
  114. package/dist/extensions/caveman/rule-megacave-ultra.md +0 -59
  115. package/dist/extensions/caveman/rule-megacave.md +0 -65
  116. package/dist/extensions/caveman/rule-ultra.md +0 -63
  117. package/dist/extensions/caveman/rule.md +0 -89
  118. package/dist/extensions/pstack/rule.md +0 -141
  119. package/dist/probe-catalog.js +0 -48
  120. package/dist/probe-catalog188.js +0 -44
@@ -1,6 +1,6 @@
1
1
  // cli/common.ts — shared foundation: CLI flags, paths, exec, file/config helpers.
2
2
  import { execFile } from 'node:child_process';
3
- import { promises as fs } from 'node:fs';
3
+ import { existsSync, promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { createRequire } from 'node:module';
@@ -14,6 +14,10 @@ const OMP_AGENT_DIR = path.join(HOME, '.omp', 'agent');
14
14
  const OMP_PLUGINS_DIR = path.join(HOME, '.omp', 'plugins');
15
15
  const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
16
16
  const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
17
+ // npm and bun are `.cmd` shims on Windows, and CreateProcess spawns neither a bare name nor a shim,
18
+ // so the spawn name itself carries the extension the way `omp.cmd` does.
19
+ const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
20
+ const BUN_BIN = IS_WINDOWS ? 'bun.cmd' : 'bun';
17
21
  const PACKAGE_NAME = '@krtclcdy/tersio';
18
22
  const PACKAGE_BIN = 'tersio';
19
23
  const { version: PACKAGE_VERSION } = createRequire(import.meta.url)(path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'));
@@ -112,8 +116,26 @@ function debug(...a) {
112
116
  console.log(' [debug]', ...a);
113
117
  }
114
118
  const execFileP = promisify(execFile);
119
+ /** Windows has no real `omp`, `npm`, or `pgrep`: `.cmd` shims throw EINVAL when spawned, so a bare name wraps through cmd.exe only when a `<name>.cmd` really sits on PATH. */
120
+ export function shimCommand(cmd, args) {
121
+ if (!IS_WINDOWS)
122
+ return { cmd, args };
123
+ if (/\.(?:cmd|bat)$/i.test(cmd)) {
124
+ return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
125
+ }
126
+ if (hasCmdShim(cmd)) {
127
+ return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
128
+ }
129
+ return { cmd, args };
130
+ }
131
+ function hasCmdShim(cmd) {
132
+ if (cmd.includes('/') || cmd.includes('\\'))
133
+ return false;
134
+ return (process.env.PATH || '').split(path.delimiter).some((dir) => dir && existsSync(path.join(dir, `${cmd}.cmd`)));
135
+ }
115
136
  async function execP(cmd, args, opts = {}) {
116
- return execFileP(cmd, args, {
137
+ const shim = shimCommand(cmd, args);
138
+ return execFileP(shim.cmd, shim.args, {
117
139
  timeout: opts.timeout || 120000,
118
140
  encoding: 'utf8',
119
141
  ...opts,
@@ -316,4 +338,4 @@ function relTime(ageMs) {
316
338
  const weeks = Math.floor(days / 7);
317
339
  return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
318
340
  }
319
- export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
341
+ export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, NPM_BIN, BUN_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
@@ -457,8 +457,13 @@ function readBody(req) {
457
457
  });
458
458
  }
459
459
  function openBrowser(url) {
460
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
461
- spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
460
+ // `start` is a cmd builtin, not an executable: spawning it by name fails with ENOENT.
461
+ const [cmd, args] = process.platform === 'darwin'
462
+ ? ['open', [url]]
463
+ : process.platform === 'win32'
464
+ ? [process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'start', '', url]]
465
+ : ['xdg-open', [url]];
466
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
462
467
  }
463
468
  // Replacers throughout: session data holds `$'` (shell quoting) and `</script>`, which String.replace would mangle.
464
469
  function escapeInline(json) {
@@ -2,7 +2,7 @@ import { existsSync, promises as fs } from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, } from './common.js';
5
+ import { BUN_BIN_DIR, HOME, NPM_BIN, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, shimCommand, } from './common.js';
6
6
  import { execNetwork, sayTagged } from './interactive.js';
7
7
  import { wireRtkOmp, wireRtkOpencode, wireRtkPi, ensureRtkInConfig } from './rtk-wiring.js';
8
8
  import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, resolveHostBinary, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
@@ -14,18 +14,20 @@ export function serviceOrphanPids() {
14
14
  const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
15
15
  if (!bin)
16
16
  return [];
17
+ const shim = shimCommand(bin, ['service', 'status']);
17
18
  let status = '';
18
19
  try {
19
- status = spawnSync(bin, ['service', 'status'], { encoding: 'utf8' }).stdout.trim().toLowerCase();
20
+ status = spawnSync(shim.cmd, shim.args, { encoding: 'utf8' }).stdout.trim().toLowerCase();
20
21
  }
21
22
  catch {
22
23
  return [];
23
24
  }
24
25
  if (serviceRunning(status))
25
26
  return [];
27
+ const grepShim = shimCommand('pgrep', ['-f', 'opencode serve --service']);
26
28
  let out = '';
27
29
  try {
28
- out = spawnSync('pgrep', ['-f', 'opencode serve --service'], { encoding: 'utf8' }).stdout ?? '';
30
+ out = spawnSync(grepShim.cmd, grepShim.args, { encoding: 'utf8' }).stdout ?? '';
29
31
  }
30
32
  catch {
31
33
  return [];
@@ -38,9 +40,10 @@ export function serviceState() {
38
40
  const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
39
41
  if (!bin)
40
42
  return null;
43
+ const shim = shimCommand(bin, ['service', 'status']);
41
44
  let status = '';
42
45
  try {
43
- status = spawnSync(bin, ['service', 'status'], { encoding: 'utf8' }).stdout.trim().toLowerCase();
46
+ status = spawnSync(shim.cmd, shim.args, { encoding: 'utf8' }).stdout.trim().toLowerCase();
44
47
  }
45
48
  catch {
46
49
  return null;
@@ -49,7 +52,8 @@ export function serviceState() {
49
52
  return null;
50
53
  let live = false;
51
54
  try {
52
- const out = spawnSync('pgrep', ['-f', 'opencode serve --service'], { encoding: 'utf8' }).stdout ?? '';
55
+ const grepShim = shimCommand('pgrep', ['-f', 'opencode serve --service']);
56
+ const out = spawnSync(grepShim.cmd, grepShim.args, { encoding: 'utf8' }).stdout ?? '';
53
57
  live = out.split(/[\s,]+/).some((line) => /^\d+$/.test(line.trim()));
54
58
  }
55
59
  catch {
@@ -365,7 +369,7 @@ async function fixPonytail(pluginsDir) {
365
369
  return;
366
370
  }
367
371
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
368
- await execNetwork('Restoring bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
372
+ await execNetwork('Restoring bundled Ponytail', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
369
373
  throw new Error(`bundled ponytail restore failed: ${e.message}`);
370
374
  });
371
375
  }
@@ -405,7 +409,8 @@ async function fixOpencodeService() {
405
409
  sayTagged(' [warn] opencode not found on PATH; start it with `opencode service start`');
406
410
  return;
407
411
  }
408
- await execNetwork('Starting the OpenCode service', bin, ['service', 'start'], { timeout: 60000 }).catch((e) => {
412
+ const shim = shimCommand(bin, ['service', 'start']);
413
+ await execNetwork('Starting the OpenCode service', shim.cmd, shim.args, { timeout: 60000 }).catch((e) => {
409
414
  throw new Error(`service start failed: ${shortError(e)}`);
410
415
  });
411
416
  }
@@ -2,13 +2,13 @@
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { existsSync, readdirSync, readFileSync, statSync, promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
- import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, } from './common.js';
5
+ import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, shimCommand, } from './common.js';
6
6
  import { detectHosts, hostExtensionsDir, ompPackageDir, piPonytailPackageDir } from './hosts.js';
7
7
  import { GENERATED_TREE_FILES } from './manifest.js';
8
8
  import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
9
9
  import { usageDbPath } from '../extensions/shared/usage-store.js';
10
10
  import { pricesCachePath } from '../extensions/shared/pricing.js';
11
- import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
11
+ import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, rtkPlatformSpec, ruleBodyProblem, tersioHome } from '../extensions/shared/utils.js';
12
12
  import { sqliteScript, statsDbPath } from '../extensions/shared/agent-stats.js';
13
13
  import { OMP_SUBAGENT_MARKER } from '../extensions/shared/session-state.js';
14
14
  import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, TREE_PSTACK_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
@@ -201,6 +201,18 @@ async function runDoctor(recheck = false) {
201
201
  function unused(label, detail) {
202
202
  console.log(` — ${label}: not installed ${detail}`);
203
203
  }
204
+ // A Windows failure names its cause here: several rows below silently degrade when the
205
+ // platform has no rtk build, and a wrong home would move every one of them.
206
+ section('Platform');
207
+ const rtkSpec = rtkPlatformSpec();
208
+ check('Platform', rtkSpec !== null, `${process.platform}/${process.arch}${rtkSpec ? ` · node ${process.version} · rtk ${rtkSpec.triple}` : ''}`);
209
+ if (rtkSpec === null) {
210
+ // Windows on ARM today: rtk publishes no build, so the RTK rows read as not installed.
211
+ console.log(` rtk publishes no build for ${process.platform}/${process.arch}; every RTK row below reads as missing`);
212
+ }
213
+ // Windows resolves data through the profile, so name the resolved home and data dir.
214
+ check('Home', HOME !== '', HOME);
215
+ check('Data', true, path.join(tersioHome(), 'usage.db'));
204
216
  section('Hosts');
205
217
  for (const host of hosts) {
206
218
  // pi can hold a working tree while reading as not installed; use the tree as ground truth.
@@ -420,7 +432,8 @@ function subagentMarkerStatus(ompBin) {
420
432
  return null;
421
433
  let haystack;
422
434
  try {
423
- haystack = execFileSync(ompBin, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] })
435
+ const shim = shimCommand(ompBin, ['--version']);
436
+ haystack = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] })
424
437
  + readFileSync(ompBin, 'utf8');
425
438
  }
426
439
  catch {
@@ -3,7 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { BUN_BIN_DIR, COMBO_PRESET_MODES, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { BUN_BIN_DIR, BUN_BIN, COMBO_PRESET_MODES, HOME, IS_WINDOWS, NPM_BIN, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
7
7
  import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, withInteractiveTask, sayTagged } from './interactive.js';
8
8
  import { printWelcome } from './banner.js';
9
9
  import { checkForUpdate, runLatestUpdate } from './update.js';
@@ -80,11 +80,11 @@ async function stepPonytail(pluginsDir, options) {
80
80
  else {
81
81
  // Reinstall only when the self-plugin step did not materialize it.
82
82
  try {
83
- await execNetwork('Installing bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
83
+ await execNetwork('Installing bundled Ponytail', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
84
84
  }
85
85
  catch {
86
86
  try {
87
- await execNetwork('Installing bundled Ponytail', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
87
+ await execNetwork('Installing bundled Ponytail', BUN_BIN, ['install'], { cwd: pluginsDir, timeout: 180000 });
88
88
  }
89
89
  catch (e) {
90
90
  sayTagged(` [fail] Could not install bundled ponytail: ${e.message}`);
@@ -136,11 +136,11 @@ async function stepSelfPlugin(pluginsDir, options) {
136
136
  await fs.mkdir(pluginsDir, { recursive: true });
137
137
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
138
138
  try {
139
- await execNetwork('Installing Tersio plugin dependencies', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
139
+ await execNetwork('Installing Tersio plugin dependencies', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
140
140
  }
141
141
  catch {
142
142
  try {
143
- await execNetwork('Installing Tersio plugin dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
143
+ await execNetwork('Installing Tersio plugin dependencies', BUN_BIN, ['install'], { cwd: pluginsDir, timeout: 180000 });
144
144
  }
145
145
  catch (e) {
146
146
  sayTagged(` [fail] Could not install ${PACKAGE_NAME} into plugins dir: ${e.message}`);
@@ -62,11 +62,12 @@ async function storedProfile() {
62
62
  return storedProfileSync();
63
63
  }
64
64
  function legacyOmpLockPath() {
65
- const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
65
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
66
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
66
67
  return [
67
- path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
68
+ path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json'),
68
69
  path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
69
- ].find((p) => existsSync(p)) ?? path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json');
70
+ ].find((p) => existsSync(p)) ?? path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json');
70
71
  }
71
72
  function parseStored(raw) {
72
73
  if (!raw)
@@ -83,6 +84,29 @@ function parseStored(raw) {
83
84
  return null;
84
85
  }
85
86
  }
87
+ // Upstream Ponytail's pi extension reads its own config for the session default, so the saved default
88
+ // is mirrored there. Without it a headless pi session starts at upstream's default and never injects.
89
+ function ponytailConfigFile() {
90
+ const base = (process.env.XDG_CONFIG_HOME || '').trim() || path.join(os.homedir(), '.config');
91
+ return path.join(base, 'ponytail', 'config.json');
92
+ }
93
+ async function mirrorPonytailDefault(mode, options) {
94
+ const file = ponytailConfigFile();
95
+ if (options.dryRun) {
96
+ if (verbose && !options.quiet)
97
+ console.log(` [dry-run] would write the Ponytail default to ${file}`);
98
+ return;
99
+ }
100
+ try {
101
+ const existing = JSON.parse(readTextIfExistsSync(file) ?? '{}');
102
+ if (existing.defaultMode === mode)
103
+ return;
104
+ await fs.mkdir(path.dirname(file), { recursive: true });
105
+ await fs.writeFile(file, `${JSON.stringify({ ...existing, defaultMode: mode }, null, 2)}\n`, 'utf8');
106
+ console.log(` [write] Ponytail default in ${file}`);
107
+ }
108
+ catch { /* upstream's config is best-effort; its own default stands */ }
109
+ }
86
110
  // Persist profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
87
111
  async function writePluginSettings(profile, options) {
88
112
  const file = tersioSettingsFile();
@@ -106,6 +130,7 @@ async function writePluginSettings(profile, options) {
106
130
  await fs.mkdir(path.dirname(file), { recursive: true });
107
131
  await fs.writeFile(file, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
108
132
  console.log(` [write] Session defaults in ${file}`);
133
+ await mirrorPonytailDefault(profile.ponytailDefault, options);
109
134
  }
110
135
  function formatProfile(profile) {
111
136
  return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault} · pstack=${profile.pstackDefault}) · currency=${profile.currency}`;
@@ -3,6 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { execFile } from 'node:child_process';
6
+ import { shimCommand } from '../extensions/shared/utils.js';
6
7
  function debugWire(options, msg) {
7
8
  if (!options.quiet)
8
9
  console.log(` ${msg}`);
@@ -14,17 +15,19 @@ function shortWiringError(e) {
14
15
  return why.slice(0, 200);
15
16
  }
16
17
  function execFileP(cmd, args, timeout) {
17
- return new Promise((resolve, reject) => {
18
- execFile(cmd, args, { timeout, encoding: 'utf8' }, (err, stdout, stderr) => {
19
- if (err) {
20
- err.stderr = String(stderr || '');
21
- reject(err);
22
- }
23
- else {
24
- resolve({ stdout: String(stdout), stderr: String(stderr) });
25
- }
26
- });
18
+ const { promise, resolve, reject } = Promise.withResolvers();
19
+ // A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
20
+ const shim = shimCommand(cmd, args);
21
+ execFile(shim.cmd, shim.args, { timeout, encoding: 'utf8' }, (err, stdout, stderr) => {
22
+ if (err) {
23
+ err.stderr = String(stderr || '');
24
+ reject(err);
25
+ }
26
+ else {
27
+ resolve({ stdout: String(stdout), stderr: String(stderr) });
28
+ }
27
29
  });
30
+ return promise;
28
31
  }
29
32
  async function runWire(rtkBin, spec, options) {
30
33
  if (!options.dryRun)
package/dist/cli/usage.js CHANGED
@@ -215,6 +215,11 @@ function printReport(report) {
215
215
  ['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
216
216
  ['Cost', `${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)${costNote}`],
217
217
  ['RTK adoption', `${fmt(a.rtkCalls)}/${fmt(a.eligibleCalls)} eligible Bash calls use RTK (${a.adoptionPct.toFixed(1)}%) · ${fmt(a.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`],
218
+ // A blended number hides a quiet host, so each host reporting bash calls gets its own row.
219
+ ...Object.entries(a.byHost)
220
+ .filter(([, c]) => c.bashCalls > 0)
221
+ .sort((x, y) => y[1].eligibleCalls - x[1].eligibleCalls)
222
+ .map(([name, c]) => [`RTK — ${name}`, `${fmt(c.rtkCalls)}/${fmt(c.eligibleCalls)} eligible calls (${c.adoptionPct.toFixed(1)}%) · ${fmt(c.missedCalls)} missed · ${fmt(c.bashCalls)} bash calls`]),
218
223
  ], [false, false], 118))
219
224
  console.log(l);
220
225
  if (measuredCount > 0) {
@@ -5,12 +5,9 @@ import { dirname, join } from 'node:path';
5
5
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
6
6
  import { announceStatus, formatCavemanStatus } from '../shared/status.js';
7
7
  import { readCavemanDefault } from '../shared/plugin-settings.js';
8
- const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
9
- // `rule.md`, `rule-ultra.md`, and `rule-megacave.md` track upstream's three registers: `rule.md` is bundled with
10
- // one host-adapted line, and the other two are fetched from upstream on install and updated in-session.
11
- // `lite`, `megacave-lite`, and `megacave-ultra` are our forks of upstream's rows at 84cc3c1, so they are
12
- // bundled rather than fetched. rule-ultra.md and rule-megacave.md are never edited here: the in-session
13
- // updater rewrites them from upstream and restores the backup when the write does not match.
8
+ import { ruleDir } from '../shared/utils.js';
9
+ const CAVERN_DIR = ruleDir('caveman', dirname(fileURLToPath(import.meta.url)));
10
+ // `rule.md`, `rule-ultra.md`, and `rule-megacave.md` track upstream and are refetched; `lite`, `megacave-lite`, and `megacave-ultra` are our forks and ship bundled.
14
11
  const RULE_FILES = { lite: 'rule-lite.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave-lite.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave-ultra.md' };
15
12
  const DEFAULT_MODE = 'off';
16
13
  // A rule file is immutable for the process, so read each level once; reinstalling needs a restart.
@@ -191,12 +191,7 @@ export default function comboToggleExtension(pi) {
191
191
  if (!hasModeState(entries) && (ctx?.hasUI || OPENCODE))
192
192
  restoreConfiguredDefaults();
193
193
  else if (!hasModeState(entries)) {
194
- // A headless root session (`host -p`) has no UI to publish the bridge, and Ponytail reads only
195
- // the bridge, so it never loaded: a benchmark on omp measured Block 0 for every Ponytail level and
196
- // for the Ponytail part of every Combo. Publish the saved modes only while the bridge is clear:
197
- // once a parent or a toggle has published, the bridge carries that session's own choice and a
198
- // subagent branch must keep inheriting it. All four go together, because Caveman, RTK, and pstack
199
- // already restored their own levels and a disagreeing bridge resets them.
194
+ // A headless root session has no UI to publish the bridge, so Ponytail never loaded; publish the saved modes only while the bridge is clear, or a subagent branch stops inheriting.
200
195
  const shared = getSharedComboState();
201
196
  if (shared.caveman === 'off' && shared.rtk === 'off' && shared.ponytail === 'off' && shared.pstack === 'off') {
202
197
  setSharedComboModes(savedModes());
@@ -32,8 +32,13 @@ async function openDashboard(pi, ctx) {
32
32
  const exported = await pi.exec?.('tersio', ['dashboard', '--export', file], { cwd: ctx?.cwd });
33
33
  if (!exported || exported.code !== 0)
34
34
  throw new Error((exported?.stderr || 'export failed').trim());
35
- const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
36
- await pi.exec?.(openCmd, [file], { cwd: ctx?.cwd });
35
+ // `start` is a cmd builtin, not an executable: spawning it by name fails with ENOENT.
36
+ const [openCmd, openArgs] = process.platform === 'darwin'
37
+ ? ['open', [file]]
38
+ : process.platform === 'win32'
39
+ ? [process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'start', '', file]]
40
+ : ['xdg-open', [file]];
41
+ await pi.exec?.(openCmd, openArgs, { cwd: ctx?.cwd });
37
42
  ctx?.ui?.notify?.(`tersio dashboard: opened in your browser.`, 'info');
38
43
  }
39
44
  catch (e) {
@@ -6,7 +6,8 @@ import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue
6
6
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
7
7
  import { announceStatus } from '../shared/status.js';
8
8
  import { readPstackDefault } from '../shared/plugin-settings.js';
9
- const PSTACK_DIR = dirname(fileURLToPath(import.meta.url));
9
+ import { ruleDir } from '../shared/utils.js';
10
+ const PSTACK_DIR = ruleDir('pstack', dirname(fileURLToPath(import.meta.url)));
10
11
  const RULE_FILE = 'rule.md';
11
12
  const DEFAULT_MODE = 'off';
12
13
  // The body is immutable for the process, so read it once; reinstalling needs a restart.
@@ -5,6 +5,8 @@ import { join } from 'node:path';
5
5
  import { resolveRtkBinary } from '../shared/utils.js';
6
6
  import { readRtkDefault } from '../shared/plugin-settings.js';
7
7
  import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.js';
8
+ import { appendUsage } from '../shared/usage-ledger.js';
9
+ import { isPiHost } from '../shared/host.js';
8
10
  /** Tool name to `rtk pipe --filter` name. Exported so the OpenCode host reuses one map. */
9
11
  export const FILTER_BY_TOOL = new Map([
10
12
  ['grep', 'grep'],
@@ -14,8 +16,13 @@ export const FILTER_BY_TOOL = new Map([
14
16
  /** Below this the rtk spawn costs more than the filter saves. */
15
17
  export const MIN_FILTER_BYTES = 1200;
16
18
  const FILTER_TIMEOUT_MS = 5000;
17
- /** Absolute, so a minimal PATH in the host cannot break the spawn. */
18
- const SHELL = '/bin/sh';
19
+ // A shell that understands `<`, resolved per platform; separate flags, because Node quotes each argv element and cmd.exe rejects `"/d /s /c"`.
20
+ /** The pipe's shell, and whether the whole command needs an extra quote pair: `cmd.exe /s` strips the outer one, so a quoted binary and redirect path must be wrapped. */
21
+ export function filterShell() {
22
+ return process.platform === 'win32'
23
+ ? { shell: process.env.ComSpec || 'cmd.exe', flags: ['/d', '/s', '/c'], wrap: true }
24
+ : { shell: '/bin/sh', flags: ['-c'], wrap: false };
25
+ }
19
26
  function modeOf(value) {
20
27
  if (value === true)
21
28
  return true;
@@ -46,8 +53,10 @@ export async function filterThroughRtk(exec, request) {
46
53
  dir = mkdtempSync(join(tmpdir(), 'tersio-rtk-'));
47
54
  const file = join(dir, 'in.txt');
48
55
  writeFileSync(file, text, 'utf8');
49
- // `rtk pipe` reads stdin only and execFile has no stdin, so one redirect.
50
- const result = await exec(SHELL, ['-c', `"${bin}" pipe -f ${filter} < "${file}"`], { signal, timeout: FILTER_TIMEOUT_MS });
56
+ // `rtk pipe` reads stdin only and execFile has no stdin, so one redirect through the platform shell.
57
+ const { shell, flags, wrap } = filterShell();
58
+ const line = `"${bin}" pipe -f ${filter} < "${file}"`;
59
+ const result = await exec(shell, [...flags, wrap ? `"${line}"` : line], { signal, timeout: FILTER_TIMEOUT_MS });
51
60
  if (result.code !== 0)
52
61
  return null;
53
62
  const out = result.stdout;
@@ -63,11 +72,43 @@ export async function filterThroughRtk(exec, request) {
63
72
  rmSync(dir, { recursive: true, force: true });
64
73
  }
65
74
  }
75
+ /** Ask rtk for the rewritten command; null on any failure, so the original runs. */
76
+ async function rewriteCommand(exec, bin, command, signal) {
77
+ try {
78
+ const result = await exec(bin, ['rewrite', command], { timeout: FILTER_TIMEOUT_MS, signal });
79
+ const out = result.stdout.trim();
80
+ return out || null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
66
86
  export default function rtkFilterExtension(pi) {
67
87
  const bin = resolveRtkBinary();
68
88
  const exec = pi.exec;
69
89
  if (!bin || !exec)
70
90
  return;
91
+ // omp records the post-hook command in its transcript, so only pi leaves an audit row here:
92
+ // opencode rewrites in its own plugin hook, and an omp row would double-count the transcript.
93
+ const auditHost = isPiHost(pi) ? 'pi' : null;
94
+ // Registered synchronously, unlike the rtk-init hook, whose async version probe can miss the first call of a fast session.
95
+ pi.on('tool_call', async (event, ctx) => {
96
+ const call = (event ?? {});
97
+ const input = call.input;
98
+ if (call.toolName !== 'bash' || !input)
99
+ return;
100
+ const command = input.command;
101
+ if (typeof command !== 'string' || !command.trim() || command.startsWith('rtk '))
102
+ return;
103
+ if (!rtkActive(sessionEntries(ctx)))
104
+ return;
105
+ const rewritten = await rewriteCommand(exec, bin, command, ctx?.signal);
106
+ if (!rewritten || rewritten === command)
107
+ return;
108
+ input.command = rewritten;
109
+ if (auditHost)
110
+ appendUsage('rtk-audit', `${auditHost} ${command}`);
111
+ });
71
112
  pi.on('tool_result', async (event, ctx) => {
72
113
  const filter = FILTER_BY_TOOL.get(String(event?.toolName ?? '').toLowerCase());
73
114
  if (!filter || event?.isError)
@@ -1,7 +1,7 @@
1
1
  import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
2
2
  import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
3
3
  import { readRtkDefault } from '../shared/plugin-settings.js';
4
- import { resolveRtkBinary } from '../shared/utils.js';
4
+ import { resolveRtkBinary, shimCommand } from '../shared/utils.js';
5
5
  import { announceStatus } from '../shared/status.js';
6
6
  const DEFAULT_ENABLED = false;
7
7
  function asBoolean(value) {
@@ -87,7 +87,9 @@ export default function rtkSessionExtension(pi) {
87
87
  onUpdate?.({ content: [{ type: 'text', text: `${bin === null ? '' : 'rtk '}${argv.join(' ')}` }], details: { phase: 'start' } });
88
88
  try {
89
89
  // A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
90
- const result = await pi.exec(bin ?? argv[0], argv, { signal, cwd });
90
+ // A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
91
+ const shim = shimCommand(bin ?? argv[0], argv);
92
+ const result = await pi.exec(shim.cmd, shim.args, { signal, cwd });
91
93
  const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
92
94
  return {
93
95
  isError: result.code !== 0,
@@ -11,7 +11,8 @@ import comboToggleExtension from './combo/index.js';
11
11
  import tersioCommandsExtension from './commands/index.js';
12
12
  import { getSharedComboState } from './shared/session-state.js';
13
13
  import { formatStatus } from './shared/status.js';
14
- import { resolveRtkBinary } from './shared/utils.js';
14
+ import { resolveRtkBinary, shimCommand } from './shared/utils.js';
15
+ import { appendUsage } from './shared/usage-ledger.js';
15
16
  // The npm name, so the plugin list reads the same as the OMP and pi installs.
16
17
  const plugin = {
17
18
  id: '@krtclcdy/tersio',
@@ -217,8 +218,12 @@ const plugin = {
217
218
  if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid)))
218
219
  return;
219
220
  const rewritten = await rewriteCommand(input.command);
220
- if (rewritten)
221
- input.command = rewritten;
221
+ if (!rewritten || rewritten === input.command)
222
+ return;
223
+ const original = input.command;
224
+ input.command = rewritten;
225
+ // The transcript stores the pre-hook command, so the rewrite leaves its own row.
226
+ appendUsage('rtk-audit', `opencode ${original}`);
222
227
  });
223
228
  // Built-in grep and glob output never reaches the bash hook; mutate result.content in place.
224
229
  try {
@@ -253,16 +258,20 @@ const plugin = {
253
258
  };
254
259
  /** execFile with the promise shape `filterThroughRtk` expects. */
255
260
  function execRtk(cmd, args, opts) {
256
- return new Promise((resolve) => {
257
- execFile(cmd, args, { signal: opts?.signal, timeout: opts?.timeout }, (error, stdout) => {
258
- const out = stdout ?? '';
259
- if (!error)
260
- return resolve({ stdout: out, code: 0 });
261
- // A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
262
- const raw = error.code;
263
- resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
264
- });
261
+ const { promise, resolve } = Promise.withResolvers();
262
+ // cmd.exe parses its own quoting, so the command string must reach it verbatim: Node's default escaping turns the inner quotes into `\"` and the redirect then fails.
263
+ const shim = shimCommand(cmd, args);
264
+ execFile(shim.cmd, shim.args, { signal: opts?.signal, timeout: opts?.timeout, windowsVerbatimArguments: process.platform === 'win32' }, (error, stdout) => {
265
+ const out = stdout ?? '';
266
+ if (!error) {
267
+ resolve({ stdout: out, code: 0 });
268
+ return;
269
+ }
270
+ // A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
271
+ const raw = error.code;
272
+ resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
265
273
  });
274
+ return promise;
266
275
  }
267
276
  /** `rtk rewrite` exit codes: 0 rewrite found, 1 no equivalent, 3 advisory; the code is the contract. */
268
277
  const RTK_REWRITE_FOUND = 0;
@@ -326,23 +335,29 @@ function rewriteCommand(command) {
326
335
  const bin = resolveRtkBinary();
327
336
  if (!bin)
328
337
  return Promise.resolve(null);
329
- return new Promise((resolve) => {
330
- execFile(bin, ['rewrite', command], { timeout: 5000 }, (error, stdout) => {
331
- // A kill or timeout means untrustworthy output; pass the command through.
332
- const err = error;
333
- if (err && (err.killed || err.signal))
334
- return resolve(null);
335
- const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
336
- // Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
337
- if (code === 1)
338
- return resolve(null);
339
- const out = String(stdout).trim();
340
- if (!out || out === command)
341
- return resolve(null);
342
- if (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY)
343
- return resolve(null);
344
- resolve(out);
345
- });
338
+ const { promise, resolve } = Promise.withResolvers();
339
+ // A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
340
+ const shim = shimCommand(bin, ['rewrite', command]);
341
+ execFile(shim.cmd, shim.args, { timeout: 5000 }, (error, stdout) => {
342
+ // A kill or timeout means untrustworthy output; pass the command through.
343
+ const err = error;
344
+ if (err && (err.killed || err.signal)) {
345
+ resolve(null);
346
+ return;
347
+ }
348
+ const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
349
+ // Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
350
+ if (code === 1) {
351
+ resolve(null);
352
+ return;
353
+ }
354
+ const out = String(stdout).trim();
355
+ if (!out || out === command || (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY)) {
356
+ resolve(null);
357
+ return;
358
+ }
359
+ resolve(out);
346
360
  });
361
+ return promise;
347
362
  }
348
363
  export default plugin;