@geminilight/mindos 0.3.0 → 0.5.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/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/setup/check-port/route.ts +41 -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 +19 -2
- package/app/app/setup/page.tsx +2 -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 +499 -172
- 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 +226 -19
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -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/mcp-install.js +4 -1
- package/bin/lib/port.js +8 -2
- 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 +205 -10
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
package/bin/lib/port.js
CHANGED
|
@@ -4,8 +4,14 @@ import { bold, dim, red } from './colors.js';
|
|
|
4
4
|
export function isPortInUse(port) {
|
|
5
5
|
return new Promise((resolve) => {
|
|
6
6
|
const sock = createConnection({ port, host: '127.0.0.1' });
|
|
7
|
-
|
|
8
|
-
sock.
|
|
7
|
+
const cleanup = (result) => { sock.destroy(); resolve(result); };
|
|
8
|
+
sock.setTimeout(500, () => cleanup(false));
|
|
9
|
+
sock.once('connect', () => cleanup(true));
|
|
10
|
+
sock.once('error', (err) => {
|
|
11
|
+
// ECONNREFUSED = nothing listening → port is free
|
|
12
|
+
// EACCES / ENETUNREACH / etc. = treat as unavailable (can't bind either)
|
|
13
|
+
cleanup(err.code !== 'ECONNREFUSED');
|
|
14
|
+
});
|
|
9
15
|
});
|
|
10
16
|
}
|
|
11
17
|
|
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(', ')}`);
|