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.
@@ -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;
@@ -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
- // No recognized subcommand default to launching the dashboard
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tenbo-dashboard",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Local-first architecture dashboard and CLI tools for .tenbo/ repos",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 <div style={{ padding: 24 }}>Loading…</div>;
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'] },