jgengine 0.8.0 → 0.8.2
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/dist/cli/index.js +19 -22
- package/dist/create.d.ts +1 -1
- package/dist/create.js +99 -60
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +67 -0
- package/dist/templates.d.ts +9 -0
- package/dist/templates.js +68 -11
- package/llms.txt +22 -6
- package/package.json +12 -2
package/dist/cli/index.js
CHANGED
|
@@ -7,23 +7,28 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { runCreate } from "../create.js";
|
|
8
8
|
import { runDoctor } from "../doctor.js";
|
|
9
9
|
import { cliVersion, findUp, readPackageJson } from "../pkg.js";
|
|
10
|
+
import { runSkills } from "../skills.js";
|
|
10
11
|
const ENGINE_PACKAGES = ["core", "react", "shell", "ws", "sql", "convex", "node", "assets"];
|
|
11
|
-
const HELP = `jgengine ${cliVersion()} —
|
|
12
|
-
|
|
12
|
+
const HELP = `jgengine ${cliVersion()} — pure-TypeScript game engine SDK + agent entry
|
|
13
|
+
Packages: ${ENGINE_PACKAGES.map((name) => `@jgengine/${name}`).join(", ")}.
|
|
14
|
+
docs: https://jgengine.com · source: https://github.com/Noisemaker111/jgengine
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
ENTRY (agents: run this when the user says "use jgengine" / "make a game with npx jgengine"):
|
|
17
|
+
npx jgengine create "Game Name"
|
|
18
|
+
cd Game-Name
|
|
19
|
+
# skills install automatically · AGENTS.md briefs you · then build the game
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
[--name <display name>] [--in-repo|--standalone] [--no-install] [--pm bun|npm|pnpm]
|
|
18
|
-
doctor [dir] diagnose a game project: version skew, missing peers, unstyled-UI @source gaps, shape drift
|
|
19
|
-
skills install the JGengine agent skills (API reference, game-build workflow, verify gate) into .claude/skills
|
|
20
|
-
llms [package] print packaged API docs (llms.txt) for an installed @jgengine/* package — agent-ready context
|
|
21
|
-
assets [...] delegate to the @jgengine/assets CLI: list, search, pull CC0 3D model packs
|
|
22
|
-
versions show CLI + installed @jgengine/* versions
|
|
23
|
-
help this map
|
|
21
|
+
usage: jgengine <command> [...args]
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
create "<Game Name>" scaffold playable base + install agent skills into the project
|
|
24
|
+
folder My-Game-Name; name → game.config / HUD / title
|
|
25
|
+
[--in-repo|--standalone] [--no-install] [--no-skills] [--pm bun|npm|pnpm]
|
|
26
|
+
skills -p | -g re-install skills only if create was run with --no-skills
|
|
27
|
+
doctor [dir] diagnose version skew, missing peers, unstyled HUD, shape drift
|
|
28
|
+
llms [package] print packaged API docs (llms.txt) — agent-ready
|
|
29
|
+
assets [...] @jgengine/assets CLI: list, search, pull CC0 packs
|
|
30
|
+
versions CLI + installed @jgengine/* versions
|
|
31
|
+
help this map
|
|
27
32
|
`;
|
|
28
33
|
function runVersions() {
|
|
29
34
|
console.log(`jgengine ${cliVersion()}`);
|
|
@@ -61,14 +66,6 @@ function runLlms(argv) {
|
|
|
61
66
|
process.stdout.write(readFileSync(join(found, "node_modules", packageName, "llms.txt"), "utf8"));
|
|
62
67
|
return 0;
|
|
63
68
|
}
|
|
64
|
-
function runSkills() {
|
|
65
|
-
console.log("installing JGengine agent skills (jgengine-api, jgengine-newgame, jgengine-verify)…");
|
|
66
|
-
const result = spawnSync("npx", ["--yes", "skills", "add", "Noisemaker111/jgengine"], {
|
|
67
|
-
stdio: "inherit",
|
|
68
|
-
shell: process.platform === "win32",
|
|
69
|
-
});
|
|
70
|
-
return result.status ?? 1;
|
|
71
|
-
}
|
|
72
69
|
function runAssets(argv) {
|
|
73
70
|
const require = createRequire(import.meta.url);
|
|
74
71
|
let cliPath;
|
|
@@ -91,7 +88,7 @@ switch (command) {
|
|
|
91
88
|
process.exit(runDoctor(rest));
|
|
92
89
|
break;
|
|
93
90
|
case "skills":
|
|
94
|
-
process.exit(runSkills());
|
|
91
|
+
process.exit(runSkills(rest));
|
|
95
92
|
break;
|
|
96
93
|
case "llms":
|
|
97
94
|
process.exit(runLlms(rest));
|
package/dist/create.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type TemplateVariant } from "./templates.js";
|
|
2
2
|
export declare function writeGame(targetDir: string, id: string, name: string, variant: TemplateVariant): void;
|
|
3
|
-
export declare function registerRootGameScript(rootDir: string, id: string): boolean;
|
|
3
|
+
export declare function registerRootGameScript(rootDir: string, id: string, folderName?: string): boolean;
|
|
4
4
|
export declare function runCreate(argv: string[]): number;
|
package/dist/create.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { cliVersion, findWorkspaceRoot, flag, hasFlag, isEngineMonorepo } from "./pkg.js";
|
|
5
|
-
import {
|
|
5
|
+
import { installSkills } from "./skills.js";
|
|
6
|
+
import { gameTemplate, parseCreateName } from "./templates.js";
|
|
6
7
|
export function writeGame(targetDir, id, name, variant) {
|
|
7
8
|
for (const file of gameTemplate({ id, name, variant, engineVersion: cliVersion() })) {
|
|
8
9
|
const dest = join(targetDir, file.path);
|
|
@@ -10,7 +11,7 @@ export function writeGame(targetDir, id, name, variant) {
|
|
|
10
11
|
writeFileSync(dest, file.contents);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
|
-
export function registerRootGameScript(rootDir, id) {
|
|
14
|
+
export function registerRootGameScript(rootDir, id, folderName = id) {
|
|
14
15
|
const rootPackagePath = join(rootDir, "package.json");
|
|
15
16
|
const root = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
|
16
17
|
const scripts = root.scripts ?? {};
|
|
@@ -20,7 +21,7 @@ export function registerRootGameScript(rootDir, id) {
|
|
|
20
21
|
const entries = Object.entries(scripts);
|
|
21
22
|
const games = entries.filter(([k]) => k.startsWith("games:"));
|
|
22
23
|
const rest = entries.filter(([k]) => !k.startsWith("games:"));
|
|
23
|
-
games.push([key, `bun run --cwd Games/${
|
|
24
|
+
games.push([key, `bun run --cwd Games/${folderName} dev`]);
|
|
24
25
|
games.sort(([a], [b]) => a.localeCompare(b));
|
|
25
26
|
root.scripts = Object.fromEntries([...rest, ...games]);
|
|
26
27
|
writeFileSync(rootPackagePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
@@ -32,7 +33,7 @@ function pickPackageManager(preferred) {
|
|
|
32
33
|
const probe = spawnSync("bun", ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
|
|
33
34
|
return probe.status === 0 ? "bun" : "npm";
|
|
34
35
|
}
|
|
35
|
-
const VALUE_FLAGS = new Set(["--
|
|
36
|
+
const VALUE_FLAGS = new Set(["--pm"]);
|
|
36
37
|
function positionalArg(argv) {
|
|
37
38
|
for (let index = 0; index < argv.length; index += 1) {
|
|
38
39
|
const arg = argv[index];
|
|
@@ -45,67 +46,105 @@ function positionalArg(argv) {
|
|
|
45
46
|
}
|
|
46
47
|
return undefined;
|
|
47
48
|
}
|
|
49
|
+
function splitCreateArg(arg) {
|
|
50
|
+
const normalized = arg.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
51
|
+
const trimmed = normalized.replace(/\/$/, "");
|
|
52
|
+
const slash = trimmed.lastIndexOf("/");
|
|
53
|
+
if (slash < 0)
|
|
54
|
+
return { parentHint: null, titlePart: trimmed };
|
|
55
|
+
return {
|
|
56
|
+
parentHint: trimmed.slice(0, slash) || ".",
|
|
57
|
+
titlePart: trimmed.slice(slash + 1),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function isInsideDir(child, parent) {
|
|
61
|
+
const resolvedChild = resolve(child);
|
|
62
|
+
const resolvedParent = resolve(parent);
|
|
63
|
+
return resolvedChild === resolvedParent || resolvedChild.startsWith(resolvedParent + sep);
|
|
64
|
+
}
|
|
48
65
|
export function runCreate(argv) {
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
console.error(
|
|
52
|
-
return 1;
|
|
53
|
-
}
|
|
54
|
-
const targetDir = resolve(dirArg);
|
|
55
|
-
const id = basename(targetDir);
|
|
56
|
-
if (!GAME_ID_PATTERN.test(id)) {
|
|
57
|
-
console.error(`error: directory name "${id}" must be kebab-case (lowercase letters, digits, dashes, starting with a letter)`);
|
|
58
|
-
return 1;
|
|
59
|
-
}
|
|
60
|
-
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
61
|
-
console.error(`error: ${targetDir} already exists and is not empty`);
|
|
62
|
-
return 1;
|
|
63
|
-
}
|
|
64
|
-
const workspaceRoot = findWorkspaceRoot(dirname(targetDir));
|
|
65
|
-
const insideEngineRepo = workspaceRoot !== null && isEngineMonorepo(workspaceRoot);
|
|
66
|
-
const variant = hasFlag(argv, "standalone")
|
|
67
|
-
? "standalone"
|
|
68
|
-
: hasFlag(argv, "in-repo") || insideEngineRepo
|
|
69
|
-
? "in-repo"
|
|
70
|
-
: "standalone";
|
|
71
|
-
if (variant === "in-repo" && !insideEngineRepo) {
|
|
72
|
-
console.error("error: --in-repo requires the target to live under Games/ inside the jgengine engine monorepo");
|
|
66
|
+
const nameArg = positionalArg(argv);
|
|
67
|
+
if (nameArg === undefined) {
|
|
68
|
+
console.error('usage: jgengine create "<Game Name>" [--in-repo|--standalone] [--no-install] [--no-skills] [--pm bun|npm|pnpm]');
|
|
73
69
|
return 1;
|
|
74
70
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
let displayName;
|
|
72
|
+
let folderName;
|
|
73
|
+
let id;
|
|
74
|
+
try {
|
|
75
|
+
const { parentHint, titlePart } = splitCreateArg(nameArg);
|
|
76
|
+
({ displayName, folderName, id } = parseCreateName(titlePart));
|
|
77
|
+
let parentDir = parentHint !== null ? resolve(parentHint) : process.cwd();
|
|
78
|
+
let targetDir = join(parentDir, folderName);
|
|
79
|
+
const workspaceRoot = findWorkspaceRoot(dirname(targetDir)) ?? findWorkspaceRoot(parentDir) ?? findWorkspaceRoot(process.cwd());
|
|
80
|
+
const insideEngineRepo = workspaceRoot !== null && isEngineMonorepo(workspaceRoot);
|
|
81
|
+
const variant = hasFlag(argv, "standalone")
|
|
82
|
+
? "standalone"
|
|
83
|
+
: hasFlag(argv, "in-repo") || insideEngineRepo
|
|
84
|
+
? "in-repo"
|
|
85
|
+
: "standalone";
|
|
86
|
+
if (variant === "in-repo") {
|
|
87
|
+
if (!insideEngineRepo || workspaceRoot === null) {
|
|
88
|
+
console.error("error: --in-repo requires the jgengine engine monorepo (packages/core + Games/)");
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (parentHint === null) {
|
|
92
|
+
parentDir = join(workspaceRoot, "Games");
|
|
93
|
+
targetDir = join(parentDir, folderName);
|
|
94
|
+
}
|
|
95
|
+
const gamesDir = join(workspaceRoot, "Games");
|
|
96
|
+
if (!isInsideDir(targetDir, gamesDir)) {
|
|
97
|
+
console.error(`error: in-repo games must live under Games/ (got ${targetDir}; expected under ${gamesDir})`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
81
100
|
}
|
|
101
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
102
|
+
console.error(`error: ${targetDir} already exists and is not empty`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
writeGame(targetDir, id, displayName, variant);
|
|
106
|
+
console.log(`created ${displayName} (${variant}) → ${targetDir}`);
|
|
107
|
+
console.log(` folder ${folderName} package ${id} name "${displayName}"`);
|
|
108
|
+
if (variant === "in-repo" && workspaceRoot !== null) {
|
|
109
|
+
if (registerRootGameScript(workspaceRoot, id, folderName)) {
|
|
110
|
+
console.log(`registered root script "games:${id}" in ${join(workspaceRoot, "package.json")}`);
|
|
111
|
+
}
|
|
112
|
+
console.log("\nnext steps:");
|
|
113
|
+
console.log(` bun install # from ${workspaceRoot}`);
|
|
114
|
+
console.log(` bun run games:${id} # play it standalone`);
|
|
115
|
+
console.log(` # agent: open the project and build ${displayName} (skills live in the monorepo)`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
let installed = false;
|
|
119
|
+
if (!hasFlag(argv, "no-install")) {
|
|
120
|
+
const pm = pickPackageManager(flag(argv, "pm"));
|
|
121
|
+
console.log(`installing dependencies with ${pm}…`);
|
|
122
|
+
const install = spawnSync(pm, ["install"], {
|
|
123
|
+
cwd: targetDir,
|
|
124
|
+
stdio: "inherit",
|
|
125
|
+
shell: process.platform === "win32",
|
|
126
|
+
});
|
|
127
|
+
installed = install.status === 0;
|
|
128
|
+
if (!installed)
|
|
129
|
+
console.error(`warning: ${pm} install failed — run it manually in ${targetDir}`);
|
|
130
|
+
}
|
|
131
|
+
if (!hasFlag(argv, "no-skills")) {
|
|
132
|
+
const skillsStatus = installSkills("project", targetDir);
|
|
133
|
+
if (skillsStatus !== 0) {
|
|
134
|
+
console.error("warning: skill install failed — agent can still use AGENTS.md; retry: npx jgengine skills -p");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const cdHint = relative(process.cwd(), targetDir) || ".";
|
|
82
138
|
console.log("\nnext steps:");
|
|
83
|
-
console.log(`
|
|
84
|
-
|
|
139
|
+
console.log(` cd ${cdHint}`);
|
|
140
|
+
if (!installed)
|
|
141
|
+
console.log(" bun install # or npm install");
|
|
142
|
+
console.log(" bun dev # playable base (flat world, player, HUD)");
|
|
143
|
+
console.log(` # agent: make ${displayName} with jgengine (skills already in the project)`);
|
|
85
144
|
return 0;
|
|
86
145
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.log(`installing dependencies with ${pm}…`);
|
|
91
|
-
const install = spawnSync(pm, ["install"], {
|
|
92
|
-
cwd: targetDir,
|
|
93
|
-
stdio: "inherit",
|
|
94
|
-
shell: process.platform === "win32",
|
|
95
|
-
});
|
|
96
|
-
installed = install.status === 0;
|
|
97
|
-
if (!installed)
|
|
98
|
-
console.error(`warning: ${pm} install failed — run it manually in ${targetDir}`);
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
return 1;
|
|
99
149
|
}
|
|
100
|
-
const cdHint = relative(process.cwd(), targetDir) || ".";
|
|
101
|
-
console.log("\nnext steps:");
|
|
102
|
-
console.log(` cd ${cdHint}`);
|
|
103
|
-
if (!installed)
|
|
104
|
-
console.log(" bun install # or npm install");
|
|
105
|
-
console.log(" bun dev # or npm run dev — flat world, spawned player, working HUD");
|
|
106
|
-
console.log(" npx jgengine skills # install the JGengine agent skills for AI-assisted building");
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
109
|
-
function gameTemplateFileCount() {
|
|
110
|
-
return gameTemplate({ id: "probe", name: "Probe", variant: "standalone", engineVersion: "0.0.0" }).length;
|
|
111
150
|
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const SKILLS_SOURCE = "Noisemaker111/jgengine";
|
|
2
|
+
export declare const GAME_SKILLS: readonly ["jgengine-api", "jgengine-newgame", "jgengine-verify"];
|
|
3
|
+
export type SkillsScope = "global" | "project";
|
|
4
|
+
export declare function parseSkillsArgs(argv: string[]): {
|
|
5
|
+
scope: SkillsScope;
|
|
6
|
+
} | {
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function skillsInstallArgs(scope: SkillsScope): string[];
|
|
10
|
+
export declare function installSkills(scope: SkillsScope, cwd?: string): number;
|
|
11
|
+
export declare function runSkills(argv: string[]): number;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export const SKILLS_SOURCE = "Noisemaker111/jgengine";
|
|
3
|
+
export const GAME_SKILLS = ["jgengine-api", "jgengine-newgame", "jgengine-verify"];
|
|
4
|
+
export function parseSkillsArgs(argv) {
|
|
5
|
+
let scope = null;
|
|
6
|
+
for (const arg of argv) {
|
|
7
|
+
if (arg === "-g" || arg === "--global") {
|
|
8
|
+
if (scope === "project")
|
|
9
|
+
return { error: "use either -g/--global or -p/--project, not both" };
|
|
10
|
+
scope = "global";
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (arg === "-p" || arg === "--project") {
|
|
14
|
+
if (scope === "global")
|
|
15
|
+
return { error: "use either -g/--global or -p/--project, not both" };
|
|
16
|
+
scope = "project";
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg === "-y" || arg === "--yes")
|
|
20
|
+
continue;
|
|
21
|
+
if (arg === "-h" || arg === "--help")
|
|
22
|
+
return { error: "help" };
|
|
23
|
+
return { error: `unknown skills option: ${arg}` };
|
|
24
|
+
}
|
|
25
|
+
return { scope: scope ?? "project" };
|
|
26
|
+
}
|
|
27
|
+
export function skillsInstallArgs(scope) {
|
|
28
|
+
const args = ["--yes", "skills", "add", SKILLS_SOURCE, "-y"];
|
|
29
|
+
for (const skill of GAME_SKILLS) {
|
|
30
|
+
args.push("-s", skill);
|
|
31
|
+
}
|
|
32
|
+
args.push("-a", "*");
|
|
33
|
+
if (scope === "global")
|
|
34
|
+
args.push("-g");
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
export function installSkills(scope, cwd) {
|
|
38
|
+
const where = scope === "global" ? "globally" : "in this project";
|
|
39
|
+
console.log(`installing agent skills ${where}: ${GAME_SKILLS.join(", ")}…`);
|
|
40
|
+
const result = spawnSync("npx", skillsInstallArgs(scope), {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
shell: process.platform === "win32",
|
|
43
|
+
cwd,
|
|
44
|
+
});
|
|
45
|
+
return result.status ?? 1;
|
|
46
|
+
}
|
|
47
|
+
export function runSkills(argv) {
|
|
48
|
+
const parsed = parseSkillsArgs(argv);
|
|
49
|
+
if ("error" in parsed) {
|
|
50
|
+
if (parsed.error === "help") {
|
|
51
|
+
console.log(`usage: jgengine skills (-g|--global | -p|--project)
|
|
52
|
+
|
|
53
|
+
-p, --project install into this project (default)
|
|
54
|
+
-g, --global install for your user (every project / agent session)
|
|
55
|
+
|
|
56
|
+
Installs ${GAME_SKILLS.join(", ")} from ${SKILLS_SOURCE}.
|
|
57
|
+
|
|
58
|
+
create already installs project skills for you — you usually never need this command.
|
|
59
|
+
`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
console.error(`error: ${parsed.error}`);
|
|
63
|
+
console.error("usage: jgengine skills (-g|--global | -p|--project)");
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
return installSkills(parsed.scope);
|
|
67
|
+
}
|
package/dist/templates.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ export interface TemplateFile {
|
|
|
10
10
|
contents: string;
|
|
11
11
|
}
|
|
12
12
|
export declare const GAME_ID_PATTERN: RegExp;
|
|
13
|
+
export declare const FOLDER_NAME_PATTERN: RegExp;
|
|
13
14
|
export declare function displayNameFromId(id: string): string;
|
|
15
|
+
export declare function displayNameFromInput(input: string): string;
|
|
16
|
+
export declare function folderNameFromTitle(input: string): string;
|
|
17
|
+
export declare function packageIdFromFolder(folder: string): string;
|
|
18
|
+
export declare function parseCreateName(input: string): {
|
|
19
|
+
displayName: string;
|
|
20
|
+
folderName: string;
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
14
23
|
export declare const IN_REPO_TSCONFIG_PATHS: Record<string, string[]>;
|
|
15
24
|
export declare function gameTemplate(options: TemplateOptions): TemplateFile[];
|
package/dist/templates.js
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
export const GAME_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
2
|
+
export const FOLDER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
2
3
|
export function displayNameFromId(id) {
|
|
3
4
|
return id
|
|
4
5
|
.split("-")
|
|
6
|
+
.filter((word) => word.length > 0)
|
|
5
7
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
6
8
|
.join(" ");
|
|
7
9
|
}
|
|
10
|
+
export function displayNameFromInput(input) {
|
|
11
|
+
const trimmed = input.trim().replace(/\s+/g, " ");
|
|
12
|
+
if (trimmed.length === 0) {
|
|
13
|
+
throw new Error("game name must not be empty");
|
|
14
|
+
}
|
|
15
|
+
if (/\s/.test(trimmed))
|
|
16
|
+
return trimmed;
|
|
17
|
+
if (trimmed.includes("-"))
|
|
18
|
+
return displayNameFromId(trimmed.toLowerCase());
|
|
19
|
+
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
20
|
+
}
|
|
21
|
+
export function folderNameFromTitle(input) {
|
|
22
|
+
const cleaned = input
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "")
|
|
25
|
+
.replace(/\s+/g, " ");
|
|
26
|
+
if (cleaned.length === 0) {
|
|
27
|
+
throw new Error("game name must not be empty");
|
|
28
|
+
}
|
|
29
|
+
const folder = cleaned
|
|
30
|
+
.split(" ")
|
|
31
|
+
.filter((part) => part.length > 0)
|
|
32
|
+
.join("-")
|
|
33
|
+
.replace(/-+/g, "-")
|
|
34
|
+
.replace(/^-+|-+$/g, "");
|
|
35
|
+
if (!FOLDER_NAME_PATTERN.test(folder)) {
|
|
36
|
+
throw new Error(`folder name "${folder}" must start with a letter and contain only letters, digits, and dashes`);
|
|
37
|
+
}
|
|
38
|
+
return folder;
|
|
39
|
+
}
|
|
40
|
+
export function packageIdFromFolder(folder) {
|
|
41
|
+
const id = folder
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
44
|
+
.replace(/^-+|-+$/g, "");
|
|
45
|
+
if (!GAME_ID_PATTERN.test(id)) {
|
|
46
|
+
throw new Error(`package id "${id}" must be kebab-case: lowercase letters, digits, dashes, starting with a letter`);
|
|
47
|
+
}
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
export function parseCreateName(input) {
|
|
51
|
+
const displayName = displayNameFromInput(input);
|
|
52
|
+
const folderName = folderNameFromTitle(input);
|
|
53
|
+
const id = packageIdFromFolder(folderName);
|
|
54
|
+
return { displayName, folderName, id };
|
|
55
|
+
}
|
|
8
56
|
const indexHtml = (name) => `<!doctype html>
|
|
9
57
|
<html lang="en">
|
|
10
58
|
<head>
|
|
@@ -296,21 +344,30 @@ describe("${id} world", () => {
|
|
|
296
344
|
});
|
|
297
345
|
});
|
|
298
346
|
`;
|
|
299
|
-
const agentsMd = (name, variant) => `# ${name} — agent
|
|
347
|
+
const agentsMd = (name, variant) => `# ${name} — agent briefing
|
|
348
|
+
|
|
349
|
+
You are in a **JGengine** game project. JGengine is a pure-TypeScript game engine SDK on npm (\`@jgengine/core\`, \`react\`, \`shell\`, …). The CLI is \`npx jgengine\`. Site: https://jgengine.com · source: https://github.com/Noisemaker111/jgengine
|
|
350
|
+
|
|
351
|
+
## What to do when the user wants this game built
|
|
352
|
+
|
|
353
|
+
1. Read skills if present: \`jgengine-newgame\` (workflow), \`jgengine-api\` (API), \`jgengine-verify\` (prove it works). They land under \`.agents/skills/\` or \`.claude/skills/\` when scaffolded via \`npx jgengine create\`.
|
|
354
|
+
2. If skills are missing: \`npx jgengine skills -p\` (or use this file + \`npx jgengine llms core\`).
|
|
355
|
+
3. **User-facing first reply is short** — game name, fantasy in 2–4 lines, POV (1st / 3rd / top-down / HUD-only), world kind, scale vibe. Ask a few tight questions (POV, world, multiplayer, how big). **Do not** dump file trees, catalog ids, keybind tables, or full phase plans to the user.
|
|
356
|
+
4. Keep the full engineering plan (files, systems, budgets) internal. After they answer, scaffold is already here — build in phases, full game not a slice.
|
|
300
357
|
|
|
301
|
-
|
|
358
|
+
## Engine loaders
|
|
302
359
|
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
-
|
|
360
|
+
- API docs in the tarball: \`npx jgengine llms core\` (any package: react, shell, …)
|
|
361
|
+
- Doctor: \`npx jgengine doctor\`
|
|
362
|
+
- Dev: \`bun dev\` / \`npm run dev\`
|
|
306
363
|
|
|
307
|
-
|
|
364
|
+
## Project rules
|
|
308
365
|
|
|
309
|
-
- Shape: \`src/\` holds only \`game.config.ts\`, \`index.tsx\`, \`main.tsx\`, \`loop.ts\`, \`world.ts\`, \`index.css\`;
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
-
|
|
366
|
+
- Shape: \`src/\` holds only \`game.config.ts\`, \`index.tsx\`, \`main.tsx\`, \`loop.ts\`, \`world.ts\`, \`index.css\`; everything else under \`src/game/\`.
|
|
367
|
+
- Entry: \`defineGame({...})\` from \`@jgengine/shell/defineGame\` in \`game.config.ts\`.
|
|
368
|
+
- Prove world content with \`summarizeEnvironment\` in \`bun test\` (\`src/game/world.world.test.ts\`), not screenshot loops.
|
|
369
|
+
- Tailwind v4: \`@source\` in \`src/index.css\` must cover \`@jgengine/react\` and \`@jgengine/shell\`${variant === "in-repo" ? " (engine source under packages/)" : " (dist under node_modules)"}, or the HUD is silently unstyled.
|
|
370
|
+
- Spawn player with \`id === ctx.player.userId\` in \`onNewPlayer\`; \`onTick\` \`dt\` is game time.
|
|
314
371
|
`;
|
|
315
372
|
export function gameTemplate(options) {
|
|
316
373
|
const { id, name, variant, engineVersion } = options;
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @jgengine/jgengine
|
|
2
|
-
>
|
|
2
|
+
> JGengine CLI — pure-TypeScript game engine entry for humans and agents. Run: npx jgengine create "My Game Name" (scaffolds a playable base and installs agent skills). Then build the game. Docs: https://jgengine.com
|
|
3
3
|
|
|
4
|
-
Version: 0.8.
|
|
4
|
+
Version: 0.8.2
|
|
5
5
|
License: AGPL-3.0-only
|
|
6
6
|
Repository: https://github.com/Noisemaker111/jgengine
|
|
7
7
|
Docs: https://jgengine.com
|
|
@@ -12,7 +12,7 @@ Imports use deep paths: `@jgengine/jgengine/<path>`.
|
|
|
12
12
|
### @jgengine/jgengine/create
|
|
13
13
|
|
|
14
14
|
- writeGame (function): function writeGame(targetDir: string, id: string, name: string, variant: TemplateVariant): void
|
|
15
|
-
- registerRootGameScript (function): function registerRootGameScript(rootDir: string, id: string): boolean
|
|
15
|
+
- registerRootGameScript (function): function registerRootGameScript(rootDir: string, id: string, folderName: string = id): boolean
|
|
16
16
|
- runCreate (function): function runCreate(argv: string[]): number
|
|
17
17
|
|
|
18
18
|
### @jgengine/jgengine/doctor
|
|
@@ -32,7 +32,7 @@ Imports use deep paths: `@jgengine/jgengine/<path>`.
|
|
|
32
32
|
- TemplateVariant (type): type TemplateVariant = "standalone" | "in-repo"
|
|
33
33
|
- runCreate (function): function runCreate(argv: string[]): number
|
|
34
34
|
- writeGame (function): function writeGame(targetDir: string, id: string, name: string, variant: TemplateVariant): void
|
|
35
|
-
- registerRootGameScript (function): function registerRootGameScript(rootDir: string, id: string): boolean
|
|
35
|
+
- registerRootGameScript (function): function registerRootGameScript(rootDir: string, id: string, folderName: string = id): boolean
|
|
36
36
|
- diagnose (function): function diagnose(dir: string): Finding[]
|
|
37
37
|
- runDoctor (function): function runDoctor(argv: string[]): number
|
|
38
38
|
- Finding (interface): interface Finding
|
|
@@ -49,14 +49,29 @@ Imports use deep paths: `@jgengine/jgengine/<path>`.
|
|
|
49
49
|
- hasFlag (function): function hasFlag(argv: string[], name: string): boolean
|
|
50
50
|
- PackageJson (interface): interface PackageJson
|
|
51
51
|
|
|
52
|
+
### @jgengine/jgengine/skills
|
|
53
|
+
|
|
54
|
+
- parseSkillsArgs (function): function parseSkillsArgs(argv: string[]): { scope: SkillsScope } | { error: string }
|
|
55
|
+
- skillsInstallArgs (function): function skillsInstallArgs(scope: SkillsScope): string[]
|
|
56
|
+
- installSkills (function): function installSkills(scope: SkillsScope, cwd?: string): number
|
|
57
|
+
- runSkills (function): function runSkills(argv: string[]): number
|
|
58
|
+
- SKILLS_SOURCE (const): const SKILLS_SOURCE: "Noisemaker111/jgengine"
|
|
59
|
+
- GAME_SKILLS (const): const GAME_SKILLS: readonly ["jgengine-api", "jgengine-newgame", "jgengine-verify"]
|
|
60
|
+
- SkillsScope (type): type SkillsScope = "global" | "project"
|
|
61
|
+
|
|
52
62
|
### @jgengine/jgengine/templates
|
|
53
63
|
|
|
54
64
|
- displayNameFromId (function): function displayNameFromId(id: string): string
|
|
65
|
+
- displayNameFromInput (function): function displayNameFromInput(input: string): string
|
|
66
|
+
- folderNameFromTitle (function): function folderNameFromTitle(input: string): string
|
|
67
|
+
- packageIdFromFolder (function): function packageIdFromFolder(folder: string): string
|
|
68
|
+
- parseCreateName (function): function parseCreateName(input: string): { displayName: string; folderName: string; id: string }
|
|
55
69
|
- gameTemplate (function): function gameTemplate(options: TemplateOptions): TemplateFile[]
|
|
56
70
|
- TemplateVariant (type): type TemplateVariant = "standalone" | "in-repo"
|
|
57
71
|
- TemplateOptions (interface): interface TemplateOptions
|
|
58
72
|
- TemplateFile (interface): interface TemplateFile
|
|
59
73
|
- GAME_ID_PATTERN (const): const GAME_ID_PATTERN: RegExp
|
|
74
|
+
- FOLDER_NAME_PATTERN (const): const FOLDER_NAME_PATTERN: RegExp
|
|
60
75
|
- IN_REPO_TSCONFIG_PATHS (const): const IN_REPO_TSCONFIG_PATHS: Record<string, string[]>
|
|
61
76
|
|
|
62
77
|
## Guides
|
|
@@ -292,8 +307,9 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
292
307
|
Fastest path — the `jgengine` CLI scaffolds the entire canonical shape below (harness, skeleton, stub game, verify test, AGENTS.md) as a booting game:
|
|
293
308
|
|
|
294
309
|
```sh
|
|
295
|
-
npx jgengine create
|
|
296
|
-
|
|
310
|
+
npx jgengine create "My Game Name" # folder → My-Game-Name; skills install automatically
|
|
311
|
+
cd My-Game-Name && bun dev
|
|
312
|
+
npx jgengine doctor # later, if setup drifts (version skew, unstyled HUD, shape)
|
|
297
313
|
```
|
|
298
314
|
|
|
299
315
|
Manual equivalent:
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jgengine",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "JGengine CLI — pure-TypeScript game engine entry for humans and agents. Run: npx jgengine create \"My Game Name\" (scaffolds a playable base and installs agent skills). Then build the game. Docs: https://jgengine.com",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"game-engine",
|
|
7
|
+
"typescript",
|
|
8
|
+
"jgengine",
|
|
9
|
+
"cli",
|
|
10
|
+
"three.js",
|
|
11
|
+
"agent",
|
|
12
|
+
"scaffold"
|
|
13
|
+
],
|
|
5
14
|
"license": "AGPL-3.0-only",
|
|
6
15
|
"type": "module",
|
|
7
16
|
"sideEffects": false,
|
|
17
|
+
"homepage": "https://jgengine.com",
|
|
8
18
|
"repository": {
|
|
9
19
|
"type": "git",
|
|
10
20
|
"url": "git+https://github.com/Noisemaker111/jgengine.git",
|