builder-assistant-engineer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/analyst/prompt.js +19 -0
- package/dist/analyst/retry.js +38 -0
- package/dist/artifacts/diff.js +47 -0
- package/dist/artifacts/lessons.js +32 -0
- package/dist/artifacts/merge.js +70 -0
- package/dist/artifacts/write.js +44 -0
- package/dist/backends/agent-cli.js +174 -0
- package/dist/backends/api.js +171 -0
- package/dist/backends/index.js +10 -0
- package/dist/backends/manual.js +52 -0
- package/dist/backends/sse.js +24 -0
- package/dist/backends/types.js +1 -0
- package/dist/bin.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/commands/context.js +12 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/next.js +103 -0
- package/dist/commands/plan.js +71 -0
- package/dist/commands/replan.js +37 -0
- package/dist/commands/review.js +37 -0
- package/dist/commands/shared.js +40 -0
- package/dist/commands/status.js +95 -0
- package/dist/config/commands.js +85 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/settings.js +8 -0
- package/dist/config/store.js +38 -0
- package/dist/core/bash.js +105 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/fs.js +35 -0
- package/dist/core/git.js +104 -0
- package/dist/core/gitignore.js +14 -0
- package/dist/core/json.js +13 -0
- package/dist/core/paths.js +14 -0
- package/dist/core/process.js +56 -0
- package/dist/core/prompt-loader.js +27 -0
- package/dist/core/state.js +27 -0
- package/dist/core/which.js +24 -0
- package/dist/detect/agents.js +17 -0
- package/dist/detect/mode.js +11 -0
- package/dist/digest/baseline.js +192 -0
- package/dist/digest/budget.js +31 -0
- package/dist/digest/files.js +167 -0
- package/dist/digest/format.js +18 -0
- package/dist/digest/git.js +19 -0
- package/dist/digest/index.js +100 -0
- package/dist/digest/manifests.js +133 -0
- package/dist/digest/read.js +28 -0
- package/dist/digest/redact.js +37 -0
- package/dist/digest/render.js +138 -0
- package/dist/digest/select.js +116 -0
- package/dist/digest/todos.js +16 -0
- package/dist/digest/tree.js +55 -0
- package/dist/digest/walk.js +72 -0
- package/dist/gates/capture.js +145 -0
- package/dist/gates/contract.js +339 -0
- package/dist/gates/enforce.js +143 -0
- package/dist/gates/findings.js +17 -0
- package/dist/gates/gate.js +315 -0
- package/dist/gates/ignore-rules.js +104 -0
- package/dist/gates/protected-files.js +311 -0
- package/dist/gates/regression.js +219 -0
- package/dist/gates/reports.js +349 -0
- package/dist/gates/results.js +295 -0
- package/dist/gates/runners.js +189 -0
- package/dist/gates/state-guard.js +56 -0
- package/dist/i18n/en.js +288 -0
- package/dist/i18n/es.js +288 -0
- package/dist/i18n/index.js +18 -0
- package/dist/interview/adaptive.js +69 -0
- package/dist/interview/brief.js +27 -0
- package/dist/interview/questions.js +18 -0
- package/dist/interview/render.js +19 -0
- package/dist/interview/reply.js +34 -0
- package/dist/next/attempts.js +151 -0
- package/dist/next/start.js +179 -0
- package/dist/plan/continuation.js +40 -0
- package/dist/plan/evidence.js +239 -0
- package/dist/plan/filter.js +20 -0
- package/dist/plan/parser.js +283 -0
- package/dist/plan/prior.js +32 -0
- package/dist/plan/questions.js +48 -0
- package/dist/plan/repair.js +33 -0
- package/dist/plan/report.js +37 -0
- package/dist/plan/run.js +191 -0
- package/dist/prompts/analyst.md +101 -0
- package/dist/prompts/continue.md +11 -0
- package/dist/prompts/fix-format.md +13 -0
- package/dist/prompts/fix-paths.md +23 -0
- package/dist/prompts/lesson.md +18 -0
- package/dist/prompts/retry.md +9 -0
- package/dist/prompts/review.md +36 -0
- package/dist/prompts/task.md +9 -0
- package/dist/review/changes.js +177 -0
- package/dist/review/diff.js +115 -0
- package/dist/review/integrity.js +150 -0
- package/dist/review/mechanical.js +62 -0
- package/dist/review/parse.js +40 -0
- package/dist/review/reviewer.js +23 -0
- package/dist/review/run.js +134 -0
- package/dist/review/scope.js +55 -0
- package/dist/review/secrets.js +163 -0
- package/dist/review/snapshot.js +38 -0
- package/dist/review/tests.js +140 -0
- package/dist/tasks/attempts.js +71 -0
- package/dist/tasks/checks.js +181 -0
- package/dist/tasks/frontmatter.js +58 -0
- package/dist/tasks/graph.js +27 -0
- package/dist/tasks/handoff.js +62 -0
- package/dist/tasks/learn.js +128 -0
- package/dist/tasks/load.js +37 -0
- package/dist/tasks/metrics.js +40 -0
- package/dist/tasks/runs.js +12 -0
- package/dist/tasks/schema.js +96 -0
- package/dist/tasks/select.js +15 -0
- package/dist/tasks/shell-words.js +176 -0
- package/dist/tasks/status.js +13 -0
- package/dist/tasks/verify.js +181 -0
- package/dist/ui/clack.js +46 -0
- package/dist/ui/prompter.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { LANGS } from "../i18n/index.js";
|
|
3
|
+
export const MODES = ["greenfield", "brownfield"];
|
|
4
|
+
export const BACKENDS = ["claude", "opencode", "codex", "gemini", "api", "manual"];
|
|
5
|
+
export const TARGETS = ["claude-code", "opencode", "codex", "gemini"];
|
|
6
|
+
export const COMMAND_KEYS = ["test", "lint", "typecheck", "build"];
|
|
7
|
+
export const REGRESSION_MODES = ["full", "task", "off"];
|
|
8
|
+
export const DEFAULT_DIGEST_MAX_CHARS = 100_000;
|
|
9
|
+
export const DEFAULT_AGENT_TIMEOUT_MINUTES = 45;
|
|
10
|
+
export const DEFAULT_CHECK_TIMEOUT_MINUTES = 15;
|
|
11
|
+
const command = z.string().trim().min(1).optional();
|
|
12
|
+
export const commandsSchema = z.object({
|
|
13
|
+
test: command,
|
|
14
|
+
lint: command,
|
|
15
|
+
typecheck: command,
|
|
16
|
+
build: command,
|
|
17
|
+
});
|
|
18
|
+
export const configSchema = z.object({
|
|
19
|
+
version: z.literal(1),
|
|
20
|
+
mode: z.enum(MODES),
|
|
21
|
+
backend: z.enum(BACKENDS),
|
|
22
|
+
targets: z.array(z.enum(TARGETS)).min(1),
|
|
23
|
+
lang: z.enum(LANGS),
|
|
24
|
+
digest: z
|
|
25
|
+
.object({ maxChars: z.number().int().positive() })
|
|
26
|
+
.default({ maxChars: DEFAULT_DIGEST_MAX_CHARS }),
|
|
27
|
+
commands: commandsSchema.default({}),
|
|
28
|
+
gates: z
|
|
29
|
+
.object({
|
|
30
|
+
regression: z.enum(REGRESSION_MODES).default("full"),
|
|
31
|
+
timeoutMinutes: z.number().int().positive().default(DEFAULT_CHECK_TIMEOUT_MINUTES),
|
|
32
|
+
})
|
|
33
|
+
.default({ regression: "full", timeoutMinutes: DEFAULT_CHECK_TIMEOUT_MINUTES }),
|
|
34
|
+
verify: z.object({ allow: z.array(z.string().trim().min(1)).default([]) }).default({ allow: [] }),
|
|
35
|
+
secrets: z
|
|
36
|
+
.object({ allow: z.array(z.string().trim().min(1)).default([]) })
|
|
37
|
+
.default({ allow: [] }),
|
|
38
|
+
agent: z
|
|
39
|
+
.object({
|
|
40
|
+
timeoutMinutes: z.number().int().positive().default(DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
41
|
+
})
|
|
42
|
+
.default({ timeoutMinutes: DEFAULT_AGENT_TIMEOUT_MINUTES }),
|
|
43
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { UserError } from "../core/errors.js";
|
|
4
|
+
import { readTextIfExists, writeText } from "../core/fs.js";
|
|
5
|
+
import { baePaths } from "../core/paths.js";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
7
|
+
import { configSchema } from "./schema.js";
|
|
8
|
+
export async function readConfig(cwd) {
|
|
9
|
+
const path = baePaths(cwd).config;
|
|
10
|
+
const text = await readTextIfExists(path);
|
|
11
|
+
if (text === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
return parseConfig(text, relative(cwd, path));
|
|
14
|
+
}
|
|
15
|
+
export function parseConfig(text, displayPath) {
|
|
16
|
+
const result = configSchema.safeParse(parseJson(text, displayPath));
|
|
17
|
+
if (result.success)
|
|
18
|
+
return result.data;
|
|
19
|
+
throw new UserError(t("error.configInvalid", { path: displayPath, details: z.prettifyError(result.error) }));
|
|
20
|
+
}
|
|
21
|
+
export async function writeConfig(cwd, config) {
|
|
22
|
+
await writeText(baePaths(cwd).config, `${JSON.stringify(config, null, 2)}\n`);
|
|
23
|
+
}
|
|
24
|
+
export function readInterview(cwd) {
|
|
25
|
+
return readTextIfExists(baePaths(cwd).interview);
|
|
26
|
+
}
|
|
27
|
+
export async function writeInterview(cwd, text) {
|
|
28
|
+
await writeText(baePaths(cwd).interview, text.endsWith("\n") ? text : `${text}\n`);
|
|
29
|
+
}
|
|
30
|
+
function parseJson(text, displayPath) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
36
|
+
throw new UserError(t("error.configJson", { path: displayPath, details }));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { findExecutable } from "./which.js";
|
|
6
|
+
const SCRIPT_TIMEOUT_MS = 15 * 60_000;
|
|
7
|
+
const WSL_BASH = /[\\/](system32|sysnative|windowsapps)[\\/]/i;
|
|
8
|
+
let cached;
|
|
9
|
+
export function bashPath() {
|
|
10
|
+
cached ??= findBash();
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
export async function findBash(env = process.env, platform = process.platform) {
|
|
14
|
+
if (platform !== "win32")
|
|
15
|
+
return findExecutable("bash", env, platform);
|
|
16
|
+
const roots = [env.ProgramFiles, env["ProgramFiles(x86)"], env.ProgramW6432].filter(Boolean);
|
|
17
|
+
const candidates = [
|
|
18
|
+
...roots.map((root) => join(root ?? "", "Git", "bin", "bash.exe")),
|
|
19
|
+
...(env.LOCALAPPDATA ? [join(env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")] : []),
|
|
20
|
+
];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (await isFile(candidate))
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
const found = await findExecutable("bash", env, platform);
|
|
26
|
+
return found && !WSL_BASH.test(found) ? found : undefined;
|
|
27
|
+
}
|
|
28
|
+
export async function runScript(bash, script, options) {
|
|
29
|
+
const dir = await mkdtemp(join(tmpdir(), "bae-script-"));
|
|
30
|
+
const file = join(dir, "script.sh");
|
|
31
|
+
await writeFile(file, script, "utf8");
|
|
32
|
+
const group = process.platform !== "win32";
|
|
33
|
+
const limit = options.timeoutMs ?? SCRIPT_TIMEOUT_MS;
|
|
34
|
+
try {
|
|
35
|
+
const subprocess = execa(bash, ["--noprofile", "--norc", file], {
|
|
36
|
+
cwd: options.cwd,
|
|
37
|
+
all: true,
|
|
38
|
+
reject: false,
|
|
39
|
+
detached: group,
|
|
40
|
+
...(group ? {} : { timeout: limit }),
|
|
41
|
+
env: { ...options.env, PWD: options.cwd },
|
|
42
|
+
});
|
|
43
|
+
const { onOutput } = options;
|
|
44
|
+
if (onOutput)
|
|
45
|
+
subprocess.all?.on("data", (chunk) => onOutput(chunk.toString()));
|
|
46
|
+
let timedOut = false;
|
|
47
|
+
const stop = (signal) => {
|
|
48
|
+
if (group && subprocess.pid)
|
|
49
|
+
killGroup(subprocess.pid, signal);
|
|
50
|
+
};
|
|
51
|
+
const timer = group
|
|
52
|
+
? setTimeout(() => {
|
|
53
|
+
timedOut = true;
|
|
54
|
+
stop("SIGTERM");
|
|
55
|
+
setTimeout(() => stop("SIGKILL"), 3_000).unref();
|
|
56
|
+
}, limit)
|
|
57
|
+
: undefined;
|
|
58
|
+
subprocess.on("exit", () => stop("SIGKILL"));
|
|
59
|
+
const forward = (signal) => {
|
|
60
|
+
stop(signal);
|
|
61
|
+
release();
|
|
62
|
+
process.kill(process.pid, signal);
|
|
63
|
+
};
|
|
64
|
+
const release = () => {
|
|
65
|
+
process.off("SIGINT", forward);
|
|
66
|
+
process.off("SIGTERM", forward);
|
|
67
|
+
};
|
|
68
|
+
if (group) {
|
|
69
|
+
process.once("SIGINT", forward);
|
|
70
|
+
process.once("SIGTERM", forward);
|
|
71
|
+
}
|
|
72
|
+
const result = await subprocess.finally(release);
|
|
73
|
+
if (timer)
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
if (timedOut) {
|
|
76
|
+
return {
|
|
77
|
+
exitCode: -1,
|
|
78
|
+
output: result.all ?? "",
|
|
79
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
exitCode: result.exitCode ?? -1,
|
|
84
|
+
output: result.all ?? result.message ?? "",
|
|
85
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await rm(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function killGroup(pid, signal) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(-pid, signal);
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}
|
|
98
|
+
async function isFile(path) {
|
|
99
|
+
try {
|
|
100
|
+
return (await stat(path)).isFile();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class UserError extends Error {
|
|
2
|
+
name = "UserError";
|
|
3
|
+
}
|
|
4
|
+
export class FormatError extends Error {
|
|
5
|
+
name = "FormatError";
|
|
6
|
+
}
|
|
7
|
+
export class ExitCode extends Error {
|
|
8
|
+
code;
|
|
9
|
+
name = "ExitCode";
|
|
10
|
+
constructor(code) {
|
|
11
|
+
super(`exit ${code}`);
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/core/fs.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
export async function readTextIfExists(path) {
|
|
4
|
+
try {
|
|
5
|
+
return await readFile(path, "utf8");
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (isNotFound(error))
|
|
9
|
+
return undefined;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function writeText(path, text) {
|
|
14
|
+
await mkdir(dirname(path), { recursive: true });
|
|
15
|
+
await writeFile(path, text, "utf8");
|
|
16
|
+
}
|
|
17
|
+
function isNotFound(error) {
|
|
18
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
19
|
+
}
|
|
20
|
+
export async function isSymlink(path) {
|
|
21
|
+
return (await lstat(path).catch(() => undefined))?.isSymbolicLink() ?? false;
|
|
22
|
+
}
|
|
23
|
+
export async function looksBinary(path) {
|
|
24
|
+
const handle = await open(path, "r").catch(() => undefined);
|
|
25
|
+
if (!handle)
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
const buffer = Buffer.alloc(8192);
|
|
29
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
30
|
+
return buffer.subarray(0, bytesRead).includes(0);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await handle.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
3
|
+
export const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
4
|
+
const HARDENED = [
|
|
5
|
+
"--no-replace-objects",
|
|
6
|
+
"-c",
|
|
7
|
+
"core.quotePath=false",
|
|
8
|
+
"-c",
|
|
9
|
+
"core.fsmonitor=false",
|
|
10
|
+
"-c",
|
|
11
|
+
"core.untrackedCache=false",
|
|
12
|
+
"-c",
|
|
13
|
+
"diff.noprefix=false",
|
|
14
|
+
"-c",
|
|
15
|
+
"diff.mnemonicPrefix=false",
|
|
16
|
+
"-c",
|
|
17
|
+
"diff.relative=false",
|
|
18
|
+
"-c",
|
|
19
|
+
"diff.external=",
|
|
20
|
+
"-c",
|
|
21
|
+
"color.ui=never",
|
|
22
|
+
];
|
|
23
|
+
export const DIFF_FLAGS = [
|
|
24
|
+
"--no-color",
|
|
25
|
+
"--no-ext-diff",
|
|
26
|
+
"--no-textconv",
|
|
27
|
+
"--text",
|
|
28
|
+
"--no-renames",
|
|
29
|
+
"--src-prefix=a/",
|
|
30
|
+
"--dst-prefix=b/",
|
|
31
|
+
];
|
|
32
|
+
export async function git(cwd, args) {
|
|
33
|
+
const result = await runCommand("git", [...HARDENED, ...args], {
|
|
34
|
+
cwd,
|
|
35
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
36
|
+
});
|
|
37
|
+
return result.exitCode === 0 ? result.stdout : undefined;
|
|
38
|
+
}
|
|
39
|
+
export async function gitPaths(cwd, args) {
|
|
40
|
+
const out = await git(cwd, [...args.slice(0, 1), "-z", ...args.slice(1)]);
|
|
41
|
+
return out === undefined ? undefined : out.split("\0").filter((path) => path !== "");
|
|
42
|
+
}
|
|
43
|
+
export async function flaggedFiles(cwd) {
|
|
44
|
+
const entries = await gitPaths(cwd, ["ls-files", "-v"]);
|
|
45
|
+
return entries
|
|
46
|
+
?.filter((entry) => /^[a-zS] /.test(entry))
|
|
47
|
+
.map((entry) => entry.slice(2))
|
|
48
|
+
.sort();
|
|
49
|
+
}
|
|
50
|
+
export function unquotePath(raw) {
|
|
51
|
+
if (!raw.startsWith('"') || !raw.endsWith('"'))
|
|
52
|
+
return raw;
|
|
53
|
+
const bytes = [];
|
|
54
|
+
const body = raw.slice(1, -1);
|
|
55
|
+
const escapes = {
|
|
56
|
+
n: 10,
|
|
57
|
+
t: 9,
|
|
58
|
+
r: 13,
|
|
59
|
+
'"': 34,
|
|
60
|
+
"\\": 92,
|
|
61
|
+
a: 7,
|
|
62
|
+
b: 8,
|
|
63
|
+
f: 12,
|
|
64
|
+
v: 11,
|
|
65
|
+
};
|
|
66
|
+
for (let index = 0; index < body.length; index++) {
|
|
67
|
+
const char = body[index] ?? "";
|
|
68
|
+
if (char !== "\\") {
|
|
69
|
+
bytes.push(...Buffer.from(char, "utf8"));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const next = body[index + 1] ?? "";
|
|
73
|
+
if (/[0-7]/.test(next)) {
|
|
74
|
+
bytes.push(Number.parseInt(body.slice(index + 1, index + 4), 8));
|
|
75
|
+
index += 3;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
bytes.push(escapes[next] ?? next.charCodeAt(0));
|
|
79
|
+
index += 1;
|
|
80
|
+
}
|
|
81
|
+
return Buffer.from(bytes).toString("utf8");
|
|
82
|
+
}
|
|
83
|
+
export async function isGitRepo(cwd) {
|
|
84
|
+
return (await git(cwd, ["rev-parse", "--is-inside-work-tree"]))?.trim() === "true";
|
|
85
|
+
}
|
|
86
|
+
export async function headCommit(cwd) {
|
|
87
|
+
return (await git(cwd, ["rev-parse", "--verify", "HEAD"]))?.trim() || undefined;
|
|
88
|
+
}
|
|
89
|
+
export async function verifyCommit(cwd, ref) {
|
|
90
|
+
return ((await git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]))?.trim() || undefined);
|
|
91
|
+
}
|
|
92
|
+
export async function resolveBase(cwd, base) {
|
|
93
|
+
const verified = base && (await verifyCommit(cwd, base));
|
|
94
|
+
return verified || (await headCommit(cwd)) || EMPTY_TREE;
|
|
95
|
+
}
|
|
96
|
+
export function lines(text) {
|
|
97
|
+
return (text ?? "").split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
98
|
+
}
|
|
99
|
+
export function literal(path) {
|
|
100
|
+
return `:(literal)${path}`;
|
|
101
|
+
}
|
|
102
|
+
export function excluded(path) {
|
|
103
|
+
return `:(exclude,literal)${path}`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readTextIfExists, writeText } from "./fs.js";
|
|
3
|
+
export const BAE_IGNORED = [".bae/tmp/"];
|
|
4
|
+
export async function ensureGitignore(cwd) {
|
|
5
|
+
const path = join(cwd, ".gitignore");
|
|
6
|
+
const current = (await readTextIfExists(path)) ?? "";
|
|
7
|
+
const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
8
|
+
const missing = BAE_IGNORED.filter((entry) => !present.has(entry));
|
|
9
|
+
if (missing.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const separator = current && !current.endsWith("\n") ? "\n" : "";
|
|
12
|
+
await writeText(path, `${current}${separator}${missing.join("\n")}\n`);
|
|
13
|
+
return missing;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function asRecord(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: {};
|
|
5
|
+
}
|
|
6
|
+
export function parseObject(text) {
|
|
7
|
+
try {
|
|
8
|
+
return asRecord(JSON.parse(text));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
export const BAE_DIR = ".bae";
|
|
3
|
+
export function baePaths(cwd) {
|
|
4
|
+
const dir = join(cwd, BAE_DIR);
|
|
5
|
+
return {
|
|
6
|
+
dir,
|
|
7
|
+
config: join(dir, "config.json"),
|
|
8
|
+
interview: join(dir, "interview.md"),
|
|
9
|
+
prompts: join(dir, "prompts"),
|
|
10
|
+
runs: join(dir, "runs"),
|
|
11
|
+
tmp: join(dir, "tmp"),
|
|
12
|
+
ignore: join(cwd, ".baeignore"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { findExecutable } from "./which.js";
|
|
3
|
+
const SHELL_TIMEOUT_MS = 15 * 60_000;
|
|
4
|
+
export async function runCommand(file, args, options) {
|
|
5
|
+
if (!(await findExecutable(file)))
|
|
6
|
+
return missing(file);
|
|
7
|
+
const subprocess = execa(file, args, {
|
|
8
|
+
cwd: options.cwd,
|
|
9
|
+
input: options.input,
|
|
10
|
+
timeout: options.timeoutMs,
|
|
11
|
+
reject: false,
|
|
12
|
+
env: { PWD: options.cwd },
|
|
13
|
+
});
|
|
14
|
+
const { onStdout } = options;
|
|
15
|
+
if (onStdout)
|
|
16
|
+
subprocess.stdout?.on("data", (chunk) => onStdout(chunk.toString()));
|
|
17
|
+
const result = await subprocess;
|
|
18
|
+
const notFound = "code" in result && result.code === "ENOENT";
|
|
19
|
+
return {
|
|
20
|
+
exitCode: result.exitCode ?? -1,
|
|
21
|
+
stdout: result.stdout ?? "",
|
|
22
|
+
stderr: result.exitCode === undefined ? (result.message ?? "") : (result.stderr ?? ""),
|
|
23
|
+
notFound,
|
|
24
|
+
...(result.timedOut ? { timedOut: true } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function runInteractive(file, args, options) {
|
|
28
|
+
if (!(await findExecutable(file)))
|
|
29
|
+
return missing(file);
|
|
30
|
+
const result = await execa(file, args, {
|
|
31
|
+
cwd: options.cwd,
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
reject: false,
|
|
34
|
+
env: { PWD: options.cwd },
|
|
35
|
+
});
|
|
36
|
+
const notFound = "code" in result && result.code === "ENOENT";
|
|
37
|
+
return { exitCode: result.exitCode ?? -1, stdout: "", stderr: result.message ?? "", notFound };
|
|
38
|
+
}
|
|
39
|
+
function missing(file) {
|
|
40
|
+
return { exitCode: -1, stdout: "", stderr: `${file}: command not found`, notFound: true };
|
|
41
|
+
}
|
|
42
|
+
export async function runShell(command, options) {
|
|
43
|
+
const subprocess = execa(command, {
|
|
44
|
+
cwd: options.cwd,
|
|
45
|
+
shell: true,
|
|
46
|
+
all: true,
|
|
47
|
+
reject: false,
|
|
48
|
+
timeout: options.timeoutMs ?? SHELL_TIMEOUT_MS,
|
|
49
|
+
env: { PWD: options.cwd },
|
|
50
|
+
});
|
|
51
|
+
const { onOutput } = options;
|
|
52
|
+
if (onOutput)
|
|
53
|
+
subprocess.all?.on("data", (chunk) => onOutput(chunk.toString()));
|
|
54
|
+
const result = await subprocess;
|
|
55
|
+
return { exitCode: result.exitCode ?? -1, output: result.all ?? result.message ?? "" };
|
|
56
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
5
|
+
import { UserError } from "./errors.js";
|
|
6
|
+
import { readTextIfExists } from "./fs.js";
|
|
7
|
+
import { baePaths } from "./paths.js";
|
|
8
|
+
const BUILTIN_DIR = fileURLToPath(new URL("../prompts/", import.meta.url));
|
|
9
|
+
const VARIABLE = /\{\{(\w+)\}\}/g;
|
|
10
|
+
export async function loadPrompt(name, cwd) {
|
|
11
|
+
const overridePath = join(baePaths(cwd).prompts, `${name}.md`);
|
|
12
|
+
const override = await readTextIfExists(overridePath);
|
|
13
|
+
if (override !== undefined)
|
|
14
|
+
return { path: overridePath, text: override };
|
|
15
|
+
const path = join(BUILTIN_DIR, `${name}.md`);
|
|
16
|
+
return { path, text: await readFile(path, "utf8") };
|
|
17
|
+
}
|
|
18
|
+
export function renderPrompt(prompt, vars) {
|
|
19
|
+
const missing = findVariables(prompt.text).filter((name) => !Object.hasOwn(vars, name));
|
|
20
|
+
if (missing.length > 0) {
|
|
21
|
+
throw new UserError(t("error.promptMissingVars", { path: prompt.path, vars: missing.join(", ") }));
|
|
22
|
+
}
|
|
23
|
+
return prompt.text.replace(VARIABLE, (_, name) => vars[name] ?? "");
|
|
24
|
+
}
|
|
25
|
+
export function findVariables(text) {
|
|
26
|
+
return [...new Set(Array.from(text.matchAll(VARIABLE), (match) => match[1] ?? ""))];
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const KEY_LENGTH = 16;
|
|
6
|
+
export function stateHome() {
|
|
7
|
+
return process.env.BAE_HOME || join(homedir(), ".bae");
|
|
8
|
+
}
|
|
9
|
+
export function repoState(cwd) {
|
|
10
|
+
return join(stateHome(), repoKey(cwd));
|
|
11
|
+
}
|
|
12
|
+
export function repoKey(cwd) {
|
|
13
|
+
return createHash("sha256").update(realPath(cwd)).digest("hex").slice(0, KEY_LENGTH);
|
|
14
|
+
}
|
|
15
|
+
export function displayPath(path) {
|
|
16
|
+
const home = homedir();
|
|
17
|
+
const shown = path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
18
|
+
return shown.split("\\").join("/");
|
|
19
|
+
}
|
|
20
|
+
function realPath(cwd) {
|
|
21
|
+
try {
|
|
22
|
+
return realpathSync.native(cwd);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return cwd;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
+
export async function findExecutable(name, env = process.env, platform = process.platform) {
|
|
4
|
+
if (isAbsolute(name) || /[\\/]/.test(name))
|
|
5
|
+
return (await isFile(name)) ? name : undefined;
|
|
6
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
7
|
+
const extensions = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
|
|
8
|
+
for (const dir of dirs) {
|
|
9
|
+
for (const extension of extensions) {
|
|
10
|
+
const candidate = join(dir, `${name}${extension}`);
|
|
11
|
+
if (await isFile(candidate))
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
async function isFile(path) {
|
|
18
|
+
try {
|
|
19
|
+
return (await stat(path)).isFile();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { hasApiCredentials } from "../backends/api.js";
|
|
2
|
+
import { findExecutable } from "../core/which.js";
|
|
3
|
+
export { findExecutable };
|
|
4
|
+
export const AGENT_BACKENDS = ["claude", "opencode", "codex", "gemini"];
|
|
5
|
+
export async function detectInstalledAgents(env = process.env, platform = process.platform) {
|
|
6
|
+
const found = await Promise.all(AGENT_BACKENDS.map(async (name) => (await findExecutable(name, env, platform)) ? name : undefined));
|
|
7
|
+
return found.filter((name) => name !== undefined);
|
|
8
|
+
}
|
|
9
|
+
export async function suggestBackend(env = process.env, platform = process.platform) {
|
|
10
|
+
return pickDefaultBackend(await detectInstalledAgents(env, platform), env);
|
|
11
|
+
}
|
|
12
|
+
export function pickDefaultBackend(installed, env) {
|
|
13
|
+
const [first] = installed;
|
|
14
|
+
if (first)
|
|
15
|
+
return first;
|
|
16
|
+
return hasApiCredentials(env) ? "api" : "manual";
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { languageOf } from "../digest/files.js";
|
|
2
|
+
import { ecosystemOf } from "../digest/manifests.js";
|
|
3
|
+
import { scanFiles } from "../digest/walk.js";
|
|
4
|
+
export function detectMode(paths) {
|
|
5
|
+
const hasCode = paths.some((path) => ecosystemOf(path) !== undefined || languageOf(path) !== undefined);
|
|
6
|
+
return hasCode ? "brownfield" : "greenfield";
|
|
7
|
+
}
|
|
8
|
+
export async function detectProjectMode(root) {
|
|
9
|
+
const { files } = await scanFiles(root);
|
|
10
|
+
return detectMode(files.map((file) => file.path));
|
|
11
|
+
}
|