@@ -910,6 +910,189 @@ cd "$(dirname "$0")"
910910 return zip ;
911911 }
912912
913+ async addNodeCLI ( projectZip ) {
914+ const packageName = this . options . app . packageName ;
915+ validatePackageName ( packageName ) ;
916+
917+ const zip = new ( await getJSZip ( ) ) ;
918+
919+ // Get the SB3 file from the original project array buffer
920+ const sb3Buffer = this . project . arrayBuffer ;
921+
922+ // Convert SB3 buffer to base64
923+ const sb3Base64 = Buffer . from ( sb3Buffer ) . toString ( 'base64' ) ;
924+
925+ // Determine pkg target based on user selection
926+ let pkgTarget ;
927+ if ( this . options . target === 'node-cli-win64' ) {
928+ pkgTarget = 'node18-win-x64' ;
929+ } else if ( this . options . target === 'node-cli-mac' ) {
930+ pkgTarget = 'node18-macos-x64' ;
931+ } else if ( this . options . target === 'node-cli-linux64' ) {
932+ pkgTarget = 'node18-linux-x64' ;
933+ }
934+
935+ // Create package.json
936+ const packageJson = {
937+ name : packageName ,
938+ version : this . options . app . version ,
939+ description : `${ packageName } - Scratch CLI Application` ,
940+ main : 'main.js' ,
941+ bin : {
942+ [ packageName ] : 'main.js'
943+ } ,
944+ scripts : {
945+ 'build' : 'pkg .'
946+ } ,
947+ dependencies : {
948+ 'scratch-vm' : 'github:02engine/scratch-vm#main' ,
949+ '@turbowarp/scratch-storage' : '^0.0.202505311821' ,
950+ 'jsdom' : '^24.0.0' ,
951+ 'pkg' : '^5.8.1'
952+ } ,
953+ pkg : {
954+ targets : [ pkgTarget ]
955+ }
956+ } ;
957+ zip . file ( 'package.json' , JSON . stringify ( packageJson , null , 2 ) ) ;
958+
959+ // Create main.js with CLI API and scratch-vm integration
960+ const mainJS = `#!/usr/bin/env node
961+ const fs = require('fs');
962+ const path = require('path');
963+ const VirtualMachine = require('scratch-vm');
964+
965+ // Parse command line arguments
966+ const parseArgs = () => {
967+ const args = process.argv.slice(2);
968+ const parsed = {};
969+ for (let i = 0; i < args.length; i++) {
970+ if (args[i].startsWith('--')) {
971+ const key = args[i].slice(2);
972+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
973+ parsed[key] = args[i + 1];
974+ i++;
975+ } else {
976+ parsed[key] = true;
977+ }
978+ }
979+ }
980+ return parsed;
981+ };
982+
983+ const commandLineArgs = parseArgs();
984+
985+ // CLI API
986+ const cli = {
987+ log: (...args) => {
988+ console.log(...args);
989+ },
990+ error: (...args) => {
991+ console.error('[CLI ERROR]', ...args);
992+ },
993+ warn: (...args) => {
994+ console.warn('[CLI WARNING]', ...args);
995+ },
996+ info: (...args) => {
997+ console.info('[CLI INFO]', ...args);
998+ },
999+ exit: (code) => {
1000+ console.log('[CLI] Exiting with code:', code || 0);
1001+ process.exit(code || 0);
1002+ },
1003+ getArgs: () => {
1004+ return commandLineArgs;
1005+ },
1006+ getArg: (key) => {
1007+ return commandLineArgs[key];
1008+ }
1009+ };
1010+
1011+ // Expose CLI API globally for Scratch projects
1012+ global.cli = cli;
1013+
1014+ // Wait for all threads to complete
1015+ const waitForCompletion = (vm) => {
1016+ return new Promise((resolve) => {
1017+ const checkInterval = setInterval(() => {
1018+ const activeThreads = vm.runtime.threads.filter(thread => !thread.updateMonitor);
1019+ if (activeThreads.length === 0) {
1020+ clearInterval(checkInterval);
1021+ vm.stopAll();
1022+ vm.quit();
1023+ resolve();
1024+ }
1025+ }, 100);
1026+ });
1027+ };
1028+
1029+ async function runSB3(base64Data) {
1030+ // Decode base64 to buffer
1031+ const buffer = Buffer.from(base64Data, 'base64');
1032+
1033+ // Initialize VM
1034+ const vm = new VirtualMachine();
1035+
1036+ // Listen to Scratch events
1037+ vm.runtime.on('SAY', (target, type, text) => {
1038+ cli.log(\`\${text}\`);
1039+ });
1040+
1041+ // Load project
1042+ await vm.loadProject(buffer);
1043+
1044+ // Start VM and trigger green flag
1045+ vm.start();
1046+ vm.greenFlag();
1047+
1048+ // Wait for all threads to complete
1049+ await waitForCompletion(vm);
1050+ }
1051+
1052+ // Embedded SB3 project (base64 encoded)
1053+ const embeddedProject = '${ sb3Base64 } ';
1054+
1055+ // Run the embedded project
1056+ runSB3(embeddedProject).catch(err => {
1057+ cli.error('Execution failed:', err);
1058+ cli.exit(1);
1059+ });
1060+ ` ;
1061+
1062+ zip . file ( 'main.js' , mainJS ) ;
1063+
1064+ // Add README
1065+ const readme = `${ packageName } - Scratch CLI Application
1066+
1067+ Usage:
1068+ ${ packageName } [options]
1069+
1070+ Options:
1071+ --arg value Pass arguments to the application
1072+ --flag Enable a flag
1073+
1074+ CLI API:
1075+ cli.log(message) - Output text to console
1076+ cli.error(message) - Output error to console
1077+ cli.warn(message) - Output warning to console
1078+ cli.info(message) - Output info to console
1079+ cli.exit(code) - Exit the application
1080+ cli.getArgs() - Get all command line arguments
1081+ cli.getArg(key) - Get specific command line argument
1082+
1083+ Example:
1084+ ${ packageName } --mode test --verbose
1085+
1086+ Important Notes:
1087+ - The SB3 project is embedded in the executable, no external files needed
1088+ - After running 'npm run build', the executable will be created in the same folder
1089+ - Simply run the executable to start your project
1090+ ` ;
1091+ zip . file ( 'README.txt' , readme ) ;
1092+
1093+ return zip ;
1094+ }
1095+
9131096 async addWebViewMac ( projectZip ) {
9141097 validatePackageName ( this . options . app . packageName ) ;
9151098
@@ -1846,6 +2029,8 @@ For detailed setup instructions, refer to the Cordova documentation.`;
18462029 zip = await this . addWebViewMac ( zip ) ;
18472030 } else if ( this . options . target === 'cordova-android' ) {
18482031 zip = await this . addCordovaAndroid ( zip ) ;
2032+ } else if ( this . options . target . startsWith ( 'node-cli-' ) ) {
2033+ zip = await this . addNodeCLI ( zip ) ;
18492034 }
18502035
18512036 this . ensureNotAborted ( ) ;
@@ -1992,6 +2177,9 @@ Packager.DEFAULT_OPTIONS = () => ({
19922177 // 'ignore' (no alert), 'warning' (alert and continue), or 'error' (alert and exit)
19932178 onError : 'warning'
19942179 } ,
2180+ nodeCli : {
2181+ exposeSystemAPIs : false
2182+ } ,
19952183 extensions : [ ] ,
19962184 bakeExtensions : true ,
19972185 maxTextureDimension : 2048
0 commit comments