diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb857dca1d..e5cb7d5ac1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -297,13 +297,12 @@ jobs: if: steps.plan.outputs.code == 'true' run: npm run astryx:theme -- --check - - name: Knip (apps/desktop) + - name: Knip (root + all workspaces) if: steps.plan.outputs.code == 'true' - run: npx knip --workspace apps/desktop - - - name: Knip (packages/ui) - if: steps.plan.outputs.code == 'true' - run: npx knip --workspace packages/ui + run: | + node --test scripts/check-knip-workspace-coverage.test.mjs + node scripts/check-knip-workspace-coverage.mjs + npx knip - name: Linux sandbox smoke if: steps.plan.outputs.runtime_sandbox == 'true' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89ec9df3eb..90f52168f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,8 +72,7 @@ npm run lint npm run format:check npm run build npm run typecheck -npx knip --workspace apps/desktop -npx knip --workspace packages/ui +npx knip ``` Architecture is documented in [ARCHITECTURE.md](./ARCHITECTURE.md); evaluation commands and contracts live in [`packages/eval`](./packages/eval). diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index f7c4d3d041..8f0a1f7f4d 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -72,8 +72,7 @@ npm run lint npm run format:check npm run build npm run typecheck -npx knip --workspace apps/desktop -npx knip --workspace packages/ui +npx knip ``` 架构说明见 [ARCHITECTURE.zh-CN.md](./ARCHITECTURE.zh-CN.md);Eval 的命令与 contract 见 [`packages/eval`](./packages/eval)。 diff --git a/knip.json b/knip.json index 8158cfd292..711bd81123 100644 --- a/knip.json +++ b/knip.json @@ -3,6 +3,62 @@ "ignoreExportsUsedInFile": true, "tags": ["-knipignore"], "workspaces": { + ".": { + "entry": [ + "scripts/audit-alignment.mjs", + "scripts/computer-use/direct-runtime-ledger.mjs", + "scripts/computer-use/e2e-fixture.mjs", + "scripts/computer-use/prepare.mjs", + "scripts/computer-use/process-restart-harness.mjs", + "scripts/computer-use/process-restart-launcher.mjs", + "scripts/computer-use/provider-matrix.mjs", + "scripts/computer-use/real-ax-harness.mjs", + "scripts/computer-use/real-ax-launcher.mjs", + "scripts/computer-use/real-model-fixture.mjs", + "scripts/computer-use/real-model.mjs", + "scripts/generate-cu-status-icons.mjs", + "scripts/issue-pr-lifecycle.mjs", + "scripts/perf/*.{js,mjs}", + "scripts/pip-interaction-smoke.mjs", + "scripts/pr-effort.mjs", + "scripts/prepare-deepseek-harness-toolchain.mjs" + ], + "project": ["scripts/**/*.{mjs,js,cjs}"], + "ignoreBinaries": ["gpg", "swift"], + "ignoreDependencies": ["app-builder-lib", "builder-util", "patch-package"] + }, + "packages/runtime": { + "entry": ["src/filesystem-worker/worker-entry.ts"], + "project": ["src/**/*.ts", "scripts/*.mjs"] + }, + "packages/runtime-host": { + "ignoreBinaries": ["mkfifo", "taskkill.exe"], + "entry": [ + "src/__tests__/fixtures/stderr-after-launcher-exit.ts", + "src/__tests__/fixtures/windows-local-ipc-trust-host.ts", + "src/test-only/owned-candidate-gated-recovery-main.ts" + ], + "project": ["src/**/*.ts", "scripts/*.mjs"], + "ignoreIssues": { + "src/protocol/artifact.ts": ["duplicates"], + "src/protocol/session-transcript.ts": ["duplicates"], + "src/protocol/usage-pricing.ts": ["duplicates"] + } + }, + "packages/storage": { + "ignoreBinaries": ["mkfifo"], + "entry": [ + "src/__tests__/fixtures/session-bundle-hydration-binding-crash.ts", + "src/__tests__/fixtures/session-bundle-hydration-owner-write-failure.ts", + "src/__tests__/fixtures/session-bundle-inspect-child.ts", + "src/__tests__/fixtures/session-bundle-inspect-source-mutator.ts", + "src/__tests__/fixtures/session-bundle-pack-destination-replacer.ts", + "src/__tests__/fixtures/session-bundle-pack-link-replacer.ts", + "src/__tests__/fixtures/session-bundle-pack-linked-temp-remover.ts", + "src/__tests__/fixtures/session-bundle-pack-temp-replacer.ts" + ], + "project": ["src/**/*.ts"] + }, "apps/desktop": { "entry": [ "src/main/main.ts", @@ -27,8 +83,29 @@ }, "packages/ui": { "entry": ["src/**/*.test.ts", "src/**/*.test.tsx", "stories/**/*.@(ts|tsx)"], - "project": ["src/**/*.{ts,tsx}", "stories/**/*.{ts,tsx}"], + "project": ["src/**/*.{ts,tsx}", "stories/**/*.{ts,tsx}", "scripts/*.mjs"], "ignoreDependencies": ["@storybook/react-vite", "storybook"] + }, + "packages/core": { + "project": ["src/**/*.ts"] + }, + "packages/mcp": { + "project": ["src/**/*.ts"] + }, + "packages/computer-use": { + "project": ["src/**/*.ts"] + }, + "packages/cli": { + "project": ["src/**/*.ts", "scripts/*.mjs"] + }, + "packages/eval": { + "entry": ["harbor/deepseek-harness-toolchain/patch-subprocess-local.mjs"], + "project": ["src/**/*.ts", "harbor/**/*.mjs"], + "ignoreBinaries": ["taskkill.exe"] + }, + "website": { + "ignoreDependencies": ["cookie"], + "project": ["src/**/*.{ts,astro,mjs}", "scripts/*.mjs", "test/*.mjs"] } } } diff --git a/package-lock.json b/package-lock.json index 872cfd700d..a0bfd659bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,14 +26,20 @@ "@ai-sdk/provider-utils": "5.0.34", "@astryxdesign/cli": "0.5.2", "@astryxdesign/core": "0.5.2", + "@babel/parser": "7.29.7", "@biomejs/biome": "2.5.11", "@electron/asar": "4.3.0", + "@playwright/test": "^1.62.1", "@types/node": "^26.4.0", + "electron": "43.4.1", + "electron-builder": "26.15.3", + "electron-updater": "^6.8.9", "esbuild": "^0.28.1", "husky": "^9.1.7", "knip": "^6.33.0", "patch-package": "8.0.1", "typescript": "^7.0.2", + "ws": "^8.21.3", "yaml": "2.9.0" }, "engines": { diff --git a/package.json b/package.json index 862fe01018..112d2d223b 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,13 @@ "knip": "^6.33.0", "patch-package": "8.0.1", "typescript": "^7.0.2", - "yaml": "2.9.0" + "yaml": "2.9.0", + "electron": "43.4.1", + "electron-builder": "26.15.3", + "electron-updater": "^6.8.9", + "@playwright/test": "^1.62.1", + "@babel/parser": "7.29.7", + "ws": "^8.21.3" }, "allowScripts": { "esbuild@0.27.7": true, diff --git a/scripts/check-knip-workspace-coverage.mjs b/scripts/check-knip-workspace-coverage.mjs new file mode 100644 index 0000000000..fa7a61ad20 --- /dev/null +++ b/scripts/check-knip-workspace-coverage.mjs @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Run by .github/workflows/ci.yml before `npx knip`. + +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); + +/** Unconfigured workspace = knip's weak defaults; stale key = a gate that guards nothing. */ +export function evaluateWorkspaceCoverage({ knipKeys, packageWorkspaces }) { + const memberKeys = knipKeys.filter((key) => key !== '.'); + const missing = packageWorkspaces.filter((dir) => !memberKeys.includes(dir)); + const stale = memberKeys.filter((key) => !packageWorkspaces.includes(key)); + return { ok: missing.length === 0 && stale.length === 0, missing, stale }; +} + +function main() { + const readJson = (path) => JSON.parse(readFileSync(resolve(repoRoot, path), 'utf8')); + const { ok, missing, stale } = evaluateWorkspaceCoverage({ + knipKeys: Object.keys(readJson('knip.json').workspaces ?? {}), + packageWorkspaces: readJson('package.json').workspaces ?? [], + }); + if (missing.length > 0) { + console.error(`package.json workspaces missing from knip.json: ${missing.join(', ')}`); + } + if (stale.length > 0) { + console.error(`knip.json workspaces without a package.json entry: ${stale.join(', ')}`); + } + if (!ok) process.exitCode = 1; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(); diff --git a/scripts/check-knip-workspace-coverage.test.mjs b/scripts/check-knip-workspace-coverage.test.mjs new file mode 100644 index 0000000000..bc2e6e480c --- /dev/null +++ b/scripts/check-knip-workspace-coverage.test.mjs @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { evaluateWorkspaceCoverage } from './check-knip-workspace-coverage.mjs'; + +test('a package.json workspace missing from knip.json is reported', () => { + const { ok, missing, stale } = evaluateWorkspaceCoverage({ + knipKeys: ['.', 'packages/runtime'], + packageWorkspaces: ['packages/runtime', 'packages/storage'], + }); + assert.equal(ok, false); + assert.deepEqual(missing, ['packages/storage']); + assert.deepEqual(stale, []); +}); + +test('a stale knip.json key is reported even when other workspaces are covered', () => { + const { ok, missing, stale } = evaluateWorkspaceCoverage({ + knipKeys: ['.', 'packages/runtime', 'packages/retired'], + packageWorkspaces: ['packages/runtime'], + }); + assert.equal(ok, false); + assert.deepEqual(missing, []); + assert.deepEqual(stale, ['packages/retired']); +}); + +test('the root knip key is exempt from the stale check', () => { + const { ok, stale } = evaluateWorkspaceCoverage({ + knipKeys: ['.', 'packages/runtime'], + packageWorkspaces: ['packages/runtime'], + }); + assert.equal(ok, true); + assert.deepEqual(stale, []); +});