@puzzmo/cli 1.0.36 → 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.
- package/lib/commands/upload.d.ts +1 -1
- package/lib/commands/upload.js +126 -54
- package/lib/commands/upload.js.map +1 -1
- package/lib/commands/validate.d.ts +1 -1
- package/lib/commands/validate.js +24 -31
- package/lib/commands/validate.js.map +1 -1
- package/lib/index.js +3 -7
- package/lib/index.js.map +1 -1
- package/lib/util/api.d.ts +7 -0
- package/lib/util/api.js +6 -4
- package/lib/util/api.js.map +1 -1
- package/lib/util/discoverGames.d.ts +25 -0
- package/lib/util/discoverGames.js +181 -0
- package/lib/util/discoverGames.js.map +1 -0
- package/lib/util/validatePuzzmoFile.d.ts +1 -1
- package/lib/util/validatePuzzmoFile.js +2 -2
- package/lib/util/validatePuzzmoFile.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/upload.ts +149 -53
- package/src/commands/validate.ts +23 -30
- package/src/index.ts +3 -7
- package/src/util/api.ts +20 -4
- package/src/util/discoverGames.ts +203 -0
- package/src/util/validatePuzzmoFile.ts +2 -2
|
@@ -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:
|
|
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":"AAAA,OAAO,EAAE,SAAS,EAAe,MAAM,uBAAuB,CAAA;AAI9D,MAAM,SAAS,GAAG,8DAA8D,CAAA,CAAC,yDAAyD;AAE1I,IAAI,eAAsC,CAAA;AAE1C,uEAAuE;AACvE,MAAM,YAAY,GAAG,KAAK,IAAI,EAAE,CAAC;IAC/B,IAAI,eAAe;QAAE,OAAO,eAAe,CAAA;IAC3C,IAAI,GAAa,CAAA;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,CAAA;IAC9B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAI,CAAyB,CAAC,KAAK,CAAA;QAC9C,MAAM,QAAQ,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,CAAW,CAAC,OAAO,CAAA;QACtG,MAAM,IAAI,KAAK,CAAC,8CAA8C,SAAS,MAAM,QAAQ,EAAE,CAAC,CAAA;IAC1F,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,uCAAuC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,SAAS,SAAS,EAAE,CAAC,CAAA;IAC1G,CAAC;IACD,IAAI,YAAqB,CAAA;IACzB,IAAI,CAAC;QACH,YAAY,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;IACjC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,uCAAuC,SAAS,MAAM,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC7G,CAAC;IACD,eAAe,GAAG,IAAI,SAAS,CAAC,YAAsB,EAAE,GAAG,CAAC,CAAA;IAC5D,OAAO,eAAe,CAAA;AAAA,CACvB,CAAA;AAID,
|
|
1
|
+
{"version":3,"file":"validatePuzzmoFile.js","sourceRoot":"","sources":["../../src/util/validatePuzzmoFile.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands/upload.ts
CHANGED
|
@@ -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 {
|
|
8
|
+
import { discoverGames, type DiscoveredGame } from "../util/discoverGames.js"
|
|
9
9
|
|
|
10
10
|
type UploadOptions = {
|
|
11
11
|
verbose?: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
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,64 +37,87 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
|
20
37
|
process.exit(1)
|
|
21
38
|
}
|
|
22
39
|
|
|
23
|
-
const
|
|
24
|
-
if (!fs.existsSync(
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
console.
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
const sha = getGitSHA() || hashFiles(files)
|
|
93
|
+
printReport(results)
|
|
62
94
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
const anyFailed = results.some((r) => !r.ok)
|
|
96
|
+
if (anyFailed) process.exit(1)
|
|
97
|
+
}
|
|
66
98
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
type UploadOneOptions = {
|
|
100
|
+
token: string
|
|
101
|
+
sha: string
|
|
102
|
+
description: string | null
|
|
103
|
+
repoURL: string | null
|
|
104
|
+
verbose: boolean
|
|
105
|
+
rootDir: string
|
|
106
|
+
}
|
|
74
107
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
113
|
+
|
|
114
|
+
const files = collectFiles(distDir)
|
|
115
|
+
if (!files.length) throw new Error(`Dist folder is empty: ${distDir}`)
|
|
77
116
|
|
|
78
|
-
console.log(
|
|
79
|
-
|
|
80
|
-
|
|
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)})`)
|
|
81
121
|
|
|
82
122
|
const result = await uploadFiles(
|
|
83
123
|
token,
|
|
@@ -87,13 +127,34 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
|
87
127
|
distDir,
|
|
88
128
|
puzzmoFile,
|
|
89
129
|
(batch, totalBatches, uploaded) => {
|
|
90
|
-
console.log(`
|
|
130
|
+
console.log(` Batch ${batch}/${totalBatches} done (${uploaded} file(s) uploaded)`)
|
|
91
131
|
},
|
|
92
|
-
{ verbose },
|
|
132
|
+
{ verbose, description, repoURL },
|
|
93
133
|
)
|
|
94
134
|
|
|
95
|
-
console.log(
|
|
96
|
-
|
|
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`)
|
|
97
158
|
}
|
|
98
159
|
|
|
99
160
|
/** Collects all files in a directory recursively */
|
|
@@ -109,19 +170,54 @@ const collectFiles = (dir: string): string[] => {
|
|
|
109
170
|
}
|
|
110
171
|
|
|
111
172
|
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
112
|
-
const getGitSHA = (): string | null => {
|
|
173
|
+
const getGitSHA = (cwd: string): string | null => {
|
|
113
174
|
try {
|
|
114
|
-
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim()
|
|
175
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8", cwd }).trim()
|
|
115
176
|
} catch {
|
|
116
177
|
return null
|
|
117
178
|
}
|
|
118
179
|
}
|
|
119
180
|
|
|
120
|
-
/**
|
|
121
|
-
const
|
|
181
|
+
/** Gets the subject line of the latest commit, or null if not in a git repo */
|
|
182
|
+
const getGitMessage = (cwd: string): string | null => {
|
|
183
|
+
try {
|
|
184
|
+
return execSync("git log -1 --pretty=%s", { encoding: "utf-8", cwd }).trim() || null
|
|
185
|
+
} catch {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Gets the origin remote URL normalized to https, or null if unavailable */
|
|
191
|
+
const getGitRepoURL = (cwd: string): string | null => {
|
|
192
|
+
try {
|
|
193
|
+
const raw = execSync("git config --get remote.origin.url", { encoding: "utf-8", cwd }).trim()
|
|
194
|
+
return raw ? normalizeRepoURL(raw) : null
|
|
195
|
+
} catch {
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Converts SSH-style git URLs to https; strips trailing .git */
|
|
201
|
+
const normalizeRepoURL = (url: string): string => {
|
|
202
|
+
let normalized = url
|
|
203
|
+
// git@host:owner/repo(.git) -> https://host/owner/repo
|
|
204
|
+
const sshMatch = normalized.match(/^git@([^:]+):(.+)$/)
|
|
205
|
+
if (sshMatch) normalized = `https://${sshMatch[1]}/${sshMatch[2]}`
|
|
206
|
+
// ssh://git@host/owner/repo(.git) -> https://host/owner/repo
|
|
207
|
+
else if (normalized.startsWith("ssh://")) normalized = normalized.replace(/^ssh:\/\/(?:[^@]+@)?/, "https://")
|
|
208
|
+
// git://host/owner/repo(.git) -> https://host/owner/repo
|
|
209
|
+
else if (normalized.startsWith("git://")) normalized = normalized.replace(/^git:\/\//, "https://")
|
|
210
|
+
return normalized.replace(/\.git$/, "")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Hashes the dist contents of every game to produce a deterministic SHA across all uploads */
|
|
214
|
+
const hashGames = (games: DiscoveredGame[]): string => {
|
|
122
215
|
const hash = crypto.createHash("sha256")
|
|
123
|
-
for (const
|
|
124
|
-
|
|
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
|
+
}
|
|
125
221
|
}
|
|
126
222
|
return hash.digest("hex").slice(0, 12)
|
|
127
223
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -1,46 +1,39 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
|
|
4
|
-
import {
|
|
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
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
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
|
|
18
|
-
puzzmo validate [dir]
|
|
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
|
|
@@ -25,6 +29,10 @@ export type UploadProgress = (batch: number, totalBatches: number, uploaded: num
|
|
|
25
29
|
/** Options for uploadFiles */
|
|
26
30
|
export type UploadFilesOptions = {
|
|
27
31
|
verbose?: boolean
|
|
32
|
+
/** Commit message subject for the new GameRuntimeVersion */
|
|
33
|
+
description?: string | null
|
|
34
|
+
/** HTTPS repo URL to set on the GameRuntime */
|
|
35
|
+
repoURL?: string | null
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/** Wraps fetch to surface the underlying network cause (DNS, ECONNREFUSED, TLS, etc.) */
|
|
@@ -56,9 +64,11 @@ const readResponse = async (res: Response, url: string, step: string, verbose: b
|
|
|
56
64
|
throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`)
|
|
57
65
|
}
|
|
58
66
|
if (!res.ok) {
|
|
59
|
-
const
|
|
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 - ")}` : ""
|
|
60
70
|
const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : ""
|
|
61
|
-
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}`)
|
|
62
72
|
}
|
|
63
73
|
return json
|
|
64
74
|
}
|
|
@@ -112,12 +122,18 @@ export const uploadFiles = async (
|
|
|
112
122
|
onProgress?: UploadProgress,
|
|
113
123
|
options: UploadFilesOptions = {},
|
|
114
124
|
): Promise<CompleteResponse> => {
|
|
115
|
-
const { verbose = false } = options
|
|
125
|
+
const { verbose = false, description, repoURL } = options
|
|
116
126
|
const apiURL = getAPIURL()
|
|
117
127
|
if (verbose) console.log(` API URL: ${apiURL}`)
|
|
118
128
|
|
|
119
129
|
// Step 1: Init session (includes puzzmo.json metadata)
|
|
120
|
-
const init = (await jsonPost(
|
|
130
|
+
const init = (await jsonPost(
|
|
131
|
+
`${apiURL}/cliUpload`,
|
|
132
|
+
token,
|
|
133
|
+
{ gameSlug, sha, puzzmoFile, description, repoURL },
|
|
134
|
+
"upload init",
|
|
135
|
+
verbose,
|
|
136
|
+
)) as InitResponse
|
|
121
137
|
if (verbose) console.log(` session: ${init.sessionID}`)
|
|
122
138
|
|
|
123
139
|
// Step 2: Upload files in concurrent batches
|