@ovipakla/gm-cli 2.1.1 → 2.1.3

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.
Files changed (2) hide show
  1. package/app.js +870 -854
  2. package/package.json +1 -1
package/app.js CHANGED
@@ -1,855 +1,871 @@
1
1
  #! /usr/bin/env node
2
-
3
- import { watch, sync } from './GMFileWatcher.js';
4
- import path from 'node:path';
5
- import { Command } from 'commander';
6
- import { spawn, execSync } from 'child_process';
7
- import fs from 'fs';
8
- import readline from 'readline';
9
- import { fileURLToPath } from "node:url";
10
-
11
- function findFileUpwardsSync(filename, maxLevels = 99) {
12
- let currentDir = process.cwd();
13
- for (let i = 0; i < maxLevels; i++) {
14
- const candidate = path.join(currentDir, filename);
15
- if (fs.existsSync(candidate)) {
16
- return candidate;
17
- }
18
-
19
- const parentDir = path.dirname(currentDir);
20
- if (parentDir === currentDir) {
21
- break;
22
- }
23
-
24
- currentDir = parentDir;
25
- }
26
-
27
- return null;
28
- }
29
-
30
- function getPackageGM() {
31
- const file = findFileUpwardsSync("package-gm.json");
32
- if (file === null) {
33
- throw new Error('❌ package-gm.json was not found')
34
- }
35
-
36
- return {
37
- file: file,
38
- data: JSON.parse(fs.readFileSync(file, "utf8")),
39
- }
40
- }
41
-
42
- function getYYPPathFromPackageGM(packageGM) {
43
- return path.join(path.dirname(packageGM.file), packageGM.data.main).replaceAll("\\", "/");
44
- }
45
-
46
- function backupPackageGM(packageGM) {
47
- console.log(`📝 Backup package-gm.json: ${packageGM.file}.old`);
48
- fs.copyFileSync(packageGM.file, `${packageGM.file}.old`);
49
- }
50
-
51
- function savePackageGM(packageGM) {
52
- fs.writeFileSync(packageGM.file, JSON.stringify(packageGM.data, null, 2), "utf8");
53
- }
54
-
55
- function clamp(value, min, max) {
56
- return Math.min(Math.max(value, min), max);
57
- }
58
-
59
- function getPSMonitorRAMCommand(processName, interval, name) {
60
- const psCommand = `
61
- \$processName = "${processName}"
62
- \$interval = ${interval}
63
- \$reportFile = "${name}"
64
- \$timestamp = Get-Date -Format "yyyy-MM-dd_hh-mm"
65
- if (\$reportFile -eq "") {
66
- \$reportFile = "\$processName_\$timestamp-ram-report.csv"
67
- }
68
-
69
- if (-not (Test-Path \$reportFile)) {
70
- "date,RAM_MB" | Out-File -FilePath \$reportFile -Encoding UTF8
71
- }
72
-
73
- echo "Monitoring RAM usage..."
74
-
75
- while (\$true) {
76
- \$date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
77
- try {
78
- \$process = Get-Process -Name \$processName -ErrorAction Stop
79
- \$ramMB = [math]::Round(\$process.WorkingSet64 / 1MB, 2)
80
- "\$date,\$ramMB" | Out-File -Append -Encoding utf8 -FilePath \$reportFile
81
- echo "\$date TEST [${processName}::monitor-ram]: \$ramMB"
82
- } catch {
83
- #"\$date,PROCESS_NOT_RUNNING" | Out-File -Append -Encoding utf8 -FilePath \$reportFile
84
- echo "\$date TEST [${processName}::monitor-ram]: PROCESS_NOT_RUNNING"
85
- }
86
-
87
- Start-Sleep -Milliseconds \$interval
88
- }
89
- `
90
-
91
- return psCommand
92
- }
93
-
94
- function runShellScript(scriptData) {
95
- const bashProcess = spawn("bash", ["-s"], { stdio: ["pipe", "inherit", "inherit"] });
96
- bashProcess.stdin.write(`#!/bin/bash\nset -Eeuo pipefail\n${scriptData}`);
97
- bashProcess.stdin.end();
98
- bashProcess.on("close", (code) => {
99
- console.log(`Exited with code ${code}`);
100
- process.exit(code);
101
- });
102
- return bashProcess
103
- }
104
-
105
-
106
- const packageJson = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "package.json"), "utf-8"));
107
-
108
- const program = new Command()
109
- .version(packageJson.version, '-v, --version, ', 'output the current version');
110
-
111
- const config = program
112
- .command('config')
113
- .description('Manage configuration');
114
-
115
- const configSet = config
116
- .command('set')
117
- .description('Set config values');
118
-
119
- const configUnset = config
120
- .command('unset')
121
- .description('Unset config values');
122
-
123
- const resource = program
124
- .command("resource")
125
- .description("Manage resources");
126
-
127
-
128
- program
129
- .command('init')
130
- .description('CLI creator for package-gm.json')
131
- .action(async () => {
132
- const rl = readline.createInterface({
133
- input: process.stdin,
134
- output: process.stdout
135
- });
136
- const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
137
-
138
- try {
139
- console.log("This utility will walk you through creating a package-gm.json file.");
140
- console.log("It only covers the most common items, and tries to guess sensible defaults.");
141
-
142
- const projectPath = process.cwd();
143
- const basename = path.basename(projectPath);
144
- const version = "0.0.1";
145
- const propertyPackage = await askQuestion(`package name: (${basename}) `);
146
- const propertyVersion = await askQuestion(`version: (${version}) `);
147
- const propertyDescription = await askQuestion('description: ');
148
- const propertyYYP = await askQuestion('gamemaker project file (.yyp): ');
149
- const propertyTest = await askQuestion('test command: ');
150
- const propertyGit = await askQuestion('git repository: ');
151
- const propertyKeywords = await askQuestion('keywords: ');
152
- const propertyAuthor = await askQuestion('author: ');
153
- const propertyLicense = await askQuestion('license: (ISC) ');
154
- const data = {
155
- name: propertyPackage === null || propertyPackage === ''
156
- ? basename
157
- : propertyPackage,
158
- version: propertyVersion === null || propertyVersion === ''
159
- ? version
160
- : propertyVersion,
161
- description: propertyDescription,
162
- main: propertyYYP,
163
- git: propertyGit,
164
- keywords: propertyKeywords,
165
- author: propertyAuthor,
166
- license: propertyLicense,
167
- scripts: {
168
- test: propertyTest
169
- },
170
- dependencies: {},
171
- runtimes: {},
172
- };
173
-
174
- const filePath = path.join(projectPath, 'package-gm.json');
175
- const dataString = JSON.stringify(data, null, 2);
176
-
177
- console.log(`About to write to ${filePath}:\n\n${dataString}\n\n`);
178
- const response = await askQuestion(`Is this OK? (yes) `)
179
- if (typeof response === 'string' && (response.includes('y') || response.includes('Y'))) {
180
- fs.writeFileSync(filePath, dataString, 'utf8');
181
- } else {
182
- console.log('Aborted.\n');
183
- }
184
- } catch (error) {
185
- console.error('An error occurred:', error);
186
- } finally {
187
- rl.close();
188
- process.exit(0);
189
- }
190
- });
191
-
192
- program
193
- .command('watch')
194
- .description('Watch modules dir and copy code to gamemaker project')
195
- .action(() => {
196
- watch(path.normalize(path.join(process.cwd(), 'package-gm.json')))
197
- });
198
-
199
- program
200
- .command('sync')
201
- .description('Copy code from modules dir to gamemaker project')
202
- .action(() => {
203
- sync(path.normalize(path.join(process.cwd(), 'package-gm.json')))
204
- });
205
-
206
- program
207
- .command('install')
208
- .description('Install dependencies listed in package-gm.json to gm_modules folder')
209
- .option('-c, --clean', 'remove existing gm_modules')
210
- .option('-s, --shallow', 'git clone will use depth=1 branch=REVISION')
211
- .action(function() {
212
- const options = this.opts();
213
- const clean = options.clean !== undefined;
214
- const shallow = options.shallow !== undefined;
215
- const modulesDir = 'gm_modules';
216
-
217
- if (!fs.existsSync(modulesDir)) {
218
- fs.mkdirSync(modulesDir);
219
- } else if (clean) {
220
- fs.rmdirSync(modulesDir, { recursive: true });
221
- fs.mkdirSync(modulesDir);
222
- }
223
-
224
- const packageGM = getPackageGM()
225
- const dependencies = packageGM.data.dependencies;
226
- Object.entries(dependencies).forEach(([key, dependency]) => {
227
- console.log(`\n📦️ Install ${key}\n===========${"=".repeat(key.length)}`)
228
- const modulePath = path.join(modulesDir, key);
229
- const cloneOptions = shallow ? `--depth 1 --branch ${dependency.revision}` : ''
230
- if (fs.existsSync(modulePath)) {
231
- try {
232
- execSync('git rev-parse --is-inside-work-tree', { cwd: modulePath, stdio: 'ignore' });
233
- console.log(`🌐 Syncing ${modulePath} to revision ${dependency.revision}`);
234
- execSync('git reset --hard HEAD', { cwd: modulePath, stdio: 'inherit' });
235
- execSync('git clean -fdx', { cwd: modulePath, stdio: 'inherit' });
236
- execSync(`git checkout ${dependency.revision}`, { cwd: modulePath, stdio: 'inherit' });
237
- } catch (error) {
238
- console.log(`🗑️ Removing ${modulePath} because it's not a git repository`);
239
- fs.rmSync(modulePath, { recursive: true, force: true });
240
- console.log(`🔧 Initializing ${modulePath} to revision ${dependency.revision}`);
241
- execSync(`git clone ${cloneOptions} ${dependency.remote} ${modulePath}`, { stdio: 'inherit' });
242
- execSync(`git checkout ${dependency.revision}`, { cwd: modulePath, stdio: 'inherit' });
243
- }
244
- } else {
245
- console.log(`🔧 Initializing ${modulePath} to revision ${dependency.revision}`);
246
- execSync(`git clone ${cloneOptions} ${dependency.remote} ${modulePath}`, { stdio: 'inherit' });
247
- execSync(`git checkout ${dependency.revision}`, { cwd: modulePath, stdio: 'inherit' });
248
- }
249
- });
250
-
251
- console.log('\n\n✅ All dependencies processed.');
252
- process.exit(0);
253
- })
254
-
255
- program
256
- .command('run')
257
- .description('Run the script named <foo>')
258
- .argument('<foo>', 'script name')
259
- .action((foo) => {
260
- if (typeof foo !== 'string') {
261
- console.log(`missing argument`);
262
- console.log(`Exited with code 1`);
263
- return process.exit(1);
264
- }
265
-
266
- const packageGM = getPackageGM()
267
- const scriptData = packageGM.data.scripts[foo]
268
- if (typeof scriptData !== 'string') {
269
- console.log(`script ${foo} wasn't found`);
270
- console.log(`Exited with code 1`);
271
- return process.exit(1);
272
- }
273
-
274
- const shellScript = `#!/bin/bash
275
- cd ${path.dirname(packageGM.file).replaceAll("\\", "/")}
276
-
277
- ${scriptData}
278
- `;
279
- runShellScript(shellScript)
280
- });
281
-
282
- program
283
- .command('generate')
284
- .description('Generate *.yyp IncludedFiles section')
285
- .action(function() {
286
- function getFilesRecursively(dir, root) {
287
- let files = [];
288
- for (const entry of fs.readdirSync(dir)) {
289
- const fullPath = path.join(dir, entry).replaceAll("\\", "/");
290
- if (fs.statSync(fullPath).isDirectory()) {
291
- files = files.concat(getFilesRecursively(fullPath, root));
292
- } else {
293
- const filePath = `datafiles${(fullPath.startsWith(root) ? fullPath.slice(root.length) : fullPath)}`.replaceAll(`/${entry}`, '');
294
- const line = `{"$GMIncludedFile":"","%Name":"${entry}","CopyToMask":-1,"filePath":"${filePath}","name":"${entry}","resourceType":"GMIncludedFile","resourceVersion":"2.0",},`;
295
- files.push(line);
296
- }
297
- }
298
- return files;
299
- }
300
-
301
- function parseEnvFile(filePath) {
302
- const content = fs.readFileSync(filePath, "utf8");
303
- const result = new Map();
304
- content.split(/\r?\n/).forEach(line => {
305
- line = line.trim();
306
- if (!line || line.startsWith("#")) {
307
- return;
308
- }
309
-
310
- const match = line.match(/^([^=]+)="(.*)"$/);
311
- if (match) {
312
- const [, key, value] = match;
313
- result.set(key.trim(), value);
314
- }
315
- });
316
-
317
- return result;
318
- }
319
-
320
- const envFile = findFileUpwardsSync(".gm-cli.env");
321
- if (envFile === null) {
322
- console.error('.gm-cli.env was not found')
323
- return
324
- }
325
-
326
- const packageGM = getPackageGM()
327
- if (packageGM === null) {
328
- return null
329
- }
330
-
331
- const envPath = path.dirname(envFile).replaceAll("\\", "/");
332
- const envMap = parseEnvFile(envFile);
333
- const projectPath = path.dirname(path.join(path.dirname(packageGM.file), packageGM.data.main.replaceAll("\\", "/")));
334
- const yypPath = getYYPPathFromPackageGM(packageGM)
335
- const yypOldPath = `${yypPath}.old`
336
- const yyp = fs.readFileSync(yypPath, "utf8");
337
- console.log(`📝 Backup yyp:`, yypOldPath);
338
- fs.copyFileSync(yypPath, yypOldPath);
339
-
340
- const datafilesPath = path.join(projectPath, "datafiles").replaceAll("\\", "/")
341
- const datafiles = getFilesRecursively(datafilesPath, datafilesPath)
342
- const replaced = yyp.replace(/"IncludedFiles"\s*:\s*\[(.*?)\]/s, `"IncludedFiles":[
343
- ${datafiles.join("\n ")}
344
- ]`);
345
- console.log(`📝 Save yyp:`, yypPath);
346
- fs.writeFileSync(yypPath, replaced, "utf8");
347
- });
348
-
349
- program
350
- .command('make')
351
- .description('Build and run gamemaker project')
352
- .option('-t, --target <target>', 'available targets: windows')
353
- .option('-r, --runtime <type>', 'use VM or YYC runtime')
354
- .option('-n, --name <name>', 'The actual file name of the ZIP file that is created')
355
- .option('-l, --launch', 'launch the executable after building')
356
- .option('-c, --clean', 'make clean build')
357
- .option('-p, --projectool', 'Path to ProjectTool.exe')
358
- .action(function() {
359
- const packageGM = getPackageGM()
360
- const targetMap = new Map([ [ 'windows', 'win' ] ])
361
- const options = this.opts();
362
- const config = {
363
- runtime: '$GM_CLI_DEFAULT_RUNTIME',
364
- target: '$GM_CLI_DEFAULT_TARGET',
365
- projectool: '$GM_CLI_PROJECT_TOOL_PATH',
366
- clean: 'false',
367
- launch: 'PackageZip',
368
- name: packageGM.data.name,
369
- zip: packageGM.data.name,
370
- yyp: path.basename(packageGM.data.main).replaceAll("\\", "/"),
371
- path: path.join(path.dirname(packageGM.file), path.dirname(packageGM.data.main)).replaceAll("\\", "/"),
372
- };
373
-
374
- if (options.runtime !== undefined) {
375
- config.runtime = options.runtime;
376
- }
377
-
378
- if (options.target !== undefined && targetMap.has(options.target)) {
379
- config.target = options.target;
380
- config.targetExt = targetMap.get(config.target);
381
- }
382
-
383
- if (options.projectool !== undefined) {
384
- config.projectool = options.projectool;
385
- }
386
-
387
- if (options.clean !== undefined) {
388
- config.clean = 'true';
389
- }
390
-
391
- if (options.launch !== undefined) {
392
- config.launch = 'Run';
393
- }
394
-
395
- if (options.name !== undefined && typeof options.name === 'string' && options.name.trim() !== '') {
396
- config.zip = options.name;
397
- }
398
-
399
- const shellScript = `#!/bin/bash
400
- function log_info {
401
- local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
402
- echo -e "\\\e[90m$timestamp\\\e[0m \\\e[32mINFO\\\e[0m \\\e[35m[gm-cli::make]\\\e[0m $1"
403
- }
404
-
405
- function log_error {
406
- local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
407
- echo -e "\\\e[90m$timestamp\\\e[0m \\\e[31mERROR\\\e[0m \\\e[35m[gm-cli::make]\\\e[0m $1"
408
- }
409
-
410
- gm_cli_env_path=""
411
- dir=$(realpath "$PWD")
412
- while [ "$dir" != "/" ]; do
413
- if [ -f "$dir/.gm-cli.env" ]; then
414
- gm_cli_env_path="$dir/.gm-cli.env"
415
- log_info "Load configuration '$gm_cli_env_path'"
416
- set -a
417
- . "$gm_cli_env_path"
418
- set +a
419
- break
420
- fi
421
- dir=$(dirname "$dir")
422
- done
423
-
424
- runtime_path=$GM_CLI_RUNTIME_PATH
425
- if [ -z "$runtime_path" ]; then
426
- log_error "GM_CLI_RUNTIME_PATH must be defined! exit 1"
427
- exit 1
428
- fi
429
- runtime_path=$(realpath $runtime_path)
430
-
431
- igor_path="$\{GM_CLI_RUNTIME_PATH%/\}/bin/igor/windows/x64/Igor.exe"
432
- if [ -z "$igor_path" ]; then
433
- log_error "GM_CLI_RUNTIME_PATH must be defined! exit 1"
434
- exit 1
435
- fi
436
- igor_path=$(realpath $igor_path)
437
-
438
- project_name=${config.name}
439
- if [ -z "$project_name" ]; then
440
- log_error "package-gm.json name field must be defined! exit 1"
441
- exit 1
442
- fi
443
-
444
- project_yyp=${config.yyp}
445
- if [ -z "$project_yyp" ]; then
446
- log_error "package-gm.json yyp field must be defined! exit 1"
447
- exit 1
448
- fi
449
-
450
- project_path=${config.path}
451
- if [ -z "$project_path" ]; then
452
- log_error "package-gm.json yyp field must be defined! exit 1"
453
- exit 1
454
- fi
455
- project_path=$(realpath $project_path)
456
-
457
- user_path=$GM_CLI_USER_PATH
458
- if [ -z "$user_path" ]; then
459
- log_error "GM_CLI_USER_PATH must be defined! exit 1"
460
- exit 1
461
- fi
462
- user_path=$(realpath $user_path)
463
-
464
- runtime=${config.runtime}
465
- if [ -z "$runtime" ]; then
466
- log_error "GM_CLI_DEFAULT_RUNTIME must be defined! exit 1"
467
- exit 1
468
- fi
469
-
470
- target=${config.target}
471
- if [ -z "$target" ]; then
472
- log_error "GM_CLI_DEFAULT_TARGET must be defined! exit 1"
473
- exit 1
474
- fi
475
-
476
- zip_name=${config.zip}
477
- echo $zip_name
478
- if [ -z "$zip_name" ]; then
479
- log_error "--name must be defined! exit 1"
480
- exit 1
481
- fi
482
-
483
- project_tool=${config.projectool}
484
- echo $project_tool
485
- if [ -z "$project_tool" ]; then
486
- log_error "--projectool must be defined! exit 1"
487
- exit 1
488
- fi
489
-
490
- clean=${config.clean}
491
- if [ "$clean" = "true" ]; then
492
- log_info "Clean '$project_path/tmp/igor'"
493
- rm -rf $project_path/tmp/igor
494
-
495
- log_info "Execute shell command:\n\\\e[33m$igor_path \\ \n --runtimePath="$runtime_path" \\ \n --runtime=$runtime \\ \n --project="$\{project_path\}/$\{project_yyp\}" \\ \n --projectool="$\{project_tool\}" \\ \n --uf="$user_path" \\ \n -- $target Clean\n\\\e[0m"
496
- $igor_path \
497
- --runtimePath="$runtime_path" \
498
- --runtime=$runtime \
499
- --project="$\{project_path\}/$\{project_yyp\}" \
500
- --projectool="$\{project_tool\}" \
501
- --uf="$user_path" \
502
- -- $target Clean | GREP_COLORS='mt=01;31' grep --color=always -E 'Error : |$'
503
- fi
504
-
505
- log_info "Clean '$\{project_path\}/tmp/igor/out'"
506
- rm -rf $\{project_path\}/tmp/igor/out
507
-
508
- log_info "Execute shell command:\n\\\e[33m$igor_path \\ \n --project="$\{project_path\}/$\{project_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 --projectool="$\{project_tool\}" \\ \n --uf="$user_path" \\ \n -- $target ${config.launch}\\\e[0m"
509
- $igor_path \
510
- --project="$\{project_path\}/$\{project_yyp\}" \
511
- --user="$user_path" \
512
- --runtimePath="$runtime_path" \
513
- --runtime=$runtime \
514
- --cache="$\{project_path\}/tmp/igor/cache" \
515
- --temp="$\{project_path\}/tmp/igor/temp" \
516
- --of="$\{project_path\}/tmp/igor/out/$\{project_name\}.win" \
517
- --tf="$\{zip_name\}.zip" \
518
- --projectool="$\{project_tool\}" \
519
- --uf="$user_path" \
520
- -- $target ${config.launch} | GREP_COLORS='mt=01;31' grep --color=always -E 'Error : |$'
521
-
522
- exit 0
523
- `;
524
-
525
- runShellScript(shellScript)
526
- });
527
-
528
- program
529
- .command('env')
530
- .description('CLI creator for .gm-cli.env')
531
- .action(async () => {
532
- const rl = readline.createInterface({
533
- input: process.stdin,
534
- output: process.stdout
535
- });
536
- const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
537
-
538
- try {
539
- console.log("This utility will walk you through creating a .gm-cli.env file.");
540
-
541
- const runtimes = [ "VM", "YYC" ]
542
- const targets = [ "windows" ]
543
-
544
- const projectPath = process.cwd();
545
- const propertyDefaultRuntime = await askQuestion('Default runtime [ VM, YYC ]: ');
546
- const propertyDefaultTarget = await askQuestion('Default target [ windows ]: ');
547
- const propertyProjectoolPath = await askQuestion('Path to ProjectTool.exe: ');
548
- const propertyRuntimePath = await askQuestion('Path to gamemaker runtime: ');
549
- const propertyUserPath = await askQuestion('Path to gamemaker user: ');
550
- const propertyVsDevCmdPath = await askQuestion('Path to VsDevCmd.bat: ');
551
- const data = {
552
- GM_CLI_DEFAULT_RUNTIME: runtimes.includes(propertyDefaultRuntime) ? propertyDefaultRuntime : runtimes[0],
553
- GM_CLI_DEFAULT_TARGET: runtimes.includes(propertyDefaultTarget) ? propertyDefaultTarget : targets[0],
554
- GM_CLI_PROJECT_TOOL_PATH: path.normalize(propertyProjectoolPath),
555
- GM_CLI_RUNTIME_PATH: path.normalize(propertyRuntimePath),
556
- GM_CLI_USER_PATH: path.normalize(propertyUserPath),
557
- GM_CLI_VS_DEV_CMD_PATH: path.normalize(propertyVsDevCmdPath),
558
- };
559
-
560
- const filePath = path.join(projectPath, '.gm-cli.env');
561
- const dataString = Object.entries(data)
562
- .map(([key, value]) => `${key}="${value}"`)
563
- .join("\n");
564
-
565
- console.log(`About to write to ${filePath}:\n\n${dataString}\n\n`);
566
- const response = await askQuestion(`Is this OK? (yes) `)
567
- if (typeof response === 'string' && (response.includes('y') || response.includes('Y'))) {
568
- fs.writeFileSync(filePath, dataString, 'utf8');
569
- } else {
570
- console.log('Aborted.\n');
571
- }
572
- } catch (error) {
573
- console.error('An error occurred:', error);
574
- } finally {
575
- rl.close();
576
- process.exit(0);
577
- }
578
- });
579
-
580
- program
581
- .command('monitor-ram')
582
- .description('Monitor RAM usage and save it to csv file')
583
- .option('-i, --interval <interval>', 'step value in seconds (default = 15)')
584
- .option('-n, --name <name>', 'name of binary')
585
- .option('-r, --report <report>', 'name of CSV report file')
586
- .action((options) => {
587
- const packageGM = getPackageGM()
588
- const interval = clamp((Number.isNaN(Number(options.interval)) ? 15.0 : Number(options.interval)), 1.0 / 60.0, 999.0)
589
- const name = options.name === undefined ? packageGM.data.name : options.name
590
- const report = options.report === undefined ? '' : options.report
591
- const psCommand = getPSMonitorRAMCommand(name, 1000.0 * interval, report)
592
- const ps = spawn('powershell.exe', [
593
- '-NoProfile',
594
- '-Command',
595
- psCommand
596
- ]);
597
-
598
- ps.stdout.on('data', data => {
599
- console.log(data.toString().replace(/\r?\n$/, ''));
600
- });
601
-
602
- ps.stderr.on('data', data => {
603
- console.error(data.toString().replace(/\r?\n$/, ''));
604
- });
605
-
606
- ps.on('close', code => {
607
- console.log(`Exited with code ${code}`);
608
- });
609
- })
610
-
611
- program
612
- .command('test')
613
- .description('Run tests')
614
- .option('-t, --tests <tests>', 'List of paths to json test cases')
615
- .option('-b, --build <build>', 'Path to executable')
616
- .option('-m, --monitorRAM', 'Monitor RAM while testing')
617
- .action((options) => {
618
- const packageGM = getPackageGM()
619
-
620
- const shellMonitorRAMScript = options.monitorRAM === undefined ? `` : `
621
- set -m
622
- gm-cli monitor-ram --name \$\{EXE_FILE%.exe\} &
623
- pid=\$!
624
- trap "kill -- -\$pid 2>/dev/null" EXIT INT TERM
625
- `;
626
-
627
- const shellBuildScript = options.build !== undefined ? `
628
- cd ${path.dirname(options.build).replaceAll("\\", "/")}
629
- build_name="\$\{PWD##*/\}"
630
-
631
- EXE_FILE="${path.basename(options.build)}"
632
- EXE_COUNT=1
633
- ` : `
634
- build_name="${packageGM.data.name}_test"
635
- rm -rf \$build_name.zip
636
- rm -rf \$build_name
637
- gm-cli make --name \$build_name
638
- unzip \$build_name.zip -d \$build_name
639
- cd \$build_name
640
-
641
- EXE_FILE=\$(find . -maxdepth 1 -type f -name "*.exe" -printf "%f\n" 2>/dev/null)
642
- EXE_COUNT=\$(printf "%s\n" "\$EXE_FILE" | grep -c .)
643
- `;
644
-
645
- const shellTestScript = options.tests === undefined ? `
646
- TESTS=\$(find . -type f -name "*test.json" -print0 | xargs -0 echo | sed 's/ /, /g')
647
- ` : `
648
- TESTS=\"${options.tests}\"
649
- `
650
-
651
- const shellCommandScript = `
652
- TIMESTAMP=\$(date +"%Y-%m-%d_%H-%M")
653
- OUTPUT_FILE="\$\{TIMESTAMP\}_\$\{EXE_FILE%.exe\}_run-test.log"
654
- COMMAND="./\$EXE_FILE -output \"\$OUTPUT_FILE\" --tests \\\"\$TESTS\\\""
655
-
656
- if [ "\$EXE_COUNT" -eq 0 ]; then
657
- echo "ERROR 1: binary was not found"
658
- exit 1
659
- elif [ "\$EXE_COUNT" -gt 1 ]; then
660
- echo "ERROR 2: found more executables: \$EXE_FILE"
661
- exit 2
662
- fi
663
- `;
664
-
665
- const shellScript = `#!/bin/bash
666
- cd ${path.dirname(packageGM.file).replaceAll("\\", "/")}
667
-
668
- ${shellBuildScript}
669
-
670
- ${shellTestScript}
671
-
672
- ${shellCommandScript}
673
-
674
- ${shellMonitorRAMScript}
675
-
676
- eval "\$COMMAND" | cat
677
- `
678
-
679
- runShellScript(shellScript)
680
- })
681
-
682
- configSet
683
- .command('dependency <name> <revision>')
684
- .description('Manage dependencies in package-gm.json')
685
- .option('--remote <remote>')
686
- .action((name, revision, options) => {
687
- const resolve = () => {
688
- const current = packageGM.data.dependencies[name] ?? {};
689
-
690
- packageGM.data.dependencies[name] = {
691
- ...current,
692
- ...(options.remote !== undefined && { remote: options.remote }),
693
- ...(revision !== undefined && { revision }),
694
- };
695
-
696
- console.log("🔨 Set dependency", name, "as", packageGM.data.dependencies[name])
697
- };
698
-
699
- const packageGM = getPackageGM()
700
-
701
- backupPackageGM(packageGM)
702
- resolve()
703
- savePackageGM(packageGM)
704
- });
705
-
706
- configSet
707
- .command('script <name> [command]')
708
- .description('Manage scripts in package-gm.json')
709
- .action((name, command = '') => {
710
- const resolve = () => {
711
- packageGM.data.scripts[name] = command !== undefined ? command : ''
712
- console.log("🔨 Set script", name, "as", packageGM.data.scripts[name])
713
- }
714
-
715
- const packageGM = getPackageGM()
716
-
717
- backupPackageGM(packageGM)
718
- resolve()
719
- savePackageGM(packageGM)
720
- });
721
-
722
- configSet
723
- .command('runtime <name> [supported]')
724
- .description('Manage runtimes in package-gm.json')
725
- .action((name, supported = 'true') => {
726
- const resolve = () => {
727
- packageGM.data.runtimes[name] = supported === "false" ? supported : "true"
728
- console.log("🔨 Set runtime", name, "as", supported === "false" ? "false" : "true")
729
- }
730
-
731
- const packageGM = getPackageGM()
732
-
733
- backupPackageGM(packageGM)
734
- resolve()
735
- savePackageGM(packageGM)
736
- });
737
-
738
- configUnset
739
- .command('dependency <name>')
740
- .description('Remove dependencies from package-gm.json')
741
- .action((name) => {
742
- const resolve = () => {
743
- if (name in packageGM.data.dependencies) {
744
- console.log("🗑️ Unset dependency", name)
745
- delete packageGM.data.dependencies[name]
746
- }
747
- }
748
-
749
- const packageGM = getPackageGM()
750
-
751
- backupPackageGM(packageGM)
752
- resolve()
753
- savePackageGM(packageGM)
754
- });
755
-
756
- configUnset
757
- .command('script <name>')
758
- .description('Remove scripts from package-gm.json')
759
- .action((name) => {
760
- const resolve = () => {
761
- if (name in packageGM.data.scripts) {
762
- console.log("🗑️ Unset script", name)
763
- delete packageGM.data.scripts[name]
764
- }
765
- }
766
-
767
- const packageGM = getPackageGM()
768
-
769
- backupPackageGM(packageGM)
770
- resolve()
771
- savePackageGM(packageGM)
772
- });
773
-
774
- configUnset
775
- .command('runtime <name>')
776
- .description('Remove runtimes from package-gm.json')
777
- .action((name) => {
778
- const resolve = () => {
779
- if (name in packageGM.data.scripts) {
780
- console.log("🗑️ Unset runtime", name)
781
- delete packageGM.data.runtimes[name]
782
- }
783
- }
784
-
785
- const packageGM = getPackageGM()
786
-
787
- backupPackageGM(packageGM)
788
- resolve()
789
- savePackageGM(packageGM)
790
- });
791
-
792
- resource
793
- .command("create")
794
- .description("Create a resource")
795
- .requiredOption("-t, --type <type>", "Resource type")
796
- .requiredOption("-n, --name <name>", "Resource name")
797
- .option("-f, --folder <folder>", "Resource folder")
798
- .action((options) => {
799
- const packageGM = getPackageGM()
800
- const yypPath = getYYPPathFromPackageGM(packageGM)
801
- const folderOption = options.folder !== undefined ? `folder=${options.folder}` : ``
802
- const shellScript = `yy-gm-cli resourcetool eval "resource create type=${options.type} name=${options.name} ${folderOption}" ${yypPath}`
803
- runShellScript(shellScript)
804
- });
805
-
806
- resource
807
- .command("update")
808
- .description("Update a resource property")
809
- .requiredOption("-e, --expr <expr>", "Resource expression")
810
- .requiredOption("-v, --value <value>", "New value")
811
- .action((options) => {
812
- const packageGM = getPackageGM()
813
- const yypPath = getYYPPathFromPackageGM(packageGM)
814
- const shellScript = `yy-gm-cli resourcetool eval "resource set expr=${options.expr} value=${options.value}" ${yypPath}`
815
- runShellScript(shellScript)
816
- });
817
-
818
- resource
819
- .command("get")
820
- .description(" a resource")
821
- .requiredOption("-e, --expr <expr>", "Resource expression")
822
- .action((options) => {
823
- const packageGM = getPackageGM()
824
- const yypPath = getYYPPathFromPackageGM(packageGM)
825
- const shellScript = `yy-gm-cli resourcetool eval "resource info expr=${options.expr}" ${yypPath}`
826
- runShellScript(shellScript)
827
- });
828
-
829
- resource
830
- .command("delete")
831
- .description("Delete a resource")
832
- .requiredOption("-n, --name <name>", "Resource name")
833
- .option("--type <type>", "Resource type")
834
- .action((options) => {
835
- const packageGM = getPackageGM()
836
- const yypPath = getYYPPathFromPackageGM(packageGM)
837
- const typeOptions = options.type !== undefined ? `type=${options.type}` : ``
838
- const shellScript = `yy-gm-cli resourcetool eval "resource delete name=${options.name} ${typeOptions}" ${yypPath}`
839
- runShellScript(shellScript)
840
- });
841
-
842
- resource
843
- .command("list")
844
- .description("List resources")
845
- .option("--type <type>", "Resource type")
846
- .action((options) => {
847
- const packageGM = getPackageGM()
848
- const yypPath = getYYPPathFromPackageGM(packageGM)
849
- const typeOptions = options.type !== undefined ? `type=${options.type}` : ``
850
- const shellScript = `yy-gm-cli resourcetool eval "resource list ${typeOptions}" ${yypPath}`
851
- runShellScript(shellScript)
852
- });
853
-
854
-
855
- program.parse(process.argv);
2
+
3
+ import { watch, sync } from './GMFileWatcher.js';
4
+ import path from 'node:path';
5
+ import { Command } from 'commander';
6
+ import { spawn, execSync } from 'child_process';
7
+ import fs from 'fs';
8
+ import readline from 'readline';
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ function findFileUpwardsSync(filename, maxLevels = 99) {
12
+ let currentDir = process.cwd();
13
+ for (let i = 0; i < maxLevels; i++) {
14
+ const candidate = path.join(currentDir, filename);
15
+ if (fs.existsSync(candidate)) {
16
+ return candidate;
17
+ }
18
+
19
+ const parentDir = path.dirname(currentDir);
20
+ if (parentDir === currentDir) {
21
+ break;
22
+ }
23
+
24
+ currentDir = parentDir;
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ function getPackageGM() {
31
+ const file = findFileUpwardsSync("package-gm.json");
32
+ if (file === null) {
33
+ throw new Error('❌ package-gm.json was not found')
34
+ }
35
+
36
+ return {
37
+ file: file,
38
+ data: JSON.parse(fs.readFileSync(file, "utf8")),
39
+ }
40
+ }
41
+
42
+ function getYYPPathFromPackageGM(packageGM) {
43
+ return path.join(path.dirname(packageGM.file), packageGM.data.main).replaceAll("\\", "/");
44
+ }
45
+
46
+ function backupPackageGM(packageGM) {
47
+ console.log(`📝 Backup package-gm.json: ${packageGM.file}.old`);
48
+ fs.copyFileSync(packageGM.file, `${packageGM.file}.old`);
49
+ }
50
+
51
+ function savePackageGM(packageGM) {
52
+ fs.writeFileSync(packageGM.file, JSON.stringify(packageGM.data, null, 2), "utf8");
53
+ }
54
+
55
+ function clamp(value, min, max) {
56
+ return Math.min(Math.max(value, min), max);
57
+ }
58
+
59
+ function getPSMonitorRAMCommand(processName, interval, name) {
60
+ const psCommand = `
61
+ \$processName = "${processName}"
62
+ \$interval = ${interval}
63
+ \$reportFile = "${name}"
64
+ \$timestamp = Get-Date -Format "yyyy-MM-dd_hh-mm"
65
+ if (\$reportFile -eq "") {
66
+ \$reportFile = "\$processName_\$timestamp-ram-report.csv"
67
+ }
68
+
69
+ if (-not (Test-Path \$reportFile)) {
70
+ "date,RAM_MB" | Out-File -FilePath \$reportFile -Encoding UTF8
71
+ }
72
+
73
+ echo "Monitoring RAM usage..."
74
+
75
+ while (\$true) {
76
+ \$date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
77
+ try {
78
+ \$process = Get-Process -Name \$processName -ErrorAction Stop
79
+ \$ramMB = [math]::Round(\$process.WorkingSet64 / 1MB, 2)
80
+ "\$date,\$ramMB" | Out-File -Append -Encoding utf8 -FilePath \$reportFile
81
+ echo "\$date TEST [${processName}::monitor-ram]: \$ramMB"
82
+ } catch {
83
+ #"\$date,PROCESS_NOT_RUNNING" | Out-File -Append -Encoding utf8 -FilePath \$reportFile
84
+ echo "\$date TEST [${processName}::monitor-ram]: PROCESS_NOT_RUNNING"
85
+ }
86
+
87
+ Start-Sleep -Milliseconds \$interval
88
+ }
89
+ `
90
+
91
+ return psCommand
92
+ }
93
+
94
+ function runShellScript(scriptData) {
95
+ const bashProcess = spawn("bash", ["-s"], { stdio: ["pipe", "inherit", "inherit"] });
96
+ bashProcess.stdin.write(`#!/bin/bash\nset -Eeuo pipefail\n${scriptData}`);
97
+ bashProcess.stdin.end();
98
+ bashProcess.on("close", (code) => {
99
+ console.log(`Exited with code ${code}`);
100
+ process.exit(code);
101
+ });
102
+ return bashProcess
103
+ }
104
+
105
+
106
+ const packageJson = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "package.json"), "utf-8"));
107
+
108
+ const program = new Command()
109
+ .version(packageJson.version, '-v, --version, ', 'output the current version');
110
+
111
+ const config = program
112
+ .command('config')
113
+ .description('Manage configuration');
114
+
115
+ const configSet = config
116
+ .command('set')
117
+ .description('Set config values');
118
+
119
+ const configUnset = config
120
+ .command('unset')
121
+ .description('Unset config values');
122
+
123
+ const resource = program
124
+ .command("resource")
125
+ .description("Manage resources");
126
+
127
+
128
+ program
129
+ .command('init')
130
+ .description('CLI creator for package-gm.json')
131
+ .action(async () => {
132
+ const rl = readline.createInterface({
133
+ input: process.stdin,
134
+ output: process.stdout
135
+ });
136
+ const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
137
+
138
+ try {
139
+ console.log("This utility will walk you through creating a package-gm.json file.");
140
+ console.log("It only covers the most common items, and tries to guess sensible defaults.");
141
+
142
+ const projectPath = process.cwd();
143
+ const basename = path.basename(projectPath);
144
+ const version = "0.0.1";
145
+ const propertyPackage = await askQuestion(`package name: (${basename}) `);
146
+ const propertyVersion = await askQuestion(`version: (${version}) `);
147
+ const propertyDescription = await askQuestion('description: ');
148
+ const propertyYYP = await askQuestion('gamemaker project file (.yyp): ');
149
+ const propertyTest = await askQuestion('test command: ');
150
+ const propertyGit = await askQuestion('git repository: ');
151
+ const propertyKeywords = await askQuestion('keywords: ');
152
+ const propertyAuthor = await askQuestion('author: ');
153
+ const propertyLicense = await askQuestion('license: (ISC) ');
154
+ const data = {
155
+ name: propertyPackage === null || propertyPackage === ''
156
+ ? basename
157
+ : propertyPackage,
158
+ version: propertyVersion === null || propertyVersion === ''
159
+ ? version
160
+ : propertyVersion,
161
+ description: propertyDescription,
162
+ main: propertyYYP,
163
+ git: propertyGit,
164
+ keywords: propertyKeywords,
165
+ author: propertyAuthor,
166
+ license: propertyLicense,
167
+ scripts: {
168
+ test: propertyTest
169
+ },
170
+ dependencies: {},
171
+ runtimes: {},
172
+ };
173
+
174
+ const filePath = path.join(projectPath, 'package-gm.json');
175
+ const dataString = JSON.stringify(data, null, 2);
176
+
177
+ console.log(`About to write to ${filePath}:\n\n${dataString}\n\n`);
178
+ const response = await askQuestion(`Is this OK? (yes) `)
179
+ if (typeof response === 'string' && (response.includes('y') || response.includes('Y'))) {
180
+ fs.writeFileSync(filePath, dataString, 'utf8');
181
+ } else {
182
+ console.log('Aborted.\n');
183
+ }
184
+ } catch (error) {
185
+ console.error('An error occurred:', error);
186
+ } finally {
187
+ rl.close();
188
+ process.exit(0);
189
+ }
190
+ });
191
+
192
+ program
193
+ .command('watch')
194
+ .description('Watch modules dir and copy code to gamemaker project')
195
+ .action(() => {
196
+ watch(path.normalize(path.join(process.cwd(), 'package-gm.json')))
197
+ });
198
+
199
+ program
200
+ .command('sync')
201
+ .description('Copy code from modules dir to gamemaker project')
202
+ .action(() => {
203
+ sync(path.normalize(path.join(process.cwd(), 'package-gm.json')))
204
+ });
205
+
206
+ program
207
+ .command('install')
208
+ .description('Install dependencies listed in package-gm.json to gm_modules folder')
209
+ .option('-c, --clean', 'remove existing gm_modules')
210
+ .option('-s, --shallow', 'git clone will use depth=1 branch=REVISION')
211
+ .action(function() {
212
+ const options = this.opts();
213
+ const clean = options.clean !== undefined;
214
+ const shallow = options.shallow !== undefined;
215
+ const modulesDir = 'gm_modules';
216
+
217
+ if (!fs.existsSync(modulesDir)) {
218
+ fs.mkdirSync(modulesDir);
219
+ } else if (clean) {
220
+ fs.rmdirSync(modulesDir, { recursive: true });
221
+ fs.mkdirSync(modulesDir);
222
+ }
223
+
224
+ const packageGM = getPackageGM()
225
+ const dependencies = packageGM.data.dependencies;
226
+ Object.entries(dependencies).forEach(([key, dependency]) => {
227
+ console.log(`\n📦️ Install ${key}\n===========${"=".repeat(key.length)}`)
228
+ const modulePath = path.join(modulesDir, key).replaceAll("\\", "/");
229
+ const cloneOptions = shallow ? `--depth 1 --branch ${dependency.revision}` : ''
230
+ const fetchOptions = shallow ? `origin "${dependency.revision}"` : `--all --tags`
231
+ const commit = `
232
+ COMMIT="${dependencies.revision}"
233
+ if git rev-parse --verify "${dependency.revision}^{commit}" >/dev/null 2>&1; then
234
+ COMMIT=\$(git rev-parse "${dependency.revision}^{commit}")
235
+ elif git rev-parse --verify "origin/${dependency.revision}^{commit}" >/dev/null 2>&1; then
236
+ COMMIT=\$(git rev-parse "origin/${dependency.revision}^{commit}")
237
+ else
238
+ echo "Cannot resolve revision ${dependency.revision}"
239
+ exit 1
240
+ fi
241
+
242
+ `
243
+ if (fs.existsSync(modulePath)) {
244
+ try {
245
+ execSync('git rev-parse --is-inside-work-tree', { shell: "bash", cwd: modulePath, stdio: 'ignore' });
246
+ console.log(`🌐 Syncing ${modulePath} to revision ${dependency.revision}`);
247
+ execSync(`git fetch ${fetchOptions}`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
248
+ execSync(`${commit}git checkout --detach --force \$COMMIT`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
249
+ execSync(`${commit}git reset --hard \$COMMIT`, { shell: "bash", cwd: modulePath, stdio: 'ignore' });
250
+ execSync(`git clean -fd`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
251
+ } catch (error) {
252
+ console.log(`🗑️ Removing ${modulePath} because it's not a git repository`);
253
+ fs.rmSync(modulePath, { recursive: true, force: true });
254
+ console.log(`🔧 Initializing ${modulePath} to revision ${dependency.revision}`);
255
+ execSync(`git clone ${cloneOptions} ${dependency.remote} ${modulePath}`, { shell: "bash", stdio: 'inherit' });
256
+ execSync(`git fetch ${fetchOptions}`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
257
+ execSync(`${commit}git checkout --detach --force \$COMMIT`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
258
+ }
259
+ } else {
260
+ console.log(`🔧 Initializing ${modulePath} to revision ${dependency.revision}`);
261
+ execSync(`git clone ${cloneOptions} ${dependency.remote} ${modulePath}`, { shell: "bash", stdio: 'inherit' });
262
+ execSync(`git fetch ${fetchOptions}`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
263
+ execSync(`${commit}git checkout --detach --force \$COMMIT`, { shell: "bash", cwd: modulePath, stdio: 'inherit' });
264
+ }
265
+ });
266
+
267
+ console.log('\n\n✅ All dependencies processed.');
268
+ process.exit(0);
269
+ })
270
+
271
+ program
272
+ .command('run')
273
+ .description('Run the script named <foo>')
274
+ .argument('<foo>', 'script name')
275
+ .action((foo) => {
276
+ if (typeof foo !== 'string') {
277
+ console.log(`missing argument`);
278
+ console.log(`Exited with code 1`);
279
+ return process.exit(1);
280
+ }
281
+
282
+ const packageGM = getPackageGM()
283
+ const scriptData = packageGM.data.scripts[foo]
284
+ if (typeof scriptData !== 'string') {
285
+ console.log(`script ${foo} wasn't found`);
286
+ console.log(`Exited with code 1`);
287
+ return process.exit(1);
288
+ }
289
+
290
+ const shellScript = `#!/bin/bash
291
+ cd ${path.dirname(packageGM.file).replaceAll("\\", "/")}
292
+
293
+ ${scriptData}
294
+ `;
295
+ runShellScript(shellScript)
296
+ });
297
+
298
+ program
299
+ .command('generate')
300
+ .description('Generate *.yyp IncludedFiles section')
301
+ .action(function() {
302
+ function getFilesRecursively(dir, root) {
303
+ let files = [];
304
+ for (const entry of fs.readdirSync(dir)) {
305
+ const fullPath = path.join(dir, entry).replaceAll("\\", "/");
306
+ if (fs.statSync(fullPath).isDirectory()) {
307
+ files = files.concat(getFilesRecursively(fullPath, root));
308
+ } else {
309
+ const filePath = `datafiles${(fullPath.startsWith(root) ? fullPath.slice(root.length) : fullPath)}`.replaceAll(`/${entry}`, '');
310
+ const line = `{"$GMIncludedFile":"","%Name":"${entry}","CopyToMask":-1,"filePath":"${filePath}","name":"${entry}","resourceType":"GMIncludedFile","resourceVersion":"2.0",},`;
311
+ files.push(line);
312
+ }
313
+ }
314
+ return files;
315
+ }
316
+
317
+ function parseEnvFile(filePath) {
318
+ const content = fs.readFileSync(filePath, "utf8");
319
+ const result = new Map();
320
+ content.split(/\r?\n/).forEach(line => {
321
+ line = line.trim();
322
+ if (!line || line.startsWith("#")) {
323
+ return;
324
+ }
325
+
326
+ const match = line.match(/^([^=]+)="(.*)"$/);
327
+ if (match) {
328
+ const [, key, value] = match;
329
+ result.set(key.trim(), value);
330
+ }
331
+ });
332
+
333
+ return result;
334
+ }
335
+
336
+ const envFile = findFileUpwardsSync(".gm-cli.env");
337
+ if (envFile === null) {
338
+ console.error('.gm-cli.env was not found')
339
+ return
340
+ }
341
+
342
+ const packageGM = getPackageGM()
343
+ if (packageGM === null) {
344
+ return null
345
+ }
346
+
347
+ const envPath = path.dirname(envFile).replaceAll("\\", "/");
348
+ const envMap = parseEnvFile(envFile);
349
+ const projectPath = path.dirname(path.join(path.dirname(packageGM.file), packageGM.data.main.replaceAll("\\", "/")));
350
+ const yypPath = getYYPPathFromPackageGM(packageGM)
351
+ const yypOldPath = `${yypPath}.old`
352
+ const yyp = fs.readFileSync(yypPath, "utf8");
353
+ console.log(`📝 Backup yyp:`, yypOldPath);
354
+ fs.copyFileSync(yypPath, yypOldPath);
355
+
356
+ const datafilesPath = path.join(projectPath, "datafiles").replaceAll("\\", "/")
357
+ const datafiles = getFilesRecursively(datafilesPath, datafilesPath)
358
+ const replaced = yyp.replace(/"IncludedFiles"\s*:\s*\[(.*?)\]/s, `"IncludedFiles":[
359
+ ${datafiles.join("\n ")}
360
+ ]`);
361
+ console.log(`📝 Save yyp:`, yypPath);
362
+ fs.writeFileSync(yypPath, replaced, "utf8");
363
+ });
364
+
365
+ program
366
+ .command('make')
367
+ .description('Build and run gamemaker project')
368
+ .option('-t, --target <target>', 'available targets: windows')
369
+ .option('-r, --runtime <type>', 'use VM or YYC runtime')
370
+ .option('-n, --name <name>', 'The actual file name of the ZIP file that is created')
371
+ .option('-l, --launch', 'launch the executable after building')
372
+ .option('-c, --clean', 'make clean build')
373
+ .option('-p, --projectool', 'Path to ProjectTool.exe')
374
+ .action(function() {
375
+ const packageGM = getPackageGM()
376
+ const targetMap = new Map([ [ 'windows', 'win' ] ])
377
+ const options = this.opts();
378
+ const config = {
379
+ runtime: '$GM_CLI_DEFAULT_RUNTIME',
380
+ target: '$GM_CLI_DEFAULT_TARGET',
381
+ projectool: '$GM_CLI_PROJECT_TOOL_PATH',
382
+ clean: 'false',
383
+ launch: 'PackageZip',
384
+ name: packageGM.data.name,
385
+ zip: packageGM.data.name,
386
+ yyp: path.basename(packageGM.data.main).replaceAll("\\", "/"),
387
+ path: path.join(path.dirname(packageGM.file), path.dirname(packageGM.data.main)).replaceAll("\\", "/"),
388
+ };
389
+
390
+ if (options.runtime !== undefined) {
391
+ config.runtime = options.runtime;
392
+ }
393
+
394
+ if (options.target !== undefined && targetMap.has(options.target)) {
395
+ config.target = options.target;
396
+ config.targetExt = targetMap.get(config.target);
397
+ }
398
+
399
+ if (options.projectool !== undefined) {
400
+ config.projectool = options.projectool;
401
+ }
402
+
403
+ if (options.clean !== undefined) {
404
+ config.clean = 'true';
405
+ }
406
+
407
+ if (options.launch !== undefined) {
408
+ config.launch = 'Run';
409
+ }
410
+
411
+ if (options.name !== undefined && typeof options.name === 'string' && options.name.trim() !== '') {
412
+ config.zip = options.name;
413
+ }
414
+
415
+ const shellScript = `#!/bin/bash
416
+ function log_info {
417
+ local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
418
+ echo -e "\\\e[90m$timestamp\\\e[0m \\\e[32mINFO\\\e[0m \\\e[35m[gm-cli::make]\\\e[0m $1"
419
+ }
420
+
421
+ function log_error {
422
+ local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
423
+ echo -e "\\\e[90m$timestamp\\\e[0m \\\e[31mERROR\\\e[0m \\\e[35m[gm-cli::make]\\\e[0m $1"
424
+ }
425
+
426
+ gm_cli_env_path=""
427
+ dir=$(realpath "$PWD")
428
+ while [ "$dir" != "/" ]; do
429
+ if [ -f "$dir/.gm-cli.env" ]; then
430
+ gm_cli_env_path="$dir/.gm-cli.env"
431
+ log_info "Load configuration '$gm_cli_env_path'"
432
+ set -a
433
+ . "$gm_cli_env_path"
434
+ set +a
435
+ break
436
+ fi
437
+ dir=$(dirname "$dir")
438
+ done
439
+
440
+ runtime_path=$GM_CLI_RUNTIME_PATH
441
+ if [ -z "$runtime_path" ]; then
442
+ log_error "GM_CLI_RUNTIME_PATH must be defined! exit 1"
443
+ exit 1
444
+ fi
445
+ runtime_path=$(realpath $runtime_path)
446
+
447
+ igor_path="$\{GM_CLI_RUNTIME_PATH%/\}/bin/igor/windows/x64/Igor.exe"
448
+ if [ -z "$igor_path" ]; then
449
+ log_error "GM_CLI_RUNTIME_PATH must be defined! exit 1"
450
+ exit 1
451
+ fi
452
+ igor_path=$(realpath $igor_path)
453
+
454
+ project_name=${config.name}
455
+ if [ -z "$project_name" ]; then
456
+ log_error "package-gm.json name field must be defined! exit 1"
457
+ exit 1
458
+ fi
459
+
460
+ project_yyp=${config.yyp}
461
+ if [ -z "$project_yyp" ]; then
462
+ log_error "package-gm.json yyp field must be defined! exit 1"
463
+ exit 1
464
+ fi
465
+
466
+ project_path=${config.path}
467
+ if [ -z "$project_path" ]; then
468
+ log_error "package-gm.json yyp field must be defined! exit 1"
469
+ exit 1
470
+ fi
471
+ project_path=$(realpath $project_path)
472
+
473
+ user_path=$GM_CLI_USER_PATH
474
+ if [ -z "$user_path" ]; then
475
+ log_error "GM_CLI_USER_PATH must be defined! exit 1"
476
+ exit 1
477
+ fi
478
+ user_path=$(realpath $user_path)
479
+
480
+ runtime=${config.runtime}
481
+ if [ -z "$runtime" ]; then
482
+ log_error "GM_CLI_DEFAULT_RUNTIME must be defined! exit 1"
483
+ exit 1
484
+ fi
485
+
486
+ target=${config.target}
487
+ if [ -z "$target" ]; then
488
+ log_error "GM_CLI_DEFAULT_TARGET must be defined! exit 1"
489
+ exit 1
490
+ fi
491
+
492
+ zip_name=${config.zip}
493
+ echo $zip_name
494
+ if [ -z "$zip_name" ]; then
495
+ log_error "--name must be defined! exit 1"
496
+ exit 1
497
+ fi
498
+
499
+ project_tool=${config.projectool}
500
+ echo $project_tool
501
+ if [ -z "$project_tool" ]; then
502
+ log_error "--projectool must be defined! exit 1"
503
+ exit 1
504
+ fi
505
+
506
+ clean=${config.clean}
507
+ if [ "$clean" = "true" ]; then
508
+ log_info "Clean '$project_path/tmp/igor'"
509
+ rm -rf $project_path/tmp/igor
510
+
511
+ log_info "Execute shell command:\n\\\e[33m$igor_path \\ \n --runtimePath="$runtime_path" \\ \n --runtime=$runtime \\ \n --project="$\{project_path\}/$\{project_yyp\}" \\ \n --projectool="$\{project_tool\}" \\ \n --uf="$user_path" \\ \n -- $target Clean\n\\\e[0m"
512
+ $igor_path \
513
+ --runtimePath="$runtime_path" \
514
+ --runtime=$runtime \
515
+ --project="$\{project_path\}/$\{project_yyp\}" \
516
+ --projectool="$\{project_tool\}" \
517
+ --uf="$user_path" \
518
+ -- $target Clean | GREP_COLORS='mt=01;31' grep --color=always -E 'Error : |$'
519
+ fi
520
+
521
+ log_info "Clean '$\{project_path\}/tmp/igor/out'"
522
+ rm -rf $\{project_path\}/tmp/igor/out
523
+
524
+ log_info "Execute shell command:\n\\\e[33m$igor_path \\ \n --project="$\{project_path\}/$\{project_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 --projectool="$\{project_tool\}" \\ \n --uf="$user_path" \\ \n -- $target ${config.launch}\\\e[0m"
525
+ $igor_path \
526
+ --project="$\{project_path\}/$\{project_yyp\}" \
527
+ --user="$user_path" \
528
+ --runtimePath="$runtime_path" \
529
+ --runtime=$runtime \
530
+ --cache="$\{project_path\}/tmp/igor/cache" \
531
+ --temp="$\{project_path\}/tmp/igor/temp" \
532
+ --of="$\{project_path\}/tmp/igor/out/$\{project_name\}.win" \
533
+ --tf="$\{zip_name\}.zip" \
534
+ --projectool="$\{project_tool\}" \
535
+ --uf="$user_path" \
536
+ -- $target ${config.launch} | GREP_COLORS='mt=01;31' grep --color=always -E 'Error : |$'
537
+
538
+ exit 0
539
+ `;
540
+
541
+ runShellScript(shellScript)
542
+ });
543
+
544
+ program
545
+ .command('env')
546
+ .description('CLI creator for .gm-cli.env')
547
+ .action(async () => {
548
+ const rl = readline.createInterface({
549
+ input: process.stdin,
550
+ output: process.stdout
551
+ });
552
+ const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
553
+
554
+ try {
555
+ console.log("This utility will walk you through creating a .gm-cli.env file.");
556
+
557
+ const runtimes = [ "VM", "YYC" ]
558
+ const targets = [ "windows" ]
559
+
560
+ const projectPath = process.cwd();
561
+ const propertyDefaultRuntime = await askQuestion('Default runtime [ VM, YYC ]: ');
562
+ const propertyDefaultTarget = await askQuestion('Default target [ windows ]: ');
563
+ const propertyProjectoolPath = await askQuestion('Path to ProjectTool.exe: ');
564
+ const propertyRuntimePath = await askQuestion('Path to gamemaker runtime: ');
565
+ const propertyUserPath = await askQuestion('Path to gamemaker user: ');
566
+ const propertyVsDevCmdPath = await askQuestion('Path to VsDevCmd.bat: ');
567
+ const data = {
568
+ GM_CLI_DEFAULT_RUNTIME: runtimes.includes(propertyDefaultRuntime) ? propertyDefaultRuntime : runtimes[0],
569
+ GM_CLI_DEFAULT_TARGET: runtimes.includes(propertyDefaultTarget) ? propertyDefaultTarget : targets[0],
570
+ GM_CLI_PROJECT_TOOL_PATH: path.normalize(propertyProjectoolPath),
571
+ GM_CLI_RUNTIME_PATH: path.normalize(propertyRuntimePath),
572
+ GM_CLI_USER_PATH: path.normalize(propertyUserPath),
573
+ GM_CLI_VS_DEV_CMD_PATH: path.normalize(propertyVsDevCmdPath),
574
+ };
575
+
576
+ const filePath = path.join(projectPath, '.gm-cli.env');
577
+ const dataString = Object.entries(data)
578
+ .map(([key, value]) => `${key}="${value}"`)
579
+ .join("\n");
580
+
581
+ console.log(`About to write to ${filePath}:\n\n${dataString}\n\n`);
582
+ const response = await askQuestion(`Is this OK? (yes) `)
583
+ if (typeof response === 'string' && (response.includes('y') || response.includes('Y'))) {
584
+ fs.writeFileSync(filePath, dataString, 'utf8');
585
+ } else {
586
+ console.log('Aborted.\n');
587
+ }
588
+ } catch (error) {
589
+ console.error('An error occurred:', error);
590
+ } finally {
591
+ rl.close();
592
+ process.exit(0);
593
+ }
594
+ });
595
+
596
+ program
597
+ .command('monitor-ram')
598
+ .description('Monitor RAM usage and save it to csv file')
599
+ .option('-i, --interval <interval>', 'step value in seconds (default = 15)')
600
+ .option('-n, --name <name>', 'name of binary')
601
+ .option('-r, --report <report>', 'name of CSV report file')
602
+ .action((options) => {
603
+ const packageGM = getPackageGM()
604
+ const interval = clamp((Number.isNaN(Number(options.interval)) ? 15.0 : Number(options.interval)), 1.0 / 60.0, 999.0)
605
+ const name = options.name === undefined ? packageGM.data.name : options.name
606
+ const report = options.report === undefined ? '' : options.report
607
+ const psCommand = getPSMonitorRAMCommand(name, 1000.0 * interval, report)
608
+ const ps = spawn('powershell.exe', [
609
+ '-NoProfile',
610
+ '-Command',
611
+ psCommand
612
+ ]);
613
+
614
+ ps.stdout.on('data', data => {
615
+ console.log(data.toString().replace(/\r?\n$/, ''));
616
+ });
617
+
618
+ ps.stderr.on('data', data => {
619
+ console.error(data.toString().replace(/\r?\n$/, ''));
620
+ });
621
+
622
+ ps.on('close', code => {
623
+ console.log(`Exited with code ${code}`);
624
+ });
625
+ })
626
+
627
+ program
628
+ .command('test')
629
+ .description('Run tests')
630
+ .option('-t, --tests <tests>', 'List of paths to json test cases')
631
+ .option('-b, --build <build>', 'Path to executable')
632
+ .option('-m, --monitorRAM', 'Monitor RAM while testing')
633
+ .action((options) => {
634
+ const packageGM = getPackageGM()
635
+
636
+ const shellMonitorRAMScript = options.monitorRAM === undefined ? `` : `
637
+ set -m
638
+ gm-cli monitor-ram --name \$\{EXE_FILE%.exe\} &
639
+ pid=\$!
640
+ trap "kill -- -\$pid 2>/dev/null" EXIT INT TERM
641
+ `;
642
+
643
+ const shellBuildScript = options.build !== undefined ? `
644
+ cd ${path.dirname(options.build).replaceAll("\\", "/")}
645
+ build_name="\$\{PWD##*/\}"
646
+
647
+ EXE_FILE="${path.basename(options.build)}"
648
+ EXE_COUNT=1
649
+ ` : `
650
+ build_name="${packageGM.data.name}_test"
651
+ rm -rf \$build_name.zip
652
+ rm -rf \$build_name
653
+ gm-cli make --name \$build_name
654
+ unzip \$build_name.zip -d \$build_name
655
+ cd \$build_name
656
+
657
+ EXE_FILE=\$(find . -maxdepth 1 -type f -name "*.exe" -printf "%f\n" 2>/dev/null)
658
+ EXE_COUNT=\$(printf "%s\n" "\$EXE_FILE" | grep -c .)
659
+ `;
660
+
661
+ const shellTestScript = options.tests === undefined ? `
662
+ TESTS=\$(find . -type f -name "*test.json" -print0 | xargs -0 echo | sed 's/ /, /g')
663
+ ` : `
664
+ TESTS=\"${options.tests}\"
665
+ `
666
+
667
+ const shellCommandScript = `
668
+ TIMESTAMP=\$(date +"%Y-%m-%d_%H-%M")
669
+ OUTPUT_FILE="\$\{TIMESTAMP\}_\$\{EXE_FILE%.exe\}_run-test.log"
670
+ COMMAND="./\$EXE_FILE -output \"\$OUTPUT_FILE\" --tests \\\"\$TESTS\\\""
671
+
672
+ if [ "\$EXE_COUNT" -eq 0 ]; then
673
+ echo "ERROR 1: binary was not found"
674
+ exit 1
675
+ elif [ "\$EXE_COUNT" -gt 1 ]; then
676
+ echo "ERROR 2: found more executables: \$EXE_FILE"
677
+ exit 2
678
+ fi
679
+ `;
680
+
681
+ const shellScript = `#!/bin/bash
682
+ cd ${path.dirname(packageGM.file).replaceAll("\\", "/")}
683
+
684
+ ${shellBuildScript}
685
+
686
+ ${shellTestScript}
687
+
688
+ ${shellCommandScript}
689
+
690
+ ${shellMonitorRAMScript}
691
+
692
+ eval "\$COMMAND" | cat
693
+ `
694
+
695
+ runShellScript(shellScript)
696
+ })
697
+
698
+ configSet
699
+ .command('dependency <name> <revision>')
700
+ .description('Manage dependencies in package-gm.json')
701
+ .option('--remote <remote>')
702
+ .action((name, revision, options) => {
703
+ const resolve = () => {
704
+ const current = packageGM.data.dependencies[name] ?? {};
705
+
706
+ packageGM.data.dependencies[name] = {
707
+ ...current,
708
+ ...(options.remote !== undefined && { remote: options.remote }),
709
+ ...(revision !== undefined && { revision }),
710
+ };
711
+
712
+ console.log("🔨 Set dependency", name, "as", packageGM.data.dependencies[name])
713
+ };
714
+
715
+ const packageGM = getPackageGM()
716
+
717
+ backupPackageGM(packageGM)
718
+ resolve()
719
+ savePackageGM(packageGM)
720
+ });
721
+
722
+ configSet
723
+ .command('script <name> [command]')
724
+ .description('Manage scripts in package-gm.json')
725
+ .action((name, command = '') => {
726
+ const resolve = () => {
727
+ packageGM.data.scripts[name] = command !== undefined ? command : ''
728
+ console.log("🔨 Set script", name, "as", packageGM.data.scripts[name])
729
+ }
730
+
731
+ const packageGM = getPackageGM()
732
+
733
+ backupPackageGM(packageGM)
734
+ resolve()
735
+ savePackageGM(packageGM)
736
+ });
737
+
738
+ configSet
739
+ .command('runtime <name> [supported]')
740
+ .description('Manage runtimes in package-gm.json')
741
+ .action((name, supported = 'true') => {
742
+ const resolve = () => {
743
+ packageGM.data.runtimes[name] = supported === "false" ? supported : "true"
744
+ console.log("🔨 Set runtime", name, "as", supported === "false" ? "false" : "true")
745
+ }
746
+
747
+ const packageGM = getPackageGM()
748
+
749
+ backupPackageGM(packageGM)
750
+ resolve()
751
+ savePackageGM(packageGM)
752
+ });
753
+
754
+ configUnset
755
+ .command('dependency <name>')
756
+ .description('Remove dependencies from package-gm.json')
757
+ .action((name) => {
758
+ const resolve = () => {
759
+ if (name in packageGM.data.dependencies) {
760
+ console.log("🗑️ Unset dependency", name)
761
+ delete packageGM.data.dependencies[name]
762
+ }
763
+ }
764
+
765
+ const packageGM = getPackageGM()
766
+
767
+ backupPackageGM(packageGM)
768
+ resolve()
769
+ savePackageGM(packageGM)
770
+ });
771
+
772
+ configUnset
773
+ .command('script <name>')
774
+ .description('Remove scripts from package-gm.json')
775
+ .action((name) => {
776
+ const resolve = () => {
777
+ if (name in packageGM.data.scripts) {
778
+ console.log("🗑️ Unset script", name)
779
+ delete packageGM.data.scripts[name]
780
+ }
781
+ }
782
+
783
+ const packageGM = getPackageGM()
784
+
785
+ backupPackageGM(packageGM)
786
+ resolve()
787
+ savePackageGM(packageGM)
788
+ });
789
+
790
+ configUnset
791
+ .command('runtime <name>')
792
+ .description('Remove runtimes from package-gm.json')
793
+ .action((name) => {
794
+ const resolve = () => {
795
+ if (name in packageGM.data.scripts) {
796
+ console.log("🗑️ Unset runtime", name)
797
+ delete packageGM.data.runtimes[name]
798
+ }
799
+ }
800
+
801
+ const packageGM = getPackageGM()
802
+
803
+ backupPackageGM(packageGM)
804
+ resolve()
805
+ savePackageGM(packageGM)
806
+ });
807
+
808
+ resource
809
+ .command("create")
810
+ .description("Create a resource")
811
+ .requiredOption("-t, --type <type>", "Resource type")
812
+ .requiredOption("-n, --name <name>", "Resource name")
813
+ .option("-f, --folder <folder>", "Resource folder")
814
+ .action((options) => {
815
+ const packageGM = getPackageGM()
816
+ const yypPath = getYYPPathFromPackageGM(packageGM)
817
+ const folderOption = options.folder !== undefined ? `folder=${options.folder}` : ``
818
+ const shellScript = `yy-gm-cli resourcetool eval "resource create type=${options.type} name=${options.name} ${folderOption}" ${yypPath}`
819
+ runShellScript(shellScript)
820
+ });
821
+
822
+ resource
823
+ .command("update")
824
+ .description("Update a resource property")
825
+ .requiredOption("-e, --expr <expr>", "Resource expression")
826
+ .requiredOption("-v, --value <value>", "New value")
827
+ .action((options) => {
828
+ const packageGM = getPackageGM()
829
+ const yypPath = getYYPPathFromPackageGM(packageGM)
830
+ const shellScript = `yy-gm-cli resourcetool eval "resource set expr=${options.expr} value=${options.value}" ${yypPath}`
831
+ runShellScript(shellScript)
832
+ });
833
+
834
+ resource
835
+ .command("get")
836
+ .description(" a resource")
837
+ .requiredOption("-e, --expr <expr>", "Resource expression")
838
+ .action((options) => {
839
+ const packageGM = getPackageGM()
840
+ const yypPath = getYYPPathFromPackageGM(packageGM)
841
+ const shellScript = `yy-gm-cli resourcetool eval "resource info expr=${options.expr}" ${yypPath}`
842
+ runShellScript(shellScript)
843
+ });
844
+
845
+ resource
846
+ .command("delete")
847
+ .description("Delete a resource")
848
+ .requiredOption("-n, --name <name>", "Resource name")
849
+ .option("--type <type>", "Resource type")
850
+ .action((options) => {
851
+ const packageGM = getPackageGM()
852
+ const yypPath = getYYPPathFromPackageGM(packageGM)
853
+ const typeOptions = options.type !== undefined ? `type=${options.type}` : ``
854
+ const shellScript = `yy-gm-cli resourcetool eval "resource delete name=${options.name} ${typeOptions}" ${yypPath}`
855
+ runShellScript(shellScript)
856
+ });
857
+
858
+ resource
859
+ .command("list")
860
+ .description("List resources")
861
+ .option("--type <type>", "Resource type")
862
+ .action((options) => {
863
+ const packageGM = getPackageGM()
864
+ const yypPath = getYYPPathFromPackageGM(packageGM)
865
+ const typeOptions = options.type !== undefined ? `type=${options.type}` : ``
866
+ const shellScript = `yy-gm-cli resourcetool eval "resource list ${typeOptions}" ${yypPath}`
867
+ runShellScript(shellScript)
868
+ });
869
+
870
+
871
+ program.parse(process.argv);