batch-run-local 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -57,12 +57,51 @@ async function addProject(input) {
57
57
  store.set("projects", [...projects, project]);
58
58
  return project;
59
59
  }
60
+ async function updateProject(projectId, input) {
61
+ const root = resolve(input.root);
62
+ await access(root);
63
+ const projects = getProjects();
64
+ const projectIndex = projects.findIndex((project) => project.id === projectId);
65
+ if (projectIndex < 0) {
66
+ return void 0;
67
+ }
68
+ if (projects.some((project) => project.id !== projectId && project.root.toLowerCase() === root.toLowerCase())) {
69
+ throw new Error("\u8BE5\u9879\u76EE\u8DEF\u5F84\u5DF2\u7ECF\u5B58\u5728");
70
+ }
71
+ const nextProject = {
72
+ ...projects[projectIndex],
73
+ name: input.name.trim(),
74
+ root,
75
+ updatedAt: Date.now()
76
+ };
77
+ const nextProjects = [...projects];
78
+ nextProjects[projectIndex] = nextProject;
79
+ store.set("projects", nextProjects);
80
+ return nextProject;
81
+ }
60
82
  function removeProject(projectId) {
61
83
  const projects = getProjects();
62
84
  const nextProjects = projects.filter((project) => project.id !== projectId);
63
85
  store.set("projects", nextProjects);
64
86
  return nextProjects.length !== projects.length;
65
87
  }
88
+ function reorderProjects(projectIds) {
89
+ const projects = getProjects();
90
+ const projectMap = new Map(projects.map((project) => [project.id, project]));
91
+ const uniqueProjectIds = new Set(projectIds);
92
+ if (uniqueProjectIds.size !== projects.length) {
93
+ throw new Error("\u9879\u76EE\u6392\u5E8F\u6570\u636E\u4E0D\u5B8C\u6574");
94
+ }
95
+ const nextProjects = projectIds.map((projectId) => {
96
+ const project = projectMap.get(projectId);
97
+ if (!project) {
98
+ throw new Error("\u9879\u76EE\u6392\u5E8F\u5305\u542B\u4E0D\u5B58\u5728\u7684\u9879\u76EE");
99
+ }
100
+ return project;
101
+ });
102
+ store.set("projects", nextProjects);
103
+ return nextProjects;
104
+ }
66
105
  function getSettings() {
67
106
  return getConfig().settings;
68
107
  }
@@ -199,17 +238,20 @@ async function readPackageScripts(projectRoot) {
199
238
 
200
239
  // src/runner/git-bash.ts
201
240
  import { spawn } from "child_process";
241
+ import { mkdtempSync, writeFileSync } from "fs";
242
+ import { tmpdir } from "os";
243
+ import { join as join3 } from "path";
202
244
 
203
245
  // src/runner/path.ts
204
246
  import { existsSync } from "fs";
205
247
  import { platform } from "os";
206
- import { dirname, join as join2 } from "path";
248
+ import { basename, dirname, join as join2 } from "path";
207
249
  var defaultWindowsGitBashPaths = [
208
250
  "C:\\Program Files\\Git\\git-bash.exe",
209
251
  "C:\\Program Files (x86)\\Git\\git-bash.exe"
210
252
  ];
211
253
  function findGitBashPath(configuredPath) {
212
- if (configuredPath && existsSync(configuredPath)) {
254
+ if (configuredPath && existsSync(configuredPath) && isGitBashExecutable(configuredPath)) {
213
255
  return configuredPath;
214
256
  }
215
257
  if (platform() !== "win32") {
@@ -230,8 +272,31 @@ function shellSingleQuote(value) {
230
272
  return `'${value.replaceAll("'", "'\\''")}'`;
231
273
  }
232
274
  function findMinttyPath(gitBashPath) {
233
- const minttyPath = join2(dirname(gitBashPath), "usr", "bin", "mintty.exe");
234
- return existsSync(minttyPath) ? minttyPath : void 0;
275
+ const gitBashDir = dirname(gitBashPath);
276
+ const candidates = [
277
+ join2(gitBashDir, "usr", "bin", "mintty.exe"),
278
+ join2(gitBashDir, "mintty.exe"),
279
+ join2(dirname(gitBashDir), "usr", "bin", "mintty.exe")
280
+ ];
281
+ return candidates.find((minttyPath) => existsSync(minttyPath));
282
+ }
283
+ function findBashExecutablePath(gitBashPath) {
284
+ const executableName = basename(gitBashPath).toLowerCase();
285
+ if (executableName === "bash.exe" || executableName === "sh.exe") {
286
+ return gitBashPath;
287
+ }
288
+ const gitBashDir = dirname(gitBashPath);
289
+ const candidates = [
290
+ join2(gitBashDir, "bin", "bash.exe"),
291
+ join2(gitBashDir, "usr", "bin", "bash.exe"),
292
+ join2(gitBashDir, "bin", "sh.exe"),
293
+ join2(gitBashDir, "usr", "bin", "sh.exe")
294
+ ];
295
+ return candidates.find((bashPath) => existsSync(bashPath)) ?? gitBashPath;
296
+ }
297
+ function isGitBashExecutable(inputPath) {
298
+ const executableName = basename(inputPath).toLowerCase();
299
+ return executableName === "git-bash.exe" || executableName === "bash.exe" || executableName === "sh.exe";
235
300
  }
236
301
 
237
302
  // src/runner/git-bash.ts
@@ -240,19 +305,18 @@ function runScriptInGitBash(project, scriptName, settings) {
240
305
  if (!gitBashPath) {
241
306
  throw new Error("\u672A\u627E\u5230 Git Bash\uFF0C\u8BF7\u5148\u901A\u8FC7 br config \u8BBE\u7F6E git-bash.exe \u8DEF\u5F84");
242
307
  }
243
- const command = [
244
- `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,
245
- `npm run ${shellSingleQuote(scriptName)}`,
246
- "echo",
247
- "echo Process exited. Press Ctrl+D or close this window.",
248
- "exec bash"
249
- ].join("; ");
308
+ const title = createTerminalTitle(project.name, scriptName);
309
+ const gitBashRoot = toGitBashPath(project.root);
310
+ const scriptPath = writeRunScript(project, scriptName, title, gitBashRoot);
311
+ const gitBashScriptPath = toGitBashPath(scriptPath);
312
+ const bashPath = findBashExecutablePath(gitBashPath);
313
+ const command = `${bashPath} --login ${scriptPath}`;
250
314
  const minttyPath = findMinttyPath(gitBashPath);
251
- const child = minttyPath ? spawn(minttyPath, ["--dir", toGitBashPath(project.root), "/usr/bin/bash", "-lc", command], {
315
+ const child = minttyPath ? spawn(minttyPath, ["--title", title, "--dir", gitBashRoot, toGitBashPath(bashPath), "--login", gitBashScriptPath], {
252
316
  detached: true,
253
317
  stdio: "ignore",
254
318
  windowsHide: false
255
- }) : spawn("cmd.exe", ["/d", "/c", "start", "", gitBashPath, `--cd=${toGitBashPath(project.root)}`, "-c", command], {
319
+ }) : spawn("cmd.exe", ["/d", "/c", "start", title, "/D", project.root, bashPath, "--login", gitBashScriptPath], {
256
320
  detached: true,
257
321
  stdio: "ignore",
258
322
  windowsHide: false
@@ -263,6 +327,42 @@ function runScriptInGitBash(project, scriptName, settings) {
263
327
  command
264
328
  };
265
329
  }
330
+ function createTerminalTitle(projectName, scriptName) {
331
+ const title = `batch-run - ${projectName} - npm run ${scriptName}`;
332
+ return title.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 120);
333
+ }
334
+ function writeRunScript(project, scriptName, title, gitBashRoot) {
335
+ const tempDir = mkdtempSync(join3(tmpdir(), "batch-run-"));
336
+ const scriptPath = join3(tempDir, "run.sh");
337
+ const scriptContent = [
338
+ "#!/usr/bin/env bash",
339
+ "set +e",
340
+ `cd ${shellSingleQuote(gitBashRoot)}`,
341
+ 'rm -f "$0" >/dev/null 2>&1 || true',
342
+ `__batch_run_title=${shellSingleQuote(title)}`,
343
+ `set_batch_run_title() { printf '\\033]0;%s\\007' "$__batch_run_title"; }`,
344
+ "set_batch_run_title",
345
+ "(while true; do set_batch_run_title; sleep 2; done) &",
346
+ "__batch_run_title_pid=$!",
347
+ "cleanup_batch_run_title() {",
348
+ ' kill "$__batch_run_title_pid" >/dev/null 2>&1 || true',
349
+ ' wait "$__batch_run_title_pid" 2>/dev/null || true',
350
+ " set_batch_run_title",
351
+ "}",
352
+ "trap cleanup_batch_run_title EXIT",
353
+ `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,
354
+ `npm run ${shellSingleQuote(scriptName)}`,
355
+ "__batch_run_exit_code=$?",
356
+ "set_batch_run_title",
357
+ "echo",
358
+ 'echo "Process exited with code $__batch_run_exit_code. Press Ctrl+D or close this window."',
359
+ "bash --login",
360
+ 'exit "$__batch_run_exit_code"',
361
+ ""
362
+ ].join("\n");
363
+ writeFileSync(scriptPath, scriptContent, "utf8");
364
+ return scriptPath;
365
+ }
266
366
 
267
367
  // src/cli/commands/run.ts
268
368
  async function runCommand() {
@@ -314,7 +414,7 @@ async function runCommand() {
314
414
  // src/server/index.ts
315
415
  import fastify from "fastify";
316
416
  import { readFile as readFile2 } from "fs/promises";
317
- import { dirname as dirname2, join as join3 } from "path";
417
+ import { dirname as dirname2, join as join4 } from "path";
318
418
  import { fileURLToPath } from "url";
319
419
  import open2 from "open";
320
420
 
@@ -333,6 +433,35 @@ async function registerApiRoutes(app, settings) {
333
433
  const project = await addProject({ name, root });
334
434
  return { project };
335
435
  });
436
+ app.put("/api/projects/order", async (request, reply) => {
437
+ const projectIds = request.body.projectIds;
438
+ if (!Array.isArray(projectIds) || !projectIds.every((projectId) => typeof projectId === "string")) {
439
+ return reply.code(400).send({ message: "\u9879\u76EE\u6392\u5E8F\u6570\u636E\u65E0\u6548" });
440
+ }
441
+ try {
442
+ return { projects: reorderProjects(projectIds) };
443
+ } catch (error) {
444
+ const message = error instanceof Error ? error.message : "\u4FDD\u5B58\u9879\u76EE\u6392\u5E8F\u5931\u8D25";
445
+ return reply.code(400).send({ message });
446
+ }
447
+ });
448
+ app.put("/api/projects/:id", async (request, reply) => {
449
+ const name = request.body.name?.trim();
450
+ const root = request.body.root?.trim();
451
+ if (!name || !root) {
452
+ return reply.code(400).send({ message: "\u9879\u76EE\u540D\u79F0\u548C\u6839\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A" });
453
+ }
454
+ try {
455
+ const project = await updateProject(request.params.id, { name, root });
456
+ if (!project) {
457
+ return reply.code(404).send({ message: "\u9879\u76EE\u4E0D\u5B58\u5728" });
458
+ }
459
+ return { project };
460
+ } catch (error) {
461
+ const message = error instanceof Error ? error.message : "\u66F4\u65B0\u9879\u76EE\u5931\u8D25";
462
+ return reply.code(400).send({ message });
463
+ }
464
+ });
336
465
  app.delete("/api/projects/:id", async (request, reply) => {
337
466
  const removed = removeProject(request.params.id);
338
467
  if (!removed) {
@@ -379,7 +508,7 @@ async function registerApiRoutes(app, settings) {
379
508
 
380
509
  // src/server/index.ts
381
510
  function getWebRoot() {
382
- return join3(dirname2(dirname2(fileURLToPath(import.meta.url))), "web");
511
+ return join4(dirname2(dirname2(fileURLToPath(import.meta.url))), "web");
383
512
  }
384
513
  async function startServer(options = {}) {
385
514
  const settings = getSettings();
@@ -389,13 +518,16 @@ async function startServer(options = {}) {
389
518
  const webRoot = getWebRoot();
390
519
  await registerApiRoutes(app, settings);
391
520
  app.get("/", async (_request, reply) => {
392
- return reply.type("text/html").send(await readFile2(join3(webRoot, "index.html"), "utf8"));
521
+ return reply.type("text/html").send(await readFile2(join4(webRoot, "index.html"), "utf8"));
393
522
  });
394
523
  app.get("/main.js", async (_request, reply) => {
395
- return reply.type("application/javascript").send(await readFile2(join3(webRoot, "main.js"), "utf8"));
524
+ return reply.type("application/javascript").send(await readFile2(join4(webRoot, "main.js"), "utf8"));
396
525
  });
397
526
  app.get("/style.css", async (_request, reply) => {
398
- return reply.type("text/css").send(await readFile2(join3(webRoot, "style.css"), "utf8"));
527
+ return reply.type("text/css").send(await readFile2(join4(webRoot, "style.css"), "utf8"));
528
+ });
529
+ app.get("/favicon.ico", async (_request, reply) => {
530
+ return reply.type("image/x-icon").send(await readFile2(join4(webRoot, "favicon.ico")));
399
531
  });
400
532
  const url = await app.listen({
401
533
  host: "127.0.0.1",
@@ -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/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 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 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\";\r\nimport type { AppSettings, ProjectConfig } from \"../shared/types.js\";\r\nimport { findGitBashPath, findMinttyPath, shellSingleQuote, toGitBashPath } from \"./path.js\";\r\n\r\nexport type RunResult = {\r\n pid?: number;\r\n command: string;\r\n};\r\n\r\nexport function runScriptInGitBash(project: ProjectConfig, scriptName: string, settings: AppSettings): RunResult {\r\n const gitBashPath = findGitBashPath(settings.gitBashPath);\r\n\r\n if (!gitBashPath) {\r\n throw new Error(\"未找到 Git Bash,请先通过 br config 设置 git-bash.exe 路径\");\r\n }\r\n\r\n const command = [\r\n `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,\r\n `npm run ${shellSingleQuote(scriptName)}`,\r\n \"echo\",\r\n \"echo Process exited. Press Ctrl+D or close this window.\",\r\n \"exec bash\",\r\n ].join(\"; \");\r\n\r\n const minttyPath = findMinttyPath(gitBashPath);\r\n const child = minttyPath\r\n ? spawn(minttyPath, [\"--dir\", toGitBashPath(project.root), \"/usr/bin/bash\", \"-lc\", command], {\r\n detached: true,\r\n stdio: \"ignore\",\r\n windowsHide: false,\r\n })\r\n : spawn(\"cmd.exe\", [\"/d\", \"/c\", \"start\", \"\", gitBashPath, `--cd=${toGitBashPath(project.root)}`, \"-c\", command], {\r\n detached: true,\r\n stdio: \"ignore\",\r\n windowsHide: false,\r\n });\r\n\r\n child.unref();\r\n\r\n return {\r\n pid: child.pid,\r\n command,\r\n };\r\n}\r\n","import { existsSync } from \"node:fs\";\r\nimport { platform } from \"node:os\";\r\nimport { 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)) {\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 minttyPath = join(dirname(gitBashPath), \"usr\", \"bin\", \"mintty.exe\");\r\n return existsSync(minttyPath) ? minttyPath : undefined;\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 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 } from \"../config/store.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 RunScriptBody = {\r\n script?: 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.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.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 { 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","prompts","project","readFile","dirname","join","open","join","dirname","readFile","open","prompts"]}
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/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\";\r\nimport { mkdtempSync, writeFileSync } from \"node:fs\";\r\nimport { tmpdir } from \"node:os\";\r\nimport { join } from \"node:path\";\r\nimport type { AppSettings, ProjectConfig } from \"../shared/types.js\";\r\nimport { findBashExecutablePath, findGitBashPath, findMinttyPath, shellSingleQuote, toGitBashPath } from \"./path.js\";\r\n\r\nexport type RunResult = {\r\n pid?: number;\r\n command: string;\r\n};\r\n\r\nexport function runScriptInGitBash(project: ProjectConfig, scriptName: string, settings: AppSettings): RunResult {\r\n const gitBashPath = findGitBashPath(settings.gitBashPath);\r\n\r\n if (!gitBashPath) {\r\n throw new Error(\"未找到 Git Bash,请先通过 br config 设置 git-bash.exe 路径\");\r\n }\r\n\r\n const title = createTerminalTitle(project.name, scriptName);\r\n const gitBashRoot = toGitBashPath(project.root);\r\n const scriptPath = writeRunScript(project, scriptName, title, gitBashRoot);\r\n const gitBashScriptPath = toGitBashPath(scriptPath);\r\n const bashPath = findBashExecutablePath(gitBashPath);\r\n const command = `${bashPath} --login ${scriptPath}`;\r\n\r\n const minttyPath = findMinttyPath(gitBashPath);\r\n const child = minttyPath\r\n ? spawn(minttyPath, [\"--title\", title, \"--dir\", gitBashRoot, toGitBashPath(bashPath), \"--login\", gitBashScriptPath], {\r\n detached: true,\r\n stdio: \"ignore\",\r\n windowsHide: false,\r\n })\r\n : spawn(\"cmd.exe\", [\"/d\", \"/c\", \"start\", title, \"/D\", project.root, bashPath, \"--login\", gitBashScriptPath], {\r\n detached: true,\r\n stdio: \"ignore\",\r\n windowsHide: false,\r\n });\r\n\r\n child.unref();\r\n\r\n return {\r\n pid: child.pid,\r\n command,\r\n };\r\n}\r\n\r\nfunction createTerminalTitle(projectName: string, scriptName: string): string {\r\n const title = `batch-run - ${projectName} - npm run ${scriptName}`;\r\n return title.replace(/[\\u0000-\\u001f\\u007f]/g, \" \").slice(0, 120);\r\n}\r\n\r\nfunction writeRunScript(project: ProjectConfig, scriptName: string, title: string, gitBashRoot: string): string {\r\n const tempDir = mkdtempSync(join(tmpdir(), \"batch-run-\"));\r\n const scriptPath = join(tempDir, \"run.sh\");\r\n const scriptContent = [\r\n \"#!/usr/bin/env bash\",\r\n \"set +e\",\r\n `cd ${shellSingleQuote(gitBashRoot)}`,\r\n 'rm -f \"$0\" >/dev/null 2>&1 || true',\r\n `__batch_run_title=${shellSingleQuote(title)}`,\r\n \"set_batch_run_title() { printf '\\\\033]0;%s\\\\007' \\\"$__batch_run_title\\\"; }\",\r\n \"set_batch_run_title\",\r\n \"(while true; do set_batch_run_title; sleep 2; done) &\",\r\n \"__batch_run_title_pid=$!\",\r\n \"cleanup_batch_run_title() {\",\r\n ' kill \"$__batch_run_title_pid\" >/dev/null 2>&1 || true',\r\n ' wait \"$__batch_run_title_pid\" 2>/dev/null || true',\r\n \" set_batch_run_title\",\r\n \"}\",\r\n \"trap cleanup_batch_run_title EXIT\",\r\n `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,\r\n `npm run ${shellSingleQuote(scriptName)}`,\r\n \"__batch_run_exit_code=$?\",\r\n \"set_batch_run_title\",\r\n \"echo\",\r\n 'echo \"Process exited with code $__batch_run_exit_code. Press Ctrl+D or close this window.\"',\r\n \"bash --login\",\r\n 'exit \"$__batch_run_exit_code\"',\r\n \"\",\r\n ].join(\"\\n\");\r\n\r\n writeFileSync(scriptPath, scriptContent, \"utf8\");\r\n return scriptPath;\r\n}\r\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 { 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 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.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 { 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","join","dirname","readFile","open","prompts"]}
Binary file
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Batch Run</title>
7
7
  <link rel="stylesheet" href="/style.css" />
8
+ <link rel="icon" href="/favicon.ico" type="image/x-icon" />
8
9
  </head>
9
10
  <body>
10
11
  <main class="app">
@@ -12,23 +13,15 @@
12
13
  <div class="panel-header">
13
14
  <div>
14
15
  <p class="eyebrow">Batch Run</p>
15
- <h1>项目配置</h1>
16
+ <h2>项目列表</h2>
17
+ <p id="projectCount" class="project-count">共 0 个项目</p>
18
+ </div>
19
+ <div class="header-actions">
20
+ <button id="openProjectConfig" class="secondary" type="button">项目配置</button>
21
+ <button id="refreshProjects" type="button">刷新</button>
16
22
  </div>
17
- <button id="refreshProjects" type="button">刷新</button>
18
23
  </div>
19
24
 
20
- <form id="projectForm" class="form">
21
- <label>
22
- 项目名称
23
- <input id="projectName" name="name" placeholder="例如:后台服务" required />
24
- </label>
25
- <label>
26
- 项目根路径
27
- <input id="projectRoot" name="root" placeholder="C:\wang\project\server" required />
28
- </label>
29
- <button type="submit">新增项目</button>
30
- </form>
31
-
32
25
  <div id="projectList" class="project-list"></div>
33
26
  </section>
34
27
 
@@ -41,11 +34,37 @@
41
34
  <button id="openProjectRoot" class="secondary" type="button" disabled>打开项目目录</button>
42
35
  </div>
43
36
 
44
- <div id="scriptList" class="script-list empty">选择左侧项目后读取 scripts</div>
45
- <pre id="status" class="status"></pre>
37
+ <div class="content-body">
38
+ <div id="scriptList" class="script-list empty">选择左侧项目后读取 scripts</div>
39
+ <pre id="status" class="status"></pre>
40
+ </div>
46
41
  </section>
47
42
  </main>
48
43
 
44
+ <div id="projectConfigModal" class="modal-backdrop" hidden>
45
+ <section class="modal" role="dialog" aria-modal="true" aria-labelledby="projectConfigTitle">
46
+ <div class="panel-header">
47
+ <div>
48
+ <p class="eyebrow">Project Settings</p>
49
+ <h2 id="projectConfigTitle">项目配置</h2>
50
+ </div>
51
+ <button id="closeProjectConfig" class="secondary" type="button">关闭</button>
52
+ </div>
53
+
54
+ <form id="projectForm" class="form">
55
+ <label>
56
+ 项目名称
57
+ <input id="projectName" name="name" placeholder="例如:后台服务" required />
58
+ </label>
59
+ <label>
60
+ 项目根路径
61
+ <input id="projectRoot" name="root" placeholder="C:\wang\project\server" required />
62
+ </label>
63
+ <button id="projectSubmit" type="submit">新增项目</button>
64
+ </form>
65
+ </section>
66
+ </div>
67
+
49
68
  <script type="module" src="/main.js"></script>
50
69
  </body>
51
70
  </html>
package/dist/web/main.js CHANGED
@@ -1,6 +1,9 @@
1
1
  const state = {
2
2
  projects: [],
3
3
  currentProjectId: undefined,
4
+ draggedProjectId: undefined,
5
+ suppressProjectClick: false,
6
+ editingProjectId: undefined,
4
7
  };
5
8
 
6
9
  const projectList = document.querySelector("#projectList");
@@ -8,8 +11,32 @@ const scriptList = document.querySelector("#scriptList");
8
11
  const statusBox = document.querySelector("#status");
9
12
  const currentProjectTitle = document.querySelector("#currentProjectTitle");
10
13
  const projectForm = document.querySelector("#projectForm");
14
+ const projectCount = document.querySelector("#projectCount");
11
15
  const refreshProjects = document.querySelector("#refreshProjects");
12
16
  const openProjectRoot = document.querySelector("#openProjectRoot");
17
+ const openProjectConfig = document.querySelector("#openProjectConfig");
18
+ const closeProjectConfig = document.querySelector("#closeProjectConfig");
19
+ const projectConfigModal = document.querySelector("#projectConfigModal");
20
+ const projectNameInput = document.querySelector("#projectName");
21
+ const projectRootInput = document.querySelector("#projectRoot");
22
+ const projectConfigTitle = document.querySelector("#projectConfigTitle");
23
+ const projectSubmit = document.querySelector("#projectSubmit");
24
+
25
+ openProjectConfig.addEventListener("click", openCreateProjectModal);
26
+
27
+ closeProjectConfig.addEventListener("click", closeConfigModal);
28
+
29
+ projectConfigModal.addEventListener("click", (event) => {
30
+ if (event.target === projectConfigModal) {
31
+ closeConfigModal();
32
+ }
33
+ });
34
+
35
+ document.addEventListener("keydown", (event) => {
36
+ if (event.key === "Escape" && !projectConfigModal.hidden) {
37
+ closeConfigModal();
38
+ }
39
+ });
13
40
 
14
41
  projectForm.addEventListener("submit", async (event) => {
15
42
  event.preventDefault();
@@ -17,9 +44,10 @@ projectForm.addEventListener("submit", async (event) => {
17
44
  const formData = new FormData(projectForm);
18
45
  const name = String(formData.get("name") ?? "");
19
46
  const root = String(formData.get("root") ?? "");
47
+ const editingProjectId = state.editingProjectId;
20
48
 
21
- await request("/api/projects", {
22
- method: "POST",
49
+ await request(editingProjectId ? `/api/projects/${editingProjectId}` : "/api/projects", {
50
+ method: editingProjectId ? "PUT" : "POST",
23
51
  headers: {
24
52
  "Content-Type": "application/json",
25
53
  },
@@ -27,7 +55,8 @@ projectForm.addEventListener("submit", async (event) => {
27
55
  });
28
56
 
29
57
  projectForm.reset();
30
- setStatus(`已新增项目:${name}`);
58
+ closeConfigModal();
59
+ setStatus(editingProjectId ? `已更新项目:${name}` : `已新增项目:${name}`);
31
60
  await loadProjects();
32
61
  });
33
62
 
@@ -56,8 +85,10 @@ async function loadProjects() {
56
85
  }
57
86
 
58
87
  function renderProjects() {
88
+ projectCount.textContent = `共 ${state.projects.length} 个项目`;
89
+
59
90
  if (state.projects.length === 0) {
60
- projectList.innerHTML = `<div class="empty">暂无项目,请先新增。</div>`;
91
+ projectList.innerHTML = `<div class="empty">暂无项目,请点击“项目配置”进行配置。</div>`;
61
92
  currentProjectTitle.textContent = "请选择项目";
62
93
  openProjectRoot.disabled = true;
63
94
  scriptList.textContent = "选择左侧项目后读取 scripts";
@@ -69,19 +100,69 @@ function renderProjects() {
69
100
  for (const project of state.projects) {
70
101
  const card = document.createElement("article");
71
102
  card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
103
+ card.draggable = true;
104
+ card.dataset.projectId = project.id;
72
105
  card.innerHTML = `
73
106
  <div class="project-card-main">
74
107
  <h3>${escapeHtml(project.name)}</h3>
75
108
  <div class="path">${escapeHtml(project.root)}</div>
76
109
  </div>
77
- <button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
110
+ <div class="project-actions">
111
+ <button class="secondary project-edit" type="button" aria-label="编辑 ${escapeHtml(project.name)}">编辑</button>
112
+ <button class="danger project-delete" type="button" aria-label="删除 ${escapeHtml(project.name)}">删除</button>
113
+ </div>
78
114
  `;
79
115
 
80
- card.addEventListener("click", () => selectProject(project.id));
116
+ card.addEventListener("click", () => {
117
+ if (state.suppressProjectClick) {
118
+ return;
119
+ }
120
+ selectProject(project.id);
121
+ });
122
+ card.addEventListener("dragstart", (event) => {
123
+ state.draggedProjectId = project.id;
124
+ state.suppressProjectClick = true;
125
+ event.dataTransfer.effectAllowed = "move";
126
+ event.dataTransfer.setData("text/plain", project.id);
127
+ card.classList.add("dragging");
128
+ });
129
+ card.addEventListener("dragover", (event) => {
130
+ if (!state.draggedProjectId || state.draggedProjectId === project.id) {
131
+ return;
132
+ }
133
+ event.preventDefault();
134
+ event.dataTransfer.dropEffect = "move";
135
+ card.classList.toggle("drop-after", shouldDropAfter(event, card));
136
+ });
137
+ card.addEventListener("dragleave", () => {
138
+ card.classList.remove("drop-after");
139
+ });
140
+ card.addEventListener("drop", (event) => {
141
+ if (!state.draggedProjectId) {
142
+ return;
143
+ }
144
+
145
+ event.preventDefault();
146
+ card.classList.remove("drop-after");
147
+ moveProject(state.draggedProjectId, project.id, shouldDropAfter(event, card)).catch((error) => {
148
+ setStatus(error.message);
149
+ });
150
+ });
151
+ card.addEventListener("dragend", () => {
152
+ state.draggedProjectId = undefined;
153
+ card.classList.remove("dragging", "drop-after");
154
+ setTimeout(() => {
155
+ state.suppressProjectClick = false;
156
+ }, 0);
157
+ });
81
158
  card.querySelector(".project-delete").addEventListener("click", (event) => {
82
159
  event.stopPropagation();
83
160
  deleteProject(project);
84
161
  });
162
+ card.querySelector(".project-edit").addEventListener("click", (event) => {
163
+ event.stopPropagation();
164
+ openEditProjectModal(project);
165
+ });
85
166
  projectList.append(card);
86
167
  }
87
168
  }
@@ -137,6 +218,75 @@ async function runScript(projectId, script) {
137
218
  setStatus(`已打开 Git Bash 执行:npm run ${script}${data.pid ? `\nPID: ${data.pid}` : ""}`);
138
219
  }
139
220
 
221
+ async function moveProject(draggedProjectId, targetProjectId, insertAfterTarget) {
222
+ if (draggedProjectId === targetProjectId) {
223
+ return;
224
+ }
225
+
226
+ const nextProjects = [...state.projects];
227
+ const draggedIndex = nextProjects.findIndex((project) => project.id === draggedProjectId);
228
+ if (draggedIndex < 0) {
229
+ return;
230
+ }
231
+
232
+ const [draggedProject] = nextProjects.splice(draggedIndex, 1);
233
+ const targetIndex = nextProjects.findIndex((project) => project.id === targetProjectId);
234
+ if (targetIndex < 0) {
235
+ return;
236
+ }
237
+
238
+ nextProjects.splice(targetIndex + (insertAfterTarget ? 1 : 0), 0, draggedProject);
239
+ state.projects = nextProjects;
240
+ renderProjects();
241
+
242
+ try {
243
+ const data = await request("/api/projects/order", {
244
+ method: "PUT",
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ },
248
+ body: JSON.stringify({
249
+ projectIds: state.projects.map((project) => project.id),
250
+ }),
251
+ });
252
+ state.projects = data.projects;
253
+ renderProjects();
254
+ setStatus("项目排序已保存。");
255
+ } catch (error) {
256
+ await loadProjects();
257
+ throw error;
258
+ }
259
+ }
260
+
261
+ function shouldDropAfter(event, element) {
262
+ const rect = element.getBoundingClientRect();
263
+ return event.clientY > rect.top + rect.height / 2;
264
+ }
265
+
266
+ function openCreateProjectModal() {
267
+ state.editingProjectId = undefined;
268
+ projectForm.reset();
269
+ projectConfigTitle.textContent = "项目配置";
270
+ projectSubmit.textContent = "新增项目";
271
+ projectConfigModal.hidden = false;
272
+ projectNameInput.focus();
273
+ }
274
+
275
+ function openEditProjectModal(project) {
276
+ state.editingProjectId = project.id;
277
+ projectConfigTitle.textContent = "编辑项目";
278
+ projectSubmit.textContent = "保存修改";
279
+ projectNameInput.value = project.name;
280
+ projectRootInput.value = project.root;
281
+ projectConfigModal.hidden = false;
282
+ projectNameInput.focus();
283
+ }
284
+
285
+ function closeConfigModal() {
286
+ projectConfigModal.hidden = true;
287
+ state.editingProjectId = undefined;
288
+ }
289
+
140
290
  async function deleteProject(project) {
141
291
  const confirmed = window.confirm(`确定要删除项目“${project.name}”吗?\n\n${project.root}`);
142
292
  if (!confirmed) {
@@ -13,6 +13,7 @@
13
13
 
14
14
  body {
15
15
  margin: 0;
16
+ overflow: hidden;
16
17
  }
17
18
 
18
19
  button,
@@ -21,12 +22,13 @@ input {
21
22
  }
22
23
 
23
24
  button {
25
+ font-size: 14px;
24
26
  border: 0;
25
27
  border-radius: 10px;
26
28
  background: #2563eb;
27
29
  color: #fff;
28
30
  cursor: pointer;
29
- padding: 10px 14px;
31
+ padding: 6px 10px;
30
32
  }
31
33
 
32
34
  button.secondary {
@@ -54,7 +56,7 @@ input {
54
56
  display: grid;
55
57
  grid-template-columns: 380px minmax(0, 1fr);
56
58
  gap: 20px;
57
- min-height: 100vh;
59
+ height: 100vh;
58
60
  padding: 20px;
59
61
  }
60
62
 
@@ -66,6 +68,13 @@ input {
66
68
  padding: 20px;
67
69
  }
68
70
 
71
+ .sidebar,
72
+ .content {
73
+ display: flex;
74
+ min-height: 0;
75
+ flex-direction: column;
76
+ }
77
+
69
78
  .panel-header {
70
79
  display: flex;
71
80
  align-items: flex-start;
@@ -74,6 +83,12 @@ input {
74
83
  margin-bottom: 18px;
75
84
  }
76
85
 
86
+ .header-actions {
87
+ display: flex;
88
+ flex: 0 0 auto;
89
+ gap: 10px;
90
+ }
91
+
77
92
  .eyebrow {
78
93
  margin: 0 0 4px;
79
94
  color: #64748b;
@@ -88,6 +103,12 @@ h2 {
88
103
  margin: 0;
89
104
  }
90
105
 
106
+ .project-count {
107
+ margin: 6px 0 0;
108
+ color: #64748b;
109
+ font-size: 13px;
110
+ }
111
+
91
112
  .form {
92
113
  display: grid;
93
114
  gap: 12px;
@@ -108,6 +129,22 @@ h2 {
108
129
  gap: 10px;
109
130
  }
110
131
 
132
+ .project-list,
133
+ .content-body {
134
+ min-height: 0;
135
+ overflow-y: auto;
136
+ padding-right: 4px;
137
+ }
138
+
139
+ .project-list {
140
+ flex: 1;
141
+ align-content: start;
142
+ }
143
+
144
+ .content-body {
145
+ flex: 1;
146
+ }
147
+
111
148
  .project-card,
112
149
  .script-card {
113
150
  border: 1px solid #e2e8f0;
@@ -117,20 +154,49 @@ h2 {
117
154
  }
118
155
 
119
156
  .project-card {
157
+ position: relative;
120
158
  display: flex;
121
159
  align-items: flex-start;
122
160
  justify-content: space-between;
123
161
  gap: 12px;
162
+ transition:
163
+ border-color 0.15s ease,
164
+ background 0.15s ease,
165
+ opacity 0.15s ease;
124
166
  cursor: pointer;
125
167
  }
126
168
 
169
+ .project-card.dragging {
170
+ opacity: 0.45;
171
+ }
172
+
173
+ .project-card.drop-after::after {
174
+ position: absolute;
175
+ right: 12px;
176
+ bottom: -7px;
177
+ left: 12px;
178
+ height: 3px;
179
+ border-radius: 999px;
180
+ background: #2563eb;
181
+ content: "";
182
+ }
183
+
127
184
  .project-card-main {
128
185
  min-width: 0;
129
186
  }
130
187
 
131
- .project-delete {
188
+ .project-actions {
189
+ display: flex;
132
190
  flex: 0 0 auto;
133
- padding: 8px 10px;
191
+ gap: 6px;
192
+ }
193
+
194
+ .project-edit,
195
+ .project-delete {
196
+ border-radius: 8px;
197
+ padding: 4px 8px;
198
+ font-size: 12px;
199
+ line-height: 1.3;
134
200
  }
135
201
 
136
202
  .project-card.active {
@@ -176,8 +242,47 @@ h2 {
176
242
  white-space: pre-wrap;
177
243
  }
178
244
 
245
+ .modal-backdrop {
246
+ position: fixed;
247
+ inset: 0;
248
+ display: grid;
249
+ place-items: center;
250
+ background: rgb(15 23 42 / 42%);
251
+ padding: 20px;
252
+ }
253
+
254
+ .modal-backdrop[hidden] {
255
+ display: none;
256
+ }
257
+
258
+ .modal {
259
+ width: min(520px, 100%);
260
+ max-height: calc(100vh - 40px);
261
+ overflow-y: auto;
262
+ border: 1px solid #dbe3ef;
263
+ border-radius: 18px;
264
+ background: #fff;
265
+ box-shadow: 0 24px 80px rgb(15 23 42 / 24%);
266
+ padding: 20px;
267
+ }
268
+
269
+ .modal .form {
270
+ margin-bottom: 0;
271
+ }
272
+
179
273
  @media (max-width: 900px) {
274
+ body {
275
+ overflow: auto;
276
+ }
277
+
180
278
  .app {
181
279
  grid-template-columns: 1fr;
280
+ height: auto;
281
+ min-height: 100vh;
282
+ }
283
+
284
+ .sidebar,
285
+ .content {
286
+ max-height: calc(100vh - 40px);
182
287
  }
183
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "batch-run-local",
3
- "version": "1.0.3",
3
+ "version": "1.1.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": {