create-remix-game 1.1.14 → 1.2.2

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/bin/auth.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/auth.js')
package/bin/link.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/link.js')
package/dist/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function auth(): Promise<void>;
package/dist/auth.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import prompts from 'prompts';
7
+ import { pathToFileURL } from 'url';
8
+ export async function auth() {
9
+ console.log(chalk.bold('\nšŸ”‘ Remix API Key Setup\n'));
10
+ // 1. Check if already configured
11
+ if (process.env.REMIX_API_KEY) {
12
+ console.log(chalk.green('āœ“ API key already configured'));
13
+ console.log(chalk.gray(` Current key: ${process.env.REMIX_API_KEY.slice(0, 15)}...`));
14
+ const { reconfigure } = await prompts({
15
+ type: 'confirm',
16
+ name: 'reconfigure',
17
+ message: 'Reconfigure with a new key?',
18
+ initial: false,
19
+ });
20
+ if (!reconfigure) {
21
+ return;
22
+ }
23
+ }
24
+ // 2. Detect shell config file
25
+ const homeDir = os.homedir();
26
+ const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'];
27
+ let shellConfig = null;
28
+ for (const file of shellFiles) {
29
+ const filePath = path.join(homeDir, file);
30
+ if (fs.existsSync(filePath)) {
31
+ shellConfig = filePath;
32
+ break;
33
+ }
34
+ }
35
+ if (!shellConfig) {
36
+ // Default to .zshrc on macOS, .bashrc elsewhere
37
+ shellConfig = path.join(homeDir, process.platform === 'darwin' ? '.zshrc' : '.bashrc');
38
+ }
39
+ console.log(chalk.cyan(`Using shell config: ${shellConfig}`));
40
+ console.log(chalk.gray('Get your API key at: https://remix.gg/api-keys\n'));
41
+ // 3. Prompt for API key
42
+ const { apiKey } = await prompts({
43
+ type: 'password',
44
+ name: 'apiKey',
45
+ message: 'Paste your API key (starts with sk_live_):',
46
+ validate: (value) => {
47
+ const apiKeyRegex = /^sk_live_[A-Za-z0-9_]{43}$/;
48
+ if (!apiKeyRegex.test(value)) {
49
+ return 'Invalid API key format. Should be sk_live_ followed by 43 characters.';
50
+ }
51
+ return true;
52
+ },
53
+ }, {
54
+ onCancel: () => {
55
+ console.log(chalk.yellow('\nCancelled'));
56
+ process.exit(0);
57
+ },
58
+ });
59
+ if (!apiKey) {
60
+ console.log(chalk.yellow('Cancelled'));
61
+ return;
62
+ }
63
+ // 4. Append to shell config
64
+ const configLine = `\n# Remix API Key (added by create-remix-game)\nexport REMIX_API_KEY="${apiKey}"\n`;
65
+ try {
66
+ fs.appendFileSync(shellConfig, configLine);
67
+ console.log(chalk.green('\nāœ“ API key saved to'), chalk.cyan(shellConfig));
68
+ console.log(chalk.yellow('\nTo activate in this terminal:'));
69
+ console.log(chalk.cyan(` source ${shellConfig}`));
70
+ console.log(chalk.yellow('\nOr restart your terminal\n'));
71
+ }
72
+ catch (error) {
73
+ console.error(chalk.red('Failed to write to shell config:'), error);
74
+ console.log(chalk.yellow('\nManually add this line to your shell config:'));
75
+ console.log(chalk.cyan(`export REMIX_API_KEY="${apiKey}"`));
76
+ }
77
+ }
78
+ // If run directly (use pathToFileURL for Windows compatibility)
79
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
80
+ auth().catch((error) => {
81
+ console.error(chalk.red('Error:'), error);
82
+ process.exit(1);
83
+ });
84
+ }
package/dist/cli.js CHANGED
@@ -1,10 +1,88 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk';
3
+ import { randomUUID } from 'crypto';
4
+ import fs from 'fs';
5
+ import os from 'os';
3
6
  import path from 'path';
4
7
  import prompts from 'prompts';
5
8
  import { initGitRepo } from './git.js';
6
9
  import { installDependencies, installLatestPackages } from './install.js';
7
10
  import { scaffold } from './scaffold.js';
11
+ /**
12
+ * Extracts game ID from a Remix URL or returns the input if already a UUID
13
+ * Supports any remix.gg URL format (games, preview, play.remix.gg, etc.)
14
+ */
15
+ function extractGameId(input) {
16
+ // Match UUID v4 pattern anywhere in the string
17
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;
18
+ const match = input.match(uuidPattern);
19
+ if (match) {
20
+ return match[0];
21
+ }
22
+ return input.trim();
23
+ }
24
+ /**
25
+ * Generates a new UUID v4 for local game ID
26
+ */
27
+ function generateUUID() {
28
+ return randomUUID();
29
+ }
30
+ /**
31
+ * Sets up the Remix API key in the user's shell config
32
+ */
33
+ async function setupApiKey() {
34
+ // Detect shell config file
35
+ const homeDir = os.homedir();
36
+ const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'];
37
+ let shellConfig = null;
38
+ for (const file of shellFiles) {
39
+ const filePath = path.join(homeDir, file);
40
+ if (fs.existsSync(filePath)) {
41
+ shellConfig = filePath;
42
+ break;
43
+ }
44
+ }
45
+ if (!shellConfig) {
46
+ // Default to .zshrc on macOS, .bashrc elsewhere
47
+ shellConfig = path.join(homeDir, process.platform === 'darwin' ? '.zshrc' : '.bashrc');
48
+ }
49
+ console.log(chalk.cyan(`\nShell config: ${shellConfig}`));
50
+ console.log(chalk.gray('Get your API key at: https://remix.gg/api-keys\n'));
51
+ const { apiKey } = await prompts({
52
+ type: 'password',
53
+ name: 'apiKey',
54
+ message: 'Paste your API key (starts with sk_live_):',
55
+ validate: (value) => {
56
+ const apiKeyRegex = /^sk_live_[A-Za-z0-9_]{43}$/;
57
+ if (!apiKeyRegex.test(value)) {
58
+ return 'Invalid API key format. Should be sk_live_ followed by 43 characters.';
59
+ }
60
+ return true;
61
+ },
62
+ }, {
63
+ onCancel: () => {
64
+ return false;
65
+ },
66
+ });
67
+ if (!apiKey) {
68
+ return false;
69
+ }
70
+ // Append to shell config
71
+ const configLine = `\n# Remix API Key (added by create-remix-game)\nexport REMIX_API_KEY="${apiKey}"\n`;
72
+ try {
73
+ fs.appendFileSync(shellConfig, configLine);
74
+ console.log(chalk.green('\nāœ“ API key saved to'), chalk.cyan(shellConfig));
75
+ console.log(chalk.yellow("\nNote: You'll need to restart your terminal or run:"));
76
+ console.log(chalk.cyan(` source ${shellConfig}\n`));
77
+ return true;
78
+ }
79
+ catch (error) {
80
+ console.error(chalk.red('Failed to write to shell config:'), error);
81
+ console.log(chalk.yellow('\nManually add this line to your shell config:'));
82
+ console.log(chalk.cyan(`export REMIX_API_KEY="${apiKey}"\n`));
83
+ return false;
84
+ }
85
+ }
8
86
  async function main() {
9
87
  console.log(chalk.greenBright(`
10
88
 
@@ -51,6 +129,30 @@ async function main() {
51
129
  return true;
52
130
  },
53
131
  },
132
+ {
133
+ type: 'confirm',
134
+ name: 'hasRemixGame',
135
+ message: 'Do you have a game from Remix to link?\nCreate one at: https://remix.gg',
136
+ initial: false,
137
+ },
138
+ {
139
+ type: (prev) => (prev ? 'text' : null),
140
+ name: 'remixGameInput',
141
+ message: 'Enter your game URL or ID:',
142
+ validate: (input) => {
143
+ if (!input || !input.trim()) {
144
+ return 'Game URL or ID is required';
145
+ }
146
+ // Accept any format - just need to find a UUID v4 in the string
147
+ // Works with: remix.gg/games/{uuid}, play.remix.gg/{uuid}, or just {uuid}
148
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;
149
+ const match = input.match(uuidPattern);
150
+ if (!match) {
151
+ return 'No valid game ID found. Expected a UUID v4 or Remix URL.';
152
+ }
153
+ return true;
154
+ },
155
+ },
54
156
  {
55
157
  type: 'confirm',
56
158
  name: 'multiplayer',
@@ -85,6 +187,29 @@ async function main() {
85
187
  console.log('Cancelled');
86
188
  process.exit(0);
87
189
  }
190
+ // If user is linking a Remix game, check for API key
191
+ if (config.remixGameInput) {
192
+ if (!process.env.REMIX_API_KEY) {
193
+ console.log(chalk.cyan('\nšŸ’” Want to publish your game updates with one click?'));
194
+ console.log(chalk.gray('Add an API key to deploy and update your game directly from your local dev environment.\n'));
195
+ const { setupKey } = await prompts({
196
+ type: 'confirm',
197
+ name: 'setupKey',
198
+ message: 'Set up your API key now?',
199
+ initial: true,
200
+ });
201
+ if (setupKey) {
202
+ await setupApiKey();
203
+ }
204
+ else {
205
+ console.log(chalk.gray('\nNo problem! You can add it anytime by running:'));
206
+ console.log(chalk.cyan(' npx create-remix-game auth\n'));
207
+ }
208
+ }
209
+ else {
210
+ console.log(chalk.green('\nāœ“ API key found - one-click publishing enabled!'));
211
+ }
212
+ }
88
213
  // Generate a safe package name from the game name
89
214
  const projectName = targetDir ||
90
215
  config.gameName
@@ -92,11 +217,16 @@ async function main() {
92
217
  .replace(/[^a-z0-9]+/g, '-')
93
218
  .replace(/^-+|-+$/g, '');
94
219
  const projectPath = path.resolve(process.cwd(), projectName);
220
+ // Generate or extract game ID
221
+ const gameId = config.remixGameInput ? extractGameId(config.remixGameInput) : generateUUID();
222
+ const isRemixGame = !!config.remixGameInput;
95
223
  console.log(chalk.cyan(`\nāœ“ Creating project in ${projectPath}`));
96
224
  // Scaffold the project
97
225
  await scaffold(projectPath, {
98
226
  ...config,
99
227
  projectName,
228
+ gameId,
229
+ isRemixGame,
100
230
  });
101
231
  // Install dependencies
102
232
  console.log(chalk.cyan('āœ“ Installing dependencies...'));
package/dist/link.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function link(): Promise<void>;
package/dist/link.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import prompts from 'prompts';
6
+ import { pathToFileURL } from 'url';
7
+ export async function link() {
8
+ console.log(chalk.bold('\nšŸ”— Remix Game Linking\n'));
9
+ // 1. Find remix.config.js or remix.config.ts
10
+ const configPathJs = path.resolve(process.cwd(), 'remix.config.js');
11
+ const configPathTs = path.resolve(process.cwd(), 'remix.config.ts');
12
+ const configPath = fs.existsSync(configPathJs) ? configPathJs : configPathTs;
13
+ if (!fs.existsSync(configPath)) {
14
+ console.error(chalk.red('āŒ Error: No remix.config.js or remix.config.ts found'));
15
+ console.log(chalk.yellow(' Run this command from your Remix project root\n'));
16
+ process.exit(1);
17
+ }
18
+ // 2. Read current config
19
+ let currentConfig;
20
+ try {
21
+ // Dynamic import to read TypeScript config (use pathToFileURL for Windows compatibility)
22
+ const configModule = await import(pathToFileURL(configPath).href);
23
+ currentConfig = configModule.default;
24
+ }
25
+ catch (error) {
26
+ console.error(chalk.red('Failed to read remix.config.ts:'), error);
27
+ process.exit(1);
28
+ }
29
+ // 3. Check if already linked
30
+ if (currentConfig.isRemixGame) {
31
+ console.log(chalk.yellow(`⚠ Project already linked to: ${currentConfig.gameId}`));
32
+ const { replace } = await prompts({
33
+ type: 'confirm',
34
+ name: 'replace',
35
+ message: 'Replace with new game ID?',
36
+ initial: false,
37
+ });
38
+ if (!replace) {
39
+ console.log(chalk.gray('Cancelled\n'));
40
+ return;
41
+ }
42
+ }
43
+ // 4. Prompt for game URL or ID
44
+ console.log(chalk.gray('Create a game at: https://remix.gg\n'));
45
+ const { gameInput } = await prompts({
46
+ type: 'text',
47
+ name: 'gameInput',
48
+ message: 'Paste your game URL or ID:',
49
+ validate: (input) => {
50
+ if (!input.trim()) {
51
+ return 'Game URL or ID is required';
52
+ }
53
+ return true;
54
+ },
55
+ }, {
56
+ onCancel: () => {
57
+ console.log(chalk.yellow('\nCancelled'));
58
+ process.exit(0);
59
+ },
60
+ });
61
+ // 5. Extract and validate game ID
62
+ let gameId = gameInput.trim();
63
+ // Extract UUID from various URL formats
64
+ const urlPatterns = [
65
+ /remix\.gg\/games\/([0-9a-f-]{36})/i, // /games/{uuid}
66
+ /remix\.gg\/preview\/([0-9a-f-]{36})/i, // /preview/{uuid}
67
+ /remix\.gg\/game\/([0-9a-f-]{36})/i, // /game/{uuid}
68
+ ];
69
+ for (const pattern of urlPatterns) {
70
+ const match = gameInput.match(pattern);
71
+ if (match) {
72
+ gameId = match[1];
73
+ break;
74
+ }
75
+ }
76
+ // Validate UUID v4 format
77
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
78
+ if (!uuidRegex.test(gameId)) {
79
+ console.error(chalk.red('\nāŒ Invalid game ID or URL'));
80
+ console.log(chalk.yellow(' Expected: https://remix.gg/games/{uuid}'));
81
+ console.log(chalk.yellow(' Or just the UUID (e.g., 45b7d6db-94f4-4c2d-b5d7-7501d10a3b70)\n'));
82
+ process.exit(1);
83
+ }
84
+ console.log(chalk.green(`āœ“ Extracted game ID: ${gameId}`));
85
+ // 6. Update remix.config.ts
86
+ try {
87
+ let configContent = fs.readFileSync(configPath, 'utf-8');
88
+ // Replace gameId
89
+ configContent = configContent.replace(/gameId:\s*['"][^'"]*['"]/, `gameId: '${gameId}'`);
90
+ // Replace isRemixGame
91
+ configContent = configContent.replace(/isRemixGame:\s*(true|false)/, 'isRemixGame: true');
92
+ fs.writeFileSync(configPath, configContent, 'utf-8');
93
+ console.log(chalk.green('āœ“ Updated remix.config.ts'));
94
+ console.log(chalk.green('āœ“ Project linked to Remix!\n'));
95
+ console.log(chalk.bold('Next steps:'));
96
+ console.log(chalk.cyan(' 1. Set up API key: npx create-remix-game auth'));
97
+ console.log(chalk.cyan(' 2. Build and deploy: pnpm deploy\n'));
98
+ console.log(chalk.gray(`View your game: https://remix.gg/games/${gameId}\n`));
99
+ }
100
+ catch (error) {
101
+ console.error(chalk.red('Failed to update remix.config.ts:'), error);
102
+ process.exit(1);
103
+ }
104
+ }
105
+ // If run directly (use pathToFileURL for Windows compatibility)
106
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
107
+ link().catch((error) => {
108
+ console.error(chalk.red('Error:'), error);
109
+ process.exit(1);
110
+ });
111
+ }
@@ -4,6 +4,8 @@ export interface ScaffoldConfig {
4
4
  multiplayer: boolean;
5
5
  packageManager: string;
6
6
  initGit: boolean;
7
+ gameId: string;
8
+ isRemixGame: boolean;
7
9
  useLocalDeps?: boolean;
8
10
  }
9
11
  export declare function scaffold(targetPath: string, config: ScaffoldConfig): Promise<void>;
package/dist/scaffold.js CHANGED
@@ -62,6 +62,7 @@ async function processTemplates(targetPath, config) {
62
62
  'src/config/GameSettings.ts.template',
63
63
  'README.md.template',
64
64
  'index.html.template',
65
+ 'remix.config.ts.template',
65
66
  ];
66
67
  // Get remix-dev version for dependency injection
67
68
  const remixDevVersion = config.useLocalDeps ? 'workspace:*' : `^${getRemixDevVersion()}`;
@@ -74,7 +75,9 @@ async function processTemplates(targetPath, config) {
74
75
  .replace(/\{\{PROJECT_NAME\}\}/g, config.projectName)
75
76
  .replace(/\{\{MULTIPLAYER\}\}/g, String(config.multiplayer))
76
77
  .replace(/\{\{PACKAGE_MANAGER\}\}/g, config.packageManager)
77
- .replace(/\{\{REMIX_DEV_VERSION\}\}/g, remixDevVersion);
78
+ .replace(/\{\{REMIX_DEV_VERSION\}\}/g, remixDevVersion)
79
+ .replace(/\{\{GAME_ID\}\}/g, config.gameId)
80
+ .replace(/\{\{IS_REMIX_GAME\}\}/g, String(config.isRemixGame));
78
81
  // Write to actual file (remove .template)
79
82
  const outputPath = filePath.replace('.template', '');
80
83
  await fs.writeFile(outputPath, processed);
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "create-remix-game",
3
- "version": "1.1.14",
3
+ "version": "1.2.2",
4
4
  "description": "CLI for scaffolding Remix games",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
7
7
  "bin": {
8
- "remix-game": "./bin/remix-game.js"
8
+ "remix-game": "./bin/remix-game.js",
9
+ "create-remix-game": "./bin/remix-game.js",
10
+ "create-remix-game-auth": "./bin/auth.js",
11
+ "create-remix-game-link": "./bin/link.js"
9
12
  },
10
13
  "files": [
11
14
  "bin",
@@ -3,11 +3,11 @@
3
3
  "version": "1.0.0",
4
4
  "description": "{{GAME_NAME}} game for Remix platform",
5
5
  "type": "module",
6
- "multiplayer": {{MULTIPLAYER}},
7
6
  "scripts": {
8
7
  "dev": "remix-dev dev",
9
8
  "build": "remix-dev build",
10
- "preview": "remix-dev preview"
9
+ "preview": "remix-dev preview",
10
+ "deploy": "remix-dev deploy"
11
11
  },
12
12
  "keywords": [
13
13
  "game",
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Remix Game Configuration
3
+ *
4
+ * This file contains your game's configuration for the Remix framework.
5
+ *
6
+ * Getting Started:
7
+ * 1. This gameId is auto-generated for local save states (localStorage)
8
+ * 2. To publish your game to Remix:
9
+ * - Run: npx create-remix-game link
10
+ * - This will authenticate and link your game to Remix
11
+ * - Your gameId will be updated automatically
12
+ *
13
+ * API Key (for publishing):
14
+ * - Set REMIX_API_KEY environment variable in your .env file
15
+ * - Get your API key from: https://remix.gg/dashboard
16
+ * - Example: REMIX_API_KEY=your_api_key_here
17
+ *
18
+ * Publishing your game:
19
+ * - Run: npx remix-dev deploy
20
+ * - This uploads your game to Remix (requires auth via link command)
21
+ */
22
+ export default {
23
+ // Unique identifier for your game
24
+ gameId: '{{GAME_ID}}', // UUID format - auto-generated or linked to Remix
25
+
26
+ // Whether this game is linked to Remix (true after running link command)
27
+ isRemixGame: {{IS_REMIX_GAME}},
28
+
29
+ // Display name for your game
30
+ gameName: '{{GAME_NAME}}',
31
+
32
+ // Multiplayer mode (true = multiplayer, false = singleplayer)
33
+ multiplayer: {{MULTIPLAYER}},
34
+ }
@@ -1,15 +1,7 @@
1
1
  import { remixPlugin } from '@insidethesim/remix-dev/vite'
2
- import fs from 'fs'
3
2
  import { defineConfig } from 'vite'
4
-
5
- // Read multiplayer setting from package.json
6
- const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'))
7
- const isMultiplayer = packageJson.multiplayer === true
3
+ import remixConfig from './remix.config'
8
4
 
9
5
  export default defineConfig({
10
- plugins: [
11
- remixPlugin({
12
- multiplayer: isMultiplayer,
13
- }),
14
- ],
6
+ plugins: [remixPlugin(remixConfig)],
15
7
  })