feat: add searxng web-search provider, openrouter cost balance UI, offline scripts; update source-launch and docs
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
# AGENTS.md — Repository scripts
|
||||
|
||||
Gate scripts invoke pnpm shell-free, normalize repository-relative glob paths to `/` at ingestion, and keep platform adaptation in the gate that needs it instead of a shared platform layer.
|
||||
|
||||
Air-gap dependency caches live under ignored `.offline-store/` and `.offline-cache/`; [`offline/download-deps`](offline/download-deps.ps1) prefetches the official Node distro and lockfile while networked, and [`offline/install-deps`](offline/install-deps.ps1) unpacks that Node, then runs `pnpm install --offline` from the store (POSIX twins: `.sh`). Run download and install on the same OS and CPU. `DSH_NPM_HTTP_PROXY` overlays HTTP proxy env only for Corepack/pnpm and restores the caller session; curl keeps inherited SOCKS or HTTP. These scripts do not fetch LLM weights or a local inference server. The root `dsh` script runs [`run-source-dsh.ts`](run-source-dsh.ts), which respawns Node with HTTP proxy variables from `DSH_HTTP_PROXY` or gitignored [`.dsh-http-proxy.env`](../.gitignore) so `fetch` binds them at process start — not the calling shell, and not `.env`, which rejects bootstrap proxy names.
|
||||
|
||||
98
scripts/apply-source-dsh-http-proxy.spec.ts
Normal file
98
scripts/apply-source-dsh-http-proxy.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
applySourceDshHttpProxy,
|
||||
isSocksProxyUrl,
|
||||
parseSourceDshHttpProxyFile,
|
||||
shouldReexecSourceDshForFetchProxy,
|
||||
SOURCE_DSH_PROXY_REEXEC_MARKER,
|
||||
sourceDshHttpProxyOverlayPresent,
|
||||
} from './apply-source-dsh-http-proxy.ts'
|
||||
|
||||
describe('applySourceDshHttpProxy', () => {
|
||||
it('leaves the inherited environment unchanged when no overlay is set', () => {
|
||||
const env: NodeJS.ProcessEnv = { HTTPS_PROXY: 'socks5h://127.0.0.1:1080' }
|
||||
applySourceDshHttpProxy({ env, fileText: null })
|
||||
expect(env.HTTPS_PROXY).toBe('socks5h://127.0.0.1:1080')
|
||||
expect(env.NODE_USE_ENV_PROXY).toBeUndefined()
|
||||
})
|
||||
|
||||
it('overwrites inherited SOCKS with DSH_HTTP_PROXY for this process only', () => {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
HTTPS_PROXY: 'socks5h://127.0.0.1:1080',
|
||||
ALL_PROXY: 'socks5h://127.0.0.1:1080',
|
||||
}
|
||||
applySourceDshHttpProxy({ env, fileText: null, overlayUrl: 'http://127.0.0.1:3067' })
|
||||
expect(env.HTTPS_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.HTTP_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.ALL_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.https_proxy).toBe('http://127.0.0.1:3067')
|
||||
expect(env.NODE_USE_ENV_PROXY).toBe('1')
|
||||
expect(env.NO_PROXY).toBe('localhost,127.0.0.1,::1')
|
||||
})
|
||||
|
||||
it('applies a checkout overlay file and outranks it with DSH_HTTP_PROXY', () => {
|
||||
const env: NodeJS.ProcessEnv = {}
|
||||
const fileText = [
|
||||
'HTTPS_PROXY=http://127.0.0.1:3067',
|
||||
'NODE_USE_ENV_PROXY=1',
|
||||
'# comment',
|
||||
'',
|
||||
].join('\n')
|
||||
applySourceDshHttpProxy({ env, fileText })
|
||||
expect(env.HTTPS_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.NODE_USE_ENV_PROXY).toBe('1')
|
||||
|
||||
applySourceDshHttpProxy({ env, fileText, overlayUrl: 'http://127.0.0.1:8080' })
|
||||
expect(env.HTTPS_PROXY).toBe('http://127.0.0.1:8080')
|
||||
})
|
||||
|
||||
it('replaces inherited SOCKS on names the overlay file omitted', () => {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
HTTPS_PROXY: 'socks5h://proxy.home:4067',
|
||||
ALL_PROXY: 'socks5h://proxy.home:4067',
|
||||
}
|
||||
applySourceDshHttpProxy({
|
||||
env,
|
||||
fileText: 'HTTPS_PROXY=http://127.0.0.1:3067\nNODE_USE_ENV_PROXY=1\n',
|
||||
})
|
||||
expect(env.HTTPS_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.ALL_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.HTTP_PROXY).toBe('http://127.0.0.1:3067')
|
||||
expect(env.NODE_USE_ENV_PROXY).toBe('1')
|
||||
})
|
||||
|
||||
it('rejects a SOCKS overlay before the CLI boots', () => {
|
||||
expect(() => applySourceDshHttpProxy({
|
||||
env: {},
|
||||
fileText: null,
|
||||
overlayUrl: 'socks5h://127.0.0.1:1080',
|
||||
})).toThrow(/cannot fetch through SOCKS/)
|
||||
expect(() => applySourceDshHttpProxy({
|
||||
env: {},
|
||||
fileText: 'ALL_PROXY=socks5://127.0.0.1:1080\n',
|
||||
})).toThrow(/cannot fetch through SOCKS/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseSourceDshHttpProxyFile', () => {
|
||||
it('parses quoted values and export lines', () => {
|
||||
expect(parseSourceDshHttpProxyFile('export HTTPS_PROXY="http://127.0.0.1:3067"\n')).toEqual({
|
||||
HTTPS_PROXY: 'http://127.0.0.1:3067',
|
||||
})
|
||||
})
|
||||
|
||||
it('detects SOCKS schemes', () => {
|
||||
expect(isSocksProxyUrl('socks5h://127.0.0.1:1080')).toBe(true)
|
||||
expect(isSocksProxyUrl('http://127.0.0.1:3067')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldReexecSourceDshForFetchProxy', () => {
|
||||
it('respawns once when an overlay is present so fetch binds HTTP_PROXY at start', () => {
|
||||
expect(sourceDshHttpProxyOverlayPresent(undefined, null)).toBe(false)
|
||||
expect(shouldReexecSourceDshForFetchProxy({}, false)).toBe(false)
|
||||
expect(shouldReexecSourceDshForFetchProxy({}, true)).toBe(true)
|
||||
expect(shouldReexecSourceDshForFetchProxy({ [SOURCE_DSH_PROXY_REEXEC_MARKER]: '1' }, true)).toBe(false)
|
||||
})
|
||||
})
|
||||
175
scripts/apply-source-dsh-http-proxy.ts
Normal file
175
scripts/apply-source-dsh-http-proxy.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Overlay HTTP proxy variables onto the source `dsh` Node process only.
|
||||
* Bootstrap proxy names cannot come from `.env` files; this overlay is the
|
||||
* checkout-local way to give OpenRouter (and other HTTPS fetches) an HTTP
|
||||
* proxy without changing the calling shell.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
/** Repo-root file the source launcher reads when present; gitignored. */
|
||||
export const SOURCE_DSH_HTTP_PROXY_FILE = '.dsh-http-proxy.env'
|
||||
|
||||
/** Process variables Node, undici, and pi-ai consult for HTTP(S) proxies. */
|
||||
export const SOURCE_DSH_PROXY_VAR_NAMES = [
|
||||
'HTTPS_PROXY',
|
||||
'HTTP_PROXY',
|
||||
'ALL_PROXY',
|
||||
'https_proxy',
|
||||
'http_proxy',
|
||||
'all_proxy',
|
||||
] as const
|
||||
|
||||
const PROXY_VAR_NAME_SET = new Set<string>(SOURCE_DSH_PROXY_VAR_NAMES)
|
||||
|
||||
const LOCAL_NO_PROXY = 'localhost,127.0.0.1,::1'
|
||||
|
||||
/**
|
||||
* Write one HTTP(S) proxy URL onto every name Node and pi-ai read, including
|
||||
* leftover inherited SOCKS values the overlay file did not mention.
|
||||
*/
|
||||
function installHttpProxy(env: NodeJS.ProcessEnv, url: string): void {
|
||||
for (const name of SOURCE_DSH_PROXY_VAR_NAMES) {
|
||||
env[name] = url
|
||||
}
|
||||
env.NODE_USE_ENV_PROXY = '1'
|
||||
if (!env.NO_PROXY && !env.no_proxy) {
|
||||
env.NO_PROXY = LOCAL_NO_PROXY
|
||||
env.no_proxy = LOCAL_NO_PROXY
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First non-SOCKS proxy URL among the names Node consults.
|
||||
* @param env - environment after overlay assignments.
|
||||
*/
|
||||
function firstHttpProxyUrl(env: NodeJS.ProcessEnv): string | undefined {
|
||||
for (const name of SOURCE_DSH_PROXY_VAR_NAMES) {
|
||||
const value = env[name]?.trim()
|
||||
if (value !== undefined && value !== '' && !isSocksProxyUrl(value)) return value
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Set on the child after this wrapper respawns so `fetch` sees the HTTP overlay
|
||||
* at process start. Node `--env-file` does not replace an inherited SOCKS
|
||||
* `HTTPS_PROXY`, and `NODE_USE_ENV_PROXY` assigned after start is ignored.
|
||||
*/
|
||||
export const SOURCE_DSH_PROXY_REEXEC_MARKER = 'DSH_SOURCE_HTTP_PROXY_APPLIED'
|
||||
|
||||
/** @returns whether `value` is a SOCKS URL Node cannot use for fetch/pnpm. */
|
||||
export function isSocksProxyUrl(value: string): boolean {
|
||||
return /^socks5h?:\/\//i.test(value.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a dotenv-like overlay file into assignments.
|
||||
* @param text - file contents.
|
||||
* @returns name → value; empty lines and `#` comments are skipped.
|
||||
*/
|
||||
export function parseSourceDshHttpProxyFile(text: string): Record<string, string> {
|
||||
const out: Record<string, string> = {}
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = raw.trim()
|
||||
if (line === '' || line.startsWith('#')) continue
|
||||
const body = line.startsWith('export ') ? line.slice('export '.length).trim() : line
|
||||
const eq = body.indexOf('=')
|
||||
if (eq <= 0) continue
|
||||
const name = body.slice(0, eq).trim()
|
||||
let value = body.slice(eq + 1).trim()
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
if (name !== '') out[name] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Inputs for {@link applySourceDshHttpProxy}. Tests pass a fake env and file text.
|
||||
*/
|
||||
export interface ApplySourceDshHttpProxyInput {
|
||||
/** Mutable environment of this Node process (normally `process.env`). */
|
||||
env: NodeJS.ProcessEnv
|
||||
/** Contents of `.dsh-http-proxy.env`; `null` when the file is absent. */
|
||||
fileText: string | null
|
||||
/** Optional explicit HTTP overlay URL (`DSH_HTTP_PROXY`), which outranks the file. */
|
||||
overlayUrl?: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an HTTP proxy overlay onto `env` for this process only.
|
||||
* @param input - env, optional file, optional `DSH_HTTP_PROXY`.
|
||||
* @throws when the winning overlay is a SOCKS URL.
|
||||
*/
|
||||
export function applySourceDshHttpProxy(input: ApplySourceDshHttpProxyInput): void {
|
||||
const overlayUrl = input.overlayUrl?.trim()
|
||||
if (overlayUrl) {
|
||||
if (isSocksProxyUrl(overlayUrl)) {
|
||||
throw new Error(
|
||||
`DSH_HTTP_PROXY=${overlayUrl}. Node cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067).`,
|
||||
)
|
||||
}
|
||||
installHttpProxy(input.env, overlayUrl)
|
||||
return
|
||||
}
|
||||
if (input.fileText === null) return
|
||||
const assignments = parseSourceDshHttpProxyFile(input.fileText)
|
||||
for (const [name, value] of Object.entries(assignments)) {
|
||||
if (PROXY_VAR_NAME_SET.has(name) && isSocksProxyUrl(value)) {
|
||||
throw new Error(
|
||||
`${name}=${value}. Node cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067).`,
|
||||
)
|
||||
}
|
||||
input.env[name] = value
|
||||
}
|
||||
const httpUrl = firstHttpProxyUrl(input.env)
|
||||
if (httpUrl !== undefined) installHttpProxy(input.env, httpUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP overlay URL now on `env`, if any.
|
||||
* @param env - environment after {@link applySourceDshHttpProxy}.
|
||||
*/
|
||||
export function sourceDshAppliedHttpProxyUrl(env: NodeJS.ProcessEnv): string | undefined {
|
||||
return firstHttpProxyUrl(env)
|
||||
}
|
||||
|
||||
/** Absolute path of the gitignored overlay file beside this repository root. */
|
||||
export function sourceDshHttpProxyFilePath(): string {
|
||||
return fileURLToPath(new URL(`../${SOURCE_DSH_HTTP_PROXY_FILE}`, import.meta.url))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the overlay file when it exists.
|
||||
* @param path - absolute path; defaults to the repo-root gitignored file.
|
||||
* @returns file text, or `null` when absent.
|
||||
*/
|
||||
export function readSourceDshHttpProxyFile(path = sourceDshHttpProxyFilePath()): string | null {
|
||||
if (!existsSync(path)) return null
|
||||
return readFileSync(path, 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an overlay source is present (file or `DSH_HTTP_PROXY`).
|
||||
* @param overlayUrl - `DSH_HTTP_PROXY` value.
|
||||
* @param fileText - overlay file contents, or `null` when absent.
|
||||
*/
|
||||
export function sourceDshHttpProxyOverlayPresent(overlayUrl: string | undefined, fileText: string | null): boolean {
|
||||
return Boolean(overlayUrl?.trim()) || fileText !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this process must respawn so `fetch` binds the overlay at start.
|
||||
* @param env - the environment after {@link applySourceDshHttpProxy}.
|
||||
* @param overlayPresent - {@link sourceDshHttpProxyOverlayPresent}.
|
||||
*/
|
||||
export function shouldReexecSourceDshForFetchProxy(env: NodeJS.ProcessEnv, overlayPresent: boolean): boolean {
|
||||
return overlayPresent && env[SOURCE_DSH_PROXY_REEXEC_MARKER] !== '1'
|
||||
}
|
||||
@@ -560,6 +560,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
TypertDisposer: 'Typert lifecycle contract is owned by packages/typert/protocol/README.md',
|
||||
InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
|
||||
LocaleDict: 'service-local dictionary fields are owned by packages/client/i18n/src/index.ts',
|
||||
OpenRouterBalance: 'service-local balance payload is owned by packages/llm/openrouter-usage/src/types.ts',
|
||||
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
|
||||
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
|
||||
WebUpgradeRoute:
|
||||
|
||||
@@ -483,7 +483,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'web',
|
||||
title: 'Web access provider registry',
|
||||
mode: 'seam',
|
||||
implementations: ['web-search-exa', 'web-search-perplexity', 'web-search-deepseek', 'web-fetch-http'],
|
||||
implementations: ['web-search-exa', 'web-search-searxng', 'web-search-perplexity', 'web-search-deepseek', 'web-fetch-http'],
|
||||
consumers: ['tool-web'],
|
||||
note: 'Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names.',
|
||||
},
|
||||
@@ -547,6 +547,14 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['connection'],
|
||||
note: 'The transport-agnostic host gateway face: it dispatches browser API calls, and each open host stream subscribes to the events it forwards rather than being pushed to through a broadcast verb.',
|
||||
},
|
||||
{
|
||||
key: 'openRouterUsage',
|
||||
pkg: 'openrouter-usage',
|
||||
title: 'OpenRouter usage and balance gateway',
|
||||
mode: 'core',
|
||||
consumers: ['ui-openrouter-usage'],
|
||||
note: 'The host gateway folds token-usage projections and a live balance snapshot; the web UI client reaches the same service over the wire through its remote namespace.',
|
||||
},
|
||||
{
|
||||
key: 'dynamicCordisRunner',
|
||||
pkg: 'cordis-host-runner',
|
||||
|
||||
206
scripts/offline/download-deps.ps1
Normal file
206
scripts/offline/download-deps.ps1
Normal file
@@ -0,0 +1,206 @@
|
||||
# Prefetch official Node, Corepack pnpm, and lockfile packages into in-repo caches.
|
||||
# Run on the same OS/CPU as the later install, while nodejs.org and the npm registry
|
||||
# are reachable. Node/pnpm cannot use socks5h:// proxies; pass an HTTP proxy via
|
||||
# DSH_NPM_HTTP_PROXY (example: http://127.0.0.1:3067). That overlay is applied only
|
||||
# around Corepack/pnpm and is restored before the script returns, so a caller SOCKS
|
||||
# proxy stays in the current PowerShell session. curl keeps the inherited proxy
|
||||
# (SOCKS included). Optional DSH_NODE_VERSION (no leading v) pins the Node distro;
|
||||
# otherwise the running Node is used, or the newest v24.x from nodejs.org/dist/index.json.
|
||||
#
|
||||
# Usage (repo root): powershell -File scripts/offline/download-deps.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
Set-Location $Root
|
||||
|
||||
$ProxyVarNames = @('HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'https_proxy', 'http_proxy', 'all_proxy')
|
||||
|
||||
function Get-ProcessVar([string]$Name) {
|
||||
return [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||
}
|
||||
|
||||
function Get-InheritedProxy {
|
||||
foreach ($name in $ProxyVarNames) {
|
||||
$value = Get-ProcessVar $name
|
||||
if ($value) { return $value }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-NodeHttpProxy {
|
||||
$override = Get-ProcessVar 'DSH_NPM_HTTP_PROXY'
|
||||
if ($override) {
|
||||
if ($override -match '^socks5') {
|
||||
throw "DSH_NPM_HTTP_PROXY=$override. Node and pnpm cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067)."
|
||||
}
|
||||
return $override
|
||||
}
|
||||
$socks = $null
|
||||
foreach ($name in $ProxyVarNames) {
|
||||
$value = Get-ProcessVar $name
|
||||
if (-not $value) { continue }
|
||||
if ($value -match '^socks5') {
|
||||
$socks = "$name=$value"
|
||||
continue
|
||||
}
|
||||
return $value
|
||||
}
|
||||
if ($socks) {
|
||||
throw "$socks. Node and pnpm cannot fetch through SOCKS. Set DSH_NPM_HTTP_PROXY to an HTTP proxy (for example http://127.0.0.1:3067)."
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-WithNodeHttpProxy([scriptblock]$Action) {
|
||||
$http = Resolve-NodeHttpProxy
|
||||
if (-not $http) {
|
||||
& $Action
|
||||
return
|
||||
}
|
||||
$saved = @{}
|
||||
foreach ($name in $ProxyVarNames) {
|
||||
$saved[$name] = Get-ProcessVar $name
|
||||
}
|
||||
try {
|
||||
foreach ($name in $ProxyVarNames) {
|
||||
[Environment]::SetEnvironmentVariable($name, $http, 'Process')
|
||||
}
|
||||
& $Action
|
||||
} finally {
|
||||
foreach ($name in $ProxyVarNames) {
|
||||
[Environment]::SetEnvironmentVariable($name, $saved[$name], 'Process')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CurlProxyArgs {
|
||||
$proxy = Get-InheritedProxy
|
||||
if (-not $proxy) { $proxy = Get-ProcessVar 'DSH_NPM_HTTP_PROXY' }
|
||||
if ($proxy) { @('--proxy', $proxy) } else { @() }
|
||||
}
|
||||
|
||||
function Save-Url([string]$Url, [string]$Destination) {
|
||||
$dir = Split-Path -Parent $Destination
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
$curl = Get-Command curl.exe -ErrorAction SilentlyContinue
|
||||
if ($null -eq $curl) {
|
||||
throw 'curl.exe is required to download Node (bundled with Windows 10+).'
|
||||
}
|
||||
& $curl.Source -fsSL --retry 3 @(Get-CurlProxyArgs) -o $Destination $Url
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Download failed ($LASTEXITCODE): $Url"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NodePlatform {
|
||||
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||
if ($arch -eq 'ARM64') { return 'win-arm64' }
|
||||
if ($arch -eq 'AMD64') { return 'win-x64' }
|
||||
throw "Unsupported Windows architecture: $arch"
|
||||
}
|
||||
|
||||
function Get-NodeArchiveName([string]$Version, [string]$Platform) {
|
||||
return "node-v$Version-$Platform.zip"
|
||||
}
|
||||
|
||||
function Resolve-NodeVersion([string]$Platform) {
|
||||
if ($env:DSH_NODE_VERSION) {
|
||||
return $env:DSH_NODE_VERSION.TrimStart('v')
|
||||
}
|
||||
$node = Get-Command node -ErrorAction SilentlyContinue
|
||||
if ($null -ne $node) {
|
||||
$running = (& $node.Source --version).Trim().TrimStart('v')
|
||||
$parts = $running.Split('.')
|
||||
$major = [int]$parts[0]
|
||||
$minor = [int]$parts[1]
|
||||
if (-not (($major -eq 22 -and $minor -ge 19) -or ($major -ge 24))) {
|
||||
throw "Running Node v$running is outside engines.node (^22.19.0 || >=24.0.0). Set DSH_NODE_VERSION."
|
||||
}
|
||||
return $running
|
||||
}
|
||||
$indexPath = Join-Path $env:TEMP 'dsh-node-index.json'
|
||||
$distBase = if ($env:DSH_NODE_DIST_BASE) { $env:DSH_NODE_DIST_BASE.TrimEnd('/') } else { 'https://nodejs.org/dist' }
|
||||
Save-Url "$distBase/index.json" $indexPath
|
||||
$releases = Get-Content $indexPath -Raw | ConvertFrom-Json
|
||||
$wantedFile = if ($Platform -eq 'win-arm64') { 'win-arm64-zip' } else { 'win-x64-zip' }
|
||||
foreach ($release in $releases) {
|
||||
$ver = [string]$release.version
|
||||
if ($ver -notmatch '^v24\.') { continue }
|
||||
if (@($release.files) -notcontains $wantedFile) { continue }
|
||||
return $ver.TrimStart('v')
|
||||
}
|
||||
throw "No Node v24.x zip published for $Platform at $distBase."
|
||||
}
|
||||
|
||||
function Assert-NodeChecksum([string]$SumsPath, [string]$ArchivePath, [string]$ArchiveName) {
|
||||
$expected = $null
|
||||
foreach ($line in Get-Content $SumsPath) {
|
||||
if ($line -match ('^[0-9a-fA-F]{64} ' + [regex]::Escape($ArchiveName) + '$')) {
|
||||
$expected = $line.Substring(0, 64)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $expected) {
|
||||
throw "SHASUMS256.txt has no entry for $ArchiveName"
|
||||
}
|
||||
$actual = (Get-FileHash -Algorithm SHA256 -Path $ArchivePath).Hash
|
||||
if ($actual.ToLowerInvariant() -ne $expected.ToLowerInvariant()) {
|
||||
throw "SHA256 mismatch for $ArchiveName"
|
||||
}
|
||||
}
|
||||
|
||||
$manifest = Get-Content (Join-Path $Root 'package.json') -Raw | ConvertFrom-Json
|
||||
$packageManager = [string]$manifest.packageManager
|
||||
if ($packageManager -notmatch '^pnpm@') {
|
||||
throw "package.json packageManager must be pnpm@<version>, got $packageManager"
|
||||
}
|
||||
|
||||
$distBase = if ($env:DSH_NODE_DIST_BASE) { $env:DSH_NODE_DIST_BASE.TrimEnd('/') } else { 'https://nodejs.org/dist' }
|
||||
$platform = Get-NodePlatform
|
||||
$nodeVersion = Resolve-NodeVersion $platform
|
||||
$archiveName = Get-NodeArchiveName $nodeVersion $platform
|
||||
$nodeCache = Join-Path $Root '.offline-cache\node'
|
||||
$archivePath = Join-Path $nodeCache $archiveName
|
||||
$sumsPath = Join-Path $nodeCache "SHASUMS256-v$nodeVersion.txt"
|
||||
$runtimeDir = Join-Path $nodeCache 'runtime'
|
||||
$nodeHome = Join-Path $runtimeDir "node-v$nodeVersion-$platform"
|
||||
|
||||
Write-Host "Prefetch Node v$nodeVersion ($platform)"
|
||||
Save-Url "$distBase/v$nodeVersion/SHASUMS256.txt" $sumsPath
|
||||
Save-Url "$distBase/v$nodeVersion/$archiveName" $archivePath
|
||||
Assert-NodeChecksum $sumsPath $archivePath $archiveName
|
||||
if (-not (Test-Path (Join-Path $nodeHome 'node.exe'))) {
|
||||
if (Test-Path $runtimeDir) { Remove-Item -Recurse -Force $runtimeDir }
|
||||
New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null
|
||||
Expand-Archive -LiteralPath $archivePath -DestinationPath $runtimeDir
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $nodeHome 'node.exe'))) {
|
||||
throw "Unpacked Node is missing: $nodeHome\node.exe"
|
||||
}
|
||||
$json = @{
|
||||
version = $nodeVersion
|
||||
archive = $archiveName
|
||||
platform = $platform
|
||||
} | ConvertTo-Json
|
||||
[System.IO.File]::WriteAllText((Join-Path $nodeCache 'runtime.json'), $json)
|
||||
|
||||
$storeDir = Join-Path $Root '.offline-store'
|
||||
$corepackHome = Join-Path $Root '.offline-cache\corepack'
|
||||
New-Item -ItemType Directory -Force -Path $storeDir, $corepackHome | Out-Null
|
||||
$env:COREPACK_HOME = $corepackHome
|
||||
$env:PATH = "$nodeHome;$env:PATH"
|
||||
|
||||
Write-Host "Node $((& (Join-Path $nodeHome 'node.exe') --version)); prefetch $packageManager into $storeDir"
|
||||
Invoke-WithNodeHttpProxy {
|
||||
& (Join-Path $nodeHome 'corepack.cmd') prepare $packageManager --activate
|
||||
if ($LASTEXITCODE -ne 0) { throw "corepack prepare failed ($LASTEXITCODE)" }
|
||||
& (Join-Path $nodeHome 'corepack.cmd') pnpm fetch --frozen-lockfile --store-dir $storeDir
|
||||
if ($LASTEXITCODE -ne 0) { throw "pnpm fetch failed ($LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
Write-Host @"
|
||||
Prefetch complete (Node v$nodeVersion zip + pnpm store).
|
||||
Copy this checkout (including .offline-store and .offline-cache) to the offline machine.
|
||||
Then run: powershell -File scripts/offline/install-deps.ps1
|
||||
"@
|
||||
293
scripts/offline/download-deps.sh
Normal file
293
scripts/offline/download-deps.sh
Normal file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prefetch official Node, Corepack pnpm, and lockfile packages into in-repo caches.
|
||||
# Run on the same OS/CPU as the later install, while nodejs.org and the npm registry
|
||||
# are reachable. Node/pnpm cannot use socks5h:// proxies; pass an HTTP proxy via
|
||||
# DSH_NPM_HTTP_PROXY (example: http://127.0.0.1:3067). That overlay is applied only
|
||||
# around Corepack/pnpm and is restored before the script returns, so a caller SOCKS
|
||||
# proxy stays in the current shell. curl keeps the inherited proxy (SOCKS included).
|
||||
# Optional DSH_NODE_VERSION (no leading v) pins the Node distro; otherwise the
|
||||
# running Node is used, or the newest v24.x from nodejs.org/dist/index.json.
|
||||
#
|
||||
# Usage (repo root): bash scripts/offline/download-deps.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
process_var() {
|
||||
local name="$1"
|
||||
if [[ -n "${!name+x}" ]]; then
|
||||
printf '%s' "${!name}"
|
||||
fi
|
||||
}
|
||||
|
||||
inherited_proxy() {
|
||||
local name value
|
||||
for name in HTTPS_PROXY HTTP_PROXY ALL_PROXY https_proxy http_proxy all_proxy; do
|
||||
value="$(process_var "$name")"
|
||||
if [[ -n "$value" ]]; then
|
||||
printf '%s' "$value"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
resolve_node_http_proxy() {
|
||||
local override socks name value
|
||||
override="$(process_var DSH_NPM_HTTP_PROXY)"
|
||||
if [[ -n "$override" ]]; then
|
||||
if [[ "$override" == socks5* ]]; then
|
||||
echo "DSH_NPM_HTTP_PROXY=$override. Node and pnpm cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067)." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$override"
|
||||
return
|
||||
fi
|
||||
socks=
|
||||
for name in HTTPS_PROXY HTTP_PROXY ALL_PROXY https_proxy http_proxy all_proxy; do
|
||||
value="$(process_var "$name")"
|
||||
if [[ -z "$value" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "$value" == socks5* ]]; then
|
||||
socks="$name=$value"
|
||||
continue
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
return
|
||||
done
|
||||
if [[ -n "$socks" ]]; then
|
||||
echo "$socks. Node and pnpm cannot fetch through SOCKS. Set DSH_NPM_HTTP_PROXY to an HTTP proxy (for example http://127.0.0.1:3067)." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_with_node_http_proxy() {
|
||||
local http status
|
||||
http="$(resolve_node_http_proxy)"
|
||||
if [[ -z "$http" ]]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
local HTTPS_PROXY_BAK="${HTTPS_PROXY-}" HTTP_PROXY_BAK="${HTTP_PROXY-}" ALL_PROXY_BAK="${ALL_PROXY-}"
|
||||
local https_proxy_bak="${https_proxy-}" http_proxy_bak="${http_proxy-}" all_proxy_bak="${all_proxy-}"
|
||||
local had_HTTPS_PROXY=0 had_HTTP_PROXY=0 had_ALL_PROXY=0
|
||||
local had_https_proxy=0 had_http_proxy=0 had_all_proxy=0
|
||||
if [[ -n "${HTTPS_PROXY+x}" ]]; then had_HTTPS_PROXY=1; fi
|
||||
if [[ -n "${HTTP_PROXY+x}" ]]; then had_HTTP_PROXY=1; fi
|
||||
if [[ -n "${ALL_PROXY+x}" ]]; then had_ALL_PROXY=1; fi
|
||||
if [[ -n "${https_proxy+x}" ]]; then had_https_proxy=1; fi
|
||||
if [[ -n "${http_proxy+x}" ]]; then had_http_proxy=1; fi
|
||||
if [[ -n "${all_proxy+x}" ]]; then had_all_proxy=1; fi
|
||||
export HTTPS_PROXY="$http" HTTP_PROXY="$http" ALL_PROXY="$http"
|
||||
export https_proxy="$http" http_proxy="$http" all_proxy="$http"
|
||||
set +e
|
||||
"$@"
|
||||
status=$?
|
||||
set -e
|
||||
if (( had_HTTPS_PROXY )); then export HTTPS_PROXY="$HTTPS_PROXY_BAK"; else unset HTTPS_PROXY; fi
|
||||
if (( had_HTTP_PROXY )); then export HTTP_PROXY="$HTTP_PROXY_BAK"; else unset HTTP_PROXY; fi
|
||||
if (( had_ALL_PROXY )); then export ALL_PROXY="$ALL_PROXY_BAK"; else unset ALL_PROXY; fi
|
||||
if (( had_https_proxy )); then export https_proxy="$https_proxy_bak"; else unset https_proxy; fi
|
||||
if (( had_http_proxy )); then export http_proxy="$http_proxy_bak"; else unset http_proxy; fi
|
||||
if (( had_all_proxy )); then export all_proxy="$all_proxy_bak"; else unset all_proxy; fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
curl_proxy_args=()
|
||||
curl_proxy="$(inherited_proxy)"
|
||||
if [[ -z "$curl_proxy" ]]; then
|
||||
curl_proxy="$(process_var DSH_NPM_HTTP_PROXY)"
|
||||
fi
|
||||
if [[ -n "$curl_proxy" ]]; then
|
||||
curl_proxy_args=(--proxy "$curl_proxy")
|
||||
fi
|
||||
|
||||
save_url() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
curl -fsSL --retry 3 "${curl_proxy_args[@]}" -o "$dest" "$url"
|
||||
}
|
||||
|
||||
node_platform() {
|
||||
local sys arch
|
||||
sys="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
case "$sys:$arch" in
|
||||
Linux:x86_64) echo linux-x64 ;;
|
||||
Linux:aarch64) echo linux-arm64 ;;
|
||||
Darwin:x86_64) echo darwin-x64 ;;
|
||||
Darwin:arm64) echo darwin-arm64 ;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
echo "Use scripts/offline/download-deps.ps1 on Windows." >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported OS/CPU: $sys $arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
archive_name() {
|
||||
local version="$1"
|
||||
local platform="$2"
|
||||
case "$platform" in
|
||||
linux-*) echo "node-v${version}-${platform}.tar.xz" ;;
|
||||
darwin-*) echo "node-v${version}-${platform}.tar.gz" ;;
|
||||
*) echo "node-v${version}-${platform}.zip" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
file_tag() {
|
||||
case "$1" in
|
||||
linux-x64) echo linux-x64 ;;
|
||||
linux-arm64) echo linux-arm64 ;;
|
||||
darwin-x64) echo osx-x64-tar ;;
|
||||
darwin-arm64) echo osx-arm64-tar ;;
|
||||
win-x64) echo win-x64-zip ;;
|
||||
win-arm64) echo win-arm64-zip ;;
|
||||
esac
|
||||
}
|
||||
|
||||
engines_ok() {
|
||||
local version="$1"
|
||||
local major minor
|
||||
major="${version%%.*}"
|
||||
minor="${version#*.}"
|
||||
minor="${minor%%.*}"
|
||||
if (( major == 22 && minor >= 19 )); then return 0; fi
|
||||
if (( major >= 24 )); then return 0; fi
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_node_version() {
|
||||
local platform="$1"
|
||||
local dist_base="$2"
|
||||
if [[ -n "${DSH_NODE_VERSION:-}" ]]; then
|
||||
echo "${DSH_NODE_VERSION#v}"
|
||||
return
|
||||
fi
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
local running
|
||||
running="$(node --version)"
|
||||
running="${running#v}"
|
||||
if ! engines_ok "$running"; then
|
||||
echo "Running Node v$running is outside engines.node (^22.19.0 || >=24.0.0). Set DSH_NODE_VERSION." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$running"
|
||||
return
|
||||
fi
|
||||
local index_path tag version
|
||||
index_path="$(mktemp)"
|
||||
save_url "$dist_base/index.json" "$index_path"
|
||||
tag="$(file_tag "$platform")"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
version="$(python3 -c 'import json,sys
|
||||
releases=json.load(open(sys.argv[1]))
|
||||
tag=sys.argv[2]
|
||||
for r in releases:
|
||||
if r["version"].startswith("v24.") and tag in r["files"]:
|
||||
print(r["version"][1:])
|
||||
break
|
||||
' "$index_path" "$tag")"
|
||||
else
|
||||
echo "python3 or node is required to pick a Node v24.x from index.json, or set DSH_NODE_VERSION." >&2
|
||||
rm -f "$index_path"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$index_path"
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "No Node v24.x archive published for $platform at $dist_base." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
assert_checksum() {
|
||||
local sums_path="$1"
|
||||
local archive_path="$2"
|
||||
local archive="$3"
|
||||
local expected actual
|
||||
expected="$(awk -v name="$archive" '$2 == name { print $1; exit }' "$sums_path")"
|
||||
if [[ -z "$expected" ]]; then
|
||||
echo "SHASUMS256.txt has no entry for $archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual="$(sha256sum "$archive_path" | awk '{ print $1 }')"
|
||||
else
|
||||
actual="$(shasum -a 256 "$archive_path" | awk '{ print $1 }')"
|
||||
fi
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "SHA256 mismatch for $archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
extract_node() {
|
||||
local archive_path="$1"
|
||||
local runtime_dir="$2"
|
||||
mkdir -p "$runtime_dir"
|
||||
case "$archive_path" in
|
||||
*.tar.xz) tar -xJf "$archive_path" -C "$runtime_dir" ;;
|
||||
*.tar.gz) tar -xzf "$archive_path" -C "$runtime_dir" ;;
|
||||
*.zip) unzip -q "$archive_path" -d "$runtime_dir" ;;
|
||||
*)
|
||||
echo "Unknown Node archive: $archive_path" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
PACKAGE_MANAGER="$(sed -n 's/.*"packageManager": "\(pnpm@[0-9.]*\)".*/\1/p' package.json | head -n 1)"
|
||||
case "$PACKAGE_MANAGER" in
|
||||
pnpm@*) ;;
|
||||
*)
|
||||
echo "package.json packageManager must be pnpm@<version>, got ${PACKAGE_MANAGER:-<empty>}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
DIST_BASE="${DSH_NODE_DIST_BASE:-https://nodejs.org/dist}"
|
||||
DIST_BASE="${DIST_BASE%/}"
|
||||
PLATFORM="$(node_platform)"
|
||||
NODE_VERSION="$(resolve_node_version "$PLATFORM" "$DIST_BASE")"
|
||||
ARCHIVE_NAME="$(archive_name "$NODE_VERSION" "$PLATFORM")"
|
||||
NODE_CACHE="$ROOT/.offline-cache/node"
|
||||
ARCHIVE_PATH="$NODE_CACHE/$ARCHIVE_NAME"
|
||||
SUMS_PATH="$NODE_CACHE/SHASUMS256-v${NODE_VERSION}.txt"
|
||||
RUNTIME_DIR="$NODE_CACHE/runtime"
|
||||
NODE_PREFIX="$RUNTIME_DIR/node-v${NODE_VERSION}-${PLATFORM}"
|
||||
|
||||
echo "Prefetch Node v$NODE_VERSION ($PLATFORM)"
|
||||
save_url "$DIST_BASE/v$NODE_VERSION/SHASUMS256.txt" "$SUMS_PATH"
|
||||
save_url "$DIST_BASE/v$NODE_VERSION/$ARCHIVE_NAME" "$ARCHIVE_PATH"
|
||||
assert_checksum "$SUMS_PATH" "$ARCHIVE_PATH" "$ARCHIVE_NAME"
|
||||
if [[ ! -x "$NODE_PREFIX/bin/node" ]]; then
|
||||
rm -rf "$RUNTIME_DIR"
|
||||
extract_node "$ARCHIVE_PATH" "$RUNTIME_DIR"
|
||||
fi
|
||||
if [[ ! -x "$NODE_PREFIX/bin/node" ]]; then
|
||||
echo "Unpacked Node is missing: $NODE_PREFIX/bin/node" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "{\"version\":\"$NODE_VERSION\",\"archive\":\"$ARCHIVE_NAME\",\"platform\":\"$PLATFORM\"}" > "$NODE_CACHE/runtime.json"
|
||||
|
||||
STORE_DIR="$ROOT/.offline-store"
|
||||
COREPACK_HOME="$ROOT/.offline-cache/corepack"
|
||||
mkdir -p "$STORE_DIR" "$COREPACK_HOME"
|
||||
export COREPACK_HOME
|
||||
export PATH="$NODE_PREFIX/bin:$PATH"
|
||||
|
||||
echo "Node $("$NODE_PREFIX/bin/node" --version); prefetch $PACKAGE_MANAGER into $STORE_DIR"
|
||||
run_with_node_http_proxy corepack prepare "$PACKAGE_MANAGER" --activate
|
||||
run_with_node_http_proxy corepack pnpm fetch --frozen-lockfile --store-dir "$STORE_DIR"
|
||||
|
||||
cat <<EOF
|
||||
Prefetch complete (Node v$NODE_VERSION archive + pnpm store).
|
||||
Copy this checkout (including .offline-store and .offline-cache) to the offline machine.
|
||||
Then run: bash scripts/offline/install-deps.sh
|
||||
EOF
|
||||
69
scripts/offline/install-deps.ps1
Normal file
69
scripts/offline/install-deps.ps1
Normal file
@@ -0,0 +1,69 @@
|
||||
# Install from caches filled by download-deps.ps1. Unpacks the official Node zip
|
||||
# into .offline-cache/node/runtime and prepends it to PATH so the offline machine
|
||||
# does not need a system Node. Fails if any lockfile package is missing from
|
||||
# .offline-store (no registry access).
|
||||
#
|
||||
# Usage (repo root): powershell -File scripts/offline/install-deps.ps1 [-Build]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Build
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
Set-Location $Root
|
||||
|
||||
function Import-OfflineNode {
|
||||
$nodeCache = Join-Path $Root '.offline-cache\node'
|
||||
$metaPath = Join-Path $nodeCache 'runtime.json'
|
||||
if (-not (Test-Path $metaPath)) {
|
||||
throw "Missing $metaPath. Run scripts/offline/download-deps.ps1 on a networked machine first, then copy the checkout."
|
||||
}
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
$archivePath = Join-Path $nodeCache ([string]$meta.archive)
|
||||
$runtimeDir = Join-Path $nodeCache 'runtime'
|
||||
$nodeHome = Join-Path $runtimeDir ("node-v{0}-{1}" -f $meta.version, $meta.platform)
|
||||
if (-not (Test-Path (Join-Path $nodeHome 'node.exe'))) {
|
||||
if (-not (Test-Path $archivePath)) {
|
||||
throw "Missing Node archive $archivePath. Re-run download-deps.ps1."
|
||||
}
|
||||
if (Test-Path $runtimeDir) { Remove-Item -Recurse -Force $runtimeDir }
|
||||
New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null
|
||||
Expand-Archive -LiteralPath $archivePath -DestinationPath $runtimeDir
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $nodeHome 'node.exe'))) {
|
||||
throw "Unpacked Node is missing: $nodeHome\node.exe"
|
||||
}
|
||||
$env:PATH = "$nodeHome;$env:PATH"
|
||||
return $nodeHome
|
||||
}
|
||||
|
||||
$nodeHome = Import-OfflineNode
|
||||
$storeDir = Join-Path $Root '.offline-store'
|
||||
$corepackHome = Join-Path $Root '.offline-cache\corepack'
|
||||
if (-not (Test-Path $storeDir)) {
|
||||
throw "Missing $storeDir. Run scripts/offline/download-deps.ps1 on a networked machine first, then copy the checkout."
|
||||
}
|
||||
if (-not (Test-Path $corepackHome)) {
|
||||
throw "Missing $corepackHome. Re-run download-deps.ps1 so Corepack pnpm is cached in-repo."
|
||||
}
|
||||
|
||||
$env:COREPACK_HOME = $corepackHome
|
||||
$env:COREPACK_ENABLE_NETWORK = '0'
|
||||
|
||||
$manifest = Get-Content (Join-Path $Root 'package.json') -Raw | ConvertFrom-Json
|
||||
$packageManager = [string]$manifest.packageManager
|
||||
$corepack = Join-Path $nodeHome 'corepack.cmd'
|
||||
|
||||
Write-Host "Offline install $packageManager from $storeDir (Node $(& (Join-Path $nodeHome 'node.exe') --version))"
|
||||
& $corepack pnpm install --offline --frozen-lockfile --store-dir $storeDir
|
||||
|
||||
if ($Build) {
|
||||
Write-Host 'Building host/client libs, then the web frontend'
|
||||
npm run build:lib
|
||||
& $corepack pnpm --dir apps/web run build
|
||||
Write-Host "Build complete. Start the UI with: `"$nodeHome\corepack.cmd`" pnpm dsh web"
|
||||
} else {
|
||||
Write-Host 'Install complete. For dsh web also run: powershell -File scripts/offline/install-deps.ps1 -Build'
|
||||
}
|
||||
87
scripts/offline/install-deps.sh
Normal file
87
scripts/offline/install-deps.sh
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install from caches filled by download-deps.sh. Unpacks the official Node
|
||||
# archive into .offline-cache/node/runtime and prepends it to PATH so the
|
||||
# offline machine does not need a system Node. Fails if any lockfile package
|
||||
# is missing from .offline-store (no registry access).
|
||||
#
|
||||
# Usage (repo root): bash scripts/offline/install-deps.sh [--build]
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
BUILD=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--build) BUILD=1 ;;
|
||||
*)
|
||||
echo "Unknown argument: $arg (expected --build)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
NODE_CACHE="$ROOT/.offline-cache/node"
|
||||
META_PATH="$NODE_CACHE/runtime.json"
|
||||
if [[ ! -f "$META_PATH" ]]; then
|
||||
echo "Missing $META_PATH. Run scripts/offline/download-deps.sh on a networked machine first, then copy the checkout." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_VERSION="$(sed -n 's/.*"version":"\([^"]*\)".*/\1/p' "$META_PATH" | head -n 1)"
|
||||
ARCHIVE_NAME="$(sed -n 's/.*"archive":"\([^"]*\)".*/\1/p' "$META_PATH" | head -n 1)"
|
||||
PLATFORM="$(sed -n 's/.*"platform":"\([^"]*\)".*/\1/p' "$META_PATH" | head -n 1)"
|
||||
ARCHIVE_PATH="$NODE_CACHE/$ARCHIVE_NAME"
|
||||
RUNTIME_DIR="$NODE_CACHE/runtime"
|
||||
NODE_PREFIX="$RUNTIME_DIR/node-v${NODE_VERSION}-${PLATFORM}"
|
||||
|
||||
if [[ ! -x "$NODE_PREFIX/bin/node" ]]; then
|
||||
if [[ ! -f "$ARCHIVE_PATH" ]]; then
|
||||
echo "Missing Node archive $ARCHIVE_PATH. Re-run download-deps.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$RUNTIME_DIR"
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
case "$ARCHIVE_PATH" in
|
||||
*.tar.xz) tar -xJf "$ARCHIVE_PATH" -C "$RUNTIME_DIR" ;;
|
||||
*.tar.gz) tar -xzf "$ARCHIVE_PATH" -C "$RUNTIME_DIR" ;;
|
||||
*.zip) unzip -q "$ARCHIVE_PATH" -d "$RUNTIME_DIR" ;;
|
||||
*)
|
||||
echo "Unknown Node archive: $ARCHIVE_PATH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ ! -x "$NODE_PREFIX/bin/node" ]]; then
|
||||
echo "Unpacked Node is missing: $NODE_PREFIX/bin/node" >&2
|
||||
exit 1
|
||||
fi
|
||||
export PATH="$NODE_PREFIX/bin:$PATH"
|
||||
|
||||
STORE_DIR="$ROOT/.offline-store"
|
||||
COREPACK_HOME="$ROOT/.offline-cache/corepack"
|
||||
if [[ ! -d "$STORE_DIR" ]]; then
|
||||
echo "Missing $STORE_DIR. Run scripts/offline/download-deps.sh on a networked machine first, then copy the checkout." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$COREPACK_HOME" ]]; then
|
||||
echo "Missing $COREPACK_HOME. Re-run download-deps.sh so Corepack pnpm is cached in-repo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export COREPACK_HOME
|
||||
export COREPACK_ENABLE_NETWORK=0
|
||||
|
||||
PACKAGE_MANAGER="$(sed -n 's/.*"packageManager": "\(pnpm@[0-9.]*\)".*/\1/p' package.json | head -n 1)"
|
||||
|
||||
echo "Offline install $PACKAGE_MANAGER from $STORE_DIR (Node $(node --version))"
|
||||
corepack pnpm install --offline --frozen-lockfile --store-dir "$STORE_DIR"
|
||||
|
||||
if [[ "$BUILD" -eq 1 ]]; then
|
||||
echo 'Building host/client libs, then the web frontend'
|
||||
npm run build:lib
|
||||
corepack pnpm --dir apps/web run build
|
||||
echo "Build complete. Start the UI with: $NODE_PREFIX/bin/corepack pnpm dsh web"
|
||||
else
|
||||
echo 'Install complete. For dsh web also run: bash scripts/offline/install-deps.sh --build'
|
||||
fi
|
||||
65
scripts/run-source-dsh.ts
Normal file
65
scripts/run-source-dsh.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Source `pnpm dsh` entry: overlay a checkout-local HTTP proxy, respawn this
|
||||
* Node process so `fetch` binds it at start, then dispatch the CLI. The
|
||||
* calling shell is unchanged.
|
||||
* @module
|
||||
*/
|
||||
|
||||
/* v8 ignore file -- overlay behavior is tested in apply-source-dsh-http-proxy.spec.ts. */
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import {
|
||||
applySourceDshHttpProxy,
|
||||
readSourceDshHttpProxyFile,
|
||||
shouldReexecSourceDshForFetchProxy,
|
||||
SOURCE_DSH_PROXY_REEXEC_MARKER,
|
||||
sourceDshAppliedHttpProxyUrl,
|
||||
sourceDshHttpProxyOverlayPresent,
|
||||
} from './apply-source-dsh-http-proxy.ts'
|
||||
|
||||
const fileText = readSourceDshHttpProxyFile()
|
||||
const overlayUrl = process.env.DSH_HTTP_PROXY
|
||||
try {
|
||||
applySourceDshHttpProxy({
|
||||
env: process.env,
|
||||
fileText,
|
||||
overlayUrl,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
process.stderr.write(`dsh: ${message}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const appliedProxy = sourceDshAppliedHttpProxyUrl(process.env)
|
||||
if (
|
||||
appliedProxy !== undefined
|
||||
&& process.env[SOURCE_DSH_PROXY_REEXEC_MARKER] !== '1'
|
||||
) {
|
||||
process.stderr.write(`dsh: HTTP proxy overlay ${appliedProxy} (this Node process only)\n`)
|
||||
}
|
||||
|
||||
if (shouldReexecSourceDshForFetchProxy(
|
||||
process.env,
|
||||
sourceDshHttpProxyOverlayPresent(overlayUrl, fileText),
|
||||
)) {
|
||||
process.env[SOURCE_DSH_PROXY_REEXEC_MARKER] = '1'
|
||||
const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
|
||||
env: { ...process.env },
|
||||
stdio: 'inherit',
|
||||
})
|
||||
child.on('error', error => {
|
||||
process.stderr.write(`dsh: ${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exit(1)
|
||||
})
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal)
|
||||
return
|
||||
}
|
||||
process.exit(code ?? 1)
|
||||
})
|
||||
} else {
|
||||
await import('../apps/cli/src/bin.ts')
|
||||
}
|
||||
@@ -162,6 +162,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/web/web': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-web.' },
|
||||
'packages/web/web-fetch-http': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
|
||||
'packages/web/web-search-exa': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
|
||||
'packages/web/web-search-searxng': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
|
||||
'packages/workflow/workflow': { kind: 'indirect', reason: 'The service delegates parent and child model rendering to its consumer and engine.' },
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user