tenbo-dashboard 0.1.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 +31 -7
- package/bin/launch-dashboard.mjs +8 -0
- package/bin/tenbo-dashboard.mjs +33 -14
- package/package.json +6 -6
- package/scripts/init-check.ts +133 -0
- package/scripts/sync.ts +144 -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,21 +9,47 @@ 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
|
+
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.
|
|
27
51
|
|
|
28
|
-
Install
|
|
52
|
+
Install: [github.com/poyi/tenbo](https://github.com/poyi/tenbo) (instructions for both Claude Code and Cursor)
|
|
29
53
|
|
|
30
54
|
## License
|
|
31
55
|
|
|
@@ -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,48 @@
|
|
|
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',
|
|
41
|
+
sync: 'scripts/sync.ts',
|
|
16
42
|
};
|
|
17
43
|
|
|
18
44
|
function run(script, scriptArgs) {
|
|
19
|
-
const tsx =
|
|
45
|
+
const tsx = findTsx();
|
|
20
46
|
const child = spawn(tsx, [path.join(root, script), ...scriptArgs], {
|
|
21
47
|
stdio: 'inherit',
|
|
22
48
|
cwd: process.cwd(),
|
|
@@ -25,18 +51,8 @@ function run(script, scriptArgs) {
|
|
|
25
51
|
}
|
|
26
52
|
|
|
27
53
|
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], {
|
|
54
|
+
const launcher = path.join(__dirname, 'launch-dashboard.mjs');
|
|
55
|
+
const child = spawn(process.execPath, [launcher], {
|
|
40
56
|
stdio: 'inherit',
|
|
41
57
|
cwd: process.cwd(),
|
|
42
58
|
env: { ...process.env, TENBO_CWD: process.cwd() },
|
|
@@ -44,15 +60,18 @@ function startDashboard() {
|
|
|
44
60
|
child.on('close', (code) => process.exit(code ?? 0));
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
if (
|
|
63
|
+
if (command === 'help' || command === '--help') {
|
|
48
64
|
console.log(`
|
|
49
65
|
tenbo-dashboard — local architecture dashboard for .tenbo/ repos
|
|
50
66
|
|
|
51
67
|
Usage:
|
|
52
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)
|
|
53
70
|
tenbo-dashboard validate Run validation rules
|
|
71
|
+
tenbo-dashboard init-check Strict completeness check for fresh init (errors on missing skeletons, file_count:0, etc)
|
|
54
72
|
tenbo-dashboard next-id <prefix> Allocate next roadmap item ID
|
|
55
73
|
tenbo-dashboard metrics --all Compute scope metrics
|
|
74
|
+
tenbo-dashboard --version Print package version
|
|
56
75
|
tenbo-dashboard help Show this help
|
|
57
76
|
`);
|
|
58
77
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tenbo-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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/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
|
+
}
|
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
|
});
|