@geminilight/mindos 0.5.14 → 0.5.16

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/cli.js CHANGED
@@ -29,6 +29,7 @@
29
29
  * mindos gateway status — show service status
30
30
  * mindos gateway logs — tail service logs
31
31
  * mindos doctor — health check (config, ports, build, daemon)
32
+ * mindos uninstall — fully uninstall MindOS (stop, remove daemon, npm uninstall)
32
33
  * mindos update — update to latest version
33
34
  * mindos logs — tail service logs (~/.mindos/mindos.log)
34
35
  * mindos config show — print current config (API keys masked)
@@ -42,7 +43,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
42
43
  import { resolve } from 'node:path';
43
44
  import { homedir } from 'node:os';
44
45
 
45
- import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH } from './lib/constants.js';
46
+ import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH, MINDOS_DIR } from './lib/constants.js';
46
47
  import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
47
48
  import { run } from './lib/utils.js';
48
49
  import { loadConfig, getStartMode, isDaemonMode } from './lib/config.js';
@@ -242,7 +243,7 @@ const commands = {
242
243
  // causing a port-conflict race condition with KeepAlive restart loops.
243
244
  console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
244
245
  console.log(dim(' Follow live progress with: mindos logs\n'));
245
- const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
246
+ const ready = await waitForHttp(Number(webPort), { retries: 60, intervalMs: 2000, label: 'Web UI' });
246
247
  if (!ready) {
247
248
  console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
248
249
  console.error(dim(' Check logs with: mindos logs\n'));
@@ -604,31 +605,178 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
604
605
  console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
605
606
  await runGatewayCommand('stop');
606
607
  await runGatewayCommand('install');
607
- // Note: install() already starts the service via launchctl bootstrap + RunAtLoad=true.
608
- // Do NOT call start() here — kickstart -k would kill the just-started process,
609
- // causing a port-conflict race condition with KeepAlive restart loops.
610
- const webPort = (() => {
611
- try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).port ?? 3456; } catch { return 3456; }
608
+ // install() starts the service:
609
+ // - systemd: daemon-reload + enable + start
610
+ // - launchd: bootstrap (RunAtLoad=true auto-starts)
611
+ // Do NOT call start() again — on macOS kickstart -k would kill the
612
+ // just-started process, causing a port-conflict race with KeepAlive.
613
+ const updateConfig = (() => {
614
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
612
615
  })();
616
+ const webPort = updateConfig.port ?? 3456;
617
+ const mcpPort = updateConfig.mcpPort ?? 8781;
613
618
  console.log(dim(' (Waiting for Web UI to come back up — first run after update includes a rebuild...)'));
614
- const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
619
+ const ready = await waitForHttp(Number(webPort), { retries: 60, intervalMs: 2000, label: 'Web UI' });
615
620
  if (ready) {
616
- console.log(green('✔ MindOS restarted and ready.\n'));
621
+ const localIP = getLocalIP();
622
+ console.log(`\n${'─'.repeat(53)}`);
623
+ console.log(`${green('✔')} ${bold(`MindOS updated: ${currentVersion} → ${newVersion}`)}\n`);
624
+ console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
625
+ if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
626
+ console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
627
+ console.log(`\n ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}`);
628
+ console.log(`${'─'.repeat(53)}\n`);
617
629
  } else {
618
630
  console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
619
631
  process.exit(1);
620
632
  }
621
633
  } else {
622
- console.log(dim(' Run `mindos start` it will rebuild automatically.\n'));
634
+ console.log(`\n${green('✔')} ${bold(`Updated: ${currentVersion} ${newVersion}`)}`);
635
+ console.log(dim(' Run `mindos start` — it will rebuild automatically.'));
636
+ console.log(` ${dim('View changelog:')} ${cyan('https://github.com/GeminiLight/MindOS/releases')}\n`);
623
637
  }
624
638
  },
625
639
 
640
+ // ── uninstall ──────────────────────────────────────────────────────────────
641
+ uninstall: async () => {
642
+ const readline = await import('node:readline');
643
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
644
+
645
+ // Buffer lines eagerly — readline.question() loses buffered lines when
646
+ // piped stdin delivers multiple lines at once (Node.js known behavior).
647
+ const lineBuffer = [];
648
+ let lineResolve = null;
649
+ rl.on('line', (line) => {
650
+ if (lineResolve) {
651
+ const r = lineResolve;
652
+ lineResolve = null;
653
+ r(line);
654
+ } else {
655
+ lineBuffer.push(line);
656
+ }
657
+ });
658
+ // On EOF with no pending resolve, close gracefully
659
+ rl.on('close', () => {
660
+ if (lineResolve) { lineResolve(''); lineResolve = null; }
661
+ });
662
+
663
+ function prompt(question) {
664
+ process.stdout.write(question + ' ');
665
+ if (lineBuffer.length > 0) return Promise.resolve(lineBuffer.shift());
666
+ return new Promise((resolve) => { lineResolve = resolve; });
667
+ }
668
+
669
+ async function confirm(question) {
670
+ const a = (await prompt(question + ' [y/N]')).trim().toLowerCase();
671
+ return a === 'y' || a === 'yes';
672
+ }
673
+
674
+ async function askInput(question) {
675
+ return (await prompt(question)).trim();
676
+ }
677
+
678
+ async function askPassword(question) {
679
+ // Mute echoed keystrokes
680
+ const stdout = process.stdout;
681
+ const origWrite = stdout.write.bind(stdout);
682
+ stdout.write = (chunk, ...args) => {
683
+ // Suppress everything except the prompt itself
684
+ if (typeof chunk === 'string' && chunk.includes(question)) return origWrite(chunk, ...args);
685
+ return true;
686
+ };
687
+ const answer = await prompt(question);
688
+ stdout.write = origWrite;
689
+ console.log(); // newline after hidden input
690
+ return answer.trim();
691
+ }
692
+
693
+ const done = () => rl.close();
694
+
695
+ console.log(`\n${bold('🗑 MindOS Uninstall')}\n`);
696
+ console.log(' This will:');
697
+ console.log(` ${green('✓')} Stop running MindOS processes`);
698
+ console.log(` ${green('✓')} Remove background service (if installed)`);
699
+ console.log(` ${green('✓')} Uninstall npm package\n`);
700
+
701
+ if (!await confirm('Proceed?')) {
702
+ console.log(dim('\n Aborted.\n'));
703
+ done();
704
+ return;
705
+ }
706
+
707
+ // 1. Stop processes
708
+ console.log(`\n${cyan('Stopping MindOS...')}`);
709
+ try { stopMindos(); } catch { /* may not be running */ }
710
+
711
+ // 2. Remove daemon (skip if platform unsupported)
712
+ if (getPlatform()) {
713
+ try {
714
+ await runGatewayCommand('uninstall');
715
+ } catch {
716
+ // Daemon may not be installed — that's fine
717
+ }
718
+ }
719
+
720
+ // Read config before potentially deleting ~/.mindos/
721
+ let config = {};
722
+ try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
723
+ const mindRoot = config.mindRoot?.replace(/^~/, homedir());
724
+
725
+ // 3. Ask to remove ~/.mindos/
726
+ if (existsSync(MINDOS_DIR)) {
727
+ if (await confirm(`Remove config directory (${dim(MINDOS_DIR)})?`)) {
728
+ rmSync(MINDOS_DIR, { recursive: true, force: true });
729
+ console.log(`${green('✔')} Removed ${dim(MINDOS_DIR)}`);
730
+ } else {
731
+ console.log(dim(` Kept ${MINDOS_DIR}`));
732
+ }
733
+ }
734
+
735
+ // 4. Ask to remove knowledge base (triple protection: confirm → type YES → password)
736
+ if (mindRoot && existsSync(mindRoot)) {
737
+ if (await confirm(`Remove knowledge base (${dim(mindRoot)})?`)) {
738
+ const typed = await askInput(`${yellow('⚠ This is irreversible.')} Type ${bold('YES')} to confirm:`);
739
+ if (typed === 'YES') {
740
+ const webPassword = config.webPassword;
741
+ let authorized = true;
742
+ if (webPassword) {
743
+ const pw = await askPassword('Enter web password:');
744
+ if (pw !== webPassword) {
745
+ console.log(red(' Wrong password. Knowledge base kept.'));
746
+ authorized = false;
747
+ }
748
+ }
749
+ if (authorized) {
750
+ rmSync(mindRoot, { recursive: true, force: true });
751
+ console.log(`${green('✔')} Removed ${dim(mindRoot)}`);
752
+ }
753
+ } else {
754
+ console.log(dim(' Knowledge base kept.'));
755
+ }
756
+ } else {
757
+ console.log(dim(` Kept ${mindRoot}`));
758
+ }
759
+ }
760
+
761
+ // 5. npm uninstall -g
762
+ console.log(`\n${cyan('Uninstalling npm package...')}`);
763
+ try {
764
+ execSync('npm uninstall -g @geminilight/mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
765
+ } catch {
766
+ console.log(yellow(' npm uninstall failed — you may need to run manually:'));
767
+ console.log(dim(' npm uninstall -g @geminilight/mindos'));
768
+ }
769
+
770
+ console.log(`\n${green('✔ MindOS uninstalled.')}\n`);
771
+ done();
772
+ },
773
+
626
774
  // ── logs ───────────────────────────────────────────────────────────────────
627
775
  logs: () => {
628
776
  ensureMindosDir();
629
777
  if (!existsSync(LOG_PATH)) {
630
778
  console.log(dim(`No log file yet at ${LOG_PATH}`));
631
- console.log(dim('Logs are written when running in daemon mode (mindos start --daemon).'));
779
+ console.log(dim('Logs are created when starting MindOS (mindos start, mindos onboard, or daemon mode).'));
632
780
  process.exit(0);
633
781
  }
634
782
  const noFollow = process.argv.includes('--no-follow');
@@ -932,6 +1080,7 @@ ${bold('Config & Diagnostics:')}
932
1080
  ${row('mindos config <subcommand>', 'View/update config (show/validate/set/unset)')}
933
1081
  ${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
934
1082
  ${row('mindos update', 'Update MindOS to the latest version')}
1083
+ ${row('mindos uninstall', 'Fully uninstall MindOS (stop, remove daemon, npm uninstall)')}
935
1084
  ${row('mindos logs', 'Tail service logs (~/.mindos/mindos.log)')}
936
1085
  ${row('mindos', 'Start using mode saved in ~/.mindos/config.json')}
937
1086
  `);
package/bin/lib/build.js CHANGED
@@ -38,14 +38,15 @@ export function cleanNextDir() {
38
38
  }
39
39
 
40
40
  function depsHash() {
41
- // Use package.json (not package-lock.json) so we don't need to ship the
42
- // 560kB lock file in the npm tarball. package.json changes whenever
43
- // dependencies are added/removed/bumped, which is the only case that
44
- // requires a fresh `npm install`.
41
+ // Hash both package.json and package-lock.json (if present) so that
42
+ // changes to either trigger a fresh npm install.
45
43
  const pkgPath = resolve(ROOT, 'app', 'package.json');
44
+ const lockPath = resolve(ROOT, 'app', 'package-lock.json');
46
45
  try {
47
- const content = readFileSync(pkgPath);
48
- return createHash('sha256').update(content).digest('hex').slice(0, 16);
46
+ const h = createHash('sha256');
47
+ h.update(readFileSync(pkgPath));
48
+ try { h.update(readFileSync(lockPath)); } catch {}
49
+ return h.digest('hex').slice(0, 16);
49
50
  } catch {
50
51
  return null;
51
52
  }
@@ -50,8 +50,19 @@ export async function waitForPortFree(port, { retries = 30, intervalMs = 500 } =
50
50
  return !(await isPortInUse(port));
51
51
  }
52
52
 
53
- export async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
54
- process.stdout.write(cyan(` Waiting for ${label} to be ready`));
53
+ export async function waitForHttp(port, { retries = 60, intervalMs = 2000, label = 'service' } = {}) {
54
+ const start = Date.now();
55
+ const elapsed = () => {
56
+ const s = Math.round((Date.now() - start) / 1000);
57
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`;
58
+ };
59
+ const phases = [
60
+ { after: 0, msg: 'installing dependencies' },
61
+ { after: 15, msg: 'building app' },
62
+ { after: 60, msg: 'still building (first run takes a while)' },
63
+ ];
64
+ let currentPhase = -1;
65
+
55
66
  for (let i = 0; i < retries; i++) {
56
67
  try {
57
68
  const { request } = await import('node:http');
@@ -62,12 +73,30 @@ export async function waitForHttp(port, { retries = 120, intervalMs = 2000, labe
62
73
  req.on('timeout', () => { req.destroy(); resolve(false); });
63
74
  req.end();
64
75
  });
65
- if (ok) { process.stdout.write(` ${green('\u2714')}\n`); return true; }
76
+ if (ok) {
77
+ // Clear line and print success
78
+ process.stdout.write(`\r\x1b[K`);
79
+ process.stdout.write(` ${green('\u2714')} ${label} ready ${dim(`(${elapsed()})`)}\n`);
80
+ return true;
81
+ }
66
82
  } catch { /* not ready yet */ }
67
- process.stdout.write('.');
83
+
84
+ // Update phase hint
85
+ const secs = (Date.now() - start) / 1000;
86
+ const nextPhase = phases.reduce((idx, p, i) => secs >= p.after ? i : idx, -1);
87
+ if (nextPhase !== currentPhase) {
88
+ currentPhase = nextPhase;
89
+ }
90
+ const hint = currentPhase >= 0 ? phases[currentPhase].msg : '';
91
+
92
+ // Rewrite the status line in place
93
+ process.stdout.write(`\r\x1b[K`);
94
+ process.stdout.write(cyan(` ⏳ Waiting for ${label}`) + dim(` — ${hint} (${elapsed()})`));
95
+
68
96
  await new Promise(r => setTimeout(r, intervalMs));
69
97
  }
70
- process.stdout.write(` ${red('\u2718')}\n`);
98
+ process.stdout.write(`\r\x1b[K`);
99
+ process.stdout.write(` ${red('\u2718')} ${label} did not start ${dim(`(${elapsed()})`)}\n`);
71
100
  return false;
72
101
  }
73
102
 
@@ -107,14 +136,15 @@ const systemd = {
107
136
  ].join('\n');
108
137
  writeFileSync(SYSTEMD_UNIT, unit, 'utf-8');
109
138
  console.log(green(`\u2714 Wrote ${SYSTEMD_UNIT}`));
110
- execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
111
- execSync('systemctl --user enable mindos', { stdio: 'inherit' });
112
- console.log(green('\u2714 Service installed and enabled'));
139
+ execSync('systemctl --user daemon-reload', { stdio: ['ignore', 'inherit', 'inherit'] });
140
+ execSync('systemctl --user enable mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
141
+ execSync('systemctl --user start mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
142
+ console.log(green('\u2714 Service installed and started'));
113
143
  },
114
144
 
115
145
  async start() {
116
146
  rotateLogs();
117
- execSync('systemctl --user start mindos', { stdio: 'inherit' });
147
+ execSync('systemctl --user start mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
118
148
  const ok = await waitForService(() => {
119
149
  try {
120
150
  const out = execSync('systemctl --user is-active mindos', { encoding: 'utf-8' }).trim();
@@ -123,36 +153,36 @@ const systemd = {
123
153
  });
124
154
  if (!ok) {
125
155
  console.error(red('\n\u2718 Service failed to start. Last log output:'));
126
- try { execSync(`journalctl --user -u mindos -n 30 --no-pager`, { stdio: 'inherit' }); } catch {}
156
+ try { execSync(`journalctl --user -u mindos -n 30 --no-pager`, { stdio: ['ignore', 'inherit', 'inherit'] }); } catch {}
127
157
  process.exit(1);
128
158
  }
129
159
  console.log(green('\u2714 Service started'));
130
160
  },
131
161
 
132
162
  stop() {
133
- execSync('systemctl --user stop mindos', { stdio: 'inherit' });
163
+ execSync('systemctl --user stop mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
134
164
  console.log(green('\u2714 Service stopped'));
135
165
  },
136
166
 
137
167
  status() {
138
168
  try {
139
- execSync('systemctl --user status mindos', { stdio: 'inherit' });
169
+ execSync('systemctl --user status mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
140
170
  } catch { /* status exits non-zero when stopped */ }
141
171
  },
142
172
 
143
173
  logs() {
144
- execSync(`journalctl --user -u mindos -f`, { stdio: 'inherit' });
174
+ execSync(`journalctl --user -u mindos -f`, { stdio: ['ignore', 'inherit', 'inherit'] });
145
175
  },
146
176
 
147
177
  uninstall() {
148
178
  try {
149
- execSync('systemctl --user disable --now mindos', { stdio: 'inherit' });
179
+ execSync('systemctl --user disable --now mindos', { stdio: ['ignore', 'inherit', 'inherit'] });
150
180
  } catch { /* may already be stopped */ }
151
181
  if (existsSync(SYSTEMD_UNIT)) {
152
182
  rmSync(SYSTEMD_UNIT);
153
183
  console.log(green(`\u2714 Removed ${SYSTEMD_UNIT}`));
154
184
  }
155
- execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
185
+ execSync('systemctl --user daemon-reload', { stdio: ['ignore', 'inherit', 'inherit'] });
156
186
  console.log(green('\u2714 Service uninstalled'));
157
187
  },
158
188
  };
@@ -213,7 +243,7 @@ const launchd = {
213
243
 
214
244
  async start() {
215
245
  rotateLogs();
216
- execSync(`launchctl kickstart -k gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
246
+ execSync(`launchctl kickstart -k gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: ['ignore', 'inherit', 'inherit'] });
217
247
  const ok = await waitForService(() => {
218
248
  try {
219
249
  const out = execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { encoding: 'utf-8' });
@@ -222,7 +252,7 @@ const launchd = {
222
252
  });
223
253
  if (!ok) {
224
254
  console.error(red('\n\u2718 Service failed to start. Last log output:'));
225
- try { execSync(`tail -n 30 ${LOG_PATH}`, { stdio: 'inherit' }); } catch {}
255
+ try { execSync(`tail -n 30 ${LOG_PATH}`, { stdio: ['ignore', 'inherit', 'inherit'] }); } catch {}
226
256
  process.exit(1);
227
257
  }
228
258
  console.log(green('\u2714 Service started'));
@@ -238,7 +268,7 @@ const launchd = {
238
268
  } catch {}
239
269
 
240
270
  try {
241
- execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
271
+ execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: ['ignore', 'inherit', 'inherit'] });
242
272
  } catch { /* may not be running */ }
243
273
 
244
274
  // launchctl bootout is async — wait for ports to actually be freed
@@ -264,19 +294,19 @@ const launchd = {
264
294
 
265
295
  status() {
266
296
  try {
267
- execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: 'inherit' });
297
+ execSync(`launchctl print gui/${launchctlUid()}/${LAUNCHD_LABEL}`, { stdio: ['ignore', 'inherit', 'inherit'] });
268
298
  } catch {
269
299
  console.log(dim('Service is not running'));
270
300
  }
271
301
  },
272
302
 
273
303
  logs() {
274
- execSync(`tail -f ${LOG_PATH}`, { stdio: 'inherit' });
304
+ execSync(`tail -f ${LOG_PATH}`, { stdio: ['ignore', 'inherit', 'inherit'] });
275
305
  },
276
306
 
277
307
  uninstall() {
278
308
  try {
279
- execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: 'inherit' });
309
+ execSync(`launchctl bootout gui/${launchctlUid()} ${LAUNCHD_PLIST}`, { stdio: ['ignore', 'inherit', 'inherit'] });
280
310
  } catch { /* may not be running */ }
281
311
  if (existsSync(LAUNCHD_PLIST)) {
282
312
  rmSync(LAUNCHD_PLIST);
package/bin/lib/stop.js CHANGED
@@ -110,8 +110,8 @@ export function stopMindos(opts = {}) {
110
110
 
111
111
  if (!pids.length && portKilled === 0) {
112
112
  // Last resort: pattern match (for envs without lsof)
113
- try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
114
- try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
113
+ try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: ['ignore', 'inherit', 'inherit'] }); } catch {}
114
+ try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: ['ignore', 'inherit', 'inherit'] }); } catch {}
115
115
  }
116
116
 
117
117
  if (!pids.length) console.log(green('\u2714 Done'));
package/mcp/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "zod": "^3.23.8"
12
12
  },
13
13
  "devDependencies": {
14
- "@types/node": "^25.4.0",
14
+ "@types/node": "^22",
15
15
  "tsx": "^4.19.0",
16
16
  "typescript": "^5"
17
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
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",
@@ -57,8 +57,7 @@
57
57
  "!assets/images",
58
58
  "!mcp/node_modules",
59
59
  "!mcp/dist",
60
- "!mcp/package-lock.json",
61
- "!app/package-lock.json"
60
+ "!mcp/package-lock.json"
62
61
  ],
63
62
  "scripts": {
64
63
  "setup": "node scripts/setup.js",
package/scripts/setup.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * Enter confirms
21
21
  */
22
22
 
23
- import { existsSync, cpSync, writeFileSync, readFileSync, mkdirSync, createWriteStream, rmSync } from 'node:fs';
23
+ import { existsSync, cpSync, writeFileSync, readFileSync, mkdirSync, createWriteStream, rmSync, openSync } from 'node:fs';
24
24
  import { resolve, dirname, join } from 'node:path';
25
25
  import { homedir, tmpdir, networkInterfaces } from 'node:os';
26
26
  import { fileURLToPath } from 'node:url';
@@ -36,6 +36,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
36
36
  const ROOT = resolve(__dirname, '..');
37
37
  const MINDOS_DIR = resolve(homedir(), '.mindos');
38
38
  const CONFIG_PATH = resolve(MINDOS_DIR, 'config.json');
39
+ const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');
39
40
 
40
41
  // ── i18n ─────────────────────────────────────────────────────────────────────
41
42
 
@@ -886,19 +887,34 @@ async function startGuiSetup() {
886
887
  // Pass MINDOS_WEB_PORT (not PORT) so loadConfig() won't override with the
887
888
  // config file port — this is critical when we need a temporary port.
888
889
  const cliPath = resolve(__dirname, '../bin/cli.js');
890
+ const logFd = openSync(LOG_PATH, 'a');
889
891
  const child = spawn(process.execPath, [cliPath, 'start'], {
890
892
  detached: true,
891
- stdio: 'ignore',
893
+ stdio: ['ignore', logFd, logFd],
892
894
  env: { ...process.env, MINDOS_WEB_PORT: String(usePort) },
893
895
  });
894
896
  child.unref();
895
897
 
896
- // Wait for the server to be ready
898
+ // First-time install hint
899
+ if (isFirstTime) {
900
+ write(c.dim(' First run: installing dependencies and building app (may take a few minutes)...\n'));
901
+ }
902
+
903
+ // Wait for the server to be ready (120s timeout)
897
904
  const { waitForHttp } = await import('../bin/lib/gateway.js');
898
- const ready = await waitForHttp(usePort, { retries: 60, intervalMs: 1000, label: 'MindOS' });
905
+ const ready = await waitForHttp(usePort, { retries: 120, intervalMs: 1000, label: 'MindOS' });
899
906
 
900
907
  if (!ready) {
901
908
  write(c.red('\n✘ Server failed to start.\n'));
909
+ if (existsSync(LOG_PATH)) {
910
+ write(c.dim(`\n Last log output (${LOG_PATH}):\n`));
911
+ try {
912
+ const lines = readFileSync(LOG_PATH, 'utf-8').trim().split('\n').slice(-15);
913
+ for (const line of lines) write(c.dim(` ${line}\n`));
914
+ } catch {}
915
+ }
916
+ write(c.dim(`\n Full logs: mindos logs\n`));
917
+ write(c.dim(` Manual start: mindos start\n\n`));
902
918
  process.exit(1);
903
919
  }
904
920