@puzzmo/cli 1.0.22 → 1.0.23
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/lib/commands/game/create.js +101 -45
- package/lib/commands/game/create.js.map +1 -1
- package/lib/commands/game/detectRepo.d.ts +9 -0
- package/lib/commands/game/detectRepo.js +77 -0
- package/lib/commands/game/detectRepo.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/game/create.ts +104 -46
- package/src/commands/game/detectRepo.ts +81 -0
|
@@ -8,6 +8,7 @@ import { runCommand, gitCommit } from "../../util/exec.js";
|
|
|
8
8
|
import { runSkillsPipelineTUI } from "../../skills/runner.js";
|
|
9
9
|
import { login } from "../login.js";
|
|
10
10
|
import { getToken } from "../../util/config.js";
|
|
11
|
+
import { detectRepoContext } from "./detectRepo.js";
|
|
11
12
|
/** Parses CLI args into CreateOptions */
|
|
12
13
|
const parseArgs = (args) => {
|
|
13
14
|
const opts = {};
|
|
@@ -33,32 +34,79 @@ const parseArgs = (args) => {
|
|
|
33
34
|
}
|
|
34
35
|
return opts;
|
|
35
36
|
};
|
|
36
|
-
/** Converts a string to a URL-friendly slug */
|
|
37
|
-
const slugify = (
|
|
37
|
+
/** Converts a string to a URL-friendly slug (matches packages/shared/slugify.ts) */
|
|
38
|
+
const slugify = (text) => text
|
|
39
|
+
.toString()
|
|
40
|
+
.normalize("NFD")
|
|
41
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
38
42
|
.toLowerCase()
|
|
39
|
-
.
|
|
40
|
-
.replace(
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/\s+/g, "-")
|
|
45
|
+
.replace(/[^\w-]+/g, "")
|
|
46
|
+
.replace(/--+/g, "-");
|
|
47
|
+
/** Writes .mcp.json with dev server config */
|
|
48
|
+
const writeMcpConfig = (dir) => {
|
|
49
|
+
const token = getToken();
|
|
50
|
+
const mcpConfig = {
|
|
51
|
+
mcpServers: {
|
|
52
|
+
"dev.puzzmo.com": {
|
|
53
|
+
type: "http",
|
|
54
|
+
// url: "https://dev.puzzmo.com/api/mcp",
|
|
55
|
+
url: "https://dev-dj9e.onrender.com/api/mcp",
|
|
56
|
+
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
fs.writeFileSync(path.join(dir, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
61
|
+
};
|
|
62
|
+
/** Sets up a standalone new repo for the game */
|
|
63
|
+
const setupNewRepo = (gameDir) => {
|
|
64
|
+
fs.writeFileSync(path.join(gameDir, ".gitignore"), ["node_modules", "dist", ".DS_Store", ".yarn", ".pnp.*", ".puzzmo", ""].join("\n"));
|
|
65
|
+
writeMcpConfig(gameDir);
|
|
66
|
+
if (!fs.existsSync(path.join(gameDir, ".git"))) {
|
|
67
|
+
runCommand("git init", { cwd: gameDir });
|
|
68
|
+
runCommand("git add -A", { cwd: gameDir });
|
|
69
|
+
gitCommit("Initial game import", { cwd: gameDir });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** Sets up the game inside an existing monorepo */
|
|
73
|
+
const setupMonorepoGame = (tmpDir, slug, repoRoot, parentFolder) => {
|
|
74
|
+
const parentDir = path.join(repoRoot, parentFolder);
|
|
75
|
+
if (!fs.existsSync(parentDir))
|
|
76
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
77
|
+
const gameDir = path.join(parentDir, slug);
|
|
78
|
+
if (fs.existsSync(gameDir))
|
|
79
|
+
fs.rmSync(gameDir, { recursive: true });
|
|
80
|
+
fs.renameSync(tmpDir, gameDir);
|
|
81
|
+
// Write .mcp.json at repo root if not already present
|
|
82
|
+
const mcpPath = path.join(repoRoot, ".mcp.json");
|
|
83
|
+
if (!fs.existsSync(mcpPath))
|
|
84
|
+
writeMcpConfig(repoRoot);
|
|
85
|
+
runCommand("git add -A", { cwd: repoRoot });
|
|
86
|
+
gitCommit(`New game: ${slug}`, { cwd: repoRoot });
|
|
87
|
+
return gameDir;
|
|
88
|
+
};
|
|
41
89
|
/** Main game create wizard */
|
|
42
90
|
export const gameCreate = async (args) => {
|
|
43
91
|
const opts = parseArgs(args);
|
|
44
92
|
const require = createRequire(import.meta.url);
|
|
45
93
|
const { version } = require("../../../package.json");
|
|
46
|
-
p.intro(`Puzzmo Game
|
|
47
|
-
// Step 1:
|
|
94
|
+
p.intro(`Puzzmo Game Creator v${version}`);
|
|
95
|
+
// Step 1: Detect repo context and choose mode
|
|
96
|
+
const repo = detectRepoContext();
|
|
97
|
+
const isMonorepo = repo.inGitRepo && repo.hasWorkspaces && repo.workspaceFolders.length > 0;
|
|
98
|
+
const modeOptions = [
|
|
99
|
+
{ value: "new-repo", label: "Create game in a new repo" },
|
|
100
|
+
{ value: "add-to-repo", label: "Add a game to this repo" },
|
|
101
|
+
];
|
|
102
|
+
if (isMonorepo)
|
|
103
|
+
modeOptions.reverse();
|
|
48
104
|
const mode = await p.select({
|
|
49
105
|
message: "How would you like to create your game?",
|
|
50
|
-
options:
|
|
51
|
-
{ value: "import", label: "Import an HTML game from a web page" },
|
|
52
|
-
{ value: "new-repo", label: "Create game in a new repo", hint: "coming soon" },
|
|
53
|
-
{ value: "add-to-repo", label: "Add a game to this repo", hint: "coming soon" },
|
|
54
|
-
],
|
|
106
|
+
options: modeOptions,
|
|
55
107
|
});
|
|
56
108
|
if (p.isCancel(mode))
|
|
57
109
|
process.exit(0);
|
|
58
|
-
if (mode !== "import") {
|
|
59
|
-
p.log.warn("This mode is not yet supported.");
|
|
60
|
-
process.exit(0);
|
|
61
|
-
}
|
|
62
110
|
// Step 2: Source URL
|
|
63
111
|
let url = opts.url;
|
|
64
112
|
if (!url) {
|
|
@@ -74,7 +122,7 @@ export const gameCreate = async (args) => {
|
|
|
74
122
|
s.start(`Downloading ${url}`);
|
|
75
123
|
const { title } = await downloadPage(url, tmpDir);
|
|
76
124
|
s.stop("Download complete.");
|
|
77
|
-
// Step 4: Game name
|
|
125
|
+
// Step 4: Game name
|
|
78
126
|
const defaultName = opts.name || title;
|
|
79
127
|
const name = (await p.text({
|
|
80
128
|
message: "Game name",
|
|
@@ -83,39 +131,46 @@ export const gameCreate = async (args) => {
|
|
|
83
131
|
}));
|
|
84
132
|
if (p.isCancel(name))
|
|
85
133
|
process.exit(0);
|
|
86
|
-
// Step 5: Move downloaded files to slugified directory
|
|
87
134
|
const slug = slugify(name);
|
|
88
|
-
|
|
89
|
-
if (fs.existsSync(gameDir))
|
|
90
|
-
fs.rmSync(gameDir, { recursive: true });
|
|
91
|
-
fs.renameSync(tmpDir, gameDir);
|
|
92
|
-
// Step 6: Login if token provided
|
|
135
|
+
// Step 5: Login if token provided
|
|
93
136
|
if (opts.accessToken) {
|
|
94
137
|
p.log.step("Logging in...");
|
|
95
138
|
login(opts.accessToken);
|
|
96
139
|
}
|
|
97
|
-
// Step
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
// Step 6: Diamond — set up repo or monorepo subdir
|
|
141
|
+
let gameDir;
|
|
142
|
+
if (mode === "new-repo") {
|
|
143
|
+
gameDir = path.resolve(slug);
|
|
144
|
+
if (fs.existsSync(gameDir))
|
|
145
|
+
fs.rmSync(gameDir, { recursive: true });
|
|
146
|
+
fs.renameSync(tmpDir, gameDir);
|
|
147
|
+
setupNewRepo(gameDir);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (!repo.repoRoot) {
|
|
151
|
+
p.log.error("Could not detect repository root. Are you inside a git repo?");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
// Determine which workspace folder to place the game in
|
|
155
|
+
let parentFolder;
|
|
156
|
+
const hasGames = repo.workspaceFolders.includes("games");
|
|
157
|
+
if (hasGames) {
|
|
158
|
+
parentFolder = "games";
|
|
159
|
+
}
|
|
160
|
+
else if (repo.workspaceFolders.length === 1) {
|
|
161
|
+
parentFolder = repo.workspaceFolders[0];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
parentFolder = (await p.select({
|
|
165
|
+
message: "Which folder should the game be added to?",
|
|
166
|
+
options: repo.workspaceFolders.map((f) => ({ value: f, label: f })),
|
|
167
|
+
}));
|
|
168
|
+
if (p.isCancel(parentFolder))
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
gameDir = setupMonorepoGame(tmpDir, slug, repo.repoRoot, parentFolder);
|
|
117
172
|
}
|
|
118
|
-
// Step
|
|
173
|
+
// Step 7: Detect agent and run skills pipeline
|
|
119
174
|
const agents = detectAgent();
|
|
120
175
|
const agentChoices = [
|
|
121
176
|
...agents.map((a) => ({ value: a.binary, label: `${a.displayName} (${a.path})` })),
|
|
@@ -141,7 +196,8 @@ export const gameCreate = async (args) => {
|
|
|
141
196
|
// Done
|
|
142
197
|
const pm = opts.pm || "npm";
|
|
143
198
|
const runCmd = pm === "npm" ? "npx" : pm === "yarn" ? "yarn dlx" : "pnpm dlx";
|
|
144
|
-
|
|
145
|
-
p.
|
|
199
|
+
const relativePath = path.relative(process.cwd(), gameDir);
|
|
200
|
+
p.note([`cd ${relativePath}`, `${runCmd} vite # Start development server`, `${runCmd} vite build # Build for production`].join("\n"), "Next steps");
|
|
201
|
+
p.outro(`Done! Your game is in ./${relativePath}/`);
|
|
146
202
|
};
|
|
147
203
|
//# sourceMappingURL=create.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../../src/commands/game/create.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,KAAK,CAAC,MAAM,gBAAgB,CAAA;AAEnC,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../../src/commands/game/create.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type RepoContext = {
|
|
2
|
+
inGitRepo: boolean;
|
|
3
|
+
repoRoot: string | null;
|
|
4
|
+
hasWorkspaces: boolean;
|
|
5
|
+
/** Top-level workspace folders derived from globs (e.g. ["apps", "packages", "games"]) */
|
|
6
|
+
workspaceFolders: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Detects whether we're inside a git repo and if it's a monorepo with workspace folders */
|
|
9
|
+
export declare const detectRepoContext: () => RepoContext;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
/** Extracts top-level folder names from workspace glob patterns like "apps/*" or "packages/**" */
|
|
5
|
+
const foldersFromGlobs = (globs) => {
|
|
6
|
+
const folders = new Set();
|
|
7
|
+
for (const glob of globs) {
|
|
8
|
+
const topLevel = glob.split("/")[0];
|
|
9
|
+
if (topLevel && !topLevel.includes("*"))
|
|
10
|
+
folders.add(topLevel);
|
|
11
|
+
}
|
|
12
|
+
return [...folders];
|
|
13
|
+
};
|
|
14
|
+
/** Reads workspace globs from package.json (npm/yarn) */
|
|
15
|
+
const readPackageJsonWorkspaces = (repoRoot) => {
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
18
|
+
if (!fs.existsSync(pkgPath))
|
|
19
|
+
return null;
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
21
|
+
const ws = pkg.workspaces;
|
|
22
|
+
if (Array.isArray(ws))
|
|
23
|
+
return ws;
|
|
24
|
+
if (typeof ws === "object" && ws !== null)
|
|
25
|
+
return ws.packages ?? [];
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// invalid JSON
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
/** Reads workspace globs from pnpm-workspace.yaml */
|
|
33
|
+
const readPnpmWorkspaces = (repoRoot) => {
|
|
34
|
+
try {
|
|
35
|
+
const yamlPath = path.join(repoRoot, "pnpm-workspace.yaml");
|
|
36
|
+
if (!fs.existsSync(yamlPath))
|
|
37
|
+
return null;
|
|
38
|
+
const content = fs.readFileSync(yamlPath, "utf-8");
|
|
39
|
+
// Simple YAML parsing for the packages array — avoids a yaml dependency
|
|
40
|
+
const packages = [];
|
|
41
|
+
let inPackages = false;
|
|
42
|
+
for (const line of content.split("\n")) {
|
|
43
|
+
if (/^packages\s*:/.test(line)) {
|
|
44
|
+
inPackages = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (inPackages) {
|
|
48
|
+
const match = line.match(/^\s+-\s+['"]?([^'"#]+)['"]?/);
|
|
49
|
+
if (match)
|
|
50
|
+
packages.push(match[1].trim());
|
|
51
|
+
else if (/^\S/.test(line))
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return packages.length > 0 ? packages : null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// missing or unreadable
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
/** Detects whether we're inside a git repo and if it's a monorepo with workspace folders */
|
|
63
|
+
export const detectRepoContext = () => {
|
|
64
|
+
let repoRoot = null;
|
|
65
|
+
try {
|
|
66
|
+
const result = execSync("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
67
|
+
repoRoot = result.trim();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return { inGitRepo: false, repoRoot: null, hasWorkspaces: false, workspaceFolders: [] };
|
|
71
|
+
}
|
|
72
|
+
const globs = readPackageJsonWorkspaces(repoRoot) ?? readPnpmWorkspaces(repoRoot);
|
|
73
|
+
const hasWorkspaces = globs !== null;
|
|
74
|
+
const workspaceFolders = globs ? foldersFromGlobs(globs) : [];
|
|
75
|
+
return { inGitRepo: true, repoRoot, hasWorkspaces, workspaceFolders };
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=detectRepo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectRepo.js","sourceRoot":"","sources":["../../../src/commands/game/detectRepo.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { runCommand, gitCommit } from "../../util/exec.js"
|
|
|
9
9
|
import { runSkillsPipelineTUI } from "../../skills/runner.js"
|
|
10
10
|
import { login } from "../login.js"
|
|
11
11
|
import { getToken } from "../../util/config.js"
|
|
12
|
+
import { detectRepoContext } from "./detectRepo.js"
|
|
12
13
|
|
|
13
14
|
type CreateOptions = {
|
|
14
15
|
name?: string
|
|
@@ -42,12 +43,64 @@ const parseArgs = (args: string[]): CreateOptions => {
|
|
|
42
43
|
return opts
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/** Converts a string to a URL-friendly slug */
|
|
46
|
-
const slugify = (
|
|
47
|
-
|
|
46
|
+
/** Converts a string to a URL-friendly slug (matches packages/shared/slugify.ts) */
|
|
47
|
+
const slugify = (text: string) =>
|
|
48
|
+
text
|
|
49
|
+
.toString()
|
|
50
|
+
.normalize("NFD")
|
|
51
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
48
52
|
.toLowerCase()
|
|
49
|
-
.
|
|
50
|
-
.replace(
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/\s+/g, "-")
|
|
55
|
+
.replace(/[^\w-]+/g, "")
|
|
56
|
+
.replace(/--+/g, "-")
|
|
57
|
+
|
|
58
|
+
/** Writes .mcp.json with dev server config */
|
|
59
|
+
const writeMcpConfig = (dir: string) => {
|
|
60
|
+
const token = getToken()
|
|
61
|
+
const mcpConfig = {
|
|
62
|
+
mcpServers: {
|
|
63
|
+
"dev.puzzmo.com": {
|
|
64
|
+
type: "http",
|
|
65
|
+
// url: "https://dev.puzzmo.com/api/mcp",
|
|
66
|
+
url: "https://dev-dj9e.onrender.com/api/mcp",
|
|
67
|
+
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
fs.writeFileSync(path.join(dir, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Sets up a standalone new repo for the game */
|
|
75
|
+
const setupNewRepo = (gameDir: string) => {
|
|
76
|
+
fs.writeFileSync(path.join(gameDir, ".gitignore"), ["node_modules", "dist", ".DS_Store", ".yarn", ".pnp.*", ".puzzmo", ""].join("\n"))
|
|
77
|
+
writeMcpConfig(gameDir)
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(path.join(gameDir, ".git"))) {
|
|
80
|
+
runCommand("git init", { cwd: gameDir })
|
|
81
|
+
runCommand("git add -A", { cwd: gameDir })
|
|
82
|
+
gitCommit("Initial game import", { cwd: gameDir })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Sets up the game inside an existing monorepo */
|
|
87
|
+
const setupMonorepoGame = (tmpDir: string, slug: string, repoRoot: string, parentFolder: string): string => {
|
|
88
|
+
const parentDir = path.join(repoRoot, parentFolder)
|
|
89
|
+
if (!fs.existsSync(parentDir)) fs.mkdirSync(parentDir, { recursive: true })
|
|
90
|
+
|
|
91
|
+
const gameDir = path.join(parentDir, slug)
|
|
92
|
+
if (fs.existsSync(gameDir)) fs.rmSync(gameDir, { recursive: true })
|
|
93
|
+
fs.renameSync(tmpDir, gameDir)
|
|
94
|
+
|
|
95
|
+
// Write .mcp.json at repo root if not already present
|
|
96
|
+
const mcpPath = path.join(repoRoot, ".mcp.json")
|
|
97
|
+
if (!fs.existsSync(mcpPath)) writeMcpConfig(repoRoot)
|
|
98
|
+
|
|
99
|
+
runCommand("git add -A", { cwd: repoRoot })
|
|
100
|
+
gitCommit(`New game: ${slug}`, { cwd: repoRoot })
|
|
101
|
+
|
|
102
|
+
return gameDir
|
|
103
|
+
}
|
|
51
104
|
|
|
52
105
|
/** Main game create wizard */
|
|
53
106
|
export const gameCreate = async (args: string[]) => {
|
|
@@ -55,25 +108,25 @@ export const gameCreate = async (args: string[]) => {
|
|
|
55
108
|
|
|
56
109
|
const require = createRequire(import.meta.url)
|
|
57
110
|
const { version } = require("../../../package.json")
|
|
58
|
-
p.intro(`Puzzmo Game
|
|
111
|
+
p.intro(`Puzzmo Game Creator v${version}`)
|
|
112
|
+
|
|
113
|
+
// Step 1: Detect repo context and choose mode
|
|
114
|
+
const repo = detectRepoContext()
|
|
115
|
+
const isMonorepo = repo.inGitRepo && repo.hasWorkspaces && repo.workspaceFolders.length > 0
|
|
116
|
+
|
|
117
|
+
const modeOptions = [
|
|
118
|
+
{ value: "new-repo" as const, label: "Create game in a new repo" },
|
|
119
|
+
{ value: "add-to-repo" as const, label: "Add a game to this repo" },
|
|
120
|
+
]
|
|
121
|
+
if (isMonorepo) modeOptions.reverse()
|
|
59
122
|
|
|
60
|
-
// Step 1: Mode selection
|
|
61
123
|
const mode = await p.select({
|
|
62
124
|
message: "How would you like to create your game?",
|
|
63
|
-
options:
|
|
64
|
-
{ value: "import", label: "Import an HTML game from a web page" },
|
|
65
|
-
{ value: "new-repo", label: "Create game in a new repo", hint: "coming soon" },
|
|
66
|
-
{ value: "add-to-repo", label: "Add a game to this repo", hint: "coming soon" },
|
|
67
|
-
],
|
|
125
|
+
options: modeOptions,
|
|
68
126
|
})
|
|
69
127
|
|
|
70
128
|
if (p.isCancel(mode)) process.exit(0)
|
|
71
129
|
|
|
72
|
-
if (mode !== "import") {
|
|
73
|
-
p.log.warn("This mode is not yet supported.")
|
|
74
|
-
process.exit(0)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
130
|
// Step 2: Source URL
|
|
78
131
|
let url = opts.url
|
|
79
132
|
if (!url) {
|
|
@@ -89,7 +142,7 @@ export const gameCreate = async (args: string[]) => {
|
|
|
89
142
|
const { title } = await downloadPage(url, tmpDir)
|
|
90
143
|
s.stop("Download complete.")
|
|
91
144
|
|
|
92
|
-
// Step 4: Game name
|
|
145
|
+
// Step 4: Game name
|
|
93
146
|
const defaultName = opts.name || title
|
|
94
147
|
const name = (await p.text({
|
|
95
148
|
message: "Game name",
|
|
@@ -98,44 +151,48 @@ export const gameCreate = async (args: string[]) => {
|
|
|
98
151
|
})) as string
|
|
99
152
|
if (p.isCancel(name)) process.exit(0)
|
|
100
153
|
|
|
101
|
-
// Step 5: Move downloaded files to slugified directory
|
|
102
154
|
const slug = slugify(name)
|
|
103
|
-
const gameDir = path.resolve(slug)
|
|
104
|
-
if (fs.existsSync(gameDir)) fs.rmSync(gameDir, { recursive: true })
|
|
105
|
-
fs.renameSync(tmpDir, gameDir)
|
|
106
155
|
|
|
107
|
-
// Step
|
|
156
|
+
// Step 5: Login if token provided
|
|
108
157
|
if (opts.accessToken) {
|
|
109
158
|
p.log.step("Logging in...")
|
|
110
159
|
login(opts.accessToken)
|
|
111
160
|
}
|
|
112
161
|
|
|
113
|
-
// Step
|
|
114
|
-
|
|
162
|
+
// Step 6: Diamond — set up repo or monorepo subdir
|
|
163
|
+
let gameDir: string
|
|
115
164
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
},
|
|
127
|
-
}
|
|
165
|
+
if (mode === "new-repo") {
|
|
166
|
+
gameDir = path.resolve(slug)
|
|
167
|
+
if (fs.existsSync(gameDir)) fs.rmSync(gameDir, { recursive: true })
|
|
168
|
+
fs.renameSync(tmpDir, gameDir)
|
|
169
|
+
setupNewRepo(gameDir)
|
|
170
|
+
} else {
|
|
171
|
+
if (!repo.repoRoot) {
|
|
172
|
+
p.log.error("Could not detect repository root. Are you inside a git repo?")
|
|
173
|
+
process.exit(1)
|
|
174
|
+
}
|
|
128
175
|
|
|
129
|
-
|
|
176
|
+
// Determine which workspace folder to place the game in
|
|
177
|
+
let parentFolder: string
|
|
178
|
+
const hasGames = repo.workspaceFolders.includes("games")
|
|
179
|
+
|
|
180
|
+
if (hasGames) {
|
|
181
|
+
parentFolder = "games"
|
|
182
|
+
} else if (repo.workspaceFolders.length === 1) {
|
|
183
|
+
parentFolder = repo.workspaceFolders[0]
|
|
184
|
+
} else {
|
|
185
|
+
parentFolder = (await p.select({
|
|
186
|
+
message: "Which folder should the game be added to?",
|
|
187
|
+
options: repo.workspaceFolders.map((f) => ({ value: f, label: f })),
|
|
188
|
+
})) as string
|
|
189
|
+
if (p.isCancel(parentFolder)) process.exit(0)
|
|
190
|
+
}
|
|
130
191
|
|
|
131
|
-
|
|
132
|
-
if (!fs.existsSync(path.join(gameDir, ".git"))) {
|
|
133
|
-
runCommand("git init", { cwd: gameDir })
|
|
134
|
-
runCommand("git add -A", { cwd: gameDir })
|
|
135
|
-
gitCommit("Initial game import", { cwd: gameDir })
|
|
192
|
+
gameDir = setupMonorepoGame(tmpDir, slug, repo.repoRoot, parentFolder)
|
|
136
193
|
}
|
|
137
194
|
|
|
138
|
-
// Step
|
|
195
|
+
// Step 7: Detect agent and run skills pipeline
|
|
139
196
|
const agents = detectAgent()
|
|
140
197
|
const agentChoices = [
|
|
141
198
|
...agents.map((a) => ({ value: a.binary, label: `${a.displayName} (${a.path})` })),
|
|
@@ -161,11 +218,12 @@ export const gameCreate = async (args: string[]) => {
|
|
|
161
218
|
// Done
|
|
162
219
|
const pm = opts.pm || "npm"
|
|
163
220
|
const runCmd = pm === "npm" ? "npx" : pm === "yarn" ? "yarn dlx" : "pnpm dlx"
|
|
221
|
+
const relativePath = path.relative(process.cwd(), gameDir)
|
|
164
222
|
|
|
165
223
|
p.note(
|
|
166
|
-
[`cd ${
|
|
224
|
+
[`cd ${relativePath}`, `${runCmd} vite # Start development server`, `${runCmd} vite build # Build for production`].join("\n"),
|
|
167
225
|
"Next steps",
|
|
168
226
|
)
|
|
169
227
|
|
|
170
|
-
p.outro(`Done! Your game is in ./${
|
|
228
|
+
p.outro(`Done! Your game is in ./${relativePath}/`)
|
|
171
229
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { execSync } from "node:child_process"
|
|
4
|
+
|
|
5
|
+
export type RepoContext = {
|
|
6
|
+
inGitRepo: boolean
|
|
7
|
+
repoRoot: string | null
|
|
8
|
+
hasWorkspaces: boolean
|
|
9
|
+
/** Top-level workspace folders derived from globs (e.g. ["apps", "packages", "games"]) */
|
|
10
|
+
workspaceFolders: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Extracts top-level folder names from workspace glob patterns like "apps/*" or "packages/**" */
|
|
14
|
+
const foldersFromGlobs = (globs: string[]): string[] => {
|
|
15
|
+
const folders = new Set<string>()
|
|
16
|
+
for (const glob of globs) {
|
|
17
|
+
const topLevel = glob.split("/")[0]
|
|
18
|
+
if (topLevel && !topLevel.includes("*")) folders.add(topLevel)
|
|
19
|
+
}
|
|
20
|
+
return [...folders]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Reads workspace globs from package.json (npm/yarn) */
|
|
24
|
+
const readPackageJsonWorkspaces = (repoRoot: string): string[] | null => {
|
|
25
|
+
try {
|
|
26
|
+
const pkgPath = path.join(repoRoot, "package.json")
|
|
27
|
+
if (!fs.existsSync(pkgPath)) return null
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
29
|
+
const ws = pkg.workspaces
|
|
30
|
+
if (Array.isArray(ws)) return ws
|
|
31
|
+
if (typeof ws === "object" && ws !== null) return ws.packages ?? []
|
|
32
|
+
} catch {
|
|
33
|
+
// invalid JSON
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reads workspace globs from pnpm-workspace.yaml */
|
|
39
|
+
const readPnpmWorkspaces = (repoRoot: string): string[] | null => {
|
|
40
|
+
try {
|
|
41
|
+
const yamlPath = path.join(repoRoot, "pnpm-workspace.yaml")
|
|
42
|
+
if (!fs.existsSync(yamlPath)) return null
|
|
43
|
+
const content = fs.readFileSync(yamlPath, "utf-8")
|
|
44
|
+
// Simple YAML parsing for the packages array — avoids a yaml dependency
|
|
45
|
+
const packages: string[] = []
|
|
46
|
+
let inPackages = false
|
|
47
|
+
for (const line of content.split("\n")) {
|
|
48
|
+
if (/^packages\s*:/.test(line)) {
|
|
49
|
+
inPackages = true
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (inPackages) {
|
|
53
|
+
const match = line.match(/^\s+-\s+['"]?([^'"#]+)['"]?/)
|
|
54
|
+
if (match) packages.push(match[1].trim())
|
|
55
|
+
else if (/^\S/.test(line)) break
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return packages.length > 0 ? packages : null
|
|
59
|
+
} catch {
|
|
60
|
+
// missing or unreadable
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Detects whether we're inside a git repo and if it's a monorepo with workspace folders */
|
|
66
|
+
export const detectRepoContext = (): RepoContext => {
|
|
67
|
+
let repoRoot: string | null = null
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = execSync("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] })
|
|
71
|
+
repoRoot = result.trim()
|
|
72
|
+
} catch {
|
|
73
|
+
return { inGitRepo: false, repoRoot: null, hasWorkspaces: false, workspaceFolders: [] }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const globs = readPackageJsonWorkspaces(repoRoot) ?? readPnpmWorkspaces(repoRoot)
|
|
77
|
+
const hasWorkspaces = globs !== null
|
|
78
|
+
const workspaceFolders = globs ? foldersFromGlobs(globs) : []
|
|
79
|
+
|
|
80
|
+
return { inGitRepo: true, repoRoot, hasWorkspaces, workspaceFolders }
|
|
81
|
+
}
|