chore: pin dependency versions and harden CI caches (#2189)

* chore: pin dependency versions

* ci: enforce pinned dependency specs

* ci: fix pnpm executable invocation
This commit is contained in:
PerishFire 2026-05-19 13:58:27 +08:00 committed by GitHub
parent 477dd627b1
commit bd48c597b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1336 additions and 1542 deletions

View file

@ -126,15 +126,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -181,15 +192,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -213,15 +235,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -247,15 +280,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -278,15 +322,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -346,15 +401,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
@ -374,29 +440,41 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Cache Playwright browser binaries between runs. The key follows the
# @playwright/test version so browser revisions update with package bumps.
# Restore Playwright browser binaries without saving from CI runs. The
# key follows the @playwright/test version so browser revisions update
# with package bumps.
- name: Resolve Playwright version
id: playwright-version
run: |
version=$(node -p "require('./e2e/package.json').devDependencies['@playwright/test'].replace(/[^0-9.]/g,'')")
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
uses: actions/cache@v4
- name: Restore Playwright browser cache
uses: actions/cache/restore@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
@ -427,15 +505,26 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v6.4.0
with:
node-version: 24
cache: pnpm
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile

View file

@ -32,7 +32,7 @@
"typecheck": "pnpm --filter @open-design/contracts build && pnpm --filter @open-design/registry-protocol build && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@open-design/agui-adapter": "workspace:*",
"@open-design/contracts": "workspace:*",
"@open-design/platform": "workspace:*",
@ -40,26 +40,25 @@
"@open-design/registry-protocol": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"@opentelemetry/api": "^1.9.0",
"better-sqlite3": "^12.9.0",
"@opentelemetry/api": "1.9.1",
"better-sqlite3": "12.10.0",
"blake3-wasm": "2.1.5",
"chokidar": "^5.0.0",
"express": "^4.19.2",
"jszip": "^3.10.1",
"multer": "^2.1.1",
"posthog-node": "^4.18.0",
"prom-client": "^15.1.0",
"tar": "^7.5.13",
"undici": "^7.16.0"
"chokidar": "5.0.0",
"express": "4.22.1",
"jszip": "3.10.1",
"multer": "2.1.1",
"posthog-node": "4.18.0",
"prom-client": "15.1.3",
"tar": "7.5.15",
"undici": "7.25.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^4.17.21",
"@types/multer": "^2.1.0",
"@types/node": "^20.17.10",
"@types/tar": "^6.1.13",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"@types/better-sqlite3": "7.6.13",
"@types/express": "4.17.25",
"@types/multer": "2.1.0",
"@types/node": "20.19.39",
"typescript": "5.9.3",
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -23,6 +23,7 @@ import {
validateCodexGeneratedImagesDir,
} from '../src/server.js';
import { getAgentDef } from '../src/agents.js';
import { readMemoryConfig, writeMemoryConfig } from '../src/memory.js';
import { renderCodexImagegenOverride } from '../src/prompts/system.js';
function symlinkDir(target: string, link: string): void {
@ -60,11 +61,19 @@ async function withFakeAgent<T>(
describe('/api/chat', () => {
let server: http.Server;
let baseUrl: string;
let originalMemoryConfig: Awaited<ReturnType<typeof readMemoryConfig>> | null = null;
const originalPath = process.env.PATH;
const originalAgentHome = process.env.OD_AGENT_HOME;
const tempDirs: string[] = [];
beforeAll(async () => {
if (process.env.OD_DATA_DIR) {
originalMemoryConfig = await readMemoryConfig(process.env.OD_DATA_DIR);
await writeMemoryConfig(process.env.OD_DATA_DIR, {
enabled: false,
extraction: null,
});
}
const started = await startServer({ port: 0, returnServer: true }) as {
url: string;
server: http.Server;
@ -86,12 +95,19 @@ describe('/api/chat', () => {
}
});
afterAll(() => {
afterAll(async () => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
if (!server) return;
return new Promise<void>((resolve) => server.close(() => resolve()));
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
if (process.env.OD_DATA_DIR && originalMemoryConfig) {
await writeMemoryConfig(process.env.OD_DATA_DIR, {
enabled: originalMemoryConfig.enabled,
extraction: originalMemoryConfig.extraction,
});
}
});
it('does not reference an out-of-scope response while starting a run', async () => {
@ -538,7 +554,7 @@ setInterval(() => {}, 1000);
it('keeps Claude stream runs alive while structured output is still flowing', async () => {
const previous = process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = '1800';
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = '3000';
try {
await withFakeAgent(
'claude',
@ -552,6 +568,7 @@ const lines = [
JSON.stringify({ type: 'result', usage: { input_tokens: 1, output_tokens: 2 }, duration_ms: 700, stop_reason: 'end_turn' }),
];
let index = 0;
console.log(lines[index++]);
const timer = setInterval(() => {
if (index >= lines.length) {
clearInterval(timer);
@ -559,7 +576,7 @@ const timer = setInterval(() => {
return;
}
console.log(lines[index++]);
}, 400);
}, 750);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
@ -776,13 +793,15 @@ async function waitForRunStatus(
runId: string,
done: (status: string) => boolean = (status) => status !== 'queued' && status !== 'running',
): Promise<{ status: string }> {
for (let attempt = 0; attempt < 120; attempt += 1) {
let lastStatus = 'unknown';
for (let attempt = 0; attempt < 500; attempt += 1) {
const statusResponse = await fetch(`${baseUrl}/api/runs/${runId}`);
const statusBody = await statusResponse.json() as { status: string };
lastStatus = statusBody.status;
if (done(statusBody.status)) return statusBody;
await new Promise((resolve) => setTimeout(resolve, 25));
}
throw new Error('run did not reach expected status');
throw new Error(`run did not reach expected status; last status: ${lastStatus}`);
}
describe('chat prompt helpers', () => {

View file

@ -1149,7 +1149,10 @@ describe('POST /api/test/connection provider mode', () => {
describe('POST /api/test/connection agent mode', () => {
it('reports success for a fake Codex agent response', async () => {
await withFakeCodex(
`console.log(JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'ok' } }));`,
`
console.log(JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'ok' } }));
setImmediate(() => process.exit(0));
`,
async () => {
const res = await realFetch(`${baseUrl}/api/test/connection`, {
method: 'POST',
@ -1181,6 +1184,7 @@ fs.writeFileSync(${JSON.stringify(envFile)}, JSON.stringify({
SHOULD_NOT_PASS: process.env.OD_CONNECTION_TEST_SHOULD_NOT_PASS || null,
}));
console.log(JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'ok' } }));
setImmediate(() => process.exit(0));
`,
async () => {
const res = await realFetch(`${baseUrl}/api/test/connection`, {

View file

@ -3,6 +3,9 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
// These suites mutate process-wide env/PATH and bind real local servers.
// Keep files serial so fake agent binaries stay scoped to their tests.
fileParallelism: false,
include: ['tests/**/*.test.{ts,tsx,js,mjs,cjs}'],
setupFiles: ['tests/setup.ts'],
testTimeout: 20_000,

View file

@ -27,7 +27,7 @@
"@types/node": "24.12.2",
"electron": "41.3.0",
"typescript": "6.0.3",
"vitest": "^2.1.8"
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -11,20 +11,20 @@
"typecheck": "astro check"
},
"dependencies": {
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.6.0",
"astro": "^5.15.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"@astrojs/rss": "4.0.18",
"@astrojs/sitemap": "3.7.2",
"astro": "6.3.5",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@types/node": "^20.17.10",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"playwright": "^1.59.1",
"tsx": "^4.21.0",
"typescript": "^5.6.3"
"@astrojs/check": "0.9.9",
"@types/node": "20.19.39",
"@types/react": "18.3.28",
"@types/react-dom": "18.3.7",
"playwright": "1.60.0",
"tsx": "4.22.2",
"typescript": "5.9.3"
},
"engines": {
"node": "~24"

View file

@ -34,9 +34,9 @@
"devDependencies": {
"@types/node": "24.12.2",
"electron": "41.3.0",
"esbuild": "0.27.7",
"esbuild": "0.28.0",
"typescript": "6.0.3",
"vitest": "^2.1.8"
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -10,8 +10,8 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"typescript": "5.9.3",
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -28,30 +28,30 @@
"test": "vitest run -c vitest.config.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@anthropic-ai/sdk": "0.32.1",
"@open-design/contracts": "workspace:*",
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"lucide-react": "^1.14.0",
"next": "^16.2.5",
"openai": "^6.36.0",
"posthog-js": "^1.205.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"lucide-react": "1.16.0",
"next": "16.2.6",
"openai": "6.38.0",
"posthog-js": "1.374.2",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@testing-library/react": "^16.3.2",
"@types/jsdom": "^28.0.1",
"@types/node": "^20.17.10",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"jsdom": "29.1.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"@tailwindcss/postcss": "4.3.0",
"@testing-library/react": "16.3.2",
"@types/jsdom": "28.0.3",
"@types/node": "20.19.39",
"@types/react": "18.3.28",
"@types/react-dom": "18.3.7",
"jsdom": "29.1.1",
"postcss": "8.5.14",
"tailwindcss": "4.3.0",
"typescript": "5.9.3",
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -3,8 +3,8 @@ import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { Simulate } from 'react-dom/test-utils';
import { JSDOM } from 'jsdom';
import { ManualEditPanel, emptyManualEditDraft, manualEditPatchSummary, normalizeManualEditStyles } from '../../src/components/ManualEditPanel';
import { emptyManualEditStyles, type ManualEditTarget } from '../../src/edit-mode/types';
import { ManualEditPanel, emptyManualEditDraft, manualEditPatchSummary, normalizeManualEditStyles, type ManualEditDraft } from '../../src/components/ManualEditPanel';
import { emptyManualEditStyles, type ManualEditPatch, type ManualEditStyles, type ManualEditTarget } from '../../src/edit-mode/types';
const target: ManualEditTarget = {
id: 'hero-title',
@ -21,6 +21,13 @@ const target: ManualEditTarget = {
outerHtml: '<h1 data-od-id="hero-title">Original</h1>',
};
type OnDraftChange = (draft: ManualEditDraft) => void;
type OnStyleChange = (id: string, styles: Partial<ManualEditStyles>, label: string) => void;
type OnInvalidStyle = (id: string, keys: Array<keyof ManualEditStyles>) => void;
type OnApplyPatch = (patch: ManualEditPatch, label: string) => void;
type OnError = (message: string) => void;
type OnClearSelection = () => void;
describe('ManualEditPanel', () => {
let dom: JSDOM;
let host: HTMLDivElement;
@ -429,23 +436,23 @@ describe('ManualEditPanel', () => {
}
function renderPanel({
onDraftChange = vi.fn(),
onApplyPatch = vi.fn(),
onError = vi.fn(),
onStyleChange = vi.fn(),
onInvalidStyle = vi.fn(),
onClearSelection = vi.fn(),
onDraftChange = vi.fn<OnDraftChange>(),
onApplyPatch = vi.fn<OnApplyPatch>(),
onError = vi.fn<OnError>(),
onStyleChange = vi.fn<OnStyleChange>(),
onInvalidStyle = vi.fn<OnInvalidStyle>(),
onClearSelection = vi.fn<OnClearSelection>(),
attributesText = '{}',
selectedTarget = target,
styles = emptyManualEditStyles(),
pageStylesEnabled = true,
}: {
onDraftChange?: ReturnType<typeof vi.fn>;
onApplyPatch?: ReturnType<typeof vi.fn>;
onError?: ReturnType<typeof vi.fn>;
onStyleChange?: ReturnType<typeof vi.fn>;
onInvalidStyle?: ReturnType<typeof vi.fn>;
onClearSelection?: ReturnType<typeof vi.fn>;
onDraftChange?: OnDraftChange;
onApplyPatch?: OnApplyPatch;
onError?: OnError;
onStyleChange?: OnStyleChange;
onInvalidStyle?: OnInvalidStyle;
onClearSelection?: OnClearSelection;
attributesText?: string;
selectedTarget?: ManualEditTarget | null;
styles?: ReturnType<typeof emptyManualEditStyles>;
@ -469,16 +476,16 @@ describe('ManualEditPanel', () => {
canUndo={false}
canRedo={false}
pageStylesEnabled={pageStylesEnabled}
onSelectTarget={vi.fn()}
onSelectTarget={vi.fn<(target: ManualEditTarget) => void>()}
onDraftChange={onDraftChange}
onStyleChange={onStyleChange}
onInvalidStyle={onInvalidStyle}
onApplyPatch={onApplyPatch}
onError={onError}
onClearSelection={onClearSelection}
onCancelDraft={vi.fn()}
onUndo={vi.fn()}
onRedo={vi.fn()}
onCancelDraft={vi.fn<() => void>()}
onUndo={vi.fn<() => void>()}
onRedo={vi.fn<() => void>()}
/>,
);
});

View file

@ -13,12 +13,15 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { useState } from 'react';
import { PluginInputsForm } from '../../src/components/PluginInputsForm';
let onChange: ReturnType<typeof vi.fn>;
let onValidityChange: ReturnType<typeof vi.fn>;
type OnChange = (values: Record<string, unknown>) => void;
type OnValidityChange = (valid: boolean) => void;
let onChange: ReturnType<typeof vi.fn<OnChange>>;
let onValidityChange: ReturnType<typeof vi.fn<OnValidityChange>>;
beforeEach(() => {
onChange = vi.fn();
onValidityChange = vi.fn();
onChange = vi.fn<OnChange>();
onValidityChange = vi.fn<OnValidityChange>();
});
afterEach(() => cleanup());

View file

@ -64,7 +64,7 @@ vi.mock('../../src/providers/provider-models', () => ({
}));
import { SettingsDialog } from '../../src/components/SettingsDialog';
import type { SettingsSection } from '../../src/components/SettingsDialog';
import type { AgentRefreshOptions, SettingsSection } from '../../src/components/SettingsDialog';
import { I18nProvider } from '../../src/i18n';
import { LOCALES } from '../../src/i18n/types';
import type { AgentInfo, AppConfig, AppVersionInfo } from '../../src/types';
@ -98,6 +98,10 @@ const availableAgents: AgentInfo[] = [
},
];
type OnRefreshAgents = (
options?: AgentRefreshOptions,
) => void | AgentInfo[] | Promise<void | AgentInfo[]>;
const sampleBundledPets = [
{
id: 'dario',
@ -184,7 +188,7 @@ function renderSettingsDialog(
options: {
agents?: AgentInfo[];
daemonLive?: boolean;
onRefreshAgents?: ReturnType<typeof vi.fn>;
onRefreshAgents?: OnRefreshAgents;
initialSection?: SettingsSection;
appVersionInfo?: AppVersionInfo | null;
} = {},
@ -192,7 +196,7 @@ function renderSettingsDialog(
const onPersist = vi.fn();
const onPersistComposioKey = vi.fn();
const onClose = vi.fn();
const onRefreshAgents = options.onRefreshAgents ?? vi.fn();
const onRefreshAgents = options.onRefreshAgents ?? vi.fn<OnRefreshAgents>();
const view = render(
<SettingsDialog

View file

@ -88,16 +88,19 @@ const orbitTemplates = [
const clipboardDescriptor = Object.getOwnPropertyDescriptor(window.navigator, 'clipboard');
type OnPersist = (cfg: AppConfig, options?: { forceMediaProviderSync?: boolean }) => void | Promise<void>;
type OnClose = () => void;
function renderOrbitSettings(
initial: Partial<AppConfig> = {},
options: {
composioApiKeyConfigured?: boolean;
onPersist?: ReturnType<typeof vi.fn>;
onClose?: ReturnType<typeof vi.fn>;
onPersist?: OnPersist;
onClose?: OnClose;
} = {},
) {
const onPersist = options.onPersist ?? vi.fn();
const onClose = options.onClose ?? vi.fn();
const onPersist = options.onPersist ?? vi.fn<OnPersist>();
const onClose = options.onClose ?? vi.fn<OnClose>();
render(
<SettingsDialog
@ -114,9 +117,9 @@ function renderOrbitSettings(
appVersionInfo={null}
initialSection="orbit"
onPersist={onPersist}
onPersistComposioKey={vi.fn()}
onPersistComposioKey={vi.fn<(composio: AppConfig['composio']) => void>()}
onClose={onClose}
onRefreshAgents={vi.fn()}
onRefreshAgents={vi.fn<() => void>()}
/>,
);

View file

@ -1,5 +1,6 @@
import { execFile } from 'node:child_process';
import { createServer } from 'node:net';
import { extname } from 'node:path';
import { promisify } from 'node:util';
import { e2eWorkspaceRoot, type SmokeSuite } from './smoke-suite.ts';
@ -7,6 +8,7 @@ import { e2eWorkspaceRoot, type SmokeSuite } from './smoke-suite.ts';
const execFileAsync = promisify(execFile);
const pnpmCommand = process.env.OD_E2E_PNPM_COMMAND ?? 'pnpm';
const pnpmExecPath = process.env.npm_execpath;
const nodeLoadablePackageManagerExtensions = new Set(['.js', '.cjs', '.mjs']);
export type ToolsDevAppStatus = {
pid?: number;
@ -140,10 +142,13 @@ export async function readToolsDevLogs(suite: SmokeSuite): Promise<Record<string
}
async function runToolsDevJson<T>(suite: SmokeSuite, args: string[]): Promise<T> {
const command = process.env.OD_E2E_PNPM_COMMAND == null && pnpmExecPath
const useNpmExecPathWithNode = process.env.OD_E2E_PNPM_COMMAND == null
&& pnpmExecPath != null
&& nodeLoadablePackageManagerExtensions.has(extname(pnpmExecPath).toLowerCase());
const command = useNpmExecPathWithNode
? process.execPath
: pnpmCommand;
const commandArgs = command === process.execPath && process.env.OD_E2E_PNPM_COMMAND == null && pnpmExecPath
: (process.env.OD_E2E_PNPM_COMMAND == null && pnpmExecPath ? pnpmExecPath : pnpmCommand);
const commandArgs = useNpmExecPathWithNode
? [pnpmExecPath, 'tools-dev', ...args]
: ['tools-dev', ...args];
const { stdout } = await execFileAsync(command, commandArgs, {

View file

@ -10,11 +10,11 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@types/node": "^20.17.10",
"tsx": "4.21.0",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"@playwright/test": "1.60.0",
"@types/node": "20.19.39",
"tsx": "4.22.2",
"typescript": "5.9.3",
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -43,7 +43,7 @@ let
# `nix build .#daemon` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-lROdH5HgKFf3R7DYGbc8n/GrmINwLbfVwC4Xp7SrHN4=";
pnpmDepsHash = "sha256-BqnA3aBPHiy+o04atLF6RCZGJKA24qneuqPzV0WH2G8=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {

View file

@ -30,7 +30,7 @@ let
# `nix build .#web` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-lROdH5HgKFf3R7DYGbc8n/GrmINwLbfVwC4Xp7SrHN4=";
pnpmDepsHash = "sha256-BqnA3aBPHiy+o04atLF6RCZGJKA24qneuqPzV0WH2G8=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {

View file

@ -29,15 +29,24 @@
"@open-design/tools-pack": "workspace:*",
"@open-design/tools-pr": "workspace:*",
"@open-design/tools-serve": "workspace:*",
"@types/node": "^20.17.10",
"tsx": "4.21.0",
"typescript": "^5.6.3"
"@types/node": "20.19.39",
"tsx": "4.22.2",
"typescript": "5.9.3"
},
"engines": {
"node": "~24",
"pnpm": ">=10.33.2 <11"
},
"pnpm": {
"overrides": {
"brace-expansion": "5.0.6",
"devalue": "5.8.1",
"fast-uri": "3.1.2",
"hono": "4.12.19",
"ip-address": "10.2.0",
"postcss": "8.5.14",
"yaml": "2.9.0"
},
"onlyBuiltDependencies": [
"better-sqlite3",
"electron",

View file

@ -6,7 +6,9 @@
"description": "Pure-TS bidirectional adapter between Open Design's PersistedAgentEvent / GenUIEvent / PluginPipelineStageEvent union and the AG-UI canonical event protocol (see https://github.com/CopilotKit/CopilotKit). No node:fs imports — daemon emits, web/CopilotKit consumes.",
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
@ -22,10 +24,10 @@
"@open-design/contracts": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.17.10",
"esbuild": "0.27.7",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"@types/node": "20.19.39",
"esbuild": "0.28.0",
"typescript": "5.9.3",
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -49,11 +49,11 @@
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"dependencies": {
"zod": "^3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"esbuild": "0.27.7",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"esbuild": "0.28.0",
"typescript": "5.9.3",
"vitest": "4.1.6"
}
}

View file

@ -21,8 +21,8 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"vitest": "^2.1.8",
"esbuild": "0.28.0",
"vitest": "4.1.6",
"typescript": "6.0.3"
},
"engines": {

View file

@ -2,7 +2,7 @@ import { execFile, spawn, type ChildProcess, type StdioOptions } from "node:chil
import { existsSync, readdirSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { isAbsolute, join } from "node:path";
import { extname, isAbsolute, join } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
export type CommandInvocation = {
@ -193,6 +193,8 @@ function buildCmdShimInvocation(command: string, args: string[], env: NodeJS.Pro
};
}
const nodeLoadablePackageManagerExtensions = new Set([".js", ".cjs", ".mjs"]);
export function createCommandInvocation({ args = [], command, env = process.env }: CommandInvocationRequest): CommandInvocation {
if (process.platform === "win32" && /\.(bat|cmd)$/i.test(command)) {
return buildCmdShimInvocation(command, args, env);
@ -202,7 +204,12 @@ export function createCommandInvocation({ args = [], command, env = process.env
export function createPackageManagerInvocation(args: string[], env: NodeJS.ProcessEnv = process.env): CommandInvocation {
const execPath = env.npm_execpath;
if (execPath) return { args: [execPath, ...args], command: process.execPath };
if (execPath) {
if (nodeLoadablePackageManagerExtensions.has(extname(execPath).toLowerCase())) {
return { args: [execPath, ...args], command: process.execPath };
}
return createCommandInvocation({ args, command: execPath, env });
}
if (process.platform === "win32") {
return buildCmdShimInvocation("pnpm", args, env);
}

View file

@ -271,7 +271,7 @@ describe("createPackageManagerInvocation", () => {
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
});
it("uses npm_execpath via process.execPath when set, regardless of platform", () => {
it("uses Node-loadable npm_execpath via process.execPath when set", () => {
setPlatform("win32");
const invocation = createPackageManagerInvocation(["install"], {
npm_execpath: "C:\\Users\\u\\.nvm\\pnpm.cjs",
@ -282,6 +282,33 @@ describe("createPackageManagerInvocation", () => {
expect(invocation.windowsVerbatimArguments).toBeUndefined();
});
it("uses binary npm_execpath directly on POSIX", () => {
setPlatform("linux");
const invocation = createPackageManagerInvocation(["install"], {
npm_execpath: "/home/runner/setup-pnpm/node_modules/.bin/pnpm",
} as NodeJS.ProcessEnv);
expect(invocation).toEqual({
args: ["install"],
command: "/home/runner/setup-pnpm/node_modules/.bin/pnpm",
});
});
it("wraps binary npm_execpath shims through cmd.exe on Windows", () => {
setPlatform("win32");
const invocation = createPackageManagerInvocation(["install"], {
ComSpec: "cmd.exe",
npm_execpath: "C:\\Users\\u\\setup-pnpm\\pnpm.cmd",
} as NodeJS.ProcessEnv);
expect(invocation.command).toBe("cmd.exe");
expect(invocation.windowsVerbatimArguments).toBe(true);
expect(invocation.args).toEqual([
"/d",
"/s",
"/c",
'"C:\\Users\\u\\setup-pnpm\\pnpm.cmd install"',
]);
});
it("returns plain pnpm invocation on POSIX without npm_execpath", () => {
setPlatform("linux");
const invocation = createPackageManagerInvocation(["install"], {} as NodeJS.ProcessEnv);

View file

@ -22,13 +22,13 @@
},
"dependencies": {
"@open-design/contracts": "workspace:*",
"zod": "^3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@types/node": "^20.17.10",
"esbuild": "0.27.7",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"@types/node": "20.19.39",
"esbuild": "0.28.0",
"typescript": "5.9.3",
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -21,11 +21,11 @@
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"dependencies": {
"zod": "^3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"esbuild": "0.27.7",
"typescript": "^5.6.3",
"vitest": "^2.1.8"
"esbuild": "0.28.0",
"typescript": "5.9.3",
"vitest": "4.1.6"
}
}

View file

@ -21,9 +21,9 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"esbuild": "0.28.0",
"typescript": "6.0.3",
"vitest": "^2.1.8"
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -21,8 +21,8 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"vitest": "^2.1.8",
"esbuild": "0.28.0",
"vitest": "4.1.6",
"typescript": "6.0.3"
},
"engines": {

File diff suppressed because it is too large Load diff

View file

@ -184,6 +184,197 @@ async function checkResidualJavaScript(): Promise<boolean> {
return true;
}
const sourcePackageManifestRootPaths = ["package.json", "e2e/package.json"];
const sourcePackageManifestScopedDirectories = ["apps", "packages", "tools"];
const packageDependencySections = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
const packageManagerOverridePaths = ["pnpm.overrides", "overrides", "resolutions"];
const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const exactNpmAliasPattern = /^npm:(?:@[^/]+\/)?[^@]+@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
type DependencySpecViolation = {
filePath: string;
fieldPath: string;
name: string;
spec: unknown;
reason: string;
};
type DependencySpecStats = {
exact: number;
manifests: number;
total: number;
workspace: number;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isAllowedDependencySpec(spec: string): boolean {
return spec === "workspace:*" || exactVersionPattern.test(spec) || exactNpmAliasPattern.test(spec);
}
function dependencySpecReason(spec: string): string {
if (spec.startsWith("workspace:") && spec !== "workspace:*") {
return "workspace dependencies must use exactly workspace:*";
}
return "dependency specs must be exact versions like 1.2.3 or workspace:*";
}
function dependencySpecFieldValue(value: unknown): string {
return typeof value === "string" ? value : JSON.stringify(value);
}
async function collectScopedPackageManifestPaths(scopeDirectory: string): Promise<string[]> {
const scopeRoot = path.join(repoRoot, scopeDirectory);
const entries = await readdir(scopeRoot, { withFileTypes: true });
const manifestPaths: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const packageDirectory = path.join(scopeRoot, entry.name);
const packageEntries = await readdir(packageDirectory, { withFileTypes: true });
if (packageEntries.some((packageEntry) => packageEntry.isFile() && packageEntry.name === "package.json")) {
manifestPaths.push(`${scopeDirectory}/${entry.name}/package.json`);
}
}
return manifestPaths;
}
async function collectSourcePackageManifestPaths(): Promise<string[]> {
const scopedManifestPaths = (
await Promise.all(sourcePackageManifestScopedDirectories.map((scope) => collectScopedPackageManifestPaths(scope)))
).flat();
return [...sourcePackageManifestRootPaths, ...scopedManifestPaths].sort();
}
function getPackageJsonField(packageJson: Record<string, unknown>, fieldPath: string): unknown {
let current: unknown = packageJson;
for (const part of fieldPath.split(".")) {
if (!isRecord(current)) return undefined;
current = current[part];
}
return current;
}
function checkDependencySpecRecord(
record: Record<string, unknown>,
filePath: string,
fieldPath: string,
violations: DependencySpecViolation[],
stats: DependencySpecStats,
): void {
for (const [name, spec] of Object.entries(record).sort(([left], [right]) => left.localeCompare(right))) {
if (isRecord(spec)) {
checkDependencySpecRecord(spec, filePath, `${fieldPath}.${name}`, violations, stats);
continue;
}
stats.total += 1;
if (typeof spec !== "string") {
violations.push({
filePath,
fieldPath,
name,
spec,
reason: "dependency specs must be strings",
});
continue;
}
if (spec === "workspace:*") {
stats.workspace += 1;
continue;
}
if (isAllowedDependencySpec(spec)) {
stats.exact += 1;
continue;
}
violations.push({
filePath,
fieldPath,
name,
spec,
reason: dependencySpecReason(spec),
});
}
}
async function checkPackageDependencySpecs(): Promise<boolean> {
const manifestPaths = await collectSourcePackageManifestPaths();
const violations: DependencySpecViolation[] = [];
const stats: DependencySpecStats = {
exact: 0,
manifests: manifestPaths.length,
total: 0,
workspace: 0,
};
for (const manifestPath of manifestPaths) {
const packageJson = JSON.parse(await readFile(path.join(repoRoot, manifestPath), "utf8")) as Record<string, unknown>;
for (const section of packageDependencySections) {
const value = packageJson[section];
if (value === undefined) continue;
if (!isRecord(value)) {
violations.push({
filePath: manifestPath,
fieldPath: section,
name: section,
spec: value,
reason: "dependency sections must be objects",
});
continue;
}
checkDependencySpecRecord(value, manifestPath, section, violations, stats);
}
for (const overridePath of packageManagerOverridePaths) {
const value = getPackageJsonField(packageJson, overridePath);
if (value === undefined) continue;
if (!isRecord(value)) {
violations.push({
filePath: manifestPath,
fieldPath: overridePath,
name: overridePath,
spec: value,
reason: "package-manager override sections must be objects",
});
continue;
}
checkDependencySpecRecord(value, manifestPath, overridePath, violations, stats);
}
}
if (violations.length > 0) {
console.error("Package dependency spec violations found:");
for (const violation of violations) {
console.error(
`- ${violation.filePath} ${violation.fieldPath}.${violation.name}=${dependencySpecFieldValue(violation.spec)} -> ${violation.reason}`,
);
}
return false;
}
console.log(
`Package dependency spec check passed: ${stats.manifests} package.json files, ${stats.exact} exact specs, ${stats.workspace} workspace:* specs.`,
);
return true;
}
const testLayoutScopedDirectories = ["apps", "packages", "tools"];
const testLayoutSkippedDirectories = new Set([".next", ".od-data", "dist", "node_modules", "out", "reports", "test-results"]);
@ -705,6 +896,7 @@ async function checkStylePolicy(): Promise<boolean> {
const checks: GuardCheck[] = [
{ name: "residual JavaScript", run: checkResidualJavaScript },
{ name: "package dependency specs", run: checkPackageDependencySpecs },
{ name: "test layout", run: checkTestLayout },
{ name: "e2e layout", run: checkE2eLayout },
{ name: "web test layout", run: checkWebTestLayout },

View file

@ -20,8 +20,8 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"tsx": "4.21.0",
"esbuild": "0.28.0",
"tsx": "4.22.2",
"typescript": "6.0.3"
},
"engines": {

View file

@ -23,10 +23,10 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"tsx": "4.21.0",
"esbuild": "0.28.0",
"tsx": "4.22.2",
"typescript": "6.0.3",
"vitest": "^2.1.8"
"vitest": "4.1.6"
},
"engines": {
"node": "~24"

View file

@ -17,8 +17,8 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"tsx": "4.21.0",
"esbuild": "0.28.0",
"tsx": "4.22.2",
"typescript": "6.0.3"
},
"engines": {

View file

@ -17,10 +17,10 @@
},
"devDependencies": {
"@types/node": "24.12.2",
"esbuild": "0.27.7",
"tsx": "4.21.0",
"esbuild": "0.28.0",
"tsx": "4.22.2",
"typescript": "6.0.3",
"vitest": "^2.1.8"
"vitest": "4.1.6"
},
"engines": {
"node": "~24"