@puzzmo/cli 1.0.37 → 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.
Files changed (46) hide show
  1. package/lib/commands/game/create.js +187 -71
  2. package/lib/commands/game/create.js.map +1 -1
  3. package/lib/commands/upload.d.ts +1 -1
  4. package/lib/commands/upload.js +143 -64
  5. package/lib/commands/upload.js.map +1 -1
  6. package/lib/commands/validate.d.ts +1 -1
  7. package/lib/commands/validate.js +24 -31
  8. package/lib/commands/validate.js.map +1 -1
  9. package/lib/index.js +4 -8
  10. package/lib/index.js.map +1 -1
  11. package/lib/skills/runner.d.ts +2 -0
  12. package/lib/skills/runner.js +33 -0
  13. package/lib/skills/runner.js.map +1 -1
  14. package/lib/util/api.d.ts +8 -0
  15. package/lib/util/api.js +22 -3
  16. package/lib/util/api.js.map +1 -1
  17. package/lib/util/createGame.d.ts +13 -0
  18. package/lib/util/createGame.js +36 -0
  19. package/lib/util/createGame.js.map +1 -0
  20. package/lib/util/discoverGames.d.ts +25 -0
  21. package/lib/util/discoverGames.js +181 -0
  22. package/lib/util/discoverGames.js.map +1 -0
  23. package/lib/util/validatePuzzmoFile.d.ts +1 -1
  24. package/lib/util/validatePuzzmoFile.js +2 -2
  25. package/lib/util/validatePuzzmoFile.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/commands/game/create.ts +186 -74
  28. package/src/commands/upload.ts +171 -61
  29. package/src/commands/validate.ts +23 -30
  30. package/src/index.ts +6 -8
  31. package/src/skills/runner.ts +35 -0
  32. package/src/util/api.ts +32 -9
  33. package/src/util/createGame.ts +60 -0
  34. package/src/util/discoverGames.ts +203 -0
  35. package/src/util/validatePuzzmoFile.ts +2 -2
  36. package/templates/minesweeper/README.md +13 -0
  37. package/templates/minesweeper/fixtures/puzzles/easy/9x9.json +16 -0
  38. package/templates/minesweeper/fixtures/puzzles/hard/16x16.json +46 -0
  39. package/templates/minesweeper/index.html +19 -0
  40. package/templates/minesweeper/package.json +17 -0
  41. package/templates/minesweeper/puzzmo.json +11 -0
  42. package/templates/minesweeper/src/appBundle.ts +122 -0
  43. package/templates/minesweeper/src/main.ts +236 -0
  44. package/templates/minesweeper/src/style.css +160 -0
  45. package/templates/minesweeper/tsconfig.json +14 -0
  46. package/templates/minesweeper/vite.config.ts +10 -0
@@ -1,46 +1,39 @@
1
1
  import fs from "node:fs"
2
2
  import path from "node:path"
3
3
 
4
- import { validatePuzzmoJson } from "../util/validatePuzzmoFile.js"
4
+ import { discoverGames } from "../util/discoverGames.js"
5
5
 
6
- /** CLI command: puzzmo validate [dir] */
6
+ /** CLI command: puzzmo validate [dir] — discovers every puzzmo.json under dir and validates each */
7
7
  export const validate = async (dir: string) => {
8
- const resolvedDir = path.resolve(dir)
9
- const puzzmoJsonPath = path.join(resolvedDir, "puzzmo.json")
10
-
11
- if (!fs.existsSync(puzzmoJsonPath)) {
12
- console.error(`No puzzmo.json found in ${dir}`)
8
+ const rootDir = path.resolve(dir)
9
+ if (!fs.existsSync(rootDir)) {
10
+ console.error(`Directory not found: ${dir}`)
13
11
  process.exit(1)
14
12
  }
15
13
 
16
- let raw: string
17
- try {
18
- raw = fs.readFileSync(puzzmoJsonPath, "utf-8")
19
- } catch (e) {
20
- console.error(`Could not read puzzmo.json: ${e instanceof Error ? e.message : e}`)
21
- process.exit(1)
22
- }
14
+ const { games, errors } = await discoverGames(rootDir)
23
15
 
24
- let data: unknown
25
- try {
26
- data = JSON.parse(raw)
27
- } catch (e) {
28
- console.error(`Invalid JSON in puzzmo.json: ${e instanceof Error ? e.message : e}`)
16
+ if (!games.length && !errors.length) {
17
+ console.error(`No puzzmo.json files found under ${rootDir}`)
29
18
  process.exit(1)
30
19
  }
31
20
 
32
- const result = await validatePuzzmoJson(data)
33
- if (!result.valid) {
34
- console.error(`puzzmo.json has ${result.errors.length} error(s):\n`)
35
- for (const err of result.errors) console.error(` ${err}`)
36
- process.exit(1)
21
+ for (const game of games) {
22
+ const rel = path.relative(rootDir, game.puzzmoJsonPath) || game.puzzmoJsonPath
23
+ const integrations = game.puzzmoFile.integrations ? Object.keys(game.puzzmoFile.integrations) : []
24
+ const distRel = path.relative(rootDir, game.distDir) || game.distDir
25
+ console.log(`OK ${game.puzzmoFile.game.slug.padEnd(24)} (${rel})`)
26
+ console.log(` dist: ${distRel}`)
27
+ if (integrations.length) console.log(` integrations: ${integrations.join(", ")}`)
37
28
  }
38
29
 
39
- const { data: puzzmoFile } = result
40
- console.log(`Valid puzzmo.json for "${puzzmoFile.game.displayName}" (${puzzmoFile.game.slug})`)
41
-
42
- if (puzzmoFile.integrations) {
43
- const keys = Object.keys(puzzmoFile.integrations)
44
- if (keys.length) console.log(`Integrations: ${keys.join(", ")}`)
30
+ for (const err of errors) {
31
+ const rel = path.relative(rootDir, err.puzzmoJsonPath) || err.puzzmoJsonPath
32
+ const label = err.slug ?? rel
33
+ console.error(`FAIL ${label.padEnd(24)} (${rel})`)
34
+ for (const e of err.errors) console.error(` ${e}`)
45
35
  }
36
+
37
+ console.log(`\n${games.length} valid, ${errors.length} invalid`)
38
+ if (errors.length) process.exit(1)
46
39
  }
package/src/index.ts CHANGED
@@ -14,8 +14,8 @@ 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] Upload game build from <dir> (slug from puzzmo.json). -v/--verbose prints request URLs and full error bodies.
18
- puzzmo validate [dir] Validate puzzmo.json in a directory (default: .)
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
+ 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
  }
21
21
 
@@ -32,11 +32,7 @@ const run = async () => {
32
32
  }
33
33
  case "upload": {
34
34
  const verbose = args.includes("--verbose") || args.includes("-v")
35
- const dir = args.find((a) => !a.startsWith("-"))
36
- if (!dir) {
37
- console.error("Usage: puzzmo upload <dir> [-v|--verbose]")
38
- process.exit(1)
39
- }
35
+ const dir = args.find((a) => !a.startsWith("-")) ?? "."
40
36
  await upload(dir, { verbose })
41
37
  break
42
38
  }
@@ -45,7 +41,9 @@ const run = async () => {
45
41
  if (subcommand === "create") {
46
42
  await gameCreate(subArgs)
47
43
  } else {
48
- 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
+ )
49
47
  process.exit(1)
50
48
  }
51
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
@@ -10,11 +10,26 @@ export type PuzzmoFile = {
10
10
  slug: string
11
11
  teamID: string
12
12
  }
13
+ // This is what we're calling 'augmentations' publicly
13
14
  integrations?: Record<string, unknown>
15
+ output?: {
16
+ dir: string
17
+ }
14
18
  }
15
19
 
16
20
  const BATCH_SIZE = 10
17
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
+
18
33
  type InitResponse = { sessionID: string; basePath: string; error?: string }
19
34
  type FileResponse = { path: string; error?: string }
20
35
  type CompleteResponse = { assetsBase: string; versionID: string; error?: string }
@@ -60,9 +75,11 @@ const readResponse = async (res: Response, url: string, step: string, verbose: b
60
75
  throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`)
61
76
  }
62
77
  if (!res.ok) {
63
- const serverMsg = (json as { error?: string }).error || res.statusText || "Unknown error"
78
+ const body = json as { error?: string; validationErrors?: string[] }
79
+ const serverMsg = body.error || res.statusText || "Unknown error"
80
+ const validation = body.validationErrors?.length ? `\n - ${body.validationErrors.join("\n - ")}` : ""
64
81
  const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : ""
65
- throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${detail}`)
82
+ throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${validation}${detail}`)
66
83
  }
67
84
  return json
68
85
  }
@@ -121,13 +138,19 @@ export const uploadFiles = async (
121
138
  if (verbose) console.log(` API URL: ${apiURL}`)
122
139
 
123
140
  // Step 1: Init session (includes puzzmo.json metadata)
124
- const init = (await jsonPost(
125
- `${apiURL}/cliUpload`,
126
- token,
127
- { gameSlug, sha, puzzmoFile, description, repoURL },
128
- "upload init",
129
- verbose,
130
- )) 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
+ }
131
154
  if (verbose) console.log(` session: ${init.sessionID}`)
132
155
 
133
156
  // Step 2: Upload files in concurrent batches
@@ -0,0 +1,60 @@
1
+ import { getAPIURL } from "./config.js"
2
+
3
+ const createUserGameMutation = `
4
+ mutation cliCreateUserGameMutation($teamAccessToken: String!, $input: CreateUserGameInput!) {
5
+ createUserGame(teamAccessToken: $teamAccessToken, input: $input) {
6
+ game { id slug displayName }
7
+ newAccessToken
8
+ }
9
+ }
10
+ `
11
+
12
+ type GraphQLError = { message: string }
13
+
14
+ type CreateUserGameResponse = {
15
+ data?: {
16
+ createUserGame: {
17
+ game: { id: string; slug: string; displayName: string }
18
+ newAccessToken: string | null
19
+ } | null
20
+ }
21
+ errors?: GraphQLError[]
22
+ }
23
+
24
+ export type CreateUserGameOptions = {
25
+ /** A teamAccessToken (the same token saved by `puzzmo login`). The mutation uses it to identify the team. */
26
+ teamAccessToken: string
27
+ displayName: string
28
+ }
29
+
30
+ export type CreatedGame = { id: string; slug: string; displayName: string; newAccessToken: string | null }
31
+
32
+ /** Calls the createUserGame mutation using a teamAccessToken for auth. */
33
+ export const createUserGame = async (options: CreateUserGameOptions): Promise<CreatedGame> => {
34
+ const url = `${getAPIURL()}/graphql`
35
+
36
+ const response = await fetch(url, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({
40
+ query: createUserGameMutation,
41
+ variables: {
42
+ teamAccessToken: options.teamAccessToken,
43
+ input: { displayName: options.displayName },
44
+ },
45
+ }),
46
+ })
47
+
48
+ if (!response.ok) throw new Error(`createUserGame request failed: ${response.status} ${response.statusText}`)
49
+
50
+ const result = (await response.json()) as CreateUserGameResponse
51
+
52
+ if (result.errors?.length) {
53
+ const message = result.errors.map((e) => e.message).join("; ")
54
+ throw new Error(`createUserGame failed: ${message}`)
55
+ }
56
+ if (!result.data?.createUserGame) throw new Error("createUserGame returned no data")
57
+
58
+ const { game, newAccessToken } = result.data.createUserGame
59
+ return { id: game.id, slug: game.slug, displayName: game.displayName, newAccessToken }
60
+ }
@@ -0,0 +1,203 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ import type { PuzzmoFile } from "./api.js"
5
+ import { validatePuzzmoJson } from "./validatePuzzmoFile.js"
6
+
7
+ /** Folder names that are skipped while walking the repo for puzzmo.json files */
8
+ const ignoredDirs = new Set(["node_modules", ".git", ".turbo", ".next", ".cache", "dist", "build", "built", "output"])
9
+
10
+ /** Folder names that are searched (in order) when resolving a game's dist directory */
11
+ const outputDirNames = ["dist", "build", "built", "output"]
12
+
13
+ /** A game discovered in the repo, with its parsed puzzmo.json and resolved dist directory */
14
+ export type DiscoveredGame = {
15
+ /** Absolute path to the puzzmo.json file */
16
+ puzzmoJsonPath: string
17
+ /** Directory containing the puzzmo.json file */
18
+ puzzmoJsonDir: string
19
+ /** Validated puzzmo.json contents */
20
+ puzzmoFile: PuzzmoFile
21
+ /** Absolute path to the dist directory containing the build artifacts */
22
+ distDir: string
23
+ }
24
+
25
+ /** A puzzmo.json that was found but could not be used (invalid JSON, schema errors, missing dist) */
26
+ export type DiscoveryError = {
27
+ puzzmoJsonPath: string
28
+ /** Slug pulled from the file when available, used for reporting */
29
+ slug?: string
30
+ errors: string[]
31
+ }
32
+
33
+ export type DiscoveryResult = {
34
+ games: DiscoveredGame[]
35
+ errors: DiscoveryError[]
36
+ }
37
+
38
+ /** Walks `rootDir` looking for puzzmo.json files, validates each, and resolves their dist directory */
39
+ export const discoverGames = async (rootDir: string): Promise<DiscoveryResult> => {
40
+ const root = path.resolve(rootDir)
41
+ const puzzmoJsonPaths = findPuzzmoJsonFiles(root)
42
+
43
+ const games: DiscoveredGame[] = []
44
+ const errors: DiscoveryError[] = []
45
+
46
+ for (const puzzmoJsonPath of puzzmoJsonPaths) {
47
+ const fileErrors: string[] = []
48
+ let parsed: unknown
49
+ try {
50
+ parsed = JSON.parse(fs.readFileSync(puzzmoJsonPath, "utf-8"))
51
+ } catch (e) {
52
+ errors.push({ puzzmoJsonPath, errors: [`Invalid JSON: ${e instanceof Error ? e.message : e}`] })
53
+ continue
54
+ }
55
+
56
+ const validation = await validatePuzzmoJson(parsed)
57
+ if (!validation.valid) {
58
+ const slug = pullSlug(parsed)
59
+ errors.push({ puzzmoJsonPath, slug, errors: validation.errors })
60
+ continue
61
+ }
62
+
63
+ const puzzmoFile = validation.data
64
+ const puzzmoJsonDir = path.dirname(puzzmoJsonPath)
65
+ const distDir = resolveDistDir(puzzmoFile, puzzmoJsonDir, root)
66
+
67
+ if (!distDir) {
68
+ fileErrors.push(`Could not find a dist/build folder for ${puzzmoFile.game.slug}. Set "output.dir" in puzzmo.json.`)
69
+ } else if (!hasFiles(distDir)) {
70
+ fileErrors.push(`Dist folder is empty: ${distDir}`)
71
+ }
72
+
73
+ if (fileErrors.length) {
74
+ errors.push({ puzzmoJsonPath, slug: puzzmoFile.game.slug, errors: fileErrors })
75
+ continue
76
+ }
77
+
78
+ games.push({ puzzmoJsonPath, puzzmoJsonDir, puzzmoFile, distDir: distDir as string })
79
+ }
80
+
81
+ return { games, errors }
82
+ }
83
+
84
+ /** Recursively finds puzzmo.json files under `dir`, skipping output / vendored folders. Tracks realpaths to avoid symlink cycles. */
85
+ const findPuzzmoJsonFiles = (dir: string): string[] => {
86
+ const found: string[] = []
87
+ const visited = new Set<string>()
88
+ const stack = [dir]
89
+ while (stack.length) {
90
+ const current = stack.pop() as string
91
+ const realCurrent = safeRealpath(current)
92
+ if (!realCurrent || visited.has(realCurrent)) continue
93
+ visited.add(realCurrent)
94
+
95
+ let entries: fs.Dirent[]
96
+ try {
97
+ entries = fs.readdirSync(realCurrent, { withFileTypes: true })
98
+ } catch {
99
+ continue
100
+ }
101
+ for (const entry of entries) {
102
+ const full = path.join(realCurrent, entry.name)
103
+ const kind = resolveEntryKind(entry, full)
104
+ if (kind === "directory") {
105
+ if (ignoredDirs.has(entry.name)) continue
106
+ if (entry.name.startsWith(".")) continue
107
+ stack.push(full)
108
+ } else if (kind === "file" && entry.name === "puzzmo.json") {
109
+ found.push(full)
110
+ }
111
+ }
112
+ }
113
+ return found.sort()
114
+ }
115
+
116
+ /** Resolves the dist directory for a discovered game, returning null if none can be found */
117
+ const resolveDistDir = (puzzmoFile: PuzzmoFile, puzzmoJsonDir: string, repoRoot: string): string | null => {
118
+ if (puzzmoFile.output?.dir) {
119
+ const resolved = path.resolve(puzzmoJsonDir, puzzmoFile.output.dir)
120
+ return fs.existsSync(resolved) ? resolved : null
121
+ }
122
+
123
+ const slug = puzzmoFile.game.slug
124
+ const candidateDirs = walkUp(puzzmoJsonDir, repoRoot)
125
+ for (const baseDir of candidateDirs) {
126
+ for (const name of outputDirNames) {
127
+ const withSlug = path.join(baseDir, name, slug)
128
+ if (fs.existsSync(withSlug) && fs.statSync(withSlug).isDirectory()) return withSlug
129
+ const plain = path.join(baseDir, name)
130
+ if (fs.existsSync(plain) && fs.statSync(plain).isDirectory()) return plain
131
+ }
132
+ }
133
+ return null
134
+ }
135
+
136
+ /** Returns the directories from `start` walking up to (and including) `stop` */
137
+ const walkUp = (start: string, stop: string): string[] => {
138
+ const dirs: string[] = []
139
+ let current = start
140
+ while (true) {
141
+ dirs.push(current)
142
+ if (current === stop) break
143
+ const parent = path.dirname(current)
144
+ if (parent === current) break
145
+ if (!current.startsWith(stop)) break
146
+ current = parent
147
+ }
148
+ return dirs
149
+ }
150
+
151
+ /** Returns true if the directory contains at least one file (recursively). Follows symlinks safely via a visited realpath set. */
152
+ const hasFiles = (dir: string, visited: Set<string> = new Set()): boolean => {
153
+ const realDir = safeRealpath(dir)
154
+ if (!realDir || visited.has(realDir)) return false
155
+ visited.add(realDir)
156
+
157
+ let entries: fs.Dirent[]
158
+ try {
159
+ entries = fs.readdirSync(realDir, { withFileTypes: true })
160
+ } catch {
161
+ return false
162
+ }
163
+ for (const entry of entries) {
164
+ const full = path.join(realDir, entry.name)
165
+ const kind = resolveEntryKind(entry, full)
166
+ if (kind === "file") return true
167
+ if (kind === "directory" && hasFiles(full, visited)) return true
168
+ }
169
+ return false
170
+ }
171
+
172
+ /** Resolves the underlying type of a directory entry, following symlinks one level. Returns null for dangling links or other types. */
173
+ const resolveEntryKind = (entry: fs.Dirent, fullPath: string): "file" | "directory" | null => {
174
+ if (entry.isFile()) return "file"
175
+ if (entry.isDirectory()) return "directory"
176
+ if (!entry.isSymbolicLink()) return null
177
+ try {
178
+ const stat = fs.statSync(fullPath)
179
+ if (stat.isFile()) return "file"
180
+ if (stat.isDirectory()) return "directory"
181
+ } catch {
182
+ // dangling symlink
183
+ }
184
+ return null
185
+ }
186
+
187
+ /** Resolves a path to its real location; returns null if the path can't be resolved (broken link, missing dir, etc.) */
188
+ const safeRealpath = (p: string): string | null => {
189
+ try {
190
+ return fs.realpathSync(p)
191
+ } catch {
192
+ return null
193
+ }
194
+ }
195
+
196
+ /** Best-effort extraction of the slug from a malformed puzzmo.json for error reporting */
197
+ const pullSlug = (parsed: unknown): string | undefined => {
198
+ if (!parsed || typeof parsed !== "object") return undefined
199
+ const game = (parsed as { game?: unknown }).game
200
+ if (!game || typeof game !== "object") return undefined
201
+ const slug = (game as { slug?: unknown }).slug
202
+ return typeof slug === "string" ? slug : undefined
203
+ }
@@ -32,12 +32,12 @@ const getValidator = async () => {
32
32
 
33
33
  type ValidationResult = { valid: true; data: PuzzmoFile } | { valid: false; errors: string[] }
34
34
 
35
- /** Validates a parsed object against the puzzmo.json JSON schema */
35
+ /** Validates a parsed object against the puzzmo.json JSON schema. Returned `data` has `$schema` stripped. */
36
36
  export const validatePuzzmoJson = async (data: unknown): Promise<ValidationResult> => {
37
37
  const validator = await getValidator()
38
38
  const toValidate = stripSchemaProperty(data)
39
39
  const result = validator.validate(toValidate)
40
- if (result.valid) return { valid: true, data: data as PuzzmoFile }
40
+ if (result.valid) return { valid: true, data: toValidate as PuzzmoFile }
41
41
  return {
42
42
  valid: false,
43
43
  errors: result.errors
@@ -0,0 +1,13 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ A Minesweeper-style Puzzmo game scaffold.
4
+
5
+ ## Develop
6
+
7
+ ```
8
+ yarn install # or npm/pnpm
9
+ yarn dev # opens the Puzzmo simulator
10
+ yarn build
11
+ ```
12
+
13
+ Edit puzzles in `fixtures/puzzles/`. Game logic is in `src/main.ts`.
@@ -0,0 +1,16 @@
1
+ {
2
+ "rows": 9,
3
+ "cols": 9,
4
+ "mines": [
5
+ [0, 2],
6
+ [1, 5],
7
+ [2, 0],
8
+ [3, 7],
9
+ [4, 3],
10
+ [5, 8],
11
+ [6, 1],
12
+ [7, 4],
13
+ [7, 6],
14
+ [8, 2]
15
+ ]
16
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "rows": 16,
3
+ "cols": 16,
4
+ "mines": [
5
+ [0, 3],
6
+ [0, 11],
7
+ [1, 5],
8
+ [1, 14],
9
+ [2, 1],
10
+ [2, 8],
11
+ [2, 12],
12
+ [3, 4],
13
+ [3, 9],
14
+ [4, 0],
15
+ [4, 6],
16
+ [4, 13],
17
+ [5, 2],
18
+ [5, 10],
19
+ [5, 15],
20
+ [6, 5],
21
+ [6, 8],
22
+ [7, 1],
23
+ [7, 11],
24
+ [7, 14],
25
+ [8, 3],
26
+ [8, 7],
27
+ [9, 0],
28
+ [9, 9],
29
+ [9, 12],
30
+ [10, 5],
31
+ [10, 14],
32
+ [11, 2],
33
+ [11, 8],
34
+ [11, 11],
35
+ [12, 0],
36
+ [12, 6],
37
+ [12, 13],
38
+ [13, 4],
39
+ [13, 9],
40
+ [13, 15],
41
+ [14, 1],
42
+ [14, 7],
43
+ [14, 12],
44
+ [15, 10]
45
+ ]
46
+ }
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
6
+ <title>__DISPLAY_NAME__</title>
7
+ <link rel="stylesheet" href="./src/style.css" />
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <div class="hud">
12
+ <span id="hudLeft">0 mines</span>
13
+ <span id="hudRight">0 flags</span>
14
+ </div>
15
+ <div id="board" class="board" aria-label="minesweeper board"></div>
16
+ </main>
17
+ <script type="module" src="./src/main.ts"></script>
18
+ </body>
19
+ </html>
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "__SLUG__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@puzzmo/sdk": "*"
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "^5.4.0",
15
+ "vite": "^5.4.0"
16
+ }
17
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://dev.puzzmo.com/schema/puzzmo-file-schema.json",
3
+ "game": {
4
+ "displayName": "__DISPLAY_NAME__",
5
+ "slug": "__SLUG__",
6
+ "teamID": "REPLACE_ME"
7
+ },
8
+ "output": {
9
+ "dir": "dist"
10
+ }
11
+ }