batch-run-local 1.0.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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # batch-run
2
+
3
+ 一个本地 npm scripts 管理工具,支持通过命令行配置多个项目,并从 CLI 或本地 Web 页面一键打开 Git Bash 执行项目脚本。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g batch-run
9
+ ```
10
+
11
+ 本地开发:
12
+
13
+ ```bash
14
+ npm install
15
+ npm run build
16
+ npm link
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ ```bash
22
+ br
23
+ ```
24
+
25
+ 常用命令:
26
+
27
+ ```bash
28
+ br add # 新增项目
29
+ br list # 查看项目
30
+ br run # 选择项目和 package.json scripts 后执行
31
+ br ui # 打开本地 Web 管理页面
32
+ br config # 设置 Git Bash 路径和 Web 端口
33
+ br remove # 删除项目
34
+ ```
35
+
36
+ ## 配置
37
+
38
+ 项目配置存储在用户电脑的应用配置目录中,不写入被管理项目。
39
+
40
+ 每个项目配置包含:
41
+
42
+ - 项目名称
43
+ - 项目根路径
44
+
45
+ 工具会在执行时实时读取项目根路径下的 `package.json`,并展示其中的 `scripts`。
46
+
47
+ ## Web 页面
48
+
49
+ 运行:
50
+
51
+ ```bash
52
+ br ui
53
+ ```
54
+
55
+ 工具会启动 `127.0.0.1` 上的本地服务并打开浏览器。页面支持:
56
+
57
+ - 查看已配置项目
58
+ - 新增项目
59
+ - 读取项目 `package.json` 中的 `scripts`
60
+ - 点击按钮后打开 Git Bash 窗口执行对应脚本
61
+
62
+ ## Windows + Git Bash
63
+
64
+ 当前版本优先支持 Windows + Git Bash。默认会自动检测:
65
+
66
+ ```text
67
+ C:\Program Files\Git\git-bash.exe
68
+ C:\Program Files (x86)\Git\git-bash.exe
69
+ ```
70
+
71
+ 如果自动检测失败,可以运行:
72
+
73
+ ```bash
74
+ br config
75
+ ```
76
+
77
+ 手动填写 `git-bash.exe` 路径。
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { cac } from "cac";
5
+ import prompts5 from "prompts";
6
+
7
+ // src/cli/commands/add.ts
8
+ import prompts from "prompts";
9
+
10
+ // src/config/store.ts
11
+ import Conf from "conf";
12
+ import { randomUUID } from "crypto";
13
+ import { access } from "fs/promises";
14
+ import { resolve } from "path";
15
+ var defaultSettings = {
16
+ shell: "git-bash",
17
+ port: 1234
18
+ };
19
+ var store = new Conf({
20
+ projectName: "batch-run",
21
+ defaults: {
22
+ projects: [],
23
+ settings: defaultSettings
24
+ }
25
+ });
26
+ function getConfig() {
27
+ return {
28
+ projects: store.get("projects") ?? [],
29
+ settings: {
30
+ ...defaultSettings,
31
+ ...store.get("settings") ?? {}
32
+ }
33
+ };
34
+ }
35
+ function getProjects() {
36
+ return getConfig().projects;
37
+ }
38
+ function getProject(projectId) {
39
+ return getProjects().find((project) => project.id === projectId);
40
+ }
41
+ async function addProject(input) {
42
+ const root = resolve(input.root);
43
+ await access(root);
44
+ const now = Date.now();
45
+ const project = {
46
+ id: randomUUID(),
47
+ name: input.name.trim(),
48
+ root,
49
+ createdAt: now,
50
+ updatedAt: now
51
+ };
52
+ const projects = getProjects();
53
+ if (projects.some((item) => item.root.toLowerCase() === root.toLowerCase())) {
54
+ throw new Error("\u8BE5\u9879\u76EE\u8DEF\u5F84\u5DF2\u7ECF\u5B58\u5728");
55
+ }
56
+ store.set("projects", [...projects, project]);
57
+ return project;
58
+ }
59
+ function removeProject(projectId) {
60
+ const projects = getProjects();
61
+ const nextProjects = projects.filter((project) => project.id !== projectId);
62
+ store.set("projects", nextProjects);
63
+ return nextProjects.length !== projects.length;
64
+ }
65
+ function getSettings() {
66
+ return getConfig().settings;
67
+ }
68
+ function updateSettings(settings) {
69
+ const nextSettings = {
70
+ ...getSettings(),
71
+ ...settings
72
+ };
73
+ store.set("settings", nextSettings);
74
+ return nextSettings;
75
+ }
76
+ function getConfigPath() {
77
+ return store.path;
78
+ }
79
+
80
+ // src/cli/commands/add.ts
81
+ async function addCommand() {
82
+ const answers = await prompts([
83
+ {
84
+ type: "text",
85
+ name: "name",
86
+ message: "\u9879\u76EE\u540D\u79F0",
87
+ validate: (value) => value.trim().length > 0 || "\u8BF7\u8F93\u5165\u9879\u76EE\u540D\u79F0"
88
+ },
89
+ {
90
+ type: "text",
91
+ name: "root",
92
+ message: "\u9879\u76EE\u6839\u8DEF\u5F84",
93
+ validate: (value) => value.trim().length > 0 || "\u8BF7\u8F93\u5165\u9879\u76EE\u6839\u8DEF\u5F84"
94
+ }
95
+ ]);
96
+ if (!answers.name || !answers.root) {
97
+ return;
98
+ }
99
+ const project = await addProject({
100
+ name: answers.name,
101
+ root: answers.root
102
+ });
103
+ console.log(`\u5DF2\u6DFB\u52A0\u9879\u76EE: ${project.name}`);
104
+ console.log(project.root);
105
+ }
106
+
107
+ // src/cli/commands/config.ts
108
+ import prompts2 from "prompts";
109
+ async function configCommand() {
110
+ const settings = getSettings();
111
+ console.log(`\u5F53\u524D\u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}`);
112
+ console.log(`\u5F53\u524D Git Bash \u8DEF\u5F84: ${settings.gitBashPath ?? "\u81EA\u52A8\u68C0\u6D4B"}`);
113
+ console.log(`\u5F53\u524D Web \u7AEF\u53E3: ${settings.port}`);
114
+ const answers = await prompts2([
115
+ {
116
+ type: "text",
117
+ name: "gitBashPath",
118
+ message: "Git Bash \u8DEF\u5F84\uFF0C\u7559\u7A7A\u8868\u793A\u81EA\u52A8\u68C0\u6D4B",
119
+ initial: settings.gitBashPath ?? ""
120
+ },
121
+ {
122
+ type: "number",
123
+ name: "port",
124
+ message: "Web \u7AEF\u53E3",
125
+ initial: settings.port,
126
+ min: 1,
127
+ max: 65535
128
+ }
129
+ ]);
130
+ updateSettings({
131
+ gitBashPath: answers.gitBashPath?.trim() || void 0,
132
+ port: answers.port || settings.port
133
+ });
134
+ console.log("\u914D\u7F6E\u5DF2\u66F4\u65B0\u3002");
135
+ }
136
+
137
+ // src/cli/commands/list.ts
138
+ function listCommand() {
139
+ const projects = getProjects();
140
+ if (projects.length === 0) {
141
+ console.log("\u6682\u65E0\u9879\u76EE\u914D\u7F6E\uFF0C\u53EF\u8FD0\u884C br add \u65B0\u589E\u3002");
142
+ console.log(`\u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}`);
143
+ return;
144
+ }
145
+ for (const project of projects) {
146
+ console.log(`${project.name}`);
147
+ console.log(` id: ${project.id}`);
148
+ console.log(` root: ${project.root}`);
149
+ }
150
+ console.log(`
151
+ \u914D\u7F6E\u6587\u4EF6: ${getConfigPath()}`);
152
+ }
153
+
154
+ // src/cli/commands/remove.ts
155
+ import prompts3 from "prompts";
156
+ async function removeCommand() {
157
+ const projects = getProjects();
158
+ if (projects.length === 0) {
159
+ console.log("\u6682\u65E0\u9879\u76EE\u53EF\u5220\u9664\u3002");
160
+ return;
161
+ }
162
+ const answer = await prompts3({
163
+ type: "select",
164
+ name: "projectId",
165
+ message: "\u9009\u62E9\u8981\u5220\u9664\u7684\u9879\u76EE",
166
+ choices: projects.map((project) => ({
167
+ title: project.name,
168
+ description: project.root,
169
+ value: project.id
170
+ }))
171
+ });
172
+ if (!answer.projectId) {
173
+ return;
174
+ }
175
+ const removed = removeProject(answer.projectId);
176
+ console.log(removed ? "\u5DF2\u5220\u9664\u9879\u76EE\u3002" : "\u672A\u627E\u5230\u9879\u76EE\u3002");
177
+ }
178
+
179
+ // src/cli/commands/run.ts
180
+ import prompts4 from "prompts";
181
+
182
+ // src/project/package-json.ts
183
+ import { readFile } from "fs/promises";
184
+ import { join } from "path";
185
+ async function readPackageJson(projectRoot) {
186
+ const packageJsonPath = join(projectRoot, "package.json");
187
+ const content = await readFile(packageJsonPath, "utf8");
188
+ try {
189
+ return JSON.parse(content);
190
+ } catch {
191
+ throw new Error(`package.json \u89E3\u6790\u5931\u8D25: ${packageJsonPath}`);
192
+ }
193
+ }
194
+ async function readPackageScripts(projectRoot) {
195
+ const packageJson = await readPackageJson(projectRoot);
196
+ return packageJson.scripts ?? {};
197
+ }
198
+
199
+ // src/runner/git-bash.ts
200
+ import { spawn } from "child_process";
201
+
202
+ // src/runner/path.ts
203
+ import { existsSync } from "fs";
204
+ import { platform } from "os";
205
+ import { dirname, join as join2 } from "path";
206
+ var defaultWindowsGitBashPaths = [
207
+ "C:\\Program Files\\Git\\git-bash.exe",
208
+ "C:\\Program Files (x86)\\Git\\git-bash.exe"
209
+ ];
210
+ function findGitBashPath(configuredPath) {
211
+ if (configuredPath && existsSync(configuredPath)) {
212
+ return configuredPath;
213
+ }
214
+ if (platform() !== "win32") {
215
+ return void 0;
216
+ }
217
+ return defaultWindowsGitBashPaths.find((item) => existsSync(item));
218
+ }
219
+ function toGitBashPath(inputPath) {
220
+ const normalized = inputPath.replaceAll("\\", "/");
221
+ const driveMatch = /^([a-zA-Z]):\/(.*)$/.exec(normalized);
222
+ if (!driveMatch) {
223
+ return normalized;
224
+ }
225
+ const [, drive, rest] = driveMatch;
226
+ return `/${drive.toLowerCase()}/${rest}`;
227
+ }
228
+ function shellSingleQuote(value) {
229
+ return `'${value.replaceAll("'", "'\\''")}'`;
230
+ }
231
+ function findMinttyPath(gitBashPath) {
232
+ const minttyPath = join2(dirname(gitBashPath), "usr", "bin", "mintty.exe");
233
+ return existsSync(minttyPath) ? minttyPath : void 0;
234
+ }
235
+
236
+ // src/runner/git-bash.ts
237
+ function runScriptInGitBash(project, scriptName, settings) {
238
+ const gitBashPath = findGitBashPath(settings.gitBashPath);
239
+ if (!gitBashPath) {
240
+ throw new Error("\u672A\u627E\u5230 Git Bash\uFF0C\u8BF7\u5148\u901A\u8FC7 br config \u8BBE\u7F6E git-bash.exe \u8DEF\u5F84");
241
+ }
242
+ const command = [
243
+ `echo Running ${shellSingleQuote(project.name)}: npm run ${shellSingleQuote(scriptName)}`,
244
+ `npm run ${shellSingleQuote(scriptName)}`,
245
+ "echo",
246
+ "echo Process exited. Press Ctrl+D or close this window.",
247
+ "exec bash"
248
+ ].join("; ");
249
+ const minttyPath = findMinttyPath(gitBashPath);
250
+ const child = minttyPath ? spawn(minttyPath, ["--dir", toGitBashPath(project.root), "/usr/bin/bash", "-lc", command], {
251
+ detached: true,
252
+ stdio: "ignore",
253
+ windowsHide: false
254
+ }) : spawn("cmd.exe", ["/d", "/c", "start", "", gitBashPath, `--cd=${toGitBashPath(project.root)}`, "-c", command], {
255
+ detached: true,
256
+ stdio: "ignore",
257
+ windowsHide: false
258
+ });
259
+ child.unref();
260
+ return {
261
+ pid: child.pid,
262
+ command
263
+ };
264
+ }
265
+
266
+ // src/cli/commands/run.ts
267
+ async function runCommand() {
268
+ const projects = getProjects();
269
+ if (projects.length === 0) {
270
+ console.log("\u6682\u65E0\u9879\u76EE\u914D\u7F6E\uFF0C\u53EF\u8FD0\u884C br add \u65B0\u589E\u3002");
271
+ return;
272
+ }
273
+ const projectAnswer = await prompts4({
274
+ type: "select",
275
+ name: "projectId",
276
+ message: "\u9009\u62E9\u9879\u76EE",
277
+ choices: projects.map((project2) => ({
278
+ title: project2.name,
279
+ description: project2.root,
280
+ value: project2.id
281
+ }))
282
+ });
283
+ const project = projects.find((item) => item.id === projectAnswer.projectId);
284
+ if (!project) {
285
+ return;
286
+ }
287
+ const scripts = await readPackageScripts(project.root);
288
+ const scriptEntries = Object.entries(scripts);
289
+ if (scriptEntries.length === 0) {
290
+ console.log("\u8BE5\u9879\u76EE package.json \u4E2D\u6CA1\u6709 scripts\u3002");
291
+ return;
292
+ }
293
+ const scriptAnswer = await prompts4({
294
+ type: "select",
295
+ name: "script",
296
+ message: "\u9009\u62E9\u8981\u6267\u884C\u7684\u811A\u672C",
297
+ choices: scriptEntries.map(([name, command]) => ({
298
+ title: name,
299
+ description: command,
300
+ value: name
301
+ }))
302
+ });
303
+ if (!scriptAnswer.script) {
304
+ return;
305
+ }
306
+ const result = runScriptInGitBash(project, scriptAnswer.script, getSettings());
307
+ console.log(`\u5DF2\u5728 Git Bash \u7A97\u53E3\u6267\u884C: npm run ${scriptAnswer.script}`);
308
+ if (result.pid) {
309
+ console.log(`\u8FDB\u7A0B PID: ${result.pid}`);
310
+ }
311
+ }
312
+
313
+ // src/server/index.ts
314
+ import fastify from "fastify";
315
+ import { readFile as readFile2 } from "fs/promises";
316
+ import { dirname as dirname2, join as join3 } from "path";
317
+ import { fileURLToPath } from "url";
318
+ import open2 from "open";
319
+
320
+ // src/server/routes.ts
321
+ import open from "open";
322
+ async function registerApiRoutes(app, settings) {
323
+ app.get("/api/projects", async () => ({
324
+ projects: getProjects()
325
+ }));
326
+ app.post("/api/projects", async (request, reply) => {
327
+ const name = request.body.name?.trim();
328
+ const root = request.body.root?.trim();
329
+ if (!name || !root) {
330
+ return reply.code(400).send({ message: "\u9879\u76EE\u540D\u79F0\u548C\u6839\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A" });
331
+ }
332
+ const project = await addProject({ name, root });
333
+ return { project };
334
+ });
335
+ app.delete("/api/projects/:id", async (request, reply) => {
336
+ const removed = removeProject(request.params.id);
337
+ if (!removed) {
338
+ return reply.code(404).send({ message: "\u9879\u76EE\u4E0D\u5B58\u5728" });
339
+ }
340
+ return { ok: true };
341
+ });
342
+ app.get("/api/projects/:id/scripts", async (request, reply) => {
343
+ const project = getProject(request.params.id);
344
+ if (!project) {
345
+ return reply.code(404).send({ message: "\u9879\u76EE\u4E0D\u5B58\u5728" });
346
+ }
347
+ const scripts = await readPackageScripts(project.root);
348
+ return { scripts };
349
+ });
350
+ app.post("/api/projects/:id/open", async (request, reply) => {
351
+ const project = getProject(request.params.id);
352
+ if (!project) {
353
+ return reply.code(404).send({ message: "\u9879\u76EE\u4E0D\u5B58\u5728" });
354
+ }
355
+ await open(project.root);
356
+ return { ok: true };
357
+ });
358
+ app.post("/api/projects/:id/run", async (request, reply) => {
359
+ const project = getProject(request.params.id);
360
+ const script = request.body.script?.trim();
361
+ if (!project) {
362
+ return reply.code(404).send({ message: "\u9879\u76EE\u4E0D\u5B58\u5728" });
363
+ }
364
+ if (!script) {
365
+ return reply.code(400).send({ message: "\u811A\u672C\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A" });
366
+ }
367
+ const scripts = await readPackageScripts(project.root);
368
+ if (!scripts[script]) {
369
+ return reply.code(404).send({ message: "\u811A\u672C\u4E0D\u5B58\u5728" });
370
+ }
371
+ const result = runScriptInGitBash(project, script, settings);
372
+ return {
373
+ ok: true,
374
+ pid: result.pid
375
+ };
376
+ });
377
+ }
378
+
379
+ // src/server/index.ts
380
+ function getWebRoot() {
381
+ return join3(dirname2(dirname2(fileURLToPath(import.meta.url))), "web");
382
+ }
383
+ async function startServer(options = {}) {
384
+ const settings = getSettings();
385
+ const app = fastify({
386
+ logger: false
387
+ });
388
+ const webRoot = getWebRoot();
389
+ await registerApiRoutes(app, settings);
390
+ app.get("/", async (_request, reply) => {
391
+ return reply.type("text/html").send(await readFile2(join3(webRoot, "index.html"), "utf8"));
392
+ });
393
+ app.get("/main.js", async (_request, reply) => {
394
+ return reply.type("application/javascript").send(await readFile2(join3(webRoot, "main.js"), "utf8"));
395
+ });
396
+ app.get("/style.css", async (_request, reply) => {
397
+ return reply.type("text/css").send(await readFile2(join3(webRoot, "style.css"), "utf8"));
398
+ });
399
+ const url = await app.listen({
400
+ host: "127.0.0.1",
401
+ port: settings.port
402
+ });
403
+ console.log(`Batch Run Web \u5DF2\u542F\u52A8: ${url}`);
404
+ if (options.openBrowser ?? true) {
405
+ await open2(url);
406
+ }
407
+ }
408
+
409
+ // src/cli/commands/ui.ts
410
+ async function uiCommand() {
411
+ await startServer({ openBrowser: true });
412
+ }
413
+
414
+ // src/cli/index.ts
415
+ var cli = cac("br");
416
+ cli.command("add", "\u65B0\u589E\u9879\u76EE\u914D\u7F6E").action(wrap(addCommand));
417
+ cli.command("list", "\u67E5\u770B\u9879\u76EE\u914D\u7F6E").action(wrap(async () => listCommand()));
418
+ cli.command("run", "\u9009\u62E9\u5E76\u6267\u884C\u9879\u76EE\u811A\u672C").action(wrap(runCommand));
419
+ cli.command("remove", "\u5220\u9664\u9879\u76EE\u914D\u7F6E").action(wrap(removeCommand));
420
+ cli.command("ui", "\u6253\u5F00 Web \u7BA1\u7406\u9875\u9762").action(wrap(uiCommand));
421
+ cli.command("config", "\u914D\u7F6E Git Bash \u8DEF\u5F84\u548C Web \u7AEF\u53E3").action(wrap(configCommand));
422
+ cli.help();
423
+ cli.version("0.1.0");
424
+ async function mainMenu() {
425
+ const answer = await prompts5({
426
+ type: "select",
427
+ name: "action",
428
+ message: "\u8BF7\u9009\u62E9\u64CD\u4F5C",
429
+ choices: [
430
+ { title: "\u6253\u5F00\u53EF\u89C6\u5316\u64CD\u4F5C\u9875\u9762", value: "ui" },
431
+ { title: "\u542F\u52A8\u9879\u76EE", value: "run" },
432
+ { title: "\u67E5\u770B\u9879\u76EE", value: "list" },
433
+ { title: "\u65B0\u589E\u9879\u76EE", value: "add" },
434
+ { title: "\u5220\u9664\u9879\u76EE", value: "remove" },
435
+ { title: "\u8BBE\u7F6E Git Bash \u8DEF\u5F84", value: "config" }
436
+ ]
437
+ });
438
+ switch (answer.action) {
439
+ case "run":
440
+ await runCommand();
441
+ break;
442
+ case "add":
443
+ await addCommand();
444
+ break;
445
+ case "list":
446
+ listCommand();
447
+ break;
448
+ case "remove":
449
+ await removeCommand();
450
+ break;
451
+ case "ui":
452
+ await uiCommand();
453
+ break;
454
+ case "config":
455
+ await configCommand();
456
+ break;
457
+ }
458
+ }
459
+ function wrap(fn) {
460
+ return async () => {
461
+ try {
462
+ await fn();
463
+ } catch (error) {
464
+ handleError(error);
465
+ }
466
+ };
467
+ }
468
+ function handleError(error) {
469
+ const message = error instanceof Error ? error.message : String(error);
470
+ console.error(`\u6267\u884C\u5931\u8D25: ${message}`);
471
+ process.exitCode = 1;
472
+ }
473
+ if (process.argv.length <= 2) {
474
+ await wrap(mainMenu)();
475
+ } else {
476
+ cli.parse();
477
+ }
478
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 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 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('0.1.0')\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\",\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","join","prompts","project","readFile","dirname","join","open","join","dirname","readFile","open","prompts"]}
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Batch Run</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ </head>
9
+ <body>
10
+ <main class="app">
11
+ <section class="panel sidebar">
12
+ <div class="panel-header">
13
+ <div>
14
+ <p class="eyebrow">Batch Run</p>
15
+ <h1>项目配置</h1>
16
+ </div>
17
+ <button id="refreshProjects" type="button">刷新</button>
18
+ </div>
19
+
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
+ <div id="projectList" class="project-list"></div>
33
+ </section>
34
+
35
+ <section class="panel content">
36
+ <div class="panel-header">
37
+ <div>
38
+ <p class="eyebrow">package.json</p>
39
+ <h2 id="currentProjectTitle">请选择项目</h2>
40
+ </div>
41
+ <button id="openProjectRoot" class="secondary" type="button" disabled>打开项目目录</button>
42
+ </div>
43
+
44
+ <div id="scriptList" class="script-list empty">选择左侧项目后读取 scripts</div>
45
+ <pre id="status" class="status"></pre>
46
+ </section>
47
+ </main>
48
+
49
+ <script type="module" src="/main.js"></script>
50
+ </body>
51
+ </html>
@@ -0,0 +1,160 @@
1
+ const state = {
2
+ projects: [],
3
+ currentProjectId: undefined,
4
+ };
5
+
6
+ const projectList = document.querySelector("#projectList");
7
+ const scriptList = document.querySelector("#scriptList");
8
+ const statusBox = document.querySelector("#status");
9
+ const currentProjectTitle = document.querySelector("#currentProjectTitle");
10
+ const projectForm = document.querySelector("#projectForm");
11
+ const refreshProjects = document.querySelector("#refreshProjects");
12
+ const openProjectRoot = document.querySelector("#openProjectRoot");
13
+
14
+ projectForm.addEventListener("submit", async (event) => {
15
+ event.preventDefault();
16
+
17
+ const formData = new FormData(projectForm);
18
+ const name = String(formData.get("name") ?? "");
19
+ const root = String(formData.get("root") ?? "");
20
+
21
+ await request("/api/projects", {
22
+ method: "POST",
23
+ headers: {
24
+ "Content-Type": "application/json",
25
+ },
26
+ body: JSON.stringify({ name, root }),
27
+ });
28
+
29
+ projectForm.reset();
30
+ setStatus(`已新增项目:${name}`);
31
+ await loadProjects();
32
+ });
33
+
34
+ refreshProjects.addEventListener("click", loadProjects);
35
+
36
+ openProjectRoot.addEventListener("click", async () => {
37
+ if (!state.currentProjectId) {
38
+ return;
39
+ }
40
+
41
+ await request(`/api/projects/${state.currentProjectId}/open`, {
42
+ method: "POST",
43
+ });
44
+ setStatus("已打开项目目录。");
45
+ });
46
+
47
+ async function loadProjects() {
48
+ const data = await request("/api/projects");
49
+ state.projects = data.projects;
50
+ if (!state.projects.some((project) => project.id === state.currentProjectId)) {
51
+ state.currentProjectId = undefined;
52
+ currentProjectTitle.textContent = "请选择项目";
53
+ openProjectRoot.disabled = true;
54
+ }
55
+ renderProjects();
56
+ }
57
+
58
+ function renderProjects() {
59
+ if (state.projects.length === 0) {
60
+ projectList.innerHTML = `<div class="empty">暂无项目,请先新增。</div>`;
61
+ currentProjectTitle.textContent = "请选择项目";
62
+ openProjectRoot.disabled = true;
63
+ scriptList.textContent = "选择左侧项目后读取 scripts";
64
+ return;
65
+ }
66
+
67
+ projectList.innerHTML = "";
68
+
69
+ for (const project of state.projects) {
70
+ const card = document.createElement("article");
71
+ card.className = `project-card ${project.id === state.currentProjectId ? "active" : ""}`;
72
+ card.innerHTML = `
73
+ <h3>${escapeHtml(project.name)}</h3>
74
+ <div class="path">${escapeHtml(project.root)}</div>
75
+ `;
76
+
77
+ card.addEventListener("click", () => selectProject(project.id));
78
+ projectList.append(card);
79
+ }
80
+ }
81
+
82
+ async function selectProject(projectId) {
83
+ state.currentProjectId = projectId;
84
+ renderProjects();
85
+
86
+ const project = state.projects.find((item) => item.id === projectId);
87
+ currentProjectTitle.textContent = project?.name ?? "请选择项目";
88
+ openProjectRoot.disabled = !project;
89
+ scriptList.textContent = "正在读取 scripts...";
90
+
91
+ const data = await request(`/api/projects/${projectId}/scripts`);
92
+ renderScripts(projectId, data.scripts);
93
+ }
94
+
95
+ function renderScripts(projectId, scripts) {
96
+ const entries = Object.entries(scripts);
97
+
98
+ if (entries.length === 0) {
99
+ scriptList.innerHTML = `<div class="empty">该项目 package.json 中没有 scripts。</div>`;
100
+ return;
101
+ }
102
+
103
+ scriptList.innerHTML = "";
104
+
105
+ for (const [name, command] of entries) {
106
+ const card = document.createElement("article");
107
+ card.className = "script-card";
108
+ card.innerHTML = `
109
+ <div>
110
+ <h3>${escapeHtml(name)}</h3>
111
+ <div class="command">${escapeHtml(command)}</div>
112
+ </div>
113
+ <button type="button">执行</button>
114
+ `;
115
+
116
+ card.querySelector("button").addEventListener("click", () => runScript(projectId, name));
117
+ scriptList.append(card);
118
+ }
119
+ }
120
+
121
+ async function runScript(projectId, script) {
122
+ const data = await request(`/api/projects/${projectId}/run`, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ },
127
+ body: JSON.stringify({ script }),
128
+ });
129
+
130
+ setStatus(`已打开 Git Bash 执行:npm run ${script}${data.pid ? `\nPID: ${data.pid}` : ""}`);
131
+ }
132
+
133
+ async function request(url, options) {
134
+ const response = await fetch(url, options);
135
+ const contentType = response.headers.get("content-type") ?? "";
136
+ const data = contentType.includes("application/json") ? await response.json() : {};
137
+
138
+ if (!response.ok) {
139
+ const message = data.message ?? `请求失败:${response.status}`;
140
+ setStatus(message);
141
+ throw new Error(message);
142
+ }
143
+
144
+ return data;
145
+ }
146
+
147
+ function setStatus(message) {
148
+ statusBox.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
149
+ }
150
+
151
+ function escapeHtml(value) {
152
+ return String(value)
153
+ .replaceAll("&", "&amp;")
154
+ .replaceAll("<", "&lt;")
155
+ .replaceAll(">", "&gt;")
156
+ .replaceAll('"', "&quot;")
157
+ .replaceAll("'", "&#039;");
158
+ }
159
+
160
+ loadProjects().catch((error) => setStatus(error.message));
@@ -0,0 +1,170 @@
1
+ :root {
2
+ color-scheme: light;
3
+ font-family:
4
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
5
+ sans-serif;
6
+ color: #172033;
7
+ background: #eef2f7;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ margin: 0;
16
+ }
17
+
18
+ button,
19
+ input {
20
+ font: inherit;
21
+ }
22
+
23
+ button {
24
+ border: 0;
25
+ border-radius: 10px;
26
+ background: #2563eb;
27
+ color: #fff;
28
+ cursor: pointer;
29
+ padding: 10px 14px;
30
+ }
31
+
32
+ button.secondary {
33
+ background: #e2e8f0;
34
+ color: #172033;
35
+ }
36
+
37
+ button.danger {
38
+ background: #dc2626;
39
+ }
40
+
41
+ button:disabled {
42
+ cursor: not-allowed;
43
+ opacity: 0.6;
44
+ }
45
+
46
+ input {
47
+ width: 100%;
48
+ border: 1px solid #cbd5e1;
49
+ border-radius: 10px;
50
+ padding: 10px 12px;
51
+ }
52
+
53
+ .app {
54
+ display: grid;
55
+ grid-template-columns: 380px minmax(0, 1fr);
56
+ gap: 20px;
57
+ min-height: 100vh;
58
+ padding: 20px;
59
+ }
60
+
61
+ .panel {
62
+ border: 1px solid #dbe3ef;
63
+ border-radius: 18px;
64
+ background: #fff;
65
+ box-shadow: 0 10px 30px rgb(15 23 42 / 8%);
66
+ padding: 20px;
67
+ }
68
+
69
+ .panel-header {
70
+ display: flex;
71
+ align-items: flex-start;
72
+ justify-content: space-between;
73
+ gap: 16px;
74
+ margin-bottom: 18px;
75
+ }
76
+
77
+ .eyebrow {
78
+ margin: 0 0 4px;
79
+ color: #64748b;
80
+ font-size: 12px;
81
+ font-weight: 700;
82
+ letter-spacing: 0.08em;
83
+ text-transform: uppercase;
84
+ }
85
+
86
+ h1,
87
+ h2 {
88
+ margin: 0;
89
+ }
90
+
91
+ .form {
92
+ display: grid;
93
+ gap: 12px;
94
+ margin-bottom: 20px;
95
+ }
96
+
97
+ .form label {
98
+ display: grid;
99
+ gap: 6px;
100
+ color: #334155;
101
+ font-size: 14px;
102
+ font-weight: 600;
103
+ }
104
+
105
+ .project-list,
106
+ .script-list {
107
+ display: grid;
108
+ gap: 10px;
109
+ }
110
+
111
+ .project-card,
112
+ .script-card {
113
+ border: 1px solid #e2e8f0;
114
+ border-radius: 14px;
115
+ background: #f8fafc;
116
+ padding: 14px;
117
+ }
118
+
119
+ .project-card {
120
+ cursor: pointer;
121
+ }
122
+
123
+ .project-card.active {
124
+ border-color: #2563eb;
125
+ background: #eff6ff;
126
+ }
127
+
128
+ .project-card h3,
129
+ .script-card h3 {
130
+ margin: 0 0 6px;
131
+ }
132
+
133
+ .path,
134
+ .command {
135
+ overflow-wrap: anywhere;
136
+ color: #64748b;
137
+ font-family: "Cascadia Code", Consolas, monospace;
138
+ font-size: 13px;
139
+ }
140
+
141
+ .script-card {
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: space-between;
145
+ gap: 16px;
146
+ }
147
+
148
+ .script-card > div {
149
+ min-width: 0;
150
+ }
151
+
152
+ .empty {
153
+ color: #64748b;
154
+ }
155
+
156
+ .status {
157
+ min-height: 80px;
158
+ margin: 20px 0 0;
159
+ border-radius: 14px;
160
+ background: #0f172a;
161
+ color: #dbeafe;
162
+ padding: 14px;
163
+ white-space: pre-wrap;
164
+ }
165
+
166
+ @media (max-width: 900px) {
167
+ .app {
168
+ grid-template-columns: 1fr;
169
+ }
170
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "batch-run-local",
3
+ "version": "1.0.0",
4
+ "description": "Manage and run package.json scripts across local projects from CLI and a local web UI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "br": "dist/cli/index.js",
8
+ "batch-run": "dist/cli/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "dev": "tsx src/cli/index.ts",
16
+ "build": "tsc --noEmit && tsup && node scripts/copy-web.mjs",
17
+ "start": "node dist/cli/index.js"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "scripts",
22
+ "package-json",
23
+ "git-bash"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "dependencies": {
31
+ "cac": "^6.7.14",
32
+ "conf": "^12.0.0",
33
+ "fastify": "^4.29.1",
34
+ "open": "^10.2.0",
35
+ "prompts": "^2.4.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "latest",
39
+ "@types/prompts": "latest",
40
+ "tsup": "latest",
41
+ "tsx": "latest",
42
+ "typescript": "latest"
43
+ }
44
+ }