@puzzmo/cli 1.0.38 → 1.0.39

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/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)
@@ -75,7 +84,15 @@ export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzz
75
84
  if (verbose)
76
85
  console.log(` API URL: ${apiURL}`);
77
86
  // Step 1: Init session (includes puzzmo.json metadata)
78
- const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile, description, repoURL }, "upload init", verbose));
87
+ let init;
88
+ try {
89
+ init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile, description, repoURL }, "upload init", verbose));
90
+ }
91
+ catch (e) {
92
+ if (e instanceof Error && e.message.includes("Game not found:"))
93
+ throw new GameNotFoundError(gameSlug, e.message);
94
+ throw e;
95
+ }
79
96
  if (verbose)
80
97
  console.log(` session: ${init.sessionID}`);
81
98
  // Step 2: Upload files in concurrent batches
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/util/api.ts"],"names":[],"mappings":"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"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@puzzmo/cli",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "puzzmo": "./lib/index.js"
@@ -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
- opts.name = args[++i]
32
- } else if (arg === "--url" && args[i + 1]) {
33
- opts.url = args[++i]
34
- } else if (arg === "--agent" && args[i + 1]) {
35
- opts.agent = args[++i]
36
- } else if (arg === "--token" && args[i + 1]) {
37
- opts.accessToken = args[++i]
38
- } else if (arg === "--pm" && args[i + 1]) {
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(/[\u0300-\u036f]/g, "")
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: Detect repo context and choose mode
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 modeOptions = [
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") modeOptions.reverse()
262
+ if (canAddToRepo && repo.repoType !== "none") placementOptions.reverse()
177
263
 
178
- const mode = await p.select({
179
- message: "How would you like to create your game?",
180
- options: modeOptions,
181
- })
182
-
183
- if (p.isCancel(mode)) process.exit(0)
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: Download page to a temp dir, extract title
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
- const s = p.spinner()
196
- s.start(`Downloading ${url}`)
197
- const { title } = await downloadPage(url, tmpDir)
198
- s.stop("Download complete.")
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
- const defaultName = opts.name || title
202
- const name = (await p.text({
203
- message: "Game name",
204
- initialValue: defaultName,
205
- validate: (v) => (!v ? "Game name is required" : undefined),
206
- })) as string
207
- if (p.isCancel(name)) process.exit(0)
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: Diamond — set up repo or add to existing
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: Detect agent and run skills pipeline. Only offer CLIs we have an adapter for.
265
- const supported = new Set(agentNames())
266
- const agents = detectAgent().filter((a) => supported.has(a.id))
267
- const agentChoices = [
268
- ...agents.map((a) => ({ value: a.id, label: `${a.displayName} (${a.path})` })),
269
- { value: "none", label: "None - I'll run the steps manually" },
270
- ]
271
-
272
- let selectedAgent: string
273
- if (opts.agent) {
274
- selectedAgent = opts.agent
275
- } else if (agents.length > 0) {
276
- selectedAgent = (await p.select({ message: "Which LLM agent do you use?", options: agentChoices })) as string
277
- if (p.isCancel(selectedAgent)) process.exit(0)
278
- } else {
279
- p.log.info(`No supported LLM agents detected (checked: ${agentNames().join(", ")})`)
280
- selectedAgent = "none"
281
- }
282
-
283
- if (selectedAgent !== "none") {
284
- const repoContextLines = [
285
- `Repo type: ${repoType}`,
286
- `Package manager: ${repo.packageManager} (use this instead of npm/npx when running commands or adding dependencies)`,
287
- ]
288
- if (repoType === "multi-game")
289
- repoContextLines.push(
290
- "This is a multi-game repo with a shared root package.json. Do not create a per-game package.json or vite config.",
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
@@ -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
- const result = await uploadOneGame(game, { token, sha, description, repoURL, verbose, rootDir })
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("Usage: puzzmo game create [token] [--name <name>] [--url <url>] [--agent <agent>] [--pm <npm|yarn|pnpm>]")
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
@@ -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
+ }
package/src/util/api.ts CHANGED
@@ -19,6 +19,17 @@ export type PuzzmoFile = {
19
19
 
20
20
  const BATCH_SIZE = 10
21
21
 
22
+ /** Thrown when /cliUpload init returns a 404 because the game slug isn't registered yet. */
23
+ export class GameNotFoundError extends Error {
24
+ constructor(
25
+ public readonly slug: string,
26
+ message: string,
27
+ ) {
28
+ super(message)
29
+ this.name = "GameNotFoundError"
30
+ }
31
+ }
32
+
22
33
  type InitResponse = { sessionID: string; basePath: string; error?: string }
23
34
  type FileResponse = { path: string; error?: string }
24
35
  type CompleteResponse = { assetsBase: string; versionID: string; error?: string }
@@ -127,13 +138,19 @@ export const uploadFiles = async (
127
138
  if (verbose) console.log(` API URL: ${apiURL}`)
128
139
 
129
140
  // Step 1: Init session (includes puzzmo.json metadata)
130
- const init = (await jsonPost(
131
- `${apiURL}/cliUpload`,
132
- token,
133
- { gameSlug, sha, puzzmoFile, description, repoURL },
134
- "upload init",
135
- verbose,
136
- )) as InitResponse
141
+ let init: InitResponse
142
+ try {
143
+ init = (await jsonPost(
144
+ `${apiURL}/cliUpload`,
145
+ token,
146
+ { gameSlug, sha, puzzmoFile, description, repoURL },
147
+ "upload init",
148
+ verbose,
149
+ )) as InitResponse
150
+ } catch (e) {
151
+ if (e instanceof Error && e.message.includes("Game not found:")) throw new GameNotFoundError(gameSlug, e.message)
152
+ throw e
153
+ }
137
154
  if (verbose) console.log(` session: ${init.sessionID}`)
138
155
 
139
156
  // Step 2: Upload files in concurrent batches