Merge pull request #87 from NeuralNomadsAI/dev

Release 0.7.6 - Question tool fixes + Split test
This commit is contained in:
Shantur Rathore
2026-01-22 17:20:19 +00:00
committed by GitHub
27 changed files with 3397 additions and 147 deletions

43
.github/workflows/release-ui.yml vendored Normal file
View 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
View File

@@ -7,4 +7,9 @@ release/
.electron-vite/
out/
.dir-locals.el
.opencode/bashOutputs/
.opencode/bashOutputs/
# Local runtime artifacts
.codenomad/
.tmp/
packages/cloudflare/.wrangler/

19
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codenomad-workspace",
"version": "0.7.5",
"version": "0.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codenomad-workspace",
"version": "0.7.5",
"version": "0.7.6",
"dependencies": {
"7zip-bin": "^5.2.0",
"google-auth-library": "^10.5.0"
@@ -1632,7 +1632,6 @@
"version": "2.10.3",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
@@ -2271,7 +2270,6 @@
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -3674,7 +3672,6 @@
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
@@ -5352,7 +5349,6 @@
},
"node_modules/pend": {
"version": "1.2.0",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
@@ -7324,7 +7320,6 @@
},
"node_modules/yauzl": {
"version": "2.10.0",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
@@ -7389,7 +7384,7 @@
},
"packages/electron-app": {
"name": "@neuralnomads/codenomad-electron-app",
"version": "0.7.5",
"version": "0.7.6",
"dependencies": {
"@codenomad/ui": "file:../ui",
"@neuralnomads/codenomad": "file:../server"
@@ -7423,7 +7418,7 @@
},
"packages/server": {
"name": "@neuralnomads/codenomad",
"version": "0.7.5",
"version": "0.7.6",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",
@@ -7433,12 +7428,14 @@
"fuzzysort": "^2.0.4",
"pino": "^9.4.0",
"undici": "^6.19.8",
"yauzl": "^2.10.0",
"zod": "^3.23.8"
},
"bin": {
"codenomad": "dist/bin.js"
},
"devDependencies": {
"@types/yauzl": "^2.10.0",
"cross-env": "^7.0.3",
"ts-node": "^10.9.2",
"tsx": "^4.20.6",
@@ -7458,14 +7455,14 @@
},
"packages/tauri-app": {
"name": "@codenomad/tauri-app",
"version": "0.7.5",
"version": "0.7.6",
"devDependencies": {
"@tauri-apps/cli": "^2.9.4"
}
},
"packages/ui": {
"name": "@codenomad/ui",
"version": "0.7.5",
"version": "0.7.6",
"dependencies": {
"@git-diff-view/solid": "^0.0.8",
"@kobalte/core": "0.13.11",

View File

@@ -1,6 +1,6 @@
{
"name": "codenomad-workspace",
"version": "0.7.5",
"version": "0.7.6",
"private": true,
"description": "CodeNomad monorepo workspace",
"workspaces": {

1
packages/cloudflare/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist/

1515
packages/cloudflare/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -0,0 +1,4 @@
{
"minServerVersion": "0.7.5",
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
}

View 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}`)

View 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 })
}

View 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 })
},
}

View 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"]

View File

@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad-electron-app",
"version": "0.7.5",
"version": "0.7.6",
"description": "CodeNomad - AI coding assistant",
"author": {
"name": "Neural Nomads",

View File

@@ -1,20 +1,30 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.7.5",
"version": "0.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@neuralnomads/codenomad",
"version": "0.7.5",
"version": "0.7.6",
"dependencies": {
"@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0",
"@fastify/static": "^7.0.4",
"commander": "^12.1.0",
"fastify": "^4.28.1",
"fuzzysort": "^2.0.4",
"pino": "^9.4.0",
"undici": "^6.19.8",
"yauzl": "^2.10.0",
"zod": "^3.23.8"
},
"bin": {
"codenomad": "dist/bin.js"
},
"devDependencies": {
"@types/yauzl": "^2.10.0",
"cross-env": "^7.0.3",
"ts-node": "^10.9.2",
"tsx": "^4.20.6",
"typescript": "^5.6.3"
@@ -475,6 +485,15 @@
"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": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz",
@@ -486,6 +505,15 @@
"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": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz",
@@ -520,6 +548,77 @@
"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": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -548,12 +647,31 @@
"@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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"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": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -593,6 +711,16 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@@ -674,6 +802,30 @@
],
"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": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -700,6 +852,48 @@
"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": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -709,6 +903,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": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
@@ -725,6 +931,48 @@
"dev": true,
"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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -735,6 +983,27 @@
"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": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -777,6 +1046,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": {
"version": "1.1.0",
"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"
}
},
"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": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz",
@@ -905,6 +1189,22 @@
"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": {
"version": "0.2.0",
"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_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": {
"version": "4.13.0",
"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"
}
},
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -951,6 +1299,36 @@
"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": {
"version": "1.0.1",
"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"
}
},
"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": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -984,6 +1368,42 @@
"dev": true,
"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": {
"version": "0.39.6",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz",
@@ -1008,6 +1428,52 @@
"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": {
"version": "9.14.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
@@ -1139,6 +1605,26 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"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": {
"version": "3.1.0",
"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==",
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
@@ -1199,6 +1724,111 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
@@ -1217,6 +1847,15 @@
"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": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -1296,6 +1935,15 @@
"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": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -1310,6 +1958,128 @@
"dev": true,
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@neuralnomads/codenomad",
"version": "0.7.5",
"version": "0.7.6",
"description": "CodeNomad Server",
"author": {
"name": "Neural Nomads",
@@ -32,9 +32,11 @@
"fuzzysort": "^2.0.4",
"pino": "^9.4.0",
"undici": "^6.19.8",
"yauzl": "^2.10.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/yauzl": "^2.10.0",
"cross-env": "^7.0.3",
"ts-node": "^10.9.2",
"tsx": "^4.20.6",

View File

@@ -167,7 +167,6 @@ export type WorkspaceEventType =
| "instance.dataChanged"
| "instance.event"
| "instance.eventStatus"
| "app.releaseAvailable"
export type WorkspaceEventPayload =
| { type: "workspace.created"; workspace: WorkspaceDescriptor }
@@ -180,7 +179,6 @@ export type WorkspaceEventPayload =
| { type: "instance.dataChanged"; instanceId: string; data: InstanceData }
| { type: "instance.event"; instanceId: string; event: InstanceStreamEvent }
| { type: "instance.eventStatus"; instanceId: string; status: InstanceStreamStatus; reason?: string }
| { type: "app.releaseAvailable"; release: LatestReleaseInfo }
export interface NetworkAddress {
ip: string
@@ -198,6 +196,19 @@ export interface LatestReleaseInfo {
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 {
/** Base URL clients should target for REST calls (useful for Electron embedding). */
httpBaseUrl: string
@@ -215,8 +226,9 @@ export interface ServerMeta {
workspaceRoot: string
/** Reachable addresses for this server, external first. */
addresses: NetworkAddress[]
/** Optional metadata about the most recent public release. */
latestRelease?: LatestReleaseInfo
serverVersion?: string
ui?: UiMeta
support?: SupportMeta
}
export type BackgroundProcessStatus = "running" | "stopped" | "error"

View File

@@ -29,7 +29,6 @@ export class EventBus extends EventEmitter {
this.on("instance.dataChanged", handler)
this.on("instance.event", handler)
this.on("instance.eventStatus", handler)
this.on("app.releaseAvailable", handler)
return () => {
this.off("workspace.created", handler)
this.off("workspace.started", handler)
@@ -41,7 +40,6 @@ export class EventBus extends EventEmitter {
this.off("instance.dataChanged", handler)
this.off("instance.event", handler)
this.off("instance.eventStatus", handler)
this.off("app.releaseAvailable", handler)
}
}
}

View File

@@ -17,7 +17,7 @@ import { InstanceStore } from "./storage/instance-store"
import { InstanceEventBridge } from "./workspaces/instance-events"
import { createLogger } from "./logger"
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"
const require = createRequire(import.meta.url)
@@ -37,6 +37,9 @@ interface CliOptions {
logDestination?: string
uiStaticDir: string
uiDevServer?: string
uiAutoUpdate: boolean
uiNoUpdate: boolean
uiManifestUrl?: string
launch: boolean
authUsername: 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),
)
.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("--username <username>", "Username for server authentication")
@@ -91,6 +97,9 @@ function parseCliOptions(argv: string[]): CliOptions {
logDestination?: string
uiDir: string
uiDevServer?: string
uiNoUpdate?: boolean
uiAutoUpdate?: string
uiManifestUrl?: string
launch?: boolean
username: string
password?: string
@@ -101,6 +110,9 @@ function parseCliOptions(argv: string[]): CliOptions {
const normalizedHost = resolveHost(parsed.host)
const autoUpdateString = (parsed.uiAutoUpdate ?? "true").trim().toLowerCase()
const uiAutoUpdate = autoUpdateString === "1" || autoUpdateString === "true" || autoUpdateString === "yes"
return {
port: parsed.port,
host: normalizedHost,
@@ -111,6 +123,9 @@ function parseCliOptions(argv: string[]): CliOptions {
logDestination: parsed.logDestination,
uiStaticDir: parsed.uiDir,
uiDevServer: parsed.uiDevServer,
uiAutoUpdate,
uiNoUpdate: Boolean(parsed.uiNoUpdate),
uiManifestUrl: parsed.uiManifestUrl,
launch: Boolean(parsed.launch),
authUsername: parsed.username,
authPassword: parsed.password,
@@ -141,6 +156,10 @@ function resolveHost(input: string | undefined): string {
return trimmed
}
function programHasArg(argv: string[], flag: string): boolean {
return argv.includes(flag)
}
async function main() {
const options = parseCliOptions(process.argv.slice(2))
const logger = createLogger({ level: options.logLevel, destination: options.logDestination, component: "app" })
@@ -205,19 +224,36 @@ async function main() {
logger: logger.child({ component: "instance-events" }),
})
const releaseMonitor = startReleaseMonitor({
currentVersion: packageJson.version,
logger: logger.child({ component: "release-monitor" }),
onUpdate: (release) => {
if (release) {
serverMeta.latestRelease = release
eventBus.publish({ type: "app.releaseAvailable", release })
} else {
delete serverMeta.latestRelease
}
},
const uiDirEnvOverride = Boolean(process.env.CLI_UI_DIR)
const uiDirCliOverride = programHasArg(process.argv.slice(2), "--ui-dir")
const uiOverrideIsExplicit = uiDirEnvOverride || uiDirCliOverride
const uiDirOverride = uiOverrideIsExplicit ? options.uiStaticDir : undefined
const autoUpdateEnabled = options.uiAutoUpdate && !options.uiNoUpdate
const uiResolution = await resolveUi({
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({
host: options.host,
port: options.port,
@@ -229,8 +265,8 @@ async function main() {
serverMeta,
instanceStore,
authManager,
uiStaticDir: options.uiStaticDir,
uiDevServerUrl: options.uiDevServer,
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
uiDevServerUrl: uiResolution.uiDevServerUrl,
logger,
})
@@ -266,7 +302,7 @@ async function main() {
logger.error({ err: error }, "Workspace manager shutdown failed")
}
releaseMonitor.stop()
// no-op: remote UI manifest replaces GitHub release monitor
logger.info("Exiting process")
process.exit(0)

View 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]),
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/tauri-app",
"version": "0.7.5",
"version": "0.7.6",
"private": true,
"scripts": {
"dev": "tauri dev",

View File

@@ -1,6 +1,6 @@
{
"name": "@codenomad/ui",
"version": "0.7.5",
"version": "0.7.6",
"private": true,
"type": "module",
"scripts": {

View File

@@ -5,6 +5,7 @@ import AdvancedSettingsModal from "./advanced-settings-modal"
import DirectoryBrowserDialog from "./directory-browser-dialog"
import Kbd from "./kbd"
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
@@ -248,6 +249,9 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</div>
<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>
<div class="mt-2 flex justify-center">
<VersionPill />
</div>
</div>

View File

@@ -7,8 +7,7 @@ import {
getPermissionQueue,
getQuestionQueue,
getQuestionEnqueuedAtForInstance,
setActivePermissionIdForInstance,
setActiveQuestionIdForInstance,
sendPermissionResponse,
} from "../stores/instances"
import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions"
import { messageStoreBus } from "../stores/message-v2/bus"
@@ -132,6 +131,45 @@ function resolveToolCallFromQuestion(instanceId: string, request: QuestionReques
const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props) => {
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 questionQueue = createMemo(() => getQuestionQueue(props.instanceId))
@@ -265,19 +303,10 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
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 (
<div
class={`permission-center-item${isActive() ? " permission-center-item-active" : ""}`}
role="listitem"
onClick={handleActivate}
>
<div class="permission-center-item-header">
<div class="permission-center-item-heading">
@@ -315,17 +344,52 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
</div>
</div>
<Show
when={resolved()}
fallback={
<div class="permission-center-fallback">
<div class="permission-center-fallback-title">
<code>{primaryTitle()}</code>
<Show
when={resolved()}
fallback={
<div class="permission-center-fallback">
<div class="permission-center-fallback-title">
<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 class="permission-center-fallback-hint">Load session for more information.</div>
</div>
}
>
}
>
{(data) => (
<ToolCall
toolCall={data().toolPart}

View File

@@ -7,6 +7,7 @@ import { useGlobalCache } from "../lib/hooks/use-global-cache"
import { useConfig } from "../stores/preferences"
import type { DiffViewMode } from "../stores/preferences"
import { activeInterruption, sendPermissionResponse, sendQuestionReject, sendQuestionReply } from "../stores/instances"
import type { PermissionRequestLike } from "../types/permission"
import { getPermissionDisplayTitle, getPermissionKind, getPermissionSessionId } from "../types/permission"
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
import type { TextPart, RenderCache } from "../types/message"
@@ -205,8 +206,9 @@ function QuestionToolBlock(props: QuestionToolBlockProps) {
const toggleFromCustomInput = (questionIndex: number, input: HTMLInputElement | null) => {
if (!props.active()) return
const value = input?.value?.trim() ?? ""
if (!value) return
const rawValue = input?.value ?? ""
const value = rawValue
if (value.trim().length === 0) return
const info = questions()[questionIndex]
const multi = info?.multiple === true
@@ -229,12 +231,13 @@ function QuestionToolBlock(props: QuestionToolBlockProps) {
const handleCustomTyping = (questionIndex: number, input: HTMLInputElement) => {
if (!props.active()) return
const value = input.value.trim()
const value = input.value
const trimmed = value.trim()
const info = questions()[questionIndex]
const multi = info?.multiple === true
if (!multi) {
updateAnswer(questionIndex, value ? [value] : [])
updateAnswer(questionIndex, trimmed.length > 0 ? [value] : [])
return
}
@@ -244,10 +247,11 @@ function QuestionToolBlock(props: QuestionToolBlockProps) {
let next = existing.filter((item) => item !== last)
if (value) {
if (!optionLabels.has(value) && !next.includes(value)) {
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(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]
@@ -859,15 +863,17 @@ export default function ToolCall(props: ToolCallProps) {
const activeKey = activePermissionKey()
if (!activeKey) return
const handler = (event: KeyboardEvent) => {
const permission = permissionDetails()
if (!permission || !isPermissionActive()) return
if (event.key === "Enter") {
event.preventDefault()
handlePermissionResponse("once")
void handlePermissionResponse(permission, "once")
} else if (event.key === "a" || event.key === "A") {
event.preventDefault()
handlePermissionResponse("always")
void handlePermissionResponse(permission, "always")
} else if (event.key === "d" || event.key === "D") {
event.preventDefault()
handlePermissionResponse("reject")
void handlePermissionResponse(permission, "reject")
}
}
document.addEventListener("keydown", handler)
@@ -894,7 +900,10 @@ export default function ToolCall(props: ToolCallProps) {
return
}
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)) {
setQuestionError("Please answer all questions before submitting.")
return
@@ -1240,11 +1249,8 @@ export default function ToolCall(props: ToolCallProps) {
return renderer().renderBody(rendererContext)
}
async function handlePermissionResponse(response: "once" | "always" | "reject") {
const permission = permissionDetails()
if (!permission || !isPermissionActive()) {
return
}
async function handlePermissionResponse(permission: PermissionRequestLike, response: "once" | "always" | "reject") {
if (!permission) return
setPermissionSubmitting(true)
setPermissionError(null)
try {
@@ -1310,37 +1316,37 @@ export default function ToolCall(props: ToolCallProps) {
</div>
)}
</Show>
<Show
when={active}
fallback={<p class="tool-call-permission-queued-text">Waiting for earlier permission responses.</p>}
>
<div class="tool-call-permission-actions">
<div class="tool-call-permission-buttons">
<button
type="button"
class="tool-call-permission-button"
disabled={permissionSubmitting()}
onClick={() => handlePermissionResponse("once")}
>
Allow Once
</button>
<button
type="button"
class="tool-call-permission-button"
disabled={permissionSubmitting()}
onClick={() => handlePermissionResponse("always")}
>
Always Allow
</button>
<button
type="button"
class="tool-call-permission-button"
disabled={permissionSubmitting()}
onClick={() => handlePermissionResponse("reject")}
>
Deny
</button>
</div>
<Show when={!active}>
<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-buttons">
<button
type="button"
class="tool-call-permission-button"
disabled={permissionSubmitting()}
onClick={() => void handlePermissionResponse(permission, "once")}
>
Allow Once
</button>
<button
type="button"
class="tool-call-permission-button"
disabled={permissionSubmitting()}
onClick={() => void handlePermissionResponse(permission, "always")}
>
Always Allow
</button>
<button
type="button"
class="tool-call-permission-button"
disabled={permissionSubmitting()}
onClick={() => void handlePermissionResponse(permission, "reject")}
>
Deny
</button>
</div>
<Show when={active}>
<div class="tool-call-permission-shortcuts">
<kbd class="kbd">Enter</kbd>
<span>Allow once</span>
@@ -1349,10 +1355,10 @@ export default function ToolCall(props: ToolCallProps) {
<kbd class="kbd">D</kbd>
<span>Deny</span>
</div>
</div>
<Show when={permissionError()}>
<div class="tool-call-permission-error">{permissionError()}</div>
</Show>
</div>
<Show when={permissionError()}>
<div class="tool-call-permission-error">{permissionError()}</div>
</Show>
</div>
</div>

View 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>
)
}

View File

@@ -1,25 +1,24 @@
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 { serverEvents } from "../lib/server-events"
import { showToastNotification, ToastHandle } from "../lib/notifications"
import { getLogger } from "../lib/logger"
import { hasInstances, showFolderSelection } from "./ui"
const log = getLogger("actions")
const [availableRelease, setAvailableRelease] = createSignal<LatestReleaseInfo | null>(null)
const [supportInfo, setSupportInfo] = createSignal<SupportMeta | null>(null)
let initialized = false
let visibilityEffectInitialized = false
let activeToast: ToastHandle | null = null
let activeToastVersion: string | null = null
let activeToastKey: string | null = null
function dismissActiveToast() {
if (activeToast) {
activeToast.dismiss()
activeToast = null
activeToastVersion = null
activeToastKey = null
}
}
@@ -30,28 +29,34 @@ function ensureVisibilityEffect() {
visibilityEffectInitialized = true
createEffect(() => {
const release = availableRelease()
const shouldShow = Boolean(release) && (!hasInstances() || showFolderSelection())
const support = supportInfo()
const shouldShow = Boolean(support && support.supported === false) && (!hasInstances() || showFolderSelection())
if (!shouldShow || !release) {
if (!shouldShow || !support || support.supported !== false) {
dismissActiveToast()
return
}
if (!activeToast || activeToastVersion !== release.version) {
const key = `${support.minServerVersion ?? "unknown"}:${support.latestServerVersion ?? "unknown"}`
if (!activeToast || activeToastKey !== key) {
dismissActiveToast()
activeToast = showToastNotification({
title: `CodeNomad ${release.version}`,
message: release.channel === "dev" ? "Dev release build available." : "New stable build on GitHub.",
title: support.message ?? "Upgrade required",
message: support.latestServerVersion
? `Update to CodeNomad ${support.latestServerVersion} to use the latest UI.`
: "Update CodeNomad to use the latest UI.",
variant: "info",
duration: Number.POSITIVE_INFINITY,
position: "bottom-right",
action: {
label: "View release",
href: release.url,
},
action: support.latestServerUrl
? {
label: "Get update",
href: support.latestServerUrl,
}
: undefined,
})
activeToastVersion = release.version
activeToastKey = key
}
})
}
@@ -64,32 +69,17 @@ export function initReleaseNotifications() {
ensureVisibilityEffect()
void refreshFromMeta()
serverEvents.on("app.releaseAvailable", (event) => {
const typedEvent = event as Extract<WorkspaceEventPayload, { type: "app.releaseAvailable" }>
applyRelease(typedEvent.release)
})
}
async function refreshFromMeta() {
try {
const meta = await getServerMeta(true)
if (meta.latestRelease) {
applyRelease(meta.latestRelease)
}
setSupportInfo(meta.support ?? null)
} 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) {
if (!release) {
setAvailableRelease(null)
return
}
setAvailableRelease(release)
}
export function useAvailableRelease() {
return availableRelease
export function useSupportInfo() {
return supportInfo
}

View File

@@ -1,10 +1,26 @@
import fs from "fs"
import { defineConfig } from "vite"
import solid from "vite-plugin-solid"
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({
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: {
postcss: "./postcss.config.js",
},