build: add assertions to electron workflow modifiers (#11003)

- Add post-condition assertions to all file modification operations
- Add verify-desktop-patch.yml workflow for CI validation
- Add invariant, updateFile, writeFileEnsuring, removePathEnsuring utilities
- Improve error messages and validation in workflow scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Innei 2025-12-27 02:04:53 +08:00 committed by GitHub
parent 841f3e4db5
commit a763f12fd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 420 additions and 217 deletions

View file

@ -62,13 +62,9 @@ jobs:
- name: Install deps
run: bun i
env:
NODE_OPTIONS: --max-old-space-size=6144
- name: Lint
run: bun run lint
env:
NODE_OPTIONS: --max-old-space-size=6144
version:
name: Determine version

View file

@ -0,0 +1,55 @@
name: Verify Desktop Patch
on:
push:
branches:
- main
- next
- dev
paths:
- 'scripts/electronWorkflow/**'
- 'src/libs/next/config/**'
- 'src/app/**'
- 'src/layout/**'
- 'src/components/mdx/**'
- 'src/features/DevPanel/**'
- 'src/server/translation.ts'
pull_request:
paths:
- 'scripts/electronWorkflow/**'
- 'src/libs/next/config/**'
- 'src/app/**'
- 'src/layout/**'
- 'src/components/mdx/**'
- 'src/features/DevPanel/**'
- 'src/server/translation.ts'
workflow_dispatch:
permissions:
contents: read
env:
NODE_VERSION: 24.11.1
BUN_VERSION: 1.2.23
jobs:
verify:
name: Desktop patch smoke test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node & Bun
uses: ./.github/actions/setup-node-bun
with:
node-version: ${{ env.NODE_VERSION }}
bun-version: ${{ env.BUN_VERSION }}
- name: Install deps
run: bun i
- name: Verify desktop patch
run: bun scripts/electronWorkflow/modifiers/index.mts

View file

@ -1,19 +1,14 @@
import { Lang, parse } from '@ast-grep/napi';
import fs from 'fs-extra';
import path from 'node:path';
import { isDirectRun, runStandalone } from './utils.mjs';
const rewriteFile = async (filePath: string, transformer: (code: string) => string) => {
if (!fs.existsSync(filePath)) return;
const original = await fs.readFile(filePath, 'utf8');
const updated = transformer(original);
if (updated !== original) {
await fs.writeFile(filePath, updated);
}
};
import {
isDirectRun,
normalizeEol,
removePathEnsuring,
runStandalone,
updateFile,
writeFileEnsuring,
} from './utils.mjs';
const desktopOnlyVariantsPage = `import { DynamicLayoutProps } from '@/types/next';
@ -35,6 +30,11 @@ const stripDevPanel = (code: string) => {
return result;
};
const assertDevPanelStripped = (code: string) =>
!/import\s+DevPanel\s+from\s+['"]@\/features\/DevPanel['"]/.test(code) &&
!/<DevPanel\b/.test(code) &&
!/NEXT_PUBLIC_ENABLE_DEV_PANEL|DevPanel\s*\/>/.test(code);
const removeSecurityTab = (code: string) => {
const componentEntryRegex =
/[\t ]*\[SettingsTabs\.Security]: dynamic\(\(\) => import\('\.\.\/security'\), {[\s\S]+?}\),\s*\r?\n/;
@ -43,6 +43,8 @@ const removeSecurityTab = (code: string) => {
return code.replace(componentEntryRegex, '').replace(securityTabRegex, '');
};
const assertSecurityTabRemoved = (code: string) => !/\bSettingsTabs\.Security\b/.test(code);
const removeSpeedInsightsAndAnalytics = (code: string) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
@ -130,6 +132,12 @@ const removeSpeedInsightsAndAnalytics = (code: string) => {
return result;
};
const assertSpeedInsightsAndAnalyticsRemoved = (code: string) =>
!/<Analytics\s*\/>/.test(code) &&
!/<SpeedInsights\s*\/>/.test(code) &&
!/import\s+\{\s*SpeedInsights\s*\}\s+from\b/.test(code) &&
!/import\s+Analytics\s+from\b/.test(code);
const removeClerkLogic = (code: string) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
@ -244,6 +252,10 @@ const removeClerkLogic = (code: string) => {
return result;
};
const assertClerkLogicRemoved = (code: string) =>
!/\bNEXT_PUBLIC_ENABLE_CLERK_AUTH\b/.test(code) &&
!/\bauthEnv\.NEXT_PUBLIC_ENABLE_CLERK_AUTH\b/.test(code);
const removeManifestFromMetadata = (code: string) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
@ -320,65 +332,90 @@ const removeManifestFromMetadata = (code: string) => {
return result;
};
const assertMetadataManifestRemoved = (code: string) =>
!/\bmanifest\s*:/.test(code) && !/\bmetadataBase\s*:/.test(code);
export const modifyAppCode = async (TEMP_DIR: string) => {
// 1. Replace src/app/[variants]/page.tsx with a desktop-only entry
const variantsPagePath = path.join(TEMP_DIR, 'src/app/[variants]/page.tsx');
if (fs.existsSync(variantsPagePath)) {
console.log(' Processing src/app/[variants]/page.tsx...');
await fs.writeFile(variantsPagePath, desktopOnlyVariantsPage);
}
console.log(' Processing src/app/[variants]/page.tsx...');
await writeFileEnsuring({
filePath: variantsPagePath,
name: 'modifyAppCode:variantsPage',
text: desktopOnlyVariantsPage,
assertAfter: (code) => normalizeEol(code) === normalizeEol(desktopOnlyVariantsPage),
});
// 2. Remove DevPanel from src/layout/GlobalProvider/index.tsx
const globalProviderPath = path.join(TEMP_DIR, 'src/layout/GlobalProvider/index.tsx');
if (fs.existsSync(globalProviderPath)) {
console.log(' Processing src/layout/GlobalProvider/index.tsx...');
await rewriteFile(globalProviderPath, stripDevPanel);
}
console.log(' Processing src/layout/GlobalProvider/index.tsx...');
await updateFile({
filePath: globalProviderPath,
name: 'modifyAppCode:stripDevPanel',
transformer: stripDevPanel,
assertAfter: assertDevPanelStripped,
});
// 3. Delete src/app/[variants]/(main)/settings/security directory
const securityDirPath = path.join(TEMP_DIR, 'src/app/[variants]/(main)/settings/security');
if (fs.existsSync(securityDirPath)) {
console.log(' Deleting src/app/[variants]/(main)/settings/security directory...');
await fs.remove(securityDirPath);
}
console.log(' Deleting src/app/[variants]/(main)/settings/security directory...');
await removePathEnsuring({
name: 'modifyAppCode:deleteSecurityDir',
path: securityDirPath,
});
// 4. Remove Security tab wiring from SettingsContent
const settingsContentPath = path.join(
TEMP_DIR,
'src/app/[variants]/(main)/settings/features/SettingsContent.tsx',
);
if (fs.existsSync(settingsContentPath)) {
console.log(' Processing src/app/[variants]/(main)/settings/features/SettingsContent.tsx...');
await rewriteFile(settingsContentPath, removeSecurityTab);
}
console.log(' Processing src/app/[variants]/(main)/settings/features/SettingsContent.tsx...');
await updateFile({
filePath: settingsContentPath,
name: 'modifyAppCode:removeSecurityTab',
transformer: removeSecurityTab,
assertAfter: assertSecurityTabRemoved,
});
// 5. Remove SpeedInsights and Analytics from src/app/[variants]/layout.tsx
const variantsLayoutPath = path.join(TEMP_DIR, 'src/app/[variants]/layout.tsx');
if (fs.existsSync(variantsLayoutPath)) {
console.log(' Processing src/app/[variants]/layout.tsx...');
await rewriteFile(variantsLayoutPath, removeSpeedInsightsAndAnalytics);
}
console.log(' Processing src/app/[variants]/layout.tsx...');
await updateFile({
filePath: variantsLayoutPath,
name: 'modifyAppCode:removeSpeedInsightsAndAnalytics',
transformer: removeSpeedInsightsAndAnalytics,
assertAfter: assertSpeedInsightsAndAnalyticsRemoved,
});
// 6. Remove Clerk logic from src/layout/AuthProvider/index.tsx
const authProviderPath = path.join(TEMP_DIR, 'src/layout/AuthProvider/index.tsx');
if (fs.existsSync(authProviderPath)) {
console.log(' Processing src/layout/AuthProvider/index.tsx...');
await rewriteFile(authProviderPath, removeClerkLogic);
}
console.log(' Processing src/layout/AuthProvider/index.tsx...');
await updateFile({
filePath: authProviderPath,
name: 'modifyAppCode:removeClerkLogic',
transformer: removeClerkLogic,
assertAfter: assertClerkLogicRemoved,
});
// 7. Replace mdx Image component with next/image export
const mdxImagePath = path.join(TEMP_DIR, 'src/components/mdx/Image.tsx');
if (fs.existsSync(mdxImagePath)) {
console.log(' Processing src/components/mdx/Image.tsx...');
await fs.writeFile(mdxImagePath, "export { default } from 'next/image';\n");
}
console.log(' Processing src/components/mdx/Image.tsx...');
await writeFileEnsuring({
filePath: mdxImagePath,
name: 'modifyAppCode:replaceMdxImage',
text: "export { default } from 'next/image';\n",
assertAfter: (code) => normalizeEol(code).trim() === "export { default } from 'next/image';",
});
// 8. Remove manifest from metadata
const metadataPath = path.join(TEMP_DIR, 'src/app/[variants]/metadata.ts');
if (fs.existsSync(metadataPath)) {
console.log(' Processing src/app/[variants]/metadata.ts...');
await rewriteFile(metadataPath, removeManifestFromMetadata);
}
console.log(' Processing src/app/[variants]/metadata.ts...');
await updateFile({
filePath: metadataPath,
name: 'modifyAppCode:removeManifestFromMetadata',
transformer: removeManifestFromMetadata,
assertAfter: assertMetadataManifestRemoved,
});
};
if (isDirectRun(import.meta.url)) {

View file

@ -1,41 +1,44 @@
import { Lang, parse } from '@ast-grep/napi';
import fs from 'fs-extra';
import path from 'node:path';
import { isDirectRun, runStandalone } from './utils.mjs';
import { isDirectRun, runStandalone, updateFile } from './utils.mjs';
const hasUseServerDirective = (code: string) =>
/^\s*['"]use server['"]\s*;?/m.test(code.trimStart());
export const cleanUpCode = async (TEMP_DIR: string) => {
// Remove 'use server'
const filesToRemoveUseServer = [
'src/components/mdx/Image.tsx',
'src/features/DevPanel/CacheViewer/getCacheEntries.ts',
'src/server/translation.ts',
];
for (const file of filesToRemoveUseServer) {
const filePath = path.join(TEMP_DIR, file);
if (fs.existsSync(filePath)) {
console.log(` Processing ${file}...`);
const code = await fs.readFile(filePath, 'utf8');
const ast = parse(Lang.TypeScript, code);
const root = ast.root();
console.log(` Processing ${file}...`);
await updateFile({
filePath,
name: `cleanUpCode:removeUseServer:${file}`,
transformer: (code) => {
// Prefer a deterministic text rewrite for directive prologue:
// remove ONLY the top-level `'use server';` directive if present.
const next = code.replace(/^\s*['"]use server['"]\s*;\s*\r?\n?/, '');
if (next !== code) return next;
// 'use server' is usually an expression statement at the top
// We look for the literal string 'use server' or "use server"
const useServer =
root.find({
rule: {
pattern: "'use server'",
},
}) ||
root.find({
rule: {
pattern: '"use server"',
},
});
// Fallback to AST rewrite (in case of odd formatting)
const ast = parse(Lang.TypeScript, code);
const root = ast.root();
const useServer =
root.find({
rule: { pattern: "'use server'" },
}) ||
root.find({
rule: { pattern: '"use server"' },
});
if (!useServer) return code;
if (useServer) {
// Find the statement containing this string
let curr = useServer.parent();
while (curr) {
if (curr.kind() === 'expression_statement') {
@ -45,10 +48,11 @@ export const cleanUpCode = async (TEMP_DIR: string) => {
if (curr.kind() === 'program') break;
curr = curr.parent();
}
}
await fs.writeFile(filePath, root.text());
}
return root.text();
},
assertAfter: (code) => !hasUseServerDirective(code),
});
}
};

View file

@ -2,7 +2,7 @@ import { Lang, parse } from '@ast-grep/napi';
import fs from 'fs-extra';
import path from 'node:path';
import { isDirectRun, runStandalone } from './utils.mjs';
import { invariant, isDirectRun, runStandalone, updateFile } from './utils.mjs';
interface Edit {
end: number;
@ -11,123 +11,149 @@ interface Edit {
}
export const modifyNextConfig = async (TEMP_DIR: string) => {
const nextConfigPath = path.join(TEMP_DIR, 'next.config.ts');
if (!fs.existsSync(nextConfigPath)) return;
const defineConfigPath = path.join(TEMP_DIR, 'src', 'libs', 'next', 'config', 'define-config.ts');
const legacyNextConfigPath = path.join(TEMP_DIR, 'next.config.ts');
console.log(' Processing next.config.ts...');
const code = await fs.readFile(nextConfigPath, 'utf8');
const ast = parse(Lang.TypeScript, code);
const root = ast.root();
const edits: Edit[] = [];
const nextConfigPath = fs.existsSync(defineConfigPath) ? defineConfigPath : legacyNextConfigPath;
if (!fs.existsSync(nextConfigPath)) {
throw new Error(`[modifyNextConfig] next config not found: ${nextConfigPath}`);
}
// Find nextConfig declaration
const nextConfigDecl = root.find({
rule: {
pattern: 'const nextConfig: NextConfig = { $$$ }',
},
});
console.log(` Processing ${path.relative(TEMP_DIR, nextConfigPath)}...`);
await updateFile({
filePath: nextConfigPath,
name: 'modifyNextConfig',
transformer: (code) => {
const ast = parse(Lang.TypeScript, code);
const root = ast.root();
const edits: Edit[] = [];
if (nextConfigDecl) {
// 1. Remove redirects
const redirectsProp = nextConfigDecl.find({
rule: {
kind: 'property_identifier',
regex: '^redirects$',
},
});
if (redirectsProp) {
let curr = redirectsProp.parent();
while (curr) {
if (curr.kind() === 'pair') {
const range = curr.range();
edits.push({ end: range.end.index, start: range.start.index, text: '' });
break;
}
if (curr.kind() === 'object') break;
curr = curr.parent();
}
}
// 2. Remove headers
const headersProp = nextConfigDecl.find({
rule: {
kind: 'property_identifier',
regex: '^headers$',
},
});
if (headersProp) {
let curr = headersProp.parent();
while (curr) {
if (curr.kind() === 'pair' || curr.kind() === 'method_definition') {
const range = curr.range();
edits.push({ end: range.end.index, start: range.start.index, text: '' });
break;
}
if (curr.kind() === 'object') break;
curr = curr.parent();
}
}
// 3. Remove spread element
const spread = nextConfigDecl.find({
rule: {
kind: 'spread_element',
},
});
if (spread) {
const range = spread.range();
edits.push({ end: range.end.index, start: range.start.index, text: '' });
}
// 4. Inject output: 'export'
const objectNode = nextConfigDecl.find({
rule: { kind: 'object' },
});
if (objectNode) {
const range = objectNode.range();
// Insert after the opening brace `{
edits.push({
end: range.start.index + 1,
start: range.start.index + 1,
text: "\n output: 'export',",
// Find nextConfig declaration
const nextConfigDecl = root.find({
rule: {
pattern: 'const nextConfig: NextConfig = { $$$ }',
},
});
}
}
if (!nextConfigDecl) {
throw new Error('[modifyNextConfig] nextConfig declaration not found');
}
// Remove withPWA wrapper
const withPWA = root.find({
rule: {
pattern: 'withPWA($A)',
// 1. Remove redirects
const redirectsPair = nextConfigDecl.find({
rule: {
pattern: 'redirects: $A',
},
});
if (redirectsPair) {
const range = redirectsPair.range();
edits.push({ end: range.end.index, start: range.start.index, text: '' });
}
// 2. Remove headers
const headersMethod = nextConfigDecl
.findAll({
rule: {
kind: 'method_definition',
},
})
.find((node) => {
const text = node.text();
return text.startsWith('async headers') || text.startsWith('headers');
});
if (headersMethod) {
const range = headersMethod.range();
edits.push({ end: range.end.index, start: range.start.index, text: '' });
}
// 3. Remove spread element
const spreads = nextConfigDecl.findAll({
rule: {
kind: 'spread_element',
},
});
const isObjectLevelSpread = (node: any) => node.parent()?.kind() === 'object';
const standaloneSpread = spreads.find((node) => {
if (!isObjectLevelSpread(node)) return false;
const text = node.text();
return text.includes('isStandaloneMode') && text.includes('standaloneConfig');
});
const objectLevelSpread = standaloneSpread ? null : spreads.find(isObjectLevelSpread);
const spreadToRemove = standaloneSpread || objectLevelSpread;
if (spreadToRemove) {
const range = spreadToRemove.range();
edits.push({ end: range.end.index, start: range.start.index, text: '' });
}
// 4. Inject/force output: 'export'
const outputPair = nextConfigDecl.find({
rule: {
pattern: 'output: $A',
},
});
if (outputPair) {
const range = outputPair.range();
edits.push({ end: range.end.index, start: range.start.index, text: "output: 'export'" });
} else {
const objectNode = nextConfigDecl.find({
rule: { kind: 'object' },
});
if (!objectNode) {
throw new Error('[modifyNextConfig] nextConfig object not found');
}
{
const range = objectNode.range();
// Insert after the opening brace `{
edits.push({
end: range.start.index + 1,
start: range.start.index + 1,
text: "\n output: 'export',",
});
}
}
// Remove withPWA wrapper
const withPWA = root.find({
rule: {
pattern: 'withPWA($A)',
},
});
if (withPWA) {
const inner = withPWA.getMatch('A');
if (!inner) {
throw new Error('[modifyNextConfig] withPWA inner config not found');
}
{
const range = withPWA.range();
edits.push({ end: range.end.index, start: range.start.index, text: inner.text() });
}
}
// Apply edits
edits.sort((a, b) => b.start - a.start);
let newCode = code;
for (const edit of edits) {
newCode = newCode.slice(0, edit.start) + edit.text + newCode.slice(edit.end);
}
// Cleanup commas (syntax fix)
// 1. Double commas ,, -> , (handle spaces/newlines between)
newCode = newCode.replaceAll(/,(\s*,)+/g, ',');
// 2. Leading comma in object { , -> {
newCode = newCode.replaceAll(/{\s*,/g, '{');
return newCode;
},
assertAfter: (code) => /output\s*:\s*['"]export['"]/.test(code) && !/withPWA\s*\(/.test(code),
});
if (withPWA) {
const inner = withPWA.getMatch('A');
if (inner) {
const range = withPWA.range();
edits.push({ end: range.end.index, start: range.start.index, text: inner.text() });
}
}
// Apply edits
edits.sort((a, b) => b.start - a.start);
let newCode = code;
for (const edit of edits) {
newCode = newCode.slice(0, edit.start) + edit.text + newCode.slice(edit.end);
}
// Cleanup commas (syntax fix)
// 1. Double commas ,, -> , (handle spaces/newlines between)
newCode = newCode.replaceAll(/,(\s*,)+/g, ',');
// 2. Leading comma in object { , -> {
newCode = newCode.replaceAll(/{\s*,/g, '{');
// 3. Trailing comma before closing brace is valid in JS/TS
await fs.writeFile(nextConfigPath, newCode);
};
if (isDirectRun(import.meta.url)) {
await runStandalone('modifyNextConfig', modifyNextConfig, [
{ lang: Lang.TypeScript, path: process.cwd() + '/next.config.ts' },
{ lang: Lang.TypeScript, path: 'src/libs/next/config/define-config.ts' },
{ lang: Lang.TypeScript, path: 'next.config.ts' },
]);
}

View file

@ -1,8 +1,7 @@
import { Lang, parse } from '@ast-grep/napi';
import fs from 'fs-extra';
import path from 'node:path';
import { isDirectRun, runStandalone } from './utils.mjs';
import { isDirectRun, removePathEnsuring, runStandalone, updateFile } from './utils.mjs';
export const modifyRoutes = async (TEMP_DIR: string) => {
// 1. Delete routes
@ -40,7 +39,10 @@ export const modifyRoutes = async (TEMP_DIR: string) => {
for (const file of filesToDelete) {
const fullPath = path.join(TEMP_DIR, file);
await fs.remove(fullPath);
await removePathEnsuring({
name: `modifyRoutes:delete:${file}`,
path: fullPath,
});
}
// 2. Modify desktopRouter.config.tsx
@ -48,39 +50,44 @@ export const modifyRoutes = async (TEMP_DIR: string) => {
TEMP_DIR,
'src/app/[variants]/router/desktopRouter.config.tsx',
);
if (fs.existsSync(routerConfigPath)) {
console.log(' Processing src/app/[variants]/router/desktopRouter.config.tsx...');
const code = await fs.readFile(routerConfigPath, 'utf8');
const ast = parse(Lang.Tsx, code);
const root = ast.root();
console.log(' Processing src/app/[variants]/router/desktopRouter.config.tsx...');
await updateFile({
filePath: routerConfigPath,
name: 'modifyRoutes:desktopRouterConfig',
transformer: (code) => {
const ast = parse(Lang.Tsx, code);
const root = ast.root();
const changelogNode = root.find({
rule: {
pattern: "{ path: 'changelog', $$$ }",
},
});
if (changelogNode) {
changelogNode.replace('');
}
const changelogImport = root.find({
rule: {
pattern: "import('../(main)/changelog')",
},
});
if (changelogImport) {
// Find the closest object (route definition) and remove it
let curr = changelogImport.parent();
while (curr) {
if (curr.kind() === 'object') {
curr.replace('');
break;
}
curr = curr.parent();
const changelogNode = root.find({
rule: {
pattern: "{ path: 'changelog', $$$ }",
},
});
if (changelogNode) {
changelogNode.replace('');
}
}
await fs.writeFile(routerConfigPath, root.text());
}
const changelogImport = root.find({
rule: {
pattern: "import('../(main)/changelog')",
},
});
if (changelogImport) {
// Find the closest object (route definition) and remove it
let curr = changelogImport.parent();
while (curr) {
if (curr.kind() === 'object') {
curr.replace('');
break;
}
curr = curr.parent();
}
}
return root.text();
},
assertAfter: (code) => !/\bchangelog\b/.test(code),
});
};
if (isDirectRun(import.meta.url)) {

View file

@ -8,6 +8,84 @@ interface ValidationTarget {
path: string;
}
interface UpdateFileOptions {
assertAfter?: (code: string) => boolean;
filePath: string;
name: string;
transformer: (code: string) => string;
}
interface WriteFileOptions {
assertAfter?: (code: string) => boolean;
filePath: string;
name: string;
text: string;
}
interface RemovePathOptions {
name: string;
path: string;
requireExists?: boolean;
}
export const invariant = (condition: unknown, message: string) => {
if (!condition) throw new Error(message);
};
export const normalizeEol = (code: string) => code.replaceAll('\r\n', '\n');
export const updateFile = async ({
assertAfter,
filePath,
name,
transformer,
}: UpdateFileOptions) => {
invariant(fs.existsSync(filePath), `[${name}] File not found: ${filePath}`);
const original = await fs.readFile(filePath, 'utf8');
const updated = transformer(original);
if (assertAfter) {
invariant(assertAfter(updated), `[${name}] Post-condition failed: ${filePath}`);
}
if (updated !== original) {
await fs.writeFile(filePath, updated);
}
};
export const writeFileEnsuring = async ({
assertAfter,
filePath,
name,
text,
}: WriteFileOptions) => {
await updateFile({
assertAfter,
filePath,
name,
transformer: () => text,
});
};
export const removePathEnsuring = async ({
name,
path: targetPath,
requireExists,
}: RemovePathOptions) => {
const exists = await fs.pathExists(targetPath);
if (requireExists) {
invariant(exists, `[${name}] Path not found: ${targetPath}`);
}
if (exists) {
await fs.remove(targetPath);
}
const stillExists = await fs.pathExists(targetPath);
invariant(!stillExists, `[${name}] Failed to remove path: ${targetPath}`);
};
export const isDirectRun = (importMetaUrl: string) => {
const entry = process.argv[1];
if (!entry) return false;