|
@@ -1,11 +1,9 @@
|
|
|
/** Transactional owner of the reserved desktop profile and its private pnpm state. */
|
|
/** Transactional owner of the reserved desktop profile and its private pnpm state. */
|
|
|
|
|
|
|
|
|
|
+import { valid } from 'semver'
|
|
|
import { spawn } from 'node:child_process'
|
|
import { spawn } from 'node:child_process'
|
|
|
-import { createHash, randomUUID } from 'node:crypto'
|
|
|
|
|
|
|
+import { randomUUID } from 'node:crypto'
|
|
|
import {
|
|
import {
|
|
|
- constants,
|
|
|
|
|
- copyFileSync,
|
|
|
|
|
- cpSync,
|
|
|
|
|
existsSync,
|
|
existsSync,
|
|
|
fsyncSync,
|
|
fsyncSync,
|
|
|
ftruncateSync,
|
|
ftruncateSync,
|
|
@@ -13,7 +11,6 @@ import {
|
|
|
mkdirSync,
|
|
mkdirSync,
|
|
|
openSync,
|
|
openSync,
|
|
|
closeSync,
|
|
closeSync,
|
|
|
- readdirSync,
|
|
|
|
|
readFileSync,
|
|
readFileSync,
|
|
|
renameSync,
|
|
renameSync,
|
|
|
rmSync,
|
|
rmSync,
|
|
@@ -23,31 +20,23 @@ import {
|
|
|
} from 'node:fs'
|
|
} from 'node:fs'
|
|
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
|
import {
|
|
import {
|
|
|
- DESKTOP_PACKAGES_DIR,
|
|
|
|
|
- DESKTOP_PACKAGE_SET_FILE,
|
|
|
|
|
DESKTOP_HOST_PACKAGE,
|
|
DESKTOP_HOST_PACKAGE,
|
|
|
desktopCorePackageOverrides,
|
|
desktopCorePackageOverrides,
|
|
|
- desktopDshPackageSpec,
|
|
|
|
|
- readDesktopCorePackageSet,
|
|
|
|
|
verifyDesktopCorePackageSet,
|
|
verifyDesktopCorePackageSet,
|
|
|
} from './core-package-set.ts'
|
|
} from './core-package-set.ts'
|
|
|
import type { DesktopPaths } from './paths.ts'
|
|
import type { DesktopPaths } from './paths.ts'
|
|
|
-import { parseDesktopRelease, type DesktopRelease } from './release.ts'
|
|
|
|
|
-import { extractPnpmStoreArchives, mergePnpmStore } from './seed-store.ts'
|
|
|
|
|
-
|
|
|
|
|
-/** Files the package transaction copies between active and staging projects. */
|
|
|
|
|
-const DESKTOP_PROJECT_FILES = [
|
|
|
|
|
- 'package.json',
|
|
|
|
|
- 'pnpm-lock.yaml',
|
|
|
|
|
- 'pnpm-workspace.yaml',
|
|
|
|
|
- 'desktop-release.json',
|
|
|
|
|
- DESKTOP_PACKAGE_SET_FILE,
|
|
|
|
|
-] as const
|
|
|
|
|
|
|
+import type { DesktopRelease } from './release.ts'
|
|
|
|
|
+import { desktopRuntimeId, verifyDesktopRuntime, type DesktopRuntimeDescriptor } from './runtime-tree.ts'
|
|
|
|
|
+import {
|
|
|
|
|
+ copyDesktopProfile, desktopPluginLockHash, linkDesktopHostPackages, readDesktopProfileState,
|
|
|
|
|
+ unlinkDesktopHostPackages, validateDesktopPluginGraph, type DesktopProfileState,
|
|
|
|
|
+} from './profile-packages.ts'
|
|
|
|
|
|
|
|
/** Desktop plugin record derived from the installed profile. */
|
|
/** Desktop plugin record derived from the installed profile. */
|
|
|
export interface DesktopPluginRecord {
|
|
export interface DesktopPluginRecord {
|
|
|
readonly name: string
|
|
readonly name: string
|
|
|
readonly version: string
|
|
readonly version: string
|
|
|
|
|
+ readonly enabled: boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Installed desktop project manifest slice. */
|
|
/** Installed desktop project manifest slice. */
|
|
@@ -68,6 +57,8 @@ interface DesktopPendingTransaction {
|
|
|
readonly schemaVersion: 1
|
|
readonly schemaVersion: 1
|
|
|
readonly id: string
|
|
readonly id: string
|
|
|
readonly stagingProfile: string
|
|
readonly stagingProfile: string
|
|
|
|
|
+ readonly fromRuntimeId: string | null
|
|
|
|
|
+ readonly toRuntimeId: string
|
|
|
readonly step: 'prepared' | 'active-moved' | 'staging-activated'
|
|
readonly step: 'prepared' | 'active-moved' | 'staging-activated'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -75,6 +66,7 @@ interface DesktopPendingTransaction {
|
|
|
export interface DesktopRuntimeExecutables {
|
|
export interface DesktopRuntimeExecutables {
|
|
|
readonly node: string
|
|
readonly node: string
|
|
|
readonly pnpm: string
|
|
readonly pnpm: string
|
|
|
|
|
+ readonly dsh: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Hooks that bind project replacement to backend lifecycle and health. */
|
|
/** Hooks that bind project replacement to backend lifecycle and health. */
|
|
@@ -92,12 +84,8 @@ export type DesktopProjectMutation =
|
|
|
| { readonly type: 'plugin-add'; readonly spec: string }
|
|
| { readonly type: 'plugin-add'; readonly spec: string }
|
|
|
| { readonly type: 'plugin-remove'; readonly name: string }
|
|
| { readonly type: 'plugin-remove'; readonly name: string }
|
|
|
| { readonly type: 'plugin-update'; readonly name: string; readonly version: string }
|
|
| { readonly type: 'plugin-update'; readonly name: string; readonly version: string }
|
|
|
-
|
|
|
|
|
-interface DesktopSeedIntegrityRecord {
|
|
|
|
|
- readonly path: string
|
|
|
|
|
- readonly bytes: number
|
|
|
|
|
- readonly sha256: string
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ | { readonly type: 'plugin-toggle'; readonly name: string; readonly enabled: boolean }
|
|
|
|
|
+ | { readonly type: 'plugins-disable-all' }
|
|
|
|
|
|
|
|
const PROJECT_NAME = '@deepseek-ai/dsh-desktop-runtime'
|
|
const PROJECT_NAME = '@deepseek-ai/dsh-desktop-runtime'
|
|
|
const DSH_PACKAGE = '@deepseek-ai/dsh'
|
|
const DSH_PACKAGE = '@deepseek-ai/dsh'
|
|
@@ -133,10 +121,6 @@ function workspaceFile(overrides: Readonly<Record<string, string>> = {}): string
|
|
|
return `packages:\n - .\n\n${overrideSection}${WORKSPACE_SETTINGS}allowBuilds:\n node-pty: true\n koffi: true\n fs-ext: true\n ${JSON.stringify(coreBuildKey)}: true\n '@google/genai': false\n protobufjs: false\n node-addon-require-builtin: false\n`
|
|
return `packages:\n - .\n\n${overrideSection}${WORKSPACE_SETTINGS}allowBuilds:\n node-pty: true\n koffi: true\n fs-ext: true\n ${JSON.stringify(coreBuildKey)}: true\n '@google/genai': false\n protobufjs: false\n node-addon-require-builtin: false\n`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function releaseFile(projectDir: string): DesktopRelease {
|
|
|
|
|
- return parseDesktopRelease(readJson(join(projectDir, 'desktop-release.json')))
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
return typeof value === 'object' && value !== null
|
|
return typeof value === 'object' && value !== null
|
|
|
}
|
|
}
|
|
@@ -190,83 +174,20 @@ function removeOwnedDirectory(path: string): void {
|
|
|
rmSync(path, { recursive: true })
|
|
rmSync(path, { recursive: true })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function copyMetadata(source: string, target: string): void {
|
|
|
|
|
- mkdirSync(target, { recursive: true, mode: 0o700 })
|
|
|
|
|
- for (const filename of DESKTOP_PROJECT_FILES) {
|
|
|
|
|
- const from = join(source, filename)
|
|
|
|
|
- if (existsSync(from)) copyFileSync(from, join(target, filename), constants.COPYFILE_EXCL)
|
|
|
|
|
- }
|
|
|
|
|
- cpSync(join(source, DESKTOP_PACKAGES_DIR), join(target, DESKTOP_PACKAGES_DIR), {
|
|
|
|
|
- recursive: true,
|
|
|
|
|
- force: false,
|
|
|
|
|
- errorOnExist: true,
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function seedFiles(root: string): readonly DesktopSeedIntegrityRecord[] {
|
|
|
|
|
- const files: DesktopSeedIntegrityRecord[] = []
|
|
|
|
|
- const visit = (directory: string): void => {
|
|
|
|
|
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
|
|
|
- const path = join(directory, entry.name)
|
|
|
|
|
- const relativePath = path.slice(root.length + 1).split(sep).join('/')
|
|
|
|
|
- if (relativePath === 'integrity.json') continue
|
|
|
|
|
- if (entry.isSymbolicLink()) throw new Error(`desktop seed: symbolic link is not allowed: ${relativePath}`)
|
|
|
|
|
- if (entry.isDirectory()) {
|
|
|
|
|
- visit(path)
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- if (!entry.isFile()) throw new Error(`desktop seed: unsupported file type: ${relativePath}`)
|
|
|
|
|
- const body = readFileSync(path)
|
|
|
|
|
- files.push({
|
|
|
|
|
- path: relativePath,
|
|
|
|
|
- bytes: body.byteLength,
|
|
|
|
|
- sha256: createHash('sha256').update(body).digest('hex'),
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- visit(root)
|
|
|
|
|
- return files.sort((left, right) => left.path.localeCompare(right.path))
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** Verify the packaged offline seed before any content enters writable desktop state. */
|
|
|
|
|
-export function verifySeedIntegrity(seedDir: string): void {
|
|
|
|
|
- const integrityPath = join(seedDir, 'integrity.json')
|
|
|
|
|
- const integrity = readJson(integrityPath)
|
|
|
|
|
- if (!isRecord(integrity) || integrity.schemaVersion !== 2 || !Array.isArray(integrity.files)) {
|
|
|
|
|
- throw new Error(`desktop seed: invalid integrity inventory ${integrityPath}`)
|
|
|
|
|
- }
|
|
|
|
|
- const expected: DesktopSeedIntegrityRecord[] = integrity.files.map((record) => {
|
|
|
|
|
- if (!isRecord(record) || typeof record.path !== 'string' || record.path === '' || record.path.startsWith('/')
|
|
|
|
|
- || record.path.split('/').includes('..') || typeof record.bytes !== 'number'
|
|
|
|
|
- || !Number.isSafeInteger(record.bytes) || record.bytes < 0
|
|
|
|
|
- || typeof record.sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(record.sha256)) {
|
|
|
|
|
- throw new Error(`desktop seed: invalid integrity record in ${integrityPath}`)
|
|
|
|
|
- }
|
|
|
|
|
- return { path: record.path, bytes: record.bytes, sha256: record.sha256 }
|
|
|
|
|
- }).sort((left, right) => left.path.localeCompare(right.path))
|
|
|
|
|
- const actual = seedFiles(seedDir)
|
|
|
|
|
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
|
|
|
- throw new Error('desktop seed: integrity verification failed')
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function projectManifest(projectDir: string): DesktopProjectManifest {
|
|
function projectManifest(projectDir: string): DesktopProjectManifest {
|
|
|
const path = join(projectDir, 'package.json')
|
|
const path = join(projectDir, 'package.json')
|
|
|
const value = readJson(path)
|
|
const value = readJson(path)
|
|
|
const dsh = isRecord(value) && isRecord(value.dsh) ? value.dsh : undefined
|
|
const dsh = isRecord(value) && isRecord(value.dsh) ? value.dsh : undefined
|
|
|
const profile = isRecord(dsh?.profile) ? dsh.profile : undefined
|
|
const profile = isRecord(dsh?.profile) ? dsh.profile : undefined
|
|
|
if (!isRecord(value) || value.name !== PROJECT_NAME || value.private !== true
|
|
if (!isRecord(value) || value.name !== PROJECT_NAME || value.private !== true
|
|
|
- || typeof value.version !== 'string' || !isRecord(value.dependencies)
|
|
|
|
|
|
|
+ || typeof value.version !== 'string' || (value.dependencies !== undefined && !isRecord(value.dependencies))
|
|
|
|| !Array.isArray(profile?.bundles) || !profile.bundles.every(bundle => typeof bundle === 'string')) {
|
|
|| !Array.isArray(profile?.bundles) || !profile.bundles.every(bundle => typeof bundle === 'string')) {
|
|
|
throw new Error(`desktop project: invalid desktop profile manifest ${path}`)
|
|
throw new Error(`desktop project: invalid desktop profile manifest ${path}`)
|
|
|
}
|
|
}
|
|
|
- const manifest = value as unknown as DesktopProjectManifest
|
|
|
|
|
- const packageSet = readDesktopCorePackageSet(projectDir, releaseFile(projectDir).version)
|
|
|
|
|
- const expectedOverrides = desktopCorePackageOverrides(packageSet)
|
|
|
|
|
- if (manifest.dependencies[DSH_PACKAGE] !== desktopDshPackageSpec(packageSet)
|
|
|
|
|
- || Object.entries(expectedOverrides).some(([name, spec]) => manifest.dependencies[name] !== spec)
|
|
|
|
|
- || readFileSync(join(projectDir, 'pnpm-workspace.yaml'), 'utf8') !== workspaceFile(expectedOverrides)) {
|
|
|
|
|
- throw new Error(`desktop project: core package mapping does not match ${DESKTOP_PACKAGE_SET_FILE}`)
|
|
|
|
|
|
|
+ const manifest = { ...value, dependencies: value.dependencies ?? {} } as unknown as DesktopProjectManifest
|
|
|
|
|
+ if (Object.entries(manifest.dependencies).some(([name, version]) => !PACKAGE_NAME_PATTERN.test(name)
|
|
|
|
|
+ || typeof version !== 'string' || valid(version) !== version)) {
|
|
|
|
|
+ throw new Error('desktop project: plugin dependencies must use exact registry versions')
|
|
|
}
|
|
}
|
|
|
return manifest
|
|
return manifest
|
|
|
}
|
|
}
|
|
@@ -285,7 +206,7 @@ function profilePluginNames(projectDir: string): readonly string[] {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function pluginRecords(projectDir: string): readonly DesktopPluginRecord[] {
|
|
function pluginRecords(projectDir: string): readonly DesktopPluginRecord[] {
|
|
|
- return profilePluginNames(projectDir).map(name => inspectPlugin(projectDir, name))
|
|
|
|
|
|
|
+ return Object.keys(projectManifest(projectDir).dependencies).sort().map(name => inspectPlugin(projectDir, name))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function writeProfilePlugins(projectDir: string, plugins: readonly DesktopPluginRecord[]): void {
|
|
function writeProfilePlugins(projectDir: string, plugins: readonly DesktopPluginRecord[]): void {
|
|
@@ -296,7 +217,7 @@ function writeProfilePlugins(projectDir: string, plugins: readonly DesktopPlugin
|
|
|
...manifest.dsh,
|
|
...manifest.dsh,
|
|
|
profile: {
|
|
profile: {
|
|
|
...manifest.dsh.profile,
|
|
...manifest.dsh.profile,
|
|
|
- bundles: [...DESKTOP_PROFILE_BUNDLES, ...plugins.map(plugin => plugin.name)],
|
|
|
|
|
|
|
+ bundles: [...DESKTOP_PROFILE_BUNDLES, ...plugins.filter(plugin => plugin.enabled).map(plugin => plugin.name)],
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
} satisfies DesktopProjectManifest)
|
|
} satisfies DesktopProjectManifest)
|
|
@@ -322,12 +243,13 @@ function inspectPlugin(projectDir: string, requestedName: string): DesktopPlugin
|
|
|
if ((patchPath !== packageDir && !patchPath.startsWith(packageDir + sep)) || !existsSync(patchPath)) {
|
|
if ((patchPath !== packageDir && !patchPath.startsWith(packageDir + sep)) || !existsSync(patchPath)) {
|
|
|
throw new Error(`desktop project: ${requestedName}@${manifest.version} declares an invalid bundle patch`)
|
|
throw new Error(`desktop project: ${requestedName}@${manifest.version} declares an invalid bundle patch`)
|
|
|
}
|
|
}
|
|
|
- return { name: requestedName, version: manifest.version }
|
|
|
|
|
|
|
+ return { name: requestedName, version: manifest.version, enabled: profilePluginNames(projectDir).includes(requestedName) }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Transactional desktop npm project manager. */
|
|
/** Transactional desktop npm project manager. */
|
|
|
export class DesktopProjectManager {
|
|
export class DesktopProjectManager {
|
|
|
private lockDescriptor: number | undefined
|
|
private lockDescriptor: number | undefined
|
|
|
|
|
+ private descriptor: DesktopRuntimeDescriptor | undefined
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* @param paths - Electron-owned package state and reserved desktop profile paths.
|
|
* @param paths - Electron-owned package state and reserved desktop profile paths.
|
|
@@ -345,6 +267,9 @@ export class DesktopProjectManager {
|
|
|
if (!isRecord(value) || value.schemaVersion !== 1
|
|
if (!isRecord(value) || value.schemaVersion !== 1
|
|
|
|| typeof value.id !== 'string' || typeof value.stagingProfile !== 'string'
|
|
|| typeof value.id !== 'string' || typeof value.stagingProfile !== 'string'
|
|
|
|| !isDescendant(this.paths.staging, value.stagingProfile)
|
|
|| !isDescendant(this.paths.staging, value.stagingProfile)
|
|
|
|
|
+ || value.stagingProfile !== join(this.paths.staging, value.id, 'profile')
|
|
|
|
|
+ || (value.fromRuntimeId !== null && (typeof value.fromRuntimeId !== 'string' || !/^[a-f0-9]{64}$/u.test(value.fromRuntimeId)))
|
|
|
|
|
+ || typeof value.toRuntimeId !== 'string' || !/^[a-f0-9]{64}$/u.test(value.toRuntimeId)
|
|
|
|| (value.step !== 'prepared' && value.step !== 'active-moved' && value.step !== 'staging-activated')) {
|
|
|| (value.step !== 'prepared' && value.step !== 'active-moved' && value.step !== 'staging-activated')) {
|
|
|
throw new Error(`desktop project: invalid activation journal ${this.paths.pending}`)
|
|
throw new Error(`desktop project: invalid activation journal ${this.paths.pending}`)
|
|
|
}
|
|
}
|
|
@@ -352,9 +277,20 @@ export class DesktopProjectManager {
|
|
|
schemaVersion: 1,
|
|
schemaVersion: 1,
|
|
|
id: value.id,
|
|
id: value.id,
|
|
|
stagingProfile: value.stagingProfile,
|
|
stagingProfile: value.stagingProfile,
|
|
|
|
|
+ fromRuntimeId: value.fromRuntimeId,
|
|
|
|
|
+ toRuntimeId: value.toRuntimeId,
|
|
|
step: value.step,
|
|
step: value.step,
|
|
|
}
|
|
}
|
|
|
- if (!existsSync(this.paths.profile) && existsSync(this.paths.rollback)) {
|
|
|
|
|
|
|
+ if (pending.step === 'staging-activated' && !existsSync(pending.stagingProfile) && existsSync(this.paths.profile)) {
|
|
|
|
|
+ if (readDesktopProfileState(this.paths.profile)?.runtimeId !== pending.toRuntimeId) {
|
|
|
|
|
+ throw new Error('desktop project: activated profile does not match its journal')
|
|
|
|
|
+ }
|
|
|
|
|
+ removeOwnedDirectory(this.paths.profile)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!existsSync(this.paths.profile) && pending.fromRuntimeId !== null && existsSync(this.paths.rollback)) {
|
|
|
|
|
+ if (readDesktopProfileState(this.paths.rollback)?.runtimeId !== pending.fromRuntimeId) {
|
|
|
|
|
+ throw new Error('desktop project: rollback profile does not match its journal')
|
|
|
|
|
+ }
|
|
|
mkdirSync(dirname(this.paths.profile), { recursive: true })
|
|
mkdirSync(dirname(this.paths.profile), { recursive: true })
|
|
|
renameSync(this.paths.rollback, this.paths.profile)
|
|
renameSync(this.paths.rollback, this.paths.profile)
|
|
|
}
|
|
}
|
|
@@ -368,64 +304,59 @@ export class DesktopProjectManager {
|
|
|
return pluginRecords(this.paths.profile)
|
|
return pluginRecords(this.paths.profile)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Read the exact dsh version installed in the active desktop project. */
|
|
|
|
|
|
|
+ /** Read the dsh version supplied by this application's verified resources. */
|
|
|
dshVersion(): string {
|
|
dshVersion(): string {
|
|
|
- if (!existsSync(this.paths.profile)) throw new Error('desktop project: active profile is not installed')
|
|
|
|
|
- return this.installedPackageVersion(DSH_PACKAGE)
|
|
|
|
|
|
|
+ return this.currentRuntime().release.version
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private installedPackageVersion(packageName: string): string {
|
|
|
|
|
- const manifestPath = join(this.paths.profile, 'node_modules', ...packageName.split('/'), 'package.json')
|
|
|
|
|
- const manifest = readJson(manifestPath)
|
|
|
|
|
- if (!isRecord(manifest) || typeof manifest.version !== 'string') {
|
|
|
|
|
- throw new Error(`desktop project: installed ${packageName} package has no version`)
|
|
|
|
|
|
|
+ /** Read the release most recently applied to the active profile. */
|
|
|
|
|
+ releaseVersion(): string {
|
|
|
|
|
+ const state = readDesktopProfileState(this.paths.profile)
|
|
|
|
|
+ if (state === undefined) throw new Error('desktop project: active profile has no runtime state')
|
|
|
|
|
+ return state.version
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Reject a profile whose dependency links were prepared for another runtime. */
|
|
|
|
|
+ assertProfileRuntime(projectDir: string): void {
|
|
|
|
|
+ if (readDesktopProfileState(projectDir)?.runtimeId !== desktopRuntimeId(this.currentRuntime())) {
|
|
|
|
|
+ throw new Error('desktop project: profile does not match this application runtime')
|
|
|
}
|
|
}
|
|
|
- assertVersion(manifest.version)
|
|
|
|
|
- return manifest.version
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Read the release version applied to the active desktop project. */
|
|
|
|
|
- releaseVersion(): string {
|
|
|
|
|
- if (!existsSync(this.paths.profile)) throw new Error('desktop project: active profile is not installed')
|
|
|
|
|
- return releaseFile(this.paths.profile).version
|
|
|
|
|
|
|
+ private currentRuntime(): DesktopRuntimeDescriptor {
|
|
|
|
|
+ if (this.descriptor === undefined) throw new Error('desktop project: runtime has not been verified')
|
|
|
|
|
+ return this.descriptor
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Install or reconcile the active project to the Electron package's exact release. */
|
|
|
|
|
- async applyRelease(seedDir: string, electronVersion: string, hooks: DesktopProjectHooks): Promise<boolean> {
|
|
|
|
|
|
|
+ private prepareProfile(projectDir: string): void {
|
|
|
|
|
+ const runtime = this.currentRuntime()
|
|
|
|
|
+ linkDesktopHostPackages(projectDir, this.runtime.dsh, runtime)
|
|
|
|
|
+ validateDesktopPluginGraph(projectDir, this.runtime.dsh, runtime, profilePluginNames(projectDir))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Verify this release and reconcile its external profile without installing core packages. */
|
|
|
|
|
+ async applyRelease(electronVersion: string, hooks: DesktopProjectHooks): Promise<boolean> {
|
|
|
return this.withLock(async () => {
|
|
return this.withLock(async () => {
|
|
|
this.recover()
|
|
this.recover()
|
|
|
- verifySeedIntegrity(seedDir)
|
|
|
|
|
- const target = releaseFile(seedDir)
|
|
|
|
|
- verifyDesktopCorePackageSet(seedDir, target.version)
|
|
|
|
|
- if (target.version !== electronVersion) {
|
|
|
|
|
- throw new Error(`desktop project: seed ${target.version} does not match Electron ${electronVersion}`)
|
|
|
|
|
|
|
+ const target = verifyDesktopRuntime(this.runtime.dsh, electronVersion)
|
|
|
|
|
+ this.descriptor = target
|
|
|
|
|
+ const previous = readDesktopProfileState(this.paths.profile)
|
|
|
|
|
+ if (existsSync(this.paths.profile) && previous === undefined) {
|
|
|
|
|
+ throw new Error('desktop project: existing profile is not a Desktop plugin profile')
|
|
|
}
|
|
}
|
|
|
- if (existsSync(this.paths.profile) && this.releaseVersion() === target.version
|
|
|
|
|
- && this.dshVersion() === target.version
|
|
|
|
|
- && this.installedPackageVersion(DESKTOP_HOST_PACKAGE) === target.version) {
|
|
|
|
|
- verifyDesktopCorePackageSet(this.paths.profile, target.version)
|
|
|
|
|
|
|
+ if (previous?.runtimeId === desktopRuntimeId(target)
|
|
|
|
|
+ && previous.lockHash === desktopPluginLockHash(this.paths.profile)
|
|
|
|
|
+ && previous.links.length === target.sharedPackages.length
|
|
|
|
|
+ && previous.links.every(link => link.target === join(this.runtime.dsh, 'node_modules', link.name)
|
|
|
|
|
+ && existsSync(join(this.paths.profile, 'node_modules', link.name)))) {
|
|
|
|
|
+ validateDesktopPluginGraph(this.paths.profile, this.runtime.dsh, target, profilePluginNames(this.paths.profile))
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
- this.mergeSeedPnpmState(seedDir)
|
|
|
|
|
const stagingProfile = this.newStagingProfile()
|
|
const stagingProfile = this.newStagingProfile()
|
|
|
try {
|
|
try {
|
|
|
- if (existsSync(this.paths.profile)) {
|
|
|
|
|
- const plugins = pluginRecords(this.paths.profile)
|
|
|
|
|
- copyMetadata(seedDir, stagingProfile)
|
|
|
|
|
- await this.runPnpm(stagingProfile, ['install', '--offline', '--frozen-lockfile', '--trust-lockfile'])
|
|
|
|
|
- if (plugins.length > 0) {
|
|
|
|
|
- await this.runPnpm(stagingProfile, [
|
|
|
|
|
- 'add',
|
|
|
|
|
- ...plugins.map(plugin => `${plugin.name}@${plugin.version}`),
|
|
|
|
|
- '--save-exact',
|
|
|
|
|
- '--offline',
|
|
|
|
|
- ])
|
|
|
|
|
- writeProfilePlugins(stagingProfile, plugins)
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- copyMetadata(seedDir, stagingProfile)
|
|
|
|
|
- await this.runPnpm(stagingProfile, ['install', '--offline', '--frozen-lockfile', '--trust-lockfile'])
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (previous === undefined) createPluginProfile(stagingProfile)
|
|
|
|
|
+ else copyDesktopProfile(this.paths.profile, stagingProfile)
|
|
|
|
|
+ await this.reconcileProfile(stagingProfile, previous)
|
|
|
await hooks.healthCheck(stagingProfile)
|
|
await hooks.healthCheck(stagingProfile)
|
|
|
await this.activate(stagingProfile, hooks)
|
|
await this.activate(stagingProfile, hooks)
|
|
|
return true
|
|
return true
|
|
@@ -436,16 +367,18 @@ export class DesktopProjectManager {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Apply one exact dependency mutation through a staging project. */
|
|
|
|
|
|
|
+ /** Apply an exact plugin dependency or activation change through a staging project. */
|
|
|
async mutate(mutation: DesktopProjectMutation, hooks: DesktopProjectHooks): Promise<void> {
|
|
async mutate(mutation: DesktopProjectMutation, hooks: DesktopProjectHooks): Promise<void> {
|
|
|
await this.withLock(async () => {
|
|
await this.withLock(async () => {
|
|
|
this.recover()
|
|
this.recover()
|
|
|
|
|
+ this.currentRuntime()
|
|
|
if (!existsSync(this.paths.profile)) throw new Error('desktop project: active profile is not installed')
|
|
if (!existsSync(this.paths.profile)) throw new Error('desktop project: active profile is not installed')
|
|
|
- verifyDesktopCorePackageSet(this.paths.profile, this.releaseVersion())
|
|
|
|
|
const stagingProfile = this.newStagingProfile()
|
|
const stagingProfile = this.newStagingProfile()
|
|
|
try {
|
|
try {
|
|
|
- copyMetadata(this.paths.profile, stagingProfile)
|
|
|
|
|
|
|
+ copyDesktopProfile(this.paths.profile, stagingProfile)
|
|
|
await this.applyMutation(stagingProfile, mutation)
|
|
await this.applyMutation(stagingProfile, mutation)
|
|
|
|
|
+ await this.reconcileProfile(stagingProfile, readDesktopProfileState(stagingProfile),
|
|
|
|
|
+ mutation.type !== 'plugin-toggle' && mutation.type !== 'plugins-disable-all')
|
|
|
await hooks.healthCheck(stagingProfile)
|
|
await hooks.healthCheck(stagingProfile)
|
|
|
await this.activate(stagingProfile, hooks)
|
|
await this.activate(stagingProfile, hooks)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -455,6 +388,25 @@ export class DesktopProjectManager {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private async reconcileProfile(projectDir: string, previous: DesktopProfileState | undefined, packagesChanged = false): Promise<void> {
|
|
|
|
|
+ const target = this.currentRuntime()
|
|
|
|
|
+ this.prepareProfile(projectDir)
|
|
|
|
|
+ const rebuild = previous !== undefined && pluginRecords(projectDir).length > 0
|
|
|
|
|
+ && (previous.nodeVersion !== target.release.nodeVersion || previous.platform !== target.platform || previous.arch !== target.arch)
|
|
|
|
|
+ if (rebuild) {
|
|
|
|
|
+ unlinkDesktopHostPackages(projectDir)
|
|
|
|
|
+ removeOwnedDirectory(join(projectDir, 'node_modules'))
|
|
|
|
|
+ await this.runPnpm(projectDir, ['install', '--frozen-lockfile', '--ignore-scripts'])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (rebuild || packagesChanged) await this.finishPackageOperation(projectDir)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async finishPackageOperation(projectDir: string): Promise<void> {
|
|
|
|
|
+ this.prepareProfile(projectDir)
|
|
|
|
|
+ await this.runPnpm(projectDir, ['rebuild', '--pending'])
|
|
|
|
|
+ this.prepareProfile(projectDir)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private newStagingProfile(): string {
|
|
private newStagingProfile(): string {
|
|
|
const path = join(this.paths.staging, randomUUID(), 'profile')
|
|
const path = join(this.paths.staging, randomUUID(), 'profile')
|
|
|
mkdirSync(path, { recursive: true, mode: 0o700 })
|
|
mkdirSync(path, { recursive: true, mode: 0o700 })
|
|
@@ -466,8 +418,11 @@ export class DesktopProjectManager {
|
|
|
case 'plugin-add': {
|
|
case 'plugin-add': {
|
|
|
const requestedName = packageNameFromSpec(mutation.spec)
|
|
const requestedName = packageNameFromSpec(mutation.spec)
|
|
|
if (requestedName === undefined) throw new Error('desktop project: plugin package name is required')
|
|
if (requestedName === undefined) throw new Error('desktop project: plugin package name is required')
|
|
|
- await this.runPnpm(projectDir, ['add', mutation.spec, '--save-exact'])
|
|
|
|
|
- const installed = inspectPlugin(projectDir, requestedName)
|
|
|
|
|
|
|
+ if (this.currentRuntime().sharedPackages.some(entry => entry.name === requestedName)) {
|
|
|
|
|
+ throw new Error(`desktop project: cannot install host-owned package ${requestedName}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ await this.runPnpm(projectDir, ['add', mutation.spec, '--save-exact', '--ignore-scripts'])
|
|
|
|
|
+ const installed = { ...inspectPlugin(projectDir, requestedName), enabled: true }
|
|
|
const current = pluginRecords(projectDir).filter(plugin => plugin.name !== installed.name)
|
|
const current = pluginRecords(projectDir).filter(plugin => plugin.name !== installed.name)
|
|
|
writeProfilePlugins(
|
|
writeProfilePlugins(
|
|
|
projectDir,
|
|
projectDir,
|
|
@@ -477,21 +432,21 @@ export class DesktopProjectManager {
|
|
|
}
|
|
}
|
|
|
case 'plugin-remove': {
|
|
case 'plugin-remove': {
|
|
|
assertPackageName(mutation.name)
|
|
assertPackageName(mutation.name)
|
|
|
- if (!profilePluginNames(projectDir).includes(mutation.name)) {
|
|
|
|
|
|
|
+ if (!Object.hasOwn(projectManifest(projectDir).dependencies, mutation.name)) {
|
|
|
throw new Error(`desktop project: plugin ${JSON.stringify(mutation.name)} is not installed`)
|
|
throw new Error(`desktop project: plugin ${JSON.stringify(mutation.name)} is not installed`)
|
|
|
}
|
|
}
|
|
|
const remaining = pluginRecords(projectDir).filter(plugin => plugin.name !== mutation.name)
|
|
const remaining = pluginRecords(projectDir).filter(plugin => plugin.name !== mutation.name)
|
|
|
- await this.runPnpm(projectDir, ['remove', mutation.name])
|
|
|
|
|
|
|
+ await this.runPnpm(projectDir, ['remove', mutation.name, '--config.ignore-scripts=true'])
|
|
|
writeProfilePlugins(projectDir, remaining)
|
|
writeProfilePlugins(projectDir, remaining)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
case 'plugin-update':
|
|
case 'plugin-update':
|
|
|
assertPackageName(mutation.name)
|
|
assertPackageName(mutation.name)
|
|
|
assertVersion(mutation.version)
|
|
assertVersion(mutation.version)
|
|
|
- if (!profilePluginNames(projectDir).includes(mutation.name)) {
|
|
|
|
|
|
|
+ if (!Object.hasOwn(projectManifest(projectDir).dependencies, mutation.name)) {
|
|
|
throw new Error(`desktop project: plugin ${JSON.stringify(mutation.name)} is not installed`)
|
|
throw new Error(`desktop project: plugin ${JSON.stringify(mutation.name)} is not installed`)
|
|
|
}
|
|
}
|
|
|
- await this.runPnpm(projectDir, ['add', `${mutation.name}@${mutation.version}`, '--save-exact'])
|
|
|
|
|
|
|
+ await this.runPnpm(projectDir, ['add', `${mutation.name}@${mutation.version}`, '--save-exact', '--ignore-scripts'])
|
|
|
{
|
|
{
|
|
|
const installed = inspectPlugin(projectDir, mutation.name)
|
|
const installed = inspectPlugin(projectDir, mutation.name)
|
|
|
writeProfilePlugins(
|
|
writeProfilePlugins(
|
|
@@ -500,33 +455,37 @@ export class DesktopProjectManager {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
return
|
|
return
|
|
|
|
|
+ case 'plugins-disable-all':
|
|
|
|
|
+ writeProfilePlugins(projectDir, pluginRecords(projectDir).map(plugin => ({ ...plugin, enabled: false })))
|
|
|
|
|
+ return
|
|
|
|
|
+ case 'plugin-toggle': {
|
|
|
|
|
+ assertPackageName(mutation.name)
|
|
|
|
|
+ const plugins = pluginRecords(projectDir)
|
|
|
|
|
+ if (!plugins.some(plugin => plugin.name === mutation.name)) throw new Error(`desktop project: plugin ${mutation.name} is not installed`)
|
|
|
|
|
+ writeProfilePlugins(projectDir, plugins.map(plugin => (
|
|
|
|
|
+ plugin.name === mutation.name ? { ...plugin, enabled: mutation.enabled } : plugin
|
|
|
|
|
+ )))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
default:
|
|
default:
|
|
|
mutation satisfies never
|
|
mutation satisfies never
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private mergeSeedPnpmState(seedDir: string): void {
|
|
|
|
|
- const transactionRoot = join(this.paths.staging, randomUUID())
|
|
|
|
|
- const extractedStore = join(transactionRoot, 'store')
|
|
|
|
|
- try {
|
|
|
|
|
- extractPnpmStoreArchives(seedDir, extractedStore)
|
|
|
|
|
- mergePnpmStore(extractedStore, this.paths.pnpm.store)
|
|
|
|
|
- } finally {
|
|
|
|
|
- removeOwnedDirectory(transactionRoot)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private async activate(stagingProfile: string, hooks: DesktopProjectHooks): Promise<void> {
|
|
private async activate(stagingProfile: string, hooks: DesktopProjectHooks): Promise<void> {
|
|
|
const pending: DesktopPendingTransaction = {
|
|
const pending: DesktopPendingTransaction = {
|
|
|
schemaVersion: 1,
|
|
schemaVersion: 1,
|
|
|
id: basename(dirname(stagingProfile)),
|
|
id: basename(dirname(stagingProfile)),
|
|
|
stagingProfile,
|
|
stagingProfile,
|
|
|
|
|
+ fromRuntimeId: readDesktopProfileState(this.paths.profile)?.runtimeId ?? null,
|
|
|
|
|
+ toRuntimeId: desktopRuntimeId(this.currentRuntime()),
|
|
|
step: 'prepared',
|
|
step: 'prepared',
|
|
|
}
|
|
}
|
|
|
writeJson(this.paths.pending, pending)
|
|
writeJson(this.paths.pending, pending)
|
|
|
- await hooks.beforeActivate()
|
|
|
|
|
let activeMoved = false
|
|
let activeMoved = false
|
|
|
|
|
+ let stagingActivated = false
|
|
|
try {
|
|
try {
|
|
|
|
|
+ await hooks.beforeActivate()
|
|
|
removeOwnedDirectory(this.paths.rollback)
|
|
removeOwnedDirectory(this.paths.rollback)
|
|
|
mkdirSync(dirname(this.paths.rollback), { recursive: true, mode: 0o700 })
|
|
mkdirSync(dirname(this.paths.rollback), { recursive: true, mode: 0o700 })
|
|
|
writeJson(this.paths.pending, { ...pending, step: 'active-moved' } satisfies DesktopPendingTransaction)
|
|
writeJson(this.paths.pending, { ...pending, step: 'active-moved' } satisfies DesktopPendingTransaction)
|
|
@@ -537,10 +496,11 @@ export class DesktopProjectManager {
|
|
|
mkdirSync(dirname(this.paths.profile), { recursive: true, mode: 0o700 })
|
|
mkdirSync(dirname(this.paths.profile), { recursive: true, mode: 0o700 })
|
|
|
writeJson(this.paths.pending, { ...pending, step: 'staging-activated' } satisfies DesktopPendingTransaction)
|
|
writeJson(this.paths.pending, { ...pending, step: 'staging-activated' } satisfies DesktopPendingTransaction)
|
|
|
renameSync(stagingProfile, this.paths.profile)
|
|
renameSync(stagingProfile, this.paths.profile)
|
|
|
|
|
+ stagingActivated = true
|
|
|
await hooks.afterActivate()
|
|
await hooks.afterActivate()
|
|
|
unlinkSync(this.paths.pending)
|
|
unlinkSync(this.paths.pending)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- if (existsSync(this.paths.profile)) removeOwnedDirectory(this.paths.profile)
|
|
|
|
|
|
|
+ if (stagingActivated) removeOwnedDirectory(this.paths.profile)
|
|
|
if (activeMoved && existsSync(this.paths.rollback)) renameSync(this.paths.rollback, this.paths.profile)
|
|
if (activeMoved && existsSync(this.paths.rollback)) renameSync(this.paths.rollback, this.paths.profile)
|
|
|
if (existsSync(this.paths.pending)) unlinkSync(this.paths.pending)
|
|
if (existsSync(this.paths.pending)) unlinkSync(this.paths.pending)
|
|
|
await hooks.afterActivate().catch(() => undefined)
|
|
await hooks.afterActivate().catch(() => undefined)
|
|
@@ -558,7 +518,7 @@ export class DesktopProjectManager {
|
|
|
const npmrc = join(this.paths.pnpm.config, 'npmrc')
|
|
const npmrc = join(this.paths.pnpm.config, 'npmrc')
|
|
|
if (!existsSync(npmrc)) writeFileSync(npmrc, '', { mode: 0o600 })
|
|
if (!existsSync(npmrc)) writeFileSync(npmrc, '', { mode: 0o600 })
|
|
|
const inherited = Object.fromEntries(Object.entries(process.env).filter(([name]) => (
|
|
const inherited = Object.fromEntries(Object.entries(process.env).filter(([name]) => (
|
|
|
- !/^DSH_DESKTOP_/u.test(name) && !/^(?:npm|pnpm|corepack)_/iu.test(name)
|
|
|
|
|
|
|
+ name !== 'NODE_OPTIONS' && name !== 'NODE_PATH' && !/^DSH_DESKTOP_/u.test(name) && !/^(?:npm|pnpm|corepack)_/iu.test(name)
|
|
|
)))
|
|
)))
|
|
|
await new Promise<void>((settle, reject) => {
|
|
await new Promise<void>((settle, reject) => {
|
|
|
const child = spawn(this.runtime.node, [
|
|
const child = spawn(this.runtime.node, [
|
|
@@ -585,19 +545,7 @@ export class DesktopProjectManager {
|
|
|
},
|
|
},
|
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
|
})
|
|
})
|
|
|
- const childPid = child.pid
|
|
|
|
|
- if (childPid === undefined) {
|
|
|
|
|
- child.kill('SIGKILL')
|
|
|
|
|
- reject(new Error('desktop project: pnpm did not report a process id'))
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- try {
|
|
|
|
|
- this.writeLockOwner(childPid)
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- child.kill('SIGKILL')
|
|
|
|
|
- reject(errorOf(error, 'desktop project: failed to assign the package transaction lock to pnpm'))
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ let failure: Error | undefined
|
|
|
let diagnostics = ''
|
|
let diagnostics = ''
|
|
|
let completed = false
|
|
let completed = false
|
|
|
const appendDiagnostics = (chunk: string): void => {
|
|
const appendDiagnostics = (chunk: string): void => {
|
|
@@ -618,9 +566,10 @@ export class DesktopProjectManager {
|
|
|
}
|
|
}
|
|
|
settleChild()
|
|
settleChild()
|
|
|
}
|
|
}
|
|
|
- child.once('error', (error) => { complete(() => { reject(error) }) })
|
|
|
|
|
|
|
+ child.once('error', (error) => { failure = error })
|
|
|
child.once('close', (code, signal) => {
|
|
child.once('close', (code, signal) => {
|
|
|
complete(() => {
|
|
complete(() => {
|
|
|
|
|
+ if (failure !== undefined) { reject(failure); return }
|
|
|
if (code === 0) {
|
|
if (code === 0) {
|
|
|
settle()
|
|
settle()
|
|
|
return
|
|
return
|
|
@@ -630,6 +579,13 @@ export class DesktopProjectManager {
|
|
|
))
|
|
))
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (child.pid === undefined) throw new Error('desktop project: pnpm did not report a process id')
|
|
|
|
|
+ this.writeLockOwner(child.pid)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ failure = errorOf(error, 'desktop project: failed to assign the package transaction lock to pnpm')
|
|
|
|
|
+ child.kill('SIGKILL')
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -682,10 +638,10 @@ export class DesktopProjectManager {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** Create seed metadata for one exact Electron and dsh release. */
|
|
|
|
|
-export function createSeedMetadata(seedDir: string, release: DesktopRelease): void {
|
|
|
|
|
- mkdirSync(seedDir, { recursive: true, mode: 0o700 })
|
|
|
|
|
- const packageSet = verifyDesktopCorePackageSet(seedDir, release.version)
|
|
|
|
|
|
|
+/** Create build-only project metadata for materializing the signed runtime. */
|
|
|
|
|
+export function createRuntimeProjectMetadata(projectDir: string, release: DesktopRelease): void {
|
|
|
|
|
+ mkdirSync(projectDir, { recursive: true, mode: 0o700 })
|
|
|
|
|
+ const packageSet = verifyDesktopCorePackageSet(projectDir, release.version)
|
|
|
const manifest: DesktopProjectManifest = {
|
|
const manifest: DesktopProjectManifest = {
|
|
|
name: PROJECT_NAME,
|
|
name: PROJECT_NAME,
|
|
|
private: true,
|
|
private: true,
|
|
@@ -693,13 +649,13 @@ export function createSeedMetadata(seedDir: string, release: DesktopRelease): vo
|
|
|
dependencies: desktopCorePackageOverrides(packageSet),
|
|
dependencies: desktopCorePackageOverrides(packageSet),
|
|
|
dsh: { profile: { bundles: [...DESKTOP_PROFILE_BUNDLES] } },
|
|
dsh: { profile: { bundles: [...DESKTOP_PROFILE_BUNDLES] } },
|
|
|
}
|
|
}
|
|
|
- writeJson(join(seedDir, 'package.json'), manifest)
|
|
|
|
|
|
|
+ writeJson(join(projectDir, 'package.json'), manifest)
|
|
|
writeFileSync(
|
|
writeFileSync(
|
|
|
- join(seedDir, 'pnpm-workspace.yaml'),
|
|
|
|
|
|
|
+ join(projectDir, 'pnpm-workspace.yaml'),
|
|
|
workspaceFile(desktopCorePackageOverrides(packageSet)),
|
|
workspaceFile(desktopCorePackageOverrides(packageSet)),
|
|
|
{ mode: 0o600 },
|
|
{ mode: 0o600 },
|
|
|
)
|
|
)
|
|
|
- writeJson(join(seedDir, 'desktop-release.json'), release)
|
|
|
|
|
|
|
+ writeJson(join(projectDir, 'desktop-release.json'), release)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -723,3 +679,13 @@ export function createDevelopmentProjectMetadata(projectDir: string, release: De
|
|
|
writeFileSync(join(projectDir, 'pnpm-workspace.yaml'), workspaceFile(), { mode: 0o600 })
|
|
writeFileSync(join(projectDir, 'pnpm-workspace.yaml'), workspaceFile(), { mode: 0o600 })
|
|
|
writeJson(join(projectDir, 'desktop-release.json'), release)
|
|
writeJson(join(projectDir, 'desktop-release.json'), release)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/** Create the first external plugin profile without running a package manager. */
|
|
|
|
|
+export function createPluginProfile(projectDir: string): void {
|
|
|
|
|
+ mkdirSync(projectDir, { recursive: true, mode: 0o700 })
|
|
|
|
|
+ writeJson(join(projectDir, 'package.json'), {
|
|
|
|
|
+ name: PROJECT_NAME, private: true, version: '0.0.0', dependencies: {},
|
|
|
|
|
+ dsh: { profile: { bundles: [...DESKTOP_PROFILE_BUNDLES] } },
|
|
|
|
|
+ } satisfies DesktopProjectManifest)
|
|
|
|
|
+ writeFileSync(join(projectDir, 'pnpm-workspace.yaml'), workspaceFile(), { mode: 0o600 })
|
|
|
|
|
+}
|