@roster-lock/config-editor-cli 0.0.1
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/.claude-plugin/plugin.json +6 -0
- package/LICENSE.md +11 -0
- package/README.md +40 -0
- package/dist/commands/draft/from-lock.js +31 -0
- package/dist/commands/draft/from-lock.js.map +1 -0
- package/dist/commands/draft/index.js +22 -0
- package/dist/commands/draft/index.js.map +1 -0
- package/dist/commands/draft/init.js +25 -0
- package/dist/commands/draft/init.js.map +1 -0
- package/dist/commands/draft/promote.js +24 -0
- package/dist/commands/draft/promote.js.map +1 -0
- package/dist/commands/draft/publish.js +26 -0
- package/dist/commands/draft/publish.js.map +1 -0
- package/dist/commands/draft/validate.js +30 -0
- package/dist/commands/draft/validate.js.map +1 -0
- package/dist/commands/engine/add-asset.js +44 -0
- package/dist/commands/engine/add-asset.js.map +1 -0
- package/dist/commands/engine/add-piece-type.js +47 -0
- package/dist/commands/engine/add-piece-type.js.map +1 -0
- package/dist/commands/engine/edit-definition.js +49 -0
- package/dist/commands/engine/edit-definition.js.map +1 -0
- package/dist/commands/engine/index.js +24 -0
- package/dist/commands/engine/index.js.map +1 -0
- package/dist/commands/engine/remove-asset.js +27 -0
- package/dist/commands/engine/remove-asset.js.map +1 -0
- package/dist/commands/engine/remove-piece-type.js +28 -0
- package/dist/commands/engine/remove-piece-type.js.map +1 -0
- package/dist/commands/engine/set-info.js +22 -0
- package/dist/commands/engine/set-info.js.map +1 -0
- package/dist/commands/engine/show.js +23 -0
- package/dist/commands/engine/show.js.map +1 -0
- package/dist/commands/piece-meta/index.js +19 -0
- package/dist/commands/piece-meta/index.js.map +1 -0
- package/dist/commands/piece-meta/set.js +27 -0
- package/dist/commands/piece-meta/set.js.map +1 -0
- package/dist/commands/piece-meta/show.js +15 -0
- package/dist/commands/piece-meta/show.js.map +1 -0
- package/dist/commands/roster/add-piece.js +93 -0
- package/dist/commands/roster/add-piece.js.map +1 -0
- package/dist/commands/roster/edit-piece.js +92 -0
- package/dist/commands/roster/edit-piece.js.map +1 -0
- package/dist/commands/roster/import-piece.js +83 -0
- package/dist/commands/roster/import-piece.js.map +1 -0
- package/dist/commands/roster/index.js +24 -0
- package/dist/commands/roster/index.js.map +1 -0
- package/dist/commands/roster/list.js +24 -0
- package/dist/commands/roster/list.js.map +1 -0
- package/dist/commands/roster/remove-piece.js +30 -0
- package/dist/commands/roster/remove-piece.js.map +1 -0
- package/dist/commands/roster/rescan.js +55 -0
- package/dist/commands/roster/rescan.js.map +1 -0
- package/dist/commands/roster/test-download.js +72 -0
- package/dist/commands/roster/test-download.js.map +1 -0
- package/dist/commands/selection/add-script.js +43 -0
- package/dist/commands/selection/add-script.js.map +1 -0
- package/dist/commands/selection/index.js +21 -0
- package/dist/commands/selection/index.js.map +1 -0
- package/dist/commands/selection/run-script.js +27 -0
- package/dist/commands/selection/run-script.js.map +1 -0
- package/dist/commands/selection/set.js +41 -0
- package/dist/commands/selection/set.js.map +1 -0
- package/dist/commands/selection/show.js +15 -0
- package/dist/commands/selection/show.js.map +1 -0
- package/dist/commands/show.js +15 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/validate.js +31 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cli-options.js +7 -0
- package/dist/lib/cli-options.js.map +1 -0
- package/dist/lib/draft-io.js +38 -0
- package/dist/lib/draft-io.js.map +1 -0
- package/dist/lib/errors.js +32 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/fs-walk.js +28 -0
- package/dist/lib/fs-walk.js.map +1 -0
- package/dist/lib/json-input.js +16 -0
- package/dist/lib/json-input.js.map +1 -0
- package/dist/lib/lock-io.js +20 -0
- package/dist/lib/lock-io.js.map +1 -0
- package/dist/lib/parse.js +52 -0
- package/dist/lib/parse.js.map +1 -0
- package/dist/lib/piece-scan.js +12 -0
- package/dist/lib/piece-scan.js.map +1 -0
- package/dist/lib/schema-help.js +38 -0
- package/dist/lib/schema-help.js.map +1 -0
- package/dist/lib/schemas.js +71 -0
- package/dist/lib/schemas.js.map +1 -0
- package/dist/lib/semver-report.js +13 -0
- package/dist/lib/semver-report.js.map +1 -0
- package/dist/lib/slugify.js +13 -0
- package/dist/lib/slugify.js.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -0
- package/scripts/bundle.js +15 -0
- package/scripts/generate-version.js +14 -0
- package/skills/rosterlock-config-editor/SKILL.md +151 -0
- package/src/commands/draft/from-lock.ts +34 -0
- package/src/commands/draft/index.ts +5 -0
- package/src/commands/draft/init.ts +21 -0
- package/src/commands/draft/promote.ts +24 -0
- package/src/commands/draft/publish.ts +26 -0
- package/src/commands/draft/validate.ts +30 -0
- package/src/commands/engine/add-asset.ts +46 -0
- package/src/commands/engine/add-piece-type.ts +56 -0
- package/src/commands/engine/edit-definition.ts +58 -0
- package/src/commands/engine/index.ts +7 -0
- package/src/commands/engine/remove-asset.ts +25 -0
- package/src/commands/engine/remove-piece-type.ts +27 -0
- package/src/commands/engine/set-info.ts +17 -0
- package/src/commands/engine/show.ts +20 -0
- package/src/commands/piece-meta/index.ts +2 -0
- package/src/commands/piece-meta/set.ts +29 -0
- package/src/commands/piece-meta/show.ts +12 -0
- package/src/commands/roster/add-piece.ts +109 -0
- package/src/commands/roster/edit-piece.ts +99 -0
- package/src/commands/roster/import-piece.ts +112 -0
- package/src/commands/roster/index.ts +7 -0
- package/src/commands/roster/list.ts +23 -0
- package/src/commands/roster/remove-piece.ts +28 -0
- package/src/commands/roster/rescan.ts +63 -0
- package/src/commands/roster/test-download.ts +76 -0
- package/src/commands/selection/add-script.ts +44 -0
- package/src/commands/selection/index.ts +4 -0
- package/src/commands/selection/run-script.ts +30 -0
- package/src/commands/selection/set.ts +54 -0
- package/src/commands/selection/show.ts +12 -0
- package/src/commands/show.ts +12 -0
- package/src/commands/validate.ts +26 -0
- package/src/index.ts +85 -0
- package/src/lib/cli-options.ts +8 -0
- package/src/lib/draft-io.ts +41 -0
- package/src/lib/errors.ts +30 -0
- package/src/lib/fs-walk.ts +31 -0
- package/src/lib/json-input.ts +21 -0
- package/src/lib/lock-io.ts +18 -0
- package/src/lib/parse.ts +42 -0
- package/src/lib/piece-scan.ts +19 -0
- package/src/lib/schema-help.ts +44 -0
- package/src/lib/schemas.ts +100 -0
- package/src/lib/semver-report.ts +11 -0
- package/src/lib/slugify.ts +9 -0
- package/test.rosterlock.draft.json +62 -0
- package/tests/commands/add-piece.spec.ts +92 -0
- package/tests/commands/draft-validate.spec.ts +85 -0
- package/tests/commands/edit-definition.spec.ts +94 -0
- package/tests/commands/edit-piece.spec.ts +153 -0
- package/tests/commands/from-lock.spec.ts +80 -0
- package/tests/commands/help-arguments.spec.ts +31 -0
- package/tests/commands/import-piece.spec.ts +228 -0
- package/tests/commands/piece-meta.spec.ts +123 -0
- package/tests/commands/rescan.spec.ts +137 -0
- package/tests/lib/draft-io.spec.ts +76 -0
- package/tests/lib/errors.spec.ts +48 -0
- package/tests/lib/fs-walk.spec.ts +35 -0
- package/tests/lib/json-input.spec.ts +63 -0
- package/tests/lib/parse.spec.ts +85 -0
- package/tests/lib/schema-help.spec.ts +42 -0
- package/tests/lib/schemas.spec.ts +82 -0
- package/tests/lib/slugify.spec.ts +28 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +10 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ErrorObject } from "ajv";
|
|
3
|
+
import { formatError, withErrorHandling } from "../../src/lib/errors";
|
|
4
|
+
|
|
5
|
+
describe("formatError", () => {
|
|
6
|
+
it("formats a plain Error by its message", () => {
|
|
7
|
+
expect(formatError(new Error("boom"))).toBe("boom");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("formats an array of AJV ErrorObjects, one per line", () => {
|
|
11
|
+
const errors: Array<ErrorObject> = [
|
|
12
|
+
{ instancePath: "/foo", schemaPath: "", keyword: "type", message: "must be string", params: {} },
|
|
13
|
+
{ instancePath: "", schemaPath: "", keyword: "required", message: "must have required property 'bar'", params: {} },
|
|
14
|
+
];
|
|
15
|
+
expect(formatError(errors)).toBe(
|
|
16
|
+
"/foo must be string\n/ must have required property 'bar'"
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("stringifies anything else", () => {
|
|
21
|
+
expect(formatError("plain string")).toBe("plain string");
|
|
22
|
+
expect(formatError(42)).toBe("42");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("withErrorHandling", () => {
|
|
27
|
+
it("runs the wrapped function and does not touch exitCode on success", async () => {
|
|
28
|
+
process.exitCode = undefined;
|
|
29
|
+
const fn = vi.fn(async (x: number) => { void x; });
|
|
30
|
+
await withErrorHandling(fn)(1);
|
|
31
|
+
expect(fn).toHaveBeenCalledWith(1);
|
|
32
|
+
expect(process.exitCode).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("logs the formatted error and sets exitCode to 1 on failure", async () => {
|
|
36
|
+
process.exitCode = undefined;
|
|
37
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
38
|
+
const fn = async () => { throw new Error("nope"); };
|
|
39
|
+
|
|
40
|
+
await withErrorHandling(fn)();
|
|
41
|
+
|
|
42
|
+
expect(errorSpy).toHaveBeenCalledWith("nope");
|
|
43
|
+
expect(process.exitCode).toBe(1);
|
|
44
|
+
|
|
45
|
+
errorSpy.mockRestore();
|
|
46
|
+
process.exitCode = undefined;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join as pathJoin } from "node:path";
|
|
5
|
+
import { walkRelative, getFileFromRoot } from "../../src/lib/fs-walk";
|
|
6
|
+
|
|
7
|
+
describe("fs-walk", () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = mkdtempSync(pathJoin(tmpdir(), "rosterlock-fs-walk-"));
|
|
12
|
+
mkdirSync(pathJoin(tempDir, "nested"));
|
|
13
|
+
writeFileSync(pathJoin(tempDir, "top.txt"), "top");
|
|
14
|
+
writeFileSync(pathJoin(tempDir, "nested", "inner.txt"), "inner");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("yields all files recursively as forward-slash relative paths", async () => {
|
|
22
|
+
const paths: Array<string> = [];
|
|
23
|
+
for await (const p of walkRelative(tempDir)) paths.push(p);
|
|
24
|
+
expect(paths.sort()).toEqual(["nested/inner.txt", "top.txt"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("resolves file size and a readable stream from root", async () => {
|
|
28
|
+
const { byteSize, stream } = await getFileFromRoot(tempDir, "top.txt");
|
|
29
|
+
expect(byteSize).toBe(3);
|
|
30
|
+
|
|
31
|
+
const chunks: Array<Buffer> = [];
|
|
32
|
+
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
33
|
+
expect(Buffer.concat(chunks).toString("utf-8")).toBe("top");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join as pathJoin } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { readJsonInput } from "../../src/lib/json-input";
|
|
8
|
+
|
|
9
|
+
const fooSchema = z.object({ foo: z.string() }).strict();
|
|
10
|
+
|
|
11
|
+
describe("readJsonInput", () => {
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tempDir = mkdtempSync(pathJoin(tmpdir(), "rosterlock-json-input-"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("reads, parses, and validates JSON from a file path", async () => {
|
|
23
|
+
const filePath = pathJoin(tempDir, "data.json");
|
|
24
|
+
writeFileSync(filePath, JSON.stringify({ foo: "bar" }));
|
|
25
|
+
|
|
26
|
+
await expect(readJsonInput(filePath, fooSchema)).resolves.toEqual({ foo: "bar" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("collects, parses, and validates JSON from stdin when given \"-\"", async () => {
|
|
30
|
+
const originalStdin = process.stdin;
|
|
31
|
+
Object.defineProperty(process, "stdin", {
|
|
32
|
+
value: Readable.from([JSON.stringify({ foo: "stdin" })]),
|
|
33
|
+
configurable: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await expect(readJsonInput("-", fooSchema)).resolves.toEqual({ foo: "stdin" });
|
|
38
|
+
} finally {
|
|
39
|
+
Object.defineProperty(process, "stdin", { value: originalStdin, configurable: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("throws on invalid JSON", async () => {
|
|
44
|
+
const filePath = pathJoin(tempDir, "bad.json");
|
|
45
|
+
writeFileSync(filePath, "{not valid json");
|
|
46
|
+
|
|
47
|
+
await expect(readJsonInput(filePath, fooSchema)).rejects.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("throws a ZodError when the JSON doesn't match the schema", async () => {
|
|
51
|
+
const filePath = pathJoin(tempDir, "wrong-shape.json");
|
|
52
|
+
writeFileSync(filePath, JSON.stringify({ foo: 42 }));
|
|
53
|
+
|
|
54
|
+
await expect(readJsonInput(filePath, fooSchema)).rejects.toThrow(z.ZodError);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("rejects unknown properties (schemas are declared .strict())", async () => {
|
|
58
|
+
const filePath = pathJoin(tempDir, "extra-props.json");
|
|
59
|
+
writeFileSync(filePath, JSON.stringify({ foo: "bar", extra: true }));
|
|
60
|
+
|
|
61
|
+
await expect(readJsonInput(filePath, fooSchema)).rejects.toThrow(z.ZodError);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseKeyValueList, parseCommaList, parseCount, parseStrategy } from "../../src/lib/parse";
|
|
3
|
+
|
|
4
|
+
describe("parseKeyValueList", () => {
|
|
5
|
+
it("returns an empty object for undefined", () => {
|
|
6
|
+
expect(parseKeyValueList(undefined)).toEqual({});
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("parses comma-separated key=value pairs", () => {
|
|
10
|
+
expect(parseKeyValueList("a=1,b=2")).toEqual({ a: "1", b: "2" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("trims whitespace around pairs", () => {
|
|
14
|
+
expect(parseKeyValueList(" a=1 , b=2 ")).toEqual({ a: "1", b: "2" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("skips empty segments", () => {
|
|
18
|
+
expect(parseKeyValueList("a=1,,b=2")).toEqual({ a: "1", b: "2" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("allows '=' inside the value", () => {
|
|
22
|
+
expect(parseKeyValueList("a=1=2")).toEqual({ a: "1=2" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("throws on a segment with no '='", () => {
|
|
26
|
+
expect(() => parseKeyValueList("a=1,bad")).toThrow(/Invalid key=value pair/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("parseCommaList", () => {
|
|
31
|
+
it("returns an empty array for undefined", () => {
|
|
32
|
+
expect(parseCommaList(undefined)).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("splits and trims", () => {
|
|
36
|
+
expect(parseCommaList(" a, b ,c")).toEqual(["a", "b", "c"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("drops empty entries", () => {
|
|
40
|
+
expect(parseCommaList("a,,b,")).toEqual(["a", "b"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("parseCount", () => {
|
|
45
|
+
it("parses '*' as-is", () => {
|
|
46
|
+
expect(parseCount("*")).toBe("*");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("parses a plain integer", () => {
|
|
50
|
+
expect(parseCount("3")).toBe(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("throws on a non-numeric plain value", () => {
|
|
54
|
+
expect(() => parseCount("abc")).toThrow(/Invalid count/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("parses a min:max range", () => {
|
|
58
|
+
expect(parseCount("1:5")).toEqual([1, 5]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("parses a min:* range", () => {
|
|
62
|
+
expect(parseCount("1:*")).toEqual([1, "*"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("throws on an invalid range", () => {
|
|
66
|
+
expect(() => parseCount("a:5")).toThrow(/Invalid count range/);
|
|
67
|
+
expect(() => parseCount("1:b")).toThrow(/Invalid count range/);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("parseStrategy", () => {
|
|
72
|
+
it("passes through valid strategies", () => {
|
|
73
|
+
expect(parseStrategy("mandatory")).toBe("mandatory");
|
|
74
|
+
expect(parseStrategy("personal")).toBe("personal");
|
|
75
|
+
expect(parseStrategy("shared")).toBe("shared");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("normalizes 'on-demand' to 'on demand'", () => {
|
|
79
|
+
expect(parseStrategy("on-demand")).toBe("on demand");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("throws on an unknown strategy", () => {
|
|
83
|
+
expect(() => parseStrategy("bogus")).toThrow(/Invalid strategy/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { describeSchemaShape } from "../../src/lib/schema-help";
|
|
4
|
+
|
|
5
|
+
describe("describeSchemaShape", () => {
|
|
6
|
+
it("renders primitives and objects", () => {
|
|
7
|
+
const schema = z.object({ name: z.string(), age: z.number(), active: z.boolean() }).strict();
|
|
8
|
+
expect(describeSchemaShape(schema)).toBe("{ name: string, age: number, active: boolean }");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("marks optional fields with a trailing ?", () => {
|
|
12
|
+
const schema = z.object({ name: z.string(), nickname: z.string().optional() }).strict();
|
|
13
|
+
expect(describeSchemaShape(schema)).toBe("{ name: string, nickname?: string }");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders arrays, tuples, unions, enums, and literals", () => {
|
|
17
|
+
const schema = z.object({
|
|
18
|
+
tags: z.array(z.string()),
|
|
19
|
+
pair: z.tuple([z.number(), z.string()]),
|
|
20
|
+
mode: z.union([z.string(), z.number()]),
|
|
21
|
+
kind: z.enum(["a", "b"]),
|
|
22
|
+
star: z.literal("*"),
|
|
23
|
+
}).strict();
|
|
24
|
+
|
|
25
|
+
expect(describeSchemaShape(schema)).toBe(
|
|
26
|
+
'{ tags: string[], pair: [number, string], mode: string | number, kind: "a" | "b", star: "*" }'
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders records", () => {
|
|
31
|
+
const schema = z.record(z.string(), z.number());
|
|
32
|
+
expect(describeSchemaShape(schema)).toBe("Record<string, number>");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders a placeholder for recursive (lazy) schemas instead of infinitely recursing", () => {
|
|
36
|
+
type Node = { children: Array<Node> };
|
|
37
|
+
const nodeSchema: z.ZodType<Node> = z.lazy(() => z.object({ children: z.array(nodeSchema) }).strict());
|
|
38
|
+
const schema = z.object({ root: nodeSchema }).strict();
|
|
39
|
+
|
|
40
|
+
expect(describeSchemaShape(schema)).toBe("{ root: <recursive> }");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { pieceDefinitionSchema, pieceOverridesSchema, selectionOverridesSchemas } from "../../src/lib/schemas";
|
|
3
|
+
|
|
4
|
+
describe("pieceDefinitionSchema", () => {
|
|
5
|
+
const valid = {
|
|
6
|
+
selectionStrategy: "mandatory",
|
|
7
|
+
requires: [],
|
|
8
|
+
pathVariables: ["skin"],
|
|
9
|
+
assets: [{ name: "sprite", classification: "media", count: 1, glob: ["*.png"] }],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
it("accepts a well-formed piece definition", () => {
|
|
13
|
+
expect(pieceDefinitionSchema.parse(valid)).toEqual(valid);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("accepts count as '*' or a [min, max] range", () => {
|
|
17
|
+
expect(() => pieceDefinitionSchema.parse({ ...valid, assets: [{ ...valid.assets[0], count: "*" }] })).not.toThrow();
|
|
18
|
+
expect(() => pieceDefinitionSchema.parse({ ...valid, assets: [{ ...valid.assets[0], count: [1, "*"] }] })).not.toThrow();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects an invalid selectionStrategy", () => {
|
|
22
|
+
expect(() => pieceDefinitionSchema.parse({ ...valid, selectionStrategy: "bogus" })).toThrow();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects unknown top-level properties", () => {
|
|
26
|
+
expect(() => pieceDefinitionSchema.parse({ ...valid, extra: true })).toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rejects an asset missing required fields", () => {
|
|
30
|
+
expect(() => pieceDefinitionSchema.parse({ ...valid, assets: [{ name: "sprite" }] })).toThrow();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("pieceOverridesSchema", () => {
|
|
35
|
+
it("accepts a fully-populated overrides object", () => {
|
|
36
|
+
const input = {
|
|
37
|
+
humanInfo: { name: "Blue", author: "author", url: "https://example.com", image: "https://example.com/i.png" },
|
|
38
|
+
downloadSources: ["https://example.com/blue.tar"],
|
|
39
|
+
pathVariables: { skin: "blue" },
|
|
40
|
+
};
|
|
41
|
+
expect(pieceOverridesSchema.parse(input)).toEqual(input);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("accepts an empty object (all fields optional)", () => {
|
|
45
|
+
expect(pieceOverridesSchema.parse({})).toEqual({});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects humanInfo missing a required field", () => {
|
|
49
|
+
expect(() => pieceOverridesSchema.parse({ humanInfo: { name: "Blue" } })).toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects non-string pathVariables values", () => {
|
|
53
|
+
expect(() => pieceOverridesSchema.parse({ pathVariables: { skin: 1 } })).toThrow();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("selectionOverridesSchemas", () => {
|
|
58
|
+
it("normal accepts validation and mergeAlgorithm", () => {
|
|
59
|
+
const input = {
|
|
60
|
+
validation: { count: "*", unique: false, banList: [], customValidation: [] },
|
|
61
|
+
mergeAlgorithm: { src: "merge.ts" },
|
|
62
|
+
};
|
|
63
|
+
expect(() => selectionOverridesSchemas.normal.parse(input)).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("normal rejects a 'pieces' field (that's preselected-only)", () => {
|
|
67
|
+
expect(() => selectionOverridesSchemas.normal.parse({ pieces: [] })).toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("preselected accepts a pieces list, including nested required pieces", () => {
|
|
71
|
+
const input = {
|
|
72
|
+
pieces: [{ id: "@a/b", required: { minion: { mandatory: [], selectable: [] } } }],
|
|
73
|
+
};
|
|
74
|
+
expect(() => selectionOverridesSchemas.preselected.parse(input)).not.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("unselectable and game-controlled accept no override fields", () => {
|
|
78
|
+
expect(() => selectionOverridesSchemas.unselectable.parse({})).not.toThrow();
|
|
79
|
+
expect(() => selectionOverridesSchemas.unselectable.parse({ pieces: [] })).toThrow();
|
|
80
|
+
expect(() => selectionOverridesSchemas["game-controlled"].parse({})).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { slugify } from "../../src/lib/slugify";
|
|
3
|
+
|
|
4
|
+
describe("slugify", () => {
|
|
5
|
+
it("lowercases and replaces spaces with dashes", () => {
|
|
6
|
+
expect(slugify("Hello World")).toBe("hello-world");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("replaces underscores with dashes", () => {
|
|
10
|
+
expect(slugify("hello_world")).toBe("hello-world");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("strips characters outside a-z0-9-", () => {
|
|
14
|
+
expect(slugify("Hello, World! 2.0")).toBe("hello-world-20");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("trims leading/trailing dashes", () => {
|
|
18
|
+
expect(slugify(" --hello-- ")).toBe("hello");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("collapses repeated dashes", () => {
|
|
22
|
+
expect(slugify("a---b")).toBe("a-b");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns an empty string for input with no valid characters", () => {
|
|
26
|
+
expect(slugify("!!!")).toBe("");
|
|
27
|
+
});
|
|
28
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"lib": ["es2024"],
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"removeComments": true,
|
|
15
|
+
"noImplicitAny": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "./typings", "./types", "./@types"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
19
|
+
}
|