gamekit777 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  ## 开始
15
15
 
16
16
  ```sh
17
- bunx gamekit777 new # 一个能跑的抛硬币(my-game)+ bun install;名字改 game.meta.json
17
+ bunx gamekit777 new [名字] # 一个能跑的抛硬币 + bun install:不给名字就地用当前空目录,给名字生成到子目录
18
18
  cd my-game
19
19
  bun run dev # 起 studio:http://127.0.0.1:4301/ ,预览 / 调参 / 建表 / Case / 发布
20
20
  ```
@@ -43,11 +43,19 @@ const walk = (dir: string): string[] =>
43
43
  const pascal = (id: string): string =>
44
44
  id.split('-').map((w) => w[0]!.toUpperCase() + w.slice(1)).join('');
45
45
 
46
+ /** 存在也不算「非空」的东西:新建仓库时常见的那几样 */
47
+ const IGNORABLE = new Set(['README.md', 'LICENSE', 'CLAUDE.md']);
48
+
46
49
  export function scaffoldGame(o: ScaffoldOptions): ScaffoldResult {
47
50
  if (!ID_PATTERN.test(o.id)) {
48
51
  throw new Error(`id "${o.id}" 要是小写短横线形式——它同时是包名、输出目录名和 CSS 命名空间`);
49
52
  }
50
- if (existsSync(o.dest)) throw new Error(`${o.dest} 已经存在`);
53
+ /* 目标目录可以已存在,但必须是「空」的:只有 .git、README、编辑器配置这类不算内容。
54
+ 有 package.json / game.ts / src 就是别的项目,往里铺模板会把它搅成两个项目的混合物 */
55
+ if (existsSync(o.dest)) {
56
+ const left = readdirSync(o.dest).filter((f) => !IGNORABLE.has(f) && !f.startsWith('.'));
57
+ if (left.length) throw new Error(`${o.dest} 不是空目录(有 ${left.slice(0, 5).join('、')}${left.length > 5 ? '…' : ''})`);
58
+ }
51
59
 
52
60
  cpSync(TEMPLATE_DIR, o.dest, { recursive: true });
53
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamekit777",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gamekit777": "./studio/cli.ts"
package/studio/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  /* `gamekit777` 这个命令的入口:按子命令分派。
3
3
  *
4
4
  * gamekit777 studio [dir] [--mcp|--no-mcp] [--game-port N] [--backend URL] 起工作台(MCP 走 stdio)
5
- * gamekit777 new 在当前目录脚手架一个新游戏(my-game)并装依赖;改名改 game.meta.json
5
+ * gamekit777 new [名字] 新建游戏并装依赖:给名字 → 子目录 ./名字;不给 → 就地用当前(空)目录
6
6
  *
7
7
  * 只是薄薄一层分派,逻辑各在 bin.ts 和 src/scaffold.ts。 */
8
8
  const [sub, ...rest] = process.argv.slice(2);
@@ -10,7 +10,7 @@ const [sub, ...rest] = process.argv.slice(2);
10
10
  const usage = (): never => {
11
11
  process.stderr.write(`用法:
12
12
  gamekit777 studio [dir] [--mcp|--no-mcp] [--game-port N] [--backend URL]
13
- gamekit777 new 在当前目录生成 my-game(占了加序号)并装依赖;名字之后改 game.meta.json
13
+ gamekit777 new [名字] 新建游戏并装依赖:给名字 → 生成 ./名字;不给 → 就地用当前目录(必须是空目录,id 取目录名)
14
14
 
15
15
  接 Claude Code:在游戏目录里
16
16
  claude mcp add gamekit -- bunx gamekit777 studio
@@ -25,12 +25,13 @@ switch (sub) {
25
25
  break;
26
26
  }
27
27
  case 'new': {
28
- // 刻意不收参数:名字、标题都在生成后的 game.meta.json 里改,少一层「命令行参数 vs 配置文件」的分叉
29
- if (rest.length > 0) usage();
28
+ /* 只收一个可选的名字。不给就用当前目录:AI 多半已经站在要建的目录里,再套一层 my-game 只会让它 cd 错地方。
29
+ 标题之类都在生成后的 game.meta.json 里改,少一层「命令行参数 vs 配置文件」的分叉 */
30
+ if (rest.length > 1 || rest[0]?.startsWith('-')) usage();
30
31
  const { scaffold } = await import('./src/scaffold.ts');
31
32
  try {
32
- const r = await scaffold({}, process.cwd());
33
- process.stdout.write(`✓ ${r.dest}\n\n${r.next.map((n) => ` ${n}`).join('\n')}\n\n cd ${r.dest} && bunx gamekit777 studio\n`);
33
+ const r = await scaffold({ name: rest[0] }, process.cwd());
34
+ process.stdout.write(`✓ ${r.inPlace ? `就地生成在 ${r.dest}` : r.dest}\n\n${r.next.map((n) => ` ${n}`).join('\n')}\n\n ${r.inPlace ? '' : `cd ${r.dest} && `}bunx gamekit777 studio\n`);
34
35
  } catch (e) {
35
36
  process.stderr.write(`✗ ${e instanceof Error ? e.message : String(e)}\n`);
36
37
  process.exit(1);
package/studio/src/mcp.ts CHANGED
@@ -145,12 +145,12 @@ export function createMcpServer(engine: Engine): McpServer {
145
145
  });
146
146
 
147
147
  s.registerTool('scaffold', {
148
- description: '在当前目录下新建一个游戏(my-game,占了加序号;一个能跑的抛硬币),写好 package.json 并 bun install。名字之后改 game.meta.json。请在那个目录里重启 studio。',
149
- inputSchema: {},
150
- }, async () => {
148
+ description: '新建一个游戏(能跑的抛硬币)并 bun install。不给 name 就在当前目录就地生成(当前目录必须是空的,id 取目录名);给 name 就生成到子目录 ./name。当前目录已经是项目时会拒绝并让你给名字。标题之后改 game.meta.json。生成后要在那个目录里重启 studio。',
149
+ inputSchema: { name: z.string().optional().describe('子目录名,同时是 id(小写短横线)。不给 = 用当前目录') },
150
+ }, async (a) => {
151
151
  try {
152
- const r = await scaffold({}, process.cwd());
153
- return out(`## ✅ 已生成 ${r.dest}\n\n${r.files} 个文件,依赖${r.installed ? '已装' : '未装'}。\n\n${r.next.map((n) => `- ${n}`).join('\n')}`, r);
152
+ const r = await scaffold({ name: a.name }, process.cwd());
153
+ return out(`## ✅ ${r.inPlace ? `已就地生成在 ${r.dest}` : `已生成 ${r.dest}`}\n\n${r.files} 个文件,依赖${r.installed ? '已装' : '未装'}。\n\n${r.next.map((n) => `- ${n}`).join('\n')}`, r);
154
154
  } catch (e) { return fail(e instanceof Error ? e.message : String(e)); }
155
155
  });
156
156
 
@@ -3,25 +3,45 @@
3
3
  * 这是 MCP 的 scaffold 工具背后的东西。装依赖这一步只有这里会自动做——
4
4
  * 别处发现缺 node_modules 一律只提示,不代作者动手。 */
5
5
  import { spawn } from 'node:child_process';
6
- import { existsSync, readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
6
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
7
+ import { basename, join } from 'node:path';
8
8
  import { scaffoldGame, ID_PATTERN } from 'gamekit777/create-game';
9
9
 
10
10
  export const DEFAULT_ID = 'my-game';
11
11
  export const DEFAULT_TITLE = '我的游戏';
12
12
 
13
- /** 不给 id 就叫 my-game;占了就 my-game-2、my-game-3……创作者改名只需要动 slug 和目录名 */
14
- export function defaultId(cwd: string): string {
15
- if (!existsSync(join(cwd, DEFAULT_ID))) return DEFAULT_ID;
16
- for (let n = 2; ; n++) if (!existsSync(join(cwd, `${DEFAULT_ID}-${n}`))) return `${DEFAULT_ID}-${n}`;
13
+ /** 目录名能当 id 就当 id(同时是 slug 和 CSS 命名空间),不能就退回 my-game */
14
+ export function idFromDir(dir: string): string {
15
+ const b = basename(dir).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
16
+ return ID_PATTERN.test(b) ? b : DEFAULT_ID;
17
+ }
18
+
19
+ /** 当前目录像不像一个已有的项目:有这些就别往里铺模板 */
20
+ export function looksLikeProject(dir: string): string | null {
21
+ for (const f of ['package.json', 'game.ts', 'src', 'vite.config.ts']) if (existsSync(join(dir, f))) return f;
22
+ return null;
23
+ }
24
+
25
+ /**
26
+ * 不带名字时的目标就是 cwd 本身:AI 通常已经站在它要建的目录里,再套一层 my-game 只会让它 cd 错地方。
27
+ * cwd 已经是个项目、或者有别的文件,就拒绝并让它给名字——不猜、不覆盖
28
+ */
29
+ export function resolveTarget(cwd: string, name?: string): { id: string; dest: string; inPlace: boolean } {
30
+ if (name) {
31
+ if (!ID_PATTERN.test(name)) throw new Error(`名字 "${name}" 要是小写短横线形式(a-z 0-9 -),它同时是 slug、目录名和 CSS 命名空间`);
32
+ return { id: name, dest: join(cwd, name), inPlace: false };
33
+ }
34
+ const hit = looksLikeProject(cwd);
35
+ if (hit) throw new Error(`当前目录 ${cwd} 已经是一个项目(有 ${hit})。要在它里面新建就给个名字:gamekit777 new <名字>`);
36
+ const others = readdirSync(cwd).filter((f) => !f.startsWith('.') && !['README.md', 'LICENSE', 'CLAUDE.md'].includes(f));
37
+ if (others.length) throw new Error(`当前目录 ${cwd} 不是空的(有 ${others.slice(0, 5).join('、')}${others.length > 5 ? '…' : ''})。在空目录里跑,或给个名字:gamekit777 new <名字>`);
38
+ return { id: idFromDir(cwd), dest: cwd, inPlace: true };
17
39
  }
18
40
 
19
41
  export interface ScaffoldRequest {
20
- /** 不给就是 my-game(占了就加序号) */
21
- id?: string;
42
+ /** 子目录名,同时是 id。不给就在 cwd 里就地生成,id 取自目录名 */
43
+ name?: string;
22
44
  title?: string;
23
- /** 目标目录。默认 <cwd>/<id> */
24
- dest?: string;
25
45
  /** package.json 里 gamekit777 的版本说明符。仓库内是 workspace:*,创作者机器上是版本号 */
26
46
  gamekitSpec?: string;
27
47
  /** 跳过 bun install */
@@ -30,6 +50,9 @@ export interface ScaffoldRequest {
30
50
 
31
51
  export interface ScaffoldResult {
32
52
  dest: string;
53
+ id: string;
54
+ /** true = 就地生成在 cwd,不用再 cd */
55
+ inPlace: boolean;
33
56
  files: number;
34
57
  installed: boolean;
35
58
  installOutput: string;
@@ -71,9 +94,7 @@ function defaultSpec(dest: string): string {
71
94
  }
72
95
 
73
96
  export async function scaffold(req: ScaffoldRequest, cwd: string): Promise<ScaffoldResult> {
74
- const id = req.id ?? defaultId(cwd);
75
- if (!ID_PATTERN.test(id)) throw new Error(`id "${id}" 要是小写短横线形式`);
76
- const dest = req.dest ?? join(cwd, id);
97
+ const { id, dest, inPlace } = resolveTarget(cwd, req.name);
77
98
  const r = scaffoldGame({ id, title: req.title ?? DEFAULT_TITLE, dest, gamekitSpec: req.gamekitSpec ?? defaultSpec(dest) });
78
99
 
79
100
  let installed = false, installOutput = '';
@@ -84,9 +105,11 @@ export async function scaffold(req: ScaffoldRequest, cwd: string): Promise<Scaff
84
105
  }
85
106
 
86
107
  return {
87
- dest, files: r.files.length, installed, installOutput,
108
+ dest, id, inPlace, files: r.files.length, installed, installOutput,
88
109
  next: [
89
- `游戏在 ${dest},是一个能跑的抛硬币。从能跑的东西开始改;名字和标题在 game.meta.json 里改。`,
110
+ inPlace
111
+ ? `就地生成在当前目录 ${dest}(id = ${id},取自目录名),不用 cd。是一个能跑的抛硬币;名字和标题在 game.meta.json 里改。`
112
+ : `游戏在 ${dest}(id = ${id}),是一个能跑的抛硬币。从能跑的东西开始改;名字和标题在 game.meta.json 里改。`,
90
113
  `先读 ${join(dest, 'CLAUDE.md')}:清单字段、round() 的硬约束、modes/book/restore 怎么选、演出侧约定。`,
91
114
  '改 src/rules/ 下的纯逻辑(零 DOM、零自取随机),改 src/components/ 下的演出。',
92
115
  '已发布过的 mode 解码逻辑不能改,要改就换 slug;结果漂移由 studio/cases.jsonl 兜。',