mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-09 09:57:05 +08:00
refactor(desktop): own recoverable plugin install lifecycle
This commit is contained in:
@@ -54,16 +54,16 @@ export interface MarketDesktopPnpm {
|
||||
invokingDir: string,
|
||||
signal?: AbortSignal,
|
||||
): MarketDesktopPnpmHandle
|
||||
runPluginInstall(
|
||||
args: readonly string[],
|
||||
invokingDir: string,
|
||||
recovery: {
|
||||
installPlugin(request: {
|
||||
readonly pnpmOptions?: readonly string[]
|
||||
readonly invokingDir: string
|
||||
readonly recovery: {
|
||||
readonly packageName: string
|
||||
readonly packageVersion: string
|
||||
readonly receiptId: string
|
||||
},
|
||||
signal?: AbortSignal,
|
||||
): Promise<MarketDesktopPnpmHandle>
|
||||
}
|
||||
readonly signal?: AbortSignal
|
||||
}): Promise<MarketDesktopPnpmHandle>
|
||||
recoveredInstallReceiptIds(): Promise<readonly string[]>
|
||||
acknowledgeRecoveredInstall(receiptId: string): Promise<void>
|
||||
rollbackPluginInstall(receiptId: string): Promise<boolean>
|
||||
@@ -850,7 +850,7 @@ export class MarketInstallService {
|
||||
}
|
||||
try {
|
||||
await this.runPlugin(
|
||||
this.installArgs(candidate.packageName, candidate.version),
|
||||
this.installOptions(candidate.packageName),
|
||||
profile,
|
||||
operationSignal,
|
||||
true,
|
||||
@@ -1162,7 +1162,12 @@ export class MarketInstallService {
|
||||
try {
|
||||
handle = installRecovery === undefined
|
||||
? this.pnpm.runPlugin(args, profile.dir, combinedSignal)
|
||||
: await this.pnpm.runPluginInstall(args, profile.dir, installRecovery, combinedSignal)
|
||||
: await this.pnpm.installPlugin({
|
||||
pnpmOptions: args,
|
||||
invokingDir: profile.dir,
|
||||
recovery: installRecovery,
|
||||
signal: combinedSignal,
|
||||
})
|
||||
}
|
||||
catch { throw new MarketInstallError('operation-failed', 'The desktop package manager could not start.') }
|
||||
handle.stdout.resume()
|
||||
@@ -1182,14 +1187,12 @@ export class MarketInstallService {
|
||||
}
|
||||
}
|
||||
|
||||
private installArgs(packageName: string, version: string): readonly string[] {
|
||||
private installOptions(packageName: string): readonly string[] {
|
||||
const scope = packageName.startsWith('@') ? packageName.split('/', 1)[0] : undefined
|
||||
return [
|
||||
'add',
|
||||
'--save-exact',
|
||||
`--registry=${NPM_REGISTRY}`,
|
||||
...(scope === undefined ? [] : [`--${scope}:registry=${NPM_REGISTRY}`]),
|
||||
`${packageName}@${version}`,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -242,9 +242,13 @@ function recoverableRunner(
|
||||
let pendingPackageName: string | undefined
|
||||
return {
|
||||
...implementation,
|
||||
async runPluginInstall(args, dir, recovery, signal) {
|
||||
pendingPackageName = recovery.packageName
|
||||
return implementation.runPlugin(args, dir, signal)
|
||||
async installPlugin(request) {
|
||||
pendingPackageName = request.recovery.packageName
|
||||
return implementation.runPlugin([
|
||||
'add',
|
||||
...(request.pnpmOptions ?? []),
|
||||
`${request.recovery.packageName}@${request.recovery.packageVersion}`,
|
||||
], request.invokingDir, request.signal)
|
||||
},
|
||||
async recoveredInstallReceiptIds() { return [] },
|
||||
async acknowledgeRecoveredInstall() {},
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
import {
|
||||
DesktopInstallRecoveryStore,
|
||||
type BeginDesktopInstallRecoveryInput,
|
||||
} from './install-recovery.ts'
|
||||
import { assertDesktopProfileName } from './profile-manager.ts'
|
||||
|
||||
@@ -67,6 +66,34 @@ export interface DesktopPnpmHandle {
|
||||
cancel(): void
|
||||
}
|
||||
|
||||
/** Receipt identity tied to one recoverable plugin installation. */
|
||||
export interface DesktopPluginInstallRecovery {
|
||||
readonly packageName: string
|
||||
readonly packageVersion: string
|
||||
/** Host-generated before installation so every crash window can reconcile the receipt. */
|
||||
readonly receiptId: string
|
||||
}
|
||||
|
||||
/** Complete request for one Desktop-owned, recoverable plugin installation. */
|
||||
export interface DesktopPluginInstallRequest {
|
||||
/** pnpm flags after the enforced `add` command and before the exact generated target. */
|
||||
readonly pnpmOptions?: readonly string[]
|
||||
/** Absolute caller directory used to anchor relative package specifications. */
|
||||
readonly invokingDir: string
|
||||
readonly recovery: DesktopPluginInstallRecovery
|
||||
readonly signal?: AbortSignal
|
||||
}
|
||||
|
||||
/** Public package-operation interface for one immutable Desktop profile generation. */
|
||||
export interface DesktopPnpm {
|
||||
run(args: readonly string[], signal?: AbortSignal): DesktopPnpmHandle
|
||||
runPlugin(args: readonly string[], invokingDir: string, signal?: AbortSignal): DesktopPnpmHandle
|
||||
installPlugin(request: DesktopPluginInstallRequest): Promise<DesktopPnpmHandle>
|
||||
recoveredInstallReceiptIds(): Promise<readonly string[]>
|
||||
acknowledgeRecoveredInstall(receiptId: string): Promise<void>
|
||||
rollbackPluginInstall(receiptId: string): Promise<boolean>
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
/** Launcher-private inputs from which the Host provider constructs the service. */
|
||||
@@ -130,8 +157,8 @@ function validateBootstrap(bootstrap: DesktopPnpmBootstrap): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Host service providing one managed pnpm operation at a time. */
|
||||
export class DesktopPnpm extends Service {
|
||||
/** Cordis adapter implementing the public Desktop package-operation interface. */
|
||||
class DesktopPnpmService extends Service implements DesktopPnpm {
|
||||
private active: ActiveOperation | undefined
|
||||
private installPreparationActive = false
|
||||
private closed = false
|
||||
@@ -220,26 +247,21 @@ export class DesktopPnpm extends Service {
|
||||
* Snapshot the active profile before running one `dsh plugin add` operation.
|
||||
* The returned handle seals the post-install image before `done` resolves.
|
||||
*/
|
||||
async runPluginInstall(
|
||||
args: readonly string[],
|
||||
invokingDir: string,
|
||||
recovery: BeginDesktopInstallRecoveryInput,
|
||||
signal?: AbortSignal,
|
||||
): Promise<DesktopPnpmHandle> {
|
||||
const resolvedArgs = validatedArgs(args)
|
||||
if (resolvedArgs[0] !== 'add') {
|
||||
throw new Error(`${BIN_NAME}: recoverable plugin install requires the add command`)
|
||||
async installPlugin(request: DesktopPluginInstallRequest): Promise<DesktopPnpmHandle> {
|
||||
const resolvedOptions = request.pnpmOptions === undefined ? [] : [...request.pnpmOptions]
|
||||
if (resolvedOptions.some(argument => argument.includes('\0'))) {
|
||||
throw new Error(`${BIN_NAME}: desktop pnpm arguments must not contain NUL`)
|
||||
}
|
||||
assertAbsolutePath('plugin invoking directory', invokingDir)
|
||||
assertAbsolutePath('plugin invoking directory', request.invokingDir)
|
||||
if (this.closed) throw new Error(`${BIN_NAME}: desktop pnpm generation is closed`)
|
||||
if (this.active !== undefined || this.installPreparationActive) {
|
||||
throw new Error(`${BIN_NAME}: another desktop pnpm operation is already running`)
|
||||
}
|
||||
signal?.throwIfAborted()
|
||||
request.signal?.throwIfAborted()
|
||||
this.installPreparationActive = true
|
||||
let transaction: Awaited<ReturnType<DesktopInstallRecoveryStore['begin']>> | undefined
|
||||
try {
|
||||
transaction = await this.installRecovery.begin(recovery)
|
||||
transaction = await this.installRecovery.begin(request.recovery)
|
||||
const handle = this.start({
|
||||
argv: [
|
||||
this.bootstrap.appExecutable,
|
||||
@@ -248,12 +270,14 @@ export class DesktopPnpm extends Service {
|
||||
'plugin',
|
||||
'--profile',
|
||||
this.bootstrap.activeProfileName,
|
||||
...resolvedArgs,
|
||||
'add',
|
||||
...resolvedOptions,
|
||||
`${request.recovery.packageName}@${request.recovery.packageVersion}`,
|
||||
],
|
||||
cwd: invokingDir,
|
||||
cwd: request.invokingDir,
|
||||
recoveryTransactionId: transaction.transactionId,
|
||||
allowInstallPreparation: true,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
...(request.signal === undefined ? {} : { signal: request.signal }),
|
||||
})
|
||||
this.installPreparationActive = false
|
||||
return handle
|
||||
@@ -404,5 +428,5 @@ export const inject = ['desktopPnpmBootstrap', 'subprocess']
|
||||
* @param ctx - Host context carrying launcher bootstrap values and subprocess ownership.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
new DesktopPnpm(ctx, ctx.desktopPnpmBootstrap)
|
||||
new DesktopPnpmService(ctx, ctx.desktopPnpmBootstrap)
|
||||
}
|
||||
|
||||
@@ -201,15 +201,15 @@ describe('desktop pnpm Host service', () => {
|
||||
writeFileSync(manifestPath, JSON.stringify({ dependencies: {} }))
|
||||
const harness = await createHarness([child], selectedBootstrap)
|
||||
|
||||
const pending = harness.service.runPluginInstall(
|
||||
['add', '--save-exact', '[email protected]'],
|
||||
'/workspace',
|
||||
{
|
||||
const pending = harness.service.installPlugin({
|
||||
pnpmOptions: ['--save-exact'],
|
||||
invokingDir: '/workspace',
|
||||
recovery: {
|
||||
packageName: 'example-plugin',
|
||||
packageVersion: '1.0.0',
|
||||
receiptId: 'receipt:test-install-0001',
|
||||
},
|
||||
)
|
||||
})
|
||||
expect(() => harness.service.runPlugin(['remove', 'other-plugin'], '/workspace')).toThrow(
|
||||
'another desktop pnpm operation is already running',
|
||||
)
|
||||
@@ -218,7 +218,17 @@ describe('desktop pnpm Host service', () => {
|
||||
finish(child)
|
||||
await expect(operation.done).resolves.toEqual({ exitCode: 0, signal: null })
|
||||
|
||||
expect(harness.spawn.mock.calls[0]?.[0].argv).toContain('[email protected]')
|
||||
expect(harness.spawn.mock.calls[0]?.[0].argv).toEqual([
|
||||
selectedBootstrap.appExecutable,
|
||||
'--expose-internals',
|
||||
selectedBootstrap.dshBootstrapPath,
|
||||
'plugin',
|
||||
'--profile',
|
||||
selectedBootstrap.activeProfileName,
|
||||
'add',
|
||||
'--save-exact',
|
||||
'[email protected]',
|
||||
])
|
||||
expect(JSON.parse(readFileSync(selectedBootstrap.installRecoveryStatePath, 'utf8'))).toMatchObject({
|
||||
packageName: 'example-plugin',
|
||||
packageVersion: '1.0.0',
|
||||
@@ -241,15 +251,14 @@ describe('desktop pnpm Host service', () => {
|
||||
mkdirSync(selectedBootstrap.activeProfileDir, { recursive: true })
|
||||
writeFileSync(manifestPath, originalManifest)
|
||||
const harness = await createHarness([child], selectedBootstrap)
|
||||
const operation = await harness.service.runPluginInstall(
|
||||
['add', '[email protected]'],
|
||||
'/workspace',
|
||||
{
|
||||
const operation = await harness.service.installPlugin({
|
||||
invokingDir: '/workspace',
|
||||
recovery: {
|
||||
packageName: 'broken-plugin',
|
||||
packageVersion: '1.0.0',
|
||||
receiptId: 'receipt:test-install-failure-0001',
|
||||
},
|
||||
)
|
||||
})
|
||||
writeFileSync(manifestPath, JSON.stringify({ dependencies: { 'broken-plugin': '1.0.0' } }))
|
||||
finish(child, { exitCode: 1, signal: null })
|
||||
|
||||
@@ -276,6 +285,15 @@ describe('desktop pnpm Host service', () => {
|
||||
expect(() => harness.service.runPlugin(['add', 'plugin'], '/workspace')).toThrow(
|
||||
'plugin add must use the recoverable install boundary',
|
||||
)
|
||||
await expect(harness.service.installPlugin({
|
||||
pnpmOptions: ['--registry=https://registry.example\0.invalid'],
|
||||
invokingDir: '/workspace',
|
||||
recovery: {
|
||||
packageName: 'plugin',
|
||||
packageVersion: '1.0.0',
|
||||
receiptId: 'receipt:test-empty-install',
|
||||
},
|
||||
})).rejects.toThrow('arguments must not contain NUL')
|
||||
expect(harness.spawn).not.toHaveBeenCalled()
|
||||
await harness.dispose()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user