openlimiter 1.3.2 → 1.3.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/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 +134 -55
- 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 +394 -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,77 @@ 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
|
+
const root = value === undefined || value === "" ? fallback : value;
|
|
33
|
+
if (!path.isAbsolute(root))
|
|
34
|
+
throw new Error(`${variable} must be an absolute path.`);
|
|
35
|
+
let existing = null;
|
|
36
|
+
try {
|
|
37
|
+
existing = await lstat(root);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT")
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (existing?.isSymbolicLink())
|
|
44
|
+
return await realpath(root);
|
|
45
|
+
try {
|
|
46
|
+
return await realpath(root);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== "ENOENT")
|
|
50
|
+
throw error;
|
|
51
|
+
// Default roots need the same canonical identity as overrides. Resolve the
|
|
52
|
+
// nearest existing ancestor when a first install has not created them yet.
|
|
53
|
+
const parentPath = path.dirname(root);
|
|
54
|
+
if (parentPath === root)
|
|
55
|
+
throw error;
|
|
56
|
+
const parent = await canonicalOverrideRoot(parentPath, parentPath, variable);
|
|
57
|
+
return path.join(parent, path.basename(root));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isWithinDirectory(root, target) {
|
|
61
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
62
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
63
|
+
}
|
|
64
|
+
async function canonicalConfigFile(root, name, variable) {
|
|
65
|
+
const candidate = path.join(root, name);
|
|
66
|
+
try {
|
|
67
|
+
const target = await realpath(candidate);
|
|
68
|
+
if (!isWithinDirectory(root, target))
|
|
69
|
+
throw new Error(`${variable} must point to a file under its root.`);
|
|
70
|
+
return target;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code !== "ENOENT")
|
|
74
|
+
throw error;
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
30
77
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
78
|
+
async function claudeSettingsPath(context) {
|
|
79
|
+
const root = await canonicalOverrideRoot(context.environment?.["CLAUDE_CONFIG_DIR"], path.join(context.homeDirectory, ".claude"), "CLAUDE_CONFIG_DIR");
|
|
80
|
+
return await canonicalConfigFile(root, "settings.json", "CLAUDE_CONFIG_DIR");
|
|
33
81
|
}
|
|
34
|
-
function
|
|
35
|
-
return path.join(
|
|
82
|
+
function antigravitySettingsPath(context) {
|
|
83
|
+
return path.join(context.homeDirectory, ".gemini", "antigravity-cli", "settings.json");
|
|
36
84
|
}
|
|
37
|
-
function
|
|
38
|
-
return path.join(
|
|
85
|
+
function grokConfigPath(context) {
|
|
86
|
+
return path.join(context.homeDirectory, ".grok", "config.toml");
|
|
39
87
|
}
|
|
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");
|
|
88
|
+
async function codexConfigPath(context) {
|
|
89
|
+
const root = await canonicalOverrideRoot(context.environment?.["CODEX_HOME"], path.join(context.homeDirectory, ".codex"), "CODEX_HOME");
|
|
90
|
+
return await canonicalConfigFile(root, "config.toml", "CODEX_HOME");
|
|
46
91
|
}
|
|
47
92
|
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;
|
|
93
|
+
/** Ask the selected PowerShell, including redirected Documents and PS 7 layouts.
|
|
94
|
+
* A shell that cannot identify its active profile is not a verified target. */
|
|
95
|
+
async function resolvePowerShellProfilePath(executable, runner) {
|
|
96
|
+
if (!runner)
|
|
97
|
+
throw new Error("Unsupported shell");
|
|
98
|
+
const result = await runner(executable, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE"], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
|
|
99
|
+
if (!result.ok || !path.isAbsolute(result.stdout.trim()))
|
|
100
|
+
throw new Error("Unsupported shell");
|
|
101
|
+
return result.stdout.trim();
|
|
85
102
|
}
|
|
86
103
|
export async function readJsonConfig(filePath) {
|
|
87
104
|
try {
|
|
@@ -108,114 +125,13 @@ export async function readJsonConfig(filePath) {
|
|
|
108
125
|
}
|
|
109
126
|
}
|
|
110
127
|
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
|
-
}
|
|
128
|
+
try {
|
|
129
|
+
parseToml(text);
|
|
130
|
+
return true;
|
|
213
131
|
}
|
|
214
|
-
|
|
215
|
-
return false;
|
|
216
|
-
if (bracketDepth !== 0 || braceDepth !== 0)
|
|
132
|
+
catch {
|
|
217
133
|
return false;
|
|
218
|
-
|
|
134
|
+
}
|
|
219
135
|
}
|
|
220
136
|
export async function readTomlConfig(filePath) {
|
|
221
137
|
try {
|
|
@@ -239,10 +155,6 @@ async function readJsonFile(filePath) {
|
|
|
239
155
|
const res = await readJsonConfig(filePath);
|
|
240
156
|
return res.kind === "ok" ? res.data : null;
|
|
241
157
|
}
|
|
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
158
|
async function readTextFile(filePath) {
|
|
247
159
|
try {
|
|
248
160
|
return await readFile(filePath, "utf8");
|
|
@@ -251,455 +163,289 @@ async function readTextFile(filePath) {
|
|
|
251
163
|
return null;
|
|
252
164
|
}
|
|
253
165
|
}
|
|
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"];
|
|
166
|
+
const HOST_CONFIG = {
|
|
167
|
+
claude: { path: claudeSettingsPath, label: "Claude Code", file: "Claude Code settings", json: true, wired: "Wired Claude Code status line." },
|
|
168
|
+
antigravity: { path: antigravitySettingsPath, label: "Antigravity", file: "Antigravity settings", json: true, wired: "Wired Antigravity CLI status line." },
|
|
169
|
+
grok: { path: grokConfigPath, label: "Grok", file: "Grok config", json: false, wired: "Wired Grok Build status line." },
|
|
170
|
+
codex: { path: codexConfigPath, label: "Codex", file: "Codex config", json: false, wired: "Wired Codex status line." }
|
|
171
|
+
};
|
|
172
|
+
const CODEX_ITEMS = ["five-hour-limit", "weekly-limit", "context-used", "model-with-reasoning", "current-dir"];
|
|
173
|
+
async function durableCommand(context, shell) {
|
|
174
|
+
const runtime = await installLauncher(context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter"));
|
|
175
|
+
return launcherCommand(runtime, shell);
|
|
176
|
+
}
|
|
177
|
+
function ownedMarker(text, json) {
|
|
178
|
+
return json ? JSON.parse(text)["openlimiter managed"] === true
|
|
179
|
+
: text.split(/\r?\n/).some(line => line.trim() === "# openlimiter managed");
|
|
180
|
+
}
|
|
181
|
+
async function changeConfigHost(host, context, install) {
|
|
182
|
+
const spec = HOST_CONFIG[host];
|
|
183
|
+
let file;
|
|
338
184
|
try {
|
|
339
|
-
await
|
|
340
|
-
return { ok: true, message: "Uninstalled Claude Code status line." };
|
|
185
|
+
file = await spec.path(context);
|
|
341
186
|
}
|
|
342
|
-
catch {
|
|
343
|
-
return {
|
|
187
|
+
catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
message: error instanceof Error ? error.message : `Could not resolve ${spec.file}.`
|
|
191
|
+
};
|
|
344
192
|
}
|
|
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;
|
|
193
|
+
const read = spec.json ? await readJsonConfig(file) : await readTomlConfig(file);
|
|
194
|
+
if (read.kind === "parse_error")
|
|
195
|
+
return { ok: false, message: `Could not read ${file}, fix it or move it aside` };
|
|
376
196
|
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." };
|
|
197
|
+
const original = await readOptional(file);
|
|
198
|
+
const text = original ?? (spec.json ? "{}" : "");
|
|
199
|
+
const marker = ownedMarker(text, spec.json);
|
|
200
|
+
if (!install) {
|
|
201
|
+
const restored = original !== null && await restoreOwned(file, original, marker);
|
|
202
|
+
if (marker && !restored)
|
|
203
|
+
return { ok: false, message: `Could not update ${file}.` };
|
|
204
|
+
return { ok: true, message: restored ? `Uninstalled ${spec.label} status line.` : `${spec.label} status line is not installed.` };
|
|
412
205
|
}
|
|
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];
|
|
206
|
+
const previous = spec.json
|
|
207
|
+
? claudeLikeStatusLineCommand(JSON.parse(text)["statusLine"])
|
|
208
|
+
: host === "grok"
|
|
209
|
+
? (() => {
|
|
210
|
+
const value = tomlValue(text, ["ui", "status_line", "command"]);
|
|
211
|
+
return typeof value === "string" ? value : null;
|
|
212
|
+
})()
|
|
213
|
+
: null;
|
|
214
|
+
const legacyOpenLimiter = previous !== null && isOpenLimiterStatuslineCommand(previous);
|
|
215
|
+
const owned = original !== null && await isOwned(file, original, marker);
|
|
216
|
+
if (marker && !owned && !legacyOpenLimiter)
|
|
217
|
+
return { ok: false, message: `Could not write ${file}.` };
|
|
218
|
+
let updated;
|
|
219
|
+
if (spec.json) {
|
|
220
|
+
const data = JSON.parse(text);
|
|
221
|
+
const base = await durableCommand(context, context.platform === "win32" ? "cmd" : "posix");
|
|
222
|
+
const userCommand = previous === null
|
|
223
|
+
? null
|
|
224
|
+
: legacyOpenLimiter ? unwrapOpenLimiterStatuslineCommand(previous) : previous;
|
|
225
|
+
const command = `${base} statusline --host ${host}` +
|
|
226
|
+
(userCommand === null ? "" : ` --wrap ${encodeWrappedStatuslineCommand(userCommand)}`);
|
|
227
|
+
data["openlimiter managed"] = true;
|
|
228
|
+
data["statusLine"] = host === "claude" ? { type: "command", command } : command;
|
|
229
|
+
updated = JSON.stringify(data, null, 2) + "\n";
|
|
230
|
+
JSON.parse(updated);
|
|
441
231
|
}
|
|
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
|
-
}
|
|
232
|
+
else if (host === "codex") {
|
|
233
|
+
updated = editToml(text, ["tui"], { status_line: CODEX_ITEMS, status_line_use_colors: true });
|
|
521
234
|
}
|
|
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." };
|
|
235
|
+
else {
|
|
236
|
+
const base = await durableCommand(context, context.platform === "win32" ? "cmd" : "posix");
|
|
237
|
+
const userCommand = previous === null
|
|
238
|
+
? null
|
|
239
|
+
: legacyOpenLimiter ? unwrapOpenLimiterStatuslineCommand(previous) : previous;
|
|
240
|
+
const command = `${base} statusline --host grok` +
|
|
241
|
+
(typeof userCommand === "string" ? ` --wrap ${encodeWrappedStatuslineCommand(userCommand)}` : "");
|
|
242
|
+
updated = editToml(text, ["ui", "status_line"], { type: "command", command });
|
|
243
|
+
}
|
|
244
|
+
if (owned && updated === text)
|
|
245
|
+
return { ok: true, message: spec.wired };
|
|
246
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
247
|
+
await writeOwned(file, original, updated);
|
|
248
|
+
return { ok: true, message: spec.wired };
|
|
542
249
|
}
|
|
543
250
|
catch {
|
|
544
|
-
return { ok: false, message:
|
|
545
|
-
}
|
|
251
|
+
return { ok: false, message: `Could not ${install ? "write" : "update"} ${file}.` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
export const installClaude = (context) => changeConfigHost("claude", context, true);
|
|
255
|
+
export const uninstallClaude = (context) => changeConfigHost("claude", context, false);
|
|
256
|
+
export const installAntigravity = (context) => changeConfigHost("antigravity", context, true);
|
|
257
|
+
export const uninstallAntigravity = (context) => changeConfigHost("antigravity", context, false);
|
|
258
|
+
export const installGrok = (context) => changeConfigHost("grok", context, true);
|
|
259
|
+
export const uninstallGrok = (context) => changeConfigHost("grok", context, false);
|
|
260
|
+
export const installCodex = (context) => changeConfigHost("codex", context, true);
|
|
261
|
+
export const uninstallCodex = (context) => changeConfigHost("codex", context, false);
|
|
262
|
+
function shellSnippets(posixCommand, powerShellCommand) {
|
|
263
|
+
const command = posixCommand + " statusline --host shell";
|
|
264
|
+
return {
|
|
265
|
+
starship: [
|
|
266
|
+
"[custom.openlimiter]", "command = " + JSON.stringify(command),
|
|
267
|
+
'when = "true"', 'shell = ["bash", "--noprofile", "--norc"]',
|
|
268
|
+
'format = "[$output]($style) "'
|
|
269
|
+
].join("\n"),
|
|
270
|
+
tmux: "set -g status-right " + JSON.stringify("#(" + command + ")") + "\nset -g status-interval 60",
|
|
271
|
+
ohMyPosh: JSON.stringify({ type: "command", properties: {
|
|
272
|
+
command: powerShellCommand + " statusline --host shell", shell: "powershell", cache: { duration: "60s" }
|
|
273
|
+
} }, null, 2)
|
|
274
|
+
};
|
|
546
275
|
}
|
|
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
|
-
|
|
276
|
+
class UnsupportedShellError extends Error {
|
|
277
|
+
shell;
|
|
278
|
+
constructor(shell) {
|
|
279
|
+
super("Unsupported shell");
|
|
280
|
+
this.shell = shell;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function shellTarget(context) {
|
|
284
|
+
const env = context.environment ?? process.env;
|
|
285
|
+
let executable = env["SHELL"] || "";
|
|
286
|
+
if (!executable && context.platform === "win32" && context.shellRunner) {
|
|
287
|
+
// npm can sit between Node and the interactive shell. Walk ancestors until
|
|
288
|
+
// the actual PowerShell or Unix shell is found, rather than trusting COMSPEC
|
|
289
|
+
// (which remains cmd.exe even inside PowerShell).
|
|
290
|
+
const query = "$ancestorId = " + process.ppid + "; for ($n = 0; $n -lt 12 -and $ancestorId; $n++) { " +
|
|
291
|
+
"$p = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $ancestorId); " +
|
|
292
|
+
"if ($p.Name -match '^(pwsh|powershell|bash|zsh)(\\.exe)?$') { $p.ExecutablePath; break }; " +
|
|
293
|
+
"$ancestorId = $p.ParentProcessId }";
|
|
294
|
+
const found = await context.shellRunner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
|
|
295
|
+
if (found.ok)
|
|
296
|
+
executable = found.stdout.trim();
|
|
297
|
+
}
|
|
298
|
+
const name = path.basename(executable).toLowerCase();
|
|
299
|
+
if (name === "bash" || name === "bash.exe") {
|
|
300
|
+
return { kind: "bash", file: path.join(context.homeDirectory, ".bashrc") };
|
|
301
|
+
}
|
|
302
|
+
if (name === "zsh") {
|
|
303
|
+
return { kind: "zsh", file: path.join(env["ZDOTDIR"] || context.homeDirectory, ".zshrc") };
|
|
304
|
+
}
|
|
305
|
+
if (name === "pwsh" || name === "pwsh.exe" || name === "powershell" || name === "powershell.exe") {
|
|
306
|
+
return { kind: "powershell", file: await resolvePowerShellProfilePath(executable, context.shellRunner) };
|
|
307
|
+
}
|
|
308
|
+
if (name !== "")
|
|
309
|
+
throw new UnsupportedShellError(name);
|
|
310
|
+
throw new Error("Unsupported shell");
|
|
311
|
+
}
|
|
312
|
+
function escapedRegExp(text) {
|
|
313
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
314
|
+
}
|
|
315
|
+
function userFunctionExists(text, name, powershell) {
|
|
316
|
+
const escaped = escapedRegExp(name);
|
|
317
|
+
return powershell
|
|
318
|
+
? new RegExp(`(?:^|\\r?\\n)\\s*function\\s+(?:global:)?${escaped}\\b`, "i").test(text)
|
|
319
|
+
: new RegExp(`(?:^|\\r?\\n)\\s*(?:function\\s+)?${escaped}\\s*(?:\\(\\s*\\))?\\s*\\{`).test(text);
|
|
320
|
+
}
|
|
321
|
+
function uniqueShellName(text, base, powershell) {
|
|
322
|
+
for (let suffix = 0;; suffix += 1) {
|
|
323
|
+
const candidate = suffix === 0 ? base : `${base}_${suffix + 1}`;
|
|
324
|
+
if (!userFunctionExists(text, candidate, powershell))
|
|
325
|
+
return candidate;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const MANAGED_SHELL_BLOCK = /(^|\r?\n)# openlimiter managed\r?\n[\s\S]*?\r?\n# end openlimiter managed/g;
|
|
329
|
+
function hasManagedShellBlock(text) {
|
|
330
|
+
return /(?:^|\r?\n)# openlimiter managed\r?\n[\s\S]*?\r?\n# end openlimiter managed/.test(text);
|
|
331
|
+
}
|
|
332
|
+
function withoutManagedShellBlock(text) {
|
|
333
|
+
return text.replace(MANAGED_SHELL_BLOCK, "$1");
|
|
334
|
+
}
|
|
335
|
+
function replaceManagedShellBlock(text, snippet) {
|
|
336
|
+
return text.replace(MANAGED_SHELL_BLOCK, `$1${snippet}`);
|
|
337
|
+
}
|
|
338
|
+
function shellSnippet(kind, command, source = "") {
|
|
339
|
+
const begin = "# openlimiter managed";
|
|
340
|
+
const end = "# end openlimiter managed";
|
|
341
|
+
const hook = uniqueShellName(source, kind === "powershell" ? "__openlimiter_prompt_hook" : "__openlimiter_statusline", kind === "powershell");
|
|
342
|
+
if (kind === "bash")
|
|
343
|
+
return [
|
|
344
|
+
begin,
|
|
345
|
+
`if ! declare -F ${hook} >/dev/null 2>&1; then`,
|
|
346
|
+
` ${hook}() { ${command} statusline --host shell; }`,
|
|
347
|
+
"fi",
|
|
348
|
+
'if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then',
|
|
349
|
+
` [[ " \${PROMPT_COMMAND[*]} " == *" ${hook} "* ]] || PROMPT_COMMAND+=(${hook})`,
|
|
350
|
+
`elif [[ ";\${PROMPT_COMMAND};" != *";${hook};"* ]]; then`,
|
|
351
|
+
` PROMPT_COMMAND="\${PROMPT_COMMAND:+\${PROMPT_COMMAND};}${hook}"`,
|
|
352
|
+
"fi", end
|
|
353
|
+
].join("\n");
|
|
354
|
+
if (kind === "zsh")
|
|
355
|
+
return [
|
|
356
|
+
begin,
|
|
357
|
+
`if (( ! $+functions[${hook}] )); then`,
|
|
358
|
+
` ${hook}() { ${command} statusline --host shell; }`,
|
|
359
|
+
"fi",
|
|
360
|
+
"autoload -Uz add-zsh-hook",
|
|
361
|
+
`if (( \${precmd_functions[(I)${hook}]} == 0 )); then add-zsh-hook precmd ${hook}; fi`, end
|
|
362
|
+
].join("\n");
|
|
363
|
+
const originalPrompt = uniqueShellName(source, "__openlimiter_original_prompt", true);
|
|
364
|
+
const guard = uniqueShellName(source, "__openlimiter_prompt_guard", true);
|
|
365
|
+
return [
|
|
366
|
+
begin,
|
|
367
|
+
`if (-not (Test-Path Function:global:${originalPrompt})) {`,
|
|
368
|
+
` $function:global:${originalPrompt} = $function:prompt`,
|
|
369
|
+
"}",
|
|
370
|
+
`if (-not (Test-Path Function:global:${hook})) {`,
|
|
371
|
+
` function global:${hook} {`,
|
|
372
|
+
` $bar = ${command} statusline --host shell`,
|
|
373
|
+
" if ($bar) { Write-Host $bar }",
|
|
374
|
+
` & $function:global:${originalPrompt}`,
|
|
646
375
|
" }",
|
|
647
|
-
"}"
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
"
|
|
652
|
-
|
|
653
|
-
"
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
376
|
+
"}",
|
|
377
|
+
`if (-not (Test-Path Function:global:${guard})) {`,
|
|
378
|
+
` function global:${guard} { & $function:global:${hook} }`,
|
|
379
|
+
"}",
|
|
380
|
+
"function global:prompt {",
|
|
381
|
+
` & $function:global:${guard}`,
|
|
382
|
+
"}", end
|
|
383
|
+
].join("\n");
|
|
384
|
+
}
|
|
385
|
+
async function manualShellSnippetPath(context) {
|
|
386
|
+
const directory = context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter");
|
|
387
|
+
const file = path.join(directory, "shell-snippet.txt");
|
|
388
|
+
await mkdir(directory, { recursive: true }).then(() => writeFileAtomically(file, "openlimiter statusline --host shell\n")).catch(() => undefined);
|
|
389
|
+
return file;
|
|
390
|
+
}
|
|
662
391
|
export async function installShell(context) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
392
|
+
try {
|
|
393
|
+
const target = await shellTarget(context);
|
|
394
|
+
const original = await readOptional(target.file);
|
|
395
|
+
const runtime = await installLauncher(context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter"));
|
|
396
|
+
const posixCommand = launcherCommand(runtime, "posix");
|
|
397
|
+
const powerShellCommand = launcherCommand(runtime, "powershell");
|
|
398
|
+
const command = target.kind === "powershell" ? powerShellCommand : posixCommand;
|
|
399
|
+
const snippets = shellSnippets(posixCommand, powerShellCommand);
|
|
400
|
+
const alreadyOwned = original !== null && await isOwned(target.file, original, ownedMarker(original, false));
|
|
401
|
+
const source = withoutManagedShellBlock(original ?? "");
|
|
402
|
+
const snippet = shellSnippet(target.kind, command, source);
|
|
403
|
+
const marker = original !== null && ownedMarker(original, false);
|
|
404
|
+
if (alreadyOwned || (marker && hasManagedShellBlock(original ?? ""))) {
|
|
405
|
+
const updated = replaceManagedShellBlock(original, snippet);
|
|
406
|
+
if (updated !== original) {
|
|
407
|
+
await writeOwned(target.file, original, updated);
|
|
408
|
+
}
|
|
669
409
|
}
|
|
670
|
-
|
|
671
|
-
|
|
410
|
+
else if (marker) {
|
|
411
|
+
return { ok: false, message: `Could not write ${target.file}.` };
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const updated = (original ?? "") + "\n\n" + snippet + "\n";
|
|
415
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
416
|
+
await writeOwned(target.file, original, updated);
|
|
672
417
|
}
|
|
418
|
+
const message = [
|
|
419
|
+
"Wired shell prompt integration.", "",
|
|
420
|
+
"Starship snippet (~/.config/starship.toml):", snippets.starship, "",
|
|
421
|
+
"tmux snippet (~/.tmux.conf):", snippets.tmux, "",
|
|
422
|
+
"Oh My Posh segment:", snippets.ohMyPosh
|
|
423
|
+
].join("\n");
|
|
424
|
+
return { ok: true, message };
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
if (error instanceof UnsupportedShellError) {
|
|
428
|
+
const file = await manualShellSnippetPath(context);
|
|
429
|
+
return { ok: true, message: `Skipped ${error.shell} shell. See ${file} for the manual snippet.` };
|
|
430
|
+
}
|
|
431
|
+
return { ok: false, message: "Could not write configuration." };
|
|
673
432
|
}
|
|
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
433
|
}
|
|
688
434
|
export async function uninstallShell(context) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
// Ignored
|
|
435
|
+
try {
|
|
436
|
+
const target = await shellTarget(context);
|
|
437
|
+
const original = await readOptional(target.file);
|
|
438
|
+
if (original !== null) {
|
|
439
|
+
const marker = ownedMarker(original, false);
|
|
440
|
+
const restored = await restoreOwned(target.file, original, marker);
|
|
441
|
+
if (marker && !restored)
|
|
442
|
+
return { ok: false, message: `Could not update ${target.file}.` };
|
|
700
443
|
}
|
|
444
|
+
return { ok: true, message: "Uninstalled shell prompt integration." };
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return { ok: false, message: "Could not write configuration." };
|
|
701
448
|
}
|
|
702
|
-
return { ok: true, message: "Uninstalled shell prompt integration." };
|
|
703
449
|
}
|
|
704
450
|
/**
|
|
705
451
|
* The command a Claude Code or Antigravity style status line field carries,
|
|
@@ -718,25 +464,35 @@ function claudeLikeStatusLineCommand(value) {
|
|
|
718
464
|
}
|
|
719
465
|
return null;
|
|
720
466
|
}
|
|
467
|
+
function isOpenLimiterStatuslineCommand(command) {
|
|
468
|
+
return /(?:^|[\\/"'\s])openlimiter(?:\.cjs|\.cmd|\.exe)?(?=$|[\\/"'\s])/i.test(command) &&
|
|
469
|
+
/\bstatusline\s+--host\s+[A-Za-z0-9_-]+\b/i.test(command);
|
|
470
|
+
}
|
|
471
|
+
function unwrapOpenLimiterStatuslineCommand(command) {
|
|
472
|
+
if (!isOpenLimiterStatuslineCommand(command))
|
|
473
|
+
return command;
|
|
474
|
+
const encoded = /\s--wrap\s+([A-Za-z0-9_-]+)/i.exec(command)?.[1];
|
|
475
|
+
return encoded === undefined ? null : decodeWrappedStatuslineCommand(encoded);
|
|
476
|
+
}
|
|
721
477
|
/** Wired, somebody else's, or nothing there, from the command alone. */
|
|
722
478
|
function classifyCommand(command) {
|
|
723
479
|
if (command === null)
|
|
724
480
|
return STATUS_NOT_WIRED;
|
|
725
|
-
return command
|
|
481
|
+
return isOpenLimiterStatuslineCommand(command) ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
|
|
726
482
|
}
|
|
727
483
|
/** The command inside Grok's `[ui.status_line]` table, or nothing found. */
|
|
728
484
|
function grokStatusLineCommand(text) {
|
|
729
485
|
if (text === null)
|
|
730
486
|
return null;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
487
|
+
try {
|
|
488
|
+
const parsed = parseToml(text);
|
|
489
|
+
const command = tomlValue(text, ["ui", "status_line", "command"]);
|
|
490
|
+
return typeof command === "string" ? command
|
|
491
|
+
: parsed.tables.some(t => JSON.stringify(t.keys) === '["ui","status_line"]') ? "" : null;
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
734
494
|
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] ?? "";
|
|
495
|
+
}
|
|
740
496
|
}
|
|
741
497
|
/**
|
|
742
498
|
* Wired, somebody else's, or nothing there, for Codex's `[tui]` table.
|
|
@@ -749,13 +505,14 @@ function grokStatusLineCommand(text) {
|
|
|
749
505
|
function classifyCodexSection(text) {
|
|
750
506
|
if (text === null)
|
|
751
507
|
return STATUS_NOT_WIRED;
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
508
|
+
try {
|
|
509
|
+
if (tomlValue(text, ["tui", "status_line"]) === undefined)
|
|
510
|
+
return STATUS_NOT_WIRED;
|
|
511
|
+
return ownedMarker(text, false) ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
755
514
|
return STATUS_NOT_WIRED;
|
|
756
|
-
|
|
757
|
-
return STATUS_WIRED;
|
|
758
|
-
return /status_line\s*=/.test(section) ? STATUS_OWN_LINE_FOUND : STATUS_NOT_WIRED;
|
|
515
|
+
}
|
|
759
516
|
}
|
|
760
517
|
/**
|
|
761
518
|
* Check wiring status for a host.
|
|
@@ -772,28 +529,44 @@ export async function hostStatus(host, context) {
|
|
|
772
529
|
return UNSUPPORTED_HOST_ALTERNATIVE;
|
|
773
530
|
}
|
|
774
531
|
if (h === "claude") {
|
|
775
|
-
|
|
532
|
+
let settingsPath;
|
|
533
|
+
try {
|
|
534
|
+
settingsPath = await claudeSettingsPath(context);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return STATUS_NOT_WIRED;
|
|
538
|
+
}
|
|
539
|
+
const settings = await readJsonFile(settingsPath);
|
|
776
540
|
return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
|
|
777
541
|
}
|
|
778
542
|
if (h === "antigravity") {
|
|
779
|
-
const settings = await readJsonFile(antigravitySettingsPath(context
|
|
543
|
+
const settings = await readJsonFile(antigravitySettingsPath(context));
|
|
780
544
|
return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
|
|
781
545
|
}
|
|
782
546
|
if (h === "grok") {
|
|
783
|
-
const text = await readTextFile(grokConfigPath(context
|
|
547
|
+
const text = await readTextFile(grokConfigPath(context));
|
|
784
548
|
return classifyCommand(grokStatusLineCommand(text));
|
|
785
549
|
}
|
|
786
550
|
if (h === "codex") {
|
|
787
|
-
|
|
551
|
+
let configPath;
|
|
552
|
+
try {
|
|
553
|
+
configPath = await codexConfigPath(context);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return STATUS_NOT_WIRED;
|
|
557
|
+
}
|
|
558
|
+
const text = await readTextFile(configPath);
|
|
788
559
|
return classifyCodexSection(text);
|
|
789
560
|
}
|
|
790
561
|
if (h === "shell") {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return STATUS_WIRED;
|
|
562
|
+
try {
|
|
563
|
+
const target = await shellTarget(context);
|
|
564
|
+
const text = await readOptional(target.file);
|
|
565
|
+
return text !== null && await isOwned(target.file, text, ownedMarker(text, false)) ? STATUS_WIRED : STATUS_NOT_WIRED;
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return STATUS_NOT_WIRED;
|
|
795
569
|
}
|
|
796
|
-
return STATUS_NOT_WIRED;
|
|
797
570
|
}
|
|
798
571
|
return STATUS_NOT_WIRED;
|
|
799
572
|
}
|
|
@@ -910,7 +683,8 @@ export async function terminalShow(providerIds, context) {
|
|
|
910
683
|
...config,
|
|
911
684
|
statusline: {
|
|
912
685
|
...config.statusline,
|
|
913
|
-
show: currentShow
|
|
686
|
+
show: currentShow,
|
|
687
|
+
showMode: "explicit"
|
|
914
688
|
}
|
|
915
689
|
};
|
|
916
690
|
try {
|
|
@@ -944,7 +718,7 @@ export async function terminalHide(providerIds, context) {
|
|
|
944
718
|
};
|
|
945
719
|
}
|
|
946
720
|
let currentShow;
|
|
947
|
-
if (config.statusline.show.length === 0) {
|
|
721
|
+
if (config.statusline.show.length === 0 && config.statusline.showMode !== "explicit") {
|
|
948
722
|
const allProviders = connectors.map((c) => c.id);
|
|
949
723
|
currentShow = allProviders.filter((id) => !providerIds.map((p) => p.toLowerCase()).includes(id.toLowerCase()));
|
|
950
724
|
}
|
|
@@ -955,7 +729,8 @@ export async function terminalHide(providerIds, context) {
|
|
|
955
729
|
...config,
|
|
956
730
|
statusline: {
|
|
957
731
|
...config.statusline,
|
|
958
|
-
show: currentShow
|
|
732
|
+
show: currentShow,
|
|
733
|
+
showMode: "explicit"
|
|
959
734
|
}
|
|
960
735
|
};
|
|
961
736
|
try {
|