From 4356937f7d99d71d407f7d4adaa1fb0bc267a906 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 17 Jul 2026 22:26:32 -0400 Subject: [PATCH 1/2] Preserve Composer overlay source revisions --- package.json | 1 + packages/cli/src/commands/agent-task-run.ts | 2 +- .../cli/src/commands/recipe-runtime-setup.ts | 1 + packages/cli/src/recipe-sources.ts | 112 +++++++++++++++++- .../composer-backed-source-hydration-smoke.ts | 78 ++++++++++-- 5 files changed, 179 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index e7e53c31..f6e28529 100644 --- a/package.json +++ b/package.json @@ -201,6 +201,7 @@ "test:preview-options": "tsx tests/preview-options.test.ts", "test:evidence-bundle-digest-and-recipe-artifact": "tsx tests/evidence-bundle-digest-and-recipe-artifact.test.ts", "test:runtime-overlay-descriptors": "tsx tests/runtime-overlay-descriptors.test.ts", + "test:composer-package-overlay-revision": "tsx scripts/composer-backed-source-hydration-smoke.ts", "test:runtime-preset-registry": "tsx tests/runtime-preset-registry.test.ts", "test:generic-ability-runtime-run": "tsx tests/generic-ability-runtime-run.test.ts", "test:provider-runtime-contracts": "tsx tests/provider-runtime-contracts.test.ts", diff --git a/packages/cli/src/commands/agent-task-run.ts b/packages/cli/src/commands/agent-task-run.ts index 094e7490..a397ad84 100644 --- a/packages/cli/src/commands/agent-task-run.ts +++ b/packages/cli/src/commands/agent-task-run.ts @@ -793,7 +793,7 @@ function compactPreparedPaths(context: Record, recipeInputs: Re component_contracts: arrayRecords(context.preparedComponentContracts).map((contract) => compactRecord(contract, ["slug", "requestedPath", "preparedPath", "pluginFile", "loadAs", "activate", "status"])), workspaces: arrayRecords(context.preparedWorkspaces).map((workspace) => compactRecord(workspace, ["target", "mode", "metadata"])), staged_files: arrayRecords(context.preparedStagedFiles).map((file) => compactRecord(file, ["sourceRef", "target", "type", "provenance", "metadata"])), - dependency_overlays: arrayRecords(context.preparedDependencyOverlays).map((overlay) => compactRecord(overlay, ["package", "target", "type", "mode", "metadata"])), + dependency_overlays: arrayRecords(context.preparedDependencyOverlays).map((overlay) => compactRecord(overlay, ["package", "reference", "target", "type", "mode", "metadata"])), runtime_overlays: arrayRecords(context.preparedRuntimeOverlays).map((overlay) => compactRecord(overlay, ["target", "type", "mode", "metadata"])), requested_component_contracts: arrayRecords(recipeInputs.component_contracts ?? taskInputs.component_contracts).map((contract) => compactRecord(contract, ["slug", "path", "loadAs", "activate"])), }) diff --git a/packages/cli/src/commands/recipe-runtime-setup.ts b/packages/cli/src/commands/recipe-runtime-setup.ts index 5cbcab0d..f4e0fce4 100644 --- a/packages/cli/src/commands/recipe-runtime-setup.ts +++ b/packages/cli/src/commands/recipe-runtime-setup.ts @@ -249,6 +249,7 @@ export function recipeRunDependencyOverlay(overlay: PreparedDependencyOverlay): return { source: overlay.source, sourceRef: overlay.sourceRef, + ...(overlay.reference ? { reference: overlay.reference } : {}), target: overlay.target, package: overlay.package, consumer: overlay.consumer, diff --git a/packages/cli/src/recipe-sources.ts b/packages/cli/src/recipe-sources.ts index 0d923ddd..d726c2fe 100644 --- a/packages/cli/src/recipe-sources.ts +++ b/packages/cli/src/recipe-sources.ts @@ -108,6 +108,7 @@ export interface PreparedRuntimeOverlay { export interface PreparedDependencyOverlay { source: string sourceRef: string + reference?: string target: string package: string consumer: string @@ -428,14 +429,15 @@ function composerInstalledPackageClassmapPaths(pkg: ComposerInstalledPackage): s export async function prepareRecipeDependencyOverlays(recipe: WorkspaceRecipe, recipeDirectory: string, extraPlugins: PreparedExtraPlugin[]): Promise { const overlays: PreparedDependencyOverlay[] = [] + const stagedConsumers = new Set() for (const [index, overlay] of (recipe.inputs?.dependency_overlays ?? []).entries()) { - overlays.push(await prepareRecipeDependencyOverlay(overlay, recipeDirectory, extraPlugins, index)) + overlays.push(await prepareRecipeDependencyOverlay(overlay, recipeDirectory, extraPlugins, stagedConsumers, index)) } return overlays } -async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependencyOverlay, recipeDirectory: string, extraPlugins: PreparedExtraPlugin[], index: number): Promise { +async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependencyOverlay, recipeDirectory: string, extraPlugins: PreparedExtraPlugin[], stagedConsumers: Set, index: number): Promise { if (overlay.kind !== "composer-package") { throw new Error(`Unsupported dependency overlay kind: ${overlay.kind}`) } @@ -450,6 +452,10 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency const source = resolve(recipeDirectory, overlay.source) await validateExistingDirectoryForOverlay(source, overlay.source) + const reference = await resolvedGitSourceReference(source) + if (reference) { + await preserveComposerDependencyReference(consumer, overlay.package, reference, stagedConsumers) + } const stagingRoot = await mkdtemp(join(tmpdir(), "wp-codebox-dependency-overlay-")) const preparedSource = await prepareComposerBackedSource(source, stagingRoot, `dependency overlay ${overlay.package}`) const target = `${consumer.target}/vendor/${composerPackageVendorPath(overlay.package)}` @@ -458,6 +464,7 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency return { source: preparedSource, sourceRef: overlay.source, + ...(reference ? { reference } : {}), target, package: overlay.package, consumer: overlay.consumer, @@ -470,6 +477,7 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency overlayKind: overlay.kind, package: overlay.package, source: overlay.source, + ...(reference ? { reference } : {}), consumer: overlay.consumer, target, digest: { sha256: digest }, @@ -478,6 +486,106 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency } } +/** + * Capture the clean checkout commit before Composer staging so runtime + * provenance remains tied to the source revision rather than its staged path. + */ +async function resolvedGitSourceReference(source: string): Promise { + try { + const status = await executeManagedHostCommand({ + command: "git", + args: ["status", "--porcelain", "--untracked-files=all"], + cwd: source, + allowedCwdRoots: [source], + timeoutMs: 10_000, + maxOutputBytes: 64 * 1024, + label: "inspect Composer overlay Git source", + }) + if (status.stdout.trim()) { + return undefined + } + + const revision = await executeManagedHostCommand({ + command: "git", + args: ["rev-parse", "--verify", "HEAD"], + cwd: source, + allowedCwdRoots: [source], + timeoutMs: 10_000, + maxOutputBytes: 64 * 1024, + label: "resolve Composer overlay Git revision", + }) + const reference = revision.stdout.trim() + return /^[a-f0-9]{40,64}$/i.test(reference) ? reference : undefined + } catch { + return undefined + } +} + +async function preserveComposerDependencyReference(consumer: PreparedExtraPlugin, packageName: string, reference: string, stagedConsumers: Set): Promise { + const installedJson = join(consumer.source, "vendor", "composer", "installed.json") + if (!await composerInstalledJsonHasPackage(installedJson, packageName)) { + return + } + + if (!stagedConsumers.has(consumer)) { + const stagingRoot = await mkdtemp(join(tmpdir(), `wp-codebox-dependency-consumer-${consumer.slug}-`)) + const stagedSource = join(stagingRoot, basename(consumer.source)) + await cp(consumer.source, stagedSource, { recursive: true }) + consumer.source = stagedSource + consumer.cleanupPaths.push(stagingRoot) + stagedConsumers.add(consumer) + } + + await writeComposerDependencyReference(join(consumer.source, "vendor", "composer", "installed.json"), packageName, reference) + await writeComposerInstalledPhpReference(join(consumer.source, "vendor", "composer", "installed.php"), packageName, reference) +} + +async function composerInstalledJsonHasPackage(path: string, packageName: string): Promise { + try { + return composerInstalledPackageRecords(JSON.parse(await readFile(path, "utf8"))).some((pkg) => pkg.name === packageName) + } catch { + return false + } +} + +async function writeComposerDependencyReference(path: string, packageName: string, reference: string): Promise { + const installed = JSON.parse(await readFile(path, "utf8")) as unknown + const pkg = composerInstalledPackageRecords(installed).find((candidate) => candidate.name === packageName) + if (!pkg) { + return + } + pkg.source = { ...(isRecord(pkg.source) ? pkg.source : {}), reference } + await writeFile(path, `${JSON.stringify(installed, null, 2)}\n`) +} + +async function writeComposerInstalledPhpReference(path: string, packageName: string, reference: string): Promise { + if (!await pathIsFile(path)) { + return + } + const contents = await readFile(path, "utf8") + const packageStart = contents.search(new RegExp(`['\"]${escapeRegExp(packageName)}['\"]\\s*=>\\s*array\\s*\\(`)) + if (packageStart < 0) { + return + } + const before = contents.slice(0, packageStart) + const entry = contents.slice(packageStart) + const updatedEntry = entry.replace(/(['"]reference['"]\s*=>\s*)(?:NULL|null|'[^']*'|"[^"]*")/, `$1'${reference}'`) + if (updatedEntry !== entry) { + await writeFile(path, before + updatedEntry) + } +} + +function composerInstalledPackageRecords(installed: unknown): Array & { name?: string }> { + if (Array.isArray(installed)) { + return installed.filter(isRecord) + } + return isRecord(installed) && Array.isArray(installed.packages) ? installed.packages.filter(isRecord) : [] +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + export async function prepareRecipeStagedFiles(recipe: WorkspaceRecipe, recipeDirectory: string): Promise { const stagedFiles: PreparedStagedFile[] = [] for (const [index, stagedFile] of (recipe.inputs?.stagedFiles ?? []).entries()) { diff --git a/scripts/composer-backed-source-hydration-smoke.ts b/scripts/composer-backed-source-hydration-smoke.ts index 9d2a3c6f..e077c6d5 100644 --- a/scripts/composer-backed-source-hydration-smoke.ts +++ b/scripts/composer-backed-source-hydration-smoke.ts @@ -1,7 +1,10 @@ import assert from "node:assert/strict" +import { execFile as execFileCallback } from "node:child_process" import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" +import { promisify } from "node:util" +import { recipeRunDependencyOverlay } from "../packages/cli/src/commands/recipe-runtime-setup.js" import { prepareRecipeDependencyOverlays, prepareRecipeRuntimeOverlays } from "../packages/cli/src/recipe-sources.js" import type { PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js" import type { WorkspaceRecipe } from "../packages/runtime-core/src/runtime-contracts.js" @@ -9,10 +12,12 @@ import type { WorkspaceRecipe } from "../packages/runtime-core/src/runtime-contr const root = await mkdtemp(join(tmpdir(), "wp-codebox-runtime-overlay-hydration-")) const overlaySource = join(root, "php-ai-client") const dependencySource = join(root, "generic-composer-package") +const nonGitDependencySource = join(root, "non-git-composer-package") const binDir = join(root, "bin") const scoperPath = join(root, "php-scoper.phar") const originalPath = process.env.PATH const originalScoper = process.env.WP_CODEBOX_PHP_SCOPER_PHAR +const execFile = promisify(execFileCallback) async function exists(path: string): Promise { try { @@ -51,6 +56,36 @@ await writeFile(join(dependencySource, "composer.json"), JSON.stringify({ autoload: { "psr-4": { "Acme\\Package\\": "src/" } }, require: { "psr/log": "^3.0" }, }, null, 2)) +await execFile("git", ["init", "--quiet"], { cwd: dependencySource }) +await execFile("git", ["add", "."], { cwd: dependencySource }) +await execFile("git", ["-c", "user.name=WP Codebox", "-c", "user.email=wp-codebox@example.test", "commit", "--quiet", "-m", "fixture"], { cwd: dependencySource }) +const { stdout: dependencyReference } = await execFile("git", ["rev-parse", "HEAD"], { cwd: dependencySource }) + +await mkdir(join(nonGitDependencySource, "src"), { recursive: true }) +await writeFile(join(nonGitDependencySource, "src", "Package.php"), " array( + 'acme/package' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'reference' => NULL, + ), + 'acme/non-git-package' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'reference' => NULL, + ), + ), +); +`) await writeFile(scoperPath, ` }).packages[0].source, undefined, "original consumer Composer provenance remains unchanged") + const runtimeInstalled = JSON.parse(await readFile(join(consumers[0].source, "vendor", "composer", "installed.json"), "utf8")) as { packages: Array<{ name: string, version: string, source?: { reference?: string } }> } + assert.deepEqual(runtimeInstalled.packages[0], { name: "acme/package", version: "1.0.0+no-version-set", source: { reference: dependencyReference.trim() } }, "runtime Composer dependency provenance includes the immutable source reference") + assert.deepEqual(runtimeInstalled.packages[1], { name: "acme/non-git-package", version: "1.0.0+no-version-set" }, "runtime Composer provenance omits unresolved source references") + const { stdout: runtimeInstalledPhp } = await execFile("php", ["-r", "echo json_encode(require $argv[1]);", join(consumers[0].source, "vendor", "composer", "installed.php")]) + const runtimePhpVersions = JSON.parse(runtimeInstalledPhp) as { versions: Record } + assert.equal(runtimePhpVersions.versions["acme/package"]?.reference, dependencyReference.trim(), "Composer runtime metadata includes the immutable source reference") + assert.equal(runtimePhpVersions.versions["acme/non-git-package"]?.reference, null, "Composer runtime metadata leaves unresolved references unchanged") assert.match(await readFile(join(overlays[0].source, "src", "Client.php"), "utf8"), /WordPress\\AiClientDependencies\\Psr\\Log\\LoggerInterface/) } finally { - await Promise.all([...overlays, ...dependencyOverlays].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true }))) + await Promise.all([...overlays, ...dependencyOverlays, ...consumers].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true }))) process.env.PATH = originalPath if (originalScoper === undefined) { delete process.env.WP_CODEBOX_PHP_SCOPER_PHAR From 22105709896efb41053810630f74b4c5d50c6c7d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 17 Jul 2026 23:21:05 -0400 Subject: [PATCH 2/2] Align Composer overlay provenance datasets --- package.json | 1 + packages/cli/src/recipe-sources.ts | 63 ++++++++++++++++-- .../composer-backed-source-hydration-smoke.ts | 10 ++- ...r-installed-versions-loader-order-smoke.ts | 66 +++++++++++++++++++ 4 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 scripts/composer-installed-versions-loader-order-smoke.ts diff --git a/package.json b/package.json index f6e28529..bd7a800e 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,7 @@ "test:evidence-bundle-digest-and-recipe-artifact": "tsx tests/evidence-bundle-digest-and-recipe-artifact.test.ts", "test:runtime-overlay-descriptors": "tsx tests/runtime-overlay-descriptors.test.ts", "test:composer-package-overlay-revision": "tsx scripts/composer-backed-source-hydration-smoke.ts", + "test:composer-installed-versions-loader-order": "tsx scripts/composer-installed-versions-loader-order-smoke.ts", "test:runtime-preset-registry": "tsx tests/runtime-preset-registry.test.ts", "test:generic-ability-runtime-run": "tsx tests/generic-ability-runtime-run.test.ts", "test:provider-runtime-contracts": "tsx tests/provider-runtime-contracts.test.ts", diff --git a/packages/cli/src/recipe-sources.ts b/packages/cli/src/recipe-sources.ts index d726c2fe..a5d1b8c4 100644 --- a/packages/cli/src/recipe-sources.ts +++ b/packages/cli/src/recipe-sources.ts @@ -453,11 +453,12 @@ async function prepareRecipeDependencyOverlay(overlay: WorkspaceRecipeDependency const source = resolve(recipeDirectory, overlay.source) await validateExistingDirectoryForOverlay(source, overlay.source) const reference = await resolvedGitSourceReference(source) + const stagingRoot = await mkdtemp(join(tmpdir(), "wp-codebox-dependency-overlay-")) + const preparedSource = await prepareComposerBackedSource(source, stagingRoot, `dependency overlay ${overlay.package}`) if (reference) { await preserveComposerDependencyReference(consumer, overlay.package, reference, stagedConsumers) + await preserveComposerPackageReference(preparedSource, overlay.package, reference) } - const stagingRoot = await mkdtemp(join(tmpdir(), "wp-codebox-dependency-overlay-")) - const preparedSource = await prepareComposerBackedSource(source, stagingRoot, `dependency overlay ${overlay.package}`) const target = `${consumer.target}/vendor/${composerPackageVendorPath(overlay.package)}` const digest = await directoryContentDigest(preparedSource) @@ -540,6 +541,11 @@ async function preserveComposerDependencyReference(consumer: PreparedExtraPlugin await writeComposerInstalledPhpReference(join(consumer.source, "vendor", "composer", "installed.php"), packageName, reference) } +async function preserveComposerPackageReference(source: string, packageName: string, reference: string): Promise { + await writeComposerDependencyReference(join(source, "vendor", "composer", "installed.json"), packageName, reference) + await writeComposerInstalledPhpReference(join(source, "vendor", "composer", "installed.php"), packageName, reference) +} + async function composerInstalledJsonHasPackage(path: string, packageName: string): Promise { try { return composerInstalledPackageRecords(JSON.parse(await readFile(path, "utf8"))).some((pkg) => pkg.name === packageName) @@ -569,10 +575,57 @@ async function writeComposerInstalledPhpReference(path: string, packageName: str } const before = contents.slice(0, packageStart) const entry = contents.slice(packageStart) - const updatedEntry = entry.replace(/(['"]reference['"]\s*=>\s*)(?:NULL|null|'[^']*'|"[^"]*")/, `$1'${reference}'`) - if (updatedEntry !== entry) { - await writeFile(path, before + updatedEntry) + const packageEntryEnd = composerInstalledPhpPackageEntryEnd(entry, packageName) + if (packageEntryEnd === undefined) { + return + } + const packageEntry = entry.slice(0, packageEntryEnd) + const remainingEntries = entry.slice(packageEntryEnd) + const updatedEntry = packageEntry.replace(/(['"]reference['"]\s*=>\s*)(?:NULL|null|'[^']*'|"[^"]*")/, `$1'${reference}'`) + if (updatedEntry !== packageEntry) { + await writeFile(path, before + updatedEntry + remainingEntries) + return + } + + // Composer may omit an unavailable reference entirely. Add it to this + // package record so InstalledVersions exposes the clean overlay revision. + const entryWithReference = packageEntry.replace( + new RegExp(`((?:['"]${escapeRegExp(packageName)}['"]\\s*=>\\s*array\\s*\\(\\s*))`), + `$1'reference' => '${reference}',\n`, + ) + if (entryWithReference !== packageEntry) { + await writeFile(path, before + entryWithReference + remainingEntries) + } +} + +function composerInstalledPhpPackageEntryEnd(entry: string, packageName: string): number | undefined { + const header = new RegExp(`['"]${escapeRegExp(packageName)}['"]\\s*=>\\s*array\\s*\\(`).exec(entry) + if (!header || header.index === undefined) { + return undefined + } + + const openingParenthesis = header.index + header[0].lastIndexOf("(") + let depth = 0 + let quote = "" + for (let index = openingParenthesis; index < entry.length; index++) { + const character = entry[index] + if (quote) { + if (character === "\\") { + index++ + } else if (character === quote) { + quote = "" + } + continue + } + if (character === "'" || character === '"') { + quote = character + } else if (character === "(") { + depth++ + } else if (character === ")" && --depth === 0) { + return index + 1 + } } + return undefined } function composerInstalledPackageRecords(installed: unknown): Array & { name?: string }> { diff --git a/scripts/composer-backed-source-hydration-smoke.ts b/scripts/composer-backed-source-hydration-smoke.ts index e077c6d5..518c0823 100644 --- a/scripts/composer-backed-source-hydration-smoke.ts +++ b/scripts/composer-backed-source-hydration-smoke.ts @@ -77,7 +77,6 @@ return array( 'versions' => array( 'acme/package' => array( 'pretty_version' => '1.0.0+no-version-set', - 'reference' => NULL, ), 'acme/non-git-package' => array( 'pretty_version' => '1.0.0+no-version-set', @@ -199,6 +198,15 @@ try { const runtimePhpVersions = JSON.parse(runtimeInstalledPhp) as { versions: Record } assert.equal(runtimePhpVersions.versions["acme/package"]?.reference, dependencyReference.trim(), "Composer runtime metadata includes the immutable source reference") assert.equal(runtimePhpVersions.versions["acme/non-git-package"]?.reference, null, "Composer runtime metadata leaves unresolved references unchanged") + const runtimePackageRow = runtimePhpVersions.versions["acme/package"] + assert.deepEqual(runtimePackageRow, { + pretty_version: "1.0.0+no-version-set", + reference: dependencyReference.trim(), + }, "the final PHP-visible package row exposes the clean Git reference") + assert.deepEqual(runtimePhpVersions.versions["acme/non-git-package"], { + pretty_version: "1.0.0+no-version-set", + reference: null, + }, "the final PHP-visible package row leaves an unavailable reference unchanged") assert.match(await readFile(join(overlays[0].source, "src", "Client.php"), "utf8"), /WordPress\\AiClientDependencies\\Psr\\Log\\LoggerInterface/) } finally { await Promise.all([...overlays, ...dependencyOverlays, ...consumers].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true }))) diff --git a/scripts/composer-installed-versions-loader-order-smoke.ts b/scripts/composer-installed-versions-loader-order-smoke.ts new file mode 100644 index 00000000..0f6a8fe9 --- /dev/null +++ b/scripts/composer-installed-versions-loader-order-smoke.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict" +import { execFile as execFileCallback } from "node:child_process" +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { promisify } from "node:util" +import { installPluginComposerAutoloadersCode, type PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js" + +const execFile = promisify(execFileCallback) +const root = await mkdtemp(join(tmpdir(), "wp-codebox-composer-installed-versions-")) +const pluginDir = join(root, "plugins", "consumer") +const muPluginDir = join(root, "mu-plugins") +const packageName = "acme/package" + +function installedVersionsSource(reference: string | null): string { + const encodedReference = reference === null ? "null" : JSON.stringify(reference) + return ` array('${packageName}' => array('reference' => ${encodedReference}))); + public static function getReference($package) { return self::$data['versions'][$package]['reference'] ?? null; } + public static function reload($data) { self::$data = $data; } +} +` +} + +function installedDataSource(reference: string | null): string { + return ` array('${packageName}' => array('reference' => ${reference === null ? "null" : JSON.stringify(reference)})));\n` +} + +async function run(reference: string | null): Promise { + await rm(root, { recursive: true, force: true }) + await mkdir(join(pluginDir, "vendor", "composer"), { recursive: true }) + await mkdir(join(pluginDir, "vendor", "acme", "package", "vendor", "composer"), { recursive: true }) + await mkdir(muPluginDir, { recursive: true }) + await writeFile(join(pluginDir, "vendor", "composer", "InstalledVersions.php"), installedVersionsSource(reference)) + await writeFile(join(pluginDir, "vendor", "composer", "installed.php"), installedDataSource(reference)) + await writeFile(join(pluginDir, "vendor", "autoload.php"), "