tenbo-dashboard 0.2.0 → 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 +2 -2
- package/bin/tenbo-dashboard.mjs +2 -0
- package/package.json +1 -1
- package/scripts/sync.ts +144 -0
package/README.md
CHANGED
|
@@ -47,9 +47,9 @@ npx tenbo-dashboard --version # Print the installed version
|
|
|
47
47
|
|
|
48
48
|
## What is tenbo?
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
An AI cofounder that gives your coding assistant persistent project memory — architecture docs, roadmaps, and health signals that survive across sessions. Available as a Claude Code skill and as a Cursor rule package; both editors share the same `.tenbo/` data and the same companion dashboard. The dashboard is the optional visual companion; the skill / rule is the always-on conversational brain.
|
|
51
51
|
|
|
52
|
-
Install
|
|
52
|
+
Install: [github.com/poyi/tenbo](https://github.com/poyi/tenbo) (instructions for both Claude Code and Cursor)
|
|
53
53
|
|
|
54
54
|
## License
|
|
55
55
|
|
package/bin/tenbo-dashboard.mjs
CHANGED
|
@@ -38,6 +38,7 @@ const commands = {
|
|
|
38
38
|
'next-id': 'scripts/next-id.ts',
|
|
39
39
|
metrics: 'scripts/compute-metrics.ts',
|
|
40
40
|
'init-check': 'scripts/init-check.ts',
|
|
41
|
+
sync: 'scripts/sync.ts',
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
function run(script, scriptArgs) {
|
|
@@ -65,6 +66,7 @@ tenbo-dashboard — local architecture dashboard for .tenbo/ repos
|
|
|
65
66
|
|
|
66
67
|
Usage:
|
|
67
68
|
tenbo-dashboard Launch the dashboard (http://localhost:5174)
|
|
69
|
+
tenbo-dashboard sync Refresh tenbo state after a change (metrics + init-check + validate, surfaces new findings)
|
|
68
70
|
tenbo-dashboard validate Run validation rules
|
|
69
71
|
tenbo-dashboard init-check Strict completeness check for fresh init (errors on missing skeletons, file_count:0, etc)
|
|
70
72
|
tenbo-dashboard next-id <prefix> Allocate next roadmap item ID
|
package/package.json
CHANGED
package/scripts/sync.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync.ts — single command for "make tenbo state fresh after a change".
|
|
3
|
+
*
|
|
4
|
+
* Runs metrics --all (or --scope <id>) + init-check + validate, then prints
|
|
5
|
+
* a unified summary of NEW findings (severity >= warning) introduced since
|
|
6
|
+
* the prior metrics.json snapshot. Replaces three separate commands the
|
|
7
|
+
* skill used to invoke at init and completion time, eliminating the
|
|
8
|
+
* silent-staleness failure mode where an agent forgot one of the three.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 — everything refreshed cleanly, no new errors
|
|
12
|
+
* 1 — an error in any step (validation failure, init defect, metrics throw)
|
|
13
|
+
* 2 — could not locate repo root
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { findRepoRoot } from '../src/api/lib/repoRoot';
|
|
18
|
+
import { runComputeMetrics } from './compute-metrics';
|
|
19
|
+
import { runInitCheck } from './init-check';
|
|
20
|
+
import { readState } from '../src/api/lib/tenboFs';
|
|
21
|
+
import { validate } from '../src/api/lib/validator';
|
|
22
|
+
import type { ScopeMetrics } from '../src/types';
|
|
23
|
+
|
|
24
|
+
interface NewFinding {
|
|
25
|
+
scope: string;
|
|
26
|
+
layer: string;
|
|
27
|
+
severity: string;
|
|
28
|
+
signal: string;
|
|
29
|
+
headline: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readMetrics(metricsPath: string): ScopeMetrics | null {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(metricsPath)) return null;
|
|
35
|
+
return JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findingKey(scope: string, f: { signal: string; layer: string; headline: string }): string {
|
|
42
|
+
return `${scope}::${f.signal}::${f.layer}::${f.headline}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SyncArgs {
|
|
46
|
+
repoRoot: string;
|
|
47
|
+
scope?: string; // single-scope refresh; omit to refresh all
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runSync(args: SyncArgs): Promise<number> {
|
|
51
|
+
const { repoRoot, scope } = args;
|
|
52
|
+
|
|
53
|
+
// Snapshot prior findings so we can diff after the refresh.
|
|
54
|
+
const state = readState(repoRoot);
|
|
55
|
+
const targetScopes = scope ? [scope] : state.scopes.map((s) => s.id);
|
|
56
|
+
const priorFindingKeys = new Set<string>();
|
|
57
|
+
for (const id of targetScopes) {
|
|
58
|
+
const m = readMetrics(path.join(repoRoot, '.tenbo', 'scopes', id, 'metrics.json'));
|
|
59
|
+
for (const f of m?.findings ?? []) priorFindingKeys.add(findingKey(id, f));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 1. metrics
|
|
63
|
+
const metricsCode = await runComputeMetrics({
|
|
64
|
+
repoRoot,
|
|
65
|
+
all: !scope,
|
|
66
|
+
scope,
|
|
67
|
+
});
|
|
68
|
+
if (metricsCode !== 0) {
|
|
69
|
+
process.stderr.write('sync: metrics step failed\n');
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. init-check
|
|
74
|
+
const { defects } = runInitCheck(repoRoot);
|
|
75
|
+
if (defects.length > 0) {
|
|
76
|
+
process.stderr.write(`sync: init-check FAILED — ${defects.length} init defect(s):\n`);
|
|
77
|
+
for (const d of defects) {
|
|
78
|
+
const loc = d.scope ? ` [${d.scope}${d.layerId ? `/${d.layerId}` : ''}]` : '';
|
|
79
|
+
process.stderr.write(` ❌${loc} ${d.message}\n`);
|
|
80
|
+
}
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. validate (re-read state after metrics may have changed scope dirs)
|
|
85
|
+
const stateAfter = readState(repoRoot);
|
|
86
|
+
const result = validate(stateAfter);
|
|
87
|
+
if (result.errors.length > 0) {
|
|
88
|
+
process.stderr.write(`sync: validate FAILED — ${result.errors.length} error(s):\n`);
|
|
89
|
+
for (const e of result.errors) process.stderr.write(` ❌ ${e.message}\n`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 4. Diff findings: surface NEW critical/warning findings introduced this run.
|
|
94
|
+
const newFindings: NewFinding[] = [];
|
|
95
|
+
for (const id of targetScopes) {
|
|
96
|
+
const m = readMetrics(path.join(repoRoot, '.tenbo', 'scopes', id, 'metrics.json'));
|
|
97
|
+
for (const f of m?.findings ?? []) {
|
|
98
|
+
if (f.severity === 'info') continue;
|
|
99
|
+
if (priorFindingKeys.has(findingKey(id, f))) continue;
|
|
100
|
+
newFindings.push({
|
|
101
|
+
scope: id,
|
|
102
|
+
layer: f.layer,
|
|
103
|
+
severity: f.severity,
|
|
104
|
+
signal: f.signal,
|
|
105
|
+
headline: f.headline,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 5. Summary line + (optional) new-finding lines.
|
|
111
|
+
const scopeLabel = scope ? `scope "${scope}"` : `${targetScopes.length} scope(s)`;
|
|
112
|
+
if (newFindings.length === 0) {
|
|
113
|
+
process.stdout.write(`sync: ${scopeLabel} fresh. No new errors, no new warning/critical findings.\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write(`sync: ${scopeLabel} fresh. ${newFindings.length} new finding(s) since last refresh:\n`);
|
|
117
|
+
for (const f of newFindings) {
|
|
118
|
+
const icon = f.severity === 'critical' ? '🔴' : '⚠️';
|
|
119
|
+
process.stdout.write(` ${icon} [${f.scope}/${f.layer}] ${f.signal}: ${f.headline}\n`);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isMain(): boolean {
|
|
125
|
+
try {
|
|
126
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
127
|
+
const here = path.resolve(new URL(import.meta.url).pathname);
|
|
128
|
+
return invoked === here;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isMain()) {
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
const scopeIdx = args.indexOf('--scope');
|
|
137
|
+
const scope = scopeIdx >= 0 ? args[scopeIdx + 1] : undefined;
|
|
138
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
139
|
+
if (!repoRoot) {
|
|
140
|
+
process.stderr.write('sync: unable to find repo root (no .git in any parent directory)\n');
|
|
141
|
+
process.exit(2);
|
|
142
|
+
}
|
|
143
|
+
runSync({ repoRoot, scope }).then((code) => process.exit(code));
|
|
144
|
+
}
|