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