tenbo-dashboard 0.1.0 → 0.2.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 +30 -6
- package/bin/launch-dashboard.mjs +8 -0
- package/bin/tenbo-dashboard.mjs +31 -14
- package/package.json +6 -6
- package/scripts/init-check.ts +133 -0
- package/vite.config.ts +21 -0
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Local architecture dashboard and CLI tools for [tenbo](https://github.com/poyi/tenbo) — an agentic project manager for AI-assisted coding.
|
|
4
4
|
|
|
5
|
-

|
|
6
|
-
|
|
7
5
|
## Quick start
|
|
8
6
|
|
|
9
7
|
```bash
|
|
@@ -11,19 +9,45 @@ Local architecture dashboard and CLI tools for [tenbo](https://github.com/poyi/t
|
|
|
11
9
|
npx tenbo-dashboard
|
|
12
10
|
```
|
|
13
11
|
|
|
14
|
-
Opens a local dashboard
|
|
12
|
+
Opens a local dashboard at http://localhost:5174.
|
|
13
|
+
|
|
14
|
+
## What it gives you
|
|
15
|
+
|
|
16
|
+
Three views over the same `.tenbo/` files Claude maintains. Edits in either surface show up in the other on next refresh.
|
|
17
|
+
|
|
18
|
+
### Roadmap — what to work on next
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
Kanban across `now` / `next` / `later` / `done`, grouped by scope and layer. Drag to reprioritize, click to edit. The same roadmap Claude reads when you ask "what should I build next?".
|
|
23
|
+
|
|
24
|
+
### Docs — what the project actually does
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
Project overview, principles, and glossary alongside per-layer narratives, intents (responsibilities + boundaries + invariants), and code maps (entry points + key files + extension recipes). Onboard new contributors — human or AI — in minutes instead of an hour of grep.
|
|
29
|
+
|
|
30
|
+
### Health — where the codebase is quietly rotting
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
Surfaces the things that would otherwise become tech debt nobody mentions: oversized layers (hotspots), code-map references that no longer match real files (doc drift), unreferenced files, coupling violations, dead code. Each finding carries a severity, the file it points at, and a suggested fix.
|
|
15
35
|
|
|
16
36
|
## CLI tools
|
|
17
37
|
|
|
38
|
+
The same package ships commands the skill uses behind the scenes — also runnable directly:
|
|
39
|
+
|
|
18
40
|
```bash
|
|
19
|
-
npx tenbo-dashboard validate #
|
|
41
|
+
npx tenbo-dashboard validate # Schema + consistency checks
|
|
42
|
+
npx tenbo-dashboard init-check # Strict completeness check after fresh init
|
|
43
|
+
npx tenbo-dashboard metrics --all # Recompute scope metrics + health findings
|
|
20
44
|
npx tenbo-dashboard next-id <prefix> # Allocate next roadmap item ID
|
|
21
|
-
npx tenbo-dashboard
|
|
45
|
+
npx tenbo-dashboard --version # Print the installed version
|
|
22
46
|
```
|
|
23
47
|
|
|
24
48
|
## What is tenbo?
|
|
25
49
|
|
|
26
|
-
|
|
50
|
+
A Claude Code skill that gives your AI coding assistant persistent project memory — architecture docs, roadmaps, and health signals that survive across sessions. The dashboard is the optional visual companion; the skill is the always-on conversational brain.
|
|
27
51
|
|
|
28
52
|
Install the skill: [github.com/poyi/tenbo](https://github.com/poyi/tenbo)
|
|
29
53
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createServer } from 'vite';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
const server = await createServer({ root, configFile: path.join(root, 'vite.config.ts') });
|
|
7
|
+
await server.listen();
|
|
8
|
+
server.printUrls();
|
package/bin/tenbo-dashboard.mjs
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const root = path.resolve(__dirname, '..');
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Find the tsx binary by walking up node_modules from this file's directory.
|
|
13
|
+
* Required because npm hoists shared dependencies to a parent node_modules,
|
|
14
|
+
* so `<package>/node_modules/.bin/tsx` may not exist for consumers.
|
|
15
|
+
*/
|
|
16
|
+
function findTsx() {
|
|
17
|
+
let dir = __dirname;
|
|
18
|
+
for (let i = 0; i < 10; i++) {
|
|
19
|
+
const candidate = path.join(dir, 'node_modules', '.bin', 'tsx');
|
|
20
|
+
if (existsSync(candidate)) return candidate;
|
|
21
|
+
const parent = path.dirname(dir);
|
|
22
|
+
if (parent === dir) break;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
return 'tsx'; // last-resort fallback to PATH
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
const [command, ...args] = process.argv.slice(2);
|
|
11
29
|
|
|
30
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
32
|
+
console.log(pkg.version);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
const commands = {
|
|
13
37
|
validate: 'scripts/validate-cli.ts',
|
|
14
38
|
'next-id': 'scripts/next-id.ts',
|
|
15
39
|
metrics: 'scripts/compute-metrics.ts',
|
|
40
|
+
'init-check': 'scripts/init-check.ts',
|
|
16
41
|
};
|
|
17
42
|
|
|
18
43
|
function run(script, scriptArgs) {
|
|
19
|
-
const tsx =
|
|
44
|
+
const tsx = findTsx();
|
|
20
45
|
const child = spawn(tsx, [path.join(root, script), ...scriptArgs], {
|
|
21
46
|
stdio: 'inherit',
|
|
22
47
|
cwd: process.cwd(),
|
|
@@ -25,18 +50,8 @@ function run(script, scriptArgs) {
|
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
function startDashboard() {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
import { createServer } from 'vite';
|
|
31
|
-
import { fileURLToPath } from 'node:url';
|
|
32
|
-
import path from 'node:path';
|
|
33
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
-
const server = await createServer({ root, configFile: path.join(root, 'vite.config.ts') });
|
|
35
|
-
await server.listen();
|
|
36
|
-
server.printUrls();
|
|
37
|
-
`;
|
|
38
|
-
const tsx = path.join(root, 'node_modules', '.bin', 'tsx');
|
|
39
|
-
const child = spawn(tsx, ['--eval', viteScript], {
|
|
53
|
+
const launcher = path.join(__dirname, 'launch-dashboard.mjs');
|
|
54
|
+
const child = spawn(process.execPath, [launcher], {
|
|
40
55
|
stdio: 'inherit',
|
|
41
56
|
cwd: process.cwd(),
|
|
42
57
|
env: { ...process.env, TENBO_CWD: process.cwd() },
|
|
@@ -44,15 +59,17 @@ function startDashboard() {
|
|
|
44
59
|
child.on('close', (code) => process.exit(code ?? 0));
|
|
45
60
|
}
|
|
46
61
|
|
|
47
|
-
if (
|
|
62
|
+
if (command === 'help' || command === '--help') {
|
|
48
63
|
console.log(`
|
|
49
64
|
tenbo-dashboard — local architecture dashboard for .tenbo/ repos
|
|
50
65
|
|
|
51
66
|
Usage:
|
|
52
67
|
tenbo-dashboard Launch the dashboard (http://localhost:5174)
|
|
53
68
|
tenbo-dashboard validate Run validation rules
|
|
69
|
+
tenbo-dashboard init-check Strict completeness check for fresh init (errors on missing skeletons, file_count:0, etc)
|
|
54
70
|
tenbo-dashboard next-id <prefix> Allocate next roadmap item ID
|
|
55
71
|
tenbo-dashboard metrics --all Compute scope metrics
|
|
72
|
+
tenbo-dashboard --version Print package version
|
|
56
73
|
tenbo-dashboard help Show this help
|
|
57
74
|
`);
|
|
58
75
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tenbo-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local-first architecture dashboard and CLI tools for .tenbo/ repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,20 +45,22 @@
|
|
|
45
45
|
"@dnd-kit/core": "^6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "^8.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "^3.2.2",
|
|
48
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
48
49
|
"@xyflow/react": "^12.10.2",
|
|
49
50
|
"chokidar": "^3.6.0",
|
|
50
51
|
"dagre": "^0.8.5",
|
|
51
52
|
"gray-matter": "^4.0.3",
|
|
53
|
+
"jscpd": "^4",
|
|
52
54
|
"lucide-react": "^0.545.0",
|
|
53
55
|
"react": "^18.3.1",
|
|
54
56
|
"react-dom": "^18.3.1",
|
|
55
57
|
"react-markdown": "^9.0.1",
|
|
56
58
|
"remark-gfm": "^4.0.0",
|
|
57
|
-
"
|
|
58
|
-
"@vitejs/plugin-react": "^4.3.1",
|
|
59
|
+
"ts-morph": "^25",
|
|
59
60
|
"tsx": "^4.19.0",
|
|
60
61
|
"typescript": "^5.5.4",
|
|
61
|
-
"vite": "^5.4.3"
|
|
62
|
+
"vite": "^5.4.3",
|
|
63
|
+
"yaml": "^2.5.1"
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -67,9 +69,7 @@
|
|
|
67
69
|
"@types/node": "^22.5.4",
|
|
68
70
|
"@types/react": "^18.3.5",
|
|
69
71
|
"@types/react-dom": "^18.3.0",
|
|
70
|
-
"jscpd": "^4",
|
|
71
72
|
"jsdom": "^25.0.0",
|
|
72
|
-
"ts-morph": "^25",
|
|
73
73
|
"vitest": "^2.0.5"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-check.ts — strict variant of validate, used by the skill at the end of
|
|
3
|
+
* the Initialize Project Memory flow.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the standard validator, then upgrades or adds checks that catch
|
|
6
|
+
* silent init failures (missing skeleton docs, glob mistakes producing
|
|
7
|
+
* `file_count: 0`, missing principles.md/glossary.md, missing metrics.json).
|
|
8
|
+
*
|
|
9
|
+
* Exit codes:
|
|
10
|
+
* 0 — every init artifact is present and structurally sound
|
|
11
|
+
* 1 — at least one init defect found
|
|
12
|
+
* 2 — could not locate repo root
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { findRepoRoot } from '../src/api/lib/repoRoot';
|
|
17
|
+
import { readState } from '../src/api/lib/tenboFs';
|
|
18
|
+
import { validate } from '../src/api/lib/validator';
|
|
19
|
+
|
|
20
|
+
interface InitDefect {
|
|
21
|
+
message: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
layerId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runInitCheck(repoRoot: string): { defects: InitDefect[]; preExistingErrors: number } {
|
|
27
|
+
const defects: InitDefect[] = [];
|
|
28
|
+
const tenbo = path.join(repoRoot, '.tenbo');
|
|
29
|
+
|
|
30
|
+
// Repo-level files
|
|
31
|
+
if (!fs.existsSync(path.join(tenbo, 'workspace.yaml'))) {
|
|
32
|
+
defects.push({ message: 'missing .tenbo/workspace.yaml' });
|
|
33
|
+
}
|
|
34
|
+
if (!fs.existsSync(path.join(tenbo, 'principles.md'))) {
|
|
35
|
+
defects.push({ message: 'missing .tenbo/principles.md (init step 8 — write from template)' });
|
|
36
|
+
}
|
|
37
|
+
if (!fs.existsSync(path.join(tenbo, 'glossary.md'))) {
|
|
38
|
+
defects.push({ message: 'missing .tenbo/glossary.md (init step 8 — write from template)' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const state = readState(repoRoot);
|
|
42
|
+
|
|
43
|
+
// Per-scope and per-layer
|
|
44
|
+
for (const scope of state.scopes) {
|
|
45
|
+
const scopeDir = path.join(tenbo, 'scopes', scope.id);
|
|
46
|
+
const metricsPath = path.join(scopeDir, 'metrics.json');
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(metricsPath)) {
|
|
49
|
+
defects.push({
|
|
50
|
+
message: `scope "${scope.id}" missing metrics.json — run "npx tenbo-dashboard metrics --all"`,
|
|
51
|
+
scope: scope.id,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
// Check for the silent glob bug: file_count: 0 in a scope that obviously has files.
|
|
55
|
+
try {
|
|
56
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
|
57
|
+
const layers = metrics?.layers ?? {};
|
|
58
|
+
// Quick file existence check on the scope path
|
|
59
|
+
const scopeRoot = path.resolve(repoRoot, scope.path ?? '.');
|
|
60
|
+
const scopeHasFiles = fs.existsSync(scopeRoot) && fs.readdirSync(scopeRoot).length > 0;
|
|
61
|
+
for (const [layerId, m] of Object.entries(layers) as [string, { file_count?: number }][]) {
|
|
62
|
+
if (scopeHasFiles && (m.file_count ?? 0) === 0) {
|
|
63
|
+
defects.push({
|
|
64
|
+
message: `layer "${layerId}" has file_count: 0 despite scope "${scope.id}" containing files — likely a glob path mistake. Globs in architecture.yaml are RELATIVE to scope.path (not the repo root). See architecture.yaml.tmpl header comment.`,
|
|
65
|
+
scope: scope.id,
|
|
66
|
+
layerId,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
defects.push({
|
|
72
|
+
message: `scope "${scope.id}" metrics.json is unreadable: ${(e as Error).message}`,
|
|
73
|
+
scope: scope.id,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const layer of scope.layers) {
|
|
79
|
+
const docs = state.layerDocs?.[`${scope.id}/${layer.id}`];
|
|
80
|
+
if (!docs?.hasIntent) {
|
|
81
|
+
defects.push({
|
|
82
|
+
message: `layer "${layer.id}" missing intent.md (init step 8 — write skeleton from template)`,
|
|
83
|
+
scope: scope.id,
|
|
84
|
+
layerId: layer.id,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!docs?.hasCodeMap) {
|
|
88
|
+
defects.push({
|
|
89
|
+
message: `layer "${layer.id}" missing code-map.md (init step 8 — write skeleton from template)`,
|
|
90
|
+
scope: scope.id,
|
|
91
|
+
layerId: layer.id,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Reuse standard validator for everything else; treat its errors as defects.
|
|
98
|
+
const result = validate(state);
|
|
99
|
+
for (const e of result.errors) {
|
|
100
|
+
defects.push({ message: e.message, scope: e.scope, layerId: e.layerId });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { defects, preExistingErrors: result.errors.length };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isMain(): boolean {
|
|
107
|
+
try {
|
|
108
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
109
|
+
const here = path.resolve(new URL(import.meta.url).pathname);
|
|
110
|
+
return invoked === here;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isMain()) {
|
|
117
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
118
|
+
if (!repoRoot) {
|
|
119
|
+
process.stderr.write('init-check: unable to find repo root (no .tenbo/ in any parent directory)\n');
|
|
120
|
+
process.exit(2);
|
|
121
|
+
}
|
|
122
|
+
const { defects } = runInitCheck(repoRoot);
|
|
123
|
+
if (defects.length === 0) {
|
|
124
|
+
process.stdout.write('init-check: all init artifacts present.\n');
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
process.stderr.write(`init-check FAILED — ${defects.length} init defect(s):\n`);
|
|
128
|
+
for (const d of defects) {
|
|
129
|
+
const loc = d.scope ? ` [${d.scope}${d.layerId ? `/${d.layerId}` : ''}]` : '';
|
|
130
|
+
process.stderr.write(` ❌${loc} ${d.message}\n`);
|
|
131
|
+
}
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -5,5 +5,26 @@ import { tenboApiPlugin } from './src/api/plugin';
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
plugins: [react(), tenboApiPlugin()],
|
|
7
7
|
server: { port: 5174 },
|
|
8
|
+
// Force pre-bundling of the markdown chain. When the dashboard is installed as a
|
|
9
|
+
// consumer dependency, Vite's optimizer fails to walk into transitive CJS deps
|
|
10
|
+
// like `style-to-js`, and serving them raw via `/@fs/` produces a silent
|
|
11
|
+
// "module does not provide an export named 'default'" SyntaxError that aborts
|
|
12
|
+
// React mounting. Listing them here makes Vite pre-bundle them with proper
|
|
13
|
+
// ESM interop.
|
|
14
|
+
optimizeDeps: {
|
|
15
|
+
include: [
|
|
16
|
+
// React subpath — the optimizer auto-handles `react-dom` root but NOT
|
|
17
|
+
// `react-dom/client`, so the subpath gets served raw via `/@fs/` and
|
|
18
|
+
// breaks default-import interop in consumer installs.
|
|
19
|
+
'react-dom/client',
|
|
20
|
+
// Markdown chain — transitive CJS deps (style-to-js, hast-util-to-jsx-runtime)
|
|
21
|
+
// also need explicit pre-bundling for consumer installs. Without this,
|
|
22
|
+
// they're served raw and produce silent SyntaxError aborting React mount.
|
|
23
|
+
'react-markdown',
|
|
24
|
+
'remark-gfm',
|
|
25
|
+
'style-to-js',
|
|
26
|
+
'hast-util-to-jsx-runtime',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
8
29
|
test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] },
|
|
9
30
|
});
|