agy-powerline 1.2.0 → 1.4.0
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/CHANGELOG.md +72 -0
- package/README.fr.md +243 -0
- package/README.ja.md +144 -49
- package/README.md +160 -52
- package/README.zh-CN.md +144 -49
- package/package.json +5 -2
- package/src/cli/caps.js +2 -0
- package/src/cli/configure.js +70 -172
- package/src/cli/doctor.js +12 -0
- package/src/cli/export.js +82 -0
- package/src/cli/gradient.js +121 -0
- package/src/cli/import.js +273 -0
- package/src/cli/init.js +186 -7
- package/src/cli/layout.js +2 -0
- package/src/cli/preset.js +2 -0
- package/src/cli/repo-url.js +7 -0
- package/src/cli/theme.js +2 -0
- package/src/cli/uninstall.js +6 -2
- package/src/index.js +53 -1
- package/src/lib/catalog.js +54 -2
- package/src/lib/colors.js +144 -14
- package/src/lib/config-check.js +74 -0
- package/src/lib/config.js +89 -21
- package/src/lib/exec.js +140 -21
- package/src/lib/fit.js +7 -0
- package/src/lib/formatters.js +67 -0
- package/src/lib/git.js +385 -4
- package/src/lib/layouts/lines.js +175 -28
- package/src/lib/layouts/pills.js +232 -33
- package/src/lib/paths.js +73 -13
- package/src/lib/prompt.js +30 -0
- package/src/lib/run.js +2 -0
- package/src/lib/session.js +17 -3
- package/src/lib/shared.js +13 -2
- package/src/lib/themes.js +79 -5
- package/src/lib/usage.js +524 -0
- package/src/lib/widget-options.js +194 -0
- package/src/lib/widget-style.js +45 -0
- package/src/runtime/statusline.js +15 -1
package/src/lib/exec.js
CHANGED
|
@@ -5,7 +5,8 @@ const path = require("path");
|
|
|
5
5
|
const os = require("os");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
const { execSync } = require("child_process");
|
|
8
|
-
const { stripControl } = require("./formatters");
|
|
8
|
+
const { stripControl, sanitizeDeep } = require("./formatters");
|
|
9
|
+
const { RESET, getActiveDepth } = require("./colors");
|
|
9
10
|
const { logWarning } = require("./log");
|
|
10
11
|
const { PRIVATE_FILE_MODE } = require("./paths");
|
|
11
12
|
|
|
@@ -92,16 +93,123 @@ function cacheKey(cmd, cwd = "") {
|
|
|
92
93
|
.slice(0, 16);
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// The only escape sequence "colors": true keeps: SGR color and style. Colon
|
|
97
|
+
// forms (38:2::r:g:b) are dropped with the rest, since not every terminal reads them.
|
|
98
|
+
const SGR_SPLIT = /(\x1b\[[0-9;]*m)/;
|
|
99
|
+
const SGR_ALL = /\x1b\[([0-9;]*)m/g;
|
|
100
|
+
|
|
95
101
|
// Command output is untrusted text: strip terminal control and cap length.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
// With colors, SGR sequences survive and the visible text alone is capped;
|
|
103
|
+
// the output then ends with a reset so its colors can't leak past it.
|
|
104
|
+
function sanitizeExecOutput(text, { colors = false } = {}) {
|
|
105
|
+
if (!colors) {
|
|
106
|
+
const clean = stripControl(text).trim();
|
|
107
|
+
const chars = Array.from(clean);
|
|
108
|
+
|
|
109
|
+
if (chars.length <= MAX_OUTPUT_CHARS) {
|
|
110
|
+
return clean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return chars.slice(0, MAX_OUTPUT_CHARS - 1).join("") + "…";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parts = String(text ?? "")
|
|
117
|
+
.split(SGR_SPLIT)
|
|
118
|
+
.map((part, i) => (i % 2 ? part : stripControl(part)));
|
|
119
|
+
const visible = parts.filter((_, i) => i % 2 === 0).join("");
|
|
120
|
+
const lead = visible.length - visible.trimStart().length;
|
|
121
|
+
const chars = Array.from(visible.trim());
|
|
122
|
+
const cut = chars.length > MAX_OUTPUT_CHARS;
|
|
123
|
+
const budget = cut ? MAX_OUTPUT_CHARS - 1 : chars.length;
|
|
124
|
+
let skip = lead;
|
|
125
|
+
let used = 0;
|
|
126
|
+
let out = "";
|
|
127
|
+
let styled = false;
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < parts.length && used < budget; i++) {
|
|
130
|
+
if (i % 2) {
|
|
131
|
+
out += parts[i];
|
|
132
|
+
styled = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const char of Array.from(parts[i])) {
|
|
136
|
+
if (skip > 0) {
|
|
137
|
+
skip -= char.length;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (used === budget) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
out += char;
|
|
144
|
+
used++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
99
147
|
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
return out + (cut ? "…" : "") + (styled ? RESET : "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// After a reset in colored output, the widget's own colors come back (the
|
|
152
|
+
// pill's background and text in Pills), so the rest of the widget isn't left
|
|
153
|
+
// in the terminal's default colors.
|
|
154
|
+
function restoreAfterReset(text, restore) {
|
|
155
|
+
if (!restore) {
|
|
156
|
+
return text;
|
|
102
157
|
}
|
|
158
|
+
return String(text).replace(SGR_ALL, (seq, body) => {
|
|
159
|
+
const params = body === "" ? [0] : body.split(";").map((p) => (p === "" ? 0 : Number(p)));
|
|
160
|
+
let lastReset = -1;
|
|
161
|
+
for (let i = 0; i < params.length; i++) {
|
|
162
|
+
// 38/48/58 take a color as their next parameters; a 0 there isn't a reset.
|
|
163
|
+
if (params[i] === 38 || params[i] === 48 || params[i] === 58) {
|
|
164
|
+
i += params[i + 1] === 5 ? 2 : params[i + 1] === 2 ? 4 : 0;
|
|
165
|
+
} else if (params[i] === 0) {
|
|
166
|
+
lastReset = i;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (lastReset < 0) {
|
|
170
|
+
return seq;
|
|
171
|
+
}
|
|
172
|
+
const rest = params.slice(lastReset + 1);
|
|
173
|
+
return `${RESET}${restore}${rest.length ? `\x1b[${rest.join(";")}m` : ""}`;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Never handed to a command: agy-powerline doesn't read the account email or
|
|
178
|
+
// the session and conversation IDs, so it doesn't pass them on either.
|
|
179
|
+
const PRIVATE_KEYS = new Set(["email", "session_id", "conversation_id"]);
|
|
180
|
+
|
|
181
|
+
function withoutPrivateKeys(value, depth = 0) {
|
|
182
|
+
if (value === null || typeof value !== "object") {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
if (depth > 8) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return value.map((item) => withoutPrivateKeys(item, depth + 1));
|
|
190
|
+
}
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const [key, item] of Object.entries(value)) {
|
|
193
|
+
if (!PRIVATE_KEYS.has(key)) {
|
|
194
|
+
out[key] = withoutPrivateKeys(item, depth + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The session JSON an exec widget with "payload": true reads on stdin.
|
|
201
|
+
function execPayloadJson(payload) {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.stringify(sanitizeDeep(withoutPrivateKeys(payload ?? {}))) ?? "{}";
|
|
204
|
+
} catch {
|
|
205
|
+
return "{}";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
103
208
|
|
|
104
|
-
|
|
209
|
+
// Widgets that share a command but not its options must not share its output.
|
|
210
|
+
function widgetCacheKey(widget, cwd) {
|
|
211
|
+
const variant = `${widget.payload === true ? "\0payload" : ""}${widget.colors === true ? "\0colors" : ""}`;
|
|
212
|
+
return cacheKey(`${String(widget.cmd).trim()}${variant}`, cwd);
|
|
105
213
|
}
|
|
106
214
|
|
|
107
215
|
// session.execMode: "run" executes the command, "blocked" marks a config the
|
|
@@ -115,11 +223,12 @@ function execWidgetOutput(widget, session = {}) {
|
|
|
115
223
|
// Fitting the bar to the terminal can draw a row several times; a command
|
|
116
224
|
// still runs at most once per refresh, even with "interval": 0.
|
|
117
225
|
const memo = session.execMemo;
|
|
118
|
-
const key =
|
|
226
|
+
const key = widgetCacheKey(widget, session.cwd || process.cwd());
|
|
119
227
|
if (memo?.has(key)) {
|
|
120
228
|
return memo.get(key);
|
|
121
229
|
}
|
|
122
|
-
const
|
|
230
|
+
const colors = widget.colors === true && getActiveDepth() !== "none";
|
|
231
|
+
const output = sanitizeExecOutput(runExecWidget(widget, session), { colors });
|
|
123
232
|
memo?.set(key, output);
|
|
124
233
|
return output;
|
|
125
234
|
}
|
|
@@ -134,14 +243,15 @@ function runExecWidget(widget, session = {}) {
|
|
|
134
243
|
}
|
|
135
244
|
|
|
136
245
|
const cwd = session.cwd || process.cwd();
|
|
137
|
-
const key =
|
|
246
|
+
const key = widgetCacheKey(widget, cwd);
|
|
138
247
|
const now = Date.now();
|
|
248
|
+
const input = widget.payload === true ? `${session.execPayload || "{}"}\n` : undefined;
|
|
139
249
|
|
|
140
250
|
let intervalMs = DEFAULT_INTERVAL_SEC * 1000;
|
|
141
251
|
if (widget.interval != null) {
|
|
142
252
|
const parsed = Number(widget.interval);
|
|
143
253
|
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
144
|
-
intervalMs = parsed
|
|
254
|
+
intervalMs = parsed * 1000;
|
|
145
255
|
}
|
|
146
256
|
}
|
|
147
257
|
|
|
@@ -169,24 +279,30 @@ function runExecWidget(widget, session = {}) {
|
|
|
169
279
|
|
|
170
280
|
// 3. Cache expired or missing: execute command with timeout
|
|
171
281
|
let output = diskHit?.output || memHit?.output || "";
|
|
282
|
+
const firstLine = (stdout) =>
|
|
283
|
+
String(stdout || "")
|
|
284
|
+
.trim()
|
|
285
|
+
.split(/\r?\n/)[0]
|
|
286
|
+
?.trim() || "";
|
|
172
287
|
try {
|
|
173
288
|
const stdout = execSync(cmd, {
|
|
174
289
|
cwd,
|
|
175
290
|
timeout: timeoutMs,
|
|
176
291
|
encoding: "utf8",
|
|
177
|
-
|
|
292
|
+
input,
|
|
293
|
+
stdio: [input === undefined ? "ignore" : "pipe", "pipe", "ignore"],
|
|
178
294
|
windowsHide: true,
|
|
179
295
|
});
|
|
180
|
-
output =
|
|
181
|
-
String(stdout || "")
|
|
182
|
-
.trim()
|
|
183
|
-
.split(/\r?\n/)[0]
|
|
184
|
-
?.trim() || "";
|
|
296
|
+
output = firstLine(stdout);
|
|
185
297
|
} catch (err) {
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
298
|
+
// A command that exits without reading the whole payload closes the pipe
|
|
299
|
+
// under the write; it still succeeded, so its output counts. Otherwise the
|
|
300
|
+
// previous output stays. A non-zero exit is normal for many commands (grep
|
|
301
|
+
// with no match), so only timeouts and missing commands are logged, and
|
|
302
|
+
// never the command itself.
|
|
303
|
+
if (input !== undefined && err.status === 0 && !err.signal) {
|
|
304
|
+
output = firstLine(err.stdout);
|
|
305
|
+
} else if (err.code === "ETIMEDOUT" || err.signal === "SIGTERM") {
|
|
190
306
|
logWarning("EXEC_TIMEOUT", { timeoutMs });
|
|
191
307
|
} else if (err.status === 127 || err.code === "ENOENT") {
|
|
192
308
|
logWarning("EXEC_COMMAND_NOT_FOUND");
|
|
@@ -223,6 +339,9 @@ module.exports = {
|
|
|
223
339
|
runExecWidget,
|
|
224
340
|
execWidgetOutput,
|
|
225
341
|
sanitizeExecOutput,
|
|
342
|
+
restoreAfterReset,
|
|
343
|
+
execPayloadJson,
|
|
344
|
+
widgetCacheKey,
|
|
226
345
|
cacheKey,
|
|
227
346
|
clearExecCache,
|
|
228
347
|
getCacheFilePath,
|
package/src/lib/fit.js
CHANGED
|
@@ -6,12 +6,16 @@ const { charWidth, visibleWidth } = require("./formatters");
|
|
|
6
6
|
// first entry first. Ids not listed (custom ones) go before all of them.
|
|
7
7
|
const DROP_ORDER = [
|
|
8
8
|
"customText",
|
|
9
|
+
"gitStash",
|
|
10
|
+
"gitWorktree",
|
|
9
11
|
"title",
|
|
10
12
|
"version",
|
|
11
13
|
"cacheCreate",
|
|
12
14
|
"cacheRead",
|
|
13
15
|
"totalTokens",
|
|
14
16
|
"cacheHit",
|
|
17
|
+
"tokensToday",
|
|
18
|
+
"tokensWeek",
|
|
15
19
|
"artifacts",
|
|
16
20
|
"tasks",
|
|
17
21
|
"plan",
|
|
@@ -28,6 +32,9 @@ const DROP_ORDER = [
|
|
|
28
32
|
"weeklyReset",
|
|
29
33
|
"fiveHourReset",
|
|
30
34
|
"contextTokens",
|
|
35
|
+
"gitSha",
|
|
36
|
+
"gitFiles",
|
|
37
|
+
"gitChanges",
|
|
31
38
|
"git",
|
|
32
39
|
"sandbox",
|
|
33
40
|
"weeklyPct",
|
package/src/lib/formatters.js
CHANGED
|
@@ -271,6 +271,24 @@ function formatTokenSplit(input, output, ascii = false) {
|
|
|
271
271
|
: `${formatTokens(input)}↑ ${formatTokens(output)}↓`;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// The usage widgets' text: "1.2M", or "1.0M↑ 245.3K↓" with "split", plus the
|
|
275
|
+
// share of a "budget" when one is set ("1.2M (60%)"). null with no tokens.
|
|
276
|
+
function formatUsage(tokens, widget = {}, ascii = false) {
|
|
277
|
+
const input = Number(tokens?.input) || 0;
|
|
278
|
+
const output = Number(tokens?.output) || 0;
|
|
279
|
+
const total = input + output;
|
|
280
|
+
if (!(total > 0)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const value = widget?.split ? formatTokenSplit(input, output, ascii) : formatTokens(total);
|
|
284
|
+
const budget = widget?.budget;
|
|
285
|
+
const pct =
|
|
286
|
+
typeof budget === "number" && Number.isFinite(budget) && budget > 0
|
|
287
|
+
? (total * 100) / budget
|
|
288
|
+
: null;
|
|
289
|
+
return { text: pct === null ? value : `${value} (${Math.round(pct)}%)`, pct };
|
|
290
|
+
}
|
|
291
|
+
|
|
274
292
|
// "used/window", or just the used tokens while the window size is unknown.
|
|
275
293
|
function formatContextTokens(used, size) {
|
|
276
294
|
return size > 0 ? `${formatTokens(used)}/${formatTokens(size)}` : formatTokens(used);
|
|
@@ -460,6 +478,31 @@ function visibleWidth(text) {
|
|
|
460
478
|
return width;
|
|
461
479
|
}
|
|
462
480
|
|
|
481
|
+
// "+128 −34" as parts, so Lines can color each; zero counts are left out.
|
|
482
|
+
function gitChangeParts(changes, ascii = false) {
|
|
483
|
+
const parts = [];
|
|
484
|
+
if (changes?.added > 0) {
|
|
485
|
+
parts.push({ kind: "added", text: `+${changes.added}` });
|
|
486
|
+
}
|
|
487
|
+
if (changes?.removed > 0) {
|
|
488
|
+
parts.push({ kind: "removed", text: `${ascii ? "-" : "−"}${changes.removed}` });
|
|
489
|
+
}
|
|
490
|
+
return parts;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const GIT_FILE_MARKS = {
|
|
494
|
+
unicode: { staged: "+", modified: "●", untracked: "?", conflicts: "≠" },
|
|
495
|
+
ascii: { staged: "S", modified: "M", untracked: "?", conflicts: "!" },
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// "+2 ●3 ?1 ≠1" as parts: staged, modified, untracked, in conflict.
|
|
499
|
+
function gitFileParts(files, ascii = false) {
|
|
500
|
+
const marks = GIT_FILE_MARKS[ascii ? "ascii" : "unicode"];
|
|
501
|
+
return Object.keys(marks)
|
|
502
|
+
.filter((kind) => files?.[kind] > 0)
|
|
503
|
+
.map((kind) => ({ kind, text: `${marks[kind]}${files[kind]}` }));
|
|
504
|
+
}
|
|
505
|
+
|
|
463
506
|
function formatGitSync(ahead, behind, ascii = false) {
|
|
464
507
|
const up = ascii ? "^" : "⇡";
|
|
465
508
|
const down = ascii ? "v" : "⇣";
|
|
@@ -477,6 +520,26 @@ function formatGitSync(ahead, behind, ascii = false) {
|
|
|
477
520
|
return "";
|
|
478
521
|
}
|
|
479
522
|
|
|
523
|
+
const GIT_OPERATION_LABELS = {
|
|
524
|
+
rebase: "REBASE",
|
|
525
|
+
am: "AM",
|
|
526
|
+
merge: "MERGING",
|
|
527
|
+
"cherry-pick": "CHERRY-PICKING",
|
|
528
|
+
revert: "REVERTING",
|
|
529
|
+
bisect: "BISECTING",
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// "REBASE 2/5", "MERGING", or "" with no operation. Plain ASCII in both modes.
|
|
533
|
+
function formatGitOperation(op) {
|
|
534
|
+
const name = GIT_OPERATION_LABELS[op?.name];
|
|
535
|
+
if (!name) {
|
|
536
|
+
return "";
|
|
537
|
+
}
|
|
538
|
+
return Number.isInteger(op.step) && Number.isInteger(op.total)
|
|
539
|
+
? `${name} ${op.step}/${op.total}`
|
|
540
|
+
: name;
|
|
541
|
+
}
|
|
542
|
+
|
|
480
543
|
// "2 subagents, 1 task", or "" when nothing runs.
|
|
481
544
|
function formatTasks(subagents, tasks) {
|
|
482
545
|
const part = (n, word) => (n > 0 ? `${n} ${word}${n === 1 ? "" : "s"}` : "");
|
|
@@ -585,7 +648,11 @@ module.exports = {
|
|
|
585
648
|
formatContextTokens,
|
|
586
649
|
shortModelName,
|
|
587
650
|
formatTokenSplit,
|
|
651
|
+
formatUsage,
|
|
588
652
|
formatGitSync,
|
|
653
|
+
formatGitOperation,
|
|
654
|
+
gitChangeParts,
|
|
655
|
+
gitFileParts,
|
|
589
656
|
formatTasks,
|
|
590
657
|
formatMode,
|
|
591
658
|
clipText,
|