batch-run-local 1.1.2 → 1.2.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/dist/cli/index.js +169 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/web/index.html +5 -0
- package/dist/web/main.js +416 -64
- package/dist/web/style.css +103 -0
- package/package.json +9 -1
package/dist/cli/index.js
CHANGED
|
@@ -13,6 +13,12 @@ import Conf from "conf";
|
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
14
14
|
import { access } from "fs/promises";
|
|
15
15
|
import { resolve } from "path";
|
|
16
|
+
|
|
17
|
+
// src/shared/types.ts
|
|
18
|
+
var UNGROUPED_TAG = "";
|
|
19
|
+
var UNGROUPED_TAG_LABEL = "\u672A\u5206\u7EC4";
|
|
20
|
+
|
|
21
|
+
// src/config/store.ts
|
|
16
22
|
var defaultSettings = {
|
|
17
23
|
shell: "git-bash",
|
|
18
24
|
port: 1234
|
|
@@ -24,13 +30,85 @@ var store = new Conf({
|
|
|
24
30
|
settings: defaultSettings
|
|
25
31
|
}
|
|
26
32
|
});
|
|
33
|
+
function normalizeTag(tag) {
|
|
34
|
+
const trimmed = tag?.trim();
|
|
35
|
+
if (!trimmed || trimmed === UNGROUPED_TAG_LABEL) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
return trimmed;
|
|
39
|
+
}
|
|
40
|
+
function getProjectTagKey(project) {
|
|
41
|
+
return project.tag ?? UNGROUPED_TAG;
|
|
42
|
+
}
|
|
43
|
+
function collectTagKeysFromProjects(projects) {
|
|
44
|
+
const keys = [];
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
for (const project of projects) {
|
|
47
|
+
const key = getProjectTagKey(project);
|
|
48
|
+
if (!seen.has(key)) {
|
|
49
|
+
seen.add(key);
|
|
50
|
+
keys.push(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return keys;
|
|
54
|
+
}
|
|
55
|
+
function groupProjectsByTag(projects) {
|
|
56
|
+
const groups = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const project of projects) {
|
|
58
|
+
const key = getProjectTagKey(project);
|
|
59
|
+
const group = groups.get(key);
|
|
60
|
+
if (group) {
|
|
61
|
+
group.push(project);
|
|
62
|
+
} else {
|
|
63
|
+
groups.set(key, [project]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
function flattenProjectsByTagOrder(projects, tagOrder) {
|
|
69
|
+
const grouped = groupProjectsByTag(projects);
|
|
70
|
+
const nextProjects = [];
|
|
71
|
+
for (const tagKey of tagOrder) {
|
|
72
|
+
const group = grouped.get(tagKey);
|
|
73
|
+
if (group) {
|
|
74
|
+
nextProjects.push(...group);
|
|
75
|
+
grouped.delete(tagKey);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const group of grouped.values()) {
|
|
79
|
+
nextProjects.push(...group);
|
|
80
|
+
}
|
|
81
|
+
return nextProjects;
|
|
82
|
+
}
|
|
83
|
+
function syncTagOrder(projects, preferredOrder) {
|
|
84
|
+
const projectTagKeys = collectTagKeysFromProjects(projects);
|
|
85
|
+
const projectTagKeySet = new Set(projectTagKeys);
|
|
86
|
+
const sourceOrder = preferredOrder ?? store.get("tagOrder") ?? [];
|
|
87
|
+
const nextOrder = [];
|
|
88
|
+
const seen = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const tagKey of sourceOrder) {
|
|
90
|
+
if (!seen.has(tagKey) && projectTagKeySet.has(tagKey)) {
|
|
91
|
+
seen.add(tagKey);
|
|
92
|
+
nextOrder.push(tagKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const tagKey of projectTagKeys) {
|
|
96
|
+
if (!seen.has(tagKey)) {
|
|
97
|
+
seen.add(tagKey);
|
|
98
|
+
nextOrder.push(tagKey);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
store.set("tagOrder", nextOrder);
|
|
102
|
+
return nextOrder;
|
|
103
|
+
}
|
|
27
104
|
function getConfig() {
|
|
28
105
|
return {
|
|
29
106
|
projects: store.get("projects") ?? [],
|
|
30
107
|
settings: {
|
|
31
108
|
...defaultSettings,
|
|
32
109
|
...store.get("settings") ?? {}
|
|
33
|
-
}
|
|
110
|
+
},
|
|
111
|
+
tagOrder: store.get("tagOrder")
|
|
34
112
|
};
|
|
35
113
|
}
|
|
36
114
|
function getProjects() {
|
|
@@ -39,6 +117,12 @@ function getProjects() {
|
|
|
39
117
|
function getProject(projectId) {
|
|
40
118
|
return getProjects().find((project) => project.id === projectId);
|
|
41
119
|
}
|
|
120
|
+
function getTagOrder() {
|
|
121
|
+
return syncTagOrder(getProjects());
|
|
122
|
+
}
|
|
123
|
+
function getTags() {
|
|
124
|
+
return getTagOrder().filter((tagKey) => tagKey !== UNGROUPED_TAG);
|
|
125
|
+
}
|
|
42
126
|
async function addProject(input) {
|
|
43
127
|
const root = resolve(input.root);
|
|
44
128
|
await access(root);
|
|
@@ -47,6 +131,7 @@ async function addProject(input) {
|
|
|
47
131
|
id: randomUUID(),
|
|
48
132
|
name: input.name.trim(),
|
|
49
133
|
root,
|
|
134
|
+
tag: normalizeTag(input.tag),
|
|
50
135
|
createdAt: now,
|
|
51
136
|
updatedAt: now
|
|
52
137
|
};
|
|
@@ -54,7 +139,9 @@ async function addProject(input) {
|
|
|
54
139
|
if (projects.some((item) => item.root.toLowerCase() === root.toLowerCase())) {
|
|
55
140
|
throw new Error("\u8BE5\u9879\u76EE\u8DEF\u5F84\u5DF2\u7ECF\u5B58\u5728");
|
|
56
141
|
}
|
|
57
|
-
|
|
142
|
+
const nextProjects = [...projects, project];
|
|
143
|
+
store.set("projects", nextProjects);
|
|
144
|
+
syncTagOrder(nextProjects);
|
|
58
145
|
return project;
|
|
59
146
|
}
|
|
60
147
|
async function updateProject(projectId, input) {
|
|
@@ -72,17 +159,20 @@ async function updateProject(projectId, input) {
|
|
|
72
159
|
...projects[projectIndex],
|
|
73
160
|
name: input.name.trim(),
|
|
74
161
|
root,
|
|
162
|
+
tag: normalizeTag(input.tag),
|
|
75
163
|
updatedAt: Date.now()
|
|
76
164
|
};
|
|
77
165
|
const nextProjects = [...projects];
|
|
78
166
|
nextProjects[projectIndex] = nextProject;
|
|
79
167
|
store.set("projects", nextProjects);
|
|
168
|
+
syncTagOrder(nextProjects);
|
|
80
169
|
return nextProject;
|
|
81
170
|
}
|
|
82
171
|
function removeProject(projectId) {
|
|
83
172
|
const projects = getProjects();
|
|
84
173
|
const nextProjects = projects.filter((project) => project.id !== projectId);
|
|
85
174
|
store.set("projects", nextProjects);
|
|
175
|
+
syncTagOrder(nextProjects);
|
|
86
176
|
return nextProjects.length !== projects.length;
|
|
87
177
|
}
|
|
88
178
|
function reorderProjects(projectIds) {
|
|
@@ -100,6 +190,27 @@ function reorderProjects(projectIds) {
|
|
|
100
190
|
return project;
|
|
101
191
|
});
|
|
102
192
|
store.set("projects", nextProjects);
|
|
193
|
+
syncTagOrder(nextProjects);
|
|
194
|
+
return nextProjects;
|
|
195
|
+
}
|
|
196
|
+
function reorderTagGroups(tagKeys) {
|
|
197
|
+
const projects = getProjects();
|
|
198
|
+
const projectTagKeys = collectTagKeysFromProjects(projects);
|
|
199
|
+
const uniqueTagKeys = new Set(tagKeys);
|
|
200
|
+
if (uniqueTagKeys.size !== tagKeys.length) {
|
|
201
|
+
throw new Error("\u6807\u7B7E\u6392\u5E8F\u5305\u542B\u91CD\u590D\u9879");
|
|
202
|
+
}
|
|
203
|
+
if (uniqueTagKeys.size !== projectTagKeys.length) {
|
|
204
|
+
throw new Error("\u6807\u7B7E\u6392\u5E8F\u6570\u636E\u4E0D\u5B8C\u6574");
|
|
205
|
+
}
|
|
206
|
+
for (const tagKey of projectTagKeys) {
|
|
207
|
+
if (!uniqueTagKeys.has(tagKey)) {
|
|
208
|
+
throw new Error("\u6807\u7B7E\u6392\u5E8F\u5305\u542B\u4E0D\u5B58\u5728\u7684\u6807\u7B7E");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const nextProjects = flattenProjectsByTagOrder(projects, tagKeys);
|
|
212
|
+
store.set("tagOrder", tagKeys);
|
|
213
|
+
store.set("projects", nextProjects);
|
|
103
214
|
return nextProjects;
|
|
104
215
|
}
|
|
105
216
|
function getSettings() {
|
|
@@ -119,6 +230,7 @@ function getConfigPath() {
|
|
|
119
230
|
|
|
120
231
|
// src/cli/commands/add.ts
|
|
121
232
|
async function addCommand() {
|
|
233
|
+
const existingTags = getTags();
|
|
122
234
|
const answers = await prompts([
|
|
123
235
|
{
|
|
124
236
|
type: "text",
|
|
@@ -131,6 +243,12 @@ async function addCommand() {
|
|
|
131
243
|
name: "root",
|
|
132
244
|
message: "\u9879\u76EE\u6839\u8DEF\u5F84",
|
|
133
245
|
validate: (value) => value.trim().length > 0 || "\u8BF7\u8F93\u5165\u9879\u76EE\u6839\u8DEF\u5F84"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: existingTags.length > 0 ? "autocomplete" : "text",
|
|
249
|
+
name: "tag",
|
|
250
|
+
message: "\u6807\u7B7E\uFF08\u7559\u7A7A\u4E3A\u672A\u5206\u7EC4\uFF09",
|
|
251
|
+
choices: existingTags.map((tag) => ({ title: tag, value: tag }))
|
|
134
252
|
}
|
|
135
253
|
]);
|
|
136
254
|
if (!answers.name || !answers.root) {
|
|
@@ -138,9 +256,13 @@ async function addCommand() {
|
|
|
138
256
|
}
|
|
139
257
|
const project = await addProject({
|
|
140
258
|
name: answers.name,
|
|
141
|
-
root: answers.root
|
|
259
|
+
root: answers.root,
|
|
260
|
+
tag: answers.tag
|
|
142
261
|
});
|
|
143
262
|
console.log(`\u5DF2\u6DFB\u52A0\u9879\u76EE: ${project.name}`);
|
|
263
|
+
if (project.tag) {
|
|
264
|
+
console.log(` tag: ${project.tag}`);
|
|
265
|
+
}
|
|
144
266
|
console.log(project.root);
|
|
145
267
|
}
|
|
146
268
|
|
|
@@ -175,6 +297,9 @@ async function configCommand() {
|
|
|
175
297
|
}
|
|
176
298
|
|
|
177
299
|
// src/cli/commands/list.ts
|
|
300
|
+
function getTagLabel(tagKey) {
|
|
301
|
+
return tagKey === UNGROUPED_TAG ? UNGROUPED_TAG_LABEL : tagKey;
|
|
302
|
+
}
|
|
178
303
|
function listCommand() {
|
|
179
304
|
const projects = getProjects();
|
|
180
305
|
if (projects.length === 0) {
|
|
@@ -182,13 +307,21 @@ function listCommand() {
|
|
|
182
307
|
console.log(`\u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}`);
|
|
183
308
|
return;
|
|
184
309
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
310
|
+
const tagOrder = getTagOrder();
|
|
311
|
+
for (const tagKey of tagOrder) {
|
|
312
|
+
const groupedProjects = projects.filter((project) => (project.tag ?? UNGROUPED_TAG) === tagKey);
|
|
313
|
+
if (groupedProjects.length === 0) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
console.log(`[${getTagLabel(tagKey)}]`);
|
|
317
|
+
for (const project of groupedProjects) {
|
|
318
|
+
console.log(` ${project.name}`);
|
|
319
|
+
console.log(` id: ${project.id}`);
|
|
320
|
+
console.log(` root: ${project.root}`);
|
|
321
|
+
}
|
|
322
|
+
console.log("");
|
|
189
323
|
}
|
|
190
|
-
console.log(`
|
|
191
|
-
\u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}`);
|
|
324
|
+
console.log(`\u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}`);
|
|
192
325
|
}
|
|
193
326
|
|
|
194
327
|
// src/cli/commands/remove.ts
|
|
@@ -507,7 +640,12 @@ function processEnvWithoutGitOverrides2() {
|
|
|
507
640
|
// src/server/routes.ts
|
|
508
641
|
async function registerApiRoutes(app, settings) {
|
|
509
642
|
app.get("/api/projects", async () => ({
|
|
510
|
-
projects: getProjects()
|
|
643
|
+
projects: getProjects(),
|
|
644
|
+
tagOrder: getTagOrder()
|
|
645
|
+
}));
|
|
646
|
+
app.get("/api/tags", async () => ({
|
|
647
|
+
tags: getTags(),
|
|
648
|
+
tagOrder: getTagOrder()
|
|
511
649
|
}));
|
|
512
650
|
app.post("/api/projects", async (request, reply) => {
|
|
513
651
|
const name = request.body.name?.trim();
|
|
@@ -515,7 +653,7 @@ async function registerApiRoutes(app, settings) {
|
|
|
515
653
|
if (!name || !root) {
|
|
516
654
|
return reply.code(400).send({ message: "\u9879\u76EE\u540D\u79F0\u548C\u6839\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
517
655
|
}
|
|
518
|
-
const project = await addProject({ name, root });
|
|
656
|
+
const project = await addProject({ name, root, tag: request.body.tag });
|
|
519
657
|
return { project };
|
|
520
658
|
});
|
|
521
659
|
app.put("/api/projects/order", async (request, reply) => {
|
|
@@ -524,12 +662,30 @@ async function registerApiRoutes(app, settings) {
|
|
|
524
662
|
return reply.code(400).send({ message: "\u9879\u76EE\u6392\u5E8F\u6570\u636E\u65E0\u6548" });
|
|
525
663
|
}
|
|
526
664
|
try {
|
|
527
|
-
return {
|
|
665
|
+
return {
|
|
666
|
+
projects: reorderProjects(projectIds),
|
|
667
|
+
tagOrder: getTagOrder()
|
|
668
|
+
};
|
|
528
669
|
} catch (error) {
|
|
529
670
|
const message = error instanceof Error ? error.message : "\u4FDD\u5B58\u9879\u76EE\u6392\u5E8F\u5931\u8D25";
|
|
530
671
|
return reply.code(400).send({ message });
|
|
531
672
|
}
|
|
532
673
|
});
|
|
674
|
+
app.put("/api/tag-order", async (request, reply) => {
|
|
675
|
+
const tagKeys = request.body.tagKeys;
|
|
676
|
+
if (!Array.isArray(tagKeys) || !tagKeys.every((tagKey) => typeof tagKey === "string")) {
|
|
677
|
+
return reply.code(400).send({ message: "\u6807\u7B7E\u6392\u5E8F\u6570\u636E\u65E0\u6548" });
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
return {
|
|
681
|
+
projects: reorderTagGroups(tagKeys),
|
|
682
|
+
tagOrder: getTagOrder()
|
|
683
|
+
};
|
|
684
|
+
} catch (error) {
|
|
685
|
+
const message = error instanceof Error ? error.message : "\u4FDD\u5B58\u6807\u7B7E\u6392\u5E8F\u5931\u8D25";
|
|
686
|
+
return reply.code(400).send({ message });
|
|
687
|
+
}
|
|
688
|
+
});
|
|
533
689
|
app.put("/api/projects/:id", async (request, reply) => {
|
|
534
690
|
const name = request.body.name?.trim();
|
|
535
691
|
const root = request.body.root?.trim();
|
|
@@ -537,7 +693,7 @@ async function registerApiRoutes(app, settings) {
|
|
|
537
693
|
return reply.code(400).send({ message: "\u9879\u76EE\u540D\u79F0\u548C\u6839\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
538
694
|
}
|
|
539
695
|
try {
|
|
540
|
-
const project = await updateProject(request.params.id, { name, root });
|
|
696
|
+
const project = await updateProject(request.params.id, { name, root, tag: request.body.tag });
|
|
541
697
|
if (!project) {
|
|
542
698
|
return reply.code(404).send({ message: "\u9879\u76EE\u4E0D\u5B58\u5728" });
|
|
543
699
|
}
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/add.ts","../../src/config/store.ts","../../src/cli/commands/config.ts","../../src/cli/commands/list.ts","../../src/cli/commands/remove.ts","../../src/cli/commands/run.ts","../../src/project/package-json.ts","../../src/runner/git-bash.ts","../../src/runner/path.ts","../../src/server/index.ts","../../src/server/routes.ts","../../src/project/git.ts","../../src/cli/commands/ui.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { cac } from 'cac'\r\nimport { readFileSync } from 'node:fs'\r\nimport prompts from 'prompts'\r\nimport { addCommand } from './commands/add.js'\r\nimport { configCommand } from './commands/config.js'\r\nimport { listCommand } from './commands/list.js'\r\nimport { removeCommand } from './commands/remove.js'\r\nimport { runCommand } from './commands/run.js'\r\nimport { uiCommand } from './commands/ui.js'\r\n\r\nconst packageJson = JSON.parse(\r\n readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),\r\n) as { version?: string }\r\nconst packageVersion = packageJson.version ?? '0.0.0'\r\n\r\nconst cli = cac('br')\r\n\r\ncli.command('add', '新增项目配置').action(wrap(addCommand))\r\n\r\ncli.command('list', '查看项目配置').action(wrap(async () => listCommand()))\r\n\r\ncli.command('run', '选择并执行项目脚本').action(wrap(runCommand))\r\n\r\ncli.command('remove', '删除项目配置').action(wrap(removeCommand))\r\n\r\ncli.command('ui', '打开 Web 管理页面').action(wrap(uiCommand))\r\n\r\ncli.command('config', '配置 Git Bash 路径和 Web 端口').action(wrap(configCommand))\r\n\r\ncli.help()\r\ncli.version(packageVersion)\r\n\r\nasync function mainMenu(): Promise<void> {\r\n const answer = await prompts({\r\n type: 'select',\r\n name: 'action',\r\n message: '请选择操作',\r\n choices: [\r\n { title: '打开可视化操作页面', value: 'ui' },\r\n { title: '启动项目', value: 'run' },\r\n { title: '查看项目', value: 'list' },\r\n { title: '新增项目', value: 'add' },\r\n { title: '删除项目', value: 'remove' },\r\n { title: '设置 Git Bash 路径', value: 'config' },\r\n ],\r\n })\r\n\r\n switch (answer.action) {\r\n case 'run':\r\n await runCommand()\r\n break\r\n case 'add':\r\n await addCommand()\r\n break\r\n case 'list':\r\n listCommand()\r\n break\r\n case 'remove':\r\n await removeCommand()\r\n break\r\n case 'ui':\r\n await uiCommand()\r\n break\r\n case 'config':\r\n await configCommand()\r\n break\r\n }\r\n}\r\n\r\nfunction wrap(fn: () => Promise<void>) {\r\n return async () => {\r\n try {\r\n await fn()\r\n } catch (error) {\r\n handleError(error)\r\n }\r\n }\r\n}\r\n\r\nfunction handleError(error: unknown): void {\r\n const message = error instanceof Error ? error.message : String(error)\r\n console.error(`执行失败: ${message}`)\r\n process.exitCode = 1\r\n}\r\n\r\nif (process.argv.length <= 2) {\r\n await wrap(mainMenu)()\r\n} else {\r\n cli.parse()\r\n}\r\n","import prompts from \"prompts\";\r\nimport { addProject } from \"../../config/store.js\";\r\n\r\nexport async function addCommand(): Promise<void> {\r\n const answers = await prompts([\r\n {\r\n type: \"text\",\r\n name: \"name\",\r\n message: \"项目名称\",\r\n validate: (value) => value.trim().length > 0 || \"请输入项目名称\",\r\n },\r\n {\r\n type: \"text\",\r\n name: \"root\",\r\n message: \"项目根路径\",\r\n validate: (value) => value.trim().length > 0 || \"请输入项目根路径\",\r\n },\r\n ]);\r\n\r\n if (!answers.name || !answers.root) {\r\n return;\r\n }\r\n\r\n const project = await addProject({\r\n name: answers.name,\r\n root: answers.root,\r\n });\r\n\r\n console.log(`已添加项目: ${project.name}`);\r\n console.log(project.root);\r\n}\r\n","import Conf from \"conf\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { access } from \"node:fs/promises\";\r\nimport { resolve } from \"node:path\";\r\nimport type { AppConfig, AppSettings, ProjectConfig } from \"../shared/types.js\";\r\n\r\nconst defaultSettings: AppSettings = {\r\n shell: \"git-bash\",\r\n port: 1234,\r\n};\r\n\r\nconst store = new Conf<AppConfig>({\r\n projectName: \"batch-run-local\",\r\n defaults: {\r\n projects: [],\r\n settings: defaultSettings,\r\n },\r\n});\r\n\r\nexport function getConfig(): AppConfig {\r\n return {\r\n projects: store.get(\"projects\") ?? [],\r\n settings: {\r\n ...defaultSettings,\r\n ...(store.get(\"settings\") ?? {}),\r\n },\r\n };\r\n}\r\n\r\nexport function getProjects(): ProjectConfig[] {\r\n return getConfig().projects;\r\n}\r\n\r\nexport function getProject(projectId: string): ProjectConfig | undefined {\r\n return getProjects().find((project) => project.id === projectId);\r\n}\r\n\r\nexport async function addProject(input: { name: string; root: string }): Promise<ProjectConfig> {\r\n const root = resolve(input.root);\r\n await access(root);\r\n\r\n const now = Date.now();\r\n const project: ProjectConfig = {\r\n id: randomUUID(),\r\n name: input.name.trim(),\r\n root,\r\n createdAt: now,\r\n updatedAt: now,\r\n };\r\n\r\n const projects = getProjects();\r\n if (projects.some((item) => item.root.toLowerCase() === root.toLowerCase())) {\r\n throw new Error(\"该项目路径已经存在\");\r\n }\r\n\r\n store.set(\"projects\", [...projects, project]);\r\n return project;\r\n}\r\n\r\nexport async function updateProject(\r\n projectId: string,\r\n input: { name: string; root: string },\r\n): Promise<ProjectConfig | undefined> {\r\n const root = resolve(input.root);\r\n await access(root);\r\n\r\n const projects = getProjects();\r\n const projectIndex = projects.findIndex((project) => project.id === projectId);\r\n if (projectIndex < 0) {\r\n return undefined;\r\n }\r\n\r\n if (projects.some((project) => project.id !== projectId && project.root.toLowerCase() === root.toLowerCase())) {\r\n throw new Error(\"该项目路径已经存在\");\r\n }\r\n\r\n const nextProject: ProjectConfig = {\r\n ...projects[projectIndex],\r\n name: input.name.trim(),\r\n root,\r\n updatedAt: Date.now(),\r\n };\r\n const nextProjects = [...projects];\r\n nextProjects[projectIndex] = nextProject;\r\n store.set(\"projects\", nextProjects);\r\n return nextProject;\r\n}\r\n\r\nexport function removeProject(projectId: string): boolean {\r\n const projects = getProjects();\r\n const nextProjects = projects.filter((project) => project.id !== projectId);\r\n store.set(\"projects\", nextProjects);\r\n return nextProjects.length !== projects.length;\r\n}\r\n\r\nexport function reorderProjects(projectIds: string[]): ProjectConfig[] {\r\n const projects = getProjects();\r\n const projectMap = new Map(projects.map((project) => [project.id, project]));\r\n const uniqueProjectIds = new Set(projectIds);\r\n\r\n if (uniqueProjectIds.size !== projects.length) {\r\n throw new Error(\"项目排序数据不完整\");\r\n }\r\n\r\n const nextProjects = projectIds.map((projectId) => {\r\n const project = projectMap.get(projectId);\r\n if (!project) {\r\n throw new Error(\"项目排序包含不存在的项目\");\r\n }\r\n return project;\r\n });\r\n\r\n store.set(\"projects\", nextProjects);\r\n return nextProjects;\r\n}\r\n\r\nexport function getSettings(): AppSettings {\r\n return getConfig().settings;\r\n}\r\n\r\nexport function updateSettings(settings: Partial<AppSettings>): AppSettings {\r\n const nextSettings = {\r\n ...getSettings(),\r\n ...settings,\r\n };\r\n store.set(\"settings\", nextSettings);\r\n return nextSettings;\r\n}\r\n\r\nexport function getConfigPath(): string {\r\n return store.path;\r\n}\r\n","import prompts from \"prompts\";\r\nimport { getConfigPath, getSettings, updateSettings } from \"../../config/store.js\";\r\n\r\nexport async function configCommand(): Promise<void> {\r\n const settings = getSettings();\r\n\r\n console.log(`当前配置文件: ${getConfigPath()}`);\r\n console.log(`当前 Git Bash 路径: ${settings.gitBashPath ?? \"自动检测\"}`);\r\n console.log(`当前 Web 端口: ${settings.port}`);\r\n\r\n const answers = await prompts([\r\n {\r\n type: \"text\",\r\n name: \"gitBashPath\",\r\n message: \"Git Bash 路径,留空表示自动检测\",\r\n initial: settings.gitBashPath ?? \"\",\r\n },\r\n {\r\n type: \"number\",\r\n name: \"port\",\r\n message: \"Web 端口\",\r\n initial: settings.port,\r\n min: 1,\r\n max: 65535,\r\n },\r\n ]);\r\n\r\n updateSettings({\r\n gitBashPath: answers.gitBashPath?.trim() || undefined,\r\n port: answers.port || settings.port,\r\n });\r\n\r\n console.log(\"配置已更新。\");\r\n}\r\n","import { getConfigPath, getProjects } from \"../../config/store.js\";\r\n\r\nexport function listCommand(): void {\r\n const projects = getProjects();\r\n\r\n if (projects.length === 0) {\r\n console.log(\"暂无项目配置,可运行 br add 新增。\");\r\n console.log(`配置文件: ${getConfigPath()}`);\r\n return;\r\n }\r\n\r\n for (const project of projects) {\r\n console.log(`${project.name}`);\r\n console.log(` id: ${project.id}`);\r\n console.log(` root: ${project.root}`);\r\n }\r\n\r\n console.log(`\\n配置文件: ${getConfigPath()}`);\r\n}\r\n","import prompts from \"prompts\";\r\nimport { getProjects, removeProject } from \"../../config/store.js\";\r\n\r\nexport async function removeCommand(): Promise<void> {\r\n const projects = getProjects();\r\n\r\n if (projects.length === 0) {\r\n console.log(\"暂无项目可删除。\");\r\n return;\r\n }\r\n\r\n const answer = await prompts({\r\n type: \"select\",\r\n name: \"projectId\",\r\n message: \"选择要删除的项目\",\r\n choices: projects.map((project) => ({\r\n title: project.name,\r\n description: project.root,\r\n value: project.id,\r\n })),\r\n });\r\n\r\n if (!answer.projectId) {\r\n return;\r\n }\r\n\r\n const removed = removeProject(answer.projectId);\r\n console.log(removed ? \"已删除项目。\" : \"未找到项目。\");\r\n}\r\n","import prompts from \"prompts\";\r\nimport { getProjects, getSettings } from \"../../config/store.js\";\r\nimport { readPackageScripts } from \"../../project/package-json.js\";\r\nimport { runScriptInGitBash } from \"../../runner/git-bash.js\";\r\n\r\nexport async function runCommand(): Promise<void> {\r\n const projects = getProjects();\r\n\r\n if (projects.length === 0) {\r\n console.log(\"暂无项目配置,可运行 br add 新增。\");\r\n return;\r\n }\r\n\r\n const projectAnswer = await prompts({\r\n type: \"select\",\r\n name: \"projectId\",\r\n message: \"选择项目\",\r\n choices: projects.map((project) => ({\r\n title: project.name,\r\n description: project.root,\r\n value: project.id,\r\n })),\r\n });\r\n\r\n const project = projects.find((item) => item.id === projectAnswer.projectId);\r\n if (!project) {\r\n return;\r\n }\r\n\r\n const scripts = await readPackageScripts(project.root);\r\n const scriptEntries = Object.entries(scripts);\r\n\r\n if (scriptEntries.length === 0) {\r\n console.log(\"该项目 package.json 中没有 scripts。\");\r\n return;\r\n }\r\n\r\n const scriptAnswer = await prompts({\r\n type: \"select\",\r\n name: \"script\",\r\n message: \"选择要执行的脚本\",\r\n choices: scriptEntries.map(([name, command]) => ({\r\n title: name,\r\n description: command,\r\n value: name,\r\n })),\r\n });\r\n\r\n if (!scriptAnswer.script) {\r\n return;\r\n }\r\n\r\n const result = runScriptInGitBash(project, scriptAnswer.script, getSettings());\r\n console.log(`已在 Git Bash 窗口执行: npm run ${scriptAnswer.script}`);\r\n if (result.pid) {\r\n console.log(`进程 PID: ${result.pid}`);\r\n }\r\n}\r\n","import { readFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport type { PackageScripts } from \"../shared/types.js\";\r\n\r\ntype PackageJson = {\r\n scripts?: PackageScripts;\r\n};\r\n\r\nexport async function readPackageJson(projectRoot: string): Promise<PackageJson> {\r\n const packageJsonPath = join(projectRoot, \"package.json\");\r\n const content = await readFile(packageJsonPath, \"utf8\");\r\n\r\n try {\r\n return JSON.parse(content) as PackageJson;\r\n } catch {\r\n throw new Error(`package.json 解析失败: ${packageJsonPath}`);\r\n }\r\n}\r\n\r\nexport async function readPackageScripts(projectRoot: string): Promise<PackageScripts> {\r\n const packageJson = await readPackageJson(projectRoot);\r\n return packageJson.scripts ?? {};\r\n}\r\n","import { spawn } from \"node:child_process\";\nimport { mkdtempSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AppSettings, ProjectConfig } from \"../shared/types.js\";\nimport { findBashExecutablePath, findGitBashPath, findMinttyPath, shellSingleQuote, toGitBashPath } from \"./path.js\";\n\n/** Strip Git repo overrides so child processes (e.g. Vite calling `git`) use cwd discovery, not a stale inherited GIT_DIR. */\nconst GIT_ENV_KEYS = [\n \"GIT_DIR\",\n \"GIT_WORK_TREE\",\n \"GIT_INDEX_FILE\",\n \"GIT_PREFIX\",\n \"GIT_OBJECT_DIRECTORY\",\n \"GIT_ALTERNATE_OBJECT_DIRECTORIES\",\n] as const;\n\nfunction processEnvWithoutGitOverrides(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n for (const key of GIT_ENV_KEYS) {\n delete env[key];\n }\n return env;\n}\n\nexport type RunResult = {\n pid?: number;\n command: string;\n};\n\nexport function runScriptInGitBash(project: ProjectConfig, scriptName: string, settings: AppSettings): RunResult {\n const gitBashPath = findGitBashPath(settings.gitBashPath);\n\n if (!gitBashPath) {\n throw new Error(\"未找到 Git Bash,请先通过 br config 设置 git-bash.exe 路径\");\n }\n\n const title = createTerminalTitle(project.name, scriptName);\n const gitBashRoot = toGitBashPath(project.root);\n const scriptPath = writeRunScript(project, scriptName, title, gitBashRoot);\n const gitBashScriptPath = toGitBashPath(scriptPath);\n const bashPath = findBashExecutablePath(gitBashPath);\n const command = `${bashPath} --login ${scriptPath}`;\n\n const minttyPath = findMinttyPath(gitBashPath);\n const child = minttyPath\n ? spawn(minttyPath, [\"--title\", title, \"--dir\", gitBashRoot, toGitBashPath(bashPath), \"--login\", gitBashScriptPath], {\n detached: true,\n stdio: \"ignore\",\n windowsHide: false,\n env: processEnvWithoutGitOverrides(),\n })\n : spawn(\"cmd.exe\", [\"/d\", \"/c\", \"start\", title, \"/D\", project.root, bashPath, \"--login\", gitBashScriptPath], {\n detached: true,\n stdio: \"ignore\",\n windowsHide: false,\n env: processEnvWithoutGitOverrides(),\n });\n\n child.unref();\n\n return {\n pid: child.pid,\n command,\n };\n}\n\nfunction createTerminalTitle(projectName: string, scriptName: string): string {\n const title = `batch-run - ${projectName} - npm run ${scriptName}`;\n return title.replace(/[\\u0000-\\u001f\\u007f]/g, \" \").slice(0, 120);\n}\n\nfunction writeRunScript(project: ProjectConfig, scriptName: string, title: string, gitBashRoot: string): string {\n const tempDir = mkdtempSync(join(tmpdir(), \"batch-run-\"));\n const scriptPath = join(tempDir, \"run.sh\");\n const scriptContent = [\n \"#!/usr/bin/env bash\",\n \"set +e\",\n `cd ${shellSingleQuote(gitBashRoot)}`,\n 'rm -f \"$0\" >/dev/null 2>&1 || true',\n `unset ${GIT_ENV_KEYS.join(\" \")}`,\n `__batch_run_title=${shellSingleQuote(title)}`,\n \"set_batch_run_title() { printf '\\\\033]0;%s\\\\007' \\\"$__batch_run_title\\\"; }\",\n \"set_batch_run_title\",\n \"(while true; do set_batch_run_title; sleep 2; done) &\",\n \"__batch_run_title_pid=$!\",\n \"cleanup_batch_run_title() {\",\n ' kill \"$__batch_run_title_pid\" >/dev/null 2>&1 || true',\n ' wait \"$__batch_run_title_pid\" 2>/dev/null || true',\n \" set_batch_run_title\",\n \"}\",\n \"trap cleanup_batch_run_title EXIT\",\n `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,\n `npm run ${shellSingleQuote(scriptName)}`,\n \"__batch_run_exit_code=$?\",\n \"set_batch_run_title\",\n \"echo\",\n 'echo \"Process exited with code $__batch_run_exit_code. Press Ctrl+D or close this window.\"',\n \"bash --login\",\n 'exit \"$__batch_run_exit_code\"',\n \"\",\n ].join(\"\\n\");\n\n writeFileSync(scriptPath, scriptContent, \"utf8\");\n return scriptPath;\n}\n","import { existsSync } from \"node:fs\";\r\nimport { platform } from \"node:os\";\r\nimport { basename, dirname, join } from \"node:path\";\r\n\r\nconst defaultWindowsGitBashPaths = [\r\n \"C:\\\\Program Files\\\\Git\\\\git-bash.exe\",\r\n \"C:\\\\Program Files (x86)\\\\Git\\\\git-bash.exe\",\r\n];\r\n\r\nexport function findGitBashPath(configuredPath?: string): string | undefined {\r\n if (configuredPath && existsSync(configuredPath) && isGitBashExecutable(configuredPath)) {\r\n return configuredPath;\r\n }\r\n\r\n if (platform() !== \"win32\") {\r\n return undefined;\r\n }\r\n\r\n return defaultWindowsGitBashPaths.find((item) => existsSync(item));\r\n}\r\n\r\nexport function toGitBashPath(inputPath: string): string {\r\n const normalized = inputPath.replaceAll(\"\\\\\", \"/\");\r\n const driveMatch = /^([a-zA-Z]):\\/(.*)$/.exec(normalized);\r\n\r\n if (!driveMatch) {\r\n return normalized;\r\n }\r\n\r\n const [, drive, rest] = driveMatch;\r\n return `/${drive.toLowerCase()}/${rest}`;\r\n}\r\n\r\nexport function shellSingleQuote(value: string): string {\r\n return `'${value.replaceAll(\"'\", \"'\\\\''\")}'`;\r\n}\r\n\r\nexport function findMinttyPath(gitBashPath: string): string | undefined {\r\n const gitBashDir = dirname(gitBashPath);\r\n const candidates = [\r\n join(gitBashDir, \"usr\", \"bin\", \"mintty.exe\"),\r\n join(gitBashDir, \"mintty.exe\"),\r\n join(dirname(gitBashDir), \"usr\", \"bin\", \"mintty.exe\"),\r\n ];\r\n\r\n return candidates.find((minttyPath) => existsSync(minttyPath));\r\n}\r\n\r\nexport function findBashExecutablePath(gitBashPath: string): string {\r\n const executableName = basename(gitBashPath).toLowerCase();\r\n if (executableName === \"bash.exe\" || executableName === \"sh.exe\") {\r\n return gitBashPath;\r\n }\r\n\r\n const gitBashDir = dirname(gitBashPath);\r\n const candidates = [\r\n join(gitBashDir, \"bin\", \"bash.exe\"),\r\n join(gitBashDir, \"usr\", \"bin\", \"bash.exe\"),\r\n join(gitBashDir, \"bin\", \"sh.exe\"),\r\n join(gitBashDir, \"usr\", \"bin\", \"sh.exe\"),\r\n ];\r\n\r\n return candidates.find((bashPath) => existsSync(bashPath)) ?? gitBashPath;\r\n}\r\n\r\nfunction isGitBashExecutable(inputPath: string): boolean {\r\n const executableName = basename(inputPath).toLowerCase();\r\n return executableName === \"git-bash.exe\" || executableName === \"bash.exe\" || executableName === \"sh.exe\";\r\n}\r\n","import fastify from \"fastify\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { dirname, join } from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport open from \"open\";\r\nimport { getSettings } from \"../config/store.js\";\r\nimport { registerApiRoutes } from \"./routes.js\";\r\n\r\nfunction getWebRoot(): string {\r\n return join(dirname(dirname(fileURLToPath(import.meta.url))), \"web\");\r\n}\r\n\r\nexport async function startServer(options: { openBrowser?: boolean } = {}): Promise<void> {\r\n const settings = getSettings();\r\n const app = fastify({\r\n logger: false,\r\n });\r\n const webRoot = getWebRoot();\r\n\r\n await registerApiRoutes(app, settings);\r\n\r\n app.get(\"/\", async (_request, reply) => {\r\n return reply.type(\"text/html\").send(await readFile(join(webRoot, \"index.html\"), \"utf8\"));\r\n });\r\n\r\n app.get(\"/main.js\", async (_request, reply) => {\r\n return reply.type(\"application/javascript\").send(await readFile(join(webRoot, \"main.js\"), \"utf8\"));\r\n });\r\n\r\n app.get(\"/style.css\", async (_request, reply) => {\r\n return reply.type(\"text/css\").send(await readFile(join(webRoot, \"style.css\"), \"utf8\"));\r\n });\r\n\r\n app.get(\"/favicon.ico\", async (_request, reply) => {\r\n return reply.type(\"image/x-icon\").send(await readFile(join(webRoot, \"favicon.ico\")));\r\n });\r\n\r\n const url = await app.listen({\r\n host: \"127.0.0.1\",\r\n port: settings.port,\r\n });\r\n\r\n console.log(`Batch Run Web 已启动: ${url}`);\r\n\r\n if (options.openBrowser ?? true) {\r\n await open(url);\r\n }\r\n}\r\n","import type { FastifyInstance } from \"fastify\";\r\nimport open from \"open\";\r\nimport { addProject, getProject, getProjects, removeProject, reorderProjects, updateProject } from \"../config/store.js\";\r\nimport { readGitBranches, switchGitBranch } from \"../project/git.js\";\r\nimport { readPackageScripts } from \"../project/package-json.js\";\r\nimport { runScriptInGitBash } from \"../runner/git-bash.js\";\r\nimport type { AppSettings } from \"../shared/types.js\";\r\n\r\ntype AddProjectBody = {\r\n name?: string;\r\n root?: string;\r\n};\r\n\r\ntype UpdateProjectBody = AddProjectBody;\r\n\r\ntype RunScriptBody = {\r\n script?: string;\r\n};\r\n\r\ntype SwitchBranchBody = {\r\n branch?: string;\r\n};\r\n\r\ntype ReorderProjectsBody = {\r\n projectIds?: string[];\r\n};\r\n\r\nexport async function registerApiRoutes(app: FastifyInstance, settings: AppSettings): Promise<void> {\r\n app.get(\"/api/projects\", async () => ({\r\n projects: getProjects(),\r\n }));\r\n\r\n app.post<{ Body: AddProjectBody }>(\"/api/projects\", async (request, reply) => {\r\n const name = request.body.name?.trim();\r\n const root = request.body.root?.trim();\r\n\r\n if (!name || !root) {\r\n return reply.code(400).send({ message: \"项目名称和根路径不能为空\" });\r\n }\r\n\r\n const project = await addProject({ name, root });\r\n return { project };\r\n });\r\n\r\n app.put<{ Body: ReorderProjectsBody }>(\"/api/projects/order\", async (request, reply) => {\r\n const projectIds = request.body.projectIds;\r\n\r\n if (!Array.isArray(projectIds) || !projectIds.every((projectId) => typeof projectId === \"string\")) {\r\n return reply.code(400).send({ message: \"项目排序数据无效\" });\r\n }\r\n\r\n try {\r\n return { projects: reorderProjects(projectIds) };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"保存项目排序失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.put<{ Params: { id: string }; Body: UpdateProjectBody }>(\"/api/projects/:id\", async (request, reply) => {\r\n const name = request.body.name?.trim();\r\n const root = request.body.root?.trim();\r\n\r\n if (!name || !root) {\r\n return reply.code(400).send({ message: \"项目名称和根路径不能为空\" });\r\n }\r\n\r\n try {\r\n const project = await updateProject(request.params.id, { name, root });\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n return { project };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"更新项目失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.delete<{ Params: { id: string } }>(\"/api/projects/:id\", async (request, reply) => {\r\n const removed = removeProject(request.params.id);\r\n\r\n if (!removed) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n return { ok: true };\r\n });\r\n\r\n app.get<{ Params: { id: string } }>(\"/api/projects/:id/scripts\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n const scripts = await readPackageScripts(project.root);\r\n return { scripts };\r\n });\r\n\r\n app.get<{ Params: { id: string } }>(\"/api/projects/:id/branches\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n try {\r\n return await readGitBranches(project.root);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"读取 Git 分支失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.post<{ Params: { id: string }; Body: SwitchBranchBody }>(\"/api/projects/:id/branches/switch\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n const branch = request.body.branch?.trim();\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n if (!branch) {\r\n return reply.code(400).send({ message: \"分支名称不能为空\" });\r\n }\r\n\r\n try {\r\n const branchState = await switchGitBranch(project.root, branch);\r\n const scripts = await readPackageScripts(project.root);\r\n return {\r\n ...branchState,\r\n scripts,\r\n };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"切换 Git 分支失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.post<{ Params: { id: string } }>(\"/api/projects/:id/open\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n await open(project.root);\r\n return { ok: true };\r\n });\r\n\r\n app.post<{ Params: { id: string }; Body: RunScriptBody }>(\"/api/projects/:id/run\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n const script = request.body.script?.trim();\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n if (!script) {\r\n return reply.code(400).send({ message: \"脚本名称不能为空\" });\r\n }\r\n\r\n const scripts = await readPackageScripts(project.root);\r\n if (!scripts[script]) {\r\n return reply.code(404).send({ message: \"脚本不存在\" });\r\n }\r\n\r\n const result = runScriptInGitBash(project, script, settings);\r\n return {\r\n ok: true,\r\n pid: result.pid,\r\n };\r\n });\r\n}\r\n","import { execFile } from \"node:child_process\";\r\nimport { promisify } from \"node:util\";\r\nimport type { GitBranch, GitBranchState } from \"../shared/types.js\";\r\n\r\nconst execFileAsync = promisify(execFile);\r\n\r\nexport async function readGitBranches(projectRoot: string): Promise<GitBranchState> {\r\n const [current, localBranches] = await Promise.all([\r\n runGit(projectRoot, [\"branch\", \"--show-current\"]),\r\n runGit(projectRoot, [\"branch\", \"--format=%(refname:short)\"]),\r\n ]);\r\n\r\n const currentBranch = current.trim() || undefined;\r\n const localNames = parseBranchLines(localBranches);\r\n const branches: GitBranch[] = localNames.map((name) => ({\r\n name,\r\n current: name === currentBranch,\r\n remote: false,\r\n }));\r\n\r\n return {\r\n current: currentBranch,\r\n branches,\r\n };\r\n}\r\n\r\nexport async function switchGitBranch(projectRoot: string, branchName: string): Promise<GitBranchState> {\r\n const name = branchName.trim();\r\n if (!name) {\r\n throw new Error(\"分支名称不能为空\");\r\n }\r\n\r\n const branchState = await readGitBranches(projectRoot);\r\n const branch = branchState.branches.find((item) => item.name === name);\r\n if (!branch) {\r\n throw new Error(\"本地分支不存在\");\r\n }\r\n\r\n if (branch.current) {\r\n return branchState;\r\n }\r\n\r\n await runGit(projectRoot, [\"switch\", name]);\r\n\r\n return readGitBranches(projectRoot);\r\n}\r\n\r\nasync function runGit(projectRoot: string, args: string[]): Promise<string> {\r\n try {\r\n const { stdout } = await execFileAsync(\"git\", args, {\r\n cwd: projectRoot,\r\n env: processEnvWithoutGitOverrides(),\r\n windowsHide: true,\r\n });\r\n return stdout;\r\n } catch (error) {\r\n const stderr = typeof error === \"object\" && error && \"stderr\" in error ? String(error.stderr) : \"\";\r\n const message = stderr.trim() || (error instanceof Error ? error.message : \"Git 命令执行失败\");\r\n throw new Error(message);\r\n }\r\n}\r\n\r\nfunction parseBranchLines(output: string): string[] {\r\n return output\r\n .split(/\\r?\\n/)\r\n .map((line) => line.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nfunction processEnvWithoutGitOverrides(): NodeJS.ProcessEnv {\r\n const env = { ...process.env };\r\n delete env.GIT_DIR;\r\n delete env.GIT_WORK_TREE;\r\n delete env.GIT_INDEX_FILE;\r\n delete env.GIT_PREFIX;\r\n delete env.GIT_OBJECT_DIRECTORY;\r\n delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;\r\n return env;\r\n}\r\n","import { startServer } from \"../../server/index.js\";\r\n\r\nexport async function uiCommand(): Promise<void> {\r\n await startServer({ openBrowser: true });\r\n}\r\n"],"mappings":"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","names":["prompts","prompts","prompts","prompts","prompts","prompts","packageJson","join","join","join","prompts","project","readFile","dirname","join","open","processEnvWithoutGitOverrides","join","dirname","readFile","open","prompts"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/add.ts","../../src/config/store.ts","../../src/shared/types.ts","../../src/cli/commands/config.ts","../../src/cli/commands/list.ts","../../src/cli/commands/remove.ts","../../src/cli/commands/run.ts","../../src/project/package-json.ts","../../src/runner/git-bash.ts","../../src/runner/path.ts","../../src/server/index.ts","../../src/server/routes.ts","../../src/project/git.ts","../../src/cli/commands/ui.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { cac } from 'cac'\r\nimport { readFileSync } from 'node:fs'\r\nimport prompts from 'prompts'\r\nimport { addCommand } from './commands/add.js'\r\nimport { configCommand } from './commands/config.js'\r\nimport { listCommand } from './commands/list.js'\r\nimport { removeCommand } from './commands/remove.js'\r\nimport { runCommand } from './commands/run.js'\r\nimport { uiCommand } from './commands/ui.js'\r\n\r\nconst packageJson = JSON.parse(\r\n readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),\r\n) as { version?: string }\r\nconst packageVersion = packageJson.version ?? '0.0.0'\r\n\r\nconst cli = cac('br')\r\n\r\ncli.command('add', '新增项目配置').action(wrap(addCommand))\r\n\r\ncli.command('list', '查看项目配置').action(wrap(async () => listCommand()))\r\n\r\ncli.command('run', '选择并执行项目脚本').action(wrap(runCommand))\r\n\r\ncli.command('remove', '删除项目配置').action(wrap(removeCommand))\r\n\r\ncli.command('ui', '打开 Web 管理页面').action(wrap(uiCommand))\r\n\r\ncli.command('config', '配置 Git Bash 路径和 Web 端口').action(wrap(configCommand))\r\n\r\ncli.help()\r\ncli.version(packageVersion)\r\n\r\nasync function mainMenu(): Promise<void> {\r\n const answer = await prompts({\r\n type: 'select',\r\n name: 'action',\r\n message: '请选择操作',\r\n choices: [\r\n { title: '打开可视化操作页面', value: 'ui' },\r\n { title: '启动项目', value: 'run' },\r\n { title: '查看项目', value: 'list' },\r\n { title: '新增项目', value: 'add' },\r\n { title: '删除项目', value: 'remove' },\r\n { title: '设置 Git Bash 路径', value: 'config' },\r\n ],\r\n })\r\n\r\n switch (answer.action) {\r\n case 'run':\r\n await runCommand()\r\n break\r\n case 'add':\r\n await addCommand()\r\n break\r\n case 'list':\r\n listCommand()\r\n break\r\n case 'remove':\r\n await removeCommand()\r\n break\r\n case 'ui':\r\n await uiCommand()\r\n break\r\n case 'config':\r\n await configCommand()\r\n break\r\n }\r\n}\r\n\r\nfunction wrap(fn: () => Promise<void>) {\r\n return async () => {\r\n try {\r\n await fn()\r\n } catch (error) {\r\n handleError(error)\r\n }\r\n }\r\n}\r\n\r\nfunction handleError(error: unknown): void {\r\n const message = error instanceof Error ? error.message : String(error)\r\n console.error(`执行失败: ${message}`)\r\n process.exitCode = 1\r\n}\r\n\r\nif (process.argv.length <= 2) {\r\n await wrap(mainMenu)()\r\n} else {\r\n cli.parse()\r\n}\r\n","import prompts from \"prompts\";\r\nimport { addProject, getTags } from \"../../config/store.js\";\r\n\r\nexport async function addCommand(): Promise<void> {\r\n const existingTags = getTags();\r\n const answers = await prompts([\r\n {\r\n type: \"text\",\r\n name: \"name\",\r\n message: \"项目名称\",\r\n validate: (value) => value.trim().length > 0 || \"请输入项目名称\",\r\n },\r\n {\r\n type: \"text\",\r\n name: \"root\",\r\n message: \"项目根路径\",\r\n validate: (value) => value.trim().length > 0 || \"请输入项目根路径\",\r\n },\r\n {\r\n type: existingTags.length > 0 ? \"autocomplete\" : \"text\",\r\n name: \"tag\",\r\n message: \"标签(留空为未分组)\",\r\n choices: existingTags.map((tag) => ({ title: tag, value: tag })),\r\n },\r\n ]);\r\n\r\n if (!answers.name || !answers.root) {\r\n return;\r\n }\r\n\r\n const project = await addProject({\r\n name: answers.name,\r\n root: answers.root,\r\n tag: answers.tag,\r\n });\r\n\r\n console.log(`已添加项目: ${project.name}`);\r\n if (project.tag) {\r\n console.log(` tag: ${project.tag}`);\r\n }\r\n console.log(project.root);\r\n}\r\n","import Conf from \"conf\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { access } from \"node:fs/promises\";\r\nimport { resolve } from \"node:path\";\r\nimport type { AppConfig, AppSettings, ProjectConfig } from \"../shared/types.js\";\r\nimport { UNGROUPED_TAG, UNGROUPED_TAG_LABEL } from \"../shared/types.js\";\r\n\r\nconst defaultSettings: AppSettings = {\r\n shell: \"git-bash\",\r\n port: 1234,\r\n};\r\n\r\nconst store = new Conf<AppConfig>({\r\n projectName: \"batch-run-local\",\r\n defaults: {\r\n projects: [],\r\n settings: defaultSettings,\r\n },\r\n});\r\n\r\nexport function normalizeTag(tag?: string): string | undefined {\r\n const trimmed = tag?.trim();\r\n if (!trimmed || trimmed === UNGROUPED_TAG_LABEL) {\r\n return undefined;\r\n }\r\n return trimmed;\r\n}\r\n\r\nexport function getProjectTagKey(project: ProjectConfig): string {\r\n return project.tag ?? UNGROUPED_TAG;\r\n}\r\n\r\nfunction collectTagKeysFromProjects(projects: ProjectConfig[]): string[] {\r\n const keys: string[] = [];\r\n const seen = new Set<string>();\r\n\r\n for (const project of projects) {\r\n const key = getProjectTagKey(project);\r\n if (!seen.has(key)) {\r\n seen.add(key);\r\n keys.push(key);\r\n }\r\n }\r\n\r\n return keys;\r\n}\r\n\r\nfunction groupProjectsByTag(projects: ProjectConfig[]): Map<string, ProjectConfig[]> {\r\n const groups = new Map<string, ProjectConfig[]>();\r\n\r\n for (const project of projects) {\r\n const key = getProjectTagKey(project);\r\n const group = groups.get(key);\r\n if (group) {\r\n group.push(project);\r\n } else {\r\n groups.set(key, [project]);\r\n }\r\n }\r\n\r\n return groups;\r\n}\r\n\r\nfunction flattenProjectsByTagOrder(projects: ProjectConfig[], tagOrder: string[]): ProjectConfig[] {\r\n const grouped = groupProjectsByTag(projects);\r\n const nextProjects: ProjectConfig[] = [];\r\n\r\n for (const tagKey of tagOrder) {\r\n const group = grouped.get(tagKey);\r\n if (group) {\r\n nextProjects.push(...group);\r\n grouped.delete(tagKey);\r\n }\r\n }\r\n\r\n for (const group of grouped.values()) {\r\n nextProjects.push(...group);\r\n }\r\n\r\n return nextProjects;\r\n}\r\n\r\nfunction syncTagOrder(projects: ProjectConfig[], preferredOrder?: string[]): string[] {\r\n const projectTagKeys = collectTagKeysFromProjects(projects);\r\n const projectTagKeySet = new Set(projectTagKeys);\r\n const sourceOrder = preferredOrder ?? store.get(\"tagOrder\") ?? [];\r\n const nextOrder: string[] = [];\r\n const seen = new Set<string>();\r\n\r\n for (const tagKey of sourceOrder) {\r\n if (!seen.has(tagKey) && projectTagKeySet.has(tagKey)) {\r\n seen.add(tagKey);\r\n nextOrder.push(tagKey);\r\n }\r\n }\r\n\r\n for (const tagKey of projectTagKeys) {\r\n if (!seen.has(tagKey)) {\r\n seen.add(tagKey);\r\n nextOrder.push(tagKey);\r\n }\r\n }\r\n\r\n store.set(\"tagOrder\", nextOrder);\r\n return nextOrder;\r\n}\r\n\r\nexport function getConfig(): AppConfig {\r\n return {\r\n projects: store.get(\"projects\") ?? [],\r\n settings: {\r\n ...defaultSettings,\r\n ...(store.get(\"settings\") ?? {}),\r\n },\r\n tagOrder: store.get(\"tagOrder\"),\r\n };\r\n}\r\n\r\nexport function getProjects(): ProjectConfig[] {\r\n return getConfig().projects;\r\n}\r\n\r\nexport function getProject(projectId: string): ProjectConfig | undefined {\r\n return getProjects().find((project) => project.id === projectId);\r\n}\r\n\r\nexport function getTagOrder(): string[] {\r\n return syncTagOrder(getProjects());\r\n}\r\n\r\nexport function getTags(): string[] {\r\n return getTagOrder().filter((tagKey) => tagKey !== UNGROUPED_TAG);\r\n}\r\n\r\nexport async function addProject(input: { name: string; root: string; tag?: string }): Promise<ProjectConfig> {\r\n const root = resolve(input.root);\r\n await access(root);\r\n\r\n const now = Date.now();\r\n const project: ProjectConfig = {\r\n id: randomUUID(),\r\n name: input.name.trim(),\r\n root,\r\n tag: normalizeTag(input.tag),\r\n createdAt: now,\r\n updatedAt: now,\r\n };\r\n\r\n const projects = getProjects();\r\n if (projects.some((item) => item.root.toLowerCase() === root.toLowerCase())) {\r\n throw new Error(\"该项目路径已经存在\");\r\n }\r\n\r\n const nextProjects = [...projects, project];\r\n store.set(\"projects\", nextProjects);\r\n syncTagOrder(nextProjects);\r\n return project;\r\n}\r\n\r\nexport async function updateProject(\r\n projectId: string,\r\n input: { name: string; root: string; tag?: string },\r\n): Promise<ProjectConfig | undefined> {\r\n const root = resolve(input.root);\r\n await access(root);\r\n\r\n const projects = getProjects();\r\n const projectIndex = projects.findIndex((project) => project.id === projectId);\r\n if (projectIndex < 0) {\r\n return undefined;\r\n }\r\n\r\n if (projects.some((project) => project.id !== projectId && project.root.toLowerCase() === root.toLowerCase())) {\r\n throw new Error(\"该项目路径已经存在\");\r\n }\r\n\r\n const nextProject: ProjectConfig = {\r\n ...projects[projectIndex],\r\n name: input.name.trim(),\r\n root,\r\n tag: normalizeTag(input.tag),\r\n updatedAt: Date.now(),\r\n };\r\n const nextProjects = [...projects];\r\n nextProjects[projectIndex] = nextProject;\r\n store.set(\"projects\", nextProjects);\r\n syncTagOrder(nextProjects);\r\n return nextProject;\r\n}\r\n\r\nexport function removeProject(projectId: string): boolean {\r\n const projects = getProjects();\r\n const nextProjects = projects.filter((project) => project.id !== projectId);\r\n store.set(\"projects\", nextProjects);\r\n syncTagOrder(nextProjects);\r\n return nextProjects.length !== projects.length;\r\n}\r\n\r\nexport function reorderProjects(projectIds: string[]): ProjectConfig[] {\r\n const projects = getProjects();\r\n const projectMap = new Map(projects.map((project) => [project.id, project]));\r\n const uniqueProjectIds = new Set(projectIds);\r\n\r\n if (uniqueProjectIds.size !== projects.length) {\r\n throw new Error(\"项目排序数据不完整\");\r\n }\r\n\r\n const nextProjects = projectIds.map((projectId) => {\r\n const project = projectMap.get(projectId);\r\n if (!project) {\r\n throw new Error(\"项目排序包含不存在的项目\");\r\n }\r\n return project;\r\n });\r\n\r\n store.set(\"projects\", nextProjects);\r\n syncTagOrder(nextProjects);\r\n return nextProjects;\r\n}\r\n\r\nexport function reorderTagGroups(tagKeys: string[]): ProjectConfig[] {\r\n const projects = getProjects();\r\n const projectTagKeys = collectTagKeysFromProjects(projects);\r\n const uniqueTagKeys = new Set(tagKeys);\r\n\r\n if (uniqueTagKeys.size !== tagKeys.length) {\r\n throw new Error(\"标签排序包含重复项\");\r\n }\r\n\r\n if (uniqueTagKeys.size !== projectTagKeys.length) {\r\n throw new Error(\"标签排序数据不完整\");\r\n }\r\n\r\n for (const tagKey of projectTagKeys) {\r\n if (!uniqueTagKeys.has(tagKey)) {\r\n throw new Error(\"标签排序包含不存在的标签\");\r\n }\r\n }\r\n\r\n const nextProjects = flattenProjectsByTagOrder(projects, tagKeys);\r\n store.set(\"tagOrder\", tagKeys);\r\n store.set(\"projects\", nextProjects);\r\n return nextProjects;\r\n}\r\n\r\nexport function getSettings(): AppSettings {\r\n return getConfig().settings;\r\n}\r\n\r\nexport function updateSettings(settings: Partial<AppSettings>): AppSettings {\r\n const nextSettings = {\r\n ...getSettings(),\r\n ...settings,\r\n };\r\n store.set(\"settings\", nextSettings);\r\n return nextSettings;\r\n}\r\n\r\nexport function getConfigPath(): string {\r\n return store.path;\r\n}\r\n","export const UNGROUPED_TAG = \"\";\r\nexport const UNGROUPED_TAG_LABEL = \"未分组\";\r\n\r\nexport type ProjectConfig = {\r\n id: string;\r\n name: string;\r\n root: string;\r\n tag?: string;\r\n createdAt: number;\r\n updatedAt: number;\r\n};\r\n\r\nexport type AppSettings = {\r\n shell: \"git-bash\";\r\n gitBashPath?: string;\r\n port: number;\r\n};\r\n\r\nexport type AppConfig = {\r\n projects: ProjectConfig[];\r\n settings: AppSettings;\r\n tagOrder?: string[];\r\n};\r\n\r\nexport type PackageScripts = Record<string, string>;\r\n\r\nexport type GitBranch = {\r\n name: string;\r\n current: boolean;\r\n remote: boolean;\r\n};\r\n\r\nexport type GitBranchState = {\r\n current?: string;\r\n branches: GitBranch[];\r\n};\r\n","import prompts from \"prompts\";\r\nimport { getConfigPath, getSettings, updateSettings } from \"../../config/store.js\";\r\n\r\nexport async function configCommand(): Promise<void> {\r\n const settings = getSettings();\r\n\r\n console.log(`当前配置文件: ${getConfigPath()}`);\r\n console.log(`当前 Git Bash 路径: ${settings.gitBashPath ?? \"自动检测\"}`);\r\n console.log(`当前 Web 端口: ${settings.port}`);\r\n\r\n const answers = await prompts([\r\n {\r\n type: \"text\",\r\n name: \"gitBashPath\",\r\n message: \"Git Bash 路径,留空表示自动检测\",\r\n initial: settings.gitBashPath ?? \"\",\r\n },\r\n {\r\n type: \"number\",\r\n name: \"port\",\r\n message: \"Web 端口\",\r\n initial: settings.port,\r\n min: 1,\r\n max: 65535,\r\n },\r\n ]);\r\n\r\n updateSettings({\r\n gitBashPath: answers.gitBashPath?.trim() || undefined,\r\n port: answers.port || settings.port,\r\n });\r\n\r\n console.log(\"配置已更新。\");\r\n}\r\n","import { getConfigPath, getProjects, getTagOrder } from \"../../config/store.js\";\r\nimport { UNGROUPED_TAG, UNGROUPED_TAG_LABEL } from \"../../shared/types.js\";\r\n\r\nfunction getTagLabel(tagKey: string): string {\r\n return tagKey === UNGROUPED_TAG ? UNGROUPED_TAG_LABEL : tagKey;\r\n}\r\n\r\nexport function listCommand(): void {\r\n const projects = getProjects();\r\n\r\n if (projects.length === 0) {\r\n console.log(\"暂无项目配置,可运行 br add 新增。\");\r\n console.log(`配置文件: ${getConfigPath()}`);\r\n return;\r\n }\r\n\r\n const tagOrder = getTagOrder();\r\n\r\n for (const tagKey of tagOrder) {\r\n const groupedProjects = projects.filter((project) => (project.tag ?? UNGROUPED_TAG) === tagKey);\r\n if (groupedProjects.length === 0) {\r\n continue;\r\n }\r\n\r\n console.log(`[${getTagLabel(tagKey)}]`);\r\n for (const project of groupedProjects) {\r\n console.log(` ${project.name}`);\r\n console.log(` id: ${project.id}`);\r\n console.log(` root: ${project.root}`);\r\n }\r\n console.log(\"\");\r\n }\r\n\r\n console.log(`配置文件: ${getConfigPath()}`);\r\n}\r\n","import prompts from \"prompts\";\r\nimport { getProjects, removeProject } from \"../../config/store.js\";\r\n\r\nexport async function removeCommand(): Promise<void> {\r\n const projects = getProjects();\r\n\r\n if (projects.length === 0) {\r\n console.log(\"暂无项目可删除。\");\r\n return;\r\n }\r\n\r\n const answer = await prompts({\r\n type: \"select\",\r\n name: \"projectId\",\r\n message: \"选择要删除的项目\",\r\n choices: projects.map((project) => ({\r\n title: project.name,\r\n description: project.root,\r\n value: project.id,\r\n })),\r\n });\r\n\r\n if (!answer.projectId) {\r\n return;\r\n }\r\n\r\n const removed = removeProject(answer.projectId);\r\n console.log(removed ? \"已删除项目。\" : \"未找到项目。\");\r\n}\r\n","import prompts from \"prompts\";\r\nimport { getProjects, getSettings } from \"../../config/store.js\";\r\nimport { readPackageScripts } from \"../../project/package-json.js\";\r\nimport { runScriptInGitBash } from \"../../runner/git-bash.js\";\r\n\r\nexport async function runCommand(): Promise<void> {\r\n const projects = getProjects();\r\n\r\n if (projects.length === 0) {\r\n console.log(\"暂无项目配置,可运行 br add 新增。\");\r\n return;\r\n }\r\n\r\n const projectAnswer = await prompts({\r\n type: \"select\",\r\n name: \"projectId\",\r\n message: \"选择项目\",\r\n choices: projects.map((project) => ({\r\n title: project.name,\r\n description: project.root,\r\n value: project.id,\r\n })),\r\n });\r\n\r\n const project = projects.find((item) => item.id === projectAnswer.projectId);\r\n if (!project) {\r\n return;\r\n }\r\n\r\n const scripts = await readPackageScripts(project.root);\r\n const scriptEntries = Object.entries(scripts);\r\n\r\n if (scriptEntries.length === 0) {\r\n console.log(\"该项目 package.json 中没有 scripts。\");\r\n return;\r\n }\r\n\r\n const scriptAnswer = await prompts({\r\n type: \"select\",\r\n name: \"script\",\r\n message: \"选择要执行的脚本\",\r\n choices: scriptEntries.map(([name, command]) => ({\r\n title: name,\r\n description: command,\r\n value: name,\r\n })),\r\n });\r\n\r\n if (!scriptAnswer.script) {\r\n return;\r\n }\r\n\r\n const result = runScriptInGitBash(project, scriptAnswer.script, getSettings());\r\n console.log(`已在 Git Bash 窗口执行: npm run ${scriptAnswer.script}`);\r\n if (result.pid) {\r\n console.log(`进程 PID: ${result.pid}`);\r\n }\r\n}\r\n","import { readFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport type { PackageScripts } from \"../shared/types.js\";\r\n\r\ntype PackageJson = {\r\n scripts?: PackageScripts;\r\n};\r\n\r\nexport async function readPackageJson(projectRoot: string): Promise<PackageJson> {\r\n const packageJsonPath = join(projectRoot, \"package.json\");\r\n const content = await readFile(packageJsonPath, \"utf8\");\r\n\r\n try {\r\n return JSON.parse(content) as PackageJson;\r\n } catch {\r\n throw new Error(`package.json 解析失败: ${packageJsonPath}`);\r\n }\r\n}\r\n\r\nexport async function readPackageScripts(projectRoot: string): Promise<PackageScripts> {\r\n const packageJson = await readPackageJson(projectRoot);\r\n return packageJson.scripts ?? {};\r\n}\r\n","import { spawn } from \"node:child_process\";\nimport { mkdtempSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AppSettings, ProjectConfig } from \"../shared/types.js\";\nimport { findBashExecutablePath, findGitBashPath, findMinttyPath, shellSingleQuote, toGitBashPath } from \"./path.js\";\n\n/** Strip Git repo overrides so child processes (e.g. Vite calling `git`) use cwd discovery, not a stale inherited GIT_DIR. */\nconst GIT_ENV_KEYS = [\n \"GIT_DIR\",\n \"GIT_WORK_TREE\",\n \"GIT_INDEX_FILE\",\n \"GIT_PREFIX\",\n \"GIT_OBJECT_DIRECTORY\",\n \"GIT_ALTERNATE_OBJECT_DIRECTORIES\",\n] as const;\n\nfunction processEnvWithoutGitOverrides(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n for (const key of GIT_ENV_KEYS) {\n delete env[key];\n }\n return env;\n}\n\nexport type RunResult = {\n pid?: number;\n command: string;\n};\n\nexport function runScriptInGitBash(project: ProjectConfig, scriptName: string, settings: AppSettings): RunResult {\n const gitBashPath = findGitBashPath(settings.gitBashPath);\n\n if (!gitBashPath) {\n throw new Error(\"未找到 Git Bash,请先通过 br config 设置 git-bash.exe 路径\");\n }\n\n const title = createTerminalTitle(project.name, scriptName);\n const gitBashRoot = toGitBashPath(project.root);\n const scriptPath = writeRunScript(project, scriptName, title, gitBashRoot);\n const gitBashScriptPath = toGitBashPath(scriptPath);\n const bashPath = findBashExecutablePath(gitBashPath);\n const command = `${bashPath} --login ${scriptPath}`;\n\n const minttyPath = findMinttyPath(gitBashPath);\n const child = minttyPath\n ? spawn(minttyPath, [\"--title\", title, \"--dir\", gitBashRoot, toGitBashPath(bashPath), \"--login\", gitBashScriptPath], {\n detached: true,\n stdio: \"ignore\",\n windowsHide: false,\n env: processEnvWithoutGitOverrides(),\n })\n : spawn(\"cmd.exe\", [\"/d\", \"/c\", \"start\", title, \"/D\", project.root, bashPath, \"--login\", gitBashScriptPath], {\n detached: true,\n stdio: \"ignore\",\n windowsHide: false,\n env: processEnvWithoutGitOverrides(),\n });\n\n child.unref();\n\n return {\n pid: child.pid,\n command,\n };\n}\n\nfunction createTerminalTitle(projectName: string, scriptName: string): string {\n const title = `batch-run - ${projectName} - npm run ${scriptName}`;\n return title.replace(/[\\u0000-\\u001f\\u007f]/g, \" \").slice(0, 120);\n}\n\nfunction writeRunScript(project: ProjectConfig, scriptName: string, title: string, gitBashRoot: string): string {\n const tempDir = mkdtempSync(join(tmpdir(), \"batch-run-\"));\n const scriptPath = join(tempDir, \"run.sh\");\n const scriptContent = [\n \"#!/usr/bin/env bash\",\n \"set +e\",\n `cd ${shellSingleQuote(gitBashRoot)}`,\n 'rm -f \"$0\" >/dev/null 2>&1 || true',\n `unset ${GIT_ENV_KEYS.join(\" \")}`,\n `__batch_run_title=${shellSingleQuote(title)}`,\n \"set_batch_run_title() { printf '\\\\033]0;%s\\\\007' \\\"$__batch_run_title\\\"; }\",\n \"set_batch_run_title\",\n \"(while true; do set_batch_run_title; sleep 2; done) &\",\n \"__batch_run_title_pid=$!\",\n \"cleanup_batch_run_title() {\",\n ' kill \"$__batch_run_title_pid\" >/dev/null 2>&1 || true',\n ' wait \"$__batch_run_title_pid\" 2>/dev/null || true',\n \" set_batch_run_title\",\n \"}\",\n \"trap cleanup_batch_run_title EXIT\",\n `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,\n `npm run ${shellSingleQuote(scriptName)}`,\n \"__batch_run_exit_code=$?\",\n \"set_batch_run_title\",\n \"echo\",\n 'echo \"Process exited with code $__batch_run_exit_code. Press Ctrl+D or close this window.\"',\n \"bash --login\",\n 'exit \"$__batch_run_exit_code\"',\n \"\",\n ].join(\"\\n\");\n\n writeFileSync(scriptPath, scriptContent, \"utf8\");\n return scriptPath;\n}\n","import { existsSync } from \"node:fs\";\r\nimport { platform } from \"node:os\";\r\nimport { basename, dirname, join } from \"node:path\";\r\n\r\nconst defaultWindowsGitBashPaths = [\r\n \"C:\\\\Program Files\\\\Git\\\\git-bash.exe\",\r\n \"C:\\\\Program Files (x86)\\\\Git\\\\git-bash.exe\",\r\n];\r\n\r\nexport function findGitBashPath(configuredPath?: string): string | undefined {\r\n if (configuredPath && existsSync(configuredPath) && isGitBashExecutable(configuredPath)) {\r\n return configuredPath;\r\n }\r\n\r\n if (platform() !== \"win32\") {\r\n return undefined;\r\n }\r\n\r\n return defaultWindowsGitBashPaths.find((item) => existsSync(item));\r\n}\r\n\r\nexport function toGitBashPath(inputPath: string): string {\r\n const normalized = inputPath.replaceAll(\"\\\\\", \"/\");\r\n const driveMatch = /^([a-zA-Z]):\\/(.*)$/.exec(normalized);\r\n\r\n if (!driveMatch) {\r\n return normalized;\r\n }\r\n\r\n const [, drive, rest] = driveMatch;\r\n return `/${drive.toLowerCase()}/${rest}`;\r\n}\r\n\r\nexport function shellSingleQuote(value: string): string {\r\n return `'${value.replaceAll(\"'\", \"'\\\\''\")}'`;\r\n}\r\n\r\nexport function findMinttyPath(gitBashPath: string): string | undefined {\r\n const gitBashDir = dirname(gitBashPath);\r\n const candidates = [\r\n join(gitBashDir, \"usr\", \"bin\", \"mintty.exe\"),\r\n join(gitBashDir, \"mintty.exe\"),\r\n join(dirname(gitBashDir), \"usr\", \"bin\", \"mintty.exe\"),\r\n ];\r\n\r\n return candidates.find((minttyPath) => existsSync(minttyPath));\r\n}\r\n\r\nexport function findBashExecutablePath(gitBashPath: string): string {\r\n const executableName = basename(gitBashPath).toLowerCase();\r\n if (executableName === \"bash.exe\" || executableName === \"sh.exe\") {\r\n return gitBashPath;\r\n }\r\n\r\n const gitBashDir = dirname(gitBashPath);\r\n const candidates = [\r\n join(gitBashDir, \"bin\", \"bash.exe\"),\r\n join(gitBashDir, \"usr\", \"bin\", \"bash.exe\"),\r\n join(gitBashDir, \"bin\", \"sh.exe\"),\r\n join(gitBashDir, \"usr\", \"bin\", \"sh.exe\"),\r\n ];\r\n\r\n return candidates.find((bashPath) => existsSync(bashPath)) ?? gitBashPath;\r\n}\r\n\r\nfunction isGitBashExecutable(inputPath: string): boolean {\r\n const executableName = basename(inputPath).toLowerCase();\r\n return executableName === \"git-bash.exe\" || executableName === \"bash.exe\" || executableName === \"sh.exe\";\r\n}\r\n","import fastify from \"fastify\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { dirname, join } from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport open from \"open\";\r\nimport { getSettings } from \"../config/store.js\";\r\nimport { registerApiRoutes } from \"./routes.js\";\r\n\r\nfunction getWebRoot(): string {\r\n return join(dirname(dirname(fileURLToPath(import.meta.url))), \"web\");\r\n}\r\n\r\nexport async function startServer(options: { openBrowser?: boolean } = {}): Promise<void> {\r\n const settings = getSettings();\r\n const app = fastify({\r\n logger: false,\r\n });\r\n const webRoot = getWebRoot();\r\n\r\n await registerApiRoutes(app, settings);\r\n\r\n app.get(\"/\", async (_request, reply) => {\r\n return reply.type(\"text/html\").send(await readFile(join(webRoot, \"index.html\"), \"utf8\"));\r\n });\r\n\r\n app.get(\"/main.js\", async (_request, reply) => {\r\n return reply.type(\"application/javascript\").send(await readFile(join(webRoot, \"main.js\"), \"utf8\"));\r\n });\r\n\r\n app.get(\"/style.css\", async (_request, reply) => {\r\n return reply.type(\"text/css\").send(await readFile(join(webRoot, \"style.css\"), \"utf8\"));\r\n });\r\n\r\n app.get(\"/favicon.ico\", async (_request, reply) => {\r\n return reply.type(\"image/x-icon\").send(await readFile(join(webRoot, \"favicon.ico\")));\r\n });\r\n\r\n const url = await app.listen({\r\n host: \"127.0.0.1\",\r\n port: settings.port,\r\n });\r\n\r\n console.log(`Batch Run Web 已启动: ${url}`);\r\n\r\n if (options.openBrowser ?? true) {\r\n await open(url);\r\n }\r\n}\r\n","import type { FastifyInstance } from \"fastify\";\r\nimport open from \"open\";\r\nimport { addProject, getProject, getProjects, getTagOrder, getTags, removeProject, reorderProjects, reorderTagGroups, updateProject } from \"../config/store.js\";\r\nimport { readGitBranches, switchGitBranch } from \"../project/git.js\";\r\nimport { readPackageScripts } from \"../project/package-json.js\";\r\nimport { runScriptInGitBash } from \"../runner/git-bash.js\";\r\nimport type { AppSettings } from \"../shared/types.js\";\r\n\r\ntype AddProjectBody = {\r\n name?: string;\r\n root?: string;\r\n tag?: string;\r\n};\r\n\r\ntype UpdateProjectBody = AddProjectBody;\r\n\r\ntype RunScriptBody = {\r\n script?: string;\r\n};\r\n\r\ntype SwitchBranchBody = {\r\n branch?: string;\r\n};\r\n\r\ntype ReorderProjectsBody = {\r\n projectIds?: string[];\r\n};\r\n\r\ntype ReorderTagGroupsBody = {\r\n tagKeys?: string[];\r\n};\r\n\r\nexport async function registerApiRoutes(app: FastifyInstance, settings: AppSettings): Promise<void> {\r\n app.get(\"/api/projects\", async () => ({\r\n projects: getProjects(),\r\n tagOrder: getTagOrder(),\r\n }));\r\n\r\n app.get(\"/api/tags\", async () => ({\r\n tags: getTags(),\r\n tagOrder: getTagOrder(),\r\n }));\r\n\r\n app.post<{ Body: AddProjectBody }>(\"/api/projects\", async (request, reply) => {\r\n const name = request.body.name?.trim();\r\n const root = request.body.root?.trim();\r\n\r\n if (!name || !root) {\r\n return reply.code(400).send({ message: \"项目名称和根路径不能为空\" });\r\n }\r\n\r\n const project = await addProject({ name, root, tag: request.body.tag });\r\n return { project };\r\n });\r\n\r\n app.put<{ Body: ReorderProjectsBody }>(\"/api/projects/order\", async (request, reply) => {\r\n const projectIds = request.body.projectIds;\r\n\r\n if (!Array.isArray(projectIds) || !projectIds.every((projectId) => typeof projectId === \"string\")) {\r\n return reply.code(400).send({ message: \"项目排序数据无效\" });\r\n }\r\n\r\n try {\r\n return {\r\n projects: reorderProjects(projectIds),\r\n tagOrder: getTagOrder(),\r\n };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"保存项目排序失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.put<{ Body: ReorderTagGroupsBody }>(\"/api/tag-order\", async (request, reply) => {\r\n const tagKeys = request.body.tagKeys;\r\n\r\n if (!Array.isArray(tagKeys) || !tagKeys.every((tagKey) => typeof tagKey === \"string\")) {\r\n return reply.code(400).send({ message: \"标签排序数据无效\" });\r\n }\r\n\r\n try {\r\n return {\r\n projects: reorderTagGroups(tagKeys),\r\n tagOrder: getTagOrder(),\r\n };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"保存标签排序失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.put<{ Params: { id: string }; Body: UpdateProjectBody }>(\"/api/projects/:id\", async (request, reply) => {\r\n const name = request.body.name?.trim();\r\n const root = request.body.root?.trim();\r\n\r\n if (!name || !root) {\r\n return reply.code(400).send({ message: \"项目名称和根路径不能为空\" });\r\n }\r\n\r\n try {\r\n const project = await updateProject(request.params.id, { name, root, tag: request.body.tag });\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n return { project };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"更新项目失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.delete<{ Params: { id: string } }>(\"/api/projects/:id\", async (request, reply) => {\r\n const removed = removeProject(request.params.id);\r\n\r\n if (!removed) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n return { ok: true };\r\n });\r\n\r\n app.get<{ Params: { id: string } }>(\"/api/projects/:id/scripts\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n const scripts = await readPackageScripts(project.root);\r\n return { scripts };\r\n });\r\n\r\n app.get<{ Params: { id: string } }>(\"/api/projects/:id/branches\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n try {\r\n return await readGitBranches(project.root);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"读取 Git 分支失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.post<{ Params: { id: string }; Body: SwitchBranchBody }>(\"/api/projects/:id/branches/switch\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n const branch = request.body.branch?.trim();\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n if (!branch) {\r\n return reply.code(400).send({ message: \"分支名称不能为空\" });\r\n }\r\n\r\n try {\r\n const branchState = await switchGitBranch(project.root, branch);\r\n const scripts = await readPackageScripts(project.root);\r\n return {\r\n ...branchState,\r\n scripts,\r\n };\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"切换 Git 分支失败\";\r\n return reply.code(400).send({ message });\r\n }\r\n });\r\n\r\n app.post<{ Params: { id: string } }>(\"/api/projects/:id/open\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n await open(project.root);\r\n return { ok: true };\r\n });\r\n\r\n app.post<{ Params: { id: string }; Body: RunScriptBody }>(\"/api/projects/:id/run\", async (request, reply) => {\r\n const project = getProject(request.params.id);\r\n const script = request.body.script?.trim();\r\n\r\n if (!project) {\r\n return reply.code(404).send({ message: \"项目不存在\" });\r\n }\r\n\r\n if (!script) {\r\n return reply.code(400).send({ message: \"脚本名称不能为空\" });\r\n }\r\n\r\n const scripts = await readPackageScripts(project.root);\r\n if (!scripts[script]) {\r\n return reply.code(404).send({ message: \"脚本不存在\" });\r\n }\r\n\r\n const result = runScriptInGitBash(project, script, settings);\r\n return {\r\n ok: true,\r\n pid: result.pid,\r\n };\r\n });\r\n}\r\n","import { execFile } from \"node:child_process\";\r\nimport { promisify } from \"node:util\";\r\nimport type { GitBranch, GitBranchState } from \"../shared/types.js\";\r\n\r\nconst execFileAsync = promisify(execFile);\r\n\r\nexport async function readGitBranches(projectRoot: string): Promise<GitBranchState> {\r\n const [current, localBranches] = await Promise.all([\r\n runGit(projectRoot, [\"branch\", \"--show-current\"]),\r\n runGit(projectRoot, [\"branch\", \"--format=%(refname:short)\"]),\r\n ]);\r\n\r\n const currentBranch = current.trim() || undefined;\r\n const localNames = parseBranchLines(localBranches);\r\n const branches: GitBranch[] = localNames.map((name) => ({\r\n name,\r\n current: name === currentBranch,\r\n remote: false,\r\n }));\r\n\r\n return {\r\n current: currentBranch,\r\n branches,\r\n };\r\n}\r\n\r\nexport async function switchGitBranch(projectRoot: string, branchName: string): Promise<GitBranchState> {\r\n const name = branchName.trim();\r\n if (!name) {\r\n throw new Error(\"分支名称不能为空\");\r\n }\r\n\r\n const branchState = await readGitBranches(projectRoot);\r\n const branch = branchState.branches.find((item) => item.name === name);\r\n if (!branch) {\r\n throw new Error(\"本地分支不存在\");\r\n }\r\n\r\n if (branch.current) {\r\n return branchState;\r\n }\r\n\r\n await runGit(projectRoot, [\"switch\", name]);\r\n\r\n return readGitBranches(projectRoot);\r\n}\r\n\r\nasync function runGit(projectRoot: string, args: string[]): Promise<string> {\r\n try {\r\n const { stdout } = await execFileAsync(\"git\", args, {\r\n cwd: projectRoot,\r\n env: processEnvWithoutGitOverrides(),\r\n windowsHide: true,\r\n });\r\n return stdout;\r\n } catch (error) {\r\n const stderr = typeof error === \"object\" && error && \"stderr\" in error ? String(error.stderr) : \"\";\r\n const message = stderr.trim() || (error instanceof Error ? error.message : \"Git 命令执行失败\");\r\n throw new Error(message);\r\n }\r\n}\r\n\r\nfunction parseBranchLines(output: string): string[] {\r\n return output\r\n .split(/\\r?\\n/)\r\n .map((line) => line.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nfunction processEnvWithoutGitOverrides(): NodeJS.ProcessEnv {\r\n const env = { ...process.env };\r\n delete env.GIT_DIR;\r\n delete env.GIT_WORK_TREE;\r\n delete env.GIT_INDEX_FILE;\r\n delete env.GIT_PREFIX;\r\n delete env.GIT_OBJECT_DIRECTORY;\r\n delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;\r\n return env;\r\n}\r\n","import { startServer } from \"../../server/index.js\";\r\n\r\nexport async function uiCommand(): Promise<void> {\r\n await startServer({ openBrowser: true });\r\n}\r\n"],"mappings":"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","names":["prompts","prompts","prompts","prompts","prompts","prompts","packageJson","join","join","join","prompts","project","readFile","dirname","join","open","processEnvWithoutGitOverrides","join","dirname","readFile","open","prompts"]}
|
package/dist/web/index.html
CHANGED
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
项目名称
|
|
60
60
|
<input id="projectName" name="name" placeholder="例如:后台服务" required />
|
|
61
61
|
</label>
|
|
62
|
+
<label>
|
|
63
|
+
标签
|
|
64
|
+
<input id="projectTag" name="tag" list="tagOptions" placeholder="选择已有标签,或输入新标签(留空为未分组)" />
|
|
65
|
+
<datalist id="tagOptions"></datalist>
|
|
66
|
+
</label>
|
|
62
67
|
<label>
|
|
63
68
|
项目根路径
|
|
64
69
|
<input id="projectRoot" name="root" placeholder="C:\wang\project\server" required />
|
package/dist/web/main.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
const UNGROUPED_TAG = "";
|
|
2
|
+
const UNGROUPED_TAG_LABEL = "未分组";
|
|
3
|
+
|
|
1
4
|
const state = {
|
|
2
5
|
projects: [],
|
|
6
|
+
tagOrder: [],
|
|
3
7
|
branches: [],
|
|
4
8
|
currentProjectId: undefined,
|
|
5
9
|
draggedProjectId: undefined,
|
|
10
|
+
draggedTagKey: undefined,
|
|
6
11
|
suppressProjectClick: false,
|
|
7
12
|
editingProjectId: undefined,
|
|
13
|
+
tagExpandState: new Map(),
|
|
8
14
|
};
|
|
9
15
|
|
|
10
16
|
const projectList = document.querySelector("#projectList");
|
|
@@ -20,12 +26,16 @@ const closeProjectConfig = document.querySelector("#closeProjectConfig");
|
|
|
20
26
|
const projectConfigModal = document.querySelector("#projectConfigModal");
|
|
21
27
|
const projectNameInput = document.querySelector("#projectName");
|
|
22
28
|
const projectRootInput = document.querySelector("#projectRoot");
|
|
29
|
+
const projectTagInput = document.querySelector("#projectTag");
|
|
30
|
+
const tagOptions = document.querySelector("#tagOptions");
|
|
23
31
|
const projectConfigTitle = document.querySelector("#projectConfigTitle");
|
|
24
32
|
const projectSubmit = document.querySelector("#projectSubmit");
|
|
25
33
|
const branchPanel = document.querySelector("#branchPanel");
|
|
26
34
|
const branchSelect = document.querySelector("#branchSelect");
|
|
27
35
|
|
|
28
|
-
openProjectConfig.addEventListener("click",
|
|
36
|
+
openProjectConfig.addEventListener("click", () => {
|
|
37
|
+
openCreateProjectModal().catch((error) => setStatus(error.message));
|
|
38
|
+
});
|
|
29
39
|
|
|
30
40
|
closeProjectConfig.addEventListener("click", closeConfigModal);
|
|
31
41
|
|
|
@@ -47,6 +57,7 @@ projectForm.addEventListener("submit", async (event) => {
|
|
|
47
57
|
const formData = new FormData(projectForm);
|
|
48
58
|
const name = String(formData.get("name") ?? "");
|
|
49
59
|
const root = String(formData.get("root") ?? "");
|
|
60
|
+
const tag = String(formData.get("tag") ?? "");
|
|
50
61
|
const editingProjectId = state.editingProjectId;
|
|
51
62
|
|
|
52
63
|
await request(editingProjectId ? `/api/projects/${editingProjectId}` : "/api/projects", {
|
|
@@ -54,7 +65,7 @@ projectForm.addEventListener("submit", async (event) => {
|
|
|
54
65
|
headers: {
|
|
55
66
|
"Content-Type": "application/json",
|
|
56
67
|
},
|
|
57
|
-
body: JSON.stringify({ name, root }),
|
|
68
|
+
body: JSON.stringify({ name, root, tag }),
|
|
58
69
|
});
|
|
59
70
|
|
|
60
71
|
projectForm.reset();
|
|
@@ -104,6 +115,7 @@ branchSelect.addEventListener("change", async () => {
|
|
|
104
115
|
async function loadProjects() {
|
|
105
116
|
const data = await request("/api/projects");
|
|
106
117
|
state.projects = data.projects;
|
|
118
|
+
state.tagOrder = data.tagOrder ?? deriveTagOrder(state.projects);
|
|
107
119
|
if (!state.projects.some((project) => project.id === state.currentProjectId)) {
|
|
108
120
|
state.currentProjectId = undefined;
|
|
109
121
|
state.branches = [];
|
|
@@ -114,11 +126,59 @@ async function loadProjects() {
|
|
|
114
126
|
renderProjects();
|
|
115
127
|
}
|
|
116
128
|
|
|
129
|
+
async function loadTagOptions() {
|
|
130
|
+
const data = await request("/api/tags");
|
|
131
|
+
tagOptions.innerHTML = data.tags
|
|
132
|
+
.map((tag) => `<option value="${escapeHtml(tag)}"></option>`)
|
|
133
|
+
.join("");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function deriveTagOrder(projects) {
|
|
137
|
+
const keys = [];
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
|
|
140
|
+
for (const project of projects) {
|
|
141
|
+
const tagKey = getProjectTagKey(project);
|
|
142
|
+
if (!seen.has(tagKey)) {
|
|
143
|
+
seen.add(tagKey);
|
|
144
|
+
keys.push(tagKey);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return keys;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getProjectTagKey(project) {
|
|
152
|
+
return project.tag ?? UNGROUPED_TAG;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getTagLabel(tagKey) {
|
|
156
|
+
return tagKey === UNGROUPED_TAG ? UNGROUPED_TAG_LABEL : tagKey;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getProjectsByTag(tagKey) {
|
|
160
|
+
return state.projects.filter((project) => getProjectTagKey(project) === tagKey);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isTagExpanded(tagKey) {
|
|
164
|
+
if (state.tagExpandState.has(tagKey)) {
|
|
165
|
+
return state.tagExpandState.get(tagKey);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return tagKey === UNGROUPED_TAG;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toggleTagExpand(tagKey) {
|
|
172
|
+
state.tagExpandState.set(tagKey, !isTagExpanded(tagKey));
|
|
173
|
+
renderProjects();
|
|
174
|
+
}
|
|
175
|
+
|
|
117
176
|
function renderProjects() {
|
|
177
|
+
const tagOrder = state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects);
|
|
118
178
|
projectCount.textContent = `共 ${state.projects.length} 个项目`;
|
|
119
179
|
|
|
120
180
|
if (state.projects.length === 0) {
|
|
121
|
-
projectList.innerHTML = `<div class="empty"
|
|
181
|
+
projectList.innerHTML = `<div class="empty">暂无项目,请点击“新增项目”进行配置。</div>`;
|
|
122
182
|
currentProjectTitle.textContent = "请选择项目";
|
|
123
183
|
openProjectRoot.disabled = true;
|
|
124
184
|
hideBranchPanel();
|
|
@@ -128,73 +188,239 @@ function renderProjects() {
|
|
|
128
188
|
|
|
129
189
|
projectList.innerHTML = "";
|
|
130
190
|
|
|
131
|
-
for (const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
for (const tagKey of tagOrder) {
|
|
192
|
+
const projects = getProjectsByTag(tagKey);
|
|
193
|
+
if (projects.length === 0) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const expanded = isTagExpanded(tagKey);
|
|
198
|
+
const group = document.createElement("section");
|
|
199
|
+
group.className = `tag-group${expanded ? "" : " collapsed"}`;
|
|
200
|
+
group.dataset.tagKey = tagKey;
|
|
201
|
+
|
|
202
|
+
const header = document.createElement("div");
|
|
203
|
+
header.className = "tag-header";
|
|
204
|
+
header.dataset.tagKey = tagKey;
|
|
205
|
+
header.innerHTML = `
|
|
206
|
+
<button class="tag-toggle" type="button" aria-expanded="${expanded}" aria-label="${expanded ? "折叠" : "展开"} ${escapeHtml(getTagLabel(tagKey))}">
|
|
207
|
+
${expanded ? "▼" : "▶"}
|
|
208
|
+
</button>
|
|
209
|
+
<span class="tag-title" draggable="true">${escapeHtml(getTagLabel(tagKey))}</span>
|
|
210
|
+
<span class="tag-count">${projects.length}</span>
|
|
145
211
|
`;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
selectProject(project.id);
|
|
212
|
+
header.querySelector(".tag-toggle").addEventListener("click", (event) => {
|
|
213
|
+
event.stopPropagation();
|
|
214
|
+
toggleTagExpand(tagKey);
|
|
152
215
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
216
|
+
bindTagHeaderDrag(header, tagKey);
|
|
217
|
+
|
|
218
|
+
const projectsContainer = document.createElement("div");
|
|
219
|
+
projectsContainer.className = "tag-projects";
|
|
220
|
+
projectsContainer.dataset.tagKey = tagKey;
|
|
221
|
+
projectsContainer.hidden = !expanded;
|
|
222
|
+
bindTagProjectsDropZone(projectsContainer, tagKey);
|
|
223
|
+
|
|
224
|
+
for (const project of projects) {
|
|
225
|
+
projectsContainer.append(createProjectCard(project, tagKey));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
group.append(header, projectsContainer);
|
|
229
|
+
projectList.append(group);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function createProjectCard(project, tagKey) {
|
|
234
|
+
const card = document.createElement("article");
|
|
235
|
+
card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
|
|
236
|
+
card.draggable = true;
|
|
237
|
+
card.dataset.projectId = project.id;
|
|
238
|
+
card.dataset.tagKey = tagKey;
|
|
239
|
+
card.innerHTML = `
|
|
240
|
+
<div class="project-card-main">
|
|
241
|
+
<h3>${escapeHtml(project.name)}</h3>
|
|
242
|
+
<div class="path">${escapeHtml(project.root)}</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="project-actions">
|
|
245
|
+
<button class="project-edit" type="button" aria-label="编辑 ${escapeHtml(project.name)}">编辑</button>
|
|
246
|
+
<button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
|
|
247
|
+
</div>
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
card.addEventListener("click", () => {
|
|
251
|
+
if (state.suppressProjectClick) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
selectProject(project.id);
|
|
255
|
+
});
|
|
256
|
+
card.addEventListener("dragstart", (event) => {
|
|
257
|
+
state.draggedProjectId = project.id;
|
|
258
|
+
state.draggedTagKey = undefined;
|
|
259
|
+
state.suppressProjectClick = true;
|
|
260
|
+
event.dataTransfer.effectAllowed = "move";
|
|
261
|
+
event.dataTransfer.setData("text/plain", project.id);
|
|
262
|
+
card.classList.add("dragging");
|
|
263
|
+
});
|
|
264
|
+
card.addEventListener("dragover", (event) => {
|
|
265
|
+
if (!state.draggedProjectId || state.draggedProjectId === project.id) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
event.dataTransfer.dropEffect = "move";
|
|
271
|
+
card.classList.toggle("drop-after", shouldDropAfter(event, card));
|
|
272
|
+
});
|
|
273
|
+
card.addEventListener("dragleave", () => {
|
|
274
|
+
card.classList.remove("drop-after");
|
|
275
|
+
});
|
|
276
|
+
card.addEventListener("drop", (event) => {
|
|
277
|
+
if (!state.draggedProjectId) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.stopPropagation();
|
|
283
|
+
card.classList.remove("drop-after");
|
|
284
|
+
moveProject(
|
|
285
|
+
state.draggedProjectId,
|
|
286
|
+
project.id,
|
|
287
|
+
shouldDropAfter(event, card),
|
|
288
|
+
getProjectTagKey(project),
|
|
289
|
+
).catch((error) => {
|
|
290
|
+
setStatus(error.message);
|
|
159
291
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
292
|
+
});
|
|
293
|
+
card.addEventListener("dragend", () => {
|
|
294
|
+
state.draggedProjectId = undefined;
|
|
295
|
+
card.classList.remove("dragging", "drop-after");
|
|
296
|
+
clearTagDropIndicators();
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
state.suppressProjectClick = false;
|
|
299
|
+
}, 0);
|
|
300
|
+
});
|
|
301
|
+
card.querySelector(".project-delete").addEventListener("click", (event) => {
|
|
302
|
+
event.stopPropagation();
|
|
303
|
+
deleteProject(project);
|
|
304
|
+
});
|
|
305
|
+
card.querySelector(".project-edit").addEventListener("click", (event) => {
|
|
306
|
+
event.stopPropagation();
|
|
307
|
+
openEditProjectModal(project).catch((error) => setStatus(error.message));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return card;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function bindTagHeaderDrag(header, tagKey) {
|
|
314
|
+
const dragHandle = header.querySelector(".tag-title");
|
|
315
|
+
|
|
316
|
+
dragHandle.addEventListener("dragstart", (event) => {
|
|
317
|
+
state.draggedTagKey = tagKey;
|
|
318
|
+
state.draggedProjectId = undefined;
|
|
319
|
+
event.dataTransfer.effectAllowed = "move";
|
|
320
|
+
event.dataTransfer.setData("application/x-tag-key", tagKey);
|
|
321
|
+
header.classList.add("dragging");
|
|
322
|
+
});
|
|
323
|
+
header.addEventListener("dragover", (event) => {
|
|
324
|
+
if (state.draggedProjectId) {
|
|
325
|
+
const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
|
|
326
|
+
if (draggedProject && getProjectTagKey(draggedProject) !== tagKey) {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
event.stopPropagation();
|
|
329
|
+
event.dataTransfer.dropEffect = "move";
|
|
330
|
+
header.classList.add("collapsed-drop-target");
|
|
163
331
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (state.draggedTagKey === undefined || state.draggedTagKey === tagKey) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
event.preventDefault();
|
|
339
|
+
event.dataTransfer.dropEffect = "move";
|
|
340
|
+
header.classList.toggle("drop-after", shouldDropAfter(event, header));
|
|
341
|
+
});
|
|
342
|
+
header.addEventListener("dragleave", (event) => {
|
|
343
|
+
if (event.currentTarget.contains(event.relatedTarget)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
header.classList.remove("drop-after", "collapsed-drop-target");
|
|
347
|
+
});
|
|
348
|
+
header.addEventListener("drop", (event) => {
|
|
349
|
+
if (state.draggedProjectId) {
|
|
350
|
+
const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
|
|
351
|
+
if (!draggedProject || getProjectTagKey(draggedProject) === tagKey) {
|
|
173
352
|
return;
|
|
174
353
|
}
|
|
175
354
|
|
|
176
355
|
event.preventDefault();
|
|
177
|
-
|
|
178
|
-
|
|
356
|
+
event.stopPropagation();
|
|
357
|
+
header.classList.remove("collapsed-drop-target");
|
|
358
|
+
state.tagExpandState.set(tagKey, true);
|
|
359
|
+
moveProjectToTagEnd(state.draggedProjectId, tagKey).catch((error) => {
|
|
179
360
|
setStatus(error.message);
|
|
180
361
|
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (state.draggedTagKey === undefined || state.draggedTagKey === tagKey) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
event.stopPropagation();
|
|
371
|
+
header.classList.remove("drop-after");
|
|
372
|
+
moveTagGroup(state.draggedTagKey, tagKey, shouldDropAfter(event, header)).catch((error) => {
|
|
373
|
+
setStatus(error.message);
|
|
181
374
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
375
|
+
});
|
|
376
|
+
dragHandle.addEventListener("dragend", () => {
|
|
377
|
+
state.draggedTagKey = undefined;
|
|
378
|
+
header.classList.remove("dragging", "drop-after", "collapsed-drop-target");
|
|
379
|
+
clearTagDropIndicators();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function bindTagProjectsDropZone(container, tagKey) {
|
|
384
|
+
container.addEventListener("dragover", (event) => {
|
|
385
|
+
if (!state.draggedProjectId) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const draggedProject = state.projects.find((project) => project.id === state.draggedProjectId);
|
|
390
|
+
if (!draggedProject || getProjectTagKey(draggedProject) === tagKey) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
event.dataTransfer.dropEffect = "move";
|
|
397
|
+
container.classList.add("drag-over");
|
|
398
|
+
});
|
|
399
|
+
container.addEventListener("dragleave", (event) => {
|
|
400
|
+
if (event.currentTarget.contains(event.relatedTarget)) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
container.classList.remove("drag-over");
|
|
404
|
+
});
|
|
405
|
+
container.addEventListener("drop", (event) => {
|
|
406
|
+
if (!state.draggedProjectId) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
event.stopPropagation();
|
|
412
|
+
container.classList.remove("drag-over");
|
|
413
|
+
moveProjectToTagEnd(state.draggedProjectId, tagKey).catch((error) => {
|
|
414
|
+
setStatus(error.message);
|
|
196
415
|
});
|
|
197
|
-
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function clearTagDropIndicators() {
|
|
420
|
+
for (const element of projectList.querySelectorAll(
|
|
421
|
+
".tag-header.drop-after, .tag-header.collapsed-drop-target, .tag-projects.drag-over",
|
|
422
|
+
)) {
|
|
423
|
+
element.classList.remove("drop-after", "collapsed-drop-target", "drag-over");
|
|
198
424
|
}
|
|
199
425
|
}
|
|
200
426
|
|
|
@@ -292,24 +518,146 @@ function hideBranchPanel() {
|
|
|
292
518
|
branchSelect.disabled = true;
|
|
293
519
|
}
|
|
294
520
|
|
|
295
|
-
async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget) {
|
|
521
|
+
async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget, targetTagKey) {
|
|
296
522
|
if (draggedProjectId === targetProjectId) {
|
|
297
523
|
return;
|
|
298
524
|
}
|
|
299
525
|
|
|
526
|
+
const draggedProject = state.projects.find((project) => project.id === draggedProjectId);
|
|
527
|
+
if (!draggedProject) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const normalizedTag = targetTagKey === UNGROUPED_TAG ? undefined : targetTagKey;
|
|
532
|
+
const tagChanged = getProjectTagKey(draggedProject) !== targetTagKey;
|
|
533
|
+
|
|
534
|
+
if (tagChanged) {
|
|
535
|
+
await request(`/api/projects/${draggedProjectId}`, {
|
|
536
|
+
method: "PUT",
|
|
537
|
+
headers: {
|
|
538
|
+
"Content-Type": "application/json",
|
|
539
|
+
},
|
|
540
|
+
body: JSON.stringify({
|
|
541
|
+
name: draggedProject.name,
|
|
542
|
+
root: draggedProject.root,
|
|
543
|
+
tag: normalizedTag,
|
|
544
|
+
}),
|
|
545
|
+
});
|
|
546
|
+
draggedProject.tag = normalizedTag;
|
|
547
|
+
}
|
|
548
|
+
|
|
300
549
|
const nextProjects = [...state.projects];
|
|
301
550
|
const draggedIndex = nextProjects.findIndex((project) => project.id === draggedProjectId);
|
|
302
551
|
if (draggedIndex < 0) {
|
|
303
552
|
return;
|
|
304
553
|
}
|
|
305
554
|
|
|
306
|
-
const [
|
|
555
|
+
const [movingProject] = nextProjects.splice(draggedIndex, 1);
|
|
556
|
+
const updatedProject = { ...movingProject, tag: normalizedTag };
|
|
307
557
|
const targetIndex = nextProjects.findIndex((project) => project.id === targetProjectId);
|
|
308
558
|
if (targetIndex < 0) {
|
|
309
559
|
return;
|
|
310
560
|
}
|
|
311
561
|
|
|
312
|
-
nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0,
|
|
562
|
+
nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, updatedProject);
|
|
563
|
+
await persistProjectOrder(nextProjects, tagChanged ? "项目已移动到新标签。" : "项目排序已保存。");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function moveProjectToTagEnd(draggedProjectId, targetTagKey) {
|
|
567
|
+
const draggedProject = state.projects.find((project) => project.id === draggedProjectId);
|
|
568
|
+
if (!draggedProject) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const normalizedTag = targetTagKey === UNGROUPED_TAG ? undefined : targetTagKey;
|
|
573
|
+
const tagChanged = getProjectTagKey(draggedProject) !== targetTagKey;
|
|
574
|
+
|
|
575
|
+
if (tagChanged) {
|
|
576
|
+
await request(`/api/projects/${draggedProjectId}`, {
|
|
577
|
+
method: "PUT",
|
|
578
|
+
headers: {
|
|
579
|
+
"Content-Type": "application/json",
|
|
580
|
+
},
|
|
581
|
+
body: JSON.stringify({
|
|
582
|
+
name: draggedProject.name,
|
|
583
|
+
root: draggedProject.root,
|
|
584
|
+
tag: normalizedTag,
|
|
585
|
+
}),
|
|
586
|
+
});
|
|
587
|
+
draggedProject.tag = normalizedTag;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const nextProjects = state.projects.filter((project) => project.id !== draggedProjectId);
|
|
591
|
+
const updatedProject = { ...draggedProject, tag: normalizedTag };
|
|
592
|
+
const projectsInTargetTag = nextProjects.filter((project) => getProjectTagKey(project) === targetTagKey);
|
|
593
|
+
const lastProject = projectsInTargetTag[projectsInTargetTag.length - 1];
|
|
594
|
+
|
|
595
|
+
if (lastProject) {
|
|
596
|
+
const lastIndex = nextProjects.findIndex((project) => project.id === lastProject.id);
|
|
597
|
+
nextProjects.splice(lastIndex + 1, 0, updatedProject);
|
|
598
|
+
} else {
|
|
599
|
+
const tagOrder = state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects);
|
|
600
|
+
const targetTagIndex = tagOrder.indexOf(targetTagKey);
|
|
601
|
+
if (targetTagIndex < 0) {
|
|
602
|
+
nextProjects.push(updatedProject);
|
|
603
|
+
} else {
|
|
604
|
+
let insertIndex = nextProjects.length;
|
|
605
|
+
for (let index = targetTagIndex + 1; index < tagOrder.length; index += 1) {
|
|
606
|
+
const nextTagProjectIndex = nextProjects.findIndex(
|
|
607
|
+
(project) => getProjectTagKey(project) === tagOrder[index],
|
|
608
|
+
);
|
|
609
|
+
if (nextTagProjectIndex >= 0) {
|
|
610
|
+
insertIndex = nextTagProjectIndex;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
nextProjects.splice(insertIndex, 0, updatedProject);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
await persistProjectOrder(nextProjects, "项目已移动到新标签。");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function moveTagGroup(draggedTagKey, targetTagKey, insertAfterTarget) {
|
|
622
|
+
if (draggedTagKey === targetTagKey) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const nextTagOrder = [...(state.tagOrder.length > 0 ? state.tagOrder : deriveTagOrder(state.projects))];
|
|
627
|
+
const draggedIndex = nextTagOrder.indexOf(draggedTagKey);
|
|
628
|
+
if (draggedIndex < 0) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
nextTagOrder.splice(draggedIndex, 1);
|
|
633
|
+
const targetIndex = nextTagOrder.indexOf(targetTagKey);
|
|
634
|
+
if (targetIndex < 0) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
nextTagOrder.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, draggedTagKey);
|
|
639
|
+
state.tagOrder = nextTagOrder;
|
|
640
|
+
renderProjects();
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const data = await request("/api/tag-order", {
|
|
644
|
+
method: "PUT",
|
|
645
|
+
headers: {
|
|
646
|
+
"Content-Type": "application/json",
|
|
647
|
+
},
|
|
648
|
+
body: JSON.stringify({ tagKeys: nextTagOrder }),
|
|
649
|
+
});
|
|
650
|
+
state.projects = data.projects;
|
|
651
|
+
state.tagOrder = data.tagOrder ?? nextTagOrder;
|
|
652
|
+
renderProjects();
|
|
653
|
+
setStatus("标签排序已保存。");
|
|
654
|
+
} catch (error) {
|
|
655
|
+
await loadProjects();
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function persistProjectOrder(nextProjects, successMessage) {
|
|
313
661
|
state.projects = nextProjects;
|
|
314
662
|
renderProjects();
|
|
315
663
|
|
|
@@ -324,8 +672,9 @@ async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget)
|
|
|
324
672
|
}),
|
|
325
673
|
});
|
|
326
674
|
state.projects = data.projects;
|
|
675
|
+
state.tagOrder = data.tagOrder ?? deriveTagOrder(state.projects);
|
|
327
676
|
renderProjects();
|
|
328
|
-
setStatus(
|
|
677
|
+
setStatus(successMessage);
|
|
329
678
|
} catch (error) {
|
|
330
679
|
await loadProjects();
|
|
331
680
|
throw error;
|
|
@@ -337,21 +686,24 @@ function shouldDropAfter(event, element) {
|
|
|
337
686
|
return event.clientY > rect.top + rect.height / 2;
|
|
338
687
|
}
|
|
339
688
|
|
|
340
|
-
function openCreateProjectModal() {
|
|
689
|
+
async function openCreateProjectModal() {
|
|
341
690
|
state.editingProjectId = undefined;
|
|
342
691
|
projectForm.reset();
|
|
343
692
|
projectConfigTitle.textContent = "项目配置";
|
|
344
693
|
projectSubmit.textContent = "新增项目";
|
|
694
|
+
await loadTagOptions();
|
|
345
695
|
projectConfigModal.hidden = false;
|
|
346
696
|
projectNameInput.focus();
|
|
347
697
|
}
|
|
348
698
|
|
|
349
|
-
function openEditProjectModal(project) {
|
|
699
|
+
async function openEditProjectModal(project) {
|
|
350
700
|
state.editingProjectId = project.id;
|
|
351
701
|
projectConfigTitle.textContent = "编辑项目";
|
|
352
702
|
projectSubmit.textContent = "保存修改";
|
|
703
|
+
await loadTagOptions();
|
|
353
704
|
projectNameInput.value = project.name;
|
|
354
705
|
projectRootInput.value = project.root;
|
|
706
|
+
projectTagInput.value = project.tag ?? "";
|
|
355
707
|
projectConfigModal.hidden = false;
|
|
356
708
|
projectNameInput.focus();
|
|
357
709
|
}
|
package/dist/web/style.css
CHANGED
|
@@ -152,6 +152,109 @@ h2 {
|
|
|
152
152
|
align-content: start;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
.tag-group {
|
|
156
|
+
display: grid;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.tag-group + .tag-group {
|
|
161
|
+
margin-top: 14px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.tag-header {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 8px;
|
|
168
|
+
border: 1px dashed #cbd5e1;
|
|
169
|
+
border-radius: 12px;
|
|
170
|
+
background: #f1f5f9;
|
|
171
|
+
color: #334155;
|
|
172
|
+
padding: 8px 12px;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
user-select: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.tag-toggle {
|
|
179
|
+
flex: 0 0 auto;
|
|
180
|
+
width: 24px;
|
|
181
|
+
height: 24px;
|
|
182
|
+
border: 0;
|
|
183
|
+
border-radius: 6px;
|
|
184
|
+
background: transparent;
|
|
185
|
+
color: #475569;
|
|
186
|
+
padding: 0;
|
|
187
|
+
line-height: 1;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.tag-toggle:hover {
|
|
192
|
+
background: #e2e8f0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.tag-title {
|
|
196
|
+
flex: 1;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
cursor: grab;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.tag-title:active {
|
|
202
|
+
cursor: grabbing;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.tag-header.dragging .tag-title {
|
|
206
|
+
cursor: grabbing;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.tag-header.collapsed-drop-target {
|
|
210
|
+
border-color: #93c5fd;
|
|
211
|
+
background: #eff6ff;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.tag-group.collapsed .tag-projects {
|
|
215
|
+
display: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.tag-header.dragging {
|
|
219
|
+
opacity: 0.45;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.tag-header.drop-after::after,
|
|
223
|
+
.tag-group.drop-target::after {
|
|
224
|
+
display: block;
|
|
225
|
+
height: 3px;
|
|
226
|
+
margin-top: 6px;
|
|
227
|
+
border-radius: 999px;
|
|
228
|
+
background: #2563eb;
|
|
229
|
+
content: '';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.tag-group.drop-target {
|
|
233
|
+
border-radius: 12px;
|
|
234
|
+
outline: 2px dashed #93c5fd;
|
|
235
|
+
outline-offset: 2px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.tag-count {
|
|
239
|
+
flex: 0 0 auto;
|
|
240
|
+
color: #64748b;
|
|
241
|
+
font-size: 12px;
|
|
242
|
+
font-weight: 600;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.tag-projects {
|
|
246
|
+
display: grid;
|
|
247
|
+
gap: 10px;
|
|
248
|
+
min-height: 8px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.tag-projects.drag-over {
|
|
252
|
+
border-radius: 12px;
|
|
253
|
+
background: #eff6ff;
|
|
254
|
+
outline: 1px dashed #93c5fd;
|
|
255
|
+
outline-offset: 2px;
|
|
256
|
+
}
|
|
257
|
+
|
|
155
258
|
.content-body {
|
|
156
259
|
flex: 1;
|
|
157
260
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "batch-run-local",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Manage and run package.json scripts across local projects from CLI and a local web UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
],
|
|
25
25
|
"author": "",
|
|
26
26
|
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/liwangying55555/batch-run-local.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/liwangying55555/batch-run-local/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/liwangying55555/batch-run-local#readme",
|
|
27
35
|
"engines": {
|
|
28
36
|
"node": ">=18"
|
|
29
37
|
},
|