Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfd397803f | ||
|
|
267f1592c4 | ||
|
|
668ac7fa88 | ||
|
|
43a476e967 | ||
|
|
adbfab5c25 | ||
|
|
02f1284f7f | ||
|
|
a014ce555a | ||
|
|
db3c13c463 | ||
|
|
7c0bf382ba | ||
|
|
6e9c5a88b4 | ||
|
|
0bf22a323f | ||
|
|
cc997576cf | ||
|
|
05f193df7b | ||
|
|
c9b5bb1b7a | ||
|
|
ba1013cd35 | ||
|
|
ec6428702b | ||
|
|
e08ebb2057 | ||
|
|
9683f90f7e | ||
|
|
06cb986aa6 | ||
|
|
a85c2f1700 | ||
|
|
bd2a0d1bec | ||
|
|
df9722cd16 | ||
|
|
dffa4907ec | ||
|
|
e567d35438 | ||
|
|
62f52fc534 | ||
|
|
69f221942c | ||
|
|
7749225f71 | ||
|
|
ae322c53cc |
43
.github/workflows/release-ui.yml
vendored
Normal file
43
.github/workflows/release-ui.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 20
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-ui:
|
||||||
|
if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --workspaces --include=optional
|
||||||
|
|
||||||
|
- name: Install Cloudflare worker deps
|
||||||
|
run: npm ci
|
||||||
|
working-directory: packages/cloudflare
|
||||||
|
|
||||||
|
- name: Build UI
|
||||||
|
run: npm run build --workspace @codenomad/ui
|
||||||
|
|
||||||
|
- name: Publish UI zip + update manifest
|
||||||
|
working-directory: packages/cloudflare
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
CODENOMAD_R2_BUCKET: ${{ vars.CODENOMAD_R2_BUCKET }}
|
||||||
|
run: npm run release:ui
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,4 +7,9 @@ release/
|
|||||||
.electron-vite/
|
.electron-vite/
|
||||||
out/
|
out/
|
||||||
.dir-locals.el
|
.dir-locals.el
|
||||||
.opencode/bashOutputs/
|
.opencode/bashOutputs/
|
||||||
|
|
||||||
|
# Local runtime artifacts
|
||||||
|
.codenomad/
|
||||||
|
.tmp/
|
||||||
|
packages/cloudflare/.wrangler/
|
||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
"google-auth-library": "^10.5.0"
|
"google-auth-library": "^10.5.0"
|
||||||
@@ -1632,7 +1632,6 @@
|
|||||||
"version": "2.10.3",
|
"version": "2.10.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -2271,7 +2270,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/buffer-crc32": {
|
"node_modules/buffer-crc32": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
@@ -3674,7 +3672,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/fd-slicer": {
|
"node_modules/fd-slicer": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
@@ -5352,7 +5349,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/pend": {
|
"node_modules/pend": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
@@ -7324,7 +7320,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/yauzl": {
|
"node_modules/yauzl": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "~0.2.3",
|
"buffer-crc32": "~0.2.3",
|
||||||
@@ -7389,7 +7384,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
"@neuralnomads/codenomad": "file:../server"
|
"@neuralnomads/codenomad": "file:../server"
|
||||||
@@ -7423,7 +7418,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
@@ -7433,12 +7428,14 @@
|
|||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"undici": "^6.19.8",
|
"undici": "^6.19.8",
|
||||||
|
"yauzl": "^2.10.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"codenomad": "dist/bin.js"
|
"codenomad": "dist/bin.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/yauzl": "^2.10.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
@@ -7458,14 +7455,14 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
|
|||||||
1
packages/cloudflare/.gitignore
vendored
Normal file
1
packages/cloudflare/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
1515
packages/cloudflare/package-lock.json
generated
Normal file
1515
packages/cloudflare/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
packages/cloudflare/package.json
Normal file
14
packages/cloudflare/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@codenomad/ui-host-worker",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build:manifest": "node ./scripts/build-manifest.mjs",
|
||||||
|
"release:ui": "node ./scripts/release-ui.mjs",
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"deploy": "wrangler deploy"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"wrangler": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/cloudflare/release-config.json
Normal file
4
packages/cloudflare/release-config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"minServerVersion": "0.7.5",
|
||||||
|
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||||
|
}
|
||||||
75
packages/cloudflare/scripts/build-manifest.mjs
Normal file
75
packages/cloudflare/scripts/build-manifest.mjs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createHash } from "crypto"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..")
|
||||||
|
const repoRoot = path.resolve(root, "..", "..")
|
||||||
|
|
||||||
|
const releaseConfigPath = path.join(root, "release-config.json")
|
||||||
|
const uiPackageJsonPath = path.join(repoRoot, "packages/ui/package.json")
|
||||||
|
const serverPackageJsonPath = path.join(repoRoot, "packages/server/package.json")
|
||||||
|
|
||||||
|
const distDir = path.join(root, "dist")
|
||||||
|
const manifestPath = path.join(distDir, "version.json")
|
||||||
|
|
||||||
|
const args = new Set(process.argv.slice(2))
|
||||||
|
|
||||||
|
function getArgValue(flag) {
|
||||||
|
const idx = process.argv.indexOf(flag)
|
||||||
|
if (idx === -1) return null
|
||||||
|
return process.argv[idx + 1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipPath = getArgValue("--zip")
|
||||||
|
|
||||||
|
if (!zipPath) {
|
||||||
|
console.error("Usage: node scripts/build-manifest.mjs --zip <path-to-ui-zip>")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedZipPath = path.resolve(process.cwd(), zipPath)
|
||||||
|
if (!fs.existsSync(resolvedZipPath)) {
|
||||||
|
console.error(`Zip not found: ${resolvedZipPath}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseConfig = JSON.parse(fs.readFileSync(releaseConfigPath, "utf-8"))
|
||||||
|
const uiPackageJson = JSON.parse(fs.readFileSync(uiPackageJsonPath, "utf-8"))
|
||||||
|
const serverPackageJson = JSON.parse(fs.readFileSync(serverPackageJsonPath, "utf-8"))
|
||||||
|
|
||||||
|
const bucket = process.env.CODENOMAD_R2_BUCKET
|
||||||
|
|
||||||
|
if (!bucket) {
|
||||||
|
console.error("Missing env var: CODENOMAD_R2_BUCKET")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiVersion = uiPackageJson.version
|
||||||
|
const serverVersion = serverPackageJson.version
|
||||||
|
|
||||||
|
if (!uiVersion || !serverVersion) {
|
||||||
|
console.error("Missing version fields in package.json")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha256 = createHash("sha256").update(fs.readFileSync(resolvedZipPath)).digest("hex")
|
||||||
|
|
||||||
|
const uiPackageURL = `https://download.codenomad.neuralnomads.ai/ui/ui-${uiVersion}.zip`
|
||||||
|
|
||||||
|
const manifest = {
|
||||||
|
minServerVersion: releaseConfig.minServerVersion,
|
||||||
|
latestUIVersion: uiVersion,
|
||||||
|
uiPackageURL,
|
||||||
|
sha256,
|
||||||
|
latestServerVersion: serverVersion,
|
||||||
|
latestServerUrl: releaseConfig.latestServerUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(distDir, { recursive: true })
|
||||||
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8")
|
||||||
|
|
||||||
|
console.log(`Wrote ${manifestPath}`)
|
||||||
81
packages/cloudflare/scripts/release-ui.mjs
Normal file
81
packages/cloudflare/scripts/release-ui.mjs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { execFileSync } from "child_process"
|
||||||
|
import fs from "fs"
|
||||||
|
import os from "os"
|
||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..")
|
||||||
|
const repoRoot = path.resolve(root, "..", "..")
|
||||||
|
|
||||||
|
const r2Bucket = process.env.CODENOMAD_R2_BUCKET
|
||||||
|
|
||||||
|
if (!r2Bucket) {
|
||||||
|
console.error("Missing env var: CODENOMAD_R2_BUCKET")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiPackageJsonPath = path.join(repoRoot, "packages/ui/package.json")
|
||||||
|
const uiPackageJson = JSON.parse(fs.readFileSync(uiPackageJsonPath, "utf-8"))
|
||||||
|
const uiVersion = uiPackageJson.version
|
||||||
|
|
||||||
|
if (!uiVersion) {
|
||||||
|
console.error("Missing packages/ui/package.json version")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiBuildDir = path.join(repoRoot, "packages/ui/src/renderer/dist")
|
||||||
|
if (!fs.existsSync(uiBuildDir)) {
|
||||||
|
console.error(`Missing UI build dir: ${uiBuildDir}. Run UI build first.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-ui-release-"))
|
||||||
|
const zipPath = path.join(tmpDir, `ui-${uiVersion}.zip`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Zip the CONTENTS of the dist dir (so index.html is at zip root).
|
||||||
|
execFileSync("/usr/bin/zip", ["-q", "-r", zipPath, "."], { cwd: uiBuildDir, stdio: "inherit" })
|
||||||
|
|
||||||
|
// Upload to R2.
|
||||||
|
const objectKey = `ui/ui-${uiVersion}.zip`
|
||||||
|
console.log(`[release-ui] Uploading ${zipPath} -> r2://${r2Bucket}/${objectKey}`)
|
||||||
|
|
||||||
|
execFileSync(
|
||||||
|
"npx",
|
||||||
|
["wrangler", "r2", "object", "put", "--remote", `${r2Bucket}/${objectKey}`, "--file", zipPath],
|
||||||
|
{ cwd: root, stdio: "inherit" },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate version.json into packages/cloudflare/dist
|
||||||
|
console.log("[release-ui] Generating version.json")
|
||||||
|
execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[path.join(root, "scripts/build-manifest.mjs"), "--zip", zipPath],
|
||||||
|
{
|
||||||
|
cwd: root,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CODENOMAD_R2_BUCKET: r2Bucket,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log("[release-ui] Deploying worker")
|
||||||
|
execFileSync("npx", ["wrangler", "deploy"], {
|
||||||
|
cwd: root,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN,
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("[release-ui] Done")
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
29
packages/cloudflare/src/index.ts
Normal file
29
packages/cloudflare/src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export interface Env {
|
||||||
|
ASSETS: { fetch: (request: Request) => Promise<Response> }
|
||||||
|
}
|
||||||
|
|
||||||
|
function withHeader(response: Response, key: string, value: string): Response {
|
||||||
|
const headers = new Headers(response.headers)
|
||||||
|
headers.set(key, value)
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
|
||||||
|
if (url.pathname === "/version.json") {
|
||||||
|
const assetResponse = await env.ASSETS.fetch(request)
|
||||||
|
|
||||||
|
// Ensure this stays fresh; the server uses it on startup.
|
||||||
|
const withCache = withHeader(assetResponse, "Cache-Control", "no-cache")
|
||||||
|
return withHeader(withCache, "Content-Type", "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Not found", { status: 404 })
|
||||||
|
},
|
||||||
|
}
|
||||||
15
packages/cloudflare/wrangler.toml
Normal file
15
packages/cloudflare/wrangler.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name = "codenomad-ui-host"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2026-01-22"
|
||||||
|
|
||||||
|
# Custom domain for the manifest host.
|
||||||
|
# Note: Custom domains apply to all paths on the hostname.
|
||||||
|
[[routes]]
|
||||||
|
pattern = "ui.codenomad.neuralnomads.ai"
|
||||||
|
custom_domain = true
|
||||||
|
|
||||||
|
[assets]
|
||||||
|
directory = "./dist"
|
||||||
|
binding = "ASSETS"
|
||||||
|
not_found_handling = "404-page"
|
||||||
|
run_worker_first = ["/version.json"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Neural Nomads",
|
"name": "Neural Nomads",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.1.12"
|
"@opencode-ai/plugin": "1.1.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
774
packages/server/package-lock.json
generated
774
packages/server/package-lock.json
generated
@@ -1,20 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
"@fastify/static": "^7.0.4",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
|
"fuzzysort": "^2.0.4",
|
||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
|
"undici": "^6.19.8",
|
||||||
|
"yauzl": "^2.10.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"codenomad": "dist/bin.js"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/yauzl": "^2.10.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
@@ -475,6 +485,15 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/accept-negotiator": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/ajv-compiler": {
|
"node_modules/@fastify/ajv-compiler": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz",
|
||||||
@@ -486,6 +505,15 @@
|
|||||||
"fast-uri": "^2.0.0"
|
"fast-uri": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/busboy": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/cors": {
|
"node_modules/@fastify/cors": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz",
|
||||||
@@ -520,6 +548,77 @@
|
|||||||
"fast-deep-equal": "^3.1.3"
|
"fast-deep-equal": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/reply-from": {
|
||||||
|
"version": "9.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.8.0.tgz",
|
||||||
|
"integrity": "sha512-bPNVaFhEeNI0Lyl6404YZaPFokudCplidE3QoOcr78yOy6H9sYw97p5KPYvY/NJNUHfFtvxOaSAHnK+YSiv/Mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/error": "^3.0.0",
|
||||||
|
"end-of-stream": "^1.4.4",
|
||||||
|
"fast-content-type-parse": "^1.1.0",
|
||||||
|
"fast-querystring": "^1.0.0",
|
||||||
|
"fastify-plugin": "^4.0.0",
|
||||||
|
"toad-cache": "^3.7.0",
|
||||||
|
"undici": "^5.19.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/reply-from/node_modules/undici": {
|
||||||
|
"version": "5.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
||||||
|
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/busboy": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/send": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lukeed/ms": "^2.0.1",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"fast-decode-uri-component": "^1.0.1",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"mime": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/static": {
|
||||||
|
"version": "7.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz",
|
||||||
|
"integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/accept-negotiator": "^1.0.0",
|
||||||
|
"@fastify/send": "^2.0.0",
|
||||||
|
"content-disposition": "^0.5.3",
|
||||||
|
"fastify-plugin": "^4.0.0",
|
||||||
|
"fastq": "^1.17.0",
|
||||||
|
"glob": "^10.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^5.1.2",
|
||||||
|
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||||
|
"strip-ansi": "^7.0.1",
|
||||||
|
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||||
|
"wrap-ansi": "^8.1.0",
|
||||||
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@@ -548,12 +647,31 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lukeed/ms": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pinojs/redact": {
|
"node_modules/@pinojs/redact": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@pkgjs/parseargs": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -593,6 +711,16 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/yauzl": {
|
||||||
|
"version": "2.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/abstract-logging": {
|
"node_modules/abstract-logging": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||||
@@ -674,6 +802,30 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
@@ -700,6 +852,48 @@
|
|||||||
"fastq": "^1.17.1"
|
"fastq": "^1.17.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "12.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
@@ -709,6 +903,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
@@ -725,6 +931,48 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "src/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cross-spawn": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.1.0",
|
||||||
|
"shebang-command": "^2.0.0",
|
||||||
|
"which": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@@ -735,6 +983,27 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eastasianwidth": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -777,6 +1046,12 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-content-type-parse": {
|
"node_modules/fast-content-type-parse": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
|
||||||
@@ -891,6 +1166,15 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/find-my-way": {
|
"node_modules/find-my-way": {
|
||||||
"version": "8.2.2",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz",
|
||||||
@@ -905,6 +1189,22 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/foreground-child": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
|
"signal-exit": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -929,6 +1229,12 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuzzysort": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/get-tsconfig": {
|
"node_modules/get-tsconfig": {
|
||||||
"version": "4.13.0",
|
"version": "4.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||||
@@ -942,6 +1248,48 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -951,6 +1299,36 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/isexe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/jackspeak": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-schema-ref-resolver": {
|
"node_modules/json-schema-ref-resolver": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz",
|
||||||
@@ -977,6 +1355,12 @@
|
|||||||
"set-cookie-parser": "^2.4.1"
|
"set-cookie-parser": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
@@ -984,6 +1368,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mnemonist": {
|
"node_modules/mnemonist": {
|
||||||
"version": "0.39.6",
|
"version": "0.39.6",
|
||||||
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz",
|
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz",
|
||||||
@@ -1008,6 +1428,52 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/package-json-from-dist": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
|
"license": "BlueOak-1.0.0"
|
||||||
|
},
|
||||||
|
"node_modules/path-key": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^10.2.0",
|
||||||
|
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pino": {
|
"node_modules/pino": {
|
||||||
"version": "9.14.0",
|
"version": "9.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
|
||||||
@@ -1139,6 +1605,26 @@
|
|||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-regex2": {
|
"node_modules/safe-regex2": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz",
|
||||||
@@ -1181,6 +1667,45 @@
|
|||||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/shebang-command": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"shebang-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shebang-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sonic-boom": {
|
"node_modules/sonic-boom": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
||||||
@@ -1199,6 +1724,111 @@
|
|||||||
"node": ">= 10.x"
|
"node": ">= 10.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eastasianwidth": "^0.2.0",
|
||||||
|
"emoji-regex": "^9.2.2",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs": {
|
||||||
|
"name": "string-width",
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi-cjs": {
|
||||||
|
"name": "strip-ansi",
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thread-stream": {
|
"node_modules/thread-stream": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||||
@@ -1217,6 +1847,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-node": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
@@ -1296,6 +1935,15 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||||
|
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
@@ -1310,6 +1958,128 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/which": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/node-which"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.1.0",
|
||||||
|
"string-width": "^5.0.1",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs": {
|
||||||
|
"name": "wrap-ansi",
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Neural Nomads",
|
"name": "Neural Nomads",
|
||||||
@@ -32,9 +32,11 @@
|
|||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"undici": "^6.19.8",
|
"undici": "^6.19.8",
|
||||||
|
"yauzl": "^2.10.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/yauzl": "^2.10.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ export type WorkspaceEventType =
|
|||||||
| "instance.dataChanged"
|
| "instance.dataChanged"
|
||||||
| "instance.event"
|
| "instance.event"
|
||||||
| "instance.eventStatus"
|
| "instance.eventStatus"
|
||||||
| "app.releaseAvailable"
|
|
||||||
|
|
||||||
export type WorkspaceEventPayload =
|
export type WorkspaceEventPayload =
|
||||||
| { type: "workspace.created"; workspace: WorkspaceDescriptor }
|
| { type: "workspace.created"; workspace: WorkspaceDescriptor }
|
||||||
@@ -180,7 +179,6 @@ export type WorkspaceEventPayload =
|
|||||||
| { type: "instance.dataChanged"; instanceId: string; data: InstanceData }
|
| { type: "instance.dataChanged"; instanceId: string; data: InstanceData }
|
||||||
| { type: "instance.event"; instanceId: string; event: InstanceStreamEvent }
|
| { type: "instance.event"; instanceId: string; event: InstanceStreamEvent }
|
||||||
| { type: "instance.eventStatus"; instanceId: string; status: InstanceStreamStatus; reason?: string }
|
| { type: "instance.eventStatus"; instanceId: string; status: InstanceStreamStatus; reason?: string }
|
||||||
| { type: "app.releaseAvailable"; release: LatestReleaseInfo }
|
|
||||||
|
|
||||||
export interface NetworkAddress {
|
export interface NetworkAddress {
|
||||||
ip: string
|
ip: string
|
||||||
@@ -198,6 +196,19 @@ export interface LatestReleaseInfo {
|
|||||||
notes?: string
|
notes?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UiMeta {
|
||||||
|
version?: string
|
||||||
|
source: "bundled" | "downloaded" | "previous" | "override" | "dev-proxy" | "missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupportMeta {
|
||||||
|
supported: boolean
|
||||||
|
message?: string
|
||||||
|
minServerVersion?: string
|
||||||
|
latestServerVersion?: string
|
||||||
|
latestServerUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerMeta {
|
export interface ServerMeta {
|
||||||
/** Base URL clients should target for REST calls (useful for Electron embedding). */
|
/** Base URL clients should target for REST calls (useful for Electron embedding). */
|
||||||
httpBaseUrl: string
|
httpBaseUrl: string
|
||||||
@@ -215,8 +226,9 @@ export interface ServerMeta {
|
|||||||
workspaceRoot: string
|
workspaceRoot: string
|
||||||
/** Reachable addresses for this server, external first. */
|
/** Reachable addresses for this server, external first. */
|
||||||
addresses: NetworkAddress[]
|
addresses: NetworkAddress[]
|
||||||
/** Optional metadata about the most recent public release. */
|
serverVersion?: string
|
||||||
latestRelease?: LatestReleaseInfo
|
ui?: UiMeta
|
||||||
|
support?: SupportMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BackgroundProcessStatus = "running" | "stopped" | "error"
|
export type BackgroundProcessStatus = "running" | "stopped" | "error"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn, type ChildProcess } from "child_process"
|
import { spawn, spawnSync, type ChildProcess } from "child_process"
|
||||||
import { createWriteStream, existsSync, promises as fs } from "fs"
|
import { createWriteStream, existsSync, promises as fs } from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
@@ -60,10 +60,13 @@ export class BackgroundProcessManager {
|
|||||||
|
|
||||||
const outputStream = createWriteStream(outputPath, { flags: "a" })
|
const outputStream = createWriteStream(outputPath, { flags: "a" })
|
||||||
|
|
||||||
const child = spawn("bash", ["-c", command], {
|
const { shellCommand, shellArgs, spawnOptions } = this.buildShellSpawn(command)
|
||||||
|
|
||||||
|
const child = spawn(shellCommand, shellArgs, {
|
||||||
cwd: workspace.path,
|
cwd: workspace.path,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
detached: process.platform !== "win32",
|
detached: process.platform !== "win32",
|
||||||
|
...spawnOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
child.on("exit", () => {
|
child.on("exit", () => {
|
||||||
@@ -274,7 +277,15 @@ export class BackgroundProcessManager {
|
|||||||
const pid = child.pid
|
const pid = child.pid
|
||||||
if (!pid) return
|
if (!pid) return
|
||||||
|
|
||||||
if (process.platform !== "win32") {
|
if (process.platform === "win32") {
|
||||||
|
const args = this.buildWindowsTaskkillArgs(pid, signal)
|
||||||
|
try {
|
||||||
|
spawnSync("taskkill", args, { stdio: "ignore" })
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
// Fall back to killing the direct child.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
process.kill(-pid, signal)
|
process.kill(-pid, signal)
|
||||||
return
|
return
|
||||||
@@ -321,6 +332,30 @@ export class BackgroundProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private buildShellSpawn(command: string): { shellCommand: string; shellArgs: string[]; spawnOptions?: Record<string, unknown> } {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
const comspec = process.env.ComSpec || "cmd.exe"
|
||||||
|
return {
|
||||||
|
shellCommand: comspec,
|
||||||
|
shellArgs: ["/d", "/s", "/c", command],
|
||||||
|
spawnOptions: { windowsVerbatimArguments: true },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep bash for macOS/Linux.
|
||||||
|
return { shellCommand: "bash", shellArgs: ["-c", command] }
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildWindowsTaskkillArgs(pid: number, signal: NodeJS.Signals): string[] {
|
||||||
|
// Default to graceful termination (no /F), then force kill when we escalate.
|
||||||
|
const force = signal === "SIGKILL"
|
||||||
|
const args = ["/PID", String(pid), "/T"]
|
||||||
|
if (force) {
|
||||||
|
args.push("/F")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
private statusFromExit(code: number | null): BackgroundProcessStatus {
|
private statusFromExit(code: number | null): BackgroundProcessStatus {
|
||||||
if (code === null) return "stopped"
|
if (code === null) return "stopped"
|
||||||
if (code === 0) return "stopped"
|
if (code === 0) return "stopped"
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {
|
|||||||
BinaryUpdateRequest,
|
BinaryUpdateRequest,
|
||||||
BinaryValidationResult,
|
BinaryValidationResult,
|
||||||
} from "../api-types"
|
} from "../api-types"
|
||||||
|
import { spawnSync } from "child_process"
|
||||||
import { ConfigStore } from "./store"
|
import { ConfigStore } from "./store"
|
||||||
import { EventBus } from "../events/bus"
|
import { EventBus } from "../events/bus"
|
||||||
import type { ConfigFile } from "./schema"
|
import type { ConfigFile } from "./schema"
|
||||||
import { Logger } from "../logger"
|
import { Logger } from "../logger"
|
||||||
|
import { buildSpawnSpec } from "../workspaces/runtime"
|
||||||
|
|
||||||
export class BinaryRegistry {
|
export class BinaryRegistry {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -135,8 +137,42 @@ export class BinaryRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private validateRecord(record: BinaryRecord): BinaryValidationResult {
|
private validateRecord(record: BinaryRecord): BinaryValidationResult {
|
||||||
// TODO: call actual binary -v check.
|
const inputPath = record.path
|
||||||
return { valid: true, version: record.version }
|
if (!inputPath) {
|
||||||
|
return { valid: false, error: "Missing binary path" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const spec = buildSpawnSpec(inputPath, ["--version"])
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = spawnSync(spec.command, spec.args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
windowsVerbatimArguments: Boolean((spec.options as { windowsVerbatimArguments?: boolean }).windowsVerbatimArguments),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return { valid: false, error: result.error.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const stderr = result.stderr?.trim()
|
||||||
|
const stdout = result.stdout?.trim()
|
||||||
|
const combined = stderr || stdout
|
||||||
|
const error = combined ? `Exited with code ${result.status}: ${combined}` : `Exited with code ${result.status}`
|
||||||
|
return { valid: false, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdout = (result.stdout ?? "").trim()
|
||||||
|
const firstLine = stdout.split(/\r?\n/).find((line) => line.trim().length > 0)
|
||||||
|
const normalized = firstLine?.trim()
|
||||||
|
|
||||||
|
const versionMatch = normalized?.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/)
|
||||||
|
const version = versionMatch?.[1]
|
||||||
|
|
||||||
|
return { valid: true, version }
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: error instanceof Error ? error.message : String(error) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildFallbackRecord(path: string): BinaryRecord {
|
private buildFallbackRecord(path: string): BinaryRecord {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export class EventBus extends EventEmitter {
|
|||||||
this.on("instance.dataChanged", handler)
|
this.on("instance.dataChanged", handler)
|
||||||
this.on("instance.event", handler)
|
this.on("instance.event", handler)
|
||||||
this.on("instance.eventStatus", handler)
|
this.on("instance.eventStatus", handler)
|
||||||
this.on("app.releaseAvailable", handler)
|
|
||||||
return () => {
|
return () => {
|
||||||
this.off("workspace.created", handler)
|
this.off("workspace.created", handler)
|
||||||
this.off("workspace.started", handler)
|
this.off("workspace.started", handler)
|
||||||
@@ -41,7 +40,6 @@ export class EventBus extends EventEmitter {
|
|||||||
this.off("instance.dataChanged", handler)
|
this.off("instance.dataChanged", handler)
|
||||||
this.off("instance.event", handler)
|
this.off("instance.event", handler)
|
||||||
this.off("instance.eventStatus", handler)
|
this.off("instance.eventStatus", handler)
|
||||||
this.off("app.releaseAvailable", handler)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { InstanceStore } from "./storage/instance-store"
|
|||||||
import { InstanceEventBridge } from "./workspaces/instance-events"
|
import { InstanceEventBridge } from "./workspaces/instance-events"
|
||||||
import { createLogger } from "./logger"
|
import { createLogger } from "./logger"
|
||||||
import { launchInBrowser } from "./launcher"
|
import { launchInBrowser } from "./launcher"
|
||||||
import { startReleaseMonitor } from "./releases/release-monitor"
|
import { resolveUi } from "./ui/remote-ui"
|
||||||
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
||||||
|
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
@@ -37,6 +37,9 @@ interface CliOptions {
|
|||||||
logDestination?: string
|
logDestination?: string
|
||||||
uiStaticDir: string
|
uiStaticDir: string
|
||||||
uiDevServer?: string
|
uiDevServer?: string
|
||||||
|
uiAutoUpdate: boolean
|
||||||
|
uiNoUpdate: boolean
|
||||||
|
uiManifestUrl?: string
|
||||||
launch: boolean
|
launch: boolean
|
||||||
authUsername: string
|
authUsername: string
|
||||||
authPassword?: string
|
authPassword?: string
|
||||||
@@ -66,6 +69,9 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
new Option("--ui-dir <path>", "Directory containing the built UI bundle").env("CLI_UI_DIR").default(DEFAULT_UI_STATIC_DIR),
|
new Option("--ui-dir <path>", "Directory containing the built UI bundle").env("CLI_UI_DIR").default(DEFAULT_UI_STATIC_DIR),
|
||||||
)
|
)
|
||||||
.addOption(new Option("--ui-dev-server <url>", "Proxy UI requests to a running dev server").env("CLI_UI_DEV_SERVER"))
|
.addOption(new Option("--ui-dev-server <url>", "Proxy UI requests to a running dev server").env("CLI_UI_DEV_SERVER"))
|
||||||
|
.addOption(new Option("--ui-no-update", "Disable remote UI updates").env("CLI_UI_NO_UPDATE").default(false))
|
||||||
|
.addOption(new Option("--ui-auto-update <enabled>", "Enable remote UI updates (true|false)").env("CLI_UI_AUTO_UPDATE").default("true"))
|
||||||
|
.addOption(new Option("--ui-manifest-url <url>", "Remote UI manifest URL").env("CLI_UI_MANIFEST_URL"))
|
||||||
.addOption(new Option("--launch", "Launch the UI in a browser after start").env("CLI_LAUNCH").default(false))
|
.addOption(new Option("--launch", "Launch the UI in a browser after start").env("CLI_LAUNCH").default(false))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("--username <username>", "Username for server authentication")
|
new Option("--username <username>", "Username for server authentication")
|
||||||
@@ -91,6 +97,9 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
logDestination?: string
|
logDestination?: string
|
||||||
uiDir: string
|
uiDir: string
|
||||||
uiDevServer?: string
|
uiDevServer?: string
|
||||||
|
uiNoUpdate?: boolean
|
||||||
|
uiAutoUpdate?: string
|
||||||
|
uiManifestUrl?: string
|
||||||
launch?: boolean
|
launch?: boolean
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
@@ -101,6 +110,9 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
|
|
||||||
const normalizedHost = resolveHost(parsed.host)
|
const normalizedHost = resolveHost(parsed.host)
|
||||||
|
|
||||||
|
const autoUpdateString = (parsed.uiAutoUpdate ?? "true").trim().toLowerCase()
|
||||||
|
const uiAutoUpdate = autoUpdateString === "1" || autoUpdateString === "true" || autoUpdateString === "yes"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port: parsed.port,
|
port: parsed.port,
|
||||||
host: normalizedHost,
|
host: normalizedHost,
|
||||||
@@ -111,6 +123,9 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
logDestination: parsed.logDestination,
|
logDestination: parsed.logDestination,
|
||||||
uiStaticDir: parsed.uiDir,
|
uiStaticDir: parsed.uiDir,
|
||||||
uiDevServer: parsed.uiDevServer,
|
uiDevServer: parsed.uiDevServer,
|
||||||
|
uiAutoUpdate,
|
||||||
|
uiNoUpdate: Boolean(parsed.uiNoUpdate),
|
||||||
|
uiManifestUrl: parsed.uiManifestUrl,
|
||||||
launch: Boolean(parsed.launch),
|
launch: Boolean(parsed.launch),
|
||||||
authUsername: parsed.username,
|
authUsername: parsed.username,
|
||||||
authPassword: parsed.password,
|
authPassword: parsed.password,
|
||||||
@@ -127,10 +142,22 @@ function parsePort(input: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveHost(input: string | undefined): string {
|
function resolveHost(input: string | undefined): string {
|
||||||
if (input && input.trim() === "0.0.0.0") {
|
const trimmed = input?.trim()
|
||||||
|
if (!trimmed) return DEFAULT_HOST
|
||||||
|
|
||||||
|
if (trimmed === "0.0.0.0") {
|
||||||
return "0.0.0.0"
|
return "0.0.0.0"
|
||||||
}
|
}
|
||||||
return DEFAULT_HOST
|
|
||||||
|
if (trimmed === "localhost") {
|
||||||
|
return DEFAULT_HOST
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
function programHasArg(argv: string[], flag: string): boolean {
|
||||||
|
return argv.includes(flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -149,11 +176,13 @@ async function main() {
|
|||||||
|
|
||||||
const eventBus = new EventBus(eventLogger)
|
const eventBus = new EventBus(eventLogger)
|
||||||
|
|
||||||
|
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
|
|
||||||
const serverMeta: ServerMeta = {
|
const serverMeta: ServerMeta = {
|
||||||
httpBaseUrl: `http://${options.host}:${options.port}`,
|
httpBaseUrl: `http://${options.host}:${options.port}`,
|
||||||
eventsUrl: `/api/events`,
|
eventsUrl: `/api/events`,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
listeningMode: options.host === "0.0.0.0" ? "all" : "local",
|
listeningMode: isLoopbackHost(options.host) ? "local" : "all",
|
||||||
port: options.port,
|
port: options.port,
|
||||||
hostLabel: options.host,
|
hostLabel: options.host,
|
||||||
workspaceRoot: options.rootDir,
|
workspaceRoot: options.rootDir,
|
||||||
@@ -195,19 +224,36 @@ async function main() {
|
|||||||
logger: logger.child({ component: "instance-events" }),
|
logger: logger.child({ component: "instance-events" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const releaseMonitor = startReleaseMonitor({
|
const uiDirEnvOverride = Boolean(process.env.CLI_UI_DIR)
|
||||||
currentVersion: packageJson.version,
|
const uiDirCliOverride = programHasArg(process.argv.slice(2), "--ui-dir")
|
||||||
logger: logger.child({ component: "release-monitor" }),
|
const uiOverrideIsExplicit = uiDirEnvOverride || uiDirCliOverride
|
||||||
onUpdate: (release) => {
|
const uiDirOverride = uiOverrideIsExplicit ? options.uiStaticDir : undefined
|
||||||
if (release) {
|
|
||||||
serverMeta.latestRelease = release
|
const autoUpdateEnabled = options.uiAutoUpdate && !options.uiNoUpdate
|
||||||
eventBus.publish({ type: "app.releaseAvailable", release })
|
|
||||||
} else {
|
const uiResolution = await resolveUi({
|
||||||
delete serverMeta.latestRelease
|
serverVersion: packageJson.version,
|
||||||
}
|
bundledUiDir: DEFAULT_UI_STATIC_DIR,
|
||||||
},
|
autoUpdate: autoUpdateEnabled,
|
||||||
|
overrideUiDir: uiDirOverride,
|
||||||
|
uiDevServerUrl: options.uiDevServer,
|
||||||
|
manifestUrl: options.uiManifestUrl,
|
||||||
|
logger: logger.child({ component: "ui" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
serverMeta.serverVersion = packageJson.version
|
||||||
|
serverMeta.ui = {
|
||||||
|
version: uiResolution.uiVersion,
|
||||||
|
source: uiResolution.source,
|
||||||
|
}
|
||||||
|
serverMeta.support = {
|
||||||
|
supported: uiResolution.supported,
|
||||||
|
message: uiResolution.message,
|
||||||
|
latestServerVersion: uiResolution.latestServerVersion,
|
||||||
|
latestServerUrl: uiResolution.latestServerUrl,
|
||||||
|
minServerVersion: uiResolution.minServerVersion,
|
||||||
|
}
|
||||||
|
|
||||||
const server = createHttpServer({
|
const server = createHttpServer({
|
||||||
host: options.host,
|
host: options.host,
|
||||||
port: options.port,
|
port: options.port,
|
||||||
@@ -219,8 +265,8 @@ async function main() {
|
|||||||
serverMeta,
|
serverMeta,
|
||||||
instanceStore,
|
instanceStore,
|
||||||
authManager,
|
authManager,
|
||||||
uiStaticDir: options.uiStaticDir,
|
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
||||||
uiDevServerUrl: options.uiDevServer,
|
uiDevServerUrl: uiResolution.uiDevServerUrl,
|
||||||
logger,
|
logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -256,7 +302,7 @@ async function main() {
|
|||||||
logger.error({ err: error }, "Workspace manager shutdown failed")
|
logger.error({ err: error }, "Workspace manager shutdown failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseMonitor.stop()
|
// no-op: remote UI manifest replaces GitHub release monitor
|
||||||
|
|
||||||
logger.info("Exiting process")
|
logger.info("Exiting process")
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const allowedDevOrigins = new Set(["http://localhost:3000", "http://127.0.0.1:3000"])
|
const allowedDevOrigins = new Set(["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||||
|
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
|
|
||||||
app.register(cors, {
|
app.register(cors, {
|
||||||
origin: (origin, cb) => {
|
origin: (origin, cb) => {
|
||||||
@@ -113,10 +114,17 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowedDevOrigins.has(origin)) {
|
if (allowedDevOrigins.has(origin)) {
|
||||||
cb(null, true)
|
cb(null, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When we bind to a non-loopback host (e.g., 0.0.0.0 or LAN IP), allow cross-origin UI access.
|
||||||
|
if (deps.host === "0.0.0.0" || !isLoopbackHost(deps.host)) {
|
||||||
|
cb(null, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cb(null, false)
|
cb(null, false)
|
||||||
},
|
},
|
||||||
@@ -275,13 +283,13 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host === "127.0.0.1" ? "localhost" : deps.host
|
const displayHost = deps.host === "127.0.0.1" ? "localhost" : deps.host
|
||||||
const serverUrl = `http://${displayHost}:${actualPort}`
|
const serverUrl = `http://${displayHost}:${actualPort}`
|
||||||
|
|
||||||
deps.serverMeta.httpBaseUrl = serverUrl
|
deps.serverMeta.httpBaseUrl = serverUrl
|
||||||
deps.serverMeta.host = deps.host
|
deps.serverMeta.host = deps.host
|
||||||
deps.serverMeta.port = actualPort
|
deps.serverMeta.port = actualPort
|
||||||
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" ? "all" : "local"
|
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" || !isLoopbackHost(deps.host) ? "all" : "local"
|
||||||
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
|
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
|
||||||
console.log(`CodeNomad Server is ready at ${serverUrl}`)
|
console.log(`CodeNomad Server is ready at ${serverUrl}`)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
|||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
port,
|
port,
|
||||||
listeningMode: meta.host === "0.0.0.0" ? "all" : "local",
|
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
||||||
addresses,
|
addresses,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,10 @@ function resolvePort(meta: ServerMeta): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLoopbackHost(host: string): boolean {
|
||||||
|
return host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAddresses(port: number, host: string): NetworkAddress[] {
|
function resolveAddresses(port: number, host: string): NetworkAddress[] {
|
||||||
const interfaces = os.networkInterfaces()
|
const interfaces = os.networkInterfaces()
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
|
|||||||
535
packages/server/src/ui/remote-ui.ts
Normal file
535
packages/server/src/ui/remote-ui.ts
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
import { createHash } from "crypto"
|
||||||
|
import fs from "fs"
|
||||||
|
import { promises as fsp } from "fs"
|
||||||
|
import os from "os"
|
||||||
|
import path from "path"
|
||||||
|
import { Readable } from "stream"
|
||||||
|
import { fetch } from "undici"
|
||||||
|
import yauzl from "yauzl"
|
||||||
|
import type { Logger } from "../logger"
|
||||||
|
|
||||||
|
export interface RemoteUiManifest {
|
||||||
|
minServerVersion: string
|
||||||
|
latestUIVersion: string
|
||||||
|
uiPackageURL: string
|
||||||
|
sha256: string
|
||||||
|
latestServerVersion?: string
|
||||||
|
latestServerUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UiSource = "bundled" | "downloaded" | "previous" | "override" | "dev-proxy" | "missing"
|
||||||
|
|
||||||
|
export interface UiResolution {
|
||||||
|
uiStaticDir?: string
|
||||||
|
uiDevServerUrl?: string
|
||||||
|
source: UiSource
|
||||||
|
uiVersion?: string
|
||||||
|
supported: boolean
|
||||||
|
message?: string
|
||||||
|
latestServerVersion?: string
|
||||||
|
latestServerUrl?: string
|
||||||
|
minServerVersion?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteUiOptions {
|
||||||
|
serverVersion: string
|
||||||
|
bundledUiDir: string
|
||||||
|
autoUpdate: boolean
|
||||||
|
overrideUiDir?: string
|
||||||
|
uiDevServerUrl?: string
|
||||||
|
manifestUrl?: string
|
||||||
|
configDir?: string
|
||||||
|
logger: Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MANIFEST_URL = "https://ui.codenomad.neuralnomads.ai/version.json"
|
||||||
|
|
||||||
|
const MANIFEST_TIMEOUT_MS = 5_000
|
||||||
|
const ZIP_TIMEOUT_MS = 30_000
|
||||||
|
|
||||||
|
export async function resolveUi(options: RemoteUiOptions): Promise<UiResolution> {
|
||||||
|
const manifestUrl = options.manifestUrl ?? DEFAULT_MANIFEST_URL
|
||||||
|
|
||||||
|
if (options.uiDevServerUrl) {
|
||||||
|
return {
|
||||||
|
uiDevServerUrl: options.uiDevServerUrl,
|
||||||
|
source: "dev-proxy",
|
||||||
|
supported: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.overrideUiDir) {
|
||||||
|
const resolved = await resolveStaticUiDir(options.overrideUiDir)
|
||||||
|
return {
|
||||||
|
uiStaticDir: resolved ?? options.overrideUiDir,
|
||||||
|
source: "override",
|
||||||
|
uiVersion: await readUiVersion(resolved ?? options.overrideUiDir),
|
||||||
|
supported: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiRoot = resolveUiCacheRoot(options.configDir)
|
||||||
|
const currentDir = path.join(uiRoot, "current")
|
||||||
|
const previousDir = path.join(uiRoot, "previous")
|
||||||
|
|
||||||
|
if (!options.autoUpdate) {
|
||||||
|
const local = await resolveStaticUiDir(currentDir)
|
||||||
|
if (local) {
|
||||||
|
return {
|
||||||
|
uiStaticDir: local,
|
||||||
|
source: "downloaded",
|
||||||
|
uiVersion: await readUiVersion(local),
|
||||||
|
supported: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundled = await resolveStaticUiDir(options.bundledUiDir)
|
||||||
|
return {
|
||||||
|
uiStaticDir: bundled ?? options.bundledUiDir,
|
||||||
|
source: bundled ? "bundled" : "missing",
|
||||||
|
uiVersion: bundled ? await readUiVersion(bundled) : undefined,
|
||||||
|
supported: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest: RemoteUiManifest | null = null
|
||||||
|
try {
|
||||||
|
manifest = await fetchManifest(manifestUrl, options.logger)
|
||||||
|
} catch (error) {
|
||||||
|
options.logger.debug({ err: error }, "Remote UI manifest unavailable; using cached/bundled UI")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest) {
|
||||||
|
return await resolveFromCacheOrBundled({
|
||||||
|
logger: options.logger,
|
||||||
|
bundledUiDir: options.bundledUiDir,
|
||||||
|
currentDir,
|
||||||
|
previousDir,
|
||||||
|
supported: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const supported = compareSemverCore(options.serverVersion, manifest.minServerVersion) >= 0
|
||||||
|
if (!supported) {
|
||||||
|
const message = "Upgrade App to use latest features"
|
||||||
|
return await resolveFromCacheOrBundled({
|
||||||
|
logger: options.logger,
|
||||||
|
bundledUiDir: options.bundledUiDir,
|
||||||
|
currentDir,
|
||||||
|
previousDir,
|
||||||
|
supported: false,
|
||||||
|
message,
|
||||||
|
latestServerVersion: manifest.latestServerVersion,
|
||||||
|
latestServerUrl: manifest.latestServerUrl,
|
||||||
|
minServerVersion: manifest.minServerVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = await readUiVersion(currentDir)
|
||||||
|
if (currentVersion && currentVersion === manifest.latestUIVersion) {
|
||||||
|
const currentResolved = await resolveStaticUiDir(currentDir)
|
||||||
|
if (currentResolved) {
|
||||||
|
return {
|
||||||
|
uiStaticDir: currentResolved,
|
||||||
|
source: "downloaded",
|
||||||
|
uiVersion: currentVersion,
|
||||||
|
supported: true,
|
||||||
|
latestServerVersion: manifest.latestServerVersion,
|
||||||
|
latestServerUrl: manifest.latestServerUrl,
|
||||||
|
minServerVersion: manifest.minServerVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await installRemoteUi({
|
||||||
|
manifest,
|
||||||
|
uiRoot,
|
||||||
|
currentDir,
|
||||||
|
previousDir,
|
||||||
|
logger: options.logger,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
options.logger.warn({ err: error }, "Failed to install remote UI; falling back")
|
||||||
|
return await resolveFromCacheOrBundled({
|
||||||
|
logger: options.logger,
|
||||||
|
bundledUiDir: options.bundledUiDir,
|
||||||
|
currentDir,
|
||||||
|
previousDir,
|
||||||
|
supported: true,
|
||||||
|
latestServerVersion: manifest.latestServerVersion,
|
||||||
|
latestServerUrl: manifest.latestServerUrl,
|
||||||
|
minServerVersion: manifest.minServerVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const installed = await resolveStaticUiDir(currentDir)
|
||||||
|
if (installed) {
|
||||||
|
return {
|
||||||
|
uiStaticDir: installed,
|
||||||
|
source: "downloaded",
|
||||||
|
uiVersion: await readUiVersion(installed),
|
||||||
|
supported: true,
|
||||||
|
latestServerVersion: manifest.latestServerVersion,
|
||||||
|
latestServerUrl: manifest.latestServerUrl,
|
||||||
|
minServerVersion: manifest.minServerVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await resolveFromCacheOrBundled({
|
||||||
|
logger: options.logger,
|
||||||
|
bundledUiDir: options.bundledUiDir,
|
||||||
|
currentDir,
|
||||||
|
previousDir,
|
||||||
|
supported: true,
|
||||||
|
latestServerVersion: manifest.latestServerVersion,
|
||||||
|
latestServerUrl: manifest.latestServerUrl,
|
||||||
|
minServerVersion: manifest.minServerVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUiCacheRoot(configDir?: string): string {
|
||||||
|
if (configDir) {
|
||||||
|
return path.join(configDir, "ui")
|
||||||
|
}
|
||||||
|
return path.join(os.homedir(), ".config", "codenomad", "ui")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveFromCacheOrBundled(args: {
|
||||||
|
logger: Logger
|
||||||
|
bundledUiDir: string
|
||||||
|
currentDir: string
|
||||||
|
previousDir: string
|
||||||
|
supported: boolean
|
||||||
|
message?: string
|
||||||
|
latestServerVersion?: string
|
||||||
|
latestServerUrl?: string
|
||||||
|
minServerVersion?: string
|
||||||
|
}): Promise<UiResolution> {
|
||||||
|
const currentResolved = await resolveStaticUiDir(args.currentDir)
|
||||||
|
if (currentResolved) {
|
||||||
|
return {
|
||||||
|
uiStaticDir: currentResolved,
|
||||||
|
source: "downloaded",
|
||||||
|
uiVersion: await readUiVersion(currentResolved),
|
||||||
|
supported: args.supported,
|
||||||
|
message: args.message,
|
||||||
|
latestServerVersion: args.latestServerVersion,
|
||||||
|
latestServerUrl: args.latestServerUrl,
|
||||||
|
minServerVersion: args.minServerVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousResolved = await resolveStaticUiDir(args.previousDir)
|
||||||
|
if (previousResolved) {
|
||||||
|
return {
|
||||||
|
uiStaticDir: previousResolved,
|
||||||
|
source: "previous",
|
||||||
|
uiVersion: await readUiVersion(previousResolved),
|
||||||
|
supported: args.supported,
|
||||||
|
message: args.message,
|
||||||
|
latestServerVersion: args.latestServerVersion,
|
||||||
|
latestServerUrl: args.latestServerUrl,
|
||||||
|
minServerVersion: args.minServerVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundledResolved = await resolveStaticUiDir(args.bundledUiDir)
|
||||||
|
if (bundledResolved) {
|
||||||
|
return {
|
||||||
|
uiStaticDir: bundledResolved,
|
||||||
|
source: "bundled",
|
||||||
|
uiVersion: await readUiVersion(bundledResolved),
|
||||||
|
supported: args.supported,
|
||||||
|
message: args.message,
|
||||||
|
latestServerVersion: args.latestServerVersion,
|
||||||
|
latestServerUrl: args.latestServerUrl,
|
||||||
|
minServerVersion: args.minServerVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args.logger.warn({ bundledUiDir: args.bundledUiDir }, "No UI assets found")
|
||||||
|
return {
|
||||||
|
uiStaticDir: args.bundledUiDir,
|
||||||
|
source: "missing",
|
||||||
|
supported: args.supported,
|
||||||
|
message: args.message,
|
||||||
|
latestServerVersion: args.latestServerVersion,
|
||||||
|
latestServerUrl: args.latestServerUrl,
|
||||||
|
minServerVersion: args.minServerVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveStaticUiDir(uiDir: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const indexPath = path.join(uiDir, "index.html")
|
||||||
|
await fsp.access(indexPath, fs.constants.R_OK)
|
||||||
|
return uiDir
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UiVersionFile {
|
||||||
|
uiVersion?: string
|
||||||
|
version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readUiVersion(uiDir: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const content = await fsp.readFile(path.join(uiDir, "ui-version.json"), "utf-8")
|
||||||
|
const parsed = JSON.parse(content) as UiVersionFile
|
||||||
|
return parsed.uiVersion ?? parsed.version
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchManifest(url: string, logger: Logger): Promise<RemoteUiManifest> {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), MANIFEST_TIMEOUT_MS)
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"User-Agent": "CodeNomad-CLI",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Manifest responded with ${response.status}`)
|
||||||
|
}
|
||||||
|
const json = (await response.json()) as RemoteUiManifest
|
||||||
|
validateManifest(json)
|
||||||
|
return json
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug({ err: error, url }, "Failed to fetch remote UI manifest")
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateManifest(manifest: RemoteUiManifest) {
|
||||||
|
const required: Array<keyof RemoteUiManifest> = ["minServerVersion", "latestUIVersion", "uiPackageURL", "sha256"]
|
||||||
|
for (const key of required) {
|
||||||
|
const value = manifest[key]
|
||||||
|
if (typeof value !== "string" || value.trim().length === 0) {
|
||||||
|
throw new Error(`Manifest missing ${key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!/^https:\/\//i.test(manifest.uiPackageURL)) {
|
||||||
|
throw new Error("uiPackageURL must be https")
|
||||||
|
}
|
||||||
|
if (!/^[a-f0-9]{64}$/i.test(manifest.sha256.trim())) {
|
||||||
|
throw new Error("sha256 must be 64 hex chars")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installRemoteUi(args: {
|
||||||
|
manifest: RemoteUiManifest
|
||||||
|
uiRoot: string
|
||||||
|
currentDir: string
|
||||||
|
previousDir: string
|
||||||
|
logger: Logger
|
||||||
|
}) {
|
||||||
|
await fsp.mkdir(args.uiRoot, { recursive: true })
|
||||||
|
|
||||||
|
const tmpDir = path.join(args.uiRoot, `tmp-${Date.now()}`)
|
||||||
|
const zipPath = path.join(args.uiRoot, `ui-${args.manifest.latestUIVersion}.zip`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(args.manifest.uiPackageURL, zipPath, args.logger)
|
||||||
|
const digest = await sha256File(zipPath)
|
||||||
|
if (digest.toLowerCase() !== args.manifest.sha256.toLowerCase()) {
|
||||||
|
throw new Error(`sha256 mismatch for UI zip (expected ${args.manifest.sha256}, got ${digest})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await extractZip(zipPath, tmpDir)
|
||||||
|
|
||||||
|
const indexPath = path.join(tmpDir, "index.html")
|
||||||
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
throw new Error("Extracted UI missing index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
await rotateDirs({ currentDir: args.currentDir, previousDir: args.previousDir, logger: args.logger })
|
||||||
|
|
||||||
|
fs.rmSync(args.currentDir, { recursive: true, force: true })
|
||||||
|
fs.renameSync(tmpDir, args.currentDir)
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
|
fs.rmSync(zipPath, { force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateDirs(args: { currentDir: string; previousDir: string; logger: Logger }) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(args.previousDir)) {
|
||||||
|
fs.rmSync(args.previousDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
if (fs.existsSync(args.currentDir)) {
|
||||||
|
fs.renameSync(args.currentDir, args.previousDir)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
args.logger.warn({ err: error }, "Failed to rotate UI cache directories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(url: string, targetPath: string, logger: Logger) {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), ZIP_TIMEOUT_MS)
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/octet-stream",
|
||||||
|
"User-Agent": "CodeNomad-CLI",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error(`UI zip download failed with ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.mkdir(path.dirname(targetPath), { recursive: true })
|
||||||
|
const fileStream = fs.createWriteStream(targetPath)
|
||||||
|
|
||||||
|
const body = response.body
|
||||||
|
if (!body) {
|
||||||
|
throw new Error("UI zip response missing body")
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeStream = Readable.fromWeb(body as any)
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
nodeStream.pipe(fileStream)
|
||||||
|
nodeStream.on("error", reject)
|
||||||
|
fileStream.on("error", reject)
|
||||||
|
fileStream.on("finish", () => resolve())
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug({ url, targetPath }, "Downloaded remote UI bundle")
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sha256File(filePath: string): Promise<string> {
|
||||||
|
const hash = createHash("sha256")
|
||||||
|
const stream = fs.createReadStream(filePath)
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
stream.on("data", (chunk) => hash.update(chunk))
|
||||||
|
stream.on("error", reject)
|
||||||
|
stream.on("end", () => resolve())
|
||||||
|
})
|
||||||
|
return hash.digest("hex")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractZip(zipPath: string, targetDir: string): Promise<void> {
|
||||||
|
await fsp.mkdir(targetDir, { recursive: true })
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
yauzl.open(zipPath, { lazyEntries: true }, (openErr, zipfile) => {
|
||||||
|
if (openErr || !zipfile) {
|
||||||
|
reject(openErr ?? new Error("Unable to open zip"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = path.resolve(targetDir)
|
||||||
|
|
||||||
|
const closeWithError = (error: unknown) => {
|
||||||
|
try {
|
||||||
|
zipfile.close()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
zipfile.readEntry()
|
||||||
|
|
||||||
|
zipfile.on("entry", (entry) => {
|
||||||
|
// Normalize and guard against zip-slip.
|
||||||
|
const entryPath = entry.fileName.replace(/\\/g, "/")
|
||||||
|
|
||||||
|
const segments = entryPath.split("/").filter(Boolean)
|
||||||
|
if (segments.some((segment: string) => segment === "..") || path.isAbsolute(entryPath)) {
|
||||||
|
closeWithError(new Error(`Invalid zip entry path: ${entry.fileName}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = path.resolve(targetDir, entryPath)
|
||||||
|
if (!destination.startsWith(root + path.sep) && destination !== root) {
|
||||||
|
closeWithError(new Error(`Zip entry escapes target dir: ${entry.fileName}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirectory = entry.fileName.endsWith("/")
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
fsp
|
||||||
|
.mkdir(destination, { recursive: true })
|
||||||
|
.then(() => zipfile.readEntry())
|
||||||
|
.catch((error) => closeWithError(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fsp
|
||||||
|
.mkdir(path.dirname(destination), { recursive: true })
|
||||||
|
.then(() => {
|
||||||
|
zipfile.openReadStream(entry, (streamErr, readStream) => {
|
||||||
|
if (streamErr || !readStream) {
|
||||||
|
closeWithError(streamErr ?? new Error("Unable to read zip entry"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeStream = fs.createWriteStream(destination)
|
||||||
|
const cleanup = (error?: unknown) => {
|
||||||
|
readStream.destroy()
|
||||||
|
writeStream.destroy()
|
||||||
|
if (error) {
|
||||||
|
closeWithError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readStream.on("error", cleanup)
|
||||||
|
writeStream.on("error", cleanup)
|
||||||
|
writeStream.on("finish", () => zipfile.readEntry())
|
||||||
|
|
||||||
|
readStream.pipe(writeStream)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => closeWithError(error))
|
||||||
|
})
|
||||||
|
|
||||||
|
zipfile.on("end", () => {
|
||||||
|
zipfile.close()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
zipfile.on("error", (error) => closeWithError(error))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareSemverCore(a: string, b: string): number {
|
||||||
|
const pa = parseSemverCore(a)
|
||||||
|
const pb = parseSemverCore(b)
|
||||||
|
if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1
|
||||||
|
if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1
|
||||||
|
if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSemverCore(value: string): { major: number; minor: number; patch: number } {
|
||||||
|
const core = value.trim().replace(/^v/i, "").split("-", 1)[0] ?? "0.0.0"
|
||||||
|
const parts = core.split(".")
|
||||||
|
const parsePart = (input: string | undefined) => {
|
||||||
|
const n = Number.parseInt((input ?? "0").replace(/[^0-9]/g, ""), 10)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
major: parsePart(parts[0]),
|
||||||
|
minor: parsePart(parts[1]),
|
||||||
|
patch: parsePart(parts[2]),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -225,13 +225,15 @@ export class WorkspaceManager {
|
|||||||
try {
|
try {
|
||||||
const result = spawnSync(locator, [identifier], { encoding: "utf8" })
|
const result = spawnSync(locator, [identifier], { encoding: "utf8" })
|
||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const resolved = result.stdout
|
const candidates = result.stdout
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.find((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
|
.filter((line) => !/^INFO:/i.test(line))
|
||||||
|
|
||||||
if (resolved) {
|
if (candidates.length > 0) {
|
||||||
this.options.logger.debug({ identifier, resolved }, "Resolved binary path from system PATH")
|
const resolved = this.pickBinaryCandidate(candidates)
|
||||||
|
this.options.logger.debug({ identifier, resolved, candidates }, "Resolved binary path from system PATH")
|
||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
@@ -244,6 +246,23 @@ export class WorkspaceManager {
|
|||||||
return identifier
|
return identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pickBinaryCandidate(candidates: string[]): string {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return candidates[0] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionPreference = [".exe", ".cmd", ".bat", ".ps1"]
|
||||||
|
|
||||||
|
for (const ext of extensionPreference) {
|
||||||
|
const match = candidates.find((candidate) => candidate.toLowerCase().endsWith(ext))
|
||||||
|
if (match) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
private detectBinaryVersion(resolvedPath: string): string | undefined {
|
private detectBinaryVersion(resolvedPath: string): string | undefined {
|
||||||
if (!resolvedPath) {
|
if (!resolvedPath) {
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
@@ -5,6 +5,41 @@ import { EventBus } from "../events/bus"
|
|||||||
import { LogLevel, WorkspaceLogEntry } from "../api-types"
|
import { LogLevel, WorkspaceLogEntry } from "../api-types"
|
||||||
import { Logger } from "../logger"
|
import { Logger } from "../logger"
|
||||||
|
|
||||||
|
export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"])
|
||||||
|
export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"])
|
||||||
|
|
||||||
|
export function buildSpawnSpec(binaryPath: string, args: string[]) {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return { command: binaryPath, args, options: {} as const }
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(binaryPath).toLowerCase()
|
||||||
|
|
||||||
|
if (WINDOWS_CMD_EXTENSIONS.has(extension)) {
|
||||||
|
const comspec = process.env.ComSpec || "cmd.exe"
|
||||||
|
// cmd.exe requires the full command as a single string.
|
||||||
|
// Using the ""<script> <args>"" pattern ensures paths with spaces are handled.
|
||||||
|
const commandLine = `""${binaryPath}" ${args.join(" ")}"`
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: comspec,
|
||||||
|
args: ["/d", "/s", "/c", commandLine],
|
||||||
|
options: { windowsVerbatimArguments: true } as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WINDOWS_POWERSHELL_EXTENSIONS.has(extension)) {
|
||||||
|
// powershell.exe ships with Windows. (pwsh may not.)
|
||||||
|
return {
|
||||||
|
command: "powershell.exe",
|
||||||
|
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args],
|
||||||
|
options: {} as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { command: binaryPath, args, options: {} as const }
|
||||||
|
}
|
||||||
|
|
||||||
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i
|
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i
|
||||||
|
|
||||||
function redactEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
function redactEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
||||||
@@ -73,22 +108,25 @@ export class WorkspaceRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const commandLine = [options.binaryPath, ...args].join(" ")
|
const spec = buildSpawnSpec(options.binaryPath, args)
|
||||||
|
const commandLine = [spec.command, ...spec.args].join(" ")
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
{
|
{
|
||||||
workspaceId: options.workspaceId,
|
workspaceId: options.workspaceId,
|
||||||
folder: options.folder,
|
folder: options.folder,
|
||||||
binary: options.binaryPath,
|
binary: options.binaryPath,
|
||||||
args,
|
spawnCommand: spec.command,
|
||||||
|
spawnArgs: spec.args,
|
||||||
commandLine,
|
commandLine,
|
||||||
env: redactEnvironment(env),
|
env: redactEnvironment(env),
|
||||||
},
|
},
|
||||||
"Launching OpenCode process",
|
"Launching OpenCode process",
|
||||||
)
|
)
|
||||||
const child = spawn(options.binaryPath, args, {
|
const child = spawn(spec.command, spec.args, {
|
||||||
cwd: options.folder,
|
cwd: options.folder,
|
||||||
env,
|
env,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
...spec.options,
|
||||||
})
|
})
|
||||||
|
|
||||||
const managed: ManagedProcess = { child, requestedStop: false }
|
const managed: ManagedProcess = { child, requestedStop: false }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.7.0",
|
"version": "0.7.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AdvancedSettingsModal from "./advanced-settings-modal"
|
|||||||
import DirectoryBrowserDialog from "./directory-browser-dialog"
|
import DirectoryBrowserDialog from "./directory-browser-dialog"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions"
|
import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions"
|
||||||
|
import VersionPill from "./version-pill"
|
||||||
|
|
||||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||||
|
|
||||||
@@ -248,6 +249,9 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<h1 class="mb-2 text-3xl font-semibold text-primary">CodeNomad</h1>
|
<h1 class="mb-2 text-3xl font-semibold text-primary">CodeNomad</h1>
|
||||||
<p class="text-base text-secondary">Select a folder to start coding with AI</p>
|
<p class="text-base text-secondary">Select a folder to start coding with AI</p>
|
||||||
|
<div class="mt-2 flex justify-center">
|
||||||
|
<VersionPill />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,20 @@ interface TaskSessionLocation {
|
|||||||
parentId: string | null
|
parentId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function findTaskSessionLocation(sessionId: string): TaskSessionLocation | null {
|
function findTaskSessionLocation(sessionId: string, preferredInstanceId?: string): TaskSessionLocation | null {
|
||||||
if (!sessionId) return null
|
if (!sessionId) return null
|
||||||
|
|
||||||
|
if (preferredInstanceId) {
|
||||||
|
const session = sessions().get(preferredInstanceId)?.get(sessionId)
|
||||||
|
if (session) {
|
||||||
|
return {
|
||||||
|
sessionId: session.id,
|
||||||
|
instanceId: preferredInstanceId,
|
||||||
|
parentId: session.parentId ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const allSessions = sessions()
|
const allSessions = sessions()
|
||||||
for (const [instanceId, sessionMap] of allSessions) {
|
for (const [instanceId, sessionMap] of allSessions) {
|
||||||
const session = sessionMap?.get(sessionId)
|
const session = sessionMap?.get(sessionId)
|
||||||
@@ -440,7 +452,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
const hasToolState =
|
const hasToolState =
|
||||||
Boolean(toolState) && (isToolStateRunning(toolState) || isToolStateCompleted(toolState) || isToolStateError(toolState))
|
Boolean(toolState) && (isToolStateRunning(toolState) || isToolStateCompleted(toolState) || isToolStateError(toolState))
|
||||||
const taskSessionId = hasToolState ? extractTaskSessionId(toolState) : ""
|
const taskSessionId = hasToolState ? extractTaskSessionId(toolState) : ""
|
||||||
const taskLocation = taskSessionId ? findTaskSessionLocation(taskSessionId) : null
|
const taskLocation = taskSessionId ? findTaskSessionLocation(taskSessionId, props.instanceId) : null
|
||||||
const handleGoToTaskSession = (event: MouseEvent) => {
|
const handleGoToTaskSession = (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import {
|
|||||||
getPermissionQueue,
|
getPermissionQueue,
|
||||||
getQuestionQueue,
|
getQuestionQueue,
|
||||||
getQuestionEnqueuedAtForInstance,
|
getQuestionEnqueuedAtForInstance,
|
||||||
setActivePermissionIdForInstance,
|
sendPermissionResponse,
|
||||||
setActiveQuestionIdForInstance,
|
|
||||||
} from "../stores/instances"
|
} from "../stores/instances"
|
||||||
import { loadMessages, setActiveSession } from "../stores/sessions"
|
import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions"
|
||||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||||
import ToolCall from "./tool-call"
|
import ToolCall from "./tool-call"
|
||||||
|
|
||||||
@@ -132,6 +131,45 @@ function resolveToolCallFromQuestion(instanceId: string, request: QuestionReques
|
|||||||
|
|
||||||
const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props) => {
|
const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props) => {
|
||||||
const [loadingSession, setLoadingSession] = createSignal<string | null>(null)
|
const [loadingSession, setLoadingSession] = createSignal<string | null>(null)
|
||||||
|
const [permissionSubmitting, setPermissionSubmitting] = createSignal<Set<string>>(new Set())
|
||||||
|
const [permissionError, setPermissionError] = createSignal<Map<string, string>>(new Map())
|
||||||
|
|
||||||
|
const setPermissionBusy = (permissionId: string, busy: boolean) => {
|
||||||
|
setPermissionSubmitting((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (busy) next.add(permissionId)
|
||||||
|
else next.delete(permissionId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPermissionItemError = (permissionId: string, message: string | null) => {
|
||||||
|
setPermissionError((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
if (!message) next.delete(permissionId)
|
||||||
|
else next.set(permissionId, message)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePermissionDecision(permission: PermissionRequestLike, response: "once" | "always" | "reject") {
|
||||||
|
const permissionId = permission?.id
|
||||||
|
if (!permissionId) return
|
||||||
|
|
||||||
|
if (permissionSubmitting().has(permissionId)) return
|
||||||
|
|
||||||
|
setPermissionBusy(permissionId, true)
|
||||||
|
setPermissionItemError(permissionId, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionId = getPermissionSessionId(permission) || ""
|
||||||
|
await sendPermissionResponse(props.instanceId, sessionId, permissionId, response)
|
||||||
|
} catch (error) {
|
||||||
|
setPermissionItemError(permissionId, error instanceof Error ? error.message : "Unable to update permission")
|
||||||
|
} finally {
|
||||||
|
setPermissionBusy(permissionId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const permissionQueue = createMemo(() => getPermissionQueue(props.instanceId))
|
const permissionQueue = createMemo(() => getPermissionQueue(props.instanceId))
|
||||||
const questionQueue = createMemo(() => getQuestionQueue(props.instanceId))
|
const questionQueue = createMemo(() => getQuestionQueue(props.instanceId))
|
||||||
@@ -201,7 +239,14 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||||||
|
|
||||||
function handleGoToSession(sessionId: string) {
|
function handleGoToSession(sessionId: string) {
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
setActiveSession(props.instanceId, sessionId)
|
|
||||||
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
|
const parentId = session?.parentId ?? session?.id
|
||||||
|
if (parentId) {
|
||||||
|
ensureSessionParentExpanded(props.instanceId, parentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSessionFromList(props.instanceId, sessionId)
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,19 +303,10 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||||||
return count === 1 ? "1 question" : `${count} questions`
|
return count === 1 ? "1 question" : `${count} questions`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActivate = () => {
|
|
||||||
if (item.kind === "permission") {
|
|
||||||
setActivePermissionIdForInstance(props.instanceId, item.id)
|
|
||||||
} else {
|
|
||||||
setActiveQuestionIdForInstance(props.instanceId, item.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`permission-center-item${isActive() ? " permission-center-item-active" : ""}`}
|
class={`permission-center-item${isActive() ? " permission-center-item-active" : ""}`}
|
||||||
role="listitem"
|
role="listitem"
|
||||||
onClick={handleActivate}
|
|
||||||
>
|
>
|
||||||
<div class="permission-center-item-header">
|
<div class="permission-center-item-header">
|
||||||
<div class="permission-center-item-heading">
|
<div class="permission-center-item-heading">
|
||||||
@@ -308,17 +344,52 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={resolved()}
|
when={resolved()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="permission-center-fallback">
|
<div class="permission-center-fallback">
|
||||||
<div class="permission-center-fallback-title">
|
<div class="permission-center-fallback-title">
|
||||||
<code>{primaryTitle()}</code>
|
<code>{primaryTitle()}</code>
|
||||||
|
</div>
|
||||||
|
<Show when={item.kind === "permission"}>
|
||||||
|
<div class="tool-call-permission-actions">
|
||||||
|
<div class="tool-call-permission-buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-permission-button"
|
||||||
|
disabled={permissionSubmitting().has(item.id)}
|
||||||
|
onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "once")}
|
||||||
|
>
|
||||||
|
Allow Once
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-permission-button"
|
||||||
|
disabled={permissionSubmitting().has(item.id)}
|
||||||
|
onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "always")}
|
||||||
|
>
|
||||||
|
Always Allow
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-permission-button"
|
||||||
|
disabled={permissionSubmitting().has(item.id)}
|
||||||
|
onClick={() => void handlePermissionDecision(item.payload as PermissionRequestLike, "reject")}
|
||||||
|
>
|
||||||
|
Deny
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={permissionError().get(item.id)}>
|
||||||
|
{(err) => <div class="tool-call-permission-error">{err()}</div>}
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.kind !== "permission"}>
|
||||||
|
<div class="permission-center-fallback-hint">Load session for more information.</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="permission-center-fallback-hint">Load session for more information.</div>
|
}
|
||||||
</div>
|
>
|
||||||
}
|
|
||||||
>
|
|
||||||
{(data) => (
|
{(data) => (
|
||||||
<ToolCall
|
<ToolCall
|
||||||
toolCall={data().toolPart}
|
toolCall={data().toolPart}
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExpandState("normal")
|
||||||
clearPrompt()
|
clearPrompt()
|
||||||
|
|
||||||
// Ignore attachments for slash commands, but keep them for next prompt.
|
// Ignore attachments for slash commands, but keep them for next prompt.
|
||||||
@@ -843,7 +844,10 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const currentPrompt = prompt()
|
const currentPrompt = prompt()
|
||||||
const pos = atPosition()
|
const pos = atPosition()
|
||||||
const cursorPos = textareaRef?.selectionStart || 0
|
const cursorPos = textareaRef?.selectionStart || 0
|
||||||
const folderMention = relativePath === "." || relativePath === "" ? "/" : displayPath
|
const folderMention =
|
||||||
|
relativePath === "." || relativePath === ""
|
||||||
|
? "/"
|
||||||
|
: relativePath.replace(/\/+$/, "") + "/"
|
||||||
|
|
||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
const before = currentPrompt.substring(0, pos + 1)
|
const before = currentPrompt.substring(0, pos + 1)
|
||||||
@@ -887,7 +891,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
const before = currentPrompt.substring(0, pos)
|
const before = currentPrompt.substring(0, pos)
|
||||||
const after = currentPrompt.substring(cursorPos)
|
const after = currentPrompt.substring(cursorPos)
|
||||||
const attachmentText = `@${filename}`
|
const attachmentText = `@${normalizedPath}`
|
||||||
const newPrompt = before + attachmentText + " " + after
|
const newPrompt = before + attachmentText + " " + after
|
||||||
setPrompt(newPrompt)
|
setPrompt(newPrompt)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Show, For, createMemo, createEffect, type Component } from "solid-js"
|
import { Show, For, createMemo, createEffect, type Component } from "solid-js"
|
||||||
|
import { Expand } from "lucide-solid"
|
||||||
import type { Session } from "../../types/session"
|
import type { Session } from "../../types/session"
|
||||||
import type { Attachment } from "../../types/attachment"
|
import type { Attachment } from "../../types/attachment"
|
||||||
import type { ClientPart } from "../../types/message"
|
import type { ClientPart } from "../../types/message"
|
||||||
import MessageSection from "../message-section"
|
import MessageSection from "../message-section"
|
||||||
import { messageStoreBus } from "../../stores/message-v2/bus"
|
import { messageStoreBus } from "../../stores/message-v2/bus"
|
||||||
import PromptInput from "../prompt-input"
|
import PromptInput from "../prompt-input"
|
||||||
import AttachmentChip from "../attachment-chip"
|
import type { Attachment as PromptAttachment } from "../../types/attachment"
|
||||||
import { getAttachments, removeAttachment } from "../../stores/attachments"
|
import { getAttachments, removeAttachment } from "../../stores/attachments"
|
||||||
import { instances } from "../../stores/instances"
|
import { instances } from "../../stores/instances"
|
||||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
|
||||||
@@ -49,6 +50,54 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const attachments = createMemo(() => getAttachments(props.instanceId, props.sessionId))
|
const attachments = createMemo(() => getAttachments(props.instanceId, props.sessionId))
|
||||||
|
|
||||||
|
function handleExpandTextAttachment(attachment: PromptAttachment) {
|
||||||
|
if (attachment.source.type !== "text") return
|
||||||
|
|
||||||
|
const textarea = rootRef?.querySelector(".prompt-input") as HTMLTextAreaElement | null
|
||||||
|
const value = attachment.source.value
|
||||||
|
const match = attachment.display.match(/pasted #(\d+)/)
|
||||||
|
const placeholder = match ? `[pasted #${match[1]}]` : null
|
||||||
|
|
||||||
|
const currentText = textarea?.value ?? ""
|
||||||
|
|
||||||
|
let nextText = currentText
|
||||||
|
let selectionTarget: number | null = null
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
const placeholderIndex = currentText.indexOf(placeholder)
|
||||||
|
if (placeholderIndex !== -1) {
|
||||||
|
nextText =
|
||||||
|
currentText.substring(0, placeholderIndex) +
|
||||||
|
value +
|
||||||
|
currentText.substring(placeholderIndex + placeholder.length)
|
||||||
|
selectionTarget = placeholderIndex + value.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextText === currentText) {
|
||||||
|
if (textarea) {
|
||||||
|
const start = textarea.selectionStart
|
||||||
|
const end = textarea.selectionEnd
|
||||||
|
nextText = currentText.substring(0, start) + value + currentText.substring(end)
|
||||||
|
selectionTarget = start + value.length
|
||||||
|
} else {
|
||||||
|
nextText = currentText + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
textarea.value = nextText
|
||||||
|
textarea.dispatchEvent(new Event("input", { bubbles: true }))
|
||||||
|
textarea.focus()
|
||||||
|
if (selectionTarget !== null) {
|
||||||
|
textarea.setSelectionRange(selectionTarget, selectionTarget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAttachment(props.instanceId, props.sessionId, attachment.id)
|
||||||
|
}
|
||||||
|
|
||||||
let scrollToBottomHandle: (() => void) | undefined
|
let scrollToBottomHandle: (() => void) | undefined
|
||||||
let rootRef: HTMLDivElement | undefined
|
let rootRef: HTMLDivElement | undefined
|
||||||
function scheduleScrollToBottom() {
|
function scheduleScrollToBottom() {
|
||||||
@@ -235,14 +284,35 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
|
|
||||||
|
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
<div class="flex flex-wrap gap-1.5 border-t px-3 py-2" style="border-color: var(--border-base);">
|
<div class="flex flex-wrap items-center gap-1.5 border-t px-3 py-2" style="border-color: var(--border-base);">
|
||||||
<For each={attachments()}>
|
<For each={attachments()}>
|
||||||
{(attachment) => (
|
{(attachment) => {
|
||||||
<AttachmentChip
|
const isText = attachment.source.type === "text"
|
||||||
attachment={attachment}
|
return (
|
||||||
onRemove={() => removeAttachment(props.instanceId, props.sessionId, attachment.id)}
|
<div class="attachment-chip" title={attachment.source.type === "file" ? attachment.source.path : undefined}>
|
||||||
/>
|
<span class="font-mono">{attachment.display}</span>
|
||||||
)}
|
<Show when={isText}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="attachment-expand"
|
||||||
|
onClick={() => handleExpandTextAttachment(attachment)}
|
||||||
|
aria-label="Expand pasted text"
|
||||||
|
title="Insert pasted text"
|
||||||
|
>
|
||||||
|
<Expand class="h-3 w-3" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="attachment-remove"
|
||||||
|
onClick={() => removeAttachment(props.instanceId, props.sessionId, attachment.id)}
|
||||||
|
aria-label="Remove attachment"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, Show, For, createEffect, createMemo, onCleanup } from "solid-js"
|
import { createSignal, Show, For, createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
|
||||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { ToolCallDiffViewer } from "./diff-viewer"
|
import { ToolCallDiffViewer } from "./diff-viewer"
|
||||||
@@ -7,6 +7,7 @@ import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
|||||||
import { useConfig } from "../stores/preferences"
|
import { useConfig } from "../stores/preferences"
|
||||||
import type { DiffViewMode } from "../stores/preferences"
|
import type { DiffViewMode } from "../stores/preferences"
|
||||||
import { activeInterruption, sendPermissionResponse, sendQuestionReject, sendQuestionReply } from "../stores/instances"
|
import { activeInterruption, sendPermissionResponse, sendQuestionReject, sendQuestionReply } from "../stores/instances"
|
||||||
|
import type { PermissionRequestLike } from "../types/permission"
|
||||||
import { getPermissionDisplayTitle, getPermissionKind, getPermissionSessionId } from "../types/permission"
|
import { getPermissionDisplayTitle, getPermissionKind, getPermissionSessionId } from "../types/permission"
|
||||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import type { TextPart, RenderCache } from "../types/message"
|
import type { TextPart, RenderCache } from "../types/message"
|
||||||
@@ -32,6 +33,29 @@ type ToolState = import("@opencode-ai/sdk").ToolState
|
|||||||
|
|
||||||
type AnsiRenderCache = RenderCache & { hasAnsi: boolean }
|
type AnsiRenderCache = RenderCache & { hasAnsi: boolean }
|
||||||
|
|
||||||
|
type QuestionOption = { label: string; description: string }
|
||||||
|
|
||||||
|
type QuestionPrompt = {
|
||||||
|
header: string
|
||||||
|
question: string
|
||||||
|
options: QuestionOption[]
|
||||||
|
multiple?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionToolBlockProps = {
|
||||||
|
toolName: Accessor<string>
|
||||||
|
toolState: Accessor<ToolState | undefined>
|
||||||
|
toolCallId: Accessor<string>
|
||||||
|
request: Accessor<QuestionRequest | undefined>
|
||||||
|
active: Accessor<boolean>
|
||||||
|
submitting: Accessor<boolean>
|
||||||
|
error: Accessor<string | null>
|
||||||
|
draftAnswers: Accessor<Record<string, string[][]>>
|
||||||
|
setDraftAnswers: (updater: (prev: Record<string, string[][]>) => Record<string, string[][]>) => void
|
||||||
|
onSubmit: () => void | Promise<void>
|
||||||
|
onDismiss: () => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
const TOOL_CALL_CACHE_SCOPE = "tool-call"
|
const TOOL_CALL_CACHE_SCOPE = "tool-call"
|
||||||
const TOOL_SCROLL_SENTINEL_MARGIN_PX = 48
|
const TOOL_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||||
const TOOL_SCROLL_INTENT_WINDOW_MS = 600
|
const TOOL_SCROLL_INTENT_WINDOW_MS = 600
|
||||||
@@ -107,6 +131,291 @@ function getSeverityMeta(tone: DiagnosticEntry["tone"]) {
|
|||||||
return { label: "INFO", icon: "i", rank: 2 }
|
return { label: "INFO", icon: "i", rank: 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function QuestionToolBlock(props: QuestionToolBlockProps) {
|
||||||
|
const requestId = createMemo(() => {
|
||||||
|
const state = props.toolState()
|
||||||
|
const request = props.request()
|
||||||
|
return request?.id ?? (state as any)?.input?.requestID ?? `question-${props.toolCallId()}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const questions = createMemo(() => {
|
||||||
|
const state = props.toolState()
|
||||||
|
const request = props.request()
|
||||||
|
const isQuestionTool = props.toolName() === "question"
|
||||||
|
if (!request && !isQuestionTool) return [] as QuestionPrompt[]
|
||||||
|
|
||||||
|
const questionsSource = request?.questions ?? ((state as any)?.input?.questions as any[] | undefined) ?? []
|
||||||
|
const list = Array.isArray(questionsSource) ? questionsSource : []
|
||||||
|
return list as QuestionPrompt[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const isVisible = createMemo(() => {
|
||||||
|
const request = props.request()
|
||||||
|
const isQuestionTool = props.toolName() === "question"
|
||||||
|
return Boolean(request) || isQuestionTool
|
||||||
|
})
|
||||||
|
|
||||||
|
const answers = createMemo(() => {
|
||||||
|
const state = props.toolState()
|
||||||
|
|
||||||
|
const completedAnswers =
|
||||||
|
(state as any)?.status === "completed" && Array.isArray((state as any)?.metadata?.answers)
|
||||||
|
? ((state as any).metadata.answers as string[][])
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (completedAnswers) return completedAnswers
|
||||||
|
|
||||||
|
const request = props.request()
|
||||||
|
const requestAnswers = request?.questions?.map((q) => (q as any)?.answer) // defensive (if server ever inlines)
|
||||||
|
|
||||||
|
if (Array.isArray(requestAnswers) && requestAnswers.some((row) => Array.isArray(row) && row.length > 0)) {
|
||||||
|
return requestAnswers as string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = props.draftAnswers()[requestId()] ?? []
|
||||||
|
return Array.isArray(draft) ? draft : []
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateAnswer = (questionIndex: number, next: string[]) => {
|
||||||
|
if (!props.active()) return
|
||||||
|
props.setDraftAnswers((prev) => {
|
||||||
|
const current = prev[requestId()] ?? []
|
||||||
|
const updated = [...current]
|
||||||
|
updated[questionIndex] = next
|
||||||
|
return { ...prev, [requestId()]: updated }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleOption = (questionIndex: number, label: string) => {
|
||||||
|
const info = questions()[questionIndex]
|
||||||
|
const multi = info?.multiple === true
|
||||||
|
const existing = answers()[questionIndex] ?? []
|
||||||
|
if (multi) {
|
||||||
|
const next = existing.includes(label) ? existing.filter((x) => x !== label) : [...existing, label]
|
||||||
|
updateAnswer(questionIndex, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateAnswer(questionIndex, [label])
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitDisabled = () => {
|
||||||
|
if (!props.active()) return true
|
||||||
|
if (props.submitting()) return true
|
||||||
|
return questions().some((_, index) => (answers()[index]?.length ?? 0) === 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFromCustomInput = (questionIndex: number, input: HTMLInputElement | null) => {
|
||||||
|
if (!props.active()) return
|
||||||
|
const rawValue = input?.value ?? ""
|
||||||
|
const value = rawValue
|
||||||
|
if (value.trim().length === 0) return
|
||||||
|
|
||||||
|
const info = questions()[questionIndex]
|
||||||
|
const multi = info?.multiple === true
|
||||||
|
if (!multi) {
|
||||||
|
// When switching a radio to custom, clear existing selection first.
|
||||||
|
updateAnswer(questionIndex, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOption(questionIndex, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCustomAnswer = (questionIndex: number, valuesToRemove: string[]) => {
|
||||||
|
if (!props.active()) return
|
||||||
|
if (valuesToRemove.length === 0) return
|
||||||
|
const existing = answers()[questionIndex] ?? []
|
||||||
|
const next = existing.filter((value) => !valuesToRemove.includes(value))
|
||||||
|
updateAnswer(questionIndex, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCustomTyping = (questionIndex: number, input: HTMLInputElement) => {
|
||||||
|
if (!props.active()) return
|
||||||
|
|
||||||
|
const value = input.value
|
||||||
|
const trimmed = value.trim()
|
||||||
|
const info = questions()[questionIndex]
|
||||||
|
const multi = info?.multiple === true
|
||||||
|
|
||||||
|
if (!multi) {
|
||||||
|
updateAnswer(questionIndex, trimmed.length > 0 ? [value] : [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionLabels = new Set((info?.options ?? []).map((opt) => opt.label))
|
||||||
|
const existing = answers()[questionIndex] ?? []
|
||||||
|
const last = input.dataset.lastValue ?? ""
|
||||||
|
|
||||||
|
let next = existing.filter((item) => item !== last)
|
||||||
|
|
||||||
|
if (trimmed.length > 0) {
|
||||||
|
// Only treat it as custom if it doesn't match an existing option label.
|
||||||
|
if (!optionLabels.has(trimmed) && !next.includes(value)) {
|
||||||
|
next = [...next, value]
|
||||||
|
} else if (optionLabels.has(trimmed)) {
|
||||||
|
// If they typed an existing option label, don't treat it as custom.
|
||||||
|
} else if (!next.includes(value)) {
|
||||||
|
next = [...next, value]
|
||||||
|
}
|
||||||
|
input.dataset.lastValue = value
|
||||||
|
} else {
|
||||||
|
delete input.dataset.lastValue
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnswer(questionIndex, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={isVisible() && questions().length > 0}>
|
||||||
|
<div class={`tool-call-permission ${props.active() ? "tool-call-permission-active" : "tool-call-permission-queued"}`}>
|
||||||
|
<div class="tool-call-permission-header">
|
||||||
|
<span class="tool-call-permission-label">
|
||||||
|
{props.active() ? "Question Required" : props.request() ? "Question Queued" : "Questions"}
|
||||||
|
</span>
|
||||||
|
<span class="tool-call-permission-type">{questions().length === 1 ? "Question" : "Questions"}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-call-permission-body">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<For each={questions()}>
|
||||||
|
{(q, index) => {
|
||||||
|
const i = () => index()
|
||||||
|
const multi = () => q?.multiple === true
|
||||||
|
const selected = () => answers()[i()] ?? []
|
||||||
|
const inputType = () => (multi() ? "checkbox" : "radio")
|
||||||
|
const groupName = () => `question-${requestId()}-${i()}`
|
||||||
|
const optionLabels = () => new Set((q?.options ?? []).map((opt) => opt.label))
|
||||||
|
const customSelected = () => selected().filter((value) => !optionLabels().has(value))
|
||||||
|
const customValue = () => customSelected()[0] ?? ""
|
||||||
|
const customChecked = () => customValue().length > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="rounded-md border border-base/60 bg-surface/30 p-3">
|
||||||
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
|
<div class="text-xs">
|
||||||
|
Q{i() + 1}: <span class="font-semibold">{q?.header}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={multi()}>
|
||||||
|
<div class="text-xs text-muted">Multiple</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 text-sm font-medium">{q?.question}</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-col gap-1">
|
||||||
|
<For each={q?.options ?? []}>
|
||||||
|
{(opt) => {
|
||||||
|
const checked = () => selected().includes(opt.label)
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
class={`flex items-start gap-2 py-1 ${props.active() ? "cursor-pointer" : props.request() ? "opacity-80" : ""}`}
|
||||||
|
title={opt.description}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type={inputType()}
|
||||||
|
name={groupName()}
|
||||||
|
checked={checked()}
|
||||||
|
disabled={!props.active() || props.submitting()}
|
||||||
|
onChange={() => toggleOption(i(), opt.label)}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="text-sm leading-tight">{opt.label}</div>
|
||||||
|
<div class="text-xs text-muted leading-tight">{opt.description}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class={`mt-2 flex items-start gap-2 py-1 ${props.active() ? "cursor-pointer" : props.request() ? "opacity-80" : ""}`}
|
||||||
|
title="Type a custom answer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type={inputType()}
|
||||||
|
name={groupName()}
|
||||||
|
checked={customChecked()}
|
||||||
|
disabled={!props.active() || props.submitting()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const container = e.currentTarget.closest("label")
|
||||||
|
const input = container?.querySelector("input[type='text']") as HTMLInputElement | null
|
||||||
|
if (!props.active()) return
|
||||||
|
if (customChecked()) {
|
||||||
|
clearCustomAnswer(i(), customSelected())
|
||||||
|
if (input) {
|
||||||
|
delete input.dataset.lastValue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toggleFromCustomInput(i(), input)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-1 flex-col gap-2">
|
||||||
|
<div class="text-sm leading-tight">Custom answer</div>
|
||||||
|
<input
|
||||||
|
class="w-full rounded-md border border-base/50 bg-surface px-2 py-1 text-sm"
|
||||||
|
type="text"
|
||||||
|
placeholder="Type your own answer"
|
||||||
|
disabled={!props.active() || props.submitting()}
|
||||||
|
value={customValue()}
|
||||||
|
onFocus={(e) => {
|
||||||
|
if (!props.active()) return
|
||||||
|
// Keep the radio/checkbox selected while editing.
|
||||||
|
toggleFromCustomInput(i(), e.currentTarget)
|
||||||
|
}}
|
||||||
|
onInput={(e) => handleCustomTyping(i(), e.currentTarget)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Show when={props.active()}>
|
||||||
|
<div class="tool-call-permission-actions">
|
||||||
|
<div class="tool-call-permission-buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-permission-button"
|
||||||
|
disabled={submitDisabled()}
|
||||||
|
onClick={() => props.onSubmit()}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-permission-button"
|
||||||
|
disabled={props.submitting()}
|
||||||
|
onClick={() => props.onDismiss()}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-call-permission-shortcuts">
|
||||||
|
<kbd class="kbd">Enter</kbd>
|
||||||
|
<span>Submit</span>
|
||||||
|
<kbd class="kbd">Esc</kbd>
|
||||||
|
<span>Dismiss</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.error()}>
|
||||||
|
<div class="tool-call-permission-error">{props.error()}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!props.active() && props.request()}>
|
||||||
|
<p class="tool-call-permission-queued-text">Waiting for earlier responses.</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function extractDiagnostics(state: ToolState | undefined): DiagnosticEntry[] {
|
function extractDiagnostics(state: ToolState | undefined): DiagnosticEntry[] {
|
||||||
if (!state) return []
|
if (!state) return []
|
||||||
const supportsMetadata = isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state)
|
const supportsMetadata = isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state)
|
||||||
@@ -554,15 +863,17 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const activeKey = activePermissionKey()
|
const activeKey = activePermissionKey()
|
||||||
if (!activeKey) return
|
if (!activeKey) return
|
||||||
const handler = (event: KeyboardEvent) => {
|
const handler = (event: KeyboardEvent) => {
|
||||||
|
const permission = permissionDetails()
|
||||||
|
if (!permission || !isPermissionActive()) return
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handlePermissionResponse("once")
|
void handlePermissionResponse(permission, "once")
|
||||||
} else if (event.key === "a" || event.key === "A") {
|
} else if (event.key === "a" || event.key === "A") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handlePermissionResponse("always")
|
void handlePermissionResponse(permission, "always")
|
||||||
} else if (event.key === "d" || event.key === "D") {
|
} else if (event.key === "d" || event.key === "D") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handlePermissionResponse("reject")
|
void handlePermissionResponse(permission, "reject")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("keydown", handler)
|
document.addEventListener("keydown", handler)
|
||||||
@@ -573,7 +884,6 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const [questionError, setQuestionError] = createSignal<string | null>(null)
|
const [questionError, setQuestionError] = createSignal<string | null>(null)
|
||||||
|
|
||||||
const [questionDraftAnswers, setQuestionDraftAnswers] = createSignal<Record<string, string[][]>>({})
|
const [questionDraftAnswers, setQuestionDraftAnswers] = createSignal<Record<string, string[][]>>({})
|
||||||
const [questionCustomDraft, setQuestionCustomDraft] = createSignal<Record<string, string[]>>({})
|
|
||||||
|
|
||||||
function isTextInputFocused() {
|
function isTextInputFocused() {
|
||||||
const active = document.activeElement
|
const active = document.activeElement
|
||||||
@@ -590,7 +900,10 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const answers = (questionDraftAnswers()[request.id] ?? []).map((x) => (Array.isArray(x) ? x : []))
|
const answers = (questionDraftAnswers()[request.id] ?? []).map((x) => (Array.isArray(x) ? x : []))
|
||||||
const normalized = request.questions.map((_, index) => answers[index] ?? [])
|
const normalized = request.questions.map((_, index) => {
|
||||||
|
const row = answers[index] ?? []
|
||||||
|
return row.map((value) => value.trim()).filter((value) => value.length > 0)
|
||||||
|
})
|
||||||
if (normalized.some((item) => (item?.length ?? 0) === 0)) {
|
if (normalized.some((item) => (item?.length ?? 0) === 0)) {
|
||||||
setQuestionError("Please answer all questions before submitting.")
|
setQuestionError("Please answer all questions before submitting.")
|
||||||
return
|
return
|
||||||
@@ -936,11 +1249,8 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
return renderer().renderBody(rendererContext)
|
return renderer().renderBody(rendererContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePermissionResponse(response: "once" | "always" | "reject") {
|
async function handlePermissionResponse(permission: PermissionRequestLike, response: "once" | "always" | "reject") {
|
||||||
const permission = permissionDetails()
|
if (!permission) return
|
||||||
if (!permission || !isPermissionActive()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setPermissionSubmitting(true)
|
setPermissionSubmitting(true)
|
||||||
setPermissionError(null)
|
setPermissionError(null)
|
||||||
try {
|
try {
|
||||||
@@ -1006,37 +1316,37 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show when={!active}>
|
||||||
when={active}
|
<p class="tool-call-permission-queued-text">Waiting for earlier permission responses.</p>
|
||||||
fallback={<p class="tool-call-permission-queued-text">Waiting for earlier permission responses.</p>}
|
</Show>
|
||||||
>
|
<div class="tool-call-permission-actions">
|
||||||
<div class="tool-call-permission-actions">
|
<div class="tool-call-permission-buttons">
|
||||||
<div class="tool-call-permission-buttons">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="tool-call-permission-button"
|
||||||
class="tool-call-permission-button"
|
disabled={permissionSubmitting()}
|
||||||
disabled={permissionSubmitting()}
|
onClick={() => void handlePermissionResponse(permission, "once")}
|
||||||
onClick={() => handlePermissionResponse("once")}
|
>
|
||||||
>
|
Allow Once
|
||||||
Allow Once
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="tool-call-permission-button"
|
||||||
class="tool-call-permission-button"
|
disabled={permissionSubmitting()}
|
||||||
disabled={permissionSubmitting()}
|
onClick={() => void handlePermissionResponse(permission, "always")}
|
||||||
onClick={() => handlePermissionResponse("always")}
|
>
|
||||||
>
|
Always Allow
|
||||||
Always Allow
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="tool-call-permission-button"
|
||||||
class="tool-call-permission-button"
|
disabled={permissionSubmitting()}
|
||||||
disabled={permissionSubmitting()}
|
onClick={() => void handlePermissionResponse(permission, "reject")}
|
||||||
onClick={() => handlePermissionResponse("reject")}
|
>
|
||||||
>
|
Deny
|
||||||
Deny
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<Show when={active}>
|
||||||
<div class="tool-call-permission-shortcuts">
|
<div class="tool-call-permission-shortcuts">
|
||||||
<kbd class="kbd">Enter</kbd>
|
<kbd class="kbd">Enter</kbd>
|
||||||
<span>Allow once</span>
|
<span>Allow once</span>
|
||||||
@@ -1045,206 +1355,31 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
<kbd class="kbd">D</kbd>
|
<kbd class="kbd">D</kbd>
|
||||||
<span>Deny</span>
|
<span>Deny</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Show when={permissionError()}>
|
|
||||||
<div class="tool-call-permission-error">{permissionError()}</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={permissionError()}>
|
||||||
|
<div class="tool-call-permission-error">{permissionError()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderQuestionBlock = () => {
|
const renderQuestionBlock = () => (
|
||||||
const state = toolState()
|
<QuestionToolBlock
|
||||||
const request = questionDetails()
|
toolName={toolName}
|
||||||
const isQuestionTool = toolName() === "question"
|
toolState={toolState}
|
||||||
|
toolCallId={toolCallIdentifier}
|
||||||
if (!request && !isQuestionTool) return null
|
request={questionDetails}
|
||||||
|
active={isQuestionActive}
|
||||||
const questionsSource = request?.questions ?? ((state as any)?.input?.questions as any[] | undefined) ?? []
|
submitting={questionSubmitting}
|
||||||
const questions = Array.isArray(questionsSource) ? questionsSource : []
|
error={questionError}
|
||||||
if (questions.length === 0) return null
|
draftAnswers={questionDraftAnswers}
|
||||||
|
setDraftAnswers={setQuestionDraftAnswers}
|
||||||
const requestId = request?.id ?? (state as any)?.input?.requestID ?? `question-${toolCallMemo()?.id ?? "unknown"}`
|
onSubmit={() => void handleQuestionSubmit()}
|
||||||
const active = Boolean(request && isQuestionActive())
|
onDismiss={() => void handleQuestionDismiss()}
|
||||||
|
/>
|
||||||
const completedAnswers = Array.isArray((state as any)?.metadata?.answers) ? ((state as any).metadata.answers as string[][]) : undefined
|
)
|
||||||
const answers = completedAnswers ?? questionDraftAnswers()[requestId] ?? []
|
|
||||||
const customInputs = questionCustomDraft()[requestId] ?? []
|
|
||||||
|
|
||||||
const updateAnswer = (questionIndex: number, next: string[]) => {
|
|
||||||
if (!active) return
|
|
||||||
setQuestionDraftAnswers((prev) => {
|
|
||||||
const current = prev[requestId] ?? []
|
|
||||||
const updated = [...current]
|
|
||||||
updated[questionIndex] = next
|
|
||||||
return { ...prev, [requestId]: updated }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCustom = (questionIndex: number, value: string) => {
|
|
||||||
if (!active) return
|
|
||||||
setQuestionCustomDraft((prev) => {
|
|
||||||
const current = prev[requestId] ?? []
|
|
||||||
const updated = [...current]
|
|
||||||
updated[questionIndex] = value
|
|
||||||
return { ...prev, [requestId]: updated }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleOption = (questionIndex: number, label: string) => {
|
|
||||||
const info = questions[questionIndex]
|
|
||||||
const multi = info?.multiple === true
|
|
||||||
const existing = answers[questionIndex] ?? []
|
|
||||||
if (multi) {
|
|
||||||
const next = existing.includes(label) ? existing.filter((x) => x !== label) : [...existing, label]
|
|
||||||
updateAnswer(questionIndex, next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateAnswer(questionIndex, [label])
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitDisabled = () => {
|
|
||||||
if (!active) return true
|
|
||||||
if (questionSubmitting()) return true
|
|
||||||
return questions.some((_, index) => (answers[index]?.length ?? 0) === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showButtons = () => active
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={`tool-call-permission ${active ? "tool-call-permission-active" : "tool-call-permission-queued"}`}>
|
|
||||||
<div class="tool-call-permission-header">
|
|
||||||
<span class="tool-call-permission-label">
|
|
||||||
{active ? "Question Required" : request ? "Question Queued" : "Questions"}
|
|
||||||
</span>
|
|
||||||
<span class="tool-call-permission-type">{questions.length === 1 ? "Question" : "Questions"}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tool-call-permission-body">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<For each={questions}>
|
|
||||||
{(q, index) => {
|
|
||||||
const i = () => index()
|
|
||||||
const multi = () => q?.multiple === true
|
|
||||||
const selected = () => answers[i()] ?? []
|
|
||||||
const customValue = () => customInputs[i()] ?? ""
|
|
||||||
const inputType = () => (multi() ? "checkbox" : "radio")
|
|
||||||
const groupName = () => `question-${requestId}-${i()}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="rounded-md border border-base/60 bg-surface/30 p-3">
|
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
|
||||||
<div class="text-xs">
|
|
||||||
Q{i() + 1}: <span class="font-semibold">{q?.header}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={multi()}>
|
|
||||||
<div class="text-xs text-muted">Multiple</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-1 text-sm font-medium">{q?.question}</div>
|
|
||||||
|
|
||||||
<div class="mt-3 flex flex-col gap-1">
|
|
||||||
<For each={q?.options ?? []}>
|
|
||||||
{(opt) => {
|
|
||||||
const checked = () => selected().includes(opt.label)
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
class={`flex items-start gap-2 py-1 ${active ? "cursor-pointer" : request ? "opacity-80" : ""}`}
|
|
||||||
title={opt.description}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type={inputType()}
|
|
||||||
name={groupName()}
|
|
||||||
checked={checked()}
|
|
||||||
disabled={!active || questionSubmitting()}
|
|
||||||
onChange={() => toggleOption(i(), opt.label)}
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="text-sm leading-tight">{opt.label}</div>
|
|
||||||
<div class="text-xs text-muted leading-tight">{opt.description}</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Show when={active}>
|
|
||||||
<div class="mt-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
class="flex-1 rounded-md border border-base/50 bg-surface px-2 py-1 text-sm"
|
|
||||||
type="text"
|
|
||||||
placeholder="Type your own answer"
|
|
||||||
value={customValue()}
|
|
||||||
disabled={!active || questionSubmitting()}
|
|
||||||
onInput={(e) => updateCustom(i(), e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tool-call-permission-button"
|
|
||||||
disabled={!active || questionSubmitting() || !customValue().trim()}
|
|
||||||
onClick={() => {
|
|
||||||
const value = customValue().trim()
|
|
||||||
if (!value) return
|
|
||||||
updateCustom(i(), value)
|
|
||||||
toggleOption(i(), value)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{multi() ? "Toggle" : "Select"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Show when={showButtons()}>
|
|
||||||
<div class="tool-call-permission-actions">
|
|
||||||
<div class="tool-call-permission-buttons">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tool-call-permission-button"
|
|
||||||
disabled={submitDisabled()}
|
|
||||||
onClick={() => handleQuestionSubmit()}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tool-call-permission-button"
|
|
||||||
disabled={questionSubmitting()}
|
|
||||||
onClick={() => handleQuestionDismiss()}
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tool-call-permission-shortcuts">
|
|
||||||
<kbd class="kbd">Enter</kbd>
|
|
||||||
<span>Submit</span>
|
|
||||||
<kbd class="kbd">Esc</kbd>
|
|
||||||
<span>Dismiss</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={questionError()}>
|
|
||||||
<div class="tool-call-permission-error">{questionError()}</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!active && request}>
|
|
||||||
<p class="tool-call-permission-queued-text">Waiting for earlier responses.</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const request = questionDetails()
|
const request = questionDetails()
|
||||||
@@ -1260,11 +1395,7 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const initial = request.questions.map(() => [])
|
const initial = request.questions.map(() => [])
|
||||||
return { ...prev, [requestId]: initial }
|
return { ...prev, [requestId]: initial }
|
||||||
})
|
})
|
||||||
setQuestionCustomDraft((prev) => {
|
|
||||||
if (prev[requestId]) return prev
|
|
||||||
const initial = request.questions.map(() => "")
|
|
||||||
return { ...prev, [requestId]: initial }
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const status = () => toolState()?.status || ""
|
const status = () => toolState()?.status || ""
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
||||||
scrollToSelected()
|
scrollToSelected()
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter" || e.key === "Tab") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const selected = items[selectedIndex()]
|
const selected = items[selectedIndex()]
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -534,7 +534,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
|
|||||||
|
|
||||||
<div class="dropdown-footer">
|
<div class="dropdown-footer">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">↑↓</span> navigate • <span class="font-medium">Enter</span> select •{" "}
|
<span class="font-medium">↑↓</span> navigate • <span class="font-medium">Tab/Enter</span> select •{" "}
|
||||||
<span class="font-medium">Esc</span> close
|
<span class="font-medium">Esc</span> close
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
38
packages/ui/src/components/version-pill.tsx
Normal file
38
packages/ui/src/components/version-pill.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Show, createEffect, createSignal } from "solid-js"
|
||||||
|
import type { ServerMeta } from "../../../server/src/api-types"
|
||||||
|
import { getServerMeta } from "../lib/server-meta"
|
||||||
|
|
||||||
|
export default function VersionPill() {
|
||||||
|
const [meta, setMeta] = createSignal<ServerMeta | null>(null)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
void getServerMeta()
|
||||||
|
.then((result) => setMeta(result))
|
||||||
|
.catch(() => setMeta(null))
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverVersion = () => meta()?.serverVersion
|
||||||
|
const uiVersion = () => meta()?.ui?.version
|
||||||
|
const uiSource = () => meta()?.ui?.source
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={serverVersion() || uiVersion() || uiSource()}>
|
||||||
|
<div class="text-[11px] text-muted whitespace-nowrap">
|
||||||
|
<Show when={serverVersion()}>
|
||||||
|
{(v) => <span>App {v()}</span>}
|
||||||
|
</Show>
|
||||||
|
<Show when={uiVersion() || uiSource()}>
|
||||||
|
<>
|
||||||
|
<Show when={serverVersion()}>
|
||||||
|
<span class="mx-2">·</span>
|
||||||
|
</Show>
|
||||||
|
<span>
|
||||||
|
UI{uiVersion() ? ` ${uiVersion()}` : ""}
|
||||||
|
<Show when={uiSource()}>{(s) => <span class="opacity-70"> ({s()})</span>}</Show>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -92,6 +92,19 @@ function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instanc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureActiveInstanceSelected(): void {
|
||||||
|
const current = activeInstanceId()
|
||||||
|
const instanceMap = instances()
|
||||||
|
if (current && instanceMap.has(current)) return
|
||||||
|
|
||||||
|
for (const [id, instance] of instanceMap.entries()) {
|
||||||
|
if (instance.status === "ready") {
|
||||||
|
setActiveInstanceId(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
||||||
const mapped = workspaceDescriptorToInstance(descriptor)
|
const mapped = workspaceDescriptorToInstance(descriptor)
|
||||||
if (instances().has(descriptor.id)) {
|
if (instances().has(descriptor.id)) {
|
||||||
@@ -102,6 +115,9 @@ function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
|||||||
|
|
||||||
if (descriptor.status === "ready") {
|
if (descriptor.status === "ready") {
|
||||||
attachClient(descriptor)
|
attachClient(descriptor)
|
||||||
|
// If no tab is currently selected (common after UI refresh),
|
||||||
|
// auto-select the first ready instance.
|
||||||
|
ensureActiveInstanceSelected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,15 +241,18 @@ async function hydrateInstanceData(instanceId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async function initializeWorkspaces() {
|
void (async function initializeWorkspaces() {
|
||||||
try {
|
try {
|
||||||
const workspaces = await serverApi.fetchWorkspaces()
|
const workspaces = await serverApi.fetchWorkspaces()
|
||||||
workspaces.forEach((workspace) => upsertWorkspace(workspace))
|
workspaces.forEach((workspace) => upsertWorkspace(workspace))
|
||||||
|
// After a UI refresh, we may have instances but no active selection.
|
||||||
|
ensureActiveInstanceSelected()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to load workspaces", error)
|
log.error("Failed to load workspaces", error)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
||||||
serverEvents.on("*", (event) => handleWorkspaceEvent(event))
|
serverEvents.on("*", (event) => handleWorkspaceEvent(event))
|
||||||
|
|
||||||
function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import { createEffect, createSignal } from "solid-js"
|
import { createEffect, createSignal } from "solid-js"
|
||||||
import type { LatestReleaseInfo, WorkspaceEventPayload } from "../../../server/src/api-types"
|
import type { SupportMeta } from "../../../server/src/api-types"
|
||||||
import { getServerMeta } from "../lib/server-meta"
|
import { getServerMeta } from "../lib/server-meta"
|
||||||
import { serverEvents } from "../lib/server-events"
|
|
||||||
import { showToastNotification, ToastHandle } from "../lib/notifications"
|
import { showToastNotification, ToastHandle } from "../lib/notifications"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { hasInstances, showFolderSelection } from "./ui"
|
import { hasInstances, showFolderSelection } from "./ui"
|
||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
const [availableRelease, setAvailableRelease] = createSignal<LatestReleaseInfo | null>(null)
|
const [supportInfo, setSupportInfo] = createSignal<SupportMeta | null>(null)
|
||||||
|
|
||||||
let initialized = false
|
let initialized = false
|
||||||
let visibilityEffectInitialized = false
|
let visibilityEffectInitialized = false
|
||||||
let activeToast: ToastHandle | null = null
|
let activeToast: ToastHandle | null = null
|
||||||
let activeToastVersion: string | null = null
|
let activeToastKey: string | null = null
|
||||||
|
|
||||||
function dismissActiveToast() {
|
function dismissActiveToast() {
|
||||||
if (activeToast) {
|
if (activeToast) {
|
||||||
activeToast.dismiss()
|
activeToast.dismiss()
|
||||||
activeToast = null
|
activeToast = null
|
||||||
activeToastVersion = null
|
activeToastKey = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,28 +29,34 @@ function ensureVisibilityEffect() {
|
|||||||
visibilityEffectInitialized = true
|
visibilityEffectInitialized = true
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const release = availableRelease()
|
const support = supportInfo()
|
||||||
const shouldShow = Boolean(release) && (!hasInstances() || showFolderSelection())
|
const shouldShow = Boolean(support && support.supported === false) && (!hasInstances() || showFolderSelection())
|
||||||
|
|
||||||
if (!shouldShow || !release) {
|
if (!shouldShow || !support || support.supported !== false) {
|
||||||
dismissActiveToast()
|
dismissActiveToast()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeToast || activeToastVersion !== release.version) {
|
const key = `${support.minServerVersion ?? "unknown"}:${support.latestServerVersion ?? "unknown"}`
|
||||||
|
|
||||||
|
if (!activeToast || activeToastKey !== key) {
|
||||||
dismissActiveToast()
|
dismissActiveToast()
|
||||||
activeToast = showToastNotification({
|
activeToast = showToastNotification({
|
||||||
title: `CodeNomad ${release.version}`,
|
title: support.message ?? "Upgrade required",
|
||||||
message: release.channel === "dev" ? "Dev release build available." : "New stable build on GitHub.",
|
message: support.latestServerVersion
|
||||||
|
? `Update to CodeNomad ${support.latestServerVersion} to use the latest UI.`
|
||||||
|
: "Update CodeNomad to use the latest UI.",
|
||||||
variant: "info",
|
variant: "info",
|
||||||
duration: Number.POSITIVE_INFINITY,
|
duration: Number.POSITIVE_INFINITY,
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
action: {
|
action: support.latestServerUrl
|
||||||
label: "View release",
|
? {
|
||||||
href: release.url,
|
label: "Get update",
|
||||||
},
|
href: support.latestServerUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
activeToastVersion = release.version
|
activeToastKey = key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -64,32 +69,17 @@ export function initReleaseNotifications() {
|
|||||||
|
|
||||||
ensureVisibilityEffect()
|
ensureVisibilityEffect()
|
||||||
void refreshFromMeta()
|
void refreshFromMeta()
|
||||||
|
|
||||||
serverEvents.on("app.releaseAvailable", (event) => {
|
|
||||||
const typedEvent = event as Extract<WorkspaceEventPayload, { type: "app.releaseAvailable" }>
|
|
||||||
applyRelease(typedEvent.release)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshFromMeta() {
|
async function refreshFromMeta() {
|
||||||
try {
|
try {
|
||||||
const meta = await getServerMeta(true)
|
const meta = await getServerMeta(true)
|
||||||
if (meta.latestRelease) {
|
setSupportInfo(meta.support ?? null)
|
||||||
applyRelease(meta.latestRelease)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn("Unable to load server metadata for release info", error)
|
log.warn("Unable to load server metadata for support info", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyRelease(release: LatestReleaseInfo | null | undefined) {
|
export function useSupportInfo() {
|
||||||
if (!release) {
|
return supportInfo
|
||||||
setAvailableRelease(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setAvailableRelease(release)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAvailableRelease() {
|
|
||||||
return availableRelease
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { loadMessages } from "./session-api"
|
|||||||
import {
|
import {
|
||||||
applyPartUpdateV2,
|
applyPartUpdateV2,
|
||||||
replaceMessageIdV2,
|
replaceMessageIdV2,
|
||||||
|
reconcilePendingQuestionsV2,
|
||||||
upsertMessageInfoV2,
|
upsertMessageInfoV2,
|
||||||
upsertPermissionV2,
|
upsertPermissionV2,
|
||||||
upsertQuestionV2,
|
upsertQuestionV2,
|
||||||
@@ -230,6 +231,10 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
|
|
||||||
applyPartUpdateV2(instanceId, { ...part, sessionID: sessionId, messageID: messageId })
|
applyPartUpdateV2(instanceId, { ...part, sessionID: sessionId, messageID: messageId })
|
||||||
|
|
||||||
|
if (part.type === "tool" && part.tool === "question") {
|
||||||
|
// Questions can arrive before their tool part exists; re-link now.
|
||||||
|
reconcilePendingQuestionsV2(instanceId, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
} else if (event.type === "message.updated") {
|
} else if (event.type === "message.updated") {
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
|
import fs from "fs"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
import solid from "vite-plugin-solid"
|
import solid from "vite-plugin-solid"
|
||||||
import { resolve } from "path"
|
import { resolve } from "path"
|
||||||
|
|
||||||
|
const uiPackageJson = JSON.parse(fs.readFileSync(resolve(__dirname, "package.json"), "utf-8")) as { version?: string }
|
||||||
|
const uiVersion = uiPackageJson.version ?? "0.0.0"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
root: "./src/renderer",
|
root: "./src/renderer",
|
||||||
plugins: [solid()],
|
plugins: [
|
||||||
|
solid(),
|
||||||
|
{
|
||||||
|
name: "emit-ui-version",
|
||||||
|
generateBundle() {
|
||||||
|
this.emitFile({
|
||||||
|
type: "asset",
|
||||||
|
fileName: "ui-version.json",
|
||||||
|
source: JSON.stringify({ uiVersion }, null, 2),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: "./postcss.config.js",
|
postcss: "./postcss.config.js",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user