agy-powerline 1.1.0 → 1.1.1

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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.1] - 2026-10-12
9
+
10
+ ### Changed
11
+
12
+ - The statusline is installed as a single `statusline.js` instead of about 16 files, which cuts agy-powerline's own share of each refresh by about a third on Windows; run `npx agy-powerline init --yes` to switch, and `init` removes the old files (#23)
13
+ - The ahead/behind counts are kept per pair of commits, so a branch that is ahead of or behind its upstream no longer starts `git` on every refresh (about 60 ms each on Windows) (#24)
14
+
8
15
  ## [1.1.0] - 2026-10-12
9
16
 
10
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-powerline",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Interactive statusline for Google Antigravity",
5
5
  "bin": {
6
6
  "agy-powerline": "src/index.js"
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ // Builds the installed statusline.js: the runtime script and the library
4
+ // files in RUNTIME_FILES, joined into one file. Node loads a single file much
5
+ // faster than ~16 small ones (on Windows, opening each file costs more than
6
+ // running its code), and Antigravity starts the script on every refresh.
7
+ //
8
+ // Each file keeps its own scope: its source is wrapped in a function that
9
+ // gets its own module, exports and require, the way Node wraps a file. No
10
+ // eval, no code generation beyond that wrapper. LINES records where each file
11
+ // starts, so the problem log can still name the original file and line.
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const { RUNTIME_FILES } = require("../lib/paths");
16
+
17
+ const LIB_DIR = path.join(__dirname, "..", "lib");
18
+ const ENTRY = path.join(__dirname, "..", "runtime", "statusline.js");
19
+
20
+ // Requires between bundled files resolve inside the bundle; anything else
21
+ // (Node built-ins) goes to Node's own require.
22
+ const PRELUDE = `const __files = {};
23
+ const __cache = {};
24
+ function __require(from, id) {
25
+ if (!id.startsWith(".")) {
26
+ return require(id);
27
+ }
28
+ let key = require("path").posix.join(require("path").posix.dirname(from), id);
29
+ if (!key.endsWith(".js")) {
30
+ key += ".js";
31
+ }
32
+ if (!__files[key]) {
33
+ const err = new Error(\`Cannot find module '\${id}' from '\${from}'\`);
34
+ err.code = "MODULE_NOT_FOUND";
35
+ throw err;
36
+ }
37
+ if (!__cache[key]) {
38
+ const module = { exports: {} };
39
+ __cache[key] = module;
40
+ __files[key](module, module.exports, (next) => __require(key, next));
41
+ }
42
+ return __cache[key].exports;
43
+ }
44
+ `;
45
+
46
+ function toKey(rel) {
47
+ return rel.split(path.sep).join("/");
48
+ }
49
+
50
+ function wrapperLine(key) {
51
+ return `__files[${JSON.stringify(key)}] = function (module, exports, require) {`;
52
+ }
53
+
54
+ function buildRuntimeBundle() {
55
+ const files = [
56
+ ...RUNTIME_FILES.map((rel) => [toKey(rel), path.join(LIB_DIR, rel)]),
57
+ ["statusline.js", ENTRY],
58
+ ];
59
+ const parts = ["#!/usr/bin/env node", PRELUDE];
60
+
61
+ for (const [key, file] of files) {
62
+ const body = fs
63
+ .readFileSync(file, "utf8")
64
+ .replace(/^#!.*\n/, "")
65
+ .replace(/\n+$/, "");
66
+ parts.push(wrapperLine(key), body, "};");
67
+ }
68
+
69
+ // [first line of the file in the bundle, file name]; 1-based like stack traces.
70
+ const text = parts.join("\n");
71
+ const bundleLines = text.split("\n");
72
+ const lines = files.map(([key]) => [bundleLines.indexOf(wrapperLine(key)) + 2, key]);
73
+
74
+ return [
75
+ text,
76
+ `__require("statusline.js", "./log").setBundleLines(${JSON.stringify(lines)});`,
77
+ `__require("statusline.js", "./statusline");`,
78
+ "",
79
+ ].join("\n");
80
+ }
81
+
82
+ module.exports = { buildRuntimeBundle };
package/src/cli/init.js CHANGED
@@ -18,9 +18,8 @@ const {
18
18
  cmdBreakingMessage,
19
19
  } = require("../lib/paths");
20
20
  const { createRl, confirm } = require("../lib/prompt");
21
+ const { buildRuntimeBundle } = require("./bundle");
21
22
 
22
- const RUNTIME_DIR = path.join(__dirname, "..", "runtime");
23
- const LIB_DIR = path.join(__dirname, "..", "lib");
24
23
  const DIM = "\x1b[2m";
25
24
  const BOLD = "\x1b[1m";
26
25
  const CYAN = "\x1b[36m";
@@ -29,6 +28,27 @@ const YELLOW = "\x1b[33m";
29
28
  const RED = "\x1b[31m";
30
29
  const RESET = "\x1b[0m";
31
30
 
31
+ // Up to 1.1.0 the runtime was installed as separate files next to
32
+ // statusline.js. They are part of the one-file statusline.js now; delete the
33
+ // old copies by name (regular files only), then the emptied layouts folder.
34
+ function removeSeparateRuntimeFiles(installDir) {
35
+ for (const rel of RUNTIME_FILES) {
36
+ const file = path.join(installDir, rel);
37
+ try {
38
+ if (fs.lstatSync(file).isFile()) {
39
+ fs.unlinkSync(file);
40
+ }
41
+ } catch {
42
+ // Not there: nothing to remove.
43
+ }
44
+ }
45
+ try {
46
+ fs.rmdirSync(path.join(installDir, "layouts"));
47
+ } catch {
48
+ // Missing, or holds something that isn't ours: leave it.
49
+ }
50
+ }
51
+
32
52
  function assertUnixNode() {
33
53
  if (!isWindowsNodeOnWsl()) {
34
54
  return;
@@ -104,13 +124,8 @@ async function initCommand(options = {}) {
104
124
  }
105
125
 
106
126
  if (writeScripts) {
107
- fs.copyFileSync(path.join(RUNTIME_DIR, "statusline.js"), destStatusline);
108
- for (const rel of RUNTIME_FILES) {
109
- const src = path.join(LIB_DIR, rel);
110
- const dest = path.join(installDir, rel);
111
- fs.mkdirSync(path.dirname(dest), { recursive: true });
112
- fs.copyFileSync(src, dest);
113
- }
127
+ fs.writeFileSync(destStatusline, buildRuntimeBundle(), "utf8");
128
+ removeSeparateRuntimeFiles(installDir);
114
129
 
115
130
  if (process.platform !== "win32") {
116
131
  try {
package/src/lib/git.js CHANGED
@@ -4,6 +4,14 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { logWarning } = require("./log");
6
6
  const { firstText } = require("./formatters");
7
+ const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
8
+
9
+ // Ahead/behind counts depend only on the two commits compared, so they are
10
+ // kept per commit pair: git runs again only when the branch or its upstream
11
+ // moves. Starting git costs ~60 ms on Windows, on every refresh otherwise.
12
+ const SYNC_CACHE_NAME = ".git-sync-cache.json";
13
+ const SYNC_CACHE_MAX = 20;
14
+ const COMMIT = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
7
15
 
8
16
  // Only an absolute folder that exists can belong to the user's session. A
9
17
  // relative path (a Windows path read on Linux, for one) resolves against
@@ -217,6 +225,54 @@ function upstreamRef(commonDir, branch) {
217
225
  return remote === "." ? merge : `refs/remotes/${remote}/${tracked}`;
218
226
  }
219
227
 
228
+ // Only next to a real install's config (like the exec cache); CLI previews
229
+ // and other copies don't write it.
230
+ function syncCachePath() {
231
+ try {
232
+ const { getConfigDir } = require("./config");
233
+ const dir = getConfigDir();
234
+ return dir && fs.existsSync(path.join(dir, "statusline.config.json"))
235
+ ? path.join(dir, SYNC_CACHE_NAME)
236
+ : null;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ function readSyncCache(file) {
243
+ try {
244
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
245
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
246
+ } catch {
247
+ return {};
248
+ }
249
+ }
250
+
251
+ function cachedSync(file, key) {
252
+ const value = file ? readSyncCache(file)[key] : null;
253
+ const valid =
254
+ Array.isArray(value) &&
255
+ value.length === 2 &&
256
+ value.every((n) => Number.isInteger(n) && n >= 0 && n <= 1e7);
257
+ return valid ? { ahead: value[0], behind: value[1] } : null;
258
+ }
259
+
260
+ function saveSync(file, key, sync) {
261
+ if (!file) {
262
+ return;
263
+ }
264
+ try {
265
+ // Newest last; the oldest pairs go once there are more than SYNC_CACHE_MAX.
266
+ const cache = readSyncCache(file);
267
+ delete cache[key];
268
+ cache[key] = [sync.ahead, sync.behind];
269
+ const kept = Object.fromEntries(Object.entries(cache).slice(-SYNC_CACHE_MAX));
270
+ writeFileAtomic(file, JSON.stringify(kept), { mode: PRIVATE_FILE_MODE });
271
+ } catch {
272
+ // Best effort: the next refresh runs git again.
273
+ }
274
+ }
275
+
220
276
  function getGitSync(gitDir, branch) {
221
277
  if (!gitDir || !isSafeBranchName(branch)) {
222
278
  return { ahead: 0, behind: 0 };
@@ -232,10 +288,19 @@ function getGitSync(gitDir, branch) {
232
288
  return { ahead: 0, behind: 0 };
233
289
  }
234
290
 
235
- if (readRef(commonDir, upstream) === localHash) {
291
+ const upstreamHash = readRef(commonDir, upstream);
292
+ if (upstreamHash === localHash) {
236
293
  return { ahead: 0, behind: 0 };
237
294
  }
238
295
 
296
+ const key =
297
+ COMMIT.test(localHash) && COMMIT.test(upstreamHash) ? `${localHash}..${upstreamHash}` : "";
298
+ const cacheFile = key ? syncCachePath() : null;
299
+ const cached = cachedSync(cacheFile, key);
300
+ if (cached) {
301
+ return cached;
302
+ }
303
+
239
304
  const { execFileSync } = require("child_process");
240
305
  const out = execFileSync(
241
306
  "git",
@@ -253,10 +318,12 @@ function getGitSync(gitDir, branch) {
253
318
 
254
319
  const parts = out.split(/\s+/);
255
320
  if (parts.length >= 2) {
256
- return {
321
+ const sync = {
257
322
  ahead: parseInt(parts[0], 10) || 0,
258
323
  behind: parseInt(parts[1], 10) || 0,
259
324
  };
325
+ saveSync(cacheFile, key, sync);
326
+ return sync;
260
327
  }
261
328
  } catch (err) {
262
329
  // A timeout or missing git is worth knowing about when ahead/behind
@@ -327,4 +394,5 @@ module.exports = {
327
394
  gitHasDirtyMarkers,
328
395
  vcsFromPayload,
329
396
  resolveGit,
397
+ SYNC_CACHE_NAME,
330
398
  };
package/src/lib/log.js CHANGED
@@ -28,6 +28,28 @@ const IN_REPO = path.basename(__dirname) === "lib";
28
28
  let logDir = null;
29
29
  let context = {};
30
30
 
31
+ // The installed statusline.js is the runtime files joined into one (see
32
+ // src/cli/bundle.js). [first line, file name] for each of them, so a stack
33
+ // frame can still name the original file and line.
34
+ let bundleLines = null;
35
+
36
+ function setBundleLines(lines) {
37
+ bundleLines = Array.isArray(lines) ? lines : null;
38
+ }
39
+
40
+ function originalFrame(rel, line) {
41
+ if (!bundleLines || rel !== "statusline.js") {
42
+ return `${rel}:${line}`;
43
+ }
44
+ let match = null;
45
+ for (const entry of bundleLines) {
46
+ if (entry[0] <= line) {
47
+ match = entry;
48
+ }
49
+ }
50
+ return match ? `${match[1]}:${line - match[0] + 1}` : `${rel}:${line}`;
51
+ }
52
+
31
53
  // The copied runtime has no package.json; init records the version in
32
54
  // install.json instead.
33
55
  function agyPowerlineVersion() {
@@ -139,7 +161,7 @@ function ourFrames(stack) {
139
161
  continue;
140
162
  }
141
163
  const fn = match[1] && /^[\w$.<> ]{1,60}$/.test(match[1]) ? `${match[1]} ` : "";
142
- frames.push(`${fn}${rel.split(path.sep).join("/")}:${match[3]}`);
164
+ frames.push(`${fn}${originalFrame(rel.split(path.sep).join("/"), Number(match[3]))}`);
143
165
  if (frames.length >= 8) {
144
166
  break;
145
167
  }
@@ -283,4 +305,5 @@ module.exports = {
283
305
  logError,
284
306
  logWarning,
285
307
  clearLog,
308
+ setBundleLines,
286
309
  };
package/src/lib/paths.js CHANGED
@@ -8,7 +8,8 @@ const AGY_SETTINGS = path.join(os.homedir(), ".gemini", "antigravity-cli", "sett
8
8
 
9
9
  const INSTALL_META_NAME = "install.json";
10
10
 
11
- // Library files init copies next to statusline.js (paths relative to src/lib).
11
+ // Library files the runtime needs (paths relative to src/lib). init joins them
12
+ // with src/runtime/statusline.js into the one installed statusline.js.
12
13
  const RUNTIME_FILES = [
13
14
  "shared.js",
14
15
  "colors.js",
@@ -35,13 +36,16 @@ const PRIVATE_FILES = [
35
36
  "statusline.config.json",
36
37
  ".exec-cache.json",
37
38
  "last-payload.json",
39
+ // SYNC_CACHE_NAME in git.js: commit IDs of the repos you work in.
40
+ ".git-sync-cache.json",
38
41
  // LOG_NAME in log.js, spelled out because log.js requires this file.
39
42
  "agy-powerline.log",
40
43
  ];
41
44
  const PRIVATE_FILE_MODE = 0o600;
42
45
 
43
46
  // Everything init or the runtime may create in the install dir. uninstall
44
- // --delete removes only these, never the whole directory tree.
47
+ // --delete removes only these, never the whole directory tree. RUNTIME_FILES
48
+ // are listed for installs from 1.1.0 and earlier, which copied them separately.
45
49
  const INSTALLED_FILES = [
46
50
  "statusline.js",
47
51
  "statusline.cmd",