agy-powerline 1.0.1 → 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 +6 -0
- package/package.json +1 -1
- package/src/cli/init.js +2 -0
- package/src/lib/config.js +4 -2
- package/src/lib/exec.js +8 -2
- package/src/lib/log.js +2 -2
- package/src/lib/paths.js +55 -8
- package/src/lib/payload-dump.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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
|
+
|
|
8
14
|
## [1.0.1] - 2026-10-11
|
|
9
15
|
|
|
10
16
|
### Fixed
|
package/package.json
CHANGED
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), {
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
package/src/lib/payload-dump.js
CHANGED
|
@@ -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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
}
|