@puzzmo/cli 1.0.38 → 1.0.40
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 +187 -71
- package/lib/commands/game/create.js.map +1 -1
- package/lib/commands/upload.js +49 -2
- package/lib/commands/upload.js.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/skills/runner.d.ts +2 -0
- package/lib/skills/runner.js +33 -0
- package/lib/skills/runner.js.map +1 -1
- package/lib/util/api.d.ts +5 -0
- package/lib/util/api.js +28 -1
- package/lib/util/api.js.map +1 -1
- package/lib/util/createGame.d.ts +13 -0
- package/lib/util/createGame.js +36 -0
- package/lib/util/createGame.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/game/create.ts +186 -74
- package/src/commands/upload.ts +52 -2
- package/src/index.ts +4 -2
- package/src/skills/runner.ts +35 -0
- package/src/util/api.ts +32 -7
- package/src/util/createGame.ts +60 -0
- package/templates/minesweeper/README.md +13 -0
- package/templates/minesweeper/fixtures/puzzles/easy/9x9.json +16 -0
- package/templates/minesweeper/fixtures/puzzles/hard/16x16.json +46 -0
- package/templates/minesweeper/index.html +19 -0
- package/templates/minesweeper/package.json +18 -0
- package/templates/minesweeper/puzzmo.json +11 -0
- package/templates/minesweeper/src/appBundle.ts +122 -0
- package/templates/minesweeper/src/main.ts +236 -0
- package/templates/minesweeper/src/style.css +160 -0
- package/templates/minesweeper/tsconfig.json +14 -0
- package/templates/minesweeper/vite.config.ts +10 -0
package/lib/util/api.js
CHANGED
|
@@ -2,6 +2,15 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getAPIURL } from "./config.js";
|
|
4
4
|
const BATCH_SIZE = 10;
|
|
5
|
+
/** Thrown when /cliUpload init returns a 404 because the game slug isn't registered yet. */
|
|
6
|
+
export class GameNotFoundError extends Error {
|
|
7
|
+
slug;
|
|
8
|
+
constructor(slug, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.slug = slug;
|
|
11
|
+
this.name = "GameNotFoundError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
5
14
|
/** Wraps fetch to surface the underlying network cause (DNS, ECONNREFUSED, TLS, etc.) */
|
|
6
15
|
const fetchWithContext = async (url, init, step, verbose) => {
|
|
7
16
|
if (verbose)
|
|
@@ -66,8 +75,18 @@ const uploadFile = async (url, token, filePath, baseDir, verbose) => {
|
|
|
66
75
|
},
|
|
67
76
|
body: content,
|
|
68
77
|
}, step, verbose);
|
|
78
|
+
if (res.status === 413)
|
|
79
|
+
throw new Error(`File ${relativePath} is too large to upload (${formatBytes(content.length)})`);
|
|
69
80
|
return (await readResponse(res, fullURL, step, verbose));
|
|
70
81
|
};
|
|
82
|
+
/** Formats a byte count as a human-readable string (e.g. "1.6 MB") */
|
|
83
|
+
const formatBytes = (bytes) => {
|
|
84
|
+
if (bytes < 1024)
|
|
85
|
+
return `${bytes} B`;
|
|
86
|
+
if (bytes < 1024 * 1024)
|
|
87
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
88
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
89
|
+
};
|
|
71
90
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
72
91
|
export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzzmoFile, onProgress, options = {}) => {
|
|
73
92
|
const { verbose = false, description, repoURL } = options;
|
|
@@ -75,7 +94,15 @@ export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzz
|
|
|
75
94
|
if (verbose)
|
|
76
95
|
console.log(` API URL: ${apiURL}`);
|
|
77
96
|
// Step 1: Init session (includes puzzmo.json metadata)
|
|
78
|
-
|
|
97
|
+
let init;
|
|
98
|
+
try {
|
|
99
|
+
init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile, description, repoURL }, "upload init", verbose));
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (e instanceof Error && e.message.includes("Game not found:"))
|
|
103
|
+
throw new GameNotFoundError(gameSlug, e.message);
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
79
106
|
if (verbose)
|
|
80
107
|
console.log(` session: ${init.sessionID}`);
|
|
81
108
|
// Step 2: Upload files in concurrent batches
|
package/lib/util/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/util/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAgBvC,MAAM,UAAU,GAAG,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/util/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type CreateUserGameOptions = {
|
|
2
|
+
/** A teamAccessToken (the same token saved by `puzzmo login`). The mutation uses it to identify the team. */
|
|
3
|
+
teamAccessToken: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export type CreatedGame = {
|
|
7
|
+
id: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
newAccessToken: string | null;
|
|
11
|
+
};
|
|
12
|
+
/** Calls the createUserGame mutation using a teamAccessToken for auth. */
|
|
13
|
+
export declare const createUserGame: (options: CreateUserGameOptions) => Promise<CreatedGame>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getAPIURL } from "./config.js";
|
|
2
|
+
const createUserGameMutation = `
|
|
3
|
+
mutation cliCreateUserGameMutation($teamAccessToken: String!, $input: CreateUserGameInput!) {
|
|
4
|
+
createUserGame(teamAccessToken: $teamAccessToken, input: $input) {
|
|
5
|
+
game { id slug displayName }
|
|
6
|
+
newAccessToken
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
/** Calls the createUserGame mutation using a teamAccessToken for auth. */
|
|
11
|
+
export const createUserGame = async (options) => {
|
|
12
|
+
const url = `${getAPIURL()}/graphql`;
|
|
13
|
+
const response = await fetch(url, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
query: createUserGameMutation,
|
|
18
|
+
variables: {
|
|
19
|
+
teamAccessToken: options.teamAccessToken,
|
|
20
|
+
input: { displayName: options.displayName },
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok)
|
|
25
|
+
throw new Error(`createUserGame request failed: ${response.status} ${response.statusText}`);
|
|
26
|
+
const result = (await response.json());
|
|
27
|
+
if (result.errors?.length) {
|
|
28
|
+
const message = result.errors.map((e) => e.message).join("; ");
|
|
29
|
+
throw new Error(`createUserGame failed: ${message}`);
|
|
30
|
+
}
|
|
31
|
+
if (!result.data?.createUserGame)
|
|
32
|
+
throw new Error("createUserGame returned no data");
|
|
33
|
+
const { game, newAccessToken } = result.data.createUserGame;
|
|
34
|
+
return { id: game.id, slug: game.slug, displayName: game.displayName, newAccessToken };
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=createGame.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createGame.js","sourceRoot":"","sources":["../../src/util/createGame.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,MAAM,sBAAsB,GAAG;;;;;;;CAO9B,CAAA;AAsBD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,OAA8B,EAAwB,EAAE,CAAC;IAC5F,MAAM,GAAG,GAAG,GAAG,SAAS,EAAE,UAAU,CAAA;IAEpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,sBAAsB;YAC7B,SAAS,EAAE;gBACT,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE;aAC5C;SACF,CAAC;KACH,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAE7G,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAA;IAEhE,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9D,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAA;IACtD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IAEpF,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAA;IAC3D,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,cAAc,EAAE,CAAA;AAAA,CACvF,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
3
4
|
import { createRequire } from "node:module"
|
|
4
5
|
import * as p from "@clack/prompts"
|
|
5
6
|
|
|
@@ -7,17 +8,21 @@ import { agentNames } from "../../agents/index.js"
|
|
|
7
8
|
import { detectAgent } from "../../wizard/agent-detect.js"
|
|
8
9
|
import { downloadPage } from "../../download/page-downloader.js"
|
|
9
10
|
import { runCommand, gitCommit } from "../../util/exec.js"
|
|
10
|
-
import { runSkillsPipelineTUI } from "../../skills/runner.js"
|
|
11
|
+
import { runSkillsPipelineTUI, runAgentWithBuildLoop } from "../../skills/runner.js"
|
|
11
12
|
import { login } from "../login.js"
|
|
12
13
|
import { getToken } from "../../util/config.js"
|
|
13
14
|
import { detectRepoContext, type RepoType } from "./detectRepo.js"
|
|
14
15
|
|
|
16
|
+
type Strategy = "import" | "blank" | "prompt"
|
|
17
|
+
|
|
15
18
|
type CreateOptions = {
|
|
16
19
|
name?: string
|
|
17
20
|
url?: string
|
|
18
21
|
agent?: string
|
|
19
22
|
accessToken?: string
|
|
20
23
|
pm?: string
|
|
24
|
+
strategy?: Strategy
|
|
25
|
+
prompt?: string
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/** Parses CLI args into CreateOptions */
|
|
@@ -27,16 +32,15 @@ const parseArgs = (args: string[]): CreateOptions => {
|
|
|
27
32
|
|
|
28
33
|
while (i < args.length) {
|
|
29
34
|
const arg = args[i]
|
|
30
|
-
if (arg === "--name" && args[i + 1])
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
opts.pm = args[++i]
|
|
35
|
+
if (arg === "--name" && args[i + 1]) opts.name = args[++i]
|
|
36
|
+
else if (arg === "--url" && args[i + 1]) opts.url = args[++i]
|
|
37
|
+
else if (arg === "--agent" && args[i + 1]) opts.agent = args[++i]
|
|
38
|
+
else if (arg === "--token" && args[i + 1]) opts.accessToken = args[++i]
|
|
39
|
+
else if (arg === "--pm" && args[i + 1]) opts.pm = args[++i]
|
|
40
|
+
else if (arg === "--prompt" && args[i + 1]) opts.prompt = args[++i]
|
|
41
|
+
else if (arg === "--strategy" && args[i + 1]) {
|
|
42
|
+
const v = args[++i]
|
|
43
|
+
if (v === "import" || v === "blank" || v === "prompt") opts.strategy = v
|
|
40
44
|
}
|
|
41
45
|
i++
|
|
42
46
|
}
|
|
@@ -49,7 +53,7 @@ const slugify = (text: string) =>
|
|
|
49
53
|
text
|
|
50
54
|
.toString()
|
|
51
55
|
.normalize("NFD")
|
|
52
|
-
.replace(/[
|
|
56
|
+
.replace(/[̀-ͯ]/g, "")
|
|
53
57
|
.toLowerCase()
|
|
54
58
|
.trim()
|
|
55
59
|
.replace(/\s+/g, "-")
|
|
@@ -157,6 +161,71 @@ const convertToMultiGame = (repoRoot: string): string => {
|
|
|
157
161
|
return existingName
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
/** Resolves the bundled template directory by name. Templates ship at packages/cli/templates/<name>/ */
|
|
165
|
+
const resolveTemplateDir = (name: string): string => {
|
|
166
|
+
const here = fileURLToPath(import.meta.url)
|
|
167
|
+
// here = .../packages/cli/lib/commands/game/create.js → ../../../templates/<name>
|
|
168
|
+
return path.resolve(here, "..", "..", "..", "..", "templates", name)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** File extensions where we apply template variable substitution */
|
|
172
|
+
const textExtension = new Set([".md", ".json", ".html", ".css", ".ts", ".tsx", ".js", ".jsx", ".mjs"])
|
|
173
|
+
|
|
174
|
+
/** Recursively copies a template directory, substituting placeholders in text files. */
|
|
175
|
+
const copyTemplate = (sourceDir: string, targetDir: string, replacements: Record<string, string>) => {
|
|
176
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
177
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
178
|
+
const src = path.join(sourceDir, entry.name)
|
|
179
|
+
const dst = path.join(targetDir, entry.name)
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
copyTemplate(src, dst, replacements)
|
|
182
|
+
} else if (entry.isFile()) {
|
|
183
|
+
const ext = path.extname(entry.name)
|
|
184
|
+
if (textExtension.has(ext) || entry.name === ".gitignore") {
|
|
185
|
+
let content = fs.readFileSync(src, "utf-8")
|
|
186
|
+
for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(key, value)
|
|
187
|
+
fs.writeFileSync(dst, content)
|
|
188
|
+
} else {
|
|
189
|
+
fs.copyFileSync(src, dst)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Picks an agent interactively, returning the agent id or "none" */
|
|
196
|
+
const pickAgent = async (preselected?: string): Promise<string> => {
|
|
197
|
+
const supported = new Set(agentNames())
|
|
198
|
+
const agents = detectAgent().filter((a) => supported.has(a.id))
|
|
199
|
+
if (preselected) return preselected
|
|
200
|
+
if (agents.length === 0) {
|
|
201
|
+
p.log.info(`No supported LLM agents detected (checked: ${agentNames().join(", ")})`)
|
|
202
|
+
return "none"
|
|
203
|
+
}
|
|
204
|
+
const choices = [
|
|
205
|
+
...agents.map((a) => ({ value: a.id, label: `${a.displayName} (${a.path})` })),
|
|
206
|
+
{ value: "none", label: "None - I'll run the steps manually" },
|
|
207
|
+
]
|
|
208
|
+
const result = (await p.select({ message: "Which LLM agent do you use?", options: choices })) as string
|
|
209
|
+
if (p.isCancel(result)) process.exit(0)
|
|
210
|
+
return result
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Composes the prompt sent to the agent for the "from prompt" strategy */
|
|
214
|
+
const buildPromptStrategyMessage = (userPrompt: string, displayName: string): string =>
|
|
215
|
+
[
|
|
216
|
+
`You are scaffolding a Puzzmo game called "${displayName}" in the current directory.`,
|
|
217
|
+
`The starter is a working Vite + Puzzmo SDK Minesweeper. Replace the gameplay with the game described below.`,
|
|
218
|
+
``,
|
|
219
|
+
`Game description:`,
|
|
220
|
+
userPrompt,
|
|
221
|
+
``,
|
|
222
|
+
`Constraints:`,
|
|
223
|
+
`- Keep puzzmo.json valid (do not remove the slug or displayName fields).`,
|
|
224
|
+
`- Use the Puzzmo SDK lifecycle: gameReady → gameLoaded → on("start"|"retry") → updateGameState → gameCompleted.`,
|
|
225
|
+
`- Replace fixtures in fixtures/puzzles/ with puzzles for the new game.`,
|
|
226
|
+
`- Keep the build green (\`npx vite build\` should succeed).`,
|
|
227
|
+
].join("\n")
|
|
228
|
+
|
|
160
229
|
/** Main game create wizard */
|
|
161
230
|
export const gameCreate = async (args: string[]) => {
|
|
162
231
|
const opts = parseArgs(args)
|
|
@@ -165,56 +234,105 @@ export const gameCreate = async (args: string[]) => {
|
|
|
165
234
|
const { version } = require("../../../package.json")
|
|
166
235
|
p.intro(`Puzzmo Game Creator v${version}`)
|
|
167
236
|
|
|
168
|
-
// Step 1:
|
|
237
|
+
// Step 1: Pick strategy
|
|
238
|
+
let strategy: Strategy
|
|
239
|
+
if (opts.strategy) {
|
|
240
|
+
strategy = opts.strategy
|
|
241
|
+
} else {
|
|
242
|
+
const choice = await p.select({
|
|
243
|
+
message: "How would you like to create your game?",
|
|
244
|
+
options: [
|
|
245
|
+
{ value: "blank" as const, label: "Blank game (Minesweeper template)" },
|
|
246
|
+
{ value: "import" as const, label: "Import from an existing URL (uses an LLM to migrate)" },
|
|
247
|
+
{ value: "prompt" as const, label: "From a prompt (Builds on template + LLM customizations)" },
|
|
248
|
+
],
|
|
249
|
+
})
|
|
250
|
+
if (p.isCancel(choice)) process.exit(0)
|
|
251
|
+
strategy = choice as Strategy
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Step 2: Detect repo and pick placement
|
|
169
255
|
const repo = detectRepoContext()
|
|
170
256
|
const canAddToRepo = repo.repoType === "multi-game" || repo.repoType === "workspace-monorepo" || repo.repoType === "standalone"
|
|
171
257
|
|
|
172
|
-
const
|
|
258
|
+
const placementOptions = [
|
|
173
259
|
{ value: "new-repo" as const, label: "Create game in a new repo" },
|
|
174
260
|
...(canAddToRepo ? [{ value: "add-to-repo" as const, label: "Add a game to this repo" }] : []),
|
|
175
261
|
]
|
|
176
|
-
if (canAddToRepo && repo.repoType !== "none")
|
|
262
|
+
if (canAddToRepo && repo.repoType !== "none") placementOptions.reverse()
|
|
177
263
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Step 2: Source URL
|
|
186
|
-
let url = opts.url
|
|
187
|
-
if (!url) {
|
|
188
|
-
url = (await p.text({ message: "Source URL to import", validate: (v) => (!v ? "URL is required" : undefined) })) as string
|
|
189
|
-
if (p.isCancel(url)) process.exit(0)
|
|
264
|
+
let mode: "new-repo" | "add-to-repo"
|
|
265
|
+
if (placementOptions.length === 1) {
|
|
266
|
+
mode = placementOptions[0].value
|
|
267
|
+
} else {
|
|
268
|
+
const selected = await p.select({ message: "Where should the game live?", options: placementOptions })
|
|
269
|
+
if (p.isCancel(selected)) process.exit(0)
|
|
270
|
+
mode = selected as "new-repo" | "add-to-repo"
|
|
190
271
|
}
|
|
191
272
|
|
|
192
|
-
// Step 3:
|
|
273
|
+
// Step 3: Acquire source content into tmpDir
|
|
193
274
|
const tmpDir = path.resolve(".puzzmo-import-tmp")
|
|
194
275
|
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true })
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
276
|
+
|
|
277
|
+
let importedTitle: string | undefined
|
|
278
|
+
let userPrompt: string | undefined
|
|
279
|
+
|
|
280
|
+
if (strategy === "import") {
|
|
281
|
+
let url = opts.url
|
|
282
|
+
if (!url) {
|
|
283
|
+
url = (await p.text({ message: "Source URL to import", validate: (v) => (!v ? "URL is required" : undefined) })) as string
|
|
284
|
+
if (p.isCancel(url)) process.exit(0)
|
|
285
|
+
}
|
|
286
|
+
const s = p.spinner()
|
|
287
|
+
s.start(`Downloading ${url}`)
|
|
288
|
+
const { title } = await downloadPage(url, tmpDir)
|
|
289
|
+
s.stop("Download complete.")
|
|
290
|
+
importedTitle = title
|
|
291
|
+
} else if (strategy === "prompt") {
|
|
292
|
+
userPrompt = opts.prompt
|
|
293
|
+
if (!userPrompt) {
|
|
294
|
+
userPrompt = (await p.text({
|
|
295
|
+
message: "Describe the game you want to build",
|
|
296
|
+
placeholder: "A word search where letters appear in waves...",
|
|
297
|
+
validate: (v) => (!v ? "Prompt is required" : undefined),
|
|
298
|
+
})) as string
|
|
299
|
+
if (p.isCancel(userPrompt)) process.exit(0)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
199
302
|
|
|
200
303
|
// Step 4: Game name
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
304
|
+
let name: string
|
|
305
|
+
if (opts.name) {
|
|
306
|
+
name = opts.name
|
|
307
|
+
} else {
|
|
308
|
+
const result = (await p.text({
|
|
309
|
+
message: "Game name",
|
|
310
|
+
initialValue: importedTitle || "My Game",
|
|
311
|
+
validate: (v) => (!v ? "Game name is required" : undefined),
|
|
312
|
+
})) as string
|
|
313
|
+
if (p.isCancel(result)) process.exit(0)
|
|
314
|
+
name = result
|
|
315
|
+
}
|
|
208
316
|
|
|
209
317
|
const slug = slugify(name)
|
|
210
318
|
|
|
319
|
+
// Materialize template content for blank/prompt strategies (after we know the slug/name)
|
|
320
|
+
if (strategy !== "import") {
|
|
321
|
+
const templateDir = resolveTemplateDir("minesweeper")
|
|
322
|
+
if (!fs.existsSync(templateDir)) {
|
|
323
|
+
p.log.error(`Bundled template not found at ${templateDir}`)
|
|
324
|
+
process.exit(1)
|
|
325
|
+
}
|
|
326
|
+
copyTemplate(templateDir, tmpDir, { __SLUG__: slug, __DISPLAY_NAME__: name })
|
|
327
|
+
}
|
|
328
|
+
|
|
211
329
|
// Step 5: Login if token provided
|
|
212
330
|
if (opts.accessToken) {
|
|
213
331
|
p.log.step("Logging in...")
|
|
214
332
|
login(opts.accessToken)
|
|
215
333
|
}
|
|
216
334
|
|
|
217
|
-
// Step 6:
|
|
335
|
+
// Step 6: Place files
|
|
218
336
|
let gameDir: string
|
|
219
337
|
let repoType: RepoType = repo.repoType
|
|
220
338
|
|
|
@@ -230,14 +348,12 @@ export const gameCreate = async (args: string[]) => {
|
|
|
230
348
|
process.exit(1)
|
|
231
349
|
}
|
|
232
350
|
|
|
233
|
-
// Standalone repo → convert to multi-game first
|
|
234
351
|
if (repo.repoType === "standalone") {
|
|
235
352
|
const existingGame = convertToMultiGame(repo.repoRoot)
|
|
236
353
|
p.log.step(`Moved existing game to games/${existingGame}/`)
|
|
237
354
|
repoType = "multi-game"
|
|
238
355
|
}
|
|
239
356
|
|
|
240
|
-
// Determine which folder to place the game in
|
|
241
357
|
let parentFolder: string
|
|
242
358
|
if (repoType === "multi-game") {
|
|
243
359
|
parentFolder = "games"
|
|
@@ -261,38 +377,34 @@ export const gameCreate = async (args: string[]) => {
|
|
|
261
377
|
gameDir = setupRepoGame(tmpDir, slug, repo.repoRoot, parentFolder)
|
|
262
378
|
}
|
|
263
379
|
|
|
264
|
-
// Step 7:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (repoType === "workspace-monorepo") repoContextLines.push("This is a workspace monorepo. The game has its own package.json.")
|
|
293
|
-
|
|
294
|
-
p.log.step("Running Puzzmo migration pipeline...")
|
|
295
|
-
await runSkillsPipelineTUI(selectedAgent, gameDir, repoContextLines.join("\n"))
|
|
380
|
+
// Step 7: Strategy-specific agent work
|
|
381
|
+
if (strategy === "import") {
|
|
382
|
+
const selectedAgent = await pickAgent(opts.agent)
|
|
383
|
+
if (selectedAgent !== "none") {
|
|
384
|
+
const repoContextLines = [
|
|
385
|
+
`Repo type: ${repoType}`,
|
|
386
|
+
`Package manager: ${repo.packageManager} (use this instead of npm/npx when running commands or adding dependencies)`,
|
|
387
|
+
]
|
|
388
|
+
if (repoType === "multi-game")
|
|
389
|
+
repoContextLines.push(
|
|
390
|
+
"This is a multi-game repo with a shared root package.json. Do not create a per-game package.json or vite config.",
|
|
391
|
+
)
|
|
392
|
+
if (repoType === "workspace-monorepo") repoContextLines.push("This is a workspace monorepo. The game has its own package.json.")
|
|
393
|
+
|
|
394
|
+
p.log.step("Running Puzzmo migration pipeline...")
|
|
395
|
+
await runSkillsPipelineTUI(selectedAgent, gameDir, repoContextLines.join("\n"))
|
|
396
|
+
}
|
|
397
|
+
} else if (strategy === "prompt") {
|
|
398
|
+
const selectedAgent = await pickAgent(opts.agent)
|
|
399
|
+
const message = buildPromptStrategyMessage(userPrompt!, name)
|
|
400
|
+
if (selectedAgent === "none") {
|
|
401
|
+
p.log.warn("No agent selected; skipping LLM customization.")
|
|
402
|
+
p.note(message, "Paste this prompt into your LLM agent:")
|
|
403
|
+
} else {
|
|
404
|
+
p.log.step("Running agent with your prompt...")
|
|
405
|
+
const ok = runAgentWithBuildLoop(selectedAgent, message, gameDir, "from-prompt")
|
|
406
|
+
if (!ok) p.log.warn("Agent step did not complete cleanly. The Minesweeper starter is still in place.")
|
|
407
|
+
}
|
|
296
408
|
}
|
|
297
409
|
|
|
298
410
|
// Done
|
package/src/commands/upload.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { execSync } from "node:child_process"
|
|
|
2
2
|
import crypto from "node:crypto"
|
|
3
3
|
import fs from "node:fs"
|
|
4
4
|
import path from "node:path"
|
|
5
|
+
import * as p from "@clack/prompts"
|
|
5
6
|
|
|
6
|
-
import { uploadFiles } from "../util/api.js"
|
|
7
|
+
import { GameNotFoundError, uploadFiles } from "../util/api.js"
|
|
7
8
|
import { getAPIURL, getToken } from "../util/config.js"
|
|
9
|
+
import { createUserGame } from "../util/createGame.js"
|
|
8
10
|
import { discoverGames, type DiscoveredGame } from "../util/discoverGames.js"
|
|
9
11
|
|
|
10
12
|
type UploadOptions = {
|
|
@@ -81,7 +83,15 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
|
81
83
|
const game = games[i]
|
|
82
84
|
console.log(`\n[${i + 1}/${games.length}] Uploading ${game.puzzmoFile.game.slug}`)
|
|
83
85
|
try {
|
|
84
|
-
|
|
86
|
+
let result: GameSuccess
|
|
87
|
+
try {
|
|
88
|
+
result = await uploadOneGame(game, { token, sha, description, repoURL, verbose, rootDir })
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (!(e instanceof GameNotFoundError)) throw e
|
|
91
|
+
const created = await maybeCreateMissingGame(e, game, { teamAccessToken: token })
|
|
92
|
+
if (!created) throw e
|
|
93
|
+
result = await uploadOneGame(game, { token, sha, description, repoURL, verbose, rootDir })
|
|
94
|
+
}
|
|
85
95
|
results.push(result)
|
|
86
96
|
} catch (e) {
|
|
87
97
|
const message = e instanceof Error ? e.message : String(e)
|
|
@@ -222,6 +232,46 @@ const hashGames = (games: DiscoveredGame[]): string => {
|
|
|
222
232
|
return hash.digest("hex").slice(0, 12)
|
|
223
233
|
}
|
|
224
234
|
|
|
235
|
+
/**
|
|
236
|
+
* When a game can't be uploaded because it doesn't exist on the user's account,
|
|
237
|
+
* offer to create it interactively. Returns true if the game was created and the
|
|
238
|
+
* caller can retry the upload. Returns false otherwise (non-interactive shell or
|
|
239
|
+
* user declined).
|
|
240
|
+
*/
|
|
241
|
+
const maybeCreateMissingGame = async (
|
|
242
|
+
err: GameNotFoundError,
|
|
243
|
+
game: DiscoveredGame,
|
|
244
|
+
opts: { teamAccessToken: string },
|
|
245
|
+
): Promise<boolean> => {
|
|
246
|
+
const slug = err.slug
|
|
247
|
+
const displayName = game.puzzmoFile.game.displayName
|
|
248
|
+
|
|
249
|
+
if (!process.stdin.isTTY) {
|
|
250
|
+
console.error(` Game "${slug}" not found on your account. Re-run interactively to create it.`)
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const consent = await p.confirm({
|
|
255
|
+
message: `Game "${slug}" doesn't exist on your account. Create it now (displayName: "${displayName}")?`,
|
|
256
|
+
initialValue: true,
|
|
257
|
+
})
|
|
258
|
+
if (p.isCancel(consent) || !consent) return false
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const created = await createUserGame({ displayName, teamAccessToken: opts.teamAccessToken })
|
|
262
|
+
console.log(` Created game ${created.slug} (${created.id})`)
|
|
263
|
+
if (created.slug !== slug) {
|
|
264
|
+
console.warn(` Server picked slug "${created.slug}" but your puzzmo.json uses "${slug}". Update puzzmo.json before re-uploading.`)
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
return true
|
|
268
|
+
} catch (e) {
|
|
269
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
270
|
+
console.error(` Could not create game: ${message}`)
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
225
275
|
/** Formats a byte count as a human-readable string */
|
|
226
276
|
const formatBytes = (bytes: number): string => {
|
|
227
277
|
if (bytes < 1024) return `${bytes} B`
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ const printUsage = () => {
|
|
|
14
14
|
puzzmo login <token> Save a CLI auth token
|
|
15
15
|
puzzmo game create [token] Create a new Puzzmo game project
|
|
16
16
|
|
|
17
|
-
puzzmo upload [dir] [-v] Discover puzzmo.json files in [dir] (default: .) and upload each game's build. -v/--verbose prints request URLs and full error bodies.
|
|
17
|
+
puzzmo upload [dir] [-v] Discover puzzmo.json files in [dir] (default: .) and upload each game's build. -v/--verbose prints request URLs and full error bodies. If a game does not exist on your account and stdin is a TTY, you'll be asked whether to create it (using your saved CLI token).
|
|
18
18
|
puzzmo validate [dir] Discover and validate every puzzmo.json under [dir] (default: .)
|
|
19
19
|
puzzmo migrate List and select migration skills from dev.puzzmo.com`)
|
|
20
20
|
}
|
|
@@ -41,7 +41,9 @@ const run = async () => {
|
|
|
41
41
|
if (subcommand === "create") {
|
|
42
42
|
await gameCreate(subArgs)
|
|
43
43
|
} else {
|
|
44
|
-
console.error(
|
|
44
|
+
console.error(
|
|
45
|
+
"Usage: puzzmo game create [--strategy <import|blank|prompt>] [--prompt <text>] [--name <name>] [--url <url>] [--agent <agent>] [--token <token>] [--pm <npm|yarn|pnpm>]",
|
|
46
|
+
)
|
|
45
47
|
process.exit(1)
|
|
46
48
|
}
|
|
47
49
|
break
|
package/src/skills/runner.ts
CHANGED
|
@@ -82,3 +82,38 @@ export const runSkillsPipeline = async (agent: string, gameDir: string, repoCont
|
|
|
82
82
|
export const runSkillsPipelineTUI = async (agent: string, gameDir: string, repoContext: string) => {
|
|
83
83
|
await runPipelineTUI(agent, gameDir, repoContext)
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/** Runs a single agent invocation, verifies the build, and commits. Used by one-shot prompt-driven flows. */
|
|
87
|
+
export const runAgentWithBuildLoop = (agent: string, prompt: string, gameDir: string, label: string): boolean => {
|
|
88
|
+
console.log(`Running agent: ${label}`)
|
|
89
|
+
let result = invokeAgent(agent, prompt, gameDir)
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
console.log(` Agent failed, retrying...`)
|
|
92
|
+
result = invokeAgent(agent, prompt, gameDir)
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
console.error(` Agent failed after retry. Stopping.`)
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const buildResult = verifyBuild(gameDir)
|
|
100
|
+
if (!buildResult.success) {
|
|
101
|
+
console.log(` Build failed, asking agent to fix...`)
|
|
102
|
+
const fixPrompt = `The vite build failed after the "${label}" step. Fix the build errors:\n\n${buildResult.error}`
|
|
103
|
+
invokeAgent(agent, fixPrompt, gameDir)
|
|
104
|
+
const retry = verifyBuild(gameDir)
|
|
105
|
+
if (!retry.success) {
|
|
106
|
+
console.error(` Build still failing after fix attempt.`)
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
runCommand("git add -A", { cwd: gameDir })
|
|
113
|
+
gitCommit(`step: ${label}`, { cwd: gameDir })
|
|
114
|
+
console.log(` Committed.`)
|
|
115
|
+
} catch {
|
|
116
|
+
console.log(` No changes to commit.`)
|
|
117
|
+
}
|
|
118
|
+
return true
|
|
119
|
+
}
|