@coderook/cli 0.8.0 → 0.9.0

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.
@@ -42,15 +42,44 @@ function configDirectory() {
42
42
  }
43
43
  const tokenFile = () => node_path_1.default.join(configDirectory(), "token");
44
44
  const linksFile = () => node_path_1.default.join(configDirectory(), "links.json");
45
- /** Write a file only its owner can read, and atomically. */
45
+ /**
46
+ * Write a file only its owner can read, and atomically.
47
+ *
48
+ * The temporary name carries this process's own identity. Sharing one — as
49
+ * `${target}.tmp` did — means two runs at once write the same scratch file
50
+ * and then race to move it, which on Windows fails outright and elsewhere
51
+ * quietly hands one run the other's bytes.
52
+ *
53
+ * The move is retried briefly because Windows refuses a rename while another
54
+ * process still has the destination open, which during a race is ordinary
55
+ * rather than exceptional.
56
+ */
46
57
  async function writePrivate(target, body) {
47
58
  await (0, promises_1.mkdir)(node_path_1.default.dirname(target), { recursive: true });
48
- const temporary = `${target}.tmp`;
59
+ const temporary = `${target}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
49
60
  await (0, promises_1.writeFile)(temporary, body, "utf8");
50
61
  // Windows ignores the mode; on everything else this is what keeps the
51
62
  // token out of other accounts' reach.
52
63
  await (0, promises_1.chmod)(temporary, 0o600).catch(() => undefined);
53
- await (0, promises_1.rename)(temporary, target);
64
+ try {
65
+ for (let attempt = 0;; attempt += 1) {
66
+ try {
67
+ await (0, promises_1.rename)(temporary, target);
68
+ return;
69
+ }
70
+ catch (error) {
71
+ const code = error.code;
72
+ if (attempt >= 20 || (code !== "EPERM" && code !== "EACCES" && code !== "EBUSY")) {
73
+ throw error;
74
+ }
75
+ await new Promise((wake) => setTimeout(wake, 10 + attempt * 5));
76
+ }
77
+ }
78
+ }
79
+ finally {
80
+ // A failed move must not leave scratch files accumulating beside it.
81
+ await (0, promises_1.rm)(temporary, { force: true }).catch(() => undefined);
82
+ }
54
83
  }
55
84
  async function storeToken(token) {
56
85
  await writePrivate(tokenFile(), `${token.trim()}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",
@@ -32,12 +32,13 @@
32
32
  "test:matrix": "node test/state-matrix.mjs",
33
33
  "test:attempt": "node test/attempt-identity.mjs",
34
34
  "test:get": "node test/get-safety.mjs",
35
- "test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
35
+ "test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
36
36
  "test:getedits": "node test/get-protects-edits.mjs",
37
37
  "test:upgrade": "node test/upgrade-migration.mjs",
38
38
  "test:faultget": "node test/fault-get.mjs",
39
39
  "test:floor": "node test/version-floor.mjs",
40
- "test:faultsubmit": "node test/fault-submit.mjs"
40
+ "test:faultsubmit": "node test/fault-submit.mjs",
41
+ "test:race": "node test/race-attempts.mjs"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/node": "24.10.1",