obsidian-plugin-config
Version:
Global CLI injection tool for Obsidian plugins
871 lines (775 loc) โข 27.9 kB
text/typescript
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;
}
}