@panerelay/cli
Advanced tools
| import { type BrowserFetchRequest, type BrowserFetchPermissionResult, type BrowserFetchResponse, type BrowserFetchSessionCreated, type BrowserFetchBindingPolicy, type BridgeState } from '@panerelay/protocol'; | ||
| export type BrowserFetchHttpClient = (input: string | URL, init?: RequestInit) => Promise<Response>; | ||
| export interface BrowserFetchClientOptions { | ||
| fetch?: BrowserFetchHttpClient; | ||
| signal?: AbortSignal; | ||
| } | ||
| export interface BrowserFetchSessionAuthority { | ||
| allowedOrigins: string[]; | ||
| bindingPolicies?: BrowserFetchBindingPolicy[]; | ||
| } | ||
| export interface ActiveBrowserFetchSession { | ||
| state: BridgeState; | ||
| session: BrowserFetchSessionCreated; | ||
| } | ||
| export declare function requestBrowserFetchPermission(state: BridgeState, domain: string, options?: BrowserFetchClientOptions): Promise<BrowserFetchPermissionResult>; | ||
| export declare function createBrowserFetchSession(state: BridgeState, authority: BrowserFetchSessionAuthority, options?: BrowserFetchClientOptions): Promise<ActiveBrowserFetchSession>; | ||
| export declare function releaseBrowserFetchSession(active: ActiveBrowserFetchSession, options?: BrowserFetchClientOptions): Promise<void>; | ||
| export declare function runBrowserFetchInSession(active: ActiveBrowserFetchSession, request: BrowserFetchRequest, options?: BrowserFetchClientOptions): Promise<BrowserFetchResponse>; | ||
| export declare function runBrowserFetch(state: BridgeState, request: BrowserFetchRequest, options?: BrowserFetchClientOptions): Promise<BrowserFetchResponse>; | ||
| //# sourceMappingURL=browser-fetch-client.d.ts.map |
| {"version":3,"file":"browser-fetch-client.d.ts","sourceRoot":"","sources":["../src/browser-fetch-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,EACjC,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEpG,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,4BAA4B;IAC3C,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,yBAAyB,EAAE,CAAC;CAC/C;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,0BAA0B,CAAC;CACrC;AA8BD,wBAAsB,6BAA6B,CACjD,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,4BAA4B,CAAC,CAwBvC;AAED,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,4BAA4B,EACvC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,yBAAyB,CAAC,CA2BpC;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,yBAAyB,EACjC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,oBAAoB,CAAC,CA0B/B;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAS/B"} |
| import { PANERELAY_FETCH_SESSION_PROTOCOL, PANERELAY_FETCH_PERMISSION_PROTOCOL, isBrowserFetchRequest, isBrowserFetchPermissionError, isBrowserFetchPermissionResult, isBrowserFetchResponse, isBrowserFetchSessionCreated, isBrowserFetchSessionError, browserFetchOriginForUrl, } from '@panerelay/protocol'; | ||
| function httpClient(options) { | ||
| return options.fetch ?? fetch; | ||
| } | ||
| function boundedJsonPayload(value, maximumBytes) { | ||
| if (Buffer.byteLength(value) > maximumBytes) | ||
| return null; | ||
| try { | ||
| return JSON.parse(value); | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| } | ||
| function responseError(response, payload, fallback, secrets) { | ||
| const detail = secrets.reduce((current, secret) => (secret ? current.replaceAll(secret, '[redacted]') : current), isBrowserFetchSessionError(payload) || isBrowserFetchPermissionError(payload) | ||
| ? payload.error | ||
| : fallback); | ||
| return new Error(`${detail} (Bridge HTTP ${response.status})`); | ||
| } | ||
| export async function requestBrowserFetchPermission(state, domain, options = {}) { | ||
| const response = await httpClient(options)(`http://127.0.0.1:${state.port}/fetch/permissions`, { | ||
| method: 'POST', | ||
| headers: { | ||
| authorization: `Bearer ${state.token}`, | ||
| 'content-type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| protocol: PANERELAY_FETCH_PERMISSION_PROTOCOL, | ||
| browser: { browserId: state.browserId, generation: state.generation }, | ||
| domain, | ||
| }), | ||
| signal: options.signal, | ||
| }); | ||
| const payload = boundedJsonPayload(await response.text(), 4_096); | ||
| if (!response.ok) { | ||
| throw responseError(response, payload, 'Unable to request browser fetch authorization', [ | ||
| state.token, | ||
| ]); | ||
| } | ||
| if (!isBrowserFetchPermissionResult(payload)) { | ||
| throw new Error('Panerelay Bridge returned an invalid browser fetch authorization result'); | ||
| } | ||
| return payload; | ||
| } | ||
| export async function createBrowserFetchSession(state, authority, options = {}) { | ||
| const response = await httpClient(options)(`http://127.0.0.1:${state.port}/fetch/sessions`, { | ||
| method: 'POST', | ||
| headers: { | ||
| authorization: `Bearer ${state.token}`, | ||
| 'content-type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| protocol: PANERELAY_FETCH_SESSION_PROTOCOL, | ||
| browser: { browserId: state.browserId, generation: state.generation }, | ||
| allowedOrigins: authority.allowedOrigins, | ||
| ...(authority.bindingPolicies ? { bindingPolicies: authority.bindingPolicies } : {}), | ||
| }), | ||
| signal: options.signal, | ||
| }); | ||
| const payload = boundedJsonPayload(await response.text(), 4_096); | ||
| if (!response.ok) { | ||
| throw responseError(response, payload, 'Unable to create browser fetch session', [state.token]); | ||
| } | ||
| if (!isBrowserFetchSessionCreated(payload)) { | ||
| throw new Error('Panerelay Bridge returned an invalid browser fetch session'); | ||
| } | ||
| const endpoint = new URL(payload.endpoint); | ||
| if (endpoint.hostname !== '127.0.0.1' || Number(endpoint.port) !== state.port) { | ||
| throw new Error('Panerelay Bridge returned a fetch endpoint for a different listener'); | ||
| } | ||
| return { state, session: payload }; | ||
| } | ||
| export async function releaseBrowserFetchSession(active, options = {}) { | ||
| const endpoint = new URL(active.session.endpoint); | ||
| const response = await httpClient(options)(`${endpoint.origin}/fetch/sessions/${encodeURIComponent(active.session.sessionId)}`, { | ||
| method: 'DELETE', | ||
| headers: { authorization: `Bearer ${active.state.token}` }, | ||
| }); | ||
| if (!response.ok) | ||
| throw new Error(`Unable to release browser fetch session (${response.status})`); | ||
| } | ||
| export async function runBrowserFetchInSession(active, request, options = {}) { | ||
| if (!isBrowserFetchRequest(request)) | ||
| throw new Error('Invalid browser fetch request'); | ||
| if (Date.parse(active.session.expiresAt) <= Date.now()) { | ||
| throw new Error('Panerelay browser fetch session expired'); | ||
| } | ||
| const response = await httpClient(options)(active.session.endpoint, { | ||
| method: 'POST', | ||
| headers: { | ||
| authorization: `Bearer ${active.session.token}`, | ||
| 'content-type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(request), | ||
| signal: options.signal, | ||
| }); | ||
| const responseText = await response.text(); | ||
| const payload = boundedJsonPayload(responseText, response.ok ? 48 * 1024 * 1024 : 4_096); | ||
| if (!response.ok) { | ||
| throw responseError(response, payload, 'Browser fetch failed', [ | ||
| active.state.token, | ||
| active.session.token, | ||
| ]); | ||
| } | ||
| if (!isBrowserFetchResponse(payload)) { | ||
| throw new Error('Panerelay Bridge returned an invalid browser fetch response'); | ||
| } | ||
| return payload; | ||
| } | ||
| export async function runBrowserFetch(state, request, options = {}) { | ||
| const origin = browserFetchOriginForUrl(request.url); | ||
| if (!origin) | ||
| throw new Error('Browser fetch requires an absolute HTTP(S) URL'); | ||
| const active = await createBrowserFetchSession(state, { allowedOrigins: [origin] }, options); | ||
| try { | ||
| return await runBrowserFetchInSession(active, request, options); | ||
| } | ||
| finally { | ||
| await releaseBrowserFetchSession(active, { fetch: options.fetch }).catch(() => undefined); | ||
| } | ||
| } |
| import { type FetchAdapterCommand, type FetchAdapterInvocationArtifact } from '@panerelay/protocol'; | ||
| export interface PreparedFetchAdapterArtifacts { | ||
| args: Record<string, string | number | boolean>; | ||
| artifacts?: FetchAdapterInvocationArtifact[]; | ||
| } | ||
| export declare function prepareFetchAdapterArtifacts(command: FetchAdapterCommand, args: Record<string, string | number | boolean>): Promise<PreparedFetchAdapterArtifacts>; | ||
| //# sourceMappingURL=fetch-adapter-artifact.d.ts.map |
| {"version":3,"file":"fetch-adapter-artifact.d.ts","sourceRoot":"","sources":["../src/fetch-adapter-artifact.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACpC,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,8BAA8B,EAAE,CAAC;CAC9C;AA+FD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAC9C,OAAO,CAAC,6BAA6B,CAAC,CAWxC"} |
| import { randomUUID } from 'node:crypto'; | ||
| import { constants } from 'node:fs'; | ||
| import { lstat, open } from 'node:fs/promises'; | ||
| import { basename, extname } from 'node:path'; | ||
| import { PANERELAY_FETCH_ADAPTER_MAX_FILE_BYTES, PANERELAY_FETCH_ADAPTER_MAX_SOURCE_PATH_BYTES, } from '@panerelay/protocol'; | ||
| function mediaTypeFor(name) { | ||
| const types = { | ||
| '.csv': 'text/csv', | ||
| '.gif': 'image/gif', | ||
| '.jpeg': 'image/jpeg', | ||
| '.jpg': 'image/jpeg', | ||
| '.json': 'application/json', | ||
| '.pdf': 'application/pdf', | ||
| '.png': 'image/png', | ||
| '.txt': 'text/plain', | ||
| '.webp': 'image/webp', | ||
| '.zip': 'application/zip', | ||
| }; | ||
| return types[extname(name).toLowerCase()] ?? 'application/octet-stream'; | ||
| } | ||
| function sameIdentity(left, right) { | ||
| return (left.dev === right.dev && | ||
| left.ino === right.ino && | ||
| left.size === right.size && | ||
| left.mtimeMs === right.mtimeMs && | ||
| left.ctimeMs === right.ctimeMs); | ||
| } | ||
| async function prepareArtifact(argumentName, path) { | ||
| if (!path || | ||
| Buffer.byteLength(path, 'utf8') > PANERELAY_FETCH_ADAPTER_MAX_SOURCE_PATH_BYTES || | ||
| /\0/.test(path)) { | ||
| throw new Error(`File argument ${argumentName} has an invalid path`); | ||
| } | ||
| let handle; | ||
| try { | ||
| const pathBefore = await lstat(path); | ||
| if (!pathBefore.isFile() || pathBefore.isSymbolicLink()) { | ||
| throw new Error(`File argument ${argumentName} must be a non-symlink regular file`); | ||
| } | ||
| if (pathBefore.size > PANERELAY_FETCH_ADAPTER_MAX_FILE_BYTES) { | ||
| throw new Error(`File argument ${argumentName} exceeds the 12 MiB limit`); | ||
| } | ||
| handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); | ||
| const openedBefore = await handle.stat(); | ||
| if (!openedBefore.isFile() || !sameIdentity(pathBefore, openedBefore)) { | ||
| throw new Error(`File argument ${argumentName} changed before it could be read`); | ||
| } | ||
| const data = await handle.readFile(); | ||
| if (data.length > PANERELAY_FETCH_ADAPTER_MAX_FILE_BYTES) { | ||
| throw new Error(`File argument ${argumentName} exceeds the 12 MiB limit`); | ||
| } | ||
| const [openedAfter, pathAfter] = await Promise.all([handle.stat(), lstat(path)]); | ||
| if (!sameIdentity(openedBefore, openedAfter) || | ||
| openedAfter.dev !== pathAfter.dev || | ||
| openedAfter.ino !== pathAfter.ino || | ||
| openedAfter.size !== data.length) { | ||
| throw new Error(`File argument ${argumentName} changed while it was read`); | ||
| } | ||
| const safeBasename = basename(path); | ||
| if (!safeBasename || | ||
| safeBasename === '.' || | ||
| safeBasename === '..' || | ||
| /[\r\n\p{Cc}]/u.test(safeBasename)) { | ||
| throw new Error(`File argument ${argumentName} has an unsafe filename`); | ||
| } | ||
| return { | ||
| id: `artifact_${randomUUID().replaceAll('-', '')}`, | ||
| basename: safeBasename, | ||
| mediaType: mediaTypeFor(safeBasename), | ||
| size: data.length, | ||
| data: data.toString('base64'), | ||
| }; | ||
| } | ||
| catch (error) { | ||
| if (error instanceof Error && error.message.startsWith(`File argument ${argumentName}`)) { | ||
| throw error; | ||
| } | ||
| throw new Error(`Unable to read file argument ${argumentName}`, { cause: error }); | ||
| } | ||
| finally { | ||
| await handle?.close().catch(() => undefined); | ||
| } | ||
| } | ||
| export async function prepareFetchAdapterArtifacts(command, args) { | ||
| const fileArgument = command.args.find(argument => argument.type === 'file'); | ||
| if (!fileArgument) | ||
| return { args }; | ||
| const path = args[fileArgument.name]; | ||
| if (path === undefined) | ||
| return { args }; | ||
| if (typeof path !== 'string') | ||
| throw new Error(`File argument ${fileArgument.name} is invalid`); | ||
| const artifact = await prepareArtifact(fileArgument.name, path); | ||
| return { | ||
| args: { ...args, [fileArgument.name]: artifact.id }, | ||
| artifacts: [artifact], | ||
| }; | ||
| } |
| import { type FetchAdapterInvocationResponse, type FetchAdapterInvocationArtifact, type FetchAdapterRegistration } from '@panerelay/protocol'; | ||
| import type { ActiveBrowserFetchSession } from './browser-fetch-client.js'; | ||
| export interface FetchAdapterDispatchOptions { | ||
| environment?: NodeJS.ProcessEnv; | ||
| timeoutMs?: number; | ||
| artifacts?: FetchAdapterInvocationArtifact[]; | ||
| } | ||
| export declare class FetchAdapterCommandError extends Error { | ||
| readonly code: NonNullable<FetchAdapterInvocationResponse['error']>['code']; | ||
| readonly retryable?: boolean | undefined; | ||
| constructor(code: NonNullable<FetchAdapterInvocationResponse['error']>['code'], message: string, retryable?: boolean | undefined); | ||
| } | ||
| export declare function dispatchFetchAdapter(registration: FetchAdapterRegistration, active: ActiveBrowserFetchSession, command: string, args: Record<string, string | number | boolean>, options?: FetchAdapterDispatchOptions): Promise<unknown>; | ||
| //# sourceMappingURL=fetch-adapter-dispatcher.d.ts.map |
| {"version":3,"file":"fetch-adapter-dispatcher.d.ts","sourceRoot":"","sources":["../src/fetch-adapter-dispatcher.ts"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,8BAA8B,EACnC,KAAK,8BAA8B,EACnC,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAI3E,MAAM,WAAW,2BAA2B;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,8BAA8B,EAAE,CAAC;CAC9C;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IAE/C,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,8BAA8B,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAE3E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO;gBAFnB,IAAI,EAAE,WAAW,CAAC,8BAA8B,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAC3E,OAAO,EAAE,MAAM,EACN,SAAS,CAAC,EAAE,OAAO,YAAA;CAK/B;AA0BD,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,wBAAwB,EACtC,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAC/C,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,OAAO,CAAC,CAyFlB"} |
| import { spawn } from 'node:child_process'; | ||
| import { randomUUID } from 'node:crypto'; | ||
| import { PANERELAY_FETCH_ADAPTER_MAX_OUTPUT_BYTES, PANERELAY_FETCH_ADAPTER_MAX_STDERR_BYTES, PANERELAY_FETCH_ADAPTER_PROTOCOL, isFetchAdapterInvocationResponse, serializeFetchAdapterMessage, } from '@panerelay/protocol'; | ||
| const DEFAULT_ADAPTER_TIMEOUT_MS = 120_000; | ||
| export class FetchAdapterCommandError extends Error { | ||
| code; | ||
| retryable; | ||
| constructor(code, message, retryable) { | ||
| super(`[${code}] ${message}`); | ||
| this.code = code; | ||
| this.retryable = retryable; | ||
| this.name = 'FetchAdapterCommandError'; | ||
| } | ||
| } | ||
| function minimalEnvironment(source) { | ||
| const result = {}; | ||
| for (const name of [ | ||
| 'PATH', | ||
| 'SystemRoot', | ||
| 'SYSTEMROOT', | ||
| 'WINDIR', | ||
| 'ComSpec', | ||
| 'TEMP', | ||
| 'TMP', | ||
| 'TMPDIR', | ||
| ]) { | ||
| if (source[name] !== undefined) | ||
| result[name] = source[name]; | ||
| } | ||
| return result; | ||
| } | ||
| function redact(value, secrets) { | ||
| return secrets.reduce((current, secret) => (secret ? current.replaceAll(secret, '[redacted]') : current), value); | ||
| } | ||
| export async function dispatchFetchAdapter(registration, active, command, args, options = {}) { | ||
| const request = { | ||
| protocol: PANERELAY_FETCH_ADAPTER_PROTOCOL, | ||
| requestId: randomUUID(), | ||
| operation: 'execute', | ||
| command, | ||
| args, | ||
| ...(options.artifacts ? { artifacts: options.artifacts } : {}), | ||
| fetch: { | ||
| endpoint: active.session.endpoint, | ||
| token: active.session.token, | ||
| expiresAt: active.session.expiresAt, | ||
| }, | ||
| }; | ||
| const serialized = serializeFetchAdapterMessage(request); | ||
| return new Promise((resolve, reject) => { | ||
| const child = spawn(process.execPath, [registration.executablePath], { | ||
| env: minimalEnvironment(options.environment ?? process.env), | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| windowsHide: true, | ||
| }); | ||
| let output = ''; | ||
| let outputBytes = 0; | ||
| let stderrBytes = 0; | ||
| let settled = false; | ||
| const finish = (action) => { | ||
| if (settled) | ||
| return; | ||
| settled = true; | ||
| clearTimeout(timer); | ||
| action(); | ||
| }; | ||
| const fail = (message) => { | ||
| child.kill('SIGKILL'); | ||
| finish(() => reject(new Error(message))); | ||
| }; | ||
| const timer = setTimeout(() => fail('Fetch adapter timed out'), options.timeoutMs ?? DEFAULT_ADAPTER_TIMEOUT_MS); | ||
| timer.unref(); | ||
| child.stdout.setEncoding('utf8'); | ||
| child.stdout.on('data', (chunk) => { | ||
| outputBytes += Buffer.byteLength(chunk); | ||
| if (outputBytes > PANERELAY_FETCH_ADAPTER_MAX_OUTPUT_BYTES) { | ||
| fail('Fetch adapter response exceeded the protocol limit'); | ||
| return; | ||
| } | ||
| output += chunk; | ||
| }); | ||
| child.stderr.on('data', (chunk) => { | ||
| stderrBytes += Buffer.byteLength(chunk); | ||
| if (stderrBytes > PANERELAY_FETCH_ADAPTER_MAX_STDERR_BYTES) { | ||
| fail('Fetch adapter diagnostic output exceeded the protocol limit'); | ||
| } | ||
| }); | ||
| child.once('error', () => finish(() => reject(new Error('Fetch adapter could not be started')))); | ||
| child.once('close', code => { | ||
| if (settled) | ||
| return; | ||
| try { | ||
| const response = JSON.parse(output.trim()); | ||
| if (code !== 0 || | ||
| !isFetchAdapterInvocationResponse(response) || | ||
| response.requestId !== request.requestId) { | ||
| throw new Error('invalid response'); | ||
| } | ||
| if (!response.success) { | ||
| const message = redact(response.error?.message ?? 'Fetch adapter failed', [ | ||
| active.session.token, | ||
| ]); | ||
| const code = response.error?.code ?? 'command-failed'; | ||
| finish(() => reject(new FetchAdapterCommandError(code, message, response.error?.retryable))); | ||
| return; | ||
| } | ||
| finish(() => resolve(response.result)); | ||
| } | ||
| catch { | ||
| finish(() => reject(new Error('Fetch adapter returned an invalid response'))); | ||
| } | ||
| }); | ||
| child.stdin.on('error', () => undefined); | ||
| child.stdin.end(serialized); | ||
| }); | ||
| } |
| import { type FetchAdapterRegistration, type FetchAdapterRegistry } from '@panerelay/protocol'; | ||
| export declare const PANERELAY_FETCH_ADAPTER_REGISTRY_PATH_ENV: "PANERELAY_FETCH_ADAPTER_REGISTRY_PATH"; | ||
| export interface FetchAdapterRegistryOptions { | ||
| environment?: NodeJS.ProcessEnv; | ||
| registryPath?: string; | ||
| verifyExecutables?: boolean; | ||
| } | ||
| export declare function fetchAdapterRegistryPath(options?: FetchAdapterRegistryOptions): string; | ||
| export declare function fetchAdapterDataDirectory(options?: FetchAdapterRegistryOptions): string; | ||
| export declare function readFetchAdapterRegistry(options?: FetchAdapterRegistryOptions): Promise<FetchAdapterRegistry>; | ||
| export declare function readFetchAdapterRegistration(id: string, options?: FetchAdapterRegistryOptions): Promise<FetchAdapterRegistration | null>; | ||
| //# sourceMappingURL=fetch-adapter-registry.d.ts.map |
| {"version":3,"file":"fetch-adapter-registry.d.ts","sourceRoot":"","sources":["../src/fetch-adapter-registry.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,yCAAyC,EACpD,uCAAgD,CAAC;AAEnD,MAAM,WAAW,2BAA2B;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,2BAAgC,GAAG,MAAM,CAM1F;AAED,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,2BAAgC,GAAG,MAAM,CAE3F;AAiFD,wBAAsB,wBAAwB,CAC5C,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,oBAAoB,CAAC,CAkB/B;AAED,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAG1C"} |
| import { createHash } from 'node:crypto'; | ||
| import { lstat, readFile, realpath } from 'node:fs/promises'; | ||
| import { homedir } from 'node:os'; | ||
| import { dirname, join, resolve, sep } from 'node:path'; | ||
| import { PANERELAY_FETCH_ADAPTER_REGISTRY_PROTOCOL, isFetchAdapterRegistry, } from '@panerelay/protocol'; | ||
| export const PANERELAY_FETCH_ADAPTER_REGISTRY_PATH_ENV = 'PANERELAY_FETCH_ADAPTER_REGISTRY_PATH'; | ||
| export function fetchAdapterRegistryPath(options = {}) { | ||
| return (options.registryPath ?? | ||
| (options.environment ?? process.env)[PANERELAY_FETCH_ADAPTER_REGISTRY_PATH_ENV] ?? | ||
| join(homedir(), '.panerelay', 'fetch-adapters', 'registry.json')); | ||
| } | ||
| export function fetchAdapterDataDirectory(options = {}) { | ||
| return dirname(fetchAdapterRegistryPath(options)); | ||
| } | ||
| function isPosix() { | ||
| return process.platform !== 'win32'; | ||
| } | ||
| async function assertProtectedRegularFile(path, label) { | ||
| const metadata = await lstat(path); | ||
| if (!metadata.isFile() || metadata.isSymbolicLink()) { | ||
| throw new Error(`${label} is not a regular file`); | ||
| } | ||
| if (isPosix()) { | ||
| if ((metadata.mode & 0o077) !== 0) | ||
| throw new Error(`${label} permissions must be 0600`); | ||
| if (typeof process.getuid === 'function' && metadata.uid !== process.getuid()) { | ||
| throw new Error(`${label} is owned by another user`); | ||
| } | ||
| } | ||
| } | ||
| async function assertProtectedDirectory(path, label) { | ||
| const metadata = await lstat(path); | ||
| if (!metadata.isDirectory() || metadata.isSymbolicLink()) { | ||
| throw new Error(`${label} is not a regular directory`); | ||
| } | ||
| if (isPosix()) { | ||
| if ((metadata.mode & 0o077) !== 0) | ||
| throw new Error(`${label} permissions must be 0700`); | ||
| if (typeof process.getuid === 'function' && metadata.uid !== process.getuid()) { | ||
| throw new Error(`${label} is owned by another user`); | ||
| } | ||
| } | ||
| } | ||
| async function verifyRegistration(registration, options) { | ||
| const registryDirectory = fetchAdapterDataDirectory(options); | ||
| const expected = resolve(registryDirectory, registration.manifest.id, registration.manifest.version, registration.manifest.entry); | ||
| if (resolve(registration.executablePath) !== expected) { | ||
| throw new Error(`Fetch adapter ${registration.manifest.id} executable is outside its version directory`); | ||
| } | ||
| await assertProtectedRegularFile(expected, `Fetch adapter ${registration.manifest.id}`); | ||
| const root = resolve(registryDirectory); | ||
| let currentDirectory = dirname(expected); | ||
| while (true) { | ||
| await assertProtectedDirectory(currentDirectory, `Fetch adapter ${registration.manifest.id} directory`); | ||
| if (currentDirectory === root) | ||
| break; | ||
| const parent = dirname(currentDirectory); | ||
| if (parent === currentDirectory || !`${currentDirectory}${sep}`.startsWith(`${root}${sep}`)) { | ||
| throw new Error(`Fetch adapter ${registration.manifest.id} directory is outside protected storage`); | ||
| } | ||
| currentDirectory = parent; | ||
| } | ||
| const [realDirectory, realExecutable] = await Promise.all([ | ||
| realpath(registryDirectory), | ||
| realpath(expected), | ||
| ]); | ||
| const expectedRealPrefix = `${join(realDirectory, registration.manifest.id, registration.manifest.version)}${sep}`; | ||
| if (!realExecutable.startsWith(expectedRealPrefix)) { | ||
| throw new Error(`Fetch adapter ${registration.manifest.id} resolves outside protected storage`); | ||
| } | ||
| const digest = createHash('sha256') | ||
| .update(await readFile(realExecutable)) | ||
| .digest('hex'); | ||
| if (digest !== registration.sha256) { | ||
| throw new Error(`Fetch adapter ${registration.manifest.id} executable digest changed`); | ||
| } | ||
| } | ||
| export async function readFetchAdapterRegistry(options = {}) { | ||
| const path = fetchAdapterRegistryPath(options); | ||
| try { | ||
| await assertProtectedDirectory(dirname(path), 'Fetch adapter registry directory'); | ||
| await assertProtectedRegularFile(path, 'Fetch adapter registry'); | ||
| const value = JSON.parse(await readFile(path, 'utf8')); | ||
| if (!isFetchAdapterRegistry(value)) | ||
| throw new Error('Fetch adapter registry is invalid'); | ||
| if (options.verifyExecutables) { | ||
| for (const registration of value.adapters) | ||
| await verifyRegistration(registration, options); | ||
| } | ||
| return value; | ||
| } | ||
| catch (error) { | ||
| const code = error.code; | ||
| if (code === 'ENOENT') { | ||
| return { protocol: PANERELAY_FETCH_ADAPTER_REGISTRY_PROTOCOL, adapters: [] }; | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| export async function readFetchAdapterRegistration(id, options = {}) { | ||
| const registry = await readFetchAdapterRegistry({ ...options, verifyExecutables: true }); | ||
| return registry.adapters.find(adapter => adapter.manifest.id === id) ?? null; | ||
| } |
| import { type BrowserSelection, type BrowserRegistryOptions } from '@panerelay/browser-registry'; | ||
| import { type BrowserFetchRequest } from '@panerelay/protocol'; | ||
| import { createBrowserFetchSession, releaseBrowserFetchSession, requestBrowserFetchPermission, runBrowserFetch } from './browser-fetch-client.js'; | ||
| import { dispatchFetchAdapter } from './fetch-adapter-dispatcher.js'; | ||
| import { prepareFetchAdapterArtifacts } from './fetch-adapter-artifact.js'; | ||
| import { readFetchAdapterRegistration, readFetchAdapterRegistry } from './fetch-adapter-registry.js'; | ||
| import type { SupportedLocale } from './i18n.js'; | ||
| export interface ParsedRawFetch { | ||
| browserSelector?: string; | ||
| request: BrowserFetchRequest; | ||
| } | ||
| export interface ParsedFetchAuthorization { | ||
| browserSelector?: string; | ||
| domain: string; | ||
| } | ||
| export interface FetchCommandDependencies { | ||
| createBrowserFetchSession?: typeof createBrowserFetchSession; | ||
| dispatchFetchAdapter?: typeof dispatchFetchAdapter; | ||
| readFetchAdapterRegistration?: typeof readFetchAdapterRegistration; | ||
| readFetchAdapterRegistry?: typeof readFetchAdapterRegistry; | ||
| releaseBrowserFetchSession?: typeof releaseBrowserFetchSession; | ||
| requestBrowserFetchPermission?: typeof requestBrowserFetchPermission; | ||
| runBrowserFetch?: typeof runBrowserFetch; | ||
| prepareFetchAdapterArtifacts?: typeof prepareFetchAdapterArtifacts; | ||
| selectBrowserFetchRegistration?: (options?: BrowserRegistryOptions) => Promise<BrowserSelection>; | ||
| now?: () => number; | ||
| } | ||
| export interface FetchCommandOptions { | ||
| dependencies?: FetchCommandDependencies; | ||
| environment?: NodeJS.ProcessEnv; | ||
| locale: SupportedLocale; | ||
| } | ||
| export declare function parseRawFetchArguments(argv: string[], locale?: SupportedLocale): ParsedRawFetch; | ||
| export declare function parseFetchAuthorizationArguments(argv: string[], locale?: SupportedLocale): ParsedFetchAuthorization; | ||
| export declare function runFetchCommand(argv: string[], options: FetchCommandOptions): Promise<number>; | ||
| //# sourceMappingURL=fetch-command.d.ts.map |
| {"version":3,"file":"fetch-command.d.ts","sourceRoot":"","sources":["../src/fetch-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAKL,KAAK,mBAAmB,EAOzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,yBAAyB,EACzB,0BAA0B,EAC1B,6BAA6B,EAC7B,eAAe,EAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,EACL,4BAA4B,EAC5B,wBAAwB,EAEzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,yBAAyB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IAC7D,oBAAoB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACnD,4BAA4B,CAAC,EAAE,OAAO,4BAA4B,CAAC;IACnE,wBAAwB,CAAC,EAAE,OAAO,wBAAwB,CAAC;IAC3D,0BAA0B,CAAC,EAAE,OAAO,0BAA0B,CAAC;IAC/D,6BAA6B,CAAC,EAAE,OAAO,6BAA6B,CAAC;IACrE,eAAe,CAAC,EAAE,OAAO,eAAe,CAAC;IACzC,4BAA4B,CAAC,EAAE,OAAO,4BAA4B,CAAC;IACnE,8BAA8B,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,sBAAsB,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjG,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,EAAE,wBAAwB,CAAC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,MAAM,EAAE,eAAe,CAAC;CACzB;AAgMD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,GAAE,eAAsB,GAC7B,cAAc,CAiHhB;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,GAAE,eAAsB,GAC7B,wBAAwB,CAsC1B;AAmRD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CA4GjB"} |
| import { PANERELAY_BROWSER_ENV, PANERELAY_BROWSER_ID_ENV, selectBrowserFetchRegistration, } from '@panerelay/browser-registry'; | ||
| import { PANERELAY_FETCH_METHODS, isBrowserFetchRequest, normalizeBrowserFetchDomain, } from '@panerelay/protocol'; | ||
| import { createBrowserFetchSession, releaseBrowserFetchSession, requestBrowserFetchPermission, runBrowserFetch, } from './browser-fetch-client.js'; | ||
| import { dispatchFetchAdapter } from './fetch-adapter-dispatcher.js'; | ||
| import { prepareFetchAdapterArtifacts } from './fetch-adapter-artifact.js'; | ||
| import { readFetchAdapterRegistration, readFetchAdapterRegistry, } from './fetch-adapter-registry.js'; | ||
| function localized(locale, english, chinese) { | ||
| return locale === 'zh-CN' ? chinese : english; | ||
| } | ||
| function fetchHelp(locale, registry) { | ||
| const adapters = registry.adapters.length | ||
| ? registry.adapters | ||
| .map(adapter => ` ${adapter.manifest.id.padEnd(16)}${adapter.manifest.description}`) | ||
| .join('\n') | ||
| : localized(locale, ' (none installed)', ' (尚未安装)'); | ||
| return localized(locale, `Panerelay browser fetch | ||
| Usage: | ||
| panerelay fetch <url> [options] | ||
| panerelay fetch --authorize <hostname|*.domain|url> [--browser <selector>] | ||
| panerelay <site> <command> [arguments] | ||
| panerelay <site> --help | ||
| panerelay fetch <site> <command> [arguments] Explicit form | ||
| Raw fetch options: | ||
| --method <method> GET, POST, PUT, DELETE, PATCH, HEAD, or OPTIONS | ||
| --header, -H <name:value> Add a request header; repeatable | ||
| --query <name:value> Append a query value; repeatable | ||
| --data <text> Send a UTF-8 body | ||
| --data-base64 <base64> Send a Base64 body | ||
| --response <type> auto, json, text, or base64 | ||
| --timeout <ms> 100 through 120000 | ||
| --cookies | --no-cookies Include browser cookies (default: include) | ||
| --browser <selector> Select one live Panerelay browser | ||
| --help, -h Show help without connecting to a browser | ||
| Authorization: | ||
| --authorize <domain> Ask the user to allow a hostname or wildcard domain | ||
| Adapter invocation options: | ||
| --json Print the adapter result as JSON instead of a table | ||
| --browser <selector> Select one live Panerelay browser | ||
| Installed site adapters: | ||
| ${adapters} | ||
| Manage adapters with @panerelay/setup add, remove, and adapters.`, `Panerelay 浏览器 Fetch | ||
| 用法: | ||
| panerelay fetch <URL> [选项] | ||
| panerelay fetch --authorize <主机名|*.域名|URL> [--browser <选择器>] | ||
| panerelay <站点> <命令> [参数] | ||
| panerelay <站点> --help | ||
| panerelay fetch <站点> <命令> [参数] 显式形式 | ||
| 原始 Fetch 选项: | ||
| --method <方法> GET、POST、PUT、DELETE、PATCH、HEAD 或 OPTIONS | ||
| --header, -H <名称:值> 添加请求头,可重复 | ||
| --query <名称:值> 追加查询参数,可重复 | ||
| --data <文本> 发送 UTF-8 请求体 | ||
| --data-base64 <base64> 发送 Base64 请求体 | ||
| --response <类型> auto、json、text 或 base64 | ||
| --timeout <毫秒> 100 到 120000 | ||
| --cookies | --no-cookies 携带浏览器 Cookie(默认携带) | ||
| --browser <选择器> 选择一个在线 Panerelay 浏览器 | ||
| --help, -h 不连接浏览器并显示帮助 | ||
| 授权: | ||
| --authorize <域名> 请求用户允许主机名或通配域名 | ||
| 适配器调用选项: | ||
| --json 使用 JSON 输出适配器结果,不渲染表格 | ||
| --browser <选择器> 选择一个在线 Panerelay 浏览器 | ||
| 已安装站点适配器: | ||
| ${adapters} | ||
| 使用 @panerelay/setup 的 add、remove 和 adapters 管理适配器。`); | ||
| } | ||
| function siteHelp(locale, manifest) { | ||
| const commands = manifest.commands | ||
| .map(command => ` ${command.name.padEnd(16)}${command.description}`) | ||
| .join('\n'); | ||
| return `${manifest.name} (${manifest.id})\n\n${manifest.description}\n\n${localized(locale, 'Usage:', '用法:')}\n panerelay ${manifest.id} <command> [arguments] [--json]\n panerelay fetch ${manifest.id} <command> [arguments] [--json] ${localized(locale, '(explicit form)', '(显式形式)')}\n\n${localized(locale, 'Commands:', '命令:')}\n${commands}\n\n${localized(locale, 'Options:', '选项:')} | ||
| --json ${localized(locale, 'Print JSON instead of a table', '使用 JSON 输出,不渲染表格')} | ||
| --browser <selector> ${localized(locale, 'Select one live Panerelay browser', '选择一个在线 Panerelay 浏览器')}`; | ||
| } | ||
| function commandHelp(locale, manifest, command) { | ||
| const positional = command.args.filter(argument => argument.positional); | ||
| const usageArguments = [ | ||
| ...positional.map(argument => argument.required ? `<${argument.name}>` : `[${argument.name}]`), | ||
| '[options]', | ||
| ].join(' '); | ||
| const argumentLines = command.args.length | ||
| ? command.args | ||
| .map(argument => { | ||
| const label = argument.positional ? argument.name : `--${argument.name}`; | ||
| return ` ${label.padEnd(20)}${argument.description}`; | ||
| }) | ||
| .join('\n') | ||
| : localized(locale, ' (none)', ' (无)'); | ||
| return `${manifest.name}: ${command.name}\n\n${command.description}\n\n${localized(locale, 'Usage:', '用法:')}\n panerelay ${manifest.id} ${command.name}${usageArguments ? ` ${usageArguments}` : ''}\n panerelay fetch ${manifest.id} ${command.name}${usageArguments ? ` ${usageArguments}` : ''} ${localized(locale, '(explicit form)', '(显式形式)')}\n\n${localized(locale, 'Arguments:', '参数:')}\n${argumentLines}\n\n${localized(locale, 'Options:', '选项:')} | ||
| --json ${localized(locale, 'Print JSON instead of a table', '使用 JSON 输出,不渲染表格')} | ||
| --browser <selector> ${localized(locale, 'Select one live Panerelay browser', '选择一个在线 Panerelay 浏览器')}\n\n${localized(locale, 'Output fields:', '输出字段:')} | ||
| ${command.output.join(', ')}\n\n${localized(locale, 'Examples:', '示例:')}\n${command.examples | ||
| .map(example => ` ${example}`) | ||
| .join('\n')}`; | ||
| } | ||
| function optionValue(argv, index) { | ||
| const argument = argv[index]; | ||
| const separator = argument.indexOf('='); | ||
| if (separator >= 0) { | ||
| return { | ||
| consumed: 1, | ||
| option: argument.slice(0, separator), | ||
| value: argument.slice(separator + 1), | ||
| }; | ||
| } | ||
| return { consumed: 2, option: argument, value: argv[index + 1] }; | ||
| } | ||
| function requiredOptionValue(locale, argv, index) { | ||
| const parsed = optionValue(argv, index); | ||
| if (parsed.value === undefined) { | ||
| throw new Error(localized(locale, `${parsed.option} requires a value`, `${parsed.option} 后需要指定值`)); | ||
| } | ||
| return { ...parsed, value: parsed.value }; | ||
| } | ||
| function splitPair(locale, option, value) { | ||
| const separator = value.indexOf(':'); | ||
| if (separator <= 0) { | ||
| throw new Error(localized(locale, `${option} requires name:value`, `${option} 需要使用“名称:值”格式`)); | ||
| } | ||
| return [value.slice(0, separator).trim(), value.slice(separator + 1).trimStart()]; | ||
| } | ||
| function absoluteHttpUrl(value) { | ||
| if (!value) | ||
| return false; | ||
| try { | ||
| const url = new URL(value); | ||
| return url.protocol === 'http:' || url.protocol === 'https:'; | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| } | ||
| export function parseRawFetchArguments(argv, locale = 'en') { | ||
| const url = argv[0]; | ||
| if (!url || !absoluteHttpUrl(url)) { | ||
| throw new Error(localized(locale, 'Raw fetch requires an absolute HTTP(S) URL', '原始 Fetch 需要绝对 HTTP(S) URL')); | ||
| } | ||
| const request = { url }; | ||
| let browserSelector; | ||
| let bodySeen = false; | ||
| let cookiesSeen; | ||
| for (let index = 1; index < argv.length;) { | ||
| const argument = argv[index]; | ||
| if (argument === '--cookies' || argument === '--no-cookies') { | ||
| const value = argument === '--cookies'; | ||
| if (cookiesSeen !== undefined && cookiesSeen !== value) { | ||
| throw new Error(localized(locale, '--cookies and --no-cookies cannot be combined', '--cookies 与 --no-cookies 不能同时使用')); | ||
| } | ||
| cookiesSeen = value; | ||
| request.withCookies = value; | ||
| index += 1; | ||
| continue; | ||
| } | ||
| const parsed = requiredOptionValue(locale, argv, index); | ||
| index += parsed.consumed; | ||
| if (parsed.option === '--method') { | ||
| const method = parsed.value.toUpperCase(); | ||
| if (!PANERELAY_FETCH_METHODS.includes(method)) { | ||
| throw new Error(localized(locale, `Unsupported fetch method: ${parsed.value}`, `不支持的 Fetch 方法:${parsed.value}`)); | ||
| } | ||
| request.method = method; | ||
| } | ||
| else if (parsed.option === '--header' || parsed.option === '-H') { | ||
| const [name, value] = splitPair(locale, parsed.option, parsed.value); | ||
| if (name.toLowerCase() === 'cookie') { | ||
| throw new Error(localized(locale, 'Cookie headers are not accepted; use browser cookies or --no-cookies', '不接受 Cookie 请求头;请使用浏览器 Cookie 或 --no-cookies')); | ||
| } | ||
| request.headers = { ...(request.headers ?? {}), [name]: value }; | ||
| } | ||
| else if (parsed.option === '--query') { | ||
| const [name, value] = splitPair(locale, parsed.option, parsed.value); | ||
| request.query = [...(request.query ?? []), { name, value }]; | ||
| } | ||
| else if (parsed.option === '--data' || parsed.option === '--data-base64') { | ||
| if (bodySeen) | ||
| throw new Error(localized(locale, 'Only one request body may be provided', '只能指定一种请求体')); | ||
| bodySeen = true; | ||
| request.body = { | ||
| encoding: parsed.option === '--data' ? 'utf8' : 'base64', | ||
| data: parsed.value, | ||
| }; | ||
| } | ||
| else if (parsed.option === '--response') { | ||
| if (!['auto', 'json', 'text', 'base64'].includes(parsed.value)) { | ||
| throw new Error(localized(locale, `Unsupported response type: ${parsed.value}`, `不支持的响应类型:${parsed.value}`)); | ||
| } | ||
| request.responseType = parsed.value; | ||
| } | ||
| else if (parsed.option === '--timeout') { | ||
| request.timeoutMs = Number(parsed.value); | ||
| } | ||
| else if (parsed.option === '--browser') { | ||
| if (!parsed.value) | ||
| throw new Error(localized(locale, '--browser requires a value', '--browser 后需要指定值')); | ||
| if (browserSelector) | ||
| throw new Error(localized(locale, '--browser can only be provided once', '--browser 只能指定一次')); | ||
| browserSelector = parsed.value; | ||
| } | ||
| else { | ||
| throw new Error(localized(locale, `Unknown fetch option: ${parsed.option}`, `未知 Fetch 选项:${parsed.option}`)); | ||
| } | ||
| } | ||
| if (!isBrowserFetchRequest(request)) { | ||
| throw new Error(localized(locale, 'Invalid or out-of-bounds browser fetch request', '浏览器 Fetch 请求无效或超出限制')); | ||
| } | ||
| return { request, ...(browserSelector ? { browserSelector } : {}) }; | ||
| } | ||
| export function parseFetchAuthorizationArguments(argv, locale = 'en') { | ||
| const authorization = requiredOptionValue(locale, argv, 0); | ||
| if (authorization.option !== '--authorize') { | ||
| throw new Error(localized(locale, 'Fetch authorization requires --authorize', 'Fetch 授权需要 --authorize')); | ||
| } | ||
| const domain = normalizeBrowserFetchDomain(authorization.value); | ||
| if (!domain) { | ||
| throw new Error(localized(locale, 'Fetch authorization requires a hostname, *.domain wildcard, or URL', 'Fetch 授权需要主机名、*.域名通配符或 URL')); | ||
| } | ||
| let browserSelector; | ||
| for (let index = authorization.consumed; index < argv.length;) { | ||
| const parsed = requiredOptionValue(locale, argv, index); | ||
| index += parsed.consumed; | ||
| if (parsed.option !== '--browser') { | ||
| throw new Error(localized(locale, `Unknown fetch authorization option: ${parsed.option}`, `未知 Fetch 授权选项:${parsed.option}`)); | ||
| } | ||
| if (browserSelector) { | ||
| throw new Error(localized(locale, '--browser can only be provided once', '--browser 只能指定一次')); | ||
| } | ||
| browserSelector = parsed.value; | ||
| } | ||
| return { domain, ...(browserSelector ? { browserSelector } : {}) }; | ||
| } | ||
| function selectionEnvironment(environment, selector) { | ||
| return { | ||
| ...(environment ?? process.env), | ||
| ...(selector | ||
| ? { [PANERELAY_BROWSER_ID_ENV]: undefined, [PANERELAY_BROWSER_ENV]: selector } | ||
| : {}), | ||
| }; | ||
| } | ||
| function extractAdapterInvocationOptions(argv, locale) { | ||
| const remaining = []; | ||
| let browserSelector; | ||
| let json = false; | ||
| for (let index = 0; index < argv.length;) { | ||
| const argument = argv[index]; | ||
| if (argument === '--json') { | ||
| if (json) | ||
| throw new Error(localized(locale, '--json can only be provided once', '--json 只能指定一次')); | ||
| json = true; | ||
| index += 1; | ||
| continue; | ||
| } | ||
| if (argument.startsWith('--json=')) { | ||
| throw new Error(localized(locale, '--json does not accept a value', '--json 不接受参数值')); | ||
| } | ||
| const isBrowser = argument === '--browser' || argument.startsWith('--browser='); | ||
| if (!isBrowser) { | ||
| remaining.push(argument); | ||
| index += 1; | ||
| continue; | ||
| } | ||
| const parsed = requiredOptionValue(locale, argv, index); | ||
| if (isBrowser) { | ||
| if (browserSelector) | ||
| throw new Error(localized(locale, '--browser can only be provided once', '--browser 只能指定一次')); | ||
| browserSelector = parsed.value; | ||
| } | ||
| index += parsed.consumed; | ||
| } | ||
| return { | ||
| argv: remaining, | ||
| json, | ||
| ...(browserSelector ? { browserSelector } : {}), | ||
| }; | ||
| } | ||
| function tableCell(value) { | ||
| if (value === null || value === undefined) | ||
| return ''; | ||
| const rendered = typeof value === 'object' ? (JSON.stringify(value) ?? String(value)) : String(value); | ||
| return rendered.replaceAll('\r', '\\r').replaceAll('\n', '\\n'); | ||
| } | ||
| function tableWidth(value) { | ||
| let width = 0; | ||
| for (const character of value) { | ||
| const codePoint = character.codePointAt(0) ?? 0; | ||
| if (codePoint === 0 || | ||
| (codePoint >= 0x0300 && codePoint <= 0x036f) || | ||
| (codePoint >= 0xfe00 && codePoint <= 0xfe0f)) | ||
| continue; | ||
| width += | ||
| codePoint >= 0x1100 && | ||
| (codePoint <= 0x115f || | ||
| codePoint === 0x2329 || | ||
| codePoint === 0x232a || | ||
| (codePoint >= 0x2e80 && codePoint <= 0xa4cf) || | ||
| (codePoint >= 0xac00 && codePoint <= 0xd7a3) || | ||
| (codePoint >= 0xf900 && codePoint <= 0xfaff) || | ||
| (codePoint >= 0xfe10 && codePoint <= 0xfe6f) || | ||
| (codePoint >= 0xff00 && codePoint <= 0xff60) || | ||
| (codePoint >= 0xffe0 && codePoint <= 0xffe6) || | ||
| (codePoint >= 0x1f300 && codePoint <= 0x1faff)) | ||
| ? 2 | ||
| : 1; | ||
| } | ||
| return width; | ||
| } | ||
| function padTableCell(value, width) { | ||
| return `${value}${' '.repeat(Math.max(0, width - tableWidth(value)))}`; | ||
| } | ||
| function capitalize(value) { | ||
| return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value; | ||
| } | ||
| function adapterResultRows(result) { | ||
| if (Array.isArray(result)) { | ||
| return result.map(value => value && typeof value === 'object' && !Array.isArray(value) | ||
| ? value | ||
| : { value }); | ||
| } | ||
| if (result && typeof result === 'object') | ||
| return [result]; | ||
| return [{ value: result }]; | ||
| } | ||
| function formatAdapterTable(result, outputFields, locale, elapsedSeconds) { | ||
| const rows = adapterResultRows(result); | ||
| if (rows.length === 0) | ||
| return localized(locale, '(no data)', '(无数据)'); | ||
| const columns = outputFields.length ? outputFields : Object.keys(rows[0] ?? {}); | ||
| if (columns.length === 0) | ||
| return localized(locale, '(no data)', '(无数据)'); | ||
| const headers = columns.map(capitalize); | ||
| const values = rows.map(resultRow => columns.map(column => tableCell(resultRow[column]))); | ||
| const widths = columns.map((_, columnIndex) => Math.max(tableWidth(headers[columnIndex] ?? ''), ...values.map(resultRow => tableWidth(resultRow[columnIndex] ?? '')))); | ||
| const border = (left, middle, right, fill) => `${left}${widths.map(width => fill.repeat(width + 2)).join(middle)}${right}`; | ||
| const renderRow = (cells) => `│ ${cells.map((cell, index) => padTableCell(cell, widths[index] ?? 0)).join(' │ ')} │`; | ||
| return [ | ||
| '', | ||
| border('┌', '┬', '┐', '─'), | ||
| renderRow(headers), | ||
| border('├', '┼', '┤', '─'), | ||
| ...values.map(renderRow), | ||
| border('└', '┴', '┘', '─'), | ||
| `${rows.length} items · ${elapsedSeconds.toFixed(1)}s`, | ||
| ].join('\n'); | ||
| } | ||
| function parseArgumentValue(locale, definition, value) { | ||
| if (definition.type === 'string' || definition.type === 'file') | ||
| return value; | ||
| if (definition.type === 'number') { | ||
| const number = Number(value); | ||
| if (!Number.isFinite(number)) | ||
| throw new Error(localized(locale, `${definition.name} must be a number`, `${definition.name} 必须是数字`)); | ||
| return number; | ||
| } | ||
| if (value === 'true' || value === '1') | ||
| return true; | ||
| if (value === 'false' || value === '0') | ||
| return false; | ||
| throw new Error(localized(locale, `${definition.name} must be true or false`, `${definition.name} 必须是 true 或 false`)); | ||
| } | ||
| function parseAdapterArguments(locale, command, argv) { | ||
| const result = {}; | ||
| const definitions = new Map(command.args.map(argument => [argument.name, argument])); | ||
| const positional = command.args.filter(argument => argument.positional); | ||
| let positionalIndex = 0; | ||
| for (let index = 0; index < argv.length;) { | ||
| const argument = argv[index]; | ||
| if (!argument.startsWith('--')) { | ||
| const definition = positional[positionalIndex++]; | ||
| if (!definition) | ||
| throw new Error(localized(locale, `Unexpected argument: ${argument}`, `意外参数:${argument}`)); | ||
| result[definition.name] = parseArgumentValue(locale, definition, argument); | ||
| index += 1; | ||
| continue; | ||
| } | ||
| const negated = argument.startsWith('--no-'); | ||
| const separator = argument.indexOf('='); | ||
| const rawOption = separator < 0 ? argument : argument.slice(0, separator); | ||
| const name = negated ? rawOption.slice('--no-'.length) : rawOption.slice(2); | ||
| const definition = definitions.get(name); | ||
| if (!definition || definition.positional) | ||
| throw new Error(localized(locale, `Unknown adapter option: ${rawOption}`, `未知适配器选项:${rawOption}`)); | ||
| if (definition.type === 'boolean' && separator < 0) { | ||
| result[name] = !negated; | ||
| index += 1; | ||
| continue; | ||
| } | ||
| const parsed = optionValue(argv, index); | ||
| if (negated || parsed.value === undefined) | ||
| throw new Error(localized(locale, `${rawOption} requires a value`, `${rawOption} 后需要指定值`)); | ||
| result[name] = parseArgumentValue(locale, definition, parsed.value); | ||
| index += parsed.consumed; | ||
| } | ||
| for (const definition of command.args) { | ||
| if (result[definition.name] !== undefined) | ||
| continue; | ||
| if (definition.default !== undefined) | ||
| result[definition.name] = definition.default; | ||
| else if (definition.required) | ||
| throw new Error(localized(locale, `Missing required argument: ${definition.name}`, `缺少必填参数:${definition.name}`)); | ||
| } | ||
| return result; | ||
| } | ||
| async function selectBrowser(selector, options) { | ||
| return await (options.dependencies?.selectBrowserFetchRegistration ?? selectBrowserFetchRegistration)({ environment: selectionEnvironment(options.environment, selector) }); | ||
| } | ||
| function registryOptions(options) { | ||
| return { environment: options.environment }; | ||
| } | ||
| async function executeAdapter(registration, command, args, artifacts, browserSelector, options) { | ||
| const selection = await selectBrowser(browserSelector, options); | ||
| const active = await (options.dependencies?.createBrowserFetchSession ?? createBrowserFetchSession)(selection.state, { | ||
| allowedOrigins: registration.manifest.origins, | ||
| ...(registration.manifest.bindings ? { bindingPolicies: registration.manifest.bindings } : {}), | ||
| }); | ||
| try { | ||
| return await (options.dependencies?.dispatchFetchAdapter ?? dispatchFetchAdapter)(registration, active, command.name, args, { | ||
| environment: options.environment, | ||
| ...(artifacts ? { artifacts } : {}), | ||
| }); | ||
| } | ||
| finally { | ||
| await (options.dependencies?.releaseBrowserFetchSession ?? releaseBrowserFetchSession)(active).catch(() => undefined); | ||
| } | ||
| } | ||
| export async function runFetchCommand(argv, options) { | ||
| const readRegistry = options.dependencies?.readFetchAdapterRegistry ?? readFetchAdapterRegistry; | ||
| if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') { | ||
| console.log(fetchHelp(options.locale, await readRegistry(registryOptions(options)))); | ||
| return 0; | ||
| } | ||
| if (argv[0] === '--authorize' || argv[0]?.startsWith('--authorize=')) { | ||
| const parsed = parseFetchAuthorizationArguments(argv, options.locale); | ||
| const selection = await selectBrowser(parsed.browserSelector, options); | ||
| const result = await (options.dependencies?.requestBrowserFetchPermission ?? requestBrowserFetchPermission)(selection.state, parsed.domain); | ||
| if (!result.granted) { | ||
| throw new Error(localized(options.locale, `Fetch authorization for "${parsed.domain}" was denied. Ask the user to approve it, then retry: panerelay fetch --authorize ${parsed.domain}`, `域名“${parsed.domain}”的 Fetch 授权被拒绝。请用户确认后重试:panerelay fetch --authorize ${parsed.domain}`)); | ||
| } | ||
| console.log(JSON.stringify(result, null, 2)); | ||
| return 0; | ||
| } | ||
| if (absoluteHttpUrl(argv[0])) { | ||
| if (argv.includes('--help') || argv.includes('-h')) { | ||
| console.log(fetchHelp(options.locale, await readRegistry(registryOptions(options)))); | ||
| return 0; | ||
| } | ||
| const parsed = parseRawFetchArguments(argv, options.locale); | ||
| const selection = await selectBrowser(parsed.browserSelector, options); | ||
| const result = await (options.dependencies?.runBrowserFetch ?? runBrowserFetch)(selection.state, parsed.request); | ||
| console.log(JSON.stringify(result, null, 2)); | ||
| return 0; | ||
| } | ||
| const adapterId = argv[0]; | ||
| const registry = await readRegistry(registryOptions(options)); | ||
| const metadata = registry.adapters.find(adapter => adapter.manifest.id === adapterId); | ||
| if (!metadata) { | ||
| throw new Error(localized(options.locale, `Unknown fetch target: ${adapterId}. Use an absolute URL or an installed site adapter.`, `未知 Fetch 目标:${adapterId}。请使用绝对 URL 或已安装的站点适配器。`)); | ||
| } | ||
| if (argv.length === 1 || argv[1] === '--help' || argv[1] === '-h') { | ||
| console.log(siteHelp(options.locale, metadata.manifest)); | ||
| return 0; | ||
| } | ||
| const commandName = argv[1]; | ||
| const command = metadata.manifest.commands.find(candidate => candidate.name === commandName); | ||
| if (!command) | ||
| throw new Error(localized(options.locale, `Unknown ${adapterId} command: ${commandName}`, `未知 ${adapterId} 命令:${commandName}`)); | ||
| if (argv.slice(2).includes('--help') || argv.slice(2).includes('-h')) { | ||
| console.log(commandHelp(options.locale, metadata.manifest, command)); | ||
| return 0; | ||
| } | ||
| const now = options.dependencies?.now ?? Date.now; | ||
| const startedAt = now(); | ||
| const selectedArguments = extractAdapterInvocationOptions(argv.slice(2), options.locale); | ||
| const args = parseAdapterArguments(options.locale, command, selectedArguments.argv); | ||
| const registration = await (options.dependencies?.readFetchAdapterRegistration ?? readFetchAdapterRegistration)(adapterId, registryOptions(options)); | ||
| if (!registration) | ||
| throw new Error(localized(options.locale, `Fetch adapter is not installed: ${adapterId}`, `Fetch 适配器未安装:${adapterId}`)); | ||
| const prepared = await (options.dependencies?.prepareFetchAdapterArtifacts ?? prepareFetchAdapterArtifacts)(command, args); | ||
| const result = await executeAdapter(registration, command, prepared.args, prepared.artifacts, selectedArguments.browserSelector, options); | ||
| console.log(selectedArguments.json | ||
| ? JSON.stringify(result, null, 2) | ||
| : formatAdapterTable(result, command.output, options.locale, Math.max(0, now() - startedAt) / 1000)); | ||
| return 0; | ||
| } |
+6
-1
@@ -8,3 +8,5 @@ #!/usr/bin/env node | ||
| import { runCliConnectionCommand } from './command-runner.js'; | ||
| export type CliOperation = 'browsers' | 'browser-use' | 'browser-clear' | 'connection-use' | 'connection-resolve' | 'run'; | ||
| import { readFetchAdapterRegistry } from './fetch-adapter-registry.js'; | ||
| import { runFetchCommand } from './fetch-command.js'; | ||
| export type CliOperation = 'browsers' | 'browser-use' | 'browser-clear' | 'connection-use' | 'connection-resolve' | 'fetch' | 'run'; | ||
| export interface ParsedCliArgs { | ||
@@ -15,2 +17,3 @@ browserSelector?: string; | ||
| childCommand?: string[]; | ||
| fetchArguments?: string[]; | ||
| actorName?: string; | ||
@@ -28,2 +31,3 @@ sessionLabel?: string; | ||
| readBrowserDefault?: typeof readBrowserDefault; | ||
| readFetchAdapterRegistry?: typeof readFetchAdapterRegistry; | ||
| selectBrowserRegistration?: typeof selectBrowserRegistration; | ||
@@ -35,2 +39,3 @@ setBrowserDefault?: typeof setBrowserDefault; | ||
| runCliConnectionCommand?: typeof runCliConnectionCommand; | ||
| runFetchCommand?: typeof runFetchCommand; | ||
| systemLocale?: string; | ||
@@ -37,0 +42,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAGL,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,4BAA4B,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,OAAO,EAA6C,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5F,OAAO,EAEL,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,MAAM,MAAM,YAAY,GACtB,UAAU,GAAG,aAAa,GAAG,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,KAAK,CAAC;AAEjG,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAoED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAmI1D;AAMD,MAAM,WAAW,eAAe;IAC9B,mBAAmB,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,wBAAwB,CAAC,EAAE,OAAO,wBAAwB,CAAC;IAC3D,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IAC/C,yBAAyB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IAC7D,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAC7C,oBAAoB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACnD,qBAAqB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACrD,4BAA4B,CAAC,EAAE,OAAO,4BAA4B,CAAC;IACnE,uBAAuB,CAAC,EAAE,OAAO,uBAAuB,CAAC;IACzD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAwDD,wBAAsB,IAAI,CACxB,IAAI,GAAE,MAAM,EAA0B,EACtC,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAuKjB"} | ||
| {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAGL,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,4BAA4B,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,OAAO,EAA6C,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5F,OAAO,EAEL,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAIrD,MAAM,MAAM,YAAY,GACpB,UAAU,GACV,aAAa,GACb,eAAe,GACf,gBAAgB,GAChB,oBAAoB,GACpB,OAAO,GACP,KAAK,CAAC;AAEV,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAwID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAwI1D;AAMD,MAAM,WAAW,eAAe;IAC9B,mBAAmB,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,wBAAwB,CAAC,EAAE,OAAO,wBAAwB,CAAC;IAC3D,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IAC/C,wBAAwB,CAAC,EAAE,OAAO,wBAAwB,CAAC;IAC3D,yBAAyB,CAAC,EAAE,OAAO,yBAAyB,CAAC;IAC7D,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAC7C,oBAAoB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACnD,qBAAqB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACrD,4BAA4B,CAAC,EAAE,OAAO,4BAA4B,CAAC;IACnE,uBAAuB,CAAC,EAAE,OAAO,uBAAuB,CAAC;IACzD,eAAe,CAAC,EAAE,OAAO,eAAe,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAwDD,wBAAsB,IAAI,CACxB,IAAI,GAAE,MAAM,EAA0B,EACtC,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAwLjB"} |
+113
-11
@@ -11,2 +11,5 @@ #!/usr/bin/env node | ||
| import { runCliConnectionCommand } from './command-runner.js'; | ||
| import { readFetchAdapterRegistry } from './fetch-adapter-registry.js'; | ||
| import { runFetchCommand } from './fetch-command.js'; | ||
| const BUILT_IN_COMMANDS = new Set(['browsers', 'browser', 'connection', 'fetch', 'run']); | ||
| function topLevelArguments(argv) { | ||
@@ -18,4 +21,46 @@ const separator = argv.indexOf('--'); | ||
| const commandArguments = topLevelArguments(argv); | ||
| return commandArguments.includes('--version') || commandArguments.includes('-v'); | ||
| const meaningful = []; | ||
| for (let index = 0; index < commandArguments.length; index += 1) { | ||
| const argument = commandArguments[index]; | ||
| if (argument === '--lang') { | ||
| index += 1; | ||
| continue; | ||
| } | ||
| if (argument?.startsWith('--lang=')) | ||
| continue; | ||
| if (argument) | ||
| meaningful.push(argument); | ||
| } | ||
| return meaningful.length === 1 && ['--version', '-v'].includes(meaningful[0]); | ||
| } | ||
| function directSiteCandidateIndex(argv) { | ||
| for (let index = 0; index < argv.length; index += 1) { | ||
| const argument = argv[index]; | ||
| if (!argument || argument === '--' || argument.startsWith('-')) { | ||
| if (argument === '--lang') { | ||
| index += 1; | ||
| continue; | ||
| } | ||
| if (argument?.startsWith('--lang=')) | ||
| continue; | ||
| return undefined; | ||
| } | ||
| return index; | ||
| } | ||
| return undefined; | ||
| } | ||
| async function resolveDirectSiteAlias(argv, dependencies) { | ||
| const candidateIndex = directSiteCandidateIndex(argv); | ||
| if (candidateIndex === undefined) | ||
| return argv; | ||
| const candidate = argv[candidateIndex]; | ||
| if (BUILT_IN_COMMANDS.has(candidate) || /^https?:\/\//i.test(candidate)) | ||
| return argv; | ||
| const registry = await (dependencies.readFetchAdapterRegistry ?? readFetchAdapterRegistry)({ | ||
| environment: dependencies.environment, | ||
| }); | ||
| if (!registry.adapters.some(adapter => adapter.manifest.id === candidate)) | ||
| return argv; | ||
| return [...argv.slice(0, candidateIndex), 'fetch', ...argv.slice(candidateIndex)]; | ||
| } | ||
| async function packageVersion() { | ||
@@ -33,3 +78,29 @@ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); | ||
| } | ||
| function fetchAdapterCommandOptionStart(argv) { | ||
| const fetchIndex = argv.indexOf('fetch'); | ||
| if (fetchIndex < 0) | ||
| return undefined; | ||
| const operands = []; | ||
| for (let index = fetchIndex + 1; index < argv.length; index += 1) { | ||
| const argument = argv[index]; | ||
| if (!argument) | ||
| continue; | ||
| if (argument === '--lang') { | ||
| index += 1; | ||
| continue; | ||
| } | ||
| if (argument.startsWith('--lang=')) | ||
| continue; | ||
| if (argument.startsWith('-')) | ||
| return undefined; | ||
| operands.push(argument); | ||
| if (operands.length === 1 && /^https?:\/\//i.test(argument)) | ||
| return undefined; | ||
| if (operands.length === 2) | ||
| return index + 1; | ||
| } | ||
| return undefined; | ||
| } | ||
| function languageValue(argv) { | ||
| const adapterOptionsStart = fetchAdapterCommandOptionStart(argv); | ||
| for (let index = 0; index < argv.length; index += 1) { | ||
@@ -41,2 +112,4 @@ const argument = argv[index]; | ||
| break; | ||
| if (adapterOptionsStart !== undefined && index >= adapterOptionsStart) | ||
| continue; | ||
| if (argument === '--lang') | ||
@@ -52,2 +125,3 @@ return argv[index + 1]; | ||
| let rawLanguage; | ||
| const adapterOptionsStart = fetchAdapterCommandOptionStart(argv); | ||
| for (let index = 0; index < argv.length; index += 1) { | ||
@@ -61,2 +135,6 @@ const argument = argv[index]; | ||
| } | ||
| if (adapterOptionsStart !== undefined && index >= adapterOptionsStart) { | ||
| remaining.push(argument); | ||
| continue; | ||
| } | ||
| if (argument !== '--lang' && !argument.startsWith('--lang=')) { | ||
@@ -84,5 +162,6 @@ remaining.push(argument); | ||
| const commandArguments = topLevelArguments(localized.argv); | ||
| const command = localized.argv[0]; | ||
| if (localized.argv.length === 0 || | ||
| commandArguments.includes('--help') || | ||
| commandArguments.includes('-h')) { | ||
| (command !== 'fetch' && | ||
| (commandArguments.includes('--help') || commandArguments.includes('-h')))) { | ||
| return { | ||
@@ -93,3 +172,3 @@ help: true, | ||
| } | ||
| const command = localized.argv[0]; | ||
| const selectedCommand = command; | ||
| let browserSelector; | ||
@@ -103,7 +182,7 @@ let adapterId; | ||
| let optionStart; | ||
| if (command === 'browsers') { | ||
| if (selectedCommand === 'browsers') { | ||
| operation = 'browsers'; | ||
| optionStart = 1; | ||
| } | ||
| else if (command === 'browser') { | ||
| else if (selectedCommand === 'browser') { | ||
| const action = localized.argv[1]; | ||
@@ -126,3 +205,3 @@ if (action === 'use') { | ||
| } | ||
| else if (command === 'connection') { | ||
| else if (selectedCommand === 'connection') { | ||
| const action = localized.argv[1]; | ||
@@ -175,3 +254,7 @@ if (action !== 'use' && action !== 'resolve') { | ||
| } | ||
| else if (command === 'run') { | ||
| else if (selectedCommand === 'fetch') { | ||
| operation = 'fetch'; | ||
| optionStart = localized.argv.length; | ||
| } | ||
| else if (selectedCommand === 'run') { | ||
| adapterId = localized.argv[1]; | ||
@@ -214,3 +297,3 @@ if (!adapterId || adapterId.startsWith('-')) | ||
| else { | ||
| throw new Error(`Unknown command: ${command}`); | ||
| throw new Error(`Unknown command: ${selectedCommand}`); | ||
| } | ||
@@ -225,2 +308,3 @@ for (let index = optionStart; index < localized.argv.length; index += 1) { | ||
| ...(childCommand ? { childCommand } : {}), | ||
| ...(operation === 'fetch' ? { fetchArguments: localized.argv.slice(1) } : {}), | ||
| ...(actorName ? { actorName } : {}), | ||
@@ -301,8 +385,20 @@ ...(sessionLabel ? { sessionLabel } : {}), | ||
| environment: dependencies.environment, | ||
| requestedLocale: languageValue(argv), | ||
| systemLocale: dependencies.systemLocale, | ||
| }); | ||
| let routedArgv; | ||
| try { | ||
| routedArgv = await resolveDirectSiteAlias(argv, dependencies); | ||
| } | ||
| catch (error) { | ||
| console.error(localizeRuntimeError(error, locale)); | ||
| return 1; | ||
| } | ||
| locale = resolveLocale({ | ||
| environment: dependencies.environment, | ||
| requestedLocale: languageValue(routedArgv), | ||
| systemLocale: dependencies.systemLocale, | ||
| }); | ||
| let parsed; | ||
| try { | ||
| parsed = parseCliArgs(argv); | ||
| parsed = parseCliArgs(routedArgv); | ||
| } | ||
@@ -321,2 +417,8 @@ catch (error) { | ||
| const registryOptions = { environment: dependencies.environment }; | ||
| if (parsed.operation === 'fetch') { | ||
| return await (dependencies.runFetchCommand ?? runFetchCommand)(parsed.fetchArguments ?? [], { | ||
| environment: dependencies.environment, | ||
| locale, | ||
| }); | ||
| } | ||
| if (parsed.operation === 'browsers') { | ||
@@ -323,0 +425,0 @@ const registrations = await (dependencies.listBrowserRegistrations ?? listBrowserRegistrations)(registryOptions); |
+1
-1
@@ -31,3 +31,3 @@ export type SupportedLocale = 'en' | 'zh-CN'; | ||
| readonly errorUnknownOption: "Unknown option: {option}"; | ||
| readonly help: "Panerelay CLI\n\nUsage:\n panerelay browsers [--lang <language>]\n panerelay browser use <registration-id|family> [--lang <language>]\n panerelay browser clear [--lang <language>]\n panerelay connection use <adapter-id> <direct|extension> [--lang <language>]\n panerelay connection resolve <adapter-id> [--mode <mode>] [--browser <selector>]\n panerelay run <adapter-id> [--mode <mode>] [--browser <selector>] -- <command> [arguments...]\n\nCommands:\n browsers List live Panerelay browser registrations\n browser use Save one live registration as the routing default\n browser clear\n Clear the saved browser default\n connection use\n Save a connection mode for one installed adapter\n connection resolve\n Resolve connection material without running an automation command\n run Run the caller's command with the selected connection\n\nOptions:\n --lang Use en or zh-CN instead of the system language\n --version, -v\n Show the version\n --help, -h Show this help\n\nOccasional use without a global installation:\n npx --yes @panerelay/cli browsers"; | ||
| readonly help: "Panerelay CLI\n\nUsage:\n panerelay browsers [--lang <language>]\n panerelay browser use <registration-id|family> [--lang <language>]\n panerelay browser clear [--lang <language>]\n panerelay connection use <adapter-id> <direct|extension> [--lang <language>]\n panerelay connection resolve <adapter-id> [--mode <mode>] [--browser <selector>]\n panerelay <site> <command> [site arguments]\n panerelay fetch <url|site> [request or site arguments]\n panerelay run <adapter-id> [--mode <mode>] [--browser <selector>] -- <command> [arguments...]\n\nCommands:\n browsers List live Panerelay browser registrations\n browser use Save one live registration as the routing default\n browser clear\n Clear the saved browser default\n connection use\n Save a connection mode for one installed adapter\n connection resolve\n Resolve connection material without running an automation command\n fetch Send a browser-backed request or explicitly run a site adapter\n run Run the caller's command with the selected connection\n\nOptions:\n --lang Use en or zh-CN instead of the system language\n --version, -v\n Show the version\n --help, -h Show this help\n\nOccasional use without a global installation:\n npx --yes @panerelay/cli browsers\n\nInstalled site adapters can run directly; use fetch for raw URLs or disambiguation."; | ||
| }; | ||
@@ -34,0 +34,0 @@ type MessageKey = keyof typeof englishMessages; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,OAAO,CAAC;AAE7C,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DX,CAAC;AAEX,KAAK,UAAU,GAAG,MAAM,OAAO,eAAe,CAAC;AA8D/C,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAMtF;AAyBD,wBAAgB,aAAa,CAAC,OAAO,GAAE,uBAA4B,GAAG,eAAe,CAWpF;AAED,wBAAgB,SAAS,CACvB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,UAAU,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAGR"} | ||
| {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,OAAO,CAAC;AAE7C,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+DX,CAAC;AAEX,KAAK,UAAU,GAAG,MAAM,OAAO,eAAe,CAAC;AAmE/C,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAMtF;AAyBD,wBAAgB,aAAa,CAAC,OAAO,GAAE,uBAA4B,GAAG,eAAe,CAWpF;AAED,wBAAgB,SAAS,CACvB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,UAAU,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAGR"} |
+12
-2
@@ -39,2 +39,4 @@ import { execFileSync } from 'node:child_process'; | ||
| panerelay connection resolve <adapter-id> [--mode <mode>] [--browser <selector>] | ||
| panerelay <site> <command> [site arguments] | ||
| panerelay fetch <url|site> [request or site arguments] | ||
| panerelay run <adapter-id> [--mode <mode>] [--browser <selector>] -- <command> [arguments...] | ||
@@ -51,2 +53,3 @@ | ||
| Resolve connection material without running an automation command | ||
| fetch Send a browser-backed request or explicitly run a site adapter | ||
| run Run the caller's command with the selected connection | ||
@@ -61,3 +64,5 @@ | ||
| Occasional use without a global installation: | ||
| npx --yes @panerelay/cli browsers`, | ||
| npx --yes @panerelay/cli browsers | ||
| Installed site adapters can run directly; use fetch for raw URLs or disambiguation.`, | ||
| }; | ||
@@ -101,2 +106,4 @@ const chineseMessages = { | ||
| panerelay connection resolve <adapter ID> [--mode <模式>] [--browser <选择器>] | ||
| panerelay <站点> <命令> [站点参数] | ||
| panerelay fetch <URL|站点> [请求或站点参数] | ||
| panerelay run <adapter ID> [--mode <模式>] [--browser <选择器>] -- <命令> [参数...] | ||
@@ -113,2 +120,3 @@ | ||
| 解析连接信息,但不运行自动化命令 | ||
| fetch 发送浏览器请求或显式运行站点适配器 | ||
| run 使用所选连接运行调用者指定的命令 | ||
@@ -123,3 +131,5 @@ | ||
| 无需全局安装的临时用法: | ||
| npx --yes @panerelay/cli browsers`, | ||
| npx --yes @panerelay/cli browsers | ||
| 已安装的站点适配器可直接运行;原始 URL 请求或命令消歧请使用 fetch。`, | ||
| }; | ||
@@ -126,0 +136,0 @@ export function normalizeLocale(value) { |
+10
-0
@@ -7,2 +7,4 @@ export { main, parseCliArgs } from './cli.js'; | ||
| export { runCliConnectionCommand } from './command-runner.js'; | ||
| export { createBrowserFetchSession, requestBrowserFetchPermission, releaseBrowserFetchSession, runBrowserFetch, runBrowserFetchInSession, } from './browser-fetch-client.js'; | ||
| export type { ActiveBrowserFetchSession, BrowserFetchClientOptions, BrowserFetchHttpClient, } from './browser-fetch-client.js'; | ||
| export type { CliCommandRunnerDependencies, CliCommandRunnerOptions, RunCliConnectionInput, } from './command-runner.js'; | ||
@@ -16,2 +18,10 @@ export { PANERELAY_CLI_ADAPTER_LOCK_DIRECTORY_ENV, acquireCliConcurrencyLock, } from './concurrency-lock.js'; | ||
| export type { LocaleResolutionOptions, SupportedLocale } from './i18n.js'; | ||
| export { PANERELAY_FETCH_ADAPTER_REGISTRY_PATH_ENV, fetchAdapterDataDirectory, fetchAdapterRegistryPath, readFetchAdapterRegistration, readFetchAdapterRegistry, } from './fetch-adapter-registry.js'; | ||
| export type { FetchAdapterRegistryOptions } from './fetch-adapter-registry.js'; | ||
| export { dispatchFetchAdapter, FetchAdapterCommandError } from './fetch-adapter-dispatcher.js'; | ||
| export type { FetchAdapterDispatchOptions } from './fetch-adapter-dispatcher.js'; | ||
| export { prepareFetchAdapterArtifacts } from './fetch-adapter-artifact.js'; | ||
| export type { PreparedFetchAdapterArtifacts } from './fetch-adapter-artifact.js'; | ||
| export { parseFetchAuthorizationArguments, parseRawFetchArguments, runFetchCommand, } from './fetch-command.js'; | ||
| export type { FetchCommandDependencies, FetchCommandOptions, ParsedRawFetch, ParsedFetchAuthorization, } from './fetch-command.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,EACL,0CAA0C,EAC1C,yCAAyC,EACzC,yBAAyB,EACzB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACnG,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EACV,4BAA4B,EAC5B,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wCAAwC,EACxC,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,YAAY,EACV,2BAA2B,EAC3B,iCAAiC,EACjC,4BAA4B,EAC5B,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uCAAuC,EACvC,sCAAsC,EACtC,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtE,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,EACL,0CAA0C,EAC1C,yCAAyC,EACzC,yBAAyB,EACzB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACnG,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,0BAA0B,EAC1B,eAAe,EACf,wBAAwB,GACzB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,yBAAyB,EACzB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,4BAA4B,EAC5B,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wCAAwC,EACxC,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,YAAY,EACV,2BAA2B,EAC3B,iCAAiC,EACjC,4BAA4B,EAC5B,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,uCAAuC,EACvC,sCAAsC,EACtC,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtE,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EACL,yCAAyC,EACzC,yBAAyB,EACzB,wBAAwB,EACxB,4BAA4B,EAC5B,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC/F,YAAY,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAC3E,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,EACL,gCAAgC,EAChC,sBAAsB,EACtB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EACd,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"} |
+5
-0
@@ -5,4 +5,9 @@ export { main, parseCliArgs } from './cli.js'; | ||
| export { runCliConnectionCommand } from './command-runner.js'; | ||
| export { createBrowserFetchSession, requestBrowserFetchPermission, releaseBrowserFetchSession, runBrowserFetch, runBrowserFetchInSession, } from './browser-fetch-client.js'; | ||
| export { PANERELAY_CLI_ADAPTER_LOCK_DIRECTORY_ENV, acquireCliConcurrencyLock, } from './concurrency-lock.js'; | ||
| export { PANERELAY_CLI_ADAPTER_REGISTRY_PATH_ENV, PANERELAY_CLI_ADAPTER_REGISTRY_VERSION, cliAdapterDataDirectory, cliAdapterRegistryPath, isCliAdapterRegistration, readCliAdapterRegistration, readCliAdapterRegistry, registerCliAdapter, removeCliAdapterRegistration, } from './adapter-registry.js'; | ||
| export { normalizeLocale, resolveLocale, translate } from './i18n.js'; | ||
| export { PANERELAY_FETCH_ADAPTER_REGISTRY_PATH_ENV, fetchAdapterDataDirectory, fetchAdapterRegistryPath, readFetchAdapterRegistration, readFetchAdapterRegistry, } from './fetch-adapter-registry.js'; | ||
| export { dispatchFetchAdapter, FetchAdapterCommandError } from './fetch-adapter-dispatcher.js'; | ||
| export { prepareFetchAdapterArtifacts } from './fetch-adapter-artifact.js'; | ||
| export { parseFetchAuthorizationArguments, parseRawFetchArguments, runFetchCommand, } from './fetch-command.js'; |
+10
-5
| { | ||
| "name": "@panerelay/cli", | ||
| "version": "0.8.0", | ||
| "version": "0.9.0", | ||
| "description": "Engine-neutral browser administration CLI for Panerelay.", | ||
@@ -30,2 +30,6 @@ "type": "module", | ||
| "import": "./dist/adapter-config.js" | ||
| }, | ||
| "./browser-fetch-client": { | ||
| "types": "./dist/browser-fetch-client.d.ts", | ||
| "import": "./dist/browser-fetch-client.js" | ||
| } | ||
@@ -43,5 +47,5 @@ }, | ||
| "dependencies": { | ||
| "@panerelay/browser-use": "0.8.0", | ||
| "@panerelay/browser-registry": "0.8.0", | ||
| "@panerelay/protocol": "0.8.0" | ||
| "@panerelay/browser-registry": "0.9.0", | ||
| "@panerelay/browser-use": "0.9.0", | ||
| "@panerelay/protocol": "0.9.0" | ||
| }, | ||
@@ -51,4 +55,5 @@ "scripts": { | ||
| "typecheck": "tsc -p tsconfig.json --noEmit", | ||
| "test": "pnpm run build && node ../../scripts/run-compiled-tests.mjs dist" | ||
| "test": "pnpm run build && pnpm run test:compiled", | ||
| "test:compiled": "node ../../scripts/run-compiled-tests.mjs dist" | ||
| } | ||
| } |
+34
-0
@@ -20,2 +20,36 @@ # `@panerelay/cli` | ||
| ## Browser-backed fetch | ||
| The persistent CLI can issue Fetch-shaped requests through one selected live Panerelay Extension: | ||
| ```bash | ||
| panerelay fetch https://api.example.com/me \ | ||
| -H 'Origin: https://www.example.com' \ | ||
| -H 'Referer: https://www.example.com/' \ | ||
| --response json | ||
| ``` | ||
| Supported raw options are `--method`, repeatable `--header/-H`, repeatable `--query`, `--data`, `--data-base64`, `--response`, `--timeout`, `--cookies`, `--no-cookies`, and `--browser`. The output is a structured JSON object containing HTTP status, headers, decoded body type, final URL, redirect state, and attached-cookie count. Browser cookies are included by default but never printed or passed through the Bridge protocol as header material. Redirects always fail closed. | ||
| `Origin` and `Referer` are customizable. When omitted they default to the target origin and `<origin>/panerelay`; an explicit empty value removes the generated header. The target still needs a Panerelay domain grant and Chrome site access. Raw CLI execution does not open permission UI automatically, so a missing grant or Chrome rejection returns bounded guidance to authorize and retry. | ||
| Fetch adapters installed by `@panerelay/setup` use an OpenCLI-like form: | ||
| ```bash | ||
| panerelay fetch --help | ||
| panerelay fetch bilibili --help | ||
| panerelay fetch bilibili me | ||
| panerelay fetch bilibili me --json | ||
| panerelay fetch bilibili subtitle BV1xx411c7mD --lang zh-CN | ||
| panerelay fetch bilibili comment BV1xx411c7mD 'test' --execute | ||
| ``` | ||
| Help reads only protected manifest metadata and does not select a browser. Adapter results use an OpenCLI-style table with an item-count and one-decimal elapsed-time footer by default; like OpenCLI, timing starts when the concrete command action begins and stops before rendering. `--json` prints only the underlying structured result for scripts. Execution verifies the registered digest and runs the adapter once in a bounded child process with a short-lived fetch-only token. Raw requests receive one exact-origin session; adapters receive only the origin and protected binding authority declared in their installed manifest. The Bridge and Extension enforce that authority independently. Fetch still creates no tab-control lease. | ||
| The built-in Bilibili command inventory is `whoami`, `me`, `video`, `search`, `hot`, `ranking`, `dynamic`, `feed`, `feed-detail`, `favorite`, `history`, `following`, `user-videos`, `comments`, `subtitle`, `summary`, `comment`, `follow`, and `unfollow`. The final three are writes. Comment requires `--execute`; each write selects a protected manifest binding that asks the Extension to inject `bili_jct` as the `csrf` form field without disclosing its value to the adapter. Interactive `login` and media/filesystem-oriented `download` are excluded. | ||
| The installed Native Host also supports a bounded stdio MCP mode used by Panerelay's Codex/Claude integrations. It exposes one `browser_fetch` tool with the same exact-origin, permission, Cookie, timeout, redirect, response, cancellation, and non-disclosure boundaries. It is a request tool rather than browser automation or a search engine. Arbitrary Cookie names and browser storage keys are not MCP inputs. | ||
| Once `<site> <command>` has been parsed, manifest-declared `--lang` remains a command argument. A global CLI locale must appear before `fetch`, such as `panerelay --lang zh-CN fetch bilibili --help`. | ||
| The CLI lists live Panerelay browser registrations and manages the saved routing default. It does not install or control an automation engine, grant site access, authorize tabs, or change control leases. Browser automation remains the responsibility of integrations such as agent-browser or browser-use. | ||
@@ -22,0 +56,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
157621
71.8%45
50%2752
70.83%72
89.47%23
64.29%11
175%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated
Updated