agy-powerline 1.0.0 → 1.0.2

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,19 @@ 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.0.2] - 2026-10-11
9
+
10
+ ### Security
11
+
12
+ - On macOS and Linux, the config, `exec` cache, problem log and payload dump are now readable by your account only, and saving Antigravity's `settings.json` keeps its permissions. Run `agy-powerline init --yes` after updating to apply this to existing files (#11)
13
+
14
+ ## [1.0.1] - 2026-10-11
15
+
16
+ ### Fixed
17
+
18
+ - WSL: the install notes and the Windows-Node error now recommend Node.js 20+ installed for your user (for example with nvm) instead of Ubuntu's often too old `/usr/bin/node` (#6)
19
+ - The git widget's uncommitted-changes color no longer looks like the caution color in the `light-paper`, `light-sky`, `light-sunset`, `dark-onyx`, `dark-metro` and `colorblind-light` themes (#7)
20
+
8
21
  ## [1.0.0] - 2026-10-11
9
22
 
10
23
  First release of **agy-powerline**, a statusline for Google Antigravity's CLI (`agy`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-powerline",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Interactive statusline for Google Antigravity",
5
5
  "bin": {
6
6
  "agy-powerline": "src/index.js"
package/src/cli/init.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  readSettingsForUpdate,
12
12
  settingsBackupPath,
13
13
  writeJsonSafely,
14
+ restrictPrivateFiles,
14
15
  isWindowsNodeOnWsl,
15
16
  windowsNodeOnWslMessage,
16
17
  cmdBreakingChars,
@@ -81,6 +82,7 @@ async function initCommand(options = {}) {
81
82
 
82
83
  try {
83
84
  fs.mkdirSync(installDir, { recursive: true });
85
+ restrictPrivateFiles(installDir);
84
86
 
85
87
  const destStatusline = path.join(installDir, "statusline.js");
86
88
  const destConfig = path.join(installDir, "statusline.config.json");
package/src/lib/config.js CHANGED
@@ -6,7 +6,7 @@ const { resolveColorModeId } = require("./colors");
6
6
  const { resolveThemeId } = require("./themes");
7
7
  const { stripControl, normalizeClock } = require("./formatters");
8
8
  const { normalizePowerline } = require("./powerline");
9
- const { writeFileAtomic } = require("./paths");
9
+ const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
10
10
 
11
11
  // Display text in widget configs is printed as-is, so strip terminal control
12
12
  // from it. cmd/command are left alone: they are executed, not printed.
@@ -225,7 +225,9 @@ function saveConfig(config) {
225
225
  throw new InvalidConfigError(getConfigPath(), current.error);
226
226
  }
227
227
 
228
- writeFileAtomic(getConfigPath(), JSON.stringify(mergeConfig(config), null, 2) + "\n");
228
+ writeFileAtomic(getConfigPath(), JSON.stringify(mergeConfig(config), null, 2) + "\n", {
229
+ mode: PRIVATE_FILE_MODE,
230
+ });
229
231
  }
230
232
 
231
233
  // Presets set the widget list only; the layout (Pills or Lines) stays the one
package/src/lib/exec.js CHANGED
@@ -7,6 +7,7 @@ const crypto = require("crypto");
7
7
  const { execSync } = require("child_process");
8
8
  const { stripControl } = require("./formatters");
9
9
  const { logWarning } = require("./log");
10
+ const { PRIVATE_FILE_MODE } = require("./paths");
10
11
 
11
12
  const DEFAULT_TIMEOUT_MS = 500;
12
13
  const DEFAULT_INTERVAL_SEC = 10;
@@ -54,7 +55,8 @@ function loadDiskCache() {
54
55
 
55
56
  // Several agy windows refresh at once, so the cache is written to a fresh temp
56
57
  // file and renamed into place: a reader never sees half a file. "wx" refuses a
57
- // file that already exists (or a planted link) in a shared temp folder.
58
+ // file that already exists (or a planted link) in a shared temp folder, and the
59
+ // file is owner-only because it holds the output of the user's commands.
58
60
  function saveDiskCache(cache) {
59
61
  let tmp = "";
60
62
  try {
@@ -64,7 +66,11 @@ function saveDiskCache(cache) {
64
66
  fs.mkdirSync(dir, { recursive: true });
65
67
  }
66
68
  tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
67
- fs.writeFileSync(tmp, JSON.stringify(cache), { encoding: "utf8", flag: "wx" });
69
+ fs.writeFileSync(tmp, JSON.stringify(cache), {
70
+ encoding: "utf8",
71
+ flag: "wx",
72
+ mode: PRIVATE_FILE_MODE,
73
+ });
68
74
  fs.renameSync(tmp, file);
69
75
  } catch {
70
76
  // The cache is best effort: skip this write rather than risk a torn file.
package/src/lib/log.js CHANGED
@@ -9,7 +9,7 @@
9
9
  const fs = require("fs");
10
10
  const os = require("os");
11
11
  const path = require("path");
12
- const { writeFileAtomic, readInstallMeta } = require("./paths");
12
+ const { writeFileAtomic, readInstallMeta, PRIVATE_FILE_MODE } = require("./paths");
13
13
  const { stripControl } = require("./formatters");
14
14
 
15
15
  const LOG_NAME = "agy-powerline.log";
@@ -246,7 +246,7 @@ function logEvent(level, code, details = {}) {
246
246
  lines = lines.slice(1);
247
247
  }
248
248
 
249
- writeFileAtomic(file, lines.join("\n") + "\n");
249
+ writeFileAtomic(file, lines.join("\n") + "\n", { mode: PRIVATE_FILE_MODE });
250
250
  } catch {
251
251
  // Read-only disk, full disk, permissions: the bar matters more.
252
252
  }
package/src/lib/paths.js CHANGED
@@ -28,17 +28,25 @@ const RUNTIME_FILES = [
28
28
  path.join("layouts", "pills.js"),
29
29
  ];
30
30
 
31
+ // Files that can hold the user's commands, their output or session details.
32
+ // They are written readable by the owner only, so other accounts on a shared
33
+ // machine can't read them (Windows profile folders are private already).
34
+ const PRIVATE_FILES = [
35
+ "statusline.config.json",
36
+ ".exec-cache.json",
37
+ "last-payload.json",
38
+ // LOG_NAME in log.js, spelled out because log.js requires this file.
39
+ "agy-powerline.log",
40
+ ];
41
+ const PRIVATE_FILE_MODE = 0o600;
42
+
31
43
  // Everything init or the runtime may create in the install dir. uninstall
32
44
  // --delete removes only these, never the whole directory tree.
33
45
  const INSTALLED_FILES = [
34
46
  "statusline.js",
35
47
  "statusline.cmd",
36
48
  INSTALL_META_NAME,
37
- "statusline.config.json",
38
- ".exec-cache.json",
39
- "last-payload.json",
40
- // LOG_NAME in log.js, spelled out because log.js requires this file.
41
- "agy-powerline.log",
49
+ ...PRIVATE_FILES,
42
50
  ...RUNTIME_FILES,
43
51
  ];
44
52
 
@@ -81,9 +89,9 @@ function windowsNodeOnWslMessage(execPath = process.execPath) {
81
89
  "This Node binary is the Windows build (a /mnt/c/... path).",
82
90
  ` ${execPath}`,
83
91
  "Antigravity on Ubuntu/WSL needs Linux Node, or settings will get a Windows command.",
84
- "Re-run with Linux Node:",
85
- " /usr/bin/node src/index.js init --yes",
86
- " /usr/bin/node $(which agy-powerline) init --yes",
92
+ "Install Node.js 20 or newer inside WSL (for example with nvm), then re-run:",
93
+ " npm install -g agy-powerline",
94
+ " agy-powerline init --yes",
87
95
  ].join("\n");
88
96
  }
89
97
 
@@ -238,11 +246,24 @@ function writeJsonSafely(file, data) {
238
246
  }
239
247
 
240
248
  // Writes via temp file + rename so an interrupted write cannot leave a
241
- // truncated file behind.
242
- function writeFileAtomic(file, body) {
249
+ // truncated file behind. The new file gets `mode` when given, otherwise the
250
+ // mode the existing file had, so rewriting a file never widens who can read
251
+ // it (the rename would otherwise reset it to the default).
252
+ function writeFileAtomic(file, body, { mode } = {}) {
243
253
  fs.mkdirSync(path.dirname(file), { recursive: true });
244
254
  const tmp = `${file}.${process.pid}.tmp`;
245
- fs.writeFileSync(tmp, body, "utf8");
255
+ const target = process.platform === "win32" ? undefined : (mode ?? existingMode(file));
256
+ fs.writeFileSync(tmp, body, { encoding: "utf8", mode: target ?? 0o666 });
257
+ if (target !== undefined) {
258
+ // The mode passed to writeFileSync is reduced by the umask; set it exactly.
259
+ // Some filesystems (e.g. Windows drives under WSL) refuse chmod; the write
260
+ // still matters more.
261
+ try {
262
+ fs.chmodSync(tmp, target);
263
+ } catch {
264
+ // Keep the mode writeFileSync gave it.
265
+ }
266
+ }
246
267
 
247
268
  try {
248
269
  fs.renameSync(tmp, file);
@@ -253,6 +274,29 @@ function writeFileAtomic(file, body) {
253
274
  }
254
275
  }
255
276
 
277
+ function existingMode(file) {
278
+ try {
279
+ return fs.statSync(file).mode & 0o777;
280
+ } catch {
281
+ return undefined;
282
+ }
283
+ }
284
+
285
+ // Installs made before the private-file change left these files readable by
286
+ // everyone; init narrows them. Best effort: a file we can't chmod stays as is.
287
+ function restrictPrivateFiles(installDir) {
288
+ if (process.platform === "win32") {
289
+ return;
290
+ }
291
+ for (const name of PRIVATE_FILES) {
292
+ try {
293
+ fs.chmodSync(path.join(installDir, name), PRIVATE_FILE_MODE);
294
+ } catch {
295
+ // Missing file, or not ours to change.
296
+ }
297
+ }
298
+ }
299
+
256
300
  function removeSettingsFile(file = AGY_SETTINGS) {
257
301
  if (fs.existsSync(file)) {
258
302
  fs.copyFileSync(file, settingsBackupPath(file));
@@ -305,6 +349,9 @@ module.exports = {
305
349
  INSTALL_META_NAME,
306
350
  RUNTIME_FILES,
307
351
  INSTALLED_FILES,
352
+ PRIVATE_FILES,
353
+ PRIVATE_FILE_MODE,
354
+ restrictPrivateFiles,
308
355
  resolveInstallDir,
309
356
  isTrustedInstallDir,
310
357
  statuslineCommand,
@@ -7,9 +7,9 @@
7
7
  // branch names become placeholders. A field Antigravity adds later is left
8
8
  // out until it is listed here.
9
9
 
10
- const fs = require("fs");
11
10
  const path = require("path");
12
11
  const { stripControl } = require("./formatters");
12
+ const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
13
13
 
14
14
  const NUMBER = "number";
15
15
  const BOOL = "boolean";
@@ -160,9 +160,9 @@ function writePayloadDump(target, rawInput) {
160
160
  }
161
161
 
162
162
  try {
163
- const resolved = path.resolve(target);
164
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
165
- fs.writeFileSync(resolved, JSON.stringify(body, null, 2) + "\n", "utf8");
163
+ writeFileAtomic(path.resolve(target), JSON.stringify(body, null, 2) + "\n", {
164
+ mode: PRIVATE_FILE_MODE,
165
+ });
166
166
  } catch {
167
167
  // Best-effort debug write; the bar matters more.
168
168
  }
package/src/lib/themes.js CHANGED
@@ -10,11 +10,12 @@ const { parseColor } = require("./colors");
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
12
  // nocturne, onyx, metro and cream take their palettes from community themes
13
- // (credited below). A few colors are darkened or lightened, same hue, where
14
- // the contrast checks need it.
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.
15
16
  // Palettes are checked by tests/shared/themes.test.js: pill text and lines
16
- // text reach WCAG AA (4.5:1), and the ok / caution / danger states are
17
- // clearly different from each other.
17
+ // text reach WCAG AA (4.5:1), the ok / caution / danger states are clearly
18
+ // different from each other, and so are gitDirty and caution.
18
19
  //
19
20
  // Color roles:
20
21
  // text pill text track empty dots in lines meters
@@ -152,7 +153,7 @@ const THEMES = {
152
153
  thirdParty: "#d19a66",
153
154
  context: "#61afef",
154
155
  git: "#abb2bf",
155
- gitDirty: "#d19a66",
156
+ gitDirty: "#e88a50",
156
157
  meta: "#abb2bf",
157
158
  caution: "#e5c07b",
158
159
  warn: "#e1727b",
@@ -168,7 +169,7 @@ const THEMES = {
168
169
  thirdParty: "#ff9e64",
169
170
  context: "#7aa2f7",
170
171
  git: "#73daca",
171
- gitDirty: "#ff9e64",
172
+ gitDirty: "#ff8b52",
172
173
  meta: "#a9b1d6",
173
174
  caution: "#e0af68",
174
175
  warn: "#f7768e",
@@ -183,7 +184,7 @@ const THEMES = {
183
184
  thirdParty: "#795e26",
184
185
  context: "#0451a5",
185
186
  git: "#001080",
186
- gitDirty: "#895503",
187
+ gitDirty: "#b04a00",
187
188
  meta: "#3b3b3b",
188
189
  caution: "#8d6503",
189
190
  warn: "#cd3131",
@@ -198,7 +199,7 @@ const THEMES = {
198
199
  thirdParty: "#0070c1",
199
200
  context: "#005fb8",
200
201
  git: "#001080",
201
- gitDirty: "#895503",
202
+ gitDirty: "#b04a00",
202
203
  meta: "#3b3b3b",
203
204
  caution: "#8d6503",
204
205
  warn: "#cd3131",
@@ -213,7 +214,7 @@ const THEMES = {
213
214
  thirdParty: "#795e26",
214
215
  context: "#0451a5",
215
216
  git: "#bc05bc",
216
- gitDirty: "#895503",
217
+ gitDirty: "#b04a00",
217
218
  meta: "#616161",
218
219
  caution: "#8d6503",
219
220
  warn: "#cd3131",
@@ -268,7 +269,7 @@ const THEMES = {
268
269
  thirdParty: "#8a5200",
269
270
  context: "#2856a8",
270
271
  git: "#4e4596",
271
- gitDirty: "#8a5200",
272
+ gitDirty: "#a64800",
272
273
  meta: "#4b5466",
273
274
  caution: "#876900",
274
275
  warn: "#87002d",