obsidian-plugin-config 1.7.2 → 1.7.5
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/README.md +7 -7
- package/bin/obsidian-inject.js +21 -13
- package/docs/INTERACTIVE_INJECTION.md +41 -50
- package/docs/LLM-GUIDE.md +8 -7
- package/docs/SCSS-FLOW.md +277 -0
- package/package.json +7 -1
- package/scripts/acp.ts +9 -8
- package/scripts/build-npm.ts +397 -393
- package/scripts/help.ts +87 -87
- package/scripts/inject-core.ts +870 -897
- package/scripts/inject-path.ts +157 -156
- package/scripts/inject-prompt.ts +104 -104
- package/scripts/utils.ts +173 -151
- package/templates/.github/workflows/release.yml +2 -2
- package/templates/.vscode/tasks.json +1 -1
- package/templates/{package.json → package.json.template} +1 -1
- package/templates/scripts/acp.ts +2 -2
- package/templates/scripts/env.ts +4 -5
- package/templates/scripts/esbuild.config.ts +4 -5
- package/templates/scripts/release.ts +14 -17
- package/templates/scripts/utils.ts +42 -25
- package/tsconfig.json +2 -2
- /package/templates/{tsconfig.json → tsconfig.json.template} +0 -0
package/scripts/inject-core.ts
CHANGED
|
@@ -1,897 +1,870 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.log(`\n
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.log(
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.log(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
'
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
'
|
|
201
|
-
'
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'templates/scripts/
|
|
291
|
-
'templates/scripts/
|
|
292
|
-
'templates/scripts/
|
|
293
|
-
'templates/scripts/
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
['templates/
|
|
311
|
-
['templates
|
|
312
|
-
['templates/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
targetPath
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Different → ask user (or auto-approve if autoConfirm)
|
|
425
|
-
hasChanges = true;
|
|
426
|
-
const relDest = path.relative(targetPath, entry.dest);
|
|
427
|
-
|
|
428
|
-
if (autoConfirm) {
|
|
429
|
-
console.log(` ✅ ${relDest} (will be updated)`);
|
|
430
|
-
approved.add(entry.dest);
|
|
431
|
-
} else {
|
|
432
|
-
const update = await askConfirmation(
|
|
433
|
-
` Update ${relDest}? (content differs)`,
|
|
434
|
-
rl!
|
|
435
|
-
);
|
|
436
|
-
if (update) {
|
|
437
|
-
approved.add(entry.dest);
|
|
438
|
-
} else {
|
|
439
|
-
console.log(` ⏭️ Kept existing ${relDest}`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (!hasChanges) {
|
|
445
|
-
console.log(` ✅ All existing files are up to date`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (rl) rl.close();
|
|
449
|
-
return approved;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Inject scripts and config files
|
|
454
|
-
*/
|
|
455
|
-
export async function injectScripts(
|
|
456
|
-
targetPath: string,
|
|
457
|
-
approvedDests: Set<string>
|
|
458
|
-
): Promise<void> {
|
|
459
|
-
const scriptsPath = path.join(targetPath, 'scripts');
|
|
460
|
-
|
|
461
|
-
if (!(await isValidPath(scriptsPath))) {
|
|
462
|
-
|
|
463
|
-
console.log(`📁 Created scripts directory`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
await cleanOldScripts(scriptsPath, approvedDests);
|
|
467
|
-
await cleanOldLintFiles(targetPath);
|
|
468
|
-
|
|
469
|
-
const scriptFiles = [
|
|
470
|
-
'templates/scripts/utils.ts',
|
|
471
|
-
'templates/scripts/esbuild.config.ts',
|
|
472
|
-
'templates/scripts/acp.ts',
|
|
473
|
-
'templates/scripts/update-version.ts',
|
|
474
|
-
'templates/scripts/release.ts',
|
|
475
|
-
'templates/scripts/help.ts',
|
|
476
|
-
'templates/scripts/constants.ts',
|
|
477
|
-
'templates/scripts/env.ts',
|
|
478
|
-
'templates/scripts/reload.ts',
|
|
479
|
-
'templates/scripts/typingsPlugin.ts'
|
|
480
|
-
];
|
|
481
|
-
|
|
482
|
-
// Files that need value-preserving merge instead
|
|
483
|
-
// of full overwrite (user fills in their paths)
|
|
484
|
-
const mergeEnvFile = new Set(['.env']);
|
|
485
|
-
|
|
486
|
-
// Files with .template suffix (NPM excludes dotfiles)
|
|
487
|
-
// Map: { source: targetName }
|
|
488
|
-
const configFileMap: Record<string, string> = {
|
|
489
|
-
'templates/tsconfig.json': 'tsconfig.json',
|
|
490
|
-
'templates/gitignore.template': '.gitignore',
|
|
491
|
-
'templates/eslint.config.mts': 'eslint.config.mts',
|
|
492
|
-
'templates/.editorconfig': '.editorconfig',
|
|
493
|
-
'templates/.prettierrc': '.prettierrc',
|
|
494
|
-
'templates/.prettierignore': '.prettierignore',
|
|
495
|
-
'templates/npmrc.template': '.npmrc',
|
|
496
|
-
'templates
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
'templates/.vscode/
|
|
502
|
-
'templates/.vscode/
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
'templates/.github/workflows/release
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const existingVals: Record<string, string> = {};
|
|
546
|
-
for (const line of existing.split(/\r?\n/)) {
|
|
547
|
-
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
548
|
-
if (m) existingVals[m[1].trim()] = m[2].trim();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
if (!
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
await createInjectionInfo(targetPath);
|
|
872
|
-
|
|
873
|
-
console.log(`\n✅ Injection completed successfully!`);
|
|
874
|
-
console.log(`\n📋 Next steps:`);
|
|
875
|
-
console.log(` 1. cd ${targetPath}`);
|
|
876
|
-
console.log(` 2. yarn build # Test the build`);
|
|
877
|
-
console.log(` 3. yarn start # Test development mode`);
|
|
878
|
-
console.log(` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`);
|
|
879
|
-
|
|
880
|
-
// Check for .old directories and remind user to delete them
|
|
881
|
-
const oldDirs = fs
|
|
882
|
-
.readdirSync(targetPath)
|
|
883
|
-
.filter((name) => name.startsWith('node_modules.old.'))
|
|
884
|
-
.map((name) => path.basename(name));
|
|
885
|
-
|
|
886
|
-
if (oldDirs.length > 0) {
|
|
887
|
-
console.log(`\n🧹 Cleanup reminder:`);
|
|
888
|
-
for (const oldDir of oldDirs) {
|
|
889
|
-
console.log(` 🗑️ Delete manually: ${oldDir}`);
|
|
890
|
-
}
|
|
891
|
-
console.log(` 💡 Close all processes first, then delete these folders`);
|
|
892
|
-
}
|
|
893
|
-
} catch (error) {
|
|
894
|
-
console.error(`\n❌ Injection failed: ${error}`);
|
|
895
|
-
throw error;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'fs/promises';
|
|
6
|
+
import {
|
|
7
|
+
askConfirmation,
|
|
8
|
+
createReadlineInterface,
|
|
9
|
+
gitExec,
|
|
10
|
+
gitOutput,
|
|
11
|
+
isValidPath
|
|
12
|
+
} from './utils.ts';
|
|
13
|
+
|
|
14
|
+
export interface InjectionPlan {
|
|
15
|
+
targetPath: string;
|
|
16
|
+
isObsidianPlugin: boolean;
|
|
17
|
+
hasPackageJson: boolean;
|
|
18
|
+
hasManifest: boolean;
|
|
19
|
+
hasScriptsFolder: boolean;
|
|
20
|
+
currentDependencies: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Analyze the target plugin directory
|
|
25
|
+
*/
|
|
26
|
+
export async function analyzePlugin(pluginPath: string): Promise<InjectionPlan> {
|
|
27
|
+
const packageJsonPath = path.join(pluginPath, 'package.json');
|
|
28
|
+
const manifestPath = path.join(pluginPath, 'manifest.json');
|
|
29
|
+
const scriptsPath = path.join(pluginPath, 'scripts');
|
|
30
|
+
|
|
31
|
+
const plan: InjectionPlan = {
|
|
32
|
+
targetPath: pluginPath,
|
|
33
|
+
isObsidianPlugin: false,
|
|
34
|
+
hasPackageJson: await isValidPath(packageJsonPath),
|
|
35
|
+
hasManifest: await isValidPath(manifestPath),
|
|
36
|
+
hasScriptsFolder: await isValidPath(scriptsPath),
|
|
37
|
+
currentDependencies: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (plan.hasManifest) {
|
|
41
|
+
try {
|
|
42
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
43
|
+
plan.isObsidianPlugin = !!(manifest.id && manifest.name && manifest.version);
|
|
44
|
+
} catch {
|
|
45
|
+
console.warn('Warning: Could not parse manifest.json');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (plan.hasPackageJson) {
|
|
50
|
+
try {
|
|
51
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
52
|
+
plan.currentDependencies = [
|
|
53
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
54
|
+
...Object.keys(packageJson.devDependencies || {})
|
|
55
|
+
];
|
|
56
|
+
} catch {
|
|
57
|
+
console.warn('Warning: Could not parse package.json');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return plan;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find plugin-config root directory (handles NPM global installs)
|
|
66
|
+
*/
|
|
67
|
+
export async function findPluginConfigRoot(): Promise<string> {
|
|
68
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
const npmPackageRoot = path.resolve(scriptDir, '..');
|
|
70
|
+
const npmPackageJson = path.join(npmPackageRoot, 'package.json');
|
|
71
|
+
|
|
72
|
+
if (await isValidPath(npmPackageJson)) {
|
|
73
|
+
try {
|
|
74
|
+
const packageContent = JSON.parse(await readFile(npmPackageJson, 'utf8'));
|
|
75
|
+
if (packageContent.name === 'obsidian-plugin-config') {
|
|
76
|
+
return npmPackageRoot;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore parsing errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return process.cwd();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Copy file content from local plugin-config directory
|
|
88
|
+
*/
|
|
89
|
+
export async function copyFromLocal(filePath: string): Promise<string> {
|
|
90
|
+
const configRoot = await findPluginConfigRoot();
|
|
91
|
+
const sourcePath = path.join(configRoot, filePath);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
return await readFile(sourcePath, 'utf8');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(`Failed to copy ${filePath}: ${error}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if plugin-config repo is clean and commit if needed
|
|
102
|
+
*/
|
|
103
|
+
export async function ensurePluginConfigClean(): Promise<void> {
|
|
104
|
+
const configRoot = await findPluginConfigRoot();
|
|
105
|
+
const gitDir = path.join(configRoot, '.git');
|
|
106
|
+
|
|
107
|
+
// Skip git check if not a git repo
|
|
108
|
+
// (e.g. NPM global install)
|
|
109
|
+
if (!(await isValidPath(gitDir))) {
|
|
110
|
+
console.log(`✅ Plugin-config repo is clean` + ` (NPM install, no git check)`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const gitStatus = gitOutput('git status --porcelain', configRoot);
|
|
116
|
+
|
|
117
|
+
if (gitStatus) {
|
|
118
|
+
console.log(`\n⚠️ Plugin-config has uncommitted changes:`);
|
|
119
|
+
console.log(gitStatus);
|
|
120
|
+
console.log(`\n🔧 Auto-committing changes...`);
|
|
121
|
+
|
|
122
|
+
const msg = '🔧 Update plugin-config templates';
|
|
123
|
+
gitExec('git add -A', configRoot);
|
|
124
|
+
gitExec(`git commit -m "${msg}"`, configRoot);
|
|
125
|
+
|
|
126
|
+
const branch = gitOutput('git rev-parse --abbrev-ref HEAD', configRoot);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
gitExec(`git push origin ${branch}`, configRoot);
|
|
130
|
+
console.log(`✅ Changes committed and pushed`);
|
|
131
|
+
} catch {
|
|
132
|
+
try {
|
|
133
|
+
gitExec(`git push --set-upstream origin ${branch}`, configRoot);
|
|
134
|
+
console.log(`✅ New branch pushed with upstream`);
|
|
135
|
+
} catch {
|
|
136
|
+
console.log(`⚠️ Committed locally, push failed`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`✅ Plugin-config repo is clean`);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`⚠️ Failed to check or commit plugin-config: ${error}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Display injection plan and ask for confirmation
|
|
149
|
+
*/
|
|
150
|
+
export async function showInjectionPlan(
|
|
151
|
+
plan: InjectionPlan,
|
|
152
|
+
autoConfirm: boolean = false
|
|
153
|
+
): Promise<boolean> {
|
|
154
|
+
console.log(`\n🎯 Injection Plan for: ${plan.targetPath}`);
|
|
155
|
+
console.log(`📁 Target: ${path.basename(plan.targetPath)}`);
|
|
156
|
+
console.log(`📦 Package.json: ${plan.hasPackageJson ? '✅' : '❌'}`);
|
|
157
|
+
console.log(`📋 Manifest.json: ${plan.hasManifest ? '✅' : '❌'}`);
|
|
158
|
+
console.log(
|
|
159
|
+
`📂 Scripts folder: ${plan.hasScriptsFolder ? '✅ (will be updated)' : '❌ (will be created)'}`
|
|
160
|
+
);
|
|
161
|
+
console.log(`🔌 Obsidian plugin: ${plan.isObsidianPlugin ? '✅' : '❌'}`);
|
|
162
|
+
|
|
163
|
+
if (!plan.isObsidianPlugin) {
|
|
164
|
+
console.log(`\n⚠️ Warning: This doesn't appear to be a valid Obsidian plugin`);
|
|
165
|
+
console.log(` Missing manifest.json or invalid structure`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\n📋 Will inject:`);
|
|
169
|
+
console.log(
|
|
170
|
+
` ✅ Local scripts (esbuild.config.ts, utils.ts, env.ts, constants.ts, etc.)`
|
|
171
|
+
);
|
|
172
|
+
console.log(` ✅ Updated package.json scripts`);
|
|
173
|
+
console.log(` ✅ Required dependencies`);
|
|
174
|
+
|
|
175
|
+
if (autoConfirm) {
|
|
176
|
+
console.log(`\n✅ Auto-confirming all file replacements...`);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// No global confirmation needed - file-by-file confirmation will happen in diffAndPromptFiles
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Clean old script files
|
|
186
|
+
*/
|
|
187
|
+
export async function cleanOldScripts(
|
|
188
|
+
scriptsPath: string,
|
|
189
|
+
approvedDests: Set<string>
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const scriptNames = [
|
|
192
|
+
'utils',
|
|
193
|
+
'esbuild.config',
|
|
194
|
+
'acp',
|
|
195
|
+
'update-version',
|
|
196
|
+
'release',
|
|
197
|
+
'help',
|
|
198
|
+
'constants',
|
|
199
|
+
'env',
|
|
200
|
+
'reload',
|
|
201
|
+
'typingsPlugin'
|
|
202
|
+
];
|
|
203
|
+
const extensions = ['.ts', '.mts', '.js', '.mjs'];
|
|
204
|
+
|
|
205
|
+
for (const scriptName of scriptNames) {
|
|
206
|
+
for (const ext of extensions) {
|
|
207
|
+
const scriptFile = path.join(scriptsPath, `${scriptName}${ext}`);
|
|
208
|
+
if (await isValidPath(scriptFile)) {
|
|
209
|
+
if (approvedDests.has(scriptFile)) {
|
|
210
|
+
await unlink(scriptFile);
|
|
211
|
+
console.log(`🗑️ Removed existing ${scriptName}${ext} (will be replaced)`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const obsoleteRootFiles = ['help-plugin.ts'];
|
|
218
|
+
for (const fileName of obsoleteRootFiles) {
|
|
219
|
+
const filePath = path.join(path.dirname(scriptsPath), fileName);
|
|
220
|
+
if (await isValidPath(filePath)) {
|
|
221
|
+
await unlink(filePath);
|
|
222
|
+
console.log(`🗑️ Removed obsolete root file: ${fileName}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const obsoleteFiles = ['start.mjs', 'start.js'];
|
|
227
|
+
for (const fileName of obsoleteFiles) {
|
|
228
|
+
const filePath = path.join(scriptsPath, fileName);
|
|
229
|
+
if (await isValidPath(filePath)) {
|
|
230
|
+
await unlink(filePath);
|
|
231
|
+
console.log(`🗑️ Removed obsolete file: ${fileName}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clean old ESLint config files
|
|
238
|
+
*/
|
|
239
|
+
export async function cleanOldLintFiles(targetPath: string): Promise<void> {
|
|
240
|
+
const oldLintFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintignore'];
|
|
241
|
+
const conflictingLintFiles = [
|
|
242
|
+
'eslint.config.ts',
|
|
243
|
+
'eslint.config.cjs',
|
|
244
|
+
'eslint.config.js',
|
|
245
|
+
'eslint.config.mjs'
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
for (const fileName of oldLintFiles) {
|
|
249
|
+
const filePath = path.join(targetPath, fileName);
|
|
250
|
+
if (await isValidPath(filePath)) {
|
|
251
|
+
await unlink(filePath);
|
|
252
|
+
console.log(
|
|
253
|
+
`🗑️ Removed old ESLint file: ${fileName} (replaced by eslint.config.mts)`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const fileName of conflictingLintFiles) {
|
|
259
|
+
const filePath = path.join(targetPath, fileName);
|
|
260
|
+
if (await isValidPath(filePath)) {
|
|
261
|
+
await unlink(filePath);
|
|
262
|
+
console.log(
|
|
263
|
+
`🗑️ Removed existing ESLint file: ${fileName} (will be replaced by injection)`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface FileEntry {
|
|
270
|
+
src: string; // path relative to configRoot
|
|
271
|
+
dest: string; // absolute path in target plugin
|
|
272
|
+
mergeEnv?: boolean; // special .env merge logic
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Build the full list of files to inject, with source and destination paths
|
|
277
|
+
*/
|
|
278
|
+
function buildFileList(targetPath: string): FileEntry[] {
|
|
279
|
+
const scriptsPath = path.join(targetPath, 'scripts');
|
|
280
|
+
const entries: FileEntry[] = [];
|
|
281
|
+
|
|
282
|
+
// Scripts
|
|
283
|
+
const scriptFiles = [
|
|
284
|
+
'templates/scripts/utils.ts',
|
|
285
|
+
'templates/scripts/esbuild.config.ts',
|
|
286
|
+
'templates/scripts/acp.ts',
|
|
287
|
+
'templates/scripts/update-version.ts',
|
|
288
|
+
'templates/scripts/release.ts',
|
|
289
|
+
'templates/scripts/help.ts',
|
|
290
|
+
'templates/scripts/constants.ts',
|
|
291
|
+
'templates/scripts/env.ts',
|
|
292
|
+
'templates/scripts/reload.ts',
|
|
293
|
+
'templates/scripts/typingsPlugin.ts'
|
|
294
|
+
];
|
|
295
|
+
for (const src of scriptFiles) {
|
|
296
|
+
entries.push({
|
|
297
|
+
src,
|
|
298
|
+
dest: path.join(scriptsPath, path.basename(src))
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Root config files
|
|
303
|
+
const configFileMap: Array<[string, string, boolean?]> = [
|
|
304
|
+
['templates/tsconfig.json.template', 'tsconfig.json'],
|
|
305
|
+
['templates/gitignore.template', '.gitignore'],
|
|
306
|
+
['templates/eslint.config.mts', 'eslint.config.mts'],
|
|
307
|
+
['templates/.editorconfig', '.editorconfig'],
|
|
308
|
+
['templates/.prettierrc', '.prettierrc'],
|
|
309
|
+
['templates/.prettierignore', '.prettierignore'],
|
|
310
|
+
['templates/npmrc.template', '.npmrc'],
|
|
311
|
+
['templates/.gitattributes', '.gitattributes'],
|
|
312
|
+
['templates/env.template', '.env', true]
|
|
313
|
+
];
|
|
314
|
+
for (const [src, destName, mergeEnv] of configFileMap) {
|
|
315
|
+
entries.push({
|
|
316
|
+
src,
|
|
317
|
+
dest: path.join(targetPath, destName),
|
|
318
|
+
mergeEnv: !!mergeEnv
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// VSCode config files
|
|
323
|
+
const configVscodeMap: Array<[string, string]> = [
|
|
324
|
+
['templates/.vscode/settings.json', '.vscode/settings.json'],
|
|
325
|
+
['templates/.vscode/tasks.json', '.vscode/tasks.json'],
|
|
326
|
+
['templates/.vscode/extensions.json', '.vscode/extensions.json']
|
|
327
|
+
];
|
|
328
|
+
for (const [src, destName] of configVscodeMap) {
|
|
329
|
+
entries.push({
|
|
330
|
+
src,
|
|
331
|
+
dest: path.join(targetPath, destName)
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// GitHub workflow files
|
|
336
|
+
const workflowFiles = [
|
|
337
|
+
'templates/.github/workflows/release.yml',
|
|
338
|
+
'templates/.github/workflows/release-body.md'
|
|
339
|
+
];
|
|
340
|
+
for (const src of workflowFiles) {
|
|
341
|
+
entries.push({
|
|
342
|
+
src,
|
|
343
|
+
dest: path.join(targetPath, src.replace('templates/', ''))
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return entries;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Compare source templates with existing target files.
|
|
352
|
+
* Prompt user only when content differs and file already exists.
|
|
353
|
+
* Returns the Set of dest paths approved for injection.
|
|
354
|
+
*/
|
|
355
|
+
export async function diffAndPromptFiles(
|
|
356
|
+
targetPath: string,
|
|
357
|
+
autoConfirm: boolean
|
|
358
|
+
): Promise<Set<string>> {
|
|
359
|
+
const rl = autoConfirm ? null : createReadlineInterface();
|
|
360
|
+
const configRoot = await findPluginConfigRoot();
|
|
361
|
+
const entries = buildFileList(targetPath);
|
|
362
|
+
const approved = new Set<string>();
|
|
363
|
+
|
|
364
|
+
console.log(`\n🔍 Comparing files with existing content...`);
|
|
365
|
+
|
|
366
|
+
let hasChanges = false;
|
|
367
|
+
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
// Skip .env merge (always approved, merge logic handled separately)
|
|
370
|
+
if (entry.mergeEnv) {
|
|
371
|
+
approved.add(entry.dest);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const srcPath = path.join(configRoot, entry.src);
|
|
376
|
+
let srcContent: string;
|
|
377
|
+
try {
|
|
378
|
+
srcContent = await readFile(srcPath, 'utf8');
|
|
379
|
+
} catch {
|
|
380
|
+
// Source doesn't exist, skip
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Target doesn't exist yet → inject without prompting
|
|
385
|
+
if (!(await isValidPath(entry.dest))) {
|
|
386
|
+
approved.add(entry.dest);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Special case: eslint.config.mts - auto-approve if old .eslintrc exists
|
|
391
|
+
if (entry.dest.endsWith('eslint.config.mts')) {
|
|
392
|
+
const oldEslintFiles = [
|
|
393
|
+
'.eslintrc',
|
|
394
|
+
'.eslintrc.js',
|
|
395
|
+
'.eslintrc.json',
|
|
396
|
+
'.eslintrc.cjs'
|
|
397
|
+
];
|
|
398
|
+
let hasOldEslint = false;
|
|
399
|
+
for (const file of oldEslintFiles) {
|
|
400
|
+
if (await isValidPath(path.join(targetPath, file))) {
|
|
401
|
+
hasOldEslint = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (hasOldEslint) {
|
|
406
|
+
console.log(
|
|
407
|
+
` 🔄 ${path.relative(targetPath, entry.dest).replace(/\\/g, '/')} (migrating from old .eslintrc format)`
|
|
408
|
+
);
|
|
409
|
+
approved.add(entry.dest);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const destContent = await readFile(entry.dest, 'utf8');
|
|
415
|
+
|
|
416
|
+
// Identical → skip silently
|
|
417
|
+
if (srcContent === destContent) {
|
|
418
|
+
console.log(
|
|
419
|
+
` ✅ ${path.relative(targetPath, entry.dest).replace(/\\/g, '/')} (unchanged)`
|
|
420
|
+
);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Different → ask user (or auto-approve if autoConfirm)
|
|
425
|
+
hasChanges = true;
|
|
426
|
+
const relDest = path.relative(targetPath, entry.dest);
|
|
427
|
+
|
|
428
|
+
if (autoConfirm) {
|
|
429
|
+
console.log(` ✅ ${relDest.replace(/\\/g, '/')} (will be updated)`);
|
|
430
|
+
approved.add(entry.dest);
|
|
431
|
+
} else {
|
|
432
|
+
const update = await askConfirmation(
|
|
433
|
+
` Update ${relDest.replace(/\\/g, '/')}? (content differs)`,
|
|
434
|
+
rl!
|
|
435
|
+
);
|
|
436
|
+
if (update) {
|
|
437
|
+
approved.add(entry.dest);
|
|
438
|
+
} else {
|
|
439
|
+
console.log(` ⏭️ Kept existing ${relDest.replace(/\\/g, '/')}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!hasChanges) {
|
|
445
|
+
console.log(` ✅ All existing files are up to date`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (rl) rl.close();
|
|
449
|
+
return approved;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Inject scripts and config files
|
|
454
|
+
*/
|
|
455
|
+
export async function injectScripts(
|
|
456
|
+
targetPath: string,
|
|
457
|
+
approvedDests: Set<string>
|
|
458
|
+
): Promise<void> {
|
|
459
|
+
const scriptsPath = path.join(targetPath, 'scripts');
|
|
460
|
+
|
|
461
|
+
if (!(await isValidPath(scriptsPath))) {
|
|
462
|
+
await mkdir(scriptsPath, { recursive: true });
|
|
463
|
+
console.log(`📁 Created scripts directory`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await cleanOldScripts(scriptsPath, approvedDests);
|
|
467
|
+
await cleanOldLintFiles(targetPath);
|
|
468
|
+
|
|
469
|
+
const scriptFiles = [
|
|
470
|
+
'templates/scripts/utils.ts',
|
|
471
|
+
'templates/scripts/esbuild.config.ts',
|
|
472
|
+
'templates/scripts/acp.ts',
|
|
473
|
+
'templates/scripts/update-version.ts',
|
|
474
|
+
'templates/scripts/release.ts',
|
|
475
|
+
'templates/scripts/help.ts',
|
|
476
|
+
'templates/scripts/constants.ts',
|
|
477
|
+
'templates/scripts/env.ts',
|
|
478
|
+
'templates/scripts/reload.ts',
|
|
479
|
+
'templates/scripts/typingsPlugin.ts'
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
// Files that need value-preserving merge instead
|
|
483
|
+
// of full overwrite (user fills in their paths)
|
|
484
|
+
const mergeEnvFile = new Set(['.env']);
|
|
485
|
+
|
|
486
|
+
// Files with .template suffix (NPM excludes dotfiles)
|
|
487
|
+
// Map: { source: targetName }
|
|
488
|
+
const configFileMap: Record<string, string> = {
|
|
489
|
+
'templates/tsconfig.json.template': 'tsconfig.json',
|
|
490
|
+
'templates/gitignore.template': '.gitignore',
|
|
491
|
+
'templates/eslint.config.mts': 'eslint.config.mts',
|
|
492
|
+
'templates/.editorconfig': '.editorconfig',
|
|
493
|
+
'templates/.prettierrc': '.prettierrc',
|
|
494
|
+
'templates/.prettierignore': '.prettierignore',
|
|
495
|
+
'templates/npmrc.template': '.npmrc',
|
|
496
|
+
'templates/.gitattributes': '.gitattributes',
|
|
497
|
+
'templates/env.template': '.env'
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const configVscodeMap: Record<string, string> = {
|
|
501
|
+
'templates/.vscode/settings.json': '.vscode/settings.json',
|
|
502
|
+
'templates/.vscode/tasks.json': '.vscode/tasks.json',
|
|
503
|
+
'templates/.vscode/extensions.json': '.vscode/extensions.json'
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const workflowFiles = [
|
|
507
|
+
'templates/.github/workflows/release.yml',
|
|
508
|
+
'templates/.github/workflows/release-body.md'
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
console.log(`\n📥 Copying scripts from local files...`);
|
|
512
|
+
|
|
513
|
+
for (const scriptFile of scriptFiles) {
|
|
514
|
+
try {
|
|
515
|
+
const fileName = path.basename(scriptFile);
|
|
516
|
+
const targetFile = path.join(scriptsPath, fileName);
|
|
517
|
+
if (!approvedDests.has(targetFile)) {
|
|
518
|
+
console.log(` ⏭️ Skipped ${fileName} (kept existing)`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const content = await copyFromLocal(scriptFile);
|
|
522
|
+
await writeFile(targetFile, content, 'utf8');
|
|
523
|
+
console.log(` ✅ ${fileName}`);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error(` ❌ Failed to inject ${scriptFile}: ${error}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
console.log(`\n📥 Copying config files...`);
|
|
530
|
+
|
|
531
|
+
// Copy root config files
|
|
532
|
+
for (const [src, destName] of Object.entries(configFileMap)) {
|
|
533
|
+
// Skip if not approved by diff step
|
|
534
|
+
const targetFile = path.join(targetPath, destName);
|
|
535
|
+
if (!approvedDests.has(targetFile)) {
|
|
536
|
+
continue; // already logged during diff step
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const templateContent = await copyFromLocal(src);
|
|
541
|
+
|
|
542
|
+
// For .env: merge existing values into the template
|
|
543
|
+
if (mergeEnvFile.has(destName) && (await isValidPath(targetFile))) {
|
|
544
|
+
const existing = await readFile(targetFile, 'utf8');
|
|
545
|
+
const existingVals: Record<string, string> = {};
|
|
546
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
547
|
+
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
548
|
+
if (m) existingVals[m[1].trim()] = m[2].trim();
|
|
549
|
+
}
|
|
550
|
+
const merged = templateContent
|
|
551
|
+
.split(/\r?\n/)
|
|
552
|
+
.map((line) => {
|
|
553
|
+
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
554
|
+
if (m) {
|
|
555
|
+
const key = m[1].trim();
|
|
556
|
+
const val = existingVals[key] ?? m[2].trim();
|
|
557
|
+
return `${key}=${val}`;
|
|
558
|
+
}
|
|
559
|
+
return line;
|
|
560
|
+
})
|
|
561
|
+
.join('\n');
|
|
562
|
+
await writeFile(targetFile, merged, 'utf8');
|
|
563
|
+
console.log(` ✅ ${destName} (values preserved)`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
await writeFile(targetFile, templateContent, 'utf8');
|
|
568
|
+
console.log(` ✅ ${destName}`);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error(` ❌ Failed to inject ${destName}: ${error}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Copy .vscode config files
|
|
575
|
+
for (const [src, destName] of Object.entries(configVscodeMap)) {
|
|
576
|
+
try {
|
|
577
|
+
const targetFile = path.join(targetPath, destName);
|
|
578
|
+
if (!approvedDests.has(targetFile)) continue;
|
|
579
|
+
const content = await copyFromLocal(src);
|
|
580
|
+
const targetDir = path.dirname(targetFile);
|
|
581
|
+
if (!(await isValidPath(targetDir))) {
|
|
582
|
+
await mkdir(targetDir, { recursive: true });
|
|
583
|
+
}
|
|
584
|
+
await writeFile(targetFile, content, 'utf8');
|
|
585
|
+
console.log(` ✅ ${destName}`);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error(` ❌ Failed to inject ${destName}: ${error}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
console.log(`\n📥 Copying GitHub workflows from local files...`);
|
|
592
|
+
for (const workflowFile of workflowFiles) {
|
|
593
|
+
try {
|
|
594
|
+
const content = await copyFromLocal(workflowFile);
|
|
595
|
+
const relativePath = workflowFile.replace('templates/', '');
|
|
596
|
+
const targetFile = path.join(targetPath, relativePath);
|
|
597
|
+
if (!approvedDests.has(targetFile)) continue;
|
|
598
|
+
const targetDir = path.dirname(targetFile);
|
|
599
|
+
if (!(await isValidPath(targetDir))) {
|
|
600
|
+
await mkdir(targetDir, { recursive: true });
|
|
601
|
+
}
|
|
602
|
+
await writeFile(targetFile, content, 'utf8');
|
|
603
|
+
console.log(` ✅ ${relativePath}`);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error(` ❌ Failed to inject ${workflowFile}: ${error}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Update package.json with autonomous configuration
|
|
612
|
+
*/
|
|
613
|
+
export async function updatePackageJson(targetPath: string): Promise<void> {
|
|
614
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
615
|
+
|
|
616
|
+
if (!(await isValidPath(packageJsonPath))) {
|
|
617
|
+
console.log(`❌ No package.json found, skipping package.json update`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
623
|
+
|
|
624
|
+
const configRoot = await findPluginConfigRoot();
|
|
625
|
+
const templatePkg = JSON.parse(
|
|
626
|
+
await readFile(path.join(configRoot, 'templates/package.json.template'), 'utf8')
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const obsoleteScripts = ['version'];
|
|
630
|
+
for (const script of obsoleteScripts) {
|
|
631
|
+
if (packageJson.scripts?.[script]) {
|
|
632
|
+
console.log(` 🧹 Removing obsolete script: "${script}"`);
|
|
633
|
+
delete packageJson.scripts[script];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
packageJson.scripts = {
|
|
638
|
+
...packageJson.scripts,
|
|
639
|
+
...templatePkg.scripts
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
if (!packageJson.devDependencies) packageJson.devDependencies = {};
|
|
643
|
+
|
|
644
|
+
const requiredDeps: Record<string, string> = templatePkg.devDependencies;
|
|
645
|
+
|
|
646
|
+
let addedDeps = 0;
|
|
647
|
+
let updatedDeps = 0;
|
|
648
|
+
for (const [dep, version] of Object.entries(requiredDeps)) {
|
|
649
|
+
if (!packageJson.devDependencies[dep]) {
|
|
650
|
+
packageJson.devDependencies[dep] = version as string;
|
|
651
|
+
addedDeps++;
|
|
652
|
+
} else if (packageJson.devDependencies[dep] !== version) {
|
|
653
|
+
packageJson.devDependencies[dep] = version as string;
|
|
654
|
+
updatedDeps++;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!packageJson.engines) packageJson.engines = {};
|
|
659
|
+
packageJson.engines.npm = templatePkg.engines.npm;
|
|
660
|
+
packageJson.engines.yarn = templatePkg.engines.yarn;
|
|
661
|
+
packageJson.type = templatePkg.type;
|
|
662
|
+
|
|
663
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
|
|
664
|
+
console.log(
|
|
665
|
+
` ✅ Updated package.json (${addedDeps} new, ${updatedDeps} updated dependencies)`
|
|
666
|
+
);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error(` ❌ Failed to update package.json: ${error}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Create required directories
|
|
674
|
+
*/
|
|
675
|
+
export async function createRequiredDirectories(targetPath: string): Promise<void> {
|
|
676
|
+
const directories = [path.join(targetPath, '.github', 'workflows')];
|
|
677
|
+
for (const dir of directories) {
|
|
678
|
+
if (!(await isValidPath(dir))) {
|
|
679
|
+
await mkdir(dir, { recursive: true });
|
|
680
|
+
console.log(` 📁 Created ${path.relative(targetPath, dir).replace(/\\/g, '/')}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Create injection info file
|
|
687
|
+
*/
|
|
688
|
+
export async function createInjectionInfo(targetPath: string): Promise<void> {
|
|
689
|
+
const configRoot = await findPluginConfigRoot();
|
|
690
|
+
const configPackageJsonPath = path.join(configRoot, 'package.json');
|
|
691
|
+
let injectorVersion = 'unknown';
|
|
692
|
+
try {
|
|
693
|
+
const configPackageJson = JSON.parse(await readFile(configPackageJsonPath, 'utf8'));
|
|
694
|
+
injectorVersion = configPackageJson.version || 'unknown';
|
|
695
|
+
} catch {
|
|
696
|
+
console.warn('Warning: Could not read injector version');
|
|
697
|
+
}
|
|
698
|
+
const injectionInfo = {
|
|
699
|
+
injectorVersion,
|
|
700
|
+
injectionDate: new Date().toISOString(),
|
|
701
|
+
injectorName: 'obsidian-plugin-config'
|
|
702
|
+
};
|
|
703
|
+
const infoPath = path.join(targetPath, '.injection-info.json');
|
|
704
|
+
await writeFile(infoPath, JSON.stringify(injectionInfo, null, 2));
|
|
705
|
+
console.log(` ✅ Created injection info file (.injection-info.json)`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Read injection info from target plugin
|
|
710
|
+
*/
|
|
711
|
+
export async function readInjectionInfo(
|
|
712
|
+
targetPath: string
|
|
713
|
+
): Promise<Record<string, string> | null> {
|
|
714
|
+
const infoPath = path.join(targetPath, '.injection-info.json');
|
|
715
|
+
if (!(await isValidPath(infoPath))) return null;
|
|
716
|
+
try {
|
|
717
|
+
return JSON.parse(await readFile(infoPath, 'utf8'));
|
|
718
|
+
} catch {
|
|
719
|
+
console.warn('Warning: Could not parse .injection-info.json');
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Clean NPM/Yarn lock files and node_modules to ensure fresh install
|
|
726
|
+
*/
|
|
727
|
+
export async function cleanNpmArtifactsIfNeeded(targetPath: string): Promise<void> {
|
|
728
|
+
const packageLockPath = path.join(targetPath, 'package-lock.json');
|
|
729
|
+
const yarnLockPath = path.join(targetPath, 'yarn.lock');
|
|
730
|
+
const nodeModulesPath = path.join(targetPath, 'node_modules');
|
|
731
|
+
|
|
732
|
+
const hasPackageLock = await isValidPath(packageLockPath);
|
|
733
|
+
const hasYarnLock = await isValidPath(yarnLockPath);
|
|
734
|
+
|
|
735
|
+
if (hasPackageLock) {
|
|
736
|
+
console.log(`\n🧹 Cleaning NPM artifacts (migrating to Yarn)...`);
|
|
737
|
+
try {
|
|
738
|
+
if (await isValidPath(nodeModulesPath)) {
|
|
739
|
+
console.log(` ⏳ Removing node_modules (this may take a moment)...`);
|
|
740
|
+
try {
|
|
741
|
+
await rm(nodeModulesPath, { recursive: true, force: true });
|
|
742
|
+
} catch {
|
|
743
|
+
// ignore
|
|
744
|
+
}
|
|
745
|
+
if (await isValidPath(nodeModulesPath)) {
|
|
746
|
+
const timestamp = Date.now();
|
|
747
|
+
const oldPath = `${nodeModulesPath}.old.${timestamp}`;
|
|
748
|
+
try {
|
|
749
|
+
await rename(nodeModulesPath, oldPath);
|
|
750
|
+
console.log(` 🔄 Renamed locked node_modules to ${path.basename(oldPath)}`);
|
|
751
|
+
console.log(` 💡 Delete it manually later: ${oldPath}`);
|
|
752
|
+
} catch {
|
|
753
|
+
console.log(
|
|
754
|
+
` ⚠️ Could not remove/rename node_modules (locked by processes)`
|
|
755
|
+
);
|
|
756
|
+
console.log(` 💡 Close Obsidian/VSCode and run: obsidian-inject again`);
|
|
757
|
+
throw new Error('node_modules locked - close processes and retry');
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
console.log(` 🗑️ Removed node_modules (will be reinstalled with Yarn)`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (hasPackageLock) {
|
|
764
|
+
await unlink(packageLockPath);
|
|
765
|
+
console.log(` 🗑️ Removed package-lock.json`);
|
|
766
|
+
}
|
|
767
|
+
if (hasYarnLock) {
|
|
768
|
+
await unlink(yarnLockPath);
|
|
769
|
+
console.log(` 🗑️ Removed yarn.lock`);
|
|
770
|
+
}
|
|
771
|
+
console.log(` ✅ Lock files and artifacts cleaned for fresh install`);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (error instanceof Error && error.message.includes('locked')) throw error;
|
|
774
|
+
console.error(` ❌ Failed to clean artifacts: ${error}`);
|
|
775
|
+
console.log(
|
|
776
|
+
` 💡 You may need to manually remove package-lock.json, yarn.lock and node_modules`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Check if tsx is installed locally and install it if needed
|
|
784
|
+
*/
|
|
785
|
+
export async function ensureTsxInstalled(targetPath: string): Promise<void> {
|
|
786
|
+
console.log(`\n🔍 Checking tsx installation...`);
|
|
787
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
788
|
+
try {
|
|
789
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
790
|
+
const devDependencies = packageJson.devDependencies || {};
|
|
791
|
+
const dependencies = packageJson.dependencies || {};
|
|
792
|
+
if (devDependencies.tsx || dependencies.tsx) {
|
|
793
|
+
console.log(` ✅ tsx is already installed`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
console.log(` ⚠️ tsx not found, installing as dev dependency...`);
|
|
797
|
+
gitExec('yarn add -D tsx', targetPath);
|
|
798
|
+
console.log(` ✅ tsx installed successfully`);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
console.error(` ❌ Failed to install tsx: ${error}`);
|
|
801
|
+
console.log(` 💡 You may need to install tsx manually: yarn add -D tsx`);
|
|
802
|
+
throw new Error('tsx installation failed');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Run yarn install in target directory
|
|
808
|
+
*/
|
|
809
|
+
export async function runYarnInstall(targetPath: string): Promise<void> {
|
|
810
|
+
console.log(`\n📦 Installing dependencies...`);
|
|
811
|
+
try {
|
|
812
|
+
gitExec('yarn install', targetPath);
|
|
813
|
+
console.log(` ✅ Dependencies installed successfully`);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
console.error(` ❌ Failed to install dependencies: ${error}`);
|
|
816
|
+
console.log(
|
|
817
|
+
` 💡 You may need to run 'yarn install' manually in the target directory`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Main injection orchestration function
|
|
824
|
+
*/
|
|
825
|
+
export async function performInjection(
|
|
826
|
+
targetPath: string,
|
|
827
|
+
autoConfirm: boolean = false
|
|
828
|
+
): Promise<void> {
|
|
829
|
+
console.log(`\n🚀 Starting injection process...`);
|
|
830
|
+
try {
|
|
831
|
+
const approvedDests = await diffAndPromptFiles(targetPath, autoConfirm);
|
|
832
|
+
await cleanNpmArtifactsIfNeeded(targetPath);
|
|
833
|
+
await ensureTsxInstalled(targetPath);
|
|
834
|
+
await injectScripts(targetPath, approvedDests);
|
|
835
|
+
|
|
836
|
+
console.log(`\n📦 Updating package.json...`);
|
|
837
|
+
await updatePackageJson(targetPath);
|
|
838
|
+
|
|
839
|
+
console.log(`\n📁 Creating required directories...`);
|
|
840
|
+
await createRequiredDirectories(targetPath);
|
|
841
|
+
|
|
842
|
+
await runYarnInstall(targetPath);
|
|
843
|
+
|
|
844
|
+
console.log(`\n📝 Creating injection info...`);
|
|
845
|
+
await createInjectionInfo(targetPath);
|
|
846
|
+
|
|
847
|
+
console.log(`\n✅ Injection completed successfully!`);
|
|
848
|
+
console.log(`\n📋 Next steps:`);
|
|
849
|
+
console.log(` 1. cd ${targetPath}`);
|
|
850
|
+
console.log(` 2. yarn build # Test the build`);
|
|
851
|
+
console.log(` 3. yarn start # Test development mode`);
|
|
852
|
+
console.log(` 4. yarn acp # Commit changes (or yarn bacp for build+commit)`);
|
|
853
|
+
|
|
854
|
+
const allEntries = await readdir(targetPath);
|
|
855
|
+
const oldDirs = allEntries
|
|
856
|
+
.filter((name) => name.startsWith('node_modules.old.'))
|
|
857
|
+
.map((name) => path.basename(name));
|
|
858
|
+
|
|
859
|
+
if (oldDirs.length > 0) {
|
|
860
|
+
console.log(`\n🧹 Cleanup reminder:`);
|
|
861
|
+
for (const oldDir of oldDirs) {
|
|
862
|
+
console.log(` 🗑️ Delete manually: ${oldDir}`);
|
|
863
|
+
}
|
|
864
|
+
console.log(` 💡 Close all processes first, then delete these folders`);
|
|
865
|
+
}
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error(`\n❌ Injection failed: ${error}`);
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
}
|