minecraft-dev-starter 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.
- package/AGENTS.md +166 -0
- package/README.md +124 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +1521 -0
- package/package.json +54 -4
- package/src/client/Capsule.js +158 -0
- package/src/client/DetailTab.js +228 -0
- package/src/client/Flyout.js +191 -0
- package/src/client/LogTab.js +180 -0
- package/src/client/Skeleton.js +18 -0
- package/src/client/api.js +33 -0
- package/src/client/debug.js +27 -0
- package/src/client/format.js +89 -0
- package/src/client/h.js +8 -0
- package/src/client/icons.js +147 -0
- package/src/client/index.js +205 -0
- package/src/client/runtime.js +41 -0
- package/src/client/selection.js +154 -0
- package/src/client/state.js +233 -0
- package/src/client/store.js +34 -0
- package/src/client/styles.js +329 -0
- package/src/index.js +165 -0
- package/src/lib/api.js +186 -0
- package/src/lib/argv.js +66 -0
- package/src/lib/detect.js +460 -0
- package/src/lib/gradle.js +295 -0
- package/src/lib/java.js +204 -0
- package/src/lib/launches.js +495 -0
- package/src/lib/metadata.js +175 -0
- package/src/lib/scanner.js +131 -0
- package/src/lib/state.js +91 -0
- package/src/lib/text.js +92 -0
- package/src/lib/toml.js +281 -0
- package/src/tools.js +291 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 样式:一次性注入 <style>,**只使用 DSH 的设计 token**(`--dsw-*`)。
|
|
3
|
+
*
|
|
4
|
+
* 规则来自系统自身:颜色一律走 `--dsw-alias-*`,字号走 `--dsw-font-*`,
|
|
5
|
+
* 控件高度对齐 24/28、圆角 3/4/8。不写死任何色值——跟随主题与皮肤切换,
|
|
6
|
+
* 也不会在亮色主题下突然出现一块深色面板。
|
|
7
|
+
*
|
|
8
|
+
* 这条约束由测试守护:`test/styles.test.js` 会拒绝出现原始色值的声明。
|
|
9
|
+
* @module minecraft-dev-starter/client/styles
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const STYLE_ID = 'mcdev-styles'
|
|
13
|
+
|
|
14
|
+
export const CSS = `
|
|
15
|
+
.mcdev-anchor { position: relative; display: inline-flex; }
|
|
16
|
+
.mcdev-capsule { display: inline-flex; align-items: center; gap: 4px; }
|
|
17
|
+
|
|
18
|
+
/* 标题栏胶囊:与系统控件同高同圆角,默认无底色 */
|
|
19
|
+
.mcdev-pill {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 6px;
|
|
23
|
+
height: 24px;
|
|
24
|
+
max-width: 220px;
|
|
25
|
+
padding: 0 8px;
|
|
26
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
27
|
+
border-radius: 999px;
|
|
28
|
+
background: transparent;
|
|
29
|
+
color: var(--dsw-alias-label-primary);
|
|
30
|
+
font: var(--dsw-font-xxs-12);
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
.mcdev-pill:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
34
|
+
/* 窄的时候让工程名让步:目标名永远完整显示 */
|
|
35
|
+
.mcdev-pill-text { display: flex; align-items: center; min-width: 0; }
|
|
36
|
+
.mcdev-pill-project {
|
|
37
|
+
min-width: 0;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
text-overflow: ellipsis;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
color: var(--dsw-alias-label-secondary);
|
|
42
|
+
}
|
|
43
|
+
.mcdev-pill-sep { flex: none; padding: 0 4px; color: var(--dsw-alias-label-tertiary); }
|
|
44
|
+
.mcdev-pill-target { flex: none; white-space: nowrap; }
|
|
45
|
+
|
|
46
|
+
.mcdev-dot {
|
|
47
|
+
width: 7px;
|
|
48
|
+
height: 7px;
|
|
49
|
+
flex: none;
|
|
50
|
+
border-radius: 50%;
|
|
51
|
+
background: var(--dsw-alias-label-tertiary);
|
|
52
|
+
}
|
|
53
|
+
.mcdev-dot[data-state="ok"] { background: var(--dsw-alias-state-success-primary); }
|
|
54
|
+
.mcdev-dot[data-state="warn"] { background: var(--dsw-alias-state-warn-primary); }
|
|
55
|
+
.mcdev-dot[data-state="error"] { background: var(--dsw-alias-state-error-primary); }
|
|
56
|
+
|
|
57
|
+
/* 浮层:三层结构(表头 / 列表 / 选择区),选工程时列表高度不变 */
|
|
58
|
+
.mcdev-flyout {
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: 30px;
|
|
61
|
+
right: 0;
|
|
62
|
+
z-index: 60;
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
width: 340px;
|
|
66
|
+
max-height: 60vh;
|
|
67
|
+
padding: 0;
|
|
68
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
71
|
+
color: var(--dsw-alias-label-primary);
|
|
72
|
+
font: var(--dsw-font-xxs-12);
|
|
73
|
+
box-shadow: 0 8px 24px var(--dsw-alias-bg-mask-1);
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
.mcdev-flyout-head {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 4px;
|
|
80
|
+
flex: none;
|
|
81
|
+
padding: 10px 10px 8px;
|
|
82
|
+
border-bottom: 1px solid var(--dsw-alias-hairline);
|
|
83
|
+
}
|
|
84
|
+
.mcdev-flyout-list {
|
|
85
|
+
flex: 1 1 auto;
|
|
86
|
+
min-height: 0;
|
|
87
|
+
overflow: auto;
|
|
88
|
+
padding: 6px;
|
|
89
|
+
}
|
|
90
|
+
.mcdev-flyout-pane {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: 4px;
|
|
94
|
+
flex: none;
|
|
95
|
+
padding: 8px 10px 10px;
|
|
96
|
+
border-top: 1px solid var(--dsw-alias-hairline);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* 列表行:一行一个工程,高度恒定 */
|
|
100
|
+
.mcdev-list-row {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: space-between;
|
|
104
|
+
gap: 8px;
|
|
105
|
+
width: 100%;
|
|
106
|
+
height: 30px;
|
|
107
|
+
padding: 0 8px;
|
|
108
|
+
border: 0;
|
|
109
|
+
border-radius: 4px;
|
|
110
|
+
background: transparent;
|
|
111
|
+
color: var(--dsw-alias-label-primary);
|
|
112
|
+
font: var(--dsw-font-xxs-12);
|
|
113
|
+
text-align: left;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
}
|
|
116
|
+
.mcdev-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
117
|
+
.mcdev-list-row[data-current="true"] { background: var(--dsw-alias-interactive-bg-hover); }
|
|
118
|
+
.mcdev-list-row[data-current="true"] .mcdev-list-name { font: var(--dsw-font-xxs-strong-12); }
|
|
119
|
+
.mcdev-list-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
120
|
+
.mcdev-list-meta { flex: none; color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxxs-11); }
|
|
121
|
+
.mcdev-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
122
|
+
|
|
123
|
+
.mcdev-title { font: var(--dsw-font-xxs-strong-12); }
|
|
124
|
+
.mcdev-sub { color: var(--dsw-alias-label-secondary); }
|
|
125
|
+
.mcdev-muted { color: var(--dsw-alias-label-tertiary); }
|
|
126
|
+
.mcdev-code { font-family: var(--dsw-font-mono); font-size: 11px; }
|
|
127
|
+
.mcdev-warn { color: var(--dsw-alias-state-warn-primary); }
|
|
128
|
+
.mcdev-error { color: var(--dsw-alias-state-error-primary); }
|
|
129
|
+
|
|
130
|
+
.mcdev-row { display: flex; align-items: baseline; gap: 8px; }
|
|
131
|
+
.mcdev-key { flex: none; width: 78px; color: var(--dsw-alias-label-tertiary); }
|
|
132
|
+
|
|
133
|
+
.mcdev-mini {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 3px;
|
|
137
|
+
height: 22px;
|
|
138
|
+
padding: 0 8px;
|
|
139
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
background: transparent;
|
|
142
|
+
color: var(--dsw-alias-label-secondary);
|
|
143
|
+
font: var(--dsw-font-xxxs-11);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
}
|
|
146
|
+
.mcdev-mini:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
147
|
+
.mcdev-mini-icon { width: 22px; padding: 0; justify-content: center; }
|
|
148
|
+
|
|
149
|
+
/* 目标标签:中性徽标,不是按钮——M2 之前没有可点的动作 */
|
|
150
|
+
.mcdev-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
|
151
|
+
.mcdev-badge {
|
|
152
|
+
display: inline-flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 4px;
|
|
155
|
+
height: 20px;
|
|
156
|
+
padding: 0 6px;
|
|
157
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
158
|
+
border-radius: 4px;
|
|
159
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
160
|
+
color: var(--dsw-alias-label-secondary);
|
|
161
|
+
font: var(--dsw-font-xxxs-11);
|
|
162
|
+
}
|
|
163
|
+
.mcdev-badge[data-guessed="true"] {
|
|
164
|
+
border-style: dashed;
|
|
165
|
+
background: transparent;
|
|
166
|
+
color: var(--dsw-alias-label-tertiary);
|
|
167
|
+
}
|
|
168
|
+
.mcdev-badge-tag { color: var(--dsw-alias-label-tertiary); }
|
|
169
|
+
|
|
170
|
+
.mcdev-hint { color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxxs-11); }
|
|
171
|
+
|
|
172
|
+
/* 运行按钮:系统尺寸(24 高、4 圆角),中性底色 */
|
|
173
|
+
.mcdev-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
|
174
|
+
.mcdev-run {
|
|
175
|
+
display: inline-flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 4px;
|
|
178
|
+
height: 24px;
|
|
179
|
+
padding: 0 8px;
|
|
180
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
181
|
+
border-radius: 4px;
|
|
182
|
+
background: transparent;
|
|
183
|
+
color: var(--dsw-alias-label-primary);
|
|
184
|
+
font: var(--dsw-font-xxs-12);
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
}
|
|
187
|
+
.mcdev-run:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
188
|
+
.mcdev-run:disabled { opacity: 0.4; cursor: default; }
|
|
189
|
+
.mcdev-run[data-tone="stop"] { color: var(--dsw-alias-danger); border-color: var(--dsw-alias-danger); }
|
|
190
|
+
.mcdev-run-icon { width: 24px; padding: 0; justify-content: center; }
|
|
191
|
+
|
|
192
|
+
/* 目标单选列表:一行一个,模块名做小标题 */
|
|
193
|
+
.mcdev-options { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
|
|
194
|
+
.mcdev-option-group { display: flex; flex-direction: column; gap: 2px; }
|
|
195
|
+
.mcdev-option-module { margin-top: 4px; color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxxs-11); }
|
|
196
|
+
.mcdev-option {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 6px;
|
|
200
|
+
height: 22px;
|
|
201
|
+
padding: 0 4px;
|
|
202
|
+
border-radius: 4px;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
.mcdev-option:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
206
|
+
.mcdev-option.is-disabled { opacity: 0.5; cursor: default; }
|
|
207
|
+
.mcdev-radio { margin: 0; accent-color: var(--dsw-alias-accent); }
|
|
208
|
+
.mcdev-option-label { font: var(--dsw-font-xxs-12); }
|
|
209
|
+
|
|
210
|
+
.mcdev-status { margin-top: 6px; color: var(--dsw-alias-state-success-primary); font: var(--dsw-font-xxxs-11); }
|
|
211
|
+
.mcdev-inline-status {
|
|
212
|
+
display: inline-flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 4px;
|
|
215
|
+
margin-left: 8px;
|
|
216
|
+
color: var(--dsw-alias-state-success-primary);
|
|
217
|
+
font: var(--dsw-font-xxxs-11);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* 日志尾部:等宽、可滚,底色用系统层级 3 */
|
|
221
|
+
.mcdev-logs {
|
|
222
|
+
max-height: 220px;
|
|
223
|
+
margin: 8px 0 0;
|
|
224
|
+
padding: 8px;
|
|
225
|
+
overflow: auto;
|
|
226
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
229
|
+
color: var(--dsw-alias-label-secondary);
|
|
230
|
+
font-family: var(--dsw-font-mono);
|
|
231
|
+
font-size: 11px;
|
|
232
|
+
line-height: 1.5;
|
|
233
|
+
white-space: pre-wrap;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* 运行日志标签:顶部一条控制带,下面是整块日志 */
|
|
237
|
+
.mcdev-logtab { display: flex; flex-direction: column; height: 100%; min-height: 0; padding: 8px 10px 10px; }
|
|
238
|
+
.mcdev-log-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding-bottom: 8px; }
|
|
239
|
+
.mcdev-select {
|
|
240
|
+
height: 24px;
|
|
241
|
+
max-width: 220px;
|
|
242
|
+
padding: 0 6px;
|
|
243
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
244
|
+
border-radius: 4px;
|
|
245
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
246
|
+
color: var(--dsw-alias-label-primary);
|
|
247
|
+
font: var(--dsw-font-xxxs-11);
|
|
248
|
+
}
|
|
249
|
+
.mcdev-follow { display: inline-flex; align-items: center; gap: 4px; color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxxs-11); }
|
|
250
|
+
.mcdev-diagnosis {
|
|
251
|
+
margin-bottom: 8px;
|
|
252
|
+
padding: 8px;
|
|
253
|
+
border: 1px solid var(--dsw-alias-state-error-primary);
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
256
|
+
}
|
|
257
|
+
.mcdev-diagnosis-hint { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxs-strong-12); }
|
|
258
|
+
.mcdev-logs-full { flex: 1 1 auto; min-height: 0; max-height: none; margin: 0; }
|
|
259
|
+
|
|
260
|
+
.mcdev-section { margin-top: 12px; }
|
|
261
|
+
.mcdev-section > h4 {
|
|
262
|
+
margin: 0 0 4px;
|
|
263
|
+
color: var(--dsw-alias-label-tertiary);
|
|
264
|
+
font: var(--dsw-font-xxxs-strong-11);
|
|
265
|
+
letter-spacing: 0.04em;
|
|
266
|
+
text-transform: uppercase;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.mcdev-table { width: 100%; border-collapse: collapse; font: var(--dsw-font-xxxs-11); }
|
|
270
|
+
.mcdev-table th,
|
|
271
|
+
.mcdev-table td { padding: 3px 6px 3px 0; text-align: left; vertical-align: top; }
|
|
272
|
+
.mcdev-table thead th {
|
|
273
|
+
color: var(--dsw-alias-label-tertiary);
|
|
274
|
+
font: var(--dsw-font-xxxs-strong-11);
|
|
275
|
+
border-bottom: 1px solid var(--dsw-alias-hairline);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.mcdev-detail { padding: 12px; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-primary); }
|
|
279
|
+
.mcdev-stack { display: flex; flex-direction: column; gap: 4px; }
|
|
280
|
+
.mcdev-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
|
281
|
+
.mcdev-icon-text { display: inline-flex; align-items: center; gap: 4px; }
|
|
282
|
+
|
|
283
|
+
/* 动效:短、克制;系统开了"减少动态效果"就全部关掉 */
|
|
284
|
+
@keyframes mcdev-pop-in {
|
|
285
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
286
|
+
to { opacity: 1; transform: none; }
|
|
287
|
+
}
|
|
288
|
+
@keyframes mcdev-pulse {
|
|
289
|
+
0%, 100% { opacity: 1; }
|
|
290
|
+
50% { opacity: 0.45; }
|
|
291
|
+
}
|
|
292
|
+
.mcdev-flyout { animation: mcdev-pop-in 120ms ease-out; }
|
|
293
|
+
.mcdev-dot[data-pulse="true"] { animation: mcdev-pulse 1.6s ease-in-out infinite; }
|
|
294
|
+
.mcdev-pill,
|
|
295
|
+
.mcdev-run,
|
|
296
|
+
.mcdev-mini,
|
|
297
|
+
.mcdev-option { transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease; }
|
|
298
|
+
|
|
299
|
+
/* 骨架:扫描/加载中的占位,避免一片空白 */
|
|
300
|
+
.mcdev-skeleton { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
|
|
301
|
+
.mcdev-skeleton-row {
|
|
302
|
+
height: 10px;
|
|
303
|
+
border-radius: 3px;
|
|
304
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
305
|
+
animation: mcdev-pulse 1.4s ease-in-out infinite;
|
|
306
|
+
}
|
|
307
|
+
.mcdev-skeleton-row:nth-child(2) { width: 72%; }
|
|
308
|
+
.mcdev-skeleton-row:nth-child(3) { width: 46%; }
|
|
309
|
+
|
|
310
|
+
@media (prefers-reduced-motion: reduce) {
|
|
311
|
+
.mcdev-flyout,
|
|
312
|
+
.mcdev-dot[data-pulse="true"],
|
|
313
|
+
.mcdev-skeleton-row { animation: none; }
|
|
314
|
+
.mcdev-pill,
|
|
315
|
+
.mcdev-run,
|
|
316
|
+
.mcdev-mini,
|
|
317
|
+
.mcdev-option { transition: none; }
|
|
318
|
+
}
|
|
319
|
+
`
|
|
320
|
+
|
|
321
|
+
/** 注入一次;重复调用无副作用。 */
|
|
322
|
+
export function ensureStyles() {
|
|
323
|
+
if (typeof document === 'undefined') return
|
|
324
|
+
if (document.getElementById(STYLE_ID) !== null) return
|
|
325
|
+
const style = document.createElement('style')
|
|
326
|
+
style.id = STYLE_ID
|
|
327
|
+
style.textContent = CSS
|
|
328
|
+
document.head.appendChild(style)
|
|
329
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主入口:挂 `/mcdev/api` 路由,把请求交给方法表。
|
|
3
|
+
*
|
|
4
|
+
* 工作区根从"最近一个活跃会话的 cwd"取——HTTP 请求本身不带会话,
|
|
5
|
+
* 而模型工具那条路径有 `exec.agent.session.header.cwd`;两条路径最终都落到同一个扫描器。
|
|
6
|
+
* @module minecraft-dev-starter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createApi } from './lib/api.js'
|
|
10
|
+
import { createLaunchService } from './lib/launches.js'
|
|
11
|
+
import { applyTools } from './tools.js'
|
|
12
|
+
|
|
13
|
+
/** Cordis 插件名。 */
|
|
14
|
+
export const name = 'mcdev-host'
|
|
15
|
+
|
|
16
|
+
/** 依赖的宿主服务。 */
|
|
17
|
+
export const inject = ['webServer', 'sessions', 'tools']
|
|
18
|
+
|
|
19
|
+
/** 插件版本;客户端会显示,用来确认两边同一份代码。 */
|
|
20
|
+
const VERSION = '0.1.0'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('node:http').IncomingMessage} req
|
|
24
|
+
* @returns {Promise<unknown>}
|
|
25
|
+
*/
|
|
26
|
+
function readJsonBody(req) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let raw = ''
|
|
29
|
+
req.setEncoding('utf8')
|
|
30
|
+
req.on('data', (chunk) => {
|
|
31
|
+
raw += chunk
|
|
32
|
+
if (raw.length > 1024 * 1024) reject(new Error('body too large'))
|
|
33
|
+
})
|
|
34
|
+
req.on('end', () => {
|
|
35
|
+
if (raw.trim() === '') return resolve(undefined)
|
|
36
|
+
try {
|
|
37
|
+
resolve(JSON.parse(raw))
|
|
38
|
+
} catch (error) {
|
|
39
|
+
reject(error)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
req.on('error', reject)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {import('node:http').ServerResponse} res
|
|
48
|
+
* @param {number} status
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
*/
|
|
51
|
+
function writeJson(res, status, value) {
|
|
52
|
+
const body = JSON.stringify(value)
|
|
53
|
+
res.writeHead(status, {
|
|
54
|
+
'content-type': 'application/json; charset=utf-8',
|
|
55
|
+
'cache-control': 'no-store',
|
|
56
|
+
'content-length': Buffer.byteLength(body),
|
|
57
|
+
})
|
|
58
|
+
res.end(body)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 活跃会话的工作目录清单。
|
|
63
|
+
*
|
|
64
|
+
* 面板切会话时由客户端把 cwd 显式传下来;这里是"没传时"的兜底与启动校验的依据。
|
|
65
|
+
* `MCDEV_WORKSPACE` 是测试/固定工作区的逃生门。
|
|
66
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
function sessionCwds(ctx) {
|
|
70
|
+
const pinned = process.env.MCDEV_WORKSPACE
|
|
71
|
+
if (typeof pinned === 'string' && pinned !== '') return [pinned]
|
|
72
|
+
/** @type {string[]} */
|
|
73
|
+
const roots = []
|
|
74
|
+
try {
|
|
75
|
+
for (const session of ctx.sessions.list()) {
|
|
76
|
+
const cwd = session?.header?.cwd
|
|
77
|
+
if (typeof cwd === 'string' && cwd !== '' && !roots.includes(cwd)) roots.push(cwd)
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
/* 没有会话服务时退回进程 cwd */
|
|
81
|
+
}
|
|
82
|
+
if (roots.length === 0) roots.push(process.cwd())
|
|
83
|
+
return roots
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 最近一个活跃会话的工作目录(扫描默认值)。
|
|
88
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
89
|
+
* @returns {string | undefined}
|
|
90
|
+
*/
|
|
91
|
+
function resolveWorkspaceRoot(ctx) {
|
|
92
|
+
return sessionCwds(ctx).at(-1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 部署配置默认值;patch 行的 `config` 会覆盖同名项。 */
|
|
96
|
+
const DEFAULTS = {
|
|
97
|
+
autoOpenLogTab: true,
|
|
98
|
+
readyTimeoutMs: 90_000,
|
|
99
|
+
logRingLines: 2_000,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
104
|
+
* @param {Record<string, unknown>} [config]
|
|
105
|
+
*/
|
|
106
|
+
export function apply(ctx, config = {}) {
|
|
107
|
+
const settings = { ...DEFAULTS, ...(config ?? {}) }
|
|
108
|
+
const api = createApi({
|
|
109
|
+
version: VERSION,
|
|
110
|
+
resolveRoot: () => resolveWorkspaceRoot(ctx),
|
|
111
|
+
resolveRoots: () => sessionCwds(ctx),
|
|
112
|
+
config: { autoOpenLogTab: settings.autoOpenLogTab !== false },
|
|
113
|
+
launchService: createLaunchService({
|
|
114
|
+
readyTimeoutMs: Number(settings.readyTimeoutMs) || DEFAULTS.readyTimeoutMs,
|
|
115
|
+
maxLogLines: Number(settings.logRingLines) || DEFAULTS.logRingLines,
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// 面板与模型共用同一套服务;工具只是薄封装(M5)
|
|
120
|
+
applyTools(ctx, { api })
|
|
121
|
+
|
|
122
|
+
ctx.effect(
|
|
123
|
+
() =>
|
|
124
|
+
ctx.webServer.register({
|
|
125
|
+
kind: 'prefix',
|
|
126
|
+
path: '/mcdev/api',
|
|
127
|
+
handler: async (req, res) => {
|
|
128
|
+
const { pathname } = new URL(req.url ?? '/', 'http://dsh.internal')
|
|
129
|
+
|
|
130
|
+
if (req.method === 'GET' && pathname === '/mcdev/api/ping') {
|
|
131
|
+
writeJson(res, 200, { ok: true, version: VERSION, pid: process.pid })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (req.method !== 'POST') {
|
|
135
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'use POST' } })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const method = pathname.startsWith('/mcdev/api/') ? pathname.slice('/mcdev/api/'.length) : ''
|
|
140
|
+
if (method === '' || method.includes('/')) {
|
|
141
|
+
writeJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown method "${method}"` } })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const handler = Object.hasOwn(api, method) ? api[method] : undefined
|
|
145
|
+
if (typeof handler !== 'function') {
|
|
146
|
+
writeJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown method "${method}"` } })
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const payload = await readJsonBody(req)
|
|
152
|
+
const result = await handler(payload)
|
|
153
|
+
writeJson(res, 200, { ok: true, result })
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
156
|
+
console.warn(`[mcdev] ${method} failed: ${message}`)
|
|
157
|
+
writeJson(res, 500, { ok: false, error: { code: 'internal', message } })
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
'mcdev: /mcdev/api routes',
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
console.log(`[mcdev] host ready (${VERSION}) — /mcdev/api/{projects.scan,projects.detail,state.info}`)
|
|
165
|
+
}
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/mcdev/api` 的方法表。宿主入口只负责挂路由,具体动作在这里。
|
|
3
|
+
*
|
|
4
|
+
* 方法名与请求体形状一旦定下就是客户端合同,改名前先改客户端与文档。
|
|
5
|
+
* @module minecraft-dev-starter/lib/api
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { detectProject } from './detect.js'
|
|
9
|
+
import { discoverJdks } from './java.js'
|
|
10
|
+
import { createLaunchService } from './launches.js'
|
|
11
|
+
import { scanWorkspace } from './scanner.js'
|
|
12
|
+
|
|
13
|
+
/** 扫描结果的短缓存:面板打开/切会话会连着问几次,没必要每次重扫。 */
|
|
14
|
+
const CACHE_TTL_MS = 5000
|
|
15
|
+
|
|
16
|
+
/** 客户端诊断上报的最近若干条(只在开发期用)。 */
|
|
17
|
+
const clientReports = []
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 判断子路径是否落在父路径之下(Windows 下大小写不敏感)。
|
|
21
|
+
* @param {string} child
|
|
22
|
+
* @param {string} parent
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
function isInside(child, parent) {
|
|
26
|
+
const normalize = (value) => value.replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase()
|
|
27
|
+
const a = normalize(child)
|
|
28
|
+
const b = normalize(parent)
|
|
29
|
+
return a === b || a.startsWith(`${b}\\`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{ resolveRoot: () => Promise<string | undefined>, resolveRoots?: () => Promise<string[]>, version?: string, config?: Record<string, unknown>, launchService?: ReturnType<typeof createLaunchService> }} options
|
|
34
|
+
*/
|
|
35
|
+
export function createApi(options) {
|
|
36
|
+
/** @type {Map<string, { at: number, value: unknown }>} */
|
|
37
|
+
const cache = new Map()
|
|
38
|
+
const launches = options.launchService ?? createLaunchService()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string | undefined} requested
|
|
42
|
+
* @returns {Promise<string>}
|
|
43
|
+
*/
|
|
44
|
+
async function resolveRoot(requested) {
|
|
45
|
+
const root = requested ?? (await options.resolveRoot())
|
|
46
|
+
if (root === undefined || root === '') {
|
|
47
|
+
throw new Error('找不到工作区目录:请求里没带 root,宿主也没有活跃会话')
|
|
48
|
+
}
|
|
49
|
+
return root
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* 扫工作区:`{ root?, depth?, refresh? }` → `{ root, projects, truncated, scannedAt }`。
|
|
55
|
+
* @param {unknown} payload
|
|
56
|
+
*/
|
|
57
|
+
'projects.scan': async (payload) => {
|
|
58
|
+
const input = /** @type {{ root?: string, depth?: number, refresh?: boolean }} */ (payload ?? {})
|
|
59
|
+
const root = await resolveRoot(input.root)
|
|
60
|
+
const key = `${root}|${input.depth ?? 3}`
|
|
61
|
+
const cached = cache.get(key)
|
|
62
|
+
if (input.refresh !== true && cached !== undefined && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
63
|
+
return cached.value
|
|
64
|
+
}
|
|
65
|
+
const jdks = await discoverJdks()
|
|
66
|
+
const result = await scanWorkspace(root, { depth: input.depth, jdks })
|
|
67
|
+
console.log(`[mcdev] projects.scan ${root} → ${result.projects.length} 个工程(${result.scannedDirs} 个目录)`)
|
|
68
|
+
const value = { ...result, scannedAt: new Date().toISOString() }
|
|
69
|
+
cache.set(key, { at: Date.now(), value })
|
|
70
|
+
return value
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 单工程详情:`{ root }` → 完整画像。
|
|
75
|
+
* @param {unknown} payload
|
|
76
|
+
*/
|
|
77
|
+
'projects.detail': async (payload) => {
|
|
78
|
+
const input = /** @type {{ root?: string }} */ (payload ?? {})
|
|
79
|
+
const root = await resolveRoot(input.root)
|
|
80
|
+
return detectProject(root, { jdks: await discoverJdks() })
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/** 宿主/插件版本、工作区根与部署配置,客户端启动时打一次。 */
|
|
84
|
+
'state.info': async () => ({
|
|
85
|
+
version: options.version,
|
|
86
|
+
root: await options.resolveRoot(),
|
|
87
|
+
config: options.config ?? {},
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 启动实例:`{ root, taskPath, label?, kind?, module?, extraArgs? }`。
|
|
92
|
+
* 只允许启动"识别得出的目标",并且工程必须落在当前工作区之内。
|
|
93
|
+
* @param {unknown} payload
|
|
94
|
+
*/
|
|
95
|
+
'launch.start': async (payload) => {
|
|
96
|
+
const input = /** @type {{ root?: string, taskPath?: string, label?: string, kind?: string, module?: string, extraArgs?: string[] }} */ (payload ?? {})
|
|
97
|
+
const root = await resolveRoot(input.root)
|
|
98
|
+
// 工作区可能不止一个(每个会话一个 cwd):只要落在其中任意一个之内就放行,
|
|
99
|
+
// 否则切到另一个工程的会话后会被误判成"越界"。
|
|
100
|
+
const workspaces = options.resolveRoots === undefined ? [await resolveRoot(undefined)] : await options.resolveRoots()
|
|
101
|
+
if (!workspaces.some((workspace) => isInside(root, workspace))) {
|
|
102
|
+
throw Object.assign(new Error(`工程不在当前工作区内:${root}`), { code: 'OUTSIDE_WORKSPACE' })
|
|
103
|
+
}
|
|
104
|
+
if (typeof input.taskPath !== 'string' || input.taskPath === '') {
|
|
105
|
+
throw Object.assign(new Error('缺少 taskPath'), { code: 'BAD_REQUEST' })
|
|
106
|
+
}
|
|
107
|
+
const profile = await detectProject(root, { jdks: await discoverJdks() })
|
|
108
|
+
const targets = /** @type {Array<Record<string, any>>} */ (profile.runTargets ?? [])
|
|
109
|
+
const target = targets.find((entry) => entry.taskPath === input.taskPath)
|
|
110
|
+
if (target === undefined) {
|
|
111
|
+
throw Object.assign(new Error(`工程里没有这个可运行目标:${input.taskPath}`), { code: 'UNKNOWN_TARGET' })
|
|
112
|
+
}
|
|
113
|
+
const record = await launches.start({
|
|
114
|
+
root,
|
|
115
|
+
taskPath: target.taskPath,
|
|
116
|
+
label: input.label ?? target.label ?? target.taskPath,
|
|
117
|
+
kind: input.kind ?? target.kind ?? 'client',
|
|
118
|
+
module: input.module ?? target.module ?? '.',
|
|
119
|
+
extraArgs: Array.isArray(input.extraArgs) ? input.extraArgs.filter((entry) => typeof entry === 'string') : [],
|
|
120
|
+
})
|
|
121
|
+
console.log(`[mcdev] launch.start ${record.id} ${record.taskPath} @ ${root} (pid ${record.pid})`)
|
|
122
|
+
return record
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 停止实例:`{ id }`。
|
|
127
|
+
* @param {unknown} payload
|
|
128
|
+
*/
|
|
129
|
+
'launch.stop': async (payload) => {
|
|
130
|
+
const input = /** @type {{ id?: string }} */ (payload ?? {})
|
|
131
|
+
if (typeof input.id !== 'string' || input.id === '') {
|
|
132
|
+
throw Object.assign(new Error('缺少实例 id'), { code: 'BAD_REQUEST' })
|
|
133
|
+
}
|
|
134
|
+
const record = await launches.stop(input.id)
|
|
135
|
+
console.log(`[mcdev] launch.stop ${input.id} → ${record.status}/${record.outcome ?? '-'}`)
|
|
136
|
+
return record
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 列实例:`{ root? }`,最新的在前。
|
|
141
|
+
* @param {unknown} payload
|
|
142
|
+
*/
|
|
143
|
+
'launch.list': async (payload) => {
|
|
144
|
+
const input = /** @type {{ root?: string }} */ (payload ?? {})
|
|
145
|
+
const all = await launches.list()
|
|
146
|
+
if (typeof input.root !== 'string' || input.root === '') return all
|
|
147
|
+
const root = input.root.toLowerCase()
|
|
148
|
+
return all.filter((record) => String(record.root).toLowerCase() === root)
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 这个工程当前活跃的实例:`{ root }`。
|
|
153
|
+
* @param {unknown} payload
|
|
154
|
+
*/
|
|
155
|
+
'launch.current': async (payload) => {
|
|
156
|
+
const input = /** @type {{ root?: string }} */ (payload ?? {})
|
|
157
|
+
const root = await resolveRoot(input.root)
|
|
158
|
+
return (await launches.current(root)) ?? null
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 读日志:`{ id, offset?, limit? }`。
|
|
163
|
+
* @param {unknown} payload
|
|
164
|
+
*/
|
|
165
|
+
'launch.logs': async (payload) => {
|
|
166
|
+
const input = /** @type {{ id?: string, offset?: number, limit?: number }} */ (payload ?? {})
|
|
167
|
+
if (typeof input.id !== 'string' || input.id === '') {
|
|
168
|
+
throw Object.assign(new Error('缺少实例 id'), { code: 'BAD_REQUEST' })
|
|
169
|
+
}
|
|
170
|
+
return launches.logs(input.id, Number.isFinite(input.offset) ? Number(input.offset) : 0, Number.isFinite(input.limit) ? Number(input.limit) : 400)
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 客户端诊断上报(开发期):`{ event, detail }` → `{ ok, count }`。
|
|
175
|
+
* 只在宿主日志里留一行,并保留最近 20 条供后续排查。
|
|
176
|
+
* @param {unknown} payload
|
|
177
|
+
*/
|
|
178
|
+
'client.report': async (payload) => {
|
|
179
|
+
const input = /** @type {{ event?: string, detail?: unknown }} */ (payload ?? {})
|
|
180
|
+
clientReports.push({ at: new Date().toISOString(), event: input.event, detail: input.detail })
|
|
181
|
+
if (clientReports.length > 20) clientReports.shift()
|
|
182
|
+
console.log(`[mcdev] client: ${input.event} ${JSON.stringify(input.detail ?? {})}`)
|
|
183
|
+
return { ok: true, count: clientReports.length }
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|