@puzzmo/cli 1.0.31 → 1.0.33
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 +5 -1
- package/lib/commands/upload.js +41 -40
- package/lib/commands/upload.js.map +1 -1
- package/lib/index.js +9 -4
- package/lib/index.js.map +1 -1
- package/lib/util/api.d.ts +5 -1
- package/lib/util/api.js +58 -18
- package/lib/util/api.js.map +1 -1
- package/lib/util/validatePuzzmoFile.js +31 -6
- package/lib/util/validatePuzzmoFile.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/upload.ts +53 -39
- package/src/index.ts +9 -4
- package/src/util/api.ts +76 -23
- package/src/util/validatePuzzmoFile.ts +30 -7
package/lib/commands/upload.d.ts
CHANGED
package/lib/commands/upload.js
CHANGED
|
@@ -5,46 +5,9 @@ import path from "node:path";
|
|
|
5
5
|
import { uploadFiles } from "../util/api.js";
|
|
6
6
|
import { getAPIURL, getToken } from "../util/config.js";
|
|
7
7
|
import { validatePuzzmoJson } from "../util/validatePuzzmoFile.js";
|
|
8
|
-
/** Collects all files in a directory recursively */
|
|
9
|
-
const collectFiles = (dir) => {
|
|
10
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
11
|
-
const files = [];
|
|
12
|
-
for (const entry of entries) {
|
|
13
|
-
const full = path.join(dir, entry.name);
|
|
14
|
-
if (entry.isDirectory())
|
|
15
|
-
files.push(...collectFiles(full));
|
|
16
|
-
else
|
|
17
|
-
files.push(full);
|
|
18
|
-
}
|
|
19
|
-
return files;
|
|
20
|
-
};
|
|
21
|
-
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
22
|
-
const getGitSHA = () => {
|
|
23
|
-
try {
|
|
24
|
-
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
/** Hashes all file contents to produce a deterministic SHA */
|
|
31
|
-
const hashFiles = (filePaths) => {
|
|
32
|
-
const hash = crypto.createHash("sha256");
|
|
33
|
-
for (const fp of filePaths.sort()) {
|
|
34
|
-
hash.update(fs.readFileSync(fp));
|
|
35
|
-
}
|
|
36
|
-
return hash.digest("hex").slice(0, 12);
|
|
37
|
-
};
|
|
38
|
-
/** Formats a byte count as a human-readable string */
|
|
39
|
-
const formatBytes = (bytes) => {
|
|
40
|
-
if (bytes < 1024)
|
|
41
|
-
return `${bytes} B`;
|
|
42
|
-
if (bytes < 1024 * 1024)
|
|
43
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
44
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
45
|
-
};
|
|
46
8
|
/** Uploads game build artifacts to Puzzmo */
|
|
47
|
-
export const upload = async (dir) => {
|
|
9
|
+
export const upload = async (dir, options = {}) => {
|
|
10
|
+
const { verbose = false } = options;
|
|
48
11
|
const token = getToken();
|
|
49
12
|
if (!token) {
|
|
50
13
|
console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.");
|
|
@@ -105,8 +68,46 @@ export const upload = async (dir) => {
|
|
|
105
68
|
console.log("Uploading...");
|
|
106
69
|
const result = await uploadFiles(token, gameSlug, sha, files, distDir, puzzmoFile, (batch, totalBatches, uploaded) => {
|
|
107
70
|
console.log(` Batch ${batch}/${totalBatches} done (${uploaded} file(s) uploaded)`);
|
|
108
|
-
});
|
|
71
|
+
}, { verbose });
|
|
109
72
|
console.log(`\nDone - ${result.versionID}`);
|
|
110
73
|
console.log(`Assets: ${result.assetsBase}`);
|
|
111
74
|
};
|
|
75
|
+
/** Collects all files in a directory recursively */
|
|
76
|
+
const collectFiles = (dir) => {
|
|
77
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
78
|
+
const files = [];
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const full = path.join(dir, entry.name);
|
|
81
|
+
if (entry.isDirectory())
|
|
82
|
+
files.push(...collectFiles(full));
|
|
83
|
+
else
|
|
84
|
+
files.push(full);
|
|
85
|
+
}
|
|
86
|
+
return files;
|
|
87
|
+
};
|
|
88
|
+
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
89
|
+
const getGitSHA = () => {
|
|
90
|
+
try {
|
|
91
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Hashes all file contents to produce a deterministic SHA */
|
|
98
|
+
const hashFiles = (filePaths) => {
|
|
99
|
+
const hash = crypto.createHash("sha256");
|
|
100
|
+
for (const fp of filePaths.sort()) {
|
|
101
|
+
hash.update(fs.readFileSync(fp));
|
|
102
|
+
}
|
|
103
|
+
return hash.digest("hex").slice(0, 12);
|
|
104
|
+
};
|
|
105
|
+
/** Formats a byte count as a human-readable string */
|
|
106
|
+
const formatBytes = (bytes) => {
|
|
107
|
+
if (bytes < 1024)
|
|
108
|
+
return `${bytes} B`;
|
|
109
|
+
if (bytes < 1024 * 1024)
|
|
110
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
111
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
112
|
+
};
|
|
112
113
|
//# sourceMappingURL=upload.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const printUsage = () => {
|
|
|
10
10
|
puzzmo login <token> Save a CLI auth token
|
|
11
11
|
puzzmo game create [token] Create a new Puzzmo game project
|
|
12
12
|
|
|
13
|
-
puzzmo upload <dir>
|
|
13
|
+
puzzmo upload <dir> [-v] Upload game build from <dir> (slug from puzzmo.json). -v/--verbose prints request URLs and full error bodies.
|
|
14
14
|
puzzmo validate [dir] Validate puzzmo.json in a directory (default: .)
|
|
15
15
|
puzzmo migrate List and select migration skills from dev.puzzmo.com`);
|
|
16
16
|
};
|
|
@@ -26,12 +26,13 @@ const run = async () => {
|
|
|
26
26
|
break;
|
|
27
27
|
}
|
|
28
28
|
case "upload": {
|
|
29
|
-
const
|
|
29
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
30
|
+
const dir = args.find((a) => !a.startsWith("-"));
|
|
30
31
|
if (!dir) {
|
|
31
|
-
console.error("Usage: puzzmo upload <dir>");
|
|
32
|
+
console.error("Usage: puzzmo upload <dir> [-v|--verbose]");
|
|
32
33
|
process.exit(1);
|
|
33
34
|
}
|
|
34
|
-
await upload(dir);
|
|
35
|
+
await upload(dir, { verbose });
|
|
35
36
|
break;
|
|
36
37
|
}
|
|
37
38
|
case "game": {
|
|
@@ -60,6 +61,10 @@ const run = async () => {
|
|
|
60
61
|
};
|
|
61
62
|
run().catch((err) => {
|
|
62
63
|
console.error(err.message || err);
|
|
64
|
+
if (err && err.cause) {
|
|
65
|
+
const cause = err.cause;
|
|
66
|
+
console.error(`Caused by: ${cause instanceof Error ? cause.message : cause}`);
|
|
67
|
+
}
|
|
63
68
|
process.exit(1);
|
|
64
69
|
});
|
|
65
70
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAE/C,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAEhD,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC;;;;;;uFAMyE,CAAC,CAAA;AAAA,CACvF,CAAA;AAED,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE,CAAC;IACtB,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;YACrB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;gBAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;YACD,KAAK,CAAC,KAAK,CAAC,CAAA;YACZ,MAAK;QACP,CAAC;QACD,KAAK,QAAQ,EAAE,CAAC;YACd,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAE/C,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAEhD,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC;;;;;;uFAMyE,CAAC,CAAA;AAAA,CACvF,CAAA;AAED,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE,CAAC;IACtB,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;YACrB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;gBAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;YACD,KAAK,CAAC,KAAK,CAAC,CAAA;YACZ,MAAK;QACP,CAAC;QACD,KAAK,QAAQ,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;YACjE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;YAChD,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAA;gBAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;YACD,MAAM,MAAM,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;YAC9B,MAAK;QACP,CAAC;QACD,KAAK,MAAM,EAAE,CAAC;YACZ,MAAM,CAAC,UAAU,EAAE,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;YACrC,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;gBAC5B,MAAM,UAAU,CAAC,OAAO,CAAC,CAAA;YAC3B,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,0GAA0G,CAAC,CAAA;gBACzH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;YACD,MAAK;QACP,CAAC;QACD,KAAK,UAAU,EAAE,CAAC;YAChB,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;YACxB,MAAK;QACP,CAAC;QACD,KAAK,SAAS,EAAE,CAAC;YACf,MAAM,OAAO,EAAE,CAAA;YACf,MAAK;QACP,CAAC;QACD;YACE,UAAU,EAAE,CAAA;YACZ,MAAK;IACT,CAAC;AAAA,CACF,CAAA;AAED,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;IACnB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,CAAA;IACjC,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;QACvB,OAAO,CAAC,KAAK,CAAC,cAAc,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;IAC/E,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAAA,CAChB,CAAC,CAAA"}
|
package/lib/util/api.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ type CompleteResponse = {
|
|
|
14
14
|
};
|
|
15
15
|
/** Callback for reporting batch upload progress */
|
|
16
16
|
export type UploadProgress = (batch: number, totalBatches: number, uploaded: number) => void;
|
|
17
|
+
/** Options for uploadFiles */
|
|
18
|
+
export type UploadFilesOptions = {
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
};
|
|
17
21
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
18
|
-
export declare const uploadFiles: (token: string, gameSlug: string, sha: string, filePaths: string[], baseDir: string, puzzmoFile: PuzzmoFile, onProgress?: UploadProgress | undefined) => Promise<CompleteResponse>;
|
|
22
|
+
export declare const uploadFiles: (token: string, gameSlug: string, sha: string, filePaths: string[], baseDir: string, puzzmoFile: PuzzmoFile, onProgress?: UploadProgress | undefined, options?: UploadFilesOptions) => Promise<CompleteResponse>;
|
|
19
23
|
export {};
|
package/lib/util/api.js
CHANGED
|
@@ -2,40 +2,80 @@ 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
|
+
/** Wraps fetch to surface the underlying network cause (DNS, ECONNREFUSED, TLS, etc.) */
|
|
6
|
+
const fetchWithContext = async (url, init, step, verbose) => {
|
|
7
|
+
if (verbose)
|
|
8
|
+
console.log(` → ${init.method ?? "GET"} ${url} [${step}]`);
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(url, init);
|
|
11
|
+
if (verbose)
|
|
12
|
+
console.log(` ← ${res.status} ${res.statusText} [${step}]`);
|
|
13
|
+
return res;
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
const cause = e.cause;
|
|
17
|
+
const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : e.message;
|
|
18
|
+
if (verbose && e instanceof Error && e.stack)
|
|
19
|
+
console.error(e.stack);
|
|
20
|
+
throw new Error(`Network error during ${step} (${url}): ${causeMsg}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/** Reads a response body, parsing JSON when possible and including status + body in errors */
|
|
24
|
+
const readResponse = async (res, url, step, verbose) => {
|
|
25
|
+
const text = await res.text();
|
|
26
|
+
let json;
|
|
27
|
+
try {
|
|
28
|
+
json = text ? JSON.parse(text) : {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const snippet = verbose ? text : text.slice(0, 200).replace(/\s+/g, " ").trim();
|
|
33
|
+
throw new Error(`Server error during ${step} (${res.status} ${res.statusText} from ${url}): ${snippet || "no body"}`);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`);
|
|
36
|
+
}
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const serverMsg = json.error || res.statusText || "Unknown error";
|
|
39
|
+
const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : "";
|
|
40
|
+
throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${detail}`);
|
|
41
|
+
}
|
|
42
|
+
return json;
|
|
43
|
+
};
|
|
5
44
|
/** Sends a JSON POST request */
|
|
6
|
-
const jsonPost = async (url, token, body) => {
|
|
7
|
-
const res = await
|
|
45
|
+
const jsonPost = async (url, token, body, step, verbose) => {
|
|
46
|
+
const res = await fetchWithContext(url, {
|
|
8
47
|
method: "POST",
|
|
9
48
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
10
49
|
body: JSON.stringify(body),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
if (!res.ok)
|
|
14
|
-
throw new Error(`Server error (${res.status}): ${json.error || "Unknown error"}`);
|
|
15
|
-
return json;
|
|
50
|
+
}, step, verbose);
|
|
51
|
+
return readResponse(res, url, step, verbose);
|
|
16
52
|
};
|
|
17
53
|
/** Uploads a single file as raw binary with filename in query param */
|
|
18
|
-
const uploadFile = async (url, token, filePath, baseDir) => {
|
|
54
|
+
const uploadFile = async (url, token, filePath, baseDir, verbose) => {
|
|
19
55
|
const relativePath = path.relative(baseDir, filePath);
|
|
20
56
|
const content = fs.readFileSync(filePath);
|
|
21
|
-
const
|
|
57
|
+
const step = `file upload (${relativePath})`;
|
|
58
|
+
const fullURL = `${url}?name=${encodeURIComponent(relativePath)}`;
|
|
59
|
+
const res = await fetchWithContext(fullURL, {
|
|
22
60
|
method: "POST",
|
|
23
61
|
headers: {
|
|
24
62
|
Authorization: `Bearer ${token}`,
|
|
25
63
|
"Content-Type": "application/octet-stream",
|
|
26
64
|
},
|
|
27
65
|
body: content,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (!res.ok)
|
|
31
|
-
throw new Error(`Server error (${res.status}): ${json.error || "Unknown error"}`);
|
|
32
|
-
return json;
|
|
66
|
+
}, step, verbose);
|
|
67
|
+
return (await readResponse(res, fullURL, step, verbose));
|
|
33
68
|
};
|
|
34
69
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
35
|
-
export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzzmoFile, onProgress) => {
|
|
70
|
+
export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzzmoFile, onProgress, options = {}) => {
|
|
71
|
+
const { verbose = false } = options;
|
|
36
72
|
const apiURL = getAPIURL();
|
|
73
|
+
if (verbose)
|
|
74
|
+
console.log(` API URL: ${apiURL}`);
|
|
37
75
|
// Step 1: Init session (includes puzzmo.json metadata)
|
|
38
|
-
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile }));
|
|
76
|
+
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile }, "upload init", verbose));
|
|
77
|
+
if (verbose)
|
|
78
|
+
console.log(` session: ${init.sessionID}`);
|
|
39
79
|
// Step 2: Upload files in concurrent batches
|
|
40
80
|
const fileURL = `${apiURL}/cliUpload/${init.sessionID}/file`;
|
|
41
81
|
const totalBatches = Math.ceil(filePaths.length / BATCH_SIZE);
|
|
@@ -43,11 +83,11 @@ export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzz
|
|
|
43
83
|
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
44
84
|
const batch = filePaths.slice(i, i + BATCH_SIZE);
|
|
45
85
|
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
|
|
46
|
-
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir)));
|
|
86
|
+
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir, verbose)));
|
|
47
87
|
totalUploaded += batch.length;
|
|
48
88
|
onProgress?.(batchNum, totalBatches, totalUploaded);
|
|
49
89
|
}
|
|
50
90
|
// Step 3: Complete
|
|
51
|
-
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}));
|
|
91
|
+
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}, "upload complete", verbose));
|
|
52
92
|
};
|
|
53
93
|
//# sourceMappingURL=api.js.map
|
package/lib/util/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/util/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAYvC,MAAM,UAAU,GAAG,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/util/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import { Validator } from "@cfworker/json-schema";
|
|
2
|
-
const schemaURL = "https://dev.puzzmo.com/schema/puzzmo-file-schema.json"
|
|
2
|
+
const schemaURL = "https://dev-dj9e.onrender.com/schema/puzzmo-file-schema.json"; //"https://dev.puzzmo.com/schema/puzzmo-file-schema.json"
|
|
3
3
|
let cachedValidator;
|
|
4
4
|
/** Fetches and caches a JSON schema validator for puzzmo.json files */
|
|
5
5
|
const getValidator = async () => {
|
|
6
6
|
if (cachedValidator)
|
|
7
7
|
return cachedValidator;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
let res;
|
|
9
|
+
try {
|
|
10
|
+
res = await fetch(schemaURL);
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
const cause = e.cause;
|
|
14
|
+
const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : e.message;
|
|
15
|
+
throw new Error(`Network error fetching puzzmo file schema (${schemaURL}): ${causeMsg}`);
|
|
16
|
+
}
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
throw new Error(`Failed to fetch puzzmo file schema: ${res.status} ${res.statusText} from ${schemaURL}`);
|
|
19
|
+
}
|
|
20
|
+
let puzzmoSchema;
|
|
21
|
+
try {
|
|
22
|
+
puzzmoSchema = await res.json();
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
throw new Error(`Invalid JSON in puzzmo file schema (${schemaURL}): ${e instanceof Error ? e.message : e}`);
|
|
26
|
+
}
|
|
12
27
|
cachedValidator = new Validator(puzzmoSchema, "7");
|
|
13
28
|
return cachedValidator;
|
|
14
29
|
};
|
|
15
30
|
/** Validates a parsed object against the puzzmo.json JSON schema */
|
|
16
31
|
export const validatePuzzmoJson = async (data) => {
|
|
17
32
|
const validator = await getValidator();
|
|
18
|
-
const
|
|
33
|
+
const toValidate = stripSchemaProperty(data);
|
|
34
|
+
const result = validator.validate(toValidate);
|
|
19
35
|
if (result.valid)
|
|
20
36
|
return { valid: true, data: data };
|
|
21
37
|
return {
|
|
@@ -25,4 +41,13 @@ export const validatePuzzmoJson = async (data) => {
|
|
|
25
41
|
.map((e) => `${e.instanceLocation}: ${e.error}`),
|
|
26
42
|
};
|
|
27
43
|
};
|
|
44
|
+
/** Removes the `$schema` editor-hint property so it doesn't trip additionalProperties checks */
|
|
45
|
+
const stripSchemaProperty = (data) => {
|
|
46
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
47
|
+
return data;
|
|
48
|
+
if (!("$schema" in data))
|
|
49
|
+
return data;
|
|
50
|
+
const { $schema: _, ...rest } = data;
|
|
51
|
+
return rest;
|
|
52
|
+
};
|
|
28
53
|
//# sourceMappingURL=validatePuzzmoFile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validatePuzzmoFile.js","sourceRoot":"","sources":["../../src/util/validatePuzzmoFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,
|
|
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
|
@@ -7,45 +7,13 @@ import { uploadFiles } from "../util/api.js"
|
|
|
7
7
|
import { getAPIURL, getToken } from "../util/config.js"
|
|
8
8
|
import { validatePuzzmoJson } from "../util/validatePuzzmoFile.js"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
13
|
-
const files: string[] = []
|
|
14
|
-
for (const entry of entries) {
|
|
15
|
-
const full = path.join(dir, entry.name)
|
|
16
|
-
if (entry.isDirectory()) files.push(...collectFiles(full))
|
|
17
|
-
else files.push(full)
|
|
18
|
-
}
|
|
19
|
-
return files
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
23
|
-
const getGitSHA = (): string | null => {
|
|
24
|
-
try {
|
|
25
|
-
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim()
|
|
26
|
-
} catch {
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Hashes all file contents to produce a deterministic SHA */
|
|
32
|
-
const hashFiles = (filePaths: string[]): string => {
|
|
33
|
-
const hash = crypto.createHash("sha256")
|
|
34
|
-
for (const fp of filePaths.sort()) {
|
|
35
|
-
hash.update(fs.readFileSync(fp))
|
|
36
|
-
}
|
|
37
|
-
return hash.digest("hex").slice(0, 12)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Formats a byte count as a human-readable string */
|
|
41
|
-
const formatBytes = (bytes: number): string => {
|
|
42
|
-
if (bytes < 1024) return `${bytes} B`
|
|
43
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
44
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
10
|
+
type UploadOptions = {
|
|
11
|
+
verbose?: boolean
|
|
45
12
|
}
|
|
46
13
|
|
|
47
14
|
/** Uploads game build artifacts to Puzzmo */
|
|
48
|
-
export const upload = async (dir: string) => {
|
|
15
|
+
export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
16
|
+
const { verbose = false } = options
|
|
49
17
|
const token = getToken()
|
|
50
18
|
if (!token) {
|
|
51
19
|
console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.")
|
|
@@ -111,10 +79,56 @@ export const upload = async (dir: string) => {
|
|
|
111
79
|
if (apiURL !== defaultURL) console.log(`Uploading to ${apiURL}...`)
|
|
112
80
|
else console.log("Uploading...")
|
|
113
81
|
|
|
114
|
-
const result = await uploadFiles(
|
|
115
|
-
|
|
116
|
-
|
|
82
|
+
const result = await uploadFiles(
|
|
83
|
+
token,
|
|
84
|
+
gameSlug,
|
|
85
|
+
sha,
|
|
86
|
+
files,
|
|
87
|
+
distDir,
|
|
88
|
+
puzzmoFile,
|
|
89
|
+
(batch, totalBatches, uploaded) => {
|
|
90
|
+
console.log(` Batch ${batch}/${totalBatches} done (${uploaded} file(s) uploaded)`)
|
|
91
|
+
},
|
|
92
|
+
{ verbose },
|
|
93
|
+
)
|
|
117
94
|
|
|
118
95
|
console.log(`\nDone - ${result.versionID}`)
|
|
119
96
|
console.log(`Assets: ${result.assetsBase}`)
|
|
120
97
|
}
|
|
98
|
+
|
|
99
|
+
/** Collects all files in a directory recursively */
|
|
100
|
+
const collectFiles = (dir: string): string[] => {
|
|
101
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
102
|
+
const files: string[] = []
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const full = path.join(dir, entry.name)
|
|
105
|
+
if (entry.isDirectory()) files.push(...collectFiles(full))
|
|
106
|
+
else files.push(full)
|
|
107
|
+
}
|
|
108
|
+
return files
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
112
|
+
const getGitSHA = (): string | null => {
|
|
113
|
+
try {
|
|
114
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim()
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Hashes all file contents to produce a deterministic SHA */
|
|
121
|
+
const hashFiles = (filePaths: string[]): string => {
|
|
122
|
+
const hash = crypto.createHash("sha256")
|
|
123
|
+
for (const fp of filePaths.sort()) {
|
|
124
|
+
hash.update(fs.readFileSync(fp))
|
|
125
|
+
}
|
|
126
|
+
return hash.digest("hex").slice(0, 12)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Formats a byte count as a human-readable string */
|
|
130
|
+
const formatBytes = (bytes: number): string => {
|
|
131
|
+
if (bytes < 1024) return `${bytes} B`
|
|
132
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
133
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
134
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ const printUsage = () => {
|
|
|
13
13
|
puzzmo login <token> Save a CLI auth token
|
|
14
14
|
puzzmo game create [token] Create a new Puzzmo game project
|
|
15
15
|
|
|
16
|
-
puzzmo upload <dir>
|
|
16
|
+
puzzmo upload <dir> [-v] Upload game build from <dir> (slug from puzzmo.json). -v/--verbose prints request URLs and full error bodies.
|
|
17
17
|
puzzmo validate [dir] Validate puzzmo.json in a directory (default: .)
|
|
18
18
|
puzzmo migrate List and select migration skills from dev.puzzmo.com`)
|
|
19
19
|
}
|
|
@@ -30,12 +30,13 @@ const run = async () => {
|
|
|
30
30
|
break
|
|
31
31
|
}
|
|
32
32
|
case "upload": {
|
|
33
|
-
const
|
|
33
|
+
const verbose = args.includes("--verbose") || args.includes("-v")
|
|
34
|
+
const dir = args.find((a) => !a.startsWith("-"))
|
|
34
35
|
if (!dir) {
|
|
35
|
-
console.error("Usage: puzzmo upload <dir>")
|
|
36
|
+
console.error("Usage: puzzmo upload <dir> [-v|--verbose]")
|
|
36
37
|
process.exit(1)
|
|
37
38
|
}
|
|
38
|
-
await upload(dir)
|
|
39
|
+
await upload(dir, { verbose })
|
|
39
40
|
break
|
|
40
41
|
}
|
|
41
42
|
case "game": {
|
|
@@ -64,5 +65,9 @@ const run = async () => {
|
|
|
64
65
|
|
|
65
66
|
run().catch((err) => {
|
|
66
67
|
console.error(err.message || err)
|
|
68
|
+
if (err && err.cause) {
|
|
69
|
+
const cause = err.cause
|
|
70
|
+
console.error(`Caused by: ${cause instanceof Error ? cause.message : cause}`)
|
|
71
|
+
}
|
|
67
72
|
process.exit(1)
|
|
68
73
|
})
|
package/src/util/api.ts
CHANGED
|
@@ -22,34 +22,83 @@ type CompleteResponse = { assetsBase: string; versionID: string; error?: string
|
|
|
22
22
|
/** Callback for reporting batch upload progress */
|
|
23
23
|
export type UploadProgress = (batch: number, totalBatches: number, uploaded: number) => void
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
/** Options for uploadFiles */
|
|
26
|
+
export type UploadFilesOptions = {
|
|
27
|
+
verbose?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Wraps fetch to surface the underlying network cause (DNS, ECONNREFUSED, TLS, etc.) */
|
|
31
|
+
const fetchWithContext = async (url: string, init: RequestInit, step: string, verbose: boolean): Promise<Response> => {
|
|
32
|
+
if (verbose) console.log(` → ${init.method ?? "GET"} ${url} [${step}]`)
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(url, init)
|
|
35
|
+
if (verbose) console.log(` ← ${res.status} ${res.statusText} [${step}]`)
|
|
36
|
+
return res
|
|
37
|
+
} catch (e) {
|
|
38
|
+
const cause = (e as { cause?: unknown }).cause
|
|
39
|
+
const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : (e as Error).message
|
|
40
|
+
if (verbose && e instanceof Error && e.stack) console.error(e.stack)
|
|
41
|
+
throw new Error(`Network error during ${step} (${url}): ${causeMsg}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Reads a response body, parsing JSON when possible and including status + body in errors */
|
|
46
|
+
const readResponse = async (res: Response, url: string, step: string, verbose: boolean) => {
|
|
47
|
+
const text = await res.text()
|
|
48
|
+
let json: unknown
|
|
49
|
+
try {
|
|
50
|
+
json = text ? JSON.parse(text) : {}
|
|
51
|
+
} catch {
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const snippet = verbose ? text : text.slice(0, 200).replace(/\s+/g, " ").trim()
|
|
54
|
+
throw new Error(`Server error during ${step} (${res.status} ${res.statusText} from ${url}): ${snippet || "no body"}`)
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`)
|
|
57
|
+
}
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const serverMsg = (json as { error?: string }).error || res.statusText || "Unknown error"
|
|
60
|
+
const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : ""
|
|
61
|
+
throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${detail}`)
|
|
62
|
+
}
|
|
34
63
|
return json
|
|
35
64
|
}
|
|
36
65
|
|
|
66
|
+
/** Sends a JSON POST request */
|
|
67
|
+
const jsonPost = async (url: string, token: string, body: object, step: string, verbose: boolean) => {
|
|
68
|
+
const res = await fetchWithContext(
|
|
69
|
+
url,
|
|
70
|
+
{
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
},
|
|
75
|
+
step,
|
|
76
|
+
verbose,
|
|
77
|
+
)
|
|
78
|
+
return readResponse(res, url, step, verbose)
|
|
79
|
+
}
|
|
80
|
+
|
|
37
81
|
/** Uploads a single file as raw binary with filename in query param */
|
|
38
|
-
const uploadFile = async (url: string, token: string, filePath: string, baseDir: string): Promise<FileResponse> => {
|
|
82
|
+
const uploadFile = async (url: string, token: string, filePath: string, baseDir: string, verbose: boolean): Promise<FileResponse> => {
|
|
39
83
|
const relativePath = path.relative(baseDir, filePath)
|
|
40
84
|
const content = fs.readFileSync(filePath)
|
|
85
|
+
const step = `file upload (${relativePath})`
|
|
41
86
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
const fullURL = `${url}?name=${encodeURIComponent(relativePath)}`
|
|
88
|
+
const res = await fetchWithContext(
|
|
89
|
+
fullURL,
|
|
90
|
+
{
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: `Bearer ${token}`,
|
|
94
|
+
"Content-Type": "application/octet-stream",
|
|
95
|
+
},
|
|
96
|
+
body: content,
|
|
47
97
|
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return json as FileResponse
|
|
98
|
+
step,
|
|
99
|
+
verbose,
|
|
100
|
+
)
|
|
101
|
+
return (await readResponse(res, fullURL, step, verbose)) as FileResponse
|
|
53
102
|
}
|
|
54
103
|
|
|
55
104
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
@@ -61,11 +110,15 @@ export const uploadFiles = async (
|
|
|
61
110
|
baseDir: string,
|
|
62
111
|
puzzmoFile: PuzzmoFile,
|
|
63
112
|
onProgress?: UploadProgress,
|
|
113
|
+
options: UploadFilesOptions = {},
|
|
64
114
|
): Promise<CompleteResponse> => {
|
|
115
|
+
const { verbose = false } = options
|
|
65
116
|
const apiURL = getAPIURL()
|
|
117
|
+
if (verbose) console.log(` API URL: ${apiURL}`)
|
|
66
118
|
|
|
67
119
|
// Step 1: Init session (includes puzzmo.json metadata)
|
|
68
|
-
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile })) as InitResponse
|
|
120
|
+
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile }, "upload init", verbose)) as InitResponse
|
|
121
|
+
if (verbose) console.log(` session: ${init.sessionID}`)
|
|
69
122
|
|
|
70
123
|
// Step 2: Upload files in concurrent batches
|
|
71
124
|
const fileURL = `${apiURL}/cliUpload/${init.sessionID}/file`
|
|
@@ -75,11 +128,11 @@ export const uploadFiles = async (
|
|
|
75
128
|
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
76
129
|
const batch = filePaths.slice(i, i + BATCH_SIZE)
|
|
77
130
|
const batchNum = Math.floor(i / BATCH_SIZE) + 1
|
|
78
|
-
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir)))
|
|
131
|
+
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir, verbose)))
|
|
79
132
|
totalUploaded += batch.length
|
|
80
133
|
onProgress?.(batchNum, totalBatches, totalUploaded)
|
|
81
134
|
}
|
|
82
135
|
|
|
83
136
|
// Step 3: Complete
|
|
84
|
-
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {})) as CompleteResponse
|
|
137
|
+
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}, "upload complete", verbose)) as CompleteResponse
|
|
85
138
|
}
|
|
@@ -1,18 +1,32 @@
|
|
|
1
|
-
import { Validator } from "@cfworker/json-schema"
|
|
1
|
+
import { Validator, type Schema } from "@cfworker/json-schema"
|
|
2
2
|
|
|
3
3
|
import type { PuzzmoFile } from "./api.js"
|
|
4
4
|
|
|
5
|
-
const schemaURL = "https://dev.puzzmo.com/schema/puzzmo-file-schema.json"
|
|
5
|
+
const schemaURL = "https://dev-dj9e.onrender.com/schema/puzzmo-file-schema.json" //"https://dev.puzzmo.com/schema/puzzmo-file-schema.json"
|
|
6
6
|
|
|
7
7
|
let cachedValidator: Validator | undefined
|
|
8
8
|
|
|
9
9
|
/** Fetches and caches a JSON schema validator for puzzmo.json files */
|
|
10
10
|
const getValidator = async () => {
|
|
11
11
|
if (cachedValidator) return cachedValidator
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
let res: Response
|
|
13
|
+
try {
|
|
14
|
+
res = await fetch(schemaURL)
|
|
15
|
+
} catch (e) {
|
|
16
|
+
const cause = (e as { cause?: unknown }).cause
|
|
17
|
+
const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : (e as Error).message
|
|
18
|
+
throw new Error(`Network error fetching puzzmo file schema (${schemaURL}): ${causeMsg}`)
|
|
19
|
+
}
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
throw new Error(`Failed to fetch puzzmo file schema: ${res.status} ${res.statusText} from ${schemaURL}`)
|
|
22
|
+
}
|
|
23
|
+
let puzzmoSchema: unknown
|
|
24
|
+
try {
|
|
25
|
+
puzzmoSchema = await res.json()
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(`Invalid JSON in puzzmo file schema (${schemaURL}): ${e instanceof Error ? e.message : e}`)
|
|
28
|
+
}
|
|
29
|
+
cachedValidator = new Validator(puzzmoSchema as Schema, "7")
|
|
16
30
|
return cachedValidator
|
|
17
31
|
}
|
|
18
32
|
|
|
@@ -21,7 +35,8 @@ type ValidationResult = { valid: true; data: PuzzmoFile } | { valid: false; erro
|
|
|
21
35
|
/** Validates a parsed object against the puzzmo.json JSON schema */
|
|
22
36
|
export const validatePuzzmoJson = async (data: unknown): Promise<ValidationResult> => {
|
|
23
37
|
const validator = await getValidator()
|
|
24
|
-
const
|
|
38
|
+
const toValidate = stripSchemaProperty(data)
|
|
39
|
+
const result = validator.validate(toValidate)
|
|
25
40
|
if (result.valid) return { valid: true, data: data as PuzzmoFile }
|
|
26
41
|
return {
|
|
27
42
|
valid: false,
|
|
@@ -30,3 +45,11 @@ export const validatePuzzmoJson = async (data: unknown): Promise<ValidationResul
|
|
|
30
45
|
.map((e) => `${e.instanceLocation}: ${e.error}`),
|
|
31
46
|
}
|
|
32
47
|
}
|
|
48
|
+
|
|
49
|
+
/** Removes the `$schema` editor-hint property so it doesn't trip additionalProperties checks */
|
|
50
|
+
const stripSchemaProperty = (data: unknown): unknown => {
|
|
51
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return data
|
|
52
|
+
if (!("$schema" in data)) return data
|
|
53
|
+
const { $schema: _, ...rest } = data as Record<string, unknown>
|
|
54
|
+
return rest
|
|
55
|
+
}
|