@puzzmo/cli 1.0.22 → 1.0.24
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 +104 -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 +80 -0
- package/lib/commands/game/detectRepo.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/game/create.ts +106 -46
- package/src/commands/game/detectRepo.ts +84 -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,49 @@ 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 === 0) {
|
|
161
|
+
parentFolder = "games";
|
|
162
|
+
}
|
|
163
|
+
else if (repo.workspaceFolders.length === 1) {
|
|
164
|
+
parentFolder = repo.workspaceFolders[0];
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
parentFolder = (await p.select({
|
|
168
|
+
message: "Which folder should the game be added to?",
|
|
169
|
+
options: repo.workspaceFolders.map((f) => ({ value: f, label: f })),
|
|
170
|
+
}));
|
|
171
|
+
if (p.isCancel(parentFolder))
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
gameDir = setupMonorepoGame(tmpDir, slug, repo.repoRoot, parentFolder);
|
|
117
175
|
}
|
|
118
|
-
// Step
|
|
176
|
+
// Step 7: Detect agent and run skills pipeline
|
|
119
177
|
const agents = detectAgent();
|
|
120
178
|
const agentChoices = [
|
|
121
179
|
...agents.map((a) => ({ value: a.binary, label: `${a.displayName} (${a.path})` })),
|
|
@@ -141,7 +199,8 @@ export const gameCreate = async (args) => {
|
|
|
141
199
|
// Done
|
|
142
200
|
const pm = opts.pm || "npm";
|
|
143
201
|
const runCmd = pm === "npm" ? "npx" : pm === "yarn" ? "yarn dlx" : "pnpm dlx";
|
|
144
|
-
|
|
145
|
-
p.
|
|
202
|
+
const relativePath = path.relative(process.cwd(), gameDir);
|
|
203
|
+
p.note([`cd ${relativePath}`, `${runCmd} vite # Start development server`, `${runCmd} vite build # Build for production`].join("\n"), "Next steps");
|
|
204
|
+
p.outro(`Done! Your game is in ./${relativePath}/`);
|
|
146
205
|
};
|
|
147
206
|
//# 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,80 @@
|
|
|
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
|
+
// Also recognize a games/ directory even without formal workspaces config
|
|
76
|
+
if (!workspaceFolders.includes("games") && fs.existsSync(path.join(repoRoot, "games")))
|
|
77
|
+
workspaceFolders.push("games");
|
|
78
|
+
return { inGitRepo: true, repoRoot, hasWorkspaces: hasWorkspaces || workspaceFolders.length > 0, workspaceFolders };
|
|
79
|
+
};
|
|
80
|
+
//# 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,50 @@ 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 === 0) {
|
|
183
|
+
parentFolder = "games"
|
|
184
|
+
} else if (repo.workspaceFolders.length === 1) {
|
|
185
|
+
parentFolder = repo.workspaceFolders[0]
|
|
186
|
+
} else {
|
|
187
|
+
parentFolder = (await p.select({
|
|
188
|
+
message: "Which folder should the game be added to?",
|
|
189
|
+
options: repo.workspaceFolders.map((f) => ({ value: f, label: f })),
|
|
190
|
+
})) as string
|
|
191
|
+
if (p.isCancel(parentFolder)) process.exit(0)
|
|
192
|
+
}
|
|
130
193
|
|
|
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 })
|
|
194
|
+
gameDir = setupMonorepoGame(tmpDir, slug, repo.repoRoot, parentFolder)
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
// Step
|
|
197
|
+
// Step 7: Detect agent and run skills pipeline
|
|
139
198
|
const agents = detectAgent()
|
|
140
199
|
const agentChoices = [
|
|
141
200
|
...agents.map((a) => ({ value: a.binary, label: `${a.displayName} (${a.path})` })),
|
|
@@ -161,11 +220,12 @@ export const gameCreate = async (args: string[]) => {
|
|
|
161
220
|
// Done
|
|
162
221
|
const pm = opts.pm || "npm"
|
|
163
222
|
const runCmd = pm === "npm" ? "npx" : pm === "yarn" ? "yarn dlx" : "pnpm dlx"
|
|
223
|
+
const relativePath = path.relative(process.cwd(), gameDir)
|
|
164
224
|
|
|
165
225
|
p.note(
|
|
166
|
-
[`cd ${
|
|
226
|
+
[`cd ${relativePath}`, `${runCmd} vite # Start development server`, `${runCmd} vite build # Build for production`].join("\n"),
|
|
167
227
|
"Next steps",
|
|
168
228
|
)
|
|
169
229
|
|
|
170
|
-
p.outro(`Done! Your game is in ./${
|
|
230
|
+
p.outro(`Done! Your game is in ./${relativePath}/`)
|
|
171
231
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
// Also recognize a games/ directory even without formal workspaces config
|
|
81
|
+
if (!workspaceFolders.includes("games") && fs.existsSync(path.join(repoRoot, "games"))) workspaceFolders.push("games")
|
|
82
|
+
|
|
83
|
+
return { inGitRepo: true, repoRoot, hasWorkspaces: hasWorkspaces || workspaceFolders.length > 0, workspaceFolders }
|
|
84
|
+
}
|