@geminilight/mindos 0.2.1 → 0.4.0
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/api/init/route.ts +7 -41
- package/app/app/api/mcp/agents/route.ts +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/settings/route.ts +3 -0
- package/app/app/api/setup/generate-token/route.ts +23 -0
- package/app/app/api/setup/route.ts +81 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +22 -2
- package/app/app/setup/page.tsx +9 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +479 -0
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +270 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +15 -1
- package/app/lib/template.ts +45 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +117 -1
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
package/bin/lib/config.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { CONFIG_PATH } from './constants.js';
|
|
3
3
|
|
|
4
|
+
let loaded = false;
|
|
5
|
+
|
|
4
6
|
export function loadConfig() {
|
|
7
|
+
if (loaded) return;
|
|
8
|
+
loaded = true;
|
|
5
9
|
if (!existsSync(CONFIG_PATH)) return;
|
|
6
10
|
let config;
|
|
7
11
|
try {
|
package/bin/lib/constants.js
CHANGED
|
@@ -11,3 +11,5 @@ export const MINDOS_DIR = resolve(homedir(), '.mindos');
|
|
|
11
11
|
export const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');
|
|
12
12
|
export const CLI_PATH = resolve(__dirname, '..', 'cli.js');
|
|
13
13
|
export const NODE_BIN = process.execPath;
|
|
14
|
+
export const UPDATE_CHECK_PATH = resolve(MINDOS_DIR, 'update-check.json');
|
|
15
|
+
export const DEPS_STAMP = resolve(MINDOS_DIR, 'deps-hash');
|
package/bin/lib/debug.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dim } from './colors.js';
|
|
2
|
+
|
|
3
|
+
const enabled = process.env.MINDOS_DEBUG === '1' || process.argv.includes('--verbose');
|
|
4
|
+
|
|
5
|
+
export function debug(...args) {
|
|
6
|
+
if (enabled) {
|
|
7
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
8
|
+
console.error(dim(`[${ts}]`), ...args);
|
|
9
|
+
}
|
|
10
|
+
}
|
package/bin/lib/startup.js
CHANGED
|
@@ -3,6 +3,7 @@ import { networkInterfaces } from 'node:os';
|
|
|
3
3
|
import { CONFIG_PATH } from './constants.js';
|
|
4
4
|
import { bold, dim, cyan, green, yellow } from './colors.js';
|
|
5
5
|
import { getSyncStatus } from './sync.js';
|
|
6
|
+
import { checkForUpdate, printUpdateHint } from './update-check.js';
|
|
6
7
|
|
|
7
8
|
export function getLocalIP() {
|
|
8
9
|
try {
|
|
@@ -15,39 +16,32 @@ export function getLocalIP() {
|
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function printStartupInfo(webPort, mcpPort) {
|
|
19
|
+
export async function printStartupInfo(webPort, mcpPort) {
|
|
20
|
+
// Fire update check immediately (non-blocking)
|
|
21
|
+
const updatePromise = checkForUpdate().catch(() => null);
|
|
22
|
+
|
|
19
23
|
let config = {};
|
|
20
24
|
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
|
|
21
25
|
const authToken = config.authToken || '';
|
|
22
26
|
const localIP = getLocalIP();
|
|
23
27
|
|
|
24
|
-
const auth = authToken
|
|
25
|
-
? `,\n "headers": { "Authorization": "Bearer ${authToken}" }`
|
|
26
|
-
: '';
|
|
27
|
-
const block = (host) =>
|
|
28
|
-
` {\n "mcpServers": {\n "mindos": {\n "url": "http://${host}:${mcpPort}/mcp"${auth}\n }\n }\n }`;
|
|
29
|
-
|
|
30
28
|
console.log(`\n${'─'.repeat(53)}`);
|
|
31
29
|
console.log(`${bold('🧠 MindOS is starting')}\n`);
|
|
32
30
|
console.log(` ${green('●')} Web UI ${cyan(`http://localhost:${webPort}`)}`);
|
|
33
31
|
if (localIP) console.log(` ${cyan(`http://${localIP}:${webPort}`)}`);
|
|
34
32
|
console.log(` ${green('●')} MCP ${cyan(`http://localhost:${mcpPort}/mcp`)}`);
|
|
35
33
|
if (localIP) console.log(` ${cyan(`http://${localIP}:${mcpPort}/mcp`)}`);
|
|
36
|
-
|
|
37
|
-
console.log();
|
|
38
|
-
console.log(bold('Configure MCP in your Agent:'));
|
|
39
|
-
console.log(dim(' Local (same machine):'));
|
|
40
|
-
console.log(block('localhost'));
|
|
41
|
-
if (localIP) {
|
|
42
|
-
console.log(dim('\n Remote (other device):'));
|
|
43
|
-
console.log(block(localIP));
|
|
44
|
-
}
|
|
34
|
+
|
|
45
35
|
if (authToken) {
|
|
46
|
-
|
|
47
|
-
console.log(dim('
|
|
36
|
+
const maskedToken = authToken.length > 8 ? authToken.slice(0, 8) + '····' : (authToken.length > 4 ? authToken.slice(0, 4) + '····' : '····');
|
|
37
|
+
console.log(` ${green('●')} Auth ${cyan(maskedToken)} ${dim('(run `mindos token` for full config)')}`);
|
|
48
38
|
}
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
|
|
40
|
+
// MCP quick-connect hint
|
|
41
|
+
console.log(`\n ${dim('Quick connect:')} ${cyan('mindos mcp install claude-code -g -y')}`);
|
|
42
|
+
console.log(` ${dim('Full config:')} ${cyan('mindos token')}`);
|
|
43
|
+
|
|
44
|
+
if (localIP) console.log(dim(`\n 💡 Remote? SSH port forwarding: ssh -L ${webPort}:localhost:${webPort} -L ${mcpPort}:localhost:${mcpPort} user@${localIP}`));
|
|
51
45
|
|
|
52
46
|
// Sync status
|
|
53
47
|
const mindRoot = config.mindRoot;
|
|
@@ -70,5 +64,12 @@ export function printStartupInfo(webPort, mcpPort) {
|
|
|
70
64
|
} catch { /* sync check is best-effort */ }
|
|
71
65
|
}
|
|
72
66
|
|
|
67
|
+
// Wait for update check result (max 4s, then give up)
|
|
68
|
+
const latestVersion = await Promise.race([
|
|
69
|
+
updatePromise,
|
|
70
|
+
new Promise(r => setTimeout(() => r(null), 4000)),
|
|
71
|
+
]);
|
|
72
|
+
if (latestVersion) printUpdateHint(latestVersion);
|
|
73
|
+
|
|
73
74
|
console.log(`${'─'.repeat(53)}\n`);
|
|
74
75
|
}
|
package/bin/lib/stop.js
CHANGED
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { green, yellow, dim } from './colors.js';
|
|
3
4
|
import { loadPids, clearPids } from './pid.js';
|
|
5
|
+
import { CONFIG_PATH } from './constants.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Kill processes listening on the given port.
|
|
9
|
+
* Returns number of processes killed.
|
|
10
|
+
*/
|
|
11
|
+
function killByPort(port) {
|
|
12
|
+
let killed = 0;
|
|
13
|
+
try {
|
|
14
|
+
const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
15
|
+
if (output) {
|
|
16
|
+
for (const p of output.split('\n')) {
|
|
17
|
+
const pid = Number(p);
|
|
18
|
+
if (pid > 0) {
|
|
19
|
+
try { process.kill(pid, 'SIGTERM'); killed++; } catch {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// lsof not available or no processes found
|
|
25
|
+
}
|
|
26
|
+
return killed;
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
export function stopMindos() {
|
|
6
30
|
const pids = loadPids();
|
|
7
31
|
if (!pids.length) {
|
|
8
|
-
console.log(yellow('No PID file found, trying
|
|
9
|
-
|
|
10
|
-
|
|
32
|
+
console.log(yellow('No PID file found, trying port-based stop...'));
|
|
33
|
+
// Read ports from config
|
|
34
|
+
let webPort = '3000', mcpPort = '8787';
|
|
35
|
+
try {
|
|
36
|
+
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
37
|
+
if (config.port) webPort = String(config.port);
|
|
38
|
+
if (config.mcpPort) mcpPort = String(config.mcpPort);
|
|
39
|
+
} catch {}
|
|
40
|
+
let stopped = 0;
|
|
41
|
+
for (const port of [webPort, mcpPort]) {
|
|
42
|
+
stopped += killByPort(port);
|
|
43
|
+
}
|
|
44
|
+
if (stopped === 0) {
|
|
45
|
+
// Fallback: pkill pattern match (for envs without lsof)
|
|
46
|
+
try { execSync('pkill -f "next start|next dev" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
47
|
+
try { execSync('pkill -f "mcp/src/index" 2>/dev/null || true', { stdio: 'inherit' }); } catch {}
|
|
48
|
+
}
|
|
11
49
|
console.log(green('\u2714 Done'));
|
|
12
50
|
return;
|
|
13
51
|
}
|
package/bin/lib/sync.js
CHANGED
|
@@ -139,52 +139,66 @@ let activePullInterval = null;
|
|
|
139
139
|
/**
|
|
140
140
|
* Interactive sync init — configure remote git repo
|
|
141
141
|
*/
|
|
142
|
-
export async function initSync(mindRoot) {
|
|
142
|
+
export async function initSync(mindRoot, opts = {}) {
|
|
143
143
|
if (!mindRoot) { console.error(red('No mindRoot configured.')); process.exit(1); }
|
|
144
144
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const nonInteractive = opts.nonInteractive || false;
|
|
146
|
+
let remoteUrl = opts.remote || '';
|
|
147
|
+
let token = opts.token || '';
|
|
148
|
+
let branch = opts.branch || 'main';
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
150
|
+
if (nonInteractive) {
|
|
151
|
+
// Non-interactive mode: all params from opts
|
|
152
|
+
if (!remoteUrl) {
|
|
153
|
+
throw new Error('Remote URL is required in non-interactive mode');
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Interactive mode: prompt user
|
|
157
|
+
const readline = await import('node:readline');
|
|
158
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
159
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
160
|
+
|
|
161
|
+
// 2. Remote URL
|
|
162
|
+
const currentRemote = getRemoteUrl(mindRoot);
|
|
163
|
+
const defaultUrl = currentRemote || '';
|
|
164
|
+
const urlPrompt = currentRemote
|
|
165
|
+
? `${bold('Remote URL')} ${dim(`[${currentRemote}]`)}: `
|
|
166
|
+
: `${bold('Remote URL')} ${dim('(HTTPS or SSH)')}: `;
|
|
167
|
+
remoteUrl = (await ask(urlPrompt)).trim() || defaultUrl;
|
|
168
|
+
|
|
169
|
+
if (!remoteUrl) {
|
|
170
|
+
console.error(red('Remote URL is required.'));
|
|
171
|
+
rl.close();
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
155
174
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
? `${bold('Remote URL')} ${dim(`[${currentRemote}]`)}: `
|
|
161
|
-
: `${bold('Remote URL')} ${dim('(HTTPS or SSH)')}: `;
|
|
162
|
-
let remoteUrl = (await ask(urlPrompt)).trim() || defaultUrl;
|
|
175
|
+
// 3. Token for HTTPS
|
|
176
|
+
if (remoteUrl.startsWith('https://')) {
|
|
177
|
+
token = (await ask(`${bold('Access Token')} ${dim('(GitHub PAT / GitLab PAT, leave empty if SSH)')}: `)).trim();
|
|
178
|
+
}
|
|
163
179
|
|
|
164
|
-
if (!remoteUrl) {
|
|
165
|
-
console.error(red('Remote URL is required.'));
|
|
166
180
|
rl.close();
|
|
167
|
-
process.exit(1);
|
|
168
181
|
}
|
|
169
182
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
// 1. Ensure git repo
|
|
184
|
+
if (!isGitRepo(mindRoot)) {
|
|
185
|
+
if (!nonInteractive) console.log(dim('Initializing git repository...'));
|
|
186
|
+
execSync('git init', { cwd: mindRoot, stdio: 'pipe' });
|
|
187
|
+
try { execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle token for HTTPS
|
|
191
|
+
if (token && remoteUrl.startsWith('https://')) {
|
|
192
|
+
const urlObj = new URL(remoteUrl);
|
|
193
|
+
urlObj.username = 'oauth2';
|
|
194
|
+
urlObj.password = token;
|
|
195
|
+
// Configure credential helper
|
|
196
|
+
try { execSync(`git config credential.helper store`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
197
|
+
// Store the credential
|
|
198
|
+
try {
|
|
199
|
+
const credInput = `protocol=${urlObj.protocol.replace(':', '')}\nhost=${urlObj.host}\nusername=oauth2\npassword=${token}\n\n`;
|
|
200
|
+
execSync('git credential approve', { cwd: mindRoot, input: credInput, stdio: 'pipe' });
|
|
201
|
+
} catch {}
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
// 4. Set remote
|
|
@@ -195,50 +209,48 @@ export async function initSync(mindRoot) {
|
|
|
195
209
|
}
|
|
196
210
|
|
|
197
211
|
// 5. Test connection
|
|
198
|
-
console.log(dim('Testing connection...'));
|
|
212
|
+
if (!nonInteractive) console.log(dim('Testing connection...'));
|
|
199
213
|
try {
|
|
200
|
-
execSync('git ls-remote --exit-code origin', { cwd: mindRoot, stdio: 'pipe' });
|
|
201
|
-
console.log(green('✔ Connection successful'));
|
|
214
|
+
execSync('git ls-remote --exit-code origin', { cwd: mindRoot, stdio: 'pipe', timeout: 15000 });
|
|
215
|
+
if (!nonInteractive) console.log(green('✔ Connection successful'));
|
|
202
216
|
} catch {
|
|
217
|
+
const errMsg = 'Remote not reachable — check URL and credentials';
|
|
218
|
+
if (nonInteractive) throw new Error(errMsg);
|
|
203
219
|
console.error(red('✘ Could not connect to remote. Check your URL and credentials.'));
|
|
204
|
-
rl.close();
|
|
205
220
|
process.exit(1);
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
rl.close();
|
|
209
|
-
|
|
210
223
|
// 6. Save sync config
|
|
211
224
|
const syncConfig = {
|
|
212
225
|
enabled: true,
|
|
213
226
|
provider: 'git',
|
|
214
227
|
remote: 'origin',
|
|
215
|
-
branch: getBranch(mindRoot),
|
|
228
|
+
branch: branch || getBranch(mindRoot),
|
|
216
229
|
autoCommitInterval: 30,
|
|
217
230
|
autoPullInterval: 300,
|
|
218
231
|
};
|
|
219
232
|
saveSyncConfig(syncConfig);
|
|
220
|
-
console.log(green('✔ Sync configured'));
|
|
233
|
+
if (!nonInteractive) console.log(green('✔ Sync configured'));
|
|
221
234
|
|
|
222
235
|
// 7. First sync: pull if remote has content, push otherwise
|
|
223
236
|
try {
|
|
224
237
|
const refs = gitExec('git ls-remote --heads origin', mindRoot);
|
|
225
238
|
if (refs) {
|
|
226
|
-
console.log(dim('Pulling from remote...'));
|
|
239
|
+
if (!nonInteractive) console.log(dim('Pulling from remote...'));
|
|
227
240
|
try {
|
|
228
|
-
execSync(`git pull origin ${syncConfig.branch} --allow-unrelated-histories`, { cwd: mindRoot, stdio: 'inherit' });
|
|
241
|
+
execSync(`git pull origin ${syncConfig.branch} --allow-unrelated-histories`, { cwd: mindRoot, stdio: nonInteractive ? 'pipe' : 'inherit' });
|
|
229
242
|
} catch {
|
|
230
|
-
|
|
231
|
-
console.log(yellow('Pull completed with warnings. Check for conflicts.'));
|
|
243
|
+
if (!nonInteractive) console.log(yellow('Pull completed with warnings. Check for conflicts.'));
|
|
232
244
|
}
|
|
233
245
|
} else {
|
|
234
|
-
console.log(dim('Pushing to remote...'));
|
|
246
|
+
if (!nonInteractive) console.log(dim('Pushing to remote...'));
|
|
235
247
|
autoCommitAndPush(mindRoot);
|
|
236
248
|
}
|
|
237
249
|
} catch {
|
|
238
|
-
console.log(dim('Performing initial push...'));
|
|
250
|
+
if (!nonInteractive) console.log(dim('Performing initial push...'));
|
|
239
251
|
autoCommitAndPush(mindRoot);
|
|
240
252
|
}
|
|
241
|
-
console.log(green('✔ Initial sync complete\n'));
|
|
253
|
+
if (!nonInteractive) console.log(green('✔ Initial sync complete\n'));
|
|
242
254
|
}
|
|
243
255
|
|
|
244
256
|
/**
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { ROOT, UPDATE_CHECK_PATH } from './constants.js';
|
|
4
|
+
import { bold, dim, cyan, yellow } from './colors.js';
|
|
5
|
+
|
|
6
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
7
|
+
|
|
8
|
+
const REGISTRIES = [
|
|
9
|
+
'https://registry.npmmirror.com/@geminilight/mindos/latest',
|
|
10
|
+
'https://registry.npmjs.org/@geminilight/mindos/latest',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/** Simple semver "a > b" comparison (major.minor.patch only). */
|
|
14
|
+
function semverGt(a, b) {
|
|
15
|
+
const pa = a.split('.').map(Number);
|
|
16
|
+
const pb = b.split('.').map(Number);
|
|
17
|
+
for (let i = 0; i < 3; i++) {
|
|
18
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
19
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getCurrentVersion() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version;
|
|
27
|
+
} catch {
|
|
28
|
+
return '0.0.0';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readCache() {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(UPDATE_CHECK_PATH, 'utf-8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeCache(latestVersion) {
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(UPDATE_CHECK_PATH, JSON.stringify({
|
|
43
|
+
lastCheck: new Date().toISOString(),
|
|
44
|
+
latestVersion,
|
|
45
|
+
}), 'utf-8');
|
|
46
|
+
} catch { /* best-effort */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchLatest() {
|
|
50
|
+
for (const url of REGISTRIES) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
53
|
+
if (res.ok) {
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
return data.version;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check for updates. Returns the latest version string if an update is
|
|
66
|
+
* available, or null if up-to-date / check fails.
|
|
67
|
+
*/
|
|
68
|
+
export async function checkForUpdate() {
|
|
69
|
+
if (process.env.MINDOS_NO_UPDATE_CHECK === '1') return null;
|
|
70
|
+
|
|
71
|
+
const current = getCurrentVersion();
|
|
72
|
+
const cache = readCache();
|
|
73
|
+
|
|
74
|
+
// Cache hit — still fresh
|
|
75
|
+
if (cache?.lastCheck) {
|
|
76
|
+
const age = Date.now() - new Date(cache.lastCheck).getTime();
|
|
77
|
+
if (age < TTL_MS) {
|
|
78
|
+
return (cache.latestVersion && semverGt(cache.latestVersion, current))
|
|
79
|
+
? cache.latestVersion
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cache miss or expired — fetch
|
|
85
|
+
const latest = await fetchLatest();
|
|
86
|
+
if (latest) writeCache(latest);
|
|
87
|
+
return (latest && semverGt(latest, current)) ? latest : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Print update hint line if an update is available. */
|
|
91
|
+
export function printUpdateHint(latestVersion) {
|
|
92
|
+
const current = getCurrentVersion();
|
|
93
|
+
console.log(`\n ${yellow('⬆')} ${bold(`MindOS v${latestVersion}`)} available ${dim(`(current: v${current})`)}. Run ${cyan('mindos update')} to upgrade.`);
|
|
94
|
+
}
|
package/bin/lib/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scans app/components/renderers/*/manifest.ts and generates
|
|
3
|
+
// app/lib/renderers/index.ts with auto-discovered imports.
|
|
4
|
+
//
|
|
5
|
+
// Run: node scripts/gen-renderer-index.js
|
|
6
|
+
// Hooked into: npm run build (via prebuild)
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const renderersDir = path.resolve(__dirname, '../app/components/renderers');
|
|
14
|
+
const outputFile = path.resolve(__dirname, '../app/lib/renderers/index.ts');
|
|
15
|
+
|
|
16
|
+
// Scan for manifest.ts in immediate subdirectories
|
|
17
|
+
const dirs = fs.readdirSync(renderersDir, { withFileTypes: true })
|
|
18
|
+
.filter(d => d.isDirectory())
|
|
19
|
+
.filter(d => fs.existsSync(path.join(renderersDir, d.name, 'manifest.ts')))
|
|
20
|
+
.map(d => d.name)
|
|
21
|
+
.sort();
|
|
22
|
+
|
|
23
|
+
if (dirs.length === 0) {
|
|
24
|
+
console.error('No manifest.ts files found in', renderersDir);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// kebab-case → camelCase
|
|
29
|
+
function toCamel(s) {
|
|
30
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const imports = dirs.map(dir => {
|
|
34
|
+
const varName = toCamel(dir);
|
|
35
|
+
return `import { manifest as ${varName} } from '@/components/renderers/${dir}/manifest';`;
|
|
36
|
+
}).join('\n');
|
|
37
|
+
|
|
38
|
+
const varNames = dirs.map(toCamel);
|
|
39
|
+
|
|
40
|
+
const code = `/**
|
|
41
|
+
* AUTO-GENERATED by scripts/gen-renderer-index.js — do not edit manually.
|
|
42
|
+
* To regenerate: node scripts/gen-renderer-index.js
|
|
43
|
+
*/
|
|
44
|
+
import { registerRenderer } from './registry';
|
|
45
|
+
${imports}
|
|
46
|
+
|
|
47
|
+
const manifests = [
|
|
48
|
+
${varNames.join(', ')},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
for (const m of manifests) {
|
|
52
|
+
registerRenderer(m);
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
fs.writeFileSync(outputFile, code, 'utf-8');
|
|
57
|
+
console.log(`Generated ${path.relative(process.cwd(), outputFile)} with ${dirs.length} renderers: ${dirs.join(', ')}`);
|
package/scripts/setup.js
CHANGED
|
@@ -26,7 +26,7 @@ import { homedir, tmpdir, networkInterfaces } from 'node:os';
|
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { createInterface } from 'node:readline';
|
|
28
28
|
import { pipeline } from 'node:stream/promises';
|
|
29
|
-
import { execSync } from 'node:child_process';
|
|
29
|
+
import { execSync, spawn } from 'node:child_process';
|
|
30
30
|
import { randomBytes, createHash } from 'node:crypto';
|
|
31
31
|
import { createConnection } from 'node:net';
|
|
32
32
|
|
|
@@ -41,6 +41,14 @@ const T = {
|
|
|
41
41
|
title: { en: '🧠 MindOS Setup', zh: '🧠 MindOS 初始化' },
|
|
42
42
|
langHint: { en: ' ← → switch language / 切换语言 ↑ ↓ navigate Enter confirm', zh: ' ← → switch language / 切换语言 ↑ ↓ 上下切换 Enter 确认' },
|
|
43
43
|
|
|
44
|
+
// mode selection
|
|
45
|
+
modePrompt: { en: 'Setup mode', zh: '配置方式' },
|
|
46
|
+
modeOpts: { en: ['CLI — terminal wizard', 'GUI — browser wizard (recommended)'], zh: ['CLI — 终端向导', 'GUI — 浏览器向导(推荐)'] },
|
|
47
|
+
modeVals: ['cli', 'gui'],
|
|
48
|
+
guiStarting: { en: '⏳ Starting server for GUI setup...', zh: '⏳ 正在启动服务...' },
|
|
49
|
+
guiReady: { en: (url) => `🌐 Complete setup in browser: ${url}`, zh: (url) => `🌐 在浏览器中完成配置: ${url}` },
|
|
50
|
+
guiOpenFailed: { en: (url) => ` Could not open browser automatically. Open this URL manually:\n ${url}`, zh: (url) => ` 无法自动打开浏览器,请手动访问:\n ${url}` },
|
|
51
|
+
|
|
44
52
|
// step labels
|
|
45
53
|
step: { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
|
|
46
54
|
stepTitles: {
|
|
@@ -106,6 +114,8 @@ const T = {
|
|
|
106
114
|
cfgKept: { en: '✔ Keeping existing config', zh: '✔ 保留现有配置' },
|
|
107
115
|
cfgKeptNote: { en: ' Settings from this session were not saved', zh: ' 本次填写的设置未保存' },
|
|
108
116
|
cfgSaved: { en: '✔ Config saved', zh: '✔ 配置已保存' },
|
|
117
|
+
cfgConfirm: { en: 'Save this configuration?', zh: '保存此配置?' },
|
|
118
|
+
cfgAborted: { en: '✘ Setup cancelled. Run `mindos onboard` to try again.', zh: '✘ 设置已取消。运行 `mindos onboard` 重新开始。' },
|
|
109
119
|
yesNo: { en: '[y/N]', zh: '[y/N]' },
|
|
110
120
|
yesNoDefault: { en: '[Y/n]', zh: '[Y/n]' },
|
|
111
121
|
startNow: { en: 'Start MindOS now?', zh: '现在启动 MindOS?' },
|
|
@@ -493,11 +503,95 @@ async function applyTemplate(tpl, mindDir) {
|
|
|
493
503
|
}
|
|
494
504
|
}
|
|
495
505
|
|
|
506
|
+
// ── GUI Setup ─────────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
function openBrowser(url) {
|
|
509
|
+
try {
|
|
510
|
+
const platform = process.platform;
|
|
511
|
+
if (platform === 'darwin') {
|
|
512
|
+
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
513
|
+
} else if (platform === 'linux') {
|
|
514
|
+
// Check for WSL
|
|
515
|
+
const isWSL = existsSync('/proc/version') &&
|
|
516
|
+
readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft');
|
|
517
|
+
if (isWSL) {
|
|
518
|
+
execSync(`cmd.exe /c start "${url}"`, { stdio: 'ignore' });
|
|
519
|
+
} else {
|
|
520
|
+
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
execSync(`cmd.exe /c start "${url}"`, { stdio: 'ignore' });
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function startGuiSetup() {
|
|
532
|
+
// Ensure ~/.mindos directory exists
|
|
533
|
+
mkdirSync(MINDOS_DIR, { recursive: true });
|
|
534
|
+
|
|
535
|
+
// Read or create config, set setupPending
|
|
536
|
+
let config = {};
|
|
537
|
+
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }
|
|
538
|
+
config.setupPending = true;
|
|
539
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
540
|
+
|
|
541
|
+
// Find a free port
|
|
542
|
+
const port = await findFreePort(3000);
|
|
543
|
+
if (config.port === undefined) {
|
|
544
|
+
config.port = port;
|
|
545
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
546
|
+
}
|
|
547
|
+
const usePort = config.port || port;
|
|
548
|
+
|
|
549
|
+
write(c.yellow(t('guiStarting') + '\n'));
|
|
550
|
+
|
|
551
|
+
// Start the server in the background
|
|
552
|
+
const cliPath = resolve(__dirname, '../bin/cli.js');
|
|
553
|
+
const child = spawn(process.execPath, [cliPath, 'start'], {
|
|
554
|
+
detached: true,
|
|
555
|
+
stdio: 'ignore',
|
|
556
|
+
env: { ...process.env, PORT: String(usePort) },
|
|
557
|
+
});
|
|
558
|
+
child.unref();
|
|
559
|
+
|
|
560
|
+
// Wait for the server to be ready
|
|
561
|
+
const { waitForHttp } = await import('../bin/lib/gateway.js');
|
|
562
|
+
const ready = await waitForHttp(usePort, { retries: 60, intervalMs: 1000, label: 'MindOS' });
|
|
563
|
+
|
|
564
|
+
if (!ready) {
|
|
565
|
+
write(c.red('\n✘ Server failed to start.\n'));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const url = `http://localhost:${usePort}/setup`;
|
|
570
|
+
console.log(`\n${c.green(tf('guiReady', url))}\n`);
|
|
571
|
+
|
|
572
|
+
const opened = openBrowser(url);
|
|
573
|
+
if (!opened) {
|
|
574
|
+
console.log(c.dim(tf('guiOpenFailed', url)));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
process.exit(0);
|
|
578
|
+
}
|
|
579
|
+
|
|
496
580
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
497
581
|
|
|
498
582
|
async function main() {
|
|
499
583
|
console.log(`\n${c.bold(t('title'))}\n\n${c.dim(t('langHint'))}\n`);
|
|
500
584
|
|
|
585
|
+
// ── Mode selection: CLI or GUI ───────────────────────────────────────────
|
|
586
|
+
const mode = await select('modePrompt', 'modeOpts', 'modeVals');
|
|
587
|
+
|
|
588
|
+
if (mode === 'gui') {
|
|
589
|
+
await startGuiSetup();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── CLI mode continues below ─────────────────────────────────────────────
|
|
594
|
+
|
|
501
595
|
// ── Early overwrite check ─────────────────────────────────────────────────
|
|
502
596
|
if (existsSync(CONFIG_PATH)) {
|
|
503
597
|
let existing = {};
|
|
@@ -672,6 +766,28 @@ async function main() {
|
|
|
672
766
|
},
|
|
673
767
|
};
|
|
674
768
|
|
|
769
|
+
// ── Configuration Summary & Confirmation ──────────────────────────────────
|
|
770
|
+
const maskPw = (s) => s ? '•'.repeat(Math.min(s.length, 8)) : '';
|
|
771
|
+
const maskTk = (s) => s && s.length > 8 ? s.slice(0, 8) + '····' : (s ? s.slice(0, 4) + '····' : '');
|
|
772
|
+
const sep = '━'.repeat(40);
|
|
773
|
+
write(`\n${sep}\n`);
|
|
774
|
+
write(`${c.bold(uiLang === 'zh' ? '配置摘要' : 'Configuration Summary')}\n`);
|
|
775
|
+
write(`${sep}\n`);
|
|
776
|
+
write(` ${c.dim('Knowledge base:')} ${mindDir}\n`);
|
|
777
|
+
write(` ${c.dim('Web port:')} ${webPort}\n`);
|
|
778
|
+
write(` ${c.dim('MCP port:')} ${mcpPort}\n`);
|
|
779
|
+
write(` ${c.dim('Auth token:')} ${maskTk(authToken)}\n`);
|
|
780
|
+
if (webPassword) write(` ${c.dim('Web password:')} ${maskPw(webPassword)}\n`);
|
|
781
|
+
write(` ${c.dim('AI provider:')} ${config.ai.provider}\n`);
|
|
782
|
+
write(` ${c.dim('Start mode:')} ${startMode}\n`);
|
|
783
|
+
write(`${sep}\n`);
|
|
784
|
+
|
|
785
|
+
const confirmSave = await askYesNoDefault('cfgConfirm');
|
|
786
|
+
if (!confirmSave) {
|
|
787
|
+
console.log(c.red(t('cfgAborted')));
|
|
788
|
+
process.exit(0);
|
|
789
|
+
}
|
|
790
|
+
|
|
675
791
|
mkdirSync(MINDOS_DIR, { recursive: true });
|
|
676
792
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
677
793
|
console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);
|
|
File without changes
|