sandtable 0.3.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/README.md +40 -0
- package/dashboard/css/dashboard.css +731 -0
- package/dashboard/dashboard.html +1086 -0
- package/dashboard/js/conventions-viewer.js +37 -0
- package/dashboard/js/data-loader.js +147 -0
- package/dashboard/js/event-stream-renderer.js +158 -0
- package/dashboard/js/filter-controller.js +102 -0
- package/dashboard/js/journal-timeline.js +29 -0
- package/dashboard/js/roadmap-renderer.js +72 -0
- package/dashboard/js/timeline-renderer.js +283 -0
- package/dashboard/js/waterfall-renderer.js +189 -0
- package/harness/install-hooks.sh +34 -0
- package/harness/post-commit +17 -0
- package/harness/post-merge +17 -0
- package/harness/summary-hook.md +18 -0
- package/package.json +38 -0
- package/server.js +60 -0
- package/skills/build-json.md +36 -0
- package/skills/scan-docs.md +38 -0
- package/skills/summarize.md +66 -0
- package/src/builder/build.js +1019 -0
- package/src/cli/sandtable.js +970 -0
- package/src/scanner/scan.js +415 -0
- package/templates/.sandtable.template.json +51 -0
- package/templates/journal-entry.md +22 -0
- package/templates/summary-block.md +42 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const { exec } = require('child_process');
|
|
8
|
+
const { scan } = require('../scanner/scan');
|
|
9
|
+
const { build, EVENT_TYPES, classifyEventPriority } = require('../builder/build');
|
|
10
|
+
|
|
11
|
+
const VERSION = '0.3.0';
|
|
12
|
+
const command = process.argv[2] || 'help';
|
|
13
|
+
const root = process.argv[3] || process.cwd();
|
|
14
|
+
|
|
15
|
+
// ---- detectEnvironment: 扫描项目,检测所有可用的 IDE 指令目录 ----
|
|
16
|
+
function detectEnvironment(projectRoot) {
|
|
17
|
+
var envs = [];
|
|
18
|
+
|
|
19
|
+
// 1. Cursor: .cursor/rules/
|
|
20
|
+
if (fs.existsSync(path.join(projectRoot, '.cursor', 'rules'))) {
|
|
21
|
+
envs.push({ type: 'cursor', targetDir: '.cursor/rules', targetFile: '.cursor/rules/sandtable-event-log.mdc', format: 'mdc', entryLine: null });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. libero: docs/skills/
|
|
25
|
+
if (fs.existsSync(path.join(projectRoot, 'docs', 'skills'))) {
|
|
26
|
+
envs.push({ type: 'docs-skills', targetDir: 'docs/skills', targetFile: 'docs/skills/sandtable-event-log.md', format: 'md', entryLine: null });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 3. Claude Code: .claude/
|
|
30
|
+
if (fs.existsSync(path.join(projectRoot, '.claude'))) {
|
|
31
|
+
envs.push({ type: 'claude', targetDir: '.claude/skills/sandtable-event-log', targetFile: '.claude/skills/sandtable-event-log/SKILL.md', format: 'skill-md', entryLine: null });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 4. Trae: .trae/rules/
|
|
35
|
+
if (fs.existsSync(path.join(projectRoot, '.trae', 'rules'))) {
|
|
36
|
+
envs.push({ type: 'trae', targetDir: '.trae/rules', targetFile: '.trae/rules/sandtable-event-log.md', format: 'md', entryLine: null });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 5. Cline: .clinerules/
|
|
40
|
+
if (fs.existsSync(path.join(projectRoot, '.clinerules'))) {
|
|
41
|
+
// .clinerules can be a file or directory
|
|
42
|
+
var clineStat = fs.statSync(path.join(projectRoot, '.clinerules'));
|
|
43
|
+
if (clineStat.isDirectory()) {
|
|
44
|
+
envs.push({ type: 'cline', targetDir: '.clinerules', targetFile: '.clinerules/sandtable-event-log.md', format: 'md', entryLine: null });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 6. GitHub Copilot: .github/
|
|
49
|
+
if (fs.existsSync(path.join(projectRoot, '.github'))) {
|
|
50
|
+
var githubDir = path.join(projectRoot, '.github', 'instructions');
|
|
51
|
+
if (!fs.existsSync(githubDir)) {
|
|
52
|
+
// Will create on --apply
|
|
53
|
+
}
|
|
54
|
+
envs.push({ type: 'github', targetDir: '.github/instructions', targetFile: '.github/instructions/sandtable-event-log.instructions.md', format: 'instructions-md', entryLine: null });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 7. AGENTS.md exists (but no exclusive directory matched above)
|
|
58
|
+
var hasExclusiveDir = envs.some(function(e) {
|
|
59
|
+
return e.type === 'cursor' || e.type === 'docs-skills' || e.type === 'claude' || e.type === 'trae' || e.type === 'cline' || e.type === 'github';
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!hasExclusiveDir) {
|
|
63
|
+
var agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
64
|
+
var claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(agentsPath)) {
|
|
67
|
+
envs.push({ type: 'agents', targetDir: '.sandtable', targetFile: '.sandtable/rules.md', format: 'rules-md',
|
|
68
|
+
entryLine: '--apply 时将自动注入事件日志规则到 AGENTS.md', autoInject: true });
|
|
69
|
+
} else if (fs.existsSync(claudeMdPath)) {
|
|
70
|
+
envs.push({ type: 'claude-md', targetDir: '.sandtable', targetFile: '.sandtable/rules.md', format: 'rules-md',
|
|
71
|
+
entryLine: '--apply 时将自动注入事件日志规则到 CLAUDE.md', autoInject: true });
|
|
72
|
+
} else {
|
|
73
|
+
// Nothing detected — offer generic
|
|
74
|
+
envs.push({ type: 'generic', targetDir: '.sandtable', targetFile: '.sandtable/rules.md', format: 'rules-md',
|
|
75
|
+
entryLine: '--apply 时将自动创建 AGENTS.md 并注入事件日志规则', autoInject: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return envs;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- generateRuleContent: 按格式和语言生成规则内容 ----
|
|
83
|
+
function generateRuleContent(format, lang) {
|
|
84
|
+
var isZh = lang === 'zh';
|
|
85
|
+
|
|
86
|
+
var bodyLines = [];
|
|
87
|
+
if (isZh) {
|
|
88
|
+
bodyLines.push('# Sandtable 事件记录');
|
|
89
|
+
bodyLines.push('');
|
|
90
|
+
bodyLines.push('当以下重要事件发生时,调用 `sandtable event-log` 记录到项目仪表盘。');
|
|
91
|
+
bodyLines.push('');
|
|
92
|
+
bodyLines.push('## 触发场景');
|
|
93
|
+
bodyLines.push('');
|
|
94
|
+
bodyLines.push('| 场景 | 命令示例 |');
|
|
95
|
+
bodyLines.push('|------|----------|');
|
|
96
|
+
bodyLines.push('| 需求对齐/方案拍板 | `sandtable event-log 1 "确定使用JWT登录方案" alignment high @user "登录" "docs/specs/login.md" --continue <threadId>` |');
|
|
97
|
+
bodyLines.push('| 规格文档变更 | `sandtable event-log 2 "API响应格式新增分页字段" spec-change medium @AI "API" "docs/specs/api.md"` |');
|
|
98
|
+
bodyLines.push('| 代码完成/待审查 | `sandtable event-log 3 "完成用户注册接口" code-done medium @AI "注册"` |');
|
|
99
|
+
bodyLines.push('| 审查通过 | `sandtable event-log 3 "登录模块审查通过" review-pass medium @user "登录"` |');
|
|
100
|
+
bodyLines.push('| 审查驳回/返工 | `sandtable event-log 3 "支付模块需重写异常处理" review-reject high @user "支付"` |');
|
|
101
|
+
bodyLines.push('| Git 重要操作 | `sandtable event-log 3 "合并feature/login到main" git-merge medium @AI "git"` |');
|
|
102
|
+
bodyLines.push('| 测试通过 | `sandtable event-log 4 "用户模块单测全部通过" test-pass medium` |');
|
|
103
|
+
bodyLines.push('| 测试失败 | `sandtable event-log 4 "支付集成测试3个失败" test-fail high` |');
|
|
104
|
+
bodyLines.push('| 审批/交接完成 | `sandtable event-log 5 "发版审批通过" approved high @user` |');
|
|
105
|
+
bodyLines.push('| 基础设施变更 | `sandtable event-log 6 "数据库迁移v2.1完成" infra medium` |');
|
|
106
|
+
bodyLines.push('| 人类提出新需求 | `sandtable event-log 1 "用户要求增加导出PDF功能" new-requirement high @user "导出"` |');
|
|
107
|
+
bodyLines.push('| 教训/坑位沉淀 | `sandtable event-log 7 "批量操作前必须备份,防止数据丢失" lesson high @AI "备份"` |');
|
|
108
|
+
bodyLines.push('');
|
|
109
|
+
bodyLines.push('## 参数说明');
|
|
110
|
+
bodyLines.push('');
|
|
111
|
+
bodyLines.push('- **typeId**: 1=对齐与拍板 2=规格演进 3=代码变更 4=测试与质量 5=审批与交接 6=运维与基建 7=教训沉淀');
|
|
112
|
+
bodyLines.push('- **impact**: high / medium / low');
|
|
113
|
+
bodyLines.push('- **actor**: @AI 或 @user');
|
|
114
|
+
bodyLines.push('- **tags**: 逗号分隔');
|
|
115
|
+
bodyLines.push('- **refDoc**: 关联文档路径(可选)');
|
|
116
|
+
bodyLines.push('- **threadId**: `--continue <id>` 关联同一线程的多个事件');
|
|
117
|
+
bodyLines.push('');
|
|
118
|
+
bodyLines.push('## 线程示例');
|
|
119
|
+
bodyLines.push('');
|
|
120
|
+
bodyLines.push('```bash');
|
|
121
|
+
bodyLines.push('# 开始一个线程');
|
|
122
|
+
bodyLines.push('sandtable event-log 3 "开始实现登录功能" code-start medium @AI "登录" "" login-2026-05-24');
|
|
123
|
+
bodyLines.push('# 追加到同一线程');
|
|
124
|
+
bodyLines.push('sandtable event-log 3 "登录实现完成" code-done medium @AI "登录" "" --continue login-2026-05-24');
|
|
125
|
+
bodyLines.push('```');
|
|
126
|
+
bodyLines.push('');
|
|
127
|
+
bodyLines.push('## 注意');
|
|
128
|
+
bodyLines.push('');
|
|
129
|
+
bodyLines.push('- 每次重要操作后主动记录,不要等用户提醒');
|
|
130
|
+
bodyLines.push('- 标题要有意义,便于后续在仪表盘中检索');
|
|
131
|
+
bodyLines.push('- 关联事件使用 --continue 保持线程连贯');
|
|
132
|
+
} else {
|
|
133
|
+
bodyLines.push('# Sandtable Event Logging');
|
|
134
|
+
bodyLines.push('');
|
|
135
|
+
bodyLines.push('Record significant development events using `sandtable event-log` for the project dashboard.');
|
|
136
|
+
bodyLines.push('');
|
|
137
|
+
bodyLines.push('## Trigger Scenarios');
|
|
138
|
+
bodyLines.push('');
|
|
139
|
+
bodyLines.push('| Scenario | Example Command |');
|
|
140
|
+
bodyLines.push('|----------|----------------|');
|
|
141
|
+
bodyLines.push('| Alignment/decision | `sandtable event-log 1 "Decided on JWT auth" alignment high @user "auth" "docs/specs/auth.md" --continue <threadId>` |');
|
|
142
|
+
bodyLines.push('| Spec evolution | `sandtable event-log 2 "Added pagination to API response" spec-change medium @AI "API" "docs/specs/api.md"` |');
|
|
143
|
+
bodyLines.push('| Code complete/review | `sandtable event-log 3 "User registration endpoint done" code-done medium @AI "registration"` |');
|
|
144
|
+
bodyLines.push('| Review passed | `sandtable event-log 3 "Login module review passed" review-pass medium @user "login"` |');
|
|
145
|
+
bodyLines.push('| Review rejected/rework | `sandtable event-log 3 "Payment module needs error handling rewrite" review-reject high @user "payment"` |');
|
|
146
|
+
bodyLines.push('| Git operations | `sandtable event-log 3 "Merged feature/login to main" git-merge medium @AI "git"` |');
|
|
147
|
+
bodyLines.push('| Tests passed | `sandtable event-log 4 "All user module unit tests passing" test-pass medium` |');
|
|
148
|
+
bodyLines.push('| Tests failed | `sandtable event-log 4 "3 payment integration tests failing" test-fail high` |');
|
|
149
|
+
bodyLines.push('| Approval/handover | `sandtable event-log 5 "Release approval granted" approved high @user` |');
|
|
150
|
+
bodyLines.push('| Infra/ops | `sandtable event-log 6 "Database migration v2.1 applied" infra medium` |');
|
|
151
|
+
bodyLines.push('| New requirements | `sandtable event-log 1 "User requests PDF export feature" new-requirement high @user "export"` |');
|
|
152
|
+
bodyLines.push('| Lessons learned | `sandtable event-log 7 "Always backup before batch ops" lesson high @AI "backup"` |');
|
|
153
|
+
bodyLines.push('');
|
|
154
|
+
bodyLines.push('## Parameters');
|
|
155
|
+
bodyLines.push('');
|
|
156
|
+
bodyLines.push('- **typeId**: 1=Alignment 2=Spec 3=Code 4=Test 5=Approval 6=Infra 7=Lesson');
|
|
157
|
+
bodyLines.push('- **impact**: high / medium / low');
|
|
158
|
+
bodyLines.push('- **actor**: @AI or @user');
|
|
159
|
+
bodyLines.push('- **tags**: comma-separated');
|
|
160
|
+
bodyLines.push('- **refDoc**: optional linked document path');
|
|
161
|
+
bodyLines.push('- **threadId**: `--continue <id>` to chain events in a thread');
|
|
162
|
+
bodyLines.push('');
|
|
163
|
+
bodyLines.push('## Thread Example');
|
|
164
|
+
bodyLines.push('');
|
|
165
|
+
bodyLines.push('```bash');
|
|
166
|
+
bodyLines.push('# Start a thread');
|
|
167
|
+
bodyLines.push('sandtable event-log 3 "Start implementing login" code-start medium @AI "login" "" login-2026-05-24');
|
|
168
|
+
bodyLines.push('# Continue the thread');
|
|
169
|
+
bodyLines.push('sandtable event-log 3 "Login implementation complete" code-done medium @AI "login" "" --continue login-2026-05-24');
|
|
170
|
+
bodyLines.push('```');
|
|
171
|
+
bodyLines.push('');
|
|
172
|
+
bodyLines.push('## Notes');
|
|
173
|
+
bodyLines.push('');
|
|
174
|
+
bodyLines.push('- Record events proactively after significant actions');
|
|
175
|
+
bodyLines.push('- Use meaningful titles for dashboard searchability');
|
|
176
|
+
bodyLines.push('- Use --continue to keep related events in one thread');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var body = bodyLines.join('\n');
|
|
180
|
+
|
|
181
|
+
switch (format) {
|
|
182
|
+
case 'mdc':
|
|
183
|
+
// Cursor .mdc with YAML frontmatter
|
|
184
|
+
return '---\nalwaysApply: true\n---\n\n' + body;
|
|
185
|
+
case 'skill-md':
|
|
186
|
+
// Claude Code SKILL.md with YAML frontmatter
|
|
187
|
+
return '---\nname: sandtable-event-log\ndescription: ' + (isZh ? '记录重要开发事件到 sandtable 项目仪表盘' : 'Record significant development events to the sandtable project dashboard') + '\n---\n\n' + body;
|
|
188
|
+
case 'instructions-md':
|
|
189
|
+
// GitHub Copilot instructions
|
|
190
|
+
return '# Sandtable Event Logging\n\n' + bodyLines.slice(1).join('\n');
|
|
191
|
+
default:
|
|
192
|
+
// Plain markdown for docs/skills, .trae/rules, .clinerules, .sandtable/rules.md
|
|
193
|
+
return body;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- injectToMainDoc: 向 CLAUDE.md / AGENTS.md 追加事件日志规则 ----
|
|
198
|
+
function injectToMainDoc(filePath, lang) {
|
|
199
|
+
var isZh = lang === 'zh';
|
|
200
|
+
var marker = 'sandtable event-log';
|
|
201
|
+
var existing = '';
|
|
202
|
+
if (fs.existsSync(filePath)) {
|
|
203
|
+
existing = fs.readFileSync(filePath, 'utf-8');
|
|
204
|
+
if (existing.indexOf(marker) !== -1) return false; // already injected
|
|
205
|
+
}
|
|
206
|
+
var lines = [];
|
|
207
|
+
if (isZh) {
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push('## Sandtable 事件日志');
|
|
210
|
+
lines.push('');
|
|
211
|
+
lines.push('**每次完成可独立表述的代码/设计/决策动作后,立即调用 `sandtable event-log` 记录。不要等用户提醒。**');
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push('详细规则见 `.sandtable/rules.md`。');
|
|
214
|
+
} else {
|
|
215
|
+
lines.push('');
|
|
216
|
+
lines.push('## Sandtable Event Logging');
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('**After every significant code/design/decision action, immediately call `sandtable event-log` to record it. Do not wait for a reminder.**');
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push('See `.sandtable/rules.md` for details.');
|
|
221
|
+
}
|
|
222
|
+
var content = existing + lines.join('\n') + '\n';
|
|
223
|
+
writeWithBackup(filePath, content);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- writeClaudeCodeHook: 创建 SessionStart 强提醒 hook ----
|
|
228
|
+
function writeClaudeCodeHook(projectRoot, lang) {
|
|
229
|
+
var hooksDir = path.join(projectRoot, '.claude', 'hooks');
|
|
230
|
+
var hookPath = path.join(hooksDir, 'sandtable-reminder.sh');
|
|
231
|
+
var isZh = lang === 'zh';
|
|
232
|
+
|
|
233
|
+
var script = '#!/bin/bash\n';
|
|
234
|
+
if (isZh) {
|
|
235
|
+
script += '# Sandtable 事件日志强提醒 — 每次 session 开始时注入\n';
|
|
236
|
+
script += "cat <<'SANDBTABLE_EOR'\n";
|
|
237
|
+
script += '<system-reminder>\n';
|
|
238
|
+
script += '【Sandtable 事件日志铁律】\n';
|
|
239
|
+
script += '本次 session 中,每完成一个独立动作(代码修改/设计定稿/决策拍板/错误修复),\n';
|
|
240
|
+
script += '必须立即调用 sandtable event-log 记录。禁止事后补录,禁止等用户提醒。\n';
|
|
241
|
+
script += '命令速查: sandtable event-log <typeId> "标题" <subtype> <impact> @AI "标签"\n';
|
|
242
|
+
script += 'typeId: 1=对齐拍板 2=规格演进 3=代码变更 4=测试质量 5=审批交接 6=运维基建 7=教训沉淀\n';
|
|
243
|
+
script += '详细规则见 .sandtable/rules.md\n';
|
|
244
|
+
script += '</system-reminder>\n';
|
|
245
|
+
script += 'SANDBTABLE_EOR\n';
|
|
246
|
+
} else {
|
|
247
|
+
script += '# Sandtable event log reminder — injected at session start\n';
|
|
248
|
+
script += "cat <<'SANDBTABLE_EOR'\n";
|
|
249
|
+
script += '<system-reminder>\n';
|
|
250
|
+
script += '[Sandtable Event Log Rule]\n';
|
|
251
|
+
script += 'After every significant action (code change, design decision, bug fix) in this session,\n';
|
|
252
|
+
script += 'immediately call sandtable event-log to record it. No backfilling, no waiting for reminders.\n';
|
|
253
|
+
script += 'Quick ref: sandtable event-log <typeId> "title" <subtype> <impact> @AI "tags"\n';
|
|
254
|
+
script += 'See .sandtable/rules.md for details.\n';
|
|
255
|
+
script += '</system-reminder>\n';
|
|
256
|
+
script += 'SANDBTABLE_EOR\n';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!fs.existsSync(hooksDir)) {
|
|
260
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
261
|
+
}
|
|
262
|
+
writeWithBackup(hookPath, script);
|
|
263
|
+
|
|
264
|
+
// Update .claude/settings.json to register the hook
|
|
265
|
+
var settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
266
|
+
var settings = {};
|
|
267
|
+
if (fs.existsSync(settingsPath)) {
|
|
268
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); } catch(e) {}
|
|
269
|
+
}
|
|
270
|
+
if (!settings.hooks) settings.hooks = {};
|
|
271
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
272
|
+
|
|
273
|
+
// Check if already registered
|
|
274
|
+
var alreadyRegistered = settings.hooks.SessionStart.some(function(h) {
|
|
275
|
+
return h.command && h.command.indexOf('sandtable-reminder') !== -1;
|
|
276
|
+
});
|
|
277
|
+
if (!alreadyRegistered) {
|
|
278
|
+
settings.hooks.SessionStart.push({
|
|
279
|
+
command: 'bash ' + hookPath.replace(/\\/g, '/')
|
|
280
|
+
});
|
|
281
|
+
writeWithBackup(settingsPath, JSON.stringify(settings, null, 2));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- initCommand: dry-run 报告 or --apply 写入 ----
|
|
288
|
+
function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
289
|
+
var envs = detectEnvironment(projectRoot);
|
|
290
|
+
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
291
|
+
var configPath = path.join(sandtableDir, 'config.json');
|
|
292
|
+
var eventLogPath = path.join(sandtableDir, 'event-log.jsonl');
|
|
293
|
+
|
|
294
|
+
// ---- Print detection report ----
|
|
295
|
+
console.log('Sandtable Init — 项目环境扫描报告');
|
|
296
|
+
console.log('项目: ' + projectRoot);
|
|
297
|
+
console.log('');
|
|
298
|
+
|
|
299
|
+
if (envs.length === 0) {
|
|
300
|
+
console.log('未检测到任何已知 IDE 指令目录。');
|
|
301
|
+
console.log('将使用通用模式:.sandtable/rules.md');
|
|
302
|
+
} else {
|
|
303
|
+
console.log('检测到 ' + envs.length + ' 个指令目标:');
|
|
304
|
+
for (var i = 0; i < envs.length; i++) {
|
|
305
|
+
var e = envs[i];
|
|
306
|
+
console.log(' ' + (i + 1) + '. [' + e.type + '] → ' + e.targetFile);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log('');
|
|
311
|
+
console.log('将创建/更新:');
|
|
312
|
+
console.log(' - ' + configPath);
|
|
313
|
+
console.log(' - ' + eventLogPath + ' (空容器)');
|
|
314
|
+
for (var i = 0; i < envs.length; i++) {
|
|
315
|
+
console.log(' - ' + envs[i].targetFile);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Entry line hints — separate auto-inject from manual
|
|
319
|
+
var entryHints = [], autoHints = [];
|
|
320
|
+
for (var i = 0; i < envs.length; i++) {
|
|
321
|
+
if (envs[i].entryLine) {
|
|
322
|
+
if (envs[i].autoInject) autoHints.push(envs[i].entryLine);
|
|
323
|
+
else entryHints.push(envs[i].entryLine);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (autoHints.length > 0) {
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log('⚡ --apply 将自动注入:');
|
|
329
|
+
for (var i = 0; i < autoHints.length; i++) {
|
|
330
|
+
console.log(' ' + (i + 1) + '. ' + autoHints[i]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (entryHints.length > 0) {
|
|
334
|
+
console.log('');
|
|
335
|
+
console.log('⚠ 手动步骤:');
|
|
336
|
+
for (var i = 0; i < entryHints.length; i++) {
|
|
337
|
+
console.log(' ' + (i + 1) + '. ' + entryHints[i]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Hook availability for Claude Code
|
|
342
|
+
var hasClaudeEnv = envs.some(function(e) { return e.type === 'claude'; });
|
|
343
|
+
if (hasClaudeEnv) {
|
|
344
|
+
console.log('');
|
|
345
|
+
if (hooksMode) {
|
|
346
|
+
console.log('⚡ SessionStart 强提醒 hook → .claude/hooks/sandtable-reminder.sh');
|
|
347
|
+
} else {
|
|
348
|
+
console.log('💡 可选: --hooks 安装 SessionStart 强提醒 hook(每次 session 开头注入日志铁律)');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!applyMode) {
|
|
353
|
+
console.log('');
|
|
354
|
+
console.log('以上为 dry-run 报告。要实际写入文件,请运行:');
|
|
355
|
+
console.log(' sandtable init --apply' + (lang ? ' --lang ' + lang : '') + (hasClaudeEnv ? ' [--hooks]' : ''));
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log('已存在文件将在写入前备份为 .bak.<timestamp>');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---- --apply: write files ----
|
|
362
|
+
console.log('');
|
|
363
|
+
console.log('正在写入文件...');
|
|
364
|
+
|
|
365
|
+
// 1. Create .sandtable/ directory
|
|
366
|
+
if (!fs.existsSync(sandtableDir)) {
|
|
367
|
+
fs.mkdirSync(sandtableDir, { recursive: true });
|
|
368
|
+
console.log(' ✓ 创建 ' + sandtableDir);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 2. Write .sandtable/config.json
|
|
372
|
+
var config = {
|
|
373
|
+
version: '0.3.0',
|
|
374
|
+
createdAt: new Date().toISOString(),
|
|
375
|
+
projectRoot: projectRoot,
|
|
376
|
+
paths: {
|
|
377
|
+
convention: 'docs/convention',
|
|
378
|
+
spec: 'docs/specs',
|
|
379
|
+
archive: 'docs/archive'
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
writeWithBackup(configPath, JSON.stringify(config, null, 2));
|
|
383
|
+
|
|
384
|
+
// 3. Create empty event-log.jsonl
|
|
385
|
+
if (!fs.existsSync(eventLogPath)) {
|
|
386
|
+
fs.writeFileSync(eventLogPath, '', 'utf-8');
|
|
387
|
+
console.log(' ✓ 创建 ' + eventLogPath + ' (空)');
|
|
388
|
+
} else {
|
|
389
|
+
console.log(' - ' + eventLogPath + ' 已存在,保留');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 4. Copy .sandtable.json template
|
|
393
|
+
var templateSrc = path.join(__dirname, '..', '..', 'templates', '.sandtable.template.json');
|
|
394
|
+
var sandtableJsonDest = path.join(projectRoot, '.sandtable.json');
|
|
395
|
+
if (fs.existsSync(templateSrc)) {
|
|
396
|
+
if (!fs.existsSync(sandtableJsonDest)) {
|
|
397
|
+
writeWithBackup(sandtableJsonDest, fs.readFileSync(templateSrc, 'utf-8'));
|
|
398
|
+
} else {
|
|
399
|
+
console.log(' - .sandtable.json 已存在,保留');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 5. Write rule files
|
|
404
|
+
for (var i = 0; i < envs.length; i++) {
|
|
405
|
+
var env = envs[i];
|
|
406
|
+
var fullPath = path.join(projectRoot, env.targetFile);
|
|
407
|
+
var targetDir = path.dirname(fullPath);
|
|
408
|
+
|
|
409
|
+
if (!fs.existsSync(targetDir)) {
|
|
410
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
var content = generateRuleContent(env.format, lang);
|
|
414
|
+
writeWithBackup(fullPath, content);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 6. Inject event-log rule into main doc (CLAUDE.md / AGENTS.md)
|
|
418
|
+
var mainDocInjected = false;
|
|
419
|
+
for (var i = 0; i < envs.length; i++) {
|
|
420
|
+
var env = envs[i];
|
|
421
|
+
var mainDocPath = null;
|
|
422
|
+
if (env.type === 'agents') {
|
|
423
|
+
mainDocPath = path.join(projectRoot, 'AGENTS.md');
|
|
424
|
+
} else if (env.type === 'claude-md') {
|
|
425
|
+
mainDocPath = path.join(projectRoot, 'CLAUDE.md');
|
|
426
|
+
} else if (env.type === 'generic') {
|
|
427
|
+
mainDocPath = path.join(projectRoot, 'AGENTS.md');
|
|
428
|
+
}
|
|
429
|
+
if (mainDocPath) {
|
|
430
|
+
var injected = injectToMainDoc(mainDocPath, lang);
|
|
431
|
+
if (injected) {
|
|
432
|
+
console.log(' ✓ 注入事件日志规则 → ' + path.basename(mainDocPath));
|
|
433
|
+
mainDocInjected = true;
|
|
434
|
+
} else if (fs.existsSync(mainDocPath)) {
|
|
435
|
+
console.log(' - ' + path.basename(mainDocPath) + ' 已有 sandtable 规则,跳过');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 7. Install Claude Code SessionStart hook (only with --hooks)
|
|
441
|
+
if (hooksMode && hasClaudeEnv) {
|
|
442
|
+
writeClaudeCodeHook(projectRoot, lang);
|
|
443
|
+
console.log(' ✓ 安装 SessionStart 强提醒 hook → .claude/hooks/sandtable-reminder.sh');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log('');
|
|
447
|
+
console.log('✓ 初始化完成。运行 sandtable build 生成数据,然后 sandtable serve 查看仪表盘。');
|
|
448
|
+
|
|
449
|
+
// Only show manual hints for non-main-doc entry types
|
|
450
|
+
var remainingHints = [];
|
|
451
|
+
for (var i = 0; i < entryHints.length; i++) {
|
|
452
|
+
var env = envs[i];
|
|
453
|
+
if (env && (env.type === 'agents' || env.type === 'claude-md' || env.type === 'generic')) continue;
|
|
454
|
+
remainingHints.push(entryHints[i]);
|
|
455
|
+
}
|
|
456
|
+
if (remainingHints.length > 0) {
|
|
457
|
+
console.log('');
|
|
458
|
+
console.log('⚠ 别忘了手动步骤:');
|
|
459
|
+
for (var i = 0; i < remainingHints.length; i++) {
|
|
460
|
+
console.log(' ' + (i + 1) + '. ' + remainingHints[i]);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---- writeWithBackup: 写前备份 ----
|
|
466
|
+
function writeWithBackup(filePath, content) {
|
|
467
|
+
if (fs.existsSync(filePath)) {
|
|
468
|
+
var bakPath = filePath + '.bak.' + Date.now();
|
|
469
|
+
fs.copyFileSync(filePath, bakPath);
|
|
470
|
+
console.log(' ⓘ 备份 ' + path.basename(filePath) + ' → ' + path.basename(bakPath));
|
|
471
|
+
}
|
|
472
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
473
|
+
console.log(' ✓ 写入 ' + filePath);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function enableWatchMode(projectRoot) {
|
|
477
|
+
var docsDir = path.join(projectRoot, 'docs');
|
|
478
|
+
var configFile = path.join(projectRoot, '.sandtable.json');
|
|
479
|
+
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
480
|
+
|
|
481
|
+
console.log('Watch: 监控 docs/ 、.sandtable/ 和 .sandtable.json 变更...');
|
|
482
|
+
|
|
483
|
+
function scheduleRebuild() {
|
|
484
|
+
if (watchTimer) clearTimeout(watchTimer);
|
|
485
|
+
watchTimer = setTimeout(function() {
|
|
486
|
+
console.log('\n[watch] 检测到变更,自动 rebuild...');
|
|
487
|
+
var result = build(projectRoot);
|
|
488
|
+
console.log('[watch] rebuild 完成 — ' + (result.outputs ? result.outputs.length + ' files' : 'ok'));
|
|
489
|
+
}, 300); // debounce 300ms
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
var watchTimer = null;
|
|
493
|
+
|
|
494
|
+
// Watch docs directory
|
|
495
|
+
if (fs.existsSync(docsDir)) {
|
|
496
|
+
try {
|
|
497
|
+
fs.watch(docsDir, { recursive: true }, function(evt, filename) {
|
|
498
|
+
if (filename) scheduleRebuild();
|
|
499
|
+
});
|
|
500
|
+
} catch (e) {
|
|
501
|
+
fs.watch(docsDir, function(evt, filename) {
|
|
502
|
+
if (filename) scheduleRebuild();
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Watch .sandtable/ directory (event-log.jsonl, token-log.jsonl, etc.)
|
|
508
|
+
if (fs.existsSync(sandtableDir)) {
|
|
509
|
+
try {
|
|
510
|
+
fs.watch(sandtableDir, { recursive: true }, function(evt, filename) {
|
|
511
|
+
if (filename) scheduleRebuild();
|
|
512
|
+
});
|
|
513
|
+
} catch (e) {
|
|
514
|
+
fs.watch(sandtableDir, function(evt, filename) {
|
|
515
|
+
if (filename) scheduleRebuild();
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Watch .sandtable.json
|
|
521
|
+
if (fs.existsSync(configFile)) {
|
|
522
|
+
fs.watch(configFile, function(evt, filename) {
|
|
523
|
+
scheduleRebuild();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
529
|
+
const MIME = {
|
|
530
|
+
'.html': 'text/html; charset=utf-8',
|
|
531
|
+
'.css': 'text/css; charset=utf-8',
|
|
532
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
533
|
+
'.json': 'application/json; charset=utf-8',
|
|
534
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
535
|
+
'.png': 'image/png',
|
|
536
|
+
'.jsonl': 'text/plain; charset=utf-8',
|
|
537
|
+
'.svg': 'image/svg+xml',
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const sandtableRoot = path.resolve(__dirname, '..', '..');
|
|
541
|
+
const allowedPrefixes = [
|
|
542
|
+
path.join(sandtableRoot, 'dashboard'),
|
|
543
|
+
path.join(sandtableRoot, 'data'),
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
function isAllowed(filePath) {
|
|
547
|
+
// Always allow files under sandtable's dashboard/data
|
|
548
|
+
for (var i = 0; i < allowedPrefixes.length; i++) {
|
|
549
|
+
if (filePath.startsWith(allowedPrefixes[i])) return true;
|
|
550
|
+
}
|
|
551
|
+
// Allow any file under the project root (but not .env, .git, node_modules, credentials)
|
|
552
|
+
if (filePath.startsWith(projectRoot)) {
|
|
553
|
+
var rel = path.relative(projectRoot, filePath);
|
|
554
|
+
if (rel.startsWith('.git') || rel.startsWith('node_modules') ||
|
|
555
|
+
rel.startsWith('.env') || rel.includes('credentials') ||
|
|
556
|
+
rel.includes('.pem') || rel.includes('.pfx')) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
http.createServer(function(req, res) {
|
|
565
|
+
var url = req.url === '/' ? '/dashboard/dashboard.html' : req.url;
|
|
566
|
+
url = url.replace(/^\/(css|js)\//, '/dashboard/$1/');
|
|
567
|
+
|
|
568
|
+
var filePath = path.join(sandtableRoot, url);
|
|
569
|
+
|
|
570
|
+
// If not found under sandtable root, try project root
|
|
571
|
+
if (!(fs.existsSync(filePath) && fs.statSync(filePath).isFile())) {
|
|
572
|
+
var clean = url.split('?')[0].split('#')[0];
|
|
573
|
+
var projPath = path.join(projectRoot, clean);
|
|
574
|
+
if (fs.existsSync(projPath) && fs.statSync(projPath).isFile()) {
|
|
575
|
+
filePath = projPath;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!isAllowed(filePath)) {
|
|
580
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
581
|
+
return res.end('403 Forbidden');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
var ext = path.extname(filePath);
|
|
585
|
+
try {
|
|
586
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
587
|
+
res.writeHead(200, {
|
|
588
|
+
'Content-Type': MIME[ext] || 'text/plain',
|
|
589
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
590
|
+
'Pragma': 'no-cache',
|
|
591
|
+
'Expires': '0',
|
|
592
|
+
});
|
|
593
|
+
res.end(fs.readFileSync(filePath));
|
|
594
|
+
} else {
|
|
595
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
596
|
+
res.end('404 Not Found');
|
|
597
|
+
}
|
|
598
|
+
} catch (e) {
|
|
599
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
600
|
+
res.end('500: ' + e.message);
|
|
601
|
+
}
|
|
602
|
+
}).listen(port, function() {
|
|
603
|
+
console.log('Sandtable v' + VERSION + ' — http://localhost:' + port);
|
|
604
|
+
console.log('Project: ' + projectRoot);
|
|
605
|
+
|
|
606
|
+
if (watchMode) enableWatchMode(projectRoot);
|
|
607
|
+
|
|
608
|
+
if (openBrowser) {
|
|
609
|
+
var platform = process.platform;
|
|
610
|
+
var cmd;
|
|
611
|
+
if (platform === 'darwin') {
|
|
612
|
+
cmd = 'open http://localhost:' + port;
|
|
613
|
+
} else if (platform === 'win32') {
|
|
614
|
+
cmd = 'start http://localhost:' + port;
|
|
615
|
+
} else {
|
|
616
|
+
cmd = 'xdg-open http://localhost:' + port;
|
|
617
|
+
}
|
|
618
|
+
exec(cmd, function(err) {
|
|
619
|
+
if (err) console.log('请手动打开: http://localhost:' + port);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
switch (command) {
|
|
626
|
+
case 'scan':
|
|
627
|
+
console.log(JSON.stringify(scan(root), null, 2));
|
|
628
|
+
break;
|
|
629
|
+
case 'build':
|
|
630
|
+
console.log(JSON.stringify(build(root), null, 2));
|
|
631
|
+
break;
|
|
632
|
+
case 'init': {
|
|
633
|
+
var applyMode = process.argv.indexOf('--apply') !== -1;
|
|
634
|
+
var hooksMode = process.argv.indexOf('--hooks') !== -1;
|
|
635
|
+
var langIdx = process.argv.indexOf('--lang');
|
|
636
|
+
var lang = 'zh';
|
|
637
|
+
if (langIdx !== -1 && process.argv[langIdx + 1]) {
|
|
638
|
+
lang = process.argv[langIdx + 1] === 'en' ? 'en' : 'zh';
|
|
639
|
+
}
|
|
640
|
+
initCommand(process.cwd(), applyMode, lang, hooksMode);
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case 'summarize': {
|
|
644
|
+
const scanResult = scan(root);
|
|
645
|
+
const missing = scanResult.files.filter(f => !f.hasSummary);
|
|
646
|
+
console.log(JSON.stringify({
|
|
647
|
+
command: 'summarize',
|
|
648
|
+
projectRoot: root,
|
|
649
|
+
totalFiles: scanResult.totalFiles,
|
|
650
|
+
missingCount: missing.length,
|
|
651
|
+
files: missing.map(f => ({
|
|
652
|
+
path: f.path,
|
|
653
|
+
category: f.category,
|
|
654
|
+
elementType: f.elementType,
|
|
655
|
+
title: f.title
|
|
656
|
+
}))
|
|
657
|
+
}, null, 2));
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case 'version':
|
|
661
|
+
console.log('sandtable v' + VERSION);
|
|
662
|
+
break;
|
|
663
|
+
case 'serve': {
|
|
664
|
+
var port = parseInt(process.argv[3], 10) || 3000;
|
|
665
|
+
var watchMode = process.argv.indexOf('--watch') !== -1;
|
|
666
|
+
startServer(process.cwd(), port, true, watchMode);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case 'event-log': {
|
|
670
|
+
// Record event: sandtable event-log <typeId> <title> [subtype] [impact] [actor] [tags] [refDoc] [threadId] [--tokens <in>,<out>] [--continue <threadId>]
|
|
671
|
+
|
|
672
|
+
// Extract named flags first, then build clean positional args
|
|
673
|
+
var rawArgs = process.argv.slice(3); // everything after "event-log"
|
|
674
|
+
var contThreadId = null;
|
|
675
|
+
var tokensVal = null;
|
|
676
|
+
var cleanArgs = [];
|
|
677
|
+
for (var ai = 0; ai < rawArgs.length; ai++) {
|
|
678
|
+
if (rawArgs[ai] === '--continue' && ai + 1 < rawArgs.length) {
|
|
679
|
+
contThreadId = rawArgs[ai + 1]; ai++; // skip value
|
|
680
|
+
} else if (rawArgs[ai] === '--tokens' && ai + 1 < rawArgs.length) {
|
|
681
|
+
tokensVal = rawArgs[ai + 1]; ai++; // skip value
|
|
682
|
+
} else {
|
|
683
|
+
cleanArgs.push(rawArgs[ai]);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
var typeId = cleanArgs[0] || '3';
|
|
688
|
+
var title = cleanArgs[1] || '';
|
|
689
|
+
var subtype = cleanArgs[2] || 'manual';
|
|
690
|
+
var impact = cleanArgs[3] || 'medium';
|
|
691
|
+
var actor = cleanArgs[4] || 'AI';
|
|
692
|
+
var tagsRaw = cleanArgs[5] || '';
|
|
693
|
+
var refDoc = cleanArgs[6] || '';
|
|
694
|
+
var threadId = contThreadId || cleanArgs[7] || '';
|
|
695
|
+
|
|
696
|
+
if (!title) {
|
|
697
|
+
console.log('用法: sandtable event-log <typeId> <title> [subtype] [impact] [actor] [tags] [refDoc] [threadId] [--tokens <in>,<out>] [--continue <threadId>]');
|
|
698
|
+
console.log(' typeId: 1=对齐与拍板 2=规格演进 3=代码变更 4=测试与质量 5=审批与交接 6=运维与基建 7=教训沉淀');
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (!EVENT_TYPES[typeId]) {
|
|
703
|
+
console.log('错误:typeId 必须为 1-7');
|
|
704
|
+
console.log(' 1=对齐与拍板 2=规格演进 3=代码变更 4=测试与质量 5=审批与交接 6=运维与基建 7=教训沉淀');
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Resolve project root
|
|
709
|
+
var projectRoot = cleanArgs[8] || process.cwd();
|
|
710
|
+
|
|
711
|
+
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
712
|
+
if (!fs.existsSync(sandtableDir)) fs.mkdirSync(sandtableDir, { recursive: true });
|
|
713
|
+
|
|
714
|
+
var tags = tagsRaw ? tagsRaw.split(',').map(function(t) { return t.trim(); }) : [];
|
|
715
|
+
var ref = refDoc ? { doc: refDoc } : {};
|
|
716
|
+
|
|
717
|
+
var eventId = 'evt-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
|
|
718
|
+
|
|
719
|
+
var event = {
|
|
720
|
+
id: eventId,
|
|
721
|
+
timestamp: new Date().toISOString(),
|
|
722
|
+
type: EVENT_TYPES[typeId] || '代码变更',
|
|
723
|
+
typeId: typeId,
|
|
724
|
+
subtype: subtype,
|
|
725
|
+
title: title,
|
|
726
|
+
ref: ref,
|
|
727
|
+
impact: impact,
|
|
728
|
+
actor: actor,
|
|
729
|
+
tags: tags,
|
|
730
|
+
threadId: threadId || null,
|
|
731
|
+
};
|
|
732
|
+
event.priority = classifyEventPriority(event);
|
|
733
|
+
|
|
734
|
+
// Write event-log.jsonl
|
|
735
|
+
var logPath = path.join(sandtableDir, 'event-log.jsonl');
|
|
736
|
+
fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
|
|
737
|
+
|
|
738
|
+
// Write audit log
|
|
739
|
+
var auditPath = path.join(sandtableDir, 'audit.log');
|
|
740
|
+
var auditEntry = JSON.stringify({
|
|
741
|
+
timestamp: event.timestamp,
|
|
742
|
+
command: 'event-log',
|
|
743
|
+
eventId: eventId,
|
|
744
|
+
type: event.type,
|
|
745
|
+
title: title,
|
|
746
|
+
cwd: projectRoot,
|
|
747
|
+
});
|
|
748
|
+
fs.appendFileSync(auditPath, auditEntry + '\n');
|
|
749
|
+
|
|
750
|
+
console.log('event-log: [' + event.priority + '] ' + event.type + ' — ' + title + (threadId ? ' (thread: ' + threadId + ')' : ''));
|
|
751
|
+
|
|
752
|
+
// --tokens <in>,<out> : auto-log token usage for this event
|
|
753
|
+
if (tokensVal) {
|
|
754
|
+
var tokensParts = tokensVal.split(',');
|
|
755
|
+
var tIn = parseInt(tokensParts[0], 10) || 0;
|
|
756
|
+
var tOut = parseInt(tokensParts[1], 10) || 0;
|
|
757
|
+
if (tIn > 0 || tOut > 0) {
|
|
758
|
+
var COST_PER_1K = { input: 0.003, output: 0.006 }; // DeepSeek V4 Pro ¥/1K
|
|
759
|
+
var tCost = (tIn / 1000) * COST_PER_1K.input + (tOut / 1000) * COST_PER_1K.output;
|
|
760
|
+
tCost = Math.round(tCost * 10000) / 10000;
|
|
761
|
+
var tokenEntry = {
|
|
762
|
+
timestamp: event.timestamp,
|
|
763
|
+
skill: 'event-log',
|
|
764
|
+
tokensIn: tIn,
|
|
765
|
+
tokensOut: tOut,
|
|
766
|
+
totalTokens: tIn + tOut,
|
|
767
|
+
cost: tCost,
|
|
768
|
+
source: 'event-log',
|
|
769
|
+
eventId: eventId,
|
|
770
|
+
note: null,
|
|
771
|
+
};
|
|
772
|
+
var tokenLogPath = path.join(sandtableDir, 'token-log.jsonl');
|
|
773
|
+
fs.appendFileSync(tokenLogPath, JSON.stringify(tokenEntry) + '\n');
|
|
774
|
+
console.log(' token-log: ' + (tIn + tOut).toLocaleString() + ' tokens (est. ¥' + tCost + ')');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
case 'token-log': {
|
|
780
|
+
var skill = process.argv[3] || '';
|
|
781
|
+
var tokensIn = parseInt(process.argv[4], 10) || 0;
|
|
782
|
+
var tokensOut = parseInt(process.argv[5], 10) || 0;
|
|
783
|
+
var note = process.argv[6] || '';
|
|
784
|
+
var projectRoot = process.argv[7] || process.cwd();
|
|
785
|
+
|
|
786
|
+
if (!skill) {
|
|
787
|
+
console.log('用法: sandtable token-log <skill> <tokensIn> <tokensOut> [note] [projectRoot]');
|
|
788
|
+
console.log(' skill: 操作名称(如 build, event-log, scan)');
|
|
789
|
+
console.log(' tokensIn: 输入 token 数');
|
|
790
|
+
console.log(' tokensOut: 输出 token 数');
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
795
|
+
if (!fs.existsSync(sandtableDir)) fs.mkdirSync(sandtableDir, { recursive: true });
|
|
796
|
+
|
|
797
|
+
var COST_PER_1K = { input: 0.003, output: 0.006 }; // DeepSeek V4 Pro ¥/1K
|
|
798
|
+
var cost = (tokensIn / 1000) * COST_PER_1K.input + (tokensOut / 1000) * COST_PER_1K.output;
|
|
799
|
+
cost = Math.round(cost * 10000) / 10000;
|
|
800
|
+
|
|
801
|
+
var entry = {
|
|
802
|
+
timestamp: new Date().toISOString(),
|
|
803
|
+
skill: skill,
|
|
804
|
+
tokensIn: tokensIn,
|
|
805
|
+
tokensOut: tokensOut,
|
|
806
|
+
totalTokens: tokensIn + tokensOut,
|
|
807
|
+
cost: cost,
|
|
808
|
+
note: note || null,
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
var logPath = path.join(sandtableDir, 'token-log.jsonl');
|
|
812
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
813
|
+
|
|
814
|
+
// Audit
|
|
815
|
+
var auditPath = path.join(sandtableDir, 'audit.log');
|
|
816
|
+
var auditEntry = JSON.stringify({
|
|
817
|
+
timestamp: entry.timestamp,
|
|
818
|
+
command: 'token-log',
|
|
819
|
+
skill: skill,
|
|
820
|
+
tokensIn: tokensIn,
|
|
821
|
+
tokensOut: tokensOut,
|
|
822
|
+
cwd: projectRoot,
|
|
823
|
+
});
|
|
824
|
+
fs.appendFileSync(auditPath, auditEntry + '\n');
|
|
825
|
+
|
|
826
|
+
console.log('token-log: ' + skill + ' — ' + (tokensIn + tokensOut).toLocaleString() + ' tokens (est. $' + cost + ')');
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
case 'uninstall': {
|
|
830
|
+
var applyMode = process.argv.indexOf('--apply') !== -1;
|
|
831
|
+
var dryRun = !applyMode;
|
|
832
|
+
var projectRoot = process.argv[3] || process.cwd();
|
|
833
|
+
|
|
834
|
+
// Known sandtable files (relative to project root)
|
|
835
|
+
var knownFiles = [
|
|
836
|
+
'.sandtable/config.json',
|
|
837
|
+
'.sandtable/event-log.jsonl',
|
|
838
|
+
'.sandtable/event-log.errors.jsonl',
|
|
839
|
+
'.sandtable/audit.log',
|
|
840
|
+
'.sandtable/token-log.jsonl',
|
|
841
|
+
'.sandtable/rules.md',
|
|
842
|
+
'.sandtable.json',
|
|
843
|
+
'.cursor/rules/sandtable-event-log.mdc',
|
|
844
|
+
'docs/skills/sandtable-event-log.md',
|
|
845
|
+
'.claude/skills/sandtable-event-log/SKILL.md',
|
|
846
|
+
'.trae/rules/sandtable-event-log.md',
|
|
847
|
+
'.clinerules/sandtable-event-log.md',
|
|
848
|
+
'.github/instructions/sandtable-event-log.instructions.md',
|
|
849
|
+
];
|
|
850
|
+
|
|
851
|
+
var existing = [];
|
|
852
|
+
for (var i = 0; i < knownFiles.length; i++) {
|
|
853
|
+
var fp = path.join(projectRoot, knownFiles[i]);
|
|
854
|
+
if (fs.existsSync(fp)) {
|
|
855
|
+
var stat = fs.statSync(fp);
|
|
856
|
+
existing.push({ rel: knownFiles[i], abs: fp, isDir: stat.isDirectory() });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Also check .sandtable/ for any other files
|
|
861
|
+
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
862
|
+
if (fs.existsSync(sandtableDir)) {
|
|
863
|
+
var entries = fs.readdirSync(sandtableDir);
|
|
864
|
+
for (var i = 0; i < entries.length; i++) {
|
|
865
|
+
var full = path.join(sandtableDir, entries[i]);
|
|
866
|
+
var rel = '.sandtable/' + entries[i];
|
|
867
|
+
// Skip if already in the list
|
|
868
|
+
var alreadyKnown = false;
|
|
869
|
+
for (var j = 0; j < existing.length; j++) {
|
|
870
|
+
if (existing[j].rel === rel) { alreadyKnown = true; break; }
|
|
871
|
+
}
|
|
872
|
+
if (!alreadyKnown) {
|
|
873
|
+
var stat = fs.statSync(full);
|
|
874
|
+
existing.push({ rel: rel, abs: full, isDir: stat.isDirectory() });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (existing.length === 0) {
|
|
880
|
+
console.log('未找到 sandtable 创建的文件。');
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
console.log('Sandtable Uninstall — ' + (dryRun ? 'dry-run(预览)' : '执行清理'));
|
|
885
|
+
console.log('项目: ' + projectRoot);
|
|
886
|
+
console.log('');
|
|
887
|
+
console.log('找到 ' + existing.length + ' 个 sandtable 文件/目录:');
|
|
888
|
+
for (var i = 0; i < existing.length; i++) {
|
|
889
|
+
console.log(' ' + (existing[i].isDir ? '[DIR] ' : '[ ] ') + existing[i].rel);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (dryRun) {
|
|
893
|
+
console.log('');
|
|
894
|
+
console.log('以上为 dry-run 预览。要实际删除,请运行:');
|
|
895
|
+
console.log(' sandtable uninstall --apply');
|
|
896
|
+
console.log('');
|
|
897
|
+
console.log('注意:');
|
|
898
|
+
console.log(' - sandtable 不会删除用户主文档(AGENTS.md、CLAUDE.md 等)');
|
|
899
|
+
console.log(' - 如果你在 AGENTS.md/CLAUDE.md 中手动添加了 @.sandtable/rules.md 入口行,请自行删除');
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// --apply: delete files
|
|
904
|
+
console.log('');
|
|
905
|
+
console.log('正在删除...');
|
|
906
|
+
var deleted = 0;
|
|
907
|
+
var errors = 0;
|
|
908
|
+
|
|
909
|
+
// Delete files first, then directories (reverse order so files go before their parent dirs)
|
|
910
|
+
var filesFirst = existing.filter(function(e) { return !e.isDir; });
|
|
911
|
+
var dirsFirst = existing.filter(function(e) { return e.isDir; });
|
|
912
|
+
|
|
913
|
+
for (var i = 0; i < filesFirst.length; i++) {
|
|
914
|
+
try {
|
|
915
|
+
fs.unlinkSync(filesFirst[i].abs);
|
|
916
|
+
console.log(' ✓ 删除 ' + filesFirst[i].rel);
|
|
917
|
+
deleted++;
|
|
918
|
+
} catch (e) {
|
|
919
|
+
console.log(' ✗ 删除失败 ' + filesFirst[i].rel + ': ' + e.message);
|
|
920
|
+
errors++;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Try to remove directories (may fail if not empty — that's OK)
|
|
925
|
+
for (var i = 0; i < dirsFirst.length; i++) {
|
|
926
|
+
try {
|
|
927
|
+
fs.rmdirSync(dirsFirst[i].abs);
|
|
928
|
+
console.log(' ✓ 删除 ' + dirsFirst[i].rel);
|
|
929
|
+
deleted++;
|
|
930
|
+
} catch (e) {
|
|
931
|
+
// Directory not empty or other error — skip silently
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Try to remove .sandtable/ if empty
|
|
936
|
+
try {
|
|
937
|
+
fs.rmdirSync(sandtableDir);
|
|
938
|
+
} catch (e) {
|
|
939
|
+
// Not empty or doesn't exist — fine
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
console.log('');
|
|
943
|
+
console.log('✓ 完成: 删除 ' + deleted + ' 个, ' + (errors > 0 ? errors + ' 个失败' : '全部成功'));
|
|
944
|
+
console.log('');
|
|
945
|
+
console.log('⚠ 如果你在 AGENTS.md/CLAUDE.md 中手动添加了入口行,请自行删除。');
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
case 'help':
|
|
949
|
+
default:
|
|
950
|
+
console.log('sandtable v' + VERSION + ' — AI 编程项目可视化指挥面板\n');
|
|
951
|
+
console.log('用法:');
|
|
952
|
+
console.log(' sandtable init [--apply] [--lang zh|en] 扫描环境 + 准备规则文件');
|
|
953
|
+
console.log(' sandtable scan [projectRoot] 扫描文档目录');
|
|
954
|
+
console.log(' sandtable build [projectRoot] 生成 data/*.json');
|
|
955
|
+
console.log(' sandtable serve [port] [--watch] 启动服务 + 打开浏览器');
|
|
956
|
+
console.log(' sandtable summarize [projectRoot] 列出缺少摘要块的文件');
|
|
957
|
+
console.log(' sandtable event-log <typeId> <title> [...] 记录人机协同事件');
|
|
958
|
+
console.log(' sandtable token-log <skill> <in> <out> [...] 追加 token 消耗日志');
|
|
959
|
+
console.log(' sandtable uninstall [--apply] 清理 sandtable 创建的文件');
|
|
960
|
+
console.log(' sandtable version 显示版本');
|
|
961
|
+
console.log(' sandtable help 显示帮助');
|
|
962
|
+
console.log('');
|
|
963
|
+
console.log('v0.3 新特性:');
|
|
964
|
+
console.log(' - serve 命令启动 HTTP 服务 + 自动打开浏览器');
|
|
965
|
+
console.log(' - 双视图架构:doc 树 + 事件流');
|
|
966
|
+
console.log(' - event-log 记录人机对话中的真实事件');
|
|
967
|
+
console.log(' - init --apply 写入专属命名空间,不污染主 doc');
|
|
968
|
+
console.log(' - npm 全局安装:npm install -g sandtable');
|
|
969
|
+
break;
|
|
970
|
+
}
|