@geminilight/mindos 0.1.2 → 0.1.4
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/app/app/layout.tsx +5 -11
- package/app/app/view/[...path]/ViewPageClient.tsx +1 -1
- package/app/components/DirView.tsx +14 -8
- package/app/components/ShellLayout.tsx +16 -0
- package/app/components/ThemeToggle.tsx +14 -12
- package/app/components/settings/KnowledgeTab.tsx +8 -2
- package/app/lib/LocaleContext.tsx +16 -8
- package/bin/cli.js +98 -4
- package/package.json +1 -1
package/app/app/layout.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</SidebarLayout>
|
|
96
|
-
)}
|
|
88
|
+
<ShellLayout fileTree={fileTree}>
|
|
89
|
+
{children}
|
|
90
|
+
</ShellLayout>
|
|
97
91
|
</ErrorBoundary>
|
|
98
92
|
</TooltipProvider>
|
|
99
93
|
</LocaleProvider>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
@@ -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 {
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
4
|
import { Sun, Moon } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
export default function ThemeToggle() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
<> · MCP URL: <code className="font-mono select-all">
|
|
121
|
-
{
|
|
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,
|
|
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
|
-
|
|
19
|
-
const
|
|
18
|
+
function getLocaleSnapshot(): Locale {
|
|
19
|
+
const saved = localStorage.getItem('locale');
|
|
20
|
+
return saved === 'zh' ? 'zh' : 'en';
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
@@ -225,6 +240,7 @@ const systemd = {
|
|
|
225
240
|
install() {
|
|
226
241
|
if (!existsSync(SYSTEMD_DIR)) mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
227
242
|
ensureMindosDir();
|
|
243
|
+
const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
|
|
228
244
|
const unit = [
|
|
229
245
|
'[Unit]',
|
|
230
246
|
'Description=MindOS app + MCP server',
|
|
@@ -236,6 +252,7 @@ const systemd = {
|
|
|
236
252
|
'Restart=on-failure',
|
|
237
253
|
'RestartSec=3',
|
|
238
254
|
`Environment=HOME=${homedir()}`,
|
|
255
|
+
`Environment=PATH=${currentPath}`,
|
|
239
256
|
`EnvironmentFile=-${resolve(MINDOS_DIR, 'env')}`,
|
|
240
257
|
`StandardOutput=append:${LOG_PATH}`,
|
|
241
258
|
`StandardError=append:${LOG_PATH}`,
|
|
@@ -309,6 +326,9 @@ const launchd = {
|
|
|
309
326
|
install() {
|
|
310
327
|
if (!existsSync(LAUNCHD_DIR)) mkdirSync(LAUNCHD_DIR, { recursive: true });
|
|
311
328
|
ensureMindosDir();
|
|
329
|
+
// Capture current PATH so the daemon can find npm/node even when launched by
|
|
330
|
+
// launchd (which only sets a minimal PATH and doesn't source shell profiles).
|
|
331
|
+
const currentPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
|
|
312
332
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
313
333
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
314
334
|
<plist version="1.0">
|
|
@@ -327,6 +347,7 @@ const launchd = {
|
|
|
327
347
|
<key>EnvironmentVariables</key>
|
|
328
348
|
<dict>
|
|
329
349
|
<key>HOME</key><string>${homedir()}</string>
|
|
350
|
+
<key>PATH</key><string>${currentPath}</string>
|
|
330
351
|
</dict>
|
|
331
352
|
</dict>
|
|
332
353
|
</plist>
|
|
@@ -334,8 +355,15 @@ const launchd = {
|
|
|
334
355
|
writeFileSync(LAUNCHD_PLIST, plist, 'utf-8');
|
|
335
356
|
console.log(green(`✔ Wrote ${LAUNCHD_PLIST}`));
|
|
336
357
|
try {
|
|
337
|
-
execSync(`launchctl bootstrap gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: '
|
|
338
|
-
} catch {
|
|
358
|
+
execSync(`launchctl bootstrap gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'pipe' });
|
|
359
|
+
} catch (e) {
|
|
360
|
+
const msg = e.stderr?.toString() ?? e.message ?? '';
|
|
361
|
+
// Error 5 (ENOENT / already loaded) is benign — service is already bootstrapped
|
|
362
|
+
if (!msg.includes('5:') && !msg.includes('already')) {
|
|
363
|
+
console.error(yellow(` ⚠ launchctl bootstrap: ${msg.trim()}`));
|
|
364
|
+
console.error(dim(' If this persists, try: launchctl bootout gui/$(id -u)/com.mindos.app'));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
339
367
|
console.log(green('✔ Service installed'));
|
|
340
368
|
},
|
|
341
369
|
|
|
@@ -397,6 +425,27 @@ async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
|
|
|
397
425
|
return check();
|
|
398
426
|
}
|
|
399
427
|
|
|
428
|
+
async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
|
|
429
|
+
process.stdout.write(cyan(` Waiting for ${label} to be ready`));
|
|
430
|
+
for (let i = 0; i < retries; i++) {
|
|
431
|
+
try {
|
|
432
|
+
const { request } = await import('node:http');
|
|
433
|
+
const ok = await new Promise((resolve) => {
|
|
434
|
+
const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
|
|
435
|
+
(res) => { res.resume(); resolve(res.statusCode < 500); });
|
|
436
|
+
req.on('error', () => resolve(false));
|
|
437
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
438
|
+
req.end();
|
|
439
|
+
});
|
|
440
|
+
if (ok) { process.stdout.write(` ${green('✔')}\n`); return true; }
|
|
441
|
+
} catch { /* not ready yet */ }
|
|
442
|
+
process.stdout.write('.');
|
|
443
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
444
|
+
}
|
|
445
|
+
process.stdout.write(` ${red('✘')}\n`);
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
400
449
|
async function runGatewayCommand(sub) {
|
|
401
450
|
const platform = getPlatform();
|
|
402
451
|
if (!platform) {
|
|
@@ -551,6 +600,14 @@ const commands = {
|
|
|
551
600
|
console.log(cyan(`Installing MindOS as a background service (${platform})...`));
|
|
552
601
|
await runGatewayCommand('install');
|
|
553
602
|
await runGatewayCommand('start');
|
|
603
|
+
console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
|
|
604
|
+
console.log(dim(' Follow live progress with: mindos logs\n'));
|
|
605
|
+
const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
|
|
606
|
+
if (!ready) {
|
|
607
|
+
console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
|
|
608
|
+
console.error(dim(' Check logs with: mindos logs\n'));
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
554
611
|
printStartupInfo(webPort, mcpPort);
|
|
555
612
|
console.log(`${green('✔ MindOS is running as a background service')}`);
|
|
556
613
|
console.log(dim(' View logs: mindos logs'));
|
|
@@ -686,6 +743,17 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
686
743
|
ok(`Node.js ${nodeVersion}`);
|
|
687
744
|
}
|
|
688
745
|
|
|
746
|
+
// 4b. npm reachable from /bin/sh
|
|
747
|
+
try {
|
|
748
|
+
const npmVersion = execSync('npm --version', { stdio: 'pipe' }).toString().trim();
|
|
749
|
+
ok(`npm ${npmVersion} reachable`);
|
|
750
|
+
} catch {
|
|
751
|
+
err('npm not found in PATH — app dependencies cannot be installed');
|
|
752
|
+
console.log(dim(' Node.js may be installed via nvm/fnm/volta and not visible to /bin/sh.'));
|
|
753
|
+
console.log(dim(' Fix: add your Node.js bin path to ~/.profile so non-interactive shells can find it.'));
|
|
754
|
+
hasError = true;
|
|
755
|
+
}
|
|
756
|
+
|
|
689
757
|
// 5. Build
|
|
690
758
|
if (!existsSync(resolve(ROOT, 'app', '.next'))) {
|
|
691
759
|
warn(`App not built yet — will build automatically on next ${dim('mindos start')}`);
|
|
@@ -736,7 +804,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
736
804
|
},
|
|
737
805
|
|
|
738
806
|
// ── update ─────────────────────────────────────────────────────────────────
|
|
739
|
-
update: () => {
|
|
807
|
+
update: async () => {
|
|
740
808
|
const currentVersion = (() => {
|
|
741
809
|
try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
|
|
742
810
|
})();
|
|
@@ -754,9 +822,35 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
754
822
|
})();
|
|
755
823
|
if (newVersion !== currentVersion) {
|
|
756
824
|
console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
|
|
757
|
-
console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
|
|
758
825
|
} else {
|
|
759
826
|
console.log(`\n${green('✔ Already on the latest version')} ${dim(`(${currentVersion})`)}\n`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// If daemon is running, restart it so the new version takes effect immediately
|
|
831
|
+
const platform = getPlatform();
|
|
832
|
+
let daemonRunning = false;
|
|
833
|
+
if (platform === 'systemd') {
|
|
834
|
+
try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
835
|
+
} else if (platform === 'launchd') {
|
|
836
|
+
try { execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (daemonRunning) {
|
|
840
|
+
console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
|
|
841
|
+
await runGatewayCommand('stop');
|
|
842
|
+
await runGatewayCommand('start');
|
|
843
|
+
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
844
|
+
console.log(dim(' (Waiting for Web UI to come back up...)'));
|
|
845
|
+
const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
|
|
846
|
+
if (ready) {
|
|
847
|
+
console.log(green('✔ MindOS restarted and ready.\n'));
|
|
848
|
+
} else {
|
|
849
|
+
console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
|
|
760
854
|
}
|
|
761
855
|
},
|
|
762
856
|
|
package/package.json
CHANGED