obsidian-plugin-config
Version:
Système d'injection pour plugins Obsidian autonomes
517 lines (435 loc) β’ 16.1 kB
text/typescript
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import {
askQuestion,
askConfirmation,
createReadlineInterface,
isValidPath,
gitExec
} from "./utils.js";
const rl = createReadlineInterface();
interface InjectionPlan {
targetPath: string;
isObsidianPlugin: boolean;
hasPackageJson: boolean;
hasManifest: boolean;
hasScriptsFolder: boolean;
currentDependencies: string[];
}
/**
* Analyze the target plugin directory
*/
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: []
};
// Check if it's an Obsidian plugin
if (plan.hasManifest) {
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
plan.isObsidianPlugin = !!(manifest.id && manifest.name && manifest.version);
} catch {
console.warn("Warning: Could not parse manifest.json");
}
}
// Get current dependencies
if (plan.hasPackageJson) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
plan.currentDependencies = [
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.devDependencies || {})
];
} catch {
console.warn("Warning: Could not parse package.json");
}
}
return plan;
}
/**
* Display injection plan and ask for confirmation
*/
async function showInjectionPlan(plan: InjectionPlan): 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 (utils.ts, esbuild.config.ts, acp.ts, etc.)`);
console.log(` β
Updated package.json scripts`);
console.log(` β
Required dependencies`);
console.log(` π Analyze centralized imports (manual commenting may be needed)`);
return await askConfirmation(`\nProceed with injection?`, rl);
}
/**
* Check if plugin-config repo is clean and commit if needed
*/
async function ensurePluginConfigClean(): Promise<void> {
const configRoot = findPluginConfigRoot();
const originalCwd = process.cwd();
try {
// Change to plugin-config directory
process.chdir(configRoot);
// Check git status
const gitStatus = execSync("git status --porcelain", { encoding: "utf8" }).trim();
if (gitStatus) {
console.log(`\nβ οΈ Plugin-config has uncommitted changes:`);
console.log(gitStatus);
console.log(`\nπ§ Auto-committing changes with yarn bacp...`);
// Use yarn bacp with standard message
const commitMessage = "π§ Update plugin-config templates and scripts";
gitExec("git add -A");
gitExec(`git commit -m "${commitMessage}"`);
// Try to push
try {
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
gitExec(`git push origin ${currentBranch}`);
console.log(`β
Changes committed and pushed successfully`);
} catch {
// Try setting upstream if it's a new branch
try {
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
gitExec(`git push --set-upstream origin ${currentBranch}`);
console.log(`β
New branch pushed with upstream set`);
} catch {
console.log(`β οΈ Changes committed locally but push failed. Continue with injection.`);
}
}
} else {
console.log(`β
Plugin-config repo is clean`);
}
} finally {
// Always restore original directory
process.chdir(originalCwd);
}
}
/**
* Find plugin-config root directory
*/
function findPluginConfigRoot(): string {
// Auto-detect parent, fallback to current
const parentPath = path.resolve(process.cwd(), "../obsidian-plugin-config");
if (fs.existsSync(parentPath)) {
return parentPath;
}
// Fallback to current directory
return process.cwd();
}
/**
* Copy file content from local plugin-config directory
*/
function copyFromLocal(filePath: string): string {
const configRoot = findPluginConfigRoot();
const sourcePath = path.join(configRoot, filePath);
try {
return fs.readFileSync(sourcePath, 'utf8');
} catch (error) {
throw new Error(`Failed to copy ${filePath}: ${error}`);
}
}
/**
* Inject scripts from local files
*/
async function injectScripts(targetPath: string): Promise<void> {
const scriptsPath = path.join(targetPath, "scripts");
// Create scripts directory if it doesn't exist
if (!await isValidPath(scriptsPath)) {
fs.mkdirSync(scriptsPath, { recursive: true });
console.log(`π Created scripts directory`);
}
const scriptFiles = [
"scripts/utils.ts",
"scripts/esbuild.config.ts",
"scripts/acp.ts",
"scripts/update-version.ts",
"scripts/release.ts",
"scripts/help.ts"
];
const configFiles = [
"templates/tsconfig.json",
"templates/.gitignore"
];
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 content = copyFromLocal(scriptFile);
const fileName = path.basename(scriptFile);
const targetFile = path.join(scriptsPath, fileName);
fs.writeFileSync(targetFile, content, 'utf8');
console.log(` β
${fileName}`);
} catch (error) {
console.error(` β Failed to inject ${scriptFile}: ${error}`);
}
}
console.log(`\nπ₯ Copying config files from local files...`);
for (const configFile of configFiles) {
try {
const content = copyFromLocal(configFile);
const fileName = path.basename(configFile);
const targetFile = path.join(targetPath, fileName);
// Handle existing config files
if (await isValidPath(targetFile)) {
if (fileName === 'tsconfig.json') {
console.log(` π ${fileName} exists, updating with template`);
} else if (fileName === '.gitignore') {
console.log(` π ${fileName} exists, updating with template (keeps .injection-info.json)`);
}
}
fs.writeFileSync(targetFile, content, 'utf8');
console.log(` β
${fileName}`);
} catch (error) {
console.error(` β Failed to inject ${configFile}: ${error}`);
}
}
console.log(`\nπ₯ Copying GitHub workflows from local files...`);
for (const workflowFile of workflowFiles) {
try {
const content = copyFromLocal(workflowFile);
const relativePath = workflowFile.replace('templates/', '');
const targetFile = path.join(targetPath, relativePath);
// Ensure directory exists
const targetDir = path.dirname(targetFile);
if (!await isValidPath(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
fs.writeFileSync(targetFile, content, 'utf8');
console.log(` β
${relativePath}`);
} catch (error) {
console.error(` β Failed to inject ${workflowFile}: ${error}`);
}
}
}
/**
* Update package.json with autonomous configuration
*/
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(fs.readFileSync(packageJsonPath, "utf8"));
// Update scripts
packageJson.scripts = {
...packageJson.scripts,
"start": "yarn install && yarn dev",
"dev": "tsx scripts/esbuild.config.ts",
"build": "tsc -noEmit -skipLibCheck && tsx scripts/esbuild.config.ts production",
"real": "tsx scripts/esbuild.config.ts production real",
"acp": "tsx scripts/acp.ts",
"bacp": "tsx scripts/acp.ts -b",
"update-version": "tsx scripts/update-version.ts",
"v": "tsx scripts/update-version.ts",
"release": "tsx scripts/release.ts",
"r": "tsx scripts/release.ts",
"help": "tsx scripts/help.ts",
"h": "tsx scripts/help.ts"
};
// Remove centralized dependency
if (packageJson.dependencies && packageJson.dependencies["obsidian-plugin-config"]) {
delete packageJson.dependencies["obsidian-plugin-config"];
console.log(` ποΈ Removed obsidian-plugin-config dependency`);
}
// Add required dependencies
if (!packageJson.devDependencies) packageJson.devDependencies = {};
const requiredDeps = {
"@types/node": "^22.15.26",
"@types/semver": "^7.7.0",
"builtin-modules": "3.3.0",
"dedent": "^1.6.0",
"dotenv": "^16.4.5",
"esbuild": "latest",
"obsidian": "*",
"obsidian-typings": "^3.9.5",
"semver": "^7.7.2",
"tsx": "^4.19.4",
"typescript": "^5.8.2"
};
let addedDeps = 0;
let updatedDeps = 0;
for (const [dep, version] of Object.entries(requiredDeps)) {
if (!packageJson.devDependencies[dep]) {
packageJson.devDependencies[dep] = version;
addedDeps++;
} else if (packageJson.devDependencies[dep] !== version) {
packageJson.devDependencies[dep] = version;
updatedDeps++;
}
}
// Ensure yarn protection
if (!packageJson.engines) packageJson.engines = {};
packageJson.engines.npm = "please-use-yarn";
packageJson.engines.yarn = ">=1.22.0";
// Ensure ESM module type for modern configuration
packageJson.type = "module";
// Write updated package.json
fs.writeFileSync(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}`);
}
}
/**
* Run yarn install in target directory
*/
async function runYarnInstall(targetPath: string): Promise<void> {
console.log(`\nπ¦ Installing dependencies...`);
try {
execSync('yarn install', {
cwd: targetPath,
stdio: 'inherit'
});
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`);
}
}
/**
* Create injection info file
*/
async function createInjectionInfo(targetPath: string): Promise<void> {
const configRoot = findPluginConfigRoot();
const configPackageJsonPath = path.join(configRoot, "package.json");
let injectorVersion = "unknown";
try {
const configPackageJson = JSON.parse(fs.readFileSync(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");
fs.writeFileSync(infoPath, JSON.stringify(injectionInfo, null, 2));
console.log(` β
Created injection info file (.injection-info.json)`);
}
/**
* Direct injection function (without argument parsing)
*/
async function performInjectionDirect(targetPath: string): Promise<void> {
console.log(`\nπ Starting injection process...`);
try {
// Step 1: Inject scripts
await injectScripts(targetPath);
// Step 2: Update package.json
console.log(`\nπ¦ Updating package.json...`);
await updatePackageJson(targetPath);
// Step 3: Install dependencies
await runYarnInstall(targetPath);
// Step 4: Create injection info file
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)`);
} catch (error) {
console.error(`\nβ Injection failed: ${error}`);
throw error;
}
}
/**
* Ask user for target directory path
*/
async function promptForTargetPath(): Promise<string> {
console.log(`\nπ Select target plugin directory:`);
console.log(` Common paths (copy-paste ready):`);
console.log(` - ../test-sample-plugin`);
console.log(` - ../sample-plugin-modif`);
console.log(` - ../my-obsidian-plugin`);
console.log(` π‘ Tip: You can paste paths with or without quotes`);
while (true) {
const rawInput = await askQuestion(`\nEnter plugin directory path: `, rl);
if (!rawInput.trim()) {
console.log(`β Please enter a valid path`);
continue;
}
// Remove quotes if present and trim
const cleanPath = rawInput.trim().replace(/^["']|["']$/g, '');
const resolvedPath = path.resolve(cleanPath);
if (!await isValidPath(resolvedPath)) {
console.log(`β Directory not found: ${resolvedPath}`);
const retry = await askConfirmation(`Try again?`, rl);
if (!retry) {
throw new Error("User cancelled");
}
continue;
}
console.log(`π Target directory: ${resolvedPath}`);
return resolvedPath;
}
}
/**
* Main function
*/
async function main(): Promise<void> {
try {
console.log(`π― Obsidian Plugin Config - Interactive Injection Tool`);
console.log(`π₯ Inject autonomous configuration with prompts\n`);
// Check if path provided as argument
const args = process.argv.slice(2);
let targetPath: string;
if (args.length > 0) {
// Use provided argument
const rawPath = args[0];
const cleanPath = rawPath.trim().replace(/^["']|["']$/g, '');
targetPath = path.resolve(cleanPath);
if (!await isValidPath(targetPath)) {
console.error(`β Directory not found: ${targetPath}`);
process.exit(1);
}
console.log(`π Using provided path: ${targetPath}`);
} else {
// Prompt for target directory
targetPath = await promptForTargetPath();
}
// Ensure plugin-config repo is clean before injection
console.log(`\nπ Checking plugin-config repository status...`);
await ensurePluginConfigClean();
// Analyze the plugin
console.log(`\nπ Analyzing plugin...`);
const plan = await analyzePlugin(targetPath);
// Show plan and ask for confirmation
const confirmed = await showInjectionPlan(plan);
if (!confirmed) {
console.log(`β Injection cancelled by user`);
process.exit(0);
}
// Perform injection directly
await performInjectionDirect(targetPath);
} catch (error) {
console.error(`π₯ Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
} finally {
rl.close();
}
}
// Run the script
main().catch(console.error);