obsidian-plugin-config 1.6.18 โ†’ 1.7.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.
@@ -7,346 +7,354 @@ import { fileURLToPath } from 'url';
7
7
  import { isValidPath, gitExec } from './utils.js';
8
8
 
9
9
  export interface InjectionPlan {
10
- targetPath: string;
11
- isObsidianPlugin: boolean;
12
- hasPackageJson: boolean;
13
- hasManifest: boolean;
14
- hasScriptsFolder: boolean;
15
- currentDependencies: string[];
10
+ targetPath: string;
11
+ isObsidianPlugin: boolean;
12
+ hasPackageJson: boolean;
13
+ hasManifest: boolean;
14
+ hasScriptsFolder: boolean;
15
+ currentDependencies: string[];
16
16
  }
17
17
 
18
18
  /**
19
19
  * Analyze the target plugin directory
20
20
  */
21
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;
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
57
  }
58
58
 
59
59
  /**
60
60
  * Find plugin-config root directory (handles NPM global installs)
61
61
  */
62
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();
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
79
  }
80
80
 
81
81
  /**
82
82
  * Copy file content from local plugin-config directory
83
83
  */
84
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
- }
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
93
  }
94
94
 
95
95
  /**
96
96
  * Check if plugin-config repo is clean and commit if needed
97
97
  */
98
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
- }
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
147
  }
148
148
 
149
149
  /**
150
150
  * Display injection plan and ask for confirmation
151
151
  */
152
152
  export async function showInjectionPlan(
153
- plan: InjectionPlan,
154
- autoConfirm: boolean = false,
155
- useSass: boolean = false
153
+ plan: InjectionPlan,
154
+ autoConfirm: boolean = false,
155
+ useSass: boolean = false
156
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 all file replacements...`);
185
- rl.close();
186
- return true;
187
- }
188
-
189
- // No global confirmation needed - file-by-file confirmation will happen in diffAndPromptFiles
190
- rl.close();
191
- return true;
157
+ const { 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 (esbuild.config.ts, utils.ts, env.ts, constants.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 all file replacements...`);
185
+ rl.close();
186
+ return true;
187
+ }
188
+
189
+ // No global confirmation needed - file-by-file confirmation will happen in diffAndPromptFiles
190
+ rl.close();
191
+ return true;
192
192
  }
193
193
 
194
194
  /**
195
195
  * Clean old script files
196
196
  */
197
197
  export async function cleanOldScripts(
198
- scriptsPath: string,
199
- approvedDests: Set<string>
198
+ scriptsPath: string,
199
+ approvedDests: Set<string>
200
200
  ): Promise<void> {
201
- const scriptNames = [
202
- 'utils',
203
- 'esbuild.config',
204
- 'acp',
205
- 'update-version',
206
- 'release',
207
- 'help'
208
- ];
209
- const extensions = ['.ts', '.mts', '.js', '.mjs'];
210
-
211
- for (const scriptName of scriptNames) {
212
- for (const ext of extensions) {
213
- const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`);
214
- if (await isValidPath(scriptFile)) {
215
- if (approvedDests.has(scriptFile)) {
216
- fs.unlinkSync(scriptFile);
217
- console.log(`๐Ÿ—‘๏ธ Removed existing ${scriptName}${ext} (will be replaced)`);
218
- }
219
- }
220
- }
221
- }
222
-
223
- const obsoleteRootFiles = ['help-plugin.ts'];
224
- for (const fileName of obsoleteRootFiles) {
225
- const filePath = path.join(path.dirname(scriptsPath), fileName);
226
- if (await isValidPath(filePath)) {
227
- fs.unlinkSync(filePath);
228
- console.log(`๐Ÿ—‘๏ธ Removed obsolete root file: ${fileName}`);
229
- }
230
- }
231
-
232
- const obsoleteFiles = ['start.mjs', 'start.js'];
233
- for (const fileName of obsoleteFiles) {
234
- const filePath = path.join(scriptsPath, fileName);
235
- if (await isValidPath(filePath)) {
236
- fs.unlinkSync(filePath);
237
- console.log(`๐Ÿ—‘๏ธ Removed obsolete file: ${fileName}`);
238
- }
239
- }
201
+ const scriptNames = [
202
+ 'utils',
203
+ 'esbuild.config',
204
+ 'acp',
205
+ 'update-version',
206
+ 'release',
207
+ 'help',
208
+ 'constants',
209
+ 'env',
210
+ 'reload',
211
+ 'typingsPlugin'
212
+ ];
213
+ const extensions = ['.ts', '.mts', '.js', '.mjs'];
214
+
215
+ for (const scriptName of scriptNames) {
216
+ for (const ext of extensions) {
217
+ const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`);
218
+ if (await isValidPath(scriptFile)) {
219
+ if (approvedDests.has(scriptFile)) {
220
+ fs.unlinkSync(scriptFile);
221
+ console.log(`๐Ÿ—‘๏ธ Removed existing ${scriptName}${ext} (will be replaced)`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ const obsoleteRootFiles = ['help-plugin.ts'];
228
+ for (const fileName of obsoleteRootFiles) {
229
+ const filePath = path.join(path.dirname(scriptsPath), fileName);
230
+ if (await isValidPath(filePath)) {
231
+ fs.unlinkSync(filePath);
232
+ console.log(`๐Ÿ—‘๏ธ Removed obsolete root file: ${fileName}`);
233
+ }
234
+ }
235
+
236
+ const obsoleteFiles = ['start.mjs', 'start.js'];
237
+ for (const fileName of obsoleteFiles) {
238
+ const filePath = path.join(scriptsPath, fileName);
239
+ if (await isValidPath(filePath)) {
240
+ fs.unlinkSync(filePath);
241
+ console.log(`๐Ÿ—‘๏ธ Removed obsolete file: ${fileName}`);
242
+ }
243
+ }
240
244
  }
241
245
 
242
246
  /**
243
247
  * Clean old ESLint config files
244
248
  */
245
249
  export async function cleanOldLintFiles(targetPath: string): Promise<void> {
246
- const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore'];
247
- const conflictingLintFiles = [
248
- 'eslint.config.ts',
249
- 'eslint.config.cjs',
250
- 'eslint.config.js',
251
- 'eslint.config.mjs'
252
- ];
253
-
254
- for (const fileName of oldLintFiles) {
255
- const filePath = path.join(targetPath, fileName);
256
- if (await isValidPath(filePath)) {
257
- fs.unlinkSync(filePath);
258
- console.log(
259
- `๐Ÿ—‘๏ธ Removed old ESLint file: ${fileName} (replaced by 1
250
+ const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore'];
251
+ const conflictingLintFiles = [
252
+ 'eslint.config.ts',
253
+ 'eslint.config.cjs',
254
+ 'eslint.config.js',
255
+ 'eslint.config.mjs'
256
+ ];
257
+
258
+ for (const fileName of oldLintFiles) {
259
+ const filePath = path.join(targetPath, fileName);
260
+ if (await isValidPath(filePath)) {
261
+ fs.unlinkSync(filePath);
262
+ console.log(
263
+ `๐Ÿ—‘๏ธ Removed old ESLint file: ${fileName} (replaced by 1
260
264
  fig.ts)`
261
- );
262
- }
263
- }
264
-
265
- for (const fileName of conflictingLintFiles) {
266
- const filePath = path.join(targetPath, fileName);
267
- if (await isValidPath(filePath)) {
268
- fs.unlinkSync(filePath);
269
- console.log(
270
- `๐Ÿ—‘๏ธ Removed existing ESLint file: ${fileName} (will be replaced by injection)`
271
- );
272
- }
273
- }
265
+ );
266
+ }
267
+ }
268
+
269
+ for (const fileName of conflictingLintFiles) {
270
+ const filePath = path.join(targetPath, fileName);
271
+ if (await isValidPath(filePath)) {
272
+ fs.unlinkSync(filePath);
273
+ console.log(
274
+ `๐Ÿ—‘๏ธ Removed existing ESLint file: ${fileName} (will be replaced by injection)`
275
+ );
276
+ }
277
+ }
274
278
  }
275
279
 
276
280
  interface FileEntry {
277
- src: string; // path relative to configRoot
278
- dest: string; // absolute path in target plugin
279
- mergeEnv?: boolean; // special .env merge logic
281
+ src: string; // path relative to configRoot
282
+ dest: string; // absolute path in target plugin
283
+ mergeEnv?: boolean; // special .env merge logic
280
284
  }
281
285
 
282
286
  /**
283
287
  * Build the full list of files to inject, with source and destination paths
284
288
  */
285
289
  function buildFileList(targetPath: string): FileEntry[] {
286
- const scriptsPath = path.join(targetPath, 'scripts');
287
- const entries: FileEntry[] = [];
288
-
289
- // Scripts
290
- const scriptFiles = [
291
- 'templates/scripts/utils.ts',
292
- 'templates/scripts/esbuild.config.ts',
293
- 'templates/scripts/acp.ts',
294
- 'templates/scripts/update-version.ts',
295
- 'templates/scripts/release.ts',
296
- 'templates/scripts/help.ts'
297
- ];
298
- for (const src of scriptFiles) {
299
- entries.push({
300
- src,
301
- dest: path.join(scriptsPath, path.basename(src))
302
- });
303
- }
304
-
305
- // Root config files
306
- const configFileMap: Array<[string, string, boolean?]> = [
307
- ['templates/tsconfig.json', 'tsconfig.json'],
308
- ['templates/gitignore.template', '.gitignore'],
309
- ['templates/eslint.config.mts', 'eslint.config.mts'],
310
- ['templates/.editorconfig', '.editorconfig'],
311
- ['templates/.prettierrc', '.prettierrc'],
312
- ['templates/.prettierignore', '.prettierignore'],
313
- ['templates/npmrc.template', '.npmrc'],
314
- ['templates/env.template', '.env', true]
315
- ];
316
- for (const [src, destName, mergeEnv] of configFileMap) {
317
- entries.push({
318
- src,
319
- dest: path.join(targetPath, destName),
320
- mergeEnv: !!mergeEnv
321
- });
322
- }
323
-
324
- // VSCode config files
325
- const configVscodeMap: Array<[string, string]> = [
326
- ['templates/.vscode/settings.json', '.vscode/settings.json'],
327
- ['templates/.vscode/tasks.json', '.vscode/tasks.json'],
328
- ['templates/.vscode/extensions.json', '.vscode/extensions.json']
329
- ];
330
- for (const [src, destName] of configVscodeMap) {
331
- entries.push({
332
- src,
333
- dest: path.join(targetPath, destName)
334
- });
335
- }
336
-
337
- // GitHub workflow files
338
- const workflowFiles = [
339
- 'templates/.github/workflows/release.yml',
340
- 'templates/.github/workflows/release-body.md'
341
- ];
342
- for (const src of workflowFiles) {
343
- entries.push({
344
- src,
345
- dest: path.join(targetPath, src.replace('templates/', ''))
346
- });
347
- }
348
-
349
- return entries;
290
+ const scriptsPath = path.join(targetPath, 'scripts');
291
+ const entries: FileEntry[] = [];
292
+
293
+ // Scripts
294
+ const scriptFiles = [
295
+ 'templates/scripts/utils.ts',
296
+ 'templates/scripts/esbuild.config.ts',
297
+ 'templates/scripts/acp.ts',
298
+ 'templates/scripts/update-version.ts',
299
+ 'templates/scripts/release.ts',
300
+ 'templates/scripts/help.ts',
301
+ 'templates/scripts/constants.ts',
302
+ 'templates/scripts/env.ts',
303
+ 'templates/scripts/reload.ts',
304
+ 'templates/scripts/typingsPlugin.ts'
305
+ ];
306
+ for (const src of scriptFiles) {
307
+ entries.push({
308
+ src,
309
+ dest: path.join(scriptsPath, path.basename(src))
310
+ });
311
+ }
312
+
313
+ // Root config files
314
+ const configFileMap: Array<[string, string, boolean?]> = [
315
+ ['templates/tsconfig.json', 'tsconfig.json'],
316
+ ['templates/gitignore.template', '.gitignore'],
317
+ ['templates/eslint.config.mts', 'eslint.config.mts'],
318
+ ['templates/.editorconfig', '.editorconfig'],
319
+ ['templates/.prettierrc', '.prettierrc'],
320
+ ['templates/.prettierignore', '.prettierignore'],
321
+ ['templates/npmrc.template', '.npmrc'],
322
+ ['templates/env.template', '.env', true]
323
+ ];
324
+ for (const [src, destName, mergeEnv] of configFileMap) {
325
+ entries.push({
326
+ src,
327
+ dest: path.join(targetPath, destName),
328
+ mergeEnv: !!mergeEnv
329
+ });
330
+ }
331
+
332
+ // VSCode config files
333
+ const configVscodeMap: Array<[string, string]> = [
334
+ ['templates/.vscode/settings.json', '.vscode/settings.json'],
335
+ ['templates/.vscode/tasks.json', '.vscode/tasks.json'],
336
+ ['templates/.vscode/extensions.json', '.vscode/extensions.json']
337
+ ];
338
+ for (const [src, destName] of configVscodeMap) {
339
+ entries.push({
340
+ src,
341
+ dest: path.join(targetPath, destName)
342
+ });
343
+ }
344
+
345
+ // GitHub workflow files
346
+ const workflowFiles = [
347
+ 'templates/.github/workflows/release.yml',
348
+ 'templates/.github/workflows/release-body.md'
349
+ ];
350
+ for (const src of workflowFiles) {
351
+ entries.push({
352
+ src,
353
+ dest: path.join(targetPath, src.replace('templates/', ''))
354
+ });
355
+ }
356
+
357
+ return entries;
350
358
  }
351
359
 
352
360
  /**
@@ -355,607 +363,611 @@ function buildFileList(targetPath: string): FileEntry[] {
355
363
  * Returns the Set of dest paths approved for injection.
356
364
  */
357
365
  export async function diffAndPromptFiles(
358
- targetPath: string,
359
- autoConfirm: boolean
366
+ targetPath: string,
367
+ autoConfirm: boolean
360
368
  ): Promise<Set<string>> {
361
- const { askConfirmation, createReadlineInterface } = await import('./utils.js');
362
- const rl = autoConfirm ? null : createReadlineInterface();
363
- const configRoot = findPluginConfigRoot();
364
- const entries = buildFileList(targetPath);
365
- const approved = new Set<string>();
366
-
367
- console.log(`\n๐Ÿ” Comparing files with existing content...`);
368
-
369
- let hasChanges = false;
370
-
371
- for (const entry of entries) {
372
- // Skip .env merge (always approved, merge logic handled separately)
373
- if (entry.mergeEnv) {
374
- approved.add(entry.dest);
375
- continue;
376
- }
377
-
378
- const srcPath = path.join(configRoot, entry.src);
379
- let srcContent: string;
380
- try {
381
- srcContent = fs.readFileSync(srcPath, 'utf8');
382
- } catch {
383
- // Source doesn't exist, skip
384
- continue;
385
- }
386
-
387
- // Target doesn't exist yet โ†’ inject without prompting
388
- if (!fs.existsSync(entry.dest)) {
389
- approved.add(entry.dest);
390
- continue;
391
- }
392
-
393
- // Special case: eslint.config.mts - auto-approve if old .eslintrc exists
394
- if (entry.dest.endsWith('eslint.config.mts')) {
395
- const oldEslintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.cjs'];
396
- const hasOldEslint = oldEslintFiles.some(file =>
397
- fs.existsSync(path.join(targetPath, file))
398
- );
399
- if (hasOldEslint) {
400
- console.log(` ๐Ÿ”„ ${path.relative(targetPath, entry.dest)} (migrating from old .eslintrc format)`);
401
- approved.add(entry.dest);
402
- continue;
403
- }
404
- }
405
-
406
- const destContent = fs.readFileSync(entry.dest, 'utf8');
407
-
408
- // Identical โ†’ skip silently
409
- if (srcContent === destContent) {
410
- console.log(` โœ… ${path.relative(targetPath, entry.dest)} (unchanged)`);
411
- continue;
412
- }
413
-
414
- // Different โ†’ ask user (or auto-approve if autoConfirm)
415
- hasChanges = true;
416
- const relDest = path.relative(targetPath, entry.dest);
417
-
418
- if (autoConfirm) {
419
- console.log(` โœ… ${relDest} (will be updated)`);
420
- approved.add(entry.dest);
421
- } else {
422
- const update = await askConfirmation(
423
- ` Update ${relDest}? (content differs)`,
424
- rl!
425
- );
426
- if (update) {
427
- approved.add(entry.dest);
428
- } else {
429
- console.log(` โญ๏ธ Kept existing ${relDest}`);
430
- }
431
- }
432
- }
433
-
434
- if (!hasChanges) {
435
- console.log(` โœ… All existing files are up to date`);
436
- }
437
-
438
- if (rl) rl.close();
439
- return approved;
369
+ const { askConfirmation, createReadlineInterface } = await import('./utils.js');
370
+ const rl = autoConfirm ? null : createReadlineInterface();
371
+ const configRoot = findPluginConfigRoot();
372
+ const entries = buildFileList(targetPath);
373
+ const approved = new Set<string>();
374
+
375
+ console.log(`\n๐Ÿ” Comparing files with existing content...`);
376
+
377
+ let hasChanges = false;
378
+
379
+ for (const entry of entries) {
380
+ // Skip .env merge (always approved, merge logic handled separately)
381
+ if (entry.mergeEnv) {
382
+ approved.add(entry.dest);
383
+ continue;
384
+ }
385
+
386
+ const srcPath = path.join(configRoot, entry.src);
387
+ let srcContent: string;
388
+ try {
389
+ srcContent = fs.readFileSync(srcPath, 'utf8');
390
+ } catch {
391
+ // Source doesn't exist, skip
392
+ continue;
393
+ }
394
+
395
+ // Target doesn't exist yet โ†’ inject without prompting
396
+ if (!fs.existsSync(entry.dest)) {
397
+ approved.add(entry.dest);
398
+ continue;
399
+ }
400
+
401
+ // Special case: eslint.config.mts - auto-approve if old .eslintrc exists
402
+ if (entry.dest.endsWith('eslint.config.mts')) {
403
+ const oldEslintFiles = [
404
+ '.eslintrc',
405
+ '.eslintrc.js',
406
+ '.eslintrc.json',
407
+ '.eslintrc.cjs'
408
+ ];
409
+ const hasOldEslint = oldEslintFiles.some((file) =>
410
+ fs.existsSync(path.join(targetPath, file))
411
+ );
412
+ if (hasOldEslint) {
413
+ console.log(
414
+ ` ๐Ÿ”„ ${path.relative(targetPath, entry.dest)} (migrating from old .eslintrc format)`
415
+ );
416
+ approved.add(entry.dest);
417
+ continue;
418
+ }
419
+ }
420
+
421
+ const destContent = fs.readFileSync(entry.dest, 'utf8');
422
+
423
+ // Identical โ†’ skip silently
424
+ if (srcContent === destContent) {
425
+ console.log(` โœ… ${path.relative(targetPath, entry.dest)} (unchanged)`);
426
+ continue;
427
+ }
428
+
429
+ // Different โ†’ ask user (or auto-approve if autoConfirm)
430
+ hasChanges = true;
431
+ const relDest = path.relative(targetPath, entry.dest);
432
+
433
+ if (autoConfirm) {
434
+ console.log(` โœ… ${relDest} (will be updated)`);
435
+ approved.add(entry.dest);
436
+ } else {
437
+ const update = await askConfirmation(
438
+ ` Update ${relDest}? (content differs)`,
439
+ rl!
440
+ );
441
+ if (update) {
442
+ approved.add(entry.dest);
443
+ } else {
444
+ console.log(` โญ๏ธ Kept existing ${relDest}`);
445
+ }
446
+ }
447
+ }
448
+
449
+ if (!hasChanges) {
450
+ console.log(` โœ… All existing files are up to date`);
451
+ }
452
+
453
+ if (rl) rl.close();
454
+ return approved;
440
455
  }
441
456
 
442
457
  /**
443
458
  * Inject scripts and config files
444
459
  */
445
460
  export async function injectScripts(
446
- targetPath: string,
447
- approvedDests: Set<string>
461
+ targetPath: string,
462
+ approvedDests: Set<string>
448
463
  ): Promise<void> {
449
- const scriptsPath = path.join(targetPath, 'scripts');
450
-
451
- if (!(await isValidPath(scriptsPath))) {
452
- fs.mkdirSync(scriptsPath, { recursive: true });
453
- console.log(`๐Ÿ“ Created scripts directory`);
454
- }
455
-
456
- await cleanOldScripts(scriptsPath, approvedDests);
457
- await cleanOldLintFiles(targetPath);
458
-
459
- const scriptFiles = [
460
- 'templates/scripts/utils.ts',
461
- 'templates/scripts/esbuild.config.ts',
462
- 'templates/scripts/acp.ts',
463
- 'templates/scripts/update-version.ts',
464
- 'templates/scripts/release.ts',
465
- 'templates/scripts/help.ts'
466
- ];
467
-
468
- // Files that need value-preserving merge instead
469
- // of full overwrite (user fills in their paths)
470
- const mergeEnvFile = new Set(['.env']);
471
-
472
- // Files with .template suffix (NPM excludes dotfiles)
473
- // Map: { source: targetName }
474
- const configFileMap: Record<string, string> = {
475
- 'templates/tsconfig.json': 'tsconfig.json',
476
- 'templates/gitignore.template': '.gitignore',
477
- 'templates/eslint.config.mts': 'eslint.config.mts',
478
- 'templates/.editorconfig': '.editorconfig',
479
- 'templates/.prettierrc': '.prettierrc',
480
- 'templates/.prettierignore': '.prettierignore',
481
- 'templates/npmrc.template': '.npmrc',
482
- 'templates/env.template': '.env'
483
- };
484
-
485
- const configVscodeMap: Record<string, string> = {
486
- 'templates/.vscode/settings.json': '.vscode/settings.json',
487
- 'templates/.vscode/tasks.json': '.vscode/tasks.json',
488
- 'templates/.vscode/extensions.json': '.vscode/extensions.json'
489
- };
490
-
491
- const workflowFiles = [
492
- 'templates/.github/workflows/release.yml',
493
- 'templates/.github/workflows/release-body.md'
494
- ];
495
-
496
- console.log(`\n๐Ÿ“ฅ Copying scripts from local files...`);
497
-
498
- for (const scriptFile of scriptFiles) {
499
- try {
500
- const fileName = path.basename(scriptFile);
501
- const targetFile = path.join(scriptsPath, fileName);
502
- if (!approvedDests.has(targetFile)) {
503
- console.log(` โญ๏ธ Skipped ${fileName} (kept existing)`);
504
- continue;
505
- }
506
- const content = copyFromLocal(scriptFile);
507
- fs.writeFileSync(targetFile, content, 'utf8');
508
- console.log(` โœ… ${fileName}`);
509
- } catch (error) {
510
- console.error(` โŒ Failed to inject ${scriptFile}: ${error}`);
511
- }
512
- }
513
-
514
- console.log(`\n๐Ÿ“ฅ Copying config files...`);
515
-
516
- // Copy root config files
517
- for (const [src, destName] of Object.entries(configFileMap)) {
518
- // Skip if not approved by diff step
519
- const targetFile = path.join(targetPath, destName);
520
- if (!approvedDests.has(targetFile)) {
521
- continue; // already logged during diff step
522
- }
523
-
524
- try {
525
- const templateContent = copyFromLocal(src);
526
-
527
- // For .env: merge existing values into the template
528
- if (mergeEnvFile.has(destName) && fs.existsSync(targetFile)) {
529
- const existing = fs.readFileSync(targetFile, 'utf8');
530
- // Parse existing key=value pairs
531
- const existingVals: Record<string, string> = {};
532
- for (const line of existing.split(/\r?\n/)) {
533
- const m = line.match(/^([^#=]+)=(.*)$/);
534
- if (m) existingVals[m[1].trim()] = m[2].trim();
535
- }
536
- // Re-write template, substituting existing values
537
- const merged = templateContent
538
- .split(/\r?\n/)
539
- .map((line) => {
540
- const m = line.match(/^([^#=]+)=(.*)$/);
541
- if (m) {
542
- const key = m[1].trim();
543
- const val = existingVals[key] ?? m[2].trim();
544
- return `${key}=${val}`;
545
- }
546
- return line;
547
- })
548
- .join('\n');
549
- fs.writeFileSync(targetFile, merged, 'utf8');
550
- console.log(` โœ… ${destName} (values preserved)`);
551
- continue;
552
- }
553
-
554
- fs.writeFileSync(targetFile, templateContent, 'utf8');
555
- console.log(` โœ… ${destName}`);
556
- } catch (error) {
557
- console.error(` โŒ Failed to inject ${destName}: ${error}`);
558
- }
559
- }
560
-
561
- // Copy .vscode config files
562
- for (const [src, destName] of Object.entries(configVscodeMap)) {
563
- try {
564
- const targetFile = path.join(targetPath, destName);
565
- if (!approvedDests.has(targetFile)) continue;
566
- const content = copyFromLocal(src);
567
- const targetDir = path.dirname(targetFile);
568
- if (!(await isValidPath(targetDir))) {
569
- fs.mkdirSync(targetDir, { recursive: true });
570
- }
571
- fs.writeFileSync(targetFile, content, 'utf8');
572
- console.log(` โœ… ${destName}`);
573
- } catch (error) {
574
- console.error(` โŒ Failed to inject ${destName}: ${error}`);
575
- }
576
- }
577
-
578
- console.log(`\n๐Ÿ“ฅ Copying GitHub workflows from local files...`);
579
-
580
- for (const workflowFile of workflowFiles) {
581
- try {
582
- const content = copyFromLocal(workflowFile);
583
- const relativePath = workflowFile.replace('templates/', '');
584
- const targetFile = path.join(targetPath, relativePath);
585
- if (!approvedDests.has(targetFile)) continue;
586
- const targetDir = path.dirname(targetFile);
587
-
588
- if (!(await isValidPath(targetDir))) {
589
- fs.mkdirSync(targetDir, { recursive: true });
590
- }
591
-
592
- fs.writeFileSync(targetFile, content, 'utf8');
593
- console.log(` โœ… ${relativePath}`);
594
- } catch (error) {
595
- console.error(` โŒ Failed to inject ${workflowFile}: ${error}`);
596
- }
597
- }
464
+ const scriptsPath = path.join(targetPath, 'scripts');
465
+
466
+ if (!(await isValidPath(scriptsPath))) {
467
+ fs.mkdirSync(scriptsPath, { recursive: true });
468
+ console.log(`๐Ÿ“ Created scripts directory`);
469
+ }
470
+
471
+ await cleanOldScripts(scriptsPath, approvedDests);
472
+ await cleanOldLintFiles(targetPath);
473
+
474
+ const scriptFiles = [
475
+ 'templates/scripts/utils.ts',
476
+ 'templates/scripts/esbuild.config.ts',
477
+ 'templates/scripts/acp.ts',
478
+ 'templates/scripts/update-version.ts',
479
+ 'templates/scripts/release.ts',
480
+ 'templates/scripts/help.ts',
481
+ 'templates/scripts/constants.ts',
482
+ 'templates/scripts/env.ts',
483
+ 'templates/scripts/reload.ts',
484
+ 'templates/scripts/typingsPlugin.ts'
485
+ ];
486
+
487
+ // Files that need value-preserving merge instead
488
+ // of full overwrite (user fills in their paths)
489
+ const mergeEnvFile = new Set(['.env']);
490
+
491
+ // Files with .template suffix (NPM excludes dotfiles)
492
+ // Map: { source: targetName }
493
+ const configFileMap: Record<string, string> = {
494
+ 'templates/tsconfig.json': 'tsconfig.json',
495
+ 'templates/gitignore.template': '.gitignore',
496
+ 'templates/eslint.config.mts': 'eslint.config.mts',
497
+ 'templates/.editorconfig': '.editorconfig',
498
+ 'templates/.prettierrc': '.prettierrc',
499
+ 'templates/.prettierignore': '.prettierignore',
500
+ 'templates/npmrc.template': '.npmrc',
501
+ 'templates/env.template': '.env'
502
+ };
503
+
504
+ const configVscodeMap: Record<string, string> = {
505
+ 'templates/.vscode/settings.json': '.vscode/settings.json',
506
+ 'templates/.vscode/tasks.json': '.vscode/tasks.json',
507
+ 'templates/.vscode/extensions.json': '.vscode/extensions.json'
508
+ };
509
+
510
+ const workflowFiles = [
511
+ 'templates/.github/workflows/release.yml',
512
+ 'templates/.github/workflows/release-body.md'
513
+ ];
514
+
515
+ console.log(`\n๐Ÿ“ฅ Copying scripts from local files...`);
516
+
517
+ for (const scriptFile of scriptFiles) {
518
+ try {
519
+ const fileName = path.basename(scriptFile);
520
+ const targetFile = path.join(scriptsPath, fileName);
521
+ if (!approvedDests.has(targetFile)) {
522
+ console.log(` โญ๏ธ Skipped ${fileName} (kept existing)`);
523
+ continue;
524
+ }
525
+ const content = copyFromLocal(scriptFile);
526
+ fs.writeFileSync(targetFile, content, 'utf8');
527
+ console.log(` โœ… ${fileName}`);
528
+ } catch (error) {
529
+ console.error(` โŒ Failed to inject ${scriptFile}: ${error}`);
530
+ }
531
+ }
532
+
533
+ console.log(`\n๐Ÿ“ฅ Copying config files...`);
534
+
535
+ // Copy root config files
536
+ for (const [src, destName] of Object.entries(configFileMap)) {
537
+ // Skip if not approved by diff step
538
+ const targetFile = path.join(targetPath, destName);
539
+ if (!approvedDests.has(targetFile)) {
540
+ continue; // already logged during diff step
541
+ }
542
+
543
+ try {
544
+ const templateContent = copyFromLocal(src);
545
+
546
+ // For .env: merge existing values into the template
547
+ if (mergeEnvFile.has(destName) && fs.existsSync(targetFile)) {
548
+ const existing = fs.readFileSync(targetFile, 'utf8');
549
+ // Parse existing key=value pairs
550
+ const existingVals: Record<string, string> = {};
551
+ for (const line of existing.split(/\r?\n/)) {
552
+ const m = line.match(/^([^#=]+)=(.*)$/);
553
+ if (m) existingVals[m[1].trim()] = m[2].trim();
554
+ }
555
+ // Re-write template, substituting existing values
556
+ const merged = templateContent
557
+ .split(/\r?\n/)
558
+ .map((line) => {
559
+ const m = line.match(/^([^#=]+)=(.*)$/);
560
+ if (m) {
561
+ const key = m[1].trim();
562
+ const val = existingVals[key] ?? m[2].trim();
563
+ return `${key}=${val}`;
564
+ }
565
+ return line;
566
+ })
567
+ .join('\n');
568
+ fs.writeFileSync(targetFile, merged, 'utf8');
569
+ console.log(` โœ… ${destName} (values preserved)`);
570
+ continue;
571
+ }
572
+
573
+ fs.writeFileSync(targetFile, templateContent, 'utf8');
574
+ console.log(` โœ… ${destName}`);
575
+ } catch (error) {
576
+ console.error(` โŒ Failed to inject ${destName}: ${error}`);
577
+ }
578
+ }
579
+
580
+ // Copy .vscode config files
581
+ for (const [src, destName] of Object.entries(configVscodeMap)) {
582
+ try {
583
+ const targetFile = path.join(targetPath, destName);
584
+ if (!approvedDests.has(targetFile)) continue;
585
+ const content = copyFromLocal(src);
586
+ const targetDir = path.dirname(targetFile);
587
+ if (!(await isValidPath(targetDir))) {
588
+ fs.mkdirSync(targetDir, { recursive: true });
589
+ }
590
+ fs.writeFileSync(targetFile, content, 'utf8');
591
+ console.log(` โœ… ${destName}`);
592
+ } catch (error) {
593
+ console.error(` โŒ Failed to inject ${destName}: ${error}`);
594
+ }
595
+ }
596
+
597
+ console.log(`\n๐Ÿ“ฅ Copying GitHub workflows from local files...`);
598
+
599
+ for (const workflowFile of workflowFiles) {
600
+ try {
601
+ const content = copyFromLocal(workflowFile);
602
+ const relativePath = workflowFile.replace('templates/', '');
603
+ const targetFile = path.join(targetPath, relativePath);
604
+ if (!approvedDests.has(targetFile)) continue;
605
+ const targetDir = path.dirname(targetFile);
606
+
607
+ if (!(await isValidPath(targetDir))) {
608
+ fs.mkdirSync(targetDir, { recursive: true });
609
+ }
610
+
611
+ fs.writeFileSync(targetFile, content, 'utf8');
612
+ console.log(` โœ… ${relativePath}`);
613
+ } catch (error) {
614
+ console.error(` โŒ Failed to inject ${workflowFile}: ${error}`);
615
+ }
616
+ }
598
617
  }
599
618
 
600
619
  /**
601
620
  * Update package.json with autonomous configuration
602
621
  */
603
622
  export async function updatePackageJson(
604
- targetPath: string,
605
- useSass: boolean = false
623
+ targetPath: string,
624
+ useSass: boolean = false
606
625
  ): Promise<void> {
607
- const packageJsonPath = path.join(targetPath, 'package.json');
608
-
609
- if (!(await isValidPath(packageJsonPath))) {
610
- console.log(`โŒ No package.json found, skipping package.json update`);
611
- return;
612
- }
613
-
614
- try {
615
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
616
-
617
- const configRoot = findPluginConfigRoot();
618
- const templatePkg = JSON.parse(
619
- fs.readFileSync(path.join(configRoot, 'templates/package.json'), 'utf8')
620
- );
621
-
622
- if (useSass) {
623
- const sassPkg = JSON.parse(
624
- fs.readFileSync(
625
- path.join(configRoot, 'templates/package-sass.json'),
626
- 'utf8'
627
- )
628
- );
629
- Object.assign(templatePkg.devDependencies, sassPkg.devDependencies);
630
- }
631
-
632
- const obsoleteScripts = ['version'];
633
- for (const script of obsoleteScripts) {
634
- if (packageJson.scripts?.[script]) {
635
- console.log(` ๐Ÿงน Removing obsolete script: "${script}"`);
636
- delete packageJson.scripts[script];
637
- }
638
- }
639
-
640
- packageJson.scripts = {
641
- ...packageJson.scripts,
642
- ...templatePkg.scripts
643
- };
644
-
645
- if (!packageJson.devDependencies) packageJson.devDependencies = {};
646
-
647
- const requiredDeps: Record<string, string> = templatePkg.devDependencies;
648
-
649
- let addedDeps = 0;
650
- let updatedDeps = 0;
651
- for (const [dep, version] of Object.entries(requiredDeps)) {
652
- if (!packageJson.devDependencies[dep]) {
653
- packageJson.devDependencies[dep] = version as string;
654
- addedDeps++;
655
- } else if (packageJson.devDependencies[dep] !== version) {
656
- packageJson.devDependencies[dep] = version as string;
657
- updatedDeps++;
658
- }
659
- }
660
-
661
- if (!packageJson.engines) packageJson.engines = {};
662
- packageJson.engines.npm = templatePkg.engines.npm;
663
- packageJson.engines.yarn = templatePkg.engines.yarn;
664
- packageJson.type = templatePkg.type;
665
-
666
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
667
- console.log(
668
- ` โœ… Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)`
669
- );
670
- } catch (error) {
671
- console.error(` โŒ Failed to update package.json: ${error}`);
672
- }
626
+ const packageJsonPath = path.join(targetPath, 'package.json');
627
+
628
+ if (!(await isValidPath(packageJsonPath))) {
629
+ console.log(`โŒ No package.json found, skipping package.json update`);
630
+ return;
631
+ }
632
+
633
+ try {
634
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
635
+
636
+ const configRoot = findPluginConfigRoot();
637
+ const templatePkg = JSON.parse(
638
+ fs.readFileSync(path.join(configRoot, 'templates/package.json'), 'utf8')
639
+ );
640
+
641
+ if (useSass) {
642
+ const sassPkg = JSON.parse(
643
+ fs.readFileSync(path.join(configRoot, 'templates/package-sass.json'), 'utf8')
644
+ );
645
+ Object.assign(templatePkg.devDependencies, sassPkg.devDependencies);
646
+ }
647
+
648
+ const obsoleteScripts = ['version'];
649
+ for (const script of obsoleteScripts) {
650
+ if (packageJson.scripts?.[script]) {
651
+ console.log(` ๐Ÿงน Removing obsolete script: "${script}"`);
652
+ delete packageJson.scripts[script];
653
+ }
654
+ }
655
+
656
+ packageJson.scripts = {
657
+ ...packageJson.scripts,
658
+ ...templatePkg.scripts
659
+ };
660
+
661
+ if (!packageJson.devDependencies) packageJson.devDependencies = {};
662
+
663
+ const requiredDeps: Record<string, string> = templatePkg.devDependencies;
664
+
665
+ let addedDeps = 0;
666
+ let updatedDeps = 0;
667
+ for (const [dep, version] of Object.entries(requiredDeps)) {
668
+ if (!packageJson.devDependencies[dep]) {
669
+ packageJson.devDependencies[dep] = version as string;
670
+ addedDeps++;
671
+ } else if (packageJson.devDependencies[dep] !== version) {
672
+ packageJson.devDependencies[dep] = version as string;
673
+ updatedDeps++;
674
+ }
675
+ }
676
+
677
+ if (!packageJson.engines) packageJson.engines = {};
678
+ packageJson.engines.npm = templatePkg.engines.npm;
679
+ packageJson.engines.yarn = templatePkg.engines.yarn;
680
+ packageJson.type = templatePkg.type;
681
+
682
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
683
+ console.log(
684
+ ` โœ… Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)`
685
+ );
686
+ } catch (error) {
687
+ console.error(` โŒ Failed to update package.json: ${error}`);
688
+ }
673
689
  }
674
690
 
675
691
  /**
676
692
  * Analyze centralized imports in source files (without modifying)
677
693
  */
678
694
  export async function analyzeCentralizedImports(targetPath: string): Promise<void> {
679
- const srcPath = path.join(targetPath, 'src');
680
-
681
- if (!(await isValidPath(srcPath))) {
682
- console.log(` โ„น๏ธ No src directory found`);
683
- return;
684
- }
685
-
686
- console.log(`\n๐Ÿ” Analyzing centralized imports...`);
687
-
688
- try {
689
- const findTsFiles = (dir: string): string[] => {
690
- const files: string[] = [];
691
- for (const item of fs.readdirSync(dir)) {
692
- const fullPath = path.join(dir, item);
693
- if (fs.statSync(fullPath).isDirectory()) {
694
- files.push(...findTsFiles(fullPath));
695
- } else if (item.endsWith('.ts') || item.endsWith('.tsx')) {
696
- files.push(fullPath);
697
- }
698
- }
699
- return files;
700
- };
701
-
702
- const tsFiles = findTsFiles(srcPath);
703
- let filesWithImports = 0;
704
-
705
- for (const filePath of tsFiles) {
706
- try {
707
- const content = fs.readFileSync(filePath, 'utf8');
708
- const importRegex =
709
- /import\s+.*from\s+["']obsidian-plugin-config[^"']*["']/g;
710
- if (importRegex.test(content)) {
711
- filesWithImports++;
712
- console.log(
713
- ` โš ๏ธ ${path.relative(targetPath, filePath)} - contains centralized imports`
714
- );
715
- }
716
- } catch (error) {
717
- console.warn(
718
- ` โš ๏ธ Could not analyze ${path.relative(targetPath, filePath)}: ${error}`
719
- );
720
- }
721
- }
722
-
723
- if (filesWithImports === 0) {
724
- console.log(` โœ… No centralized imports found`);
725
- } else {
726
- console.log(
727
- ` โš ๏ธ Found ${filesWithImports} files with centralized imports`
728
- );
729
- console.log(
730
- ` ๐Ÿ’ก You may need to manually comment these imports for the plugin to work`
731
- );
732
- }
733
- } catch (error) {
734
- console.error(` โŒ Failed to analyze imports: ${error}`);
735
- }
695
+ const srcPath = path.join(targetPath, 'src');
696
+
697
+ if (!(await isValidPath(srcPath))) {
698
+ console.log(` โ„น๏ธ No src directory found`);
699
+ return;
700
+ }
701
+
702
+ console.log(`\n๐Ÿ” Analyzing centralized imports...`);
703
+
704
+ try {
705
+ const findTsFiles = (dir: string): string[] => {
706
+ const files: string[] = [];
707
+ for (const item of fs.readdirSync(dir)) {
708
+ const fullPath = path.join(dir, item);
709
+ if (fs.statSync(fullPath).isDirectory()) {
710
+ files.push(...findTsFiles(fullPath));
711
+ } else if (item.endsWith('.ts') || item.endsWith('.tsx')) {
712
+ files.push(fullPath);
713
+ }
714
+ }
715
+ return files;
716
+ };
717
+
718
+ const tsFiles = findTsFiles(srcPath);
719
+ let filesWithImports = 0;
720
+
721
+ for (const filePath of tsFiles) {
722
+ try {
723
+ const content = fs.readFileSync(filePath, 'utf8');
724
+ const importRegex = /import\s+.*from\s+["']obsidian-plugin-config[^"']*["']/g;
725
+ if (importRegex.test(content)) {
726
+ filesWithImports++;
727
+ console.log(
728
+ ` โš ๏ธ ${path.relative(targetPath, filePath)} - contains centralized imports`
729
+ );
730
+ }
731
+ } catch (error) {
732
+ console.warn(
733
+ ` โš ๏ธ Could not analyze ${path.relative(targetPath, filePath)}: ${error}`
734
+ );
735
+ }
736
+ }
737
+
738
+ if (filesWithImports === 0) {
739
+ console.log(` โœ… No centralized imports found`);
740
+ } else {
741
+ console.log(` โš ๏ธ Found ${filesWithImports} files with centralized imports`);
742
+ console.log(
743
+ ` ๐Ÿ’ก You may need to manually comment these imports for the plugin to work`
744
+ );
745
+ }
746
+ } catch (error) {
747
+ console.error(` โŒ Failed to analyze imports: ${error}`);
748
+ }
736
749
  }
737
750
 
738
751
  /**
739
752
  * Create required directories
740
753
  */
741
754
  export async function createRequiredDirectories(targetPath: string): Promise<void> {
742
- const directories = [path.join(targetPath, '.github', 'workflows')];
743
-
744
- for (const dir of directories) {
745
- if (!(await isValidPath(dir))) {
746
- fs.mkdirSync(dir, { recursive: true });
747
- console.log(` ๐Ÿ“ Created ${path.relative(targetPath, dir)}`);
748
- }
749
- }
755
+ const directories = [path.join(targetPath, '.github', 'workflows')];
756
+
757
+ for (const dir of directories) {
758
+ if (!(await isValidPath(dir))) {
759
+ fs.mkdirSync(dir, { recursive: true });
760
+ console.log(` ๐Ÿ“ Created ${path.relative(targetPath, dir)}`);
761
+ }
762
+ }
750
763
  }
751
764
 
752
765
  /**
753
766
  * Create injection info file
754
767
  */
755
768
  export async function createInjectionInfo(targetPath: string): Promise<void> {
756
- const configRoot = findPluginConfigRoot();
757
- const configPackageJsonPath = path.join(configRoot, 'package.json');
758
-
759
- let injectorVersion = 'unknown';
760
- try {
761
- const configPackageJson = JSON.parse(
762
- fs.readFileSync(configPackageJsonPath, 'utf8')
763
- );
764
- injectorVersion = configPackageJson.version || 'unknown';
765
- } catch {
766
- console.warn('Warning: Could not read injector version');
767
- }
768
-
769
- const injectionInfo = {
770
- injectorVersion,
771
- injectionDate: new Date().toISOString(),
772
- injectorName: 'obsidian-plugin-config'
773
- };
774
-
775
- const infoPath = path.join(targetPath, '.injection-info.json');
776
- fs.writeFileSync(infoPath, JSON.stringify(injectionInfo, null, 2));
777
- console.log(` โœ… Created injection info file (.injection-info.json)`);
769
+ const configRoot = findPluginConfigRoot();
770
+ const configPackageJsonPath = path.join(configRoot, 'package.json');
771
+
772
+ let injectorVersion = 'unknown';
773
+ try {
774
+ const configPackageJson = JSON.parse(fs.readFileSync(configPackageJsonPath, 'utf8'));
775
+ injectorVersion = configPackageJson.version || 'unknown';
776
+ } catch {
777
+ console.warn('Warning: Could not read injector version');
778
+ }
779
+
780
+ const injectionInfo = {
781
+ injectorVersion,
782
+ injectionDate: new Date().toISOString(),
783
+ injectorName: 'obsidian-plugin-config'
784
+ };
785
+
786
+ const infoPath = path.join(targetPath, '.injection-info.json');
787
+ fs.writeFileSync(infoPath, JSON.stringify(injectionInfo, null, 2));
788
+ console.log(` โœ… Created injection info file (.injection-info.json)`);
778
789
  }
779
790
 
780
791
  /**
781
792
  * Read injection info from target plugin
782
793
  */
783
794
  export function readInjectionInfo(targetPath: string): Record<string, string> | null {
784
- const infoPath = path.join(targetPath, '.injection-info.json');
795
+ const infoPath = path.join(targetPath, '.injection-info.json');
785
796
 
786
- if (!fs.existsSync(infoPath)) return null;
797
+ if (!fs.existsSync(infoPath)) return null;
787
798
 
788
- try {
789
- return JSON.parse(fs.readFileSync(infoPath, 'utf8'));
790
- } catch {
791
- console.warn('Warning: Could not parse .injection-info.json');
792
- return null;
793
- }
799
+ try {
800
+ return JSON.parse(fs.readFileSync(infoPath, 'utf8'));
801
+ } catch {
802
+ console.warn('Warning: Could not parse .injection-info.json');
803
+ return null;
804
+ }
794
805
  }
795
806
 
796
807
  /**
797
808
  * Clean NPM/Yarn lock files and node_modules to ensure fresh install
798
809
  */
799
810
  export async function cleanNpmArtifactsIfNeeded(targetPath: string): Promise<void> {
800
- const packageLockPath = path.join(targetPath, 'package-lock.json');
801
- const yarnLockPath = path.join(targetPath, 'yarn.lock');
802
- const nodeModulesPath = path.join(targetPath, 'node_modules');
803
-
804
- const hasPackageLock = fs.existsSync(packageLockPath);
805
- const hasYarnLock = fs.existsSync(yarnLockPath);
806
-
807
- if (hasPackageLock) {
808
- console.log(`\n๐Ÿงน Cleaning NPM artifacts (migrating to Yarn)...`);
809
-
810
- try {
811
- // Remove node_modules FIRST (before lock files)
812
- if (fs.existsSync(nodeModulesPath)) {
813
- console.log(` โณ Removing node_modules (this may take a moment)...`);
814
-
815
- execSync(`rmdir /s /q "${nodeModulesPath}"`, {
816
- stdio: 'pipe',
817
- windowsHide: true
818
- });
819
-
820
- if (fs.existsSync(nodeModulesPath)) {
821
- // rmdir failed silently (locked .exe files) - rename instead
822
- const timestamp = Date.now();
823
- const oldPath = `${nodeModulesPath}.old.${timestamp}`;
824
- try {
825
- fs.renameSync(nodeModulesPath, oldPath);
826
- console.log(` ๐Ÿ”„ Renamed locked node_modules to ${path.basename(oldPath)}`);
827
- console.log(` ๐Ÿ’ก Delete it manually later: ${oldPath}`);
828
- } catch {
829
- console.log(` โš ๏ธ Could not remove/rename node_modules (locked by processes)`);
830
- console.log(` ๐Ÿ’ก Close Obsidian/VSCode and run: obsidian-inject again`);
831
- throw new Error('node_modules locked - close processes and retry');
832
- }
833
- } else {
834
- console.log(` ๐Ÿ—‘๏ธ Removed node_modules (will be reinstalled with Yarn)`);
835
- }
836
- }
837
-
838
- // Then remove lock files
839
- if (hasPackageLock) {
840
- fs.unlinkSync(packageLockPath);
841
- console.log(` ๐Ÿ—‘๏ธ Removed package-lock.json`);
842
- }
843
-
844
- if (hasYarnLock) {
845
- fs.unlinkSync(yarnLockPath);
846
- console.log(` ๐Ÿ—‘๏ธ Removed yarn.lock`);
847
- }
848
-
849
- console.log(` โœ… Lock files and artifacts cleaned for fresh install`);
850
- } catch (error) {
851
- if (error instanceof Error && error.message.includes('locked')) {
852
- throw error;
853
- }
854
- console.error(` โŒ Failed to clean artifacts: ${error}`);
855
- console.log(
856
- ` ๐Ÿ’ก You may need to manually remove package-lock.json, yarn.lock and node_modules`
857
- );
858
- }
859
- }
811
+ const packageLockPath = path.join(targetPath, 'package-lock.json');
812
+ const yarnLockPath = path.join(targetPath, 'yarn.lock');
813
+ const nodeModulesPath = path.join(targetPath, 'node_modules');
814
+
815
+ const hasPackageLock = fs.existsSync(packageLockPath);
816
+ const hasYarnLock = fs.existsSync(yarnLockPath);
817
+
818
+ if (hasPackageLock) {
819
+ console.log(`\n๐Ÿงน Cleaning NPM artifacts (migrating to Yarn)...`);
820
+
821
+ try {
822
+ // Remove node_modules FIRST (before lock files)
823
+ if (fs.existsSync(nodeModulesPath)) {
824
+ console.log(` โณ Removing node_modules (this may take a moment)...`);
825
+
826
+ execSync(`rmdir /s /q "${nodeModulesPath}"`, {
827
+ stdio: 'pipe',
828
+ windowsHide: true
829
+ });
830
+
831
+ if (fs.existsSync(nodeModulesPath)) {
832
+ // rmdir failed silently (locked .exe files) - rename instead
833
+ const timestamp = Date.now();
834
+ const oldPath = `${nodeModulesPath}.old.${timestamp}`;
835
+ try {
836
+ fs.renameSync(nodeModulesPath, oldPath);
837
+ console.log(` ๐Ÿ”„ Renamed locked node_modules to ${path.basename(oldPath)}`);
838
+ console.log(` ๐Ÿ’ก Delete it manually later: ${oldPath}`);
839
+ } catch {
840
+ console.log(
841
+ ` โš ๏ธ Could not remove/rename node_modules (locked by processes)`
842
+ );
843
+ console.log(` ๐Ÿ’ก Close Obsidian/VSCode and run: obsidian-inject again`);
844
+ throw new Error('node_modules locked - close processes and retry');
845
+ }
846
+ } else {
847
+ console.log(` ๐Ÿ—‘๏ธ Removed node_modules (will be reinstalled with Yarn)`);
848
+ }
849
+ }
850
+
851
+ // Then remove lock files
852
+ if (hasPackageLock) {
853
+ fs.unlinkSync(packageLockPath);
854
+ console.log(` ๐Ÿ—‘๏ธ Removed package-lock.json`);
855
+ }
856
+
857
+ if (hasYarnLock) {
858
+ fs.unlinkSync(yarnLockPath);
859
+ console.log(` ๐Ÿ—‘๏ธ Removed yarn.lock`);
860
+ }
861
+
862
+ console.log(` โœ… Lock files and artifacts cleaned for fresh install`);
863
+ } catch (error) {
864
+ if (error instanceof Error && error.message.includes('locked')) {
865
+ throw error;
866
+ }
867
+ console.error(` โŒ Failed to clean artifacts: ${error}`);
868
+ console.log(
869
+ ` ๐Ÿ’ก You may need to manually remove package-lock.json, yarn.lock and node_modules`
870
+ );
871
+ }
872
+ }
860
873
  }
861
874
 
862
875
  /**
863
876
  * Check if tsx is installed locally and install it if needed
864
877
  */
865
878
  export async function ensureTsxInstalled(targetPath: string): Promise<void> {
866
- console.log(`\n๐Ÿ” Checking tsx installation...`);
867
-
868
- const packageJsonPath = path.join(targetPath, 'package.json');
869
-
870
- try {
871
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
872
- const devDependencies = packageJson.devDependencies || {};
873
- const dependencies = packageJson.dependencies || {};
874
-
875
- if (devDependencies.tsx || dependencies.tsx) {
876
- console.log(` โœ… tsx is already installed`);
877
- return;
878
- }
879
-
880
- console.log(` โš ๏ธ tsx not found, installing as dev dependency...`);
881
- execSync('yarn add -D tsx', { cwd: targetPath, stdio: 'inherit' });
882
- console.log(` โœ… tsx installed successfully`);
883
- } catch (error) {
884
- console.error(` โŒ Failed to install tsx: ${error}`);
885
- console.log(` ๐Ÿ’ก You may need to install tsx manually: yarn add -D tsx`);
886
- throw new Error('tsx installation failed');
887
- }
879
+ console.log(`\n๐Ÿ” Checking tsx installation...`);
880
+
881
+ const packageJsonPath = path.join(targetPath, 'package.json');
882
+
883
+ try {
884
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
885
+ const devDependencies = packageJson.devDependencies || {};
886
+ const dependencies = packageJson.dependencies || {};
887
+
888
+ if (devDependencies.tsx || dependencies.tsx) {
889
+ console.log(` โœ… tsx is already installed`);
890
+ return;
891
+ }
892
+
893
+ console.log(` โš ๏ธ tsx not found, installing as dev dependency...`);
894
+ execSync('yarn add -D tsx', { cwd: targetPath, stdio: 'inherit' });
895
+ console.log(` โœ… tsx installed successfully`);
896
+ } catch (error) {
897
+ console.error(` โŒ Failed to install tsx: ${error}`);
898
+ console.log(` ๐Ÿ’ก You may need to install tsx manually: yarn add -D tsx`);
899
+ throw new Error('tsx installation failed');
900
+ }
888
901
  }
889
902
 
890
903
  /**
891
904
  * Run yarn install in target directory
892
905
  */
893
906
  export async function runYarnInstall(targetPath: string): Promise<void> {
894
- console.log(`\n๐Ÿ“ฆ Installing dependencies...`);
895
-
896
- try {
897
- execSync('yarn install', { cwd: targetPath, stdio: 'inherit' });
898
- console.log(` โœ… Dependencies installed successfully`);
899
- } catch (error) {
900
- console.error(` โŒ Failed to install dependencies: ${error}`);
901
- console.log(
902
- ` ๐Ÿ’ก You may need to run 'yarn install' manually in the target directory`
903
- );
904
- }
907
+ console.log(`\n๐Ÿ“ฆ Installing dependencies...`);
908
+
909
+ try {
910
+ execSync('yarn install', { cwd: targetPath, stdio: 'inherit' });
911
+ console.log(` โœ… Dependencies installed successfully`);
912
+ } catch (error) {
913
+ console.error(` โŒ Failed to install dependencies: ${error}`);
914
+ console.log(
915
+ ` ๐Ÿ’ก You may need to run 'yarn install' manually in the target directory`
916
+ );
917
+ }
905
918
  }
906
919
 
907
920
  /**
908
921
  * Main injection orchestration function
909
922
  */
910
923
  export async function performInjection(
911
- targetPath: string,
912
- autoConfirm: boolean = false,
913
- useSass: boolean = false
924
+ targetPath: string,
925
+ autoConfirm: boolean = false,
926
+ useSass: boolean = false
914
927
  ): Promise<void> {
915
- console.log(`\n๐Ÿš€ Starting injection process...`);
916
-
917
- try {
918
- const approvedDests = await diffAndPromptFiles(targetPath, autoConfirm);
919
- await cleanNpmArtifactsIfNeeded(targetPath);
920
- await ensureTsxInstalled(targetPath);
921
- await injectScripts(targetPath, approvedDests);
922
-
923
- console.log(`\n๐Ÿ“ฆ Updating package.json...`);
924
- await updatePackageJson(targetPath, useSass);
925
-
926
- await analyzeCentralizedImports(targetPath);
927
-
928
- console.log(`\n๐Ÿ“ Creating required directories...`);
929
- await createRequiredDirectories(targetPath);
930
-
931
- await runYarnInstall(targetPath);
932
-
933
- console.log(`\n๐Ÿ“ Creating injection info...`);
934
- await createInjectionInfo(targetPath);
935
-
936
- console.log(`\nโœ… Injection completed successfully!`);
937
- console.log(`\n๐Ÿ“‹ Next steps:`);
938
- console.log(` 1. cd ${targetPath}`);
939
- console.log(` 2. yarn build # Test the build`);
940
- console.log(` 3. yarn start # Test development mode`);
941
- console.log(
942
- ` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`
943
- );
944
-
945
- // Check for .old directories and remind user to delete them
946
- const oldDirs = fs.readdirSync(targetPath)
947
- .filter(name => name.startsWith('node_modules.old.'))
948
- .map(name => path.basename(name));
949
-
950
- if (oldDirs.length > 0) {
951
- console.log(`\n๐Ÿงน Cleanup reminder:`);
952
- for (const oldDir of oldDirs) {
953
- console.log(` ๐Ÿ—‘๏ธ Delete manually: ${oldDir}`);
954
- }
955
- console.log(` ๐Ÿ’ก Close all processes first, then delete these folders`);
956
- }
957
- } catch (error) {
958
- console.error(`\nโŒ Injection failed: ${error}`);
959
- throw error;
960
- }
928
+ console.log(`\n๐Ÿš€ Starting injection process...`);
929
+
930
+ try {
931
+ const approvedDests = await diffAndPromptFiles(targetPath, autoConfirm);
932
+ await cleanNpmArtifactsIfNeeded(targetPath);
933
+ await ensureTsxInstalled(targetPath);
934
+ await injectScripts(targetPath, approvedDests);
935
+
936
+ console.log(`\n๐Ÿ“ฆ Updating package.json...`);
937
+ await updatePackageJson(targetPath, useSass);
938
+
939
+ await analyzeCentralizedImports(targetPath);
940
+
941
+ console.log(`\n๐Ÿ“ Creating required directories...`);
942
+ await createRequiredDirectories(targetPath);
943
+
944
+ await runYarnInstall(targetPath);
945
+
946
+ console.log(`\n๐Ÿ“ Creating injection info...`);
947
+ await createInjectionInfo(targetPath);
948
+
949
+ console.log(`\nโœ… Injection completed successfully!`);
950
+ console.log(`\n๐Ÿ“‹ Next steps:`);
951
+ console.log(` 1. cd ${targetPath}`);
952
+ console.log(` 2. yarn build # Test the build`);
953
+ console.log(` 3. yarn start # Test development mode`);
954
+ console.log(` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`);
955
+
956
+ // Check for .old directories and remind user to delete them
957
+ const oldDirs = fs
958
+ .readdirSync(targetPath)
959
+ .filter((name) => name.startsWith('node_modules.old.'))
960
+ .map((name) => path.basename(name));
961
+
962
+ if (oldDirs.length > 0) {
963
+ console.log(`\n๐Ÿงน Cleanup reminder:`);
964
+ for (const oldDir of oldDirs) {
965
+ console.log(` ๐Ÿ—‘๏ธ Delete manually: ${oldDir}`);
966
+ }
967
+ console.log(` ๐Ÿ’ก Close all processes first, then delete these folders`);
968
+ }
969
+ } catch (error) {
970
+ console.error(`\nโŒ Injection failed: ${error}`);
971
+ throw error;
972
+ }
961
973
  }