@giwonn/claude-daily-review 0.3.13 → 0.3.14
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/.claude-plugin/marketplace.json +1 -1
- package/hooks/recover-sessions.mjs +15 -9
- package/lib/github-storage.mjs +32 -18
- package/lib/storage.mjs +6 -2
- package/package.json +1 -1
|
@@ -23,19 +23,25 @@ function acquireLock() {
|
|
|
23
23
|
|
|
24
24
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const lockData = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(lockPath, lockData, { flag: 'wx' });
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
// File exists — check if stale
|
|
28
33
|
try {
|
|
29
34
|
const lock = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
30
35
|
const age = Date.now() - new Date(lock.timestamp).getTime();
|
|
31
|
-
if (age < LOCK_STALE_MS) return false;
|
|
32
|
-
|
|
36
|
+
if (age < LOCK_STALE_MS) return false;
|
|
37
|
+
unlinkSync(lockPath);
|
|
38
|
+
// Retry once after removing stale lock
|
|
39
|
+
try {
|
|
40
|
+
writeFileSync(lockPath, lockData, { flag: 'wx' });
|
|
41
|
+
return true;
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
} catch { return false; }
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
writeFileSync(lockPath, JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() }));
|
|
37
|
-
return true;
|
|
38
|
-
} catch { return false; }
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function releaseLock() {
|
package/lib/github-storage.mjs
CHANGED
|
@@ -15,21 +15,35 @@ export class GitHubStorageAdapter {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** @private @param {string} path @returns {string} */
|
|
18
|
-
getUrl(path) {
|
|
18
|
+
getUrl(path) {
|
|
19
|
+
if (path.split('/').includes('..')) throw new Error('Invalid path: traversal not allowed');
|
|
20
|
+
return this.basePath ? `${this.baseUrl}/${this.basePath}/${path}` : `${this.baseUrl}/${path}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @private
|
|
25
|
+
* @param {string} url
|
|
26
|
+
* @param {RequestInit} [options]
|
|
27
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
28
|
+
*/
|
|
29
|
+
async fetchOrNull(url, options) {
|
|
30
|
+
const res = await fetch(url, { ...options, headers: this.headers });
|
|
31
|
+
if (res.status === 404) return null;
|
|
32
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
|
|
33
|
+
return /** @type {Record<string, unknown>} */ (await res.json());
|
|
34
|
+
}
|
|
19
35
|
|
|
20
36
|
/** @private @param {string} path @returns {Promise<string | null>} */
|
|
21
37
|
async getSha(path) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
const data = /** @type {Record<string, unknown>} */ (await res.json());
|
|
38
|
+
const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
|
|
39
|
+
if (!data) return null;
|
|
25
40
|
return /** @type {string | null} */ (data.sha || null);
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
/** @param {string} path @returns {Promise<string | null>} */
|
|
29
44
|
async read(path) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
const data = /** @type {Record<string, unknown>} */ (await res.json());
|
|
45
|
+
const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
|
|
46
|
+
if (!data) return null;
|
|
33
47
|
return Buffer.from(/** @type {string} */ (data.content), 'base64').toString('utf-8');
|
|
34
48
|
}
|
|
35
49
|
|
|
@@ -40,10 +54,13 @@ export class GitHubStorageAdapter {
|
|
|
40
54
|
const body = { message: `update ${path}`, content: Buffer.from(content).toString('base64') };
|
|
41
55
|
if (sha) body.sha = sha;
|
|
42
56
|
const res = await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
|
|
43
|
-
if (
|
|
57
|
+
if (res.status === 409) {
|
|
44
58
|
const freshSha = await this.getSha(path);
|
|
45
59
|
if (freshSha) body.sha = freshSha;
|
|
46
|
-
await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
|
|
60
|
+
const retry = await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
|
|
61
|
+
if (!retry.ok) throw new Error(`GitHub API error: ${retry.status}`);
|
|
62
|
+
} else if (!res.ok) {
|
|
63
|
+
throw new Error(`GitHub API error: ${res.status}`);
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
66
|
|
|
@@ -55,16 +72,14 @@ export class GitHubStorageAdapter {
|
|
|
55
72
|
|
|
56
73
|
/** @param {string} path @returns {Promise<boolean>} */
|
|
57
74
|
async exists(path) {
|
|
58
|
-
const
|
|
59
|
-
return
|
|
75
|
+
const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
|
|
76
|
+
return data !== null;
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
/** @param {string} dir @returns {Promise<string[]>} */
|
|
63
80
|
async list(dir) {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
const data = await res.json();
|
|
67
|
-
if (!Array.isArray(data)) return [];
|
|
81
|
+
const data = await this.fetchOrNull(this.getUrl(dir), { method: 'GET' });
|
|
82
|
+
if (!data || !Array.isArray(data)) return [];
|
|
68
83
|
return data.map((/** @type {{ name: string }} */ entry) => entry.name);
|
|
69
84
|
}
|
|
70
85
|
|
|
@@ -73,9 +88,8 @@ export class GitHubStorageAdapter {
|
|
|
73
88
|
|
|
74
89
|
/** @param {string} path @returns {Promise<boolean>} */
|
|
75
90
|
async isDirectory(path) {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
const data = await res.json();
|
|
91
|
+
const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
|
|
92
|
+
if (!data) return false;
|
|
79
93
|
return Array.isArray(data);
|
|
80
94
|
}
|
|
81
95
|
}
|
package/lib/storage.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
3
3
|
|
|
4
4
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
5
|
-
import { dirname, join } from 'path';
|
|
5
|
+
import { dirname, join, resolve as pathResolve } from 'path';
|
|
6
6
|
|
|
7
7
|
/** @implements {StorageAdapter} */
|
|
8
8
|
export class LocalStorageAdapter {
|
|
@@ -14,7 +14,11 @@ export class LocalStorageAdapter {
|
|
|
14
14
|
|
|
15
15
|
/** @private @param {string} path @returns {string} */
|
|
16
16
|
resolve(path) {
|
|
17
|
-
|
|
17
|
+
const full = pathResolve(this.basePath, path);
|
|
18
|
+
if (full !== this.basePath && !full.startsWith(this.basePath + '/')) {
|
|
19
|
+
throw new Error('Invalid path: traversal outside base directory');
|
|
20
|
+
}
|
|
21
|
+
return full;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** @param {string} path @returns {Promise<string | null>} */
|
package/package.json
CHANGED