refactor(scripts): migrate JavaScript tools to TypeScript (#121005)

* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
This commit is contained in:
Peter Steinberger
2026-08-09 07:21:35 -07:00
committed by GitHub
parent 2433fa213c
commit c70aee247e
937 changed files with 48087 additions and 50044 deletions
@@ -50,7 +50,7 @@ every human `Thanks @...` attribution.
writing grouped prose:
```bash
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
node --import tsx .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
--base <base-tag> \
--target <target-ref> \
--main-ref origin/main \
@@ -200,7 +200,7 @@ every human `Thanks @...` attribution.
- after the manifest-driven rewrite, regenerate and verify the complete
contribution record before committing:
```bash
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
node --import tsx .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
--base <base-tag> \
--target <target-ref> \
--main-ref origin/main \
@@ -228,7 +228,7 @@ every human `Thanks @...` attribution.
- after the GitHub release or prerelease is published, verify every matching
release page against the same source section:
```bash
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
node --import tsx .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
--base <base-tag> \
--target <target-ref> \
--version <YYYY.M.PATCH> \
@@ -238,7 +238,7 @@ every human `Thanks @...` attribution.
- add one `--release-tag` for every beta and stable page in the train; a
`### Release verification` tail is permitted, but any other body drift
fails the check
- `scripts/render-github-release-notes.mjs` is the canonical release-body
- `scripts/render-github-release-notes.mts` is the canonical release-body
renderer used by candidate validation, publish, and verification. When the
complete `## YYYY.M.PATCH` section fits GitHub's 125,000-character limit and
the renderer's matching 125,000-byte safety ceiling, the body must contain
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --import tsx
import { execFileSync, spawnSync } from "node:child_process";
import {
@@ -20,7 +20,7 @@ import {
parseShippedBaselineExclusions,
releaseNotesVersionForTag,
verifyGithubReleaseNotes,
} from "../../../../scripts/render-github-release-notes.mjs";
} from "../../../../scripts/render-github-release-notes.mts";
const repo = "openclaw/openclaw";
const githubSnapshotSchemaVersion = 1;
@@ -78,7 +78,7 @@ function fail(message) {
function printUsage() {
console.log(`Usage:
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \\
node --import tsx .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \\
--base <tag-or-sha> --target <tag-or-sha> --version <version> [options]
Required:
+1 -1
View File
@@ -184,7 +184,7 @@ For workflow-only or docs/skill-only changes in a Codex worktree:
```bash
node scripts/run-vitest.mjs test/scripts/ci-workflow-guards.test.ts
node scripts/check-workflows.mjs
node --import tsx scripts/check-workflows.mts
node scripts/docs-list.js
./node_modules/.bin/oxfmt --check .github/workflows/ci.yml .github/workflows/codeql-critical-quality.yml docs/ci.md test/scripts/ci-workflow-guards.test.ts .agents/skills/openclaw-ci-limits/SKILL.md .agents/skills/openclaw-ci-limits/agents/openai.yaml
git diff --check
@@ -20,7 +20,7 @@ Keep `/Users/steipete/openclaw` a read-only-to-the-agent deployment mirror: clea
1. Run the deterministic updater and retain its JSON:
```bash
node .agents/skills/openclaw-live-updater/scripts/update-main.mjs
node --import tsx .agents/skills/openclaw-live-updater/scripts/update-main.mjs
```
Stop on any failed invariant. Do not repair the mirror destructively. The helper holds one checkout-scoped lock across update, build, Gateway proof, and Mac work. A concurrent heartbeat returns `reason: "overlap"`; it must not start another build. A dead owner lock may be recovered, but unreadable or unsafe lock state fails closed.
@@ -24,16 +24,16 @@ import { isDirectRunUrl } from "../../../../scripts/lib/direct-run.mjs";
import {
BUILD_STAMP_FILE,
RUNTIME_POSTBUILD_STAMP_FILE,
} from "../../../../scripts/lib/local-build-metadata.mjs";
import { runManagedCommand } from "../../../../scripts/lib/managed-child-process.mjs";
} from "../../../../scripts/lib/local-build-metadata.mts";
import { runManagedCommand } from "../../../../scripts/lib/managed-child-process.mts";
import {
runNodeConfigFiles,
runNodeSourceRoots,
} from "../../../../scripts/run-node-watch-paths.mjs";
} from "../../../../scripts/run-node-watch-paths.mts";
import {
resolveBuildRequirement,
resolveRuntimePostBuildRequirement,
} from "../../../../scripts/run-node.mjs";
} from "../../../../scripts/run-node.mts";
const DEFAULT_CHECKOUT = "/Users/steipete/openclaw";
const DEFAULT_EXPECTED_ORIGIN = "openclaw/openclaw";
@@ -90,6 +90,68 @@ exec "$@"
const DEPENDENCY_INPUT_RE =
/^(?:\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|patches\/)|(?:^|\/)package\.json$/u;
/**
* @typedef {object} GatewayDeploymentRef
* @property {string} entrypoint
*/
/**
* The fields required when the updater invokes the managed Gateway CLI.
* LaunchAgent inspection returns a richer object, while focused probes may
* provide only this execution view.
*
* @typedef {object} GatewayCliDeploymentBase
* @property {string} configPath
* @property {string} entrypoint
* @property {string} executable
* @property {string[]} invocationPrefix
* @property {number} port
* @property {Record<string, string>} [serviceEnvironment]
* @property {string | null} [workingDirectory]
*/
/**
* @typedef {GatewayCliDeploymentBase & {
* envFilePath?: null,
* runtime?: string,
* wrapperPath?: null,
* }} DirectGatewayCliDeployment
*/
/**
* @typedef {GatewayCliDeploymentBase & {
* envFilePath: string,
* runtime: string,
* wrapperPath: string,
* }} WrappedGatewayCliDeployment
*/
/** @typedef {DirectGatewayCliDeployment | WrappedGatewayCliDeployment} GatewayCliDeployment */
/**
* The stable identity fields used to verify a LaunchAgent retarget.
* Repointing deliberately does not require the execution-only fields above.
*
* @typedef {object} GatewayRepointDeployment
* @property {string} configPath
* @property {string} entrypoint
* @property {string} label
* @property {number} port
*/
/**
* The updater's established test/API result keeps owner details extensible,
* while naming the fields every completed maintenance run exposes.
*
* @typedef {Record<string, unknown> & {
* actions: Record<string, unknown>,
* buildBefore: Record<string, unknown>,
* changedPaths?: string[],
* macTarget?: Record<string, unknown>,
* release?: () => void,
* }} UpdateResult
*/
class UpdateInvariantError extends Error {
constructor(code, message, details, options) {
super(message, options);
@@ -115,7 +177,11 @@ class UpdateCommandError extends Error {
}
}
/** Re-throw the original runtime value while exposing the Error contract to type-aware lint. */
/**
* Re-throw the original runtime value while exposing the Error contract to type-aware lint.
*
* @returns {never}
*/
function throwPreservingValue(value) {
throw /** @type {Error} */ (value);
}
@@ -1377,6 +1443,13 @@ function inspectManagedGatewayDeployment(checkout) {
return readManagedGatewayLaunchAgent(checkout);
}
/**
* @param {string} checkout
* @param {GatewayRepointDeployment} deployment
* @param {(deployment: GatewayRepointDeployment, replacement: string) => void} replaceEntrypoint
* @param {(checkout: string) => GatewayRepointDeployment | null} [inspectDeployment]
* @returns {GatewayRepointDeployment & { changed: boolean, previousEntrypoint?: string }}
*/
export function repointManagedGatewayDeployment(
checkout,
deployment,
@@ -1631,6 +1704,13 @@ export function parseLaunchctlArguments(output) {
: [];
}
/**
* @param {string} checkout
* @param {string[]} args
* @param {GatewayCliDeployment | null | undefined} deployment
* @param {{ stderr?: "inherit" | "pipe", timeoutMs?: number }} [options]
* @returns {string}
*/
export function runBuiltGatewayCli(checkout, args, deployment, options = {}) {
const observedDeployment = deployment ?? readManagedGatewayLaunchAgent(checkout);
const sourceEntrypoint = path.join(checkout, "dist/index.js");
@@ -1738,6 +1818,13 @@ export function runBuiltGatewayCli(checkout, args, deployment, options = {}) {
}
}
/**
* @param {string} checkout
* @param {string} method
* @param {Record<string, unknown>} params
* @param {GatewayCliDeployment | null | undefined} deployment
* @returns {string}
*/
export function runBuiltGatewayCall(checkout, method, params, deployment) {
const managedDeployment = deployment ?? readManagedGatewayLaunchAgent(checkout);
return runBuiltGatewayCli(
@@ -1756,6 +1843,11 @@ export function runBuiltGatewayCall(checkout, method, params, deployment) {
);
}
/**
* @param {string} checkout
* @param {(checkout: string, method: string, params: { requestId: string }, deployment: GatewayDeploymentRef | null) => string} [callGateway]
* @param {GatewayDeploymentRef | null} [deployment]
*/
export function prepareGatewaySuspension(
checkout,
callGateway = runBuiltGatewayCall,
@@ -2629,6 +2721,23 @@ function defaultSleep(ms) {
return delay(ms);
}
/**
* @param {(command: string, args: string[], checkout: string, options?: Record<string, unknown>) => unknown | Promise<unknown>} runCommand
* @param {string} checkout
* @param {string} expectedSha
* @param {(ms: number) => void | Promise<void>} [sleep]
* @param {GatewayCliDeployment | null} [deployment]
* @param {{
* now?: () => number,
* probeMilestones?: (deployment: GatewayCliDeployment) => {
* listenerReady: boolean,
* healthzReady: boolean,
* readyzReady: boolean,
* },
* timing?: Record<string, unknown>,
* }} [options]
* @returns {Promise<Record<string, unknown>>}
*/
export async function verifyGatewayReadiness(
runCommand,
checkout,
@@ -2825,6 +2934,12 @@ function summarizeGatewayLogAudit(entries) {
};
}
/**
* @param {string} output
* @param {number} sinceMs
* @param {string | null} [sourceRoot]
* @param {string[] | null} [managedSourceRoots]
*/
export function parseGatewayLogAudit(output, sinceMs, sourceRoot = null, managedSourceRoots = []) {
const entries = parseGatewayLogEntries(output, sinceMs).filter((entry) =>
isCurrentGatewayLogSource(entry.source, sourceRoot, managedSourceRoots),
@@ -2888,6 +3003,10 @@ export function resolveManagedPluginSourceRoots(report) {
return roots;
}
/**
* @param {string} checkout
* @param {GatewayDeploymentRef | null | undefined} deployment
*/
export function resolveManagedGatewaySourceRoot(checkout, deployment) {
return typeof deployment?.entrypoint === "string" && deployment.entrypoint.length > 0
? path.dirname(path.resolve(deployment.entrypoint))
@@ -3026,6 +3145,12 @@ async function defaultVerifyMacTarget(checkout) {
return target;
}
/**
* @overload
* @param {Record<string, unknown>} options
* @param {Record<string, unknown>} [dependencies]
* @returns {Promise<UpdateResult>}
*/
export async function maintainMain(options, dependencies = {}) {
const lock = acquireMaintenanceLock(options.checkout, options.lockPath);
if (!lock.acquired) {
@@ -24,7 +24,7 @@ For **runtime fixes** (e.g., closure leaks in long-running services like the gat
```
- For a suspected file, rerun that file with one worker and collect wall/RSS evidence: `/usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose`.
- Current `pnpm test` execution is planned by `scripts/test-projects.mjs`. Record the printed Vitest config or shard and preserve that shape when the report is configuration- or worker-budget-specific.
- Current `pnpm test` execution is planned by `scripts/test-projects.mts`. Record the printed Vitest config or shard and preserve that shape when the report is configuration- or worker-budget-specific.
2. Collect the strongest available heap evidence.
- Run `pnpm test:perf:profile:runner -- --output-dir .artifacts/test-perf/vitest-runner-profile -- <file>` for a CPU profile plus a sampling heap profile of the unit runner. Open the heap profile in DevTools and inspect the largest allocation families.
@@ -53,7 +53,7 @@ For **runtime fixes** (e.g., closure leaks in long-running services like the gat
## Heuristics
- Do not call everything a leak. Growth in a non-isolated shared Vitest project can be a worker-lifetime problem rather than an application object leak.
- `scripts/test-projects.mjs`, `scripts/test-group-report.mjs`, and `scripts/run-vitest-profile.mjs` are the current execution, grouped-RSS, and profile entrypoints.
- `scripts/test-projects.mts`, `scripts/test-group-report.mts`, and `scripts/run-vitest-profile.mts` are the current execution, grouped-RSS, and profile entrypoints.
- The `[test] starting ...` lines identify the Vitest config or shard to reproduce.
- `.artifacts/vitest-shard-timings.json` stores config/shard durations for scheduling. It is not a file-level memory-hotspot or behavior manifest.
- When the same retained object families grow across multiple intervals in the same worker PID, trust the snapshots over intuition, then confirm ambiguous calls with retainer evidence.
@@ -480,7 +480,7 @@ HEAD/worktree-bound manifest under git metadata for cutover review.
credit from that PR's record on the same bullet.
- Changelog entries should be user-facing, not internal release-process notes.
- GitHub release and prerelease bodies use
`scripts/render-github-release-notes.mjs`. When the full matching
`scripts/render-github-release-notes.mts`. When the full matching
`CHANGELOG.md` version section fits GitHub's 125,000-character limit and
the renderer's matching 125,000-byte safety ceiling, publish the exact
`## YYYY.M.PATCH` block through the line before the next level-2 heading,
+1 -1
View File
@@ -482,7 +482,7 @@ runs:
path: .artifacts/build-all-cache
# Exact keys deduplicate concurrent jobs. Coarse restore supplies the
# newest declaration groups; build-all rehashes every group's inputs.
key: ${{ github.repository }}-build-all-v1-${{ inputs.build-all-cache-scope }}-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'tsconfig*.json', 'tsdown*.config.ts', 'scripts/build-all.mjs', 'scripts/tsdown-build.mjs', 'scripts/lib/tsdown-*.mjs', 'scripts/lib/plugin-sdk-*', 'scripts/lib/bundled-plugin-*', 'scripts/lib/optional-bundled-clusters.mjs', 'src/**', 'packages/**', 'extensions/**') }}
key: ${{ github.repository }}-build-all-v1-${{ inputs.build-all-cache-scope }}-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'tsconfig*.json', 'tsdown*.config.ts', 'scripts/build-all.mts', 'scripts/tsdown-build.mts', 'scripts/lib/tsdown-*.mts', 'scripts/lib/plugin-sdk-*', 'scripts/lib/bundled-plugin-*', 'scripts/lib/optional-bundled-clusters.mjs', 'src/**', 'packages/**', 'extensions/**') }}
restore-keys: |
${{ github.repository }}-build-all-v1-${{ inputs.build-all-cache-scope }}-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-
@@ -156,7 +156,7 @@ than Telegram-visible behavior`. Use this manifest shape and do not create
`--sut-lane`/`--sut-repo-root` during `start`.
```bash
node scripts/mantis/build-telegram-desktop-proof-evidence.mjs \
node --import tsx scripts/mantis/build-telegram-desktop-proof-evidence.mts \
--output-dir "$MANTIS_OUTPUT_DIR" \
--baseline-repo-root "$GITHUB_WORKSPACE" \
--baseline-output-dir <baseline-session-output-dir> \
+56 -38
View File
@@ -59,6 +59,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
jobs:
# Preflight: establish routing truth and job matrices once, then let real
@@ -379,6 +380,19 @@ jobs:
node scripts/ci-changed-scope.mjs --base "$BASE" --head "$HEAD_SHA"
fi
- name: Setup manifest TypeScript runtime
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup manifest pnpm
uses: ./.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install manifest dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Build CI manifest
id: manifest
env:
@@ -404,7 +418,7 @@ jobs:
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }}
run: |
node --input-type=module <<'EOF'
node --import tsx --input-type=module <<'EOF'
import { appendFileSync, existsSync, readFileSync } from "node:fs";
const eventName = process.env.OPENCLAW_CI_EVENT_NAME ?? "";
@@ -423,7 +437,10 @@ jobs:
const frozenTarget =
eventName === "workflow_dispatch" && checkoutRevision !== workflowRevision;
const nodeTestPlan = await import("./scripts/lib/ci-node-test-plan.mjs");
const nodeTestPlanPath = existsSync("./scripts/lib/ci-node-test-plan.mts")
? "./scripts/lib/ci-node-test-plan.mts"
: "./scripts/lib/ci-node-test-plan.mjs";
const nodeTestPlan = await import(nodeTestPlanPath);
const createNodeTestPlan =
typeof nodeTestPlan.createNodeTestShardBundles === "function"
? nodeTestPlan.createNodeTestShardBundles
@@ -435,9 +452,14 @@ jobs:
}
let changedNodeTestPlan = {};
if (existsSync("./scripts/lib/ci-changed-node-test-plan.mjs")) {
const changedNodeTestPlanPath = existsSync(
"./scripts/lib/ci-changed-node-test-plan.mts",
)
? "./scripts/lib/ci-changed-node-test-plan.mts"
: "./scripts/lib/ci-changed-node-test-plan.mjs";
if (existsSync(changedNodeTestPlanPath)) {
try {
changedNodeTestPlan = await import("./scripts/lib/ci-changed-node-test-plan.mjs");
changedNodeTestPlan = await import(changedNodeTestPlanPath);
} catch (error) {
console.warn(`Changed Node test planner import failed; using compact full suite: ${error}`);
}
@@ -453,7 +475,9 @@ jobs:
return {};
};
const channelContractPlan = await importTargetPlan(
"./scripts/lib/channel-contract-test-plan.mjs",
existsSync("./scripts/lib/channel-contract-test-plan.mts")
? "./scripts/lib/channel-contract-test-plan.mts"
: "./scripts/lib/channel-contract-test-plan.mjs",
);
const createChannelContractTestShards =
typeof channelContractPlan.createChannelContractTestShards === "function"
@@ -469,7 +493,9 @@ jobs:
};
const pluginContractPlan = await importTargetPlan(
"./scripts/lib/plugin-contract-test-plan.mjs",
existsSync("./scripts/lib/plugin-contract-test-plan.mts")
? "./scripts/lib/plugin-contract-test-plan.mts"
: "./scripts/lib/plugin-contract-test-plan.mjs",
);
const createPluginContractTestShards =
typeof pluginContractPlan.createPluginContractTestShards === "function"
@@ -1124,7 +1150,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .artifacts/build-all-cache
key: ${{ runner.os }}-build-all-v4-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'scripts/build-all.mjs', 'scripts/runtime-postbuild.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-private-local-only-subpaths.json', 'scripts/lib/plugin-sdk-deprecated-public-subpaths.json', 'scripts/lib/plugin-sdk-deprecated-barrel-subpaths.json', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/**', 'packages/**', '!src/**/dist/**', '!src/**/node_modules/**', '!packages/**/dist/**', '!packages/**/node_modules/**') }}
key: ${{ runner.os }}-build-all-v4-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'scripts/build-all.mts', 'scripts/runtime-postbuild.mjs', 'scripts/runtime-postbuild.mts', 'scripts/lib/tsx-cli-shim.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-private-local-only-subpaths.json', 'scripts/lib/plugin-sdk-deprecated-public-subpaths.json', 'scripts/lib/plugin-sdk-deprecated-barrel-subpaths.json', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/**', 'packages/**', '!src/**/dist/**', '!src/**/node_modules/**', '!packages/**/dist/**', '!packages/**/node_modules/**') }}
restore-keys: |
${{ runner.os }}-build-all-v4-
@@ -1305,7 +1331,7 @@ jobs:
if [ "$RUN_GATEWAY_WATCH" = "true" ] && [ "$PARALLEL_GATEWAY_WATCH" = "true" ]; then
start_check "gateway-watch" \
node scripts/check-gateway-watch-regression.mjs --skip-build
node --import tsx scripts/check-gateway-watch-regression.mts --skip-build
fi
wait_checks
@@ -1314,7 +1340,7 @@ jobs:
# starve the Gateway readiness deadline used by this regression gate.
if [ "$RUN_GATEWAY_WATCH" = "true" ] && [ "$PARALLEL_GATEWAY_WATCH" != "true" ]; then
start_check "gateway-watch" \
node scripts/check-gateway-watch-regression.mjs --skip-build
node --import tsx scripts/check-gateway-watch-regression.mts --skip-build
wait_checks
fi
@@ -1441,7 +1467,7 @@ jobs:
# Install the managed browser revision pinned by the selected target instead.
pnpm --dir ui exec playwright install chromium
else
node scripts/ensure-playwright-chromium.mjs
node --import tsx scripts/ensure-playwright-chromium.mts
fi
- name: Lint Control UI window.open usage
@@ -1491,7 +1517,7 @@ jobs:
use-actions-cache: ${{ (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.run_attempt > 1)) && 'true' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true') }}
- name: Install Playwright Chromium
run: node scripts/ensure-playwright-chromium.mjs
run: node --import tsx scripts/ensure-playwright-chromium.mts
- name: Test Control UI end-to-end
run: >-
@@ -1731,18 +1757,18 @@ jobs:
run: |
# Pack the public runtime before overlaying private QA artifacts so they cannot leak.
unset OPENCLAW_BUILD_PRIVATE_QA
node scripts/build-all.mjs qaRuntime
node --import tsx scripts/build-all.mts qaRuntime
pnpm ui:build
package_args=(
--skip-build
--output-dir .artifacts/qa-e2e/smoke-ci-package
--output-name openclaw-current.tgz
)
if grep -Fq -- '--allow-unreleased-changelog' scripts/package-openclaw-for-docker.mjs; then
if grep -Fq -- '--allow-unreleased-changelog' scripts/package-openclaw-for-docker.mts; then
package_args=(--allow-unreleased-changelog "${package_args[@]}")
fi
node scripts/package-openclaw-for-docker.mjs "${package_args[@]}"
OPENCLAW_BUILD_PRIVATE_QA=1 node scripts/build-all.mjs qaRuntime
OPENCLAW_BUILD_PRIVATE_QA=1 node --import tsx scripts/build-all.mts qaRuntime
- name: Run smoke profile part
env:
@@ -2061,13 +2087,13 @@ jobs:
# Frozen release targets can predate the workflow-owned shard runner.
# Keep its implementation pinned to this workflow revision, while tests
# continue to run against the checked-out candidate.
if: ${{ hashFiles('scripts/ci-run-node-test-shard.mjs') == '' }}
if: ${{ hashFiles('scripts/ci-run-node-test-shard.mts') == '' }}
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.workflow_sha }}
path: .ci-workflow
sparse-checkout: |
scripts/ci-run-node-test-shard.mjs
scripts/ci-run-node-test-shard.mts
scripts/lib/direct-run.mjs
scripts/lib/local-heavy-check-runtime.mjs
sparse-checkout-cone-mode: false
@@ -2092,12 +2118,12 @@ jobs:
shell: bash
run: |
set -euo pipefail
runner="scripts/ci-run-node-test-shard.mjs"
runner="scripts/ci-run-node-test-shard.mts"
if [[ ! -f "$runner" ]]; then
runner=".ci-workflow/${runner}"
[[ -f "$runner" ]]
fi
node "$runner"
node --import tsx "$runner"
# Types, lint, and format check shards.
check-shard:
@@ -2175,7 +2201,7 @@ jobs:
env:
# Config/toolchain inputs the tree-OID gate below cannot see; must
# stay identical to the boundary lane's fingerprint composition.
BOUNDARY_CONFIG_HASH: ${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'package.json', 'pnpm-lock.yaml') }}
BOUNDARY_CONFIG_HASH: ${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mts', 'scripts/prepare-extension-package-boundary-artifacts.mts', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mts', 'package.json', 'pnpm-lock.yaml') }}
run: |
set -euo pipefail
sticky_root=/var/tmp/openclaw-ext-boundary
@@ -2218,14 +2244,6 @@ jobs:
case "$TASK" in
guards)
pnpm check:no-conflict-markers
if has_package_script "check:script-declarations"; then
pnpm check:script-declarations
elif [[ "$HISTORICAL_TARGET" != "true" ]]; then
echo "Current CI targets must provide the check:script-declarations package script." >&2
exit 1
else
echo "[skip] historical target predates the script declaration contract"
fi
if [[ "$HISTORICAL_TARGET" == "true" ]]; then
echo "[skip] historical target skips the wall-clock doctor deprecation registry guard"
elif has_package_script "check:doctor-deprecation-registry"; then
@@ -2278,7 +2296,7 @@ jobs:
pnpm lint "${lint_args[@]}"
else
echo "[skip] changed scope cannot affect control-UI i18n catalogs"
node scripts/run-oxlint-shards.mjs "${lint_args[@]}"
node --import tsx scripts/run-oxlint-shards.mts "${lint_args[@]}"
fi
if [ "$FORMAT_CHECK" = "true" ]; then
pnpm format:check
@@ -2451,7 +2469,7 @@ jobs:
env:
# Config/toolchain inputs the tree-OID gate below cannot see; must
# stay identical to the seed step and check-lint's restore gate.
BOUNDARY_CONFIG_HASH: ${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'package.json', 'pnpm-lock.yaml') }}
BOUNDARY_CONFIG_HASH: ${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mts', 'scripts/prepare-extension-package-boundary-artifacts.mts', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mts', 'package.json', 'pnpm-lock.yaml') }}
run: |
set -euo pipefail
sticky_root=/var/tmp/openclaw-ext-boundary
@@ -2489,7 +2507,7 @@ jobs:
packages/plugin-sdk/dist
extensions/*/dist/.boundary-tsc.tsbuildinfo
extensions/*/dist/.boundary-tsc.stamp
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/plugins/types.ts', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mts', 'scripts/prepare-extension-package-boundary-artifacts.mts', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mts', 'src/plugin-sdk/**', 'src/plugins/types.ts', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-extension-package-boundary-v1-
@@ -2522,11 +2540,11 @@ jobs:
packages/plugin-sdk/tsconfig.json \
packages/llm-core/package.json \
packages/model-catalog-core/package.json \
scripts/check-extension-package-tsc-boundary.mjs \
scripts/prepare-extension-package-boundary-artifacts.mjs \
scripts/check-extension-package-tsc-boundary.mts \
scripts/prepare-extension-package-boundary-artifacts.mts \
scripts/write-plugin-sdk-entry-dts.ts \
scripts/lib/plugin-sdk-entrypoints.json \
scripts/lib/plugin-sdk-entries.mjs \
scripts/lib/plugin-sdk-entries.mts \
package.json \
pnpm-lock.yaml
)
@@ -2567,7 +2585,7 @@ jobs:
case "$ADDITIONAL_CHECK_GROUP" in
boundaries)
node scripts/run-additional-boundary-checks.mjs
node --import tsx scripts/run-additional-boundary-checks.mts
;;
prompt-snapshots)
# No presence fallback: the boundary runner previously invoked
@@ -2582,14 +2600,14 @@ jobs:
fi
;;
session-accessor-boundary)
if [ ! -f scripts/check-session-accessor-boundary.mjs ]; then
if [ ! -f scripts/check-session-accessor-boundary.mts ]; then
echo "[skip] session accessor boundary check is not present in this checkout"
elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:session-accessor-boundary"] ? 0 : 1);'; then
echo "[skip] session accessor boundary script is not present in package.json"
else
run_check "lint:tmp:session-accessor-boundary" pnpm run lint:tmp:session-accessor-boundary
fi
if [ ! -f scripts/check-sqlite-transaction-boundary.mjs ]; then
if [ ! -f scripts/check-sqlite-transaction-boundary.mts ]; then
echo "[skip] SQLite transaction boundary check is not present in this checkout"
elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:sqlite-transaction-boundary"] ? 0 : 1);'; then
echo "[skip] SQLite transaction boundary script is not present in package.json"
@@ -2598,7 +2616,7 @@ jobs:
fi
;;
session-transcript-reader-boundary)
if [ ! -f scripts/check-session-transcript-reader-boundary.mjs ]; then
if [ ! -f scripts/check-session-transcript-reader-boundary.mts ]; then
echo "[skip] session transcript reader boundary check is not present in this checkout"
elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:session-transcript-reader-boundary"] ? 0 : 1);'; then
echo "[skip] session transcript reader boundary script is not present in package.json"
@@ -2649,7 +2667,7 @@ jobs:
shell: bash
env:
# Must stay identical to the restore gates above.
BOUNDARY_CONFIG_HASH: ${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'package.json', 'pnpm-lock.yaml') }}
BOUNDARY_CONFIG_HASH: ${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mts', 'scripts/prepare-extension-package-boundary-artifacts.mts', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mts', 'package.json', 'pnpm-lock.yaml') }}
run: |
set -euo pipefail
sticky_root=/var/tmp/openclaw-ext-boundary
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
install-bun: "false"
- name: Collect dated TODO candidates
run: node scripts/dated-todo-scan.mjs
run: node --import tsx scripts/dated-todo-scan.mts
- name: Capture sweep date
id: sweep-date
+5 -1
View File
@@ -8,6 +8,10 @@ on:
- docs/**
- scripts/docs-list.js
- scripts/docs-sync-publish.mjs
- scripts/check-docs-mdx.mjs
- scripts/check-docs-mdx.mts
- scripts/lib/mintlify-accordion.mjs
- scripts/lib/tsx-cli-shim.mjs
- .github/workflows/docs-sync-publish.yml
workflow_dispatch:
@@ -84,7 +88,7 @@ jobs:
- name: Install docs MDX checker dependency
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
working-directory: publish
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1 tsx@4.23.1
- name: Check publish docs MDX
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
+1
View File
@@ -64,6 +64,7 @@ jobs:
"scripts/format-swift.sh",
"scripts/install-swift-tools.sh",
"scripts/ios-write-swift-filelist.mjs",
"scripts/ios-write-swift-filelist.mts",
"scripts/lint-swift.sh",
], { ignoreReturnCode: true, silent: true });
if (result.exitCode !== 0 && result.exitCode !== 1) {
+1 -1
View File
@@ -508,7 +508,7 @@ jobs:
fi
echo "telegram_exit=${telegram_exit}" >> "$GITHUB_OUTPUT"
node "${GITHUB_WORKSPACE}/scripts/mantis/build-telegram-evidence.mjs" \
node --import tsx "${GITHUB_WORKSPACE}/scripts/mantis/build-telegram-evidence.mts" \
--output-dir "$root" \
--candidate-ref "$CANDIDATE_SHA" \
--candidate-sha "$CANDIDATE_SHA" \
@@ -297,7 +297,7 @@ jobs:
"$candidate_repo/ui/src/test-helpers/control-ui-e2e.ts"
cd "$candidate_repo"
node scripts/ensure-playwright-chromium.mjs
node --import tsx scripts/ensure-playwright-chromium.mts
set +e
OPENCLAW_MANTIS_WEB_UI_CHAT_OUTPUT_DIR="$root" \
+1 -1
View File
@@ -237,7 +237,7 @@ jobs:
OPENCLAW_BUILD_PRIVATE_QA: "1"
run: |
set -euo pipefail
node scripts/build-all.mjs qaRuntime
node --import tsx scripts/build-all.mts qaRuntime
test -f dist/plugin-sdk/qa-runtime.js
test -f dist/extensions/qa-lab/runtime-api.js
@@ -619,7 +619,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
node workflow/scripts/resolve-openclaw-package-candidate.mjs \
pnpm --dir workflow exec node --import tsx scripts/resolve-openclaw-package-candidate.mts \
--source artifact \
--artifact-dir "$INPUT_DIR" \
--package-sha256 "$INPUT_CANDIDATE_SHA256" \
@@ -737,11 +737,11 @@ jobs:
env:
BASELINE_PACK_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/pack.json
run: |
node --input-type=module <<'NODE' >>"$GITHUB_OUTPUT"
node --import tsx --input-type=module <<'NODE' >>"$GITHUB_OUTPUT"
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mts";
function resolveTarballFileName(value, label) {
const fileName = typeof value === "string" ? value.trim() : "";
if (
@@ -2296,7 +2296,7 @@ jobs:
)
# Frozen targets that predate the opt-in flag still strict-pack against their versioned changelog.
if [[ "$ALLOW_UNRELEASED_CHANGELOG" == "true" ]] && \
grep -Fq -- '--allow-unreleased-changelog' scripts/package-openclaw-for-docker.mjs; then
grep -Fq -- '--allow-unreleased-changelog' scripts/package-openclaw-for-docker.mts; then
package_args+=(--allow-unreleased-changelog)
fi
node scripts/package-openclaw-for-docker.mjs "${package_args[@]}"
@@ -2341,12 +2341,12 @@ jobs:
cp "${tgzs[0]}" "$target"
fi
echo "Validating Docker E2E package tarball: $target"
validator="scripts/check-openclaw-package-tarball.mjs"
validator=(node scripts/check-openclaw-package-tarball.mjs)
if [[ -n "${EXPECTED_PACKAGE_FILE_NAME// }" ]]; then
validator=".release-harness/scripts/check-openclaw-package-tarball.mjs"
validator=(pnpm --dir .release-harness exec node scripts/check-openclaw-package-tarball.mjs)
fi
started_at="$(date +%s)"
timeout --foreground 5m node "$validator" "$target"
timeout --foreground 5m "${validator[@]}" "$GITHUB_WORKSPACE/$target"
finished_at="$(date +%s)"
echo "Docker E2E package tarball validation finished in $((finished_at - started_at))s."
digest="$(sha256sum "$target" | awk '{print $1}')"
+2 -2
View File
@@ -358,7 +358,7 @@ jobs:
for package_dir in "${package_dirs[@]}"; do
runtime_args+=(--package "$package_dir")
done
node scripts/check-plugin-npm-runtime-builds.mjs "${runtime_args[@]}"
node --import tsx scripts/check-plugin-npm-runtime-builds.mts "${runtime_args[@]}"
for package_dir in "${package_dirs[@]}"; do
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD=0 \
@@ -387,7 +387,7 @@ jobs:
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
node scripts/generate-dependency-release-evidence.mjs \
node --import tsx scripts/generate-dependency-release-evidence.mts \
--release-ref "$RELEASE_REF" \
--npm-dist-tag "$RELEASE_NPM_DIST_TAG" \
--output-dir "$RUNNER_TEMP/openclaw-release-dependency-evidence" \
+17 -11
View File
@@ -393,14 +393,20 @@ jobs:
shell: bash
run: |
set -euo pipefail
npm_wrapper="$PERFORMANCE_HELPER_DIR/scripts/ocm-npm-workspace-deps.mjs"
npm_wrapper="$PERFORMANCE_HELPER_DIR/scripts/ocm-npm-workspace-deps.mts"
npm_adapter="${RUNNER_TEMP}/openclaw-ocm-npm"
workspace_dependency_dirs=""
if [[ -f "${GITHUB_WORKSPACE}/packages/ai/package.json" ]]; then
workspace_dependency_dirs="${GITHUB_WORKSPACE}/packages/ai"
fi
chmod 0755 "$npm_wrapper"
cat > "$npm_adapter" <<'EOF'
#!/usr/bin/env bash
exec node --import tsx "$OPENCLAW_OCM_NPM_WRAPPER" "$@"
EOF
chmod 0755 "$npm_adapter"
{
echo "OCM_INTERNAL_NPM_BIN=$npm_wrapper"
echo "OCM_INTERNAL_NPM_BIN=$npm_adapter"
echo "OPENCLAW_OCM_NPM_WRAPPER=$npm_wrapper"
echo "OPENCLAW_OCM_REAL_NPM_BIN=$(command -v npm)"
echo "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS=$workspace_dependency_dirs"
} >> "$GITHUB_ENV"
@@ -529,7 +535,7 @@ jobs:
echo "report_md=$report_md" >> "$GITHUB_OUTPUT"
set +e
node "$PERFORMANCE_HELPER_DIR/scripts/lib/kova-workflow-evidence.mjs" \
node --import tsx "$PERFORMANCE_HELPER_DIR/scripts/lib/kova-workflow-evidence.mts" \
--plan "$KOVA_PLAN_JSON" \
--report "$report_json" \
--profile "$PROFILE" \
@@ -547,7 +553,7 @@ jobs:
if [[ "$KOVA_REF" == "$KOVA_CANONICAL_CONFIG_REF" || "$KOVA_REF" == "$KOVA_LEGACY_LIST_CONFIG_REF" ]]; then
gate_args+=(--require-instrumented-performance-contract)
fi
if node "$PERFORMANCE_HELPER_DIR/scripts/lib/kova-report-gate.mjs" "${gate_args[@]}"
if node --import tsx "$PERFORMANCE_HELPER_DIR/scripts/lib/kova-report-gate.mts" "${gate_args[@]}"
then
effective_status=0
{
@@ -564,7 +570,7 @@ jobs:
set -e
summary_path="$SUMMARY_DIR/${LANE_ID}.md"
summary_args=(node "$PERFORMANCE_HELPER_DIR/scripts/kova-ci-summary.mjs" --report "$report_json" --output "$summary_path" --lane "$LANE_ID")
summary_args=(node --import tsx "$PERFORMANCE_HELPER_DIR/scripts/kova-ci-summary.mts" --report "$report_json" --output "$summary_path" --lane "$LANE_ID")
set +e
"${summary_args[@]}"
summary_status=$?
@@ -733,7 +739,7 @@ jobs:
fi
mkdir -p "$SOURCE_PERF_DIR/mock-hello"
if ! node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:gateway:cpu-scenarios'] && scripts['test:extensions:memory'] && scripts.openclaw && fs.existsSync('openclaw.mjs') && fs.existsSync('scripts/profile-extension-memory.mjs') ? 0 : 1)"; then
if ! node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:gateway:cpu-scenarios'] && scripts['test:extensions:memory'] && scripts.openclaw && fs.existsSync('openclaw.mjs') && fs.existsSync('scripts/profile-extension-memory.mts') ? 0 : 1)"; then
cat > "$SOURCE_PERF_DIR/index.md" <<EOF
# OpenClaw Source Performance
@@ -745,15 +751,15 @@ jobs:
- Tested ref: ${TESTED_REF}
- Tested SHA: ${TESTED_SHA}
- Required scripts: test:gateway:cpu-scenarios, test:extensions:memory, openclaw, openclaw.mjs, scripts/profile-extension-memory.mjs
- Required scripts: test:gateway:cpu-scenarios, test:extensions:memory, openclaw, openclaw.mjs, scripts/profile-extension-memory.mts
EOF
cat "$SOURCE_PERF_DIR/index.md" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# target_ref may predate the dedicated profile; preserve the historical full build there.
if node -e "import('./scripts/build-all.mjs').then((module) => process.exit(module.BUILD_ALL_PROFILES?.sourcePerformance ? 0 : 1)).catch(() => process.exit(1))"; then
OPENCLAW_BUILD_PRIVATE_QA=1 node scripts/build-all.mjs sourcePerformance
if node --import tsx -e "import('./scripts/build-all.mts').then((module) => process.exit(module.BUILD_ALL_PROFILES?.sourcePerformance ? 0 : 1)).catch(() => process.exit(1))"; then
OPENCLAW_BUILD_PRIVATE_QA=1 node --import tsx scripts/build-all.mts sourcePerformance
else
pnpm build
fi
@@ -918,7 +924,7 @@ jobs:
echo "SQLite state smoke probe is not available in ${TESTED_REF}; continuing with the remaining source probes." >> "$GITHUB_STEP_SUMMARY"
fi
summary_args=(node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
summary_args=(node --import tsx "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mts" \
--source-dir "$SOURCE_PERF_DIR" \
--output "$SOURCE_PERF_DIR/index.md")
if [[ -n "${SOURCE_PERF_BASELINE_DIR:-}" && -d "$SOURCE_PERF_BASELINE_DIR" ]]; then
@@ -728,7 +728,7 @@ jobs:
--required-plugin-packages-json "$required_packages"
)
fi
node scripts/resolve-openclaw-package-candidate.mjs \
node --import tsx scripts/resolve-openclaw-package-candidate.mts \
"${source_args[@]}" \
"${registry_args[@]}" \
--output-dir .artifacts/docker-e2e-package \
@@ -1253,7 +1253,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Run parity lane
id: run_lane
@@ -1434,7 +1434,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Generate parity report
id: generate_report
@@ -1517,7 +1517,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Run runtime-pair lane
id: candidate_runtime_pair
@@ -1581,7 +1581,7 @@ jobs:
if [[ "$CANDIDATE_SUITE_OUTCOME" != "success" ]]; then
validator_args+=(--require-explicit-gap)
fi
node trusted-suite-validator/scripts/validate-qa-runtime-pair-summary.mjs "${validator_args[@]}"
node --import tsx trusted-suite-validator/scripts/validate-qa-runtime-pair-summary.mts "${validator_args[@]}"
if [[ "$CANDIDATE_SUITE_OUTCOME" != "success" ]]; then
echo "::notice::Trusted workflow validation accepted frozen-candidate runtime-pair evidence after its suite failed."
fi
@@ -1653,7 +1653,7 @@ jobs:
if [[ "$CANDIDATE_REPORT_OUTCOME" != "success" ]]; then
validator_args+=(--require-explicit-gap)
fi
node trusted-report-validator/scripts/validate-qa-runtime-pair-summary.mjs "${validator_args[@]}"
node --import tsx trusted-report-validator/scripts/validate-qa-runtime-pair-summary.mts "${validator_args[@]}"
if [[ "$CANDIDATE_REPORT_OUTCOME" != "success" ]]; then
echo "::notice::Trusted workflow validation accepted the frozen-candidate runtime-pair report after its reporter failed."
fi
@@ -2127,7 +2127,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Run Discord live lane
id: run_lane
@@ -2226,7 +2226,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Run WhatsApp live lane
id: run_lane
@@ -2322,7 +2322,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Run Slack live lane
id: run_lane
@@ -779,7 +779,7 @@ jobs:
changelog_file="${RUNNER_TEMP}/CHANGELOG.md"
notes_file="${RUNNER_TEMP}/release-notes.md"
git show "${TARGET_SHA}:CHANGELOG.md" > "${changelog_file}"
node scripts/render-github-release-notes.mjs \
node --import tsx scripts/render-github-release-notes.mts \
--changelog "${changelog_file}" \
--tag "${RELEASE_TAG}" \
--repository "${GITHUB_REPOSITORY}" \
@@ -1399,7 +1399,7 @@ jobs:
echo "openclaw@${release_version} is already published; openclaw_npm_resume_run_id is required to bind postpublish proof to the original workflow identity." >&2
exit 1
fi
resume_state="$(node "${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-resume-run.mjs" \
resume_state="$(node --import tsx "${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-resume-run.mts" \
--repo "${GITHUB_REPOSITORY}" \
--run-id "${OPENCLAW_NPM_RESUME_RUN_ID}")"
resume_url="$(printf '%s' "${resume_state}" | jq -er '.url')"
@@ -1469,7 +1469,7 @@ jobs:
local metadata_file="${3:-}"
local changelog_file="${RUNNER_TEMP}/CHANGELOG.md"
local -a render_args=(
node scripts/render-github-release-notes.mjs
node --import tsx scripts/render-github-release-notes.mts
--changelog "${changelog_file}"
--tag "${RELEASE_TAG}"
--repository "${GITHUB_REPOSITORY}"
@@ -1514,12 +1514,12 @@ jobs:
RELEASE_CHANGELOG_FILE="${changelog_file}" \
RELEASE_REPOSITORY="${GITHUB_REPOSITORY}" \
RELEASE_TAG="${RELEASE_TAG}" \
node --input-type=module <<'NODE'
node --import tsx --input-type=module <<'NODE'
import { readFileSync } from "node:fs";
import {
releaseNotesVersionForTag,
verifyGithubReleaseNotes,
} from "./scripts/render-github-release-notes.mjs";
} from "./scripts/render-github-release-notes.mts";
const body = readFileSync(process.env.RELEASE_BODY_FILE, "utf8");
const changelog = readFileSync(process.env.RELEASE_CHANGELOG_FILE, "utf8");
@@ -477,7 +477,7 @@ jobs:
OPENCLAW_BUILD_PRIVATE_QA=1 \
PATH="$PATH" \
RUNNER_TEMP="$RUNNER_TEMP" \
pnpm exec node scripts/build-all.mjs qaRuntime
pnpm exec node --import tsx scripts/build-all.mts qaRuntime
- name: Move built candidate outside trusted workspace
id: move_candidate
@@ -539,7 +539,7 @@ jobs:
pnpm_version="$(pnpm --version)"
jq -n \
--arg archiveRoot "$archive_root" \
--arg buildCommand "node scripts/build-all.mjs qaRuntime" \
--arg buildCommand "node --import tsx scripts/build-all.mts qaRuntime" \
--arg candidateSha "$TARGET_SHA" \
--arg candidateTree "$CANDIDATE_TREE" \
--arg nodeVersion "$node_version" \
@@ -748,7 +748,7 @@ jobs:
.candidateSha == $candidateSha and
.candidateTree == $candidateTree and
.archiveRoot == "openclaw-telegram-candidate" and
.buildCommand == "node scripts/build-all.mjs qaRuntime" and
.buildCommand == "node --import tsx scripts/build-all.mts qaRuntime" and
.sourceJob == "build_candidate"
' "$manifest_path" >/dev/null
[[ -d "$candidate_root/node_modules" && -f "$candidate_root/dist/index.js" ]]
@@ -899,7 +899,7 @@ jobs:
id: build_harness
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Validate candidate artifact metadata
id: metadata
+2 -2
View File
@@ -575,7 +575,7 @@ jobs:
}
fi
node scripts/resolve-openclaw-package-candidate.mjs \
node --import tsx scripts/resolve-openclaw-package-candidate.mts \
--source "$SOURCE" \
--package-ref "$PACKAGE_REF" \
--package-spec "$PACKAGE_SPEC" \
@@ -707,7 +707,7 @@ jobs:
--pre-date 2026-03-15T00:00:00Z
)
fi
node scripts/resolve-upgrade-survivor-baselines.mjs "${args[@]}" >/dev/null
node --import tsx scripts/resolve-upgrade-survivor-baselines.mts "${args[@]}" >/dev/null
echo "baseline=$fallback_baseline" >> "$GITHUB_OUTPUT"
- name: Upload package-under-test artifact
+1 -1
View File
@@ -537,7 +537,7 @@ jobs:
package_slug="${package_name#@openclaw/}"
output_dir="${artifact_root}/packages/${package_slug}"
mkdir -p "${output_dir}"
node scripts/check-plugin-npm-runtime-builds.mjs --package "${package_dir}"
node --import tsx scripts/check-plugin-npm-runtime-builds.mts --package "${package_dir}"
OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR="${output_dir}" \
PACKAGE_TAG="${package_tag}" \
bash .release-harness/scripts/plugin-clawhub-publish.sh --pack "${package_dir}"
+1 -1
View File
@@ -340,7 +340,7 @@ jobs:
install-deps: "true"
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
run: node --import tsx scripts/check-plugin-npm-runtime-builds.mts --package "${{ matrix.plugin.packageDir }}"
- name: Install pinned ClawHub CLI wrapper
run: |
+39 -12
View File
@@ -10,17 +10,20 @@ on:
- "extensions/**"
- "package.json"
- "scripts/generate-npm-package-lock.mjs"
- "scripts/generate-npm-package-lock.mts"
- "scripts/lib/npm-publish-plan.mjs"
- "scripts/lib/npm-json-output.mjs"
- "scripts/lib/npm-json-output.mts"
- "scripts/lib/release-version.mjs"
- "scripts/lib/plugin-npm-package-manifest.mjs"
- "scripts/lib/plugin-npm-package-manifest.mts"
- "scripts/lib/tsx-cli-shim.mjs"
- "scripts/lib/plugin-npm-release.ts"
- "scripts/lib/actions-artifact-archive.mjs"
- "scripts/plugin-npm-publish.sh"
- "scripts/plugin-publication-artifact.mjs"
- "scripts/plugin-npm-release-check.ts"
- "scripts/plugin-npm-release-plan.ts"
- "scripts/verify-plugin-npm-published-runtime.mjs"
- "scripts/verify-plugin-npm-published-runtime.mts"
workflow_dispatch:
inputs:
publish_scope:
@@ -339,8 +342,11 @@ jobs:
fetch-depth: 1
sparse-checkout: |
scripts/generate-npm-package-lock.mjs
scripts/lib/npm-json-output.mjs
scripts/generate-npm-package-lock.mts
scripts/lib/npm-json-output.mts
scripts/lib/plugin-npm-package-manifest.mjs
scripts/lib/plugin-npm-package-manifest.mts
scripts/lib/tsx-cli-shim.mjs
sparse-checkout-cone-mode: false
- name: Overlay trusted packaging helper
@@ -350,11 +356,20 @@ jobs:
.release-tooling/scripts/generate-npm-package-lock.mjs \
scripts/generate-npm-package-lock.mjs
cp \
.release-tooling/scripts/lib/npm-json-output.mjs \
scripts/lib/npm-json-output.mjs
.release-tooling/scripts/generate-npm-package-lock.mts \
scripts/generate-npm-package-lock.mts
cp \
.release-tooling/scripts/lib/npm-json-output.mts \
scripts/lib/npm-json-output.mts
cp \
.release-tooling/scripts/lib/plugin-npm-package-manifest.mjs \
scripts/lib/plugin-npm-package-manifest.mjs
cp \
.release-tooling/scripts/lib/plugin-npm-package-manifest.mts \
scripts/lib/plugin-npm-package-manifest.mts
cp \
.release-tooling/scripts/lib/tsx-cli-shim.mjs \
scripts/lib/tsx-cli-shim.mjs
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -403,9 +418,9 @@ jobs:
OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR="${artifact_dir}" \
bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}" > "${pack_output}"
node --input-type=module - "${pack_output}" "${pack_json}" <<'NODE'
node --import tsx --input-type=module - "${pack_output}" "${pack_json}" <<'NODE'
import fs from "node:fs";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mts";
const raw = fs.readFileSync(process.argv[2], "utf8").trim();
let pack;
@@ -1245,11 +1260,23 @@ jobs:
scripts/generate-npm-package-lock.mjs \
.publication-target/scripts/generate-npm-package-lock.mjs
cp \
scripts/lib/npm-json-output.mjs \
.publication-target/scripts/lib/npm-json-output.mjs
scripts/generate-npm-package-lock.mts \
.publication-target/scripts/generate-npm-package-lock.mts
cp \
scripts/lib/npm-json-output.mts \
.publication-target/scripts/lib/npm-json-output.mts
cp \
scripts/lib/plugin-npm-package-manifest.mjs \
.publication-target/scripts/lib/plugin-npm-package-manifest.mjs
cp \
scripts/lib/plugin-npm-package-manifest.mts \
.publication-target/scripts/lib/plugin-npm-package-manifest.mts
cp \
scripts/lib/tsx-cli-shim.mjs \
.publication-target/scripts/lib/tsx-cli-shim.mjs
cp \
scripts/lib/tsx-cli-shim.mjs \
.publication-target/scripts/lib/tsx-cli-shim.mjs
- name: Setup OIDC publication target
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc'
@@ -1286,7 +1313,7 @@ jobs:
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
run: node --import tsx scripts/verify-plugin-npm-published-runtime.mts "${PACKAGE_NAME}@${PACKAGE_VERSION}"
- name: Check bootstrap npm package version
id: bootstrap_npm_package_version
@@ -1385,14 +1412,14 @@ jobs:
env:
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }}
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
run: node --import tsx scripts/verify-plugin-npm-published-runtime.mts "${PACKAGE_NAME}@${PACKAGE_VERSION}"
- name: Verify immutable npm readback runtime
if: steps.publication_evidence.outputs.publish_route == 'npm-readback'
env:
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }}
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
run: node --import tsx scripts/verify-plugin-npm-published-runtime.mts "${PACKAGE_NAME}@${PACKAGE_VERSION}"
- name: Record Meta trusted publisher checkpoint
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap'
+35 -9
View File
@@ -67,14 +67,27 @@ jobs:
persist-credentials: false
submodules: false
- name: Setup manifest TypeScript runtime
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.x"
- name: Setup manifest pnpm
uses: ./.github/actions/setup-pnpm-store-cache
with:
node-version: "24.x"
- name: Install manifest dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Build plugin prerelease manifest
id: manifest
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }}
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
node --import tsx --input-type=module <<'EOF'
import { appendFileSync, existsSync } from "node:fs";
import { execFileSync } from "node:child_process";
const createMatrix = (include) => ({ include });
@@ -95,9 +108,19 @@ jobs:
let extensionShards = [];
let nodeShards = [];
const targetPlanPaths = {
"plugin-prerelease-test-plan": "./scripts/lib/plugin-prerelease-test-plan.mts",
"extension-test-plan": "./scripts/lib/extension-test-plan.mts",
"ci-node-test-plan": "./scripts/lib/ci-node-test-plan.mts",
};
const targetPlanPath = (name) => {
const mtsPath = targetPlanPaths[name];
return existsSync(mtsPath) ? mtsPath : mtsPath.replace(/\.mts$/u, ".mjs");
};
try {
const { assertPluginPrereleaseTestPlanComplete } = await import(
"./scripts/lib/plugin-prerelease-test-plan.mjs"
targetPlanPath("plugin-prerelease-test-plan")
);
pluginPrereleasePlan = assertPluginPrereleaseTestPlanComplete();
} catch (error) {
@@ -107,7 +130,8 @@ jobs:
error && typeof error === "object" && "url" in error ? String(error.url) : "";
if (
errorCode === "ERR_MODULE_NOT_FOUND" &&
moduleUrl.endsWith("/scripts/lib/plugin-prerelease-test-plan.mjs")
(moduleUrl.endsWith("/scripts/lib/plugin-prerelease-test-plan.mjs") ||
moduleUrl.endsWith("/scripts/lib/plugin-prerelease-test-plan.mts"))
) {
console.warn(
"Plugin prerelease plan unavailable in target ref; skipping static and Docker plugin prerelease lanes.",
@@ -119,7 +143,7 @@ jobs:
try {
const { createExtensionTestShards, DEFAULT_EXTENSION_TEST_SHARD_COUNT } = await import(
"./scripts/lib/extension-test-plan.mjs"
targetPlanPath("extension-test-plan")
);
extensionShards = createExtensionTestShards({
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
@@ -146,7 +170,8 @@ jobs:
error && typeof error === "object" && "url" in error ? String(error.url) : "";
if (
errorCode === "ERR_MODULE_NOT_FOUND" &&
moduleUrl.endsWith("/scripts/lib/extension-test-plan.mjs")
(moduleUrl.endsWith("/scripts/lib/extension-test-plan.mjs") ||
moduleUrl.endsWith("/scripts/lib/extension-test-plan.mts"))
) {
console.warn(
"Extension test plan unavailable in target ref; skipping extension prerelease shards.",
@@ -157,7 +182,7 @@ jobs:
}
try {
const { createNodeTestShards } = await import("./scripts/lib/ci-node-test-plan.mjs");
const { createNodeTestShards } = await import(targetPlanPath("ci-node-test-plan"));
nodeShards = createNodeTestShards({
includeReleaseOnlyPluginShards: true,
})
@@ -178,7 +203,8 @@ jobs:
error && typeof error === "object" && "url" in error ? String(error.url) : "";
if (
errorCode === "ERR_MODULE_NOT_FOUND" &&
moduleUrl.endsWith("/scripts/lib/ci-node-test-plan.mjs")
(moduleUrl.endsWith("/scripts/lib/ci-node-test-plan.mjs") ||
moduleUrl.endsWith("/scripts/lib/ci-node-test-plan.mts"))
) {
console.warn(
"Node test plan unavailable in target ref; skipping release-only plugin Node shard.",
@@ -322,7 +348,7 @@ jobs:
const result = spawnSync(
"pnpm",
["exec", "node", "scripts/test-projects.mjs", ...configs],
["exec", "node", "--import", "tsx", "scripts/test-projects.mts", ...configs],
{
env: childEnv,
stdio: "inherit",
+2 -2
View File
@@ -290,10 +290,10 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: node --import tsx scripts/build-all.mts qaRuntime
- name: Ensure Playwright Chromium
run: node scripts/ensure-playwright-chromium.mjs
run: node --import tsx scripts/ensure-playwright-chromium.mts
- name: Run QA profile
id: run_profile
@@ -60,6 +60,7 @@ jobs:
"scripts/periphery-intersection.mjs",
"scripts/ios-configure-signing.sh",
"scripts/ios-write-swift-filelist.mjs",
"scripts/ios-write-swift-filelist.mts",
"scripts/ios-write-version-xcconfig.sh",
"test/scripts/periphery-intersection.test.ts",
], { ignoreReturnCode: true, silent: true });
+3 -3
View File
@@ -41,9 +41,9 @@ jobs:
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'EOF'
node --import tsx --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import { createNodeTestShards } from "./scripts/lib/ci-node-test-plan.mjs";
import { createNodeTestShards } from "./scripts/lib/ci-node-test-plan.mts";
// Warm the selected planner envelopes for the striped unit-fast graph
// plus import-bound graphs that remained cold in protected-cache readers.
@@ -90,4 +90,4 @@ jobs:
EOF
- name: Warm transform and compile caches
run: node scripts/ci-run-node-test-shard.mjs
run: node --import tsx scripts/ci-run-node-test-shard.mts
+1 -1
View File
@@ -170,7 +170,7 @@ Skills own workflows; root owns hard policy and routing. Product direction and m
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
- Formatting: `oxfmt`, not Prettier. Write paths with `pnpm format <paths>`; no `format:write` script. Checks use repo wrappers (`pnpm format:*`, `scripts/run-oxlint.mjs`; full `pnpm lint:*` only when scope requires).
- SDK surface gate: `pnpm plugin-sdk:surface:check`; no `plugin-sdk:surface-report` script.
- `scripts/*.mjs` exports: matching declaration in sibling `.d.mts` mandatory. `pnpm check:script-declarations` (check-guards) + `check-test-types` enforce; new export without declaration = red CI.
- Script implementations use TypeScript where their runtime supports `tsx`; plain-Node lifecycle, packaged, Docker, and loader closures remain JavaScript and are included in the scripts program through `allowJs`.
- Script wrappers: failing or crashed run must end with one final `[tool] FAILED (exit N)` stderr line; crash = nonzero exit. Truncated output must never read as success. Pattern: `scripts/run-oxlint.mjs`.
- Tooling crash `Cannot find module ...` right after pulling/merging main = stale `node_modules`, not a code bug. `pnpm install` first; only then debug.
- Build before push when build output, packaging, lazy/module boundaries, dynamic imports, or published surfaces can change; agent builds default to the selected remote box unless platform-specific proof requires another remote host.
+3 -3
View File
@@ -63,9 +63,9 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
- These commands also cover the shared seam/smoke files that the default unit lane skips
- If you changed broader runtime behavior, still run the relevant wider lanes (`pnpm test:extensions`, `pnpm test:channels`, or `pnpm test`) before asking for review
- If you touched bundled-plugin boundaries in shared code, run the matching inventories:
- `node scripts/check-src-extension-import-boundary.mjs --json` for `src/**`
- `node scripts/check-sdk-package-extension-import-boundary.mjs --json` for `src/plugin-sdk/**` and `packages/**`
- `node scripts/check-test-helper-extension-import-boundary.mjs --json` for `test/helpers/**`
- `node --import tsx scripts/check-src-extension-import-boundary.mts --json` for `src/**`
- `node --import tsx scripts/check-sdk-package-extension-import-boundary.mts --json` for `src/plugin-sdk/**` and `packages/**`
- `node --import tsx scripts/check-test-helper-extension-import-boundary.mts --json` for `test/helpers/**`
- Shared test helpers must use `src/test-utils/bundled-plugin-public-surface.ts` instead of repo-relative `extensions/**` imports. Keep plugin-local deep mocks inside the owning bundled plugin package.
- If you are using an AI coding agent with OpenClaw skills available, run the `autoreview` skill before opening or updating your PR. Address accepted/actionable findings before asking for review.
- Do not submit refactor-only PRs unless a maintainer explicitly requested that refactor for an active fix or deliverable.
+1 -1
View File
@@ -75,7 +75,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs scripts/prepare-git-hooks.mjs ./scripts/
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/windows-cmd-helpers.mjs scripts/prepare-git-hooks.mjs ./scripts/
COPY scripts/lib/guard-inventory-utils.mjs ./scripts/lib/guard-inventory-utils.mjs
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
+1 -1
View File
@@ -275,7 +275,7 @@ Security boundary notes:
- Enforcement reference points:
- temp root resolver: `src/infra/tmp-openclaw-dir.ts`
- SDK temp helpers: `src/plugin-sdk/temp-path.ts`
- messaging/channel tmp guardrail: `scripts/check-no-random-messaging-tmp.mjs`
- messaging/channel tmp guardrail: `scripts/check-no-random-messaging-tmp.mts`
### Operational Guidance
+5 -3
View File
@@ -152,7 +152,9 @@ abstract class StageCanvasA2uiTask
workingDir(root)
commandLine(
"node",
"scripts/sync-native-a2ui.mjs",
"--import",
"tsx",
"scripts/sync-native-a2ui.mts",
"--write",
"--output",
outputDirectory
@@ -172,8 +174,8 @@ val stageCanvasA2ui =
sourceFiles.from(
openClawRepositoryRoot.resolve("package.json"),
openClawRepositoryRoot.resolve("pnpm-lock.yaml"),
openClawRepositoryRoot.resolve("scripts/bundle-a2ui.mjs"),
openClawRepositoryRoot.resolve("scripts/sync-native-a2ui.mjs"),
openClawRepositoryRoot.resolve("scripts/bundle-a2ui.mts"),
openClawRepositoryRoot.resolve("scripts/sync-native-a2ui.mts"),
openClawRepositoryRoot.resolve("extensions/canvas/package.json"),
openClawRepositoryRoot.resolve("extensions/canvas/scripts/bundle-a2ui.mjs"),
openClawRepositoryRoot.resolve("extensions/canvas/src/host/a2ui/index.html"),
+1 -1
View File
@@ -117,7 +117,7 @@ targets:
resource_bundle="$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/OpenClawKit_OpenClawKit.bundle"
test -d "$resource_product"
test -d "$resource_bundle"
node "$repo_root/scripts/sync-native-a2ui.mjs" \
node --import tsx "$repo_root/scripts/sync-native-a2ui.mts" \
--write \
--output "$resource_product/CanvasA2UI"
rm -rf "$resource_bundle/CanvasA2UI"
+1 -1
View File
@@ -59,7 +59,7 @@ The running app gives the headless `openclaw node run` host a single Canvas WebV
The Canvas plugin sources remain the source of truth for the A2UI renderer. Each native build
generates `index.html` and `a2ui.bundle.js` into its isolated build output before compiling. Run
`node scripts/sync-native-a2ui.mjs --check` from the repository root to verify fresh bundles are
`node --import tsx scripts/sync-native-a2ui.mts --check` from the repository root to verify fresh bundles are
byte-identical and every native build owner is wired.
## Quick Chat widgets
+9 -3
View File
@@ -33,8 +33,8 @@ fn stage_canvas_a2ui() {
for input in [
"package.json",
"pnpm-lock.yaml",
"scripts/bundle-a2ui.mjs",
"scripts/sync-native-a2ui.mjs",
"scripts/bundle-a2ui.mts",
"scripts/sync-native-a2ui.mts",
"extensions/canvas/package.json",
"extensions/canvas/scripts/bundle-a2ui.mjs",
"extensions/canvas/src/host/a2ui/index.html",
@@ -44,7 +44,13 @@ fn stage_canvas_a2ui() {
}
let status = Command::new("node")
.args(["scripts/sync-native-a2ui.mjs", "--write", "--output"])
.args([
"--import",
"tsx",
"scripts/sync-native-a2ui.mts",
"--write",
"--output",
])
.arg(&output_dir)
.current_dir(&repo_root)
.status()
@@ -6,7 +6,7 @@ struct HostEnvOverrideDiagnostics: Equatable {
}
enum HostEnvSanitizer {
/// Generated from src/infra/host-env-security-policy.json via scripts/generate-host-env-security-policy-swift.mjs.
/// Generated from src/infra/host-env-security-policy.json via scripts/generate-host-env-security-policy-swift.mts.
/// Parity is validated by src/infra/host-env-security.policy-parity.test.ts.
private static let blockedInheritedKeys = HostEnvSecurityPolicy.blockedInheritedKeys
private static let blockedInheritedPrefixes = HostEnvSecurityPolicy.blockedInheritedPrefixes
@@ -1,6 +1,6 @@
// Generated file. Do not edit directly.
// Source: src/infra/host-env-security-policy.json
// Regenerate: node scripts/generate-host-env-security-policy-swift.mjs --write
// Regenerate: node --import tsx scripts/generate-host-env-security-policy-swift.mts --write
import Foundation
+1 -1
View File
@@ -61,7 +61,7 @@ const ROOT_TEST_ENTRY_GLOBS = [
// QA scenario YAML dispatches these scripts/tests by path rather than import.
...QA_SCENARIO_EXECUTION_ENTRIES,
// Invoked directly by the sandbox bind-conflict E2E verification script.
"scripts/e2e-sandbox-bind-conflict.mjs!",
"scripts/e2e-sandbox-bind-conflict.mts!",
// The Voice Call QA scenario loads this fixture through a generated plugin directory.
"test/e2e/qa-lab/runtime/fixtures/voice-call-runtime-plugin/index.js!",
// Loaded with cache-busting query strings so configuration fallback tests
+32 -11
View File
@@ -1,6 +1,9 @@
/**
* Knip configuration for OpenClaw root and bundled plugin dependency hygiene.
*/
import fs from "node:fs";
import path from "node:path";
const BUNDLED_PLUGIN_ROOT_DIR = "extensions";
function bundledPluginFile(pluginId: string, relativePath: string, suffix = ""): string {
@@ -16,13 +19,14 @@ const repositoryScriptEntries = [
".github/actions/register-bind-mount-cleanup/main.cjs!",
".github/actions/register-bind-mount-cleanup/post.cjs!",
"apps/android/scripts/build-release-artifacts.ts!",
"scripts/build-discord-activity-sdk.mjs!",
"scripts/bundle-a2ui.mts!",
"scripts/build-discord-activity-sdk.mts!",
"scripts/check-live-cache.ts!",
"scripts/check-package-dist-imports.mjs!",
"scripts/dev/ios-node-e2e.ts!",
"scripts/diffs-shiki-curated.ts!",
// Reusable Docker workflows invoke this from the downloaded .release-harness tree.
"scripts/docker-e2e.mjs!",
"scripts/docker-e2e.mts!",
"scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs!",
"scripts/e2e/lib/browser-cdp-snapshot/fixture-server.mjs!",
"scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs!",
@@ -40,7 +44,7 @@ const repositoryScriptEntries = [
"scripts/e2e/lib/fixtures/config.mjs!",
"scripts/e2e/lib/fixtures/plugins.mjs!",
"scripts/e2e/lib/fixtures/workspace.mjs!",
"scripts/e2e/lib/npm-telegram-live/prepare-package.mjs!",
"scripts/e2e/lib/npm-telegram-live/prepare-package.mts!",
"scripts/e2e/lib/onboard/assert-config.mjs!",
"scripts/e2e/lib/onboard/write-config.mjs!",
"scripts/e2e/lib/openai-chat-tools/client.mjs!",
@@ -61,9 +65,9 @@ const repositoryScriptEntries = [
"scripts/fixtures/packed-plugin-sdk-type-smoke.ts!",
"scripts/ios-release-cut.ts!",
"scripts/ios-release-plan.ts!",
"scripts/ios-release-signing.mjs!",
"scripts/ios-release-signing.mts!",
"scripts/lib/docker-plugin-selection.mjs!",
"scripts/lib/openclaw-test-state.mjs!",
"scripts/lib/openclaw-test-state.mts!",
"scripts/list-prod-store-packages.mjs!",
// Invoked by scripts/lib/live-docker-stage.sh during container validation.
"scripts/live-docker-normalize-config.ts!",
@@ -73,10 +77,10 @@ const repositoryScriptEntries = [
"scripts/openclaw-release-clawhub-runtime-state.ts!",
// Oxlint loads this JS plugin by path from config/oxlint/boundary-guards.json.
"scripts/oxlint-boundary-guards.mjs!",
"scripts/plugin-prerelease-liveish-matrix.mjs!",
"scripts/plugin-prerelease-liveish-matrix.mts!",
// Generates the checked-in native protocol models from core descriptor metadata.
"scripts/protocol-gen.ts!",
"scripts/pr-gates-lock.mjs!",
"scripts/pr-gates-lock.mts!",
"scripts/pr-lib/ci-dispatch.mjs!",
"scripts/pr-lib/review-artifacts.mjs!",
"scripts/pr-lib/process-group-runner.mjs!",
@@ -87,7 +91,7 @@ const repositoryScriptEntries = [
"scripts/secrets/openclaw-bws-resolver.mjs!",
"scripts/sqlite-session-entry-cache-lifetime-proof.ts!",
"scripts/sync-labels.ts!",
"scripts/test-built-bundled-channel-entry-smoke.mjs!",
"scripts/test-built-bundled-channel-entry-smoke.mts!",
"scripts/update-clawtributors.ts!",
"scripts/verify-stable-main-closeout.mjs!",
"scripts/write-package-dist-inventory.ts!",
@@ -97,8 +101,25 @@ const repositoryScriptEntries = [
"skills/meme-maker/scripts/meme.mjs!",
] as const;
// Compatibility shims are executable roots and load their typed implementations by computed URL,
// which Knip cannot follow in either direction.
function listScriptShimEntries(dir = "scripts"): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return listScriptShimEntries(entryPath);
}
if (!entry.isFile() || (!entry.name.endsWith(".mjs") && !entry.name.endsWith(".js"))) {
return [];
}
const implementationPath = entryPath.replace(/\.(?:mjs|js)$/u, ".mts");
return fs.existsSync(implementationPath) ? [`${entryPath}!`, `${implementationPath}!`] : [];
});
}
const rootEntries = [
...repositoryScriptEntries,
...listScriptShimEntries(),
// Knip loads these audit configurations directly by command-line path.
"config/knip.config.ts!",
"config/knip.all-exports.config.ts!",
@@ -161,7 +182,7 @@ const rootEntries = [
// Package-script owners invoke these generated-artifact modules directly.
"src/config/doc-baseline.ts!",
"src/plugins/runtime-sidecar-paths-baseline.ts!",
// Imported by scripts/tsdown-build.mjs as the AI package build configuration.
// Imported by scripts/tsdown-build.mts as the AI package build configuration.
"tsdown.ai.config.ts!",
// Maintainer-owned compatibility data referenced by release/docs workflows.
"src/commands/doctor/shared/deprecation-compat.ts!",
@@ -266,7 +287,7 @@ const rootToolingAndWorkspaceDependencies = [
"@lit-labs/signals",
"@lit/context",
"@lit/task",
// scripts/ui.js anchors these lookups at ui/package.json before invoking the UI workspace.
// scripts/ui.mts anchors these lookups at ui/package.json before invoking the UI workspace.
"@vitest/browser-playwright",
"dompurify",
// Root typecheck/test projects compile @openclaw/net-policy source directly.
@@ -691,7 +712,7 @@ const config = {
[`${BUNDLED_PLUGIN_ROOT_DIR}/deepinfra`]: bundledPluginWorkspace(),
[`${BUNDLED_PLUGIN_ROOT_DIR}/discord`]: bundledPluginWorkspace(),
[`${BUNDLED_PLUGIN_ROOT_DIR}/diffs`]: bundledPluginWorkspace([
// scripts/build-diffs-viewer-runtime.mjs bundles this browser entry.
// scripts/build-diffs-viewer-runtime.mts bundles this browser entry.
"src/viewer-client.ts!",
]),
[`${BUNDLED_PLUGIN_ROOT_DIR}/elevenlabs`]: bundledPluginWorkspace(),
+13 -2
View File
@@ -5,10 +5,21 @@
* companion pass keeps the rest of scripts/** as library project files and
* makes repository tests real consumers of deliberately testable helpers.
*/
import fs from "node:fs";
import productionConfig from "./knip.config.ts";
const scriptEntries = productionConfig.workspaces["."].entry.filter((entry) =>
entry.startsWith("scripts/"),
function isTypedShimImplementationEntry(entry: string): boolean {
const filePath = entry.endsWith("!") ? entry.slice(0, -1) : entry;
// The export-free Crabbox implementation must remain a root so its library imports stay live.
if (!filePath.endsWith(".mts") || filePath === "scripts/crabbox-wrapper.mts") {
return false;
}
const basePath = filePath.slice(0, -".mts".length);
return fs.existsSync(`${basePath}.mjs`) || fs.existsSync(`${basePath}.js`);
}
const scriptEntries = productionConfig.workspaces["."].entry.filter(
(entry) => entry.startsWith("scripts/") && !isTypedShimImplementationEntry(entry),
);
const repositoryToolEntries = [
+1 -1
View File
@@ -20,7 +20,7 @@ excluded:
- "*.playground"
# Generated (protocol-gen-swift.ts)
- ../apps/macos/Sources/OpenClawProtocol/GatewayModels.swift
# Generated (generate-host-env-security-policy-swift.mjs)
# Generated (generate-host-env-security-policy-swift.mts)
- ../apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift
analyzer_rules:
+4 -4
View File
@@ -120,7 +120,7 @@ The slowest Node test families are split or balanced so each job stays small wit
- Node shard and build-artifact jobs also restore Node's portable on-disk compile cache through immutable Actions caches. Independent `test` and `build` namespaces prevent their writers from replacing each other's archives: the scheduled test warmer owns the protected test seed, while `build-artifacts` may publish at most one protected build archive per UTC day from trusted `main` pushes. PR and ordinary test jobs only read protected snapshots, so feature-branch bytecode never enters the shared seed and PR traffic creates no cache archives. This reuses V8 bytecode for Node-loaded orchestration, build tooling, and external dependencies across different checkout paths, including when only part of the source graph changes. Vitest child processes disable an inherited compile cache because coverage can be enabled inside dynamic configs and V8 coverage can lose source-position precision when scripts are deserialized from bytecode.
- The build-artifact job also persists content-fingerprinted `build-all` step outputs. CI's self-built plugin SDK declarations hash the complete repository-owned TypeScript/JSON source graph, exclude installed and generated directories, and restore both flat declarations and package bridges after `tsdown` clears `dist`. Documentation, workflow, plugin, and other changes outside that graph can reuse the declaration snapshot; source changes rebuild it before the export gate runs.
- Full declaration builds split `tsdown` into AI, workspace-package, and unified groups. Each group caches declarations only, then still rebuilds runtime JavaScript before restoring those declarations. Core or plugin changes therefore invalidate only the large unified graph, while workspace-package changes conservatively invalidate every dependent declaration group. Public full builds generally use an immutable Actions cache; coarse restore keys seed partial changes, per-group content fingerprints reject stale data, and GitHub's cache quota evicts old generations. The weekly Node 22 lane instead publishes a 14-day artifact after successful `main` runs and restores only artifacts whose immutable producer identity resolves to that workflow on `main`, avoiding quota churn without allowing PR code to write a shared cache. Private-QA declarations are never persisted in Actions caches because cache namespaces are not confidentiality boundaries.
- `check-additional-*` stripes the supplemental boundary guard list (`scripts/run-additional-boundary-checks.mjs`) into one prompt-heavy shard (`check-additional-boundaries-a`, which includes the Codex prompt snapshot drift check) and one combined shard for the remaining stripes (`check-additional-boundaries-bcd`), each running independent guards concurrently and printing per-check timings. Package-boundary compile/canary work stays together, and runtime topology architecture runs separately from the gateway watch coverage embedded in `build-artifacts`.
- `check-additional-*` stripes the supplemental boundary guard list (`scripts/run-additional-boundary-checks.mts`) into one prompt-heavy shard (`check-additional-boundaries-a`, which includes the Codex prompt snapshot drift check) and one combined shard for the remaining stripes (`check-additional-boundaries-bcd`), each running independent guards concurrently and printing per-check timings. Package-boundary compile/canary work stays together, and runtime topology architecture runs separately from the gateway watch coverage embedded in `build-artifacts`.
- On the 32-vCPU self-hosted build runner, Gateway watch, channel tests, and the core support-boundary shard start together inside `build-artifacts` after `dist/` and `dist-runtime/` are already built. GitHub-hosted fallback runs keep Gateway watch serial so low-core contention cannot consume its readiness deadline. Both paths then run the two built TUI PTY artifact canaries alone; the dedicated Node shard owns the full serial suite.
Once admitted, canonical Linux CI permits up to 28 concurrent Node test jobs and
@@ -219,7 +219,7 @@ ratchet-down when cleanup lowers the real count.
- `config/env-var-count-budget.txt` caps the number of distinct `OPENCLAW_*`
names in production source under `src/`, `packages/`, and `extensions/`
(tests and QA Lab excluded). Checked by `node scripts/check-env-var-count.mjs`.
(tests and QA Lab excluded). Checked by `node --import tsx scripts/check-env-var-count.mts`.
Removing env vars: lower the number in the same PR. Adding one is a
config-surface decision — justify it in the PR body.
- `docs/.generated/config-baseline.counts.json` caps the per-kind
@@ -521,7 +521,7 @@ The slow Bun global install image-provider smoke is separately gated by `run_bun
- a bare Node/Git runner for installer/update/plugin-dependency lanes;
- a functional image that installs the same tarball into `/app` for normal functionality lanes.
Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`, planner logic lives in `scripts/lib/docker-e2e-plan.mjs`, and the runner only executes the selected plan. The scheduler selects the image per lane with `OPENCLAW_DOCKER_E2E_BARE_IMAGE` and `OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`, then runs lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1`.
Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mts`, planner logic lives in `scripts/lib/docker-e2e-plan.mts`, and the runner only executes the selected plan. The scheduler selects the image per lane with `OPENCLAW_DOCKER_E2E_BARE_IMAGE` and `OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`, then runs lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1`.
### Tunables
@@ -679,7 +679,7 @@ Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by
- release metadata-only version bumps run targeted version/config/root-dependency checks;
- unknown root/config changes fail safe to all check lanes.
Local changed-test routing lives in `scripts/test-projects.test-support.mjs` and is intentionally cheaper than `check:changed`: direct test edits run themselves, source edits prefer explicit mappings, then sibling tests and import-graph dependents. Shared group-room delivery config is one of the explicit mappings: changes to the group visible-reply config, source reply delivery mode, or the message-tool system prompt route through the core reply tests plus Discord and Slack delivery regressions so a shared default change fails before the first PR push. Use `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` only when the change is harness-wide enough that the cheap mapped set is not a trustworthy proxy.
Local changed-test routing lives in `scripts/test-projects.test-support.mts` and is intentionally cheaper than `check:changed`: direct test edits run themselves, source edits prefer explicit mappings, then sibling tests and import-graph dependents. Shared group-room delivery config is one of the explicit mappings: changes to the group visible-reply config, source reply delivery mode, or the message-tool system prompt route through the core reply tests plus Discord and Slack delivery regressions so a shared default change fails before the first PR push. Use `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` only when the change is harness-wide enough that the cheap mapped set is not a trustworthy proxy.
## Testbox validation
+3 -3
View File
@@ -416,7 +416,7 @@ OPENCLAW_LIVE_CODEX_HARNESS=1 \
OPENCLAW_LIVE_CODEX_HARNESS_COMPACTION_STRESS_TURNS=8 \
OPENCLAW_LIVE_CODEX_HARNESS_LARGE_OUTPUT_BYTES=800000 \
OPENCLAW_LIVE_CODEX_HARNESS_DEBUG=1 \
node scripts/test-live.mjs --quiet src/gateway/gateway-codex-harness.live.test.ts
node --import tsx scripts/test-live.mts --quiet src/gateway/gateway-codex-harness.live.test.ts
```
GPT-5.6 native Codex matrix:
@@ -456,7 +456,7 @@ OPENCLAW_LIVE_OPENAI_LONG_CONTEXT=1 \
OPENCLAW_LIVE_OPENAI_LONG_CONTEXT_PROFILE=full \
OPENCLAW_LIVE_OPENAI_LONG_CONTEXT_METRICS=1 \
OPENCLAW_LIVE_OPENAI_LONG_CONTEXT_OUTPUT=1 \
node scripts/test-live.mjs --quiet src/gateway/gateway-openai-long-context.live.test.ts
node --import tsx scripts/test-live.mts --quiet src/gateway/gateway-openai-long-context.live.test.ts
```
Reduced-budget recipe:
@@ -466,7 +466,7 @@ OPENCLAW_LIVE_OPENAI_LONG_CONTEXT=1 \
OPENCLAW_LIVE_OPENAI_LONG_CONTEXT_PROFILE=reduced \
OPENCLAW_LIVE_OPENAI_LONG_CONTEXT_METRICS=1 \
OPENCLAW_LIVE_OPENAI_LONG_CONTEXT_OUTPUT=1 \
node scripts/test-live.mjs --quiet src/gateway/gateway-openai-long-context.live.test.ts
node --import tsx scripts/test-live.mts --quiet src/gateway/gateway-openai-long-context.live.test.ts
```
### Long-context hard oracles
+3 -3
View File
@@ -741,7 +741,7 @@ Native dependency policy:
committed diff and prints wall time plus macOS max RSS.
- `pnpm test:perf:changed:bench -- --worktree` benchmarks the current
dirty tree by routing the changed file list through
`scripts/test-projects.mjs` and the root Vitest config.
`scripts/test-projects.mts` and the root Vitest config.
- `pnpm test:perf:profile:main` writes a main-thread CPU profile for
Vitest/Vite startup and transform overhead.
- `pnpm test:perf:profile:runner` writes runner CPU+heap profiles for
@@ -882,9 +882,9 @@ These Docker runners split into two buckets:
`OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=45000`, and
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Set `OPENCLAW_LIVE_MAX_MODELS`
or the gateway env vars when you explicitly want a smaller cap or larger scan.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. If a single lane is heavier than the active caps, the scheduler can still start it when the pool is empty and then keeps it running alone until capacity is available again. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=5`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` (and other `OPENCLAW_DOCKER_ALL_<RESOURCE>_LIMIT` overrides) only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mts`; planner logic lives in `scripts/lib/docker-e2e-plan.mts`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. If a single lane is heavier than the active caps, the scheduler can still start it when the pool is empty and then keeps it running alone until capacity is available again. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=5`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` (and other `OPENCLAW_DOCKER_ALL_<RESOURCE>_LIMIT` overrides) only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
- `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, `source=trusted-url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. Profiles are ordered by breadth: `smoke`, `package`, `product`, and `full` (plus `custom` for an explicit lane list). See [Testing updates and plugins](/help/testing-updates-plugins) for the package/update/plugin contract, published-upgrade survivor matrix, release defaults, and failure triage.
- Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if that pre-dispatch bootstrap graph statically imports any external package (Commander, prompt UI, undici, logging, and similar startup-heavy deps all count) before command dispatch; it also caps the bundled gateway run chunk at 70 KB and rejects static imports of known cold gateway paths (`control-ui-assets`, `diagnostic-stability-bundle`, `onboard-helpers`, `process-respawn`, `restart-sentinel`, `server-close`, `server-reload-handlers`) from that chunk. `scripts/release-check.ts` separately smoke-tests the packed CLI with `--help`, `onboard --help`, `doctor --help`, `status --json --timeout 1`, `config schema`, and `models list --provider openai`.
- Build and release checks run `scripts/check-cli-bootstrap-imports.mts` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if that pre-dispatch bootstrap graph statically imports any external package (Commander, prompt UI, undici, logging, and similar startup-heavy deps all count) before command dispatch; it also caps the bundled gateway run chunk at 70 KB and rejects static imports of known cold gateway paths (`control-ui-assets`, `diagnostic-stability-bundle`, `onboard-helpers`, `process-respawn`, `restart-sentinel`, `server-close`, `server-reload-handlers`) from that chunk. `scripts/release-check.ts` separately smoke-tests the packed CLI with `--help`, `onboard --help`, `doctor --help`, `status --json --timeout 1`, `config schema`, and `models list --provider openai`.
- Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:release-user-journey`, `test:docker:release-typed-onboarding`, `test:docker:release-media-memory`, `test:docker:release-upgrade-user-journey`, `test:docker:release-plugin-marketplace`, `test:docker:skill-install`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:agent-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
- Docker/Bash E2E lanes that install the packed OpenClaw tarball through `scripts/lib/openclaw-e2e-instance.sh` cap `npm install` at `OPENCLAW_E2E_NPM_INSTALL_TIMEOUT` (default `600s`; set `0` to disable the wrapper for debugging).
+1 -1
View File
@@ -337,7 +337,7 @@ pnpm test src/plugins/contracts/runtime-seams.contract.test.ts
## Lint enforcement (in-repo plugins)
`scripts/run-additional-boundary-checks.mjs` runs a set of `lint:plugins:*`
`scripts/run-additional-boundary-checks.mts` runs a set of `lint:plugins:*`
import-boundary checks in CI; each can also be run standalone locally:
| Command | Enforces |
+2 -2
View File
@@ -1131,8 +1131,8 @@ compatibility fallback when the shared
never prints token material:
```bash
OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_GPT_LIVE=1 node scripts/test-live.mjs -- extensions/openai/realtime-quicksilver.live.test.ts
OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_GPT_LIVE=1 node scripts/test-live.mjs -- extensions/openai/realtime-quicksilver-gateway-bridge.live.test.ts
OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_GPT_LIVE=1 node --import tsx scripts/test-live.mts -- extensions/openai/realtime-quicksilver.live.test.ts
OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_GPT_LIVE=1 node --import tsx scripts/test-live.mts -- extensions/openai/realtime-quicksilver-gateway-bridge.live.test.ts
```
<Note>
+2 -2
View File
@@ -490,7 +490,7 @@ Use this box to answer "does the release behave correctly in QA scenarios and li
### Package
The Package box is the installable-product gate. It is backed by `Package Acceptance` and the resolver `scripts/resolve-openclaw-package-candidate.mjs`. The resolver normalizes a candidate into the `package-under-test` tarball consumed by Docker E2E, validates the package inventory, records the package version and SHA-256, and keeps the workflow harness ref separate from the package source ref.
The Package box is the installable-product gate. It is backed by `Package Acceptance` and the resolver `scripts/resolve-openclaw-package-candidate.mts`. The resolver normalizes a candidate into the `package-under-test` tarball consumed by Docker E2E, validates the package inventory, records the package version and SHA-256, and keeps the workflow harness ref separate from the package source ref.
Supported candidate sources:
@@ -719,7 +719,7 @@ If a maintainer must fall back to local npm authentication, run any 1Password CL
- [`.github/workflows/openclaw-release-checks.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-release-checks.yml)
- [`.github/workflows/openclaw-cross-os-release-checks-reusable.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-cross-os-release-checks-reusable.yml)
- [`.github/workflows/docker-release.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/docker-release.yml)
- [`scripts/resolve-openclaw-package-candidate.mjs`](https://github.com/openclaw/openclaw/blob/main/scripts/resolve-openclaw-package-candidate.mjs)
- [`scripts/resolve-openclaw-package-candidate.mts`](https://github.com/openclaw/openclaw/blob/main/scripts/resolve-openclaw-package-candidate.mts)
- [`scripts/openclaw-npm-release-check.ts`](https://github.com/openclaw/openclaw/blob/main/scripts/openclaw-npm-release-check.ts)
- [`scripts/package-mac-dist.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/package-mac-dist.sh)
- [`scripts/make_appcast.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/make_appcast.sh)
+3 -3
View File
@@ -95,7 +95,7 @@ Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` sum
- `src/test-utils/openclaw-test-state.ts`: use from Vitest when a test needs an isolated `HOME`, `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, config fixture, workspace, agent dir, or auth-profile store.
- `pnpm test:env-mutations:report`: non-blocking report of tests/harnesses that mutate `HOME`, `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, `OPENCLAW_WORKSPACE_DIR`, or related env keys directly. Use it to find migration candidates for the shared test-state helper.
- `test/helpers/openclaw-test-instance.ts`: process-level E2E tests needing a running Gateway, CLI env, log capture, and cleanup in one place.
- Docker/Bash E2E lanes that source `scripts/lib/docker-e2e-image.sh` can pass `docker_e2e_test_state_shell_b64 <label> <scenario>` into the container and decode it with `scripts/lib/openclaw-e2e-instance.sh`; multi-home scripts can pass `docker_e2e_test_state_function_b64` and call `openclaw_test_state_create <label> <scenario>` in each flow. `node scripts/lib/openclaw-test-state.mjs -- create --label <name> --scenario <name> --env-file <path> --json` writes a sourceable host env file (the `--` before `create` keeps newer Node runtimes from treating `--env-file` as a Node flag). Lanes that launch a Gateway can source `scripts/lib/openclaw-e2e-instance.sh` for entrypoint resolution, mock OpenAI startup, foreground/background launch, readiness probes, state env export, log dumps, and process cleanup.
- Docker/Bash E2E lanes that source `scripts/lib/docker-e2e-image.sh` can pass `docker_e2e_test_state_shell_b64 <label> <scenario>` into the container and decode it with `scripts/lib/openclaw-e2e-instance.sh`; multi-home scripts can pass `docker_e2e_test_state_function_b64` and call `openclaw_test_state_create <label> <scenario>` in each flow. `node --import tsx scripts/lib/openclaw-test-state.mts -- create --label <name> --scenario <name> --env-file <path> --json` writes a sourceable host env file (the `--` before `create` keeps newer Node runtimes from treating `--env-file` as a Node flag). Lanes that launch a Gateway can source `scripts/lib/openclaw-e2e-instance.sh` for entrypoint resolution, mock OpenAI startup, foreground/background launch, readiness probes, state env export, log dumps, and process cleanup.
## Control UI, TUI, and extension lanes
@@ -118,11 +118,11 @@ Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` sum
## Full Docker suite (`pnpm test:docker:all`)
Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes through a weighted scheduler. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it.
Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes through a weighted scheduler. `scripts/package-openclaw-for-docker.mjs` is the stable local/CI package packer entrypoint and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it.
- Bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`): installer/update/plugin-dependency lanes; mounts the prebuilt tarball instead of copied repo sources.
- Functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`): normal built-app functionality lanes.
- Lane definitions: `scripts/lib/docker-e2e-scenarios.mjs`. Planner: `scripts/lib/docker-e2e-plan.mjs`. Executor: `scripts/test-docker-all.mjs`.
- Lane definitions: `scripts/lib/docker-e2e-scenarios.mts`. Planner: `scripts/lib/docker-e2e-plan.mts`. Executor: `scripts/test-docker-all.mjs`.
- `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan (lanes, image kinds, package/live-image needs, state scenarios, credential checks) without building or running Docker.
Scheduling knobs (env vars, defaults in parentheses):
+1 -1
View File
@@ -25,7 +25,7 @@
"pluginApi": ">=2026.8.1"
},
"assetScripts": {
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs full"
"build": "node --import tsx ../../scripts/build-diffs-viewer-runtime.mts full"
},
"build": {
"openclawVersion": "2026.8.1",
+1 -1
View File
@@ -31,7 +31,7 @@
"pluginApi": ">=2026.8.1"
},
"assetScripts": {
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs curated"
"build": "node --import tsx ../../scripts/build-diffs-viewer-runtime.mts curated"
},
"build": {
"openclawVersion": "2026.8.1",
+7 -3
View File
@@ -36,9 +36,13 @@ export async function ensureCuratedViewerRuntimeForTests(): Promise<void> {
// The curated runtime is generated output. Source tests that serve viewer
// assets need a clean-checkout fixture before the normal build hook runs.
await execFileAsync(process.execPath, ["scripts/build-diffs-viewer-runtime.mjs", "curated"], {
cwd: repoRoot,
});
await execFileAsync(
process.execPath,
["--import", "tsx", "scripts/build-diffs-viewer-runtime.mts", "curated"],
{
cwd: repoRoot,
},
);
}
export async function createTempDiffRoot(prefix: string): Promise<{
+1 -1
View File
@@ -108,7 +108,7 @@
]
},
"assetScripts": {
"build": "node ../../scripts/build-discord-activity-sdk.mjs"
"build": "node --import tsx ../../scripts/build-discord-activity-sdk.mts"
},
"release": {
"publishToClawHub": true,
@@ -262,7 +262,7 @@ describe("qa test file scenario runner", () => {
expect(result.executionKind).toBe("playwright");
expect(commands.map((command) => command.args)).toEqual([
["scripts/ensure-playwright-chromium.mjs"],
["--import", "tsx", "scripts/ensure-playwright-chromium.mts"],
[
"scripts/run-vitest.mjs",
"run",
@@ -143,7 +143,7 @@ function playwrightSteps(
return [
{
command: process.execPath,
args: ["scripts/ensure-playwright-chromium.mjs"],
args: ["--import", "tsx", "scripts/ensure-playwright-chromium.mts"],
},
{
command: process.execPath,
+1 -1
View File
@@ -139,7 +139,7 @@ describe("qa web runtime", () => {
const launchOptions = requireLaunchOptions();
expect(spawnSync).toHaveBeenCalledWith(
process.execPath,
["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"],
["--import", "tsx", "scripts/ensure-playwright-chromium.mts", "--skip-ffmpeg"],
expect.objectContaining({ cwd: process.cwd(), stdio: "inherit" }),
);
expect(launchOptions?.channel).toBeUndefined();
+1 -1
View File
@@ -109,7 +109,7 @@ function resolveRunnableChromiumExecutablePath(): string | undefined {
function ensureChromiumAvailable(repoRoot: string) {
const result = spawnSync(
process.execPath,
["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"],
["--import", "tsx", "scripts/ensure-playwright-chromium.mts", "--skip-ffmpeg"],
{
cwd: repoRoot,
env: process.env,
+169 -168
View File
@@ -362,13 +362,14 @@
"!docs/**/*.jpg",
"!docs/**/*.png",
"src/agents/templates/",
"scripts/crabbox-routing-policy.mjs",
"scripts/crabbox-wrapper-providers.mjs",
"scripts/crabbox-routing-policy.mts",
"scripts/crabbox-wrapper-providers.mts",
"scripts/crabbox-wrapper.mjs",
"scripts/testbox-lease-freshness.mjs",
"scripts/crabbox-wrapper.mts",
"scripts/testbox-lease-freshness.mts",
"scripts/lib/tsx-cli-shim.mjs",
"patches/",
"skills/",
"scripts/npm-runner.mjs",
"scripts/prepare-git-hooks.mjs",
"scripts/preinstall-package-manager-warning.mjs",
"scripts/lib/official-external-channel-catalog.json",
@@ -1477,16 +1478,16 @@
"./cli-entry": "./openclaw.mjs"
},
"scripts": {
"android:assemble": "node scripts/run-android-gradle.mjs :app:assemblePlayDebug :wear:assembleDebug :wear-shared:assembleDebug",
"android:assemble:third-party": "node scripts/run-android-gradle.mjs :app:assembleThirdPartyDebug",
"android:assemble": "node --import tsx scripts/run-android-gradle.mts :app:assemblePlayDebug :wear:assembleDebug :wear-shared:assembleDebug",
"android:assemble:third-party": "node --import tsx scripts/run-android-gradle.mts :app:assembleThirdPartyDebug",
"android:bundle:release": "bash -lc 'source ./scripts/lib/android-fastlane.sh && cd apps/android && run_android_fastlane android play_store_archive'",
"android:format": "cd apps/android && ./gradlew :app:ktlintFormat :benchmark:ktlintFormat :wear:ktlintFormat :wear-shared:ktlintFormat",
"android:install": "node scripts/run-android-gradle.mjs :app:installPlayDebug",
"android:install:third-party": "node scripts/run-android-gradle.mjs :app:installThirdPartyDebug",
"android:install": "node --import tsx scripts/run-android-gradle.mts :app:installPlayDebug",
"android:install:third-party": "node --import tsx scripts/run-android-gradle.mts :app:installThirdPartyDebug",
"android:lint": "cd apps/android && ./gradlew :app:ktlintCheck :benchmark:ktlintCheck :wear:ktlintCheck :wear-shared:ktlintCheck",
"android:lint:android": "node scripts/run-android-gradle.mjs :app:lintPlayDebug :app:lintThirdPartyDebug :wear:lintDebug :wear-shared:lintDebug",
"android:run": "node scripts/run-android-gradle.mjs :app:installPlayDebug -- adb shell am start -n ai.openclaw.app/.MainActivity",
"android:run:third-party": "node scripts/run-android-gradle.mjs :app:installThirdPartyDebug -- adb shell am start -n ai.openclaw.app/.MainActivity",
"android:lint:android": "node --import tsx scripts/run-android-gradle.mts :app:lintPlayDebug :app:lintThirdPartyDebug :wear:lintDebug :wear-shared:lintDebug",
"android:run": "node --import tsx scripts/run-android-gradle.mts :app:installPlayDebug -- adb shell am start -n ai.openclaw.app/.MainActivity",
"android:run:third-party": "node --import tsx scripts/run-android-gradle.mts :app:installThirdPartyDebug -- adb shell am start -n ai.openclaw.app/.MainActivity",
"android:release": "bash scripts/android-release.sh",
"android:release:archive": "bash -lc 'source ./scripts/lib/android-fastlane.sh && cd apps/android && run_android_fastlane android play_store_archive'",
"android:release:auth:check": "bash -lc 'source ./scripts/lib/android-fastlane.sh && cd apps/android && run_android_fastlane android auth_check'",
@@ -1498,55 +1499,54 @@
"android:release:signing:sync:push": "bash -lc 'source ./scripts/lib/android-fastlane.sh && cd apps/android && run_android_fastlane android signing_sync_push'",
"android:release:upload": "bash scripts/android-release-upload.sh",
"android:screenshots": "bash scripts/android-screenshots.sh",
"android:test": "node scripts/run-android-gradle.mjs :app:testPlayDebugUnitTest :wear:testDebugUnitTest :wear-shared:testDebugUnitTest",
"android:test:integration": "node scripts/run-with-env.mjs OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
"android:test:third-party": "node scripts/run-android-gradle.mjs :app:testThirdPartyDebugUnitTest",
"android:test": "node --import tsx scripts/run-android-gradle.mts :app:testPlayDebugUnitTest :wear:testDebugUnitTest :wear-shared:testDebugUnitTest",
"android:test:integration": "node --import tsx scripts/run-with-env.mts OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
"android:test:third-party": "node --import tsx scripts/run-android-gradle.mts :app:testThirdPartyDebugUnitTest",
"android:version": "node --import tsx scripts/android-version.ts --json",
"android:version:check": "node --import tsx scripts/android-sync-versioning.ts --check",
"android:version:pin": "node --import tsx scripts/android-pin-version.ts",
"android:version:sync": "node --import tsx scripts/android-sync-versioning.ts --write",
"audit:seams": "node scripts/audit-seams.mjs",
"build": "node scripts/build-all.mjs",
"build:ci-artifacts": "node scripts/build-all.mjs ciArtifacts",
"build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm plugins:assets:build && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts",
"audit:seams": "node --import tsx scripts/audit-seams.mts",
"build": "node --import tsx scripts/build-all.mts",
"build:ci-artifacts": "node --import tsx scripts/build-all.mts ciArtifacts",
"build:docker": "node --import tsx scripts/tsdown-build.mts && node --import tsx scripts/check-cli-bootstrap-imports.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/build-stamp.mts && node --import tsx scripts/runtime-postbuild-stamp.mts && pnpm plugins:assets:build && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts",
"build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true",
"build:plugin-sdk:strict-smoke": "node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/run-with-env.mjs OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
"build:strict-smoke": "pnpm plugins:assets:build && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && node scripts/run-with-env.mjs OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
"canvas:a2ui:bundle": "node scripts/bundle-a2ui.mjs",
"canvas:a2ui:native:check": "node scripts/sync-native-a2ui.mjs --check",
"build:plugin-sdk:strict-smoke": "node --import tsx scripts/tsdown-build.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/run-with-env.mts OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/check-plugin-sdk-exports.mts",
"build:strict-smoke": "pnpm plugins:assets:build && node --import tsx scripts/tsdown-build.mts && node --import tsx scripts/check-cli-bootstrap-imports.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/build-stamp.mts && node --import tsx scripts/runtime-postbuild-stamp.mts && node --import tsx scripts/run-with-env.mts OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/check-plugin-sdk-exports.mts",
"canvas:a2ui:bundle": "node --import tsx scripts/bundle-a2ui.mts",
"canvas:a2ui:native:check": "node --import tsx scripts/sync-native-a2ui.mts --check",
"channels:catalog:check": "node scripts/write-official-channel-catalog.mjs --check",
"channels:catalog:gen": "node scripts/write-official-channel-catalog.mjs --write",
"changed:lanes": "node scripts/changed-lanes.mjs",
"check": "node scripts/check.mjs",
"check": "node --import tsx scripts/check.mts",
"check:architecture": "pnpm check:import-cycles && pnpm check:madge-import-cycles && pnpm check:deprecated-api-usage && pnpm check:deprecated-jsdoc && pnpm db:kysely:check && pnpm lint:kysely && pnpm check:database-first-legacy-stores",
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
"check:changed": "node scripts/check-changed.mjs",
"check:changelog-attributions": "node scripts/check-changelog-attributions.mjs",
"check:database-first-legacy-stores": "node scripts/check-database-first-legacy-stores.mjs",
"check:deprecated-api-usage": "node scripts/check-deprecated-api-usage.mjs",
"check:deprecated-jsdoc": "node scripts/check-deprecated-jsdoc.mjs",
"check:changelog-attributions": "node --import tsx scripts/check-changelog-attributions.mts",
"check:database-first-legacy-stores": "node --import tsx scripts/check-database-first-legacy-stores.mts",
"check:deprecated-api-usage": "node --import tsx scripts/check-deprecated-api-usage.mts",
"check:deprecated-jsdoc": "node --import tsx scripts/check-deprecated-jsdoc.mts",
"check:doctor-deprecation-registry": "node --import tsx scripts/check-doctor-deprecation-registry.ts",
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-mdx && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
"check:env-var-count": "node scripts/check-env-var-count.mjs",
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
"check:env-var-count": "node --import tsx scripts/check-env-var-count.mts",
"check:host-env-policy:swift": "node --import tsx scripts/generate-host-env-security-policy-swift.mts --check",
"check:import-cycles": "node --import tsx scripts/check-import-cycles.ts",
"check:max-lines-ratchet": "node scripts/check-max-lines-ratchet.mjs",
"check:max-lines-ratchet": "node --import tsx scripts/check-max-lines-ratchet.mts",
"check:madge-import-cycles": "node --import tsx scripts/check-madge-import-cycles.ts",
"check:media-download-helpers": "node scripts/check-media-download-helper-roundtrip.mjs",
"check:media-download-helpers": "node --import tsx scripts/check-media-download-helper-roundtrip.mts",
"check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs",
"check:no-runtime-action-load-config": "node scripts/check-no-runtime-action-load-config.mjs",
"check:no-runtime-action-load-config": "node --import tsx scripts/check-no-runtime-action-load-config.mts",
"check:opengrep-rule-metadata": "node security/opengrep/check-rule-metadata.mjs",
"check:protocol-coverage": "node scripts/check-protocol-event-coverage.mjs",
"check:runtime-sidecar-loaders": "node --import tsx scripts/check-runtime-sidecar-loaders.mjs",
"check:script-declarations": "node scripts/check-script-declarations.mjs",
"check:runtime-sidecar-loaders": "node --import tsx scripts/check-runtime-sidecar-loaders.mts",
"check:static-import-sccs": "pnpm check:madge-import-cycles",
"check:temp-path-guardrails": "node --import tsx scripts/check-temp-path-guardrails.ts",
"check:test-types": "pnpm tsgo:test",
"check:timed": "node scripts/check-timed.mjs",
"check:timed:all-types": "node scripts/check-timed.mjs --include-test-types",
"check:timed:architecture": "node scripts/check-timed.mjs --include-architecture",
"check:workflows": "node scripts/check-workflows.mjs",
"check:timed": "node --import tsx scripts/check-timed.mts",
"check:timed:all-types": "node --import tsx scripts/check-timed.mts --include-test-types",
"check:timed:architecture": "node --import tsx scripts/check-timed.mts --include-architecture",
"check:workflows": "node --import tsx scripts/check-workflows.mts",
"ci:full-release": "node scripts/full-release-validation-at-sha.mjs",
"ci:timings": "node scripts/ci-run-timings.mjs --latest-main",
"ci:timings:recent": "node scripts/ci-run-timings.mjs --recent 10",
@@ -1566,27 +1566,27 @@
"crabbox:stop": "node scripts/crabbox-wrapper.mjs stop",
"crabbox:warmup": "node scripts/crabbox-wrapper.mjs warmup",
"deadcode:dependencies": "pnpm deadcode:full",
"deadcode:exports": "node scripts/check-deadcode-exports.mjs",
"deadcode:exports": "node --import tsx scripts/check-deadcode-exports.mts",
"deadcode:full": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --no-config-hints --exclude duplicates && pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.all-exports.config.ts --no-progress --reporter compact --no-config-hints --exclude duplicates",
"deadcode:knip": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --files --dependencies",
"deadcode:report": "pnpm deadcode:full; pnpm deadcode:exports",
"deadcode:unused-files": "node scripts/check-deadcode-unused-files.mjs",
"deps:root-ownership": "node scripts/root-dependency-ownership-audit.mjs",
"deps:root-ownership:check": "node scripts/root-dependency-ownership-audit.mjs --check",
"deps:changes:report": "node scripts/dependency-changes-report.mjs",
"deps:patches:check": "node scripts/check-package-patches.mjs",
"deps:pins:check": "node scripts/check-dependency-pins.mjs",
"deadcode:unused-files": "node --import tsx scripts/check-deadcode-unused-files.mts",
"deps:root-ownership": "node --import tsx scripts/root-dependency-ownership-audit.mts",
"deps:root-ownership:check": "node --import tsx scripts/root-dependency-ownership-audit.mts --check",
"deps:changes:report": "node --import tsx scripts/dependency-changes-report.mts",
"deps:patches:check": "node --import tsx scripts/check-package-patches.mts",
"deps:pins:check": "node --import tsx scripts/check-dependency-pins.mts",
"deps:npm-lock:check": "node scripts/generate-npm-package-lock.mjs --all",
"deps:npm-lock:check:changed": "node scripts/generate-npm-package-lock.mjs --changed",
"deps:ownership-surface:check": "node scripts/dependency-ownership-surface-report.mjs --check",
"deps:ownership-surface:report": "node scripts/dependency-ownership-surface-report.mjs",
"deps:transitive-risk:report": "node scripts/transitive-manifest-risk-report.mjs",
"deps:vuln:gate": "node scripts/dependency-vulnerability-gate.mjs",
"db:kysely:check": "node scripts/generate-kysely-types.mjs --verify",
"db:kysely:gen": "node scripts/generate-kysely-types.mjs",
"deps:ownership-surface:check": "node --import tsx scripts/dependency-ownership-surface-report.mts --check",
"deps:ownership-surface:report": "node --import tsx scripts/dependency-ownership-surface-report.mts",
"deps:transitive-risk:report": "node --import tsx scripts/transitive-manifest-risk-report.mts",
"deps:vuln:gate": "node --import tsx scripts/dependency-vulnerability-gate.mts",
"db:kysely:check": "node --import tsx scripts/generate-kysely-types.mts --verify",
"db:kysely:gen": "node --import tsx scripts/generate-kysely-types.mts",
"dev": "node scripts/run-node.mjs",
"dev:ui:mock": "node --import tsx scripts/control-ui-mock-dev.ts",
"docs:check-i18n-glossary": "node scripts/check-docs-i18n-glossary.mjs",
"docs:check-i18n-glossary": "node --import tsx scripts/check-docs-i18n-glossary.mts",
"docs:check-links": "node scripts/docs-link-audit.mjs",
"docs:check-links:anchors": "node scripts/docs-link-audit.mjs --anchors",
"docs:check-mdx": "node scripts/check-docs-mdx.mjs docs README.md",
@@ -1597,23 +1597,23 @@
"docs:spellcheck:fix": "bash scripts/docs-spellcheck.sh --write",
"maturity:check": "node --import tsx scripts/qa/render-maturity-docs.ts --check",
"maturity:render": "node --import tsx scripts/qa/render-maturity-docs.ts",
"dup:check": "node scripts/check-duplicates.mjs",
"dup:check:coverage": "node scripts/check-duplicates.mjs --coverage",
"dup:check:json": "node scripts/check-duplicates.mjs --json",
"dup:check": "node --import tsx scripts/check-duplicates.mts",
"dup:check:coverage": "node --import tsx scripts/check-duplicates.mts --coverage",
"dup:check:json": "node --import tsx scripts/check-duplicates.mts --json",
"format": "oxfmt --write --threads=1",
"format:all": "pnpm format && pnpm format:swift",
"format:check": "oxfmt --check",
"format:diff": "oxfmt --write --threads=1 && git --no-pager diff",
"format:docs": "node scripts/format-docs.mjs",
"format:docs:check": "node scripts/format-docs.mjs --check",
"format:docs": "node --import tsx scripts/format-docs.mts",
"format:docs:check": "node --import tsx scripts/format-docs.mts --check",
"format:fix": "oxfmt --write --threads=1",
"format:swift": "./scripts/format-swift.sh",
"gateway:dev": "node scripts/run-with-env.mjs OPENCLAW_SKIP_CHANNELS=1 -- node scripts/run-node.mjs --dev gateway",
"gateway:dev:reset": "node scripts/run-with-env.mjs OPENCLAW_SKIP_CHANNELS=1 -- node scripts/run-node.mjs --dev gateway --reset",
"gateway:watch": "node scripts/gateway-watch-tmux.mjs gateway --force",
"gateway:dev": "node --import tsx scripts/run-with-env.mts OPENCLAW_SKIP_CHANNELS=1 -- node scripts/run-node.mjs --dev gateway",
"gateway:dev:reset": "node --import tsx scripts/run-with-env.mts OPENCLAW_SKIP_CHANNELS=1 -- node scripts/run-node.mjs --dev gateway --reset",
"gateway:watch": "node --import tsx scripts/gateway-watch-tmux.mts gateway --force",
"gateway:watch:raw": "node scripts/watch-node.mjs gateway --force",
"gen:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --write",
"ghsa:patch": "node scripts/ghsa-patch.mjs",
"gen:host-env-policy:swift": "node --import tsx scripts/generate-host-env-security-policy-swift.mts --write",
"ghsa:patch": "node --import tsx scripts/ghsa-patch.mts",
"ios:app-review-notes:pdf": "xcrun swift scripts/ios-app-review-notes-pdf.swift apps/ios/APP-REVIEW-NOTES.md apps/ios/build/app-review/APP-REVIEW-NOTES.pdf",
"ios:build": "bash -c 'export PATH=\"$PATH:/opt/homebrew/bin:/usr/local/bin\"; ./scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && node scripts/ios-write-swift-filelist.mjs && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-generic/platform=iOS Simulator}\" -configuration Debug build'",
"ios:filelist:gen": "node scripts/ios-write-swift-filelist.mjs",
@@ -1635,51 +1635,51 @@
"ios:version:check": "node --import tsx scripts/ios-sync-versioning.ts --check",
"ios:version:sync": "node --import tsx scripts/ios-sync-versioning.ts --write",
"leak:embedded-run": "node --import tsx --expose-gc scripts/embedded-run-abort-leak.ts",
"lint": "node scripts/run-lint.mjs",
"lint:agent:ingress-owner": "node scripts/check-ingress-agent-owner-context.mjs",
"lint": "node --import tsx scripts/run-lint.mts",
"lint:agent:ingress-owner": "node --import tsx scripts/check-ingress-agent-owner-context.mts",
"lint:all": "node scripts/run-oxlint.mjs",
"lint:apps": "pnpm lint:swift",
"lint:auth:no-pairing-store-group": "node scripts/check-no-pairing-store-group-auth.mjs",
"lint:auth:pairing-account-scope": "node scripts/check-pairing-account-scope.mjs",
"lint:core": "node scripts/run-oxlint-shards.mjs --only=core --split-core",
"lint:docker-e2e": "node scripts/check-docker-e2e-boundaries.mjs",
"lint:kysely": "node scripts/check-kysely-guardrails.mjs",
"lint:auth:no-pairing-store-group": "node --import tsx scripts/check-no-pairing-store-group-auth.mts",
"lint:auth:pairing-account-scope": "node --import tsx scripts/check-pairing-account-scope.mts",
"lint:core": "node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core",
"lint:docker-e2e": "node --import tsx scripts/check-docker-e2e-boundaries.mts",
"lint:kysely": "node --import tsx scripts/check-kysely-guardrails.mts",
"lint:docs": "pnpm dlx --config.resolution-mode=highest markdownlint-cli2 --config config/markdownlint-cli2.jsonc",
"lint:docs:fix": "pnpm dlx --config.resolution-mode=highest markdownlint-cli2 --config config/markdownlint-cli2.jsonc --fix",
"lint:extensions:no-deprecated-channel-access": "node --import tsx scripts/check-no-deprecated-channel-access.ts",
"lint:extensions:telegram-grammy-types": "node scripts/check-telegram-grammy-types-imports.mjs",
"lint:extensions:telegram-grammy-types": "node --import tsx scripts/check-telegram-grammy-types-imports.mts",
"lint:extensions": "node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json extensions",
"lint:extensions:no-guarded-wildcard-reexports": "node scripts/check-extension-wildcard-reexports.mjs",
"lint:extensions:no-plugin-sdk-internal": "node scripts/check-extension-plugin-sdk-boundary.mjs --mode=plugin-sdk-internal",
"lint:extensions:no-plugin-sdk-wildcard-reexports": "node scripts/check-plugin-sdk-wildcard-reexports.mjs",
"lint:extensions:no-relative-outside-package": "node scripts/check-extension-plugin-sdk-boundary.mjs --mode=relative-outside-package",
"lint:extensions:no-src-outside-plugin-sdk": "node scripts/check-extension-plugin-sdk-boundary.mjs --mode=src-outside-plugin-sdk",
"lint:extensions:no-guarded-wildcard-reexports": "node --import tsx scripts/check-extension-wildcard-reexports.mts",
"lint:extensions:no-plugin-sdk-internal": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=plugin-sdk-internal",
"lint:extensions:no-plugin-sdk-wildcard-reexports": "node --import tsx scripts/check-plugin-sdk-wildcard-reexports.mts",
"lint:extensions:no-relative-outside-package": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=relative-outside-package",
"lint:extensions:no-src-outside-plugin-sdk": "node --import tsx scripts/check-extension-plugin-sdk-boundary.mts --mode=src-outside-plugin-sdk",
"lint:fix": "node scripts/run-oxlint.mjs --fix && pnpm format",
"lint:plugins:no-extension-imports": "node scripts/check-plugin-extension-import-boundary.mjs",
"lint:plugins:no-extension-imports": "node --import tsx scripts/check-plugin-extension-import-boundary.mts",
"lint:plugins:no-extension-src-imports": "node --import tsx scripts/check-no-extension-src-imports.ts",
"lint:plugins:no-extension-test-core-imports": "node --import tsx scripts/check-no-extension-test-core-imports.ts",
"lint:plugins:no-monolithic-plugin-sdk-entry-imports": "node --import tsx scripts/check-no-monolithic-plugin-sdk-entry-imports.ts",
"lint:plugins:no-register-http-handler": "node scripts/run-oxlint.mjs --openclaw-focused-config --config config/oxlint/boundary-guards.json src extensions",
"lint:plugins:plugin-sdk-subpaths-exported": "node scripts/check-plugin-sdk-subpath-exports.mjs",
"lint:plugins:plugin-sdk-subpaths-exported": "node --import tsx scripts/check-plugin-sdk-subpath-exports.mts",
"lint:scripts": "pnpm lint:docker-e2e && pnpm lint:tmp:no-raw-http2-imports && node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.scripts.json scripts",
"lint:swift": "./scripts/lint-swift.sh",
"lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs",
"lint:tmp:dynamic-import-warts": "node scripts/check-dynamic-import-warts.mjs",
"lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs",
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
"lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs",
"lint:tmp:session-accessor-boundary": "node scripts/check-session-accessor-boundary.mjs",
"lint:tmp:session-accessor-boundary:gen": "node scripts/check-session-accessor-boundary.mjs --update-debt-baseline",
"lint:tmp:session-transcript-reader-boundary": "node scripts/check-session-transcript-reader-boundary.mjs",
"lint:tmp:sqlite-transaction-boundary": "node scripts/check-sqlite-transaction-boundary.mjs",
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
"lint:tmp:channel-agnostic-boundaries": "node --import tsx scripts/check-channel-agnostic-boundaries.mts",
"lint:tmp:dynamic-import-warts": "node --import tsx scripts/check-dynamic-import-warts.mts",
"lint:tmp:no-random-messaging": "node --import tsx scripts/check-no-random-messaging-tmp.mts",
"lint:tmp:no-raw-channel-fetch": "node --import tsx scripts/check-no-raw-channel-fetch.mts",
"lint:tmp:no-raw-http2-imports": "node --import tsx scripts/check-no-raw-http2-imports.mts",
"lint:tmp:session-accessor-boundary": "node --import tsx scripts/check-session-accessor-boundary.mts",
"lint:tmp:session-accessor-boundary:gen": "node --import tsx scripts/check-session-accessor-boundary.mts --update-debt-baseline",
"lint:tmp:session-transcript-reader-boundary": "node --import tsx scripts/check-session-transcript-reader-boundary.mts",
"lint:tmp:sqlite-transaction-boundary": "node --import tsx scripts/check-sqlite-transaction-boundary.mts",
"lint:tmp:tsgo-core-boundary": "node --import tsx scripts/check-tsgo-core-boundary.mts",
"lint:ui:i18n": "pnpm ui:i18n:verify",
"lint:ui:lit": "lit-analyzer \"ui/src/**/*.ts\" --quiet",
"lint:ui:no-raw-window-open": "node scripts/run-oxlint.mjs --openclaw-focused-config --config config/oxlint/boundary-guards.json ui/src",
"lint:ui:styles": "stylelint --config config/stylelint.config.mjs \"ui/src/**/*.css\" \"ui/src/**/*.ts\"",
"lint:web-fetch-provider-boundaries": "node scripts/check-web-fetch-provider-boundaries.mjs",
"lint:web-search-provider-boundaries": "node scripts/check-web-search-provider-boundaries.mjs",
"lint:webhook:no-low-level-body-read": "node scripts/check-webhook-auth-body-order.mjs",
"lint:web-fetch-provider-boundaries": "node --import tsx scripts/check-web-fetch-provider-boundaries.mts",
"lint:web-search-provider-boundaries": "node --import tsx scripts/check-web-search-provider-boundaries.mts",
"lint:webhook:no-low-level-body-read": "node --import tsx scripts/check-webhook-auth-body-order.mts",
"mac:open": "open dist/OpenClaw.app",
"mac:package": "bash scripts/package-mac-app.sh",
"mac:restart": "bash scripts/restart-mac.sh",
@@ -1689,25 +1689,25 @@
"openclaw": "node scripts/run-node.mjs",
"openclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
"perf:web-fetch": "node --import tsx scripts/bench-web-fetch.ts",
"perf:kova:summary": "node scripts/kova-ci-summary.mjs",
"perf:source:summary": "node scripts/openclaw-performance-source-summary.mjs",
"perf:kova:summary": "node --import tsx scripts/kova-ci-summary.mts",
"perf:source:summary": "node --import tsx scripts/openclaw-performance-source-summary.mts",
"plugin-sdk:api:check": "node --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --check",
"plugin-sdk:api:gen": "node --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --write",
"plugin-sdk:check-exports": "node scripts/sync-plugin-sdk-exports.mjs --check",
"plugin-sdk:surface": "node --max-old-space-size=8192 scripts/plugin-sdk-surface-report.mjs",
"plugin-sdk:surface:check": "node --max-old-space-size=8192 scripts/plugin-sdk-surface-report.mjs --check",
"plugin-sdk:sync-exports": "node scripts/sync-plugin-sdk-exports.mjs",
"plugin-sdk:check-exports": "node --import tsx scripts/sync-plugin-sdk-exports.mts --check",
"plugin-sdk:surface": "node --max-old-space-size=8192 --import tsx scripts/plugin-sdk-surface-report.mts",
"plugin-sdk:surface:check": "node --max-old-space-size=8192 --import tsx scripts/plugin-sdk-surface-report.mts --check",
"plugin-sdk:sync-exports": "node --import tsx scripts/sync-plugin-sdk-exports.mts",
"plugin-sdk:usage": "node --max-old-space-size=8192 --import tsx scripts/analyze-plugin-sdk-usage.ts",
"policy:config-coverage": "node --import tsx scripts/check-policy-config-coverage.ts",
"plugins:boundary-report": "node --import tsx scripts/plugin-boundary-report.ts",
"plugins:boundary-report:ci": "node --import tsx scripts/plugin-boundary-report.ts --summary --fail-on-cross-owner --fail-on-unclassified-unused-reserved --fail-on-eligible-compat",
"plugins:boundary-report:json": "node --import tsx scripts/plugin-boundary-report.ts --json",
"plugins:boundary-report:summary": "node --import tsx scripts/plugin-boundary-report.ts --summary",
"plugins:assets:build": "node scripts/bundled-plugin-assets.mjs --phase build",
"plugins:assets:check": "node scripts/bundled-plugin-assets.mjs --phase build --check",
"plugins:assets:copy": "node scripts/bundled-plugin-assets.mjs --phase copy",
"plugins:inventory:check": "node scripts/generate-plugin-inventory-doc.mjs --check",
"plugins:inventory:gen": "node scripts/generate-plugin-inventory-doc.mjs --write",
"plugins:assets:build": "node --import tsx scripts/bundled-plugin-assets.mts --phase build",
"plugins:assets:check": "node --import tsx scripts/bundled-plugin-assets.mts --phase build --check",
"plugins:assets:copy": "node --import tsx scripts/bundled-plugin-assets.mts --phase copy",
"plugins:inventory:check": "node --import tsx scripts/generate-plugin-inventory-doc.mts --check",
"plugins:inventory:gen": "node --import tsx scripts/generate-plugin-inventory-doc.mts --write",
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
"plugins:sync:check": "node --import tsx scripts/sync-plugin-versions.ts --check",
"postinstall": "node scripts/postinstall-bundled-plugins.mjs",
@@ -1720,16 +1720,16 @@
"prompt:snapshots:check": "node --import tsx scripts/generate-prompt-snapshots.ts --check",
"prompt:snapshots:gen": "node --import tsx scripts/generate-prompt-snapshots.ts --write",
"prompt:snapshots:sync-codex-model": "node --import tsx scripts/sync-codex-model-prompt-fixture.ts",
"protocol:check": "pnpm protocol-registry:check && pnpm protocol:gen && pnpm protocol:check:swift && pnpm protocol:gen:kotlin && node scripts/check-protocol-since.mjs && git diff --exit-code -- dist/protocol.schema.json apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt apps/android/app/src/main/java/ai/openclaw/app/protocol/OpenClawProtocolConstants.kt",
"protocol:check": "pnpm protocol-registry:check && pnpm protocol:gen && pnpm protocol:check:swift && pnpm protocol:gen:kotlin && node --import tsx scripts/check-protocol-since.mts && git diff --exit-code -- dist/protocol.schema.json apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt apps/android/app/src/main/java/ai/openclaw/app/protocol/OpenClawProtocolConstants.kt",
"protocol:check:kotlin": "pnpm protocol:gen:kotlin && git diff --exit-code -- apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt apps/android/app/src/main/java/ai/openclaw/app/protocol/OpenClawProtocolConstants.kt",
"protocol:check:swift": "node --import tsx scripts/protocol-gen-swift.ts --check",
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
"protocol:gen:kotlin": "node --import tsx scripts/protocol-gen-kotlin.ts",
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
"protocol-registry:check": "node --import tsx scripts/check-protocol-registry.mjs",
"protocol-registry:check": "node --import tsx scripts/check-protocol-registry.mts",
"proxy:coverage": "node scripts/run-node.mjs proxy coverage",
"proxy:gateway": "node scripts/run-node.mjs proxy run -- node scripts/run-node.mjs gateway",
"proxy:install-ca": "node --import tsx scripts/proxy-install-ca.mjs",
"proxy:install-ca": "node --import tsx scripts/proxy-install-ca.mts",
"proxy:run": "node scripts/run-node.mjs proxy run",
"proxy:start": "node scripts/run-node.mjs proxy start",
"qa:e2e": "node --import tsx scripts/qa-e2e.ts",
@@ -1744,10 +1744,10 @@
"qa:otel:smoke": "node --import tsx test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts",
"qa:code-mode-models": "node --import tsx scripts/code-mode-model-matrix.ts",
"qa:prometheus:smoke": "node scripts/run-node.mjs qa suite --provider-mode mock-openai --scenario docker-prometheus-smoke --concurrency 1 --fast",
"release-metadata:check": "node scripts/check-release-metadata-only.mjs",
"release:beta": "node scripts/release-candidate-checklist.mjs",
"release-metadata:check": "node --import tsx scripts/check-release-metadata-only.mts",
"release:beta": "node --import tsx scripts/release-candidate-checklist.mts",
"release:beta-smoke": "node --import tsx scripts/release-beta-smoke.ts",
"release:candidate": "node scripts/release-candidate-checklist.mjs",
"release:candidate": "node --import tsx scripts/release-candidate-checklist.mts",
"release:check": "pnpm release:generated:check && node --import tsx scripts/release-check.ts",
"release:fast-pretag-check": "bash scripts/release-fast-pretag-check.sh",
"release:generated:check": "node scripts/release-preflight.mjs --check",
@@ -1767,17 +1767,17 @@
"sqlite:sessions-schema:check": "node --import tsx scripts/generate-sqlite-session-schema-baseline.ts --check",
"sqlite:sessions-schema:gen": "node --import tsx scripts/generate-sqlite-session-schema-baseline.ts --write",
"start": "node openclaw.mjs",
"test": "node scripts/test-projects.mjs",
"test": "node --import tsx scripts/test-projects.mts",
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
"test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts && node scripts/run-vitest.mjs run packages/gateway-protocol/src/connect-error-details.test.ts",
"test:build:singleton": "node scripts/test-built-plugin-singleton.mjs",
"test:build:status-message-runtime": "node scripts/test-built-status-message-runtime.mjs",
"test:build:singleton": "node --import tsx scripts/test-built-plugin-singleton.mts",
"test:build:status-message-runtime": "node --import tsx scripts/test-built-status-message-runtime.mts",
"test:bundled": "node scripts/run-vitest.mjs run --config test/vitest/vitest.bundled.config.ts",
"test:changed": "node scripts/test-projects.mjs --changed origin/main",
"test:changed:max": "node scripts/test-projects-max.mjs --changed origin/main",
"test:changed": "node --import tsx scripts/test-projects.mts --changed origin/main",
"test:changed:max": "node --import tsx scripts/test-projects-max.mts --changed origin/main",
"test:channels": "node scripts/run-vitest.mjs run --config test/vitest/vitest.channels.config.ts",
"test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins",
"test:contracts:channels": "node scripts/test-projects.mjs --maxWorkers=1 test/vitest/vitest.contracts-channel-surface.config.ts test/vitest/vitest.contracts-channel-config.config.ts test/vitest/vitest.contracts-channel-registry.config.ts test/vitest/vitest.contracts-channel-session.config.ts",
"test:contracts:channels": "node --import tsx scripts/test-projects.mts --maxWorkers=1 test/vitest/vitest.contracts-channel-surface.config.ts test/vitest/vitest.contracts-channel-config.config.ts test/vitest/vitest.contracts-channel-registry.config.ts test/vitest/vitest.contracts-channel-session.config.ts",
"test:contracts:plugins": "node scripts/run-vitest.mjs run --config test/vitest/vitest.contracts-plugin.config.ts --maxWorkers=1",
"test:coverage": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage",
"test:coverage:changed": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage --changed origin/main",
@@ -1829,8 +1829,8 @@
"test:docker:live-models": "bash scripts/test-live-models-docker.sh",
"test:docker:live-models:claude": "OPENCLAW_LIVE_PROVIDERS=claude-cli OPENCLAW_LIVE_MODELS=claude-cli/claude-sonnet-4-6 bash scripts/test-live-models-docker.sh",
"test:docker:live-models:gemini": "OPENCLAW_LIVE_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-models-docker.sh",
"test:docker:live:all": "node scripts/run-with-env.mjs OPENCLAW_DOCKER_ALL_LIVE_MODE=only -- node scripts/test-docker-all.mjs",
"test:docker:local:all": "node scripts/run-with-env.mjs OPENCLAW_DOCKER_ALL_LIVE_MODE=skip -- node scripts/test-docker-all.mjs",
"test:docker:live:all": "node --import tsx scripts/run-with-env.mts OPENCLAW_DOCKER_ALL_LIVE_MODE=only -- node scripts/test-docker-all.mjs",
"test:docker:local:all": "node --import tsx scripts/run-with-env.mts OPENCLAW_DOCKER_ALL_LIVE_MODE=skip -- node scripts/test-docker-all.mjs",
"test:docker:cli-installer-distribution": "bash scripts/e2e/cli-installer-distribution-docker.sh",
"test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh",
"test:docker:mcp-code-mode-gateway": "bash scripts/e2e/mcp-code-mode-gateway-docker.sh",
@@ -1857,12 +1857,12 @@
"test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
"test:docker:published-upgrade-survivor": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest} OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT=${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s} bash scripts/e2e/upgrade-survivor-docker.sh",
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
"test:docker:rerun": "node scripts/docker-e2e-rerun.mjs",
"test:docker:rerun": "node --import tsx scripts/docker-e2e-rerun.mts",
"test:docker:root-managed-vps-upgrade": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.5.7} OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT=${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s} bash scripts/e2e/upgrade-survivor-docker.sh",
"test:docker:selected-plugins": "bash scripts/e2e/docker-selected-plugins.sh",
"test:docker:session-runtime-context": "bash scripts/e2e/session-runtime-context-docker.sh",
"test:docker:skill-install": "bash scripts/e2e/skill-install-docker.sh",
"test:docker:timings": "node scripts/docker-e2e-timings.mjs",
"test:docker:timings": "node --import tsx scripts/docker-e2e-timings.mts",
"test:docker:update-channel-switch": "bash scripts/e2e/update-channel-switch-docker.sh",
"test:docker:update-corrupt-plugin": "bash scripts/e2e/update-corrupt-plugin-docker.sh",
"test:docker:update-migration": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.4.23} OPENCLAW_UPGRADE_SURVIVOR_SCENARIO=${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-plugin-deps-cleanup} bash scripts/e2e/upgrade-survivor-docker.sh",
@@ -1874,40 +1874,40 @@
"test:type-suppression-inventory:report": "node --import tsx scripts/type-suppression-inventory.ts",
"test:e2e": "pnpm test:e2e:gateway && pnpm test:e2e:agent-plugin-gateway && pnpm test:ui:e2e",
"test:e2e:agent-plugin-gateway": "node --import tsx scripts/agent-plugin-gateway-e2e.ts",
"test:e2e:browser-copilot": "node scripts/run-with-env.mjs PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers -- node scripts/ensure-playwright-chromium.mjs --require-playwright-chromium && node scripts/run-with-env.mjs PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers OPENCLAW_BROWSER_COPILOT_E2E=1 OPENCLAW_E2E_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/browser/chrome-extension/page-share.e2e.test.ts extensions/browser/chrome-extension/sidepanel.e2e.test.ts",
"test:e2e:browser-copilot": "node --import tsx scripts/run-with-env.mts PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers -- node --import tsx scripts/ensure-playwright-chromium.mts --require-playwright-chromium && node --import tsx scripts/run-with-env.mts PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers OPENCLAW_BROWSER_COPILOT_E2E=1 OPENCLAW_E2E_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/browser/chrome-extension/page-share.e2e.test.ts extensions/browser/chrome-extension/sidepanel.e2e.test.ts",
"test:e2e:gateway": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts",
"test:e2e:openshell": "node scripts/run-with-env.mjs OPENCLAW_E2E_OPENSHELL=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts",
"test:e2e:openshell": "node --import tsx scripts/run-with-env.mts OPENCLAW_E2E_OPENSHELL=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts",
"test:e2e:status-corrupt-plugin-deps": "bash scripts/e2e/status-corrupt-plugin-deps.sh",
"test:extension": "node scripts/test-extension.mjs",
"test:extensions": "node scripts/test-projects.mjs extensions",
"test:extensions:batch": "node scripts/test-extension-batch.mjs",
"test:extensions:memory": "node scripts/profile-extension-memory.mjs",
"test:extensions:package-boundary": "node scripts/check-extension-package-tsc-boundary.mjs",
"test:extensions:package-boundary:canary": "node scripts/check-extension-package-tsc-boundary.mjs --mode=canary",
"test:extensions:package-boundary:compile": "node scripts/check-extension-package-tsc-boundary.mjs --mode=compile",
"test:extension": "node --import tsx scripts/test-extension.mts",
"test:extensions": "node --import tsx scripts/test-projects.mts extensions",
"test:extensions:batch": "node --import tsx scripts/test-extension-batch.mts",
"test:extensions:memory": "node --import tsx scripts/profile-extension-memory.mts",
"test:extensions:package-boundary": "node --import tsx scripts/check-extension-package-tsc-boundary.mts",
"test:extensions:package-boundary:canary": "node --import tsx scripts/check-extension-package-tsc-boundary.mts --mode=canary",
"test:extensions:package-boundary:compile": "node --import tsx scripts/check-extension-package-tsc-boundary.mts --mode=compile",
"test:fast": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts",
"test:force": "node --import tsx scripts/test-force.ts",
"test:gateway": "node scripts/run-with-env.mjs OPENCLAW_GATEWAY_PROJECT_SHARDS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts",
"test:gateway:cpu-scenarios": "node scripts/check-gateway-cpu-scenarios.mjs",
"test:gateway": "node --import tsx scripts/run-with-env.mts OPENCLAW_GATEWAY_PROJECT_SHARDS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts",
"test:gateway:cpu-scenarios": "node --import tsx scripts/check-gateway-cpu-scenarios.mts",
"test:gateway:concurrency": "node scripts/bench-gateway-concurrency.ts",
"test:gateway:memory-fd-repro": "node scripts/check-memory-fd-repro.mjs",
"test:gateway:watch-regression": "node scripts/check-gateway-watch-regression.mjs",
"test:gateway:memory-fd-repro": "node --import tsx scripts/check-memory-fd-repro.mts",
"test:gateway:watch-regression": "node --import tsx scripts/check-gateway-watch-regression.mts",
"test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
"test:install:e2e:anthropic": "OPENCLAW_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
"test:install:e2e:openai": "OPENCLAW_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
"test:install:smoke": "bash scripts/test-install-sh-docker.sh",
"test:live": "node scripts/test-live.mjs",
"test:live:cache": "node scripts/run-with-env.mjs OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_CACHE_TEST=1 -- node --import tsx scripts/check-live-cache.ts",
"test:live:codex-harness": "node scripts/test-live.mjs --codex-harness -- src/gateway/gateway-codex-harness.live.test.ts",
"test:live:system-agent-rescue-channel": "node scripts/run-with-env.mjs OPENCLAW_LIVE_SYSTEM_AGENT_RESCUE_CHANNEL=1 -- node scripts/test-live.mjs -- src/system-agent/rescue-channel.live.test.ts",
"test:live:gateway-profiles": "node scripts/test-live.mjs -- src/gateway/gateway-models.profiles.live.test.ts",
"test:live": "node --import tsx scripts/test-live.mts",
"test:live:cache": "node --import tsx scripts/run-with-env.mts OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_CACHE_TEST=1 -- node --import tsx scripts/check-live-cache.ts",
"test:live:codex-harness": "node --import tsx scripts/test-live.mts --codex-harness -- src/gateway/gateway-codex-harness.live.test.ts",
"test:live:system-agent-rescue-channel": "node --import tsx scripts/run-with-env.mts OPENCLAW_LIVE_SYSTEM_AGENT_RESCUE_CHANNEL=1 -- node --import tsx scripts/test-live.mts -- src/system-agent/rescue-channel.live.test.ts",
"test:live:gateway-profiles": "node --import tsx scripts/test-live.mts -- src/gateway/gateway-models.profiles.live.test.ts",
"test:live:media": "node --import tsx test/e2e/qa-lab/media/hosted-media-provider-live.ts",
"test:live:media:image": "node --import tsx test/e2e/qa-lab/media/hosted-media-provider-live.ts image",
"test:live:media:music": "node --import tsx test/e2e/qa-lab/media/hosted-media-provider-live.ts music",
"test:live:media:video": "node --import tsx test/e2e/qa-lab/media/hosted-media-provider-live.ts video",
"test:live:models-profiles": "node scripts/test-live.mjs -- src/agents/models.profiles.live.test.ts",
"test:macos:ci": "node scripts/test-projects.mjs src/daemon/launchd.test.ts src/daemon/runtime-paths.test.ts src/daemon/runtime-binary.test.ts src/gateway/worker-environments/workspace-rsync-path.test.ts src/infra/brew.test.ts src/infra/stable-node-path.test.ts test/scripts/vitest-process-group.test.ts test/scripts/package-mac-app.test.ts test/scripts/package-mac-dist.test.ts test/scripts/create-dmg.test.ts test/scripts/codesign-mac-app.test.ts test/scripts/notarize-mac-artifact.test.ts",
"test:max": "node scripts/test-projects-max.mjs",
"test:live:models-profiles": "node --import tsx scripts/test-live.mts -- src/agents/models.profiles.live.test.ts",
"test:macos:ci": "node --import tsx scripts/test-projects.mts src/daemon/launchd.test.ts src/daemon/runtime-paths.test.ts src/daemon/runtime-binary.test.ts src/gateway/worker-environments/workspace-rsync-path.test.ts src/infra/brew.test.ts src/infra/stable-node-path.test.ts test/scripts/vitest-process-group.test.ts test/scripts/package-mac-app.test.ts test/scripts/package-mac-dist.test.ts test/scripts/create-dmg.test.ts test/scripts/codesign-mac-app.test.ts test/scripts/notarize-mac-artifact.test.ts",
"test:max": "node --import tsx scripts/test-projects-max.mts",
"test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh",
"test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh",
"test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh",
@@ -1915,43 +1915,43 @@
"test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh",
"test:agents:concurrency": "node --import tsx scripts/bench-agent-concurrency.ts",
"test:tasks:sqlite-churn": "node --import tsx scripts/bench-task-registry-sqlite.ts",
"test:perf:budget": "node scripts/test-perf-budget.mjs",
"test:perf:changed:bench": "node scripts/bench-test-changed.mjs",
"test:perf:groups": "node scripts/test-group-report.mjs",
"test:perf:groups:compare": "node scripts/test-group-report.mjs --compare",
"test:perf:hotspots": "node scripts/test-hotspots.mjs",
"test:perf:imports": "node scripts/test-projects-imports.mjs",
"test:perf:imports:changed": "node scripts/test-projects-imports.mjs --changed origin/main",
"test:perf:profile:main": "node scripts/run-vitest-profile.mjs main",
"test:perf:profile:runner": "node scripts/run-vitest-profile.mjs runner",
"test:perf:budget": "node --import tsx scripts/test-perf-budget.mts",
"test:perf:changed:bench": "node --import tsx scripts/bench-test-changed.mts",
"test:perf:groups": "node --import tsx scripts/test-group-report.mts",
"test:perf:groups:compare": "node --import tsx scripts/test-group-report.mts --compare",
"test:perf:hotspots": "node --import tsx scripts/test-hotspots.mts",
"test:perf:imports": "node --import tsx scripts/test-projects-imports.mts",
"test:perf:imports:changed": "node --import tsx scripts/test-projects-imports.mts --changed origin/main",
"test:perf:profile:main": "node --import tsx scripts/run-vitest-profile.mts main",
"test:perf:profile:runner": "node --import tsx scripts/run-vitest-profile.mts runner",
"test:sqlite:perf": "node --import tsx scripts/bench-sqlite-state.ts --profile default --output .artifacts/sqlite-perf/default.json",
"test:sqlite:perf:large": "node --import tsx scripts/bench-sqlite-state.ts --profile large --output .artifacts/sqlite-perf/large.json",
"test:sqlite:perf:smoke": "node --import tsx scripts/bench-sqlite-state.ts --profile smoke --output .artifacts/sqlite-perf/smoke.json",
"test:plugins:gateway-gauntlet": "node scripts/check-plugin-gateway-gauntlet.mjs",
"test:plugins:gateway-gauntlet": "node --import tsx scripts/check-plugin-gateway-gauntlet.mts",
"test:plugins:init-provider-scaffold": "node --import tsx scripts/validate-plugin-init-provider-scaffold.ts",
"test:plugins:kitchen-sink-live": "bash -lc 'if [ -x \"$HOME/.local/bin/openclaw-testbox-env\" ]; then exec \"$HOME/.local/bin/openclaw-testbox-env\" pnpm openclaw qa suite --provider-mode live-frontier --scenario kitchen-sink-live-openai; fi; exec pnpm openclaw qa suite --provider-mode live-frontier --scenario kitchen-sink-live-openai'",
"test:plugins:kitchen-sink-rpc": "node --import tsx scripts/e2e/kitchen-sink-rpc-walk.mjs",
"test:sectriage": "node scripts/run-with-env.mjs OPENCLAW_GATEWAY_PROJECT_SHARDS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts && node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --exclude src/process/exec.test.ts",
"test:serial": "node scripts/test-projects-serial.mjs",
"test:stability:gateway": "node scripts/run-with-env.mjs OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/gateway-stability.test.ts && node scripts/run-with-env.mjs OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.logging.config.ts src/logging/diagnostic-stability-bundle.test.ts && node scripts/run-with-env.mjs OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.infra.config.ts src/infra/fatal-error-hooks.test.ts",
"test:cli-response:contract": "node scripts/build-all.mjs cliStartup && node scripts/test-cli-startup-bench-budget.mjs --preset response --runs 1 --warmup 0 --timeout-ms 10000 --skip-baseline",
"test:plugins:kitchen-sink-rpc": "node --import tsx scripts/e2e/kitchen-sink-rpc-walk.mts",
"test:sectriage": "node --import tsx scripts/run-with-env.mts OPENCLAW_GATEWAY_PROJECT_SHARDS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts && node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --exclude src/process/exec.test.ts",
"test:serial": "node --import tsx scripts/test-projects-serial.mts",
"test:stability:gateway": "node --import tsx scripts/run-with-env.mts OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/gateway-stability.test.ts && node --import tsx scripts/run-with-env.mts OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.logging.config.ts src/logging/diagnostic-stability-bundle.test.ts && node --import tsx scripts/run-with-env.mts OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.infra.config.ts src/infra/fatal-error-hooks.test.ts",
"test:cli-response:contract": "node --import tsx scripts/build-all.mts cliStartup && node --import tsx scripts/test-cli-startup-bench-budget.mts --preset response --runs 1 --warmup 0 --timeout-ms 10000 --skip-baseline",
"test:startup:bench": "node --import tsx scripts/bench-cli-startup.ts",
"test:startup:bench:check": "node scripts/test-cli-startup-bench-budget.mjs",
"test:startup:bench:check": "node --import tsx scripts/test-cli-startup-bench-budget.mts",
"test:startup:bench:save": "node --import tsx scripts/bench-cli-startup.ts --preset all --runs 5 --warmup 1 --output .artifacts/cli-startup-bench-all.json",
"test:startup:bench:smoke": "node scripts/ensure-cli-startup-build.mjs && node --import tsx scripts/bench-cli-startup.ts --preset real --case gatewayStatusJson --runs 1 --warmup 0 --output .artifacts/cli-startup-bench-smoke.json",
"test:startup:bench:update": "node scripts/test-update-cli-startup-bench.mjs",
"test:startup:bench:smoke": "node --import tsx scripts/ensure-cli-startup-build.mts && node --import tsx scripts/bench-cli-startup.ts --preset real --case gatewayStatusJson --runs 1 --warmup 0 --output .artifacts/cli-startup-bench-smoke.json",
"test:startup:bench:update": "node --import tsx scripts/test-update-cli-startup-bench.mts",
"test:startup:gateway": "node --import tsx scripts/bench-gateway-startup.ts",
"test:restart:gateway": "node --import tsx scripts/bench-gateway-restart.ts",
"test:startup:memory": "node scripts/ensure-cli-startup-build.mjs && node scripts/check-cli-startup-memory.mjs",
"test:ui": "pnpm lint:ui:no-raw-window-open && node scripts/ensure-playwright-chromium.mjs && pnpm --dir ui test",
"test:ui:e2e": "node scripts/ensure-playwright-chromium.mjs && node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner",
"test:startup:memory": "node --import tsx scripts/ensure-cli-startup-build.mts && node scripts/check-cli-startup-memory.mjs",
"test:ui": "pnpm lint:ui:no-raw-window-open && node --import tsx scripts/ensure-playwright-chromium.mts && pnpm --dir ui test",
"test:ui:e2e": "node --import tsx scripts/ensure-playwright-chromium.mts && node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner",
"test:unit": "pnpm test:unit:fast && node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts",
"test:unit:fast": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit-fast.config.ts",
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
"test:watch": "node scripts/test-projects.mjs --watch",
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"test:windows:schtasks:integration": "node scripts/run-with-env.mjs CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts",
"test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts",
"test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts",
"test:watch": "node --import tsx scripts/test-projects.mts --watch",
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts",
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
"ts-topology": "node --import tsx scripts/ts-topology.ts",
@@ -1964,7 +1964,7 @@
"tsgo:extensions:all": "node scripts/run-tsgo.mjs -b tsconfig.extensions.projects.json",
"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:ui && pnpm tsgo:extensions",
"tsgo:profile": "node scripts/profile-tsgo.mjs",
"tsgo:profile": "node --import tsx scripts/profile-tsgo.mts",
"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 && pnpm tsgo:test:root",
"tsgo:test:extensions": "pnpm tsgo:extensions:test",
@@ -1974,12 +1974,12 @@
"tsgo:test:ui": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.ui.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-ui.tsbuildinfo",
"tsgo:ui": "node scripts/run-tsgo.mjs -p tsconfig.ui.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/ui.tsbuildinfo",
"tui": "node scripts/run-node.mjs tui",
"tui:dev": "node scripts/run-with-env.mjs OPENCLAW_PROFILE=dev -- node scripts/run-node.mjs --dev tui",
"tui:dev": "node --import tsx scripts/run-with-env.mts OPENCLAW_PROFILE=dev -- node scripts/run-node.mjs --dev tui",
"tui:pty:test:watch": "node --import tsx scripts/dev/tui-pty-test-watch.ts",
"tui:pty:test:watch:all": "node --import tsx scripts/dev/tui-pty-test-watch.ts --mode all",
"tui:pty:test:watch:fake": "node --import tsx scripts/dev/tui-pty-test-watch.ts --mode fake",
"tui:pty:test:watch:local": "node --import tsx scripts/dev/tui-pty-test-watch.ts --mode local",
"ui:build": "node scripts/ui.js build && node scripts/check-control-ui-precompressed-assets.mjs && node scripts/check-control-ui-performance.mjs",
"ui:build": "node scripts/ui.js build && node --import tsx scripts/check-control-ui-precompressed-assets.mts && node --import tsx scripts/check-control-ui-performance.mts",
"ui:dev": "node scripts/ui.js dev",
"ui:i18n:baseline": "node --import tsx scripts/control-ui-i18n-verify.ts baseline",
"ui:i18n:check": "node --import tsx scripts/control-ui-i18n.ts check",
@@ -1993,7 +1993,7 @@
"android:i18n:check": "node --import tsx scripts/android-app-i18n.ts check",
"apple:i18n:check": "node --import tsx scripts/apple-app-i18n.ts check",
"ui:install": "node scripts/ui.js install",
"verify": "node scripts/verify.mjs"
"verify": "node --import tsx scripts/verify.mts"
},
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
@@ -2075,6 +2075,7 @@
"@types/express": "5.0.6",
"@types/hosted-git-info": "3.0.5",
"@types/markdown-it": "14.1.2",
"@types/mdast": "4.0.4",
"@types/ms": "2.1.0",
"@types/node": "26.1.2",
"@types/semver": "7.7.1",
+1 -1
View File
@@ -2,7 +2,7 @@ import { spawn, type SpawnOptionsWithoutStdio } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { resolveNpmRunner } from "../../../scripts/npm-runner.mjs";
import { resolveNpmRunner } from "../../../scripts/npm-runner.mts";
import { createNodeEvalArgs } from "../../../src/test-utils/node-process.js";
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
+2 -2
View File
@@ -5,8 +5,8 @@ import fs from "node:fs/promises";
import { createServer, type Server } from "node:http";
import os from "node:os";
import path from "node:path";
import { resolveNpmRunner } from "../../../scripts/npm-runner.mjs";
import { createPnpmRunnerSpawnSpec } from "../../../scripts/pnpm-runner.mjs";
import { resolveNpmRunner } from "../../../scripts/npm-runner.mts";
import { createPnpmRunnerSpawnSpec } from "../../../scripts/pnpm-runner.mts";
import { type FileLockOptions, withFileLock } from "../../../src/infra/file-lock.js";
import { getWindowsSystem32ExePath } from "../../../src/infra/windows-install-roots.js";
import { createNodeEvalArgs } from "../../../src/test-utils/node-process.js";
+3
View File
@@ -278,6 +278,9 @@ importers:
'@types/markdown-it':
specifier: 14.1.2
version: 14.1.2
'@types/mdast':
specifier: 4.0.4
version: 4.0.4
'@types/ms':
specifier: 2.1.0
version: 2.1.0
+2 -2
View File
@@ -32,8 +32,8 @@ scenario:
- src/docker-healthcheck.ts
- src/gateway/server-http.ts
- scripts/e2e/compose-setup.sh
- scripts/lib/docker-e2e-scenarios.mjs
- scripts/package-openclaw-for-docker.mjs
- scripts/lib/docker-e2e-scenarios.mts
- scripts/package-openclaw-for-docker.mts
- test/e2e/qa-lab/runtime/docker-artifact-proof.ts
execution:
kind: script
@@ -23,7 +23,7 @@ scenario:
- docs/help/testing.md
codeRefs:
- scripts/e2e/gateway-network-docker.sh
- scripts/e2e/lib/gateway-network/client.mjs
- scripts/e2e/lib/gateway-network/client.mts
execution:
kind: script
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
@@ -23,9 +23,9 @@ scenario:
- docs/help/testing.md
- docs/concepts/qa-e2e-automation.md
codeRefs:
- scripts/package-openclaw-for-docker.mjs
- scripts/package-openclaw-for-docker.mts
- scripts/e2e/docker-package-install.sh
- scripts/lib/docker-e2e-scenarios.mjs
- scripts/lib/docker-e2e-scenarios.mts
- test/e2e/qa-lab/runtime/docker-artifact-proof.ts
execution:
kind: script
+2 -2
View File
@@ -13,7 +13,7 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
## Local Heavy-Check Lock
- Respect the local heavy-check lock behavior in `scripts/lib/local-heavy-check-runtime.mjs`.
- Respect the local heavy-check lock behavior in `scripts/lib/local-heavy-check-runtime.mts`.
- Do not bypass that lock for real heavy commands just to make a local loop look faster.
- Metadata-only or explicitly narrow commands may skip the lock when the existing helper logic says that is safe.
- If you change the lock heuristics, add or update the narrow tests under `test/scripts/`.
@@ -21,7 +21,7 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
## PR Prepare Gates
- `scripts/pr` serializes review, prepare, and merge operations per PR across linked worktrees; `scripts/pr gc` skips active or indeterminate locks. Its subcommand classification table is the canonical wrapper trust boundary: a mismatched local wrapper may run only a classified `advisory` subcommand with `--dev-wrapper` or `OPENCLAW_PR_DEV_WRAPPER=1`; classified `landing` subcommands always require canonical/origin-main wrapper code. A successful command return is the trusted synchronous-completion contract: every PR-state-mutating child must be joined before returning, and such work must never daemonize or explicitly escape both the operation group and lock-notification FD. A failed command auto-releases only while its explicit pre-side-effect validation marker remains active; failures after mutation/tool launch, interruptions, and controller loss stay locked because detached children cannot be disproved. After verifying no child tools remain, use the reported exact-OID `scripts/pr lock-recover` command. Never bypass or delete these refs manually.
- `scripts/pr prepare-gates` holds the heavy-check lock for its whole local gate block (`scripts/pr-gates-lock.mjs`), so concurrent gate runs across `.worktrees` queue as units instead of dying on child lock timeouts or vitest no-output watchdog kills.
- `scripts/pr prepare-gates` holds the heavy-check lock for its whole local gate block (`scripts/pr-gates-lock.mts`), so concurrent gate runs across `.worktrees` queue as units instead of dying on child lock timeouts or vitest no-output watchdog kills.
- `OPENCLAW_PR_GATES_REMOTE=testbox` runs the full-suite `pnpm test` gate on a Blacksmith Testbox through `scripts/crabbox-wrapper.mjs` (same delegation as `check:changed`); `pnpm build`/`pnpm check` stay local. The `tbx_` lease id and Actions run URL land in `.local/gates.env` (`REMOTE_GATES_*`) and `.local/prep.md`. Use it for reviewed trusted code when a loaded host makes the local 88-shard run stall-kill; contributor/fork code stays on secretless CI or sanitized AWS unless a maintainer explicitly approves credentialed execution.
## Generated Outputs
+5 -5
View File
@@ -29,10 +29,10 @@ new directory taxonomy.
| Area | Prefer | Notes |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| Build | `pnpm build` | Runs `scripts/build-all.mjs`; use specific build scripts only when debugging a build stage. |
| Build | `pnpm build` | Runs `scripts/build-all.mts`; use specific build scripts only when debugging a build stage. |
| Changed checks | `pnpm changed:lanes --json`, `pnpm check:changed` | Lane classification lives in `scripts/changed-lanes.mjs`; changed-file checks live in `scripts/check-changed.mjs`. |
| Docs | `pnpm docs:list`, `pnpm docs:check-mdx`, `pnpm docs:check-links` | Backed by `scripts/docs-list.js`, `scripts/check-docs-mdx.mjs`, and `scripts/docs-link-audit.mjs`. |
| Formatting docs | `pnpm format:docs:check` | Uses `scripts/format-docs.mjs`; use write mode only when intentionally formatting docs. |
| Formatting docs | `pnpm format:docs:check` | Uses `scripts/format-docs.mts`; use write mode only when intentionally formatting docs. |
| Lint | `pnpm lint`, `pnpm lint:core`, `pnpm lint:all` | Wrapper scripts keep oxlint behavior aligned with repo config. |
| Targeted tests | `pnpm test <path-or-filter>` or `node scripts/run-vitest.mjs <path-or-filter>` | Avoid bare `vitest`; it can start watch mode. |
| Changed tests | `pnpm test:changed` | Uses the repo's changed-test resolver instead of a broad Vitest run. |
@@ -45,11 +45,11 @@ new directory taxonomy.
## Script Families
- `check-*.mjs` / `check-*.ts`: guardrails for architecture, docs, package
- `check-*.mts` / `check-*.ts` / retained `check-*.mjs`: guardrails for architecture, docs, package
contents, boundaries, workflows, and generated artifacts.
- `run-*.mjs`: wrappers around repo runtimes or tools, such as Node, Vitest,
- `run-*.mjs` / `run-*.mts`: stable wrappers and typed implementations for Node, Vitest,
oxlint, tsgo, and environment setup.
- `test-*.mjs` / `test-*.sh` / `test-*.ts`: test planners, Docker lanes, live
- `test-*.mts` / retained `test-*.mjs` / `test-*.sh` / `test-*.ts`: test planners, Docker lanes, live
checks, and focused validation helpers.
- `docs-*` and `check-docs-*`: docs listing, link auditing, MDX checks,
spellcheck, sync, and i18n glossary checks.
+23 -14
View File
@@ -3,7 +3,7 @@ import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { runAndroidSigningCommandSync } from "./lib/android-release-signing-process.mjs";
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.runtime.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const rootDir = resolveRepoRoot(import.meta.url);
const defaultManifestPath = path.join(rootDir, "apps", "android", "Config", "ReleaseSigning.json");
@@ -99,8 +99,27 @@ function requireString(value, key) {
return value.trim();
}
// This release entrypoint runs before dependencies are installed.
function asRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function requireGradlePropertyNames(value) {
if (
!Array.isArray(value) ||
!value.every((name) => typeof name === "string") ||
value.length !== requiredPropertyNames.length ||
!requiredPropertyNames.every((name) => value.includes(name))
) {
throw new Error(
`Android release signing manifest must list Gradle properties: ${requiredPropertyNames.join(", ")}.`,
);
}
return value;
}
function readManifest(manifestPath) {
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const parsed = asRecord(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
const manifest = {
signingRepo: requireString(parsed.signingRepo, "signingRepo"),
signingBranch: requireString(parsed.signingBranch, "signingBranch"),
@@ -115,18 +134,8 @@ function readManifest(manifestPath) {
),
apkCertificateSha256: requireString(parsed.apkCertificateSha256, "apkCertificateSha256"),
materializedRoot: requireString(parsed.materializedRoot, "materializedRoot"),
gradlePropertyNames: parsed.gradlePropertyNames,
gradlePropertyNames: requireGradlePropertyNames(parsed.gradlePropertyNames),
};
if (
!Array.isArray(manifest.gradlePropertyNames) ||
manifest.gradlePropertyNames.length !== requiredPropertyNames.length ||
!requiredPropertyNames.every((name) => manifest.gradlePropertyNames.includes(name))
) {
throw new Error(
`Android release signing manifest must list Gradle properties: ${requiredPropertyNames.join(", ")}.`,
);
}
if (!/^[a-f0-9]{64}$/u.test(manifest.apkCertificateSha256)) {
throw new Error(
"Android release signing manifest apkCertificateSha256 must be 64 lowercase hex digits.",
@@ -472,6 +481,6 @@ try {
throw new Error(`Unknown mode: ${options.mode}`);
}
} catch (error) {
process.stderr.write(`${error.message}\n`);
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
}
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env node
export function describeSeamKinds(relativePath: unknown, source: unknown): string[];
export function determineSeamTestStatus(
seamKinds: unknown,
relatedTestMatches: unknown,
): {
status: string;
reason: string;
};
export function main(argv?: string[]): Promise<void>;
export const HELP_TEXT: "Usage: node scripts/audit-seams.mjs [--help]\n\nAudit repo seam inventory and emit JSON to stdout.\n\nSections:\n duplicatedSeamFamilies Plugin SDK seam families imported from multiple production files\n overlapFiles Production files that touch multiple seam families\n optionalClusterStaticLeaks Optional extension/plugin clusters referenced from the static graph\n missingPackages Workspace packages whose deps are not mirrored at the root\n seamTestInventory High-signal seam candidates with nearby-test gap signals,\n including cron orchestration seams for agent handoff,\n outbound/media delivery, heartbeat/followup handoff,\n and scheduler state crossings, plus subagent seams\n for spawn/session handoff, announce delivery,\n lifecycle registry, cleanup, and parent streaming\n\nNotes:\n - Output is JSON only.\n - For clean redirected JSON through package scripts, prefer:\n pnpm --silent audit:seams > seam-inventory.json\n";
@@ -13,15 +13,47 @@ import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs";
import { optionalBundledClusterSet } from "./lib/optional-bundled-clusters.mjs";
import { escapeRegExp } from "./lib/regexp.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { toLine } from "./lib/ts-guard-utils.mjs";
import { toLine } from "./lib/ts-guard-utils.mts";
type ImportEntry = {
family: string;
file: string;
kind: string;
line: number;
resolvedPath: string;
specifier: string;
};
type OptionalClusterImportEntry = Omit<ImportEntry, "family"> & { cluster: string };
type ModuleSpecifierVisit = {
kind: string;
specifierNode: ts.Node;
specifier: string;
};
const MATCH_QUALITY_RANK = {
"exact-stem": 0,
"path-nearby": 1,
"direct-import": 2,
"dir-token": 3,
} as const satisfies Record<string, number>;
type MatchQuality = keyof typeof MATCH_QUALITY_RANK;
type RelatedTestMatch = { file: string; matchQuality: MatchQuality };
type PackageJson = {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
name?: string;
private?: boolean;
openclaw?: { install?: { npmSpec?: string } };
};
const repoRoot = resolveRepoRoot(import.meta.url);
const srcRoot = path.join(repoRoot, "src");
const extensionsRoot = path.join(repoRoot, BUNDLED_PLUGIN_ROOT_DIR);
const testRoot = path.join(repoRoot, "test");
const workspacePackagePaths = ["ui/package.json"];
const MAX_SCAN_BYTES = 2 * 1024 * 1024;
const compareStrings = (left, right) => left.localeCompare(right);
export const HELP_TEXT = `Usage: node scripts/audit-seams.mjs [--help]
const compareStrings = (left: string, right: string) => left.localeCompare(right);
export const HELP_TEXT = `Usage: node --import tsx scripts/audit-seams.mts [--help]
Audit repo seam inventory and emit JSON to stdout.
@@ -52,11 +84,11 @@ async function collectWorkspacePackagePaths() {
}
}
function normalizePath(filePath) {
function normalizePath(filePath: string) {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
async function readScannableText(filePath, maxBytes = MAX_SCAN_BYTES) {
async function readScannableText(filePath: string, maxBytes = MAX_SCAN_BYTES) {
const stat = await fs.stat(filePath);
if (stat.size <= maxBytes) {
return fs.readFile(filePath, "utf8");
@@ -71,7 +103,7 @@ async function readScannableText(filePath, maxBytes = MAX_SCAN_BYTES) {
}
}
function redactNpmSpec(npmSpec) {
function redactNpmSpec(npmSpec: unknown) {
if (typeof npmSpec !== "string") {
return npmSpec ?? null;
}
@@ -80,11 +112,11 @@ function redactNpmSpec(npmSpec) {
.replace(/(https?:\/\/)([^/\s:@]+)@/gi, "$1***@");
}
function isCodeFile(fileName) {
function isCodeFile(fileName: string) {
return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(fileName);
}
function isTestLikePath(relativePath) {
function isTestLikePath(relativePath: string) {
return (
/(^|\/)(__tests__|fixtures|test-utils|test-fixtures)\//.test(relativePath) ||
/(?:^|\/)[^/]*(?:[.-](?:test|spec))(?:[.-][^/]+)?\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(
@@ -93,13 +125,13 @@ function isTestLikePath(relativePath) {
);
}
function isProductionLikeFile(relativePath) {
function isProductionLikeFile(relativePath: string) {
return !isTestLikePath(relativePath);
}
async function walkCodeFiles(rootDir) {
const out = [];
async function walk(dir) {
async function walkCodeFiles(rootDir: string) {
const out: string[] = [];
async function walk(dir: string): Promise<void> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === "dist" || entry.name === "node_modules") {
@@ -124,11 +156,11 @@ async function walkCodeFiles(rootDir) {
return out.toSorted((left, right) => normalizePath(left).localeCompare(normalizePath(right)));
}
async function walkAllCodeFiles(rootDir, options = {}) {
const out = [];
async function walkAllCodeFiles(rootDir: string, options: { includeTests?: boolean } = {}) {
const out: string[] = [];
const includeTests = options.includeTests === true;
async function walk(dir) {
async function walk(dir: string): Promise<void> {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
@@ -159,31 +191,31 @@ async function walkAllCodeFiles(rootDir, options = {}) {
return out.toSorted((left, right) => normalizePath(left).localeCompare(normalizePath(right)));
}
function resolveRelativeSpecifier(specifier, importerFile) {
function resolveRelativeSpecifier(specifier: string, importerFile: string) {
if (!specifier.startsWith(".")) {
return null;
}
return normalizePath(path.resolve(path.dirname(importerFile), specifier));
}
function normalizePluginSdkFamily(resolvedPath) {
function normalizePluginSdkFamily(resolvedPath: string) {
const relative = resolvedPath.replace(/^src\/plugin-sdk\//, "");
return relative.replace(/\.(m|c)?[jt]sx?$/, "");
}
function resolveOptionalClusterFromPath(resolvedPath) {
function resolveOptionalClusterFromPath(resolvedPath: string) {
if (resolvedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
const cluster = resolvedPath.split("/")[1];
return optionalBundledClusterSet.has(cluster) ? cluster : null;
const [, cluster] = resolvedPath.split("/");
return cluster && optionalBundledClusterSet.has(cluster) ? cluster : null;
}
if (resolvedPath.startsWith("src/plugin-sdk/")) {
const cluster = normalizePluginSdkFamily(resolvedPath).split("/")[0];
return optionalBundledClusterSet.has(cluster) ? cluster : null;
const [cluster] = normalizePluginSdkFamily(resolvedPath).split("/");
return cluster && optionalBundledClusterSet.has(cluster) ? cluster : null;
}
return null;
}
function compareImports(left, right) {
function compareImports(left: ImportEntry, right: ImportEntry) {
return (
left.family.localeCompare(right.family) ||
left.file.localeCompare(right.file) ||
@@ -193,10 +225,10 @@ function compareImports(left, right) {
);
}
function collectPluginSdkImports(filePath, sourceFile) {
const entries = [];
function collectPluginSdkImports(filePath: string, sourceFile: ts.SourceFile): ImportEntry[] {
const entries: ImportEntry[] = [];
function push(kind, specifierNode, specifier) {
function push(kind: string, specifierNode: ts.Node, specifier: string) {
const resolvedPath = resolveRelativeSpecifier(specifier, filePath);
if (!resolvedPath?.startsWith("src/plugin-sdk/")) {
return;
@@ -211,15 +243,15 @@ function collectPluginSdkImports(filePath, sourceFile) {
});
}
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => {
const visit = ({ kind, specifierNode, specifier }: ModuleSpecifierVisit) =>
push(kind, specifierNode, specifier);
});
visitModuleSpecifiers(ts, sourceFile, visit);
return entries;
}
async function collectCorePluginSdkImports() {
const files = await walkCodeFiles(srcRoot);
const inventory = [];
const inventory: ImportEntry[] = [];
for (const filePath of files) {
if (normalizePath(filePath).startsWith("src/plugin-sdk/")) {
continue;
@@ -231,10 +263,13 @@ async function collectCorePluginSdkImports() {
return inventory.toSorted(compareImports);
}
function collectOptionalClusterStaticImports(filePath, sourceFile) {
const entries = [];
function collectOptionalClusterStaticImports(
filePath: string,
sourceFile: ts.SourceFile,
): OptionalClusterImportEntry[] {
const entries: OptionalClusterImportEntry[] = [];
function push(kind, specifierNode, specifier) {
function push(kind: string, specifierNode: ts.Node, specifier: string) {
if (!specifier.startsWith(".")) {
return;
}
@@ -256,17 +291,18 @@ function collectOptionalClusterStaticImports(filePath, sourceFile) {
});
}
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => {
const visit = ({ kind, specifierNode, specifier }: ModuleSpecifierVisit) => {
if (kind !== "dynamic-import") {
push(kind, specifierNode, specifier);
}
});
};
visitModuleSpecifiers(ts, sourceFile, visit);
return entries;
}
async function collectOptionalClusterStaticLeaks() {
const files = await walkCodeFiles(srcRoot);
const inventory = [];
const inventory: OptionalClusterImportEntry[] = [];
for (const filePath of files) {
const relativePath = normalizePath(filePath);
if (relativePath.startsWith("src/plugin-sdk/")) {
@@ -287,8 +323,8 @@ async function collectOptionalClusterStaticLeaks() {
});
}
function buildDuplicatedSeamFamilies(inventory) {
const grouped = new Map();
function buildDuplicatedSeamFamilies(inventory: ImportEntry[]) {
const grouped = new Map<string, ImportEntry[]>();
for (const entry of inventory) {
const bucket = grouped.get(entry.family) ?? [];
bucket.push(entry);
@@ -307,7 +343,7 @@ function buildDuplicatedSeamFamilies(inventory) {
files,
imports: entries,
},
];
] as const;
})
.filter(([, value]) => value.files.length > 1)
.toSorted((left, right) => {
@@ -322,8 +358,8 @@ function buildDuplicatedSeamFamilies(inventory) {
return duplicated;
}
function buildOverlapFiles(inventory) {
const byFile = new Map();
function buildOverlapFiles(inventory: ImportEntry[]) {
const byFile = new Map<string, ImportEntry[]>();
for (const entry of inventory) {
const bucket = byFile.get(entry.file) ?? [];
bucket.push(entry);
@@ -349,8 +385,8 @@ function buildOverlapFiles(inventory) {
});
}
function buildOptionalClusterStaticLeaks(inventory) {
const grouped = new Map();
function buildOptionalClusterStaticLeaks(inventory: OptionalClusterImportEntry[]) {
const grouped = new Map<string, OptionalClusterImportEntry[]>();
for (const entry of inventory) {
const bucket = grouped.get(entry.cluster) ?? [];
bucket.push(entry);
@@ -359,21 +395,24 @@ function buildOptionalClusterStaticLeaks(inventory) {
return Object.fromEntries(
[...grouped.entries()]
.map(([cluster, entries]) => [
cluster,
{
count: entries.length,
files: [...new Set(entries.map((entry) => entry.file))].toSorted(compareStrings),
imports: entries,
},
])
.map(
([cluster, entries]) =>
[
cluster,
{
count: entries.length,
files: [...new Set(entries.map((entry) => entry.file))].toSorted(compareStrings),
imports: entries,
},
] as const,
)
.toSorted((left, right) => {
return right[1].count - left[1].count || left[0].localeCompare(right[0]);
}),
);
}
function packageClusterMeta(relativePackagePath) {
function packageClusterMeta(relativePackagePath: string) {
if (relativePackagePath === "ui/package.json") {
return {
cluster: "ui",
@@ -382,7 +421,7 @@ function packageClusterMeta(relativePackagePath) {
reachability: "workspace-ui",
};
}
const cluster = relativePackagePath.split("/")[1];
const cluster = path.basename(path.dirname(relativePackagePath));
return {
cluster,
packageName: null,
@@ -393,7 +432,11 @@ function packageClusterMeta(relativePackagePath) {
};
}
function classifyMissingPackageCluster(params) {
function classifyMissingPackageCluster(params: {
cluster: string;
pluginSdkEntries: string[];
hasStaticLeak: boolean;
}) {
if (params.hasStaticLeak) {
return {
decision: "required",
@@ -429,8 +472,10 @@ function classifyMissingPackageCluster(params) {
};
}
async function buildMissingPackages(params = {}) {
const rootPackage = JSON.parse(await fs.readFile(path.join(repoRoot, "package.json"), "utf8"));
async function buildMissingPackages(params: { staticLeakClusters?: Set<string> } = {}) {
const rootPackage: PackageJson = JSON.parse(
await fs.readFile(path.join(repoRoot, "package.json"), "utf8"),
);
const rootDeps = new Set([
...Object.keys(rootPackage.dependencies ?? {}),
...Object.keys(rootPackage.optionalDependencies ?? {}),
@@ -438,12 +483,15 @@ async function buildMissingPackages(params = {}) {
]);
const pluginSdkEntrySources = await walkCodeFiles(path.join(repoRoot, "src", "plugin-sdk"));
const pluginSdkReachability = new Map();
const pluginSdkReachability = new Map<string, Set<string>>();
for (const filePath of pluginSdkEntrySources) {
const source = await fs.readFile(filePath, "utf8");
const matches = [...source.matchAll(/from\s+"(\.\.\/\.\.\/extensions\/([^/]+)\/[^"]+)"/g)];
for (const match of matches) {
const cluster = match[2];
if (!cluster) {
continue;
}
const bucket = pluginSdkReachability.get(cluster) ?? new Set();
bucket.add(normalizePath(filePath));
pluginSdkReachability.set(cluster, bucket);
@@ -453,7 +501,7 @@ async function buildMissingPackages(params = {}) {
const output = [];
for (const relativePackagePath of workspacePackagePaths.toSorted(compareStrings)) {
const packagePath = path.join(repoRoot, relativePackagePath);
let pkg;
let pkg: PackageJson;
try {
pkg = JSON.parse(await fs.readFile(packagePath, "utf8"));
} catch {
@@ -493,33 +541,33 @@ async function buildMissingPackages(params = {}) {
});
}
function stemFromRelativePath(relativePath) {
function stemFromRelativePath(relativePath: string) {
return relativePath.replace(/\.(m|c)?[jt]sx?$/, "");
}
function splitNameTokens(name) {
function splitNameTokens(name: string) {
return name
.split(/[^a-zA-Z0-9]+/)
.map((token) => token.trim().toLowerCase())
.filter(Boolean);
}
function hasImportSource(source, specifier) {
function hasImportSource(source: string, specifier: string) {
const escaped = escapeRegExp(specifier);
return new RegExp(`from\\s+["']${escaped}["']|import\\s*\\(\\s*["']${escaped}["']\\s*\\)`).test(
source,
);
}
function hasAnyImportSource(source, specifiers) {
function hasAnyImportSource(source: string, specifiers: string[]) {
return specifiers.some((specifier) => hasImportSource(source, specifier));
}
function isCronProductionPath(relativePath) {
function isCronProductionPath(relativePath: string) {
return relativePath.startsWith("src/cron/") && isProductionLikeFile(relativePath);
}
function isSubagentProductionPath(relativePath) {
function isSubagentProductionPath(relativePath: string) {
return (
(relativePath.startsWith("src/agents/") || relativePath.startsWith("src/cron/")) &&
isProductionLikeFile(relativePath) &&
@@ -529,7 +577,7 @@ function isSubagentProductionPath(relativePath) {
);
}
function describeCronSeamKinds(relativePath, source) {
function describeCronSeamKinds(relativePath: string, source: string) {
if (!isCronProductionPath(relativePath)) {
return [];
}
@@ -630,7 +678,7 @@ function describeCronSeamKinds(relativePath, source) {
return seamKinds;
}
function describeSubagentSeamKinds(relativePath, source) {
function describeSubagentSeamKinds(relativePath: string, source: string) {
if (!isSubagentProductionPath(relativePath)) {
return [];
}
@@ -726,7 +774,7 @@ function describeSubagentSeamKinds(relativePath, source) {
return seamKinds;
}
export function describeSeamKinds(relativePath, source) {
export function describeSeamKinds(relativePath: string, source: string) {
const seamKinds = [];
const isReplyDeliveryPath =
/reply-delivery|reply-dispatcher|deliver-reply|reply\/.*delivery|monitor\/(?:replies|deliver|native-command)|outbound\/deliver|outbound\/message/.test(
@@ -770,7 +818,7 @@ export function describeSeamKinds(relativePath, source) {
return [...new Set(seamKinds)].toSorted(compareStrings);
}
async function buildTestIndex(testFiles) {
async function buildTestIndex(testFiles: string[]) {
return Promise.all(
testFiles.map(async (filePath) => {
const relativePath = normalizePath(filePath);
@@ -790,7 +838,9 @@ async function buildTestIndex(testFiles) {
);
}
function hasExecutableImportReference(source, importPath) {
type TestIndexEntry = Awaited<ReturnType<typeof buildTestIndex>>[number];
function hasExecutableImportReference(source: string, importPath: string) {
const escapedImportPath = escapeRegExp(importPath);
const suffix = String.raw`(?:\.[^"'\\\`]+)?`;
const patterns = [
@@ -802,7 +852,7 @@ function hasExecutableImportReference(source, importPath) {
return patterns.some((pattern) => pattern.test(source));
}
function hasModuleMockReference(source, importPath) {
function hasModuleMockReference(source: string, importPath: string) {
const escapedImportPath = escapeRegExp(importPath);
const suffix = String.raw`(?:\.[^"'\\\`]+)?`;
const patterns = [
@@ -812,29 +862,16 @@ function hasModuleMockReference(source, importPath) {
return patterns.some((pattern) => pattern.test(source));
}
function matchQualityRank(quality) {
switch (quality) {
case "exact-stem":
return 0;
case "path-nearby":
return 1;
case "direct-import":
return 2;
case "dir-token":
return 3;
default:
return 4;
}
}
const matchQualityRank = (quality: MatchQuality) => MATCH_QUALITY_RANK[quality] ?? 4;
function findRelatedTests(relativePath, testIndex) {
function findRelatedTests(relativePath: string, testIndex: TestIndexEntry[]): RelatedTestMatch[] {
const stem = stemFromRelativePath(relativePath);
const baseName = path.basename(stem);
const dirName = path.dirname(relativePath);
const normalizedDir = dirName.split(path.sep).join("/");
const baseTokens = new Set(splitNameTokens(baseName).filter((token) => token.length >= 7));
const matches = testIndex.flatMap((entry) => {
const matches: RelatedTestMatch[] = testIndex.flatMap((entry): RelatedTestMatch[] => {
if (entry.stem === stem) {
return [{ file: entry.relativePath, matchQuality: "exact-stem" }];
}
@@ -864,7 +901,7 @@ function findRelatedTests(relativePath, testIndex) {
return [];
});
const byFile = new Map();
const byFile = new Map<string, RelatedTestMatch>();
for (const match of matches) {
const existing = byFile.get(match.file);
if (
@@ -883,7 +920,10 @@ function findRelatedTests(relativePath, testIndex) {
});
}
export function determineSeamTestStatus(seamKinds, relatedTestMatches) {
export function determineSeamTestStatus(
seamKinds: string[],
relatedTestMatches: RelatedTestMatch[],
) {
if (relatedTestMatches.length === 0) {
return {
status: "gap",
@@ -962,7 +1002,7 @@ async function buildSeamTestInventory() {
});
}
export async function main(argv = process.argv.slice(2)) {
export async function main(argv: string[] = process.argv.slice(2)) {
const args = new Set(argv);
if (args.has("--help") || args.has("-h")) {
process.stdout.write(`${HELP_TEXT}\n`);
-24
View File
@@ -1,24 +0,0 @@
export function parseArgs(argv: unknown): {
maxWorkers?: unknown;
cwd: string;
mode: unknown;
ref: unknown;
rss: unknown;
};
export function parseMaxRssBytes(output: unknown): number | null;
export function formatRss(valueBytes: unknown): string;
export function resolveBenchRssResult({
label,
output,
rss,
status,
}: {
label: unknown;
output: unknown;
rss: unknown;
status: unknown;
}): {
maxRssBytes: number | null;
output: unknown;
status: unknown;
};
@@ -2,10 +2,25 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { formatMs } from "./lib/vitest-report-cli-utils.mjs";
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.mts";
import { formatMs } from "./lib/vitest-report-cli-utils.mts";
function parsePositiveInteger(raw, label) {
type BenchOptions = {
cwd: string;
maxWorkers?: number;
mode: "ref" | "worktree";
ref: string;
rss: boolean;
};
type BenchResult = ReturnType<typeof runBenchCommand>;
type BenchCommandParams = Pick<BenchOptions, "cwd" | "maxWorkers" | "rss"> & {
command: [string, ...string[]];
label: string;
};
function parsePositiveInteger(raw: string | undefined, label: string) {
const value = raw?.trim();
if (!value) {
throw new Error(`${label} requires a value`);
@@ -20,9 +35,9 @@ function parsePositiveInteger(raw, label) {
return parsed;
}
function positiveIntegerFlag(flag, key) {
function positiveIntegerFlag(flag: string, key: "maxWorkers") {
return {
consume(argv, index) {
consume(argv: readonly string[], index: number) {
if (argv[index] !== flag) {
return null;
}
@@ -34,7 +49,7 @@ function positiveIntegerFlag(flag, key) {
flag,
nextIndex: index + 1,
repeatable: false,
apply(target) {
apply(target: BenchOptions) {
target[key] = parsePositiveInteger(rawValue, flag);
},
};
@@ -42,7 +57,7 @@ function positiveIntegerFlag(flag, key) {
};
}
export function parseArgs(argv) {
export function parseArgs(argv: string[]): BenchOptions {
const args = parseFlagArgs(
argv,
{
@@ -57,7 +72,7 @@ export function parseArgs(argv) {
positiveIntegerFlag("--max-workers", "maxWorkers"),
],
{
onUnhandledArg(arg, target) {
onUnhandledArg(arg: string, target: BenchOptions) {
if (arg === "--no-rss") {
target.rss = false;
return "handled";
@@ -79,11 +94,11 @@ export function parseArgs(argv) {
};
}
function quoteArg(arg) {
function quoteArg(arg: string) {
return /[^A-Za-z0-9_./:-]/.test(arg) ? JSON.stringify(arg) : arg;
}
function runGitList(args, cwd) {
function runGitList(args: string[], cwd: string) {
const result = spawnSync("git", args, {
cwd,
encoding: "utf8",
@@ -97,7 +112,7 @@ function runGitList(args, cwd) {
.filter((line) => line.length > 0);
}
function listChangedPaths(opts) {
function listChangedPaths(opts: BenchOptions) {
if (opts.mode === "worktree") {
return [
...new Set([
@@ -109,7 +124,7 @@ function listChangedPaths(opts) {
return runGitList(["diff", "--name-only", `${opts.ref}...HEAD`], opts.cwd);
}
export function parseMaxRssBytes(output) {
export function parseMaxRssBytes(output: string) {
const match = output.match(
/(?:^|\n)[^\S\r\n]*(\d+)[^\S\r\n]+maximum resident set size[^\S\r\n]*(?:\r?\n|$)/u,
);
@@ -120,14 +135,24 @@ export function parseMaxRssBytes(output) {
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
}
export function formatRss(valueBytes) {
export function formatRss(valueBytes: number | null) {
if (valueBytes === null) {
return "n/a";
}
return `${(valueBytes / 1024 / 1024).toFixed(1)}MB`;
}
export function resolveBenchRssResult({ label, output, rss, status }) {
export function resolveBenchRssResult({
label,
output,
rss,
status,
}: {
label: string;
output: string;
rss: boolean;
status: number;
}) {
if (!rss) {
return { maxRssBytes: null, output, status };
}
@@ -142,7 +167,7 @@ export function resolveBenchRssResult({ label, output, rss, status }) {
return { maxRssBytes, output, status };
}
function runBenchCommand(params) {
function runBenchCommand(params: BenchCommandParams) {
const env = { ...process.env };
if (typeof params.maxWorkers === "number") {
env.OPENCLAW_VITEST_MAX_WORKERS = String(params.maxWorkers);
@@ -150,7 +175,7 @@ function runBenchCommand(params) {
const startedAt = process.hrtime.bigint();
const commandArgs = params.rss ? ["-l", ...params.command] : params.command;
const result = spawnSync(
params.rss ? "/usr/bin/time" : commandArgs[0],
params.rss ? "/usr/bin/time" : params.command[0],
params.rss ? commandArgs : commandArgs.slice(1),
{
cwd: params.cwd,
@@ -175,7 +200,7 @@ function runBenchCommand(params) {
};
}
function printRunSummary(label, result) {
function printRunSummary(label: string, result: BenchResult) {
console.log(
`${label.padEnd(8, " ")} wall=${formatMs(result.elapsedMs).padStart(9, " ")} rss=${formatRss(
result.maxRssBytes,
@@ -211,11 +236,11 @@ function main() {
console.log(`- ${changedPath}`);
}
const routedCommand =
const routedCommand: [string, ...string[]] =
opts.mode === "worktree"
? [process.execPath, "scripts/test-projects.mjs", ...changedPaths]
: [process.execPath, "scripts/test-projects.mjs", "--changed", opts.ref];
const rootCommand = [
? [process.execPath, "--import", "tsx", "scripts/test-projects.mts", ...changedPaths]
: [process.execPath, "--import", "tsx", "scripts/test-projects.mts", "--changed", opts.ref];
const rootCommand: [string, ...string[]] = [
process.execPath,
"scripts/run-vitest.mjs",
"run",
+1 -1
View File
@@ -7,7 +7,7 @@ import { createWebFetchTool } from "../src/agents/tools/web-fetch.js";
import type { OpenClawConfig } from "../src/config/types.openclaw.js";
import type { LookupFn } from "../src/infra/net/ssrf.js";
import { extractReadableContent } from "../src/web-fetch/content-extractors.runtime.js";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts";
type BenchmarkCaseId =
| "tool-create"
-109
View File
@@ -1,109 +0,0 @@
import type fs from "node:fs";
export type BuildCacheEntry =
| string
| {
path: string;
excludeDirectories?: string[];
extensions?: string[];
recursive?: boolean;
};
export type BuildAllStep = {
label: string;
kind?: "node" | "pnpm";
args?: string[];
pnpmArgs?: string[];
env?: NodeJS.ProcessEnv;
windowsNodeOptions?: string;
cache?: {
env?: string[];
inputs: BuildCacheEntry[];
outputs: BuildCacheEntry[];
requiredOutputs?: string[] | ((env: NodeJS.ProcessEnv) => string[]);
restore?: "always";
runOnHit?: {
env?: NodeJS.ProcessEnv;
finalize?: "refresh";
};
};
};
export type BuildAllCacheState = {
cacheable: boolean;
fresh: boolean;
restorable?: boolean;
reason: string;
signature?: string;
outputRoot?: string;
stampPath?: string;
inputFiles?: number;
outputFiles?: number;
relativeOutputFiles?: string[];
stampedOutputs?: string[];
};
export const BUILD_ALL_STEPS: BuildAllStep[];
export const BUILD_ALL_PROFILES: Record<string, string[]>;
export const BUILD_ALL_PROFILE_STEP_ENV: Record<string, Record<string, NodeJS.ProcessEnv>>;
export function buildAllUsage(): string;
export function parseBuildAllArgs(argv: string[]): { help: boolean; profile: string };
export function resolveBuildAllSteps(profile?: string): BuildAllStep[];
export function resolveBuildAllEnvironment(
env?: NodeJS.ProcessEnv,
now?: () => Date,
readGitCommit?: () => string | null,
): { [key: string]: string | undefined; OPENCLAW_BUILD_TIMESTAMP: string };
export function resolveBuildAllStep(
step: BuildAllStep,
params?: {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
nodeExecPath?: string;
npmExecPath?: string;
comSpec?: string;
},
): {
command: string;
args: string[];
options: {
stdio: "inherit";
env: NodeJS.ProcessEnv;
shell?: boolean;
windowsVerbatimArguments?: boolean;
};
};
export function resolveBuildAllStepOnCacheHit(step: BuildAllStep): BuildAllStep | null;
export function resolveBuildAllStepCacheState(
step: BuildAllStep,
params?: { rootDir?: string; fs?: typeof fs; env?: NodeJS.ProcessEnv },
): BuildAllCacheState;
export function writeBuildAllStepCacheStamp(
step: BuildAllStep,
cacheState: BuildAllCacheState,
params?: { rootDir?: string; fs?: typeof fs; env?: NodeJS.ProcessEnv },
): void;
export function resolveBuildAllStepCacheStampState(
step: BuildAllStep,
cacheState: BuildAllCacheState,
params?: { rootDir?: string; fs?: typeof fs },
): BuildAllCacheState;
export function restoreBuildAllStepCacheOutputs(
cacheState: BuildAllCacheState,
params?: { rootDir?: string; fs?: typeof fs },
): boolean;
export function finalizeBuildAllStepCache(
step: BuildAllStep,
cacheState: BuildAllCacheState,
params?: {
rootDir?: string;
fs?: typeof fs;
env?: NodeJS.ProcessEnv;
reusedCache?: boolean;
},
): boolean;
export function formatBuildAllDuration(durationMs: number): string;
export function formatBuildAllTimingSummary(
timings: Array<{ label: string; durationMs: number; status: string }>,
): string;
+182 -140
View File
@@ -1,28 +1,64 @@
#!/usr/bin/env node
// Builds OpenClaw packages and plugin SDK artifacts with cache-aware orchestration.
import { spawnSync } from "node:child_process";
import { spawnSync, type SpawnSyncOptions } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { pathToFileURL } from "node:url";
import { asRecord } from "@openclaw/normalization-core/record-coerce";
import prettyMilliseconds from "pretty-ms";
import {
listPluginSdkDeclarationOutputs,
pluginSdkEntrypoints,
} from "./lib/plugin-sdk-entries.mjs";
} from "./lib/plugin-sdk-entries.mts";
import {
TSDOWN_PACKAGE_CONFIG_GROUP,
TSDOWN_UNIFIED_CONFIG_GROUP,
} from "./lib/tsdown-config-groups.mjs";
} from "./lib/tsdown-config-groups.mts";
import {
TSDOWN_PACKAGE_OUTPUT_ROOTS,
tsdownPackageOutputRoot,
} from "./lib/tsdown-output-roots.mjs";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
} from "./lib/tsdown-output-roots.mts";
import { resolvePnpmRunner } from "./pnpm-runner.mts";
const nodeBin = process.execPath;
type BuildCachePath = {
path: string;
excludeDirectories?: string[];
extensions?: string[];
recursive?: boolean;
};
export type BuildCacheEntry = string | BuildCachePath;
type BuildCache = {
env?: string[];
inputs: BuildCacheEntry[];
outputs: BuildCacheEntry[];
requiredOutputs?: string[] | ((env: NodeJS.ProcessEnv) => string[]);
restore?: "always";
runOnHit?: { env?: NodeJS.ProcessEnv; finalize?: "refresh" };
};
type BuildCacheStep = { label: string; env?: NodeJS.ProcessEnv; cache?: BuildCache };
export type BuildAllStep = BuildCacheStep &
(
| { kind: "pnpm"; args?: never; pnpmArgs: string[]; windowsNodeOptions?: string }
| { kind?: "node"; args: string[]; pnpmArgs?: never; windowsNodeOptions?: string }
);
type BuildAllTiming = { label: string; durationMs: number; status: string };
type BuildAllFs = typeof fs;
type BuildAllStepParams = {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
nodeExecPath?: string;
npmExecPath?: string;
comSpec?: string;
};
type BuildAllCacheParams = { rootDir?: string; fs?: BuildAllFs; env?: NodeJS.ProcessEnv };
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu;
const BUILD_CACHE_VERSION = 4;
const TSDOWN_DECLARATION_EXTENSIONS = [".d.ts", ".d.mts", ".d.cts"];
@@ -48,17 +84,17 @@ const TSDOWN_DECLARATION_TOOL_INPUTS = [
"package.json",
"pnpm-lock.yaml",
"tsconfig.json",
"scripts/tsdown-build.mjs",
"scripts/tsdown-build.mts",
"scripts/lib/bundled-plugin-build-entries.mjs",
"scripts/lib/bundled-plugin-paths.mjs",
"scripts/lib/optional-bundled-clusters.mjs",
"scripts/lib/plugin-sdk-entries.mjs",
"scripts/lib/plugin-sdk-entries.mts",
"scripts/lib/plugin-sdk-entrypoints.json",
"scripts/lib/plugin-sdk-private-local-only-subpaths.json",
"scripts/lib/plugin-sdk-deprecated-public-subpaths.json",
"scripts/lib/plugin-sdk-deprecated-barrel-subpaths.json",
"scripts/lib/tsdown-config-groups.mjs",
"scripts/lib/tsdown-output-roots.mjs",
"scripts/lib/tsdown-config-groups.mts",
"scripts/lib/tsdown-output-roots.mts",
];
const TSDOWN_PACKAGES_CACHE_INPUT = {
path: "packages",
@@ -78,7 +114,7 @@ const TSDOWN_UNIFIED_CACHE_INPUTS = [
},
TSDOWN_PACKAGES_CACHE_INPUT,
];
const declarationCacheOutputs = (roots) =>
const declarationCacheOutputs = (roots: string[]) =>
roots.map((root) => ({ path: root, extensions: TSDOWN_DECLARATION_EXTENSIONS }));
const PLUGIN_SDK_ENTRY_DTS_CACHE_ENV = [
"OPENCLAW_BUILD_PRIVATE_QA",
@@ -86,7 +122,7 @@ const PLUGIN_SDK_ENTRY_DTS_CACHE_ENV = [
];
const PLUGIN_SDK_ENTRY_DTS_SHARED_CACHE_INPUTS = [
"scripts/write-plugin-sdk-entry-dts.ts",
"scripts/lib/plugin-sdk-entries.mjs",
"scripts/lib/plugin-sdk-entries.mts",
"scripts/lib/plugin-sdk-entrypoints.json",
"scripts/lib/plugin-sdk-private-local-only-subpaths.json",
"scripts/lib/plugin-sdk-deprecated-public-subpaths.json",
@@ -121,18 +157,24 @@ const PLUGIN_SDK_SELF_BUILT_ENTRY_DTS_CACHE_OUTPUTS = [
{ path: "dist/plugin-sdk", extensions: [".d.ts"], recursive: false },
...PLUGIN_SDK_ENTRY_DTS_CACHE_OUTPUTS,
];
const tsxScript = (script: string, ...args: string[]) => ["--import", "tsx", script, ...args];
const nodeStep = (label: string, args: string[]): Extract<BuildAllStep, { kind?: "node" }> => ({
label,
kind: "node",
args,
});
const tsxStep = (label: string, script: string, ...args: string[]) =>
nodeStep(label, tsxScript(script, ...args));
const PNPM_STEP_NODE_FALLBACKS = new Map([
["plugins:assets:build", ["scripts/bundled-plugin-assets.mjs", "--phase", "build"]],
["plugins:assets:copy", ["scripts/bundled-plugin-assets.mjs", "--phase", "copy"]],
["plugins:assets:build", tsxScript("scripts/bundled-plugin-assets.mts", "--phase", "build")],
["plugins:assets:copy", tsxScript("scripts/bundled-plugin-assets.mts", "--phase", "copy")],
["ui:build", ["scripts/ui.js", "build"]],
]);
export const BUILD_ALL_STEPS = [
export const BUILD_ALL_STEPS: BuildAllStep[] = [
{ label: "plugins:assets:build", kind: "pnpm", pnpmArgs: ["plugins:assets:build"] },
{ label: "tsdown", kind: "node", args: ["scripts/tsdown-build.mjs"] },
tsxStep("tsdown", "scripts/tsdown-build.mts"),
{
label: "tsdown-ai",
kind: "node",
args: ["scripts/tsdown-build.mjs", "--config", "tsdown.ai.config.ts"],
...tsxStep("tsdown-ai", "scripts/tsdown-build.mts", "--config", "tsdown.ai.config.ts"),
cache: {
env: ["OPENCLAW_RUN_NODE_SKIP_DTS_BUILD"],
inputs: [
@@ -148,15 +190,14 @@ export const BUILD_ALL_STEPS = [
},
},
{
label: "tsdown-packages",
kind: "node",
args: [
"scripts/tsdown-build.mjs",
...tsxStep(
"tsdown-packages",
"scripts/tsdown-build.mts",
"--config",
"tsdown.config.ts",
"--filter",
TSDOWN_PACKAGE_CONFIG_GROUP,
],
),
cache: {
env: ["OPENCLAW_RUN_NODE_SKIP_DTS_BUILD"],
inputs: [...TSDOWN_DECLARATION_TOOL_INPUTS, "tsdown.config.ts", TSDOWN_PACKAGES_CACHE_INPUT],
@@ -168,15 +209,14 @@ export const BUILD_ALL_STEPS = [
},
},
{
label: "tsdown-unified",
kind: "node",
args: [
"scripts/tsdown-build.mjs",
...tsxStep(
"tsdown-unified",
"scripts/tsdown-build.mts",
"--config",
"tsdown.config.ts",
"--filter",
TSDOWN_UNIFIED_CONFIG_GROUP,
],
),
cache: {
env: ["OPENCLAW_BUILD_PRIVATE_QA", "OPENCLAW_RUN_NODE_SKIP_DTS_BUILD"],
inputs: [
@@ -195,27 +235,17 @@ export const BUILD_ALL_STEPS = [
},
},
},
{
label: "check-cli-bootstrap-imports",
kind: "node",
args: ["scripts/check-cli-bootstrap-imports.mjs"],
},
tsxStep("check-cli-bootstrap-imports", "scripts/check-cli-bootstrap-imports.mts"),
{
label: "plugins:assets:copy",
kind: "pnpm",
pnpmArgs: ["plugins:assets:copy"],
},
{ label: "runtime-postbuild", kind: "node", args: ["scripts/runtime-postbuild.mjs"] },
{ label: "build-stamp", kind: "node", args: ["scripts/build-stamp.mjs"] },
nodeStep("runtime-postbuild", ["scripts/runtime-postbuild.mjs"]),
tsxStep("build-stamp", "scripts/build-stamp.mts"),
tsxStep("runtime-postbuild-stamp", "scripts/runtime-postbuild-stamp.mts"),
{
label: "runtime-postbuild-stamp",
kind: "node",
args: ["scripts/runtime-postbuild-stamp.mjs"],
},
{
label: "write-plugin-sdk-entry-dts",
kind: "node",
args: ["--import", "tsx", "scripts/write-plugin-sdk-entry-dts.ts"],
...tsxStep("write-plugin-sdk-entry-dts", "scripts/write-plugin-sdk-entry-dts.ts"),
env: {
OPENCLAW_PLUGIN_SDK_CANONICAL_DTS: "1",
},
@@ -226,16 +256,8 @@ export const BUILD_ALL_STEPS = [
restore: "always",
},
},
{
label: "check-plugin-sdk-exports",
kind: "node",
args: ["scripts/check-plugin-sdk-exports.mjs"],
},
{
label: "copy-hook-metadata",
kind: "node",
args: ["--import", "tsx", "scripts/copy-hook-metadata.ts"],
},
tsxStep("check-plugin-sdk-exports", "scripts/check-plugin-sdk-exports.mts"),
tsxStep("copy-hook-metadata", "scripts/copy-hook-metadata.ts"),
{
label: "ui:build",
kind: "pnpm",
@@ -246,15 +268,9 @@ export const BUILD_ALL_STEPS = [
// warm hit could restore stale service-worker/app cache metadata.
cache: undefined,
},
tsxStep("write-build-info", "scripts/write-build-info.ts"),
{
label: "write-build-info",
kind: "node",
args: ["--import", "tsx", "scripts/write-build-info.ts"],
},
{
label: "write-cli-startup-metadata",
kind: "node",
args: ["--import", "tsx", "scripts/write-cli-startup-metadata.ts"],
...tsxStep("write-cli-startup-metadata", "scripts/write-cli-startup-metadata.ts"),
cache: {
inputs: [
"scripts/write-cli-startup-metadata.ts",
@@ -267,7 +283,7 @@ export const BUILD_ALL_STEPS = [
},
];
export const BUILD_ALL_PROFILES = {
export const BUILD_ALL_PROFILES: Record<string, string[]> = {
full: [
"plugins:assets:build",
"tsdown-ai",
@@ -337,7 +353,7 @@ export const BUILD_ALL_PROFILES = {
],
};
export const BUILD_ALL_PROFILE_STEP_ENV = {
export const BUILD_ALL_PROFILE_STEP_ENV: Record<string, Record<string, NodeJS.ProcessEnv>> = {
full: {
"tsdown-unified": {
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
@@ -388,7 +404,7 @@ export const BUILD_ALL_PROFILE_STEP_ENV = {
export function buildAllUsage() {
return [
"Usage: node scripts/build-all.mjs [profile]",
"Usage: node --import tsx scripts/build-all.mts [profile]",
"",
"Builds OpenClaw artifacts for the selected profile.",
"",
@@ -400,7 +416,7 @@ export function buildAllUsage() {
].join("\n");
}
export function parseBuildAllArgs(argv) {
export function parseBuildAllArgs(argv: string[]) {
const args = {
help: false,
profile: "full",
@@ -424,7 +440,7 @@ export function parseBuildAllArgs(argv) {
return args;
}
export function resolveBuildAllSteps(profile = "full") {
export function resolveBuildAllSteps(profile = "full"): BuildAllStep[] {
const labels = BUILD_ALL_PROFILES[profile];
if (!labels) {
throw new Error(`Unknown build profile: ${profile}`);
@@ -435,28 +451,30 @@ export function resolveBuildAllSteps(profile = "full") {
throw new Error(`Build profile ${profile} references unknown steps: ${missing.join(", ")}`);
}
const envOverrides = BUILD_ALL_PROFILE_STEP_ENV[profile] ?? {};
return selected.map((step) => {
const env = envOverrides[step.label];
if (!env) {
return step;
}
const mergedEnv = Object.assign({}, step.env, env);
const merged = Object.assign({}, step, { env: mergedEnv });
// Self-built declarations need both the complete repository-owned type
// graph and the flat declarations that this step generates after tsdown
// clears dist. Canonical mode keeps its narrower generated-dts cache.
if (
step.label === "write-plugin-sdk-entry-dts" &&
mergedEnv.OPENCLAW_PLUGIN_SDK_CANONICAL_DTS !== "1"
) {
merged.cache = {
...step.cache,
inputs: PLUGIN_SDK_SELF_BUILT_ENTRY_DTS_CACHE_INPUTS,
outputs: PLUGIN_SDK_SELF_BUILT_ENTRY_DTS_CACHE_OUTPUTS,
};
}
return merged;
});
return selected
.filter((step): step is NonNullable<typeof step> => step !== undefined)
.map((step) => {
const env = envOverrides[step.label];
if (!env) {
return step;
}
const mergedEnv = Object.assign({}, "env" in step ? step.env : undefined, env);
const merged: BuildAllStep = Object.assign({}, step, { env: mergedEnv });
// Self-built declarations need both the complete repository-owned type
// graph and the flat declarations that this step generates after tsdown
// clears dist. Canonical mode keeps its narrower generated-dts cache.
if (
step.label === "write-plugin-sdk-entry-dts" &&
mergedEnv.OPENCLAW_PLUGIN_SDK_CANONICAL_DTS !== "1"
) {
merged.cache = {
...step.cache,
inputs: PLUGIN_SDK_SELF_BUILT_ENTRY_DTS_CACHE_INPUTS,
outputs: PLUGIN_SDK_SELF_BUILT_ENTRY_DTS_CACHE_OUTPUTS,
};
}
return merged;
});
}
function readCurrentGitCommit() {
@@ -469,9 +487,9 @@ function readCurrentGitCommit() {
/** Pin one source identity for every child process that contributes to this build. */
export function resolveBuildAllEnvironment(
env = process.env,
now = () => new Date(),
readGitCommit = readCurrentGitCommit,
env: NodeJS.ProcessEnv = process.env,
now: () => Date = () => new Date(),
readGitCommit: () => string | null = readCurrentGitCommit,
) {
const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim();
const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
@@ -481,17 +499,14 @@ export function resolveBuildAllEnvironment(
if (commit && !FULL_GIT_COMMIT_RE.test(commit)) {
throw new Error("build commit must be a full 40-character hexadecimal SHA");
}
const buildEnv = {
return {
...env,
OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(),
...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}),
};
if (commit) {
buildEnv.GIT_COMMIT = commit.toLowerCase();
}
return buildEnv;
}
function resolveStepEnv(step, env, platform) {
function resolveStepEnv(step: BuildAllStep, env: NodeJS.ProcessEnv, platform: NodeJS.Platform) {
const stepEnv = step.env ? Object.assign({}, env, step.env) : env;
if (platform !== "win32" || !step.windowsNodeOptions) {
return stepEnv;
@@ -508,7 +523,7 @@ function resolveStepEnv(step, env, platform) {
};
}
export function resolveBuildAllStep(step, params = {}) {
export function resolveBuildAllStep(step: BuildAllStep, params: BuildAllStepParams = {}) {
const platform = params.platform ?? process.platform;
const env = resolveStepEnv(step, params.env ?? process.env, platform);
if (step.kind === "pnpm") {
@@ -521,7 +536,7 @@ export function resolveBuildAllStep(step, params = {}) {
options: {
stdio: "inherit",
env,
},
} satisfies SpawnSyncOptions,
};
}
const runner = resolvePnpmRunner({
@@ -540,7 +555,7 @@ export function resolveBuildAllStep(step, params = {}) {
env,
shell: runner.shell,
windowsVerbatimArguments: runner.windowsVerbatimArguments,
},
} satisfies SpawnSyncOptions,
};
}
return {
@@ -549,11 +564,11 @@ export function resolveBuildAllStep(step, params = {}) {
options: {
stdio: "inherit",
env,
},
} satisfies SpawnSyncOptions,
};
}
export function resolveBuildAllStepOnCacheHit(step) {
export function resolveBuildAllStepOnCacheHit(step: BuildAllStep) {
if (!step.cache?.runOnHit) {
return null;
}
@@ -563,25 +578,18 @@ export function resolveBuildAllStepOnCacheHit(step) {
};
}
function normalizeCacheEntry(entry) {
if (typeof entry === "string") {
return { path: entry };
}
return entry;
}
function cacheEntryIncludesFile(entry, filePath) {
function cacheEntryIncludesFile(entry: BuildCachePath, filePath: string) {
if (!entry.extensions?.length) {
return true;
}
return entry.extensions.some((extension) => filePath.endsWith(extension));
}
function cacheEntryExcludesDirectory(entry, name) {
return entry.excludeDirectories?.includes(name) ?? false;
}
function listFilesRecursively(rootPath, fsImpl, cacheEntry = { path: rootPath }) {
function listFilesRecursively(
rootPath: string,
fsImpl: BuildAllFs,
cacheEntry: BuildCachePath = { path: rootPath },
) {
let stat;
try {
stat = fsImpl.statSync(rootPath);
@@ -594,7 +602,7 @@ function listFilesRecursively(rootPath, fsImpl, cacheEntry = { path: rootPath })
if (!stat.isDirectory()) {
return [];
}
const out = [];
const out: string[] = [];
const entries = fsImpl.readdirSync(rootPath, { withFileTypes: true });
const recursive = cacheEntry.recursive !== false;
for (const dirent of entries) {
@@ -602,7 +610,7 @@ function listFilesRecursively(rootPath, fsImpl, cacheEntry = { path: rootPath })
continue;
}
const entryPath = path.join(rootPath, dirent.name);
if (dirent.isDirectory() && cacheEntryExcludesDirectory(cacheEntry, dirent.name)) {
if (dirent.isDirectory() && cacheEntry.excludeDirectories?.includes(dirent.name)) {
continue;
}
if (dirent.isDirectory() && recursive) {
@@ -614,22 +622,22 @@ function listFilesRecursively(rootPath, fsImpl, cacheEntry = { path: rootPath })
return out;
}
function listCacheFiles(rootDir, entries, fsImpl) {
function listCacheFiles(rootDir: string, entries: BuildCacheEntry[], fsImpl: BuildAllFs) {
return entries
.map(normalizeCacheEntry)
.map((entry) => (typeof entry === "string" ? { path: entry } : entry))
.flatMap((entry) => listFilesRecursively(path.resolve(rootDir, entry.path), fsImpl, entry))
.toSorted();
}
function portableRelativePath(rootDir, filePath) {
function portableRelativePath(rootDir: string, filePath: string) {
return path.relative(rootDir, filePath).split(path.sep).join("/");
}
function normalizePortablePath(filePath) {
function normalizePortablePath(filePath: string) {
return filePath.replaceAll("\\", "/");
}
function resolveCacheRequiredOutputs(cache, env) {
function resolveCacheRequiredOutputs(cache: BuildCache, env: NodeJS.ProcessEnv) {
const outputs =
typeof cache.requiredOutputs === "function"
? cache.requiredOutputs(env)
@@ -637,7 +645,7 @@ function resolveCacheRequiredOutputs(cache, env) {
return outputs.map((output) => normalizePortablePath(output));
}
function resolveBuildCacheRoot(rootDir, env) {
function resolveBuildCacheRoot(rootDir: string, env: NodeJS.ProcessEnv) {
// Dev update preflight and final builds run in separate worktrees. A shared
// root lets content signatures decide reuse without relocating built trees.
const configuredRoot = env?.BUILD_ALL_CACHE_ROOT?.trim();
@@ -649,7 +657,7 @@ function resolveBuildCacheRoot(rootDir, env) {
: path.resolve(rootDir, configuredRoot);
}
function resolveCachePaths(rootDir, step, env) {
function resolveCachePaths(rootDir: string, step: BuildCacheStep, env: NodeJS.ProcessEnv) {
const safeLabel = step.label.replace(/[^a-zA-Z0-9._-]+/g, "_");
const cacheDir = path.join(resolveBuildCacheRoot(rootDir, env), safeLabel);
return {
@@ -659,7 +667,13 @@ function resolveCachePaths(rootDir, step, env) {
};
}
function hashInputFiles(rootDir, files, fsImpl, envEntries = [], env = process.env) {
function hashInputFiles(
rootDir: string,
files: string[],
fsImpl: BuildAllFs,
envEntries: string[] = [],
env: NodeJS.ProcessEnv = process.env,
) {
const hash = createHash("sha256");
hash.update(`v${BUILD_CACHE_VERSION}\0`);
for (const name of envEntries.toSorted((left, right) => left.localeCompare(right))) {
@@ -677,15 +691,24 @@ function hashInputFiles(rootDir, files, fsImpl, envEntries = [], env = process.e
return hash.digest("hex");
}
function readCacheStamp(stampPath, fsImpl) {
function readCacheStamp(stampPath: string, fsImpl: BuildAllFs) {
try {
return JSON.parse(fsImpl.readFileSync(stampPath, "utf8"));
const stamp = asRecord(JSON.parse(fsImpl.readFileSync(stampPath, "utf8")));
const { version, signature, outputs: rawOutputs } = stamp;
const outputs = Array.isArray(rawOutputs)
? rawOutputs.filter((entry) => typeof entry === "string")
: undefined;
return {
version: typeof version === "number" ? version : undefined,
signature: typeof signature === "string" ? signature : undefined,
outputs,
};
} catch {
return undefined;
}
}
function hasAllFiles(rootDir, relativeFiles, fsImpl) {
function hasAllFiles(rootDir: string, relativeFiles: string[], fsImpl: BuildAllFs) {
return relativeFiles.every((relativeFile) => {
try {
return fsImpl.statSync(path.resolve(rootDir, relativeFile)).isFile();
@@ -695,12 +718,15 @@ function hasAllFiles(rootDir, relativeFiles, fsImpl) {
});
}
function copyFileSync(fsImpl, sourcePath, targetPath) {
function copyFileSync(fsImpl: BuildAllFs, sourcePath: string, targetPath: string) {
fsImpl.mkdirSync(path.dirname(targetPath), { recursive: true });
fsImpl.copyFileSync(sourcePath, targetPath);
}
export function resolveBuildAllStepCacheState(step, params = {}) {
export function resolveBuildAllStepCacheState(
step: BuildCacheStep,
params: BuildAllCacheParams = {},
) {
if (!step.cache) {
return { cacheable: false, fresh: false, reason: "no-cache" };
}
@@ -721,9 +747,7 @@ export function resolveBuildAllStepCacheState(step, params = {}) {
const stamp = readCacheStamp(stampPath, fsImpl);
const outputFiles = listCacheFiles(rootDir, step.cache.outputs, fsImpl);
const relativeOutputFiles = outputFiles.map((file) => portableRelativePath(rootDir, file));
const stampedOutputs = Array.isArray(stamp?.outputs)
? stamp.outputs.map((entry) => normalizePortablePath(entry))
: [];
const stampedOutputs = stamp?.outputs?.map((entry) => normalizePortablePath(entry)) ?? [];
const requiredOutputs = resolveCacheRequiredOutputs(step.cache, params.env ?? process.env);
const stampedOutputSet = new Set(stampedOutputs);
// Restore trusts the stamp inventory, so legacy partial stamps must name the
@@ -759,8 +783,15 @@ export function resolveBuildAllStepCacheState(step, params = {}) {
};
}
export function writeBuildAllStepCacheStamp(step, cacheState, params = {}) {
type BuildAllCacheState = ReturnType<typeof resolveBuildAllStepCacheState>;
export function writeBuildAllStepCacheStamp(
step: BuildCacheStep,
cacheState: BuildAllCacheState,
params: Pick<BuildAllCacheParams, "rootDir" | "fs" | "env"> = {},
) {
if (
!step.cache ||
!cacheState.cacheable ||
!cacheState.signature ||
!cacheState.stampPath ||
@@ -802,7 +833,11 @@ export function writeBuildAllStepCacheStamp(step, cacheState, params = {}) {
);
}
export function resolveBuildAllStepCacheStampState(step, cacheState, params = {}) {
export function resolveBuildAllStepCacheStampState(
step: BuildCacheStep,
cacheState: BuildAllCacheState,
params: Pick<BuildAllCacheParams, "rootDir" | "fs"> = {},
) {
if (!cacheState.cacheable || !cacheState.signature || !step.cache) {
return cacheState;
}
@@ -816,7 +851,10 @@ export function resolveBuildAllStepCacheStampState(step, cacheState, params = {}
};
}
export function restoreBuildAllStepCacheOutputs(cacheState, params = {}) {
export function restoreBuildAllStepCacheOutputs(
cacheState: BuildAllCacheState,
params: Pick<BuildAllCacheParams, "rootDir" | "fs"> = {},
) {
if (!cacheState.restorable || !cacheState.outputRoot || !cacheState.stampedOutputs?.length) {
return false;
}
@@ -832,7 +870,11 @@ export function restoreBuildAllStepCacheOutputs(cacheState, params = {}) {
return true;
}
export function finalizeBuildAllStepCache(step, cacheState, params = {}) {
export function finalizeBuildAllStepCache(
step: BuildCacheStep,
cacheState: BuildAllCacheState,
params: BuildAllCacheParams & { reusedCache?: boolean } = {},
) {
if (params.reusedCache && step.cache?.runOnHit?.finalize !== "refresh") {
return restoreBuildAllStepCacheOutputs(cacheState, params);
}
@@ -846,7 +888,7 @@ export function finalizeBuildAllStepCache(step, cacheState, params = {}) {
return true;
}
export function formatBuildAllDuration(durationMs) {
export function formatBuildAllDuration(durationMs: number) {
const clampedMs = Math.max(0, durationMs);
const roundedMs =
clampedMs < 1000
@@ -859,7 +901,7 @@ export function formatBuildAllDuration(durationMs) {
});
}
export function formatBuildAllTimingSummary(timings) {
export function formatBuildAllTimingSummary(timings: BuildAllTiming[]) {
if (timings.length === 0) {
return "[build-all] phase timings: no phases ran";
}
@@ -894,7 +936,7 @@ if (isMainModule()) {
console.log(buildAllUsage());
} else {
const buildEnv = resolveBuildAllEnvironment();
const timings = [];
const timings: BuildAllTiming[] = [];
let exitCode = 0;
for (const step of resolveBuildAllSteps(args.profile)) {
const startedAt = performance.now();
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env node
/**
* Creates the esbuild plugin that neutralizes Pierre diffs' browser side-effect import.
*/
export function createPierreDiffsSideEffectImportPlugin(): {
name: string;
setup(buildContext: unknown): void;
};
@@ -3,8 +3,8 @@
// Builds browser runtime bundles for the diffs viewer assets.
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { writeGeneratedTextAsset } from "./lib/generated-text-asset.mjs";
import { build, type Plugin } from "esbuild";
import { writeGeneratedTextAsset } from "./lib/generated-text-asset.mts";
const modulePath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(modulePath), "..");
@@ -25,13 +25,17 @@ const targets = {
},
};
function toPosixPath(value) {
return String(value ?? "").replaceAll("\\", "/");
function toPosixPath(value: string) {
return value.replaceAll("\\", "/");
}
/**
* Creates the esbuild plugin that neutralizes Pierre diffs' browser side-effect import.
*/
export function createPierreDiffsSideEffectImportPlugin(): {
name: string;
setup(buildContext: unknown): void;
};
export function createPierreDiffsSideEffectImportPlugin() {
return {
name: "openclaw-diffs-pierre-side-effect-imports",
@@ -58,21 +62,22 @@ export function createPierreDiffsSideEffectImportPlugin() {
}),
);
},
};
} satisfies Plugin;
}
/**
* Builds one configured diffs viewer runtime target.
*/
async function buildDiffsViewerRuntime(targetName) {
const target = targets[targetName];
async function buildDiffsViewerRuntime(targetName: string | undefined) {
const target = Object.entries(targets).find(([name]) => name === targetName)?.[1];
if (!target) {
throw new Error(
`Usage: node scripts/build-diffs-viewer-runtime.mjs ${Object.keys(targets).join("|")}`,
`Usage: node --import tsx scripts/build-diffs-viewer-runtime.mts ${Object.keys(targets).join("|")}`,
);
}
const outputPath = path.join(repoRoot, target.output);
const shikiAlias = "shikiAlias" in target ? target.shikiAlias : undefined;
const result = await build({
entryPoints: [path.join(repoRoot, target.entry)],
bundle: true,
@@ -89,16 +94,16 @@ async function buildDiffsViewerRuntime(targetName) {
write: false,
plugins: [
createPierreDiffsSideEffectImportPlugin(),
...(target.shikiAlias
...(shikiAlias
? [
{
name: "openclaw-diffs-curated-shiki",
setup(buildContext) {
buildContext.onResolve({ filter: /^shiki$/ }, () => ({
path: path.join(repoRoot, target.shikiAlias),
path: path.join(repoRoot, shikiAlias),
}));
},
},
} satisfies Plugin,
]
: []),
],
-20
View File
@@ -1,20 +0,0 @@
type DiscordActivitySdkBuild = (options: {
absWorkingDir: string;
bundle: boolean;
entryPoints: string[];
format: string;
legalComments: string;
minify: boolean;
outfile: string;
platform: string;
target: string;
write: false;
}) => Promise<{
outputFiles?: Array<{ text: string }>;
}>;
/** Builds the browser SDK bundle and returns whether the generated asset changed. */
export function buildDiscordActivitySdk(params?: {
build?: DiscordActivitySdkBuild;
outputPath?: string;
}): Promise<boolean>;
@@ -3,7 +3,14 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { writeGeneratedTextAsset } from "./lib/generated-text-asset.mjs";
import { writeGeneratedTextAsset } from "./lib/generated-text-asset.mts";
type DiscordActivitySdkBuildParams = {
build?: (
options: Parameters<typeof build>[0],
) => Promise<{ outputFiles?: Array<{ text: string }> }>;
outputPath?: string;
};
const modulePath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(modulePath), "..");
@@ -11,7 +18,7 @@ const discordDir = path.join(repoRoot, "extensions/discord");
const outputPath = path.join(repoRoot, "extensions/discord/assets/embedded-app-sdk.mjs");
/** Builds the browser SDK bundle without rewriting an identical generated asset. */
export async function buildDiscordActivitySdk(params = {}) {
export async function buildDiscordActivitySdk(params: DiscordActivitySdkBuildParams = {}) {
const buildImpl = params.build ?? build;
const targetPath = params.outputPath ?? outputPath;
const result = await buildImpl({
-1
View File
@@ -1 +0,0 @@
export { BUILD_STAMP_FILE, resolveGitHead, writeBuildStamp } from "./lib/local-build-metadata.mjs";
@@ -3,9 +3,9 @@
// Writes the local build stamp and re-exports build metadata helpers.
import process from "node:process";
import { pathToFileURL } from "node:url";
import { writeBuildStamp } from "./lib/local-build-metadata.mjs";
import { writeBuildStamp } from "./lib/local-build-metadata.mts";
export { BUILD_STAMP_FILE, resolveGitHead, writeBuildStamp } from "./lib/local-build-metadata.mjs";
export { BUILD_STAMP_FILE, resolveGitHead, writeBuildStamp } from "./lib/local-build-metadata.mts";
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
@@ -2,7 +2,7 @@
// Runs bundled asset build hooks for the Canvas A2UI runtime.
import { pathToFileURL } from "node:url";
import { runBundledPluginAssetHooks } from "./bundled-plugin-assets.mjs";
import { runBundledPluginAssetHooks } from "./bundled-plugin-assets.mts";
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await runBundledPluginAssetHooks({ phase: "build", plugins: ["canvas"] });
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env node
/**
* Reads bundled plugin asset hook commands for a build or copy phase.
*/
export function readBundledPluginAssetHooks(options?: Record<string, unknown>): Promise<
{
aliases: string[];
command: string;
packageName: unknown;
phase: unknown;
pluginDir: string;
pluginId: string;
}[]
>;
/**
* Runs bundled plugin asset hook commands for the selected phase/plugins.
*/
export function runBundledPluginAssetHooks(options?: Record<string, unknown>): Promise<void>;
/**
* Lists declared generated source-tree outputs that differ from the committed bytes.
*/
export function listStaleGeneratedPluginAssets(options?: Record<string, unknown>): string[];
/**
* Parses `--phase`, repeated `--plugin`, and `--check` flags for asset hook scripts.
*/
export function parseBundledPluginAssetArgs(argv: unknown): {
check: boolean;
phase: unknown;
plugins: unknown[];
};
@@ -1,24 +1,41 @@
#!/usr/bin/env node
// Discovers and runs bundled plugin package asset hooks.
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { runManagedCommand } from "./lib/managed-child-process.mjs";
// Discovers and runs bundled plugin package asset hooks.
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { runManagedCommand } from "./lib/managed-child-process.mts";
import { assertRealOutputRoot } from "./lib/output-root-guard.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mjs";
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mts";
const rootDir = resolveRepoRoot(import.meta.url);
const VALID_PHASES = new Set(["build", "copy"]);
// Each complete bundled-plugin asset generator gets the same 10-minute build ceiling.
const BUNDLED_PLUGIN_ASSET_HOOK_TIMEOUT_MS = 600_000;
async function readJsonFile(filePath) {
return JSON.parse(await fs.readFile(filePath, "utf8"));
type AssetPhase = "build" | "copy";
type AssetOptions = {
phase?: AssetPhase;
plugins?: string[];
rootDir?: string;
timeoutMs?: number;
};
function isAssetPhase(value: unknown): value is AssetPhase {
return typeof value === "string" && VALID_PHASES.has(value);
}
async function pathExists(filePath) {
async function readJsonFile(filePath: string) {
const value: unknown = JSON.parse(await fs.readFile(filePath, "utf8"));
if (!isRecord(value)) {
throw new Error(`${filePath} must contain a JSON object`);
}
return value;
}
async function pathExists(filePath: string) {
try {
await fs.stat(filePath);
return true;
@@ -27,7 +44,7 @@ async function pathExists(filePath) {
}
}
function packagePluginAliases(packageName) {
function packagePluginAliases(packageName: unknown) {
if (typeof packageName !== "string") {
return [];
}
@@ -42,7 +59,7 @@ function packagePluginAliases(packageName) {
return aliases;
}
async function resolvePluginAliases(pluginDir, packageJson) {
async function resolvePluginAliases(pluginDir: string, packageJson: Record<string, unknown>) {
const aliases = new Set([path.basename(pluginDir), ...packagePluginAliases(packageJson.name)]);
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
if (await pathExists(manifestPath)) {
@@ -54,9 +71,11 @@ async function resolvePluginAliases(pluginDir, packageJson) {
return aliases;
}
function resolveAssetCommand(packageJson, phase) {
const assetScripts = packageJson.openclaw?.assetScripts;
if (!assetScripts || typeof assetScripts !== "object") {
function resolveAssetCommand(packageJson: Record<string, unknown>, phase: AssetPhase) {
const assetScripts = isRecord(packageJson.openclaw)
? packageJson.openclaw.assetScripts
: undefined;
if (!isRecord(assetScripts)) {
return null;
}
const command = assetScripts[phase];
@@ -66,10 +85,10 @@ function resolveAssetCommand(packageJson, phase) {
/**
* Reads bundled plugin asset hook commands for a build or copy phase.
*/
export async function readBundledPluginAssetHooks(options = {}) {
export async function readBundledPluginAssetHooks(options: AssetOptions = {}) {
const repoRoot = options.rootDir ?? rootDir;
const phase = options.phase;
if (!VALID_PHASES.has(phase)) {
if (!isAssetPhase(phase)) {
throw new Error(`Unsupported bundled plugin asset phase: ${String(phase)}`);
}
@@ -110,7 +129,7 @@ export async function readBundledPluginAssetHooks(options = {}) {
packageName: packageJson.name,
phase,
pluginDir,
pluginId: aliases.has(entry.name) ? entry.name : [...aliases][0],
pluginId: aliases.has(entry.name) ? entry.name : ([...aliases][0] ?? entry.name),
});
}
@@ -120,7 +139,7 @@ export async function readBundledPluginAssetHooks(options = {}) {
/**
* Runs bundled plugin asset hook commands for the selected phase/plugins.
*/
export async function runBundledPluginAssetHooks(options = {}) {
export async function runBundledPluginAssetHooks(options: AssetOptions = {}) {
const phase = options.phase;
const timeoutMs = options.timeoutMs ?? BUNDLED_PLUGIN_ASSET_HOOK_TIMEOUT_MS;
const hooks = await readBundledPluginAssetHooks(options);
@@ -168,7 +187,7 @@ export async function runBundledPluginAssetHooks(options = {}) {
* selection skips extension suites for packages-only diffs, so this check is
* the guard that keeps upstream changes from landing stale committed bundles.
*/
export function listStaleGeneratedPluginAssets(options = {}) {
export function listStaleGeneratedPluginAssets(options: Pick<AssetOptions, "rootDir"> = {}) {
const repoRoot = options.rootDir ?? rootDir;
const sources = listGeneratedExtensionAssetSources({ rootDir: repoRoot });
if (sources.length === 0) {
@@ -194,10 +213,10 @@ export function listStaleGeneratedPluginAssets(options = {}) {
/**
* Parses `--phase`, repeated `--plugin`, and `--check` flags for asset hook scripts.
*/
export function parseBundledPluginAssetArgs(argv) {
export function parseBundledPluginAssetArgs(argv: string[]) {
const args = [...argv];
const plugins = [];
let phase = null;
const plugins: string[] = [];
let phase: string | null = null;
let check = false;
while (args.length > 0) {
@@ -228,7 +247,7 @@ export function parseBundledPluginAssetArgs(argv) {
throw new Error(`Unknown bundled plugin asset argument: ${String(arg)}`);
}
if (!VALID_PHASES.has(phase)) {
if (!isAssetPhase(phase)) {
throw new Error(`Expected --phase ${[...VALID_PHASES].join("|")}`);
}
// The stale-asset scan covers every declared buildOutput, so a filtered run
-57
View File
@@ -1,57 +0,0 @@
export type ChangedLane =
| "core"
| "coreTests"
| "ui"
| "extensions"
| "extensionTests"
| "scripts"
| "testRoot"
| "apps"
| "docs"
| "tooling"
| "liveDockerTooling"
| "bundledChannelConfigMetadata"
| "releaseMetadata"
| "all";
export type ChangedLanes = Record<ChangedLane, boolean>;
export type ChangedLaneResult = {
paths: string[];
lanes: ChangedLanes;
extensionImpactFromCore: boolean;
docsOnly: boolean;
reasons: string[];
};
export type DetectChangedLanesOptions = {
packageJsonChangeKind?: "liveDockerTooling" | "tooling" | null;
};
export function createEmptyChangedLanes(): ChangedLanes;
export function isChangedLaneTestPath(changedPath: string): boolean;
export function detectChangedLanes(
changedPaths: string[],
options?: DetectChangedLanesOptions,
): ChangedLaneResult;
export function detectChangedLanesForPaths(params: {
paths: string[];
base: string;
head?: string;
staged?: boolean;
mergeHeadFirstParent?: boolean;
}): ChangedLaneResult;
export function listChangedPathsFromGit(params: {
base: string;
head?: string;
includeWorktree?: boolean;
cwd?: string;
mergeHeadFirstParent?: boolean;
}): string[];
export function listStagedChangedPaths(cwd?: string): string[];
export function hasDeadcodeScannedSource(changedPaths: string[]): boolean;
export function isLiveDockerPackageScriptOnlyChange(before: string, after: string): boolean;
export function isPackageScriptOnlyChange(before: string, after: string): boolean;
export const LIVE_DOCKER_AUTH_SHELL_TARGETS: string[];
export const RELEASE_METADATA_PATHS: Set<string>;
+4 -658
View File
@@ -1,659 +1,5 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
import { runTsxCliShim } from "./lib/tsx-cli-shim.mjs";
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
const RAW_SYNC_CHANGED_LANES_ENV = "OPENCLAW_CHANGED_LANES_RAW_SYNC";
// Source files knip's production scan reads. Any edit to one of these can orphan
// an export -- including an import-only edit that drops a barrel re-export's last
// consumer -- so the scan is selected by path, not by inspecting changed lines.
const DEADCODE_SOURCE_PATH_RE = /^(?:src|extensions|ui|packages)\/.+\.[cm]?[jt]sx?$/u;
/** Returns whether any changed path is production source knip scans. */
export function hasDeadcodeScannedSource(changedPaths) {
return changedPaths.map(normalizeChangedPath).some((p) => DEADCODE_SOURCE_PATH_RE.test(p));
}
const SCRIPTS_TYPECHECK_PATH_RE =
/^(?:scripts\/.*\.(?:[cm]?ts|[cm]?tsx)|tsconfig\.scripts\.json)$/u;
const TEST_ROOT_TYPECHECK_PATH_RE =
/^(?:test\/(?!fixtures\/).*\.(?:[cm]?ts|[cm]?tsx)|test\/tsconfig\/tsconfig\.test\.root\.json)$/u;
/** @internal Shared repository-script contract. */
export const LIVE_DOCKER_AUTH_SHELL_TARGETS = [
"scripts/lib/live-docker-auth.sh",
"scripts/test-live-acp-bind-docker.sh",
"scripts/test-live-cli-backend-docker.sh",
"scripts/test-live-codex-harness-docker.sh",
"scripts/test-live-gateway-models-docker.sh",
"scripts/test-live-models-docker.sh",
"scripts/test-live-subagent-announce-docker.sh",
];
const LIVE_DOCKER_TOOLING_PATHS = new Set([
...LIVE_DOCKER_AUTH_SHELL_TARGETS,
"scripts/test-docker-all.mjs",
"src/gateway/gateway-acp-bind.live.test.ts",
"src/gateway/live-agent-probes.test.ts",
]);
const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u;
const PUBLIC_EXTENSION_CONTRACT_RE =
/^(?:src\/plugin-sdk\/|src\/plugins\/contracts\/|src\/channels\/plugins\/|scripts\/lib\/plugin-sdk-entrypoints\.json$|scripts\/sync-plugin-sdk-exports\.mjs$|scripts\/generate-plugin-sdk-api-baseline\.ts$)/u;
const BUNDLED_CHANNEL_CONFIG_METADATA_PATH_RE =
/^(?:src\/config\/(?:bundled-channel-config-metadata\.generated|zod-schema\.[^/]+)\.ts|src\/channels\/plugins\/config-schema\.ts|src\/plugin-sdk\/(?:bundled-channel-config-schema|channel-config-schema)\.ts|src\/plugins\/(?:bundled-dir|public-surface-loader|public-surface-runtime|sdk-alias)\.ts|scripts\/(?:generate-bundled-channel-config-metadata\.ts|load-channel-config-surface\.ts|lib\/(?:bundled-plugin-source-utils|format-generated-module|generated-output-utils)\.mjs)|extensions\/[^/]+\/(?:openclaw\.plugin\.json|package\.json|(?:config|security-contract)-api\.[cm]?[jt]sx?|src\/config-(?:schema(?:-[^/]+)?|surface|ui-hints)\.[cm]?[jt]sx?))$/u;
/**
* Files whose changes are treated as release metadata only.
* @internal Shared repository-script contract.
*/
export const RELEASE_METADATA_PATHS = new Set([
"CHANGELOG.md",
"apps/android/CHANGELOG.md",
"apps/android/Config/Version.properties",
"apps/android/fastlane/metadata/android/en-US/release_notes.txt",
"apps/android/version.json",
"apps/ios/CHANGELOG.md",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"docs/.generated/config-baseline.counts.json",
"docs/.generated/config-baseline.sha256",
"docs/install/updating.md",
"package.json",
]);
/** @typedef {"core" | "coreTests" | "ui" | "extensions" | "extensionTests" | "scripts" | "testRoot" | "apps" | "docs" | "tooling" | "liveDockerTooling" | "bundledChannelConfigMetadata" | "releaseMetadata" | "all"} ChangedLane */
/**
* @typedef {{
* paths: string[];
* lanes: Record<ChangedLane, boolean>;
* extensionImpactFromCore: boolean;
* docsOnly: boolean;
* reasons: string[];
* }} ChangedLaneResult
*/
/**
* Creates the default changed-lanes result object.
* @internal Directly tested script implementation detail.
*/
export function createEmptyChangedLanes() {
return {
core: false,
coreTests: false,
ui: false,
extensions: false,
extensionTests: false,
scripts: false,
testRoot: false,
apps: false,
docs: false,
tooling: false,
liveDockerTooling: false,
bundledChannelConfigMetadata: false,
releaseMetadata: false,
all: false,
};
}
export function isChangedLaneTestPath(changedPath) {
return getChangedPathFacts(normalizeChangedPath(changedPath)).isChangedLaneTest;
}
/**
* Classifies a list of changed paths into docs, app, extension, core, and tooling lanes.
* @internal Shared repository-script contract.
*/
export function detectChangedLanes(changedPaths, options = {}) {
const paths = [...new Set(changedPaths.map(normalizeChangedPath).filter(Boolean))]
.toSorted((left, right) => left.localeCompare(right))
.filter((changedPath) => changedPath !== "--");
const lanes = createEmptyChangedLanes();
const reasons = [];
let extensionImpactFromCore = false;
let hasNonDocs = false;
const packageJsonIsLiveDockerTooling =
paths.includes("package.json") && options.packageJsonChangeKind === "liveDockerTooling";
const packageJsonIsTooling =
paths.includes("package.json") && options.packageJsonChangeKind === "tooling";
if (paths.length === 0) {
reasons.push("no changed paths");
return { paths, lanes, extensionImpactFromCore: false, docsOnly: false, reasons };
}
if (
!packageJsonIsLiveDockerTooling &&
!packageJsonIsTooling &&
paths.some((changedPath) => RELEASE_METADATA_PATHS.has(changedPath)) &&
paths.every((changedPath) => RELEASE_METADATA_PATHS.has(changedPath))
) {
lanes.releaseMetadata = true;
lanes.docs = paths.some((changedPath) => getChangedPathFacts(changedPath).surface === "docs");
for (const changedPath of paths) {
reasons.push(`${changedPath}: release metadata`);
}
return { paths, lanes, extensionImpactFromCore: false, docsOnly: false, reasons };
}
for (const changedPath of paths) {
const facts = getChangedPathFacts(changedPath);
if (BUNDLED_CHANNEL_CONFIG_METADATA_PATH_RE.test(changedPath)) {
lanes.bundledChannelConfigMetadata = true;
reasons.push(`${changedPath}: bundled channel config metadata input`);
}
if (SCRIPTS_TYPECHECK_PATH_RE.test(changedPath)) {
lanes.scripts = true;
}
if (TEST_ROOT_TYPECHECK_PATH_RE.test(changedPath)) {
lanes.testRoot = true;
}
if (facts.surface === "docs") {
lanes.docs = true;
continue;
}
hasNonDocs = true;
if (changedPath === "package.json" && packageJsonIsLiveDockerTooling) {
lanes.liveDockerTooling = true;
reasons.push(`${changedPath}: live Docker package scripts`);
continue;
}
if (changedPath === "package.json" && packageJsonIsTooling) {
lanes.tooling = true;
reasons.push(`${changedPath}: package scripts`);
continue;
}
if (LIVE_DOCKER_TOOLING_PATHS.has(changedPath)) {
lanes.liveDockerTooling = true;
reasons.push(`${changedPath}: live Docker tooling surface`);
continue;
}
if (facts.surface === "rootGlobal") {
lanes.all = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: root config/package surface`);
continue;
}
if (PUBLIC_EXTENSION_CONTRACT_RE.test(changedPath)) {
lanes.core = true;
lanes.coreTests = true;
lanes.extensions = true;
lanes.extensionTests = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: public core/plugin contract affects extensions`);
continue;
}
if (facts.surface === "extension") {
if (facts.isChangedLaneTest) {
lanes.extensionTests = true;
reasons.push(`${changedPath}: extension test`);
} else {
lanes.extensions = true;
lanes.extensionTests = true;
reasons.push(`${changedPath}: extension production`);
}
continue;
}
if (facts.surface === "source" || facts.surface === "package") {
if (facts.isChangedLaneTest) {
lanes.coreTests = true;
reasons.push(`${changedPath}: core test`);
} else {
lanes.core = true;
lanes.coreTests = true;
reasons.push(`${changedPath}: core production`);
}
continue;
}
if (facts.surface === "ui") {
if (facts.isChangedLaneTest) {
lanes.coreTests = true;
reasons.push(`${changedPath}: UI test`);
} else {
lanes.ui = true;
lanes.coreTests = true;
reasons.push(`${changedPath}: UI production`);
}
continue;
}
if (facts.surface === "app") {
lanes.apps = true;
reasons.push(`${changedPath}: app surface`);
continue;
}
if (facts.surface === "rootTest" || facts.surface === "testFixture") {
lanes.tooling = true;
reasons.push(`${changedPath}: root test/support surface`);
continue;
}
if (facts.surface === "rootTooling") {
lanes.tooling = true;
reasons.push(`${changedPath}: tooling surface`);
continue;
}
if (facts.surface === "legacyRootAsset") {
lanes.tooling = true;
reasons.push(`${changedPath}: legacy root asset cleanup`);
continue;
}
lanes.all = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: unknown surface; fail-safe all lanes`);
}
return {
paths,
lanes,
extensionImpactFromCore,
docsOnly: lanes.docs && !hasNonDocs,
reasons,
};
}
/**
* Classifies changed paths with optional package.json before/after contents.
* @internal Shared repository-script contract.
*/
export function detectChangedLanesForPaths(params) {
const base = params.staged
? params.base
: resolveMergeHeadDiffBase({
base: params.base,
head: params.head ?? "HEAD",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
preferFirstParent: params.mergeHeadFirstParent === true,
});
const packageJsonChangeKind = params.paths.includes("package.json")
? classifyPackageJsonChangeFromGit({
base,
head: params.head,
staged: params.staged,
})
: null;
return detectChangedLanes(params.paths, { packageJsonChangeKind });
}
/**
* Lists changed paths from git for a base/head comparison.
*/
export function listChangedPathsFromGit(params) {
const head = params.head ?? "HEAD";
const cwd = params.cwd ?? process.cwd();
const base = resolveMergeHeadDiffBase({
base: params.base,
head,
cwd,
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
preferFirstParent: params.mergeHeadFirstParent === true,
});
if (!base) {
return [];
}
let rangePaths;
let noMergeBase = false;
try {
// oxlint-disable-next-line typescript/no-base-to-string, typescript/restrict-template-expressions -- resolveMergeHeadDiffBase returns a git ref string when present.
rangePaths = runGitNameOnlyDiff([`${base}...${head}`], cwd);
} catch (error) {
if (!isGitNoMergeBaseError(error)) {
throw error;
}
noMergeBase = true;
// oxlint-disable-next-line typescript/no-base-to-string, typescript/restrict-template-expressions -- resolveMergeHeadDiffBase returns a git ref string when present.
rangePaths = runGitNameOnlyDiff([`${base}..${head}`], cwd);
}
if (params.includeWorktree === false) {
return rangePaths;
}
const worktreePaths = [
...runGitNameOnlyDiff(["--cached", "--diff-filter=ACMRD"], cwd),
...runGitNameOnlyDiff(["--diff-filter=ACMRD"], cwd),
...runGitLsFiles(["--others", "--exclude-standard"], cwd),
];
// Raw Crabbox syncs can have unrelated synthetic refs; prefer the synced
// worktree delta instead of turning that into an accidental whole-repo gate.
if (
noMergeBase &&
process.env[RAW_SYNC_CHANGED_LANES_ENV] === "1" &&
worktreePaths.length > 0 &&
rangePaths.length > IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS
) {
rangePaths = [];
}
return [...new Set([...rangePaths, ...worktreePaths])].toSorted((left, right) =>
left.localeCompare(right),
);
}
function runGitNameOnlyDiff(extraArgs, cwd = process.cwd()) {
const output = execFileSync("git", ["diff", "--name-only", ...extraArgs], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
});
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
function isGitNoMergeBaseError(error) {
const text = [
error?.message,
error?.stderr?.toString?.("utf8"),
Array.isArray(error?.output)
? error.output.map((value) => value?.toString?.("utf8")).join("\n")
: "",
].join("\n");
return text.includes("no merge base");
}
function runGitLsFiles(extraArgs, cwd = process.cwd()) {
const output = execFileSync("git", ["ls-files", ...extraArgs], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
});
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
/**
* Lists staged changed paths for pre-commit checks.
*/
export function listStagedChangedPaths(cwd = process.cwd()) {
const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMRD"], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
});
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
/**
* Classifies package.json script-only changes from git content.
*/
function classifyPackageJsonChangeFromGit(params) {
try {
const { before, after } = readPackageJsonBeforeAfter(params);
if (isLiveDockerPackageScriptOnlyChange(before, after)) {
return "liveDockerTooling";
}
return isPackageScriptOnlyChange(before, after) ? "tooling" : null;
} catch {
return null;
}
}
/**
* Checks whether package scripts changed only live Docker script entries.
* @internal Directly tested script implementation detail.
*/
export function isLiveDockerPackageScriptOnlyChange(before, after) {
const beforePackage = JSON.parse(before);
const afterPackage = JSON.parse(after);
const beforeAllowed = extractLiveDockerPackageScripts(beforePackage);
const afterAllowed = extractLiveDockerPackageScripts(afterPackage);
const beforeStripped = stripLiveDockerPackageScripts(beforePackage);
const afterStripped = stripLiveDockerPackageScripts(afterPackage);
return (
stableJson(beforeStripped) === stableJson(afterStripped) &&
stableJson(beforeAllowed) !== stableJson(afterAllowed)
);
}
/**
* Checks whether package.json changes are limited to scripts.
* @internal Directly tested script implementation detail.
*/
export function isPackageScriptOnlyChange(before, after) {
const beforePackage = JSON.parse(before);
const afterPackage = JSON.parse(after);
const beforeScripts = extractPackageScripts(beforePackage);
const afterScripts = extractPackageScripts(afterPackage);
const beforeStripped = stripPackageScripts(beforePackage);
const afterStripped = stripPackageScripts(afterPackage);
return (
stableJson(beforeStripped) === stableJson(afterStripped) &&
stableJson(beforeScripts) !== stableJson(afterScripts)
);
}
function readPackageJsonBeforeAfter(params) {
const before = readGitText(params.staged ? "HEAD" : params.base, "package.json");
if (params.staged) {
return { before, after: readGitText("INDEX", "package.json") };
}
let after = readGitText(params.head ?? "HEAD", "package.json");
if (params.includeWorktree !== false && existsSync("package.json")) {
const worktree = readGitText("WORKTREE", "package.json");
if (worktree !== after) {
after = worktree;
}
}
return { before, after };
}
function readGitText(ref, filePath) {
if (ref === "WORKTREE") {
return readFileSync(filePath, "utf8");
}
const spec = ref === "INDEX" ? `:${filePath}` : `${ref}:${filePath}`;
return execFileSync("git", ["show", spec], {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
}
function extractLiveDockerPackageScripts(packageJson) {
const scripts = packageJson?.scripts;
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
return {};
}
return Object.fromEntries(
Object.entries(scripts).filter(([name]) => LIVE_DOCKER_PACKAGE_SCRIPT_RE.test(name)),
);
}
function stripLiveDockerPackageScripts(packageJson) {
const clone = structuredClone(packageJson);
const scripts = clone.scripts;
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
return clone;
}
for (const name of Object.keys(scripts)) {
if (LIVE_DOCKER_PACKAGE_SCRIPT_RE.test(name)) {
delete scripts[name];
}
}
return clone;
}
function extractPackageScripts(packageJson) {
const scripts = packageJson?.scripts;
return scripts && typeof scripts === "object" && !Array.isArray(scripts) ? scripts : {};
}
function stripPackageScripts(packageJson) {
const clone = structuredClone(packageJson);
delete clone.scripts;
return clone;
}
function stableJson(value) {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(",")}]`;
}
if (value && typeof value === "object") {
return `{${Object.keys(value)
.toSorted((left, right) => left.localeCompare(right))
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
/**
* Writes changed-lane booleans to the GitHub Actions output file.
*/
function writeChangedLaneGitHubOutput(result, outputPath = process.env.GITHUB_OUTPUT) {
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
}
for (const [lane, enabled] of Object.entries(result.lanes)) {
appendFileSync(outputPath, `run_${toSnakeCase(lane)}=${String(enabled)}\n`, "utf8");
}
appendFileSync(outputPath, `docs_only=${result.docsOnly}\n`, "utf8");
appendFileSync(
outputPath,
`extension_impact_from_core=${result.extensionImpactFromCore}\n`,
"utf8",
);
}
function toSnakeCase(value) {
return value.replace(/[A-Z]/gu, (match) => `_${match.toLowerCase()}`);
}
function parseArgs(argv) {
const separatorIndex = argv.indexOf("--");
const flagArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
const explicitPaths = separatorIndex === -1 ? [] : argv.slice(separatorIndex + 1);
const args = {
base: "origin/main",
head: "HEAD",
staged: false,
mergeHeadFirstParent: false,
json: false,
githubOutput: false,
help: false,
paths: [],
};
const parsed = parseFlagArgs(
flagArgv,
args,
[
stringFlag("--base", "base"),
stringFlag("--head", "head"),
booleanFlag("--staged", "staged"),
booleanFlag("--merge-head-first-parent", "mergeHeadFirstParent"),
booleanFlag("--json", "json"),
booleanFlag("--github-output", "githubOutput"),
booleanFlag("--help", "help"),
booleanFlag("-h", "help"),
],
{
onUnhandledArg(arg, target) {
if (arg.startsWith("-")) {
throw new Error(`Unknown option: ${arg}`);
}
target.paths.push(arg);
return "handled";
},
},
);
parsed.paths.push(...explicitPaths);
return parsed;
}
function printUsage() {
console.log(
[
"Usage: node scripts/changed-lanes.mjs [options] [-- <paths...>]",
"",
"Options:",
" --base <ref> Base ref for changed paths (default: origin/main)",
" --head <ref> Head ref for changed paths (default: HEAD)",
" --staged Inspect staged changes",
" --json Print JSON result",
" --github-output Append GitHub output variables",
" -h, --help Show this help",
].join("\n"),
);
}
function isDirectRun() {
return isDirectRunUrl(process.argv[1], import.meta.url);
}
function printHuman(result) {
const enabled = Object.entries(result.lanes)
.filter(([, value]) => value)
.map(([lane]) => lane);
console.log(`lanes: ${enabled.length > 0 ? enabled.join(", ") : "none"}`);
if (result.docsOnly) {
console.log("docs-only: true");
}
if (result.extensionImpactFromCore) {
console.log("extension-impact-from-core: true");
}
if (result.paths.length > 0) {
console.log("paths:");
for (const changedPath of result.paths) {
console.log(`- ${changedPath}`);
}
}
if (result.reasons.length > 0) {
console.log("reasons:");
for (const reason of result.reasons) {
console.log(`- ${reason}`);
}
}
}
if (isDirectRun()) {
let args;
try {
args = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
if (args.help) {
printUsage();
process.exit(0);
}
const paths =
args.paths.length > 0
? args.paths
: args.staged
? listStagedChangedPaths()
: listChangedPathsFromGit({
base: args.base,
head: args.head,
mergeHeadFirstParent: args.mergeHeadFirstParent,
});
const result = detectChangedLanesForPaths({
paths,
base: args.base,
head: args.head,
staged: args.staged,
mergeHeadFirstParent: args.mergeHeadFirstParent,
});
if (args.githubOutput) {
writeChangedLaneGitHubOutput(result);
}
if (args.json) {
console.log(JSON.stringify(result, null, 2));
} else if (!args.githubOutput) {
printHuman(result);
}
}
await runTsxCliShim(import.meta.url, {
implementation: "./changed-lanes.mts",
});
+710
View File
@@ -0,0 +1,710 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts";
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
const RAW_SYNC_CHANGED_LANES_ENV = "OPENCLAW_CHANGED_LANES_RAW_SYNC";
// The CLI is invoked from temporary Git repositories, outside workspace package resolution.
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
// Source files knip's production scan reads. Any edit to one of these can orphan
// an export -- including an import-only edit that drops a barrel re-export's last
// consumer -- so the scan is selected by path, not by inspecting changed lines.
const DEADCODE_SOURCE_PATH_RE = /^(?:src|extensions|ui|packages)\/.+\.[cm]?[jt]sx?$/u;
/** Returns whether any changed path is production source knip scans. */
export function hasDeadcodeScannedSource(changedPaths: string[]): boolean {
return changedPaths.map(normalizeChangedPath).some((p) => DEADCODE_SOURCE_PATH_RE.test(p));
}
const SCRIPTS_TYPECHECK_PATH_RE =
/^(?:scripts\/.*\.(?:[cm]?ts|[cm]?tsx)|tsconfig\.scripts\.json)$/u;
const TEST_ROOT_TYPECHECK_PATH_RE =
/^(?:test\/(?!fixtures\/).*\.(?:[cm]?ts|[cm]?tsx)|test\/tsconfig\/tsconfig\.test\.root\.json)$/u;
/** @internal Shared repository-script contract. */
export const LIVE_DOCKER_AUTH_SHELL_TARGETS = [
"scripts/lib/live-docker-auth.sh",
"scripts/test-live-acp-bind-docker.sh",
"scripts/test-live-cli-backend-docker.sh",
"scripts/test-live-codex-harness-docker.sh",
"scripts/test-live-gateway-models-docker.sh",
"scripts/test-live-models-docker.sh",
"scripts/test-live-subagent-announce-docker.sh",
];
const LIVE_DOCKER_TOOLING_PATHS = new Set([
...LIVE_DOCKER_AUTH_SHELL_TARGETS,
"scripts/test-docker-all.mjs",
"scripts/test-docker-all.mts",
"src/gateway/gateway-acp-bind.live.test.ts",
"src/gateway/live-agent-probes.test.ts",
]);
const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u;
const PUBLIC_EXTENSION_CONTRACT_RE =
/^(?:src\/plugin-sdk\/|src\/plugins\/contracts\/|src\/channels\/plugins\/|scripts\/lib\/plugin-sdk-entrypoints\.json$|scripts\/sync-plugin-sdk-exports\.mts$|scripts\/generate-plugin-sdk-api-baseline\.ts$)/u;
const BUNDLED_CHANNEL_CONFIG_METADATA_PATH_RE =
/^(?:src\/config\/(?:bundled-channel-config-metadata\.generated|zod-schema\.[^/]+)\.ts|src\/channels\/plugins\/config-schema\.ts|src\/plugin-sdk\/(?:bundled-channel-config-schema|channel-config-schema)\.ts|src\/plugins\/(?:bundled-dir|public-surface-loader|public-surface-runtime|sdk-alias)\.ts|scripts\/(?:generate-bundled-channel-config-metadata\.ts|load-channel-config-surface\.ts|lib\/(?:bundled-plugin-source-utils|format-generated-module|generated-output-utils)\.mts)|extensions\/[^/]+\/(?:openclaw\.plugin\.json|package\.json|(?:config|security-contract)-api\.[cm]?[jt]sx?|src\/config-(?:schema(?:-[^/]+)?|surface|ui-hints)\.[cm]?[jt]sx?))$/u;
/**
* Files whose changes are treated as release metadata only.
* @internal Shared repository-script contract.
*/
export const RELEASE_METADATA_PATHS = new Set([
"CHANGELOG.md",
"apps/android/CHANGELOG.md",
"apps/android/Config/Version.properties",
"apps/android/fastlane/metadata/android/en-US/release_notes.txt",
"apps/android/version.json",
"apps/ios/CHANGELOG.md",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"docs/.generated/config-baseline.counts.json",
"docs/.generated/config-baseline.sha256",
"docs/install/updating.md",
"package.json",
]);
type ChangedLanes = ReturnType<typeof createEmptyChangedLanes>;
export type ChangedLaneResult = {
paths: string[];
lanes: ChangedLanes;
extensionImpactFromCore: boolean;
docsOnly: boolean;
reasons: string[];
};
type DetectChangedLanesOptions = {
packageJsonChangeKind?: "liveDockerTooling" | "tooling" | null;
};
type PackageJsonGitParams = {
base: string;
head?: string;
staged?: boolean;
includeWorktree?: boolean;
};
/**
* Creates the default changed-lanes result object.
* @internal Directly tested script implementation detail.
*/
export function createEmptyChangedLanes() {
return {
core: false,
coreTests: false,
ui: false,
extensions: false,
extensionTests: false,
scripts: false,
testRoot: false,
apps: false,
docs: false,
tooling: false,
liveDockerTooling: false,
bundledChannelConfigMetadata: false,
releaseMetadata: false,
all: false,
};
}
export function isChangedLaneTestPath(changedPath: string) {
return getChangedPathFacts(normalizeChangedPath(changedPath)).isChangedLaneTest;
}
/**
* Classifies a list of changed paths into docs, app, extension, core, and tooling lanes.
* @internal Shared repository-script contract.
*/
export function detectChangedLanes(
changedPaths: string[],
options: DetectChangedLanesOptions = {},
): ChangedLaneResult {
const paths = [...new Set(changedPaths.map(normalizeChangedPath).filter(Boolean))]
.toSorted((left, right) => left.localeCompare(right))
.filter((changedPath) => changedPath !== "--");
const lanes = createEmptyChangedLanes();
const reasons = [];
let extensionImpactFromCore = false;
let hasNonDocs = false;
const packageJsonIsLiveDockerTooling =
paths.includes("package.json") && options.packageJsonChangeKind === "liveDockerTooling";
const packageJsonIsTooling =
paths.includes("package.json") && options.packageJsonChangeKind === "tooling";
if (paths.length === 0) {
reasons.push("no changed paths");
return { paths, lanes, extensionImpactFromCore: false, docsOnly: false, reasons };
}
if (
!packageJsonIsLiveDockerTooling &&
!packageJsonIsTooling &&
paths.some((changedPath) => RELEASE_METADATA_PATHS.has(changedPath)) &&
paths.every((changedPath) => RELEASE_METADATA_PATHS.has(changedPath))
) {
lanes.releaseMetadata = true;
lanes.docs = paths.some((changedPath) => getChangedPathFacts(changedPath).surface === "docs");
for (const changedPath of paths) {
reasons.push(`${changedPath}: release metadata`);
}
return { paths, lanes, extensionImpactFromCore: false, docsOnly: false, reasons };
}
for (const changedPath of paths) {
const facts = getChangedPathFacts(changedPath);
if (BUNDLED_CHANNEL_CONFIG_METADATA_PATH_RE.test(changedPath)) {
lanes.bundledChannelConfigMetadata = true;
reasons.push(`${changedPath}: bundled channel config metadata input`);
}
if (SCRIPTS_TYPECHECK_PATH_RE.test(changedPath)) {
lanes.scripts = true;
}
if (TEST_ROOT_TYPECHECK_PATH_RE.test(changedPath)) {
lanes.testRoot = true;
}
if (facts.surface === "docs") {
lanes.docs = true;
continue;
}
hasNonDocs = true;
if (changedPath === "package.json" && packageJsonIsLiveDockerTooling) {
lanes.liveDockerTooling = true;
reasons.push(`${changedPath}: live Docker package scripts`);
continue;
}
if (changedPath === "package.json" && packageJsonIsTooling) {
lanes.tooling = true;
reasons.push(`${changedPath}: package scripts`);
continue;
}
if (LIVE_DOCKER_TOOLING_PATHS.has(changedPath)) {
lanes.liveDockerTooling = true;
reasons.push(`${changedPath}: live Docker tooling surface`);
continue;
}
if (facts.surface === "rootGlobal") {
lanes.all = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: root config/package surface`);
continue;
}
if (PUBLIC_EXTENSION_CONTRACT_RE.test(changedPath)) {
lanes.core = true;
lanes.coreTests = true;
lanes.extensions = true;
lanes.extensionTests = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: public core/plugin contract affects extensions`);
continue;
}
if (facts.surface === "extension") {
if (facts.isChangedLaneTest) {
lanes.extensionTests = true;
reasons.push(`${changedPath}: extension test`);
} else {
lanes.extensions = true;
lanes.extensionTests = true;
reasons.push(`${changedPath}: extension production`);
}
continue;
}
if (facts.surface === "source" || facts.surface === "package") {
if (facts.isChangedLaneTest) {
lanes.coreTests = true;
reasons.push(`${changedPath}: core test`);
} else {
lanes.core = true;
lanes.coreTests = true;
reasons.push(`${changedPath}: core production`);
}
continue;
}
if (facts.surface === "ui") {
if (facts.isChangedLaneTest) {
lanes.coreTests = true;
reasons.push(`${changedPath}: UI test`);
} else {
lanes.ui = true;
lanes.coreTests = true;
reasons.push(`${changedPath}: UI production`);
}
continue;
}
if (facts.surface === "app") {
lanes.apps = true;
reasons.push(`${changedPath}: app surface`);
continue;
}
if (facts.surface === "rootTest" || facts.surface === "testFixture") {
lanes.tooling = true;
reasons.push(`${changedPath}: root test/support surface`);
continue;
}
if (facts.surface === "rootTooling") {
lanes.tooling = true;
reasons.push(`${changedPath}: tooling surface`);
continue;
}
if (facts.surface === "legacyRootAsset") {
lanes.tooling = true;
reasons.push(`${changedPath}: legacy root asset cleanup`);
continue;
}
lanes.all = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: unknown surface; fail-safe all lanes`);
}
return {
paths,
lanes,
extensionImpactFromCore,
docsOnly: lanes.docs && !hasNonDocs,
reasons,
};
}
/**
* Classifies changed paths with optional package.json before/after contents.
* @internal Shared repository-script contract.
*/
export function detectChangedLanesForPaths(params: {
paths: string[];
base: string;
head?: string;
staged?: boolean;
mergeHeadFirstParent?: boolean;
}): ChangedLaneResult {
const resolvedBase = params.staged
? params.base
: resolveMergeHeadDiffBase({
base: params.base,
head: params.head ?? "HEAD",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
preferFirstParent: params.mergeHeadFirstParent === true,
});
const base = typeof resolvedBase === "string" ? resolvedBase : "";
const packageJsonChangeKind = params.paths.includes("package.json")
? classifyPackageJsonChangeFromGit({
base,
head: params.head,
staged: params.staged,
})
: null;
return detectChangedLanes(params.paths, { packageJsonChangeKind });
}
/**
* Lists changed paths from git for a base/head comparison.
*/
export function listChangedPathsFromGit(params: {
base: string;
head?: string;
includeWorktree?: boolean;
cwd?: string;
mergeHeadFirstParent?: boolean;
}): string[] {
const head = params.head ?? "HEAD";
const cwd = params.cwd ?? process.cwd();
const resolvedBase = resolveMergeHeadDiffBase({
base: params.base,
head,
cwd,
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
preferFirstParent: params.mergeHeadFirstParent === true,
});
const base = typeof resolvedBase === "string" ? resolvedBase : "";
if (!base) {
return [];
}
let rangePaths: string[];
let noMergeBase = false;
try {
rangePaths = runGitNameOnlyDiff([`${base}...${head}`], cwd);
} catch (error) {
if (!isGitNoMergeBaseError(error)) {
throw error;
}
noMergeBase = true;
rangePaths = runGitNameOnlyDiff([`${base}..${head}`], cwd);
}
if (params.includeWorktree === false) {
return rangePaths;
}
const worktreePaths = [
...runGitNameOnlyDiff(["--cached", "--diff-filter=ACMRD"], cwd),
...runGitNameOnlyDiff(["--diff-filter=ACMRD"], cwd),
...runGitLsFiles(["--others", "--exclude-standard"], cwd),
];
// Raw Crabbox syncs can have unrelated synthetic refs; prefer the synced
// worktree delta instead of turning that into an accidental whole-repo gate.
if (
noMergeBase &&
process.env[RAW_SYNC_CHANGED_LANES_ENV] === "1" &&
worktreePaths.length > 0 &&
rangePaths.length > IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS
) {
rangePaths = [];
}
return [...new Set([...rangePaths, ...worktreePaths])].toSorted((left, right) =>
left.localeCompare(right),
);
}
function runGitNameOnlyDiff(extraArgs: string[], cwd = process.cwd()): string[] {
const output = execFileSync("git", ["diff", "--name-only", ...extraArgs], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
});
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
function gitOutputText(value: unknown) {
return typeof value === "string" || Buffer.isBuffer(value) ? value.toString() : "";
}
function isGitNoMergeBaseError(error: unknown) {
const errorRecord = isRecord(error) ? error : null;
const output = Array.isArray(errorRecord?.output)
? errorRecord.output.map(gitOutputText).join("\n")
: "";
const text = [
error instanceof Error ? error.message : "",
gitOutputText(errorRecord?.stderr),
output,
].join("\n");
return text.includes("no merge base");
}
function runGitLsFiles(extraArgs: string[], cwd = process.cwd()): string[] {
const output = execFileSync("git", ["ls-files", ...extraArgs], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
});
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
/**
* Lists staged changed paths for pre-commit checks.
*/
export function listStagedChangedPaths(cwd = process.cwd()) {
const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMRD"], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
});
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
/**
* Classifies package.json script-only changes from git content.
*/
function classifyPackageJsonChangeFromGit(params: PackageJsonGitParams) {
try {
const { before, after } = readPackageJsonBeforeAfter(params);
if (isLiveDockerPackageScriptOnlyChange(before, after)) {
return "liveDockerTooling";
}
return isPackageScriptOnlyChange(before, after) ? "tooling" : null;
} catch {
return null;
}
}
/**
* Checks whether package scripts changed only live Docker script entries.
* @internal Directly tested script implementation detail.
*/
export function isLiveDockerPackageScriptOnlyChange(before: string, after: string): boolean {
const beforePackage = parsePackageJson(before);
const afterPackage = parsePackageJson(after);
if (!beforePackage || !afterPackage) {
return false;
}
const beforeAllowed = extractLiveDockerPackageScripts(beforePackage);
const afterAllowed = extractLiveDockerPackageScripts(afterPackage);
const beforeStripped = stripLiveDockerPackageScripts(beforePackage);
const afterStripped = stripLiveDockerPackageScripts(afterPackage);
return (
stableJson(beforeStripped) === stableJson(afterStripped) &&
stableJson(beforeAllowed) !== stableJson(afterAllowed)
);
}
/**
* Checks whether package.json changes are limited to scripts.
* @internal Directly tested script implementation detail.
*/
export function isPackageScriptOnlyChange(before: string, after: string): boolean {
const beforePackage = parsePackageJson(before);
const afterPackage = parsePackageJson(after);
if (!beforePackage || !afterPackage) {
return false;
}
const beforeScripts = extractPackageScripts(beforePackage);
const afterScripts = extractPackageScripts(afterPackage);
const beforeStripped = stripPackageScripts(beforePackage);
const afterStripped = stripPackageScripts(afterPackage);
return (
stableJson(beforeStripped) === stableJson(afterStripped) &&
stableJson(beforeScripts) !== stableJson(afterScripts)
);
}
function parsePackageJson(value: string) {
const parsed: unknown = JSON.parse(value);
return isRecord(parsed) ? parsed : null;
}
function readPackageJsonBeforeAfter(params: PackageJsonGitParams) {
const before = readGitText(params.staged ? "HEAD" : params.base, "package.json");
if (params.staged) {
return { before, after: readGitText("INDEX", "package.json") };
}
let after = readGitText(params.head ?? "HEAD", "package.json");
if (params.includeWorktree !== false && existsSync("package.json")) {
const worktree = readGitText("WORKTREE", "package.json");
if (worktree !== after) {
after = worktree;
}
}
return { before, after };
}
function readGitText(ref: string, filePath: string) {
if (ref === "WORKTREE") {
return readFileSync(filePath, "utf8");
}
const spec = ref === "INDEX" ? `:${filePath}` : `${ref}:${filePath}`;
return execFileSync("git", ["show", spec], {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
}
function extractLiveDockerPackageScripts(packageJson: Record<string, unknown>) {
const scripts = packageJson.scripts;
if (!isRecord(scripts)) {
return {};
}
return Object.fromEntries(
Object.entries(scripts).filter(([name]) => LIVE_DOCKER_PACKAGE_SCRIPT_RE.test(name)),
);
}
function stripLiveDockerPackageScripts(packageJson: Record<string, unknown>) {
const clone = structuredClone(packageJson);
const scripts = clone.scripts;
if (!isRecord(scripts)) {
return clone;
}
for (const name of Object.keys(scripts)) {
if (LIVE_DOCKER_PACKAGE_SCRIPT_RE.test(name)) {
delete scripts[name];
}
}
return clone;
}
function extractPackageScripts(packageJson: Record<string, unknown>) {
const scripts = packageJson.scripts;
return isRecord(scripts) ? scripts : {};
}
function stripPackageScripts(packageJson: Record<string, unknown>) {
const clone = structuredClone(packageJson);
delete clone.scripts;
return clone;
}
function stableJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(",")}]`;
}
if (isRecord(value)) {
return `{${Object.keys(value)
.toSorted((left, right) => left.localeCompare(right))
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "undefined";
}
/**
* Writes changed-lane booleans to the GitHub Actions output file.
*/
function writeChangedLaneGitHubOutput(
result: ChangedLaneResult,
outputPath = process.env.GITHUB_OUTPUT,
) {
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
}
for (const [lane, enabled] of Object.entries(result.lanes)) {
appendFileSync(outputPath, `run_${toSnakeCase(lane)}=${String(enabled)}\n`, "utf8");
}
appendFileSync(outputPath, `docs_only=${result.docsOnly}\n`, "utf8");
appendFileSync(
outputPath,
`extension_impact_from_core=${result.extensionImpactFromCore}\n`,
"utf8",
);
}
function toSnakeCase(value: string) {
return value.replace(/[A-Z]/gu, (match) => `_${match.toLowerCase()}`);
}
function parseArgs(argv: string[]) {
const separatorIndex = argv.indexOf("--");
const flagArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
const explicitPaths = separatorIndex === -1 ? [] : argv.slice(separatorIndex + 1);
const args = {
base: "origin/main",
head: "HEAD",
staged: false,
mergeHeadFirstParent: false,
json: false,
githubOutput: false,
help: false,
paths: new Array<string>(),
};
const parsed = parseFlagArgs(
flagArgv,
args,
[
stringFlag("--base", "base"),
stringFlag("--head", "head"),
booleanFlag("--staged", "staged"),
booleanFlag("--merge-head-first-parent", "mergeHeadFirstParent"),
booleanFlag("--json", "json"),
booleanFlag("--github-output", "githubOutput"),
booleanFlag("--help", "help"),
booleanFlag("-h", "help"),
],
{
onUnhandledArg(arg, target) {
if (arg.startsWith("-")) {
throw new Error(`Unknown option: ${arg}`);
}
target.paths.push(arg);
return "handled";
},
},
);
parsed.paths.push(...explicitPaths);
return parsed;
}
function printUsage() {
console.log(
[
"Usage: node scripts/changed-lanes.mjs [options] [-- <paths...>]",
"",
"Options:",
" --base <ref> Base ref for changed paths (default: origin/main)",
" --head <ref> Head ref for changed paths (default: HEAD)",
" --staged Inspect staged changes",
" --json Print JSON result",
" --github-output Append GitHub output variables",
" -h, --help Show this help",
].join("\n"),
);
}
function isDirectRun() {
return isDirectRunUrl(process.argv[1], import.meta.url);
}
function printHuman(result: ChangedLaneResult) {
const enabled = Object.entries(result.lanes)
.filter(([, value]) => value)
.map(([lane]) => lane);
console.log(`lanes: ${enabled.length > 0 ? enabled.join(", ") : "none"}`);
if (result.docsOnly) {
console.log("docs-only: true");
}
if (result.extensionImpactFromCore) {
console.log("extension-impact-from-core: true");
}
if (result.paths.length > 0) {
console.log("paths:");
for (const changedPath of result.paths) {
console.log(`- ${changedPath}`);
}
}
if (result.reasons.length > 0) {
console.log("reasons:");
for (const reason of result.reasons) {
console.log(`- ${reason}`);
}
}
}
if (isDirectRun()) {
let args;
try {
args = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
if (args.help) {
printUsage();
process.exit(0);
}
const paths =
args.paths.length > 0
? args.paths
: args.staged
? listStagedChangedPaths()
: listChangedPathsFromGit({
base: args.base,
head: args.head,
mergeHeadFirstParent: args.mergeHeadFirstParent,
});
const result = detectChangedLanesForPaths({
paths,
base: args.base,
head: args.head,
staged: args.staged,
mergeHeadFirstParent: args.mergeHeadFirstParent,
});
if (args.githubOutput) {
writeChangedLaneGitHubOutput(result);
}
if (args.json) {
console.log(JSON.stringify(result, null, 2));
} else if (!args.githubOutput) {
printHuman(result);
}
}
@@ -1,23 +0,0 @@
export type BuiltPluginControlPlaneModule = {
pluginId: string;
kind: string;
relativePath: string;
};
export type BuiltPluginControlPlaneModuleFailure = BuiltPluginControlPlaneModule & {
error: string;
};
export function listBuiltPluginControlPlaneModules(params?: {
rootDir?: string;
}): BuiltPluginControlPlaneModule[];
export function probeBuiltPluginControlPlaneModules(
modules: BuiltPluginControlPlaneModule[],
params?: { rootDir?: string; timeoutMs?: number },
): BuiltPluginControlPlaneModuleFailure[];
export function verifyBuiltPluginControlPlaneModules(params?: {
rootDir?: string;
timeoutMs?: number;
}): void;
@@ -8,9 +8,29 @@ import { pathToFileURL } from "node:url";
import ts from "typescript";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
// The live-updater fixture copies this script without workspace packages.
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
type BuiltPluginControlPlaneModule = {
pluginId: string;
kind: string;
relativePath: string;
};
type BuiltPluginControlPlaneModuleFailure = BuiltPluginControlPlaneModule & {
error: string;
};
type ProbeParams = {
rootDir?: string;
timeoutMs?: number;
};
const ROOT = resolveRepoRoot(import.meta.url);
const DIRECT_CONTRACT_FILES = ["contract-api.js", "doctor-contract-api.js"];
const LEGACY_SETUP_PROPERTIES = new Map([
const LEGACY_SETUP_PROPERTIES = new Map<string, string>([
["legacyStateMigrations", "channel-legacy-state-migrations"],
["legacySessionSurface", "channel-legacy-session-surface"],
["legacySessionSurfaces", "channel-legacy-session-surface"],
@@ -36,15 +56,15 @@ for (const target of targets) {
process.stdout.write("\n${PROBE_RESULT_MARKER}" + JSON.stringify({ failures }));
`;
function propertyNameText(name) {
return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : null;
function propertyNameText(name: ts.PropertyName) {
return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : "";
}
function listLegacySetupModuleSpecifiers(setupEntryPath) {
function listLegacySetupModuleSpecifiers(setupEntryPath: string) {
const source = fs.readFileSync(setupEntryPath, "utf8");
const sourceFile = ts.createSourceFile(setupEntryPath, source, ts.ScriptTarget.Latest, true);
const specifiers = [];
const visit = (node) => {
const specifiers: Array<{ kind: string; specifier: string }> = [];
const visit = (node: ts.Node): void => {
if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.initializer)) {
const kind = LEGACY_SETUP_PROPERTIES.get(propertyNameText(node.name));
if (kind) {
@@ -68,13 +88,13 @@ function listLegacySetupModuleSpecifiers(setupEntryPath) {
}
/** Lists exact built doctor, contract, and channel legacy migration artifacts. */
export function listBuiltPluginControlPlaneModules(params = {}) {
export function listBuiltPluginControlPlaneModules(params: { rootDir?: string } = {}) {
const rootDir = path.resolve(params.rootDir ?? ROOT);
const extensionsDir = path.join(rootDir, "dist", "extensions");
if (!fs.existsSync(extensionsDir)) {
return [];
}
const modules = new Map();
const modules = new Map<string, BuiltPluginControlPlaneModule>();
for (const entry of fs
.readdirSync(extensionsDir, { withFileTypes: true })
.filter((candidate) => candidate.isDirectory())
@@ -112,7 +132,10 @@ export function listBuiltPluginControlPlaneModules(params = {}) {
}
/** Loads every selected artifact in one timeout-bounded native-require child. */
export function probeBuiltPluginControlPlaneModules(modules, params = {}) {
export function probeBuiltPluginControlPlaneModules(
modules: BuiltPluginControlPlaneModule[],
params: ProbeParams = {},
) {
if (modules.length === 0) {
return [];
}
@@ -135,12 +158,24 @@ export function probeBuiltPluginControlPlaneModules(modules, params = {}) {
`built plugin control-plane native-require probe exited ${String(result.status)} without a result`,
);
}
const payload = JSON.parse(result.stdout.slice(markerIndex + PROBE_RESULT_MARKER.length));
return Array.isArray(payload.failures) ? payload.failures : [];
const payload: unknown = JSON.parse(
result.stdout.slice(markerIndex + PROBE_RESULT_MARKER.length),
);
if (!isRecord(payload) || !Array.isArray(payload.failures)) {
return [];
}
return payload.failures.filter(
(failure): failure is BuiltPluginControlPlaneModuleFailure =>
isRecord(failure) &&
typeof failure.pluginId === "string" &&
typeof failure.kind === "string" &&
typeof failure.relativePath === "string" &&
typeof failure.error === "string",
);
}
/** Fails the build when a generated plugin control-plane module cannot be required natively. */
export function verifyBuiltPluginControlPlaneModules(params = {}) {
export function verifyBuiltPluginControlPlaneModules(params: ProbeParams = {}) {
const modules = listBuiltPluginControlPlaneModules(params);
const failures = probeBuiltPluginControlPlaneModules(modules, params);
if (failures.length > 0) {
-79
View File
@@ -1,79 +0,0 @@
import type { ChangedLaneResult } from "./changed-lanes.mjs";
export type ChangedCheckCommand = {
name: string;
args: string[];
bin?: string;
env?: NodeJS.ProcessEnv;
};
export type ChangedCheckPlan = {
commands: ChangedCheckCommand[];
summary: string;
};
export type ChangedCheckPlanOptions = {
env?: NodeJS.ProcessEnv;
staged?: boolean;
base?: string;
head?: string;
platform?: NodeJS.Platform;
swiftlintAvailable?: boolean;
};
export type TargetedLintOptions = {
fileExists?: (path: string) => boolean;
};
export type TargetedLintCommand = Required<
Pick<ChangedCheckCommand, "name" | "bin" | "args" | "env">
>;
export function createChangedCheckChildEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
export function changedCheckLocalDependenciesReady(cwd?: string): boolean;
export function changedCheckRequiresRemote(result?: ChangedLaneResult): boolean;
export function shouldDelegateChangedCheckToCrabbox(
argv?: string[],
env?: NodeJS.ProcessEnv,
options?: { cwd?: string; result?: ChangedLaneResult; diffRefsReady?: boolean },
): boolean;
export function buildChangedCheckCrabboxArgs(argv?: string[], options?: { cwd?: string }): string[];
export function delegationFailedBeforeRunning(output: string): boolean;
export function shouldRunNpmLockGuard(paths: string[]): boolean;
export function shouldRunPromptSnapshotCheck(paths: string[]): boolean;
export function shouldRunPromptSnapshotOwnerTest(paths: string[]): boolean;
export function shouldRunControlUiI18nVerify(paths: string[]): boolean;
export function shouldRunRuntimeSidecarBaselineCheck(paths: string[]): boolean;
export function shouldRunDoctorContractOwnerTests(paths: string[]): boolean;
export function shouldRunSqliteSessionSchemaBaselineCheck(paths: string[]): boolean;
export function shouldRunPluginSdkApiBaselineCheck(paths: string[]): boolean;
export function shouldRunPluginSdkSurfaceChecks(paths: string[]): boolean;
export function shouldRunDeprecationHygieneChecks(paths: string[]): boolean;
export function shouldRunCanvasA2uiNativeResourceCheck(paths: string[]): boolean;
export function shouldRunAppcastOwnerTest(paths: string[]): boolean;
export function shouldRunTestTempCreationReport(paths: string[]): boolean;
export function createNpmLockGuardCommand(paths: string[]): ChangedCheckCommand | null;
export function createChangedCheckPlan(
result: ChangedLaneResult,
options?: ChangedCheckPlanOptions,
): ChangedCheckPlan;
export function createTargetedCoreLintCommand(
paths: string[],
env?: NodeJS.ProcessEnv,
options?: TargetedLintOptions,
): TargetedLintCommand | null;
export function createTargetedExtensionLintCommand(
paths: string[],
env?: NodeJS.ProcessEnv,
options?: TargetedLintOptions,
): TargetedLintCommand | null;
export function createTargetedScriptLintCommand(
paths: string[],
env?: NodeJS.ProcessEnv,
options?: TargetedLintOptions,
): TargetedLintCommand | null;
export function createPnpmManagedCommand<T extends ChangedCheckCommand>(
command: T,
env?: NodeJS.ProcessEnv,
): T & { bin: string; env: NodeJS.ProcessEnv };
export function cleanupCorepackPnpmShimDir(): void;
+4 -1191
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,20 +0,0 @@
#!/usr/bin/env node
/**
* Reports whether a handle is forbidden in changelog thanks text.
*/
export function isForbiddenChangelogThanksHandle(
handle: unknown,
options?: Record<string, unknown>,
): boolean;
/**
* Reports whether a handle needs a separate human credit.
*/
export function requiresExplicitHumanChangelogThanks(handle: unknown): boolean;
/**
* Finds changelog lines that thank forbidden handles.
*/
export function findForbiddenChangelogThanks(content: unknown): unknown;
/**
* Runs the changelog attribution check.
*/
export function main(argv?: string[]): Promise<void>;
+2 -157
View File
@@ -1,159 +1,4 @@
#!/usr/bin/env node
import { runTsxCliShim } from "./lib/tsx-cli-shim.mjs";
// Rejects changelog thanks entries that credit bots or internal handles.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Exact handles that changelog thanks entries must not credit.
*/
const FORBIDDEN_CHANGELOG_THANKS_HANDLES = [
"codex",
"openclaw",
"steipete",
"clawsweeper",
"openclaw-clawsweeper",
"clawsweeper[bot]",
"openclaw-clawsweeper[bot]",
];
/**
* Handle prefixes that identify forbidden changelog thanks credits.
*/
const FORBIDDEN_CHANGELOG_THANKS_HANDLE_PREFIXES = ["app/"];
/**
* Handle suffixes that identify forbidden changelog thanks credits.
*/
const FORBIDDEN_CHANGELOG_THANKS_HANDLE_SUFFIXES = ["[bot]"];
/**
* Handles that require an explicit human credit instead.
*/
const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLES = new Set([
"clawsweeper",
"openclaw-clawsweeper",
"clawsweeper[bot]",
"openclaw-clawsweeper[bot]",
]);
/**
* Handle prefixes that require explicit human credit instead.
*/
const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_PREFIXES = ["app/"];
/**
* Handle suffixes that require explicit human credit instead.
*/
const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_SUFFIXES = ["[bot]"];
const THANKS_PATTERN = /\bThanks\b/iu;
const THANKED_HANDLE_PATTERN = /@([-_/A-Za-z0-9]+(?:\[bot\])?)/giu;
/**
* Reports whether a handle is forbidden in changelog thanks text.
*/
export function isForbiddenChangelogThanksHandle(handle, options = {}) {
const { strictBotHandle = false } = options;
const normalized = handle.toLowerCase();
if (normalized === "" || normalized === "null") {
// Empty/null input is not a GitHub handle, but the shell query path may pass it through.
return true;
}
if (
FORBIDDEN_CHANGELOG_THANKS_HANDLES.includes(normalized) ||
FORBIDDEN_CHANGELOG_THANKS_HANDLE_PREFIXES.some((prefix) => normalized.startsWith(prefix)) ||
FORBIDDEN_CHANGELOG_THANKS_HANDLE_SUFFIXES.some((suffix) => normalized.endsWith(suffix))
) {
return true;
}
if (strictBotHandle) {
// PR-author checks should not reject a real human whose login merely contains a bot keyword.
return false;
}
return false;
}
/**
* Reports whether a handle needs a separate human credit.
*/
export function requiresExplicitHumanChangelogThanks(handle) {
const normalized = handle.toLowerCase();
if (normalized === "" || normalized === "null") {
return false;
}
return (
CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLES.has(normalized) ||
CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_PREFIXES.some((prefix) =>
normalized.startsWith(prefix),
) ||
CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_SUFFIXES.some((suffix) =>
normalized.endsWith(suffix),
)
);
}
/**
* Finds changelog lines that thank forbidden handles.
*/
export function findForbiddenChangelogThanks(content) {
return content
.split(/\r?\n/u)
.map((text, index) => {
if (!THANKS_PATTERN.test(text)) {
return null;
}
// A single changelog line may thank multiple handles; scan all of them.
for (const match of text.matchAll(THANKED_HANDLE_PATTERN)) {
if (isForbiddenChangelogThanksHandle(match[1])) {
return { line: index + 1, handle: match[1].toLowerCase(), text };
}
}
return null;
})
.filter(Boolean);
}
/**
* Runs the changelog attribution check.
*/
export async function main(argv = process.argv.slice(2)) {
if (argv[0] === "--is-forbidden-handle") {
process.exitCode = isForbiddenChangelogThanksHandle(argv[1] ?? "", {
strictBotHandle: true,
})
? 0
: 1;
return;
}
if (argv[0] === "--requires-explicit-human-thanks") {
process.exitCode = requiresExplicitHumanChangelogThanks(argv[1] ?? "") ? 0 : 1;
return;
}
const changelogPath = argv[0] ?? "CHANGELOG.md";
const absolutePath = path.resolve(process.cwd(), changelogPath);
const content = fs.readFileSync(absolutePath, "utf8");
const violations = findForbiddenChangelogThanks(content);
if (violations.length === 0) {
return;
}
console.error("Forbidden changelog thanks attribution:");
for (const violation of violations) {
const relativePath = path.relative(process.cwd(), absolutePath) || changelogPath;
console.error(`- ${relativePath}:${violation.line} uses Thanks @${violation.handle}`);
}
console.error(
`Use a credited external GitHub username instead of ${FORBIDDEN_CHANGELOG_THANKS_HANDLES.map(
(handle) => `@${handle}`,
).join(", ")}.`,
);
process.exitCode = 1;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch(
/** @param {unknown} error */ (error) => {
console.error(error);
process.exit(1);
},
);
}
await runTsxCliShim(import.meta.url, { implementation: "./check-changelog-attributions.mts" });
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env node
// Rejects changelog thanks entries that credit bots or internal handles.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Exact handles that changelog thanks entries must not credit.
*/
const FORBIDDEN_CHANGELOG_THANKS_HANDLES = [
"codex",
"openclaw",
"steipete",
"clawsweeper",
"openclaw-clawsweeper",
"clawsweeper[bot]",
"openclaw-clawsweeper[bot]",
];
/**
* Handle prefixes that identify forbidden changelog thanks credits.
*/
const FORBIDDEN_CHANGELOG_THANKS_HANDLE_PREFIXES = ["app/"];
/**
* Handle suffixes that identify forbidden changelog thanks credits.
*/
const FORBIDDEN_CHANGELOG_THANKS_HANDLE_SUFFIXES = ["[bot]"];
/**
* Handles that require an explicit human credit instead.
*/
const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLES = new Set([
"clawsweeper",
"openclaw-clawsweeper",
"clawsweeper[bot]",
"openclaw-clawsweeper[bot]",
]);
/**
* Handle prefixes that require explicit human credit instead.
*/
const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_PREFIXES = ["app/"];
/**
* Handle suffixes that require explicit human credit instead.
*/
const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_SUFFIXES = ["[bot]"];
const THANKS_PATTERN = /\bThanks\b/iu;
const THANKED_HANDLE_PATTERN = /@([-_/A-Za-z0-9]+(?:\[bot\])?)/giu;
type ThanksOptions = { strictBotHandle?: boolean };
/**
* Reports whether a handle is forbidden in changelog thanks text.
*/
export function isForbiddenChangelogThanksHandle(handle: string, options: ThanksOptions = {}) {
const { strictBotHandle = false } = options;
const normalized = handle.toLowerCase();
if (normalized === "" || normalized === "null") {
// Empty/null input is not a GitHub handle, but the shell query path may pass it through.
return true;
}
if (
FORBIDDEN_CHANGELOG_THANKS_HANDLES.includes(normalized) ||
FORBIDDEN_CHANGELOG_THANKS_HANDLE_PREFIXES.some((prefix) => normalized.startsWith(prefix)) ||
FORBIDDEN_CHANGELOG_THANKS_HANDLE_SUFFIXES.some((suffix) => normalized.endsWith(suffix))
) {
return true;
}
if (strictBotHandle) {
// PR-author checks should not reject a real human whose login merely contains a bot keyword.
return false;
}
return false;
}
/**
* Reports whether a handle needs a separate human credit.
*/
export function requiresExplicitHumanChangelogThanks(handle: string) {
const normalized = handle.toLowerCase();
if (normalized === "" || normalized === "null") {
return false;
}
return (
CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLES.has(normalized) ||
CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_PREFIXES.some((prefix) =>
normalized.startsWith(prefix),
) ||
CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_SUFFIXES.some((suffix) =>
normalized.endsWith(suffix),
)
);
}
/**
* Finds changelog lines that thank forbidden handles.
*/
export function findForbiddenChangelogThanks(content: string) {
return content
.split(/\r?\n/u)
.map((text, index) => {
if (!THANKS_PATTERN.test(text)) {
return null;
}
// A single changelog line may thank multiple handles; scan all of them.
for (const match of text.matchAll(THANKED_HANDLE_PATTERN)) {
const handle = match[1];
if (handle && isForbiddenChangelogThanksHandle(handle)) {
return { line: index + 1, handle: handle.toLowerCase(), text };
}
}
return null;
})
.filter((violation) => violation !== null);
}
/**
* Runs the changelog attribution check.
*/
export async function main(argv = process.argv.slice(2)) {
if (argv[0] === "--is-forbidden-handle") {
process.exitCode = isForbiddenChangelogThanksHandle(argv[1] ?? "", {
strictBotHandle: true,
})
? 0
: 1;
return;
}
if (argv[0] === "--requires-explicit-human-thanks") {
process.exitCode = requiresExplicitHumanChangelogThanks(argv[1] ?? "") ? 0 : 1;
return;
}
const changelogPath = argv[0] ?? "CHANGELOG.md";
const absolutePath = path.resolve(process.cwd(), changelogPath);
const content = fs.readFileSync(absolutePath, "utf8");
const violations = findForbiddenChangelogThanks(content);
if (violations.length === 0) {
return;
}
console.error("Forbidden changelog thanks attribution:");
for (const violation of violations) {
const relativePath = path.relative(process.cwd(), absolutePath) || changelogPath;
console.error(`- ${relativePath}:${violation.line} uses Thanks @${violation.handle}`);
}
console.error(
`Use a credited external GitHub username instead of ${FORBIDDEN_CHANGELOG_THANKS_HANDLES.map(
(handle) => `@${handle}`,
).join(", ")}.`,
);
process.exitCode = 1;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error: unknown) => {
console.error(error);
process.exit(1);
});
}
@@ -1,31 +0,0 @@
#!/usr/bin/env node
/**
* Finds channel-specific references inside channel-agnostic protected sources.
*/
export function findChannelAgnosticBoundaryViolations(
content: unknown,
fileName?: string,
options?: Record<string, unknown>,
): unknown[];
/**
* Finds reverse dependencies from channel core into plugin/runtime surfaces.
*/
export function findChannelCoreReverseDependencyViolations(
content: unknown,
fileName?: string,
): unknown[];
/**
* Finds user-facing channel names in ACP-owned text sources.
*/
export function findAcpUserFacingChannelNameViolations(
content: unknown,
fileName?: string,
): unknown[];
/**
* Finds raw system mark literals where shared constants should be used.
*/
export function findSystemMarkLiteralViolations(content: unknown, fileName?: string): unknown[];
/**
* Runs all channel-agnostic boundary checks.
*/
export function main(): Promise<void>;
@@ -11,7 +11,7 @@ import {
getPropertyNameText,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
} from "./lib/ts-guard-utils.mts";
const repoRoot = resolveRepoRoot(import.meta.url);
@@ -65,16 +65,30 @@ const channelIds = [
const channelIdSet = new Set(channelIds);
const channelSegmentRe = new RegExp(`(^|[._/-])(?:${channelIds.join("|")})([._/-]|$)`);
const comparisonOperators = new Set([
const comparisonOperators: ReadonlySet<ts.SyntaxKind> = new Set([
ts.SyntaxKind.EqualsEqualsEqualsToken,
ts.SyntaxKind.ExclamationEqualsEqualsToken,
ts.SyntaxKind.EqualsEqualsToken,
ts.SyntaxKind.ExclamationEqualsToken,
]);
const allowedViolations = new Set([]);
const allowedViolations = new Set<string>();
function isChannelsPropertyAccess(node) {
type BoundaryViolation = { line: number; reason: string };
type BoundaryOptions = {
checkModuleSpecifiers?: boolean;
checkConfigPaths?: boolean;
checkChannelComparisons?: boolean;
checkChannelAssignments?: boolean;
moduleSpecifierMatcher?: (specifier: string) => boolean;
};
type ModuleSpecifierVisit = {
kind: string;
node: ts.Node;
specifier: string;
specifierNode: ts.Node;
};
function isChannelsPropertyAccess(node: ts.Node) {
if (ts.isPropertyAccessExpression(node)) {
return node.name.text === "channels";
}
@@ -84,7 +98,7 @@ function isChannelsPropertyAccess(node) {
return false;
}
function readStringLiteral(node) {
function readStringLiteral(node: ts.Node) {
if (ts.isStringLiteral(node)) {
return node.text;
}
@@ -94,12 +108,12 @@ function readStringLiteral(node) {
return null;
}
function isChannelLiteralNode(node) {
function isChannelLiteralNode(node: ts.Node) {
const text = readStringLiteral(node);
return text ? channelIdSet.has(text) : false;
}
function matchesChannelModuleSpecifier(specifier) {
function matchesChannelModuleSpecifier(specifier: string) {
return channelSegmentRe.test(specifier.replaceAll("\\", "/"));
}
@@ -107,7 +121,7 @@ const userFacingChannelNameRe =
/\b(?:discord|telegram|slack|signal|imessage|whatsapp|google\s*chat|irc|line|zalo|matrix|msteams)\b/i;
const systemMarkLiteral = "⚙️";
function isModuleSpecifierStringNode(node) {
function isModuleSpecifierStringNode(node: ts.Node) {
const parent = node.parent;
if (ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent)) {
return true;
@@ -123,9 +137,9 @@ function isModuleSpecifierStringNode(node) {
* Finds channel-specific references inside channel-agnostic protected sources.
*/
export function findChannelAgnosticBoundaryViolations(
content,
content: string,
fileName = "source.ts",
options = {},
options: BoundaryOptions = {},
) {
const checkModuleSpecifiers = options.checkModuleSpecifiers ?? true;
const checkConfigPaths = options.checkConfigPaths ?? true;
@@ -134,13 +148,13 @@ export function findChannelAgnosticBoundaryViolations(
const moduleSpecifierMatcher = options.moduleSpecifierMatcher ?? matchesChannelModuleSpecifier;
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
const moduleViolations = new Map();
const violations: BoundaryViolation[] = [];
const moduleViolations = new Map<ts.Node, BoundaryViolation>();
if (checkModuleSpecifiers) {
visitModuleSpecifiers(
ts,
sourceFile,
({ kind, node, specifier, specifierNode }) => {
({ kind, node, specifier, specifierNode }: ModuleSpecifierVisit) => {
if (moduleSpecifierMatcher(specifier)) {
const verb =
kind === "export"
@@ -158,7 +172,7 @@ export function findChannelAgnosticBoundaryViolations(
);
}
const visit = (node) => {
const visit = (node: ts.Node): void => {
const moduleViolation = moduleViolations.get(node);
if (moduleViolation) {
violations.push(moduleViolation);
@@ -226,7 +240,10 @@ export function findChannelAgnosticBoundaryViolations(
/**
* Finds reverse dependencies from channel core into plugin/runtime surfaces.
*/
export function findChannelCoreReverseDependencyViolations(content, fileName = "source.ts") {
export function findChannelCoreReverseDependencyViolations(
content: string,
fileName = "source.ts",
) {
return findChannelAgnosticBoundaryViolations(content, fileName, {
checkModuleSpecifiers: true,
checkConfigPaths: false,
@@ -239,11 +256,11 @@ export function findChannelCoreReverseDependencyViolations(content, fileName = "
/**
* Finds user-facing channel names in ACP-owned text sources.
*/
export function findAcpUserFacingChannelNameViolations(content, fileName = "source.ts") {
export function findAcpUserFacingChannelNameViolations(content: string, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
const violations: BoundaryViolation[] = [];
const visit = (node) => {
const visit = (node: ts.Node): void => {
const text = readStringLiteral(node);
if (text && userFacingChannelNameRe.test(text) && !isModuleSpecifierStringNode(node)) {
violations.push({
@@ -261,11 +278,11 @@ export function findAcpUserFacingChannelNameViolations(content, fileName = "sour
/**
* Finds raw system mark literals where shared constants should be used.
*/
export function findSystemMarkLiteralViolations(content, fileName = "source.ts") {
export function findSystemMarkLiteralViolations(content: string, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
const violations: BoundaryViolation[] = [];
const visit = (node) => {
const visit = (node: ts.Node): void => {
const text = readStringLiteral(node);
if (text && text.includes(systemMarkLiteral) && !isModuleSpecifierStringNode(node)) {
violations.push({
@@ -284,22 +301,22 @@ const boundaryRuleSets = [
{
id: "acp-core",
sources: acpCoreProtectedSources,
scan: (content, fileName) => findChannelAgnosticBoundaryViolations(content, fileName),
scan: findChannelAgnosticBoundaryViolations,
},
{
id: "channel-core-reverse-deps",
sources: channelCoreProtectedSources,
scan: (content, fileName) => findChannelCoreReverseDependencyViolations(content, fileName),
scan: findChannelCoreReverseDependencyViolations,
},
{
id: "acp-user-facing-text",
sources: acpUserFacingTextSources,
scan: (content, fileName) => findAcpUserFacingChannelNameViolations(content, fileName),
scan: findAcpUserFacingChannelNameViolations,
},
{
id: "system-mark-literal-usage",
sources: systemMarkLiteralGuardSources,
scan: (content, fileName) => findSystemMarkLiteralViolations(content, fileName),
scan: findSystemMarkLiteralViolations,
},
];
@@ -307,7 +324,7 @@ const boundaryRuleSets = [
* Runs all channel-agnostic boundary checks.
*/
export async function main() {
const violations = [];
const violations: string[] = [];
for (const ruleSet of boundaryRuleSets) {
const files = (
await Promise.all(
-15
View File
@@ -1,15 +0,0 @@
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;

Some files were not shown because too many files have changed in this diff Show More