sandtable 0.3.1 → 0.4.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.
- package/dashboard/dashboard.html +5 -3
- package/harness/install-hooks.sh +40 -4
- package/package.json +1 -1
- package/server.js +44 -2
- package/src/cli/sandtable.js +166 -8
- package/src/scanner/scan.js +3 -3
package/dashboard/dashboard.html
CHANGED
|
@@ -285,10 +285,12 @@ body.filter-active #left{border-right-color:#d5dce6}
|
|
|
285
285
|
<div id="tree-tabs">
|
|
286
286
|
<button class="on" data-c="all">全部</button>
|
|
287
287
|
<button data-c="roadmap">路线图</button>
|
|
288
|
+
<button data-c="todo">待办</button>
|
|
288
289
|
<button data-c="decision">决策</button>
|
|
289
290
|
<button data-c="spec">规格</button>
|
|
290
291
|
<button data-c="convention">纪律</button>
|
|
291
292
|
<button data-c="ops">运维</button>
|
|
293
|
+
|
|
292
294
|
<button data-c="archive">档案</button>
|
|
293
295
|
</div>
|
|
294
296
|
<div id="tree"></div>
|
|
@@ -358,9 +360,9 @@ function toggleTagCat(cn){
|
|
|
358
360
|
if(expandedTagCats[cn]){expandedTagCats[cn]=false}else{expandedTagCats[cn]=true}
|
|
359
361
|
renderTagBar();
|
|
360
362
|
}
|
|
361
|
-
var CLABEL = {roadmap:'路线图与进度',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
|
|
362
|
-
var PRIMARY = {roadmap:1,decision:1};
|
|
363
|
-
var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'
|
|
363
|
+
var CLABEL = {roadmap:'路线图与进度',todo:'待办清单',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
|
|
364
|
+
var PRIMARY = {roadmap:1,decision:1,todo:1};
|
|
365
|
+
var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'todo',todo:'todo',conclusion:'roadmap',decision:'decision',refactor:'decision',spec:'spec',intent:'spec',prompt:'spec',convention:'convention',agent:'ops',runbook:'ops',optimization:'roadmap',journal:'archive',handover:'archive',plan_doc:'archive',template:'template'};
|
|
364
366
|
function normCat(c){if(!c)return'unknown';var m={conventions:'convention',specs:'spec',decisions:'decision',plans:'roadmap',journals:'archive',journal:'archive'};return m[c]||c}
|
|
365
367
|
function normTg(tg){if(!tg||tg==='archived')return'past';return tg}
|
|
366
368
|
|
package/harness/install-hooks.sh
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Install sandtable git hooks into the current repo
|
|
3
|
-
# Usage: sh harness/install-hooks.sh [project-root]
|
|
3
|
+
# Usage: sh harness/install-hooks.sh [project-root] [--force]
|
|
4
|
+
|
|
5
|
+
ROOT=""
|
|
6
|
+
FORCE=0
|
|
7
|
+
for arg in "$@"; do
|
|
8
|
+
case "$arg" in
|
|
9
|
+
--force) FORCE=1 ;;
|
|
10
|
+
*) ROOT="$arg" ;;
|
|
11
|
+
esac
|
|
12
|
+
done
|
|
13
|
+
ROOT="${ROOT:-$(pwd)}"
|
|
4
14
|
|
|
5
|
-
ROOT="${1:-$(pwd)}"
|
|
6
15
|
HOOKS_DIR="$ROOT/.git/hooks"
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
17
|
|
|
8
18
|
if [ ! -d "$HOOKS_DIR" ]; then
|
|
9
19
|
echo "Error: not a git repository (no .git/hooks)"
|
|
10
20
|
exit 1
|
|
11
21
|
fi
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
# ---- Conflict detection: check for existing non-sandtable hooks ----
|
|
24
|
+
CONFLICTS=""
|
|
25
|
+
for hook in post-commit post-merge; do
|
|
26
|
+
HOOK_PATH="$HOOKS_DIR/$hook"
|
|
27
|
+
if [ -f "$HOOK_PATH" ] && ! grep -q "sandtable" "$HOOK_PATH" 2>/dev/null; then
|
|
28
|
+
CONFLICTS="$CONFLICTS $hook"
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [ -n "$CONFLICTS" ] && [ "$FORCE" != "1" ]; then
|
|
33
|
+
echo "Error: 已有非 sandtable hook 存在:$CONFLICTS"
|
|
34
|
+
echo ""
|
|
35
|
+
echo "sandtable 不会覆盖已有 hook,以免破坏其他工具链。选项:"
|
|
36
|
+
echo " 1. 手动合并: 在现有 hook 末尾添加以下行:"
|
|
37
|
+
echo " sh .git/hooks/<hook>.sandtable"
|
|
38
|
+
echo " 2. 强制覆盖: sh $(basename "$0") --force"
|
|
39
|
+
echo ""
|
|
40
|
+
echo "已有 hook 内容预览:"
|
|
41
|
+
for hook in $CONFLICTS; do
|
|
42
|
+
echo " --- $hook ---"
|
|
43
|
+
sed 's/^/ | /' "$HOOKS_DIR/$hook"
|
|
44
|
+
echo " -------------"
|
|
45
|
+
done
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
cp "$SCRIPT_DIR/post-commit" "$HOOKS_DIR/post-commit.sandtable"
|
|
50
|
+
cp "$SCRIPT_DIR/post-merge" "$HOOKS_DIR/post-merge.sandtable"
|
|
15
51
|
chmod +x "$HOOKS_DIR/post-commit.sandtable"
|
|
16
52
|
chmod +x "$HOOKS_DIR/post-merge.sandtable"
|
|
17
53
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -5,6 +5,26 @@ const path = require('path');
|
|
|
5
5
|
const ROOT = path.resolve(__dirname);
|
|
6
6
|
const PORT = parseInt(process.env.PORT || process.argv[2], 10) || 5199;
|
|
7
7
|
|
|
8
|
+
// Parse --host and --token from CLI args (safe by default: 127.0.0.1)
|
|
9
|
+
let HOST = '127.0.0.1';
|
|
10
|
+
let TOKEN = '';
|
|
11
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
12
|
+
if (process.argv[i] === '--host' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) {
|
|
13
|
+
HOST = process.argv[i + 1];
|
|
14
|
+
i++;
|
|
15
|
+
} else if (process.argv[i] === '--token') {
|
|
16
|
+
if (process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) {
|
|
17
|
+
TOKEN = process.argv[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
TOKEN = require('crypto').randomBytes(16).toString('hex');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if ((HOST === '0.0.0.0' || HOST === '::') && !TOKEN) {
|
|
25
|
+
TOKEN = require('crypto').randomBytes(16).toString('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
const MIME = {
|
|
9
29
|
'.html': 'text/html; charset=utf-8',
|
|
10
30
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -31,6 +51,27 @@ function isAllowed(filePath) {
|
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
http.createServer((req, res) => {
|
|
54
|
+
// Token authentication (when binding to public interface)
|
|
55
|
+
if (TOKEN) {
|
|
56
|
+
let qIdx = req.url.indexOf('?');
|
|
57
|
+
let hasToken = false;
|
|
58
|
+
if (qIdx !== -1) {
|
|
59
|
+
let qs = req.url.substring(qIdx + 1);
|
|
60
|
+
let pairs = qs.split('&');
|
|
61
|
+
for (let pi = 0; pi < pairs.length; pi++) {
|
|
62
|
+
let kv = pairs[pi].split('=');
|
|
63
|
+
if (decodeURIComponent(kv[0]) === 'token' && kv[1] === TOKEN) {
|
|
64
|
+
hasToken = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!hasToken) {
|
|
70
|
+
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
71
|
+
return res.end('401 Unauthorized — 请在 URL 后添加 ?token=' + TOKEN);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
let url = req.url === '/' ? '/dashboard/dashboard.html' : req.url;
|
|
35
76
|
url = url.replace(/^\/(css|js)\//, '/dashboard/$1/');
|
|
36
77
|
|
|
@@ -54,7 +95,8 @@ http.createServer((req, res) => {
|
|
|
54
95
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
55
96
|
res.end('500: ' + e.message);
|
|
56
97
|
}
|
|
57
|
-
}).listen(PORT, () => {
|
|
58
|
-
console.log('Sandtable: http://
|
|
98
|
+
}).listen(PORT, HOST, () => {
|
|
99
|
+
console.log('Sandtable: http://' + HOST + ':' + PORT);
|
|
100
|
+
if (TOKEN) console.log('Token: ' + TOKEN + ' (访问需携带 ?token=' + TOKEN + ')');
|
|
59
101
|
console.log('Project:', ROOT);
|
|
60
102
|
});
|
package/src/cli/sandtable.js
CHANGED
|
@@ -8,7 +8,7 @@ const { exec } = require('child_process');
|
|
|
8
8
|
const { scan } = require('../scanner/scan');
|
|
9
9
|
const { build, EVENT_TYPES, classifyEventPriority } = require('../builder/build');
|
|
10
10
|
|
|
11
|
-
const VERSION = '0.
|
|
11
|
+
const VERSION = '0.4.0';
|
|
12
12
|
const command = process.argv[2] || 'help';
|
|
13
13
|
const root = process.argv[3] || process.cwd();
|
|
14
14
|
|
|
@@ -279,6 +279,79 @@ function removeFromMainDoc(filePath, dryRun) {
|
|
|
279
279
|
return true;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
// ---- appendToGitignore: 自动追加 sandtable 条目到 .gitignore ----
|
|
283
|
+
// 使用 # sandtable:begin/end 标记包裹,uninstall 时精确擦除
|
|
284
|
+
function appendToGitignore(projectRoot, dryRun) {
|
|
285
|
+
var gitignorePath = path.join(projectRoot, '.gitignore');
|
|
286
|
+
var beginMark = '# sandtable:begin';
|
|
287
|
+
var endMark = '# sandtable:end';
|
|
288
|
+
var existing = '';
|
|
289
|
+
if (fs.existsSync(gitignorePath)) {
|
|
290
|
+
existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
291
|
+
if (existing.indexOf(beginMark) !== -1) {
|
|
292
|
+
if (!dryRun) console.log(' - .gitignore 已有 sandtable 标记,跳过');
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var block = [
|
|
298
|
+
'',
|
|
299
|
+
beginMark,
|
|
300
|
+
'# Sandtable 自动生成条目 — 请勿手动编辑此块',
|
|
301
|
+
'data/',
|
|
302
|
+
'.sandtable/token-log.jsonl',
|
|
303
|
+
'.sandtable/audit.log',
|
|
304
|
+
'.sandtable/.token',
|
|
305
|
+
endMark,
|
|
306
|
+
'',
|
|
307
|
+
].join('\n');
|
|
308
|
+
|
|
309
|
+
if (dryRun) {
|
|
310
|
+
console.log(' [DRY-RUN] 将追加到 .gitignore:');
|
|
311
|
+
console.log(' --- diff ---');
|
|
312
|
+
console.log(' + ' + block.replace(/\n/g, '\n + '));
|
|
313
|
+
console.log(' --- end ---');
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
writeWithBackup(gitignorePath, existing + block);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- removeFromGitignore: uninstall 时擦除 sandtable gitignore 块 ----
|
|
322
|
+
function removeFromGitignore(projectRoot, dryRun) {
|
|
323
|
+
var gitignorePath = path.join(projectRoot, '.gitignore');
|
|
324
|
+
var beginMark = '# sandtable:begin';
|
|
325
|
+
var endMark = '# sandtable:end';
|
|
326
|
+
|
|
327
|
+
if (!fs.existsSync(gitignorePath)) return false;
|
|
328
|
+
|
|
329
|
+
var content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
330
|
+
var beginIdx = content.indexOf(beginMark);
|
|
331
|
+
var endIdx = content.indexOf(endMark);
|
|
332
|
+
|
|
333
|
+
if (beginIdx === -1 || endIdx === -1) return false;
|
|
334
|
+
|
|
335
|
+
var beforeBegin = content.lastIndexOf('\n', beginIdx);
|
|
336
|
+
if (beforeBegin === -1) beforeBegin = 0;
|
|
337
|
+
|
|
338
|
+
var afterEnd = endIdx + endMark.length;
|
|
339
|
+
if (content[afterEnd] === '\n') afterEnd++;
|
|
340
|
+
if (content[afterEnd] === '\n') afterEnd++; // consume trailing blank line
|
|
341
|
+
|
|
342
|
+
if (dryRun) {
|
|
343
|
+
console.log(' [DRY-RUN] 将从 .gitignore 移除 sandtable 块:');
|
|
344
|
+
console.log(' --- to remove ---');
|
|
345
|
+
console.log(content.substring(beforeBegin, afterEnd).replace(/^/gm, ' - '));
|
|
346
|
+
console.log(' --- end ---');
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
var cleaned = content.substring(0, beforeBegin) + content.substring(afterEnd);
|
|
351
|
+
writeWithBackup(gitignorePath, cleaned);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
282
355
|
// ---- writeClaudeCodeHook: 创建 SessionStart 强提醒 hook ----
|
|
283
356
|
function writeClaudeCodeHook(projectRoot, lang) {
|
|
284
357
|
var hooksDir = path.join(projectRoot, '.claude', 'hooks');
|
|
@@ -432,7 +505,7 @@ function initCommand(projectRoot, applyMode, lang, hooksMode, injectAgentsMd) {
|
|
|
432
505
|
|
|
433
506
|
// 2. Write .sandtable/config.json
|
|
434
507
|
var config = {
|
|
435
|
-
version: '0.
|
|
508
|
+
version: '0.4.0',
|
|
436
509
|
createdAt: new Date().toISOString(),
|
|
437
510
|
projectRoot: projectRoot,
|
|
438
511
|
paths: {
|
|
@@ -510,6 +583,13 @@ function initCommand(projectRoot, applyMode, lang, hooksMode, injectAgentsMd) {
|
|
|
510
583
|
console.log(' ✓ 安装 SessionStart 强提醒 hook → .claude/hooks/sandtable-reminder.sh');
|
|
511
584
|
}
|
|
512
585
|
|
|
586
|
+
// 8. Append sandtable entries to .gitignore (auto, always)
|
|
587
|
+
var gitignoreDryRun = !applyMode;
|
|
588
|
+
appendToGitignore(projectRoot, gitignoreDryRun);
|
|
589
|
+
if (applyMode && fs.existsSync(path.join(projectRoot, '.gitignore'))) {
|
|
590
|
+
console.log(' ✓ 追加 sandtable 条目至 .gitignore');
|
|
591
|
+
}
|
|
592
|
+
|
|
513
593
|
console.log('');
|
|
514
594
|
console.log('✓ 初始化完成。运行 sandtable build 生成数据,然后 sandtable serve 查看仪表盘。');
|
|
515
595
|
|
|
@@ -592,7 +672,9 @@ function enableWatchMode(projectRoot) {
|
|
|
592
672
|
}
|
|
593
673
|
}
|
|
594
674
|
|
|
595
|
-
function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
675
|
+
function startServer(projectRoot, port, openBrowser, watchMode, host, token) {
|
|
676
|
+
host = host || '127.0.0.1';
|
|
677
|
+
token = token || '';
|
|
596
678
|
const MIME = {
|
|
597
679
|
'.html': 'text/html; charset=utf-8',
|
|
598
680
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -629,6 +711,27 @@ function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
|
629
711
|
}
|
|
630
712
|
|
|
631
713
|
http.createServer(function(req, res) {
|
|
714
|
+
// Token authentication (when binding to public interface)
|
|
715
|
+
if (token) {
|
|
716
|
+
var qIdx = req.url.indexOf('?');
|
|
717
|
+
var hasToken = false;
|
|
718
|
+
if (qIdx !== -1) {
|
|
719
|
+
var qs = req.url.substring(qIdx + 1);
|
|
720
|
+
var pairs = qs.split('&');
|
|
721
|
+
for (var pi = 0; pi < pairs.length; pi++) {
|
|
722
|
+
var kv = pairs[pi].split('=');
|
|
723
|
+
if (decodeURIComponent(kv[0]) === 'token' && kv[1] === token) {
|
|
724
|
+
hasToken = true;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!hasToken) {
|
|
730
|
+
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
731
|
+
return res.end('401 Unauthorized — 请在 URL 后添加 ?token=' + token);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
632
735
|
var url = req.url === '/' ? '/dashboard/dashboard.html' : req.url;
|
|
633
736
|
url = url.replace(/^\/(css|js)\//, '/dashboard/$1/');
|
|
634
737
|
|
|
@@ -666,9 +769,12 @@ function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
|
666
769
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
667
770
|
res.end('500: ' + e.message);
|
|
668
771
|
}
|
|
669
|
-
}).listen(port, function() {
|
|
670
|
-
console.log('Sandtable v' + VERSION + ' — http://
|
|
772
|
+
}).listen(port, host, function() {
|
|
773
|
+
console.log('Sandtable v' + VERSION + ' — http://' + host + ':' + port);
|
|
671
774
|
console.log('Project: ' + projectRoot);
|
|
775
|
+
if (token) {
|
|
776
|
+
console.log('Token: ' + token + ' (访问需携带 ?token=' + token + ')');
|
|
777
|
+
}
|
|
672
778
|
|
|
673
779
|
if (watchMode) enableWatchMode(projectRoot);
|
|
674
780
|
|
|
@@ -731,7 +837,40 @@ switch (command) {
|
|
|
731
837
|
case 'serve': {
|
|
732
838
|
var port = parseInt(process.argv[3], 10) || 5199;
|
|
733
839
|
var watchMode = process.argv.indexOf('--watch') !== -1;
|
|
734
|
-
|
|
840
|
+
|
|
841
|
+
// Parse --host (default: 127.0.0.1, safe by default)
|
|
842
|
+
var hostIdx = process.argv.indexOf('--host');
|
|
843
|
+
var host = '127.0.0.1';
|
|
844
|
+
if (hostIdx !== -1 && process.argv[hostIdx + 1] && process.argv[hostIdx + 1].indexOf('--') !== 0) {
|
|
845
|
+
host = process.argv[hostIdx + 1];
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Parse --token (required when binding to 0.0.0.0)
|
|
849
|
+
var tokenIdx = process.argv.indexOf('--token');
|
|
850
|
+
var token = '';
|
|
851
|
+
var tokenPath = path.join(process.cwd(), '.sandtable', '.token');
|
|
852
|
+
if (tokenIdx !== -1) {
|
|
853
|
+
var nextArg = process.argv[tokenIdx + 1];
|
|
854
|
+
if (nextArg && nextArg.indexOf('--') !== 0) {
|
|
855
|
+
token = nextArg;
|
|
856
|
+
// Persist user-provided token
|
|
857
|
+
try { fs.writeFileSync(tokenPath, token, 'utf-8'); } catch(e) {}
|
|
858
|
+
} else {
|
|
859
|
+
token = require('crypto').randomBytes(16).toString('hex');
|
|
860
|
+
try { fs.writeFileSync(tokenPath, token, 'utf-8'); } catch(e) {}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if ((host === '0.0.0.0' || host === '::') && !token) {
|
|
864
|
+
// Try to load persisted token first
|
|
865
|
+
try { if (fs.existsSync(tokenPath)) token = fs.readFileSync(tokenPath, 'utf-8').trim(); } catch(e) {}
|
|
866
|
+
if (!token) {
|
|
867
|
+
token = require('crypto').randomBytes(16).toString('hex');
|
|
868
|
+
try { fs.writeFileSync(tokenPath, token, 'utf-8'); } catch(e) {}
|
|
869
|
+
}
|
|
870
|
+
console.log('⚠ 绑定 ' + host + ' 将暴露服务到网络,token: ' + token);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
startServer(process.cwd(), port, true, watchMode, host, token);
|
|
735
874
|
break;
|
|
736
875
|
}
|
|
737
876
|
case 'event-log': {
|
|
@@ -902,7 +1041,9 @@ switch (command) {
|
|
|
902
1041
|
|
|
903
1042
|
var slugName = TYPE_SLUG_NAMES[typeId] || '';
|
|
904
1043
|
console.log('event-log: [' + event.priority + '] ' + event.type + ' — ' + title + (threadId ? ' (thread: ' + threadId + ')' : ''));
|
|
905
|
-
|
|
1044
|
+
if (!isNamedMode) {
|
|
1045
|
+
console.log(' (提示: 位置参数已弃用,下次推荐: --type ' + slugName + ' --title "...")');
|
|
1046
|
+
}
|
|
906
1047
|
|
|
907
1048
|
// --tokens-in + --tokens-out: auto-log token usage
|
|
908
1049
|
if (tokensIn > 0 || tokensOut > 0) {
|
|
@@ -1060,6 +1201,18 @@ case 'token-log': {
|
|
|
1060
1201
|
console.log('主文档中未找到 sandtable 注入标记。');
|
|
1061
1202
|
}
|
|
1062
1203
|
|
|
1204
|
+
// Check for sandtable gitignore block
|
|
1205
|
+
var gitignorePath = path.join(projectRoot, '.gitignore');
|
|
1206
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1207
|
+
var giContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1208
|
+
if (giContent.indexOf('# sandtable:begin') !== -1) {
|
|
1209
|
+
if (dryRun) {
|
|
1210
|
+
console.log('');
|
|
1211
|
+
removeFromGitignore(projectRoot, true);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1063
1216
|
if (dryRun) {
|
|
1064
1217
|
console.log('');
|
|
1065
1218
|
console.log('以上为 dry-run 预览。要实际删除,请运行:');
|
|
@@ -1067,6 +1220,7 @@ case 'token-log': {
|
|
|
1067
1220
|
console.log('');
|
|
1068
1221
|
console.log('注意:');
|
|
1069
1222
|
console.log(' - sandtable 注入标记 (<!-- sandtable:begin/end -->) 可被 uninstall --apply 精确擦除');
|
|
1223
|
+
console.log(' - .gitignore 中 sandtable 条目 (# sandtable:begin/end) 也会被移除');
|
|
1070
1224
|
console.log(' - 如果你在 AGENTS.md/CLAUDE.md 中手动添加了其他 sandtable 内容,请自行删除');
|
|
1071
1225
|
break;
|
|
1072
1226
|
}
|
|
@@ -1080,6 +1234,10 @@ case 'token-log': {
|
|
|
1080
1234
|
}
|
|
1081
1235
|
}
|
|
1082
1236
|
|
|
1237
|
+
// --apply: remove sandtable gitignore block
|
|
1238
|
+
var giRemoved = removeFromGitignore(projectRoot, false);
|
|
1239
|
+
if (giRemoved) console.log(' ✓ 从 .gitignore 移除 sandtable 条目');
|
|
1240
|
+
|
|
1083
1241
|
// --apply: delete files
|
|
1084
1242
|
console.log('');
|
|
1085
1243
|
console.log('正在删除...');
|
|
@@ -1132,7 +1290,7 @@ case 'token-log': {
|
|
|
1132
1290
|
console.log(' sandtable init [--apply] [--lang zh|en] 扫描环境 + 准备规则文件');
|
|
1133
1291
|
console.log(' sandtable scan [projectRoot] 扫描文档目录');
|
|
1134
1292
|
console.log(' sandtable build [projectRoot] 生成 data/*.json');
|
|
1135
|
-
console.log(' sandtable serve [port] [--watch]
|
|
1293
|
+
console.log(' sandtable serve [port] [--watch] [--host <ip>] [--token <token>] 启动服务 + 打开浏览器');
|
|
1136
1294
|
console.log(' sandtable summarize [projectRoot] 列出缺少摘要块的文件');
|
|
1137
1295
|
console.log(' sandtable event-log <typeId> <title> [...] 记录人机协同事件');
|
|
1138
1296
|
console.log(' sandtable token-log <skill> <in> <out> [...] 追加 token 消耗日志');
|
package/src/scanner/scan.js
CHANGED
|
@@ -45,10 +45,10 @@ const PRIMARY_TYPES = new Set([
|
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
// ---- MECE display categories (PM 6 大分类 + template) ----
|
|
48
|
-
// §10 libero PM
|
|
48
|
+
// §10 libero PM 视角 — 9 大分类
|
|
49
49
|
// Primary-only 类别 (roadmap, decision) 不出现在 filterType checkbox 中
|
|
50
50
|
const DISPLAY_CATEGORIES = {
|
|
51
|
-
roadmap: { label: '路线图与进度', elementTypes: ['phase', 'milestone', 'task', 'subtask', 'roadmap', 'backlog'] },
|
|
51
|
+
roadmap: { label: '路线图与进度', elementTypes: ['phase', 'milestone', 'task', 'subtask', 'roadmap', 'backlog', 'optimization', 'todo'] },
|
|
52
52
|
decision: { label: '决策记录', elementTypes: ['decision', 'refactor'] },
|
|
53
53
|
spec: { label: '业务规格', elementTypes: ['spec', 'intent', 'prompt'] },
|
|
54
54
|
convention: { label: '协作纪律', elementTypes: ['convention'] },
|
|
@@ -58,7 +58,7 @@ const DISPLAY_CATEGORIES = {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
// Primary-only categories (all elementTypes in these categories are kind=primary, show always)
|
|
61
|
-
const PRIMARY_CATEGORIES = new Set(['roadmap', 'decision']);
|
|
61
|
+
const PRIMARY_CATEGORIES = new Set(['roadmap', 'decision', 'todo']);
|
|
62
62
|
|
|
63
63
|
function normalizeCategory(elementType) {
|
|
64
64
|
for (const [cat, def] of Object.entries(DISPLAY_CATEGORIES)) {
|