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 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
- store.set("projects", [...projects, project]);
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
- for (const project of projects) {
186
- console.log(`${project.name}`);
187
- console.log(` id: ${project.id}`);
188
- console.log(` root: ${project.root}`);
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 { projects: reorderProjects(projectIds) };
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
  }
@@ -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"]}
@@ -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", openCreateProjectModal);
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">暂无项目,请点击“项目配置”进行配置。</div>`;
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 project of state.projects) {
132
- const card = document.createElement("article");
133
- card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
134
- card.draggable = true;
135
- card.dataset.projectId = project.id;
136
- card.innerHTML = `
137
- <div class="project-card-main">
138
- <h3>${escapeHtml(project.name)}</h3>
139
- <div class="path">${escapeHtml(project.root)}</div>
140
- </div>
141
- <div class="project-actions">
142
- <button class="project-edit" type="button" aria-label="编辑 ${escapeHtml(project.name)}">编辑</button>
143
- <button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
144
- </div>
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
- card.addEventListener("click", () => {
148
- if (state.suppressProjectClick) {
149
- return;
150
- }
151
- selectProject(project.id);
212
+ header.querySelector(".tag-toggle").addEventListener("click", (event) => {
213
+ event.stopPropagation();
214
+ toggleTagExpand(tagKey);
152
215
  });
153
- card.addEventListener("dragstart", (event) => {
154
- state.draggedProjectId = project.id;
155
- state.suppressProjectClick = true;
156
- event.dataTransfer.effectAllowed = "move";
157
- event.dataTransfer.setData("text/plain", project.id);
158
- card.classList.add("dragging");
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
- card.addEventListener("dragover", (event) => {
161
- if (!state.draggedProjectId || state.draggedProjectId === project.id) {
162
- return;
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
- event.preventDefault();
165
- event.dataTransfer.dropEffect = "move";
166
- card.classList.toggle("drop-after", shouldDropAfter(event, card));
167
- });
168
- card.addEventListener("dragleave", () => {
169
- card.classList.remove("drop-after");
170
- });
171
- card.addEventListener("drop", (event) => {
172
- if (!state.draggedProjectId) {
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
- card.classList.remove("drop-after");
178
- moveProject(state.draggedProjectId, project.id, shouldDropAfter(event, card)).catch((error) => {
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
- card.addEventListener("dragend", () => {
183
- state.draggedProjectId = undefined;
184
- card.classList.remove("dragging", "drop-after");
185
- setTimeout(() => {
186
- state.suppressProjectClick = false;
187
- }, 0);
188
- });
189
- card.querySelector(".project-delete").addEventListener("click", (event) => {
190
- event.stopPropagation();
191
- deleteProject(project);
192
- });
193
- card.querySelector(".project-edit").addEventListener("click", (event) => {
194
- event.stopPropagation();
195
- openEditProjectModal(project);
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
- projectList.append(card);
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 [draggedProject] = nextProjects.splice(draggedIndex, 1);
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, draggedProject);
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
  }
@@ -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.1.2",
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
  },