openlimiter 1.3.1 → 1.3.3
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/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +130 -78
- package/dist/cli.js.map +1 -1
- package/dist/codex-device-login.d.ts +2 -1
- package/dist/codex-device-login.d.ts.map +1 -1
- package/dist/codex-device-login.js +6 -4
- package/dist/codex-device-login.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/hub-auth.d.ts +12 -14
- package/dist/hub-auth.d.ts.map +1 -1
- package/dist/hub-auth.js +232 -47
- package/dist/hub-auth.js.map +1 -1
- package/dist/hub-sync.d.ts +14 -13
- package/dist/hub-sync.d.ts.map +1 -1
- package/dist/hub-sync.js +139 -18
- package/dist/hub-sync.js.map +1 -1
- package/dist/hub.d.ts +6 -3
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js +47 -11
- package/dist/hub.js.map +1 -1
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +16 -21
- package/dist/ingest.js.map +1 -1
- package/dist/session.d.ts +16 -8
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +144 -31
- package/dist/session.js.map +1 -1
- package/dist/statusline.d.ts +1 -1
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +10 -8
- package/dist/statusline.js.map +1 -1
- package/dist/terminal-backup.d.ts +5 -0
- package/dist/terminal-backup.d.ts.map +1 -0
- package/dist/terminal-backup.js +88 -0
- package/dist/terminal-backup.js.map +1 -0
- package/dist/terminal-launcher.d.ts +12 -0
- package/dist/terminal-launcher.d.ts.map +1 -0
- package/dist/terminal-launcher.js +208 -0
- package/dist/terminal-launcher.js.map +1 -0
- package/dist/terminal-toml.d.ts +38 -0
- package/dist/terminal-toml.d.ts.map +1 -0
- package/dist/terminal-toml.js +357 -0
- package/dist/terminal-toml.js.map +1 -0
- package/dist/terminal.d.ts +10 -37
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +390 -619
- package/dist/terminal.js.map +1 -1
- package/package.json +4 -4
package/dist/terminal.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { readFile, mkdir } from "node:fs/promises";
|
|
1
|
+
import { lstat, readFile, mkdir, realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { connectors } from "@openlimiter/connectors";
|
|
4
4
|
import { writeFileAtomically } from "@openlimiter/core";
|
|
5
5
|
import { DEFAULT_PROVIDERS, DEFAULT_STATUSLINE, readConfig, writeConfig } from "./config.js";
|
|
6
6
|
import { decodeWrappedStatuslineCommand, encodeWrappedStatuslineCommand } from "./statusline-wrapper.js";
|
|
7
|
+
import { parseToml, editToml, tomlValue } from "./terminal-toml.js";
|
|
8
|
+
import { installLauncher, launcherCommand } from "./terminal-launcher.js";
|
|
9
|
+
import { isOwned, readOptional, restoreOwned, writeOwned } from "./terminal-backup.js";
|
|
7
10
|
export const TERMINAL_HOST_NAMES = [
|
|
8
11
|
"claude",
|
|
9
12
|
"antigravity",
|
|
@@ -25,63 +28,73 @@ export const STATUS_WIRED = "Wired";
|
|
|
25
28
|
export const STATUS_OWN_LINE_FOUND = "Your own status line found, install wraps it";
|
|
26
29
|
export const STATUS_NOT_WIRED = "Not wired";
|
|
27
30
|
export const CONNECT_FIRST_SENTENCE = "Connect it first";
|
|
28
|
-
function
|
|
29
|
-
|
|
31
|
+
async function canonicalOverrideRoot(value, fallback, variable) {
|
|
32
|
+
if (value === undefined || value === "")
|
|
33
|
+
return fallback;
|
|
34
|
+
if (!path.isAbsolute(value))
|
|
35
|
+
throw new Error(`${variable} must be an absolute path.`);
|
|
36
|
+
let existing = null;
|
|
37
|
+
try {
|
|
38
|
+
existing = await lstat(value);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== "ENOENT")
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
if (existing?.isSymbolicLink())
|
|
45
|
+
return await realpath(value);
|
|
46
|
+
try {
|
|
47
|
+
return await realpath(value);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT")
|
|
51
|
+
throw error;
|
|
52
|
+
const parent = await realpath(path.dirname(value));
|
|
53
|
+
return path.join(parent, path.basename(value));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isWithinDirectory(root, target) {
|
|
57
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
58
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
59
|
+
}
|
|
60
|
+
async function canonicalConfigFile(root, name, variable) {
|
|
61
|
+
const candidate = path.join(root, name);
|
|
62
|
+
try {
|
|
63
|
+
const target = await realpath(candidate);
|
|
64
|
+
if (!isWithinDirectory(root, target))
|
|
65
|
+
throw new Error(`${variable} must point to a file under its root.`);
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ENOENT")
|
|
70
|
+
throw error;
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
30
73
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
74
|
+
async function claudeSettingsPath(context) {
|
|
75
|
+
const root = await canonicalOverrideRoot(context.environment?.["CLAUDE_CONFIG_DIR"], path.join(context.homeDirectory, ".claude"), "CLAUDE_CONFIG_DIR");
|
|
76
|
+
return await canonicalConfigFile(root, "settings.json", "CLAUDE_CONFIG_DIR");
|
|
33
77
|
}
|
|
34
|
-
function
|
|
35
|
-
return path.join(
|
|
78
|
+
function antigravitySettingsPath(context) {
|
|
79
|
+
return path.join(context.homeDirectory, ".gemini", "antigravity-cli", "settings.json");
|
|
36
80
|
}
|
|
37
|
-
function
|
|
38
|
-
return path.join(
|
|
81
|
+
function grokConfigPath(context) {
|
|
82
|
+
return path.join(context.homeDirectory, ".grok", "config.toml");
|
|
39
83
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return path.join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
44
|
-
}
|
|
45
|
-
return path.join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
|
|
84
|
+
async function codexConfigPath(context) {
|
|
85
|
+
const root = await canonicalOverrideRoot(context.environment?.["CODEX_HOME"], path.join(context.homeDirectory, ".codex"), "CODEX_HOME");
|
|
86
|
+
return await canonicalConfigFile(root, "config.toml", "CODEX_HOME");
|
|
46
87
|
}
|
|
47
88
|
const POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS = 5_000;
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
* `pwsh`, the PowerShell somebody installed on purpose and the one most
|
|
59
|
-
* people who have it actually run, is asked first, on every platform it ships
|
|
60
|
-
* for. `powershell.exe`, Windows only and present on every Windows machine by
|
|
61
|
-
* default, is the fallback there. Neither answering, including no runner
|
|
62
|
-
* being injected at all, falls back to the hardcoded guess: a feature that
|
|
63
|
-
* degrades to its old behaviour rather than one that breaks outright.
|
|
64
|
-
*/
|
|
65
|
-
async function resolvePowerShellProfilePath(home, platform, runner) {
|
|
66
|
-
const fallback = powerShellProfilePath(home, platform);
|
|
67
|
-
if (runner === undefined)
|
|
68
|
-
return fallback;
|
|
69
|
-
const candidates = platform === "win32" ? ["pwsh.exe", "powershell.exe"] : ["pwsh"];
|
|
70
|
-
for (const executable of candidates) {
|
|
71
|
-
let result;
|
|
72
|
-
try {
|
|
73
|
-
result = await runner(executable, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE"], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (!result.ok)
|
|
79
|
-
continue;
|
|
80
|
-
const trimmed = result.stdout.trim();
|
|
81
|
-
if (trimmed.length > 0)
|
|
82
|
-
return trimmed;
|
|
83
|
-
}
|
|
84
|
-
return fallback;
|
|
89
|
+
/** Ask the selected PowerShell, including redirected Documents and PS 7 layouts.
|
|
90
|
+
* A shell that cannot identify its active profile is not a verified target. */
|
|
91
|
+
async function resolvePowerShellProfilePath(executable, runner) {
|
|
92
|
+
if (!runner)
|
|
93
|
+
throw new Error("Unsupported shell");
|
|
94
|
+
const result = await runner(executable, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE"], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
|
|
95
|
+
if (!result.ok || !path.isAbsolute(result.stdout.trim()))
|
|
96
|
+
throw new Error("Unsupported shell");
|
|
97
|
+
return result.stdout.trim();
|
|
85
98
|
}
|
|
86
99
|
export async function readJsonConfig(filePath) {
|
|
87
100
|
try {
|
|
@@ -108,114 +121,13 @@ export async function readJsonConfig(filePath) {
|
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
123
|
export function validateToml(text) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let bracketDepth = 0;
|
|
115
|
-
let braceDepth = 0;
|
|
116
|
-
for (const line of lines) {
|
|
117
|
-
let i = 0;
|
|
118
|
-
let inSingle = false;
|
|
119
|
-
let inDouble = false;
|
|
120
|
-
let cleanedLine = "";
|
|
121
|
-
const wasInsideGroup = bracketDepth > 0 || braceDepth > 0 || inSingleTriple || inDoubleTriple;
|
|
122
|
-
while (i < line.length) {
|
|
123
|
-
if (inDoubleTriple) {
|
|
124
|
-
if (line.slice(i, i + 3) === '"""') {
|
|
125
|
-
inDoubleTriple = false;
|
|
126
|
-
i += 3;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
i++;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (inSingleTriple) {
|
|
133
|
-
if (line.slice(i, i + 3) === "'''") {
|
|
134
|
-
inSingleTriple = false;
|
|
135
|
-
i += 3;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
i++;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (inDouble) {
|
|
142
|
-
if (line[i] === "\\" && i + 1 < line.length) {
|
|
143
|
-
i += 2;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (line[i] === '"') {
|
|
147
|
-
inDouble = false;
|
|
148
|
-
}
|
|
149
|
-
i++;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (inSingle) {
|
|
153
|
-
if (line[i] === "'") {
|
|
154
|
-
inSingle = false;
|
|
155
|
-
}
|
|
156
|
-
i++;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (line.slice(i, i + 3) === '"""') {
|
|
160
|
-
inDoubleTriple = true;
|
|
161
|
-
i += 3;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (line.slice(i, i + 3) === "'''") {
|
|
165
|
-
inSingleTriple = true;
|
|
166
|
-
i += 3;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (line[i] === '"') {
|
|
170
|
-
inDouble = true;
|
|
171
|
-
i++;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (line[i] === "'") {
|
|
175
|
-
inSingle = true;
|
|
176
|
-
i++;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (line[i] === "#") {
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
const ch = line[i];
|
|
183
|
-
cleanedLine += ch;
|
|
184
|
-
if (ch === "[")
|
|
185
|
-
bracketDepth++;
|
|
186
|
-
else if (ch === "]") {
|
|
187
|
-
bracketDepth--;
|
|
188
|
-
if (bracketDepth < 0)
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
else if (ch === "{")
|
|
192
|
-
braceDepth++;
|
|
193
|
-
else if (ch === "}") {
|
|
194
|
-
braceDepth--;
|
|
195
|
-
if (braceDepth < 0)
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
i++;
|
|
199
|
-
}
|
|
200
|
-
if (inSingle || inDouble) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
if (!wasInsideGroup && !inSingleTriple && !inDoubleTriple && bracketDepth === 0 && braceDepth === 0) {
|
|
204
|
-
const trimmed = cleanedLine.trim();
|
|
205
|
-
if (trimmed.length > 0) {
|
|
206
|
-
if (!(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
207
|
-
if (!trimmed.includes("=")) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
124
|
+
try {
|
|
125
|
+
parseToml(text);
|
|
126
|
+
return true;
|
|
213
127
|
}
|
|
214
|
-
|
|
215
|
-
return false;
|
|
216
|
-
if (bracketDepth !== 0 || braceDepth !== 0)
|
|
128
|
+
catch {
|
|
217
129
|
return false;
|
|
218
|
-
|
|
130
|
+
}
|
|
219
131
|
}
|
|
220
132
|
export async function readTomlConfig(filePath) {
|
|
221
133
|
try {
|
|
@@ -239,10 +151,6 @@ async function readJsonFile(filePath) {
|
|
|
239
151
|
const res = await readJsonConfig(filePath);
|
|
240
152
|
return res.kind === "ok" ? res.data : null;
|
|
241
153
|
}
|
|
242
|
-
async function writeJsonFile(filePath, data) {
|
|
243
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
244
|
-
await writeFileAtomically(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
245
|
-
}
|
|
246
154
|
async function readTextFile(filePath) {
|
|
247
155
|
try {
|
|
248
156
|
return await readFile(filePath, "utf8");
|
|
@@ -251,455 +159,289 @@ async function readTextFile(filePath) {
|
|
|
251
159
|
return null;
|
|
252
160
|
}
|
|
253
161
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (typeof cmd === "string")
|
|
273
|
-
existingCommand = cmd;
|
|
274
|
-
}
|
|
275
|
-
else if (typeof currentStatusLine === "string") {
|
|
276
|
-
existingCommand = currentStatusLine;
|
|
277
|
-
}
|
|
278
|
-
/*
|
|
279
|
-
* A command that already runs us, wrapped or not, is left exactly as it
|
|
280
|
-
* is: re-running install must never re-wrap an already wrapped command,
|
|
281
|
-
* and must never fall back to the bare default and silently drop the
|
|
282
|
-
* user's original status line the wrap exists to protect.
|
|
283
|
-
*/
|
|
284
|
-
let commandToSet = "openlimiter statusline --host claude";
|
|
285
|
-
if (existingCommand !== null) {
|
|
286
|
-
commandToSet = existingCommand.includes("openlimiter statusline")
|
|
287
|
-
? existingCommand
|
|
288
|
-
: `openlimiter statusline --host claude --wrap ${encodeWrappedStatuslineCommand(existingCommand)}`;
|
|
289
|
-
}
|
|
290
|
-
existing["openlimiter managed"] = true;
|
|
291
|
-
existing["statusLine"] = {
|
|
292
|
-
type: "command",
|
|
293
|
-
command: commandToSet
|
|
294
|
-
};
|
|
295
|
-
try {
|
|
296
|
-
await writeJsonFile(settingsFile, existing);
|
|
297
|
-
return { ok: true, message: "Wired Claude Code status line." };
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
return { ok: false, message: "Could not write Claude Code settings." };
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
export async function uninstallClaude(context) {
|
|
304
|
-
const settingsFile = claudeSettingsPath(context.homeDirectory);
|
|
305
|
-
const readRes = await readJsonConfig(settingsFile);
|
|
306
|
-
if (readRes.kind === "parse_error") {
|
|
307
|
-
return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
|
|
308
|
-
}
|
|
309
|
-
if (readRes.kind === "missing" || !readRes.data["statusLine"]) {
|
|
310
|
-
return { ok: true, message: "Claude Code status line is not installed." };
|
|
311
|
-
}
|
|
312
|
-
const existing = readRes.data;
|
|
313
|
-
const current = existing["statusLine"];
|
|
314
|
-
let cmd = null;
|
|
315
|
-
if (typeof current === "object" && current !== null) {
|
|
316
|
-
const c = current["command"];
|
|
317
|
-
if (typeof c === "string")
|
|
318
|
-
cmd = c;
|
|
319
|
-
}
|
|
320
|
-
else if (typeof current === "string") {
|
|
321
|
-
cmd = current;
|
|
322
|
-
}
|
|
323
|
-
if (cmd !== null && cmd.includes("--wrap")) {
|
|
324
|
-
const match = /--wrap\s+([A-Za-z0-9_-]+)/.exec(cmd);
|
|
325
|
-
const restored = match?.[1] ? decodeWrappedStatuslineCommand(match[1]) : null;
|
|
326
|
-
if (restored !== null) {
|
|
327
|
-
existing["statusLine"] = {
|
|
328
|
-
type: "command",
|
|
329
|
-
command: restored
|
|
330
|
-
};
|
|
331
|
-
delete existing["openlimiter managed"];
|
|
332
|
-
await writeJsonFile(settingsFile, existing);
|
|
333
|
-
return { ok: true, message: "Restored original Claude Code status line." };
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
delete existing["statusLine"];
|
|
337
|
-
delete existing["openlimiter managed"];
|
|
162
|
+
const HOST_CONFIG = {
|
|
163
|
+
claude: { path: claudeSettingsPath, label: "Claude Code", file: "Claude Code settings", json: true, wired: "Wired Claude Code status line." },
|
|
164
|
+
antigravity: { path: antigravitySettingsPath, label: "Antigravity", file: "Antigravity settings", json: true, wired: "Wired Antigravity CLI status line." },
|
|
165
|
+
grok: { path: grokConfigPath, label: "Grok", file: "Grok config", json: false, wired: "Wired Grok Build status line." },
|
|
166
|
+
codex: { path: codexConfigPath, label: "Codex", file: "Codex config", json: false, wired: "Wired Codex status line." }
|
|
167
|
+
};
|
|
168
|
+
const CODEX_ITEMS = ["five-hour-limit", "weekly-limit", "context-used", "model-with-reasoning", "current-dir"];
|
|
169
|
+
async function durableCommand(context, shell) {
|
|
170
|
+
const runtime = await installLauncher(context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter"));
|
|
171
|
+
return launcherCommand(runtime, shell);
|
|
172
|
+
}
|
|
173
|
+
function ownedMarker(text, json) {
|
|
174
|
+
return json ? JSON.parse(text)["openlimiter managed"] === true
|
|
175
|
+
: text.split(/\r?\n/).some(line => line.trim() === "# openlimiter managed");
|
|
176
|
+
}
|
|
177
|
+
async function changeConfigHost(host, context, install) {
|
|
178
|
+
const spec = HOST_CONFIG[host];
|
|
179
|
+
let file;
|
|
338
180
|
try {
|
|
339
|
-
await
|
|
340
|
-
return { ok: true, message: "Uninstalled Claude Code status line." };
|
|
181
|
+
file = await spec.path(context);
|
|
341
182
|
}
|
|
342
|
-
catch {
|
|
343
|
-
return {
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
message: error instanceof Error ? error.message : `Could not resolve ${spec.file}.`
|
|
187
|
+
};
|
|
344
188
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
*/
|
|
349
|
-
export async function installAntigravity(context) {
|
|
350
|
-
const settingsFile = antigravitySettingsPath(context.homeDirectory);
|
|
351
|
-
const readRes = await readJsonConfig(settingsFile);
|
|
352
|
-
if (readRes.kind === "parse_error") {
|
|
353
|
-
return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
|
|
354
|
-
}
|
|
355
|
-
const existing = readRes.kind === "ok" ? readRes.data : {};
|
|
356
|
-
const current = existing["statusLine"];
|
|
357
|
-
let existingCommand = null;
|
|
358
|
-
if (typeof current === "string") {
|
|
359
|
-
existingCommand = current;
|
|
360
|
-
}
|
|
361
|
-
else if (typeof current === "object" && current !== null) {
|
|
362
|
-
const cmd = current["command"];
|
|
363
|
-
if (typeof cmd === "string")
|
|
364
|
-
existingCommand = cmd;
|
|
365
|
-
}
|
|
366
|
-
/* See installClaude: an already installed command, wrapped or not, is
|
|
367
|
-
left exactly as it is on a second install. */
|
|
368
|
-
let commandToSet = "openlimiter statusline --host antigravity";
|
|
369
|
-
if (existingCommand !== null) {
|
|
370
|
-
commandToSet = existingCommand.includes("openlimiter statusline")
|
|
371
|
-
? existingCommand
|
|
372
|
-
: `openlimiter statusline --host antigravity --wrap ${encodeWrappedStatuslineCommand(existingCommand)}`;
|
|
373
|
-
}
|
|
374
|
-
existing["openlimiter managed"] = true;
|
|
375
|
-
existing["statusLine"] = commandToSet;
|
|
189
|
+
const read = spec.json ? await readJsonConfig(file) : await readTomlConfig(file);
|
|
190
|
+
if (read.kind === "parse_error")
|
|
191
|
+
return { ok: false, message: `Could not read ${file}, fix it or move it aside` };
|
|
376
192
|
try {
|
|
377
|
-
await
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const settingsFile = antigravitySettingsPath(context.homeDirectory);
|
|
386
|
-
const readRes = await readJsonConfig(settingsFile);
|
|
387
|
-
if (readRes.kind === "parse_error") {
|
|
388
|
-
return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
|
|
389
|
-
}
|
|
390
|
-
if (readRes.kind === "missing" || !readRes.data["statusLine"]) {
|
|
391
|
-
return { ok: true, message: "Antigravity status line is not installed." };
|
|
392
|
-
}
|
|
393
|
-
const existing = readRes.data;
|
|
394
|
-
const current = existing["statusLine"];
|
|
395
|
-
let cmd = null;
|
|
396
|
-
if (typeof current === "string") {
|
|
397
|
-
cmd = current;
|
|
398
|
-
}
|
|
399
|
-
else if (typeof current === "object" && current !== null) {
|
|
400
|
-
const c = current["command"];
|
|
401
|
-
if (typeof c === "string")
|
|
402
|
-
cmd = c;
|
|
403
|
-
}
|
|
404
|
-
if (cmd !== null && cmd.includes("--wrap")) {
|
|
405
|
-
const match = /--wrap\s+([A-Za-z0-9_-]+)/.exec(cmd);
|
|
406
|
-
const restored = match?.[1] ? decodeWrappedStatuslineCommand(match[1]) : null;
|
|
407
|
-
if (restored !== null) {
|
|
408
|
-
existing["statusLine"] = restored;
|
|
409
|
-
delete existing["openlimiter managed"];
|
|
410
|
-
await writeJsonFile(settingsFile, existing);
|
|
411
|
-
return { ok: true, message: "Restored original Antigravity status line." };
|
|
193
|
+
const original = await readOptional(file);
|
|
194
|
+
const text = original ?? (spec.json ? "{}" : "");
|
|
195
|
+
const marker = ownedMarker(text, spec.json);
|
|
196
|
+
if (!install) {
|
|
197
|
+
const restored = original !== null && await restoreOwned(file, original, marker);
|
|
198
|
+
if (marker && !restored)
|
|
199
|
+
return { ok: false, message: `Could not update ${file}.` };
|
|
200
|
+
return { ok: true, message: restored ? `Uninstalled ${spec.label} status line.` : `${spec.label} status line is not installed.` };
|
|
412
201
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const cmdMatch = /command\s*=\s*"([^"]+)"/.exec(rawSection);
|
|
439
|
-
if (cmdMatch && cmdMatch[1]) {
|
|
440
|
-
existingCommand = cmdMatch[1];
|
|
202
|
+
const previous = spec.json
|
|
203
|
+
? claudeLikeStatusLineCommand(JSON.parse(text)["statusLine"])
|
|
204
|
+
: host === "grok"
|
|
205
|
+
? (() => {
|
|
206
|
+
const value = tomlValue(text, ["ui", "status_line", "command"]);
|
|
207
|
+
return typeof value === "string" ? value : null;
|
|
208
|
+
})()
|
|
209
|
+
: null;
|
|
210
|
+
const legacyOpenLimiter = previous !== null && isOpenLimiterStatuslineCommand(previous);
|
|
211
|
+
const owned = original !== null && await isOwned(file, original, marker);
|
|
212
|
+
if (marker && !owned && !legacyOpenLimiter)
|
|
213
|
+
return { ok: false, message: `Could not write ${file}.` };
|
|
214
|
+
let updated;
|
|
215
|
+
if (spec.json) {
|
|
216
|
+
const data = JSON.parse(text);
|
|
217
|
+
const base = await durableCommand(context, context.platform === "win32" ? "cmd" : "posix");
|
|
218
|
+
const userCommand = previous === null
|
|
219
|
+
? null
|
|
220
|
+
: legacyOpenLimiter ? unwrapOpenLimiterStatuslineCommand(previous) : previous;
|
|
221
|
+
const command = `${base} statusline --host ${host}` +
|
|
222
|
+
(userCommand === null ? "" : ` --wrap ${encodeWrappedStatuslineCommand(userCommand)}`);
|
|
223
|
+
data["openlimiter managed"] = true;
|
|
224
|
+
data["statusLine"] = host === "claude" ? { type: "command", command } : command;
|
|
225
|
+
updated = JSON.stringify(data, null, 2) + "\n";
|
|
226
|
+
JSON.parse(updated);
|
|
441
227
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
left exactly as it is on a second install. */
|
|
445
|
-
let commandToSet = "openlimiter statusline --host grok";
|
|
446
|
-
if (existingCommand !== null) {
|
|
447
|
-
commandToSet = existingCommand.includes("openlimiter statusline")
|
|
448
|
-
? existingCommand
|
|
449
|
-
: `openlimiter statusline --host grok --wrap ${encodeWrappedStatuslineCommand(existingCommand)}`;
|
|
450
|
-
}
|
|
451
|
-
let updated;
|
|
452
|
-
if (sectionMatch && rawSection !== undefined) {
|
|
453
|
-
let section = rawSection
|
|
454
|
-
.replace(/[ \t]*# openlimiter managed\r?\n?/g, "")
|
|
455
|
-
.replace(/[ \t]*type\s*=\s*"[^"]*"\r?\n?/g, "")
|
|
456
|
-
.replace(/[ \t]*command\s*=\s*"[^"]*"\r?\n?/g, "");
|
|
457
|
-
const lines = section.split(/\r?\n/);
|
|
458
|
-
const header = lines[0] ?? "";
|
|
459
|
-
const rest = lines.slice(1).filter((l) => l.trim().length > 0);
|
|
460
|
-
const managedLines = [
|
|
461
|
-
"# openlimiter managed",
|
|
462
|
-
'type = "command"',
|
|
463
|
-
`command = "${commandToSet}"`
|
|
464
|
-
];
|
|
465
|
-
const newSection = [header, ...managedLines, ...rest].join("\n");
|
|
466
|
-
const startIndex = sectionMatch.index + (text[sectionMatch.index] === "\n" ? 1 : 0);
|
|
467
|
-
updated = text.slice(0, startIndex) + newSection + text.slice(sectionMatch.index + sectionMatch[0].length);
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
const managedSection = [
|
|
471
|
-
"[ui.status_line]",
|
|
472
|
-
"# openlimiter managed",
|
|
473
|
-
'type = "command"',
|
|
474
|
-
`command = "${commandToSet}"`
|
|
475
|
-
].join("\n");
|
|
476
|
-
updated = text ? text.trimEnd() + "\n\n" + managedSection + "\n" : managedSection + "\n";
|
|
477
|
-
}
|
|
478
|
-
if (!updated.endsWith("\n")) {
|
|
479
|
-
updated += "\n";
|
|
480
|
-
}
|
|
481
|
-
try {
|
|
482
|
-
await writeTextFile(configFile, updated);
|
|
483
|
-
return { ok: true, message: "Wired Grok Build status line." };
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
return { ok: false, message: "Could not write Grok config." };
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
export async function uninstallGrok(context) {
|
|
490
|
-
const configFile = grokConfigPath(context.homeDirectory);
|
|
491
|
-
const readRes = await readTomlConfig(configFile);
|
|
492
|
-
if (readRes.kind === "parse_error") {
|
|
493
|
-
return { ok: false, message: `Could not read ${configFile}, fix it or move it aside` };
|
|
494
|
-
}
|
|
495
|
-
if (readRes.kind === "missing") {
|
|
496
|
-
return { ok: true, message: "Grok status line is not installed." };
|
|
497
|
-
}
|
|
498
|
-
const text = readRes.data;
|
|
499
|
-
const sectionMatch = /(?:^|\n)(\[ui\.status_line\][\s\S]*?)(?=\n\[|$)/.exec(text);
|
|
500
|
-
if (!sectionMatch || sectionMatch[1] === undefined) {
|
|
501
|
-
return { ok: true, message: "Grok status line is not installed." };
|
|
502
|
-
}
|
|
503
|
-
const section = sectionMatch[1];
|
|
504
|
-
const cmdMatch = /command\s*=\s*"([^"]+)"/.exec(section);
|
|
505
|
-
const cmd = cmdMatch?.[1] ?? null;
|
|
506
|
-
const startIndex = sectionMatch.index + (text[sectionMatch.index] === "\n" ? 1 : 0);
|
|
507
|
-
if (cmd !== null && cmd.includes("--wrap")) {
|
|
508
|
-
const match = /--wrap\s+([A-Za-z0-9_-]+)/.exec(cmd);
|
|
509
|
-
const restored = match?.[1] ? decodeWrappedStatuslineCommand(match[1]) : null;
|
|
510
|
-
if (restored !== null) {
|
|
511
|
-
let updatedSection = section.replace(/[ \t]*# openlimiter managed\r?\n?/g, "");
|
|
512
|
-
updatedSection = updatedSection.replace(/command\s*=\s*"[^"]*"/, `command = "${restored}"`);
|
|
513
|
-
const updated = text.slice(0, startIndex) + updatedSection + text.slice(sectionMatch.index + sectionMatch[0].length);
|
|
514
|
-
try {
|
|
515
|
-
await writeTextFile(configFile, updated);
|
|
516
|
-
return { ok: true, message: "Restored original Grok status line." };
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
return { ok: false, message: "Could not update Grok config." };
|
|
520
|
-
}
|
|
228
|
+
else if (host === "codex") {
|
|
229
|
+
updated = editToml(text, ["tui"], { status_line: CODEX_ITEMS, status_line_use_colors: true });
|
|
521
230
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const newSection = [lines[0] ?? "", ...rest].join("\n");
|
|
537
|
-
updated = text.slice(0, startIndex) + newSection + text.slice(sectionMatch.index + sectionMatch[0].length);
|
|
538
|
-
}
|
|
539
|
-
try {
|
|
540
|
-
await writeTextFile(configFile, updated);
|
|
541
|
-
return { ok: true, message: "Uninstalled Grok status line." };
|
|
231
|
+
else {
|
|
232
|
+
const base = await durableCommand(context, context.platform === "win32" ? "cmd" : "posix");
|
|
233
|
+
const userCommand = previous === null
|
|
234
|
+
? null
|
|
235
|
+
: legacyOpenLimiter ? unwrapOpenLimiterStatuslineCommand(previous) : previous;
|
|
236
|
+
const command = `${base} statusline --host grok` +
|
|
237
|
+
(typeof userCommand === "string" ? ` --wrap ${encodeWrappedStatuslineCommand(userCommand)}` : "");
|
|
238
|
+
updated = editToml(text, ["ui", "status_line"], { type: "command", command });
|
|
239
|
+
}
|
|
240
|
+
if (owned && updated === text)
|
|
241
|
+
return { ok: true, message: spec.wired };
|
|
242
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
243
|
+
await writeOwned(file, original, updated);
|
|
244
|
+
return { ok: true, message: spec.wired };
|
|
542
245
|
}
|
|
543
246
|
catch {
|
|
544
|
-
return { ok: false, message:
|
|
545
|
-
}
|
|
247
|
+
return { ok: false, message: `Could not ${install ? "write" : "update"} ${file}.` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export const installClaude = (context) => changeConfigHost("claude", context, true);
|
|
251
|
+
export const uninstallClaude = (context) => changeConfigHost("claude", context, false);
|
|
252
|
+
export const installAntigravity = (context) => changeConfigHost("antigravity", context, true);
|
|
253
|
+
export const uninstallAntigravity = (context) => changeConfigHost("antigravity", context, false);
|
|
254
|
+
export const installGrok = (context) => changeConfigHost("grok", context, true);
|
|
255
|
+
export const uninstallGrok = (context) => changeConfigHost("grok", context, false);
|
|
256
|
+
export const installCodex = (context) => changeConfigHost("codex", context, true);
|
|
257
|
+
export const uninstallCodex = (context) => changeConfigHost("codex", context, false);
|
|
258
|
+
function shellSnippets(posixCommand, powerShellCommand) {
|
|
259
|
+
const command = posixCommand + " statusline --host shell";
|
|
260
|
+
return {
|
|
261
|
+
starship: [
|
|
262
|
+
"[custom.openlimiter]", "command = " + JSON.stringify(command),
|
|
263
|
+
'when = "true"', 'shell = ["bash", "--noprofile", "--norc"]',
|
|
264
|
+
'format = "[$output]($style) "'
|
|
265
|
+
].join("\n"),
|
|
266
|
+
tmux: "set -g status-right " + JSON.stringify("#(" + command + ")") + "\nset -g status-interval 60",
|
|
267
|
+
ohMyPosh: JSON.stringify({ type: "command", properties: {
|
|
268
|
+
command: powerShellCommand + " statusline --host shell", shell: "powershell", cache: { duration: "60s" }
|
|
269
|
+
} }, null, 2)
|
|
270
|
+
};
|
|
546
271
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
272
|
+
class UnsupportedShellError extends Error {
|
|
273
|
+
shell;
|
|
274
|
+
constructor(shell) {
|
|
275
|
+
super("Unsupported shell");
|
|
276
|
+
this.shell = shell;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function shellTarget(context) {
|
|
280
|
+
const env = context.environment ?? process.env;
|
|
281
|
+
let executable = env["SHELL"] || "";
|
|
282
|
+
if (!executable && context.platform === "win32" && context.shellRunner) {
|
|
283
|
+
// npm can sit between Node and the interactive shell. Walk ancestors until
|
|
284
|
+
// the actual PowerShell or Unix shell is found, rather than trusting COMSPEC
|
|
285
|
+
// (which remains cmd.exe even inside PowerShell).
|
|
286
|
+
const query = "$ancestorId = " + process.ppid + "; for ($n = 0; $n -lt 12 -and $ancestorId; $n++) { " +
|
|
287
|
+
"$p = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $ancestorId); " +
|
|
288
|
+
"if ($p.Name -match '^(pwsh|powershell|bash|zsh)(\\.exe)?$') { $p.ExecutablePath; break }; " +
|
|
289
|
+
"$ancestorId = $p.ParentProcessId }";
|
|
290
|
+
const found = await context.shellRunner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
|
|
291
|
+
if (found.ok)
|
|
292
|
+
executable = found.stdout.trim();
|
|
293
|
+
}
|
|
294
|
+
const name = path.basename(executable).toLowerCase();
|
|
295
|
+
if (name === "bash" || name === "bash.exe") {
|
|
296
|
+
return { kind: "bash", file: path.join(context.homeDirectory, ".bashrc") };
|
|
297
|
+
}
|
|
298
|
+
if (name === "zsh") {
|
|
299
|
+
return { kind: "zsh", file: path.join(env["ZDOTDIR"] || context.homeDirectory, ".zshrc") };
|
|
300
|
+
}
|
|
301
|
+
if (name === "pwsh" || name === "pwsh.exe" || name === "powershell" || name === "powershell.exe") {
|
|
302
|
+
return { kind: "powershell", file: await resolvePowerShellProfilePath(executable, context.shellRunner) };
|
|
303
|
+
}
|
|
304
|
+
if (name !== "")
|
|
305
|
+
throw new UnsupportedShellError(name);
|
|
306
|
+
throw new Error("Unsupported shell");
|
|
307
|
+
}
|
|
308
|
+
function escapedRegExp(text) {
|
|
309
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
310
|
+
}
|
|
311
|
+
function userFunctionExists(text, name, powershell) {
|
|
312
|
+
const escaped = escapedRegExp(name);
|
|
313
|
+
return powershell
|
|
314
|
+
? new RegExp(`(?:^|\\r?\\n)\\s*function\\s+(?:global:)?${escaped}\\b`, "i").test(text)
|
|
315
|
+
: new RegExp(`(?:^|\\r?\\n)\\s*(?:function\\s+)?${escaped}\\s*(?:\\(\\s*\\))?\\s*\\{`).test(text);
|
|
316
|
+
}
|
|
317
|
+
function uniqueShellName(text, base, powershell) {
|
|
318
|
+
for (let suffix = 0;; suffix += 1) {
|
|
319
|
+
const candidate = suffix === 0 ? base : `${base}_${suffix + 1}`;
|
|
320
|
+
if (!userFunctionExists(text, candidate, powershell))
|
|
321
|
+
return candidate;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const MANAGED_SHELL_BLOCK = /(^|\r?\n)# openlimiter managed\r?\n[\s\S]*?\r?\n# end openlimiter managed/g;
|
|
325
|
+
function hasManagedShellBlock(text) {
|
|
326
|
+
return /(?:^|\r?\n)# openlimiter managed\r?\n[\s\S]*?\r?\n# end openlimiter managed/.test(text);
|
|
327
|
+
}
|
|
328
|
+
function withoutManagedShellBlock(text) {
|
|
329
|
+
return text.replace(MANAGED_SHELL_BLOCK, "$1");
|
|
330
|
+
}
|
|
331
|
+
function replaceManagedShellBlock(text, snippet) {
|
|
332
|
+
return text.replace(MANAGED_SHELL_BLOCK, `$1${snippet}`);
|
|
333
|
+
}
|
|
334
|
+
function shellSnippet(kind, command, source = "") {
|
|
335
|
+
const begin = "# openlimiter managed";
|
|
336
|
+
const end = "# end openlimiter managed";
|
|
337
|
+
const hook = uniqueShellName(source, kind === "powershell" ? "__openlimiter_prompt_hook" : "__openlimiter_statusline", kind === "powershell");
|
|
338
|
+
if (kind === "bash")
|
|
339
|
+
return [
|
|
340
|
+
begin,
|
|
341
|
+
`if ! declare -F ${hook} >/dev/null 2>&1; then`,
|
|
342
|
+
` ${hook}() { ${command} statusline --host shell; }`,
|
|
343
|
+
"fi",
|
|
344
|
+
'if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then',
|
|
345
|
+
` [[ " \${PROMPT_COMMAND[*]} " == *" ${hook} "* ]] || PROMPT_COMMAND+=(${hook})`,
|
|
346
|
+
`elif [[ ";\${PROMPT_COMMAND};" != *";${hook};"* ]]; then`,
|
|
347
|
+
` PROMPT_COMMAND="\${PROMPT_COMMAND:+\${PROMPT_COMMAND};}${hook}"`,
|
|
348
|
+
"fi", end
|
|
349
|
+
].join("\n");
|
|
350
|
+
if (kind === "zsh")
|
|
351
|
+
return [
|
|
352
|
+
begin,
|
|
353
|
+
`if (( ! $+functions[${hook}] )); then`,
|
|
354
|
+
` ${hook}() { ${command} statusline --host shell; }`,
|
|
355
|
+
"fi",
|
|
356
|
+
"autoload -Uz add-zsh-hook",
|
|
357
|
+
`if (( \${precmd_functions[(I)${hook}]} == 0 )); then add-zsh-hook precmd ${hook}; fi`, end
|
|
358
|
+
].join("\n");
|
|
359
|
+
const originalPrompt = uniqueShellName(source, "__openlimiter_original_prompt", true);
|
|
360
|
+
const guard = uniqueShellName(source, "__openlimiter_prompt_guard", true);
|
|
361
|
+
return [
|
|
362
|
+
begin,
|
|
363
|
+
`if (-not (Test-Path Function:global:${originalPrompt})) {`,
|
|
364
|
+
` $function:global:${originalPrompt} = $function:prompt`,
|
|
365
|
+
"}",
|
|
366
|
+
`if (-not (Test-Path Function:global:${hook})) {`,
|
|
367
|
+
` function global:${hook} {`,
|
|
368
|
+
` $bar = ${command} statusline --host shell`,
|
|
369
|
+
" if ($bar) { Write-Host $bar }",
|
|
370
|
+
` & $function:global:${originalPrompt}`,
|
|
646
371
|
" }",
|
|
647
|
-
"}"
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
"
|
|
652
|
-
|
|
653
|
-
"
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
372
|
+
"}",
|
|
373
|
+
`if (-not (Test-Path Function:global:${guard})) {`,
|
|
374
|
+
` function global:${guard} { & $function:global:${hook} }`,
|
|
375
|
+
"}",
|
|
376
|
+
"function global:prompt {",
|
|
377
|
+
` & $function:global:${guard}`,
|
|
378
|
+
"}", end
|
|
379
|
+
].join("\n");
|
|
380
|
+
}
|
|
381
|
+
async function manualShellSnippetPath(context) {
|
|
382
|
+
const directory = context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter");
|
|
383
|
+
const file = path.join(directory, "shell-snippet.txt");
|
|
384
|
+
await mkdir(directory, { recursive: true }).then(() => writeFileAtomically(file, "openlimiter statusline --host shell\n")).catch(() => undefined);
|
|
385
|
+
return file;
|
|
386
|
+
}
|
|
662
387
|
export async function installShell(context) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
388
|
+
try {
|
|
389
|
+
const target = await shellTarget(context);
|
|
390
|
+
const original = await readOptional(target.file);
|
|
391
|
+
const runtime = await installLauncher(context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter"));
|
|
392
|
+
const posixCommand = launcherCommand(runtime, "posix");
|
|
393
|
+
const powerShellCommand = launcherCommand(runtime, "powershell");
|
|
394
|
+
const command = target.kind === "powershell" ? powerShellCommand : posixCommand;
|
|
395
|
+
const snippets = shellSnippets(posixCommand, powerShellCommand);
|
|
396
|
+
const alreadyOwned = original !== null && await isOwned(target.file, original, ownedMarker(original, false));
|
|
397
|
+
const source = withoutManagedShellBlock(original ?? "");
|
|
398
|
+
const snippet = shellSnippet(target.kind, command, source);
|
|
399
|
+
const marker = original !== null && ownedMarker(original, false);
|
|
400
|
+
if (alreadyOwned || (marker && hasManagedShellBlock(original ?? ""))) {
|
|
401
|
+
const updated = replaceManagedShellBlock(original, snippet);
|
|
402
|
+
if (updated !== original) {
|
|
403
|
+
await writeOwned(target.file, original, updated);
|
|
404
|
+
}
|
|
669
405
|
}
|
|
670
|
-
|
|
671
|
-
|
|
406
|
+
else if (marker) {
|
|
407
|
+
return { ok: false, message: `Could not write ${target.file}.` };
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const updated = (original ?? "") + "\n\n" + snippet + "\n";
|
|
411
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
412
|
+
await writeOwned(target.file, original, updated);
|
|
413
|
+
}
|
|
414
|
+
const message = [
|
|
415
|
+
"Wired shell prompt integration.", "",
|
|
416
|
+
"Starship snippet (~/.config/starship.toml):", snippets.starship, "",
|
|
417
|
+
"tmux snippet (~/.tmux.conf):", snippets.tmux, "",
|
|
418
|
+
"Oh My Posh segment:", snippets.ohMyPosh
|
|
419
|
+
].join("\n");
|
|
420
|
+
return { ok: true, message };
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
if (error instanceof UnsupportedShellError) {
|
|
424
|
+
const file = await manualShellSnippetPath(context);
|
|
425
|
+
return { ok: true, message: `Skipped ${error.shell} shell. See ${file} for the manual snippet.` };
|
|
672
426
|
}
|
|
427
|
+
return { ok: false, message: "Could not write configuration." };
|
|
673
428
|
}
|
|
674
|
-
const message = [
|
|
675
|
-
"Wired shell prompt integration.",
|
|
676
|
-
"",
|
|
677
|
-
"Starship snippet (~/.config/starship.toml):",
|
|
678
|
-
SHELL_SNIPPETS.starship,
|
|
679
|
-
"",
|
|
680
|
-
"tmux snippet (~/.tmux.conf):",
|
|
681
|
-
SHELL_SNIPPETS.tmux,
|
|
682
|
-
"",
|
|
683
|
-
"Oh My Posh segment:",
|
|
684
|
-
SHELL_SNIPPETS.ohMyPosh
|
|
685
|
-
].join("\n");
|
|
686
|
-
return { ok: true, message };
|
|
687
429
|
}
|
|
688
430
|
export async function uninstallShell(context) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
// Ignored
|
|
431
|
+
try {
|
|
432
|
+
const target = await shellTarget(context);
|
|
433
|
+
const original = await readOptional(target.file);
|
|
434
|
+
if (original !== null) {
|
|
435
|
+
const marker = ownedMarker(original, false);
|
|
436
|
+
const restored = await restoreOwned(target.file, original, marker);
|
|
437
|
+
if (marker && !restored)
|
|
438
|
+
return { ok: false, message: `Could not update ${target.file}.` };
|
|
700
439
|
}
|
|
440
|
+
return { ok: true, message: "Uninstalled shell prompt integration." };
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return { ok: false, message: "Could not write configuration." };
|
|
701
444
|
}
|
|
702
|
-
return { ok: true, message: "Uninstalled shell prompt integration." };
|
|
703
445
|
}
|
|
704
446
|
/**
|
|
705
447
|
* The command a Claude Code or Antigravity style status line field carries,
|
|
@@ -718,25 +460,35 @@ function claudeLikeStatusLineCommand(value) {
|
|
|
718
460
|
}
|
|
719
461
|
return null;
|
|
720
462
|
}
|
|
463
|
+
function isOpenLimiterStatuslineCommand(command) {
|
|
464
|
+
return /(?:^|[\\/"'\s])openlimiter(?:\.cjs|\.cmd|\.exe)?(?=$|[\\/"'\s])/i.test(command) &&
|
|
465
|
+
/\bstatusline\s+--host\s+[A-Za-z0-9_-]+\b/i.test(command);
|
|
466
|
+
}
|
|
467
|
+
function unwrapOpenLimiterStatuslineCommand(command) {
|
|
468
|
+
if (!isOpenLimiterStatuslineCommand(command))
|
|
469
|
+
return command;
|
|
470
|
+
const encoded = /\s--wrap\s+([A-Za-z0-9_-]+)/i.exec(command)?.[1];
|
|
471
|
+
return encoded === undefined ? null : decodeWrappedStatuslineCommand(encoded);
|
|
472
|
+
}
|
|
721
473
|
/** Wired, somebody else's, or nothing there, from the command alone. */
|
|
722
474
|
function classifyCommand(command) {
|
|
723
475
|
if (command === null)
|
|
724
476
|
return STATUS_NOT_WIRED;
|
|
725
|
-
return command
|
|
477
|
+
return isOpenLimiterStatuslineCommand(command) ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
|
|
726
478
|
}
|
|
727
479
|
/** The command inside Grok's `[ui.status_line]` table, or nothing found. */
|
|
728
480
|
function grokStatusLineCommand(text) {
|
|
729
481
|
if (text === null)
|
|
730
482
|
return null;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
483
|
+
try {
|
|
484
|
+
const parsed = parseToml(text);
|
|
485
|
+
const command = tomlValue(text, ["ui", "status_line", "command"]);
|
|
486
|
+
return typeof command === "string" ? command
|
|
487
|
+
: parsed.tables.some(t => JSON.stringify(t.keys) === '["ui","status_line"]') ? "" : null;
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
734
490
|
return null;
|
|
735
|
-
|
|
736
|
-
/* The table exists whether or not it carries a command line this build can
|
|
737
|
-
read, and an existing table with no readable command is still somebody
|
|
738
|
-
else's, not nothing. An empty string is never confused with absence. */
|
|
739
|
-
return commandMatch?.[1] ?? "";
|
|
491
|
+
}
|
|
740
492
|
}
|
|
741
493
|
/**
|
|
742
494
|
* Wired, somebody else's, or nothing there, for Codex's `[tui]` table.
|
|
@@ -749,13 +501,14 @@ function grokStatusLineCommand(text) {
|
|
|
749
501
|
function classifyCodexSection(text) {
|
|
750
502
|
if (text === null)
|
|
751
503
|
return STATUS_NOT_WIRED;
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
504
|
+
try {
|
|
505
|
+
if (tomlValue(text, ["tui", "status_line"]) === undefined)
|
|
506
|
+
return STATUS_NOT_WIRED;
|
|
507
|
+
return ownedMarker(text, false) ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
755
510
|
return STATUS_NOT_WIRED;
|
|
756
|
-
|
|
757
|
-
return STATUS_WIRED;
|
|
758
|
-
return /status_line\s*=/.test(section) ? STATUS_OWN_LINE_FOUND : STATUS_NOT_WIRED;
|
|
511
|
+
}
|
|
759
512
|
}
|
|
760
513
|
/**
|
|
761
514
|
* Check wiring status for a host.
|
|
@@ -772,28 +525,44 @@ export async function hostStatus(host, context) {
|
|
|
772
525
|
return UNSUPPORTED_HOST_ALTERNATIVE;
|
|
773
526
|
}
|
|
774
527
|
if (h === "claude") {
|
|
775
|
-
|
|
528
|
+
let settingsPath;
|
|
529
|
+
try {
|
|
530
|
+
settingsPath = await claudeSettingsPath(context);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return STATUS_NOT_WIRED;
|
|
534
|
+
}
|
|
535
|
+
const settings = await readJsonFile(settingsPath);
|
|
776
536
|
return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
|
|
777
537
|
}
|
|
778
538
|
if (h === "antigravity") {
|
|
779
|
-
const settings = await readJsonFile(antigravitySettingsPath(context
|
|
539
|
+
const settings = await readJsonFile(antigravitySettingsPath(context));
|
|
780
540
|
return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
|
|
781
541
|
}
|
|
782
542
|
if (h === "grok") {
|
|
783
|
-
const text = await readTextFile(grokConfigPath(context
|
|
543
|
+
const text = await readTextFile(grokConfigPath(context));
|
|
784
544
|
return classifyCommand(grokStatusLineCommand(text));
|
|
785
545
|
}
|
|
786
546
|
if (h === "codex") {
|
|
787
|
-
|
|
547
|
+
let configPath;
|
|
548
|
+
try {
|
|
549
|
+
configPath = await codexConfigPath(context);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return STATUS_NOT_WIRED;
|
|
553
|
+
}
|
|
554
|
+
const text = await readTextFile(configPath);
|
|
788
555
|
return classifyCodexSection(text);
|
|
789
556
|
}
|
|
790
557
|
if (h === "shell") {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return STATUS_WIRED;
|
|
558
|
+
try {
|
|
559
|
+
const target = await shellTarget(context);
|
|
560
|
+
const text = await readOptional(target.file);
|
|
561
|
+
return text !== null && await isOwned(target.file, text, ownedMarker(text, false)) ? STATUS_WIRED : STATUS_NOT_WIRED;
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return STATUS_NOT_WIRED;
|
|
795
565
|
}
|
|
796
|
-
return STATUS_NOT_WIRED;
|
|
797
566
|
}
|
|
798
567
|
return STATUS_NOT_WIRED;
|
|
799
568
|
}
|
|
@@ -910,7 +679,8 @@ export async function terminalShow(providerIds, context) {
|
|
|
910
679
|
...config,
|
|
911
680
|
statusline: {
|
|
912
681
|
...config.statusline,
|
|
913
|
-
show: currentShow
|
|
682
|
+
show: currentShow,
|
|
683
|
+
showMode: "explicit"
|
|
914
684
|
}
|
|
915
685
|
};
|
|
916
686
|
try {
|
|
@@ -944,7 +714,7 @@ export async function terminalHide(providerIds, context) {
|
|
|
944
714
|
};
|
|
945
715
|
}
|
|
946
716
|
let currentShow;
|
|
947
|
-
if (config.statusline.show.length === 0) {
|
|
717
|
+
if (config.statusline.show.length === 0 && config.statusline.showMode !== "explicit") {
|
|
948
718
|
const allProviders = connectors.map((c) => c.id);
|
|
949
719
|
currentShow = allProviders.filter((id) => !providerIds.map((p) => p.toLowerCase()).includes(id.toLowerCase()));
|
|
950
720
|
}
|
|
@@ -955,7 +725,8 @@ export async function terminalHide(providerIds, context) {
|
|
|
955
725
|
...config,
|
|
956
726
|
statusline: {
|
|
957
727
|
...config.statusline,
|
|
958
|
-
show: currentShow
|
|
728
|
+
show: currentShow,
|
|
729
|
+
showMode: "explicit"
|
|
959
730
|
}
|
|
960
731
|
};
|
|
961
732
|
try {
|