@puzzmo/cli 1.0.32 → 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 +5 -4
- package/lib/index.js.map +1 -1
- package/lib/util/api.d.ts +5 -1
- package/lib/util/api.js +29 -16
- 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 +5 -4
- package/src/util/api.ts +29 -13
- 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": {
|
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":"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"}
|
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
|
@@ -3,18 +3,25 @@ import path from "node:path";
|
|
|
3
3
|
import { getAPIURL } from "./config.js";
|
|
4
4
|
const BATCH_SIZE = 10;
|
|
5
5
|
/** Wraps fetch to surface the underlying network cause (DNS, ECONNREFUSED, TLS, etc.) */
|
|
6
|
-
const fetchWithContext = async (url, init, step) => {
|
|
6
|
+
const fetchWithContext = async (url, init, step, verbose) => {
|
|
7
|
+
if (verbose)
|
|
8
|
+
console.log(` → ${init.method ?? "GET"} ${url} [${step}]`);
|
|
7
9
|
try {
|
|
8
|
-
|
|
10
|
+
const res = await fetch(url, init);
|
|
11
|
+
if (verbose)
|
|
12
|
+
console.log(` ← ${res.status} ${res.statusText} [${step}]`);
|
|
13
|
+
return res;
|
|
9
14
|
}
|
|
10
15
|
catch (e) {
|
|
11
16
|
const cause = e.cause;
|
|
12
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);
|
|
13
20
|
throw new Error(`Network error during ${step} (${url}): ${causeMsg}`);
|
|
14
21
|
}
|
|
15
22
|
};
|
|
16
23
|
/** Reads a response body, parsing JSON when possible and including status + body in errors */
|
|
17
|
-
const readResponse = async (res, url, step) => {
|
|
24
|
+
const readResponse = async (res, url, step, verbose) => {
|
|
18
25
|
const text = await res.text();
|
|
19
26
|
let json;
|
|
20
27
|
try {
|
|
@@ -22,28 +29,29 @@ const readResponse = async (res, url, step) => {
|
|
|
22
29
|
}
|
|
23
30
|
catch {
|
|
24
31
|
if (!res.ok) {
|
|
25
|
-
const snippet = text.slice(0, 200).replace(/\s+/g, " ").trim();
|
|
32
|
+
const snippet = verbose ? text : text.slice(0, 200).replace(/\s+/g, " ").trim();
|
|
26
33
|
throw new Error(`Server error during ${step} (${res.status} ${res.statusText} from ${url}): ${snippet || "no body"}`);
|
|
27
34
|
}
|
|
28
|
-
throw new Error(`Invalid JSON response during ${step} (${url}): ${text.slice(0, 200)}`);
|
|
35
|
+
throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`);
|
|
29
36
|
}
|
|
30
37
|
if (!res.ok) {
|
|
31
38
|
const serverMsg = json.error || res.statusText || "Unknown error";
|
|
32
|
-
|
|
39
|
+
const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : "";
|
|
40
|
+
throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${detail}`);
|
|
33
41
|
}
|
|
34
42
|
return json;
|
|
35
43
|
};
|
|
36
44
|
/** Sends a JSON POST request */
|
|
37
|
-
const jsonPost = async (url, token, body, step) => {
|
|
45
|
+
const jsonPost = async (url, token, body, step, verbose) => {
|
|
38
46
|
const res = await fetchWithContext(url, {
|
|
39
47
|
method: "POST",
|
|
40
48
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
41
49
|
body: JSON.stringify(body),
|
|
42
|
-
}, step);
|
|
43
|
-
return readResponse(res, url, step);
|
|
50
|
+
}, step, verbose);
|
|
51
|
+
return readResponse(res, url, step, verbose);
|
|
44
52
|
};
|
|
45
53
|
/** Uploads a single file as raw binary with filename in query param */
|
|
46
|
-
const uploadFile = async (url, token, filePath, baseDir) => {
|
|
54
|
+
const uploadFile = async (url, token, filePath, baseDir, verbose) => {
|
|
47
55
|
const relativePath = path.relative(baseDir, filePath);
|
|
48
56
|
const content = fs.readFileSync(filePath);
|
|
49
57
|
const step = `file upload (${relativePath})`;
|
|
@@ -55,14 +63,19 @@ const uploadFile = async (url, token, filePath, baseDir) => {
|
|
|
55
63
|
"Content-Type": "application/octet-stream",
|
|
56
64
|
},
|
|
57
65
|
body: content,
|
|
58
|
-
}, step);
|
|
59
|
-
return (await readResponse(res, fullURL, step));
|
|
66
|
+
}, step, verbose);
|
|
67
|
+
return (await readResponse(res, fullURL, step, verbose));
|
|
60
68
|
};
|
|
61
69
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
62
|
-
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;
|
|
63
72
|
const apiURL = getAPIURL();
|
|
73
|
+
if (verbose)
|
|
74
|
+
console.log(` API URL: ${apiURL}`);
|
|
64
75
|
// Step 1: Init session (includes puzzmo.json metadata)
|
|
65
|
-
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile }, "upload init"));
|
|
76
|
+
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile }, "upload init", verbose));
|
|
77
|
+
if (verbose)
|
|
78
|
+
console.log(` session: ${init.sessionID}`);
|
|
66
79
|
// Step 2: Upload files in concurrent batches
|
|
67
80
|
const fileURL = `${apiURL}/cliUpload/${init.sessionID}/file`;
|
|
68
81
|
const totalBatches = Math.ceil(filePaths.length / BATCH_SIZE);
|
|
@@ -70,11 +83,11 @@ export const uploadFiles = async (token, gameSlug, sha, filePaths, baseDir, puzz
|
|
|
70
83
|
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
71
84
|
const batch = filePaths.slice(i, i + BATCH_SIZE);
|
|
72
85
|
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
|
|
73
|
-
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir)));
|
|
86
|
+
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir, verbose)));
|
|
74
87
|
totalUploaded += batch.length;
|
|
75
88
|
onProgress?.(batchNum, totalBatches, totalUploaded);
|
|
76
89
|
}
|
|
77
90
|
// Step 3: Complete
|
|
78
|
-
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}, "upload complete"));
|
|
91
|
+
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}, "upload complete", verbose));
|
|
79
92
|
};
|
|
80
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": {
|
package/src/util/api.ts
CHANGED
|
@@ -22,39 +22,49 @@ 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
|
+
/** Options for uploadFiles */
|
|
26
|
+
export type UploadFilesOptions = {
|
|
27
|
+
verbose?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/** Wraps fetch to surface the underlying network cause (DNS, ECONNREFUSED, TLS, etc.) */
|
|
26
|
-
const fetchWithContext = async (url: string, init: RequestInit, step: string): Promise<Response> => {
|
|
31
|
+
const fetchWithContext = async (url: string, init: RequestInit, step: string, verbose: boolean): Promise<Response> => {
|
|
32
|
+
if (verbose) console.log(` → ${init.method ?? "GET"} ${url} [${step}]`)
|
|
27
33
|
try {
|
|
28
|
-
|
|
34
|
+
const res = await fetch(url, init)
|
|
35
|
+
if (verbose) console.log(` ← ${res.status} ${res.statusText} [${step}]`)
|
|
36
|
+
return res
|
|
29
37
|
} catch (e) {
|
|
30
38
|
const cause = (e as { cause?: unknown }).cause
|
|
31
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)
|
|
32
41
|
throw new Error(`Network error during ${step} (${url}): ${causeMsg}`)
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/** Reads a response body, parsing JSON when possible and including status + body in errors */
|
|
37
|
-
const readResponse = async (res: Response, url: string, step: string) => {
|
|
46
|
+
const readResponse = async (res: Response, url: string, step: string, verbose: boolean) => {
|
|
38
47
|
const text = await res.text()
|
|
39
48
|
let json: unknown
|
|
40
49
|
try {
|
|
41
50
|
json = text ? JSON.parse(text) : {}
|
|
42
51
|
} catch {
|
|
43
52
|
if (!res.ok) {
|
|
44
|
-
const snippet = text.slice(0, 200).replace(/\s+/g, " ").trim()
|
|
53
|
+
const snippet = verbose ? text : text.slice(0, 200).replace(/\s+/g, " ").trim()
|
|
45
54
|
throw new Error(`Server error during ${step} (${res.status} ${res.statusText} from ${url}): ${snippet || "no body"}`)
|
|
46
55
|
}
|
|
47
|
-
throw new Error(`Invalid JSON response during ${step} (${url}): ${text.slice(0, 200)}`)
|
|
56
|
+
throw new Error(`Invalid JSON response during ${step} (${url}): ${verbose ? text : text.slice(0, 200)}`)
|
|
48
57
|
}
|
|
49
58
|
if (!res.ok) {
|
|
50
59
|
const serverMsg = (json as { error?: string }).error || res.statusText || "Unknown error"
|
|
51
|
-
|
|
60
|
+
const detail = verbose ? `\n${JSON.stringify(json, null, 2)}` : ""
|
|
61
|
+
throw new Error(`Server error during ${step} (${res.status} from ${url}): ${serverMsg}${detail}`)
|
|
52
62
|
}
|
|
53
63
|
return json
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/** Sends a JSON POST request */
|
|
57
|
-
const jsonPost = async (url: string, token: string, body: object, step: string) => {
|
|
67
|
+
const jsonPost = async (url: string, token: string, body: object, step: string, verbose: boolean) => {
|
|
58
68
|
const res = await fetchWithContext(
|
|
59
69
|
url,
|
|
60
70
|
{
|
|
@@ -63,12 +73,13 @@ const jsonPost = async (url: string, token: string, body: object, step: string)
|
|
|
63
73
|
body: JSON.stringify(body),
|
|
64
74
|
},
|
|
65
75
|
step,
|
|
76
|
+
verbose,
|
|
66
77
|
)
|
|
67
|
-
return readResponse(res, url, step)
|
|
78
|
+
return readResponse(res, url, step, verbose)
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
/** Uploads a single file as raw binary with filename in query param */
|
|
71
|
-
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> => {
|
|
72
83
|
const relativePath = path.relative(baseDir, filePath)
|
|
73
84
|
const content = fs.readFileSync(filePath)
|
|
74
85
|
const step = `file upload (${relativePath})`
|
|
@@ -85,8 +96,9 @@ const uploadFile = async (url: string, token: string, filePath: string, baseDir:
|
|
|
85
96
|
body: content,
|
|
86
97
|
},
|
|
87
98
|
step,
|
|
99
|
+
verbose,
|
|
88
100
|
)
|
|
89
|
-
return (await readResponse(res, fullURL, step)) as FileResponse
|
|
101
|
+
return (await readResponse(res, fullURL, step, verbose)) as FileResponse
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
@@ -98,11 +110,15 @@ export const uploadFiles = async (
|
|
|
98
110
|
baseDir: string,
|
|
99
111
|
puzzmoFile: PuzzmoFile,
|
|
100
112
|
onProgress?: UploadProgress,
|
|
113
|
+
options: UploadFilesOptions = {},
|
|
101
114
|
): Promise<CompleteResponse> => {
|
|
115
|
+
const { verbose = false } = options
|
|
102
116
|
const apiURL = getAPIURL()
|
|
117
|
+
if (verbose) console.log(` API URL: ${apiURL}`)
|
|
103
118
|
|
|
104
119
|
// Step 1: Init session (includes puzzmo.json metadata)
|
|
105
|
-
const init = (await jsonPost(`${apiURL}/cliUpload`, token, { gameSlug, sha, puzzmoFile }, "upload init")) 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}`)
|
|
106
122
|
|
|
107
123
|
// Step 2: Upload files in concurrent batches
|
|
108
124
|
const fileURL = `${apiURL}/cliUpload/${init.sessionID}/file`
|
|
@@ -112,11 +128,11 @@ export const uploadFiles = async (
|
|
|
112
128
|
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
113
129
|
const batch = filePaths.slice(i, i + BATCH_SIZE)
|
|
114
130
|
const batchNum = Math.floor(i / BATCH_SIZE) + 1
|
|
115
|
-
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir)))
|
|
131
|
+
await Promise.all(batch.map((fp) => uploadFile(fileURL, token, fp, baseDir, verbose)))
|
|
116
132
|
totalUploaded += batch.length
|
|
117
133
|
onProgress?.(batchNum, totalBatches, totalUploaded)
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
// Step 3: Complete
|
|
121
|
-
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}, "upload complete")) as CompleteResponse
|
|
137
|
+
return (await jsonPost(`${apiURL}/cliUpload/${init.sessionID}/complete`, token, {}, "upload complete", verbose)) as CompleteResponse
|
|
122
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
|
+
}
|