@puzzmo/cli 1.0.37 → 1.0.38

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.
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { validatePuzzmoJson } from "./validatePuzzmoFile.js";
4
+ /** Folder names that are skipped while walking the repo for puzzmo.json files */
5
+ const ignoredDirs = new Set(["node_modules", ".git", ".turbo", ".next", ".cache", "dist", "build", "built", "output"]);
6
+ /** Folder names that are searched (in order) when resolving a game's dist directory */
7
+ const outputDirNames = ["dist", "build", "built", "output"];
8
+ /** Walks `rootDir` looking for puzzmo.json files, validates each, and resolves their dist directory */
9
+ export const discoverGames = async (rootDir) => {
10
+ const root = path.resolve(rootDir);
11
+ const puzzmoJsonPaths = findPuzzmoJsonFiles(root);
12
+ const games = [];
13
+ const errors = [];
14
+ for (const puzzmoJsonPath of puzzmoJsonPaths) {
15
+ const fileErrors = [];
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(fs.readFileSync(puzzmoJsonPath, "utf-8"));
19
+ }
20
+ catch (e) {
21
+ errors.push({ puzzmoJsonPath, errors: [`Invalid JSON: ${e instanceof Error ? e.message : e}`] });
22
+ continue;
23
+ }
24
+ const validation = await validatePuzzmoJson(parsed);
25
+ if (!validation.valid) {
26
+ const slug = pullSlug(parsed);
27
+ errors.push({ puzzmoJsonPath, slug, errors: validation.errors });
28
+ continue;
29
+ }
30
+ const puzzmoFile = validation.data;
31
+ const puzzmoJsonDir = path.dirname(puzzmoJsonPath);
32
+ const distDir = resolveDistDir(puzzmoFile, puzzmoJsonDir, root);
33
+ if (!distDir) {
34
+ fileErrors.push(`Could not find a dist/build folder for ${puzzmoFile.game.slug}. Set "output.dir" in puzzmo.json.`);
35
+ }
36
+ else if (!hasFiles(distDir)) {
37
+ fileErrors.push(`Dist folder is empty: ${distDir}`);
38
+ }
39
+ if (fileErrors.length) {
40
+ errors.push({ puzzmoJsonPath, slug: puzzmoFile.game.slug, errors: fileErrors });
41
+ continue;
42
+ }
43
+ games.push({ puzzmoJsonPath, puzzmoJsonDir, puzzmoFile, distDir: distDir });
44
+ }
45
+ return { games, errors };
46
+ };
47
+ /** Recursively finds puzzmo.json files under `dir`, skipping output / vendored folders. Tracks realpaths to avoid symlink cycles. */
48
+ const findPuzzmoJsonFiles = (dir) => {
49
+ const found = [];
50
+ const visited = new Set();
51
+ const stack = [dir];
52
+ while (stack.length) {
53
+ const current = stack.pop();
54
+ const realCurrent = safeRealpath(current);
55
+ if (!realCurrent || visited.has(realCurrent))
56
+ continue;
57
+ visited.add(realCurrent);
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(realCurrent, { withFileTypes: true });
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ for (const entry of entries) {
66
+ const full = path.join(realCurrent, entry.name);
67
+ const kind = resolveEntryKind(entry, full);
68
+ if (kind === "directory") {
69
+ if (ignoredDirs.has(entry.name))
70
+ continue;
71
+ if (entry.name.startsWith("."))
72
+ continue;
73
+ stack.push(full);
74
+ }
75
+ else if (kind === "file" && entry.name === "puzzmo.json") {
76
+ found.push(full);
77
+ }
78
+ }
79
+ }
80
+ return found.sort();
81
+ };
82
+ /** Resolves the dist directory for a discovered game, returning null if none can be found */
83
+ const resolveDistDir = (puzzmoFile, puzzmoJsonDir, repoRoot) => {
84
+ if (puzzmoFile.output?.dir) {
85
+ const resolved = path.resolve(puzzmoJsonDir, puzzmoFile.output.dir);
86
+ return fs.existsSync(resolved) ? resolved : null;
87
+ }
88
+ const slug = puzzmoFile.game.slug;
89
+ const candidateDirs = walkUp(puzzmoJsonDir, repoRoot);
90
+ for (const baseDir of candidateDirs) {
91
+ for (const name of outputDirNames) {
92
+ const withSlug = path.join(baseDir, name, slug);
93
+ if (fs.existsSync(withSlug) && fs.statSync(withSlug).isDirectory())
94
+ return withSlug;
95
+ const plain = path.join(baseDir, name);
96
+ if (fs.existsSync(plain) && fs.statSync(plain).isDirectory())
97
+ return plain;
98
+ }
99
+ }
100
+ return null;
101
+ };
102
+ /** Returns the directories from `start` walking up to (and including) `stop` */
103
+ const walkUp = (start, stop) => {
104
+ const dirs = [];
105
+ let current = start;
106
+ while (true) {
107
+ dirs.push(current);
108
+ if (current === stop)
109
+ break;
110
+ const parent = path.dirname(current);
111
+ if (parent === current)
112
+ break;
113
+ if (!current.startsWith(stop))
114
+ break;
115
+ current = parent;
116
+ }
117
+ return dirs;
118
+ };
119
+ /** Returns true if the directory contains at least one file (recursively). Follows symlinks safely via a visited realpath set. */
120
+ const hasFiles = (dir, visited = new Set()) => {
121
+ const realDir = safeRealpath(dir);
122
+ if (!realDir || visited.has(realDir))
123
+ return false;
124
+ visited.add(realDir);
125
+ let entries;
126
+ try {
127
+ entries = fs.readdirSync(realDir, { withFileTypes: true });
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ for (const entry of entries) {
133
+ const full = path.join(realDir, entry.name);
134
+ const kind = resolveEntryKind(entry, full);
135
+ if (kind === "file")
136
+ return true;
137
+ if (kind === "directory" && hasFiles(full, visited))
138
+ return true;
139
+ }
140
+ return false;
141
+ };
142
+ /** Resolves the underlying type of a directory entry, following symlinks one level. Returns null for dangling links or other types. */
143
+ const resolveEntryKind = (entry, fullPath) => {
144
+ if (entry.isFile())
145
+ return "file";
146
+ if (entry.isDirectory())
147
+ return "directory";
148
+ if (!entry.isSymbolicLink())
149
+ return null;
150
+ try {
151
+ const stat = fs.statSync(fullPath);
152
+ if (stat.isFile())
153
+ return "file";
154
+ if (stat.isDirectory())
155
+ return "directory";
156
+ }
157
+ catch {
158
+ // dangling symlink
159
+ }
160
+ return null;
161
+ };
162
+ /** Resolves a path to its real location; returns null if the path can't be resolved (broken link, missing dir, etc.) */
163
+ const safeRealpath = (p) => {
164
+ try {
165
+ return fs.realpathSync(p);
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ };
171
+ /** Best-effort extraction of the slug from a malformed puzzmo.json for error reporting */
172
+ const pullSlug = (parsed) => {
173
+ if (!parsed || typeof parsed !== "object")
174
+ return undefined;
175
+ const game = parsed.game;
176
+ if (!game || typeof game !== "object")
177
+ return undefined;
178
+ const slug = game.slug;
179
+ return typeof slug === "string" ? slug : undefined;
180
+ };
181
+ //# sourceMappingURL=discoverGames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discoverGames.js","sourceRoot":"","sources":["../../src/util/discoverGames.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,6 @@ type ValidationResult = {
6
6
  valid: false;
7
7
  errors: string[];
8
8
  };
9
- /** Validates a parsed object against the puzzmo.json JSON schema */
9
+ /** Validates a parsed object against the puzzmo.json JSON schema. Returned `data` has `$schema` stripped. */
10
10
  export declare const validatePuzzmoJson: (data: unknown) => Promise<ValidationResult>;
11
11
  export {};
@@ -27,13 +27,13 @@ const getValidator = async () => {
27
27
  cachedValidator = new Validator(puzzmoSchema, "7");
28
28
  return cachedValidator;
29
29
  };
30
- /** Validates a parsed object against the puzzmo.json JSON schema */
30
+ /** Validates a parsed object against the puzzmo.json JSON schema. Returned `data` has `$schema` stripped. */
31
31
  export const validatePuzzmoJson = async (data) => {
32
32
  const validator = await getValidator();
33
33
  const toValidate = stripSchemaProperty(data);
34
34
  const result = validator.validate(toValidate);
35
35
  if (result.valid)
36
- return { valid: true, data: data };
36
+ return { valid: true, data: toValidate };
37
37
  return {
38
38
  valid: false,
39
39
  errors: result.errors
@@ -1 +1 @@
1
- {"version":3,"file":"validatePuzzmoFile.js","sourceRoot":"","sources":["../../src/util/validatePuzzmoFile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validatePuzzmoFile.js","sourceRoot":"","sources":["../../src/util/validatePuzzmoFile.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puzzmo/cli",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "puzzmo": "./lib/index.js"
@@ -5,13 +5,30 @@ import path from "node:path"
5
5
 
6
6
  import { uploadFiles } from "../util/api.js"
7
7
  import { getAPIURL, getToken } from "../util/config.js"
8
- import { validatePuzzmoJson } from "../util/validatePuzzmoFile.js"
8
+ import { discoverGames, type DiscoveredGame } from "../util/discoverGames.js"
9
9
 
10
10
  type UploadOptions = {
11
11
  verbose?: boolean
12
12
  }
13
13
 
14
- /** Uploads game build artifacts to Puzzmo */
14
+ type GameSuccess = {
15
+ ok: true
16
+ slug: string
17
+ fileCount: number
18
+ totalBytes: number
19
+ versionID: string
20
+ assetsBase: string
21
+ }
22
+
23
+ type GameFailure = {
24
+ ok: false
25
+ slug: string
26
+ error: string
27
+ }
28
+
29
+ type GameResult = GameSuccess | GameFailure
30
+
31
+ /** Uploads game build artifacts to Puzzmo by discovering puzzmo.json files in `dir` */
15
32
  export const upload = async (dir: string, options: UploadOptions = {}) => {
16
33
  const { verbose = false } = options
17
34
  const token = getToken()
@@ -20,68 +37,87 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
20
37
  process.exit(1)
21
38
  }
22
39
 
23
- const distDir = path.resolve(dir)
24
- if (!fs.existsSync(distDir)) {
40
+ const rootDir = path.resolve(dir)
41
+ if (!fs.existsSync(rootDir)) {
25
42
  console.error(`Directory not found: ${dir}`)
26
43
  process.exit(1)
27
44
  }
28
45
 
29
- const files = collectFiles(distDir)
30
- if (!files.length) {
31
- console.error(`Directory is empty: ${dir}`)
46
+ const { games, errors: discoveryErrors } = await discoverGames(rootDir)
47
+
48
+ if (!games.length && !discoveryErrors.length) {
49
+ console.error(`No puzzmo.json files found under ${rootDir}`)
32
50
  process.exit(1)
33
51
  }
34
52
 
35
- // Require and validate puzzmo.json
36
- const puzzmoJsonPath = path.join(distDir, "puzzmo.json")
37
- if (!fs.existsSync(puzzmoJsonPath)) {
38
- console.error(`Missing puzzmo.json in ${dir}`)
39
- console.error("Every game upload must include a puzzmo.json file.")
40
- process.exit(1)
53
+ console.log(`Found ${games.length} game(s) under ${rootDir}`)
54
+ for (const game of games) {
55
+ console.log(` - ${game.puzzmoFile.game.slug} (${path.relative(rootDir, game.distDir) || "."})`)
56
+ }
57
+ if (discoveryErrors.length) {
58
+ console.log(`\n${discoveryErrors.length} puzzmo.json file(s) could not be loaded:`)
59
+ for (const err of discoveryErrors) {
60
+ console.log(` - ${err.slug ?? path.relative(rootDir, err.puzzmoJsonPath)}`)
61
+ for (const e of err.errors) console.log(` ${e}`)
62
+ }
41
63
  }
42
64
 
43
- let parsed: unknown
44
- try {
45
- parsed = JSON.parse(fs.readFileSync(puzzmoJsonPath, "utf-8"))
46
- } catch (e) {
47
- console.error(`Invalid puzzmo.json: ${e instanceof Error ? e.message : e}`)
48
- process.exit(1)
65
+ const sha = getGitSHA(rootDir) || hashGames(games)
66
+ const description = getGitMessage(rootDir)
67
+ const repoURL = getGitRepoURL(rootDir)
68
+ if (description) console.log(`\nMessage: ${description}`)
69
+ if (repoURL) console.log(`Repo: ${repoURL}`)
70
+
71
+ const apiURL = getAPIURL()
72
+ const defaultURL = "https://api.puzzmo.com"
73
+ if (apiURL !== defaultURL) console.log(`API: ${apiURL}`)
74
+
75
+ const results: GameResult[] = []
76
+ for (const err of discoveryErrors) {
77
+ results.push({ ok: false, slug: err.slug ?? path.relative(rootDir, err.puzzmoJsonPath), error: err.errors.join("; ") })
49
78
  }
50
79
 
51
- const validation = await validatePuzzmoJson(parsed)
52
- if (!validation.valid) {
53
- console.error(`Invalid puzzmo.json:\n`)
54
- for (const err of validation.errors) console.error(` ${err}`)
55
- process.exit(1)
80
+ for (let i = 0; i < games.length; i++) {
81
+ const game = games[i]
82
+ console.log(`\n[${i + 1}/${games.length}] Uploading ${game.puzzmoFile.game.slug}`)
83
+ try {
84
+ const result = await uploadOneGame(game, { token, sha, description, repoURL, verbose, rootDir })
85
+ results.push(result)
86
+ } catch (e) {
87
+ const message = e instanceof Error ? e.message : String(e)
88
+ console.error(` Failed: ${message}`)
89
+ results.push({ ok: false, slug: game.puzzmoFile.game.slug, error: message })
90
+ }
56
91
  }
57
- const puzzmoFile = validation.data
58
- const gameSlug = puzzmoFile.game.slug
59
92
 
60
- // Determine SHA
61
- const sha = getGitSHA() || hashFiles(files)
62
- const description = getGitMessage()
63
- const repoURL = getGitRepoURL()
93
+ printReport(results)
64
94
 
65
- console.log(`\nUploading ${gameSlug} (${sha.slice(0, 8)})`)
66
- console.log(`Directory: ${distDir}`)
67
- if (description) console.log(`Message: ${description}`)
68
- if (repoURL) console.log(`Repo: ${repoURL}`)
69
- console.log("")
95
+ const anyFailed = results.some((r) => !r.ok)
96
+ if (anyFailed) process.exit(1)
97
+ }
70
98
 
71
- let totalBytes = 0
72
- for (const file of files) {
73
- const size = fs.statSync(file).size
74
- totalBytes += size
75
- const rel = path.relative(distDir, file)
76
- console.log(` ${rel} (${formatBytes(size)})`)
77
- }
99
+ type UploadOneOptions = {
100
+ token: string
101
+ sha: string
102
+ description: string | null
103
+ repoURL: string | null
104
+ verbose: boolean
105
+ rootDir: string
106
+ }
78
107
 
79
- const apiURL = getAPIURL()
80
- const defaultURL = "https://api.puzzmo.com"
108
+ /** Uploads a single discovered game; throws on failure */
109
+ const uploadOneGame = async (game: DiscoveredGame, opts: UploadOneOptions): Promise<GameSuccess> => {
110
+ const { token, sha, description, repoURL, verbose, rootDir } = opts
111
+ const { puzzmoFile, distDir } = game
112
+ const gameSlug = puzzmoFile.game.slug
81
113
 
82
- console.log(`\n${files.length} file(s), ${formatBytes(totalBytes)} total`)
83
- if (apiURL !== defaultURL) console.log(`Uploading to ${apiURL}...`)
84
- else console.log("Uploading...")
114
+ const files = collectFiles(distDir)
115
+ if (!files.length) throw new Error(`Dist folder is empty: ${distDir}`)
116
+
117
+ console.log(` Directory: ${path.relative(rootDir, distDir) || distDir}`)
118
+ let totalBytes = 0
119
+ for (const file of files) totalBytes += fs.statSync(file).size
120
+ console.log(` ${files.length} file(s), ${formatBytes(totalBytes)} total (sha ${sha.slice(0, 8)})`)
85
121
 
86
122
  const result = await uploadFiles(
87
123
  token,
@@ -91,13 +127,34 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
91
127
  distDir,
92
128
  puzzmoFile,
93
129
  (batch, totalBatches, uploaded) => {
94
- console.log(` Batch ${batch}/${totalBatches} done (${uploaded} file(s) uploaded)`)
130
+ console.log(` Batch ${batch}/${totalBatches} done (${uploaded} file(s) uploaded)`)
95
131
  },
96
132
  { verbose, description, repoURL },
97
133
  )
98
134
 
99
- console.log(`\nDone - ${result.versionID}`)
100
- console.log(`Assets: ${result.assetsBase}`)
135
+ console.log(` Done - ${result.versionID}`)
136
+ return {
137
+ ok: true,
138
+ slug: gameSlug,
139
+ fileCount: files.length,
140
+ totalBytes,
141
+ versionID: result.versionID,
142
+ assetsBase: result.assetsBase,
143
+ }
144
+ }
145
+
146
+ /** Prints a final summary of every game's outcome */
147
+ const printReport = (results: GameResult[]) => {
148
+ console.log(`\nUpload report (${results.length} game${results.length === 1 ? "" : "s"})`)
149
+ const successes = results.filter((r): r is GameSuccess => r.ok)
150
+ const failures = results.filter((r): r is GameFailure => !r.ok)
151
+ for (const r of successes) {
152
+ console.log(` OK ${r.slug.padEnd(24)} ${formatBytes(r.totalBytes).padStart(8)} ${r.versionID}`)
153
+ }
154
+ for (const r of failures) {
155
+ console.log(` FAIL ${r.slug.padEnd(24)} ${r.error}`)
156
+ }
157
+ console.log(`\n${successes.length} succeeded, ${failures.length} failed`)
101
158
  }
102
159
 
103
160
  /** Collects all files in a directory recursively */
@@ -113,27 +170,27 @@ const collectFiles = (dir: string): string[] => {
113
170
  }
114
171
 
115
172
  /** Tries to get the shortest unique git SHA, returns null if not in a git repo */
116
- const getGitSHA = (): string | null => {
173
+ const getGitSHA = (cwd: string): string | null => {
117
174
  try {
118
- return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim()
175
+ return execSync("git rev-parse --short HEAD", { encoding: "utf-8", cwd }).trim()
119
176
  } catch {
120
177
  return null
121
178
  }
122
179
  }
123
180
 
124
181
  /** Gets the subject line of the latest commit, or null if not in a git repo */
125
- const getGitMessage = (): string | null => {
182
+ const getGitMessage = (cwd: string): string | null => {
126
183
  try {
127
- return execSync("git log -1 --pretty=%s", { encoding: "utf-8" }).trim() || null
184
+ return execSync("git log -1 --pretty=%s", { encoding: "utf-8", cwd }).trim() || null
128
185
  } catch {
129
186
  return null
130
187
  }
131
188
  }
132
189
 
133
190
  /** Gets the origin remote URL normalized to https, or null if unavailable */
134
- const getGitRepoURL = (): string | null => {
191
+ const getGitRepoURL = (cwd: string): string | null => {
135
192
  try {
136
- const raw = execSync("git config --get remote.origin.url", { encoding: "utf-8" }).trim()
193
+ const raw = execSync("git config --get remote.origin.url", { encoding: "utf-8", cwd }).trim()
137
194
  return raw ? normalizeRepoURL(raw) : null
138
195
  } catch {
139
196
  return null
@@ -153,11 +210,14 @@ const normalizeRepoURL = (url: string): string => {
153
210
  return normalized.replace(/\.git$/, "")
154
211
  }
155
212
 
156
- /** Hashes all file contents to produce a deterministic SHA */
157
- const hashFiles = (filePaths: string[]): string => {
213
+ /** Hashes the dist contents of every game to produce a deterministic SHA across all uploads */
214
+ const hashGames = (games: DiscoveredGame[]): string => {
158
215
  const hash = crypto.createHash("sha256")
159
- for (const fp of filePaths.sort()) {
160
- hash.update(fs.readFileSync(fp))
216
+ for (const game of games) {
217
+ for (const file of collectFiles(game.distDir).sort()) {
218
+ hash.update(file)
219
+ hash.update(fs.readFileSync(file))
220
+ }
161
221
  }
162
222
  return hash.digest("hex").slice(0, 12)
163
223
  }
@@ -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.
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
  }
package/src/util/api.ts CHANGED
@@ -10,7 +10,11 @@ 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
@@ -60,9 +64,11 @@ const readResponse = async (res: Response, url: string, step: string, verbose: b
60
64
  throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`)
61
65
  }
62
66
  if (!res.ok) {
63
- const serverMsg = (json as { error?: string }).error || res.statusText || "Unknown error"
67
+ const body = json as { error?: string; validationErrors?: string[] }
68
+ const serverMsg = body.error || res.statusText || "Unknown error"
69
+ const validation = body.validationErrors?.length ? `\n - ${body.validationErrors.join("\n - ")}` : ""
64
70
  const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : ""
65
- throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${detail}`)
71
+ throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${validation}${detail}`)
66
72
  }
67
73
  return json
68
74
  }