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 +7 -0
- package/package.json +1 -1
- package/src/cli/bundle.js +82 -0
- package/src/cli/init.js +24 -9
- package/src/lib/git.js +70 -2
- package/src/lib/log.js +24 -1
- package/src/lib/paths.js +6 -2
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
|
@@ -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.
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("/")
|
|
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
|
|
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",
|