2 Commits

Author SHA1 Message Date
Pine 98b3221eec feat(plugin): add registry name handling and release age exclusion for pnpm workspace
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / node 22.19 (push) Has been cancelled
CI / node 26 (push) Has been cancelled
CI / python 3.10 / keyless SDK (push) Has been cancelled
CI / windows node 24 / wine blocking (push) Has been cancelled
CI / wine apt cache (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Has been cancelled
Release (vendor) / Pack npm tarballs (push) Has been cancelled
Release (dsh) / Pack npm tarballs (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Release (vendor) / Publish to npm (push) Has been cancelled
Release (dsh) / Publish to npm (push) Has been cancelled
2026-08-16 16:11:22 +08:00
Pine 43c4b825e8 feat(build-harness): enhance robustness of harness removal and update exclusion list
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / node 22.19 (push) Has been cancelled
CI / node 26 (push) Has been cancelled
CI / python 3.10 / keyless SDK (push) Has been cancelled
CI / windows node 24 / wine blocking (push) Has been cancelled
CI / wine apt cache (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Has been cancelled
Release (vendor) / Pack npm tarballs (push) Has been cancelled
Release (vendor) / Publish to npm (push) Has been cancelled
Release (dsh) / Pack npm tarballs (push) Has been cancelled
Release (dsh) / Publish to npm (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Has been cancelled
2026-08-16 10:53:01 +08:00
7 changed files with 197 additions and 51 deletions
+54 -1
View File
@@ -11,8 +11,9 @@
*/
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { dump, load } from 'js-yaml'
import {
DEFAULT_PROFILE_BUNDLES,
initProfile,
@@ -26,6 +27,52 @@ import { INSTALL_ANCHOR } from './profile-boot.ts'
const NAME = 'dsh'
/**
* The bare package name when `spec` is a registry specifier, else undefined —
* the same shape the in-app install uses to decide registry participation.
* Everything pnpm treats as a non-registry source (paths, `file:`/`link:`/
* `github:`/`git+`, tarballs, http(s) repo URLs, `.git` markers) is not a
* registry spec.
* @param spec - the pnpm specifier verbatim.
*/
export function registryNameOf(spec: string): string | undefined {
const trimmed = spec.trim()
if (trimmed.length === 0) return undefined
if (/^(?:file:|link:|github:|gitlab:|bitbucket:|git\+|git@)/.test(trimmed)) return undefined
if (/^(?:\.{1,2}|~|[/\\])/.test(trimmed) || /^[a-zA-Z]:[\\/]/.test(trimmed)) return undefined
if (/\.(?:tgz|tar\.gz)(?:[?#]|$)/.test(trimmed)) return undefined
if (/^https?:\/\//.test(trimmed) && trimmed.replace(/^https?:\/\//, '').split('/').length > 1) return undefined
if (/\.git(?:[#@]|$)/.test(trimmed)) return undefined
return trimmed.replace(/@[^/@]+$/, '')
}
/**
* Exempt registry package names from pnpm's `minimumReleaseAge` check by writing
* the `minimumReleaseAgeExclude` setting into the profile's `pnpm-workspace.yaml`
* (honored by pnpm ≥10.16; an older pnpm ignores the key rather than aborting,
* unlike the `--minimum-release-age-exclude` CLI flag). This mirrors the in-app
* install so an external `dsh plugin add <name>` gets the same latest-version
* behavior as the desktop's install, not a stale age-blocked fallback.
* @param profileDir - the profile directory.
* @param names - the registry package names to exempt.
*/
export function writeReleaseAgeExclude(profileDir: string, names: readonly string[]): void {
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
let doc: Record<string, unknown>
try {
const parsed = load(readFileSync(workspacePath, 'utf8'))
doc = parsed !== null && typeof parsed === 'object'
? parsed as Record<string, unknown>
: {}
} catch {
doc = {}
}
const excluded = new Set<string>((doc.minimumReleaseAgeExclude ?? []) as string[])
for (const name of names) excluded.add(name)
if (excluded.size > 0) doc.minimumReleaseAgeExclude = [...excluded]
writeFileSync(workspacePath, dump(doc))
}
/**
* Rewrite relative filesystem specs against the user's invoking directory.
* pnpm runs with cwd = the profile directory, so a bare `.` or `../plugin`
@@ -66,6 +113,12 @@ export function runPlugin(profile: string, args: readonly string[]): number {
// its .cmd shim, which spawn() refuses without a shell since the
// CVE-2024-27980 hardening; the vendored path runs node against pnpm.cjs.
const anchored = args.map(argument => anchorPathSpec(argument, process.cwd()))
// An external `add <registry-name>` gets the same minimum-release-age exemption
// the in-app install applies, so a just-published plugin installs at latest.
if (args[0] === 'add') {
const registryNames = args.slice(1).map(registryNameOf).filter((name): name is string => name !== undefined)
if (registryNames.length > 0) writeReleaseAgeExclude(dir, registryNames)
}
const vendored = resolvePnpm(process.execPath)
const result = vendored === undefined
? spawnSync('pnpm', anchored, { cwd: dir, stdio: 'inherit', shell: process.platform === 'win32' })
+49
View File
@@ -0,0 +1,49 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { load } from 'js-yaml'
import { registryNameOf, writeReleaseAgeExclude } from '../src/plugin.ts'
const dirs: string[] = []
function tempDir(): string {
const dir = mkdtempSync(join(tmpdir(), 'dsh-cli-plugin-'))
dirs.push(dir)
return dir
}
afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }) })
describe('registryNameOf', () => {
it('returns the bare name for a registry specifier', () => {
expect(registryNameOf('dsh-theme-plugin')).toBe('dsh-theme-plugin')
expect(registryNameOf('dsh-theme-plugin@latest')).toBe('dsh-theme-plugin')
expect(registryNameOf('@scope/pkg@1.2.0')).toBe('@scope/pkg')
})
it('returns undefined for non-registry sources', () => {
expect(registryNameOf('github:user/repo')).toBeUndefined()
expect(registryNameOf('git+https://github.com/user/repo.git')).toBeUndefined()
expect(registryNameOf('file:../plugin')).toBeUndefined()
expect(registryNameOf('./plugin')).toBeUndefined()
expect(registryNameOf('https://example.com/p.tgz')).toBeUndefined()
})
})
describe('writeReleaseAgeExclude', () => {
it('writes the exclusion into pnpm-workspace.yaml, preserving existing settings', () => {
const dir = tempDir()
writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'packages:\n - .\nallowBuilds:\n node-pty: true\n')
writeReleaseAgeExclude(dir, ['dsh-theme-plugin'])
writeReleaseAgeExclude(dir, ['@scope/pkg'])
const doc = load(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')) as Record<string, unknown>
expect(doc.minimumReleaseAgeExclude).toEqual(['dsh-theme-plugin', '@scope/pkg'])
expect((doc.allowBuilds as Record<string, unknown>)['node-pty']).toBe(true)
})
it('creates the file when absent', () => {
const dir = tempDir()
writeReleaseAgeExclude(dir, ['x'])
const doc = load(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')) as Record<string, unknown>
expect(doc.minimumReleaseAgeExclude).toEqual(['x'])
})
})
+27 -2
View File
@@ -67,7 +67,11 @@ for (const [label, path] of [
}
}
rmSync(out, { recursive: true, force: true })
// Recursively removing the previous (multi-GB, symlink-heavy) harness can hit
// ENOTEMPTY/EBUSY transiently — on Windows especially, and on macOS under load.
// Retry so the rebuild is robust; maxRetries/retryDelay are Node's built-in
// answer to exactly these codes.
rmSync(out, { recursive: true, force: true, maxRetries: 10, retryDelay: 250 })
mkdirSync(join(out, 'apps'), { recursive: true })
mkdirSync(join(out, 'bin'), { recursive: true })
@@ -223,7 +227,28 @@ function pruneNodeModules(nmDir) {
console.log(` [exclude] ${name} — removed top-level link (${fmtSize(sz)})`)
}
// --- 5. Drop .bin shims that pointed into the removed store entries ---
// --- 5. Delete the flat .pnpm/node_modules/<name> copy. cpSync dereferences
// links while copying, so an excluded package lands here as a REAL directory
// (e.g. electron → 837 MB) that steps 2–4 never touch; it was shipping anyway.
// This shares the same safety check in step 2 — no kept package depends on it.
// Probe with lstatSync (not existsSync): it inspects the link itself rather
// than following the target, so a dangling symlink/junction (its .pnpm store
// entry already removed in step 3) is still found and removed on Windows. ---
const flat = join(pnpmDir, 'node_modules', ...name.split('/'))
let flatStat
try {
flatStat = lstatSync(flat)
} catch {
flatStat = null
}
if (flatStat) {
const sz = flatStat.isSymbolicLink() || flatStat.isFile() ? flatStat.size : dirSize(flat)
rmSync(flat, { recursive: true, force: true })
totalFreed += sz
console.log(` [exclude] ${name} — removed flat .pnpm/node_modules copy (${fmtSize(sz)})`)
}
// --- 6. Drop .bin shims that pointed into the removed store entries ---
removeBinShims(join(nmDir, '.bin'), matching, pnpmDir)
}
@@ -58,6 +58,18 @@
{
"name": "@electron/universal",
"reason": "electron-builder toolchain (group): macOS universal binaries; build-time only."
},
{
"name": "@electron/get",
"reason": "Orphan of the excluded electron toolchain: @electron/rebuild's download helper. No kept package imports it at runtime."
},
{
"name": "@electron/rebuild",
"reason": "Orphan of the excluded electron toolchain: rebuilds native addons for the Electron ABI, which this app never needs (harness runs under system-Node)."
},
{
"name": "@electron-internal/extract-zip",
"reason": "Orphan of the excluded electron-builder toolchain: internal zip extraction used only during packaging. Build-time only."
}
]
}
+6 -2
View File
@@ -17,6 +17,7 @@ import {
runPnpmRemove,
uninstallBundle,
writeAllowBuilds,
writeReleaseAgeExclude,
} from './install.ts'
import {
fetchMarketplaceCatalog,
@@ -253,8 +254,12 @@ export class PluginInventoryGateway extends TypertRemoteService {
writeAllowBuilds(profileDir, spec.consentBuilds)
}
// A registry-name spec participates in the registry fallback loop and the
// minimum-release-age exemption; a git, tarball, or path spec runs once.
// minimum-release-age exemption; a git, tarball, or path spec runs once. The
// release-age exemption is written into pnpm-workspace.yaml (not passed as a
// CLI flag) so an older pnpm ignores the setting instead of aborting on an
// unknown option.
const registryName = registryPackageName(spec.spec)
if (registryName !== undefined) writeReleaseAgeExclude(profileDir, [registryName])
const result = registryName === undefined
? runPnpmInstall({
binName: 'dsh',
@@ -273,7 +278,6 @@ export class PluginInventoryGateway extends TypertRemoteService {
pnpmCjs: pnpm.pnpmCjs,
spec: spec.spec,
before,
minimumReleaseAgeExclude: registryName,
})
if (result.pendingBuilds !== undefined) {
return { ok: true, restartRequired: false, pendingBuilds: result.pendingBuilds }
+32 -9
View File
@@ -150,8 +150,6 @@ export interface PnpmInstallOptions {
readonly before: ProfileManifest
/** An npm registry to install from (`pnpm add --registry`); defaults to pnpm's configured one. */
readonly registry?: string
/** A bare registry package name to exempt from pnpm's minimum-release-age check. */
readonly minimumReleaseAgeExclude?: string
}
/**
@@ -164,15 +162,9 @@ export interface PnpmInstallOptions {
* @returns `pendingBuilds` when pnpm blocked build scripts, else an empty result.
*/
export function runPnpmInstall(options: PnpmInstallOptions): PnpmAddResult {
const { binName, profileDir, installAnchor, nodeBin, pnpmCjs, spec, before, registry, minimumReleaseAgeExclude } = options
const { binName, profileDir, installAnchor, nodeBin, pnpmCjs, spec, before, registry } = options
const args = ['add', spec]
if (registry !== undefined) args.push('--registry', registry)
// `--minimum-release-age-exclude` is understood only by pnpm ≥10.7. It is safe
// with the vendored pnpm (pinned 11.7); a PATH-pnpm fallback in a development
// checkout may be older and reject the option, so skip it there.
if (minimumReleaseAgeExclude !== undefined && nodeBin !== undefined) {
args.push(`--minimum-release-age-exclude=${minimumReleaseAgeExclude}`)
}
const result = nodeBin === undefined ? spawn(pnpmCjs, args, profileDir) : spawn(nodeBin, [pnpmCjs, ...args], profileDir)
if (result.exitCode !== 0) {
const pendingBuilds = parseBlockedBuilds(result.output)
@@ -264,6 +256,37 @@ export function writeAllowBuilds(profileDir: string, names: readonly string[]):
writeFileSync(workspacePath, dump(doc))
}
/**
* Exempt the given registry package names from pnpm's `minimumReleaseAge` check
* by writing the `minimumReleaseAgeExclude` setting into the profile's
* `pnpm-workspace.yaml`. Writing the config directly is the robust way to beat
* the release-age check: the setting is honored by pnpm ≥10.16 (the documented
* mechanism, which pnpm reads only from `pnpm-workspace.yaml`, not `.npmrc`),
* and — unlike the `--minimum-release-age-exclude` CLI flag — an unknown-key pnpm
* ignores it instead of aborting with `Unknown option`. This lets a plugin
* published minutes ago install at its latest version on whichever pnpm the
* runtime resolves. Existing workspace settings are preserved and the exclusion
* list is merged.
* @param profileDir - the writable profile directory.
* @param names - the registry package names to exempt from the release-age check.
*/
export function writeReleaseAgeExclude(profileDir: string, names: readonly string[]): void {
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
let doc: Record<string, unknown>
try {
const parsed = load(readFileSync(workspacePath, 'utf8'))
doc = parsed !== null && typeof parsed === 'object'
? parsed as Record<string, unknown>
: { ...PROFILE_WORKSPACE_BASE }
} catch {
doc = { ...PROFILE_WORKSPACE_BASE }
}
const excluded = new Set<string>((doc.minimumReleaseAgeExclude ?? []) as string[])
for (const name of names) excluded.add(name)
if (excluded.size > 0) doc.minimumReleaseAgeExclude = [...excluded]
writeFileSync(workspacePath, dump(doc))
}
/** Options for removing one plugin dependency. */
export interface PnpmRemoveOptions {
readonly binName: string
@@ -1,11 +1,13 @@
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { load } from 'js-yaml'
import { readProfileManifest } from '@deepseek-ai/dsh-app-boot'
import {
composeOfflineBundle, INSTALL_REGISTRIES, parseBlockedBuilds, registryPackageName,
resolvePnpm, resolvePnpmCommand, runPnpmInstall, writeAllowBuilds,
writeReleaseAgeExclude,
runPnpmInstallWithRegistries, runPnpmRemove, uninstallBundle,
} from '../src/install.ts'
@@ -67,24 +69,6 @@ function makeBlockedBuildPnpm(dir: string): string {
return file
}
/**
* A self-executable fake pnpm (shebang + executable bit) that records its argv
* and exits 0 — simulates a `pnpm` command invoked directly off PATH, without a
* `node` prefix (as the PATH-pnpm fallback does).
*/
function makeExecutablePnpm(dir: string): string {
const file = join(dir, 'pnpm-path')
writeFileSync(file, [
'#!/usr/bin/env node',
"const fs = require('fs')",
'const args = process.argv.slice(2)',
'fs.writeFileSync(process.env.RECORD, JSON.stringify(args))',
'process.exit(0)',
].join('\n'))
chmodSync(file, 0o755)
return file
}
/**
* A fake pnpm that, for `remove <name>`, drops the named dependency from the
* profile's package.json (as pnpm does) and exits 0.
@@ -171,7 +155,7 @@ describe('runPnpmInstall', () => {
}
})
it('passes minimum-release-age-exclude for a registry name', () => {
it('never passes a release-age CLI flag (the exemption is a workspace setting, not a flag)', () => {
const dir = makeProfile()
const record = join(dir, 'record.json')
const pnpm = makeRecordingPnpm(dir)
@@ -181,32 +165,28 @@ describe('runPnpmInstall', () => {
runPnpmInstall({
binName: 'dsh', profileDir: dir, installAnchor: join(dir, 'package.json'),
nodeBin: process.execPath, pnpmCjs: pnpm, spec: 'x',
before: readProfileManifest('dsh', dir), minimumReleaseAgeExclude: 'x',
before: readProfileManifest('dsh', dir),
})
const args = JSON.parse(readFileSync(record, 'utf8')) as string[]
expect(args).toContain('--minimum-release-age-exclude=x')
expect(args).toEqual(['add', 'x'])
expect(args.some(arg => arg.includes('minimum-release-age'))).toBe(false)
} finally {
delete process.env.RECORD
delete process.env.EXIT
}
})
it('omits minimum-release-age-exclude for a PATH-pnpm fallback (may be an older pnpm)', () => {
it('writes the minimum-release-age exemption into pnpm-workspace.yaml, preserving existing settings', () => {
const dir = makeProfile()
const record = join(dir, 'record.json')
const pnpm = makeExecutablePnpm(dir)
process.env.RECORD = record
try {
runPnpmInstall({
binName: 'dsh', profileDir: dir, installAnchor: join(dir, 'package.json'),
nodeBin: undefined, pnpmCjs: pnpm, spec: 'x',
before: readProfileManifest('dsh', dir), minimumReleaseAgeExclude: 'x',
})
const args = JSON.parse(readFileSync(record, 'utf8')) as string[]
expect(args).toEqual(['add', 'x'])
} finally {
delete process.env.RECORD
}
const workspacePath = join(dir, 'pnpm-workspace.yaml')
writeFileSync(workspacePath, 'autoInstallPeers: false\nallowBuilds:\n node-pty: true\n')
writeReleaseAgeExclude(dir, ['dsh-theme-plugin'])
writeReleaseAgeExclude(dir, ['@scope/pkg'])
const doc = JSON.parse(JSON.stringify(load(readFileSync(workspacePath, 'utf8')))) as Record<string, unknown>
expect(doc.minimumReleaseAgeExclude).toEqual(['dsh-theme-plugin', '@scope/pkg'])
// Existing settings survive.
expect((doc.allowBuilds as Record<string, unknown>).node_pty ?? (doc.allowBuilds as Record<string, unknown>)['node-pty']).toBe(true)
expect(doc.autoInstallPeers).toBe(false)
})
it('returns pendingBuilds when pnpm blocks build scripts', () => {