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 +150 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +35 -16
- package/dist/web/main.js +156 -6
- package/dist/web/style.css +109 -4
- package/package.json +1 -1
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
|
|
234
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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(
|
|
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", "",
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/add.ts","../../src/config/store.ts","../../src/cli/commands/config.ts","../../src/cli/commands/list.ts","../../src/cli/commands/remove.ts","../../src/cli/commands/run.ts","../../src/project/package-json.ts","../../src/runner/git-bash.ts","../../src/runner/path.ts","../../src/server/index.ts","../../src/server/routes.ts","../../src/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
|
package/dist/web/index.html
CHANGED
|
@@ -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
|
-
<
|
|
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
|
|
45
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
<
|
|
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", () =>
|
|
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) {
|
package/dist/web/style.css
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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-
|
|
188
|
+
.project-actions {
|
|
189
|
+
display: flex;
|
|
132
190
|
flex: 0 0 auto;
|
|
133
|
-
|
|
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
|
}
|