@aion0/forge 0.2.12 → 0.2.13
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/api/upgrade/route.ts +31 -0
- package/app/api/version/route.ts +59 -0
- package/cli/mw.ts +30 -1
- package/components/Dashboard.tsx +37 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { lstatSync } from 'node:fs';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export async function POST() {
|
|
7
|
+
try {
|
|
8
|
+
// Check if installed via npm link (symlink = local dev)
|
|
9
|
+
let isLinked = false;
|
|
10
|
+
try { isLinked = lstatSync(join(process.cwd())).isSymbolicLink(); } catch {}
|
|
11
|
+
|
|
12
|
+
if (isLinked) {
|
|
13
|
+
return NextResponse.json({
|
|
14
|
+
ok: false,
|
|
15
|
+
error: 'Local dev install (npm link). Run: git pull && pnpm install && pnpm build',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
execSync('cd /tmp && npm install -g @aion0/forge', { timeout: 120000 });
|
|
20
|
+
|
|
21
|
+
return NextResponse.json({
|
|
22
|
+
ok: true,
|
|
23
|
+
message: 'Upgraded. Restart server to apply.',
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return NextResponse.json({
|
|
27
|
+
ok: false,
|
|
28
|
+
error: `Upgrade failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Cache npm version check for 1 hour
|
|
6
|
+
let cachedLatest: { version: string; checkedAt: number } | null = null;
|
|
7
|
+
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
8
|
+
|
|
9
|
+
function getCurrentVersion(): string {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
|
|
12
|
+
return pkg.version;
|
|
13
|
+
} catch {
|
|
14
|
+
return '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getLatestVersion(): Promise<string> {
|
|
19
|
+
if (cachedLatest && Date.now() - cachedLatest.checkedAt < CACHE_TTL) {
|
|
20
|
+
return cachedLatest.version;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
25
|
+
const res = await fetch('https://registry.npmjs.org/@aion0/forge/latest', {
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
headers: { 'Accept': 'application/json' },
|
|
28
|
+
});
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
if (!res.ok) return cachedLatest?.version || '';
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
cachedLatest = { version: data.version, checkedAt: Date.now() };
|
|
33
|
+
return data.version;
|
|
34
|
+
} catch {
|
|
35
|
+
return cachedLatest?.version || '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function compareVersions(a: string, b: string): number {
|
|
40
|
+
const pa = a.split('.').map(Number);
|
|
41
|
+
const pb = b.split('.').map(Number);
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
44
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function GET() {
|
|
50
|
+
const current = getCurrentVersion();
|
|
51
|
+
const latest = await getLatestVersion();
|
|
52
|
+
const hasUpdate = latest && compareVersions(current, latest) < 0;
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
current,
|
|
56
|
+
latest: latest || current,
|
|
57
|
+
hasUpdate,
|
|
58
|
+
});
|
|
59
|
+
}
|
package/cli/mw.ts
CHANGED
|
@@ -20,6 +20,35 @@ const BASE = process.env.MW_URL || 'http://localhost:3000';
|
|
|
20
20
|
|
|
21
21
|
const [, , cmd, ...args] = process.argv;
|
|
22
22
|
|
|
23
|
+
/** Check npm for newer version, print reminder if available */
|
|
24
|
+
async function checkForUpdate() {
|
|
25
|
+
try {
|
|
26
|
+
const { readFileSync } = await import('node:fs');
|
|
27
|
+
const { join, dirname } = await import('node:path');
|
|
28
|
+
const { fileURLToPath } = await import('node:url');
|
|
29
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
|
|
30
|
+
const current = pkg.version;
|
|
31
|
+
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
34
|
+
const res = await fetch('https://registry.npmjs.org/@aion0/forge/latest', {
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
headers: { 'Accept': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
if (!res.ok) return;
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
const latest = data.version;
|
|
42
|
+
|
|
43
|
+
const [ca, cb, cc] = current.split('.').map(Number);
|
|
44
|
+
const [la, lb, lc] = latest.split('.').map(Number);
|
|
45
|
+
if (la > ca || (la === ca && lb > cb) || (la === ca && lb === cb && lc > cc)) {
|
|
46
|
+
console.log(`\n Update available: v${current} → v${latest}`);
|
|
47
|
+
console.log(` Run: forge upgrade\n`);
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
async function api(path: string, opts?: RequestInit) {
|
|
24
53
|
const res = await fetch(`${BASE}${path}`, opts);
|
|
25
54
|
if (!res.ok) {
|
|
@@ -512,7 +541,7 @@ Shortcuts: t=task, ls=tasks, w=watch, s=status, l=log, f=flows, p=projects, pw=p
|
|
|
512
541
|
}
|
|
513
542
|
}
|
|
514
543
|
|
|
515
|
-
main().catch(err => {
|
|
544
|
+
main().then(() => checkForUpdate()).catch(err => {
|
|
516
545
|
console.error(err.message);
|
|
517
546
|
process.exit(1);
|
|
518
547
|
});
|
package/components/Dashboard.tsx
CHANGED
|
@@ -50,8 +50,16 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
50
50
|
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
51
51
|
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
52
52
|
const [onlineCount, setOnlineCount] = useState<{ total: number; remote: number }>({ total: 0, remote: 0 });
|
|
53
|
+
const [versionInfo, setVersionInfo] = useState<{ current: string; latest: string; hasUpdate: boolean } | null>(null);
|
|
54
|
+
const [upgrading, setUpgrading] = useState(false);
|
|
55
|
+
const [upgradeResult, setUpgradeResult] = useState<string | null>(null);
|
|
53
56
|
const terminalRef = useRef<WebTerminalHandle>(null);
|
|
54
57
|
|
|
58
|
+
// Version check (once on mount)
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
fetch('/api/version').then(r => r.json()).then(setVersionInfo).catch(() => {});
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
55
63
|
// Heartbeat for online user tracking
|
|
56
64
|
useEffect(() => {
|
|
57
65
|
const ping = () => {
|
|
@@ -96,6 +104,35 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
96
104
|
<header className="h-12 border-b-2 border-[var(--border)] flex items-center justify-between px-4 shrink-0 bg-[var(--bg-secondary)]">
|
|
97
105
|
<div className="flex items-center gap-4">
|
|
98
106
|
<span className="text-sm font-bold text-[var(--accent)]">Forge</span>
|
|
107
|
+
{versionInfo && (
|
|
108
|
+
<span className="flex items-center gap-1.5">
|
|
109
|
+
<span className="text-[10px] text-[var(--text-secondary)]">v{versionInfo.current}</span>
|
|
110
|
+
{versionInfo.hasUpdate && !upgradeResult && (
|
|
111
|
+
<button
|
|
112
|
+
disabled={upgrading}
|
|
113
|
+
onClick={async () => {
|
|
114
|
+
setUpgrading(true);
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch('/api/upgrade', { method: 'POST' });
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
setUpgradeResult(data.ok ? data.message : data.error);
|
|
119
|
+
if (data.ok) setVersionInfo(v => v ? { ...v, hasUpdate: false } : v);
|
|
120
|
+
} catch { setUpgradeResult('Upgrade failed'); }
|
|
121
|
+
setUpgrading(false);
|
|
122
|
+
}}
|
|
123
|
+
className="text-[9px] px-1.5 py-0.5 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
|
|
124
|
+
title={`Update to v${versionInfo.latest}`}
|
|
125
|
+
>
|
|
126
|
+
{upgrading ? 'Upgrading...' : `Update v${versionInfo.latest}`}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
{upgradeResult && (
|
|
130
|
+
<span className="text-[9px] text-[var(--green)] max-w-[200px] truncate" title={upgradeResult}>
|
|
131
|
+
{upgradeResult}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
99
136
|
|
|
100
137
|
{/* View mode toggle */}
|
|
101
138
|
<div className="flex bg-[var(--bg-tertiary)] rounded p-0.5">
|
package/package.json
CHANGED