@crashcontinuum/crashcart-packager 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.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # crashcart-packager
2
+
3
+ Package Crash BASIC games for distribution on Windows, macOS, and Linux.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx crashcart-packager mygame.crashcart "My Game"
9
+ ```
10
+
11
+ This will:
12
+ 1. Stamp your CrashCart with your Crash BASIC Arcade license
13
+ 2. Download CrashPlayer for your platform
14
+ 3. Create a ready-to-distribute package
15
+
16
+ ## Options
17
+
18
+ ```
19
+ Usage: crashcart-packager [options] <crashcart> <game-name>
20
+
21
+ Arguments:
22
+ crashcart Path to the .crashcart file
23
+ game-name Name of your game
24
+
25
+ Options:
26
+ -p, --platform <platform> Target platform (windows, macos, linux)
27
+ -i, --icon <path> Path to icon file (PNG for best results)
28
+ -o, --output <dir> Output directory (default: ".")
29
+ --skip-stamp Skip the stamping step (if already stamped)
30
+ --all Build for all platforms
31
+
32
+ macOS Code Signing:
33
+ --apple-team-id <id> Apple Developer Team ID
34
+ --signing-identity <id> Code signing identity
35
+ --apple-id <email> Apple ID for notarization
36
+ --apple-password <pass> App-specific password for notarization
37
+
38
+ -h, --help Display help
39
+ ```
40
+
41
+ ## Examples
42
+
43
+ ### Package for current platform
44
+ ```bash
45
+ npx crashcart-packager mygame.crashcart "My Awesome Game"
46
+ ```
47
+
48
+ ### Package for all platforms
49
+ ```bash
50
+ npx crashcart-packager mygame.crashcart "My Awesome Game" --all
51
+ ```
52
+
53
+ ### Package with custom icon
54
+ ```bash
55
+ npx crashcart-packager mygame.crashcart "My Awesome Game" --icon icon.png
56
+ ```
57
+
58
+ ### Package for a specific platform
59
+ ```bash
60
+ npx crashcart-packager mygame.crashcart "My Awesome Game" --platform windows
61
+ ```
62
+
63
+ ## Output
64
+
65
+ The packager creates platform-specific bundles:
66
+
67
+ - **Windows**: Folder containing CrashPlayer.exe, your game, and launcher scripts (.bat and .vbs)
68
+ - **macOS**: A proper .app bundle that can be distributed directly
69
+ - **Linux**: Folder containing CrashPlayer, your game, launcher script, and .desktop file
70
+
71
+ ## macOS Code Signing & Notarization
72
+
73
+ For your macOS app to run without security warnings, it must be signed and notarized. The packager will automatically sign and notarize if you provide Apple Developer credentials.
74
+
75
+ ### Setup
76
+
77
+ 1. **Join the Apple Developer Program** at https://developer.apple.com/programs/
78
+
79
+ 2. **Create a Developer ID Application certificate**
80
+ - Open Keychain Access on your Mac
81
+ - Go to developer.apple.com → Certificates
82
+ - Create a "Developer ID Application" certificate
83
+ - Download and install it in your keychain
84
+
85
+ 3. **Create an app-specific password**
86
+ - Go to appleid.apple.com → Sign-In and Security
87
+ - Click "App-Specific Passwords" → Generate
88
+
89
+ 4. **Find your credentials**
90
+ - **Team ID**: developer.apple.com → Membership → Team ID
91
+ - **Signing Identity**: Run `security find-identity -v -p codesigning`
92
+ Look for "Developer ID Application: Your Name (TEAM_ID)"
93
+
94
+ ### Using credentials
95
+
96
+ Via command line options:
97
+ ```bash
98
+ npx crashcart-packager mygame.crashcart "My Game" \
99
+ --apple-team-id "YOUR_TEAM_ID" \
100
+ --signing-identity "Developer ID Application: Your Name (TEAM_ID)" \
101
+ --apple-id "your@email.com" \
102
+ --apple-password "xxxx-xxxx-xxxx-xxxx"
103
+ ```
104
+
105
+ Or via environment variables:
106
+ ```bash
107
+ export APPLE_TEAM_ID="YOUR_TEAM_ID"
108
+ export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
109
+ export APPLE_ID="your@email.com"
110
+ export APPLE_PASSWORD="xxxx-xxxx-xxxx-xxxx"
111
+
112
+ npx crashcart-packager mygame.crashcart "My Game"
113
+ ```
114
+
115
+ ## Requirements
116
+
117
+ - Node.js 18 or later
118
+ - A Crash BASIC Arcade account with an active subscription (Indie or Studio tier)
119
+ - On Windows: PowerShell (for ZIP extraction)
120
+ - On Unix: unzip command (for cross-platform Windows builds)
121
+ - On macOS (for signing): Apple Developer Program membership, Developer ID certificate
122
+
123
+ ## License
124
+
125
+ © 2026 Crash Continuum LLC. All rights reserved.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,739 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import pngToIco from 'png-to-ico';
6
+ import { spawn, execSync } from 'child_process';
7
+ import { createWriteStream, existsSync, mkdirSync, rmSync, copyFileSync, writeFileSync, chmodSync, readFileSync } from 'fs';
8
+ import { join, basename, dirname } from 'path';
9
+ import { pipeline } from 'stream/promises';
10
+ import { createGunzip } from 'zlib';
11
+ import { extract } from 'tar-stream';
12
+ import { createReadStream } from 'fs';
13
+ import { Readable } from 'stream';
14
+ const DEFAULT_ARCADE_URL = 'https://arcade.crashbasic.com';
15
+ // Crash-tan ASCII art data (color, char pairs)
16
+ function printCrashTan() {
17
+ // Helper to convert hex to RGB
18
+ const hex = (h) => {
19
+ const r = parseInt(h.slice(0, 2), 16);
20
+ const g = parseInt(h.slice(2, 4), 16);
21
+ const b = parseInt(h.slice(4, 6), 16);
22
+ return chalk.rgb(r, g, b);
23
+ };
24
+ // Art data: each line is an array of [hexColor, char] pairs
25
+ const art = [
26
+ [['808080', ' '], ['685f6a', '╔'], ['776a77', ','], ['7c767a', '_ '], ['7a787a', '_'], ['736d74', ',']],
27
+ [['808080', ' '], ['767477', '`'], ['52385d', '╫'], ['632968', '╣'], ['763a6c', '▒'], ['825371', '╦'], ['796b75', ', '], ['55415f', '╟'], ['562362', '▌'], ['6f4a6d', '▒']],
28
+ [['808080', ' '], ['736f75', ','], ['716173', '╓'], ['705471', '╗'], ['6b496c', '#'], ['72476d', '▒'], ['79456e', '▒'], ['7f466f', '▒'], ['864a72', '▒'], ['844d71', '╦'], ['7d506f', '╗'], ['775a72', '╔'], ['6e646f', '╓'], ['4c3857', '╫'], ['572364', '╬'], ['5e2566', '╣'], ['772e6c', '╬'], ['7a3e6e', '╠'], ['756b75', ', '], ['5d5164', '#'], ['3f1c55', '▓'], ['3e1854', '▓'], ['522160', '▓'], ['624968', '▒']],
29
+ [['808080', ' '], ['747175', '`'], ['6b6570', '"'], ['605368', '╙'], ['543d60', '▀'], ['4d295c', '╣'], ['4f215e', '▓'], ['562263', '╬'], ['5c2466', '╣'], ['642667', '╣'], ['6c286a', '╣'], ['6d296b', '╬'], ['672869', '╬'], ['53215e', '╣'], ['461c58', '▓'], ['592365', '╣'], ['592364', '╣'], ['542162', '▓'], ['482457', '▓'], ['4a3855', '▓'], ['3c1952', '▓'], ['3b1752', '▓'], ['391751', '▓'], ['4a1e5b', '▓'], ['522f5b', '╬'], ['767076', ','], ['797676', '_'], ['7c7a79', '_'], ['7d7c7b', '_']],
30
+ [['808080', ' '], ['797679', '_'], ['726975', ','], ['6f5c71', '╔'], ['6d5370', '╗'], ['644367', '║'], ['502556', '╬'], ['531f5d', '▓'], ['5b2362', '╣'], ['55205f', '▓'], ['501f5e', '▓'], ['532060', '╬'], ['582364', '╬'], ['532061', '╣'], ['4d1f5e', '▓'], ['552262', '╣'], ['542161', '╣'], ['491c59', '▓'], ['491e54', '▓'], ['491f4f', '▓'], ['51264c', '╣'], ['562b4c', '╣'], ['68405a', '╩'], ['6d4e57', '╜'], ['725955', '╙'], ['76634e', '╙'], ['705e47', '╠'], ['6f5c47', '╚'], ['5c4037', '╣'], ['4b3531', '▓'], ['57433e', '▌'], ['645551', '▄'], ['6f6765', '╓'], ['7a7978', '_ '], ['7a7479', '_'], ['757375', '.']],
31
+ [['808080', ' '], ['736775', '╓'], ['69486d', '#'], ['6d356b', '╬'], ['742c6d', '╬'], ['792c6f', '╬'], ['7b2c6f', '╬'], ['7a2c6f', '╬'], ['782b6e', '╬'], ['752a6c', '╬'], ['742a6d', '╬'], ['6e286b', '╬'], ['612466', '╣'], ['572162', '╣'], ['4d1d5b', '▓'], ['3f1853', '▓'], ['4a1d5a', '▓'], ['4f205a', '▓'], ['4c204a', '▓'], ['421d32', '▓'], ['3b1b20', '█'], ['391b18', '█'], ['2b1414', '█'], ['50312b', '▓'], ['5e4539', '╬'], ['93825c', ','], ['95845d', ','], ['92845e', ','], ['8b7f5c', '┐'], ['605744', '╨'], ['7b6d52', '['], ['583930', '╣'], ['3c221e', '▓'], ['351918', '█'], ['361a18', '█'], ['381b19', '█'], ['3e211e', '▓'], ['4c2e2f', '▓'], ['6e3b54', '▄'], ['7b416c', '▒'], ['7c416c', '▒'], ['7d416e', '▒'], ['793c6e', '▒'], ['683268', '╣'], ['5f4462', '╩']],
32
+ [['808080', ' '], ['6b5e70', '╔'], ['573064', '╣'], ['562465', '╣'], ['592365', '╬'], ['5d2466', '╣'], ['5e2466', '╣'], ['5e2466', '╣'], ['5a2265', '╣'], ['521f5f', '▓'], ['501f5d', '▓'], ['542060', '╣'], ['521f5e', '▓'], ['481c5a', '▓'], ['3e1854', '▓'], ['3a1650', '▓'], ['451b57', '▓'], ['491e4f', '▓'], ['401c2d', '▓'], ['3b1c1a', '█'], ['3a1c19', '█'], ['371a18', '█'], ['331816', '█'], ['2d1514', '█'], ['241112', '█'], ['513127', '▓'], ['6d4b2f', '╬'], ['786143', '▒'], ['78654e', '▄'], ['715a4f', '▄'], ['68534d', '▄'], ['6a514e', '▄'], ['654a4a', '▄'], ['4d2c32', '▓'], ['46213f', '▓'], ['4e1e59', '▓'], ['4d1d56', '▓'], ['491c53', '▓'], ['481c51', '▓'], ['491d51', '▓'], ['51204f', '╬'], ['5f2557', '╬'], ['63265d', '╬'], ['592b55', '╬'], ['655367', '▒']],
33
+ [['808080', ' '], ['777677', '`'], ['6b646f', '"'], ['6f6973', '`'], ['6f6973', '`'], ['6d6672', '`'], ['645b6a', 'T'], ['513c5a', '╫'], ['4a1e59', '▓'], ['542162', '╬'], ['572263', '╬'], ['582264', '╬'], ['582263', '╬'], ['4e1f5e', '▓'], ['3c1753', '▓'], ['331348', '▓'], ['351543', '▓'], ['341626', '█'], ['381b18', '█'], ['381b18', '█'], ['321816', '█'], ['2c1415', '█'], ['231211', '█'], ['372219', '█'], ['634927', '▀'], ['996f3a', '▒'], ['b0783d', '░'], ['a56c36', '▒'], ['411e48', '▓'], ['4e1d5d', '▓'], ['5a245c', '╣'], ['58274c', '╣'], ['662565', '╬'], ['782c6a', '╬'], ['732a69', '╬'], ['73296a', '╬'], ['7b2c6b', '╬'], ['822f6c', '╬'], ['732964', '╣'], ['672562', '╬'], ['762a6a', '╬'], ['702969', '╬'], ['6a2768', '╬'], ['632567', '╣'], ['752c6b', '╣'], ['8d3770', '▒'], ['89496f', '▒'], ['776270', '╔'], ['7c787b', '_']],
34
+ [['808080', ' '], ['67586b', '╗'], ['572c62', '╣'], ['572265', '╬'], ['502060', '▓'], ['501f60', '▓'], ['5b2366', '╬'], ['572263', '╣'], ['451b59', '▓'], ['381650', '▓'], ['311248', '█'], ['1d0b19', '█'], ['220f14', '█'], ['311616', '█'], ['2c1414', '█'], ['301b17', '█'], ['513924', '▓'], ['7d5731', 'Ñ'], ['a7733b', 'Ü'], ['ac7138', '▒'], ['a96832', '▒'], ['af6630', '▒'], ['97572e', '╠'], ['52253d', '▓'], ['4d1c5c', '▓'], ['5b245d', '▌'], ['8c5f40', 'Ö'], ['945b36', '╠'], ['55205f', '▓'], ['562160', '▓'], ['602465', '╣'], ['612466', '╣'], ['602465', '╣'], ['5b2164', '╣'], ['5d2c4c', '╬'], ['653430', '╫'], ['541e5c', '▓'], ['692767', '╣'], ['5b2261', '╣'], ['6d2869', '╬'], ['622567', '╣'], ['5d2365', '╣'], ['612567', '╣'], ['712a6b', '╣'], ['7c346d', '╠'], ['78446d', '▒'], ['7a5973', '╔'], ['756775', '╓'], ['79787a', '_']],
35
+ [['808080', ' '], ['593d63', '╢'], ['4a1f5d', '▓'], ['3c1853', '▓'], ['35154d', '▓'], ['3b1750', '▓'], ['421955', '▓'], ['3d1853', '▓'], ['381650', '▓'], ['36154e', '▓'], ['33144c', '▓'], ['2d1141', '█'], ['1d0c15', '█'], ['2c1415', '█'], ['2a0f16', '█'], ['725631', 'Ñ'], ['b28145', '░'], ['ba7f42', '░'], ['b36e36', '▒'], ['ac6530', 'Ö'], ['b06430', '▒'], ['ad6330', '▒'], ['92562e', '╟'], ['38134b', '▓'], ['491e52', '▓'], ['643d42', '╬'], ['81613c', '╩'], ['886f3f', 'K'], ['a27c43', '░'], ['734339', '╢'], ['4b1a5a', '▓'], ['5a2363', '╬'], ['5c2464', '╣'], ['572261', '╣'], ['511a61', '▓'], ['876242', 'Ü'], ['b37e41', '░'], ['865032', '╠'], ['491d50', '▓'], ['481b58', '▓'], ['582262', '╬'], ['5c2465', '╣'], ['592364', '╣'], ['582264', '╣'], ['501f5b', '▓'], ['533959', '╬'], ['65566b', '╙'], ['695e6e', '"'], ['726a74', '`']],
36
+ [['808080', ' '], ['564b5e', '║'], ['3c224e', '▓'], ['493956', '▀'], ['5a5062', '╨'], ['443150', '╫'], ['34154c', '▓'], ['32144b', '▓'], ['32144b', '▓'], ['33144b', '▓'], ['35143f', '▓'], ['a63f11', '╬'], ['a23b09', '╣'], ['161315', '█'], ['3f1e18', '█'], ['3b1616', '█'], ['5a3a24', '▓'], ['885730', '▄'], ['9b5d2e', '▒'], ['a75f2e', '▒'], ['a45e2e', '▒'], ['93552c', '╢'], ['5a342b', '▓'], ['653b31', '▀'], ['93663d', 'Ü'], ['b18447', '»'], ['b08749', '»'], ['a37e45', '*'], ['a88348', '░'], ['b0894b', '»'], ['b38849', '»'], ['694437', '╫'], ['41175b', '▓'], ['542657', '▓'], ['3c2147', '▓'], ['7f5944', 'Ö'], ['ac8649', '»'], ['a17e45', '░'], ['ac8145', '░'], ['a26a38', '╚'], ['411c3a', '▓'], ['4f1e5d', '▓'], ['582363', '╣'], ['572263', '╣'], ['542161', '▓'], ['50205f', '▓'], ['592564', '╣'], ['69596c', 'm']],
37
+ [['808080', ' '], ['777678', '` '], ['5f5665', '▐'], ['37164f', '▓'], ['34174a', '▓'], ['2c1540', '█'], ['2c1247', '█'], ['571a21', '▓'], ['c44b0a', '╬'], ['c94609', '╣'], ['c24108', '╬'], ['6b230b', '▓'], ['301712', '█'], ['371a17', '█'], ['3a1c17', '█'], ['371917', '█'], ['2f1317', '█'], ['251019', '█'], ['200e14', '█'], ['230d3b', '█'], ['61313a', '▌'], ['aa7337', '╗'], ['775b30', '╣'], ['6c5831', '▀'], ['6c5c34', '▀'], ['776035', 'Φ'], ['9a7c45', '╦'], ['b18b4c', ':'], ['b28b4c', ':'], ['977748', '7'], ['755441', 'Ñ'], ['a98449', '└'], ['ab874a', '»'], ['705b33', '╫'], ['423d32', '▀'], ['383631', '▓'], ['5d584a', '╩'], ['654b2b', '▌'], ['a66733', '╠'], ['501f5c', '▓'], ['52205f', '▓'], ['542162', '▓'], ['552262', '╣'], ['4a2955', '╬'], ['442455', '▓'], ['4e2a5c', '▌']],
38
+ [['808080', ' '], ['584b5f', '╠'], ['4d3a58', '▀'], ['726e72', '`'], ['584149', '╢'], ['9c3914', '╬'], ['dc540b', '╬'], ['ed600b', '╠'], ['e95f0b', '╠'], ['d8550a', '╬'], ['c54609', '╣'], ['af3808', '╬'], ['952f09', '╬'], ['8a2b0a', '▓'], ['842909', '▓'], ['2b0d26', '█'], ['260f38', '█'], ['210d22', '█'], ['411853', '▓'], ['6a3740', '╬'], ['bb7b3b', '░'], ['be773b', 'µ'], ['c2793d', '░'], ['bc8042', '░'], ['b58647', '»'], ['a88348', '└'], ['b18a4c', ':'], ['b28a4c', ':'], ['b28a4c', ':'], ['b38a4b', ':'], ['b28a4b', ':'], ['8d7443', '▐'], ['23353f', '▓'], ['353e48', '▓'], ['47494a', '╬ '], ['61513d', '╢'], ['663c34', '╫'], ['3e1551', '▓'], ['471c58', '▓'], ['4d1e5d', '▓'], ['542262', '╣'], ['624c67', '▒'], ['757376', "'"], ['514558', '╩']],
39
+ [['808080', ' '], ['9f6842', '1'], ['e46210', '╠'], ['f0620c', '╠'], ['f0620c', '╠'], ['f0620c', '╠'], ['ef610b', '╠'], ['ed5f0b', '╠'], ['e5590b', '╬'], ['c94509', '╬'], ['b23808', '╬'], ['731f0a', '▓'], ['26091d', '█'], ['19092d', '█'], ['200c28', '█'], ['240d34', '█'], ['3c1750', '▓'], ['4d1d5b', '▓'], ['8e5839', '▒'], ['c3773a', '░'], ['c3773b', '░'], ['bf7d3f', '░'], ['b78545', '░'], ['ab8448', '░'], ['ad8749', '░'], ['b1894a', ':'], ['b38a4b', ':'], ['b4894a', ':'], ['b4894a', ':'], ['a18049', '░'], ['696b50', '╚'], ['5b5b47', '╩'], ['8c7860', ','], ['a47f53', '='], ['9d723e', '▐'], ['28123c', '█'], ['220e32', '█'], ['3a174f', '▓'], ['4a215a', '▓'], ['5f4a66', '╩']],
40
+ [['808080', ' '], ['7d7570', ','], ['c06022', '╠'], ['ed620c', '╠'], ['ef620b', '╠'], ['f0630b', '╠'], ['ef620b', '╠'], ['ed5f0b', '╠'], ['e3580b', '╢'], ['d9510a', '╬'], ['d04b0a', '╬'], ['a93906', '╣'], ['290a20', '█'], ['1c092d', '█'], ['19092b', '█'], ['200c37', '█'], ['1f0b34', '█'], ['280f41', '█'], ['2d1146', '█'], ['402234', '▓'], ['723e33', '╬'], ['b2763a', 'Ü'], ['bf7f3f', '░'], ['bc8242', '░'], ['8c6332', '║'], ['a2290d', '▓'], ['ae2e10', '╬'], ['a63a14', '╣'], ['9a3e1e', '╣'], ['7a542c', '▄'], ['af8345', '░'], ['bc8142', '»'], ['c3773b', '░'], ['c67037', ']'], ['c2773b', '░'], ['ba7e3f', '░'], ['4a302e', '▓'], ['321349', '▓'], ['3b2547', '▓'], ['301346', '█'], ['4b3857', '▌']],
41
+ [['808080', ' '], ['8b614d', '╚'], ['da520c', '╬'], ['e85c0b', '╠'], ['ee610b', '╠'], ['ed600b', '╠'], ['e95e0b', '╠'], ['e3590b', '╬'], ['d5500a', '╣'], ['c04209', '╣'], ['cf4c09', '╬'], ['b84607', '╬'], ['78340f', '▓'], ['4d3438', '▌'], ['5f4242', '╢'], ['4b161c', '▓'], ['45111a', '█'], ['39122f', '█'], ['39174f', '▓'], ['4b1b58', '▓'], ['372119', '█'], ['835729', '▄'], ['b27638', '▒'], ['bb8141', '░'], ['b58545', '»'], ['ca6f31', '╙'], ['db6022', '╚'], ['dd5a20', '╩'], ['d55f23', '╚'], ['a66e37', '╙'], ['b68545', '»'], ['bc8242', '»'], ['c07d3e', '░'], ['c0783a', '░'], ['9c6637', '▄'], ['492d2f', '▓'], ['3f1754', '▓'], ['52215f', '▓'], ['4f2958', '╬'], ['5a4c63', '╨'], ['787579', '`']],
42
+ [['808080', ' '], ['8b5b48', '╚'], ['b7400f', '╣'], ['cc480a', '╬'], ['dd540a', '╬'], ['e0570b', '╬'], ['d9510a', '╬'], ['d24c0a', '╬'], ['e55a0b', '╬'], ['d4530a', '╣'], ['c74809', '╬'], ['ec650b', '╠'], ['e6650c', '╠'], ['d4550a', '╬'], ['cc4b09', '╬'], ['bb3e09', '╣'], ['b73b08', '╬'], ['952f0e', '╬'], ['6e2123', '▓'], ['5d1c1b', '▓'], ['873309', '▓'], ['73320f', '▓'], ['a55826', '╬'], ['a9652f', '▒'], ['a97139', '╦'], ['aa793f', '╦'], ['a07c42', '╥'], ['9f7840', '╦'], ['986a38', '▄'], ['89582d', '▄'], ['63442b', '▓'], ['503b35', '▌'], ['3e2b3f', '▓'], ['2f1449', '▓'], ['3e284a', '▓'], ['432052', '▓'], ['54235f', '▓'], ['6f5e6e', 'H']],
43
+ [['808080', ' '], ['826f68', "'"], ['9f4a28', '╢'], ['b43b0b', '╣'], ['bb3d0a', '╣'], ['c8460a', '╣'], ['cc480a', '╣'], ['c6450a', '╬'], ['db520b', '╬'], ['e65c0b', '╠'], ['f1650c', '╠'], ['f1670c', '╠'], ['db5e0a', '╬'], ['e45e0b', '╬'], ['ee660c', '╠'], ['df5a0b', '╬'], ['c64a09', '╣'], ['d6520a', '╬'], ['e95f0a', '╠'], ['eb600b', '╠'], ['e95f0a', '╠'], ['e2620c', '╠'], ['ce6915', '╠'], ['b46424', '╠'], ['a65525', '╢'], ['622f15', '▓'], ['471c0b', '█'], ['4d1303', '█'], ['6a2004', '█'], ['c35009', '╬'], ['9e5d23', '╬'], ['7b685c', '░'], ['5b4b57', '╙ '], ['675d69', '╙'], ['685a6b', 'Γ']],
44
+ [['808080', ' '], ['806f69', "'"], ['965237', '╩'], ['ae3c10', '╣'], ['b4390a', '╣'], ['b93b09', '╣'], ['c9470a', '╬'], ['dc540b', '╬'], ['de550b', '╬'], ['e2580b', '╬'], ['ec620b', '╠'], ['cc4e09', '╬'], ['ec630b', '╠'], ['ed640b', '╠'], ['ec690b', '╠'], ['e7650a', '╠'], ['e4610a', '╬'], ['dd5b0a', '╬'], ['d9580a', '╬'], ['de580a', '╬'], ['e45c0a', '╬'], ['e7650a', '╠'], ['d16b12', '╠'], ['ae5f23', '╠'], ['683215', '▓'], ['5c1d04', '█'], ['bc4d08', '╬'], ['e95f0a', '╠'], ['e5600a', '╬'], ['ec670c', '╠'], ['e67210', '╠'], ['9a7041', '▒']],
45
+ [['808080', ' '], ['8c5f4e', '╙'], ['9e4420', '╫'], ['b03a0c', '╣'], ['b53909', '╣'], ['b73a09', '╣'], ['b83b09', '╣'], ['d54e0a', '╬'], ['cd4909', '╣'], ['cc4b0a', '╬'], ['e0580b', '╬'], ['d8560a', '╬'], ['e7620b', '╠'], ['f1690c', '╠'], ['f2690c', '╠'], ['f1670c', '╠'], ['ef660c', '╠'], ['e9630b', '╠'], ['de5b0a', '╬'], ['d8570a', '╬'], ['df600a', '╬'], ['b14e09', '╬'], ['dd5f0a', '╬'], ['dd5a09', '╬'], ['d15409', '╬'], ['e5630a', '╠'], ['e4640a', '╬'], ['e2640a', '╬'], ['d9640a', '╬'], ['c37722', 'D'], ['897b63', ',']],
46
+ [['808080', ' '], ['807571', '`'], ['895b4a', '╙'], ['9c4423', '╣'], ['ac390d', '╣'], ['b23809', '╣'], ['b43908', '╣'], ['b33808', '╣'], ['bc3d09', '╣'], ['c64708', '╬'], ['e75e0b', '╬'], ['f2650c', '╠'], ['c87036', 'Ü'], ['b0774d', '╓'], ['b67448', '░'], ['c16c3d', '░'], ['af774e', '`'], ['bf743f', '╙'], ['e66718', '╠'], ['c67138', '╙'], ['d46c2a', '╚'], ['e76a17', '╩'], ['d57028', '╙'], ['e3691b', '╚'], ['e0681d', '╚'], ['f0690d', '╠'], ['ea660d', '╠'], ['e25e0a', '╢'], ['df7b11', '╠'], ['89795c', '=']],
47
+ [['808080', ' '], ['7b5a4e', '╙'], ['9b330b', '▓'], ['9a2e06', '▓'], ['a73307', '▓'], ['bd3e08', '╣'], ['d9530a', '╬'], ['ec5f0c', '╠'], ['c3703a', '░'], ['b17a4c', '²'], ['c47239', '╙'], ['b86f45', '░'], ['9e7f60', '_'], ['9f805e', '`'], ['bf6c3e', '░'], ['9a8363', '`'], ['9d8060', '`'], ['bb6a41', '░'], ['ab7651', ','], ['b97143', ')'], ['a1805c', '¬'], ['b87044', '"'], ['9c8060', "'"], ['d24913', '╢'], ['eb650f', '╠'], ['de8114', '╠'], ['887c65', ',']],
48
+ [['808080', ' '], ['835b4b', '╙'], ['a9370b', '╣'], ['a53306', '▓'], ['b83b08', '╬'], ['bd3e09', '╬'], ['ca4709', '╬'], ['ea600a', '╠'], ['eb6612', '╠'], ['e66818', '╠'], ['cc6c31', 'Ü'], ['b57348', '²'], ['b67647', '='], ['bd6e40', '░'], ['c4673a', '≡'], ['c0663e', 'U'], ['c86d35', '╦'], ['af754e', '∩'], ['b1784c', '»'], ['bd6e3f', '░'], ['c76635', '['], ['b57247', '░'], ['be5422', '╠'], ['d85209', '╬'], ['de5c0b', '╬'], ['d27e18', '╠'], ['847a69', '.']],
49
+ [['808080', ' '], ['89523c', '╠'], ['b13708', '╬'], ['b13807', '▓'], ['bc3d08', '╬'], ['bf3f09', '╬'], ['d14c0a', '╬'], ['ee630a', '╠'], ['f0650b', '╠'], ['f1660c', '╠'], ['bf713e', '░'], ['aa7b54', '|'], ['e35f1b', '▒'], ['9f815f', '` '], ['ce6c30', '╚'], ['ad7d51', '⌐'], ['998565', '`'], ['c2703c', 'Ü'], ['a47e5a', "'"], ['f3640a', '╠'], ['ea650b', '╠'], ['c34509', '╣'], ['c9460a', '╬'], ['e8630b', '╠'], ['d27a1a', '╠'], ['80776b', '.']],
50
+ [['808080', ' '], ['904e32', '╟'], ['b43908', '╣'], ['af3707', '▓'], ['c84509', '╬'], ['e55a0a', '╬'], ['ec610a', '╠'], ['ed630b', '╠'], ['f36708', '╠'], ['cf6a2d', '▒'], ['ba7244', '╓'], ['d26b2b', '▒'], ['b17a4c', '╓'], ['d1702c', '╦'], ['ab7953', ','], ['ac7851', ','], ['b97444', 'j'], ['a67d57', ','], ['9c8462', '_'], ['ee680f', '╠'], ['f1680b', '╠'], ['d35509', '╣'], ['c13f09', '╬'], ['d9520a', '╬'], ['f16a0a', '╠'], ['ce7215', '╠'], ['8b7d64', '_']],
51
+ [['808080', ' '], ['7f716b', '!'], ['c24d11', '╬'], ['dd5409', '╬'], ['d34f08', '╬'], ['d54d08', '╣'], ['d34b09', '╬'], ['d54d09', '╬'], ['ca4b07', '╬'], ['db5708', '╬'], ['ea620a', '╠'], ['ee650a', '╠'], ['ed670a', '╠'], ['ec670a', '╠'], ['eb6809', '╠'], ['ea650a', '╟'], ['ea6709', '╟'], ['e9660a', '╟'], ['e7650c', '╟'], ['e06313', '╠'], ['e96709', '╟'], ['e66509', '╢'], ['e46309', '╬'], ['c34608', '╣'], ['d04a09', '╬'], ['eb5f09', '╠'], ['ec630a', '╠'], ['df7d11', '╠'], ['8d7c5d', '=']],
52
+ [['808080', ' '], ['7f7875', '_'], ['9a5535', 'Æ'], ['af3908', '╣'], ['b23907', '▓'], ['c94709', '╬'], ['e35909', '╬'], ['e55a09', '╬'], ['d35408', '╬'], ['e55b09', '╬'], ['d15108', '╬'], ['d44d09', '╬'], ['d34d09', '╬'], ['d54f09', '╬'], ['dd5609', '╬'], ['ee630a', '╠'], ['f1650a', '╠'], ['f0640a', '╠'], ['ea5f0a', '╠'], ['e2580a', '╬'], ['e3590a', '╬'], ['ed620a', '╠'], ['f1680a', '╠'], ['e36409', '╬'], ['e46009', '╢'], ['c34409', '╬'], ['d24d09', '╣'], ['e15809', '╬'], ['ed620b', '╠'], ['e27c11', '╠'], ['8a795b', '╓']],
53
+ [['808080', ' '], ['84675a', '╔'], ['ad4517', '╣'], ['c04008', '╣'], ['be4007', '╬'], ['d54f08', '╬'], ['e75c0a', '╬'], ['e65e09', '╢'], ['de5b08', '╬'], ['e85f09', '╠'], ['de5b08', '╬'], ['ea5f0a', '╠'], ['ed610a', '╠'], ['ef630a', '╠'], ['f0640a', '╠'], ['f0650a', '╠'], ['f1660a', '╠'], ['f1660a', '╠'], ['f2660a', '╠'], ['f1660a', '╠'], ['f1650a', '╠'], ['f1650b', '╠'], ['f1660a', '╠'], ['f1660a', '╠'], ['e56309', '╬'], ['ee640a', '╠'], ['d15308', '╣'], ['bc3e08', '╣'], ['d64f09', '╬'], ['eb5f0a', '╠'], ['ef640b', '╠'], ['de7712', '╠'], ['8a7961', '⌐']],
54
+ [['808080', ' '], ['904b2f', '╢'], ['ab3608', '▓'], ['b23a07', '▓'], ['c34408', '╬'], ['d75009', '╬'], ['e05609', '╬'], ['c54a07', '╣'], ['c84807', '╣'], ['c64708', '╬'], ['d14b09', '╬'], ['d54e09', '╬'], ['d65009', '╬'], ['d75009', '╬'], ['d75009', '╬'], ['d54e09', '╬'], ['d54e09', '╬'], ['d75009', '╬'], ['db5309', '╬'], ['e35909', '╬'], ['ec600a', '╠'], ['ec610a', '╠'], ['e55e0a', '╬'], ['d6590b', '╬'], ['bb510d', '╣'], ['ab4c0d', '╣'], ['ac4d10', '╣'], ['a2480f', '╣'], ['a93e0d', '╬'], ['e35c0a', '╬'], ['eb5f0a', '╠'], ['ee620a', '╠'], ['d9620d', '╢'], ['c96d22', 'K'], ['97734d', '╔']],
55
+ [['808080', ' '], ['7f5f53', '1'], ['b43b0a', '╣'], ['b13807', '╣'], ['a73306', '▓'], ['af3606', '▓'], ['b03707', '▓'], ['b13706', '▓'], ['a93405', '▓'], ['ab3506', '▓'], ['b13707', '▓'], ['b63907', '╣'], ['b83a07', '╣'], ['b93b07', '╣'], ['bf3f08', '╣'], ['c44308', '╣'], ['cb4808', '╣'], ['d34d08', '╬'], ['d95108', '╬'], ['d45008', '╣'], ['c14407', '╣'], ['a13808', '▓'], ['331c13', '█'], ['442016', '█'], ['472217', '▓'], ['4d2518', '▓'], ['4f2718', '▓'], ['4f2618', '▓'], ['4d2417', '▓'], ['823711', '▓'], ['d64f09', '╬'], ['d85109', '╬'], ['d75209', '╬'], ['d3530d', '╬'], ['88634c', 'Ü']],
56
+ [['808080', ' '], ['76564a', '╠'], ['7e2f10', '▓'], ['91300a', '▓'], ['9e3307', '▓'], ['ae3707', '╬'], ['b53908', '╬'], ['c24107', '╬'], ['db5109', '╬'], ['d85408', '╬'], ['d55208', '╬'], ['e05509', '╬'], ['d95308', '╬'], ['ce5007', '╬'], ['de5409', '╬'], ['d95108', '╬'], ['c24906', '╬'], ['d54e08', '╬'], ['d54d08', '╬'], ['d14b08', '╬'], ['c04007', '╬'], ['9b3107', '▓'], ['1c1310', '█'], ['482216', '▓'], ['421f15', '█'], ['472216', '▓'], ['5b2815', '▓'], ['7c420f', '▓'], ['4b2412', '█'], ['62210a', '█'], ['952f07', '▓'], ['8d2d08', '▓'], ['682209', '▓'], ['431b0f', '█'], ['4b3933', '▌'], ['777675', '_']],
57
+ [['808080', ' '], ['666b6b', '╓'], ['203a3f', '▓'], ['14373b', '█'], ['153b3c', '▓'], ['153c3b', '▓'], ['183937', '▓'], ['192f2d', '█'], ['1d231d', '█'], ['35261a', '▓'], ['472a16', '▓'], ['563013', '▓'], ['743810', '▓'], ['863f10', '▓'], ['8f410e', '▓'], ['a0460e', '▌'], ['a4490c', '▌'], ['99470a', '▓'], ['b74e0c', '╬'], ['bb4f0b', '╬'], ['ba4c0b', '╬'], ['b9490b', '╬'], ['9f3d09', '╣'], ['20100d', '█'], ['411e15', '█'], ['442015', '█'], ['411f15', '█'], ['431f14', '█'], ['462013', '█'], ['3e1d13', '█'], ['381a12', '█'], ['1d0b0b', '█'], ['210e0e', '█'], ['2d1310', '█'], ['3a1a13', '█'], ['391b12', '█'], ['4d3730', '▌'], ['6c5e58', '╦'], ['76706c', ',']],
58
+ [['808080', ' '], ['525c5e', '▄'], ['18373c', '▓'], ['194441', '▓'], ['205849', '▓'], ['1e5548', '▓'], ['23604d', '╣'], ['23604d', '╣'], ['225d4c', '▓'], ['225d4a', '▓'], ['235f4d', '╬'], ['215a4a', '╬'], ['1d4f46', '▓'], ['1a473f', '▓'], ['1d4a43', '▓'], ['1b4641', '▓'], ['19413f', '▓'], ['143538', '█'], ['18423f', '▓'], ['174643', '▓'], ['164743', '▓'], ['174742', '▓'], ['17453f', '▓'], ['143839', '█'], ['0f151a', '█'], ['231412', '█'], ['301813', '█'], ['381b14', '█'], ['391c14', '█'], ['361b14', '█'], ['311d18', '█'], ['574d4b', 'Ñ'], ['322626', '▓'], ['25100f', '█'], ['311511', '█'], ['462116', '█'], ['4b2518', '▓'], ['4e2719', '▓'], ['4b2517', '▓'], ['52291c', '▓'], ['9d7742', 'Ü'], ['7c7469', '⌐']],
59
+ [['808080', ' '], ['3b4d50', '╣'], ['14353a', '█'], ['1b4c45', '▓'], ['225d4c', '▓'], ['215d4b', '▓'], ['23604e', '╣'], ['23614f', '╣'], ['225f4e', '╣'], ['1e5648', '▓'], ['23604d', '╬'], ['24614e', '╣'], ['24614e', '╣'], ['23614e', '╣'], ['22604c', '▓'], ['24624f', '╣'], ['23624e', '╣'], ['23614e', '╣'], ['1e5648', '▓'], ['23604d', '╬'], ['24624e', '╣'], ['24624f', '╣'], ['24624e', '╣'], ['215d4a', '▓'], ['215c4c', '╣'], ['1b4a44', '▓'], ['194541', '▓'], ['133535', '█'], ['123235', '█'], ['143739', '█'], ['153a3b', '█'], ['143237', '█'], ['4b5b5b', '▒ '], ['646060', '╙'], ['49312a', '▓'], ['723d20', '▌'], ['83552c', '╬'], ['4e2b1b', '▓'], ['4b2417', '▓'], ['865127', 'Ñ'], ['b77236', 'Ü'], ['84715b', 'H']],
60
+ [['808080', ' '], ['6e7272', '`'], ['485d5b', '╨'], ['3a6357', '╣'], ['2b5d4d', '╣'], ['225a47', '▓'], ['23614e', '╣'], ['22624e', '╣'], ['23614e', '╣'], ['215d49', '▓'], ['24634f', '╣'], ['24624e', '╣'], ['24624e', '╣'], ['22604c', '▓'], ['23614d', '▓'], ['23624e', '╣'], ['23624e', '╣'], ['23624e', '╣'], ['215f4b', '▓'], ['23614d', '╣'], ['24624e', '╣'], ['24624e', '╣'], ['24634f', '╣'], ['215c48', '▓'], ['23614e', '╣'], ['23614e', '╣'], ['24624e', '╣'], ['23604c', '╣'], ['225d49', '▓'], ['24614e', '╣'], ['24604d', '╣'], ['1d5045', '▓'], ['204945', '▓'], ['626a69', 'H '], ['7a7471', '`'], ['8f6951', '╙'], ['a3653a', 'Ü'], ['ac703a', 'Ü'], ['8a5b37', '╩'], ['9b6847', '╙'], ['827061', '^']],
61
+ [['808080', ' '], ['8e5c36', '╠'], ['9a5e2d', '╠'], ['88592d', '╬'], ['74552d', '╬'], ['5e552f', '╬'], ['4b5432', '╬'], ['405233', '╣'], ['345133', '╣'], ['2a5035', '▓'], ['295942', '╣'], ['26604c', '╣'], ['25624e', '╣'], ['25634f', '╣'], ['235b48', '▓'], ['255f4b', '╣'], ['23614b', '╣'], ['24624c', '╣'], ['24634d', '╬'], ['225b45', '▓'], ['255f49', '╣'], ['265f49', '╣'], ['285e47', '╣'], ['2a5a42', '╣'], ['2c5540', '▌'], ['3f675a', '╩'], ['486b60', '╩'], ['526b63', '╙'], ['6f7473', '`']],
62
+ [['808080', ' '], ['9a663e', 'Ü'], ['b6773c', 'ù'], ['b4743a', '╙'], ['b36e35', '╙'], ['b16832', '╚'], ['af632e', '╩'], ['ad5f2c', '╠'], ['ac5e2b', '╠'], ['ac5f2c', '╠'], ['8a6344', '╚ '], ['727775', '` '], ['766b63', '!'], ['a15c2c', '╠'], ['a75e2b', '╠'], ['a75e2b', '╠'], ['a95f2c', '╠'], ['ab602d', '╩'], ['ac632e', '╩'], ['ae642e', '╩'], ['b1642f', '╩'], ['95623b', 'Ü']],
63
+ [['808080', ' '], ['9a6e41', 'Ü'], ['b98746', '»'], ['b88847', '»'], ['b88847', '»'], ['b98746', '»'], ['bb8443', '»'], ['bb7d3e', '░'], ['b66c33', '╚'], ['9a6038', '╩ '], ['8a6147', '╚'], ['af612d', '▒'], ['b26630', '╩'], ['b6783b', 'Ü'], ['b88343', '»'], ['b98544', '»'], ['ba8544', '»'], ['bb8443', '»'], ['a27845', ']'], ['757574', '_']],
64
+ [['808080', ' '], ['787470', ','], ['a97840', 'H'], ['b88746', '»'], ['b78847', '»'], ['b88847', '»'], ['b88746', '»'], ['ba8644', '»'], ['bb8543', '»'], ['a97941', '['], ['7c716a', '` '], ['9c633c', '╚'], ['bc8041', '░'], ['b78847', '»'], ['b68948', '»'], ['b78848', '»'], ['b88746', '»'], ['ba8644', '»'], ['aa7e48', '░'], ['797672', '_']],
65
+ [['808080', ' '], ['79756e', ','], ['9e7646', 'Ü'], ['b98545', '»'], ['b78847', '»'], ['bb8645', '»'], ['c17d3d', '░'], ['bc8342', '»'], ['bb8443', '»'], ['ab7e44', '░'], ['7f7568', '⌐ '], ['7d7773', '`'], ['9f7344', 'U'], ['b68847', '»'], ['b78848', '»'], ['b98746', '»'], ['bd8342', '░'], ['bc8443', '»'], ['af8045', '░'], ['7f776d', '_']],
66
+ [['808080', ' '], ['7a746d', ':'], ['a77d46', '░'], ['b78847', '»'], ['b68948', '»'], ['b78948', '»'], ['b88847', '»'], ['ba8544', '»'], ['b98343', '░'], ['987549', '░'], ['7c7873', '` '], ['86725d', 'j'], ['b38646', '░'], ['b68848', '»'], ['b98746', '»'], ['be8141', '░'], ['bd8242', '»'], ['bb8443', '»'], ['95754c', ']'], ['7c7a78', '_']],
67
+ [['808080', ' '], ['8b7354', '░'], ['b78645', '»'], ['b68948', '»'], ['b68a49', '»'], ['b68949', '»'], ['b78948', '»'], ['b98645', '»'], ['b38043', '░'], ['7a7167', '⌐ '], ['7e7165', '|'], ['b28446', '░'], ['b68849', '»'], ['b78948', '»'], ['b88847', '»'], ['b98746', '»'], ['bb8544', '»'], ['b78545', '»'], ['857257', '░']],
68
+ [['808080', ' '], ['94764f', '░'], ['b88746', '»'], ['b68848', '»'], ['b58949', '»'], ['b68949', '»'], ['b78847', '»'], ['b98645', '»'], ['8f734d', '] '], ['8d6f4a', ']'], ['b78847', '»'], ['b78847', '»'], ['b78747', '»'], ['b88746', '»'], ['ba8545', '»'], ['ba8444', '»'], ['a47e49', '░'], ['7e7b75', '_']],
69
+ [['808080', ' '], ['727576', ','], ['4d595c', '▄'], ['514633', '▌'], ['97703b', '▄'], ['a67c42', '╥'], ['ae8145', '░'], ['b08345', '░'], ['b07f42', '░'], ['a17842', ']'], ['7c756b', '⌐ '], ['7c7772', "'"], ['a17743', 'U'], ['b98444', '░'], ['b98545', '»'], ['b98544', '»'], ['ba8443', '»'], ['b88243', '░'], ['af7e42', '░'], ['655d50', '▄'], ['6e7373', ',']],
70
+ [['808080', ' '], ['4e5e5d', '║'], ['1a4944', '▓'], ['1a3b32', '▓'], ['1d3222', '▓'], ['1f241c', '█'], ['282119', '█'], ['33251a', '█'], ['35241a', '█'], ['1e1a17', '█'], ['2a4a46', '▓'], ['707372', '⌐ '], ['6e7374', 'j'], ['233336', '▓'], ['34321f', '▓'], ['473622', '▓'], ['4a3f25', '▓'], ['3f3d25', '▓'], ['2f3822', '▓'], ['213723', '▓'], ['1e4234', '▓'], ['225747', '▓'], ['4b625b', '▒']],
71
+ [['808080', ' '], ['435a54', '╢'], ['215949', '▓'], ['235b49', '▓'], ['245d49', '▓'], ['235945', '▓'], ['1f5847', '▓'], ['295f46', '╣'], ['305d42', '╣'], ['2e5841', '╣'], ['2a4b43', '▓'], ['767877', '_ '], ['727676', "'"], ['26504f', '▓'], ['1c504a', '▓'], ['245d48', '▓'], ['255e48', '▓'], ['255d48', '▓'], ['215844', '▓'], ['245e48', '▓'], ['2b5e47', '╣'], ['33614a', '▀'], ['435a47', 'Ñ'], ['747470', '.']],
72
+ [['808080', ' '], ['3f585a', '╢'], ['19484c', '▓'], ['194748', '▓'], ['1c4b46', '▓'], ['4c5946', '╬'], ['485040', '╫'], ['5d5d47', 'Å'], ['555743', '╬'], ['51523f', '╬'], ['6a624a', '▒'], ['6d695c', 'H '], ['385655', '╫'], ['1a4e4a', '▓'], ['1c4f48', '▓'], ['1c4f48', '▓'], ['1b4d47', '▓'], ['2a4d43', '▓'], ['4f5844', '╢'], ['585542', '╢'], ['4e4b39', '╬'], ['645f47', '╠'], ['656150', '▒'], ['7a7977', '_']],
73
+ [['808080', ' '], ['6f7373', ','], ['25534c', '▓'], ['19494b', '▓'], ['1a4849', '▓'], ['1e4c4a', '▓'], ['3f483e', '╬'], ['354740', '╣'], ['405141', '╬'], ['4b5a45', '╠'], ['455642', '╣'], ['616149', '╩'], ['6d6955', 'Ü'], ['757471', '. '], ['425d5c', '╟'], ['194c4e', '▓'], ['19484b', '▓'], ['1e534a', '▓'], ['215649', '▓'], ['264d45', '▓'], ['3d4d40', '╬'], ['30433b', '▓'], ['47493f', '╬'], ['475242', '╣'], ['515844', '╬'], ['6c674f', 'Ü'], ['736f60', '░'], ['73726c', ','], ['7c7c7b', '_']],
74
+ [['808080', ' '], ['656963', '['], ['425f4e', '╠'], ['2d5147', '▓'], ['29584b', '▓'], ['275c49', '╣'], ['235b49', '▓'], ['215146', '▓'], ['2c4e44', '▓'], ['435441', '╬'], ['556049', '▄'], ['3d5d46', '╣'], ['3a5b43', '╣'], ['395c44', '╣'], ['4c6451', '▒'], ['6d736b', '╓ '], ['5c6359', '╚'], ['425a4a', '╬'], ['32564b', '▀'], ['2f5446', '▀'], ['2e5d49', '╣'], ['295e4a', '╣'], ['235d49', '╬'], ['235746', '▓'], ['1b4e47', '▓'], ['305346', '▌'], ['5d644a', '╠'], ['395f48', '╣'], ['316048', '╣'], ['2e5f46', '╣'], ['355f46', '╣'], ['4c6751', '▒'], ['6f736b', '┐']],
75
+ [['808080', ' '], ['787775', '`'], ['706e62', '╙'], ['6a6555', '╚'], ['65604d', '╠'], ['6b654e', '▒'], ['5e6a4e', '╚'], ['3d6047', '╬'], ['295b48', '╣'], ['19574a', '▓'], ['215647', '▓'], ['255a47', '▓'], ['265a47', '╣'], ['265946', '▓'], ['215749', '▓'], ['25544a', '▓'], ['5d6653', '▒ '], ['7a7a77', '`'], ['736f66', '²'], ['6c685b', '╚'], ['6d6756', 'Ü'], ['706954', 'Ü'], ['746c52', 'Ü'], ['787153', '░'], ['666e4f', '╙'], ['435f47', '╩'], ['3b5745', '▀'], ['2e5445', '▀'], ['2a5447', '▓'], ['2b5446', '▓'], ['2d5847', '╣'], ['315948', '▀'], ['3d5945', '▀'], ['6d6f4f', '╙'], ['787671', '-']],
76
+ [['808080', ' '], ['777673', '`'], ['787263', '!'], ['83775a', '░'], ['8e7d56', '░'], ['8f7f55', '└'], ['847a54', '┘'], ['787652', '┘'], ['797752', '┘'], ['827a54', '┘'], ['8c7d55', '│'], ['8e7e58', ';'], ['7e7660', '= '], ['797874', '`'], ['79756a', '^'], ['7e7661', '='], ['867b5f', '='], ['887b5b', '»'], ['867b5b', '░'], ['887c5b', '░'], ['8a7c59', '»'], ['8b7e5d', '='], ['877c63', '='], ['7c776c', "'"]],
77
+ [['808080', ' '], ['787775', '`'], ['7b7872', '`'], ['7d7a72', '`'], ['7d7a72', '`'], ['7b7973', '`'], ['797976', '`']],
78
+ ];
79
+ // Print each line
80
+ for (const line of art) {
81
+ let output = '';
82
+ for (const [color, text] of line) {
83
+ output += hex(color)(text);
84
+ }
85
+ console.log(output);
86
+ }
87
+ console.log();
88
+ }
89
+ function getDownloadUrls(arcadeUrl) {
90
+ return {
91
+ windows: `${arcadeUrl}/downloads/crashplayer-windows-x64.zip`,
92
+ macos: `${arcadeUrl}/downloads/crashplayer-macos-universal.tar.gz`,
93
+ linux: `${arcadeUrl}/downloads/crashplayer-linux-x64.tar.gz`,
94
+ };
95
+ }
96
+ function buildCrashPlayerArgs(options) {
97
+ const args = [];
98
+ if (options.windowSize) {
99
+ args.push(`--window-size ${options.windowSize}`);
100
+ }
101
+ if (options.fullscreen) {
102
+ args.push('--fullscreen');
103
+ }
104
+ if (options.debug) {
105
+ args.push('--debug');
106
+ }
107
+ if (options.escapeExit === false) {
108
+ args.push('--no-escape-exit');
109
+ }
110
+ return args.join(' ');
111
+ }
112
+ function detectPlatform() {
113
+ switch (process.platform) {
114
+ case 'win32':
115
+ return 'windows';
116
+ case 'darwin':
117
+ return 'macos';
118
+ default:
119
+ return 'linux';
120
+ }
121
+ }
122
+ function isAlreadyStamped(crashcartPath) {
123
+ try {
124
+ // Read the file and look for stamp markers
125
+ // Stamps typically include the arcade URL or license info
126
+ const buffer = readFileSync(crashcartPath);
127
+ const content = buffer.toString('utf8', 0, Math.min(buffer.length, 8192));
128
+ // Check for common stamp indicators
129
+ const stampMarkers = [
130
+ 'arcade.crashbasic.com',
131
+ 'CRASHBASIC_LICENSE',
132
+ '"license":',
133
+ '"stamped":',
134
+ '"tier":',
135
+ ];
136
+ for (const marker of stampMarkers) {
137
+ if (content.includes(marker)) {
138
+ return true;
139
+ }
140
+ }
141
+ // Also check the last 1KB for appended stamp data
142
+ if (buffer.length > 1024) {
143
+ const tail = buffer.toString('utf8', buffer.length - 1024);
144
+ for (const marker of stampMarkers) {
145
+ if (tail.includes(marker)) {
146
+ return true;
147
+ }
148
+ }
149
+ }
150
+ return false;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ async function downloadFile(url, destPath) {
157
+ const response = await fetch(url);
158
+ if (!response.ok) {
159
+ throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
160
+ }
161
+ const fileStream = createWriteStream(destPath);
162
+ await pipeline(Readable.fromWeb(response.body), fileStream);
163
+ }
164
+ async function extractTarGz(tarPath, destDir) {
165
+ return new Promise((resolve, reject) => {
166
+ const extractor = extract();
167
+ const entries = [];
168
+ extractor.on('entry', (header, stream, next) => {
169
+ const chunks = [];
170
+ stream.on('data', (chunk) => chunks.push(chunk));
171
+ stream.on('end', () => {
172
+ entries.push({
173
+ name: header.name,
174
+ data: Buffer.concat(chunks),
175
+ mode: header.mode,
176
+ });
177
+ next();
178
+ });
179
+ stream.resume();
180
+ });
181
+ extractor.on('finish', () => {
182
+ for (const entry of entries) {
183
+ const fullPath = join(destDir, entry.name);
184
+ const dir = dirname(fullPath);
185
+ if (!existsSync(dir)) {
186
+ mkdirSync(dir, { recursive: true });
187
+ }
188
+ if (entry.data.length > 0) {
189
+ writeFileSync(fullPath, entry.data);
190
+ if (entry.mode) {
191
+ chmodSync(fullPath, entry.mode);
192
+ }
193
+ }
194
+ }
195
+ resolve();
196
+ });
197
+ extractor.on('error', reject);
198
+ createReadStream(tarPath)
199
+ .pipe(createGunzip())
200
+ .pipe(extractor);
201
+ });
202
+ }
203
+ async function extractZip(zipPath, destDir) {
204
+ // Use unzip command on Unix or PowerShell on Windows
205
+ if (process.platform === 'win32') {
206
+ execSync(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, {
207
+ stdio: 'inherit',
208
+ });
209
+ }
210
+ else {
211
+ execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
212
+ }
213
+ }
214
+ function runCommand(command, args) {
215
+ return new Promise((resolve, reject) => {
216
+ const proc = spawn(command, args, {
217
+ stdio: 'inherit',
218
+ shell: process.platform === 'win32',
219
+ });
220
+ proc.on('close', (code) => {
221
+ if (code === 0) {
222
+ resolve();
223
+ }
224
+ else {
225
+ reject(new Error(`Command failed with code ${code}`));
226
+ }
227
+ });
228
+ proc.on('error', reject);
229
+ });
230
+ }
231
+ function sanitizeName(name) {
232
+ return name
233
+ .toLowerCase()
234
+ .replace(/[^a-z0-9]+/g, '-')
235
+ .replace(/^-|-$/g, '');
236
+ }
237
+ function getAppleCredentials(options) {
238
+ const teamId = options.appleTeamId || process.env.APPLE_TEAM_ID;
239
+ const signingIdentity = options.signingIdentity || process.env.APPLE_SIGNING_IDENTITY;
240
+ const appleId = options.appleId || process.env.APPLE_ID;
241
+ const password = options.applePassword || process.env.APPLE_PASSWORD;
242
+ if (teamId && signingIdentity && appleId && password) {
243
+ return { teamId, signingIdentity, appleId, password };
244
+ }
245
+ return null;
246
+ }
247
+ function printMacOSSigningInstructions() {
248
+ console.log(chalk.yellow('\n ╭─────────────────────────────────────────────────────────────────╮'));
249
+ console.log(chalk.yellow(' │') + chalk.bold(' macOS Code Signing & Notarization ') + chalk.yellow('│'));
250
+ console.log(chalk.yellow(' ╰─────────────────────────────────────────────────────────────────╯\n'));
251
+ console.log(chalk.white(' Your app was created but is ') + chalk.bold.red('NOT signed or notarized') + chalk.white('.'));
252
+ console.log(chalk.white(' Users will see security warnings when trying to open it.\n'));
253
+ console.log(chalk.bold(' To enable code signing and notarization:\n'));
254
+ console.log(chalk.cyan(' 1. Join the Apple Developer Program'));
255
+ console.log(chalk.gray(' https://developer.apple.com/programs/\n'));
256
+ console.log(chalk.cyan(' 2. Create a Developer ID Application certificate'));
257
+ console.log(chalk.gray(' - Open Keychain Access on your Mac'));
258
+ console.log(chalk.gray(' - Go to developer.apple.com → Certificates'));
259
+ console.log(chalk.gray(' - Create a "Developer ID Application" certificate'));
260
+ console.log(chalk.gray(' - Download and install it in your keychain\n'));
261
+ console.log(chalk.cyan(' 3. Create an app-specific password'));
262
+ console.log(chalk.gray(' - Go to appleid.apple.com → Sign-In and Security'));
263
+ console.log(chalk.gray(' - Click "App-Specific Passwords" → Generate\n'));
264
+ console.log(chalk.cyan(' 4. Find your credentials:'));
265
+ console.log(chalk.gray(' - Team ID: developer.apple.com → Membership → Team ID'));
266
+ console.log(chalk.gray(' - Signing Identity: Run ') + chalk.white('security find-identity -v -p codesigning'));
267
+ console.log(chalk.gray(' Look for "Developer ID Application: Your Name (TEAM_ID)"\n'));
268
+ console.log(chalk.bold(' Then run the packager with these options:\n'));
269
+ console.log(chalk.white(' npx crashcart-packager mygame.crashcart "My Game" \\'));
270
+ console.log(chalk.white(' --apple-team-id "YOUR_TEAM_ID" \\'));
271
+ console.log(chalk.white(' --signing-identity "Developer ID Application: Your Name (TEAM_ID)" \\'));
272
+ console.log(chalk.white(' --apple-id "your@email.com" \\'));
273
+ console.log(chalk.white(' --apple-password "xxxx-xxxx-xxxx-xxxx"\n'));
274
+ console.log(chalk.bold(' Or set environment variables:\n'));
275
+ console.log(chalk.white(' export APPLE_TEAM_ID="YOUR_TEAM_ID"'));
276
+ console.log(chalk.white(' export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"'));
277
+ console.log(chalk.white(' export APPLE_ID="your@email.com"'));
278
+ console.log(chalk.white(' export APPLE_PASSWORD="xxxx-xxxx-xxxx-xxxx"\n'));
279
+ }
280
+ async function signAndNotarizeMacApp(appPath, bundleId, credentials) {
281
+ const spinner = ora('Code signing app bundle...').start();
282
+ try {
283
+ // Sign all executables and the app bundle
284
+ const macosDir = join(appPath, 'Contents', 'MacOS');
285
+ // Sign CrashPlayer first
286
+ const crashPlayerPath = join(macosDir, 'CrashPlayer');
287
+ if (existsSync(crashPlayerPath)) {
288
+ execSync(`codesign --force --options runtime --sign "${credentials.signingIdentity}" --timestamp "${crashPlayerPath}"`, { stdio: 'pipe' });
289
+ }
290
+ // Sign the launch script
291
+ const launchScript = join(macosDir, 'launch.sh');
292
+ if (existsSync(launchScript)) {
293
+ execSync(`codesign --force --options runtime --sign "${credentials.signingIdentity}" --timestamp "${launchScript}"`, { stdio: 'pipe' });
294
+ }
295
+ // Sign the entire app bundle
296
+ execSync(`codesign --force --deep --options runtime --sign "${credentials.signingIdentity}" --timestamp "${appPath}"`, { stdio: 'pipe' });
297
+ spinner.succeed('Code signing complete');
298
+ // Create ZIP for notarization
299
+ const zipSpinner = ora('Creating ZIP for notarization...').start();
300
+ const appName = basename(appPath);
301
+ const zipPath = `${appPath}.zip`;
302
+ execSync(`ditto -c -k --keepParent "${appPath}" "${zipPath}"`, { stdio: 'pipe' });
303
+ zipSpinner.succeed('ZIP created');
304
+ // Submit for notarization
305
+ const notarizeSpinner = ora('Submitting to Apple for notarization (this may take a few minutes)...').start();
306
+ try {
307
+ const result = execSync(`xcrun notarytool submit "${zipPath}" ` +
308
+ `--apple-id "${credentials.appleId}" ` +
309
+ `--password "${credentials.password}" ` +
310
+ `--team-id "${credentials.teamId}" ` +
311
+ `--wait`, { stdio: 'pipe', encoding: 'utf-8', timeout: 600000 } // 10 minute timeout
312
+ );
313
+ if (result.includes('status: Accepted')) {
314
+ notarizeSpinner.succeed('Notarization successful');
315
+ }
316
+ else if (result.includes('status: Invalid')) {
317
+ notarizeSpinner.fail('Notarization rejected by Apple');
318
+ console.log(chalk.yellow(' Run with --verbose to see details, or check:'));
319
+ console.log(chalk.gray(' xcrun notarytool log <submission-id> --apple-id ... --password ... --team-id ...'));
320
+ rmSync(zipPath);
321
+ return false;
322
+ }
323
+ }
324
+ catch (notaryError) {
325
+ notarizeSpinner.fail('Notarization failed');
326
+ console.error(chalk.red(' ' + (notaryError.message || 'Unknown error')));
327
+ rmSync(zipPath);
328
+ return false;
329
+ }
330
+ // Staple the notarization ticket
331
+ const stapleSpinner = ora('Stapling notarization ticket...').start();
332
+ try {
333
+ execSync(`xcrun stapler staple "${appPath}"`, { stdio: 'pipe' });
334
+ stapleSpinner.succeed('Notarization ticket stapled');
335
+ }
336
+ catch {
337
+ stapleSpinner.warn('Could not staple ticket (app is still notarized)');
338
+ }
339
+ // Clean up zip
340
+ rmSync(zipPath);
341
+ return true;
342
+ }
343
+ catch (err) {
344
+ spinner.fail('Code signing failed');
345
+ console.error(chalk.red(' ' + (err.message || 'Unknown error')));
346
+ return false;
347
+ }
348
+ }
349
+ async function packageWindows(crashcartPath, gameName, outputDir, downloadUrl, iconPath, playerOptions) {
350
+ const spinner = ora('Downloading CrashPlayer for Windows...').start();
351
+ try {
352
+ const tempZip = join(outputDir, 'crashplayer.zip');
353
+ await downloadFile(downloadUrl, tempZip);
354
+ spinner.text = 'Extracting CrashPlayer...';
355
+ await extractZip(tempZip, outputDir);
356
+ rmSync(tempZip);
357
+ // Copy crashcart
358
+ const cartName = basename(crashcartPath);
359
+ copyFileSync(crashcartPath, join(outputDir, cartName));
360
+ // Convert icon to ICO if provided
361
+ if (iconPath && existsSync(iconPath)) {
362
+ spinner.text = 'Converting icon to ICO...';
363
+ try {
364
+ const icoBuffer = await pngToIco(iconPath);
365
+ writeFileSync(join(outputDir, 'icon.ico'), icoBuffer);
366
+ }
367
+ catch (iconErr) {
368
+ console.warn(chalk.yellow('\nWarning: Could not convert icon to ICO. Skipping.'));
369
+ }
370
+ }
371
+ // Build CrashPlayer arguments
372
+ const playerArgs = playerOptions ? buildCrashPlayerArgs(playerOptions) : '';
373
+ const fullArgs = playerArgs ? `${playerArgs} "${cartName}"` : `"${cartName}"`;
374
+ // Create .cmd launcher
375
+ const cmdContent = `@echo off\r\ncd /d "%~dp0"\r\nstart "" "CrashPlayer.exe" ${fullArgs}\r\n`;
376
+ writeFileSync(join(outputDir, `${gameName}.cmd`), cmdContent);
377
+ // Create README with launch instructions
378
+ const launchArgs = playerArgs ? `${playerArgs} "${cartName}"` : `"${cartName}"`;
379
+ const readme = `${gameName}
380
+ ${'='.repeat(gameName.length)}
381
+
382
+ TO PLAY
383
+ -------
384
+ Double-click "${gameName}.cmd" to launch the game.
385
+
386
+ To create a desktop shortcut with your custom icon:
387
+ 1. Right-click "${gameName}.cmd" > Create shortcut
388
+ 2. Right-click the new shortcut > Properties
389
+ 3. Click "Change Icon" > Browse > select "icon.ico"
390
+ 4. Move the shortcut to your Desktop
391
+
392
+
393
+ DISTRIBUTION SETUP
394
+ ------------------
395
+
396
+ STEAM:
397
+ 1. Upload this folder to Steam
398
+ 2. Set launch executable: CrashPlayer.exe
399
+ 3. Set launch arguments: ${launchArgs}
400
+
401
+ ITCH.IO:
402
+ 1. Zip this folder and upload
403
+ 2. Set launch executable: CrashPlayer.exe
404
+ 3. Set launch arguments: ${launchArgs}
405
+
406
+ GOG / OTHER STOREFRONTS:
407
+ Configure the launcher to run: CrashPlayer.exe ${launchArgs}
408
+ `;
409
+ writeFileSync(join(outputDir, 'README.txt'), readme.replace(/\n/g, '\r\n'));
410
+ spinner.succeed('Windows package created');
411
+ return outputDir;
412
+ }
413
+ catch (err) {
414
+ spinner.fail('Failed to create Windows package');
415
+ throw err;
416
+ }
417
+ }
418
+ async function packageMacOS(crashcartPath, gameName, outputDir, downloadUrl, iconPath, appleCredentials, playerOptions) {
419
+ const spinner = ora('Creating macOS app bundle...').start();
420
+ try {
421
+ const appName = `${gameName}.app`;
422
+ const appDir = join(outputDir, appName);
423
+ const contentsDir = join(appDir, 'Contents');
424
+ const macosDir = join(contentsDir, 'MacOS');
425
+ const resourcesDir = join(contentsDir, 'Resources');
426
+ mkdirSync(macosDir, { recursive: true });
427
+ mkdirSync(resourcesDir, { recursive: true });
428
+ // Download and extract CrashPlayer
429
+ spinner.text = 'Downloading CrashPlayer for macOS...';
430
+ const tempTar = join(outputDir, 'crashplayer.tar.gz');
431
+ await downloadFile(downloadUrl, tempTar);
432
+ spinner.text = 'Extracting CrashPlayer...';
433
+ await extractTarGz(tempTar, macosDir);
434
+ rmSync(tempTar);
435
+ // Copy crashcart
436
+ const cartName = basename(crashcartPath);
437
+ copyFileSync(crashcartPath, join(resourcesDir, cartName));
438
+ // Build CrashPlayer arguments
439
+ const playerArgs = playerOptions ? buildCrashPlayerArgs(playerOptions) : '';
440
+ // Create launcher script
441
+ const launcherScript = `#!/bin/bash
442
+ DIR="$(cd "$(dirname "$0")" && pwd)"
443
+ "$DIR/CrashPlayer" ${playerArgs ? playerArgs + ' ' : ''}"$DIR/../Resources/${cartName}"
444
+ `;
445
+ writeFileSync(join(macosDir, 'launch.sh'), launcherScript);
446
+ chmodSync(join(macosDir, 'launch.sh'), 0o755);
447
+ // Create Info.plist
448
+ const bundleId = `com.crashbasic.${sanitizeName(gameName)}`;
449
+ const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
450
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
451
+ <plist version="1.0">
452
+ <dict>
453
+ <key>CFBundleName</key>
454
+ <string>${gameName}</string>
455
+ <key>CFBundleDisplayName</key>
456
+ <string>${gameName}</string>
457
+ <key>CFBundleIdentifier</key>
458
+ <string>${bundleId}</string>
459
+ <key>CFBundleVersion</key>
460
+ <string>1.0</string>
461
+ <key>CFBundleShortVersionString</key>
462
+ <string>1.0</string>
463
+ <key>CFBundleExecutable</key>
464
+ <string>launch.sh</string>
465
+ <key>CFBundleIconFile</key>
466
+ <string>AppIcon</string>
467
+ <key>CFBundlePackageType</key>
468
+ <string>APPL</string>
469
+ <key>NSHighResolutionCapable</key>
470
+ <true/>
471
+ </dict>
472
+ </plist>
473
+ `;
474
+ writeFileSync(join(contentsDir, 'Info.plist'), infoPlist);
475
+ // Convert icon if provided and on macOS
476
+ if (iconPath && existsSync(iconPath) && process.platform === 'darwin') {
477
+ spinner.text = 'Converting icon...';
478
+ try {
479
+ const iconsetDir = join(resourcesDir, 'AppIcon.iconset');
480
+ mkdirSync(iconsetDir);
481
+ const sizes = [16, 32, 64, 128, 256, 512, 1024];
482
+ for (const size of sizes) {
483
+ execSync(`sips -z ${size} ${size} "${iconPath}" --out "${join(iconsetDir, `icon_${size}x${size}.png`)}"`, {
484
+ stdio: 'pipe',
485
+ });
486
+ if (size <= 512) {
487
+ execSync(`sips -z ${size * 2} ${size * 2} "${iconPath}" --out "${join(iconsetDir, `icon_${size}x${size}@2x.png`)}"`, { stdio: 'pipe' });
488
+ }
489
+ }
490
+ execSync(`iconutil -c icns "${iconsetDir}" -o "${join(resourcesDir, 'AppIcon.icns')}"`, { stdio: 'pipe' });
491
+ rmSync(iconsetDir, { recursive: true });
492
+ }
493
+ catch {
494
+ console.warn(chalk.yellow('\nWarning: Could not convert icon. Skipping.'));
495
+ }
496
+ }
497
+ else if (iconPath) {
498
+ console.warn(chalk.yellow('\nWarning: Icon conversion only works on macOS. Skipping.'));
499
+ }
500
+ spinner.succeed('macOS app bundle created');
501
+ // Sign and notarize if credentials provided and on macOS
502
+ let signed = false;
503
+ if (appleCredentials && process.platform === 'darwin') {
504
+ signed = await signAndNotarizeMacApp(appDir, bundleId, appleCredentials);
505
+ }
506
+ return { appPath: appDir, signed };
507
+ }
508
+ catch (err) {
509
+ spinner.fail('Failed to create macOS package');
510
+ throw err;
511
+ }
512
+ }
513
+ async function packageLinux(crashcartPath, gameName, outputDir, downloadUrl, iconPath, playerOptions) {
514
+ const spinner = ora('Downloading CrashPlayer for Linux...').start();
515
+ try {
516
+ const tempTar = join(outputDir, 'crashplayer.tar.gz');
517
+ await downloadFile(downloadUrl, tempTar);
518
+ spinner.text = 'Extracting CrashPlayer...';
519
+ await extractTarGz(tempTar, outputDir);
520
+ rmSync(tempTar);
521
+ // Copy crashcart
522
+ const cartName = basename(crashcartPath);
523
+ copyFileSync(crashcartPath, join(outputDir, cartName));
524
+ // Build CrashPlayer arguments
525
+ const playerArgs = playerOptions ? buildCrashPlayerArgs(playerOptions) : '';
526
+ // Create launcher script
527
+ const safeName = sanitizeName(gameName);
528
+ const launcherScript = `#!/bin/bash
529
+ DIR="$(cd "$(dirname "$0")" && pwd)"
530
+ "$DIR/CrashPlayer" ${playerArgs ? playerArgs + ' ' : ''}"$DIR/${cartName}"
531
+ `;
532
+ const launcherPath = join(outputDir, `${safeName}.sh`);
533
+ writeFileSync(launcherPath, launcherScript);
534
+ chmodSync(launcherPath, 0o755);
535
+ // Make CrashPlayer executable
536
+ const crashPlayerPath = join(outputDir, 'CrashPlayer');
537
+ if (existsSync(crashPlayerPath)) {
538
+ chmodSync(crashPlayerPath, 0o755);
539
+ }
540
+ // Copy icon if provided
541
+ if (iconPath && existsSync(iconPath)) {
542
+ copyFileSync(iconPath, join(outputDir, 'icon.png'));
543
+ }
544
+ // Create .desktop file for Linux desktop integration
545
+ const desktopEntry = `[Desktop Entry]
546
+ Type=Application
547
+ Name=${gameName}
548
+ Exec=${launcherPath}
549
+ Icon=${iconPath ? join(outputDir, 'icon.png') : ''}
550
+ Terminal=false
551
+ Categories=Game;
552
+ `;
553
+ writeFileSync(join(outputDir, `${safeName}.desktop`), desktopEntry);
554
+ spinner.succeed('Linux package created');
555
+ return outputDir;
556
+ }
557
+ catch (err) {
558
+ spinner.fail('Failed to create Linux package');
559
+ throw err;
560
+ }
561
+ }
562
+ function printUsage() {
563
+ console.log(chalk.bold.cyan('\n Crash BASIC Game Packager'));
564
+ console.log(chalk.gray(' Package your games for Windows, macOS, and Linux\n'));
565
+ console.log(chalk.bold(' Usage:'));
566
+ console.log(chalk.white(' npx crashcart-packager <crashcart> <game-name> [options]\n'));
567
+ console.log(chalk.bold(' Arguments:'));
568
+ console.log(chalk.cyan(' crashcart') + chalk.gray(' Path to the .crashcart file'));
569
+ console.log(chalk.cyan(' game-name') + chalk.gray(' Name of your game (used for output folder)\n'));
570
+ console.log(chalk.bold(' Packaging Options:'));
571
+ console.log(chalk.cyan(' -p, --platform') + chalk.gray(' Target platform (windows, macos, linux)'));
572
+ console.log(chalk.cyan(' -i, --icon') + chalk.gray(' Path to icon file (PNG recommended)'));
573
+ console.log(chalk.cyan(' -o, --output') + chalk.gray(' Output directory (default: ".")'));
574
+ console.log(chalk.cyan(' --all') + chalk.gray(' Build for all platforms'));
575
+ console.log(chalk.cyan(' --skip-stamp') + chalk.gray(' Skip license stamping\n'));
576
+ console.log(chalk.bold(' CrashPlayer Options:') + chalk.gray(' (baked into launcher)'));
577
+ console.log(chalk.cyan(' --window-size') + chalk.gray(' Force window size (e.g., 1280x720)'));
578
+ console.log(chalk.cyan(' -f, --fullscreen') + chalk.gray(' Start in fullscreen mode'));
579
+ console.log(chalk.cyan(' -d, --debug') + chalk.gray(' Enable debug mode'));
580
+ console.log(chalk.cyan(' --no-escape-exit') + chalk.gray(' Disable Escape key exit prompt\n'));
581
+ console.log(chalk.bold(' macOS Code Signing:'));
582
+ console.log(chalk.cyan(' --apple-team-id') + chalk.gray(' Apple Developer Team ID'));
583
+ console.log(chalk.cyan(' --signing-identity') + chalk.gray(' Code signing identity'));
584
+ console.log(chalk.cyan(' --apple-id') + chalk.gray(' Apple ID for notarization'));
585
+ console.log(chalk.cyan(' --apple-password') + chalk.gray(' App-specific password\n'));
586
+ console.log(chalk.bold(' Examples:'));
587
+ console.log(chalk.gray(' # Package for current platform'));
588
+ console.log(chalk.white(' npx crashcart-packager mygame.crashcart "My Awesome Game"\n'));
589
+ console.log(chalk.gray(' # Package fullscreen for all platforms'));
590
+ console.log(chalk.white(' npx crashcart-packager mygame.crashcart "My Game" --all --fullscreen\n'));
591
+ console.log(chalk.gray(' # Package with fixed window size'));
592
+ console.log(chalk.white(' npx crashcart-packager mygame.crashcart "My Game" --window-size 1280x720\n'));
593
+ // Show Crash-tan
594
+ printCrashTan();
595
+ console.log(chalk.gray(' Crash BASIC, Crash-tan, and Crash BASIC Arcade are'));
596
+ console.log(chalk.gray(' © 2026 Crash Continuum LLC. All rights reserved.\n'));
597
+ }
598
+ async function main() {
599
+ // Check if no arguments provided or help requested
600
+ const args = process.argv.slice(2);
601
+ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
602
+ printUsage();
603
+ process.exit(0);
604
+ }
605
+ program
606
+ .name('crashcart-packager')
607
+ .description('Package Crash BASIC games for distribution')
608
+ .version('1.0.0')
609
+ .argument('<crashcart>', 'Path to the .crashcart file')
610
+ .argument('<game-name>', 'Name of your game')
611
+ .option('-p, --platform <platform>', 'Target platform (windows, macos, linux)', detectPlatform())
612
+ .option('-i, --icon <path>', 'Path to icon file (PNG for best results)')
613
+ .option('-o, --output <dir>', 'Output directory', '.')
614
+ .option('--skip-stamp', 'Skip the stamping step (if already stamped)')
615
+ .option('--all', 'Build for all platforms')
616
+ .option('--arcade-url <url>', 'Arcade URL for downloads (default: https://arcade.crashbasic.com)')
617
+ // CrashPlayer options (passed through to launcher)
618
+ .option('--window-size <WxH>', 'Force window size (e.g., 1280x720)')
619
+ .option('-f, --fullscreen', 'Start in fullscreen mode')
620
+ .option('-d, --debug', 'Enable debug mode')
621
+ .option('--no-escape-exit', 'Disable Escape key exit prompt')
622
+ // macOS signing options
623
+ .option('--apple-team-id <id>', 'Apple Developer Team ID (or set APPLE_TEAM_ID env var)')
624
+ .option('--signing-identity <identity>', 'Code signing identity (or set APPLE_SIGNING_IDENTITY env var)')
625
+ .option('--apple-id <email>', 'Apple ID email for notarization (or set APPLE_ID env var)')
626
+ .option('--apple-password <password>', 'App-specific password for notarization (or set APPLE_PASSWORD env var)')
627
+ .configureOutput({
628
+ outputError: (str, write) => {
629
+ // Show custom help on error
630
+ printUsage();
631
+ console.log(chalk.red(` Error: ${str.replace('error: ', '')}`));
632
+ process.exit(1);
633
+ }
634
+ })
635
+ .parse();
636
+ const [crashcartPath, gameName] = program.args;
637
+ const options = program.opts();
638
+ const arcadeUrl = options.arcadeUrl || process.env.ARCADE_URL || DEFAULT_ARCADE_URL;
639
+ const downloads = getDownloadUrls(arcadeUrl);
640
+ // Show header
641
+ console.log();
642
+ printCrashTan();
643
+ console.log(chalk.bold.cyan(' Crash BASIC Game Packager'));
644
+ console.log(chalk.gray(' © 2026 Crash Continuum LLC\n'));
645
+ if (arcadeUrl !== DEFAULT_ARCADE_URL) {
646
+ console.log(chalk.yellow(` Using custom Arcade URL: ${arcadeUrl}\n`));
647
+ }
648
+ // Validate crashcart exists
649
+ if (!existsSync(crashcartPath)) {
650
+ console.error(chalk.red(`Error: CrashCart file not found: ${crashcartPath}`));
651
+ process.exit(1);
652
+ }
653
+ // Validate icon if provided
654
+ if (options.icon && !existsSync(options.icon)) {
655
+ console.error(chalk.red(`Error: Icon file not found: ${options.icon}`));
656
+ process.exit(1);
657
+ }
658
+ // Check for Apple credentials if building for macOS
659
+ const platforms = options.all
660
+ ? ['windows', 'macos', 'linux']
661
+ : [options.platform];
662
+ const buildingForMacOS = platforms.includes('macos');
663
+ const appleCredentials = buildingForMacOS ? getAppleCredentials(options) : null;
664
+ // Stamp the crashcart first (unless skipped or already stamped)
665
+ if (!options.skipStamp) {
666
+ if (isAlreadyStamped(crashcartPath)) {
667
+ console.log(chalk.green('CrashCart already has a license stamp. Skipping stamping.\n'));
668
+ }
669
+ else {
670
+ console.log(chalk.cyan('Stamping CrashCart with your license...\n'));
671
+ try {
672
+ await runCommand('npx', ['@crashcontinuum/crashcart-stamp', crashcartPath]);
673
+ console.log();
674
+ }
675
+ catch (err) {
676
+ console.error(chalk.red('Failed to stamp CrashCart. Make sure you are logged in.'));
677
+ process.exit(1);
678
+ }
679
+ }
680
+ }
681
+ let macOSWasUnsigned = false;
682
+ // Build CrashPlayer options
683
+ const playerOptions = {
684
+ windowSize: options.windowSize,
685
+ fullscreen: options.fullscreen,
686
+ debug: options.debug,
687
+ escapeExit: options.escapeExit,
688
+ };
689
+ for (const platform of platforms) {
690
+ const safeName = sanitizeName(gameName);
691
+ const outputDir = join(options.output, `${safeName}-${platform}`);
692
+ // Clean and create output directory
693
+ if (existsSync(outputDir)) {
694
+ rmSync(outputDir, { recursive: true });
695
+ }
696
+ mkdirSync(outputDir, { recursive: true });
697
+ console.log(chalk.cyan(`\nPackaging for ${platform}...`));
698
+ try {
699
+ let result;
700
+ switch (platform) {
701
+ case 'windows':
702
+ result = await packageWindows(crashcartPath, gameName, outputDir, downloads.windows, options.icon, playerOptions);
703
+ console.log(chalk.green(` Output: ${result}`));
704
+ break;
705
+ case 'macos':
706
+ const macResult = await packageMacOS(crashcartPath, gameName, outputDir, downloads.macos, options.icon, appleCredentials, playerOptions);
707
+ result = macResult.appPath;
708
+ console.log(chalk.green(` Output: ${result}`));
709
+ if (!macResult.signed) {
710
+ macOSWasUnsigned = true;
711
+ }
712
+ else {
713
+ console.log(chalk.green(' ✓ Signed and notarized'));
714
+ }
715
+ break;
716
+ case 'linux':
717
+ result = await packageLinux(crashcartPath, gameName, outputDir, downloads.linux, options.icon, playerOptions);
718
+ console.log(chalk.green(` Output: ${result}`));
719
+ break;
720
+ }
721
+ }
722
+ catch (err) {
723
+ console.error(chalk.red(`Failed to package for ${platform}:`), err);
724
+ }
725
+ }
726
+ console.log(chalk.bold.green('\nDone! Your game is ready for distribution.\n'));
727
+ // Show macOS signing instructions if the app wasn't signed
728
+ if (macOSWasUnsigned && process.platform === 'darwin') {
729
+ printMacOSSigningInstructions();
730
+ }
731
+ else if (macOSWasUnsigned) {
732
+ console.log(chalk.yellow('Note: macOS code signing requires building on a Mac with'));
733
+ console.log(chalk.yellow('Apple Developer credentials. See documentation for details.\n'));
734
+ }
735
+ }
736
+ main().catch((err) => {
737
+ console.error(chalk.red('Fatal error:'), err);
738
+ process.exit(1);
739
+ });
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@crashcontinuum/crashcart-packager",
3
+ "version": "1.0.0",
4
+ "description": "Package Crash BASIC games for distribution on Windows, macOS, and Linux",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "crashcart-packager": "./dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "crash-basic",
16
+ "crashbasic",
17
+ "crashcart",
18
+ "game",
19
+ "packager",
20
+ "game-distribution",
21
+ "electron-alternative",
22
+ "desktop-games",
23
+ "windows",
24
+ "macos",
25
+ "linux"
26
+ ],
27
+ "author": {
28
+ "name": "Crash Continuum LLC",
29
+ "url": "https://crashcontinuum.com"
30
+ },
31
+ "license": "UNLICENSED",
32
+ "homepage": "https://arcade.crashbasic.com",
33
+ "dependencies": {
34
+ "chalk": "^5.3.0",
35
+ "commander": "^12.1.0",
36
+ "ora": "^8.0.1",
37
+ "png-to-ico": "^2.1.8",
38
+ "tar-stream": "^3.1.7"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "@types/tar-stream": "^3.1.3",
43
+ "typescript": "^5.0.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "files": [
49
+ "dist"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }