@ronkovic/aad 0.3.5 → 0.3.7
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/package.json +4 -3
- package/src/__tests__/e2e/pipeline-e2e.test.ts +6 -0
- package/src/main.ts +10 -2
- package/src/modules/cli/__tests__/interactive/auth-step.test.ts +24 -0
- package/src/modules/cli/__tests__/interactive/interactive.test.ts +14 -0
- package/src/modules/cli/__tests__/interactive/process-supervisor.test.ts +45 -0
- package/src/modules/cli/__tests__/interactive/prompts.test.ts +75 -0
- package/src/modules/cli/__tests__/update.test.ts +20 -0
- package/src/modules/cli/commands/interactive/auth-step.ts +61 -0
- package/src/modules/cli/commands/interactive/launch-step.ts +76 -0
- package/src/modules/cli/commands/interactive/process-supervisor.ts +105 -0
- package/src/modules/cli/commands/interactive/project-step.ts +157 -0
- package/src/modules/cli/commands/interactive/prompts.ts +75 -0
- package/src/modules/cli/commands/interactive.ts +38 -0
- package/src/modules/cli/commands/resume.ts +16 -0
- package/src/modules/cli/commands/run.ts +28 -7
- package/src/modules/cli/commands/task-dispatch-handler.ts +11 -3
- package/src/modules/cli/commands/update.ts +119 -0
- package/src/modules/cli/index.ts +1 -0
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +9 -5
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +19 -4
- package/src/modules/git-workspace/branch-manager.ts +10 -3
- package/src/modules/git-workspace/worktree-manager.ts +43 -27
- package/src/modules/planning/__tests__/feature-name-generator.test.ts +124 -0
- package/src/modules/planning/feature-name-generator.ts +103 -0
- package/src/modules/task-queue/dispatcher.ts +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ronkovic/aad",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Autonomous Agent Development Orchestrator - Multi-agent TDD pipeline powered by Claude",
|
|
5
5
|
"module": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@anthropic-ai/claude-agent-sdk": "^0.2.34",
|
|
41
|
+
"@inquirer/prompts": "^8.2.0",
|
|
42
|
+
"chokidar": "^4.0",
|
|
43
|
+
"commander": "^13.0",
|
|
41
44
|
"hono": "^4.6",
|
|
42
45
|
"pino": "^9.6",
|
|
43
46
|
"pino-pretty": "^13.0",
|
|
44
|
-
"chokidar": "^4.0",
|
|
45
|
-
"commander": "^13.0",
|
|
46
47
|
"zod": "^3.24"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
@@ -270,6 +270,12 @@ describe("E2E Pipeline", () => {
|
|
|
270
270
|
result: { taskId: createTaskId("task-1"), status: "completed", duration: 50 },
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
// Emit worker:idle to trigger dispatch (mirrors real task-dispatch-handler behavior)
|
|
274
|
+
eventBus.emit({
|
|
275
|
+
type: "worker:idle",
|
|
276
|
+
workerId: createWorkerId("w-1"),
|
|
277
|
+
});
|
|
278
|
+
|
|
273
279
|
await waitFor(() => dispatchedTasks.length >= 2, 3000);
|
|
274
280
|
|
|
275
281
|
expect(dispatchedTasks[1]).toBe(createTaskId("task-2"));
|
package/src/main.ts
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
createResumeCommand,
|
|
12
12
|
createStatusCommand,
|
|
13
13
|
createCleanupCommand,
|
|
14
|
+
createUpdateCommand,
|
|
14
15
|
type App,
|
|
15
16
|
type AppOptions,
|
|
16
17
|
} from "./modules/cli";
|
|
18
|
+
import { registerInteractiveCommand } from "./modules/cli/commands/interactive.js";
|
|
17
19
|
import packageJson from "../package.json" with { type: "json" };
|
|
18
20
|
|
|
19
21
|
const program = new Command()
|
|
@@ -47,6 +49,12 @@ program.addCommand(createRunCommand(getApp));
|
|
|
47
49
|
program.addCommand(createResumeCommand(getApp));
|
|
48
50
|
program.addCommand(createStatusCommand(getApp));
|
|
49
51
|
program.addCommand(createCleanupCommand(getApp));
|
|
52
|
+
program.addCommand(createUpdateCommand());
|
|
53
|
+
registerInteractiveCommand(program);
|
|
50
54
|
|
|
51
|
-
//
|
|
52
|
-
|
|
55
|
+
// Default to interactive mode when no command is given
|
|
56
|
+
if (process.argv.length <= 2) {
|
|
57
|
+
program.parse(["node", "aad", "interactive"]);
|
|
58
|
+
} else {
|
|
59
|
+
program.parse();
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { runAuthStep } from "../../commands/interactive/auth-step";
|
|
3
|
+
|
|
4
|
+
// Mock @inquirer/prompts by testing the env detection logic directly
|
|
5
|
+
describe("Auth Step", () => {
|
|
6
|
+
let origApiKey: string | undefined;
|
|
7
|
+
let origOauth: string | undefined;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
origApiKey = process.env.ANTHROPIC_API_KEY;
|
|
11
|
+
origOauth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (origApiKey !== undefined) process.env.ANTHROPIC_API_KEY = origApiKey;
|
|
16
|
+
else delete process.env.ANTHROPIC_API_KEY;
|
|
17
|
+
if (origOauth !== undefined) process.env.CLAUDE_CODE_OAUTH_TOKEN = origOauth;
|
|
18
|
+
else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("module exports runAuthStep function", () => {
|
|
22
|
+
expect(typeof runAuthStep).toBe("function");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { registerInteractiveCommand } from "../../commands/interactive";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
describe("Interactive Command Registration", () => {
|
|
6
|
+
it("registers interactive command on program", () => {
|
|
7
|
+
const program = new Command();
|
|
8
|
+
registerInteractiveCommand(program);
|
|
9
|
+
|
|
10
|
+
const cmd = program.commands.find((c) => c.name() === "interactive");
|
|
11
|
+
expect(cmd).toBeDefined();
|
|
12
|
+
expect(cmd?.description()).toContain("Interactive");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { spawnProject, killAll } from "../../commands/interactive/process-supervisor";
|
|
3
|
+
import type { ProjectConfig } from "../../commands/interactive/project-step";
|
|
4
|
+
import { mkdtempSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
describe("Process Supervisor", () => {
|
|
9
|
+
it("exports spawnProject and killAll functions", () => {
|
|
10
|
+
expect(typeof spawnProject).toBe("function");
|
|
11
|
+
expect(typeof killAll).toBe("function");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("spawns a simple process and returns ManagedProcess", async () => {
|
|
15
|
+
const tmpDir = mkdtempSync(resolve(tmpdir(), "aad-test-"));
|
|
16
|
+
const project: ProjectConfig = {
|
|
17
|
+
directory: tmpDir,
|
|
18
|
+
branch: "main",
|
|
19
|
+
requirementsPath: "/dev/null",
|
|
20
|
+
workers: 1,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mp = spawnProject(project, 0);
|
|
24
|
+
expect(mp.dashboardPort).toBe(7333);
|
|
25
|
+
expect(mp.project).toBe(project);
|
|
26
|
+
expect(mp.proc).toBeDefined();
|
|
27
|
+
|
|
28
|
+
// Kill immediately
|
|
29
|
+
await killAll([mp]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("increments dashboard port by index", async () => {
|
|
33
|
+
const tmpDir = mkdtempSync(resolve(tmpdir(), "aad-test-"));
|
|
34
|
+
const project: ProjectConfig = {
|
|
35
|
+
directory: tmpDir,
|
|
36
|
+
branch: "main",
|
|
37
|
+
requirementsPath: "/dev/null",
|
|
38
|
+
workers: 1,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mp = spawnProject(project, 3);
|
|
42
|
+
expect(mp.dashboardPort).toBe(7336);
|
|
43
|
+
await killAll([mp]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { printBox, success, warn, error } from "../../commands/interactive/prompts";
|
|
3
|
+
|
|
4
|
+
describe("Interactive Prompts Utilities", () => {
|
|
5
|
+
describe("printBox", () => {
|
|
6
|
+
it("prints a box with lines", () => {
|
|
7
|
+
const logs: string[] = [];
|
|
8
|
+
const origLog = console.log;
|
|
9
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
10
|
+
try {
|
|
11
|
+
printBox(["line 1", "line 2"]);
|
|
12
|
+
expect(logs.length).toBe(4); // top + 2 lines + bottom
|
|
13
|
+
expect(logs[0]).toContain("┌");
|
|
14
|
+
expect(logs[3]).toContain("└");
|
|
15
|
+
} finally {
|
|
16
|
+
console.log = origLog;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("prints a box with title", () => {
|
|
21
|
+
const logs: string[] = [];
|
|
22
|
+
const origLog = console.log;
|
|
23
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
24
|
+
try {
|
|
25
|
+
printBox(["content"], "Title");
|
|
26
|
+
expect(logs.length).toBe(5); // top + title + separator + content + bottom
|
|
27
|
+
expect(logs[1]).toContain("Title");
|
|
28
|
+
expect(logs[2]).toContain("├");
|
|
29
|
+
} finally {
|
|
30
|
+
console.log = origLog;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("status helpers", () => {
|
|
36
|
+
it("success prints green check", () => {
|
|
37
|
+
const logs: string[] = [];
|
|
38
|
+
const origLog = console.log;
|
|
39
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
40
|
+
try {
|
|
41
|
+
success("done");
|
|
42
|
+
expect(logs[0]).toContain("✓");
|
|
43
|
+
expect(logs[0]).toContain("done");
|
|
44
|
+
} finally {
|
|
45
|
+
console.log = origLog;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("warn prints yellow warning", () => {
|
|
50
|
+
const logs: string[] = [];
|
|
51
|
+
const origLog = console.log;
|
|
52
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
53
|
+
try {
|
|
54
|
+
warn("caution");
|
|
55
|
+
expect(logs[0]).toContain("⚠");
|
|
56
|
+
expect(logs[0]).toContain("caution");
|
|
57
|
+
} finally {
|
|
58
|
+
console.log = origLog;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("error prints red cross", () => {
|
|
63
|
+
const logs: string[] = [];
|
|
64
|
+
const origLog = console.log;
|
|
65
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
66
|
+
try {
|
|
67
|
+
error("fail");
|
|
68
|
+
expect(logs[0]).toContain("✗");
|
|
69
|
+
expect(logs[0]).toContain("fail");
|
|
70
|
+
} finally {
|
|
71
|
+
console.log = origLog;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { createUpdateCommand } from "../commands/update";
|
|
3
|
+
|
|
4
|
+
describe("Update Command", () => {
|
|
5
|
+
it("creates a command named 'update'", () => {
|
|
6
|
+
const cmd = createUpdateCommand();
|
|
7
|
+
expect(cmd.name()).toBe("update");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("has --check option", () => {
|
|
11
|
+
const cmd = createUpdateCommand();
|
|
12
|
+
const checkOpt = cmd.options.find((o) => o.long === "--check");
|
|
13
|
+
expect(checkOpt).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("has description", () => {
|
|
17
|
+
const cmd = createUpdateCommand();
|
|
18
|
+
expect(cmd.description()).toContain("Update");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth check / input step for interactive mode
|
|
3
|
+
*/
|
|
4
|
+
import { askConfirm, askSelect, askPassword, success, warn } from "./prompts.js";
|
|
5
|
+
|
|
6
|
+
export interface AuthResult {
|
|
7
|
+
method: "api-key" | "oauth";
|
|
8
|
+
token: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function maskToken(token: string): string {
|
|
12
|
+
if (token.length <= 8) return "••••••••";
|
|
13
|
+
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runAuthStep(): Promise<AuthResult> {
|
|
17
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
18
|
+
const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
19
|
+
|
|
20
|
+
// Existing API key detected
|
|
21
|
+
if (apiKey) {
|
|
22
|
+
console.log(`\n API Key検出: ${maskToken(apiKey)}`);
|
|
23
|
+
const useExisting = await askConfirm("この設定を使用しますか?", true);
|
|
24
|
+
if (useExisting) {
|
|
25
|
+
success("API Key認証を使用");
|
|
26
|
+
return { method: "api-key", token: apiKey };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Existing OAuth token detected
|
|
31
|
+
if (oauthToken) {
|
|
32
|
+
console.log(`\n OAuth Token検出: ${maskToken(oauthToken)}`);
|
|
33
|
+
const useExisting = await askConfirm("この設定を使用しますか?", true);
|
|
34
|
+
if (useExisting) {
|
|
35
|
+
success("OAuth認証を使用");
|
|
36
|
+
return { method: "oauth", token: oauthToken };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Manual input
|
|
41
|
+
if (!apiKey && !oauthToken) {
|
|
42
|
+
warn("認証トークンが未設定です");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const method = await askSelect<"api-key" | "oauth">("認証方式を選択:", [
|
|
46
|
+
{ name: "API Key (ANTHROPIC_API_KEY)", value: "api-key" },
|
|
47
|
+
{ name: "OAuth Token (CLAUDE_CODE_OAUTH_TOKEN)", value: "oauth" },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const token = await askPassword("トークンを入力:");
|
|
51
|
+
|
|
52
|
+
// Set in process env for this session
|
|
53
|
+
if (method === "api-key") {
|
|
54
|
+
process.env.ANTHROPIC_API_KEY = token;
|
|
55
|
+
} else {
|
|
56
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
success(`${method === "api-key" ? "API Key" : "OAuth"}認証を設定(セッション限定)`);
|
|
60
|
+
return { method, token };
|
|
61
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch confirmation + execution step
|
|
3
|
+
*/
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { getMemoryStatus } from "@aad/shared/memory-check";
|
|
6
|
+
import type { ProjectConfig } from "./project-step.js";
|
|
7
|
+
import {
|
|
8
|
+
type ManagedProcess,
|
|
9
|
+
spawnAll,
|
|
10
|
+
setupSignalHandler,
|
|
11
|
+
} from "./process-supervisor.js";
|
|
12
|
+
import { askConfirm, printBox, success } from "./prompts.js";
|
|
13
|
+
|
|
14
|
+
function formatPlanLines(projects: ProjectConfig[]): string[] {
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
for (let i = 0; i < projects.length; i++) {
|
|
17
|
+
const p = projects[i]!;
|
|
18
|
+
lines.push(`#${i + 1} ${p.directory}`);
|
|
19
|
+
lines.push(` ブランチ: ${p.branch}`);
|
|
20
|
+
lines.push(` 要件: ${p.requirementsPath}`);
|
|
21
|
+
lines.push(` ワーカー: ${p.workers}`);
|
|
22
|
+
if (i < projects.length - 1) lines.push("");
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runLaunchStep(
|
|
28
|
+
projects: ProjectConfig[],
|
|
29
|
+
): Promise<ManagedProcess[] | null> {
|
|
30
|
+
const memStatus = await getMemoryStatus();
|
|
31
|
+
const availableGB = memStatus.freeGB;
|
|
32
|
+
const totalWorkers = projects.reduce((sum, p) => sum + p.workers, 0);
|
|
33
|
+
|
|
34
|
+
const planLines = formatPlanLines(projects);
|
|
35
|
+
planLines.push("");
|
|
36
|
+
planLines.push(
|
|
37
|
+
`合計ワーカー: ${totalWorkers} メモリ: ${availableGB.toFixed(1)}GB free`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
printBox(planLines, "実行プラン");
|
|
41
|
+
console.log();
|
|
42
|
+
|
|
43
|
+
const confirmed = await askConfirm("実行しますか?", true);
|
|
44
|
+
if (!confirmed) {
|
|
45
|
+
console.log("\n 実行をキャンセルしました\n");
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log();
|
|
50
|
+
const managed = spawnAll(projects);
|
|
51
|
+
|
|
52
|
+
for (const mp of managed) {
|
|
53
|
+
const name = basename(mp.project.directory);
|
|
54
|
+
success(`${name} 起動 — Dashboard: http://localhost:${mp.dashboardPort}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setupSignalHandler(managed);
|
|
58
|
+
|
|
59
|
+
console.log("\n 全プロジェクト起動完了。ダッシュボードで進捗を確認できます。");
|
|
60
|
+
console.log(" 終了: Ctrl+C (全プロジェクト停止)\n");
|
|
61
|
+
|
|
62
|
+
// Wait for all processes to complete
|
|
63
|
+
const exitCodes = await Promise.all(managed.map((mp) => mp.proc.exited));
|
|
64
|
+
for (let i = 0; i < exitCodes.length; i++) {
|
|
65
|
+
const mp = managed[i]!;
|
|
66
|
+
const code = exitCodes[i]!;
|
|
67
|
+
const name = basename(mp.project.directory);
|
|
68
|
+
if (code === 0) {
|
|
69
|
+
success(`${name} 完了 (exit: 0)`);
|
|
70
|
+
} else {
|
|
71
|
+
console.log(` \x1b[31m✗\x1b[0m ${name} 終了 (exit: ${code})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return managed;
|
|
76
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child process management — spawn, monitor, and terminate AAD runs
|
|
3
|
+
*/
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { mkdirSync, existsSync, openSync } from "node:fs";
|
|
6
|
+
import type { ProjectConfig } from "./project-step.js";
|
|
7
|
+
|
|
8
|
+
const BASE_DASHBOARD_PORT = 7333;
|
|
9
|
+
|
|
10
|
+
export interface ManagedProcess {
|
|
11
|
+
project: ProjectConfig;
|
|
12
|
+
proc: ReturnType<typeof Bun.spawn>;
|
|
13
|
+
dashboardPort: number;
|
|
14
|
+
logDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureLogDir(directory: string, runId: string): string {
|
|
18
|
+
const logDir = resolve(directory, ".aad", "logs", runId);
|
|
19
|
+
if (!existsSync(logDir)) {
|
|
20
|
+
mkdirSync(logDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
return logDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateRunId(): string {
|
|
26
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function spawnProject(
|
|
30
|
+
project: ProjectConfig,
|
|
31
|
+
index: number,
|
|
32
|
+
): ManagedProcess {
|
|
33
|
+
const runId = generateRunId();
|
|
34
|
+
const logDir = ensureLogDir(project.directory, runId);
|
|
35
|
+
const dashboardPort = BASE_DASHBOARD_PORT + index;
|
|
36
|
+
|
|
37
|
+
const stdoutFd = openSync(resolve(logDir, "stdout.log"), "w");
|
|
38
|
+
const stderrFd = openSync(resolve(logDir, "stderr.log"), "w");
|
|
39
|
+
|
|
40
|
+
const proc = Bun.spawn(
|
|
41
|
+
[
|
|
42
|
+
"bun",
|
|
43
|
+
"run",
|
|
44
|
+
resolve(import.meta.dir, "../../../../main.ts"),
|
|
45
|
+
"run",
|
|
46
|
+
project.requirementsPath,
|
|
47
|
+
"--branch",
|
|
48
|
+
project.branch,
|
|
49
|
+
"--workers",
|
|
50
|
+
String(project.workers),
|
|
51
|
+
],
|
|
52
|
+
{
|
|
53
|
+
cwd: project.directory,
|
|
54
|
+
stdout: stdoutFd,
|
|
55
|
+
stderr: stderrFd,
|
|
56
|
+
env: {
|
|
57
|
+
...process.env,
|
|
58
|
+
AAD_DASHBOARD_PORT: String(dashboardPort),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return { project, proc, dashboardPort, logDir };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function spawnAll(projects: ProjectConfig[]): ManagedProcess[] {
|
|
67
|
+
return projects.map((project, i) => spawnProject(project, i));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function killAll(processes: ManagedProcess[]): Promise<void> {
|
|
71
|
+
for (const mp of processes) {
|
|
72
|
+
try {
|
|
73
|
+
mp.proc.kill("SIGTERM");
|
|
74
|
+
} catch {
|
|
75
|
+
// Process may have already exited
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wait for all to exit (with timeout)
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
for (const mp of processes) {
|
|
82
|
+
try {
|
|
83
|
+
mp.proc.kill("SIGKILL");
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, 5000);
|
|
89
|
+
|
|
90
|
+
await Promise.allSettled(processes.map((mp) => mp.proc.exited));
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function setupSignalHandler(processes: ManagedProcess[]): void {
|
|
95
|
+
const handler = (): void => {
|
|
96
|
+
console.log("\n\n 全プロセスを停止中...");
|
|
97
|
+
void killAll(processes).then(() => {
|
|
98
|
+
console.log(" 停止完了\n");
|
|
99
|
+
process.exit(0);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
process.on("SIGINT", handler);
|
|
104
|
+
process.on("SIGTERM", handler);
|
|
105
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project configuration step — repeatable loop
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, statSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { gitExec } from "@aad/git-workspace/git-exec";
|
|
7
|
+
import { getMemoryStatus } from "@aad/shared/memory-check";
|
|
8
|
+
import {
|
|
9
|
+
askConfirm,
|
|
10
|
+
askInput,
|
|
11
|
+
askSelect,
|
|
12
|
+
success,
|
|
13
|
+
warn,
|
|
14
|
+
error,
|
|
15
|
+
} from "./prompts.js";
|
|
16
|
+
|
|
17
|
+
export interface ProjectConfig {
|
|
18
|
+
directory: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
requirementsPath: string;
|
|
21
|
+
workers: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function isGitRepo(dir: string): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
await gitExec(["rev-parse", "--is-inside-work-tree"], { cwd: dir });
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function listBranches(dir: string): Promise<string[]> {
|
|
34
|
+
const result = await gitExec(["branch", "--format=%(refname:short)"], {
|
|
35
|
+
cwd: dir,
|
|
36
|
+
});
|
|
37
|
+
return result.stdout
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map((b: string) => b.trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getCurrentBranch(dir: string): Promise<string> {
|
|
44
|
+
const result = await gitExec(["branch", "--show-current"], { cwd: dir });
|
|
45
|
+
return result.stdout.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function pullBranch(dir: string, branch: string): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
await gitExec(["checkout", branch], { cwd: dir });
|
|
51
|
+
await gitExec(["pull", "--ff-only"], { cwd: dir });
|
|
52
|
+
success(`${branch} を最新に更新`);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
warn(`git pull失敗: ${e instanceof Error ? e.message : String(e)}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function suggestWorkers(availableGB: number): number {
|
|
61
|
+
if (availableGB >= 12) return 3;
|
|
62
|
+
if (availableGB >= 8) return 2;
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function configureProject(index: number): Promise<ProjectConfig> {
|
|
67
|
+
console.log(`\n── プロジェクト #${index + 1} ──\n`);
|
|
68
|
+
|
|
69
|
+
// Directory
|
|
70
|
+
const directory = await askInput("プロジェクトディレクトリパス:", {
|
|
71
|
+
validate: (v) => {
|
|
72
|
+
const resolved = resolve(v);
|
|
73
|
+
if (!existsSync(resolved)) return "ディレクトリが存在しません";
|
|
74
|
+
if (!statSync(resolved).isDirectory()) return "ディレクトリではありません";
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const resolvedDir = resolve(directory);
|
|
80
|
+
|
|
81
|
+
if (!(await isGitRepo(resolvedDir))) {
|
|
82
|
+
error("gitリポジトリが検出されませんでした");
|
|
83
|
+
throw new Error(`Not a git repository: ${resolvedDir}`);
|
|
84
|
+
}
|
|
85
|
+
success("gitリポジトリ検出");
|
|
86
|
+
|
|
87
|
+
// Branch selection
|
|
88
|
+
const branches = await listBranches(resolvedDir);
|
|
89
|
+
const currentBranch = await getCurrentBranch(resolvedDir);
|
|
90
|
+
const branchChoices = branches.map((b) => ({
|
|
91
|
+
name: b === currentBranch ? `${b} (現在)` : b,
|
|
92
|
+
value: b,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const branch = await askSelect("worktree元ブランチ:", branchChoices);
|
|
96
|
+
|
|
97
|
+
// Pull
|
|
98
|
+
const doPull = await askConfirm("ブランチを最新にしますか? (git pull)", true);
|
|
99
|
+
if (doPull) {
|
|
100
|
+
await pullBranch(resolvedDir, branch);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Requirements file
|
|
104
|
+
const requirementsPath = await askInput("要件定義ドキュメントパス:", {
|
|
105
|
+
validate: (v) => {
|
|
106
|
+
const absPath = resolve(resolvedDir, v);
|
|
107
|
+
if (!existsSync(absPath)) return `ファイルが存在しません: ${absPath}`;
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
success("ファイル存在確認");
|
|
112
|
+
|
|
113
|
+
// Workers
|
|
114
|
+
const memStatus = await getMemoryStatus();
|
|
115
|
+
const availableGB = memStatus.freeGB;
|
|
116
|
+
const suggested = suggestWorkers(availableGB);
|
|
117
|
+
|
|
118
|
+
const workersStr = await askInput(
|
|
119
|
+
`並行ワーカー数 (推奨: ${suggested}, 空きメモリ: ${availableGB.toFixed(1)}GB):`,
|
|
120
|
+
{
|
|
121
|
+
default: String(suggested),
|
|
122
|
+
validate: (v) => {
|
|
123
|
+
const n = parseInt(v, 10);
|
|
124
|
+
if (isNaN(n) || n < 1) return "1以上の整数を入力してください";
|
|
125
|
+
if (n > 8) return "最大8ワーカーまでです";
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const workers = parseInt(workersStr, 10);
|
|
132
|
+
|
|
133
|
+
console.log(`\n────────────────────────────`);
|
|
134
|
+
console.log(` プロジェクト #${index + 1} 設定完了`);
|
|
135
|
+
console.log(`────────────────────────────`);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
directory: resolvedDir,
|
|
139
|
+
branch,
|
|
140
|
+
requirementsPath: resolve(resolvedDir, requirementsPath),
|
|
141
|
+
workers,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function runProjectStep(): Promise<ProjectConfig[]> {
|
|
146
|
+
const projects: ProjectConfig[] = [];
|
|
147
|
+
|
|
148
|
+
// At least one project
|
|
149
|
+
projects.push(await configureProject(0));
|
|
150
|
+
|
|
151
|
+
// Additional projects
|
|
152
|
+
while (await askConfirm("別のプロジェクトを追加しますか?", false)) {
|
|
153
|
+
projects.push(await configureProject(projects.length));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return projects;
|
|
157
|
+
}
|