obsidian-plugin-config 1.4.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +13 -13
- package/.injection-info.json +5 -5
- package/.vscode/settings.json +11 -11
- package/.vscode/tasks.json +118 -118
- package/README.md +147 -192
- package/bin/obsidian-inject.js +8 -1
- package/eslint.config.mts +64 -64
- package/package.json +1 -1
- package/scripts/build-npm.ts +357 -346
- package/scripts/esbuild.config.ts +268 -268
- package/scripts/help.ts +152 -152
- package/scripts/inject-core.ts +725 -725
- package/scripts/inject-path.ts +10 -0
- package/scripts/inject-prompt.ts +11 -0
- package/scripts/utils.ts +151 -151
- package/tsconfig.json +30 -30
- package/versions.json +3 -1
package/scripts/inject-core.ts
CHANGED
|
@@ -1,725 +1,725 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import { isValidPath, gitExec } from './utils.js';
|
|
8
|
-
|
|
9
|
-
export interface InjectionPlan {
|
|
10
|
-
targetPath: string;
|
|
11
|
-
isObsidianPlugin: boolean;
|
|
12
|
-
hasPackageJson: boolean;
|
|
13
|
-
hasManifest: boolean;
|
|
14
|
-
hasScriptsFolder: boolean;
|
|
15
|
-
currentDependencies: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Analyze the target plugin directory
|
|
20
|
-
*/
|
|
21
|
-
export async function analyzePlugin(pluginPath: string): Promise<InjectionPlan> {
|
|
22
|
-
const packageJsonPath = path.join(pluginPath, 'package.json');
|
|
23
|
-
const manifestPath = path.join(pluginPath, 'manifest.json');
|
|
24
|
-
const scriptsPath = path.join(pluginPath, 'scripts');
|
|
25
|
-
|
|
26
|
-
const plan: InjectionPlan = {
|
|
27
|
-
targetPath: pluginPath,
|
|
28
|
-
isObsidianPlugin: false,
|
|
29
|
-
hasPackageJson: await isValidPath(packageJsonPath),
|
|
30
|
-
hasManifest: await isValidPath(manifestPath),
|
|
31
|
-
hasScriptsFolder: await isValidPath(scriptsPath),
|
|
32
|
-
currentDependencies: []
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
if (plan.hasManifest) {
|
|
36
|
-
try {
|
|
37
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
38
|
-
plan.isObsidianPlugin = !!(manifest.id && manifest.name && manifest.version);
|
|
39
|
-
} catch {
|
|
40
|
-
console.warn('Warning: Could not parse manifest.json');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (plan.hasPackageJson) {
|
|
45
|
-
try {
|
|
46
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
47
|
-
plan.currentDependencies = [
|
|
48
|
-
...Object.keys(packageJson.dependencies || {}),
|
|
49
|
-
...Object.keys(packageJson.devDependencies || {})
|
|
50
|
-
];
|
|
51
|
-
} catch {
|
|
52
|
-
console.warn('Warning: Could not parse package.json');
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return plan;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Find plugin-config root directory (handles NPM global installs)
|
|
61
|
-
*/
|
|
62
|
-
export function findPluginConfigRoot(): string {
|
|
63
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
64
|
-
const npmPackageRoot = path.resolve(scriptDir, '..');
|
|
65
|
-
const npmPackageJson = path.join(npmPackageRoot, 'package.json');
|
|
66
|
-
|
|
67
|
-
if (fs.existsSync(npmPackageJson)) {
|
|
68
|
-
try {
|
|
69
|
-
const packageContent = JSON.parse(fs.readFileSync(npmPackageJson, 'utf8'));
|
|
70
|
-
if (packageContent.name === 'obsidian-plugin-config') {
|
|
71
|
-
return npmPackageRoot;
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
// Ignore parsing errors
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return process.cwd();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Copy file content from local plugin-config directory
|
|
83
|
-
*/
|
|
84
|
-
export function copyFromLocal(filePath: string): string {
|
|
85
|
-
const configRoot = findPluginConfigRoot();
|
|
86
|
-
const sourcePath = path.join(configRoot, filePath);
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
return fs.readFileSync(sourcePath, 'utf8');
|
|
90
|
-
} catch (error) {
|
|
91
|
-
throw new Error(`Failed to copy ${filePath}: ${error}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Check if plugin-config repo is clean and commit if needed
|
|
97
|
-
*/
|
|
98
|
-
export async function ensurePluginConfigClean(): Promise<void> {
|
|
99
|
-
const configRoot = findPluginConfigRoot();
|
|
100
|
-
const gitDir = path.join(configRoot, '.git');
|
|
101
|
-
|
|
102
|
-
// Skip git check if not a git repo
|
|
103
|
-
// (e.g. NPM global install)
|
|
104
|
-
if (!fs.existsSync(gitDir)) {
|
|
105
|
-
console.log(`✅ Plugin-config repo is clean` + ` (NPM install, no git check)`);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const originalCwd = process.cwd();
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
process.chdir(configRoot);
|
|
113
|
-
const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
114
|
-
|
|
115
|
-
if (gitStatus) {
|
|
116
|
-
console.log(`\n⚠️ Plugin-config has uncommitted changes:`);
|
|
117
|
-
console.log(gitStatus);
|
|
118
|
-
console.log(`\n🔧 Auto-committing changes...`);
|
|
119
|
-
|
|
120
|
-
const msg = '🔧 Update plugin-config templates';
|
|
121
|
-
gitExec('git add -A');
|
|
122
|
-
gitExec(`git commit -m "${msg}"`);
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
126
|
-
encoding: 'utf8'
|
|
127
|
-
}).trim();
|
|
128
|
-
gitExec(`git push origin ${branch}`);
|
|
129
|
-
console.log(`✅ Changes committed and pushed`);
|
|
130
|
-
} catch {
|
|
131
|
-
try {
|
|
132
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
133
|
-
encoding: 'utf8'
|
|
134
|
-
}).trim();
|
|
135
|
-
gitExec(`git push --set-upstream origin ${branch}`);
|
|
136
|
-
console.log(`✅ New branch pushed with upstream`);
|
|
137
|
-
} catch {
|
|
138
|
-
console.log(`⚠️ Committed locally, push failed`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
console.log(`✅ Plugin-config repo is clean`);
|
|
143
|
-
}
|
|
144
|
-
} finally {
|
|
145
|
-
process.chdir(originalCwd);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Display injection plan and ask for confirmation
|
|
151
|
-
*/
|
|
152
|
-
export async function showInjectionPlan(
|
|
153
|
-
plan: InjectionPlan,
|
|
154
|
-
autoConfirm: boolean = false,
|
|
155
|
-
useSass: boolean = false
|
|
156
|
-
): Promise<boolean> {
|
|
157
|
-
const { askConfirmation, createReadlineInterface } = await import('./utils.js');
|
|
158
|
-
const rl = createReadlineInterface();
|
|
159
|
-
|
|
160
|
-
console.log(`\n🎯 Injection Plan for: ${plan.targetPath}`);
|
|
161
|
-
console.log(`📁 Target: ${path.basename(plan.targetPath)}`);
|
|
162
|
-
console.log(`📦 Package.json: ${plan.hasPackageJson ? '✅' : '❌'}`);
|
|
163
|
-
console.log(`📋 Manifest.json: ${plan.hasManifest ? '✅' : '❌'}`);
|
|
164
|
-
console.log(
|
|
165
|
-
`📂 Scripts folder: ${plan.hasScriptsFolder ? '✅ (will be updated)' : '❌ (will be created)'}`
|
|
166
|
-
);
|
|
167
|
-
console.log(`🔌 Obsidian plugin: ${plan.isObsidianPlugin ? '✅' : '❌'}`);
|
|
168
|
-
console.log(
|
|
169
|
-
`🎨 SASS support: ${useSass ? '✅ (esbuild-sass-plugin will be added)' : '❌'}`
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
if (!plan.isObsidianPlugin) {
|
|
173
|
-
console.log(`\n⚠️ Warning: This doesn't appear to be a valid Obsidian plugin`);
|
|
174
|
-
console.log(` Missing manifest.json or invalid structure`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
console.log(`\n📋 Will inject:`);
|
|
178
|
-
console.log(` ✅ Local scripts (utils.ts, esbuild.config.ts, acp.ts, etc.)`);
|
|
179
|
-
console.log(` ✅ Updated package.json scripts`);
|
|
180
|
-
console.log(` ✅ Required dependencies`);
|
|
181
|
-
console.log(` 🔍 Analyze centralized imports (manual commenting may be needed)`);
|
|
182
|
-
|
|
183
|
-
if (autoConfirm) {
|
|
184
|
-
console.log(`\n✅ Auto-confirming injection...`);
|
|
185
|
-
rl.close();
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const result = await askConfirmation(`\nProceed with injection?`, rl);
|
|
190
|
-
rl.close();
|
|
191
|
-
return result;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Clean old script files
|
|
196
|
-
*/
|
|
197
|
-
export async function cleanOldScripts(scriptsPath: string): Promise<void> {
|
|
198
|
-
const scriptNames = [
|
|
199
|
-
'utils',
|
|
200
|
-
'esbuild.config',
|
|
201
|
-
'acp',
|
|
202
|
-
'update-version',
|
|
203
|
-
'release',
|
|
204
|
-
'help'
|
|
205
|
-
];
|
|
206
|
-
const extensions = ['.ts', '.mts', '.js', '.mjs'];
|
|
207
|
-
|
|
208
|
-
for (const scriptName of scriptNames) {
|
|
209
|
-
for (const ext of extensions) {
|
|
210
|
-
const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`);
|
|
211
|
-
if (await isValidPath(scriptFile)) {
|
|
212
|
-
fs.unlinkSync(scriptFile);
|
|
213
|
-
console.log(
|
|
214
|
-
`🗑️ Removed existing ${scriptName}${ext} (will be replaced)`
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const obsoleteRootFiles = ['help-plugin.ts'];
|
|
221
|
-
for (const fileName of obsoleteRootFiles) {
|
|
222
|
-
const filePath = path.join(path.dirname(scriptsPath), fileName);
|
|
223
|
-
if (await isValidPath(filePath)) {
|
|
224
|
-
fs.unlinkSync(filePath);
|
|
225
|
-
console.log(`🗑️ Removed obsolete root file: ${fileName}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const obsoleteFiles = ['start.mjs', 'start.js'];
|
|
230
|
-
for (const fileName of obsoleteFiles) {
|
|
231
|
-
const filePath = path.join(scriptsPath, fileName);
|
|
232
|
-
if (await isValidPath(filePath)) {
|
|
233
|
-
fs.unlinkSync(filePath);
|
|
234
|
-
console.log(`🗑️ Removed obsolete file: ${fileName}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Clean old ESLint config files
|
|
241
|
-
*/
|
|
242
|
-
export async function cleanOldLintFiles(targetPath: string): Promise<void> {
|
|
243
|
-
const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore'];
|
|
244
|
-
const conflictingLintFiles = [
|
|
245
|
-
'eslint.config.ts',
|
|
246
|
-
'eslint.config.cjs',
|
|
247
|
-
'eslint.config.js',
|
|
248
|
-
'eslint.config.mjs'
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
for (const fileName of oldLintFiles) {
|
|
252
|
-
const filePath = path.join(targetPath, fileName);
|
|
253
|
-
if (await isValidPath(filePath)) {
|
|
254
|
-
fs.unlinkSync(filePath);
|
|
255
|
-
console.log(
|
|
256
|
-
`🗑️ Removed old ESLint file: ${fileName} (replaced by eslint.config.ts)`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
for (const fileName of conflictingLintFiles) {
|
|
262
|
-
const filePath = path.join(targetPath, fileName);
|
|
263
|
-
if (await isValidPath(filePath)) {
|
|
264
|
-
fs.unlinkSync(filePath);
|
|
265
|
-
console.log(
|
|
266
|
-
`🗑️ Removed existing ESLint file: ${fileName} (will be replaced by injection)`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Inject scripts and config files
|
|
274
|
-
*/
|
|
275
|
-
export async function injectScripts(targetPath: string): Promise<void> {
|
|
276
|
-
const scriptsPath = path.join(targetPath, 'scripts');
|
|
277
|
-
|
|
278
|
-
if (!(await isValidPath(scriptsPath))) {
|
|
279
|
-
fs.mkdirSync(scriptsPath, { recursive: true });
|
|
280
|
-
console.log(`📁 Created scripts directory`);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
await cleanOldScripts(scriptsPath);
|
|
284
|
-
await cleanOldLintFiles(targetPath);
|
|
285
|
-
|
|
286
|
-
const scriptFiles = [
|
|
287
|
-
'templates/scripts/utils.ts',
|
|
288
|
-
'templates/scripts/esbuild.config.ts',
|
|
289
|
-
'templates/scripts/acp.ts',
|
|
290
|
-
'templates/scripts/update-version.ts',
|
|
291
|
-
'templates/scripts/release.ts',
|
|
292
|
-
'templates/scripts/help.ts'
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
// Files that need value-preserving merge instead
|
|
296
|
-
// of full overwrite (user fills in their paths)
|
|
297
|
-
const mergeEnvFile = new Set(['.env']);
|
|
298
|
-
|
|
299
|
-
// Files with .template suffix (NPM excludes dotfiles)
|
|
300
|
-
// Map: { source: targetName }
|
|
301
|
-
const configFileMap: Record<string, string> = {
|
|
302
|
-
'templates/tsconfig.json': 'tsconfig.json',
|
|
303
|
-
'templates/gitignore.template': '.gitignore',
|
|
304
|
-
'templates/eslint.config.mts': 'eslint.config.mts',
|
|
305
|
-
'templates/.editorconfig': '.editorconfig',
|
|
306
|
-
'templates/.prettierrc': '.prettierrc',
|
|
307
|
-
'templates/npmrc.template': '.npmrc',
|
|
308
|
-
'templates/env.template': '.env'
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const configVscodeMap: Record<string, string> = {
|
|
312
|
-
'templates/.vscode/settings.json': '.vscode/settings.json',
|
|
313
|
-
'templates/.vscode/tasks.json': '.vscode/tasks.json'
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const workflowFiles = [
|
|
317
|
-
'templates/.github/workflows/release.yml',
|
|
318
|
-
'templates/.github/workflows/release-body.md'
|
|
319
|
-
];
|
|
320
|
-
|
|
321
|
-
console.log(`\n📥 Copying scripts from local files...`);
|
|
322
|
-
|
|
323
|
-
for (const scriptFile of scriptFiles) {
|
|
324
|
-
try {
|
|
325
|
-
const content = copyFromLocal(scriptFile);
|
|
326
|
-
const fileName = path.basename(scriptFile);
|
|
327
|
-
const targetFile = path.join(scriptsPath, fileName);
|
|
328
|
-
fs.writeFileSync(targetFile, content, 'utf8');
|
|
329
|
-
console.log(` ✅ ${fileName}`);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
console.error(` ❌ Failed to inject ${scriptFile}: ${error}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
console.log(`\n📥 Copying config files...`);
|
|
336
|
-
|
|
337
|
-
// Copy root config files
|
|
338
|
-
for (const [src, destName] of Object.entries(configFileMap)) {
|
|
339
|
-
try {
|
|
340
|
-
const targetFile = path.join(targetPath, destName);
|
|
341
|
-
const templateContent = copyFromLocal(src);
|
|
342
|
-
|
|
343
|
-
// For .env: merge existing values into the template
|
|
344
|
-
if (mergeEnvFile.has(destName) && fs.existsSync(targetFile)) {
|
|
345
|
-
const existing = fs.readFileSync(targetFile, 'utf8');
|
|
346
|
-
// Parse existing key=value pairs
|
|
347
|
-
const existingVals: Record<string, string> = {};
|
|
348
|
-
for (const line of existing.split(/\r?\n/)) {
|
|
349
|
-
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
350
|
-
if (m) existingVals[m[1].trim()] = m[2].trim();
|
|
351
|
-
}
|
|
352
|
-
// Re-write template, substituting existing values
|
|
353
|
-
const merged = templateContent
|
|
354
|
-
.split(/\r?\n/)
|
|
355
|
-
.map((line) => {
|
|
356
|
-
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
357
|
-
if (m) {
|
|
358
|
-
const key = m[1].trim();
|
|
359
|
-
const val = existingVals[key] ?? m[2].trim();
|
|
360
|
-
return `${key}=${val}`;
|
|
361
|
-
}
|
|
362
|
-
return line;
|
|
363
|
-
})
|
|
364
|
-
.join('\n');
|
|
365
|
-
fs.writeFileSync(targetFile, merged, 'utf8');
|
|
366
|
-
console.log(` ✅ ${destName} (values preserved)`);
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
fs.writeFileSync(targetFile, templateContent, 'utf8');
|
|
371
|
-
console.log(` ✅ ${destName}`);
|
|
372
|
-
} catch (error) {
|
|
373
|
-
console.error(` ❌ Failed to inject ${destName}: ${error}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Copy .vscode config files
|
|
378
|
-
for (const [src, destName] of Object.entries(configVscodeMap)) {
|
|
379
|
-
try {
|
|
380
|
-
const content = copyFromLocal(src);
|
|
381
|
-
const targetFile = path.join(targetPath, destName);
|
|
382
|
-
const targetDir = path.dirname(targetFile);
|
|
383
|
-
if (!(await isValidPath(targetDir))) {
|
|
384
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
385
|
-
}
|
|
386
|
-
fs.writeFileSync(targetFile, content, 'utf8');
|
|
387
|
-
console.log(` ✅ ${destName}`);
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error(` ❌ Failed to inject ${destName}: ${error}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
console.log(`\n📥 Copying GitHub workflows from local files...`);
|
|
394
|
-
|
|
395
|
-
for (const workflowFile of workflowFiles) {
|
|
396
|
-
try {
|
|
397
|
-
const content = copyFromLocal(workflowFile);
|
|
398
|
-
const relativePath = workflowFile.replace('templates/', '');
|
|
399
|
-
const targetFile = path.join(targetPath, relativePath);
|
|
400
|
-
const targetDir = path.dirname(targetFile);
|
|
401
|
-
|
|
402
|
-
if (!(await isValidPath(targetDir))) {
|
|
403
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
fs.writeFileSync(targetFile, content, 'utf8');
|
|
407
|
-
console.log(` ✅ ${relativePath}`);
|
|
408
|
-
} catch (error) {
|
|
409
|
-
console.error(` ❌ Failed to inject ${workflowFile}: ${error}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Update package.json with autonomous configuration
|
|
416
|
-
*/
|
|
417
|
-
export async function updatePackageJson(
|
|
418
|
-
targetPath: string,
|
|
419
|
-
useSass: boolean = false
|
|
420
|
-
): Promise<void> {
|
|
421
|
-
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
422
|
-
|
|
423
|
-
if (!(await isValidPath(packageJsonPath))) {
|
|
424
|
-
console.log(`❌ No package.json found, skipping package.json update`);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
430
|
-
|
|
431
|
-
const configRoot = findPluginConfigRoot();
|
|
432
|
-
const templatePkg = JSON.parse(
|
|
433
|
-
fs.readFileSync(path.join(configRoot, 'templates/package.json'), 'utf8')
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
if (useSass) {
|
|
437
|
-
const sassPkg = JSON.parse(
|
|
438
|
-
fs.readFileSync(
|
|
439
|
-
path.join(configRoot, 'templates/package-sass.json'),
|
|
440
|
-
'utf8'
|
|
441
|
-
)
|
|
442
|
-
);
|
|
443
|
-
Object.assign(templatePkg.devDependencies, sassPkg.devDependencies);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const obsoleteScripts = ['version'];
|
|
447
|
-
for (const script of obsoleteScripts) {
|
|
448
|
-
if (packageJson.scripts?.[script]) {
|
|
449
|
-
console.log(` 🧹 Removing obsolete script: "${script}"`);
|
|
450
|
-
delete packageJson.scripts[script];
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
packageJson.scripts = {
|
|
455
|
-
...packageJson.scripts,
|
|
456
|
-
...templatePkg.scripts
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
if (!packageJson.devDependencies) packageJson.devDependencies = {};
|
|
460
|
-
|
|
461
|
-
const requiredDeps: Record<string, string> = templatePkg.devDependencies;
|
|
462
|
-
|
|
463
|
-
let addedDeps = 0;
|
|
464
|
-
let updatedDeps = 0;
|
|
465
|
-
for (const [dep, version] of Object.entries(requiredDeps)) {
|
|
466
|
-
if (!packageJson.devDependencies[dep]) {
|
|
467
|
-
packageJson.devDependencies[dep] = version as string;
|
|
468
|
-
addedDeps++;
|
|
469
|
-
} else if (packageJson.devDependencies[dep] !== version) {
|
|
470
|
-
packageJson.devDependencies[dep] = version as string;
|
|
471
|
-
updatedDeps++;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (!packageJson.engines) packageJson.engines = {};
|
|
476
|
-
packageJson.engines.npm = templatePkg.engines.npm;
|
|
477
|
-
packageJson.engines.yarn = templatePkg.engines.yarn;
|
|
478
|
-
packageJson.type = templatePkg.type;
|
|
479
|
-
|
|
480
|
-
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
|
|
481
|
-
console.log(
|
|
482
|
-
` ✅ Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)`
|
|
483
|
-
);
|
|
484
|
-
} catch (error) {
|
|
485
|
-
console.error(` ❌ Failed to update package.json: ${error}`);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Analyze centralized imports in source files (without modifying)
|
|
491
|
-
*/
|
|
492
|
-
export async function analyzeCentralizedImports(targetPath: string): Promise<void> {
|
|
493
|
-
const srcPath = path.join(targetPath, 'src');
|
|
494
|
-
|
|
495
|
-
if (!(await isValidPath(srcPath))) {
|
|
496
|
-
console.log(` ℹ️ No src directory found`);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
console.log(`\n🔍 Analyzing centralized imports...`);
|
|
501
|
-
|
|
502
|
-
try {
|
|
503
|
-
const findTsFiles = (dir: string): string[] => {
|
|
504
|
-
const files: string[] = [];
|
|
505
|
-
for (const item of fs.readdirSync(dir)) {
|
|
506
|
-
const fullPath = path.join(dir, item);
|
|
507
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
508
|
-
files.push(...findTsFiles(fullPath));
|
|
509
|
-
} else if (item.endsWith('.ts') || item.endsWith('.tsx')) {
|
|
510
|
-
files.push(fullPath);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return files;
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
const tsFiles = findTsFiles(srcPath);
|
|
517
|
-
let filesWithImports = 0;
|
|
518
|
-
|
|
519
|
-
for (const filePath of tsFiles) {
|
|
520
|
-
try {
|
|
521
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
522
|
-
const importRegex =
|
|
523
|
-
/import\s+.*from\s+["']obsidian-plugin-config[^"']*["']/g;
|
|
524
|
-
if (importRegex.test(content)) {
|
|
525
|
-
filesWithImports++;
|
|
526
|
-
console.log(
|
|
527
|
-
` ⚠️ ${path.relative(targetPath, filePath)} - contains centralized imports`
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
} catch (error) {
|
|
531
|
-
console.warn(
|
|
532
|
-
` ⚠️ Could not analyze ${path.relative(targetPath, filePath)}: ${error}`
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (filesWithImports === 0) {
|
|
538
|
-
console.log(` ✅ No centralized imports found`);
|
|
539
|
-
} else {
|
|
540
|
-
console.log(
|
|
541
|
-
` ⚠️ Found ${filesWithImports} files with centralized imports`
|
|
542
|
-
);
|
|
543
|
-
console.log(
|
|
544
|
-
` 💡 You may need to manually comment these imports for the plugin to work`
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
} catch (error) {
|
|
548
|
-
console.error(` ❌ Failed to analyze imports: ${error}`);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Create required directories
|
|
554
|
-
*/
|
|
555
|
-
export async function createRequiredDirectories(targetPath: string): Promise<void> {
|
|
556
|
-
const directories = [path.join(targetPath, '.github', 'workflows')];
|
|
557
|
-
|
|
558
|
-
for (const dir of directories) {
|
|
559
|
-
if (!(await isValidPath(dir))) {
|
|
560
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
561
|
-
console.log(` 📁 Created ${path.relative(targetPath, dir)}`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Create injection info file
|
|
568
|
-
*/
|
|
569
|
-
export async function createInjectionInfo(targetPath: string): Promise<void> {
|
|
570
|
-
const configRoot = findPluginConfigRoot();
|
|
571
|
-
const configPackageJsonPath = path.join(configRoot, 'package.json');
|
|
572
|
-
|
|
573
|
-
let injectorVersion = 'unknown';
|
|
574
|
-
try {
|
|
575
|
-
const configPackageJson = JSON.parse(
|
|
576
|
-
fs.readFileSync(configPackageJsonPath, 'utf8')
|
|
577
|
-
);
|
|
578
|
-
injectorVersion = configPackageJson.version || 'unknown';
|
|
579
|
-
} catch {
|
|
580
|
-
console.warn('Warning: Could not read injector version');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const injectionInfo = {
|
|
584
|
-
injectorVersion,
|
|
585
|
-
injectionDate: new Date().toISOString(),
|
|
586
|
-
injectorName: 'obsidian-plugin-config'
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
const infoPath = path.join(targetPath, '.injection-info.json');
|
|
590
|
-
fs.writeFileSync(infoPath, JSON.stringify(injectionInfo, null, 2));
|
|
591
|
-
console.log(` ✅ Created injection info file (.injection-info.json)`);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Read injection info from target plugin
|
|
596
|
-
*/
|
|
597
|
-
export function readInjectionInfo(targetPath: string): Record<string, string> | null {
|
|
598
|
-
const infoPath = path.join(targetPath, '.injection-info.json');
|
|
599
|
-
|
|
600
|
-
if (!fs.existsSync(infoPath)) return null;
|
|
601
|
-
|
|
602
|
-
try {
|
|
603
|
-
return JSON.parse(fs.readFileSync(infoPath, 'utf8'));
|
|
604
|
-
} catch {
|
|
605
|
-
console.warn('Warning: Could not parse .injection-info.json');
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Clean NPM artifacts if package-lock.json is found
|
|
612
|
-
*/
|
|
613
|
-
export async function cleanNpmArtifactsIfNeeded(targetPath: string): Promise<void> {
|
|
614
|
-
const packageLockPath = path.join(targetPath, 'package-lock.json');
|
|
615
|
-
|
|
616
|
-
if (fs.existsSync(packageLockPath)) {
|
|
617
|
-
console.log(`\n🧹 NPM installation detected, cleaning artifacts...`);
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
fs.unlinkSync(packageLockPath);
|
|
621
|
-
console.log(` 🗑️ Removed package-lock.json`);
|
|
622
|
-
|
|
623
|
-
const nodeModulesPath = path.join(targetPath, 'node_modules');
|
|
624
|
-
if (fs.existsSync(nodeModulesPath)) {
|
|
625
|
-
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
626
|
-
console.log(
|
|
627
|
-
` 🗑️ Removed node_modules (will be reinstalled with Yarn)`
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
console.log(` ✅ NPM artifacts cleaned to avoid Yarn conflicts`);
|
|
632
|
-
} catch (error) {
|
|
633
|
-
console.error(` ❌ Failed to clean NPM artifacts: ${error}`);
|
|
634
|
-
console.log(
|
|
635
|
-
` 💡 You may need to manually remove package-lock.json and node_modules`
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Check if tsx is installed locally and install it if needed
|
|
643
|
-
*/
|
|
644
|
-
export async function ensureTsxInstalled(targetPath: string): Promise<void> {
|
|
645
|
-
console.log(`\n🔍 Checking tsx installation...`);
|
|
646
|
-
|
|
647
|
-
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
651
|
-
const devDependencies = packageJson.devDependencies || {};
|
|
652
|
-
const dependencies = packageJson.dependencies || {};
|
|
653
|
-
|
|
654
|
-
if (devDependencies.tsx || dependencies.tsx) {
|
|
655
|
-
console.log(` ✅ tsx is already installed`);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
console.log(` ⚠️ tsx not found, installing as dev dependency...`);
|
|
660
|
-
execSync('yarn add -D tsx', { cwd: targetPath, stdio: 'inherit' });
|
|
661
|
-
console.log(` ✅ tsx installed successfully`);
|
|
662
|
-
} catch (error) {
|
|
663
|
-
console.error(` ❌ Failed to install tsx: ${error}`);
|
|
664
|
-
console.log(` 💡 You may need to install tsx manually: yarn add -D tsx`);
|
|
665
|
-
throw new Error('tsx installation failed');
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Run yarn install in target directory
|
|
671
|
-
*/
|
|
672
|
-
export async function runYarnInstall(targetPath: string): Promise<void> {
|
|
673
|
-
console.log(`\n📦 Installing dependencies...`);
|
|
674
|
-
|
|
675
|
-
try {
|
|
676
|
-
execSync('yarn install', { cwd: targetPath, stdio: 'inherit' });
|
|
677
|
-
console.log(` ✅ Dependencies installed successfully`);
|
|
678
|
-
} catch (error) {
|
|
679
|
-
console.error(` ❌ Failed to install dependencies: ${error}`);
|
|
680
|
-
console.log(
|
|
681
|
-
` 💡 You may need to run 'yarn install' manually in the target directory`
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Main injection orchestration function
|
|
688
|
-
*/
|
|
689
|
-
export async function performInjection(
|
|
690
|
-
targetPath: string,
|
|
691
|
-
useSass: boolean = false
|
|
692
|
-
): Promise<void> {
|
|
693
|
-
console.log(`\n🚀 Starting injection process...`);
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
await cleanNpmArtifactsIfNeeded(targetPath);
|
|
697
|
-
await ensureTsxInstalled(targetPath);
|
|
698
|
-
await injectScripts(targetPath);
|
|
699
|
-
|
|
700
|
-
console.log(`\n📦 Updating package.json...`);
|
|
701
|
-
await updatePackageJson(targetPath, useSass);
|
|
702
|
-
|
|
703
|
-
await analyzeCentralizedImports(targetPath);
|
|
704
|
-
|
|
705
|
-
console.log(`\n📁 Creating required directories...`);
|
|
706
|
-
await createRequiredDirectories(targetPath);
|
|
707
|
-
|
|
708
|
-
await runYarnInstall(targetPath);
|
|
709
|
-
|
|
710
|
-
console.log(`\n📝 Creating injection info...`);
|
|
711
|
-
await createInjectionInfo(targetPath);
|
|
712
|
-
|
|
713
|
-
console.log(`\n✅ Injection completed successfully!`);
|
|
714
|
-
console.log(`\n📋 Next steps:`);
|
|
715
|
-
console.log(` 1. cd ${targetPath}`);
|
|
716
|
-
console.log(` 2. yarn build # Test the build`);
|
|
717
|
-
console.log(` 3. yarn start # Test development mode`);
|
|
718
|
-
console.log(
|
|
719
|
-
` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`
|
|
720
|
-
);
|
|
721
|
-
} catch (error) {
|
|
722
|
-
console.error(`\n❌ Injection failed: ${error}`);
|
|
723
|
-
throw error;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { isValidPath, gitExec } from './utils.js';
|
|
8
|
+
|
|
9
|
+
export interface InjectionPlan {
|
|
10
|
+
targetPath: string;
|
|
11
|
+
isObsidianPlugin: boolean;
|
|
12
|
+
hasPackageJson: boolean;
|
|
13
|
+
hasManifest: boolean;
|
|
14
|
+
hasScriptsFolder: boolean;
|
|
15
|
+
currentDependencies: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Analyze the target plugin directory
|
|
20
|
+
*/
|
|
21
|
+
export async function analyzePlugin(pluginPath: string): Promise<InjectionPlan> {
|
|
22
|
+
const packageJsonPath = path.join(pluginPath, 'package.json');
|
|
23
|
+
const manifestPath = path.join(pluginPath, 'manifest.json');
|
|
24
|
+
const scriptsPath = path.join(pluginPath, 'scripts');
|
|
25
|
+
|
|
26
|
+
const plan: InjectionPlan = {
|
|
27
|
+
targetPath: pluginPath,
|
|
28
|
+
isObsidianPlugin: false,
|
|
29
|
+
hasPackageJson: await isValidPath(packageJsonPath),
|
|
30
|
+
hasManifest: await isValidPath(manifestPath),
|
|
31
|
+
hasScriptsFolder: await isValidPath(scriptsPath),
|
|
32
|
+
currentDependencies: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (plan.hasManifest) {
|
|
36
|
+
try {
|
|
37
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
38
|
+
plan.isObsidianPlugin = !!(manifest.id && manifest.name && manifest.version);
|
|
39
|
+
} catch {
|
|
40
|
+
console.warn('Warning: Could not parse manifest.json');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (plan.hasPackageJson) {
|
|
45
|
+
try {
|
|
46
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
47
|
+
plan.currentDependencies = [
|
|
48
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
49
|
+
...Object.keys(packageJson.devDependencies || {})
|
|
50
|
+
];
|
|
51
|
+
} catch {
|
|
52
|
+
console.warn('Warning: Could not parse package.json');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return plan;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find plugin-config root directory (handles NPM global installs)
|
|
61
|
+
*/
|
|
62
|
+
export function findPluginConfigRoot(): string {
|
|
63
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const npmPackageRoot = path.resolve(scriptDir, '..');
|
|
65
|
+
const npmPackageJson = path.join(npmPackageRoot, 'package.json');
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(npmPackageJson)) {
|
|
68
|
+
try {
|
|
69
|
+
const packageContent = JSON.parse(fs.readFileSync(npmPackageJson, 'utf8'));
|
|
70
|
+
if (packageContent.name === 'obsidian-plugin-config') {
|
|
71
|
+
return npmPackageRoot;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Ignore parsing errors
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return process.cwd();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Copy file content from local plugin-config directory
|
|
83
|
+
*/
|
|
84
|
+
export function copyFromLocal(filePath: string): string {
|
|
85
|
+
const configRoot = findPluginConfigRoot();
|
|
86
|
+
const sourcePath = path.join(configRoot, filePath);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(sourcePath, 'utf8');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`Failed to copy ${filePath}: ${error}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if plugin-config repo is clean and commit if needed
|
|
97
|
+
*/
|
|
98
|
+
export async function ensurePluginConfigClean(): Promise<void> {
|
|
99
|
+
const configRoot = findPluginConfigRoot();
|
|
100
|
+
const gitDir = path.join(configRoot, '.git');
|
|
101
|
+
|
|
102
|
+
// Skip git check if not a git repo
|
|
103
|
+
// (e.g. NPM global install)
|
|
104
|
+
if (!fs.existsSync(gitDir)) {
|
|
105
|
+
console.log(`✅ Plugin-config repo is clean` + ` (NPM install, no git check)`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const originalCwd = process.cwd();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
process.chdir(configRoot);
|
|
113
|
+
const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
114
|
+
|
|
115
|
+
if (gitStatus) {
|
|
116
|
+
console.log(`\n⚠️ Plugin-config has uncommitted changes:`);
|
|
117
|
+
console.log(gitStatus);
|
|
118
|
+
console.log(`\n🔧 Auto-committing changes...`);
|
|
119
|
+
|
|
120
|
+
const msg = '🔧 Update plugin-config templates';
|
|
121
|
+
gitExec('git add -A');
|
|
122
|
+
gitExec(`git commit -m "${msg}"`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
126
|
+
encoding: 'utf8'
|
|
127
|
+
}).trim();
|
|
128
|
+
gitExec(`git push origin ${branch}`);
|
|
129
|
+
console.log(`✅ Changes committed and pushed`);
|
|
130
|
+
} catch {
|
|
131
|
+
try {
|
|
132
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
133
|
+
encoding: 'utf8'
|
|
134
|
+
}).trim();
|
|
135
|
+
gitExec(`git push --set-upstream origin ${branch}`);
|
|
136
|
+
console.log(`✅ New branch pushed with upstream`);
|
|
137
|
+
} catch {
|
|
138
|
+
console.log(`⚠️ Committed locally, push failed`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.log(`✅ Plugin-config repo is clean`);
|
|
143
|
+
}
|
|
144
|
+
} finally {
|
|
145
|
+
process.chdir(originalCwd);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Display injection plan and ask for confirmation
|
|
151
|
+
*/
|
|
152
|
+
export async function showInjectionPlan(
|
|
153
|
+
plan: InjectionPlan,
|
|
154
|
+
autoConfirm: boolean = false,
|
|
155
|
+
useSass: boolean = false
|
|
156
|
+
): Promise<boolean> {
|
|
157
|
+
const { askConfirmation, createReadlineInterface } = await import('./utils.js');
|
|
158
|
+
const rl = createReadlineInterface();
|
|
159
|
+
|
|
160
|
+
console.log(`\n🎯 Injection Plan for: ${plan.targetPath}`);
|
|
161
|
+
console.log(`📁 Target: ${path.basename(plan.targetPath)}`);
|
|
162
|
+
console.log(`📦 Package.json: ${plan.hasPackageJson ? '✅' : '❌'}`);
|
|
163
|
+
console.log(`📋 Manifest.json: ${plan.hasManifest ? '✅' : '❌'}`);
|
|
164
|
+
console.log(
|
|
165
|
+
`📂 Scripts folder: ${plan.hasScriptsFolder ? '✅ (will be updated)' : '❌ (will be created)'}`
|
|
166
|
+
);
|
|
167
|
+
console.log(`🔌 Obsidian plugin: ${plan.isObsidianPlugin ? '✅' : '❌'}`);
|
|
168
|
+
console.log(
|
|
169
|
+
`🎨 SASS support: ${useSass ? '✅ (esbuild-sass-plugin will be added)' : '❌'}`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (!plan.isObsidianPlugin) {
|
|
173
|
+
console.log(`\n⚠️ Warning: This doesn't appear to be a valid Obsidian plugin`);
|
|
174
|
+
console.log(` Missing manifest.json or invalid structure`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`\n📋 Will inject:`);
|
|
178
|
+
console.log(` ✅ Local scripts (utils.ts, esbuild.config.ts, acp.ts, etc.)`);
|
|
179
|
+
console.log(` ✅ Updated package.json scripts`);
|
|
180
|
+
console.log(` ✅ Required dependencies`);
|
|
181
|
+
console.log(` 🔍 Analyze centralized imports (manual commenting may be needed)`);
|
|
182
|
+
|
|
183
|
+
if (autoConfirm) {
|
|
184
|
+
console.log(`\n✅ Auto-confirming injection...`);
|
|
185
|
+
rl.close();
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await askConfirmation(`\nProceed with injection?`, rl);
|
|
190
|
+
rl.close();
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clean old script files
|
|
196
|
+
*/
|
|
197
|
+
export async function cleanOldScripts(scriptsPath: string): Promise<void> {
|
|
198
|
+
const scriptNames = [
|
|
199
|
+
'utils',
|
|
200
|
+
'esbuild.config',
|
|
201
|
+
'acp',
|
|
202
|
+
'update-version',
|
|
203
|
+
'release',
|
|
204
|
+
'help'
|
|
205
|
+
];
|
|
206
|
+
const extensions = ['.ts', '.mts', '.js', '.mjs'];
|
|
207
|
+
|
|
208
|
+
for (const scriptName of scriptNames) {
|
|
209
|
+
for (const ext of extensions) {
|
|
210
|
+
const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`);
|
|
211
|
+
if (await isValidPath(scriptFile)) {
|
|
212
|
+
fs.unlinkSync(scriptFile);
|
|
213
|
+
console.log(
|
|
214
|
+
`🗑️ Removed existing ${scriptName}${ext} (will be replaced)`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const obsoleteRootFiles = ['help-plugin.ts'];
|
|
221
|
+
for (const fileName of obsoleteRootFiles) {
|
|
222
|
+
const filePath = path.join(path.dirname(scriptsPath), fileName);
|
|
223
|
+
if (await isValidPath(filePath)) {
|
|
224
|
+
fs.unlinkSync(filePath);
|
|
225
|
+
console.log(`🗑️ Removed obsolete root file: ${fileName}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const obsoleteFiles = ['start.mjs', 'start.js'];
|
|
230
|
+
for (const fileName of obsoleteFiles) {
|
|
231
|
+
const filePath = path.join(scriptsPath, fileName);
|
|
232
|
+
if (await isValidPath(filePath)) {
|
|
233
|
+
fs.unlinkSync(filePath);
|
|
234
|
+
console.log(`🗑️ Removed obsolete file: ${fileName}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clean old ESLint config files
|
|
241
|
+
*/
|
|
242
|
+
export async function cleanOldLintFiles(targetPath: string): Promise<void> {
|
|
243
|
+
const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore'];
|
|
244
|
+
const conflictingLintFiles = [
|
|
245
|
+
'eslint.config.ts',
|
|
246
|
+
'eslint.config.cjs',
|
|
247
|
+
'eslint.config.js',
|
|
248
|
+
'eslint.config.mjs'
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
for (const fileName of oldLintFiles) {
|
|
252
|
+
const filePath = path.join(targetPath, fileName);
|
|
253
|
+
if (await isValidPath(filePath)) {
|
|
254
|
+
fs.unlinkSync(filePath);
|
|
255
|
+
console.log(
|
|
256
|
+
`🗑️ Removed old ESLint file: ${fileName} (replaced by eslint.config.ts)`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const fileName of conflictingLintFiles) {
|
|
262
|
+
const filePath = path.join(targetPath, fileName);
|
|
263
|
+
if (await isValidPath(filePath)) {
|
|
264
|
+
fs.unlinkSync(filePath);
|
|
265
|
+
console.log(
|
|
266
|
+
`🗑️ Removed existing ESLint file: ${fileName} (will be replaced by injection)`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Inject scripts and config files
|
|
274
|
+
*/
|
|
275
|
+
export async function injectScripts(targetPath: string): Promise<void> {
|
|
276
|
+
const scriptsPath = path.join(targetPath, 'scripts');
|
|
277
|
+
|
|
278
|
+
if (!(await isValidPath(scriptsPath))) {
|
|
279
|
+
fs.mkdirSync(scriptsPath, { recursive: true });
|
|
280
|
+
console.log(`📁 Created scripts directory`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await cleanOldScripts(scriptsPath);
|
|
284
|
+
await cleanOldLintFiles(targetPath);
|
|
285
|
+
|
|
286
|
+
const scriptFiles = [
|
|
287
|
+
'templates/scripts/utils.ts',
|
|
288
|
+
'templates/scripts/esbuild.config.ts',
|
|
289
|
+
'templates/scripts/acp.ts',
|
|
290
|
+
'templates/scripts/update-version.ts',
|
|
291
|
+
'templates/scripts/release.ts',
|
|
292
|
+
'templates/scripts/help.ts'
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
// Files that need value-preserving merge instead
|
|
296
|
+
// of full overwrite (user fills in their paths)
|
|
297
|
+
const mergeEnvFile = new Set(['.env']);
|
|
298
|
+
|
|
299
|
+
// Files with .template suffix (NPM excludes dotfiles)
|
|
300
|
+
// Map: { source: targetName }
|
|
301
|
+
const configFileMap: Record<string, string> = {
|
|
302
|
+
'templates/tsconfig.json': 'tsconfig.json',
|
|
303
|
+
'templates/gitignore.template': '.gitignore',
|
|
304
|
+
'templates/eslint.config.mts': 'eslint.config.mts',
|
|
305
|
+
'templates/.editorconfig': '.editorconfig',
|
|
306
|
+
'templates/.prettierrc': '.prettierrc',
|
|
307
|
+
'templates/npmrc.template': '.npmrc',
|
|
308
|
+
'templates/env.template': '.env'
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const configVscodeMap: Record<string, string> = {
|
|
312
|
+
'templates/.vscode/settings.json': '.vscode/settings.json',
|
|
313
|
+
'templates/.vscode/tasks.json': '.vscode/tasks.json'
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const workflowFiles = [
|
|
317
|
+
'templates/.github/workflows/release.yml',
|
|
318
|
+
'templates/.github/workflows/release-body.md'
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
console.log(`\n📥 Copying scripts from local files...`);
|
|
322
|
+
|
|
323
|
+
for (const scriptFile of scriptFiles) {
|
|
324
|
+
try {
|
|
325
|
+
const content = copyFromLocal(scriptFile);
|
|
326
|
+
const fileName = path.basename(scriptFile);
|
|
327
|
+
const targetFile = path.join(scriptsPath, fileName);
|
|
328
|
+
fs.writeFileSync(targetFile, content, 'utf8');
|
|
329
|
+
console.log(` ✅ ${fileName}`);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(` ❌ Failed to inject ${scriptFile}: ${error}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log(`\n📥 Copying config files...`);
|
|
336
|
+
|
|
337
|
+
// Copy root config files
|
|
338
|
+
for (const [src, destName] of Object.entries(configFileMap)) {
|
|
339
|
+
try {
|
|
340
|
+
const targetFile = path.join(targetPath, destName);
|
|
341
|
+
const templateContent = copyFromLocal(src);
|
|
342
|
+
|
|
343
|
+
// For .env: merge existing values into the template
|
|
344
|
+
if (mergeEnvFile.has(destName) && fs.existsSync(targetFile)) {
|
|
345
|
+
const existing = fs.readFileSync(targetFile, 'utf8');
|
|
346
|
+
// Parse existing key=value pairs
|
|
347
|
+
const existingVals: Record<string, string> = {};
|
|
348
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
349
|
+
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
350
|
+
if (m) existingVals[m[1].trim()] = m[2].trim();
|
|
351
|
+
}
|
|
352
|
+
// Re-write template, substituting existing values
|
|
353
|
+
const merged = templateContent
|
|
354
|
+
.split(/\r?\n/)
|
|
355
|
+
.map((line) => {
|
|
356
|
+
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
357
|
+
if (m) {
|
|
358
|
+
const key = m[1].trim();
|
|
359
|
+
const val = existingVals[key] ?? m[2].trim();
|
|
360
|
+
return `${key}=${val}`;
|
|
361
|
+
}
|
|
362
|
+
return line;
|
|
363
|
+
})
|
|
364
|
+
.join('\n');
|
|
365
|
+
fs.writeFileSync(targetFile, merged, 'utf8');
|
|
366
|
+
console.log(` ✅ ${destName} (values preserved)`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fs.writeFileSync(targetFile, templateContent, 'utf8');
|
|
371
|
+
console.log(` ✅ ${destName}`);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error(` ❌ Failed to inject ${destName}: ${error}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Copy .vscode config files
|
|
378
|
+
for (const [src, destName] of Object.entries(configVscodeMap)) {
|
|
379
|
+
try {
|
|
380
|
+
const content = copyFromLocal(src);
|
|
381
|
+
const targetFile = path.join(targetPath, destName);
|
|
382
|
+
const targetDir = path.dirname(targetFile);
|
|
383
|
+
if (!(await isValidPath(targetDir))) {
|
|
384
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
fs.writeFileSync(targetFile, content, 'utf8');
|
|
387
|
+
console.log(` ✅ ${destName}`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(` ❌ Failed to inject ${destName}: ${error}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log(`\n📥 Copying GitHub workflows from local files...`);
|
|
394
|
+
|
|
395
|
+
for (const workflowFile of workflowFiles) {
|
|
396
|
+
try {
|
|
397
|
+
const content = copyFromLocal(workflowFile);
|
|
398
|
+
const relativePath = workflowFile.replace('templates/', '');
|
|
399
|
+
const targetFile = path.join(targetPath, relativePath);
|
|
400
|
+
const targetDir = path.dirname(targetFile);
|
|
401
|
+
|
|
402
|
+
if (!(await isValidPath(targetDir))) {
|
|
403
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
fs.writeFileSync(targetFile, content, 'utf8');
|
|
407
|
+
console.log(` ✅ ${relativePath}`);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(` ❌ Failed to inject ${workflowFile}: ${error}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Update package.json with autonomous configuration
|
|
416
|
+
*/
|
|
417
|
+
export async function updatePackageJson(
|
|
418
|
+
targetPath: string,
|
|
419
|
+
useSass: boolean = false
|
|
420
|
+
): Promise<void> {
|
|
421
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
422
|
+
|
|
423
|
+
if (!(await isValidPath(packageJsonPath))) {
|
|
424
|
+
console.log(`❌ No package.json found, skipping package.json update`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
430
|
+
|
|
431
|
+
const configRoot = findPluginConfigRoot();
|
|
432
|
+
const templatePkg = JSON.parse(
|
|
433
|
+
fs.readFileSync(path.join(configRoot, 'templates/package.json'), 'utf8')
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (useSass) {
|
|
437
|
+
const sassPkg = JSON.parse(
|
|
438
|
+
fs.readFileSync(
|
|
439
|
+
path.join(configRoot, 'templates/package-sass.json'),
|
|
440
|
+
'utf8'
|
|
441
|
+
)
|
|
442
|
+
);
|
|
443
|
+
Object.assign(templatePkg.devDependencies, sassPkg.devDependencies);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const obsoleteScripts = ['version'];
|
|
447
|
+
for (const script of obsoleteScripts) {
|
|
448
|
+
if (packageJson.scripts?.[script]) {
|
|
449
|
+
console.log(` 🧹 Removing obsolete script: "${script}"`);
|
|
450
|
+
delete packageJson.scripts[script];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
packageJson.scripts = {
|
|
455
|
+
...packageJson.scripts,
|
|
456
|
+
...templatePkg.scripts
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
if (!packageJson.devDependencies) packageJson.devDependencies = {};
|
|
460
|
+
|
|
461
|
+
const requiredDeps: Record<string, string> = templatePkg.devDependencies;
|
|
462
|
+
|
|
463
|
+
let addedDeps = 0;
|
|
464
|
+
let updatedDeps = 0;
|
|
465
|
+
for (const [dep, version] of Object.entries(requiredDeps)) {
|
|
466
|
+
if (!packageJson.devDependencies[dep]) {
|
|
467
|
+
packageJson.devDependencies[dep] = version as string;
|
|
468
|
+
addedDeps++;
|
|
469
|
+
} else if (packageJson.devDependencies[dep] !== version) {
|
|
470
|
+
packageJson.devDependencies[dep] = version as string;
|
|
471
|
+
updatedDeps++;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!packageJson.engines) packageJson.engines = {};
|
|
476
|
+
packageJson.engines.npm = templatePkg.engines.npm;
|
|
477
|
+
packageJson.engines.yarn = templatePkg.engines.yarn;
|
|
478
|
+
packageJson.type = templatePkg.type;
|
|
479
|
+
|
|
480
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
|
|
481
|
+
console.log(
|
|
482
|
+
` ✅ Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)`
|
|
483
|
+
);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.error(` ❌ Failed to update package.json: ${error}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Analyze centralized imports in source files (without modifying)
|
|
491
|
+
*/
|
|
492
|
+
export async function analyzeCentralizedImports(targetPath: string): Promise<void> {
|
|
493
|
+
const srcPath = path.join(targetPath, 'src');
|
|
494
|
+
|
|
495
|
+
if (!(await isValidPath(srcPath))) {
|
|
496
|
+
console.log(` ℹ️ No src directory found`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(`\n🔍 Analyzing centralized imports...`);
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const findTsFiles = (dir: string): string[] => {
|
|
504
|
+
const files: string[] = [];
|
|
505
|
+
for (const item of fs.readdirSync(dir)) {
|
|
506
|
+
const fullPath = path.join(dir, item);
|
|
507
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
508
|
+
files.push(...findTsFiles(fullPath));
|
|
509
|
+
} else if (item.endsWith('.ts') || item.endsWith('.tsx')) {
|
|
510
|
+
files.push(fullPath);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return files;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const tsFiles = findTsFiles(srcPath);
|
|
517
|
+
let filesWithImports = 0;
|
|
518
|
+
|
|
519
|
+
for (const filePath of tsFiles) {
|
|
520
|
+
try {
|
|
521
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
522
|
+
const importRegex =
|
|
523
|
+
/import\s+.*from\s+["']obsidian-plugin-config[^"']*["']/g;
|
|
524
|
+
if (importRegex.test(content)) {
|
|
525
|
+
filesWithImports++;
|
|
526
|
+
console.log(
|
|
527
|
+
` ⚠️ ${path.relative(targetPath, filePath)} - contains centralized imports`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.warn(
|
|
532
|
+
` ⚠️ Could not analyze ${path.relative(targetPath, filePath)}: ${error}`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (filesWithImports === 0) {
|
|
538
|
+
console.log(` ✅ No centralized imports found`);
|
|
539
|
+
} else {
|
|
540
|
+
console.log(
|
|
541
|
+
` ⚠️ Found ${filesWithImports} files with centralized imports`
|
|
542
|
+
);
|
|
543
|
+
console.log(
|
|
544
|
+
` 💡 You may need to manually comment these imports for the plugin to work`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error(` ❌ Failed to analyze imports: ${error}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Create required directories
|
|
554
|
+
*/
|
|
555
|
+
export async function createRequiredDirectories(targetPath: string): Promise<void> {
|
|
556
|
+
const directories = [path.join(targetPath, '.github', 'workflows')];
|
|
557
|
+
|
|
558
|
+
for (const dir of directories) {
|
|
559
|
+
if (!(await isValidPath(dir))) {
|
|
560
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
561
|
+
console.log(` 📁 Created ${path.relative(targetPath, dir)}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Create injection info file
|
|
568
|
+
*/
|
|
569
|
+
export async function createInjectionInfo(targetPath: string): Promise<void> {
|
|
570
|
+
const configRoot = findPluginConfigRoot();
|
|
571
|
+
const configPackageJsonPath = path.join(configRoot, 'package.json');
|
|
572
|
+
|
|
573
|
+
let injectorVersion = 'unknown';
|
|
574
|
+
try {
|
|
575
|
+
const configPackageJson = JSON.parse(
|
|
576
|
+
fs.readFileSync(configPackageJsonPath, 'utf8')
|
|
577
|
+
);
|
|
578
|
+
injectorVersion = configPackageJson.version || 'unknown';
|
|
579
|
+
} catch {
|
|
580
|
+
console.warn('Warning: Could not read injector version');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const injectionInfo = {
|
|
584
|
+
injectorVersion,
|
|
585
|
+
injectionDate: new Date().toISOString(),
|
|
586
|
+
injectorName: 'obsidian-plugin-config'
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const infoPath = path.join(targetPath, '.injection-info.json');
|
|
590
|
+
fs.writeFileSync(infoPath, JSON.stringify(injectionInfo, null, 2));
|
|
591
|
+
console.log(` ✅ Created injection info file (.injection-info.json)`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Read injection info from target plugin
|
|
596
|
+
*/
|
|
597
|
+
export function readInjectionInfo(targetPath: string): Record<string, string> | null {
|
|
598
|
+
const infoPath = path.join(targetPath, '.injection-info.json');
|
|
599
|
+
|
|
600
|
+
if (!fs.existsSync(infoPath)) return null;
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
return JSON.parse(fs.readFileSync(infoPath, 'utf8'));
|
|
604
|
+
} catch {
|
|
605
|
+
console.warn('Warning: Could not parse .injection-info.json');
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Clean NPM artifacts if package-lock.json is found
|
|
612
|
+
*/
|
|
613
|
+
export async function cleanNpmArtifactsIfNeeded(targetPath: string): Promise<void> {
|
|
614
|
+
const packageLockPath = path.join(targetPath, 'package-lock.json');
|
|
615
|
+
|
|
616
|
+
if (fs.existsSync(packageLockPath)) {
|
|
617
|
+
console.log(`\n🧹 NPM installation detected, cleaning artifacts...`);
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
fs.unlinkSync(packageLockPath);
|
|
621
|
+
console.log(` 🗑️ Removed package-lock.json`);
|
|
622
|
+
|
|
623
|
+
const nodeModulesPath = path.join(targetPath, 'node_modules');
|
|
624
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
625
|
+
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
626
|
+
console.log(
|
|
627
|
+
` 🗑️ Removed node_modules (will be reinstalled with Yarn)`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
console.log(` ✅ NPM artifacts cleaned to avoid Yarn conflicts`);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.error(` ❌ Failed to clean NPM artifacts: ${error}`);
|
|
634
|
+
console.log(
|
|
635
|
+
` 💡 You may need to manually remove package-lock.json and node_modules`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Check if tsx is installed locally and install it if needed
|
|
643
|
+
*/
|
|
644
|
+
export async function ensureTsxInstalled(targetPath: string): Promise<void> {
|
|
645
|
+
console.log(`\n🔍 Checking tsx installation...`);
|
|
646
|
+
|
|
647
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
651
|
+
const devDependencies = packageJson.devDependencies || {};
|
|
652
|
+
const dependencies = packageJson.dependencies || {};
|
|
653
|
+
|
|
654
|
+
if (devDependencies.tsx || dependencies.tsx) {
|
|
655
|
+
console.log(` ✅ tsx is already installed`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log(` ⚠️ tsx not found, installing as dev dependency...`);
|
|
660
|
+
execSync('yarn add -D tsx', { cwd: targetPath, stdio: 'inherit' });
|
|
661
|
+
console.log(` ✅ tsx installed successfully`);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.error(` ❌ Failed to install tsx: ${error}`);
|
|
664
|
+
console.log(` 💡 You may need to install tsx manually: yarn add -D tsx`);
|
|
665
|
+
throw new Error('tsx installation failed');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Run yarn install in target directory
|
|
671
|
+
*/
|
|
672
|
+
export async function runYarnInstall(targetPath: string): Promise<void> {
|
|
673
|
+
console.log(`\n📦 Installing dependencies...`);
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
execSync('yarn install', { cwd: targetPath, stdio: 'inherit' });
|
|
677
|
+
console.log(` ✅ Dependencies installed successfully`);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.error(` ❌ Failed to install dependencies: ${error}`);
|
|
680
|
+
console.log(
|
|
681
|
+
` 💡 You may need to run 'yarn install' manually in the target directory`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Main injection orchestration function
|
|
688
|
+
*/
|
|
689
|
+
export async function performInjection(
|
|
690
|
+
targetPath: string,
|
|
691
|
+
useSass: boolean = false
|
|
692
|
+
): Promise<void> {
|
|
693
|
+
console.log(`\n🚀 Starting injection process...`);
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await cleanNpmArtifactsIfNeeded(targetPath);
|
|
697
|
+
await ensureTsxInstalled(targetPath);
|
|
698
|
+
await injectScripts(targetPath);
|
|
699
|
+
|
|
700
|
+
console.log(`\n📦 Updating package.json...`);
|
|
701
|
+
await updatePackageJson(targetPath, useSass);
|
|
702
|
+
|
|
703
|
+
await analyzeCentralizedImports(targetPath);
|
|
704
|
+
|
|
705
|
+
console.log(`\n📁 Creating required directories...`);
|
|
706
|
+
await createRequiredDirectories(targetPath);
|
|
707
|
+
|
|
708
|
+
await runYarnInstall(targetPath);
|
|
709
|
+
|
|
710
|
+
console.log(`\n📝 Creating injection info...`);
|
|
711
|
+
await createInjectionInfo(targetPath);
|
|
712
|
+
|
|
713
|
+
console.log(`\n✅ Injection completed successfully!`);
|
|
714
|
+
console.log(`\n📋 Next steps:`);
|
|
715
|
+
console.log(` 1. cd ${targetPath}`);
|
|
716
|
+
console.log(` 2. yarn build # Test the build`);
|
|
717
|
+
console.log(` 3. yarn start # Test development mode`);
|
|
718
|
+
console.log(
|
|
719
|
+
` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`
|
|
720
|
+
);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error(`\n❌ Injection failed: ${error}`);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
}
|