agy-powerline 1.2.0 → 1.3.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/src/lib/paths.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const fs = require("fs");
2
4
  const os = require("os");
3
5
  const path = require("path");
@@ -25,6 +27,7 @@ const RUNTIME_FILES = [
25
27
  "fit.js",
26
28
  "log.js",
27
29
  "payload-dump.js",
30
+ "usage.js",
28
31
  path.join("layouts", "lines.js"),
29
32
  path.join("layouts", "pills.js"),
30
33
  ];
@@ -34,10 +37,17 @@ const RUNTIME_FILES = [
34
37
  // machine can't read them (Windows profile folders are private already).
35
38
  const PRIVATE_FILES = [
36
39
  "statusline.config.json",
40
+ // The config before the last import (BACKUP_SUFFIX in cli/import.js).
41
+ "statusline.config.json.bak",
37
42
  ".exec-cache.json",
38
43
  "last-payload.json",
39
44
  // SYNC_CACHE_NAME in git.js: commit IDs of the repos you work in.
40
45
  ".git-sync-cache.json",
46
+ // STATUS_CACHE_NAME in git.js: repo paths with their file and line counts.
47
+ ".git-status-cache.json",
48
+ // USAGE_CACHE_NAME in usage.js: tokens per day, keyed by a hash of each
49
+ // conversation database's path.
50
+ ".usage-cache.json",
41
51
  // LOG_NAME in log.js, spelled out because log.js requires this file.
42
52
  "agy-powerline.log",
43
53
  ];
@@ -238,15 +248,54 @@ function settingsBackupPath(file = AGY_SETTINGS) {
238
248
  return `${file}.agy-powerline.bak`;
239
249
  }
240
250
 
241
- // Backs up the current file, then writes atomically.
242
- function writeJsonSafely(file, data) {
243
- fs.mkdirSync(path.dirname(file), { recursive: true });
251
+ function settingsOriginalBackupPath(file = AGY_SETTINGS) {
252
+ return `${file}.agy-powerline.original.bak`;
253
+ }
244
254
 
245
- if (fs.existsSync(file)) {
246
- fs.copyFileSync(file, settingsBackupPath(file));
255
+ // Two copies before every change: the latest file, replaced each time, and
256
+ // the file as it was before agy-powerline first changed it, written once and
257
+ // never replaced, so running init again can't lose the user's own version.
258
+ // Returns the original backup's path when this call created it.
259
+ function backUpSettings(file) {
260
+ if (!fs.existsSync(file)) {
261
+ return null;
262
+ }
263
+ const original = settingsOriginalBackupPath(file);
264
+ const created = !fs.existsSync(original);
265
+ if (created) {
266
+ fs.copyFileSync(file, original);
247
267
  }
268
+ fs.copyFileSync(file, settingsBackupPath(file));
269
+ return created ? original : null;
270
+ }
248
271
 
272
+ // Backs up the current file, then writes atomically. Returns what
273
+ // backUpSettings() returns.
274
+ function writeJsonSafely(file, data) {
275
+ fs.mkdirSync(path.dirname(file), { recursive: true });
276
+ const originalBackup = backUpSettings(file);
249
277
  writeFileAtomic(file, JSON.stringify(data, null, 2) + "\n");
278
+ return originalBackup;
279
+ }
280
+
281
+ // Dotfile managers (stow, chezmoi, yadm) link files such as settings.json
282
+ // into place. A write goes through the link to the file it points to, so the
283
+ // link stays a link and the user's real file gets the change; a rename onto
284
+ // the link itself would replace it with a plain file.
285
+ function writeTarget(file) {
286
+ try {
287
+ return fs.realpathSync(file);
288
+ } catch {
289
+ // Missing file, or a link whose target doesn't exist yet: follow it once.
290
+ try {
291
+ if (fs.lstatSync(file).isSymbolicLink()) {
292
+ return path.resolve(path.dirname(file), fs.readlinkSync(file));
293
+ }
294
+ } catch {
295
+ // Nothing at that path.
296
+ }
297
+ return file;
298
+ }
250
299
  }
251
300
 
252
301
  // Writes via temp file + rename so an interrupted write cannot leave a
@@ -254,9 +303,10 @@ function writeJsonSafely(file, data) {
254
303
  // mode the existing file had, so rewriting a file never widens who can read
255
304
  // it (the rename would otherwise reset it to the default).
256
305
  function writeFileAtomic(file, body, { mode } = {}) {
257
- fs.mkdirSync(path.dirname(file), { recursive: true });
258
- const tmp = `${file}.${process.pid}.tmp`;
259
- const target = process.platform === "win32" ? undefined : (mode ?? existingMode(file));
306
+ const dest = writeTarget(file);
307
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
308
+ const tmp = `${dest}.${process.pid}.tmp`;
309
+ const target = process.platform === "win32" ? undefined : (mode ?? existingMode(dest));
260
310
  fs.writeFileSync(tmp, body, { encoding: "utf8", mode: target ?? 0o666 });
261
311
  if (target !== undefined) {
262
312
  // The mode passed to writeFileSync is reduced by the umask; set it exactly.
@@ -270,10 +320,10 @@ function writeFileAtomic(file, body, { mode } = {}) {
270
320
  }
271
321
 
272
322
  try {
273
- fs.renameSync(tmp, file);
323
+ fs.renameSync(tmp, dest);
274
324
  } catch {
275
325
  // Windows can refuse to replace a file another process holds open.
276
- fs.writeFileSync(file, body, "utf8");
326
+ fs.writeFileSync(dest, body, "utf8");
277
327
  fs.rmSync(tmp, { force: true });
278
328
  }
279
329
  }
@@ -301,11 +351,20 @@ function restrictPrivateFiles(installDir) {
301
351
  }
302
352
  }
303
353
 
354
+ // Removes a settings.json that would be left empty. A symlinked one is emptied
355
+ // instead: deleting the link would break the user's dotfiles and leave the
356
+ // real file unchanged. Returns "deleted", "emptied" or null (nothing there).
304
357
  function removeSettingsFile(file = AGY_SETTINGS) {
305
- if (fs.existsSync(file)) {
306
- fs.copyFileSync(file, settingsBackupPath(file));
307
- fs.rmSync(file, { force: true });
358
+ if (!fs.existsSync(file)) {
359
+ return null;
360
+ }
361
+ backUpSettings(file);
362
+ if (fs.lstatSync(file).isSymbolicLink()) {
363
+ writeFileAtomic(file, "{}\n");
364
+ return "emptied";
308
365
  }
366
+ fs.rmSync(file, { force: true });
367
+ return "deleted";
309
368
  }
310
369
 
311
370
  // Antigravity runs the Windows launcher through `cmd /c`, which splits the
@@ -368,6 +427,7 @@ module.exports = {
368
427
  readAgySettings,
369
428
  readSettingsForUpdate,
370
429
  settingsBackupPath,
430
+ settingsOriginalBackupPath,
371
431
  writeJsonSafely,
372
432
  writeFileAtomic,
373
433
  removeSettingsFile,
package/src/lib/prompt.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const readline = require("readline");
2
4
 
3
5
  function createRl() {
@@ -24,8 +26,36 @@ async function confirm(rl, message, defaultYes) {
24
26
  return answer === "y" || answer === "yes";
25
27
  }
26
28
 
29
+ // Numbered options, answered with a number; Enter keeps the default. Anything
30
+ // else asks again, at most 3 times, then the default stands. No raw mode or
31
+ // arrow keys, so it works in every terminal and in cmd. An option can span
32
+ // several lines (a label and its preview). Returns the chosen index.
33
+ async function choose(rl, message, options, defaultIndex = 0, print = console.log) {
34
+ print(message);
35
+ options.forEach((option, i) => {
36
+ const [first, ...rest] = String(option).split("\n");
37
+ print(` ${String(i + 1).padStart(2)}) ${first}`);
38
+ rest.forEach((line) => print(` ${line}`));
39
+ });
40
+
41
+ for (let attempt = 0; attempt < 3; attempt++) {
42
+ const answer = (
43
+ await question(rl, ` Choose 1-${options.length} (Enter: ${defaultIndex + 1}) `)
44
+ ).trim();
45
+ if (!answer) {
46
+ return defaultIndex;
47
+ }
48
+ const index = /^\d{1,3}$/.test(answer) ? Number(answer) - 1 : -1;
49
+ if (index >= 0 && index < options.length) {
50
+ return index;
51
+ }
52
+ }
53
+ return defaultIndex;
54
+ }
55
+
27
56
  module.exports = {
28
57
  createRl,
29
58
  question,
30
59
  confirm,
60
+ choose,
31
61
  };
package/src/lib/run.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const { spawn } = require("child_process");
2
4
 
3
5
  function spawnCapture(file, args, input, timeoutMs = 15000) {
@@ -2,7 +2,8 @@
2
2
 
3
3
  const os = require("os");
4
4
  const { formatReset, resetInSeconds, firstText } = require("./formatters");
5
- const { resolveGit } = require("./git");
5
+ const { resolveGit, resolveGitDetails } = require("./git");
6
+ const { resolveUsage } = require("./usage");
6
7
 
7
8
  const FIVE_HOURS = 5 * 3600;
8
9
  const SEVEN_DAYS = 7 * 86400;
@@ -144,7 +145,17 @@ function contextUsedPercent(data) {
144
145
  return (current * 100) / size;
145
146
  }
146
147
 
147
- function extractSession(data, config) {
148
+ // options.readUsage: the statusline itself may read agy's conversation
149
+ // databases; previews only use the usage cache.
150
+ function extractSession(data, config, options = {}) {
151
+ const git = resolveGit(data, config);
152
+ const gitDetails = resolveGitDetails(data, config);
153
+ // Once git status has been read for gitFiles, the branch's dirty marker can
154
+ // count uncommitted files too, not only an unfinished merge or rebase.
155
+ const files = gitDetails?.files;
156
+ if (git && files && gitDetails.branch === git.branch) {
157
+ git.dirty = git.dirty || files.staged + files.modified + files.conflicts > 0;
158
+ }
148
159
  const contextPct = percent(contextUsedPercent(data));
149
160
  const contextSize = count(data.context_window?.context_window_size);
150
161
  const current = data.context_window?.current_usage;
@@ -224,7 +235,10 @@ function extractSession(data, config) {
224
235
  version: firstText(data.version),
225
236
  sandbox: data.sandbox?.enabled === true,
226
237
  sandboxNet: data.sandbox?.allow_network === true,
227
- git: resolveGit(data, config),
238
+ git,
239
+ gitDetails,
240
+ // Only read when tokensToday or tokensWeek is on the bar.
241
+ usage: resolveUsage(config, { read: options.readUsage === true }),
228
242
  terminalWidth: terminalWidth(data.terminal_width ?? process.stdout.columns ?? 0),
229
243
  };
230
244
  }
package/src/lib/shared.js CHANGED
@@ -54,6 +54,7 @@ const {
54
54
  } = require("./formatters");
55
55
 
56
56
  const { extractSession } = require("./session");
57
+ const { execPayloadJson } = require("./exec");
57
58
  const { fitWidgets, truncateLines, widestLine } = require("./fit");
58
59
 
59
60
  const {
@@ -92,12 +93,21 @@ function renderBar(data, config, overrides) {
92
93
  // sequences from every string before any layout prints it.
93
94
  // Valid JSON can still be null, a number or an array; read those as empty.
94
95
  const payload = data && typeof data === "object" && !Array.isArray(data) ? data : {};
95
- const session = sanitizeDeep(extractSession(payload, resolved));
96
+ const session = sanitizeDeep(
97
+ extractSession(payload, resolved, { readUsage: overrides.usage === "read" })
98
+ );
96
99
  // Exec widgets only run when the caller opts in ("run"); CLI previews keep
97
100
  // the default placeholder so rendering a config never executes it.
98
101
  session.execMode =
99
102
  overrides.exec === "run" || overrides.exec === "blocked" ? overrides.exec : "preview";
100
103
  session.execMemo = new Map();
104
+ // Built only when a command will run and asked for it.
105
+ if (
106
+ session.execMode === "run" &&
107
+ resolved.widgets.some((widget) => widget.id === "exec" && widget.payload === true)
108
+ ) {
109
+ session.execPayload = execPayloadJson(payload);
110
+ }
101
111
 
102
112
  const prevDepth = getActiveDepth();
103
113
 
package/src/lib/themes.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const { parseColor } = require("./colors");
3
+ const { parseColor, parseGradientStops, luminance } = require("./colors");
4
4
 
5
5
  // Every theme targets one terminal background:
6
6
  // dark-* bright colors, dark pill text, for dark terminals
@@ -9,10 +9,10 @@ const { parseColor } = require("./colors");
9
9
  // protanopia, deuteranopia and tritanopia
10
10
  // Most dark-* and light-* colors come from VS Code's default themes (Dark+
11
11
  // and Light+ syntax, git decorations, warning/error and terminal colors);
12
- // nocturne, onyx, metro and cream take their palettes from community themes
13
- // (credited below). A few colors are adjusted where the checks below need
14
- // it: darkened or lightened for contrast, and gitDirty moved toward orange so
15
- // a dirty branch never looks like the caution state.
12
+ // nocturne, onyx, metro, cream and macchiato take their palettes from
13
+ // community themes (credited below). A few colors are adjusted where the
14
+ // checks below need it: darkened or lightened for contrast, and gitDirty
15
+ // moved toward orange so a dirty branch never looks like the caution state.
16
16
  // Palettes are checked by tests/shared/themes.test.js: pill text and lines
17
17
  // text reach WCAG AA (4.5:1), the ok / caution / danger states are clearly
18
18
  // different from each other, and so are gitDirty and caution.
@@ -175,6 +175,22 @@ const THEMES = {
175
175
  warn: "#f7768e",
176
176
  }),
177
177
 
178
+ // Palette from Catppuccin Macchiato (catppuccin.com, MIT).
179
+ "dark-macchiato": theme("dark", "Dark Macchiato", "Pastel mauve, teal, peach and green", {
180
+ text: DARK_TEXT,
181
+ track: "#494d64",
182
+ model: "#c6a0f6",
183
+ fiveHour: "#a6da95",
184
+ weekly: "#7dc4e4",
185
+ thirdParty: "#f5a97f",
186
+ context: "#8aadf4",
187
+ git: "#8bd5ca",
188
+ gitDirty: "#f0c6c6",
189
+ meta: "#b8c0e0",
190
+ caution: "#eed49f",
191
+ warn: "#ed8796",
192
+ }),
193
+
178
194
  "light-paper": theme("light", "Light Paper", "VS Code Light+ colors", {
179
195
  text: LIGHT_TEXT,
180
196
  track: "#c8c8c8",
@@ -311,6 +327,7 @@ const THEME_ORDER = [
311
327
  "dark-nocturne",
312
328
  "dark-onyx",
313
329
  "dark-metro",
330
+ "dark-macchiato",
314
331
  "light-paper",
315
332
  "light-sky",
316
333
  "light-sunset",
@@ -398,6 +415,57 @@ function themeColors(themeId, customThemeConfig) {
398
415
  return resolveTheme(themeId, customThemeConfig).colors;
399
416
  }
400
417
 
418
+ // Row gradients. Pill text and Lines text are adjusted per color to stay
419
+ // readable, so a preset works on dark and light terminals alike.
420
+ const GRADIENTS = {
421
+ aurora: { name: "Aurora", stops: ["#00c9a7", "#4facfe", "#a18cd1"] },
422
+ sunset: { name: "Sunset", stops: ["#ffd166", "#ff8c61", "#e8457c"] },
423
+ ocean: { name: "Ocean", stops: ["#2af598", "#009efd"] },
424
+ forest: { name: "Forest", stops: ["#d4fc79", "#3fb68b", "#1d7a5f"] },
425
+ neon: { name: "Neon", stops: ["#f72585", "#7209b7", "#4cc9f0"] },
426
+ candy: { name: "Candy", stops: ["#ff9a9e", "#fbc2eb", "#a1c4fd"] },
427
+ rainbow: { name: "Rainbow", stops: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"] },
428
+ mono: { name: "Mono", stops: ["#e0e0e0", "#6e6e6e"] },
429
+ };
430
+
431
+ const GRADIENT_ORDER = Object.keys(GRADIENTS);
432
+
433
+ const toHex = (rgb) => `#${rgb.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
434
+
435
+ // A preset name, or hex stops written as "#rrggbb-#rrggbb"; null when the
436
+ // value is anything else (or "off").
437
+ function normalizeGradient(value) {
438
+ if (typeof value !== "string") {
439
+ return null;
440
+ }
441
+ const key = value.trim().toLowerCase();
442
+ if (GRADIENTS[key]) {
443
+ return key;
444
+ }
445
+ const stops = parseGradientStops(key);
446
+ return stops ? stops.map(toHex).join("-") : null;
447
+ }
448
+
449
+ function gradientStops(value) {
450
+ const key = normalizeGradient(value);
451
+ if (!key) {
452
+ return null;
453
+ }
454
+ return GRADIENTS[key]
455
+ ? GRADIENTS[key].stops.map((stop) => parseColor(stop))
456
+ : parseGradientStops(key);
457
+ }
458
+
459
+ // Typical terminal backgrounds, a little toward the hard side. Themes for
460
+ // light terminals have light pill text on deep colors; dark themes the reverse.
461
+ const TERMINAL_BACKGROUNDS = { dark: [40, 42, 54], light: [245, 245, 240] };
462
+
463
+ function terminalBackground(colors) {
464
+ return luminance(colors?.text ?? [0, 0, 0]) > 0.5
465
+ ? TERMINAL_BACKGROUNDS.light
466
+ : TERMINAL_BACKGROUNDS.dark;
467
+ }
468
+
401
469
  module.exports = {
402
470
  THEMES,
403
471
  THEME_ORDER,
@@ -411,4 +479,10 @@ module.exports = {
411
479
  resolveThemeId,
412
480
  resolveTheme,
413
481
  themeColors,
482
+ GRADIENTS,
483
+ GRADIENT_ORDER,
484
+ normalizeGradient,
485
+ gradientStops,
486
+ TERMINAL_BACKGROUNDS,
487
+ terminalBackground,
414
488
  };