@geminilight/mindos 0.5.43 → 0.5.45

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.
@@ -153,8 +153,8 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
153
153
  { id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
154
154
  { id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
155
155
  { id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
156
- { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
157
156
  { id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
157
+ { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
158
158
  { id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} />, badge: hasUpdate },
159
159
  ];
160
160
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
- import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink } from 'lucide-react';
4
+ import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink, Circle } from 'lucide-react';
5
5
  import { apiFetch } from '@/lib/api';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
 
@@ -11,18 +11,53 @@ interface UpdateInfo {
11
11
  hasUpdate: boolean;
12
12
  }
13
13
 
14
+ interface StageInfo {
15
+ id: string;
16
+ status: 'pending' | 'running' | 'done' | 'failed';
17
+ }
18
+
19
+ interface UpdateStatus {
20
+ stage: string;
21
+ stages: StageInfo[];
22
+ error: string | null;
23
+ version: { from: string | null; to: string | null } | null;
24
+ startedAt: string | null;
25
+ }
26
+
14
27
  type UpdateState = 'idle' | 'checking' | 'updating' | 'updated' | 'error' | 'timeout';
15
28
 
16
29
  const CHANGELOG_URL = 'https://github.com/GeminiLight/MindOS/releases';
17
- const POLL_INTERVAL = 5_000;
18
- const POLL_TIMEOUT = 4 * 60 * 1000; // 4 minutes
30
+ const POLL_INTERVAL = 3_000;
31
+ const POLL_TIMEOUT = 5 * 60 * 1000; // 5 minutes
32
+
33
+ const STAGE_LABELS: Record<string, { en: string; zh: string }> = {
34
+ downloading: { en: 'Downloading update', zh: '下载更新' },
35
+ skills: { en: 'Updating skills', zh: '更新 Skills' },
36
+ rebuilding: { en: 'Rebuilding app', zh: '重新构建应用' },
37
+ restarting: { en: 'Restarting server', zh: '重启服务' },
38
+ };
39
+
40
+ function StageIcon({ status }: { status: string }) {
41
+ switch (status) {
42
+ case 'done':
43
+ return <CheckCircle2 size={14} className="text-success shrink-0" />;
44
+ case 'running':
45
+ return <Loader2 size={14} className="animate-spin shrink-0" style={{ color: 'var(--amber)' }} />;
46
+ case 'failed':
47
+ return <AlertCircle size={14} className="text-destructive shrink-0" />;
48
+ default:
49
+ return <Circle size={14} className="text-muted-foreground/40 shrink-0" />;
50
+ }
51
+ }
19
52
 
20
53
  export function UpdateTab() {
21
- const { t } = useLocale();
54
+ const { t, locale } = useLocale();
22
55
  const u = t.settings.update;
23
56
  const [info, setInfo] = useState<UpdateInfo | null>(null);
24
57
  const [state, setState] = useState<UpdateState>('idle');
25
58
  const [errorMsg, setErrorMsg] = useState('');
59
+ const [stages, setStages] = useState<StageInfo[]>([]);
60
+ const [updateError, setUpdateError] = useState<string | null>(null);
26
61
  const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
27
62
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
28
63
  const originalVersion = useRef<string>('');
@@ -43,16 +78,23 @@ export function UpdateTab() {
43
78
 
44
79
  useEffect(() => { checkUpdate(); }, [checkUpdate]);
45
80
 
46
- useEffect(() => {
47
- return () => {
48
- clearInterval(pollRef.current);
49
- clearTimeout(timeoutRef.current);
50
- };
81
+ const cleanup = useCallback(() => {
82
+ clearInterval(pollRef.current);
83
+ clearTimeout(timeoutRef.current);
51
84
  }, []);
52
85
 
86
+ useEffect(() => cleanup, [cleanup]);
87
+
53
88
  const handleUpdate = useCallback(async () => {
54
89
  setState('updating');
55
90
  setErrorMsg('');
91
+ setUpdateError(null);
92
+ setStages([
93
+ { id: 'downloading', status: 'pending' },
94
+ { id: 'skills', status: 'pending' },
95
+ { id: 'rebuilding', status: 'pending' },
96
+ { id: 'restarting', status: 'pending' },
97
+ ]);
56
98
 
57
99
  try {
58
100
  await apiFetch('/api/update', { method: 'POST' });
@@ -60,26 +102,67 @@ export function UpdateTab() {
60
102
  // Expected — server may die during update
61
103
  }
62
104
 
105
+ // Poll update-status for stage progress
63
106
  pollRef.current = setInterval(async () => {
107
+ // Try status endpoint first (may fail when server is restarting)
64
108
  try {
65
- const data = await apiFetch<UpdateInfo>('/api/update-check');
66
- if (data.current !== originalVersion.current) {
67
- clearInterval(pollRef.current);
68
- clearTimeout(timeoutRef.current);
69
- setInfo(data);
70
- setState('updated');
71
- setTimeout(() => window.location.reload(), 2000);
109
+ const status = await apiFetch<UpdateStatus>('/api/update-status', { timeout: 5000 });
110
+
111
+ if (status.stages?.length > 0) {
112
+ setStages(status.stages);
113
+ }
114
+
115
+ if (status.stage === 'failed') {
116
+ cleanup();
117
+ setUpdateError(status.error || 'Update failed');
118
+ setState('error');
119
+ return;
120
+ }
121
+
122
+ if (status.stage === 'done') {
123
+ // Verify version actually changed
124
+ try {
125
+ const data = await apiFetch<UpdateInfo>('/api/update-check');
126
+ if (data.current !== originalVersion.current) {
127
+ cleanup();
128
+ setInfo(data);
129
+ setState('updated');
130
+ setTimeout(() => window.location.reload(), 2000);
131
+ return;
132
+ }
133
+ } catch { /* new server may not be fully ready */ }
72
134
  }
73
135
  } catch {
74
- // Server still restarting
136
+ // Server restarting — also try update-check as fallback
137
+ try {
138
+ const data = await apiFetch<UpdateInfo>('/api/update-check', { timeout: 5000 });
139
+ if (data.current !== originalVersion.current) {
140
+ cleanup();
141
+ setStages(prev => prev.map(s => ({ ...s, status: 'done' as const })));
142
+ setInfo(data);
143
+ setState('updated');
144
+ setTimeout(() => window.location.reload(), 2000);
145
+ }
146
+ } catch {
147
+ // Both endpoints down — server still restarting
148
+ }
75
149
  }
76
150
  }, POLL_INTERVAL);
77
151
 
78
152
  timeoutRef.current = setTimeout(() => {
79
- clearInterval(pollRef.current);
153
+ cleanup();
80
154
  setState('timeout');
81
155
  }, POLL_TIMEOUT);
82
- }, []);
156
+ }, [cleanup]);
157
+
158
+ const handleRetry = useCallback(() => {
159
+ setUpdateError(null);
160
+ handleUpdate();
161
+ }, [handleUpdate]);
162
+
163
+ const lang = locale === 'zh' ? 'zh' : 'en';
164
+ const doneCount = stages.filter(s => s.status === 'done').length;
165
+ const progress = stages.length > 0 ? Math.round((doneCount / stages.length) * 100) : 0;
83
166
 
84
167
  return (
85
168
  <div className="space-y-5">
@@ -114,11 +197,27 @@ export function UpdateTab() {
114
197
  )}
115
198
 
116
199
  {state === 'updating' && (
117
- <div className="space-y-2">
118
- <div className="flex items-center gap-2 text-xs" style={{ color: 'var(--amber)' }}>
119
- <Loader2 size={13} className="animate-spin" />
120
- {u?.updating ?? 'Updating MindOS... The server will restart shortly.'}
200
+ <div className="space-y-3">
201
+ {/* Stage list */}
202
+ <div className="space-y-1.5">
203
+ {stages.map((s) => (
204
+ <div key={s.id} className="flex items-center gap-2 text-xs">
205
+ <StageIcon status={s.status} />
206
+ <span className={s.status === 'pending' ? 'text-muted-foreground/50' : s.status === 'running' ? 'text-foreground' : 'text-muted-foreground'}>
207
+ {STAGE_LABELS[s.id]?.[lang] ?? s.id}
208
+ </span>
209
+ </div>
210
+ ))}
211
+ </div>
212
+
213
+ {/* Progress bar */}
214
+ <div className="h-1 rounded-full bg-muted overflow-hidden">
215
+ <div
216
+ className="h-full rounded-full transition-all duration-500 ease-out"
217
+ style={{ width: `${Math.max(progress, 5)}%`, background: 'var(--amber)' }}
218
+ />
121
219
  </div>
220
+
122
221
  <p className="text-2xs text-muted-foreground">
123
222
  {u?.updatingHint ?? 'This may take 1–3 minutes. Do not close this page.'}
124
223
  </p>
@@ -133,21 +232,39 @@ export function UpdateTab() {
133
232
  )}
134
233
 
135
234
  {state === 'timeout' && (
136
- <div className="space-y-1">
235
+ <div className="space-y-2">
137
236
  <div className="flex items-center gap-2 text-xs text-amber-600 dark:text-amber-400">
138
237
  <AlertCircle size={13} />
139
238
  {u?.timeout ?? 'Update may still be in progress.'}
140
239
  </div>
141
240
  <p className="text-2xs text-muted-foreground">
142
- {u?.timeoutHint ?? 'Check your terminal:'} <code className="font-mono bg-muted px-1 py-0.5 rounded">mindos logs</code>
241
+ {u?.timeoutHint ?? 'The server may need more time to rebuild. Try refreshing.'}
143
242
  </p>
243
+ <button
244
+ onClick={() => window.location.reload()}
245
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
246
+ >
247
+ <RefreshCw size={12} />
248
+ {u?.refreshButton ?? 'Refresh Page'}
249
+ </button>
144
250
  </div>
145
251
  )}
146
252
 
147
253
  {state === 'error' && (
148
- <div className="flex items-center gap-2 text-xs text-destructive">
149
- <AlertCircle size={13} />
150
- {errorMsg}
254
+ <div className="space-y-2">
255
+ <div className="flex items-center gap-2 text-xs text-destructive">
256
+ <AlertCircle size={13} />
257
+ {updateError || errorMsg}
258
+ </div>
259
+ {updateError && (
260
+ <button
261
+ onClick={handleRetry}
262
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
263
+ >
264
+ <RefreshCw size={12} />
265
+ {u?.retryButton ?? 'Retry Update'}
266
+ </button>
267
+ )}
151
268
  </div>
152
269
  )}
153
270
  </div>
@@ -60,6 +60,18 @@ async function removeSession(id: string): Promise<void> {
60
60
  }
61
61
  }
62
62
 
63
+ async function removeSessions(ids: string[]): Promise<void> {
64
+ try {
65
+ await fetch('/api/ask-sessions', {
66
+ method: 'DELETE',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ ids }),
69
+ });
70
+ } catch {
71
+ // ignore persistence errors
72
+ }
73
+ }
74
+
63
75
  export function useAskSession(currentFile?: string) {
64
76
  const [messages, setMessages] = useState<Message[]>([]);
65
77
  const [sessions, setSessions] = useState<ChatSession[]>([]);
@@ -166,6 +178,17 @@ export function useAskSession(currentFile?: string) {
166
178
  [activeSessionId, currentFile, sessions],
167
179
  );
168
180
 
181
+ const clearAllSessions = useCallback(() => {
182
+ const allIds = sessions.map(s => s.id);
183
+ void removeSessions(allIds);
184
+
185
+ const fresh = createSession(currentFile);
186
+ setActiveSessionId(fresh.id);
187
+ setMessages([]);
188
+ setSessions([fresh]);
189
+ void upsertSession(fresh);
190
+ }, [currentFile, sessions]);
191
+
169
192
  return {
170
193
  messages,
171
194
  setMessages,
@@ -177,5 +200,6 @@ export function useAskSession(currentFile?: string) {
177
200
  resetSession,
178
201
  loadSession,
179
202
  deleteSession,
203
+ clearAllSessions,
180
204
  };
181
205
  }
@@ -25,6 +25,9 @@ export const en = {
25
25
  spaceLocation: 'Location',
26
26
  rootLevel: 'Root',
27
27
  optional: 'optional',
28
+ aiInit: 'AI initialize content',
29
+ aiInitHint: 'AI will generate README and INSTRUCTION for this space',
30
+ aiInitNoKey: 'Configure an API key in Settings → AI to enable',
28
31
  createSpace: 'Create',
29
32
  cancelCreate: 'Cancel',
30
33
  continueEditing: 'Continue editing',
@@ -108,6 +111,10 @@ export const en = {
108
111
  'What are the key points?',
109
112
  'Find related notes on this topic',
110
113
  ],
114
+ sessionHistory: 'Session History',
115
+ clearAll: 'Clear all',
116
+ confirmClear: 'Confirm clear?',
117
+ noSessions: 'No saved sessions.',
111
118
  },
112
119
  panels: {
113
120
  agents: {
@@ -161,6 +168,8 @@ export const en = {
161
168
  pluginMarketDesc: 'Extend how files are rendered and edited',
162
169
  skillMarket: 'Skill Market',
163
170
  skillMarketDesc: 'Add new abilities to your AI agents',
171
+ spaceTemplates: 'Space Templates',
172
+ spaceTemplatesDesc: 'Pre-built space structures for common workflows',
164
173
  comingSoon: 'Coming soon',
165
174
  viewAll: 'View all use cases',
166
175
  tryIt: 'Try',
@@ -439,7 +448,9 @@ export const en = {
439
448
  updatingHint: 'This may take 1–3 minutes. Do not close this page.',
440
449
  updated: 'Updated successfully! Reloading...',
441
450
  timeout: 'Update may still be in progress.',
442
- timeoutHint: 'Check your terminal:',
451
+ timeoutHint: 'The server may need more time to rebuild. Try refreshing.',
452
+ refreshButton: 'Refresh Page',
453
+ retryButton: 'Retry Update',
443
454
  error: 'Failed to check for updates. Check your network connection.',
444
455
  checkButton: 'Check for Updates',
445
456
  updateButton: (version: string) => `Update to v${version}`,
@@ -666,12 +677,22 @@ export const en = {
666
677
  subtitle: 'Discover what you can do with MindOS — pick a scenario and try it now.',
667
678
  tryIt: 'Try it',
668
679
  categories: {
669
- 'getting-started': 'Getting Started',
670
- 'cross-agent': 'Cross-Agent',
671
- 'knowledge-evolution': 'Knowledge Evolution',
680
+ 'knowledge-management': 'Knowledge Management',
681
+ 'memory-sync': 'Memory Sync',
682
+ 'auto-execute': 'Auto Execute',
683
+ 'experience-evolution': 'Experience Evolution',
684
+ 'human-insights': 'Human Insights',
685
+ 'audit-control': 'Audit & Control',
686
+ },
687
+ scenarios: {
688
+ 'first-day': 'First Day',
689
+ 'daily': 'Daily Work',
690
+ 'project': 'Project Work',
672
691
  'advanced': 'Advanced',
673
692
  },
674
693
  all: 'All',
694
+ byCapability: 'By Capability',
695
+ byScenario: 'By Scenario',
675
696
  c1: {
676
697
  title: 'Inject Your Identity',
677
698
  desc: 'Tell all AI agents who you are — preferences, tech stack, communication style — in one shot.',
@@ -50,6 +50,9 @@ export const zh = {
50
50
  spaceLocation: '位置',
51
51
  rootLevel: '根目录',
52
52
  optional: '可选',
53
+ aiInit: 'AI 初始化内容',
54
+ aiInitHint: 'AI 将为此空间生成 README 和 INSTRUCTION',
55
+ aiInitNoKey: '在 设置 → AI 中配置 API 密钥以启用',
53
56
  createSpace: '创建',
54
57
  cancelCreate: '取消',
55
58
  continueEditing: '继续编辑',
@@ -133,6 +136,10 @@ export const zh = {
133
136
  '这篇文档的核心要点是什么?',
134
137
  '查找与这个主题相关的笔记',
135
138
  ],
139
+ sessionHistory: '对话历史',
140
+ clearAll: '清除全部',
141
+ confirmClear: '确认清除?',
142
+ noSessions: '暂无历史对话。',
136
143
  },
137
144
  panels: {
138
145
  agents: {
@@ -186,6 +193,8 @@ export const zh = {
186
193
  pluginMarketDesc: '扩展文件的渲染和编辑方式',
187
194
  skillMarket: '技能市场',
188
195
  skillMarketDesc: '为 AI 智能体添加新能力',
196
+ spaceTemplates: '空间模板',
197
+ spaceTemplatesDesc: '预设的空间结构,适用于常见工作流场景',
189
198
  comingSoon: '即将推出',
190
199
  viewAll: '查看所有使用案例',
191
200
  tryIt: '试试',
@@ -464,7 +473,9 @@ export const zh = {
464
473
  updatingHint: '预计 1–3 分钟,请勿关闭此页面。',
465
474
  updated: '更新成功!正在刷新...',
466
475
  timeout: '更新可能仍在进行中。',
467
- timeoutHint: '请在终端查看:',
476
+ timeoutHint: '服务器可能需要更多时间重新构建,请尝试刷新页面。',
477
+ refreshButton: '刷新页面',
478
+ retryButton: '重试更新',
468
479
  error: '检查更新失败,请检查网络连接。',
469
480
  checkButton: '检查更新',
470
481
  updateButton: (version: string) => `更新到 v${version}`,
@@ -691,12 +702,22 @@ export const zh = {
691
702
  subtitle: '发现 MindOS 能帮你做什么 — 选一个场景,立即体验。',
692
703
  tryIt: '试一试',
693
704
  categories: {
694
- 'getting-started': '快速上手',
695
- 'cross-agent': '跨 Agent',
696
- 'knowledge-evolution': '知识演进',
705
+ 'knowledge-management': '知识管理',
706
+ 'memory-sync': '记忆同步',
707
+ 'auto-execute': '自动执行',
708
+ 'experience-evolution': '经验进化',
709
+ 'human-insights': '人类洞察',
710
+ 'audit-control': '审计纠错',
711
+ },
712
+ scenarios: {
713
+ 'first-day': '初次使用',
714
+ 'daily': '日常工作',
715
+ 'project': '项目协作',
697
716
  'advanced': '高级',
698
717
  },
699
718
  all: '全部',
719
+ byCapability: '按能力',
720
+ byScenario: '按场景',
700
721
  c1: {
701
722
  title: '注入身份',
702
723
  desc: '让所有 AI Agent 一次认识你 — 偏好、技术栈、沟通风格。',
package/app/lib/utils.ts CHANGED
@@ -32,3 +32,14 @@ export function relativeTime(mtime: number, labels: {
32
32
  if (days < 7) return labels.daysAgo(days);
33
33
  return new Date(mtime).toLocaleDateString();
34
34
  }
35
+
36
+ /** Extract leading emoji from a string, e.g. "📝 Notes" → "📝" */
37
+ export function extractEmoji(name: string): string {
38
+ const match = name.match(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+/u);
39
+ return match?.[0] ?? '';
40
+ }
41
+
42
+ /** Strip leading emoji+space from a string, e.g. "📝 Notes" → "Notes" */
43
+ export function stripEmoji(name: string): string {
44
+ return name.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '') || name;
45
+ }
package/bin/cli.js CHANGED
@@ -38,7 +38,7 @@
38
38
  * mindos config validate — validate config file
39
39
  */
40
40
 
41
- import { execSync } from 'node:child_process';
41
+ import { execSync, spawn as nodeSpawn } from 'node:child_process';
42
42
  import { existsSync, readFileSync, writeFileSync, rmSync, cpSync } from 'node:fs';
43
43
  import { resolve } from 'node:path';
44
44
  import { homedir } from 'node:os';
@@ -697,32 +697,41 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
697
697
 
698
698
  // ── update ─────────────────────────────────────────────────────────────────
699
699
  update: async () => {
700
+ const { writeUpdateStatus, writeUpdateFailed, clearUpdateStatus } = await import('./lib/update-status.js');
700
701
  const currentVersion = (() => {
701
702
  try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
702
703
  })();
703
704
  console.log(`\n${bold('⬆ Updating MindOS...')} ${dim(`(current: ${currentVersion})`)}\n`);
705
+
706
+ // Stage 1: Download
707
+ writeUpdateStatus('downloading', { fromVersion: currentVersion });
704
708
  try {
705
709
  execSync('npm install -g @geminilight/mindos@latest', { stdio: 'inherit' });
706
710
  } catch {
711
+ writeUpdateFailed('downloading', 'npm install failed', { fromVersion: currentVersion });
707
712
  console.error(red('Update failed. Try: npm install -g @geminilight/mindos@latest'));
708
713
  process.exit(1);
709
714
  }
710
715
  if (existsSync(BUILD_STAMP)) rmSync(BUILD_STAMP);
711
716
 
712
- // Silently update installed skills to match the new package
717
+ // Resolve the new installation path (after npm install -g, ROOT is stale)
718
+ const updatedRoot = getUpdatedRoot();
719
+ const newVersion = (() => {
720
+ try { return JSON.parse(readFileSync(resolve(updatedRoot, 'package.json'), 'utf-8')).version; } catch { return '?'; }
721
+ })();
722
+ const vOpts = { fromVersion: currentVersion, toVersion: newVersion };
723
+
724
+ // Stage 2: Skills
725
+ writeUpdateStatus('skills', vOpts);
713
726
  try {
714
- const newRoot = getUpdatedRoot();
715
727
  const { checkSkillVersions, updateSkill } = await import('./lib/skill-check.js');
716
- const mismatches = checkSkillVersions(newRoot);
728
+ const mismatches = checkSkillVersions(updatedRoot);
717
729
  for (const m of mismatches) {
718
730
  updateSkill(m.bundledPath, m.installPath);
719
731
  console.log(` ${green('✓')} ${dim(`Skill ${m.name}: v${m.installed} → v${m.bundled}`)}`);
720
732
  }
721
733
  } catch { /* best-effort */ }
722
734
 
723
- const newVersion = (() => {
724
- try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
725
- })();
726
735
  if (newVersion !== currentVersion) {
727
736
  console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
728
737
  } else {
@@ -746,10 +755,12 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
746
755
  console.log(cyan('\n Daemon is running — stopping to apply the new version...'));
747
756
  await runGatewayCommand('stop');
748
757
 
749
- // After npm install -g, resolve the new installation path and pre-build
750
- const newRoot = getUpdatedRoot();
751
- buildIfNeeded(newRoot);
758
+ // Stage 3: Rebuild
759
+ writeUpdateStatus('rebuilding', vOpts);
760
+ buildIfNeeded(updatedRoot);
752
761
 
762
+ // Stage 4: Restart
763
+ writeUpdateStatus('restarting', vOpts);
753
764
  await runGatewayCommand('install');
754
765
  // install() starts the service:
755
766
  // - systemd: daemon-reload + enable + start
@@ -772,17 +783,78 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
772
783
  console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
773
784
  console.log(`\n ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}`);
774
785
  console.log(`${'─'.repeat(53)}\n`);
786
+ writeUpdateStatus('done', vOpts);
775
787
  } else {
788
+ writeUpdateFailed('restarting', 'Server did not come back up in time', vOpts);
776
789
  console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
777
790
  process.exit(1);
778
791
  }
779
792
  } else {
780
- // Non-daemon mode: build in foreground for better UX
781
- buildIfNeeded(getUpdatedRoot());
793
+ // Non-daemon mode: check if a MindOS instance is currently running
794
+ // (e.g. user started via `mindos start`, or GUI triggered this update).
795
+ // If so, stop it and restart from the NEW installation path.
796
+ const updateConfig = (() => {
797
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
798
+ })();
799
+ const webPort = Number(updateConfig.port ?? 3456);
800
+ const mcpPort = Number(updateConfig.mcpPort ?? 8781);
801
+
802
+ const wasRunning = await isPortInUse(webPort) || await isPortInUse(mcpPort);
803
+
804
+ if (wasRunning) {
805
+ console.log(cyan('\n MindOS is running — restarting to apply the new version...'));
806
+ stopMindos();
807
+ // Wait for ports to free (up to 15s)
808
+ const deadline = Date.now() + 15_000;
809
+ while (Date.now() < deadline) {
810
+ const busy = await isPortInUse(webPort) || await isPortInUse(mcpPort);
811
+ if (!busy) break;
812
+ await new Promise((r) => setTimeout(r, 500));
813
+ }
782
814
 
783
- console.log(`\n${green('✔')} ${bold(`Updated: ${currentVersion} → ${newVersion}`)}`);
784
- console.log(dim(' Run `mindos start` to start the updated version.'));
785
- console.log(` ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}\n`);
815
+ // Stage 3: Rebuild
816
+ writeUpdateStatus('rebuilding', vOpts);
817
+ buildIfNeeded(updatedRoot);
818
+
819
+ // Stage 4: Restart
820
+ writeUpdateStatus('restarting', vOpts);
821
+ const newCliPath = resolve(updatedRoot, 'bin', 'cli.js');
822
+ const childEnv = { ...process.env };
823
+ delete childEnv.MINDOS_WEB_PORT;
824
+ delete childEnv.MINDOS_MCP_PORT;
825
+ delete childEnv.MIND_ROOT;
826
+ delete childEnv.AUTH_TOKEN;
827
+ delete childEnv.WEB_PASSWORD;
828
+ const child = nodeSpawn(
829
+ process.execPath, [newCliPath, 'start'],
830
+ { detached: true, stdio: 'ignore', env: childEnv },
831
+ );
832
+ child.unref();
833
+
834
+ console.log(dim(' (Waiting for Web UI to come back up...)'));
835
+ const ready = await waitForHttp(webPort, { retries: 120, intervalMs: 2000, label: 'Web UI', logFile: LOG_PATH });
836
+ if (ready) {
837
+ const localIP = getLocalIP();
838
+ console.log(`\n${'─'.repeat(53)}`);
839
+ console.log(`${green('✔')} ${bold(`MindOS updated: ${currentVersion} → ${newVersion}`)}\n`);
840
+ console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
841
+ if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
842
+ console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
843
+ console.log(`\n ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}`);
844
+ console.log(`${'─'.repeat(53)}\n`);
845
+ writeUpdateStatus('done', vOpts);
846
+ } else {
847
+ writeUpdateFailed('restarting', 'Server did not come back up in time', vOpts);
848
+ console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
849
+ process.exit(1);
850
+ }
851
+ } else {
852
+ // No running instance — just build and tell user to start manually
853
+ buildIfNeeded(updatedRoot);
854
+ console.log(`\n${green('✔')} ${bold(`Updated: ${currentVersion} → ${newVersion}`)}`);
855
+ console.log(dim(' Run `mindos start` to start the updated version.'));
856
+ console.log(` ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}\n`);
857
+ }
786
858
  }
787
859
  },
788
860
 
@@ -5,6 +5,7 @@ import { bold, dim, cyan, green, yellow } from './colors.js';
5
5
  import { getSyncStatus } from './sync.js';
6
6
  import { checkForUpdate, printUpdateHint } from './update-check.js';
7
7
  import { runSkillCheck } from './skill-check.js';
8
+ import { clearUpdateStatus } from './update-status.js';
8
9
 
9
10
  export function getLocalIP() {
10
11
  try {
@@ -18,6 +19,9 @@ export function getLocalIP() {
18
19
  }
19
20
 
20
21
  export async function printStartupInfo(webPort, mcpPort) {
22
+ // Clear stale update status from previous update cycles
23
+ clearUpdateStatus();
24
+
21
25
  // Fire update check immediately (non-blocking)
22
26
  const updatePromise = checkForUpdate().catch(() => null);
23
27