History

version and sidebar

Rhys Sullivan committed 7 months ago

Commit 3ce7bca1 parent caf89fa22 files · +237 −24Browse files

Unified · Split

22 files
  1. apps/server/package.json +1 −1
  2. apps/web/package.json +1 −1
  3. apps/web/src/shell.tsx +179 −4
  4. apps/web/vite.config.ts +19 −0
  5. package.json +20 −1
  6. packages/clients/react/package.json +1 −1
  7. packages/core/api/package.json +1 −1
  8. packages/core/execution/package.json +1 −1
  9. packages/core/sdk/package.json +1 −1
  10. packages/core/storage-file/package.json +1 −1
  11. packages/hosts/mcp/package.json +1 −1
  12. packages/kernel/core/package.json +1 −1
  13. packages/kernel/ir/package.json +1 −1
  14. packages/kernel/runtime-quickjs/package.json +1 −1
  15. packages/plugins/file-secrets/package.json +1 −1
  16. packages/plugins/google-discovery/package.json +1 −1
  17. packages/plugins/graphql/package.json +1 −1
  18. packages/plugins/keychain/package.json +1 −1
  19. packages/plugins/mcp/package.json +1 −1
  20. packages/plugins/onepassword/package.json +1 −1
  21. packages/plugins/openapi/package.json +1 −1
  22. packages/ui/package.json +1 −1
apps/server/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/server",
3− "version": "0.0.0",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "exports": {
apps/web/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/web",
3− "version": "0.0.0",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "dependencies": {
apps/web/src/shell.tsx+179−4
@@ -1,5 +1,5 @@
11 import { Link, Outlet, useLocation } from "@tanstack/react-router";
2−import { useEffect, useRef, useState } from "react";
2+import { useCallback, useEffect, useRef, useState } from "react";
33 import {
44 useAtomRefresh,
55 useAtomValue,
@@ -8,6 +8,165 @@ import { · 2 unmodified lines
88 sourcesAtom,
99 toolsAtom,
1010 } from "@executor/react";
11+import { Button } from "@executor/ui/components/button";
12+
13+// ── Env ─────────────────────────────────────────────────────────────────
14+
15+type AppMetaEnv = {
16+ readonly VITE_APP_VERSION: string;
17+ readonly VITE_GITHUB_URL: string;
18+};
19+
20+const { VITE_APP_VERSION, VITE_GITHUB_URL } = (import.meta as ImportMeta & {
21+ readonly env: AppMetaEnv;
22+}).env;
23+
24+// ── Version helpers ─────────────────────────────────────────────────────
25+
26+type UpdateChannel = "latest" | "beta";
27+
28+const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags";
29+
30+type ParsedVersion = {
31+ readonly major: number;
32+ readonly minor: number;
33+ readonly patch: number;
34+ readonly prerelease: ReadonlyArray<string | number> | null;
35+};
36+
37+const semverPattern =
38+ /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
39+
40+const resolveUpdateChannel = (version: string): UpdateChannel =>
41+ version.includes("-beta.") ? "beta" : "latest";
42+
43+const parseVersion = (version: string): ParsedVersion | null => {
44+ const match = version.trim().match(semverPattern);
45+ if (!match?.groups) return null;
46+ return {
47+ major: Number(match.groups.major),
48+ minor: Number(match.groups.minor),
49+ patch: Number(match.groups.patch),
50+ prerelease: match.groups.prerelease
51+ ? match.groups.prerelease.split(".").map((id) =>
52+ /^\d+$/.test(id) ? Number(id) : id,
53+ )
54+ : null,
55+ };
56+};
57+
58+const comparePrereleaseIdentifiers = (
59+ left: ReadonlyArray<string | number> | null,
60+ right: ReadonlyArray<string | number> | null,
61+): number => {
62+ if (left === null && right === null) return 0;
63+ if (left === null) return 1;
64+ if (right === null) return -1;
65+ const max = Math.max(left.length, right.length);
66+ for (let i = 0; i < max; i++) {
67+ const l = left[i];
68+ const r = right[i];
69+ if (l === undefined) return -1;
70+ if (r === undefined) return 1;
71+ if (l === r) continue;
72+ if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
73+ if (typeof l === "number") return -1;
74+ if (typeof r === "number") return 1;
75+ return l < r ? -1 : 1;
76+ }
77+ return 0;
78+};
79+
80+const compareVersions = (left: string, right: string): number | null => {
81+ const lv = parseVersion(left);
82+ const rv = parseVersion(right);
83+ if (!lv || !rv) return null;
84+ if (lv.major !== rv.major) return lv.major < rv.major ? -1 : 1;
85+ if (lv.minor !== rv.minor) return lv.minor < rv.minor ? -1 : 1;
86+ if (lv.patch !== rv.patch) return lv.patch < rv.patch ? -1 : 1;
87+ return comparePrereleaseIdentifiers(lv.prerelease, rv.prerelease);
88+};
89+
90+// ── useLatestVersion ────────────────────────────────────────────────────
91+
92+function useLatestVersion(currentVersion: string) {
93+ const channel = resolveUpdateChannel(currentVersion);
94+ const [latestVersion, setLatestVersion] = useState<string | null>(null);
95+
96+ useEffect(() => {
97+ let cancelled = false;
98+ fetch(EXECUTOR_DIST_TAGS_PATH)
99+ .then(async (res) => {
100+ if (!res.ok) throw new Error(`Failed to load dist tags: ${res.status}`);
101+ return res.json() as Promise<Partial<Record<UpdateChannel, string>>>;
102+ })
103+ .then((data) => {
104+ if (!cancelled) setLatestVersion(data[channel] ?? null);
105+ })
106+ .catch(() => {});
107+ return () => { cancelled = true; };
108+ }, [channel]);
109+
110+ const updateAvailable =
111+ latestVersion !== null && compareVersions(currentVersion, latestVersion) === -1;
112+
113+ return { latestVersion, updateAvailable, channel };
114+}
115+
116+// ── UpdateCard ──────────────────────────────────────────────────────────
117+
118+function UpdateCard(props: { latestVersion: string; channel: UpdateChannel }) {
119+ const command = `npm i -g executor@${props.channel}`;
120+ const [copied, setCopied] = useState(false);
121+
122+ const handleCopy = useCallback(() => {
123+ void navigator.clipboard.writeText(command).then(() => {
124+ setCopied(true);
125+ setTimeout(() => setCopied(false), 1500);
126+ });
127+ }, [command]);
128+
129+ return (
130+ <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3">
131+ <div className="flex items-center gap-2">
132+ <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15">
133+ <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary">
134+ <path d="M8 3v7M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
135+ <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
136+ </svg>
137+ </div>
138+ <div className="min-w-0">
139+ <p className="text-[11px] font-semibold text-foreground">Update available</p>
140+ <p className="text-[10px] text-muted-foreground">
141+ v{props.latestVersion}
142+ </p>
143+ </div>
144+ </div>
145+ <Button
146+ type="button"
147+ variant="outline"
148+ onClick={handleCopy}
149+ className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80"
150+ >
151+ <code className="truncate font-mono text-[10px] text-sidebar-foreground">
152+ {command}
153+ </code>
154+ <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground">
155+ {copied ? (
156+ <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary">
157+ <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
158+ </svg>
159+ ) : (
160+ <svg viewBox="0 0 16 16" fill="none" className="size-3">
161+ <rect x="5" y="5" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
162+ <path d="M3 11V3.5A.5.5 0 013.5 3H11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
163+ </svg>
164+ )}
165+ </span>
166+ </Button>
167+ </div>
168+ );
169+}
11170
12171 // ── NavItem ──────────────────────────────────────────────────────────────
13172
@@ -83,6 +242,9 @@ function SidebarContent(props: { · 69 unmodified lines
83242 pathname: string;
84243 onNavigate?: () => void;
85244 showBrand?: boolean;
245+ updateAvailable: boolean;
246+ latestVersion: string | null;
247+ channel: UpdateChannel;
86248 }) {
87249 const isHome = props.pathname === "/" || props.pathname === "/tools";
88250 const isSecrets = props.pathname === "/secrets";
@@ -141,12 +303,16 @@ function SidebarContent(props: { · 52 unmodified lines
141303 <SourceList pathname={props.pathname} onNavigate={props.onNavigate} />
142304 </nav>
143305
306+ {props.updateAvailable && props.latestVersion && (
307+ <UpdateCard latestVersion={props.latestVersion} channel={props.channel} />
308+ )}
309+
144310 {/* Footer */}
145311 <div className="shrink-0 border-t border-sidebar-border px-4 py-2.5">
146312 <div className="flex items-center justify-between text-[10px] leading-none">
147− <span className="text-muted-foreground/70 tabular-nums">v4.0.0-dev</span>
313+ <span className="text-muted-foreground/70 tabular-nums">v{VITE_APP_VERSION}</span>
148314 <a
149− href="https://github.com"
315+ href={VITE_GITHUB_URL}
150316 target="_blank"
151317 rel="noreferrer"
152318 className="text-muted-foreground/70 transition-colors hover:text-foreground"
@@ -172,6 +338,7 @@ export function Shell() { · 19 unmodified lines
172338 const refreshSourceTools = useAtomRefresh(
173339 sourceToolsAtom(currentSourceId ?? "__runtime__"),
174340 );
341+ const { latestVersion, updateAvailable, channel } = useLatestVersion(VITE_APP_VERSION);
175342 const lastPathname = useRef(pathname);
176343 const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
177344 if (lastPathname.current !== pathname) {
@@ -213,7 +380,12 @@ export function Shell() { · 35 unmodified lines
213380 <div className="flex h-screen overflow-hidden">
214381 {/* Desktop sidebar */}
215382 <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">
216− <SidebarContent pathname={pathname} />
383+ <SidebarContent
384+ pathname={pathname}
385+ updateAvailable={updateAvailable}
386+ latestVersion={latestVersion}
387+ channel={channel}
388+ />
217389 </aside>
218390
219391 {/* Mobile sidebar overlay */}
@@ -250,6 +422,9 @@ export function Shell() { · 30 unmodified lines
250422 pathname={pathname}
251423 onNavigate={() => setMobileSidebarOpen(false)}
252424 showBrand={false}
425+ updateAvailable={updateAvailable}
426+ latestVersion={latestVersion}
427+ channel={channel}
253428 />
254429 </div>
255430 </div>
apps/web/vite.config.ts+19−0
@@ -1,3 +1,4 @@
1+import { readFileSync } from "node:fs";
12 import { resolve } from "node:path";
23 import { defineConfig } from "vite";
34 import react from "@vitejs/plugin-react";
@@ -77,7 +78,25 @@ const sendWebResponse = async ( · 73 unmodified lines
7778 }
7879 };
7980
81+const rootPackage = JSON.parse(
82+ readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
83+) as { version: string; homepage?: string; repository?: string | { url?: string } };
84+
85+const repositoryUrl =
86+ typeof rootPackage.repository === "string"
87+ ? rootPackage.repository
88+ : rootPackage.repository?.url;
89+
90+const EXECUTOR_VERSION = rootPackage.version;
91+const EXECUTOR_GITHUB_URL = (rootPackage.homepage ?? repositoryUrl ?? "https://github.com/RhysSullivan/executor")
92+ .replace(/^git\+/, "")
93+ .replace(/\.git$/, "");
94+
8095 export default defineConfig({
96+ define: {
97+ "import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION),
98+ "import.meta.env.VITE_GITHUB_URL": JSON.stringify(EXECUTOR_GITHUB_URL),
99+ },
81100 plugins: [
82101 react(),
83102 tailwindcss(),
package.json+20−1
@@ -1,5 +1,24 @@
11 {
2− "name": "v4-executor",
2+ "name": "executor",
3+ "version": "1.4.0",
4+ "description": "Local AI executor with a CLI, local API server, and web UI.",
5+ "keywords": [
6+ "executor",
7+ "ai",
8+ "agent",
9+ "cli",
10+ "automation",
11+ "local-first"
12+ ],
13+ "homepage": "https://github.com/RhysSullivan/executor",
14+ "bugs": {
15+ "url": "https://github.com/RhysSullivan/executor/issues"
16+ },
17+ "repository": {
18+ "type": "git",
19+ "url": "git+https://github.com/RhysSullivan/executor.git"
20+ },
21+ "license": "MIT",
322 "private": true,
423 "type": "module",
524 "packageManager": "bun@1.2.22",
packages/clients/react/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/react",
3− "version": "0.0.0",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "exports": {
packages/core/api/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/api",
3− "version": "0.0.0",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "exports": {
packages/core/execution/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/execution",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/core/sdk/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/sdk",
3− "version": "0.0.1",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "exports": {
packages/core/storage-file/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/storage-file",
3− "version": "0.0.0",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "exports": {
packages/hosts/mcp/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/host-mcp",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/kernel/core/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/codemode-core",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/kernel/ir/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/ir",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/kernel/runtime-quickjs/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/runtime-quickjs",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/plugins/file-secrets/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-file-secrets",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/plugins/google-discovery/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-google-discovery",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
packages/plugins/graphql/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-graphql",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
packages/plugins/keychain/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-keychain",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/index.ts"
88 },
packages/plugins/mcp/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-mcp",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
packages/plugins/onepassword/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-onepassword",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
packages/plugins/openapi/package.json+1−1
@@ -2,7 +2,7 @@ · 1 unmodified line
22 "name": "@executor/plugin-openapi",
33 "private": true,
44 "type": "module",
5− "version": "0.0.0",
5+ "version": "1.4.0",
66 "exports": {
77 ".": "./src/sdk/index.ts",
88 "./api": "./src/api/index.ts",
packages/ui/package.json+1−1
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor/ui",
3− "version": "0.0.0",
3+ "version": "1.4.0",
44 "private": true,
55 "type": "module",
66 "exports": {