@khorsheed/dsh-game 0.0.0-stage → 0.1.0

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 (99) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.en.md +53 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +52 -2
  5. package/cordis.patch.yml +23 -0
  6. package/lib/client.js +30348 -0
  7. package/lib/index.js +4154 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3769 -0
  10. package/lib/typert.remote-client.d.ts +101 -0
  11. package/lib/typert.remote-client.js +2082 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +42 -0
  14. package/lib/types/checks.d.ts +44 -0
  15. package/lib/types/checks.js +267 -0
  16. package/lib/types/client/DesignView.d.ts +22 -0
  17. package/lib/types/client/DesignView.js +134 -0
  18. package/lib/types/client/ElementsView.d.ts +21 -0
  19. package/lib/types/client/ElementsView.js +187 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +256 -0
  22. package/lib/types/client/PlayFrame.d.ts +54 -0
  23. package/lib/types/client/PlayFrame.js +129 -0
  24. package/lib/types/client/PlayView.d.ts +19 -0
  25. package/lib/types/client/PlayView.js +161 -0
  26. package/lib/types/client/Preview3D.d.ts +50 -0
  27. package/lib/types/client/Preview3D.js +176 -0
  28. package/lib/types/client/ProjectSwitcher.d.ts +20 -0
  29. package/lib/types/client/ProjectSwitcher.js +75 -0
  30. package/lib/types/client/Recipe3DEditor.d.ts +19 -0
  31. package/lib/types/client/Recipe3DEditor.js +44 -0
  32. package/lib/types/client/StyleGallery.d.ts +18 -0
  33. package/lib/types/client/StyleGallery.js +27 -0
  34. package/lib/types/client/TurnCard.d.ts +62 -0
  35. package/lib/types/client/TurnCard.js +100 -0
  36. package/lib/types/client/VersionsView.d.ts +17 -0
  37. package/lib/types/client/VersionsView.js +68 -0
  38. package/lib/types/client/chat-node.d.ts +42 -0
  39. package/lib/types/client/chat-node.js +77 -0
  40. package/lib/types/client/contract.d.ts +125 -0
  41. package/lib/types/client/contract.js +6 -0
  42. package/lib/types/client/definition.d.ts +22 -0
  43. package/lib/types/client/definition.js +28 -0
  44. package/lib/types/client/index.d.ts +21 -0
  45. package/lib/types/client/index.js +229 -0
  46. package/lib/types/client/locales.d.ts +376 -0
  47. package/lib/types/client/locales.js +727 -0
  48. package/lib/types/client/looks.d.ts +36 -0
  49. package/lib/types/client/looks.js +105 -0
  50. package/lib/types/client/markdown.d.ts +11 -0
  51. package/lib/types/client/markdown.js +52 -0
  52. package/lib/types/client/preset-visibility.d.ts +94 -0
  53. package/lib/types/client/preset-visibility.js +115 -0
  54. package/lib/types/client/preview-entry.d.ts +24 -0
  55. package/lib/types/client/preview-entry.js +107 -0
  56. package/lib/types/client/refs.d.ts +36 -0
  57. package/lib/types/client/refs.js +44 -0
  58. package/lib/types/client/style-samples.d.ts +5 -0
  59. package/lib/types/client/style-samples.js +196 -0
  60. package/lib/types/fsutil.d.ts +26 -0
  61. package/lib/types/fsutil.js +104 -0
  62. package/lib/types/hooks.d.ts +83 -0
  63. package/lib/types/hooks.js +108 -0
  64. package/lib/types/index.d.ts +30 -0
  65. package/lib/types/index.js +22 -0
  66. package/lib/types/kits/canvas2d.d.ts +6 -0
  67. package/lib/types/kits/canvas2d.js +60 -0
  68. package/lib/types/kits/ids.d.ts +11 -0
  69. package/lib/types/kits/ids.js +6 -0
  70. package/lib/types/kits/index.d.ts +26 -0
  71. package/lib/types/kits/index.js +28 -0
  72. package/lib/types/kits/three3d.d.ts +12 -0
  73. package/lib/types/kits/three3d.js +188 -0
  74. package/lib/types/kits/types.d.ts +52 -0
  75. package/lib/types/kits/types.js +6 -0
  76. package/lib/types/look3d.d.ts +20 -0
  77. package/lib/types/look3d.js +175 -0
  78. package/lib/types/panel-session.d.ts +21 -0
  79. package/lib/types/panel-session.js +16 -0
  80. package/lib/types/project.d.ts +132 -0
  81. package/lib/types/project.js +494 -0
  82. package/lib/types/remote.d.ts +182 -0
  83. package/lib/types/remote.js +256 -0
  84. package/lib/types/skills.d.ts +13 -0
  85. package/lib/types/skills.js +140 -0
  86. package/lib/types/store.d.ts +325 -0
  87. package/lib/types/store.js +1285 -0
  88. package/lib/types/tools.d.ts +12 -0
  89. package/lib/types/tools.js +301 -0
  90. package/lib/types/trash.d.ts +5 -0
  91. package/lib/types/trash.js +64 -0
  92. package/lib/types/types.d.ts +522 -0
  93. package/lib/types/types.js +85 -0
  94. package/lib/types/vfs.d.ts +92 -0
  95. package/lib/types/vfs.js +217 -0
  96. package/locale/en.json +6 -0
  97. package/locale/zh.json +6 -0
  98. package/package.json +132 -3
  99. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,12 @@
1
+ import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
2
+ import type { GameService } from './store.ts';
3
+ import { type ProjectView } from './types.ts';
4
+ /** The model-facing overview of a project. */
5
+ export declare function renderProject(view: ProjectView): string;
6
+ /**
7
+ * The tool definitions.
8
+ * @param store - the service core.
9
+ * @returns the two definitions, tagged.
10
+ */
11
+ export declare function gameToolDefinitions(store: GameService): ToolDefinition[];
12
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The game mode's model tools, registered by the `./agent` entry:
3
+ * `game_open` (list, switch to, create or adopt a project — the light gate
4
+ * lives here), `game_read` (the bound project's state), `game_propose` (look
5
+ * variants) and `game_publish` (run the checks now and put a passing build
6
+ * into the play area). There is no write tool: the agent writes project files
7
+ * with the ordinary fs tools. The checks also run by themselves at the end of
8
+ * every turn — but a turn can stay open for a long time (asking the user a
9
+ * question does not close it), so `game_publish` is how a build reaches the
10
+ * play area before the agent hands the user something to play.
11
+ *
12
+ * Origin-tagged with the community `Symbol.for('dsh.tool.origin')` key (the
13
+ * catalog's documented no-import path).
14
+ * @module @khorsheed/dsh-game/agent
15
+ */
16
+ import { isAbsolute, join } from 'node:path';
17
+ import { defineTool } from '@deepseek-ai/dsh-tools';
18
+ import { artStyleLabel, lookText } from "./project.js";
19
+ import { ART_STYLES, ART_STYLES_3D, CHAPTERS } from "./types.js";
20
+ /** The community tool-origin tag. */
21
+ function tagOrigin(definition) {
22
+ ;
23
+ definition[Symbol.for('dsh.tool.origin')] = { channel: 'plugin', owner: '@khorsheed/dsh-game' };
24
+ return definition;
25
+ }
26
+ const STATUS_LABEL = { todo: '未写', draft: '草稿', done: '定稿' };
27
+ /** The model-facing overview of a project. */
28
+ export function renderProject(view) {
29
+ const lines = [`项目《${view.title}》(${view.id})`, `目录:${view.root}`];
30
+ if (view.manifest === null)
31
+ lines.push(`清单有问题:${view.manifestError ?? '读不到 game.json'}`);
32
+ else {
33
+ lines.push(`pitch:${view.manifest.pitch}`, `运行时:${view.manifest.runtime?.kit === '3d-three' ? '3D(3d-three 套件,用 dsh3d)' : '2D 画布'} 元素类型:${view.manifest.elementTypes.join('、')} 美术风格:${artStyleLabel(view.manifest.artStyle)} 自动确认:${view.manifest.autoConfirm ? '开' : '关'}`);
34
+ if (view.manifest.pillars.length > 0)
35
+ lines.push(`支柱:${view.manifest.pillars.join(';')}`);
36
+ if (view.manifest.params.length > 0) {
37
+ lines.push(`参数(game.json,游戏每帧从 window.dshParams 读):${view.manifest.params.map(p => {
38
+ const live = view.paramOverrides[p.key];
39
+ return `${p.label}(${p.key})=${p.value}${live === undefined ? '' : `,用户试玩值 ${live}`}`;
40
+ }).join(';')}`);
41
+ }
42
+ if (view.restyleTo !== undefined)
43
+ lines.push(`用户要求换成「${artStyleLabel(view.restyleTo)}」风格:重画所有外观、更新 game.json 的 artStyle 与 palette,下个通过的版本会作为候选等用户确认。`);
44
+ }
45
+ lines.push(`策划:${view.chapters.map(c => `${c.title}${c.generated ? '' : `(${STATUS_LABEL[c.status]})`}`).join('、')}`);
46
+ const byType = new Map();
47
+ for (const e of view.elements)
48
+ byType.set(e.type, (byType.get(e.type) ?? 0) + 1);
49
+ lines.push(`元素:${view.elements.length === 0 ? '还没有' : [...byType].map(([t, n]) => `${t} ${n}`).join('、')}${view.elements.length === 0 ? '' : `(${view.elements.map(e => `${e.name}=${e.id}${e.look === null && e.look3d === null ? '' : '·有外观'}`).join('、')})`}`);
50
+ if (view.pending.length > 0) {
51
+ lines.push(`用户在面板里改了(已写进项目文件,以文件为准,别改回去):${view.pending.map(p => p.text).join(';')}`);
52
+ }
53
+ if (view.uncommitted.length > 0) {
54
+ lines.push(`其中还没提交成版本的(试玩区仍是旧版,用户会在面板里提交;你发版时会一起带上):${view.uncommitted.map(p => p.text).join('、')}`);
55
+ }
56
+ const open = view.proposals.filter(p => p.picked === undefined);
57
+ if (open.length > 0)
58
+ lines.push(`等用户挑选的外观方案:${open.map(p => `${p.elementId}(${p.variants.length} 个)`).join('、')}`);
59
+ for (const f of view.elementErrors.slice(0, 5))
60
+ lines.push(` ✗ ${f.where}:${f.message}`);
61
+ lines.push(view.hasEntry ? '入口:index.html 已有' : '入口:还没有 index.html(写好它和 window.dshGame 后才会检查和进试玩区)');
62
+ const byLabel = (by) => (by === 'user' ? '(用户发布,用户认可过的版本)' : by === 'agent' ? '(你发布)' : by === 'turn' ? '(回合结束自动发布)' : '');
63
+ lines.push(`试玩区:${view.current === null ? '还没有版本' : `v${view.current.version}${byLabel(view.current.by)}`}${view.candidate === null ? '' : `;待确认 v${view.candidate.version}${byLabel(view.candidate.by)}`}`);
64
+ const check = view.lastCheck;
65
+ if (check !== null) {
66
+ lines.push(check.ok
67
+ ? `最近检查:通过(${check.passed} 个用例${check.skipped === undefined ? '' : `;${check.skipped}`})`
68
+ : `最近检查:未通过${check.gaveUp === true ? ',已停止自动修复' : ''}`);
69
+ if (!check.ok)
70
+ for (const f of check.failures.slice(0, 6))
71
+ lines.push(` ✗ ${f.where}:${f.message}`);
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+ /**
76
+ * The tool definitions.
77
+ * @param store - the service core.
78
+ * @returns the two definitions, tagged.
79
+ */
80
+ export function gameToolDefinitions(store) {
81
+ const listText = async (sessionId) => {
82
+ const projects = await store.listProjects();
83
+ const bound = sessionId === undefined ? undefined : await store.boundProject(sessionId);
84
+ const lines = projects.length === 0
85
+ ? ['还没有游戏项目。']
86
+ : ['游戏项目:', ...projects.map(p => `- ${p.id}《${p.title}》${p.id === bound?.id ? '(本会话)' : ''} ${p.root}`)];
87
+ lines.push('', `2D 美术风格示例(让用户挑):${ART_STYLES.map(s => `${s.id}=${s.label}(${s.look})`).join(';')};或 custom:描述。`);
88
+ lines.push(`3D 美术风格示例(runtime=3d 时):${ART_STYLES_3D.map(s => `${s.id}=${s.label}(${s.look})`).join(';')};或 custom:描述。`);
89
+ return lines.join('\n');
90
+ };
91
+ const open = defineTool({
92
+ name: 'game_open',
93
+ description: 'Choose this session\'s game project. No arguments: list projects and the art-style examples. project: switch to '
94
+ + 'one (title or id). create: make a new project in the session workspace — requires title, pitch, elementTypes and '
95
+ + 'artStyle the user agreed to (the answer lists what is missing); runtime: 3d makes a three.js project (default 2d). '
96
+ + 'path: adopt an existing folder with a game.json. '
97
+ + 'The right-sidebar game tab follows. Load the gamedev skill before writing game code.',
98
+ parameters: {
99
+ project: { type: 'string', description: 'Project title or id to switch to.' },
100
+ create: { type: 'string', description: 'Title of a new project.' },
101
+ pitch: { type: 'string', description: 'create: one sentence — what the player does and why it is fun.' },
102
+ elementTypes: { type: 'string', description: 'create: element categories, separated by 、 (e.g. 角色、敌人、道具).' },
103
+ artStyle: { type: 'string', description: 'create: an art-style id from the list (2D or 3D list to match runtime), or custom:<description>.' },
104
+ runtime: { type: 'string', description: 'create: 2d (default) or 3d — 3d pins the three.js kit (dsh3d runtime).' },
105
+ pillars: { type: 'string', description: 'create: optional design pillars, separated by ;.' },
106
+ path: { type: 'string', description: 'Adopt this existing project folder (absolute, or relative to the workspace).' },
107
+ },
108
+ output: {
109
+ schema: { type: 'string' },
110
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
111
+ },
112
+ execute: async (args, exec) => {
113
+ const session = exec.agent?.session;
114
+ const create = args.create?.trim() ?? '';
115
+ const wanted = args.project?.trim() ?? '';
116
+ const path = args.path?.trim() ?? '';
117
+ if (create === '' && wanted === '' && path === '')
118
+ return listText(session?.id);
119
+ if (session === undefined)
120
+ return '失败:这次调用没有会话。';
121
+ if (create !== '') {
122
+ const created = await store.create({
123
+ title: create, pitch: args.pitch, elementTypes: args.elementTypes, artStyle: args.artStyle, runtime: args.runtime, pillars: args.pillars?.split(/[;;\n]/),
124
+ }, session);
125
+ if (!created.ok) {
126
+ return 'missing' in created
127
+ ? `还不能建项目,缺:\n${created.missing.map(m => `- ${m}`).join('\n')}\n先和用户把这些定下来(给选项让用户挑),再调用 create。`
128
+ : `失败:${created.error}`;
129
+ }
130
+ return `完成:已建项目《${created.project.title}》(${created.project.id}),目录 ${created.project.root},本会话已绑定。`
131
+ + '\n已写 game.json 与 design/pillars.md。接下来:load gamedev 技能,写 index.html + src/ 与 window.dshGame,并一起写 tests/。';
132
+ }
133
+ if (path !== '') {
134
+ const cwd = session.header?.cwd ?? '';
135
+ const root = isAbsolute(path) ? path : join(cwd, path);
136
+ const adopted = await store.adopt(root);
137
+ if (!adopted.ok)
138
+ return adopted.error === 'not-found' ? `失败:${root} 下没有 game.json。` : `失败:${root} 的 game.json 读不了。`;
139
+ await store.bind(session.id, adopted.project.id);
140
+ return `完成:已登记并切到《${adopted.project.title}》(${adopted.project.id})。`;
141
+ }
142
+ const projects = await store.listProjects();
143
+ const exact = projects.filter(p => p.id === wanted || p.title === wanted);
144
+ const pool = exact.length > 0 ? exact : projects.filter(p => p.title.includes(wanted));
145
+ if (pool.length === 0)
146
+ return `失败:没有叫「${wanted}」的项目。\n${await listText(session.id)}`;
147
+ if (pool.length > 1)
148
+ return `「${wanted}」对应多个项目,请问用户是哪一个:${pool.map(p => `${p.id}《${p.title}》`).join('、')}`;
149
+ const target = pool[0];
150
+ const bound = await store.bind(session.id, target.id);
151
+ if (!bound.ok)
152
+ return `失败:${bound.error}`;
153
+ const view = await store.view(target.id);
154
+ return view === undefined ? `完成:已切到《${target.title}》。` : `完成:已切到这个项目。\n${renderProject(view)}`;
155
+ },
156
+ });
157
+ const read = defineTool({
158
+ name: 'game_read',
159
+ description: 'Read this session\'s game project: no arguments gives the overview (manifest, design chapters and their status, '
160
+ + 'elements, the last end-of-turn check, the deployed version). chapter: one design chapter in full (id: '
161
+ + `${CHAPTERS.map(c => c.id).join(', ')}).`,
162
+ parameters: {
163
+ chapter: { type: 'string', description: 'A design chapter id to read in full.' },
164
+ element: { type: 'string', description: 'An element id: its fields and its look as pixel rows.' },
165
+ },
166
+ output: {
167
+ schema: { type: 'string' },
168
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
169
+ },
170
+ execute: async (args, exec) => {
171
+ const session = exec.agent?.session;
172
+ if (session === undefined)
173
+ return '失败:这次调用没有会话。';
174
+ const row = await store.boundProject(session.id);
175
+ if (row === undefined)
176
+ return `本会话还没有游戏项目。用 game_open 选一个或新建。\n${await listText(session.id)}`;
177
+ const view = await store.view(row.id);
178
+ if (view === undefined)
179
+ return '失败:项目登记丢了,用 game_open 重新选。';
180
+ const elementId = args.element?.trim() ?? '';
181
+ if (elementId !== '') {
182
+ const element = view.elements.find(e => e.id === elementId || e.name === elementId);
183
+ if (element === undefined)
184
+ return `没有元素「${elementId}」。有:${view.elements.map(e => `${e.id}(${e.name})`).join('、') || '(无)'}`;
185
+ const lines = [`元素「${element.name}」id=${element.id} type=${element.type} 文件 ${element.file}`,
186
+ `字段:${Object.entries(element.stats).map(([k, v]) => `${k}=${String(v)}`).join('、') || '(无)'}`,
187
+ element.lore === '' ? '设定:还没写(lore 字段)。' : `设定(lore):${element.lore}`];
188
+ lines.push(element.look3d !== null ? `外观(3D look):${JSON.stringify(element.look3d)}`
189
+ : element.look === null ? '外观:还没有 look(画面里是代码画的或色块)。' : `外观(look):\n${lookText(element.look)}`);
190
+ const proposal = view.proposals.find(p => p.elementId === element.id);
191
+ if (proposal !== undefined)
192
+ lines.push(proposal.picked === undefined ? `有 ${proposal.variants.length} 个外观方案等用户挑。` : `用户上次挑了方案「${proposal.variants[proposal.picked]?.label ?? ''}」。`);
193
+ return lines.join('\n');
194
+ }
195
+ const wanted = args.chapter?.trim() ?? '';
196
+ if (wanted === '') {
197
+ // The overview shows the pending panel changes once; after that they are not news.
198
+ await store.clearPending(row.id);
199
+ return renderProject(view);
200
+ }
201
+ const chapter = view.chapters.find(c => c.id === wanted || c.title === wanted);
202
+ if (chapter === undefined)
203
+ return `没有章节「${wanted}」。可选:${view.chapters.map(c => `${c.id}(${c.title})`).join('、')}`;
204
+ if (chapter.text === '')
205
+ return `「${chapter.title}」还是空的${chapter.generated ? '(自动生成的章节)' : `,写在 ${join(view.root, chapter.file)}`}。`;
206
+ return `${chapter.generated ? '(自动生成)' : `文件:${join(view.root, chapter.file)} 状态:${STATUS_LABEL[chapter.status]}`}\n\n${chapter.text}`;
207
+ },
208
+ });
209
+ const propose = defineTool({
210
+ name: 'game_propose',
211
+ description: 'Offer the user up to 3 appearance variants for one element; they appear side by side in the game tab and the user '
212
+ + 'picks one (picking applies it — never write the look yourself while variants are open). variants: a JSON array of '
213
+ + '{label, note, look} where look = {palette: ["#rrggbb", …], states: {idle: ["..00..", …], move: […]}}; each row is a '
214
+ + 'string, "." is transparent, 0-9a-z index the palette; all rows of a state are the same width (16×16 is typical).',
215
+ parameters: {
216
+ element: { type: 'string', required: true, description: 'The element id.' },
217
+ request: { type: 'string', description: 'What the user asked for, in their words.' },
218
+ variants: { type: 'string', required: true, description: 'JSON array of up to 3 {label, note, look}.' },
219
+ },
220
+ output: {
221
+ schema: { type: 'string' },
222
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
223
+ },
224
+ execute: async (args, exec) => {
225
+ const session = exec.agent?.session;
226
+ if (session === undefined)
227
+ return '失败:这次调用没有会话。';
228
+ let variants = args.variants;
229
+ if (typeof variants === 'string') {
230
+ try {
231
+ variants = JSON.parse(variants);
232
+ }
233
+ catch (error) {
234
+ return `失败:variants 不是合法 JSON(${error.message})。`;
235
+ }
236
+ }
237
+ if (!Array.isArray(variants) || variants.length === 0)
238
+ return '失败:variants 要是 1–3 个方案的数组。';
239
+ const result = await store.proposeLooks(session.id, (args.element ?? '').trim(), args.request ?? '', variants.map(v => (v !== null && typeof v === 'object' ? v : { look: null })));
240
+ if (!result.ok)
241
+ return `失败:${result.error}`;
242
+ return `完成:「${result.name}」的 ${result.count} 个外观方案已放到右栏「元素」里等用户挑;挑中的会直接替换进游戏,下一轮你会收到用户的选择。`;
243
+ },
244
+ });
245
+ const publish = defineTool({
246
+ name: 'game_publish',
247
+ description: 'Run the checks on this session\'s game project now (data validation → load and a 2-second smoke run → tests/) and, '
248
+ + 'when they pass, cut a new version into the play area (or a candidate when auto-confirm is off). Call it whenever a '
249
+ + 'playable build is ready and ALWAYS before asking the user to play or try something: asking a question does not end '
250
+ + 'the turn, so without it the play area still shows the previous version. A failure lists what to fix.',
251
+ parameters: {},
252
+ output: {
253
+ schema: { type: 'string' },
254
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
255
+ },
256
+ execute: async (_args, exec) => {
257
+ const session = exec.agent?.session;
258
+ if (session === undefined)
259
+ return '失败:这次调用没有会话。';
260
+ const row = await store.boundProject(session.id);
261
+ if (row === undefined)
262
+ return '本会话还没有游戏项目。用 game_open 选一个或新建。';
263
+ // No fix-round cap here: an explicit publish never marks a build as given up.
264
+ const outcome = await store.checkTurn(session, 0, Number.POSITIVE_INFINITY, 'agent');
265
+ const view = await store.view(row.id);
266
+ const liveVersion = view?.current?.version;
267
+ const live = liveVersion === undefined ? '试玩区还没有版本' : `试玩区仍是 v${liveVersion}`;
268
+ if (outcome.kind === 'fail') {
269
+ const failures = outcome.record.failures;
270
+ return [
271
+ `没通过检查,没有发版(${live}):`,
272
+ ...failures.slice(0, 8).map(f => `- ${f.where}:${f.message}`),
273
+ ...(failures.length > 8 ? [`- ……另有 ${failures.length - 8} 项`] : []),
274
+ ...(outcome.record.skipped === undefined ? [] : [`(${outcome.record.skipped})`]),
275
+ '修好再调用 game_publish;别为了过检查删测试。修好之前不要请用户试玩。',
276
+ ].join('\n');
277
+ }
278
+ if (outcome.kind === 'pass') {
279
+ const tail = outcome.record.skipped === undefined ? '' : `(${outcome.record.skipped})`;
280
+ if (outcome.deployed !== null)
281
+ return `完成:v${outcome.deployed.version} 通过检查${outcome.record.passed > 0 ? `(${outcome.record.passed} 个用例)` : ''},已进试玩区,可以请用户试玩了。${tail}`;
282
+ if (outcome.candidate !== null)
283
+ return `完成:v${outcome.candidate.version} 通过检查,作为候选等用户在右栏确认(自动上线关着或是换风格的版本);确认后才进试玩区。${tail}`;
284
+ return `检查通过,但这个会话是只读模式,没有生成版本。${tail}`;
285
+ }
286
+ // Skipped: no entry yet, or the build is the one already checked.
287
+ if (view === undefined)
288
+ return '失败:项目登记丢了,用 game_open 重新选。';
289
+ if (!view.hasEntry)
290
+ return '还没有 index.html,没东西可发。写好入口和 window.dshGame(并写 tests/)再调用。';
291
+ const check = view.lastCheck;
292
+ if (check !== null && !check.ok) {
293
+ return [`这一版和上次检查时一模一样,上次没通过(${live}):`,
294
+ ...check.failures.slice(0, 8).map(f => `- ${f.where}:${f.message}`), '修好再调用 game_publish。'].join('\n');
295
+ }
296
+ const pending = view.candidate === null ? '' : `;候选 v${view.candidate.version} 等用户确认`;
297
+ return `没有新改动:这一版已经检查过,${liveVersion === undefined ? '试玩区还没有版本' : `试玩区是 v${liveVersion}`}${pending}。`;
298
+ },
299
+ });
300
+ return [tagOrigin(open), tagOrigin(read), tagOrigin(propose), tagOrigin(publish)];
301
+ }
@@ -0,0 +1,5 @@
1
+ /** Moves a folder to the trash; resolves with where it went, rejects with a user-facing reason. */
2
+ export type TrashMover = (folder: string) => Promise<string>;
3
+ /** The system trash mover for this platform (`home` is a test seam). */
4
+ export declare function systemTrash(home?: string, platform?: NodeJS.Platform): TrashMover;
5
+ //# sourceMappingURL=trash.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Moving a deleted project folder to the system trash, so a delete from the
3
+ * panel is complete (gone from the workspace and the plugin) yet recoverable.
4
+ *
5
+ * - macOS: a rename into `~/.Trash`, de-duplicated with a timestamp suffix.
6
+ * - Linux: the freedesktop trash (`$XDG_DATA_HOME/Trash`, default
7
+ * `~/.local/share/Trash`): the folder goes to `files/`, a `.trashinfo`
8
+ * record to `info/` so the file manager can restore it to where it was.
9
+ * - Elsewhere, or across volumes (a rename cannot cross devices), the move is
10
+ * refused and nothing is touched: the panel then offers "remove from the
11
+ * list only". A project folder is never deleted outright.
12
+ * @module @khorsheed/dsh-game
13
+ */
14
+ import { mkdir, rename, stat, writeFile } from 'node:fs/promises';
15
+ import { homedir } from 'node:os';
16
+ import { basename, join } from 'node:path';
17
+ /** A free name inside `dir` for `name`, suffixed with the time when taken. */
18
+ async function freeName(dir, name) {
19
+ const taken = async (candidate) => stat(join(dir, candidate)).then(() => true, () => false);
20
+ if (!await taken(name))
21
+ return name;
22
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
23
+ for (let n = 0;; n++) {
24
+ const candidate = `${name} ${stamp}${n === 0 ? '' : `-${n}`}`;
25
+ if (!await taken(candidate))
26
+ return candidate;
27
+ }
28
+ }
29
+ /** Rename, turning a cross-device failure into the refusal the panel explains. */
30
+ async function move(from, to) {
31
+ try {
32
+ await rename(from, to);
33
+ }
34
+ catch (error) {
35
+ if (error.code === 'EXDEV')
36
+ throw new Error('项目和废纸篓不在同一块磁盘上,没法移过去');
37
+ throw error;
38
+ }
39
+ }
40
+ /** The system trash mover for this platform (`home` is a test seam). */
41
+ export function systemTrash(home = homedir(), platform = process.platform) {
42
+ if (platform === 'darwin') {
43
+ return async (folder) => {
44
+ const dir = join(home, '.Trash');
45
+ await mkdir(dir, { recursive: true });
46
+ const target = join(dir, await freeName(dir, basename(folder)));
47
+ await move(folder, target);
48
+ return target;
49
+ };
50
+ }
51
+ if (platform === 'linux') {
52
+ return async (folder) => {
53
+ const root = join(process.env.XDG_DATA_HOME || join(home, '.local', 'share'), 'Trash');
54
+ await mkdir(join(root, 'files'), { recursive: true });
55
+ await mkdir(join(root, 'info'), { recursive: true });
56
+ const name = await freeName(join(root, 'files'), basename(folder));
57
+ const date = new Date().toISOString().slice(0, 19);
58
+ await writeFile(join(root, 'info', `${name}.trashinfo`), `[Trash Info]\nPath=${encodeURI(folder)}\nDeletionDate=${date}\n`);
59
+ await move(folder, join(root, 'files', name));
60
+ return join(root, 'files', name);
61
+ };
62
+ }
63
+ return async () => { throw new Error('这个系统上插件不会动项目文件夹'); };
64
+ }