minecraft-dev-starter 0.0.0-stage → 0.1.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/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 +193 -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,193 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* 必需服务:会话(取工作区)与工具注册表。
|
|
19
|
+
*
|
|
20
|
+
* `webServer` 不在这里——它是可选的(纯 CLI profile 没有 web 层),
|
|
21
|
+
* 由 `apply` 里的 `ctx.inject(['webServer'], …)` 单独等。
|
|
22
|
+
*/
|
|
23
|
+
export const inject = ['sessions', 'tools']
|
|
24
|
+
|
|
25
|
+
/** 插件版本;客户端会显示,用来确认两边同一份代码。 */
|
|
26
|
+
const VERSION = '0.1.0'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('node:http').IncomingMessage} req
|
|
30
|
+
* @returns {Promise<unknown>}
|
|
31
|
+
*/
|
|
32
|
+
function readJsonBody(req) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let raw = ''
|
|
35
|
+
req.setEncoding('utf8')
|
|
36
|
+
req.on('data', (chunk) => {
|
|
37
|
+
raw += chunk
|
|
38
|
+
if (raw.length > 1024 * 1024) reject(new Error('body too large'))
|
|
39
|
+
})
|
|
40
|
+
req.on('end', () => {
|
|
41
|
+
if (raw.trim() === '') return resolve(undefined)
|
|
42
|
+
try {
|
|
43
|
+
resolve(JSON.parse(raw))
|
|
44
|
+
} catch (error) {
|
|
45
|
+
reject(error)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
req.on('error', reject)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {import('node:http').ServerResponse} res
|
|
54
|
+
* @param {number} status
|
|
55
|
+
* @param {unknown} value
|
|
56
|
+
*/
|
|
57
|
+
function writeJson(res, status, value) {
|
|
58
|
+
const body = JSON.stringify(value)
|
|
59
|
+
res.writeHead(status, {
|
|
60
|
+
'content-type': 'application/json; charset=utf-8',
|
|
61
|
+
'cache-control': 'no-store',
|
|
62
|
+
'content-length': Buffer.byteLength(body),
|
|
63
|
+
})
|
|
64
|
+
res.end(body)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 活跃会话的工作目录清单。
|
|
69
|
+
*
|
|
70
|
+
* 面板切会话时由客户端把 cwd 显式传下来;这里是"没传时"的兜底与启动校验的依据。
|
|
71
|
+
* `MCDEV_WORKSPACE` 是测试/固定工作区的逃生门。
|
|
72
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
73
|
+
* @returns {string[]}
|
|
74
|
+
*/
|
|
75
|
+
function sessionCwds(ctx) {
|
|
76
|
+
const pinned = process.env.MCDEV_WORKSPACE
|
|
77
|
+
if (typeof pinned === 'string' && pinned !== '') return [pinned]
|
|
78
|
+
/** @type {string[]} */
|
|
79
|
+
const roots = []
|
|
80
|
+
try {
|
|
81
|
+
for (const session of ctx.sessions.list()) {
|
|
82
|
+
const cwd = session?.header?.cwd
|
|
83
|
+
if (typeof cwd === 'string' && cwd !== '' && !roots.includes(cwd)) roots.push(cwd)
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* 没有会话服务时退回进程 cwd */
|
|
87
|
+
}
|
|
88
|
+
if (roots.length === 0) roots.push(process.cwd())
|
|
89
|
+
return roots
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 最近一个活跃会话的工作目录(扫描默认值)。
|
|
94
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
95
|
+
* @returns {string | undefined}
|
|
96
|
+
*/
|
|
97
|
+
function resolveWorkspaceRoot(ctx) {
|
|
98
|
+
return sessionCwds(ctx).at(-1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 部署配置默认值;patch 行的 `config` 会覆盖同名项。 */
|
|
102
|
+
const DEFAULTS = {
|
|
103
|
+
autoOpenLogTab: true,
|
|
104
|
+
readyTimeoutMs: 90_000,
|
|
105
|
+
logRingLines: 2_000,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
110
|
+
* @param {Record<string, unknown>} [config]
|
|
111
|
+
*/
|
|
112
|
+
export function apply(ctx, config = {}) {
|
|
113
|
+
const settings = { ...DEFAULTS, ...(config ?? {}) }
|
|
114
|
+
const api = createApi({
|
|
115
|
+
version: VERSION,
|
|
116
|
+
resolveRoot: () => resolveWorkspaceRoot(ctx),
|
|
117
|
+
resolveRoots: () => sessionCwds(ctx),
|
|
118
|
+
config: { autoOpenLogTab: settings.autoOpenLogTab !== false },
|
|
119
|
+
launchService: createLaunchService({
|
|
120
|
+
readyTimeoutMs: Number(settings.readyTimeoutMs) || DEFAULTS.readyTimeoutMs,
|
|
121
|
+
maxLogLines: Number(settings.logRingLines) || DEFAULTS.logRingLines,
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 面板与模型共用同一套服务;工具只是薄封装(M5)
|
|
126
|
+
applyTools(ctx, { api })
|
|
127
|
+
|
|
128
|
+
// webServer 是**可选**的:纯 CLI / headless profile 里没有它,
|
|
129
|
+
// 这时插件应当照常激活(工具可用),只是没有 HTTP 路由。
|
|
130
|
+
// 早期把它写进 inject 会让整个 profile 加载失败("pending: waiting for service")。
|
|
131
|
+
ctx.effect(() => {
|
|
132
|
+
const mount = (scoped) => {
|
|
133
|
+
const webServer = scoped?.webServer
|
|
134
|
+
if (webServer === undefined || typeof webServer.register !== 'function') return undefined
|
|
135
|
+
const dispose = webServer.register({
|
|
136
|
+
kind: 'prefix',
|
|
137
|
+
path: '/mcdev/api',
|
|
138
|
+
handler: async (req, res) => {
|
|
139
|
+
const { pathname } = new URL(req.url ?? '/', 'http://dsh.internal')
|
|
140
|
+
|
|
141
|
+
if (req.method === 'GET' && pathname === '/mcdev/api/ping') {
|
|
142
|
+
writeJson(res, 200, { ok: true, version: VERSION, pid: process.pid })
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (req.method !== 'POST') {
|
|
146
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'use POST' } })
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const method = pathname.startsWith('/mcdev/api/') ? pathname.slice('/mcdev/api/'.length) : ''
|
|
151
|
+
if (method === '' || method.includes('/')) {
|
|
152
|
+
writeJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown method "${method}"` } })
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
const handler = Object.hasOwn(api, method) ? api[method] : undefined
|
|
156
|
+
if (typeof handler !== 'function') {
|
|
157
|
+
writeJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown method "${method}"` } })
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const payload = await readJsonBody(req)
|
|
163
|
+
const result = await handler(payload)
|
|
164
|
+
writeJson(res, 200, { ok: true, result })
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
167
|
+
console.warn(`[mcdev] ${method} failed: ${message}`)
|
|
168
|
+
writeJson(res, 500, { ok: false, error: { code: 'internal', message } })
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
console.log('[mcdev] http routes mounted at /mcdev/api')
|
|
173
|
+
return () => {
|
|
174
|
+
try {
|
|
175
|
+
dispose?.()
|
|
176
|
+
} catch {
|
|
177
|
+
/* 已经在卸载 */
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof ctx.inject === 'function') {
|
|
183
|
+
return ctx.inject(['webServer'], (scoped) => mount(scoped))
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
return mount(ctx)
|
|
187
|
+
} catch {
|
|
188
|
+
return undefined
|
|
189
|
+
}
|
|
190
|
+
}, 'mcdev: /mcdev/api routes')
|
|
191
|
+
|
|
192
|
+
console.log(`[mcdev] host ready (${VERSION}) — /mcdev/api/{projects.scan,projects.detail,state.info}`)
|
|
193
|
+
}
|