UNPKG

obsidian-plugin-config

Version:

Global CLI injection tool for Obsidian plugins

871 lines (775 loc) โ€ข 27.9 kB
#!/usr/bin/env tsx import path from 'path'; import { fileURLToPath } from 'url'; import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'fs/promises'; import { askConfirmation, createReadlineInterface, gitExec, gitOutput, isValidPath } from './utils.ts'; export interface InjectionPlan { targetPath: string; isObsidianPlugin: boolean; hasPackageJson: boolean; hasManifest: boolean; hasScriptsFolder: boolean; currentDependencies: string[]; } /** * Analyze the target plugin directory */ export async function analyzePlugin(pluginPath: string): Promise<InjectionPlan> { const packageJsonPath = path.join(pluginPath, 'package.json'); const manifestPath = path.join(pluginPath, 'manifest.json'); const scriptsPath = path.join(pluginPath, 'scripts'); const plan: InjectionPlan = { targetPath: pluginPath, isObsidianPlugin: false, hasPackageJson: await isValidPath(packageJsonPath), hasManifest: await isValidPath(manifestPath), hasScriptsFolder: await isValidPath(scriptsPath), currentDependencies: [] }; if (plan.hasManifest) { try { const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); plan.isObsidianPlugin = !!(manifest.id && manifest.name && manifest.version); } catch { console.warn('Warning: Could not parse manifest.json'); } } if (plan.hasPackageJson) { try { const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); plan.currentDependencies = [ ...Object.keys(packageJson.dependencies || {}), ...Object.keys(packageJson.devDependencies || {}) ]; } catch { console.warn('Warning: Could not parse package.json'); } } return plan; } /** * Find plugin-config root directory (handles NPM global installs) */ export async function findPluginConfigRoot(): Promise<string> { const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const npmPackageRoot = path.resolve(scriptDir, '..'); const npmPackageJson = path.join(npmPackageRoot, 'package.json'); if (await isValidPath(npmPackageJson)) { try { const packageContent = JSON.parse(await readFile(npmPackageJson, 'utf8')); if (packageContent.name === 'obsidian-plugin-config') { return npmPackageRoot; } } catch { // Ignore parsing errors } } return process.cwd(); } /** * Copy file content from local plugin-config directory */ export async function copyFromLocal(filePath: string): Promise<string> { const configRoot = await findPluginConfigRoot(); const sourcePath = path.join(configRoot, filePath); try { return await readFile(sourcePath, 'utf8'); } catch (error) { throw new Error(`Failed to copy ${filePath}: ${error}`); } } /** * Check if plugin-config repo is clean and commit if needed */ export async function ensurePluginConfigClean(): Promise<void> { const configRoot = await findPluginConfigRoot(); const gitDir = path.join(configRoot, '.git'); // Skip git check if not a git repo // (e.g. NPM global install) if (!(await isValidPath(gitDir))) { console.log(`โœ… Plugin-config repo is clean` + ` (NPM install, no git check)`); return; } try { const gitStatus = gitOutput('git status --porcelain', configRoot); if (gitStatus) { console.log(`\nโš ๏ธ Plugin-config has uncommitted changes:`); console.log(gitStatus); console.log(`\n๐Ÿ”ง Auto-committing changes...`); const msg = '๐Ÿ”ง Update plugin-config templates'; gitExec('git add -A', configRoot); gitExec(`git commit -m "${msg}"`, configRoot); const branch = gitOutput('git rev-parse --abbrev-ref HEAD', configRoot); try { gitExec(`git push origin ${branch}`, configRoot); console.log(`โœ… Changes committed and pushed`); } catch { try { gitExec(`git push --set-upstream origin ${branch}`, configRoot); console.log(`โœ… New branch pushed with upstream`); } catch { console.log(`โš ๏ธ Committed locally, push failed`); } } } else { console.log(`โœ… Plugin-config repo is clean`); } } catch (error) { console.error(`โš ๏ธ Failed to check or commit plugin-config: ${error}`); } } /** * Display injection plan and ask for confirmation */ export async function showInjectionPlan( plan: InjectionPlan, autoConfirm: boolean = false ): Promise<boolean> { console.log(`\n๐ŸŽฏ Injection Plan for: ${plan.targetPath}`); console.log(`๐Ÿ“ Target: ${path.basename(plan.targetPath)}`); console.log(`๐Ÿ“ฆ Package.json: ${plan.hasPackageJson ? 'โœ…' : 'โŒ'}`); console.log(`๐Ÿ“‹ Manifest.json: ${plan.hasManifest ? 'โœ…' : 'โŒ'}`); console.log( `๐Ÿ“‚ Scripts folder: ${plan.hasScriptsFolder ? 'โœ… (will be updated)' : 'โŒ (will be created)'}` ); console.log(`๐Ÿ”Œ Obsidian plugin: ${plan.isObsidianPlugin ? 'โœ…' : 'โŒ'}`); if (!plan.isObsidianPlugin) { console.log(`\nโš ๏ธ Warning: This doesn't appear to be a valid Obsidian plugin`); console.log(` Missing manifest.json or invalid structure`); } console.log(`\n๐Ÿ“‹ Will inject:`); console.log( ` โœ… Local scripts (esbuild.config.ts, utils.ts, env.ts, constants.ts, etc.)` ); console.log(` โœ… Updated package.json scripts`); console.log(` โœ… Required dependencies`); if (autoConfirm) { console.log(`\nโœ… Auto-confirming all file replacements...`); return true; } // No global confirmation needed - file-by-file confirmation will happen in diffAndPromptFiles return true; } /** * Clean old script files */ export async function cleanOldScripts( scriptsPath: string, approvedDests: Set<string> ): Promise<void> { const scriptNames = [ 'utils', 'esbuild.config', 'acp', 'update-version', 'release', 'help', 'constants', 'env', 'reload', 'typingsPlugin' ]; const extensions = ['.ts', '.mts', '.js', '.mjs']; for (const scriptName of scriptNames) { for (const ext of extensions) { const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`); if (await isValidPath(scriptFile)) { if (approvedDests.has(scriptFile)) { await unlink(scriptFile); console.log(`๐Ÿ—‘๏ธ Removed existing ${scriptName}${ext} (will be replaced)`); } } } } const obsoleteRootFiles = ['help-plugin.ts']; for (const fileName of obsoleteRootFiles) { const filePath = path.join(path.dirname(scriptsPath), fileName); if (await isValidPath(filePath)) { await unlink(filePath); console.log(`๐Ÿ—‘๏ธ Removed obsolete root file: ${fileName}`); } } const obsoleteFiles = ['start.mjs', 'start.js']; for (const fileName of obsoleteFiles) { const filePath = path.join(scriptsPath, fileName); if (await isValidPath(filePath)) { await unlink(filePath); console.log(`๐Ÿ—‘๏ธ Removed obsolete file: ${fileName}`); } } } /** * Clean old ESLint config files */ export async function cleanOldLintFiles(targetPath: string): Promise<void> { const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore']; const conflictingLintFiles = [ 'eslint.config.ts', 'eslint.config.cjs', 'eslint.config.js', 'eslint.config.mjs' ]; for (const fileName of oldLintFiles) { const filePath = path.join(targetPath, fileName); if (await isValidPath(filePath)) { await unlink(filePath); console.log( `๐Ÿ—‘๏ธ Removed old ESLint file: ${fileName} (replaced by eslint.config.mts)` ); } } for (const fileName of conflictingLintFiles) { const filePath = path.join(targetPath, fileName); if (await isValidPath(filePath)) { await unlink(filePath); console.log( `๐Ÿ—‘๏ธ Removed existing ESLint file: ${fileName} (will be replaced by injection)` ); } } } interface FileEntry { src: string; // path relative to configRoot dest: string; // absolute path in target plugin mergeEnv?: boolean; // special .env merge logic } /** * Build the full list of files to inject, with source and destination paths */ function buildFileList(targetPath: string): FileEntry[] { const scriptsPath = path.join(targetPath, 'scripts'); const entries: FileEntry[] = []; // Scripts const scriptFiles = [ 'templates/scripts/utils.ts', 'templates/scripts/esbuild.config.ts', 'templates/scripts/acp.ts', 'templates/scripts/update-version.ts', 'templates/scripts/release.ts', 'templates/scripts/help.ts', 'templates/scripts/constants.ts', 'templates/scripts/env.ts', 'templates/scripts/reload.ts', 'templates/scripts/typingsPlugin.ts' ]; for (const src of scriptFiles) { entries.push({ src, dest: path.join(scriptsPath, path.basename(src)) }); } // Root config files const configFileMap: Array<[string, string, boolean?]> = [ ['templates/tsconfig.json.template', 'tsconfig.json'], ['templates/gitignore.template', '.gitignore'], ['templates/eslint.config.mts', 'eslint.config.mts'], ['templates/.editorconfig', '.editorconfig'], ['templates/.prettierrc', '.prettierrc'], ['templates/.prettierignore', '.prettierignore'], ['templates/npmrc.template', '.npmrc'], ['templates/.gitattributes', '.gitattributes'], ['templates/env.template', '.env', true] ]; for (const [src, destName, mergeEnv] of configFileMap) { entries.push({ src, dest: path.join(targetPath, destName), mergeEnv: !!mergeEnv }); } // VSCode config files const configVscodeMap: Array<[string, string]> = [ ['templates/.vscode/settings.json', '.vscode/settings.json'], ['templates/.vscode/tasks.json', '.vscode/tasks.json'], ['templates/.vscode/extensions.json', '.vscode/extensions.json'] ]; for (const [src, destName] of configVscodeMap) { entries.push({ src, dest: path.join(targetPath, destName) }); } // GitHub workflow files const workflowFiles = [ 'templates/.github/workflows/release.yml', 'templates/.github/workflows/release-body.md' ]; for (const src of workflowFiles) { entries.push({ src, dest: path.join(targetPath, src.replace('templates/', '')) }); } return entries; } /** * Compare source templates with existing target files. * Prompt user only when content differs and file already exists. * Returns the Set of dest paths approved for injection. */ export async function diffAndPromptFiles( targetPath: string, autoConfirm: boolean ): Promise<Set<string>> { const rl = autoConfirm ? null : createReadlineInterface(); const configRoot = await findPluginConfigRoot(); const entries = buildFileList(targetPath); const approved = new Set<string>(); console.log(`\n๐Ÿ” Comparing files with existing content...`); let hasChanges = false; for (const entry of entries) { // Skip .env merge (always approved, merge logic handled separately) if (entry.mergeEnv) { approved.add(entry.dest); continue; } const srcPath = path.join(configRoot, entry.src); let srcContent: string; try { srcContent = await readFile(srcPath, 'utf8'); } catch { // Source doesn't exist, skip continue; } // Target doesn't exist yet โ†’ inject without prompting if (!(await isValidPath(entry.dest))) { approved.add(entry.dest); continue; } // Special case: eslint.config.mts - auto-approve if old .eslintrc exists if (entry.dest.endsWith('eslint.config.mts')) { const oldEslintFiles = [ '.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.cjs' ]; let hasOldEslint = false; for (const file of oldEslintFiles) { if (await isValidPath(path.join(targetPath, file))) { hasOldEslint = true; break; } } if (hasOldEslint) { console.log( ` ๐Ÿ”„ ${path.relative(targetPath, entry.dest).replace(/\\/g, '/')} (migrating from old .eslintrc format)` ); approved.add(entry.dest); continue; } } const destContent = await readFile(entry.dest, 'utf8'); // Identical โ†’ skip silently if (srcContent === destContent) { console.log( ` โœ… ${path.relative(targetPath, entry.dest).replace(/\\/g, '/')} (unchanged)` ); continue; } // Different โ†’ ask user (or auto-approve if autoConfirm) hasChanges = true; const relDest = path.relative(targetPath, entry.dest); if (autoConfirm) { console.log(` โœ… ${relDest.replace(/\\/g, '/')} (will be updated)`); approved.add(entry.dest); } else { const update = await askConfirmation( ` Update ${relDest.replace(/\\/g, '/')}? (content differs)`, rl! ); if (update) { approved.add(entry.dest); } else { console.log(` โญ๏ธ Kept existing ${relDest.replace(/\\/g, '/')}`); } } } if (!hasChanges) { console.log(` โœ… All existing files are up to date`); } if (rl) rl.close(); return approved; } /** * Inject scripts and config files */ export async function injectScripts( targetPath: string, approvedDests: Set<string> ): Promise<void> { const scriptsPath = path.join(targetPath, 'scripts'); if (!(await isValidPath(scriptsPath))) { await mkdir(scriptsPath, { recursive: true }); console.log(`๐Ÿ“ Created scripts directory`); } await cleanOldScripts(scriptsPath, approvedDests); await cleanOldLintFiles(targetPath); const scriptFiles = [ 'templates/scripts/utils.ts', 'templates/scripts/esbuild.config.ts', 'templates/scripts/acp.ts', 'templates/scripts/update-version.ts', 'templates/scripts/release.ts', 'templates/scripts/help.ts', 'templates/scripts/constants.ts', 'templates/scripts/env.ts', 'templates/scripts/reload.ts', 'templates/scripts/typingsPlugin.ts' ]; // Files that need value-preserving merge instead // of full overwrite (user fills in their paths) const mergeEnvFile = new Set(['.env']); // Files with .template suffix (NPM excludes dotfiles) // Map: { source: targetName } const configFileMap: Record<string, string> = { 'templates/tsconfig.json.template': 'tsconfig.json', 'templates/gitignore.template': '.gitignore', 'templates/eslint.config.mts': 'eslint.config.mts', 'templates/.editorconfig': '.editorconfig', 'templates/.prettierrc': '.prettierrc', 'templates/.prettierignore': '.prettierignore', 'templates/npmrc.template': '.npmrc', 'templates/.gitattributes': '.gitattributes', 'templates/env.template': '.env' }; const configVscodeMap: Record<string, string> = { 'templates/.vscode/settings.json': '.vscode/settings.json', 'templates/.vscode/tasks.json': '.vscode/tasks.json', 'templates/.vscode/extensions.json': '.vscode/extensions.json' }; const workflowFiles = [ 'templates/.github/workflows/release.yml', 'templates/.github/workflows/release-body.md' ]; console.log(`\n๐Ÿ“ฅ Copying scripts from local files...`); for (const scriptFile of scriptFiles) { try { const fileName = path.basename(scriptFile); const targetFile = path.join(scriptsPath, fileName); if (!approvedDests.has(targetFile)) { console.log(` โญ๏ธ Skipped ${fileName} (kept existing)`); continue; } const content = await copyFromLocal(scriptFile); await writeFile(targetFile, content, 'utf8'); console.log(` โœ… ${fileName}`); } catch (error) { console.error(` โŒ Failed to inject ${scriptFile}: ${error}`); } } console.log(`\n๐Ÿ“ฅ Copying config files...`); // Copy root config files for (const [src, destName] of Object.entries(configFileMap)) { // Skip if not approved by diff step const targetFile = path.join(targetPath, destName); if (!approvedDests.has(targetFile)) { continue; // already logged during diff step } try { const templateContent = await copyFromLocal(src); // For .env: merge existing values into the template if (mergeEnvFile.has(destName) && (await isValidPath(targetFile))) { const existing = await readFile(targetFile, 'utf8'); const existingVals: Record<string, string> = {}; for (const line of existing.split(/\r?\n/)) { const m = line.match(/^([^#=]+)=(.*)$/); if (m) existingVals[m[1].trim()] = m[2].trim(); } const merged = templateContent .split(/\r?\n/) .map((line) => { const m = line.match(/^([^#=]+)=(.*)$/); if (m) { const key = m[1].trim(); const val = existingVals[key] ?? m[2].trim(); return `${key}=${val}`; } return line; }) .join('\n'); await writeFile(targetFile, merged, 'utf8'); console.log(` โœ… ${destName} (values preserved)`); continue; } await writeFile(targetFile, templateContent, 'utf8'); console.log(` โœ… ${destName}`); } catch (error) { console.error(` โŒ Failed to inject ${destName}: ${error}`); } } // Copy .vscode config files for (const [src, destName] of Object.entries(configVscodeMap)) { try { const targetFile = path.join(targetPath, destName); if (!approvedDests.has(targetFile)) continue; const content = await copyFromLocal(src); const targetDir = path.dirname(targetFile); if (!(await isValidPath(targetDir))) { await mkdir(targetDir, { recursive: true }); } await writeFile(targetFile, content, 'utf8'); console.log(` โœ… ${destName}`); } catch (error) { console.error(` โŒ Failed to inject ${destName}: ${error}`); } } console.log(`\n๐Ÿ“ฅ Copying GitHub workflows from local files...`); for (const workflowFile of workflowFiles) { try { const content = await copyFromLocal(workflowFile); const relativePath = workflowFile.replace('templates/', ''); const targetFile = path.join(targetPath, relativePath); if (!approvedDests.has(targetFile)) continue; const targetDir = path.dirname(targetFile); if (!(await isValidPath(targetDir))) { await mkdir(targetDir, { recursive: true }); } await writeFile(targetFile, content, 'utf8'); console.log(` โœ… ${relativePath}`); } catch (error) { console.error(` โŒ Failed to inject ${workflowFile}: ${error}`); } } } /** * Update package.json with autonomous configuration */ export async function updatePackageJson(targetPath: string): Promise<void> { const packageJsonPath = path.join(targetPath, 'package.json'); if (!(await isValidPath(packageJsonPath))) { console.log(`โŒ No package.json found, skipping package.json update`); return; } try { const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); const configRoot = await findPluginConfigRoot(); const templatePkg = JSON.parse( await readFile(path.join(configRoot, 'templates/package.json.template'), 'utf8') ); const obsoleteScripts = ['version']; for (const script of obsoleteScripts) { if (packageJson.scripts?.[script]) { console.log(` ๐Ÿงน Removing obsolete script: "${script}"`); delete packageJson.scripts[script]; } } packageJson.scripts = { ...packageJson.scripts, ...templatePkg.scripts }; if (!packageJson.devDependencies) packageJson.devDependencies = {}; const requiredDeps: Record<string, string> = templatePkg.devDependencies; let addedDeps = 0; let updatedDeps = 0; for (const [dep, version] of Object.entries(requiredDeps)) { if (!packageJson.devDependencies[dep]) { packageJson.devDependencies[dep] = version as string; addedDeps++; } else if (packageJson.devDependencies[dep] !== version) { packageJson.devDependencies[dep] = version as string; updatedDeps++; } } if (!packageJson.engines) packageJson.engines = {}; packageJson.engines.npm = templatePkg.engines.npm; packageJson.engines.yarn = templatePkg.engines.yarn; packageJson.type = templatePkg.type; await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8'); console.log( ` โœ… Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)` ); } catch (error) { console.error(` โŒ Failed to update package.json: ${error}`); } } /** * Create required directories */ export async function createRequiredDirectories(targetPath: string): Promise<void> { const directories = [path.join(targetPath, '.github', 'workflows')]; for (const dir of directories) { if (!(await isValidPath(dir))) { await mkdir(dir, { recursive: true }); console.log(` ๐Ÿ“ Created ${path.relative(targetPath, dir).replace(/\\/g, '/')}`); } } } /** * Create injection info file */ export async function createInjectionInfo(targetPath: string): Promise<void> { const configRoot = await findPluginConfigRoot(); const configPackageJsonPath = path.join(configRoot, 'package.json'); let injectorVersion = 'unknown'; try { const configPackageJson = JSON.parse(await readFile(configPackageJsonPath, 'utf8')); injectorVersion = configPackageJson.version || 'unknown'; } catch { console.warn('Warning: Could not read injector version'); } const injectionInfo = { injectorVersion, injectionDate: new Date().toISOString(), injectorName: 'obsidian-plugin-config' }; const infoPath = path.join(targetPath, '.injection-info.json'); await writeFile(infoPath, JSON.stringify(injectionInfo, null, 2)); console.log(` โœ… Created injection info file (.injection-info.json)`); } /** * Read injection info from target plugin */ export async function readInjectionInfo( targetPath: string ): Promise<Record<string, string> | null> { const infoPath = path.join(targetPath, '.injection-info.json'); if (!(await isValidPath(infoPath))) return null; try { return JSON.parse(await readFile(infoPath, 'utf8')); } catch { console.warn('Warning: Could not parse .injection-info.json'); return null; } } /** * Clean NPM/Yarn lock files and node_modules to ensure fresh install */ export async function cleanNpmArtifactsIfNeeded(targetPath: string): Promise<void> { const packageLockPath = path.join(targetPath, 'package-lock.json'); const yarnLockPath = path.join(targetPath, 'yarn.lock'); const nodeModulesPath = path.join(targetPath, 'node_modules'); const hasPackageLock = await isValidPath(packageLockPath); const hasYarnLock = await isValidPath(yarnLockPath); if (hasPackageLock) { console.log(`\n๐Ÿงน Cleaning NPM artifacts (migrating to Yarn)...`); try { if (await isValidPath(nodeModulesPath)) { console.log(` โณ Removing node_modules (this may take a moment)...`); try { await rm(nodeModulesPath, { recursive: true, force: true }); } catch { // ignore } if (await isValidPath(nodeModulesPath)) { const timestamp = Date.now(); const oldPath = `${nodeModulesPath}.old.${timestamp}`; try { await rename(nodeModulesPath, oldPath); console.log(` ๐Ÿ”„ Renamed locked node_modules to ${path.basename(oldPath)}`); console.log(` ๐Ÿ’ก Delete it manually later: ${oldPath}`); } catch { console.log( ` โš ๏ธ Could not remove/rename node_modules (locked by processes)` ); console.log(` ๐Ÿ’ก Close Obsidian/VSCode and run: obsidian-inject again`); throw new Error('node_modules locked - close processes and retry'); } } else { console.log(` ๐Ÿ—‘๏ธ Removed node_modules (will be reinstalled with Yarn)`); } } if (hasPackageLock) { await unlink(packageLockPath); console.log(` ๐Ÿ—‘๏ธ Removed package-lock.json`); } if (hasYarnLock) { await unlink(yarnLockPath); console.log(` ๐Ÿ—‘๏ธ Removed yarn.lock`); } console.log(` โœ… Lock files and artifacts cleaned for fresh install`); } catch (error) { if (error instanceof Error && error.message.includes('locked')) throw error; console.error(` โŒ Failed to clean artifacts: ${error}`); console.log( ` ๐Ÿ’ก You may need to manually remove package-lock.json, yarn.lock and node_modules` ); } } } /** * Check if tsx is installed locally and install it if needed */ export async function ensureTsxInstalled(targetPath: string): Promise<void> { console.log(`\n๐Ÿ” Checking tsx installation...`); const packageJsonPath = path.join(targetPath, 'package.json'); try { const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); const devDependencies = packageJson.devDependencies || {}; const dependencies = packageJson.dependencies || {}; if (devDependencies.tsx || dependencies.tsx) { console.log(` โœ… tsx is already installed`); return; } console.log(` โš ๏ธ tsx not found, installing as dev dependency...`); gitExec('yarn add -D tsx', targetPath); console.log(` โœ… tsx installed successfully`); } catch (error) { console.error(` โŒ Failed to install tsx: ${error}`); console.log(` ๐Ÿ’ก You may need to install tsx manually: yarn add -D tsx`); throw new Error('tsx installation failed'); } } /** * Run yarn install in target directory */ export async function runYarnInstall(targetPath: string): Promise<void> { console.log(`\n๐Ÿ“ฆ Installing dependencies...`); try { gitExec('yarn install', targetPath); console.log(` โœ… Dependencies installed successfully`); } catch (error) { console.error(` โŒ Failed to install dependencies: ${error}`); console.log( ` ๐Ÿ’ก You may need to run 'yarn install' manually in the target directory` ); } } /** * Main injection orchestration function */ export async function performInjection( targetPath: string, autoConfirm: boolean = false ): Promise<void> { console.log(`\n๐Ÿš€ Starting injection process...`); try { const approvedDests = await diffAndPromptFiles(targetPath, autoConfirm); await cleanNpmArtifactsIfNeeded(targetPath); await ensureTsxInstalled(targetPath); await injectScripts(targetPath, approvedDests); console.log(`\n๐Ÿ“ฆ Updating package.json...`); await updatePackageJson(targetPath); console.log(`\n๐Ÿ“ Creating required directories...`); await createRequiredDirectories(targetPath); await runYarnInstall(targetPath); console.log(`\n๐Ÿ“ Creating injection info...`); await createInjectionInfo(targetPath); console.log(`\nโœ… Injection completed successfully!`); console.log(`\n๐Ÿ“‹ Next steps:`); console.log(` 1. cd ${targetPath}`); console.log(` 2. yarn build # Test the build`); console.log(` 3. yarn start # Test development mode`); console.log(` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`); const allEntries = await readdir(targetPath); const oldDirs = allEntries .filter((name) => name.startsWith('node_modules.old.')) .map((name) => path.basename(name)); if (oldDirs.length > 0) { console.log(`\n๐Ÿงน Cleanup reminder:`); for (const oldDir of oldDirs) { console.log(` ๐Ÿ—‘๏ธ Delete manually: ${oldDir}`); } console.log(` ๐Ÿ’ก Close all processes first, then delete these folders`); } } catch (error) { console.error(`\nโŒ Injection failed: ${error}`); throw error; } }