@ovipakla/gm-cli 2.1.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GMFileWatcher.js CHANGED
@@ -1,367 +1,367 @@
1
- import childProcess from 'child_process';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import chokidar from 'chokidar';
5
- import { readFile } from 'fs/promises';
6
-
7
- /* ---------------------------------------------------------
8
- * GMModule
9
- * --------------------------------------------------------- */
10
- class GMModule {
11
- constructor(dir, version, hook = defaultWatcherHook()) {
12
- this.dir = dir;
13
- this.version = version;
14
-
15
- this.scriptWatcher = this.createWatcher("src", hook);
16
- this.testWatcher = this.createWatcher("test", hook);
17
- this.shaderWatcher = this.createWatcher("resource/shader", hook);
18
-
19
- this.objectWatchers = this.findTopFolders(path.join(dir, "resource/object"))
20
- .map(entry => ({
21
- ...entry,
22
- watcher: chokidar.watch(entry.dir, hook)
23
- }));
24
-
25
- this.sceneWatchers = this.findTopFolders(path.join(dir, "resource/scene"))
26
- .map(entry => ({
27
- ...entry,
28
- watcher: chokidar.watch(entry.dir, hook)
29
- }));
30
-
31
- console.log(`🔍️ Watching [${dir}] for changes...`);
32
- }
33
-
34
- createWatcher(subdir, hook) {
35
- return chokidar.watch(path.join(this.dir, subdir), hook);
36
- }
37
-
38
- findTopFolders(dir, results = []) {
39
- if (!fs.existsSync(dir)) return results;
40
-
41
- const entries = fs.readdirSync(dir, { withFileTypes: true });
42
- const gmlFiles = entries
43
- .filter(e => e.isFile() && e.name.endsWith(".gml"))
44
- .map(e => e.name);
45
-
46
- if (gmlFiles.length > 0) {
47
- results.push({
48
- name: path.basename(dir),
49
- dir: dir.replace(/^\.\//, ""),
50
- files: gmlFiles,
51
- });
52
- return results;
53
- }
54
-
55
- entries
56
- .filter(e => e.isDirectory())
57
- .forEach(subdir => this.findTopFolders(path.join(dir, subdir.name), results));
58
-
59
- return results;
60
- }
61
- }
62
-
63
- function defaultWatcherHook() {
64
- return {
65
- ignored: /[\/\\]\./,
66
- persistent: true,
67
- ignoreInitial: true,
68
- depth: 99,
69
- };
70
- }
71
-
72
- function syncWatcherHook() {
73
- return {
74
- ignored: /[\/\\]\./,
75
- persistent: false,
76
- ignoreInitial: true,
77
- depth: 99,
78
- };
79
- }
80
-
81
- /* ---------------------------------------------------------
82
- * GMFileWatcher
83
- * --------------------------------------------------------- */
84
- class GMFileWatcher {
85
- constructor(gmPackage, modulesDir, watch = false) {
86
- this.gmPath = path.dirname(resolvePath(gmPackage.main));
87
- this.modulesDirName = modulesDir;
88
- this.modulesDirPath = resolvePath(modulesDir);
89
- this.timestamp = "";
90
-
91
- const dependencyEntries = Object.entries(gmPackage.dependencies);
92
-
93
- this.modules = watch
94
- ? this.initializeWatchedModules(dependencyEntries)
95
- : this.initializeSyncModules(dependencyEntries);
96
- }
97
-
98
- initializeWatchedModules(dependencies) {
99
- return dependencies.map(([name, version]) => {
100
- const gmModule = this.parseModule(name, version, true);
101
- this.syncModuleFiles(name, gmModule.dir, gmModule.objectWatchers, gmModule.sceneWatchers);
102
- return gmModule;
103
- });
104
- }
105
-
106
- initializeSyncModules(dependencies) {
107
- dependencies.forEach(([name, version]) => {
108
- const gmModule = this.parseModule(name, version, false);
109
- this.syncModuleFiles(name, gmModule.dir, gmModule.objectWatchers, gmModule.sceneWatchers);
110
- });
111
- return [];
112
- }
113
-
114
- /* ---------------------------------------------
115
- * Sync helpers
116
- * --------------------------------------------- */
117
- syncModuleFiles(name, moduleDir, objectWatchers = [], sceneWatchers = []) {
118
- const src = path.join(moduleDir, 'src');
119
- const test = path.join(moduleDir, 'test');
120
- const shader = path.join(moduleDir, 'resource/shader');
121
-
122
- this.syncFiles(src, this.gmPath, `${name}/src`, 'scripts', 'gml');
123
- this.syncFiles(test, this.gmPath, `${name}/test`, 'scripts', 'gml');
124
- this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'scripts', 'gml');
125
- this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'shaders', 'fsh');
126
- this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'shaders', 'vsh');
127
-
128
- objectWatchers?.forEach(entry => {
129
- this.syncFiles(
130
- entry.dir,
131
- path.join(this.gmPath, 'objects', entry.name),
132
- `${name}/resource/object/../${entry.name}`,
133
- `objects/${entry.name}`,
134
- 'gml',
135
- '/.*',
136
- true
137
- );
138
- });
139
-
140
- sceneWatchers?.forEach(entry => {
141
- this.syncFiles(
142
- entry.dir,
143
- path.join(this.gmPath, 'rooms', entry.name),
144
- `${name}/resource/scene/${entry.name}`,
145
- `rooms/${entry.name}`,
146
- 'gml',
147
- '/.*',
148
- true
149
- );
150
- });
151
- }
152
-
153
- syncFiles = function (source, target, pkgName, gmFolderName, extension, suffix = '/../*.', isObject = false) {
154
- childProcess.exec(`find ${source} -iname "*.${extension}"`, (err, stdout) => {
155
- const loc = stdout.split('\n')
156
- .filter(Boolean)
157
- .map(f => this.readFileMetadata(f, extension))
158
- .map(script => this.writeIfChanged(script, extension, target, gmFolderName, isObject))
159
- .reduce(sumLineChanges, { before: 0, after: 0 });
160
-
161
- const timestamp = new Intl.DateTimeFormat('pl-PL', {
162
- year: 'numeric',
163
- month: '2-digit',
164
- day: '2-digit',
165
- hour: '2-digit',
166
- minute: '2-digit',
167
- second: '2-digit',
168
- hour12: false,
169
- }).format(new Date()).replaceAll(',', '');
170
-
171
- if (timestamp !== this.timestamp) {
172
- this.timestamp = timestamp
173
- console.log(`⌚ ${timestamp}`)
174
- }
175
-
176
- logSyncSummary(pkgName, suffix, extension, loc);
177
- });
178
- }
179
-
180
- /* ---------------------------------------------
181
- * Watcher handlers
182
- * --------------------------------------------- */
183
- parseModule(name, version, watch = true) {
184
- const gmModule = new GMModule(resolvePath(path.join(this.modulesDirName, name)), version, watch
185
- ? defaultWatcherHook()
186
- : syncWatcherHook());
187
-
188
- const moduleFilter = (p, module) =>
189
- path.normalize(p).includes(path.normalize(module.dir));
190
-
191
- gmModule.scriptWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
192
- gmModule.testWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
193
- gmModule.shaderWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
194
-
195
- gmModule.objectWatchers.forEach(entry => {
196
- entry.watcher.on('change', () => this.objectHook(entry));
197
- });
198
-
199
- gmModule.sceneWatchers.forEach(entry => {
200
- entry.watcher.on('change', () => this.sceneHook(entry));
201
- });
202
-
203
- return gmModule;
204
- }
205
-
206
- async hook(p) {
207
- const pkgName = extractPkgName(p, this.modulesDirName);
208
- const source = path.join(this.modulesDirPath, pkgName);
209
-
210
- this.syncModuleFiles(pkgName, source);
211
- }
212
-
213
- async objectHook(entry) {
214
- this.syncFiles(
215
- entry.dir,
216
- path.join(this.gmPath, 'objects', entry.name),
217
- `${this.modulesDirName}/resource/object/../${entry.name}`,
218
- `objects/${entry.name}`,
219
- 'gml',
220
- '/.*',
221
- true
222
- );
223
- }
224
-
225
- async sceneHook(entry) {
226
- this.syncFiles(
227
- entry.dir,
228
- path.join(this.gmPath, 'rooms', entry.name),
229
- `${this.modulesDirName}/resource/scene/${entry.name}`,
230
- `rooms/${entry.name}`,
231
- 'gml',
232
- '/.*',
233
- true
234
- );
235
- }
236
-
237
- readFileMetadata(filePath, extension) {
238
- return {
239
- name: path.basename(filePath).replace(`.${extension}`, ''),
240
- dir: path.normalize(filePath.replaceAll('./', '')),
241
- content: fs.readFileSync(path.normalize(filePath), 'utf8'),
242
- };
243
- }
244
-
245
- writeIfChanged(script, extension, target, gmFolderName, isObject) {
246
- const diffStats = (oldLines, newLines) => {
247
- const m = oldLines.length;
248
- const n = newLines.length;
249
-
250
- // tablica LCS
251
- const dp = Array.from({ length: m + 1 }, () =>
252
- Array(n + 1).fill(0)
253
- );
254
-
255
- for (let i = 1; i <= m; i++) {
256
- for (let j = 1; j <= n; j++) {
257
- if (oldLines[i - 1] === newLines[j - 1]) {
258
- dp[i][j] = dp[i - 1][j - 1] + 1;
259
- } else {
260
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
261
- }
262
- }
263
- }
264
-
265
- const same = dp[m][n];
266
- const removed = m - same;
267
- const added = n - same;
268
- const total = same + added
269
-
270
- return { added, removed, total };
271
- }
272
-
273
- const removeZeroFields = (obj) => {
274
- return Object.fromEntries(
275
- Object.entries(obj).filter(([_, value]) => value !== 0)
276
- );
277
- }
278
-
279
- const dst = isObject
280
- ? path.join(target, `${script.name}.${extension}`)
281
- : path.join(target, gmFolderName, `${script.name}/${script.name}.${extension}`);
282
-
283
- if (!fs.existsSync(dst)) {
284
- const objName = isObject ? gmFolderName.split("/").slice(-1).pop() : undefined
285
- const dstYY = isObject
286
- ? path.join(target, `${objName}.yy`)
287
- : path.join(target, gmFolderName, `${script.name}/${script.name}.yy`);
288
- if (!fs.existsSync(dstYY)) {
289
- console.log(`⚠️ File ${dstYY} does not exists, cannot sync.`)
290
- return {
291
- before: 0,
292
- after: 0,
293
- }
294
- }
295
-
296
- fs.writeFileSync(dst, "");
297
- }
298
-
299
- const existing = fs.readFileSync(dst, 'utf8');
300
- if (existing !== script.content) {
301
- const result = removeZeroFields(diffStats(existing.split('\n'), script.content.split('\n')))
302
- console.log(`📝 Save ${script.name}.${extension}:`, result);
303
- fs.writeFileSync(dst, script.content, { encoding: 'utf8', flag: 'w' });
304
- }
305
-
306
- return {
307
- before: existing.split('\n').length,
308
- after: script.content.split('\n').length,
309
- };
310
- }
311
- }
312
-
313
- /* ---------------------------------------------------------
314
- * Helpers (pure functions)
315
- * --------------------------------------------------------- */
316
- function resolvePath(p) {
317
- return path.join(process.cwd(), path.normalize(p));
318
- }
319
-
320
- function extractPkgName(file, modulesDir) {
321
- return file
322
- .split(modulesDir)[1]
323
- .replaceAll('\\', '/')
324
- .split('/')
325
- .filter(Boolean)[0];
326
- }
327
-
328
- function sumLineChanges(acc, cur) {
329
- acc.before += cur.before;
330
- acc.after += cur.after;
331
- return acc;
332
- }
333
-
334
- function logSyncSummary(pkgName, suffix, extension, loc) {
335
- const newLines = loc.after - loc.before;
336
- if (newLines !== 0) {
337
- console.log(`🚀 [${pkgName}${suffix}${extension}]:`, { diff: newLines, total: loc.after } );
338
- }
339
- }
340
-
341
- /* ---------------------------------------------------------
342
- * Public API
343
- * --------------------------------------------------------- */
344
- export async function watch(gmPackagePath, modulesDir = 'gm_modules') {
345
- try {
346
- return new GMFileWatcher(
347
- JSON.parse(await readFile(gmPackagePath, 'utf8')),
348
- modulesDir,
349
- true
350
- );
351
- } catch (e) {
352
- console.error(`❌ Unable to parse package-gm.json at: ${gmPackagePath}\n${e.message}`);
353
- console.error(e.stack);
354
- }
355
- }
356
-
357
- export async function sync(gmPackagePath, modulesDir = 'gm_modules') {
358
- try {
359
- return new GMFileWatcher(
360
- JSON.parse(await readFile(gmPackagePath, 'utf8')),
361
- modulesDir
362
- );
363
- } catch (e) {
364
- console.error(`❌ Unable to parse package-gm.json at: ${gmPackagePath}\n${e.message}`);
365
- console.error(e.stack);
366
- }
367
- }
1
+ import childProcess from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import chokidar from 'chokidar';
5
+ import { readFile } from 'fs/promises';
6
+
7
+ /* ---------------------------------------------------------
8
+ * GMModule
9
+ * --------------------------------------------------------- */
10
+ class GMModule {
11
+ constructor(dir, version, hook = defaultWatcherHook()) {
12
+ this.dir = dir;
13
+ this.version = version;
14
+
15
+ this.scriptWatcher = this.createWatcher("src", hook);
16
+ this.testWatcher = this.createWatcher("test", hook);
17
+ this.shaderWatcher = this.createWatcher("resource/shader", hook);
18
+
19
+ this.objectWatchers = this.findTopFolders(path.join(dir, "resource/object"))
20
+ .map(entry => ({
21
+ ...entry,
22
+ watcher: chokidar.watch(entry.dir, hook)
23
+ }));
24
+
25
+ this.sceneWatchers = this.findTopFolders(path.join(dir, "resource/scene"))
26
+ .map(entry => ({
27
+ ...entry,
28
+ watcher: chokidar.watch(entry.dir, hook)
29
+ }));
30
+
31
+ console.log(`🔍️ Watching [${dir}] for changes...`);
32
+ }
33
+
34
+ createWatcher(subdir, hook) {
35
+ return chokidar.watch(path.join(this.dir, subdir), hook);
36
+ }
37
+
38
+ findTopFolders(dir, results = []) {
39
+ if (!fs.existsSync(dir)) return results;
40
+
41
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ const gmlFiles = entries
43
+ .filter(e => e.isFile() && e.name.endsWith(".gml"))
44
+ .map(e => e.name);
45
+
46
+ if (gmlFiles.length > 0) {
47
+ results.push({
48
+ name: path.basename(dir),
49
+ dir: dir.replace(/^\.\//, ""),
50
+ files: gmlFiles,
51
+ });
52
+ return results;
53
+ }
54
+
55
+ entries
56
+ .filter(e => e.isDirectory())
57
+ .forEach(subdir => this.findTopFolders(path.join(dir, subdir.name), results));
58
+
59
+ return results;
60
+ }
61
+ }
62
+
63
+ function defaultWatcherHook() {
64
+ return {
65
+ ignored: /[\/\\]\./,
66
+ persistent: true,
67
+ ignoreInitial: true,
68
+ depth: 99,
69
+ };
70
+ }
71
+
72
+ function syncWatcherHook() {
73
+ return {
74
+ ignored: /[\/\\]\./,
75
+ persistent: false,
76
+ ignoreInitial: true,
77
+ depth: 99,
78
+ };
79
+ }
80
+
81
+ /* ---------------------------------------------------------
82
+ * GMFileWatcher
83
+ * --------------------------------------------------------- */
84
+ class GMFileWatcher {
85
+ constructor(gmPackage, modulesDir, watch = false) {
86
+ this.gmPath = path.dirname(resolvePath(gmPackage.main));
87
+ this.modulesDirName = modulesDir;
88
+ this.modulesDirPath = resolvePath(modulesDir);
89
+ this.timestamp = "";
90
+
91
+ const dependencyEntries = Object.entries(gmPackage.dependencies);
92
+
93
+ this.modules = watch
94
+ ? this.initializeWatchedModules(dependencyEntries)
95
+ : this.initializeSyncModules(dependencyEntries);
96
+ }
97
+
98
+ initializeWatchedModules(dependencies) {
99
+ return dependencies.map(([name, version]) => {
100
+ const gmModule = this.parseModule(name, version, true);
101
+ this.syncModuleFiles(name, gmModule.dir, gmModule.objectWatchers, gmModule.sceneWatchers);
102
+ return gmModule;
103
+ });
104
+ }
105
+
106
+ initializeSyncModules(dependencies) {
107
+ dependencies.forEach(([name, version]) => {
108
+ const gmModule = this.parseModule(name, version, false);
109
+ this.syncModuleFiles(name, gmModule.dir, gmModule.objectWatchers, gmModule.sceneWatchers);
110
+ });
111
+ return [];
112
+ }
113
+
114
+ /* ---------------------------------------------
115
+ * Sync helpers
116
+ * --------------------------------------------- */
117
+ syncModuleFiles(name, moduleDir, objectWatchers = [], sceneWatchers = []) {
118
+ const src = path.join(moduleDir, 'src');
119
+ const test = path.join(moduleDir, 'test');
120
+ const shader = path.join(moduleDir, 'resource/shader');
121
+
122
+ this.syncFiles(src, this.gmPath, `${name}/src`, 'scripts', 'gml');
123
+ this.syncFiles(test, this.gmPath, `${name}/test`, 'scripts', 'gml');
124
+ this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'scripts', 'gml');
125
+ this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'shaders', 'fsh');
126
+ this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'shaders', 'vsh');
127
+
128
+ objectWatchers?.forEach(entry => {
129
+ this.syncFiles(
130
+ entry.dir,
131
+ path.join(this.gmPath, 'objects', entry.name),
132
+ `${name}/resource/object/../${entry.name}`,
133
+ `objects/${entry.name}`,
134
+ 'gml',
135
+ '/.*',
136
+ true
137
+ );
138
+ });
139
+
140
+ sceneWatchers?.forEach(entry => {
141
+ this.syncFiles(
142
+ entry.dir,
143
+ path.join(this.gmPath, 'rooms', entry.name),
144
+ `${name}/resource/scene/${entry.name}`,
145
+ `rooms/${entry.name}`,
146
+ 'gml',
147
+ '/.*',
148
+ true
149
+ );
150
+ });
151
+ }
152
+
153
+ syncFiles = function (source, target, pkgName, gmFolderName, extension, suffix = '/../*.', isObject = false) {
154
+ childProcess.exec(`find ${source} -iname "*.${extension}"`, (err, stdout) => {
155
+ const loc = stdout.split('\n')
156
+ .filter(Boolean)
157
+ .map(f => this.readFileMetadata(f, extension))
158
+ .map(script => this.writeIfChanged(script, extension, target, gmFolderName, isObject))
159
+ .reduce(sumLineChanges, { before: 0, after: 0 });
160
+
161
+ const timestamp = new Intl.DateTimeFormat('pl-PL', {
162
+ year: 'numeric',
163
+ month: '2-digit',
164
+ day: '2-digit',
165
+ hour: '2-digit',
166
+ minute: '2-digit',
167
+ second: '2-digit',
168
+ hour12: false,
169
+ }).format(new Date()).replaceAll(',', '');
170
+
171
+ if (timestamp !== this.timestamp) {
172
+ this.timestamp = timestamp
173
+ console.log(`⌚ ${timestamp}`)
174
+ }
175
+
176
+ logSyncSummary(pkgName, suffix, extension, loc);
177
+ });
178
+ }
179
+
180
+ /* ---------------------------------------------
181
+ * Watcher handlers
182
+ * --------------------------------------------- */
183
+ parseModule(name, version, watch = true) {
184
+ const gmModule = new GMModule(resolvePath(path.join(this.modulesDirName, name)), version, watch
185
+ ? defaultWatcherHook()
186
+ : syncWatcherHook());
187
+
188
+ const moduleFilter = (p, module) =>
189
+ path.normalize(p).includes(path.normalize(module.dir));
190
+
191
+ gmModule.scriptWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
192
+ gmModule.testWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
193
+ gmModule.shaderWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
194
+
195
+ gmModule.objectWatchers.forEach(entry => {
196
+ entry.watcher.on('change', () => this.objectHook(entry));
197
+ });
198
+
199
+ gmModule.sceneWatchers.forEach(entry => {
200
+ entry.watcher.on('change', () => this.sceneHook(entry));
201
+ });
202
+
203
+ return gmModule;
204
+ }
205
+
206
+ async hook(p) {
207
+ const pkgName = extractPkgName(p, this.modulesDirName);
208
+ const source = path.join(this.modulesDirPath, pkgName);
209
+
210
+ this.syncModuleFiles(pkgName, source);
211
+ }
212
+
213
+ async objectHook(entry) {
214
+ this.syncFiles(
215
+ entry.dir,
216
+ path.join(this.gmPath, 'objects', entry.name),
217
+ `${this.modulesDirName}/resource/object/../${entry.name}`,
218
+ `objects/${entry.name}`,
219
+ 'gml',
220
+ '/.*',
221
+ true
222
+ );
223
+ }
224
+
225
+ async sceneHook(entry) {
226
+ this.syncFiles(
227
+ entry.dir,
228
+ path.join(this.gmPath, 'rooms', entry.name),
229
+ `${this.modulesDirName}/resource/scene/${entry.name}`,
230
+ `rooms/${entry.name}`,
231
+ 'gml',
232
+ '/.*',
233
+ true
234
+ );
235
+ }
236
+
237
+ readFileMetadata(filePath, extension) {
238
+ return {
239
+ name: path.basename(filePath).replace(`.${extension}`, ''),
240
+ dir: path.normalize(filePath.replaceAll('./', '')),
241
+ content: fs.readFileSync(path.normalize(filePath), 'utf8'),
242
+ };
243
+ }
244
+
245
+ writeIfChanged(script, extension, target, gmFolderName, isObject) {
246
+ const diffStats = (oldLines, newLines) => {
247
+ const m = oldLines.length;
248
+ const n = newLines.length;
249
+
250
+ // tablica LCS
251
+ const dp = Array.from({ length: m + 1 }, () =>
252
+ Array(n + 1).fill(0)
253
+ );
254
+
255
+ for (let i = 1; i <= m; i++) {
256
+ for (let j = 1; j <= n; j++) {
257
+ if (oldLines[i - 1] === newLines[j - 1]) {
258
+ dp[i][j] = dp[i - 1][j - 1] + 1;
259
+ } else {
260
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
261
+ }
262
+ }
263
+ }
264
+
265
+ const same = dp[m][n];
266
+ const removed = m - same;
267
+ const added = n - same;
268
+ const total = same + added
269
+
270
+ return { added, removed, total };
271
+ }
272
+
273
+ const removeZeroFields = (obj) => {
274
+ return Object.fromEntries(
275
+ Object.entries(obj).filter(([_, value]) => value !== 0)
276
+ );
277
+ }
278
+
279
+ const dst = isObject
280
+ ? path.join(target, `${script.name}.${extension}`)
281
+ : path.join(target, gmFolderName, `${script.name}/${script.name}.${extension}`);
282
+
283
+ if (!fs.existsSync(dst)) {
284
+ const objName = isObject ? gmFolderName.split("/").slice(-1).pop() : undefined
285
+ const dstYY = isObject
286
+ ? path.join(target, `${objName}.yy`)
287
+ : path.join(target, gmFolderName, `${script.name}/${script.name}.yy`);
288
+ if (!fs.existsSync(dstYY)) {
289
+ console.log(`⚠️ File ${dstYY} does not exists, cannot sync.`)
290
+ return {
291
+ before: 0,
292
+ after: 0,
293
+ }
294
+ }
295
+
296
+ fs.writeFileSync(dst, "");
297
+ }
298
+
299
+ const existing = fs.readFileSync(dst, 'utf8');
300
+ if (existing !== script.content) {
301
+ const result = removeZeroFields(diffStats(existing.split('\n'), script.content.split('\n')))
302
+ console.log(`📝 Save ${script.name}.${extension}:`, result);
303
+ fs.writeFileSync(dst, script.content, { encoding: 'utf8', flag: 'w' });
304
+ }
305
+
306
+ return {
307
+ before: existing.split('\n').length,
308
+ after: script.content.split('\n').length,
309
+ };
310
+ }
311
+ }
312
+
313
+ /* ---------------------------------------------------------
314
+ * Helpers (pure functions)
315
+ * --------------------------------------------------------- */
316
+ function resolvePath(p) {
317
+ return path.join(process.cwd(), path.normalize(p));
318
+ }
319
+
320
+ function extractPkgName(file, modulesDir) {
321
+ return file
322
+ .split(modulesDir)[1]
323
+ .replaceAll('\\', '/')
324
+ .split('/')
325
+ .filter(Boolean)[0];
326
+ }
327
+
328
+ function sumLineChanges(acc, cur) {
329
+ acc.before += cur.before;
330
+ acc.after += cur.after;
331
+ return acc;
332
+ }
333
+
334
+ function logSyncSummary(pkgName, suffix, extension, loc) {
335
+ const newLines = loc.after - loc.before;
336
+ if (newLines !== 0) {
337
+ console.log(`🚀 [${pkgName}${suffix}${extension}]:`, { diff: newLines, total: loc.after } );
338
+ }
339
+ }
340
+
341
+ /* ---------------------------------------------------------
342
+ * Public API
343
+ * --------------------------------------------------------- */
344
+ export async function watch(gmPackagePath, modulesDir = 'gm_modules') {
345
+ try {
346
+ return new GMFileWatcher(
347
+ JSON.parse(await readFile(gmPackagePath, 'utf8')),
348
+ modulesDir,
349
+ true
350
+ );
351
+ } catch (e) {
352
+ console.error(`❌ Unable to parse package-gm.json at: ${gmPackagePath}\n${e.message}`);
353
+ console.error(e.stack);
354
+ }
355
+ }
356
+
357
+ export async function sync(gmPackagePath, modulesDir = 'gm_modules') {
358
+ try {
359
+ return new GMFileWatcher(
360
+ JSON.parse(await readFile(gmPackagePath, 'utf8')),
361
+ modulesDir
362
+ );
363
+ } catch (e) {
364
+ console.error(`❌ Unable to parse package-gm.json at: ${gmPackagePath}\n${e.message}`);
365
+ console.error(e.stack);
366
+ }
367
+ }