Merge branch 'master' into feat/apply-patch
Resolves the headless snapshot runner (keeps both the readonly-patch-directory and cua-script-guidance workspace setups), regenerates the client slot and tool catalogs with their pairing record, adds the apply_patch tool title to the five locales master introduced, and refreshes the apply-patch system-prompt pin for master's subagent guidance change.
This commit is contained in:
commit
d37b2ba52b
3740 files changed
+95972
-10460
No files matched your search
@@ -159,6 +159,19 @@ class Target {
|
||||
/**
|
||||
* Validated CLI configuration; construction owns help and parse-error exits.
|
||||
*/
|
||||
/** pkg's `--compress` values, whose build cost differs by two orders of magnitude. */
|
||||
const COMPRESS_TYPES = ['None', 'GZip', 'Brotli', 'Zstd'] as const
|
||||
type CompressType = typeof COMPRESS_TYPES[number]
|
||||
|
||||
/**
|
||||
* Narrow a raw `--compress` value to a supported pkg compressor.
|
||||
* @param value - the command-line value.
|
||||
* @returns whether pkg accepts the value.
|
||||
*/
|
||||
function isCompressType(value: string): value is CompressType {
|
||||
return (COMPRESS_TYPES as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
class BuildCli {
|
||||
private constructor(
|
||||
/** Build targets; defaults to the host platform only. */
|
||||
@@ -167,6 +180,8 @@ class BuildCli {
|
||||
readonly skipBuild: boolean,
|
||||
/** Build through `pnpm run build:artifacts`, which omits the aggregate test and script typechecks. */
|
||||
readonly buildArtifacts: boolean,
|
||||
/** pkg archive compression; Brotli is the smallest and by far the slowest. */
|
||||
readonly compress: CompressType,
|
||||
/** Print every command and config patch instead of executing. */
|
||||
readonly dryRun: boolean,
|
||||
) {}
|
||||
@@ -205,7 +220,11 @@ class BuildCli {
|
||||
}
|
||||
seen.add(key)
|
||||
}
|
||||
return new BuildCli(targets, values['skip-build'], values['build-artifacts'], values['dry-run'])
|
||||
const compress = values.compress ?? 'Brotli'
|
||||
if (!isCompressType(compress)) {
|
||||
throw new Error(`build-exe-for-python-sdk: --compress must be one of ${COMPRESS_TYPES.join(' | ')}, got ${JSON.stringify(compress)}.`)
|
||||
}
|
||||
return new BuildCli(targets, values['skip-build'], values['build-artifacts'], compress, values['dry-run'])
|
||||
}
|
||||
|
||||
private static parseRaw(argv: string[]) {
|
||||
@@ -215,6 +234,7 @@ class BuildCli {
|
||||
'targets': { type: 'string' },
|
||||
'skip-build': { type: 'boolean', default: false },
|
||||
'build-artifacts': { type: 'boolean', default: false },
|
||||
'compress': { type: 'string' },
|
||||
'dry-run': { type: 'boolean', default: false },
|
||||
'help': { type: 'boolean', default: false },
|
||||
},
|
||||
@@ -229,10 +249,11 @@ class BuildCli {
|
||||
' Default: the host platform only (on node24).',
|
||||
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
|
||||
' --build-artifacts run `pnpm run build:artifacts` instead, omitting the aggregate test and script typechecks.',
|
||||
' --compress=<type> pkg archive compression: None | GZip | Brotli | Zstd. Default: Brotli (smallest, slowest).',
|
||||
' --dry-run print every command and config patch without executing.',
|
||||
' --help print this help.',
|
||||
'',
|
||||
'Build route: @yao-pkg/pkg --sea --compress Brotli (root devDependency, pnpm-patched); see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.',
|
||||
'Build route: @yao-pkg/pkg --sea with the selected --compress type (root devDependency, pnpm-patched); see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.',
|
||||
`Stages the node carrier in ${PYTHON_RUNTIME_DIR}/${PYTHON_NODE_SUBDIR} and writes executables to ${OUT_DIR}/.`,
|
||||
].join('\n')
|
||||
}
|
||||
@@ -400,7 +421,7 @@ class SingleExeBuild {
|
||||
this.staging,
|
||||
'--sea',
|
||||
'--compress',
|
||||
'Brotli',
|
||||
this.cli.compress,
|
||||
'--targets',
|
||||
target.spec,
|
||||
'--output',
|
||||
|
||||
+11
-1
@@ -60,7 +60,17 @@ describe('repository build CLI', () => {
|
||||
})
|
||||
|
||||
describe('repository build steps', () => {
|
||||
it('keeps artifact steps in serial dependency order', () => {
|
||||
it.each(['complete', 'check', 'typecheck'] as const)('gives Host compilers an 8 GiB heap in %s scope', (scope) => {
|
||||
const gates = buildGates(root, scope)
|
||||
for (const gate of gates.filter(gate => gate.id === 'host-src' || gate.id === 'host-check')) {
|
||||
expect(gate.args[0]).toBe('--max-old-space-size=8192')
|
||||
}
|
||||
for (const gate of gates.filter(gate => gate.id === 'client-src' || gate.id === 'client-check')) {
|
||||
expect(gate.args.some(arg => arg.startsWith('--max-old-space-size'))).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('finishes Host and Desktop readers before Client compilation rewrites shared outputs', () => {
|
||||
expect(graph(buildGates(root, 'artifacts'))).toEqual(artifactGraph)
|
||||
})
|
||||
|
||||
|
||||
@@ -556,11 +556,11 @@ it('requires the local speech worker and locked runtime in the published payload
|
||||
}
|
||||
})
|
||||
|
||||
it('requires both Node execution bootstraps in the published payload', () => {
|
||||
it('requires Node profile configuration and both execution bootstraps in the published payload', () => {
|
||||
const dir = 'packages/ptc-runtime/ptc-runtime-node'
|
||||
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
|
||||
expect(checkWorkspaceManifest({ dir, manifest })).toEqual([])
|
||||
for (const omitted of ['lib/process.js', 'lib/persistent-process.js']) {
|
||||
for (const omitted of ['lib/configuration.js', 'lib/process.js', 'lib/persistent-process.js']) {
|
||||
const files = expectedDshPackageFiles(manifest).filter(file => file !== omitted)
|
||||
expect(checkWorkspaceManifest({ dir, manifest: { ...manifest, files } }))
|
||||
.toEqual([expect.stringContaining('package.json files must be')])
|
||||
|
||||
@@ -21,7 +21,7 @@ import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publica
|
||||
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
|
||||
import { bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
|
||||
import {
|
||||
PACKAGE_ASSET_EXPORT,
|
||||
PACKAGE_ASSET_EXPORT_TARGET,
|
||||
@@ -211,12 +211,16 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// The CPython side ships as source .py files, published as-is rather than built.
|
||||
'@deepseek-ai/dsh-experimental-ptc-runtime-python': ['py/**/*.py'],
|
||||
'@deepseek-ai/dsh-experimental-speech-to-text-sensevoice': ['runtime/assets.json'],
|
||||
// Node execution modes each launch a separately bundled bootstrap.
|
||||
'@deepseek-ai/dsh-ptc-runtime-node': ['lib/process.js', 'lib/persistent-process.js'],
|
||||
// Node profile configuration and process bootstraps have separate bundles.
|
||||
'@deepseek-ai/dsh-ptc-runtime-node': ['lib/configuration.js', 'lib/process.js', 'lib/persistent-process.js'],
|
||||
// The Inspector owns a Worker and a mirrored frontend outside package export paths.
|
||||
'@deepseek-ai/dsh-experimental-inspector': ['lib/client.*.js', 'lib/worker.js', 'lib/devtools/**'],
|
||||
// Creator's composition guidance travels with the declaration package.
|
||||
'@deepseek-ai/dsh-agent-preset': ['skills'],
|
||||
// Personalized pages ship the location guidance used by their Creator.
|
||||
'@deepseek-ai/dsh-creation-recommendations': ['references/*.md', 'skills/*.md'],
|
||||
// Official-plugin recommendations ship the skill text their provider loads.
|
||||
'@deepseek-ai/dsh-plugin-recommendations': ['skills/*.md'],
|
||||
// The Web Host mounts the default-off settings owner independently of the
|
||||
// delegation-tool instances.
|
||||
'@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'],
|
||||
|
||||
+95
-12
@@ -14,7 +14,7 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{
|
||||
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
|
||||
|
||||
describe('CI workflow', () => {
|
||||
it('keeps at most ten worker jobs active across PR workflows while Python runtime targets follow the SDK suite', () => {
|
||||
it('keeps at most eleven worker jobs active across PR workflows while Python runtime targets follow the SDK suite', () => {
|
||||
const ci = loadWorkflow('.github/workflows/ci.yml')
|
||||
const ciWindows = loadWorkflow('.github/workflows/ci-windows.yml')
|
||||
const ciPython = loadWorkflow('.github/workflows/ci-python.yml')
|
||||
@@ -86,10 +86,12 @@ describe('CI workflow', () => {
|
||||
// Python's runtime targets start only after the SDK suite; Windows starts
|
||||
// immediately. Plan and wheel stages precede the two-target matrix.
|
||||
const windowsActive = windowsJobs.length
|
||||
// The lint lane owns its own runner, so it raises the ceiling by one; the
|
||||
// self-hosted failover pool is still admitted only up to its own limit.
|
||||
const beforeSdk = ordinaryJobs.length + compatibilityLimit + windowsActive + 1
|
||||
const afterSdk = ordinaryJobs.length + compatibilityLimit + windowsActive + Math.max(1, targets.split(',').length)
|
||||
expect(beforeSdk).toBeLessThanOrEqual(10)
|
||||
expect(afterSdk).toBeLessThanOrEqual(10)
|
||||
expect(beforeSdk).toBeLessThanOrEqual(11)
|
||||
expect(afterSdk).toBeLessThanOrEqual(11)
|
||||
})
|
||||
|
||||
it('keeps the union of required PR verdicts equal to the pre-split blocking set', () => {
|
||||
@@ -117,6 +119,7 @@ describe('CI workflow', () => {
|
||||
'node-24-bench',
|
||||
'node-24-consumers',
|
||||
'node-24-coverage',
|
||||
'node-24-lint',
|
||||
'node-compat',
|
||||
'python-runtime',
|
||||
'python-sdk',
|
||||
@@ -143,7 +146,7 @@ describe('CI workflow', () => {
|
||||
const scripts = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { scripts: Record<string, string> }
|
||||
const hostSteps = scripts.scripts['build:lib:host']!.split(' && ')
|
||||
expect(hostSteps[0]).toBe('tsx scripts/compile-referenced-projects.ts host-libraries')
|
||||
expect(hostSteps).toContain('node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -p tsconfig.host.json')
|
||||
expect(hostSteps).toContain('node --max-old-space-size=8192 ./node_modules/typescript/bin/tsc -p tsconfig.host.json')
|
||||
expect(hostSteps.at(-1)).toBe('pnpm --filter @deepseek-ai/dsh-desktop run bundle')
|
||||
expect(scripts.scripts['build:lib:client']).toContain('tsc -b tsconfig.client.json')
|
||||
expect(scripts.scripts.typecheck).toBe('tsx scripts/build.ts --scope typecheck')
|
||||
@@ -371,13 +374,14 @@ describe('CI workflow', () => {
|
||||
|| !isRecord(windowsWorkflow.jobs['windows-checks-passed'])
|
||||
|| !isRecord(workflow.jobs['node-24'])
|
||||
|| !isRecord(workflow.jobs['node-24-coverage'])
|
||||
|| !isRecord(workflow.jobs['node-24-lint'])
|
||||
|| !isRecord(workflow.jobs['node-24-bench'])
|
||||
|| !isRecord(workflow.jobs['node-24-consumers'])
|
||||
|| !isRecord(workflow.jobs['node-compat'])
|
||||
|| !isRecord(workflow.jobs['all-checks-passed'])
|
||||
|| !isRecord(masterWorkflow.jobs)
|
||||
|| !isRecord(masterWorkflow.jobs['serial-windows'])) {
|
||||
throw new TypeError('CI workflow must define node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; CI Windows must define windows-build, windows-coverage, windows-native-tests, and windows-checks-passed; ci-master must define serial-windows')
|
||||
throw new TypeError('CI workflow must define node-24, node-24-coverage, node-24-lint, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; CI Windows must define windows-build, windows-coverage, windows-native-tests, and windows-checks-passed; ci-master must define serial-windows')
|
||||
}
|
||||
|
||||
const windowsBuild = windowsWorkflow.jobs['windows-build']
|
||||
@@ -670,12 +674,19 @@ describe('CI workflow', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('runs required benchmarks on standard hosted Linux independently of failover', () => {
|
||||
it('runs required benchmarks on a four-vCPU Blacksmith runner and falls back to hosted Linux', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
const benchmark = workflowJob(workflow, 'node-24-bench')
|
||||
const aggregate = workflowJob(workflow, 'all-checks-passed')
|
||||
|
||||
expect(benchmark['runs-on']).toBe('ubuntu-24.04')
|
||||
// The shared self-hosted pool is not an idle runner, so only the
|
||||
// Blacksmith leg moves the benchmarks off hosted Linux.
|
||||
expect(evaluateRunsOn(benchmark['runs-on'], { vars: { DSH_CI_FAILOVER_LINUX: 'blacksmith' } }))
|
||||
.toBe('blacksmith-4vcpu-ubuntu-2404')
|
||||
for (const mode of ['', 'selfhosted', 'unexpected']) {
|
||||
expect(evaluateRunsOn(benchmark['runs-on'], { vars: { DSH_CI_FAILOVER_LINUX: mode } })).toBe('ubuntu-24.04')
|
||||
}
|
||||
expect(JSON.stringify(benchmark.steps)).not.toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(benchmark.if).toBe("github.event_name == 'pull_request'")
|
||||
expect(benchmark.needs).toBeUndefined()
|
||||
expect(benchmark['continue-on-error']).toBeUndefined()
|
||||
@@ -738,7 +749,7 @@ describe('CI workflow', () => {
|
||||
'wine_node "$scratch/logs/host-references.log" --import tsx/esm scripts/compile-referenced-projects.ts host',
|
||||
)
|
||||
const aggregate = wineGates.indexOf(
|
||||
'wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -p tsconfig.host.json --pretty false',
|
||||
'wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.host.json --pretty false',
|
||||
)
|
||||
const metadata = wineGates.indexOf('wine_node "$scratch/logs/host-typert.log" --import tsx/esm scripts/generate-typert.ts')
|
||||
expect(metadata).toBeGreaterThan(-1)
|
||||
@@ -1078,7 +1089,7 @@ describe('DeepSeek e2e workflow', () => {
|
||||
if (!Array.isArray(e2e.steps)) throw new TypeError('DeepSeek e2e workflow must define steps')
|
||||
|
||||
const step = e2e.steps.filter(isRecord).find(candidate => candidate.name === 'E2E tests (real DeepSeek API)')
|
||||
expect(step).toMatchObject({ env: { DSH_E2E_MAX_WORKERS: 4 } })
|
||||
expect(step).toMatchObject({ env: { DSH_E2E_MAX_WORKERS: 6 } })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1371,6 +1382,27 @@ describe('Weighted approval workflow', () => {
|
||||
'persist-credentials': false,
|
||||
},
|
||||
})
|
||||
const token = steps.find(step => step.id === 'decision-token')
|
||||
expect(token).toMatchObject({
|
||||
'continue-on-error': true,
|
||||
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1',
|
||||
with: {
|
||||
'client-id': '${{ vars.DSH_APPROVAL_APP_CLIENT_ID }}',
|
||||
'private-key': '${{ secrets.DSH_APPROVAL_APP_PRIVATE_KEY }}',
|
||||
owner: 'deepseek-harness',
|
||||
repositories: 'deepseek-harness',
|
||||
'permission-checks': 'write',
|
||||
},
|
||||
})
|
||||
const tokenIndex = steps.indexOf(token!)
|
||||
expect(tokenIndex).toBeGreaterThan(steps.indexOf(checkout!))
|
||||
expect(tokenIndex).toBeLessThan(steps.findIndex(step => step.id === 'revoke'))
|
||||
for (const step of steps.filter(step => typeof step.run === 'string' && step.run.startsWith('node .github/review-ownership/check-approval.mjs'))) {
|
||||
expect(step.env).toMatchObject({
|
||||
DECISION_TOKEN: '${{ steps.decision-token.outputs.token }}',
|
||||
DECISION_APP_SLUG: '${{ vars.DSH_APPROVAL_APP_SLUG }}',
|
||||
})
|
||||
}
|
||||
const setupIndex = steps.findIndex(step => typeof step.uses === 'string' && step.uses.startsWith('actions/setup-python@'))
|
||||
expect(steps[setupIndex]?.if).toBe("steps.revoke.outputs.active == 'true'")
|
||||
expect(steps[setupIndex]?.uses).toBe('actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1')
|
||||
@@ -1409,7 +1441,7 @@ describe('Weighted approval workflow', () => {
|
||||
expect(record?.run).toBe("echo 'Recorded a weighted approval review event.'")
|
||||
expect(recordSteps).toHaveLength(1)
|
||||
expect(JSON.stringify(publisher)).not.toContain('github.event.pull_request.head')
|
||||
expect(JSON.stringify(publisher)).not.toContain('secrets.')
|
||||
expect(JSON.stringify(publisher).match(/secrets\.[A-Za-z0-9_]+/gu)).toEqual(['secrets.DSH_APPROVAL_APP_PRIVATE_KEY'])
|
||||
expect(JSON.stringify(reviewEvent)).not.toContain('github.token')
|
||||
expect(JSON.stringify(reviewEvent)).not.toContain('secrets.')
|
||||
})
|
||||
@@ -1554,8 +1586,13 @@ describe('npm release workflows', () => {
|
||||
isRecord(step) && typeof step.run === 'string' ? [step.run] : [])
|
||||
|
||||
expect(Object.keys(workflow.on).sort()).toEqual(['pull_request', 'push', 'workflow_dispatch'])
|
||||
expect(commands).toContain('pnpm run verify-package-dependencies')
|
||||
expect(commands).toContain('pnpm run verify-npm-install-layout')
|
||||
// Both verifications run from one step so the lane pays their longer half.
|
||||
const combined = commands.find(command => command.includes('verify-package-dependencies'))
|
||||
expect(combined).toContain('verify-npm-install-layout')
|
||||
expect(combined).toContain('wait "$dependencies_pid"')
|
||||
expect(commands).not.toContain('pnpm run verify-package-dependencies')
|
||||
expect(commands).not.toContain('pnpm run verify-npm-install-layout')
|
||||
expect(String(dependencies['runs-on'])).toContain('blacksmith-4vcpu-ubuntu-2404')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1649,3 +1686,49 @@ function workflowJob(workflow: Record<string, unknown>, job: string): Record<str
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
describe('Policy workflows', () => {
|
||||
const workflows = [
|
||||
['pr-check-on-head-branch.yml', ['master']],
|
||||
['pr-check-after-approve.yml', ['approve', 'review']],
|
||||
] as const
|
||||
|
||||
function stepsOf(job: Record<string, unknown>): Record<string, unknown>[] {
|
||||
const steps = job.steps as unknown[]
|
||||
if (!Array.isArray(steps)) throw new TypeError('job must define steps')
|
||||
return steps.filter(isRecord)
|
||||
}
|
||||
|
||||
it.each(workflows)('%s runs each check from master and reads its mode from a repository variable', (file, jobs) => {
|
||||
const workflow = loadWorkflow('.github/workflows/' + file)
|
||||
expect(Object.keys(workflow).filter(key => key !== 'name')).toEqual(['on', 'permissions', 'concurrency', 'jobs'])
|
||||
const events = workflow.on
|
||||
if (!isRecord(events)) throw new TypeError(`${file} must declare events`)
|
||||
expect(events).not.toHaveProperty('pull_request')
|
||||
expect(events).toHaveProperty('pull_request_target')
|
||||
for (const job of jobs) {
|
||||
const check = stepsOf(workflowJob(workflow, job)).find(step => typeof step.uses === 'string' && step.uses.startsWith('deepseek-harness/deepseek-harness-check@'))
|
||||
if (check === undefined || !isRecord(check.env)) throw new TypeError(`${file} ${job} must run the check action with env`)
|
||||
expect(String(check.env.CHECK_MODE)).toContain('vars.MERGE_POLICY_')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the pre-merge build behind the merge step, which honours the mode on its failures', () => {
|
||||
const steps = stepsOf(workflowJob(loadWorkflow('.github/workflows/pr-check-on-head-branch.yml'), 'pre-merge'))
|
||||
const merge = steps.findIndex(step => step.id === 'merge')
|
||||
const build = steps.findIndex(step => step.name === 'Run the pre-merge command')
|
||||
expect(merge).toBeGreaterThanOrEqual(0)
|
||||
expect(build).toBeGreaterThan(merge)
|
||||
const merging = steps[merge]
|
||||
if (merging === undefined || !isRecord(merging.env)) throw new TypeError('the merge step must set env')
|
||||
expect(String(merging.env.CHECK_MODE)).toContain('MERGE_POLICY_PRE_MERGE')
|
||||
for (const step of steps.slice(merge)) {
|
||||
if (typeof step.run !== 'string') continue
|
||||
expect(step.run).toContain('"$CHECK_MODE" = block')
|
||||
}
|
||||
for (const step of steps.slice(merge + 1)) {
|
||||
if (step.name === 'Run the pre-merge command') continue
|
||||
expect(step.if).toBe("steps.merge.outputs.merged == 'true'")
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,7 @@ import { describe, expect, it } from 'vitest'
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
function clientCssDeclarations(): string[] {
|
||||
const clientGroups = ['client', 'extensions']
|
||||
const clientGroups = ['client', 'extensions', 'experimental']
|
||||
return clientGroups.flatMap((group) => {
|
||||
const clientRoot = resolve(root, 'packages', group)
|
||||
return readdirSync(clientRoot, { withFileTypes: true })
|
||||
|
||||
@@ -48,7 +48,7 @@ export function runPnpmCommand(root: string, args: readonly string[]): void {
|
||||
* @param root - Repository root containing the aggregate tsconfig.
|
||||
* @param face - Independent compiler face whose package projects are emitted.
|
||||
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
|
||||
* @returns Arguments for `process.execPath`, including the heap limit the Host face needs.
|
||||
* @returns Arguments for `process.execPath`, including an 8 GiB old-space heap limit for the Host face.
|
||||
* @throws If the aggregate configuration is invalid, has no references, or can emit files.
|
||||
*/
|
||||
export function referencedProjectsCompilerArgs(
|
||||
@@ -109,7 +109,7 @@ export function aggregateBuildArgs(face: 'host' | 'client'): string[] {
|
||||
}
|
||||
|
||||
function hostHeapArgs(face: 'host' | 'client'): string[] {
|
||||
return face === 'host' ? ['--max-old-space-size=4096'] : []
|
||||
return face === 'host' ? ['--max-old-space-size=8192'] : []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,10 +166,12 @@ function pnpmStep(id: LibraryStepId, label: string, args: readonly string[], nee
|
||||
/**
|
||||
* List the package-library build steps in Host, Desktop, then Client dependency order.
|
||||
* Source-only Typert generation supplies declarations before any package compilation.
|
||||
* Client compilation waits for Host and Desktop bundling because split compiler faces
|
||||
* rewrite shared lib/types files consumed by those bundles.
|
||||
* @param root - Repository root containing the compiler and bundle configurations.
|
||||
* @param faces - `host` lists Typert generation, Host compilation, and bundling; `all` adds Desktop and Client.
|
||||
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
|
||||
* @returns The ordered steps; each step needs its predecessor.
|
||||
* @returns Steps with explicit compiler and bundle dependencies.
|
||||
* @throws If an aggregate configuration is invalid.
|
||||
*/
|
||||
export function libraryBuildSteps(root: string, faces: 'host' | 'all', mode: CompilationMode = 'checked'): LibraryStep[] {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/** Loader resolves configuration imports in their project when Node internals are unavailable. */
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { existsSync } from 'node:fs'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { createResolve } from '../vendor/loader/src/resolve.ts'
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>()
|
||||
const mkdir = vi.fn(actual.mkdir)
|
||||
return { ...actual, mkdir, default: { ...actual, mkdir } }
|
||||
})
|
||||
|
||||
async function fixture() {
|
||||
const root = await fs.mkdtemp(join(tmpdir(), 'dsh-cordis-resolve-'))
|
||||
onTestFinished(() => fs.rm(root, { recursive: true, force: true }))
|
||||
await fs.writeFile(join(root, 'package.json'), JSON.stringify({
|
||||
name: 'fixture-app', type: 'module',
|
||||
imports: { '#local': './local.js' }, exports: { './sub': './sub.js' },
|
||||
}))
|
||||
await fs.writeFile(join(root, 'local.js'), 'export const tag = "subpath-import"\n')
|
||||
await fs.writeFile(join(root, 'sub.js'), 'export const tag = "self-reference"\n')
|
||||
const dep = join(root, 'node_modules', 'fixture-dep')
|
||||
await fs.mkdir(dep, { recursive: true })
|
||||
await fs.writeFile(join(dep, 'package.json'), JSON.stringify({
|
||||
name: 'fixture-dep', type: 'module',
|
||||
exports: { '.': { import: './index.js' }, './sub': { import: './sub.js' } },
|
||||
}))
|
||||
await fs.writeFile(join(dep, 'index.js'), 'export const tag = "bare-dep"\n')
|
||||
await fs.writeFile(join(dep, 'sub.js'), 'export const tag = "dependency-subpath"\n')
|
||||
await fs.mkdir(join(root, 'src'))
|
||||
await fs.writeFile(join(root, 'src', 'plugin.js'), 'export const tag = "relative"\n')
|
||||
return { root, baseUrl: pathToFileURL(join(root, 'src', 'cordis.yml')).href }
|
||||
}
|
||||
|
||||
async function treeFor(baseUrl: string) {
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
await ctx.plugin(Loader, { baseUrl })
|
||||
ctx.loader.internal = undefined
|
||||
return ctx.loader.root.tree
|
||||
}
|
||||
|
||||
describe('Loader project resolution', () => {
|
||||
it('loads import-only dependencies, subpaths and package mappings from the config project', async () => {
|
||||
const { root, baseUrl } = await fixture()
|
||||
const tree = await treeFor(baseUrl)
|
||||
for (const [specifier, tag] of [
|
||||
['fixture-dep', 'bare-dep'], ['fixture-dep/sub', 'dependency-subpath'],
|
||||
['#local', 'subpath-import'], ['fixture-app/sub', 'self-reference'],
|
||||
] as const) {
|
||||
expect(await tree.import(specifier)).toMatchObject({ tag })
|
||||
}
|
||||
expect(existsSync(join(root, '.cordis', 'resolve.mjs'))).toBe(true)
|
||||
expect(existsSync(join(root, 'src', '.cordis'))).toBe(false)
|
||||
expect(await fs.readFile(join(root, '.cordis', '.gitignore'), 'utf8')).toContain('*')
|
||||
await expect(tree.import('fixture-dep/missing')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('keeps project scopes independent and shares concurrent resolution within one scope', async () => {
|
||||
const first = await fixture()
|
||||
const second = await fixture()
|
||||
const [a, b, c] = await Promise.all([
|
||||
createResolve(first.baseUrl),
|
||||
createResolve(pathToFileURL(join(first.root, 'cordis.yml')).href),
|
||||
createResolve(second.baseUrl),
|
||||
])
|
||||
expect(a).toBeTypeOf('function')
|
||||
expect(a).toBe(b)
|
||||
expect(a?.('fixture-dep')).not.toBe(c?.('fixture-dep'))
|
||||
})
|
||||
|
||||
it('imports relative, URL and builtin specifiers without changing their identity', async () => {
|
||||
const { root, baseUrl } = await fixture()
|
||||
const tree = await treeFor(baseUrl)
|
||||
expect(await tree.import('./plugin.js')).toMatchObject({ tag: 'relative' })
|
||||
expect(existsSync(join(root, '.cordis'))).toBe(false)
|
||||
expect(await tree.import(pathToFileURL(join(root, 'local.js')).href)).toMatchObject({ tag: 'subpath-import' })
|
||||
expect(await tree.import('node:path')).toMatchObject({ join })
|
||||
await expect(tree.import('./missing.js')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('retains native import when helper creation is denied', async () => {
|
||||
const { root, baseUrl } = await fixture()
|
||||
const tree = await treeFor(baseUrl)
|
||||
const denied = vi.mocked(fs.mkdir).mockRejectedValueOnce(Object.assign(new Error('read-only project'), { code: 'EACCES' }))
|
||||
onTestFinished(() => { denied.mockReset() })
|
||||
expect(await createResolve(baseUrl)).toBeUndefined()
|
||||
expect(await tree.import('node:path')).toMatchObject({ join })
|
||||
await expect(tree.import('fixture-dep')).rejects.toThrow()
|
||||
expect(existsSync(join(root, '.cordis'))).toBe(false)
|
||||
})
|
||||
|
||||
it('retains native import without a file-backed package scope', async () => {
|
||||
expect(await createResolve(undefined)).toBeUndefined()
|
||||
expect(await createResolve('https://example.com/cordis.yml')).toBeUndefined()
|
||||
const tree = await treeFor('https://example.com/cordis.yml')
|
||||
expect(await tree.import('node:path')).toMatchObject({ join })
|
||||
})
|
||||
})
|
||||
@@ -59,7 +59,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
|
||||
'packages/experimental/browser-use-runtime': 'Provider-owned browser resource management and MCP integration helpers; no plugin entry.',
|
||||
'packages/boot/app-boot': 'Boot library the app bins import; plain helper exports.',
|
||||
'packages/boot/cmdline': 'Command-line library the app bins import; plain module exports.',
|
||||
'packages/boot/official-bundles': 'Bundle-list data library the launcher and plugin manager import; plain constant exports.',
|
||||
'packages/util/official-bundles': 'Bundle-list data library the launcher and plugin manager import; plain constant exports.',
|
||||
'packages/client/store': 'Browser-side state primitives; plain function/type exports.',
|
||||
'packages/client/ui-primitives': 'Browser-side UI component library; plain component exports.',
|
||||
'packages/client/ui-slots': 'Browser-side slot-map declarations; plain type exports.',
|
||||
@@ -68,6 +68,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
|
||||
'packages/experimental/webworker-packer': 'Build-time VFS image packer and command library.',
|
||||
'packages/experimental/webworker-runtime': 'Browser worker runtime library with explicit host entry points.',
|
||||
'packages/experimental/hook-protocol': 'Shared wire-protocol library between the hook bridges.',
|
||||
'packages/experimental/liveshare-protocol': 'Shared wire frames and identity contract; plain module exports with no plugin entry.',
|
||||
'packages/identity/anonymous-user-id': 'Harness-home identity helper with no plugin registration.',
|
||||
'packages/sandbox/sandbox-windows-acl': 'Windows ACL sandbox library consumed by sandbox-local.',
|
||||
'packages/sdk/client': 'Client-process library; the spawned runtime owns plugin behavior.',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/** Experimental package identities, publication policy, and retained npm names. */
|
||||
|
||||
/** Explicit npm identities retained across experimental classification and package-role moves. */
|
||||
/**
|
||||
* Experimental packages whose npm names omit the experimental prefix, by directory: identities retained across
|
||||
* experimental classification and package-role moves, and packages named without the prefix so that leaving
|
||||
* experimental status keeps their names.
|
||||
*/
|
||||
export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, string>> = {
|
||||
'packages/experimental/cua': '@deepseek-ai/dsh-cua',
|
||||
'packages/experimental/cua-bundle': '@deepseek-ai/dsh-cua-bundle',
|
||||
@@ -14,6 +18,13 @@ export const EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS: Readonly<Record<string, strin
|
||||
'packages/experimental/hooks-codex': '@deepseek-ai/dsh-hooks-codex',
|
||||
'packages/experimental/webhook': '@deepseek-ai/dsh-webhook',
|
||||
'packages/experimental/webhook-github': '@deepseek-ai/dsh-webhook-github',
|
||||
'packages/experimental/action-cards': '@deepseek-ai/dsh-action-cards',
|
||||
'packages/experimental/client-ui-action-cards': '@deepseek-ai/dsh-client-ui-action-cards',
|
||||
'packages/experimental/creation-recommendations': '@deepseek-ai/dsh-creation-recommendations',
|
||||
'packages/experimental/client-ui-creation-recommendations': '@deepseek-ai/dsh-client-ui-creation-recommendations',
|
||||
'packages/experimental/plugin-recommendations': '@deepseek-ai/dsh-plugin-recommendations',
|
||||
'packages/experimental/client-ui-plugin-recommendations': '@deepseek-ai/dsh-client-ui-plugin-recommendations',
|
||||
'packages/experimental/personalized-recommendations': '@deepseek-ai/dsh-personalized-recommendations',
|
||||
} satisfies Record<`packages/experimental/${string}`, `@deepseek-ai/dsh-${string}`>
|
||||
|
||||
/** npm namespace for experimental packages without retained-name exceptions. */
|
||||
|
||||
@@ -48,9 +48,9 @@ const MAX_DECL_CHARS = 1200
|
||||
* share that hands down a subsystem instead of a share, and prose that grew
|
||||
* into a manual. Every report also lists one line per standard prop seat of its
|
||||
* scope, so the widest Session-scope slot (`tool.call.toolview`, with the
|
||||
* global and Session access seats) sets the current 122.
|
||||
* global and Session access seats) sets the current 123.
|
||||
*/
|
||||
const MAX_ENTRY_LINES = 122
|
||||
const MAX_ENTRY_LINES = 123
|
||||
|
||||
/** One register-call option as the catalog teaches it. */
|
||||
interface OptionDoc {
|
||||
|
||||
@@ -60,6 +60,8 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
productAnalytics: 'product-telemetry.md',
|
||||
connection: 'web-server.md',
|
||||
pluginManager: 'boot.md',
|
||||
actionCards: 'boot.md',
|
||||
pluginRecommendations: 'boot.md',
|
||||
pluginMarket: 'boot.md',
|
||||
pluginRegistryProbe: 'boot.md',
|
||||
configEditor: 'boot.md',
|
||||
@@ -324,6 +326,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
PluginInstallRequestId: 'boot.md',
|
||||
PluginSpecInspection: 'boot.md',
|
||||
PluginChange: 'boot.md',
|
||||
PluginOperation: 'boot.md',
|
||||
PluginInstallLogChunk: 'boot.md',
|
||||
PluginInstallProgress: 'boot.md',
|
||||
PluginUpdateProgress: 'boot.md',
|
||||
@@ -449,6 +452,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SessionModelsRequest: 'session.md',
|
||||
SessionPage: 'session.md',
|
||||
SessionPageRequest: 'session.md',
|
||||
SessionEventsPage: 'session.md',
|
||||
SessionEventFrame: 'session.md',
|
||||
SessionObserveEventRequest: 'session.md',
|
||||
SessionProjectionFrame: 'session.md',
|
||||
SessionProjectionChange: 'session.md',
|
||||
SessionState: 'session.md',
|
||||
SessionStateRequest: 'session.md',
|
||||
SessionWaitRequest: 'session.md',
|
||||
SessionWaitValue: 'session.md',
|
||||
PendingRemoteEventInfo: 'typert.md',
|
||||
SessionPromptRequest: 'session.md',
|
||||
SessionPromptValue: 'session.md',
|
||||
SessionRenameRequest: 'session.md',
|
||||
@@ -549,6 +562,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ScheduleListRequest: 'schedule.md',
|
||||
ScheduleDeleteRequest: 'schedule.md',
|
||||
ScheduleDeleteResult: 'schedule.md',
|
||||
ScheduleDeleteInactiveResult: 'schedule.md',
|
||||
ScheduleTimingChange: 'schedule.md',
|
||||
ScheduleUpdateRequest: 'schedule.md',
|
||||
ScheduleUpdateResult: 'schedule.md',
|
||||
@@ -908,6 +922,11 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
|
||||
|
||||
/** Project types deliberately documented outside the subsystems catalog. */
|
||||
export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
ActionCardDecision: 'packages/experimental/action-cards/README.md',
|
||||
ActionCardKind: 'packages/experimental/action-cards/README.md',
|
||||
ActionCardResult: 'packages/experimental/action-cards/README.md',
|
||||
ActionCardView: 'packages/experimental/action-cards/README.md',
|
||||
RecommendationDetails: 'packages/experimental/plugin-recommendations/README.md',
|
||||
ProductEvent: 'Desktop event fields are owned by packages/client/product-analytics/README.md and src/events.ts',
|
||||
ConnectionFetchHandler: 'shared Fetch dispatch is owned by packages/client/connection/src/rpc.ts',
|
||||
ConnectionRequestRejection: 'transport rejection status is owned by packages/client/connection/src/rpc.ts',
|
||||
|
||||
@@ -107,6 +107,22 @@ const GROUP_ORDER = [
|
||||
]
|
||||
|
||||
const SERVICE_ROLES: ServiceRole[] = [
|
||||
{
|
||||
key: 'actionCards',
|
||||
pkg: 'action-cards',
|
||||
title: 'Optional action cards',
|
||||
mode: 'service',
|
||||
consumers: ['creation-recommendations', 'client-ui-action-cards'],
|
||||
note: 'Owns the card tool, card state, user actions and status notices; card kinds register their actions and handlers.',
|
||||
},
|
||||
{
|
||||
key: 'pluginRecommendations',
|
||||
pkg: 'plugin-recommendations',
|
||||
title: 'Explicit-need plugin recommendations',
|
||||
mode: 'service',
|
||||
consumers: ['client-ui-plugin-recommendations'],
|
||||
note: 'Reads the current catalog text of an official plugin card and enables the plugin through the profile plugin manager after a click.',
|
||||
},
|
||||
{
|
||||
key: 'headlessStartup',
|
||||
pkg: 'headless',
|
||||
@@ -1506,6 +1522,8 @@ function renderEventRelations(pkgs: Pkg[], events: readonly EventEntry[]): strin
|
||||
lines.push(
|
||||
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. Events are many-to-many, so the dense relation data is presented as a table rather than one large graph. Receiver and event-name types also cover contained dispatch sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.',
|
||||
'',
|
||||
'Application listeners are outside this package matrix. Desktop Host subscribes to `plugin-manager/operation` through [`installOperationDiagnostics`](../apps/desktop-host/src/operation-diagnostics.ts) and forwards bounded records to the shell.',
|
||||
'',
|
||||
)
|
||||
const rows = ['| Event | Mode | Declared in | Dispatchers | Listeners |', '| --- | --- | --- | --- | --- |']
|
||||
for (const event of [...events].sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
|
||||
@@ -4,11 +4,13 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, uti
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/official-bundles/src/index.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/util/official-bundles/src/index.ts'
|
||||
import { generateOfficialBundleCatalog, renderOfficialBundleCatalog, syncOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
|
||||
import { SHIPPED_LOCALE_IDS } from './shipped-locales.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const output = 'packages/boot/official-bundles/src/catalog.generated.ts'
|
||||
const output = 'packages/util/official-bundles/src/catalog.generated.ts'
|
||||
|
||||
function write(root: string, path: string, content: unknown): void {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true })
|
||||
@@ -32,6 +34,7 @@ function fixture(): string {
|
||||
write(root, `${dir}/locale/en.json`, { meta: { title: 'Native agent', description: 'Delegate a task.' } })
|
||||
write(root, `${dir}/locale/zh.json`, { meta: { title: '原生智能体', description: '委派任务。' } })
|
||||
write(root, `${dir}/locale/ja.json`, { meta: { title: 'ネイティブエージェント', description: 'タスクを委派します。' } })
|
||||
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) write(root, `${dir}/locale/${language}.json`, { meta: { title: 'Native agent', description: 'Delegate a task.' } })
|
||||
}
|
||||
write(root, output, '')
|
||||
return root
|
||||
@@ -50,8 +53,8 @@ describe('Official bundle catalog generation', () => {
|
||||
const entries = generateOfficialBundleCatalog(fixture())
|
||||
expect(entries.map(entry => entry.packageName)).toEqual(ON_DEMAND_BUNDLES)
|
||||
expect(entries[0]?.meta).toEqual({
|
||||
title: { en: 'Native agent', zh: '原生智能体', ja: 'ネイティブエージェント' },
|
||||
description: { en: 'Delegate a task.', zh: '委派任务。', ja: 'タスクを委派します。' },
|
||||
title: { en: 'Native agent', zh: '原生智能体', ja: 'ネイティブエージェント', ...Object.fromEntries(['es', 'fr', 'de', 'ru', 'pt'].map(id => [id, 'Native agent'])) },
|
||||
description: { en: 'Delegate a task.', zh: '委派任务。', ja: 'タスクを委派します。', ...Object.fromEntries(['es', 'fr', 'de', 'ru', 'pt'].map(id => [id, 'Delegate a task.'])) },
|
||||
icon: `data:image/svg+xml;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"/>').toString('base64')}`,
|
||||
})
|
||||
expect(renderOfficialBundleCatalog(entries)).not.toContain('1.2.3')
|
||||
@@ -67,6 +70,23 @@ describe('Official bundle catalog generation', () => {
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow(/public DSH bundle|duplicate workspace/)
|
||||
})
|
||||
|
||||
it('embeds the package platform declaration and detects changes before providers are installed', () => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
updateManifest(root, name, { dsh: { bundle: { patch: './cordis.patch.yml', platforms: ['darwin'] } } })
|
||||
expect(generateOfficialBundleCatalog(root)[0]?.platforms).toEqual(['darwin'])
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(readFileSync(join(root, output), 'utf8')).toContain("platforms: ['darwin']")
|
||||
updateManifest(root, name, { dsh: { bundle: { patch: './cordis.patch.yml', platforms: ['win32'] } } })
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).toThrow('is stale')
|
||||
})
|
||||
|
||||
it.each([[], ['macos'], 'darwin'].map(platforms => ({ platforms })))('rejects invalid package platform metadata $platforms', ({ platforms }) => {
|
||||
const root = fixture()
|
||||
updateManifest(root, ON_DEMAND_BUNDLES[0]!, { dsh: { bundle: { patch: './cordis.patch.yml', platforms } } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow('dsh.bundle.platforms')
|
||||
})
|
||||
|
||||
it.each([false, true])('rejects an on-demand provider in the installed dependency closure (indirect=%s)', (indirect) => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
@@ -81,14 +101,14 @@ describe('Official bundle catalog generation', () => {
|
||||
expect(() => generateOfficialBundleCatalog(root, [ON_DEMAND_BUNDLES[0]!, ON_DEMAND_BUNDLES[0]!])).toThrow('distinct package names')
|
||||
})
|
||||
|
||||
it.each(['icon', 'en', 'zh', 'ja'])('rejects missing %s metadata', (resource) => {
|
||||
it.each(['icon', ...SHIPPED_LOCALE_IDS])('rejects missing %s metadata', (resource) => {
|
||||
const root = fixture()
|
||||
const dir = directory(ON_DEMAND_BUNDLES[0]!)
|
||||
rmSync(join(root, dir, resource === 'icon' ? 'icon.svg' : `locale/${resource}.json`))
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow()
|
||||
})
|
||||
|
||||
it.each(['en', 'zh', 'ja'])('requires explicit %s fields instead of metadata fallback text', (language) => {
|
||||
it.each(SHIPPED_LOCALE_IDS)('requires explicit %s fields instead of metadata fallback text', (language) => {
|
||||
const root = fixture()
|
||||
write(root, `${directory(ON_DEMAND_BUNDLES[0]!)}/locale/${language}.json`, { meta: { description: 'Only a description.' } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow(`explicit ${language} title and description`)
|
||||
|
||||
@@ -4,12 +4,15 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import type { LocalizedText } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import type { OfficialBundleCatalogEntry } from '../packages/boot/official-bundles/src/index.ts'
|
||||
import type { DshBundleManifest, LocalizedText } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
|
||||
import type { OfficialBundleCatalogEntry } from '../packages/util/official-bundles/src/index.ts'
|
||||
import { readPluginMeta, resolvePluginResource } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
import { assertBundlePlatform, UnsupportedBundlePlatformError } from '../packages/boot/app-boot/src/bundle-platform.ts'
|
||||
|
||||
const OUTPUT = 'packages/boot/official-bundles/src/catalog.generated.ts'
|
||||
import { SHIPPED_LOCALE_IDS } from './shipped-locales.ts'
|
||||
|
||||
const OUTPUT = 'packages/util/official-bundles/src/catalog.generated.ts'
|
||||
|
||||
/**
|
||||
* Read complete metadata without importing or activating any provider code.
|
||||
@@ -19,7 +22,7 @@ const OUTPUT = 'packages/boot/official-bundles/src/catalog.generated.ts'
|
||||
*/
|
||||
export function generateOfficialBundleCatalog(root: string, names: readonly string[] = ON_DEMAND_BUNDLES): OfficialBundleCatalogEntry[] {
|
||||
if (names.length === 0 || new Set(names).size !== names.length) throw new Error('Official on-demand catalog must contain distinct package names')
|
||||
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; runtime: string[] }>()
|
||||
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; platforms?: DshBundleManifest['platforms']; runtime: string[] }>()
|
||||
for (const path of globSync(['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json'], { cwd: root }).map(path => path.replaceAll('\\', '/')).sort()) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as {
|
||||
name: string
|
||||
@@ -27,11 +30,12 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
dsh?: { bundle?: { patch?: unknown } }
|
||||
dsh?: { bundle?: { patch?: unknown; platforms?: DshBundleManifest['platforms'] } }
|
||||
}
|
||||
if (manifests.has(manifest.name)) throw new Error(`duplicate workspace package ${manifest.name}`)
|
||||
const runtime = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
manifests.set(manifest.name, { path, private: manifest.private === true, bundle: manifest.dsh?.bundle?.patch !== undefined,
|
||||
...manifest.dsh?.bundle?.platforms === undefined ? {} : { platforms: manifest.dsh.bundle.platforms },
|
||||
runtime: runtime.flatMap(section => Object.keys(manifest[section] ?? {})) })
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
@@ -48,8 +52,12 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
|
||||
if (entry === undefined || !entry.path.startsWith('packages/') || !packageName.startsWith('@deepseek-ai/dsh-') || entry.private || !entry.bundle) {
|
||||
throw new Error(`${packageName}: Official catalog entries must be public DSH bundle packages`)
|
||||
}
|
||||
try { assertBundlePlatform(entry.platforms, process.platform) } catch (error) {
|
||||
// Catalog generation validates every declaration, including bundles for another Host.
|
||||
if (!(error instanceof UnsupportedBundlePlatformError)) throw error
|
||||
}
|
||||
const parentURL = pathToFileURL(resolve(root, entry.path)).href
|
||||
for (const language of ['en', 'zh', 'ja']) {
|
||||
for (const language of SHIPPED_LOCALE_IDS) {
|
||||
const file = resolvePluginResource(`${packageName}/locale/${language}.json`, parentURL)
|
||||
const dictionary = JSON.parse(readFileSync(file, 'utf8')) as { meta?: { title?: unknown; description?: unknown } }
|
||||
if (typeof dictionary.meta?.title !== 'string' || dictionary.meta.title.trim() === ''
|
||||
@@ -63,13 +71,14 @@ export function generateOfficialBundleCatalog(root: string, names: readonly stri
|
||||
throw new Error(`${packageName}: Official catalog entries require an icon and localized title and description`)
|
||||
}
|
||||
for (const value of [meta.title, meta.description]) {
|
||||
for (const language of ['en', 'zh', 'ja']) {
|
||||
for (const language of SHIPPED_LOCALE_IDS) {
|
||||
if (typeof value[language] !== 'string' || value[language].trim() === '') {
|
||||
throw new Error(`${packageName}: Official metadata requires ${language} text`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { packageName, meta: { title: meta.title, description: meta.description, icon: meta.icon } }
|
||||
return { packageName, ...entry.platforms === undefined ? {} : { platforms: entry.platforms },
|
||||
meta: { title: meta.title, description: meta.description, icon: meta.icon } }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,6 +94,7 @@ export function renderOfficialBundleCatalog(entries: readonly OfficialBundleCata
|
||||
+ 'export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [\n'
|
||||
+ entries.map(entry => ' {\n'
|
||||
+ ` packageName: ${quote(entry.packageName)},\n`
|
||||
+ (entry.platforms === undefined ? '' : ` platforms: [${entry.platforms.map(quote).join(', ')}],\n`)
|
||||
+ ' meta: {\n'
|
||||
+ ` title: ${localized(entry.meta.title)},\n`
|
||||
+ ` description: ${localized(entry.meta.description)},\n`
|
||||
|
||||
@@ -43,6 +43,8 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
|
||||
import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control'
|
||||
import SkillRegistry from '@deepseek-ai/dsh-skill'
|
||||
import ActionCards from '@deepseek-ai/dsh-action-cards'
|
||||
import RemoteAccess from '@deepseek-ai/dsh-api-remote-access'
|
||||
import * as SkillFileSystem from '@deepseek-ai/dsh-skill-filesystem'
|
||||
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
|
||||
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
@@ -239,6 +241,19 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-action-cards',
|
||||
dir: 'action-cards',
|
||||
source: 'packages/experimental/action-cards/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.agents', 'ctx.sessionProjections', 'ctx.remoteAccess'],
|
||||
writes: ['tool/call', 'tool/result', 'plugin:action-cards/decision', 'user/message'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(RemoteAccess)
|
||||
await ctx.plugin(ActionCards)
|
||||
},
|
||||
note: 'Declared only to an Agent that loaded the skill of a registered card kind; the kind packages register their actions, and the Personalized recommendations bundle supplies the shipped kinds.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-ask-user',
|
||||
dir: 'tool-ask-user',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
|
||||
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
import {
|
||||
createRegistryManifest,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Gate for the invariant `FALLBACK_LOCALE` rests on: every shipped dictionary
|
||||
* declares the same keys in `zh`, `en`, and `ja`; Japanese dictionaries also
|
||||
* preserve the English literal-template placeholders.
|
||||
* declares the same keys in every shipped language; translations also
|
||||
* preserve the English placeholders, command examples, and API field names.
|
||||
*
|
||||
* The locale runtime resolves a key through the active locale, then through
|
||||
* the single fallback locale (`en`), then surfaces the key itself. With
|
||||
@@ -27,6 +27,8 @@ import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { SHIPPED_LOCALE_IDS, localeTokens, hasInvisibleLocaleCharacters, type ShippedLocaleId } from './shipped-locales.ts'
|
||||
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
/** Repo-relative path with `/` separators, so messages and suffix tests match on every OS. */
|
||||
@@ -101,7 +103,7 @@ function dictionariesIn(file: string): Dictionary[] {
|
||||
// before parsing — the silent narrowing this gate exists to prevent. A bare
|
||||
// `\b(zh|en)\b` misses `zhSettings`/`accessZh`, because `\b` does not hold
|
||||
// between `h` and an uppercase letter.
|
||||
if (!/\b(zh|en|ja)\b|\b(zh|en|ja)[A-Z]|(Zh|En|Ja)\b/.test(text)) return []
|
||||
if (!/\b(zh|en|ja|es|fr|de|ru|pt)\b|\b(zh|en|ja|es|fr|de|ru|pt)[A-Z]|(Zh|En|Ja|Es|Fr|De|Ru|Pt)\b/.test(text)) return []
|
||||
const source = ts.createSourceFile(file, text, ts.ScriptTarget.ESNext, true)
|
||||
const found: Dictionary[] = []
|
||||
const rel = relative(file)
|
||||
@@ -153,7 +155,7 @@ function dictionariesIn(file: string): Dictionary[] {
|
||||
if (name === 'register' && node.arguments.length >= 3) {
|
||||
const [ns, tag, dict] = node.arguments
|
||||
if (ns === undefined || tag === undefined || !ts.isStringLiteral(tag)) return
|
||||
if (tag.text !== 'zh' && tag.text !== 'en' && tag.text !== 'ja') return
|
||||
if (!SHIPPED_LOCALE_IDS.some(id => id === tag.text)) return
|
||||
const raw = unwrap(dict)
|
||||
const literal = raw !== undefined && ts.isIdentifier(raw)
|
||||
? (() => {
|
||||
@@ -185,7 +187,7 @@ function dictionariesIn(file: string): Dictionary[] {
|
||||
const literal = unwrap(dict)
|
||||
if (tag === undefined || !ts.isStringLiteral(tag)) continue
|
||||
if (literal === undefined || !ts.isObjectLiteralExpression(literal)) continue
|
||||
if (tag.text !== 'zh' && tag.text !== 'en' && tag.text !== 'ja') continue
|
||||
if (!SHIPPED_LOCALE_IDS.some(id => id === tag.text)) continue
|
||||
found.push({ file: rel, name: `${tag.text}@inline:${site}`, keys: keysOf(literal), templates: templatesOf(literal) })
|
||||
}
|
||||
}
|
||||
@@ -239,8 +241,8 @@ function unwrap(node: ts.Expression | undefined): ts.Expression | undefined {
|
||||
* @param name - export name or synthetic inline name.
|
||||
* @returns locale plus pair key, or undefined when the name names no locale.
|
||||
*/
|
||||
function localeOf(name: string): { locale: 'zh' | 'en' | 'ja'; pair: string } | undefined {
|
||||
for (const locale of ['zh', 'en', 'ja'] as const) {
|
||||
function localeOf(name: string): { locale: ShippedLocaleId; pair: string } | undefined {
|
||||
for (const locale of SHIPPED_LOCALE_IDS) {
|
||||
const other = locale[0]!.toUpperCase() + locale.slice(1)
|
||||
if (name === locale) return { locale, pair: '' }
|
||||
// Synthetic names for inline shapes carry their own pair key after the
|
||||
@@ -255,9 +257,9 @@ function localeOf(name: string): { locale: 'zh' | 'en' | 'ja'; pair: string } |
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that each object-form registration supplies its Japanese dictionary.
|
||||
* Check that each object-form registration supplies every shipped-language dictionary.
|
||||
* @param file - absolute source path.
|
||||
* @returns registration sites missing Japanese, or using an unresolved local identifier.
|
||||
* @returns registration sites missing a shipped language, or using an unresolved local identifier.
|
||||
*/
|
||||
function registrationProblems(file: string): string[] {
|
||||
const text = readFileSync(file, 'utf8')
|
||||
@@ -289,11 +291,13 @@ function registrationProblems(file: string): string[] {
|
||||
}
|
||||
}
|
||||
if (entries.has('zh') && entries.has('en')) {
|
||||
const ja = entries.get('ja')
|
||||
const site = `${relative(file)}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}`
|
||||
if (ja === undefined) problems.push(`${site}: registration has no Japanese dictionary`)
|
||||
else if (ts.isIdentifier(ja) && !imported.has(ja.text) && !locals.has(ja.text)) {
|
||||
problems.push(`${site}: Japanese dictionary ${ja.text} is not imported or declared`)
|
||||
for (const language of SHIPPED_LOCALE_IDS) {
|
||||
const dictionary = entries.get(language)
|
||||
if (dictionary === undefined) problems.push(`${site}: registration has no ${language} dictionary`)
|
||||
else if (ts.isIdentifier(dictionary) && !imported.has(dictionary.text) && !locals.has(dictionary.text)) {
|
||||
problems.push(`${site}: ${language} dictionary ${dictionary.text} is not imported or declared`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,11 +309,11 @@ function registrationProblems(file: string): string[] {
|
||||
}
|
||||
|
||||
describe('shipped locale dictionaries', () => {
|
||||
it('registers Japanese alongside every object-form bilingual dictionary', () => {
|
||||
it('registers every shipped language alongside every object-form bilingual dictionary', () => {
|
||||
expect(sourceFiles().flatMap(registrationProblems)).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps bilingual keys aligned and Japanese keys and placeholders complete', () => {
|
||||
it('keeps bilingual keys aligned and shipped-language keys and placeholders complete', () => {
|
||||
const files = sourceFiles()
|
||||
// Guard the discovery itself: an empty or narrowed sweep would pass every
|
||||
// assertion below while checking nothing.
|
||||
@@ -322,12 +326,18 @@ describe('shipped locale dictionaries', () => {
|
||||
const perFile = new Map<string, Dictionary[]>()
|
||||
for (const file of files) {
|
||||
const dicts = dictionariesIn(file)
|
||||
if (dicts.length > 0) perFile.set(relative(file), dicts)
|
||||
if (dicts.length > 0) {
|
||||
expect(hasInvisibleLocaleCharacters(readFileSync(file, 'utf8')), relative(file)).toBe(false)
|
||||
for (const dictionary of dicts) {
|
||||
for (const text of Object.values(dictionary.templates)) expect(hasInvisibleLocaleCharacters(text), relative(file)).toBe(false)
|
||||
}
|
||||
perFile.set(relative(file), dicts)
|
||||
}
|
||||
}
|
||||
|
||||
const groups = new Map<string, Map<'zh' | 'en' | 'ja', Dictionary>>()
|
||||
const place = (key: string, locale: 'zh' | 'en' | 'ja', dict: Dictionary): void => {
|
||||
const slot = groups.get(key) ?? new Map<'zh' | 'en' | 'ja', Dictionary>()
|
||||
const groups = new Map<string, Map<ShippedLocaleId, Dictionary>>()
|
||||
const place = (key: string, locale: ShippedLocaleId, dict: Dictionary): void => {
|
||||
const slot = groups.get(key) ?? new Map<ShippedLocaleId, Dictionary>()
|
||||
if (slot.has(locale)) {
|
||||
throw new Error(`two ${locale} dictionaries claim pair ${key}: ${slot.get(locale)?.file} and ${dict.file}`)
|
||||
}
|
||||
@@ -367,21 +377,23 @@ describe('shipped locale dictionaries', () => {
|
||||
const enOnly = en.keys.filter(k => !zh.keys.includes(k))
|
||||
if (zhOnly.length > 0) problems.push(`${zh.file} ${zh.name} has keys absent from ${en.name}: ${zhOnly.join(', ')}`)
|
||||
if (enOnly.length > 0) problems.push(`${en.file} ${en.name} has keys absent from ${zh.name}: ${enOnly.join(', ')}`)
|
||||
const ja = slot.get('ja')
|
||||
if (ja === undefined) {
|
||||
problems.push(`${key} has no Japanese dictionary`)
|
||||
} else {
|
||||
const missing = en.keys.filter(k => !ja.keys.includes(k))
|
||||
const extra = ja.keys.filter(k => !en.keys.includes(k))
|
||||
if (missing.length > 0 || extra.length > 0) {
|
||||
problems.push(`${ja.file} ${ja.name}: missing [${missing.join(', ')}], extra [${extra.join(', ')}]`)
|
||||
}
|
||||
for (const [name, text] of Object.entries(en.templates)) {
|
||||
const translated = ja.templates[name]
|
||||
if (translated === undefined) continue
|
||||
const placeholders = (value: string): string => (value.match(/\{\w+\}/gu) ?? []).sort().join(', ')
|
||||
if (placeholders(text) !== placeholders(translated)) {
|
||||
problems.push(`${ja.file} ${ja.name}.${name} changes placeholders from ${en.name}`)
|
||||
for (const language of SHIPPED_LOCALE_IDS.filter(id => id !== 'en' && id !== 'zh')) {
|
||||
const translatedDictionary = slot.get(language)
|
||||
if (translatedDictionary === undefined) {
|
||||
problems.push(`${key} has no ${language} dictionary`)
|
||||
} else {
|
||||
const missing = en.keys.filter(k => !translatedDictionary.keys.includes(k))
|
||||
const extra = translatedDictionary.keys.filter(k => !en.keys.includes(k))
|
||||
if (missing.length > 0 || extra.length > 0) {
|
||||
problems.push(`${translatedDictionary.file} ${translatedDictionary.name}: missing [${missing.join(', ')}], extra [${extra.join(', ')}]`)
|
||||
}
|
||||
for (const [name, text] of Object.entries(en.templates)) {
|
||||
const translated = translatedDictionary.templates[name]
|
||||
if (translated === undefined) continue
|
||||
const placeholders = (value: string): string => localeTokens(value).join(', ')
|
||||
if (placeholders(text) !== placeholders(translated)) {
|
||||
problems.push(`${translatedDictionary.file} ${translatedDictionary.name}.${name} changes placeholders or literal tokens from ${en.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,16 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github
|
||||
concurrency: { group: string; 'cancel-in-progress': boolean }
|
||||
jobs: Record<string, {
|
||||
'runs-on': string[]
|
||||
steps: Array<{ id?: string; name?: string; if?: string; uses?: string; run?: string; with?: Record<string, unknown> }>
|
||||
env: Record<string, string>
|
||||
steps: Array<{
|
||||
id?: string
|
||||
name?: string
|
||||
if?: string
|
||||
uses?: string
|
||||
run?: string
|
||||
env?: Record<string, string>
|
||||
with?: Record<string, unknown>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -29,6 +38,7 @@ it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silic
|
||||
expect(uploads[0]?.if).toBe("always() && steps.checkout.outcome == 'success'")
|
||||
expect(uploads[1]?.if).toBe('success()')
|
||||
expect(uploads[1]?.with?.path).toBe('${{ steps.retained.outputs.directory }}/*')
|
||||
expect(uploads[1]?.with?.['compression-level']).toBe(0)
|
||||
})
|
||||
|
||||
it('defaults to notarization and labels the explicit debugging option', () => {
|
||||
@@ -40,3 +50,21 @@ it('defaults to notarization and labels the explicit debugging option', () => {
|
||||
expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2)
|
||||
expect(steps.find(step => step.name === 'Retain installers for the selected mode')?.with?.name).toContain('${{ steps.settings.outputs.mode }}')
|
||||
})
|
||||
|
||||
it('retains build inputs outside checkout and run-specific pnpm installations', () => {
|
||||
const env = workflow.jobs.package!.env
|
||||
expect(JSON.stringify(env)).not.toContain('runner.')
|
||||
const steps = workflow.jobs.package!.steps
|
||||
const cache = steps.find(step => step.name === 'Configure persistent package caches')!
|
||||
expect(cache.env?.DSH_DESKTOP_PACKAGE_CACHE_DIR).toBe('${{ runner.tool_cache }}/dsh-desktop-package-cache')
|
||||
expect(cache.run).toContain('PNPM_CONFIG_STORE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR/workspace-pnpm-store')
|
||||
expect(cache.run).toContain('DSH_DESKTOP_PACKAGE_CACHE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR')
|
||||
expect(steps.indexOf(cache)).toBeLessThan(steps.findIndex(step => step.name === 'Install locked dependencies'))
|
||||
})
|
||||
|
||||
it('offers ZIP compression choices with a fast default', () => {
|
||||
expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { zip_compression_level: {
|
||||
type: 'choice', options: ['3', '7'], default: '3', required: true,
|
||||
} } })
|
||||
expect(workflow.jobs.package!.env.PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL).toBe('${{ inputs.zip_compression_level }}')
|
||||
})
|
||||
@@ -214,7 +214,6 @@
|
||||
"e929814f19128d947cc0188ecfe39833ffd5cf3f2c7155ddfef94ba9740d2bfd": 1
|
||||
},
|
||||
"packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts": {
|
||||
"8204ecbe96e5e470dde7a6dad4f8a0e6809c30bd8095af16a3a5d378e6c6c0c7": 4,
|
||||
"e9721557773cba96e50cea1b0c6737991a9fb2490bcd83e934311e46310d8b68": 1
|
||||
},
|
||||
"packages/api/session-controller/tests/control-queue.host.spec.ts": {
|
||||
@@ -459,9 +458,6 @@
|
||||
"packages/client/ui-commands/tests/browser-plugin.client.spec.ts": {
|
||||
"04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 1
|
||||
},
|
||||
"packages/client/ui-conversation/src/client/apply.ts": {
|
||||
"4c73f40f3ce989f285848573d7154fb295886bba707ad829e2c8c089e326aeea": 1
|
||||
},
|
||||
"packages/client/ui-conversation/src/client/conversation/location-index.ts": {
|
||||
"a7d8cc8d42e37ad5cc4882241492ac572ed8163c7fb8663180e893c7178e906f": 1
|
||||
},
|
||||
@@ -559,9 +555,6 @@
|
||||
"e01c2c40d53d557b621628de47a0a6677e11b4f151fbefd865c97be99a7b6c48": 1,
|
||||
"e69e0ec4368790661e1f7cae5c2059f757c213f0ebb0bd40ec23af982bc73d20": 1
|
||||
},
|
||||
"packages/client/ui-plan/tests/plan-resource.client.spec.ts": {
|
||||
"fa53d5cd55d87b522c83a98968e3a2bf933b1cc1b84d2f009e79d6c702099786": 1
|
||||
},
|
||||
"packages/client/ui-primitives/tests/highlight-viewport.client.spec.tsx": {
|
||||
"6061de0a1e01547eb1abf9d4e1b5fa79fda112fa35dda980775491123413dcce": 1
|
||||
},
|
||||
@@ -1278,11 +1271,8 @@
|
||||
"d423bbcd1caff7aab2e2de52564efbc9061e775620a530157805a43d851c6fe9": 1
|
||||
},
|
||||
"packages/interaction/user-approval/tests/approval.spec.ts": {
|
||||
"3261a2af17649cc35cf8698e3b7278101044ed1a07efce757615616e0f918d3a": 1,
|
||||
"4dd1f265ec710503bb27f057a582099a1e9d8b5db80fb45f148e32fd2afb5325": 2,
|
||||
"744b30e13b6390439c34f4970441a292a07f4ac3d26f25e9be75d1b5c18f4f33": 1,
|
||||
"971ec45a1146ab52f2fb13f75b782d5d35c0fb2ddba1fb9ccab14cc54f7291d5": 1,
|
||||
"a6a2f2761dfd14ff3624a7f8684b025d6aa733d41cac4381db7c83f2a38f0c35": 1,
|
||||
"f33c4a795e2cda267cb28d2937c632d56134b8e6438d21e4bf74da2735b1c154": 1
|
||||
},
|
||||
"packages/interaction/user-questions/tests/user-questions.spec.ts": {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** npm `os`/`cpu` package-field matching for build and release scripts. */
|
||||
|
||||
/**
|
||||
* Test whether an npm `os` or `cpu` field admits a value, following npm's install checks.
|
||||
* @param value - Raw manifest field; `undefined` admits every value.
|
||||
* @param current - Target platform or architecture.
|
||||
* @returns Whether the package may install for the target.
|
||||
* @throws When the field is not an array of strings.
|
||||
*/
|
||||
export function acceptsNpmPlatform(value: unknown, current: string): boolean {
|
||||
if (value === undefined) return true
|
||||
if (!Array.isArray(value) || !value.every((tag): tag is string => typeof tag === 'string')) {
|
||||
throw new Error('npm platform constraints must be arrays of strings')
|
||||
}
|
||||
let accepted = value.every(tag => tag.startsWith('!'))
|
||||
for (const tag of value) {
|
||||
if (tag === `!${current}`) return false
|
||||
if (tag === current || (tag === 'any' && value.length === 1)) accepted = true
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { loadOverlayPatches } from '../packages/boot/app-boot/src/index.ts'
|
||||
import { readPluginMeta } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
import { bundlePatchPaths, composeEntries, initProfile, readProfileManifest } from '../packages/boot/app-boot/src/profile.ts'
|
||||
import { OPTIONAL_BUNDLES, PROFILE_TEMPLATES } from '../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { OPTIONAL_BUNDLES, PROFILE_TEMPLATES } from '../packages/util/official-bundles/src/bundles.ts'
|
||||
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { createPluginProfile } from '../apps/desktop/src/project-manager.ts'
|
||||
|
||||
@@ -92,6 +92,9 @@ describe.each(['web', 'desktop'] as const)('%s optional bundle overrides', (surf
|
||||
})
|
||||
|
||||
describe('optional bundles', () => {
|
||||
it('admits Computer Use only on macOS Hosts', () => {
|
||||
expect(bundles.get('@deepseek-ai/dsh-cua-bundle')?.manifest.dsh?.bundle?.platforms).toEqual(['darwin'])
|
||||
})
|
||||
const shipped = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'].map(name => bundle(name).patches)
|
||||
|
||||
it('ships at least one bundle switched off', () => {
|
||||
|
||||
@@ -57,12 +57,15 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = {
|
||||
'@deepseek-ai/dsh-deque': ['Deque'],
|
||||
'@deepseek-ai/dsh-llm': ['callConfigEquals'],
|
||||
'@deepseek-ai/dsh-session-format': ['sessionFormatLogFilename'],
|
||||
'@deepseek-ai/dsh-storage-domain': ['defineDomain'],
|
||||
'@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
|
||||
'@deepseek-ai/schemastery': ['default'],
|
||||
} as const satisfies HostDependencyExports
|
||||
|
||||
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
|
||||
const PEER_REQUIRED_HOST_EXPORTS = {
|
||||
// Profile settings must match the installed Node provider's configuration schema.
|
||||
'@deepseek-ai/dsh-ptc-runtime-node/configuration': ['resolvePersistentExecution'],
|
||||
// CUA checks UserQuestionError and preserves AttachmentError from image admission.
|
||||
'@deepseek-ai/dsh-attachment': ['admitEncodedImages'],
|
||||
'@deepseek-ai/dsh-user-questions': ['UserQuestionError'],
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/** Worker entry: collect the source package uses of one slice of workspace packages. */
|
||||
import { parentPort, workerData } from 'node:worker_threads'
|
||||
import { readAllSourceUses } from './source-parse-uses.ts'
|
||||
import type { PackageDependencyManifest } from './verify-package-dependencies.ts'
|
||||
|
||||
/** One package whose `src` tree this worker walks. */
|
||||
interface SourceUsesTask {
|
||||
/** Position of the package in the caller's selection, used to restore that order. */
|
||||
readonly index: number
|
||||
/** Package directory relative to the repository root. */
|
||||
readonly dir: string
|
||||
/** Package name, reported back so the caller can key the result. */
|
||||
readonly name: string
|
||||
/** Workspace manifest of the package, which decides @types-versus-runtime usage. */
|
||||
readonly manifest: PackageDependencyManifest
|
||||
}
|
||||
|
||||
/** Work one worker receives when it starts. */
|
||||
export interface SourceUsesWorkerRequest {
|
||||
/** Repository root containing the workspace sources. */
|
||||
readonly root: string
|
||||
/** Packages this worker walks, in caller order. */
|
||||
readonly tasks: readonly SourceUsesTask[]
|
||||
}
|
||||
|
||||
/** Source uses collected for one package. */
|
||||
export interface SourceUsesWorkerResult {
|
||||
/** Package name of the analysed package. */
|
||||
readonly name: string
|
||||
/** Bare package name to the repository paths whose source references it. */
|
||||
readonly uses: Map<string, string[]>
|
||||
}
|
||||
|
||||
const request = workerData as SourceUsesWorkerRequest
|
||||
const results: SourceUsesWorkerResult[] = request.tasks.map(task => ({
|
||||
name: task.name,
|
||||
uses: readAllSourceUses(request.root, {
|
||||
dir: task.dir,
|
||||
manifestPath: `${task.dir}/package.json`,
|
||||
manifest: task.manifest,
|
||||
name: task.name,
|
||||
}),
|
||||
}))
|
||||
parentPort?.postMessage(results)
|
||||
@@ -10,28 +10,48 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github
|
||||
env: Record<string, string>
|
||||
jobs: Record<'preview', {
|
||||
'runs-on': string
|
||||
steps: Array<{ name?: string; uses?: string; run?: string; with?: Record<string, unknown>; env?: Record<string, string> }>
|
||||
steps: Array<{ name?: string; uses?: string; run?: string; if?: string; 'continue-on-error'?: boolean; with?: Record<string, unknown>; env?: Record<string, string> }>
|
||||
}>
|
||||
}
|
||||
const preview = workflow.jobs.preview
|
||||
|
||||
describe('PR preview workflow', () => {
|
||||
it('keeps every PR author on the selected GitHub-hosted runner', () => {
|
||||
it('keeps every PR author on the selected runner', () => {
|
||||
expect(Object.keys(workflow.jobs)).toEqual(['preview'])
|
||||
expect(preview['runs-on']).toBe('ubuntu-24.04')
|
||||
// The required outcome is the build, so the lane runs on four vCPUs; the
|
||||
// hosted label stays as the fallback when the Blacksmith pool is not chosen.
|
||||
expect(preview['runs-on']).toContain('blacksmith-4vcpu-ubuntu-2404')
|
||||
expect(preview['runs-on']).toContain("'ubuntu-24.04'")
|
||||
expect(preview['runs-on']).toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(workflow.on).toEqual({ pull_request: { types: ['opened', 'synchronize', 'reopened'] } })
|
||||
expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'write' })
|
||||
expect(preview.steps.find(step => step.uses === 'actions/[email protected]')?.with).toEqual({ 'persist-credentials': false })
|
||||
})
|
||||
|
||||
it('fails the check only for build failures and warns about publication', () => {
|
||||
const byName = (name: string) => preview.steps.find(step => step.name === name)
|
||||
for (const name of ['Build workspace and check aggregate programs', 'Build the preview page and pack the VFS image', 'Shape the upload']) {
|
||||
expect(byName(name)?.['continue-on-error'], `${name} must stay blocking`).toBeUndefined()
|
||||
}
|
||||
for (const name of ['Upload to Cloudflare Pages', 'Verify the protected deployment serves the image', 'Comment the preview URL']) {
|
||||
expect(byName(name)?.['continue-on-error'], `${name} must warn instead of failing`).toBe(true)
|
||||
}
|
||||
const report = byName('Report preview publication failures')
|
||||
expect(report?.if).toBe("steps.upload.outcome == 'failure' || steps.verify.outcome == 'failure' || steps.comment.outcome == 'failure'")
|
||||
expect(report?.run).toContain('::warning::')
|
||||
})
|
||||
|
||||
it('keeps the immutable complete artifact build and restore-only dependency cache', () => {
|
||||
expect(workflow.env.PRIMARY_NODE_VERSION).toBe('24')
|
||||
expect(workflow.env.DSH_TELEMETRY_DISABLED).toBe('1')
|
||||
const commands = preview.steps.map(step => step.run)
|
||||
expect(commands).toContain('pnpm install --frozen-lockfile')
|
||||
expect(commands).toContain('pnpm run build:artifacts')
|
||||
expect(commands).toContain('pnpm run build')
|
||||
expect(commands).toContain('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview')
|
||||
expect(commands.indexOf('pnpm run build:artifacts')).toBeLessThan(commands.indexOf('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview'))
|
||||
expect(commands.indexOf('pnpm run build')).toBeLessThan(commands.indexOf('pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview'))
|
||||
// The complete build also runs the aggregate type checks the required
|
||||
// consumers dropped.
|
||||
expect(commands).not.toContain('pnpm run build:check')
|
||||
expect(preview.steps.filter(step => step.uses?.startsWith('actions/cache'))).toHaveLength(1)
|
||||
expect(preview.steps.find(step => step.uses === 'actions/cache/[email protected]')?.with).toMatchObject({
|
||||
key: "${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, join } from 'node:path'
|
||||
import { zipSync } from 'fflate'
|
||||
import { expect, it } from 'vitest'
|
||||
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from './prepare.ts'
|
||||
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, prunePnpmForeignNativePackages, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from './prepare.ts'
|
||||
import lock from './lock.json' with { type: 'json' }
|
||||
|
||||
it('covers every SDK wheel target with the shared interpreter lock', () => {
|
||||
@@ -167,3 +167,41 @@ it('keeps carrier pnpm versions aligned with the shared payload build', () => {
|
||||
expect(desktop.devDependencies.pnpm).toBe(root.devDependencies.pnpm)
|
||||
expect(root.packageManager).toBe(`pnpm@${root.devDependencies.pnpm}`)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['darwin', 'arm64', ['reflink', 'reflink-darwin-arm64'], ['.bin', '@reflink', 'any-cpu', 'not-windows', 'semver']],
|
||||
['darwin', 'x64', ['reflink', 'reflink-darwin-x64'], ['.bin', '@reflink', 'any-cpu', 'not-windows', 'semver']],
|
||||
['win32', 'x64', ['reflink', 'reflink-win32-x64-msvc'], ['.bin', '@reflink', 'any-cpu', 'not-darwin', 'semver']],
|
||||
] as const)('keeps only pnpm-bundled packages that %s/%s admits', async (platform, arch, reflink, top) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'primary-pnpm-natives-'))
|
||||
try {
|
||||
const modules = join(root, 'dist', 'node_modules')
|
||||
const packages: Record<string, { os?: string[]; cpu?: string[] }> = {
|
||||
'semver': {}, '@reflink/reflink': {}, 'any-cpu': { cpu: ['any'] },
|
||||
'@reflink/reflink-darwin-arm64': { os: ['darwin'], cpu: ['arm64'] },
|
||||
'@reflink/reflink-darwin-x64': { os: ['darwin'], cpu: ['x64'] },
|
||||
'@reflink/reflink-win32-x64-msvc': { os: ['win32'], cpu: ['x64'] },
|
||||
'not-windows': { os: ['!win32'] },
|
||||
'not-darwin': { os: ['!darwin'] },
|
||||
'contradictory': { os: ['darwin', 'win32', '!darwin', '!win32'] },
|
||||
}
|
||||
for (const [name, fields] of Object.entries(packages)) {
|
||||
await mkdir(join(modules, name), { recursive: true })
|
||||
await writeFile(join(modules, name, 'package.json'), JSON.stringify({ name, ...fields }))
|
||||
}
|
||||
await mkdir(join(modules, '.bin'))
|
||||
await writeFile(join(modules, '.modules.yaml'), '')
|
||||
prunePnpmForeignNativePackages(root, platform, arch)
|
||||
expect((await readdir(modules)).filter(name => name !== '.modules.yaml').sort()).toEqual(top)
|
||||
expect((await readdir(join(modules, '@reflink'))).sort()).toEqual(reflink)
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
|
||||
it('names the bundled manifest whose platform field is invalid', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'primary-pnpm-invalid-'))
|
||||
try {
|
||||
await mkdir(join(root, 'dist', 'node_modules', 'invalid'), { recursive: true })
|
||||
await writeFile(join(root, 'dist', 'node_modules', 'invalid', 'package.json'), JSON.stringify({ os: 'darwin' }))
|
||||
expect(() => { prunePnpmForeignNativePackages(root, 'darwin', 'arm64') }).toThrow(/invalid.package\.json: npm platform constraints must be arrays of strings/u)
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { cp } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import extractZip from 'extract-zip'
|
||||
import { x as extractTar } from 'tar'
|
||||
import { copyPrimaryRuntime, parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
import { acceptsNpmPlatform } from '../npm-platform.ts'
|
||||
import lock from './lock.json' with { type: 'json' }
|
||||
import { prunePrimaryRuntimePythonTests } from './prune-python-tests.ts'
|
||||
|
||||
@@ -125,7 +126,7 @@ export function primaryRuntimePayloadDigest(
|
||||
// Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order.
|
||||
// Bump format when extraction or assembly changes payload bytes without changing locked inputs.
|
||||
return createHash('sha256').update(JSON.stringify({
|
||||
format: 7, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
|
||||
format: 8, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
|
||||
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
|
||||
})).digest('hex')
|
||||
}
|
||||
@@ -148,6 +149,29 @@ export async function unpackPrimaryRuntimeWheel(archive: string, destination: st
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove packages bundled directly under pnpm's dist/node_modules whose npm `os`/`cpu` fields exclude the target.
|
||||
* The pnpm tarball carries every platform's optional native packages, such as `@reflink/reflink-darwin-x64`.
|
||||
* Nested node_modules and the `libc` field are not inspected; pnpm 11 bundles neither nested native packages nor Linux variants.
|
||||
* @param pnpm - Unpacked pnpm package directory.
|
||||
* @param platform - Target Node.js platform.
|
||||
* @param arch - Target Node.js architecture.
|
||||
* @throws When a bundled package manifest is missing or has invalid `os`/`cpu` fields.
|
||||
*/
|
||||
export function prunePnpmForeignNativePackages(pnpm: string, platform: NodeJS.Platform, arch: NodeJS.Architecture): void {
|
||||
const modules = join(pnpm, 'dist', 'node_modules')
|
||||
const directories = (parent: string): string[] => readdirSync(parent, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && entry.name !== '.bin').map(entry => join(parent, entry.name))
|
||||
for (const directory of directories(modules).flatMap(entry => basename(entry).startsWith('@') ? directories(entry) : [entry])) {
|
||||
const { os, cpu } = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as { os?: unknown; cpu?: unknown }
|
||||
let admitted: boolean
|
||||
try { admitted = acceptsNpmPlatform(os, platform) && acceptsNpmPlatform(cpu, arch) } catch (error) {
|
||||
throw new Error(`primary runtime: ${directory}/package.json: ${(error as Error).message}`, { cause: error })
|
||||
}
|
||||
if (!admitted) rmSync(directory, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the skill package's complete asset tree to ordinary filesystem resources.
|
||||
* @param source - The package's assets directory.
|
||||
@@ -195,6 +219,8 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
|
||||
const output = join(staging, 'payload')
|
||||
const dependencies = join(output, 'dependencies')
|
||||
mkdirSync(dependencies, { recursive: true })
|
||||
const platform = target === 'win-x64' ? 'win32' : target.startsWith('linux-') ? 'linux' : 'darwin'
|
||||
const arch = target.endsWith('-arm64') ? 'arm64' : 'x64'
|
||||
let pnpmVersion: string | undefined
|
||||
if (!options.pythonOnly) {
|
||||
const node = join(dependencies, 'node')
|
||||
@@ -209,12 +235,13 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
|
||||
pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version
|
||||
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
|
||||
}
|
||||
prunePnpmForeignNativePackages(join(dependencies, 'pnpm'), platform, arch)
|
||||
}
|
||||
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
|
||||
const manifest: PrimaryRuntimeManifest = {
|
||||
desktopVersion: options.version,
|
||||
platform: target === 'win-x64' ? 'win32' : target.startsWith('linux-') ? 'linux' : 'darwin',
|
||||
arch: target.endsWith('-arm64') ? 'arm64' : 'x64',
|
||||
platform,
|
||||
arch,
|
||||
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpmVersion),
|
||||
python: lock.pythonVersion,
|
||||
...(pnpmVersion === undefined ? {} : { node: lock.nodeVersion, pnpm: pnpmVersion }),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { officialClientBuildEnvironment, writeClientBuildRecord } from '../client-build-environment.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/util/official-bundles/src/bundles.ts'
|
||||
import { releaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { compareVersions, nextVendorVersion, planShared, reachesPayload } from './bump.ts'
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('release families', () => {
|
||||
expect(members
|
||||
.filter(member => member.directory.startsWith('packages/experimental/'))
|
||||
.map(member => member.name)).toEqual([
|
||||
'@deepseek-ai/dsh-action-cards',
|
||||
'@deepseek-ai/dsh-experimental-agent-team-profile',
|
||||
'@deepseek-ai/dsh-experimental-agent-team',
|
||||
'@deepseek-ai/dsh-experimental-api-speech-to-text',
|
||||
@@ -59,13 +60,18 @@ describe('release families', () => {
|
||||
'@deepseek-ai/dsh-experimental-browser-use-runtime',
|
||||
'@deepseek-ai/dsh-experimental-browser-use-stagehand-native',
|
||||
'@deepseek-ai/dsh-experimental-claude-code-mods',
|
||||
'@deepseek-ai/dsh-client-ui-action-cards',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-agent-team',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-claude-code-mods',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-cot-translation',
|
||||
'@deepseek-ai/dsh-client-ui-creation-recommendations',
|
||||
'@deepseek-ai/dsh-client-ui-plugin-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-stats',
|
||||
'@deepseek-ai/dsh-experimental-client-ui-voice-input',
|
||||
'@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp',
|
||||
'@deepseek-ai/dsh-experimental-computer-use-cua-driver-native',
|
||||
'@deepseek-ai/dsh-experimental-cot-translation-bundle',
|
||||
'@deepseek-ai/dsh-creation-recommendations',
|
||||
'@deepseek-ai/dsh-cua-bundle',
|
||||
'@deepseek-ai/dsh-experimental-cua-macos',
|
||||
'@deepseek-ai/dsh-cua',
|
||||
@@ -74,7 +80,10 @@ describe('release families', () => {
|
||||
'@deepseek-ai/dsh-hooks-codex',
|
||||
'@deepseek-ai/dsh-experimental-inspector-profile',
|
||||
'@deepseek-ai/dsh-experimental-inspector',
|
||||
'@deepseek-ai/dsh-experimental-liveshare-protocol',
|
||||
'@deepseek-ai/dsh-personalized-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-plugin-market-bundle',
|
||||
'@deepseek-ai/dsh-plugin-recommendations',
|
||||
'@deepseek-ai/dsh-experimental-ptc-runtime-python',
|
||||
'@deepseek-ai/dsh-experimental-ralph-bundle',
|
||||
'@deepseek-ai/dsh-experimental-session-inspector',
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
readClientBuildRecord,
|
||||
} from '../client-build-environment.ts'
|
||||
import { validateTarballPayload } from '../publication-payload.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/util/official-bundles/src/bundles.ts'
|
||||
|
||||
/**
|
||||
* Dependency sections a consumer must publish after, because npm resolves them
|
||||
|
||||
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
import { hasExperimentalPackageReference } from '../experimental-package-policy.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../../packages/boot/official-bundles/src/bundles.ts'
|
||||
import { OPTIONAL_BUNDLES } from '../../packages/util/official-bundles/src/bundles.ts'
|
||||
|
||||
interface InstalledManifest {
|
||||
name: string
|
||||
|
||||
@@ -22,6 +22,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { acceptsNpmPlatform } from '../npm-platform.ts'
|
||||
import { releaseFamily } from './families.ts'
|
||||
import { capture, isEntry } from './process.ts'
|
||||
import { packedManifest } from './tarball.ts'
|
||||
@@ -45,19 +46,6 @@ function consumerEnvironment(consumerRoot: string): NodeJS.ProcessEnv {
|
||||
return environment
|
||||
}
|
||||
|
||||
function acceptsPlatform(value: unknown, current: string): boolean {
|
||||
if (value === undefined) return true
|
||||
if (!Array.isArray(value) || !value.every((tag): tag is string => typeof tag === 'string')) {
|
||||
throw new Error('packed platform constraints must be arrays of strings')
|
||||
}
|
||||
let accepted = value.every(tag => tag.startsWith('!'))
|
||||
for (const tag of value) {
|
||||
if (tag === `!${current}`) return false
|
||||
if (tag === current || (tag === 'any' && value.length === 1)) accepted = true
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
|
||||
/**
|
||||
* Packed tarballs and their compatibility with the verification host, as `file:` dependency entries.
|
||||
*
|
||||
@@ -77,7 +65,7 @@ function packedDependencies(directories: readonly string[]): Map<string, { url:
|
||||
const manifest = packedManifest(tarball)
|
||||
const { name, version } = manifest
|
||||
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`${tarball} manifest lacks name/version`)
|
||||
const compatible = acceptsPlatform(manifest.os, process.platform) && acceptsPlatform(manifest.cpu, process.arch)
|
||||
const compatible = acceptsNpmPlatform(manifest.os, process.platform) && acceptsNpmPlatform(manifest.cpu, process.arch)
|
||||
dependencies.set(name, { url: pathToFileURL(tarball).href, version, compatible })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
|
||||
{ file: 'apps/cli/tests/profiles/web/tests/fixtures/creator-plugin-manager.mjs', upstream: ['cordis'] },
|
||||
{ file: 'apps/web/tests/agent-preset-authoring.e2e.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/preset/agent-preset-registry/tests/session.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/experimental/creation-recommendations/src/index.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/experimental/creation-recommendations/tests/creation.spec.ts', upstream: ['cordis'] },
|
||||
{ file: 'packages/experimental/creation-recommendations/tests/loader-composition.spec.ts', upstream: ['cordis'] },
|
||||
// Preset selection, reminder policy and UI copy use the same product id.
|
||||
{ file: 'apps/cli/tests/optional-bundle-transitions.e2e.ts', upstream: ['cordis'] },
|
||||
{ file: 'apps/web/tests/agent-preset-selection.e2e.ts', upstream: ['cordis'] },
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/** Run the benchmark suite in the two phases its measurements tolerate. */
|
||||
import { resolve } from 'node:path'
|
||||
import { runPnpmCommand } from './compile-referenced-projects.ts'
|
||||
|
||||
/**
|
||||
* Cases whose wall-clock endpoints cannot absorb a sibling: they drive a
|
||||
* browser page, compare animation frames or paced streams, or spawn fresh
|
||||
* children and budget their startup. Corpus cases tolerate a sibling.
|
||||
*/
|
||||
export const SERIAL_BENCH_FILES = [
|
||||
'benchmarks/long-session-browser/long-session.bench.ts',
|
||||
'benchmarks/user-text/user-text.bench.client.ts',
|
||||
'benchmarks/conversation-fold/conversation-fold.bench.client.ts',
|
||||
'benchmarks/active-stream-reconnect/reconnect.bench.client.ts',
|
||||
// Fresh-child reopen case: its endpoints budget 63 ms and 120 ms, and a
|
||||
// sibling process pushed them to 76 ms and 131 ms.
|
||||
'benchmarks/session-open/session-open.bench.ts',
|
||||
// Request-history budgets 297 ms against a 212 ms healthy median; a sibling
|
||||
// pushed it to 342 ms.
|
||||
'benchmarks/agent-continuation/agent-continuation.bench.ts',
|
||||
] as const
|
||||
|
||||
const CONFIG = ['--config', 'vitest.bench.config.ts'] as const
|
||||
|
||||
/**
|
||||
* Run one phase of the suite.
|
||||
* @param root - Repository root containing the Vitest configuration.
|
||||
* @param phase - `first` clears the report and runs the CPU cases two at a time;
|
||||
* `last` runs the browser cases one at a time and renders the report.
|
||||
* @throws If the phase fails.
|
||||
*/
|
||||
export function runBenchPhase(root: string, phase: 'first' | 'last'): void {
|
||||
process.env.DSH_BENCH_REPORT_PHASE = phase
|
||||
try {
|
||||
if (phase === 'first') {
|
||||
runPnpmCommand(root, [
|
||||
'exec', 'vitest', 'run', ...CONFIG,
|
||||
...SERIAL_BENCH_FILES.map(file => `--exclude=${file}`),
|
||||
])
|
||||
return
|
||||
}
|
||||
runPnpmCommand(root, [
|
||||
'exec', 'vitest', 'run', ...CONFIG,
|
||||
'--no-file-parallelism', '--maxWorkers=1',
|
||||
...SERIAL_BENCH_FILES,
|
||||
])
|
||||
} finally {
|
||||
delete process.env.DSH_BENCH_REPORT_PHASE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run both phases, so a first-phase failure still reports every later case.
|
||||
* @param root - Repository root containing the Vitest configuration.
|
||||
* @returns the process exit code: 1 when either phase failed.
|
||||
*/
|
||||
export function runBenchSuite(root: string): number {
|
||||
let failed = false
|
||||
for (const phase of ['first', 'last'] as const) {
|
||||
try {
|
||||
runBenchPhase(root, phase)
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error))
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
return failed ? 1 : 0
|
||||
}
|
||||
|
||||
if (import.meta.main) process.exitCode = runBenchSuite(resolve(import.meta.dirname, '..'))
|
||||
+12
-10
@@ -282,7 +282,7 @@ describe('gate graph validation', () => {
|
||||
'npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts',
|
||||
)
|
||||
expect(scripts['build:native-system']).toBe('tsx native/system/scripts/build.ts --host-addon-only')
|
||||
expect(scripts['test:bench:built']).toBe('vitest run --config vitest.bench.config.ts')
|
||||
expect(scripts['test:bench:built']).toBe('tsx scripts/run-bench-suite.ts')
|
||||
})
|
||||
|
||||
it('checks all maintained repository references locally and in CI', () => {
|
||||
@@ -837,10 +837,12 @@ describe('Typert contract preparation', () => {
|
||||
it('reuses contracts from the validated consumer build', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
||||
|
||||
expect(subject.find(item => item.id === 'lint-and-duplication')).toMatchObject({
|
||||
displayCommand: 'pnpm run check:ci:lint:contracts-ready',
|
||||
args: ['/private/pnpm.cjs', 'run', 'check:ci:lint:contracts-ready'],
|
||||
})
|
||||
// Lint and duplication run in their own lane; the consumers graph only
|
||||
// keeps the readers that consume built artifacts.
|
||||
expect(subject.map(item => item.id)).not.toContain('lint-and-duplication')
|
||||
expect(withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready'))).toContainEqual(expect.objectContaining({
|
||||
id: 'lint',
|
||||
}))
|
||||
expect(subject.find(item => item.id === 'doc-typecheck')).toMatchObject({
|
||||
displayCommand: 'pnpm run doc-typecheck:contracts-ready',
|
||||
args: ['/private/pnpm.cjs', 'run', 'doc-typecheck:contracts-ready'],
|
||||
@@ -956,7 +958,6 @@ describe('Node 24 lane ownership', () => {
|
||||
'build',
|
||||
'node-compat',
|
||||
'publint',
|
||||
'lint-and-duplication',
|
||||
'snapshot',
|
||||
'expected-output',
|
||||
'web-snapshot',
|
||||
@@ -965,13 +966,15 @@ describe('Node 24 lane ownership', () => {
|
||||
'built-bin-smoke',
|
||||
])
|
||||
expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
|
||||
expect(subject.find(item => item.id === 'build')?.env).toEqual({
|
||||
DSH_BUILD_CLIENT_PROFILE: 'official',
|
||||
// The consumers lane emits artifacts only: the aggregate test and script
|
||||
// type checks belong to the master lanes, not to every pull request.
|
||||
expect(subject.find(item => item.id === 'build')).toMatchObject({
|
||||
displayCommand: 'pnpm run build:artifacts',
|
||||
env: { DSH_BUILD_CLIENT_PROFILE: 'official' },
|
||||
})
|
||||
expect(subject.find(item => item.id === 'node-compat')?.env).toEqual({
|
||||
DSH_BUILD_CLIENT_PROFILE: 'official',
|
||||
})
|
||||
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['build'])
|
||||
for (const id of [
|
||||
'snapshot',
|
||||
'expected-output',
|
||||
@@ -1003,7 +1006,6 @@ describe('Node 24 lane ownership', () => {
|
||||
env: { DSH_SNAPSHOT: 'replay' },
|
||||
after: [
|
||||
'publint',
|
||||
'lint-and-duplication',
|
||||
'snapshot',
|
||||
'expected-output',
|
||||
'doc-typecheck',
|
||||
|
||||
+15
-8
@@ -279,6 +279,17 @@ function ciBuildGate(id = 'build', options: Partial<Gate> = {}): Gate {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the artifacts every artifact consumer reads, without the aggregate
|
||||
* test and script type checks that the master lanes own.
|
||||
*/
|
||||
function ciArtifactBuildGate(id = 'build', options: Partial<Gate> = {}): Gate {
|
||||
return pnpmScript(id, 'build:artifacts', {
|
||||
...options,
|
||||
env: { ...options.env, [CLIENT_BUILD_PROFILE_SELECTOR]: 'official' },
|
||||
})
|
||||
}
|
||||
|
||||
function pnpmExec(id: string, args: string[], options: Partial<Gate> = {}): Gate {
|
||||
return {
|
||||
id,
|
||||
@@ -392,8 +403,8 @@ function ciSharedStaticGates(): Gate[] {
|
||||
function i18nGates(includeDictionaries: boolean): Gate[] {
|
||||
return [
|
||||
pnpmScript('installer-locales', 'verify-installer-locales', { label: 'Windows installer locales' }),
|
||||
pnpmScript('locale-metadata', 'verify-locale-metadata', { label: 'Japanese plugin metadata' }),
|
||||
...includeDictionaries ? [pnpmExec('locale-dictionaries', ['vitest', 'run', 'scripts/locale-dictionary-parity.spec.ts'], { label: 'locale dictionaries' })] : [],
|
||||
pnpmScript('locale-metadata', 'verify-locale-metadata', { label: 'plugin locale metadata' }),
|
||||
...includeDictionaries ? [pnpmExec('locale-dictionaries', ['vitest', 'run', 'scripts/locale-dictionary-parity.spec.ts', 'scripts/shipped-locales.spec.ts', 'packages/client/locale/tests/apply.client.spec.ts'], { label: 'locale dictionaries' })] : [],
|
||||
]
|
||||
}
|
||||
|
||||
@@ -530,7 +541,6 @@ function ciConsumerGates(): Gate[] {
|
||||
// writer starts; `after` preserves the web diagnostic even if a reader fails.
|
||||
const buildArtifactReaders = [
|
||||
'publint',
|
||||
'lint-and-duplication',
|
||||
'snapshot',
|
||||
'expected-output',
|
||||
'doc-typecheck',
|
||||
@@ -538,16 +548,12 @@ function ciConsumerGates(): Gate[] {
|
||||
'built-bin-smoke',
|
||||
]
|
||||
return [
|
||||
ciBuildGate(),
|
||||
ciArtifactBuildGate(),
|
||||
pnpmScript('node-compat', 'check:node-compat', {
|
||||
label: 'Node compatibility',
|
||||
env: { [CLIENT_BUILD_PROFILE_SELECTOR]: 'official' },
|
||||
}),
|
||||
pnpmScript('publint', 'publint', { needs: builtTree }),
|
||||
pnpmScript('lint-and-duplication', 'check:ci:lint:contracts-ready', {
|
||||
label: 'lint and duplication',
|
||||
needs: builtTree,
|
||||
}),
|
||||
snapshotGate(builtTree),
|
||||
expectedOutputGate(builtTree),
|
||||
webSnapshotGate(builtTree, buildArtifactReaders),
|
||||
@@ -873,6 +879,7 @@ function docSyncLeafGates(options: {
|
||||
'website/tests/mermaid-viewer.spec.ts',
|
||||
'website/tests/image-viewer.spec.ts',
|
||||
'website/tests/code-groups.spec.ts',
|
||||
'website/tests/table-filter.spec.ts',
|
||||
'website/tests/page-markdown-actions.spec.ts', 'website/tests/raw-markdown.spec.ts',
|
||||
], {
|
||||
label: 'documentation site checks',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as desktop from '../apps/desktop/src/locale.ts'
|
||||
import { readInstallerLocales } from '../apps/desktop/scripts/installer-locales.mjs'
|
||||
/** Translations preserve executable command examples and named interpolation fields. */
|
||||
import { expect, it } from 'vitest'
|
||||
import { SHIPPED_LOCALE_IDS, localeTokens, hasInvisibleLocaleCharacters } from './shipped-locales.ts'
|
||||
|
||||
it('retains repeated placeholders, commands, and API identifiers', () => {
|
||||
expect(localeTokens('Use /plan off, /compact and /THEME=dark with {count}, {count} and reasoning_effort.'))
|
||||
.toEqual(['/THEME=dark', '/compact', '/plan off', 'reasoning_effort', '{count}', '{count}'])
|
||||
})
|
||||
|
||||
it('ignores translated prose and non-command slashes', () => {
|
||||
expect(localeTokens('Cmd/Ctrl+Enter · tok/s · https://example.test/v1')).toEqual([])
|
||||
expect(localeTokens('Используйте /plan off и {count}'))
|
||||
.toEqual(localeTokens('Use /plan off and {count}'))
|
||||
})
|
||||
|
||||
it.each([['/plan off', '/plan désactivé'], ['/THEME=light', '/THEME=claro'], ['reasoning_effort', 'Reasoning_effort'], ['{count}', '{other}']])('detects a change from %s to %s', (source, translated) => {
|
||||
expect(localeTokens(translated)).not.toEqual(localeTokens(source))
|
||||
})
|
||||
|
||||
it('recognizes commands at the start of copy or after CJK punctuation', () => {
|
||||
expect(localeTokens('/THEME=auto、/THEME=light')).toEqual(['/THEME=auto', '/THEME=light'])
|
||||
})
|
||||
|
||||
it.each(['\u200b', '\ufeff'])('rejects an invisible translation character', (character) => {
|
||||
expect(hasInvisibleLocaleCharacters(`text${character}text`)).toBe(true)
|
||||
expect(hasInvisibleLocaleCharacters('normal text')).toBe(false)
|
||||
})
|
||||
|
||||
it('matches Desktop dictionaries, resolution and installer resources to the shipped catalog', () => {
|
||||
const dictionaries = Object.keys(desktop).filter(name => /^[a-z]{2}$/u.test(name)).sort()
|
||||
expect(dictionaries).toEqual([...SHIPPED_LOCALE_IDS].sort())
|
||||
for (const id of SHIPPED_LOCALE_IDS) {
|
||||
expect(desktop.resolveDesktopLocale(id).id).toBe(id === 'zh' ? 'zh-CN' : id)
|
||||
expect(desktop.resolveDesktopStartupLocale(null, [id]).id).toBe(id === 'zh' ? 'zh-CN' : id)
|
||||
}
|
||||
expect(readInstallerLocales().map(locale => locale.id.split('_')[0]).sort()).toEqual(dictionaries)
|
||||
})
|
||||
|
||||
it('rejects invisible characters in the Desktop locale source', () => {
|
||||
expect(hasInvisibleLocaleCharacters(readFileSync(new URL('../apps/desktop/src/locale.ts', import.meta.url), 'utf8'))).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
/** Languages whose UI dictionaries and package metadata ship with Harness. */
|
||||
export const SHIPPED_LOCALE_IDS = ['en', 'zh', 'ja', 'es', 'fr', 'de', 'ru', 'pt'] as const
|
||||
|
||||
/** Locale id required by repository translation checks. */
|
||||
export type ShippedLocaleId = typeof SHIPPED_LOCALE_IDS[number]
|
||||
|
||||
/**
|
||||
* Extract copy tokens that translations must preserve verbatim.
|
||||
* @param text - Dictionary text containing placeholders, slash commands or API field names.
|
||||
* @returns Sorted tokens, retaining repeated occurrences.
|
||||
*/
|
||||
export function localeTokens(text: string): string[] {
|
||||
return (text.match(/\{\w+\}|(?<![A-Za-z0-9/])\/[A-Za-z][\w-]*(?:=[\w-]+| (?:on|off))?|\b[a-z]+_[a-z_]+\b/gu) ?? []).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect translation-service artifacts that must not enter locale resources.
|
||||
* @param text - Raw resource or localized text.
|
||||
* @returns Whether the text contains a zero-width space or byte-order mark.
|
||||
*/
|
||||
export function hasInvisibleLocaleCharacters(text: string): boolean {
|
||||
return /[\u200b\ufeff]/u.test(text)
|
||||
}
|
||||
@@ -98,7 +98,7 @@
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Serial agent creation completed."
|
||||
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
@@ -1156,7 +1156,7 @@
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Serial agent creation completed."
|
||||
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
|
||||
{"type":"approval/policy","data":{"policy":"never"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"source":{"kind":"plugin:serial-created-fixture"}}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"python-snapshot-image-offload"}},"surfaceOp":"append"}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Serial agent creation completed."
|
||||
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
@@ -1628,7 +1628,7 @@
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Serial agent creation completed."
|
||||
"text": "Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
|
||||
{"type":"approval/policy","data":{"policy":"never"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"source":{"kind":"plugin:serial-created-fixture"}}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Ordinary and nested forks are independent; historical fork metadata remains readable."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"python-snapshot-image-offload"}},"surfaceOp":"append"}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/** Source-text parsing shared by the package dependency and client package gates. */
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import type { PackageDependencyManifest, WorkspacePackageManifest } from './verify-package-dependencies.ts'
|
||||
|
||||
export type DependencySection = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies'
|
||||
|
||||
export const ROOT_TYPE_PACKAGES: ReadonlySet<string> = new Set(['@types/react', '@types/react-dom'])
|
||||
|
||||
/**
|
||||
* Report whether a specifier names a bare package rather than a path or subpath import.
|
||||
* @param specifier - module specifier text.
|
||||
* @returns whether the specifier resolves outside this package.
|
||||
*/
|
||||
function isBareSpecifier(specifier: string): boolean {
|
||||
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the leading package segments of one bare module specifier.
|
||||
* @param specifier - module specifier text that already passed {@link isBareSpecifier}.
|
||||
* @returns the package name this specifier belongs to.
|
||||
*/
|
||||
function packageNameForUses(specifier: string): string {
|
||||
const segments = specifier.split('/')
|
||||
return segments.slice(0, specifier.startsWith('@') ? 2 : 1).join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the bare package name of one module specifier.
|
||||
* @param specifier - module specifier text.
|
||||
* @returns the package name, or undefined for paths, builtins, and non-package specifiers.
|
||||
*/
|
||||
export function packageNameOf(specifier: string): string | undefined {
|
||||
if (isBuiltin(specifier) || specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')
|
||||
|| specifier.includes(':') || specifier.includes('*')) {
|
||||
return undefined
|
||||
}
|
||||
const parts = specifier.split('/')
|
||||
return specifier.startsWith('@') ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined : parts[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read package manifests used for scope discovery and workspace-name checks.
|
||||
* @param root - Repository containing the workspace manifests.
|
||||
* @returns Every workspace package, the non-experimental release packages, and the packages the policy covers.
|
||||
*/
|
||||
export function addUse(target: Map<string, string[]>, name: string, path: string): void {
|
||||
const paths = target.get(name) ?? []
|
||||
if (!paths.includes(path)) paths.push(path)
|
||||
target.set(name, paths)
|
||||
}
|
||||
|
||||
export function section(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
|
||||
return manifest[name] ?? {}
|
||||
}
|
||||
|
||||
export function mutableSection(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
|
||||
manifest[name] ??= {}
|
||||
return manifest[name]
|
||||
}
|
||||
|
||||
export function declaredSections(manifest: PackageDependencyManifest, name: string): DependencySection[] {
|
||||
return (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const)
|
||||
.filter(sectionName => section(manifest, sectionName)[name] !== undefined)
|
||||
}
|
||||
|
||||
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
|
||||
const clause = node.importClause
|
||||
if (clause === undefined) return true
|
||||
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return false
|
||||
const bindings = clause.namedBindings
|
||||
return clause.name !== undefined
|
||||
|| bindings === undefined
|
||||
|| ts.isNamespaceImport(bindings)
|
||||
|| bindings.elements.length === 0
|
||||
|| bindings.elements.some(element => !element.isTypeOnly)
|
||||
}
|
||||
|
||||
function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
|
||||
if (node.isTypeOnly) return false
|
||||
const clause = node.exportClause
|
||||
if (clause === undefined || ts.isNamespaceExport(clause)) return true
|
||||
return clause.elements.length === 0 || clause.elements.some(element => !element.isTypeOnly)
|
||||
}
|
||||
|
||||
export function collectSourceFileUses(
|
||||
sourceFile: ts.SourceFile,
|
||||
runtimeOnly: boolean,
|
||||
key: 'local' | 'package' | 'specifier',
|
||||
includeRootRelative = false,
|
||||
): Set<string> {
|
||||
const uses = new Set<string>()
|
||||
|
||||
const add = (specifier: ts.Expression | undefined): void => {
|
||||
if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return
|
||||
if (key === 'local') {
|
||||
if (specifier.text.startsWith('.') || includeRootRelative && specifier.text.startsWith('/')) uses.add(specifier.text)
|
||||
return
|
||||
}
|
||||
if (!isBareSpecifier(specifier.text)) return
|
||||
uses.add(key === 'package' ? packageNameForUses(specifier.text) : specifier.text)
|
||||
}
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
if (!runtimeOnly || importCarriesRuntimeValue(node)) add(node.moduleSpecifier)
|
||||
} else if (ts.isExportDeclaration(node)) {
|
||||
if (!runtimeOnly || exportCarriesRuntimeValue(node)) add(node.moduleSpecifier)
|
||||
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
||||
if (!runtimeOnly || !node.isTypeOnly) add(node.moduleReference.expression)
|
||||
} else if (!runtimeOnly && ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
||||
add(node.argument.literal)
|
||||
} else if (ts.isCallExpression(node)
|
||||
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|
||||
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
|
||||
add(node.arguments[0])
|
||||
} else if (!runtimeOnly && key !== 'local' && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
add(node.name)
|
||||
} else if (key !== 'local'
|
||||
&& (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node))) {
|
||||
uses.add('react')
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
return uses
|
||||
}
|
||||
|
||||
/**
|
||||
* Read browser-module declarations from workspace manifests.
|
||||
* @param root - Absolute repository root.
|
||||
* @returns Declarations and malformed dsh.client fields.
|
||||
*/
|
||||
export function collectSourcePackageUses(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, false, 'package')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect bare packages whose values one production source file reaches at runtime.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Bare package names retained by runtime imports, exports, requires, or JSX.
|
||||
*/
|
||||
export function collectRuntimeSourcePackageUses(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, true, 'package')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect exact bare specifiers retained by one production source file.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Exact specifiers retained by runtime imports, exports, requires, or JSX.
|
||||
*/
|
||||
export function collectRuntimeSourceSpecifiers(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, true, 'specifier')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect relative module specifiers used to follow one source entry's local closure.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Relative imports, exports, requires, and import types.
|
||||
*/
|
||||
export function collectLocalSourceSpecifiers(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, false, 'local')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect local module specifiers retained by one production source file.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @param includeRootRelative - Include absolute paths, such as Vite's Web-root imports.
|
||||
* @returns Local imports, exports, and requires that survive compilation.
|
||||
*/
|
||||
export function collectRuntimeLocalSourceSpecifiers(path: string, source: string, includeRootRelative = false): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, true, 'local', includeRootRelative)
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect package-asset module specifiers retained by one production source file.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Relative runtime imports, re-exports, and requires whose extension has a package-asset media type.
|
||||
*/
|
||||
/**
|
||||
* Normalize a repository path to forward slashes.
|
||||
* @param path - platform path inside the repository.
|
||||
* @returns the same path with POSIX separators.
|
||||
*/
|
||||
export function normalizePath(path: string): string {
|
||||
return path.split(sep).join('/')
|
||||
}
|
||||
|
||||
export function readAllSourceUses(root: string, pkg: WorkspacePackageManifest): Map<string, string[]> {
|
||||
const uses = new Map<string, string[]>()
|
||||
for (const sourcePath of globSync('src/**/*.{ts,tsx,mts,cts}', { cwd: resolve(root, pkg.dir) }).sort()) {
|
||||
const source = readFileSync(resolve(root, pkg.dir, sourcePath), 'utf8')
|
||||
const displayPath = `${pkg.dir}/${normalizePath(sourcePath)}`
|
||||
let runtimeUses: Set<string> | undefined
|
||||
for (const specifier of collectSourcePackageUses(sourcePath, source)) {
|
||||
const name = packageNameOf(specifier)
|
||||
if (name === undefined) continue
|
||||
const typesName = `@types/${name.replace(/^@/, '').replace('/', '__')}`
|
||||
if (!ROOT_TYPE_PACKAGES.has(typesName)
|
||||
&& declaredSections(pkg.manifest, name).length === 0
|
||||
&& declaredSections(pkg.manifest, typesName).length > 0) {
|
||||
runtimeUses ??= collectRuntimeSourcePackageUses(sourcePath, source)
|
||||
if (!runtimeUses.has(name)) {
|
||||
addUse(uses, typesName, displayPath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
addUse(uses, name, displayPath)
|
||||
}
|
||||
}
|
||||
return uses
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export async function probeRestrictedWorker(
|
||||
// The probe program calls no bindings and prints nothing, so the host serves only readiness.
|
||||
const channel = createProcessRpc<ProgramMethods, Pick<HostMethods, 'ready'>>(control, 1_048_576, {
|
||||
ready() {
|
||||
void channel.run(workspace, { code, namespaces: [], maxOutputBytes: 65_536 }).then((result) => {
|
||||
void channel.run(workspace, { code, retainsState: false, namespaces: [], maxOutputBytes: 65_536 }).then((result) => {
|
||||
if (result.error !== undefined) completed.reject(new Error(JSON.stringify(result.error)))
|
||||
else completed.resolve(decodePtcJsonWire(result.value))
|
||||
channel.$close()
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/** Unit suites cannot reach the official DeepSeek model listing unless they answer `fetch` themselves. */
|
||||
import { createServer } from 'node:http'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import type { Server } from 'node:http'
|
||||
|
||||
const servers: Server[] = []
|
||||
afterEach(async () => {
|
||||
await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve))))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['string', () => fetch('https://api.deepseek.com/models')],
|
||||
['URL', () => fetch(new URL('https://api.deepseek.com/models'))],
|
||||
['Request', () => fetch(new Request('https://api.deepseek.com/models', { headers: { 'x-api-key': 'fixture-key' } }))],
|
||||
])('rejects the official model listing given as a %s', async (_kind, request) => {
|
||||
await expect(request()).rejects.toThrow('unit suites do not reach https://api.deepseek.com/models; answer it with a fetch stub')
|
||||
})
|
||||
|
||||
it('passes every other request to the ambient fetch', async () => {
|
||||
const server = createServer((_request, response) => { response.end('local') })
|
||||
servers.push(server)
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('no port')
|
||||
await expect((await fetch(`http://127.0.0.1:${address.port}/models`)).text()).resolves.toBe('local')
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Keep unit suites from requesting the official DeepSeek model listing.
|
||||
*
|
||||
* The API-key and account routes request `GET https://api.deepseek.com/models` whenever they hold a
|
||||
* credential for the official endpoint, and suites routinely boot those routes with a dummy key or
|
||||
* inherit a developer's real one. Such a request would reach production and, on a network that never
|
||||
* answers, hold the route's first catalog access for its whole listing deadline. Here it fails at once,
|
||||
* so the route serves its built-in catalog; a suite that exercises the listing replaces `fetch` itself.
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** The listing endpoint of the official DeepSeek API. */
|
||||
const OFFICIAL_LISTING = 'https://api.deepseek.com/models'
|
||||
|
||||
const ambientFetch = globalThis.fetch
|
||||
globalThis.fetch = (input, init) => {
|
||||
const url = input instanceof Request ? input.url : input instanceof URL ? input.href : input
|
||||
if (url === OFFICIAL_LISTING) {
|
||||
return Promise.reject(new TypeError(`unit suites do not reach ${OFFICIAL_LISTING}; answer it with a fetch stub`))
|
||||
}
|
||||
return ambientFetch(input, init)
|
||||
}
|
||||
@@ -139,8 +139,10 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea
|
||||
it('retains the dependency and pack verification commands', () => {
|
||||
const commands = job.steps.flatMap(step => step.run === undefined ? [] : [step.run])
|
||||
if (jobId === 'dependencies') {
|
||||
expect(commands).toContain('pnpm run verify-package-dependencies')
|
||||
expect(commands).toContain('pnpm run verify-npm-install-layout')
|
||||
// Both verifications share one step so the lane pays the longer half.
|
||||
const combined = commands.find(command => command.includes('verify-package-dependencies'))
|
||||
expect(combined).toContain('verify-npm-install-layout')
|
||||
expect(combined).toContain('wait "$layout_pid"')
|
||||
} else {
|
||||
const family = file === 'release.yml' ? 'dsh' : 'vendor'
|
||||
const output = family === 'dsh' ? 'dist/npm' : 'dist/npm-vendor'
|
||||
|
||||
@@ -342,6 +342,11 @@
|
||||
"symbol": "LegacyScheduleRecord",
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleDeleteInactiveResult",
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "LocalAtInput",
|
||||
@@ -1431,6 +1436,11 @@
|
||||
"symbol": "PtcJsonValue",
|
||||
"source": "packages/ptc-runtime/ptc-runtime/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/ptc-runtime.md",
|
||||
"symbol": "PtcProgramFormat",
|
||||
"source": "packages/ptc-runtime/ptc-runtime/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/ptc-runtime.md",
|
||||
"symbol": "PtcRunRequest",
|
||||
|
||||
@@ -15,6 +15,14 @@ import {
|
||||
} from '../packages/client/modules/src/assets.ts'
|
||||
import { TypeScriptProject } from './ts-project.ts'
|
||||
|
||||
export {
|
||||
collectLocalSourceSpecifiers,
|
||||
collectRuntimeLocalSourceSpecifiers,
|
||||
collectRuntimeSourcePackageUses,
|
||||
collectRuntimeSourceSpecifiers,
|
||||
collectSourcePackageUses,
|
||||
} from './source-parse-uses.ts'
|
||||
import { collectSourceFileUses } from './source-parse-uses.ts'
|
||||
const GATE = 'verify-client-packages'
|
||||
const CLIENT_MANIFEST_GLOB = 'packages/client/*/package.json'
|
||||
const MANIFEST_GLOBS = ['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json']
|
||||
@@ -89,62 +97,6 @@ export interface ClientDeclarations {
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Bare package names referenced by imports, declarations, or JSX.
|
||||
*/
|
||||
export function collectSourcePackageUses(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, false, 'package')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect bare packages whose values one production source file reaches at runtime.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Bare package names retained by runtime imports, exports, requires, or JSX.
|
||||
*/
|
||||
export function collectRuntimeSourcePackageUses(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, true, 'package')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect exact bare specifiers retained by one production source file.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Exact specifiers retained by runtime imports, exports, requires, or JSX.
|
||||
*/
|
||||
export function collectRuntimeSourceSpecifiers(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, true, 'specifier')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect relative module specifiers used to follow one source entry's local closure.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Relative imports, exports, requires, and import types.
|
||||
*/
|
||||
export function collectLocalSourceSpecifiers(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, false, 'local')
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect local module specifiers retained by one production source file.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @param includeRootRelative - Include absolute paths, such as Vite's Web-root imports.
|
||||
* @returns Local imports, exports, and requires that survive compilation.
|
||||
*/
|
||||
export function collectRuntimeLocalSourceSpecifiers(path: string, source: string, includeRootRelative = false): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFileUses(sourceFile, true, 'local', includeRootRelative)
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect package-asset module specifiers retained by one production source file.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Relative runtime imports, re-exports, and requires whose extension has a package-asset media type.
|
||||
*/
|
||||
export function collectSourceAssetSpecifiers(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return new Set([...collectSourceFileUses(sourceFile, true, 'local')].filter(isAssetSpecifier))
|
||||
@@ -227,73 +179,6 @@ export function readStylesheetAssetReferences(
|
||||
}
|
||||
}
|
||||
|
||||
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
|
||||
const clause = node.importClause
|
||||
if (clause === undefined) return true
|
||||
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return false
|
||||
const bindings = clause.namedBindings
|
||||
return clause.name !== undefined
|
||||
|| bindings === undefined
|
||||
|| ts.isNamespaceImport(bindings)
|
||||
|| bindings.elements.length === 0
|
||||
|| bindings.elements.some(element => !element.isTypeOnly)
|
||||
}
|
||||
|
||||
function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
|
||||
if (node.isTypeOnly) return false
|
||||
const clause = node.exportClause
|
||||
if (clause === undefined || ts.isNamespaceExport(clause)) return true
|
||||
return clause.elements.length === 0 || clause.elements.some(element => !element.isTypeOnly)
|
||||
}
|
||||
|
||||
function collectSourceFileUses(
|
||||
sourceFile: ts.SourceFile,
|
||||
runtimeOnly: boolean,
|
||||
key: 'local' | 'package' | 'specifier',
|
||||
includeRootRelative = false,
|
||||
): Set<string> {
|
||||
const uses = new Set<string>()
|
||||
|
||||
const add = (specifier: ts.Expression | undefined): void => {
|
||||
if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return
|
||||
if (key === 'local') {
|
||||
if (specifier.text.startsWith('.') || includeRootRelative && specifier.text.startsWith('/')) uses.add(specifier.text)
|
||||
return
|
||||
}
|
||||
if (!isBareSpecifier(specifier.text)) return
|
||||
uses.add(key === 'package' ? packageNameOf(specifier.text) : specifier.text)
|
||||
}
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
if (!runtimeOnly || importCarriesRuntimeValue(node)) add(node.moduleSpecifier)
|
||||
} else if (ts.isExportDeclaration(node)) {
|
||||
if (!runtimeOnly || exportCarriesRuntimeValue(node)) add(node.moduleSpecifier)
|
||||
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
||||
if (!runtimeOnly || !node.isTypeOnly) add(node.moduleReference.expression)
|
||||
} else if (!runtimeOnly && ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
||||
add(node.argument.literal)
|
||||
} else if (ts.isCallExpression(node)
|
||||
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|
||||
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
|
||||
add(node.arguments[0])
|
||||
} else if (!runtimeOnly && key !== 'local' && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
add(node.name)
|
||||
} else if (key !== 'local'
|
||||
&& (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node))) {
|
||||
uses.add('react')
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
return uses
|
||||
}
|
||||
|
||||
/**
|
||||
* Read browser-module declarations from workspace manifests.
|
||||
* @param root - Absolute repository root.
|
||||
* @returns Declarations and malformed dsh.client fields.
|
||||
*/
|
||||
export function readClientDeclarations(root: string): ClientDeclarations {
|
||||
const malformed: string[] = []
|
||||
const declarations = globSync(MANIFEST_GLOBS, { cwd: root })
|
||||
@@ -853,10 +738,6 @@ function rowPackageOf(specifier: string, rows: ReadonlySet<string>): string | un
|
||||
return rows.has(stripped) ? stripped : undefined
|
||||
}
|
||||
|
||||
function isBareSpecifier(specifier: string): boolean {
|
||||
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#')
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function findConcreteTermViolations(file: string, source: string): Concre
|
||||
}
|
||||
|
||||
function trackedFiles(repoRoot: string): string[] {
|
||||
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
|
||||
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
|
||||
.split('\0')
|
||||
.filter(file => file !== '')
|
||||
if (!files.includes('AGENTS.md')
|
||||
|
||||
@@ -13,7 +13,7 @@ const roots: string[] = []
|
||||
const experimental = '@deepseek-ai/dsh-experimental-prototype'
|
||||
const core = '@deepseek-ai/dsh-core'
|
||||
const base = '@deepseek-ai/dsh-base'
|
||||
const profile = 'packages/boot/official-bundles/src/bundles.ts'
|
||||
const profile = 'packages/util/official-bundles/src/bundles.ts'
|
||||
const preset = 'packages/bundle/web-app/presets/standard.patch.yml'
|
||||
const patch = 'packages/bundle/base/cordis.patch.yml'
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { isExperimentalPackageName } from './experimental-package-policy.ts'
|
||||
// The independently published entry package owns platform-engine dependencies.
|
||||
const EXTERNAL_KIT_PACKAGES = new Set(['@deepseek-ai/libreoffice-kit'])
|
||||
const PROFILE_SOURCE = 'packages/boot/official-bundles/src/bundles.ts'
|
||||
const PROFILE_SOURCE = 'packages/util/official-bundles/src/bundles.ts'
|
||||
const PRESET_PATTERN = 'packages/bundle/web-app/presets/*.patch.yml'
|
||||
const RUNTIME_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/** Metadata coverage rejects missing translations through the same checker used by CI. */
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { localeMetadataProblems } from './verify-locale-metadata.ts'
|
||||
|
||||
import { SHIPPED_LOCALE_IDS } from './shipped-locales.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) })
|
||||
|
||||
@@ -16,6 +18,9 @@ function fixture(ja: unknown = { meta: { title: '名前 {name}', description: '
|
||||
writeFileSync(join(directory, 'en.json'), JSON.stringify({ meta: { title: 'Name {name}' } }))
|
||||
writeFileSync(join(directory, 'zh.json'), JSON.stringify({ meta: { title: '名称 {name}', description: '描述' } }))
|
||||
writeFileSync(join(directory, 'ja.json'), JSON.stringify(ja))
|
||||
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) {
|
||||
writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta: { title: 'Name {name}', description: 'Localized description.' } }))
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -41,8 +46,8 @@ it.each([
|
||||
it('checks referenced detail documents', () => {
|
||||
const root = fixture()
|
||||
const directory = join(root, 'packages/demo/plugin/locale')
|
||||
const meta = { title: 'Name {name}', description: 'Description.', detail: 'docs/DETAIL.md' }
|
||||
for (const language of ['en', 'zh', 'ja']) writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta }))
|
||||
const meta = { title: 'Name {name}', detail: 'docs/DETAIL.md' }
|
||||
for (const language of SHIPPED_LOCALE_IDS) writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta }))
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain('missing detail resource')
|
||||
mkdirSync(join(directory, '../docs'))
|
||||
writeFileSync(join(directory, '../docs/DETAIL.md'), '# Detail\n')
|
||||
@@ -84,6 +89,7 @@ it('checks nested subpath locale resources and package-relative detail files', (
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain('locale/tools')
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain('ja.json')
|
||||
writeFileSync(join(directory, 'ja.json'), JSON.stringify({ meta: { title: 'ツール', detail: 'docs/TOOLS.md' } }))
|
||||
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) writeFileSync(join(directory, `${language}.json`), JSON.stringify({ meta: { title: 'Tools', detail: 'docs/TOOLS.md' } }))
|
||||
expect(localeMetadataProblems(root)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -97,7 +103,7 @@ it('requires sentence punctuation for English descriptions', () => {
|
||||
const file = join(root, 'packages/demo/plugin/locale/en.json')
|
||||
writeFileSync(file, JSON.stringify({ meta: { title: 'Name {name}', description: 'Description' } }))
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain('sentence punctuation')
|
||||
writeFileSync(file, JSON.stringify({ meta: { title: 'Name {name}', description: 'Description.' } }))
|
||||
writeFileSync(file, JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
|
||||
expect(localeMetadataProblems(root)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -108,6 +114,7 @@ it('permits missing Chinese resources for loadable plugins', () => {
|
||||
writeFileSync(join(directory, 'src/index.ts'), 'export function apply() {}')
|
||||
writeFileSync(join(directory, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-demo' }))
|
||||
rmSync(join(directory, 'locale/zh.json'))
|
||||
for (const language of ['es', 'fr', 'de', 'ru', 'pt']) writeFileSync(join(directory, `locale/${language}.json`), JSON.stringify({ meta: { title: 'Name {name}' } }))
|
||||
expect(localeMetadataProblems(root)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -119,3 +126,30 @@ it('excludes packages formally classified as test helpers', () => {
|
||||
writeFileSync(join(directory, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-llm-replay' }))
|
||||
expect(localeMetadataProblems(root)).toEqual([])
|
||||
})
|
||||
|
||||
it.each(['es', 'fr', 'de', 'ru', 'pt'])('rejects missing %s metadata', (language) => {
|
||||
const root = fixture()
|
||||
rmSync(join(root, `packages/demo/plugin/locale/${language}.json`))
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain(`${language}.json`)
|
||||
})
|
||||
|
||||
it.each(['es', 'fr', 'de', 'ru', 'pt'])('rejects untranslated Chinese descriptions in %s metadata', (language) => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'packages/demo/plugin/locale/en.json'), JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
|
||||
writeFileSync(join(root, `packages/demo/plugin/locale/${language}.json`), JSON.stringify({ meta: { title: 'Name {name}', description: '描述' } }))
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain(`${language}.json: meta.description contains untranslated source text`)
|
||||
})
|
||||
|
||||
it.each(['es', 'fr', 'de', 'ru', 'pt'])('rejects copied English descriptions in %s metadata', (language) => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'packages/demo/plugin/locale/en.json'), JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
|
||||
writeFileSync(join(root, `packages/demo/plugin/locale/${language}.json`), JSON.stringify({ meta: { title: 'Name {name}', description: 'English description.' } }))
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain(`${language}.json: meta.description contains untranslated source text`)
|
||||
})
|
||||
|
||||
it.each(['\u200b', '\ufeff', String.raw`\u200b`, String.raw`\ufeff`])('rejects invisible characters in JSON resources', (character) => {
|
||||
const root = fixture()
|
||||
const file = join(root, 'packages/demo/plugin/locale/es.json')
|
||||
writeFileSync(file, readFileSync(file, 'utf8').replace('Localized description.', `Localized${character} description.`))
|
||||
expect(localeMetadataProblems(root).join('\n')).toContain('invisible locale character')
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
/** Check Japanese coverage of package metadata and plugin templates without loading plugins. */
|
||||
/** Check shipped-language coverage of package metadata and plugin templates without loading plugins. */
|
||||
import { existsSync, globSync, readFileSync } from 'node:fs'
|
||||
import { basename, posix, join, resolve } from 'node:path'
|
||||
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
|
||||
import { PRODUCT_PACKAGE_POLICY } from './product-package-policy.ts'
|
||||
import { SHIPPED_LOCALE_IDS, localeTokens, hasInvisibleLocaleCharacters } from './shipped-locales.ts'
|
||||
import { collectConfigCatalog } from './gen-config-catalog.ts'
|
||||
|
||||
/** Flatten metadata strings and reject values that cannot be translated. */
|
||||
@@ -13,14 +14,14 @@ function strings(value: unknown, key = ''): Record<string, string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare Japanese metadata with every field supplied by English or Chinese.
|
||||
* Compare shipped-language metadata with every field supplied by English or Chinese.
|
||||
* @param root - repository or fixture root.
|
||||
* @returns actionable missing-file, field, placeholder and resource diagnostics.
|
||||
*/
|
||||
export function localeMetadataProblems(root: string): string[] {
|
||||
const directories = new Set(globSync('packages/**/locale/**/*.json', {
|
||||
cwd: root, exclude: ['packages/**/node_modules/**', 'packages/**/lib/**', 'packages/**/tests/**'],
|
||||
}).map(file => file.replaceAll('\\', '/')).filter(file => ['en.json', 'zh.json', 'ja.json'].includes(basename(file))).map(file => posix.dirname(file)))
|
||||
}).map(file => file.replaceAll('\\', '/')).filter(file => SHIPPED_LOCALE_IDS.some(language => basename(file) === `${language}.json`)).map(file => posix.dirname(file)))
|
||||
for (const entry of collectConfigCatalog(root)) {
|
||||
const dir = entry.dir.replaceAll('\\', '/')
|
||||
if (PRODUCT_PACKAGE_POLICY[dir]?.category === 'test') continue
|
||||
@@ -34,7 +35,9 @@ export function localeMetadataProblems(root: string): string[] {
|
||||
try {
|
||||
const read = (language: string): Record<string, string> => {
|
||||
const file = join(root, directory, `${language}.json`)
|
||||
const value: unknown = JSON.parse(readFileSync(file, 'utf8'))
|
||||
const source = readFileSync(file, 'utf8')
|
||||
if (hasInvisibleLocaleCharacters(source) || /\\u(?:200b|feff)/iu.test(source)) throw new Error(`${language}.json: invisible locale character`)
|
||||
const value: unknown = JSON.parse(source)
|
||||
const dictionary = strings(value)
|
||||
const detail = detailPath(dictionary['meta.detail'], `${directory}/${language}.json: meta.detail`)
|
||||
const packageDir = directory.slice(0, directory.lastIndexOf('/locale'))
|
||||
@@ -47,14 +50,20 @@ export function localeMetadataProblems(root: string): string[] {
|
||||
const zh = existsSync(join(root, directory, 'zh.json')) ? read('zh') : {}
|
||||
const description = en['meta.description']
|
||||
if (description !== undefined && !/[.!?]$/u.test(description)) problems.push(`${directory}/en.json: meta.description must end with sentence punctuation`)
|
||||
const ja = read('ja')
|
||||
const expected = { ...zh, ...en }
|
||||
for (const key of new Set([...Object.keys(expected), ...Object.keys(ja)])) {
|
||||
if (ja[key] === undefined) problems.push(`${directory}/ja.json: missing ${key}`)
|
||||
else if (expected[key] === undefined) problems.push(`${directory}/ja.json: unexpected ${key}`)
|
||||
else {
|
||||
const placeholders = (text: string): string => (text.match(/\{\w+\}/gu) ?? []).sort().join(',')
|
||||
if (placeholders(ja[key]) !== placeholders(expected[key])) problems.push(`${directory}/ja.json: ${key} changes placeholders`)
|
||||
for (const language of SHIPPED_LOCALE_IDS.filter(id => id !== 'en' && id !== 'zh')) {
|
||||
const translated = read(language)
|
||||
for (const key of new Set([...Object.keys(expected), ...Object.keys(translated)])) {
|
||||
if (translated[key] === undefined) problems.push(`${directory}/${language}.json: missing ${key}`)
|
||||
else if (expected[key] === undefined) problems.push(`${directory}/${language}.json: unexpected ${key}`)
|
||||
else {
|
||||
if (language !== 'ja' && key === 'meta.description' && ((zh[key] !== undefined && /\p{Script=Han}/u.test(zh[key]) && translated[key] === zh[key])
|
||||
|| (en[key] !== undefined && translated[key] === en[key]))) {
|
||||
problems.push(`${directory}/${language}.json: ${key} contains untranslated source text`)
|
||||
}
|
||||
const placeholders = (text: string): string => localeTokens(text).join(',')
|
||||
if (placeholders(translated[key]) !== placeholders(expected[key])) problems.push(`${directory}/${language}.json: ${key} changes placeholders or literal tokens`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -68,5 +77,5 @@ if (import.meta.filename === resolve(process.argv[1] ?? '')) {
|
||||
const problems = localeMetadataProblems(resolve(import.meta.dirname, '..'))
|
||||
for (const problem of problems) console.error(problem)
|
||||
if (problems.length > 0) process.exitCode = 1
|
||||
else console.log('verify-locale-metadata: all package and template metadata has complete Japanese translations.')
|
||||
else console.log('verify-locale-metadata: all package and template metadata has complete shipped-language translations.')
|
||||
}
|
||||
@@ -139,6 +139,6 @@ describe('resolution work budget', () => {
|
||||
expect(assertResolutionWorkBudget({ dshPackagesPerVersion: 300, checkedDshEdges: 2800 }))
|
||||
.toBe(840_000)
|
||||
expect(() => assertResolutionWorkBudget({ dshPackagesPerVersion: 400, checkedDshEdges: 3000 }))
|
||||
.toThrow('400 package(s) per release x 3000 internal edge(s) = 1200000 unit(s), budget 875000 unit(s)')
|
||||
.toThrow(`400 package(s) per release x 3000 internal edge(s) = 1200000 unit(s), budget ${String(MAX_RESOLUTION_WORK_UNITS)} unit(s)`)
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,7 @@ const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependen
|
||||
* measured counts instead of failing on whichever runner happens to be slow.
|
||||
* Raising it requires re-measuring the resolution and recording the new counts.
|
||||
*/
|
||||
export const MAX_RESOLUTION_WORK_UNITS = 875_000
|
||||
export const MAX_RESOLUTION_WORK_UNITS = 880_950
|
||||
|
||||
/**
|
||||
* Measured npm seconds per resolution work unit on the reference host: 180.66 s
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { Worker } from 'node:worker_threads'
|
||||
import ts from 'typescript'
|
||||
import { WorkspaceTypertGenerator } from '../packages/typert/generator/src/workspace.ts'
|
||||
import { PACKAGE_ASSET_EXPORT } from '../packages/client/modules/src/assets.ts'
|
||||
@@ -17,11 +18,22 @@ import {
|
||||
PACKAGE_DEPENDENCY_POLICY,
|
||||
type PackageDependencyPolicy,
|
||||
} from './package-dependency-policy.ts'
|
||||
import type {
|
||||
SourceUsesWorkerRequest,
|
||||
SourceUsesWorkerResult,
|
||||
} from './package-source-uses.worker.ts'
|
||||
import {
|
||||
ROOT_TYPE_PACKAGES,
|
||||
addUse,
|
||||
normalizePath,
|
||||
collectRuntimeLocalSourceSpecifiers,
|
||||
collectRuntimeSourcePackageUses,
|
||||
collectSourcePackageUses,
|
||||
} from './verify-client-packages.ts'
|
||||
declaredSections,
|
||||
mutableSection,
|
||||
packageNameOf,
|
||||
readAllSourceUses,
|
||||
section,
|
||||
type DependencySection,
|
||||
} from './source-parse-uses.ts'
|
||||
|
||||
const GATE = 'verify-package-dependencies'
|
||||
const CORDIS = '@deepseek-ai/cordis'
|
||||
@@ -29,7 +41,6 @@ const CORDIS = '@deepseek-ai/cordis'
|
||||
* Type packages declared only in the root devDependencies. Each package-local pnpm link is one more
|
||||
* location that TypeScript scans when it computes module specifiers for every JSX file.
|
||||
*/
|
||||
const ROOT_TYPE_PACKAGES: ReadonlySet<string> = new Set(['@types/react', '@types/react-dom'])
|
||||
function workspaceRange(name: string): 'workspace:*' | 'workspace:~' {
|
||||
return name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-') ? 'workspace:*' : 'workspace:~'
|
||||
}
|
||||
@@ -42,9 +53,15 @@ const WORKSPACE_MANIFEST_GLOBS = [
|
||||
'vendor/*/package.json',
|
||||
]
|
||||
|
||||
type DependencySection = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies'
|
||||
|
||||
export type PackageDependencyRole = 'client-only' | 'client-host' | 'configured-host'
|
||||
|
||||
/** One workspace package the dependency policy covers, with its resolved role. */
|
||||
export type SelectedWorkspacePackage = WorkspacePackageManifest & { readonly role: PackageDependencyRole }
|
||||
|
||||
/** Upper bound on source-walk workers: each one pays its own TypeScript module load. */
|
||||
export const MAX_DEPENDENCY_WORKERS = 16
|
||||
|
||||
/** Manifest fields read and repaired by the package dependency policy. */
|
||||
export interface PackageDependencyManifest {
|
||||
name?: string
|
||||
@@ -104,24 +121,6 @@ export interface ExpectedPackageDependency {
|
||||
readonly origins: readonly string[]
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path.split(sep).join('/')
|
||||
}
|
||||
|
||||
function packageNameOf(specifier: string): string | undefined {
|
||||
if (isBuiltin(specifier) || specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')
|
||||
|| specifier.includes(':') || specifier.includes('*')) {
|
||||
return undefined
|
||||
}
|
||||
const parts = specifier.split('/')
|
||||
return specifier.startsWith('@') ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined : parts[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read package manifests used for scope discovery and workspace-name checks.
|
||||
* @param root - Repository containing the workspace manifests.
|
||||
* @returns Every workspace package, the non-experimental release packages, and the packages the policy covers.
|
||||
*/
|
||||
export function readWorkspacePackageManifests(root: string): {
|
||||
all: WorkspacePackageManifest[]
|
||||
release: WorkspacePackageManifest[]
|
||||
@@ -211,12 +210,6 @@ export function discoverPackageDependencyScope(
|
||||
}
|
||||
}
|
||||
|
||||
function addUse(target: Map<string, string[]>, name: string, path: string): void {
|
||||
const paths = target.get(name) ?? []
|
||||
if (!paths.includes(path)) paths.push(path)
|
||||
target.set(name, paths)
|
||||
}
|
||||
|
||||
const NAMESPACE_RUNTIME_EXPORT = '*'
|
||||
const SIDE_EFFECT_RUNTIME_EXPORT = '(side effect)'
|
||||
|
||||
@@ -519,31 +512,6 @@ function readHostRuntimeUses(
|
||||
}
|
||||
}
|
||||
|
||||
function readAllSourceUses(root: string, pkg: WorkspacePackageManifest): Map<string, string[]> {
|
||||
const uses = new Map<string, string[]>()
|
||||
for (const sourcePath of globSync('src/**/*.{ts,tsx,mts,cts}', { cwd: resolve(root, pkg.dir) }).sort()) {
|
||||
const source = readFileSync(resolve(root, pkg.dir, sourcePath), 'utf8')
|
||||
const displayPath = `${pkg.dir}/${normalizePath(sourcePath)}`
|
||||
let runtimeUses: Set<string> | undefined
|
||||
for (const specifier of collectSourcePackageUses(sourcePath, source)) {
|
||||
const name = packageNameOf(specifier)
|
||||
if (name === undefined) continue
|
||||
const typesName = `@types/${name.replace(/^@/, '').replace('/', '__')}`
|
||||
if (!ROOT_TYPE_PACKAGES.has(typesName)
|
||||
&& declaredSections(pkg.manifest, name).length === 0
|
||||
&& declaredSections(pkg.manifest, typesName).length > 0) {
|
||||
runtimeUses ??= collectRuntimeSourcePackageUses(sourcePath, source)
|
||||
if (!runtimeUses.has(name)) {
|
||||
addUse(uses, typesName, displayPath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
addUse(uses, name, displayPath)
|
||||
}
|
||||
}
|
||||
return uses
|
||||
}
|
||||
|
||||
/**
|
||||
* Read authored and generated source usage for one already-classified package.
|
||||
* @param root - Repository containing source files and face tsconfigs.
|
||||
@@ -562,13 +530,14 @@ export async function readPackageDependencyFacts(
|
||||
workspaceNames: ReadonlySet<string>,
|
||||
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
|
||||
generatedHostSource?: string,
|
||||
precomputedSourceUses?: Map<string, string[]>,
|
||||
): Promise<PackageDependencyFacts> {
|
||||
const inject = pkg.manifest.dsh?.client?.inject ?? []
|
||||
const hostRuntime = role === 'client-only'
|
||||
? { packageUses: new Map<string, string[]>(), exportUses: [] }
|
||||
: readHostRuntimeUses(root, pkg, await readHostBuildEntries(root, pkg), generatedHostSource)
|
||||
const servicePeers = policy.requiredServicePeers?.[pkg.name] ?? []
|
||||
const allSourceUses = readAllSourceUses(root, pkg)
|
||||
const allSourceUses = precomputedSourceUses ?? readAllSourceUses(root, pkg)
|
||||
for (const name of servicePeers) {
|
||||
if (!workspaceNames.has(name) || !allSourceUses.has(name)) {
|
||||
throw new Error(`${pkg.manifestPath}: requiredServicePeers must name a referenced workspace package: ${name}`)
|
||||
@@ -676,10 +645,7 @@ export async function readPackageDependencyState(
|
||||
const workspaceNames = new Set(packages.all.map(pkg => pkg.name))
|
||||
const discovered = discoverPackageDependencyScope(packages.policy, policy)
|
||||
const generated = generatedHostSources(root, discovered.selected.filter(pkg => pkg.role !== 'client-only'))
|
||||
const facts: PackageDependencyFacts[] = []
|
||||
for (const pkg of discovered.selected) {
|
||||
facts.push(await readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy, generated.get(pkg.name)))
|
||||
}
|
||||
const facts = await readPackageDependencyFactsConcurrently(root, discovered.selected, workspaceNames, policy, generated)
|
||||
const selectedNames = new Set(facts.map(fact => fact.manifest.name))
|
||||
return {
|
||||
facts,
|
||||
@@ -696,6 +662,117 @@ export async function readPackageDependencyState(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read dependency facts for every selected package, walking package sources in parallel.
|
||||
*
|
||||
* Walking a package's `src` tree with the TypeScript parser is the expensive part of
|
||||
* this gate and shares no state across packages, so those walks run in worker threads
|
||||
* while the caller's package order is preserved for the policy checks.
|
||||
* @param root - Repository containing the workspace sources and build configs.
|
||||
* @param packages - Selected packages, in policy order.
|
||||
* @param workspaceNames - Identities of every workspace package.
|
||||
* @param policy - Reviewed scope overrides and Host export classifications.
|
||||
* @param generated - Host modules the batched Typert pass emitted in memory, keyed by package name.
|
||||
* @param workers - Worker count for the source walk; defaults to the host CPU count, capped at {@link MAX_DEPENDENCY_WORKERS}.
|
||||
* @returns Facts in the same order as `packages`.
|
||||
*/
|
||||
export async function readPackageDependencyFactsConcurrently(
|
||||
root: string,
|
||||
packages: readonly SelectedWorkspacePackage[],
|
||||
workspaceNames: ReadonlySet<string>,
|
||||
policy: PackageDependencyPolicy,
|
||||
generated: ReadonlyMap<string, string>,
|
||||
workers = Math.min(availableParallelism(), packages.length, MAX_DEPENDENCY_WORKERS),
|
||||
): Promise<PackageDependencyFacts[]> {
|
||||
const sourceUses = await readSourceUses(root, packages, workers)
|
||||
const facts: PackageDependencyFacts[] = []
|
||||
for (const pkg of packages) {
|
||||
facts.push(await readPackageDependencyFacts(
|
||||
root,
|
||||
pkg,
|
||||
pkg.role,
|
||||
workspaceNames,
|
||||
policy,
|
||||
generated.get(pkg.name),
|
||||
sourceUses.get(pkg.dir),
|
||||
))
|
||||
}
|
||||
return facts
|
||||
}
|
||||
|
||||
/** Source uses per package directory, collected either in-process or by worker threads. */
|
||||
async function readSourceUses(
|
||||
root: string,
|
||||
packages: readonly SelectedWorkspacePackage[],
|
||||
workers: number,
|
||||
): Promise<Map<string, Map<string, string[]>>> {
|
||||
if (workers < 2 || packages.length < 2) {
|
||||
return new Map(packages.map(pkg => [pkg.dir, readAllSourceUses(root, pkg)]))
|
||||
}
|
||||
const sizes = sliceSizes(packages.length, workers)
|
||||
const tasks = packages.map((pkg, index) => ({ index, dir: pkg.dir, name: pkg.name, manifest: pkg.manifest }))
|
||||
const slices: typeof tasks[] = []
|
||||
let offset = 0
|
||||
for (const size of sizes) {
|
||||
slices.push(tasks.slice(offset, offset + size))
|
||||
offset += size
|
||||
}
|
||||
const collected = (await Promise.all(slices.map(async slice => runSourceUsesWorker(root, slice)))).flat()
|
||||
const byName = new Map(collected.map(result => [result.name, result.uses]))
|
||||
return new Map(packages.map(pkg => [pkg.dir, byName.get(pkg.name) ?? new Map<string, string[]>()]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one slice of packages in a worker thread.
|
||||
* @param root - Repository containing the workspace sources.
|
||||
* @param tasks - Packages this worker walks.
|
||||
* @returns one result per task.
|
||||
* @throws If the worker crashes or exits before reporting.
|
||||
*/
|
||||
function runSourceUsesWorker(root: string, tasks: readonly SourceUsesTaskForWorker[]): Promise<SourceUsesWorkerResult[]> {
|
||||
const request: SourceUsesWorkerRequest = { root, tasks }
|
||||
const url = new URL('./package-source-uses.worker.ts', import.meta.url)
|
||||
return new Promise((resolveResults, rejectResults) => {
|
||||
const worker = new Worker(url, { workerData: request })
|
||||
let settled = false
|
||||
worker.once('message', (results: SourceUsesWorkerResult[]) => {
|
||||
settled = true
|
||||
void worker.terminate()
|
||||
resolveResults(results)
|
||||
})
|
||||
worker.once('error', (error) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
rejectResults(error)
|
||||
})
|
||||
worker.once('exit', (code) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
rejectResults(new Error(`${GATE}: source-uses worker exited with ${String(code)} before reporting`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
interface SourceUsesTaskForWorker {
|
||||
readonly index: number
|
||||
readonly dir: string
|
||||
readonly name: string
|
||||
readonly manifest: PackageDependencyManifest
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute a task count over a worker count as evenly as possible.
|
||||
* @param tasks - Number of tasks to place.
|
||||
* @param workers - Number of workers that must stay busy.
|
||||
* @returns One slice size per non-empty slice.
|
||||
*/
|
||||
export function sliceSizes(tasks: number, workers: number): number[] {
|
||||
const slices = Math.min(tasks, Math.max(1, workers))
|
||||
const base = Math.floor(tasks / slices)
|
||||
const remainder = tasks % slices
|
||||
return Array.from({ length: slices }, (_, index) => base + (index < remainder ? 1 : 0))
|
||||
}
|
||||
|
||||
/** Derive the required npm section for each relationship owned by the policy. */
|
||||
export function expectedPackageDependencies(
|
||||
facts: PackageDependencyFacts,
|
||||
@@ -790,20 +867,6 @@ export function formatPeerRequiredRuntimeDependencies(state: PackageDependencySt
|
||||
]
|
||||
}
|
||||
|
||||
function section(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
|
||||
return manifest[name] ?? {}
|
||||
}
|
||||
|
||||
function mutableSection(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
|
||||
manifest[name] ??= {}
|
||||
return manifest[name]
|
||||
}
|
||||
|
||||
function declaredSections(manifest: PackageDependencyManifest, name: string): DependencySection[] {
|
||||
return (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const)
|
||||
.filter(sectionName => section(manifest, sectionName)[name] !== undefined)
|
||||
}
|
||||
|
||||
function describeSections(sections: readonly DependencySection[]): string {
|
||||
return sections.length === 0 ? 'no dependency section' : sections.join(' + ')
|
||||
}
|
||||
|
||||
@@ -33,12 +33,13 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
'packages/fs/fs-patch': 'The pure library only parses caller-supplied patch text and applies hunks to text; consumers own tools, execution, capture, and model output.',
|
||||
'packages/util/file-drag': 'The package resolves export names and declares native file operations; it does not construct model input.',
|
||||
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
|
||||
'packages/experimental/liveshare-protocol': 'The package only defines wire frames and identity values that its consumers exchange; it registers nothing model-facing.',
|
||||
'packages/util/brand': 'The package only constructs plain string values and registers nothing model-facing.',
|
||||
'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
|
||||
'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
|
||||
'packages/util/workspace-path': 'The package only formats Workspace paths for browser UI; it never constructs model input.',
|
||||
'packages/util/values': 'The package only validates, snapshots, compares, freezes, or rejects caller-owned values; consumers own every model-facing use.',
|
||||
'packages/boot/official-bundles': 'The package only lists bundle package names and their display metadata; the loaded plugin tree owns model context.',
|
||||
'packages/util/official-bundles': 'The package only lists bundle package names and their display metadata; the loaded plugin tree owns model context.',
|
||||
'packages/util/code-language': 'The package only maps a filename suffix to a syntax-highlighting language id; the read consumer owns the persisted hint.',
|
||||
}
|
||||
|
||||
@@ -57,6 +58,10 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/experimental/speech-to-text-sensevoice': { kind: 'none', reason: 'Local recognition returns transient text without modifying model context.' },
|
||||
'packages/experimental/client-ui-voice-input': { kind: 'none', reason: 'Inserts reviewable text into the unsent draft without submitting to the Agent.' },
|
||||
'packages/experimental/plugin-market-bundle': { kind: 'indirect', reason: 'Mounts the pluginMarket service, which lets the Creator preset register the plugin_market tool documented in packages/boot/plugin-market.' },
|
||||
'packages/experimental/client-ui-action-cards': { kind: 'indirect', reason: 'Card actions go to the Host card service, which owns the logged tool, decisions and status notice.' },
|
||||
'packages/experimental/client-ui-creation-recommendations': { kind: 'indirect', reason: 'The page card body starts a Creator through the Host page kind; the Host owns its logged task.' },
|
||||
'packages/experimental/client-ui-plugin-recommendations': { kind: 'indirect', reason: 'The plugin card body enables through the Host plugin kind; the Host card service owns the logged tool, decisions and status notice.' },
|
||||
'packages/experimental/personalized-recommendations': { kind: 'indirect', reason: 'The bundle composes the card service and both card kinds; their packages own the skills, the logged tool and the status notice.' },
|
||||
'packages/experimental/voice-input-bundle': { kind: 'none', reason: 'Composes dictation and preparation plugins without adding any model-facing contribution.' },
|
||||
'packages/telemetry/otel': { kind: 'none', reason: 'The service delivers caller-selected records without changing model context.' },
|
||||
'packages/host/product-telemetry-otel': { kind: 'none', reason: 'The plugin exports explicit analytics records without contributing model context.' },
|
||||
@@ -64,6 +69,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/client/ui-sidebar-terminal': { kind: 'none', reason: 'The browser renders user terminal screens without exposing them to the model.' },
|
||||
'packages/ssh/ssh-helper-runtime': { kind: 'none', reason: 'The private executable carrier registers no model-facing content; its providers and consumers own operation results.' },
|
||||
'packages/experimental/worktree': { kind: 'indirect', reason: 'The worktree tool renders creation results; the working-directory service owns model-visible directory context.' },
|
||||
'packages/experimental/client-ui-stats': { kind: 'none', reason: 'The panel reads Session events and renders them in the browser; it registers no tool, prompt section, or model input.' },
|
||||
'packages/ssh/ssh': { kind: 'none', reason: 'The connection owner transports private provider operations; consumers own all model-facing content.' },
|
||||
'packages/ssh/fs-ssh': { kind: 'indirect', reason: 'The remote filesystem delegates model rendering to the existing filesystem consumers.' },
|
||||
'packages/ssh/subprocess-ssh': { kind: 'indirect', reason: 'The remote process provider delegates model rendering to Bash, terminal, LSP and ptc-runtime consumers.' },
|
||||
|
||||
@@ -13,7 +13,7 @@ const base = '@deepseek-ai/dsh-base'
|
||||
const core = '@deepseek-ai/dsh-core'
|
||||
const candidate = '@deepseek-ai/dsh-candidate'
|
||||
const candidateDir = 'packages/core/candidate'
|
||||
const profile = 'packages/boot/official-bundles/src/bundles.ts'
|
||||
const profile = 'packages/util/official-bundles/src/bundles.ts'
|
||||
const patch = 'packages/bundle/base/cordis.patch.yml'
|
||||
|
||||
function pkg(root: string, directory: string, name: string, fields: Record<string, unknown> = {}): void {
|
||||
@@ -141,7 +141,7 @@ describe('product package use', () => {
|
||||
const root = fixture()
|
||||
pkg(root, candidateDir, candidate)
|
||||
const children = [{ name: candidate }]
|
||||
if (!initial) write(root, 'packages/boot/official-bundles/src/included.yml', children)
|
||||
if (!initial) write(root, 'packages/util/official-bundles/src/included.yml', children)
|
||||
write(root, patch, [{ insert: [{ name: core }, {
|
||||
name: 'cordis:include', disabled: true, config: { path: './included.yml', ...(initial ? { initial: children } : {}) },
|
||||
}] }])
|
||||
@@ -161,12 +161,12 @@ describe('product package use', () => {
|
||||
it('applies Include patches before collecting runtime mounts and uses files ahead of initial entries', () => {
|
||||
const root = fixture()
|
||||
pkg(root, candidateDir, candidate)
|
||||
write(root, 'packages/boot/official-bundles/src/included.yml', [{ id: 'child', name: candidate }])
|
||||
write(root, 'packages/util/official-bundles/src/included.yml', [{ id: 'child', name: candidate }])
|
||||
write(root, patch, [{ insert: [{ name: core }, { name: 'cordis:include', config: {
|
||||
path: './included.yml', initial: [{ name: candidate }], patches: [{ id: 'child', disabled: true }],
|
||||
} }] }])
|
||||
expect(failures(root)).toContain('no product runtime use')
|
||||
write(root, 'packages/boot/official-bundles/src/included.yml', [])
|
||||
write(root, 'packages/util/official-bundles/src/included.yml', [])
|
||||
expect(failures(root)).toContain('no product runtime use')
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { isAgentPresetEntry, isCordisGroupEntry, loadCordisYaml } from './cordis
|
||||
import { PRODUCT_PACKAGE_POLICY, type ProductPackagePolicy } from './product-package-policy.ts'
|
||||
import { collectRuntimeLocalSourceSpecifiers, collectRuntimeSourceSpecifiers } from './verify-client-packages.ts'
|
||||
|
||||
const PROFILE_SOURCE = 'packages/boot/official-bundles/src/bundles.ts'
|
||||
const PROFILE_SOURCE = 'packages/util/official-bundles/src/bundles.ts'
|
||||
const PROFILES = ['acp', 'web', 'headless', 'sdk', 'sdk-minimal']
|
||||
const PICKER_SOURCE = 'packages/host/directory-picker-auto/src/index.ts'
|
||||
const DYNAMIC_MOUNTS: Readonly<Record<string, readonly string[]>> = {
|
||||
|
||||
@@ -66,7 +66,7 @@ export function findUnavailableRepositoryReferences(file: string, source: string
|
||||
}
|
||||
|
||||
function trackedFiles(repoRoot: string): string[] {
|
||||
return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
|
||||
return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
|
||||
.split('\0')
|
||||
.filter(file => file !== '')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { prepareWindowsBuildWorkspace, removeWindowsBuildWorkspace } from '../apps/desktop/scripts/windows-build-workspace.mjs'
|
||||
|
||||
it('clones the exact clean commit and removes only its run-owned checkout', () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'package-workspace-')))
|
||||
try {
|
||||
const sourceDirectory = join(root, 'original')
|
||||
mkdirSync(sourceDirectory)
|
||||
const git = (...args: string[]) => execFileSync('git', args, { cwd: sourceDirectory, encoding: 'utf8', stdio: 'pipe' }).trim()
|
||||
git('init')
|
||||
writeFileSync(join(sourceDirectory, 'source.txt'), 'selected source')
|
||||
writeFileSync(join(sourceDirectory, '.gitignore'), 'private.env\n')
|
||||
git('add', '.')
|
||||
git('-c', 'user.name=Fixture', '-c', '[email protected]', 'commit', '-m', 'fixture')
|
||||
writeFileSync(join(sourceDirectory, 'private.env'), 'ignored fixture')
|
||||
const options = { sourceDirectory, buildRoot: join(root, 'builds'), runId: '123', runAttempt: '1', sourceSha: git('rev-parse', 'HEAD') }
|
||||
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: sourceDirectory })).toThrow('outside existing Git')
|
||||
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: 'relative' })).toThrow('absolute')
|
||||
expect(() => prepareWindowsBuildWorkspace({ ...options, runId: '../other' })).toThrow('identity')
|
||||
expect(() => prepareWindowsBuildWorkspace({ ...options, sourceSha: '0'.repeat(40) })).toThrow('match')
|
||||
writeFileSync(join(sourceDirectory, 'source.txt'), 'dirty')
|
||||
expect(() => prepareWindowsBuildWorkspace(options)).toThrow('clean')
|
||||
writeFileSync(join(sourceDirectory, 'source.txt'), 'selected source')
|
||||
const output = join(root, 'actions-output')
|
||||
execFileSync(process.execPath, [resolve(import.meta.dirname, '../apps/desktop/scripts/windows-build-workspace.mjs')], {
|
||||
cwd: sourceDirectory,
|
||||
env: {
|
||||
...process.env,
|
||||
GITHUB_WORKSPACE: sourceDirectory,
|
||||
PACKAGE_BUILD_ROOT: options.buildRoot,
|
||||
GITHUB_RUN_ID: options.runId,
|
||||
GITHUB_RUN_ATTEMPT: options.runAttempt,
|
||||
PACKAGE_SOURCE_SHA: options.sourceSha,
|
||||
GITHUB_OUTPUT: output,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
})
|
||||
const directory = join(options.buildRoot, 'windows-package-123-1', 'source')
|
||||
const temporaryDirectory = join(options.buildRoot, 'windows-package-123-1', 'temp')
|
||||
expect(readFileSync(output, 'utf8')).toBe(`directory=${directory}\ntemporaryDirectory=${temporaryDirectory}\n`)
|
||||
writeFileSync(join(temporaryDirectory, 'temporary.txt'), 'owned temporary data')
|
||||
expect(readFileSync(join(directory, 'source.txt'), 'utf8')).toBe('selected source')
|
||||
expect(existsSync(join(directory, 'private.env'))).toBe(false)
|
||||
expect(() => prepareWindowsBuildWorkspace(options)).toThrow()
|
||||
expect(() => { removeWindowsBuildWorkspace({ ...options, sourceSha: '0'.repeat(40) }) }).toThrow('ownership')
|
||||
expect(existsSync(directory)).toBe(true)
|
||||
const external = join(root, 'external')
|
||||
mkdirSync(external)
|
||||
writeFileSync(join(external, 'keep.txt'), 'keep')
|
||||
symlinkSync(external, join(directory, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
|
||||
removeWindowsBuildWorkspace(options)
|
||||
expect(existsSync(directory)).toBe(false)
|
||||
expect(existsSync(temporaryDirectory)).toBe(false)
|
||||
expect(readFileSync(join(external, 'keep.txt'), 'utf8')).toBe('keep')
|
||||
expect(readFileSync(join(sourceDirectory, 'source.txt'), 'utf8')).toBe('selected source')
|
||||
const linked = join(root, 'linked')
|
||||
symlinkSync(external, linked, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: join(linked, 'builds') })).toThrow('links')
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -1,13 +1,13 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { parse, resolve } from 'node:path'
|
||||
import yaml from 'js-yaml'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows-package.yml'), 'utf8')) as {
|
||||
on: Record<string, { inputs: Record<string, { required?: boolean; default?: string; description?: string }> }>
|
||||
on: Record<string, { inputs: Record<string, { type?: string; required?: boolean; default?: string; description?: string }> }>
|
||||
permissions: Record<string, string>
|
||||
concurrency: { group: string; 'cancel-in-progress': boolean }
|
||||
jobs: Record<string, {
|
||||
@@ -19,6 +19,7 @@ const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows
|
||||
uses?: string
|
||||
run?: string
|
||||
if?: string
|
||||
'working-directory'?: string
|
||||
env?: Record<string, string>
|
||||
with?: Record<string, unknown>
|
||||
}>
|
||||
@@ -61,12 +62,17 @@ it('routes manual packaging to the dedicated runner with explicit preset and sig
|
||||
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
|
||||
expect(workflow.jobs.package).not.toHaveProperty('name')
|
||||
expect(job['runs-on']).toBe('dsh-win-package-trial')
|
||||
expect(JSON.stringify(job.env)).not.toContain('runner.')
|
||||
expect(job.steps.find(step => step.name === 'Configure persistent pnpm store')?.env?.DSH_DESKTOP_PACKAGE_CACHE_DIR)
|
||||
.toBe('${{ runner.tool_cache }}/dsh-desktop-package-cache')
|
||||
expect(workflow.concurrency).toEqual({ group: 'windows-desktop-package', 'cancel-in-progress': false })
|
||||
expect(workflow.permissions).toEqual({ contents: 'read' })
|
||||
expect(Object.keys(workflow.on.workflow_dispatch!.inputs).sort()).toEqual([
|
||||
'build_version', 'preset', 'signing', 'source_ref', 'test_release_id', 'update_channel', 'version_mode',
|
||||
'build_root', 'build_version', 'cache_namespace', 'compression_level', 'preset', 'signing', 'source_ref', 'test_release_id', 'update_channel', 'version_mode',
|
||||
])
|
||||
expect(workflow.on.workflow_dispatch!.inputs.signing!.default).toBe('signed')
|
||||
expect(workflow.on.workflow_dispatch!.inputs.compression_level).toMatchObject({ type: 'choice', options: ['3', '7'], default: '3', required: true })
|
||||
expect(job.env.PACKAGE_WINDOWS_COMPRESSION_LEVEL).toBe('${{ inputs.compression_level }}')
|
||||
expect(workflow.on.workflow_dispatch!.inputs.build_version!.required).toBe(false)
|
||||
expect(workflow.on.workflow_dispatch!.inputs.version_mode!.default).toBe('default')
|
||||
expect(workflow.on.workflow_dispatch!.inputs.source_ref!.default).toBe('master')
|
||||
@@ -101,9 +107,182 @@ it('retains failure diagnostics and uploads only the selected successful install
|
||||
const uploads = job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))
|
||||
expect(uploads).toHaveLength(3)
|
||||
expect(uploads[0]!.if).toBe("always() && steps.checkout.outcome == 'success'")
|
||||
expect(uploads[0]!.with).toMatchObject({ path: 'apps/desktop/.desktop-build/packaging-runs/', 'include-hidden-files': true })
|
||||
expect(uploads[0]!.with).toMatchObject({
|
||||
path: '${{ steps.build_workspace.outputs.directory || github.workspace }}/apps/desktop/.desktop-build/packaging-runs/',
|
||||
'include-hidden-files': true,
|
||||
})
|
||||
for (const mode of ['unsigned', 'signed']) {
|
||||
const upload = uploads.find(step => step.if === `success() && inputs.signing == '${mode}'`)!
|
||||
expect(upload.with).toMatchObject({ path: '${{ steps.retained.outputs.directory }}/*', 'if-no-files-found': 'error' })
|
||||
expect(upload.with).toMatchObject({ path: '${{ steps.retained.outputs.directory }}/*', 'if-no-files-found': 'error', 'compression-level': 0 })
|
||||
}
|
||||
})
|
||||
|
||||
it('routes all packaging consumers to the selected workspace and removes it only after successful uploads', () => {
|
||||
expect(workflow.on.workflow_dispatch!.inputs.build_root).toMatchObject({ type: 'string', default: '', required: false })
|
||||
expect(job.env.PACKAGE_BUILD_ROOT).toBe('${{ inputs.build_root }}')
|
||||
const selected = job.steps.find(step => step.id === 'build_workspace')!
|
||||
expect(selected.name).toBe('Select packaging workspace')
|
||||
expect(selected.env).toEqual({ PACKAGE_SOURCE_SHA: '${{ steps.checkout.outputs.commit }}' })
|
||||
expect(selected['working-directory']).toBeUndefined()
|
||||
expect(selected.run).toContain('if (!$env:PACKAGE_BUILD_ROOT)')
|
||||
expect(selected.run).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT')
|
||||
expect(selected.run).toContain('} else {')
|
||||
expect(selected.run).toContain('node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs"')
|
||||
expect(selected.run).toContain('exit $LASTEXITCODE')
|
||||
const index = job.steps.indexOf(selected)
|
||||
expect(index).toBeGreaterThan(job.steps.findIndex(step => step.uses?.startsWith('actions/setup-node@')))
|
||||
expect(index).toBeLessThan(job.steps.findIndex(step => step.name === 'Configure persistent pnpm store'))
|
||||
const consumers = [
|
||||
'Install locked dependencies',
|
||||
'Validate preset and resolve build version',
|
||||
'Check packaging configuration and toolchain',
|
||||
'Build installer and verify packaged runtime',
|
||||
'Retain complete package outside the checkout',
|
||||
]
|
||||
for (const name of consumers) {
|
||||
const step = job.steps.find(entry => entry.name === name)!
|
||||
expect(step['working-directory']).toBe('${{ steps.build_workspace.outputs.directory }}')
|
||||
expect(job.steps.indexOf(step)).toBeGreaterThan(index)
|
||||
}
|
||||
expect(job.steps.filter(step => step['working-directory'] === '${{ steps.build_workspace.outputs.directory }}'))
|
||||
.toHaveLength(consumers.length)
|
||||
const store = job.steps.find(step => step.name === 'Configure persistent pnpm store')!
|
||||
expect(store.run).toContain('[IO.Path]::GetPathRoot($env:GITHUB_WORKSPACE)')
|
||||
expect(store['working-directory']).toBeUndefined()
|
||||
const cleanup = job.steps.at(-1)!
|
||||
expect(cleanup).toMatchObject({
|
||||
name: 'Remove isolated packaging workspace',
|
||||
if: "success() && inputs.build_root != ''",
|
||||
env: { PACKAGE_SOURCE_SHA: '${{ steps.checkout.outputs.commit }}' },
|
||||
run: 'node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs" --remove',
|
||||
})
|
||||
expect(cleanup['working-directory']).toBeUndefined()
|
||||
for (const upload of job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))) {
|
||||
expect(job.steps.indexOf(upload)).toBeLessThan(job.steps.indexOf(cleanup))
|
||||
}
|
||||
})
|
||||
|
||||
it('normalizes priority in the dependency and packaging shells before launching their children', () => {
|
||||
const command = '& "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort'
|
||||
expect(job.steps.find(entry => entry.name === 'Validate selected source and adapter support')!.run)
|
||||
.not.toContain('windows-packaging-priority.ps1')
|
||||
for (const name of ['Install locked dependencies', 'Build installer and verify packaged runtime']) {
|
||||
const step = job.steps.find(entry => entry.name === name)!
|
||||
expect(step.run!.trimStart().startsWith(command + '\n')).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('scopes the optional temporary directory to installer building without changing Actions or later steps', () => {
|
||||
const build = job.steps.find(step => step.name === 'Build installer and verify packaged runtime')!
|
||||
expect(build.env?.PACKAGE_BUILD_TEMP).toBe('${{ steps.build_workspace.outputs.temporaryDirectory }}')
|
||||
expect(build.run).toContain([
|
||||
'if ($env:PACKAGE_BUILD_TEMP) {',
|
||||
' $env:TEMP = $env:PACKAGE_BUILD_TEMP',
|
||||
' $env:TMP = $env:PACKAGE_BUILD_TEMP',
|
||||
'}',
|
||||
].join('\n'))
|
||||
expect(build.run!.indexOf('if ($env:PACKAGE_BUILD_TEMP)')).toBeLessThan(build.run!.indexOf('$packageArgs ='))
|
||||
expect(build.run).not.toContain('GITHUB_ENV')
|
||||
expect(build.run).not.toContain('RUNNER_TEMP')
|
||||
expect(job.env).not.toHaveProperty('TEMP')
|
||||
expect(job.env).not.toHaveProperty('TMP')
|
||||
expect(job.env).not.toHaveProperty('PACKAGE_BUILD_TEMP')
|
||||
for (const step of job.steps.filter(step => step !== build)) {
|
||||
expect(step.env ?? {}).not.toHaveProperty('PACKAGE_BUILD_TEMP')
|
||||
expect(step.env ?? {}).not.toHaveProperty('TEMP')
|
||||
expect(step.env ?? {}).not.toHaveProperty('TMP')
|
||||
expect(step.run ?? '').not.toMatch(/\$env:(?:TEMP|TMP)\s*=/u)
|
||||
}
|
||||
const selected = job.steps.find(step => step.id === 'build_workspace')!
|
||||
const defaultBranch = selected.run!.split('} else {')[0]!
|
||||
expect(defaultBranch).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT')
|
||||
expect(defaultBranch).not.toContain('temporaryDirectory')
|
||||
})
|
||||
|
||||
it.skipIf(process.platform !== 'win32')('keeps the dependency store stable across run-specific pnpm homes', () => {
|
||||
const directory = mkdtempSync(resolve(tmpdir(), 'windows-package-store-'))
|
||||
try {
|
||||
const workspace = resolve(directory, 'runner', 'repo')
|
||||
const packageCache = resolve(directory, 'tool-cache', 'desktop')
|
||||
const store = resolve(parse(directory).root, '.pnpm-store')
|
||||
const step = job.steps.find(entry => entry.name === 'Configure persistent pnpm store')!
|
||||
expect(job.steps.indexOf(step)).toBeLessThan(job.steps.findIndex(entry => entry.name === 'Install locked dependencies'))
|
||||
const script = resolve(directory, 'store.ps1')
|
||||
writeFileSync(script, "$ErrorActionPreference = 'Stop'\n" + step.run!)
|
||||
const outputs = ['first', 'second'].map((run) => {
|
||||
const environmentFile = resolve(directory, run + '.env')
|
||||
execFileSync('pwsh', ['-NoProfile', '-File', script], {
|
||||
env: { ...process.env, GITHUB_WORKSPACE: workspace,
|
||||
PACKAGE_CACHE_NAMESPACE: '', DSH_DESKTOP_PACKAGE_CACHE_DIR: packageCache,
|
||||
PNPM_HOME: resolve(directory, run, 'pnpm'), GITHUB_ENV: environmentFile },
|
||||
stdio: 'pipe',
|
||||
})
|
||||
return readFileSync(environmentFile, 'utf8').trim()
|
||||
})
|
||||
expect(outputs.map(output => output.split(/\r?\n/u))).toEqual(Array.from({ length: 2 }, () => [
|
||||
`DSH_DESKTOP_PACKAGE_CACHE_DIR=${packageCache}`, `PNPM_CONFIG_STORE_DIR=${store}`,
|
||||
]))
|
||||
const runNamespace = (namespace: string) => {
|
||||
const output = resolve(directory, 'namespace.env')
|
||||
rmSync(output, { force: true })
|
||||
execFileSync('pwsh', ['-NoProfile', '-File', script], {
|
||||
env: { ...process.env, GITHUB_WORKSPACE: workspace,
|
||||
PACKAGE_CACHE_NAMESPACE: namespace, DSH_DESKTOP_PACKAGE_CACHE_DIR: packageCache, GITHUB_ENV: output },
|
||||
stdio: 'pipe',
|
||||
})
|
||||
return readFileSync(output, 'utf8').trim().split(/\r?\n/u)
|
||||
}
|
||||
expect(runNamespace('perf-1')).toEqual([
|
||||
`DSH_DESKTOP_PACKAGE_CACHE_DIR=${resolve(packageCache, 'perf-1')}`, `PNPM_CONFIG_STORE_DIR=${resolve(store, 'perf-1')}`,
|
||||
])
|
||||
for (const invalid of ['../escape', 'Upper', 'has space', 'con', 'perf\n', 'a'.repeat(65)]) {
|
||||
expect(() => runNamespace(invalid)).toThrow()
|
||||
}
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
|
||||
}
|
||||
})
|
||||
|
||||
it('pins environment helpers to the workflow commit before selecting product source', () => {
|
||||
const helper = job.steps.find(step => step.name === 'Checkout workflow helpers')!
|
||||
const stage = job.steps.find(step => step.name === 'Stage workflow helpers')!
|
||||
const source = job.steps.find(step => step.id === 'checkout')!
|
||||
expect(helper.with).toMatchObject({ ref: '${{ github.workflow_sha }}', 'persist-credentials': false })
|
||||
expect(job.steps.indexOf(helper)).toBeLessThan(job.steps.indexOf(stage))
|
||||
expect(job.steps.indexOf(stage)).toBeLessThan(job.steps.indexOf(source))
|
||||
expect(stage.run).toContain('RUNNER_TEMP')
|
||||
expect(stage.run).toContain('windows-build-workspace.mjs')
|
||||
expect(stage.run).toContain('windows-packaging-priority.ps1')
|
||||
for (const step of job.steps.filter(step => step.name !== 'Stage workflow helpers')) {
|
||||
expect(step.run ?? '').not.toContain('./apps/desktop/scripts/windows-packaging-priority.ps1')
|
||||
expect(step.run ?? '').not.toContain('node apps/desktop/scripts/windows-build-workspace.mjs')
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.platform !== 'win32')('stages helpers that remain usable when product source lacks them', () => {
|
||||
const directory = mkdtempSync(resolve(tmpdir(), 'workflow-helpers-'))
|
||||
try {
|
||||
const source = resolve(directory, 'source')
|
||||
const scripts = resolve(source, 'apps/desktop/scripts')
|
||||
const temporary = resolve(directory, 'temporary')
|
||||
mkdirSync(scripts, { recursive: true })
|
||||
mkdirSync(temporary)
|
||||
for (const name of ['windows-packaging-priority.ps1', 'windows-build-workspace.mjs']) {
|
||||
writeFileSync(resolve(scripts, name), readFileSync(resolve(root, 'apps/desktop/scripts', name)))
|
||||
}
|
||||
const stage = resolve(directory, 'stage.ps1')
|
||||
writeFileSync(stage, job.steps.find(step => step.name === 'Stage workflow helpers')!.run!)
|
||||
const env = { ...process.env, RUNNER_TEMP: temporary, GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '1', GITHUB_ENV: resolve(directory, 'env') }
|
||||
execFileSync('pwsh', ['-NoProfile', '-File', stage], { cwd: source, env, stdio: 'pipe' })
|
||||
rmSync(scripts, { recursive: true })
|
||||
const helper = resolve(temporary, 'packaging-helpers-123-1/windows-packaging-priority.ps1')
|
||||
expect(execFileSync('pwsh', ['-NoProfile', '-File', helper, '-InspectOnly'], { cwd: source, stdio: 'pipe' }).toString())
|
||||
.toContain('packaging-process-priority')
|
||||
expect(existsSync(resolve(temporary, 'packaging-helpers-123-1/windows-build-workspace.mjs'))).toBe(true)
|
||||
expect(() => execFileSync('pwsh', ['-NoProfile', '-File', stage], {
|
||||
cwd: source, env: { ...env, GITHUB_RUN_ATTEMPT: '2' }, stdio: 'pipe',
|
||||
})).toThrow()
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
|
||||
}
|
||||
})
|
||||
@@ -253,7 +253,7 @@ grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-ga
|
||||
build_gate() {
|
||||
wine_node "$scratch/logs/host-typert.log" --import tsx/esm scripts/generate-typert.ts || return $?
|
||||
wine_node "$scratch/logs/host-references.log" --import tsx/esm scripts/compile-referenced-projects.ts host || return $?
|
||||
wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -p tsconfig.host.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.host.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
|
||||
(cd apps/desktop && wine_node "$scratch/logs/desktop-tsdown.log" "../../$tsdown_js") || return $?
|
||||
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
|
||||
|
||||
Reference in new issue
Block a user