gamekit777 0.1.5 → 0.1.7

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.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/create-game/scaffold.ts +81 -16
  3. package/create-game/template/base/game.meta.json +9 -0
  4. package/create-game/template/{game.ts.tmpl → base/game.ts} +15 -11
  5. package/create-game/template/{package.json.tmpl → base/package.json} +7 -3
  6. package/create-game/template/{src/components/App.svelte.tmpl → base/src/components/App.svelte} +8 -3
  7. package/create-game/template/{src/context.svelte.ts.tmpl → base/src/context.svelte.ts} +5 -5
  8. package/create-game/template/{src → base/src}/rules/const.ts +2 -1
  9. package/create-game/template/{src/rules/restore.ts.tmpl → base/src/rules/restore.ts} +7 -5
  10. package/create-game/template/{src/rules/schema.ts.tmpl → base/src/rules/schema.ts} +2 -2
  11. package/create-game/template/{src/rules/simulate.ts.tmpl → base/src/rules/simulate.ts} +2 -2
  12. package/create-game/template/{src/rules/types.ts.tmpl → base/src/rules/types.ts} +2 -2
  13. package/create-game/template/{src/state/game.svelte.ts.tmpl → base/src/state/game.svelte.ts} +14 -7
  14. package/create-game/template/{src/view/mount.svelte.ts.tmpl → base/src/view/mount.svelte.ts} +6 -4
  15. package/create-game/template/{src/view/present.ts.tmpl → base/src/view/present.ts} +2 -2
  16. package/create-game/template/{test/e2e.test.ts.tmpl → base/test/e2e.test.ts} +4 -1
  17. package/create-game/template/base/tsconfig.json +8 -0
  18. package/create-game/template/{CLAUDE.md.tmpl → overlay/CLAUDE.md.tmpl} +13 -3
  19. package/dev-host/DevShell.svelte +237 -73
  20. package/dev-host/index.ts +1 -0
  21. package/dev-host/layout.ts +80 -0
  22. package/dev-host/platform.svelte.ts +23 -4
  23. package/dev-host/run.ts +8 -1
  24. package/dev-host/theme.css +142 -10
  25. package/package.json +1 -1
  26. package/runtime/index.ts +1 -0
  27. package/runtime/meter.ts +72 -0
  28. package/runtime/registry.ts +5 -1
  29. package/sdk/host.ts +8 -0
  30. package/sdk/stage.ts +97 -11
  31. package/studio/app/App.svelte +92 -10
  32. package/studio/app/panels/PreviewPanel.svelte +4 -0
  33. package/studio/cli.ts +7 -6
  34. package/studio/preview/entry.ts +10 -1
  35. package/studio/src/engine.ts +5 -1
  36. package/studio/src/mcp.ts +9 -7
  37. package/studio/src/scaffold.ts +38 -15
  38. package/studio/src/studio-plugin.ts +29 -13
  39. package/create-game/template/game.meta.json.tmpl +0 -8
  40. package/create-game/template/tsconfig.json +0 -22
  41. /package/create-game/template/{scripts/verify.ts.tmpl → base/scripts/verify.ts} +0 -0
  42. /package/create-game/template/{src → base/src}/assets/critical/.gitkeep +0 -0
  43. /package/create-game/template/{src → base/src}/assets/lazy/.gitkeep +0 -0
  44. /package/create-game/template/{src/index.ts.tmpl → base/src/index.ts} +0 -0
  45. /package/create-game/template/{src → base/src}/styles/animations.css +0 -0
  46. /package/create-game/template/{src → base/src}/styles/global.css +0 -0
  47. /package/create-game/template/{src → base/src}/view/bridge.svelte.ts +0 -0
  48. /package/create-game/template/{vite.config.ts → base/vite.config.ts} +0 -0
  49. /package/create-game/template/{vitest.config.ts → base/vitest.config.ts} +0 -0
  50. /package/create-game/template/{_gitignore → overlay/_gitignore} +0 -0
@@ -3,7 +3,7 @@
3
3
  * 壳就是 dev-host 的 DevShell——和 `bun run dev` 看到的是同一个参考平台,同一条本地表模型:
4
4
  * 浏览器里就地建表、按权重抽行、restore 还原。studio 多接的只有三样:
5
5
  * 建好的正式产物(有就优先用)、存为 case、以及 node 侧的总线(MCP 的 preview_* 工具从那儿进来)。 */
6
- import { runDevShell, parseTable, localServer, type LoadedTable, type PlatformOf } from 'gamekit777/dev-host';
6
+ import { runDevShell, parseTable, localServer, layoutFromSearch, safeFromSearch, type LoadedTable, type PlatformOf } from 'gamekit777/dev-host';
7
7
  import type { GameModule } from 'gamekit777/sdk';
8
8
  import game from 'virtual:studio/game';
9
9
  import { api, connectBridge } from './bridge.ts';
@@ -23,6 +23,10 @@ let p: PlatformOf<object, unknown> | null = null;
23
23
  runDevShell(g, {
24
24
  // 嵌入模式:页眉和余额由 studio 页面画,这里只剩机台和机台下面的开发面板
25
25
  embedded: true,
26
+ /* 布局默认桌面,不按窗口判——iframe 有多宽由 studio 页面决定。首载从 ?layout= 读,之后由 set 命令就地切;
27
+ ?safe= 是设备预设模拟的刘海,iframe 里 env() 拿不到 */
28
+ layout: layoutFromSearch(location.search) ?? 'desktop',
29
+ simSafe: safeFromSearch(location.search) ?? undefined,
26
30
  server: localServer({ spec: g, external }),
27
31
  onPlatform: (plat) => { p = plat; },
28
32
  onSaveCase: async (h, title) => {
@@ -39,6 +43,7 @@ const snapshot = (): Record<string, unknown> => {
39
43
  return {
40
44
  config: plain(p.config), mode: p.tableMode, bet: p.bet, balance: p.balance, pnl: p.pnl,
41
45
  status: plain(p.game), lastError: p.err, tableModes: plain(p.tableModes),
46
+ layout: p.layout, stage: plain(p.stage),
42
47
  recent: p.history.slice(0, 10).map((h) => ({ n: h.n, mode: h.mode, multiplier: h.multiplier, payout: h.payout, simId: h.simId, ok: h.checks.every((c) => c.ok) })),
43
48
  };
44
49
  };
@@ -60,6 +65,10 @@ const bridge = connectBridge({
60
65
  if (cmd.clientSeed !== undefined) { plat.clientSeed = String(cmd.clientSeed); applied.push('clientSeed'); }
61
66
  if (cmd.skin !== undefined) { plat.setView({ skin: String(cmd.skin) }); applied.push('skin'); }
62
67
  if (cmd.turbo !== undefined) { plat.setView({ turbo: !!cmd.turbo }); applied.push('turbo'); }
68
+ if (cmd.layout !== undefined) {
69
+ if (cmd.layout === 'desktop' || cmd.layout === 'mobile') { plat.setLayout(cmd.layout); applied.push('layout'); }
70
+ else rejected.push({ key: 'layout', reason: '只能是 desktop / mobile' });
71
+ }
63
72
  return { applied, rejected, state: snapshot() };
64
73
  }
65
74
  case 'play': {
@@ -5,6 +5,7 @@
5
5
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
6
6
  import { join, relative } from 'node:path';
7
7
  import { Sha256 } from 'gamekit777/sdk/hash';
8
+ import type { StageInfo, StageLayout } from 'gamekit777/sdk';
8
9
  import type { Segment } from 'gamekit777/weights';
9
10
  import { Bus } from './bus.ts';
10
11
  import { buildMode, groupsSummary, listArtifacts, readArtifactBytes, sampleMode, type Artifact, type BuildModeOptions } from './build.ts';
@@ -31,6 +32,9 @@ export interface PreviewState {
31
32
  lastError: string | null;
32
33
  recent: unknown[];
33
34
  consoleErrors: string[];
35
+ /** 机台布局与交给游戏的舞台(含安全区)。页面的切换按钮和 MCP 的 preview_set 都以这里为准 */
36
+ layout: StageLayout | null;
37
+ stage: StageInfo | null;
34
38
  updatedAt: number;
35
39
  }
36
40
 
@@ -85,7 +89,7 @@ export class Engine {
85
89
  readonly assets: AssetStore;
86
90
  preview: PreviewState = {
87
91
  connected: false, config: null, mode: null, bet: null, balance: null,
88
- status: null, lastError: null, recent: [], consoleErrors: [], updatedAt: 0,
92
+ status: null, lastError: null, recent: [], consoleErrors: [], layout: null, stage: null, updatedAt: 0,
89
93
  };
90
94
  #publish = new Map<string, PublishRequest>();
91
95
  #lastFingerprint: string;
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
 
@@ -282,25 +282,27 @@ export function createMcpServer(engine: Engine): McpServer {
282
282
  });
283
283
 
284
284
  s.registerTool('preview_state', {
285
- description: '预览页的当前状态:连没连、当前 config / mode / 注额 / 余额、已加载的表、最近几局、控制台报错。',
285
+ description: '预览页的当前状态:连没连、当前 config / mode / 注额 / 余额、已加载的表、最近几局、控制台报错、机台布局(desktop/mobile)与交给游戏的舞台尺寸和安全区。',
286
286
  inputSchema: {},
287
287
  }, async () => {
288
288
  engine.touch();
289
289
  const p = engine.preview;
290
290
  if (!engine.bus.previewConnected) return out(`预览页没连着。请在浏览器打开 ${engine.urls.studio}`, { connected: false });
291
291
  return out([`## 预览(本地表模型)`, `- mode ${p.mode},注额 ${p.bet},余额 ${p.balance}`, `- config ${JSON.stringify(p.config)}`,
292
+ ...(p.stage ? [`- 布局 ${p.layout},舞台 ${p.stage.width}×${p.stage.height},安全区 上 ${p.stage.safe.top} 下 ${p.stage.safe.bottom}`] : []),
292
293
  `- 最近 ${p.recent.length} 局`, ...(p.lastError ? [`- ⚠️ 最近错误:${p.lastError}`] : []),
293
294
  ...(p.consoleErrors.length ? ['', '### 控制台报错', ...p.consoleErrors.slice(-5).map((e) => `- ${e}`)] : [])].join('\n'), p);
294
295
  });
295
296
 
296
297
  s.registerTool('preview_set', {
297
- description: '改预览的设置:config、注额、客户端种子、造型、极速。局中/自动中会被平台否决,rejected 里会说。',
298
+ description: '改预览的设置:config、注额、客户端种子、造型、极速、机台布局(desktop 桌面 / mobile 手机竖屏全屏)。局中/自动中会被平台否决,rejected 里会说。',
298
299
  inputSchema: {
299
300
  config: z.record(z.string(), z.unknown()).optional(),
300
301
  bet: z.number().int().optional().describe('最小货币单位(分)'),
301
302
  clientSeed: z.string().optional(),
302
303
  skin: z.string().optional(),
303
304
  turbo: z.boolean().optional(),
305
+ layout: z.enum(['desktop', 'mobile']).optional().describe('切机台布局。游戏不重挂,收到 host.stage 后自己重排'),
304
306
  },
305
307
  }, async (a) => {
306
308
  try {
@@ -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 兜。',
@@ -5,9 +5,7 @@
5
5
  *
6
6
  * 不往游戏目录写任何文件:两个 html 从内存吐,入口用 /@fs 指回 studio 包。
7
7
  * 不依赖游戏目录里有 index.html:脚手架出来的项目没有它,`bun run dev` 就是起 studio。 */
8
- import { readFileSync } from 'node:fs';
9
- import type { Plugin } from 'vite';
10
- import { compile } from 'svelte/compiler';
8
+ import type { DevEnvironment, Plugin, Rollup } from 'vite';
11
9
  import { API_PREFIX, createApi } from './api.ts';
12
10
  import type { Engine } from './engine.ts';
13
11
 
@@ -55,18 +53,36 @@ export function studioPlugin(o: StudioPluginOptions): Plugin {
55
53
  return null;
56
54
  },
57
55
 
58
- /* 资源库组件的 <style> 兜底。vite-plugin-svelte 把组件 CSS 存在模块信息里,样式虚拟模块
59
- (…svelte?svelte&type=style&lang.css)从那里取;组件还没编译过就先来要样式(重连、刷新、缓存未命中)时
60
- 它拿不到,退回读文件——整份 .svelte 源码被当 CSS 交给下游,namespace-css 直接炸页。
61
- 库组件在游戏根之外、内容自足,这里自己编一遍只取 CSS:scoped class 的哈希只由样式文本决定,和组件那次编译一致 */
62
- load(id) {
56
+ /* svelte 组件 <style> 的兜底。
57
+ vite-plugin-svelte 把编译出的 CSS 挂在组件模块的 meta 上,样式虚拟模块(…svelte?svelte&type=style&lang.css)
58
+ 用 getModuleInfo(不带 query 的文件名) 去取。两种情况取不到,它就放弃,vite 退回读文件——
59
+ 整份 .svelte 源码被当 CSS 交给下游:模板里的 `{#if}` 变成一条条空规则,`<style>` 里的选择器没有 hash,页面整个走样。
60
+ 1. 装成 npm 包后 gamekit777 在游戏的 node_modules 里,vite 给这类模块 id 加 ?v=<hash>,文件名对不上 id。
61
+ 仓库内是符号链接到 packages/,走 /@fs 路径没有 ?v=,所以只有创作者机器上才坏。
62
+ 2. 组件还没编译过就先来要样式(资源库现场编译、重连、刷新)。
63
+ 这里按文件名在模块图里找那次编译留下的 CSS;没有就先把组件本身 transform 一遍再找——
64
+ 不能自己 compile 一遍只取 CSS:dev 下 vite-plugin-svelte 用文件名算 scope hash(s-xxx),
65
+ 默认的按内容算(svelte-xxx),两边对不上,等于没修。 */
66
+ async load(id) {
63
67
  const m = /^([^?]+\.svelte)\?(?:.*&)?svelte(?:&.*)?&type=style(?:&|$)/.exec(id) ?? /^([^?]+\.svelte)\?(?:.*&)?type=style(?:&|$)/.exec(id);
64
68
  const file = m?.[1];
65
- if (!file || !file.startsWith(o.engine.assets.dir)) return null;
66
- try {
67
- const src = readFileSync(file, 'utf8');
68
- return { code: compile(src, { filename: file, generate: 'client', css: 'external' }).css?.code ?? '' };
69
- } catch { return null; }
69
+ if (!file) return null;
70
+ const env = this.environment;
71
+ if (env.mode !== 'dev') return null;
72
+ const dev = env as DevEnvironment;
73
+ const pick = (): { code: string; map: Rollup.SourceMapInput | undefined; moduleType: 'css' } | null => {
74
+ for (const mod of dev.moduleGraph.getModulesByFile(file) ?? []) {
75
+ // 插件写的 meta 落在 info.meta 上(vite 的 ModuleInfo 代理),mod.meta 是另一份、可能是空的
76
+ const meta = (mod.info?.meta ?? mod.meta) as { svelte?: { css?: { code?: string; map?: Rollup.SourceMapInput } } } | undefined;
77
+ const css = meta?.svelte?.css;
78
+ if (typeof css?.code === 'string') return { code: css.code, map: css.map, moduleType: 'css' };
79
+ }
80
+ return null;
81
+ };
82
+ const hit = pick();
83
+ if (hit) return hit;
84
+ try { await dev.transformRequest(`/@fs${file}`); } catch { return null; }
85
+ return pick();
70
86
  },
71
87
 
72
88
  configureServer(server) {
@@ -1,8 +0,0 @@
1
- {
2
- "id": "__ID__",
3
- "title": "__TITLE__",
4
- "version": "1.0.0",
5
- "aspect": "16:9",
6
- "orientation": "any",
7
- "fonts": []
8
- }
@@ -1,22 +0,0 @@
1
- {
2
- "extends": "@tsconfig/svelte/tsconfig.json",
3
- "compilerOptions": {
4
- "target": "ES2022",
5
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
- "module": "ESNext",
7
- "moduleResolution": "bundler",
8
- "strict": true,
9
- "noUnusedLocals": true,
10
- "noUnusedParameters": true,
11
- "noImplicitOverride": true,
12
- "noFallthroughCasesInSwitch": true,
13
- "noEmit": true,
14
- "isolatedModules": true,
15
- "verbatimModuleSyntax": true,
16
- "skipLibCheck": true,
17
- "forceConsistentCasingInFileNames": true,
18
- "allowImportingTsExtensions": true,
19
- "types": ["vite/client"]
20
- },
21
- "include": ["src", "test", "scripts", "*.ts"]
22
- }