diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdbc43835701..fb040c427bbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1348,6 +1348,7 @@ jobs: ;; test-types) pnpm check:test-types + pnpm tsgo:scripts ;; *) echo "Unsupported check task: $TASK" >&2 diff --git a/package.json b/package.json index 81a5380d3975..dff5b23d3833 100644 --- a/package.json +++ b/package.json @@ -1962,6 +1962,7 @@ "tsgo:extensions:test": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo", "tsgo:prod": "pnpm tsgo:core && pnpm tsgo:extensions", "tsgo:profile": "node scripts/profile-tsgo.mjs", + "tsgo:scripts": "node scripts/run-tsgo.mjs -p tsconfig.scripts.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/scripts.tsbuildinfo", "tsgo:test": "pnpm tsgo:core:test && pnpm tsgo:extensions:test", "tsgo:test:extensions": "pnpm tsgo:extensions:test", "tsgo:test:packages": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.packages.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-packages.tsbuildinfo", diff --git a/scripts/android-app-i18n.ts b/scripts/android-app-i18n.ts index 1c25912bdbb0..f72c5e0a44c2 100644 --- a/scripts/android-app-i18n.ts +++ b/scripts/android-app-i18n.ts @@ -65,7 +65,7 @@ export async function checkAndroidAppI18n() { ]); const [base, ...translations] = localeStrings; const baseKeys = new Set(base.keys()); - const problems = translations.flatMap((strings, index) => { + const problems: Array = translations.flatMap((strings, index) => { const locale = NATIVE_I18N_LOCALES[index]; const keys = new Set(strings.keys()); const placeholderMismatches = [...base].flatMap(([key, sourceValue]) => { diff --git a/scripts/anthropic-prompt-probe.ts b/scripts/anthropic-prompt-probe.ts index 2642d314a56b..3482f4b760b7 100644 --- a/scripts/anthropic-prompt-probe.ts +++ b/scripts/anthropic-prompt-probe.ts @@ -212,17 +212,16 @@ function listSetupTokenProfiles( store: { profiles: Record }, normalizeProviderId: (provider: string) => string, ): Array<{ id: string; token: string }> { - return Object.entries(store.profiles) - .filter(([, cred]) => { - if (cred.type !== "token") { - return false; - } - if (normalizeProviderId(cred.provider) !== "anthropic") { - return false; - } - return isSetupToken(cred.token ?? ""); - }) - .map(([id, cred]) => ({ id, token: cred.token ?? "" })); + return Object.entries(store.profiles).flatMap(([id, cred]) => { + if ( + cred.type !== "token" || + normalizeProviderId(cred.provider) !== "anthropic" || + !isSetupToken(cred.token ?? "") + ) { + return []; + } + return [{ id, token: cred.token ?? "" }]; + }); } function pickSetupTokenProfile(candidates: Array<{ id: string; token: string }>): { @@ -390,15 +389,16 @@ async function startAnthropicProxy(params: { port: number; upstreamBaseUrl: stri } headers.set(key, Array.isArray(value) ? value.join(", ") : value); } - const upstreamRes = await fetch(upstreamUrl, { + const upstreamInit = { method, headers, body: method === "GET" || method === "HEAD" || requestBody.byteLength === 0 ? undefined - : requestBody, + : Uint8Array.from(requestBody), duplex: "half", - }); + } as RequestInit & { duplex: "half" }; + const upstreamRes = await fetch(upstreamUrl, upstreamInit); const responseHeaders: Record = {}; for (const [key, value] of upstreamRes.headers.entries()) { const lower = key.toLowerCase(); @@ -926,19 +926,21 @@ async function runGatewayPrompt(prompt: string): Promise { mode: "cli", }); const text = extractPayloadText(waitRes); + const waitStatus = typeof waitRes.status === "string" ? waitRes.status : undefined; + const waitError = typeof waitRes.error === "string" ? waitRes.error : undefined; const logTail = await readLogTail(logPath); - const matched400 = matchesExtraUsage400(waitRes.error, logTail, JSON.stringify(waitRes)); + const matched400 = matchesExtraUsage400(waitError, logTail, JSON.stringify(waitRes)); return { prompt, - ok: waitRes.status === "ok" && !matched400, + ok: waitStatus === "ok" && !matched400, transport: "gateway", promptMode: GATEWAY_PROMPT_MODE, - status: waitRes.status, + status: waitStatus, text: text || undefined, error: - waitRes.status === "ok" + waitStatus === "ok" ? undefined - : redactForDevToolLog(waitRes.error || logTail || "agent.wait failed"), + : redactForDevToolLog(waitError || logTail || "agent.wait failed"), matchedExtraUsage400: matched400, capture: summarizeCapture(proxy?.getLastCapture(), prompt), ...promptProbeTmpResult(tmpDir), diff --git a/scripts/apple-app-i18n.ts b/scripts/apple-app-i18n.ts index 06871a7204e7..87f4b98e77e0 100644 --- a/scripts/apple-app-i18n.ts +++ b/scripts/apple-app-i18n.ts @@ -24,7 +24,12 @@ const LOCALIZED_WRAPPER_CONTRACTS: Record = { ], }; -const CATALOGS = [ +type AppleCatalogSpec = { + path: string; + coverage: Record; +}; + +const CATALOGS: readonly AppleCatalogSpec[] = [ { path: "apps/ios/Resources/Localizable.xcstrings", coverage: { @@ -99,7 +104,7 @@ const CATALOGS = [ "apps/macos/Sources/OpenClaw/CronSettings+Rows.swift": ["Run now"], }, }, -] as const; +]; type Catalog = { sourceLanguage?: string; diff --git a/scripts/bench-model.ts b/scripts/bench-model.ts index 406809e31e47..a1d79e2cdf10 100644 --- a/scripts/bench-model.ts +++ b/scripts/bench-model.ts @@ -182,6 +182,7 @@ async function main(argv = process.argv.slice(2)): Promise { name: "Claude Opus 4.6", api: "anthropic-messages", provider: "anthropic", + baseUrl: "https://api.anthropic.com", reasoning: true, input: ["text", "image"], cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, diff --git a/scripts/bench-sqlite-state.ts b/scripts/bench-sqlite-state.ts index 4f2a51b5aff6..45f09a975d4e 100644 --- a/scripts/bench-sqlite-state.ts +++ b/scripts/bench-sqlite-state.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { DatabaseSync } from "node:sqlite"; +import type { DatabaseSync, SQLInputValue } from "node:sqlite"; import { pathToFileURL } from "node:url"; import { openOpenClawAgentDatabase, @@ -465,7 +465,7 @@ function percentile(values: number[], pct: number): number { function runTimedQuery( db: DatabaseSync, query: string, - params: unknown[], + params: SQLInputValue[], runs: number, ): TimedQuery { const statement = db.prepare(query); diff --git a/scripts/bench-web-fetch.ts b/scripts/bench-web-fetch.ts index 185ce0b401ab..e8e791631d9a 100644 --- a/scripts/bench-web-fetch.ts +++ b/scripts/bench-web-fetch.ts @@ -114,13 +114,12 @@ const TEXT_BODY = "OpenClaw web_fetch direct text benchmark body.".repeat(160); const MARKDOWN_BODY = "# Web Fetch Benchmark\n\n" + "- markdown list item\n".repeat(220); const OFFLINE_PROVIDER_ENV_VARS = ["FIRECRAWL_API_KEY"] as const; -const lookupFn: LookupFn = async () => [{ address: "93.184.216.34", family: 4 }]; +const lookupFn = (async () => [{ address: "93.184.216.34", family: 4 }]) as unknown as LookupFn; const toolConfig: OpenClawConfig = { tools: { web: { fetch: { cacheTtlMinutes: 0, - firecrawl: { enabled: false }, }, }, }, @@ -270,7 +269,7 @@ function installMockFetch(params: { body: string; contentType: string }) { headers: { "content-type": params.contentType, }, - })) as typeof globalThis.fetch & { mock: object }; + })) as unknown as typeof globalThis.fetch & { mock: object }; // fetchWithSsrFGuard preserves dispatcher support unless global fetch is a // test double. The marker keeps this benchmark offline and deterministic. fetchImpl.mock = {}; diff --git a/scripts/changed-lanes.mjs b/scripts/changed-lanes.mjs index f4f2a09b3b10..f0697d84326b 100644 --- a/scripts/changed-lanes.mjs +++ b/scripts/changed-lanes.mjs @@ -13,6 +13,8 @@ const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u; const APP_PATH_RE = /^(?:apps\/|Swabble\/|appcast\.xml$)/u; const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u; const CORE_PATH_RE = /^(?:src\/|ui\/|packages\/)/u; +const SCRIPTS_TYPECHECK_PATH_RE = + /^(?:scripts\/.*\.(?:[cm]?ts|[cm]?tsx)|tsconfig\.scripts\.json)$/u; const TOOLING_PATH_RE = /^(?:scripts\/|test\/vitest\/|\.github\/|\.vscode\/|config\/|deploy\/|git-hooks\/|Dockerfile\.sandbox(?:-(?:browser|common))?$|Makefile$|docker-setup\.sh$|setup-podman\.sh$|openclaw\.podman\.env$|skills\/pyproject\.toml$|vitest(?:\..+)?\.config\.ts$|tsconfig.*\.json$|\.dockerignore$|\.gitignore$|\.jscpd\.json$|\.npmignore$|\.pre-commit-config\.yaml$|\.swiftformat$|\.swiftlint\.yml$|\.oxlint.*|\.oxfmt.*)/u; const ROOT_GLOBAL_PATH_RE = @@ -54,7 +56,7 @@ export const RELEASE_METADATA_PATHS = new Set([ "package.json", ]); -/** @typedef {"core" | "coreTests" | "extensions" | "extensionTests" | "apps" | "docs" | "tooling" | "liveDockerTooling" | "releaseMetadata" | "all"} ChangedLane */ +/** @typedef {"core" | "coreTests" | "extensions" | "extensionTests" | "scripts" | "apps" | "docs" | "tooling" | "liveDockerTooling" | "releaseMetadata" | "all"} ChangedLane */ /** * @typedef {{ @@ -85,6 +87,7 @@ export function createEmptyChangedLanes() { coreTests: false, extensions: false, extensionTests: false, + scripts: false, apps: false, docs: false, tooling: false, @@ -139,6 +142,10 @@ export function detectChangedLanes(changedPaths, options = {}) { } for (const changedPath of paths) { + if (SCRIPTS_TYPECHECK_PATH_RE.test(changedPath)) { + lanes.scripts = true; + } + if (DOCS_PATH_RE.test(changedPath)) { lanes.docs = true; continue; diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index 35c44d3f1077..73f46fc8c318 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -391,6 +391,9 @@ export function createChangedCheckPlan(result, options = {}) { if (lanes.extensionTests) { addTypecheck("typecheck extension tests", ["tsgo:extensions:test"]); } + if (lanes.scripts) { + addTypecheck("typecheck scripts", ["tsgo:scripts"]); + } if (lanes.core || lanes.coreTests) { const coreLintCommand = createTargetedCoreLintCommand(result.paths, baseEnv); diff --git a/scripts/check-cli-bootstrap-imports.d.mts b/scripts/check-cli-bootstrap-imports.d.mts new file mode 100644 index 000000000000..41998e6d8efb --- /dev/null +++ b/scripts/check-cli-bootstrap-imports.d.mts @@ -0,0 +1,15 @@ +import type fs from "node:fs"; + +type CliBootstrapCheckParams = { + rootDir?: string; + entrypoints?: string[]; + distDir?: string; + gatewayRunChunkMaxBytes?: number; + fs?: typeof fs; + logger?: { error(message: string): void }; +}; + +export function listStaticImportSpecifiers(source: string): string[]; +export function collectCliBootstrapExternalImportErrors(params?: CliBootstrapCheckParams): string[]; +export function collectGatewayRunChunkBudgetErrors(params?: CliBootstrapCheckParams): string[]; +export function checkCliBootstrapExternalImports(params?: CliBootstrapCheckParams): void; diff --git a/scripts/check.mjs b/scripts/check.mjs index cc46e55d1239..9a7fd4f1eea7 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -114,12 +114,12 @@ export async function main(argv = process.argv.slice(2)) { { name: "typecheck", parallel: false, - commands: [ - { - name: args.includeTestTypes ? "typecheck all" : "typecheck prod", - args: [args.includeTestTypes ? "tsgo:all" : "tsgo:prod"], - }, - ], + commands: args.includeTestTypes + ? [{ name: "typecheck all", args: ["tsgo:all"] }] + : [ + { name: "typecheck prod", args: ["tsgo:prod"] }, + { name: "typecheck scripts", args: ["tsgo:scripts"] }, + ], }, { name: "lint", diff --git a/scripts/dev/tui-pty-test-watch.ts b/scripts/dev/tui-pty-test-watch.ts index f189b43b1ce2..6abecf9a631e 100644 --- a/scripts/dev/tui-pty-test-watch.ts +++ b/scripts/dev/tui-pty-test-watch.ts @@ -324,8 +324,8 @@ async function main(): Promise { }, ); - let childStdout = Buffer.alloc(0); - let childStderr = Buffer.alloc(0); + let childStdout: Buffer = Buffer.alloc(0); + let childStderr: Buffer = Buffer.alloc(0); let restored = false; let mirrorOffset = 0; let mirrorFilterPending = ""; diff --git a/scripts/e2e/lib/env-limits.d.mts b/scripts/e2e/lib/env-limits.d.mts new file mode 100644 index 000000000000..8c8ac524b644 --- /dev/null +++ b/scripts/e2e/lib/env-limits.d.mts @@ -0,0 +1,2 @@ +export function readPositiveIntEnv(name: string, fallback: number, env?: NodeJS.ProcessEnv): number; +export function readTcpPortEnv(name: string, fallback: number, env?: NodeJS.ProcessEnv): number; diff --git a/scripts/ensure-playwright-chromium.d.mts b/scripts/ensure-playwright-chromium.d.mts new file mode 100644 index 000000000000..9868c9024264 --- /dev/null +++ b/scripts/ensure-playwright-chromium.d.mts @@ -0,0 +1,52 @@ +import type { spawnSync, SpawnSyncOptions } from "node:child_process"; +import type { existsSync } from "node:fs"; +import type { resolvePnpmRunner } from "./pnpm-runner.mjs"; + +type Getuid = typeof process.getuid; +type ChromiumInstallOptions = { + comSpec?: string; + cwd?: string; + env?: NodeJS.ProcessEnv; + executablePath?: string; + existsSync?: typeof existsSync; + getuid?: Getuid; + log?: (message: string) => void; + platform?: NodeJS.Platform; + spawnSync?: typeof spawnSync; + stdio?: SpawnSyncOptions["stdio"]; +}; + +export const systemChromiumExecutableCandidates: readonly string[]; +export function canRunChromiumExecutable( + executablePath: string, + spawnSync?: typeof spawnSync, +): boolean; +export function resolveSystemChromiumExecutablePath( + existsSync?: typeof existsSync, + spawnSync?: typeof spawnSync, +): string; +export function resolvePlaywrightInstallRunner(options?: { + comSpec?: string; + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + targets?: string[]; + withDeps?: boolean; +}): ReturnType; +export function shouldInstallPlaywrightSystemDependencies(options?: { + env?: NodeJS.ProcessEnv; + getuid?: Getuid; + platform?: NodeJS.Platform; +}): boolean; +export function installLinuxSystemChromiumPackage(options?: ChromiumInstallOptions): number; +export function isDirectScriptExecution( + argvEntry?: string, + modulePath?: string, + realpath?: (path: string) => string, +): boolean; +export function ensurePlaywrightChromium( + options?: ChromiumInstallOptions & { + ensureFfmpeg?: boolean; + systemExecutablePath?: string; + }, +): number; +export function shouldEnsureFfmpegFromArgv(argv?: readonly string[]): boolean; diff --git a/scripts/gh-read.ts b/scripts/gh-read.ts index ce1d7f9fbd03..6f774d91be17 100644 --- a/scripts/gh-read.ts +++ b/scripts/gh-read.ts @@ -191,7 +191,7 @@ async function withGitHubFetchTimeout( }, timeoutMs); }); try { - return await Promise.race([run(controller.signal), timeoutPromise]); + return await Promise.race([run(controller.signal, timeoutPromise), timeoutPromise]); } finally { if (timeout) { clearTimeout(timeout); diff --git a/scripts/lib/actions-artifact-archive.d.mts b/scripts/lib/actions-artifact-archive.d.mts new file mode 100644 index 000000000000..2ffa8681f780 --- /dev/null +++ b/scripts/lib/actions-artifact-archive.d.mts @@ -0,0 +1,94 @@ +export const ACTIONS_ARTIFACT_API_VERSION: "2026-03-10"; +export const DEFAULT_MAX_ACTIONS_ARTIFACT_BYTES: number; +export const DEFAULT_MAX_ACTIONS_ARTIFACT_EXPANDED_BYTES: number; + +export type ArtifactArchivePolicy = { + expectedEntries?: readonly string[]; + minEntries?: number; + maxEntries?: number; + maxArchiveBytes?: number; + maxExpandedBytes?: number; + rejectCaseFoldAliases?: boolean; + allowPath?: (name: string) => boolean; + maxCompressedEntryBytes?: (name: string) => number; + maxEntryBytes: (name: string) => number; +}; + +export type ArtifactBinding = { + artifactDigest: string; + artifactId: number; + artifactName: string; + artifactSizeBytes: number; + repository: string; + runStatePolicy: "completed-success" | "same-run-producer-success"; + runAttempt: number; + runId: number; + workflowEvent: string; + workflowHeadBranch: string; + workflowPath: string; + workflowSha: string; + consumerRunAttempt?: number; + producerJobName?: string; +}; + +export type ArtifactFileDescription = { + path: string; + sha256: string; + sizeBytes: number; +}; + +type ArtifactDownloadParams = { + expected: ArtifactBinding; + fetchImpl?: typeof fetch; + maxArchiveBytes?: number; + retryAttempts?: number; + retryDelayMs?: number; + timeoutMs?: number; + token: string; +}; + +type ArtifactDownloadResult = { + archiveBytes: Uint8Array; + artifactMetadata: Record; + binding: ArtifactBinding; + workflowJobs: Record | undefined; + workflowRun: Record; +}; + +export function sha256Digest(bytes: Uint8Array): string; +export function describeActionsArtifactFiles( + files: Map, +): ArtifactFileDescription[]; +export function readBoundedRegularFile( + path: string, + params: { label: string; maxBytes: number }, +): Buffer; +export function inspectActionsArtifactZipWithPolicy( + inputBytes: Uint8Array, + inputPolicy: ArtifactArchivePolicy, +): Map; +export function inspectActionsArtifactZip( + bytes: Uint8Array, + expectedEntries?: number | readonly string[], + limits?: { + maxArchiveBytes?: number; + maxExpandedBytes?: number; + maxCompressedEntryBytes?: number; + maxEntryBytes?: number; + }, +): Map; +export function validateActionsArtifactBinding(params: { + artifactMetadata: unknown; + expected: ArtifactBinding; + workflowRun: unknown; +}): ArtifactBinding; +export function validateActionsArtifactProducerJob(params: { + expected: ArtifactBinding; + workflowJobs: unknown; +}): ArtifactBinding; +export function downloadActionsArtifactArchive( + params: ArtifactDownloadParams, +): Promise; +export function readPublicationArtifactArchive( + params: ArtifactDownloadParams & { archivePolicy: ArtifactArchivePolicy }, +): Promise }>; diff --git a/scripts/lib/arg-utils.d.mts b/scripts/lib/arg-utils.d.mts new file mode 100644 index 000000000000..2a1a765b43c9 --- /dev/null +++ b/scripts/lib/arg-utils.d.mts @@ -0,0 +1,51 @@ +type FlagArgs = Record; +type FlagSpec = { + consume( + argv: readonly string[], + index: number, + args: T, + ): { + flag: string; + nextIndex: number; + repeatable: boolean; + apply(target: T): void; + } | null; +}; + +export function readFlagValue(args: readonly string[], name: string): string | undefined; +export function stripLeadingPackageManagerSeparator(argv: string[]): string[]; +export function stringFlag( + flag: string, + key: string, + options?: { rejectShortOptions?: boolean }, +): FlagSpec; +export function stringListFlag( + flag: string, + key: string, + options?: { rejectShortOptions?: boolean }, +): FlagSpec; +export function intFlag( + flag: string, + key: string, + options?: { min?: number }, +): FlagSpec; +export function floatFlag( + flag: string, + key: string, + options?: { includeMin?: boolean; min?: number }, +): FlagSpec; +export function booleanFlag( + flag: string, + key: string, + value?: unknown, +): FlagSpec; +export function parseFlagArgs( + argv: readonly string[], + args: T, + specs: readonly FlagSpec[], + options?: { + allowUnknownOptions?: boolean; + ignoreDoubleDash?: boolean; + onUnhandledArg?: (arg: string, args: T) => "handled" | void; + }, +): T; diff --git a/scripts/lib/bundled-plugin-paths.d.mts b/scripts/lib/bundled-plugin-paths.d.mts new file mode 100644 index 000000000000..b33dc5d589df --- /dev/null +++ b/scripts/lib/bundled-plugin-paths.d.mts @@ -0,0 +1,11 @@ +export const BUNDLED_PLUGIN_ROOT_DIR: "extensions"; +export const BUNDLED_PLUGIN_PATH_PREFIX: "extensions/"; +export const BUNDLED_PLUGIN_TEST_GLOB: "extensions/**/*.test.ts"; +export const BUNDLED_PLUGIN_E2E_TEST_GLOB: "extensions/**/*.e2e.test.ts"; +export const BUNDLED_PLUGIN_LIVE_TEST_GLOB: "extensions/**/*.live.test.ts"; + +export function bundledPluginRoot(pluginId: string): string; +export function bundledPluginFile(pluginId: string, relativePath: string): string; +export function bundledDistPluginRoot(pluginId: string): string; +export function bundledDistPluginFile(pluginId: string, relativePath: string): string; +export function bundledPluginCallsite(pluginId: string, relativePath: string, line: number): string; diff --git a/scripts/lib/npm-publish-plan.d.mts b/scripts/lib/npm-publish-plan.d.mts new file mode 100644 index 000000000000..3d35638bb2a0 --- /dev/null +++ b/scripts/lib/npm-publish-plan.d.mts @@ -0,0 +1,55 @@ +export type ParsedReleaseVersion = { + version: string; + baseVersion: string; + channel: "stable" | "alpha" | "beta"; + year: number; + month: number; + patch: number; + alphaNumber?: number; + betaNumber?: number; + correctionNumber?: number; +}; +export type NpmPublishPlan = { + channel: "stable" | "alpha" | "beta"; + publishTag: "latest" | "alpha" | "beta" | "extended-stable"; + mirrorDistTags: Array<"latest" | "alpha" | "beta">; +}; +export type PublishedNpmVersionRoute = "npm-readback" | "npm-mirror" | "npm-tag-repair"; +export type NpmRegistryPackumentResult = { + status: number; + ok: boolean; + packument: unknown; +}; +export function fetchNpmRegistryPackumentWithRetry(params: { + packageName: string; + packageUrl: string; + attempts?: number; + timeoutMs?: number; + fetchImpl?: (input: string, init: RequestInit) => Promise; + sleep?: (delayMs: number) => Promise; + createSignal?: (timeoutMs: number) => AbortSignal; +}): Promise; +export function parseReleaseVersion(version: string): ParsedReleaseVersion | null; +export function collectReleaseVersionFloorErrors( + version: string | ParsedReleaseVersion | null, +): string[]; +export function compareReleaseVersions(left: string, right: string): number | null; +export function resolveNpmPublishPlan( + version: string, + currentBetaVersion?: string | null, + publishTagOverride?: string | null, +): NpmPublishPlan; +export function resolvePublishedNpmVersionRoute(params: { + packageVersion: string; + publishPlan: NpmPublishPlan; + distTags: Record; +}): PublishedNpmVersionRoute; +export function resolveNpmDistTagMirrorAuth(params?: { + nodeAuthToken?: string | null; + npmToken?: string | null; +}): { hasAuth: boolean; source: "node-auth-token" | "npm-token" | "none" }; +export function shouldRequireNpmDistTagMirrorAuth(params: { + mode: "--dry-run" | "--publish"; + mirrorDistTags: readonly string[]; + hasAuth: boolean; +}): boolean; diff --git a/scripts/lib/npm-verify-exec.ts b/scripts/lib/npm-verify-exec.ts index 6d8e22800439..c263d93303db 100644 --- a/scripts/lib/npm-verify-exec.ts +++ b/scripts/lib/npm-verify-exec.ts @@ -1,12 +1,16 @@ // Npm Verify Exec script supports OpenClaw repository automation. -import { execFileSync } from "node:child_process"; +import { execFileSync, type ExecFileSyncOptionsWithStringEncoding } from "node:child_process"; -type NpmVerifyCommandInvocation = { +export type NpmVerifyCommandInvocation = { command: string; args: string[]; windowsVerbatimArguments?: boolean; }; +type NpmVerifyExecOptions = ExecFileSyncOptionsWithStringEncoding & { + windowsVerbatimArguments?: boolean; +}; + const DEFAULT_NPM_VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1000; const DEFAULT_NPM_VERIFY_COMMAND_MAX_BUFFER_BYTES = 16 * 1024 * 1024; @@ -40,7 +44,7 @@ export function runNpmVerifyCommand( DEFAULT_NPM_VERIFY_COMMAND_MAX_BUFFER_BYTES, ); - return execFileSync(invocation.command, invocation.args, { + const execOptions: NpmVerifyExecOptions = { cwd, encoding: "utf8", killSignal: "SIGKILL", @@ -48,5 +52,6 @@ export function runNpmVerifyCommand( stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs, windowsVerbatimArguments: invocation.windowsVerbatimArguments, - }).trim(); + }; + return execFileSync(invocation.command, invocation.args, execOptions).trim(); } diff --git a/scripts/lib/numeric-options.d.mts b/scripts/lib/numeric-options.d.mts new file mode 100644 index 000000000000..f5f9606b5ef9 --- /dev/null +++ b/scripts/lib/numeric-options.d.mts @@ -0,0 +1,3 @@ +export function parsePositiveInt(raw: string, label: string): number; +export function parseNonNegativeInt(raw: string, label: string): number; +export function parsePositiveNumber(raw: string, label: string): number; diff --git a/scripts/lib/plugin-clawhub-release.ts b/scripts/lib/plugin-clawhub-release.ts index 0abde4c6cedf..b25d48f42ee3 100644 --- a/scripts/lib/plugin-clawhub-release.ts +++ b/scripts/lib/plugin-clawhub-release.ts @@ -58,7 +58,7 @@ export type PublishablePluginPackage = { packageName: string; version: string; channel: "stable" | "alpha" | "beta"; - publishTag: "latest" | "alpha" | "beta"; + publishTag: "latest" | "alpha" | "beta" | "extended-stable"; requiredLatestDependencies?: RequiredLatestDependency[]; }; diff --git a/scripts/lib/plugin-npm-release.ts b/scripts/lib/plugin-npm-release.ts index e786bf35a146..ae2773a6f99b 100644 --- a/scripts/lib/plugin-npm-release.ts +++ b/scripts/lib/plugin-npm-release.ts @@ -40,6 +40,7 @@ export type PluginPackageJson = { pluginSdkVersion?: string; }; release?: { + publishToClawHub?: boolean; publishToNpm?: boolean; requireLatestDependencies?: unknown; }; diff --git a/scripts/lib/plugin-package-dependencies.d.mts b/scripts/lib/plugin-package-dependencies.d.mts new file mode 100644 index 000000000000..0ff21b4d946a --- /dev/null +++ b/scripts/lib/plugin-package-dependencies.d.mts @@ -0,0 +1,14 @@ +export type RuntimeDependencyPackageJson = { + dependencies?: Record; + optionalDependencies?: Record; +}; +export function collectRuntimeDependencySpecs( + packageJson?: RuntimeDependencyPackageJson, +): Map; +export function packageNameFromSpecifier(specifier: string): string | null; +export function collectBundledPluginPackageDependencySpecs( + bundledPluginsDir: string, +): Map< + string, + { conflicts: Array<{ pluginId: string; spec: string }>; pluginIds: string[]; spec: string } +>; diff --git a/scripts/lib/release-beta-verifier.ts b/scripts/lib/release-beta-verifier.ts index 61d24e3fc932..71499219c3fd 100644 --- a/scripts/lib/release-beta-verifier.ts +++ b/scripts/lib/release-beta-verifier.ts @@ -843,7 +843,12 @@ function validateBootstrapPackageEvidence( } const expectedSize = value.expectedSize; const registrySize = value.registrySize; - if (!Number.isSafeInteger(expectedSize) || expectedSize <= 0 || registrySize !== expectedSize) { + if ( + typeof expectedSize !== "number" || + !Number.isSafeInteger(expectedSize) || + expectedSize <= 0 || + registrySize !== expectedSize + ) { throw new Error( `${params.packageName} registry artifact size differs from the packed artifact.`, ); @@ -1316,6 +1321,9 @@ export async function verifyBetaRelease( } const workflowRuns: WorkflowRunSummary[] = []; + const allowedReleaseWorkflowHeadBranches = args.workflowRef + ? ["main", args.workflowRef] + : ["main"]; if (args.workflowRuns.fullReleaseValidation !== undefined) { workflowRuns.push( verifyWorkflowRun({ @@ -1323,7 +1331,7 @@ export async function verifyBetaRelease( label: "Full Release Validation", repo: args.repo, expectedWorkflowName: "Full Release Validation", - allowedHeadBranches: ["main", args.workflowRef], + allowedHeadBranches: allowedReleaseWorkflowHeadBranches, rerunFailed: false, }), ); @@ -1383,7 +1391,7 @@ export async function verifyBetaRelease( label: "NPM Telegram Beta E2E", repo: args.repo, expectedWorkflowName: "NPM Telegram Beta E2E", - allowedHeadBranches: ["main", args.workflowRef], + allowedHeadBranches: allowedReleaseWorkflowHeadBranches, rerunFailed: false, }), ); diff --git a/scripts/lib/workspace-bootstrap-smoke.d.mts b/scripts/lib/workspace-bootstrap-smoke.d.mts new file mode 100644 index 000000000000..1d1398a7a6c4 --- /dev/null +++ b/scripts/lib/workspace-bootstrap-smoke.d.mts @@ -0,0 +1,7 @@ +export const WORKSPACE_TEMPLATE_PACK_PATHS: readonly string[]; +export function createWorkspaceBootstrapSmokeEnv( + env: NodeJS.ProcessEnv, + homeDir: string, + overrides?: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv; +export function runInstalledWorkspaceBootstrapSmoke(params: { packageRoot: string }): void; diff --git a/scripts/native-app-i18n.ts b/scripts/native-app-i18n.ts index 53021d77f673..639b616dc074 100644 --- a/scripts/native-app-i18n.ts +++ b/scripts/native-app-i18n.ts @@ -659,7 +659,7 @@ function extractCandidates( uiCallNames: ReadonlySet, ): Candidate[] { const entries: Candidate[] = []; - const patterns = + const patterns: Array = surface === "apple" ? [ [APPLE_UI_MULTILINE_CALLS, "ui-call-multiline"], diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 553ceaf3ec7a..dc5147db5f62 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -2,7 +2,7 @@ // Executed directly via Node.js native type stripping in the release workflow. -import { spawn } from "node:child_process"; +import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import { appendFileSync, @@ -19,10 +19,11 @@ import { realpathSync, rmSync, statSync, + type WriteStream, writeFileSync, } from "node:fs"; import { mkdtempSync } from "node:fs"; -import { createServer } from "node:http"; +import { createServer, type Server } from "node:http"; import { createConnection as createNetConnection, createServer as createNetServer } from "node:net"; import type { Socket } from "node:net"; import { tmpdir } from "node:os"; @@ -32,19 +33,113 @@ import { isLocalBuildMetadataDistPath } from "./lib/local-build-metadata-paths.m import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs"; import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs"; +type CrossOsSuite = "packaged-fresh" | "installer-fresh" | "packaged-upgrade" | "dev-update"; +type CrossOsMode = "fresh" | "upgrade" | "both"; +type CrossOsOsId = "ubuntu" | "windows" | "macos"; +type ProviderId = "openai" | "anthropic" | "minimax"; +type ProviderConfig = { + extensionId: string; + secretEnv: string; + authChoice: string; + model: string; + baseUrl?: string; + timeoutSeconds?: number; +}; +type ParsedArgs = Record; +type LaneResult = { status: string; error?: string } & Record; +type CandidateBuild = { + candidateTgz: string; + candidateVersion: string; + candidateFileName: string; + sourceDir: string; + sourceSha: string; +}; +type PackageJson = { + name?: string; + version?: string; + scripts?: Record; + openclaw?: { commit?: string }; +}; +type LaneState = { + name: string; + rootDir: string; + prefixDir: string; + homeDir: string; + stateDir: string; + appDataDir: string; + gatewayPort: number; + phaseTimings: Array<{ name: string; status: "pass" | "fail"; durationMs: number }>; +}; +type GatewayHandle = { + child: ChildProcess; + closeLog: () => Promise; + logPath: string; +}; +type CommandResult = { exitCode: number; stdout: string; stderr: string }; +type AgentTurnResult = CommandResult | { status: number; stdout: string; stderr: string }; +type CommandOptions = { + cwd?: string; + env?: NodeJS.ProcessEnv; + logPath: string; + timeoutMs?: number; + check?: boolean; + maxOutputBytes?: number; +}; +type CommandInvocation = { + command: string; + args: string[]; + shell?: boolean | string; + windowsVerbatimArguments?: boolean; +}; +type Cleanup = () => Promise | void; +type LaneBaseParams = { + logsDir: string; + providerConfig: ProviderConfig; + providerSecretValue: string; +}; +type LaneCommandParams = { + lane: LaneState; + env: NodeJS.ProcessEnv; + logPath: string; +}; +type AgentOutputOptions = { logText?: string; logPath?: string }; +type SummaryPayload = { + provider: string; + suite: string; + mode: string; + sourceSha?: string; + candidateVersion?: string; + baselineSpec: string; + result?: { + status?: string; + installTarget?: string; + installVersion?: string; + baselineVersion?: string; + installedVersion?: string; + installedCommit?: string; + cliPath?: string; + gatewayPort?: number; + dashboardStatus?: string; + discordStatus?: string; + agentOutput?: string; + error?: string; + phaseTimings?: LaneState["phaseTimings"]; + }; +}; + const SCRIPT_PATH = fileURLToPath(import.meta.url); const PUBLISHED_INSTALLER_BASE_URL = "https://openclaw.ai"; -const SUPPORTED_MODES = new Set(["fresh", "upgrade", "both"]); -const SUPPORTED_SUITES = new Set([ +const SUPPORTED_MODES = new Set(["fresh", "upgrade", "both"]); +const SUPPORTED_SUITES = new Set([ "packaged-fresh", "installer-fresh", "packaged-upgrade", "dev-update", ]); -const SUPPORTED_OS_IDS = new Set(["ubuntu", "windows", "macos"]); +const SUPPORTED_OS_IDS = new Set(["ubuntu", "windows", "macos"]); -const CROSS_OS_SIGNAL_EXIT_CODES = { +const CROSS_OS_SIGNAL_EXIT_CODES: Partial> = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, @@ -69,6 +164,9 @@ const CROSS_OS_AGENT_TURN_OPTIONAL = resolveCrossOsAgentTurnOptional(); for (const signal of Object.keys(CROSS_OS_SIGNAL_EXIT_CODES) as NodeJS.Signals[]) { process.on(signal, () => { forwardedSignalExitCode ??= CROSS_OS_SIGNAL_EXIT_CODES[signal]; + if (forwardedSignalExitCode === undefined) { + return; + } if (CROSS_OS_ACTIVE_CHILD_TREE_KILLERS.size === 0) { process.exit(forwardedSignalExitCode); } @@ -117,13 +215,13 @@ const providerConfig = { authChoice: "minimax-global-api", model: "minimax/MiniMax-M2.7", }, -}; +} satisfies Record; -export function resolveProviderConfig(provider, env = process.env) { - const config = providerConfig[provider]; - if (!config) { +export function resolveProviderConfig(provider: string, env = process.env): ProviderConfig | null { + if (!Object.hasOwn(providerConfig, provider)) { return null; } + const config: ProviderConfig = providerConfig[provider as ProviderId]; const providerEnvKey = `OPENCLAW_CROSS_OS_${provider.toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}_MODEL`; const model = env[providerEnvKey]?.trim() || env.OPENCLAW_CROSS_OS_MODEL?.trim() || config.model; return { ...config, model }; @@ -138,7 +236,7 @@ const RELEASE_SMOKE_PLUGIN_ALLOWLIST_BASE = [ "talk-voice", ]; -export function buildCrossOsReleaseSmokePluginAllowlist(providerMeta) { +export function buildCrossOsReleaseSmokePluginAllowlist(providerMeta: ProviderConfig) { return [...new Set([providerMeta.extensionId, ...RELEASE_SMOKE_PLUGIN_ALLOWLIST_BASE])]; } @@ -146,13 +244,13 @@ export function buildCrossOsReleaseSmokeMemorySlotConfigArgs() { return ["config", "set", "plugins.slots.memory", JSON.stringify("none"), "--strict-json"]; } -function shouldSeedProviderConfigModels(providerMeta) { +function shouldSeedProviderConfigModels(providerMeta: ProviderConfig) { return ( typeof providerMeta.baseUrl === "string" || typeof providerMeta.timeoutSeconds === "number" ); } -function buildReleaseProviderConfigOverride(providerMeta) { +function buildReleaseProviderConfigOverride(providerMeta: ProviderConfig) { if (!shouldSeedProviderConfigModels(providerMeta)) { return null; } @@ -194,7 +292,7 @@ export const CROSS_OS_COMMAND_HEARTBEAT_SECONDS = parsePositiveIntegerEnv( 60, ); -export function resolveNpmPackTarballFileName(value, label = "npm pack") { +export function resolveNpmPackTarballFileName(value: unknown, label = "npm pack") { const filename = typeof value === "string" ? value.trim() : ""; if ( !filename.endsWith(".tgz") || @@ -207,7 +305,11 @@ export function resolveNpmPackTarballFileName(value, label = "npm pack") { return filename; } -export function resolvePackDestinationTarball(value, packDestination, label = "package pack") { +export function resolvePackDestinationTarball( + value: unknown, + packDestination: string, + label = "package pack", +) { const filename = typeof value === "string" ? value.trim() : ""; const fileName = basename(filename); const destinationDir = resolve(packDestination); @@ -241,8 +343,8 @@ function isMainModule() { return resolve(invokedPath) === SCRIPT_PATH; } -export function parseArgs(argv) { - const parsed = {}; +export function parseArgs(argv: string[]): ParsedArgs { + const parsed: ParsedArgs = {}; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; if (!token.startsWith("--")) { @@ -260,7 +362,7 @@ export function parseArgs(argv) { return parsed; } -export function parsePositiveIntegerEnv(name, fallback, env = process.env) { +export function parsePositiveIntegerEnv(name: string, fallback: number, env = process.env): number { const raw = env[name]?.trim(); if (!raw) { return fallback; @@ -275,7 +377,7 @@ export function parsePositiveIntegerEnv(name, fallback, env = process.env) { return value; } -function parseBooleanEnv(name, fallback, env = process.env) { +function parseBooleanEnv(name: string, fallback: boolean, env = process.env): boolean { const raw = env[name]?.trim(); if (!raw) { return fallback; @@ -293,14 +395,14 @@ export function resolveCrossOsAgentTurnOptional(env = process.env) { return parseBooleanEnv("OPENCLAW_CROSS_OS_AGENT_TURN_OPTIONAL", false, env); } -export function looksLikeReleaseVersionRef(ref) { +export function looksLikeReleaseVersionRef(ref: string) { const trimmed = normalizeRequestedRef(ref); return /^v?[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[1-9][0-9]*)|[-.](?:alpha|beta|rc)[-.]?[0-9]+)?$/iu.test( trimmed, ); } -export function normalizeRequestedRef(ref) { +export function normalizeRequestedRef(ref?: string) { const trimmed = ref?.trim() || ""; if (!trimmed) { return ""; @@ -314,16 +416,16 @@ export function normalizeRequestedRef(ref) { return trimmed; } -export function isImmutableReleaseRef(ref) { +export function isImmutableReleaseRef(ref?: string) { const trimmed = ref?.trim() || ""; return trimmed.startsWith("refs/tags/") || looksLikeReleaseVersionRef(trimmed); } -export function resolveRequestedSuites(mode, ref) { - if (!SUPPORTED_MODES.has(mode)) { +export function resolveRequestedSuites(mode: string, ref: string): CrossOsSuite[] { + if (!SUPPORTED_MODES.has(mode as CrossOsMode)) { throw new Error(`Unsupported mode "${mode}".`); } - const suites = []; + const suites: CrossOsSuite[] = []; if (mode === "fresh" || mode === "both") { suites.push("packaged-fresh", "installer-fresh"); } @@ -336,8 +438,18 @@ export function resolveRequestedSuites(mode, ref) { return suites; } -export function resolveRunnerMatrix(params) { - const pick = (...values) => +export function resolveRunnerMatrix(params: { + mode: string; + ref: string; + suiteFilter?: string; + ubuntuRunner?: string; + windowsRunner?: string; + macosRunner?: string; + varUbuntuRunner?: string; + varWindowsRunner?: string; + varMacosRunner?: string; +}) { + const pick = (...values: Array) => values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim(); const suites = resolveRequestedSuites(params.mode, params.ref); const suiteFilter = parseCrossOsSuiteFilter(params.suiteFilter ?? ""); @@ -363,7 +475,7 @@ export function resolveRunnerMatrix(params) { ]; const include = runners.flatMap((runner) => suites - .filter((suite) => suiteFilter.matches(runner.os_id, suite)) + .filter((suite) => suiteFilter.matches(runner.os_id as CrossOsOsId, suite)) .map((suite) => Object.assign({}, runner, { suite, @@ -382,8 +494,8 @@ export function resolveRunnerMatrix(params) { }; } -export function parseCrossOsSuiteFilter(rawFilter) { - const tokens = String(rawFilter ?? "") +export function parseCrossOsSuiteFilter(rawFilter: string) { + const tokens = rawFilter .split(/[, ]+/u) .map((token) => normalizeCrossOsSuiteFilterToken(token)) .filter(Boolean); @@ -395,11 +507,11 @@ export function parseCrossOsSuiteFilter(rawFilter) { } const matchers = tokens.map((token) => { - if (SUPPORTED_SUITES.has(token)) { - return { osId: "", suite: token }; + if (SUPPORTED_SUITES.has(token as CrossOsSuite)) { + return { osId: "", suite: token as CrossOsSuite }; } - if (SUPPORTED_OS_IDS.has(token)) { - return { osId: token, suite: "" }; + if (SUPPORTED_OS_IDS.has(token as CrossOsOsId)) { + return { osId: token as CrossOsOsId, suite: "" }; } for (const separator of ["/", ":", "-"]) { const matchedOs = [...SUPPORTED_OS_IDS].find((osId) => @@ -409,10 +521,10 @@ export function parseCrossOsSuiteFilter(rawFilter) { continue; } const suite = token.slice(matchedOs.length + separator.length); - if (!SUPPORTED_SUITES.has(suite)) { + if (!SUPPORTED_SUITES.has(suite as CrossOsSuite)) { break; } - return { osId: matchedOs, suite }; + return { osId: matchedOs, suite: suite as CrossOsSuite }; } throw new Error( `Unsupported cross_os_suite_filter token ${JSON.stringify(token)}. Use an OS id, suite id, or os/suite pair such as windows/packaged-upgrade.`, @@ -420,7 +532,7 @@ export function parseCrossOsSuiteFilter(rawFilter) { }); return { - matches: (osId, suite) => + matches: (osId: CrossOsOsId, suite: CrossOsSuite) => matchers.some((matcher) => { const osMatches = !matcher.osId || matcher.osId === osId; const suiteMatches = !matcher.suite || matcher.suite === suite; @@ -430,7 +542,7 @@ export function parseCrossOsSuiteFilter(rawFilter) { }; } -function normalizeCrossOsSuiteFilterToken(token) { +function normalizeCrossOsSuiteFilterToken(token: string) { return token .trim() .toLowerCase() @@ -465,7 +577,7 @@ export function readRunnerOverrideEnv(env = process.env) { }; } -function formatSuiteLabel(suite) { +function formatSuiteLabel(suite: CrossOsSuite) { if (suite === "packaged-fresh") { return "packaged fresh"; } @@ -478,7 +590,7 @@ function formatSuiteLabel(suite) { return "dev update"; } -async function main(argv) { +async function main(argv: string[]) { const args = parseArgs(argv); if (args["resolve-matrix"] === "true") { @@ -539,7 +651,7 @@ async function main(argv) { return; } - if (!SUPPORTED_SUITES.has(suite)) { + if (!SUPPORTED_SUITES.has(suite as CrossOsSuite)) { throw new Error(`Unsupported suite "${suite}".`); } if (!Object.hasOwn(providerConfig, provider)) { @@ -547,6 +659,9 @@ async function main(argv) { } const selectedProvider = resolveProviderConfig(provider); + if (!selectedProvider) { + throw new Error(`Unsupported provider "${provider}".`); + } const providerSecretValue = process.env[selectedProvider.secretEnv]?.trim(); if (!providerSecretValue) { throw new Error(`Missing ${selectedProvider.secretEnv}.`); @@ -568,11 +683,11 @@ async function main(argv) { baselineSpec, result: { status: "pending", - }, + } as LaneResult, discordRoundtrip: runDiscordRoundtrip, }; - let build; + let build: CandidateBuild; try { build = sourceDir ? await prepareCandidate({ @@ -647,7 +762,11 @@ async function main(argv) { } } -async function prepareCandidate(params) { +async function prepareCandidate(params: { + outputDir: string; + sourceDir: string; + logsDir: string; +}): Promise { logPhase("prepare", "resolve-source-sha"); const packageJson = readPackageJson(params.sourceDir); const hasUiBuildScript = packageJsonHasScript(packageJson, "ui:build"); @@ -723,7 +842,7 @@ async function prepareCandidate(params) { }; } -export function resolvePackageCandidatePackCommand(sourceDir, packDir) { +export function resolvePackageCandidatePackCommand(sourceDir: string, packDir: string) { const packageHelper = join(sourceDir, "scripts", "package-openclaw-for-docker.mjs"); if (existsSync(packageHelper)) { return { @@ -744,7 +863,12 @@ export function resolvePackageCandidatePackCommand(sourceDir, packDir) { }; } -function resolvePackedCandidateFromOutput(params) { +function resolvePackedCandidateFromOutput(params: { + output: string; + packDir: string; + packageJson: PackageJson; + packCommand: ReturnType; +}) { if (params.packCommand.kind === "docker-helper") { const packOutputLines = params.output.trim().split(/\r?\n/u).filter(Boolean); const packedTarball = resolvePackDestinationTarball( @@ -764,11 +888,13 @@ function resolvePackedCandidateFromOutput(params) { 2, )}\n`, path: packedTarball.path, - version: String(params.packageJson.version ?? "").trim(), + version: (params.packageJson.version ?? "").trim(), }; } - const parsedPack = JSON.parse(params.output); + const parsedPack = JSON.parse(params.output) as + | { filename?: string; version?: string } + | Array<{ filename?: string; version?: string }>; const lastPack = Array.isArray(parsedPack) ? parsedPack.at(-1) : parsedPack; const packedTarball = resolvePackDestinationTarball( lastPack?.filename, @@ -779,25 +905,25 @@ function resolvePackedCandidateFromOutput(params) { fileName: packedTarball.fileName, packJson: params.output, path: packedTarball.path, - version: String(lastPack?.version ?? params.packageJson.version ?? "").trim(), + version: (lastPack?.version ?? params.packageJson.version ?? "").trim(), }; } -function normalizeRelativePath(value) { +function normalizeRelativePath(value: string) { return value.replace(/\\/gu, "/"); } -function isNotFoundError(error) { - return error && typeof error === "object" && error.code === "ENOENT"; +function isNotFoundError(error: unknown) { + return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT"; } -function isInstallStageDirName(value) { +function isInstallStageDirName(value: string) { return INSTALL_STAGE_DEBRIS_DIR_PATTERN.test(value); } -function collectLegacyPluginDependencyStagingDebrisPaths(packageRoot) { +function collectLegacyPluginDependencyStagingDebrisPaths(packageRoot: string) { const rootEntries = readdirSync(packageRoot, { withFileTypes: true }); - const debris = []; + const debris: string[] = []; for (const rootEntry of rootEntries) { if (!rootEntry.isDirectory() || rootEntry.name.toLowerCase() !== "dist") { continue; @@ -854,7 +980,7 @@ function collectLegacyPluginDependencyStagingDebrisPaths(packageRoot) { return debris.toSorted((left, right) => left.localeCompare(right)); } -function assertNoLegacyPluginDependencyStagingDebris(packageRoot) { +function assertNoLegacyPluginDependencyStagingDebris(packageRoot: string) { const debris = collectLegacyPluginDependencyStagingDebrisPaths(packageRoot); if (debris.length === 0) { return; @@ -864,7 +990,7 @@ function assertNoLegacyPluginDependencyStagingDebris(packageRoot) { ); } -function isPackagedDistPath(relativePath) { +function isPackagedDistPath(relativePath: string) { if (!relativePath.startsWith("dist/")) { return false; } @@ -886,7 +1012,10 @@ function isPackagedDistPath(relativePath) { return true; } -export async function writePackageDistInventoryForCandidate(params) { +export async function writePackageDistInventoryForCandidate(params: { + sourceDir: string; + logPath: string; +}) { assertNoLegacyPluginDependencyStagingDebris(params.sourceDir); const dryRun = await runCommand( pnpmCommand(), @@ -897,7 +1026,9 @@ export async function writePackageDistInventoryForCandidate(params) { timeoutMs: 5 * 60 * 1000, }, ); - const parsedPack = JSON.parse(dryRun.stdout); + const parsedPack = JSON.parse(dryRun.stdout) as + | { files?: Array<{ path?: string }> } + | Array<{ files?: Array<{ path?: string }> }>; const lastPack = Array.isArray(parsedPack) ? parsedPack.at(-1) : parsedPack; const files = Array.isArray(lastPack?.files) ? lastPack.files : []; if (files.length === 0) { @@ -907,7 +1038,7 @@ export async function writePackageDistInventoryForCandidate(params) { } const inventory = files .flatMap((entry) => { - const relativePath = normalizeRelativePath(String(entry?.path ?? "").trim()); + const relativePath = normalizeRelativePath((entry.path ?? "").trim()); return isPackagedDistPath(relativePath) ? [relativePath] : []; }) .toSorted((left, right) => left.localeCompare(right)); @@ -916,7 +1047,11 @@ export async function writePackageDistInventoryForCandidate(params) { writeFileSync(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`, "utf8"); } -function readProvidedCandidate(params) { +function readProvidedCandidate(params: { + candidateTgz: string; + candidateVersion: string; + sourceSha: string; +}): CandidateBuild { if (!params.candidateTgz) { throw new Error("Missing required --candidate-tgz argument when --source-dir is not provided."); } @@ -940,9 +1075,9 @@ function readProvidedCandidate(params) { }; } -async function runFreshLane(params) { +async function runFreshLane(params: LaneBaseParams & { build: CandidateBuild }) { const lane = createLaneState("fresh"); - const cleanup = []; + const cleanup: Cleanup[] = []; try { const env = buildLaneEnv(lane, params.providerConfig, params.providerSecretValue); await runTimedLanePhase(lane, "install-candidate", async () => { @@ -1045,7 +1180,14 @@ async function runFreshLane(params) { } } -async function runUpgradeLane(params) { +async function runUpgradeLane( + params: LaneBaseParams & { + baselineSpec: string; + baselineTgz: string; + build: CandidateBuild; + candidateUrl: string; + }, +) { if (!params.baselineTgz && !params.baselineSpec) { throw new Error("Missing required --baseline-tgz argument for upgrade mode."); } @@ -1053,7 +1195,7 @@ async function runUpgradeLane(params) { throw new Error("Missing candidate package URL for upgrade mode."); } const lane = createLaneState("upgrade"); - const cleanup = []; + const cleanup: Cleanup[] = []; try { const env = buildLaneEnv(lane, params.providerConfig, params.providerSecretValue); await runTimedLanePhase(lane, "install-baseline", async () => { @@ -1091,7 +1233,7 @@ async function runUpgradeLane(params) { const updateEnv = buildRealUpdateEnv(env); const updateArgs = buildPackagedUpgradeUpdateArgs(params.candidateUrl); const updateLogPath = join(params.logsDir, "upgrade-update.log"); - let updateResult; + let updateResult: CommandResult | undefined; let usedWindowsPackagedUpgradeTimeoutFallback = false; await runTimedLanePhase(lane, "update", async () => { try { @@ -1119,6 +1261,9 @@ async function runUpgradeLane(params) { }; } }); + if (!updateResult) { + throw new Error("Packaged update completed without a command result."); + } const usedWindowsPackagedUpgradeFallback = usedWindowsPackagedUpgradeTimeoutFallback || isRecoverableWindowsPackagedUpgradeSwapCleanupFailure(updateResult, process.platform); @@ -1240,12 +1385,14 @@ async function runUpgradeLane(params) { } } -async function runInstallerFreshSuite(params) { +async function runInstallerFreshSuite( + params: LaneBaseParams & { build: CandidateBuild; runDiscordRoundtrip: boolean }, +) { const lane = createLaneState("installer-fresh"); - const cleanup = []; + const cleanup: Cleanup[] = []; const usesManagedGateway = shouldUseManagedGatewayService(); const useManagedGatewayAfterInstall = shouldUseManagedGatewayForInstallerRuntime(); - const manualGateway = { current: null }; + const manualGateway: { current: GatewayHandle | null } = { current: null }; try { const env = buildInstallerEnv(lane, params.providerConfig, params.providerSecretValue); // Drive the public installer against the exact candidate artifact built from the requested ref. @@ -1401,9 +1548,16 @@ async function runInstallerFreshSuite(params) { } } -async function runDevUpdateSuite(params) { +async function runDevUpdateSuite( + params: LaneBaseParams & { + baselineSpec: string; + ref: string; + sourceSha: string; + runDiscordRoundtrip: boolean; + }, +) { const lane = createLaneState("dev-update"); - const cleanup = []; + const cleanup: Cleanup[] = []; const installTarget = await resolveInstallerTargetVersion({ baselineSpec: params.baselineSpec, logsDir: params.logsDir, @@ -1421,7 +1575,7 @@ async function runDevUpdateSuite(params) { ); } const verificationRef = resolveDevUpdateVerificationRef(params.ref, params.sourceSha); - const manualGateway = { current: null }; + const manualGateway: { current: GatewayHandle | null } = { current: null }; try { const env = buildInstallerEnv(lane, params.providerConfig, params.providerSecretValue); const installerUrl = resolvePublishedInstallerUrl(); @@ -1569,7 +1723,7 @@ async function runDevUpdateSuite(params) { } } -function createLaneState(name) { +function createLaneState(name: string): LaneState { const rootDir = mkdtempSync(join(tmpdir(), `openclaw-${name}-`)); const prefixDir = join(rootDir, "prefix"); const homeDir = join(rootDir, "home"); @@ -1595,7 +1749,11 @@ function createLaneState(name) { }; } -function buildLaneEnv(lane, providerMeta, providerSecretValue) { +function buildLaneEnv( + lane: LaneState, + providerMeta: ProviderConfig, + providerSecretValue: string, +): NodeJS.ProcessEnv { ensureLocalNpmShim(lane); return { ...process.env, @@ -1614,7 +1772,11 @@ function buildLaneEnv(lane, providerMeta, providerSecretValue) { }; } -function buildInstallerEnv(lane, providerMeta, providerSecretValue) { +function buildInstallerEnv( + lane: LaneState, + providerMeta: ProviderConfig, + providerSecretValue: string, +): NodeJS.ProcessEnv { const localAppData = join(lane.homeDir, "AppData", "Local"); mkdirSync(localAppData, { recursive: true }); return { @@ -1651,27 +1813,27 @@ export function shouldStopManagedGatewayBeforeManualFallback(platform = process. return shouldUseManagedGatewayService(platform); } -function shouldRunBundledPluginPostinstall() { +function shouldRunBundledPluginPostinstall(_options?: { lane?: LaneState }) { return true; } -function looksLikeCommitSha(ref) { +function looksLikeCommitSha(ref: string) { return /^[0-9a-f]{7,40}$/iu.test(ref.trim()); } -function resolveExpectedDevUpdateRef(ref) { +function resolveExpectedDevUpdateRef(ref?: string) { const trimmed = normalizeRequestedRef(ref) || "main"; return trimmed || "main"; } -export function resolveDevUpdateVerificationRef(ref, sourceSha) { +export function resolveDevUpdateVerificationRef(ref: string, sourceSha: string) { if (resolveExpectedDevUpdateRef(ref) === "main" && looksLikeCommitSha(sourceSha ?? "")) { return sourceSha.trim(); } return resolveExpectedDevUpdateRef(ref); } -export function shouldRunMainChannelDevUpdate(ref) { +export function shouldRunMainChannelDevUpdate(ref: string) { if (isImmutableReleaseRef(ref)) { return false; } @@ -1682,8 +1844,8 @@ export function shouldSkipInstallerDaemonHealthCheck(platform = process.platform return platform === "win32"; } -export function buildRealUpdateEnv(env) { - const updateEnv = { +export function buildRealUpdateEnv(env: NodeJS.ProcessEnv) { + const updateEnv: NodeJS.ProcessEnv = { ...env, OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS: "1", NODE_DISABLE_COMPILE_CACHE: "1", @@ -1693,7 +1855,10 @@ export function buildRealUpdateEnv(env) { return updateEnv; } -export function verifyPackagedUpgradeUpdateResult(result, _options) { +export function verifyPackagedUpgradeUpdateResult( + result: CommandResult, + _options?: { candidateVersion?: string }, +) { if (result.exitCode === 0) { return; } @@ -1705,7 +1870,7 @@ export function verifyPackagedUpgradeUpdateResult(result, _options) { ); } -export function buildPackagedUpgradeUpdateArgs(candidateUrl) { +export function buildPackagedUpgradeUpdateArgs(candidateUrl: string) { return [ "update", "--tag", @@ -1719,10 +1884,10 @@ export function buildPackagedUpgradeUpdateArgs(candidateUrl) { } export function isRecoverableWindowsPackagedUpgradeSwapCleanupFailure( - result, + result: CommandResult | undefined, platform = process.platform, ) { - if (platform !== "win32" || result.exitCode === 0) { + if (platform !== "win32" || !result || result.exitCode === 0) { return false; } const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`; @@ -1736,7 +1901,7 @@ export function isRecoverableWindowsPackagedUpgradeSwapCleanupFailure( } export function isRecoverableWindowsPackagedUpgradeTimeoutError( - error, + error: unknown, platform = process.platform, ) { if (platform !== "win32") { @@ -1754,13 +1919,16 @@ export function isRecoverableWindowsPackagedUpgradeTimeoutError( export function shouldRunPackagedUpgradeStatusProbe({ platform = process.platform, usedWindowsPackagedUpgradeFallback, -} = {}) { +}: { platform?: NodeJS.Platform; usedWindowsPackagedUpgradeFallback?: boolean } = {}) { return !(platform === "win32" && usedWindowsPackagedUpgradeFallback); } export function verifyWindowsPackagedUpgradeFallbackInstall({ installedVersion, candidateVersion, +}: { + installedVersion: string; + candidateVersion: string; }) { if (installedVersion !== candidateVersion) { throw new Error( @@ -1769,7 +1937,7 @@ export function verifyWindowsPackagedUpgradeFallbackInstall({ } } -export function resolveExplicitBaselineVersion(baselineSpec) { +export function resolveExplicitBaselineVersion(baselineSpec: string) { const trimmed = baselineSpec.trim(); if (!trimmed || trimmed === "openclaw@latest") { return ""; @@ -1780,7 +1948,11 @@ export function resolveExplicitBaselineVersion(baselineSpec) { return trimmed; } -async function resolveInstallerTargetVersion(params) { +async function resolveInstallerTargetVersion(params: { + baselineSpec: string; + logsDir: string; + suiteName: string; +}) { const resolvedVersion = resolveExplicitBaselineVersion(params.baselineSpec); if (resolvedVersion) { return resolvedVersion; @@ -1796,19 +1968,19 @@ async function resolveInstallerTargetVersion(params) { return latestVersion; } -function powerShellSingleQuote(value) { +function powerShellSingleQuote(value: string) { return value.replace(/'/gu, "''"); } -function readPackageJson(packageRoot) { - return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")); +function readPackageJson(packageRoot: string): PackageJson { + return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as PackageJson; } -function packageJsonHasScript(packageJson, scriptName) { +function packageJsonHasScript(packageJson: PackageJson, scriptName: string) { return typeof packageJson?.scripts?.[scriptName] === "string"; } -export function packageHasScript(packageRoot, scriptName) { +export function packageHasScript(packageRoot: string, scriptName: string) { try { return packageJsonHasScript(readPackageJson(packageRoot), scriptName); } catch { @@ -1816,28 +1988,28 @@ export function packageHasScript(packageRoot, scriptName) { } } -function parseMarkerLine(output, marker) { - return `${output}` +function parseMarkerLine(output: string, marker: string) { + return output .split(/\r?\n/gu) .find((line) => line.startsWith(marker)) ?.slice(marker.length) .trim(); } -export function normalizeWindowsInstalledCliPath(cliPath) { +export function normalizeWindowsInstalledCliPath(cliPath: string) { return normalizeWindowsCommandShimPath(cliPath); } -export function normalizeWindowsCommandShimPath(commandPath) { +export function normalizeWindowsCommandShimPath(commandPath: string) { if (typeof commandPath !== "string") { return commandPath; } return commandPath.replace(/\.ps1$/iu, ".cmd"); } -export function resolveInstalledPrefixDirFromCliPath(cliPath, platform = process.platform) { +export function resolveInstalledPrefixDirFromCliPath(cliPath: string, platform = process.platform) { const resolvedCliPath = - platform === "win32" ? normalizeWindowsInstalledCliPath(cliPath) : String(cliPath ?? ""); + platform === "win32" ? normalizeWindowsInstalledCliPath(cliPath) : cliPath; if (!resolvedCliPath?.trim()) { throw new Error("Missing installed CLI path."); } @@ -1847,16 +2019,16 @@ export function resolveInstalledPrefixDirFromCliPath(cliPath, platform = process return dirname(dirname(resolvedCliPath)); } -function readInstalledMetadataFromCliPath(cliPath, platform = process.platform) { +function readInstalledMetadataFromCliPath(cliPath: string, platform = process.platform) { return readInstalledMetadataFromPackageRoot( resolveInstalledPackageRootFromCliPath(cliPath, platform), ); } export function resolveCommandSpawnInvocation( - command, - args, - options = { + command: string, + args: string[], + options: { platform?: NodeJS.Platform; comSpec?: string; env?: NodeJS.ProcessEnv } = { platform: process.platform, }, ) { @@ -1873,9 +2045,9 @@ export function resolveCommandSpawnInvocation( } export function resolveInstalledCliInvocation( - cliPath, - args = [], - options = { + cliPath: string, + args: string[] = [], + options: { platform?: NodeJS.Platform; comSpec?: string; env?: NodeJS.ProcessEnv } = { platform: process.platform, }, ) { @@ -1904,11 +2076,11 @@ export function resolveInstalledCliInvocation( }); } -async function runPosixShellScript(script, options) { +async function runPosixShellScript(script: string, options: CommandOptions) { return runCommand("/bin/bash", ["-lc", script], options); } -async function runPowerShellScript(script, options) { +async function runPowerShellScript(script: string, options: CommandOptions) { return runCommand( "powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], @@ -1916,7 +2088,13 @@ async function runPowerShellScript(script, options) { ); } -async function runInstallerSmoke(params) { +async function runInstallerSmoke(params: { + lane: LaneState; + env: NodeJS.ProcessEnv; + installerUrl: string; + installTarget: string; + logPath: string; +}) { if (process.platform === "win32") { const script = ` $response = Invoke-WebRequest -UseBasicParsing '${powerShellSingleQuote(params.installerUrl)}' @@ -1947,7 +2125,9 @@ if ($content -is [byte[]]) { }); } -export function buildWindowsPathBootstrapScript(options = {}) { +export function buildWindowsPathBootstrapScript( + options: { includeCurrentProcessPath?: boolean } = {}, +) { const includeCurrentProcessPath = options.includeCurrentProcessPath !== false; const pathCandidates = includeCurrentProcessPath ? "@($userPath, $machinePath, $env:Path)" @@ -1970,7 +2150,7 @@ $env:Path = [string]::Join(';', $segments) `.trim(); } -export function buildWindowsFreshShellVersionCheckScript(params = {}) { +export function buildWindowsFreshShellVersionCheckScript(params: { expectedNeedle?: string } = {}) { const expectedNeedle = powerShellSingleQuote(params.expectedNeedle ?? ""); return ` ${buildWindowsPathBootstrapScript()} @@ -2055,7 +2235,12 @@ throw 'Neither pnpm, corepack, nor npm is discoverable from the reconstructed Wi `.trim(); } -async function verifyFreshShellCommand(params) { +async function verifyFreshShellCommand(params: { + lane: LaneState; + env: NodeJS.ProcessEnv; + expectedNeedle: string; + logPath: string; +}) { if (process.platform === "win32") { const script = buildWindowsFreshShellVersionCheckScript({ expectedNeedle: params.expectedNeedle, @@ -2067,7 +2252,7 @@ async function verifyFreshShellCommand(params) { timeoutMs: 2 * 60 * 1000, }); const cliPath = normalizeWindowsInstalledCliPath( - parseMarkerLine(result.stdout, "__OPENCLAW_PATH__="), + parseMarkerLine(result.stdout, "__OPENCLAW_PATH__=") ?? "", ); if (!cliPath) { throw new Error("Failed to resolve installed openclaw path from fresh Windows shell."); @@ -2104,7 +2289,15 @@ async function verifyFreshShellCommand(params) { return { cliPath, versionOutput }; } -async function runInstalledCli(params) { +async function runInstalledCli(params: { + cliPath: string; + args: string[]; + cwd: string; + env: NodeJS.ProcessEnv; + logPath: string; + timeoutMs?: number; + check?: boolean; +}) { const invocation = resolveInstalledCliInvocation(params.cliPath, params.args, { env: params.env, platform: process.platform, @@ -2118,7 +2311,12 @@ async function runInstalledCli(params) { }); } -async function readInstalledUpdateStatus(params) { +async function readInstalledUpdateStatus(params: { + cliPath: string; + cwd: string; + env: NodeJS.ProcessEnv; + logPath: string; +}) { return runInstalledCli({ cliPath: params.cliPath, args: ["update", "status", "--json"], @@ -2129,7 +2327,13 @@ async function readInstalledUpdateStatus(params) { }); } -async function ensureDevUpdateGitInstall(params) { +async function ensureDevUpdateGitInstall(params: { + lane: LaneState; + env: NodeJS.ProcessEnv; + cliPath: string; + logsDir: string; + requestedRef: string; +}) { const updateStatus = await readInstalledUpdateStatus({ cliPath: params.cliPath, cwd: params.lane.homeDir, @@ -2143,7 +2347,14 @@ async function ensureDevUpdateGitInstall(params) { return { cliPath: params.cliPath }; } -async function runOnboardWithInstalledCli(params) { +async function runOnboardWithInstalledCli(params: { + lane: LaneState; + cliPath: string; + env: NodeJS.ProcessEnv; + providerConfig: ProviderConfig; + installDaemon: boolean; + logPath: string; +}) { await withAllocatedGatewayPort(params.lane, async () => { const args = buildReleaseOnboardArgs({ authChoice: params.providerConfig.authChoice, @@ -2162,8 +2373,13 @@ async function runOnboardWithInstalledCli(params) { }); } -export function buildReleaseOnboardArgs(params) { - const args = [ +export function buildReleaseOnboardArgs(params: { + authChoice: string; + gatewayPort: number; + installDaemon?: boolean; + skipHealth?: boolean; +}) { + const args: string[] = [ "onboard", "--non-interactive", "--mode", @@ -2190,7 +2406,12 @@ export function buildReleaseOnboardArgs(params) { return args; } -async function startManualGatewayFromInstalledCli(params) { +async function startManualGatewayFromInstalledCli(params: { + lane: LaneState; + cliPath: string; + env: NodeJS.ProcessEnv; + logPath: string; +}): Promise { mkdirSync(dirname(params.logPath), { recursive: true }); const gatewayLog = createWriteStream(params.logPath, { flags: "a" }); const invocation = resolveInstalledCliInvocation( @@ -2221,7 +2442,7 @@ async function startManualGatewayFromInstalledCli(params) { return; } logClosed = true; - await new Promise((resolvePromise) => { + await new Promise((resolvePromise) => { gatewayLog.once("error", () => resolvePromise()); gatewayLog.end(() => resolvePromise()); }); @@ -2235,7 +2456,13 @@ async function startManualGatewayFromInstalledCli(params) { return { child, closeLog, logPath: params.logPath }; } -async function resolveInstalledGatewayStatusArgs(params) { +async function resolveInstalledGatewayStatusArgs(params: { + cliPath: string; + cwd: string; + env: NodeJS.ProcessEnv; + logPath: string; + requireRpc?: boolean; +}) { const requireRpc = params.requireRpc !== false; try { const help = await runInstalledCli({ @@ -2254,7 +2481,10 @@ async function resolveInstalledGatewayStatusArgs(params) { } } -export function buildGatewayStatusArgsFromHelpText(helpText, options = {}) { +export function buildGatewayStatusArgsFromHelpText( + helpText: string, + options: { requireRpc?: boolean } = {}, +) { const requireRpc = options.requireRpc !== false; if (requireRpc && helpText.includes("--require-rpc")) { return [ @@ -2268,24 +2498,24 @@ export function buildGatewayStatusArgsFromHelpText(helpText, options = {}) { return ["gateway", "status"]; } -function appendGatewayStatusHelpProbeFallback(logPath, error) { +function appendGatewayStatusHelpProbeFallback(logPath: string, error: unknown) { appendFileSync( logPath, `${new Date().toISOString()} gateway status help probe failed; assuming current --require-rpc support: ${formatError(error)}\n`, ); } -export async function canConnectToLoopbackPort(port, timeoutMs = 1_000) { +export async function canConnectToLoopbackPort(port: number, timeoutMs = 1_000) { if (!Number.isInteger(port) || port <= 0 || port > 65535) { return false; } - return await new Promise((resolvePromise) => { + return await new Promise((resolvePromise) => { let settled = false; const socket = createNetConnection({ host: "127.0.0.1", port, }); - const settle = (value) => { + const settle = (value: boolean) => { if (settled) { return; } @@ -2300,7 +2530,12 @@ export async function canConnectToLoopbackPort(port, timeoutMs = 1_000) { }); } -async function waitForInstalledGateway(params) { +async function waitForInstalledGateway(params: { + lane: LaneState; + cliPath: string; + env: NodeJS.ProcessEnv; + logPath: string; +}) { const statusArgs = await resolveInstalledGatewayStatusArgs({ cliPath: params.cliPath, cwd: params.lane.homeDir, @@ -2326,7 +2561,12 @@ async function waitForInstalledGateway(params) { throw new Error(`Gateway did not become ready on port ${params.lane.gatewayPort}.`); } -async function waitForInstalledGatewayToStop(params) { +async function waitForInstalledGatewayToStop(params: { + lane: LaneState; + cliPath: string; + env: NodeJS.ProcessEnv; + logPath: string; +}) { const statusArgs = await resolveInstalledGatewayStatusArgs({ cliPath: params.cliPath, cwd: params.lane.homeDir, @@ -2356,7 +2596,12 @@ async function waitForInstalledGatewayToStop(params) { ); } -async function ensureManagedGatewayReady(params) { +async function ensureManagedGatewayReady(params: { + lane: LaneState; + cliPath: string; + env: NodeJS.ProcessEnv; + logPath: string; +}) { try { await waitForInstalledGateway(params); return; @@ -2374,7 +2619,13 @@ async function ensureManagedGatewayReady(params) { await waitForInstalledGateway(params); } -async function runInstalledModelsSet(params) { +async function runInstalledModelsSet(params: { + cliPath: string; + cwd: string; + env: NodeJS.ProcessEnv; + providerConfig: ProviderConfig; + logPath: string; +}) { await runInstalledCli({ cliPath: params.cliPath, args: ["models", "set", params.providerConfig.model], @@ -2441,7 +2692,13 @@ async function runInstalledModelsSet(params) { }); } -async function runInstalledAgentTurn(params) { +async function runInstalledAgentTurn(params: { + cliPath: string; + cwd: string; + env: NodeJS.ProcessEnv; + label: string; + logPath: string; +}): Promise { let lastError; for (let attempt = 1; attempt <= 2; attempt += 1) { const sessionId = buildCrossOsReleaseAgentSessionId(params.label, attempt); @@ -2486,10 +2743,15 @@ async function runInstalledAgentTurn(params) { throw lastError; } -export function verifyDevUpdateStatus(stdout, options = {}) { +export function verifyDevUpdateStatus(stdout: string, options: { ref?: string } = {}) { let payload; try { - payload = JSON.parse(stdout); + payload = JSON.parse(stdout) as { + update?: { installKind?: string; git?: { branch?: string; sha?: string } }; + channel?: { value?: string; channel?: string }; + installKind?: string; + git?: { branch?: string; sha?: string }; + }; } catch { payload = null; } @@ -2526,7 +2788,11 @@ export function verifyDevUpdateStatus(stdout, options = {}) { } } -async function verifyWindowsDevUpdateToolchain(params) { +async function verifyWindowsDevUpdateToolchain(params: { + lane: LaneState; + env: NodeJS.ProcessEnv; + logPath: string; +}) { const script = buildWindowsDevUpdateToolchainCheckScript(); const result = await runPowerShellScript(script, { cwd: params.lane.homeDir, @@ -2541,7 +2807,7 @@ async function verifyWindowsDevUpdateToolchain(params) { } } -export function buildDiscordSmokeGuildsConfig(guildId, channelId) { +export function buildDiscordSmokeGuildsConfig(guildId: string, channelId: string) { return { [guildId]: { channels: { @@ -2554,7 +2820,17 @@ export function buildDiscordSmokeGuildsConfig(guildId, channelId) { }; } -async function configureDiscordSmoke(params) { +async function configureDiscordSmoke(params: { + lane: LaneState; + cliPath: string; + cwd: string; + env: NodeJS.ProcessEnv; + gatewayHolder?: { current: GatewayHandle | null }; + logPath: string; + token: string; + guildId: string; + channelId: string; +}) { const guildsJson = JSON.stringify( buildDiscordSmokeGuildsConfig(params.guildId, params.channelId), ); @@ -2765,7 +3041,7 @@ export async function verifyDashboardAssetUrls( return { failures, ok: failures.length === 0 }; } -async function waitForDiscordMessage(params) { +async function waitForDiscordMessage(params: { token: string; channelId: string; needle: string }) { const deadline = Date.now() + 3 * 60 * 1000; while (Date.now() < deadline) { let response; @@ -2793,18 +3069,24 @@ async function waitForDiscordMessage(params) { throw new Error(`Discord host-side visibility check timed out for ${params.needle}.`); } -export function buildDiscordFetchInit(token, init = {}) { +export function buildDiscordFetchInit( + token: string, + init: RequestInit = {}, +): RequestInit & { signal: AbortSignal } { + const headers = new Headers(init.headers); + headers.set("Authorization", `Bot ${token}`); return { ...init, signal: init.signal ?? AbortSignal.timeout(CROSS_OS_DISCORD_FETCH_TIMEOUT_MS), - headers: { - ...init.headers, - Authorization: `Bot ${token}`, - }, + headers, }; } -async function postDiscordMessage(params) { +async function postDiscordMessage(params: { + token: string; + channelId: string; + content: string; +}): Promise { const init = buildDiscordFetchInit(params.token, { method: "POST", headers: { @@ -2824,13 +3106,18 @@ async function postDiscordMessage(params) { throw new Error(`Failed to post Discord smoke message: ${text}`); } try { - return JSON.parse(text)?.id ?? null; + const payload = JSON.parse(text) as { id?: string }; + return payload.id ?? null; } catch { return null; } } -export async function deleteDiscordMessage(params) { +export async function deleteDiscordMessage(params: { + token: string; + channelId: string; + messageId: string | null; +}) { if (!params.messageId) { return; } @@ -2847,7 +3134,14 @@ export async function deleteDiscordMessage(params) { } } -async function waitForInstalledDiscordReadback(params) { +async function waitForInstalledDiscordReadback(params: { + cliPath: string; + cwd: string; + env: NodeJS.ProcessEnv; + logPath: string; + channelId: string; + needle: string; +}) { const deadline = Date.now() + 3 * 60 * 1000; while (Date.now() < deadline) { const response = await runInstalledCli({ @@ -2877,7 +3171,13 @@ async function waitForInstalledDiscordReadback(params) { throw new Error(`Discord guest readback timed out for ${params.needle}.`); } -async function maybeRunDiscordRoundtrip(params) { +async function maybeRunDiscordRoundtrip(params: { + lane: LaneState; + cliPath: string; + env: NodeJS.ProcessEnv; + gatewayHolder: { current: GatewayHandle | null }; + logPath: string; +}) { const token = process.env.OPENCLAW_DISCORD_SMOKE_BOT_TOKEN?.trim() || process.env.DISCORD_BOT_TOKEN?.trim() || @@ -2889,8 +3189,8 @@ async function maybeRunDiscordRoundtrip(params) { } const { outboundNonce, inboundNonce } = buildCrossOsDiscordRoundtripNonces(); - let sentMessageId = null; - let hostMessageId = null; + let sentMessageId: string | null = null; + let hostMessageId: string | null = null; try { await configureDiscordSmoke({ lane: params.lane, @@ -2923,9 +3223,13 @@ async function maybeRunDiscordRoundtrip(params) { logPath: params.logPath, timeoutMs: 2 * 60 * 1000, }); - let parsedSendResult = null; + let parsedSendResult: { + payload?: { messageId?: string; result?: { messageId?: string } }; + } | null; try { - parsedSendResult = JSON.parse(sendResult.stdout); + parsedSendResult = JSON.parse(sendResult.stdout) as { + payload?: { messageId?: string; result?: { messageId?: string } }; + }; } catch { parsedSendResult = null; } @@ -2956,7 +3260,15 @@ async function maybeRunDiscordRoundtrip(params) { } } -async function installTarballPackage(params) { +async function installTarballPackage(params: { + lane: LaneState; + env: NodeJS.ProcessEnv; + tgzPath: string; + logPath: string; + timeoutMs?: number; + ignoreScripts?: boolean; + restoreBundledPluginPostinstall?: boolean; +}) { await installPackageSpec({ lane: params.lane, env: params.env, @@ -2977,7 +3289,14 @@ async function installTarballPackage(params) { } } -async function installPackageSpec(params) { +async function installPackageSpec(params: { + lane: LaneState; + env: NodeJS.ProcessEnv; + packageSpec: string; + logPath: string; + timeoutMs?: number; + ignoreScripts?: boolean; +}) { const installEnv = { ...params.env, npm_config_global: "true", @@ -3006,8 +3325,8 @@ async function installPackageSpec(params) { } export function appendLatestNpmDebugLogTail( - homeDir, - logPath, + homeDir: string, + logPath: string, env = process.env, platform = process.platform, ) { @@ -3036,7 +3355,11 @@ export function appendLatestNpmDebugLogTail( } } -export function resolveNpmDebugLogDirs(homeDir, env = process.env, platform = process.platform) { +export function resolveNpmDebugLogDirs( + homeDir: string, + env: NodeJS.ProcessEnv = process.env, + platform: NodeJS.Platform = process.platform, +) { const configuredLogsDir = resolveNpmConfiguredPath( homeDir, env.npm_config_logs_dir ?? env.NPM_CONFIG_LOGS_DIR, @@ -3057,19 +3380,23 @@ export function resolveNpmDebugLogDirs(homeDir, env = process.env, platform = pr return [...new Set(logDirs)]; } -function resolveNpmConfiguredPath(homeDir, value, platform) { - const raw = String(value ?? "").trim(); +function resolveNpmConfiguredPath( + homeDir: string, + value: string | undefined, + platform: NodeJS.Platform, +) { + const raw = (value ?? "").trim(); if (!raw) { return ""; } return platform === "win32" ? pathWin32.resolve(homeDir, raw) : resolve(homeDir, raw); } -function normalizeNpmCacheLogDir(logDir) { +function normalizeNpmCacheLogDir(logDir: string) { return logDir.endsWith("/_logs") || logDir.endsWith("\\_logs") ? logDir : join(logDir, "_logs"); } -function findNpmDebugLogs(logsDir) { +function findNpmDebugLogs(logsDir: string) { if (!existsSync(logsDir)) { return []; } @@ -3090,7 +3417,10 @@ function findNpmDebugLogs(logsDir) { .toSorted((left, right) => left.mtimeMs - right.mtimeMs); } -export function buildNpmGlobalInstallArgs(packageSpec, options = {}) { +export function buildNpmGlobalInstallArgs( + packageSpec: string, + options: { ignoreScripts?: boolean } = {}, +) { return [ "install", "-g", @@ -3119,7 +3449,7 @@ function updateStepTimeoutSeconds() { : 1200; } -async function runBundledPluginPostinstall(params) { +async function runBundledPluginPostinstall(params: LaneCommandParams) { const packageRoot = installedPackageRoot(params.lane.prefixDir); const scriptPath = join(packageRoot, "scripts", "postinstall-bundled-plugins.mjs"); if (!existsSync(scriptPath)) { @@ -3204,7 +3534,9 @@ export async function stopBrowserControlService() { `.trim(); } -async function runInstalledBrowserOverrideImportSmoke(params) { +async function runInstalledBrowserOverrideImportSmoke( + params: LaneCommandParams & { prefixDir: string }, +) { if (!shouldRunWindowsInstalledBrowserOverrideImportSmoke()) { return "skipped"; } @@ -3247,7 +3579,7 @@ async function runInstalledBrowserOverrideImportSmoke(params) { return "pass"; } -function ensureLocalNpmShim(lane) { +function ensureLocalNpmShim(lane: LaneState) { const shimPath = npmShimPath(lane.prefixDir); if (existsSync(shimPath)) { return; @@ -3273,7 +3605,7 @@ function ensureLocalNpmShim(lane) { chmodSync(shimPath, 0o755); } -async function runOnboard(params) { +async function runOnboard(params: LaneCommandParams & { providerConfig: ProviderConfig }) { await withAllocatedGatewayPort(params.lane, async () => { await runOpenClaw({ lane: params.lane, @@ -3289,7 +3621,9 @@ async function runOnboard(params) { }); } -async function exerciseManagedGatewayLifecycle(params) { +async function exerciseManagedGatewayLifecycle( + params: Pick & { cliPath: string; logPrefix: string }, +) { logLanePhase(params.lane, "gateway-ready"); await ensureManagedGatewayReady({ lane: params.lane, @@ -3341,7 +3675,7 @@ async function exerciseManagedGatewayLifecycle(params) { }); } -async function startGateway(params) { +async function startGateway(params: LaneCommandParams): Promise { const gatewayLog = createWriteStream(params.logPath, { flags: "a" }); const useProcessGroup = process.platform !== "win32"; const child = spawn( @@ -3377,7 +3711,7 @@ async function startGateway(params) { return; } logClosed = true; - await new Promise((resolvePromise) => { + await new Promise((resolvePromise) => { gatewayLog.once("error", () => resolvePromise()); gatewayLog.end(() => resolvePromise()); }); @@ -3393,7 +3727,7 @@ async function startGateway(params) { return { child, closeLog, logPath: params.logPath }; } -async function waitForGateway(params) { +async function waitForGateway(params: LaneCommandParams) { const statusArgs = await resolveGatewayStatusArgs(params.lane, params.env, params.logPath); const deadline = Date.now() + gatewayReadyDeadlineMs(); while (Date.now() < deadline) { @@ -3425,7 +3759,7 @@ function gatewayReadyDeadlineMs() { : CROSS_OS_GATEWAY_READY_TIMEOUT_MS; } -async function resolveGatewayStatusArgs(lane, env, logPath) { +async function resolveGatewayStatusArgs(lane: LaneState, env: NodeJS.ProcessEnv, logPath: string) { try { const help = await runOpenClaw({ lane, @@ -3442,7 +3776,7 @@ async function resolveGatewayStatusArgs(lane, env, logPath) { } } -async function runModelsSet(params) { +async function runModelsSet(params: LaneCommandParams & { providerConfig: ProviderConfig }) { await runOpenClaw({ lane: params.lane, env: params.env, @@ -3503,7 +3837,9 @@ async function runModelsSet(params) { }); } -async function runAgentTurn(params) { +async function runAgentTurn( + params: LaneCommandParams & { label: string }, +): Promise { let lastError; for (let attempt = 1; attempt <= 2; attempt += 1) { const sessionId = buildCrossOsReleaseAgentSessionId(params.label, attempt); @@ -3547,7 +3883,11 @@ async function runAgentTurn(params) { throw lastError; } -export function maybeBuildOptionalAgentTurnSkipResult(error, logPath, options = {}) { +export function maybeBuildOptionalAgentTurnSkipResult( + error: unknown, + logPath: string, + options: { attempt?: number; maxAttempts?: number; optional?: boolean } = {}, +) { const attempt = options.attempt ?? 1; const maxAttempts = options.maxAttempts ?? 2; const optional = options.optional ?? CROSS_OS_AGENT_TURN_OPTIONAL; @@ -3573,7 +3913,7 @@ export function maybeBuildOptionalAgentTurnSkipResult(error, logPath, options = }; } -export function shouldSkipOptionalCrossOsAgentTurnError(error, logPath) { +export function shouldSkipOptionalCrossOsAgentTurnError(error: unknown, logPath: string) { const message = error instanceof Error ? error.message : String(error); if ( /model idle timeout|did not produce a response before the model idle timeout|gateway request timeout for agent|Command timed out|timed out and could not be terminated cleanly/u.test( @@ -3589,7 +3929,7 @@ export function shouldSkipOptionalCrossOsAgentTurnError(error, logPath) { return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log); } -export function buildCrossOsReleaseAgentSessionId(label, attempt) { +export function buildCrossOsReleaseAgentSessionId(label: string, attempt: number) { return `cross-os-release-check-${label}-${randomUUID()}-${attempt}`; } @@ -3600,7 +3940,7 @@ export function buildCrossOsDiscordRoundtripNonces() { }; } -function buildReleaseAgentTurnArgs(sessionId) { +function buildReleaseAgentTurnArgs(sessionId: string) { return [ "agent", "--agent", @@ -3617,14 +3957,17 @@ function buildReleaseAgentTurnArgs(sessionId) { ]; } -export function shouldRetryCrossOsAgentTurnError(error) { +export function shouldRetryCrossOsAgentTurnError(error: unknown) { const message = error instanceof Error ? error.message : String(error); return /Agent output did not contain the expected OK marker|Agent turn used embedded fallback instead of gateway|model idle timeout|did not produce a response before the model idle timeout|gateway request timeout for agent|Command timed out|timed out and could not be terminated cleanly|rate limit reached|rate_limit_exceeded|HTTP 429|HTTP 503|upstream connect error|disconnect\/reset before headers|connection timeout/u.test( message, ); } -export function agentTurnUsedEmbeddedFallback(result, options = {}) { +export function agentTurnUsedEmbeddedFallback( + result: Pick, + options: AgentOutputOptions = {}, +) { const logText = typeof options.logText === "string" ? options.logText @@ -3634,7 +3977,7 @@ export function agentTurnUsedEmbeddedFallback(result, options = {}) { return /EMBEDDED FALLBACK:/u.test(`${result.stdout ?? ""}\n${result.stderr ?? ""}\n${logText}`); } -export function agentOutputHasExpectedOkMarker(stdout, options = {}) { +export function agentOutputHasExpectedOkMarker(stdout: string, options: AgentOutputOptions = {}) { const payloadTexts = parseAgentPayloadTexts(stdout); if (payloadTexts.some((text) => text.trim() === "OK")) { return true; @@ -3650,7 +3993,7 @@ export function agentOutputHasExpectedOkMarker(stdout, options = {}) { return logTexts.some((text) => text.trim() === "OK"); } -function readLogFileSize(logPath) { +function readLogFileSize(logPath: string) { try { return statSync(logPath).size; } catch { @@ -3658,20 +4001,23 @@ function readLogFileSize(logPath) { } } -function readLogTextSince(logPath, offsetBytes) { +function readLogTextSince(logPath: string, offsetBytes: number) { return readLogTextWindow(logPath, { offsetBytes, maxBytes: CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES, }); } -function readLogTextTail(logPath) { +function readLogTextTail(logPath: string) { return readLogTextWindow(logPath, { maxBytes: CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES, }); } -function readLogTextWindow(logPath, options = {}) { +function readLogTextWindow( + logPath: string, + options: { maxBytes?: number; offsetBytes?: number } = {}, +) { const maxBytes = Math.max( 1, Math.floor(options.maxBytes ?? CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES), @@ -3707,9 +4053,17 @@ function readLogTextWindow(logPath, options = {}) { } } -function parseAgentPayloadTexts(stdout) { +function parseAgentPayloadTexts(stdout: string) { try { - const payload = JSON.parse(stdout); + type AgentPayload = { + text?: string; + finalAssistantVisibleText?: string; + finalAssistantRawText?: string; + meta?: AgentPayload; + result?: AgentPayload; + payloads?: AgentPayload[]; + }; + const payload = JSON.parse(stdout) as AgentPayload; const directTexts = [ payload?.finalAssistantVisibleText, payload?.finalAssistantRawText, @@ -3739,7 +4093,7 @@ function parseAgentPayloadTexts(stdout) { } } -async function runDashboardSmoke(params) { +async function runDashboardSmoke(params: Pick) { const dashboardUrl = `http://127.0.0.1:${params.lane.gatewayPort}/`; const logStream = createWriteStream(params.logPath, { flags: "a" }); const deadline = Date.now() + CROSS_OS_DASHBOARD_SMOKE_TIMEOUT_MS; @@ -3784,11 +4138,11 @@ async function runDashboardSmoke(params) { throw new Error(`Dashboard HTML did not become ready at ${dashboardUrl}.`); } -function hasChildExited(child) { +function hasChildExited(child: ChildProcess) { return child.exitCode !== null || (child.signalCode ?? null) !== null; } -async function stopGateway(gateway) { +async function stopGateway(gateway: GatewayHandle | null) { try { if (!gateway?.child?.pid) { return; @@ -3827,7 +4181,7 @@ async function stopGateway(gateway) { } } -function signalChildProcessTree(child, signal) { +function signalChildProcessTree(child: ChildProcess, signal: NodeJS.Signals) { if (process.platform !== "win32" && child.pid) { try { process.kill(-child.pid, signal); @@ -3839,8 +4193,8 @@ function signalChildProcessTree(child, signal) { child.kill(signal); } -function registerActiveChildProcessTree(child) { - const killChildTree = (signal) => signalChildProcessTree(child, signal); +function registerActiveChildProcessTree(child: ChildProcess) { + const killChildTree = (signal: NodeJS.Signals) => signalChildProcessTree(child, signal); CROSS_OS_ACTIVE_CHILD_TREE_KILLERS.add(killChildTree); return { killChildTree, @@ -3850,13 +4204,13 @@ function registerActiveChildProcessTree(child) { }; } -async function waitForChildExit(child, timeoutMs) { +async function waitForChildExit(child: ChildProcess, timeoutMs: number) { if (hasChildExited(child)) { return true; } - return new Promise((resolvePromise) => { + return new Promise((resolvePromise) => { let settled = false; - const finish = (didExit) => { + const finish = (didExit: boolean) => { if (settled) { return; } @@ -3885,7 +4239,7 @@ async function waitForChildExit(child, timeoutMs) { }); } -async function runCleanup(cleanupFns) { +async function runCleanup(cleanupFns: Cleanup[]) { for (const cleanupFn of cleanupFns.toReversed()) { try { await cleanupFn(); @@ -3895,7 +4249,14 @@ async function runCleanup(cleanupFns) { } } -async function runOpenClaw(params) { +async function runOpenClaw(params: { + lane: LaneState; + args: string[]; + env: NodeJS.ProcessEnv; + logPath: string; + timeoutMs?: number; + check?: boolean; +}) { return runCommand(process.execPath, [installedEntryPath(params.lane.prefixDir), ...params.args], { cwd: params.lane.homeDir, env: params.env, @@ -3905,38 +4266,36 @@ async function runOpenClaw(params) { }); } -function readInstalledPackageManifest(prefixDir) { +function readInstalledPackageManifest(prefixDir: string) { const packageRoot = installedPackageRoot(prefixDir); return readInstalledPackageManifestFromPackageRoot(packageRoot); } -function readInstalledPackageManifestFromPackageRoot(packageRoot) { +function readInstalledPackageManifestFromPackageRoot(packageRoot: string) { const packageJsonPath = join(packageRoot, "package.json"); if (!existsSync(packageJsonPath)) { throw new Error(`Installed package manifest missing: ${packageJsonPath}`); } - const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { - version?: unknown; - }; + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as PackageJson; return { packageJson, packageRoot }; } -export function readInstalledVersion(prefixDir) { +export function readInstalledVersion(prefixDir: string) { const { packageJson } = readInstalledPackageManifest(prefixDir); return typeof packageJson.version === "string" ? packageJson.version.trim() : ""; } -function readInstalledMetadata(prefixDir) { +function readInstalledMetadata(prefixDir: string) { const { packageJson, packageRoot } = readInstalledPackageManifest(prefixDir); return readInstalledMetadataFromManifest(packageJson, packageRoot); } -function readInstalledMetadataFromPackageRoot(packageRoot) { +function readInstalledMetadataFromPackageRoot(packageRoot: string) { const { packageJson } = readInstalledPackageManifestFromPackageRoot(packageRoot); return readInstalledMetadataFromManifest(packageJson, packageRoot); } -function readInstalledMetadataFromManifest(packageJson, packageRoot) { +function readInstalledMetadataFromManifest(packageJson: PackageJson, packageRoot: string) { const buildInfoPath = join(packageRoot, "dist", "build-info.json"); if (!existsSync(buildInfoPath)) { throw new Error(`Installed build info missing: ${buildInfoPath}`); @@ -3950,7 +4309,10 @@ function readInstalledMetadataFromManifest(packageJson, packageRoot) { }; } -function verifyInstalledCandidate(installed, build) { +function verifyInstalledCandidate( + installed: { version: string; commit: string }, + build: CandidateBuild, +) { if (installed.version !== build.candidateVersion) { throw new Error( `Installed version mismatch. Expected ${build.candidateVersion}, found ${installed.version || ""}.`, @@ -3964,7 +4326,7 @@ function verifyInstalledCandidate(installed, build) { } export function resolveInstalledPackageRootFromCliPath( - cliPath, + cliPath: string, platform = process.platform, env = process.env, ) { @@ -3972,7 +4334,7 @@ export function resolveInstalledPackageRootFromCliPath( const candidates = [installedPackageRoot(prefixDir, platform)]; if (platform !== "win32") { - const resolvedCliPath = String(cliPath ?? "").trim(); + const resolvedCliPath = cliPath.trim(); if (resolvedCliPath) { try { const realCliPath = realpathSync(resolvedCliPath); @@ -4010,21 +4372,21 @@ export function resolveInstalledPackageRootFromCliPath( throw new Error(`Installed package manifest missing. Checked: ${checked.join(", ")}`); } -function installedPackageRoot(prefixDir, platform = process.platform) { +function installedPackageRoot(prefixDir: string, platform = process.platform) { return platform === "win32" ? join(prefixDir, "node_modules", "openclaw") : join(prefixDir, "lib", "node_modules", "openclaw"); } -function installedEntryPath(prefixDir) { +function installedEntryPath(prefixDir: string) { return join(installedPackageRoot(prefixDir), "openclaw.mjs"); } -function npmShimPath(prefixDir) { +function npmShimPath(prefixDir: string) { return process.platform === "win32" ? join(prefixDir, "npm.cmd") : join(prefixDir, "bin", "npm"); } -function binDirForPrefix(prefixDir) { +function binDirForPrefix(prefixDir: string) { return process.platform === "win32" ? prefixDir : join(prefixDir, "bin"); } @@ -4040,7 +4402,7 @@ function gitCommand() { return process.platform === "win32" ? "git.exe" : "git"; } -function resolveCommandCaptureLimit(options) { +function resolveCommandCaptureLimit(options: CommandOptions) { const value = options.maxOutputBytes; if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { return CROSS_OS_COMMAND_CAPTURE_TAIL_BYTES; @@ -4048,7 +4410,7 @@ function resolveCommandCaptureLimit(options) { return Math.max(1, Math.floor(value)); } -function appendBoundedCommandOutput(current, chunk, maxBytes) { +function appendBoundedCommandOutput(current: string, chunk: Uint8Array | string, maxBytes: number) { const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); if (chunkBuffer.byteLength >= maxBytes) { return chunkBuffer.subarray(chunkBuffer.byteLength - maxBytes).toString("utf8"); @@ -4065,7 +4427,11 @@ function appendBoundedCommandOutput(current, chunk, maxBytes) { return Buffer.concat([currentTail, chunkBuffer], maxBytes).toString("utf8"); } -export async function runCommand(command, args, options) { +export async function runCommand( + command: string, + args: string[], + options: CommandOptions, +): Promise { const invocation = resolveCommandSpawnInvocation(command, args, { env: options.env, platform: process.platform, @@ -4073,8 +4439,11 @@ export async function runCommand(command, args, options) { return runCommandInvocation(invocation, options); } -async function runCommandInvocation(invocation, options) { - return new Promise((resolvePromise, rejectPromise) => { +async function runCommandInvocation( + invocation: CommandInvocation, + options: CommandOptions, +): Promise { + return new Promise((resolvePromise, rejectPromise) => { const commandLabel = `${invocation.command} ${invocation.args.join(" ")}`; const useProcessGroup = process.platform !== "win32"; const child = spawn(invocation.command, invocation.args, { @@ -4093,9 +4462,9 @@ async function runCommandInvocation(invocation, options) { let timedOut = false; let settled = false; const startedAt = Date.now(); - let killWaitTimer = null; - let timer = null; - let heartbeatTimer = null; + let killWaitTimer: NodeJS.Timeout | null = null; + let timer: NodeJS.Timeout | null = null; + let heartbeatTimer: NodeJS.Timeout | null = null; const maxCapturedOutputBytes = resolveCommandCaptureLimit(options); const clearTimers = () => { @@ -4110,9 +4479,9 @@ async function runCommandInvocation(invocation, options) { } }; - const finishLogStream = (callback) => { + const finishLogStream = (callback: (error?: Error | null) => void) => { let completed = false; - const finish = (error) => { + const finish = (error?: Error | null) => { if (completed) { return; } @@ -4124,7 +4493,7 @@ async function runCommandInvocation(invocation, options) { logStream.end(); }; - const finalize = (callback) => { + const finalize = (callback: () => void) => { if (settled) { return; } @@ -4247,14 +4616,17 @@ async function runCommandInvocation(invocation, options) { }); } -export async function startStaticFileServer(params) { +export async function startStaticFileServer(params: { + filePath: string; + logPath: string; +}): Promise<{ url: string; close: () => Promise }> { mkdirSync(dirname(params.logPath), { recursive: true }); const logStream = createWriteStream(params.logPath, { flags: "a" }); - let logStreamError = null; + let logStreamError: Error | null = null; logStream.on("error", (error) => { logStreamError ??= error; }); - const fileName = String(params.filePath.split(/[/\\]/u).at(-1) ?? "artifact"); + const fileName = params.filePath.split(/[/\\]/u).at(-1) ?? "artifact"; const fileStat = statSync(params.filePath); const sockets = new Set(); const server = createServer((request, response) => { @@ -4287,20 +4659,20 @@ export async function startStaticFileServer(params) { sockets.delete(socket); }); }); - await new Promise((resolvePromise, rejectPromise) => { + await new Promise((resolvePromise, rejectPromise) => { server.once("error", rejectPromise); - server.listen(0, "127.0.0.1", resolvePromise); + server.listen(0, "127.0.0.1", () => resolvePromise()); }); const address = server.address(); if (!address || typeof address === "string") { throw new Error("Failed to bind static file server."); } const port = address.port; - let closePromise; + let closePromise: Promise | undefined; return { url: `http://127.0.0.1:${port}/${fileName}`, close: () => { - closePromise ??= new Promise((resolvePromise, rejectPromise) => { + closePromise ??= new Promise((resolvePromise, rejectPromise) => { closeStaticFileServerConnections(server, sockets); server.close((error) => { void (async () => { @@ -4330,7 +4702,7 @@ export async function startStaticFileServer(params) { }; } -function closeStaticFileServerConnections(server, sockets) { +function closeStaticFileServerConnections(server: Server, sockets: Set) { for (const socket of sockets) { socket.destroy(); } @@ -4339,8 +4711,8 @@ function closeStaticFileServerConnections(server, sockets) { } } -function finishStaticFileServerLog(logStream, pendingError) { - return new Promise((resolvePromise, rejectPromise) => { +function finishStaticFileServerLog(logStream: WriteStream, pendingError: Error | null) { + return new Promise((resolvePromise, rejectPromise) => { if (pendingError) { logStream.destroy(); rejectPromise(new Error(`Static file server log write failed: ${formatError(pendingError)}`)); @@ -4354,7 +4726,7 @@ function finishStaticFileServerLog(logStream, pendingError) { completed = true; resolvePromise(); }; - const fail = (error) => { + const fail = (error: unknown) => { if (completed) { return; } @@ -4367,7 +4739,7 @@ function finishStaticFileServerLog(logStream, pendingError) { }); } -export function resolveStaticFileContentType(filePath) { +export function resolveStaticFileContentType(filePath: string) { if (filePath.endsWith(".sh") || filePath.endsWith(".ps1")) { return "text/plain; charset=utf-8"; } @@ -4381,7 +4753,7 @@ export function resolvePublishedInstallerUrl(platform = process.platform) { return `${PUBLISHED_INSTALLER_BASE_URL}/install.sh`; } -function writeSummary(baseDir, summaryPayload) { +function writeSummary(baseDir: string, summaryPayload: SummaryPayload) { const summaryJsonPath = join(baseDir, "summary.json"); const summaryMarkdownPath = join(baseDir, "summary.md"); writeFileSync(summaryJsonPath, `${JSON.stringify(summaryPayload, null, 2)}\n`, "utf8"); @@ -4419,7 +4791,7 @@ function writeSummary(baseDir, summaryPayload) { writeFileSync(summaryMarkdownPath, `${lines.join("\n")}\n`, "utf8"); } -function writeCandidateManifest(baseDir, build) { +function writeCandidateManifest(baseDir: string, build: CandidateBuild) { const manifestPath = join(baseDir, "candidate.json"); writeFileSync( manifestPath, @@ -4446,7 +4818,7 @@ function platformLabel() { return "Linux Release Checks"; } -function requireArg(argsMap, key) { +function requireArg(argsMap: ParsedArgs, key: string) { const value = argsMap[key]?.trim(); if (!value) { throw new Error(`Missing required --${key} argument.`); @@ -4454,7 +4826,7 @@ function requireArg(argsMap, key) { return value; } -function resolveCommandPath(command) { +function resolveCommandPath(command: string) { const pathValue = process.env.PATH ?? ""; const pathEntries = pathValue.split(process.platform === "win32" ? ";" : ":").filter(Boolean); const candidates = @@ -4472,19 +4844,19 @@ function resolveCommandPath(command) { return null; } -function shellEscapeForSh(value) { +function shellEscapeForSh(value: string) { return value.replace(/'/gu, `'"'"'`); } -function logPhase(scope, phase) { +function logPhase(scope: string, phase: string) { process.stdout.write(`[release-checks] ${scope}: ${phase}\n`); } -function logLanePhase(lane, phase) { +function logLanePhase(lane: LaneState, phase: string) { logPhase(`lane.${lane.name}`, phase); } -async function runTimedLanePhase(lane, phase, callback) { +async function runTimedLanePhase(lane: LaneState, phase: string, callback: () => Promise) { const startedAt = Date.now(); logLanePhase(lane, phase); try { @@ -4501,7 +4873,7 @@ async function runTimedLanePhase(lane, phase, callback) { } } -function trimForSummary(value) { +function trimForSummary(value: string) { const trimmed = value.trim(); if (trimmed.length <= 600) { return trimmed; @@ -4509,20 +4881,20 @@ function trimForSummary(value) { return `${trimmed.slice(0, 600)}...`; } -function formatError(error) { +function formatError(error: unknown) { if (error instanceof Error) { return error.stack || error.message; } return String(error); } -function sleep(ms) { - return new Promise((resolvePromise) => { +function sleep(ms: number) { + return new Promise((resolvePromise) => { setTimeout(resolvePromise, ms); }); } -async function withAllocatedGatewayPort(lane, callback) { +async function withAllocatedGatewayPort(lane: LaneState, callback: () => Promise) { let lastError = null; for (let attempt = 1; attempt <= 3; attempt += 1) { const reservation = await reservePort(); @@ -4544,7 +4916,7 @@ async function withAllocatedGatewayPort(lane, callback) { ); } -function reservePort() { +function reservePort(): Promise<{ port: number; release: () => Promise }> { return new Promise((resolvePromise, rejectPromise) => { const server = createNetServer(); server.listen(0, "127.0.0.1", () => { @@ -4557,7 +4929,7 @@ function reservePort() { resolvePromise({ port: address.port, release: () => - new Promise((releaseResolve, releaseReject) => { + new Promise((releaseResolve, releaseReject) => { server.close((error) => { if (error) { releaseReject(error); @@ -4572,7 +4944,7 @@ function reservePort() { }); } -function isAddressInUseError(error) { +function isAddressInUseError(error: unknown) { const message = formatError(error); return message.includes("EADDRINUSE") || /address.+in use/iu.test(message); } diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 410b3f6569ba..4b670446cd76 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -328,7 +328,7 @@ export function runNpmReleaseCheckCommand( }, ): string { const env = options.env ?? process.env; - const output = execFileSync(invocation.command, invocation.args, { + const execOptions = { cwd: options.cwd, encoding: options.encoding, env, @@ -337,7 +337,11 @@ export function runNpmReleaseCheckCommand( stdio: options.stdio, timeout: options.timeoutMs ?? resolveNpmReleaseCheckCommandTimeoutMs(env), windowsVerbatimArguments: invocation.windowsVerbatimArguments, - }) as Buffer | string | null; + } as Parameters[2] & { windowsVerbatimArguments?: boolean }; + const output = execFileSync(invocation.command, invocation.args, execOptions) as + | Buffer + | string + | null; if (output == null) { return ""; } diff --git a/scripts/package-changelog.d.mts b/scripts/package-changelog.d.mts new file mode 100644 index 000000000000..c28c0d89be71 --- /dev/null +++ b/scripts/package-changelog.d.mts @@ -0,0 +1,16 @@ +type PackageChangelogOptions = { allowUnreleased?: boolean }; + +export function resolvePackageChangelogVersions( + packageVersion: string, + options?: PackageChangelogOptions, +): string[]; +export function extractCurrentPackageChangelog( + content: string, + packageVersion: string, + options?: PackageChangelogOptions, +): string; +export function restorePackageChangelog(cwd?: string): Promise; +export function preparePackageChangelog( + cwd?: string, + options?: PackageChangelogOptions, +): Promise; diff --git a/scripts/perf/issue-78851-model-resolution.ts b/scripts/perf/issue-78851-model-resolution.ts index 241765132815..ec5b1d88630d 100644 --- a/scripts/perf/issue-78851-model-resolution.ts +++ b/scripts/perf/issue-78851-model-resolution.ts @@ -151,18 +151,6 @@ function buildConfig(options: Options, workspaceDir: string): OpenClawConfig { controlUi: { enabled: false }, mode: "local", }, - memory: { - active: { - allowedChatTypes: ["direct"], - agents: ["main"], - logging: false, - maxSummaryChars: 220, - persistTranscripts: false, - promptStyle: "balanced", - queryMode: "recent", - timeoutMs: 15_000, - }, - }, models: { mode: "replace", providers, diff --git a/scripts/plugin-boundary-report.ts b/scripts/plugin-boundary-report.ts index ed40dc1f4562..f202dfd0512d 100644 --- a/scripts/plugin-boundary-report.ts +++ b/scripts/plugin-boundary-report.ts @@ -322,6 +322,9 @@ function extractCompatTokens(record: PluginCompatRecord): string[] { const tokens = new Set(); const values = [record.code, record.replacement, ...record.surfaces, ...record.diagnostics]; for (const value of values) { + if (value === undefined) { + continue; + } for (const match of value.matchAll(/`([^`]+)`/g)) { const token = match[1]?.trim(); if (token && !token.includes(" ")) { @@ -523,7 +526,7 @@ function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSum }; } -function buildReport(options: Pick = {}): BoundaryReport { +function buildReport(options: Partial> = {}): BoundaryReport { const files = options.summary ? collectSummaryWorkspaceTextFileSources() : collectWorkspaceTextFileSources(); diff --git a/scripts/protocol-gen-swift.ts b/scripts/protocol-gen-swift.ts index 62e0b883c006..0a736ea0a1ed 100644 --- a/scripts/protocol-gen-swift.ts +++ b/scripts/protocol-gen-swift.ts @@ -521,11 +521,9 @@ function emitDiscriminatedUnion(name: string, schema: JsonSchema): string | unde if (cases.some((entry) => !entry)) { continue; } - const resolvedCases: Array<{ - branchName: string; - caseName: string; - literal: boolean | number | string | null; - }> = cases; + const resolvedCases = cases.filter( + (entry): entry is NonNullable => entry !== undefined, + ); const [firstCase] = resolvedCases; if (!firstCase) { continue; diff --git a/scripts/qa/ux-matrix-evidence-producer.ts b/scripts/qa/ux-matrix-evidence-producer.ts index 42518436ff03..01ce5c54b44a 100644 --- a/scripts/qa/ux-matrix-evidence-producer.ts +++ b/scripts/qa/ux-matrix-evidence-producer.ts @@ -703,7 +703,10 @@ async function runUxMatrixEvidenceProducer(options: ProducerOptions) { : []), ], coverageIds: ["ui.control", "gateway.control-ui-hosting"], - failureReason: matrixScreenshotResult.failureReason, + failureReason: + "failureReason" in matrixScreenshotResult + ? matrixScreenshotResult.failureReason + : undefined, stage: "screenshot-artifact", status: matrixScreenshotResult.status, surface: "control-ui", @@ -781,7 +784,8 @@ async function runUxMatrixEvidenceProducer(options: ProducerOptions) { : []), ], coverageIds: ["qa.artifact-safety", "tools.evidence", "workspace.artifacts"], - failureReason: fixtureProofResult.failureReason, + failureReason: + "failureReason" in fixtureProofResult ? fixtureProofResult.failureReason : undefined, stage: "producer-artifact-fixture", status: fixtureProofResult.status, surface: "qa-lab", diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 56e58bc5b0dc..2b12c1cca95c 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -1,7 +1,7 @@ #!/usr/bin/env -S node --import tsx // Release Check script supports OpenClaw repository automation. -import { execFileSync } from "node:child_process"; +import { execFileSync, type ExecFileSyncOptions } from "node:child_process"; import { copyFileSync, existsSync, @@ -56,6 +56,10 @@ import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs"; import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts"; import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs"; +type ReleaseCheckExecOptions = ExecFileSyncOptions & { + windowsVerbatimArguments?: boolean; +}; + export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts"; export { packageNameFromSpecifier } from "./lib/plugin-package-dependencies.mjs"; @@ -211,7 +215,7 @@ export function runReleaseCheckCommand( timeoutMs?: number; }, ): string { - const output = execFileSync(invocation.command, invocation.args, { + const execOptions: ReleaseCheckExecOptions = { cwd: options.cwd, encoding: options.encoding, env: invocation.env ?? options.env, @@ -231,7 +235,12 @@ export function runReleaseCheckCommand( DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS, ), windowsVerbatimArguments: invocation.windowsVerbatimArguments, - }) as Buffer | string | null; + }; + const output: Buffer | string | null = execFileSync( + invocation.command, + invocation.args, + execOptions, + ); if (output == null) { return ""; } diff --git a/scripts/repro/code-mode-namespace-live.ts b/scripts/repro/code-mode-namespace-live.ts index cdea3464df32..8f82c2e5e310 100755 --- a/scripts/repro/code-mode-namespace-live.ts +++ b/scripts/repro/code-mode-namespace-live.ts @@ -200,7 +200,7 @@ function stringParam(params: Record, key: string): string { function makeTool( name: string, description: string, - properties: Record, + properties: Parameters[0], execute: (params: Record) => unknown, ): AnyAgentTool { const tool = { diff --git a/scripts/runtime-postbuild.d.mts b/scripts/runtime-postbuild.d.mts new file mode 100644 index 000000000000..2f1840ed5a54 --- /dev/null +++ b/scripts/runtime-postbuild.d.mts @@ -0,0 +1,47 @@ +import type fs from "node:fs"; + +export type StaticExtensionAsset = { + pluginDir?: string; + src: string; + dest: string; +}; + +export type RuntimePostBuildParams = { + rootDir?: string; + repoRoot?: string; + cwd?: string; + env?: NodeJS.ProcessEnv; + fs?: typeof fs; + timings?: boolean | "verbose"; + warn?: (message: string) => void; +}; + +type StaticExtensionAssetParams = Pick & { + assets?: StaticExtensionAsset[]; +}; + +type LegacyCliExitCompatChunk = { dest: string; contents: string }; + +export function copyStaticExtensionAssets(params?: StaticExtensionAssetParams): void; +export function listStaticExtensionAssetOutputs(params?: StaticExtensionAssetParams): string[]; + +export const LEGACY_CLI_EXIT_COMPAT_CHUNKS: LegacyCliExitCompatChunk[]; +export function listCoreRuntimePostBuildOutputs( + params?: Pick & { + chunks?: LegacyCliExitCompatChunk[]; + }, +): string[]; +export function writeStableRootRuntimeAliases( + params?: Pick, +): void; +export function rewriteRootRuntimeImportsToStableAliases( + params?: Pick, +): void; +export function writeLegacyRootRuntimeCompatAliases( + params?: Pick, +): void; +export function writeLegacyCliExitCompatChunks(params?: { + rootDir?: string; + chunks?: LegacyCliExitCompatChunk[]; +}): void; +export function runRuntimePostBuild(params?: RuntimePostBuildParams): void; diff --git a/scripts/sync-moonshot-docs.ts b/scripts/sync-moonshot-docs.ts deleted file mode 100644 index af5e344f4a0b..000000000000 --- a/scripts/sync-moonshot-docs.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Sync Moonshot Docs script supports OpenClaw repository automation. -import { readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - MOONSHOT_KIMI_K2_CONTEXT_WINDOW, - MOONSHOT_KIMI_K2_COST, - MOONSHOT_KIMI_K2_INPUT, - MOONSHOT_KIMI_K2_MAX_TOKENS, - MOONSHOT_KIMI_K2_MODELS, -} from "../ui/src/ui/data/moonshot-kimi-k2"; - -const here = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(here, ".."); - -function replaceBlockLines( - text: string, - startMarker: string, - endMarker: string, - lines: string[], -): string { - const startIndex = text.indexOf(startMarker); - if (startIndex === -1) { - throw new Error(`Missing start marker: ${startMarker}`); - } - const endIndex = text.indexOf(endMarker, startIndex); - if (endIndex === -1) { - throw new Error(`Missing end marker: ${endMarker}`); - } - - const startLineStart = text.lastIndexOf("\n", startIndex); - const startLineStartIndex = startLineStart === -1 ? 0 : startLineStart + 1; - const indent = text.slice(startLineStartIndex, startIndex); - - const endLineEnd = text.indexOf("\n", endIndex); - const endLineEndIndex = endLineEnd === -1 ? text.length : endLineEnd + 1; - - const before = text.slice(0, startLineStartIndex); - const after = text.slice(endLineEndIndex); - - const replacementLines = [ - `${indent}${startMarker}`, - ...lines.map((line) => `${indent}${line}`), - `${indent}${endMarker}`, - ]; - - const replacement = replacementLines.join("\n"); - if (!after) { - return `${before}${replacement}`; - } - return `${before}${replacement}\n${after}`; -} - -function renderKimiK2Ids(prefix: string) { - return [...MOONSHOT_KIMI_K2_MODELS.map((model) => `- \`${prefix}${model.id}\``), ""]; -} - -function renderMoonshotAliases() { - return MOONSHOT_KIMI_K2_MODELS.map((model, index) => { - const isLast = index === MOONSHOT_KIMI_K2_MODELS.length - 1; - const suffix = isLast ? "" : ","; - return `"moonshot/${model.id}": { alias: "${model.alias}" }${suffix}`; - }); -} - -function renderMoonshotModels() { - const input = JSON.stringify([...MOONSHOT_KIMI_K2_INPUT]); - const cost = `input: ${MOONSHOT_KIMI_K2_COST.input}, output: ${MOONSHOT_KIMI_K2_COST.output}, cacheRead: ${MOONSHOT_KIMI_K2_COST.cacheRead}, cacheWrite: ${MOONSHOT_KIMI_K2_COST.cacheWrite}`; - - return MOONSHOT_KIMI_K2_MODELS.flatMap((model, index) => { - const isLast = index === MOONSHOT_KIMI_K2_MODELS.length - 1; - const closing = isLast ? "}" : "},"; - return [ - "{", - ` id: "${model.id}",`, - ` name: "${model.name}",`, - ` reasoning: ${model.reasoning},`, - ` input: ${input},`, - ` cost: { ${cost} },`, - ` contextWindow: ${MOONSHOT_KIMI_K2_CONTEXT_WINDOW},`, - ` maxTokens: ${MOONSHOT_KIMI_K2_MAX_TOKENS}`, - closing, - ]; - }); -} - -async function syncMoonshotDocs() { - const moonshotDoc = path.join(repoRoot, "docs/providers/moonshot.md"); - const conceptsDoc = path.join(repoRoot, "docs/concepts/model-providers.md"); - - let moonshotText = await readFile(moonshotDoc, "utf8"); - moonshotText = replaceBlockLines( - moonshotText, - '[//]: # "moonshot-kimi-k2-ids:start"', - '[//]: # "moonshot-kimi-k2-ids:end"', - renderKimiK2Ids(""), - ); - moonshotText = replaceBlockLines( - moonshotText, - "// moonshot-kimi-k2-aliases:start", - "// moonshot-kimi-k2-aliases:end", - renderMoonshotAliases(), - ); - moonshotText = replaceBlockLines( - moonshotText, - "// moonshot-kimi-k2-models:start", - "// moonshot-kimi-k2-models:end", - renderMoonshotModels(), - ); - - let conceptsText = await readFile(conceptsDoc, "utf8"); - conceptsText = replaceBlockLines( - conceptsText, - '[//]: # "moonshot-kimi-k2-model-refs:start"', - '[//]: # "moonshot-kimi-k2-model-refs:end"', - renderKimiK2Ids("moonshot/"), - ); - - await writeFile(moonshotDoc, moonshotText); - await writeFile(conceptsDoc, conceptsText); -} - -syncMoonshotDocs().catch((error: unknown) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/scripts/test-env-mutation-report.ts b/scripts/test-env-mutation-report.ts index 4e4ea687378d..def09d3b64fd 100644 --- a/scripts/test-env-mutation-report.ts +++ b/scripts/test-env-mutation-report.ts @@ -165,7 +165,7 @@ function envKeysFromObjectLiteral(node: ts.Expression): string[] { } return node.properties .map((property) => (ts.isPropertyAssignment(property) ? propertyNameText(property.name) : null)) - .filter((key): key is string => Boolean(key) && TRACKED_ENV_KEYS.has(key)); + .filter((key): key is string => key !== null && TRACKED_ENV_KEYS.has(key)); } function isAssignmentOperator(kind: ts.SyntaxKind): boolean { diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 92b79fe0e017..98461df43183 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -708,6 +708,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ], ["scripts/clawtributors-map.json", ["test/scripts/update-clawtributors.test.ts"]], ["scripts/tsconfig.json", ["test/scripts/oxlint-config.test.ts"]], + [ + "tsconfig.scripts.json", + ["test/scripts/changed-lanes.test.ts", "test/scripts/test-projects.test.ts"], + ], ["scripts/build-all.mjs", ["test/scripts/build-all.test.ts"]], ["scripts/build-stamp.mjs", ["src/infra/build-stamp.test.ts"]], ["scripts/crabbox-wrapper-providers.mjs", ["test/scripts/crabbox-wrapper.test.ts"]], diff --git a/scripts/tool-display.ts b/scripts/tool-display.ts index 448bd3f54948..c95f24617c6f 100644 --- a/scripts/tool-display.ts +++ b/scripts/tool-display.ts @@ -2,7 +2,9 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "../src/agents/tool-display-config.js"; +import { TOOL_DISPLAY_CONFIG } from "../src/agents/tool-display-config.js"; + +type ToolDisplayConfig = typeof TOOL_DISPLAY_CONFIG; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, ".."); diff --git a/scripts/write-cli-startup-metadata.ts b/scripts/write-cli-startup-metadata.ts index 3319120c9fc8..2bfd7f3606b6 100644 --- a/scripts/write-cli-startup-metadata.ts +++ b/scripts/write-cli-startup-metadata.ts @@ -343,9 +343,6 @@ function createIsolatedRootHelpRenderContext( workspace: workspaceDir, }, }, - plugins: { - loadPaths: [], - }, }; return { config, env }; } diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 6c56d79e37b8..37b06fcb3f45 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -41,7 +41,6 @@ "agents": { "defaults": { "heartbeat": { - "enabled": true, "every": "30m" } } @@ -50,23 +49,6 @@ "groupChat": { "visibleReplies": "message_tool" } - }, - "tools": { - "profiles": { - "coding": { - "allow": [ - "message", - "heartbeat_respond", - "sessions_spawn", - "sessions_list", - "sessions_yield", - "cron", - "memory_search", - "memory_get", - "session_status" - ] - } - } } } ``` diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index bb59cfbc951c..83456e2201c7 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -41,7 +41,6 @@ "agents": { "defaults": { "heartbeat": { - "enabled": true, "every": "30m" } } @@ -50,23 +49,6 @@ "groupChat": { "visibleReplies": "message_tool" } - }, - "tools": { - "profiles": { - "coding": { - "allow": [ - "message", - "heartbeat_respond", - "sessions_spawn", - "sessions_list", - "sessions_yield", - "cron", - "memory_search", - "memory_get", - "session_status" - ] - } - } } } ``` diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 60aec37466af..87620d3e964f 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -41,7 +41,6 @@ "agents": { "defaults": { "heartbeat": { - "enabled": true, "every": "30m" } } @@ -50,23 +49,6 @@ "groupChat": { "visibleReplies": "message_tool" } - }, - "tools": { - "profiles": { - "coding": { - "allow": [ - "message", - "heartbeat_respond", - "sessions_spawn", - "sessions_list", - "sessions_yield", - "cron", - "memory_search", - "memory_get", - "session_status" - ] - } - } } } ``` diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index 11ecec4e6c3e..fd7920cdebe2 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -107,7 +107,10 @@ type CodexPromptSnapshotApi = { developerInstructions: string; threadStartParams: Record; threadResumeParams: Record; - turnStartParams: Record; + turnStartParams: Record & { + input?: unknown; + collaborationMode?: { settings?: { developer_instructions?: string } }; + }; }; createCodexDynamicToolSpecsForPromptSnapshot: (params: { tools: AnyAgentTool[]; @@ -249,28 +252,10 @@ const baseConfig: OpenClawConfig = { agents: { defaults: { heartbeat: { - enabled: true, every: "30m", }, }, }, - tools: { - profiles: { - coding: { - allow: [ - "message", - "heartbeat_respond", - "sessions_spawn", - "sessions_list", - "sessions_yield", - "cron", - "memory_search", - "memory_get", - "session_status", - ], - }, - }, - }, }; const dynamicToolsConfig: OpenClawConfig = { diff --git a/test/scripts/changed-lanes.test.ts b/test/scripts/changed-lanes.test.ts index 37e495257cb5..fb662d3d703e 100644 --- a/test/scripts/changed-lanes.test.ts +++ b/test/scripts/changed-lanes.test.ts @@ -598,6 +598,19 @@ describe("scripts/changed-lanes", () => { }); }); + it.each([ + "scripts/control-ui-i18n.ts", + "scripts/lib/example.ts", + "scripts/lib/example.d.mts", + "tsconfig.scripts.json", + ])("routes %s to the scripts typecheck lane", (changedPath) => { + const result = detectChangedLanes([changedPath]); + const plan = createChangedCheckPlan(result); + + expect(result.lanes.scripts).toBe(true); + expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:scripts"); + }); + it("falls back to full core lint for broad core diffs", () => { const targets = Array.from({ length: 9 }, (_, index) => `src/shared/file-${index}.ts`); const command = createTargetedCoreLintCommand(targets, { PATH: "/usr/bin" }); @@ -1818,6 +1831,7 @@ describe("scripts/changed-lanes", () => { coreTests: false, extensions: false, extensionTests: false, + scripts: false, apps: false, docs: false, tooling: false, diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index d794675e2094..bf8a4b3536c5 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -869,7 +869,9 @@ describe("scripts/openclaw-cross-os-release-checks", () => { const topLevelImports = source.slice(0, source.indexOf("const SCRIPT_PATH")); expect(topLevelImports).not.toContain("package-dist-inventory"); - expect(source).toContain("function assertNoLegacyPluginDependencyStagingDebris(packageRoot)"); + expect(source).toMatch( + /function assertNoLegacyPluginDependencyStagingDebris\(packageRoot: string\)/u, + ); }); it("filters the cross-OS runner matrix to a focused OS suite", () => { @@ -1724,11 +1726,10 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(init).toMatchObject({ method: "POST", body: "{}", - headers: { - Authorization: "Bot discord-token", - "Content-Type": "application/json", - }, }); + const headers = new Headers(init.headers); + expect(headers.get("Authorization")).toBe("Bot discord-token"); + expect(headers.get("Content-Type")).toBe("application/json"); expect(init.signal).toBeInstanceOf(AbortSignal); }); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 410c0445b9f2..b081025d77f6 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -1197,6 +1197,13 @@ describe("scripts/test-projects changed-target routing", () => { }); }); + it("keeps the scripts typecheck project on its routing tests", () => { + expect(resolveChangedTestTargetPlan(["tsconfig.scripts.json"])).toEqual({ + mode: "targets", + targets: ["test/scripts/changed-lanes.test.ts", "test/scripts/test-projects.test.ts"], + }); + }); + it("keeps docs i18n behavior fixture edits on behavior baseline tests", () => { for (const fixturePath of [ "scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/case.json", diff --git a/tsconfig.projects.json b/tsconfig.projects.json index 3b5517c1943c..476b8a158850 100644 --- a/tsconfig.projects.json +++ b/tsconfig.projects.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.core.projects.json" }, - { "path": "./tsconfig.extensions.projects.json" } + { "path": "./tsconfig.extensions.projects.json" }, + { "path": "./tsconfig.scripts.json" } ] } diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 000000000000..c253fa9a762f --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noUnusedLocals": true, + "noUnusedParameters": true, + "tsBuildInfoFile": ".artifacts/tsgo-cache/scripts.tsbuildinfo" + }, + "include": ["scripts/**/*", "src/**/*.d.ts", "packages/**/*.d.ts"], + "exclude": [ + "node_modules", + "dist", + "**/dist/**", + // E2E clients import built dist artifacts and are validated by Docker runs, + // so they cannot join this source-only program. + "scripts/e2e/**" + ] +}