pi-kanban 0.2.0 → 0.3.1
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/README.md +51 -46
- package/extensions/kanban.ts +465 -465
- package/lib/git-branch.js +112 -112
- package/lib/pi-parsers.js +1000 -901
- package/lib/task-store.js +152 -136
- package/package.json +66 -59
- package/public/app.js +5859 -5750
- package/public/icons/icon-svg.svg +6 -6
- package/public/index.html +674 -674
- package/public/manifest.json +26 -26
- package/public/style.css +3760 -3616
- package/public/sw.js +60 -60
- package/server.js +602 -491
- package/themes/kanban-dark-default.json +21 -21
- package/themes/kanban-default.json +21 -21
- package/themes/pi-dark.json +21 -21
- package/themes/pi-light.json +21 -21
package/lib/git-branch.js
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const { spawn } = require('node:child_process');
|
|
4
|
-
|
|
5
|
-
// cwd -> { branch: string|null, headMtimeMs: number, isRepo: boolean, checkedAt: number }
|
|
6
|
-
const cache = new Map();
|
|
7
|
-
const inflight = new Map();
|
|
8
|
-
|
|
9
|
-
const MAX_CONCURRENT = 4;
|
|
10
|
-
const SPAWN_TIMEOUT_MS = 500;
|
|
11
|
-
// re-check non-repos every 30s so newly-cloned repos surface without a server restart
|
|
12
|
-
const NEGATIVE_TTL_MS = 30_000;
|
|
13
|
-
// skip statSync within this window of a known-good cache hit; keeps list endpoints from
|
|
14
|
-
// hitting the FS once per session per request when the same cwds repeat
|
|
15
|
-
const POSITIVE_TTL_MS = 2_000;
|
|
16
|
-
let running = 0;
|
|
17
|
-
const queue = [];
|
|
18
|
-
|
|
19
|
-
function pump() {
|
|
20
|
-
while (running < MAX_CONCURRENT && queue.length) {
|
|
21
|
-
const job = queue.shift();
|
|
22
|
-
running++;
|
|
23
|
-
job().finally(() => { running--; pump(); });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function headMtime(cwd) {
|
|
28
|
-
try { return fs.statSync(path.join(cwd, '.git', 'HEAD')).mtimeMs; }
|
|
29
|
-
catch { return null; }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function spawnGit(cwd) {
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
let stdout = '';
|
|
35
|
-
let done = false;
|
|
36
|
-
let proc;
|
|
37
|
-
try {
|
|
38
|
-
proc = spawn('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
39
|
-
windowsHide: true,
|
|
40
|
-
});
|
|
41
|
-
} catch { resolve(null); return; }
|
|
42
|
-
const timer = setTimeout(() => {
|
|
43
|
-
if (done) return;
|
|
44
|
-
done = true;
|
|
45
|
-
try { proc.kill(); } catch {}
|
|
46
|
-
resolve(null);
|
|
47
|
-
}, SPAWN_TIMEOUT_MS);
|
|
48
|
-
proc.stdout.on('data', (b) => { stdout += b.toString(); });
|
|
49
|
-
proc.on('error', () => {
|
|
50
|
-
if (done) return;
|
|
51
|
-
done = true; clearTimeout(timer); resolve(null);
|
|
52
|
-
});
|
|
53
|
-
proc.on('close', (code) => {
|
|
54
|
-
if (done) return;
|
|
55
|
-
done = true; clearTimeout(timer);
|
|
56
|
-
if (code !== 0) { resolve(null); return; }
|
|
57
|
-
const branch = stdout.trim();
|
|
58
|
-
resolve(branch || null);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getBranch(cwd, onResolved) {
|
|
64
|
-
if (!cwd) return null;
|
|
65
|
-
const entry = cache.get(cwd);
|
|
66
|
-
const now = Date.now();
|
|
67
|
-
|
|
68
|
-
if (entry && entry.isRepo && now - entry.checkedAt < POSITIVE_TTL_MS) {
|
|
69
|
-
return entry.branch;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const mtime = headMtime(cwd);
|
|
73
|
-
|
|
74
|
-
if (mtime === null) {
|
|
75
|
-
if (entry && entry.isRepo === false && now - entry.checkedAt < NEGATIVE_TTL_MS) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
cache.set(cwd, { branch: null, headMtimeMs: 0, isRepo: false, checkedAt: now });
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (entry && entry.isRepo && entry.headMtimeMs === mtime) {
|
|
83
|
-
entry.checkedAt = now;
|
|
84
|
-
return entry.branch;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
scheduleResolve(cwd, mtime, entry, onResolved);
|
|
88
|
-
return entry && entry.isRepo ? entry.branch : null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function scheduleResolve(cwd, mtime, prevEntry, onResolved) {
|
|
92
|
-
if (inflight.has(cwd)) {
|
|
93
|
-
if (onResolved) inflight.get(cwd).then((b) => {
|
|
94
|
-
if (!prevEntry || prevEntry.branch !== b) onResolved(b);
|
|
95
|
-
});
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const prevBranch = prevEntry ? prevEntry.branch : undefined;
|
|
99
|
-
const p = new Promise((resolve) => {
|
|
100
|
-
queue.push(async () => {
|
|
101
|
-
const branch = await spawnGit(cwd);
|
|
102
|
-
cache.set(cwd, { branch, headMtimeMs: mtime, isRepo: branch !== null, checkedAt: Date.now() });
|
|
103
|
-
inflight.delete(cwd);
|
|
104
|
-
resolve(branch);
|
|
105
|
-
});
|
|
106
|
-
pump();
|
|
107
|
-
});
|
|
108
|
-
inflight.set(cwd, p);
|
|
109
|
-
if (onResolved) p.then((b) => { if (b !== prevBranch) onResolved(b); });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = { getBranch };
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
// cwd -> { branch: string|null, headMtimeMs: number, isRepo: boolean, checkedAt: number }
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
const inflight = new Map();
|
|
8
|
+
|
|
9
|
+
const MAX_CONCURRENT = 4;
|
|
10
|
+
const SPAWN_TIMEOUT_MS = 500;
|
|
11
|
+
// re-check non-repos every 30s so newly-cloned repos surface without a server restart
|
|
12
|
+
const NEGATIVE_TTL_MS = 30_000;
|
|
13
|
+
// skip statSync within this window of a known-good cache hit; keeps list endpoints from
|
|
14
|
+
// hitting the FS once per session per request when the same cwds repeat
|
|
15
|
+
const POSITIVE_TTL_MS = 2_000;
|
|
16
|
+
let running = 0;
|
|
17
|
+
const queue = [];
|
|
18
|
+
|
|
19
|
+
function pump() {
|
|
20
|
+
while (running < MAX_CONCURRENT && queue.length) {
|
|
21
|
+
const job = queue.shift();
|
|
22
|
+
running++;
|
|
23
|
+
job().finally(() => { running--; pump(); });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function headMtime(cwd) {
|
|
28
|
+
try { return fs.statSync(path.join(cwd, '.git', 'HEAD')).mtimeMs; }
|
|
29
|
+
catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function spawnGit(cwd) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
let stdout = '';
|
|
35
|
+
let done = false;
|
|
36
|
+
let proc;
|
|
37
|
+
try {
|
|
38
|
+
proc = spawn('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
} catch { resolve(null); return; }
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
if (done) return;
|
|
44
|
+
done = true;
|
|
45
|
+
try { proc.kill(); } catch {}
|
|
46
|
+
resolve(null);
|
|
47
|
+
}, SPAWN_TIMEOUT_MS);
|
|
48
|
+
proc.stdout.on('data', (b) => { stdout += b.toString(); });
|
|
49
|
+
proc.on('error', () => {
|
|
50
|
+
if (done) return;
|
|
51
|
+
done = true; clearTimeout(timer); resolve(null);
|
|
52
|
+
});
|
|
53
|
+
proc.on('close', (code) => {
|
|
54
|
+
if (done) return;
|
|
55
|
+
done = true; clearTimeout(timer);
|
|
56
|
+
if (code !== 0) { resolve(null); return; }
|
|
57
|
+
const branch = stdout.trim();
|
|
58
|
+
resolve(branch || null);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getBranch(cwd, onResolved) {
|
|
64
|
+
if (!cwd) return null;
|
|
65
|
+
const entry = cache.get(cwd);
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
|
|
68
|
+
if (entry && entry.isRepo && now - entry.checkedAt < POSITIVE_TTL_MS) {
|
|
69
|
+
return entry.branch;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const mtime = headMtime(cwd);
|
|
73
|
+
|
|
74
|
+
if (mtime === null) {
|
|
75
|
+
if (entry && entry.isRepo === false && now - entry.checkedAt < NEGATIVE_TTL_MS) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
cache.set(cwd, { branch: null, headMtimeMs: 0, isRepo: false, checkedAt: now });
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (entry && entry.isRepo && entry.headMtimeMs === mtime) {
|
|
83
|
+
entry.checkedAt = now;
|
|
84
|
+
return entry.branch;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
scheduleResolve(cwd, mtime, entry, onResolved);
|
|
88
|
+
return entry && entry.isRepo ? entry.branch : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scheduleResolve(cwd, mtime, prevEntry, onResolved) {
|
|
92
|
+
if (inflight.has(cwd)) {
|
|
93
|
+
if (onResolved) inflight.get(cwd).then((b) => {
|
|
94
|
+
if (!prevEntry || prevEntry.branch !== b) onResolved(b);
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const prevBranch = prevEntry ? prevEntry.branch : undefined;
|
|
99
|
+
const p = new Promise((resolve) => {
|
|
100
|
+
queue.push(async () => {
|
|
101
|
+
const branch = await spawnGit(cwd);
|
|
102
|
+
cache.set(cwd, { branch, headMtimeMs: mtime, isRepo: branch !== null, checkedAt: Date.now() });
|
|
103
|
+
inflight.delete(cwd);
|
|
104
|
+
resolve(branch);
|
|
105
|
+
});
|
|
106
|
+
pump();
|
|
107
|
+
});
|
|
108
|
+
inflight.set(cwd, p);
|
|
109
|
+
if (onResolved) p.then((b) => { if (b !== prevBranch) onResolved(b); });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { getBranch };
|