@afokapu/atdd-flow 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 +226 -0
- package/bin/atdd-flow +3 -0
- package/conventions/atdd-workflow.workflow/atdd-workflow.workflow.lifecycle.convention.yaml +183 -0
- package/package.json +47 -0
- package/src/adapters.ts +88 -0
- package/src/cli.ts +134 -0
- package/src/core.ts +226 -0
- package/src/judgment.ts +333 -0
- package/src/overview.ts +403 -0
- package/src/reviews.ts +448 -0
- package/src/seats.ts +233 -0
- package/src/tasks.ts +243 -0
- package/src/threads.ts +153 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { init, initProject, spawn, bind, useApplication, attach, launch, describe, checkpoint, migrate, openSeat } from "./seats";
|
|
5
|
+
import { addParticipant, openThread, post, receipt, result, startThread } from "./threads";
|
|
6
|
+
import * as tasks from "./tasks";
|
|
7
|
+
import { required, values, words } from "./core";
|
|
8
|
+
import * as judgment from "./judgment";
|
|
9
|
+
import * as reviews from "./reviews";
|
|
10
|
+
import { status } from "./overview";
|
|
11
|
+
|
|
12
|
+
const usage = `atdd-flow — filesystem-first agent seats and tasks
|
|
13
|
+
|
|
14
|
+
Run commands from a Desk containing desk.yaml.
|
|
15
|
+
Legacy coordination.yaml registries are read safely and can be migrated.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
atdd-flow init <desk-directory> [--git]
|
|
19
|
+
atdd-flow desk migrate
|
|
20
|
+
atdd-flow project init <project>
|
|
21
|
+
atdd-flow spawn <project> <role> <name> [--worktree <path>] [--branch <branch>] [--agent <legacy-executable>]
|
|
22
|
+
atdd-flow bind <address> [--application <application>] --address <native-address>
|
|
23
|
+
atdd-flow attach <address> [--application <application>]
|
|
24
|
+
atdd-flow launch <address> --application <application> --placement <native-container-address>
|
|
25
|
+
atdd-flow application use <address> <application>
|
|
26
|
+
atdd-flow describe <address> --purpose <one-line responsibility>
|
|
27
|
+
atdd-flow checkpoint <address> --summary <text> --next <text> [--status active|standby|blocked|complete|unverified]
|
|
28
|
+
atdd-flow task add <project> <task-id> --title <text> --coordinator <address> [--assignee <address>] [--body <text>] [--source <reference>] [--depends-on <task-id,...>] --done-when <text> [--done-when <text> ...]
|
|
29
|
+
atdd-flow task amend <project> <task-id> [--title <text>] [--body <text>] [--source <reference>] [--depends-on <task-id,...>]
|
|
30
|
+
atdd-flow task import <project> <task-id> --proof <reference> [--proof <reference> ...] [--done-when <text> ...] [--source <reference>]
|
|
31
|
+
atdd-flow task start|review|return <project> <task-id> --by <address>
|
|
32
|
+
atdd-flow task done <project> <task-id> --by <address> [--retire-assignee]
|
|
33
|
+
atdd-flow task prove <project> <task-id> --by <address> --item <number> --proof <reference>
|
|
34
|
+
atdd-flow task block <project> <task-id> --by <address> --reason <text>
|
|
35
|
+
atdd-flow task list <project> [--coordinator <address>] [--assignee <address>]
|
|
36
|
+
atdd-flow task open <project> <task-id>
|
|
37
|
+
atdd-flow thread start --with <address,...> --subject <text> [--task <project/task-id>]
|
|
38
|
+
atdd-flow thread add <thread-id> <address>
|
|
39
|
+
atdd-flow thread open <thread-id>
|
|
40
|
+
atdd-flow post <thread-id> --from <address> --to <all|address,...> --body <text> [--expects-result]
|
|
41
|
+
atdd-flow receipt <thread-id> <message-id> --from <address> [--body <text>]
|
|
42
|
+
atdd-flow result <thread-id> <message-id> --from <address> --body <text>
|
|
43
|
+
atdd-flow status [project <project>|task <project> <task-id>|seat <address>|thread <thread-id>] [--all]
|
|
44
|
+
atdd-flow scout --goal <text> --path <file> [--path <file> ...] [--question <text>]
|
|
45
|
+
atdd-flow focus-check <project> <task-id> --action <proposed action>
|
|
46
|
+
atdd-flow behavioral-review launch <project> <task-id> --by <coordinator> --application <application> --placement <native-container-address> [--gate <reference> ...]
|
|
47
|
+
atdd-flow behavioral-review record <project> <task-id> --by <reviewer-address> --file <result-yaml>
|
|
48
|
+
atdd-flow behavioral-review open <project> <task-id>
|
|
49
|
+
atdd-flow open <address>
|
|
50
|
+
|
|
51
|
+
Global:
|
|
52
|
+
atdd-flow --root <desk-directory> <command>
|
|
53
|
+
ATDD_WORKFLOW_ROOT=<desk-directory> atdd-flow <command>`;
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const original = process.argv.slice(2);
|
|
57
|
+
if (!original.length || original.includes("--help") || original.includes("-h")) return console.log(usage);
|
|
58
|
+
const rootIndex = original.indexOf("--root");
|
|
59
|
+
const rootOverride = rootIndex < 0 ? process.env.ATDD_WORKFLOW_ROOT : required(original[rootIndex + 1], "--root");
|
|
60
|
+
const args = rootIndex < 0 ? original : original.filter((_, index) => index !== rootIndex && index !== rootIndex + 1);
|
|
61
|
+
const [command, ...rest] = args;
|
|
62
|
+
const root = resolve(rootOverride ?? process.cwd());
|
|
63
|
+
const commands: Record<string, () => Promise<void>> = {
|
|
64
|
+
init: () => init(resolve(required(rest[0], "desk directory")), basename(required(rest[0], "desk directory")), rest.slice(1)),
|
|
65
|
+
desk: async () => {
|
|
66
|
+
if (rest[0] === "migrate") return migrate(root);
|
|
67
|
+
throw new Error("Use `atdd-flow desk migrate`.");
|
|
68
|
+
},
|
|
69
|
+
project: async () => {
|
|
70
|
+
if (rest[0] === "init") return initProject(root, required(rest[1], "project"));
|
|
71
|
+
throw new Error("Use `atdd-flow project init <project>`.");
|
|
72
|
+
},
|
|
73
|
+
spawn: () => spawn(root, required(rest[0], "project"), required(rest[1], "role"), required(rest[2], "name"), rest.slice(3)),
|
|
74
|
+
bind: () => bind(root, required(rest[0], "address"), rest.slice(1)),
|
|
75
|
+
attach: () => attach(root, required(rest[0], "address"), rest.slice(1)),
|
|
76
|
+
launch: () => launch(root, required(rest[0], "address"), rest.slice(1)),
|
|
77
|
+
application: async () => {
|
|
78
|
+
if (rest[0] === "use") return useApplication(root, required(rest[1], "address"), required(rest[2], "application"));
|
|
79
|
+
throw new Error("Use `atdd-flow application use <address> <application>`.");
|
|
80
|
+
},
|
|
81
|
+
describe: () => describe(root, required(rest[0], "address"), rest.slice(1)),
|
|
82
|
+
checkpoint: () => checkpoint(root, required(rest[0], "address"), rest.slice(1)),
|
|
83
|
+
task: async () => {
|
|
84
|
+
const [subcommand, projectName, taskId, ...tail] = rest;
|
|
85
|
+
if (subcommand === "add") return tasks.add(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
86
|
+
if (subcommand === "amend") return tasks.amend(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
87
|
+
if (subcommand === "import") return tasks.importCompleted(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
88
|
+
if (subcommand === "start") return tasks.start(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
89
|
+
if (subcommand === "review") return tasks.review(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
90
|
+
if (subcommand === "done") return tasks.done(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
91
|
+
if (subcommand === "return") return tasks.returnToWork(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
92
|
+
if (subcommand === "prove") return tasks.prove(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
93
|
+
if (subcommand === "block") return tasks.block(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
94
|
+
if (subcommand === "list") return tasks.list(root, required(projectName, "project"), rest.slice(2));
|
|
95
|
+
if (subcommand === "open") return tasks.open(root, required(projectName, "project"), required(taskId, "task id"));
|
|
96
|
+
throw new Error("Use `atdd-flow task add|amend|import|start|prove|review|return|done|block|list|open`.");
|
|
97
|
+
},
|
|
98
|
+
thread: async () => {
|
|
99
|
+
const [subcommand, ...tail] = rest;
|
|
100
|
+
if (subcommand === "start") return startThread(root, tail);
|
|
101
|
+
if (subcommand === "add") return addParticipant(root, required(tail[0], "thread id"), required(tail[1], "address"));
|
|
102
|
+
if (subcommand === "open") return openThread(root, required(tail[0], "thread id"));
|
|
103
|
+
throw new Error("Use `atdd-flow thread start|add|open`.");
|
|
104
|
+
},
|
|
105
|
+
post: () => post(root, required(rest[0], "thread id"), rest.slice(1)),
|
|
106
|
+
receipt: () => receipt(root, required(rest[0], "thread id"), required(rest[1], "message id"), rest.slice(2)),
|
|
107
|
+
result: () => result(root, required(rest[0], "thread id"), required(rest[1], "message id"), rest.slice(2)),
|
|
108
|
+
status: () => status(root, rest),
|
|
109
|
+
scout: async () => console.log(JSON.stringify(await judgment.scout({
|
|
110
|
+
goal: required(words(rest, "--goal"), "--goal"),
|
|
111
|
+
candidates: values(rest, "--path"),
|
|
112
|
+
...(words(rest, "--question") ? { question: words(rest, "--question") } : {}),
|
|
113
|
+
}), null, 2)),
|
|
114
|
+
"focus-check": async () => console.log(JSON.stringify(await judgment.focusTask(
|
|
115
|
+
root,
|
|
116
|
+
required(rest[0], "project"),
|
|
117
|
+
required(rest[1], "task id"),
|
|
118
|
+
required(words(rest.slice(2), "--action"), "--action"),
|
|
119
|
+
), null, 2)),
|
|
120
|
+
"behavioral-review": async () => {
|
|
121
|
+
const [subcommand, projectName, taskId, ...tail] = rest;
|
|
122
|
+
if (subcommand === "launch") return reviews.launchBehavioralReview(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
123
|
+
if (subcommand === "record") return reviews.recordBehavioralReview(root, required(projectName, "project"), required(taskId, "task id"), tail);
|
|
124
|
+
if (subcommand === "open") return reviews.openBehavioralReview(root, required(projectName, "project"), required(taskId, "task id"));
|
|
125
|
+
throw new Error("Use `atdd-flow behavioral-review launch|record|open <project> <task-id>`.");
|
|
126
|
+
},
|
|
127
|
+
open: () => openSeat(root, required(rest[0], "address")),
|
|
128
|
+
};
|
|
129
|
+
const action = commands[command];
|
|
130
|
+
if (!action) throw new Error(`Unknown command: ${command}`);
|
|
131
|
+
await action();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main().catch((error) => { console.error(`atdd-flow: ${(error as Error).message}`); process.exit(1); });
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export type Role = { address: string; branch: string; agent?: string; purpose?: string; worktree?: string; base?: string };
|
|
7
|
+
export type Group = { role?: string; members?: string[] };
|
|
8
|
+
export type Scope = { purpose: string; coordinator: string; umbrella_branch?: string; legacy_aliases?: string[] };
|
|
9
|
+
/**
|
|
10
|
+
* The Desk is the durable registry for a workflow: projects, seats, tasks,
|
|
11
|
+
* threads, and their handoff records all resolve from this root.
|
|
12
|
+
*/
|
|
13
|
+
export type Desk = {
|
|
14
|
+
schema: "atdd-workflow/desk/v1";
|
|
15
|
+
desk: string;
|
|
16
|
+
application: string;
|
|
17
|
+
/** Named commands that model portfolio entries may resolve through. */
|
|
18
|
+
executables?: Record<string, string>;
|
|
19
|
+
aliases?: Record<string, string>;
|
|
20
|
+
};
|
|
21
|
+
export type ModelCandidate = {
|
|
22
|
+
id: string;
|
|
23
|
+
executable: string;
|
|
24
|
+
args?: string[];
|
|
25
|
+
description?: string;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type ModelPortfolio = {
|
|
29
|
+
schema: "atdd-workflow/models/v1";
|
|
30
|
+
/** Ordered strongest to weakest. Workflow prefers the weakest sufficient enabled candidate. */
|
|
31
|
+
models: ModelCandidate[];
|
|
32
|
+
};
|
|
33
|
+
type LegacyCoordination = { schema: "atdd-workflow/coordination/v2"; site: string; application: string; aliases?: Record<string, string> };
|
|
34
|
+
export type Project = {
|
|
35
|
+
schema: string;
|
|
36
|
+
project: string;
|
|
37
|
+
repository?: string;
|
|
38
|
+
worktree_root?: string;
|
|
39
|
+
roles: Record<string, Role>;
|
|
40
|
+
groups?: Record<string, Group>;
|
|
41
|
+
scopes?: Record<string, Scope>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A seat can be reachable through more than one live application. Addresses
|
|
45
|
+
* are opaque application-owned locators: Workflow records and returns them,
|
|
46
|
+
* while the relevant bridge is responsible for using their native format.
|
|
47
|
+
*/
|
|
48
|
+
export type Runtime = { application: string; addresses: Record<string, string>; attached_at?: string; model?: string };
|
|
49
|
+
export type Seat = {
|
|
50
|
+
schema: string;
|
|
51
|
+
address: string;
|
|
52
|
+
role: string;
|
|
53
|
+
project: string;
|
|
54
|
+
worktree: string;
|
|
55
|
+
branch: string;
|
|
56
|
+
/** Legacy pinned launch executable. New seats leave model choice to Jev through models.yaml. */
|
|
57
|
+
agent?: string;
|
|
58
|
+
purpose?: string;
|
|
59
|
+
runtime?: Runtime;
|
|
60
|
+
retired?: { task: string; completed_at: string; summary: string };
|
|
61
|
+
};
|
|
62
|
+
export type Checkpoint = {
|
|
63
|
+
schema: string;
|
|
64
|
+
seat: string;
|
|
65
|
+
status: "active" | "standby" | "blocked" | "complete" | "unverified";
|
|
66
|
+
updated_at: string;
|
|
67
|
+
summary: string;
|
|
68
|
+
next_action: string;
|
|
69
|
+
references?: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const now = () => new Date().toISOString();
|
|
73
|
+
export const id = (prefix: "T" | "M" | "R") => `${prefix}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
74
|
+
export const words = (args: string[], flag: string) => {
|
|
75
|
+
const index = args.indexOf(flag);
|
|
76
|
+
return index < 0 ? undefined : args[index + 1];
|
|
77
|
+
};
|
|
78
|
+
export const values = (args: string[], flag: string) => args.flatMap((value, index) => value === flag && args[index + 1] ? [args[index + 1]] : []);
|
|
79
|
+
export const has = (args: string[], flag: string) => args.includes(flag);
|
|
80
|
+
export const required = <T>(value: T | undefined, label: string) => {
|
|
81
|
+
if (value === undefined || value === "") throw new Error(`Missing ${label}.`);
|
|
82
|
+
return value;
|
|
83
|
+
};
|
|
84
|
+
export const yaml = {
|
|
85
|
+
parse: <T>(text: string) => Bun.YAML.parse(text) as T,
|
|
86
|
+
print: (value: unknown) => Bun.YAML.stringify(value),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function addressParts(address: string) {
|
|
90
|
+
const marker = address.lastIndexOf("@");
|
|
91
|
+
if (marker < 1 || marker === address.length - 1) throw new Error(`Address must use local@project form: ${address}`);
|
|
92
|
+
return { local: address.slice(0, marker), project: address.slice(marker + 1) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function taskId(value: string) {
|
|
96
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) throw new Error(`Task id must contain only letters, numbers, dots, underscores, or hyphens: ${value}`);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const paths = (root: string) => ({
|
|
101
|
+
desk: join(root, "desk.yaml"),
|
|
102
|
+
models: join(root, "models.yaml"),
|
|
103
|
+
legacyCoordination: join(root, "coordination.yaml"),
|
|
104
|
+
work: join(root, "work"),
|
|
105
|
+
project: (name: string) => join(root, "work", name),
|
|
106
|
+
projectFile: (name: string) => join(root, "work", name, "project.yaml"),
|
|
107
|
+
seats: (name: string) => join(root, "work", name, "seats"),
|
|
108
|
+
seat: (address: string) => {
|
|
109
|
+
const entry = addressParts(address);
|
|
110
|
+
return join(root, "work", entry.project, "seats", entry.local);
|
|
111
|
+
},
|
|
112
|
+
seatFile: (address: string) => join(paths(root).seat(address), "seat.yaml"),
|
|
113
|
+
checkpointFile: (address: string) => join(paths(root).seat(address), "checkpoint.yaml"),
|
|
114
|
+
tasks: (project: string) => join(root, "work", project, "tasks"),
|
|
115
|
+
taskFile: (project: string, task: string) => join(root, "work", project, "tasks", `${taskId(task)}.yaml`),
|
|
116
|
+
behavioralReviewFile: (project: string, task: string) => join(root, "work", project, "tasks", `${taskId(task)}.reviews.yaml`),
|
|
117
|
+
threads: join(root, "threads"),
|
|
118
|
+
thread: (threadId: string) => join(root, "threads", threadId),
|
|
119
|
+
threadFile: (threadId: string) => join(root, "threads", threadId, "thread.yaml"),
|
|
120
|
+
message: (threadId: string, messageId: string) => join(root, "threads", threadId, `${messageId}.yaml`),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export async function atomicYaml(file: string, value: unknown) {
|
|
124
|
+
await mkdir(dirname(file), { recursive: true });
|
|
125
|
+
const temp = join(dirname(file), `.${basename(file)}.${randomUUID()}.tmp`);
|
|
126
|
+
await writeFile(temp, yaml.print(value), "utf8");
|
|
127
|
+
await rename(temp, file);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function readYaml<T>(file: string): Promise<T> {
|
|
131
|
+
return yaml.parse<T>(await readFile(file, "utf8"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function exists(path: string) {
|
|
135
|
+
try { await stat(path); return true; }
|
|
136
|
+
catch { return false; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function desk(root: string): Promise<Desk> {
|
|
140
|
+
const files = paths(root);
|
|
141
|
+
if (await exists(files.desk)) {
|
|
142
|
+
const value = await readYaml<Desk>(files.desk);
|
|
143
|
+
if (value.schema !== "atdd-workflow/desk/v1") throw new Error("Unsupported Desk schema.");
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
if (await exists(files.legacyCoordination)) {
|
|
147
|
+
const legacy = await readYaml<LegacyCoordination>(files.legacyCoordination);
|
|
148
|
+
if (legacy.schema !== "atdd-workflow/coordination/v2") throw new Error("Unsupported legacy coordination schema.");
|
|
149
|
+
return { schema: "atdd-workflow/desk/v1", desk: legacy.site, application: legacy.application, ...(legacy.aliases ? { aliases: legacy.aliases } : {}) };
|
|
150
|
+
}
|
|
151
|
+
throw new Error("Desk registry not found. Expected desk.yaml (or legacy coordination.yaml).");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function migrateDesk(root: string) {
|
|
155
|
+
const files = paths(root);
|
|
156
|
+
if (await exists(files.desk)) return false;
|
|
157
|
+
const record = await desk(root);
|
|
158
|
+
await atomicYaml(files.desk, record);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function modelPortfolio(root: string): Promise<ModelPortfolio | undefined> {
|
|
163
|
+
const file = paths(root).models;
|
|
164
|
+
if (!await exists(file)) return undefined;
|
|
165
|
+
const value = await readYaml<ModelPortfolio>(file);
|
|
166
|
+
if (value.schema !== "atdd-workflow/models/v1") throw new Error("Unsupported model portfolio schema.");
|
|
167
|
+
if (!Array.isArray(value.models) || !value.models.length) throw new Error("models.yaml must declare at least one model.");
|
|
168
|
+
const ids = new Set<string>();
|
|
169
|
+
for (const model of value.models) {
|
|
170
|
+
if (!model.id?.trim() || !model.executable?.trim()) throw new Error("Every model requires id and executable.");
|
|
171
|
+
if (ids.has(model.id)) throw new Error(`Duplicate model id in models.yaml: ${model.id}`);
|
|
172
|
+
ids.add(model.id);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function project(root: string, name: string) {
|
|
178
|
+
const value = await readYaml<Project>(paths(root).projectFile(name));
|
|
179
|
+
if (value.schema !== "atdd-workflow/project/v1") throw new Error("Unsupported project schema.");
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function canonicalAddress(root: string, address: string) {
|
|
184
|
+
const aliases = (await desk(root)).aliases ?? {};
|
|
185
|
+
const visited = new Set<string>();
|
|
186
|
+
let resolved = address;
|
|
187
|
+
while (aliases[resolved]) {
|
|
188
|
+
if (visited.has(resolved)) throw new Error(`Address alias cycle: ${address}`);
|
|
189
|
+
visited.add(resolved);
|
|
190
|
+
resolved = aliases[resolved];
|
|
191
|
+
}
|
|
192
|
+
addressParts(resolved);
|
|
193
|
+
return resolved;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function seat(root: string, address: string) {
|
|
197
|
+
return readYaml<Seat>(paths(root).seatFile(await canonicalAddress(root, address)));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function fill(template: string, entries: Record<string, string>) {
|
|
201
|
+
return template.replace(/\{(project|name|worktree_root|repository)\}/g, (_, key) => entries[key]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function executable(command: string, cwd?: string) {
|
|
205
|
+
if (!cwd || command.includes("/")) return command;
|
|
206
|
+
const local = join(cwd, "node_modules", ".bin", command);
|
|
207
|
+
return existsSync(local) ? local : command;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function execute(command: string[], cwd?: string) {
|
|
211
|
+
const result = Bun.spawn([executable(command[0]!, cwd), ...command.slice(1)], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
212
|
+
const [stdout, stderr, code] = await Promise.all([new Response(result.stdout).text(), new Response(result.stderr).text(), result.exited]);
|
|
213
|
+
if (code !== 0) throw new Error(`${command[0]} failed: ${stderr.trim() || stdout.trim()}`);
|
|
214
|
+
return stdout;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function runOutput(command: string[], cwd?: string) {
|
|
218
|
+
return (await execute(command, cwd)).trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function run(command: string[], quiet = false, cwd?: string) {
|
|
222
|
+
const stdout = await execute(command, cwd);
|
|
223
|
+
if (!quiet && stdout.trim()) process.stdout.write(stdout);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const rootFromCwd = () => resolve(process.cwd());
|