diff --git a/CHANGELOG.md b/CHANGELOG.md index ba80cf4d4fc1..3068d8388b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Docs: https://docs.openclaw.ai - Installer: let the local-prefix CLI installer use Alpine's `apk` Node.js, npm, and Git packages on musl Linux instead of downloading glibc Node tarballs that fail `node:sqlite`. - Scripts: use `git grep` to prefilter tracked conflict-marker scans so changed checks avoid reading every repository file on clean runs. +- Plugins: allow linked local plugin paths to probe TypeScript source entries without requiring compiled package output, restoring source-checkout plugin development on native Windows. +- CLI: route source-checkout build output to stderr before launching OpenClaw commands so stale local builds do not corrupt `--json` stdout. - Installer: install Node.js through `apk` on Alpine Linux instead of falling through to the NodeSource package-manager path. - Agents/perf: cache manifest-backed CLI provider descriptors and fallback provider resolution so model fallback retries avoid repeated bundled provider runtime scans while still invalidating across plugin reloads. - Installer: detect musl Linux shells such as Alpine as Linux instead of rejecting them before npm install. diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index 793f8d14f627..38b66341d58c 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -647,6 +647,28 @@ const createRunNodeOutputTee = (deps) => { if (!outputLogPath) { return null; } + try { + const existing = deps.fs.statSync(outputLogPath); + if (existing.isDirectory()) { + return { + outputLogPath, + write() {}, + async close() { + throw new Error(`output log path is a directory: ${outputLogPath}`); + }, + }; + } + } catch (error) { + if (error?.code && error.code !== "ENOENT") { + return { + outputLogPath, + write() {}, + async close() { + throw error; + }, + }; + } + } deps.fs.mkdirSync(path.dirname(outputLogPath), { recursive: true }); const stream = deps.fs.createWriteStream(outputLogPath, { flags: "a", @@ -936,8 +958,9 @@ const runOpenClaw = async (deps) => { return res.exitCode ?? 1; }; -const pipeSpawnedOutput = (childProcess, deps) => { - if (!shouldPipeSpawnedOutput(deps)) { +const pipeSpawnedOutput = (childProcess, deps, options = {}) => { + const stdoutTarget = options.stdoutTarget ?? "stdout"; + if (!shouldPipeSpawnedOutput(deps) && stdoutTarget !== "stderr") { return; } const stderrFilter = @@ -945,7 +968,8 @@ const pipeSpawnedOutput = (childProcess, deps) => { ? createSyncIoTraceStderrFilter(deps) : null; childProcess.stdout?.on("data", (chunk) => { - writeRunnerStream(deps, deps.stdout, chunk); + const target = stdoutTarget === "stderr" ? deps.stderr : deps.stdout; + writeRunnerStream(deps, target, chunk); deps.outputTee?.write(chunk); }); childProcess.stderr?.on("data", (chunk) => { @@ -1389,9 +1413,9 @@ export async function runNodeMain(params = {}) { const assetBuild = deps.spawn(buildCmd, bundledPluginAssetBuildArgs, { cwd: deps.cwd, env: deps.env, - stdio: shouldPipeSpawnedOutput(deps) ? ["inherit", "pipe", "pipe"] : "inherit", + stdio: ["inherit", "pipe", "pipe"], }); - pipeSpawnedOutput(assetBuild, deps); + pipeSpawnedOutput(assetBuild, deps, { stdoutTarget: "stderr" }); const assetBuildRes = await waitForSpawnedProcess(assetBuild, deps); const assetBuildInterruptedExitCode = getInterruptedSpawnExitCode(assetBuildRes); if (assetBuildInterruptedExitCode !== null) { @@ -1407,9 +1431,9 @@ export async function runNodeMain(params = {}) { ...deps.env, [RUN_NODE_SKIP_DTS_BUILD_ENV]: deps.env[RUN_NODE_SKIP_DTS_BUILD_ENV] ?? "1", }, - stdio: shouldPipeSpawnedOutput(deps) ? ["inherit", "pipe", "pipe"] : "inherit", + stdio: ["inherit", "pipe", "pipe"], }); - pipeSpawnedOutput(build, deps); + pipeSpawnedOutput(build, deps, { stdoutTarget: "stderr" }); const buildRes = await waitForSpawnedProcess(build, deps); const interruptedExitCode = getInterruptedSpawnExitCode(buildRes); diff --git a/src/cli/plugins-cli.install.test.ts b/src/cli/plugins-cli.install.test.ts index 904c3d08d904..a87a23cf021e 100644 --- a/src/cli/plugins-cli.install.test.ts +++ b/src/cli/plugins-cli.install.test.ts @@ -302,6 +302,7 @@ type MockWithCalls = { }; type PluginInstallCall = { + allowSourceTypeScriptEntries?: boolean; archivePath?: string; dangerouslyForceUnsafeInstall?: boolean; dryRun?: boolean; @@ -1321,6 +1322,7 @@ describe("plugins cli install", () => { expect(pathInstallCall().path).toBe(tmpRoot); expect(pathInstallCall().dryRun).toBe(true); + expect(pathInstallCall().allowSourceTypeScriptEntries).toBe(true); expect(pathInstallCall().dangerouslyForceUnsafeInstall).toBe(true); }); @@ -1536,6 +1538,7 @@ describe("plugins cli install", () => { expect(pathInstallCall().path).toBe(localPluginDir); expect(pathInstallCall().dryRun).toBe(true); + expect(pathInstallCall().allowSourceTypeScriptEntries).toBe(true); expect(pathInstallCall().dangerouslyForceUnsafeInstall).toBe(true); expect(typeof pathInstallCall().logger?.info).toBe("function"); expect(typeof pathInstallCall().logger?.warn).toBe("function"); diff --git a/src/cli/plugins-install-command.ts b/src/cli/plugins-install-command.ts index 0ee21bd3c159..439000e39c7b 100644 --- a/src/cli/plugins-install-command.ts +++ b/src/cli/plugins-install-command.ts @@ -693,6 +693,7 @@ export async function runPluginInstallCommand(params: { mode: installMode, path: resolved, dryRun: true, + allowSourceTypeScriptEntries: true, extensionsDir, logger: createPluginInstallLogger(runtime), }); diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index 54190851c8f1..f717d9bc9850 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -559,6 +559,68 @@ describe("run-node script", () => { }); }); + it("routes local build stdout to stderr before JSON command output", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await writeRuntimePostBuildScaffold(tmp); + const outputPath = path.join(tmp, ".artifacts", "run-node", "output.log"); + const spawn = (_cmd: string, args: string[]) => { + if (args[0] === "scripts/bundled-plugin-assets.mjs") { + return createPipedExitedProcess({ + stdout: "asset stdout\n", + stderr: "asset stderr\n", + }); + } + if (args[0] === "scripts/tsdown-build.mjs") { + return createPipedExitedProcess({ + stdout: "build stdout\n", + stderr: "build stderr\n", + }); + } + return createPipedExitedProcess({ stdout: '{"plugins":[]}\n' }); + }; + const stdoutChunks: string[] = []; + const stderrChunks: string[] = []; + const stdout = { + write: (chunk: string | Buffer) => { + stdoutChunks.push(String(chunk)); + return true; + }, + } as unknown as NodeJS.WriteStream; + const stderr = { + write: (chunk: string | Buffer) => { + stderrChunks.push(String(chunk)); + return true; + }, + } as unknown as NodeJS.WriteStream; + + const exitCode = await runNodeMain({ + cwd: tmp, + args: ["plugins", "list", "--json"], + env: { + ...process.env, + OPENCLAW_FORCE_BUILD: "1", + OPENCLAW_RUNNER_LOG: "0", + OPENCLAW_RUN_NODE_OUTPUT_LOG: outputPath, + }, + spawn, + stdout, + stderr, + execPath: process.execPath, + platform: process.platform, + } as Parameters[0] & { + stdout: NodeJS.WriteStream; + stderr: NodeJS.WriteStream; + }); + + expect(exitCode).toBe(0); + expect(stdoutChunks.join("")).toBe('{"plugins":[]}\n'); + expect(stderrChunks.join("")).toContain("asset stdout\n"); + expect(stderrChunks.join("")).toContain("asset stderr\n"); + expect(stderrChunks.join("")).toContain("build stdout\n"); + expect(stderrChunks.join("")).toContain("build stderr\n"); + }); + }); + it("routes sync I/O trace stderr blocks to the output log without flooding stderr", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp); diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index 3aed8f10fae4..1342e5589051 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -879,6 +879,45 @@ describe("discoverOpenClawPlugins", () => { }); }); + it("allows linked local install records to point at TypeScript source entries", async () => { + const stateDir = makeTempDir(); + const pluginDir = path.join(stateDir, "extensions", "linked-source-pack"); + mkdirSafe(path.join(pluginDir, "src")); + + writePluginPackageManifest({ + packageDir: pluginDir, + packageName: "@openclaw/linked-source-pack", + extensions: ["./src/index.ts"], + setupEntry: "./src/setup-entry.ts", + }); + writePluginManifest({ pluginDir, id: "linked-source-pack" }); + writePluginEntry(path.join(pluginDir, "src", "index.ts")); + writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts")); + + const installRecords = { + "linked-source-pack": { + source: "path", + installPath: pluginDir, + sourcePath: pluginDir, + }, + } satisfies Record; + const result = await discoverWithStateDir(stateDir, { installRecords }); + + expectCandidateSource( + result.candidates, + "linked-source-pack", + fs.realpathSync(path.join(pluginDir, "src", "index.ts")), + ); + expectCandidateFields(requireCandidateById(result.candidates, "linked-source-pack"), { + setupSource: fs.realpathSync(path.join(pluginDir, "src", "setup-entry.ts")), + }); + expectNoDiagnostic({ + diagnostics: result.diagnostics, + pluginId: "linked-source-pack", + messageIncludes: "requires compiled runtime output", + }); + }); + it("still requires compiled runtime output for tracked installed package plugins", async () => { const stateDir = makeTempDir(); const pluginDir = path.join(stateDir, "extensions", "source-only-pack"); diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index b2429ef38329..1437836a4493 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -389,11 +389,40 @@ function mergeDiscoveryResult( } } +type InstalledPluginRecordPath = { + path: string; + requireBuiltRuntimeEntry: boolean; +}; + +function isLinkedLocalPluginRecord(params: { + record: PluginInstallRecord; + env: NodeJS.ProcessEnv; + realpathCache: Map; +}): boolean { + if (params.record.source !== "path") { + return false; + } + if ( + typeof params.record.sourcePath !== "string" || + !params.record.sourcePath.trim() || + typeof params.record.installPath !== "string" || + !params.record.installPath.trim() + ) { + return false; + } + return resolvesToSameDirectory( + resolveUserPath(params.record.sourcePath, params.env), + resolveUserPath(params.record.installPath, params.env), + params.realpathCache, + ); +} + function collectInstalledPluginRecordPaths( installRecords: Record | undefined, env: NodeJS.ProcessEnv, -): string[] { - const paths: string[] = []; + realpathCache: Map, +): InstalledPluginRecordPath[] { + const paths: InstalledPluginRecordPath[] = []; const seen = new Set(); for (const record of Object.values(installRecords ?? {})) { const rawPath = @@ -410,7 +439,10 @@ function collectInstalledPluginRecordPaths( continue; } seen.add(resolved); - paths.push(resolved); + paths.push({ + path: resolved, + requireBuiltRuntimeEntry: !isLinkedLocalPluginRecord({ record, env, realpathCache }), + }); } return paths; } @@ -1380,19 +1412,26 @@ export function discoverOpenClawPlugins(params: { skipDirectories: readChildDirectoryNames(roots.stock), }); } - const installedPaths = collectInstalledPluginRecordPaths(params.installRecords, env); - const installedPluginDirKeys = collectManagedPluginDirKeys(installedPaths, realpathCache); + const installedPaths = collectInstalledPluginRecordPaths( + params.installRecords, + env, + realpathCache, + ); + const installedPluginDirKeys = collectManagedPluginDirKeys( + installedPaths.map((installedPath) => installedPath.path), + realpathCache, + ); const managedPluginDirs = collectManagedPluginDirKeys( collectManagedPluginRecordPaths(params.installRecords, env), realpathCache, ); for (const installedPath of installedPaths) { discoverFromPath({ - rawPath: installedPath, + rawPath: installedPath.path, origin: "global", ownershipUid: params.ownershipUid, workspaceDir, - requireBuiltRuntimeEntry: true, + requireBuiltRuntimeEntry: installedPath.requireBuiltRuntimeEntry, managedPluginDirs, env, candidates: result.candidates, diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 6f8e02b78803..fb3f8a397d58 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -293,6 +293,10 @@ function expectWarningExcludes(warnings: readonly string[], fragment: string) { expect(warnings.join("\n")).not.toContain(fragment); } +function expectMessageIncludesPath(message: string, fragment: string) { + expect(message.replaceAll("\\", "/")).toContain(fragment); +} + function requireRecord(value: unknown, label: string): Record { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`expected ${label} to be an object`); @@ -1187,6 +1191,33 @@ describe("installPluginFromArchive", () => { } }); + it("allows linked source probes when TypeScript extension entries have no compiled runtime output", async () => { + const { pluginDir, extensionsDir } = setupPluginInstallDirs(); + fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true }); + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify({ + name: "source-link-runtime-plugin", + version: "1.0.0", + openclaw: { extensions: ["./src/index.ts"] }, + }), + ); + fs.writeFileSync(path.join(pluginDir, "src", "index.ts"), "export {};\n"); + + const result = await installPluginFromDir({ + dirPath: pluginDir, + extensionsDir, + dryRun: true, + allowSourceTypeScriptEntries: true, + }); + + if (!result.ok) { + throw new Error(result.error); + } + expect(result.pluginId).toBe("source-link-runtime-plugin"); + expect(result.targetDir).toBe(resolvePluginInstallDir(result.pluginId, extensionsDir)); + }); + it("rejects package installs when runtimeExtensions length does not match extensions", async () => { const { pluginDir, extensionsDir } = setupPluginInstallDirs(); fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true }); @@ -1450,7 +1481,7 @@ describe("installPluginFromArchive", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); - expect(result.error).toContain("dist/payload.js"); + expectMessageIncludesPath(result.error, "dist/payload.js"); } expectWarningIncludes(warnings, "dangerous code pattern"); }); @@ -1601,7 +1632,7 @@ describe("installPluginFromArchive", () => { if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('blocked dependencies "plain-crypto-js" in dependencies'); - expect(result.error).toContain("declared in axios (vendor/axios/package.json)"); + expectMessageIncludesPath(result.error, "declared in axios (vendor/axios/package.json)"); } }); @@ -1628,7 +1659,7 @@ describe("installPluginFromArchive", () => { if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('blocked dependency directory "plain-crypto-js"'); - expect(result.error).toContain("vendor/node_modules/plain-crypto-js"); + expectMessageIncludesPath(result.error, "vendor/node_modules/plain-crypto-js"); } }); @@ -1655,7 +1686,7 @@ describe("installPluginFromArchive", () => { if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"'); - expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js.Js"); + expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js.Js"); } }); @@ -1682,7 +1713,7 @@ describe("installPluginFromArchive", () => { if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"'); - expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js"); + expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js"); } }); @@ -2085,7 +2116,8 @@ describe("installPluginFromArchive", () => { if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('blocked dependencies "plain-crypto-js" as package name'); - expect(result.error).toContain( + expectMessageIncludesPath( + result.error, "vendor/pkg-127/node_modules/nested-safe/node_modules/plain-crypto-js/package.json", ); } @@ -2410,7 +2442,7 @@ describe("installPluginFromArchive", () => { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('Bundle "blocked-dependency-bundle" installation blocked'); expect(result.error).toContain('blocked dependencies "plain-crypto-js" in dependencies'); - expect(result.error).toContain("declared in axios (vendor/axios/package.json)"); + expectMessageIncludesPath(result.error, "declared in axios (vendor/axios/package.json)"); } expect( warnings.some((warning) => @@ -2442,7 +2474,8 @@ describe("installPluginFromArchive", () => { 'Bundle "blocked-vendored-package-name-bundle" installation blocked', ); expect(result.error).toContain('"plain-crypto-js" as package name'); - expect(result.error).toContain( + expectMessageIncludesPath( + result.error, "declared in plain-crypto-js (vendor/plain-crypto-js/package.json)", ); } @@ -2464,7 +2497,7 @@ describe("installPluginFromArchive", () => { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('Bundle "blocked-package-dir-bundle" installation blocked'); expect(result.error).toContain('blocked dependency directory "plain-crypto-js"'); - expect(result.error).toContain("vendor/node_modules/plain-crypto-js"); + expectMessageIncludesPath(result.error, "vendor/node_modules/plain-crypto-js"); } }); @@ -2484,7 +2517,7 @@ describe("installPluginFromArchive", () => { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('Bundle "blocked-package-file-bundle" installation blocked'); expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"'); - expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js.Js"); + expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js.Js"); } }); @@ -2506,7 +2539,7 @@ describe("installPluginFromArchive", () => { 'Bundle "blocked-package-extensionless-file-bundle" installation blocked', ); expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"'); - expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js"); + expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js"); } }); @@ -3111,7 +3144,7 @@ describe("installPluginFromDir", () => { if (!result.ok) { expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED); expect(result.error).toContain('blocked dependencies "plain-crypto-js" as package name'); - expect(result.error).toContain("node_modules/plain-crypto-js/package.json"); + expectMessageIncludesPath(result.error, "node_modules/plain-crypto-js/package.json"); } expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled(); }); diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 4381412e6873..9bdec4780009 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -945,6 +945,7 @@ type PackageInstallCommonParams = InstallSafetyOverrides & { dryRun?: boolean; expectedPluginId?: string; requirePluginManifest?: boolean; + allowSourceTypeScriptEntries?: boolean; installPolicyRequest?: PluginInstallPolicyRequest; }; @@ -973,6 +974,7 @@ function pickPackageInstallCommonParams( dryRun: params.dryRun, expectedPluginId: params.expectedPluginId, requirePluginManifest: params.requirePluginManifest, + allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries, installPolicyRequest: params.installPolicyRequest, }; } @@ -1313,6 +1315,7 @@ async function validatePackagePluginInstallSource(params: { packageDir: string; expectedPluginId?: string; requirePluginManifest?: boolean; + allowSourceTypeScriptEntries?: boolean; dangerouslyForceUnsafeInstall?: boolean; trustedSourceLinkedOfficialInstall?: boolean; installPolicyRequest?: PluginInstallPolicyRequest; @@ -1422,6 +1425,7 @@ async function validatePackagePluginInstallSource(params: { packageDir: params.packageDir, extensions, manifest, + allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries, }); if (!extensionValidation.ok) { return { @@ -1531,6 +1535,7 @@ export async function installPluginFromInstalledPackageDir( packageDir: params.packageDir, expectedPluginId: params.expectedPluginId, requirePluginManifest: params.requirePluginManifest, + allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries, dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall, installPolicyRequest: params.installPolicyRequest, @@ -1598,6 +1603,7 @@ async function installPluginFromPackageDir( packageDir: params.packageDir, expectedPluginId: params.expectedPluginId, requirePluginManifest: params.requirePluginManifest, + allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries, dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall, installPolicyRequest: params.installPolicyRequest, diff --git a/src/plugins/package-entry-resolution.ts b/src/plugins/package-entry-resolution.ts index fde87d6568bd..3de2ad02e85d 100644 --- a/src/plugins/package-entry-resolution.ts +++ b/src/plugins/package-entry-resolution.ts @@ -142,6 +142,7 @@ export async function validatePackageExtensionEntriesForInstall(params: { packageDir: string; extensions: string[]; manifest: PackageManifest; + allowSourceTypeScriptEntries?: boolean; }): Promise<{ ok: true } | { ok: false; error: string }> { const runtimeResolution = resolvePackageRuntimeExtensionEntries({ manifest: params.manifest, @@ -198,6 +199,14 @@ export async function validatePackageExtensionEntriesForInstall(params: { continue; } + if ( + sourceEntry.exists && + isTypeScriptPackageEntry(entry) && + params.allowSourceTypeScriptEntries + ) { + continue; + } + if (sourceEntry.exists && isTypeScriptPackageEntry(entry)) { return { ok: false, @@ -282,6 +291,14 @@ export async function validatePackageExtensionEntriesForInstall(params: { return { ok: true }; } + if ( + sourceEntry.exists && + isTypeScriptPackageEntry(setupEntry) && + params.allowSourceTypeScriptEntries + ) { + return { ok: true }; + } + if (sourceEntry.exists && isTypeScriptPackageEntry(setupEntry)) { return { ok: false,