@ovipakla/gm-cli 1.0.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.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ ### Description
11
+
12
+ A clear and concise description of what the bug is.
13
+
14
+ ---
15
+ ### Reproducing steps
16
+
17
+ Steps to reproduce the behavior:
18
+ 1. Go to `...`
19
+ 2. Click on `...`
20
+ 3. See error `...`
21
+
22
+ ---
23
+ ### Expected behavior
24
+
25
+ A clear and concise description of what you expected to happen.
26
+
27
+ ---
28
+ ### Environment
29
+
30
+ - Version: `vYYMMDD`
31
+ - Target: `win-yyp` | `wasm`
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ ### Description
11
+
12
+ What is the issue/feature? Why is it needed?
13
+
14
+ ---
15
+ ### Requirements
16
+
17
+ What is the expected behavior or outcome?
18
+
19
+ ---
20
+ ### Test case scenario
21
+
22
+ How will it be tested or validated?
@@ -0,0 +1,353 @@
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
+ /* ---------------------------------------------------------
73
+ * GMFileWatcher
74
+ * --------------------------------------------------------- */
75
+ class GMFileWatcher {
76
+ constructor(gmPackage, modulesDir, watch = false) {
77
+ this.gmPath = resolvePath(gmPackage.main);
78
+ this.modulesDirName = modulesDir;
79
+ this.modulesDirPath = resolvePath(modulesDir);
80
+ this.timestamp = "";
81
+
82
+ const dependencyEntries = Object.entries(gmPackage.dependencies);
83
+
84
+ this.modules = watch
85
+ ? this.initializeWatchedModules(dependencyEntries)
86
+ : this.initializeSyncModules(dependencyEntries);
87
+ }
88
+
89
+ initializeWatchedModules(dependencies) {
90
+ return dependencies.map(([name, version]) => {
91
+ const gmModule = this.parseModule(name, version);
92
+ this.syncModuleFiles(name, gmModule.dir, gmModule.objectWatchers, gmModule.sceneWatchers);
93
+ return gmModule;
94
+ });
95
+ }
96
+
97
+ initializeSyncModules(dependencies) {
98
+ dependencies.forEach(([name]) => {
99
+ const modulePath = path.join(this.modulesDirPath, name);
100
+ this.syncModuleFiles(name, modulePath);
101
+ });
102
+ return [];
103
+ }
104
+
105
+ /* ---------------------------------------------
106
+ * Sync helpers
107
+ * --------------------------------------------- */
108
+ syncModuleFiles(name, moduleDir, objectWatchers = [], sceneWatchers = []) {
109
+ const src = path.join(moduleDir, 'src');
110
+ const test = path.join(moduleDir, 'test');
111
+ const shader = path.join(moduleDir, 'resource/shader');
112
+
113
+ this.syncFiles(src, this.gmPath, `${name}/src`, 'scripts', 'gml');
114
+ this.syncFiles(test, this.gmPath, `${name}/test`, 'scripts', 'gml');
115
+ this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'scripts', 'gml');
116
+ this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'shaders', 'fsh');
117
+ this.syncFiles(shader, this.gmPath, `${name}/resource/shader`, 'shaders', 'vsh');
118
+
119
+ objectWatchers?.forEach(entry => {
120
+ this.syncFiles(
121
+ entry.dir,
122
+ path.join(this.gmPath, 'objects', entry.name),
123
+ `${name}/resource/object/../${entry.name}`,
124
+ `objects/${entry.name}`,
125
+ 'gml',
126
+ '/.*',
127
+ true
128
+ );
129
+ });
130
+
131
+ sceneWatchers?.forEach(entry => {
132
+ this.syncFiles(
133
+ entry.dir,
134
+ path.join(this.gmPath, 'rooms', entry.name),
135
+ `${name}/resource/scene/${entry.name}`,
136
+ `rooms/${entry.name}`,
137
+ 'gml',
138
+ '/.*',
139
+ true
140
+ );
141
+ });
142
+ }
143
+
144
+ syncFiles = function (source, target, pkgName, gmFolderName, extension, suffix = '/../*.', isObject = false) {
145
+ childProcess.exec(`find ${source} -iname "*.${extension}"`, (err, stdout) => {
146
+ const loc = stdout.split('\n')
147
+ .filter(Boolean)
148
+ .map(f => this.readFileMetadata(f, extension))
149
+ .map(script => this.writeIfChanged(script, extension, target, gmFolderName, isObject))
150
+ .reduce(sumLineChanges, { before: 0, after: 0 });
151
+
152
+ const timestamp = new Intl.DateTimeFormat('pl-PL', {
153
+ year: 'numeric',
154
+ month: '2-digit',
155
+ day: '2-digit',
156
+ hour: '2-digit',
157
+ minute: '2-digit',
158
+ second: '2-digit',
159
+ hour12: false,
160
+ }).format(new Date()).replaceAll(',', '');
161
+
162
+ if (timestamp !== this.timestamp) {
163
+ this.timestamp = timestamp
164
+ console.log(`⌚ ${timestamp}`)
165
+ }
166
+
167
+ logSyncSummary(pkgName, suffix, extension, loc);
168
+ });
169
+ }
170
+
171
+ /* ---------------------------------------------
172
+ * Watcher handlers
173
+ * --------------------------------------------- */
174
+ parseModule(name, version) {
175
+ const gmModule = new GMModule(resolvePath(path.join(this.modulesDirName, name)), version);
176
+
177
+ const moduleFilter = (p, module) =>
178
+ path.normalize(p).includes(path.normalize(module.dir));
179
+
180
+ gmModule.scriptWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
181
+ gmModule.testWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
182
+ gmModule.shaderWatcher.on('change', p => this.modules.filter(m => moduleFilter(p, m)).forEach(() => this.hook(p)));
183
+
184
+ gmModule.objectWatchers.forEach(entry => {
185
+ entry.watcher.on('change', () => this.objectHook(entry));
186
+ });
187
+
188
+ gmModule.sceneWatchers.forEach(entry => {
189
+ entry.watcher.on('change', () => this.sceneHook(entry));
190
+ });
191
+
192
+ return gmModule;
193
+ }
194
+
195
+ async hook(p) {
196
+ const pkgName = extractPkgName(p, this.modulesDirName);
197
+ const source = path.join(this.modulesDirPath, pkgName);
198
+
199
+ this.syncModuleFiles(pkgName, source);
200
+ }
201
+
202
+ async objectHook(entry) {
203
+ this.syncFiles(
204
+ entry.dir,
205
+ path.join(this.gmPath, 'objects', entry.name),
206
+ `${this.modulesDirName}/resource/object/../${entry.name}`,
207
+ `objects/${entry.name}`,
208
+ 'gml',
209
+ '/.*',
210
+ true
211
+ );
212
+ }
213
+
214
+ async sceneHook(entry) {
215
+ this.syncFiles(
216
+ entry.dir,
217
+ path.join(this.gmPath, 'rooms', entry.name),
218
+ `${this.modulesDirName}/resource/scene/${entry.name}`,
219
+ `rooms/${entry.name}`,
220
+ 'gml',
221
+ '/.*',
222
+ true
223
+ );
224
+ }
225
+
226
+ readFileMetadata(filePath, extension) {
227
+ return {
228
+ name: path.basename(filePath).replace(`.${extension}`, ''),
229
+ dir: path.normalize(filePath.replaceAll('./', '')),
230
+ content: fs.readFileSync(path.normalize(filePath), 'utf8'),
231
+ };
232
+ }
233
+
234
+ writeIfChanged(script, extension, target, gmFolderName, isObject) {
235
+ const diffStats = (oldLines, newLines) => {
236
+ const m = oldLines.length;
237
+ const n = newLines.length;
238
+
239
+ // tablica LCS
240
+ const dp = Array.from({ length: m + 1 }, () =>
241
+ Array(n + 1).fill(0)
242
+ );
243
+
244
+ for (let i = 1; i <= m; i++) {
245
+ for (let j = 1; j <= n; j++) {
246
+ if (oldLines[i - 1] === newLines[j - 1]) {
247
+ dp[i][j] = dp[i - 1][j - 1] + 1;
248
+ } else {
249
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
250
+ }
251
+ }
252
+ }
253
+
254
+ const same = dp[m][n];
255
+ const removed = m - same;
256
+ const added = n - same;
257
+ const total = same + added
258
+
259
+ return { added, removed, total };
260
+ }
261
+
262
+ const removeZeroFields = (obj) => {
263
+ return Object.fromEntries(
264
+ Object.entries(obj).filter(([_, value]) => value !== 0)
265
+ );
266
+ }
267
+
268
+ const dst = isObject
269
+ ? path.join(target, `${script.name}.${extension}`)
270
+ : path.join(target, gmFolderName, `${script.name}/${script.name}.${extension}`);
271
+
272
+ if (!fs.existsSync(dst)) {
273
+ const dstYY = path.join(target, gmFolderName, `${script.name}/${script.name}.yy`);
274
+ if (!fs.existsSync(dstYY)) {
275
+ console.log(`⚠️ File ${dstYY} does not exists, cannot sync.`)
276
+ return {
277
+ before: 0,
278
+ after: 0,
279
+ }
280
+ }
281
+
282
+ fs.writeFileSync(dst, "");
283
+ }
284
+
285
+ const existing = fs.readFileSync(dst, 'utf8');
286
+ if (existing !== script.content) {
287
+ const result = removeZeroFields(diffStats(existing.split('\n'), script.content.split('\n')))
288
+ console.log(`➡️ Save ${script.name}.${extension}:`, result);
289
+ fs.writeFileSync(dst, script.content, { encoding: 'utf8', flag: 'w' });
290
+ }
291
+
292
+ return {
293
+ before: existing.split('\n').length,
294
+ after: script.content.split('\n').length,
295
+ };
296
+ }
297
+ }
298
+
299
+ /* ---------------------------------------------------------
300
+ * Helpers (pure functions)
301
+ * --------------------------------------------------------- */
302
+ function resolvePath(p) {
303
+ return path.join(process.cwd(), path.normalize(p));
304
+ }
305
+
306
+ function extractPkgName(file, modulesDir) {
307
+ return file
308
+ .split(modulesDir)[1]
309
+ .replaceAll('\\', '/')
310
+ .split('/')
311
+ .filter(Boolean)[0];
312
+ }
313
+
314
+ function sumLineChanges(acc, cur) {
315
+ acc.before += cur.before;
316
+ acc.after += cur.after;
317
+ return acc;
318
+ }
319
+
320
+ function logSyncSummary(pkgName, suffix, extension, loc) {
321
+ const newLines = loc.after - loc.before;
322
+ if (newLines !== 0) {
323
+ console.log(`🚀 [${pkgName}${suffix}${extension}]:`, { diff: newLines, total: loc.after } );
324
+ }
325
+ }
326
+
327
+ /* ---------------------------------------------------------
328
+ * Public API
329
+ * --------------------------------------------------------- */
330
+ export async function watch(gmPackagePath, modulesDir = 'gm_modules') {
331
+ try {
332
+ return new GMFileWatcher(
333
+ JSON.parse(await readFile(gmPackagePath, 'utf8')),
334
+ modulesDir,
335
+ true
336
+ );
337
+ } catch (e) {
338
+ console.error(`❌ Unable to parse package-gm.json at: ${gmPackagePath}\n${e.message}`);
339
+ console.error(e.stack);
340
+ }
341
+ }
342
+
343
+ export async function sync(gmPackagePath, modulesDir = 'gm_modules') {
344
+ try {
345
+ return new GMFileWatcher(
346
+ JSON.parse(await readFile(gmPackagePath, 'utf8')),
347
+ modulesDir
348
+ );
349
+ } catch (e) {
350
+ console.error(`❌ Unable to parse package-gm.json at: ${gmPackagePath}\n${e.message}`);
351
+ console.error(e.stack);
352
+ }
353
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Alkapivo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # gm-cli
2
+ Gamemaker CLI toolkit. Watch &amp; sync gml sources with yyp project.
3
+
4
+ # Requirements
5
+ - [Node.js](https://nodejs.org) 18.16^
6
+ - [find](https://en.wikipedia.org/wiki/Find_(Unix))
7
+
8
+ # Install
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ # Usage
14
+ ```bash
15
+ gm-cli watch
16
+ ```
17
+
18
+ # Project structure
19
+ ```
20
+ - gm_modules:
21
+ - core: git repo with gml `*.gml` files
22
+ - visu: git repo with gml `*.gml` files
23
+ - yyp:
24
+ - datafiles: directory created by gamemaker
25
+ - extensions: directory created by gamemaker
26
+ - fonts: directory created by gamemaker
27
+ - objects: directory created by gamemaker
28
+ - options: directory created by gamemaker
29
+ - rooms: directory created by gamemaker
30
+ - scripts: directory created by gamemaker
31
+ - shaders: directory created by gamemaker
32
+ - sounds: directory created by gamemaker
33
+ - sprites: directory created by gamemaker
34
+ - game.resource_order: file created by gamemaker
35
+ - game.yyp: file created by gamemaker
36
+ - package-gm.json: Equivalent of `npm` "package.json"
37
+ ```
38
+ Content of `package-gm.json`:
39
+ ```json
40
+ {
41
+ "name": "visu",
42
+ "version": "1.0.0",
43
+ "description": "Visu",
44
+ "main": "yyp",
45
+ "author": "Alkapivo",
46
+ "license": "ISC",
47
+ "dependencies": {
48
+ "core": "^1.0.0",
49
+ "visu": "^1.0.0"
50
+ }
51
+ }
52
+ ```
53
+ Note:
54
+ - `main` is a relative directory path, where `*.yyp` file (gamemaker studio 2.3 project).
55
+ - `dependencies` - keys should match names in `gm_modules` folder
package/app.js ADDED
@@ -0,0 +1,397 @@
1
+ #! /usr/bin/env node
2
+
3
+ import { watch, sync } from './GMFileWatcher.js';
4
+ import path from 'path';
5
+ import { program } from 'commander';
6
+ import { spawn, execSync } from 'child_process';
7
+ import fs from 'fs';
8
+ import readline from 'readline';
9
+
10
+
11
+ program.version('26.02.14', '-v, --version, ', 'output the current version');
12
+ program.command('init')
13
+ .description('CLI creator for package-gm.json')
14
+ .action(async () => {
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout
18
+ });
19
+ const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
20
+
21
+ try {
22
+ console.log("This utility will walk you through creating a package-gm.json file.");
23
+ console.log("It only covers the most common items, and tries to guess sensible defaults.");
24
+
25
+ const projectPath = process.cwd();
26
+ const basename = path.basename(projectPath);
27
+ const version = "1.0.0";
28
+ const propertyPackage = await askQuestion(`package name: (${basename}) `);
29
+ const propertyVersion = await askQuestion(`version: (${version}) `);
30
+ const propertyDescription = await askQuestion('description: ');
31
+ const propertyGamemaker = await askQuestion('gamemaker project path: ');
32
+ const propertyTest = await askQuestion('test command: ');
33
+ const propertyGit = await askQuestion('git repository: ');
34
+ const propertyKeywords = await askQuestion('keywords: ');
35
+ const propertyAuthor = await askQuestion('author: ');
36
+ const propertyLicense = await askQuestion('license: (ISC) ');
37
+ const data = {
38
+ package: propertyPackage === null || propertyPackage === '' ? basename : propertyPackage,
39
+ version: propertyVersion === null || propertyVersion === '' ? version : propertyVersion,
40
+ description: propertyDescription,
41
+ main: propertyGamemaker,
42
+ test: propertyTest,
43
+ git: propertyGit,
44
+ keywords: propertyKeywords,
45
+ author: propertyAuthor,
46
+ license: propertyLicense,
47
+ scripts: {},
48
+ dependencies: {},
49
+ };
50
+
51
+ const filePath = path.join(projectPath, 'package-gm.json');
52
+ const dataString = JSON.stringify(data, null, 2);
53
+
54
+ console.log(`About to write to ${filePath}:\n\n${dataString}\n\n`);
55
+ const response = await askQuestion(`Is this OK? (yes) `)
56
+ if (typeof response === 'string' && (response.includes('y') || response.includes('Y'))) {
57
+ fs.writeFileSync(filePath, dataString, 'utf8');
58
+ } else {
59
+ console.log('Aborted.\n');
60
+ }
61
+ } catch (error) {
62
+ console.error('An error occurred:', error);
63
+ } finally {
64
+ rl.close();
65
+ process.exit(0);
66
+ }
67
+ });
68
+ program.command('watch')
69
+ .description('Watch modules dir and copy code to gamemaker project')
70
+ .action(() => {
71
+ watch(path.normalize(path.join(process.cwd(), 'package-gm.json')))
72
+ });
73
+ program.command('sync')
74
+ .description('Copy code from modules dir to gamemaker project')
75
+ .action(() => {
76
+ sync(path.normalize(path.join(process.cwd(), 'package-gm.json')))
77
+ });
78
+ program.command('install')
79
+ .description('Install dependencies listed in package-gm.json to gm_modules folder')
80
+ .action(function() {
81
+ const packageJsonPath = 'package-gm.json';
82
+ const modulesDir = 'gm_modules';
83
+
84
+ if (!fs.existsSync(modulesDir)) {
85
+ fs.mkdirSync(modulesDir);
86
+ }
87
+
88
+ const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
89
+ const dependencies = packageData.dependencies;
90
+ Object.entries(dependencies).forEach(([key, dependency]) => {
91
+ const modulePath = path.join(modulesDir, key);
92
+ if (fs.existsSync(modulePath)) {
93
+ try {
94
+ execSync('git rev-parse --is-inside-work-tree', { cwd: modulePath, stdio: 'ignore' });
95
+ console.log(`Syncing ${modulePath} to revision ${dependency.revision}`);
96
+ execSync('git reset --hard HEAD', { cwd: modulePath, stdio: 'inherit' });
97
+ execSync('git clean -fdx -e', { cwd: modulePath, stdio: 'inherit' });
98
+ execSync(`git checkout ${dependency.revision}`, { cwd: modulePath, stdio: 'inherit' });
99
+ } catch (error) {
100
+ console.log(`Removing ${modulePath} because it's not a git repository`);
101
+ fs.rmSync(modulePath, { recursive: true, force: true });
102
+ console.log(`Initializing ${modulePath} to revision ${dependency.revision}`);
103
+ execSync(`git clone ${dependency.remote} ${modulePath}`, { stdio: 'inherit' });
104
+ execSync(`git checkout ${dependency.revision}`, { cwd: modulePath, stdio: 'inherit' });
105
+ }
106
+ } else {
107
+ console.log(`Initializing ${modulePath} to revision ${dependency.revision}`);
108
+ execSync(`git clone ${dependency.remote} ${modulePath}`, { stdio: 'inherit' });
109
+ execSync(`git checkout ${dependency.revision}`, { cwd: modulePath, stdio: 'inherit' });
110
+ }
111
+ });
112
+
113
+ console.log('All dependencies processed.');
114
+ process.exit(0);
115
+ })
116
+ program.command('run')
117
+ .description('Run the script named <foo>')
118
+ .argument('<foo>', 'script name')
119
+ .action((foo) => {
120
+ if (typeof foo !== 'string') {
121
+ console.log(`missing argument`);
122
+ console.log(`Exited with code 1`);
123
+ return process.exit(1);
124
+ }
125
+
126
+ const packageJsonPath = 'package-gm.json';
127
+ const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
128
+ const scriptData = packageData.scripts[foo]
129
+ if (typeof scriptData !== 'string') {
130
+ console.log(`script ${foo} wasn't found`);
131
+ console.log(`Exited with code 1`);
132
+ return process.exit(1);
133
+ }
134
+
135
+ const shellScript = `#!/bin/bash
136
+ ${scriptData}
137
+ `;
138
+ const bashProcess = spawn("bash", ["-s"], { stdio: ["pipe", "inherit", "inherit"] });
139
+ bashProcess.stdin.write(shellScript);
140
+ bashProcess.stdin.end();
141
+ bashProcess.on("exit", (code) => {
142
+ console.log(`Exited with code ${code}`);
143
+ process.exit(code);
144
+ });
145
+ });
146
+ program.command('generate')
147
+ .description('Generate *.yyp IncludedFiles section')
148
+ .action(function() {
149
+ function getFilesRecursively(dir, root) {
150
+ let files = [];
151
+ for (const entry of fs.readdirSync(dir)) {
152
+ const fullPath = path.join(dir, entry).replaceAll("\\", "/");
153
+ if (fs.statSync(fullPath).isDirectory()) {
154
+ files = files.concat(getFilesRecursively(fullPath, root));
155
+ } else {
156
+ const filePath = `datafiles${(fullPath.startsWith(root) ? fullPath.slice(root.length) : fullPath)}`.replaceAll(`/${entry}`, '');
157
+ const line = `{"$GMIncludedFile":"","%Name":"${entry}","CopyToMask":-1,"filePath":"${filePath}","name":"${entry}","resourceType":"GMIncludedFile","resourceVersion":"2.0",},`;
158
+ files.push(line);
159
+ }
160
+ }
161
+ return files;
162
+ }
163
+
164
+ function findFileUpwardsSync(filename = "gm-cli.env", maxLevels = 99) {
165
+ let currentDir = process.cwd();
166
+ for (let i = 0; i < maxLevels; i++) {
167
+ const candidate = path.join(currentDir, filename);
168
+ if (fs.existsSync(candidate)) {
169
+ return candidate;
170
+ }
171
+
172
+ const parentDir = path.dirname(currentDir);
173
+ if (parentDir === currentDir) {
174
+ break;
175
+ }
176
+
177
+ currentDir = parentDir;
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ function parseEnvFile(filePath) {
184
+ const content = fs.readFileSync(filePath, "utf8");
185
+ const result = new Map();
186
+ content.split(/\r?\n/).forEach(line => {
187
+ line = line.trim();
188
+ if (!line || line.startsWith("#")) {
189
+ return;
190
+ }
191
+
192
+ const match = line.match(/^([^=]+)="(.*)"$/);
193
+ if (match) {
194
+ const [, key, value] = match;
195
+ result.set(key.trim(), value);
196
+ }
197
+ });
198
+
199
+ return result;
200
+ }
201
+
202
+ const envFile = findFileUpwardsSync();
203
+ if (envFile === null) {
204
+ console.error('gm-cli.env was not found')
205
+ return
206
+ }
207
+
208
+ const envPath = path.dirname(envFile).replaceAll("\\", "/");
209
+ const envMap = parseEnvFile(envFile);
210
+ if (!envMap.has("GMS_PROJECT_PATH")) {
211
+ console.error(`GMS_PROJECT_PATH was not defined in ${envFile}`)
212
+ return
213
+ }
214
+
215
+ if (!envMap.has("GMS_PROJECT_NAME")) {
216
+ console.error(`GMS_PROJECT_NAME was not defined in ${envFile}`)
217
+ return
218
+ }
219
+
220
+ const projectPath = path.join(envPath, envMap.get("GMS_PROJECT_PATH")).replaceAll("\\", "/");
221
+ const yypPath = path.join(projectPath, `${envMap.get("GMS_PROJECT_NAME")}.yyp`).replaceAll("\\", "/");
222
+ const yypOldPath = path.join(projectPath, `${envMap.get("GMS_PROJECT_NAME")}.yyp.old`).replaceAll("\\", "/");
223
+ const yyp = fs.readFileSync(yypPath, "utf8");
224
+ fs.copyFileSync(yypPath, yypOldPath);
225
+
226
+ const datafilesPath = path.join(projectPath, "datafiles").replaceAll("\\", "/")
227
+ const datafiles = getFilesRecursively(datafilesPath, datafilesPath)
228
+ const replaced = yyp.replace(/"IncludedFiles"\s*:\s*\[(.*?)\]/s, `"IncludedFiles":[
229
+ ${datafiles.join("\n ")}
230
+ ]`);
231
+ fs.writeFileSync(yypPath, replaced, "utf8");
232
+ });
233
+ program.command('make')
234
+ .description('Build and run gamemaker project')
235
+ .option('-t, --target <target>', 'available targets: windows')
236
+ .option('-r, --runtime <type>', 'use VM or YYC runtime')
237
+ .option('-n, --name <name>', 'The actual file name of the ZIP file that is created')
238
+ .option('-l, --launch', 'launch the executable after building')
239
+ .option('-c, --clean', 'make clean build')
240
+ .action(function() {
241
+ const targetMap = new Map([ [ 'windows', 'win' ] ])
242
+ const options = this.opts();
243
+ const config = {
244
+ runtime: '$GMS_RUNTIME',
245
+ target: '$GMS_TARGET',
246
+ targetExt: 'win',
247
+ clean: 'false',
248
+ launch: 'PackageZip',
249
+ name: '$GMS_PROJECT_NAME',
250
+ };
251
+
252
+ if (options.runtime !== undefined) {
253
+ config.runtime = options.runtime;
254
+ }
255
+
256
+ if (options.target !== undefined && targetMap.has(options.target)) {
257
+ config.target = options.target;
258
+ config.targetExt = targetMap.get(config.target);
259
+ }
260
+
261
+ if (options.clean !== undefined) {
262
+ config.clean = 'true';
263
+ }
264
+
265
+ if (options.launch !== undefined) {
266
+ config.launch = 'Run';
267
+ }
268
+
269
+ if (options.name !== undefined && typeof options.name === 'string' && options.name.trim() !== '') {
270
+ config.name = options.name;
271
+ }
272
+
273
+ const shellScript = `#!/bin/bash
274
+ function log_info {
275
+ local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
276
+ echo -e "$timestamp INFO [gm-cli::run] $1"
277
+ }
278
+
279
+ function log_error {
280
+ local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
281
+ echo -e "$timestamp ERROR [gm-cli::run] $1"
282
+ }
283
+
284
+ gm_cli_env_path=""
285
+ dir=$(realpath "$PWD")
286
+ while [ "$dir" != "/" ]; do
287
+ if [ -f "$dir/gm-cli.env" ]; then
288
+ gm_cli_env_path="$dir/gm-cli.env"
289
+ log_info "Load configuration '$gm_cli_env_path'"
290
+ set -a
291
+ . "$gm_cli_env_path"
292
+ set +a
293
+ break
294
+ fi
295
+ dir=$(dirname "$dir")
296
+ done
297
+
298
+ igor_path=$GMS_IGOR_PATH
299
+ if [ -z "$igor_path" ]; then
300
+ log_error "GMS_IGOR_PATH must be defined! exit 1"
301
+ exit 1
302
+ fi
303
+
304
+ project_name=$GMS_PROJECT_NAME
305
+ if [ -z "$project_name" ]; then
306
+ log_error "GMS_PROJECT_NAME must be defined! exit 1"
307
+ exit 1
308
+ fi
309
+
310
+ project_path=$GMS_PROJECT_PATH
311
+ if [ -z "$project_path" ]; then
312
+ log_error "GMS_PROJECT_PATH must be defined! exit 1"
313
+ exit 1
314
+ fi
315
+
316
+ project_path=$(dirname "$gm_cli_env_path")/$project_path
317
+ project_path=$(realpath $project_path)
318
+
319
+ user_path=$GMS_USER_PATH
320
+ if [ -z "$user_path" ]; then
321
+ log_error "GMS_USER_PATH must be defined! exit 1"
322
+ exit 1
323
+ fi
324
+ user_path=$(realpath $user_path)
325
+
326
+ runtime_path=$GMS_RUNTIME_PATH
327
+ if [ -z "$runtime_path" ]; then
328
+ log_error "GMS_RUNTIME_PATH must be defined! exit 1"
329
+ exit 1
330
+ fi
331
+ runtime_path=$(realpath $runtime_path)
332
+
333
+ runtime=${config.runtime}
334
+ if [ -z "$runtime" ]; then
335
+ log_error "GMS_RUNTIME must be defined! exit 1"
336
+ exit 1
337
+ fi
338
+
339
+ target=${config.target}
340
+ if [ -z "$target" ]; then
341
+ log_error "GMS_TARGET must be defined! exit 1"
342
+ exit 1
343
+ fi
344
+
345
+ target_ext=${config.targetExt}
346
+ if [ -z "$target_ext" ]; then
347
+ log_error "GMS_TARGET_EXT must be defined! exit 1"
348
+ exit 1
349
+ fi
350
+
351
+ zip_name=${config.name}
352
+ echo $zip_name
353
+ if [ -z "$zip_name" ]; then
354
+ log_error "--name must be defined! exit 1"
355
+ exit 1
356
+ fi
357
+
358
+ clean=${config.clean}
359
+ if [ "$clean" = "true" ]; then
360
+ log_info "Clean '$project_path/tmp/igor'"
361
+ rm -rf $project_path/tmp/igor
362
+
363
+ log_info "Execute shell command:\n$igor_path \\ \n --runtimePath="$runtime_path" \\ \n --runtime=$runtime \\ \n --project="$\{project_path\}/$\{project_name\}.yyp" \\ \n -- $target Clean\n"
364
+ $igor_path \
365
+ --runtimePath="$runtime_path" \
366
+ --runtime=$runtime \
367
+ --project="$\{project_path\}/$\{project_name\}.yyp" \
368
+ -- $target Clean
369
+ fi
370
+
371
+ log_info "Clean '$\{project_path\}/tmp/igor/out'"
372
+ rm -rf $\{project_path\}/tmp/igor/out
373
+
374
+ log_info "Execute shell command:\n$igor_path \\ \n --project="$\{project_path\}/$\{project_name\}.yyp" \\ \n --user="$user_path" \\ \n --runtimePath="$runtime_path" \\ \n --runtime=$runtime \\ \n --cache="$\{project_path\}/tmp/igor/cache" \\ \n --temp="$\{project_path\}/tmp/igor/temp" \\ \n --of="$\{project_path\}/tmp/igor/out/$\{project_name\}.win" \\ \n --tf="$\{zip_name\}.zip" \\ \n -- $target ${config.launch}"
375
+ $igor_path \
376
+ --project="$\{project_path\}/$\{project_name\}.yyp" \
377
+ --user="$user_path" \
378
+ --runtimePath="$runtime_path" \
379
+ --runtime=$runtime \
380
+ --cache="$\{project_path\}/tmp/igor/cache" \
381
+ --temp="$\{project_path\}/tmp/igor/temp" \
382
+ --of="$\{project_path\}/tmp/igor/out/$\{project_name\}.win" \
383
+ --tf="$\{zip_name\}.zip" \
384
+ -- $target ${config.launch};
385
+
386
+ exit 0
387
+ `;
388
+
389
+ const bashProcess = spawn("bash", ["-s"], { stdio: ["pipe", "inherit", "inherit"] });
390
+ bashProcess.stdin.write(shellScript);
391
+ bashProcess.stdin.end();
392
+ bashProcess.on("exit", (code) => {
393
+ console.log(`Exited with code ${code}`);
394
+ process.exit(code);
395
+ });
396
+ });
397
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@ovipakla/gm-cli",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.0",
7
+ "description": "Gamemaker CLI toolkit. Watch &amp; sync gml sources with yyp project.",
8
+ "main": "app.js",
9
+ "scripts": {},
10
+ "author": "Alkapivo",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "chalk": "^5.3.0",
14
+ "chokidar": "^3.5.3",
15
+ "commander": "^11.1.0",
16
+ "conf": "^12.0.0"
17
+ },
18
+ "bin": {
19
+ "gm-cli": "app.js"
20
+ },
21
+ "type": "module"
22
+ }