@cryptiklemur/lattice 1.26.1 → 1.27.1
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/.github/workflows/release.yml +44 -1
- package/bin/lattice +15 -0
- package/client/src/components/sidebar/UserIsland.tsx +26 -4
- package/client/src/components/ui/UpdateBanner.tsx +110 -0
- package/client/src/router.tsx +2 -0
- package/package.json +5 -3
- package/scripts/build-binary.ts +157 -0
- package/server/src/assets.ts +69 -0
- package/server/src/daemon.ts +27 -1
- package/server/src/handlers/update.ts +127 -0
- package/server/src/index.ts +100 -3
- package/server/src/project/terminal.ts +23 -8
- package/server/src/runtime.ts +4 -0
- package/server/src/update-checker.ts +146 -0
- package/shared/src/messages.ts +30 -2
|
@@ -16,6 +16,9 @@ jobs:
|
|
|
16
16
|
name: Release
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
environment: npm
|
|
19
|
+
outputs:
|
|
20
|
+
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
|
|
21
|
+
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
|
|
19
22
|
steps:
|
|
20
23
|
- name: Checkout
|
|
21
24
|
uses: actions/checkout@v5
|
|
@@ -39,6 +42,46 @@ jobs:
|
|
|
39
42
|
run: bun run build
|
|
40
43
|
|
|
41
44
|
- name: Release
|
|
45
|
+
id: semantic
|
|
42
46
|
env:
|
|
43
47
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
44
|
-
run:
|
|
48
|
+
run: |
|
|
49
|
+
npx semantic-release
|
|
50
|
+
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
51
|
+
if [ -n "$TAG" ]; then
|
|
52
|
+
VERSION="${TAG#v}"
|
|
53
|
+
echo "new_release_published=true" >> $GITHUB_OUTPUT
|
|
54
|
+
echo "new_release_version=$VERSION" >> $GITHUB_OUTPUT
|
|
55
|
+
else
|
|
56
|
+
echo "new_release_published=false" >> $GITHUB_OUTPUT
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
build-binaries:
|
|
60
|
+
name: Build ${{ matrix.target }}
|
|
61
|
+
needs: release
|
|
62
|
+
if: needs.release.outputs.new_release_published == 'true'
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
strategy:
|
|
65
|
+
matrix:
|
|
66
|
+
target: [linux-x64, linux-arm64, darwin-x64, darwin-arm64]
|
|
67
|
+
steps:
|
|
68
|
+
- name: Checkout
|
|
69
|
+
uses: actions/checkout@v5
|
|
70
|
+
with:
|
|
71
|
+
ref: v${{ needs.release.outputs.new_release_version }}
|
|
72
|
+
|
|
73
|
+
- name: Setup Bun
|
|
74
|
+
uses: oven-sh/setup-bun@v2
|
|
75
|
+
with:
|
|
76
|
+
bun-version: latest
|
|
77
|
+
|
|
78
|
+
- name: Install dependencies
|
|
79
|
+
run: bun install --frozen-lockfile
|
|
80
|
+
|
|
81
|
+
- name: Build binary
|
|
82
|
+
run: bun scripts/build-binary.ts --target=${{ matrix.target }} --version=${{ needs.release.outputs.new_release_version }}
|
|
83
|
+
|
|
84
|
+
- name: Upload to GitHub Release
|
|
85
|
+
env:
|
|
86
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
87
|
+
run: gh release upload "v${{ needs.release.outputs.new_release_version }}" "dist/lattice-${{ matrix.target }}" --clobber
|
package/bin/lattice
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
ENTRY="$SCRIPT_DIR/../server/src/index.ts"
|
|
6
|
+
|
|
7
|
+
if command -v bun >/dev/null 2>&1; then
|
|
8
|
+
exec bun "$ENTRY" "$@"
|
|
9
|
+
else
|
|
10
|
+
echo "[lattice] Error: bun is required to run lattice from npm."
|
|
11
|
+
echo "[lattice] Install bun: https://bun.sh"
|
|
12
|
+
echo "[lattice] Or download a standalone binary from:"
|
|
13
|
+
echo "[lattice] https://github.com/cryptiklemur/lattice/releases"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Sun, Moon, Settings, Download } from "lucide-react";
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Sun, Moon, Settings, Download, ArrowUpCircle } from "lucide-react";
|
|
3
3
|
import { useStore } from "@tanstack/react-store";
|
|
4
4
|
import { useTheme } from "../../hooks/useTheme";
|
|
5
5
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
6
6
|
import { useInstallPrompt } from "../../hooks/useInstallPrompt";
|
|
7
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
7
8
|
import { getSessionStore } from "../../stores/session";
|
|
8
9
|
import pkg from "../../../package.json";
|
|
10
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
9
11
|
|
|
10
12
|
interface UserIslandProps {
|
|
11
13
|
nodeName: string;
|
|
@@ -18,6 +20,20 @@ export function UserIsland(props: UserIslandProps) {
|
|
|
18
20
|
var { canInstall, install } = useInstallPrompt();
|
|
19
21
|
var budgetStatus = useStore(getSessionStore(), function (s) { return s.budgetStatus; });
|
|
20
22
|
var [showTooltip, setShowTooltip] = useState(false);
|
|
23
|
+
var ws = useWebSocket();
|
|
24
|
+
var [updateAvailable, setUpdateAvailable] = useState(false);
|
|
25
|
+
var [latestVersion, setLatestVersion] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(function () {
|
|
28
|
+
function handleUpdateStatus(msg: ServerMessage) {
|
|
29
|
+
if (msg.type !== "update:status") return;
|
|
30
|
+
var data = msg as { type: string; updateAvailable: boolean; latestVersion: string | null };
|
|
31
|
+
setUpdateAvailable(data.updateAvailable);
|
|
32
|
+
setLatestVersion(data.latestVersion);
|
|
33
|
+
}
|
|
34
|
+
ws.subscribe("update:status", handleUpdateStatus);
|
|
35
|
+
return function () { ws.unsubscribe("update:status", handleUpdateStatus); };
|
|
36
|
+
}, []);
|
|
21
37
|
|
|
22
38
|
var initial = props.nodeName.charAt(0).toUpperCase();
|
|
23
39
|
|
|
@@ -72,8 +88,14 @@ export function UserIsland(props: UserIslandProps) {
|
|
|
72
88
|
<div className="text-[13px] font-semibold text-base-content truncate">
|
|
73
89
|
{props.nodeName}
|
|
74
90
|
</div>
|
|
75
|
-
<div className="text-[10px]
|
|
76
|
-
{"v" + pkg.version}
|
|
91
|
+
<div className="text-[10px] font-mono flex items-center gap-1">
|
|
92
|
+
<span className="text-base-content/30">{"v" + pkg.version}</span>
|
|
93
|
+
{updateAvailable && latestVersion && (
|
|
94
|
+
<span className="flex items-center gap-0.5 text-primary/70">
|
|
95
|
+
<ArrowUpCircle size={9} />
|
|
96
|
+
{latestVersion}
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
77
99
|
</div>
|
|
78
100
|
</div>
|
|
79
101
|
</button>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { ArrowUpCircle, X, Loader2, ExternalLink, RefreshCw } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
5
|
+
|
|
6
|
+
interface UpdateState {
|
|
7
|
+
currentVersion: string;
|
|
8
|
+
latestVersion: string | null;
|
|
9
|
+
updateAvailable: boolean;
|
|
10
|
+
releaseUrl: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function UpdateBanner() {
|
|
14
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
15
|
+
var [update, setUpdate] = useState<UpdateState | null>(null);
|
|
16
|
+
var [dismissed, setDismissed] = useState(false);
|
|
17
|
+
var [applying, setApplying] = useState(false);
|
|
18
|
+
var [applyResult, setApplyResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(function () {
|
|
21
|
+
function handleStatus(msg: ServerMessage) {
|
|
22
|
+
if (msg.type !== "update:status") return;
|
|
23
|
+
var data = msg as UpdateState & { type: string };
|
|
24
|
+
setUpdate({ currentVersion: data.currentVersion, latestVersion: data.latestVersion, updateAvailable: data.updateAvailable, releaseUrl: data.releaseUrl });
|
|
25
|
+
if (data.updateAvailable) setDismissed(false);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleApplyResult(msg: ServerMessage) {
|
|
29
|
+
if (msg.type !== "update:apply_result") return;
|
|
30
|
+
var data = msg as { type: string; success: boolean; message?: string };
|
|
31
|
+
setApplying(false);
|
|
32
|
+
setApplyResult({ success: data.success, message: data.message ?? "" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
subscribe("update:status", handleStatus);
|
|
36
|
+
subscribe("update:apply_result", handleApplyResult);
|
|
37
|
+
send({ type: "update:check" } as any);
|
|
38
|
+
|
|
39
|
+
return function () {
|
|
40
|
+
unsubscribe("update:status", handleStatus);
|
|
41
|
+
unsubscribe("update:apply_result", handleApplyResult);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
if (!update || !update.updateAvailable || dismissed) return null;
|
|
46
|
+
|
|
47
|
+
if (applyResult) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={
|
|
50
|
+
"flex items-center gap-2 px-4 py-2 text-[12px] border-b " +
|
|
51
|
+
(applyResult.success
|
|
52
|
+
? "bg-success/10 border-success/20 text-success"
|
|
53
|
+
: "bg-error/10 border-error/20 text-error")
|
|
54
|
+
}>
|
|
55
|
+
<span className="flex-1">{applyResult.message}</span>
|
|
56
|
+
{applyResult.success && (
|
|
57
|
+
<button
|
|
58
|
+
onClick={function () { window.location.reload(); }}
|
|
59
|
+
className="flex items-center gap-1 text-[11px] font-mono font-bold hover:underline"
|
|
60
|
+
>
|
|
61
|
+
<RefreshCw size={11} />
|
|
62
|
+
Reload
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
<button onClick={function () { setDismissed(true); }} className="btn btn-ghost btn-xs btn-square opacity-50 hover:opacity-100">
|
|
66
|
+
<X size={12} />
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-primary/8 border-b border-primary/15 text-[12px] text-base-content/70">
|
|
74
|
+
<ArrowUpCircle size={14} className="text-primary flex-shrink-0" />
|
|
75
|
+
<span className="flex-1">
|
|
76
|
+
<span className="font-semibold text-base-content">v{update.latestVersion}</span>
|
|
77
|
+
{" "}available
|
|
78
|
+
<span className="text-base-content/40 ml-1">(current: v{update.currentVersion})</span>
|
|
79
|
+
</span>
|
|
80
|
+
{update.releaseUrl && (
|
|
81
|
+
<a
|
|
82
|
+
href={update.releaseUrl}
|
|
83
|
+
target="_blank"
|
|
84
|
+
rel="noopener noreferrer"
|
|
85
|
+
className="flex items-center gap-0.5 text-[11px] text-primary/50 hover:text-primary transition-colors"
|
|
86
|
+
>
|
|
87
|
+
<ExternalLink size={10} />
|
|
88
|
+
Notes
|
|
89
|
+
</a>
|
|
90
|
+
)}
|
|
91
|
+
{applying ? (
|
|
92
|
+
<Loader2 size={12} className="text-primary animate-spin" />
|
|
93
|
+
) : (
|
|
94
|
+
<button
|
|
95
|
+
onClick={function () { setApplying(true); send({ type: "update:apply" } as any); }}
|
|
96
|
+
className="btn btn-primary btn-xs"
|
|
97
|
+
>
|
|
98
|
+
Update
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
<button
|
|
102
|
+
onClick={function () { setDismissed(true); }}
|
|
103
|
+
aria-label="Dismiss update"
|
|
104
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content"
|
|
105
|
+
>
|
|
106
|
+
<X size={12} />
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
package/client/src/router.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { AddProjectModal } from "./components/sidebar/AddProjectModal";
|
|
|
15
15
|
import { useSidebar } from "./hooks/useSidebar";
|
|
16
16
|
import { useWorkspace } from "./hooks/useWorkspace";
|
|
17
17
|
import { useWebSocket } from "./hooks/useWebSocket";
|
|
18
|
+
import { UpdateBanner } from "./components/ui/UpdateBanner";
|
|
18
19
|
import { useSwipeDrawer } from "./hooks/useSwipeDrawer";
|
|
19
20
|
import { exitSettings, getSidebarStore, handlePopState, closeDrawer, toggleDrawer } from "./stores/sidebar";
|
|
20
21
|
|
|
@@ -422,6 +423,7 @@ function RootLayout() {
|
|
|
422
423
|
/>
|
|
423
424
|
|
|
424
425
|
<main id="main-content" className="drawer-content flex flex-col h-full min-w-0 overflow-hidden">
|
|
426
|
+
<UpdateBanner />
|
|
425
427
|
<Outlet />
|
|
426
428
|
</main>
|
|
427
429
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.1",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
"lattice"
|
|
24
24
|
],
|
|
25
25
|
"bin": {
|
|
26
|
-
"lattice": "./
|
|
26
|
+
"lattice": "./bin/lattice"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"dev": "bun run --filter '*' dev",
|
|
30
30
|
"build": "bun run --filter '*' build",
|
|
31
31
|
"lint": "bun run --filter '*' lint",
|
|
32
|
-
"typecheck": "bunx tsc --noEmit -p tsconfig.json"
|
|
32
|
+
"typecheck": "bunx tsc --noEmit -p tsconfig.json",
|
|
33
|
+
"build:binary": "bun scripts/build-binary.ts",
|
|
34
|
+
"build:binary:all": "bun scripts/build-binary.ts --all"
|
|
33
35
|
},
|
|
34
36
|
"workspaces": [
|
|
35
37
|
"shared",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { join, extname, relative } from "node:path";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
var CONTENT_TYPES: Record<string, string> = {
|
|
7
|
+
".html": "text/html; charset=utf-8",
|
|
8
|
+
".js": "application/javascript",
|
|
9
|
+
".css": "text/css",
|
|
10
|
+
".json": "application/json",
|
|
11
|
+
".svg": "image/svg+xml",
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".ico": "image/x-icon",
|
|
14
|
+
".webmanifest": "application/manifest+json",
|
|
15
|
+
".woff": "font/woff",
|
|
16
|
+
".woff2": "font/woff2",
|
|
17
|
+
".ttf": "font/ttf",
|
|
18
|
+
".txt": "text/plain",
|
|
19
|
+
".map": "application/json",
|
|
20
|
+
".wasm": "application/wasm",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
var VALID_TARGETS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64"];
|
|
24
|
+
|
|
25
|
+
var ROOT = join(import.meta.dir, "..");
|
|
26
|
+
|
|
27
|
+
function parseArgs(): { targets: string[]; version: string } {
|
|
28
|
+
var args = process.argv.slice(2);
|
|
29
|
+
var targets: string[] = [];
|
|
30
|
+
var version = "";
|
|
31
|
+
|
|
32
|
+
for (var i = 0; i < args.length; i++) {
|
|
33
|
+
if (args[i] === "--target" && i + 1 < args.length) {
|
|
34
|
+
targets.push(args[i + 1]);
|
|
35
|
+
i++;
|
|
36
|
+
} else if (args[i].startsWith("--target=")) {
|
|
37
|
+
targets.push(args[i].split("=")[1]);
|
|
38
|
+
} else if (args[i] === "--version" && i + 1 < args.length) {
|
|
39
|
+
version = args[i + 1];
|
|
40
|
+
i++;
|
|
41
|
+
} else if (args[i].startsWith("--version=")) {
|
|
42
|
+
version = args[i].split("=")[1];
|
|
43
|
+
} else if (args[i] === "--all") {
|
|
44
|
+
targets = [...VALID_TARGETS];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (targets.length === 0) {
|
|
49
|
+
var platform = process.platform === "darwin" ? "darwin" : "linux";
|
|
50
|
+
var arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
51
|
+
targets = [platform + "-" + arch];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!version) {
|
|
55
|
+
var pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
56
|
+
version = pkg.version || "0.0.0-dev";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (var t = 0; t < targets.length; t++) {
|
|
60
|
+
if (VALID_TARGETS.indexOf(targets[t]) === -1) {
|
|
61
|
+
console.error("Invalid target: " + targets[t]);
|
|
62
|
+
console.error("Valid targets: " + VALID_TARGETS.join(", "));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { targets, version };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function walkDir(dir: string, base: string): Array<{ path: string; relativePath: string }> {
|
|
71
|
+
var results: Array<{ path: string; relativePath: string }> = [];
|
|
72
|
+
var entries = readdirSync(dir, { withFileTypes: true });
|
|
73
|
+
for (var i = 0; i < entries.length; i++) {
|
|
74
|
+
var fullPath = join(dir, entries[i].name);
|
|
75
|
+
if (entries[i].isDirectory()) {
|
|
76
|
+
results = results.concat(walkDir(fullPath, base));
|
|
77
|
+
} else {
|
|
78
|
+
results.push({ path: fullPath, relativePath: "/" + relative(base, fullPath) });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function generateEmbeddedAssets(clientDistDir: string): void {
|
|
85
|
+
console.log("[build] Generating embedded assets...");
|
|
86
|
+
var files = walkDir(clientDistDir, clientDistDir);
|
|
87
|
+
var genDir = join(ROOT, "server/src/_generated");
|
|
88
|
+
mkdirSync(genDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
var lines: string[] = [];
|
|
91
|
+
lines.push("var assets = new Map<string, { b64: string; type: string }>();");
|
|
92
|
+
|
|
93
|
+
var totalSize = 0;
|
|
94
|
+
for (var i = 0; i < files.length; i++) {
|
|
95
|
+
var file = files[i];
|
|
96
|
+
var ext = extname(file.path);
|
|
97
|
+
var contentType = CONTENT_TYPES[ext] || "application/octet-stream";
|
|
98
|
+
var data = readFileSync(file.path);
|
|
99
|
+
var b64 = data.toString("base64");
|
|
100
|
+
totalSize += data.length;
|
|
101
|
+
lines.push("assets.set(" + JSON.stringify(file.relativePath) + ", { b64: " + JSON.stringify(b64) + ", type: " + JSON.stringify(contentType) + " });");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
lines.push("export { assets };");
|
|
105
|
+
|
|
106
|
+
writeFileSync(join(genDir, "embedded-assets.ts"), lines.join("\n"), "utf-8");
|
|
107
|
+
console.log("[build] Embedded " + files.length + " files (" + (totalSize / 1024 / 1024).toFixed(1) + " MB)");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildClient(): void {
|
|
111
|
+
console.log("[build] Building client...");
|
|
112
|
+
execSync("bun run --filter @lattice/client build", { cwd: ROOT, stdio: "inherit" });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compileBinary(target: string, version: string): void {
|
|
116
|
+
var outDir = join(ROOT, "dist");
|
|
117
|
+
mkdirSync(outDir, { recursive: true });
|
|
118
|
+
var outFile = join(outDir, "lattice-" + target);
|
|
119
|
+
|
|
120
|
+
console.log("[build] Compiling for " + target + "...");
|
|
121
|
+
var cmd = [
|
|
122
|
+
"bun", "build", "--compile",
|
|
123
|
+
"--target=bun-" + target,
|
|
124
|
+
"--outfile=" + outFile,
|
|
125
|
+
"--define=process.env.LATTICE_VERSION=\"'" + version + "'\"",
|
|
126
|
+
join(ROOT, "server/src/index.ts"),
|
|
127
|
+
].join(" ");
|
|
128
|
+
|
|
129
|
+
execSync(cmd, { cwd: ROOT, stdio: "inherit" });
|
|
130
|
+
|
|
131
|
+
var size = statSync(outFile).size;
|
|
132
|
+
console.log("[build] Output: " + outFile + " (" + (size / 1024 / 1024).toFixed(1) + " MB)");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function main(): void {
|
|
136
|
+
var { targets, version } = parseArgs();
|
|
137
|
+
console.log("[build] Version: " + version);
|
|
138
|
+
console.log("[build] Targets: " + targets.join(", "));
|
|
139
|
+
|
|
140
|
+
buildClient();
|
|
141
|
+
|
|
142
|
+
var clientDist = join(ROOT, "client/dist");
|
|
143
|
+
if (!existsSync(clientDist)) {
|
|
144
|
+
console.error("[build] client/dist not found after build");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
generateEmbeddedAssets(clientDist);
|
|
149
|
+
|
|
150
|
+
for (var i = 0; i < targets.length; i++) {
|
|
151
|
+
compileBinary(targets[i], version);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log("[build] Done.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
main();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { IS_COMPILED } from "./runtime";
|
|
3
|
+
|
|
4
|
+
var CONTENT_TYPES: Record<string, string> = {
|
|
5
|
+
".html": "text/html; charset=utf-8",
|
|
6
|
+
".js": "application/javascript",
|
|
7
|
+
".css": "text/css",
|
|
8
|
+
".json": "application/json",
|
|
9
|
+
".svg": "image/svg+xml",
|
|
10
|
+
".png": "image/png",
|
|
11
|
+
".ico": "image/x-icon",
|
|
12
|
+
".webmanifest": "application/manifest+json",
|
|
13
|
+
".woff": "font/woff",
|
|
14
|
+
".woff2": "font/woff2",
|
|
15
|
+
".ttf": "font/ttf",
|
|
16
|
+
".txt": "text/plain",
|
|
17
|
+
".map": "application/json",
|
|
18
|
+
".wasm": "application/wasm",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface EmbeddedAssetModule {
|
|
22
|
+
assets: Map<string, { b64: string; type: string }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var embeddedAssets: Map<string, { b64: string; type: string }> | null = null;
|
|
26
|
+
var assetCache = new Map<string, Uint8Array>();
|
|
27
|
+
|
|
28
|
+
export async function initAssets(): Promise<void> {
|
|
29
|
+
if (!IS_COMPILED) return;
|
|
30
|
+
try {
|
|
31
|
+
var mod = await import("./_generated/embedded-assets") as EmbeddedAssetModule;
|
|
32
|
+
embeddedAssets = mod.assets;
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function serveStaticAsset(pathname: string): Response | null {
|
|
37
|
+
if (!embeddedAssets) return null;
|
|
38
|
+
|
|
39
|
+
var entry = embeddedAssets.get(pathname);
|
|
40
|
+
if (!entry) return null;
|
|
41
|
+
|
|
42
|
+
var cached = assetCache.get(pathname);
|
|
43
|
+
if (!cached) {
|
|
44
|
+
cached = Buffer.from(entry.b64, "base64") as unknown as Uint8Array;
|
|
45
|
+
assetCache.set(pathname, cached);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Response(cached as unknown as BodyInit, {
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": entry.type,
|
|
51
|
+
"Cache-Control": pathname === "/index.html" || pathname === "/sw.js"
|
|
52
|
+
? "no-cache"
|
|
53
|
+
: "public, max-age=31536000, immutable",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hasEmbeddedAssets(): boolean {
|
|
59
|
+
return embeddedAssets !== null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getClientDir(): string {
|
|
63
|
+
return join(import.meta.dir, "../../client/dist");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function guessContentType(path: string): string {
|
|
67
|
+
var ext = path.slice(path.lastIndexOf("."));
|
|
68
|
+
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
69
|
+
}
|
package/server/src/daemon.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
+
import { initAssets, serveStaticAsset, hasEmbeddedAssets, getClientDir } from "./assets";
|
|
2
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
4
|
import type { ServerWebSocket } from "bun";
|
|
4
5
|
import { getLatticeHome, loadConfig } from "./config";
|
|
@@ -35,8 +36,10 @@ import "./handlers/editor";
|
|
|
35
36
|
import "./handlers/analytics";
|
|
36
37
|
import "./handlers/bookmarks";
|
|
37
38
|
import "./handlers/plugins";
|
|
39
|
+
import "./handlers/update";
|
|
38
40
|
import { startScheduler } from "./features/scheduler";
|
|
39
41
|
import { loadNotes } from "./features/sticky-notes";
|
|
42
|
+
import { startPeriodicUpdateCheck, getCachedUpdateInfo } from "./update-checker";
|
|
40
43
|
import { loadBookmarks } from "./project/bookmarks";
|
|
41
44
|
import { cleanupClientTerminals } from "./handlers/terminal";
|
|
42
45
|
import { cleanupClient as cleanupClientAttachments } from "./handlers/attachment";
|
|
@@ -202,7 +205,8 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
202
205
|
log.server("Node: %s (%s)", config.name, identity.id);
|
|
203
206
|
log.server("Home: %s", getLatticeHome());
|
|
204
207
|
|
|
205
|
-
|
|
208
|
+
await initAssets();
|
|
209
|
+
var clientDir = getClientDir();
|
|
206
210
|
|
|
207
211
|
var tlsOptions: { cert: Buffer; key: Buffer } | undefined;
|
|
208
212
|
if (config.tls) {
|
|
@@ -286,6 +290,15 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
286
290
|
}
|
|
287
291
|
|
|
288
292
|
var staticPath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
293
|
+
|
|
294
|
+
if (hasEmbeddedAssets()) {
|
|
295
|
+
var embedded = serveStaticAsset(staticPath);
|
|
296
|
+
if (embedded) return embedded;
|
|
297
|
+
var embeddedIndex = serveStaticAsset("/index.html");
|
|
298
|
+
if (embeddedIndex) return embeddedIndex;
|
|
299
|
+
return new Response("Not found", { status: 404 });
|
|
300
|
+
}
|
|
301
|
+
|
|
289
302
|
var file = Bun.file(join(clientDir, staticPath));
|
|
290
303
|
if (await file.exists()) {
|
|
291
304
|
return new Response(file);
|
|
@@ -355,6 +368,8 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
355
368
|
loadNotes();
|
|
356
369
|
loadBookmarks();
|
|
357
370
|
|
|
371
|
+
startPeriodicUpdateCheck();
|
|
372
|
+
|
|
358
373
|
loadInterruptedSessions();
|
|
359
374
|
|
|
360
375
|
onPeerConnected(function (nodeId: string) {
|
|
@@ -386,5 +401,16 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
386
401
|
return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
|
|
387
402
|
}),
|
|
388
403
|
});
|
|
404
|
+
var updateInfo = getCachedUpdateInfo();
|
|
405
|
+
if (updateInfo && updateInfo.updateAvailable) {
|
|
406
|
+
broadcast({
|
|
407
|
+
type: "update:status",
|
|
408
|
+
currentVersion: updateInfo.currentVersion,
|
|
409
|
+
latestVersion: updateInfo.latestVersion,
|
|
410
|
+
updateAvailable: updateInfo.updateAvailable,
|
|
411
|
+
releaseUrl: updateInfo.releaseUrl,
|
|
412
|
+
installMode: updateInfo.installMode,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
389
415
|
}, 10000);
|
|
390
416
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { chmodSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { ClientMessage } from "@lattice/shared";
|
|
3
|
+
import { registerHandler } from "../ws/router";
|
|
4
|
+
import { sendTo, broadcast } from "../ws/broadcast";
|
|
5
|
+
import { checkForUpdate, getPackageName, getGitHubRepo, getInstallMode } from "../update-checker";
|
|
6
|
+
import { IS_COMPILED } from "../runtime";
|
|
7
|
+
|
|
8
|
+
function getAssetName(): string {
|
|
9
|
+
var platform = process.platform === "darwin" ? "darwin" : "linux";
|
|
10
|
+
var arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
11
|
+
return "lattice-" + platform + "-" + arch;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function downloadBinaryUpdate(): Promise<{ success: boolean; message: string }> {
|
|
15
|
+
var repo = getGitHubRepo();
|
|
16
|
+
var assetName = getAssetName();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
var releaseRes = await fetch("https://api.github.com/repos/" + repo + "/releases/latest", {
|
|
20
|
+
headers: { "Accept": "application/vnd.github.v3+json" },
|
|
21
|
+
signal: AbortSignal.timeout(30000),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!releaseRes.ok) {
|
|
25
|
+
return { success: false, message: "Failed to fetch release info (HTTP " + releaseRes.status + ")" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var release = await releaseRes.json() as { assets?: Array<{ name: string; browser_download_url: string }> };
|
|
29
|
+
var assets = release.assets ?? [];
|
|
30
|
+
var asset = assets.find(function (a) { return a.name === assetName; });
|
|
31
|
+
|
|
32
|
+
if (!asset) {
|
|
33
|
+
return { success: false, message: "No binary found for " + assetName + " in latest release" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var downloadRes = await fetch(asset.browser_download_url, {
|
|
37
|
+
signal: AbortSignal.timeout(120000),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!downloadRes.ok) {
|
|
41
|
+
return { success: false, message: "Failed to download binary (HTTP " + downloadRes.status + ")" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var binary = new Uint8Array(await downloadRes.arrayBuffer());
|
|
45
|
+
var execPath = process.execPath;
|
|
46
|
+
var tmpPath = execPath + ".update";
|
|
47
|
+
|
|
48
|
+
writeFileSync(tmpPath, binary);
|
|
49
|
+
chmodSync(tmpPath, 0o755);
|
|
50
|
+
renameSync(tmpPath, execPath);
|
|
51
|
+
|
|
52
|
+
return { success: true, message: "Updated successfully. Restart the server to apply." };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return { success: false, message: "Update failed: " + (err instanceof Error ? err.message : String(err)) };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerHandler("update", function (clientId: string, message: ClientMessage) {
|
|
59
|
+
if (message.type === "update:check") {
|
|
60
|
+
var checkMsg = message as { type: "update:check"; force?: boolean };
|
|
61
|
+
void checkForUpdate(checkMsg.force ?? false).then(function (info) {
|
|
62
|
+
sendTo(clientId, {
|
|
63
|
+
type: "update:status",
|
|
64
|
+
currentVersion: info.currentVersion,
|
|
65
|
+
latestVersion: info.latestVersion,
|
|
66
|
+
updateAvailable: info.updateAvailable,
|
|
67
|
+
releaseUrl: info.releaseUrl,
|
|
68
|
+
installMode: info.installMode,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (message.type === "update:apply") {
|
|
75
|
+
if (IS_COMPILED) {
|
|
76
|
+
void downloadBinaryUpdate().then(function (result) {
|
|
77
|
+
sendTo(clientId, { type: "update:apply_result", success: result.success, message: result.message });
|
|
78
|
+
if (result.success) {
|
|
79
|
+
void checkForUpdate(true).then(function (info) {
|
|
80
|
+
broadcast({
|
|
81
|
+
type: "update:status",
|
|
82
|
+
currentVersion: info.currentVersion,
|
|
83
|
+
latestVersion: info.latestVersion,
|
|
84
|
+
updateAvailable: info.updateAvailable,
|
|
85
|
+
releaseUrl: info.releaseUrl,
|
|
86
|
+
installMode: info.installMode,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
var pkgName = getPackageName();
|
|
93
|
+
try {
|
|
94
|
+
var proc = Bun.spawn(["bun", "install", "-g", pkgName + "@latest"], {
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
var timeout = setTimeout(function () {
|
|
100
|
+
proc.kill();
|
|
101
|
+
}, 120000);
|
|
102
|
+
|
|
103
|
+
void proc.exited.then(function (code) {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
if (code === 0) {
|
|
106
|
+
sendTo(clientId, { type: "update:apply_result", success: true, message: "Updated successfully. Restart the server to apply." });
|
|
107
|
+
void checkForUpdate(true).then(function (info) {
|
|
108
|
+
broadcast({
|
|
109
|
+
type: "update:status",
|
|
110
|
+
currentVersion: info.currentVersion,
|
|
111
|
+
latestVersion: info.latestVersion,
|
|
112
|
+
updateAvailable: info.updateAvailable,
|
|
113
|
+
releaseUrl: info.releaseUrl,
|
|
114
|
+
installMode: info.installMode,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
sendTo(clientId, { type: "update:apply_result", success: false, message: "Update failed (exit code " + code + ")" });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
sendTo(clientId, { type: "update:apply_result", success: false, message: "Failed to start update: " + String(err) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
});
|
package/server/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DAEMON_PID_FILE } from "@lattice/shared";
|
|
5
5
|
import { getLatticeHome, loadConfig } from "./config";
|
|
6
|
+
import { IS_COMPILED } from "./runtime";
|
|
6
7
|
|
|
7
8
|
var args = process.argv.slice(2);
|
|
8
9
|
var command = "start";
|
|
@@ -74,9 +75,12 @@ switch (command) {
|
|
|
74
75
|
case "status":
|
|
75
76
|
runStatus();
|
|
76
77
|
break;
|
|
78
|
+
case "update":
|
|
79
|
+
await runUpdate();
|
|
80
|
+
break;
|
|
77
81
|
default:
|
|
78
82
|
console.log("[lattice] Unknown command: " + command);
|
|
79
|
-
console.log("[lattice] Usage: lattice [start|stop|status|daemon]");
|
|
83
|
+
console.log("[lattice] Usage: lattice [start|stop|status|update|daemon]");
|
|
80
84
|
process.exit(1);
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -125,10 +129,13 @@ async function runStart(): Promise<void> {
|
|
|
125
129
|
|
|
126
130
|
removePid();
|
|
127
131
|
|
|
128
|
-
var scriptPath = import.meta.path;
|
|
129
132
|
var logPath = join(getLatticeHome(), "daemon.log");
|
|
130
133
|
|
|
131
|
-
var
|
|
134
|
+
var spawnArgs = IS_COMPILED
|
|
135
|
+
? [process.execPath, "daemon"]
|
|
136
|
+
: ["bun", import.meta.path, "daemon"];
|
|
137
|
+
|
|
138
|
+
var child = Bun.spawn(spawnArgs, {
|
|
132
139
|
detached: true,
|
|
133
140
|
stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)],
|
|
134
141
|
});
|
|
@@ -194,6 +201,96 @@ function runStatus(): void {
|
|
|
194
201
|
console.log("[lattice] URL: " + url);
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
async function runUpdate(): Promise<void> {
|
|
205
|
+
var { checkForUpdate, getPackageName, getGitHubRepo } = await import("./update-checker");
|
|
206
|
+
console.log("[lattice] Checking for updates...");
|
|
207
|
+
var info = await checkForUpdate(true);
|
|
208
|
+
|
|
209
|
+
if (!info.latestVersion) {
|
|
210
|
+
console.log("[lattice] Could not check for updates. Try again later.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!info.updateAvailable) {
|
|
215
|
+
console.log("[lattice] Already on latest version (%s)", info.currentVersion);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log("[lattice] Update available: %s -> %s (%s)", info.currentVersion, info.latestVersion, info.installMode);
|
|
220
|
+
console.log("[lattice] Installing...");
|
|
221
|
+
|
|
222
|
+
var code: number;
|
|
223
|
+
|
|
224
|
+
if (IS_COMPILED) {
|
|
225
|
+
var { chmodSync, renameSync, writeFileSync } = await import("node:fs");
|
|
226
|
+
var repo = getGitHubRepo();
|
|
227
|
+
var platform = process.platform === "darwin" ? "darwin" : "linux";
|
|
228
|
+
var arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
229
|
+
var assetName = "lattice-" + platform + "-" + arch;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
var releaseRes = await fetch("https://api.github.com/repos/" + repo + "/releases/latest", {
|
|
233
|
+
headers: { "Accept": "application/vnd.github.v3+json" },
|
|
234
|
+
});
|
|
235
|
+
var release = await releaseRes.json() as { assets?: Array<{ name: string; browser_download_url: string }> };
|
|
236
|
+
var asset = (release.assets ?? []).find(function (a) { return a.name === assetName; });
|
|
237
|
+
|
|
238
|
+
if (!asset) {
|
|
239
|
+
console.error("[lattice] No binary found for " + assetName);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log("[lattice] Downloading " + assetName + "...");
|
|
244
|
+
var downloadRes = await fetch(asset.browser_download_url);
|
|
245
|
+
var binary = new Uint8Array(await downloadRes.arrayBuffer());
|
|
246
|
+
var tmpPath = process.execPath + ".update";
|
|
247
|
+
writeFileSync(tmpPath, binary);
|
|
248
|
+
chmodSync(tmpPath, 0o755);
|
|
249
|
+
renameSync(tmpPath, process.execPath);
|
|
250
|
+
code = 0;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error("[lattice] Download failed:", err instanceof Error ? err.message : String(err));
|
|
253
|
+
code = 1;
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
var pkgName = getPackageName();
|
|
257
|
+
var proc = Bun.spawn(["bun", "install", "-g", pkgName + "@latest"], {
|
|
258
|
+
stdout: "inherit",
|
|
259
|
+
stderr: "inherit",
|
|
260
|
+
});
|
|
261
|
+
code = await proc.exited;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (code === 0) {
|
|
265
|
+
console.log("[lattice] Updated to %s", info.latestVersion);
|
|
266
|
+
|
|
267
|
+
var pid = readPid();
|
|
268
|
+
if (pid !== null && isDaemonRunning(pid)) {
|
|
269
|
+
console.log("[lattice] Restarting daemon...");
|
|
270
|
+
try {
|
|
271
|
+
process.kill(pid, "SIGTERM");
|
|
272
|
+
} catch {}
|
|
273
|
+
removePid();
|
|
274
|
+
await new Promise<void>(function (resolve) { setTimeout(resolve, 1000); });
|
|
275
|
+
|
|
276
|
+
var logPath = join(getLatticeHome(), "daemon.log");
|
|
277
|
+
var restartArgs = IS_COMPILED
|
|
278
|
+
? [process.execPath, "daemon"]
|
|
279
|
+
: ["bun", import.meta.path, "daemon"];
|
|
280
|
+
var child = Bun.spawn(restartArgs, {
|
|
281
|
+
detached: true,
|
|
282
|
+
stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)],
|
|
283
|
+
});
|
|
284
|
+
child.unref();
|
|
285
|
+
writePid(child.pid);
|
|
286
|
+
console.log("[lattice] Daemon restarted (PID %d)", child.pid);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
console.error("[lattice] Update failed (exit code %d)", code);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
197
294
|
function openBrowser(url: string): void {
|
|
198
295
|
var platform = process.platform;
|
|
199
296
|
try {
|
|
@@ -2,6 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import type { ChildProcess } from "node:child_process";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { IS_COMPILED } from "../runtime";
|
|
5
8
|
|
|
6
9
|
interface TerminalWorker {
|
|
7
10
|
process: ChildProcess;
|
|
@@ -10,21 +13,33 @@ interface TerminalWorker {
|
|
|
10
13
|
|
|
11
14
|
var terminals = new Map<string, TerminalWorker>();
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
function getWorkerPath(): string {
|
|
17
|
+
if (!IS_COMPILED) {
|
|
18
|
+
return join(import.meta.dir, "pty-worker.cjs");
|
|
19
|
+
}
|
|
20
|
+
var extractedPath = join(homedir(), ".lattice", "pty-worker.cjs");
|
|
21
|
+
if (!existsSync(extractedPath)) {
|
|
22
|
+
var srcPath = join(import.meta.dir, "pty-worker.cjs");
|
|
23
|
+
if (existsSync(srcPath)) {
|
|
24
|
+
mkdirSync(join(homedir(), ".lattice"), { recursive: true });
|
|
25
|
+
writeFileSync(extractedPath, readFileSync(srcPath));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return extractedPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var WORKER_PATH = getWorkerPath();
|
|
16
32
|
|
|
17
|
-
// node-pty lives in Bun's module cache — resolve the path so Node can find it.
|
|
18
33
|
var NODE_MODULES_PATH = (function () {
|
|
19
|
-
// Bun can resolve the module — use it to find the actual path
|
|
20
34
|
try {
|
|
21
35
|
var resolved = require.resolve("node-pty");
|
|
22
|
-
// resolved = .../node_modules/.bun/node-pty@X.Y.Z/node_modules/node-pty/lib/index.js
|
|
23
|
-
// We need: .../node_modules/.bun/node-pty@X.Y.Z/node_modules
|
|
24
36
|
var parts = resolved.split("/node_modules/");
|
|
25
|
-
parts.pop();
|
|
37
|
+
parts.pop();
|
|
26
38
|
return parts.join("/node_modules/") + "/node_modules";
|
|
27
39
|
} catch {
|
|
40
|
+
if (IS_COMPILED) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
28
43
|
return join(import.meta.dir, "..", "..", "..", "node_modules");
|
|
29
44
|
}
|
|
30
45
|
})();
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { log } from "./logger";
|
|
4
|
+
import { IS_COMPILED } from "./runtime";
|
|
5
|
+
|
|
6
|
+
var PKG_NAME = "@cryptiklemur/lattice";
|
|
7
|
+
var GITHUB_REPO = "cryptiklemur/lattice";
|
|
8
|
+
var CHECK_INTERVAL_MS = 3600000;
|
|
9
|
+
|
|
10
|
+
export type InstallMode = "binary" | "npm";
|
|
11
|
+
|
|
12
|
+
export interface UpdateInfo {
|
|
13
|
+
currentVersion: string;
|
|
14
|
+
latestVersion: string | null;
|
|
15
|
+
updateAvailable: boolean;
|
|
16
|
+
lastCheckedAt: number;
|
|
17
|
+
releaseUrl: string | null;
|
|
18
|
+
installMode: InstallMode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var cached: UpdateInfo | null = null;
|
|
22
|
+
var checking = false;
|
|
23
|
+
|
|
24
|
+
function getCurrentVersion(): string {
|
|
25
|
+
if (process.env.LATTICE_VERSION) return process.env.LATTICE_VERSION;
|
|
26
|
+
try {
|
|
27
|
+
var pkg = JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf-8"));
|
|
28
|
+
return pkg.version || "0.0.0";
|
|
29
|
+
} catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function compareVersions(a: string, b: string): number {
|
|
35
|
+
var pa = a.replace(/^v/, "").split(".").map(Number);
|
|
36
|
+
var pb = b.replace(/^v/, "").split(".").map(Number);
|
|
37
|
+
for (var i = 0; i < 3; i++) {
|
|
38
|
+
var va = pa[i] || 0;
|
|
39
|
+
var vb = pb[i] || 0;
|
|
40
|
+
if (va !== vb) return va - vb;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getInstallMode(): InstallMode {
|
|
46
|
+
return IS_COMPILED ? "binary" : "npm";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function checkGitHub(currentVersion: string): Promise<UpdateInfo> {
|
|
50
|
+
var res = await fetch("https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest", {
|
|
51
|
+
headers: { "Accept": "application/vnd.github.v3+json" },
|
|
52
|
+
signal: AbortSignal.timeout(10000),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
log.server("GitHub update check failed: HTTP %d", res.status);
|
|
57
|
+
return { currentVersion, latestVersion: null, updateAvailable: false, lastCheckedAt: Date.now(), releaseUrl: null, installMode: "binary" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var data = await res.json() as { tag_name?: string; html_url?: string };
|
|
61
|
+
var latestVersion = data.tag_name ? data.tag_name.replace(/^v/, "") : null;
|
|
62
|
+
var updateAvailable = latestVersion !== null && compareVersions(latestVersion, currentVersion) > 0;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
currentVersion,
|
|
66
|
+
latestVersion,
|
|
67
|
+
updateAvailable,
|
|
68
|
+
lastCheckedAt: Date.now(),
|
|
69
|
+
releaseUrl: updateAvailable ? (data.html_url ?? null) : null,
|
|
70
|
+
installMode: "binary",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function checkNpm(currentVersion: string): Promise<UpdateInfo> {
|
|
75
|
+
var res = await fetch("https://registry.npmjs.org/" + PKG_NAME + "/latest", {
|
|
76
|
+
headers: { "Accept": "application/json" },
|
|
77
|
+
signal: AbortSignal.timeout(10000),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
log.server("npm update check failed: HTTP %d", res.status);
|
|
82
|
+
return { currentVersion, latestVersion: null, updateAvailable: false, lastCheckedAt: Date.now(), releaseUrl: null, installMode: "npm" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var data = await res.json() as { version?: string };
|
|
86
|
+
var latestVersion = data.version ?? null;
|
|
87
|
+
var updateAvailable = latestVersion !== null && compareVersions(latestVersion, currentVersion) > 0;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
currentVersion,
|
|
91
|
+
latestVersion,
|
|
92
|
+
updateAvailable,
|
|
93
|
+
lastCheckedAt: Date.now(),
|
|
94
|
+
releaseUrl: updateAvailable ? "https://github.com/" + GITHUB_REPO + "/releases/tag/v" + latestVersion : null,
|
|
95
|
+
installMode: "npm",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function checkForUpdate(force: boolean = false): Promise<UpdateInfo> {
|
|
100
|
+
var currentVersion = getCurrentVersion();
|
|
101
|
+
|
|
102
|
+
if (!force && cached && Date.now() - cached.lastCheckedAt < CHECK_INTERVAL_MS) {
|
|
103
|
+
return cached;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (checking) {
|
|
107
|
+
return cached ?? { currentVersion, latestVersion: null, updateAvailable: false, lastCheckedAt: 0, releaseUrl: null, installMode: getInstallMode() };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
checking = true;
|
|
111
|
+
try {
|
|
112
|
+
cached = IS_COMPILED
|
|
113
|
+
? await checkGitHub(currentVersion)
|
|
114
|
+
: await checkNpm(currentVersion);
|
|
115
|
+
|
|
116
|
+
if (cached.updateAvailable) {
|
|
117
|
+
log.server("Update available: %s -> %s (%s)", currentVersion, cached.latestVersion, cached.installMode);
|
|
118
|
+
}
|
|
119
|
+
return cached;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
log.server("Update check error: %s", err instanceof Error ? err.message : String(err));
|
|
122
|
+
cached = { currentVersion, latestVersion: null, updateAvailable: false, lastCheckedAt: Date.now(), releaseUrl: null, installMode: getInstallMode() };
|
|
123
|
+
return cached;
|
|
124
|
+
} finally {
|
|
125
|
+
checking = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getCachedUpdateInfo(): UpdateInfo | null {
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getPackageName(): string {
|
|
134
|
+
return PKG_NAME;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getGitHubRepo(): string {
|
|
138
|
+
return GITHUB_REPO;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function startPeriodicUpdateCheck(): void {
|
|
142
|
+
void checkForUpdate();
|
|
143
|
+
setInterval(function () {
|
|
144
|
+
void checkForUpdate();
|
|
145
|
+
}, CHECK_INTERVAL_MS);
|
|
146
|
+
}
|
package/shared/src/messages.ts
CHANGED
|
@@ -505,6 +505,15 @@ export interface PluginErrorsMessage {
|
|
|
505
505
|
type: "plugin:errors";
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
export interface UpdateCheckMessage {
|
|
509
|
+
type: "update:check";
|
|
510
|
+
force?: boolean;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export interface UpdateApplyMessage {
|
|
514
|
+
type: "update:apply";
|
|
515
|
+
}
|
|
516
|
+
|
|
508
517
|
export type ClientMessage =
|
|
509
518
|
| SessionCreateMessage
|
|
510
519
|
| SessionActivateMessage
|
|
@@ -576,7 +585,9 @@ export type ClientMessage =
|
|
|
576
585
|
| PluginUpdateMessage
|
|
577
586
|
| PluginDetailsMessage
|
|
578
587
|
| PluginDiscoverMessage
|
|
579
|
-
| PluginErrorsMessage
|
|
588
|
+
| PluginErrorsMessage
|
|
589
|
+
| UpdateCheckMessage
|
|
590
|
+
| UpdateApplyMessage;
|
|
580
591
|
|
|
581
592
|
export interface SessionListMessage {
|
|
582
593
|
type: "session:list";
|
|
@@ -971,6 +982,21 @@ export interface PluginErrorsResultMessage {
|
|
|
971
982
|
errors: PluginError[];
|
|
972
983
|
}
|
|
973
984
|
|
|
985
|
+
export interface UpdateStatusMessage {
|
|
986
|
+
type: "update:status";
|
|
987
|
+
currentVersion: string;
|
|
988
|
+
latestVersion: string | null;
|
|
989
|
+
updateAvailable: boolean;
|
|
990
|
+
releaseUrl: string | null;
|
|
991
|
+
installMode: "binary" | "npm";
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export interface UpdateApplyResultMessage {
|
|
995
|
+
type: "update:apply_result";
|
|
996
|
+
success: boolean;
|
|
997
|
+
message?: string;
|
|
998
|
+
}
|
|
999
|
+
|
|
974
1000
|
export type ServerMessage =
|
|
975
1001
|
| SessionListMessage
|
|
976
1002
|
| SessionCreatedMessage
|
|
@@ -1047,7 +1073,9 @@ export type ServerMessage =
|
|
|
1047
1073
|
| PluginUpdateResultMessage
|
|
1048
1074
|
| PluginDetailsResultMessage
|
|
1049
1075
|
| PluginDiscoverResultMessage
|
|
1050
|
-
| PluginErrorsResultMessage
|
|
1076
|
+
| PluginErrorsResultMessage
|
|
1077
|
+
| UpdateStatusMessage
|
|
1078
|
+
| UpdateApplyResultMessage;
|
|
1051
1079
|
|
|
1052
1080
|
export interface BudgetStatusMessage {
|
|
1053
1081
|
type: "budget:status";
|