ai-battery 0.1.5 → 0.1.7

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.
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
- import os from "node:os";
6
5
  import path from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
7
 
@@ -170,8 +169,34 @@ function quoteCmdArg(value) {
170
169
  return `"${String(value).replace(/"/g, '""')}"`;
171
170
  }
172
171
 
172
+ function normalizeWindowsCommandPath(value) {
173
+ let text = String(value || "").trim();
174
+ for (let i = 0; i < 6; i += 1) {
175
+ const before = text;
176
+ text = text.replace(/\\"/g, "\"").trim();
177
+ if (
178
+ (text.startsWith("\"") && text.endsWith("\""))
179
+ || (text.startsWith("'") && text.endsWith("'"))
180
+ ) {
181
+ text = text.slice(1, -1).trim();
182
+ }
183
+ if (text === before) break;
184
+ }
185
+ return text;
186
+ }
187
+
188
+ function resolveWindowsCommandFile(commandPath) {
189
+ commandPath = normalizeWindowsCommandPath(commandPath);
190
+ if (path.extname(commandPath)) return commandPath;
191
+ for (const suffix of [".cmd", ".exe", ".bat", ".ps1"]) {
192
+ const candidate = `${commandPath}${suffix}`;
193
+ if (fs.existsSync(candidate)) return candidate;
194
+ }
195
+ return commandPath;
196
+ }
197
+
173
198
  function windowsCommand(command) {
174
- const exe = command[0];
199
+ const exe = resolveWindowsCommandFile(command[0]);
175
200
  const rest = command.slice(1);
176
201
  if (/\.(cmd|bat)$/i.test(exe)) {
177
202
  return {
@@ -179,6 +204,18 @@ function windowsCommand(command) {
179
204
  args: ["/d", "/s", "/c", [exe, ...rest].map(quoteCmdArg).join(" ")]
180
205
  };
181
206
  }
207
+ if (/\.ps1$/i.test(exe)) {
208
+ return {
209
+ file: "powershell.exe",
210
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", exe, ...rest]
211
+ };
212
+ }
213
+ if (/\.(js|mjs|cjs)$/i.test(exe)) {
214
+ return {
215
+ file: process.execPath,
216
+ args: [exe, ...rest]
217
+ };
218
+ }
182
219
  return { file: exe, args: rest };
183
220
  }
184
221
 
@@ -294,7 +331,26 @@ async function main() {
294
331
  process.exit(exitCode);
295
332
  }
296
333
 
297
- main().catch((error) => {
298
- console.error(`ai-battery-run-win: ${error.message}`);
299
- process.exit(1);
300
- });
334
+ export {
335
+ resolveWindowsCommandFile,
336
+ windowsCommand
337
+ };
338
+
339
+ function sameFilePath(leftPath, rightPath) {
340
+ try {
341
+ return fs.realpathSync(leftPath) === fs.realpathSync(rightPath);
342
+ } catch {
343
+ return path.resolve(leftPath) === path.resolve(rightPath);
344
+ }
345
+ }
346
+
347
+ function isDirectRun() {
348
+ return Boolean(process.argv[1]) && sameFilePath(fileURLToPath(import.meta.url), process.argv[1]);
349
+ }
350
+
351
+ if (isDirectRun()) {
352
+ main().catch((error) => {
353
+ console.error(`ai-battery-run-win: ${error.message}`);
354
+ process.exit(1);
355
+ });
356
+ }
package/bin/ai-battery.js CHANGED
@@ -492,6 +492,14 @@ function executableTarget(commandPath) {
492
492
  }
493
493
  }
494
494
 
495
+ function wrapperCommandTarget(commandPath) {
496
+ // On Windows the PATH command is often an npm-generated .cmd shim. Resolving
497
+ // it can expose the underlying .js file, which is not directly executable by
498
+ // ConPTY/spawn and fails with ERROR_BAD_EXE_FORMAT (193).
499
+ if (process.platform === "win32") return commandPath;
500
+ return executableTarget(commandPath);
501
+ }
502
+
495
503
  function findCommand(command, skipPaths = []) {
496
504
  const names = commandNames(command);
497
505
  const skips = skipPaths.filter(Boolean);
@@ -508,7 +516,7 @@ function findCommand(command, skipPaths = []) {
508
516
 
509
517
  function commandNames(command) {
510
518
  return process.platform === "win32"
511
- ? [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`]
519
+ ? [`${command}.cmd`, `${command}.exe`, `${command}.bat`, command]
512
520
  : [command];
513
521
  }
514
522
 
@@ -529,16 +537,56 @@ function cmdQuote(value) {
529
537
  return `"${String(value).replace(/"/g, '""')}"`;
530
538
  }
531
539
 
540
+ function normalizeWindowsCommandPath(value) {
541
+ let text = String(value || "").trim();
542
+ for (let i = 0; i < 6; i += 1) {
543
+ const before = text;
544
+ text = text.replace(/\\"/g, "\"").trim();
545
+ if (
546
+ (text.startsWith("\"") && text.endsWith("\""))
547
+ || (text.startsWith("'") && text.endsWith("'"))
548
+ ) {
549
+ text = text.slice(1, -1).trim();
550
+ }
551
+ if (text === before) break;
552
+ }
553
+ return text;
554
+ }
555
+
556
+ function windowsCommandSibling(commandPath) {
557
+ commandPath = normalizeWindowsCommandPath(commandPath);
558
+ if (path.extname(commandPath)) return commandPath;
559
+ for (const suffix of [".cmd", ".exe", ".bat", ".ps1"]) {
560
+ const candidate = `${commandPath}${suffix}`;
561
+ if (fs.existsSync(candidate)) return candidate;
562
+ }
563
+ return commandPath;
564
+ }
565
+
566
+ function windowsCmdFallbackCommand(commandPath) {
567
+ const command = windowsCommandSibling(commandPath);
568
+ const quotedCommand = cmdQuote(command);
569
+ if (/\.(cmd|bat)$/i.test(command)) return ` call ${quotedCommand} %*`;
570
+ if (/\.ps1$/i.test(command)) {
571
+ return ` powershell.exe -NoProfile -ExecutionPolicy Bypass -File ${quotedCommand} %*`;
572
+ }
573
+ if (/\.(js|mjs|cjs)$/i.test(command)) {
574
+ return ` ${cmdQuote(process.execPath)} ${quotedCommand} %*`;
575
+ }
576
+ return ` ${quotedCommand} %*`;
577
+ }
578
+
532
579
  function windowsCodexWrapperScript(originalCommand) {
533
580
  const runner = path.join(scriptDir(), "ai-battery-run-win.js");
581
+ const command = windowsCommandSibling(originalCommand);
534
582
  return `@echo off
535
583
  rem ${CODEX_WRAPPER_MARKER}=1
536
- set "AI_BATTERY_ORIGINAL_CODEX=${originalCommand}"
584
+ set "AI_BATTERY_ORIGINAL_CODEX=${command}"
537
585
  set "AI_BATTERY_WRAPPED_CODEX=1"
538
586
  if exist ${cmdQuote(runner)} (
539
- ${cmdQuote(process.execPath)} ${cmdQuote(runner)} --provider all -- ${cmdQuote(originalCommand)} %*
587
+ ${cmdQuote(process.execPath)} ${cmdQuote(runner)} --provider all -- ${cmdQuote(command)} %*
540
588
  ) else (
541
- ${cmdQuote(originalCommand)} %*
589
+ ${windowsCmdFallbackCommand(command)}
542
590
  )
543
591
  `;
544
592
  }
@@ -957,7 +1005,7 @@ function installCodexWrapper(args) {
957
1005
  ? configuredOriginal
958
1006
  : discoveredOriginal;
959
1007
  const originalPathForPath = discoveredOriginal || originalCandidate;
960
- const originalCommand = originalCandidate ? executableTarget(originalCandidate) : null;
1008
+ const originalCommand = originalCandidate ? wrapperCommandTarget(originalCandidate) : null;
961
1009
 
962
1010
  if (!originalCommand) {
963
1011
  return {
@@ -1069,7 +1117,11 @@ function diagnoseCodex() {
1069
1117
  notes.push(`A Codex backup is available for restore: ${activeWrapperBackup}`);
1070
1118
  }
1071
1119
  if (wrapperInstalled && !activeIsWrapper) {
1072
- notes.push(`Plain "codex" does not resolve to the AI Battery wrapper in this shell. Ensure ${path.dirname(configuredWrapper)} is before the original codex on PATH, then open a new terminal or run "hash -r" in the parent shell.`);
1120
+ const wrapperDir = path.dirname(configuredWrapper);
1121
+ const refreshNote = process.platform === "win32"
1122
+ ? `open a new cmd/PowerShell, or run: $env:Path="${wrapperDir};$env:Path"`
1123
+ : "open a new terminal or run \"hash -r\" in the parent shell";
1124
+ notes.push(`Plain "codex" does not resolve to the AI Battery wrapper in this shell. Ensure ${wrapperDir} is before the original codex on PATH, then ${refreshNote}.`);
1073
1125
  }
1074
1126
  if (!runnerExists) {
1075
1127
  notes.push(`AI Battery runner is missing or not executable: ${runnerPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-battery",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Tiny terminal battery meter for local Codex and Claude Code usage.",
5
5
  "type": "module",
6
6
  "keywords": [