Skip to content

Commit

Permalink
feat: add init, sync commands
Browse files Browse the repository at this point in the history
  • Loading branch information
SaadBazaz committed May 13, 2024
1 parent 9c410f9 commit 7876e0b
Show file tree
Hide file tree
Showing 10 changed files with 1,408 additions and 882 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"chalk": "^5.3.0",
"commander": "^11.1.0",
"download-git-repo": "^3.0.2",
"execa": "^9.0.2",
"inquirer": "^9.2.12",
"lodash": "^4.17.21",
"ora": "^8.0.1",
Expand Down
1,840 changes: 1,100 additions & 740 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

163 changes: 25 additions & 138 deletions src/index.mjs
Original file line number Diff line number Diff line change
@@ -1,155 +1,42 @@
#!/usr/bin/env node
import { program } from "commander";
import inquirer from "inquirer";
import download from "download-git-repo";
import ora from "ora";
import chalk from "chalk";
import path from "path";
import _ from "lodash";
import { promises as fs } from "fs"; // Import promises version of fs for async file operations

import templates from "./utils/data/templates.mjs";
import { replaceInDirectory } from "./utils/cookie-cutting/replacer.mjs";
import { sync } from "./utils/commands/sync.mjs";
import { init, initAction } from "./utils/commands/init.mjs";

import packageJson from "../package.json" with { type: "json" };
import { logger } from "./utils/logger";
const { version } = packageJson

let DEFAULT_CONFIG_NAME = "create-multiplayer-game.config.json";
const cmds = {
init,
sync
}

const { version } = packageJson
const isArgumentACommand = !!cmds[process.argv[2]]
const isContainsHelp = process.argv.includes(a => a === "--help")

program
.version(
version,
"-v, --version",
"Display the version number"
)
.arguments("[project-name]")
.option("-t, --template <template>", "Specify the template")
.option("-c, --config <config>", "Specify the configuration file")
.parse(process.argv);

async function main() {
let projectName = program.args[0];
let gameName = projectName;
let templateChoice = program.template;
let configPath = program.config || DEFAULT_CONFIG_NAME; // Default config file path
let currentRun = Date.now();
let generatedAt = currentRun;
let lastRunAt = currentRun;

// Check if config file exists
let configExists = false;
try {
await fs.access(configPath);
configExists = true;
} catch (err) {
// Config file doesn't exist, continue without it
}

// If config file exists, read configuration from it
if (configExists) {
const spinner = ora(
`Reading config file at ${path.join(process.cwd(), configPath)}...`
).start();
const configData = await fs.readFile(configPath, "utf-8");
const config = JSON.parse(configData);
if (!projectName && config.projectName) projectName = config.projectName;
if (!gameName && config.gameName) gameName = config.gameName;
if (!templateChoice && config.templateChoice) templateChoice = config.templateChoice;
if (config.generatedAt) generatedAt = config.generatedAt;
spinner.succeed();
}

// If config file doesn't exist, prompt user for input
if (!projectName) {
const answersGameName = await inquirer.prompt([
{
type: "input",
name: "gameName",
message: "What will you call your game?",
validate: (input) => !!input.trim(),
},
]);
gameName = answersGameName.gameName;

const kebabCaseGameName = _.kebabCase(gameName);

const answers = await inquirer.prompt([
{
type: "input",
name: "projectName",
message: "Enter the project name:",
default: kebabCaseGameName,
validate: (input) => !!input.trim(),
},
]);
projectName = answers.projectName;
.argument("[command]")
.argument("[project-name]")

if (isArgumentACommand || isContainsHelp) {
Object.values(cmds).forEach(cmd => {
program.addCommand(cmd)
})
}

if (!templateChoice) {
const answers = await inquirer.prompt([
{
type: "list",
name: "templateChoice",
message: "Which template would you like to use?",
choices: templates.map((template) => template.id + (template.source === "community" ? " (community)" : "")),
},
]);
templateChoice = answers.templateChoice.split(" ")[0]; // <-- remove any addons to the ID
else {
program
.option("-t, --template <template>", "Specify the template")
.option("-c, --config <config>", "Specify the configuration file")
.action(async (command, projectName, options) => {
// If no command is provided, run the default command (init)
await initAction(projectName, options, program);
});
}

const chosenTemplate = templates.find(
(template) => template.id === templateChoice
)

const templateRepoUrl = chosenTemplate.url;
const targetPath = path.join(process.cwd(), projectName);
const spinner = ora("Downloading project template...").start();

download(templateRepoUrl, targetPath, { clone: true }, (err) => {
if (err) {
spinner.fail(chalk.red("Failed to download project template."));
if (chosenTemplate.price === "premium") {
logger.warn("This template is 👑 Premium. We're releasing Premium purchases soon at https://grayhat.studio/games/pricing. Until then, sit tight!")
}
else {
logger.error("Couldn't clone your project. Please check your git configuration, or check if a folder with a similar name to the project you want to create already exists.");
logger.log("Detailed error log:");
}
console.error(err);
} else {

if (chosenTemplate.editable) {
// Cookie cutting
replaceInDirectory(targetPath, new RegExp("%GAME_NAME%", "g"), gameName);
}

spinner.succeed(chalk.green("Project template downloaded successfully."));
logger.warn(`\nProject initialized at ${targetPath}`);

logger.info("\nHappy coding!");

// Write configuration to file
const configData = {
projectName,
gameName,
templateChoice,
generatedAt,
lastRunAt,
version
};

let configFileWritePath = path.join(targetPath, DEFAULT_CONFIG_NAME);
fs.writeFile(configFileWritePath, JSON.stringify(configData, null, 2))
.then(() =>
logger.success(`Configuration saved to ${configFileWritePath}`)
)
.catch((err) =>
logger.error(`Error writing configuration file: ${err.message}`)
);
}
});
}

main();
program.parse(process.argv);
153 changes: 153 additions & 0 deletions src/utils/commands/init.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { logger } from '../logger.mjs'
import { Command } from "commander"
import inquirer from "inquirer";
// import download from "download-git-repo";
import _ from "lodash";
import ora from "ora";
import chalk from "chalk";
import path from "path";
import { promises as fs } from "fs"; // Import promises version of fs for async file operations

import templates from "../data/templates.mjs";
import { replaceInDirectory } from "../cookie-cutting/replacer.mjs";
import { addTemplateUpstream, cloneTemplate } from "../git-actions.mjs";

import packageJson from "../../../package.json" with { type: "json" };
import { DEFAULT_CONFIG_NAME } from '../data/cmg-config.mjs';
const { version } = packageJson

export const initAction = async (name, options, program) => {

let projectName = program.args[0];
let gameName = projectName;
let templateId = program.template;
let configPath = program.config || DEFAULT_CONFIG_NAME; // Default config file path
let currentRun = Date.now();
let generatedAt = currentRun;
let lastRunAt = currentRun;

// Check if config file exists
let configExists = false;
try {
await fs.access(configPath);
configExists = true;
} catch (err) {
// Config file doesn't exist, continue without it
}

// If config file exists, read configuration from it
if (configExists) {
const spinner = ora(
`Reading config file at ${path.join(process.cwd(), configPath)}...`
).start();
const configData = await fs.readFile(configPath, "utf-8");
const config = JSON.parse(configData);
if (!projectName && config.projectName) projectName = config.projectName;
if (!gameName && config.gameName) gameName = config.gameName;
if (!templateId && config.templateId) templateId = config.templateId;
if (config.generatedAt) generatedAt = config.generatedAt;
spinner.succeed();
}

// If config file doesn't exist, prompt user for input
if (!projectName) {
const answersGameName = await inquirer.prompt([
{
type: "input",
name: "gameName",
message: "What will you call your game?",
validate: (input) => !!input.trim(),
},
]);
gameName = answersGameName.gameName;

const kebabCaseGameName = _.kebabCase(gameName);

const answers = await inquirer.prompt([
{
type: "input",
name: "projectName",
message: "Enter the project name:",
default: kebabCaseGameName,
validate: (input) => !!input.trim(),
},
]);
projectName = answers.projectName;
}

if (!templateId) {
const answers = await inquirer.prompt([
{
type: "list",
name: "templateId",
message: "Which template would you like to use?",
choices: templates.map((template) => template.id + (template.source === "community" ? " (community)" : "")),
},
]);
templateId = answers.templateId.split(" ")[0]; // <-- remove any addons to the ID
}

const chosenTemplate = templates.find(
(template) => template.id === templateId
)

const templateRepoUrl = chosenTemplate.repository.url;
const cwd = process.cwd()
const targetPath = path.join(cwd, projectName);
const spinner = ora("Downloading project template...").start();

try {
await cloneTemplate(templateRepoUrl, projectName, cwd)
}
catch (err) {
spinner.fail(chalk.red("Failed to download project template."));
if (chosenTemplate.price === "premium") {
logger.warn("This template is 👑 Premium. We're releasing Premium purchases soon at https://grayhat.studio/games/pricing. Until then, sit tight!")
}
else {
logger.error("Couldn't clone your project. Please check your git configuration, or check if a folder with a similar name to the project you want to create already exists.");
logger.log("Detailed error log:");
}
console.error(err);
}

await addTemplateUpstream(templateRepoUrl, targetPath)

if (chosenTemplate.editable) {
// Cookie cutting
replaceInDirectory(targetPath, new RegExp("%GAME_NAME%", "g"), gameName);
}

spinner.succeed(chalk.green("Project template downloaded successfully."));
logger.warn(`\nProject initialized at ${targetPath}`);

logger.info("\nHappy coding!");

// Write configuration to file
const configData = {
projectName,
gameName,
templateId,
generatedAt,
lastRunAt,
version
};

let configFileWritePath = path.join(targetPath, DEFAULT_CONFIG_NAME);
fs.writeFile(configFileWritePath, JSON.stringify(configData, null, 2))
.then(() =>
logger.success(`Configuration saved to ${configFileWritePath}`)
)
.catch((err) =>
logger.error(`Error writing configuration file: ${err.message}`)
);

}

export const init = new Command()
.name("init")
.arguments("[project-name]")
.option("-t, --template <template>", "Specify the template")
.option("-c, --config <config>", "Specify the configuration file")
.description("Scaffold a game")
.action(initAction)
Loading

0 comments on commit 7876e0b

Please sign in to comment.