ai-battery 0.1.3 → 0.1.4
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 +29 -1
- package/bin/ai-battery.js +629 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,7 @@ ai-battery --version
|
|
|
107
107
|
ai-battery --provider codex
|
|
108
108
|
ai-battery --provider claude
|
|
109
109
|
ai-battery setup
|
|
110
|
+
ai-battery uninstall
|
|
110
111
|
ai-battery doctor
|
|
111
112
|
ai-battery hud
|
|
112
113
|
ai-battery off codex
|
|
@@ -124,6 +125,33 @@ ai-battery on codex
|
|
|
124
125
|
|
|
125
126
|
`doctor`는 설치 상태와 함께 npm latest 버전을 확인합니다. 네트워크가 막혀 있으면 버전 확인만 건너뛰고 나머지 진단은 계속 표시합니다.
|
|
126
127
|
|
|
128
|
+
## Uninstall
|
|
129
|
+
|
|
130
|
+
`off`는 표시만 숨기는 설정이고, `uninstall`은 `setup`과 HUD autostart가 만든 통합 지점을 제거합니다.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
ai-battery uninstall
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
일부만 제거할 수도 있습니다.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
ai-battery uninstall codex
|
|
140
|
+
ai-battery uninstall claude
|
|
141
|
+
ai-battery uninstall hud
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
이 명령은 AI Battery가 관리 마커를 넣은 Codex wrapper, Claude `statusLine`, HUD/menu bar autostart와 실행 중인 HUD를 정리합니다. 다른 도구가 만든 `codex` 파일이나 Claude `statusLine`은 건드리지 않습니다. 이전 버전이나 `--force`로 기존 파일을 백업한 경우에는 가능한 한 원래 파일/symlink를 복원합니다. 현재 이미 AI Battery wrapper 안에서 실행 중인 Codex 세션의 terminal row는 그 세션을 종료해야 사라집니다.
|
|
145
|
+
|
|
146
|
+
최신 npm은 패키지의 uninstall lifecycle을 실행하지 않기 때문에 `npm uninstall ai-battery` 또는 `npm uninstall -g ai-battery`만으로는 외부 통합 지점을 자동 정리할 수 없습니다. 완전히 제거하려면 npm 패키지를 지우기 전에 먼저 실행하세요.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
ai-battery uninstall
|
|
150
|
+
npm uninstall -g ai-battery
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
이미 npm 패키지를 먼저 지운 뒤라면, 다시 설치한 다음 `ai-battery uninstall`을 실행하거나 아래 항목을 직접 확인해 제거해야 합니다: AI Battery가 만든 Codex wrapper, shell rc의 `# >>> ai-battery setup >>>` block, Claude `statusLine`, HUD/menu bar autostart.
|
|
154
|
+
|
|
127
155
|
## Setup
|
|
128
156
|
|
|
129
157
|
`setup`은 한 번만 실행합니다. Claude Code에는 statusLine hook을 설치하고, Codex에는 `codex` wrapper를 설치해서 이후에는 추가 명령 없이 원래처럼 실행하게 합니다.
|
|
@@ -139,7 +167,7 @@ ai-battery setup claude
|
|
|
139
167
|
ai-battery setup codex
|
|
140
168
|
```
|
|
141
169
|
|
|
142
|
-
Codex wrapper는 기존 `codex` 명령을 직접 덮어쓰지 않습니다. `~/.local/bin/codex`에 관리형 wrapper를 만들고, 필요한 경우 셸 설정에 `~/.local/bin
|
|
170
|
+
Codex wrapper는 기존 `codex` 명령을 직접 덮어쓰지 않습니다. `~/.local/bin`이 이미 PATH에서 원본 `codex`보다 앞에 있고 `~/.local/bin/codex`가 비어 있거나 AI Battery 관리 파일이면 그 위치에 wrapper를 둬서 바로 잡히게 합니다. 그렇지 않으면 `~/.local/share/ai-battery/bin/codex`에 관리형 wrapper를 만들고, 필요한 경우 셸 설정에 이 디렉터리를 PATH 앞쪽으로 추가합니다. `~/.local/bin/codex` 같은 공용 위치에 이미 다른 파일이 있으면 덮어쓰지 않습니다. 새 터미널부터 `codex`가 자동으로 AI Battery 하단 행과 함께 실행됩니다. 같은 터미널에서 이미 `codex`를 실행한 적이 있으면 셸 캐시 때문에 `hash -r`이 한 번 필요할 수 있고, PATH 추가가 필요한 경우에는 `setup` 출력에 표시되는 `source ...` 명령을 실행하세요.
|
|
143
171
|
|
|
144
172
|
Codex 하단 행이 보이지 않으면 진단을 실행합니다.
|
|
145
173
|
|
package/bin/ai-battery.js
CHANGED
|
@@ -18,6 +18,8 @@ const DEFAULT_STATUSLINE_COLUMN_GUARD = 4;
|
|
|
18
18
|
const COMMANDS = new Set([
|
|
19
19
|
"show",
|
|
20
20
|
"setup",
|
|
21
|
+
"teardown",
|
|
22
|
+
"uninstall",
|
|
21
23
|
"doctor",
|
|
22
24
|
"hud",
|
|
23
25
|
"on",
|
|
@@ -27,7 +29,11 @@ const COMMANDS = new Set([
|
|
|
27
29
|
"uninstall-claude-statusline"
|
|
28
30
|
]);
|
|
29
31
|
const PROVIDERS = ["codex", "claude"];
|
|
32
|
+
const TEARDOWN_TARGETS = ["codex", "claude", "hud"];
|
|
30
33
|
const CODEX_WRAPPER_MARKER = "AI_BATTERY_MANAGED_CODEX_WRAPPER";
|
|
34
|
+
const CODEX_PREFERRED_BACKUP_SUFFIX = ".ai-battery-original-link";
|
|
35
|
+
const CODEX_TIMESTAMP_BACKUP_MARKER = ".ai-battery-backup-";
|
|
36
|
+
const DEFAULT_STATUSLINE_HEADER_COLUMN_GUARD = 2;
|
|
31
37
|
|
|
32
38
|
function parseArgs(argv) {
|
|
33
39
|
const args = {
|
|
@@ -60,7 +66,7 @@ function parseArgs(argv) {
|
|
|
60
66
|
args.rest = argv.slice(i + 1);
|
|
61
67
|
break;
|
|
62
68
|
}
|
|
63
|
-
} else if (!arg.startsWith("-") && ["setup", "on", "off"].includes(args.command)) {
|
|
69
|
+
} else if (!arg.startsWith("-") && ["setup", "teardown", "uninstall", "on", "off"].includes(args.command)) {
|
|
64
70
|
args.targets.push(arg);
|
|
65
71
|
} else if (arg === "--provider" || arg === "-p") {
|
|
66
72
|
args.provider = argv[++i] || "all";
|
|
@@ -126,6 +132,7 @@ function printHelp() {
|
|
|
126
132
|
Usage:
|
|
127
133
|
ai-battery [options]
|
|
128
134
|
ai-battery setup [codex|claude] [--force]
|
|
135
|
+
ai-battery uninstall [codex|claude|hud|all]
|
|
129
136
|
ai-battery doctor
|
|
130
137
|
ai-battery hud [start|stop|status] [hud options]
|
|
131
138
|
ai-battery hud autostart on|off|status
|
|
@@ -157,6 +164,7 @@ Options:
|
|
|
157
164
|
Compatibility:
|
|
158
165
|
ai-battery install-claude-statusline [--force]
|
|
159
166
|
ai-battery uninstall-claude-statusline
|
|
167
|
+
ai-battery teardown [codex|claude|hud|all]
|
|
160
168
|
`);
|
|
161
169
|
}
|
|
162
170
|
|
|
@@ -185,6 +193,12 @@ function stateDir() {
|
|
|
185
193
|
return homePath(".local", "state", "ai-battery");
|
|
186
194
|
}
|
|
187
195
|
|
|
196
|
+
function dataDir() {
|
|
197
|
+
if (process.env.AI_BATTERY_DATA_DIR) return process.env.AI_BATTERY_DATA_DIR;
|
|
198
|
+
if (process.env.XDG_DATA_HOME) return path.join(process.env.XDG_DATA_HOME, "ai-battery");
|
|
199
|
+
return homePath(".local", "share", "ai-battery");
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
function legacyStateDir() {
|
|
189
203
|
if (process.env.XDG_STATE_HOME) return path.join(process.env.XDG_STATE_HOME, "claudex-battery");
|
|
190
204
|
return homePath(".local", "state", "claudex-battery");
|
|
@@ -217,7 +231,8 @@ function defaultConfig() {
|
|
|
217
231
|
codex: true,
|
|
218
232
|
claude: true
|
|
219
233
|
},
|
|
220
|
-
codexWrapper: null
|
|
234
|
+
codexWrapper: null,
|
|
235
|
+
claudeStatusLineBackup: null
|
|
221
236
|
};
|
|
222
237
|
}
|
|
223
238
|
|
|
@@ -258,6 +273,21 @@ function providerTargets(targets) {
|
|
|
258
273
|
return [...providers];
|
|
259
274
|
}
|
|
260
275
|
|
|
276
|
+
function teardownTargets(targets) {
|
|
277
|
+
const requested = targets.length ? targets : ["all"];
|
|
278
|
+
const selected = new Set();
|
|
279
|
+
for (const target of requested) {
|
|
280
|
+
if (target === "all") {
|
|
281
|
+
TEARDOWN_TARGETS.forEach((item) => selected.add(item));
|
|
282
|
+
} else if (TEARDOWN_TARGETS.includes(target)) {
|
|
283
|
+
selected.add(target);
|
|
284
|
+
} else {
|
|
285
|
+
throw new Error("target must be one of: codex, claude, hud, all");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return [...selected];
|
|
289
|
+
}
|
|
290
|
+
|
|
261
291
|
function setProviderVisibility(targets, visible) {
|
|
262
292
|
const providers = providerTargets(targets);
|
|
263
293
|
const config = readConfig();
|
|
@@ -467,7 +497,7 @@ function codexWrapperScript(originalCommand) {
|
|
|
467
497
|
# ${CODEX_WRAPPER_MARKER}=1
|
|
468
498
|
export AI_BATTERY_ORIGINAL_CODEX=${shQuote(originalCommand)}
|
|
469
499
|
export AI_BATTERY_WRAPPED_CODEX=1
|
|
470
|
-
if [ -t 0 ] && [ -t 1 ]; then
|
|
500
|
+
if [ -t 0 ] && [ -t 1 ] && [ -x ${shQuote(runner)} ]; then
|
|
471
501
|
exec ${shQuote(runner)} --provider all -- ${shQuote(originalCommand)} "$@"
|
|
472
502
|
fi
|
|
473
503
|
exec ${shQuote(originalCommand)} "$@"
|
|
@@ -482,6 +512,86 @@ function managedCodexWrapper(filePath) {
|
|
|
482
512
|
}
|
|
483
513
|
}
|
|
484
514
|
|
|
515
|
+
function defaultCodexShimDir() {
|
|
516
|
+
return path.join(dataDir(), "bin");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function legacyCodexWrapperPath() {
|
|
520
|
+
return homePath(".local", "bin", "codex");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function legacyCodexShimDir() {
|
|
524
|
+
return path.dirname(legacyCodexWrapperPath());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function pathIndexForDir(dir) {
|
|
528
|
+
return pathEntries().findIndex((entry) => path.resolve(entry) === path.resolve(dir));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function pathDirPrecedesOriginal(dir, originalCommand) {
|
|
532
|
+
const shimIndex = pathIndexForDir(dir);
|
|
533
|
+
if (shimIndex < 0) return false;
|
|
534
|
+
const originalIndex = pathIndexForDir(path.dirname(originalCommand));
|
|
535
|
+
return originalIndex < 0 || shimIndex < originalIndex;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function codexInstallSkipPaths(config = readConfig()) {
|
|
539
|
+
return uniquePaths([
|
|
540
|
+
config.codexWrapper?.wrapperPath,
|
|
541
|
+
process.env.AI_BATTERY_SHIM_DIR ? path.join(process.env.AI_BATTERY_SHIM_DIR, "codex") : null,
|
|
542
|
+
path.join(defaultCodexShimDir(), "codex"),
|
|
543
|
+
legacyCodexWrapperPath()
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function canUseCodexShimTarget(wrapperPath, originalCommand) {
|
|
548
|
+
if (samePath(wrapperPath, originalCommand)) return false;
|
|
549
|
+
if (!fs.existsSync(wrapperPath)) return true;
|
|
550
|
+
return managedCodexWrapper(wrapperPath);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function selectCodexShimDir(originalCommand, pathCommand = originalCommand) {
|
|
554
|
+
if (process.env.AI_BATTERY_SHIM_DIR) {
|
|
555
|
+
return {
|
|
556
|
+
shimDir: process.env.AI_BATTERY_SHIM_DIR,
|
|
557
|
+
immediate: pathDirPrecedesOriginal(process.env.AI_BATTERY_SHIM_DIR, pathCommand),
|
|
558
|
+
reason: "AI_BATTERY_SHIM_DIR"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const legacyDir = legacyCodexShimDir();
|
|
563
|
+
const legacyWrapper = path.join(legacyDir, "codex");
|
|
564
|
+
if (
|
|
565
|
+
pathDirPrecedesOriginal(legacyDir, pathCommand)
|
|
566
|
+
&& canUseCodexShimTarget(legacyWrapper, originalCommand)
|
|
567
|
+
) {
|
|
568
|
+
return {
|
|
569
|
+
shimDir: legacyDir,
|
|
570
|
+
immediate: true,
|
|
571
|
+
reason: `${legacyDir} is already before the original codex on PATH`
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const shimDir = defaultCodexShimDir();
|
|
576
|
+
return {
|
|
577
|
+
shimDir,
|
|
578
|
+
immediate: pathDirPrecedesOriginal(shimDir, pathCommand),
|
|
579
|
+
reason: "AI Battery-owned data directory"
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function findOriginalCodexCommand(skipPaths = []) {
|
|
584
|
+
const skips = skipPaths.filter(Boolean);
|
|
585
|
+
for (const dir of pathEntries()) {
|
|
586
|
+
const candidate = path.join(dir, "codex");
|
|
587
|
+
if (skips.some((skip) => samePath(candidate, skip))) continue;
|
|
588
|
+
if (!safeStat(candidate)?.isFile() || !isExecutable(candidate)) continue;
|
|
589
|
+
if (managedCodexWrapper(candidate)) continue;
|
|
590
|
+
return candidate;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
485
595
|
function shellRcPath() {
|
|
486
596
|
if (process.env.AI_BATTERY_RC) return process.env.AI_BATTERY_RC;
|
|
487
597
|
const shell = path.basename(process.env.SHELL || "");
|
|
@@ -500,6 +610,50 @@ function shellPathBlock(shimDir) {
|
|
|
500
610
|
return `\n# >>> ai-battery setup >>>\nexport PATH=${shQuote(shimDir)}:"$PATH"\n# <<< ai-battery setup <<<\n`;
|
|
501
611
|
}
|
|
502
612
|
|
|
613
|
+
function removeAiBatteryShellPathBlock(text) {
|
|
614
|
+
return String(text).replace(
|
|
615
|
+
/(\r?\n)?# >>> ai-battery setup >>>\r?\n[\s\S]*?# <<< ai-battery setup <<<(?:\r?\n)?/g,
|
|
616
|
+
(match, leadingNewline, offset) => (offset === 0 ? "" : (leadingNewline || "\n"))
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function uniquePaths(paths) {
|
|
621
|
+
const seen = new Set();
|
|
622
|
+
const result = [];
|
|
623
|
+
for (const filePath of paths.filter(Boolean)) {
|
|
624
|
+
const key = path.resolve(filePath);
|
|
625
|
+
if (seen.has(key)) continue;
|
|
626
|
+
seen.add(key);
|
|
627
|
+
result.push(filePath);
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function shellRcCandidates() {
|
|
633
|
+
return uniquePaths([
|
|
634
|
+
process.env.AI_BATTERY_RC,
|
|
635
|
+
shellRcPath(),
|
|
636
|
+
homePath(".zshrc"),
|
|
637
|
+
homePath(".bashrc"),
|
|
638
|
+
homePath(".bash_profile"),
|
|
639
|
+
homePath(".profile"),
|
|
640
|
+
homePath(".config", "fish", "config.fish")
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function removeShellPathBlocks() {
|
|
645
|
+
const changed = [];
|
|
646
|
+
for (const rcPath of shellRcCandidates()) {
|
|
647
|
+
if (!fs.existsSync(rcPath)) continue;
|
|
648
|
+
const existing = fs.readFileSync(rcPath, "utf8");
|
|
649
|
+
const next = removeAiBatteryShellPathBlock(existing);
|
|
650
|
+
if (next === existing) continue;
|
|
651
|
+
fs.writeFileSync(rcPath, next, "utf8");
|
|
652
|
+
changed.push(rcPath);
|
|
653
|
+
}
|
|
654
|
+
return changed;
|
|
655
|
+
}
|
|
656
|
+
|
|
503
657
|
function ensureShimPath(shimDir, originalCommand) {
|
|
504
658
|
const entries = pathEntries();
|
|
505
659
|
const shimIndex = entries.findIndex((entry) => path.resolve(entry) === path.resolve(shimDir));
|
|
@@ -510,26 +664,159 @@ function ensureShimPath(shimDir, originalCommand) {
|
|
|
510
664
|
return {
|
|
511
665
|
changed: false,
|
|
512
666
|
rcPath: null,
|
|
513
|
-
note: `${shimDir} is already before the original codex on PATH
|
|
667
|
+
note: `${shimDir} is already before the original codex on PATH. Plain "codex" can use AI Battery in new command lookups. If this shell already cached codex, run "hash -r" once.`
|
|
514
668
|
};
|
|
515
669
|
}
|
|
516
670
|
|
|
517
671
|
const rcPath = shellRcPath();
|
|
518
672
|
fs.mkdirSync(path.dirname(rcPath), { recursive: true });
|
|
519
673
|
const existing = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, "utf8") : "";
|
|
520
|
-
|
|
521
|
-
|
|
674
|
+
const withoutAiBatteryBlock = removeAiBatteryShellPathBlock(existing);
|
|
675
|
+
const separator = withoutAiBatteryBlock && !withoutAiBatteryBlock.endsWith("\n") ? "\n" : "";
|
|
676
|
+
const next = `${withoutAiBatteryBlock}${separator}${shellPathBlock(shimDir)}`;
|
|
677
|
+
fs.writeFileSync(rcPath, next, "utf8");
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
changed: true,
|
|
681
|
+
rcPath,
|
|
682
|
+
note: `${existing === withoutAiBatteryBlock ? "Added" : "Updated"} ${shimDir} before PATH in ${rcPath}. Open a new terminal for plain "codex" to use AI Battery.`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function codexWrapperCandidates(config = readConfig()) {
|
|
687
|
+
const activeCodex = findCommand("codex");
|
|
688
|
+
return uniquePaths([
|
|
689
|
+
config.codexWrapper?.wrapperPath,
|
|
690
|
+
process.env.AI_BATTERY_SHIM_DIR ? path.join(process.env.AI_BATTERY_SHIM_DIR, "codex") : null,
|
|
691
|
+
path.join(defaultCodexShimDir(), "codex"),
|
|
692
|
+
legacyCodexWrapperPath(),
|
|
693
|
+
activeCodex && managedCodexWrapper(activeCodex) ? activeCodex : null
|
|
694
|
+
]);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function codexTimestampBackups(wrapperPath) {
|
|
698
|
+
const dir = path.dirname(wrapperPath);
|
|
699
|
+
const prefix = `${path.basename(wrapperPath)}${CODEX_TIMESTAMP_BACKUP_MARKER}`;
|
|
700
|
+
try {
|
|
701
|
+
return fs.readdirSync(dir)
|
|
702
|
+
.filter((entry) => entry.startsWith(prefix))
|
|
703
|
+
.map((entry) => path.join(dir, entry))
|
|
704
|
+
.filter((entryPath) => fs.existsSync(entryPath))
|
|
705
|
+
.sort((left, right) => {
|
|
706
|
+
const leftStat = safeStat(left);
|
|
707
|
+
const rightStat = safeStat(right);
|
|
708
|
+
return (rightStat?.mtimeMs ?? 0) - (leftStat?.mtimeMs ?? 0);
|
|
709
|
+
});
|
|
710
|
+
} catch {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function codexWrapperBackupCandidates(wrapperPath, config = readConfig()) {
|
|
716
|
+
return uniquePaths([
|
|
717
|
+
config.codexWrapper?.wrapperPath === wrapperPath ? config.codexWrapper?.backupPath : null,
|
|
718
|
+
`${wrapperPath}${CODEX_PREFERRED_BACKUP_SUFFIX}`,
|
|
719
|
+
...codexTimestampBackups(wrapperPath)
|
|
720
|
+
]);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function existingCodexWrapperBackup(wrapperPath, config = readConfig()) {
|
|
724
|
+
return codexWrapperBackupCandidates(wrapperPath, config).find((backupPath) => fs.existsSync(backupPath)) || null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function nextCodexBackupPath(wrapperPath) {
|
|
728
|
+
const preferred = `${wrapperPath}${CODEX_PREFERRED_BACKUP_SUFFIX}`;
|
|
729
|
+
if (!fs.existsSync(preferred)) return preferred;
|
|
730
|
+
return `${wrapperPath}${CODEX_TIMESTAMP_BACKUP_MARKER}${Date.now()}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function sameOrInsidePath(childPath, parentPath) {
|
|
734
|
+
const child = path.resolve(childPath);
|
|
735
|
+
const parent = path.resolve(parentPath);
|
|
736
|
+
return child === parent || child.startsWith(`${parent}${path.sep}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function canRemoveCodexWrapperWithoutBackup(wrapperPath, config = readConfig()) {
|
|
740
|
+
if (sameOrInsidePath(wrapperPath, defaultCodexShimDir())) return true;
|
|
741
|
+
if (samePath(wrapperPath, legacyCodexWrapperPath())) return true;
|
|
742
|
+
const configuredWrapper = config.codexWrapper?.wrapperPath;
|
|
743
|
+
if (
|
|
744
|
+
configuredWrapper
|
|
745
|
+
&& samePath(wrapperPath, configuredWrapper)
|
|
746
|
+
&& sameOrInsidePath(wrapperPath, dataDir())
|
|
747
|
+
) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function removeOrRestoreCodexWrapper(wrapperPath, config) {
|
|
754
|
+
const backupPath = existingCodexWrapperBackup(wrapperPath, config);
|
|
755
|
+
if (!backupPath && !canRemoveCodexWrapperWithoutBackup(wrapperPath, config)) {
|
|
522
756
|
return {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
757
|
+
wrapperPath,
|
|
758
|
+
restoredFrom: null,
|
|
759
|
+
skipped: true,
|
|
760
|
+
reason: "Managed wrapper is outside AI Battery-owned paths and no backup is available for restore"
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
fs.rmSync(wrapperPath, { force: true });
|
|
764
|
+
if (backupPath) {
|
|
765
|
+
fs.renameSync(backupPath, wrapperPath);
|
|
766
|
+
return {
|
|
767
|
+
wrapperPath,
|
|
768
|
+
restoredFrom: backupPath
|
|
526
769
|
};
|
|
527
770
|
}
|
|
771
|
+
return {
|
|
772
|
+
wrapperPath,
|
|
773
|
+
restoredFrom: null
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function uninstallCodexWrapper() {
|
|
778
|
+
const config = readConfig();
|
|
779
|
+
const candidates = codexWrapperCandidates(config);
|
|
780
|
+
const unmanaged = [];
|
|
781
|
+
const skippedWrappers = [];
|
|
782
|
+
const removedWrappers = [];
|
|
783
|
+
const restoredWrappers = [];
|
|
784
|
+
|
|
785
|
+
for (const wrapperPath of candidates) {
|
|
786
|
+
if (!fs.existsSync(wrapperPath)) continue;
|
|
787
|
+
if (!managedCodexWrapper(wrapperPath)) {
|
|
788
|
+
unmanaged.push(wrapperPath);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const result = removeOrRestoreCodexWrapper(wrapperPath, config);
|
|
792
|
+
if (result.skipped) {
|
|
793
|
+
skippedWrappers.push(result);
|
|
794
|
+
} else if (result.restoredFrom) {
|
|
795
|
+
restoredWrappers.push(result);
|
|
796
|
+
} else {
|
|
797
|
+
removedWrappers.push(result.wrapperPath);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const rcPaths = removeShellPathBlocks();
|
|
802
|
+
const hadConfig = Boolean(config.codexWrapper);
|
|
803
|
+
if (hadConfig) {
|
|
804
|
+
config.codexWrapper = null;
|
|
805
|
+
writeConfig(config);
|
|
806
|
+
}
|
|
528
807
|
|
|
529
808
|
return {
|
|
530
|
-
changed:
|
|
531
|
-
|
|
532
|
-
|
|
809
|
+
changed: Boolean(removedWrappers.length || restoredWrappers.length || rcPaths.length || hadConfig),
|
|
810
|
+
wrapperPath: restoredWrappers[0]?.wrapperPath || removedWrappers[0] || null,
|
|
811
|
+
removedWrappers,
|
|
812
|
+
restoredWrappers,
|
|
813
|
+
skippedWrappers,
|
|
814
|
+
rcPaths,
|
|
815
|
+
configPath: hadConfig ? configPath() : null,
|
|
816
|
+
unmanaged,
|
|
817
|
+
reason: (removedWrappers.length || restoredWrappers.length)
|
|
818
|
+
? null
|
|
819
|
+
: (skippedWrappers.length ? "Managed Codex wrapper found but left untouched because no restore backup was available" : "No managed Codex wrapper found")
|
|
533
820
|
};
|
|
534
821
|
}
|
|
535
822
|
|
|
@@ -542,13 +829,13 @@ function installCodexWrapper(args) {
|
|
|
542
829
|
};
|
|
543
830
|
}
|
|
544
831
|
|
|
545
|
-
const shimDir = process.env.AI_BATTERY_SHIM_DIR || homePath(".local", "bin");
|
|
546
|
-
const wrapperPath = path.join(shimDir, "codex");
|
|
547
832
|
const config = readConfig();
|
|
548
833
|
const configuredOriginal = config.codexWrapper?.originalCommand;
|
|
549
|
-
const
|
|
834
|
+
const discoveredOriginal = findOriginalCodexCommand(codexInstallSkipPaths(config));
|
|
835
|
+
const originalCandidate = configuredOriginal && fs.existsSync(configuredOriginal) && !managedCodexWrapper(configuredOriginal)
|
|
550
836
|
? configuredOriginal
|
|
551
|
-
:
|
|
837
|
+
: discoveredOriginal;
|
|
838
|
+
const originalPathForPath = discoveredOriginal || originalCandidate;
|
|
552
839
|
const originalCommand = originalCandidate ? executableTarget(originalCandidate) : null;
|
|
553
840
|
|
|
554
841
|
if (!originalCommand) {
|
|
@@ -559,20 +846,42 @@ function installCodexWrapper(args) {
|
|
|
559
846
|
};
|
|
560
847
|
}
|
|
561
848
|
|
|
849
|
+
const shimSelection = selectCodexShimDir(originalCommand, originalPathForPath);
|
|
850
|
+
const shimDir = shimSelection.shimDir;
|
|
851
|
+
const wrapperPath = path.join(shimDir, "codex");
|
|
852
|
+
|
|
853
|
+
if (samePath(wrapperPath, originalCommand)) {
|
|
854
|
+
return {
|
|
855
|
+
ok: false,
|
|
856
|
+
skipped: true,
|
|
857
|
+
reason: `Refusing to replace the original codex command at ${wrapperPath}`
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
562
861
|
fs.mkdirSync(shimDir, { recursive: true, mode: 0o755 });
|
|
862
|
+
let backupPath = null;
|
|
563
863
|
if (fs.existsSync(wrapperPath) && !managedCodexWrapper(wrapperPath)) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
864
|
+
return {
|
|
865
|
+
ok: false,
|
|
866
|
+
skipped: true,
|
|
867
|
+
reason: `Refusing to replace unmanaged codex command at ${wrapperPath}. Set AI_BATTERY_SHIM_DIR to an empty AI Battery-owned directory.`
|
|
868
|
+
};
|
|
568
869
|
}
|
|
569
870
|
|
|
570
871
|
fs.writeFileSync(wrapperPath, codexWrapperScript(originalCommand), { mode: 0o755 });
|
|
571
872
|
|
|
572
|
-
const
|
|
873
|
+
const wrapperCleanup = [];
|
|
874
|
+
for (const staleWrapperPath of codexInstallSkipPaths(config)) {
|
|
875
|
+
if (samePath(staleWrapperPath, wrapperPath)) continue;
|
|
876
|
+
if (!fs.existsSync(staleWrapperPath) || !managedCodexWrapper(staleWrapperPath)) continue;
|
|
877
|
+
wrapperCleanup.push(removeOrRestoreCodexWrapper(staleWrapperPath, config));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const pathResult = ensureShimPath(shimDir, originalPathForPath || originalCommand);
|
|
573
881
|
config.codexWrapper = {
|
|
574
882
|
wrapperPath,
|
|
575
883
|
originalCommand,
|
|
884
|
+
backupPath,
|
|
576
885
|
installedAt: new Date().toISOString()
|
|
577
886
|
};
|
|
578
887
|
writeConfig(config);
|
|
@@ -581,6 +890,10 @@ function installCodexWrapper(args) {
|
|
|
581
890
|
ok: true,
|
|
582
891
|
wrapperPath,
|
|
583
892
|
originalCommand,
|
|
893
|
+
backupPath,
|
|
894
|
+
legacyCleanup: wrapperCleanup,
|
|
895
|
+
wrapperCleanup,
|
|
896
|
+
shimSelection,
|
|
584
897
|
path: pathResult
|
|
585
898
|
};
|
|
586
899
|
}
|
|
@@ -599,11 +912,12 @@ function codexRestartNote() {
|
|
|
599
912
|
|
|
600
913
|
function diagnoseCodex() {
|
|
601
914
|
const config = readConfig();
|
|
602
|
-
const configuredWrapper = config.codexWrapper?.wrapperPath ||
|
|
915
|
+
const configuredWrapper = config.codexWrapper?.wrapperPath || path.join(defaultCodexShimDir(), "codex");
|
|
603
916
|
const configuredOriginal = config.codexWrapper?.originalCommand || null;
|
|
604
917
|
const activeCodex = findCommand("codex");
|
|
605
918
|
const wrapperInstalled = configuredWrapper ? managedCodexWrapper(configuredWrapper) : false;
|
|
606
919
|
const activeIsWrapper = activeCodex ? managedCodexWrapper(activeCodex) : false;
|
|
920
|
+
const activeWrapperBackup = activeIsWrapper ? existingCodexWrapperBackup(activeCodex, config) : null;
|
|
607
921
|
const originalExists = configuredOriginal ? fs.existsSync(configuredOriginal) : false;
|
|
608
922
|
const runnerPath = path.join(scriptDir(), "ai-battery-run");
|
|
609
923
|
const runnerExists = isExecutable(runnerPath);
|
|
@@ -617,8 +931,14 @@ function diagnoseCodex() {
|
|
|
617
931
|
if (!wrapperInstalled) {
|
|
618
932
|
notes.push("Codex wrapper is not installed. Run: ai-battery setup codex");
|
|
619
933
|
}
|
|
934
|
+
if (activeIsWrapper && !wrapperInstalled) {
|
|
935
|
+
notes.push("Plain \"codex\" still resolves to an AI Battery wrapper outside the configured shim. Run: ai-battery uninstall codex");
|
|
936
|
+
}
|
|
937
|
+
if (activeWrapperBackup) {
|
|
938
|
+
notes.push(`A Codex backup is available for restore: ${activeWrapperBackup}`);
|
|
939
|
+
}
|
|
620
940
|
if (wrapperInstalled && !activeIsWrapper) {
|
|
621
|
-
notes.push(
|
|
941
|
+
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.`);
|
|
622
942
|
}
|
|
623
943
|
if (!runnerExists) {
|
|
624
944
|
notes.push(`AI Battery runner is missing or not executable: ${runnerPath}`);
|
|
@@ -636,6 +956,7 @@ function diagnoseCodex() {
|
|
|
636
956
|
providerEnabled,
|
|
637
957
|
activeCodex,
|
|
638
958
|
activeIsWrapper,
|
|
959
|
+
activeWrapperBackup,
|
|
639
960
|
wrapperPath: configuredWrapper,
|
|
640
961
|
wrapperInstalled,
|
|
641
962
|
runnerPath,
|
|
@@ -680,7 +1001,7 @@ function runSetup(args) {
|
|
|
680
1001
|
const targets = providerTargets(args.targets);
|
|
681
1002
|
const results = {};
|
|
682
1003
|
if (targets.includes("claude")) {
|
|
683
|
-
results.claude = installClaudeStatusline(
|
|
1004
|
+
results.claude = installClaudeStatusline(args);
|
|
684
1005
|
}
|
|
685
1006
|
if (targets.includes("codex")) {
|
|
686
1007
|
results.codex = installCodexWrapper(args);
|
|
@@ -688,6 +1009,16 @@ function runSetup(args) {
|
|
|
688
1009
|
return results;
|
|
689
1010
|
}
|
|
690
1011
|
|
|
1012
|
+
function isWsl() {
|
|
1013
|
+
if (process.platform !== "linux") return false;
|
|
1014
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
1015
|
+
try {
|
|
1016
|
+
return fs.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
|
|
1017
|
+
} catch {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
691
1022
|
function runHud(args) {
|
|
692
1023
|
const hudPath = path.join(scriptDir(), "ai-battery-hud.js");
|
|
693
1024
|
return spawnSync(process.execPath, [hudPath, ...args.rest], {
|
|
@@ -696,6 +1027,71 @@ function runHud(args) {
|
|
|
696
1027
|
});
|
|
697
1028
|
}
|
|
698
1029
|
|
|
1030
|
+
function desktopHudSupported() {
|
|
1031
|
+
return process.platform === "darwin" || process.platform === "win32" || isWsl();
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function childResult(result) {
|
|
1035
|
+
return {
|
|
1036
|
+
ok: !result.error && result.status === 0,
|
|
1037
|
+
status: result.status ?? null,
|
|
1038
|
+
error: result.error?.message ?? null,
|
|
1039
|
+
stdout: (result.stdout || "").trim(),
|
|
1040
|
+
stderr: (result.stderr || "").trim()
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function runHudCommand(args) {
|
|
1045
|
+
const hudPath = path.join(scriptDir(), "ai-battery-hud.js");
|
|
1046
|
+
return childResult(spawnSync(process.execPath, [hudPath, ...args], {
|
|
1047
|
+
encoding: "utf8",
|
|
1048
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1049
|
+
timeout: 5000,
|
|
1050
|
+
windowsHide: true
|
|
1051
|
+
}));
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function uninstallHud() {
|
|
1055
|
+
if (!desktopHudSupported()) {
|
|
1056
|
+
return {
|
|
1057
|
+
changed: false,
|
|
1058
|
+
skipped: true,
|
|
1059
|
+
reason: "Desktop HUD is not supported on this platform"
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const autostart = runHudCommand(["autostart", "off"]);
|
|
1064
|
+
const stop = runHudCommand(["stop"]);
|
|
1065
|
+
return {
|
|
1066
|
+
changed: autostart.ok || stop.ok,
|
|
1067
|
+
autostart,
|
|
1068
|
+
stop
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function runTeardown(args) {
|
|
1073
|
+
const targets = teardownTargets(args.targets);
|
|
1074
|
+
const results = {};
|
|
1075
|
+
|
|
1076
|
+
const runStep = (name, action) => {
|
|
1077
|
+
results[name] = action();
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
if (targets.includes("claude")) {
|
|
1081
|
+
runStep("claude", () => uninstallClaudeStatusline({ strict: false }));
|
|
1082
|
+
}
|
|
1083
|
+
if (targets.includes("codex")) {
|
|
1084
|
+
runStep("codex", uninstallCodexWrapper);
|
|
1085
|
+
}
|
|
1086
|
+
if (targets.includes("hud")) {
|
|
1087
|
+
runStep("hud", uninstallHud);
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
targets,
|
|
1091
|
+
results
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
699
1095
|
function safeStat(filePath) {
|
|
700
1096
|
try {
|
|
701
1097
|
return fs.statSync(filePath);
|
|
@@ -1546,13 +1942,37 @@ function readClaudeStatuslineCache() {
|
|
|
1546
1942
|
return readClaudeStatuslineCacheFrom(legacyStateDir()) ?? null;
|
|
1547
1943
|
}
|
|
1548
1944
|
|
|
1945
|
+
function claudeStatuslineRefreshInterval() {
|
|
1946
|
+
const value = Number(process.env.AI_BATTERY_CLAUDE_REFRESH ?? process.env.CLAUDEX_BATTERY_CLAUDE_REFRESH);
|
|
1947
|
+
if (!Number.isFinite(value)) return 1;
|
|
1948
|
+
return clamp(Math.floor(value), 1, 60);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1549
1951
|
function installClaudeStatusline(args) {
|
|
1550
1952
|
const settingsPath = homePath(".claude", "settings.json");
|
|
1551
1953
|
const existing = readJson(settingsPath) ?? {};
|
|
1552
1954
|
const command = `${shellArg(process.execPath)} ${shellArg(fileURLToPath(import.meta.url))} capture-claude --muted --left-padding 1`;
|
|
1955
|
+
const currentCommand = existing.statusLine?.command ?? "";
|
|
1956
|
+
const installedByAiBattery = currentCommand.includes("ai-battery.js") && currentCommand.includes("capture-claude");
|
|
1957
|
+
const config = readConfig();
|
|
1553
1958
|
|
|
1554
|
-
if (existing.statusLine && !args.force) {
|
|
1555
|
-
|
|
1959
|
+
if (existing.statusLine && !installedByAiBattery && !args.force) {
|
|
1960
|
+
return {
|
|
1961
|
+
settingsPath,
|
|
1962
|
+
command,
|
|
1963
|
+
skipped: true,
|
|
1964
|
+
reason: "Claude statusLine is already configured by another tool. Re-run with --force to replace it with a restorable backup."
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
let backedUp = false;
|
|
1969
|
+
if (existing.statusLine && !installedByAiBattery && args.force) {
|
|
1970
|
+
config.claudeStatusLineBackup = {
|
|
1971
|
+
settingsPath,
|
|
1972
|
+
statusLine: existing.statusLine,
|
|
1973
|
+
savedAt: new Date().toISOString()
|
|
1974
|
+
};
|
|
1975
|
+
backedUp = true;
|
|
1556
1976
|
}
|
|
1557
1977
|
|
|
1558
1978
|
const next = {
|
|
@@ -1561,22 +1981,27 @@ function installClaudeStatusline(args) {
|
|
|
1561
1981
|
type: "command",
|
|
1562
1982
|
command,
|
|
1563
1983
|
padding: 0,
|
|
1564
|
-
refreshInterval:
|
|
1984
|
+
refreshInterval: claudeStatuslineRefreshInterval()
|
|
1565
1985
|
}
|
|
1566
1986
|
};
|
|
1567
1987
|
|
|
1568
1988
|
writeJsonAtomic(settingsPath, next);
|
|
1989
|
+
if (backedUp) writeConfig(config);
|
|
1569
1990
|
return {
|
|
1570
1991
|
settingsPath,
|
|
1571
|
-
command
|
|
1992
|
+
command,
|
|
1993
|
+
backedUp
|
|
1572
1994
|
};
|
|
1573
1995
|
}
|
|
1574
1996
|
|
|
1575
|
-
function uninstallClaudeStatusline() {
|
|
1997
|
+
function uninstallClaudeStatusline(options = {}) {
|
|
1576
1998
|
const settingsPath = homePath(".claude", "settings.json");
|
|
1577
1999
|
const existing = readJson(settingsPath) ?? {};
|
|
1578
2000
|
const command = existing.statusLine?.command ?? "";
|
|
1579
2001
|
const installedByAiBattery = command.includes("ai-battery.js") && command.includes("capture-claude");
|
|
2002
|
+
const strict = options.strict !== false;
|
|
2003
|
+
const config = readConfig();
|
|
2004
|
+
const backup = config.claudeStatusLineBackup;
|
|
1580
2005
|
|
|
1581
2006
|
if (!existing.statusLine) {
|
|
1582
2007
|
return {
|
|
@@ -1587,15 +2012,31 @@ function uninstallClaudeStatusline() {
|
|
|
1587
2012
|
}
|
|
1588
2013
|
|
|
1589
2014
|
if (!installedByAiBattery) {
|
|
2015
|
+
if (!strict) {
|
|
2016
|
+
return {
|
|
2017
|
+
settingsPath,
|
|
2018
|
+
changed: false,
|
|
2019
|
+
reason: "Claude statusLine is configured by another tool"
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
1590
2022
|
throw new Error(`Claude statusLine exists but does not look like AI Battery's command: ${command}`);
|
|
1591
2023
|
}
|
|
1592
2024
|
|
|
1593
2025
|
const next = { ...existing };
|
|
1594
|
-
|
|
2026
|
+
if (backup?.statusLine && (!backup.settingsPath || path.resolve(backup.settingsPath) === path.resolve(settingsPath))) {
|
|
2027
|
+
next.statusLine = backup.statusLine;
|
|
2028
|
+
} else {
|
|
2029
|
+
delete next.statusLine;
|
|
2030
|
+
}
|
|
1595
2031
|
writeJsonAtomic(settingsPath, next);
|
|
2032
|
+
if (backup) {
|
|
2033
|
+
config.claudeStatusLineBackup = null;
|
|
2034
|
+
writeConfig(config);
|
|
2035
|
+
}
|
|
1596
2036
|
return {
|
|
1597
2037
|
settingsPath,
|
|
1598
|
-
changed: true
|
|
2038
|
+
changed: true,
|
|
2039
|
+
restored: Boolean(backup?.statusLine)
|
|
1599
2040
|
};
|
|
1600
2041
|
}
|
|
1601
2042
|
|
|
@@ -1950,28 +2391,77 @@ function numericGuard(value) {
|
|
|
1950
2391
|
return clamp(Math.floor(number), 0, 20);
|
|
1951
2392
|
}
|
|
1952
2393
|
|
|
2394
|
+
function sttyColumns() {
|
|
2395
|
+
if (process.platform === "win32") return null;
|
|
2396
|
+
|
|
2397
|
+
let ttyFd = null;
|
|
2398
|
+
try {
|
|
2399
|
+
ttyFd = fs.openSync("/dev/tty", "r");
|
|
2400
|
+
const output = execFileSync("stty", ["size"], {
|
|
2401
|
+
encoding: "utf8",
|
|
2402
|
+
stdio: [ttyFd, "pipe", "ignore"],
|
|
2403
|
+
timeout: 100
|
|
2404
|
+
}).trim();
|
|
2405
|
+
const [, columns] = output.split(/\s+/).map((part) => Number(part));
|
|
2406
|
+
return numericColumn(columns);
|
|
2407
|
+
} catch {
|
|
2408
|
+
return null;
|
|
2409
|
+
} finally {
|
|
2410
|
+
if (ttyFd !== null) {
|
|
2411
|
+
try {
|
|
2412
|
+
fs.closeSync(ttyFd);
|
|
2413
|
+
} catch {
|
|
2414
|
+
// Nothing to clean up if the fd was already closed by the OS.
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function runtimeTerminalColumns() {
|
|
2421
|
+
return numericColumn(process.stdout.columns) ?? sttyColumns();
|
|
2422
|
+
}
|
|
2423
|
+
|
|
1953
2424
|
function statusLineColumns(input) {
|
|
1954
|
-
const
|
|
1955
|
-
process.env.
|
|
1956
|
-
|
|
2425
|
+
const explicitColumns = numericColumn(process.env.AI_BATTERY_COLUMNS)
|
|
2426
|
+
?? numericColumn(process.env.CLAUDEX_BATTERY_COLUMNS);
|
|
2427
|
+
if (explicitColumns) return explicitColumns;
|
|
2428
|
+
|
|
2429
|
+
const runtimeColumns = runtimeTerminalColumns();
|
|
2430
|
+
const payloadCandidates = [
|
|
1957
2431
|
input.terminal?.columns,
|
|
1958
2432
|
input.terminal?.cols,
|
|
1959
2433
|
input.terminal?.width,
|
|
1960
2434
|
input.terminal_columns,
|
|
1961
2435
|
input.terminal_width,
|
|
1962
2436
|
input.columns,
|
|
1963
|
-
input.width
|
|
1964
|
-
process.env.COLUMNS,
|
|
1965
|
-
process.stdout.columns
|
|
2437
|
+
input.width
|
|
1966
2438
|
];
|
|
1967
2439
|
|
|
1968
|
-
|
|
2440
|
+
let payloadColumns = null;
|
|
2441
|
+
for (const candidate of payloadCandidates) {
|
|
1969
2442
|
const columns = numericColumn(candidate);
|
|
1970
|
-
if (columns)
|
|
2443
|
+
if (columns) {
|
|
2444
|
+
payloadColumns = columns;
|
|
2445
|
+
break;
|
|
2446
|
+
}
|
|
1971
2447
|
}
|
|
2448
|
+
|
|
2449
|
+
if (payloadColumns && runtimeColumns) return Math.min(payloadColumns, runtimeColumns);
|
|
2450
|
+
if (payloadColumns) return payloadColumns;
|
|
2451
|
+
if (runtimeColumns) return runtimeColumns;
|
|
2452
|
+
|
|
2453
|
+
const envColumns = numericColumn(process.env.COLUMNS) ?? numericColumn(process.stdout.columns);
|
|
2454
|
+
if (envColumns) return envColumns;
|
|
1972
2455
|
return 80;
|
|
1973
2456
|
}
|
|
1974
2457
|
|
|
2458
|
+
function statusLineHeaderColumns(input, args) {
|
|
2459
|
+
const guard = numericGuard(process.env.AI_BATTERY_HEADER_COLUMN_GUARD)
|
|
2460
|
+
?? numericGuard(process.env.CLAUDEX_BATTERY_HEADER_COLUMN_GUARD)
|
|
2461
|
+
?? DEFAULT_STATUSLINE_HEADER_COLUMN_GUARD;
|
|
2462
|
+
return Math.max(20, statusLineColumns(input) - guard - (Number(args.leftPadding) || 0));
|
|
2463
|
+
}
|
|
2464
|
+
|
|
1975
2465
|
function statusLineUsableColumns(input) {
|
|
1976
2466
|
const guard = numericGuard(process.env.AI_BATTERY_COLUMN_GUARD)
|
|
1977
2467
|
?? numericGuard(process.env.CLAUDEX_BATTERY_COLUMN_GUARD)
|
|
@@ -2082,7 +2572,7 @@ function claudeHeader(input, args, capturedClaude = null) {
|
|
|
2082
2572
|
}
|
|
2083
2573
|
const left = parts.join(" ");
|
|
2084
2574
|
const right = contextLeftText(input, capturedClaude?.contextWindow ?? null);
|
|
2085
|
-
const columns =
|
|
2575
|
+
const columns = statusLineHeaderColumns(input, args);
|
|
2086
2576
|
const header = alignHeader(left, right, columns);
|
|
2087
2577
|
return statusColorize({ running: false }, header, args);
|
|
2088
2578
|
}
|
|
@@ -2252,6 +2742,71 @@ function render(snapshot, args) {
|
|
|
2252
2742
|
return applyLeftPadding(renderLine(snapshot, args), args);
|
|
2253
2743
|
}
|
|
2254
2744
|
|
|
2745
|
+
function printTeardownResult(result) {
|
|
2746
|
+
if (result.skipped) {
|
|
2747
|
+
console.log(`Teardown skipped: ${result.reason}`);
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
const { codex, claude, hud } = result.results;
|
|
2752
|
+
if (codex) {
|
|
2753
|
+
if (codex.error) {
|
|
2754
|
+
console.log(`Codex wrapper error: ${codex.error}`);
|
|
2755
|
+
} else {
|
|
2756
|
+
for (const restored of codex.restoredWrappers || []) {
|
|
2757
|
+
console.log(`Restored Codex command: ${restored.wrapperPath}`);
|
|
2758
|
+
console.log(`Restored from: ${restored.restoredFrom}`);
|
|
2759
|
+
}
|
|
2760
|
+
for (const wrapperPath of codex.removedWrappers || []) {
|
|
2761
|
+
console.log(`Removed Codex wrapper: ${wrapperPath}`);
|
|
2762
|
+
}
|
|
2763
|
+
for (const skipped of codex.skippedWrappers || []) {
|
|
2764
|
+
console.log(`Left Codex wrapper untouched: ${skipped.wrapperPath}`);
|
|
2765
|
+
console.log(`Reason: ${skipped.reason}`);
|
|
2766
|
+
}
|
|
2767
|
+
if (!(codex.restoredWrappers?.length || codex.removedWrappers?.length || codex.skippedWrappers?.length)) {
|
|
2768
|
+
console.log(`Codex wrapper: ${codex.reason}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
for (const rcPath of codex.rcPaths || []) {
|
|
2772
|
+
console.log(`Removed shell PATH block: ${rcPath}`);
|
|
2773
|
+
}
|
|
2774
|
+
if (codex.configPath) console.log(`Updated ${codex.configPath}`);
|
|
2775
|
+
for (const wrapperPath of codex.unmanaged || []) {
|
|
2776
|
+
console.log(`Skipped unmanaged codex command: ${wrapperPath}`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
if (claude) {
|
|
2781
|
+
if (claude.error) {
|
|
2782
|
+
console.log(`Claude statusLine error: ${claude.error}`);
|
|
2783
|
+
} else if (claude.changed) {
|
|
2784
|
+
console.log(`${claude.restored ? "Restored previous Claude statusLine" : "Removed Claude statusLine"}: ${claude.settingsPath}`);
|
|
2785
|
+
} else {
|
|
2786
|
+
console.log(`${claude.reason}: ${claude.settingsPath}`);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
if (hud) {
|
|
2791
|
+
if (hud.error) {
|
|
2792
|
+
console.log(`HUD error: ${hud.error}`);
|
|
2793
|
+
} else if (hud.skipped) {
|
|
2794
|
+
console.log(`HUD: ${hud.reason}`);
|
|
2795
|
+
} else {
|
|
2796
|
+
console.log(`HUD autostart: ${hud.autostart.ok ? "off" : "not changed"}`);
|
|
2797
|
+
if (!hud.autostart.ok && hud.autostart.error) console.log(`HUD autostart error: ${hud.autostart.error}`);
|
|
2798
|
+
if (!hud.autostart.ok && hud.autostart.stderr) console.log(`HUD autostart error: ${hud.autostart.stderr}`);
|
|
2799
|
+
console.log(`HUD process: ${hud.stop.ok ? "stopped" : "not changed"}`);
|
|
2800
|
+
if (!hud.stop.ok && hud.stop.error) console.log(`HUD stop error: ${hud.stop.error}`);
|
|
2801
|
+
if (!hud.stop.ok && hud.stop.stderr) console.log(`HUD stop error: ${hud.stop.stderr}`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
if (runningInsideAiBatteryCodexWrapper()) {
|
|
2806
|
+
console.log("Current Codex session was already started through AI Battery; exit this session to remove the terminal row.");
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2255
2810
|
async function main() {
|
|
2256
2811
|
const args = parseArgs(process.argv.slice(2));
|
|
2257
2812
|
if (args.version) {
|
|
@@ -2269,7 +2824,12 @@ async function main() {
|
|
|
2269
2824
|
console.log(JSON.stringify(result, null, 2));
|
|
2270
2825
|
} else {
|
|
2271
2826
|
if (result.claude) {
|
|
2272
|
-
|
|
2827
|
+
if (result.claude.skipped) {
|
|
2828
|
+
console.log(`Claude statusLine skipped: ${result.claude.reason}`);
|
|
2829
|
+
} else {
|
|
2830
|
+
console.log(`Claude statusLine installed: ${result.claude.settingsPath}`);
|
|
2831
|
+
if (result.claude.backedUp) console.log("Backed up previous Claude statusLine for uninstall restore.");
|
|
2832
|
+
}
|
|
2273
2833
|
}
|
|
2274
2834
|
if (result.codex?.ok) {
|
|
2275
2835
|
console.log(`Codex wrapper installed: ${result.codex.wrapperPath}`);
|
|
@@ -2280,12 +2840,23 @@ async function main() {
|
|
|
2280
2840
|
} else if (result.codex?.skipped) {
|
|
2281
2841
|
console.log(`Codex wrapper skipped: ${result.codex.reason}`);
|
|
2282
2842
|
}
|
|
2843
|
+
console.log("To remove AI Battery cleanly later, run: ai-battery uninstall");
|
|
2283
2844
|
const note = codexRestartNote();
|
|
2284
2845
|
if (result.codex && note) console.log(note);
|
|
2285
2846
|
}
|
|
2286
2847
|
return;
|
|
2287
2848
|
}
|
|
2288
2849
|
|
|
2850
|
+
if (args.command === "teardown" || args.command === "uninstall") {
|
|
2851
|
+
const result = runTeardown(args);
|
|
2852
|
+
if (args.json) {
|
|
2853
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2854
|
+
} else if (!args.silent) {
|
|
2855
|
+
printTeardownResult(result);
|
|
2856
|
+
}
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2289
2860
|
if (args.command === "doctor") {
|
|
2290
2861
|
const result = await runDoctor();
|
|
2291
2862
|
if (args.json) {
|
|
@@ -2351,8 +2922,13 @@ async function main() {
|
|
|
2351
2922
|
if (args.command === "install-claude-statusline") {
|
|
2352
2923
|
const result = installClaudeStatusline(args);
|
|
2353
2924
|
if (!args.json) {
|
|
2354
|
-
|
|
2355
|
-
|
|
2925
|
+
if (result.skipped) {
|
|
2926
|
+
console.log(`Claude statusLine skipped: ${result.reason}`);
|
|
2927
|
+
} else {
|
|
2928
|
+
console.log(`Installed Claude statusLine: ${result.command}`);
|
|
2929
|
+
console.log(`Updated ${result.settingsPath}`);
|
|
2930
|
+
if (result.backedUp) console.log("Backed up previous Claude statusLine for uninstall restore.");
|
|
2931
|
+
}
|
|
2356
2932
|
} else {
|
|
2357
2933
|
console.log(JSON.stringify(result, null, 2));
|
|
2358
2934
|
}
|
|
@@ -2363,7 +2939,7 @@ async function main() {
|
|
|
2363
2939
|
const result = uninstallClaudeStatusline();
|
|
2364
2940
|
if (!args.json) {
|
|
2365
2941
|
if (result.changed) {
|
|
2366
|
-
console.log(
|
|
2942
|
+
console.log(`${result.restored ? "Restored previous Claude statusLine in" : "Removed Claude statusLine from"} ${result.settingsPath}`);
|
|
2367
2943
|
} else {
|
|
2368
2944
|
console.log(`${result.reason}: ${result.settingsPath}`);
|
|
2369
2945
|
}
|
|
@@ -2433,10 +3009,18 @@ async function main() {
|
|
|
2433
3009
|
}
|
|
2434
3010
|
|
|
2435
3011
|
export {
|
|
3012
|
+
claudeHeader,
|
|
3013
|
+
codexWrapperScript,
|
|
2436
3014
|
firstPercentValue,
|
|
3015
|
+
installClaudeStatusline,
|
|
3016
|
+
installCodexWrapper,
|
|
2437
3017
|
normalizeLimit,
|
|
3018
|
+
removeOrRestoreCodexWrapper,
|
|
3019
|
+
removeAiBatteryShellPathBlock,
|
|
2438
3020
|
percentValue,
|
|
2439
|
-
sameFilePath
|
|
3021
|
+
sameFilePath,
|
|
3022
|
+
visibleWidth,
|
|
3023
|
+
uninstallClaudeStatusline
|
|
2440
3024
|
};
|
|
2441
3025
|
|
|
2442
3026
|
function sameFilePath(leftPath, rightPath) {
|