@geminilight/mindos 0.1.2 → 0.1.3

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,9 +1,8 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { Geist, Geist_Mono, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
3
- import { headers } from 'next/headers';
4
3
  import './globals.css';
5
4
  import { getFileTree } from '@/lib/fs';
6
- import SidebarLayout from '@/components/SidebarLayout';
5
+ import ShellLayout from '@/components/ShellLayout';
7
6
  import { TooltipProvider } from '@/components/ui/tooltip';
8
7
  import { LocaleProvider } from '@/lib/LocaleContext';
9
8
  import ErrorBoundary from '@/components/ErrorBoundary';
@@ -49,7 +48,7 @@ export const viewport = {
49
48
  viewportFit: 'cover' as const,
50
49
  };
51
50
 
52
- export default async function RootLayout({
51
+ export default function RootLayout({
53
52
  children,
54
53
  }: Readonly<{
55
54
  children: React.ReactNode;
@@ -61,9 +60,6 @@ export default async function RootLayout({
61
60
  console.error('[RootLayout] Failed to load file tree:', err);
62
61
  }
63
62
 
64
- const headersList = await headers();
65
- const isLoginPage = headersList.get('x-pathname') === '/login';
66
-
67
63
  return (
68
64
  <html lang="en" suppressHydrationWarning>
69
65
  <head>
@@ -89,11 +85,9 @@ export default async function RootLayout({
89
85
  <LocaleProvider>
90
86
  <TooltipProvider delay={300}>
91
87
  <ErrorBoundary>
92
- {isLoginPage ? children : (
93
- <SidebarLayout fileTree={fileTree}>
94
- {children}
95
- </SidebarLayout>
96
- )}
88
+ <ShellLayout fileTree={fileTree}>
89
+ {children}
90
+ </ShellLayout>
97
91
  </ErrorBoundary>
98
92
  </TooltipProvider>
99
93
  </LocaleProvider>
@@ -55,7 +55,7 @@ export default function ViewPageClient({
55
55
  },
56
56
  () => {
57
57
  const saved = localStorage.getItem('mindos-use-raw');
58
- return saved !== null ? saved === 'true' : true;
58
+ return saved !== null ? saved === 'true' : false;
59
59
  },
60
60
  () => false,
61
61
  );
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo } from 'react';
3
+ import { useState, useSyncExternalStore, useMemo } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
6
6
  import Breadcrumb from '@/components/Breadcrumb';
@@ -33,16 +33,22 @@ function countFiles(node: FileNode): number {
33
33
  const DIR_VIEW_KEY = 'mindos-dir-view';
34
34
 
35
35
  function useDirViewPref() {
36
- const [view, setViewState] = useState<'grid' | 'list'>('grid');
37
-
38
- useEffect(() => {
39
- const saved = localStorage.getItem(DIR_VIEW_KEY);
40
- if (saved === 'list' || saved === 'grid') setViewState(saved);
41
- }, []);
36
+ const view = useSyncExternalStore(
37
+ (onStoreChange) => {
38
+ const listener = () => onStoreChange();
39
+ window.addEventListener('mindos-dir-view-change', listener);
40
+ return () => window.removeEventListener('mindos-dir-view-change', listener);
41
+ },
42
+ () => {
43
+ const saved = localStorage.getItem(DIR_VIEW_KEY);
44
+ return (saved === 'list' || saved === 'grid') ? saved : 'grid';
45
+ },
46
+ () => 'grid' as const,
47
+ );
42
48
 
43
49
  const setView = (v: 'grid' | 'list') => {
44
- setViewState(v);
45
50
  localStorage.setItem(DIR_VIEW_KEY, v);
51
+ window.dispatchEvent(new Event('mindos-dir-view-change'));
46
52
  };
47
53
 
48
54
  return [view, setView] as const;
@@ -26,20 +26,19 @@ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
26
26
  const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
27
27
  const { t, locale, setLocale } = useLocale();
28
28
 
29
- // Appearance state (localStorage-based)
30
- const [font, setFont] = useState('lora');
31
- const [contentWidth, setContentWidth] = useState('780px');
32
- const [dark, setDark] = useState(true);
29
+ // Appearance state (localStorage-based) — read directly on mount; this component is client-only
30
+ const [font, setFont] = useState(() => localStorage.getItem('prose-font') ?? 'lora');
31
+ const [contentWidth, setContentWidth] = useState(() => localStorage.getItem('content-width') ?? '780px');
32
+ const [dark, setDark] = useState(() => {
33
+ const stored = localStorage.getItem('theme');
34
+ return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
35
+ });
33
36
  // Plugin enabled state
34
37
  const [pluginStates, setPluginStates] = useState<Record<string, boolean>>({});
35
38
 
36
39
  useEffect(() => {
37
40
  if (!open) return;
38
41
  apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('load-error'));
39
- setFont(localStorage.getItem('prose-font') ?? 'lora');
40
- setContentWidth(localStorage.getItem('content-width') ?? '780px');
41
- const stored = localStorage.getItem('theme');
42
- setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
43
42
  loadDisabledState();
44
43
  const initial: Record<string, boolean> = {};
45
44
  for (const r of getAllRenderers()) initial[r.id] = isRendererEnabled(r.id);
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import SidebarLayout from './SidebarLayout';
5
+ import { FileNode } from '@/lib/types';
6
+
7
+ interface ShellLayoutProps {
8
+ fileTree: FileNode[];
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ export default function ShellLayout({ fileTree, children }: ShellLayoutProps) {
13
+ const pathname = usePathname();
14
+ if (pathname === '/login') return <>{children}</>;
15
+ return <SidebarLayout fileTree={fileTree}>{children}</SidebarLayout>;
16
+ }
@@ -1,25 +1,27 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useSyncExternalStore } from 'react';
4
4
  import { Sun, Moon } from 'lucide-react';
5
5
 
6
6
  export default function ThemeToggle() {
7
- const [dark, setDark] = useState(true);
8
-
9
- useEffect(() => {
10
- const stored = localStorage.getItem('theme');
11
- const isDark = stored
12
- ? stored === 'dark'
13
- : window.matchMedia('(prefers-color-scheme: dark)').matches;
14
- setDark(isDark);
15
- document.documentElement.classList.toggle('dark', isDark);
16
- }, []);
7
+ const dark = useSyncExternalStore(
8
+ (onStoreChange) => {
9
+ const listener = () => onStoreChange();
10
+ window.addEventListener('mindos-theme-change', listener);
11
+ return () => window.removeEventListener('mindos-theme-change', listener);
12
+ },
13
+ () => {
14
+ const stored = localStorage.getItem('theme');
15
+ return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
16
+ },
17
+ () => true,
18
+ );
17
19
 
18
20
  const toggle = () => {
19
21
  const next = !dark;
20
- setDark(next);
21
22
  document.documentElement.classList.toggle('dark', next);
22
23
  localStorage.setItem('theme', next ? 'dark' : 'light');
24
+ window.dispatchEvent(new Event('mindos-theme-change'));
23
25
  };
24
26
 
25
27
  return (
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useState, useSyncExternalStore } from 'react';
4
4
  import { Copy, Check, RefreshCw, Trash2 } from 'lucide-react';
5
5
  import type { SettingsData } from './types';
6
6
  import { Field, Input, EnvBadge, SectionLabel } from './Primitives';
@@ -16,6 +16,12 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
16
16
  const env = data.envOverrides ?? {};
17
17
  const k = t.settings.knowledge;
18
18
 
19
+ const origin = useSyncExternalStore(
20
+ () => () => {},
21
+ () => `${window.location.protocol}//${window.location.hostname}`,
22
+ () => 'http://localhost',
23
+ );
24
+
19
25
  const [showPassword, setShowPassword] = useState(false);
20
26
  const isPasswordMasked = data.webPassword === '***set***';
21
27
 
@@ -118,7 +124,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
118
124
  {k.authTokenMcpPort}: <code className="font-mono">{data.mcpPort}</code>
119
125
  {displayToken && (
120
126
  <> &nbsp;·&nbsp; MCP URL: <code className="font-mono select-all">
121
- {typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:${data.mcpPort}/mcp` : `http://localhost:${data.mcpPort}/mcp`}
127
+ {`${origin}:${data.mcpPort}/mcp`}
122
128
  </code></>
123
129
  )}
124
130
  </p>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
3
+ import { createContext, useContext, useSyncExternalStore, ReactNode } from 'react';
4
4
  import { Locale, messages, Messages } from './i18n';
5
5
 
6
6
  interface LocaleContextValue {
@@ -15,17 +15,25 @@ const LocaleContext = createContext<LocaleContextValue>({
15
15
  t: messages['en'],
16
16
  });
17
17
 
18
- export function LocaleProvider({ children }: { children: ReactNode }) {
19
- const [locale, setLocaleState] = useState<Locale>('en');
18
+ function getLocaleSnapshot(): Locale {
19
+ const saved = localStorage.getItem('locale');
20
+ return saved === 'zh' ? 'zh' : 'en';
21
+ }
20
22
 
21
- useEffect(() => {
22
- const saved = localStorage.getItem('locale') as Locale | null;
23
- if (saved === 'zh' || saved === 'en') setLocaleState(saved);
24
- }, []);
23
+ export function LocaleProvider({ children }: { children: ReactNode }) {
24
+ const locale = useSyncExternalStore(
25
+ (onStoreChange) => {
26
+ const listener = () => onStoreChange();
27
+ window.addEventListener('mindos-locale-change', listener);
28
+ return () => window.removeEventListener('mindos-locale-change', listener);
29
+ },
30
+ getLocaleSnapshot,
31
+ () => 'en' as Locale,
32
+ );
25
33
 
26
34
  const setLocale = (l: Locale) => {
27
- setLocaleState(l);
28
35
  localStorage.setItem('locale', l);
36
+ window.dispatchEvent(new Event('mindos-locale-change'));
29
37
  };
30
38
 
31
39
  return (
package/bin/cli.js CHANGED
@@ -131,6 +131,21 @@ function ensureAppDeps() {
131
131
  // next (and other deps) must be resolvable from app/ for Turbopack to work.
132
132
  const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
133
133
  if (!existsSync(appNext)) {
134
+ // Check npm is accessible before trying to run it.
135
+ try {
136
+ execSync('npm --version', { stdio: 'pipe' });
137
+ } catch {
138
+ console.error(red('\n✘ npm not found in PATH.\n'));
139
+ console.error(' MindOS needs npm to install its app dependencies on first run.');
140
+ console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
141
+ console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
142
+ console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
143
+ console.error(' Example:');
144
+ console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
145
+ console.error(dim(' source ~/.profile\n'));
146
+ console.error(' Then run `mindos start` again.\n');
147
+ process.exit(1);
148
+ }
134
149
  console.log(yellow('Installing app dependencies (first run)...\n'));
135
150
  // --no-workspaces: prevent npm from hoisting deps to monorepo root.
136
151
  // When globally installed, deps must live in app/node_modules/ so that
@@ -397,6 +412,27 @@ async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
397
412
  return check();
398
413
  }
399
414
 
415
+ async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
416
+ process.stdout.write(cyan(` Waiting for ${label} to be ready`));
417
+ for (let i = 0; i < retries; i++) {
418
+ try {
419
+ const { request } = await import('node:http');
420
+ const ok = await new Promise((resolve) => {
421
+ const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
422
+ (res) => { res.resume(); resolve(res.statusCode < 500); });
423
+ req.on('error', () => resolve(false));
424
+ req.on('timeout', () => { req.destroy(); resolve(false); });
425
+ req.end();
426
+ });
427
+ if (ok) { process.stdout.write(` ${green('✔')}\n`); return true; }
428
+ } catch { /* not ready yet */ }
429
+ process.stdout.write('.');
430
+ await new Promise(r => setTimeout(r, intervalMs));
431
+ }
432
+ process.stdout.write(` ${red('✘')}\n`);
433
+ return false;
434
+ }
435
+
400
436
  async function runGatewayCommand(sub) {
401
437
  const platform = getPlatform();
402
438
  if (!platform) {
@@ -551,6 +587,14 @@ const commands = {
551
587
  console.log(cyan(`Installing MindOS as a background service (${platform})...`));
552
588
  await runGatewayCommand('install');
553
589
  await runGatewayCommand('start');
590
+ console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
591
+ console.log(dim(' Follow live progress with: mindos logs\n'));
592
+ const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
593
+ if (!ready) {
594
+ console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
595
+ console.error(dim(' Check logs with: mindos logs\n'));
596
+ process.exit(1);
597
+ }
554
598
  printStartupInfo(webPort, mcpPort);
555
599
  console.log(`${green('✔ MindOS is running as a background service')}`);
556
600
  console.log(dim(' View logs: mindos logs'));
@@ -686,6 +730,17 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
686
730
  ok(`Node.js ${nodeVersion}`);
687
731
  }
688
732
 
733
+ // 4b. npm reachable from /bin/sh
734
+ try {
735
+ const npmVersion = execSync('npm --version', { stdio: 'pipe' }).toString().trim();
736
+ ok(`npm ${npmVersion} reachable`);
737
+ } catch {
738
+ err('npm not found in PATH — app dependencies cannot be installed');
739
+ console.log(dim(' Node.js may be installed via nvm/fnm/volta and not visible to /bin/sh.'));
740
+ console.log(dim(' Fix: add your Node.js bin path to ~/.profile so non-interactive shells can find it.'));
741
+ hasError = true;
742
+ }
743
+
689
744
  // 5. Build
690
745
  if (!existsSync(resolve(ROOT, 'app', '.next'))) {
691
746
  warn(`App not built yet — will build automatically on next ${dim('mindos start')}`);
@@ -736,7 +791,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
736
791
  },
737
792
 
738
793
  // ── update ─────────────────────────────────────────────────────────────────
739
- update: () => {
794
+ update: async () => {
740
795
  const currentVersion = (() => {
741
796
  try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
742
797
  })();
@@ -754,9 +809,35 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
754
809
  })();
755
810
  if (newVersion !== currentVersion) {
756
811
  console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
757
- console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
758
812
  } else {
759
813
  console.log(`\n${green('✔ Already on the latest version')} ${dim(`(${currentVersion})`)}\n`);
814
+ return;
815
+ }
816
+
817
+ // If daemon is running, restart it so the new version takes effect immediately
818
+ const platform = getPlatform();
819
+ let daemonRunning = false;
820
+ if (platform === 'systemd') {
821
+ try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
822
+ } else if (platform === 'launchd') {
823
+ try { execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' }); daemonRunning = true; } catch {}
824
+ }
825
+
826
+ if (daemonRunning) {
827
+ console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
828
+ await runGatewayCommand('stop');
829
+ await runGatewayCommand('start');
830
+ const webPort = process.env.MINDOS_WEB_PORT || '3000';
831
+ console.log(dim(' (Waiting for Web UI to come back up...)'));
832
+ const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
833
+ if (ready) {
834
+ console.log(green('✔ MindOS restarted and ready.\n'));
835
+ } else {
836
+ console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
837
+ process.exit(1);
838
+ }
839
+ } else {
840
+ console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
760
841
  }
761
842
  },
762
843
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",