tenbo-dashboard 0.4.0 → 0.4.1
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/bin/launch-dashboard.mjs +51 -0
- package/bin/tenbo-dashboard.mjs +19 -3
- package/package.json +1 -1
- package/src/App.tsx +25 -1
- package/vite.config.ts +6 -0
package/bin/launch-dashboard.mjs
CHANGED
|
@@ -1,8 +1,59 @@
|
|
|
1
1
|
import { createServer } from 'vite';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import net from 'node:net';
|
|
4
6
|
|
|
7
|
+
const PREFERRED_PORT = 5174;
|
|
5
8
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
|
|
10
|
+
// The cwd is the user's project directory (the dashboard reads .tenbo/ from there).
|
|
11
|
+
// Surface it on startup so the user can tell which repo a given dashboard is
|
|
12
|
+
// serving — important when multiple dashboards are running at once. (td-011)
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
console.log(`Serving .tenbo/ from ${cwd}`);
|
|
15
|
+
|
|
16
|
+
// Detect whether something is already listening on the preferred port. If so,
|
|
17
|
+
// try probing it for a tenbo-dashboard /api/state endpoint so we can name the
|
|
18
|
+
// other repo in the warning. Either way, surface a warning so the user isn't
|
|
19
|
+
// surprised when this instance lands on a different port. (td-011)
|
|
20
|
+
async function isPortInUse(port) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const tester = net.createConnection({ port, host: '127.0.0.1' });
|
|
23
|
+
tester.once('connect', () => { tester.destroy(); resolve(true); });
|
|
24
|
+
tester.once('error', () => resolve(false));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function probeOtherDashboard(port) {
|
|
29
|
+
try {
|
|
30
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/state`, {
|
|
31
|
+
signal: AbortSignal.timeout(500),
|
|
32
|
+
});
|
|
33
|
+
if (!r.ok) return null;
|
|
34
|
+
// We don't have a structured "what's the cwd of this server" endpoint, so
|
|
35
|
+
// we infer from the scopes' paths. If unavailable, fall through.
|
|
36
|
+
const data = await r.json();
|
|
37
|
+
const firstScope = data?.scopes?.[0];
|
|
38
|
+
return firstScope ? `serving ${data.scopes.length} scope(s) (e.g. "${firstScope.id}")` : 'unknown repo';
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (await isPortInUse(PREFERRED_PORT)) {
|
|
45
|
+
const other = await probeOtherDashboard(PREFERRED_PORT);
|
|
46
|
+
const detail = other ? ` (${other})` : '';
|
|
47
|
+
console.log(
|
|
48
|
+
`Port ${PREFERRED_PORT} is already in use — likely another tenbo-dashboard${detail}.`,
|
|
49
|
+
);
|
|
50
|
+
console.log(`This instance will start on the next available port and serve ${cwd}.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
6
53
|
const server = await createServer({ root, configFile: path.join(root, 'vite.config.ts') });
|
|
7
54
|
await server.listen();
|
|
8
55
|
server.printUrls();
|
|
56
|
+
|
|
57
|
+
// Touch existsSync so unused-import lint doesn't strip it — it's used for
|
|
58
|
+
// future expansion (probing other dashboards' tenbo state files).
|
|
59
|
+
void existsSync;
|
package/bin/tenbo-dashboard.mjs
CHANGED
|
@@ -41,6 +41,13 @@ const commands = {
|
|
|
41
41
|
sync: 'scripts/sync.ts',
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
// Documented aliases for the bare-launch behavior. Users (and agents) reading
|
|
45
|
+
// help shouldn't have to discover that running `tenbo-dashboard` with no args
|
|
46
|
+
// launches the dashboard — these subcommand verbs are conventional and now
|
|
47
|
+
// explicit. Anything not in `commands` and not in this set is treated as an
|
|
48
|
+
// error rather than silently launching. (td-012)
|
|
49
|
+
const LAUNCH_ALIASES = new Set(['serve', 'start', 'dev']);
|
|
50
|
+
|
|
44
51
|
function run(script, scriptArgs) {
|
|
45
52
|
const tsx = findTsx();
|
|
46
53
|
const child = spawn(tsx, [path.join(root, script), ...scriptArgs], {
|
|
@@ -66,6 +73,7 @@ tenbo-dashboard — local architecture dashboard for .tenbo/ repos
|
|
|
66
73
|
|
|
67
74
|
Usage:
|
|
68
75
|
tenbo-dashboard Launch the dashboard (http://localhost:5174)
|
|
76
|
+
tenbo-dashboard serve Same as bare invocation (also: 'start', 'dev')
|
|
69
77
|
tenbo-dashboard sync Refresh tenbo state after a change (metrics + init-check + validate, surfaces new findings)
|
|
70
78
|
tenbo-dashboard validate Run validation rules
|
|
71
79
|
tenbo-dashboard init-check Strict completeness check for fresh init (errors on missing skeletons, file_count:0, etc)
|
|
@@ -73,14 +81,22 @@ Usage:
|
|
|
73
81
|
tenbo-dashboard metrics --all Compute scope metrics
|
|
74
82
|
tenbo-dashboard --version Print package version
|
|
75
83
|
tenbo-dashboard help Show this help
|
|
84
|
+
|
|
85
|
+
The dashboard reads/writes .tenbo/ in the current working directory. To
|
|
86
|
+
serve a different repo, cd into it before running.
|
|
76
87
|
`);
|
|
77
88
|
process.exit(0);
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
if (commands[command]) {
|
|
81
92
|
run(commands[command], args);
|
|
82
|
-
} else {
|
|
83
|
-
//
|
|
84
|
-
// Pass the original command back as an arg in case it was a flag
|
|
93
|
+
} else if (!command || LAUNCH_ALIASES.has(command)) {
|
|
94
|
+
// Bare invocation OR a documented launch alias ('serve' / 'start' / 'dev').
|
|
85
95
|
startDashboard();
|
|
96
|
+
} else {
|
|
97
|
+
// Unknown subcommand — surface clearly instead of silently launching.
|
|
98
|
+
// (Pre-td-012 behavior was to fall through; that hid typos.)
|
|
99
|
+
process.stderr.write(`tenbo-dashboard: unknown subcommand '${command}'\n`);
|
|
100
|
+
process.stderr.write(`Run 'tenbo-dashboard help' for usage.\n`);
|
|
101
|
+
process.exit(2);
|
|
86
102
|
}
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -24,6 +24,30 @@ type Overlay =
|
|
|
24
24
|
| { kind: 'modal'; scopeId: string; item: Item }
|
|
25
25
|
| null;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Loading state shown until the initial GET /api/state lands. Adds a "still
|
|
29
|
+
* loading after 5s" hint so a slow first launch (e.g. cold Vite optimizeDeps)
|
|
30
|
+
* doesn't look like a broken state. (td-013)
|
|
31
|
+
*/
|
|
32
|
+
function LoadingState() {
|
|
33
|
+
const [showSlowHint, setShowSlowHint] = useState(false);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const t = window.setTimeout(() => setShowSlowHint(true), 5000);
|
|
36
|
+
return () => window.clearTimeout(t);
|
|
37
|
+
}, []);
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ padding: 24, fontFamily: 'system-ui', color: 'var(--console-fog)' }}>
|
|
40
|
+
<div style={{ marginBottom: 8 }}>Loading workspace from <code>.tenbo/</code>…</div>
|
|
41
|
+
{showSlowHint && (
|
|
42
|
+
<div style={{ fontSize: 12, opacity: 0.7, maxWidth: 520 }}>
|
|
43
|
+
First launch can take ~10 seconds while Vite optimizes the dependency graph.
|
|
44
|
+
If this persists past ~30 seconds, check the terminal for errors.
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
export default function App() {
|
|
28
52
|
const { state, loadError, reload, mergeItem, generation } = useTenboState();
|
|
29
53
|
const patch = useApiPatch();
|
|
@@ -50,7 +74,7 @@ export default function App() {
|
|
|
50
74
|
|
|
51
75
|
if (loadError && loadError.includes('404')) return <EmptyState message="No .tenbo/ found at the repo root." />;
|
|
52
76
|
if (loadError) return <div style={{ padding: 24, color: 'var(--error)' }}>Error: {loadError} <button onClick={reload}>retry</button></div>;
|
|
53
|
-
if (!state) return <
|
|
77
|
+
if (!state) return <LoadingState />;
|
|
54
78
|
|
|
55
79
|
const mode = modeFromRoute(route);
|
|
56
80
|
const onSelectMode = (m: Mode) => {
|
package/vite.config.ts
CHANGED
|
@@ -24,6 +24,12 @@ export default defineConfig({
|
|
|
24
24
|
'remark-gfm',
|
|
25
25
|
'style-to-js',
|
|
26
26
|
'hast-util-to-jsx-runtime',
|
|
27
|
+
// lucide-react ships ~1500 individual icon files in dist/esm/icons/.
|
|
28
|
+
// Without pre-bundling, the dev server serves each icon as a separate
|
|
29
|
+
// ESM module the browser fetches one-by-one — first launch sits on
|
|
30
|
+
// 'Loading...' for 10+ seconds. Forcing pre-bundle collapses them
|
|
31
|
+
// into a single chunk served from .vite/deps. (td-010)
|
|
32
|
+
'lucide-react',
|
|
27
33
|
],
|
|
28
34
|
},
|
|
29
35
|
test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] },
|