@prave/cli 1.4.11 → 1.4.12

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.
@@ -1,14 +1,39 @@
1
- import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
1
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
3
  import { CONFIG } from './config.js';
4
+ /**
5
+ * Persist credentials atomically.
6
+ *
7
+ * The PostToolUse hook fires once per tool call, so two concurrent CLI
8
+ * processes refreshing the access token at the same second is the
9
+ * common case, not the edge. A plain `writeFile` then leaves the file
10
+ * in a partial / interleaved state for the brief window the bytes are
11
+ * being flushed — a concurrent `loadCredentials` reads that partial
12
+ * blob, `JSON.parse` throws, the catch swallows it and returns null,
13
+ * and the user is silently treated as logged out for every subsequent
14
+ * hook fire until they manually run `prave login` again.
15
+ *
16
+ * Writing to a sibling temp file and renaming over the target is
17
+ * atomic on POSIX (rename(2)) so readers either see the previous
18
+ * complete file or the new complete file — never a half-written one.
19
+ */
4
20
  export async function saveCredentials(creds) {
5
- await mkdir(dirname(CONFIG.credentialsPath), { recursive: true });
6
- await writeFile(CONFIG.credentialsPath, JSON.stringify(creds, null, 2), 'utf8');
7
- await chmod(CONFIG.credentialsPath, 0o600);
21
+ const target = CONFIG.credentialsPath;
22
+ await mkdir(dirname(target), { recursive: true });
23
+ // Per-process tmp suffix so two parallel writers don't stomp each
24
+ // other's tmp file — the OS still serialises the final rename.
25
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
26
+ await writeFile(tmp, JSON.stringify(creds, null, 2), 'utf8');
27
+ await chmod(tmp, 0o600);
28
+ await rename(tmp, target);
8
29
  }
9
30
  export async function loadCredentials() {
10
31
  try {
11
32
  const raw = await readFile(CONFIG.credentialsPath, 'utf8');
33
+ // Belt + suspenders: even with atomic writes, a credentials file
34
+ // can land truncated if a previous CLI version was killed mid-write
35
+ // or the FS had a power loss. Treat unparseable content as logged
36
+ // out instead of crashing — the next `prave login` repairs it.
12
37
  return JSON.parse(raw);
13
38
  }
14
39
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prave/cli",
3
- "version": "1.4.11",
3
+ "version": "1.4.12",
4
4
  "description": "Prave CLI — discover, install, version, test, and ship Claude Skills. The developer platform for the complete Skill lifecycle.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -54,7 +54,7 @@
54
54
  "ora": "^8.0.1",
55
55
  "tar": "^7.4.3",
56
56
  "undici": "^6.18.0",
57
- "@prave/shared": "1.4.11"
57
+ "@prave/shared": "1.4.12"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.12.7",
@@ -71,6 +71,6 @@
71
71
  "build": "tsc -p tsconfig.json && node scripts/inject-config.mjs",
72
72
  "typecheck": "tsc -p tsconfig.json --noEmit",
73
73
  "lint": "tsc -p tsconfig.json --noEmit",
74
- "postinstall": "node scripts/postinstall.mjs || true"
74
+ "postinstall": "test -f scripts/postinstall.mjs && node scripts/postinstall.mjs || true"
75
75
  }
76
76
  }