jgengine 0.8.0 → 0.8.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/dist/cli/index.js +17 -18
- package/dist/create.d.ts +1 -1
- package/dist/create.js +91 -60
- package/dist/skills.d.ts +9 -0
- package/dist/skills.js +65 -0
- package/dist/templates.d.ts +9 -0
- package/dist/templates.js +49 -1
- package/llms.txt +22 -6
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -7,23 +7,30 @@ 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
12
|
const HELP = `jgengine ${cliVersion()} — the JGengine command line
|
|
12
13
|
Pure-TypeScript, genre-agnostic game engine SDK. Packages: ${ENGINE_PACKAGES.map((name) => `@jgengine/${name}`).join(", ")}.
|
|
13
14
|
|
|
14
15
|
usage: jgengine <command> [...args]
|
|
15
16
|
|
|
16
|
-
create <
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
skills
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
create "<Game Name>" scaffold a playable base — flat world, spawned player, HUD, verify test.
|
|
18
|
+
folder becomes My-Game-Name; name lands in game.config / HUD / title.
|
|
19
|
+
[--in-repo|--standalone] [--no-install] [--pm bun|npm|pnpm]
|
|
20
|
+
skills -p | -g install agent skills (api, newgame, verify). -p this project (default), -g global
|
|
21
|
+
doctor [dir] diagnose a game project: version skew, missing peers, unstyled-UI @source gaps, shape drift
|
|
22
|
+
llms [package] print packaged API docs (llms.txt) for an installed @jgengine/* package — agent-ready context
|
|
23
|
+
assets [...] delegate to the @jgengine/assets CLI: list, search, pull CC0 3D model packs
|
|
24
|
+
versions show CLI + installed @jgengine/* versions
|
|
25
|
+
help this map
|
|
24
26
|
|
|
25
27
|
docs: https://jgengine.com · source: https://github.com/Noisemaker111/jgengine
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
entry (any directory, no monorepo needed):
|
|
30
|
+
npx jgengine create "Solitaire"
|
|
31
|
+
cd Solitaire
|
|
32
|
+
npx jgengine skills -p # or: npx jgengine skills -g (once, for every project)
|
|
33
|
+
# then: make Solitaire with jgengine
|
|
27
34
|
`;
|
|
28
35
|
function runVersions() {
|
|
29
36
|
console.log(`jgengine ${cliVersion()}`);
|
|
@@ -61,14 +68,6 @@ function runLlms(argv) {
|
|
|
61
68
|
process.stdout.write(readFileSync(join(found, "node_modules", packageName, "llms.txt"), "utf8"));
|
|
62
69
|
return 0;
|
|
63
70
|
}
|
|
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
71
|
function runAssets(argv) {
|
|
73
72
|
const require = createRequire(import.meta.url);
|
|
74
73
|
let cliPath;
|
|
@@ -91,7 +90,7 @@ switch (command) {
|
|
|
91
90
|
process.exit(runDoctor(rest));
|
|
92
91
|
break;
|
|
93
92
|
case "skills":
|
|
94
|
-
process.exit(runSkills());
|
|
93
|
+
process.exit(runSkills(rest));
|
|
95
94
|
break;
|
|
96
95
|
case "llms":
|
|
97
96
|
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,8 @@
|
|
|
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 { gameTemplate, parseCreateName } from "./templates.js";
|
|
6
6
|
export function writeGame(targetDir, id, name, variant) {
|
|
7
7
|
for (const file of gameTemplate({ id, name, variant, engineVersion: cliVersion() })) {
|
|
8
8
|
const dest = join(targetDir, file.path);
|
|
@@ -10,7 +10,7 @@ export function writeGame(targetDir, id, name, variant) {
|
|
|
10
10
|
writeFileSync(dest, file.contents);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
export function registerRootGameScript(rootDir, id) {
|
|
13
|
+
export function registerRootGameScript(rootDir, id, folderName = id) {
|
|
14
14
|
const rootPackagePath = join(rootDir, "package.json");
|
|
15
15
|
const root = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
|
16
16
|
const scripts = root.scripts ?? {};
|
|
@@ -20,7 +20,7 @@ export function registerRootGameScript(rootDir, id) {
|
|
|
20
20
|
const entries = Object.entries(scripts);
|
|
21
21
|
const games = entries.filter(([k]) => k.startsWith("games:"));
|
|
22
22
|
const rest = entries.filter(([k]) => !k.startsWith("games:"));
|
|
23
|
-
games.push([key, `bun run --cwd Games/${
|
|
23
|
+
games.push([key, `bun run --cwd Games/${folderName} dev`]);
|
|
24
24
|
games.sort(([a], [b]) => a.localeCompare(b));
|
|
25
25
|
root.scripts = Object.fromEntries([...rest, ...games]);
|
|
26
26
|
writeFileSync(rootPackagePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
@@ -32,7 +32,7 @@ function pickPackageManager(preferred) {
|
|
|
32
32
|
const probe = spawnSync("bun", ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
|
|
33
33
|
return probe.status === 0 ? "bun" : "npm";
|
|
34
34
|
}
|
|
35
|
-
const VALUE_FLAGS = new Set(["--
|
|
35
|
+
const VALUE_FLAGS = new Set(["--pm"]);
|
|
36
36
|
function positionalArg(argv) {
|
|
37
37
|
for (let index = 0; index < argv.length; index += 1) {
|
|
38
38
|
const arg = argv[index];
|
|
@@ -45,67 +45,98 @@ function positionalArg(argv) {
|
|
|
45
45
|
}
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
+
function splitCreateArg(arg) {
|
|
49
|
+
const normalized = arg.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
50
|
+
const trimmed = normalized.replace(/\/$/, "");
|
|
51
|
+
const slash = trimmed.lastIndexOf("/");
|
|
52
|
+
if (slash < 0)
|
|
53
|
+
return { parentHint: null, titlePart: trimmed };
|
|
54
|
+
return {
|
|
55
|
+
parentHint: trimmed.slice(0, slash) || ".",
|
|
56
|
+
titlePart: trimmed.slice(slash + 1),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isInsideDir(child, parent) {
|
|
60
|
+
const resolvedChild = resolve(child);
|
|
61
|
+
const resolvedParent = resolve(parent);
|
|
62
|
+
return resolvedChild === resolvedParent || resolvedChild.startsWith(resolvedParent + sep);
|
|
63
|
+
}
|
|
48
64
|
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");
|
|
65
|
+
const nameArg = positionalArg(argv);
|
|
66
|
+
if (nameArg === undefined) {
|
|
67
|
+
console.error('usage: jgengine create "<Game Name>" [--in-repo|--standalone] [--no-install] [--pm bun|npm|pnpm]');
|
|
73
68
|
return 1;
|
|
74
69
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
let displayName;
|
|
71
|
+
let folderName;
|
|
72
|
+
let id;
|
|
73
|
+
try {
|
|
74
|
+
const { parentHint, titlePart } = splitCreateArg(nameArg);
|
|
75
|
+
({ displayName, folderName, id } = parseCreateName(titlePart));
|
|
76
|
+
let parentDir = parentHint !== null ? resolve(parentHint) : process.cwd();
|
|
77
|
+
let targetDir = join(parentDir, folderName);
|
|
78
|
+
const workspaceRoot = findWorkspaceRoot(dirname(targetDir)) ?? findWorkspaceRoot(parentDir) ?? findWorkspaceRoot(process.cwd());
|
|
79
|
+
const insideEngineRepo = workspaceRoot !== null && isEngineMonorepo(workspaceRoot);
|
|
80
|
+
const variant = hasFlag(argv, "standalone")
|
|
81
|
+
? "standalone"
|
|
82
|
+
: hasFlag(argv, "in-repo") || insideEngineRepo
|
|
83
|
+
? "in-repo"
|
|
84
|
+
: "standalone";
|
|
85
|
+
if (variant === "in-repo") {
|
|
86
|
+
if (!insideEngineRepo || workspaceRoot === null) {
|
|
87
|
+
console.error("error: --in-repo requires the jgengine engine monorepo (packages/core + Games/)");
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (parentHint === null) {
|
|
91
|
+
parentDir = join(workspaceRoot, "Games");
|
|
92
|
+
targetDir = join(parentDir, folderName);
|
|
93
|
+
}
|
|
94
|
+
const gamesDir = join(workspaceRoot, "Games");
|
|
95
|
+
if (!isInsideDir(targetDir, gamesDir)) {
|
|
96
|
+
console.error(`error: in-repo games must live under Games/ (got ${targetDir}; expected under ${gamesDir})`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
81
99
|
}
|
|
100
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
101
|
+
console.error(`error: ${targetDir} already exists and is not empty`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
writeGame(targetDir, id, displayName, variant);
|
|
105
|
+
console.log(`created ${displayName} (${variant}) → ${targetDir}`);
|
|
106
|
+
console.log(` folder ${folderName} package ${id} name "${displayName}"`);
|
|
107
|
+
if (variant === "in-repo" && workspaceRoot !== null) {
|
|
108
|
+
if (registerRootGameScript(workspaceRoot, id, folderName)) {
|
|
109
|
+
console.log(`registered root script "games:${id}" in ${join(workspaceRoot, "package.json")}`);
|
|
110
|
+
}
|
|
111
|
+
console.log("\nnext steps:");
|
|
112
|
+
console.log(` bun install # from ${workspaceRoot}`);
|
|
113
|
+
console.log(` bun run games:${id} # play it standalone`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
let installed = false;
|
|
117
|
+
if (!hasFlag(argv, "no-install")) {
|
|
118
|
+
const pm = pickPackageManager(flag(argv, "pm"));
|
|
119
|
+
console.log(`installing dependencies with ${pm}…`);
|
|
120
|
+
const install = spawnSync(pm, ["install"], {
|
|
121
|
+
cwd: targetDir,
|
|
122
|
+
stdio: "inherit",
|
|
123
|
+
shell: process.platform === "win32",
|
|
124
|
+
});
|
|
125
|
+
installed = install.status === 0;
|
|
126
|
+
if (!installed)
|
|
127
|
+
console.error(`warning: ${pm} install failed — run it manually in ${targetDir}`);
|
|
128
|
+
}
|
|
129
|
+
const cdHint = relative(process.cwd(), targetDir) || ".";
|
|
82
130
|
console.log("\nnext steps:");
|
|
83
|
-
console.log(`
|
|
84
|
-
|
|
131
|
+
console.log(` cd ${cdHint}`);
|
|
132
|
+
if (!installed)
|
|
133
|
+
console.log(" bun install # or npm install");
|
|
134
|
+
console.log(" bun dev # or npm run dev — flat world, spawned player, working HUD");
|
|
135
|
+
console.log(" npx jgengine skills -p # agent skills in this project (-g for global)");
|
|
85
136
|
return 0;
|
|
86
137
|
}
|
|
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}`);
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
return 1;
|
|
99
141
|
}
|
|
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
142
|
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const SKILLS_SOURCE = "Noisemaker111/jgengine";
|
|
2
|
+
export type SkillsScope = "global" | "project";
|
|
3
|
+
export declare function parseSkillsArgs(argv: string[]): {
|
|
4
|
+
scope: SkillsScope;
|
|
5
|
+
} | {
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function skillsInstallArgs(scope: SkillsScope): string[];
|
|
9
|
+
export declare function runSkills(argv: string[]): number;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export const SKILLS_SOURCE = "Noisemaker111/jgengine";
|
|
3
|
+
export function parseSkillsArgs(argv) {
|
|
4
|
+
let scope = null;
|
|
5
|
+
for (const arg of argv) {
|
|
6
|
+
if (arg === "-g" || arg === "--global") {
|
|
7
|
+
if (scope === "project")
|
|
8
|
+
return { error: "use either -g/--global or -p/--project, not both" };
|
|
9
|
+
scope = "global";
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (arg === "-p" || arg === "--project") {
|
|
13
|
+
if (scope === "global")
|
|
14
|
+
return { error: "use either -g/--global or -p/--project, not both" };
|
|
15
|
+
scope = "project";
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (arg === "-y" || arg === "--yes")
|
|
19
|
+
continue;
|
|
20
|
+
if (arg === "-h" || arg === "--help")
|
|
21
|
+
return { error: "help" };
|
|
22
|
+
return { error: `unknown skills option: ${arg}` };
|
|
23
|
+
}
|
|
24
|
+
return { scope: scope ?? "project" };
|
|
25
|
+
}
|
|
26
|
+
export function skillsInstallArgs(scope) {
|
|
27
|
+
const args = ["--yes", "skills", "add", SKILLS_SOURCE, "-y", "--all"];
|
|
28
|
+
if (scope === "global")
|
|
29
|
+
args.push("-g");
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
export function runSkills(argv) {
|
|
33
|
+
const parsed = parseSkillsArgs(argv);
|
|
34
|
+
if ("error" in parsed) {
|
|
35
|
+
if (parsed.error === "help") {
|
|
36
|
+
console.log(`usage: jgengine skills (-g|--global | -p|--project)
|
|
37
|
+
|
|
38
|
+
-p, --project install into this project (.claude/skills) — default
|
|
39
|
+
-g, --global install for your user (every project / agent session)
|
|
40
|
+
|
|
41
|
+
Installs jgengine-api, jgengine-newgame, and jgengine-verify from ${SKILLS_SOURCE}.
|
|
42
|
+
|
|
43
|
+
entry:
|
|
44
|
+
npx jgengine create "Solitaire"
|
|
45
|
+
cd Solitaire
|
|
46
|
+
npx jgengine skills -p
|
|
47
|
+
# tell your agent: make Solitaire with jgengine
|
|
48
|
+
|
|
49
|
+
# or once for all projects:
|
|
50
|
+
npx jgengine skills -g
|
|
51
|
+
`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
console.error(`error: ${parsed.error}`);
|
|
55
|
+
console.error('usage: jgengine skills (-g|--global | -p|--project)');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const where = parsed.scope === "global" ? "globally" : "in this project";
|
|
59
|
+
console.log(`installing JGengine agent skills ${where} (jgengine-api, jgengine-newgame, jgengine-verify)…`);
|
|
60
|
+
const result = spawnSync("npx", skillsInstallArgs(parsed.scope), {
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
shell: process.platform === "win32",
|
|
63
|
+
});
|
|
64
|
+
return result.status ?? 1;
|
|
65
|
+
}
|
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>
|
|
@@ -301,7 +349,7 @@ const agentsMd = (name, variant) => `# ${name} — agent notes
|
|
|
301
349
|
This is a JGengine game (pure-TypeScript engine SDK, \`@jgengine/*\` on npm). Load context before writing code:
|
|
302
350
|
|
|
303
351
|
- Engine API surface: \`npx jgengine llms core\` (any package name works) prints the packaged API docs.
|
|
304
|
-
- Agent skills — API reference, phased game-build workflow, browserless verify gate: \`npx jgengine skills\`
|
|
352
|
+
- Agent skills — API reference, phased game-build workflow, browserless verify gate: \`npx jgengine skills -p\` (this project) or \`-g\` (global).
|
|
305
353
|
- Setup broken or UI unstyled: \`npx jgengine doctor\`.
|
|
306
354
|
|
|
307
355
|
Rules this project follows:
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @jgengine/jgengine
|
|
2
|
-
> The JGengine command line — scaffold a playable game, diagnose a broken setup, and load engine docs and agent skills into any project. Start with `npx jgengine create
|
|
2
|
+
> The JGengine command line — scaffold a playable game, diagnose a broken setup, and load engine docs and agent skills into any project. Start with `npx jgengine create "My Game Name"`.
|
|
3
3
|
|
|
4
|
-
Version: 0.8.
|
|
4
|
+
Version: 0.8.1
|
|
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,27 @@ 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
|
+
- runSkills (function): function runSkills(argv: string[]): number
|
|
57
|
+
- SKILLS_SOURCE (const): const SKILLS_SOURCE: "Noisemaker111/jgengine"
|
|
58
|
+
- SkillsScope (type): type SkillsScope = "global" | "project"
|
|
59
|
+
|
|
52
60
|
### @jgengine/jgengine/templates
|
|
53
61
|
|
|
54
62
|
- displayNameFromId (function): function displayNameFromId(id: string): string
|
|
63
|
+
- displayNameFromInput (function): function displayNameFromInput(input: string): string
|
|
64
|
+
- folderNameFromTitle (function): function folderNameFromTitle(input: string): string
|
|
65
|
+
- packageIdFromFolder (function): function packageIdFromFolder(folder: string): string
|
|
66
|
+
- parseCreateName (function): function parseCreateName(input: string): { displayName: string; folderName: string; id: string }
|
|
55
67
|
- gameTemplate (function): function gameTemplate(options: TemplateOptions): TemplateFile[]
|
|
56
68
|
- TemplateVariant (type): type TemplateVariant = "standalone" | "in-repo"
|
|
57
69
|
- TemplateOptions (interface): interface TemplateOptions
|
|
58
70
|
- TemplateFile (interface): interface TemplateFile
|
|
59
71
|
- GAME_ID_PATTERN (const): const GAME_ID_PATTERN: RegExp
|
|
72
|
+
- FOLDER_NAME_PATTERN (const): const FOLDER_NAME_PATTERN: RegExp
|
|
60
73
|
- IN_REPO_TSCONFIG_PATHS (const): const IN_REPO_TSCONFIG_PATHS: Record<string, string[]>
|
|
61
74
|
|
|
62
75
|
## Guides
|
|
@@ -292,8 +305,11 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
292
305
|
Fastest path — the `jgengine` CLI scaffolds the entire canonical shape below (harness, skeleton, stub game, verify test, AGENTS.md) as a booting game:
|
|
293
306
|
|
|
294
307
|
```sh
|
|
295
|
-
npx jgengine create
|
|
296
|
-
|
|
308
|
+
npx jgengine create "My Game Name" # folder → My-Game-Name; name lands in game.config / HUD / title
|
|
309
|
+
cd My-Game-Name
|
|
310
|
+
npx jgengine skills -p # agent skills here; use -g once for global
|
|
311
|
+
bun dev
|
|
312
|
+
npx jgengine doctor # later, if the setup drifts (version skew, unstyled HUD, shape strays)
|
|
297
313
|
```
|
|
298
314
|
|
|
299
315
|
Manual equivalent:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jgengine",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "The JGengine command line — scaffold a playable game, diagnose a broken setup, and load engine docs and agent skills into any project. Start with `npx jgengine create
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "The JGengine command line — scaffold a playable game, diagnose a broken setup, and load engine docs and agent skills into any project. Start with `npx jgengine create \"My Game Name\"`.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|